css: adjust parser to allow --foo: {...}

This commit is contained in:
Evan Wallace
2026-08-08 13:38:38 -04:00
parent 209db54371
commit fc2ee9babc
3 changed files with 64 additions and 12 deletions
+44 -1
View File
@@ -2309,7 +2309,50 @@ func (p *parser) scanForEndOfRule() (endOfRuleScan, int) {
var initialStack [4]css_lexer.T
stack := initialStack[:0]
// Small state machine to recognize "--foo:"
const (
customPropIdent = iota
customPropColon
customPropAccept
customPropReject
)
customProp := customPropIdent
for i, t := range p.tokens[p.index:] {
// People sometimes use CSS custom properties to store JSON. This is
// problematic because the "{...}" braces look like a CSS block unless
// this edge case is special-cased. See the (quite long) discussion on
// https://github.com/w3c/csswg-drafts/issues/7961 for more information.
//
// I couldn't find any actual specification for how this is supposed to
// work. Just a lot of discussion on GitHub. I did find this text, from
// https://drafts.csswg.org/css-syntax/#consume-a-declaration:
//
// Otherwise, if decl’s value contains a top-level simple block with
// an associated token of <{-token>, and also contains any other non-
// <whitespace-token> value, return nothing. (That is, a top-level {}-
// block is only allowed as the entire value of a non-custom property.)
//
// However, current browsers appear to accept custom properties with a
// {}-block contained anywhere inside them, not just as the entire value.
// So that's what I'm attempting to do here.
if customProp < customPropAccept {
switch customProp {
case customPropIdent:
if t.Kind == css_lexer.TIdent && strings.HasPrefix(p.source.Contents[t.Range.Loc.Start:], "--") {
customProp = customPropColon
} else if t.Kind != css_lexer.TWhitespace {
customProp = customPropReject
}
case customPropColon:
if t.Kind == css_lexer.TColon {
customProp = customPropAccept
} else if t.Kind != css_lexer.TWhitespace {
customProp = customPropReject
}
}
}
switch t.Kind {
case css_lexer.TSemicolon:
if len(stack) == 0 {
@@ -2323,7 +2366,7 @@ func (p *parser) scanForEndOfRule() (endOfRuleScan, int) {
stack = append(stack, css_lexer.TCloseBracket)
case css_lexer.TOpenBrace:
if len(stack) == 0 {
if len(stack) == 0 && customProp != customPropAccept {
return endOfRuleOpenBrace, p.index + i
}
stack = append(stack, css_lexer.TCloseBrace)
+9
View File
@@ -1361,6 +1361,15 @@ func TestNestedSelector(t *testing.T) {
"::placeholder {\n opacity: 0.5;\n}\n@layer base {\n ::placeholder {\n color: green;\n }\n}\n", "")
}
func TestCustomPropertyContainingBraces(t *testing.T) {
// CSS custom properties must be able to store JSON-like values
// See: https://github.com/w3c/csswg-drafts/issues/7961
expectPrinted(t, "div { --foo:hover{color:red}zoom:2; }", "div {\n --foo:hover{color:red}zoom:2;\n}\n", "")
expectPrinted(t, "div { --foo:hover { color: red } zoom: 2; }", "div {\n --foo:hover { color: red } zoom: 2;\n}\n", "")
expectPrinted(t, "div { --foo : hover { color : red } zoom : 2 ; }", "div {\n --foo: hover { color : red } zoom : 2 ;\n}\n", "")
expectPrinted(t, "div { --foo + hover { color : red } zoom : 2 ; }", "div {\n --foo + hover {\n color: red;\n }\n zoom: 2;\n}\n", "")
}
func TestBadQualifiedRules(t *testing.T) {
expectPrinted(t, "$bad: rule;", "$bad: rule; {\n}\n", "<stdin>: WARNING: Unexpected \"$\"\n")
expectPrinted(t, "$bad: rule; div { color: red }", "$bad: rule; div {\n color: red;\n}\n", "<stdin>: WARNING: Unexpected \"$\"\n")
+11 -11
View File
@@ -277,19 +277,19 @@ func TestVerbatimWhitespace(t *testing.T) {
expectPrintedMinify(t, "* { --x:[y ]; }", "*{--x:[y ]}")
expectPrintedMinify(t, "* { --x:[ y]; }", "*{--x:[ y]}")
// Note: These cases now behave like qualified rules
expectPrinted(t, "* { --x:{y}; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x:{y} ; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x: {y}; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x:{y }; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x:{ y}; }", "* {\n --x: {\n y;\n }\n}\n")
// These cases temporarily behaved like qualified rules, but now they're back to being declarations
expectPrinted(t, "* { --x:{y}; }", "* {\n --x:{y};\n}\n")
expectPrinted(t, "* { --x:{y} ; }", "* {\n --x:{y} ;\n}\n")
expectPrinted(t, "* { --x: {y}; }", "* {\n --x: {y};\n}\n")
expectPrinted(t, "* { --x:{y }; }", "* {\n --x:{y };\n}\n")
expectPrinted(t, "* { --x:{ y}; }", "* {\n --x:{ y};\n}\n")
// Note: These cases now behave like qualified rules
// These cases temporarily behaved like qualified rules, but now they're back to being declarations
expectPrintedMinify(t, "* { --x:{y}; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{y} ; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x: {y}; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{y }; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{ y}; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{y} ; }", "*{--x:{y} }")
expectPrintedMinify(t, "* { --x: {y}; }", "*{--x: {y}}")
expectPrintedMinify(t, "* { --x:{y }; }", "*{--x:{y }}")
expectPrintedMinify(t, "* { --x:{ y}; }", "*{--x:{ y}}")
expectPrintedMinify(t, "@supports ( --x : y , z ) { a { color: red; } }", "@supports ( --x : y , z ){a{color:red}}")
expectPrintedMinify(t, "@supports ( --x : ) { a { color: red; } }", "@supports ( --x : ){a{color:red}}")