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>