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": "