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