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