From 0004113192b3dd5a8c944a26663f5e505a059dbe Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 28 Jun 2026 21:22:24 -0400 Subject: [PATCH] Respect flex-direction:column when converting flex containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A flex container with `flex-direction: column` / `column-reverse` is a vertical stack, but the HTML transformer converted any `display:flex` container with multiple children to a horizontal `core/columns` block. Hero stacks (eyebrow, title, subhead, buttons) rendered side-by-side instead of stacked — the corpus-diagnostics `layout_direction_misrecognition :: columns_from_vertical_flex` cluster. Key off the CSS `flex-direction` value, never fixture names or classes: - ColumnsPattern::looksLikeColumnsContainer now declines a container whose resolved style declares `display:flex` with a column main axis, so the host transformer routes it to the generic vertical `core/group` path. Row / row-reverse / default flex and grid layouts are untouched, so legitimate horizontal columns are preserved. - layoutAttribute emits an explicit `orientation: vertical` for column-flex containers. A `core/group` flex layout otherwise defaults to a horizontal Row, so without this the children would still render side-by-side. Adds parity fixtures for both directions (vertical flex -> vertical core/group; horizontal flex -> core/columns), updates the corpus-detector unit test to assert the live transformer now stacks vertically, and drives the columns_from_vertical_flex cluster from 21 to 0. Co-Authored-By: Claude Opus 4.8 --- .../HtmlToBlocks/Patterns/ColumnsPattern.php | 26 ++++++++++ .../Style/StyleResolutionTrait.php | 12 +++++ .../html-horizontal-flex-stays-columns.json | 33 +++++++++++++ ...ml-vertical-flex-column-becomes-group.json | 33 +++++++++++++ .../tests/unit/corpus-detectors.php | 47 ++++++++++++++++--- 5 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 php-transformer/tests/fixtures/parity/html-horizontal-flex-stays-columns.json create mode 100644 php-transformer/tests/fixtures/parity/html-vertical-flex-column-becomes-group.json diff --git a/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php b/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php index 54abfd09..9a9509d5 100644 --- a/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php +++ b/php-transformer/src/HtmlToBlocks/Patterns/ColumnsPattern.php @@ -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'))); diff --git a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php index ebf9dc56..3679d79e 100644 --- a/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php +++ b/php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php @@ -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) ) { diff --git a/php-transformer/tests/fixtures/parity/html-horizontal-flex-stays-columns.json b/php-transformer/tests/fixtures/parity/html-horizontal-flex-stays-columns.json new file mode 100644 index 00000000..a41510a6 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-horizontal-flex-stays-columns.json @@ -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": "

A

one

B

two

" + }, + "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": "