1 /**
2 * Implement HTML color parsing. dplug:canvas internals.
3 *
4 * Copyright: Copyright Guillaume Piolat 2020.
5 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module dplug.canvas.htmlcolors;
8 
9 import std.math: PI, floor;
10 public import dplug.graphics.color;
11 import dplug.core.string;
12 
13 /// Parses a HTML color string, and gives back a RGBA color.
14 ///
15 /// Params:
16 ///     htmlColorString = A CSS string describing a color.
17 ///
18 /// Returns:
19 ///     A 32-bit RGBA color, with each component between 0 and 255.
20 ///
21 /// See_also: https://www.w3.org/TR/css-color-4/
22 ///
23 ///
24 /// Example:
25 /// ---
26 /// import dplug.canvas.htmlcolors;
27 /// parseHTMLColor("black", color, error);                      // all HTML named colors
28 /// parseHTMLColor("#fe85dc", color, error);                    // hex colors including alpha versions
29 /// parseHTMLColor("rgba(64, 255, 128, 0.24)", color, error);   // alpha
30 /// parseHTMLColor("rgb(9e-1, 50%, 128)", color, error);        // percentage, floating-point
31 /// parseHTMLColor("hsl(120deg, 25%, 75%)", color, error);      // hsv colors
32 /// parseHTMLColor("gray(0.5)", color, error);                  // gray colors
33 /// parseHTMLColor(" rgb ( 245 , 112 , 74 )  ", color, error);  // strips whitespace
34 /// ---
35 ///
36 bool parseHTMLColor(const(char)[] htmlColorString, out RGBA color, out string error) nothrow @nogc @safe
37 {
38     error = null; // indicate success
39 
40 
41     const(char)[] s = htmlColorString;   
42     int index = 0;    
43 
44     char peek() nothrow @nogc @safe
45     {
46         if (index >= htmlColorString.length)
47             return '\0';
48         else
49             return s[index];
50     }
51 
52     void next() nothrow @nogc @safe
53     {
54         index++;
55     }
56 
57     bool parseChar(char ch) nothrow @nogc @safe
58     {
59         if (peek() == ch)
60         {
61             next;
62             return true;
63         }
64         return false;
65     }
66 
67     bool expectChar(char ch) nothrow @nogc @safe // senantic difference, "expect" returning false is an input error
68     {
69         if (!parseChar(ch))
70             return false;
71         return true;
72     }
73 
74     bool parseString(string s) nothrow @nogc @safe
75     {
76         int save = index;
77 
78         for (int i = 0; i < s.length; ++i)
79         {
80             if (!parseChar(s[i]))
81             {
82                 index = save;
83                 return false;
84             }
85         }
86         return true;
87     }
88 
89     bool isWhite(char ch) nothrow @nogc @safe
90     {
91         return ch == ' ';
92     }
93 
94     bool isDigit(char ch) nothrow @nogc @safe
95     {
96         return ch >= '0' && ch <= '9';
97     }
98 
99     bool expectDigit(out char digit) nothrow @nogc @safe
100     {
101         char ch = peek();
102         if (isDigit(ch))
103         {            
104             next;
105             digit = ch;
106             return true;
107         }
108         else
109             return false;
110     }
111 
112     bool parseHexDigit(out int digit) nothrow @nogc @safe
113     {
114         char ch = peek();
115         if (isDigit(ch))
116         {
117             next;
118             digit = ch - '0';
119             return true;
120         }
121         else if (ch >= 'a' && ch <= 'f')
122         {
123             next;
124             digit = 10 + (ch - 'a');
125             return true;
126         }
127         else if (ch >= 'A' && ch <= 'F')
128         {
129             next;
130             digit = 10 + (ch - 'A');
131             return true;
132         }
133         else
134             return false;
135     }
136 
137     void skipWhiteSpace() nothrow @nogc @safe
138     {       
139         while (isWhite(peek()))
140             next;
141     }
142 
143     bool expectPunct(char ch) nothrow @nogc @safe
144     {
145         skipWhiteSpace();
146         if (!expectChar(ch))
147             return false;
148         skipWhiteSpace();
149         return true;
150     }
151 
152     ubyte clamp0to255(int a) nothrow @nogc @safe
153     {
154         if (a < 0) return 0;
155         if (a > 255) return 255;
156         return cast(ubyte)a;
157     }
158 
159     // See: https://www.w3.org/TR/css-syntax/#consume-a-number
160     bool parseNumber(double* number, out string error) nothrow @nogc @trusted
161     {
162         char[32] repr;
163         int repr_len = 0;
164 
165         if (parseChar('+'))
166         {}
167         else if (parseChar('-'))
168         {
169             if (repr_len >= 31) return false;
170             repr[repr_len++] = '-';
171         }
172         while(isDigit(peek()))
173         {
174             if (repr_len >= 31) return false;
175             repr[repr_len++] = peek();
176             next;
177         }
178         if (peek() == '.')
179         {
180             if (repr_len >= 31) return false;
181             repr[repr_len++] = '.';
182             next;
183             char digit;
184             bool parsedDigit = expectDigit(digit);
185             if (!parsedDigit)
186                 return false;
187 
188             if (repr_len >= 31) return false;
189             repr[repr_len++] = digit;
190 
191             while(isDigit(peek()))
192             {
193                 if (repr_len >= 31) return false;
194                 repr[repr_len++] = peek();
195                 next;
196             }
197         }
198         if (peek() == 'e' || peek() == 'E')
199         {
200             if (repr_len >= 31) return false;
201             repr[repr_len++] = 'e';
202             next;
203             if (parseChar('+'))
204             {}
205             else if (parseChar('-'))
206             {
207                 if (repr_len >= 31) return false;
208                 repr[repr_len++] = '-';
209             }
210             while(isDigit(peek()))
211             {
212                 if (repr_len >= 31) return false;
213                 repr[repr_len++] = peek();
214                 next;
215             }
216         }
217         repr[repr_len++] = '\0'; // force a '\0' to be there, hence rendering sscanf bounded.
218         assert(repr_len <= 32);
219 
220 
221         bool err;
222         double scanned = convertStringToDouble(repr.ptr, false, &err);
223         if (!err)
224         {
225             *number = scanned;
226             error = "";
227             return true;
228         }
229         else
230         {
231             error = "Couln't parse number";
232             return false;
233         }
234     }
235 
236     bool parseColorValue(out ubyte result, out string error) nothrow @nogc @trusted
237     {
238         double number;
239         if (!parseNumber(&number, error))
240         {
241             return false;
242         }
243         bool isPercentage = parseChar('%');
244         if (isPercentage)
245             number *= (255.0 / 100.0);
246         int c = cast(int)(0.5 + number); // round
247         result = clamp0to255(c);
248         return true; 
249     }
250 
251     bool parseOpacity(out ubyte result, out string error) nothrow @nogc @trusted
252     {
253         double number;
254         if (!parseNumber(&number, error))
255         {
256             return false;
257         }
258         bool isPercentage = parseChar('%');
259         if (isPercentage)
260             number *= 0.01;
261         int c = cast(int)(0.5 + number * 255.0);
262         result = clamp0to255(c);
263         return true;
264     }
265 
266     bool parsePercentage(out double result, out string error) nothrow @nogc @trusted
267     {
268         double number;
269         if (!parseNumber(&number, error))
270             return false;
271         if (!expectChar('%'))
272         {
273             error = "Expected % in color string";
274             return false;
275         }
276         result = number * 0.01;
277         return true;
278     }
279 
280     bool parseHueInDegrees(out double result, out string error) nothrow @nogc @trusted
281     {
282         double num;
283         if (!parseNumber(&num, error))
284             return false;
285 
286         if (parseString("deg"))
287         {
288             result = num;
289             return true;
290         }
291         else if (parseString("rad"))
292         {
293             result = num * 360.0 / (2 * PI);
294             return true;
295         }
296         else if (parseString("turn"))
297         {
298             result = num * 360.0;
299             return true;
300         }
301         else if (parseString("grad"))
302         {
303             result = num * 360.0 / 400.0;
304             return true;
305         }
306         else
307         {
308             // assume degrees
309             result = num;
310             return true;
311         }
312     }
313 
314     skipWhiteSpace();
315 
316     ubyte red, green, blue, alpha = 255;
317 
318     if (parseChar('#'))
319     {
320        int[8] digits;
321        int numDigits = 0;
322        for (int i = 0; i < 8; ++i)
323        {
324           if (parseHexDigit(digits[i]))
325               numDigits++;
326           else
327             break;
328        }
329        switch(numDigits)
330        {
331        case 4:
332            alpha  = cast(ubyte)( (digits[3] << 4) | digits[3]);
333            goto case 3;
334        case 3:
335            red   = cast(ubyte)( (digits[0] << 4) | digits[0]);
336            green = cast(ubyte)( (digits[1] << 4) | digits[1]);
337            blue  = cast(ubyte)( (digits[2] << 4) | digits[2]);
338            break;
339        case 8:
340            alpha  = cast(ubyte)( (digits[6] << 4) | digits[7]);
341            goto case 6;
342        case 6:
343            red   = cast(ubyte)( (digits[0] << 4) | digits[1]);
344            green = cast(ubyte)( (digits[2] << 4) | digits[3]);
345            blue  = cast(ubyte)( (digits[4] << 4) | digits[5]);
346            break;
347        default:
348            error = "Expected 3, 4, 6 or 8 digit in hexadecimal color literal";
349            return false;
350        }
351     }
352     else if (parseString("gray"))
353     {
354         
355         skipWhiteSpace();
356         if (!parseChar('('))
357         {
358             // This is named color "gray"
359             red = green = blue = 128;
360         }
361         else
362         {
363             skipWhiteSpace();
364             ubyte v;
365             if (!parseColorValue(v, error))
366                 return false;
367             red = green = blue = v;
368             skipWhiteSpace();
369             if (parseChar(','))
370             {
371                 // there is an alpha value
372                 skipWhiteSpace();
373                 if (!parseOpacity(alpha, error))
374                     return false;
375             }
376             if (!expectPunct(')'))
377             {
378                 error = "Expected ) in color string";
379                 return false;
380             }
381         }
382     }
383     else if (parseString("rgb"))
384     {
385         bool hasAlpha = parseChar('a');
386         if (!expectPunct('('))
387         {
388             error = "Expected ( in color string";
389             return false;
390         }
391         if (!parseColorValue(red, error))
392             return false;
393         if (!expectPunct(','))
394         {
395             error = "Expected , in color string";
396             return false;
397         }
398         if (!parseColorValue(green, error))
399             return false;
400         if (!expectPunct(','))
401         {
402             error = "Expected , in color string";
403             return false;
404         }
405         if (!parseColorValue(blue, error))
406             return false;
407         if (hasAlpha)
408         {
409             if (!expectPunct(','))
410             {
411                 error = "Expected , in color string";
412                 return false;
413             }
414             if (!parseOpacity(alpha, error))
415                 return false;
416         }
417         if (!expectPunct(')'))
418         {
419             error = "Expected , in color string";
420             return false;
421         }
422     }
423     else if (parseString("hsl"))
424     {
425         bool hasAlpha = parseChar('a');
426         expectPunct('(');
427 
428         double hueDegrees;
429         if (!parseHueInDegrees(hueDegrees, error))
430             return false;
431         
432         // Convert to turns
433         double hueTurns = hueDegrees / 360.0;
434         hueTurns -= floor(hueTurns); // take remainder
435         double hue = 6.0 * hueTurns;        
436         if (!expectPunct(','))
437         {
438             error = "Expected , in color string";
439             return false;
440         }
441         double sat;
442         if (!parsePercentage(sat, error))
443             return false;
444         if (!expectPunct(','))
445         {
446             error = "Expected , in color string";
447             return false;
448         }
449         double light;
450         if (!parsePercentage(light, error))
451             return false;
452         if (hasAlpha)
453         {
454             if (!expectPunct(','))
455             {
456                 error = "Expected , in color string";
457                 return false;
458             }
459             if (!parseOpacity(alpha, error))
460                 return false;
461         }
462         expectPunct(')');
463         double[3] rgb = convertHSLtoRGB(hue, sat, light);
464         red   = clamp0to255( cast(int)(0.5 + 255.0 * rgb[0]) );
465         green = clamp0to255( cast(int)(0.5 + 255.0 * rgb[1]) );
466         blue  = clamp0to255( cast(int)(0.5 + 255.0 * rgb[2]) );
467     }
468     else
469     {
470         // Initiate a binary search inside the sorted named color array
471         // See_also: https://en.wikipedia.org/wiki/Binary_search_algorithm
472 
473         // Current search range
474         // this range will only reduce because the color names are sorted
475         int L = 0;
476         int R = cast(int)(namedColorKeywords.length); 
477         int charPos = 0;
478 
479         matchloop:
480         while (true)
481         {
482             // Expect 
483             char ch = peek();
484             if (ch >= 'A' && ch <= 'Z')
485                 ch += ('a' - 'A');
486             if (ch < 'a' || ch > 'z') // not alpha?
487             {
488                 // Examine all alive cases. Select the one which have matched entirely.               
489                 foreach(candidate; L..R)
490                 {
491                     if (namedColorKeywords[candidate].length == charPos)// found it, return as there are no duplicates
492                     {
493                         // If we have matched all the alpha of the only remaining candidate, we have found a named color
494                         uint rgba = namedColorValues[candidate];
495                         red   = (rgba >> 24) & 0xff;
496                         green = (rgba >> 16) & 0xff;
497                         blue  = (rgba >>  8) & 0xff;
498                         alpha = (rgba >>  0) & 0xff;
499                         break matchloop;
500                     }
501                 }
502                 error = "Unexpected char in named color";
503                 return false;
504             }
505             next;
506 
507             // PERF: there could be something better with a dichotomy
508             // PERF: can elid search once we've passed the last match
509             bool firstFound = false;
510             int firstFoundIndex = R;
511             int lastFoundIndex = -1;
512             foreach(candindex; L..R)
513             {
514                 // Have we found ch in name[charPos] position?
515                 string candidate = namedColorKeywords[candindex];
516                 bool charIsMatching = (candidate.length > charPos) && (candidate[charPos] == ch);
517                 if (!firstFound && charIsMatching)
518                 {
519                     firstFound = true;
520                     firstFoundIndex = candindex;
521                 }
522                 if (charIsMatching)
523                     lastFoundIndex = candindex;
524             }
525 
526             // Zero candidate remain
527             if (lastFoundIndex < firstFoundIndex)
528             {
529                 error = "Can't recognize color string";
530                 return false;
531             }
532             else
533             {
534                 // Several candidate remain, go on and reduce the search range
535                 L = firstFoundIndex;
536                 R = lastFoundIndex + 1;
537                 charPos += 1;
538             }
539         }
540     }
541 
542     skipWhiteSpace();
543     if (!parseChar('\0'))
544     {
545         error = "Expected end of input at the end of color string";
546         return false;
547     }
548     color = RGBA(red, green, blue, alpha);
549     return true;
550 }
551 
552 RGBA parseHTMLColor(const(char)[] htmlColorString) nothrow @nogc @safe
553 {
554     RGBA res;
555     string error;
556     if (parseHTMLColor(htmlColorString, res, error))
557         return res;
558     else
559         assert(false);
560 }
561 
562 private:
563 
564 // 147 predefined color + "transparent"
565 static immutable string[147 + 1] namedColorKeywords =
566 [
567     "aliceblue", "antiquewhite", "aqua", "aquamarine",     "azure", "beige", "bisque", "black",
568     "blanchedalmond", "blue", "blueviolet", "brown",       "burlywood", "cadetblue", "chartreuse", "chocolate",
569     "coral", "cornflowerblue", "cornsilk", "crimson",      "cyan", "darkblue", "darkcyan", "darkgoldenrod",
570     "darkgray", "darkgreen", "darkgrey", "darkkhaki",      "darkmagenta", "darkolivegreen", "darkorange", "darkorchid",
571     "darkred","darksalmon","darkseagreen","darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet",
572     "deeppink", "deepskyblue", "dimgray", "dimgrey",       "dodgerblue", "firebrick", "floralwhite", "forestgreen",
573     "fuchsia", "gainsboro", "ghostwhite", "gold",          "goldenrod", "gray", "green", "greenyellow",
574     "grey", "honeydew", "hotpink", "indianred",            "indigo", "ivory", "khaki", "lavender",
575     "lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray",
576     "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey",
577     "lightsteelblue", "lightyellow", "lime", "limegreen",  "linen", "magenta", "maroon", "mediumaquamarine",
578     "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred",
579     "midnightblue", "mintcream", "mistyrose", "moccasin",  "navajowhite", "navy", "oldlace", "olive",
580     "olivedrab", "orange", "orangered",  "orchid",         "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
581     "papayawhip", "peachpuff", "peru", "pink",             "plum", "powderblue", "purple", "red",
582     "rosybrown", "royalblue", "saddlebrown", "salmon",     "sandybrown", "seagreen", "seashell", "sienna",
583     "silver", "skyblue", "slateblue", "slategray",         "slategrey", "snow", "springgreen", "steelblue",
584     "tan", "teal", "thistle", "tomato",                    "transparent", "turquoise", "violet", "wheat", 
585     "white", "whitesmoke", "yellow", "yellowgreen"
586 ];
587 
588 immutable static uint[147 + 1] namedColorValues =
589 [
590     0xf0f8ffff, 0xfaebd7ff, 0x00ffffff, 0x7fffd4ff, 0xf0ffffff, 0xf5f5dcff, 0xffe4c4ff, 0x000000ff, 
591     0xffebcdff, 0x0000ffff, 0x8a2be2ff, 0xa52a2aff, 0xdeb887ff, 0x5f9ea0ff, 0x7fff00ff, 0xd2691eff, 
592     0xff7f50ff, 0x6495edff, 0xfff8dcff, 0xdc143cff, 0x00ffffff, 0x00008bff, 0x008b8bff, 0xb8860bff, 
593     0xa9a9a9ff, 0x006400ff, 0xa9a9a9ff, 0xbdb76bff, 0x8b008bff, 0x556b2fff, 0xff8c00ff, 0x9932ccff, 
594     0x8b0000ff, 0xe9967aff, 0x8fbc8fff, 0x483d8bff, 0x2f4f4fff, 0x2f4f4fff, 0x00ced1ff, 0x9400d3ff, 
595     0xff1493ff, 0x00bfffff, 0x696969ff, 0x696969ff, 0x1e90ffff, 0xb22222ff, 0xfffaf0ff, 0x228b22ff, 
596     0xff00ffff, 0xdcdcdcff, 0xf8f8ffff, 0xffd700ff, 0xdaa520ff, 0x808080ff, 0x008000ff, 0xadff2fff, 
597     0x808080ff, 0xf0fff0ff, 0xff69b4ff, 0xcd5c5cff, 0x4b0082ff, 0xfffff0ff, 0xf0e68cff, 0xe6e6faff, 
598     0xfff0f5ff, 0x7cfc00ff, 0xfffacdff, 0xadd8e6ff, 0xf08080ff, 0xe0ffffff, 0xfafad2ff, 0xd3d3d3ff, 
599     0x90ee90ff, 0xd3d3d3ff, 0xffb6c1ff, 0xffa07aff, 0x20b2aaff, 0x87cefaff, 0x778899ff, 0x778899ff, 
600     0xb0c4deff, 0xffffe0ff, 0x00ff00ff, 0x32cd32ff, 0xfaf0e6ff, 0xff00ffff, 0x800000ff, 0x66cdaaff, 
601     0x0000cdff, 0xba55d3ff, 0x9370dbff, 0x3cb371ff, 0x7b68eeff, 0x00fa9aff, 0x48d1ccff, 0xc71585ff, 
602     0x191970ff, 0xf5fffaff, 0xffe4e1ff, 0xffe4b5ff, 0xffdeadff, 0x000080ff, 0xfdf5e6ff, 0x808000ff, 
603     0x6b8e23ff, 0xffa500ff, 0xff4500ff, 0xda70d6ff, 0xeee8aaff, 0x98fb98ff, 0xafeeeeff, 0xdb7093ff, 
604     0xffefd5ff, 0xffdab9ff, 0xcd853fff, 0xffc0cbff, 0xdda0ddff, 0xb0e0e6ff, 0x800080ff, 0xff0000ff, 
605     0xbc8f8fff, 0x4169e1ff, 0x8b4513ff, 0xfa8072ff, 0xf4a460ff, 0x2e8b57ff, 0xfff5eeff, 0xa0522dff,
606     0xc0c0c0ff, 0x87ceebff, 0x6a5acdff, 0x708090ff, 0x708090ff, 0xfffafaff, 0x00ff7fff, 0x4682b4ff, 
607     0xd2b48cff, 0x008080ff, 0xd8bfd8ff, 0xff6347ff, 0x00000000,  0x40e0d0ff, 0xee82eeff, 0xf5deb3ff, 
608     0xffffffff, 0xf5f5f5ff, 0xffff00ff, 0x9acd32ff,
609 ];
610 
611 
612 // Reference: https://www.w3.org/TR/css-color-4/#hsl-to-rgb
613 // this algorithm assumes that the hue has been normalized to a number in the half-open range [0, 6), 
614 // and the saturation and lightness have been normalized to the range [0, 1]. 
615 double[3] convertHSLtoRGB(double hue, double sat, double light) pure nothrow @nogc @safe
616 {
617     double t2;
618     if( light <= .5 ) 
619         t2 = light * (sat + 1);
620     else 
621         t2 = light + sat - (light * sat);
622     double t1 = light * 2 - t2;
623     double r = convertHueToRGB(t1, t2, hue + 2);
624     double g = convertHueToRGB(t1, t2, hue);
625     double b = convertHueToRGB(t1, t2, hue - 2);
626     return [r, g, b];
627 }
628 
629 double convertHueToRGB(double t1, double t2, double hue) pure nothrow @nogc @safe
630 {
631     if (hue < 0) 
632         hue = hue + 6;
633     if (hue >= 6) 
634         hue = hue - 6;
635     if (hue < 1) 
636         return (t2 - t1) * hue + t1;
637     else if(hue < 3) 
638         return t2;
639     else if(hue < 4) 
640         return (t2 - t1) * (4 - hue) + t1;
641     else 
642         return t1;
643 }
644 
645 unittest
646 {
647     bool doesntParse(string color)
648     {
649         RGBA parsed;
650         string error;
651         if (parseHTMLColor(color, parsed, error))
652         {
653             return false;
654         }
655         else
656             return true;
657     }
658 
659     bool testParse(string color, ubyte[4] correct)
660     {
661         RGBA parsed;
662         RGBA correctC = RGBA(correct[0], correct[1], correct[2], correct[3]);
663         string error;
664         
665 
666         if (parseHTMLColor(color, parsed, error))
667         {
668             return parsed == correctC;
669         }
670         else
671             return false;
672     }
673 
674     assert(doesntParse(""));
675 
676     // #hex colors    
677     assert(testParse("#aB9" , [0xaa, 0xBB, 0x99, 255]));
678     assert(testParse("#aB98" , [0xaa, 0xBB, 0x99, 0x88]));
679     assert(doesntParse("#"));
680     assert(doesntParse("#ab"));
681     assert(testParse(" #0f1c4A " , [0x0f, 0x1c, 0x4a, 255]));    
682     assert(testParse(" #0f1c4A43 " , [0x0f, 0x1c, 0x4A, 0x43]));
683     assert(doesntParse("#0123456"));
684     assert(doesntParse("#012345678"));
685 
686     // rgb() and rgba()
687     assert(testParse("  rgba( 14.01, 25.0e+0%, 16, 0.5)  " , [14, 64, 16, 128]));
688     assert(testParse("rgb(10e3,112,-3.4e-2)"               , [255, 112, 0, 255]));
689 
690     // hsl() and hsla()
691     assert(testParse("hsl(0   ,  100%, 50%)"         , [255, 0, 0, 255]));
692     assert(testParse("hsl(720,  100%, 50%)"          , [255, 0, 0, 255]));
693     assert(testParse("hsl(180deg,  100%, 50%)"       , [0, 255, 255, 255]));
694     assert(testParse("hsl(0grad, 100%, 50%)"         , [255, 0, 0, 255]));
695     assert(testParse("hsl(0rad,  100%, 50%)"         , [255, 0, 0, 255]));
696     assert(testParse("hsl(0turn, 100%, 50%)"         , [255, 0, 0, 255]));
697     assert(testParse("hsl(120deg, 100%, 50%)"        , [0, 255, 0, 255]));
698     assert(testParse("hsl(123deg,   2.5%, 0%)"       , [0, 0, 0, 255]));
699     assert(testParse("hsl(5.4e-5rad, 25%, 100%)"     , [255, 255, 255, 255]));
700     assert(testParse("hsla(0turn, 100%, 50%, 0.25)"  , [255, 0, 0, 64]));
701 
702     // gray values
703     assert(testParse(" gray( +0.0% )"       , [0, 0, 0, 255]));
704     assert(testParse(" gray "               , [128, 128, 128, 255]));
705     assert(testParse(" gray( 100%, 50% ) "  , [255, 255, 255, 128]));
706 
707     // Named colors
708     assert(testParse("tRaNsPaREnt"  , [0, 0, 0, 0]));
709     assert(testParse(" navy "  , [0, 0, 128, 255]));
710     assert(testParse("lightgoldenrodyellow"  , [250, 250, 210, 255]));
711     assert(doesntParse("animaginarycolorname")); // unknown named color
712     assert(doesntParse("navyblahblah")); // too much chars
713     assert(doesntParse("blac")); // incomplete color
714     assert(testParse("lime"  , [0, 255, 0, 255])); // termination with 2 candidate alive
715     assert(testParse("limegreen"  , [50, 205, 50, 255]));    
716 }
717 
718 unittest
719 {
720     // should work in CTFE
721     static immutable RGBA color = parseHTMLColor("red");
722 }