diff --git a/php-transformer/src/HtmlToBlocks/Diagnostics/DiagnosticsCollector.php b/php-transformer/src/HtmlToBlocks/Diagnostics/DiagnosticsCollector.php new file mode 100644 index 00000000..3664d4ec --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/Diagnostics/DiagnosticsCollector.php @@ -0,0 +1,148 @@ +> $scriptMetadata Static script metadata preserved as bounded data. + * @param array> $fallbacks Fallback diagnostics emitted during conversion. + * @param array> $runtimeIslands Preserved runtime islands. + * @param array $blockValidityReport Block serialization validity report. + * @param array $semanticParityReport Semantic parity report. + * @return array> + */ + public function collect( + string $transformerSource, + array $scriptMetadata, + array $fallbacks, + array $runtimeIslands, + array $blockValidityReport, + array $semanticParityReport + ): array { + $diagnostics = array( + array( + 'code' => 'html_to_blocks_core_slice', + 'message' => 'Converted supported core text, layout, media, gallery, embed, file, table, button, shortcode, spacer, definition-list, details, navigation, safe inline SVG images, and wrapper elements; unsupported elements are reported as fallbacks.', + 'source' => $transformerSource, + ), + ); + + foreach ( $scriptMetadata as $metadata ) { + $diagnostics[] = array( + 'code' => 'html_static_script_metadata', + 'message' => 'Static script data was preserved as bounded metadata and does not require client script execution.', + 'source' => $transformerSource, + 'reason' => 'script_static_metadata', + 'tag' => 'script', + 'selector' => $metadata['selector'] ?? null, + 'script_role' => $metadata['script_role'] ?? null, + ); + } + + 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' => $transformerSource, + 'reason' => $fallback['reason'] ?? null, + 'severity' => $fallback['severity'] ?? null, + 'conversion_classification' => $fallback['conversion_classification'] ?? null, + 'loss_class' => $fallback['loss_class'] ?? null, + 'diagnostic_class' => $fallback['diagnostic_class'] ?? null, + 'preservation_strategy' => $fallback['preservation_strategy'] ?? null, + 'runtime_requirement' => $fallback['runtime_requirement'] ?? null, + 'recoverability' => $fallback['recoverability'] ?? null, + 'actionability' => $fallback['actionability'] ?? null, + 'suggested_repair_class' => $fallback['suggested_repair_class'] ?? null, + 'suggested_primitive' => $fallback['suggested_primitive'] ?? null, + 'materialization_hint' => $fallback['materialization_hint'] ?? null, + 'tag' => $fallback['tag'] ?? null, + 'selector' => $fallback['selector'] ?? null, + 'pattern_family' => $fallback['pattern_family'] ?? null, + 'pattern_family_detail' => $fallback['pattern_family_detail'] ?? null, + 'source_selector' => $fallback['source_selector'] ?? null, + 'source_selector_specificity' => $fallback['source_selector_specificity'] ?? null, + 'parent_reason' => $fallback['parent_reason'] ?? null, + 'ancestor_reason' => $fallback['ancestor_reason'] ?? null, + 'suggested_generic_repair_class' => $fallback['suggested_generic_repair_class'] ?? null, + ); + } + } + + foreach ( $runtimeIslands as $island ) { + $diagnostics[] = array_filter(array( + 'code' => 'preserved_runtime_island', + 'message' => 'Runtime-dependent source markup was preserved as a bounded runtime island.', + 'source' => $transformerSource, + 'severity' => 'info', + 'conversion_classification' => 'runtime_island_preserved', + 'loss_class' => 'runtime_island_preserved', + 'diagnostic_class' => 'runtime_island_preserved', + 'suggested_repair_class' => 'preserve_runtime_island', + 'preservation_strategy' => $island['preservation_strategy'] ?? 'bounded_raw_html_runtime_island', + 'runtime_requirement' => $island['runtime_requirement'] ?? null, + 'kind' => $island['kind'] ?? null, + 'reason' => $island['preservation_reason'] ?? null, + 'tag' => $island['tag'] ?? null, + 'selector' => $island['selector'] ?? null, + 'pattern_family' => $island['pattern_family'] ?? null, + 'pattern_family_detail' => $island['pattern_family_detail'] ?? null, + 'source_selector' => $island['source_selector'] ?? null, + 'source_selector_specificity' => $island['source_selector_specificity'] ?? null, + 'parent_reason' => $island['parent_reason'] ?? null, + 'ancestor_reason' => $island['ancestor_reason'] ?? null, + 'suggested_generic_repair_class' => $island['suggested_generic_repair_class'] ?? null, + ), static fn (mixed $value): bool => null !== $value && '' !== $value); + } + + foreach ( $blockValidityReport['findings'] ?? array() as $finding ) { + if ( ! is_array($finding) ) { + continue; + } + + $diagnostics[] = array( + 'code' => 'wp_block_validity_' . (string) ($finding['code'] ?? 'warning'), + 'message' => (string) ($finding['summary'] ?? 'Generated block serialization may trigger WordPress block invalidity warnings.'), + 'source' => Runtime::class, + 'severity' => $finding['severity'] ?? 'warning', + 'block_name' => $finding['block_name'] ?? null, + 'path' => $finding['path'] ?? null, + ); + } + + foreach ( $semanticParityReport['findings'] ?? array() as $finding ) { + if ( ! is_array($finding) ) { + continue; + } + + $diagnostics[] = array( + 'code' => 'html_semantic_parity_' . (string) ($finding['code'] ?? 'warning'), + 'message' => (string) ($finding['summary'] ?? 'Generated blocks differ from source semantic structure.'), + 'source' => $transformerSource, + 'severity' => $finding['severity'] ?? 'warning', + 'selector' => $finding['selector'] ?? null, + ); + } + + return $diagnostics; + } +} diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index a5cd5278..7df78626 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -6,6 +6,7 @@ use Automattic\BlocksEngine\PhpTransformer\Contract\ConversionReportProjection; use Automattic\BlocksEngine\PhpTransformer\Contract\TransformationOptions; use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult; +use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Diagnostics\DiagnosticsCollector; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Patterns\AccordionPattern; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Patterns\ButtonsPattern; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Patterns\DetailsPattern; @@ -68,6 +69,8 @@ final class HtmlTransformer private readonly PatternRecognizerRegistry $patternRecognizers; + private readonly DiagnosticsCollector $diagnosticsCollector; + /** * @var array */ @@ -145,6 +148,7 @@ public function __construct(private readonly Runtime $runtime = new Runtime()) new AccordionPattern(), new NavigationPattern(), )); + $this->diagnosticsCollector = new DiagnosticsCollector(); } /** @@ -239,112 +243,15 @@ public function transform(string $html, array $options = array()): TransformerRe $serializedBlocks = $this->runtime->serializeBlocks($blocks); $blockValidityReport = $this->runtime->validateBlockSerialization($blocks); $semanticParityReport = $this->semanticParityReport($body, $blocks, $sourceProvenance, $html, (string) ($options['static_css'] ?? '')); - $diagnostics = array( - array( - 'code' => 'html_to_blocks_core_slice', - 'message' => 'Converted supported core text, layout, media, gallery, embed, file, table, button, shortcode, spacer, definition-list, details, navigation, safe inline SVG images, and wrapper elements; unsupported elements are reported as fallbacks.', - 'source' => self::class, - ), + $diagnostics = $this->diagnosticsCollector->collect( + self::class, + $this->scriptMetadata, + $fallbacks, + $this->runtimeIslands, + $blockValidityReport, + $semanticParityReport ); - foreach ( $this->scriptMetadata as $metadata ) { - $diagnostics[] = array( - 'code' => 'html_static_script_metadata', - 'message' => 'Static script data was preserved as bounded metadata and does not require client script execution.', - 'source' => self::class, - 'reason' => 'script_static_metadata', - 'tag' => 'script', - 'selector' => $metadata['selector'] ?? null, - 'script_role' => $metadata['script_role'] ?? null, - ); - } - - 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, - 'severity' => $fallback['severity'] ?? null, - 'conversion_classification' => $fallback['conversion_classification'] ?? null, - 'loss_class' => $fallback['loss_class'] ?? null, - 'diagnostic_class' => $fallback['diagnostic_class'] ?? null, - 'preservation_strategy' => $fallback['preservation_strategy'] ?? null, - 'runtime_requirement' => $fallback['runtime_requirement'] ?? null, - 'recoverability' => $fallback['recoverability'] ?? null, - 'actionability' => $fallback['actionability'] ?? null, - 'suggested_repair_class' => $fallback['suggested_repair_class'] ?? null, - 'suggested_primitive' => $fallback['suggested_primitive'] ?? null, - 'materialization_hint' => $fallback['materialization_hint'] ?? null, - 'tag' => $fallback['tag'] ?? null, - 'selector' => $fallback['selector'] ?? null, - 'pattern_family' => $fallback['pattern_family'] ?? null, - 'pattern_family_detail' => $fallback['pattern_family_detail'] ?? null, - 'source_selector' => $fallback['source_selector'] ?? null, - 'source_selector_specificity' => $fallback['source_selector_specificity'] ?? null, - 'parent_reason' => $fallback['parent_reason'] ?? null, - 'ancestor_reason' => $fallback['ancestor_reason'] ?? null, - 'suggested_generic_repair_class' => $fallback['suggested_generic_repair_class'] ?? null, - ); - } - } - - foreach ( $this->runtimeIslands as $island ) { - $diagnostics[] = array_filter(array( - 'code' => 'preserved_runtime_island', - 'message' => 'Runtime-dependent source markup was preserved as a bounded runtime island.', - 'source' => self::class, - 'severity' => 'info', - 'conversion_classification' => 'runtime_island_preserved', - 'loss_class' => 'runtime_island_preserved', - 'diagnostic_class' => 'runtime_island_preserved', - 'suggested_repair_class' => 'preserve_runtime_island', - 'preservation_strategy' => $island['preservation_strategy'] ?? 'bounded_raw_html_runtime_island', - 'runtime_requirement' => $island['runtime_requirement'] ?? null, - 'kind' => $island['kind'] ?? null, - 'reason' => $island['preservation_reason'] ?? null, - 'tag' => $island['tag'] ?? null, - 'selector' => $island['selector'] ?? null, - 'pattern_family' => $island['pattern_family'] ?? null, - 'pattern_family_detail' => $island['pattern_family_detail'] ?? null, - 'source_selector' => $island['source_selector'] ?? null, - 'source_selector_specificity' => $island['source_selector_specificity'] ?? null, - 'parent_reason' => $island['parent_reason'] ?? null, - 'ancestor_reason' => $island['ancestor_reason'] ?? null, - 'suggested_generic_repair_class' => $island['suggested_generic_repair_class'] ?? null, - ), static fn (mixed $value): bool => null !== $value && '' !== $value); - } - - foreach ( $blockValidityReport['findings'] ?? array() as $finding ) { - if ( ! is_array($finding) ) { - continue; - } - - $diagnostics[] = array( - 'code' => 'wp_block_validity_' . (string) ($finding['code'] ?? 'warning'), - 'message' => (string) ($finding['summary'] ?? 'Generated block serialization may trigger WordPress block invalidity warnings.'), - 'source' => Runtime::class, - 'severity' => $finding['severity'] ?? 'warning', - 'block_name' => $finding['block_name'] ?? null, - 'path' => $finding['path'] ?? null, - ); - } - - foreach ( $semanticParityReport['findings'] ?? array() as $finding ) { - if ( ! is_array($finding) ) { - continue; - } - - $diagnostics[] = array( - 'code' => 'html_semantic_parity_' . (string) ($finding['code'] ?? 'warning'), - 'message' => (string) ($finding['summary'] ?? 'Generated blocks differ from source semantic structure.'), - 'source' => self::class, - 'severity' => $finding['severity'] ?? 'warning', - 'selector' => $finding['selector'] ?? null, - ); - } - $metrics = $this->metrics($html, $blocks, $serializedBlocks, $fallbacks, $diagnostics, $startedAt); $nativeTargetBlocks = $this->runtime->availableCoreBlockNames(); $sourceReports = array(