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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Classification;

use DOMElement;

final class FormControlClassifier
{
/**
* Native elements that participate in form control semantics.
*
* @var array<int, string>
*/
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<int, string>
*/
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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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'),
Expand Down
33 changes: 4 additions & 29 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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": "<main><label class=\"field-shell\">Quantity <input class=\"js-qty\" type=\"number\" name=\"quantity\" value=\"2\"></label></main>",
"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": "<label class=\"field-shell\">" },
{ "path": "serialized_blocks", "assert": "contains", "value": "<input class=\"js-qty\" type=\"number\" name=\"quantity\" value=\"2\">" },
{ "path": "serialized_blocks", "assert": "not_contains", "value": "Quantity: 2" },
{ "path": "coverage.0.fallback_count", "assert": "equals", "value": 0 }
]
}
Loading