diff --git a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php index 5710120..b4ac9e7 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php @@ -3,6 +3,7 @@ namespace Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler; +use Automattic\BlocksEngine\PhpTransformer\AssetAnalysis\ReferenceAnalyzer; use Automattic\BlocksEngine\PhpTransformer\Contract\ConversionReportProjection; use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult; use Automattic\BlocksEngine\PhpTransformer\FormatBridge\FormatBridge; @@ -183,256 +184,11 @@ private function safeEntryImageHtml(string $html, string $entryPath, array $file */ private function referenceReports(array $files): array { - $internalLinks = array(); - $assetReferences = array(); - $imageReferences = array(); - - foreach ( $files as $file ) { - if ( ! empty($file['binary']) ) { - continue; - } - - if ( 'html' === ($file['kind'] ?? '') || 'blocks' === ($file['kind'] ?? '') ) { - foreach ( $this->htmlReferenceCandidates((string) ($file['content'] ?? ''), (string) ($file['path'] ?? '')) as $candidate ) { - if ( '' === $candidate['url'] || ! $this->isArtifactLocalReference($candidate['url']) ) { - continue; - } - - $reference = $this->normalizeReferenceCandidate($candidate, $files); - $target = $reference['target'] ?? null; - if ( is_array($target) && $this->isLinkableDocument($target) && 'a' === $candidate['element'] ) { - unset($reference['target']); - $internalLinks[] = $reference; - continue; - } - - if ( is_array($target) && ! $this->isLinkableDocument($target) ) { - unset($reference['target']); - $assetReferences[] = $reference; - if ( 'img' === $candidate['element'] && 'src' === $candidate['attribute'] ) { - $imageReferences[] = $this->legacyImageReference($reference, count($imageReferences)); - } - } - } - } - - if ( 'css' === ($file['kind'] ?? '') ) { - foreach ( $this->cssReferenceCandidates((string) ($file['content'] ?? ''), (string) ($file['path'] ?? '')) as $candidate ) { - if ( '' === $candidate['url'] || ! $this->isArtifactLocalReference($candidate['url']) ) { - continue; - } - - $reference = $this->normalizeReferenceCandidate($candidate, $files); - $target = $reference['target'] ?? null; - if ( is_array($target) && ! $this->isLinkableDocument($target) ) { - unset($reference['target']); - $assetReferences[] = $reference; - } - } - } - } - - return array( - 'internal_links' => $internalLinks, - 'asset_references' => $assetReferences, - 'image_references' => $imageReferences, - ); - } - - /** - * @return array - */ - private 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) ) { - return array(); - } - - $candidates = array(); - $counts = array(); - foreach ( $matches as $match ) { - $element = strtolower((string) $match[1]); - $attributes = $this->htmlAttributes((string) $match[2]); - $counts[$element] = ($counts[$element] ?? 0) + 1; - $selector = $element . ':nth-of-type(' . $counts[$element] . ')'; - - foreach ( $this->referenceAttributesForElement($element, $attributes) as $attribute ) { - $value = (string) ($attributes[$attribute] ?? ''); - foreach ( $this->urlsFromAttributeValue($attribute, $value) as $url ) { - $candidates[] = array( - 'source_path' => $sourcePath, - 'selector' => $selector, - 'element' => $element, - 'attribute' => $attribute, - 'value' => $value, - 'url' => $url, - ); - } - } - } - - return $candidates; - } - - /** - * @return array - */ - private function htmlAttributes(string $attributeText): array - { - $attributes = array(); - if ( ! preg_match_all('/([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:(["\'])(.*?)\2|([^\s"\'>]+))/s', $attributeText, $matches, PREG_SET_ORDER) ) { - return $attributes; - } - - foreach ( $matches as $match ) { - $attributes[strtolower((string) $match[1])] = html_entity_decode((string) ('' !== ($match[3] ?? '') ? $match[3] : ($match[4] ?? '')), ENT_QUOTES | ENT_HTML5); - } - - return $attributes; - } - - /** - * @param array $attributes - * @return array - */ - private function referenceAttributesForElement(string $element, array $attributes): array - { - $attributesByElement = array( - 'a' => array('href'), - 'audio' => array('src'), - 'img' => array('src', 'srcset'), - 'script' => array('src'), - 'link' => array('href'), - 'source' => array('src', 'srcset'), - 'video' => array('src', 'poster'), - ); - - return array_values(array_filter( - $attributesByElement[$element] ?? array(), - static fn (string $attribute): bool => isset($attributes[$attribute]) - )); - } - - /** - * @return array - */ - private function urlsFromAttributeValue(string $attribute, string $value): array - { - if ( 'srcset' !== $attribute ) { - return array(trim($value)); - } - - $urls = array(); - foreach ( explode(',', $value) as $candidate ) { - $parts = preg_split('/\s+/', trim($candidate)); - if ( is_array($parts) && '' !== ($parts[0] ?? '') ) { - $urls[] = $parts[0]; - } - } - - return $urls; - } - - /** - * @return array - */ - private function cssReferenceCandidates(string $css, string $sourcePath): array - { - if ( '' === trim($css) ) { - return array(); - } - - $candidates = array(); - - if ( preg_match_all('/@import\s+(?:url\(\s*)?(["\']?)([^"\'\)\s;]+)\1\s*\)?[^;]*;/i', $css, $matches, PREG_SET_ORDER) ) { - foreach ( $matches as $index => $match ) { - $url = html_entity_decode(trim((string) $match[2]), ENT_QUOTES | ENT_HTML5); - $candidates[] = array( - 'source_path' => $sourcePath, - 'selector' => 'css:@import(' . ($index + 1) . ')', - 'element' => 'style', - 'attribute' => '@import', - 'value' => $url, - 'url' => $url, - 'context' => 'css-import', - ); - } - } - - if ( ! preg_match_all('/url\(\s*(["\']?)([^"\')]+)\1\s*\)/i', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE) ) { - return $candidates; - } - - foreach ( $matches as $index => $match ) { - $url = html_entity_decode(trim((string) $match[2][0]), ENT_QUOTES | ENT_HTML5); - $ruleContext = $this->cssRuleContext($css, (int) $match[0][1]); - $candidates[] = array( - 'source_path' => $sourcePath, - 'selector' => ('font-face' === $ruleContext ? 'css:@font-face:url(' : 'css:url(') . ($index + 1) . ')', - 'element' => 'style', - 'attribute' => 'url', - 'value' => $url, - 'url' => $url, - 'context' => 'font-face' === $ruleContext ? 'css-font-face' : 'css-url', - ); - } - - return $candidates; - } - - private function cssRuleContext(string $css, int $offset): string - { - $before = substr($css, 0, $offset); - $ruleStart = strrpos($before, '{'); - if ( false === $ruleStart ) { - return ''; - } - - $prefix = substr($css, max(0, $ruleStart - 256), $ruleStart - max(0, $ruleStart - 256)); - return preg_match('/@font-face\s*$/i', $prefix) ? 'font-face' : ''; - } - - /** - * @param array{source_path: string, selector: string, element: string, attribute: string, value: string, url: string} $candidate - * @param array> $files - * @return array - */ - private function normalizeReferenceCandidate(array $candidate, array $files): array - { - $resolvedPath = $this->resolveHtmlReferencePath($candidate['url'], $candidate['source_path']); - $target = '' === $resolvedPath ? null : $this->findFileByPath($resolvedPath, $files); - $reference = array_filter( - array( - 'source_path' => $candidate['source_path'], - 'selector' => $candidate['selector'], - 'element' => $candidate['element'], - 'attribute' => $candidate['attribute'], - 'value' => $candidate['value'], - 'url' => $candidate['url'], - 'context' => $candidate['context'] ?? '', - 'resolved_path' => $resolvedPath, - ), - static fn (mixed $value): bool => '' !== $value + return ( new ReferenceAnalyzer() )->referenceReports( + $files, + fn (array $file): bool => $this->isLinkableDocument($file), + fn (array $asset): bool => $this->isSafeImageAsset($asset) ); - - if ( is_array($target) ) { - $targetPath = (string) ($target['path'] ?? ''); - if ( $this->isLinkableDocument($target) ) { - $reference['target_path'] = $targetPath; - } else { - $reference['asset_path'] = $targetPath; - } - $reference['kind'] = $target['kind'] ?? ''; - $reference['role'] = $target['role'] ?? ''; - $reference['mime_type'] = $target['mime_type'] ?? ''; - $reference['bytes'] = $target['bytes'] ?? 0; - if ( str_starts_with((string) ($target['mime_type'] ?? ''), 'image/') ) { - $reference['safe'] = $this->isSafeImageAsset($target); - } - $reference['target'] = $target; - } - - return $reference; } /** @@ -443,52 +199,6 @@ private function isLinkableDocument(array $file): bool return in_array($file['kind'] ?? '', array('html', 'blocks'), true) && ! $this->isTemplatePartFile($file); } - /** - * @param array> $files - * @return array|null - */ - private function findFileByPath(string $path, array $files): ?array - { - foreach ( $files as $file ) { - if ( $path === ($file['path'] ?? '') ) { - return $file; - } - } - - return null; - } - - private function isArtifactLocalReference(string $reference): bool - { - $reference = trim($reference); - if ( '' === $reference || str_starts_with($reference, '#') || str_starts_with($reference, '//') ) { - return false; - } - - return ! preg_match('#^[a-z][a-z0-9+.-]*:#i', $reference); - } - - /** - * @param array $reference - * @return array - */ - private function legacyImageReference(array $reference, int $index): array - { - return array_filter( - array( - 'source_path' => $reference['source_path'] ?? '', - 'selector' => 'img: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, - ), - static fn (mixed $value): bool => null !== $value && '' !== $value - ); - } - /** * @param array{files: array>, bytes: int, hash_payload: string} $artifact * @param array> $documents diff --git a/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php b/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php new file mode 100644 index 0000000..5f66a99 --- /dev/null +++ b/php-transformer/src/AssetAnalysis/ReferenceAnalyzer.php @@ -0,0 +1,330 @@ +> $files + * @param callable(array): bool|null $isLinkableDocument + * @param callable(array): bool|null $isSafeImageAsset + * @return array{internal_links: array>, asset_references: array>, image_references: array>} + */ + public function referenceReports(array $files, ?callable $isLinkableDocument = null, ?callable $isSafeImageAsset = null): array + { + $internalLinks = array(); + $assetReferences = array(); + $imageReferences = array(); + + foreach ( $files as $file ) { + if ( ! empty($file['binary']) ) { + continue; + } + + if ( 'html' === ($file['kind'] ?? '') || 'blocks' === ($file['kind'] ?? '') ) { + foreach ( $this->htmlReferenceCandidates((string) ($file['content'] ?? ''), (string) ($file['path'] ?? '')) as $candidate ) { + if ( '' === $candidate['url'] || ! $this->isLocalReference($candidate['url']) ) { + continue; + } + + $reference = $this->normalizeReferenceCandidate($candidate, $files, $isLinkableDocument, $isSafeImageAsset); + $target = $reference['target'] ?? null; + if ( is_array($target) && $this->isLinkableDocument($target, $isLinkableDocument) && 'a' === $candidate['element'] ) { + unset($reference['target']); + $internalLinks[] = $reference; + continue; + } + + if ( is_array($target) && ! $this->isLinkableDocument($target, $isLinkableDocument) ) { + unset($reference['target']); + $assetReferences[] = $reference; + if ( 'img' === $candidate['element'] && 'src' === $candidate['attribute'] ) { + $imageReferences[] = $this->legacyImageReference($reference, count($imageReferences)); + } + } + } + } + + if ( 'css' === ($file['kind'] ?? '') ) { + foreach ( $this->cssReferenceCandidates((string) ($file['content'] ?? ''), (string) ($file['path'] ?? '')) as $candidate ) { + if ( '' === $candidate['url'] || ! $this->isLocalReference($candidate['url']) ) { + continue; + } + + $reference = $this->normalizeReferenceCandidate($candidate, $files, $isLinkableDocument, $isSafeImageAsset); + $target = $reference['target'] ?? null; + if ( is_array($target) && ! $this->isLinkableDocument($target, $isLinkableDocument) ) { + unset($reference['target']); + $assetReferences[] = $reference; + } + } + } + } + + return array( + 'internal_links' => $internalLinks, + 'asset_references' => $assetReferences, + 'image_references' => $imageReferences, + ); + } + + /** + * @return array + */ + 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) ) { + return array(); + } + + $candidates = array(); + $counts = array(); + foreach ( $matches as $match ) { + $element = strtolower((string) $match[1]); + $attributes = $this->htmlAttributes((string) $match[2]); + $counts[$element] = ($counts[$element] ?? 0) + 1; + $selector = $element . ':nth-of-type(' . $counts[$element] . ')'; + + foreach ( $this->referenceAttributesForElement($element, $attributes) as $attribute ) { + $value = (string) ($attributes[$attribute] ?? ''); + foreach ( $this->urlsFromAttributeValue($attribute, $value) as $url ) { + $candidates[] = array( + 'source_path' => $sourcePath, + 'selector' => $selector, + 'element' => $element, + 'attribute' => $attribute, + 'value' => $value, + 'url' => $url, + ); + } + } + } + + return $candidates; + } + + /** + * @return array + */ + public function cssReferenceCandidates(string $css, string $sourcePath): array + { + if ( '' === trim($css) ) { + return array(); + } + + $candidates = array(); + + if ( preg_match_all('/@import\s+(?:url\(\s*)?(["\']?)([^"\'\)\s;]+)\1\s*\)?[^;]*;/i', $css, $matches, PREG_SET_ORDER) ) { + foreach ( $matches as $index => $match ) { + $url = html_entity_decode(trim((string) $match[2]), ENT_QUOTES | ENT_HTML5); + $candidates[] = array( + 'source_path' => $sourcePath, + 'selector' => 'css:@import(' . ($index + 1) . ')', + 'element' => 'style', + 'attribute' => '@import', + 'value' => $url, + 'url' => $url, + 'context' => 'css-import', + ); + } + } + + if ( ! preg_match_all('/url\(\s*(["\']?)([^"\')]+)\1\s*\)/i', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE) ) { + return $candidates; + } + + foreach ( $matches as $index => $match ) { + $url = html_entity_decode(trim((string) $match[2][0]), ENT_QUOTES | ENT_HTML5); + $ruleContext = $this->cssRuleContext($css, (int) $match[0][1]); + $candidates[] = array( + 'source_path' => $sourcePath, + 'selector' => ('font-face' === $ruleContext ? 'css:@font-face:url(' : 'css:url(') . ($index + 1) . ')', + 'element' => 'style', + 'attribute' => 'url', + 'value' => $url, + 'url' => $url, + 'context' => 'font-face' === $ruleContext ? 'css-font-face' : 'css-url', + ); + } + + return $candidates; + } + + /** + * @param array{source_path: string, selector: string, element: string, attribute: string, value: string, url: string, context?: string} $candidate + * @param array> $files + * @param callable(array): bool|null $isLinkableDocument + * @param callable(array): bool|null $isSafeImageAsset + * @return array + */ + public function normalizeReferenceCandidate(array $candidate, array $files, ?callable $isLinkableDocument = null, ?callable $isSafeImageAsset = null): array + { + $resolvedPath = ArtifactPath::resolveRelativePath($candidate['url'], $candidate['source_path']); + $target = '' === $resolvedPath ? null : $this->findFileByPath($resolvedPath, $files); + $reference = array_filter( + array( + 'source_path' => $candidate['source_path'], + 'selector' => $candidate['selector'], + 'element' => $candidate['element'], + 'attribute' => $candidate['attribute'], + 'value' => $candidate['value'], + 'url' => $candidate['url'], + 'context' => $candidate['context'] ?? '', + 'resolved_path' => $resolvedPath, + ), + static fn (mixed $value): bool => '' !== $value + ); + + if ( is_array($target) ) { + $targetPath = (string) ($target['path'] ?? ''); + if ( $this->isLinkableDocument($target, $isLinkableDocument) ) { + $reference['target_path'] = $targetPath; + } else { + $reference['asset_path'] = $targetPath; + } + $reference['kind'] = $target['kind'] ?? ''; + $reference['role'] = $target['role'] ?? ''; + $reference['mime_type'] = $target['mime_type'] ?? ''; + $reference['bytes'] = $target['bytes'] ?? 0; + if ( str_starts_with((string) ($target['mime_type'] ?? ''), 'image/') ) { + $reference['safe'] = is_callable($isSafeImageAsset) ? (bool) $isSafeImageAsset($target) : true; + } + $reference['target'] = $target; + } + + return $reference; + } + + /** + * @return array + */ + private function htmlAttributes(string $attributeText): array + { + $attributes = array(); + if ( ! preg_match_all('/([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:(["\'])(.*?)\2|([^\s"\'>]+))/s', $attributeText, $matches, PREG_SET_ORDER) ) { + return $attributes; + } + + foreach ( $matches as $match ) { + $attributes[strtolower((string) $match[1])] = html_entity_decode((string) ('' !== ($match[3] ?? '') ? $match[3] : ($match[4] ?? '')), ENT_QUOTES | ENT_HTML5); + } + + return $attributes; + } + + /** + * @param array $attributes + * @return array + */ + private function referenceAttributesForElement(string $element, array $attributes): array + { + $attributesByElement = array( + 'a' => array('href'), + 'audio' => array('src'), + 'img' => array('src', 'srcset'), + 'script' => array('src'), + 'link' => array('href'), + 'source' => array('src', 'srcset'), + 'video' => array('src', 'poster'), + ); + + return array_values(array_filter( + $attributesByElement[$element] ?? array(), + static fn (string $attribute): bool => isset($attributes[$attribute]) + )); + } + + /** + * @return array + */ + private function urlsFromAttributeValue(string $attribute, string $value): array + { + if ( 'srcset' !== $attribute ) { + return array(trim($value)); + } + + $urls = array(); + foreach ( explode(',', $value) as $candidate ) { + $parts = preg_split('/\s+/', trim($candidate)); + if ( is_array($parts) && '' !== ($parts[0] ?? '') ) { + $urls[] = $parts[0]; + } + } + + return $urls; + } + + private function cssRuleContext(string $css, int $offset): string + { + $before = substr($css, 0, $offset); + $ruleStart = strrpos($before, '{'); + if ( false === $ruleStart ) { + return ''; + } + + $prefix = substr($css, max(0, $ruleStart - 256), $ruleStart - max(0, $ruleStart - 256)); + return preg_match('/@font-face\s*$/i', $prefix) ? 'font-face' : ''; + } + + /** + * @param array> $files + * @return array|null + */ + private function findFileByPath(string $path, array $files): ?array + { + foreach ( $files as $file ) { + if ( $path === ($file['path'] ?? '') ) { + return $file; + } + } + + return null; + } + + private function isLocalReference(string $reference): bool + { + $reference = trim($reference); + if ( '' === $reference || str_starts_with($reference, '#') || str_starts_with($reference, '//') ) { + return false; + } + + return ! preg_match('#^[a-z][a-z0-9+.-]*:#i', $reference); + } + + /** + * @param array $file + * @param callable(array): bool|null $isLinkableDocument + */ + private function isLinkableDocument(array $file, ?callable $isLinkableDocument): bool + { + if ( is_callable($isLinkableDocument) ) { + return (bool) $isLinkableDocument($file); + } + + return in_array($file['kind'] ?? '', array('html', 'blocks'), true); + } + + /** + * @param array $reference + * @return array + */ + private function legacyImageReference(array $reference, int $index): array + { + return array_filter( + array( + 'source_path' => $reference['source_path'] ?? '', + 'selector' => 'img: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, + ), + 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 316a851..db5165d 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -5,6 +5,7 @@ use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult; use Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler\ArtifactCompiler; +use Automattic\BlocksEngine\PhpTransformer\AssetAnalysis\ReferenceAnalyzer; use Automattic\BlocksEngine\PhpTransformer\FormatBridge\FormatAdapterInterface; use Automattic\BlocksEngine\PhpTransformer\FormatBridge\FormatBridge; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer; @@ -50,6 +51,36 @@ function serialize_blocks(array $blocks): string exit(1); }; +$referenceAnalyzer = new ReferenceAnalyzer(); +$htmlCandidates = $referenceAnalyzer->htmlReferenceCandidates('AboutLogo', 'index.html'); +$assert('href' === ($htmlCandidates[0]['attribute'] ?? ''), 'reference analyzer extracts HTML href references'); +$assert('about.html' === ($htmlCandidates[0]['url'] ?? ''), 'reference analyzer preserves HTML href URL values'); +$assert('src' === ($htmlCandidates[1]['attribute'] ?? ''), 'reference analyzer extracts HTML src references'); +$assert('assets/logo.png' === ($htmlCandidates[1]['url'] ?? ''), 'reference analyzer preserves HTML src URL values'); + +$cssCandidates = $referenceAnalyzer->cssReferenceCandidates('@import "fonts/fonts.css"; body{background:url("../assets/paper.png")} @font-face{font-family:"Fixture Sans";src:url("FixtureSans.woff2") format("woff2")}', 'theme/site.css'); +$assert('css-import' === ($cssCandidates[0]['context'] ?? ''), 'reference analyzer extracts CSS @import references'); +$assert('fonts/fonts.css' === ($cssCandidates[0]['url'] ?? ''), 'reference analyzer preserves CSS @import URL values'); +$assert('css-url' === ($cssCandidates[1]['context'] ?? ''), 'reference analyzer extracts CSS url() references'); +$assert('../assets/paper.png' === ($cssCandidates[1]['url'] ?? ''), 'reference analyzer preserves CSS url() values'); +$assert('css-font-face' === ($cssCandidates[2]['context'] ?? ''), 'reference analyzer marks @font-face url() references'); +$assert('FixtureSans.woff2' === ($cssCandidates[2]['url'] ?? ''), 'reference analyzer preserves @font-face local font references'); + +$referenceReports = $referenceAnalyzer->referenceReports(array( + array('path' => 'index.html', 'kind' => 'html', 'content' => 'AboutLogo', 'binary' => false), + array('path' => 'about.html', 'kind' => 'html', 'content' => '

About

', 'binary' => false), + array('path' => 'theme/site.css', 'kind' => 'css', 'content' => '@import "fonts/fonts.css"; body{background:url("../assets/paper.png")} @font-face{font-family:"Fixture Sans";src:url("FixtureSans.woff2") format("woff2")}', 'binary' => false), + array('path' => 'theme/fonts/fonts.css', 'kind' => 'css', 'content' => '', 'binary' => false, 'mime_type' => 'text/css', 'role' => 'style', 'bytes' => 0), + 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/paper.png', 'kind' => 'image', 'content_base64' => base64_encode('paper'), 'binary' => true, 'mime_type' => 'image/png', 'role' => 'asset', 'bytes' => 5), + array('path' => 'theme/FixtureSans.woff2', 'kind' => 'font', 'content_base64' => base64_encode('font'), 'binary' => true, 'mime_type' => 'font/woff2', 'role' => 'asset', 'bytes' => 4), +)); +$assert('about.html' === ($referenceReports['internal_links'][0]['target_path'] ?? ''), 'reference analyzer assembles HTML href internal link reports'); +$assert('assets/logo.png' === ($referenceReports['asset_references'][0]['asset_path'] ?? ''), 'reference analyzer assembles HTML src asset reference reports'); +$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'); + $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"); $assert($severity === ($diagnostic['severity'] ?? ''), "conversion report exposes {$code} severity");