Skip a font face named in a comment when trimming the fallback
Found while doing plan step 46: withoutFontFace() searched the raw bundle, so the `@font-face` that foundation/base.css's and error-page.css's header comments mention read as the rule. Each cut from inside its comment to the end of the next block and left the comment open, so the no-build fallback lost base.css's html rule and icon.css's rules. The search and the brace count now read Stylesheets::mask(), and only tokens/font.css's real block leaves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
70c983b7bb
commit
0a028b158f
@@ -137,24 +137,29 @@ class ErrorPage
|
||||
/**
|
||||
* `$css` with every `@font-face` block dropped, wherever it sits: not a search for the block's
|
||||
* text (fragile the moment a comment or a value is reworded) but a structural read — find
|
||||
* `@font-face` outside nothing else looks at here, then remove the balanced `{ … }` that
|
||||
* follows it, brace for brace, however the block itself is written. `tokens/font.css`'s is the
|
||||
* only one the bundle carries, and it is also the bundle's only relative `url()` (button.css,
|
||||
* icon.css, shape.css and the rest reference nothing on disk), so this removal is also what
|
||||
* leaves the fallback with no `url()` to a font the fallback cannot serve. The typeface itself
|
||||
* `@font-face` outside comments and strings, then remove the balanced `{ … }` that follows it,
|
||||
* brace for brace, however the block itself is written. Both reads go through
|
||||
* `Stylesheets::mask()`, because the bundle's comments name the rule too (foundation/base.css's
|
||||
* and error-page.css's headers): read as the rule, such a mention cut from inside its comment to
|
||||
* the end of the next balanced block, leaving the comment open over the rules after it —
|
||||
* base.css's `html` rule and icon.css's rules left the fallback that way. `tokens/font.css`'s
|
||||
* block is the only one the bundle carries, and it is also the bundle's only relative `url()`
|
||||
* (button.css, icon.css, shape.css and the rest reference nothing on disk), so this removal is
|
||||
* also what leaves the fallback with no `url()` to a font it cannot serve. The typeface itself
|
||||
* degrades on its own: `--md-ref-typeface-brand` (tokens/type.css) lists `ui-sans-serif`,
|
||||
* `system-ui` and `sans-serif` right after the brand name, so with no `@font-face` to resolve
|
||||
* it the browser skips straight to that system stack — nothing here has to name one.
|
||||
*/
|
||||
protected static function withoutFontFace(string $css): string
|
||||
{
|
||||
$masked = Stylesheets::mask($css);
|
||||
$result = '';
|
||||
$offset = 0;
|
||||
$length = strlen($css);
|
||||
|
||||
while (($start = stripos($css, '@font-face', $offset)) !== false) {
|
||||
while (($start = stripos($masked, '@font-face', $offset)) !== false) {
|
||||
$result .= substr($css, $offset, $start - $offset);
|
||||
$open = strpos($css, '{', $start);
|
||||
$open = strpos($masked, '{', $start);
|
||||
|
||||
if ($open === false) {
|
||||
$offset = $start + strlen('@font-face');
|
||||
@@ -166,7 +171,7 @@ class ErrorPage
|
||||
$i = $open + 1;
|
||||
|
||||
while ($i < $length && $depth > 0) {
|
||||
$depth += match ($css[$i]) {
|
||||
$depth += match ($masked[$i]) {
|
||||
'{' => 1,
|
||||
'}' => -1,
|
||||
default => 0,
|
||||
|
||||
@@ -230,9 +230,10 @@ final class Stylesheets
|
||||
/**
|
||||
* `$css` with every comment and quoted string blinded to spaces (newlines kept), same length —
|
||||
* so a search on it for `@import` or `url(` never matches one written inside a comment or a
|
||||
* string, while every offset still lines up with `$css` itself.
|
||||
* string, while every offset still lines up with `$css` itself. Public for
|
||||
* `ErrorPage::withoutFontFace()`, which searches a bundle for `@font-face` the same way.
|
||||
*/
|
||||
private static function mask(string $css): string
|
||||
public static function mask(string $css): string
|
||||
{
|
||||
$masked = $css;
|
||||
$length = strlen($css);
|
||||
|
||||
@@ -406,3 +406,25 @@ it('inlines beside a build the error layout\'s bundle, which holds no font face,
|
||||
->not->toMatch('/\burl\(/i')
|
||||
->toContain('@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;');
|
||||
});
|
||||
|
||||
/**
|
||||
* The bundle's comments name `@font-face` too (foundation/base.css's and error-page.css's headers).
|
||||
* Read as the rule, each mention cut from inside its comment to the end of the next balanced block
|
||||
* and left the comment open, so base.css's `html` rule and icon.css's rules went missing from the
|
||||
* fallback. Only the real block may leave: what remains is the bundle, rule for rule, without it.
|
||||
*/
|
||||
it('drops only the real font face from the fallback, not the rules after a comment that names one', function () {
|
||||
$files = [
|
||||
realpath(__DIR__.'/../../resources/css/foundation.css'),
|
||||
realpath(__DIR__.'/../../resources/css/components/error-page.css'),
|
||||
];
|
||||
$withoutComments = fn (string $css): string => (string) preg_replace('~/\*.*?\*/~s', '', $css);
|
||||
$bundle = Stylesheets::bundle($files);
|
||||
|
||||
expect(substr_count($withoutComments($bundle), '@font-face'))->toBe(1)
|
||||
->and(substr_count($bundle, '@font-face'))->toBeGreaterThan(1)
|
||||
->and($withoutComments((string) ErrorPage::fallbackStyles()))
|
||||
->toStartWith($withoutComments((string) preg_replace('/@font-face\s*\{[^{}]*\}/', '', $bundle)))
|
||||
->toMatch('/html \{\s*background-color: var\(--md-sys-color-surface\);/')
|
||||
->toContain('[data-md-icon] {');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user