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