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
77 changes: 63 additions & 14 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3514,10 +3514,9 @@ private function readableFormBlockFromForm(DOMElement $form, bool $allowFormEven
continue;
}

$summary = $this->readableFormControlText($control);
if ( '' !== $summary ) {
$attrs = $this->isRuntimeDomTarget($control) ? $this->presentationAttributes($control) : array();
$contentBlocks[] = $this->createBlock('core/paragraph', array_merge($attrs, array( 'content' => $summary )), array(), $control);
$readableControlBlock = $this->readableFormControlBlockFromElement($control);
if ( null !== $readableControlBlock ) {
$contentBlocks[] = $readableControlBlock;
}
}

Expand Down Expand Up @@ -3597,6 +3596,13 @@ private function readableFormControlBlockFromElement(DOMElement $element): ?arra
return $this->createBlock('core/html', array( 'content' => $this->safeFallbackHtml($element) ), array(), $element);
}

if ( 'select' === $tagName ) {
$selectBlock = $this->readableSelectBlockFromElement($element);
if ( null !== $selectBlock ) {
return $selectBlock;
}
}

$summary = $this->readableFormControlText($element);
if ( '' === $summary ) {
return null;
Expand All @@ -3605,6 +3611,37 @@ private function readableFormControlBlockFromElement(DOMElement $element): ?arra
return $this->createBlock('core/paragraph', array( 'content' => $summary ), array(), $element);
}

/**
* @return array<string, mixed>|null
*/
private function readableSelectBlockFromElement(DOMElement $select): ?array
{
$label = $this->readableFormControlLabel($select);
$optionBlocks = array();

foreach ( $this->selectOptions($select) as $option ) {
$optionLabel = trim((string) ($option['label'] ?? ''));
if ( '' === $optionLabel ) {
continue;
}

if ( true === ($option['selected'] ?? false) ) {
$optionLabel .= ' (selected)';
}

$optionBlocks[] = $this->createBlock('core/list-item', array( 'content' => $this->runtime->escapeHtml($optionLabel) ));
}

if ( array() === $optionBlocks ) {
return null;
}

return $this->createBlock('core/group', array(), array(
$this->createBlock('core/paragraph', array( 'content' => $this->runtime->escapeHtml($label) ), array(), $select),
$this->createBlock('core/list', array(), $optionBlocks, $select),
), $select);
}

/**
* @return array<string, string>
*/
Expand Down Expand Up @@ -3638,16 +3675,7 @@ private function isReadableFormControl(DOMElement $control): bool

private function readableFormControlText(DOMElement $control): string
{
$label = $this->formControlLabel($control);
if ( '' === $label ) {
$label = $this->attr($control, 'aria-label');
}
if ( '' === $label ) {
$label = $this->attr($control, 'placeholder');
}
if ( '' === $label ) {
$label = $this->attr($control, 'name');
}
$label = $this->readableFormControlLabel($control);

$type = $this->formControlType($control);
if ( '' === $label ) {
Expand Down Expand Up @@ -3711,6 +3739,27 @@ private function readableFormControlText(DOMElement $control): string
return $this->runtime->escapeHtml($text);
}

private function readableFormControlLabel(DOMElement $control): string
{
$label = $this->formControlLabel($control);
if ( '' === $label ) {
$label = $this->attr($control, 'aria-label');
}
if ( '' === $label ) {
$label = $this->attr($control, 'placeholder');
}
if ( '' === $label ) {
$label = $this->attr($control, 'name');
}

$type = $this->formControlType($control);
if ( '' === $label ) {
return 'select' === $type ? 'Select option' : ucfirst($type);
}

return $label;
}

private function readableSubmitText(DOMElement $control): string
{
$text = trim(preg_replace('/\s+/', ' ', $control->textContent ?? '') ?? '');
Expand Down
12 changes: 12 additions & 0 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ function serialize_blocks(array $blocks): string
$assert(str_contains($rangeControlText, 'Density: 28'), 'range input summary preserves current value');
$assert(str_contains($rangeControlText, 'min 6, max 60, step 2'), 'range input summary preserves bounds');

$standaloneControls = ( new HtmlTransformer() )->transform(
'<main><input id="donation" type="number" aria-label="Custom donation amount" placeholder="Enter amount"><select aria-label="Sort products"><option selected>Featured</option><option>Price: Low to High</option></select><select class="js-sort-select" aria-label="Runtime sort"><option>Newest</option></select></main>',
array('runtime_dom_selectors' => array('.js-sort-select'))
)->toArray();
$standaloneControlBlocks = $standaloneControls['blocks'][0]['innerBlocks'] ?? array();
$assert(array() === ($standaloneControls['fallbacks'] ?? array()), 'standalone readable controls convert without unsupported-element fallback');
$assert('core/paragraph' === ($standaloneControlBlocks[0]['blockName'] ?? ''), 'standalone non-runtime input converts to readable paragraph');
$assert('core/list' === ($standaloneControlBlocks[1]['innerBlocks'][1]['blockName'] ?? ''), 'standalone non-runtime select options convert to readable list');
$assert('core/html' === ($standaloneControlBlocks[2]['blockName'] ?? ''), 'runtime-targeted select preserves native DOM markup');
$assert(str_contains((string) ($standaloneControls['serialized_blocks'] ?? ''), 'Featured (selected)'), 'readable select summary preserves selected option state');
$assert(str_contains((string) ($standaloneControls['serialized_blocks'] ?? ''), '<select class="js-sort-select"'), 'runtime-targeted select serialization preserves native element');

$buttonResult = ( new HtmlTransformer() )->transform(
'<main><a class="primary-button" href="#"><h3>Reserve now</h3><span aria-hidden="true"></span></a><button><strong>Call us</strong></button></main>'
)->toArray();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
"name": "html-standalone-form-control-readable-blocks",
"description": "Converts standalone non-runtime form controls into readable blocks while preserving runtime-targeted native controls.",
"source_reference": {
"repo": "php-transformer",
"path": "tests/fixtures/parity/html-standalone-form-control-readable-blocks.json",
"notes": "Covers generic SSI fixture matrix form-control fallbacks without fixture-specific selectors."
},
"legacy_comparison": {
"skip": true,
"reason": "Readable standalone form-control blocks are an upstream primitive with no legacy comparison."
},
"operation": "html_transformer.transform",
"input": {
"content": "<main><input type=\"number\" id=\"amount\" name=\"custom-amount\" class=\"form-input\" placeholder=\"Enter amount\" min=\"1\" aria-label=\"Custom donation amount\"><select class=\"sort-select\" aria-label=\"Sort products\"><option value=\"featured\" selected>Featured</option><option value=\"price-asc\">Price: Low to High</option><option value=\"price-desc\">Price: High to Low</option></select><select class=\"js-sort-select\" aria-label=\"Runtime sort\"><option>Newest</option></select></main>",
"options": {
"runtime_dom_selectors": [".js-sort-select"]
}
},
"expected_blocks": [
{ "path": "blocks.0", "name": "core/group" },
{ "path": "blocks.0.innerBlocks.0", "name": "core/paragraph" },
{ "path": "blocks.0.innerBlocks.1", "name": "core/group" },
{ "path": "blocks.0.innerBlocks.1.innerBlocks.1", "name": "core/list" },
{ "path": "blocks.0.innerBlocks.2", "name": "core/html" }
],
"expected_fallbacks": [],
"expect": [
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "fallbacks", "assert": "count", "count": 0 },
{ "path": "serialized_blocks", "assert": "contains", "value": "Custom donation amount: Enter amount" },
{ "path": "serialized_blocks", "assert": "contains", "value": "Sort products" },
{ "path": "serialized_blocks", "assert": "contains", "value": "Featured (selected)" },
{ "path": "serialized_blocks", "assert": "contains", "value": "Price: Low to High" },
{ "path": "serialized_blocks", "assert": "contains", "value": "<select class=\"js-sort-select\" aria-label=\"Runtime sort\">" },
{ "path": "serialized_blocks", "assert": "not_contains", "value": "<select class=\"sort-select\"" },
{ "path": "coverage.0.fallback_count", "assert": "equals", "value": 0 }
]
}
Loading