From dc7b924bda2667d57bbb9f8be6cd158dcdca5c18 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 9 Nov 2026 11:40:51 -0500 Subject: [PATCH] Carry the source div marker on description-list rows Author type selectors such as `.intro dl div` are rewritten to the source div tag marker, but the description-list block rendered its row wrappers as plain divs, so the projected rule matched nothing. Persist the marker in each row wrapper's className attribute, the same source identity other converted elements carry, so saved markup stays editor-valid and the rewritten selector matches. Fixes #5235. --- .../DescriptionListBlockGenerator.php | 7 ++- .../description-list-source-tag-selector.php | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 php-transformer/tests/unit/description-list-source-tag-selector.php diff --git a/php-transformer/src/HtmlToBlocks/Generators/DescriptionListBlockGenerator.php b/php-transformer/src/HtmlToBlocks/Generators/DescriptionListBlockGenerator.php index 5b501c354..e57f1112f 100644 --- a/php-transformer/src/HtmlToBlocks/Generators/DescriptionListBlockGenerator.php +++ b/php-transformer/src/HtmlToBlocks/Generators/DescriptionListBlockGenerator.php @@ -340,8 +340,13 @@ private function item(DOMElement $element): array /** @return array */ private function wrapper(DOMElement $element): array { + // Author type selectors such as `dl div` are rewritten to the source + // tag marker; persist it on the row so the projected rule still matches. $wrapper = array_filter(array( - 'className' => $element->getAttribute('class'), + 'className' => SourceDom::mergeClassNames( + $element->getAttribute('class'), + $this->session?->authorSelectorProjectionState()->tagMarker($element->tagName) ?? '' + ), 'style' => $element->getAttribute('style'), ), static fn (mixed $value): bool => '' !== $value); $attributes = array(); diff --git a/php-transformer/tests/unit/description-list-source-tag-selector.php b/php-transformer/tests/unit/description-list-source-tag-selector.php new file mode 100644 index 000000000..3862a03df --- /dev/null +++ b/php-transformer/tests/unit/description-list-source-tag-selector.php @@ -0,0 +1,56 @@ +.intro dl div{border-top:1px solid #ccc}dl + div{color:red}' + . '
Color story
Spring · Summer
Forms
Plates · Bowls
After
'; +$result = ( new HtmlTransformer() )->transform($html)->toArray(); +$assert('pass' === ($result['source_reports']['wp_block_validity']['status'] ?? null), 'Description-list rows with source markers stay editor-valid'); + +$css = ''; +foreach ( $result['assets'] as $asset ) { + if ( 'author-css' === ($asset['source'] ?? '') ) { + $css .= (string) $asset['content']; + } +} +$projected = array(); +( new CssStylesheetTransformer() )->transformStyleRules($css, static function (string $selector, string $body) use (&$projected): string { + $projected[$body] = $selector; + return $selector . '{' . $body . '}'; +}); + +$dom = new DOMDocument(); +$dom->loadHTML('' . $result['serialized_blocks'] . '', LIBXML_NOERROR); +$xpath = new DOMXPath($dom); +$rows = iterator_to_array($xpath->query('//dl/div')); +$after = $xpath->query('//*[normalize-space(text())="After"]')->item(0); +$assert(2 === count($rows) && $after instanceof DOMElement, 'Rows and the following sibling are rendered'); + +foreach ( array( + 'descendant row selector' => array( 'body' => 'border-top:1px solid #ccc', 'rows' => true, 'after' => false ), + 'sibling selector stays rewritten' => array( 'body' => 'color:red', 'rows' => false, 'after' => true ), +) as $case => $expected ) { + $selector = $projected[$expected['body']] ?? ''; + $assert(1 !== preg_match('/(?:^|[\s>+~])div\b/', $selector), $case . ': the div type selector is rewritten to its source marker, got ' . $selector); + $parsed = CssSelectorMatcher::parse($selector); + $assert($parsed['supported'], $case . ': projected selector is matchable'); + foreach ( $rows as $row ) { + $assert($expected['rows'] === CssSelectorMatcher::matches($row, $parsed)['matches'], $case . ': row match is ' . var_export($expected['rows'], true) . ' for ' . $selector . ' on ' . $dom->saveHTML($row)); + } + $assert($expected['after'] === CssSelectorMatcher::matches($after, $parsed)['matches'], $case . ': following sibling match is ' . var_export($expected['after'], true)); +} + +echo "Description-list source tag selector passed\n";