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 	if(text.length && text[0] == '/')
1080 		return true; // is schema-relative url link
1081 
1082 	bool seenHash;
1083 
1084 	import std.uni;
1085 	foreach(idx, dchar ch; text) {
1086 		if(!isIdentifierChar(idx, ch) && ch != '.') {
1087 			if(!seenHash && ch == '#') {
1088 				seenHash = true;
1089 				continue;
1090 			} else if(seenHash && ch == '-') {
1091 				continue;
1092 			}
1093 			return false;
1094 		}
1095 	}
1096 
1097 	// passed the ident test...
1098 	if(text.length)
1099 		return true;
1100 
1101 	return false;
1102 
1103 }
1104 
1105 Element getReferenceLink(string text, Decl decl, string realText = null) {
1106 	import std.uri;
1107 	auto l = uriLength(text);
1108 	string hash;
1109 
1110 	string className;
1111 
1112 	if(l != -1) { // a url
1113 		text = text;
1114 		if(realText is null)
1115 			realText = text;
1116 	} else if(text.length && text[0] == '#') {
1117 		// an anchor. add the link so it works when this is copy/pasted on other pages too (such as in search results)
1118 		hash = text;
1119 		text = decl.link;
1120 
1121 		if(realText is null) {
1122 			realText = hash[1 .. $].replace("-", " ");
1123 		}
1124 	} else {
1125 		if(realText is null)
1126 			realText = text;
1127 
1128 		auto hashIdx = text.indexOf("#");
1129 		if(hashIdx != -1) {
1130 			hash = text[hashIdx .. $];
1131 			text = text[0 .. hashIdx];
1132 		}
1133 
1134 		auto found = decl.lookupName(text);
1135 
1136 		className = "xref";
1137 
1138 		if(found is null) {
1139 			auto lastPieceIdx = text.lastIndexOf(".");
1140 			if(lastPieceIdx != -1) {
1141 				found = decl.lookupName(text[0 .. lastPieceIdx]);
1142 				if(found && hash is null)
1143 					hash = "#" ~ text[lastPieceIdx + 1 .. $];
1144 			}
1145 		}
1146 
1147 		if(found !is null)
1148 			text = found.link;
1149 		else if(auto c = text in allClasses) {
1150 			// classes may be thrown and as such can be referenced externally without import
1151 			// doing this as kinda a hack.
1152 			text = (*c).link;
1153 		} else {
1154 			text = text.handleCaseSensitivity ~ ".html";
1155 		}
1156 	}
1157 
1158 	auto element = Element.make("a");
1159 	element.className = className;
1160 	element.href = getDirectoryForPackage(text) ~ text ~ hash;
1161 	element.innerText = realText;
1162 	return element;
1163 }
1164 
1165 struct DocCommentTerminationCondition {
1166 	string[] terminationStrings;
1167 	bool mustBeAtStartOfLine;
1168 	string remaining;
1169 	string terminatedOn;
1170 }
1171 
1172 Element formatDocumentationComment2(string comment, Decl decl, string tagName = "div", DocCommentTerminationCondition* termination = null) {
1173 	Element div;
1174 	if(tagName is null)
1175 		div = new DocumentFragment(null);
1176 	else
1177 		div = Element.make(tagName);
1178 
1179 	string currentParagraph;
1180 	currentParagraph.reserve(2048);
1181 	void putch(char c) {
1182 		currentParagraph ~= c;
1183 	}
1184 	void put(in char[] c) {
1185 		currentParagraph ~= c;
1186 	}
1187 
1188 	string currentTag = (tagName is null) ? null : "p";
1189 	string currentClass = null;
1190 
1191 	void commit() {
1192 		auto cp = currentParagraph.strip;
1193 		if(cp.length) {
1194 			if(currentTag is null)
1195 				div.appendHtml(cp);
1196 			else {
1197 				if(currentTag == "p") {
1198 					// check for a markdown style header
1199 					auto test = cp.strip;
1200 					auto lines = test.splitLines();
1201 					if(lines.length == 2) {
1202 						auto hdr = lines[0].strip;
1203 						auto equals = lines[1].strip;
1204 						if(equals.length >= 4) {
1205 							foreach(ch; equals)
1206 								if(ch != '=')
1207 									goto not_special;
1208 						} else {
1209 							goto not_special;
1210 						}
1211 
1212 						if(hdr.length == 0)
1213 							goto not_special;
1214 
1215 						// passed tests, I'll allow it:
1216 						currentTag = "h3";
1217 						currentClass = "user-header";
1218 						cp = hdr;
1219 					} else if(lines.length == 1) {
1220 						int hashCount = 0;
1221 						foreach(idx, ch; test) {
1222 							if(ch == '#')
1223 								hashCount++;
1224 							else if(ch == ' ' && hashCount > 0) {
1225 								// it was special!
1226 								// there must be text after btw or else strip would have cut the space too
1227 								currentTag = "h" ~ to!string(hashCount);
1228 								currentClass = "user-header";
1229 								cp = test[idx + 1 .. $];
1230 
1231 								break;
1232 							} else
1233 								break; // not special
1234 						}
1235 					}
1236 				}
1237 
1238 				not_special:
1239 				div.addChild(currentTag, Html(cp), currentClass);
1240 			}
1241 		}
1242 		currentTag = "p";
1243 		currentParagraph = null;
1244 		currentParagraph.reserve(2048);
1245 		currentClass = null;
1246 	}
1247 
1248 	bool atStartOfLine = true;
1249 	bool earlyTermination;
1250 
1251 	main_loop:
1252 	for(size_t idx = 0; idx < comment.length; idx++) {
1253 		auto ch = comment[idx];
1254 		auto remaining = comment[idx .. $];
1255 
1256 		if(termination !is null) {
1257 			if((termination.mustBeAtStartOfLine && atStartOfLine) || !termination.mustBeAtStartOfLine) {
1258 				foreach(ts; termination.terminationStrings) {
1259 					if(remaining.startsWith(ts)) {
1260 						termination.remaining = remaining[ts.length .. $];
1261 						earlyTermination = true;
1262 						termination.terminatedOn = ts;
1263 						break main_loop;
1264 					}
1265 				}
1266 			}
1267 		}
1268 
1269 		switch(ch) {
1270 			case '\r':
1271 				continue; // don't care even a little about Windows vs Unix line endings
1272 			case ' ', '\t':
1273 				goto useCharWithoutTriggeringStartOfLine;
1274 			case '\n':
1275 				if(atStartOfLine)
1276 					commit();
1277 				atStartOfLine = true;
1278 				goto useCharWithoutTriggeringStartOfLine;
1279 			break;
1280 			// these need to be html encoded
1281 			case '"':
1282 				put("&quot;");
1283 				atStartOfLine = false;
1284 			break;
1285 			case '<':
1286 				// FIXME: support just a wee bit of inline html...
1287 				put("&lt;");
1288 				atStartOfLine = false;
1289 			break;
1290 			case '>':
1291 				put("&gt;");
1292 				atStartOfLine = false;
1293 			break;
1294 			case '&':
1295 				put("&amp;");
1296 				atStartOfLine = false;
1297 			break;
1298 			// special syntax
1299 			case '`':
1300 				// code. ` is inline, ``` is block.
1301 				if(atStartOfLine && remaining.startsWith("```")) {
1302 					idx += 3;
1303 					remaining = remaining[3 .. $];
1304 					bool braced = false;
1305 					if(remaining.length && remaining[0] == '{') {
1306 						braced = true;
1307 						remaining = remaining[1 .. $];
1308 					}
1309 
1310 					auto line = remaining.indexOf("\n");
1311 					string language;
1312 					if(line != -1) {
1313 						language = remaining[0 .. line].strip;
1314 						remaining = remaining[line + 1 .. $];
1315 						idx += line + 1;
1316 					}
1317 
1318 					size_t ending;
1319 					size_t sliceEnding;
1320 					if(braced) {
1321 						int count = 1;
1322 						while(ending < remaining.length) {
1323 							if(remaining[ending] == '{')
1324 								count++;
1325 							if(remaining[ending] == '}')
1326 								count--;
1327 
1328 							if(count == 0)
1329 								break;
1330 							ending++;
1331 						}
1332 
1333 						sliceEnding = ending;
1334 
1335 						while(ending < remaining.length) {
1336 							if(remaining[ending] == '\n') {
1337 								ending++;
1338 								break;
1339 							}
1340 							ending++;
1341 						}
1342 
1343 					} else {
1344 						ending = remaining.indexOf("```\n");
1345 						if(ending != -1) {
1346 							sliceEnding = ending;
1347 							ending += 4; // skip \n
1348 						} else {
1349 							ending = remaining.indexOf("```\r");
1350 
1351 							if(ending != -1) {
1352 								sliceEnding = ending;
1353 								ending += 5; // skip \r\n
1354 							} else {
1355 								ending = remaining.length;
1356 								sliceEnding = ending;
1357 							}
1358 						}
1359 					}
1360 
1361 					if(currentTag == "p")
1362 						commit();
1363 					// FIXME: we can prolly do more with languages...
1364 					string code = outdent(stripRight(remaining[0 .. sliceEnding]));
1365 					Element ele;
1366 					// all these languages are close enough for hack good enough.
1367 					if(language == "javascript" || language == "c" || language == "c++" || language == "java" || language == "php" || language == "c#" || language == "d" || language == "adrscript" || language == "json")
1368 						ele = div.addChild("pre", syntaxHighlightCFamily(code, language));
1369 					else if(language == "css")
1370 						ele = div.addChild("pre", syntaxHighlightCss(code));
1371 					else if(language == "html" || language == "xml")
1372 						ele = div.addChild("pre", syntaxHighlightHtml(code, language));
1373 					else if(language == "python")
1374 						ele = div.addChild("pre", syntaxHighlightPython(code));
1375 					else if(language == "ruby")
1376 						ele = div.addChild("pre", syntaxHighlightRuby(code));
1377 					else if(language == "sdlang")
1378 						ele = div.addChild("pre", syntaxHighlightSdlang(code));
1379 					else
1380 						ele = div.addChild("pre", code);
1381 					ele.addClass("block-code");
1382 					ele.dataset.language = language;
1383 					idx += ending;
1384 				} else {
1385 					// check for inline `code` style
1386 					bool foundIt = false;
1387 					size_t foundItWhere = 0;
1388 					foreach(i, scout; remaining[1 .. $]) {
1389 						if(scout == '\n')
1390 							break;
1391 						if(scout == '`') {
1392 							foundIt = true;
1393 							foundItWhere = i;
1394 							break;
1395 						}
1396 					}
1397 
1398 					if(!foundIt)
1399 						goto ordinary;
1400 
1401 					atStartOfLine = false;
1402 					auto slice = remaining[1 .. foundItWhere + 1];
1403 					idx += 1 + foundItWhere;
1404 
1405 					if(slice.length == 0) {
1406 						// empty code block is `` - doubled backtick. Just
1407 						// treat that as a literal (escaped) backtick.
1408 						putch('`');
1409 					} else {
1410 						auto ele = Element.make("tt").addClass("inline-code");
1411 						ele.innerText = slice;
1412 						put(ele.toString());
1413 					}
1414 				}
1415 			break;
1416 			case '$':
1417 				// ddoc macro. May be magical.
1418 				auto info = macroInformation(remaining);
1419 				if(info.linesSpanned == 0) {
1420 					goto ordinary;
1421 				}
1422 
1423 				atStartOfLine = false;
1424 
1425 				auto name = remaining[2 .. info.macroNameEndingOffset];
1426 
1427 				auto dzeroAdjustment = (info.textBeginningOffset == info.terminatingOffset) ? 0 : 1;
1428 				auto text = remaining[info.textBeginningOffset .. info.terminatingOffset - dzeroAdjustment];
1429 
1430 				if(name in ddocMacroBlocks) {
1431 					commit();
1432 
1433 					auto got = expandDdocMacros(remaining, decl);
1434 					div.appendChild(got);
1435 				} else {
1436 					put(expandDdocMacros(remaining, decl).toString());
1437 				}
1438 
1439 				idx += info.terminatingOffset - 1;
1440 			break;
1441 			case '-':
1442 				// ddoc style code iff --- at start of line.
1443 				if(!atStartOfLine) {
1444 					goto ordinary;
1445 				}
1446 				if(remaining.startsWith("---")) {
1447 					// a code sample has started
1448 					if(currentTag == "p")
1449 						commit();
1450 					div.addChild("pre", extractDdocCodeExample(remaining, idx)).addClass("d_code highlighted");
1451 					atStartOfLine = true;
1452 				} else
1453 					goto ordinary;
1454 			break;
1455 			case '!':
1456 				// possible markdown-style image
1457 				if(remaining.length > 1 && remaining[1] == '[') {
1458 					auto inside = remaining[1 .. $];
1459 					auto txt = extractBalanceOnSingleLine(inside);
1460 					if(txt is null)
1461 						goto ordinary;
1462 					if(txt.length < inside.length && inside[txt.length] == '(') {
1463 						// possible markdown image
1464 						auto parens = extractBalanceOnSingleLine(inside[txt.length .. $]);
1465 						if(parens.length) {
1466 							auto a = Element.make("img");
1467 							a.alt = txt[1 .. $-1];
1468 							a.src = parens[1 .. $-1];
1469 							put(a.toString());
1470 
1471 							idx ++; // skip the !
1472 							idx += parens.length;
1473 							idx += txt.length;
1474 							idx --; // so the ++ in the for loop brings us back to i
1475 							break;
1476 						}
1477 					}
1478 				}
1479 				goto ordinary;
1480 			break;
1481 			case '[':
1482 				// wiki-style reference iff [text] or [text|other text]
1483 				// or possibly markdown style link: [text](url)
1484 				// text MUST be either a valid D identifier chain, optionally with a section hash,
1485 				// or a fully-encoded http url.
1486 				// text may never include ']' or '|' or whitespace or ',' and must always start with '_' or alphabetic (like a D identifier or http url)
1487 				// other text may include anything except the string ']'
1488 				// it balances [] inside it.
1489 
1490 				auto txt = extractBalanceOnSingleLine(remaining);
1491 				if(txt is null)
1492 					goto ordinary;
1493 				if(txt.length < remaining.length && remaining[txt.length] == '(') {
1494 					// possible markdown link
1495 					auto parens = extractBalanceOnSingleLine(remaining[txt.length .. $]);
1496 					if(parens.length) {
1497 						auto a = Element.make("a", txt[1 .. $-1], parens[1 .. $-1]);
1498 						put(a.toString());
1499 
1500 						idx += parens.length;
1501 						idx += txt.length;
1502 						idx --; // so the ++ in the for loop brings us back to i
1503 						break;
1504 					}
1505 				}
1506 
1507 				auto fun = txt[1 .. $-1];
1508 				string rt;
1509 				auto idx2 = fun.indexOf("|");
1510 				if(idx2 != -1) {
1511 					rt = fun[idx2 + 1 .. $].strip;
1512 					fun = fun[0 .. idx2].strip;
1513 				}
1514 
1515 				if(fun.all!((ch) => ch >= '0' && ch <= '9')) {
1516 					// footnote reference
1517 					auto lri = getLinkReference(fun, decl);
1518 					if(lri.type == LinkReferenceInfo.Type.none)
1519 						goto ordinary;
1520 					put(lri.toHtml(fun, rt));
1521 				} else if(!fun.isIdentifierOrUrl()) {
1522 					goto ordinary;
1523 				} else {
1524 					auto lri = getLinkReference(fun, decl);
1525 					if(lri.type == LinkReferenceInfo.Type.none) {
1526 						if(auto l = getSymbolGroupReference(fun, decl, rt)) {
1527 							put(l.toString());
1528 						} else {
1529 							put(getReferenceLink(fun, decl, rt).toString);
1530 						}
1531 					} else
1532 						put(lri.toHtml(fun, rt));
1533 				}
1534 
1535 				idx += txt.length;
1536 				idx --; // so the ++ in the for loop brings us back to i
1537 			break;
1538 			case 'h':
1539 				// automatically linkify web URLs pasted in
1540 				if(tagName is null)
1541 					goto ordinary;
1542 				import std.uri;
1543 				auto l = uriLength(remaining);
1544 				if(l == -1)
1545 					goto ordinary;
1546 				auto url = remaining[0 .. l];
1547 				put("<a href=\"" ~ url ~ "\">" ~ url ~ "</a>");
1548 				idx += url.length;
1549 				idx --; // so the ++ puts us at the end
1550 			break;
1551 			case '_':
1552 				// is it that stupid ddocism where a PSYMBOL is prefixed with _?
1553 				// still, really, I want to KILL this completely, I hate it a lot
1554 				if(idx && !isIdentifierChar(1, comment[idx-1]) && checkStupidDdocIsm(remaining[1..$], decl))
1555 					break;
1556 				else
1557 					goto ordinary;
1558 			break;
1559 			/*
1560 				/+ I'm just not happy with this yet because it thinks
1561 
1562 				Paragraph
1563 					* list item
1564 				After stuff
1565 
1566 				the After stuff is part of the list item. Blargh.
1567 
1568 				+/
1569 			case '*':
1570 				// potential list item
1571 				if(atStartOfLine) {
1572 					commit();
1573 					currentTag = "li";
1574 					atStartOfLine = false;
1575 					continue;
1576 				} else {
1577 					goto ordinary;
1578 				}
1579 			break;
1580 			*/
1581 			default:
1582 			ordinary:
1583 				atStartOfLine = false;
1584 				useCharWithoutTriggeringStartOfLine:
1585 				putch(ch);
1586 		}
1587 	}
1588 
1589 	commit();
1590 
1591 	if(termination !is null && !earlyTermination)
1592 		termination.remaining = null;
1593 
1594 	/*
1595 	if(div.firstChild !is null && div.firstChild.nodeType == NodeType.Text
1596 	   && div.firstChild.nextSibling !is null && div.firstChild.nextSibling.tagName == "p")
1597 	     div.firstChild.wrapIn(Element.make("p"));
1598 	*/
1599 
1600 	return div;
1601 }
1602 
1603 bool checkStupidDdocIsm(string remaining, Decl decl) {
1604 	size_t i;
1605 	foreach(idx, dchar ch; remaining) {
1606 		if(!isIdentifierChar(idx, ch)) {
1607 			i = idx;
1608 			break;
1609 		}
1610 	}
1611 
1612 	string ident = remaining[0 ..  i];
1613 	if(ident.length == 0)
1614 		return false;
1615 
1616 	// check this name
1617 	if(ident == decl.name) {
1618 		import std.stdio; writeln("stupid ddocism(1): ", ident);
1619 		return true;
1620 	}
1621 
1622 	if(decl.isModule) {
1623 		auto name = decl.name;
1624 		auto idx = name.lastIndexOf(".");
1625 		if(idx != -1) {
1626 			name = name[idx + 1 .. $];
1627 		}
1628 
1629 		if(ident == name) {
1630 			import std.stdio; writeln("stupid ddocism(2): ", ident);
1631 			return true;
1632 		}
1633 	}
1634 
1635 	// check the whole scope too, I think dmd does... maybe
1636 	if(decl.parentModule && decl.parentModule.lookupName(ident) !is null) {
1637 		import std.stdio; writeln("stupid ddocism(3): ", ident);
1638 		return true;
1639 	}
1640 
1641 	if(decl.hasParam(ident)) {
1642 		import std.stdio; writeln("stupid ddocism(4): ", ident);
1643 		return true;
1644 	}
1645 
1646 	return false;
1647 }
1648 
1649 string extractBalanceOnSingleLine(string txt) {
1650 	if(txt.length == 0) return null;
1651 	char starter = txt[0];
1652 	char terminator;
1653 	switch(starter) {
1654 		case '[':
1655 			terminator = ']';
1656 		break;
1657 		case '(':
1658 			terminator = ')';
1659 		break;
1660 		case '{':
1661 			terminator = '}';
1662 		break;
1663 		default:
1664 			return null;
1665 	}
1666 
1667 	int count;
1668 	foreach(idx, ch; txt) {
1669 		if(ch == '\n')
1670 			return null;
1671 		if(ch == starter)
1672 			count++;
1673 		else if(ch == terminator)
1674 			count--;
1675 
1676 		if(count == 0)
1677 			return txt[0 .. idx + 1];
1678 	}
1679 
1680 	return null; // unbalanced prolly
1681 }
1682 
1683 Html extractDdocCodeExample(string comment, ref size_t idx) {
1684 	assert(comment.startsWith("---"));
1685 	auto i = comment.indexOf("\n");
1686 	if(i == -1)
1687 		return Html(htmlEncode(comment).idup);
1688 	comment = comment[i + 1 .. $];
1689 	idx += i + 1;
1690 
1691 	LexerConfig config;
1692 	StringCache stringCache = StringCache(128);
1693     	scope(exit) stringCache.freeItAll();
1694 
1695 	config.stringBehavior = StringBehavior.source;
1696 	config.whitespaceBehavior = WhitespaceBehavior.include;
1697 
1698 	ubyte[] lies = cast(ubyte[]) comment;
1699 
1700 	// I'm tokenizing to handle stuff like --- inside strings and
1701 	// comments inside the code. If we hit an "operator" -- followed by -
1702 	// or -- followed by --, right after some whitespace including a line, 
1703 	// we're done.
1704 	bool justSawNewLine = true;
1705 	bool justSawDashDash = false;
1706 	size_t terminatingIndex;
1707 	lexing:
1708 	foreach(token; byToken(lies, config, &stringCache)) {
1709 		if(justSawDashDash) {
1710 			if(token == tok!"--" || token == tok!"-") {
1711 				break lexing;
1712 			}
1713 		}
1714 
1715 		if(justSawNewLine) {
1716 			if(token == tok!"--") {
1717 				justSawDashDash = true;
1718 				justSawNewLine = false;
1719 				terminatingIndex = token.index;
1720 				continue lexing;
1721 			}
1722 		}
1723 
1724 		if(token == tok!"whitespace") {
1725 			foreach(txt; token.text)
1726 				if(txt == '\n') {
1727 					justSawNewLine = true;
1728 					continue lexing;
1729 				}
1730 		}
1731 
1732 		justSawNewLine = false;
1733 	}
1734 
1735 	i = comment.indexOf('\n', terminatingIndex);
1736 	if(i == -1)
1737 		i = comment.length;
1738 
1739 	idx += i;
1740 
1741 	comment = comment[0 .. terminatingIndex];
1742 
1743 	return Html(outdent(highlight(comment.stripRight)));
1744 }
1745 
1746 immutable string[string] ddocMacros;
1747 immutable int[string] ddocMacroInfo;
1748 immutable int[string] ddocMacroBlocks;
1749 
1750 shared static this() {
1751 	ddocMacroBlocks = [
1752 		"ADRDOX_SAMPLE" : 1,
1753 		"LIST": 1,
1754 		"NUMBERED_LIST": 1,
1755 		"SMALL_TABLE" : 1,
1756 		"TABLE_ROWS" : 1,
1757 		"EMBED_UNITTEST": 1,
1758 		"UNDOCUMENTED": 1,
1759 
1760 		"TIP":1,
1761 		"NOTE":1,
1762 		"WARNING":1,
1763 		"PITFALL":1,
1764 		"SIDEBAR":1,
1765 		"CONSOLE":1,
1766 		"H1":1,
1767 		"H2":1,
1768 		"H3":1,
1769 		"H4":1,
1770 		"H5":1,
1771 		"H5":1,
1772 		"HR":1,
1773 		"BOOKTABLE":1,
1774 		"T2":1,
1775 
1776 		"TR" : 1,
1777 		"TH" : 1,
1778 		"TD" : 1,
1779 		"TABLE" : 1,
1780 		"TDNW" : 1,
1781 		"LEADINGROWN" : 1,
1782 
1783 		"UL" : 1,
1784 		"OL" : 1,
1785 		"LI" : 1,
1786 
1787 		"P" : 1,
1788 		"PRE" : 1,
1789 		"BLOCKQUOTE" : 1,
1790 		"CITE" : 1,
1791 		"DL" : 1,
1792 		"DT" : 1,
1793 		"DD" : 1,
1794 		"POST" : 1,
1795 		"DIV" : 1,
1796 		"SIDE_BY_SIDE" : 1,
1797 		"COLUMN" : 1,
1798 
1799 		"DIVC" : 1,
1800 
1801 		// std.regex
1802 		"REG_ROW": 1,
1803 		"REG_TITLE": 1,
1804 		"REG_TABLE": 1,
1805 		"REG_START": 1,
1806 		"SECTION": 1,
1807 
1808 		// std.math
1809 		"TABLE_SV" : 1,
1810 		"TABLE_DOMRG": 2,
1811 		"DOMAIN": 1,
1812 		"RANGE": 1,
1813 		"SVH": 1,
1814 		"SV": 1,
1815 
1816 		"UDA_USES" : 1,
1817 		"UDA_STRING" : 1,
1818 		"C_HEADER_DESCRIPTION": 1,
1819 	];
1820 
1821 	ddocMacros = [
1822 		"FULLY_QUALIFIED_NAME" : "MAGIC", // decl.fullyQualifiedName,
1823 		"MODULE_NAME" : "MAGIC", // decl.parentModule.fullyQualifiedName,
1824 		"D" : "<tt class=\"D\">$0</tt>", // this is magical! D is actually handled in code.
1825 		"REF" : `<a href="$0.html">$0</a>`, // this is magical! Handles ref in code
1826 
1827 		"ALWAYS_DOCUMENT" : "",
1828 
1829 		"UDA_USES" : "",
1830 		"UDA_STRING" : "",
1831 
1832 		"TIP" : "<div class=\"tip\">$0</div>",
1833 		"NOTE" : "<div class=\"note\">$0</div>",
1834 		"WARNING" : "<div class=\"warning\">$0</div>",
1835 		"PITFALL" : "<div class=\"pitfall\">$0</div>",
1836 		"SIDEBAR" : "<div class=\"sidebar\"><aside>$0</aside></div>",
1837 		"CONSOLE" : "<pre class=\"console\">$0</pre>",
1838 
1839 		// headers have meaning to the table of contents generator
1840 		"H1" : "<h1 class=\"user-header\">$0</h1>",
1841 		"H2" : "<h2 class=\"user-header\">$0</h2>",
1842 		"H3" : "<h3 class=\"user-header\">$0</h3>",
1843 		"H4" : "<h4 class=\"user-header\">$0</h4>",
1844 		"H5" : "<h5 class=\"user-header\">$0</h5>",
1845 		"H6" : "<h6 class=\"user-header\">$0</h6>",
1846 
1847 		"BLOCKQUOTE" : "<blockquote>$0</blockquote>", // FIXME?
1848 		"CITE" : "<cite>$0</cite>", // FIXME?
1849 
1850 		"HR" : "<hr />",
1851 
1852 		"DASH" : "&#45;",
1853 		"NOTHING" : "", // used to escape magic syntax sometimes
1854 
1855 		// DDoc just expects these.
1856 		"AMP" : "&",
1857 		"LT" : "<",
1858 		"GT" : ">",
1859 		"LPAREN" : "(",
1860 		"RPAREN" : ")",
1861 		"DOLLAR" : "$",
1862 		"BACKTICK" : "`",
1863 		"COMMA": ",",
1864 		"COLON": ":",
1865 		"ARGS" : "$0",
1866 
1867 		// support for my docs' legacy components, will be removed before too long.
1868 		"DDOC_ANCHOR" : "<a id=\"$0\" href=\"$(FULLY_QUALIFIED_NAME).html#$0\">$0</a>",
1869 
1870 		"DDOC_COMMENT" : "",
1871 
1872 		// this is support for the Phobos docs
1873 
1874 		// useful parts
1875 		"BOOKTABLE" : "<table class=\"phobos-booktable\"><caption>$1</caption>$+</table>",
1876 		"T2" : "<tr><td>$(LREF $1)</td><td>$+</td></tr>",
1877 
1878 		"BIGOH" : "<span class=\"big-o\"><i>O</i>($0)</span>",
1879 
1880 		"TR" : "<tr>$0</tr>",
1881 		"TH" : "<th>$0</th>",
1882 		"TD" : "<td>$0</td>",
1883 		"TABLE": "<table>$0</table>",
1884 		"TDNW" : "<td style=\"white-space: nowrap;\">$0</td>",
1885 		"LEADINGROWN":"<tr class=\"leading-row\"><th colspan=\"$1\">$2</th></tr>",
1886 		"LEADINGROW":"<tr class=\"leading-row\"><th colspan=\"2\">$0</th></tr>",
1887 
1888 		"UL" : "<ul>$0</ul>",
1889 		"OL" : "<ol>$0</ol>",
1890 		"LI" : "<li>$0</li>",
1891 
1892 		"BR" : "<br />",
1893 		"MDASH": "\&mdash;",
1894 		"COMMA": ",",
1895 		"SECTION3": "<h3>$0</h3>",
1896 		"I" : "<i>$0</i>",
1897 		"EM" : "<em>$0</em>",
1898 		"STRONG" : "<strong>$0</strong>",
1899 		"TT" : "<tt>$0</tt>",
1900 		"B" : "<b>$0</b>",
1901 		"STRIKE" : "<s>$0</s>",
1902 		"P" : "<p>$0</p>",
1903 		"PRE" : "<pre>$0</pre>",
1904 		"DL" : "<dl>$0</dl>",
1905 		"DT" : "<dt>$0</dt>",
1906 		"DD" : "<dd>$0</dd>",
1907 		"LINK2" : "<a href=\"$1\">$2</a>",
1908 		"LINK" : `<a href="$0">$0</a>`,
1909 
1910 		"DDLINK": "<a href=\"http://dlang.org/$1\">$3</a>",
1911 		"DDSUBLINK": "<a href=\"http://dlang.org/$1#$2\">$3</a>",
1912 
1913 		"IMG": "<img src=\"$1\" alt=\"$2\" />",
1914 
1915 		"BLUE" : "<span style=\"color: blue;\">$0</span>",
1916 		"RED" : "<span style=\"color: red;\">$0</span>",
1917 
1918 		"SCRIPT" : "",
1919 
1920 		"UNDOCUMENTED": "<span class=\"undocumented-note\">$0</span>",
1921 
1922 		// Useless crap that should just be replaced
1923 		"REF_SHORT" : `<a href="$2.$3.$1.html">$1</a>`,
1924 		"SHORTREF" : `<a href="$2.$3.$1.html">$1</a>`,
1925 		"SUBMODULE" : `<a href="$(FULLY_QUALIFIED_NAME).$2.html">$1</a>`,
1926 		"SHORTXREF" : `<a href="std.$1.$2.html">$2</a>`,
1927 		"SHORTXREF_PACK" : `<a href="std.$1.$2.$3.html">$3</a>`,
1928 		"XREF" : `<a href="std.$1.$2.html">std.$1.$2</a>`,
1929 		"CXREF" : `<a href="core.$1.$2.html">core.$1.$2</a>`,
1930 		"XREF_PACK" : `<a href="std.$1.$2.$3.html">std.$1.$2.$3</a>`,
1931 		"MYREF" : `<a href="$(FULLY_QUALIFIED_NAME).$0.html">$0</a>`,
1932 		"LREF" : `<a class="symbol-reference" href="$(MODULE_NAME).$0.html">$0</a>`,
1933 		// FIXME: OBJECTREF
1934 		"LREF2" : "MAGIC",
1935 		"MREF" : `MAGIC`,
1936 		"WEB" : `<a href="http://$1">$2</a>`,
1937 		"HTTP" : `<a href="http://$1">$2</a>`,
1938 		"BUGZILLA": `<a href="https://issues.dlang.org/show_bug.cgi?id=$1">https://issues.dlang.org/show_bug.cgi?id=$1</a>`,
1939 		"XREF_PACK_NAMED" : `<a href="std.$1.$2.$3.html">$4</a>`,
1940 
1941 		"D_INLINECODE" : `<tt>$1</tt>`,
1942 		"D_STRING" : `<tt>$1</tt>`,
1943 		"D_CODE_STRING" : `<tt>$1</tt>`,
1944 		"D_CODE" : `<pre>$0</pre>`,
1945 		"HIGHLIGHT" : `<span class="specially-highlighted">$0</span>`,
1946 		"HTTPS" : `<a href="https://$1">$2</a>`,
1947 
1948 		"RES": `<i>result</i>`,
1949 		"POST": `<div class="postcondition">$0</div>`,
1950 		"COMMENT" : ``,
1951 
1952 		"DIV" : `<div>$0</div>`,
1953 		"SIDE_BY_SIDE" : `<table class="side-by-side"><tbody><tr>$0</tr></tbody></table>`,
1954 		"COLUMN" : `<td>$0</td>`,
1955 		"DIVC" : `<div class="$1">$+</div>`,
1956 
1957 		// std.regex
1958 		"REG_ROW":`<tr><td><i>$1</i></td><td>$+</td></tr>`,
1959 		"REG_TITLE":`<tr><td><b>$1</b></td><td><b>$2</b></td></tr>`,
1960 		"REG_TABLE":`<table border="1" cellspacing="0" cellpadding="5" > $0 </table>`,
1961 		"REG_START":`<h3><div align="center"> $0 </div></h3>`,
1962 		"SECTION":`<h3><a id="$0">$0</a></h3>`,
1963 		"S_LINK":`<a href="#$1">$+</a>`,
1964 
1965 		// std.math
1966 
1967 		"TABLE_SV": `<table class="std_math special-values"><caption>Special Values</caption>$0</table>`,
1968 		"SVH" : `<tr><th>$1</th><th>$2</th></tr>`,
1969 		"SV" : `<tr><td>$1</td><td>$2</td></tr>`,
1970 		"TABLE_DOMRG": `<table class="std_math domain-and-range">$1 $2</table>`,
1971 		"DOMAIN": `<tr><th>Domain</th><td>$0</td></tr>`,
1972 		"RANGE": `<tr><th>Range</th><td>$0</td></tr>`,
1973 
1974 		"PI": "\u03c0",
1975 		"INFIN": "\u221e",
1976 		"SUB": "<span>$1<sub>$2</sub></span>",
1977 		"SQRT" : "\u221a",
1978 		"SUPERSCRIPT": "<sup>$1</sup>",
1979 		"SUBSCRIPT": "<sub>$1</sub>",
1980 		"POWER": "<span>$1<sup>$2</sup></span>",
1981 		"NAN": "<span class=\"nan\">NaN</span>",
1982 		"PLUSMN": "\u00b1",
1983 		"GLOSSARY": `<a href="http://dlang.org/glosary.html#$0">$0</a>`,
1984 		"PHOBOSSRC": `<a href="https://github.com/dlang/phobos/blob/master/$0">$0</a>`,
1985 		"DRUNTIMESRC": `<a href="https://github.com/dlang/dmd/blob/master/druntime/src/$0">$0</a>`,
1986 		"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>`,
1987 	];
1988 
1989 	ddocMacroInfo = [ // number of arguments expected, if needed
1990 		"EMBED_UNITTEST": 1,
1991 		"UNDOCUMENTED": 1,
1992 		"BOOKTABLE" : 1,
1993 		"T2" : 1,
1994 		"LEADINGROWN" : 2,
1995 		"WEB" : 2,
1996 		"HTTP" : 2,
1997 		"BUGZILLA" : 1,
1998 		"XREF_PACK_NAMED" : 4,
1999 		"D_INLINECODE" : 1,
2000 		"D_STRING" : 1,
2001 		"D_CODE_STRING": 1,
2002 		"D_CODE": 1,
2003 		"HIGHLIGHT": 1,
2004 		"HTTPS": 2,
2005 		"DIVC" : 1,
2006 		"LINK2" : 2,
2007 		"DDLINK" : 3,
2008 		"DDSUBLINK" : 3,
2009 		"IMG" : 2,
2010 		"XREF" : 2,
2011 		"CXREF" : 2,
2012 		"SHORTXREF" : 2,
2013 		"SHORTREF": 3,
2014 		"REF_SHORT": 3,
2015 		"SUBMODULE" : 2,
2016 		"SHORTXREF_PACK" : 3,
2017 		"XREF_PACK" : 3,
2018 		"MREF" : 1,
2019 		"LREF2" : 2,
2020 		"C_HEADER_DESCRIPTION": 2,
2021 
2022 		"REG_ROW" : 1,
2023 		"REG_TITLE" : 2,
2024 		"S_LINK" : 1,
2025 		"PI": 1,
2026 		"SUB": 2,
2027 		"SQRT" : 1,
2028 		"SUPERSCRIPT": 1,
2029 		"SUBSCRIPT": 1,
2030 		"POWER": 2,
2031 		"NAN": 1,
2032 		"PLUSMN": 1,
2033 		"SVH" : 2,
2034 		"SV": 2,
2035 	];
2036 }
2037 
2038 string formatDocumentationComment(string comment, Decl decl) {
2039 	// remove that annoying ddocism to suppress its auto-highlight anti-feature
2040 	// FIXME
2041 	/*
2042 	auto psymbols = fullyQualifiedName[$-1].split(".");
2043 	// also trim off our .# for overload
2044 	import std.uni :isAlpha;
2045 	auto psymbol = (psymbols.length && psymbols[$-1].length) ?
2046 		((psymbols[$-1][0].isAlpha || psymbols[$-1][0] == '_') ? psymbols[$-1] : psymbols[$-2])
2047 		:
2048 		null;
2049 	if(psymbol.length)
2050 		comment = comment.replace("_" ~ psymbol, psymbol);
2051 	*/
2052 
2053 	auto data = formatDocumentationComment2(comment, decl).toString();
2054 
2055         return data;
2056 }
2057 
2058 
2059 // This is kinda deliberately not recursive right now. I might change that later but I want to keep it
2060 // simple to get decent results while keeping the door open to dropping ddoc macros.
2061 Element expandDdocMacros(string txt, Decl decl) {
2062 	auto e = expandDdocMacros2(txt, decl);
2063 
2064 	translateMagicCommands(e);
2065 
2066 	return e;
2067 }
2068 
2069 void translateMagicCommands(Element container, Element applyTo = null) {
2070 	foreach(cmd; container.querySelectorAll("magic-command")) {
2071 		Element subject;
2072 		if(applyTo is null) {
2073 			subject = cmd.parentNode;
2074 			if(subject is null)
2075 				continue;
2076 			if(subject.tagName == "caption")
2077 				subject = subject.parentNode;
2078 			if(subject is null)
2079 				continue;
2080 		} else {
2081 			subject = applyTo;
2082 		}
2083 
2084 		if(cmd.className.length) {
2085 			subject.addClass(cmd.className);
2086 		}
2087 		if(cmd.id.length)
2088 			subject.id = cmd.id;
2089 		cmd.removeFromTree();
2090 	}
2091 }
2092 
2093 Element expandDdocMacros2(string txt, Decl decl) {
2094 	auto macros = ddocMacros;
2095 	auto numberOfArguments = ddocMacroInfo;
2096 
2097 	auto userDefinedMacros = decl.parsedDocComment.userDefinedMacros;
2098 
2099 	string[string] availableMacros;
2100 	int[string] availableNumberOfArguments;
2101 
2102 	if(userDefinedMacros.length) {
2103 		foreach(name, value; userDefinedMacros) {
2104 			availableMacros[name] = value;
2105 			int count;
2106 			foreach(idx, ch; value) {
2107 				if(ch == '$') {
2108 					if(idx + 1 < value.length) {
2109 						char n = value[idx + 1];
2110 						if(n >= '0' && n <= '9') {
2111 							if(n - '0' > count)
2112 								count = n - '0';
2113 						}
2114 					}
2115 				}
2116 			}
2117 			if(count)
2118 				availableNumberOfArguments[name] = count;
2119 		}
2120 
2121 		foreach(name, value; macros)
2122 			availableMacros[name] = value;
2123 		foreach(name, n; numberOfArguments)
2124 			availableNumberOfArguments[name] = n;
2125 	} else {
2126 		availableMacros = cast(string[string]) macros;
2127 		availableNumberOfArguments = cast(int[string]) numberOfArguments;
2128 	}
2129 
2130 	auto idx = 0;
2131 	if(txt[idx] == '$') {
2132 		auto info = macroInformation(txt[idx .. $]);
2133 		if(info.linesSpanned == 0)
2134 			assert(0);
2135 
2136 		if(idx + 2 > txt.length)
2137 			assert(0);
2138 		if(idx + info.macroNameEndingOffset > txt.length)
2139 			assert(0, txt[idx .. $]);
2140 		if(info.macroNameEndingOffset < 2)
2141 			assert(0, txt[idx..$]);
2142 
2143 		auto name = txt[idx + 2 .. idx + info.macroNameEndingOffset];
2144 
2145 		auto dzeroAdjustment = (info.textBeginningOffset == info.terminatingOffset) ? 0 : 1;
2146 		auto stuff = txt[idx + info.textBeginningOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2147 		auto stuffNonStripped = txt[idx + info.macroNameEndingOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2148 		string replacement;
2149 
2150 		if(name == "RAW_HTML") {
2151 			// magic: user-defined html
2152 			auto holder = Element.make("div", "", "user-raw-html");
2153 			holder.innerHTML = stuff;
2154 			return holder;
2155 		}
2156 
2157 		if(name == "ID")
2158 			return Element.make("magic-command").setAttribute("id", stuff);
2159 		if(name == "CLASS")
2160 			return Element.make("magic-command").setAttribute("class", stuff);
2161 
2162 		if(name == "EMBED_UNITTEST") {
2163 			// FIXME: use /// Documents: Foo.bar to move a test over to something else
2164 			auto id = stuff.strip;
2165 			foreach(ref ut; decl.getProcessedUnittests()) {
2166 				if(ut.comment.canFind("$(ID " ~ id ~ ")")) {
2167 					ut.embedded = true;
2168 					return formatUnittestDocTuple(ut, decl);
2169 				}
2170 			}
2171 		}
2172 
2173 		if(name == "SMALL_TABLE") {
2174 			return translateSmallTable(stuff, decl);
2175 		}
2176 		if(name == "TABLE_ROWS") {
2177 			return translateListTable(stuff, decl);
2178 		}
2179 		if(name == "LIST") {
2180 			return translateList(stuff, decl, "ul");
2181 		}
2182 		if(name == "NUMBERED_LIST") {
2183 			return translateList(stuff, decl, "ol");
2184 		}
2185 
2186 		if(name == "MATH") {
2187 			import adrdox.latex;
2188 			import adrdox.jstex;
2189 
2190 			switch (texMathOpt) with(TexMathOpt) {
2191 				case LaTeX: {
2192 					auto got = mathToImgHtml(stuff);
2193 					if(got is null)
2194 						return Element.make("span", stuff, "user-math-render-failed");
2195 					return got;
2196 				}
2197 				case KaTeX: {
2198 					return mathToKaTeXHtml(stuff);
2199 				}
2200 				default: break;
2201 			}
2202 		}
2203 
2204 		if(name == "ADRDOX_SAMPLE") {
2205 			// show the original source as code
2206 			// then render it for display side-by-side
2207 			auto holder = Element.make("div");
2208 			holder.addClass("adrdox-sample");
2209 
2210 			auto h = holder.addChild("div");
2211 
2212 			h.addChild("pre", outdent(stuffNonStripped));
2213 			h.addChild("div", formatDocumentationComment2(stuff, decl));
2214 
2215 			return holder;
2216 		}
2217 
2218 		if(name == "D" || name == "D_PARAM" || name == "D_CODE") {
2219 			// this is magic: syntax highlight it
2220 			auto holder = Element.make(name == "D_CODE" ? "pre" : "tt");
2221 			holder.className = "D highlighted";
2222 			try {
2223 				holder.innerHTML = linkUpHtml(highlight(stuff), decl);
2224 			} catch(Throwable t) {
2225 				holder.innerText = stuff;
2226 			}
2227 			return holder;
2228 		}
2229 
2230 		if(name == "UDA_STRING") {
2231 			return new TextNode(decl.getStringUda(stuff));
2232 		}
2233 
2234 		if(name == "UDA_USES") {
2235 			Element div = Element.make("dl");
2236 			foreach(d; declsByUda(decl.name, decl.parentModule)) {
2237 				if(!d.docsShouldBeOutputted)
2238 					continue;
2239 				auto dt = div.addChild("dt");
2240 				dt.addChild("a", d.name, d.link);
2241 				div.addChild("dd", Html(formatDocumentationComment(d.parsedDocComment.ddocSummary, d)));
2242 			}
2243 			return div;
2244 		}
2245 
2246 		if(name == "MODULE_NAME") {
2247 			return new TextNode(decl.parentModule.fullyQualifiedName);
2248 		}
2249 		if(name == "FULLY_QUALIFIED_NAME") {
2250 			return new TextNode(decl.fullyQualifiedName);
2251 		}
2252 		if(name == "SUBREF") {
2253 			auto cool = stuff.split(",");
2254 			if(cool.length == 2)
2255 				return getReferenceLink(decl.fullyQualifiedName ~ "." ~ cool[0].strip ~ "." ~ cool[1].strip, decl, cool[1].strip);
2256 		}
2257 		if(name == "MREF_ALTTEXT") {
2258 			auto parts = split(stuff, ",");
2259 			string cool;
2260 			foreach(indx, p; parts[1 .. $]) {
2261 				if(indx) cool ~= ".";
2262 				cool ~= p.strip;
2263 			}
2264 
2265 			return getReferenceLink(cool, decl, parts[0].strip);
2266 		}
2267 		if(name == "LREF2") {
2268 			auto parts = split(stuff, ",");
2269 			return getReferenceLink(parts[1].strip ~ "." ~ parts[0].strip, decl, parts[0].strip);
2270 		}
2271 		if(name == "REF_ALTTEXT") {
2272 			auto parts = split(stuff, ",");
2273 			string cool;
2274 			foreach(indx, p; parts[2 .. $]) {
2275 				if(indx) cool ~= ".";
2276 				cool ~= p.strip;
2277 			}
2278 			cool ~= "." ~ parts[1].strip;
2279 
2280 			return getReferenceLink(cool, decl, parts[0].strip);
2281 		}
2282 
2283 		if(name == "NBSP") {
2284 			return new TextNode("\&nbsp;");
2285 		}
2286 
2287 		if(name == "REF" || name == "MREF" || name == "LREF" || name == "MYREF") {
2288 			// this is magic: do commas to dots then link it
2289 			string cool;
2290 			if(name == "REF") {
2291 				auto parts = split(stuff, ",");
2292 				parts = parts[1 .. $] ~ parts[0];
2293 				foreach(indx, p; parts) {
2294 					if(indx)
2295 						cool ~= ".";
2296 					cool ~= p.strip;
2297 				}
2298 			} else
2299 				cool = replace(stuff, ",", ".").replace(" ", "");
2300 			return getReferenceLink(cool, decl);
2301 		}
2302 
2303 
2304 		if(auto replacementRaw = name in availableMacros) {
2305 			if(auto nargsPtr = name in availableNumberOfArguments) {
2306 				auto nargs = *nargsPtr;
2307 				replacement = *replacementRaw;
2308 
2309 				auto orig = txt[idx + info.textBeginningOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2310 
2311 				foreach(i; 1 .. nargs + 1) {
2312 					int blargh = -1;
2313 
2314 					int parens;
2315 					foreach(cidx, ch; stuff) {
2316 						if(ch == '(')
2317 							parens++;
2318 						if(ch == ')')
2319 							parens--;
2320 						if(parens == 0 && ch == ',') {
2321 							blargh = cast(int) cidx;
2322 							break;
2323 						}
2324 					}
2325 					if(blargh == -1)
2326 						blargh = cast(int) stuff.length;
2327 						//break; // insufficient args
2328 					//blargh = stuff.length - 1; // trims off the closing paren
2329 
2330 					auto magic = stuff[0 .. blargh].strip;
2331 					if(blargh < stuff.length)
2332 						stuff = stuff[blargh + 1 .. $];
2333 					else
2334 						stuff = stuff[$..$];
2335 					// FIXME: this should probably not be full replace each time.
2336 					//if(magic.length)
2337 					magic = formatDocumentationComment2(magic, decl, null).innerHTML;
2338 
2339 					if(name == "T2" && i == 1)
2340 						replacement = replace(replacement, "$(LREF $1)", getReferenceLink(magic, decl).toString);
2341 					else
2342 						replacement = replace(replacement, "$" ~ cast(char)(i + '0'), magic);
2343 				}
2344 
2345 				// FIXME: recursive replacement doesn't work...
2346 
2347 				auto awesome = stuff.strip;
2348 				if(replacement.indexOf("$+") != -1) {
2349 					awesome = formatDocumentationComment2(awesome, decl, null).innerHTML;
2350 					replacement = replace(replacement, "$+", awesome);
2351 				}
2352 
2353 				if(replacement.indexOf("$0") != -1) {
2354 					awesome = formatDocumentationComment2(orig, decl, null).innerHTML;
2355 					replacement = replace(*replacementRaw, "$0", awesome);
2356 				}
2357 			} else {
2358 				// if there is any text, we slice off the ), otherwise, keep the empty string
2359 				auto awesome = txt[idx + info.textBeginningOffset .. idx + info.terminatingOffset - dzeroAdjustment];
2360 
2361 				if(name == "CONSOLE") {
2362 					awesome = outdent(txt[idx + info.macroNameEndingOffset .. idx + info.terminatingOffset - dzeroAdjustment]).strip;
2363 					awesome = awesome.replace("\n---", "\n$(NOTHING)---"); // disable ddoc embedded D code as it breaks for D exception messages
2364 				}
2365 
2366 
2367 				awesome = formatDocumentationComment2(awesome, decl, null).innerHTML;
2368 				replacement = replace(*replacementRaw, "$0", awesome);
2369 			}
2370 
2371 			Element element;
2372 			if(replacement != "<" && replacement.strip.startsWith("<")) {
2373 				element = Element.make("placeholder");
2374 
2375 				element.innerHTML = replacement;
2376 				element = element.childNodes[0];
2377 				element.parentNode = null;
2378 
2379 				foreach(k, v; element.attributes)
2380 					element.attrs[k] = v.replace("$(FULLY_QUALIFIED_NAME)", decl.fullyQualifiedName).replace("$(MODULE_NAME)", decl.parentModule.fullyQualifiedName);
2381 			} else {
2382 				element = new TextNode(replacement);
2383 			}
2384 
2385 			return element;
2386 		} else {
2387 			idx = idx + cast(int) info.terminatingOffset;
2388 			auto textContent = txt[0 .. info.terminatingOffset];
2389 			return new TextNode(textContent);
2390 		}
2391 	} else assert(0);
2392 
2393 
2394 	return null;
2395 }
2396 
2397 struct MacroInformation {
2398 	size_t macroNameEndingOffset; // starting is always 2 if it is actually a macro (check linesSpanned > 0)
2399 	size_t textBeginningOffset;
2400 	size_t terminatingOffset;
2401 	int linesSpanned;
2402 }
2403 
2404 // Scans the current macro
2405 MacroInformation macroInformation(in char[] str) {
2406 	assert(str[0] == '$');
2407 
2408 	MacroInformation info;
2409 	info.terminatingOffset = str.length;
2410 
2411 	if (str.length < 2 || (str[1] != '(' && str[1] != '{'))
2412 		return info;
2413 
2414 	auto open = str[1];
2415 	auto close = str[1] == '(' ? ')' : '}';
2416 
2417 	bool readingMacroName = true;
2418 	bool readingLeadingWhitespace;
2419 	int parensCount = 0;
2420 	foreach (idx, char ch; str)
2421 	{
2422 		if (readingMacroName && (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == ','))
2423 		{
2424 			readingMacroName = false;
2425 			readingLeadingWhitespace = true;
2426 			info.macroNameEndingOffset = idx;
2427 		}
2428 
2429 		if (readingLeadingWhitespace && !(ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'))
2430 		{
2431 			readingLeadingWhitespace = false;
2432 			// This is looking past the macro name
2433 			// so the offset of $(FOO bar) ought to be
2434 			// the index of "bar"
2435 			info.textBeginningOffset = idx;
2436 		}
2437 
2438 		// so i want :) and :( not to count
2439 		// also prolly 1) and 2) etc.
2440 
2441 		if (ch == '\n')
2442 			info.linesSpanned++;
2443 		if (ch == open)
2444 			parensCount++;
2445 		if (ch == close)
2446 		{
2447 			parensCount--;
2448 			if (parensCount == 0)
2449 			{
2450 				if(readingMacroName)
2451 					info.macroNameEndingOffset = idx;
2452 				info.linesSpanned++; // counts the first line it is on
2453 				info.terminatingOffset = idx + 1;
2454 				if (info.textBeginningOffset == 0)
2455 					info.textBeginningOffset = info.terminatingOffset;
2456 				break;
2457 			}
2458 		}
2459 	}
2460 
2461 	if(parensCount)
2462 		info.linesSpanned = 0; // unterminated macro, tell upstream it is plain text
2463 
2464 	return info;
2465 }
2466 
2467 
2468 
2469 string outdent(string s) {
2470 	static import std.string;
2471 	try {
2472 		return join(std..string.outdent(splitLines(s)), "\n");
2473 	} catch(Exception) {
2474 		return s;
2475 	}
2476 }
2477 
2478 
2479 //      Original code:
2480 //          Copyright Brian Schott (Hackerpilot) 2012.
2481 // Distributed under the Boost Software License, Version 1.0.
2482 //    (See accompanying file LICENSE_1_0.txt or copy at
2483 //          http://www.boost.org/LICENSE_1_0.txt)
2484 // Adam modified
2485 
2486 // http://ethanschoonover.com/solarized
2487 string highlight(string sourceCode)
2488 {
2489 
2490     import std.array;
2491     import dparse.lexer;
2492     string ret;
2493 
2494     StringCache cache = StringCache(StringCache.defaultBucketCount);
2495     scope(exit) cache.freeItAll();
2496     LexerConfig config;
2497     config.stringBehavior = StringBehavior.source;
2498     auto tokens = byToken(cast(ubyte[]) sourceCode, config, &cache);
2499 
2500 
2501     void writeSpan(string cssClass, string value, string dataIdent = "")
2502     {
2503         ret ~= `<span class="` ~ cssClass ~ `" `;
2504 	if(dataIdent.length)
2505 		ret ~= "data-ident=\""~dataIdent~"\"";
2506 	ret ~= `>` ~ value.replace("&", "&amp;").replace("<", "&lt;") ~ `</span>`;
2507     }
2508 
2509 
2510 	bool inImport;
2511 	while (!tokens.empty)
2512 	{
2513 		auto t = tokens.front;
2514 		tokens.popFront();
2515 
2516 		// to fix up the import std.string looking silly bug
2517 		if(!inImport && t.type == tok!"import")
2518 			inImport = true;
2519 		else if(inImport && t.type == tok!";")
2520 			inImport = false;
2521 
2522 		if (isBasicType(t.type))
2523 			writeSpan("type", str(t.type));
2524 		else if(!inImport && t.text == "string") // string is a de-facto basic type, though it is technically just a user-defined identifier
2525 			writeSpan("type", t.text);
2526 		else if (isKeyword(t.type))
2527 			writeSpan("kwrd", str(t.type));
2528 		else if (t.type == tok!"comment") {
2529 			auto txt = t.text;
2530 			if(txt == "/* adrdox_highlight{ */")
2531 				ret ~= "<span class=\"specially-highlighted\">";
2532 			else if(txt == "/* }adrdox_highlight */")
2533 				ret ~= "</span>";
2534 			else
2535 				writeSpan("com", t.text);
2536 		} else if (isStringLiteral(t.type) || t.type == tok!"characterLiteral") {
2537 			auto txt = t.text;
2538 			if(txt.startsWith("q{")) {
2539 				ret ~= "<span class=\"token-string-literal\"><span class=\"str\">q{</span>";
2540 				ret ~= highlight(txt[2 .. $ - 1]);
2541 				ret ~= "<span class=\"str\">}</span></span>";
2542 			} else {
2543 				writeSpan("str", txt);
2544 			}
2545 		} else if (isNumberLiteral(t.type))
2546 			writeSpan("num", t.text);
2547 		else if (isOperator(t.type))
2548 			//writeSpan("op", str(t.type));
2549 			ret ~= htmlEncode(str(t.type));
2550 		else if (t.type == tok!"specialTokenSequence" || t.type == tok!"scriptLine")
2551 			writeSpan("cons", t.text);
2552 		else if (t.type == tok!"identifier")
2553 			writeSpan("hid", t.text);
2554 		else
2555 		{
2556 			ret ~= t.text.replace("<", "&lt;");
2557 		}
2558 
2559 	}
2560 	return ret;
2561 }
2562 
2563 
2564 
2565 
2566 import dparse.formatter;
2567 
2568 class MyFormatter(Sink) : Formatter!Sink {
2569 	Decl context;
2570 	this(Sink sink, Decl context = null, bool useTabs = true, IndentStyle style = IndentStyle.otbs, uint indentWidth = 4)
2571 	{
2572 		this.context = context;
2573 		super(sink, useTabs, style, indentWidth);
2574 	}
2575 
2576 	void putTag(in char[] s) {
2577 		static if(!__traits(compiles, sink.putTag("")))
2578 			sink.put(s);
2579 		else
2580 			sink.putTag(s);
2581 	}
2582 
2583 	override void put(string s1) {
2584 		auto s = cast(const(char)[]) s1;
2585 		static if(!__traits(compiles, sink.putTag("")))
2586 			sink.put(s.htmlEncode);
2587 		else
2588 			sink.put(s);
2589 	}
2590 
2591 	override void format(const AtAttribute atAttribute) {
2592 		if(atAttribute is null || atAttribute.argumentList is null)
2593 			super.format(atAttribute);
2594 		else {
2595 			sink.put("@");
2596 			if(atAttribute.argumentList.items.length == 1) {
2597 				format(atAttribute.argumentList.items[0]);
2598 			} else {
2599 				format(atAttribute.argumentList);
2600 			}
2601 		}
2602 	}
2603 
2604 	override void format(const TemplateParameterList templateParameterList)
2605 	{
2606 		putTag("<div class=\"parameters-list\">");
2607 		foreach(i, param; templateParameterList.items)
2608 		{
2609 			putTag("<div class=\"template-parameter-item parameter-item\">");
2610 			put("\t");
2611 			putTag("<span>");
2612 			format(param);
2613 			putTag("</span>");
2614 			putTag("</div>");
2615 		}
2616 		putTag("</div>");
2617 	}
2618 
2619 	override void format(const InStatement inStatement) {
2620 		putTag("<a href=\"http://dpldocs.info/in-contract\" class=\"lang-feature\">");
2621 		put("in");
2622 		putTag("</a>");
2623 		//put(" ");
2624 		if(inStatement.blockStatement)
2625 			format(inStatement.blockStatement);
2626 		else if(inStatement.expression) {
2627 			put(" (");
2628 			format(inStatement.expression);
2629 			put(")");
2630 		}
2631 	}
2632 
2633 	override void format(const OutStatement outStatement) {
2634 		putTag("<a href=\"http://dpldocs.info/out-contract\" class=\"lang-feature\">");
2635 		put("out");
2636 		putTag("</a>");
2637 
2638 		//put(" ");
2639 		if(outStatement.expression) {
2640 			put(" (");
2641 			format(outStatement.parameter);
2642 			put("; ");
2643 			format(outStatement.expression);
2644 			put(")");
2645 		} else {
2646 			if (outStatement.parameter != tok!"")
2647 			{
2648 			    put(" (");
2649 			    format(outStatement.parameter);
2650 			    put(")");
2651 			}
2652 
2653 			format(outStatement.blockStatement);
2654 		}
2655 	}
2656 
2657 	override void format(const AssertExpression assertExpression)
2658 	{
2659 		debug(verbose) writeln("AssertExpression");
2660 
2661 		/**
2662 		  AssignExpression assertion;
2663 		  AssignExpression message;
2664 		 **/
2665 
2666 		with(assertExpression)
2667 		{
2668 			putTag("<a href=\"http://dpldocs.info/assertion\" class=\"lang-feature\">");
2669 			put("assert");
2670 			putTag("</a> (");
2671 			format(assertion);
2672 			if (message)
2673 			{
2674 				put(", ");
2675 				format(message);
2676 			}
2677 			put(")");
2678 		}
2679 	}
2680 
2681 
2682 	override void format(const TemplateAliasParameter templateAliasParameter)
2683 	{
2684 		debug(verbose) writeln("TemplateAliasParameter");
2685 
2686 		/**
2687 		  Type type;
2688 		  Token identifier;
2689 		  Type colonType;
2690 		  AssignExpression colonExpression;
2691 		  Type assignType;
2692 		  AssignExpression assignExpression;
2693 		 **/
2694 
2695 		with(templateAliasParameter)
2696 		{
2697 			putTag("<a href=\"http://dpldocs.info/template-alias-parameter\" class=\"lang-feature\">");
2698 			put("alias");
2699 			putTag("</a>");
2700 			put(" ");
2701 			if (type)
2702 			{
2703 				format(type);
2704 				space();
2705 			}
2706 			format(identifier);
2707 			if (colonType)
2708 			{
2709 				put(" : ");
2710 				format(colonType);
2711 			}
2712 			else if (colonExpression)
2713 			{
2714 				put(" : ");
2715 				format(colonExpression);
2716 			}
2717 			if (assignType)
2718 			{
2719 				put(" = ");
2720 				format(assignType);
2721 			}
2722 			else if (assignExpression)
2723 			{
2724 				put(" = ");
2725 				format(assignExpression);
2726 			}
2727 		}
2728 	}
2729 
2730 
2731 	override void format(const Constraint constraint)
2732 	{
2733 		debug(verbose) writeln("Constraint");
2734 
2735 		if (constraint.expression)
2736 		{
2737 			put(" ");
2738 			putTag("<a href=\"http://dpldocs.info/template-constraints\" class=\"lang-feature\">");
2739 			put("if");
2740 			putTag("</a>");
2741 			put(" (");
2742 			putTag("<div class=\"template-constraint-expression\">");
2743 			format(constraint.expression);
2744 			putTag("</div>");
2745 			put(")");
2746 		}
2747 	}
2748 
2749 
2750 
2751 	override void format(const PrimaryExpression primaryExpression)
2752 	{
2753 		debug(verbose) writeln("PrimaryExpression");
2754 
2755 		/**
2756 		  Token dot;
2757 		  Token primary;
2758 		  IdentifierOrTemplateInstance identifierOrTemplateInstance;
2759 		  Token basicType;
2760 		  TypeofExpression typeofExpression;
2761 		  TypeidExpression typeidExpression;
2762 		  ArrayLiteral arrayLiteral;
2763 		  AssocArrayLiteral assocArrayLiteral;
2764 		  Expression expression;
2765 		  IsExpression isExpression;
2766 		  LambdaExpression lambdaExpression;
2767 		  FunctionLiteralExpression functionLiteralExpression;
2768 		  TraitsExpression traitsExpression;
2769 		  MixinExpression mixinExpression;
2770 		  ImportExpression importExpression;
2771 		  Vector vector;
2772 		 **/
2773 
2774 		with(primaryExpression)
2775 		{
2776 			if (dot != tok!"") put(".");
2777 			if (basicType != tok!"") format(basicType);
2778 			if (primary != tok!"")
2779 			{
2780 				if (basicType != tok!"") put("."); // i.e. : uint.max
2781 				format(primary);
2782 			}
2783 
2784 			if (expression)
2785 			{
2786 				putTag("<span class=\"parenthetical-expression\">(<span class=\"parenthetical-expression-contents\">");
2787 				format(expression);
2788 				putTag("</span>)</span>");
2789 			}
2790 			else if (identifierOrTemplateInstance)
2791 			{
2792 				format(identifierOrTemplateInstance);
2793 			}
2794 			else if (typeofExpression) format(typeofExpression);
2795 			else if (typeidExpression) format(typeidExpression);
2796 			else if (arrayLiteral) format(arrayLiteral);
2797 			else if (assocArrayLiteral) format(assocArrayLiteral);
2798 			else if (isExpression) format(isExpression);
2799 			else if (lambdaExpression) { putTag("<span class=\"lambda-expression\">"); format(lambdaExpression); putTag("</span>"); }
2800 			else if (functionLiteralExpression) format(functionLiteralExpression);
2801 			else if (traitsExpression) format(traitsExpression);
2802 			else if (mixinExpression) format(mixinExpression);
2803 			else if (importExpression) format(importExpression);
2804 			else if (vector) format(vector);
2805 		}
2806 	}
2807 
2808 	override void format(const IsExpression isExpression) {
2809 		//Formatter!Sink.format(this);
2810 
2811 		with(isExpression) {
2812 			putTag(`<a href="http://dpldocs.info/is-expression" class="lang-feature">is</a>(`);
2813 			if (type) format(type);
2814 			if (identifier != tok!"") {
2815 				space();
2816 				format(identifier);
2817 			}
2818 
2819 			if (equalsOrColon) {
2820 				space();
2821 				put(tokenRep(equalsOrColon));
2822 				space();
2823 			}
2824 
2825 			if (typeSpecialization) format(typeSpecialization);
2826 			if (templateParameterList) {
2827 				put(", ");
2828 				format(templateParameterList);
2829 			}
2830 			put(")");
2831 		}
2832 	}
2833 
2834 	override void format(const TypeofExpression typeofExpr) {
2835 		debug(verbose) writeln("TypeofExpression");
2836 
2837 		/**
2838 		Expression expression;
2839 		Token return_;
2840 		**/
2841 
2842 		putTag("<a href=\"http://dpldocs.info/typeof-expression\" class=\"lang-feature\">typeof</a>(");
2843 		typeofExpr.expression ? format(typeofExpr.expression) : format(typeofExpr.return_);
2844 		put(")");
2845 	}
2846 
2847 
2848 	override void format(const Parameter parameter)
2849 	{
2850 		debug(verbose) writeln("Parameter");
2851 
2852 		/**
2853 		  IdType[] parameterAttributes;
2854 		  Type type;
2855 		  Token name;
2856 		  bool vararg;
2857 		  AssignExpression default_;
2858 		  TypeSuffix[] cstyle;
2859 		 **/
2860 
2861 		putTag("<div class=\"runtime-parameter-item parameter-item\">");
2862 
2863 
2864 	bool hadAtAttribute;
2865 
2866 		putTag("<span class=\"parameter-type-holder\">");
2867 		putTag("<span class=\"parameter-type\">");
2868 		foreach (count, attribute; parameter.parameterAttributes)
2869 		{
2870 			if (count || hadAtAttribute) space();
2871 			putTag("<span class=\"storage-class\">");
2872 			put(tokenRep(attribute));
2873 			putTag("</span>");
2874 		}
2875 
2876 		if (parameter.parameterAttributes.length > 0)
2877 			space();
2878 
2879 		if (parameter.type !is null)
2880 			format(parameter.type);
2881 
2882 		putTag(`</span>`);
2883 		putTag(`</span>`);
2884 
2885 		if (parameter.name.type != tok!"")
2886 		{
2887 			space();
2888 			putTag(`<span class="parameter-name name" data-ident="`~parameter.name.text~`">`);
2889 			putTag("<a href=\"#param-" ~ parameter.name.text ~ "\">");
2890 			put(parameter.name.text);
2891 			putTag("</a>");
2892 			putTag("</span>");
2893 		}
2894 
2895 		foreach(suffix; parameter.cstyle)
2896 			format(suffix);
2897 
2898 		if (parameter.default_)
2899 		{
2900 			putTag(`<span class="parameter-default-value">`);
2901 			putTag("&nbsp;=&nbsp;");
2902 			format(parameter.default_);
2903 			putTag(`</span>`);
2904 		}
2905 
2906 		if (parameter.vararg) {
2907 			putTag("<a href=\"http://dpldocs.info/typed-variadic-function-arguments\" class=\"lang-feature\">");
2908 			put("...");
2909 			putTag("</a>");
2910 		}
2911 
2912 		putTag("</div>");
2913 	}
2914 
2915 	override void format(const Type type)
2916 	{
2917 		debug(verbose) writeln("Type(");
2918 
2919 		/**
2920 		  IdType[] typeConstructors;
2921 		  TypeSuffix[] typeSuffixes;
2922 		  Type2 type2;
2923 		 **/
2924 
2925 		foreach (count, constructor; type.typeConstructors)
2926 		{
2927 			if (count) space();
2928 			put(tokenRep(constructor));
2929 		}
2930 
2931 		if (type.typeConstructors.length) space();
2932 
2933 		//put(`<span class="name" data-ident="`~tokenRep(token)~`">`);
2934 		format(type.type2);
2935 		//put("</span>");
2936 
2937 		foreach (suffix; type.typeSuffixes)
2938 			format(suffix);
2939 
2940 		debug(verbose) writeln(")");
2941 	}
2942 
2943 	override void format(const Type2 type2)
2944 	{
2945 		debug(verbose) writeln("Type2");
2946 
2947 		/**
2948 		  IdType builtinType;
2949 		  Symbol symbol;
2950 		  TypeofExpression typeofExpression;
2951 		  IdentifierOrTemplateChain identifierOrTemplateChain;
2952 		  IdType typeConstructor;
2953 		  Type type;
2954 		 **/
2955 
2956 		if (type2.symbol !is null)
2957 		{
2958 			suppressMagic = true;
2959 			scope(exit) suppressMagic = false;
2960 
2961 			Decl link;
2962 			if(context)
2963 				link = context.lookupName(type2.symbol);
2964 			if(link !is null) {
2965 				putTag("<a href=\""~link.link~"\" title=\""~link.fullyQualifiedName~"\" class=\"xref\">");
2966 				format(type2.symbol);
2967 				putTag("</a>");
2968 			} else if(toText(type2.symbol) == "string") {
2969 				putTag("<span class=\"builtin-type\">");
2970 				put("string");
2971 				putTag("</span>");
2972 			} else {
2973 				format(type2.symbol);
2974 			}
2975 		}
2976 		else if (type2.typeofExpression !is null)
2977 		{
2978 			format(type2.typeofExpression);
2979 			if (type2.identifierOrTemplateChain)
2980 			{
2981 				put(".");
2982 				format(type2.identifierOrTemplateChain);
2983 			}
2984 			return;
2985 		}
2986 		else if (type2.typeConstructor != tok!"")
2987 		{
2988 			putTag("<span class=\"type-constructor\">");
2989 			put(tokenRep(type2.typeConstructor));
2990 			putTag("</span>");
2991 			put("(");
2992 			format(type2.type);
2993 			put(")");
2994 		}
2995 		else
2996 		{
2997 			putTag("<span class=\"builtin-type\">");
2998 			put(tokenRep(type2.builtinType));
2999 			putTag("</span>");
3000 		}
3001 	}
3002 
3003 
3004 	override void format(const StorageClass storageClass)
3005 	{
3006 		debug(verbose) writeln("StorageClass");
3007 
3008 		/**
3009 		  AtAttribute atAttribute;
3010 		  Deprecated deprecated_;
3011 		  LinkageAttribute linkageAttribute;
3012 		  Token token;
3013 		 **/
3014 
3015 		with(storageClass)
3016 		{
3017 			if (atAttribute) format(atAttribute);
3018 			else if (deprecated_) format(deprecated_);
3019 			else if (linkageAttribute) format(linkageAttribute);
3020 			else {
3021 				putTag("<span class=\"storage-class\">");
3022 				format(token);
3023 				putTag("</span>");
3024 			}
3025 		}
3026 	}
3027 
3028 
3029 	bool noTag;
3030 	override void format(const Token token)
3031 	{
3032 		debug(verbose) writeln("Token ", tokenRep(token));
3033 		if(!noTag && token == tok!"identifier") {
3034 			putTag(`<span class="name" data-ident="`~tokenRep(token)~`">`);
3035 		}
3036 		auto rep = tokenRep(token);
3037 		if(rep == "delegate" || rep == "function") {
3038 			putTag(`<span class="lang-feature">`);
3039 			put(rep);
3040 			putTag(`</span>`);
3041 		} else {
3042 			put(rep);
3043 		}
3044 		if(!noTag && token == tok!"identifier") {
3045 			putTag("</span>");
3046 		}
3047 	}
3048 
3049 	bool suppressMagic = false;
3050 
3051 	override void format(const IdentifierOrTemplateInstance identifierOrTemplateInstance)
3052 	{
3053 		debug(verbose) writeln("IdentifierOrTemplateInstance");
3054 
3055 		bool suppressMagicGoingIn = suppressMagic;
3056 
3057 		//if(!suppressMagicGoingIn)
3058 			//putTag("<span class=\"some-ident\">");
3059 		with(identifierOrTemplateInstance)
3060 		{
3061 			format(identifier);
3062 			if (templateInstance)
3063 				format(templateInstance);
3064 		}
3065 		//if(!suppressMagicGoingIn)
3066 			//putTag("</span>");
3067 	}
3068 
3069 	version(none)
3070 	override void format(const Symbol symbol)
3071 	{
3072 		debug(verbose) writeln("Symbol");
3073 
3074 		put("GOOD/");
3075 		if (symbol.dot)
3076 			put(".");
3077 		format(symbol.identifierOrTemplateChain);
3078 		put("/GOOD");
3079 	}
3080 
3081 	override void format(const Parameters parameters)
3082 	{
3083 		debug(verbose) writeln("Parameters");
3084 
3085 		/**
3086 		  Parameter[] parameters;
3087 		  bool hasVarargs;
3088 		 **/
3089 
3090 		putTag("<div class=\"parameters-list\">");
3091 		putTag("<span class=\"paren\">(</span>");
3092 		foreach (count, param; parameters.parameters)
3093 		{
3094 			if (count) putTag("<span class=\"comma\">,</span>");
3095 			format(param);
3096 		}
3097 		if (parameters.hasVarargs)
3098 		{
3099 			if (parameters.parameters.length)
3100 				putTag("<span class=\"comma\">,</span>");
3101 			putTag("<div class=\"runtime-parameter-item parameter-item\">");
3102 			putTag("<a href=\"http://dpldocs.info/variadic-function-arguments\" class=\"lang-feature\">");
3103 			putTag("...");
3104 			putTag("</a>");
3105 			putTag("</div>");
3106 		}
3107 		putTag("<span class=\"paren\">)</span>");
3108 		putTag("</div>");
3109 	}
3110 
3111     	override void format(const IdentifierChain identifierChain)
3112 	{
3113 		//put("IDENT");
3114 		foreach(count, ident; identifierChain.identifiers)
3115 		{
3116 		    if (count) put(".");
3117 		    put(ident.text);
3118 		}
3119 		//put("/IDENT");
3120 	}
3121 
3122 	override void format(const AndAndExpression andAndExpression)
3123 	{
3124 		with(andAndExpression)
3125 		{
3126 			putTag("<div class=\"andand-left\">");
3127 			format(left);
3128 			if (right)
3129 			{
3130 				putTag(" &amp;&amp; </div><div class=\"andand-right\">");
3131 				format(right);
3132 			}
3133 			putTag("</div>");
3134 		}
3135 	}
3136 
3137 	override void format(const OrOrExpression orOrExpression)
3138 	{
3139 		with(orOrExpression)
3140 		{
3141 			putTag("<div class=\"oror-left\">");
3142 			format(left);
3143 			if (right)
3144 			{
3145 				putTag(" || </div><div class=\"oror-right\">");
3146 				format(right);
3147 			}
3148 			putTag("</div>");
3149 		}
3150 	}
3151 
3152 	alias format = Formatter!Sink.format;
3153 }
3154 
3155 
3156 /*
3157 	The small table looks like this:
3158 
3159 
3160 	Caption
3161 	Header | Row
3162 	Data | Row
3163 	Data | Row
3164 */
3165 Element translateSmallTable(string text, Decl decl) {
3166 	auto holder = Element.make("table");
3167 	holder.addClass("small-table");
3168 
3169 	void skipWhitespace() {
3170 		while(text.length && (text[0] == ' ' || text[0] == '\t'))
3171 			text = text[1 .. $];
3172 	}
3173 
3174 	string[] extractLine() {
3175 		string[] cells;
3176 
3177 		skipWhitespace();
3178 		size_t i;
3179 
3180 		while(i < text.length) {
3181 			static string lastText;
3182 			if(text[i .. $] is lastText)
3183 				assert(0, lastText);
3184 
3185 			lastText = text[i .. $];
3186 			switch(text[i]) {
3187 				case '|':
3188 					cells ~= text[0 .. i].strip;
3189 					text = text[i + 1 .. $];
3190 					i = 0;
3191 				break;
3192 				case '`':
3193 					i++;
3194 					while(i < text.length && text[i] != '`')
3195 						i++;
3196 					if(i < text.length)
3197 						i++; // skip closing `
3198 				break;
3199 				case '[':
3200 				case '(':
3201 				case '{':
3202 					i++; // FIXME? skip these?
3203 				break;
3204 				case '$':
3205 					auto info = macroInformation(text[i .. $]);
3206 					i += info.terminatingOffset + 1;
3207 				break;
3208 				case '\n':
3209 					cells ~= text[0 .. i].strip;
3210 					text = text[i + 1 .. $];
3211 					return cells;
3212 				break;
3213 				default:
3214 					i++;
3215 			}
3216 		}
3217 
3218 		return cells;
3219 	}
3220 
3221 	bool isDecorative(string[] row) {
3222 		foreach(s; row)
3223 		foreach(char c; s)
3224 			if(c != '+' && c != '-' && c != '|' && c != '=')
3225 				return false;
3226 		return true;
3227 	}
3228 
3229 	Element tbody;
3230 	bool first = true;
3231 	bool twodTable;
3232 	bool firstLine = true;
3233 	string[] headerRow;
3234 	while(text.length) {
3235 		auto row = extractLine();
3236 
3237 		if(row.length == 0)
3238 			continue; // should never happen...
3239 
3240 		if(row.length == 1 && row[0].length == 0)
3241 			continue; // blank line
3242 
3243 		if(isDecorative(row))
3244 			continue; // ascii art is allowed, but ignored
3245 
3246 		if(firstLine && row.length == 1 && row[0].length) {
3247 			firstLine = false;
3248 			holder.addChild("caption", row[0]);
3249 			continue;
3250 		}
3251 
3252 		firstLine = false;
3253 
3254 		// empty first and last rows are just surrounding pipes and can be ignored
3255 		if(row[0].length == 0)
3256 			row = row[1 .. $];
3257 		if(row[$-1].length == 0)
3258 			row = row[0 .. $-1];
3259 
3260 		if(first) {
3261 			auto thead = holder.addChild("thead");
3262 			auto tr = thead.addChild("tr");
3263 
3264 			headerRow = row;
3265 
3266 			if(row[0].length == 0)
3267 				twodTable = true;
3268 
3269 			foreach(cell; row)
3270 				tr.addChild("th", formatDocumentationComment2(cell, decl, null));
3271 			first = false;
3272 		} else {
3273 			if(tbody is null)
3274 				tbody = holder.addChild("tbody");
3275 			auto tr = tbody.addChild("tr");
3276 
3277 			while(row.length < headerRow.length)
3278 				row ~= "";
3279 
3280 			foreach(i, cell; row)
3281 				tr.addChild((twodTable && i == 0) ? "th" : "td", formatDocumentationComment2(cell, decl, null));
3282 
3283 		}
3284 	}
3285 
3286 	// FIXME: format the cells
3287 
3288 	if(twodTable)
3289 		holder.addClass("two-axes");
3290 
3291 
3292 	return holder;
3293 }
3294 
3295 Element translateListTable(string text, Decl decl) {
3296 	auto holder = Element.make("table");
3297 	holder.addClass("user-table");
3298 
3299 	DocCommentTerminationCondition termination;
3300 	termination.terminationStrings = ["*", "-", "+"];
3301 	termination.mustBeAtStartOfLine = true;
3302 
3303 	string nextTag;
3304 
3305 	Element row;
3306 
3307 	do {
3308 		auto fmt = formatDocumentationComment2(text, decl, null, &termination);
3309 		if(fmt.toString.strip.length) {
3310 			if(row is null)
3311 				holder.addChild("caption", fmt);
3312 			else
3313 				row.addChild(nextTag, fmt);
3314 		}
3315 		text = termination.remaining;
3316 		if(termination.terminatedOn == "*")
3317 			row = holder.addChild("tr");
3318 		else if(termination.terminatedOn == "+")
3319 			nextTag = "th";
3320 		else if(termination.terminatedOn == "-")
3321 			nextTag = "td";
3322 		else {}
3323 	} while(text.length);
3324 
3325 	if(holder.querySelector("th:first-child + td"))
3326 		holder.addClass("two-axes");
3327 
3328 	return holder;
3329 }
3330 
3331 Element translateList(string text, Decl decl, string tagName) {
3332 	auto holder = Element.make(tagName);
3333 	holder.addClass("user-list");
3334 
3335 	DocCommentTerminationCondition termination;
3336 	termination.terminationStrings = ["*"];
3337 	termination.mustBeAtStartOfLine = true;
3338 
3339 	auto opening = formatDocumentationComment2(text, decl, null, &termination);
3340 	text = termination.remaining;
3341 
3342 	translateMagicCommands(opening, holder);
3343 
3344 	while(text.length) {
3345 		auto fmt = formatDocumentationComment2(text, decl, null, &termination);
3346 		holder.addChild("li", fmt);
3347 		text = termination.remaining;
3348 	}
3349 
3350 	return holder;
3351 }
3352 
3353 Html syntaxHighlightCss(string code) {
3354 	string highlighted;
3355 
3356 
3357 	int pullPiece(string code, bool includeDot) {
3358 		int i;
3359 		while(i < code.length && (
3360 			(code[i] >= '0' && code[i] <= '9')
3361 			||
3362 			(code[i] >= 'a' && code[i] <= 'z')
3363 			||
3364 			(code[i] >= 'A' && code[i] <= 'Z')
3365 			||
3366 			(includeDot && code[i] == '.') || code[i] == '_' || code[i] == '-' || code[i] == ':' // for css i want to match like :first-letter
3367 		))
3368 		{
3369 			i++;
3370 		}
3371 		return i;
3372 	}
3373 
3374 
3375 
3376 	while(code.length) {
3377 		switch(code[0]) {
3378 			case '\'':
3379 			case '\"':
3380 				// string literal
3381 				char start = code[0];
3382 				size_t i = 1;
3383 				while(i < code.length && code[i] != start && code[i] != '\n') {
3384 					if(code[i] == '\\')
3385 						i++; // skip escaped char too
3386 					i++;
3387 				}
3388 
3389 				i++; // skip closer
3390 				highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3391 				code = code[i .. $];
3392 			break;
3393 			case '/':
3394 				// check for comment
3395 				if(code.length > 1 && code[1] == '*') {
3396 					// a star comment
3397 					size_t i;
3398 					while((i+1) < code.length && !(code[i] == '*' && code[i+1] == '/'))
3399 						i++;
3400 
3401 					if(i < code.length)
3402 						i+=2; // skip the */
3403 
3404 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3405 					code = code[i .. $];
3406 				} else {
3407 					highlighted ~= '/';
3408 					code = code[1 .. $];
3409 				}
3410 			break;
3411 			case '<':
3412 				highlighted ~= "&lt;";
3413 				code = code[1 .. $];
3414 			break;
3415 			case '>':
3416 				highlighted ~= "&gt;";
3417 				code = code[1 .. $];
3418 			break;
3419 			case '&':
3420 				highlighted ~= "&amp;";
3421 				code = code[1 .. $];
3422 			break;
3423 			case '@':
3424 				// @media, @import, @font-face, etc
3425 				auto i = pullPiece(code[1 .. $], false);
3426 				if(i)
3427 					i++;
3428 				else
3429 					goto plain;
3430 				highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3431 				code = code[i .. $];
3432 			break;
3433 			case '\n', ';':
3434 				// check for a new rule (imprecise since we don't actually parse, but meh
3435 				// will ignore indent, then see if there is a word-like-this followed by :
3436 				int i = 1;
3437 				while(i < code.length && (code[i] == ' ' || code[i] == '\t'))
3438 					i++;
3439 				int start = i;
3440 				while(i < code.length && (code[i] == '-' || (code[i] >= 'a' && code[i] <= 'z')))
3441 					i++;
3442 				if(start == i || i == code.length || code[i] != ':')
3443 					goto plain;
3444 
3445 				highlighted ~= code[0 .. start];
3446 				highlighted ~= "<span class=\"highlighted-named-constant\">" ~ htmlEntitiesEncode(code[start .. i]) ~ "</span>";
3447 				code = code[i .. $];
3448 			break;
3449 			case '[':
3450 				// attribute match rule
3451 				auto i = pullPiece(code[1 .. $], false);
3452 				if(i)
3453 					i++;
3454 				else
3455 					goto plain;
3456 				highlighted ~= "<span class=\"highlighted-attribute-name\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3457 				code = code[i .. $];
3458 			break;
3459 			case '#', '.': // id or class selector
3460 				auto i = pullPiece(code[1 .. $], false);
3461 				if(i)
3462 					i++;
3463 				else
3464 					goto plain;
3465 				highlighted ~= "<span class=\"highlighted-identifier\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3466 				code = code[i .. $];
3467 			break;
3468 			case ':':
3469 				// pseudoclass
3470 				auto i = pullPiece(code, false);
3471 				highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3472 				code = code[i .. $];
3473 			break;
3474 			case '(':
3475 				// just skipping stuff in parens...
3476 				int parenCount = 0;
3477 				int pos = 0;
3478 				bool inQuote;
3479 				bool escaped;
3480 				while(pos < code.length) {
3481 					if(code[pos] == '\\')
3482 						escaped = true;
3483 					if(!escaped && code[pos] == '"')
3484 						inQuote = !inQuote;
3485 					if(!inQuote && code[pos] == '(')
3486 						parenCount++;
3487 					if(!inQuote && code[pos] == ')')
3488 						parenCount--;
3489 					pos++;
3490 					if(parenCount == 0)
3491 						break;
3492 				}
3493 
3494 				highlighted ~= "<span class=\"highlighted-identifier\">" ~ htmlEntitiesEncode(code[0 .. pos]) ~ "</span>";
3495 				code = code[pos .. $];
3496 			break;
3497 			case '0': .. case '9':
3498 				// number. including text at the end cuz it is probably a unit
3499 				auto i = pullPiece(code, true);
3500 				highlighted ~= "<span class=\"highlighted-number\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3501 				code = code[i .. $];
3502 			break;
3503 			default:
3504 			plain:
3505 				highlighted ~= code[0];
3506 				code = code[1 .. $];
3507 		}
3508 	}
3509 
3510 	return Html(highlighted);
3511 }
3512 
3513 string highlightHtmlTag(string tag) {
3514 	string highlighted = "&lt;";
3515 
3516 	bool isWhite(char c) {
3517 		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
3518 	}
3519 
3520 	tag = tag[1 .. $];
3521 
3522 	if(tag[0] == '/') {
3523 		highlighted ~= '/';
3524 		tag = tag[1 .. $];
3525 	}
3526 
3527 	int i = 0;
3528 	while(i < tag.length && !isWhite(tag[i]) && tag[i] != '>') {
3529 		i++;
3530 	}
3531 
3532 	highlighted ~= "<span class=\"highlighted-tag-name\">" ~ htmlEntitiesEncode(tag[0 .. i]) ~ "</span>";
3533 	tag = tag[i .. $];
3534 
3535 	while(tag.length && tag[0] != '>') {
3536 		while(isWhite(tag[0])) {
3537 			highlighted ~= tag[0];
3538 			tag = tag[1 .. $];
3539 		}
3540 
3541 		i = 0;
3542 		while(i < tag.length && tag[i] != '=' && tag[i] != '>')
3543 			i++;
3544 
3545 		highlighted ~= "<span class=\"highlighted-attribute-name\">" ~ htmlEntitiesEncode(tag[0 .. i]) ~ "</span>";
3546 		tag = tag[i .. $];
3547 
3548 		if(tag.length && tag[0] == '=') {
3549 			highlighted ~= '=';
3550 			tag = tag[1 .. $];
3551 			if(tag.length && (tag[0] == '\'' || tag[0] == '"')) {
3552 				char end = tag[0];
3553 				i = 1;
3554 				while(i < tag.length && tag[i] != end)
3555 					i++;
3556 				if(i < tag.length)
3557 					i++;
3558 
3559 				highlighted ~= "<span class=\"highlighted-attribute-value\">" ~ htmlEntitiesEncode(tag[0 .. i]) ~ "</span>";
3560 				tag = tag[i .. $];
3561 			} else if(tag.length) {
3562 				i = 0;
3563 				while(i < tag.length && !isWhite(tag[i]))
3564 					i++;
3565 
3566 				highlighted ~= "<span class=\"highlighted-attribute-value\">" ~ htmlEntitiesHighlight(tag[0 .. i]) ~ "</span>";
3567 				tag = tag[i .. $];
3568 			}
3569 		}
3570 	}
3571 
3572 
3573 	highlighted ~= htmlEntitiesEncode(tag);
3574 
3575 	return highlighted;
3576 }
3577 
3578 string htmlEntitiesHighlight(string code) {
3579 	string highlighted;
3580 
3581 	bool isWhite(char c) {
3582 		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
3583 	}
3584 
3585 	while(code.length)
3586 	switch(code[0]) {
3587 		case '&':
3588 			// possibly an entity
3589 			int i = 0;
3590 			while(i < code.length && code[i] != ';' && !isWhite(code[i]))
3591 				i++;
3592 			if(i == code.length || isWhite(code[i])) {
3593 				highlighted ~= "&amp;";
3594 				code = code[1 .. $];
3595 			} else {
3596 				i++;
3597 				highlighted ~=
3598 					"<abbr class=\"highlighted-entity\" title=\"Entity: "~code[0 .. i].replace("\"", "&quot;")~"\">" ~
3599 						htmlEntitiesEncode(code[0 .. i]) ~
3600 					"</abbr>";
3601 				code = code[i .. $];
3602 			}
3603 		break;
3604 		case '<':
3605 			highlighted ~= "&lt;";
3606 			code = code[1 .. $];
3607 		break;
3608 		case '"':
3609 			highlighted ~= "&quot;";
3610 			code = code[1 .. $];
3611 		break;
3612 		case '>':
3613 			highlighted ~= "&gt;";
3614 			code = code[1 .. $];
3615 		break;
3616 		default:
3617 			highlighted ~= code[0];
3618 			code = code[1 .. $];
3619 	}
3620 
3621 	return highlighted;
3622 }
3623 
3624 Html syntaxHighlightRuby(string code) {
3625 	// FIXME this is quite minimal and crappy
3626 	// ruby just needs to be parsed to be lexed!
3627 	// and i didn't implement its myriad of strings
3628 
3629 	static immutable string[] rubyKeywords = [
3630 		"BEGIN", "ensure", "self", "when",
3631 		"END", "not", "super", "while",
3632 		"alias", "defined", "for", "or", "then", "yield",
3633 		"and", "do", "if", "redo",
3634 		"begin", "else", "in", "rescue", "undef",
3635 		"break", "elsif", "retry", "unless",
3636 		"case", "end", "next", "return", "until",
3637 	];
3638 
3639 	static immutable string[] rubyPreprocessors = [
3640 		"require", "include"
3641 	];
3642 
3643 	static immutable string[] rubyTypes = [
3644 		"class", "def", "module"
3645 	];
3646 
3647 	static immutable string[] rubyConstants = [
3648 		"nil", "false", "true",
3649 	];
3650 
3651 	bool lastWasType;
3652 	char lastChar;
3653 	int indentLevel;
3654 	int[] declarationIndentLevels;
3655 
3656 	string highlighted;
3657 
3658 	while(code.length) {
3659 		bool thisIterWasType = false;
3660 		auto ch = code[0];
3661 		switch(ch) {
3662 			case '#':
3663 				size_t i;
3664 				while(i < code.length && code[i] != '\n')
3665 					i++;
3666 
3667 				highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3668 				code = code[i .. $];
3669 			break;
3670 			case '<':
3671 				highlighted ~= "&lt;";
3672 				code = code[1 .. $];
3673 			break;
3674 			case '>':
3675 				highlighted ~= "&gt;";
3676 				code = code[1 .. $];
3677 			break;
3678 			case '&':
3679 				highlighted ~= "&amp;";
3680 				code = code[1 .. $];
3681 			break;
3682 			case '0': .. case '9':
3683 				// number literal
3684 				size_t i;
3685 				while(i < code.length && (
3686 					(code[i] >= '0' && code[i] <= '9')
3687 					||
3688 					(code[i] >= 'a' && code[i] <= 'z')
3689 					||
3690 					(code[i] >= 'A' && code[i] <= 'Z')
3691 					||
3692 					code[i] == '.' || code[i] == '_'
3693 				))
3694 				{
3695 					i++;
3696 				}
3697 
3698 				highlighted ~= "<span class=\"highlighted-number\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3699 				code = code[i .. $];
3700 			break;
3701 			case '"', '\'':
3702 				// string
3703 				// check for triple-quoted string
3704 				if(ch == '"') {
3705 
3706 					// double quote string
3707 					auto idx = 1;
3708 					bool escaped = false;
3709 					int nestedCount = 0;
3710 					bool justSawHash = false;
3711 					while(!escaped && nestedCount == 0 && code[idx] != '"') {
3712 						if(justSawHash && code[idx] == '{')
3713 							nestedCount++;
3714 						if(nestedCount && code[idx] == '}')
3715 							nestedCount--;
3716 
3717 						if(!escaped && code[idx] == '#')
3718 							justSawHash = true;
3719 
3720 						if(code[idx] == '\\')
3721 							escaped = true;
3722 						else {
3723 							escaped = false;
3724 						}
3725 
3726 						idx++;
3727 					}
3728 					idx++;
3729 
3730 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. idx]) ~ "</span>";
3731 					code = code[idx .. $];
3732 				} else {
3733 					int end = 1;
3734 					bool escaped;
3735 					while(end < code.length && !escaped && code[end] != ch) {
3736 						escaped = (code[end] == '\\');
3737 						end++;
3738 					}
3739 
3740 					if(end < code.length)
3741 						end++;
3742 
3743 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. end]) ~ "</span>";
3744 					code = code[end .. $];
3745 				}
3746 			break;
3747 			case '\n':
3748 				indentLevel = 0;
3749 				int i = 1;
3750 				while(i < code.length && (code[i] == ' ' || code[i] == '\t')) {
3751 					i++;
3752 					indentLevel++;
3753 				}
3754 				highlighted ~= ch;
3755 				code = code[1 .. $];
3756 			break;
3757 			case ' ', '\t':
3758 				thisIterWasType = lastWasType; // don't change the last counter on just whitespace
3759 				highlighted ~= ch;
3760 				code = code[1 .. $];
3761 			break;
3762 			case ':':
3763 				// check for ruby symbol
3764 				int nameEnd = 1;
3765 				while(nameEnd < code.length && (
3766 					(code[nameEnd] >= 'a' && code[nameEnd] <= 'z') ||
3767 					(code[nameEnd] >= 'A' && code[nameEnd] <= 'Z') ||
3768 					(code[nameEnd] >= '0' && code[nameEnd] <= '0') ||
3769 					code[nameEnd] == '_'
3770 				))
3771 				{
3772 					nameEnd++;
3773 				}
3774 
3775 				if(nameEnd > 1) {
3776 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. nameEnd]) ~ "</span>";
3777 					code = code[nameEnd .. $];
3778 				} else {
3779 					highlighted ~= ch;
3780 					code = code[1 .. $];
3781 				}
3782 			break;
3783 			default:
3784 				// check for names
3785 				int nameEnd = 0;
3786 				while(nameEnd < code.length && (
3787 					(code[nameEnd] >= 'a' && code[nameEnd] <= 'z') ||
3788 					(code[nameEnd] >= 'A' && code[nameEnd] <= 'Z') ||
3789 					(code[nameEnd] >= '0' && code[nameEnd] <= '0') ||
3790 					code[nameEnd] == '_'
3791 				))
3792 				{
3793 					nameEnd++;
3794 				}
3795 
3796 				if(nameEnd) {
3797 					auto name = code[0 .. nameEnd];
3798 					code = code[nameEnd .. $];
3799 
3800 					if(rubyTypes.canFind(name)) {
3801 						highlighted ~= "<span class=\"highlighted-type\">" ~ name ~ "</span>";
3802 						thisIterWasType = true;
3803 						declarationIndentLevels ~= indentLevel;
3804 					} else if(rubyConstants.canFind(name)) {
3805 						highlighted ~= "<span class=\"highlighted-number\">" ~ name ~ "</span>";
3806 					} else if(rubyPreprocessors.canFind(name)) {
3807 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ name ~ "</span>";
3808 					} else if(rubyKeywords.canFind(name)) {
3809 						if(name == "end") {
3810 							if(declarationIndentLevels.length && indentLevel == declarationIndentLevels[$-1]) {
3811 								// cheating on matching ends with declarations: if indentation matches...
3812 								highlighted ~= "<span class=\"highlighted-type\">" ~ name ~ "</span>";
3813 								declarationIndentLevels = declarationIndentLevels[0 .. $-1];
3814 							} else {
3815 								highlighted ~= "<span class=\"highlighted-keyword\">" ~ name ~ "</span>";
3816 							}
3817 						} else {
3818 							highlighted ~= "<span class=\"highlighted-keyword\">" ~ name ~ "</span>";
3819 						}
3820 					} else {
3821 						if(lastWasType) {
3822 							highlighted ~= "<span class=\"highlighted-identifier\">" ~ name ~ "</span>";
3823 						} else {
3824 							if(name.length && name[0] >= 'A' && name[0] <= 'Z')
3825 								highlighted ~= "<span class=\"highlighted-named-constant\">" ~ name ~ "</span>";
3826 							else
3827 								highlighted ~= name;
3828 						}
3829 					}
3830 				} else {
3831 					highlighted ~= ch;
3832 					code = code[1 .. $];
3833 				}
3834 			break;
3835 		}
3836 		lastChar = ch;
3837 		lastWasType = thisIterWasType;
3838 	}
3839 
3840 	return Html(highlighted);
3841 }
3842 
3843 Html syntaxHighlightPython(string code) {
3844 	/*
3845 		I just want to support:
3846 			numbers
3847 				anything starting with 0, then going alphanumeric
3848 			keywords
3849 				from and import are "preprocessor"
3850 				None, True, and False are number literal colored
3851 			name
3852 				an identifier after the "def" keyword, possibly including : if in there. my vim does it light blue
3853 			comments
3854 				only # .. \n is allowed.
3855 			strings
3856 				single quote must end on the same line
3857 				triple quote span multiple line.
3858 
3859 
3860 			Extracting python definitions:
3861 				class foo:
3862 					# method baz, arg: bar, doc string attached
3863 					def baz(bar):
3864 						""" this is the doc string """
3865 
3866 				will just have to follow indentation (BARF)
3867 
3868 			Note: python has default arguments.
3869 
3870 
3871 			Python implicitly does line continuation if parens, brackets, or braces open
3872 			you can also explicitly extend with \ at the end
3873 			in these cases, the indentation doesn't count.
3874 
3875 			If there's only comments on a line, the indentation is also exempt.
3876 	*/
3877 	static immutable string[] pythonKeywords = [
3878 		"and", "del", "not", "while",
3879 		"as", "elif", "global", "or", "with",
3880 		"assert", "else", "if", "pass", "yield",
3881 		"break", "except", "print",
3882 		"exec", "in", "raise",
3883 		"continue", "finally", "is", "return",
3884 		"for", "lambda", "try",
3885 	];
3886 
3887 	static immutable string[] pythonPreprocessors = [
3888 		"from", "import"
3889 	];
3890 
3891 	static immutable string[] pythonTypes = [
3892 		"class", "def"
3893 	];
3894 
3895 	static immutable string[] pythonConstants = [
3896 		"True", "False", "None"
3897 	];
3898 
3899 	string[] indentStack;
3900 	int openParenCount = 0;
3901 	int openBraceCount = 0;
3902 	int openBracketCount = 0;
3903 	char lastChar;
3904 	bool lastWasType;
3905 
3906 	string highlighted;
3907 
3908 	// ensure there is one and exactly one new line at the end
3909 	// the highlighter uses the final \n as a chance to clean up the
3910 	// indent divs
3911 	code = code.stripRight;
3912 	code ~= "\n";
3913 
3914 	int lineCountSpace = 5; // if python is > 1000 lines it can slightly throw off the indent highlight... but meh.
3915 
3916 	while(code.length) {
3917 		bool thisIterWasType = false;
3918 		auto ch = code[0];
3919 		switch(ch) {
3920 			case '#':
3921 				size_t i;
3922 				while(i < code.length && code[i] != '\n')
3923 					i++;
3924 
3925 				highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3926 				code = code[i .. $];
3927 			break;
3928 			case '(':
3929 				openParenCount++;
3930 				highlighted ~= ch;
3931 				code = code[1 .. $];
3932 			break;
3933 			case '[':
3934 				openBracketCount++;
3935 				highlighted ~= ch;
3936 				code = code[1 .. $];
3937 			break;
3938 			case '{':
3939 				openBraceCount++;
3940 				highlighted ~= ch;
3941 				code = code[1 .. $];
3942 			break;
3943 			case ')':
3944 				openParenCount--;
3945 				highlighted ~= ch;
3946 				code = code[1 .. $];
3947 			break;
3948 			case ']':
3949 				openBracketCount--;
3950 				highlighted ~= ch;
3951 				code = code[1 .. $];
3952 			break;
3953 			case '}':
3954 				openBraceCount--;
3955 				highlighted ~= ch;
3956 				code = code[1 .. $];
3957 			break;
3958 			case '<':
3959 				highlighted ~= "&lt;";
3960 				code = code[1 .. $];
3961 			break;
3962 			case '>':
3963 				highlighted ~= "&gt;";
3964 				code = code[1 .. $];
3965 			break;
3966 			case '&':
3967 				highlighted ~= "&amp;";
3968 				code = code[1 .. $];
3969 			break;
3970 			case '0': .. case '9':
3971 				// number literal
3972 				size_t i;
3973 				while(i < code.length && (
3974 					(code[i] >= '0' && code[i] <= '9')
3975 					||
3976 					(code[i] >= 'a' && code[i] <= 'z')
3977 					||
3978 					(code[i] >= 'A' && code[i] <= 'Z')
3979 					||
3980 					code[i] == '.' || code[i] == '_'
3981 				))
3982 				{
3983 					i++;
3984 				}
3985 
3986 				highlighted ~= "<span class=\"highlighted-number\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
3987 				code = code[i .. $];
3988 			break;
3989 			case '"', '\'':
3990 				// string
3991 				// check for triple-quoted string
3992 				if(ch == '"' && code.length > 2 && code[1] == '"' && code[2] == '"') {
3993 					int end = 3;
3994 					bool escaped;
3995 					while(end + 3 < code.length && !escaped && code[end .. end+ 3] != `"""`) {
3996 						escaped = (code[end] == '\\');
3997 						end++;
3998 					}
3999 
4000 					if(end < code.length)
4001 						end+= 3;
4002 
4003 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. end]) ~ "</span>";
4004 					code = code[end .. $];
4005 				} else {
4006 					// otherwise these are limited to one line, though since we
4007 					// assume the program is well-formed, I'm not going to bother;
4008 					// no need to check for Python errors here, just highlight
4009 					int end = 1;
4010 					bool escaped;
4011 					while(end < code.length && !escaped && code[end] != ch) {
4012 						escaped = (code[end] == '\\');
4013 						end++;
4014 					}
4015 
4016 					if(end < code.length)
4017 						end++;
4018 
4019 					highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(code[0 .. end]) ~ "</span>";
4020 					code = code[end .. $];
4021 				}
4022 			break;
4023 			case '\n':
4024 				string thisIndent = null;
4025 				int thisIndentLength = lineCountSpace;
4026 				// figure out indentation and stuff...
4027 				if(lastChar == '\\' || openParenCount || openBracketCount || openBraceCount) {
4028 					// line continuation, no special processing
4029 				} else {
4030 					if(code.length == 1) {
4031 						// last line in the file, clean up the indentation
4032 						int remove;
4033 						foreach_reverse(i; indentStack) {
4034 							// this isn't actually following the python rule which
4035 							// is more like .startsWith but meh
4036 							if(i.length <= thisIndent.length)
4037 								break;
4038 							highlighted ~= "</div>";
4039 							remove++;
4040 						}
4041 						indentStack = indentStack[0 .. $ - remove];
4042 						// NOT appending the final \n cuz that leads dead space in rendered doc
4043 
4044 						code = code[1 .. $];
4045 						break;
4046 					} else
4047 					foreach(idx, cha; code[1 .. $]) {
4048 						if(cha == '\n')
4049 							break;
4050 						if(cha == '#') // comments exempt from indent processing too
4051 							break;
4052 						if(cha == ' ')
4053 							thisIndentLength++;
4054 						if(cha == '\t')
4055 							thisIndentLength += 8 - (thisIndentLength % 8);
4056 						if(cha != ' ' && cha != '\t') {
4057 							thisIndent = code[1 .. idx + 1];
4058 							break;
4059 						}
4060 					}
4061 				}
4062 
4063 				bool changedDiv = false;
4064 
4065 				if(thisIndent !is null) { // !is null rather than .length is important here. null means skip, "" may need processing
4066 					// close open indents if necessary
4067 					int remove;
4068 					foreach_reverse(i; indentStack) {
4069 						// this isn't actually following the python rule which
4070 						// is more like .startsWith but meh
4071 						if(i.length <= thisIndent.length)
4072 							break;
4073 						highlighted ~= "</div>";
4074 						changedDiv = true;
4075 						remove++;
4076 					}
4077 					indentStack = indentStack[0 .. $ - remove];
4078 				}
4079 
4080 				if(thisIndent.length) { // but we only ever open a new one if there was non-zero indent
4081 					// open a new one if appropriate
4082 					if(indentStack.length == 0 || thisIndent.length > indentStack[$-1].length) {
4083 						changedDiv = true;
4084 						highlighted ~= "<div class=\"highlighted-python-indent\" style=\"background-position: calc("~to!string(thisIndentLength)~"ch - 2px);\">";
4085 						indentStack ~= thisIndent;
4086 					}
4087 				}
4088 
4089 				if(changedDiv)
4090 					highlighted ~= "<span style=\"white-space: normal;\">";
4091 
4092 				highlighted ~= ch;
4093 
4094 				if(changedDiv)
4095 					highlighted ~= "</span>";
4096 
4097 				code = code[1 .. $];
4098 			break;
4099 			case ' ', '\t':
4100 				thisIterWasType = lastWasType; // don't change the last counter on just whitespace
4101 				highlighted ~= ch;
4102 				code = code[1 .. $];
4103 			break;
4104 			default:
4105 				// check for names
4106 				int nameEnd = 0;
4107 				while(nameEnd < code.length && (
4108 					(code[nameEnd] >= 'a' && code[nameEnd] <= 'z') ||
4109 					(code[nameEnd] >= 'A' && code[nameEnd] <= 'Z') ||
4110 					(code[nameEnd] >= '0' && code[nameEnd] <= '0') ||
4111 					code[nameEnd] == '_'
4112 				))
4113 				{
4114 					nameEnd++;
4115 				}
4116 
4117 				if(nameEnd) {
4118 					auto name = code[0 .. nameEnd];
4119 					code = code[nameEnd .. $];
4120 
4121 					if(pythonTypes.canFind(name)) {
4122 						highlighted ~= "<span class=\"highlighted-type\">" ~ name ~ "</span>";
4123 						thisIterWasType = true;
4124 					} else if(pythonConstants.canFind(name)) {
4125 						highlighted ~= "<span class=\"highlighted-number\">" ~ name ~ "</span>";
4126 					} else if(pythonPreprocessors.canFind(name)) {
4127 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ name ~ "</span>";
4128 					} else if(pythonKeywords.canFind(name)) {
4129 						highlighted ~= "<span class=\"highlighted-keyword\">" ~ name ~ "</span>";
4130 					} else {
4131 						if(lastWasType) {
4132 							highlighted ~= "<span class=\"highlighted-identifier\">" ~ name ~ "</span>";
4133 						} else {
4134 							highlighted ~= name;
4135 						}
4136 					}
4137 				} else {
4138 					highlighted ~= ch;
4139 					code = code[1 .. $];
4140 				}
4141 			break;
4142 		}
4143 		lastChar = ch;
4144 		lastWasType = thisIterWasType;
4145 	}
4146 
4147 	return Html(highlighted);
4148 }
4149 
4150 Html syntaxHighlightHtml(string code, string language) {
4151 	string highlighted;
4152 
4153 	bool isWhite(char c) {
4154 		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
4155 	}
4156 
4157 	while(code.length) {
4158 		switch(code[0]) {
4159 			case '<':
4160 				if(code.length > 1 && code[1] == '!') {
4161 					// comment or processing instruction
4162 
4163 					int i = 0;
4164 					while(i < code.length && code[i] != '>') {
4165 						i++;
4166 					}
4167 					if(i < code.length)
4168 						i++;
4169 
4170 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(code[0 .. i]) ~ "</span>";
4171 					code = code[i .. $];
4172 					continue;
4173 				} else if(code.length > 1 && (code[1] == '/' || (code[1] >= 'a' && code[1] <= 'z') || (code[1] >= 'A' && code[1] <= 'Z'))) {
4174 					// possibly a tag
4175 					int i = 1;
4176 					while(i < code.length && code[i] != '>' && code[i] != '<')
4177 						i++;
4178 					if(i == code.length || code[i] == '<')
4179 						goto plain_lt;
4180 
4181 					auto tag = code[0 .. i + 1];
4182 					code = code[i + 1 .. $];
4183 
4184 					highlighted ~= "<span class=\"highlighted-tag\">" ~ highlightHtmlTag(tag) ~ "</span>";
4185 
4186 					if(language == "html" && tag.startsWith("<script")) {
4187 						auto end = code.indexOf("</script>");
4188 						if(end == -1)
4189 							continue;
4190 
4191 						auto sCode = code[0 .. end];
4192 						highlighted ~= syntaxHighlightCFamily(sCode, "javascript").source;
4193 						code = code[end .. $];
4194 					} else if(language == "html" && tag.startsWith("<style")) {
4195 						auto end = code.indexOf("</style>");
4196 						if(end == -1)
4197 							continue;
4198 						auto sCode = code[0 .. end];
4199 						highlighted ~= syntaxHighlightCss(sCode).source;
4200 						code = code[end .. $];
4201 					}
4202 
4203 					continue;
4204 				}
4205 
4206 				plain_lt:
4207 				highlighted ~= "&lt;";
4208 				code = code[1 .. $];
4209 			break;
4210 			case '"':
4211 				highlighted ~= "&quot;";
4212 				code = code[1 .. $];
4213 			break;
4214 			case '>':
4215 				highlighted ~= "&gt;";
4216 				code = code[1 .. $];
4217 			break;
4218 			case '&':
4219 				// possibly an entity
4220 
4221 				int i = 0;
4222 				while(i < code.length && code[i] != ';' && !isWhite(code[i]))
4223 					i++;
4224 				if(i == code.length || isWhite(code[i])) {
4225 					highlighted ~= "&amp;";
4226 					code = code[1 .. $];
4227 				} else {
4228 					i++;
4229 					highlighted ~= htmlEntitiesHighlight(code[0 .. i]);
4230 					code = code[i .. $];
4231 				}
4232 			break;
4233 
4234 			default:
4235 				highlighted ~= code[0];
4236 				code = code[1 .. $];
4237 		}
4238 	}
4239 
4240 	return Html(highlighted);
4241 }
4242 
4243 Html syntaxHighlightSdlang(string jsCode) {
4244 	string highlighted;
4245 
4246 	bool isIdentifierChar(char c) {
4247 		return
4248 			(c >= 'a' && c <= 'z') ||
4249 			(c >= 'A' && c <= 'Z') ||
4250 			(c >= '0' && c <= '9') ||
4251 			(c == '$' || c == '_');
4252 	}
4253 
4254 	bool startOfLine = true;
4255 
4256 	while(jsCode.length) {
4257 		switch(jsCode[0]) {
4258 			case '\'':
4259 			case '\"':
4260 				// string literal
4261 				char start = jsCode[0];
4262 				size_t i = 1;
4263 				while(i < jsCode.length && jsCode[i] != start && jsCode[i] != '\n') {
4264 					if(jsCode[i] == '\\')
4265 						i++; // skip escaped char too
4266 					i++;
4267 				}
4268 
4269 				i++; // skip closer
4270 				highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4271 				jsCode = jsCode[i .. $];
4272 				startOfLine = false;
4273 			break;
4274 			case '-':
4275 				if(jsCode.length > 1 && jsCode[1] == '-') {
4276 					int i = 0;
4277 					while(i < jsCode.length && jsCode[i] != '\n')
4278 						i++;
4279 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4280 					jsCode = jsCode[i .. $];
4281 				} else {
4282 					highlighted ~= '-';
4283 					jsCode = jsCode[1 .. $];
4284 				}
4285 			break;
4286 			case '#':
4287 				int i = 0;
4288 				while(i < jsCode.length && jsCode[i] != '\n')
4289 					i++;
4290 
4291 				highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4292 				jsCode = jsCode[i .. $];
4293 			break;
4294 			case '/':
4295 				if(jsCode.length > 1 && (jsCode[1] == '*' || jsCode[1] == '/')) {
4296 					// it is a comment
4297 					if(jsCode[1] == '/') {
4298 						size_t i;
4299 						while(i < jsCode.length && jsCode[i] != '\n')
4300 							i++;
4301 
4302 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4303 						jsCode = jsCode[i .. $];
4304 					} else {
4305 						// a star comment
4306 						size_t i;
4307 						while((i+1) < jsCode.length && !(jsCode[i] == '*' && jsCode[i+1] == '/'))
4308 							i++;
4309 
4310 						if(i < jsCode.length)
4311 							i+=2; // skip the */
4312 
4313 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4314 						jsCode = jsCode[i .. $];
4315 					}
4316 				} else {
4317 					highlighted ~= '/';
4318 					jsCode = jsCode[1 .. $];
4319 				}
4320 			break;
4321 			case '0': .. case '9':
4322 				// number literal
4323 				size_t i;
4324 				while(i < jsCode.length && (
4325 					(jsCode[i] >= '0' && jsCode[i] <= '9')
4326 					||
4327 					// really fuzzy but this is meant to highlight hex and exponents too
4328 					jsCode[i] == '.' || jsCode[i] == 'e' || jsCode[i] == 'x' ||
4329 					(jsCode[i] >= 'a' && jsCode[i] <= 'f') ||
4330 					(jsCode[i] >= 'A' && jsCode[i] <= 'F')
4331 				))
4332 					i++;
4333 
4334 				highlighted ~= "<span class=\"highlighted-number\">" ~ jsCode[0 .. i] ~ "</span>";
4335 				jsCode = jsCode[i .. $];
4336 				startOfLine = false;
4337 			break;
4338 			case '\t', ' ', '\r':
4339 				highlighted ~= jsCode[0];
4340 				jsCode = jsCode[1 .. $];
4341 			break;
4342 			case '\n':
4343 				startOfLine = true;
4344 				highlighted ~= jsCode[0];
4345 				jsCode = jsCode[1 .. $];
4346 			break;
4347 			case '\\':
4348 				if(jsCode.length > 1 && jsCode[1] == '\n') {
4349 					highlighted ~= jsCode[0 .. 1];
4350 					jsCode = jsCode[2 .. $];
4351 				}
4352 			break;
4353 			case ';':
4354 				startOfLine = true;
4355 			break;
4356 			// escape html chars
4357 			case '<':
4358 				highlighted ~= "&lt;";
4359 				jsCode = jsCode[1 .. $];
4360 				startOfLine = false;
4361 			break;
4362 			case '>':
4363 				highlighted ~= "&gt;";
4364 				jsCode = jsCode[1 .. $];
4365 				startOfLine = false;
4366 			break;
4367 			case '&':
4368 				highlighted ~= "&amp;";
4369 				jsCode = jsCode[1 .. $];
4370 				startOfLine = false;
4371 			break;
4372 			default:
4373 				if(isIdentifierChar(jsCode[0])) {
4374 					size_t i;
4375 					while(i < jsCode.length && isIdentifierChar(jsCode[i]))
4376 						i++;
4377 					auto ident = jsCode[0 .. i];
4378 					jsCode = jsCode[i .. $];
4379 
4380 					if(startOfLine)
4381 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4382 					else if(ident == "true" || ident == "false")
4383 						highlighted ~= "<span class=\"highlighted-number\">" ~ ident ~ "</span>";
4384 					else
4385 						highlighted ~= "<span class=\"highlighted-attribute-name\">" ~ ident ~ "</span>";
4386 				} else {
4387 					highlighted ~= jsCode[0];
4388 					jsCode = jsCode[1 .. $];
4389 				}
4390 
4391 				startOfLine = false;
4392 		}
4393 	}
4394 
4395 	return Html(highlighted);
4396 
4397 }
4398 
4399 Html syntaxHighlightCFamily(string jsCode, string language) {
4400 	string highlighted;
4401 
4402 	bool isJsIdentifierChar(char c) {
4403 		return
4404 			(c >= 'a' && c <= 'z') ||
4405 			(c >= 'A' && c <= 'Z') ||
4406 			(c >= '0' && c <= '9') ||
4407 			(c == '$' || c == '_');
4408 	}
4409 
4410 	while(jsCode.length) {
4411 
4412 
4413 		switch(jsCode[0]) {
4414 			case '\'':
4415 			case '\"':
4416 				// string literal
4417 				char start = jsCode[0];
4418 				size_t i = 1;
4419 				while(i < jsCode.length && jsCode[i] != start && jsCode[i] != '\n') {
4420 					if(jsCode[i] == '\\')
4421 						i++; // skip escaped char too
4422 					i++;
4423 				}
4424 
4425 				i++; // skip closer
4426 				highlighted ~= "<span class=\"highlighted-string\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4427 				jsCode = jsCode[i .. $];
4428 			break;
4429 			case '#':
4430 				// preprocessor directive / PHP # comment
4431 				size_t i;
4432 				while(i < jsCode.length && jsCode[i] != '\n')
4433 					i++;
4434 
4435 				if(language == "php")
4436 					highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4437 				else
4438 					highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4439 				jsCode = jsCode[i .. $];
4440 
4441 			break;
4442 			case '/':
4443 				// check for comment
4444 				// javascript also has that stupid regex literal, but screw it
4445 				if(jsCode.length > 1 && (jsCode[1] == '*' || jsCode[1] == '/')) {
4446 					// it is a comment
4447 					if(jsCode[1] == '/') {
4448 						size_t i;
4449 						while(i < jsCode.length && jsCode[i] != '\n')
4450 							i++;
4451 
4452 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4453 						jsCode = jsCode[i .. $];
4454 					} else {
4455 						// a star comment
4456 						size_t i;
4457 						while((i+1) < jsCode.length && !(jsCode[i] == '*' && jsCode[i+1] == '/'))
4458 							i++;
4459 
4460 						if(i < jsCode.length)
4461 							i+=2; // skip the */
4462 
4463 						highlighted ~= "<span class=\"highlighted-comment\">" ~ htmlEntitiesEncode(jsCode[0 .. i]) ~ "</span>";
4464 						jsCode = jsCode[i .. $];
4465 					}
4466 				} else {
4467 					highlighted ~= '/';
4468 					jsCode = jsCode[1 .. $];
4469 				}
4470 			break;
4471 			case '0': .. case '9':
4472 				// number literal
4473 				// FIXME: negative exponents are not done here
4474 				// nor are negative numbers, but I think that needs parsing anyway
4475 				// it also doesn't do all things right but the assumption is we're highlighting valid code anyway
4476 				size_t i;
4477 				while(i < jsCode.length && (
4478 					(jsCode[i] >= '0' && jsCode[i] <= '9')
4479 					||
4480 					// really fuzzy but this is meant to highlight hex and exponents too
4481 					jsCode[i] == '.' || jsCode[i] == 'e' || jsCode[i] == 'x' ||
4482 					(jsCode[i] >= 'a' && jsCode[i] <= 'f') ||
4483 					(jsCode[i] >= 'A' && jsCode[i] <= 'F')
4484 				))
4485 					i++;
4486 
4487 				highlighted ~= "<span class=\"highlighted-number\">" ~ jsCode[0 .. i] ~ "</span>";
4488 				jsCode = jsCode[i .. $];
4489 			break;
4490 			case '\t', ' ', '\n', '\r':
4491 				highlighted ~= jsCode[0];
4492 				jsCode = jsCode[1 .. $];
4493 			break;
4494 			case '?':
4495 				if(language == "php") {
4496 					if(jsCode.length >= 2 && jsCode[0 .. 2] == "?>") {
4497 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">?&gt;</span>";
4498 						jsCode = jsCode[2 .. $];
4499 						break;
4500 					}
4501 				}
4502 				highlighted ~= jsCode[0];
4503 				jsCode = jsCode[1 .. $];
4504 			break;
4505 			// escape html chars
4506 			case '<':
4507 				if(language == "php") {
4508 					if(jsCode.length > 5 && jsCode[0 .. 5] == "<?php") {
4509 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">&lt;?php</span>";
4510 						jsCode = jsCode[5 .. $];
4511 						break;
4512 					}
4513 				}
4514 				highlighted ~= "&lt;";
4515 				jsCode = jsCode[1 .. $];
4516 			break;
4517 			case '>':
4518 				highlighted ~= "&gt;";
4519 				jsCode = jsCode[1 .. $];
4520 			break;
4521 			case '&':
4522 				highlighted ~= "&amp;";
4523 				jsCode = jsCode[1 .. $];
4524 			break;
4525 			case '@':
4526 				// FIXME highlight UDAs
4527 				//goto case;
4528 			//break;
4529 			default:
4530 				if(isJsIdentifierChar(jsCode[0])) {
4531 					size_t i;
4532 					while(i < jsCode.length && isJsIdentifierChar(jsCode[i]))
4533 						i++;
4534 					auto ident = jsCode[0 .. i];
4535 					jsCode = jsCode[i .. $];
4536 
4537 					if(["function", "for", "in", "while", "new", "if", "else", "switch", "return", "break", "do", "delete", "this", "super", "continue", "goto"].canFind(ident))
4538 						highlighted ~= "<span class=\"highlighted-keyword\">" ~ ident ~ "</span>";
4539 					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))
4540 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4541 					else if(language == "java" && ["extends", "implements"].canFind(ident))
4542 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4543 					// FIXME: do i want to give using this same color?
4544 					else if((language == "c#" || language == "c++") && ["using", "namespace"].canFind(ident))
4545 						highlighted ~= "<span class=\"highlighted-type\">" ~ ident ~ "</span>";
4546 					else if(language == "java" && ["native", "package", "import"].canFind(ident))
4547 						highlighted ~= "<span class=\"highlighted-preprocessor-directive\">" ~ ident ~ "</span>";
4548 					else if(ident[0] == '$')
4549 						highlighted ~= "<span class=\"highlighted-identifier\">" ~ ident ~ "</span>";
4550 					else
4551 						highlighted ~= ident; // "<span>" ~ ident ~ "</span>";
4552 
4553 				} else {
4554 					highlighted ~= jsCode[0];
4555 					jsCode = jsCode[1 .. $];
4556 				}
4557 		}
4558 	}
4559 
4560 	return Html(highlighted);
4561 }