From 3dc20bed41c403cdb38352c3c144ea5958cab8cb Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 14 Jul 2026 12:11:41 -0400 Subject: [PATCH] Normalize legacy frameset navigation --- .../DocumentNormalizationPolicy.php | 42 +++++++++++++++++++ .../src/HtmlToBlocks/FallbackDiagnostic.php | 13 ++++++ .../src/HtmlToBlocks/HtmlTransformer.php | 5 ++- .../tests/contract/finding-contract.php | 21 ++++++++++ ...tml-legacy-frameset-noframes-fallback.json | 25 +++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 php-transformer/src/HtmlToBlocks/DocumentNormalizationPolicy.php create mode 100644 php-transformer/tests/fixtures/parity/html-legacy-frameset-noframes-fallback.json diff --git a/php-transformer/src/HtmlToBlocks/DocumentNormalizationPolicy.php b/php-transformer/src/HtmlToBlocks/DocumentNormalizationPolicy.php new file mode 100644 index 00000000..111b9432 --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/DocumentNormalizationPolicy.php @@ -0,0 +1,42 @@ +|null} + */ + public function normalize(string $html): array + { + if ( + ! preg_match('/]*>/i', $html) + || ! preg_match('/<\/frameset\s*>/i', $html) + || ! preg_match('/]*>(.*?)<\/noframes\s*>/is', $html, $matches) + ) { + return array( + 'html' => $html, + 'fallback' => null, + ); + } + + $fallbackHtml = preg_replace('/<\/?frameset\b[^>]*>/i', '', $matches[1]) ?? $matches[1]; + $fallbackHtml = preg_replace('/]*>/i', '', $fallbackHtml) ?? $fallbackHtml; + + return array( + 'html' => $fallbackHtml, + 'fallback' => FallbackDiagnostic::build(array( + 'type' => 'legacy_frameset_navigation', + 'reason' => 'unsupported_legacy_frameset_navigation', + 'diagnostic_code' => 'html_legacy_frameset_navigation', + 'message' => 'Legacy frameset navigation was removed and its noframes fallback content was converted.', + 'tag' => 'frameset', + )), + ); + } +} diff --git a/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php b/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php index 8813811b..fa773531 100644 --- a/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php +++ b/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php @@ -192,6 +192,19 @@ private static function defaults(array $fields): array 'suggested_primitive' => 'core/html', 'materialization_hint' => 'preserve_sanitized_markup_until_a_specific_block_mapping_exists', ), + 'html_legacy_frameset_navigation' => array( + 'severity' => 'warning', + 'conversion_classification' => 'unsupported_loss', + 'loss_class' => 'unsupported_legacy_navigation_loss', + 'diagnostic_class' => 'unsupported_legacy_navigation', + 'preservation_strategy' => 'convert_noframes_fallback_content', + 'runtime_requirement' => 'none', + 'recoverability' => 'recoverable_with_manual_navigation_mapping', + 'actionability' => 'map_legacy_frame_destinations_to_pages_or_navigation_blocks', + 'suggested_repair_class' => 'map_legacy_frameset_navigation', + 'suggested_primitive' => 'navigation', + 'materialization_hint' => 'convert_the_noframes_fallback_content_and_rebuild_frame_destinations_as_standard_navigation', + ), 'interactive_control_behavior_lost' => array( 'severity' => 'warning', 'conversion_classification' => 'behavior_loss', diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index baf3b01a..78397130 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -406,7 +406,8 @@ public function transform(string $html, array $options = array()): TransformerRe ), $this->fallbackProvenance), ); - $normalizedHtml = $this->normalizeHtml5VoidElements($this->documentBodyHtml($html)); + $documentNormalization = ( new DocumentNormalizationPolicy() )->normalize($html); + $normalizedHtml = $this->normalizeHtml5VoidElements($this->documentBodyHtml($documentNormalization['html'])); $document = new DOMDocument(); $previous = libxml_use_internal_errors(true); $loaded = $document->loadHTML('' . $normalizedHtml . '', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); @@ -461,7 +462,7 @@ public function transform(string $html, array $options = array()): TransformerRe ); } - $fallbacks = array(); + $fallbacks = null === $documentNormalization['fallback'] ? array() : array( $documentNormalization['fallback'] ); $interactionCandidates = $this->interactionCandidates($body); $this->collectSupersededNavToggleSelectors($body); $blocks = $this->deduplicateNavigationBlocks($this->convertChildren($body, $fallbacks, true)); diff --git a/php-transformer/tests/contract/finding-contract.php b/php-transformer/tests/contract/finding-contract.php index 6255650a..aebc11ce 100644 --- a/php-transformer/tests/contract/finding-contract.php +++ b/php-transformer/tests/contract/finding-contract.php @@ -257,6 +257,27 @@ $totalFindings += $walk($result, "html:{$name}"); } +$framesetResult = ( new HtmlTransformer() )->transform( + '<body><main id="main-content"><p>Kept fallback</p></main></body>' +)->toArray(); +$totalFindings += $walk($framesetResult, 'html:legacy-frameset'); +$assert( + 'html_legacy_frameset_navigation' === ($framesetResult['fallbacks'][0]['diagnostic_code'] ?? null), + 'legacy frameset navigation emits a typed diagnostic' +); +$assert( + str_contains((string) ($framesetResult['serialized_blocks'] ?? ''), 'Kept fallback'), + 'legacy frameset conversion retains noframes fallback content' +); +$assert( + ! str_contains((string) ($framesetResult['serialized_blocks'] ?? ''), 'transform( '
Fallback
', diff --git a/php-transformer/tests/fixtures/parity/html-legacy-frameset-noframes-fallback.json b/php-transformer/tests/fixtures/parity/html-legacy-frameset-noframes-fallback.json new file mode 100644 index 00000000..511aee97 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-legacy-frameset-noframes-fallback.json @@ -0,0 +1,25 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-legacy-frameset-noframes-fallback", + "description": "Converts noframes fallback content while removing unsupported self-referential legacy frame navigation.", + "operation": "html_transformer.transform", + "input": { + "content": "Legacy frame page<body><main id=\"main-content\"><p>Kept fallback</p></main></body>" + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/group" }, + { "path": "blocks.0.innerBlocks.0", "name": "core/paragraph", "attrs": { "content": "Kept fallback" } } + ], + "expected_fallbacks": [ + { "type": "legacy_frameset_navigation", "reason": "unsupported_legacy_frameset_navigation", "diagnostic_code": "html_legacy_frameset_navigation", "tag": "frameset" } + ], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 1 }, + { "path": "diagnostics.1.code", "assert": "equals", "value": "html_legacy_frameset_navigation" }, + { "path": "serialized_blocks", "assert": "contains", "value": "Kept fallback" }, + { "path": "serialized_blocks", "assert": "not_contains", "value": "