diff --git a/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php b/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php
index 5f66a99..1662f5c 100644
--- a/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php
+++ b/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php
@@ -41,7 +41,7 @@ public function referenceReports(array $files, ?callable $isLinkableDocument = n
if ( is_array($target) && ! $this->isLinkableDocument($target, $isLinkableDocument) ) {
unset($reference['target']);
$assetReferences[] = $reference;
- if ( 'img' === $candidate['element'] && 'src' === $candidate['attribute'] ) {
+ if ( str_starts_with((string) ($target['mime_type'] ?? ''), 'image/') ) {
$imageReferences[] = $this->legacyImageReference($reference, count($imageReferences));
}
}
@@ -59,6 +59,9 @@ public function referenceReports(array $files, ?callable $isLinkableDocument = n
if ( is_array($target) && ! $this->isLinkableDocument($target, $isLinkableDocument) ) {
unset($reference['target']);
$assetReferences[] = $reference;
+ if ( str_starts_with((string) ($target['mime_type'] ?? ''), 'image/') ) {
+ $imageReferences[] = $this->legacyImageReference($reference, count($imageReferences));
+ }
}
}
}
@@ -76,7 +79,7 @@ public function referenceReports(array $files, ?callable $isLinkableDocument = n
*/
public function htmlReferenceCandidates(string $html, string $sourcePath): array
{
- if ( '' === trim($html) || ! preg_match_all('/<\s*(a|audio|img|script|link|source|video)\b([^>]*)>/i', $html, $matches, PREG_SET_ORDER) ) {
+ if ( '' === trim($html) || ! preg_match_all('/<\s*([a-z][a-z0-9:-]*)\b([^>]*)>/i', $html, $matches, PREG_SET_ORDER) ) {
return array();
}
@@ -101,6 +104,21 @@ public function htmlReferenceCandidates(string $html, string $sourcePath): array
);
}
}
+
+ if ( isset($attributes['style']) ) {
+ $value = (string) $attributes['style'];
+ foreach ( $this->cssReferenceCandidates($value, $sourcePath) as $styleCandidate ) {
+ $candidates[] = array(
+ 'source_path' => $sourcePath,
+ 'selector' => $selector,
+ 'element' => $element,
+ 'attribute' => 'style',
+ 'value' => $value,
+ 'url' => $styleCandidate['url'],
+ 'context' => 'inline-style',
+ );
+ }
+ }
}
return $candidates;
@@ -229,6 +247,7 @@ private function referenceAttributesForElement(string $element, array $attribute
'link' => array('href'),
'source' => array('src', 'srcset'),
'video' => array('src', 'poster'),
+ 'image' => array('href', 'xlink:href'),
);
return array_values(array_filter(
@@ -316,13 +335,16 @@ private function legacyImageReference(array $reference, int $index): array
return array_filter(
array(
'source_path' => $reference['source_path'] ?? '',
- 'selector' => 'img:nth-of-type(' . ($index + 1) . ')',
+ 'selector' => $reference['selector'] ?? 'image-reference:nth-of-type(' . ($index + 1) . ')',
'src' => $reference['url'] ?? '',
'resolved_path' => $reference['resolved_path'] ?? '',
'asset_path' => $reference['asset_path'] ?? '',
'mime_type' => $reference['mime_type'] ?? '',
'bytes' => $reference['bytes'] ?? 0,
'safe' => $reference['safe'] ?? null,
+ 'element' => $reference['element'] ?? '',
+ 'attribute' => $reference['attribute'] ?? '',
+ 'context' => $reference['context'] ?? '',
),
static fn (mixed $value): bool => null !== $value && '' !== $value
);
diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php
index 8566230..1779758 100644
--- a/php-transformer/tests/contract/run.php
+++ b/php-transformer/tests/contract/run.php
@@ -83,6 +83,27 @@ function serialize_blocks(array $blocks): string
$assert('theme/fonts/fonts.css' === ($referenceReports['asset_references'][1]['asset_path'] ?? ''), 'reference analyzer assembles CSS @import asset reference reports');
$assert('assets/paper.png' === ($referenceReports['asset_references'][2]['asset_path'] ?? ''), 'reference analyzer resolves CSS url() reports relative to source CSS');
$assert('theme/FixtureSans.woff2' === ($referenceReports['asset_references'][3]['asset_path'] ?? ''), 'reference analyzer assembles @font-face local font reference reports');
+$assert(2 === count($referenceReports['image_references']), 'reference analyzer projects HTML and CSS image asset references');
+$assert('assets/paper.png' === ($referenceReports['image_references'][1]['asset_path'] ?? ''), 'reference analyzer projects CSS background images into image references');
+$assert('css-url' === ($referenceReports['image_references'][1]['context'] ?? ''), 'reference analyzer preserves CSS background image context');
+
+$imageReferenceReports = $referenceAnalyzer->referenceReports(array(
+ array('path' => 'pages/index.html', 'kind' => 'html', 'content' => '
', 'binary' => false),
+ array('path' => 'assets/hero-small.png', 'kind' => 'image', 'content_base64' => base64_encode('small'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 5),
+ array('path' => 'assets/hero-large.png', 'kind' => 'image', 'content_base64' => base64_encode('large'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 5),
+ array('path' => 'assets/logo.png', 'kind' => 'image', 'content_base64' => base64_encode('logo'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 4),
+ array('path' => 'assets/logo@2x.png', 'kind' => 'image', 'content_base64' => base64_encode('retina'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 6),
+ array('path' => 'assets/panel.png', 'kind' => 'image', 'content_base64' => base64_encode('panel'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 5),
+ array('path' => 'assets/vector.png', 'kind' => 'image', 'content_base64' => base64_encode('vector'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 6),
+));
+$assert(6 === count($imageReferenceReports['image_references']), 'image reference analysis reports src, srcset, inline background, picture source, and SVG image href references');
+$assert('source' === ($imageReferenceReports['image_references'][0]['element'] ?? ''), 'image reference analysis reports picture source elements');
+$assert('srcset' === ($imageReferenceReports['image_references'][0]['attribute'] ?? ''), 'image reference analysis preserves srcset attributes');
+$assert('assets/hero-small.png' === ($imageReferenceReports['image_references'][0]['asset_path'] ?? ''), 'image reference analysis resolves source srcset paths relative to the HTML document');
+$assert('inline-style' === ($imageReferenceReports['image_references'][4]['context'] ?? ''), 'image reference analysis reports inline CSS background image references');
+$assert('assets/panel.png' === ($imageReferenceReports['image_references'][4]['asset_path'] ?? ''), 'image reference analysis resolves inline style image paths relative to the HTML document');
+$assert('image' === ($imageReferenceReports['image_references'][5]['element'] ?? ''), 'image reference analysis reports SVG image href elements');
+$assert('assets/vector.png' === ($imageReferenceReports['image_references'][5]['asset_path'] ?? ''), 'image reference analysis resolves SVG image href paths relative to the HTML document');
$assertNormalizedFallbackDiagnostic = static function (array $diagnostic, string $code, string $severity, string $runtimeRequirement, string $suggestedPrimitive) use ($assert): void {
$assert($code === ($diagnostic['diagnostic_code'] ?? ''), "conversion report exposes {$code} diagnostic code");
@@ -805,6 +826,27 @@ function serialize_blocks(array $blocks): string
$assert('css-font-face' === ($fontCompiledAsset['references'][0]['context'] ?? ''), 'compiled site assets expose structured reference metadata');
$assert('css-font-face' === ($fontPlanAsset['references'][0]['context'] ?? ''), 'materialization plan assets preserve structured reference metadata');
+$imageReferenceSite = $compiler->compile(
+ array(
+ 'entrypoint' => 'pages/index.html',
+ 'files' => array(
+ 'pages/index.html' => '
',
+ 'assets/hero-small.png' => array('content_base64' => base64_encode('small'), 'mime_type' => 'image/png'),
+ 'assets/hero-large.png' => array('content_base64' => base64_encode('large'), 'mime_type' => 'image/png'),
+ 'assets/logo.png' => array('content_base64' => base64_encode('logo'), 'mime_type' => 'image/png'),
+ 'assets/panel.png' => array('content_base64' => base64_encode('panel'), 'mime_type' => 'image/png'),
+ 'assets/vector.png' => array('content_base64' => base64_encode('vector'), 'mime_type' => 'image/png'),
+ ),
+ )
+)->toArray();
+$imageReferencePlanAssets = array();
+foreach ( $imageReferenceSite['source_reports']['materialization_plan']['assets'] ?? array() as $asset ) {
+ $imageReferencePlanAssets[$asset['path'] ?? ''] = $asset;
+}
+$assert('source' === ($imageReferencePlanAssets['assets/hero-small.png']['references'][0]['element'] ?? ''), 'materialization plan image rows preserve picture source references');
+$assert('inline-style' === ($imageReferencePlanAssets['assets/panel.png']['references'][0]['context'] ?? ''), 'materialization plan image rows preserve inline background references');
+$assert('image' === ($imageReferencePlanAssets['assets/vector.png']['references'][0]['element'] ?? ''), 'materialization plan image rows preserve SVG image href references');
+
$materializationView = ( new MaterializationView() )->fromResult($staticSite);
$assert(MaterializationView::SCHEMA === ($materializationView['schema'] ?? ''), 'materialization view exposes its own schema');
$assert(TransformerResult::SCHEMA === ($materializationView['result_schema'] ?? ''), 'materialization view exposes transformer result schema');
diff --git a/php-transformer/tests/fixtures/parity/artifact-local-link-asset-reports.json b/php-transformer/tests/fixtures/parity/artifact-local-link-asset-reports.json
index 893236f..f47c002 100644
--- a/php-transformer/tests/fixtures/parity/artifact-local-link-asset-reports.json
+++ b/php-transformer/tests/fixtures/parity/artifact-local-link-asset-reports.json
@@ -79,8 +79,12 @@
{ "path": "source_reports.artifact.asset_references.5.asset_path", "assert": "equals", "value": "assets/site.css" },
{ "path": "source_reports.artifact.asset_references.6.element", "assert": "equals", "value": "style" },
{ "path": "source_reports.artifact.asset_references.6.asset_path", "assert": "equals", "value": "assets/bg.png" },
- { "path": "source_reports.artifact.image_references", "assert": "count", "count": 1 },
+ { "path": "source_reports.artifact.image_references", "assert": "count", "count": 4 },
{ "path": "source_reports.artifact.image_references.0.asset_path", "assert": "equals", "value": "assets/hero.png" },
+ { "path": "source_reports.artifact.image_references.1.asset_path", "assert": "equals", "value": "assets/hero-small.png" },
+ { "path": "source_reports.artifact.image_references.1.attribute", "assert": "equals", "value": "srcset" },
+ { "path": "source_reports.artifact.image_references.3.asset_path", "assert": "equals", "value": "assets/bg.png" },
+ { "path": "source_reports.artifact.image_references.3.context", "assert": "equals", "value": "css-url" },
{ "path": "source_reports.conversion_report.schema", "assert": "equals", "value": "blocks-engine/php-transformer/conversion-report/v1" },
{ "path": "source_reports.conversion_report.source_summary.entry_path", "assert": "equals", "value": "pages/home.html" },
{ "path": "source_reports.conversion_report.source_summary.file_count", "assert": "equals", "value": 8 },
diff --git a/php-transformer/tests/fixtures/parity/artifact-responsive-image-assets.json b/php-transformer/tests/fixtures/parity/artifact-responsive-image-assets.json
index 3f16641..04d2479 100644
--- a/php-transformer/tests/fixtures/parity/artifact-responsive-image-assets.json
+++ b/php-transformer/tests/fixtures/parity/artifact-responsive-image-assets.json
@@ -42,8 +42,10 @@
"expect": [
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "source_reports.artifact.asset_references", "assert": "count", "count": 2 },
- { "path": "source_reports.artifact.image_references", "assert": "count", "count": 1 },
+ { "path": "source_reports.artifact.image_references", "assert": "count", "count": 2 },
{ "path": "source_reports.artifact.image_references.0.resolved_path", "assert": "equals", "value": "assets/images/hero.png" },
+ { "path": "source_reports.artifact.image_references.1.attribute", "assert": "equals", "value": "srcset" },
+ { "path": "source_reports.artifact.image_references.1.resolved_path", "assert": "equals", "value": "assets/images/hero-small.png" },
{ "path": "source_reports.materialization_plan.assets", "assert": "count", "count": 2 },
{ "path": "source_reports.materialization_plan.assets.0.path", "assert": "equals", "value": "assets/images/hero.png" },
{ "path": "source_reports.materialization_plan.assets.1.path", "assert": "equals", "value": "assets/images/hero-small.png" },