Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ private function runtimeCanvasSelectors(string $html, string $sourcePath, array

$selectors = array();
foreach ( $this->documentScriptContents($html, $sourcePath, $files) as $script ) {
foreach ( $this->scriptDomSelectors($script) as $selector ) {
foreach ( $this->scriptCanvasSelectors($script) as $selector ) {
if ( isset($canvasSelectors[$selector]) ) {
$selectors[$selector] = true;
}
Expand All @@ -305,6 +305,45 @@ private function runtimeCanvasSelectors(string $html, string $sourcePath, array
return array_keys($selectors);
}

/**
* @return array<int, string>
*/
private function scriptCanvasSelectors(string $script): array
{
$selectors = array();
$getContextPattern = '\.\s*getContext\s*\(';

if ( preg_match_all('/document\s*\.\s*getElementById\s*\(\s*(["\'])([A-Za-z][A-Za-z0-9_-]*)\1\s*\)\s*' . $getContextPattern . '/', $script, $matches) ) {
foreach ( $matches[2] as $id ) {
$selectors['#' . (string) $id] = true;
}
}

if ( preg_match_all('/document\s*\.\s*querySelector\s*\(\s*(["\'])([#.][A-Za-z][A-Za-z0-9_-]*)\1\s*\)\s*' . $getContextPattern . '/', $script, $matches) ) {
foreach ( $matches[2] as $selector ) {
$selectors[(string) $selector] = true;
}
}

if ( preg_match_all('/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*document\s*\.\s*getElementById\s*\(\s*(["\'])([A-Za-z][A-Za-z0-9_-]*)\2\s*\)/', $script, $assignments, PREG_SET_ORDER) ) {
foreach ( $assignments as $assignment ) {
if ( preg_match('/\b' . preg_quote((string) $assignment[1], '/') . '\s*' . $getContextPattern . '/', $script) ) {
$selectors['#' . (string) $assignment[3]] = true;
}
}
}

if ( preg_match_all('/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*document\s*\.\s*querySelector\s*\(\s*(["\'])([#.][A-Za-z][A-Za-z0-9_-]*)\2\s*\)/', $script, $assignments, PREG_SET_ORDER) ) {
foreach ( $assignments as $assignment ) {
if ( preg_match('/\b' . preg_quote((string) $assignment[1], '/') . '\s*' . $getContextPattern . '/', $script) ) {
$selectors[(string) $assignment[3]] = true;
}
}
}

return array_keys($selectors);
}

/**
* @return array<string, bool>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ private function scriptDependencies(string $script): array
{
$dependencies = array();
$eventsBySelector = $this->eventsBySelector($script);
$canvasApi = preg_match('/\.\s*getContext\s*\(\s*(["\'])2d\1\s*\)/', $script) === 1;
$canvasSelectors = $this->scriptCanvasSelectors($script);

if ( preg_match_all('/document\s*\.\s*getElementById\s*\(\s*(["\'])([A-Za-z][A-Za-z0-9_-]*)\1\s*\)/', $script, $matches) ) {
foreach ( $matches[2] as $id ) {
Expand All @@ -177,7 +177,7 @@ private function scriptDependencies(string $script): array
'kind' => 'id',
'selector' => $selector,
'events' => $eventsBySelector[$selector] ?? array(),
'canvas_api' => $canvasApi,
'canvas_api' => isset($canvasSelectors[$selector]),
);
}
}
Expand All @@ -189,7 +189,7 @@ private function scriptDependencies(string $script): array
'kind' => str_starts_with($selector, '#') ? 'id' : 'class',
'selector' => $selector,
'events' => $eventsBySelector[$selector] ?? array(),
'canvas_api' => $canvasApi,
'canvas_api' => isset($canvasSelectors[$selector]),
);
}
}
Expand All @@ -201,7 +201,7 @@ private function scriptDependencies(string $script): array
'kind' => str_starts_with($selector, '#') ? 'id' : 'class',
'selector' => $selector,
'events' => $eventsBySelector[$selector] ?? array(),
'canvas_api' => $canvasApi,
'canvas_api' => isset($canvasSelectors[$selector]),
);
}
}
Expand All @@ -213,14 +213,53 @@ private function scriptDependencies(string $script): array
'kind' => str_starts_with($selector, '#') ? 'id' : 'class',
'selector' => $selector,
'events' => $eventsBySelector[$selector] ?? array(),
'canvas_api' => $canvasApi,
'canvas_api' => isset($canvasSelectors[$selector]),
);
}
}

return $this->dedupeDependencies($dependencies);
}

/**
* @return array<string, bool>
*/
private function scriptCanvasSelectors(string $script): array
{
$selectors = array();
$getContextPattern = '\.\s*getContext\s*\(';

if ( preg_match_all('/document\s*\.\s*getElementById\s*\(\s*(["\'])([A-Za-z][A-Za-z0-9_-]*)\1\s*\)\s*' . $getContextPattern . '/', $script, $matches) ) {
foreach ( $matches[2] as $id ) {
$selectors['#' . (string) $id] = true;
}
}

if ( preg_match_all('/document\s*\.\s*querySelector\s*\(\s*(["\'])([#.][A-Za-z][A-Za-z0-9_-]*)\1\s*\)\s*' . $getContextPattern . '/', $script, $matches) ) {
foreach ( $matches[2] as $selector ) {
$selectors[(string) $selector] = true;
}
}

if ( preg_match_all('/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*document\s*\.\s*getElementById\s*\(\s*(["\'])([A-Za-z][A-Za-z0-9_-]*)\2\s*\)/', $script, $assignments, PREG_SET_ORDER) ) {
foreach ( $assignments as $assignment ) {
if ( preg_match('/\b' . preg_quote((string) $assignment[1], '/') . '\s*' . $getContextPattern . '/', $script) ) {
$selectors['#' . (string) $assignment[3]] = true;
}
}
}

if ( preg_match_all('/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*document\s*\.\s*querySelector\s*\(\s*(["\'])([#.][A-Za-z][A-Za-z0-9_-]*)\2\s*\)/', $script, $assignments, PREG_SET_ORDER) ) {
foreach ( $assignments as $assignment ) {
if ( preg_match('/\b' . preg_quote((string) $assignment[1], '/') . '\s*' . $getContextPattern . '/', $script) ) {
$selectors[(string) $assignment[3]] = true;
}
}
}

return $selectors;
}

/**
* @return array<string, array<int, string>>
*/
Expand Down
14 changes: 14 additions & 0 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,10 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
}

if ( 'canvas' === $tagName ) {
if ( $this->isDecorativeCanvas($element) && ! $this->isRuntimeCanvasTarget($element) ) {
return null;
}

$this->captureCanvasFallback($element, $fallbacks);
if ( $this->isRuntimeCanvasTarget($element) ) {
$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element));
Expand Down Expand Up @@ -3059,6 +3063,16 @@ private function isRuntimeCanvasTarget(DOMElement $element): bool
return false;
}

private function isDecorativeCanvas(DOMElement $element): bool
{
if ( '' !== trim($element->textContent ?? '') || $this->childElementCount($element) > 0 ) {
return false;
}

return 'true' === strtolower($this->attr($element, 'aria-hidden'))
|| in_array(strtolower($this->attr($element, 'role')), array('presentation', 'none'), true);
}

/**
* @param array<string, mixed> $options
* @return array<string, bool>
Expand Down
28 changes: 28 additions & 0 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,17 @@ function serialize_blocks(array $blocks): string
$assert(! str_contains((string) ($canvasFallback['serialized_blocks'] ?? ''), '<!-- wp:html'), 'canvas fallback does not emit core/html');
$assert(! str_contains((string) ($canvasFallback['serialized_blocks'] ?? ''), '<canvas'), 'canvas fallback does not smuggle raw canvas markup into generated core blocks');

$decorativeCanvas = ( new HtmlTransformer() )->transform(
'<main><section class="hero"><canvas id="stars" aria-hidden="true"></canvas><h1>Stars</h1></section></main>',
array(
'strict' => true,
'allow_fallbacks' => false,
)
)->toArray();
$assert('success' === ($decorativeCanvas['status'] ?? ''), 'decorative canvas without runtime selectors does not trip strict fallback gates', (string) ($decorativeCanvas['status'] ?? ''));
$assert(array() === ($decorativeCanvas['fallbacks'] ?? array()), 'decorative canvas without runtime selectors is omitted instead of reported as runtime fallback');
$assert(! str_contains((string) ($decorativeCanvas['serialized_blocks'] ?? ''), '<canvas'), 'decorative canvas without runtime selectors is not emitted as raw markup');

$safeDecorativeSvg = ( new HtmlTransformer() )->transform(
'<main><svg aria-hidden="true" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5"></circle></svg><div class="site-logo"><svg viewBox="0 0 10 10"><path d="M0 0h10v10H0z"></path></svg></div></main>'
)->toArray();
Expand Down Expand Up @@ -760,9 +771,26 @@ function serialize_blocks(array $blocks): string
$assert(null !== $statusDependency, 'runtime dependency parity records preserved status container dependency');
$assert('index.html' === ($statusDependency['source_path'] ?? ''), 'runtime dependency parity records source path for preserved DOM dependency');
$assert(true === ($statusDependency['generated_present'] ?? null), 'runtime dependency parity passes preserved div id target');
$assert(false === ($statusDependency['canvas_api'] ?? null), 'runtime dependency parity does not mark non-canvas DOM targets as canvas API dependencies');
$assert(! empty($statusDependency['events'] ?? array()), 'runtime dependency parity records simple addEventListener usage');
$assert('info' === ($rumFinding['severity'] ?? ''), 'telemetry-like runtime dependency misses are info severity');

$decorativeCanvasSite = $compiler->compile(
array(
'entrypoint' => 'index.html',
'files' => array(
'index.html' => '<main><canvas id="hero-canvas" aria-hidden="true"></canvas><canvas id="lab-canvas" class="stage" aria-label="Live pattern"></canvas><script src="js/app.js"></script></main>',
'js/app.js' => 'const lab = document.getElementById("lab-canvas"); lab.getContext("2d"); document.getElementById("hero-canvas");',
),
)
)->toArray();
$decorativeCanvasMarkup = (string) ($decorativeCanvasSite['serialized_blocks'] ?? '');
$decorativeCanvasFallbacks = $decorativeCanvasSite['fallbacks'] ?? array();
$assert(str_contains($decorativeCanvasMarkup, '<canvas id="lab-canvas" class="stage" aria-label="Live pattern"></canvas>'), 'artifact compiler preserves canvas markup only for selectors with direct canvas API usage');
$assert(! str_contains($decorativeCanvasMarkup, 'hero-canvas'), 'artifact compiler omits decorative canvas touched by script without canvas API usage');
$assert(1 === count($decorativeCanvasFallbacks), 'artifact compiler records one runtime canvas fallback for the interactive canvas only');
$assert('lab-canvas' === ($decorativeCanvasFallbacks[0]['attributes']['id'] ?? ''), 'runtime canvas fallback provenance points to the interactive canvas');

$runtimeTargetContainerSite = $compiler->compile(
array(
'entrypoint' => 'index.html',
Expand Down
Loading