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
26 changes: 26 additions & 0 deletions php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,38 @@ private function looksLikeColumnsContainer(DOMElement $element): bool
return false;
}

// A genuinely vertical flex container (display:flex with
// flex-direction: column / column-reverse) lays its children out in a
// vertical stack. Emitting core/columns would render them horizontally —
// the wrong direction. Decline here so the host transformer routes the
// element to a vertical core/group instead, preserving its classes and
// styles. Horizontal flex (row / row-reverse / default) and grid layouts
// are unaffected, so legitimate horizontal columns are never disturbed.
if ( $this->isVerticalFlexContainer($style) ) {
return false;
}

return (bool) preg_match('/(?:^|[\s_-])columns?(?:$|[\s_-])/', $className)
|| ( $this->looksLikeSplitLayout($element) && 1 < $this->directElementChildCount($element) )
|| ( $this->looksLikeDocumentationLayout($element) && $this->hasSidebarAndContentChildren($element) )
|| preg_match('/(?:^|;)\s*(?:display\s*:\s*(?:inline-)?flex|grid-template-columns\s*:)/', $style);
}

/**
* True when the resolved style declares a flex container whose main axis is
* vertical (flex-direction: column / column-reverse). flex-direction only has
* meaning on a flex container, so both display:flex and the column direction
* are required before redirecting away from horizontal columns.
*/
private function isVerticalFlexContainer(string $style): bool
{
if ( ! preg_match('/(?:^|;)\s*display\s*:\s*(?:inline-)?flex\b/', $style) ) {
return false;
}

return (bool) preg_match('/(?:^|;)\s*flex-direction\s*:\s*column(?:-reverse)?\b/', $style);
}

private function looksLikeSplitLayout(DOMElement $element): bool
{
$name = strtolower(trim($this->attr($element, 'class') . ' ' . $this->attr($element, 'id')));
Expand Down
12 changes: 12 additions & 0 deletions php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,18 @@ private function layoutAttribute(DOMElement $element, string $mergedStyle = ''):

$style = strtolower('' !== trim($mergedStyle) ? $mergedStyle : $this->attr($element, 'style'));
if ( preg_match('/(?:^|;)\s*display\s*:\s*(inline-)?flex\b/', $style) ) {
// flex-direction: column / column-reverse is a vertical main axis. A
// core/group flex layout defaults to a horizontal Row, so the
// orientation must be made explicit or the children render
// side-by-side instead of stacked. Row / row-reverse / default flex
// keeps the implicit horizontal orientation.
if ( preg_match('/(?:^|;)\s*flex-direction\s*:\s*column(?:-reverse)?\b/', $style) ) {
return array(
'type' => 'flex',
'orientation' => 'vertical',
);
}

return array( 'type' => 'flex' );
}
if ( preg_match('/(?:^|;)\s*display\s*:\s*(inline-)?grid\b/', $style) ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
"name": "html-horizontal-flex-stays-columns",
"description": "A flex container with the default (row) main axis is a genuine horizontal layout, so it stays a core/columns block. Guards that the flex-direction:column redirect never disturbs legitimate horizontal column layouts.",
"source_reference": {
"repo": "php-transformer",
"path": "tests/fixtures/parity/html-horizontal-flex-stays-columns.json",
"notes": "Same display:flex container shape as the vertical fixture but without flex-direction:column. The ColumnsPattern flex-direction guard does not fire, so the horizontal columns layout is preserved unchanged."
},
"legacy_comparison": {
"skip": true,
"reason": "Covers current PHP transformer columns recognizer behavior; no downstream legacy comparison."
},
"operation": "html_transformer.transform",
"input": {
"content": "<div class=\"cols\" style=\"display:flex\"><div class=\"c\"><h2>A</h2><p>one</p></div><div class=\"c\"><h2>B</h2><p>two</p></div></div>"
},
"expected_blocks": [
{ "path": "blocks.0", "name": "core/columns", "attrs": { "className": "cols", "layout": { "type": "flex" } } },
{ "path": "blocks.0.innerBlocks.0", "name": "core/column", "attrs": { "className": "c" } },
{ "path": "blocks.0.innerBlocks.0.innerBlocks.0", "name": "core/heading", "attrs": { "content": "A", "level": 2 } },
{ "path": "blocks.0.innerBlocks.0.innerBlocks.1", "name": "core/paragraph", "attrs": { "content": "one" } },
{ "path": "blocks.0.innerBlocks.1", "name": "core/column", "attrs": { "className": "c" } },
{ "path": "blocks.0.innerBlocks.1.innerBlocks.0", "name": "core/heading", "attrs": { "content": "B", "level": 2 } },
{ "path": "blocks.0.innerBlocks.1.innerBlocks.1", "name": "core/paragraph", "attrs": { "content": "two" } }
],
"expect": [
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "blocks", "assert": "count", "count": 1 },
{ "path": "blocks.0.innerBlocks", "assert": "count", "count": 2 },
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:columns {\"className\":\"cols\",\"layout\":{\"type\":\"flex\"}}" }
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"schema": "blocks-engine/php-transformer/parity-fixture/v1",
"name": "html-vertical-flex-column-becomes-group",
"description": "A flex container with flex-direction:column is a vertical stack, so it converts to a vertical core/group (layout type flex, orientation vertical) rather than a horizontal core/columns. This is the regression guard for layout_direction_misrecognition :: columns_from_vertical_flex — a hero stack (eyebrow, title, subhead, buttons) must render stacked, not side-by-side.",
"source_reference": {
"repo": "php-transformer",
"path": "tests/fixtures/parity/html-vertical-flex-column-becomes-group.json",
"notes": "Keys solely off the CSS flex-direction:column value (never class/fixture names). ColumnsPattern declines vertical flex so the host routes to core/group, and layoutAttribute emits an explicit orientation:vertical so the flex group stacks instead of defaulting to a horizontal Row."
},
"legacy_comparison": {
"skip": true,
"reason": "Covers current PHP transformer flex-direction-aware layout behavior; no downstream legacy comparison."
},
"operation": "html_transformer.transform",
"input": {
"content": "<div class=\"hero\" style=\"display:flex;flex-direction:column;gap:1rem\"><span class=\"eyebrow\">New</span><h1>Title</h1><p>Subhead</p><a class=\"btn\" href=\"/x\">Go</a></div>"
},
"expected_blocks": [
{ "path": "blocks.0", "name": "core/group", "attrs": { "className": "hero", "layout": { "type": "flex", "orientation": "vertical" } } },
{ "path": "blocks.0.innerBlocks.0", "name": "core/paragraph", "attrs": { "content": "New", "className": "eyebrow" } },
{ "path": "blocks.0.innerBlocks.1", "name": "core/heading", "attrs": { "content": "Title", "level": 1 } },
{ "path": "blocks.0.innerBlocks.2", "name": "core/paragraph", "attrs": { "content": "Subhead" } },
{ "path": "blocks.0.innerBlocks.3", "name": "core/buttons" },
{ "path": "blocks.0.innerBlocks.3.innerBlocks.0", "name": "core/button", "attrs": { "className": "btn", "text": "Go", "url": "/x" } }
],
"expect": [
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "blocks", "assert": "count", "count": 1 },
{ "path": "blocks.0.innerBlocks", "assert": "count", "count": 4 },
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:group {\"className\":\"hero\",\"layout\":{\"type\":\"flex\",\"orientation\":\"vertical\"}}" },
{ "path": "serialized_blocks", "assert": "not_contains", "value": "wp:columns" }
]
}
47 changes: 40 additions & 7 deletions php-transformer/tests/unit/corpus-detectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,16 @@
$assert(2 === count($diagSvg), '3d: inline-svg fallback diagnostics route into svg_content_lost', 'got ' . count($diagSvg));

// ---------------------------------------------------------------------------
// 4. Layout-direction misrecognition: a vertical flex container that converts to
// core/columns flags columns_from_vertical_flex; horizontal flex does not.
// 4. Layout-direction misrecognition: the detector flags a vertical flex
// container that converts to core/columns; horizontal flex does not. The
// verifier is stubbed here so the detector logic is exercised independently
// of the live transformer (whose vertical-flex routing is asserted in 4e).
// ---------------------------------------------------------------------------
$verticalFlex = '<div style="display:flex; flex-direction:column; gap:1rem; max-width:760px;">'
. '<p>One</p><p>Two</p><p>Three</p></div>';
$layout = CorpusDetectors::layoutDirectionMisrecognition(
$verticalFlex,
static function (string $fragment): bool {
$blocks = ( new HtmlTransformer() )->transform($fragment, array())->toArray()['blocks'] ?? array();

return is_array($blocks[0] ?? null) && 'core/columns' === ($blocks[0]['blockName'] ?? '');
}
static fn (string $fragment): bool => true
);
$assert(1 === count($layout), '4: a vertical-flex container that becomes core/columns is flagged', 'got ' . count($layout));
$assert(
Expand All @@ -195,6 +193,41 @@ static function (string $fragment): bool {
);
$assert(0 === count($vetoed), '4d: a vertical-flex candidate that does not become columns is vetoed', 'got ' . count($vetoed));

// 4e. Regression guard for the fix: the live transformer must route a vertical
// flex container (display:flex; flex-direction:column) to a vertical
// core/group, not a horizontal core/columns. With the real transformer as
// the verifier the detector therefore finds nothing.
$verticalBlocks = ( new HtmlTransformer() )->transform($verticalFlex, array())->toArray()['blocks'] ?? array();
$assert(
'core/group' === ($verticalBlocks[0]['blockName'] ?? ''),
'4e: live transformer emits core/group for a vertical flex container',
(string) ($verticalBlocks[0]['blockName'] ?? '(none)')
);
$assert(
'vertical' === ($verticalBlocks[0]['attrs']['layout']['orientation'] ?? ''),
'4e: the vertical flex group carries an explicit vertical flex orientation',
(string) ($verticalBlocks[0]['attrs']['layout']['orientation'] ?? '(none)')
);
$liveLayout = CorpusDetectors::layoutDirectionMisrecognition(
$verticalFlex,
static function (string $fragment): bool {
$blocks = ( new HtmlTransformer() )->transform($fragment, array())->toArray()['blocks'] ?? array();

return is_array($blocks[0] ?? null) && 'core/columns' === ($blocks[0]['blockName'] ?? '');
}
);
$assert(0 === count($liveLayout), '4e: detector reports no misrecognition once the transformer stacks the flex column vertically', 'got ' . count($liveLayout));

// 4f. Horizontal flex still becomes core/columns — the fix must not disturb
// legitimate horizontal column layouts.
$horizontalColumns = '<div style="display:flex; gap:1rem;"><div><h2>A</h2><p>one</p></div><div><h2>B</h2><p>two</p></div></div>';
$horizontalBlocks = ( new HtmlTransformer() )->transform($horizontalColumns, array())->toArray()['blocks'] ?? array();
$assert(
'core/columns' === ($horizontalBlocks[0]['blockName'] ?? ''),
'4f: live transformer keeps horizontal flex as core/columns',
(string) ($horizontalBlocks[0]['blockName'] ?? '(none)')
);

// ---------------------------------------------------------------------------
// 5. Severity ranking and cluster keys.
// ---------------------------------------------------------------------------
Expand Down
Loading