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