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
40 changes: 40 additions & 0 deletions php-transformer/src/Contract/ConversionReportProjection.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static function fromResultParts(string $sourceFormat, array $blocks, arra
'scope' => self::firstString($provenance, 'scope'),
'source_summary' => self::sourceSummary($sourceFormat, $blocks, $fallbacks, $sourceReports, $assets, $metrics),
'selector_summary' => self::selectorSummary($sourceReports, $fallbacks),
'conversion_classification_summary' => self::conversionClassificationSummary($sourceReports, $fallbacks),
'fallback_diagnostics' => self::fallbackDiagnostics($fallbacks),
'asset_refs' => self::assetReferences($blocks, $sourceReports),
'navigation_candidates' => self::navigationCandidates($blocks, $sourceReports),
Expand Down Expand Up @@ -122,6 +123,8 @@ private static function fallbackDiagnostics(array $fallbacks): array
'reason' => $fallback['reason'] ?? '',
'diagnostic_code' => $fallback['diagnostic_code'] ?? '',
'severity' => $fallback['severity'] ?? '',
'conversion_classification' => $fallback['conversion_classification'] ?? '',
'preservation_strategy' => $fallback['preservation_strategy'] ?? '',
'runtime_requirement' => $fallback['runtime_requirement'] ?? '',
'recoverability' => $fallback['recoverability'] ?? '',
'actionability' => $fallback['actionability'] ?? '',
Expand Down Expand Up @@ -152,6 +155,41 @@ private static function fallbackDiagnostics(array $fallbacks): array
return $diagnostics;
}

/**
* @param array<string, mixed> $sourceReports
* @param array<int, array<string, mixed>> $fallbacks
* @return array<string, mixed>
*/
private static function conversionClassificationSummary(array $sourceReports, array $fallbacks): array
{
$byClassification = array();
$byStrategy = array();

foreach ( array_merge(self::sourceProvenance($sourceReports), self::fallbackDiagnostics($fallbacks)) as $entry ) {
if ( ! is_array($entry) ) {
continue;
}

$classification = (string) ($entry['conversion_classification'] ?? '');
if ( '' !== $classification ) {
$byClassification[$classification] = ($byClassification[$classification] ?? 0) + 1;
}

$strategy = (string) ($entry['preservation_strategy'] ?? '');
if ( '' !== $strategy ) {
$byStrategy[$strategy] = ($byStrategy[$strategy] ?? 0) + 1;
}
}

return array_filter(
array(
'by_classification' => $byClassification,
'by_preservation_strategy' => $byStrategy,
),
static fn (mixed $value): bool => array() !== $value
);
}

/**
* @param array<int, array<string, mixed>> $blocks
* @param array<string, mixed> $sourceReports
Expand Down Expand Up @@ -383,6 +421,8 @@ private static function appendSelector(array &$selectors, array $entry, string $
'block_name' => $entry['block_name'] ?? '',
'tag' => $entry['tag'] ?? ($entry['element'] ?? ''),
'attribute' => $entry['attribute'] ?? '',
'conversion_classification' => $entry['conversion_classification'] ?? '',
'preservation_strategy' => $entry['preservation_strategy'] ?? '',
),
static fn (mixed $value): bool => '' !== $value
);
Expand Down
16 changes: 16 additions & 0 deletions php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ private static function defaults(array $fields): array
return match ( $code ) {
'html_form_fallback' => array(
'severity' => 'warning',
'conversion_classification' => 'runtime_island_preserved',
'preservation_strategy' => 'fallback_metadata_with_readable_blocks',
'runtime_requirement' => 'server_or_client_form_handler',
'recoverability' => 'recoverable_with_runtime_mapping',
'actionability' => 'map_form_action_controls_and_submission_handler',
Expand All @@ -34,6 +36,8 @@ private static function defaults(array $fields): array
),
'html_script_fallback' => array(
'severity' => 'warning',
'conversion_classification' => 'runtime_island_preserved',
'preservation_strategy' => 'scoped_runtime_metadata',
'runtime_requirement' => 'client_script_execution',
'recoverability' => 'recoverable_with_script_enqueue_or_component_runtime',
'actionability' => 'review_script_source_and_enqueue_or_rebuild_behavior',
Expand All @@ -42,6 +46,8 @@ private static function defaults(array $fields): array
),
'html_inline_svg_fallback' => array(
'severity' => 'info',
'conversion_classification' => 'editable_approximation',
'preservation_strategy' => 'sanitized_static_markup_or_image',
'runtime_requirement' => 'none',
'recoverability' => 'recoverable_as_static_markup_or_image_asset',
'actionability' => 'review_sanitized_svg_and_materialize_as_image_or_html',
Expand All @@ -50,6 +56,8 @@ private static function defaults(array $fields): array
),
'html_unsafe_inline_svg' => array(
'severity' => 'warning',
'conversion_classification' => 'unsupported_loss',
'preservation_strategy' => 'diagnostic_only_until_security_review',
'runtime_requirement' => 'sanitization_review',
'recoverability' => 'recoverable_after_security_review',
'actionability' => 'remove_scriptable_svg_content_or_replace_with_safe_asset',
Expand All @@ -58,6 +66,8 @@ private static function defaults(array $fields): array
),
'html_iframe_embed_fallback' => array(
'severity' => 'warning',
'conversion_classification' => 'runtime_island_preserved',
'preservation_strategy' => 'sanitized_embed_markup',
'runtime_requirement' => 'third_party_embed_runtime',
'recoverability' => 'recoverable_with_embed_provider_or_html_preservation',
'actionability' => 'map_iframe_src_to_supported_embed_provider_or_preserve_html',
Expand All @@ -66,6 +76,8 @@ private static function defaults(array $fields): array
),
'html_canvas_runtime_fallback' => array(
'severity' => 'warning',
'conversion_classification' => 'runtime_island_preserved',
'preservation_strategy' => 'bounded_raw_html_runtime_island',
'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',
Expand All @@ -74,6 +86,8 @@ private static function defaults(array $fields): array
),
'html_unsupported_element' => array(
'severity' => 'info',
'conversion_classification' => 'unsupported_loss',
'preservation_strategy' => 'diagnostic_only',
'runtime_requirement' => 'unknown',
'recoverability' => 'recoverable_with_manual_mapping',
'actionability' => 'map_element_to_supported_block_or_preserve_html',
Expand All @@ -82,6 +96,8 @@ private static function defaults(array $fields): array
),
default => array(
'severity' => 'warning',
'conversion_classification' => 'unsupported_loss',
'preservation_strategy' => 'diagnostic_only',
'runtime_requirement' => 'unknown',
'recoverability' => 'unknown',
'actionability' => 'review_fallback_metadata',
Expand Down
38 changes: 37 additions & 1 deletion php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ public function transform(string $html, array $options = array()): TransformerRe
'source' => self::class,
'reason' => $fallback['reason'] ?? null,
'severity' => $fallback['severity'] ?? null,
'conversion_classification' => $fallback['conversion_classification'] ?? null,
'preservation_strategy' => $fallback['preservation_strategy'] ?? null,
'runtime_requirement' => $fallback['runtime_requirement'] ?? null,
'tag' => $fallback['tag'] ?? null,
'selector' => $fallback['selector'] ?? null,
Expand Down Expand Up @@ -1496,13 +1498,47 @@ private function resolveSourceProvenancePaths(array &$blocks, string $path, arra
*/
private function sourceProvenanceEntry(string $blockName, DOMElement $element): array
{
return array(
return array_merge(array(
'block_name' => $blockName,
'tag' => strtolower($element->tagName),
'selector' => $this->elementSelector($element),
'source_attributes' => $this->safeSourceAttributes($element),
'source_fragment' => $this->safeSourceFragment($element),
'context' => $this->sourceContext($element),
), $this->sourceConversionMetadata($blockName, $element));
}

/**
* @return array{conversion_classification: string, preservation_strategy: string}
*/
private function sourceConversionMetadata(string $blockName, DOMElement $element): array
{
$tagName = strtolower($element->tagName);

if ( 'core/html' === $blockName ) {
return array(
'conversion_classification' => 'runtime_island_preserved',
'preservation_strategy' => 'bounded_raw_html_runtime_island',
);
}

if ( $this->isRuntimeDomTarget($element) ) {
return array(
'conversion_classification' => 'runtime_island_preserved',
'preservation_strategy' => 'core_block_shell_with_runtime_target',
);
}

if ( in_array($tagName, array('form', 'input', 'select', 'textarea'), true) && 'core/search' !== $blockName ) {
return array(
'conversion_classification' => 'editable_approximation',
'preservation_strategy' => 'readable_static_block_approximation',
);
}

return array(
'conversion_classification' => 'native_block_conversion',
'preservation_strategy' => 'core_block',
);
}

Expand Down
34 changes: 31 additions & 3 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,13 @@ function serialize_blocks(array $blocks): string
$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 {
$assertNormalizedFallbackDiagnostic = static function (array $diagnostic, string $code, string $severity, string $runtimeRequirement, string $suggestedPrimitive, string $conversionClassification = '') use ($assert): void {
$assert($code === ($diagnostic['diagnostic_code'] ?? ''), "conversion report exposes {$code} diagnostic code");
$assert($severity === ($diagnostic['severity'] ?? ''), "conversion report exposes {$code} severity");
if ( '' !== $conversionClassification ) {
$assert($conversionClassification === ($diagnostic['conversion_classification'] ?? ''), "conversion report exposes {$code} conversion classification");
$assert(isset($diagnostic['preservation_strategy']) && '' !== $diagnostic['preservation_strategy'], "conversion report exposes {$code} preservation strategy");
}
$assert($runtimeRequirement === ($diagnostic['runtime_requirement'] ?? ''), "conversion report exposes {$code} runtime requirement");
$assert(isset($diagnostic['recoverability']) && '' !== $diagnostic['recoverability'], "conversion report exposes {$code} recoverability");
$assert(isset($diagnostic['actionability']) && '' !== $diagnostic['actionability'], "conversion report exposes {$code} actionability");
Expand Down Expand Up @@ -560,22 +564,46 @@ public function match(DOMElement $element, PatternContext $context): ?array
}
$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_canvas_runtime_fallback'] ?? array(), 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas');
$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_canvas_runtime_fallback'] ?? array(), 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas', 'runtime_island_preserved');
$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');
$assertNormalizedFallbackDiagnostic($canvasDiagnostic, 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas', 'runtime_island_preserved');
$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');

$runtimePreserved = ( new HtmlTransformer() )->transform(
'<main><canvas id="stage" aria-hidden="true"></canvas><input id="amount" value="10"></main>',
array(
'runtime_canvas_selectors' => array('#stage'),
'runtime_dom_selectors' => array('#amount'),
)
)->toArray();
$runtimeSelectors = $runtimePreserved['source_reports']['conversion_report']['selector_summary']['selectors'] ?? array();
$runtimeClassifications = array();
foreach ( $runtimeSelectors as $selector ) {
if ( 'block' === ($selector['kind'] ?? '') && 'core/html' === ($selector['block_name'] ?? '') ) {
$runtimeClassifications[$selector['tag'] ?? ''] = $selector['conversion_classification'] ?? '';
}
}
$assert('runtime_island_preserved' === ($runtimeClassifications['canvas'] ?? ''), 'runtime-preserved canvas core/html block is classified as runtime island preservation');
$assert('runtime_island_preserved' === ($runtimeClassifications['input'] ?? ''), 'runtime-preserved control core/html block is classified as runtime island preservation');
$runtimeSummary = $runtimePreserved['source_reports']['conversion_report']['conversion_classification_summary']['by_classification'] ?? array();
$assert(2 <= ($runtimeSummary['runtime_island_preserved'] ?? 0), 'conversion report summarizes runtime island preservation counts');

$unsupportedLoss = ( new HtmlTransformer() )->transform('<main><applet code="clock.class"></applet></main>')->toArray();
$unsupportedDiagnostic = $unsupportedLoss['source_reports']['conversion_report']['fallback_diagnostics'][0] ?? array();
$assert('html_unsupported_element' === ($unsupportedDiagnostic['diagnostic_code'] ?? ''), 'unsupported element emits fallback diagnostic');
$assert('unsupported_loss' === ($unsupportedDiagnostic['conversion_classification'] ?? ''), 'true unsupported fallback is classified as unsupported loss');

$decorativeCanvas = ( new HtmlTransformer() )->transform(
'<main><section class="hero"><canvas id="stars" aria-hidden="true"></canvas><h1>Stars</h1></section></main>',
array(
Expand Down
Loading