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