1 /** 2 This is the helper functions for cssexpand, 3 extracted from html.d. 4 */ 5 module arsd.css; 6 7 import arsd.color; 8 9 import std.exception; 10 import std.conv; 11 import std..string; 12 import std.array; 13 import std.uri; 14 import std.range; 15 16 abstract class CssPart { 17 override string toString() const; 18 CssPart clone() const; 19 } 20 21 class CssAtRule : CssPart { 22 this() {} 23 this(ref string css) { 24 assert(css.length); 25 assert(css[0] == '@'); 26 27 int braceCount = 0; 28 int startOfInnerSlice = -1; 29 30 foreach(i, c; css) { 31 if(braceCount == 0 && c == ';') { 32 content = css[0 .. i + 1]; 33 css = css[i + 1 .. $]; 34 35 opener = content; 36 break; 37 } 38 39 if(c == '{') { 40 braceCount++; 41 if(startOfInnerSlice == -1) 42 startOfInnerSlice = i; 43 } 44 if(c == '}') { 45 braceCount--; 46 if(braceCount < 0) 47 throw new Exception("Bad CSS: mismatched }"); 48 49 if(braceCount == 0) { 50 opener = css[0 .. startOfInnerSlice]; 51 inner = css[startOfInnerSlice + 1 .. i]; 52 53 content = css[0 .. i + 1]; 54 css = css[i + 1 .. $]; 55 break; 56 } 57 } 58 } 59 } 60 61 string content; 62 63 string opener; 64 string inner; 65 66 override CssAtRule clone() const { 67 auto n = new CssAtRule(); 68 n.content = content; 69 n.opener = opener; 70 n.inner = inner; 71 return n; 72 } 73 override string toString() const { return content; } 74 } 75 76 class CssRuleSet : CssPart { 77 this() {} 78 79 this(ref string css) { 80 auto idx = css.indexOf("{"); 81 assert(idx != -1); 82 foreach(selector; css[0 .. idx].split(",")) 83 selectors ~= selector.strip; 84 85 css = css[idx .. $]; 86 int braceCount = 0; 87 string content; 88 size_t f = css.length; 89 foreach(i, c; css) { 90 if(c == '{') 91 braceCount++; 92 if(c == '}') { 93 braceCount--; 94 if(braceCount == 0) { 95 f = i; 96 break; 97 } 98 } 99 } 100 101 content = css[1 .. f]; // skipping the { 102 if(f < css.length && css[f] == '}') 103 f++; 104 css = css[f .. $]; 105 106 contents = lexCss(content); 107 } 108 109 string[] selectors; 110 CssPart[] contents; 111 112 override CssRuleSet clone() const { 113 auto n = new CssRuleSet(); 114 n.selectors = selectors.dup; 115 foreach(part; contents) 116 n.contents ~= part.clone(); 117 return n; 118 } 119 120 CssRuleSet[] deNest(CssRuleSet outer = null) const { 121 CssRuleSet[] ret; 122 123 CssRuleSet levelOne = new CssRuleSet(); 124 ret ~= levelOne; 125 if(outer is null) 126 levelOne.selectors = selectors.dup; 127 else { 128 foreach(outerSelector; outer.selectors.length ? outer.selectors : [""]) 129 foreach(innerSelector; selectors) { 130 /* 131 it would be great to do a top thing and a bottom, examples: 132 .awesome, .awesome\& { 133 .something img {} 134 } 135 136 should give: 137 .awesome .something img, .awesome.something img { } 138 139 And also 140 \&.cool { 141 .something img {} 142 } 143 144 should give: 145 .something img.cool {} 146 147 OR some such syntax. 148 149 150 The idea though is it will ONLY apply to end elements with that particular class. Why is this good? We might be able to isolate the css more for composited files. 151 152 idk though. 153 */ 154 /+ 155 // FIXME: this implementation is useless, but the idea of allowing combinations at the top level rox. 156 if(outerSelector.length > 2 && outerSelector[$-2] == '\\' && outerSelector[$-1] == '&') { 157 // the outer one is an adder... so we always want to paste this on, and if the inner has it, collapse it 158 if(innerSelector.length > 2 && innerSelector[0] == '\\' && innerSelector[1] == '&') 159 levelOne.selectors ~= outerSelector[0 .. $-2] ~ innerSelector[2 .. $]; 160 else 161 levelOne.selectors ~= outerSelector[0 .. $-2] ~ innerSelector; 162 } else 163 +/ 164 165 // we want to have things like :hover, :before, etc apply without implying 166 // a descendant. 167 168 // If you want it to be a descendant pseudoclass, use the *:something - the 169 // wildcard tag - instead of just a colon. 170 171 // But having this is too useful to ignore. 172 if(innerSelector.length && innerSelector[0] == ':') 173 levelOne.selectors ~= outerSelector ~ innerSelector; 174 // we also allow \&something to get them concatenated 175 else if(innerSelector.length > 2 && innerSelector[0] == '\\' && innerSelector[1] == '&') 176 levelOne.selectors ~= outerSelector ~ innerSelector[2 .. $].strip; 177 else 178 levelOne.selectors ~= outerSelector ~ " " ~ innerSelector; // otherwise, use some other operator... 179 } 180 } 181 182 foreach(part; contents) { 183 auto set = cast(CssRuleSet) part; 184 if(set is null) 185 levelOne.contents ~= part.clone(); 186 else { 187 // actually gotta de-nest this 188 ret ~= set.deNest(levelOne); 189 } 190 } 191 192 return ret; 193 } 194 195 override string toString() const { 196 string ret; 197 198 bool outputtedSelector = false; 199 foreach(selector; selectors) { 200 if(outputtedSelector) 201 ret ~= ", "; 202 else 203 outputtedSelector = true; 204 205 ret ~= selector; 206 } 207 208 ret ~= " {\n"; 209 foreach(content; contents) { 210 auto str = content.toString(); 211 if(str.length) 212 str = "\t" ~ str.replace("\n", "\n\t") ~ "\n"; 213 214 ret ~= str; 215 } 216 ret ~= "}"; 217 218 return ret; 219 } 220 } 221 222 class CssRule : CssPart { 223 this() {} 224 225 this(ref string css, int endOfStatement) { 226 content = css[0 .. endOfStatement]; 227 if(endOfStatement < css.length && css[endOfStatement] == ';') 228 endOfStatement++; 229 230 css = css[endOfStatement .. $]; 231 } 232 233 // note: does not include the ending semicolon 234 string content; 235 236 override CssRule clone() const { 237 auto n = new CssRule(); 238 n.content = content; 239 return n; 240 } 241 242 override string toString() const { 243 if(strip(content).length == 0) 244 return ""; 245 return content ~ ";"; 246 } 247 } 248 249 CssPart[] lexCss(string css) { 250 import std.regex; 251 // strips comments 252 css = std.regex.replace(css, regex(r"\/\*[^*]*\*+([^/*][^*]*\*+)*\/", "g"), ""); 253 254 CssPart[] ret; 255 css = css.stripLeft(); 256 257 while(css.length > 1) { 258 CssPart p; 259 260 if(css[0] == '@') { 261 p = new CssAtRule(css); 262 } else { 263 // non-at rules can be either rules or sets. 264 // The question is: which comes first, the ';' or the '{' ? 265 266 auto endOfStatement = css.indexOfCssSmart(';'); 267 if(endOfStatement == -1) 268 endOfStatement = css.indexOf("}"); 269 if(endOfStatement == -1) 270 endOfStatement = css.length; 271 272 auto beginningOfBlock = css.indexOf("{"); 273 if(beginningOfBlock == -1 || endOfStatement < beginningOfBlock) 274 p = new CssRule(css, cast(int) endOfStatement); 275 else 276 p = new CssRuleSet(css); 277 } 278 279 assert(p !is null); 280 ret ~= p; 281 282 css = css.stripLeft(); 283 } 284 285 return ret; 286 } 287 288 // This needs to skip characters inside parens or quotes, so it 289 // doesn't trip up on stuff like data uris when looking for a terminating 290 // character. 291 ptrdiff_t indexOfCssSmart(string i, char find) { 292 int parenCount; 293 char quote; 294 bool escaping; 295 foreach(idx, ch; i) { 296 if(escaping) { 297 escaping = false; 298 continue; 299 } 300 if(quote != char.init) { 301 if(ch == quote) 302 quote = char.init; 303 continue; 304 } 305 if(ch == '\'' || ch == '"') { 306 quote = ch; 307 continue; 308 } 309 310 if(ch == '(') 311 parenCount++; 312 313 if(parenCount) { 314 if(ch == ')') 315 parenCount--; 316 continue; 317 } 318 319 // at this point, we are not in parenthesis nor are we in 320 // a quote, so we can actually search for the relevant character 321 322 if(ch == find) 323 return idx; 324 } 325 return -1; 326 } 327 328 string cssToString(in CssPart[] css) { 329 string ret; 330 foreach(c; css) { 331 if(ret.length) { 332 if(ret[$ -1] == '}') 333 ret ~= "\n\n"; 334 else 335 ret ~= "\n"; 336 } 337 ret ~= c.toString(); 338 } 339 340 return ret; 341 } 342 343 /// Translates nested css 344 const(CssPart)[] denestCss(CssPart[] css) { 345 CssPart[] ret; 346 foreach(part; css) { 347 auto at = cast(CssAtRule) part; 348 if(at is null) { 349 auto set = cast(CssRuleSet) part; 350 if(set is null) 351 ret ~= part; 352 else { 353 ret ~= set.deNest(); 354 } 355 } else { 356 // at rules with content may be denested at the top level... 357 // FIXME: is this even right all the time? 358 359 if(at.inner.length) { 360 auto newCss = at.opener ~ "{\n"; 361 362 // the whitespace manipulations are just a crude indentation thing 363 newCss ~= "\t" ~ (cssToString(denestCss(lexCss(at.inner))).replace("\n", "\n\t").replace("\n\t\n\t", "\n\n\t")); 364 365 newCss ~= "\n}"; 366 367 ret ~= new CssAtRule(newCss); 368 } else { 369 ret ~= part; // no inner content, nothing special needed 370 } 371 } 372 } 373 374 return ret; 375 } 376 377 /* 378 Forms: 379 380 ¤var 381 ¤lighten(¤foreground, 0.5) 382 ¤lighten(¤foreground, 0.5); -- exactly one semicolon shows up at the end 383 ¤var(something, something_else) { 384 final argument 385 } 386 387 ¤function { 388 argument 389 } 390 391 392 Possible future: 393 394 Recursive macros: 395 396 ¤define(li) { 397 <li>¤car</li> 398 list(¤cdr) 399 } 400 401 ¤define(list) { 402 ¤li(¤car) 403 } 404 405 406 car and cdr are borrowed from lisp... hmm 407 do i really want to do this... 408 409 410 411 But if the only argument is cdr, and it is empty the function call is cancelled. 412 This lets you do some looping. 413 414 415 hmmm easier would be 416 417 ¤loop(macro_name, args...) { 418 body 419 } 420 421 when you call loop, it calls the macro as many times as it can for the 422 given args, and no more. 423 424 425 426 Note that set is a macro; it doesn't expand it's arguments. 427 To force expansion, use echo (or expand?) on the argument you set. 428 */ 429 430 // Keep in mind that this does not understand comments! 431 class MacroExpander { 432 dstring delegate(dstring[])[dstring] functions; 433 dstring[dstring] variables; 434 435 /// This sets a variable inside the macro system 436 void setValue(string key, string value) { 437 variables[to!dstring(key)] = to!dstring(value); 438 } 439 440 struct Macro { 441 dstring name; 442 dstring[] args; 443 dstring definition; 444 } 445 446 Macro[dstring] macros; 447 448 // FIXME: do I want user defined functions or something? 449 450 this() { 451 functions["get"] = &get; 452 functions["set"] = &set; 453 functions["define"] = &define; 454 functions["loop"] = &loop; 455 456 functions["echo"] = delegate dstring(dstring[] args) { 457 dstring ret; 458 bool outputted; 459 foreach(arg; args) { 460 if(outputted) 461 ret ~= ", "; 462 else 463 outputted = true; 464 ret ~= arg; 465 } 466 467 return ret; 468 }; 469 470 functions["uriEncode"] = delegate dstring(dstring[] args) { 471 return to!dstring(std.uri.encodeComponent(to!string(args[0]))); 472 }; 473 474 functions["test"] = delegate dstring(dstring[] args) { 475 assert(0, to!string(args.length) ~ " args: " ~ to!string(args)); 476 }; 477 478 functions["include"] = &include; 479 } 480 481 string[string] includeFiles; 482 483 dstring include(dstring[] args) { 484 string s; 485 foreach(arg; args) { 486 string lol = to!string(arg); 487 s ~= to!string(includeFiles[lol]); 488 } 489 490 return to!dstring(s); 491 } 492 493 // the following are used inside the user text 494 495 dstring define(dstring[] args) { 496 enforce(args.length > 1, "requires at least a macro name and definition"); 497 498 Macro m; 499 m.name = args[0]; 500 if(args.length > 2) 501 m.args = args[1 .. $ - 1]; 502 m.definition = args[$ - 1]; 503 504 macros[m.name] = m; 505 506 return null; 507 } 508 509 dstring set(dstring[] args) { 510 enforce(args.length == 2, "requires two arguments. got " ~ to!string(args)); 511 variables[args[0]] = args[1]; 512 return ""; 513 } 514 515 dstring get(dstring[] args) { 516 enforce(args.length == 1); 517 if(args[0] !in variables) 518 return ""; 519 return variables[args[0]]; 520 } 521 522 dstring loop(dstring[] args) { 523 enforce(args.length > 1, "must provide a macro name and some arguments"); 524 auto m = macros[args[0]]; 525 args = args[1 .. $]; 526 dstring returned; 527 528 size_t iterations = args.length; 529 if(m.args.length != 0) 530 iterations = (args.length + m.args.length - 1) / m.args.length; 531 532 foreach(i; 0 .. iterations) { 533 returned ~= expandMacro(m, args); 534 if(m.args.length < args.length) 535 args = args[m.args.length .. $]; 536 else 537 args = null; 538 } 539 540 return returned; 541 } 542 543 /// Performs the expansion 544 string expand(string srcutf8) { 545 auto src = expand(to!dstring(srcutf8)); 546 return to!string(src); 547 } 548 549 private int depth = 0; 550 /// ditto 551 dstring expand(dstring src) { 552 return expandImpl(src, null); 553 } 554 555 // FIXME: the order of evaluation shouldn't matter. Any top level sets should be run 556 // before anything is expanded. 557 private dstring expandImpl(dstring src, dstring[dstring] localVariables) { 558 depth ++; 559 if(depth > 10) 560 throw new Exception("too much recursion depth in macro expansion"); 561 562 bool doneWithSetInstructions = false; // this is used to avoid double checks each loop 563 for(;;) { 564 // we do all the sets first since the latest one is supposed to be used site wide. 565 // this allows a later customization to apply to the entire document. 566 auto idx = doneWithSetInstructions ? -1 : src.indexOf("¤set"); 567 if(idx == -1) { 568 doneWithSetInstructions = true; 569 idx = src.indexOf("¤"); 570 } 571 if(idx == -1) { 572 depth--; 573 return src; 574 } 575 576 // the replacement goes 577 // src[0 .. startingSliceForReplacement] ~ new ~ src[endingSliceForReplacement .. $]; 578 sizediff_t startingSliceForReplacement, endingSliceForReplacement; 579 580 dstring functionName; 581 dstring[] arguments; 582 bool addTrailingSemicolon; 583 584 startingSliceForReplacement = idx; 585 // idx++; // because the star in UTF 8 is two characters. FIXME: hack -- not needed thx to dstrings 586 auto possibility = src[idx + 1 .. $]; 587 size_t argsBegin; 588 589 bool found = false; 590 foreach(i, c; possibility) { 591 if(!( 592 // valid identifiers 593 (c >= 'A' && c <= 'Z') 594 || 595 (c >= 'a' && c <= 'z') 596 || 597 (c >= '0' && c <= '9') 598 || 599 c == '_' 600 )) { 601 // not a valid identifier means 602 // we're done reading the name 603 functionName = possibility[0 .. i]; 604 argsBegin = i; 605 found = true; 606 break; 607 } 608 } 609 610 if(!found) { 611 functionName = possibility; 612 argsBegin = possibility.length; 613 } 614 615 auto endOfVariable = argsBegin + idx + 1; // this is the offset into the original source 616 617 bool checkForAllArguments = true; 618 619 moreArguments: 620 621 assert(argsBegin); 622 623 endingSliceForReplacement = argsBegin + idx + 1; 624 625 while( 626 argsBegin < possibility.length && ( 627 possibility[argsBegin] == ' ' || 628 possibility[argsBegin] == '\t' || 629 possibility[argsBegin] == '\n' || 630 possibility[argsBegin] == '\r')) 631 { 632 argsBegin++; 633 } 634 635 if(argsBegin == possibility.length) { 636 endingSliceForReplacement = src.length; 637 goto doReplacement; 638 } 639 640 switch(possibility[argsBegin]) { 641 case '(': 642 if(!checkForAllArguments) 643 goto doReplacement; 644 645 // actually parsing the arguments 646 size_t currentArgumentStarting = argsBegin + 1; 647 648 int open; 649 650 bool inQuotes; 651 bool inTicks; 652 bool justSawBackslash; 653 foreach(i, c; possibility[argsBegin .. $]) { 654 if(c == '`') 655 inTicks = !inTicks; 656 657 if(inTicks) 658 continue; 659 660 if(!justSawBackslash && c == '"') 661 inQuotes = !inQuotes; 662 663 if(c == '\\') 664 justSawBackslash = true; 665 else 666 justSawBackslash = false; 667 668 if(inQuotes) 669 continue; 670 671 if(open == 1 && c == ',') { // don't want to push a nested argument incorrectly... 672 // push the argument 673 arguments ~= possibility[currentArgumentStarting .. i + argsBegin]; 674 currentArgumentStarting = argsBegin + i + 1; 675 } 676 677 if(c == '(') 678 open++; 679 if(c == ')') { 680 open--; 681 if(open == 0) { 682 // push the last argument 683 arguments ~= possibility[currentArgumentStarting .. i + argsBegin]; 684 685 endingSliceForReplacement = argsBegin + idx + 1 + i; 686 argsBegin += i + 1; 687 break; 688 } 689 } 690 } 691 692 // then see if there's a { argument too 693 checkForAllArguments = false; 694 goto moreArguments; 695 case '{': 696 // find the match 697 int open; 698 foreach(i, c; possibility[argsBegin .. $]) { 699 if(c == '{') 700 open ++; 701 if(c == '}') { 702 open --; 703 if(open == 0) { 704 // cutting off the actual braces here 705 arguments ~= possibility[argsBegin + 1 .. i + argsBegin]; 706 // second +1 is there to cut off the } 707 endingSliceForReplacement = argsBegin + idx + 1 + i + 1; 708 709 argsBegin += i + 1; 710 break; 711 } 712 } 713 } 714 715 goto doReplacement; 716 default: 717 goto doReplacement; 718 } 719 720 doReplacement: 721 if(endingSliceForReplacement < src.length && src[endingSliceForReplacement] == ';') { 722 endingSliceForReplacement++; 723 addTrailingSemicolon = true; // don't want a doubled semicolon 724 // FIXME: what if it's just some whitespace after the semicolon? should that be 725 // stripped or no? 726 } 727 728 foreach(ref argument; arguments) { 729 argument = argument.strip(); 730 if(argument.length > 2 && argument[0] == '`' && argument[$-1] == '`') 731 argument = argument[1 .. $ - 1]; // strip ticks here 732 else 733 if(argument.length > 2 && argument[0] == '"' && argument[$-1] == '"') 734 argument = argument[1 .. $ - 1]; // strip quotes here 735 736 // recursive macro expanding 737 // these need raw text, since they expand later. FIXME: should it just be a list of functions? 738 if(functionName != "define" && functionName != "quote" && functionName != "set") 739 argument = this.expandImpl(argument, localVariables); 740 } 741 742 dstring returned = ""; 743 if(functionName in localVariables) { 744 /* 745 if(functionName == "_head") 746 returned = arguments[0]; 747 else if(functionName == "_tail") 748 returned = arguments[1 .. $]; 749 else 750 */ 751 returned = localVariables[functionName]; 752 } else if(functionName in functions) 753 returned = functions[functionName](arguments); 754 else if(functionName in variables) { 755 returned = variables[functionName]; 756 // FIXME 757 // we also need to re-attach the arguments array, since variable pulls can't have args 758 assert(endOfVariable > startingSliceForReplacement); 759 endingSliceForReplacement = endOfVariable; 760 } else if(functionName in macros) { 761 returned = expandMacro(macros[functionName], arguments); 762 } 763 764 if(addTrailingSemicolon && returned.length > 1 && returned[$ - 1] != ';') 765 returned ~= ";"; 766 767 src = src[0 .. startingSliceForReplacement] ~ returned ~ src[endingSliceForReplacement .. $]; 768 } 769 assert(0); // not reached 770 } 771 772 dstring expandMacro(Macro m, dstring[] arguments) { 773 dstring[dstring] locals; 774 foreach(i, arg; m.args) { 775 if(i == arguments.length) 776 break; 777 locals[arg] = arguments[i]; 778 } 779 780 return this.expandImpl(m.definition, locals); 781 } 782 } 783 784 785 class CssMacroExpander : MacroExpander { 786 this() { 787 super(); 788 789 functions["prefixed"] = &prefixed; 790 791 functions["lighten"] = &(colorFunctionWrapper!lighten); 792 functions["darken"] = &(colorFunctionWrapper!darken); 793 functions["moderate"] = &(colorFunctionWrapper!moderate); 794 functions["extremify"] = &(colorFunctionWrapper!extremify); 795 functions["makeTextColor"] = &(oneArgColorFunctionWrapper!makeTextColor); 796 797 functions["oppositeLightness"] = &(oneArgColorFunctionWrapper!oppositeLightness); 798 799 functions["rotateHue"] = &(colorFunctionWrapper!rotateHue); 800 801 functions["saturate"] = &(colorFunctionWrapper!saturate); 802 functions["desaturate"] = &(colorFunctionWrapper!desaturate); 803 804 functions["setHue"] = &(colorFunctionWrapper!setHue); 805 functions["setSaturation"] = &(colorFunctionWrapper!setSaturation); 806 functions["setLightness"] = &(colorFunctionWrapper!setLightness); 807 } 808 809 // prefixed(border-radius: 12px); 810 dstring prefixed(dstring[] args) { 811 dstring ret; 812 foreach(prefix; ["-moz-"d, "-webkit-"d, "-o-"d, "-ms-"d, "-khtml-"d, ""d]) 813 ret ~= prefix ~ args[0] ~ ";"; 814 return ret; 815 } 816 817 /// Runs the macro expansion but then a CSS densesting 818 string expandAndDenest(string cssSrc) { 819 return cssToString(denestCss(lexCss(this.expand(cssSrc)))); 820 } 821 822 // internal things 823 dstring colorFunctionWrapper(alias func)(dstring[] args) { 824 auto color = readCssColor(to!string(args[0])); 825 auto percentage = readCssNumber(args[1]); 826 return "#"d ~ to!dstring(func(color, percentage).toString()); 827 } 828 829 dstring oneArgColorFunctionWrapper(alias func)(dstring[] args) { 830 auto color = readCssColor(to!string(args[0])); 831 return "#"d ~ to!dstring(func(color).toString()); 832 } 833 } 834 835 836 real readCssNumber(dstring s) { 837 s = s.replace(" "d, ""d); 838 if(s.length == 0) 839 return 0; 840 if(s[$-1] == '%') 841 return (to!real(s[0 .. $-1]) / 100f); 842 return to!real(s); 843 } 844 845 import std.format; 846 847 class JavascriptMacroExpander : MacroExpander { 848 this() { 849 super(); 850 functions["foreach"] = &foreachLoop; 851 } 852 853 854 /** 855 ¤foreach(item; array) { 856 // code 857 } 858 859 so arg0 .. argn-1 is the stuff inside. Conc 860 */ 861 862 int foreachLoopCounter; 863 dstring foreachLoop(dstring[] args) { 864 enforce(args.length >= 2, "foreach needs parens and code"); 865 dstring parens; 866 bool outputted = false; 867 foreach(arg; args[0 .. $ - 1]) { 868 if(outputted) 869 parens ~= ", "; 870 else 871 outputted = true; 872 parens ~= arg; 873 } 874 875 dstring variableName, arrayName; 876 877 auto it = parens.split(";"); 878 variableName = it[0].strip; 879 arrayName = it[1].strip; 880 881 dstring insideCode = args[$-1]; 882 883 dstring iteratorName; 884 iteratorName = "arsd_foreach_loop_counter_"d ~ to!dstring(++foreachLoopCounter); 885 dstring temporaryName = "arsd_foreach_loop_temporary_"d ~ to!dstring(++foreachLoopCounter); 886 887 auto writer = appender!dstring(); 888 889 formattedWrite(writer, " 890 var %2$s = %5$s; 891 if(%2$s != null) 892 for(var %1$s = 0; %1$s < %2$s.length; %1$s++) { 893 var %3$s = %2$s[%1$s]; 894 %4$s 895 }"d, iteratorName, temporaryName, variableName, insideCode, arrayName); 896 897 auto code = writer.data; 898 899 return to!dstring(code); 900 } 901 } 902 903 string beautifyCss(string css) { 904 css = css.replace(":", ": "); 905 css = css.replace(": ", ": "); 906 css = css.replace("{", " {\n\t"); 907 css = css.replace(";", ";\n\t"); 908 css = css.replace("\t}", "}\n\n"); 909 return css.strip; 910 } 911 912 int fromHex(string s) { 913 int result = 0; 914 915 int exp = 1; 916 foreach(c; retro(s)) { 917 if(c >= 'A' && c <= 'F') 918 result += exp * (c - 'A' + 10); 919 else if(c >= 'a' && c <= 'f') 920 result += exp * (c - 'a' + 10); 921 else if(c >= '0' && c <= '9') 922 result += exp * (c - '0'); 923 else 924 throw new Exception("invalid hex character: " ~ cast(char) c); 925 926 exp *= 16; 927 } 928 929 return result; 930 } 931 932 Color readCssColor(string cssColor) { 933 cssColor = cssColor.strip().toLower(); 934 935 if(cssColor.startsWith("#")) { 936 cssColor = cssColor[1 .. $]; 937 if(cssColor.length == 3) { 938 cssColor = "" ~ cssColor[0] ~ cssColor[0] 939 ~ cssColor[1] ~ cssColor[1] 940 ~ cssColor[2] ~ cssColor[2]; 941 } 942 943 if(cssColor.length == 6) 944 cssColor ~= "ff"; 945 946 /* my extension is to do alpha */ 947 if(cssColor.length == 8) { 948 return Color( 949 fromHex(cssColor[0 .. 2]), 950 fromHex(cssColor[2 .. 4]), 951 fromHex(cssColor[4 .. 6]), 952 fromHex(cssColor[6 .. 8])); 953 } else 954 throw new Exception("invalid color " ~ cssColor); 955 } else if(cssColor.startsWith("rgba")) { 956 assert(0); // FIXME: implement 957 /* 958 cssColor = cssColor.replace("rgba", ""); 959 cssColor = cssColor.replace(" ", ""); 960 cssColor = cssColor.replace("(", ""); 961 cssColor = cssColor.replace(")", ""); 962 963 auto parts = cssColor.split(","); 964 */ 965 } else if(cssColor.startsWith("rgb")) { 966 assert(0); // FIXME: implement 967 } else if(cssColor.startsWith("hsl")) { 968 assert(0); // FIXME: implement 969 } else 970 return Color.fromNameString(cssColor); 971 /* 972 switch(cssColor) { 973 default: 974 // FIXME let's go ahead and try naked hex for compatibility with my gradient program 975 assert(0, "Unknown color: " ~ cssColor); 976 } 977 */ 978 } 979 980 /* 981 Copyright: Adam D. Ruppe, 2010 - 2015 982 License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>. 983 Authors: Adam D. Ruppe, with contributions by Nick Sabalausky and Trass3r 984 985 Copyright Adam D. Ruppe 2010-2015. 986 Distributed under the Boost Software License, Version 1.0. 987 (See accompanying file LICENSE_1_0.txt or copy at 988 http://www.boost.org/LICENSE_1_0.txt) 989 */