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";