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
27 changes: 14 additions & 13 deletions php-transformer/src/Contract/ConversionReportProjection.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,20 @@ private static function fallbackDiagnostics(array $fallbacks): array
'source_format' => $fallback['source_format'] ?? '',
'source' => $fallback['source'] ?? '',
'scope' => $fallback['scope'] ?? '',
'source_path' => $fallback['source_path'] ?? '',
'tag' => $fallback['tag'] ?? '',
'selector' => $fallback['selector'] ?? '',
'child_count' => $fallback['child_count'] ?? null,
'control_count' => $fallback['control_count'] ?? null,
'form' => $fallback['form'] ?? array(),
'control' => $fallback['control'] ?? array(),
'controls' => $fallback['controls'] ?? array(),
'context' => $fallback['context'] ?? array(),
'events' => $fallback['events'] ?? array(),
'readable_blocks' => $fallback['readable_blocks'] ?? array(),
'html_bytes' => $fallback['html_bytes'] ?? (isset($fallback['html']) && is_string($fallback['html']) ? strlen($fallback['html']) : null),
'html_truncated' => $fallback['html_truncated'] ?? null,
'source_path' => $fallback['source_path'] ?? '',
'tag' => $fallback['tag'] ?? '',
'selector' => $fallback['selector'] ?? '',
'child_count' => $fallback['child_count'] ?? null,
'control_count' => $fallback['control_count'] ?? null,
'form' => $fallback['form'] ?? array(),
'control' => $fallback['control'] ?? array(),
'controls' => $fallback['controls'] ?? array(),
'context' => $fallback['context'] ?? array(),
'events' => $fallback['events'] ?? array(),
'script_dependency_hint' => $fallback['script_dependency_hint'] ?? '',
'readable_blocks' => $fallback['readable_blocks'] ?? array(),
'html_bytes' => $fallback['html_bytes'] ?? (isset($fallback['html']) && is_string($fallback['html']) ? strlen($fallback['html']) : null),
'html_truncated' => $fallback['html_truncated'] ?? null,
),
static fn (mixed $value): bool => null !== $value && '' !== $value
);
Expand Down
8 changes: 8 additions & 0 deletions php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ private static function defaults(array $fields): array
'suggested_primitive' => 'embed',
'materialization_hint' => 'convert_supported_src_to_core_embed_or_preserve_sanitized_iframe_html',
),
'html_canvas_runtime_fallback' => array(
'severity' => 'warning',
'runtime_requirement' => 'canvas_element_and_client_script_execution',
'recoverability' => 'recoverable_with_canvas_markup_preservation_or_rebuilt_interactive_block',
'actionability' => 'preserve_canvas_markup_with_matching_script_runtime_or_rebuild_canvas_behavior',
'suggested_primitive' => 'runtime_canvas',
'materialization_hint' => 'core_blocks_cannot_emit_a_native_canvas_element_without_raw_html; preserve_bounded_canvas_metadata_for_runtime_mapping',
),
'html_unsupported_element' => array(
'severity' => 'info',
'runtime_requirement' => 'unknown',
Expand Down
65 changes: 59 additions & 6 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,14 @@ public function transform(string $html, array $options = array()): TransformerRe
foreach ( $fallbacks as $fallback ) {
if ( ! empty($fallback['diagnostic_code']) ) {
$diagnostics[] = array(
'code' => $fallback['diagnostic_code'],
'message' => $fallback['message'] ?? 'HTML element preserved as fallback metadata.',
'source' => self::class,
'reason' => $fallback['reason'] ?? null,
'tag' => $fallback['tag'] ?? null,
'selector' => $fallback['selector'] ?? null,
'code' => $fallback['diagnostic_code'],
'message' => $fallback['message'] ?? 'HTML element preserved as fallback metadata.',
'source' => self::class,
'reason' => $fallback['reason'] ?? null,
'severity' => $fallback['severity'] ?? null,
'runtime_requirement' => $fallback['runtime_requirement'] ?? null,
'tag' => $fallback['tag'] ?? null,
'selector' => $fallback['selector'] ?? null,
);
}
}
Expand Down Expand Up @@ -1143,6 +1145,11 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
return null;
}

if ( 'canvas' === $tagName ) {
$this->captureCanvasFallback($element, $fallbacks);
return null;
}

if ( 'script' === $tagName ) {
if ( $this->captureStaticScriptMetadata($element) ) {
return null;
Expand Down Expand Up @@ -2857,6 +2864,36 @@ private function captureInlineSvgFallback(DOMElement $element, array &$fallbacks
), $this->fallbackProvenance);
}

/**
* @param array<int, array<string, mixed>> $fallbacks
*/
private function captureCanvasFallback(DOMElement $element, array &$fallbacks): void
{
$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element));
$id = trim($this->attr($element, 'id'));

$fallbacks[] = FallbackDiagnostic::build(array_filter(array(
'type' => 'html',
'reason' => 'canvas_requires_runtime',
'diagnostic_code' => 'html_canvas_runtime_fallback',
'message' => 'Canvas HTML requires a native canvas element and client script runtime; core blocks cannot preserve it without raw HTML.',
'source_format' => 'html',
'tag' => 'canvas',
'selector' => $this->elementSelector($element),
'attributes' => $this->safeCanvasAttributes($element),
'context' => $this->sourceContext($element),
'events' => $this->eventMetadata($element),
'script_dependency_hint' => '' !== $id
? 'Scripts may target #' . $id . ' and call canvas APIs such as getContext(); replacing it with a wrapper block changes runtime behavior.'
: 'Scripts may target this canvas by selector and call canvas APIs such as getContext(); replacing it with a wrapper block changes runtime behavior.',
'text_length' => strlen(trim($element->textContent ?? '')),
'child_count' => $this->childElementCount($element),
'html' => $boundedHtml['html'],
'html_bytes' => $boundedHtml['bytes'],
'html_truncated' => $boundedHtml['truncated'],
), static fn (mixed $value): bool => '' !== $value && array() !== $value), $this->fallbackProvenance);
}

/**
* @param array<int, array<string, mixed>> $fallbacks
*/
Expand Down Expand Up @@ -2953,6 +2990,22 @@ private function safeScriptAttributes(DOMElement $element): array
return $safe;
}

/**
* @return array<string, string>
*/
private function safeCanvasAttributes(DOMElement $element): array
{
$safe = array();
$allowed = array_flip(array( 'aria-label', 'class', 'height', 'id', 'role', 'style', 'title', 'width' ));
foreach ( $this->htmlAttributes($element) as $name => $value ) {
if ( isset($allowed[$name]) ) {
$safe[$name] = strlen($value) > 300 ? substr($value, 0, 300) . '...' : $value;
}
}

return $safe;
}

/**
* @return array{html: string, bytes: int, truncated: bool}
*/
Expand Down
20 changes: 16 additions & 4 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,22 @@ function serialize_blocks(array $blocks): string
}
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_unsafe_inline_svg'] ?? array(), 'html_unsafe_inline_svg', 'warning', 'sanitization_review', 'image_asset');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_script_fallback'] ?? array(), 'html_script_fallback', 'warning', 'client_script_execution', 'script_asset');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_unsupported_element'] ?? array(), 'html_unsupported_element', 'info', 'unknown', 'core/html');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_canvas_runtime_fallback'] ?? array(), 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_iframe_embed_fallback'] ?? array(), 'html_iframe_embed_fallback', 'warning', 'third_party_embed_runtime', 'embed');
$assert(! isset($diagnosticsByCode['html_inline_svg_fallback']), 'safe inline SVGs convert to image blocks instead of fallback diagnostics');

$canvasFallback = ( new HtmlTransformer() )->transform(
'<main><canvas id="bonsai" class="stage" width="640" height="360">Fallback</canvas><script src="/js/script.js"></script></main>'
)->toArray();
$canvasDiagnostic = $canvasFallback['source_reports']['conversion_report']['fallback_diagnostics'][0] ?? array();
$assertNormalizedFallbackDiagnostic($canvasDiagnostic, 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas');
$assert('canvas_requires_runtime' === ($canvasDiagnostic['reason'] ?? ''), 'canvas fallback exposes runtime-specific reason');
$assert('bonsai' === ($canvasFallback['fallbacks'][0]['attributes']['id'] ?? ''), 'canvas fallback preserves id for runtime mapping');
$assert(str_contains((string) ($canvasFallback['fallbacks'][0]['html'] ?? ''), '<canvas id="bonsai"'), 'canvas fallback preserves bounded safe canvas markup');
$assert(str_contains((string) ($canvasDiagnostic['script_dependency_hint'] ?? ''), '#bonsai'), 'canvas diagnostic flags id-based script dependency risk');
$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');

$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 @@ -930,9 +942,9 @@ function serialize_blocks(array $blocks): string
assertSame('core/paragraph', $result['blocks'][0]['innerBlocks'][1]['blockName'], 'p should convert to a paragraph block.');
assertSame('core/list', $result['blocks'][1]['blockName'], 'ul should convert to a list block.');
assertSame('core/list-item', $result['blocks'][1]['innerBlocks'][0]['blockName'], 'li should convert to list-item blocks.');
assertSame('unsupported_element', $result['fallbacks'][0]['type'], 'unsupported top-level elements should be reported as fallbacks.');
assertSame('unsupported_element', $result['fallbacks'][0]['reason'], 'fallbacks should expose a stable generic reason.');
assertSame('html_unsupported_element', $result['fallbacks'][0]['diagnostic_code'], 'fallbacks should expose a diagnostic code for cross-process consumers.');
assertSame('html', $result['fallbacks'][0]['type'], 'canvas elements should be reported as HTML runtime fallbacks.');
assertSame('canvas_requires_runtime', $result['fallbacks'][0]['reason'], 'canvas fallbacks should expose a runtime-specific reason.');
assertSame('html_canvas_runtime_fallback', $result['fallbacks'][0]['diagnostic_code'], 'canvas fallbacks should expose a runtime-specific diagnostic code for cross-process consumers.');
assertSame('html', $result['fallbacks'][0]['source_format'], 'fallbacks should expose the source format.');
assertSame('canvas', $result['fallbacks'][0]['tag'], 'fallback should identify the unsupported tag.');
assertContains('html_to_blocks_core_slice', array_column($result['diagnostics'], 'code'), 'expanded core-slice conversion diagnostic should be present.');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
"name": "unsupported-html-fallback",
"description": "Reports unsupported top-level HTML as fallback metadata without failing the transform.",
"description": "Reports native canvas HTML as runtime-critical fallback metadata without emitting raw HTML blocks.",
"source_reference": {
"repo": "php-transformer",
"path": "tests/smoke-unsupported-html-fallback-hook.php",
Expand All @@ -17,13 +17,15 @@
},
"expected_blocks": [],
"expected_fallbacks": [
{ "type": "unsupported_element", "reason": "unsupported_element", "tag": "canvas", "selector": "canvas:nth-of-type(1)", "child_count": 1 }
{ "type": "html", "reason": "canvas_requires_runtime", "diagnostic_code": "html_canvas_runtime_fallback", "tag": "canvas", "selector": "canvas:nth-of-type(1)", "child_count": 1 }
],
"expect": [
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "blocks", "assert": "count", "count": 0 },
{ "path": "fallbacks", "assert": "count", "count": 1 },
{ "path": "fallbacks.0.type", "assert": "equals", "value": "unsupported_element" },
{ "path": "fallbacks.0.type", "assert": "equals", "value": "html" },
{ "path": "fallbacks.0.reason", "assert": "equals", "value": "canvas_requires_runtime" },
{ "path": "fallbacks.0.diagnostic_code", "assert": "equals", "value": "html_canvas_runtime_fallback" },
{ "path": "fallbacks.0.tag", "assert": "equals", "value": "canvas" },
{ "path": "fallbacks.0.selector", "assert": "equals", "value": "canvas:nth-of-type(1)" },
{ "path": "diagnostics.1.selector", "assert": "equals", "value": "canvas:nth-of-type(1)" },
Expand Down
Loading