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