1 module dom.cssparser;
2 
3 /**
4 Before sending the input stream to the tokenizer, implementations must make the following code point substitutions:
5     * Replace any U+000D CARRIAGE RETURN (CR) code point, U+000C FORM FEED (FF) code point, or pairs of U+000D CARRIAGE RETURN (CR) followed by U+000A LINE FEED (LF) by a single U+000A LINE FEED (LF) code point.
6     * Replace any U+0000 NULL code point with U+FFFD REPLACEMENT CHARACTER.
7 */
8 char[] preProcessCSS(char[] src) {
9     char[] res;
10     res.assumeSafeAppend();
11     int p = 0;
12     bool last0D = false;
13     foreach(ch; src) {
14         if (ch == 0) {
15             // append U+FFFD 1110xxxx 10xxxxxx 10xxxxxx == EF BF BD
16             res ~= 0xEF;
17             res ~= 0xBF;
18             res ~= 0xBD;
19         } else if (ch == 0x0D || ch == 0x0C) {
20             res ~= 0x0A;
21         } else if (ch == 0x0A) {
22             if (!last0D)
23                 res ~= 0x0A;
24         } else {
25             res ~= ch;
26         }
27         last0D = (ch == 0x0D);
28     }
29     return res;
30 }
31 
32 struct CSSImportRule {
33     /// start position - byte offset of @import
34     size_t startPos;
35     /// end position - byte offset of next char after closing ';'
36     size_t endPos;
37     /// url of CSS to import
38     string url;
39     /// content of downloaded URL to apply in place of rule
40     string content;
41 }
42 
43 enum CSSTokenType : ubyte {
44     eof, // end of file
45     delim, // delimiter (may be unknown token or error)
46     comment, /* some comment */
47     //newline, // any of \n \r\n \r \f
48     whitespace, // space, \t, newline
49     ident, // identifier
50     url, // url()
51     badUrl, // url() which is bad
52     func, // function(
53     str, // string '' or ""
54     badStr, // string '' or "" ended with newline character
55     hashToken, // #
56     prefixMatch, // ^=
57     suffixMatch, // $=
58     substringMatch, // *=
59     includeMatch, // ~=
60     dashMatch, // |=
61     column, // ||
62     parentOpen, // (
63     parentClose, // )
64     squareOpen, // [
65     squareClose, // ]
66     curlyOpen, // {
67     curlyClose, // }
68     comma, // ,
69     colon, // :
70     semicolon, // ;
71     number, // +12345.324e-3
72     dimension, // 1.23px  -- number with dimension
73     cdo, // <!--
74     cdc, // -->
75     atKeyword, // @someKeyword -- tokenText will contain keyword w/o @ prefix
76     unicodeRange, // U+XXX-XXX
77 }
78 
79 struct CSSToken {
80     CSSTokenType type;
81     string text;
82     string dimensionUnit;
83     union {
84         struct {
85             long intValue = 0; /// for number and dimension
86             double doubleValue = 0; /// for number and dimension
87             bool typeFlagInteger; /// for number and dimension - true if number is integer, false if double
88         }
89         struct {
90             uint unicodeRangeStart; /// for unicodeRange (initialized to 0 via intValue=0)
91             uint unicodeRangeEnd; /// for unicodeRange (initialized to 0 via intValue=0)
92         }
93         bool typeFlagId; // true if identifier is valid ID
94     }
95 }
96 
97 int decodeHexDigit(char ch) {
98     if (ch >= 'a' && ch <= 'f')
99         return (ch - 'a') + 10;
100     if (ch >= 'A' && ch <= 'F')
101         return (ch - 'A') + 10;
102     if (ch >= '0' && ch <= '9')
103         return (ch - '0');
104     return -1;
105 }
106 
107 bool isCSSWhiteSpaceChar(char ch) {
108     return ch == ' ' || ch == '\t' || ch == 0x0C || ch == 0x0D || ch == 0x0A;
109 }
110 
111 // returns true if code point is letter, underscore or non-ascii
112 bool isCSSNameStart(char ch) {
113     return ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch & 0x80) > 0 || ch == '_');
114 }
115 
116 bool isCSSNonPrintable(char ch) {
117     if (ch >= 0 && ch <= 8)
118         return true;
119     if (ch == 0x0B || ch == 0x7F)
120         return true;
121     if (ch >= 0x0E && ch <= 0x1F)
122         return true;
123     return false;
124 }
125 // This section describes how to check if two code points are a valid escape
126 bool isCSSValidEscSequence(char ch, char ch2) {
127     //If the first code point is not U+005D REVERSE SOLIDUS (\), return false.
128     if (ch != '\\')
129         return false;
130     if (ch2 == '\r' || ch2 == '\n')
131         return false;
132     return true;
133 }
134 
135 struct CSSTokenizer {
136     /// CSS source code (utf-8)
137     char[] src;
138     /// current token type
139     CSSTokenType tokenType;
140     /// current token start byte offset
141     size_t tokenStart;
142     /// current token end byte offset
143     size_t tokenEnd;
144     char[] tokenText;
145     char[] dimensionUnit;
146     bool tokenTypeFlagId; // true if identifier is valid ID
147     bool tokenTypeInteger; // for number and dimension - true if number is integer, false if double
148     long tokenIntValue; // for number and dimension
149     double tokenDoubleValue; // for number and dimension
150     uint unicodeRangeStart = 0; // for unicodeRange
151     uint unicodeRangeEnd = 0; // for unicodeRange
152     void start(string _src) {
153         src = _src.dup;
154         tokenStart = tokenEnd = 0;
155         tokenText.length = 1000;
156         tokenText.assumeSafeAppend;
157         dimensionUnit.length = 1000;
158         dimensionUnit.assumeSafeAppend;
159     }
160     bool eof() {
161         return tokenEnd >= src.length;
162     }
163     /**
164       Skip whitespace; return true if at least one whitespace char is skipped; move tokenEnd position
165       tokenType will be set to newline if any newline character found, otherwise - to whitespace
166     */
167     bool skipWhiteSpace() {
168         bool skipped = false;
169         tokenType = CSSTokenType.whitespace;
170         for (;;) {
171             if (tokenEnd >= src.length) {
172                 return false;
173             }
174             char ch = src.ptr[tokenEnd];
175             if (ch == '\r' || ch == '\n' || ch == 0x0C) {
176                 tokenEnd++;
177                 //tokenType = CSSTokenType.newline;
178                 skipped = true;
179             } if (ch == ' ' || ch == '\t') {
180                 tokenEnd++;
181                 skipped = true;
182             } else if (ch == 0xEF && tokenEnd  + 2 < src.length && src.ptr[tokenEnd + 1] == 0xBF && src.ptr[tokenEnd + 2] == 0xBD) {
183                 // U+FFFD 1110xxxx 10xxxxxx 10xxxxxx == EF BF BD
184                 tokenEnd++;
185                 skipped = true;
186             } else {
187                 return skipped;
188             }
189         }
190     }
191 
192     private dchar parseEscape(ref size_t p) {
193         size_t pos = p + 1;
194         if (pos >= src.length)
195             return cast(dchar)0xFFFFFFFF; // out of bounds
196         char ch = src.ptr[pos];
197         pos++;
198         if (ch == '\r' || ch == '\n' || ch == 0x0C)
199             return cast(dchar)0xFFFFFFFF; // unexpected newline: invalid esc sequence
200         int hex = decodeHexDigit(ch);
201         if (hex >= 0) {
202             dchar res = hex;
203             int count = 1;
204             while (count < 6) {
205                 if (pos >= src.length)
206                     break;
207                 ch = src.ptr[pos];
208                 hex = decodeHexDigit(ch);
209                 if (hex < 0)
210                     break;
211                 res = (res << 4) | hex;
212                 pos++;
213                 count++;
214             }
215             if (isCSSWhiteSpaceChar(ch))
216                 pos++;
217             p = pos;
218             return res;
219         } else {
220             // not a hex: one character is escaped
221             p = pos;
222             return ch;
223         }
224     }
225     private void appendEscapedIdentChar(dchar ch) {
226         if (ch < 0x80) {
227             // put as is
228             tokenText ~= cast(char)ch;
229         } else {
230             // UTF-8 encode
231             import std.utf : encode, isValidDchar;
232             char[4] buf;
233             size_t chars = isValidDchar(ch) ? encode(buf, ch) : 0;
234             if (chars)
235                 tokenText ~= buf[0 .. chars];
236             else
237                 tokenText ~= '?'; // replacement for invalid character
238         }
239     }
240 
241     /** Consume identifier at current position, append it to tokenText */
242     bool consumeIdent(ref char[] tokenText) {
243         size_t p = tokenEnd;
244         char ch = src.ptr[p];
245         bool hasHyphen = false;
246         if (ch == '-') {
247             p++;
248             if (p >= src.length)
249                 return false; // eof
250             hasHyphen = true;
251             ch = src.ptr[p];
252         }
253         if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch >= 0x80) {
254             if (hasHyphen)
255                 tokenText ~= '-';
256             tokenText ~= ch;
257             p++;
258         } else if (ch == '\\') {
259             dchar esc = parseEscape(p);
260             if (esc == 0xFFFFFFFF)
261                 return false; // invalid esc
262             // encode to UTF-8
263             appendEscapedIdentChar(esc);
264         } else {
265             return false;
266         }
267         for (;;) {
268             if (p >= src.length)
269                 break;
270             ch = src.ptr[p];
271             if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')  || (ch >= '0' && ch <= '9') || ch == '_'  || ch == '-' || ch >= 0x80) {
272                 tokenText ~= ch;
273                 p++;
274             } else if (ch == '\\') {
275                 dchar esc = parseEscape(p);
276                 if (esc == 0xFFFFFFFF)
277                     break; // invalid esc
278                 // encode to UTF-8
279                 appendEscapedIdentChar(esc);
280             } else {
281                 break;
282             }
283         }
284         tokenEnd = p;
285         return true;
286     }
287 
288     /**
289       Parse identifier.
290       Returns true if identifier is parsed. tokenText will contain identifier text.
291     */
292     bool parseIdent() {
293         if (!isIdentStart(tokenEnd))
294             return false;
295         if (consumeIdent(tokenText)) {
296             tokenType = tokenType.ident;
297             return true;
298         }
299         return false;
300     }
301 
302     /** returns true if current tokenEnd position is identifier start */
303     bool isIdentStart(size_t p) {
304         if (p >= src.length)
305             return false;
306         char ch = src.ptr[p];
307         if (isCSSNameStart(ch))
308             return true;
309         if (ch == '-') {
310             //If the second code point is a name-start code point or the second and third code points are a valid escape, return true. Otherwise, return false.
311             p++;
312             if (p >= src.length)
313                 return false;
314             ch = src.ptr[p];
315             if (isCSSNameStart(ch))
316                 return true;
317         }
318         if (ch == '\\') {
319             p++;
320             if (p >= src.length)
321                 return false;
322             char ch2 = src.ptr[p];
323             return isCSSValidEscSequence(ch, ch2);
324         }
325         return false;
326     }
327 
328     /**
329     Parse identifier.
330     Returns true if identifier is parsed. tokenText will contain identifier text.
331     */
332     bool parseNumber() {
333         tokenTypeInteger = true;
334         tokenIntValue = 0;
335         tokenDoubleValue = 0;
336         size_t p = tokenEnd;
337         char ch = src.ptr[p];
338         int numberSign = 1;
339         int exponentSign = 1;
340         bool hasPoint = false;
341         ulong intValue = 0;
342         ulong afterPointValue = 0;
343         ulong exponentValue = 0;
344         int beforePointDigits = 0;
345         int afterPointDigits = 0;
346         int exponentDigits = 0;
347         if (ch == '+' || ch == '-') {
348             if (ch == '-')
349                 numberSign = -1;
350             tokenText ~= ch;
351             p++;
352             if (p >= src.length)
353                 return false; // eof
354             ch = src.ptr[p];
355         }
356         // append digits before point
357         while (ch >= '0' && ch <= '9') {
358             tokenText ~= ch;
359             intValue = intValue * 10 + (ch - '0');
360             beforePointDigits++;
361             p++;
362             if (p >= src.length) {
363                 ch = 0;
364                 break;
365             }
366             ch = src.ptr[p];
367         }
368         // check for point
369         if (ch == '.') {
370             hasPoint = true;
371             tokenText ~= ch;
372             p++;
373             if (p >= src.length)
374                 return false; // eof
375             ch = src.ptr[p];
376         }
377         // append digits after point
378         while (ch >= '0' && ch <= '9') {
379             tokenText ~= ch;
380             afterPointValue = afterPointValue * 10 + (ch - '0');
381             afterPointDigits++;
382             p++;
383             if (p >= src.length) {
384                 ch = 0;
385                 break;
386             }
387             ch = src.ptr[p];
388         }
389         if (!beforePointDigits && !afterPointDigits) {
390             if (tokenText.length)
391                 tokenText.length = 0;
392             return false; // not a number
393         }
394         if (ch == 'e' || ch == 'E') {
395             char nextCh = p + 1 < src.length ? src.ptr[p + 1] : 0;
396             char nextCh2 = p + 2 < src.length ? src.ptr[p + 2] : 0;
397             int skip = 1;
398             if (nextCh == '+' || nextCh == '-') {
399                 if (nextCh == '-')
400                     exponentSign = -1;
401                 skip = 2;
402                 nextCh = nextCh2;
403             }
404             if (nextCh >= '0' && nextCh <= '9') {
405                 tokenText ~= src.ptr[p .. p + skip];
406                 p += skip;
407                 ch = nextCh;
408                 // append exponent digits
409                 while (ch >= '0' && ch <= '9') {
410                     tokenText ~= ch;
411                     exponentValue = exponentValue * 10 + (ch - '0');
412                     exponentDigits++;
413                     p++;
414                     if (p >= src.length) {
415                         ch = 0;
416                         break;
417                     }
418                     ch = src.ptr[p];
419                 }
420             }
421         }
422         tokenType = CSSTokenType.number;
423         tokenEnd = p;
424         if (exponentDigits || afterPointDigits) {
425             // parsed floating point
426             tokenDoubleValue = cast(long)intValue;
427             if (afterPointDigits) {
428                 long divider = 1;
429                 for (int i = 0; i < afterPointDigits; i++)
430                     divider *= 10;
431                 tokenDoubleValue += afterPointValue / cast(double)divider;
432             }
433             if (numberSign < 0)
434                 tokenDoubleValue = -tokenDoubleValue;
435             if (exponentDigits) {
436                 import std.math : pow;
437                 double exponent = (cast(long)exponentValue * exponentSign);
438                 tokenDoubleValue = tokenDoubleValue * pow(10, exponent);
439             }
440             tokenIntValue = cast(long)tokenDoubleValue;
441         } else {
442             // parsed integer
443             tokenIntValue = cast(long)intValue;
444             if (numberSign < 0)
445                 tokenIntValue = -tokenIntValue;
446             tokenDoubleValue = tokenIntValue;
447         }
448         dimensionUnit.length = 0;
449         if (isIdentStart(tokenEnd)) {
450             tokenType = CSSTokenType.dimension;
451             consumeIdent(dimensionUnit);
452         }
453         return true;
454     }
455 
456     bool parseString(char quotationChar) {
457         tokenType = CSSTokenType.str;
458         // skip first delimiter ' or "
459         size_t p = tokenEnd + 1;
460         for (;;) {
461             if (p >= src.length) {
462                 // unexpected end of file
463                 tokenEnd = p;
464                 return true;
465             }
466             char ch = src.ptr[p];
467             if (ch == '\r' || ch == '\n') {
468                 tokenType = CSSTokenType.badStr;
469                 tokenEnd = p - 1;
470                 return true;
471             } else if (ch == quotationChar) {
472                 // end of string
473                 tokenEnd = p + 1;
474                 return true;
475             } else if (ch == '\\') {
476                 if (p + 1 >= src.length) {
477                     // unexpected end of file
478                     tokenEnd = p;
479                     return true;
480                 }
481                 ch = src.ptr[p + 1];
482                 if (ch == '\r' || ch == '\n') {
483                     // \ NEWLINE
484                     //tokenText ~= 0x0A;
485                     p++;
486                 } else {
487                     dchar esc = parseEscape(p);
488                     if (esc == 0xFFFFFFFF) {
489                         esc = '?'; // replace invalid code point
490                         p++;
491                     }
492                     // encode to UTF-8
493                     appendEscapedIdentChar(esc);
494                 }
495             } else {
496                 // normal character
497                 tokenText ~= ch;
498                 p++;
499             }
500         }
501     }
502     CSSTokenType emitDelimToken() {
503         import std.utf : stride, UTFException;
504         try {
505             uint len = stride(src[tokenStart .. $]);
506             tokenEnd = tokenStart + len;
507         } catch (UTFException e) {
508             tokenEnd = tokenStart + 1;
509         }
510         tokenText ~= src[tokenStart .. tokenEnd];
511         tokenType = CSSTokenType.delim;
512         return tokenType;
513     }
514     // #token
515     CSSTokenType parseHashToken() {
516         tokenTypeFlagId = false;
517         tokenEnd++;
518         // set tokenTypeFlagId flag
519         if (parseIdent()) {
520             tokenType = CSSTokenType.hashToken;
521             if (tokenText[0] < '0' || tokenText[0] > '9')
522                 tokenTypeFlagId = true; // is valid ID
523             return tokenType;
524         }
525         // invalid ident
526         return emitDelimToken();
527     }
528     /// current chars are /*
529     CSSTokenType parseComment() {
530         size_t p = tokenEnd + 2; // skip /*
531         while (p < src.length) {
532             char ch = src.ptr[p];
533             char ch2 = p + 1 < src.length ? src.ptr[p + 1] : 0;
534             if (ch == '*' && ch2 == '/') {
535                 p += 2;
536                 break;
537             }
538             p++;
539         }
540         tokenEnd = p;
541         tokenType = CSSTokenType.comment;
542         return tokenType;
543     }
544     /// current chars are U+ or u+ followed by hex digit or ?
545     CSSTokenType parseUnicodeRangeToken() {
546         unicodeRangeStart = 0;
547         unicodeRangeEnd = 0;
548         size_t p = tokenEnd + 2; // skip U+
549         // now we have hex digit or ?
550         int hexCount = 0;
551         uint hexNumber = 0;
552         int questionCount = 0;
553         // consume hex digits
554         while (p < src.length) {
555             char ch = src.ptr[p];
556             int digit = decodeHexDigit(ch);
557             if (digit < 0)
558                 break;
559             hexCount++;
560             hexNumber = (hexNumber << 4) | digit;
561             p++;
562             if (hexCount >= 6)
563                 break;
564         }
565         // consume question marks
566         while (p < src.length && questionCount + hexCount < 6) {
567             char ch = src.ptr[p];
568             if (ch != '?')
569                 break;
570             questionCount++;
571             p++;
572         }
573         if (questionCount) {
574             int shift = 4 * questionCount;
575             unicodeRangeStart = hexNumber << shift;
576             unicodeRangeEnd = unicodeRangeStart + ((1 << shift) - 1);
577         } else {
578             unicodeRangeStart = hexNumber;
579             char ch = p < src.length ? src.ptr[p] : 0;
580             char ch2 = p + 1 < src.length ? src.ptr[p + 1] : 0;
581             int digit = decodeHexDigit(ch2);
582             if (ch == '-' && digit >= 0) {
583                 p += 2; // skip - and first digit
584                 hexCount = 1;
585                 hexNumber = digit;
586                 while (p < src.length) {
587                     ch = src.ptr[p];
588                     digit = decodeHexDigit(ch);
589                     if (digit < 0)
590                         break;
591                     hexCount++;
592                     hexNumber = (hexNumber << 4) | digit;
593                     p++;
594                     if (hexCount >= 6)
595                         break;
596                 }
597                 unicodeRangeEnd = hexNumber;
598             } else {
599                 unicodeRangeEnd = unicodeRangeStart;
600             }
601         }
602         tokenEnd = p;
603         tokenType = CSSTokenType.unicodeRange;
604         return tokenType;
605     }
606     /// emit single char token like () {} [] : ;
607     CSSTokenType emitSingleCharToken(CSSTokenType type) {
608         tokenType = type;
609         tokenEnd = tokenStart + 1;
610         tokenText ~= src[tokenStart];
611         return type;
612     }
613     /// emit double char token like $= *=
614     CSSTokenType emitDoubleCharToken(CSSTokenType type) {
615         tokenType = type;
616         tokenEnd = tokenStart + 2;
617         tokenText ~= src[tokenStart .. tokenStart + 2];
618         return type;
619     }
620     void consumeBadUrl() {
621         for (;;) {
622             char ch = tokenEnd < src.length ? src.ptr[tokenEnd] : 0;
623             char ch2 = tokenEnd + 1 < src.length ? src.ptr[tokenEnd + 1] : 0;
624             if (ch == ')' || ch == 0) {
625                 if (ch == ')')
626                     tokenEnd++;
627                 break;
628             }
629             if (isCSSValidEscSequence(ch, ch2)) {
630                 parseEscape(tokenEnd);
631             }
632             tokenEnd++;
633         }
634         tokenType = CSSTokenType.badUrl;
635     }
636     // Current position is after url(
637     void parseUrlToken() {
638         tokenText.length = 0;
639         skipWhiteSpace();
640         if (tokenEnd >= src.length)
641             return;
642         char ch = src.ptr[tokenEnd];
643         if (ch == '\'' || ch == '\"') {
644             if (parseString(ch)) {
645                 skipWhiteSpace();
646                 ch = tokenEnd < src.length ? src.ptr[tokenEnd] : 0;
647                 if (ch == ')' || ch == 0) {
648                     // valid URL token
649                     if (ch == ')')
650                         tokenEnd++;
651                     tokenType = CSSTokenType.url;
652                     return;
653                 }
654             }
655             // bad url
656             consumeBadUrl();
657             return;
658         }
659         // not quoted
660         for (;;) {
661             if (skipWhiteSpace()) {
662                 ch = tokenEnd < src.length ? src.ptr[tokenEnd] : 0;
663                 if (ch == ')' || ch == 0) {
664                     if (ch == ')')
665                         tokenEnd++;
666                     tokenType = CSSTokenType.url;
667                     return;
668                 }
669                 consumeBadUrl();
670                 return;
671             }
672             ch = tokenEnd < src.length ? src.ptr[tokenEnd] : 0;
673             char ch2 = tokenEnd + 1 < src.length ? src.ptr[tokenEnd + 1] : 0;
674             if (ch == ')' || ch == 0) {
675                 if (ch == ')')
676                     tokenEnd++;
677                 tokenType = CSSTokenType.url;
678                 return;
679             }
680             if (ch == '(' || ch == '\'' || ch == '\"' || isCSSNonPrintable(ch)) {
681                 consumeBadUrl();
682                 return;
683             }
684             if (ch == '\\') {
685                 if (isCSSValidEscSequence(ch, ch2)) {
686                     dchar esc = parseEscape(tokenEnd);
687                     appendEscapedIdentChar(ch);
688                 } else {
689                     consumeBadUrl();
690                     return;
691                 }
692             }
693             tokenText ~= ch;
694             tokenEnd++;
695         }
696     }
697     CSSTokenType next() {
698         // move beginning of token
699         tokenStart = tokenEnd;
700         tokenText.length = 0;
701         // check for whitespace
702         if (skipWhiteSpace())
703             return tokenType; // whitespace or newline token
704         // check for eof
705         if (tokenEnd >= src.length)
706             return CSSTokenType.eof;
707         char ch = src.ptr[tokenEnd];
708         char nextCh = tokenEnd + 1 < src.length ? src.ptr[tokenEnd + 1] : 0;
709         if (ch == '\"' || ch == '\'') {
710             parseString(ch);
711             return tokenType;
712         }
713         if (ch == '#') {
714             return parseHashToken();
715         }
716         if (ch == '$') {
717             if (nextCh == '=') {
718                 return emitDoubleCharToken(CSSTokenType.suffixMatch);
719             } else {
720                 return emitDelimToken();
721             }
722         }
723         if (ch == '^') {
724             if (nextCh == '=') {
725                 return emitDoubleCharToken(CSSTokenType.prefixMatch);
726             } else {
727                 return emitDelimToken();
728             }
729         }
730         if (ch == '(')
731             return emitSingleCharToken(CSSTokenType.parentOpen);
732         if (ch == ')')
733             return emitSingleCharToken(CSSTokenType.parentClose);
734         if (ch == '[')
735             return emitSingleCharToken(CSSTokenType.squareOpen);
736         if (ch == ']')
737             return emitSingleCharToken(CSSTokenType.squareClose);
738         if (ch == '{')
739             return emitSingleCharToken(CSSTokenType.curlyOpen);
740         if (ch == '}')
741             return emitSingleCharToken(CSSTokenType.curlyClose);
742         if (ch == ',')
743             return emitSingleCharToken(CSSTokenType.comma);
744         if (ch == ':')
745             return emitSingleCharToken(CSSTokenType.colon);
746         if (ch == ';')
747             return emitSingleCharToken(CSSTokenType.semicolon);
748         if (ch == '*') {
749             if (nextCh == '=') {
750                 return emitDoubleCharToken(CSSTokenType.substringMatch);
751             } else {
752                 return emitDelimToken();
753             }
754         }
755         if (ch == '~') {
756             if (nextCh == '=') {
757                 return emitDoubleCharToken(CSSTokenType.includeMatch);
758             } else {
759                 return emitDelimToken();
760             }
761         }
762         if (ch == '|') {
763             if (nextCh == '=') {
764                 return emitDoubleCharToken(CSSTokenType.dashMatch);
765             } else if (nextCh == '|') {
766                 return emitDoubleCharToken(CSSTokenType.column);
767             } else {
768                 return emitDelimToken();
769             }
770         }
771         if (ch == '/') {
772             if (nextCh == '*') {
773                 return parseComment();
774             } else {
775                 return emitDelimToken();
776             }
777         }
778         char nextCh2 = tokenEnd + 2 < src.length ? src.ptr[tokenEnd + 2] : 0;
779         if (ch == 'u' || ch == 'U') {
780             if (nextCh == '+' && (decodeHexDigit(nextCh2) >= 0 || nextCh2 == '?')) {
781                 return parseUnicodeRangeToken();
782             }
783         }
784         if (parseNumber())
785             return tokenType;
786         if (parseIdent()) {
787             ch = tokenEnd < src.length ? src.ptr[tokenEnd] : 0;
788             if (ch == '(') {
789                 tokenEnd++;
790                 import std.uni : icmp;
791                 if (tokenText.length == 3 && icmp(tokenText, "url") == 0) {
792                     // parse URL function
793                     parseUrlToken();
794                 } else {
795                     tokenType = CSSTokenType.func;
796                 }
797             }
798             return tokenType;
799         }
800         if (ch == '-') {
801             if (nextCh == '-' && nextCh2 == '>') {
802                 tokenEnd = tokenStart + 3;
803                 tokenType = CSSTokenType.cdc;
804                 tokenText ~= src[tokenStart .. tokenEnd];
805                 return tokenType;
806             }
807             return emitDelimToken();
808         }
809         if (ch == '<') {
810             char nextCh3 = tokenEnd + 3 < src.length ? src.ptr[tokenEnd + 3] : 0;
811             if (nextCh == '!' && nextCh2 == '-' && nextCh3 == '-') {
812                 tokenEnd = tokenStart + 4;
813                 tokenType = CSSTokenType.cdo;
814                 tokenText ~= src[tokenStart .. tokenEnd];
815                 return tokenType;
816             }
817             return emitDelimToken();
818         }
819         if (ch == '@') {
820             if (isIdentStart(tokenEnd + 1)) {
821                 tokenEnd++;
822                 parseIdent();
823                 tokenType = CSSTokenType.atKeyword;
824                 return tokenType;
825             }
826             return emitDelimToken();
827         }
828         return emitDelimToken();
829     }
830     /// same as next() but returns filled CSSToken struct
831     CSSToken nextToken() {
832         CSSToken res;
833         res.type = next();
834         if (res.type == CSSTokenType.str || res.type == CSSTokenType.ident || res.type == CSSTokenType.atKeyword || res.type == CSSTokenType.url || res.type == CSSTokenType.func) {
835             if (tokenText.length)
836                 res.text = tokenText.dup;
837         }
838         if (res.type == CSSTokenType.dimension && dimensionUnit.length)
839             res.dimensionUnit = dimensionUnit.dup;
840         if (res.type == CSSTokenType.dimension || res.type == CSSTokenType.number) {
841             res.doubleValue = tokenDoubleValue;
842             res.intValue = tokenIntValue;
843             res.typeFlagInteger = tokenTypeInteger;
844         } else if (res.type == CSSTokenType.ident) {
845             res.typeFlagId = tokenTypeFlagId;
846         } else if (res.type == CSSTokenType.unicodeRange) {
847             res.unicodeRangeStart = unicodeRangeStart;
848             res.unicodeRangeEnd = unicodeRangeEnd;
849         }
850         return res;
851     }
852 }
853 
854 unittest {
855     CSSTokenizer tokenizer;
856     tokenizer.start("ident-1{ }\n#id\n'blabla' \"bla bla 2\" -ident2*=12345 -.234e+5 "
857                     ~ "1.23px/* some comment */U+123?!"
858                     ~"url(   'text.css'  )url(bad url)functionName()url( bla )"
859                     ~"'\\30 \\31'");
860     assert(tokenizer.next() == CSSTokenType.ident);
861     assert(tokenizer.tokenText == "ident-1");
862     assert(tokenizer.next() == CSSTokenType.curlyOpen);
863     assert(tokenizer.next() == CSSTokenType.whitespace);
864     assert(tokenizer.next() == CSSTokenType.curlyClose);
865     assert(tokenizer.next() == CSSTokenType.whitespace); //newline
866     assert(tokenizer.next() == CSSTokenType.hashToken);
867     assert(tokenizer.tokenText == "id");
868     assert(tokenizer.tokenTypeFlagId == true);
869     assert(tokenizer.next() == CSSTokenType.whitespace); //newline
870     assert(tokenizer.next() == CSSTokenType.str);
871     assert(tokenizer.tokenText == "blabla");
872     assert(tokenizer.next() == CSSTokenType.whitespace);
873     assert(tokenizer.next() == CSSTokenType.str);
874     assert(tokenizer.tokenText == "bla bla 2");
875     assert(tokenizer.next() == CSSTokenType.whitespace);
876     assert(tokenizer.next() == CSSTokenType.ident);
877     assert(tokenizer.tokenText == "-ident2");
878     assert(tokenizer.next() == CSSTokenType.substringMatch);
879     assert(tokenizer.next() == CSSTokenType.number);
880     assert(tokenizer.tokenText == "12345");
881     assert(tokenizer.tokenIntValue == 12345);
882     assert(tokenizer.next() == CSSTokenType.whitespace);
883     assert(tokenizer.next() == CSSTokenType.number);
884     assert(tokenizer.tokenText == "-.234e+5");
885     assert(tokenizer.tokenIntValue == -23400);
886     assert(tokenizer.tokenDoubleValue == -.234e+5);
887     assert(tokenizer.next() == CSSTokenType.whitespace);
888     // next line
889     assert(tokenizer.next() == CSSTokenType.dimension);
890     assert(tokenizer.tokenText == "1.23");
891     assert(tokenizer.tokenIntValue == 1);
892     assert(tokenizer.tokenDoubleValue == 1.23);
893     assert(tokenizer.dimensionUnit == "px");
894     assert(tokenizer.next() == CSSTokenType.comment);
895     assert(tokenizer.next() == CSSTokenType.unicodeRange);
896     assert(tokenizer.unicodeRangeStart == 0x1230 && tokenizer.unicodeRangeEnd == 0x123F);
897     assert(tokenizer.next() == CSSTokenType.delim);
898     assert(tokenizer.tokenText == "!");
899     // next line
900     assert(tokenizer.next() == CSSTokenType.url);
901     assert(tokenizer.tokenText == "text.css");
902     assert(tokenizer.next() == CSSTokenType.badUrl);
903     assert(tokenizer.next() == CSSTokenType.func);
904     assert(tokenizer.tokenText == "functionName");
905     assert(tokenizer.next() == CSSTokenType.parentClose);
906     assert(tokenizer.next() == CSSTokenType.url);
907     assert(tokenizer.tokenText == "bla");
908     // next line
909     assert(tokenizer.next() == CSSTokenType.str);
910     assert(tokenizer.tokenText == "01"); //'\30 \31'
911     assert(tokenizer.next() == CSSTokenType.eof);
912 }
913 
914 
915 /**
916 Tokenizes css source, returns array of tokens (last token is EOF).
917 Source must be preprocessed utf-8 string.
918 */
919 static CSSToken[] tokenizeCSS(string src) {
920     CSSTokenizer tokenizer;
921     tokenizer.start(src);
922     CSSToken[] res;
923     res.assumeSafeAppend();
924     for(;;) {
925         res ~= tokenizer.nextToken();
926         if (res[$ - 1].type == CSSTokenType.eof)
927             break;
928     }
929     return res;
930 }
931 
932 unittest {
933     string src = "pre {123em}";
934     auto res = tokenizeCSS(src);
935     assert(res.length == 6);
936     assert(res[0].type == CSSTokenType.ident);
937     assert(res[0].text == "pre");
938     assert(res[1].type == CSSTokenType.whitespace);
939     assert(res[2].type == CSSTokenType.curlyOpen);
940     assert(res[3].type == CSSTokenType.dimension);
941     assert(res[3].typeFlagInteger == true);
942     assert(res[3].intValue == 123);
943     assert(res[3].dimensionUnit == "em");
944     assert(res[4].type == CSSTokenType.curlyClose);
945     assert(res[$ - 1].type == CSSTokenType.eof);
946 }
947 
948 // easy way to extract and apply imports w/o full document parsing
949 /**
950     Extract CSS vimport rules from source.
951 */
952 CSSImportRule[] extractCSSImportRules(string src) {
953     enum ParserState {
954         start, // before rule begin, switch to this state after ;
955         afterImport, // after @import
956         afterCharset, // after @charset
957         afterCharsetName, // after @charset
958         afterImportUrl, // after @charset
959     }
960     ParserState state = ParserState.start;
961     CSSImportRule[] res;
962     CSSTokenizer tokenizer;
963     tokenizer.start(src);
964     bool insideImportRule = false;
965     string url;
966     size_t startPos = 0;
967     size_t endPos = 0;
968     for (;;) {
969         CSSTokenType type = tokenizer.next();
970         if (type == CSSTokenType.eof)
971             break;
972         if (type == CSSTokenType.whitespace || type == CSSTokenType.comment)
973             continue; // skip whitespaces and comments
974         if (type == CSSTokenType.atKeyword) {
975             if (tokenizer.tokenText == "charset") {
976                 state = ParserState.afterCharset;
977                 continue;
978             }
979             if (tokenizer.tokenText != "import")
980                 break;
981             // import rule
982             state = ParserState.afterImport;
983             startPos = tokenizer.tokenStart;
984             continue;
985         }
986         if (type == CSSTokenType.str || type == CSSTokenType.url) {
987             if (state == ParserState.afterImport) {
988                 url = tokenizer.tokenText.dup;
989                 state = ParserState.afterImportUrl;
990                 continue;
991             }
992             if (state == ParserState.afterCharset) {
993                 state = ParserState.afterCharsetName;
994                 continue;
995             }
996             break;
997         }
998         if (type == CSSTokenType.curlyOpen)
999             break;
1000         if (type == CSSTokenType.ident && state == ParserState.start)
1001             break; // valid @imports may be only at the beginning of file
1002         if (type == CSSTokenType.semicolon) {
1003             if (state == ParserState.afterImportUrl) {
1004                 // add URL
1005                 endPos = tokenizer.tokenEnd;
1006                 CSSImportRule rule;
1007                 rule.startPos = startPos;
1008                 rule.endPos = endPos;
1009                 rule.url = url;
1010                 res ~= rule;
1011             }
1012             state = ParserState.start;
1013             continue;
1014         }
1015     }
1016     return res;
1017 }
1018 
1019 /**
1020   Replace source code import rules obtained by extractImportRules() with imported content.
1021 */
1022 string applyCSSImportRules(string src, CSSImportRule[] rules) {
1023     if (!rules.length)
1024         return src; // no rules
1025     char[] res;
1026     res.assumeSafeAppend;
1027     size_t start = 0;
1028     for (int i = 0; i < rules.length; i++) {
1029         res ~= src[start .. rules[i].startPos];
1030         res ~= rules[i].content;
1031         start = rules[i].endPos;
1032     }
1033     if (start < src.length)
1034         res ~= src[start .. $];
1035     return cast(string)res;
1036 }
1037 
1038 
1039 unittest {
1040     string src = q{
1041         @charset "utf-8";
1042         /* comment must be ignored */
1043         @import "file1.css"; /* string */
1044         @import url(file2.css); /* url */
1045         pre {}
1046         @import "ignore_me.css";
1047         p {}
1048     };
1049     auto res = extractCSSImportRules(src);
1050     assert(res.length == 2);
1051     assert(res[0].url == "file1.css");
1052     assert(res[1].url == "file2.css");
1053     res[0].content = "[file1_content]";
1054     res[1].content = "[file2_content]";
1055     string s = applyCSSImportRules(src, res);
1056     assert (s.length != src.length);
1057 }
1058 
1059 enum ASTNodeType {
1060     simpleBlock,
1061     componentValue,
1062     preservedToken,
1063     func,
1064     atRule,
1065     qualifiedRule,
1066 }
1067 
1068 class ASTNode {
1069     ASTNodeType type;
1070 }
1071 
1072 class ComponentValueNode : ASTNode {
1073     this() {
1074         type = ASTNodeType.componentValue;
1075     }
1076 }
1077 
1078 class SimpleBlockNode : ComponentValueNode {
1079     CSSTokenType blockType = CSSTokenType.curlyOpen;
1080     ComponentValueNode[] componentValues;
1081     this() {
1082         type = ASTNodeType.simpleBlock;
1083     }
1084 }
1085 
1086 class FunctionNode : ComponentValueNode {
1087     ComponentValueNode[] componentValues;
1088     this(string name) {
1089         type = ASTNodeType.func;
1090     }
1091 }
1092 
1093 class PreservedTokenNode : ComponentValueNode {
1094     CSSToken token;
1095     this(ref CSSToken token) {
1096         this.token = token;
1097         type = ASTNodeType.preservedToken;
1098     }
1099 }
1100 
1101 class QualifiedRuleNode : ASTNode {
1102     ComponentValueNode[] componentValues;
1103     SimpleBlockNode block;
1104     this() {
1105         type = ASTNodeType.qualifiedRule;
1106     }
1107 }
1108 
1109 class ATRuleNode : QualifiedRuleNode {
1110     string name;
1111     this() {
1112         type = ASTNodeType.atRule;
1113     }
1114 }
1115 
1116 
1117 class CSSParser {
1118     CSSToken[] tokens;
1119     int pos = 0;
1120     this(CSSToken[] _tokens) {
1121         tokens = _tokens;
1122     }
1123     /// peek current token
1124     @property ref CSSToken currentToken() {
1125         return tokens[pos];
1126     }
1127     /// peek next token
1128     @property ref CSSToken nextToken() {
1129         return tokens[pos + 1 < $ ? pos + 1 : pos];
1130     }
1131     /// move to next token
1132     bool next() {
1133         if (pos < tokens.length) {
1134             pos++;
1135             return true;
1136         }
1137         return false;
1138     }
1139     /// move to nearest non-whitespace token; return current token type (does not move if current token is not whitespace)
1140     CSSTokenType skipWhiteSpace() {
1141         while (currentToken.type == CSSTokenType.whitespace || currentToken.type == CSSTokenType.comment || currentToken.type == CSSTokenType.delim)
1142             next();
1143         return currentToken.type;
1144     }
1145     /// skip current token, then move to nearest non-whitespace token; return new token type
1146     @property CSSTokenType nextNonWhiteSpace() {
1147         next();
1148         return skipWhiteSpace();
1149     }
1150     SimpleBlockNode parseSimpleBlock() {
1151         auto type = skipWhiteSpace();
1152         CSSTokenType closeType;
1153         if (type == CSSTokenType.curlyOpen) {
1154             closeType = CSSTokenType.curlyClose;
1155         } else if (type == CSSTokenType.squareOpen) {
1156             closeType = CSSTokenType.squareClose;
1157         } else if (type == CSSTokenType.parentOpen) {
1158             closeType = CSSTokenType.parentClose;
1159         } else {
1160             // not a simple block
1161             return null;
1162         }
1163         SimpleBlockNode res = new SimpleBlockNode();
1164         res.blockType = type;
1165         auto t = nextNonWhiteSpace();
1166         res.componentValues = parseComponentValueList(closeType);
1167         t = skipWhiteSpace();
1168         if (t == closeType)
1169             nextNonWhiteSpace();
1170         return res;
1171     }
1172     FunctionNode parseFunctionBlock() {
1173         auto type = skipWhiteSpace();
1174         if (type != CSSTokenType.func)
1175             return null;
1176         FunctionNode res = new FunctionNode(currentToken.text);
1177         auto t = nextNonWhiteSpace();
1178         res.componentValues = parseComponentValueList(CSSTokenType.parentClose);
1179         t = skipWhiteSpace();
1180         if (t == CSSTokenType.parentClose)
1181             nextNonWhiteSpace();
1182         return res;
1183     }
1184     ComponentValueNode[] parseComponentValueList(CSSTokenType endToken1 = CSSTokenType.eof, CSSTokenType endToken2 = CSSTokenType.eof) {
1185         ComponentValueNode[] res;
1186         for (;;) {
1187             auto type = skipWhiteSpace();
1188             if (type == CSSTokenType.eof)
1189                 return res;
1190             if (type == endToken1 || type == endToken2)
1191                 return res;
1192             if (type == CSSTokenType.squareOpen || type == CSSTokenType.parentOpen || type == CSSTokenType.curlyOpen) {
1193                 res ~= parseSimpleBlock();
1194             } else if (type == CSSTokenType.func) {
1195                 res ~= parseFunctionBlock();
1196             } else {
1197                 res ~= new PreservedTokenNode(currentToken);
1198                 next();
1199             }
1200         }
1201     }
1202     ATRuleNode parseATRule() {
1203         auto type = skipWhiteSpace();
1204         if (type != CSSTokenType.atKeyword)
1205             return null;
1206         ATRuleNode res = new ATRuleNode();
1207         res.name = currentToken.text;
1208         type = nextNonWhiteSpace();
1209         res.componentValues = parseComponentValueList(CSSTokenType.semicolon, CSSTokenType.curlyOpen);
1210         type = skipWhiteSpace();
1211         if (type == CSSTokenType.semicolon) {
1212             next();
1213             return res;
1214         }
1215         if (type == CSSTokenType.curlyOpen) {
1216             res.block = parseSimpleBlock();
1217             return res;
1218         }
1219         if (type == CSSTokenType.eof)
1220             return res;
1221         return res;
1222     }
1223 
1224     QualifiedRuleNode parseQualifiedRule() {
1225         auto type = skipWhiteSpace();
1226         if (type == CSSTokenType.eof)
1227             return null;
1228         QualifiedRuleNode res = new QualifiedRuleNode();
1229         res.componentValues = parseComponentValueList(CSSTokenType.curlyOpen);
1230         type = skipWhiteSpace();
1231         if (type == CSSTokenType.curlyOpen) {
1232             res.block = parseSimpleBlock();
1233         }
1234         return res;
1235     }
1236 }
1237 
1238 unittest {
1239     ATRuleNode atRule = new CSSParser(tokenizeCSS("@atRuleName;")).parseATRule();
1240     assert(atRule !is null);
1241     assert(atRule.name == "atRuleName");
1242     assert(atRule.block is null);
1243 
1244     atRule = new CSSParser(tokenizeCSS("@atRuleName2 { }")).parseATRule();
1245     assert(atRule !is null);
1246     assert(atRule.name == "atRuleName2");
1247     assert(atRule.block !is null);
1248     assert(atRule.block.blockType == CSSTokenType.curlyOpen);
1249 
1250     atRule = new CSSParser(tokenizeCSS("@atRuleName3 url('bla') { 123 }")).parseATRule();
1251     assert(atRule !is null);
1252     assert(atRule.name == "atRuleName3");
1253     assert(atRule.componentValues.length == 1);
1254     assert(atRule.componentValues[0].type == ASTNodeType.preservedToken);
1255     assert(atRule.block !is null);
1256     assert(atRule.block.blockType == CSSTokenType.curlyOpen);
1257     assert(atRule.block.componentValues.length == 1);
1258 
1259 
1260     atRule = new CSSParser(tokenizeCSS("@atRuleName4 \"value\" { funcName(123) }")).parseATRule();
1261     assert(atRule !is null);
1262     assert(atRule.name == "atRuleName4");
1263     assert(atRule.componentValues.length == 1);
1264     assert(atRule.componentValues[0].type == ASTNodeType.preservedToken);
1265     assert(atRule.block !is null);
1266     assert(atRule.block.blockType == CSSTokenType.curlyOpen);
1267     assert(atRule.block.componentValues.length == 1);
1268     assert(atRule.block.componentValues[0].type == ASTNodeType.func);
1269 }
1270 
1271 unittest {
1272     QualifiedRuleNode qualifiedRule = new CSSParser(tokenizeCSS(" pre { display: none } ")).parseQualifiedRule();
1273     assert(qualifiedRule !is null);
1274     assert(qualifiedRule.componentValues.length == 1);
1275     assert(qualifiedRule.block !is null);
1276     assert(qualifiedRule.block.componentValues.length == 3);
1277 }