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 }