diff --git a/php-transformer/src/HtmlToBlocks/Classification/FormControlClassifier.php b/php-transformer/src/HtmlToBlocks/Classification/FormControlClassifier.php new file mode 100644 index 00000000..4e006652 --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/Classification/FormControlClassifier.php @@ -0,0 +1,65 @@ + + */ + public const CONTROL_TAGS = array( 'button', 'input', 'select', 'textarea' ); + + /** + * Controls that collect user-entered values rather than only submitting or + * carrying hidden/runtime state. + * + * @var array + */ + public const DATA_ENTRY_TAGS = array( 'input', 'select', 'textarea' ); + + public static function isControlElement(DOMElement $element): bool + { + return in_array(strtolower($element->tagName), self::CONTROL_TAGS, true); + } + + public static function controlType(DOMElement $control): string + { + $tagName = strtolower($control->tagName); + if ( 'input' === $tagName ) { + $type = strtolower(trim($control->hasAttribute('type') ? $control->getAttribute('type') : '')); + return '' !== $type ? $type : 'text'; + } + if ( 'button' === $tagName ) { + $type = strtolower(trim($control->hasAttribute('type') ? $control->getAttribute('type') : '')); + return '' !== $type ? $type : 'submit'; + } + if ( 'select' === $tagName && $control->hasAttribute('multiple') ) { + return 'select-multiple'; + } + + return $tagName; + } + + public static function isDataEntryControl(DOMElement $control): bool + { + $tagName = strtolower($control->tagName); + if ( in_array($tagName, array( 'select', 'textarea' ), true) ) { + return true; + } + + if ( 'input' !== $tagName ) { + return false; + } + + return ! in_array( + self::controlType($control), + array( 'submit', 'reset', 'button', 'image', 'hidden', 'file' ), + true + ); + } +} diff --git a/php-transformer/src/HtmlToBlocks/Classification/SubtreeClassifier.php b/php-transformer/src/HtmlToBlocks/Classification/SubtreeClassifier.php index 039998c5..880bb2c8 100644 --- a/php-transformer/src/HtmlToBlocks/Classification/SubtreeClassifier.php +++ b/php-transformer/src/HtmlToBlocks/Classification/SubtreeClassifier.php @@ -61,8 +61,6 @@ final class SubtreeClassifier 'accordion', ); - private const FORM_CONTROL_TAGS = array( 'input', 'select', 'textarea' ); - public function classify(DOMElement $element, ?ClassificationContext $context = null): ClassificationResult { $context = $context ?? new ClassificationContext(); @@ -262,7 +260,7 @@ private function gatherSignals(DOMElement $element, ClassificationContext $conte return array( // DOM-derived structural signals. - 'form_controls' => $this->countDescendants($elements, self::FORM_CONTROL_TAGS), + 'form_controls' => $this->countDescendants($elements, FormControlClassifier::DATA_ENTRY_TAGS), 'submit_or_form' => $this->hasTag($elements, array( 'form' )) || $this->hasSubmit($elements), 'canvas' => $this->hasTag($elements, array( 'canvas' )), 'contenteditable' => $this->hasAttributeNamed($elements, 'contenteditable'), diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 0fb5d73c..a49efb7d 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\Classification\FormControlClassifier; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Diagnostics\ContentRoundTripReporter; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Diagnostics\DiagnosticsCollector; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Diagnostics\FallbackEmitter; @@ -5523,20 +5524,7 @@ private function formHasDataEntryControls(DOMElement $form): bool */ private function isDataEntryControl(DOMElement $control): bool { - $tagName = strtolower($control->tagName); - if ( in_array($tagName, array( 'select', 'textarea' ), true) ) { - return true; - } - - if ( 'input' !== $tagName ) { - return false; - } - - return ! in_array( - $this->formControlType($control), - array( 'submit', 'reset', 'button', 'image', 'hidden', 'file' ), - true - ); + return FormControlClassifier::isDataEntryControl($control); } private function isReadableFormControl(DOMElement $control): bool @@ -5800,25 +5788,12 @@ private function formControlMetadata(DOMElement $control): array private function isFormControlElement(DOMElement $element): bool { - return in_array(strtolower($element->tagName), array( 'button', 'input', 'select', 'textarea' ), true); + return FormControlClassifier::isControlElement($element); } private function formControlType(DOMElement $control): string { - $tagName = strtolower($control->tagName); - if ( 'input' === $tagName ) { - $type = strtolower(trim($this->attr($control, 'type'))); - return '' !== $type ? $type : 'text'; - } - if ( 'button' === $tagName ) { - $type = strtolower(trim($this->attr($control, 'type'))); - return '' !== $type ? $type : 'submit'; - } - if ( 'select' === $tagName && $control->hasAttribute('multiple') ) { - return 'select-multiple'; - } - - return $tagName; + return FormControlClassifier::controlType($control); } private function formControlLabel(DOMElement $control): string diff --git a/php-transformer/tests/fixtures/parity/html-runtime-label-control-island.json b/php-transformer/tests/fixtures/parity/html-runtime-label-control-island.json new file mode 100644 index 00000000..6af4bdf3 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-runtime-label-control-island.json @@ -0,0 +1,38 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-runtime-label-control-island", + "description": "Preserves a label shell around a runtime-targeted control instead of flattening it to readable static text.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-runtime-label-control-island.json", + "notes": "Generic nested control-shell coverage for runtime DOM target handling." + }, + "legacy_comparison": { + "skip": true, + "reason": "Runtime control shell preservation is an upstream primitive with no legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "
", + "options": { + "runtime_dom_selectors": [".js-qty"] + } + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/html" } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "source_reports.html.runtime_islands", "assert": "count", "count": 1 }, + { "path": "source_reports.html.runtime_islands.0.kind", "assert": "equals", "value": "control" }, + { "path": "source_reports.html.runtime_islands.0.selector", "assert": "equals", "value": ".js-qty" }, + { "path": "source_reports.html.runtime_islands.0.control.name", "assert": "equals", "value": "quantity" }, + { "path": "source_reports.html.runtime_islands.0.control.type", "assert": "equals", "value": "number" }, + { "path": "serialized_blocks", "assert": "contains", "value": "