1 // <details> html tag
2 // FIXME: KEY_VALUE like params
3 module arsd.docgen.comment;
4 
5 import adrdox.main;
6 import arsd.dom;
7 
8 import dparse.ast;
9 import dparse.lexer;
10 
11 import std..string;
12 import std.algorithm;
13 
14 import std.conv;
15 
16 const(char)[] htmlEncode(const(char)[] s) {
17 	return s.
18 		replace("&", "&amp;").
19 		replace("<", "&lt;").
20 		replace(">", "&gt;");
21 }
22 
23 
24 static struct MyOutputRange {
25 	this(string* output) {
26 		this.output = output;
27 	}
28 
29 	string* output;
30 	void put(T...)(T s) {
31 		foreach(i; s)
32 			putTag(i.htmlEncode);
33 	}
34 
35 	void putTag(in char[] s) {
36 		if(s.length == 0)
37 			return;
38 		//foreach(ch; s) {
39 			assert(s.length);
40 			assert(s.indexOf("</body>") == -1);
41 		//}
42 		(*output) ~= s;
43 	}
44 }
45 
46 
47 /*
48 	Params:
49 
50 	The line, excluding whitespace, must start with
51 	identifier = to start a new thing. If a new thing starts,
52 	it closes the old.
53 
54 	The description may be formatted however. Whitespace is stripped.
55 */
56 
57 string getIdent(T)(T t) {
58 	if(t is null)
59 		return null;
60 	if(t.identifier == tok!"")
61 		return null;
62 	return t.identifier.text;
63 }
64 
65 bool hasParam(T)(const T dec, string name, Decl declObject, bool descend = true) {
66 	if(dec is null)
67 		return false;
68 
69 	static if(__traits(compiles, dec.parameters)) {
70 		if(dec.parameters && dec.parameters.parameters)
71 		foreach(parameter; dec.parameters.parameters)
72 			if(parameter.name.text == name)
73 				return true;
74 	}
75 	static if(__traits(compiles, dec.templateParameters)) {
76 		if(dec.templateParameters && dec.templateParameters.templateParameterList)
77 		foreach(parameter; dec.templateParameters.templateParameterList.items) {
78 			if(getIdent(parameter.templateTypeParameter) == name)
79 				return true;
80 			if(getIdent(parameter.templateValueParameter) == name)
81 				return true;
82 			if(getIdent(parameter.templateAliasParameter) == name)
83 				return true;
84 			if(getIdent(parameter.templateTupleParameter) == name)
85 				return true;
86 			if(parameter.templateThisParameter && getIdent(parameter.templateThisParameter.templateTypeParameter) == name)
87 				return true;
88 		}
89 	}
90 
91 	if(!descend)
92 		return false;
93 
94 	// need to check parent template parameters in case they are
95 	// referenced in a child
96 	if(declObject.parent) {
97 		bool h;
98 		if(auto pdec = cast(TemplateDecl) declObject.parent)
99 			h = hasParam(pdec.astNode, name, pdec, false);
100 		else
101 			 {}
102 		if(h)
103 			return true;
104 	}
105 
106 	// and eponymous ones
107 	static if(is(T == TemplateDeclaration)) {
108 		auto decl = cast(TemplateDecl) declObject;
109 		if(decl) {
110 			auto e = decl.eponymousMember();
111 			if(e) {
112 				if(auto a = cast(ConstructorDecl) e)
113 					return hasParam(a.astNode, name, a, false);
114 				if(auto a = cast(FunctionDecl) e)
115 					return hasParam(a.astNode, name, a, false);
116 				// FIXME: add more
117 			}
118 		}
119 	}
120 
121 	return false;
122 }
123 
124 struct LinkReferenceInfo {
125 	enum Type { none, text, link, image }
126 	Type type;
127 	bool isFootnote;
128 	string text; // or alt
129 	string link; // or src
130 
131 	string toHtml(string fun, string rt) {
132 		if(rt is null)
133 			rt = fun;
134 		if(rt is fun && text !is null && type != Type.text)
135 			rt = text;
136 
137 		string display = isFootnote ? ("[" ~ rt ~ "]") : rt;
138 
139 		Element element;
140 		final switch(type) {
141 			case Type.none:
142 				return null;
143 			case Type.text:
144 			case Type.link:
145 				element = isFootnote ? Element.make("sup", "", "footnote-ref") : Element.make("span");
146 				if(type == Type.text) {
147 					element.addChild("abbr", display).setAttribute("title", text);
148 				} else {
149 					element.addChild("a", display, link);
150 				}
151 			break;
152 			case Type.image:
153 				element = Element.make("img", link, text);
154 			break;
155 		}
156 
157 		return element.toString();
158 	}
159 }
160 
161 string[string] globalLinkReferences;
162 
163 void loadGlobalLinkReferences(string text) {
164 	foreach(line; text.splitLines) {
165 		line = line.strip;
166 		if(line.length == 0)
167 			continue;
168 		auto idx = line.indexOf("=");
169 		if(idx == -1)
170 			continue;
171 		auto name = line[0 .. idx].strip;
172 		auto value = line[idx + 1 .. $].strip;
173 		if(name.length == 0 || value.length == 0)
174 			continue;
175 		globalLinkReferences[name] = value;
176 	}
177 }
178 
179 Element getSymbolGroupReference(string name, Decl decl, string rt) {
180 	auto pm = decl.isModule() ? decl : decl.parentModule;
181 	if(pm is null)
182 		return null;
183 	if(name in pm.parsedDocComment.symbolGroups) {
184 		return Element.make("a", rt.length ? rt : name, pm.fullyQualifiedName ~ ".html#group-" ~ name);
185 	}
186 
187 	return null;
188 }
189 
190 LinkReferenceInfo getLinkReference(string name, Decl decl) {
191 	bool numeric = (name.all!((ch) => ch >= '0' && ch <= '9'));
192 
193 	bool onGlobal = false;
194 	auto refs = decl.parsedDocComment.linkReferences;
195 
196 	try_again:
197 
198 	if(name in refs) {
199 		auto txt = refs[name];
200 
201 		assert(txt.length);
202 
203 		LinkReferenceInfo lri;
204 
205 		import std.uri;
206 		auto l = uriLength(txt);
207 		if(l != -1) {
208 			lri.link = txt;
209 			lri.type = LinkReferenceInfo.Type.link;
210 		} else if(txt[0] == '$') {
211 			// should be an image or plain text
212 			if(txt.length > 5 && txt[0 .. 6] == "$(IMG " && txt[$-1] == ')') {
213 				txt = txt[6 .. $-1];
214 				auto idx = txt.indexOf(",");
215 				if(idx == -1) {
216 					lri.link = txt.strip;
217 				} else {
218 					lri.text = txt[idx + 1 .. $].strip;
219 					lri.link = txt[0 .. idx].strip;
220 				}
221 				lri.type = LinkReferenceInfo.Type.image;
222 			} else goto plain_text;
223 		} else if(txt[0] == '[' && txt[$-1] == ']') {
224 			txt = txt[1 .. $-1];
225 			auto idx = txt.indexOf("|");
226 			if(idx == -1) {
227 				lri.link = txt;
228 			} else {
229 				lri.link = txt[0 .. idx].strip;
230 				lri.text = txt[idx + 1 .. $].strip;
231 			}
232 
233 			lri.link = getReferenceLink(lri.link, decl).href;
234 
235 			lri.type = LinkReferenceInfo.Type.link;
236 		} else {
237 			plain_text:
238 			lri.type = LinkReferenceInfo.Type.text;
239 			lri.link = null;
240 			lri.text = txt;
241 		}
242 
243 		lri.isFootnote = numeric;
244 		return lri;
245 	} else if(!onGlobal && !numeric) {
246 		if(decl.parent !is null)
247 			decl = decl.parent;
248 		else {
249 			onGlobal = true;
250 			refs = globalLinkReferences;
251 			// decl = null; // so I kinda want it to be null, but that breaks like everything so I can't.
252 		}
253 		goto try_again;
254 	}
255 
256 	return LinkReferenceInfo.init;
257 }
258 
259 struct DocComment {
260 	string ddocSummary;
261 	string synopsis;
262 
263 	string details;
264 
265 	string[] params; // stored as ident=txt. You can split on first index of =.
266 	string returns;
267 	string examples;
268 	string diagnostics;
269 	string throws;
270 	string bugs;
271 	string see_alsos;
272 
273 	string[string] symbolGroups; // stored as "name" : "raw text"
274 	string[] symbolGroupsOrder;
275 	string group; // the group this belongs to, if any
276 
277 	string[string] otherSections;
278 
279 	string[string] linkReferences;
280 
281 	string[string] userDefinedMacros;
282 
283 	Decl decl;
284 
285 	bool synopsisEndedOnDoubleBlank;
286 
287 	void writeSynopsis(MyOutputRange output) {
288 		output.putTag("<div class=\"documentation-comment synopsis\">");
289 			output.putTag(formatDocumentationComment(synopsis, decl));
290 			if(details !is synopsis && details.strip.length && details.strip != "<div></div>")
291 				output.putTag(` <a id="more-link" href="#details">More...</a>`);
292 		output.putTag("</div>");
293 	}
294 
295 	void writeDetails(T = FunctionDeclaration)(MyOutputRange output) {
296 		writeDetails!T(output, cast(T) null, null);
297 	}
298 
299 	void writeDetails(T = FunctionDeclaration)(MyOutputRange output, const T functionDec = null, Decl.ProcessedUnittest[] utInfo = null) {
300 		auto f = new MyFormatter!(typeof(output))(output, decl);
301 
302 		if(params.length) {
303 			int count = 0;
304 			foreach(param; params) {
305 				auto split = param.indexOf("=");
306 				auto paramName = param[0 .. split];
307 
308 				if(!hasParam(functionDec, paramName, decl))
309 					continue;
310 				count++;
311 			}
312 
313 			if(count) {
314 				output.putTag("<h2 id=\"parameters\">Parameters</h2>");
315 				output.putTag("<dl class=\"parameter-descriptions\">");
316 				foreach(param; params) {
317 					auto split = param.indexOf("=");
318 					auto paramName = param[0 .. split];
319 
320 					if(!hasParam(functionDec, paramName, decl))
321 						continue;
322 
323 					output.putTag("<dt id=\"param-"~param[0 .. split]~"\">");
324 					output.putTag("<a href=\"#param-"~param[0 .. split]~"\" class=\"parameter-name\" data-ident=\"");
325 						output.put(param[0 .. split]);
326 					output.putTag("\">");
327 					output.put(param[0 .. split]);
328 					output.putTag("</a>");
329 
330 					static if(is(T == FunctionDeclaration) || is(T == Constructor))
331 					if(functionDec !is null) {
332 						const(Parameter)* paramAst;
333 						foreach(ref p; functionDec.parameters.parameters) {
334 							if(p.name.type != tok!"")
335 								if(p.name.text == param[0 .. split]) {
336 									paramAst = &p;
337 									break;
338 								}
339 						}
340 
341 						if(paramAst) {
342 							output.putTag(" <span class=\"parameter-type\">");
343 								f.format(paramAst.type);
344 							output.putTag("</span>");
345 						}
346 
347 
348 						if(paramAst && paramAst.atAttributes.length) {
349 							output.putTag("<div class=\"parameter-attributes\">");
350 							output.put("Attributes:");
351 							foreach (attribute; paramAst.atAttributes) {
352 								output.putTag("<div class=\"parameter-attribute\">");
353 								f.format(attribute);
354 								output.putTag("</div>");
355 							}
356 
357 							output.putTag("</div>");
358 						}
359 
360 
361 					}
362 
363 					output.putTag("</dt>");
364 					output.putTag("<dd>");
365 
366 					output.putTag("<div class=\"documentation-comment\">");
367 					output.putTag(formatDocumentationComment(param[split + 1 .. $], decl));
368 					output.putTag("</div></dd>");
369 				}
370 				output.putTag("</dl>");
371 			}
372 		}
373 
374 		if(returns !is null) {
375 			output.putTag("<h2 id=\"returns\">Return Value</h2>");
376 			output.putTag("<div>");
377 			static if(is(T == FunctionDeclaration))
378 			if(functionDec !is null) {
379 				output.putTag("<div class=\"return-type-holder\">");
380 				output.putTag("Type: ");
381 				output.putTag("<span class=\"return-type\">");
382 				if(functionDec.hasAuto && functionDec.hasRef)
383 					output.putTag(`<a class="lang-feature" href="http://dpldocs.info/auto-ref-function-return-prototype">auto ref</a> `);
384 				else {
385 					if (functionDec.hasAuto)
386 						output.putTag(`<a class="lang-feature" href="http://dpldocs.info/auto-function-return-prototype">auto</a> `);
387 					if (functionDec.hasRef)
388 						output.putTag(`<a class="lang-feature" href="http://dpldocs.info/ref-function-return-prototype">ref</a> `);
389 				}
390 
391 				if (functionDec.returnType !is null)
392 					f.format(functionDec.returnType);
393 				output.putTag("</span>");
394 				output.putTag("</div>");
395 			}
396 			output.putTag("<div class=\"documentation-comment returns-description\">");
397 			output.putTag(formatDocumentationComment(returns, decl));
398 			output.putTag("</div>");
399 			output.putTag("</div>");
400 		}
401 
402 		if(details.strip.length && details.strip != "<div></div>") {
403 			output.putTag("<h2 id=\"details\">Detailed Description</h2>");
404 			output.putTag("<div class=\"documentation-comment detailed-description\">");
405 				output.putTag(formatDocumentationComment(details, decl));
406 			output.putTag("</div>");
407 		}
408 
409 		if(throws.strip.length) {
410 			output.putTag("<h2 id=\"throws\">Throws</h2>");
411 			output.putTag("<div class=\"documentation-comment throws-description\">");
412 				output.putTag(formatDocumentationComment(throws, decl));
413 			output.putTag("</div>");
414 		}
415 
416 
417 		if(diagnostics.strip.length) {
418 			output.putTag("<h2 id=\"diagnostics\">Common Problems</h2>");
419 			output.putTag("<div class=\"documentation-comment diagnostics\">");
420 				output.putTag(formatDocumentationComment(diagnostics, decl));
421 			output.putTag("</div>");
422 		}
423 
424 		if(bugs.strip.length) {
425 			output.putTag("<h2 id=\"bugs\">Bugs</h2>");
426 			output.putTag("<div class=\"documentation-comment bugs-description\">");
427 				output.putTag(formatDocumentationComment(bugs, decl));
428 			output.putTag("</div>");
429 		}
430 
431 		bool hasUt;
432 		foreach(ut; utInfo) if(ut.embedded == false){hasUt = true; break;}
433 
434 		if(examples.length || hasUt) {
435 			output.putTag("<h2 id=\"examples\"><a href=\"#examples\" class=\"header-anchor\">Examples</a></h2>");
436 			output.putTag("<div class=\"documentation-comment\">");
437 			output.putTag(formatDocumentationComment(examples, decl));
438 			output.putTag("</div>");
439 
440 			foreach(example; utInfo) {
441 				if(example.embedded) continue;
442 				output.putTag(formatUnittestDocTuple(example, decl).toString());
443 			}
444 		}
445 
446 		if(group)
447 			see_alsos ~= "\n\n[" ~ group ~ "]";
448 
449 		if(see_alsos.length) {
450 			output.putTag("<h2 id=\"see-also\">See Also</h2>");
451 			output.putTag("<div class=\"documentation-comment see-also-section\">");
452 			output.putTag(formatDocumentationComment(see_alsos, decl));
453 			output.putTag("</div>");
454 		}
455 
456 		if(otherSections.keys.length) {
457 			output.putTag("<h2 id=\"meta\">Meta</h2>");
458 		}
459 
460 		foreach(section, content; otherSections) {
461 			output.putTag("<div class=\"documentation-comment " ~ section ~ "-section other-section\">");
462 			output.putTag("<h3>");
463 				output.put(section.capitalize);
464 			output.putTag("</h3>");
465 			output.putTag(formatDocumentationComment(content, decl));
466 			output.putTag("</div>");
467 		}
468 	}
469 }
470 
471 Element formatUnittestDocTuple(Decl.ProcessedUnittest example, Decl decl) {
472 	auto holder = Element.make("div").addClass("unittest-example-holder");
473 
474 	holder.addChild(formatDocumentationComment2(preprocessComment(example.comment, decl), decl).addClass("documentation-comment"));
475 	auto pre = holder.addChild("pre").addClass("d_code highlighted");
476 
477 	// trim off leading/trailing newlines since they just clutter output
478 	auto codeToWrite = example.code;
479 	while(codeToWrite.length && (codeToWrite[0] == '\n' || codeToWrite[0] == '\r'))
480 		codeToWrite = codeToWrite[1 .. $];
481 	while(codeToWrite.length && (codeToWrite[$-1] == '\n' || codeToWrite[$-1] == '\r'))
482 		codeToWrite = codeToWrite[0 .. $ - 1];
483 
484 	pre.innerHTML = highlight(outdent(codeToWrite));
485 
486 	return holder;
487 }
488 
489 string preprocessComment(string comment, Decl decl) {
490 	if(comment.length < 3)
491 		return comment;
492 
493 	comment = comment.replace("\r\n", "\n");
494 
495 	comment = comment[1 .. $]; // trim off the /
496 
497 	auto commentType = comment[0];
498 
499 	while(comment.length && comment[0] == commentType)
500 		comment = comment[1 .. $]; // trim off other opening spam
501 
502 	string closingSpam;
503 	if(commentType == '*' || commentType == '+') {
504 		comment = comment[0 .. $-1]; // trim off the closing /
505 		bool closingSpamFound;
506 		while(comment.length && comment[$-1] == commentType) {
507 			comment = comment[0 .. $-1]; // trim off other closing spam
508 			closingSpamFound = true;
509 		}
510 
511 		if(closingSpamFound) {
512 			// if there was closing spam we also want to count the spaces before it
513 			// to trim off other line spam. The goal here is to clean up
514 			    /**
515 			     * Resolve host name.
516 			     * Returns: false if unable to resolve.
517 			     */
518 			// into just "Resolve host name.\n Returns: false if unable to resolve."
519 			// give or take some irrelevant whitespace.
520 			while(comment.length && (comment[$-1] == '\t' || comment[$-1] == ' ')) {
521 				closingSpam = comment[$-1] ~ closingSpam;
522 				comment = comment[0 .. $-1]; // trim off other closing spam
523 			}
524 
525 			if(closingSpam.length == 0)
526 				closingSpam = " "; // some things use the " *" leader, but still end with "*/" on a line of its own
527 		}
528 	}
529 
530 
531 	string poop;
532 	if(commentType == '/')
533 		poop = "///";
534 	else
535 		poop = closingSpam ~ commentType ~ " ";
536 
537 	string newComment;
538 	foreach(line; comment.splitter("\n")) {
539 		// check for "  * some text"
540 		if(line.length >= poop.length && line.startsWith(poop)) {
541 			if(line.length > poop.length && line[poop.length] == ' ')
542 				newComment ~= line[poop.length + 1 .. $];
543 			else
544 				newComment ~= line[poop.length .. $];
545 		}
546 		// check for an empty line with just "  *"
547 		else if(line.length == poop.length-1 && line[0..poop.length-1] == poop[0..$-1])
548 			{} // this space is intentionally left blank; it is an empty line
549 		else if(line.length > 1 && commentType != '/' && line[0] == commentType && line[1] == ' ')
550 			newComment ~= line[1 .. $]; // cut off stupid leading * with no space before too
551 		else
552 			newComment ~= line;
553 		newComment ~= "\n";
554 	}
555 
556 	comment = newComment;
557 	return specialPreprocess(comment, decl);
558 }
559 
560 DocComment parseDocumentationComment(string comment, Decl decl) {
561 	DocComment c;
562 
563 	if(generatingSource) {
564 		if(decl.lineNumber)
565 			c.otherSections["source"] ~= "$(LINK2 source/"~decl.parentModule.name~".d.html#L"~to!string(decl.lineNumber)~", See Implementation)$(BR)";
566 		else if(!decl.fakeDecl)
567 			c.otherSections["source"] ~= "$(LINK2 source/"~decl.parentModule.name~".d.html, See Source File)$(BR)";
568 	}
569 	// FIXME: add links to ddoc and ddox iff std.* or core.*
570 
571 	c.decl = decl;
572 
573 	comment = preprocessComment(comment, decl);
574 
575 	void parseSections(string comment) {
576 		string remaining;
577 		string section;
578 		bool inSynopsis = true;
579 		bool justSawBlank = false;
580 		bool inCode = false;
581 		string inCodeStyle;
582 		bool hasAnySynopsis = false;
583 		bool inDdocSummary = true;
584 		bool synopsisEndedOnDoubleBlank = false;
585 
586 		string currentMacroName;
587 
588 		// for symbol_groups
589 		string* currentValue;
590 
591 		bool maybeGroupLine = true;
592 
593 
594 		auto lastLineHelper = comment.strip;
595 		auto lastLine = lastLineHelper.lastIndexOf("\n");
596 		if(lastLine != -1) {
597 			auto lastLineText = lastLineHelper[lastLine .. $].strip;
598 			if(lastLineText.startsWith("Group: ") || lastLineText.startsWith("group: ")) {
599 				c.group = lastLineText["Group: ".length .. $];
600 				maybeGroupLine = false;
601 
602 				comment = lastLineHelper[0 .. lastLine].strip;
603 			}
604 		}
605 
606 		foreach(line; comment.splitter("\n")) {
607 
608 			if(maybeGroupLine) {
609 				maybeGroupLine = false;
610 				if(line.strip.startsWith("Group: ") || line.strip.startsWith("group: ")) {
611 					c.group = line.strip["Group: ".length .. $]; // cut off closing paren
612 					continue;
613 				}
614 			}
615 
616 			auto maybe = line.strip.toLower;
617 
618 			if(maybe.startsWith("---") || maybe.startsWith("```")) {
619 				justSawBlank = false;
620 
621 				if(inCode && inCodeStyle == maybe[0 .. 3]) {
622 					inCode = false;
623 					inCodeStyle = null;
624 				} else if(inCode)
625 					goto ss; // just still inside code section
626 				else {
627 					inCodeStyle = maybe[0 .. 3];
628 					inDdocSummary = false;
629 					inCode = !inCode;
630 				}
631 			}
632 			if(inCode){ 
633 				justSawBlank = false;
634 				goto ss; // sections never change while in a code example
635 			}
636 
637 			if(inSynopsis && hasAnySynopsis && maybe.length == 0) {
638 				// any empty line ends the ddoc summary
639 				inDdocSummary = false;
640 				// two blank lines in a row ends the synopsis
641 				if(justSawBlank) {
642 					inSynopsis = false;
643 					// synopsis can also end on the presence of another
644 					// section, so this is tracked to see how intentional
645 					// the break looked (Phobos docs aren't written with
646 					// a double-break in mind)
647 					synopsisEndedOnDoubleBlank = true;
648 				}
649 				justSawBlank = true;
650 			} else {
651 				justSawBlank = false;
652 			}
653 
654 			if(maybe.startsWith("params:")) {
655 				section = "params";
656 				inSynopsis = false;
657 			} else if(maybe.startsWith("parameters:")) {
658 				section = "params";
659 				inSynopsis = false;
660 			} else if(maybe.startsWith("returns:")) {
661 				section = "returns";
662 				line = line[line.indexOf(":")+1 .. $];
663 				inSynopsis = false;
664 			} else if(maybe.startsWith("throws:")) {
665 				section = "throws";
666 				inSynopsis = false;
667 				line = line[line.indexOf(":")+1 .. $];
668 			} else if(maybe.startsWith("author:")) {
669 				section = "authors";
670 				line = line[line.indexOf(":")+1 .. $];
671 				inSynopsis = false;
672 			} else if(maybe.startsWith("details:")) {
673 				section = "details";
674 				line = line[line.indexOf(":")+1 .. $];
675 				inSynopsis = false;
676 			} else if(maybe.startsWith("authors:")) {
677 				section = "authors";
678 				line = line[line.indexOf(":")+1 .. $];
679 				inSynopsis = false;
680 			} else if(maybe.startsWith("source:")) {
681 				section = "source";
682 				line = line[line.indexOf(":")+1 .. $];
683 				inSynopsis = false;
684 			} else if(maybe.startsWith("history:")) {
685 				section = "history";
686 				line = line[line.indexOf(":")+1 .. $];
687 				inSynopsis = false;
688 			} else if(maybe.startsWith("credits:")) {
689 				section = "credits";
690 				line = line[line.indexOf(":")+1 .. $];
691 				inSynopsis = false;
692 			} else if(maybe.startsWith("version:")) {
693 				section = "version";
694 				line = line[line.indexOf(":")+1 .. $];
695 				inSynopsis = false;
696 			} else if(maybe.startsWith("since:")) {
697 				section = "since";
698 				line = line[line.indexOf(":")+1 .. $];
699 				inSynopsis = false;
700 			} else if(maybe.startsWith("license:")) {
701 				section = "license";
702 				line = line[line.indexOf(":")+1 .. $];
703 				inSynopsis = false;
704 			} else if(maybe.startsWith("copyright:")) {
705 				section = "copyright";
706 				line = line[line.indexOf(":")+1 .. $];
707 				inSynopsis = false;
708 			} else if(maybe.startsWith("see_also:")) {
709 				section = "see_also";
710 				inSynopsis = false;
711 				line = line[line.indexOf(":")+1 .. $];
712 			} else if(maybe.startsWith("diagnostics:")) {
713 				inSynopsis = false;
714 				section = "diagnostics";
715 				line = line[line.indexOf(":")+1 .. $];
716 			} else if(maybe.startsWith("examples:")) {
717 				inSynopsis = false;
718 				section = "examples";
719 				line = line[line.indexOf(":")+1 .. $];
720 			} else if(maybe.startsWith("example:")) {
721 				inSynopsis = false;
722 				line = line[line.indexOf(":")+1 .. $];
723 				section = "examples"; // Phobos uses example, the standard is examples.
724 			} else if(maybe.startsWith("standards:")) {
725 				line = line[line.indexOf(":")+1 .. $];
726 				inSynopsis = false;
727 				section = "standards";
728 			} else if(maybe.startsWith("deprecated:")) {
729 				inSynopsis = false;
730 				section = "deprecated";
731 			} else if(maybe.startsWith("date:")) {
732 				inSynopsis = false;
733 				section = "date";
734 			} else if(maybe.startsWith("bugs:")) {
735 				line = line[line.indexOf(":")+1 .. $];
736 				inSynopsis = false;
737 				section = "bugs";
738 			} else if(maybe.startsWith("macros:")) {
739 				inSynopsis = false;
740 				section = "macros";
741 				currentMacroName = null;
742 			} else if(maybe.startsWith("symbol_groups:")) {
743 				section = "symbol_groups";
744 				line = line[line.indexOf(":")+1 .. $];
745 				inSynopsis = false;
746 			} else if(maybe.startsWith("link_references:")) {
747 				inSynopsis = false;
748 				section = "link_references";
749 				line = line[line.indexOf(":")+1 .. $].strip;
750 			} else if(maybe.isDdocSection()) {
751 				inSynopsis = false;
752 
753 				auto idx2 = line.indexOf(":");
754 				auto name = line[0 .. idx2].replace("_", " ");
755 				line = "$(H3 "~name~")\n\n" ~ line[idx2+1 .. $];
756 			} else {
757 				// no change to section
758 			}
759 
760 			if(inSynopsis == false)
761 				inDdocSummary = false;
762 
763 
764 			ss: switch(section) {
765 				case "symbol_groups":
766 					// basically a copy/paste of Params but it is coincidental
767 					// they might not always share syntax.
768 					bool lookingForIdent = true;
769 					bool inIdent;
770 					bool skippingSpace;
771 					size_t space_at;
772 
773 					auto lol = line.strip;
774 					foreach(idx, ch; lol) {
775 						import std.uni, std.ascii : isAlphaNum;
776 						if(lookingForIdent && !inIdent) {
777 							if(!isAlpha(ch) && ch != '_')
778 								continue;
779 							inIdent = true;
780 							lookingForIdent = false;
781 						}
782 
783 						if(inIdent) {
784 							if(ch == '_' || isAlphaNum(ch) || isAlpha(ch))
785 								continue;
786 							else {
787 								skippingSpace = true;
788 								inIdent = false;
789 								space_at = idx;
790 							}
791 						}
792 						if(skippingSpace) {
793 							if(!isWhite(ch)) {
794 								if(ch == '=') {
795 									// we finally hit a thingy
796 									auto currentName = lol[0 .. space_at];
797 
798 									c.symbolGroups[currentName] = lol[idx + 1 .. $] ~ "\n";
799 									c.symbolGroupsOrder ~= currentName;
800 									currentValue = currentName in c.symbolGroups;
801 									break ss;
802 								} else
803 									// we are expecting whitespace or = and hit
804 									// neither.. this can't be ident = desc!
805 									break;
806 							}
807 						}
808 					}
809 
810 					if(currentValue !is null) {
811 						*currentValue ~= line ~ "\n";
812 					}
813 				break;
814 				case "params":
815 					bool lookingForIdent = true;
816 					bool inIdent;
817 					bool skippingSpace;
818 					size_t space_at;
819 
820 					auto lol = line.strip;
821 					foreach(idx, ch; lol) {
822 						import std.uni, std.ascii : isAlphaNum;
823 						if(lookingForIdent && !inIdent) {
824 							if(!isAlpha(ch) && ch != '_')
825 								continue;
826 							inIdent = true;
827 							lookingForIdent = false;
828 						}
829 
830 						if(inIdent) {
831 							if(ch == '_' || isAlphaNum(ch) || isAlpha(ch))
832 								continue;
833 							else {
834 								skippingSpace = true;
835 								inIdent = false;
836 								space_at = idx;
837 							}
838 						}
839 						if(skippingSpace) {
840 							if(!isWhite(ch)) {
841 								if(ch == '=') {
842 									// we finally hit a thingy
843 									c.params ~= lol[0 .. space_at] ~ "=" ~ lol[idx + 1 .. $] ~ "\n";
844 									break ss;
845 								} else
846 									// we are expecting whitespace or = and hit
847 									// neither.. this can't be ident = desc!
848 									break;
849 							}
850 						}
851 					}
852 
853 					if(c.params.length)
854 						c.params[$-1] ~= line ~ "\n";
855 				break;
856 				case "see_also":
857 					c.see_alsos ~= line ~ "\n";
858 				break;
859 				case "macros":
860 					// ignoring for now
861 
862 					bool lookingForIdent = true;
863 					bool inIdent;
864 					bool skippingSpace;
865 					size_t space_at;
866 
867 					auto lol = line.strip;
868 					foreach(idx, ch; lol) {
869 						import std.uni, std.ascii : isAlphaNum;
870 						if(lookingForIdent && !inIdent) {
871 							if(!isAlpha(ch) && ch != '_')
872 								continue;
873 							inIdent = true;
874 							lookingForIdent = false;
875 						}
876 
877 						if(inIdent) {
878 							if(ch == '_' || isAlphaNum(ch) || isAlpha(ch))
879 								continue;
880 							else {
881 								skippingSpace = true;
882 								inIdent = false;
883 								space_at = idx;
884 							}
885 						}
886 						if(skippingSpace) {
887 							if(!isWhite(ch)) {
888 								if(ch == '=') {
889 									// we finally hit a thingy
890 									currentMacroName = lol[0 .. space_at].strip;
891 									auto currentMacroBody = lol[idx + 1 .. $] ~ "\n";
892 
893 									c.userDefinedMacros[currentMacroName] = currentMacroBody;
894 									break ss;
895 								} else
896 									// we are expecting whitespace or = and hit
897 									// neither.. this can't be ident = desc!
898 									break;
899 							}
900 						} else {
901 							if(currentMacroName.length)
902 								c.userDefinedMacros[currentMacroName] ~= lol;
903 						}
904 					}
905 				break;
906 				case "link_references":
907 					auto eql = line.indexOf("=");
908 					if(eql != -1) {
909 						string name = line[0 .. eql].strip;
910 						string value = line[eql + 1 .. $].strip;
911 						c.linkReferences[name] = value;
912 					} else if(line.strip.length) {
913 						section = null;
914 						goto case_default;
915 					}
916 				break;
917 				case "returns":
918 					c.returns ~= line ~ "\n";
919 				break;
920 				case "diagnostics":
921 					c.diagnostics ~= line ~ "\n";
922 				break;
923 				case "throws":
924 					c.throws ~= line ~ "\n";
925 				break;
926 				case "bugs":
927 					c.bugs ~= line ~ "\n";
928 				break;
929 				case "authors":
930 				case "license":
931 				case "source":
932 				case "history":
933 				case "credits":
934 				case "standards":
935 				case "copyright":
936 				case "version":
937 				case "since":
938 				case "date":
939 					c.otherSections[section] ~= line ~ "\n";
940 				break;
941 				case "details":
942 					c.details ~= line ~ "\n";
943 				break;
944 				case "examples":
945 					c.examples ~= line ~ "\n";
946 				break;
947 				default:
948 				case_default:
949 					if(inSynopsis) {
950 						if(inDdocSummary)
951 							c.ddocSummary ~= line ~ "\n";
952 						c.synopsis ~= line ~ "\n";
953 						if(line.length)
954 							hasAnySynopsis = true;
955 					} else
956 						remaining ~= line ~ "\n";
957 			}
958 		}
959 
960 		c.details ~= remaining;
961 		c.synopsisEndedOnDoubleBlank = synopsisEndedOnDoubleBlank;
962 	}
963 
964 	parseSections(comment);
965 
966 	return c;
967 }
968 
969 bool isDdocSection(string s) {
970 	bool hasUnderscore = false;
971 	foreach(idx, char c; s) {
972 		if(c == '_')
973 			hasUnderscore = true;
974 		if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'))
975 			return hasUnderscore && c == ':';
976 	}
977 	return false;
978 }
979 
980 bool isIdentifierChar(size_t idx, dchar ch) {
981 	import std.uni;
982 	if(!(isAlpha(ch) || (idx != 0 && isNumber(ch)) || ch == '_'))
983 		return false;
984 	return true;
985 }
986 
987 bool isIdentifierOrUrl(string text) {
988 	import std.uri;
989 	auto l = uriLength(text);
990 
991 	if(l != -1)
992 		return true; // is url
993 
994 	if(text.length && text[0] == '#')
995 		return true; // is local url link
996 
997 	bool seenHash;
998 
999 	import std.uni;
1000 	foreach(idx, dchar ch; text) {
1001 		if(!isIdentifierChar(idx, ch) && ch != '.') {
1002 			if(!seenHash && ch == '#') {
1003 				seenHash = true;
1004 				continue;
1005 			} else if(seenHash && ch == '-') {
1006 				continue;
1007 			}
1008 			return false;
1009 		}
1010 	}
1011 
1012 	// passed the ident test...
1013 	if(text.length)
1014 		return true;
1015 
1016 	return false;
1017 
1018 }
1019 
1020 Element getReferenceLink(string text, Decl decl, string realText = null) {
1021 	import std.uri;
1022 	auto l = uriLength(text);
1023 	string hash;
1024 
1025 	string className;
1026 
1027 	if(l != -1) { // a url
1028 		text = text;
1029 		if(realText is null)
1030 			realText = text;
1031 	} else if(text.length && text[0] == '#') {
1032 		// an anchor. add the link so it works when this is copy/pasted on other pages too (such as in search results)
1033 		hash = text;
1034 		text = decl.link;
1035 
1036 		if(realText is null) {
1037 			realText = hash[1 .. $].replace("-", " ");
1038 		}
1039 	} else {
1040 		if(realText is null)
1041 			realText = text;
1042 
1043 		auto hashIdx = text.indexOf("#");
1044 		if(hashIdx != -1) {
1045 			hash = text[hashIdx .. $];
1046 			text = text[0 .. hashIdx];
1047 		}
1048 
1049 		auto found = decl.lookupName(text);
1050 
1051 		className = "xref";
1052 
1053 		if(found is null) {
1054 			auto lastPieceIdx = text.lastIndexOf(".");
1055 			if(lastPieceIdx != -1) {
1056 				found = decl.lookupName(text[0 .. lastPieceIdx]);
1057 				if(found && hash is null)
1058 					hash = "#" ~ text[lastPieceIdx + 1 .. $];
1059 			}
1060 		}
1061 
1062 		if(found !is null)
1063 			text = found.link;
1064 		else if(auto c = text in allClasses) {
1065 			// classes may be thrown and as such can be referenced externally without import
1066 			// doing this as kinda a hack.
1067 			text = (*c).link;
1068 		} else
1069 			text ~= ".html";
1070 	}
1071 
1072 	auto element = Element.make("a");
1073 	element.className = className;
1074 	element.href = getDirectoryForPackage(text) ~ text ~ hash;
1075 	element.innerText = realText;
1076 	return element;
1077 }
1078 
1079 struct DocCommentTerminationCondition {
1080 	string[] terminationStrings;
1081 	bool mustBeAtStartOfLine;
1082 	string remaining;
1083 	string terminatedOn;
1084 }
1085 
1086 Element formatDocumentationComment2(string comment, Decl decl, string tagName = "div", DocCommentTerminationCondition* termination = null) {
1087 	Element div;
1088 	if(tagName is null)
1089 		div = new DocumentFragment(null);
1090 	else
1091 		div = Element.make(tagName);
1092 
1093 	string currentParagraph;
1094 	void putch(char c) {
1095 		currentParagraph ~= c;
1096 	}
1097 	void put(in char[] c) {
1098 		currentParagraph ~= c;
1099 	}
1100 
1101 	string currentTag = (tagName is null) ? null : "p";
1102 	string currentClass = null;
1103 
1104 	void commit() {
1105 		auto cp = currentParagraph.strip;
1106 		if(cp.length) {
1107 			if(currentTag is null)
1108 				div.appendHtml(cp);
1109 			else {
1110 				if(currentTag == "p") {
1111 					// check for a markdown style header
1112 					auto test = cp.strip;
1113 					auto lines = test.splitLines();
1114 					if(lines.length == 2) {
1115 						auto hdr = lines[0].strip;
1116 						auto equals = lines[1].strip;
1117 						if(equals.length >= 4) {
1118 							foreach(ch; equals)
1119 								if(ch != '=')
1120 									goto not_special;
1121 						} else {
1122 							goto not_special;
1123 						}
1124 
1125 						if(hdr.length == 0)
1126 							goto not_special;
1127 
1128 						// passed tests, I'll allow it:
1129 						currentTag = "h3";
1130 						currentClass = "user-header";
1131 						cp = hdr;
1132 					} else if(lines.length == 1) {
1133 						int hashCount = 0;
1134 						foreach(idx, ch; test) {
1135 							if(ch == '#')
1136 								hashCount++;
1137 							else if(ch == ' ' && hashCount > 0) {
1138 								// it was special!
1139 								// there must be text after btw or else strip would have cut the space too
1140 								currentTag = "h" ~ to!string(hashCount);
1141 								currentClass = "user-header";
1142 								cp = test[idx + 1 .. $];
1143 
1144 								break;
1145 							} else
1146 								break; // not special
1147 						}
1148 					}
1149 				}
1150 
1151 				not_special:
1152 				div.addChild(currentTag, Html(cp), currentClass);
1153 			}
1154 		}
1155 		currentTag = "p";
1156 		currentParagraph = null;
1157 		currentClass = null;
1158 	}
1159 
1160 	bool atStartOfLine = true;
1161 	bool earlyTermination;
1162 
1163 	main_loop:
1164 	for(size_t idx = 0; idx < comment.length; idx++) {
1165 		auto ch = comment[idx];
1166 		auto remaining = comment[idx .. $];
1167 
1168 		if(termination !is null) {
1169 			if((termination.mustBeAtStartOfLine && atStartOfLine) || !termination.mustBeAtStartOfLine) {
1170 				foreach(ts; termination.terminationStrings) {
1171 					if(remaining.startsWith(ts)) {
1172 						termination.remaining = remaining[ts.length .. $];
1173 						earlyTermination = true;
1174 						termination.terminatedOn = ts;
1175 						break main_loop;
1176 					}
1177 				}
1178 			}
1179 		}
1180 
1181 		switch(ch) {
1182 			case '\r':
1183 				continue; // don't care even a little about Windows vs Unix line endings
1184 			case ' ', '\t':
1185 				goto useCharWithoutTriggeringStartOfLine;
1186 			case '\n':
1187 				if(atStartOfLine)
1188 					commit();
1189 				atStartOfLine = true;
1190 				goto useCharWithoutTriggeringStartOfLine;
1191 			break;
1192 			// these need to be html encoded
1193 			case '"':
1194 				put("&quot;");
1195 				atStartOfLine = false;
1196 			break;
1197 			case '<':
1198 				// FIXME: support just a wee bit of inline html...
1199 				put("&lt;");
1200 				atStartOfLine = false;
1201 			break;
1202 			case '>':
1203 				put("&gt;");
1204 				atStartOfLine = false;
1205 			break;
1206 			case '&':
1207 				put("&amp;");
1208 				atStartOfLine = false;
1209 			break;
1210 			// special syntax
1211 			case '`':
1212 				// code. ` is inline, ``` is block.
1213 				if(atStartOfLine && remaining.startsWith("```")) {
1214 					idx += 3;
1215 					remaining = remaining[3 .. $];
1216 					bool braced = false;
1217 					if(remaining.length && remaining[0] == '{') {
1218 						braced = true;
1219 						remaining = remaining[1 .. $];
1220 					}
1221 
1222 					auto line = remaining.indexOf("\n");
1223 					string language;
1224 					if(line != -1) {
1225 						language = remaining[0 .. line].strip;
1226 						remaining = remaining[line + 1 .. $];
1227 						idx += line + 1;
1228 					}
1229 
1230 					size_t ending;
1231 					size_t sliceEnding;
1232 					if(braced) {
1233 						int count = 1;
1234 						while(ending < remaining.length) {
1235 							if(remaining[ending] == '{')
1236 								count++;
1237 							if(remaining[ending] == '}')
1238 								count--;
1239 
1240 							if(count == 0)
1241 								break;
1242 							ending++;
1243 						}
1244 
1245 						sliceEnding = ending;
1246 
1247 						while(ending < remaining.length) {
1248 							if(remaining[ending] == '\n') {
1249 								ending++;
1250 								break;
1251 							}
1252 							ending++;
1253 						}
1254 
1255 					} else {
1256 						ending = remaining.indexOf("```\n");
1257 						if(ending != -1) {
1258 							sliceEnding = ending;
1259 							ending += 4; // skip \n
1260 						} else {
1261 							ending = remaining.indexOf("```\r");
1262 
1263 							if(ending != -1) {
1264 								sliceEnding = ending;
1265 								ending += 5; // skip \r\n
1266 							} else {
1267 								ending = remaining.length;
1268 								sliceEnding = ending;
1269 							}
1270 						}
1271 					}
1272 
1273 					if(currentTag == "p")
1274 						commit();
1275 					// FIXME: we can prolly do more with languages...
1276 					string code = outdent(stripRight(remaining[0 .. sliceEnding]));
1277 					Element ele;
1278 					// all these languages are close enough for hack good enough.
1279 					if(language == "javascript" || language == "c" || language == "c++" || language == "java" || language == "php" || language == "c#" || language == "d" || language == "adrscript" || language == "json")
1280 						ele = div.addChild("pre", syntaxHighlightCFamily(code, language));
1281 					else if(language == "css")
1282 						ele = div.addChild("pre", syntaxHighlightCss(code));
1283 					else if(language == "html" || language == "xml")
1284 						ele = div.addChild("pre", syntaxHighlightHtml(code, language));
1285 					else if(language == "python")
1286 						ele = div.addChild("pre", syntaxHighlightPython(code));
1287 					else if(language == "ruby")
1288 						ele = div.addChild("pre", syntaxHighlightRuby(code));
1289 					else if(language == "sdlang")
1290 						ele = div.addChild("pre", syntaxHighlightSdlang(code));
1291 					else
1292 						ele = div.addChild("pre", code);
1293 					ele.addClass("block-code");
1294 					ele.dataset.language = language;
1295 					idx += ending;
1296 				} else {
1297 					// check for inline `code` style
1298 					bool foundIt = false;
1299 					size_t foundItWhere = 0;
1300 					foreach(i, scout; remaining[1 .. $]) {
1301 						if(scout == '\n')
1302 							break;
1303 						if(scout == '`') {
1304 							foundIt = true;
1305 							foundItWhere = i;
1306 							break;
1307 						}
1308 					}
1309 
1310 					if(!foundIt)
1311 						goto ordinary;
1312 
1313 					atStartOfLine = false;
1314 					auto slice = remaining[1 .. foundItWhere + 1];
1315 					idx += 1 + foundItWhere;
1316 
1317 					if(slice.length == 0) {
1318 						// empty code block is `` - doubled backtick. Just
1319 						// treat that as a literal (escaped) backtick.
1320 						putch('`');
1321 					} else {
1322 						auto ele = Element.make("tt").addClass("inline-code");
1323 						ele.innerText = slice;
1324 						put(ele.toString());
1325 					}
1326 				}
1327 			break;
1328 			case '$':
1329 				// ddoc macro. May be magical.
1330 				auto info = macroInformation(remaining);
1331 				if(info.linesSpanned == 0) {
1332 					goto ordinary;
1333 				}
1334 
1335 				atStartOfLine = false;
1336 
1337 				auto name = remaining[2 .. info.macroNameEndingOffset];
1338 
1339 				auto dzeroAdjustment = (info.textBeginningOffset == info.terminatingOffset) ? 0 : 1;
1340 				auto text = remaining[info.textBeginningOffset .. info.terminatingOffset - dzeroAdjustment];
1341 
1342 				if(name in ddocMacroBlocks) {
1343 					commit();
1344 
1345 					auto got = expandDdocMacros(remaining, decl);
1346 					div.appendChild(got);
1347 				} else {
1348 					put(expandDdocMacros(remaining, decl).toString());
1349 				}
1350 
1351 				idx += info.terminatingOffset - 1;
1352 			break;
1353 			case '-':
1354 				// ddoc style code iff --- at start of line.
1355 				if(!atStartOfLine) {
1356 					goto ordinary;
1357 				}
1358 				if(remaining.startsWith("---")) {
1359 					// a code sample has started
1360 					if(currentTag == "p")
1361 						commit();
1362 					div.addChild("pre", extractDdocCodeExample(remaining, idx)).addClass("d_code highlighted");
1363 					atStartOfLine = true;
1364 				} else
1365 					goto ordinary;
1366 			break;
1367 			case '!':
1368 				// possible markdown-style image
1369 				if(remaining.length > 1 && remaining[1] == '[') {
1370 					auto inside = remaining[1 .. $];
1371 					auto txt = extractBalanceOnSingleLine(inside);
1372 					if(txt is null)
1373 						goto ordinary;
1374 					if(txt.length < inside.length && inside[txt.length] == '(') {
1375 						// possible markdown image
1376 						auto parens = extractBalanceOnSingleLine(inside[txt.length .. $]);
1377 						if(parens.length) {
1378 							auto a = Element.make("img");
1379 							a.alt = txt[1 .. $-1];
1380 							a.src = parens[1 .. $-1];
1381 							put(a.toString());
1382 
1383 							idx ++; // skip the !
1384 							idx += parens.length;
1385 							idx += txt.length;
1386 							idx --; // so the ++ in the for loop brings us back to i
1387 							break;
1388 						}
1389 					}
1390 				}
1391 				goto ordinary;
1392 			break;
1393 			case '[':
1394 				// wiki-style reference iff [text] or [text|other text]
1395 				// or possibly markdown style link: [text](url)
1396 				// text MUST be either a valid D identifier chain, optionally with a section hash,
1397 				// or a fully-encoded http url.
1398 				// text may never include ']' or '|' or whitespace or ',' and must always start with '_' or alphabetic (like a D identifier or http url)
1399 				// other text may include anything except the string ']'
1400 				// it balances [] inside it.
1401 
1402 				auto txt = extractBalanceOnSingleLine(remaining);
1403 				if(txt is null)
1404 					goto ordinary;
1405 				if(txt.length < remaining.length && remaining[txt.length] == '(') {
1406 					// possible markdown link
1407 					auto parens = extractBalanceOnSingleLine(remaining[txt.length .. $]);
1408 					if(parens.length) {
1409 						auto a = Element.make("a", txt[1 .. $-1], parens[1 .. $-1]);
1410 						put(a.toString());
1411 
1412 						idx += parens.length;
1413 						idx += txt.length;
1414 						idx --; // so the ++ in the for loop brings us back to i
1415 						break;
1416 					}
1417 				}
1418 
1419 				auto fun = txt[1 .. $-1];
1420 				string rt;
1421 				auto idx2 = fun.indexOf("|");
1422 				if(idx2 != -1) {
1423 					rt = fun[idx2 + 1 .. $].strip;
1424 					fun = fun[0 .. idx2].strip;
1425 				}
1426 
1427 				if(fun.all!((ch) => ch >= '0' && ch <= '9')) {
1428 					// footnote reference
1429 					auto lri = getLinkReference(fun, decl);
1430 					if(lri.type == LinkReferenceInfo.Type.none)
1431 						goto ordinary;
1432 					put(lri.toHtml(fun, rt));
1433 				} else if(!fun.isIdentifierOrUrl()) {
1434 					goto ordinary;
1435 				} else {
1436 					auto lri = getLinkReference(fun, decl);
1437 					if(lri.type == LinkReferenceInfo.Type.none) {
1438 						if(auto l = getSymbolGroupReference(fun, decl, rt)) {
1439 							put(l.toString());
1440 						} else {
1441 							put(getReferenceLink(fun, decl, rt).toString);
1442 						}
1443 					} else
1444 						put(lri.toHtml(fun, rt));
1445 				}
1446 
1447 				idx += txt.length;
1448 				idx --; // so the ++ in the for loop brings us back to i
1449 			break;
1450 			case 'h':
1451 				// automatically linkify web URLs pasted in
1452 				if(tagName is null)
1453 					goto ordinary;
1454 				import std.uri;
1455 				auto l = uriLength(remaining);
1456 				if(l == -1)
1457 					goto ordinary;
1458 				auto url = remaining[0 .. l];
1459 				put("<a href=\"" ~ url ~ "\">" ~ url ~ "</a>");
1460 				idx += url.length;
1461 				idx --; // so the ++ puts us at the end
1462 			break;
1463 			case '_':
1464 				// is it that stupid ddocism where a PSYMBOL is prefixed with _?
1465 				// still, really, I want to KILL this completely, I hate it a lot
1466 				if(idx && !isIdentifierChar(1, comment[idx-1]) && checkStupidDdocIsm(remaining[1..$], decl))
1467 					break;
1468 				else
1469 					goto ordinary;
1470 			break;
1471 			/*
1472 				/+ I'm just not happy with this yet because it thinks
1473 
1474 				Paragraph
1475 					* list item
1476 				After stuff
1477 
1478 				the After stuff is part of the list item. Blargh.
1479 
1480 				+/
1481 			case '*':
1482 				// potential list item
1483 				if(atStartOfLine) {
1484 					commit();
1485 					currentTag = "li";
1486 					atStartOfLine = false;
1487 					continue;
1488 				} else {
1489 					goto ordinary;
1490 				}
1491 			break;
1492 			*/
1493 			default:
1494 			ordinary:
1495 				atStartOfLine = false;
1496 				useCharWithoutTriggeringStartOfLine:
1497 				putch(ch);
1498 		}
1499 	}
1500 
1501 	commit();
1502 
1503 	if(termination !is null && !earlyTermination)
1504 		termination.remaining = null;
1505 
1506 	/*
1507 	if(div.firstChild !is null && div.firstChild.nodeType == NodeType.Text
1508 	   && div.firstChild.nextSibling !is null && div.firstChild.nextSibling.tagName == "p")
1509 	     div.firstChild.wrapIn(Element.make("p"));
1510 	*/
1511 
1512 	return div;
1513 }
1514 
1515 bool checkStupidDdocIsm(string remaining, Decl decl) {
1516 	size_t i;
1517 	foreach(idx, dchar ch; remaining) {
1518 		if(!isIdentifierChar(idx, ch)) {
1519 			i = idx;
1520 			break;
1521 		}
1522 	}
1523 
1524 	string ident = remaining[0 ..  i];
1525 	if(ident.length == 0)
1526 		return false;
1527 
1528 	// check this name
1529 	if(ident == decl.name) {
1530 		import std.stdio; writeln("stupid ddocism(1): ", ident);
1531 		return true;
1532 	}
1533 
1534 	if(decl.isModule) {
1535 		auto name = decl.name;
1536 		auto idx = name.lastIndexOf(".");
1537 		if(idx != -1) {
1538 			name = name[idx + 1 .. $];
1539 		}
1540 
1541 		if(ident == name) {
1542 			import std.stdio; writeln("stupid ddocism(2): ", ident);
1543 			return true;
1544 		}
1545 	}
1546 
1547 	// check the whole scope too, I think dmd does... maybe
1548 	if(decl.parentModule && decl.parentModule.lookupName(ident) !is null) {
1549 		import std.stdio; writeln("stupid ddocism(3): ", ident);
1550 		return true;
1551 	}
1552 
1553 	// FIXME: params?
1554 
1555 	return false;
1556 }
1557 
1558 string extractBalanceOnSingleLine(string txt) {
1559 	if(txt.length == 0) return null;
1560 	char starter = txt[0];
1561 	char terminator;
1562 	switch(starter) {
1563 		case '[':
1564 			terminator = ']';
1565 		break;
1566 		case '(':
1567 			terminator = ')';
1568 		break;
1569 		case '{':
1570 			terminator = '}';
1571 		break;
1572 		default:
1573 			return null;
1574 	}
1575 
1576 	int count;
1577 	foreach(idx, ch; txt) {
1578 		if(ch == '\n')
1579 			return null;
1580 		if(ch == starter)
1581 			count++;
1582 		else if(ch == terminator)
1583 			count--;
1584 
1585 		if(count == 0)
1586 			return txt[0 .. idx + 1];
1587 	}
1588 
1589 	return null; // unbalanced prolly
1590 }
1591 
1592 Html extractDdocCodeExample(string comment, ref size_t idx) {
1593 	assert(comment.startsWith("---"));
1594 	auto i = comment.indexOf("\n");
1595 	if(i == -1)
1596 		return Html(htmlEncode(comment).idup);
1597 	comment = comment[i + 1 .. $];
1598 	idx += i + 1;
1599 
1600 	LexerConfig config;
1601 	StringCache stringCache = StringCache(128);
1602 
1603 	config.stringBehavior = StringBehavior.source;
1604 	config.whitespaceBehavior = WhitespaceBehavior.include;
1605 
1606 	ubyte[] lies = cast(ubyte[]) comment;
1607 
1608 	// I'm tokenizing to handle stuff like --- inside strings and
1609 	// comments inside the code. If we hit an "operator" -- followed by -
1610 	// or -- followed by --, right after some whitespace including a line, 
1611 	// we're done.
1612 	bool justSawNewLine = true;
1613 	bool justSawDashDash = false;
1614 	size_t terminatingIndex;
1615 	lexing:
1616 	foreach(token; byToken(lies, config, &stringCache)) {
1617 		if(justSawDashDash) {
1618 			if(token == tok!"--" || token == tok!"-") {
1619 				break lexing;
1620 			}
1621 		}
1622 
1623 		if(justSawNewLine) {
1624 			if(token == tok!"--") {
1625 				justSawDashDash = true;
1626 				justSawNewLine = false;
1627 				terminatingIndex = token.index;
1628 				continue lexing;
1629 			}
1630 		}
1631 
1632 		if(token == tok!"whitespace") {
1633 			foreach(txt; token.text)
1634 				if(txt == '\n') {
1635 					justSawNewLine = true;
1636 					continue lexing;
1637 				}
1638 		}
1639 
1640 		justSawNewLine = false;
1641 	}
1642 
1643 	i = comment.indexOf('\n', terminatingIndex);
1644 	if(i == -1)
1645 		i = comment.length;
1646 
1647 	idx += i;
1648 
1649 	comment = comment[0 .. terminatingIndex];
1650 
1651 	return Html(outdent(highlight(comment.stripRight)));
1652 }
1653 
1654 string[string] ddocMacros;
1655 int[string] ddocMacroInfo;
1656 int[string] ddocMacroBlocks;
1657 
1658 static this() {
1659 	ddocMacroBlocks = [
1660 		"ADRDOX_SAMPLE" : 1,
1661 		"LIST": 1,
1662 		"NUMBERED_LIST": 1,
1663 		"SMALL_TABLE" : 1,
1664 		"TABLE_ROWS" : 1,
1665 		"EMBED_UNITTEST": 1,
1666 
1667 		"TIP":1,
1668 		"NOTE":1,
1669 		"WARNING":1,
1670 		"PITFALL":1,
1671 		"SIDEBAR":1,
1672 		"CONSOLE":1,
1673 		"H1":1,
1674 		"H2":1,
1675 		"H3":1,
1676 		"H4":1,
1677 		"H5":1,
1678 		"H5":1,
1679 		"HR":1,
1680 		"BOOKTABLE":1,
1681 		"T2":1,
1682 
1683 		"TR" : 1,
1684 		"TH" : 1,
1685 		"TD" : 1,
1686 		"TABLE" : 1,
1687 		"TDNW" : 1,
1688 		"LEADINGROWN" : 1,
1689 
1690 		"UL" : 1,
1691 		"OL" : 1,
1692 		"LI" : 1,
1693 
1694 		"P" : 1,
1695 		"PRE" : 1,
1696 		"BLOCKQUOTE" : 1,
1697 		"DL" : 1,
1698 		"DT" : 1,
1699 		"DD" : 1,
1700 		"POST" : 1,
1701 		"DIV" : 1,
1702 		"SIDE_BY_SIDE" : 1,
1703 		"COLUMN" : 1,
1704 
1705 		"DIVC" : 1,
1706 
1707 		// std.regex
1708 		"REG_ROW": 1,
1709 		"REG_TITLE": 1,
1710 		"REG_TABLE": 1,
1711 		"REG_START": 1,
1712 		"SECTION": 1,
1713 
1714 		// std.math
1715 		"TABLE_SV" : 1,
1716 		"TABLE_DOMRG": 2,
1717 		"DOMAIN": 1,
1718 		"RANGE": 1,
1719 		"SVH": 1,
1720 		"SV": 1,
1721 
1722 		"UDA_USES" : 1,
1723 		"C_HEADER_DESCRIPTION": 1,
1724 	];
1725 
1726 	ddocMacros = [
1727 		"FULLY_QUALIFIED_NAME" : "MAGIC", // decl.fullyQualifiedName,
1728 		"MODULE_NAME" : "MAGIC", // decl.parentModule.fullyQualifiedName,
1729 		"D" : "<tt class=\"D\">$0</tt>", // this is magical! D is actually handled in code.
1730 		"REF" : `<a href="$0.html">$0</a>`, // this is magical! Handles ref in code
1731 
1732 		"ALWAYS_DOCUMENT" : "",
1733 
1734 		"UDA_USES" : "",
1735 
1736 		"TIP" : "<div class=\"tip\">$0</div>",
1737 		"NOTE" : "<div class=\"note\">$0</div>",
1738 		"WARNING" : "<div class=\"warning\">$0</div>",
1739 		"PITFALL" : "<div class=\"pitfall\">$0</div>",
1740 		"SIDEBAR" : "<div class=\"sidebar\"><aside>$0</aside></div>",
1741 		"CONSOLE" : "<pre class=\"console\">$0</pre>",
1742 
1743 		// headers have meaning to the table of contents generator
1744 		"H1" : "<h1 class=\"user-header\">$0</h1>",
1745 		"H2" : "<h2 class=\"user-header\">$0</h2>",
1746 		"H3" : "<h3 class=\"user-header\">$0</h3>",
1747 		"H4" : "<h4 class=\"user-header\">$0</h4>",
1748 		"H5" : "<h5 class=\"user-header\">$0</h5>",
1749 		"H6" : "<h6 class=\"user-header\">$0</h6>",
1750 
1751 		"BLOCKQUOTE" : "<blockquote>$0</blockquote>", // FIXME?
1752 
1753 		"HR" : "<hr />",
1754 
1755 		"DASH" : "&#45;",
1756 		"NOTHING" : "", // used to escape magic syntax sometimes
1757 
1758 		// DDoc just expects these.
1759 		"AMP" : "&",
1760 		"LT" : "<",
1761 		"GT" : ">",
1762 		"LPAREN" : "(",
1763 		"RPAREN" : ")",
1764 		"DOLLAR" : "$",
1765 		"BACKTICK" : "`",
1766 		"COMMA": ",",
1767 		"COLON": ":",
1768 		"ARGS" : "$0",
1769 
1770 		// support for my docs' legacy components, will be removed before too long.
1771 		"DDOC_ANCHOR" : "<a id=\"$0\" href=\"$(FULLY_QUALIFIED_NAME).html#$0\">$0</a>",
1772 
1773 		"DDOC_COMMENT" : "",
1774 
1775 		// this is support for the Phobos docs
1776 
1777 		// useful parts
1778 		"BOOKTABLE" : "<table class=\"phobos-booktable\"><caption>$1</caption>$+</table>",
1779 		"T2" : "<tr><td>$(LREF $1)</td><td>$+</td></tr>",
1780 
1781 		"BIGOH" : "<span class=\"big-o\"><i>O</i>($0)</span>",
1782 
1783 		"TR" : "<tr>$0</tr>",
1784 		"TH" : "<th>$0</th>",
1785 		"TD" : "<td>$0</td>",
1786 		"TABLE": "<table>$0</table>",
1787 		"TDNW" : "<td style=\"white-space: nowrap;\">$0</td>",
1788 		"LEADINGROWN":"<tr class=\"leading-row\"><th colspan=\"$1\">$2</th></tr>",
1789 		"LEADINGROW":"<tr class=\"leading-row\"><th colspan=\"2\">$0</th></tr>",
1790 
1791 		"UL" : "<ul>$0</ul>",
1792 		"OL" : "<ol>$0</ol>",
1793 		"LI" : "<li>$0</li>",
1794 
1795 		"BR" : "<br />",
1796 		"I" : "<i>$0</i>",
1797 		"EM" : "<em>$0</em>",
1798 		"STRONG" : "<strong>$0</strong>",
1799 		"TT" : "<tt>$0</tt>",
1800 		"B" : "<b>$0</b>",
1801 		"STRIKE" : "<s>$0</s>",
1802 		"P" : "<p>$0</p>",
1803 		"PRE" : "<pre>$0</pre>",
1804 		"DL" : "<dl>$0</dl>",
1805 		"DT" : "<dt>$0</dt>",
1806 		"DD" : "<dd>$0</dd>",
1807 		"LINK2" : "<a href=\"$1\">$2</a>",
1808 		"LINK" : `<a href="$0">$0</a>`,
1809 
1810 		"DDLINK": "<a href=\"http://dlang.org/$1\">$3</a>",
1811 		"DDSUBLINK": "<a href=\"http://dlang.org/$1#$2\">$3</a>",
1812 
1813 		"IMG": "<img src=\"$1\" alt=\"$2\" />",
1814 
1815 		"BLUE" : "<span style=\"color: blue;\">$0</span>",
1816 		"RED" : "<span style=\"color: red;\">$0</span>",
1817 
1818 		"SCRIPT" : "",
1819 
1820 		// Useless crap that should just be replaced
1821 		"REF_SHORT" : `<a href="$2.$3.$1.html">$1</a>`,
1822 		"SHORTREF" : `<a href="$2.$3.$1.html">$1</a>`,
1823 		"SUBMODULE" : `<a href="$(FULLY_QUALIFIED_NAME).$2.html">$1</a>`,
1824 		"SHORTXREF" : `<a href="std.$1.$2.html">$2</a>`,
1825 		"SHORTXREF_PACK" : `<a href="std.$1.$2.$3.html">$3</a>`,
1826 		"XREF" : `<a href="std.$1.$2.html">std.$1.$2</a>`,
1827 		"CXREF" : `<a href="core.$1.$2.html">core.$1.$2</a>`,
1828 		"XREF_PACK" : `<a href="std.$1.$2.$3.html">std.$1.$2.$3</a>`,
1829 		"MYREF" : `<a href="$(FULLY_QUALIFIED_NAME).$0.html">$0</a>`,
1830 		"LREF" : `<a class="symbol-reference" href="$(MODULE_NAME).$0.html">$0</a>`,
1831 		"LREF2" : "MAGIC",
1832 		"MREF" : `MAGIC`,
1833 		"WEB" : `<a href="http://$1">$2</a>`,
1834 		"HTTP" : `<a href="http://$1">$2</a>`,
1835 		"BUGZILLA": `<a href="https://issues.dlang.org/show_bug.cgi?id=$1">https://issues.dlang.org/show_bug.cgi?id=$1</a>`,
1836 		"XREF_PACK_NAMED" : `<a href="std.$1.$2.$3.html">$4</a>`,
1837 
1838 		"D_INLINECODE" : `<tt>$1</tt>`,
1839 		"D_STRING" : `<tt>$1</tt>`,
1840 		"D_CODE_STRING" : `<tt>$1</tt>`,
1841 		"D_CODE" : `<pre>$0</pre>`,
1842 		"HTTPS" : `<a href="https://$1">$2</a>`,
1843 
1844 		"RES": `<i>result</i>`,
1845 		"POST": `<div class="postcondition">$0</div>`,
1846 		"COMMENT" : ``,
1847 
1848 		"DIV" : `<div>$0</div>`,
1849 		"SIDE_BY_SIDE" : `<table class="side-by-side"><tbody><tr>$0</tr></tbody></table>`,
1850 		"COLUMN" : `<td>$0</td>`,
1851 		"DIVC" : `<div class="$1">$+</div>`,
1852 
1853 		// std.regex
1854 		"REG_ROW":`<tr><td><i>$1</i></td><td>$+</td></tr>`,
1855 		"REG_TITLE":`<tr><td><b>$1</b></td><td><b>$2</b></td></tr>`,
1856 		"REG_TABLE":`<table border="1" cellspacing="0" cellpadding="5" > $0 </table>`,
1857 		"REG_START":`<h3><div align="center"> $0 </div></h3>`,
1858 		"SECTION":`<h3><a id="$0">$0</a></h3>`,
1859 		"S_LINK":`<a href="#$1">$+</a>`,
1860 
1861 		// std.math
1862 
1863 		"TABLE_SV": `<table class="std_math special-values"><caption>Special Values</caption>$0</table>`,
1864 		"SVH" : `<tr><th>$1</th><th>$2</th></tr>`,
1865 		"SV" : `<tr><td>$1</td><td>$2</td></tr>`,
1866 		"TABLE_DOMRG": `<table class="std_math domain-and-range">$1 $2</table>`,
1867 		"DOMAIN": `<tr><th>Domain</th><td>$0</td></tr>`,
1868 		"RANGE": `<tr><th>Range</th><td>$0</td></tr>`,
1869 
1870 		"PI": "\u03c0",
1871 		"INFIN": "\u221e",
1872 		"SUB": "<span>$1<sub>$2</sub></span>",
1873 		"SQRT" : "\u221a",
1874 		"SUPERSCRIPT": "<sup>$1</sup>",
1875 		"POWER": "<span>$1<sup>$2</sup></span>",
1876 		"NAN": "<span class=\"nan\">NaN</span>",
1877 		"PLUSMN": "\u00b1",
1878 		"GLOSSARY": `<a href="http://dlang.org/glosary.html#$0">$0</a>`,
1879 		"PHOBOSSRC": `<a href="https://github.com/dlang/phobos/blob/master/$0">$0</a>`,
1880 		"DRUNTIMESRC": `<a href="https://github.com/dlang/druntime/blob/master/src/$0">$0</a>`,
1881 		"C_HEADER_DESCRIPTION": `<p>This module contains bindings to selected types and functions from the standard C header <a href="http://$1">&lt;$2&gt;</a>. Note that this is not automatically generated, and may omit some types/functions from the original C header.</p>`,
1882 	];
1883 
1884 	ddocMacroInfo = [ // number of arguments expected, if needed
1885 		"EMBED_UNITTEST": 1,
1886 		"BOOKTABLE" : 1,
1887 		"T2" : 1,
1888 		"LEADINGROWN" : 2,
1889 		"WEB" : 2,
1890 		"HTTP" : 2,
1891 		"BUGZILLA" : 1,
1892 		"XREF_PACK_NAMED" : 4,
1893 		"D_INLINECODE" : 1,
1894 		"D_STRING" : 1,
1895 		"D_CODE_STRING": 1,
1896 		"D_CODE": 1,
1897 		"HTTPS": 2,
1898 		"DIVC" : 1,
1899 		"LINK2" : 2,
1900 		"DDLINK" : 3,
1901 		"DDSUBLINK" : 3,
1902 		"IMG" : 2,
1903 		"XREF" : 2,
1904 		"CXREF" : 2,
1905 		"SHORTXREF" : 2,
1906 		"SHORTREF": 3,
1907 		"REF_SHORT": 3,
1908 		"SUBMODULE" : 2,
1909 		"SHORTXREF_PACK" : 3,
1910 		"XREF_PACK" : 3,
1911 		"MREF" : 1,
1912 		"LREF2" : 2,
1913 		"C_HEADER_DESCRIPTION": 2,
1914 
1915 		"REG_ROW" : 1,
1916 		"REG_TITLE" : 2,
1917 		"S_LINK" : 1,
1918 		"PI": 1,
1919 		"SUB": 2,
1920 		"SQRT" : 1,
1921 		"SUPERSCRIPT": 1,
1922 		"POWER": 2,
1923 		"NAN": 1,
1924 		"PLUSMN": 1,
1925 		"SVH" : 2,
1926 		"SV": 2,
1927 	];
1928 }
1929 
1930 string formatDocumentationComment(string comment, Decl decl) {
1931 	// remove that annoying ddocism to suppress its auto-highlight anti-feature
1932 	// FIXME
1933 	/*
1934 	auto psymbols = fullyQualifiedName[$-1].split(".");
1935 	// also trim off our .# for overload
1936 	import std.uni :isAlpha;
1937 	auto psymbol = (psymbols.length && psymbols[$-1].length) ?
1938 		((psymbols[$-1][0].isAlpha || psymbols[$-1][0] == '_') ? psymbols[$-1] : psymbols[$-2])
1939 		:
1940 		null;
1941 	if(psymbol.length)
1942 		comment = comment.replace("_" ~ psymbol, psymbol);
1943 	*/
1944 
1945 	auto data = formatDocumentationComment2(comment, decl).toString();
1946 
1947         return data;
1948 }
1949 
1950 
1951 // This is kinda deliberately not recursive right now. I might change that later but I want to keep it
1952 // simple to get decent results while keeping the door open to dropping ddoc macros.
1953 Element expandDdocMacros(string txt, Decl decl) {
1954 	auto e = expandDdocMacros2(txt, decl);
1955 
1956 	translateMagicCommands(e);
1957 
1958 	return e;
1959 }
1960 
1961 void translateMagicCommands(Element container, Element applyTo = null) {
1962 	foreach(cmd; container.querySelectorAll("magic-command")) {
1963 		Element subject;
1964 		if(applyTo is null) {
1965 			subject = cmd.parentNode;
1966 			if(subject is null)
1967 				continue;
1968 			if(subject.tagName == "caption")
1969 				subject = subject.parentNode;
1970 			if(subject is null)
1971 				continue;
1972 		} else {
1973 			subject = applyTo;
1974 		}
1975 
1976 		if(cmd.className.length) {
1977 			subject.addClass(cmd.className);
1978 		}
1979 		if(cmd.id.length)
1980 			subject.id = cmd.id;
1981 		cmd.removeFromTree();
1982 	}
1983 }
1984 
1985 Element expandDdocMacros2(string txt, Decl decl) {
1986 	auto macros = ddocMacros;
1987 	auto numberOfArguments = ddocMacroInfo;
1988 
1989 	auto userDefinedMacros = decl.parsedDocComment.userDefinedMacros;
1990 
1991 	string[string] availableMacros;
1992 	int[string] availableNumberOfArguments;
1993 
1994 	if(userDefinedMacros.length) {
1995 		foreach(name, value; userDefinedMacros) {
1996 			availableMacros[name] = value;
1997 			int count;
1998 			foreach(idx, ch; value) {
1999 				if(ch == '$') {
2000 					if(idx + 1 < value.length) {
2001 						char n = value[idx + 1];
2002 						if(n >= '0' && n <= '9') {
2003 							if(n - '0' > count)
2004 								count = n - '0';
2005 						}
2006 					}
2007 				}
2008 			}
2009 			if(count)
2010 				availableNumberOfArguments[name] = count;
2011 		}
2012 
2013 		foreach(name, value; macros)
2014 			availableMacros[name] = value;
2015 		foreach(name, n; numberOfArguments)
2016 			availableNumberOfArguments[name] = n;
2017 	} else {
2018 		availableMacros = macros;
2019 		availableNumberOfArguments = numberOfArguments;
2020 	}
2021 
2022 	auto idx = 0;
2023 	if(txt[idx] == '$') {
2024 		auto info = macroInformation(txt[idx .. $]);
2025 		if(info.linesSpanned == 0)
2026 			assert(0);
2027 
2028 		if(idx + 2 > txt.length)
2029 			assert(0);
2030 		if(idx + info.macroNameEndingOffset > txt.length)
2031 			assert(0, txt[idx .. $]);
2032 		if(info.macroNameEndingOffset < 2)
2033 			assert(0, txt[idx..$]);
2034 
2035 		auto name = txt[idx + 2 .. idx + info.macroNameEndingOffset];
2036 
2037 		auto dzeroAdjustment = (info.textBeginningOffset == info.terminatingOffset) ? 0 : 1;
2038 		auto stuff = txt[idx + info.textBeginningOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2039 		auto stuffNonStripped = txt[idx + info.macroNameEndingOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2040 		string replacement;
2041 
2042 		if(name == "RAW_HTML") {
2043 			// magic: user-defined html
2044 			auto holder = Element.make("div", "", "user-raw-html");
2045 			holder.innerHTML = stuff;
2046 			return holder;
2047 		}
2048 
2049 		if(name == "ID")
2050 			return Element.make("magic-command").setAttribute("id", stuff);
2051 		if(name == "CLASS")
2052 			return Element.make("magic-command").setAttribute("class", stuff);
2053 
2054 		if(name == "EMBED_UNITTEST") {
2055 			auto id = stuff.strip;
2056 			foreach(ref ut; decl.getProcessedUnittests()) {
2057 				if(ut.comment.canFind("$(ID " ~ id ~ ")")) {
2058 					ut.embedded = true;
2059 					return formatUnittestDocTuple(ut, decl);
2060 				}
2061 			}
2062 		}
2063 
2064 		if(name == "SMALL_TABLE") {
2065 			return translateSmallTable(stuff, decl);
2066 		}
2067 		if(name == "TABLE_ROWS") {
2068 			return translateListTable(stuff, decl);
2069 		}
2070 		if(name == "LIST") {
2071 			return translateList(stuff, decl, "ul");
2072 		}
2073 		if(name == "NUMBERED_LIST") {
2074 			return translateList(stuff, decl, "ol");
2075 		}
2076 
2077 		if(name == "MATH") {
2078 			import adrdox.latex;
2079 			auto got = mathToImgHtml(stuff);
2080 			if(got is null)
2081 				return Element.make("span", stuff, "user-math-render-failed");
2082 			return got;
2083 		}
2084 
2085 		if(name == "ADRDOX_SAMPLE") {
2086 			// show the original source as code
2087 			// then render it for display side-by-side
2088 			auto holder = Element.make("div");
2089 			holder.addClass("adrdox-sample");
2090 
2091 			auto h = holder.addChild("div");
2092 
2093 			h.addChild("pre", outdent(stuffNonStripped));
2094 			h.addChild("div", formatDocumentationComment2(stuff, decl));
2095 
2096 			return holder;
2097 		}
2098 
2099 		if(name == "D" || name == "D_PARAM" || name == "D_CODE") {
2100 			// this is magic: syntax highlight it
2101 			auto holder = Element.make(name == "D_CODE" ? "pre" : "tt");
2102 			holder.className = "D highlighted";
2103 			try {
2104 				holder.innerHTML = linkUpHtml(highlight(stuff), decl);
2105 			} catch(Throwable t) {
2106 				holder.innerText = stuff;
2107 			}
2108 			return holder;
2109 		}
2110 
2111 		if(name == "UDA_USES") {
2112 			Element div = Element.make("dl");
2113 			foreach(d; declsByUda(decl.name, decl.parentModule)) {
2114 				if(!d.docsShouldBeOutputted)
2115 					continue;
2116 				auto dt = div.addChild("dt");
2117 				dt.addChild("a", d.name, d.link);
2118 				div.addChild("dd", Html(formatDocumentationComment(d.parsedDocComment.ddocSummary, d)));
2119 			}
2120 			return div;
2121 		}
2122 
2123 		if(name == "MODULE_NAME") {
2124 			return new TextNode(decl.parentModule.fullyQualifiedName);
2125 		}
2126 		if(name == "FULLY_QUALIFIED_NAME") {
2127 			return new TextNode(decl.fullyQualifiedName);
2128 		}
2129 		if(name == "SUBREF") {
2130 			auto cool = stuff.split(",");
2131 			if(cool.length == 2)
2132 				return getReferenceLink(decl.fullyQualifiedName ~ "." ~ cool[0].strip ~ "." ~ cool[1].strip, decl, cool[1].strip);
2133 		}
2134 		if(name == "MREF_ALTTEXT") {
2135 			auto parts = split(stuff, ",");
2136 			string cool;
2137 			foreach(indx, p; parts[1 .. $]) {
2138 				if(indx) cool ~= ".";
2139 				cool ~= p.strip;
2140 			}
2141 
2142 			return getReferenceLink(cool, decl, parts[0].strip);
2143 		}
2144 		if(name == "LREF2") {
2145 			auto parts = split(stuff, ",");
2146 			return getReferenceLink(parts[1].strip ~ "." ~ parts[0].strip, decl, parts[0].strip);
2147 		}
2148 		if(name == "REF_ALTTEXT") {
2149 			auto parts = split(stuff, ",");
2150 			string cool;
2151 			foreach(indx, p; parts[2 .. $]) {
2152 				if(indx) cool ~= ".";
2153 				cool ~= p.strip;
2154 			}
2155 			cool ~= "." ~ parts[1].strip;
2156 
2157 			return getReferenceLink(cool, decl, parts[0].strip);
2158 		}
2159 
2160 		if(name == "NBSP") {
2161 			return new TextNode("\&nbsp;");
2162 		}
2163 
2164 		if(name == "REF" || name == "MREF" || name == "LREF" || name == "MYREF") {
2165 			// this is magic: do commas to dots then link it
2166 			string cool;
2167 			if(name == "REF") {
2168 				auto parts = split(stuff, ",");
2169 				parts = parts[1 .. $] ~ parts[0];
2170 				foreach(indx, p; parts) {
2171 					if(indx)
2172 						cool ~= ".";
2173 					cool ~= p.strip;
2174 				}
2175 			} else
2176 				cool = replace(stuff, ",", ".").replace(" ", "");
2177 			return getReferenceLink(cool, decl);
2178 		}
2179 
2180 
2181 		if(auto replacementRaw = name in availableMacros) {
2182 			if(auto nargsPtr = name in availableNumberOfArguments) {
2183 				auto nargs = *nargsPtr;
2184 				replacement = *replacementRaw;
2185 
2186 				auto orig = txt[idx + info.textBeginningOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2187 
2188 				foreach(i; 1 .. nargs + 1) {
2189 					int blargh = -1;
2190 
2191 					int parens;
2192 					foreach(cidx, ch; stuff) {
2193 						if(ch == '(')
2194 							parens++;
2195 						if(ch == ')')
2196 							parens--;
2197 						if(parens == 0 && ch == ',') {
2198 							blargh = cast(int) cidx;
2199 							break;
2200 						}
2201 					}
2202 					if(blargh == -1)
2203 						blargh = cast(int) stuff.length;
2204 						//break; // insufficient args
2205 					//blargh = stuff.length - 1; // trims off the closing paren
2206 
2207 					auto magic = stuff[0 .. blargh].strip;
2208 					if(blargh < stuff.length)
2209 						stuff = stuff[blargh + 1 .. $];
2210 					else
2211 						stuff = stuff[$..$];
2212 					// FIXME: this should probably not be full replace each time.
2213 					//if(magic.length)
2214 					magic = formatDocumentationComment2(magic, decl, null).innerHTML;
2215 
2216 					if(name == "T2" && i == 1)
2217 						replacement = replace(replacement, "$(LREF $1)", getReferenceLink(magic, decl).toString);
2218 					else
2219 						replacement = replace(replacement, "$" ~ cast(char)(i + '0'), magic);
2220 				}
2221 
2222 				// FIXME: recursive replacement doesn't work...
2223 
2224 				auto awesome = stuff.strip;
2225 				if(replacement.indexOf("$+") != -1) {
2226 					awesome = formatDocumentationComment2(awesome, decl, null).innerHTML;
2227 					replacement = replace(replacement, "$+", awesome);
2228 				}
2229 
2230 				if(replacement.indexOf("$0") != -1) {
2231 					awesome = formatDocumentationComment2(orig, decl, null).innerHTML;
2232 					replacement = replace(*replacementRaw, "$0", awesome);
2233 				}
2234 			} else {
2235 				// if there is any text, we slice off the ), otherwise, keep the empty string
2236 				auto awesome = txt[idx + info.textBeginningOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2237 
2238 				if(name == "CONSOLE") {
2239 					awesome = outdent(txt[idx + info.macroNameEndingOffset .. idx + info.terminatingOffset - dzeroAdjustment]).strip;
2240 					awesome = awesome.replace("\n---", "\n$(NOTHING)---"); // disable ddoc embedded D code as it breaks for D exception messages
2241 				}
2242 
2243 
2244 				awesome = formatDocumentationComment2(awesome, decl, null).innerHTML;
2245 				replacement = replace(*replacementRaw, "$0", awesome);
2246 			}
2247 
2248 			Element element;
2249 			if(replacement != "<" && replacement.strip.startsWith("<")) {
2250 				element = Element.make("placeholder");
2251 
2252 				element.innerHTML = replacement;
2253 				element = element.childNodes[0];
2254 				element.parentNode = null;
2255 
2256 				foreach(k, v; element.attributes)
2257 					element.attrs[k] = v.replace("$(FULLY_QUALIFIED_NAME)", decl.fullyQualifiedName).replace("$(MODULE_NAME)", decl.parentModule.fullyQualifiedName);
2258 			} else {
2259 				element = new TextNode(replacement);
2260 			}
2261 
2262 			return element;
2263 		} else {
2264 			idx = idx + cast(int) info.terminatingOffset;
2265 			auto textContent = txt[0 .. info.terminatingOffset];
2266 			return new TextNode(textContent);
2267 		}
2268 	} else assert(0);
2269 
2270 
2271 	return null;
2272 }
2273 
2274 struct MacroInformation {
2275 	size_t macroNameEndingOffset; // starting is always 2 if it is actually a macro (check linesSpanned > 0)
2276 	size_t textBeginningOffset;
2277 	size_t terminatingOffset;
2278 	int linesSpanned;
2279 }
2280 
2281 // Scans the current macro
2282 MacroInformation macroInformation(in char[] str) {
2283 	assert(str[0] == '$');
2284 
2285 	MacroInformation info;
2286 	info.terminatingOffset = str.length;
2287 
2288 	if (str.length < 2 || (str[1] != '(' && str[1] != '{'))
2289 		return info;
2290 
2291 	auto open = str[1];
2292 	auto close = str[1] == '(' ? ')' : '}';
2293 
2294 	bool readingMacroName = true;
2295 	bool readingLeadingWhitespace;
2296 	int parensCount = 0;
2297 	foreach (idx, char ch; str)
2298 	{
2299 		if (readingMacroName && (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == ','))
2300 		{
2301 			readingMacroName = false;
2302 			readingLeadingWhitespace = true;
2303 			info.macroNameEndingOffset = idx;
2304 		}
2305 
2306 		if (readingLeadingWhitespace && !(ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'))
2307 		{
2308 			readingLeadingWhitespace = false;
2309 			// This is looking past the macro name
2310 			// so the offset of $(FOO bar) ought to be
2311 			// the index of "bar"
2312 			info.textBeginningOffset = idx;
2313 		}
2314 
2315 		// so i want :) and :( not to count
2316 		// also prolly 1) and 2) etc.
2317 
2318 		if (ch == '\n')
2319 			info.linesSpanned++;
2320 		if (ch == open)
2321 			parensCount++;
2322 		if (ch == close)
2323 		{
2324 			parensCount--;
2325 			if (parensCount == 0)
2326 			{
2327 				if(readingMacroName)
2328 					info.macroNameEndingOffset = idx;
2329 				info.linesSpanned++; // counts the first line it is on
2330 				info.terminatingOffset = idx + 1;
2331 				if (info.textBeginningOffset == 0)
2332 					info.textBeginningOffset = info.terminatingOffset;
2333 				break;
2334 			}
2335 		}
2336 	}
2337 
2338 	if(parensCount)
2339 		info.linesSpanned = 0; // unterminated macro, tell upstream it is plain text
2340 
2341 	return info;
2342 }
2343 
2344 
2345 
2346 string outdent(string s) {
2347 	static import std..string;
2348 	try {
2349 		return join(std..string.outdent(splitLines(s)), "\n");
2350 	} catch(Exception) {
2351 		return s;
2352 	}
2353 }
2354 
2355 
2356 //      Original code:
2357 //          Copyright Brian Schott (Hackerpilot) 2012.
2358 // Distributed under the Boost Software License, Version 1.0.
2359 //    (See accompanying file LICENSE_1_0.txt or copy at
2360 //          http://www.boost.org/LICENSE_1_0.txt)
2361 // Adam modified
2362 
2363 // http://ethanschoonover.com/solarized
2364 string highlight(string sourceCode)
2365 {
2366     import std.array;
2367     import dparse.lexer;
2368     string ret;
2369 
2370     StringCache cache = StringCache(StringCache.defaultBucketCount);
2371     LexerConfig config;
2372     config.stringBehavior = StringBehavior.source;
2373     auto tokens = byToken(cast(ubyte[]) sourceCode, config, &cache);
2374 
2375 
2376     void writeSpan(string cssClass, string value, string dataIdent = "")
2377     {
2378         ret ~= `<span class="` ~ cssClass ~ `" `;
2379 	if(dataIdent.length)
2380 		ret ~= "data-ident=\""~dataIdent~"\"";
2381 	ret ~= `>` ~ value.replace("&", "&amp;").replace("<", "&lt;") ~ `</span>`;
2382     }
2383 
2384 
2385 	while (!tokens.empty)
2386 	{
2387 		auto t = tokens.front;
2388 		tokens.popFront();
2389 		if (isBasicType(t.type))
2390 			writeSpan("type", str(t.type));
2391 		else if(t.text == "string") // string is a de-facto basic type, though it is technically just a user-defined identifier
2392 			writeSpan("type", t.text);
2393 		else if (isKeyword(t.type))
2394 			writeSpan("kwrd", str(t.type));
2395 		else if (t.type == tok!"comment") {
2396 			auto txt = t.text;
2397 			if(txt == "/* adrdox_highlight{ */")
2398 				ret ~= "<span class=\"specially-highlighted\">";
2399 			else if(txt == "/* }adrdox_highlight */")
2400 				ret ~= "</span>";
2401 			else
2402 				writeSpan("com", t.text);
2403 		} else if (isStringLiteral(t.type) || t.type == tok!"characterLiteral") {
2404 			auto txt = t.text;
2405 			if(txt.startsWith("q{")) {
2406 				ret ~= "<span class=\"token-string-literal\"><span class=\"str\">q{</span>";
2407 				ret ~= highlight(txt[2 .. $ - 1]);
2408 				ret ~= "<span class=\"str\">}</span></span>";
2409 			} else {
2410 				writeSpan("str", txt);
2411 			}
2412 		} else if (isNumberLiteral(t.type))
2413 			writeSpan("num", t.text);
2414 		else if (isOperator(t.type))
2415 			//writeSpan("op", str(t.type));
2416 			ret ~= htmlEncode(str(t.type));
2417 		else if (t.type == tok!"specialTokenSequence" || t.type == tok!"scriptLine")
2418 			writeSpan("cons", t.text);
2419 		else if (t.type == tok!"identifier")
2420 			writeSpan("hid", t.text);
2421 		else
2422 		{
2423 			ret ~= t.text.replace("<", "&lt;");
2424 		}
2425 
2426 	}
2427 	return ret;
2428 }
2429 
2430 
2431 
2432 
2433 import dparse.formatter;
2434 
2435 class MyFormatter(Sink) : Formatter!Sink {
2436 	Decl context;
2437 	this(Sink sink, Decl context = null, bool useTabs = true, IndentStyle style = IndentStyle.otbs, uint indentWidth = 4)
2438 	{
2439 		this.context = context;
2440 		super(sink, useTabs, style, indentWidth);
2441 	}
2442 
2443 	void putTag(in char[] s) {
2444 		static if(!__traits(compiles, sink.putTag("")))
2445 			sink.put(s);
2446 		else
2447 			sink.putTag(s);
2448 	}
2449 
2450 	override void put(string s1) {
2451 		auto s = cast(const(char)[]) s1;
2452 		static if(!__traits(compiles, sink.putTag("")))
2453 			sink.put(s.htmlEncode);
2454 		else
2455 			sink.put(s);
2456 	}
2457 
2458 	override void format(const AtAttribute atAttribute) {
2459 		if(atAttribute is null || atAttribute.argumentList is null)
2460 			super.format(atAttribute);
2461 		else {
2462 			sink.put("@");
2463 			if(atAttribute.argumentList.items.length == 1) {
2464 				format(atAttribute.argumentList.items[0]);
2465 			} else {
2466 				format(atAttribute.argumentList);
2467 			}
2468 		}
2469 	}
2470 
2471 	override void format(const TemplateParameterList templateParameterList)
2472 	{
2473 		putTag("<div class=\"parameters-list\">");
2474 		foreach(i, param; templateParameterList.items)
2475 		{
2476 			putTag("<div class=\"template-parameter-item parameter-item\">");
2477 			put("\t");
2478 			putTag("<span>");
2479 			format(param);
2480 			putTag("</span>");
2481 			putTag("</div>");
2482 		}
2483 		putTag("</div>");
2484 	}
2485 
2486 	override void format(const InStatement inStatement) {
2487 		putTag("<a href=\"http://dpldocs.info/in-contract\" class=\"lang-feature\">");
2488 		put("in");
2489 		putTag("</a>");
2490 		//put(" ");
2491 		if(inStatement.blockStatement)
2492 			format(inStatement.blockStatement);
2493 		else if(inStatement.expression) {
2494 			put(" (");
2495 			format(inStatement.expression);
2496 			put(")");
2497 		}
2498 	}
2499 
2500 	override void format(const OutStatement outStatement) {
2501 		putTag("<a href=\"http://dpldocs.info/out-contract\" class=\"lang-feature\">");
2502 		put("out");
2503 		putTag("</a>");
2504 
2505 		//put(" ");
2506 		if(outStatement.expression) {
2507 			put(" (");
2508 			format(outStatement.parameter);
2509 			put("; ");
2510 			format(outStatement.expression);
2511 			put(")");
2512 		} else {
2513 			if (outStatement.parameter != tok!"")
2514 			{
2515 			    put(" (");
2516 			    format(outStatement.parameter);
2517 			    put(")");
2518 			}
2519 
2520 			format(outStatement.blockStatement);
2521 		}
2522 	}
2523 
2524 	override void format(const AssertExpression assertExpression)
2525 	{
2526 		debug(verbose) writeln("AssertExpression");
2527 
2528 		/**
2529 		  AssignExpression assertion;
2530 		  AssignExpression message;
2531 		 **/
2532 
2533 		with(assertExpression)
2534 		{
2535 			putTag("<a href=\"http://dpldocs.info/assertion\" class=\"lang-feature\">");
2536 			put("assert");
2537 			putTag("</a> (");
2538 			format(assertion);
2539 			if (message)
2540 			{
2541 				put(", ");
2542 				format(message);
2543 			}
2544 			put(")");
2545 		}
2546 	}
2547 
2548 
2549 	override void format(const TemplateAliasParameter templateAliasParameter)
2550 	{
2551 		debug(verbose) writeln("TemplateAliasParameter");
2552 
2553 		/**
2554 		  Type type;
2555 		  Token identifier;
2556 		  Type colonType;
2557 		  AssignExpression colonExpression;
2558 		  Type assignType;
2559 		  AssignExpression assignExpression;
2560 		 **/
2561 
2562 		with(templateAliasParameter)
2563 		{
2564 			putTag("<a href=\"http://dpldocs.info/template-alias-parameter\" class=\"lang-feature\">");
2565 			put("alias");
2566 			putTag("</a>");
2567 			put(" ");
2568 			if (type)
2569 			{
2570 				format(type);
2571 				space();
2572 			}
2573 			format(identifier);
2574 			if (colonType)
2575 			{
2576 				put(" : ");
2577 				format(colonType);
2578 			}
2579 			else if (colonExpression)
2580 			{
2581 				put(" : ");
2582 				format(colonExpression);
2583 			}
2584 			if (assignType)
2585 			{
2586 				put(" = ");
2587 				format(assignType);
2588 			}
2589 			else if (assignExpression)
2590 			{
2591 				put(" = ");
2592 				format(assignExpression);
2593 			}
2594 		}
2595 	}
2596 
2597 
2598 	override void format(const Constraint constraint)
2599 	{
2600 		debug(verbose) writeln("Constraint");
2601 
2602 		if (constraint.expression)
2603 		{
2604 			put(" ");
2605 			putTag("<a href=\"http://dpldocs.info/template-constraints\" class=\"lang-feature\">");
2606 			put("if");
2607 			putTag("</a>");
2608 			put(" (");
2609 			putTag("<div class=\"template-constraint-expression\">");
2610 			format(constraint.expression);
2611 			putTag("</div>");
2612 			put(")");
2613 		}
2614 	}
2615 
2616 
2617 
2618 	override void format(const PrimaryExpression primaryExpression)
2619 	{
2620 		debug(verbose) writeln("PrimaryExpression");
2621 
2622 		/**
2623 		  Token dot;
2624 		  Token primary;
2625 		  IdentifierOrTemplateInstance identifierOrTemplateInstance;
2626 		  Token basicType;
2627 		  TypeofExpression typeofExpression;
2628 		  TypeidExpression typeidExpression;
2629 		  ArrayLiteral arrayLiteral;
2630 		  AssocArrayLiteral assocArrayLiteral;
2631 		  Expression expression;
2632 		  IsExpression isExpression;
2633 		  LambdaExpression lambdaExpression;
2634 		  FunctionLiteralExpression functionLiteralExpression;
2635 		  TraitsExpression traitsExpression;
2636 		  MixinExpression mixinExpression;
2637 		  ImportExpression importExpression;
2638 		  Vector vector;
2639 		 **/
2640 
2641 		with(primaryExpression)
2642 		{
2643 			if (dot != tok!"") put(".");
2644 			if (basicType != tok!"") format(basicType);
2645 			if (primary != tok!"")
2646 			{
2647 				if (basicType != tok!"") put("."); // i.e. : uint.max
2648 				format(primary);
2649 			}
2650 
2651 			if (expression)
2652 			{
2653 				putTag("<span class=\"parenthetical-expression\">(<span class=\"parenthetical-expression-contents\">");
2654 				format(expression);
2655 				putTag("</span>)</span>");
2656 			}
2657 			else if (identifierOrTemplateInstance)
2658 			{
2659 				format(identifierOrTemplateInstance);
2660 			}
2661 			else if (typeofExpression) format(typeofExpression);
2662 			else if (typeidExpression) format(typeidExpression);
2663 			else if (arrayLiteral) format(arrayLiteral);
2664 			else if (assocArrayLiteral) format(assocArrayLiteral);
2665 			else if (isExpression) format(isExpression);
2666 			else if (lambdaExpression) { putTag("<span class=\"lambda-expression\">"); format(lambdaExpression); putTag("</span>"); }
2667 			else if (functionLiteralExpression) format(functionLiteralExpression);
2668 			else if (traitsExpression) format(traitsExpression);
2669 			else if (mixinExpression) format(mixinExpression);
2670 			else if (importExpression) format(importExpression);
2671 			else if (vector) format(vector);
2672 		}
2673 	}
2674 
2675 	override void format(const IsExpression isExpression) {
2676 		//Formatter!Sink.format(this);
2677 
2678 		with(isExpression) {
2679 			putTag(`<a href="http://dpldocs.info/is-expression" class="lang-feature">is</a>(`);
2680 			if (type) format(type);
2681 			if (identifier != tok!"") {
2682 				space();
2683 				format(identifier);
2684 			}
2685 
2686 			if (equalsOrColon) {
2687 				space();
2688 				put(tokenRep(equalsOrColon));
2689 				space();
2690 			}
2691 
2692 			if (typeSpecialization) format(typeSpecialization);
2693 			if (templateParameterList) {
2694 				put(", ");
2695 				format(templateParameterList);
2696 			}
2697 			put(")");
2698 		}
2699 	}
2700 
2701 	override void format(const TypeofExpression typeofExpr) {
2702 		debug(verbose) writeln("TypeofExpression");
2703 
2704 		/**
2705 		Expression expression;
2706 		Token return_;
2707 		**/
2708 
2709 		putTag("<a href=\"http://dpldocs.info/typeof-expression\" class=\"lang-feature\">typeof</a>(");
2710 		typeofExpr.expression ? format(typeofExpr.expression) : format(typeofExpr.return_);
2711 		put(")");
2712 	}
2713 
2714 
2715 	override void format(const Parameter parameter)
2716 	{
2717 		debug(verbose) writeln("Parameter");
2718 
2719 		/**
2720 		  IdType[] parameterAttributes;
2721 		  Type type;
2722 		  Token name;
2723 		  bool vararg;
2724 		  AssignExpression default_;
2725 		  TypeSuffix[] cstyle;
2726 		 **/
2727 
2728 		putTag("<div class=\"runtime-parameter-item parameter-item\">");
2729 
2730 
2731 	bool hadAtAttribute;
2732 
2733 		putTag("<span class=\"parameter-type-holder\">");
2734 		putTag("<span class=\"parameter-type\">");
2735 		foreach (count, attribute; parameter.parameterAttributes)
2736 		{
2737 			if (count || hadAtAttribute) space();
2738 			putTag("<span class=\"storage-class\">");
2739 			put(tokenRep(attribute));
2740 			putTag("</span>");
2741 		}
2742 
2743 		if (parameter.parameterAttributes.length > 0)
2744 			space();
2745 
2746 		if (parameter.type !is null)
2747 			format(parameter.type);
2748 
2749 		putTag(`</span>`);
2750 		putTag(`</span>`);
2751 
2752 		if (parameter.name.type != tok!"")
2753 		{
2754 			space();
2755 			putTag(`<span class="parameter-name name" data-ident="`~parameter.name.text~`">`);
2756 			putTag("<a href=\"#param-" ~ parameter.name.text ~ "\">");
2757 			put(parameter.name.text);
2758 			putTag("</a>");
2759 			putTag("</span>");
2760 		}
2761 
2762 		foreach(suffix; parameter.cstyle)
2763 			format(suffix);
2764 
2765 		if (parameter.default_)
2766 		{
2767 			putTag(`<span class="parameter-default-value">`);
2768 			putTag("&nbsp;=&nbsp;");
2769 			format(parameter.default_);
2770 			putTag(`</span>`);
2771 		}
2772 
2773 		if (parameter.vararg) {
2774 			putTag("<a href=\"http://dpldocs.info/typed-variadic-function-arguments\" class=\"lang-feature\">");
2775 			put("...");
2776 			putTag("</a>");
2777 		}
2778 
2779 		putTag("</div>");
2780 	}
2781 
2782 	override void format(const Type type)
2783 	{
2784 		debug(verbose) writeln("Type(");
2785 
2786 		/**
2787 		  IdType[] typeConstructors;
2788 		  TypeSuffix[] typeSuffixes;
2789 		  Type2 type2;
2790 		 **/
2791 
2792 		foreach (count, constructor; type.typeConstructors)
2793 		{
2794 			if (count) space();
2795 			put(tokenRep(constructor));
2796 		}
2797 
2798 		if (type.typeConstructors.length) space();
2799 
2800 		//put(`<span class="name" data-ident="`~tokenRep(token)~`">`);
2801 		format(type.type2);
2802 		//put("</span>");
2803 
2804 		foreach (suffix; type.typeSuffixes)
2805 			format(suffix);
2806 
2807 		debug(verbose) writeln(")");
2808 	}
2809 
2810 	override void format(const Type2 type2)
2811 	{
2812 		debug(verbose) writeln("Type2");
2813 
2814 		/**
2815 		  IdType builtinType;
2816 		  Symbol symbol;
2817 		  TypeofExpression typeofExpression;
2818 		  IdentifierOrTemplateChain identifierOrTemplateChain;
2819 		  IdType typeConstructor;
2820 		  Type type;
2821 		 **/
2822 
2823 		if (type2.symbol !is null)
2824 		{
2825 			suppressMagic = true;
2826 			scope(exit) suppressMagic = false;
2827 
2828 			Decl link;
2829 			if(context)
2830 				link = context.lookupName(type2.symbol);
2831 			if(link !is null) {
2832 				putTag("<a href=\""~link.link~"\" title=\""~link.fullyQualifiedName~"\" class=\"xref\">");
2833 				format(type2.symbol);
2834 				putTag("</a>");
2835 			} else if(toText(type2.symbol) == "string") {
2836 				putTag("<span class=\"builtin-type\">");
2837 				put("string");
2838 				putTag("</span>");
2839 			} else {
2840 				format(type2.symbol);
2841 			}
2842 		}
2843 		else if (type2.typeofExpression !is null)
2844 		{
2845 			format(type2.typeofExpression);
2846 			if (type2.identifierOrTemplateChain)
2847 			{
2848 				put(".");
2849 				format(type2.identifierOrTemplateChain);
2850 			}
2851 			return;
2852 		}
2853 		else if (type2.typeConstructor != tok!"")
2854 		{
2855 			putTag("<span class=\"type-constructor\">");
2856 			put(tokenRep(type2.typeConstructor));
2857 			putTag("</span>");
2858 			put("(");
2859 			format(type2.type);
2860 			put(")");
2861 		}
2862 		else
2863 		{
2864 			putTag("<span class=\"builtin-type\">");
2865 			put(tokenRep(type2.builtinType));
2866 			putTag("</span>");
2867 		}
2868 	}
2869 
2870 
2871 	override void format(const StorageClass storageClass)
2872 	{
2873 		debug(verbose) writeln("StorageClass");
2874 
2875 		/**
2876 		  AtAttribute atAttribute;
2877 		  Deprecated deprecated_;
2878 		  LinkageAttribute linkageAttribute;
2879 		  Token token;
2880 		 **/
2881 
2882 		with(storageClass)
2883 		{
2884 			if (atAttribute) format(atAttribute);
2885 			else if (deprecated_) format(deprecated_);
2886 			else if (linkageAttribute) format(linkageAttribute);
2887 			else {
2888 				putTag("<span class=\"storage-class\">");
2889 				format(token);
2890 				putTag("</span>");
2891 			}
2892 		}
2893 	}
2894 
2895 
2896 	bool noTag;
2897 	override void format(const Token token)
2898 	{
2899 		debug(verbose) writeln("Token ", tokenRep(token));
2900 		if(!noTag && token == tok!"identifier") {
2901 			putTag(`<span class="name" data-ident="`~tokenRep(token)~`">`);
2902 		}
2903 		auto rep = tokenRep(token);
2904 		if(rep == "delegate" || rep == "function") {
2905 			putTag(`<span class="lang-feature">`);
2906 			put(rep);
2907 			putTag(`</span>`);
2908 		} else {
2909 			put(rep);
2910 		}
2911 		if(!noTag && token == tok!"identifier") {
2912 			putTag("</span>");
2913 		}
2914 	}
2915 
2916 	bool suppressMagic = false;
2917 
2918 	override void format(const IdentifierOrTemplateInstance identifierOrTemplateInstance)
2919 	{
2920 		debug(verbose) writeln("IdentifierOrTemplateInstance");
2921 
2922 		bool suppressMagicGoingIn = suppressMagic;
2923 
2924 		//if(!suppressMagicGoingIn)
2925 			//putTag("<span class=\"some-ident\">");
2926 		with(identifierOrTemplateInstance)
2927 		{
2928 			format(identifier);
2929 			if (templateInstance)
2930 				format(templateInstance);
2931 		}
2932 		//if(!suppressMagicGoingIn)
2933 			//putTag("</span>");
2934 	}
2935 
2936 	version(none)
2937 	override void format(const Symbol symbol)
2938 	{
2939 		debug(verbose) writeln("Symbol");
2940 
2941 		put("GOOD/");
2942 		if (symbol.dot)
2943 			put(".");
2944 		format(symbol.identifierOrTemplateChain);
2945 		put("/GOOD");
2946 	}
2947 
2948 	override void format(const Parameters parameters)
2949 	{
2950 		debug(verbose) writeln("Parameters");
2951 
2952 		/**
2953 		  Parameter[] parameters;
2954 		  bool hasVarargs;
2955 		 **/
2956 
2957 		putTag("<div class=\"parameters-list\">");
2958 		putTag("<span class=\"paren\">(</span>");
2959 		foreach (count, param; parameters.parameters)
2960 		{
2961 			if (count) putTag("<span class=\"comma\">,</span>");
2962 			format(param);
2963 		}
2964 		if (parameters.hasVarargs)
2965 		{
2966 			if (parameters.parameters.length)
2967 				putTag("<span class=\"comma\">,</span>");
2968 			putTag("<div class=\"runtime-parameter-item parameter-item\">");
2969 			putTag("<a href=\"http://dpldocs.info/variadic-function-arguments\" class=\"lang-feature\">");
2970 			putTag("...");
2971 			putTag("</a>");
2972 			putTag("</div>");
2973 		}
2974 		putTag("<span class=\"paren\">)</span>");
2975 		putTag("</div>");
2976 	}
2977 
2978     	override void format(const IdentifierChain identifierChain)
2979 	{
2980 		//put("IDENT");
2981 		foreach(count, ident; identifierChain.identifiers)
2982 		{
2983 		    if (count) put(".");
2984 		    put(ident.text);
2985 		}
2986 		//put("/IDENT");
2987 	}
2988 
2989 	override void format(const AndAndExpression andAndExpression)
2990 	{
2991 		with(andAndExpression)
2992 		{
2993 			putTag("<div class=\"andand-left\">");
2994 			format(left);
2995 			if (right)
2996 			{
2997 				putTag(" &amp;&amp; </div><div class=\"andand-right\">");
2998 				format(right);
2999 			}
3000 			putTag("</div>");
3001 		}
3002 	}
3003 
3004 	override void format(const OrOrExpression orOrExpression)
3005 	{
3006 		with(orOrExpression)
3007 		{
3008 			putTag("<div class=\"oror-left\">");
3009 			format(left);
3010 			if (right)
3011 			{
3012 				putTag(" || </div><div class=\"oror-right\">");
3013 				format(right);
3014 			}
3015 			putTag("</div>");
3016 		}
3017 	}
3018 
3019 	alias format = Formatter!Sink.format;
3020 }
3021 
3022 
3023 /*
3024 	The small table looks like this:
3025 
3026 
3027 	Caption
3028 	Header | Row
3029 	Data | Row
3030 	Data | Row
3031 */
3032 Element translateSmallTable(string text, Decl decl) {
3033 	auto holder = Element.make("table");
3034 	holder.addClass("small-table");
3035 
3036 	void skipWhitespace() {
3037 		while(text.length && (text[0] == ' ' || text[0] == '\t'))
3038 			text = text[1 .. $];
3039 	}
3040 
3041 	string[] extractLine() {
3042 		string[] cells;
3043 
3044 		skipWhitespace();
3045 		size_t i;
3046 
3047 		while(i < text.length) {
3048 			static string lastText;
3049 			if(text[i .. $] is lastText)
3050 				assert(0, lastText);
3051 
3052 			lastText = text[i .. $];
3053 			switch(text[i]) {
3054 				case '|':
3055 					cells ~= text[0 .. i].strip;
3056 					text = text[i + 1 .. $];
3057 					i = 0;
3058 				break;
3059 				case '`':
3060 					i++;
3061 					while(i < text.length && text[i] != '`')
3062 						i++;
3063 					if(i < text.length)
3064 						i++; // skip closing `
3065 				break;
3066 				case '[':
3067 				case '(':
3068 				case '{':
3069 					i++; // FIXME? skip these?
3070 				break;
3071 				case '$':
3072 					auto info = macroInformation(text[i .. $]);
3073 					i += info.terminatingOffset + 1;
3074 				break;
3075 				case '\n':
3076 					cells ~= text[0 .. i].strip;
3077 					text = text[i + 1 .. $];
3078 					return cells;
3079 				break;
3080 				default:
3081 					i++;
3082 			}
3083 		}
3084 
3085 		return cells;
3086 	}
3087 
3088 	bool isDecorative(string[] row) {
3089 		foreach(s; row)
3090 		foreach(char c; s)
3091 			if(c != '+' && c != '-' && c != '|' && c != '=')
3092 				return false;
3093 		return true;
3094 	}
3095 
3096 	Element tbody;
3097 	bool first = true;
3098 	bool twodTable;
3099 	bool firstLine = true;
3100 	string[] headerRow;
3101 	while(text.length) {
3102 		auto row = extractLine();
3103 
3104 		if(row.length == 0)
3105 			continue; // should never happen...
3106 
3107 		if(row.length == 1 && row[0].length == 0)
3108 			continue; // blank line
3109 
3110 		if(isDecorative(row))
3111 			continue; // ascii art is allowed, but ignored
3112 
3113 		if(firstLine && row.length == 1 && row[0].length) {
3114 			firstLine = false;
3115 			holder.addChild("caption", row[0]);
3116 			continue;
3117 		}
3118 
3119 		firstLine = false;
3120 
3121 		// empty first and last rows are just surrounding pipes and can be ignored
3122 		if(row[0].length == 0)
3123 			row = row[1 .. $];
3124 		if(row[$-1].length == 0)
3125 			row = row[0 .. $-1];
3126 
3127 		if(first) {
3128 			auto thead = holder.addChild("thead");
3129 			auto tr = thead.addChild("tr");
3130 
3131 			headerRow = row;
3132 
3133 			if(row[0].length == 0)
3134 				twodTable = true;
3135 
3136 			foreach(cell; row)
3137 				tr.addChild("th", formatDocumentationComment2(cell, decl, null));
3138 			first = false;
3139 		} else {
3140 			if(tbody is null)
3141 				tbody = holder.addChild("tbody");
3142 			auto tr = tbody.addChild("tr");
3143 
3144 			while(row.length < headerRow.length)
3145 				row ~= "";
3146 
3147 			foreach(i, cell; row)
3148 				tr.addChild((twodTable && i == 0) ? "th" : "td", formatDocumentationComment2(cell, decl, null));
3149 
3150 		}
3151 	}
3152 
3153 	// FIXME: format the cells
3154 
3155 	if(twodTable)
3156 		holder.addClass("two-axes");
3157 
3158 
3159 	return holder;
3160 }
3161 
3162 Element translateListTable(string text, Decl decl) {
3163 	auto holder = Element.make("table");
3164 	holder.addClass("user-table");
3165 
3166 	DocCommentTerminationCondition termination;
3167 	termination.terminationStrings = ["*", "-", "+"];
3168 	termination.mustBeAtStartOfLine = true;
3169 
3170 	string nextTag;
3171 
3172 	Element row;
3173 
3174 	do {
3175 		auto fmt = formatDocumentationComment2(text, decl, null, &termination);
3176 		if(fmt.toString.strip.length) {
3177 			if(row is null)
3178 				holder.addChild("caption", fmt);
3179 			else
3180 				row.addChild(nextTag, fmt);
3181 		}
3182 		text = termination.remaining;
3183 		if(termination.terminatedOn == "*")
3184 			row = holder.addChild("tr");
3185 		else if(termination.terminatedOn == "+")
3186 			nextTag = "th";
3187 		else if(termination.terminatedOn == "-")
3188 			nextTag = "td";
3189 		else {}
3190 	} while(text.length);
3191 
3192 	if(holder.querySelector("th:first-child + td"))
3193 		holder.addClass("two-axes");
3194 
3195 	return holder;
3196 }
3197 
3198 Element translateList(string text, Decl decl, string tagName) {
3199 	auto holder = Element.make(tagName);
3200 	holder.addClass("user-list");
3201 
3202 	DocCommentTerminationCondition termination;
3203 	termination.terminationStrings = ["*"];
3204 	termination.mustBeAtStartOfLine = true;
3205 
3206 	auto opening = formatDocumentationComment2(text, decl, null, &termination);
3207 	text = termination.remaining;
3208 
3209 	translateMagicCommands(opening, holder);
3210 
3211 	while(text.length) {
3212 		auto fmt = formatDocumentationComment2(text, decl, null, &termination);
3213 		holder.addChild("li", fmt);
3214 		text = termination.remaining;
3215 	}
3216 
3217 	return holder;
3218 }
3219 
3220 Html syntaxHighlightCss(string code) {
3221 	string highlighted;
3222 
3223 
3224 	int pullPiece(string code, bool includeDot) {
3225 		int i;
3226 		while(i < code.length && (
3227 			(code[i] >= '0' && code[i] <= '9')
3228 			||
3229 			(code[i] >= 'a' && code[i] <= 'z')
3230 			||
3231 			(code[i] >= 'A' && code[i] <= 'Z')
3232 			||
3233 			(includeDot && code[i] == '.') || code[i] == '_' || code[i] == '-' || code[i] == ':' // for css i want to match like :first-letter
3234 		))
3235 		{
3236 			i++;
3237 		}
3238 		return i;
3239 	}
3240 
3241 
3242 
3243 	while(code.length) {
3244 		switch(code[0]) {
3245 			case '\'':
3246 			case '\"':
3247 				// string literal
3248 				char start = code[0];
3249 				size_t i = 1;
3250 				while(i < code.length && code[i] != start && code[i] != '\n') {
3251 					if(code[i] == '\\')
3252 						i++; // skip escaped char too
3253 					i++;
3254 				}
3255 
3256 				i++; // skip closer
3257 				highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3258 				code = code[i .. $];
3259 			break;
3260 			case '/':
3261 				// check for comment
3262 				if(code.length > 1 && code[1] == '*') {
3263 					// a star comment
3264 					size_t i;
3265 					while((i+1) < code.length && !(code[i] == '*' && code[i+1] == '/'))
3266 						i++;
3267 
3268 					if(i < code.length)
3269 						i+=2; // skip the */
3270 
3271 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3272 					code = code[i .. $];
3273 				} else {
3274 					highlighted ~= '/';
3275 					code = code[1 .. $];
3276 				}
3277 			break;
3278 			case '<':
3279 				highlighted ~= "&lt;";
3280 				code = code[1 .. $];
3281 			break;
3282 			case '>':
3283 				highlighted ~= "&gt;";
3284 				code = code[1 .. $];
3285 			break;
3286 			case '&':
3287 				highlighted ~= "&amp;";
3288 				code = code[1 .. $];
3289 			break;
3290 			case '@':
3291 				// @media, @import, @font-face, etc
3292 				auto i = pullPiece(code[1 .. $], false);
3293 				if(i)
3294 					i++;
3295 				else
3296 					goto plain;
3297 				highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3298 				code = code[i .. $];
3299 			break;
3300 			case '\n', ';':
3301 				// check for a new rule (imprecise since we don't actually parse, but meh
3302 				// will ignore indent, then see if there is a word-like-this followed by :
3303 				int i = 1;
3304 				while(i < code.length && (code[i] == ' ' || code[i] == '\t'))
3305 					i++;
3306 				int start = i;
3307 				while(i < code.length && (code[i] == '-' || (code[i] >= 'a' && code[i] <= 'z')))
3308 					i++;
3309 				if(start == i || i == code.length || code[i] != ':')
3310 					goto plain;
3311 
3312 				highlighted ~= code[0 .. start];
3313 				highlighted ~= "<span class=\"highlighted-named-constant\">" ~ htmlEntitiesEncode(code[start .. i]) ~ "</span>";
3314 				code = code[i .. $];
3315 			break;
3316 			case '[':
3317 				// attribute match rule
3318 				auto i = pullPiece(code[1 .. $], false);
3319 				if(i)
3320 					i++;
3321 				else
3322 					goto plain;
3323 				highlighted ~= "<span class=\"highlighted-attribute-name\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3324 				code = code[i .. $];
3325 			break;
3326 			case '#', '.': // id or class selector
3327 				auto i = pullPiece(code[1 .. $], false);
3328 				if(i)
3329 					i++;
3330 				else
3331 					goto plain;
3332 				highlighted ~= "<span class=\"highlighted-identifier\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3333 				code = code[i .. $];
3334 			break;
3335 			case ':':
3336 				// pseudoclass
3337 				auto i = pullPiece(code, false);
3338 				highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3339 				code = code[i .. $];
3340 			break;
3341 			case '(':
3342 				// just skipping stuff in parens...
3343 				int parenCount = 0;
3344 				int pos = 0;
3345 				bool inQuote;
3346 				bool escaped;
3347 				while(pos < code.length) {
3348 					if(code[pos] == '\\')
3349 						escaped = true;
3350 					if(!escaped && code[pos] == '"')
3351 						inQuote = !inQuote;
3352 					if(!inQuote && code[pos] == '(')
3353 						parenCount++;
3354 					if(!inQuote && code[pos] == ')')
3355 						parenCount--;
3356 					pos++;
3357 					if(parenCount == 0)
3358 						break;
3359 				}
3360 
3361 				highlighted ~= "<span class=\"highlighted-identifier\">" ~ htmlEntitiesEncode(code[0 .. pos]) ~ "</span>";
3362 				code = code[pos .. $];
3363 			break;
3364 			case '0': .. case '9':
3365 				// number. including text at the end cuz it is probably a unit
3366 				auto i = pullPiece(code, true);
3367 				highlighted ~= "<span class=\"highlighted-number\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3368 				code = code[i .. $];
3369 			break;
3370 			default:
3371 			plain:
3372 				highlighted ~= code[0];
3373 				code = code[1 .. $];
3374 		}
3375 	}
3376 
3377 	return Html(highlighted);
3378 }
3379 
3380 string highlightHtmlTag(string tag) {
3381 	string highlighted = "&lt;";
3382 
3383 	bool isWhite(char c) {
3384 		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
3385 	}
3386 
3387 	tag = tag[1 .. $];
3388 
3389 	if(tag[0] == '/') {
3390 		highlighted ~= '/';
3391 		tag = tag[1 .. $];
3392 	}
3393 
3394 	int i = 0;
3395 	while(i < tag.length && !isWhite(tag[i]) && tag[i] != '>') {
3396 		i++;
3397 	}
3398 
3399 	highlighted ~= "<span class=\"highlighted-tag-name\">" ~ htmlEntitiesEncode(tag[0 .. i]) ~ "</span>";
3400 	tag = tag[i .. $];
3401 
3402 	while(tag.length && tag[0] != '>') {
3403 		while(isWhite(tag[0])) {
3404 			highlighted ~= tag[0];
3405 			tag = tag[1 .. $];
3406 		}
3407 
3408 		i = 0;
3409 		while(i < tag.length && tag[i] != '=' && tag[i] != '>')
3410 			i++;
3411 
3412 		highlighted ~= "<span class=\"highlighted-attribute-name\">" ~ htmlEntitiesEncode(tag[0 .. i]) ~ "</span>";
3413 		tag = tag[i .. $];
3414 
3415 		if(tag.length && tag[0] == '=') {
3416 			highlighted ~= '=';
3417 			tag = tag[1 .. $];
3418 			if(tag.length && (tag[0] == '\'' || tag[0] == '"')) {
3419 				char end = tag[0];
3420 				i = 1;
3421 				while(i < tag.length && tag[i] != end)
3422 					i++;
3423 				if(i < tag.length)
3424 					i++;
3425 
3426 				highlighted ~= "<span class=\"highlighted-attribute-value\">" ~ htmlEntitiesEncode(tag[0 .. i]) ~ "</span>";
3427 				tag = tag[i .. $];
3428 			} else if(tag.length) {
3429 				i = 0;
3430 				while(i < tag.length && !isWhite(tag[i]))
3431 					i++;
3432 
3433 				highlighted ~= "<span class=\"highlighted-attribute-value\">" ~ htmlEntitiesHighlight(tag[0 .. i]) ~ "</span>";
3434 				tag = tag[i .. $];
3435 			}
3436 		}
3437 	}
3438 
3439 
3440 	highlighted ~= htmlEntitiesEncode(tag);
3441 
3442 	return highlighted;
3443 }
3444 
3445 string htmlEntitiesHighlight(string code) {
3446 	string highlighted;
3447 
3448 	bool isWhite(char c) {
3449 		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
3450 	}
3451 
3452 	while(code.length)
3453 	switch(code[0]) {
3454 		case '&':
3455 			// possibly an entity
3456 			int i = 0;
3457 			while(i < code.length && code[i] != ';' && !isWhite(code[i]))
3458 				i++;
3459 			if(i == code.length || isWhite(code[i])) {
3460 				highlighted ~= "&amp;";
3461 				code = code[1 .. $];
3462 			} else {
3463 				i++;
3464 				highlighted ~=
3465 					"<abbr class=\"highlighted-entity\" title=\"Entity: "~code[0 .. i].replace("\"", "&quot;")~"\">" ~
3466 						htmlEntitiesEncode(code[0 .. i]) ~
3467 					"</abbr>";
3468 				code = code[i .. $];
3469 			}
3470 		break;
3471 		case '<':
3472 			highlighted ~= "&lt;";
3473 			code = code[1 .. $];
3474 		break;
3475 		case '"':
3476 			highlighted ~= "&quot;";
3477 			code = code[1 .. $];
3478 		break;
3479 		case '>':
3480 			highlighted ~= "&gt;";
3481 			code = code[1 .. $];
3482 		break;
3483 		default:
3484 			highlighted ~= code[0];
3485 			code = code[1 .. $];
3486 	}
3487 
3488 	return highlighted;
3489 }
3490 
3491 Html syntaxHighlightRuby(string code) {
3492 	// FIXME this is quite minimal and crappy
3493 	// ruby just needs to be parsed to be lexed!
3494 	// and i didn't implement its myriad of strings
3495 
3496 	static immutable string[] rubyKeywords = [
3497 		"BEGIN", "ensure", "self", "when",
3498 		"END", "not", "super", "while",
3499 		"alias", "defined", "for", "or", "then", "yield",
3500 		"and", "do", "if", "redo",
3501 		"begin", "else", "in", "rescue", "undef",
3502 		"break", "elsif", "retry", "unless",
3503 		"case", "end", "next", "return", "until",
3504 	];
3505 
3506 	static immutable string[] rubyPreprocessors = [
3507 		"require", "include"
3508 	];
3509 
3510 	static immutable string[] rubyTypes = [
3511 		"class", "def", "module"
3512 	];
3513 
3514 	static immutable string[] rubyConstants = [
3515 		"nil", "false", "true",
3516 	];
3517 
3518 	bool lastWasType;
3519 	char lastChar;
3520 	int indentLevel;
3521 	int[] declarationIndentLevels;
3522 
3523 	string highlighted;
3524 
3525 	while(code.length) {
3526 		bool thisIterWasType = false;
3527 		auto ch = code[0];
3528 		switch(ch) {
3529 			case '#':
3530 				size_t i;
3531 				while(i < code.length && code[i] != '\n')
3532 					i++;
3533 
3534 				highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3535 				code = code[i .. $];
3536 			break;
3537 			case '<':
3538 				highlighted ~= "&lt;";
3539 				code = code[1 .. $];
3540 			break;
3541 			case '>':
3542 				highlighted ~= "&gt;";
3543 				code = code[1 .. $];
3544 			break;
3545 			case '&':
3546 				highlighted ~= "&amp;";
3547 				code = code[1 .. $];
3548 			break;
3549 			case '0': .. case '9':
3550 				// number literal
3551 				size_t i;
3552 				while(i < code.length && (
3553 					(code[i] >= '0' && code[i] <= '9')
3554 					||
3555 					(code[i] >= 'a' && code[i] <= 'z')
3556 					||
3557 					(code[i] >= 'A' && code[i] <= 'Z')
3558 					||
3559 					code[i] == '.' || code[i] == '_'
3560 				))
3561 				{
3562 					i++;
3563 				}
3564 
3565 				highlighted ~= "<span class=\"highlighted-number\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3566 				code = code[i .. $];
3567 			break;
3568 			case '"', '\'':
3569 				// string
3570 				// check for triple-quoted string
3571 				if(ch == '"') {
3572 
3573 					// double quote string
3574 					auto idx = 1;
3575 					bool escaped = false;
3576 					int nestedCount = 0;
3577 					bool justSawHash = false;
3578 					while(!escaped && nestedCount == 0 && code[idx] != '"') {
3579 						if(justSawHash && code[idx] == '{')
3580 							nestedCount++;
3581 						if(nestedCount && code[idx] == '}')
3582 							nestedCount--;
3583 
3584 						if(!escaped && code[idx] == '#')
3585 							justSawHash = true;
3586 
3587 						if(code[idx] == '\\')
3588 							escaped = true;
3589 						else {
3590 							escaped = false;
3591 						}
3592 
3593 						idx++;
3594 					}
3595 					idx++;
3596 
3597 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. idx]) ~ "</span>";
3598 					code = code[idx .. $];
3599 				} else {
3600 					int end = 1;
3601 					bool escaped;
3602 					while(end < code.length && !escaped && code[end] != ch) {
3603 						escaped = (code[end] == '\\');
3604 						end++;
3605 					}
3606 
3607 					if(end < code.length)
3608 						end++;
3609 
3610 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. end]) ~ "</span>";
3611 					code = code[end .. $];
3612 				}
3613 			break;
3614 			case '\n':
3615 				indentLevel = 0;
3616 				int i = 1;
3617 				while(i < code.length && (code[i] == ' ' || code[i] == '\t')) {
3618 					i++;
3619 					indentLevel++;
3620 				}
3621 				highlighted ~= ch;
3622 				code = code[1 .. $];
3623 			break;
3624 			case ' ', '\t':
3625 				thisIterWasType = lastWasType; // don't change the last counter on just whitespace
3626 				highlighted ~= ch;
3627 				code = code[1 .. $];
3628 			break;
3629 			case ':':
3630 				// check for ruby symbol
3631 				int nameEnd = 1;
3632 				while(nameEnd < code.length && (
3633 					(code[nameEnd] >= 'a' && code[nameEnd] <= 'z') ||
3634 					(code[nameEnd] >= 'A' && code[nameEnd] <= 'Z') ||
3635 					(code[nameEnd] >= '0' && code[nameEnd] <= '0') ||
3636 					code[nameEnd] == '_'
3637 				))
3638 				{
3639 					nameEnd++;
3640 				}
3641 
3642 				if(nameEnd > 1) {
3643 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. nameEnd]) ~ "</span>";
3644 					code = code[nameEnd .. $];
3645 				} else {
3646 					highlighted ~= ch;
3647 					code = code[1 .. $];
3648 				}
3649 			break;
3650 			default:
3651 				// check for names
3652 				int nameEnd = 0;
3653 				while(nameEnd < code.length && (
3654 					(code[nameEnd] >= 'a' && code[nameEnd] <= 'z') ||
3655 					(code[nameEnd] >= 'A' && code[nameEnd] <= 'Z') ||
3656 					(code[nameEnd] >= '0' && code[nameEnd] <= '0') ||
3657 					code[nameEnd] == '_'
3658 				))
3659 				{
3660 					nameEnd++;
3661 				}
3662 
3663 				if(nameEnd) {
3664 					auto name = code[0 .. nameEnd];
3665 					code = code[nameEnd .. $];
3666 
3667 					if(rubyTypes.canFind(name)) {
3668 						highlighted ~= "<span class=\"highlighted-type\">" ~ name ~ "</span>";
3669 						thisIterWasType = true;
3670 						declarationIndentLevels ~= indentLevel;
3671 					} else if(rubyConstants.canFind(name)) {
3672 						highlighted ~= "<span class=\"highlighted-number\">" ~ name ~ "</span>";
3673 					} else if(rubyPreprocessors.canFind(name)) {
3674 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ name ~ "</span>";
3675 					} else if(rubyKeywords.canFind(name)) {
3676 						if(name == "end") {
3677 							if(declarationIndentLevels.length && indentLevel == declarationIndentLevels[$-1]) {
3678 								// cheating on matching ends with declarations: if indentation matches...
3679 								highlighted ~= "<span class=\"highlighted-type\">" ~ name ~ "</span>";
3680 								declarationIndentLevels = declarationIndentLevels[0 .. $-1];
3681 							} else {
3682 								highlighted ~= "<span class=\"highlighted-keyword\">" ~ name ~ "</span>";
3683 							}
3684 						} else {
3685 							highlighted ~= "<span class=\"highlighted-keyword\">" ~ name ~ "</span>";
3686 						}
3687 					} else {
3688 						if(lastWasType) {
3689 							highlighted ~= "<span class=\"highlighted-identifier\">" ~ name ~ "</span>";
3690 						} else {
3691 							if(name.length && name[0] >= 'A' && name[0] <= 'Z')
3692 								highlighted ~= "<span class=\"highlighted-named-constant\">" ~ name ~ "</span>";
3693 							else
3694 								highlighted ~= name;
3695 						}
3696 					}
3697 				} else {
3698 					highlighted ~= ch;
3699 					code = code[1 .. $];
3700 				}
3701 			break;
3702 		}
3703 		lastChar = ch;
3704 		lastWasType = thisIterWasType;
3705 	}
3706 
3707 	return Html(highlighted);
3708 }
3709 
3710 Html syntaxHighlightPython(string code) {
3711 	/*
3712 		I just want to support:
3713 			numbers
3714 				anything starting with 0, then going alphanumeric
3715 			keywords
3716 				from and import are "preprocessor"
3717 				None, True, and False are number literal colored
3718 			name
3719 				an identifier after the "def" keyword, possibly including : if in there. my vim does it light blue
3720 			comments
3721 				only # .. \n is allowed.
3722 			strings
3723 				single quote must end on the same line
3724 				triple quote span multiple line.
3725 
3726 
3727 			Extracting python definitions:
3728 				class foo:
3729 					# method baz, arg: bar, doc string attached
3730 					def baz(bar):
3731 						""" this is the doc string """
3732 
3733 				will just have to follow indentation (BARF)
3734 
3735 			Note: python has default arguments.
3736 
3737 
3738 			Python implicitly does line continuation if parens, brackets, or braces open
3739 			you can also explicitly extend with \ at the end
3740 			in these cases, the indentation doesn't count.
3741 
3742 			If there's only comments on a line, the indentation is also exempt.
3743 	*/
3744 	static immutable string[] pythonKeywords = [
3745 		"and", "del", "not", "while",
3746 		"as", "elif", "global", "or", "with",
3747 		"assert", "else", "if", "pass", "yield",
3748 		"break", "except", "print",
3749 		"exec", "in", "raise",
3750 		"continue", "finally", "is", "return",
3751 		"for", "lambda", "try",
3752 	];
3753 
3754 	static immutable string[] pythonPreprocessors = [
3755 		"from", "import"
3756 	];
3757 
3758 	static immutable string[] pythonTypes = [
3759 		"class", "def"
3760 	];
3761 
3762 	static immutable string[] pythonConstants = [
3763 		"True", "False", "None"
3764 	];
3765 
3766 	string[] indentStack;
3767 	int openParenCount = 0;
3768 	int openBraceCount = 0;
3769 	int openBracketCount = 0;
3770 	char lastChar;
3771 	bool lastWasType;
3772 
3773 	string highlighted;
3774 
3775 	// ensure there is one and exactly one new line at the end
3776 	// the highlighter uses the final \n as a chance to clean up the
3777 	// indent divs
3778 	code = code.stripRight;
3779 	code ~= "\n";
3780 
3781 	int lineCountSpace = 5; // if python is > 1000 lines it can slightly throw off the indent highlight... but meh.
3782 
3783 	while(code.length) {
3784 		bool thisIterWasType = false;
3785 		auto ch = code[0];
3786 		switch(ch) {
3787 			case '#':
3788 				size_t i;
3789 				while(i < code.length && code[i] != '\n')
3790 					i++;
3791 
3792 				highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3793 				code = code[i .. $];
3794 			break;
3795 			case '(':
3796 				openParenCount++;
3797 				highlighted ~= ch;
3798 				code = code[1 .. $];
3799 			break;
3800 			case '[':
3801 				openBracketCount++;
3802 				highlighted ~= ch;
3803 				code = code[1 .. $];
3804 			break;
3805 			case '{':
3806 				openBraceCount++;
3807 				highlighted ~= ch;
3808 				code = code[1 .. $];
3809 			break;
3810 			case ')':
3811 				openParenCount--;
3812 				highlighted ~= ch;
3813 				code = code[1 .. $];
3814 			break;
3815 			case ']':
3816 				openBracketCount--;
3817 				highlighted ~= ch;
3818 				code = code[1 .. $];
3819 			break;
3820 			case '}':
3821 				openBraceCount--;
3822 				highlighted ~= ch;
3823 				code = code[1 .. $];
3824 			break;
3825 			case '<':
3826 				highlighted ~= "&lt;";
3827 				code = code[1 .. $];
3828 			break;
3829 			case '>':
3830 				highlighted ~= "&gt;";
3831 				code = code[1 .. $];
3832 			break;
3833 			case '&':
3834 				highlighted ~= "&amp;";
3835 				code = code[1 .. $];
3836 			break;
3837 			case '0': .. case '9':
3838 				// number literal
3839 				size_t i;
3840 				while(i < code.length && (
3841 					(code[i] >= '0' && code[i] <= '9')
3842 					||
3843 					(code[i] >= 'a' && code[i] <= 'z')
3844 					||
3845 					(code[i] >= 'A' && code[i] <= 'Z')
3846 					||
3847 					code[i] == '.' || code[i] == '_'
3848 				))
3849 				{
3850 					i++;
3851 				}
3852 
3853 				highlighted ~= "<span class=\"highlighted-number\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3854 				code = code[i .. $];
3855 			break;
3856 			case '"', '\'':
3857 				// string
3858 				// check for triple-quoted string
3859 				if(ch == '"' && code.length > 2 && code[1] == '"' && code[2] == '"') {
3860 					int end = 3;
3861 					bool escaped;
3862 					while(end + 3 < code.length && !escaped && code[end .. end+ 3] != `"""`) {
3863 						escaped = (code[end] == '\\');
3864 						end++;
3865 					}
3866 
3867 					if(end < code.length)
3868 						end+= 3;
3869 
3870 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. end]) ~ "</span>";
3871 					code = code[end .. $];
3872 				} else {
3873 					// otherwise these are limited to one line, though since we
3874 					// assume the program is well-formed, I'm not going to bother;
3875 					// no need to check for Python errors here, just highlight
3876 					int end = 1;
3877 					bool escaped;
3878 					while(end < code.length && !escaped && code[end] != ch) {
3879 						escaped = (code[end] == '\\');
3880 						end++;
3881 					}
3882 
3883 					if(end < code.length)
3884 						end++;
3885 
3886 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. end]) ~ "</span>";
3887 					code = code[end .. $];
3888 				}
3889 			break;
3890 			case '\n':
3891 				string thisIndent = null;
3892 				int thisIndentLength = lineCountSpace;
3893 				// figure out indentation and stuff...
3894 				if(lastChar == '\\' || openParenCount || openBracketCount || openBraceCount) {
3895 					// line continuation, no special processing
3896 				} else {
3897 					if(code.length == 1) {
3898 						// last line in the file, clean up the indentation
3899 						int remove;
3900 						foreach_reverse(i; indentStack) {
3901 							// this isn't actually following the python rule which
3902 							// is more like .startsWith but meh
3903 							if(i.length <= thisIndent.length)
3904 								break;
3905 							highlighted ~= "</div>";
3906 							remove++;
3907 						}
3908 						indentStack = indentStack[0 .. $ - remove];
3909 						// NOT appending the final \n cuz that leads dead space in rendered doc
3910 
3911 						code = code[1 .. $];
3912 						break;
3913 					} else
3914 					foreach(idx, cha; code[1 .. $]) {
3915 						if(cha == '\n')
3916 							break;
3917 						if(cha == '#') // comments exempt from indent processing too
3918 							break;
3919 						if(cha == ' ')
3920 							thisIndentLength++;
3921 						if(cha == '\t')
3922 							thisIndentLength += 8 - (thisIndentLength % 8);
3923 						if(cha != ' ' && cha != '\t') {
3924 							thisIndent = code[1 .. idx + 1];
3925 							break;
3926 						}
3927 					}
3928 				}
3929 
3930 				bool changedDiv = false;
3931 
3932 				if(thisIndent !is null) { // !is null rather than .length is important here. null means skip, "" may need processing
3933 					// close open indents if necessary
3934 					int remove;
3935 					foreach_reverse(i; indentStack) {
3936 						// this isn't actually following the python rule which
3937 						// is more like .startsWith but meh
3938 						if(i.length <= thisIndent.length)
3939 							break;
3940 						highlighted ~= "</div>";
3941 						changedDiv = true;
3942 						remove++;
3943 					}
3944 					indentStack = indentStack[0 .. $ - remove];
3945 				}
3946 
3947 				if(thisIndent.length) { // but we only ever open a new one if there was non-zero indent
3948 					// open a new one if appropriate
3949 					if(indentStack.length == 0 || thisIndent.length > indentStack[$-1].length) {
3950 						changedDiv = true;
3951 						highlighted ~= "<div class=\"highlighted-python-indent\" style=\"background-position: calc("~to!string(thisIndentLength)~"ch - 2px);\">";
3952 						indentStack ~= thisIndent;
3953 					}
3954 				}
3955 
3956 				if(changedDiv)
3957 					highlighted ~= "<span style=\"white-space: normal;\">";
3958 
3959 				highlighted ~= ch;
3960 
3961 				if(changedDiv)
3962 					highlighted ~= "</span>";
3963 
3964 				code = code[1 .. $];
3965 			break;
3966 			case ' ', '\t':
3967 				thisIterWasType = lastWasType; // don't change the last counter on just whitespace
3968 				highlighted ~= ch;
3969 				code = code[1 .. $];
3970 			break;
3971 			default:
3972 				// check for names
3973 				int nameEnd = 0;
3974 				while(nameEnd < code.length && (
3975 					(code[nameEnd] >= 'a' && code[nameEnd] <= 'z') ||
3976 					(code[nameEnd] >= 'A' && code[nameEnd] <= 'Z') ||
3977 					(code[nameEnd] >= '0' && code[nameEnd] <= '0') ||
3978 					code[nameEnd] == '_'
3979 				))
3980 				{
3981 					nameEnd++;
3982 				}
3983 
3984 				if(nameEnd) {
3985 					auto name = code[0 .. nameEnd];
3986 					code = code[nameEnd .. $];
3987 
3988 					if(pythonTypes.canFind(name)) {
3989 						highlighted ~= "<span class=\"highlighted-type\">" ~ name ~ "</span>";
3990 						thisIterWasType = true;
3991 					} else if(pythonConstants.canFind(name)) {
3992 						highlighted ~= "<span class=\"highlighted-number\">" ~ name ~ "</span>";
3993 					} else if(pythonPreprocessors.canFind(name)) {
3994 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ name ~ "</span>";
3995 					} else if(pythonKeywords.canFind(name)) {
3996 						highlighted ~= "<span class=\"highlighted-keyword\">" ~ name ~ "</span>";
3997 					} else {
3998 						if(lastWasType) {
3999 							highlighted ~= "<span class=\"highlighted-identifier\">" ~ name ~ "</span>";
4000 						} else {
4001 							highlighted ~= name;
4002 						}
4003 					}
4004 				} else {
4005 					highlighted ~= ch;
4006 					code = code[1 .. $];
4007 				}
4008 			break;
4009 		}
4010 		lastChar = ch;
4011 		lastWasType = thisIterWasType;
4012 	}
4013 
4014 	return Html(highlighted);
4015 }
4016 
4017 Html syntaxHighlightHtml(string code, string language) {
4018 	string highlighted;
4019 
4020 	bool isWhite(char c) {
4021 		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
4022 	}
4023 
4024 	while(code.length) {
4025 		switch(code[0]) {
4026 			case '<':
4027 				if(code.length > 1 && code[1] == '!') {
4028 					// comment or processing instruction
4029 
4030 					int i = 0;
4031 					while(i < code.length && code[i] != '>') {
4032 						i++;
4033 					}
4034 					if(i < code.length)
4035 						i++;
4036 
4037 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
4038 					code = code[i .. $];
4039 					continue;
4040 				} else if(code.length > 1 && (code[1] == '/' || (code[1] >= 'a' && code[1] <= 'z') || (code[1] >= 'A' && code[1] <= 'Z'))) {
4041 					// possibly a tag
4042 					int i = 1;
4043 					while(i < code.length && code[i] != '>' && code[i] != '<')
4044 						i++;
4045 					if(i == code.length || code[i] == '<')
4046 						goto plain_lt;
4047 
4048 					auto tag = code[0 .. i + 1];
4049 					code = code[i + 1 .. $];
4050 
4051 					highlighted ~= "<span class=\"highlighted-tag\">" ~ highlightHtmlTag(tag) ~ "</span>";
4052 
4053 					if(language == "html" && tag.startsWith("<script")) {
4054 						auto end = code.indexOf("</script>");
4055 						if(end == -1)
4056 							continue;
4057 
4058 						auto sCode = code[0 .. end];
4059 						highlighted ~= syntaxHighlightCFamily(sCode, "javascript").source;
4060 						code = code[end .. $];
4061 					} else if(language == "html" && tag.startsWith("<style")) {
4062 						auto end = code.indexOf("</style>");
4063 						if(end == -1)
4064 							continue;
4065 						auto sCode = code[0 .. end];
4066 						highlighted ~= syntaxHighlightCss(sCode).source;
4067 						code = code[end .. $];
4068 					}
4069 
4070 					continue;
4071 				}
4072 
4073 				plain_lt:
4074 				highlighted ~= "&lt;";
4075 				code = code[1 .. $];
4076 			break;
4077 			case '"':
4078 				highlighted ~= "&quot;";
4079 				code = code[1 .. $];
4080 			break;
4081 			case '>':
4082 				highlighted ~= "&gt;";
4083 				code = code[1 .. $];
4084 			break;
4085 			case '&':
4086 				// possibly an entity
4087 
4088 				int i = 0;
4089 				while(i < code.length && code[i] != ';' && !isWhite(code[i]))
4090 					i++;
4091 				if(i == code.length || isWhite(code[i])) {
4092 					highlighted ~= "&amp;";
4093 					code = code[1 .. $];
4094 				} else {
4095 					i++;
4096 					highlighted ~= htmlEntitiesHighlight(code[0 .. i]);
4097 					code = code[i .. $];
4098 				}
4099 			break;
4100 
4101 			default:
4102 				highlighted ~= code[0];
4103 				code = code[1 .. $];
4104 		}
4105 	}
4106 
4107 	return Html(highlighted);
4108 }
4109 
4110 Html syntaxHighlightSdlang(string jsCode) {
4111 	string highlighted;
4112 
4113 	bool isIdentifierChar(char c) {
4114 		return
4115 			(c >= 'a' && c <= 'z') ||
4116 			(c >= 'A' && c <= 'Z') ||
4117 			(c >= '0' && c <= '9') ||
4118 			(c == '$' || c == '_');
4119 	}
4120 
4121 	bool startOfLine = true;
4122 
4123 	while(jsCode.length) {
4124 		switch(jsCode[0]) {
4125 			case '\'':
4126 			case '\"':
4127 				// string literal
4128 				char start = jsCode[0];
4129 				size_t i = 1;
4130 				while(i < jsCode.length && jsCode[i] != start && jsCode[i] != '\n') {
4131 					if(jsCode[i] == '\\')
4132 						i++; // skip escaped char too
4133 					i++;
4134 				}
4135 
4136 				i++; // skip closer
4137 				highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4138 				jsCode = jsCode[i .. $];
4139 				startOfLine = false;
4140 			break;
4141 			case '-':
4142 				if(jsCode.length > 1 && jsCode[1] == '-') {
4143 					int i = 0;
4144 					while(i < jsCode.length && jsCode[i] != '\n')
4145 						i++;
4146 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4147 					jsCode = jsCode[i .. $];
4148 				} else {
4149 					highlighted ~= '-';
4150 					jsCode = jsCode[1 .. $];
4151 				}
4152 			break;
4153 			case '#':
4154 				int i = 0;
4155 				while(i < jsCode.length && jsCode[i] != '\n')
4156 					i++;
4157 
4158 				highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4159 				jsCode = jsCode[i .. $];
4160 			break;
4161 			case '/':
4162 				if(jsCode.length > 1 && (jsCode[1] == '*' || jsCode[1] == '/')) {
4163 					// it is a comment
4164 					if(jsCode[1] == '/') {
4165 						size_t i;
4166 						while(i < jsCode.length && jsCode[i] != '\n')
4167 							i++;
4168 
4169 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4170 						jsCode = jsCode[i .. $];
4171 					} else {
4172 						// a star comment
4173 						size_t i;
4174 						while((i+1) < jsCode.length && !(jsCode[i] == '*' && jsCode[i+1] == '/'))
4175 							i++;
4176 
4177 						if(i < jsCode.length)
4178 							i+=2; // skip the */
4179 
4180 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4181 						jsCode = jsCode[i .. $];
4182 					}
4183 				} else {
4184 					highlighted ~= '/';
4185 					jsCode = jsCode[1 .. $];
4186 				}
4187 			break;
4188 			case '0': .. case '9':
4189 				// number literal
4190 				size_t i;
4191 				while(i < jsCode.length && (
4192 					(jsCode[i] >= '0' && jsCode[i] <= '9')
4193 					||
4194 					// really fuzzy but this is meant to highlight hex and exponents too
4195 					jsCode[i] == '.' || jsCode[i] == 'e' || jsCode[i] == 'x' ||
4196 					(jsCode[i] >= 'a' && jsCode[i] <= 'f') ||
4197 					(jsCode[i] >= 'A' && jsCode[i] <= 'F')
4198 				))
4199 					i++;
4200 
4201 				highlighted ~= "<span class=\"highlighted-number\">" ~ jsCode[0 .. i] ~ "</span>";
4202 				jsCode = jsCode[i .. $];
4203 				startOfLine = false;
4204 			break;
4205 			case '\t', ' ', '\r':
4206 				highlighted ~= jsCode[0];
4207 				jsCode = jsCode[1 .. $];
4208 			break;
4209 			case '\n':
4210 				startOfLine = true;
4211 				highlighted ~= jsCode[0];
4212 				jsCode = jsCode[1 .. $];
4213 			break;
4214 			case '\\':
4215 				if(jsCode.length > 1 && jsCode[1] == '\n') {
4216 					highlighted ~= jsCode[0 .. 1];
4217 					jsCode = jsCode[2 .. $];
4218 				}
4219 			break;
4220 			case ';':
4221 				startOfLine = true;
4222 			break;
4223 			// escape html chars
4224 			case '<':
4225 				highlighted ~= "&lt;";
4226 				jsCode = jsCode[1 .. $];
4227 				startOfLine = false;
4228 			break;
4229 			case '>':
4230 				highlighted ~= "&gt;";
4231 				jsCode = jsCode[1 .. $];
4232 				startOfLine = false;
4233 			break;
4234 			case '&':
4235 				highlighted ~= "&amp;";
4236 				jsCode = jsCode[1 .. $];
4237 				startOfLine = false;
4238 			break;
4239 			default:
4240 				if(isIdentifierChar(jsCode[0])) {
4241 					size_t i;
4242 					while(i < jsCode.length && isIdentifierChar(jsCode[i]))
4243 						i++;
4244 					auto ident = jsCode[0 .. i];
4245 					jsCode = jsCode[i .. $];
4246 
4247 					if(startOfLine)
4248 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4249 					else if(ident == "true" || ident == "false")
4250 						highlighted ~= "<span class=\"highlighted-number\">" ~ ident ~ "</span>";
4251 					else
4252 						highlighted ~= "<span class=\"highlighted-attribute-name\">" ~ ident ~ "</span>";
4253 				} else {
4254 					highlighted ~= jsCode[0];
4255 					jsCode = jsCode[1 .. $];
4256 				}
4257 
4258 				startOfLine = false;
4259 		}
4260 	}
4261 
4262 	return Html(highlighted);
4263 
4264 }
4265 
4266 Html syntaxHighlightCFamily(string jsCode, string language) {
4267 	string highlighted;
4268 
4269 	bool isJsIdentifierChar(char c) {
4270 		return
4271 			(c >= 'a' && c <= 'z') ||
4272 			(c >= 'A' && c <= 'Z') ||
4273 			(c >= '0' && c <= '9') ||
4274 			(c == '$' || c == '_');
4275 	}
4276 
4277 	while(jsCode.length) {
4278 
4279 
4280 		switch(jsCode[0]) {
4281 			case '\'':
4282 			case '\"':
4283 				// string literal
4284 				char start = jsCode[0];
4285 				size_t i = 1;
4286 				while(i < jsCode.length && jsCode[i] != start && jsCode[i] != '\n') {
4287 					if(jsCode[i] == '\\')
4288 						i++; // skip escaped char too
4289 					i++;
4290 				}
4291 
4292 				i++; // skip closer
4293 				highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4294 				jsCode = jsCode[i .. $];
4295 			break;
4296 			case '#':
4297 				// preprocessor directive / PHP # comment
4298 				size_t i;
4299 				while(i < jsCode.length && jsCode[i] != '\n')
4300 					i++;
4301 
4302 				if(language == "php")
4303 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4304 				else
4305 					highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4306 				jsCode = jsCode[i .. $];
4307 
4308 			break;
4309 			case '/':
4310 				// check for comment
4311 				// javascript also has that stupid regex literal, but screw it
4312 				if(jsCode.length > 1 && (jsCode[1] == '*' || jsCode[1] == '/')) {
4313 					// it is a comment
4314 					if(jsCode[1] == '/') {
4315 						size_t i;
4316 						while(i < jsCode.length && jsCode[i] != '\n')
4317 							i++;
4318 
4319 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4320 						jsCode = jsCode[i .. $];
4321 					} else {
4322 						// a star comment
4323 						size_t i;
4324 						while((i+1) < jsCode.length && !(jsCode[i] == '*' && jsCode[i+1] == '/'))
4325 							i++;
4326 
4327 						if(i < jsCode.length)
4328 							i+=2; // skip the */
4329 
4330 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4331 						jsCode = jsCode[i .. $];
4332 					}
4333 				} else {
4334 					highlighted ~= '/';
4335 					jsCode = jsCode[1 .. $];
4336 				}
4337 			break;
4338 			case '0': .. case '9':
4339 				// number literal
4340 				// FIXME: negative exponents are not done here
4341 				// nor are negative numbers, but I think that needs parsing anyway
4342 				// it also doesn't do all things right but the assumption is we're highlighting valid code anyway
4343 				size_t i;
4344 				while(i < jsCode.length && (
4345 					(jsCode[i] >= '0' && jsCode[i] <= '9')
4346 					||
4347 					// really fuzzy but this is meant to highlight hex and exponents too
4348 					jsCode[i] == '.' || jsCode[i] == 'e' || jsCode[i] == 'x' ||
4349 					(jsCode[i] >= 'a' && jsCode[i] <= 'f') ||
4350 					(jsCode[i] >= 'A' && jsCode[i] <= 'F')
4351 				))
4352 					i++;
4353 
4354 				highlighted ~= "<span class=\"highlighted-number\">" ~ jsCode[0 .. i] ~ "</span>";
4355 				jsCode = jsCode[i .. $];
4356 			break;
4357 			case '\t', ' ', '\n', '\r':
4358 				highlighted ~= jsCode[0];
4359 				jsCode = jsCode[1 .. $];
4360 			break;
4361 			case '?':
4362 				if(language == "php") {
4363 					if(jsCode.length >= 2 && jsCode[0 .. 2] == "?>") {
4364 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">?&gt;</span>";
4365 						jsCode = jsCode[2 .. $];
4366 						break;
4367 					}
4368 				}
4369 				highlighted ~= jsCode[0];
4370 				jsCode = jsCode[1 .. $];
4371 			break;
4372 			// escape html chars
4373 			case '<':
4374 				if(language == "php") {
4375 					if(jsCode.length > 5 && jsCode[0 .. 5] == "<?php") {
4376 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">&lt;?php</span>";
4377 						jsCode = jsCode[5 .. $];
4378 						break;
4379 					}
4380 				}
4381 				highlighted ~= "&lt;";
4382 				jsCode = jsCode[1 .. $];
4383 			break;
4384 			case '>':
4385 				highlighted ~= "&gt;";
4386 				jsCode = jsCode[1 .. $];
4387 			break;
4388 			case '&':
4389 				highlighted ~= "&amp;";
4390 				jsCode = jsCode[1 .. $];
4391 			break;
4392 			case '@':
4393 				// FIXME highlight UDAs
4394 				//goto case;
4395 			//break;
4396 			default:
4397 				if(isJsIdentifierChar(jsCode[0])) {
4398 					size_t i;
4399 					while(i < jsCode.length && isJsIdentifierChar(jsCode[i]))
4400 						i++;
4401 					auto ident = jsCode[0 .. i];
4402 					jsCode = jsCode[i .. $];
4403 
4404 					if(["function", "for", "in", "while", "new", "if", "else", "switch", "return", "break", "do", "delete", "this", "super", "continue", "goto"].canFind(ident))
4405 						highlighted ~= "<span class=\"highlighted-keyword\">" ~ ident ~ "</span>";
4406 					else if(["enum", "final", "virtual", "explicit", "var", "void", "const", "let", "int", "short", "unsigned", "char", "class", "struct", "float", "double", "typedef", "public", "protected", "private", "static"].canFind(ident))
4407 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4408 					else if(language == "java" && ["extends", "implements"].canFind(ident))
4409 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4410 					// FIXME: do i want to give using this same color?
4411 					else if((language == "c#" || language == "c++") && ["using", "namespace"].canFind(ident))
4412 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4413 					else if(language == "java" && ["native", "package", "import"].canFind(ident))
4414 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ ident ~ "</span>";
4415 					else if(ident[0] == '$')
4416 						highlighted ~= "<span class=\"highlighted-identifier\">" ~ ident ~ "</span>";
4417 					else
4418 						highlighted ~= ident; // "<span>" ~ ident ~ "</span>";
4419 
4420 				} else {
4421 					highlighted ~= jsCode[0];
4422 					jsCode = jsCode[1 .. $];
4423 				}
4424 		}
4425 	}
4426 
4427 	return Html(highlighted);
4428 }