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 */