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
125 changes: 106 additions & 19 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php

Large diffs are not rendered by default.

164 changes: 159 additions & 5 deletions php-transformer/src/Css/CssSelectorListCompactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ final class CssSelectorListCompactor
private static array $stylesheets = array();
private static int $cachedBytes = 0;

public static function stylesheet(string $css): string
/**
* @param bool $markerLists Also merge arms that differ only by one
* page-local marker class in a `:where()` into `:where(.m1,.m2,…)`.
* Used for the site-wide sheet, where routes contribute marker arms.
*/
public static function stylesheet(string $css, bool $markerLists = false): string
{
$key = hash('sha256', $css);
$key = hash('sha256', $css) . ($markerLists ? ':m' : '');
if ( isset(self::$stylesheets[ $key ]) ) {
return self::$stylesheets[ $key ];
}
$result = self::compactStylesheet($css);
$result = self::compactStylesheet($css, $markerLists);
if ( count(self::$stylesheets) >= 128 || self::$cachedBytes + strlen($result) > 2097152 ) {
self::$stylesheets = array();
self::$cachedBytes = 0;
Expand All @@ -28,12 +33,161 @@ public static function stylesheet(string $css): string
return $result;
}

private static function compactStylesheet(string $css): string
private static function compactStylesheet(string $css, bool $markerLists = false): string
{
if ( ! str_contains($css, ',') || preg_match('/@namespace\b/i', $css) ) {
return $css;
}
return (new CssStylesheetTransformer())->transform($css, static fn(string $prelude): string => self::compact($prelude));
return (new CssStylesheetTransformer())->transform($css, static fn(string $prelude): string => self::compact($markerLists ? self::mergeMarkerLists($prelude) : $prelude));
}

/**
* Merge arms identical but for the page-local marker class inside one
* top-level `:where(.marker)`: `X:where(.m1)Y,X:where(.m2)Y` becomes
* `X:where(.m1,.m2)Y`. `:where()` weighs nothing and matches when any
* argument does, and every argument is a plain class, so the list is
* equivalent. expand() splits such lists back into arms.
*/
public static function mergeMarkerLists(string $prelude): string
{
if ( ! str_contains($prelude, ',') || ! preg_match(self::PAGE_MARKER, $prelude) ) {
return $prelude;
}
$arms = CssStylesheetTransformer::splitSelectorList($prelude);
if ( null === $arms || count($arms) < 2 ) {
return $prelude;
}
$groups = array();
$order = array();
foreach ( $arms as $index => $arm ) {
$arm = trim($arm);
$key = "\1" . $index;
$markers = array();
if ( ! preg_match('/::|:(?:before|after|first-line|first-letter)\b/i', $arm) && ! str_contains($arm, '/*') && false === strpbrk($arm, '\\"\'') && preg_match_all(self::MARKER_WHERE, $arm, $matches, PREG_OFFSET_CAPTURE) ) {
foreach ( $matches[0] as $index2 => [$text, $offset] ) {
$before = substr($arm, 0, $offset);
if ( substr_count($before, '(') !== substr_count($before, ')') ) {
continue;
}
$key = substr($arm, 0, $offset) . "\0" . substr($arm, $offset + strlen($text));
$markers = explode(',', $matches[1][ $index2 ][0]);
break;
}
}
if ( ! isset($groups[ $key ]) ) {
$order[] = $key;
$groups[ $key ] = array('arm' => $arm, 'markers' => array());
}
foreach ( $markers as $marker ) {
$groups[ $key ]['markers'][ $marker ] = true;
}
}
if ( count($order) === count($arms) ) {
return $prelude;
}
$output = array();
foreach ( $order as $key ) {
$group = $groups[ $key ];
if ( array() === $group['markers'] ) {
$output[] = $group['arm'];
continue;
}
[$prefix, $suffix] = explode("\0", $key, 2);
$output[] = $prefix . ':where(' . implode(',', array_keys($group['markers'])) . ')' . $suffix;
}
return implode(',', $output);
}

/** A `:where()` whose arguments are page-local marker classes only. */
private const MARKER_WHERE = '/:where\(((?:\.blocks-engine-[a-z-]+-[a-f0-9]{12}-[0-9]+,)*\.blocks-engine-[a-z-]+-[a-f0-9]{12}-[0-9]+)\)/';

/**
* Inverse of compact() for page-local marker lists: expand every whole
* compound `:is(m1,m2,…)` whose alternatives share one specificity in the
* strict grammar, and at least one names a page-local block marker, back
* into one arm per alternative. Equal specificity and strict alternatives
* make the arm list cascade-equivalent to the packed form, so two pages'
* projections of one source rule align arm by arm before reconciliation.
*/
public static function expand(string $prelude): string
{
if ( ! preg_match(self::PAGE_MARKER, $prelude) ) {
return $prelude;
}
$arms = CssStylesheetTransformer::splitSelectorList($prelude);
if ( null === $arms ) {
return $prelude;
}
$output = array();
$changed = false;
while ( array() !== $arms ) {
$arm = trim((string) array_shift($arms));
$expanded = self::expandArm($arm);
if ( null === $expanded ) {
$output[] = $arm;
continue;
}
$changed = true;
array_unshift($arms, ...$expanded);
}
return $changed ? implode(',', $output) : $prelude;
}

/** Page-local block marker class (`blocks-engine-<kind>-<hash>-<n>`). */
public const PAGE_MARKER = '/(?<![a-zA-Z0-9_-])blocks-engine-[a-z-]+-[a-f0-9]{12}-[0-9]+(?![a-zA-Z0-9_-])/';

/** @return list<string>|null */
private static function expandArm(string $arm): ?array
{
// Richtext carriers use style attributes rather than marker classes.
$identity = 'blocks-engine-richtext-[a-f0-9]{12}-[0-9]+';
$richtext = '(?:mark\\[style\\*="--blocks-engine-richtext-marker:' . $identity . ';"\\]|mark\\[style\\$="--blocks-engine-richtext-marker:' . $identity . '"\\]|span\\[data-blocks-engine-richtext-marker="' . $identity . '"\\])';
if ( ! str_contains($arm, '\\') && preg_match_all('/:where\\(((?:' . $richtext . ',)+' . $richtext . ')\\)/', $arm, $lists, PREG_OFFSET_CAPTURE) ) {
foreach ( $lists[0] as $index => [$text, $offset] ) {
$before = substr($arm, 0, $offset);
if ( substr_count($before, '(') !== substr_count($before, ')') ) continue;
$suffix = substr($arm, $offset + strlen($text));
return array_map(static fn(string $marker): string => $before . ':where(' . $marker . ')' . $suffix, explode(',', $lists[1][$index][0]));
}
}
if ( false === strpbrk($arm, '\\"\'') && preg_match_all(self::MARKER_WHERE, $arm, $matches, PREG_OFFSET_CAPTURE) ) {
foreach ( $matches[0] as $index => [$text, $offset] ) {
$before = substr($arm, 0, $offset);
if ( ! str_contains($matches[1][ $index ][0], ',') || substr_count($before, '(') !== substr_count($before, ')') ) {
continue;
}
$suffix = substr($arm, $offset + strlen($text));
return array_map(static fn(string $marker): string => $before . ':where(' . $marker . ')' . $suffix, explode(',', $matches[1][ $index ][0]));
}
}
if ( ! str_contains($arm, ':is(') ) {
return null;
}
$tokens = CssSelectorTokenizer::tokenize($arm);
if ( ! $tokens['supported'] ) {
return null;
}
foreach ( $tokens['compound_spans'] as $position => $span ) {
$compound = $tokens['compounds'][ $position ];
if ( ! str_starts_with($compound, ':is(') || ! str_ends_with($compound, ')') || ! preg_match(self::PAGE_MARKER, $compound) ) {
continue;
}
$alternatives = CssStylesheetTransformer::splitSelectorList(substr($compound, 4, -1));
if ( null === $alternatives || count($alternatives) < 2 ) {
continue;
}
$weights = array();
foreach ( $alternatives as $alternative ) {
$weights[ (string) self::plainCompoundWeight(trim($alternative)) ] = true;
}
if ( 1 !== count($weights) || isset($weights['']) ) {
continue;
}
$prefix = substr($arm, 0, $span['start']);
$suffix = substr($arm, $span['end']);
return array_map(static fn(string $alternative): string => $prefix . trim($alternative) . $suffix, $alternatives);
}
return null;
}

public static function compact(string $prelude): string
Expand Down
6 changes: 4 additions & 2 deletions php-transformer/src/Css/CssSelectorMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ private static function parseCompound(string $source, int $sourceStart, bool $is
// A neutral native target can be a complete descendant
// path, or contain nested shims. Flattening it into one
// compound changes both its match and its specificity.
if (($argument['supported'] ?? false) && (count($argument['compounds']) > 1 || !empty($leaf['not']) || !empty($leaf['any']) || !empty($leaf['first_child']) || !empty($leaf['last_child']) || null !== ($leaf['nth_child'] ?? null) || null !== ($leaf['nth_type'] ?? null))) {
if (($argument['supported'] ?? false) && (count($argument['compounds']) > 1 || !empty($leaf['root']) || !empty($leaf['not']) || !empty($leaf['any']) || !empty($leaf['first_child']) || !empty($leaf['last_child']) || null !== ($leaf['nth_child'] ?? null) || null !== ($leaf['nth_type'] ?? null))) {
$group = self::parseSelectorListArgument($alternatives, 'where' === $lowerName);
if (null === $group) return null;
$compound['any'][] = $group;
Expand Down Expand Up @@ -582,7 +582,9 @@ private static function parseCompound(string $source, int $sourceStart, bool $is
if ( '' === $argument ) {
return null;
}
$negated = self::parseUncached($argument);
$alternatives = CssStylesheetTransformer::splitSelectorList($argument);
if (null === $alternatives) return null;
$negated = self::parseUncached(count($alternatives) > 1 ? ':is(' . $argument . ')' : $argument);
if ( ! ($negated['supported'] ?? false) || null !== ($negated['pseudo_state_suffix_span'] ?? null) ) {
return null;
}
Expand Down
Loading
Loading