| index ed6a3a5..f3e764b 100644 |
| --- a/check/baseline_test.go |
| +++ b/check/baseline_test.go |
| @@ -326,17 +326,15 @@ func atRuleName(prelude string) string { |
| |
| // cssItems splits a rule list into its statements, each up to its ; |
| // or through its {} block, with strings and parentheses respected so a |
| -// brace inside content: "{" or a url() is not taken for structure. A |
| -// backslash takes the next character with it here and in the two |
| -// readers below, as it does for a browser: --a: \"; opens no string, |
| -// and reading one there would hide every rule up to the next quote. |
| +// brace inside content: "{" or a url() is not taken for structure. It |
| +// and the two readers below see only what cssWithoutComments returned, |
| +// which has no backslash outside a string, so only skipString reads |
| +// escapes. |
| func cssItems(src string) ([]cssItem, error) { |
| var items []cssItem |
| start, depth := 0, 0 |
| for i := 0; i < len(src); i++ { |
| switch c := src[i]; c { |
| - case '\\': |
| - i++ |
| case '"', '\'': |
| i = skipString(src, i) |
| case '(': |
| @@ -387,8 +385,6 @@ func matchingBrace(src string, open int) (int, bool) { |
| depth := 0 |
| for i := open; i < len(src); i++ { |
| switch src[i] { |
| - case '\\': |
| - i++ |
| case '"', '\'': |
| i = skipString(src, i) |
| case '{': |
| @@ -428,8 +424,6 @@ func cssDeclarations(block string) ([][3]string, error) { |
| } |
| for i := 0; i < len(block); i++ { |
| switch block[i] { |
| - case '\\': |
| - i++ |
| case '"', '\'': |
| i = skipString(block, i) |
| case '(': |
| @@ -829,6 +823,7 @@ func refusedAdditions(t *testing.T, files []sideBySide) []string { |
| // gate must let through, so the test cannot pass by refusing |
| // everything. |
| func TestTheNewRuleGateHasNoKnownHoles(t *testing.T) { |
| + const outsideAString = "a backslash outside a string" |
| // The spelling rules that refuse escaped, spaced and capitalised |
| // names must not refuse 1.0.0's own CSS, or the grammar would be |
| // a rule 1.0 itself breaks. |
| @@ -847,8 +842,8 @@ func TestTheNewRuleGateHasNoKnownHoles(t *testing.T) { |
| {`@property --rst-gate-control { syntax: "<length>"; inherits: false; initial-value: 0px; }`, "@property --rst-gate-control", false}, |
| {`:root { --rst-gate-control: 1px; }`, "--rst-gate-control", false}, |
| {`@media (width: 731px) { [ rst-PAGE] { padding-inline: 100px; } }`, "[ rst-PAGE]", true}, |
| - {`@media (width: 731px) { @property --rst-\73p-4 { syntax: "<length>"; inherits: false; initial-value: 48px; } }`, `--rst-\73p-4`, true}, |
| - {`@media (width: 731px) { @keyframes rst-\73pin {} }`, `rst-\73pin`, true}, |
| + {`@media (width: 731px) { @property --rst-\73p-4 { syntax: "<length>"; inherits: false; initial-value: 48px; } }`, outsideAString, true}, |
| + {`@media (width: 731px) { @keyframes rst-\73pin {} }`, outsideAString, true}, |
| {`[rst-page-foot] [rst-dropdown] { margin: 0; }`, "[rst-page-foot] [rst-dropdown]", false}, |
| // A comment marker the reader takes for one and a browser does |
| // not, either side of a rule: inside a string of either quote, |
| @@ -858,23 +853,36 @@ func TestTheNewRuleGateHasNoKnownHoles(t *testing.T) { |
| {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet]::before { content: "/*"; } [rst-page] { padding-inline: 3rem; } [rst-snippet]::after { content: "*/"; } }`, "[rst-page] { padding-inline: 3rem", true}, |
| {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet]::before { content: '/*'; } [rst-page] { padding-inline: 3rem; } [rst-snippet]::after { content: '*/'; } }`, "[rst-page] { padding-inline: 3rem", true}, |
| {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet]::before { content: "\"/*"; } [rst-page] { padding-inline: 3rem; } [rst-snippet]::after { content: "*/\""; } }`, "[rst-page] { padding-inline: 3rem", true}, |
| - {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet] { --rst-gate-a: \/*; } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: */; } }`, "[rst-page] { padding-inline: 3rem", true}, |
| - // An escaped quote opens no string, so it cannot hide a rule |
| - // inside one either. |
| - {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet] { --rst-gate-a: \"; } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: "\""; } }`, "[rst-page] { padding-inline: 3rem", true}, |
| + // A backslash outside a string is refused by the reader, before |
| + // the gate: an escaped quote opens no string and an escaped |
| + // url( is still url(, so either can hide a rule. |
| + {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet] { --rst-gate-a: \/*; } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: */; } }`, outsideAString, true}, |
| + {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet] { --rst-gate-a: \"; } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: "\""; } }`, outsideAString, true}, |
| + {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet] { --rst-gate-a: \url(/*); } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: \url(*/ ); } }`, outsideAString, true}, |
| + {`@media (min-width: 700px) and (max-width: 750px) { [rst-snippet] { --rst-gate-a: u\72 l(/*); } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: u\72 l(*/ ); } }`, outsideAString, true}, |
| // A snippet whose content is a comment marker adds nothing to a |
| // 1.0 page, so the reading that refuses the rules above must |
| // still let it through. |
| {`[rst-snippet]::before { content: "/* */"; }`, "[rst-snippet]::before", false}, |
| } { |
| + // A stylesheet the reader refuses fails the gate before it |
| + // compares anything, so the refusal is the reader's error. |
| + var refusals []string |
| files := everyStylesheet(t) |
| for i := range files { |
| if files[i].src == "tokens.css" { |
| - files[i].is, files[i].isAt = parseDeclarations(t, "css/tokens.css", files[i].wasBytes+"\n"+c.css+"\n") |
| + var err error |
| + files[i].is, files[i].isAt, err = readStylesheet("css/tokens.css", files[i].wasBytes+"\n"+c.css+"\n") |
| + if err != nil { |
| + refusals = []string{err.Error()} |
| + } |
| } |
| } |
| + if refusals == nil { |
| + refusals = refusedAdditions(t, files) |
| + } |
| named := false |
| - for _, why := range refusedAdditions(t, files) { |
| + for _, why := range refusals { |
| if strings.Contains(why, c.names) { |
| named = true |
| t.Logf("refused: %s", why) |
| @@ -967,19 +975,26 @@ func TestDeclarationsAreReadInTheirContext(t *testing.T) { |
| sameDeclarations(t, got, want) |
| } |
| |
| -// The reader takes comments out and reads a backslash as a browser |
| -// does, and refuses what it cannot read the same way: a string a |
| -// newline or the end of the file cuts off, a comment never closed or |
| -// wedged between two tokens, and an unquoted url(), whose contents a |
| -// browser reads as neither a string nor a comment, so url(/*) opens |
| -// none. A function name spelt |
| -// with an escape is refused with it, since u\72l( is url( to a |
| -// browser. |
| +// The reader takes comments out and refuses what it cannot read the |
| +// way a browser does: a string a newline or the end of the file cuts |
| +// off, a comment never closed or wedged between two tokens, an |
| +// unquoted url(), whose contents a browser reads as neither a string |
| +// nor a comment, so url(/*) opens none, and a backslash outside a |
| +// string, since \url( and u\72 l( are url( to a browser and \" opens |
| +// no string. A backslash inside a string is read as a browser reads |
| +// it. |
| func TestTheReaderRefusesWhatItCannotReadAsABrowserDoes(t *testing.T) { |
| for _, c := range []struct{ css, refusal string }{ |
| {`[rst-snippet] { --rst-gate-a: url(/*); } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-gate-b: url(*/); }`, "url("}, |
| {`a { b: URL(c) }`, "url("}, |
| - {`a { b: u\72l(c) }`, "escape"}, |
| + // An escaped url( hides the rule between as an unescaped one |
| + // would, however the escape is spelt. |
| + {`[rst-snippet] { --rst-a: \url(/*); } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-b: \url(*/ ); }`, "backslash"}, |
| + {`[rst-snippet] { --rst-a: u\72 l(/*); } [rst-page] { padding-inline: 3rem; } [rst-snippet] { --rst-b: u\72 l(*/ ); }`, "backslash"}, |
| + {`a { b: u\72l(c) }`, "backslash"}, |
| + {`i\"j { k: l }`, "backslash"}, |
| + {`a { m: \"; }`, "backslash"}, |
| + {`a { b: \/* }`, "backslash"}, |
| // Two strings cut off by their lines pair up into one if the |
| // newline is not read as cutting the first one off. |
| {"a { content: \"b\n; } c { content: \"d\n; }", "string"}, |
| @@ -992,7 +1007,7 @@ func TestTheReaderRefusesWhatItCannotReadAsABrowserDoes(t *testing.T) { |
| t.Errorf("declarationsOf(%q): %v, want an error naming the %s", c.css, err, c.refusal) |
| } |
| } |
| - const css = "a { content: \"/*\"; b: url(\"*/\"); c: url( '/*' ) } d { content: \"e\\\nf\"; g: x-url( /**/ h) } i\\\"j { k: l; m: \\\"; n: o }" |
| + const css = "a { content: \"/*\"; b: url(\"*/\"); c: url( '/*' ) } d { content: \"e\\\nf\"; g: x-url( /**/ h) } i { k: \"\\\"/*\\\"\"; m: '\\'}'; n: o }" |
| got, err := declarationsOf("t.css", css) |
| if err != nil { |
| t.Fatal(err) |
| @@ -1003,9 +1018,9 @@ func TestTheReaderRefusesWhatItCannotReadAsABrowserDoes(t *testing.T) { |
| {File: "t.css", Selector: "a", Property: "c", Value: `url( '/*' )`}, |
| {File: "t.css", Selector: "d", Property: "content", Value: "\"e\\ f\""}, |
| {File: "t.css", Selector: "d", Property: "g", Value: "x-url( h)"}, |
| - {File: "t.css", Selector: `i\"j`, Property: "k", Value: "l"}, |
| - {File: "t.css", Selector: `i\"j`, Property: "m", Value: `\"`}, |
| - {File: "t.css", Selector: `i\"j`, Property: "n", Value: "o"}, |
| + {File: "t.css", Selector: "i", Property: "k", Value: `"\"/*\""`}, |
| + {File: "t.css", Selector: "i", Property: "m", Value: `'\'}'`}, |
| + {File: "t.css", Selector: "i", Property: "n", Value: "o"}, |
| } |
| sameDeclarations(t, got, want) |
| } |