The OpenD Programming Language

1 /**
2 	This module includes functions to work with HTML and CSS in a more specialized manner than [arsd.dom]. Most of this is obsolete from my really old D web stuff, but there's still some useful stuff. View source before you decide to use it, as the implementations may suck more than you want to use.
3 
4 	It publically imports the DOM module to get started.
5 	Then it adds a number of functions to enhance html
6 	DOM documents and make other changes, like scripts
7 	and stylesheets.
8 */
9 module arsd.html;
10 
11 import arsd.core : encodeUriComponent;
12 
13 public import arsd.dom;
14 import arsd.color;
15 
16 import std.array;
17 import std.string;
18 import std.variant;
19 import core.vararg;
20 import std.exception;
21 
22 
23 /// This is a list of features you can allow when using the sanitizedHtml function.
24 enum HtmlFeatures : uint {
25 	images = 1, 	/// The <img> tag
26 	links = 2, 	/// <a href=""> tags
27 	css = 4, 	/// Inline CSS
28 	cssLinkedResources = 8, // FIXME: implement this
29 	video = 16, 	/// The html5 <video> tag. autoplay is always stripped out.
30 	audio = 32, 	/// The html5 <audio> tag. autoplay is always stripped out.
31 	objects = 64, 	/// The <object> tag, which can link to many things, including Flash.
32 	iframes = 128, 	/// The <iframe> tag. sandbox and restrict attributes are always added.
33 	classes = 256, 	/// The class="" attribute
34 	forms = 512, 	/// HTML forms
35 }
36 
37 /// The things to allow in links, images, css, and aother urls.
38 /// FIXME: implement this for better flexibility
39 enum UriFeatures : uint {
40 	http, 		/// http:// protocol absolute links
41 	https, 		/// https:// protocol absolute links
42 	data, 		/// data: url links to embed content. On some browsers (old Firefoxes) this was a security concern.
43 	ftp, 		/// ftp:// protocol links
44 	relative, 	/// relative links to the current location. You might want to rebase them.
45 	anchors 	/// #anchor links
46 }
47 
48 string[] htmlTagWhitelist = [
49 	"span", "div",
50 	"p", "br",
51 	"b", "i", "u", "s", "big", "small", "sub", "sup", "strong", "em", "tt", "blockquote", "cite", "ins", "del", "strike",
52 	"ol", "ul", "li", "dl", "dt", "dd",
53 	"q",
54 	"table", "caption", "tr", "td", "th", "col", "thead", "tbody", "tfoot",
55 	"hr",
56 	"h1", "h2", "h3", "h4", "h5", "h6",
57 	"abbr",
58 
59 	"img", "object", "audio", "video", "a", "source", // note that these usually *are* stripped out - see HtmlFeatures-  but this lets them get into stage 2
60 
61 	"form", "input", "textarea", "legend", "fieldset", "label", // ditto, but with HtmlFeatures.forms
62 	// style isn't here
63 ];
64 
65 string[] htmlAttributeWhitelist = [
66 	// style isn't here
67 		/*
68 		if style, expression must be killed
69 		all urls must be checked for javascript and/or vbscript
70 		imports must be killed
71 		*/
72 	"style",
73 
74 	"colspan", "rowspan",
75 	"title", "alt", "class",
76 
77 	"href", "src", "type", "name",
78 	"id",
79 	"method", "enctype", "value", "type", // for forms only FIXME
80 
81 	"align", "valign", "width", "height",
82 ];
83 
84 /// This returns an element wrapping sanitized content, using a whitelist for html tags and attributes,
85 /// and a blacklist for css. Javascript is never allowed.
86 ///
87 /// It scans all URLs it allows and rejects
88 ///
89 /// You can tweak the allowed features with the HtmlFeatures enum.
90 ///
91 /// Note: you might want to use innerText for most user content. This is meant if you want to
92 /// give them a big section of rich text.
93 ///
94 /// userContent should just be a basic div, holding the user's actual content.
95 ///
96 /// FIXME: finish writing this
97 Element sanitizedHtml(/*in*/ Element userContent, string idPrefix = null, HtmlFeatures allow = HtmlFeatures.links | HtmlFeatures.images | HtmlFeatures.css) {
98 	auto div = Element.make("div");
99 	div.addClass("sanitized user-content");
100 
101 	auto content = div.appendChild(userContent.cloned);
102 	startOver:
103 	foreach(e; content.tree) {
104 		if(e.nodeType == NodeType.Text)
105 			continue; // text nodes are always fine.
106 
107 		e.tagName = e.tagName.toLower(); // normalize tag names...
108 
109 		if(!(e.tagName.isInArray(htmlTagWhitelist))) {
110 			e.stripOut;
111 			goto startOver;
112 		}
113 
114 		if((!(allow & HtmlFeatures.links) && e.tagName == "a")) {
115 			e.stripOut;
116 			goto startOver;
117 		}
118 
119 		if((!(allow & HtmlFeatures.video) && e.tagName == "video")
120 		  ||(!(allow & HtmlFeatures.audio) && e.tagName == "audio")
121 		  ||(!(allow & HtmlFeatures.objects) && e.tagName == "object")
122 		  ||(!(allow & HtmlFeatures.iframes) && e.tagName == "iframe")
123 		  ||(!(allow & HtmlFeatures.forms) && (
124 		  	e.tagName == "form" ||
125 		  	e.tagName == "input" ||
126 		  	e.tagName == "textarea" ||
127 		  	e.tagName == "label" ||
128 		  	e.tagName == "fieldset" ||
129 		  	e.tagName == "legend"
130 			))
131 		) {
132 			e.innerText = e.innerText; // strips out non-text children
133 			e.stripOut;
134 			goto startOver;
135 		}
136 
137 		if(e.tagName == "source" && (e.parentNode is null || e.parentNode.tagName != "video" || e.parentNode.tagName != "audio")) {
138 			// source is only allowed in the HTML5 media elements
139 			e.stripOut;
140 			goto startOver;
141 		}
142 
143 		if(!(allow & HtmlFeatures.images) && e.tagName == "img") {
144 			e.replaceWith(new TextNode(null, e.alt));
145 			continue; // images not allowed are replaced with their alt text
146 		}
147 
148 		foreach(k, v; e.attributes) {
149 			e.removeAttribute(k);
150 			k = k.toLower();
151 			if(!(k.isInArray(htmlAttributeWhitelist))) {
152 				// not allowed, don't put it back
153 				// this space is intentionally left blank
154 			} else {
155 				// it's allowed but let's make sure it's completely valid
156 				if(k == "class" && (allow & HtmlFeatures.classes)) {
157 					e.setAttribute("class", v);
158 				} else if(k == "id") {
159 					if(idPrefix !is null)
160 						e.setAttribute(k, idPrefix ~ v);
161 					// otherwise, don't allow user IDs
162 				} else if(k == "style") {
163 					if(allow & HtmlFeatures.css) {
164 						e.setAttribute(k, sanitizeCss(v));
165 					}
166 				} else if(k == "href" || k == "src") {
167 					e.setAttribute(k, sanitizeUrl(v));
168 				} else
169 					e.setAttribute(k, v); // allowed attribute
170 			}
171 		}
172 
173 		if(e.tagName == "iframe") {
174 			// some additional restrictions for supported browsers
175 			e.attrs.security = "restricted";
176 			e.attrs.sandbox = "";
177 		}
178 	}
179 	return div;
180 }
181 
182 ///
183 Element sanitizedHtml(in Html userContent, string idPrefix = null, HtmlFeatures allow = HtmlFeatures.links | HtmlFeatures.images | HtmlFeatures.css) {
184 	auto div = Element.make("div");
185 	div.innerHTML = userContent.source;
186 	return sanitizedHtml(div, idPrefix, allow);
187 }
188 
189 string sanitizeCss(string css) {
190 	// FIXME: do a proper whitelist here; I should probably bring in the parser from html.d
191 	// FIXME: sanitize urls inside too
192 	return css.replace("expression", "");
193 }
194 
195 ///
196 string sanitizeUrl(string url) {
197 	// FIXME: support other options; this is more restrictive than it has to be
198 	if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//"))
199 		return url;
200 	return null;
201 }
202 
203 /// This is some basic CSS I suggest you copy/paste into your stylesheet
204 /// if you use the sanitizedHtml function.
205 string recommendedBasicCssForUserContent = `
206 	.sanitized.user-content {
207 		position: relative;
208 		overflow: hidden;
209 	}
210 
211 	.sanitized.user-content * {
212 		max-width: 100%;
213 		max-height: 100%;
214 	}
215 `;
216 
217 /++
218 	Given arbitrary user input, find links and add `<a href>` wrappers, otherwise just escaping the rest of it for HTML display.
219 +/
220 Html linkify(string text) {
221 	auto div = Element.make("div");
222 
223 	while(text.length) {
224 		auto idx = text.indexOf("http");
225 		if(idx == -1) {
226 			idx = text.length;
227 		}
228 
229 		div.appendText(text[0 .. idx]);
230 		text = text[idx .. $];
231 
232 		if(text.length) {
233 			// where does it end? whitespace I guess
234 			auto idxSpace = text.indexOf(" ");
235 			if(idxSpace == -1) idxSpace = text.length;
236 			auto idxLine = text.indexOf("\n");
237 			if(idxLine == -1) idxLine = text.length;
238 
239 
240 			auto idxEnd = idxSpace < idxLine ? idxSpace : idxLine;
241 
242 			auto link = text[0 .. idxEnd];
243 			text = text[idxEnd .. $];
244 
245 			div.addChild("a", link, link);
246 		}
247 	}
248 
249 	return Html(div.innerHTML);
250 }
251 
252 /// Given existing encoded HTML, turns \n\n into `<p>`.
253 Html paragraphsToP(Html html) {
254 	auto text = html.source;
255 	string total;
256 	foreach(p; text.split("\n\n")) {
257 		total ~= "<p>";
258 		auto lines = p.splitLines;
259 		foreach(idx, line; lines)
260 			if(line.strip.length) {
261 				total ~= line;
262 				if(idx != lines.length - 1)
263 					total ~= "<br />";
264 			}
265 		total ~= "</p>";
266 	}
267 	return Html(total);
268 }
269 
270 /// Given user text, converts newlines to `<br>` and encodes the rest.
271 Html nl2br(string text) {
272 	auto div = Element.make("div");
273 
274 	bool first = true;
275 	foreach(line; splitLines(text)) {
276 		if(!first)
277 			div.addChild("br");
278 		else
279 			first = false;
280 		div.appendText(line);
281 	}
282 
283 	return Html(div.innerHTML);
284 }
285 
286 /// Returns true of the string appears to be html/xml - if it matches the pattern
287 /// for tags or entities.
288 bool appearsToBeHtml(string src) {
289 	import std.regex;
290 	return cast(bool) match(src, `.*\<[A-Za-z]+>.*`);
291 }
292 
293 /// Get the favicon out of a document, or return the default a browser would attempt if it isn't there.
294 string favicon(Document document) {
295 	auto item = document.querySelector("link[rel~=icon]");
296 	if(item !is null)
297 		return item.href;
298 	return "/favicon.ico"; // it pisses me off that the fucking browsers do this.... but they do, so I will too.
299 }
300 
301 ///
302 Element checkbox(string name, string value, string label, bool checked = false) {
303 	auto lbl = Element.make("label");
304 	auto input = lbl.addChild("input");
305 	input.type = "checkbox";
306 	input.name = name;
307 	input.value = value;
308 	if(checked)
309 		input.checked = "checked";
310 
311 	lbl.appendText(" ");
312 	lbl.addChild("span", label);
313 
314 	return lbl;
315 }
316 
317 /++ Convenience function to create a small <form> to POST, but the creation function is more like a link
318     than a DOM form.
319 
320     The idea is if you have a link to a page which needs to be changed since it is now taking an action,
321     this should provide an easy way to do it.
322 
323     You might want to style these with css. The form these functions create has no class - use regular
324     dom functions to add one. When styling, hit the form itself and form > [type=submit]. (That will
325     cover both input[type=submit] and button[type=submit] - the two possibilities the functions may create.)
326 
327     Param:
328     	href: the link. Query params (if present) are converted into hidden form inputs and the rest is used as the form action
329 	innerText: the text to show on the submit button
330 	params: additional parameters for the form
331 +/
332 Form makePostLink(string href, string innerText, string[string] params = null) {
333 	auto submit = Element.make("input");
334 	submit.type = "submit";
335 	submit.value = innerText;
336 
337 	return makePostLink_impl(href, params, submit);
338 }
339 
340 /// Similar to the above, but lets you pass HTML rather than just text. It puts the html inside a <button type="submit"> element.
341 ///
342 /// Using html strings imo generally sucks. I recommend you use plain text or structured Elements instead most the time.
343 Form makePostLink(string href, Html innerHtml, string[string] params = null) {
344 	auto submit = Element.make("button");
345 	submit.type = "submit";
346 	submit.innerHTML = innerHtml;
347 
348 	return makePostLink_impl(href, params, submit);
349 }
350 
351 /// Like the Html overload, this uses a <button> tag to get fancier with the submit button. The element you pass is appended to the submit button.
352 Form makePostLink(string href, Element submitButtonContents, string[string] params = null) {
353 	auto submit = Element.make("button");
354 	submit.type = "submit";
355 	submit.appendChild(submitButtonContents);
356 
357 	return makePostLink_impl(href, params, submit);
358 }
359 
360 import arsd.cgi;
361 import std.range;
362 
363 Form makePostLink_impl(string href, string[string] params, Element submitButton) {
364 	auto form = require!Form(Element.make("form"));
365 	form.method = "POST";
366 
367 	auto idx = href.indexOf("?");
368 	if(idx == -1) {
369 		form.action = href;
370 	} else {
371 		form.action = href[0 .. idx];
372 		foreach(k, arr; decodeVariables(href[idx + 1 .. $]))
373 			form.addValueArray(k, arr);
374 	}
375 
376 	foreach(k, v; params)
377 		form.setValue(k, v);
378 
379 	form.appendChild(submitButton);
380 
381 	return form;
382 }
383 
384 /++ Given an existing link, create a POST item from it.
385     You can use this to do something like:
386 
387     auto e = document.requireSelector("a.should-be-post"); // get my link from the dom
388     e.replaceWith(makePostLink(e)); // replace the link with a nice POST form that otherwise does the same thing
389 
390     It passes all attributes of the link on to the form, though I could be convinced to put some on the submit button instead.
391 ++/
392 Form makePostLink(Element link) {
393 	Form form;
394 	if(link.childNodes.length == 1) {
395 		auto fc = link.firstChild;
396 		if(fc.nodeType == NodeType.Text)
397 			form = makePostLink(link.href, fc.nodeValue);
398 		else
399 			form = makePostLink(link.href, fc);
400 	} else {
401 		form = makePostLink(link.href, Html(link.innerHTML));
402 	}
403 
404 	assert(form !is null);
405 
406 	// auto submitButton = form.requireSelector("[type=submit]");
407 
408 	foreach(k, v; link.attributes) {
409 		if(k == "href" || k == "action" || k == "method")
410 			continue;
411 
412 		form.setAttribute(k, v); // carries on class, events, etc. to the form.
413 	}
414 
415 	return form;
416 }
417 
418 /// Translates validate="" tags to inline javascript. "this" is the thing
419 /// being checked.
420 void translateValidation(Document document) {
421 	int count;
422 	foreach(f; document.getElementsByTagName("form")) {
423 	count++;
424 		string formValidation = "";
425 		string fid = f.getAttribute("id");
426 		if(fid is null) {
427 			fid = "automatic-form-" ~ to!string(count);
428 			f.setAttribute("id", "automatic-form-" ~ to!string(count));
429 		}
430 		foreach(i; f.tree) {
431 			if(i.tagName != "input" && i.tagName != "select")
432 				continue;
433 			if(i.getAttribute("id") is null)
434 				i.id = "form-input-" ~ i.name;
435 			auto validate = i.getAttribute("validate");
436 			if(validate is null)
437 				continue;
438 
439 			auto valmsg = i.getAttribute("validate-message");
440 			if(valmsg !is null) {
441 				i.removeAttribute("validate-message");
442 				valmsg ~= `\n`;
443 			}
444 
445 			string valThis = `
446 			var currentField = elements['`~i.name~`'];
447 			if(!(`~validate.replace("this", "currentField")~`)) {
448 						currentField.style.backgroundColor = '#ffcccc';
449 						if(typeof failedMessage != 'undefined')
450 							failedMessage += '`~valmsg~`';
451 						if(failed == null) {
452 							failed = currentField;
453 						}
454 						if('`~valmsg~`' != '') {
455 							var msgId = '`~i.name~`-valmsg';
456 							var msgHolder = document.getElementById(msgId);
457 							if(!msgHolder) {
458 								msgHolder = document.createElement('div');
459 								msgHolder.className = 'validation-message';
460 								msgHolder.id = msgId;
461 
462 								msgHolder.innerHTML = '<br />';
463 								msgHolder.appendChild(document.createTextNode('`~valmsg~`'));
464 
465 								var ele = currentField;
466 								ele.parentNode.appendChild(msgHolder);
467 							}
468 						}
469 					} else {
470 						currentField.style.backgroundColor = '#ffffff';
471 						var msgId = '`~i.name~`-valmsg';
472 						var msgHolder = document.getElementById(msgId);
473 						if(msgHolder)
474 							msgHolder.innerHTML = '';
475 					}`;
476 
477 			formValidation ~= valThis;
478 
479 			string oldOnBlur = i.getAttribute("onblur");
480 			i.setAttribute("onblur", `
481 				var form = document.getElementById('`~fid~`');
482 				var failed = null;
483 				with(form) { `~valThis~` }
484 			` ~ oldOnBlur);
485 
486 			i.removeAttribute("validate");
487 		}
488 
489 		if(formValidation != "") {
490 			auto os = f.getAttribute("onsubmit");
491 			f.attrs.onsubmit = `var failed = null; var failedMessage = ''; with(this) { ` ~ formValidation ~ '\n' ~ ` if(failed != null) { alert('Please complete all required fields.\n' + failedMessage); failed.focus(); return false; } `~os~` return true; }`;
492 		}
493 	}
494 }
495 
496 /// makes input[type=date] to call displayDatePicker with a button
497 void translateDateInputs(Document document) {
498 	foreach(e; document.getElementsByTagName("input")) {
499 		auto type = e.getAttribute("type");
500 		if(type is null) continue;
501 		if(type == "date") {
502 			auto name = e.getAttribute("name");
503 			assert(name !is null);
504 			auto button = document.createElement("button");
505 			button.type = "button";
506 			button.attrs.onclick = "displayDatePicker('"~name~"');";
507 			button.innerText = "Choose...";
508 			e.parentNode.insertChildAfter(button, e);
509 
510 			e.type = "text";
511 			e.setAttribute("class", "date");
512 		}
513 	}
514 }
515 
516 /// finds class="striped" and adds class="odd"/class="even" to the relevant
517 /// children
518 void translateStriping(Document document) {
519 	foreach(item; document.querySelectorAll(".striped")) {
520 		bool odd = false;
521 		string selector;
522 		switch(item.tagName) {
523 			case "ul":
524 			case "ol":
525 				selector = "> li";
526 			break;
527 			case "table":
528 				selector = "> tbody > tr";
529 			break;
530 			case "tbody":
531 				selector = "> tr";
532 			break;
533 			default:
534 		 		selector = "> *";
535 		}
536 		foreach(e; item.getElementsBySelector(selector)) {
537 			if(odd)
538 				e.addClass("odd");
539 			else
540 				e.addClass("even");
541 
542 			odd = !odd;
543 		}
544 	}
545 }
546 
547 /// tries to make an input to filter a list. it kinda sucks.
548 void translateFiltering(Document document) {
549 	foreach(e; document.querySelectorAll("input[filter_what]")) {
550 		auto filterWhat = e.attrs.filter_what;
551 		if(filterWhat[0] == '#')
552 			filterWhat = filterWhat[1..$];
553 
554 		auto fw = document.getElementById(filterWhat);
555 		assert(fw !is null);
556 
557 		foreach(a; fw.getElementsBySelector(e.attrs.filter_by)) {
558 			a.addClass("filterable_content");
559 		}
560 
561 		e.removeAttribute("filter_what");
562 		e.removeAttribute("filter_by");
563 
564 		e.attrs.onkeydown = e.attrs.onkeyup = `
565 			var value = this.value;
566 			var a = document.getElementById("`~filterWhat~`");
567 			var children = a.childNodes;
568 			for(var b = 0; b < children.length; b++) {
569 				var child = children[b];
570 				if(child.nodeType != 1)
571 					continue;
572 
573 				var spans = child.getElementsByTagName('span'); // FIXME
574 				for(var i = 0; i < spans.length; i++) {
575 					var span = spans[i];
576 					if(hasClass(span, "filterable_content")) {
577 						if(value.length && span.innerHTML.match(RegExp(value, "i"))) { // FIXME
578 							addClass(child, "good-match");
579 							removeClass(child, "bad-match");
580 							//if(!got) {
581 							//	holder.scrollTop = child.offsetTop;
582 							//	got = true;
583 							//}
584 						} else {
585 							removeClass(child, "good-match");
586 							if(value.length)
587 								addClass(child, "bad-match");
588 							else
589 								removeClass(child, "bad-match");
590 						}
591 					}
592 				}
593 			}
594 		`;
595 	}
596 }
597 
598 enum TextWrapperWhitespaceBehavior {
599 	wrap,
600 	ignore,
601 	stripOut
602 }
603 
604 /// This wraps every non-empty text mode in the document body with
605 /// <t:t></t:t>, and sets an xmlns:t to the html root.
606 ///
607 /// If you use it, be sure it's the last thing you do before
608 /// calling toString
609 ///
610 /// Why would you want this? Because CSS sucks. If it had a
611 /// :text pseudoclass, we'd be right in business, but it doesn't
612 /// so we'll hack it with this custom tag.
613 ///
614 /// It's in an xml namespace so it should affect or be affected by
615 /// your existing code, while maintaining excellent browser support.
616 ///
617 /// To style it, use myelement > t\:t { style here } in your css.
618 ///
619 /// Note: this can break the css adjacent sibling selector, first-child,
620 /// and other structural selectors. For example, if you write
621 /// <span>hello</span> <span>world</span>, normally, css span + span would
622 /// select "world". But, if you call wrapTextNodes, there's a <t:t> in the
623 /// middle.... so now it no longer matches.
624 ///
625 /// Of course, it can also have an effect on your javascript, especially,
626 /// again, when working with siblings or firstChild, etc.
627 ///
628 /// You must handle all this yourself, which may limit the usefulness of this
629 /// function.
630 ///
631 /// The second parameter, whatToDoWithWhitespaceNodes, tries to mitigate
632 /// this somewhat by giving you some options about what to do with text
633 /// nodes that consist of nothing but whitespace.
634 ///
635 /// You can: wrap them, like all other text nodes, you can ignore
636 /// them entirely, leaving them unwrapped, and in the document normally,
637 /// or you can use stripOut to remove them from the document.
638 ///
639 /// Beware with stripOut: <span>you</span> <span>rock</span> -- that space
640 /// between the spans is a text node of nothing but whitespace, so it would
641 /// be stripped out - probably not what you want!
642 ///
643 /// ignore is the default, since this should break the least of your
644 /// expectations with document structure, while still letting you use this
645 /// function.
646 void wrapTextNodes(Document document, TextWrapperWhitespaceBehavior whatToDoWithWhitespaceNodes = TextWrapperWhitespaceBehavior.ignore) {
647 	enum ourNamespace = "t";
648 	enum ourTag = ourNamespace ~ ":t";
649 	document.root.setAttribute("xmlns:" ~ ourNamespace, null);
650 	foreach(e; document.mainBody.tree) {
651 		if(e.tagName == "script")
652 			continue;
653 		if(e.nodeType != NodeType.Text)
654 			continue;
655 		auto tn = cast(TextNode) e;
656 		if(tn is null)
657 			continue;
658 
659 		if(tn.contents.length == 0)
660 			continue;
661 
662 		if(tn.parentNode !is null
663 			&& tn.parentNode.tagName == ourTag)
664 		{
665 			// this is just a sanity check to make sure
666 			// we don't double wrap anything
667 			continue;
668 		}
669 
670 		final switch(whatToDoWithWhitespaceNodes) {
671 			case TextWrapperWhitespaceBehavior.wrap:
672 				break; // treat it like all other text
673 			case TextWrapperWhitespaceBehavior.stripOut:
674 				// if it's actually whitespace...
675 				if(tn.contents.strip().length == 0) {
676 					tn.removeFromTree();
677 					continue;
678 				}
679 			break;
680 			case TextWrapperWhitespaceBehavior.ignore:
681 				// if it's actually whitespace...
682 				if(tn.contents.strip().length == 0)
683 					continue;
684 		}
685 
686 		tn.replaceWith(Element.make(ourTag, tn.contents));
687 	}
688 }
689 
690 
691 void translateInputTitles(Document document) {
692 	translateInputTitles(document.root);
693 }
694 
695 /// find <input> elements with a title. Make the title the default internal content
696 void translateInputTitles(Element rootElement) {
697 	foreach(form; rootElement.getElementsByTagName("form")) {
698 		string os;
699 		foreach(e; form.getElementsBySelector("input[type=text][title], input[type=email][title], textarea[title]")) {
700 			if(e.hasClass("has-placeholder"))
701 				continue;
702 			e.addClass("has-placeholder");
703 			e.attrs.onfocus = e.attrs.onfocus ~ `
704 				removeClass(this, 'default');
705 				if(this.value == this.getAttribute('title'))
706 					this.value = '';
707 			`;
708 
709 			e.attrs.onblur = e.attrs.onblur ~ `
710 				if(this.value == '') {
711 					addClass(this, 'default');
712 					this.value = this.getAttribute('title');
713 				}
714 			`;
715 
716 			os ~= `
717 				temporaryItem = this.elements["`~e.name~`"];
718 				if(temporaryItem.value == temporaryItem.getAttribute('title'))
719 					temporaryItem.value = '';
720 			`;
721 
722 			if(e.tagName == "input") {
723 				if(e.value == "") {
724 					e.attrs.value = e.attrs.title;
725 					e.addClass("default");
726 				}
727 			} else {
728 				if(e.innerText.length == 0) {
729 					e.innerText = e.attrs.title;
730 					e.addClass("default");
731 				}
732 			}
733 		}
734 
735 		form.attrs.onsubmit = os ~ form.attrs.onsubmit;
736 	}
737 }
738 
739 
740 /// Adds some script to run onload
741 /// FIXME: not implemented
742 void addOnLoad(Document document) {
743 
744 }
745 
746 
747 
748 
749 
750 
751 mixin template opDispatches(R) {
752 	auto opDispatch(string fieldName)(...) {
753 		if(_arguments.length == 0) {
754 			// a zero argument function call OR a getter....
755 			// we can't tell which for certain, so assume getter
756 			// since they can always use the call method on the returned
757 			// variable
758 			static if(is(R == Variable)) {
759 				auto v = *(new Variable(name ~ "." ~ fieldName, group));
760 			} else {
761 				auto v = *(new Variable(fieldName, vars));
762 			}
763 			return v;
764 		} else {
765 			// we have some kind of assignment, but no help from the
766 			// compiler to get the type of assignment...
767 
768 			// FIXME: once Variant is able to handle this, use it!
769 			static if(is(R == Variable)) {
770 				auto v = *(new Variable(this.name ~ "." ~ name, group));
771 			} else
772 				auto v = *(new Variable(fieldName, vars));
773 
774 			string attempt(string type) {
775 				return `if(_arguments[0] == typeid(`~type~`)) v = va_arg!(`~type~`)(_argptr);`;
776 			}
777 
778 			mixin(attempt("int"));
779 			mixin(attempt("string"));
780 			mixin(attempt("double"));
781 			mixin(attempt("Element"));
782 			mixin(attempt("ClientSideScript.Variable"));
783 			mixin(attempt("real"));
784 			mixin(attempt("long"));
785 
786 			return v;
787 		}
788 	}
789 
790 	auto opDispatch(string fieldName, T...)(T t) if(T.length != 0) {
791 		static if(is(R == Variable)) {
792 			auto tmp = group.codes.pop;
793 			scope(exit) group.codes.push(tmp);
794 			return *(new Variable(callFunction(name ~ "." ~ fieldName, t).toString[1..$-2], group)); // cut off the ending ;\n
795 		} else {
796 			return *(new Variable(callFunction(fieldName, t).toString, vars));
797 		}
798 	}
799 
800 
801 }
802 
803 
804 
805 /**
806 	This wraps up a bunch of javascript magic. It doesn't
807 	actually parse or run it - it just collects it for
808 	attachment to a DOM document.
809 
810 	When it returns a variable, it returns it as a string
811 	suitable for output into Javascript source.
812 
813 
814 	auto js = new ClientSideScript;
815 
816 	js.myvariable = 10;
817 
818 	js.somefunction = ClientSideScript.Function(
819 
820 
821 	js.block = {
822 		js.alert("hello");
823 		auto a = "asds";
824 
825 		js.alert(a, js.somevar);
826 	};
827 
828 	Translates into javascript:
829 		alert("hello");
830 		alert("asds", somevar);
831 
832 
833 	The passed code is evaluated lazily.
834 */
835 
836 /+
837 class ClientSideScript : Element {
838 	private Stack!(string*) codes;
839 	this(Document par) {
840 		codes = new Stack!(string*);
841 		vars = new VariablesGroup;
842 		vars.codes = codes;
843 		super(par, "script");
844 	}
845 
846 	string name;
847 
848 	struct Source { string source; string toString() { return source; } }
849 
850 	void innerCode(void delegate() theCode) {
851 		myCode = theCode;
852 	}
853 
854 	override void innerRawSource(string s) {
855 		myCode = null;
856 		super.innerRawSource(s);
857 	}
858 
859 	private void delegate() myCode;
860 
861 	override string toString() const {
862 		auto HACK = cast(ClientSideScript) this;
863 		if(HACK.myCode) {
864 			string code;
865 
866 			HACK.codes.push(&code);
867 			HACK.myCode();
868 			HACK.codes.pop();
869 
870 			HACK.innerRawSource = "\n" ~ code;
871 		}
872 
873 		return super.toString();
874 	}
875 
876 	enum commitCode = ` if(!codes.empty) { auto magic = codes.peek; (*magic) ~= code; }`;
877 
878 	struct Variable {
879 		string name;
880 		VariablesGroup group;
881 
882 		// formats it for use in an inline event handler
883 		string inline() {
884 			return name.replace("\t", "");
885 		}
886 
887 		this(string n, VariablesGroup g) {
888 			name = n;
889 			group = g;
890 		}
891 
892 		Source set(T)(T t) {
893 			string code = format("\t%s = %s;\n", name, toJavascript(t));
894 			if(!group.codes.empty) {
895 				auto magic = group.codes.peek;
896 				(*magic) ~= code;
897 			}
898 
899 			//Variant v = t;
900 			//group.repository[name] = v;
901 
902 			return Source(code);
903 		}
904 
905 		Variant _get() {
906 			return (group.repository)[name];
907 		}
908 
909 		Variable doAssignCode(string code) {
910 			if(!group.codes.empty) {
911 				auto magic = group.codes.peek;
912 				(*magic) ~= "\t" ~ code ~ ";\n";
913 			}
914 			return * ( new Variable(code, group) );
915 		}
916 
917 		Variable opSlice(size_t a, size_t b) {
918 			return * ( new Variable(name ~ ".substring("~to!string(a) ~ ", " ~ to!string(b)~")", group) );
919 		}
920 
921 		Variable opBinary(string op, T)(T rhs) {
922 			return * ( new Variable(name ~ " " ~ op ~ " " ~ toJavascript(rhs), group) );
923 		}
924 		Variable opOpAssign(string op, T)(T rhs) {
925 			return doAssignCode(name ~ " " ~  op ~ "= " ~ toJavascript(rhs));
926 		}
927 		Variable opIndex(T)(T i) {
928 			return * ( new Variable(name ~ "[" ~ toJavascript(i)  ~ "]" , group) );
929 		}
930 		Variable opIndexOpAssign(string op, T, R)(R rhs, T i) {
931 			return doAssignCode(name ~ "[" ~ toJavascript(i) ~ "] " ~ op ~ "= " ~ toJavascript(rhs));
932 		}
933 		Variable opIndexAssign(T, R)(R rhs, T i) {
934 			return doAssignCode(name ~ "[" ~ toJavascript(i) ~ "]" ~ " = " ~ toJavascript(rhs));
935 		}
936 		Variable opUnary(string op)() {
937 			return * ( new Variable(op ~ name, group) );
938 		}
939 
940 		void opAssign(T)(T rhs) {
941 			set(rhs);
942 		}
943 
944 		// used to call with zero arguments
945 		Source call() {
946 			string code = "\t" ~ name ~ "();\n";
947 			if(!group.codes.empty) {
948 				auto magic = group.codes.peek;
949 				(*magic) ~= code;
950 			}
951 			return Source(code);
952 		}
953 		mixin opDispatches!(Variable);
954 
955 		// returns code to call a function
956 		Source callFunction(T...)(string name, T t) {
957 			string code = "\t" ~ name ~ "(";
958 
959 			bool outputted = false;
960 			foreach(v; t) {
961 				if(outputted)
962 					code ~= ", ";
963 				else
964 					outputted = true;
965 
966 				code ~= toJavascript(v);
967 			}
968 
969 			code ~= ");\n";
970 
971 			if(!group.codes.empty) {
972 				auto magic = group.codes.peek;
973 				(*magic) ~= code;
974 			}
975 			return Source(code);
976 		}
977 
978 
979 	}
980 
981 	// this exists only to allow easier access
982 	class VariablesGroup {
983 		/// If the variable is a function, we call it. If not, we return the source
984 		@property Variable opDispatch(string name)() {
985 			return * ( new Variable(name, this) );
986 		}
987 
988 		Variant[string] repository;
989 		Stack!(string*) codes;
990 	}
991 
992 	VariablesGroup vars;
993 
994 	mixin opDispatches!(ClientSideScript);
995 
996 	// returns code to call a function
997 	Source callFunction(T...)(string name, T t) {
998 		string code = "\t" ~ name ~ "(";
999 
1000 		bool outputted = false;
1001 		foreach(v; t) {
1002 			if(outputted)
1003 				code ~= ", ";
1004 			else
1005 				outputted = true;
1006 
1007 			code ~= toJavascript(v);
1008 		}
1009 
1010 		code ~= ");\n";
1011 
1012 		mixin(commitCode);
1013 		return Source(code);
1014 	}
1015 
1016 	Variable thisObject() {
1017 		return Variable("this", vars);
1018 	}
1019 
1020 	Source setVariable(T)(string var, T what) {
1021 		auto v = Variable(var, vars);
1022 		return v.set(what);
1023 	}
1024 
1025 	Source appendSource(string code) {
1026 		mixin(commitCode);
1027 		return Source(code);
1028 	}
1029 
1030 	ref Variable var(string name) {
1031 		string code = "\tvar " ~ name ~ ";\n";
1032 		mixin(commitCode);
1033 
1034 		auto v = new Variable(name, vars);
1035 
1036 		return *v;
1037 	}
1038 }
1039 +/
1040 
1041 /*
1042 	Interesting things with scripts:
1043 
1044 
1045 	set script value with ease
1046 	get a script value we've already set
1047 	set script functions
1048 	set script events
1049 	call a script on pageload
1050 
1051 	document.scripts
1052 
1053 
1054 	set styles
1055 	get style precedence
1056 	get style thing
1057 
1058 */
1059 
1060 import std.conv;
1061 
1062 /+
1063 void main() {
1064 	auto document = new Document("<lol></lol>");
1065 	auto js = new ClientSideScript(document);
1066 
1067 	auto ele = document.createElement("a");
1068 	document.root.appendChild(ele);
1069 
1070 	int dInt = 50;
1071 
1072 	js.innerCode = {
1073 		js.var("funclol") = "hello, world"; // local variable definition
1074 		js.funclol = "10";    // parens are (currently) required when setting
1075 		js.funclol = 10;      // works with a variety of basic types
1076 		js.funclol = 10.4;
1077 		js.funclol = js.rofl; // can also set to another js variable
1078 		js.setVariable("name", [10, 20]); // try setVariable for complex types
1079 		js.setVariable("name", 100); // it can also set with strings for names
1080 		js.alert(js.funclol, dInt); // call functions with js and D arguments
1081 		js.funclol().call;       // to call without arguments, use the call method
1082 		js.funclol(10);        // calling with arguments looks normal
1083 		js.funclol(10, "20");  // including multiple, varied arguments
1084 		js.myelement = ele;    // works with DOM references too
1085 		js.a = js.b + js.c;    // some operators work too
1086 		js.a() += js.d; // for some ops, you need the parens to please the compiler
1087 		js.o = js.b[10]; // indexing works too
1088 		js.e[10] = js.a; // so does index assign
1089 		js.e[10] += js.a; // and index op assign...
1090 
1091 		js.eles = js.document.getElementsByTagName("as"); // js objects are accessible too
1092 		js.aaa = js.document.rofl.copter; // arbitrary depth
1093 
1094 		js.ele2 = js.myelement;
1095 
1096 		foreach(i; 0..5) 	// loops are done on the server - it may be unrolled
1097 			js.a() += js.w; // in the script outputted, or not work properly...
1098 
1099 		js.one = js.a[0..5];
1100 
1101 		js.math = js.a + js.b - js.c; // multiple things work too
1102 		js.math = js.a + (js.b - js.c); // FIXME: parens to NOT work.
1103 
1104 		js.math = js.s + 30; // and math with literals
1105 		js.math = js.s + (40 + dInt) - 10; // and D variables, which may be
1106 					// optimized by the D compiler with parens
1107 
1108 	};
1109 
1110 	write(js.toString);
1111 }
1112 +/
1113 import std.stdio;
1114 
1115 
1116 
1117 
1118 
1119 
1120 
1121 
1122 
1123 
1124 
1125 
1126 
1127 
1128 
1129 // helper for json
1130 
1131 
1132 import std.json;
1133 import std.traits;
1134 
1135 /+
1136 string toJavascript(T)(T a) {
1137 	static if(is(T == ClientSideScript.Variable)) {
1138 		return a.name;
1139 	} else static if(is(T : Element)) {
1140 		if(a is null)
1141 			return "null";
1142 
1143 		if(a.id.length == 0) {
1144 			static int count;
1145 			a.id = "javascript-referenced-element-" ~ to!string(++count);
1146 		}
1147 
1148 		return `document.getElementById("`~ a.id  ~`")`;
1149 	} else {
1150 		auto jsonv = toJsonValue(a);
1151 		return toJSON(&jsonv);
1152 	}
1153 }
1154 
1155 import arsd.web; // for toJsonValue
1156 
1157 /+
1158 string passthrough(string d)() {
1159 	return d;
1160 }
1161 
1162 string dToJs(string d)(Document document) {
1163 	auto js = new ClientSideScript(document);
1164 	mixin(passthrough!(d)());
1165 	return js.toString();
1166 }
1167 
1168 string translateJavascriptSourceWithDToStandardScript(string src)() {
1169 	// blocks of D { /* ... */ } are executed. Comments should work but
1170 	// don't.
1171 
1172 	int state = 0;
1173 
1174 	int starting = 0;
1175 	int ending = 0;
1176 
1177 	int startingString = 0;
1178 	int endingString = 0;
1179 
1180 	int openBraces = 0;
1181 
1182 
1183 	string result;
1184 
1185 	Document document = new Document("<root></root>");
1186 
1187 	foreach(i, c; src) {
1188 		switch(state) {
1189 			case 0:
1190 				if(c == 'D') {
1191 					endingString = i;
1192 					state++;
1193 				}
1194 			break;
1195 			case 1:
1196 				if(c == ' ') {
1197 					state++;
1198 				} else {
1199 					state = 0;
1200 				}
1201 			break;
1202 			case 2:
1203 				if(c == '{') {
1204 					state++;
1205 					starting = i;
1206 					openBraces = 1;
1207 				} else {
1208 					state = 0;
1209 				}
1210 			break;
1211 			case 3:
1212 				// We're inside D
1213 				if(c == '{')
1214 					openBraces++;
1215 				if(c == '}') {
1216 					openBraces--;
1217 					if(openBraces == 0) {
1218 						state = 0;
1219 						ending = i + 1;
1220 
1221 						// run some D..
1222 
1223 						string str = src[startingString .. endingString];
1224 
1225 						startingString = i + 1;
1226 						string d = src[starting .. ending];
1227 
1228 
1229 						result ~= str;
1230 
1231 						//result ~= dToJs!(d)(document);
1232 
1233 						result ~= "/* " ~ d ~ " */";
1234 					}
1235 				}
1236 			break;
1237 		}
1238 	}
1239 
1240 	result ~= src[startingString .. $];
1241 
1242 	return result;
1243 }
1244 +/
1245 +/
1246 
1247 abstract class CssPart {
1248 	string comment;
1249 	override string toString() const;
1250 	CssPart clone() const;
1251 }
1252 
1253 class CssAtRule : CssPart {
1254 	this() {}
1255 	this(ref string css) {
1256 		assert(css.length);
1257 		assert(css[0] == '@');
1258 
1259 		auto cssl = css.length;
1260 		int braceCount = 0;
1261 		int startOfInnerSlice = -1;
1262 
1263 		foreach(i, c; css) {
1264 			if(braceCount == 0 && c == ';') {
1265 				content = css[0 .. i + 1];
1266 				css = css[i + 1 .. $];
1267 
1268 				opener = content;
1269 				break;
1270 			}
1271 
1272 			if(c == '{') {
1273 				braceCount++;
1274 				if(startOfInnerSlice == -1)
1275 					startOfInnerSlice = cast(int) i;
1276 			}
1277 			if(c == '}') {
1278 				braceCount--;
1279 				if(braceCount < 0)
1280 					throw new Exception("Bad CSS: mismatched }");
1281 
1282 				if(braceCount == 0) {
1283 					opener = css[0 .. startOfInnerSlice];
1284 					inner = css[startOfInnerSlice + 1 .. i];
1285 
1286 					content = css[0 .. i + 1];
1287 					css = css[i + 1 .. $];
1288 					break;
1289 				}
1290 			}
1291 		}
1292 
1293 		if(cssl == css.length) {
1294 			throw new Exception("Bad CSS: unclosed @ rule. " ~ to!string(braceCount) ~ " brace(s) uncloced");
1295 		}
1296 
1297 		innerParts = lexCss(inner, false);
1298 	}
1299 
1300 	string content;
1301 
1302 	string opener;
1303 	string inner;
1304 
1305 	CssPart[] innerParts;
1306 
1307 	override CssAtRule clone() const {
1308 		auto n = new CssAtRule();
1309 		n.content = content;
1310 		n.opener = opener;
1311 		n.inner = inner;
1312 		foreach(part; innerParts)
1313 			n.innerParts ~= part.clone();
1314 		return n;
1315 	}
1316 	override string toString() const {
1317 		string c;
1318 		if(comment.length)
1319 			c ~= "/* " ~ comment ~ "*/\n";
1320 		c ~= opener.strip();
1321 		if(innerParts.length) {
1322 			string i;
1323 			foreach(part; innerParts)
1324 				i ~= part.toString() ~ "\n";
1325 
1326 			c ~= " {\n";
1327 			foreach(line; i.splitLines)
1328 				c ~= "\t" ~ line ~ "\n";
1329 			c ~= "}";
1330 		}
1331 		return c;
1332 	}
1333 }
1334 
1335 class CssRuleSet : CssPart {
1336 	this() {}
1337 
1338 	this(ref string css) {
1339 		auto idx = css.indexOf("{");
1340 		assert(idx != -1);
1341 		foreach(selector; css[0 .. idx].split(","))
1342 			selectors ~= selector.strip;
1343 
1344 		css = css[idx .. $];
1345 		int braceCount = 0;
1346 		string content;
1347 		size_t f = css.length;
1348 		foreach(i, c; css) {
1349 			if(c == '{')
1350 				braceCount++;
1351 			if(c == '}') {
1352 				braceCount--;
1353 				if(braceCount == 0) {
1354 					f = i;
1355 					break;
1356 				}
1357 			}
1358 		}
1359 
1360 		content = css[1 .. f]; // skipping the {
1361 		if(f < css.length && css[f] == '}')
1362 			f++;
1363 		css = css[f .. $];
1364 
1365 		contents = lexCss(content, false);
1366 	}
1367 
1368 	string[] selectors;
1369 	CssPart[] contents;
1370 
1371 	override CssRuleSet clone() const {
1372 		auto n = new CssRuleSet();
1373 		n.selectors = selectors.dup;
1374 		foreach(part; contents)
1375 			n.contents ~= part.clone();
1376 		return n;
1377 	}
1378 
1379 	CssRuleSet[] deNest(CssRuleSet outer = null) const {
1380 		CssRuleSet[] ret;
1381 
1382 		CssRuleSet levelOne = new CssRuleSet();
1383 		ret ~= levelOne;
1384 		if(outer is null)
1385 			levelOne.selectors = selectors.dup;
1386 		else {
1387 			foreach(outerSelector; outer.selectors.length ? outer.selectors : [""])
1388 			foreach(innerSelector; selectors) {
1389 				/*
1390 					it would be great to do a top thing and a bottom, examples:
1391 					.awesome, .awesome\& {
1392 						.something img {}
1393 					}
1394 
1395 					should give:
1396 						.awesome .something img, .awesome.something img { }
1397 
1398 					And also
1399 					\&.cool {
1400 						.something img {}
1401 					}
1402 
1403 					should give:
1404 						.something img.cool {}
1405 
1406 					OR some such syntax.
1407 
1408 
1409 					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.
1410 
1411 					idk though.
1412 				*/
1413 				/+
1414 				// FIXME: this implementation is useless, but the idea of allowing combinations at the top level rox.
1415 				if(outerSelector.length > 2 && outerSelector[$-2] == '\\' && outerSelector[$-1] == '&') {
1416 					// the outer one is an adder... so we always want to paste this on, and if the inner has it, collapse it
1417 					if(innerSelector.length > 2 && innerSelector[0] == '\\' && innerSelector[1] == '&')
1418 						levelOne.selectors ~= outerSelector[0 .. $-2] ~ innerSelector[2 .. $];
1419 					else
1420 						levelOne.selectors ~= outerSelector[0 .. $-2] ~ innerSelector;
1421 				} else
1422 				+/
1423 
1424 				// we want to have things like :hover, :before, etc apply without implying
1425 				// a descendant.
1426 
1427 				// If you want it to be a descendant pseudoclass, use the *:something - the
1428 				// wildcard tag - instead of just a colon.
1429 
1430 				// But having this is too useful to ignore.
1431 				if(innerSelector.length && innerSelector[0] == ':')
1432 					levelOne.selectors ~= outerSelector ~ innerSelector;
1433 				// we also allow \&something to get them concatenated
1434 				else if(innerSelector.length > 2 && innerSelector[0] == '\\' && innerSelector[1] == '&')
1435 					levelOne.selectors ~= outerSelector ~ innerSelector[2 .. $].strip;
1436 				else
1437 					levelOne.selectors ~= outerSelector ~ " " ~ innerSelector; // otherwise, use some other operator...
1438 			}
1439 		}
1440 
1441 		foreach(part; contents) {
1442 			auto set = cast(CssRuleSet) part;
1443 			if(set is null)
1444 				levelOne.contents ~= part.clone();
1445 			else {
1446 				// actually gotta de-nest this
1447 				ret ~= set.deNest(levelOne);
1448 			}
1449 		}
1450 
1451 		return ret;
1452 	}
1453 
1454 	override string toString() const {
1455 		string ret;
1456 
1457 
1458 		if(comment.length)
1459 			ret ~= "/* " ~ comment ~ "*/\n";
1460 
1461 		bool outputtedSelector = false;
1462 		foreach(selector; selectors) {
1463 			if(outputtedSelector)
1464 				ret ~= ", ";
1465 			else
1466 				outputtedSelector = true;
1467 
1468 			ret ~= selector;
1469 		}
1470 
1471 		ret ~= " {\n";
1472 		foreach(content; contents) {
1473 			auto str = content.toString();
1474 			if(str.length)
1475 				str = "\t" ~ str.replace("\n", "\n\t") ~ "\n";
1476 
1477 			ret ~= str;
1478 		}
1479 		ret ~= "}";
1480 
1481 		return ret;
1482 	}
1483 }
1484 
1485 class CssRule : CssPart {
1486 	this() {}
1487 
1488 	this(ref string css, int endOfStatement) {
1489 		content = css[0 .. endOfStatement];
1490 		if(endOfStatement < css.length && css[endOfStatement] == ';')
1491 			endOfStatement++;
1492 
1493 		css = css[endOfStatement .. $];
1494 	}
1495 
1496 	// note: does not include the ending semicolon
1497 	string content;
1498 
1499 	string key() const {
1500 		auto idx = content.indexOf(":");
1501 		if(idx == -1)
1502 			throw new Exception("Bad css, missing colon in " ~ content);
1503 		return content[0 .. idx].strip.toLower;
1504 	}
1505 
1506 	string value() const {
1507 		auto idx = content.indexOf(":");
1508 		if(idx == -1)
1509 			throw new Exception("Bad css, missing colon in " ~ content);
1510 
1511 		return content[idx + 1 .. $].strip;
1512 	}
1513 
1514 	override CssRule clone() const {
1515 		auto n = new CssRule();
1516 		n.content = content;
1517 		return n;
1518 	}
1519 
1520 	override string toString() const {
1521 		string ret;
1522 		if(strip(content).length == 0)
1523 			ret = "";
1524 		else
1525 			ret = key ~ ": " ~ value ~ ";";
1526 
1527 		if(comment.length)
1528 			ret ~= " /* " ~ comment ~ " */";
1529 
1530 		return ret;
1531 	}
1532 }
1533 
1534 // Never call stripComments = false unless you have already stripped them.
1535 // this thing can't actually handle comments intelligently.
1536 CssPart[] lexCss(string css, bool stripComments = true) {
1537 	if(stripComments) {
1538 		import std.regex;
1539 		css = std.regex.replace(css, regex(r"\/\*[^*]*\*+([^/*][^*]*\*+)*\/", "g"), "");
1540 	}
1541 
1542 	CssPart[] ret;
1543 	css = css.stripLeft();
1544 
1545 	int cnt;
1546 
1547 	while(css.length > 1) {
1548 		CssPart p;
1549 
1550 		if(css[0] == '@') {
1551 			p = new CssAtRule(css);
1552 		} else {
1553 			// non-at rules can be either rules or sets.
1554 			// The question is: which comes first, the ';' or the '{' ?
1555 
1556 			auto endOfStatement = css.indexOfCssSmart(';');
1557 			if(endOfStatement == -1)
1558 				endOfStatement = css.indexOf("}");
1559 			if(endOfStatement == -1)
1560 				endOfStatement = css.length;
1561 
1562 			auto beginningOfBlock = css.indexOf("{");
1563 			if(beginningOfBlock == -1 || endOfStatement < beginningOfBlock)
1564 				p = new CssRule(css, cast(int) endOfStatement);
1565 			else
1566 				p = new CssRuleSet(css);
1567 		}
1568 
1569 		assert(p !is null);
1570 		ret ~= p;
1571 
1572 		css = css.stripLeft();
1573 	}
1574 
1575 	return ret;
1576 }
1577 
1578 // This needs to skip characters inside parens or quotes, so it
1579 // doesn't trip up on stuff like data uris when looking for a terminating
1580 // character.
1581 ptrdiff_t indexOfCssSmart(string i, char find) {
1582 	int parenCount;
1583 	char quote;
1584 	bool escaping;
1585 	foreach(idx, ch; i) {
1586 		if(escaping) {
1587 			escaping = false;
1588 			continue;
1589 		}
1590 		if(quote != char.init) {
1591 			if(ch == quote)
1592 				quote = char.init;
1593 			continue;
1594 		}
1595 		if(ch == '\'' || ch == '"') {
1596 			quote = ch;
1597 			continue;
1598 		}
1599 
1600 		if(ch == '(')
1601 			parenCount++;
1602 
1603 		if(parenCount) {
1604 			if(ch == ')')
1605 				parenCount--;
1606 			continue;
1607 		}
1608 
1609 		// at this point, we are not in parenthesis nor are we in
1610 		// a quote, so we can actually search for the relevant character
1611 
1612 		if(ch == find)
1613 			return idx;
1614 	}
1615 	return -1;
1616 }
1617 
1618 string cssToString(in CssPart[] css) {
1619 	string ret;
1620 	foreach(c; css) {
1621 		if(ret.length) {
1622 			if(ret[$ -1] == '}')
1623 				ret ~= "\n\n";
1624 			else
1625 				ret ~= "\n";
1626 		}
1627 		ret ~= c.toString();
1628 	}
1629 
1630 	return ret;
1631 }
1632 
1633 /// Translates nested css
1634 const(CssPart)[] denestCss(CssPart[] css) {
1635 	CssPart[] ret;
1636 	foreach(part; css) {
1637 		auto at = cast(CssAtRule) part;
1638 		if(at is null) {
1639 			auto set = cast(CssRuleSet) part;
1640 			if(set is null)
1641 				ret ~= part;
1642 			else {
1643 				ret ~= set.deNest();
1644 			}
1645 		} else {
1646 			// at rules with content may be denested at the top level...
1647 			// FIXME: is this even right all the time?
1648 
1649 			if(at.inner.length) {
1650 				auto newCss = at.opener ~ "{\n";
1651 
1652 					// the whitespace manipulations are just a crude indentation thing
1653 				newCss ~= "\t" ~ (cssToString(denestCss(lexCss(at.inner, false))).replace("\n", "\n\t").replace("\n\t\n\t", "\n\n\t"));
1654 
1655 				newCss ~= "\n}";
1656 
1657 				ret ~= new CssAtRule(newCss);
1658 			} else {
1659 				ret ~= part; // no inner content, nothing special needed
1660 			}
1661 		}
1662 	}
1663 
1664 	return ret;
1665 }
1666 
1667 /*
1668 	Forms:
1669 
1670 	¤var
1671 	¤lighten(¤foreground, 0.5)
1672 	¤lighten(¤foreground, 0.5); -- exactly one semicolon shows up at the end
1673 	¤var(something, something_else) {
1674 		final argument
1675 	}
1676 
1677 	¤function {
1678 		argument
1679 	}
1680 
1681 
1682 	Possible future:
1683 
1684 	Recursive macros:
1685 
1686 	¤define(li) {
1687 		<li>¤car</li>
1688 		list(¤cdr)
1689 	}
1690 
1691 	¤define(list) {
1692 		¤li(¤car)
1693 	}
1694 
1695 
1696 	car and cdr are borrowed from lisp... hmm
1697 	do i really want to do this...
1698 
1699 
1700 
1701 	But if the only argument is cdr, and it is empty the function call is cancelled.
1702 	This lets you do some looping.
1703 
1704 
1705 	hmmm easier would be
1706 
1707 	¤loop(macro_name, args...) {
1708 		body
1709 	}
1710 
1711 	when you call loop, it calls the macro as many times as it can for the
1712 	given args, and no more.
1713 
1714 
1715 
1716 	Note that set is a macro; it doesn't expand it's arguments.
1717 	To force expansion, use echo (or expand?) on the argument you set.
1718 */
1719 
1720 // Keep in mind that this does not understand comments!
1721 class MacroExpander {
1722 	dstring delegate(dstring[])[dstring] functions;
1723 	dstring[dstring] variables;
1724 
1725 	/// This sets a variable inside the macro system
1726 	void setValue(string key, string value) {
1727 		variables[to!dstring(key)] = to!dstring(value);
1728 	}
1729 
1730 	struct Macro {
1731 		dstring name;
1732 		dstring[] args;
1733 		dstring definition;
1734 	}
1735 
1736 	Macro[dstring] macros;
1737 
1738 	// FIXME: do I want user defined functions or something?
1739 
1740 	this() {
1741 		functions["get"] = &get;
1742 		functions["set"] = &set;
1743 		functions["define"] = &define;
1744 		functions["loop"] = &loop;
1745 
1746 		functions["echo"] = delegate dstring(dstring[] args) {
1747 			dstring ret;
1748 			bool outputted;
1749 			foreach(arg; args) {
1750 				if(outputted)
1751 					ret ~= ", ";
1752 				else
1753 					outputted = true;
1754 				ret ~= arg;
1755 			}
1756 
1757 			return ret;
1758 		};
1759 
1760 		functions["uriEncode"] = delegate dstring(dstring[] args) {
1761 			return to!dstring(encodeUriComponent(to!string(args[0])));
1762 		};
1763 
1764 		functions["test"] = delegate dstring(dstring[] args) {
1765 			assert(0, to!string(args.length) ~ " args: " ~ to!string(args));
1766 		};
1767 
1768 		functions["include"] = &include;
1769 	}
1770 
1771 	string[string] includeFiles;
1772 
1773 	dstring include(dstring[] args) {
1774 		string s;
1775 		foreach(arg; args) {
1776 			string lol = to!string(arg);
1777 			s ~= to!string(includeFiles[lol]);
1778 		}
1779 
1780 		return to!dstring(s);
1781 	}
1782 
1783 	// the following are used inside the user text
1784 
1785 	dstring define(dstring[] args) {
1786 		enforce(args.length > 1, "requires at least a macro name and definition");
1787 
1788 		Macro m;
1789 		m.name = args[0];
1790 		if(args.length > 2)
1791 			m.args = args[1 .. $ - 1];
1792 		m.definition = args[$ - 1];
1793 
1794 		macros[m.name] = m;
1795 
1796 		return null;
1797 	}
1798 
1799 	dstring set(dstring[] args) {
1800 		enforce(args.length == 2, "requires two arguments. got " ~ to!string(args));
1801 		variables[args[0]] = args[1];
1802 		return "";
1803 	}
1804 
1805 	dstring get(dstring[] args) {
1806 		enforce(args.length == 1);
1807 		if(args[0] !in variables)
1808 			return "";
1809 		return variables[args[0]];
1810 	}
1811 
1812 	dstring loop(dstring[] args) {
1813 		enforce(args.length > 1, "must provide a macro name and some arguments");
1814 		auto m = macros[args[0]];
1815 		args = args[1 .. $];
1816 		dstring returned;
1817 
1818 		size_t iterations = args.length;
1819 		if(m.args.length != 0)
1820 			iterations = (args.length + m.args.length - 1) / m.args.length;
1821 
1822 		foreach(i; 0 .. iterations) {
1823 			returned ~= expandMacro(m, args);
1824 			if(m.args.length < args.length)
1825 				args = args[m.args.length .. $];
1826 			else
1827 				args = null;
1828 		}
1829 
1830 		return returned;
1831 	}
1832 
1833 	/// Performs the expansion
1834 	string expand(string srcutf8) {
1835 		auto src = expand(to!dstring(srcutf8));
1836 		return to!string(src);
1837 	}
1838 
1839 	private int depth = 0;
1840 	/// ditto
1841 	dstring expand(dstring src) {
1842 		return expandImpl(src, null);
1843 	}
1844 
1845 	// FIXME: the order of evaluation shouldn't matter. Any top level sets should be run
1846 	// before anything is expanded.
1847 	private dstring expandImpl(dstring src, dstring[dstring] localVariables) {
1848 		depth ++;
1849 		if(depth > 10)
1850 			throw new Exception("too much recursion depth in macro expansion");
1851 
1852 		bool doneWithSetInstructions = false; // this is used to avoid double checks each loop
1853 		for(;;) {
1854 			// we do all the sets first since the latest one is supposed to be used site wide.
1855 			// this allows a later customization to apply to the entire document.
1856 			auto idx = doneWithSetInstructions ? -1 : src.indexOf("¤set");
1857 			if(idx == -1) {
1858 				doneWithSetInstructions = true;
1859 				idx = src.indexOf("¤");
1860 			}
1861 			if(idx == -1) {
1862 				depth--;
1863 				return src;
1864 			}
1865 
1866 			// the replacement goes
1867 			// src[0 .. startingSliceForReplacement] ~ new ~ src[endingSliceForReplacement .. $];
1868 			sizediff_t startingSliceForReplacement, endingSliceForReplacement;
1869 
1870 			dstring functionName;
1871 			dstring[] arguments;
1872 			bool addTrailingSemicolon;
1873 
1874 			startingSliceForReplacement = idx;
1875 			// idx++; // because the star in UTF 8 is two characters. FIXME: hack -- not needed thx to dstrings
1876 			auto possibility = src[idx + 1 .. $];
1877 			size_t argsBegin;
1878 
1879 			bool found = false;
1880 			foreach(i, c; possibility) {
1881 				if(!(
1882 					// valid identifiers
1883 					(c >= 'A' && c <= 'Z')
1884 					||
1885 					(c >= 'a' && c <= 'z')
1886 					||
1887 					(c >= '0' && c <= '9')
1888 					||
1889 					c == '_'
1890 				)) {
1891 					// not a valid identifier means
1892 					// we're done reading the name
1893 					functionName = possibility[0 .. i];
1894 					argsBegin = i;
1895 					found = true;
1896 					break;
1897 				}
1898 			}
1899 
1900 			if(!found) {
1901 				functionName = possibility;
1902 				argsBegin = possibility.length;
1903 			}
1904 
1905 			auto endOfVariable = argsBegin + idx + 1; // this is the offset into the original source
1906 
1907 			bool checkForAllArguments = true;
1908 
1909 			moreArguments:
1910 
1911 			assert(argsBegin);
1912 
1913 			endingSliceForReplacement = argsBegin + idx + 1;
1914 
1915 			while(
1916 				argsBegin < possibility.length && (
1917 				possibility[argsBegin] == ' ' ||
1918 				possibility[argsBegin] == '\t' ||
1919 				possibility[argsBegin] == '\n' ||
1920 				possibility[argsBegin] == '\r'))
1921 			{
1922 				argsBegin++;
1923 			}
1924 
1925 			if(argsBegin == possibility.length) {
1926 				endingSliceForReplacement = src.length;
1927 				goto doReplacement;
1928 			}
1929 
1930 			switch(possibility[argsBegin]) {
1931 				case '(':
1932 					if(!checkForAllArguments)
1933 						goto doReplacement;
1934 
1935 					// actually parsing the arguments
1936 					size_t currentArgumentStarting = argsBegin + 1;
1937 
1938 					int open;
1939 
1940 					bool inQuotes;
1941 					bool inTicks;
1942 					bool justSawBackslash;
1943 					foreach(i, c; possibility[argsBegin .. $]) {
1944 						if(c == '`')
1945 							inTicks = !inTicks;
1946 
1947 						if(inTicks)
1948 							continue;
1949 
1950 						if(!justSawBackslash && c == '"')
1951 							inQuotes = !inQuotes;
1952 
1953 						if(c == '\\')
1954 							justSawBackslash = true;
1955 						else
1956 							justSawBackslash = false;
1957 
1958 						if(inQuotes)
1959 							continue;
1960 
1961 						if(open == 1 && c == ',') { // don't want to push a nested argument incorrectly...
1962 							// push the argument
1963 							arguments ~= possibility[currentArgumentStarting .. i + argsBegin];
1964 							currentArgumentStarting = argsBegin + i + 1;
1965 						}
1966 
1967 						if(c == '(')
1968 							open++;
1969 						if(c == ')') {
1970 							open--;
1971 							if(open == 0) {
1972 								// push the last argument
1973 								arguments ~= possibility[currentArgumentStarting .. i + argsBegin];
1974 
1975 								endingSliceForReplacement = argsBegin + idx + 1 + i;
1976 								argsBegin += i + 1;
1977 								break;
1978 							}
1979 						}
1980 					}
1981 
1982 					// then see if there's a { argument too
1983 					checkForAllArguments = false;
1984 					goto moreArguments;
1985 				case '{':
1986 					// find the match
1987 					int open;
1988 					foreach(i, c; possibility[argsBegin .. $]) {
1989 						if(c == '{')
1990 							open ++;
1991 						if(c == '}') {
1992 							open --;
1993 							if(open == 0) {
1994 								// cutting off the actual braces here
1995 								arguments ~= possibility[argsBegin + 1 .. i + argsBegin];
1996 									// second +1 is there to cut off the }
1997 								endingSliceForReplacement = argsBegin + idx + 1 + i + 1;
1998 
1999 								argsBegin += i + 1;
2000 								break;
2001 							}
2002 						}
2003 					}
2004 
2005 					goto doReplacement;
2006 				default:
2007 					goto doReplacement;
2008 			}
2009 
2010 			doReplacement:
2011 				if(endingSliceForReplacement < src.length && src[endingSliceForReplacement] == ';') {
2012 					endingSliceForReplacement++;
2013 					addTrailingSemicolon = true; // don't want a doubled semicolon
2014 					// FIXME: what if it's just some whitespace after the semicolon? should that be
2015 					// stripped or no?
2016 				}
2017 
2018 				foreach(ref argument; arguments) {
2019 					argument = argument.strip();
2020 					if(argument.length > 2 && argument[0] == '`' && argument[$-1] == '`')
2021 						argument = argument[1 .. $ - 1]; // strip ticks here
2022 					else
2023 					if(argument.length > 2 && argument[0] == '"' && argument[$-1] == '"')
2024 						argument = argument[1 .. $ - 1]; // strip quotes here
2025 
2026 					// recursive macro expanding
2027 					// these need raw text, since they expand later. FIXME: should it just be a list of functions?
2028 					if(functionName != "define" && functionName != "quote" && functionName != "set")
2029 						argument = this.expandImpl(argument, localVariables);
2030 				}
2031 
2032 				dstring returned = "";
2033 				if(functionName in localVariables) {
2034 					/*
2035 					if(functionName == "_head")
2036 						returned = arguments[0];
2037 					else if(functionName == "_tail")
2038 						returned = arguments[1 .. $];
2039 					else
2040 					*/
2041 						returned = localVariables[functionName];
2042 				} else if(functionName in functions)
2043 					returned = functions[functionName](arguments);
2044 				else if(functionName in variables) {
2045 					returned = variables[functionName];
2046 					// FIXME
2047 					// we also need to re-attach the arguments array, since variable pulls can't have args
2048 					assert(endOfVariable > startingSliceForReplacement);
2049 					endingSliceForReplacement = endOfVariable;
2050 				} else if(functionName in macros) {
2051 					returned = expandMacro(macros[functionName], arguments);
2052 				}
2053 
2054 				if(addTrailingSemicolon && returned.length > 1 && returned[$ - 1] != ';')
2055 					returned ~= ";";
2056 
2057 				src = src[0 .. startingSliceForReplacement] ~ returned ~ src[endingSliceForReplacement .. $];
2058 		}
2059 		assert(0); // not reached
2060 	}
2061 
2062 	dstring expandMacro(Macro m, dstring[] arguments) {
2063 		dstring[dstring] locals;
2064 		foreach(i, arg; m.args) {
2065 			if(i == arguments.length)
2066 				break;
2067 			locals[arg] = arguments[i];
2068 		}
2069 
2070 		return this.expandImpl(m.definition, locals);
2071 	}
2072 }
2073 
2074 
2075 class CssMacroExpander : MacroExpander {
2076 	this() {
2077 		super();
2078 
2079 		functions["prefixed"] = &prefixed;
2080 
2081 		functions["lighten"] = &(colorFunctionWrapper!lighten);
2082 		functions["darken"] = &(colorFunctionWrapper!darken);
2083 		functions["moderate"] = &(colorFunctionWrapper!moderate);
2084 		functions["extremify"] = &(colorFunctionWrapper!extremify);
2085 		functions["makeTextColor"] = &(oneArgColorFunctionWrapper!makeTextColor);
2086 
2087 		functions["oppositeLightness"] = &(oneArgColorFunctionWrapper!oppositeLightness);
2088 
2089 		functions["rotateHue"] = &(colorFunctionWrapper!rotateHue);
2090 
2091 		functions["saturate"] = &(colorFunctionWrapper!saturate);
2092 		functions["desaturate"] = &(colorFunctionWrapper!desaturate);
2093 
2094 		functions["setHue"] = &(colorFunctionWrapper!setHue);
2095 		functions["setSaturation"] = &(colorFunctionWrapper!setSaturation);
2096 		functions["setLightness"] = &(colorFunctionWrapper!setLightness);
2097 	}
2098 
2099 	// prefixed(border-radius: 12px);
2100 	dstring prefixed(dstring[] args) {
2101 		dstring ret;
2102 		foreach(prefix; ["-moz-"d, "-webkit-"d, "-o-"d, "-ms-"d, "-khtml-"d, ""d])
2103 			ret ~= prefix ~ args[0] ~ ";";
2104 		return ret;
2105 	}
2106 
2107 	/// Runs the macro expansion but then a CSS densesting
2108 	string expandAndDenest(string cssSrc) {
2109 		return cssToString(denestCss(lexCss(this.expand(cssSrc))));
2110 	}
2111 
2112 	// internal things
2113 	dstring colorFunctionWrapper(alias func)(dstring[] args) {
2114 		auto color = readCssColor(to!string(args[0]));
2115 		auto percentage = readCssNumber(args[1]);
2116 		return "#"d ~ to!dstring(func(color, percentage).toString());
2117 	}
2118 
2119 	dstring oneArgColorFunctionWrapper(alias func)(dstring[] args) {
2120 		auto color = readCssColor(to!string(args[0]));
2121 		return "#"d ~ to!dstring(func(color).toString());
2122 	}
2123 }
2124 
2125 
2126 real readCssNumber(dstring s) {
2127 	s = s.replace(" "d, ""d);
2128 	if(s.length == 0)
2129 		return 0;
2130 	if(s[$-1] == '%')
2131 		return (to!real(s[0 .. $-1]) / 100f);
2132 	return to!real(s);
2133 }
2134 
2135 import std.format;
2136 
2137 class JavascriptMacroExpander : MacroExpander {
2138 	this() {
2139 		super();
2140 		functions["foreach"] = &foreachLoop;
2141 	}
2142 
2143 
2144 	/**
2145 		¤foreach(item; array) {
2146 			// code
2147 		}
2148 
2149 		so arg0 .. argn-1 is the stuff inside. Conc
2150 	*/
2151 
2152 	int foreachLoopCounter;
2153 	dstring foreachLoop(dstring[] args) {
2154 		enforce(args.length >= 2, "foreach needs parens and code");
2155 		dstring parens;
2156 		bool outputted = false;
2157 		foreach(arg; args[0 .. $ - 1]) {
2158 			if(outputted)
2159 				parens ~= ", ";
2160 			else
2161 				outputted = true;
2162 			parens ~= arg;
2163 		}
2164 
2165 		dstring variableName, arrayName;
2166 
2167 		auto it = parens.split(";");
2168 		variableName = it[0].strip;
2169 		arrayName = it[1].strip;
2170 
2171 		dstring insideCode = args[$-1];
2172 
2173 		dstring iteratorName;
2174 		iteratorName = "arsd_foreach_loop_counter_"d ~ to!dstring(++foreachLoopCounter);
2175 		dstring temporaryName = "arsd_foreach_loop_temporary_"d ~ to!dstring(++foreachLoopCounter);
2176 
2177 		auto writer = appender!dstring();
2178 
2179 		formattedWrite(writer, "
2180 			var %2$s = %5$s;
2181 			if(%2$s != null)
2182 			for(var %1$s = 0; %1$s < %2$s.length; %1$s++) {
2183 				var %3$s = %2$s[%1$s];
2184 				%4$s
2185 		}"d, iteratorName, temporaryName, variableName, insideCode, arrayName);
2186 
2187 		auto code = writer.data;
2188 
2189 		return to!dstring(code);
2190 	}
2191 }
2192 
2193 string beautifyCss(string css) {
2194 	css = css.replace(":", ": ");
2195 	css = css.replace(":  ", ": ");
2196 	css = css.replace("{", " {\n\t");
2197 	css = css.replace(";", ";\n\t");
2198 	css = css.replace("\t}", "}\n\n");
2199 	return css.strip;
2200 }
2201 
2202 int fromHex(string s) {
2203 	int result = 0;
2204 
2205 	int exp = 1;
2206 	foreach(c; retro(s)) {
2207 		if(c >= 'A' && c <= 'F')
2208 			result += exp * (c - 'A' + 10);
2209 		else if(c >= 'a' && c <= 'f')
2210 			result += exp * (c - 'a' + 10);
2211 		else if(c >= '0' && c <= '9')
2212 			result += exp * (c - '0');
2213 		else
2214 			throw new Exception("invalid hex character: " ~ cast(char) c);
2215 
2216 		exp *= 16;
2217 	}
2218 
2219 	return result;
2220 }
2221 
2222 Color readCssColor(string cssColor) {
2223 	cssColor = cssColor.strip().toLower();
2224 
2225 	if(cssColor.startsWith("#")) {
2226 		cssColor = cssColor[1 .. $];
2227 		if(cssColor.length == 3) {
2228 			cssColor = "" ~ cssColor[0] ~ cssColor[0]
2229 					~ cssColor[1] ~ cssColor[1]
2230 					~ cssColor[2] ~ cssColor[2];
2231 		}
2232 
2233 		if(cssColor.length == 6)
2234 			cssColor ~= "ff";
2235 
2236 		/* my extension is to do alpha */
2237 		if(cssColor.length == 8) {
2238 			return Color(
2239 				fromHex(cssColor[0 .. 2]),
2240 				fromHex(cssColor[2 .. 4]),
2241 				fromHex(cssColor[4 .. 6]),
2242 				fromHex(cssColor[6 .. 8]));
2243 		} else
2244 			throw new Exception("invalid color " ~ cssColor);
2245 	} else if(cssColor.startsWith("rgba")) {
2246 		assert(0); // FIXME: implement
2247 		/*
2248 		cssColor = cssColor.replace("rgba", "");
2249 		cssColor = cssColor.replace(" ", "");
2250 		cssColor = cssColor.replace("(", "");
2251 		cssColor = cssColor.replace(")", "");
2252 
2253 		auto parts = cssColor.split(",");
2254 		*/
2255 	} else if(cssColor.startsWith("rgb")) {
2256 		assert(0); // FIXME: implement
2257 	} else if(cssColor.startsWith("hsl")) {
2258 		assert(0); // FIXME: implement
2259 	} else
2260 		return Color.fromNameString(cssColor);
2261 	/*
2262 	switch(cssColor) {
2263 		default:
2264 			// FIXME let's go ahead and try naked hex for compatibility with my gradient program
2265 			assert(0, "Unknown color: " ~ cssColor);
2266 	}
2267 	*/
2268 }
2269 
2270 /*
2271 Copyright: Adam D. Ruppe, 2010 - 2015
2272 License:   <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
2273 Authors: Adam D. Ruppe, with contributions by Nick Sabalausky and Trass3r
2274 
2275         Copyright Adam D. Ruppe 2010-2015.
2276 Distributed under the Boost Software License, Version 1.0.
2277    (See accompanying file LICENSE_1_0.txt or copy at
2278         http://www.boost.org/LICENSE_1_0.txt)
2279 */