diff --git a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php index 33b69c23d..4ee947d31 100644 --- a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php +++ b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php @@ -1365,7 +1365,9 @@ private function withResolvedNonFlexNavigationLayout(array $attrs, DOMElement $e * layout the host is core's flex row, and items the source stacked full * width shrink to their min-width instead. When neither the host nor its * list declares a display, the source list is a block box; the items prove - * the stack only when every one declares a block-level display. Floated + * the stack only when the cascade gives every one a block-level display + * (ranked by specificity: `.menu>ul li{display:block}` beats a later + * `.item{display:flex}`, as it does in the browser). Floated * block items still need flow: a flex row ignores the float that the * projected item rule carries. Without item evidence core's default stays. * @@ -1375,8 +1377,8 @@ private function withResolvedNonFlexNavigationLayout(array $attrs, DOMElement $e private function withStackedBlockListLayout(array $attrs, DOMElement $host, ?DOMElement $list, ?NavigationPatternContext $navigationContext): array { if ( null === $navigationContext || ! $list instanceof DOMElement || is_array($attrs['layout'] ?? null) - || '' !== trim($navigationContext->resolvedDisplay($host)) - || '' !== trim($navigationContext->resolvedDisplay($list)) + || '' !== $navigationContext->cascadeDisplay($host) + || '' !== $navigationContext->cascadeDisplay($list) ) { return $attrs; } @@ -1386,8 +1388,7 @@ private function withStackedBlockListLayout(array $attrs, DOMElement $host, ?DOM if ( ! $child instanceof DOMElement || 'li' !== strtolower($child->tagName) ) { continue; } - $display = strtolower(trim($navigationContext->resolvedDisplay($child))); - if ( ! in_array($display, array( 'block', 'list-item', 'flow-root' ), true) ) { + if ( ! in_array($navigationContext->cascadeDisplay($child), array( 'block', 'list-item', 'flow-root' ), true) ) { return $attrs; } ++$items; diff --git a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPatternContext.php b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPatternContext.php index 6d70234b1..b24ddac43 100644 --- a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPatternContext.php +++ b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPatternContext.php @@ -427,6 +427,22 @@ public function resolvedDisplay(DOMElement $element): string return $this->styleResolver?->resolvedConditionalDisplay($element) ?? ''; } + /** + * The display the browser cascade gives the element at the reference + * viewport: static and applying conditional rules, ranked by specificity + * and then source order. A recognition signal only, never projected. + */ + public function cascadeDisplay(DOMElement $element): string + { + if ( ! $this->styleResolver instanceof StyleResolver + || 1 !== preg_match('/(?:^|;)\s*display\s*:\s*([^;]+)/i', $this->styleResolver->controlSurfaceResolvedStyle($element), $match) + ) { + return ''; + } + + return strtolower(trim((string) preg_replace('/\s*!important\s*$/i', '', trim($match[1])))); + } + /** Compare the authored display contracts, including breakpoints outside the reference viewport. */ public function hasDistinctDisplayBoxes(DOMElement $landmark, DOMElement $list): bool { diff --git a/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json b/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json index eab861a75..c7124b00c 100644 --- a/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json +++ b/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json @@ -275,7 +275,7 @@ { "id": "2448c3c2ebf7c03839a1ad542beec24efd2a805c", "entry": "HtmlTransformer::transform", - "expected_sha256": "71634f402ac1c35b2b58aa7a84368a05df1eefae88fd57710e8613f985781b01", + "expected_sha256": "34bb1ec6191c0dfbdfbcfe8dbadb24b63b3a4f9e0e0d168d0d28165b34452e4f", "outcome": "output" }, { @@ -323,7 +323,7 @@ { "id": "2890e8f1ed5ae188e261c80d3c9a1ad1e1928cd2", "entry": "HtmlTransformer::transform", - "expected_sha256": "e6655c9ad88b524d599cffa2b6c263ce641228df86d108f33ad95fbc844dd1d5", + "expected_sha256": "85c3db03ccb2d40a35a28b76f653930c86141ca0f465a8a02241b06d03ca030f", "outcome": "output" }, { @@ -539,7 +539,7 @@ { "id": "448ead36f649a72e98e642cebc29f9f71af779f5", "entry": "HtmlTransformer::transform", - "expected_sha256": "20ec6a68f6359544ce42bda29066f636415026d7d083f93e48f1fe038d38cb47", + "expected_sha256": "9d22d09441c07ef4697e28793f495c136fcbf0fbea1e671713742f8eea12e959", "outcome": "output" }, { @@ -569,7 +569,7 @@ { "id": "46f79246a3c128cd0672e296d1164e4d6882750a", "entry": "HtmlTransformer::transform", - "expected_sha256": "56912dec0b9c4149ba7870877627087af49f6c3f16606399fae7994830a7fcdb", + "expected_sha256": "79b360ac60c13dc82ec6e71ffe4002e2c612154e58896d64e68a9698efb21a6c", "outcome": "output" }, { @@ -1349,7 +1349,7 @@ { "id": "a5a08a8615d2682ce0d38a5053bcec23199be36f", "entry": "HtmlTransformer::transform", - "expected_sha256": "2c30e166b2f434a212b5dafda6323998416b05bf6665220bf3f80f753e7e849c", + "expected_sha256": "e49df13f9e79748eec3621044d15f1303522e2a0623e8320a95b251345eb7dcd", "outcome": "output" }, { @@ -1439,7 +1439,7 @@ { "id": "b550beb0271acdd852382b101d2fc6653b1cb107", "entry": "HtmlTransformer::transform", - "expected_sha256": "880ca1ad2c50e4576c45f90f34421f93ea56faf32edd095f1683ea3be82feab5", + "expected_sha256": "af43162504a5b754c023e2f286a5a62b3679530b4ca2b8b2710f2a40acf25ac8", "outcome": "output" }, { @@ -1607,7 +1607,7 @@ { "id": "c7fff215fed715e93aa155c4e0e5e31d7db084b1", "entry": "HtmlTransformer::transform", - "expected_sha256": "7351d0b1b79718f1dc16609a6913229a3c8003202930d55efc0c1a73daa58c4e", + "expected_sha256": "27eaba8ed0fb4473fee7243c1aa3afbbceb78d3a4c3d9f433043f37e2bddc69a", "outcome": "output" }, { @@ -1817,7 +1817,7 @@ { "id": "e2e35d0c5c02ff06b23bc87fb59bc585201bc46f", "entry": "HtmlTransformer::transform", - "expected_sha256": "4f74bda305109c284db76c79e2e2af5e6996240ac2d5d4a1e486c54404ba9550", + "expected_sha256": "6a675cb25b3b2a350fbbaa7d435a4164d764e6497f476bba50cb3b3d54c78188", "outcome": "output" }, { @@ -1937,7 +1937,7 @@ { "id": "f025cf281f15d03cf61d0c76c1f4bd7adc3b257e", "entry": "HtmlTransformer::transform", - "expected_sha256": "bcad2d15a0f038f54bec7fb58bc77b7de94c821d9ac809ca2261862990766929", + "expected_sha256": "90a312d2875f21338941beb332ff46a640c17d7807941b11e09772688983269c", "outcome": "output" }, { diff --git a/php-transformer/tests/unit/navigation-stacked-block-list-layout.php b/php-transformer/tests/unit/navigation-stacked-block-list-layout.php index cc0aecc61..0ee82d1f7 100644 --- a/php-transformer/tests/unit/navigation-stacked-block-list-layout.php +++ b/php-transformer/tests/unit/navigation-stacked-block-list-layout.php @@ -49,7 +49,8 @@ $menu = static fn (string $scope, string $itemRule): string => $scope . '.menu{direction:ltr}' . $scope . '.menu>ul{box-sizing:border-box;width:100%}' . $scope . '.menu>ul li{' . $itemRule . '}' - . $scope . '.item{min-width:100px}' + // Later in source order but weaker: the typed item rule still wins. + . $scope . '.item{display:flex;min-width:100px}' . $scope . '.item .box{display:grid;height:45px}'; $scope = ':where([data-dla-device-document="mobile"]) '; @@ -73,6 +74,7 @@ ); $assert(null === $layout($menu('', 'display:block') . '.menu>ul{display:flex}'), 'a flex list keeps core\'s row'); $assert(null === $layout('.menu{direction:ltr}'), 'a menu without item display evidence keeps core\'s default'); +$assert(null === $layout('.item{display:flex}'), 'flex items keep core\'s row'); if ( 0 < $failures ) { fwrite(STDERR, "Navigation stacked block list layout: {$failures} failed, {$passes} passed\n"); diff --git a/php-transformer/tools/capture-evidence-equivalence/README.md b/php-transformer/tools/capture-evidence-equivalence/README.md index 05c3b3a1f..995b73125 100644 --- a/php-transformer/tools/capture-evidence-equivalence/README.md +++ b/php-transformer/tools/capture-evidence-equivalence/README.md @@ -71,7 +71,9 @@ replay by case: on the nav or its list, records `"layout":{"type":"default"}`. That adds the attribute in `46f79246` and `e2e35d0c`. The extra display lookups only move the style-rule candidate cache counters in `2448c3c2`, `2890e8f1`, - `448ead36`, `a5a08a86`, `b550beb0`, `c7fff215` and `f025cf28`. + `448ead36`, `a5a08a86`, `b550beb0`, `c7fff215` and `f025cf28`. Ranking + the item displays by specificity (#2726 follow-up) moves those counters + again in all nine cases; the output is otherwise unchanged. - **Self-placed modal box reset** (#2726). The captured-dialog block stylesheet gains the zero-specificity reset for a modal whose source panel positions itself. Only the emitted `style.css` text and the hashes derived