1 /**
2     Implement CSS color parsing, like specified.
3     This uses the functions defined elsewhere in `colors`.
4 
5     Copyright: Copyright Guillaume Piolat 2020-2024.
6     License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, BSL-1.0)
7 */
8 module colors.parser;
9 
10 import std.math: PI, floor;
11 
12 import core.stdc.string: strlen;
13 
14 import colors.types;
15 import colors.colorspace;
16 import colors.conversions;
17 
18 /** 
19     Parses a CSS color string, and gives back a `Color`.
20     If parsing fails, return transparent black.
21 
22     See_also: `parseCSSColor`.
23 */
24 Color color(const(char)[] cssColorString) pure nothrow @nogc @safe
25 {
26     Color c;
27     string err;
28     if (parseCSSColor(cssColorString, c, err))
29         return c;
30     else
31         return c.init;
32 }
33 unittest
34 {
35     Color c;
36     assert(color("invalidname") == Color.init);
37     assert(color("red").toRGBA8() == RGBA8(255, 0, 0, 255));
38 }
39 
40 
41 /**
42     Parses a CSS color string, and gives back a `Color`.
43    
44     Params:
45         cssColorString = A CSS string describing a color.
46         outColor       = Output color.
47         error          = Error message. `null` on success.
48    
49     Returns:
50         A specified Color, that keeps the intent of the user. 
51         This is not necessarily usable right away, and will typically 
52         need sRGB conversion.
53         In other words, colors stay in their colorspace of definition.
54    
55     See_also: https://www.w3.org/TR/css-color-4/
56    
57    
58     Example:
59     ---
60     import colors;
61     
62     // all HTML named colors
63     parseCSSColor("black", color, error);
64     
65     // hex colors including alpha versions
66     parseCSSColor("#fe85dc", color, error);
67 
68     // alpha                    
69     parseCSSColor("rgba(64, 255, 128, 0.24)", color, error);   
70 
71     // percentage, floating-point
72     parseCSSColor("rgb(9e-1, 50%, 128)", color, error);
73 
74     // hsv colors
75     parseCSSColor("hsl(120deg, 25%, 75%)", color, error);
76 
77     // gray colors
78     parseCSSColor("gray(0.5)", color, error);
79 
80     // strips whitespace
81     parseCSSColor(" rgb ( 245 , 112 , 74 )  ", color, error);  
82     ---
83    
84 */
85 bool parseCSSColor(const(char)[] cssColorString, 
86                    out Color outColor, 
87                    out string error) pure nothrow @nogc @safe
88 {
89 
90     error = null; // indicate success
91     const(char)[] s = cssColorString;   
92     int index = 0;    
93 
94     char peek() nothrow @nogc @safe
95     {
96         if (index >= cssColorString.length)
97             return '\0';
98         else
99             return s[index];
100     }
101 
102     void next() nothrow @nogc @safe
103     {
104         index++;
105     }
106 
107     bool parseChar(char ch) nothrow @nogc @safe
108     {
109         if (peek() == ch)
110         {
111             next;
112             return true;
113         }
114         return false;
115     }
116 
117     bool expectChar(char ch) nothrow @nogc @safe
118     {
119         if (!parseChar(ch))
120             return false;
121         return true;
122     }
123 
124     bool parseString(string s) nothrow @nogc @safe
125     {
126         int save = index;
127 
128         for (int i = 0; i < s.length; ++i)
129         {
130             if (!parseChar(s[i]))
131             {
132                 index = save;
133                 return false;
134             }
135         }
136         return true;
137     }
138 
139     bool isWhite(char ch) nothrow @nogc @safe
140     {
141         return ch == ' '  || ch == '\n' || ch == '\r' 
142             || ch == '\t' || ch == '\r';
143     }
144 
145     bool isDigit(char ch) nothrow @nogc @safe
146     {
147         return ch >= '0' && ch <= '9';
148     }
149 
150     bool expectDigit(out char digit) nothrow @nogc @safe
151     {
152         char ch = peek();
153         if (isDigit(ch))
154         {            
155             next;
156             digit = ch;
157             return true;
158         }
159         else
160             return false;
161     }
162 
163     bool parseHexDigit(out int digit)
164         nothrow @nogc @safe
165     {
166         char ch = peek();
167         if (isDigit(ch))
168         {
169             next;
170             digit = ch - '0';
171             return true;
172         }
173         else if (ch >= 'a' && ch <= 'f')
174         {
175             next;
176             digit = 10 + (ch - 'a');
177             return true;
178         }
179         else if (ch >= 'A' && ch <= 'F')
180         {
181             next;
182             digit = 10 + (ch - 'A');
183             return true;
184         }
185         else
186             return false;
187     }
188 
189     void skipWhiteSpace() nothrow @nogc @safe
190     {       
191         while (isWhite(peek()))
192             next;
193     }
194 
195     bool expectOptionalPunct(char ch) nothrow @nogc @safe
196     {
197         skipWhiteSpace();
198         bool seen = false;
199         char pch = peek();
200         if (pch == ch)
201         {            
202             seen = true;
203             next;
204         }
205         skipWhiteSpace();
206         return seen;
207     }
208 
209     bool expectPunct(char ch) nothrow @nogc @safe
210     {
211         skipWhiteSpace();
212         if (!expectChar(ch))
213             return false;
214         skipWhiteSpace();
215         return true;
216     }
217 
218     ubyte clamp0to255(int a) nothrow @nogc @safe
219     {
220         if (a < 0) return 0;
221         if (a > 255) return 255;
222         return cast(ubyte)a;
223     }
224 
225     // See: https://www.w3.org/TR/css-syntax/#consume-a-number
226     bool parseNumber(double* number, out string error) @trusted
227         nothrow @nogc
228     {
229         char[32] repr;
230         int repr_len = 0;
231 
232         if (parseChar('+'))
233         {}
234         else if (parseChar('-'))
235         {
236             if (repr_len >= 31) return false;
237             repr[repr_len++] = '-';
238         }
239         while(isDigit(peek()))
240         {
241             if (repr_len >= 31) return false;
242             repr[repr_len++] = peek();
243             next;
244         }
245         if (peek() == '.')
246         {
247             if (repr_len >= 31) return false;
248             repr[repr_len++] = '.';
249             next;
250             char digit;
251             bool parsedDigit = expectDigit(digit);
252             if (!parsedDigit)
253                 return false;
254 
255             if (repr_len >= 31) return false;
256             repr[repr_len++] = digit;
257 
258             while(isDigit(peek()))
259             {
260                 if (repr_len >= 31) return false;
261                 repr[repr_len++] = peek();
262                 next;
263             }
264         }
265         if (peek() == 'e' || peek() == 'E')
266         {
267             if (repr_len >= 31) return false;
268             repr[repr_len++] = 'e';
269             next;
270             if (parseChar('+'))
271             {}
272             else if (parseChar('-'))
273             {
274                 if (repr_len >= 31) return false;
275                 repr[repr_len++] = '-';
276             }
277             while(isDigit(peek()))
278             {
279                 if (repr_len >= 31) return false;
280                 repr[repr_len++] = peek();
281                 next;
282             }
283         }
284 
285         // force a '\0' to be there, making sscanf bounded
286         repr[repr_len++] = '\0'; 
287         assert(repr_len <= 32);
288 
289 
290         bool err;
291         double scanned = convertStringToDouble(repr.ptr, false, &err);
292         if (!err)
293         {
294             *number = scanned;
295             return true;
296         }
297         else
298         {
299             error = "Couln't parse number";
300             return false;
301         }
302     }
303 
304     bool parseColorValue(out float result, out string error) @trusted
305         nothrow @nogc
306     {
307         double number;
308         if (!parseNumber(&number, error))
309         {
310             return false;
311         }
312         bool isPercentage = parseChar('%');
313         if (isPercentage)
314             number *= (255.0 / 100.0);
315 
316         // No clamping!
317         // "Values outside these ranges are not invalid, but are 
318         // clamped to the ranges defined here at computed-value time."
319         result = number;
320         return true; 
321     }
322 
323     bool parseOpacity(out float result, out string error) @trusted
324         nothrow @nogc
325     {
326         double number;
327         if (!parseNumber(&number, error))
328         {
329             return false;
330         }
331 
332         // "Values outside the range [0,1] are not invalid, but are 
333         // clamped to that range when computed."
334         bool isPercentage = parseChar('%');
335         if (isPercentage)
336             number *= 0.01;
337 
338         result = number;
339         return true;
340     }
341 
342     bool parsePercentage(out double result, out string error) @trusted
343         nothrow @nogc
344     {
345         double number;
346         if (!parseNumber(&number, error))
347             return false;
348         if (!expectChar('%'))
349         {
350             error = "Expected % in color string";
351             return false;
352         }
353         result = number * 0.01;
354         return true;
355     }
356 
357     bool parseHueInDegrees(out double result, 
358                            out string error) @trusted
359         nothrow @nogc
360     {
361         double num;
362         if (!parseNumber(&num, error))
363             return false;
364 
365         if (parseString("deg"))
366         {
367             result = num;
368             return true;
369         }
370         else if (parseString("rad"))
371         {
372             result = num * 360.0 / (2 * PI);
373             return true;
374         }
375         else if (parseString("turn"))
376         {
377             result = num * 360.0;
378             return true;
379         }
380         else if (parseString("grad"))
381         {
382             result = num * 360.0 / 400.0;
383             return true;
384         }
385         else
386         {
387             // assume degrees
388             result = num;
389             return true;
390         }
391     }
392 
393     skipWhiteSpace();
394 
395     //ubyte red, green, blue, alpha = 255;
396 
397     if (parseChar('#'))
398     {
399        int red = 255, 
400            green = 255,
401            blue = 255,
402            alpha = 255;
403 
404        int[8] digits;
405        int numDigits = 0;
406        for (int i = 0; i < 8; ++i)
407        {
408           if (parseHexDigit(digits[i]))
409               numDigits++;
410           else
411             break;
412        }
413        switch(numDigits)
414        {
415        case 4:
416            alpha  = cast(ubyte)( (digits[3] << 4) | digits[3]);
417            goto case 3;
418        case 3:
419            red   = cast(ubyte)( (digits[0] << 4) | digits[0]);
420            green = cast(ubyte)( (digits[1] << 4) | digits[1]);
421            blue  = cast(ubyte)( (digits[2] << 4) | digits[2]);
422            break;
423        case 8:
424            alpha  = cast(ubyte)( (digits[6] << 4) | digits[7]);
425            goto case 6;
426        case 6:
427            red   = cast(ubyte)( (digits[0] << 4) | digits[1]);
428            green = cast(ubyte)( (digits[2] << 4) | digits[3]);
429            blue  = cast(ubyte)( (digits[4] << 4) | digits[5]);
430            break;
431        default:
432            error = "Expected 3, 4, 6, or 8 digits in hex literal";
433            return false;
434        }
435        outColor = rgba(red, green, blue, alpha / 255.0f);
436     }
437     else if (parseString("gray"))
438     {
439         float red = 255, 
440               green = 255,
441               blue = 255;
442         float alpha = 1.0f;
443         
444         skipWhiteSpace();
445         if (!parseChar('('))
446         {
447             // This is named color "gray"
448             red = green = blue = 128;
449         }
450         else
451         {
452             skipWhiteSpace();
453             float v;
454             if (!parseColorValue(v, error))
455                 return false;
456             red = green = blue = v;
457             skipWhiteSpace();
458             if (parseChar(','))
459             {
460                 // there is an alpha value
461                 skipWhiteSpace();
462                 if (!parseOpacity(alpha, error))
463                     return false;
464             }
465             if (!expectPunct(')'))
466             {
467                 error = "Expected ) in color string";
468                 return false;
469             }
470         }
471         outColor = rgba(red, green, blue, alpha);
472     }
473     else if (parseString("rgb"))
474     {
475         float red = 255, 
476               green = 255,
477               blue = 255;
478         float alpha = 1.0f;
479         bool hasAlpha = parseChar('a');
480         if (!expectPunct('('))
481         {
482             error = "Expected ( in color string";
483             return false;
484         }
485         int components = 0;
486         if (!parseColorValue(red, error))
487             return false;
488         components += 1;
489         bool parsedComma0 = expectOptionalPunct(',');
490         if (!parseColorValue(green, error))
491             return false;
492         components += 1;
493         bool parsedComma1 = expectOptionalPunct(',');
494         if (!parseColorValue(blue, error))
495             return false;
496         components += 1;
497         if (hasAlpha)
498         {
499             if (!expectPunct(','))
500             {
501                 error = "Expected , in color string";
502                 return false;
503             }
504             if (!parseOpacity(alpha, error))
505                 return false;
506             components += 1;
507         }
508         if (components <= 2)
509         {
510             error = "Not enough components";
511             return false;
512         }
513         // lack of closing paren is valid
514         bool hasClosingParen = expectOptionalPunct(')');
515         outColor = rgba(red, green, blue, alpha);
516     }
517     else if (parseString("hsl"))
518     {
519         bool hasAlpha = parseChar('a');
520         expectPunct('(');
521         float alpha = 1.0f;
522         double hueDegrees;
523         if (!parseHueInDegrees(hueDegrees, error))
524             return false;
525         // Convert to turns
526         if (!expectPunct(','))
527         {
528             error = "Expected , in color string";
529             return false;
530         }
531         double sat;
532         if (!parsePercentage(sat, error))
533             return false;
534         if (!expectPunct(','))
535         {
536             error = "Expected , in color string";
537             return false;
538         }
539         double light;
540         if (!parsePercentage(light, error))
541             return false;
542         if (hasAlpha)
543         {
544             if (!expectPunct(','))
545             {
546                 error = "Expected , in color string";
547                 return false;
548             }
549             if (!parseOpacity(alpha, error))
550                 return false;
551         }
552         expectPunct(')');
553         outColor = hsla(hueDegrees, sat, light, alpha);
554     }
555     else
556     {
557         // Initiate a binary search inside the sorted named color 
558         // array.
559 
560         // Current search range
561         // Will only reduce because the color names are sorted.
562         int L = 0;
563         int R = cast(int)(namedColors.length); 
564         int charPos = 0;
565 
566         matchloop:
567         while (true)
568         {
569             // Expect 
570             char ch = peek();
571             if (ch >= 'A' && ch <= 'Z')
572                 ch += ('a' - 'A');
573             if (ch < 'a' || ch > 'z') // not alpha?
574             {
575                 // Examine all alive cases. Select the one which have 
576                 // matched entirely.               
577                 foreach(candidate; L..R)
578                 {
579                     // found it, return as there are no duplicates
580                     if (namedColors[candidate].length == charPos)
581                     {
582                         // If we have matched all the alpha of the 
583                         // only remaining candidate, we have found a 
584                         // named color
585                         uint uintColor = namedColorValues[candidate];
586                         int r = (uintColor >> 24) & 0xff;
587                         int g = (uintColor >> 16) & 0xff;
588                         int b = (uintColor >>  8) & 0xff;
589                         int a = (uintColor >>  0) & 0xff;
590                         outColor = rgba(r, g, b, a / 255.0f);
591                         break matchloop;
592                     }
593                 }
594                 error = "Unexpected char in named color";
595                 return false;
596             }
597             next;
598 
599             // PERF: there could be something better with a dichotomy
600             // PERF: can elid search once we've passed the last match
601             bool firstFound = false;
602             int firstFoundIndex = R;
603             int lastFoundIndex = -1;
604             foreach(candindex; L..R)
605             {
606                 // Have we found ch in name[charPos] position?
607                 string candidate = namedColors[candindex];
608                 bool charIsMatching = (candidate.length > charPos) 
609                                    && (candidate[charPos] == ch);
610                 if (!firstFound && charIsMatching)
611                 {
612                     firstFound = true;
613                     firstFoundIndex = candindex;
614                 }
615                 if (charIsMatching)
616                     lastFoundIndex = candindex;
617             }
618 
619             // Zero candidate remain
620             if (lastFoundIndex < firstFoundIndex)
621             {
622                 error = "Can't recognize color string";
623                 return false;
624             }
625             else
626             {
627                 // Several candidate remain, go on and reduce the 
628                 // search range
629                 L = firstFoundIndex;
630                 R = lastFoundIndex + 1;
631                 charPos += 1;
632             }
633         }
634     }
635 
636     skipWhiteSpace();
637     if (!parseChar('\0'))
638     {
639         error = "Expected end of input at the end of color string";
640         return false;
641     }
642 
643     return true;
644 }
645 
646 private:
647 
648 // 147 predefined color + "transparent"
649 static immutable string[147 + 1] namedColors =
650 [
651     "aliceblue", "antiquewhite", "aqua", "aquamarine",     
652     "azure", "beige", "bisque", "black",
653     "blanchedalmond", "blue", "blueviolet", "brown",       
654     "burlywood", "cadetblue", "chartreuse", "chocolate",
655     "coral", "cornflowerblue", "cornsilk", "crimson",      
656     "cyan", "darkblue", "darkcyan", "darkgoldenrod",
657     "darkgray", "darkgreen", "darkgrey", "darkkhaki",      
658     "darkmagenta", "darkolivegreen", "darkorange", "darkorchid",
659     "darkred","darksalmon","darkseagreen","darkslateblue", 
660     "darkslategray", "darkslategrey", "darkturquoise", "darkviolet",
661     "deeppink", "deepskyblue", "dimgray", "dimgrey",       
662     "dodgerblue", "firebrick", "floralwhite", "forestgreen",
663     "fuchsia", "gainsboro", "ghostwhite", "gold",          
664     "goldenrod", "gray", "green", "greenyellow",
665     "grey", "honeydew", "hotpink", "indianred",            
666     "indigo", "ivory", "khaki", "lavender",
667     "lavenderblush","lawngreen","lemonchiffon","lightblue",
668     "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray",
669     "lightgreen", "lightgrey", "lightpink", "lightsalmon", 
670     "lightseagreen", "lightskyblue", "lightslategray", 
671                                                      "lightslategrey",
672     "lightsteelblue", "lightyellow", "lime", "limegreen",  
673     "linen", "magenta", "maroon", "mediumaquamarine",
674     "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", 
675     "mediumslateblue", "mediumspringgreen", "mediumturquoise", 
676                                                     "mediumvioletred",
677     "midnightblue", "mintcream", "mistyrose", "moccasin",  
678     "navajowhite", "navy", "oldlace", "olive",
679     "olivedrab", "orange", "orangered",  "orchid",         
680     "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
681     "papayawhip", "peachpuff", "peru", "pink",             
682     "plum", "powderblue", "purple", "red",
683     "rosybrown", "royalblue", "saddlebrown", "salmon",     
684     "sandybrown", "seagreen", "seashell", "sienna",
685     "silver", "skyblue", "slateblue", "slategray",         
686     "slategrey", "snow", "springgreen", "steelblue",
687     "tan", "teal", "thistle", "tomato",                    
688     "transparent", "turquoise", "violet", "wheat", 
689     "white", "whitesmoke", "yellow", "yellowgreen"
690 ];
691 
692 immutable static uint[147 + 1] namedColorValues =
693 [
694     0xf0f8ffff, 0xfaebd7ff, 0x00ffffff, 0x7fffd4ff, 
695     0xf0ffffff, 0xf5f5dcff, 0xffe4c4ff, 0x000000ff, 
696     0xffebcdff, 0x0000ffff, 0x8a2be2ff, 0xa52a2aff, 
697     0xdeb887ff, 0x5f9ea0ff, 0x7fff00ff, 0xd2691eff, 
698     0xff7f50ff, 0x6495edff, 0xfff8dcff, 0xdc143cff, 
699     0x00ffffff, 0x00008bff, 0x008b8bff, 0xb8860bff, 
700     0xa9a9a9ff, 0x006400ff, 0xa9a9a9ff, 0xbdb76bff, 
701     0x8b008bff, 0x556b2fff, 0xff8c00ff, 0x9932ccff, 
702     0x8b0000ff, 0xe9967aff, 0x8fbc8fff, 0x483d8bff, 
703     0x2f4f4fff, 0x2f4f4fff, 0x00ced1ff, 0x9400d3ff, 
704     0xff1493ff, 0x00bfffff, 0x696969ff, 0x696969ff, 
705     0x1e90ffff, 0xb22222ff, 0xfffaf0ff, 0x228b22ff, 
706     0xff00ffff, 0xdcdcdcff, 0xf8f8ffff, 0xffd700ff, 
707     0xdaa520ff, 0x808080ff, 0x008000ff, 0xadff2fff, 
708     0x808080ff, 0xf0fff0ff, 0xff69b4ff, 0xcd5c5cff, 
709     0x4b0082ff, 0xfffff0ff, 0xf0e68cff, 0xe6e6faff, 
710     0xfff0f5ff, 0x7cfc00ff, 0xfffacdff, 0xadd8e6ff, 
711     0xf08080ff, 0xe0ffffff, 0xfafad2ff, 0xd3d3d3ff, 
712     0x90ee90ff, 0xd3d3d3ff, 0xffb6c1ff, 0xffa07aff, 
713     0x20b2aaff, 0x87cefaff, 0x778899ff, 0x778899ff, 
714     0xb0c4deff, 0xffffe0ff, 0x00ff00ff, 0x32cd32ff, 
715     0xfaf0e6ff, 0xff00ffff, 0x800000ff, 0x66cdaaff, 
716     0x0000cdff, 0xba55d3ff, 0x9370dbff, 0x3cb371ff, 
717     0x7b68eeff, 0x00fa9aff, 0x48d1ccff, 0xc71585ff, 
718     0x191970ff, 0xf5fffaff, 0xffe4e1ff, 0xffe4b5ff, 
719     0xffdeadff, 0x000080ff, 0xfdf5e6ff, 0x808000ff, 
720     0x6b8e23ff, 0xffa500ff, 0xff4500ff, 0xda70d6ff, 
721     0xeee8aaff, 0x98fb98ff, 0xafeeeeff, 0xdb7093ff, 
722     0xffefd5ff, 0xffdab9ff, 0xcd853fff, 0xffc0cbff, 
723     0xdda0ddff, 0xb0e0e6ff, 0x800080ff, 0xff0000ff, 
724     0xbc8f8fff, 0x4169e1ff, 0x8b4513ff, 0xfa8072ff, 
725     0xf4a460ff, 0x2e8b57ff, 0xfff5eeff, 0xa0522dff,
726     0xc0c0c0ff, 0x87ceebff, 0x6a5acdff, 0x708090ff, 
727     0x708090ff, 0xfffafaff, 0x00ff7fff, 0x4682b4ff, 
728     0xd2b48cff, 0x008080ff, 0xd8bfd8ff, 0xff6347ff, 
729     0x00000000,  0x40e0d0ff, 0xee82eeff, 0xf5deb3ff, 
730     0xffffffff, 0xf5f5f5ff, 0xffff00ff, 0x9acd32ff,
731 ];
732 
733 unittest
734 {
735     import core.stdc.stdio;
736     bool doesntParse(string color)
737     {
738         Color parsed;
739         string error;
740         if (parseCSSColor(color, parsed, error))
741         {
742             return false;
743         }
744         else
745             return true;
746     }
747 
748     bool testParse(string color, ubyte[4] correct)
749     {
750         Color parsed;
751         RGBA8 C = RGBA8(correct[0], correct[1], 
752                          correct[2], correct[3]);
753         string error;
754 
755         if (parseCSSColor(color, parsed, error))
756         {
757             RGBA8 srgb = parsed.toRGBA8(); 
758             if (srgb != C)
759             {
760                 printf("Error: got %d,%d,%d,%d not %d,%d,%d,%d.\n",
761                        srgb.r, srgb.g, srgb.b, srgb.a,
762                        C.r, C.g, C.b, C.a);
763             }
764             return srgb == C;
765         }
766         else
767         {
768             printf("Error: didn't parse.\n");
769             return false;
770         }
771     }
772 
773     assert(doesntParse(""));
774 
775     // #hex colors    
776     assert(testParse("#aB9" , [0xaa, 0xBB, 0x99, 255]));
777     assert(testParse("#aB98" , [0xaa, 0xBB, 0x99, 0x88]));
778     assert(doesntParse("#"));
779     assert(doesntParse("#ab"));
780     assert(testParse(" #0f1c4A " , [0x0f, 0x1c, 0x4a, 255]));    
781     assert(testParse(" #0f1c4A43 " , [0x0f, 0x1c, 0x4A, 0x43]));
782     assert(doesntParse("#0123456"));
783     assert(doesntParse("#012345678"));
784 
785     // rgb() and rgba()
786     assert(testParse("  rgba( 14.01, 25.0e+0%, 16, 0.5)  " , 
787         [14, 64, 16, 128]));
788     assert(testParse("rgb(10e3,112,-3.4e-2)"               , 
789         [255, 112, 0, 255]));
790 
791     // hsl() and hsla()
792     assert(testParse("hsl(0   ,  100%, 50%)"         , 
793         [255, 0, 0, 255]));
794     assert(testParse("hsl(720,  100%, 50%)"          , 
795         [255, 0, 0, 255]));
796     assert(testParse("hsl(180deg,  100%, 50%)"       , 
797         [0, 255, 255, 255]));
798     assert(testParse("hsl(0grad, 100%, 50%)"         , 
799         [255, 0, 0, 255]));
800     assert(testParse("hsl(0rad,  100%, 50%)"         , 
801         [255, 0, 0, 255]));
802     assert(testParse("hsl(0turn, 100%, 50%)"         , 
803         [255, 0, 0, 255]));
804     assert(testParse("hsl(120deg, 100%, 50%)"        , 
805         [0, 255, 0, 255]));
806     assert(testParse("hsl(123deg,   2.5%, 0%)"       , 
807         [0, 0, 0, 255]));
808     assert(testParse("hsl(5.4e-5rad, 25%, 100%)"     , 
809         [255, 255, 255, 255]));
810     assert(testParse("hsla(0turn, 100%, 50%, 0.25)"  , 
811         [255, 0, 0, 64]));
812 
813     // gray values
814     assert(testParse(" gray( +0.0% )"       , [0, 0, 0, 255]));
815     assert(testParse(" gray "               , [128, 128, 128, 255]));
816     assert(testParse(" gray( 100%, 50% ) "  , [255, 255, 255, 128]));
817 
818     // Named colors
819     assert(testParse("tRaNsPaREnt"  , [0, 0, 0, 0]));
820     assert(testParse(" navy "  , [0, 0, 128, 255]));
821     assert(testParse("lightgoldenrodyellow"  , [250, 250, 210, 255]));
822     assert(doesntParse("animaginarycolorname")); // unknown name
823     assert(doesntParse("navyblahblah")); // too much chars
824     assert(doesntParse("blac")); // incomplete color
825     assert(testParse("lime"  , [0, 255, 0, 255])); // 2 candidates
826     assert(testParse("limegreen"  , [50, 205, 50, 255]));    
827 }
828 
829 // <copied from dplug:core to avoid a dependency>
830 
831 // C-locale independent string to float parsing.
832 // Params:
833 //     s Must be a zero-terminated string.
834 //     mustConsumeEntireInput if true, check that s is entirely 
835 //     consumed by parsing the number.
836 //     err: optional bool
837 public double convertStringToDouble(const(char)* s, 
838                                     bool mustConsumeEntireInput,
839                                     bool* err) pure nothrow @nogc
840 {
841     if (s is null)
842     {
843         if (err) *err = true;
844         return 0.0;
845     }
846 
847     const(char)* end;
848     bool strtod_err = false;
849     double r = stb__clex_parse_number_literal(s, &end, 
850         &strtod_err, true);
851 
852     if (strtod_err)
853     {
854         if (err) *err = true;
855         return 0.0;
856     }
857 
858     if (mustConsumeEntireInput)
859     {
860         size_t len = strlen(s);
861         if (end != s + len)
862         {
863             if (err) *err = true; // did not consume whole string
864             return 0.0;
865         }
866     }
867 
868     if (err) *err = false; // no error
869     return r;
870 }
871 
872 double stb__clex_parse_number_literal(const(char)* p, 
873                                       const(char)**q, 
874                                       bool* err,
875                                       bool allowFloat) 
876     pure nothrow @nogc 
877 {
878     const(char)* s = p;
879     double value=0;
880     int base=10;
881     int exponent=0;
882     int signMantissa = 1;
883 
884     // Skip leading whitespace, like scanf and strtod do
885     while (true)
886     {
887         char ch = *p;
888         if (ch == ' ' || ch == '\t' || ch == '\r' 
889             || ch == '\n' || ch == '\f' || ch == '\r')
890         {
891             p += 1;
892         }
893         else
894             break;
895     }
896 
897 
898     if (*p == '-') 
899     {
900         signMantissa = -1;
901         p += 1;
902     } 
903     else if (*p == '+') 
904     {
905         p += 1;
906     }
907 
908     if (*p == '0') 
909     {
910         if (p[1] == 'x' || p[1] == 'X') 
911         {
912             base=16;
913             p += 2;
914         }
915     }
916 
917     for (;;) 
918     {
919         if (*p >= '0' && *p <= '9')
920             value = value*base + (*p++ - '0');
921         else if (base == 16 && *p >= 'a' && *p <= 'f')
922             value = value*base + 10 + (*p++ - 'a');
923         else if (base == 16 && *p >= 'A' && *p <= 'F')
924             value = value*base + 10 + (*p++ - 'A');
925         else
926             break;
927     }
928 
929     if (allowFloat)
930     {
931         if (*p == '.') 
932         {
933             double pow, addend = 0;
934             ++p;
935             for (pow=1; ; pow*=base) 
936             {
937                 if (*p >= '0' && *p <= '9')
938                     addend = addend*base + (*p++ - '0');
939                 else if (base == 16 && *p >= 'a' && *p <= 'f')
940                     addend = addend*base + 10 + (*p++ - 'a');
941                 else if (base == 16 && *p >= 'A' && *p <= 'F')
942                     addend = addend*base + 10 + (*p++ - 'A');
943                 else
944                     break;
945             }
946             value += addend / pow;
947         }
948         if (base == 16) {
949             // exponent required for hex float literal, 
950             // else it's an integer literal like 0x123
951             exponent = (*p == 'p' || *p == 'P');
952         } else
953             exponent = (*p == 'e' || *p == 'E');
954 
955         if (exponent) 
956         {
957             int sign = p[1] == '-';
958             uint exponent2 = 0;
959             double power=1;
960             ++p;
961             if (*p == '-' || *p == '+')
962                 ++p;
963             while (*p >= '0' && *p <= '9')
964                 exponent2 = exponent2*10 + (*p++ - '0');
965 
966             if (base == 16)
967                 power = stb__clex_pow(2, exponent2);
968             else
969                 power = stb__clex_pow(10, exponent2);
970             if (sign)
971                 value /= power;
972             else
973                 value *= power;
974         }
975     }
976 
977     if (q) *q = p;
978     if (err) *err = false; // seen no error
979 
980     if (signMantissa < 0)
981         value = -value;
982 
983     if (!allowFloat)
984     {
985         // clamp and round to nearest integer
986         if (value > int.max) value = int.max;
987         if (value < int.min) value = int.min;
988     }    
989     return value;
990 }
991 
992 double stb__clex_pow(double base, uint exponent) pure
993     nothrow @nogc
994 {
995     double value=1;
996     for ( ; exponent; exponent >>= 1) {
997         if (exponent & 1)
998             value *= base;
999         base *= base;
1000     }
1001     return value;
1002 }
1003 
1004 // </copied from dplug:core to avoid a dependency>