From d3fb0043cc8e14001cdf4162dcd460a95786eb29 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Sun, 31 Mar 2024 14:46:14 +0800 Subject: [PATCH 01/25] Unify linefeeds in the repository --- .gitattributes | 6 + docs/parser-info.html | 856 +++++++++++++++++++++--------------------- example.html | 132 +++---- 3 files changed, 500 insertions(+), 494 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d55260b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +.gitattributes text +.gitignore text +*.html text +*.js text +*.json text +*.md text diff --git a/docs/parser-info.html b/docs/parser-info.html index e1543d2..6bdde54 100644 --- a/docs/parser-info.html +++ b/docs/parser-info.html @@ -1,429 +1,429 @@ - -CSS Parsing Example - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

/*

-
-
-

-				
-
- -

This illustrates the parsed tree created byTab Atkin's CSS Parser

-
-
-
{
-
-
- -

based on the stylesheet shown below - in theory you could have a collection of these...

-
-
-
    "type": "stylesheet",
-
-
- -

This will be a collection of everything described in this sheet.

-
-
-
    "value": [
-
-
- -

Each of these rule becomes an entry of some kind in this array.

-
-
-

-				
-
- -

*/

-
-
-

-				
-
- -

html,body {

-
-
-
        {
-            "type": "selector",
-            "selector": [
-                "IDENT(html)",
-                "DELIM(,)",
-                "IDENT(body)",
-                "WS"
-            ],
-            "value": [
-
-
- -

    height: 100%;

-
-
-
                {
-                    "type": "declaration",
-                    "name": "height",
-                    "value": [
-                        "WS",
-                        "PERCENTAGE(100)"
-                    ]
-                },
-
-
- -

    margin: 0px;

-
-
-
                {
-                    "type": "declaration",
-                    "name": "margin",
-                    "value": [
-                        "WS",
-                        "DIM(0,px)"
-                    ]
-                },
-
-
- -

    padding: 0px;

-
-
-
                {
-                    "type": "declaration",
-                    "name": "padding",
-                    "value": [
-                        "WS",
-                        "DIM(0,px)"
-                    ]
-                },
-
-
- -

    overflow: hidden;

-
-
-
                {
-                    "type": "declaration",
-                    "name": "overflow",
-                    "value": [
-                        "WS",
-                        "IDENT(hidden)"
-                    ]
-                }
-            ]
-
-
- -

}

-
-
-
        },
-
-
- -

/* next rule: splat with unofficial but conforming properties */

-
-
-

-				
-
- -

* {

-
-
-
        {
-            "type": "selector",
-            "selector": [
-                "DELIM(*)",
-                "WS"
-            ],
-            "value": [
-
-
- -

    /* No problem... */

-
-
-

-				
-
- -

    -o-box-sizing: border-box;

-
-
-
                {
-                    "type": "declaration",
-                    "name": "-o-box-sizing",
-                    "value": [
-                        "WS",
-                        "IDENT(border-box)"
-                    ]
-                }
-            ]
-
-
- -

}

-
-
-
        },
-
-
- -

/* next rule: @ rule with lots of tricky bits: note the prelude / func

-
-
-

-				
-
- -

@document url("http://www.w3.org/"),

-
-
-
        {
-            "type": "at",
-            "name": "document",
-            "prelude": [
-                "WS",
-                "URL(http://www.w3.org/)",
-                "DELIM(,)",
-                "WS",
-
-
- -

    url-prefix("http://www.w3.org/Style/"),

-
-
-
                {
-                    "type": "func",
-                    "name": "url-prefix",
-                    "value": [
-                        [
-                            "\"http://www.w3.org/Style/\""
-                        ]
-                    ]
-                },
-                "DELIM(,)",
-                "WS",
-
-
- -

    domain("mozilla.org"),

-
-
-
                {
-                    "type": "func",
-                    "name": "domain",
-                    "value": [
-                        [
-                            "\"mozilla.org\""
-                        ]
-                    ]
-                },
-                "DELIM(,)",
-                "WS",
-
-
- -

    regexp("https:.*") {

-
-
-
                {
-                    "type": "func",
-                    "name": "regexp",
-                    "value": [
-                        [
-                            "\"https:.*\""
-                        ]
-                    ]
-                },
-                "WS"
-            ],
-            "value": [
-
-
- -
    -
  • /* Now we'll have a collection of rules just like a sheet, but for the @ block... */

    -
-
-
                {
-
-
- -

    body {

-
-
-
                    "type": "selector",
-                    "selector": [
-                        "IDENT(body)",
-                        "WS"
-                    ],
-                    "value": [
-
-
- -

-

        color: purple;

-
-
-
                        {
-                            "type": "declaration",
-                            "name": "color",
-                            "value": [
-                                "WS",
-                                "IDENT(purple)"
-                            ]
-                        },
-
-
- -

-

        background: yellow;

-
-
-
                        {
-                            "type": "declaration",
-                            "name": "background",
-                            "value": [
-                                "WS",
-                                "IDENT(yellow)"
-                            ]
-                        }
-                    ]
-
-
- -

    }

-
-
-
                }
-            ]
-        }
-    ]
-}
-
-
+ +CSS Parsing Example + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

/*

+
+
+

+				
+
+ +

This illustrates the parsed tree created byTab Atkin's CSS Parser

+
+
+
{
+
+
+ +

based on the stylesheet shown below - in theory you could have a collection of these...

+
+
+
    "type": "stylesheet",
+
+
+ +

This will be a collection of everything described in this sheet.

+
+
+
    "value": [
+
+
+ +

Each of these rule becomes an entry of some kind in this array.

+
+
+

+				
+
+ +

*/

+
+
+

+				
+
+ +

html,body {

+
+
+
        {
+            "type": "selector",
+            "selector": [
+                "IDENT(html)",
+                "DELIM(,)",
+                "IDENT(body)",
+                "WS"
+            ],
+            "value": [
+
+
+ +

    height: 100%;

+
+
+
                {
+                    "type": "declaration",
+                    "name": "height",
+                    "value": [
+                        "WS",
+                        "PERCENTAGE(100)"
+                    ]
+                },
+
+
+ +

    margin: 0px;

+
+
+
                {
+                    "type": "declaration",
+                    "name": "margin",
+                    "value": [
+                        "WS",
+                        "DIM(0,px)"
+                    ]
+                },
+
+
+ +

    padding: 0px;

+
+
+
                {
+                    "type": "declaration",
+                    "name": "padding",
+                    "value": [
+                        "WS",
+                        "DIM(0,px)"
+                    ]
+                },
+
+
+ +

    overflow: hidden;

+
+
+
                {
+                    "type": "declaration",
+                    "name": "overflow",
+                    "value": [
+                        "WS",
+                        "IDENT(hidden)"
+                    ]
+                }
+            ]
+
+
+ +

}

+
+
+
        },
+
+
+ +

/* next rule: splat with unofficial but conforming properties */

+
+
+

+				
+
+ +

* {

+
+
+
        {
+            "type": "selector",
+            "selector": [
+                "DELIM(*)",
+                "WS"
+            ],
+            "value": [
+
+
+ +

    /* No problem... */

+
+
+

+				
+
+ +

    -o-box-sizing: border-box;

+
+
+
                {
+                    "type": "declaration",
+                    "name": "-o-box-sizing",
+                    "value": [
+                        "WS",
+                        "IDENT(border-box)"
+                    ]
+                }
+            ]
+
+
+ +

}

+
+
+
        },
+
+
+ +

/* next rule: @ rule with lots of tricky bits: note the prelude / func

+
+
+

+				
+
+ +

@document url("http://www.w3.org/"),

+
+
+
        {
+            "type": "at",
+            "name": "document",
+            "prelude": [
+                "WS",
+                "URL(http://www.w3.org/)",
+                "DELIM(,)",
+                "WS",
+
+
+ +

    url-prefix("http://www.w3.org/Style/"),

+
+
+
                {
+                    "type": "func",
+                    "name": "url-prefix",
+                    "value": [
+                        [
+                            "\"http://www.w3.org/Style/\""
+                        ]
+                    ]
+                },
+                "DELIM(,)",
+                "WS",
+
+
+ +

    domain("mozilla.org"),

+
+
+
                {
+                    "type": "func",
+                    "name": "domain",
+                    "value": [
+                        [
+                            "\"mozilla.org\""
+                        ]
+                    ]
+                },
+                "DELIM(,)",
+                "WS",
+
+
+ +

    regexp("https:.*") {

+
+
+
                {
+                    "type": "func",
+                    "name": "regexp",
+                    "value": [
+                        [
+                            "\"https:.*\""
+                        ]
+                    ]
+                },
+                "WS"
+            ],
+            "value": [
+
+
+ +
    +
  • /* Now we'll have a collection of rules just like a sheet, but for the @ block... */

    +
+
+
                {
+
+
+ +

    body {

+
+
+
                    "type": "selector",
+                    "selector": [
+                        "IDENT(body)",
+                        "WS"
+                    ],
+                    "value": [
+
+
+ +

+

        color: purple;

+
+
+
                        {
+                            "type": "declaration",
+                            "name": "color",
+                            "value": [
+                                "WS",
+                                "IDENT(purple)"
+                            ]
+                        },
+
+
+ +

+

        background: yellow;

+
+
+
                        {
+                            "type": "declaration",
+                            "name": "background",
+                            "value": [
+                                "WS",
+                                "IDENT(yellow)"
+                            ]
+                        }
+                    ]
+
+
+ +

    }

+
+
+
                }
+            ]
+        }
+    ]
+}
+
+
\ No newline at end of file diff --git a/example.html b/example.html index 96b779b..33e7a08 100644 --- a/example.html +++ b/example.html @@ -1,66 +1,66 @@ - -
- - - - + +
+ + + + From d8f318af242f8000972e914bd3c9a0db1b618360 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Sun, 31 Mar 2024 14:35:56 +0800 Subject: [PATCH 02/25] Unify indentation to 2-spaces --- README.md | 94 +-- example.html | 60 +- parse-css.js | 2274 +++++++++++++++++++++++++------------------------- test.html | 736 ++++++++-------- 4 files changed, 1582 insertions(+), 1582 deletions(-) diff --git a/README.md b/README.md index 57db388..fe9834a 100644 --- a/README.md +++ b/README.md @@ -82,17 +82,17 @@ A grammar is an object with one of the following four forms: ```js { - "qualified": , - "@foo": , - "unknown": + "qualified": , + "@foo": , + "unknown": } ``` ```js { - "declarations": true, - "@foo": - "unknown": + "declarations": true, + "@foo": + "unknown": } ``` @@ -132,47 +132,47 @@ This is what it currently looks like: ```js { - qualified: {declarations:true}, - "@media": {stylesheet:true}, - "@keyframes": {qualified:{declarations:true}}, - "@font-face": {declarations:true}, - "@supports": {stylesheet:true}, - "@scope": {stylesheet:true}, - "@counter-style": {declarations:true}, - "@import": null, - "@font-feature-values": { - // No qualified rules actually allowed, - // but have to declare it one way or the other. - qualified: true, - "@stylistic": {declarations:true}, - "@styleset": {declarations:true}, - "@character-variants": {declarations:true}, - "@swash": {declarations:true}, - "@ornaments": {declarations:true}, - "@annotation": {declarations:true}, - }, - "@viewport": {declarations:true}, - "@page": { - declarations: true, - "@top-left-corner": {declarations:true}, - "@top-left": {declarations:true}, - "@top-center": {declarations:true}, - "@top-right": {declarations:true}, - "@top-right-corner": {declarations:true}, - "@right-top": {declarations:true}, - "@right-middle": {declarations:true}, - "@right-bottom": {declarations:true}, - "@right-bottom-corner": {declarations:true}, - "@bottom-right": {declarations:true}, - "@bottom-center": {declarations:true}, - "@bottom-left": {declarations:true}, - "@bottom-left-corner": {declarations:true}, - "@left-bottom": {declarations:true}, - "@left-center": {declarations:true}, - "@left-top": {declarations:true}, - }, - "@custom-selector": null, - "@custom-media": null + qualified: {declarations:true}, + "@media": {stylesheet:true}, + "@keyframes": {qualified:{declarations:true}}, + "@font-face": {declarations:true}, + "@supports": {stylesheet:true}, + "@scope": {stylesheet:true}, + "@counter-style": {declarations:true}, + "@import": null, + "@font-feature-values": { + // No qualified rules actually allowed, + // but have to declare it one way or the other. + qualified: true, + "@stylistic": {declarations:true}, + "@styleset": {declarations:true}, + "@character-variants": {declarations:true}, + "@swash": {declarations:true}, + "@ornaments": {declarations:true}, + "@annotation": {declarations:true}, + }, + "@viewport": {declarations:true}, + "@page": { + declarations: true, + "@top-left-corner": {declarations:true}, + "@top-left": {declarations:true}, + "@top-center": {declarations:true}, + "@top-right": {declarations:true}, + "@top-right-corner": {declarations:true}, + "@right-top": {declarations:true}, + "@right-middle": {declarations:true}, + "@right-bottom": {declarations:true}, + "@right-bottom-corner": {declarations:true}, + "@bottom-right": {declarations:true}, + "@bottom-center": {declarations:true}, + "@bottom-left": {declarations:true}, + "@bottom-left-corner": {declarations:true}, + "@left-bottom": {declarations:true}, + "@left-center": {declarations:true}, + "@left-top": {declarations:true}, + }, + "@custom-selector": null, + "@custom-media": null } ``` diff --git a/example.html b/example.html index 33e7a08..d3a368f 100644 --- a/example.html +++ b/example.html @@ -1,8 +1,8 @@
diff --git a/parse-css.js b/parse-css.js index 1c7ac49..6e1eab8 100644 --- a/parse-css.js +++ b/parse-css.js @@ -1,14 +1,14 @@ "use strict"; (function (root, factory) { - // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, - // Rhino, and plain browser loading. - if (typeof define === 'function' && define.amd) { - define(['exports'], factory); - } else if (typeof exports !== 'undefined') { - factory(exports); - } else { - factory(root); - } + // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, + // Rhino, and plain browser loading. + if (typeof define === 'function' && define.amd) { + define(['exports'], factory); + } else if (typeof exports !== 'undefined') { + factory(exports); + } else { + factory(root); + } }(this, function (exports) { function between(num, first, last) { return num >= first && num <= last; } @@ -18,23 +18,23 @@ function uppercaseletter(code) { return between(code, 0x41,0x5a); } function lowercaseletter(code) { return between(code, 0x61,0x7a); } function letter(code) { return uppercaseletter(code) || lowercaseletter(code); } function nonascii(code) { - return ( - code == 0xb7 || - between(code, 0xc0, 0xd6) || - between(code, 0xd8, 0xf6) || - between(code, 0xf8, 0x37d) || - between(code, 0x37f, 0x1fff) || - code == 0x200c || - code == 0x200d || - code == 0x203f || - code == 0x2040 || - between(code, 0x2070, 0x218f) || - between(code, 0x2c00, 0x2fef) || - between(code, 0x3001, 0xd7ff) || - between(code, 0xf900, 0xfdcf) || - between(code, 0xfdf0, 0xfffd) || - code >= 0x10000 - ); + return ( + code == 0xb7 || + between(code, 0xc0, 0xd6) || + between(code, 0xd8, 0xf6) || + between(code, 0xf8, 0x37d) || + between(code, 0x37f, 0x1fff) || + code == 0x200c || + code == 0x200d || + code == 0x203f || + code == 0x2040 || + between(code, 0x2070, 0x218f) || + between(code, 0x2c00, 0x2fef) || + between(code, 0x3001, 0xd7ff) || + between(code, 0xf900, 0xfdcf) || + between(code, 0xfdf0, 0xfffd) || + code >= 0x10000 + ); } function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; } function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; } @@ -47,759 +47,759 @@ function surrogate(code) { return between(code, 0xd800, 0xdfff); } var maximumallowedcodepoint = 0x10ffff; class InvalidCharacterError extends Error { - constructor(message) { - super(); - this.name = "InvalidCharacterError"; - this.message = message; - } + constructor(message) { + super(); + this.name = "InvalidCharacterError"; + this.message = message; + } } function preprocess(str) { - // Turn a string into an array of code points, - // following the preprocessing cleanup rules. - var codepoints = []; - for(var i = 0; i < str.length; i++) { - var code = str.charCodeAt(i); - if(code == 0xd && str.charCodeAt(i+1) == 0xa) { - code = 0xa; i++; - } - if(code == 0xd || code == 0xc) code = 0xa; - if(code == 0x0) code = 0xfffd; - if(between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i+1), 0xdc00, 0xdfff)) { - // Decode a surrogate pair into an astral codepoint. - var lead = code - 0xd800; - var trail = str.charCodeAt(i+1) - 0xdc00; - code = Math.pow(2, 16) + lead * Math.pow(2, 10) + trail; - i++; - } - codepoints.push(code); - } - return codepoints; + // Turn a string into an array of code points, + // following the preprocessing cleanup rules. + var codepoints = []; + for(var i = 0; i < str.length; i++) { + var code = str.charCodeAt(i); + if(code == 0xd && str.charCodeAt(i+1) == 0xa) { + code = 0xa; i++; + } + if(code == 0xd || code == 0xc) code = 0xa; + if(code == 0x0) code = 0xfffd; + if(between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i+1), 0xdc00, 0xdfff)) { + // Decode a surrogate pair into an astral codepoint. + var lead = code - 0xd800; + var trail = str.charCodeAt(i+1) - 0xdc00; + code = Math.pow(2, 16) + lead * Math.pow(2, 10) + trail; + i++; + } + codepoints.push(code); + } + return codepoints; } function asciiCaselessMatch(s1, s2) { - return s1.toLowerCase() == s2.toLowerCase(); + return s1.toLowerCase() == s2.toLowerCase(); } function tokenize(str) { - str = preprocess(str); - var i = -1; - var tokens = []; - var code; - - // Line number information. - var line = 0; - var column = 0; - // The only use of lastLineLength is in reconsume(). - var lastLineLength = 0; - var incrLineno = function() { - line += 1; - lastLineLength = column; - column = 0; - }; - var locStart = {line:line, column:column}; - - var codepoint = function(i) { - if(i >= str.length) { - return -1; - } - return str[i]; - } - var next = function(num) { - if(num === undefined) - num = 1; - if(num > 3) - throw "Spec Error: no more than three codepoints of lookahead."; - return codepoint(i+num); - }; - var consume = function(num) { - if(num === undefined) - num = 1; - i += num; - code = codepoint(i); - if(newline(code)) incrLineno(); - else column += num; - //console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); - return true; - }; - var reconsume = function() { - i -= 1; - if (newline(code)) { - line -= 1; - column = lastLineLength; - } else { - column -= 1; - } - locStart.line = line; - locStart.column = column; - return true; - }; - var eof = function(codepoint) { - if(codepoint === undefined) codepoint = code; - return codepoint == -1; - }; - var donothing = function() {}; - var parseerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + ".");return true; }; - - var consumeAToken = function() { - consumeComments(); - consume(); - if(whitespace(code)) { - while(whitespace(next())) consume(); - return new WhitespaceToken; - } - else if(code == 0x22) return consumeAStringToken(); - else if(code == 0x23) { - if(namechar(next()) || areAValidEscape(next(1), next(2))) { - const isIdent = wouldStartAnIdentifier(next(1), next(2), next(3)); - return new HashToken(consumeAName(), isIdent); - } else { - return new DelimToken(code); - } - } - else if(code == 0x27) return consumeAStringToken(); - else if(code == 0x28) return new OpenParenToken(); - else if(code == 0x29) return new CloseParenToken(); - else if(code == 0x2b) { - if(startsWithANumber()) { - reconsume(); - return consumeANumericToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x2c) return new CommaToken(); - else if(code == 0x2d) { - if(startsWithANumber()) { - reconsume(); - return consumeANumericToken(); - } else if(next(1) == 0x2d && next(2) == 0x3e) { - consume(2); - return new CDCToken(); - } else if(startsWithAnIdentifier()) { - reconsume(); - return consumeAnIdentlikeToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x2e) { - if(startsWithANumber()) { - reconsume(); - return consumeANumericToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x3a) return new ColonToken; - else if(code == 0x3b) return new SemicolonToken; - else if(code == 0x3c) { - if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) { - consume(3); - return new CDOToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x40) { - if(wouldStartAnIdentifier(next(1), next(2), next(3))) { - return new AtKeywordToken(consumeAName()); - } else { - return new DelimToken(code); - } - } - else if(code == 0x5b) return new OpenSquareToken(); - else if(code == 0x5c) { - if(startsWithAValidEscape()) { - reconsume(); - return consumeAnIdentlikeToken(); - } else { - parseerror(); - return new DelimToken(code); - } - } - else if(code == 0x5d) return new CloseSquareToken(); - else if(code == 0x7b) return new OpenCurlyToken(); - else if(code == 0x7d) return new CloseCurlyToken(); - else if(digit(code)) { - reconsume(); - return consumeANumericToken(); - } - else if(namestartchar(code)) { - reconsume(); - return consumeAnIdentlikeToken(); - } - else if(eof()) return new EOFToken(); - else return new DelimToken(code); - }; - - var consumeComments = function() { - while(next(1) == 0x2f && next(2) == 0x2a) { - consume(2); - while(true) { - consume(); - if(code == 0x2a && next() == 0x2f) { - consume(); - break; - } else if(eof()) { - parseerror(); - return; - } - } - } - }; - - var consumeANumericToken = function() { - var {value, isInteger, sign} = consumeANumber(); - if(wouldStartAnIdentifier(next(1), next(2), next(3))) { - const unit = consumeAName(); - return new DimensionToken(value, unit, sign); - } else if(next() == 0x25) { - consume(); - return new PercentageToken(value, sign); - } else { - return new NumberToken(value, isInteger, sign); - } - }; - - var consumeAnIdentlikeToken = function() { - var str = consumeAName(); - if(str.toLowerCase() == "url" && next() == 0x28) { - consume(); - while(whitespace(next(1)) && whitespace(next(2))) consume(); - if(next() == 0x22 || next() == 0x27) { - return new FunctionToken(str); - } else if(whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) { - return new FunctionToken(str); - } else { - return consumeAURLToken(); - } - } else if(next() == 0x28) { - consume(); - return new FunctionToken(str); - } else { - return new IdentToken(str); - } - }; - - var consumeAStringToken = function(endingCodePoint) { - if(endingCodePoint === undefined) endingCodePoint = code; - var string = ""; - while(consume()) { - if(code == endingCodePoint || eof()) { - return new StringToken(string); - } else if(newline(code)) { - parseerror(); - reconsume(); - return new BadStringToken(); - } else if(code == 0x5c) { - if(eof(next())) { - donothing(); - } else if(newline(next())) { - consume(); - } else { - string += String.fromCodePoint(consumeEscape()) - } - } else { - string += String.fromCodePoint(code); - } - } - }; - - var consumeAURLToken = function() { - var token = new URLToken(""); - while(whitespace(next())) consume(); - if(eof(next())) return token; - while(consume()) { - if(code == 0x29 || eof()) { - return token; - } else if(whitespace(code)) { - while(whitespace(next())) consume(); - if(next() == 0x29 || eof(next())) { - consume(); - return token; - } else { - consumeTheRemnantsOfABadURL(); - return new BadURLToken(); - } - } else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) { - parseerror(); - consumeTheRemnantsOfABadURL(); - return new BadURLToken(); - } else if(code == 0x5c) { - if(startsWithAValidEscape()) { - token.value += String.fromCodePoint(consumeEscape()); - } else { - parseerror(); - consumeTheRemnantsOfABadURL(); - return new BadURLToken(); - } - } else { - token.value += String.fromCodePoint(code); - } - } - }; - - var consumeEscape = function() { - // Assume the the current character is the \ - // and the next code point is not a newline. - consume(); - if(hexdigit(code)) { - // Consume 1-6 hex digits - var digits = [code]; - for(var total = 0; total < 5; total++) { - if(hexdigit(next())) { - consume(); - digits.push(code); - } else { - break; - } - } - if(whitespace(next())) consume(); - var value = parseInt(digits.map(function(x){return String.fromCharCode(x);}).join(''), 16); - if( value > maximumallowedcodepoint ) value = 0xfffd; - return value; - } else if(eof()) { - return 0xfffd; - } else { - return code; - } - }; - - var areAValidEscape = function(c1, c2) { - if(c1 != 0x5c) return false; - if(newline(c2)) return false; - return true; - }; - var startsWithAValidEscape = function() { - return areAValidEscape(code, next()); - }; - - var wouldStartAnIdentifier = function(c1, c2, c3) { - if(c1 == 0x2d) { - return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3); - } else if(namestartchar(c1)) { - return true; - } else if(c1 == 0x5c) { - return areAValidEscape(c1, c2); - } else { - return false; - } - }; - var startsWithAnIdentifier = function() { - return wouldStartAnIdentifier(code, next(1), next(2)); - }; - - var wouldStartANumber = function(c1, c2, c3) { - if(c1 == 0x2b || c1 == 0x2d) { - if(digit(c2)) return true; - if(c2 == 0x2e && digit(c3)) return true; - return false; - } else if(c1 == 0x2e) { - if(digit(c2)) return true; - return false; - } else if(digit(c1)) { - return true; - } else { - return false; - } - }; - var startsWithANumber = function() { - return wouldStartANumber(code, next(1), next(2)); - }; - - var consumeAName = function() { - var result = ""; - while(consume()) { - if(namechar(code)) { - result += String.fromCodePoint(code); - } else if(startsWithAValidEscape()) { - result += String.fromCodePoint(consumeEscape()); - } else { - reconsume(); - return result; - } - } - }; - - var consumeANumber = function() { - let isInteger = true; - let sign; - let numberPart = ""; - let exponentPart = ""; - if(next() == 0x2b || next() == 0x2d) { - consume(); - sign = String.fromCodePoint(code); - numberPart += sign; - } - while(digit(next())) { - consume(); - numberPart += String.fromCodePoint(code); - } - if(next(1) == 0x2e && digit(next(2))) { - consume(); - numberPart += "."; - while(digit(next())) { - consume(); - numberPart += String.fromCodePoint(code); - } - isInteger = false; - } - var c1 = next(1), c2 = next(2), c3 = next(3); - const eDigit = (c1 == 0x45 || c1 == 0x65) && digit(c2); - const eSignDigit = (c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3); - if(eDigit || eSignDigit) { - consume(); - if(eSignDigit) { - consume(); - exponentPart += String.fromCodePoint(code); - } - while(digit(next())) { - consume(); - exponentPart += String.fromCodePoint(code); - } - isInteger = false; - } - let value = +numberPart; - if(exponentPart) value = value * Math.pow(10, +exponentPart); - - return {value, isInteger, sign}; - }; - - var consumeTheRemnantsOfABadURL = function() { - while(consume()) { - if(code == 0x29 || eof()) { - return; - } else if(startsWithAValidEscape()) { - consumeEscape(); - donothing(); - } else { - donothing(); - } - } - }; - - - - var iterationCount = 0; - while(!eof(next())) { - tokens.push(consumeAToken()); - iterationCount++; - if(iterationCount > str.length*2) return "I'm infinite-looping!"; - } - return tokens; + str = preprocess(str); + var i = -1; + var tokens = []; + var code; + + // Line number information. + var line = 0; + var column = 0; + // The only use of lastLineLength is in reconsume(). + var lastLineLength = 0; + var incrLineno = function() { + line += 1; + lastLineLength = column; + column = 0; + }; + var locStart = {line:line, column:column}; + + var codepoint = function(i) { + if(i >= str.length) { + return -1; + } + return str[i]; + } + var next = function(num) { + if(num === undefined) + num = 1; + if(num > 3) + throw "Spec Error: no more than three codepoints of lookahead."; + return codepoint(i+num); + }; + var consume = function(num) { + if(num === undefined) + num = 1; + i += num; + code = codepoint(i); + if(newline(code)) incrLineno(); + else column += num; + //console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); + return true; + }; + var reconsume = function() { + i -= 1; + if (newline(code)) { + line -= 1; + column = lastLineLength; + } else { + column -= 1; + } + locStart.line = line; + locStart.column = column; + return true; + }; + var eof = function(codepoint) { + if(codepoint === undefined) codepoint = code; + return codepoint == -1; + }; + var donothing = function() {}; + var parseerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + ".");return true; }; + + var consumeAToken = function() { + consumeComments(); + consume(); + if(whitespace(code)) { + while(whitespace(next())) consume(); + return new WhitespaceToken; + } + else if(code == 0x22) return consumeAStringToken(); + else if(code == 0x23) { + if(namechar(next()) || areAValidEscape(next(1), next(2))) { + const isIdent = wouldStartAnIdentifier(next(1), next(2), next(3)); + return new HashToken(consumeAName(), isIdent); + } else { + return new DelimToken(code); + } + } + else if(code == 0x27) return consumeAStringToken(); + else if(code == 0x28) return new OpenParenToken(); + else if(code == 0x29) return new CloseParenToken(); + else if(code == 0x2b) { + if(startsWithANumber()) { + reconsume(); + return consumeANumericToken(); + } else { + return new DelimToken(code); + } + } + else if(code == 0x2c) return new CommaToken(); + else if(code == 0x2d) { + if(startsWithANumber()) { + reconsume(); + return consumeANumericToken(); + } else if(next(1) == 0x2d && next(2) == 0x3e) { + consume(2); + return new CDCToken(); + } else if(startsWithAnIdentifier()) { + reconsume(); + return consumeAnIdentlikeToken(); + } else { + return new DelimToken(code); + } + } + else if(code == 0x2e) { + if(startsWithANumber()) { + reconsume(); + return consumeANumericToken(); + } else { + return new DelimToken(code); + } + } + else if(code == 0x3a) return new ColonToken; + else if(code == 0x3b) return new SemicolonToken; + else if(code == 0x3c) { + if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) { + consume(3); + return new CDOToken(); + } else { + return new DelimToken(code); + } + } + else if(code == 0x40) { + if(wouldStartAnIdentifier(next(1), next(2), next(3))) { + return new AtKeywordToken(consumeAName()); + } else { + return new DelimToken(code); + } + } + else if(code == 0x5b) return new OpenSquareToken(); + else if(code == 0x5c) { + if(startsWithAValidEscape()) { + reconsume(); + return consumeAnIdentlikeToken(); + } else { + parseerror(); + return new DelimToken(code); + } + } + else if(code == 0x5d) return new CloseSquareToken(); + else if(code == 0x7b) return new OpenCurlyToken(); + else if(code == 0x7d) return new CloseCurlyToken(); + else if(digit(code)) { + reconsume(); + return consumeANumericToken(); + } + else if(namestartchar(code)) { + reconsume(); + return consumeAnIdentlikeToken(); + } + else if(eof()) return new EOFToken(); + else return new DelimToken(code); + }; + + var consumeComments = function() { + while(next(1) == 0x2f && next(2) == 0x2a) { + consume(2); + while(true) { + consume(); + if(code == 0x2a && next() == 0x2f) { + consume(); + break; + } else if(eof()) { + parseerror(); + return; + } + } + } + }; + + var consumeANumericToken = function() { + var {value, isInteger, sign} = consumeANumber(); + if(wouldStartAnIdentifier(next(1), next(2), next(3))) { + const unit = consumeAName(); + return new DimensionToken(value, unit, sign); + } else if(next() == 0x25) { + consume(); + return new PercentageToken(value, sign); + } else { + return new NumberToken(value, isInteger, sign); + } + }; + + var consumeAnIdentlikeToken = function() { + var str = consumeAName(); + if(str.toLowerCase() == "url" && next() == 0x28) { + consume(); + while(whitespace(next(1)) && whitespace(next(2))) consume(); + if(next() == 0x22 || next() == 0x27) { + return new FunctionToken(str); + } else if(whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) { + return new FunctionToken(str); + } else { + return consumeAURLToken(); + } + } else if(next() == 0x28) { + consume(); + return new FunctionToken(str); + } else { + return new IdentToken(str); + } + }; + + var consumeAStringToken = function(endingCodePoint) { + if(endingCodePoint === undefined) endingCodePoint = code; + var string = ""; + while(consume()) { + if(code == endingCodePoint || eof()) { + return new StringToken(string); + } else if(newline(code)) { + parseerror(); + reconsume(); + return new BadStringToken(); + } else if(code == 0x5c) { + if(eof(next())) { + donothing(); + } else if(newline(next())) { + consume(); + } else { + string += String.fromCodePoint(consumeEscape()) + } + } else { + string += String.fromCodePoint(code); + } + } + }; + + var consumeAURLToken = function() { + var token = new URLToken(""); + while(whitespace(next())) consume(); + if(eof(next())) return token; + while(consume()) { + if(code == 0x29 || eof()) { + return token; + } else if(whitespace(code)) { + while(whitespace(next())) consume(); + if(next() == 0x29 || eof(next())) { + consume(); + return token; + } else { + consumeTheRemnantsOfABadURL(); + return new BadURLToken(); + } + } else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) { + parseerror(); + consumeTheRemnantsOfABadURL(); + return new BadURLToken(); + } else if(code == 0x5c) { + if(startsWithAValidEscape()) { + token.value += String.fromCodePoint(consumeEscape()); + } else { + parseerror(); + consumeTheRemnantsOfABadURL(); + return new BadURLToken(); + } + } else { + token.value += String.fromCodePoint(code); + } + } + }; + + var consumeEscape = function() { + // Assume the the current character is the \ + // and the next code point is not a newline. + consume(); + if(hexdigit(code)) { + // Consume 1-6 hex digits + var digits = [code]; + for(var total = 0; total < 5; total++) { + if(hexdigit(next())) { + consume(); + digits.push(code); + } else { + break; + } + } + if(whitespace(next())) consume(); + var value = parseInt(digits.map(function(x){return String.fromCharCode(x);}).join(''), 16); + if( value > maximumallowedcodepoint ) value = 0xfffd; + return value; + } else if(eof()) { + return 0xfffd; + } else { + return code; + } + }; + + var areAValidEscape = function(c1, c2) { + if(c1 != 0x5c) return false; + if(newline(c2)) return false; + return true; + }; + var startsWithAValidEscape = function() { + return areAValidEscape(code, next()); + }; + + var wouldStartAnIdentifier = function(c1, c2, c3) { + if(c1 == 0x2d) { + return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3); + } else if(namestartchar(c1)) { + return true; + } else if(c1 == 0x5c) { + return areAValidEscape(c1, c2); + } else { + return false; + } + }; + var startsWithAnIdentifier = function() { + return wouldStartAnIdentifier(code, next(1), next(2)); + }; + + var wouldStartANumber = function(c1, c2, c3) { + if(c1 == 0x2b || c1 == 0x2d) { + if(digit(c2)) return true; + if(c2 == 0x2e && digit(c3)) return true; + return false; + } else if(c1 == 0x2e) { + if(digit(c2)) return true; + return false; + } else if(digit(c1)) { + return true; + } else { + return false; + } + }; + var startsWithANumber = function() { + return wouldStartANumber(code, next(1), next(2)); + }; + + var consumeAName = function() { + var result = ""; + while(consume()) { + if(namechar(code)) { + result += String.fromCodePoint(code); + } else if(startsWithAValidEscape()) { + result += String.fromCodePoint(consumeEscape()); + } else { + reconsume(); + return result; + } + } + }; + + var consumeANumber = function() { + let isInteger = true; + let sign; + let numberPart = ""; + let exponentPart = ""; + if(next() == 0x2b || next() == 0x2d) { + consume(); + sign = String.fromCodePoint(code); + numberPart += sign; + } + while(digit(next())) { + consume(); + numberPart += String.fromCodePoint(code); + } + if(next(1) == 0x2e && digit(next(2))) { + consume(); + numberPart += "."; + while(digit(next())) { + consume(); + numberPart += String.fromCodePoint(code); + } + isInteger = false; + } + var c1 = next(1), c2 = next(2), c3 = next(3); + const eDigit = (c1 == 0x45 || c1 == 0x65) && digit(c2); + const eSignDigit = (c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3); + if(eDigit || eSignDigit) { + consume(); + if(eSignDigit) { + consume(); + exponentPart += String.fromCodePoint(code); + } + while(digit(next())) { + consume(); + exponentPart += String.fromCodePoint(code); + } + isInteger = false; + } + let value = +numberPart; + if(exponentPart) value = value * Math.pow(10, +exponentPart); + + return {value, isInteger, sign}; + }; + + var consumeTheRemnantsOfABadURL = function() { + while(consume()) { + if(code == 0x29 || eof()) { + return; + } else if(startsWithAValidEscape()) { + consumeEscape(); + donothing(); + } else { + donothing(); + } + } + }; + + + + var iterationCount = 0; + while(!eof(next())) { + tokens.push(consumeAToken()); + iterationCount++; + if(iterationCount > str.length*2) return "I'm infinite-looping!"; + } + return tokens; } class CSSParserToken { - constructor(type) { - this.type = type; - } + constructor(type) { + this.type = type; + } - toJSON() { return {type:this.type}; } - toString() { return this.type; } - toSource() { throw new Exception("Not implemented."); } + toJSON() { return {type:this.type}; } + toString() { return this.type; } + toSource() { throw new Exception("Not implemented."); } } //toJSON() //toString() //toSource() class BadStringToken extends CSSParserToken { - constructor() { - super("BADSTRING"); - } - toSource() { return '"\n"'; } + constructor() { + super("BADSTRING"); + } + toSource() { return '"\n"'; } } class BadURLToken extends CSSParserToken { - constructor() { - super("BADURL"); - } - toSource() { return "url(BADURL '')"} + constructor() { + super("BADURL"); + } + toSource() { return "url(BADURL '')"} } BadURLToken.prototype.tokenType = "BADURL"; class WhitespaceToken extends CSSParserToken { - constructor() { - super("WHITESPACE"); - } - toString() { return "WS"; } - toSource() { return " "; } + constructor() { + super("WHITESPACE"); + } + toString() { return "WS"; } + toSource() { return " "; } } class CDOToken extends CSSParserToken { - constructor() { - super("CDO"); - } - toSource() { return ""; } + constructor() { + super("CDC"); + } + toSource() { return "-->"; } } class ColonToken extends CSSParserToken { - constructor() { - super("COLON"); - } - toSource() { return ":"; } + constructor() { + super("COLON"); + } + toSource() { return ":"; } } class SemicolonToken extends CSSParserToken { - constructor() { - super("SEMICOLON"); - } - toSource() { return ";" }; + constructor() { + super("SEMICOLON"); + } + toSource() { return ";" }; } class CommaToken extends CSSParserToken { - constructor() { - super("COMMA"); - } - toSource() { return "," } + constructor() { + super("COMMA"); + } + toSource() { return "," } } class OpenCurlyToken extends CSSParserToken { - constructor() { - super("OPEN-CURLY"); - this.grouping = true; - this.mirror = CloseCurlyToken; - } - toSource() { return "{"; } + constructor() { + super("OPEN-CURLY"); + this.grouping = true; + this.mirror = CloseCurlyToken; + } + toSource() { return "{"; } } class CloseCurlyToken extends CSSParserToken { - constructor() { - super("CLOSE-CURLY"); - } - toSource() { return "}"; } + constructor() { + super("CLOSE-CURLY"); + } + toSource() { return "}"; } } class OpenSquareToken extends CSSParserToken { - constructor() { - super("OPEN-SQUARE"); - this.grouping = true; - this.mirror = CloseSquareToken; - } - toSource() { return "["; } + constructor() { + super("OPEN-SQUARE"); + this.grouping = true; + this.mirror = CloseSquareToken; + } + toSource() { return "["; } } class CloseSquareToken extends CSSParserToken { - constructor() { - super("CLOSE-SQUARE"); - } - toSource() { return "]"; } + constructor() { + super("CLOSE-SQUARE"); + } + toSource() { return "]"; } } class OpenParenToken extends CSSParserToken { - constructor() { - super("OPEN-PAREN"); - this.grouping = true; - this.mirror = CloseParenToken; - } - toSource() { return "("; } + constructor() { + super("OPEN-PAREN"); + this.grouping = true; + this.mirror = CloseParenToken; + } + toSource() { return "("; } } class CloseParenToken extends CSSParserToken { - constructor() { - super("CLOSE-PAREN"); - } - toSource() { return ")"; } + constructor() { + super("CLOSE-PAREN"); + } + toSource() { return ")"; } } class EOFToken extends CSSParserToken { - constructor() { - super("EOF"); - } - toSource() { return ""; } + constructor() { + super("EOF"); + } + toSource() { return ""; } } class DelimToken extends CSSParserToken { - constructor(val) { - super("DELIM"); - if(typeof val == "number") { - val = String.fromCodePoint(val); - } else { - val = String(val); - } - this.value = val; - } - toString() { return `DELIM(${this.value})`; } - toJSON() { return {type:this.type, value:this.value}; } - toSource() { - if(this.value == "\\") return "\\\n"; - return this.value; - } + constructor(val) { + super("DELIM"); + if(typeof val == "number") { + val = String.fromCodePoint(val); + } else { + val = String(val); + } + this.value = val; + } + toString() { return `DELIM(${this.value})`; } + toJSON() { return {type:this.type, value:this.value}; } + toSource() { + if(this.value == "\\") return "\\\n"; + return this.value; + } } class IdentToken extends CSSParserToken { - constructor(val) { - super("IDENT"); - this.value = val; - } - toString() { return `IDENT(${this.value})`; } - toJSON() { return {type:this.type, value:this.value}; } - toSource() { return escapeIdent(this.value); } + constructor(val) { + super("IDENT"); + this.value = val; + } + toString() { return `IDENT(${this.value})`; } + toJSON() { return {type:this.type, value:this.value}; } + toSource() { return escapeIdent(this.value); } } class FunctionToken extends CSSParserToken { - constructor(val) { - super("FUNCTION"); - this.value = val; - this.mirror = CloseParenToken; - } - toString() { return `FUNCTION(${this.value})`; } - toJSON() { return {type:this.type, value:this.value}; } - toSource() { return escapeIdent(this.value) + "("; } + constructor(val) { + super("FUNCTION"); + this.value = val; + this.mirror = CloseParenToken; + } + toString() { return `FUNCTION(${this.value})`; } + toJSON() { return {type:this.type, value:this.value}; } + toSource() { return escapeIdent(this.value) + "("; } } class AtKeywordToken extends CSSParserToken { - constructor(val) { - super("AT-KEYWORD"); - this.value = val; - } - toString() { return `AT(${this.value})`; } - toJSON() { return {type:this.type, value:this.value }; } - toSource() { return "@" + escapeIdent(this.value); } + constructor(val) { + super("AT-KEYWORD"); + this.value = val; + } + toString() { return `AT(${this.value})`; } + toJSON() { return {type:this.type, value:this.value }; } + toSource() { return "@" + escapeIdent(this.value); } } class HashToken extends CSSParserToken { - constructor(val, isIdent) { - super("HASH"); - this.value = val; - this.isIdent = isIdent; - } - toString() { return `HASH(${this.value})`; } - toJSON() { return {type:this.type, value:this.value, isIdent:this.isIdent}; } - toSource() { - if(this.isIdent) { - return "#" + escapeIdent(this.value); - } - return "#" + escapeHash(this.value); - } + constructor(val, isIdent) { + super("HASH"); + this.value = val; + this.isIdent = isIdent; + } + toString() { return `HASH(${this.value})`; } + toJSON() { return {type:this.type, value:this.value, isIdent:this.isIdent}; } + toSource() { + if(this.isIdent) { + return "#" + escapeIdent(this.value); + } + return "#" + escapeHash(this.value); + } } class StringToken extends CSSParserToken { - constructor(val) { - super("STRING"); - this.value = val; - } - toString() { return `STRING(${this.value})`; } - toJSON() { return {type:this.type, value:this.value}; } - toSource() { return `"${escapeString(this.value)}"`; } + constructor(val) { + super("STRING"); + this.value = val; + } + toString() { return `STRING(${this.value})`; } + toJSON() { return {type:this.type, value:this.value}; } + toSource() { return `"${escapeString(this.value)}"`; } } class URLToken extends CSSParserToken { - constructor(val) { - super("URL"); - this.value = val; - } - toString() { return `URL(${this.value})`; } - toJSON() { return {type:this.type, value:this.value}; } - toSource() { return `url("${escapeString(this.value)}")`; } + constructor(val) { + super("URL"); + this.value = val; + } + toString() { return `URL(${this.value})`; } + toJSON() { return {type:this.type, value:this.value}; } + toSource() { return `url("${escapeString(this.value)}")`; } } class NumberToken extends CSSParserToken { - constructor(val, isInteger, sign=undefined) { - super("NUMBER"); - this.value = val; - this.isInteger = isInteger; - this.sign = sign; - } - toString() { - const name = this.isInteger ? "INT" : "NUMBER"; - const sign = this.sign == "+" ? "+" : ""; - return `${name}(${sign}${this.value})`; - } - toJSON() { return {type:this.type, value:this.value, isInteger:this.isInteger, sign:this.sign}; } - toSource() { return formatNumber(this.value, this.sign); } + constructor(val, isInteger, sign=undefined) { + super("NUMBER"); + this.value = val; + this.isInteger = isInteger; + this.sign = sign; + } + toString() { + const name = this.isInteger ? "INT" : "NUMBER"; + const sign = this.sign == "+" ? "+" : ""; + return `${name}(${sign}${this.value})`; + } + toJSON() { return {type:this.type, value:this.value, isInteger:this.isInteger, sign:this.sign}; } + toSource() { return formatNumber(this.value, this.sign); } } class PercentageToken extends CSSParserToken { - constructor(val, sign=undefined) { - super("PERCENTAGE"); - this.value = val; - this.sign = sign; - } - toString() { - const sign = this.sign == "+" ? "+" : ""; - return `PERCENTAGE(${sign}${this.value})`; - } - toJSON() { return {type:this.type, value:this.value, sign:this.sign}; } - toSource() { return `${formatNumber(this.value, this.sign)}%`; } + constructor(val, sign=undefined) { + super("PERCENTAGE"); + this.value = val; + this.sign = sign; + } + toString() { + const sign = this.sign == "+" ? "+" : ""; + return `PERCENTAGE(${sign}${this.value})`; + } + toJSON() { return {type:this.type, value:this.value, sign:this.sign}; } + toSource() { return `${formatNumber(this.value, this.sign)}%`; } } class DimensionToken extends CSSParserToken { - constructor(val, unit, sign=undefined) { - super("DIMENSION"); - this.value = val; - this.unit = unit; - this.sign = sign; - } - toString() { - const sign = this.sign == "+" ? "+" : ""; - return `DIM(${sign}${this.value}, ${this.unit})`; - } - toJSON() { return {type:this.type, value:this.value, unit:this.unit}; } - toSource() { - let unit = escapeIdent(this.unit); - if(unit[0].toLowerCase() == "e" && (unit[1] == "-" || digit(unit[1].charCodeAt(0)))) { - // Unit is ambiguous with scinot - // Remove the leading "e", replace with escape. - unit = "\\65 " + unit.slice(1, unit.length); - } - return `${formatNumber(this.value, this.sign)}${unit}`; - } + constructor(val, unit, sign=undefined) { + super("DIMENSION"); + this.value = val; + this.unit = unit; + this.sign = sign; + } + toString() { + const sign = this.sign == "+" ? "+" : ""; + return `DIM(${sign}${this.value}, ${this.unit})`; + } + toJSON() { return {type:this.type, value:this.value, unit:this.unit}; } + toSource() { + let unit = escapeIdent(this.unit); + if(unit[0].toLowerCase() == "e" && (unit[1] == "-" || digit(unit[1].charCodeAt(0)))) { + // Unit is ambiguous with scinot + // Remove the leading "e", replace with escape. + unit = "\\65 " + unit.slice(1, unit.length); + } + return `${formatNumber(this.value, this.sign)}${unit}`; + } } function escapeIdent(string) { - return Array.from(String(string), (e,i)=>{ - const code = e.codePointAt(0); - if(i == 0) { - if(namestartchar(code)) return e; - return escapeIdentCode(code); - } - if(namechar(code)) return e; - return escapeIdentCode(code); - }).join(""); + return Array.from(String(string), (e,i)=>{ + const code = e.codePointAt(0); + if(i == 0) { + if(namestartchar(code)) return e; + return escapeIdentCode(code); + } + if(namechar(code)) return e; + return escapeIdentCode(code); + }).join(""); } function escapeIdentCode(code) { - if(digit(code) || letter(code)) { - return `\\${code.toString(16)} `; - } - return "\\"+String.fromCodePoint(code); + if(digit(code) || letter(code)) { + return `\\${code.toString(16)} `; + } + return "\\"+String.fromCodePoint(code); } function escapeHash(string) { - // Escapes the value (after the #) of a hash. - return Array.from(String(string), e=>{ - const code = e.codePointAt(0); - if(namechar(code)) return e; - return escapeIdentCode(code); - }).join(""); + // Escapes the value (after the #) of a hash. + return Array.from(String(string), e=>{ + const code = e.codePointAt(0); + if(namechar(code)) return e; + return escapeIdentCode(code); + }).join(""); } function escapeString(string) { - // Escapes the contents (between the quotes) of a string - return Array.from(String(string), e=>{ - const code = e.codePointAt(0); - if(between(code, 0x0, 0x1f) - || code == 0x7f - || code == 0x22 - || code == 0x5c - ) { - return "\\" + code.toString(16) + " "; - } - return e; - }).join(""); + // Escapes the contents (between the quotes) of a string + return Array.from(String(string), e=>{ + const code = e.codePointAt(0); + if(between(code, 0x0, 0x1f) + || code == 0x7f + || code == 0x22 + || code == 0x5c + ) { + return "\\" + code.toString(16) + " "; + } + return e; + }).join(""); } function formatNumber(num, sign=undefined) { - // TODO: Fix this to match CSS stringification behavior. - return (sign == "+" ? "+" : "") + String(num); + // TODO: Fix this to match CSS stringification behavior. + return (sign == "+" ? "+" : "") + String(num); } // Exportation. @@ -832,568 +832,568 @@ exports.EOFToken = EOFToken; exports.CSSParserToken = CSSParserToken; class TokenStream { - constructor(tokens) { - // Assume that tokens is an array. - this.tokens = tokens; - this.i = 0; - this.marks = []; - } - nextToken() { - if(this.i < this.tokens.length) return this.tokens[this.i]; - return new EOFToken(); - } - empty() { - return this.i >= this.tokens.length; - } - consumeToken() { - const tok = this.nextToken(); - this.i++; - return tok; - } - discardToken() { - this.i++; - } - mark() { - this.marks.push(this.i); - return this; - } - restoreMark() { - if(this.marks.length) { - this.i = this.marks.pop(); - return this; - } - throw new Error("No marks to restore."); - } - discardMark() { - if(this.marks.length) { - this.marks.pop(); - return this; - } - throw new Error("No marks to restore."); - } - discardWhitespace() { - while(this.nextToken() instanceof WhitespaceToken) { - this.discardToken(); - } - return this; - } + constructor(tokens) { + // Assume that tokens is an array. + this.tokens = tokens; + this.i = 0; + this.marks = []; + } + nextToken() { + if(this.i < this.tokens.length) return this.tokens[this.i]; + return new EOFToken(); + } + empty() { + return this.i >= this.tokens.length; + } + consumeToken() { + const tok = this.nextToken(); + this.i++; + return tok; + } + discardToken() { + this.i++; + } + mark() { + this.marks.push(this.i); + return this; + } + restoreMark() { + if(this.marks.length) { + this.i = this.marks.pop(); + return this; + } + throw new Error("No marks to restore."); + } + discardMark() { + if(this.marks.length) { + this.marks.pop(); + return this; + } + throw new Error("No marks to restore."); + } + discardWhitespace() { + while(this.nextToken() instanceof WhitespaceToken) { + this.discardToken(); + } + return this; + } } function parseerror(s, msg) { - console.log("Parse error at token " + s.i + ": " + s.tokens[s.i] + ".\n" + msg); - return true; + console.log("Parse error at token " + s.i + ": " + s.tokens[s.i] + ".\n" + msg); + return true; } function consumeAStylesheetsContents(s) { - const rules = []; - while(1) { - const token = s.nextToken(); - if(token instanceof WhitespaceToken) { - s.discardToken(); - } else if(token instanceof EOFToken) { - return rules; - } else if(token instanceof CDOToken || token instanceof CDCToken) { - s.discardToken(); - } else if(token instanceof AtKeywordToken) { - const rule = consumeAnAtRule(s) - if(rule) rules.push(rule); - } else { - const rule = consumeAQualifiedRule(s); - if(rule) rules.push(rule); - } - } + const rules = []; + while(1) { + const token = s.nextToken(); + if(token instanceof WhitespaceToken) { + s.discardToken(); + } else if(token instanceof EOFToken) { + return rules; + } else if(token instanceof CDOToken || token instanceof CDCToken) { + s.discardToken(); + } else if(token instanceof AtKeywordToken) { + const rule = consumeAnAtRule(s) + if(rule) rules.push(rule); + } else { + const rule = consumeAQualifiedRule(s); + if(rule) rules.push(rule); + } + } } function consumeAnAtRule(s, nested=false) { - const token = s.consumeToken(); - if(!(token instanceof AtKeywordToken)) - throw new Error("consumeAnAtRule() called with an invalid token stream state."); - const rule = new AtRule(token.value); - while(1) { - const token = s.nextToken(); - if(token instanceof SemicolonToken || token instanceof EOFToken) { - s.discardToken(); - return filterValid(rule); - } else if(token instanceof CloseCurlyToken) { - if(nested) return filterValid(rule); - else { - parseerror(s, "Hit an unmatched } in the prelude of an at-rule."); - rule.prelude.push(consumeToken(s)); - } - } else if(token instanceof OpenCurlyToken) { - [rule.declarations, rule.rules] = consumeABlock(s); - return filterValid(rule); - } else { - rule.prelude.push(consumeAComponentValue(s)); - } - } + const token = s.consumeToken(); + if(!(token instanceof AtKeywordToken)) + throw new Error("consumeAnAtRule() called with an invalid token stream state."); + const rule = new AtRule(token.value); + while(1) { + const token = s.nextToken(); + if(token instanceof SemicolonToken || token instanceof EOFToken) { + s.discardToken(); + return filterValid(rule); + } else if(token instanceof CloseCurlyToken) { + if(nested) return filterValid(rule); + else { + parseerror(s, "Hit an unmatched } in the prelude of an at-rule."); + rule.prelude.push(consumeToken(s)); + } + } else if(token instanceof OpenCurlyToken) { + [rule.declarations, rule.rules] = consumeABlock(s); + return filterValid(rule); + } else { + rule.prelude.push(consumeAComponentValue(s)); + } + } } function consumeAQualifiedRule(s, nested=false, stopToken=EOFToken) { - var rule = new QualifiedRule(); - while(1) { - const token = s.nextToken(); - if(token instanceof EOFToken || token instanceof stopToken) { - parseerror(s, "Hit EOF or semicolon when trying to parse the prelude of a qualified rule."); - return; - } else if(token instanceof CloseCurlyToken) { - parseerror("Hit an unmatched } in the prelude of a qualified rule."); - if(nested) return; - else { - rule.prelude.push(consumeToken(s)); - } - } else if(token instanceof OpenCurlyToken) { - if(looksLikeACustomProperty(rule.prelude)) { - consumeTheRemnantsOfABadDeclaration(s, nested); - return; - } - [rule.declarations, rule.rules] = consumeABlock(s); - return filterValid(rule); - } else { - rule.prelude.push(consumeAComponentValue(s)); - } - } + var rule = new QualifiedRule(); + while(1) { + const token = s.nextToken(); + if(token instanceof EOFToken || token instanceof stopToken) { + parseerror(s, "Hit EOF or semicolon when trying to parse the prelude of a qualified rule."); + return; + } else if(token instanceof CloseCurlyToken) { + parseerror("Hit an unmatched } in the prelude of a qualified rule."); + if(nested) return; + else { + rule.prelude.push(consumeToken(s)); + } + } else if(token instanceof OpenCurlyToken) { + if(looksLikeACustomProperty(rule.prelude)) { + consumeTheRemnantsOfABadDeclaration(s, nested); + return; + } + [rule.declarations, rule.rules] = consumeABlock(s); + return filterValid(rule); + } else { + rule.prelude.push(consumeAComponentValue(s)); + } + } } function looksLikeACustomProperty(tokens) { - let foundDashedIdent = false; - for(const token of tokens) { - if(token instanceof WhitespaceToken) continue; - if(!foundDashedIdent && token instanceof IdentToken && token.value.slice(0, 2) == "--") { - foundDashedIdent = true; - continue; - } - if(foundDashedIdent && token instanceof ColonToken) { - return true; - } - return false; - } - return false; + let foundDashedIdent = false; + for(const token of tokens) { + if(token instanceof WhitespaceToken) continue; + if(!foundDashedIdent && token instanceof IdentToken && token.value.slice(0, 2) == "--") { + foundDashedIdent = true; + continue; + } + if(foundDashedIdent && token instanceof ColonToken) { + return true; + } + return false; + } + return false; } function consumeABlock(s) { - if(!(s.nextToken() instanceof OpenCurlyToken)) { - throw new Error("consumeABlock() called with an invalid token stream state."); - } - s.discardToken(); - const [decls, rules] = consumeABlocksContents(s); - s.discardToken(); - return [decls, rules]; + if(!(s.nextToken() instanceof OpenCurlyToken)) { + throw new Error("consumeABlock() called with an invalid token stream state."); + } + s.discardToken(); + const [decls, rules] = consumeABlocksContents(s); + s.discardToken(); + return [decls, rules]; } function consumeABlocksContents(s) { - const decls = []; - const rules = []; - while(1) { - const token = s.nextToken(); - if(token instanceof WhitespaceToken || token instanceof SemicolonToken) { - s.discardToken(); - } else if(token instanceof EOFToken || token instanceof CloseCurlyToken) { - return [decls, rules]; - } else if(token instanceof AtKeywordToken) { - const rule = consumeAnAtRule(s, true); - if(rule) rules.push(rule); - } else { - s.mark(); - const decl = consumeADeclaration(s, true); - if(decl) { - decls.push(decl); - s.discardMark(); - continue; - } - s.restoreMark(); - const rule = consumeAQualifiedRule(s, true, SemicolonToken); - if(rule) rules.push(rule); - } - } + const decls = []; + const rules = []; + while(1) { + const token = s.nextToken(); + if(token instanceof WhitespaceToken || token instanceof SemicolonToken) { + s.discardToken(); + } else if(token instanceof EOFToken || token instanceof CloseCurlyToken) { + return [decls, rules]; + } else if(token instanceof AtKeywordToken) { + const rule = consumeAnAtRule(s, true); + if(rule) rules.push(rule); + } else { + s.mark(); + const decl = consumeADeclaration(s, true); + if(decl) { + decls.push(decl); + s.discardMark(); + continue; + } + s.restoreMark(); + const rule = consumeAQualifiedRule(s, true, SemicolonToken); + if(rule) rules.push(rule); + } + } } function consumeADeclaration(s, nested=false) { - let decl; - if(s.nextToken() instanceof IdentToken) { - decl = new Declaration(s.consumeToken().value); - } else { - consumeTheRemnantsOfABadDeclaration(s, nested); - return; - } - s.discardWhitespace(); - if(s.nextToken() instanceof ColonToken) { - s.discardToken(); - } else { - consumeTheRemnantsOfABadDeclaration(s, nested); - return; - } - s.discardWhitespace(); - decl.value = consumeAListOfComponentValues(s, nested, SemicolonToken); - - var foundImportant = false; - for(var i = decl.value.length - 1; i >= 0; i--) { - if(decl.value[i] instanceof WhitespaceToken) { - continue; - } else if(!foundImportant && decl.value[i] instanceof IdentToken && asciiCaselessMatch(decl.value[i].value, "important")) { - foundImportant = true; - } else if(foundImportant && decl.value[i] instanceof DelimToken && decl.value[i].value == "!") { - decl.value.length = i; - decl.important = true; - break; - } else { - break; - } - } - - var i = decl.value.length - 1; - while(decl.value[i] instanceof WhitespaceToken) { - decl.value.length = i; - i--; - } - return filterValid(decl); + let decl; + if(s.nextToken() instanceof IdentToken) { + decl = new Declaration(s.consumeToken().value); + } else { + consumeTheRemnantsOfABadDeclaration(s, nested); + return; + } + s.discardWhitespace(); + if(s.nextToken() instanceof ColonToken) { + s.discardToken(); + } else { + consumeTheRemnantsOfABadDeclaration(s, nested); + return; + } + s.discardWhitespace(); + decl.value = consumeAListOfComponentValues(s, nested, SemicolonToken); + + var foundImportant = false; + for(var i = decl.value.length - 1; i >= 0; i--) { + if(decl.value[i] instanceof WhitespaceToken) { + continue; + } else if(!foundImportant && decl.value[i] instanceof IdentToken && asciiCaselessMatch(decl.value[i].value, "important")) { + foundImportant = true; + } else if(foundImportant && decl.value[i] instanceof DelimToken && decl.value[i].value == "!") { + decl.value.length = i; + decl.important = true; + break; + } else { + break; + } + } + + var i = decl.value.length - 1; + while(decl.value[i] instanceof WhitespaceToken) { + decl.value.length = i; + i--; + } + return filterValid(decl); } function consumeTheRemnantsOfABadDeclaration(s, nested) { - while(1) { - const token = s.nextToken(); - if(token instanceof EOFToken || token instanceof SemicolonToken) { - s.discardToken(); - return; - } else if(token instanceof CloseCurlyToken) { - if(nested) return; - else s.discardToken(); - } else { - consumeAComponentValue(s); - } - } + while(1) { + const token = s.nextToken(); + if(token instanceof EOFToken || token instanceof SemicolonToken) { + s.discardToken(); + return; + } else if(token instanceof CloseCurlyToken) { + if(nested) return; + else s.discardToken(); + } else { + consumeAComponentValue(s); + } + } } function consumeAListOfComponentValues(s, nested=false, stopToken=EOFToken) { - const values = []; - while(1) { - const token = s.nextToken(); - if(token instanceof EOFToken || token instanceof stopToken) { - return values; - } else if(token instanceof CloseCurlyToken) { - if(nested) return values; - else { - parseerror(s, "Hit an unmatched } in a declaration value."); - values.push(s.consumeToken()); - } - } else { - values.push(consumeAComponentValue(s)); - } - } + const values = []; + while(1) { + const token = s.nextToken(); + if(token instanceof EOFToken || token instanceof stopToken) { + return values; + } else if(token instanceof CloseCurlyToken) { + if(nested) return values; + else { + parseerror(s, "Hit an unmatched } in a declaration value."); + values.push(s.consumeToken()); + } + } else { + values.push(consumeAComponentValue(s)); + } + } } function consumeAComponentValue(s) { - const token = s.nextToken(); - if(token instanceof OpenCurlyToken || token instanceof OpenSquareToken || token instanceof OpenParenToken) - return consumeASimpleBlock(s); - if(token instanceof FunctionToken) - return consumeAFunction(s); - return s.consumeToken(); + const token = s.nextToken(); + if(token instanceof OpenCurlyToken || token instanceof OpenSquareToken || token instanceof OpenParenToken) + return consumeASimpleBlock(s); + if(token instanceof FunctionToken) + return consumeAFunction(s); + return s.consumeToken(); } function consumeASimpleBlock(s) { - if(!s.nextToken().mirror) { - throw new Error("consumeASimpleBlock() called with an invalid token stream state."); - } - const start = s.nextToken(); - const block = new SimpleBlock(start.toSource()); - s.discardToken(); - while(1) { - const token = s.nextToken(); - if(token instanceof EOFToken || token instanceof start.mirror) { - s.discardToken(); - return block; - } else { - block.value.push(consumeAComponentValue(s)); - } - } + if(!s.nextToken().mirror) { + throw new Error("consumeASimpleBlock() called with an invalid token stream state."); + } + const start = s.nextToken(); + const block = new SimpleBlock(start.toSource()); + s.discardToken(); + while(1) { + const token = s.nextToken(); + if(token instanceof EOFToken || token instanceof start.mirror) { + s.discardToken(); + return block; + } else { + block.value.push(consumeAComponentValue(s)); + } + } } function consumeAFunction(s) { - if(!(s.nextToken() instanceof FunctionToken)) { - throw new Error("consumeAFunction() called with an invalid token stream state."); - } - var func = new Func(s.consumeToken().value); - while(1) { - const token = s.nextToken(); - if(token instanceof EOFToken || token instanceof CloseParenToken) { - s.discardToken(); - return func; - } else { - func.value.push(consumeAComponentValue(s)); - } - } + if(!(s.nextToken() instanceof FunctionToken)) { + throw new Error("consumeAFunction() called with an invalid token stream state."); + } + var func = new Func(s.consumeToken().value); + while(1) { + const token = s.nextToken(); + if(token instanceof EOFToken || token instanceof CloseParenToken) { + s.discardToken(); + return func; + } else { + func.value.push(consumeAComponentValue(s)); + } + } } function isValidInContext(construct, context) { - // Trivial validator, without any special CSS knowledge. - - // All at-rules are valid, who cares. - if(construct.type == "AT-RULE") return true; - - // Exclude qualified rules that ended up with a semicolon - // in their prelude. - // (Can only happen at the top level of a stylesheet.) - if(construct.type == "QUALIFIED-RULE") { - for(const val of construct.prelude) { - if(val.type == "SEMICOLON") return false; - } - return true; - } - - // Exclude properties that ended up with a {}-block - // in their value, unless they're custom. - if(construct.type == "DECLARATION") { - if(construct.name.slice(0, 2) == "--") return true; - for(const val of construct.value) { - if(val.type == "BLOCK" && val.name == "{") return false; - } - return true; - } + // Trivial validator, without any special CSS knowledge. + + // All at-rules are valid, who cares. + if(construct.type == "AT-RULE") return true; + + // Exclude qualified rules that ended up with a semicolon + // in their prelude. + // (Can only happen at the top level of a stylesheet.) + if(construct.type == "QUALIFIED-RULE") { + for(const val of construct.prelude) { + if(val.type == "SEMICOLON") return false; + } + return true; + } + + // Exclude properties that ended up with a {}-block + // in their value, unless they're custom. + if(construct.type == "DECLARATION") { + if(construct.name.slice(0, 2) == "--") return true; + for(const val of construct.value) { + if(val.type == "BLOCK" && val.name == "{") return false; + } + return true; + } } function filterValid(construct, context) { - if(isValidInContext(construct, context)) return construct; - return; + if(isValidInContext(construct, context)) return construct; + return; } function normalizeInput(input) { - if(typeof input == "string") - return new TokenStream(tokenize(input)); - if(input instanceof TokenStream) - return input; - if(input.length !== undefined) - return new TokenStream(input); - else throw SyntaxError(input); + if(typeof input == "string") + return new TokenStream(tokenize(input)); + if(input instanceof TokenStream) + return input; + if(input.length !== undefined) + return new TokenStream(input); + else throw SyntaxError(input); } function parseAStylesheet(s) { - s = normalizeInput(s); - var sheet = new Stylesheet(); - sheet.rules = consumeAStylesheetsContents(s); - return sheet; + s = normalizeInput(s); + var sheet = new Stylesheet(); + sheet.rules = consumeAStylesheetsContents(s); + return sheet; } function parseAStylesheetsContents(s) { - s = normalizeInput(s); - return consumeAStylesheetsContents(s); + s = normalizeInput(s); + return consumeAStylesheetsContents(s); } function parseABlocksContents(s) { - s = normalizeInput(s); - return consumeABlocksContents(s); + s = normalizeInput(s); + return consumeABlocksContents(s); } function parseARule(s) { - s = normalizeInput(s); - let rule; - s.discardWhitespace(); - if(s.nextToken() instanceof EOFToken) throw SyntaxError(); - if(s.nextToken() instanceof AtKeywordToken) { - rule = consumeAnAtRule(s); - } else { - rule = consumeAQualifiedRule(s); - if(!rule) throw SyntaxError(); - } - s.discardWhitespace(); - if(s.nextToken() instanceof EOFToken) return rule; - throw SyntaxError(); + s = normalizeInput(s); + let rule; + s.discardWhitespace(); + if(s.nextToken() instanceof EOFToken) throw SyntaxError(); + if(s.nextToken() instanceof AtKeywordToken) { + rule = consumeAnAtRule(s); + } else { + rule = consumeAQualifiedRule(s); + if(!rule) throw SyntaxError(); + } + s.discardWhitespace(); + if(s.nextToken() instanceof EOFToken) return rule; + throw SyntaxError(); } function parseADeclaration(s) { - s = normalizeInput(s); - s.discardWhitespace(); - const decl = consumeADeclaration(s); - if(decl) return decl - throw SyntaxError(); + s = normalizeInput(s); + s.discardWhitespace(); + const decl = consumeADeclaration(s); + if(decl) return decl + throw SyntaxError(); } function parseAComponentValue(s) { - s = normalizeInput(s); - s.discardWhitespace(); - if(s.empty()) throw SyntaxError(); - const val = consumeAComponentValue(s); - s.discardWhitespace(); - if(s.empty()) return val; - throw SyntaxError(); + s = normalizeInput(s); + s.discardWhitespace(); + if(s.empty()) throw SyntaxError(); + const val = consumeAComponentValue(s); + s.discardWhitespace(); + if(s.empty()) return val; + throw SyntaxError(); } function parseAListOfComponentValues(s) { - s = normalizeInput(s); - return consumeAListOfComponentValues(s); + s = normalizeInput(s); + return consumeAListOfComponentValues(s); } function parseACommaSeparatedListOfComponentValues(s) { - s = normalizeInput(s); - const groups = []; - while(!s.empty()) { - groups.push(consumeAListOfComponentValues(s, false, CommaToken)); - s.discardToken(); - } - return groups; + s = normalizeInput(s); + const groups = []; + while(!s.empty()) { + groups.push(consumeAListOfComponentValues(s, false, CommaToken)); + s.discardToken(); + } + return groups; } class CSSParserRule { - constructor(type) { this.type = type; } - toString(indent) { - return JSON.stringify(this,null,indent); - } + constructor(type) { this.type = type; } + toString(indent) { + return JSON.stringify(this,null,indent); + } } class Stylesheet extends CSSParserRule { - constructor() { - super("STYLESHEET"); - this.rules = []; - return this; - } - toJSON() { - return { - type: this.type, - rules: this.rules, - } - } - toSource() { - return this.rules.map(x=>x.toSource()).join("\n"); - } + constructor() { + super("STYLESHEET"); + this.rules = []; + return this; + } + toJSON() { + return { + type: this.type, + rules: this.rules, + } + } + toSource() { + return this.rules.map(x=>x.toSource()).join("\n"); + } } class AtRule extends CSSParserRule { - constructor(name) { - super("AT-RULE"); - this.name = name; - this.prelude = []; - this.declarations = null; - this.rules = null; - return this; - } - toJSON() { - return { - type: this.type, - name: this.name, - prelude: this.prelude, - declarations: this.declarations, - rules: this.rules, - } - } - toSource(indent=0) { - let s = printIndent(indent) + "@" + escapeIdent(this.name); - s += this.prelude.map(x=>x.toSource()).join(""); - if(this.declarations == null) { - s += ";\n"; - return s; - } - s += "{\n"; - if(this.declarations.length) { - s += this.declarations.map(x=>x.toSource(indent+1)).join("\n") + "\n"; - } - if(this.rules.length) { - s += this.rules.map(x=>x.toSource(indent+1)).join("\n") + "\n"; - } - s += printIndent(indent) + "}"; - return s; - } + constructor(name) { + super("AT-RULE"); + this.name = name; + this.prelude = []; + this.declarations = null; + this.rules = null; + return this; + } + toJSON() { + return { + type: this.type, + name: this.name, + prelude: this.prelude, + declarations: this.declarations, + rules: this.rules, + } + } + toSource(indent=0) { + let s = printIndent(indent) + "@" + escapeIdent(this.name); + s += this.prelude.map(x=>x.toSource()).join(""); + if(this.declarations == null) { + s += ";\n"; + return s; + } + s += "{\n"; + if(this.declarations.length) { + s += this.declarations.map(x=>x.toSource(indent+1)).join("\n") + "\n"; + } + if(this.rules.length) { + s += this.rules.map(x=>x.toSource(indent+1)).join("\n") + "\n"; + } + s += printIndent(indent) + "}"; + return s; + } } class QualifiedRule extends CSSParserRule { - constructor() { - super("QUALIFIED-RULE"); - this.prelude = []; - this.declarations = []; - this.rules = []; - return this; - } - toJSON() { - return { - type: this.type, - prelude: this.prelude, - declarations: this.declarations, - rules: this.rules, - } - } - toSource(indent=0) { - let s = printIndent(indent); - s += this.prelude.map(x=>x.toSource()).join(""); - s += "{\n"; - if(this.declarations.length) { - s += this.declarations.map(x=>x.toSource(indent+1)).join("\n") + "\n"; - } - if(this.rules.length) { - s += this.rules.map(x=>x.toSource(indent+1)).join("\n") + "\n"; - } - s += printIndent(indent) + "}"; - return s; - } + constructor() { + super("QUALIFIED-RULE"); + this.prelude = []; + this.declarations = []; + this.rules = []; + return this; + } + toJSON() { + return { + type: this.type, + prelude: this.prelude, + declarations: this.declarations, + rules: this.rules, + } + } + toSource(indent=0) { + let s = printIndent(indent); + s += this.prelude.map(x=>x.toSource()).join(""); + s += "{\n"; + if(this.declarations.length) { + s += this.declarations.map(x=>x.toSource(indent+1)).join("\n") + "\n"; + } + if(this.rules.length) { + s += this.rules.map(x=>x.toSource(indent+1)).join("\n") + "\n"; + } + s += printIndent(indent) + "}"; + return s; + } } class Declaration extends CSSParserRule { - constructor(name) { - super("DECLARATION") - this.name = name; - this.value = []; - this.important = false; - return this; - } - toJSON() { - return { - type: this.type, - name: this.name, - value: this.value, - important: this.important, - } - } - toSource(indent=0) { - let s = printIndent(indent) + escapeIdent(this.name) + ": "; - s += this.value.map(x=>x.toSource()).join(""); - if(this.important) { - s += "!important"; - } - s += ";"; - return s; - } + constructor(name) { + super("DECLARATION") + this.name = name; + this.value = []; + this.important = false; + return this; + } + toJSON() { + return { + type: this.type, + name: this.name, + value: this.value, + important: this.important, + } + } + toSource(indent=0) { + let s = printIndent(indent) + escapeIdent(this.name) + ": "; + s += this.value.map(x=>x.toSource()).join(""); + if(this.important) { + s += "!important"; + } + s += ";"; + return s; + } } class SimpleBlock extends CSSParserRule { - constructor(type) { - super("BLOCK"); - this.name = type; - this.value = []; - return this; - } - toJSON() { - return { - type: this.type, - name: this.name, - value: this.value, - } - } - toSource() { - const mirror = {"{":"}", "[":"]", "(":")"}; - return this.name + this.value.map(x=>x.toSource()).join("") + mirror[this.name]; - } + constructor(type) { + super("BLOCK"); + this.name = type; + this.value = []; + return this; + } + toJSON() { + return { + type: this.type, + name: this.name, + value: this.value, + } + } + toSource() { + const mirror = {"{":"}", "[":"]", "(":")"}; + return this.name + this.value.map(x=>x.toSource()).join("") + mirror[this.name]; + } } class Func extends CSSParserRule { - constructor(name) { - super("FUNCTION"); - this.name = name; - this.value = []; - return this; - } - toJSON() { - return { - type: this.type, - name: this.name, - value: this.value, - } - } - toSource() { - return escapeIdent(this.name) + "(" + this.value.map(x=>x.toSource()).join("") + ")"; - } + constructor(name) { + super("FUNCTION"); + this.name = name; + this.value = []; + return this; + } + toJSON() { + return { + type: this.type, + name: this.name, + value: this.value, + } + } + toSource() { + return escapeIdent(this.name) + "(" + this.value.map(x=>x.toSource()).join("") + ")"; + } } function printIndent(level) { - return "\t".repeat(level); + return "\t".repeat(level); } diff --git a/test.html b/test.html index 9e3c444..b74f257 100644 --- a/test.html +++ b/test.html @@ -9,396 +9,396 @@

 
 
\ No newline at end of file

From e3d0912e35ab10a0e0aa54fad4c291e182e64517 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 15:09:30 +0800
Subject: [PATCH 03/25] Use 2-space indentation for test output

---
 test.html | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/test.html b/test.html
index b74f257..672398b 100644
--- a/test.html
+++ b/test.html
@@ -371,8 +371,8 @@
 for(const [i, test] of enumerate(TESTS, 1)) {
   const tokens = tokenize(test.css);
   const sheet = parseAStylesheet(tokens);
-  const dump = sheet.toString('\t');
-  const expected_dump = JSON.stringify(test.expected, null, '\t');
+  const dump = sheet.toString('  ');
+  const expected_dump = JSON.stringify(test.expected, null, '  ');
   if (dump == expected_dump) {
       log(`Test ${i} of ${TESTS.length}: PASS`);
   } else {
@@ -397,8 +397,3 @@
     log(diffString(total+' tests, '+failures+' failures :(', total+' tests, '));
 }
 
-
\ No newline at end of file

From 4c7d753d36ff3e8727cdf633538ddd41d1dd0e20 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 14:41:35 +0800
Subject: [PATCH 04/25] Fix more bad indentation

---
 test.html | 14 +++++++-------
 tests.js  | 32 ++++++++++++++++----------------
 2 files changed, 23 insertions(+), 23 deletions(-)

diff --git a/test.html b/test.html
index 672398b..ca8639b 100644
--- a/test.html
+++ b/test.html
@@ -9,7 +9,7 @@
 
 
 

@@ -374,11 +374,11 @@
   const dump = sheet.toString('  ');
   const expected_dump = JSON.stringify(test.expected, null, '  ');
   if (dump == expected_dump) {
-      log(`Test ${i} of ${TESTS.length}: PASS`);
+    log(`Test ${i} of ${TESTS.length}: PASS`);
   } else {
-      log(`Test ${i} of ${TESTS.length}: FAIL\nCSS: ${test.css}\nTokens: ${tokens.join(' ')}`);
-      log(diffString(expected_dump, dump));
-      failures++;
+    log(`Test ${i} of ${TESTS.length}: FAIL\nCSS: ${test.css}\nTokens: ${tokens.join(' ')}`);
+    log(diffString(expected_dump, dump));
+    failures++;
   }
 
 }
@@ -392,8 +392,8 @@
 
 // Abuse the differ to get colored output
 if (failures == 0) {
-    log(diffString(total+' tests, ', total+' tests, all passed :)'));
+  log(diffString(total+' tests, ', total+' tests, all passed :)'));
 } else {
-    log(diffString(total+' tests, '+failures+' failures :(', total+' tests, '));
+  log(diffString(total+' tests, '+failures+' failures :(', total+' tests, '));
 }
 
diff --git a/tests.js b/tests.js
index a8ae3e3..99c48c9 100644
--- a/tests.js
+++ b/tests.js
@@ -51,25 +51,25 @@ var total = TESTS.length, failures = 0,
     i, test, tokens, sheet, dump, expected_dump;
 
 for (i = 0; i < total; i++) {
-    test = TESTS[i];
-    sheet = parseAStylesheet(test.css);
-    dump = sheet.toString('  ');
-    expected_dump = JSON.stringify(test.expected, null, '  ');
-    if (dump == expected_dump) {
-        console.log('Test %d of %d: PASS', i, total);
-    } else {
-        console.log('Test %d of %d: FAIL\nCSS: %s\nTokens: %s',
-            i, total, test.css, tokens.join(' '));
-        console.log(ansidiff.lines(expected_dump, dump));
-        failures++;
-    }
+  test = TESTS[i];
+  sheet = parseAStylesheet(test.css);
+  dump = sheet.toString('  ');
+  expected_dump = JSON.stringify(test.expected, null, '  ');
+  if (dump == expected_dump) {
+    console.log('Test %d of %d: PASS', i, total);
+  } else {
+    console.log('Test %d of %d: FAIL\nCSS: %s\nTokens: %s',
+        i, total, test.css, tokens.join(' '));
+    console.log(ansidiff.lines(expected_dump, dump));
+    failures++;
+  }
 }
 
 // Abuse the differ to get colored output
 if (failures == 0) {
-    console.log(ansidiff.words('%d tests, ', '%d tests, all passed :)'),
-                total);
+  console.log(ansidiff.words('%d tests, ', '%d tests, all passed :)'),
+              total);
 } else {
-    console.log(ansidiff.words('%d tests, %d failures :(', '%d tests, '),
-                total, failures);
+  console.log(ansidiff.words('%d tests, %d failures :(', '%d tests, '),
+              total, failures);
 }

From 6c911de7d7a2b41b0b19834f074c849b1efb27fb Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 14:57:15 +0800
Subject: [PATCH 05/25] Support globalThis

---
 parse-css.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/parse-css.js b/parse-css.js
index 6e1eab8..8f99a62 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -1,5 +1,5 @@
 "use strict";
-(function (root, factory) {
+(function (global, factory) {
   // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
   // Rhino, and plain browser loading.
   if (typeof define === 'function' && define.amd) {
@@ -7,7 +7,8 @@
   } else if (typeof exports !== 'undefined') {
     factory(exports);
   } else {
-    factory(root);
+    global = typeof globalThis !== 'undefined' ? globalThis : global || self;
+    factory(global);
   }
 }(this, function (exports) {
 

From f83644dc41a648c94659dc1c4c0da23c96f0262d Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 15:50:31 +0800
Subject: [PATCH 06/25] Unify all tests into refactored tests.js

---
 test.html | 385 +-------------------------------------------------
 tests.js  | 413 ++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 372 insertions(+), 426 deletions(-)

diff --git a/test.html b/test.html
index ca8639b..bf863ba 100644
--- a/test.html
+++ b/test.html
@@ -13,387 +13,4 @@
 }
 
 

-
+
diff --git a/tests.js b/tests.js
index 99c48c9..d449880 100644
--- a/tests.js
+++ b/tests.js
@@ -1,75 +1,404 @@
+"use strict";
+(function (global, factory) {
+  if (typeof define === 'function' && define.amd) {
+    require(
+      ['./parse-css', 'ansidiff'],
+      factory,
+    );
+  } else if (typeof exports !== 'undefined') {
+    factory(
+      require('./parse-css'),
+      require('ansidiff'),
+    );
+  } else {
+    global = typeof globalThis !== 'undefined' ? globalThis : global || self;
+    factory(
+      global,
+      {lines: global.diffString, words: global.diffString},
+      global.log,
+    );
+  }
+}(this, function (parseCss, ansidiff, log) {
+
 var TESTS = [
   {
-    css: 'foo { bar: baz; }',
-    expected: {"type": "stylesheet", "value": [
-      {
-        "type": "selector",
-        "selector": ["IDENT(foo)", "WS"],
-        "value": [
-          {
-            "type": "declaration",
-            "name": "bar",
-            "value": ["WS", "IDENT(baz)"]}]}]
+    css: `foo {
+        bar: baz;
+    }`,
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "IDENT",
+              "value": "foo"
+            },
+            {
+              "type": "WHITESPACE"
+            }
+          ],
+          "declarations": [
+            {
+              "type": "DECLARATION",
+              "name": "bar",
+              "value": [
+                {
+                  "type": "IDENT",
+                  "value": "baz"
+                }
+              ],
+              "important": false
+            }
+          ],
+          "rules": []
+        }
+      ]
     }
   }, {
     css: 'foo { bar: rgb(255, 0, 127); }',
-    expected: {"type": "stylesheet", "value": [
-      {
-        "type": "selector",
-        "selector": ["IDENT(foo)", "WS"],
-        "value": [
-          {
-            "type": "declaration",
-            "name": "bar",
-            "value": ["WS", {"type": "func", "name": "rgb", "value": [
-                ["INT(255)"], ["WS", "INT(0)"], ["WS", "INT(127)"]]}]}]}]
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "IDENT",
+              "value": "foo"
+            },
+            {
+              "type": "WHITESPACE"
+            }
+          ],
+          "declarations": [
+            {
+              "type": "DECLARATION",
+              "name": "bar",
+              "value": [
+                {
+                  "type": "FUNCTION",
+                  "name": "rgb",
+                  "value": [
+                    {
+                      "type": "NUMBER",
+                      "value": 255,
+                      "isInteger": true,
+                    },
+                    {
+                      "type": "COMMA"
+                    },
+                    {
+                      "type": "WHITESPACE"
+                    },
+                    {
+                      "type": "NUMBER",
+                      "value": 0,
+                      "isInteger": true,
+                    },
+                    {
+                      "type": "COMMA"
+                    },
+                    {
+                      "type": "WHITESPACE"
+                    },
+                    {
+                      "type": "NUMBER",
+                      "value": 127,
+                      "isInteger": true,
+                    }
+                  ]
+                }
+              ],
+              "important": false
+            }
+          ],
+          "rules": []
+        }
+      ]
     }
   }, {
     css: '#foo {}',
-    expected: {"type": "stylesheet", "value": [
-      {
-        "type": "selector",
-        "selector": ["HASH(foo)", "WS"],
-        "value": []}]
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "HASH",
+              "value": "foo",
+              "type": "id"
+            },
+            {
+              "type": "WHITESPACE"
+            }
+          ],
+          "declarations": [],
+          "rules": []
+        }
+      ]
     }
   }, {
     css: '@media{ }',
-    expected: {"type": "stylesheet", "value": [
-      {
-        "type": "at", "name": "media",
-        "prelude": [],
-        "value": []}]}
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "AT-RULE",
+          "name": "media",
+          "prelude": [],
+          "declarations": [],
+          "rules": []
+        }
+      ]
+    }
+  }, {
+    css: '.foo {color: red; @media { foo: bar } color: green }',
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "DELIM",
+              "value": "."
+            },
+            {
+              "type": "IDENT",
+              "value": "foo"
+            },
+            {
+              "type": "WHITESPACE"
+            }
+          ],
+          "declarations": [
+            {
+              "type": "DECLARATION",
+              "name": "color",
+              "value": [
+                {
+                  "type": "IDENT",
+                  "value": "red"
+                }
+              ],
+              "important": false
+            },
+            {
+              "type": "DECLARATION",
+              "name": "color",
+              "value": [
+                {
+                  "type": "IDENT",
+                  "value": "green"
+                }
+              ],
+              "important": false
+            }
+          ],
+          "rules": [
+            {
+              "type": "AT-RULE",
+              "name": "media",
+              "prelude": [
+                {
+                  "type": "WHITESPACE"
+                }
+              ],
+              "declarations": [
+                {
+                  "type": "DECLARATION",
+                  "name": "foo",
+                  "value": [
+                    {
+                      "type": "IDENT",
+                      "value": "bar"
+                    }
+                  ],
+                  "important": false
+                }
+              ],
+              "rules": []
+            }
+          ]
+        }
+      ]
+    }
+  }, {
+    css: 'foo{div:hover; color:red{};}',
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "IDENT",
+              "value": "foo"
+            }
+          ],
+          "declarations": [
+            {
+              "type": "DECLARATION",
+              "name": "div",
+              "value": [
+                {
+                  "type": "IDENT",
+                  "value": "hover"
+                }
+              ],
+              "important": false
+            }
+          ],
+          "rules": [
+            {
+              "type": "QUALIFIED-RULE",
+              "prelude": [
+                {
+                  "type": "IDENT",
+                  "value": "color"
+                },
+                {
+                  "type": "COLON"
+                },
+                {
+                  "type": "IDENT",
+                  "value": "red"
+                }
+              ],
+              "declarations": [],
+              "rules": []
+            }
+          ]
+        }
+      ]
+    }
+  },
+  {
+    css: `@foo;;foo {}`,
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "AT-RULE",
+          "name": "foo",
+          "prelude": [],
+          "declarations": null,
+          "rules": null
+        }
+      ]
+    }
+  }, {
+    css: `foo{@foo;;foo {}}`,
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "IDENT",
+              "value": "foo"
+            }
+          ],
+          "declarations": [],
+          "rules": [
+            {
+              "type": "AT-RULE",
+              "name": "foo",
+              "prelude": [],
+              "declarations": null,
+              "rules": null
+            },
+            {
+              "type": "QUALIFIED-RULE",
+              "prelude": [
+                {
+                  "type": "IDENT",
+                  "value": "foo"
+                },
+                {
+                  "type": "WHITESPACE"
+                }
+              ],
+              "declarations": [],
+              "rules": []
+            }
+          ]
+        }
+      ]
+    }
+  }, {
+    css: `foo { --div:hover{}}`,
+    expected: {
+      "type": "STYLESHEET",
+      "rules": [
+        {
+          "type": "QUALIFIED-RULE",
+          "prelude": [
+            {
+              "type": "IDENT",
+              "value": "foo"
+            },
+            {
+              "type": "WHITESPACE"
+            }
+          ],
+          "declarations": [
+            {
+              "type": "DECLARATION",
+              "name": "--div",
+              "value": [
+                {
+                  "type": "IDENT",
+                  "value": "hover"
+                },
+                {
+                  "type": "BLOCK",
+                  "name": "{",
+                  "value": []
+                }
+              ],
+              "important": false
+            }
+          ],
+          "rules": []
+        }
+      ]
+    }
   }
 ];
 
 
-var ansidiff = require('ansidiff'),
-    tokenize = require('./parse-css').tokenize,
-    parseAStylesheet = require('./parse-css').parseAStylesheet;
+var tokenize = parseCss.tokenize,
+    parseAStylesheet = parseCss.parseAStylesheet, 
+    log = log || console.log;
 
 var total = TESTS.length, failures = 0,
     i, test, tokens, sheet, dump, expected_dump;
 
 for (i = 0; i < total; i++) {
   test = TESTS[i];
-  sheet = parseAStylesheet(test.css);
+  tokens = tokenize(test.css);
+  sheet = parseAStylesheet(tokens);
   dump = sheet.toString('  ');
   expected_dump = JSON.stringify(test.expected, null, '  ');
   if (dump == expected_dump) {
-    console.log('Test %d of %d: PASS', i, total);
+    log(`Test ${i} of ${total}: PASS`);
   } else {
-    console.log('Test %d of %d: FAIL\nCSS: %s\nTokens: %s',
-        i, total, test.css, tokens.join(' '));
-    console.log(ansidiff.lines(expected_dump, dump));
+    log(`Test ${i} of ${total}: FAIL\nCSS: ${test.css}\nTokens: ${tokens.join(' ')}`);
+    log(ansidiff.lines(expected_dump, dump));
     failures++;
   }
 }
 
 // Abuse the differ to get colored output
 if (failures == 0) {
-  console.log(ansidiff.words('%d tests, ', '%d tests, all passed :)'),
-              total);
+  log(ansidiff.words(`${total} tests, `, `${total} tests, all passed :)`));
 } else {
-  console.log(ansidiff.words('%d tests, %d failures :(', '%d tests, '),
-              total, failures);
+  log(ansidiff.words(`${total} tests, ${failures} failures :(`, `${total} tests, `));
 }
+
+}));

From 696363e8c6b2fa8fb07756a2d755dfdb1ecc1ee0 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 16:22:15 +0800
Subject: [PATCH 07/25] Optimize test output using insertAdjacentHTML

---
 test.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test.html b/test.html
index bf863ba..2edf653 100644
--- a/test.html
+++ b/test.html
@@ -9,7 +9,7 @@
 
 
 


From ac559841bdb96607ece5c7e9c8ae7c4c44c517e2 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 15:24:43 +0800
Subject: [PATCH 08/25] Revise outdated README.md

---
 README.md | 131 +-----------------------------------------------------
 1 file changed, 2 insertions(+), 129 deletions(-)

diff --git a/README.md b/README.md
index fe9834a..5820604 100644
--- a/README.md
+++ b/README.md
@@ -53,141 +53,14 @@ They do exactly what they say in their name,
 because they're named exactly the same as the corresponding section of the Syntax spec:
 
 * `parseAStylesheet()`
-* `parseAListOfRules()`
+* `parseAStylesheetsContents()`
+* `parseABlocksContents()`
 * `parseARule()`
 * `parseADeclaration()`
-* `parseAListOfDeclarations()`
 * `parseAComponentValue()`
 * `parseAListOfComponentValues()`
 * `parseACommaSeparatedListOfComponentValues()`
 
-Canonicalizing Against A Grammar
---------------------------------
-
-By default, the parser can only do so much;
-it knows how to interpret the top-level rules in a stylesheet,
-but not how to interpret the contents of anything below that.
-This means that anything nested within a top-level block is left as a bare token stream,
-requiring you to call the correct parsing function on it.
-
-The `canonicalize()` function takes a parsing result and a grammar
-and transforms the result accordingly,
-rendering the result into an easier-to-digest form.
-
-A grammar is an object with one of the following four forms:
-
-```js
-{"stylesheet":true}
-```
-
-```js
-{
-  "qualified": ,
-  "@foo": ,
-  "unknown": 
-}
-```
-
-```js
-{
-  "declarations": true,
-  "@foo": 
-  "unknown": 
-}
-```
-
-```js
-null
-```
-
-A `stylesheet` block contains nothing else;
-it just means that this rule uses the top-level grammar for its contents.
-This is true, for example, of the `@media` rule.
-
-A `qualified` block means that the rule's contents are qualified rules (style rules) and at-rules.
-The "qualified" key must have another grammar as its value (often `{declarations:true}`).
-Any at-rules that are valid in this context must be listed,
-also with a grammar for their contents.
-Optionally, the "unknown" key can be provided with a function value;
-this will be called with any unknown at-rules (ones not listed in the grammar)/
-If it returns a truthy value, it's inserted into the structure with everything else;
-if falsey, the rule is put into the "errors" entry of the resulting block for later processing or ignoring.
-
-A `declarations` block means that the rule's contents are declarations and at-rules.
-Currently, the "declarations" key only accepts the value `true`;
-eventually it'll allow you to specify what declarations are valid.
-Similar to `qualified` blocks,
-you must list what at-rules are allowed,
-and can provide an "unknown" function.
-
-A `null` just means that the block has no contents.
-This is used for at-rules that are statements,
-ended with a semicolon rather than a block,
-like `@import`.
-
-A `CSSGrammar` object is provided with a default grammar for CSS.
-If you call `canonicalize()` without a grammar,
-this is used automatically.
-This is what it currently looks like:
-
-```js
-{
-  qualified: {declarations:true},
-  "@media": {stylesheet:true},
-  "@keyframes": {qualified:{declarations:true}},
-  "@font-face": {declarations:true},
-  "@supports": {stylesheet:true},
-  "@scope": {stylesheet:true},
-  "@counter-style": {declarations:true},
-  "@import": null,
-  "@font-feature-values": {
-    // No qualified rules actually allowed,
-    // but have to declare it one way or the other.
-    qualified: true,
-    "@stylistic": {declarations:true},
-    "@styleset": {declarations:true},
-    "@character-variants": {declarations:true},
-    "@swash": {declarations:true},
-    "@ornaments": {declarations:true},
-    "@annotation": {declarations:true},
-  },
-  "@viewport": {declarations:true},
-  "@page": {
-    declarations: true,
-    "@top-left-corner": {declarations:true},
-    "@top-left": {declarations:true},
-    "@top-center": {declarations:true},
-    "@top-right": {declarations:true},
-    "@top-right-corner": {declarations:true},
-    "@right-top": {declarations:true},
-    "@right-middle": {declarations:true},
-    "@right-bottom": {declarations:true},
-    "@right-bottom-corner": {declarations:true},
-    "@bottom-right": {declarations:true},
-    "@bottom-center": {declarations:true},
-    "@bottom-left": {declarations:true},
-    "@bottom-left-corner": {declarations:true},
-    "@left-bottom": {declarations:true},
-    "@left-center": {declarations:true},
-    "@left-top": {declarations:true},
-  },
-  "@custom-selector": null,
-  "@custom-media": null
-}
-```
-
-The return value is a nested structure of objects.
-Each has a "type" key, set to either "stylesheet", "qualified-rule" or "at-rule".
-Unless it's a statement at-rule,
-each has a "rules" key set to an array of contained rules/declarations.
-At-rules also have a "name" (string) and "prelude" (list of tokens for the part before the block).
-Qualified rules have a "declarations",
-which is an object mapping declaration name to value (list of tokens),
-for ease of use
-(all the declarations are in the `.rules` property already,
-but this gives you easy access to them by name,
-and only stores the last of each if they're repeated).
-
 Node Integration
 ----------------
 

From 558d7256073075a5b7b8f2ac12b4cf2ecd9dc5b6 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 14:43:36 +0800
Subject: [PATCH 09/25] Throw Error for CSSParserToken.toSource()

- Exception does not exist in most JS environments.
---
 parse-css.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/parse-css.js b/parse-css.js
index 8f99a62..e88f86a 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -498,7 +498,7 @@ class CSSParserToken {
 
   toJSON() { return {type:this.type}; }
   toString() { return this.type; }
-  toSource() { throw new Exception("Not implemented."); }
+  toSource() { throw new Error("Not implemented."); }
 }
 //toJSON()
 //toString()

From 064e54ee8f5065d5db4da04db0f851399e027011 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 17:02:00 +0800
Subject: [PATCH 10/25] Make sure thrown thing is an Error

---
 parse-css.js | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/parse-css.js b/parse-css.js
index e88f86a..40c0849 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -55,6 +55,13 @@ class InvalidCharacterError extends Error {
   }
 }
 
+class SpecError extends Error {
+  constructor(...args) {
+    super(...args);
+    this.name = "SpecError";
+  }
+}
+
 function preprocess(str) {
   // Turn a string into an array of code points,
   // following the preprocessing cleanup rules.
@@ -110,7 +117,7 @@ function tokenize(str) {
     if(num === undefined)
       num = 1;
     if(num > 3)
-      throw "Spec Error: no more than three codepoints of lookahead.";
+      throw new SpecError("no more than three codepoints of lookahead.");
     return codepoint(i+num);
   };
   var consume = function(num) {

From 28c723b5091d7e86d0cab760b529fccb4fbf0611 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 18:07:39 +0800
Subject: [PATCH 11/25] Support testing another method by providing "parser"
 property

---
 tests.js | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/tests.js b/tests.js
index d449880..45c3486 100644
--- a/tests.js
+++ b/tests.js
@@ -372,18 +372,17 @@ var TESTS = [
 ];
 
 
-var tokenize = parseCss.tokenize,
-    parseAStylesheet = parseCss.parseAStylesheet, 
-    log = log || console.log;
+var log = log || console.log;
 
 var total = TESTS.length, failures = 0,
-    i, test, tokens, sheet, dump, expected_dump;
+    i, test, tokens, parser, result, dump, expected_dump;
 
 for (i = 0; i < total; i++) {
   test = TESTS[i];
-  tokens = tokenize(test.css);
-  sheet = parseAStylesheet(tokens);
-  dump = sheet.toString('  ');
+  tokens = parseCss.tokenize(test.css);
+  parser = parseCss[typeof test.parser === 'string' ? test.parser : 'parseAStylesheet'];
+  result = (typeof parser === 'function') ? parser(tokens) : tokens;
+  dump = JSON.stringify(result, null, '  ');
   expected_dump = JSON.stringify(test.expected, null, '  ');
   if (dump == expected_dump) {
     log(`Test ${i} of ${total}: PASS`);

From 9b6207c032312a230710bf81350e8d48a23fb912 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 20:22:24 +0800
Subject: [PATCH 12/25] Adjust curly brackets styling

- Make commenting and code collapsing/expanding more easily.
---
 tests.js | 21 ++++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)

diff --git a/tests.js b/tests.js
index 45c3486..ef2d176 100644
--- a/tests.js
+++ b/tests.js
@@ -56,7 +56,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: 'foo { bar: rgb(255, 0, 127); }',
     expected: {
       "type": "STYLESHEET",
@@ -118,7 +119,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: '#foo {}',
     expected: {
       "type": "STYLESHEET",
@@ -140,7 +142,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: '@media{ }',
     expected: {
       "type": "STYLESHEET",
@@ -154,7 +157,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: '.foo {color: red; @media { foo: bar } color: green }',
     expected: {
       "type": "STYLESHEET",
@@ -226,7 +230,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: 'foo{div:hover; color:red{};}',
     expected: {
       "type": "STYLESHEET",
@@ -290,7 +295,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: `foo{@foo;;foo {}}`,
     expected: {
       "type": "STYLESHEET",
@@ -330,7 +336,8 @@ var TESTS = [
         }
       ]
     }
-  }, {
+  },
+  {
     css: `foo { --div:hover{}}`,
     expected: {
       "type": "STYLESHEET",

From e53bea3f3b784acfa79821b539041a9303c90a5c Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 20:28:21 +0800
Subject: [PATCH 13/25] Fix a failure test

---
 tests.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests.js b/tests.js
index ef2d176..8d4fbe9 100644
--- a/tests.js
+++ b/tests.js
@@ -131,7 +131,7 @@ var TESTS = [
             {
               "type": "HASH",
               "value": "foo",
-              "type": "id"
+              "isIdent": true
             },
             {
               "type": "WHITESPACE"

From ae68eca503ed16bf4dea9723bbc2949eb26594d0 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 18:18:51 +0800
Subject: [PATCH 14/25] Preprocess a standalone surrogate to \uFFFD

- Also optimize surrogate pair handling using native String.codePointAt().
- ref: https://www.w3.org/TR/css-syntax-3/#input-preprocessing
---
 parse-css.js | 17 +++++++----------
 tests.js     | 16 ++++++++++++++++
 2 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/parse-css.js b/parse-css.js
index 40c0849..efbfdf3 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -67,17 +67,14 @@ function preprocess(str) {
   // following the preprocessing cleanup rules.
   var codepoints = [];
   for(var i = 0; i < str.length; i++) {
-    var code = str.charCodeAt(i);
-    if(code == 0xd && str.charCodeAt(i+1) == 0xa) {
+    var code = str.codePointAt(i);
+    if (code == 0xd && str.charCodeAt(i+1) == 0xa) {
       code = 0xa; i++;
-    }
-    if(code == 0xd || code == 0xc) code = 0xa;
-    if(code == 0x0) code = 0xfffd;
-    if(between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i+1), 0xdc00, 0xdfff)) {
-      // Decode a surrogate pair into an astral codepoint.
-      var lead = code - 0xd800;
-      var trail = str.charCodeAt(i+1) - 0xdc00;
-      code = Math.pow(2, 16) + lead * Math.pow(2, 10) + trail;
+    } else if (code == 0xd || code == 0xc) {
+      code = 0xa;
+    } else if (code == 0x0 || between(code, 0xd800, 0xdfff)) {
+      code = 0xfffd;
+    } else if (code > 0xffff) {
       i++;
     }
     codepoints.push(code);
diff --git a/tests.js b/tests.js
index 8d4fbe9..aee1117 100644
--- a/tests.js
+++ b/tests.js
@@ -21,6 +21,22 @@
 }(this, function (parseCss, ansidiff, log) {
 
 var TESTS = [
+  // preprocess()
+  {
+    parser: "",
+    css: `\u{20000},\u{0},\uD800,\uDFFF`,
+    expected: [
+      {type: "IDENT", value: "\u{20000}"},
+      {type: "COMMA"},
+      {type: "IDENT", value: "\uFFFD"},
+      {type: "COMMA"},
+      {type: "IDENT", value: "\uFFFD"},
+      {type: "COMMA"},
+      {type: "IDENT", value: "\uFFFD"},
+    ]
+  },
+
+  // parseAStylesheet()
   {
     css: `foo {
         bar: baz;

From de18c28e1277d54de937b23e479e8b8885fee4c8 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 18:40:48 +0800
Subject: [PATCH 15/25] Fix escaping of \u0000 and a standalone surrogate

- ref: https://www.w3.org/TR/css-syntax-3/#consume-escaped-code-point
---
 parse-css.js | 2 +-
 tests.js     | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/parse-css.js b/parse-css.js
index efbfdf3..489d040 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -362,7 +362,7 @@ function tokenize(str) {
       }
       if(whitespace(next())) consume();
       var value = parseInt(digits.map(function(x){return String.fromCharCode(x);}).join(''), 16);
-      if( value > maximumallowedcodepoint ) value = 0xfffd;
+      if (value === 0x0 || between(value, 0xd800, 0xdfff) || value > maximumallowedcodepoint ) value = 0xfffd;
       return value;
     } else if(eof()) {
       return 0xfffd;
diff --git a/tests.js b/tests.js
index aee1117..ca1f787 100644
--- a/tests.js
+++ b/tests.js
@@ -36,6 +36,15 @@ var TESTS = [
     ]
   },
 
+  // tokenize()
+
+  // -- Escapes
+  {
+    parser: "",
+    css: `\\20000 \\0 \\D800 \\DFFF \\110000 `,
+    expected: [{type: "IDENT", value: "\u{20000}\uFFFD\uFFFD\uFFFD\uFFFD"}],
+  },
+
   // parseAStylesheet()
   {
     css: `foo {

From 04abb09ee5fb609902f9df92e331b3ae3a9cc633 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 21:02:21 +0800
Subject: [PATCH 16/25] Add more tests from Chromium source

- ref:
  https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/css/parser/css_tokenizer_test.cc
  https://chromium.googlesource.com/chromium/src/+log/refs/heads/main/third_party/blink/renderer/core/css/parser/css_tokenizer_test.cc
---
 tests.js | 1228 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 1226 insertions(+), 2 deletions(-)

diff --git a/tests.js b/tests.js
index ca1f787..1614bb8 100644
--- a/tests.js
+++ b/tests.js
@@ -38,11 +38,1235 @@ var TESTS = [
 
   // tokenize()
 
+  // -- SingleCharacterTokens
+  {
+    parser: "",
+    css: "(",
+    expected: [{type: "OPEN-PAREN"}],
+  },
+  {
+    parser: "",
+    css: ")",
+    expected: [{type: "CLOSE-PAREN"}],
+  },
+  {
+    parser: "",
+    css: "[",
+    expected: [{type: "OPEN-SQUARE"}],
+  },
+  {
+    parser: "",
+    css: "]",
+    expected: [{type: "CLOSE-SQUARE"}],
+  },
+  {
+    parser: "",
+    css: ",",
+    expected: [{type: "COMMA"}],
+  },
+  {
+    parser: "",
+    css: ":",
+    expected: [{type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: ";",
+    expected: [{type: "SEMICOLON"}],
+  },
+  {
+    parser: "",
+    css: ")[",
+    expected: [{type: "CLOSE-PAREN"}, {type: "OPEN-SQUARE"}],
+  },
+  {
+    parser: "",
+    css: "[)",
+    expected: [{type: "OPEN-SQUARE"}, {type: "CLOSE-PAREN"}],
+  },
+  {
+    parser: "",
+    css: "{}",
+    expected: [{type: "OPEN-CURLY"}, {type: "CLOSE-CURLY"}],
+  },
+  {
+    parser: "",
+    css: ",,",
+    expected: [{type: "COMMA"}, {type: "COMMA"}],
+  },
+
+  // -- MultipleCharacterTokens
+  {
+    parser: "",
+    css: "~=",
+    expected: [{type: "DELIM", value: '~'}, {type: "DELIM", value: '='}],
+  },
+  {
+    parser: "",
+    css: "|=",
+    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '='}],
+  },
+  {
+    parser: "",
+    css: "^=",
+    expected: [{type: "DELIM", value: '^'}, {type: "DELIM", value: '='}],
+  },
+  {
+    parser: "",
+    css: "$=",
+    expected: [{type: "DELIM", value: '$'}, {type: "DELIM", value: '='}],
+  },
+  {
+    parser: "",
+    css: "*=",
+    expected: [{type: "DELIM", value: '*'}, {type: "DELIM", value: '='}],
+  },
+  {
+    parser: "",
+    css: "||",
+    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}],
+  },
+  {
+    parser: "",
+    css: "|||",
+    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}],
+  },
+  {
+    parser: "",
+    css: "",
+    expected: [{type: "CDC"}],
+  },
+
+  // -- DelimiterToken
+  {
+    parser: "",
+    css: "^",
+    expected: [{type: "DELIM", value: '^'}],
+  },
+  {
+    parser: "",
+    css: "*",
+    expected: [{type: "DELIM", value: '*'}],
+  },
+  {
+    parser: "",
+    css: "%",
+    expected: [{type: "DELIM", value: '%'}],
+  },
+  {
+    parser: "",
+    css: "~",
+    expected: [{type: "DELIM", value: '~'}],
+  },
+  {
+    parser: "",
+    css: "&",
+    expected: [{type: "DELIM", value: '&'}],
+  },
+  {
+    parser: "",
+    css: "|",
+    expected: [{type: "DELIM", value: '|'}],
+  },
+  {
+    parser: "",
+    css: "\x7f",
+    expected: [{type: "DELIM", value: '\x7f'}],
+  },
+  {
+    parser: "",
+    css: "\x01",
+    expected: [{type: "DELIM", value: '\x01'}],
+  },
+  {
+    parser: "",
+    css: "~-",
+    expected: [{type: "DELIM", value: '~'}, {type: "DELIM", value: '-'}],
+  },
+  {
+    parser: "",
+    css: "^|",
+    expected: [{type: "DELIM", value: '^'}, {type: "DELIM", value: '|'}],
+  },
+  {
+    parser: "",
+    css: "$~",
+    expected: [{type: "DELIM", value: '$'}, {type: "DELIM", value: '~'}],
+  },
+  {
+    parser: "",
+    css: "*^",
+    expected: [{type: "DELIM", value: '*'}, {type: "DELIM", value: '^'}],
+  },
+
+  // -- WhitespaceTokens
+  {
+    parser: "",
+    css: "   ",
+    expected: [{type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "\n\rS",
+    expected: [{type: "WHITESPACE"}, {type: "IDENT", value: "S"}],
+  },
+  {
+    parser: "",
+    css: "   *",
+    expected: [{type: "WHITESPACE"}, {type: "DELIM", value: '*'}],
+  },
+  {
+    parser: "",
+    css: "\r\n\f\t2",
+    expected: [{type: "WHITESPACE"}, {type: "NUMBER", value: 2, isInteger: true}],
+  },
+
   // -- Escapes
   {
     parser: "",
-    css: `\\20000 \\0 \\D800 \\DFFF \\110000 `,
-    expected: [{type: "IDENT", value: "\u{20000}\uFFFD\uFFFD\uFFFD\uFFFD"}],
+    css: "hel\\6Co",
+    expected: [{type: "IDENT", value: "hello"}],
+  },
+  {
+    parser: "",
+    css: "\\26 B",
+    expected: [{type: "IDENT", value: "&B"}],
+  },
+  {
+    parser: "",
+    css: "'hel\\6c o'",
+    expected: [{type: "STRING", value: "hello"}],
+  },
+  {
+    parser: "",
+    css: "'spac\\65\r\ns'",
+    expected: [{type: "STRING", value: "spaces"}],
+  },
+  {
+    parser: "",
+    css: "spac\\65\r\ns",
+    expected: [{type: "IDENT", value: "spaces"}],
+  },
+  {
+    parser: "",
+    css: "spac\\65\n\rs",
+    expected: [{type: "IDENT", value: "space"}, {type: "WHITESPACE"}, {type: "IDENT", value: "s"}],
+  },
+  {
+    parser: "",
+    css: "sp\\61\tc\\65\fs",
+    expected: [{type: "IDENT", value: "spaces"}],
+  },
+  {
+    parser: "",
+    css: "hel\\6c  o",
+    expected: [{type: "IDENT", value: "hell"}, {type: "WHITESPACE"}, {type: "IDENT", value: "o"}],
+  },
+  {
+    parser: "",
+    css: "test\\\n",
+    expected: [{type: "IDENT", value: "test"}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "test\\D799",
+    expected: [{type: "IDENT", value: "test\uD799"}],
+  },
+  {
+    parser: "",
+    css: "\\E000",
+    expected: [{type: "IDENT", value: "\uE000"}],
+  },
+  {
+    parser: "",
+    css: "te\\s\\t",
+    expected: [{type: "IDENT", value: "test"}],
+  },
+  {
+    parser: "",
+    css: "spaces\\ in\\\tident",
+    expected: [{type: "IDENT", value: "spaces in\tident"}],
+  },
+  {
+    parser: "",
+    css: "\\.\\,\\:\\!",
+    expected: [{type: "IDENT", value: ".,:!"}],
+  },
+  {
+    parser: "",
+    css: "\\\r",
+    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "\\\f",
+    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "\\\r\n",
+    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "null\\\0",
+    expected: [{type: "IDENT", value: "null\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "null\\\0\0",
+    expected: [{type: "IDENT", value: "null\uFFFD\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "null\\0",
+    expected: [{type: "IDENT", value: "null\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "null\\0",
+    expected: [{type: "IDENT", value: "null\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "null\\0000",
+    expected: [{type: "IDENT", value: "null\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "large\\110000",
+    expected: [{type: "IDENT", value: "large\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "large\\23456a",
+    expected: [{type: "IDENT", value: "large\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "surrogate\\D800",
+    expected: [{type: "IDENT", value: "surrogate\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "surrogate\\0DABC",
+    expected: [{type: "IDENT", value: "surrogate\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "\\00DFFFsurrogate",
+    expected: [{type: "IDENT", value: "\uFFFDsurrogate"}],
+  },
+  {
+    parser: "",
+    css: "\\10fFfF",
+    expected: [{type: "IDENT", value: "\u{10ffff}"}],
+  },
+  {
+    parser: "",
+    css: "\\10fFfF0",
+    expected: [{type: "IDENT", value: "\u{10ffff}0"}],
+  },
+  {
+    parser: "",
+    css: "\\10000000",
+    expected: [{type: "IDENT", value: "\u{100000}00"}],
+  },
+  {
+    parser: "",
+    css: "eof\\",
+    expected: [{type: "IDENT", value: "eof\uFFFD"}],
+  },
+
+  // -- IdentToken
+  {
+    parser: "",
+    css: "simple-ident",
+    expected: [{type: "IDENT", value: "simple-ident"}],
+  },
+  {
+    parser: "",
+    css: "testing123",
+    expected: [{type: "IDENT", value: "testing123"}],
+  },
+  {
+    parser: "",
+    css: "hello!",
+    expected: [{type: "IDENT", value: "hello"}, {type: "DELIM", value: '!'}],
+  },
+  {
+    parser: "",
+    css: "world\x05",
+    expected: [{type: "IDENT", value: "world"}, {type: "DELIM", value: '\x05'}],
+  },
+  {
+    parser: "",
+    css: "_under score",
+    expected: [{type: "IDENT", value: "_under"}, {type: "WHITESPACE"}, {type: "IDENT", value: "score"}],
+  },
+  {
+    parser: "",
+    css: "-_underscore",
+    expected: [{type: "IDENT", value: "-_underscore"}],
+  },
+  {
+    parser: "",
+    css: "-text",
+    expected: [{type: "IDENT", value: "-text"}],
+  },
+  {
+    parser: "",
+    css: "-\\6d",
+    expected: [{type: "IDENT", value: "-m"}],
+  },
+  {
+    parser: "",
+    css: "--abc",
+    expected: [{type: "IDENT", value: "--abc"}],
+  },
+  {
+    parser: "",
+    css: "--",
+    expected: [{type: "IDENT", value: "--"}],
+  },
+  {
+    parser: "",
+    css: "--11",
+    expected: [{type: "IDENT", value: "--11"}],
+  },
+  {
+    parser: "",
+    css: "---",
+    expected: [{type: "IDENT", value: "---"}],
+  },
+  {
+    parser: "",
+    css: "\u2003",  // em-space
+    expected: [{type: "IDENT", value: "\u2003"}],
+  },
+  {
+    parser: "",
+    css: "\u{A0}",  // non-breaking space
+    expected: [{type: "IDENT", value: "\u{A0}"}],
+  },
+  {
+    parser: "",
+    css: "\u1234",
+    expected: [{type: "IDENT", value: "\u1234"}],
+  },
+  {
+    parser: "",
+    css: "\u{12345}",
+    expected: [{type: "IDENT", value: "\u{12345}"}],
+  },
+  {
+    parser: "",
+    css: "\0",
+    expected: [{type: "IDENT", value: "\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "ab\0c",
+    expected: [{type: "IDENT", value: "ab\uFFFDc"}],
+  },
+  {
+    parser: "",
+    css: "ab\0c",
+    expected: [{type: "IDENT", value: "ab\uFFFDc"}],
+  },
+
+  // -- FunctionToken
+  {
+    parser: "",
+    css: "scale(2)",
+    expected: [{type: "FUNCTION", value: "scale"}, {type: "NUMBER", value: 2, isInteger: true}, {type: "CLOSE-PAREN"}],
+  },
+  {
+    parser: "",
+    css: "foo-bar\\ baz(",
+    expected: [{type: "FUNCTION", value: "foo-bar baz"}],
+  },
+  {
+    parser: "",
+    css: "fun\\(ction(",
+    expected: [{type: "FUNCTION", value: "fun(ction"}],
+  },
+  {
+    parser: "",
+    css: "-foo(",
+    expected: [{type: "FUNCTION", value: "-foo"}],
+  },
+  {
+    parser: "",
+    css: "url(\"foo.gif\"",
+    expected: [{type: "FUNCTION", value: "url"}, {type: "STRING", value: "foo.gif"}],
+  },
+  {
+    parser: "",
+    css: "foo(  \'bar.gif\'",
+    expected: [{type: "FUNCTION", value: "foo"}, {type: "WHITESPACE"}, {type: "STRING", value: "bar.gif"}],
+  },
+  {
+    parser: "",
+    css: "url(  \'bar.gif\'",
+    expected: [{type: "FUNCTION", value: "url"}, {type: "STRING", value: "bar.gif"}],
+  },
+
+  // -- AtKeywordToken
+  {
+    parser: "",
+    css: "@at-keyword",
+    expected: [{type: "AT-KEYWORD", value: "at-keyword"}],
+  },
+  {
+    parser: "",
+    css: "@testing123",
+    expected: [{type: "AT-KEYWORD", value: "testing123"}],
+  },
+  {
+    parser: "",
+    css: "@hello!",
+    expected: [{type: "AT-KEYWORD", value: "hello"}, {type: "DELIM", value: '!'}],
+  },
+  {
+    parser: "",
+    css: "@-text",
+    expected: [{type: "AT-KEYWORD", value: "-text"}],
+  },
+  {
+    parser: "",
+    css: "@--abc",
+    expected: [{type: "AT-KEYWORD", value: "--abc"}],
+  },
+  {
+    parser: "",
+    css: "@--",
+    expected: [{type: "AT-KEYWORD", value: "--"}],
+  },
+  {
+    parser: "",
+    css: "@--11",
+    expected: [{type: "AT-KEYWORD", value: "--11"}],
+  },
+  {
+    parser: "",
+    css: "@---",
+    expected: [{type: "AT-KEYWORD", value: "---"}],
+  },
+  {
+    parser: "",
+    css: "@\\ ",
+    expected: [{type: "AT-KEYWORD", value: " "}],
+  },
+  {
+    parser: "",
+    css: "@-\\ ",
+    expected: [{type: "AT-KEYWORD", value: "- "}],
+  },
+  {
+    parser: "",
+    css: "@@",
+    expected: [{type: "DELIM", value: '@'}, {type: "DELIM", value: '@'}],
+  },
+  {
+    parser: "",
+    css: "@2",
+    expected: [{type: "DELIM", value: '@'}, {type: "NUMBER", value: 2, isInteger: true}],
+  },
+  {
+    parser: "",
+    css: "@-1",
+    expected: [{type: "DELIM", value: '@'}, {type: "NUMBER", value: -1, isInteger: true, sign: "-"}],
+  },
+
+  // -- UrlToken
+  {
+    parser: "",
+    css: "url(foo.gif)",
+    expected: [{type: "URL", value: "foo.gif"}],
+  },
+  {
+    parser: "",
+    css: "urL(https://example.com/cats.png)",
+    expected: [{type: "URL", value: "https://example.com/cats.png"}],
+  },
+  {
+    parser: "",
+    css: "uRl(what-a.crazy^URL~this\\ is!)",
+    expected: [{type: "URL", value: "what-a.crazy^URL~this is!"}],
+  },
+  {
+    parser: "",
+    css: "uRL(123#test)",
+    expected: [{type: "URL", value: "123#test"}],
+  },
+  {
+    parser: "",
+    css: "Url(escapes\\ \\\"\\'\\)\\()",
+    expected: [{type: "URL", value: "escapes \"')("}],
+  },
+  {
+    parser: "",
+    css: "UrL(   whitespace   )",
+    expected: [{type: "URL", value: "whitespace"}],
+  },
+  {
+    parser: "",
+    css: "URl( whitespace-eof ",
+    expected: [{type: "URL", value: "whitespace-eof"}],
+  },
+  {
+    parser: "",
+    css: "URL(eof",
+    expected: [{type: "URL", value: "eof"}],
+  },
+  {
+    parser: "",
+    css: "url(not/*a*/comment)",
+    expected: [{type: "URL", value: "not/*a*/comment"}],
+  },
+  {
+    parser: "",
+    css: "urL()",
+    expected: [{type: "URL", value: ""}],
+  },
+  {
+    parser: "",
+    css: "uRl(white space),",
+    expected: [{type: "BADURL"}, {type: "COMMA"}],
+  },
+  {
+    parser: "",
+    css: "Url(b(ad),",
+    expected: [{type: "BADURL"}, {type: "COMMA"}],
+  },
+  {
+    parser: "",
+    css: "uRl(ba'd):",
+    expected: [{type: "BADURL"}, {type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: "urL(b\"ad):",
+    expected: [{type: "BADURL"}, {type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: "uRl(b\"ad):",
+    expected: [{type: "BADURL"}, {type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: "Url(b\\\rad):",
+    expected: [{type: "BADURL"}, {type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: "url(b\\\nad):",
+    expected: [{type: "BADURL"}, {type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: "url(/*'bad')*/",
+    expected: [{type: "BADURL"}, {type: "DELIM", value: '*'}, {type: "DELIM", value: '/'}],
+  },
+  {
+    parser: "",
+    css: "url(ba'd\\\\))",
+    expected: [{type: "BADURL"}, {type: "CLOSE-PAREN"}],
+  },
+
+  // -- StringToken
+  {
+    parser: "",
+    css: "'text'",
+    expected: [{type: "STRING", value: "text"}],
+  },
+  {
+    parser: "",
+    css: "\"text\"",
+    expected: [{type: "STRING", value: "text"}],
+  },
+  {
+    parser: "",
+    css: "'testing, 123!'",
+    expected: [{type: "STRING", value: "testing, 123!"}],
+  },
+  {
+    parser: "",
+    css: "'es\\'ca\\\"pe'",
+    expected: [{type: "STRING", value: "es'ca\"pe"}],
+  },
+  {
+    parser: "",
+    css: "'\"quotes\"'",
+    expected: [{type: "STRING", value: "\"quotes\""}],
+  },
+  {
+    parser: "",
+    css: "\"'quotes'\"",
+    expected: [{type: "STRING", value: "'quotes'"}],
+  },
+  {
+    parser: "",
+    css: "\"mismatch'",
+    expected: [{type: "STRING", value: "mismatch'"}],
+  },
+  {
+    parser: "",
+    css: "'text\x05\t\x13'",
+    expected: [{type: "STRING", value: "text\x05\t\x13"}],
+  },
+  {
+    parser: "",
+    css: "\"end on eof",
+    expected: [{type: "STRING", value: "end on eof"}],
+  },
+  {
+    parser: "",
+    css: "'esca\\\nped'",
+    expected: [{type: "STRING", value: "escaped"}],
+  },
+  {
+    parser: "",
+    css: "\"esc\\\faped\"",
+    expected: [{type: "STRING", value: "escaped"}],
+  },
+  {
+    parser: "",
+    css: "'new\\\rline'",
+    expected: [{type: "STRING", value: "newline"}],
+  },
+  {
+    parser: "",
+    css: "\"new\\\r\nline\"",
+    expected: [{type: "STRING", value: "newline"}],
+  },
+  {
+    parser: "",
+    css: "'bad\nstring",
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+  },
+  {
+    parser: "",
+    css: "'bad\rstring",
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+  },
+  {
+    parser: "",
+    css: "'bad\r\nstring",
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+  },
+  {
+    parser: "",
+    css: "'bad\fstring",
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+  },
+  {
+    parser: "",
+    css: "'\0'",
+    expected: [{type: "STRING", value: "\uFFFD"}],
+  },
+  {
+    parser: "",
+    css: "'hel\0lo'",
+    expected: [{type: "STRING", value: "hel\uFFFDlo"}],
+  },
+  {
+    parser: "",
+    css: "'h\\65l\0lo'",
+    expected: [{type: "STRING", value: "hel\uFFFDlo"}],
+  },
+
+  // -- HashToken
+  {
+    parser: "",
+    css: "#id-selector",
+    expected: [{type: "HASH", value: "id-selector", isIdent: true}],
+  },
+  {
+    parser: "",
+    css: "#FF7700",
+    expected: [{type: "HASH", value: "FF7700", isIdent: true}],
+  },
+  {
+    parser: "",
+    css: "#3377FF",
+    expected: [{type: "HASH", value: "3377FF", isIdent: false}],
+  },
+  {
+    parser: "",
+    css: "#\\ ",
+    expected: [{type: "HASH", value: " ", isIdent: true}],
+  },
+  {
+    parser: "",
+    css: "# ",
+    expected: [{type: "DELIM", value: '#'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "#\\\n",
+    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "#\\\r\n",
+    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "#!",
+    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '!'}],
+  },
+
+  // -- NumberToken
+  {
+    parser: "",
+    css: "10",
+    expected: [{type: "NUMBER", value: 10, isInteger: true}],
+  },
+  {
+    parser: "",
+    css: "12.0",
+    expected: [{type: "NUMBER", value: 12, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "+45.6",
+    expected: [{type: "NUMBER", value: 45.6, isInteger: false, sign: "+"}],
+  },
+  {
+    parser: "",
+    css: "-7",
+    expected: [{type: "NUMBER", value: -7, isInteger: true, sign: "-"}],
+  },
+  {
+    parser: "",
+    css: "010",
+    expected: [{type: "NUMBER", value: 10, isInteger: true}],
+  },
+  {
+    parser: "",
+    css: "10e0",
+    expected: [{type: "NUMBER", value: 10, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "12e3",
+    expected: [{type: "NUMBER", value: 12000, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "3e+1",
+    expected: [{type: "NUMBER", value: 30, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "12E-1",
+    expected: [{type: "NUMBER", value: 1.2, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: ".7",
+    expected: [{type: "NUMBER", value: 0.7, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "-.3",
+    expected: [{type: "NUMBER", value: -0.3, isInteger: false, sign: "-"}],
+  },
+  {
+    parser: "",
+    css: "+637.54e-2",
+    expected: [{type: "NUMBER", value: 6.3754, isInteger: false, sign: "+"}],
+  },
+  {
+    parser: "",
+    css: "-12.34E+2",
+    expected: [{type: "NUMBER", value: -1234, isInteger: false, sign: "-"}],
+  },
+  {
+    parser: "",
+    css: "+ 5",
+    expected: [{type: "DELIM", value: '+'}, {type: "WHITESPACE"}, {type: "NUMBER", value: 5, isInteger: true}],
+  },
+  {
+    parser: "",
+    css: "-+12",
+    expected: [{type: "DELIM", value: '-'}, {type: "NUMBER", value: 12, isInteger: true, sign: "+"}],
+  },
+  {
+    parser: "",
+    css: "+-21",
+    expected: [{type: "DELIM", value: '+'}, {type: "NUMBER", value: -21, isInteger: true, sign: "-"}],
+  },
+  {
+    parser: "",
+    css: "++22",
+    expected: [{type: "DELIM", value: '+'}, {type: "NUMBER", value: 22, isInteger: true, sign: "+"}],
+  },
+  {
+    parser: "",
+    css: "13.",
+    expected: [{type: "NUMBER", value: 13, isInteger: true}, {type: "DELIM", value: '.'}],
+  },
+  {
+    parser: "",
+    css: "1.e2",
+    expected: [{type: "NUMBER", value: 1, isInteger: true}, {type: "DELIM", value: '.'}, {type: "IDENT", value: "e2"}],
+  },
+  {
+    parser: "",
+    css: "2e3.5",
+    expected: [{type: "NUMBER", value: 2000, isInteger: false}, {type: "NUMBER", value: 0.5, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "2e3.",
+    expected: [{type: "NUMBER", value: 2000, isInteger: false}, {type: "DELIM", value: '.'}],
+  },
+  {
+    parser: "",
+    css: "1000000000000000000000000",
+    expected: [{type: "NUMBER", value: 1e24, isInteger: true}],
+  },
+
+  // -- DimensionToken
+  {
+    parser: "",
+    css: "10px",
+    expected: [{type: "DIMENSION", value: 10, isInteger: true, unit: "px"}],
+  },
+  {
+    parser: "",
+    css: "12.0em",
+    expected: [{type: "DIMENSION", value: 12, isInteger: false, unit: "em"}],
+  },
+  {
+    parser: "",
+    css: "-12.0em",
+    expected: [{type: "DIMENSION", value: -12, isInteger: false, unit: "em"}],
+  },
+  {
+    parser: "",
+    css: "+45.6__qem",
+    expected: [{type: "DIMENSION", value: 45.6, isInteger: false, unit: "__qem"}],
+  },
+  {
+    parser: "",
+    css: "5e",
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e"}],
+  },
+  {
+    parser: "",
+    css: "5px-2px",
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "px-2px"}],
+  },
+  {
+    parser: "",
+    css: "5e-",
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e-"}],
+  },
+  {
+    parser: "",
+    css: "5\\ ",
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: " "}],
+  },
+  {
+    parser: "",
+    css: "40\\70\\78",
+    expected: [{type: "DIMENSION", value: 40, isInteger: true, unit: "px"}],
+  },
+  {
+    parser: "",
+    css: "4e3e2",
+    expected: [{type: "DIMENSION", value: 4000, isInteger: false, unit: "e2"}],
+  },
+  {
+    parser: "",
+    css: "0x10px",
+    expected: [{type: "DIMENSION", value: 0, isInteger: true, unit: "x10px"}],
+  },
+  {
+    parser: "",
+    css: "4unit ",
+    expected: [{type: "DIMENSION", value: 4, isInteger: true, unit: "unit"}, {type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: "5e+",
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e"}, {type: "DELIM", value: '+'}],
+  },
+  {
+    parser: "",
+    css: "2e.5",
+    expected: [{type: "DIMENSION", value: 2, isInteger: true, unit: "e"}, {type: "NUMBER", value: 0.5, isInteger: false}],
+  },
+  {
+    parser: "",
+    css: "2e+.5",
+    expected: [{type: "DIMENSION", value: 2, isInteger: true, unit: "e"}, {type: "NUMBER", value: 0.5, isInteger: false, sign: "+"}],
+  },
+
+  // -- PercentageToken
+  {
+    parser: "",
+    css: "10%",
+    expected: [{type: "PERCENTAGE", value: 10}],
+  },
+  {
+    parser: "",
+    css: "+12.0%",
+    expected: [{type: "PERCENTAGE", value: 12}],
+  },
+  {
+    parser: "",
+    css: "-48.99%",
+    expected: [{type: "PERCENTAGE", value: -48.99}],
+  },
+  {
+    parser: "",
+    css: "6e-1%",
+    expected: [{type: "PERCENTAGE", value: 0.6}],
+  },
+  {
+    parser: "",
+    css: "5%%",
+    expected: [{type: "PERCENTAGE", value: 5}, {type: "DELIM", value: '%'}],
+  },
+
+  // -- UnicodeRangeToken
+  {
+    parser: "",
+    css: "u+012345-123456",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "NUMBER", value: 12345, isInteger: true, sign: "+"},
+      {type: "NUMBER", value: -123456, isInteger: true, sign: "-"},
+    ],
+  },
+  {
+    parser: "",
+    css: "U+1234-2345",
+    expected: [
+      {type: "IDENT", value: "U"},
+      {type: "NUMBER", value: 1234, isInteger: true, sign: "+"},
+      {type: "NUMBER", value: -2345, isInteger: true, sign: "-"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+222-111",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "NUMBER", value: 222, isInteger: true, sign: "+"},
+      {type: "NUMBER", value: -111, isInteger: true, sign: "-"},
+    ],
+  },
+  {
+    parser: "",
+    css: "U+CafE-d00D",
+    expected: [
+      {type: "IDENT", value: "U"},
+      {type: "DELIM", value: "+"},
+      {type: "IDENT", value: "CafE-d00D"},
+    ],
+  },
+  {
+    parser: "",
+    css: "U+2??",
+    expected: [
+      {type: "IDENT", value: "U"},
+      {type: "NUMBER", value: 2, isInteger: true, sign: "+"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+    ],
+  },
+  {
+    parser: "",
+    css: "U+ab12??",
+    expected: [
+      {type: "IDENT", value: "U"},
+      {type: "DELIM", value: "+"},
+      {type: "IDENT", value: "ab12"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+??????",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+??",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+222+111",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "NUMBER", value: 222, isInteger: true, sign: "+"},
+      {type: "NUMBER", value: 111, isInteger: true, sign: "+"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+12345678",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "NUMBER", value: 12345678, isInteger: true, sign: "+"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+123-12345678",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "NUMBER", value: 123, isInteger: true, sign: "+"},
+      {type: "NUMBER", value: -12345678, isInteger: true, sign: "-"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+cake",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+      {type: "IDENT", value: "cake"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+1234-gggg",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DIMENSION", value: 1234, isInteger: true, unit: "-gggg", sign: "+"},
+    ],
+  },
+  {
+    parser: "",
+    css: "U+ab12???",
+    expected: [
+      {type: "IDENT", value: "U"},
+      {type: "DELIM", value: "+"},
+      {type: "IDENT", value: "ab12"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+a1?-123",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+      {type: "IDENT", value: "a1"},
+      {type: "DELIM", value: "?"},
+      {type: "NUMBER", value: -123, isInteger: true, sign: "-"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+1??4",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "NUMBER", value: 1, isInteger: true, sign: "+"},
+      {type: "DELIM", value: "?"},
+      {type: "DELIM", value: "?"},
+      {type: "NUMBER", value: 4, isInteger: true},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+z",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+      {type: "IDENT", value: "z"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+    ],
+  },
+  {
+    parser: "",
+    css: "u+-543",
+    expected: [
+      {type: "IDENT", value: "u"},
+      {type: "DELIM", value: "+"},
+      {type: "NUMBER", value: -543, isInteger: true, sign: "-"},
+    ],
+  },
+
+  // -- CommentToken
+  {
+    parser: "",
+    css: "/*comment*/a",
+    expected: [{type: "IDENT", value: "a"}],
+  },
+  {
+    parser: "",
+    css: "/**\\2f**//",
+    expected: [{type: "DELIM", value: '/'}],
+  },
+  {
+    parser: "",
+    css: "/**y*a*y**/ ",
+    expected: [{type: "WHITESPACE"}],
+  },
+  {
+    parser: "",
+    css: ",/* \n :) \n */)",
+    expected: [{type: "COMMA"}, {type: "CLOSE-PAREN"}],
+  },
+  {
+    parser: "",
+    css: ":/*/*/",
+    expected: [{type: "COLON"}],
+  },
+  {
+    parser: "",
+    css: "/**/*",
+    expected: [{type: "DELIM", value: '*'}],
+  },
+  {
+    parser: "",
+    css: ";/******",
+    expected: [{type: "SEMICOLON"}],
   },
 
   // parseAStylesheet()

From 6d44cff4e5e2e5d793d7bcb739ccc2ac32c338aa Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 21:19:45 +0800
Subject: [PATCH 17/25] Update tests to match the latest spec

---
 tests.js | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/tests.js b/tests.js
index 1614bb8..a7f36ab 100644
--- a/tests.js
+++ b/tests.js
@@ -452,12 +452,12 @@ var TESTS = [
   {
     parser: "",
     css: "\u2003",  // em-space
-    expected: [{type: "IDENT", value: "\u2003"}],
+    expected: [{type: "DELIM", value: "\u2003"}],
   },
   {
     parser: "",
     css: "\u{A0}",  // non-breaking space
-    expected: [{type: "IDENT", value: "\u{A0}"}],
+    expected: [{type: "DELIM", value: "\u{A0}"}],
   },
   {
     parser: "",
@@ -519,7 +519,7 @@ var TESTS = [
   {
     parser: "",
     css: "url(  \'bar.gif\'",
-    expected: [{type: "FUNCTION", value: "url"}, {type: "STRING", value: "bar.gif"}],
+    expected: [{type: "FUNCTION", value: "url"}, {type: "WHITESPACE"}, {type: "STRING", value: "bar.gif"}],
   },
 
   // -- AtKeywordToken
@@ -956,12 +956,12 @@ var TESTS = [
   {
     parser: "",
     css: "-12.0em",
-    expected: [{type: "DIMENSION", value: -12, isInteger: false, unit: "em"}],
+    expected: [{type: "DIMENSION", value: -12, isInteger: false, unit: "em", sign: "-"}],
   },
   {
     parser: "",
     css: "+45.6__qem",
-    expected: [{type: "DIMENSION", value: 45.6, isInteger: false, unit: "__qem"}],
+    expected: [{type: "DIMENSION", value: 45.6, isInteger: false, unit: "__qem", sign: "+"}],
   },
   {
     parser: "",
@@ -1028,12 +1028,12 @@ var TESTS = [
   {
     parser: "",
     css: "+12.0%",
-    expected: [{type: "PERCENTAGE", value: 12}],
+    expected: [{type: "PERCENTAGE", value: 12, sign: "+"}],
   },
   {
     parser: "",
     css: "-48.99%",
-    expected: [{type: "PERCENTAGE", value: -48.99}],
+    expected: [{type: "PERCENTAGE", value: -48.99, sign: "-"}],
   },
   {
     parser: "",

From b07b86ba794a8c2a2076d1b6c5f65cc5b795c7bb Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 22:37:52 +0800
Subject: [PATCH 18/25] Fix missing isInteger property for DimensionToken

- ref: https://drafts.csswg.org/css-syntax/#consume-numeric-token
---
 parse-css.js | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/parse-css.js b/parse-css.js
index 489d040..258a53e 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -258,7 +258,7 @@ function tokenize(str) {
     var {value, isInteger, sign} = consumeANumber();
     if(wouldStartAnIdentifier(next(1), next(2), next(3))) {
       const unit = consumeAName();
-      return new DimensionToken(value, unit, sign);
+      return new DimensionToken(value, isInteger, unit, sign);
     } else if(next() == 0x25) {
       consume();
       return new PercentageToken(value, sign);
@@ -737,9 +737,10 @@ class PercentageToken extends CSSParserToken {
 }
 
 class DimensionToken extends CSSParserToken {
-  constructor(val, unit, sign=undefined) {
+  constructor(val, isInteger, unit, sign=undefined) {
     super("DIMENSION");
     this.value = val;
+    this.isInteger = isInteger;
     this.unit = unit;
     this.sign = sign;
   }
@@ -747,7 +748,7 @@ class DimensionToken extends CSSParserToken {
     const sign = this.sign == "+" ? "+" : "";
     return `DIM(${sign}${this.value}, ${this.unit})`;
   }
-  toJSON() { return {type:this.type, value:this.value, unit:this.unit}; }
+  toJSON() { return {type:this.type, value:this.value, isInteger:this.isInteger, unit:this.unit, sign:this.sign}; }
   toSource() {
     let unit = escapeIdent(this.unit);
     if(unit[0].toLowerCase() == "e" && (unit[1] == "-" || digit(unit[1].charCodeAt(0)))) {

From 0cf7f4484ee8371c9e8290f252390ba7ec89970a Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 22:50:02 +0800
Subject: [PATCH 19/25] Fix precision issue for float number

---
 parse-css.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/parse-css.js b/parse-css.js
index 258a53e..8bb5cb8 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -465,8 +465,12 @@ function tokenize(str) {
       }
       isInteger = false;
     }
-    let value = +numberPart;
-    if(exponentPart) value = value * Math.pow(10, +exponentPart);
+
+    // parse with native engine to prevent a precision issue
+    // (e.g. 12E-1 becomes 1.2000000000000002)
+    let value = Number(numberPart + (exponentPart ? 'e' + exponentPart : ''));
+    // let value = +numberPart;
+    // if(exponentPart) value = value * Math.pow(10, +exponentPart);
 
     return {value, isInteger, sign};
   };

From 7ca4aa5feba4668bef7b0ac8741f5a59b0f2db33 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 22:57:35 +0800
Subject: [PATCH 20/25] Fix inconsistently missing EOFToken for tokenize()

---
 parse-css.js |   8 +-
 tests.js     | 426 +++++++++++++++++++++++++++------------------------
 2 files changed, 229 insertions(+), 205 deletions(-)

diff --git a/parse-css.js b/parse-css.js
index 8bb5cb8..0aae9c1 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -491,8 +491,12 @@ function tokenize(str) {
 
 
   var iterationCount = 0;
-  while(!eof(next())) {
-    tokens.push(consumeAToken());
+  while (true) {
+    var token = consumeAToken();
+    tokens.push(token);
+    if (token instanceof EOFToken) {
+      break;
+    }
     iterationCount++;
     if(iterationCount > str.length*2) return "I'm infinite-looping!";
   }
diff --git a/tests.js b/tests.js
index a7f36ab..e6e92b3 100644
--- a/tests.js
+++ b/tests.js
@@ -33,6 +33,7 @@ var TESTS = [
       {type: "IDENT", value: "\uFFFD"},
       {type: "COMMA"},
       {type: "IDENT", value: "\uFFFD"},
+      {type: "EOF"},
     ]
   },
 
@@ -42,1008 +43,1008 @@ var TESTS = [
   {
     parser: "",
     css: "(",
-    expected: [{type: "OPEN-PAREN"}],
+    expected: [{type: "OPEN-PAREN"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ")",
-    expected: [{type: "CLOSE-PAREN"}],
+    expected: [{type: "CLOSE-PAREN"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "[",
-    expected: [{type: "OPEN-SQUARE"}],
+    expected: [{type: "OPEN-SQUARE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "]",
-    expected: [{type: "CLOSE-SQUARE"}],
+    expected: [{type: "CLOSE-SQUARE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ",",
-    expected: [{type: "COMMA"}],
+    expected: [{type: "COMMA"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ":",
-    expected: [{type: "COLON"}],
+    expected: [{type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ";",
-    expected: [{type: "SEMICOLON"}],
+    expected: [{type: "SEMICOLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ")[",
-    expected: [{type: "CLOSE-PAREN"}, {type: "OPEN-SQUARE"}],
+    expected: [{type: "CLOSE-PAREN"}, {type: "OPEN-SQUARE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "[)",
-    expected: [{type: "OPEN-SQUARE"}, {type: "CLOSE-PAREN"}],
+    expected: [{type: "OPEN-SQUARE"}, {type: "CLOSE-PAREN"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "{}",
-    expected: [{type: "OPEN-CURLY"}, {type: "CLOSE-CURLY"}],
+    expected: [{type: "OPEN-CURLY"}, {type: "CLOSE-CURLY"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ",,",
-    expected: [{type: "COMMA"}, {type: "COMMA"}],
+    expected: [{type: "COMMA"}, {type: "COMMA"}, {type: "EOF"}],
   },
 
   // -- MultipleCharacterTokens
   {
     parser: "",
     css: "~=",
-    expected: [{type: "DELIM", value: '~'}, {type: "DELIM", value: '='}],
+    expected: [{type: "DELIM", value: '~'}, {type: "DELIM", value: '='}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "|=",
-    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '='}],
+    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '='}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "^=",
-    expected: [{type: "DELIM", value: '^'}, {type: "DELIM", value: '='}],
+    expected: [{type: "DELIM", value: '^'}, {type: "DELIM", value: '='}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "$=",
-    expected: [{type: "DELIM", value: '$'}, {type: "DELIM", value: '='}],
+    expected: [{type: "DELIM", value: '$'}, {type: "DELIM", value: '='}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "*=",
-    expected: [{type: "DELIM", value: '*'}, {type: "DELIM", value: '='}],
+    expected: [{type: "DELIM", value: '*'}, {type: "DELIM", value: '='}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "||",
-    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}],
+    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "|||",
-    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}],
+    expected: [{type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}, {type: "DELIM", value: '|'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "",
-    expected: [{type: "CDC"}],
+    expected: [{type: "CDC"}, {type: "EOF"}],
   },
 
   // -- DelimiterToken
   {
     parser: "",
     css: "^",
-    expected: [{type: "DELIM", value: '^'}],
+    expected: [{type: "DELIM", value: '^'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "*",
-    expected: [{type: "DELIM", value: '*'}],
+    expected: [{type: "DELIM", value: '*'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "%",
-    expected: [{type: "DELIM", value: '%'}],
+    expected: [{type: "DELIM", value: '%'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "~",
-    expected: [{type: "DELIM", value: '~'}],
+    expected: [{type: "DELIM", value: '~'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "&",
-    expected: [{type: "DELIM", value: '&'}],
+    expected: [{type: "DELIM", value: '&'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "|",
-    expected: [{type: "DELIM", value: '|'}],
+    expected: [{type: "DELIM", value: '|'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\x7f",
-    expected: [{type: "DELIM", value: '\x7f'}],
+    expected: [{type: "DELIM", value: '\x7f'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\x01",
-    expected: [{type: "DELIM", value: '\x01'}],
+    expected: [{type: "DELIM", value: '\x01'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "~-",
-    expected: [{type: "DELIM", value: '~'}, {type: "DELIM", value: '-'}],
+    expected: [{type: "DELIM", value: '~'}, {type: "DELIM", value: '-'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "^|",
-    expected: [{type: "DELIM", value: '^'}, {type: "DELIM", value: '|'}],
+    expected: [{type: "DELIM", value: '^'}, {type: "DELIM", value: '|'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "$~",
-    expected: [{type: "DELIM", value: '$'}, {type: "DELIM", value: '~'}],
+    expected: [{type: "DELIM", value: '$'}, {type: "DELIM", value: '~'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "*^",
-    expected: [{type: "DELIM", value: '*'}, {type: "DELIM", value: '^'}],
+    expected: [{type: "DELIM", value: '*'}, {type: "DELIM", value: '^'}, {type: "EOF"}],
   },
 
   // -- WhitespaceTokens
   {
     parser: "",
     css: "   ",
-    expected: [{type: "WHITESPACE"}],
+    expected: [{type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\n\rS",
-    expected: [{type: "WHITESPACE"}, {type: "IDENT", value: "S"}],
+    expected: [{type: "WHITESPACE"}, {type: "IDENT", value: "S"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "   *",
-    expected: [{type: "WHITESPACE"}, {type: "DELIM", value: '*'}],
+    expected: [{type: "WHITESPACE"}, {type: "DELIM", value: '*'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\r\n\f\t2",
-    expected: [{type: "WHITESPACE"}, {type: "NUMBER", value: 2, isInteger: true}],
+    expected: [{type: "WHITESPACE"}, {type: "NUMBER", value: 2, isInteger: true}, {type: "EOF"}],
   },
 
   // -- Escapes
   {
     parser: "",
     css: "hel\\6Co",
-    expected: [{type: "IDENT", value: "hello"}],
+    expected: [{type: "IDENT", value: "hello"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\26 B",
-    expected: [{type: "IDENT", value: "&B"}],
+    expected: [{type: "IDENT", value: "&B"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'hel\\6c o'",
-    expected: [{type: "STRING", value: "hello"}],
+    expected: [{type: "STRING", value: "hello"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'spac\\65\r\ns'",
-    expected: [{type: "STRING", value: "spaces"}],
+    expected: [{type: "STRING", value: "spaces"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "spac\\65\r\ns",
-    expected: [{type: "IDENT", value: "spaces"}],
+    expected: [{type: "IDENT", value: "spaces"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "spac\\65\n\rs",
-    expected: [{type: "IDENT", value: "space"}, {type: "WHITESPACE"}, {type: "IDENT", value: "s"}],
+    expected: [{type: "IDENT", value: "space"}, {type: "WHITESPACE"}, {type: "IDENT", value: "s"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "sp\\61\tc\\65\fs",
-    expected: [{type: "IDENT", value: "spaces"}],
+    expected: [{type: "IDENT", value: "spaces"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "hel\\6c  o",
-    expected: [{type: "IDENT", value: "hell"}, {type: "WHITESPACE"}, {type: "IDENT", value: "o"}],
+    expected: [{type: "IDENT", value: "hell"}, {type: "WHITESPACE"}, {type: "IDENT", value: "o"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "test\\\n",
-    expected: [{type: "IDENT", value: "test"}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+    expected: [{type: "IDENT", value: "test"}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "test\\D799",
-    expected: [{type: "IDENT", value: "test\uD799"}],
+    expected: [{type: "IDENT", value: "test\uD799"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\E000",
-    expected: [{type: "IDENT", value: "\uE000"}],
+    expected: [{type: "IDENT", value: "\uE000"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "te\\s\\t",
-    expected: [{type: "IDENT", value: "test"}],
+    expected: [{type: "IDENT", value: "test"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "spaces\\ in\\\tident",
-    expected: [{type: "IDENT", value: "spaces in\tident"}],
+    expected: [{type: "IDENT", value: "spaces in\tident"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\.\\,\\:\\!",
-    expected: [{type: "IDENT", value: ".,:!"}],
+    expected: [{type: "IDENT", value: ".,:!"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\\r",
-    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\\f",
-    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\\r\n",
-    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+    expected: [{type: "DELIM", value: '\\'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "null\\\0",
-    expected: [{type: "IDENT", value: "null\uFFFD"}],
+    expected: [{type: "IDENT", value: "null\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "null\\\0\0",
-    expected: [{type: "IDENT", value: "null\uFFFD\uFFFD"}],
+    expected: [{type: "IDENT", value: "null\uFFFD\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "null\\0",
-    expected: [{type: "IDENT", value: "null\uFFFD"}],
+    expected: [{type: "IDENT", value: "null\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "null\\0",
-    expected: [{type: "IDENT", value: "null\uFFFD"}],
+    expected: [{type: "IDENT", value: "null\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "null\\0000",
-    expected: [{type: "IDENT", value: "null\uFFFD"}],
+    expected: [{type: "IDENT", value: "null\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "large\\110000",
-    expected: [{type: "IDENT", value: "large\uFFFD"}],
+    expected: [{type: "IDENT", value: "large\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "large\\23456a",
-    expected: [{type: "IDENT", value: "large\uFFFD"}],
+    expected: [{type: "IDENT", value: "large\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "surrogate\\D800",
-    expected: [{type: "IDENT", value: "surrogate\uFFFD"}],
+    expected: [{type: "IDENT", value: "surrogate\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "surrogate\\0DABC",
-    expected: [{type: "IDENT", value: "surrogate\uFFFD"}],
+    expected: [{type: "IDENT", value: "surrogate\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\00DFFFsurrogate",
-    expected: [{type: "IDENT", value: "\uFFFDsurrogate"}],
+    expected: [{type: "IDENT", value: "\uFFFDsurrogate"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\10fFfF",
-    expected: [{type: "IDENT", value: "\u{10ffff}"}],
+    expected: [{type: "IDENT", value: "\u{10ffff}"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\10fFfF0",
-    expected: [{type: "IDENT", value: "\u{10ffff}0"}],
+    expected: [{type: "IDENT", value: "\u{10ffff}0"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\\10000000",
-    expected: [{type: "IDENT", value: "\u{100000}00"}],
+    expected: [{type: "IDENT", value: "\u{100000}00"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "eof\\",
-    expected: [{type: "IDENT", value: "eof\uFFFD"}],
+    expected: [{type: "IDENT", value: "eof\uFFFD"}, {type: "EOF"}],
   },
 
   // -- IdentToken
   {
     parser: "",
     css: "simple-ident",
-    expected: [{type: "IDENT", value: "simple-ident"}],
+    expected: [{type: "IDENT", value: "simple-ident"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "testing123",
-    expected: [{type: "IDENT", value: "testing123"}],
+    expected: [{type: "IDENT", value: "testing123"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "hello!",
-    expected: [{type: "IDENT", value: "hello"}, {type: "DELIM", value: '!'}],
+    expected: [{type: "IDENT", value: "hello"}, {type: "DELIM", value: '!'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "world\x05",
-    expected: [{type: "IDENT", value: "world"}, {type: "DELIM", value: '\x05'}],
+    expected: [{type: "IDENT", value: "world"}, {type: "DELIM", value: '\x05'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "_under score",
-    expected: [{type: "IDENT", value: "_under"}, {type: "WHITESPACE"}, {type: "IDENT", value: "score"}],
+    expected: [{type: "IDENT", value: "_under"}, {type: "WHITESPACE"}, {type: "IDENT", value: "score"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-_underscore",
-    expected: [{type: "IDENT", value: "-_underscore"}],
+    expected: [{type: "IDENT", value: "-_underscore"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-text",
-    expected: [{type: "IDENT", value: "-text"}],
+    expected: [{type: "IDENT", value: "-text"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-\\6d",
-    expected: [{type: "IDENT", value: "-m"}],
+    expected: [{type: "IDENT", value: "-m"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "--abc",
-    expected: [{type: "IDENT", value: "--abc"}],
+    expected: [{type: "IDENT", value: "--abc"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "--",
-    expected: [{type: "IDENT", value: "--"}],
+    expected: [{type: "IDENT", value: "--"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "--11",
-    expected: [{type: "IDENT", value: "--11"}],
+    expected: [{type: "IDENT", value: "--11"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "---",
-    expected: [{type: "IDENT", value: "---"}],
+    expected: [{type: "IDENT", value: "---"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\u2003",  // em-space
-    expected: [{type: "DELIM", value: "\u2003"}],
+    expected: [{type: "DELIM", value: "\u2003"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\u{A0}",  // non-breaking space
-    expected: [{type: "DELIM", value: "\u{A0}"}],
+    expected: [{type: "DELIM", value: "\u{A0}"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\u1234",
-    expected: [{type: "IDENT", value: "\u1234"}],
+    expected: [{type: "IDENT", value: "\u1234"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\u{12345}",
-    expected: [{type: "IDENT", value: "\u{12345}"}],
+    expected: [{type: "IDENT", value: "\u{12345}"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\0",
-    expected: [{type: "IDENT", value: "\uFFFD"}],
+    expected: [{type: "IDENT", value: "\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "ab\0c",
-    expected: [{type: "IDENT", value: "ab\uFFFDc"}],
+    expected: [{type: "IDENT", value: "ab\uFFFDc"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "ab\0c",
-    expected: [{type: "IDENT", value: "ab\uFFFDc"}],
+    expected: [{type: "IDENT", value: "ab\uFFFDc"}, {type: "EOF"}],
   },
 
   // -- FunctionToken
   {
     parser: "",
     css: "scale(2)",
-    expected: [{type: "FUNCTION", value: "scale"}, {type: "NUMBER", value: 2, isInteger: true}, {type: "CLOSE-PAREN"}],
+    expected: [{type: "FUNCTION", value: "scale"}, {type: "NUMBER", value: 2, isInteger: true}, {type: "CLOSE-PAREN"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "foo-bar\\ baz(",
-    expected: [{type: "FUNCTION", value: "foo-bar baz"}],
+    expected: [{type: "FUNCTION", value: "foo-bar baz"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "fun\\(ction(",
-    expected: [{type: "FUNCTION", value: "fun(ction"}],
+    expected: [{type: "FUNCTION", value: "fun(ction"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-foo(",
-    expected: [{type: "FUNCTION", value: "-foo"}],
+    expected: [{type: "FUNCTION", value: "-foo"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "url(\"foo.gif\"",
-    expected: [{type: "FUNCTION", value: "url"}, {type: "STRING", value: "foo.gif"}],
+    expected: [{type: "FUNCTION", value: "url"}, {type: "STRING", value: "foo.gif"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "foo(  \'bar.gif\'",
-    expected: [{type: "FUNCTION", value: "foo"}, {type: "WHITESPACE"}, {type: "STRING", value: "bar.gif"}],
+    expected: [{type: "FUNCTION", value: "foo"}, {type: "WHITESPACE"}, {type: "STRING", value: "bar.gif"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "url(  \'bar.gif\'",
-    expected: [{type: "FUNCTION", value: "url"}, {type: "WHITESPACE"}, {type: "STRING", value: "bar.gif"}],
+    expected: [{type: "FUNCTION", value: "url"}, {type: "WHITESPACE"}, {type: "STRING", value: "bar.gif"}, {type: "EOF"}],
   },
 
   // -- AtKeywordToken
   {
     parser: "",
     css: "@at-keyword",
-    expected: [{type: "AT-KEYWORD", value: "at-keyword"}],
+    expected: [{type: "AT-KEYWORD", value: "at-keyword"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@testing123",
-    expected: [{type: "AT-KEYWORD", value: "testing123"}],
+    expected: [{type: "AT-KEYWORD", value: "testing123"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@hello!",
-    expected: [{type: "AT-KEYWORD", value: "hello"}, {type: "DELIM", value: '!'}],
+    expected: [{type: "AT-KEYWORD", value: "hello"}, {type: "DELIM", value: '!'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@-text",
-    expected: [{type: "AT-KEYWORD", value: "-text"}],
+    expected: [{type: "AT-KEYWORD", value: "-text"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@--abc",
-    expected: [{type: "AT-KEYWORD", value: "--abc"}],
+    expected: [{type: "AT-KEYWORD", value: "--abc"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@--",
-    expected: [{type: "AT-KEYWORD", value: "--"}],
+    expected: [{type: "AT-KEYWORD", value: "--"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@--11",
-    expected: [{type: "AT-KEYWORD", value: "--11"}],
+    expected: [{type: "AT-KEYWORD", value: "--11"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@---",
-    expected: [{type: "AT-KEYWORD", value: "---"}],
+    expected: [{type: "AT-KEYWORD", value: "---"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@\\ ",
-    expected: [{type: "AT-KEYWORD", value: " "}],
+    expected: [{type: "AT-KEYWORD", value: " "}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@-\\ ",
-    expected: [{type: "AT-KEYWORD", value: "- "}],
+    expected: [{type: "AT-KEYWORD", value: "- "}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@@",
-    expected: [{type: "DELIM", value: '@'}, {type: "DELIM", value: '@'}],
+    expected: [{type: "DELIM", value: '@'}, {type: "DELIM", value: '@'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@2",
-    expected: [{type: "DELIM", value: '@'}, {type: "NUMBER", value: 2, isInteger: true}],
+    expected: [{type: "DELIM", value: '@'}, {type: "NUMBER", value: 2, isInteger: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "@-1",
-    expected: [{type: "DELIM", value: '@'}, {type: "NUMBER", value: -1, isInteger: true, sign: "-"}],
+    expected: [{type: "DELIM", value: '@'}, {type: "NUMBER", value: -1, isInteger: true, sign: "-"}, {type: "EOF"}],
   },
 
   // -- UrlToken
   {
     parser: "",
     css: "url(foo.gif)",
-    expected: [{type: "URL", value: "foo.gif"}],
+    expected: [{type: "URL", value: "foo.gif"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "urL(https://example.com/cats.png)",
-    expected: [{type: "URL", value: "https://example.com/cats.png"}],
+    expected: [{type: "URL", value: "https://example.com/cats.png"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "uRl(what-a.crazy^URL~this\\ is!)",
-    expected: [{type: "URL", value: "what-a.crazy^URL~this is!"}],
+    expected: [{type: "URL", value: "what-a.crazy^URL~this is!"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "uRL(123#test)",
-    expected: [{type: "URL", value: "123#test"}],
+    expected: [{type: "URL", value: "123#test"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "Url(escapes\\ \\\"\\'\\)\\()",
-    expected: [{type: "URL", value: "escapes \"')("}],
+    expected: [{type: "URL", value: "escapes \"')("}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "UrL(   whitespace   )",
-    expected: [{type: "URL", value: "whitespace"}],
+    expected: [{type: "URL", value: "whitespace"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "URl( whitespace-eof ",
-    expected: [{type: "URL", value: "whitespace-eof"}],
+    expected: [{type: "URL", value: "whitespace-eof"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "URL(eof",
-    expected: [{type: "URL", value: "eof"}],
+    expected: [{type: "URL", value: "eof"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "url(not/*a*/comment)",
-    expected: [{type: "URL", value: "not/*a*/comment"}],
+    expected: [{type: "URL", value: "not/*a*/comment"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "urL()",
-    expected: [{type: "URL", value: ""}],
+    expected: [{type: "URL", value: ""}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "uRl(white space),",
-    expected: [{type: "BADURL"}, {type: "COMMA"}],
+    expected: [{type: "BADURL"}, {type: "COMMA"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "Url(b(ad),",
-    expected: [{type: "BADURL"}, {type: "COMMA"}],
+    expected: [{type: "BADURL"}, {type: "COMMA"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "uRl(ba'd):",
-    expected: [{type: "BADURL"}, {type: "COLON"}],
+    expected: [{type: "BADURL"}, {type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "urL(b\"ad):",
-    expected: [{type: "BADURL"}, {type: "COLON"}],
+    expected: [{type: "BADURL"}, {type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "uRl(b\"ad):",
-    expected: [{type: "BADURL"}, {type: "COLON"}],
+    expected: [{type: "BADURL"}, {type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "Url(b\\\rad):",
-    expected: [{type: "BADURL"}, {type: "COLON"}],
+    expected: [{type: "BADURL"}, {type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "url(b\\\nad):",
-    expected: [{type: "BADURL"}, {type: "COLON"}],
+    expected: [{type: "BADURL"}, {type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "url(/*'bad')*/",
-    expected: [{type: "BADURL"}, {type: "DELIM", value: '*'}, {type: "DELIM", value: '/'}],
+    expected: [{type: "BADURL"}, {type: "DELIM", value: '*'}, {type: "DELIM", value: '/'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "url(ba'd\\\\))",
-    expected: [{type: "BADURL"}, {type: "CLOSE-PAREN"}],
+    expected: [{type: "BADURL"}, {type: "CLOSE-PAREN"}, {type: "EOF"}],
   },
 
   // -- StringToken
   {
     parser: "",
     css: "'text'",
-    expected: [{type: "STRING", value: "text"}],
+    expected: [{type: "STRING", value: "text"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\"text\"",
-    expected: [{type: "STRING", value: "text"}],
+    expected: [{type: "STRING", value: "text"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'testing, 123!'",
-    expected: [{type: "STRING", value: "testing, 123!"}],
+    expected: [{type: "STRING", value: "testing, 123!"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'es\\'ca\\\"pe'",
-    expected: [{type: "STRING", value: "es'ca\"pe"}],
+    expected: [{type: "STRING", value: "es'ca\"pe"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'\"quotes\"'",
-    expected: [{type: "STRING", value: "\"quotes\""}],
+    expected: [{type: "STRING", value: "\"quotes\""}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\"'quotes'\"",
-    expected: [{type: "STRING", value: "'quotes'"}],
+    expected: [{type: "STRING", value: "'quotes'"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\"mismatch'",
-    expected: [{type: "STRING", value: "mismatch'"}],
+    expected: [{type: "STRING", value: "mismatch'"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'text\x05\t\x13'",
-    expected: [{type: "STRING", value: "text\x05\t\x13"}],
+    expected: [{type: "STRING", value: "text\x05\t\x13"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\"end on eof",
-    expected: [{type: "STRING", value: "end on eof"}],
+    expected: [{type: "STRING", value: "end on eof"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'esca\\\nped'",
-    expected: [{type: "STRING", value: "escaped"}],
+    expected: [{type: "STRING", value: "escaped"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\"esc\\\faped\"",
-    expected: [{type: "STRING", value: "escaped"}],
+    expected: [{type: "STRING", value: "escaped"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'new\\\rline'",
-    expected: [{type: "STRING", value: "newline"}],
+    expected: [{type: "STRING", value: "newline"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "\"new\\\r\nline\"",
-    expected: [{type: "STRING", value: "newline"}],
+    expected: [{type: "STRING", value: "newline"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'bad\nstring",
-    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'bad\rstring",
-    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'bad\r\nstring",
-    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'bad\fstring",
-    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}],
+    expected: [{type: "BADSTRING"}, {type: "WHITESPACE"}, {type: "IDENT", value: "string"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'\0'",
-    expected: [{type: "STRING", value: "\uFFFD"}],
+    expected: [{type: "STRING", value: "\uFFFD"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'hel\0lo'",
-    expected: [{type: "STRING", value: "hel\uFFFDlo"}],
+    expected: [{type: "STRING", value: "hel\uFFFDlo"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "'h\\65l\0lo'",
-    expected: [{type: "STRING", value: "hel\uFFFDlo"}],
+    expected: [{type: "STRING", value: "hel\uFFFDlo"}, {type: "EOF"}],
   },
 
   // -- HashToken
   {
     parser: "",
     css: "#id-selector",
-    expected: [{type: "HASH", value: "id-selector", isIdent: true}],
+    expected: [{type: "HASH", value: "id-selector", isIdent: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "#FF7700",
-    expected: [{type: "HASH", value: "FF7700", isIdent: true}],
+    expected: [{type: "HASH", value: "FF7700", isIdent: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "#3377FF",
-    expected: [{type: "HASH", value: "3377FF", isIdent: false}],
+    expected: [{type: "HASH", value: "3377FF", isIdent: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "#\\ ",
-    expected: [{type: "HASH", value: " ", isIdent: true}],
+    expected: [{type: "HASH", value: " ", isIdent: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "# ",
-    expected: [{type: "DELIM", value: '#'}, {type: "WHITESPACE"}],
+    expected: [{type: "DELIM", value: '#'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "#\\\n",
-    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "#\\\r\n",
-    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}],
+    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '\\'}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "#!",
-    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '!'}],
+    expected: [{type: "DELIM", value: '#'}, {type: "DELIM", value: '!'}, {type: "EOF"}],
   },
 
   // -- NumberToken
   {
     parser: "",
     css: "10",
-    expected: [{type: "NUMBER", value: 10, isInteger: true}],
+    expected: [{type: "NUMBER", value: 10, isInteger: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "12.0",
-    expected: [{type: "NUMBER", value: 12, isInteger: false}],
+    expected: [{type: "NUMBER", value: 12, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "+45.6",
-    expected: [{type: "NUMBER", value: 45.6, isInteger: false, sign: "+"}],
+    expected: [{type: "NUMBER", value: 45.6, isInteger: false, sign: "+"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-7",
-    expected: [{type: "NUMBER", value: -7, isInteger: true, sign: "-"}],
+    expected: [{type: "NUMBER", value: -7, isInteger: true, sign: "-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "010",
-    expected: [{type: "NUMBER", value: 10, isInteger: true}],
+    expected: [{type: "NUMBER", value: 10, isInteger: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "10e0",
-    expected: [{type: "NUMBER", value: 10, isInteger: false}],
+    expected: [{type: "NUMBER", value: 10, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "12e3",
-    expected: [{type: "NUMBER", value: 12000, isInteger: false}],
+    expected: [{type: "NUMBER", value: 12000, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "3e+1",
-    expected: [{type: "NUMBER", value: 30, isInteger: false}],
+    expected: [{type: "NUMBER", value: 30, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "12E-1",
-    expected: [{type: "NUMBER", value: 1.2, isInteger: false}],
+    expected: [{type: "NUMBER", value: 1.2, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ".7",
-    expected: [{type: "NUMBER", value: 0.7, isInteger: false}],
+    expected: [{type: "NUMBER", value: 0.7, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-.3",
-    expected: [{type: "NUMBER", value: -0.3, isInteger: false, sign: "-"}],
+    expected: [{type: "NUMBER", value: -0.3, isInteger: false, sign: "-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "+637.54e-2",
-    expected: [{type: "NUMBER", value: 6.3754, isInteger: false, sign: "+"}],
+    expected: [{type: "NUMBER", value: 6.3754, isInteger: false, sign: "+"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-12.34E+2",
-    expected: [{type: "NUMBER", value: -1234, isInteger: false, sign: "-"}],
+    expected: [{type: "NUMBER", value: -1234, isInteger: false, sign: "-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "+ 5",
-    expected: [{type: "DELIM", value: '+'}, {type: "WHITESPACE"}, {type: "NUMBER", value: 5, isInteger: true}],
+    expected: [{type: "DELIM", value: '+'}, {type: "WHITESPACE"}, {type: "NUMBER", value: 5, isInteger: true}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-+12",
-    expected: [{type: "DELIM", value: '-'}, {type: "NUMBER", value: 12, isInteger: true, sign: "+"}],
+    expected: [{type: "DELIM", value: '-'}, {type: "NUMBER", value: 12, isInteger: true, sign: "+"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "+-21",
-    expected: [{type: "DELIM", value: '+'}, {type: "NUMBER", value: -21, isInteger: true, sign: "-"}],
+    expected: [{type: "DELIM", value: '+'}, {type: "NUMBER", value: -21, isInteger: true, sign: "-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "++22",
-    expected: [{type: "DELIM", value: '+'}, {type: "NUMBER", value: 22, isInteger: true, sign: "+"}],
+    expected: [{type: "DELIM", value: '+'}, {type: "NUMBER", value: 22, isInteger: true, sign: "+"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "13.",
-    expected: [{type: "NUMBER", value: 13, isInteger: true}, {type: "DELIM", value: '.'}],
+    expected: [{type: "NUMBER", value: 13, isInteger: true}, {type: "DELIM", value: '.'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "1.e2",
-    expected: [{type: "NUMBER", value: 1, isInteger: true}, {type: "DELIM", value: '.'}, {type: "IDENT", value: "e2"}],
+    expected: [{type: "NUMBER", value: 1, isInteger: true}, {type: "DELIM", value: '.'}, {type: "IDENT", value: "e2"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "2e3.5",
-    expected: [{type: "NUMBER", value: 2000, isInteger: false}, {type: "NUMBER", value: 0.5, isInteger: false}],
+    expected: [{type: "NUMBER", value: 2000, isInteger: false}, {type: "NUMBER", value: 0.5, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "2e3.",
-    expected: [{type: "NUMBER", value: 2000, isInteger: false}, {type: "DELIM", value: '.'}],
+    expected: [{type: "NUMBER", value: 2000, isInteger: false}, {type: "DELIM", value: '.'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "1000000000000000000000000",
-    expected: [{type: "NUMBER", value: 1e24, isInteger: true}],
+    expected: [{type: "NUMBER", value: 1e24, isInteger: true}, {type: "EOF"}],
   },
 
   // -- DimensionToken
   {
     parser: "",
     css: "10px",
-    expected: [{type: "DIMENSION", value: 10, isInteger: true, unit: "px"}],
+    expected: [{type: "DIMENSION", value: 10, isInteger: true, unit: "px"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "12.0em",
-    expected: [{type: "DIMENSION", value: 12, isInteger: false, unit: "em"}],
+    expected: [{type: "DIMENSION", value: 12, isInteger: false, unit: "em"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-12.0em",
-    expected: [{type: "DIMENSION", value: -12, isInteger: false, unit: "em", sign: "-"}],
+    expected: [{type: "DIMENSION", value: -12, isInteger: false, unit: "em", sign: "-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "+45.6__qem",
-    expected: [{type: "DIMENSION", value: 45.6, isInteger: false, unit: "__qem", sign: "+"}],
+    expected: [{type: "DIMENSION", value: 45.6, isInteger: false, unit: "__qem", sign: "+"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "5e",
-    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e"}],
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "5px-2px",
-    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "px-2px"}],
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "px-2px"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "5e-",
-    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e-"}],
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "5\\ ",
-    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: " "}],
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: " "}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "40\\70\\78",
-    expected: [{type: "DIMENSION", value: 40, isInteger: true, unit: "px"}],
+    expected: [{type: "DIMENSION", value: 40, isInteger: true, unit: "px"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "4e3e2",
-    expected: [{type: "DIMENSION", value: 4000, isInteger: false, unit: "e2"}],
+    expected: [{type: "DIMENSION", value: 4000, isInteger: false, unit: "e2"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "0x10px",
-    expected: [{type: "DIMENSION", value: 0, isInteger: true, unit: "x10px"}],
+    expected: [{type: "DIMENSION", value: 0, isInteger: true, unit: "x10px"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "4unit ",
-    expected: [{type: "DIMENSION", value: 4, isInteger: true, unit: "unit"}, {type: "WHITESPACE"}],
+    expected: [{type: "DIMENSION", value: 4, isInteger: true, unit: "unit"}, {type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "5e+",
-    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e"}, {type: "DELIM", value: '+'}],
+    expected: [{type: "DIMENSION", value: 5, isInteger: true, unit: "e"}, {type: "DELIM", value: '+'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "2e.5",
-    expected: [{type: "DIMENSION", value: 2, isInteger: true, unit: "e"}, {type: "NUMBER", value: 0.5, isInteger: false}],
+    expected: [{type: "DIMENSION", value: 2, isInteger: true, unit: "e"}, {type: "NUMBER", value: 0.5, isInteger: false}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "2e+.5",
-    expected: [{type: "DIMENSION", value: 2, isInteger: true, unit: "e"}, {type: "NUMBER", value: 0.5, isInteger: false, sign: "+"}],
+    expected: [{type: "DIMENSION", value: 2, isInteger: true, unit: "e"}, {type: "NUMBER", value: 0.5, isInteger: false, sign: "+"}, {type: "EOF"}],
   },
 
   // -- PercentageToken
   {
     parser: "",
     css: "10%",
-    expected: [{type: "PERCENTAGE", value: 10}],
+    expected: [{type: "PERCENTAGE", value: 10}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "+12.0%",
-    expected: [{type: "PERCENTAGE", value: 12, sign: "+"}],
+    expected: [{type: "PERCENTAGE", value: 12, sign: "+"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "-48.99%",
-    expected: [{type: "PERCENTAGE", value: -48.99, sign: "-"}],
+    expected: [{type: "PERCENTAGE", value: -48.99, sign: "-"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "6e-1%",
-    expected: [{type: "PERCENTAGE", value: 0.6}],
+    expected: [{type: "PERCENTAGE", value: 0.6}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "5%%",
-    expected: [{type: "PERCENTAGE", value: 5}, {type: "DELIM", value: '%'}],
+    expected: [{type: "PERCENTAGE", value: 5}, {type: "DELIM", value: '%'}, {type: "EOF"}],
   },
 
   // -- UnicodeRangeToken
@@ -1054,6 +1055,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "NUMBER", value: 12345, isInteger: true, sign: "+"},
       {type: "NUMBER", value: -123456, isInteger: true, sign: "-"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1063,6 +1065,7 @@ var TESTS = [
       {type: "IDENT", value: "U"},
       {type: "NUMBER", value: 1234, isInteger: true, sign: "+"},
       {type: "NUMBER", value: -2345, isInteger: true, sign: "-"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1072,6 +1075,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "NUMBER", value: 222, isInteger: true, sign: "+"},
       {type: "NUMBER", value: -111, isInteger: true, sign: "-"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1081,6 +1085,7 @@ var TESTS = [
       {type: "IDENT", value: "U"},
       {type: "DELIM", value: "+"},
       {type: "IDENT", value: "CafE-d00D"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1091,6 +1096,7 @@ var TESTS = [
       {type: "NUMBER", value: 2, isInteger: true, sign: "+"},
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1102,6 +1108,7 @@ var TESTS = [
       {type: "IDENT", value: "ab12"},
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1116,6 +1123,7 @@ var TESTS = [
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1126,6 +1134,7 @@ var TESTS = [
       {type: "DELIM", value: "+"},
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1135,6 +1144,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "NUMBER", value: 222, isInteger: true, sign: "+"},
       {type: "NUMBER", value: 111, isInteger: true, sign: "+"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1143,6 +1153,7 @@ var TESTS = [
     expected: [
       {type: "IDENT", value: "u"},
       {type: "NUMBER", value: 12345678, isInteger: true, sign: "+"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1152,6 +1163,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "NUMBER", value: 123, isInteger: true, sign: "+"},
       {type: "NUMBER", value: -12345678, isInteger: true, sign: "-"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1161,6 +1173,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "DELIM", value: "+"},
       {type: "IDENT", value: "cake"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1169,6 +1182,7 @@ var TESTS = [
     expected: [
       {type: "IDENT", value: "u"},
       {type: "DIMENSION", value: 1234, isInteger: true, unit: "-gggg", sign: "+"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1181,6 +1195,7 @@ var TESTS = [
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1192,6 +1207,7 @@ var TESTS = [
       {type: "IDENT", value: "a1"},
       {type: "DELIM", value: "?"},
       {type: "NUMBER", value: -123, isInteger: true, sign: "-"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1203,6 +1219,7 @@ var TESTS = [
       {type: "DELIM", value: "?"},
       {type: "DELIM", value: "?"},
       {type: "NUMBER", value: 4, isInteger: true},
+      {type: "EOF"},
     ],
   },
   {
@@ -1212,6 +1229,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "DELIM", value: "+"},
       {type: "IDENT", value: "z"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1220,6 +1238,7 @@ var TESTS = [
     expected: [
       {type: "IDENT", value: "u"},
       {type: "DELIM", value: "+"},
+      {type: "EOF"},
     ],
   },
   {
@@ -1229,6 +1248,7 @@ var TESTS = [
       {type: "IDENT", value: "u"},
       {type: "DELIM", value: "+"},
       {type: "NUMBER", value: -543, isInteger: true, sign: "-"},
+      {type: "EOF"},
     ],
   },
 
@@ -1236,37 +1256,37 @@ var TESTS = [
   {
     parser: "",
     css: "/*comment*/a",
-    expected: [{type: "IDENT", value: "a"}],
+    expected: [{type: "IDENT", value: "a"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "/**\\2f**//",
-    expected: [{type: "DELIM", value: '/'}],
+    expected: [{type: "DELIM", value: '/'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "/**y*a*y**/ ",
-    expected: [{type: "WHITESPACE"}],
+    expected: [{type: "WHITESPACE"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ",/* \n :) \n */)",
-    expected: [{type: "COMMA"}, {type: "CLOSE-PAREN"}],
+    expected: [{type: "COMMA"}, {type: "CLOSE-PAREN"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ":/*/*/",
-    expected: [{type: "COLON"}],
+    expected: [{type: "COLON"}, {type: "EOF"}],
   },
   {
     parser: "",
     css: "/**/*",
-    expected: [{type: "DELIM", value: '*'}],
+    expected: [{type: "DELIM", value: '*'}, {type: "EOF"}],
   },
   {
     parser: "",
     css: ";/******",
-    expected: [{type: "SEMICOLON"}],
+    expected: [{type: "SEMICOLON"}, {type: "EOF"}],
   },
 
   // parseAStylesheet()

From 2902404f90a0490e4ee966cfcf2858e034418b97 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Sun, 31 Mar 2024 23:25:52 +0800
Subject: [PATCH 21/25] Throw Error for infinite looping

---
 parse-css.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/parse-css.js b/parse-css.js
index 0aae9c1..4e3c88f 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -498,7 +498,7 @@ function tokenize(str) {
       break;
     }
     iterationCount++;
-    if(iterationCount > str.length*2) return "I'm infinite-looping!";
+    if(iterationCount > str.length*2) throw new Error("I'm infinite-looping!");
   }
   return tokens;
 }

From 1e4514f3a75ad1ce37a575a57650c3ea3007a7a2 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Mon, 1 Apr 2024 01:15:17 +0800
Subject: [PATCH 22/25] Add a test for escaped ")" in a bad URL

---
 tests.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/tests.js b/tests.js
index e6e92b3..b8d6fbc 100644
--- a/tests.js
+++ b/tests.js
@@ -681,6 +681,11 @@ var TESTS = [
     css: "url(/*'bad')*/",
     expected: [{type: "BADURL"}, {type: "DELIM", value: '*'}, {type: "DELIM", value: '/'}, {type: "EOF"}],
   },
+  {
+    parser: "",
+    css: "url(ba'd\\))",
+    expected: [{type: "BADURL"}, {type: "EOF"}],
+  },
   {
     parser: "",
     css: "url(ba'd\\\\))",

From f0c54b521dc480f52d92e12185f2347de824db8f Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Mon, 1 Apr 2024 09:38:01 +0800
Subject: [PATCH 23/25] Fix bad calls

---
 parse-css.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/parse-css.js b/parse-css.js
index 4e3c88f..4af7108 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -932,7 +932,7 @@ function consumeAnAtRule(s, nested=false) {
       if(nested) return filterValid(rule);
       else {
         parseerror(s, "Hit an unmatched } in the prelude of an at-rule.");
-        rule.prelude.push(consumeToken(s));
+        rule.prelude.push(s.consumeToken());
       }
     } else if(token instanceof OpenCurlyToken) {
       [rule.declarations, rule.rules] = consumeABlock(s);
@@ -951,10 +951,10 @@ function consumeAQualifiedRule(s, nested=false, stopToken=EOFToken) {
       parseerror(s, "Hit EOF or semicolon when trying to parse the prelude of a qualified rule.");
       return;
     } else if(token instanceof CloseCurlyToken) {
-      parseerror("Hit an unmatched } in the prelude of a qualified rule.");
+      parseerror(s, "Hit an unmatched } in the prelude of a qualified rule.");
       if(nested) return;
       else {
-        rule.prelude.push(consumeToken(s));
+        rule.prelude.push(s.consumeToken());
       }
     } else if(token instanceof OpenCurlyToken) {
       if(looksLikeACustomProperty(rule.prelude)) {

From 20ca4f600e86802ad52b92bbcc469fb8638634e4 Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Mon, 1 Apr 2024 09:44:27 +0800
Subject: [PATCH 24/25] Simplify BadURLToken

---
 parse-css.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/parse-css.js b/parse-css.js
index 4af7108..5d0c80a 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -527,7 +527,7 @@ class BadURLToken extends CSSParserToken {
   constructor() {
     super("BADURL");
   }
-  toSource() { return "url(BADURL '')"}
+  toSource() { return "url(BAD URL)"}
 }
 BadURLToken.prototype.tokenType = "BADURL";
 

From b7d408b5a993f51458eaf24d11f9db90f2e289aa Mon Sep 17 00:00:00 2001
From: Danny Lin 
Date: Mon, 1 Apr 2024 09:46:47 +0800
Subject: [PATCH 25/25] Prevent extra double quote for BadStringToken

- Ensure equality after re-serialization and re-tokenization.
  e.g. '"\n' === (tokenize) BADSTRING WS EOF <===> (serialize) '"\n '
---
 parse-css.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/parse-css.js b/parse-css.js
index 5d0c80a..bf5c79b 100644
--- a/parse-css.js
+++ b/parse-css.js
@@ -520,7 +520,7 @@ class BadStringToken extends CSSParserToken {
   constructor() {
     super("BADSTRING");
   }
-  toSource() { return '"\n"'; }
+  toSource() { return '"\n'; }
 }
 
 class BadURLToken extends CSSParserToken {