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' => 'Logo
', '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' => '
Logo
', + '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" },