1 module adrdox.main;
2 
3 // version=with_http_server
4 // version=with_postgres
5 
6 __gshared string dataDirectory;
7 __gshared string skeletonFile = "skeleton.html";
8 __gshared string outputDirectory = "generated-docs";
9 __gshared TexMathOpt texMathOpt = TexMathOpt.LaTeX;
10 
11 __gshared bool writePrivateDocs = false;
12 __gshared bool documentInternal = false;
13 __gshared bool documentUndocumented = false;
14 __gshared bool minimalDescent = false;
15 
16 version(linux)
17 	__gshared bool caseInsensitiveFilenames = false;
18 else
19 	__gshared bool caseInsensitiveFilenames = true;
20 
21 /*
22 
23 Glossary feature: little short links that lead somewhere else.
24 
25 
26 	FIXME: it should be able to handle bom. consider core/thread.d the unittest example is offset.
27 
28 
29 	FIXME:
30 		* make sure there's a link to the source for everything
31 		* search
32 		* package index without needing to build everything at once
33 		* version specifiers
34 		* prettified constraints
35 */
36 
37 import dparse.parser;
38 import dparse.lexer;
39 import dparse.ast;
40 
41 import arsd.dom;
42 import arsd.docgen.comment;
43 
44 version(with_postgres)
45 	import arsd.postgres;
46 else
47 	private alias PostgreSql = typeof(null);
48 
49 import std.algorithm :sort, canFind;
50 
51 import std..string;
52 import std.conv : to;
53 
54 string handleCaseSensitivity(string s) {
55 	if(!caseInsensitiveFilenames)
56 		return s;
57 	string ugh;
58 	foreach(ch; s) {
59 		if(ch >= 'A' && ch <= 'Z')
60 			ugh ~= "_";
61 		ugh ~= ch;
62 	}
63 	return ugh;
64 }
65 
66 bool checkDataDirectory(string stdpath) {
67 	import std.file : exists;
68 	import std.path : buildPath;
69 
70 	string[] stdfiles = ["script.js",
71 											 "style.css",
72 											 "search-docs.js",
73 											 "search-docs.html",
74 											 "skeleton-default.html"];
75 
76 	foreach (stdfile; stdfiles) {
77 		if (!buildPath(stdpath, stdfile).exists) {
78 			return false;
79 		}
80 	}
81 	return true;
82 }
83 
84 bool detectDataDirectory(ref string dataDir) {
85 	import std.file : thisExePath;
86 	import std.path : buildPath, dirName;
87 
88 	string exeDir = thisExePath.dirName;
89 
90 	string[] stdpaths = [exeDir,
91 											 exeDir.dirName,
92 											 buildPath(exeDir.dirName, "share/adrdox")];
93 
94 	foreach (stdpath; stdpaths) {
95 		if (checkDataDirectory(stdpath)) {
96 			dataDir = stdpath;
97 			return true;
98 		}
99 	}
100 	return false;
101 }
102 
103 // returns empty string if file not found
104 string findStandardFile(bool dofail=true) (string stdfname) {
105 	import std.file : exists;
106 	import std.path : buildPath;
107 	if (!stdfname.exists) {
108 		if (stdfname.length && stdfname[0] != '/') {
109 			string newname = buildPath(dataDirectory, stdfname);
110 			if (newname.exists) return newname;
111 		}
112 		static if (dofail) throw new Exception("standard file '" ~stdfname ~ "' not found!");
113 	}
114 	return stdfname;
115 }
116 
117 string outputFilePath(string[] names...) {
118 	import std.path : buildPath;
119 	names = outputDirectory ~ names;
120 	return buildPath(names);
121 }
122 
123 void copyStandardFileTo(bool timecheck=true) (string destname, string stdfname) {
124 	import std.file;
125 	if (exists(destname)) {
126 		static if (timecheck) {
127 			if (timeLastModified(destname) >= timeLastModified(findStandardFile(stdfname))) return;
128 		} else {
129 			return;
130 		}
131 	}
132 	copy(findStandardFile(stdfname), destname);
133 }
134 
135 __gshared static Object directoriesForPackageMonitor = new Object; // intentional CTFE
136 __gshared string[string] directoriesForPackage;
137 string getDirectoryForPackage(string packageName) {
138 
139 	if(packageName.indexOf("/") != -1)
140 		return ""; // not actually a D package!
141 	if(packageName.indexOf("#") != -1)
142 		return ""; // not actually a D package!
143 
144 	string bestMatch = "";
145 	int bestMatchDots = -1;
146 
147 	import std.path;
148 	synchronized(directoriesForPackageMonitor)
149 	foreach(pkg, dir; directoriesForPackage) {
150 		if(globMatch!(CaseSensitive.yes)(packageName, pkg)) {
151 			int cnt;
152 			foreach(ch; pkg)
153 				if(ch == '.')
154 					cnt++;
155 			if(cnt > bestMatchDots) {
156 				bestMatch = dir;
157 				bestMatchDots = cnt;
158 			}
159 		}
160 	}
161 	return bestMatch;
162 }
163 
164 
165 // FIXME: make See Also automatically list dittos that are not overloads
166 
167 enum skip_undocumented = true;
168 
169 static bool sorter(Decl a, Decl b) {
170 	if(a.declarationType == b.declarationType)
171 		return (blogMode && a.declarationType == "Article") ? (b.name < a.name) : (a.name < b.name);
172 	else if(a.declarationType == "module" || b.declarationType == "module") // always put modules up top
173 		return 
174 			(a.declarationType == "module" ? "aaaaaa" : a.declarationType)
175 			< (b.declarationType == "module" ? "aaaaaa" : b.declarationType);
176 	else
177 		return a.declarationType < b.declarationType;
178 }
179 
180 void annotatedPrototype(T)(T decl, MyOutputRange output) {
181 	static if(is(T == TemplateDecl)) {
182 		auto td = cast(TemplateDecl) decl;
183 		auto epony = td.eponymousMember;
184 		if(epony) {
185 			if(auto e = cast(FunctionDecl) epony) {
186 				doFunctionDec(e, output);
187 				return;
188 			}
189 		}
190 	}
191 
192 
193 	auto classDec = decl.astNode;
194 
195 	auto f = new MyFormatter!(typeof(output))(output, decl);
196 
197 	void writePrototype() {
198 		output.putTag("<div class=\"aggregate-prototype\">");
199 
200 		if(decl.parent !is null && !decl.parent.isModule) {
201 			output.putTag("<div class=\"parent-prototype\">");
202 			decl.parent.getSimplifiedPrototype(output);
203 			output.putTag("</div><div>");
204 		}
205 
206 		writeAttributes(f, output, decl.attributes);
207 
208 		output.putTag("<span class=\"builtin-type\">");
209 		output.put(decl.declarationType);
210 		output.putTag("</span>");
211 		output.put(" ");
212 		output.put(decl.name);
213 		output.put(" ");
214 
215 		foreach(idx, ir; decl.inheritsFrom()) {
216 			if(idx == 0)
217 				output.put(" : ");
218 			else
219 				output.put(", ");
220 			if(ir.decl is null)
221 				output.put(ir.plainText);
222 			else
223 				output.putTag(`<a class="xref parent-class" href="`~ir.decl.link~`">`~ir.decl.name~`</a> `);
224 		}
225 
226 		if(classDec.templateParameters)
227 			f.format(classDec.templateParameters);
228 		if(classDec.constraint)
229 			f.format(classDec.constraint);
230 
231 		// FIXME: invariant
232 
233 		if(decl.children.length) {
234 			output.put(" {");
235 			foreach(child; decl.children) {
236 				if((child.isPrivate() && !writePrivateDocs))
237 					continue;
238 				// I want to show undocumented plain data members (if not private)
239 				// since they might be used as public fields or in ctors for simple
240 				// structs, but I'll skip everything else undocumented.
241 				if(!child.isDocumented() && (cast(VariableDecl) child) is null)
242 					continue;
243 				output.putTag("<div class=\"aggregate-member\">");
244 				if(child.isDocumented())
245 					output.putTag("<a href=\""~child.link~"\">");
246 				child.getAggregatePrototype(output);
247 				if(child.isDocumented())
248 					output.putTag("</a>");
249 				output.putTag("</div>");
250 			}
251 			output.put("}");
252 		}
253 
254 		if(decl.parent !is null && !decl.parent.isModule) {
255 			output.putTag("</div>");
256 		}
257 
258 		output.putTag("</div>");
259 	}
260 
261 
262 	writeOverloads!writePrototype(decl, output);
263 }
264 
265 
266 	void doEnumDecl(T)(T decl, Element content)
267 	{
268 		auto enumDec = decl.astNode;
269 		static if(is(typeof(enumDec) == const(AnonymousEnumDeclaration))) {
270 			if(enumDec.members.length == 0) return;
271 			auto name = enumDec.members[0].name.text;
272 			auto type = enumDec.baseType;
273 			auto members = enumDec.members;
274 		} else {
275 			auto name = enumDec.name.text;
276 			auto type = enumDec.type;
277 			const(EnumMember)[] members;
278 			if(enumDec.enumBody)
279 				members = enumDec.enumBody.enumMembers;
280 		}
281 
282 		static if(is(typeof(enumDec) == const(AnonymousEnumDeclaration))) {
283 			// undocumented anonymous enums get a pass if any of
284 			// their members are documented because that's what dmd does...
285 			// FIXME maybe
286 
287 			bool foundOne = false;
288 			foreach(member; members) {
289 				if(member.comment.length) {
290 					foundOne = true;
291 					break;
292 				}
293 			}
294 
295 			if(!foundOne && skip_undocumented)
296 				return;
297 		} else {
298 			if(enumDec.comment.length == 0 && skip_undocumented)
299 				return;
300 		}
301 
302 		/*
303 		auto f = new MyFormatter!(typeof(output))(output);
304 
305 		if(type) {
306 			output.putTag("<div class=\"base-type\">");
307 			f.format(type);
308 			output.putTag("</div>");
309 		}
310 		*/
311 
312 		Element table;
313 
314 		if(members.length) {
315 			content.addChild("h2", "Values").attrs.id = "values";
316 
317 			table = content.addChild("table").addClass("enum-members");
318 			table.appendHtml("<tr><th>Value</th><th>Meaning</th></tr>");
319 		}
320 
321 		foreach(member; members) {
322 			auto memberComment = formatDocumentationComment(preprocessComment(member.comment, decl), decl);
323 			auto tr = table.addChild("tr");
324 			tr.addClass("enum-member");
325 			tr.attrs.id = member.name.text;
326 
327 			auto td = tr.addChild("td");
328 
329 			if(member.isDisabled) {
330 				td.addChild("span", "@disable").addClass("enum-disabled");
331 				td.addChild("br");
332 			}
333 
334 			if(member.type) {
335 				td.addChild("span", toHtml(member.type)).addClass("enum-type");
336 			}
337 
338 			td.addChild("a", member.name.text, "#" ~ member.name.text).addClass("enum-member-name");
339 
340 			if(member.assignExpression) {
341 				td.addChild("span", toHtml(member.assignExpression)).addClass("enum-member-value");
342 			}
343 
344 			auto ea = td.addChild("div", "", "enum-attributes");
345 			foreach(attribute; member.atAttributes) {
346 				ea.addChild("div", toHtml(attribute));
347 			}
348 
349 			// type
350 			// assignExpression
351 			td = tr.addChild("td");
352 			td.innerHTML = memberComment;
353 
354 			if(member.deprecated_) {
355 				auto p = td.prependChild(Element.make("div", Element.make("span", member.deprecated_.stringLiterals.length ? "Deprecated: " : "Deprecated", "deprecated-label"))).addClass("enum-deprecated");
356 				foreach(sl; member.deprecated_.stringLiterals)
357 					p.addChild("span", sl.text[1 .. $-1]);
358 			}
359 
360 			// I might write to parent list later if I can do it all semantically inside the anonymous enum
361 			// since these names are introduced into the parent scope i think it is justified to list them there
362 			// maybe.
363 		}
364 	}
365 
366 
367 	void doFunctionDec(T)(T decl, MyOutputRange output)
368 	{
369 		auto functionDec = decl.astNode;
370 
371 		//if(!decl.isDocumented() && skip_undocumented)
372 			//return;
373 
374 		string[] conceptsFound;
375 
376 		auto f = new MyFormatter!(typeof(output))(output, decl);
377 
378 		/+
379 
380 		auto comment = parseDocumentationComment(dittoSupport(functionDec, functionDec.comment), fullyQualifiedName ~ name);
381 
382 		if(auto ptr = name in overloadChain[$-1]) {
383 			*ptr += 1;
384 			name ~= "." ~ to!string(*ptr);
385 		} else {
386 			overloadChain[$-1][name] = 1;
387 			overloadNodeChain[$-1] = cast() functionDec;
388 		}
389 
390 		const(ASTNode)[] overloadsList;
391 
392 		if(auto overloads = overloadNodeChain[$-1] in additionalModuleInfo.overloadsOf) {
393 			overloadsList = *overloads;
394 		}
395 
396 		if(dittoChain[$-1])
397 		if(auto dittos = dittoChain[$-1] in additionalModuleInfo.dittos) {
398 			auto list = *dittos;
399 			outer: foreach(item; list) {
400 				if(item is functionDec)
401 					continue;
402 
403 				// already listed as a formal overload which means we
404 				// don't need to list it again under see also
405 				foreach(ol; overloadsList)
406 					if(ol is item)
407 						continue outer;
408 
409 				string linkHtml;
410 				linkHtml ~= `<a href="`~htmlEncode(linkMapping[item])~`">` ~ htmlEncode(nameMapping[item]) ~ `</a>`;
411 
412 				comment.see_alsos ~= linkHtml;
413 			}
414 		}
415 
416 		descendInto(name);
417 
418 		output.putTag("<h1><span class=\"entity-name\">" ~ name ~ "</span> "~typeString~"</h1>");
419 
420 		writeBreadcrumbs();
421 
422 		output.putTag("<div class=\"function-declaration\">");
423 
424 		comment.writeSynopsis(output);
425 		+/
426 
427 		void writeFunctionPrototype() {
428 			string outputStr;
429 			auto originalOutput = output;
430 
431 			MyOutputRange output = MyOutputRange(&outputStr);
432 			auto f = new MyFormatter!(typeof(output))(output);
433 
434 			output.putTag("<div class=\"function-prototype\">");
435 
436 			//output.putTag(`<a href="http://dpldocs.info/reading-prototypes" id="help-link">?</a>`);
437 
438 			if(decl.parent !is null && !decl.parent.isModule) {
439 				output.putTag("<div class=\"parent-prototype\">");
440 				decl.parent.getSimplifiedPrototype(output);
441 				output.putTag("</div><div>");
442 			}
443 
444 			writeAttributes(f, output, decl.attributes);
445 
446 			static if(
447 				!is(typeof(functionDec) == const(Constructor)) &&
448 				!is(typeof(functionDec) == const(Postblit)) &&
449 				!is(typeof(functionDec) == const(Destructor))
450 			) {
451 				output.putTag("<div class=\"return-type\">");
452 
453 				if (functionDec.hasAuto && functionDec.hasRef)
454 					output.putTag(`<a class="lang-feature" href="http://dpldocs.info/auto-ref-function-return-prototype">auto ref</a> `);
455 				else {
456 					if (functionDec.hasAuto)
457 						output.putTag(`<a class="lang-feature" href="http://dpldocs.info/auto-function-return-prototype">auto</a> `);
458 					if (functionDec.hasRef)
459 						output.putTag(`<a class="lang-feature" href="http://dpldocs.info/ref-function-return-prototype">ref</a> `);
460 				}
461 
462 				if (functionDec.returnType !is null)
463 					f.format(functionDec.returnType);
464 
465 				output.putTag("</div>");
466 			}
467 
468 			output.putTag("<div class=\"function-name\">");
469 			output.put(decl.name);
470 			output.putTag("</div>");
471 
472 			output.putTag("<div class=\"template-parameters\" data-count=\""~to!string((functionDec.templateParameters && functionDec.templateParameters.templateParameterList) ? functionDec.templateParameters.templateParameterList.items.length : 0)~"\">");
473 			if (functionDec.templateParameters !is null)
474 				f.format(functionDec.templateParameters);
475 			output.putTag("</div>");
476 			output.putTag("<div class=\"runtime-parameters\" data-count=\""~to!string(functionDec.parameters.parameters.length)~"\">");
477 			f.format(functionDec.parameters);
478 			output.putTag("</div>");
479 			if(functionDec.constraint !is null) {
480 				output.putTag("<div class=\"template-constraint\">");
481 				f.format(functionDec.constraint);
482 				output.putTag("</div>");
483 			}
484 			if(functionDec.functionBody !is null) {
485 				// FIXME: list inherited contracts
486 				output.putTag("<div class=\"function-contracts\">");
487 				import dparse.formatter;
488 				auto f2 = new Formatter!(typeof(output))(output);
489 
490 				// I'm skipping statements cuz they ugly. but the shorter expressions aren't too bad
491 				if(functionDec.functionBody.inStatement && functionDec.functionBody.inStatement.expression) {
492 					output.putTag("<div class=\"in-contract\">");
493 					f2.format(functionDec.functionBody.inStatement);
494 					output.putTag("</div>");
495 				}
496 				if(functionDec.functionBody.outStatement) {
497 					output.putTag("<div class=\"out-contract\">");
498 					f2.format(functionDec.functionBody.outStatement);
499 					output.putTag("</div>");
500 				}
501 
502 				output.putTag("</div>");
503 			}
504 			//output.put(" : ");
505 			//output.put(to!string(functionDec.name.line));
506 
507 
508 			if(decl.parent !is null && !decl.parent.isModule) {
509 				output.putTag("</div>");
510 				decl.parent.writeTemplateConstraint(output);
511 			}
512 
513 			output.putTag("</div>");
514 
515 			originalOutput.putTag(linkUpHtml(outputStr, decl));
516 		}
517 
518 
519 		writeOverloads!writeFunctionPrototype(decl, output);
520 	}
521 
522 	void writeOverloads(alias writePrototype, D : Decl)(D decl, ref MyOutputRange output) {
523 		auto overloadsList = decl.getImmediateDocumentedOverloads();
524 
525 		// I'm treating dittos similarly to overloads
526 		if(overloadsList.length == 1) {
527 			overloadsList = decl.getDittos();
528 		} else {
529 			foreach(ditto; decl.getDittos()) {
530 				if(!overloadsList.canFind(ditto))
531 					overloadsList ~= ditto;
532 			}
533 		}
534 		if(overloadsList.length > 1) {
535 			import std.conv;
536 			output.putTag("<ol class=\"overloads\">");
537 			foreach(idx, item; overloadsList) {
538 				assert(item.parent !is null);
539 				string cn;
540 				Decl epony;
541 				if(auto t = cast(TemplateDecl) item)
542 					epony = t.eponymousMember;
543 
544 				if(item !is decl && decl !is epony)
545 					cn = "overload-option";
546 				else
547 					cn = "active-overload-option";
548 
549 				if(item.name != decl.name)
550 					cn ~= " ditto-option";
551 
552 				output.putTag("<li class=\""~cn~"\">");
553 
554 				//if(item is decl)
555 					//writeFunctionPrototype();
556 				//} else {
557 				{
558 					if(item !is decl)
559 						output.putTag(`<a href="`~item.link~`">`);
560 
561 					output.putTag("<span class=\"overload-signature\">");
562 					item.getSimplifiedPrototype(output);
563 					output.putTag("</span>");
564 					if(item !is decl)
565 						output.putTag(`</a>`);
566 				}
567 
568 				if(item is decl || decl is epony)
569 					writePrototype();
570 
571 				output.putTag("</li>");
572 			}
573 			output.putTag("</ol>");
574 		} else {
575 			writePrototype();
576 		}
577 	}
578 
579 
580 	void writeAttributes(F, W)(F formatter, W writer, const(VersionOrAttribute)[] attrs, bool bracket = true)
581 	{
582 
583 		if(bracket) writer.putTag("<div class=\"attributes\">");
584 		IdType protection;
585 
586 		string versions;
587 
588 		const(VersionOrAttribute)[] remainingBuiltIns;
589 		const(VersionOrAttribute)[] remainingCustoms;
590 
591 		foreach(a; attrs) {
592 			if (a.attr && isProtection(a.attr.attribute.type)) {
593 				protection = a.attr.attribute.type;
594 			} else if (auto v = cast(VersionFakeAttribute) a) {
595 				if(versions.length)
596 					versions ~= " && ";
597 				if(v.inverted)
598 					versions ~= "!";
599 				versions ~= v.cond;
600 			} else if(a.attr && a.attr.attribute.type == tok!"auto") {
601 				// skipping auto because it is already handled as the return value
602 			} else if(a.isBuiltIn) {
603 				remainingBuiltIns ~= a;
604 			} else {
605 				remainingCustoms ~= a;
606 			}
607 		}
608 
609 		if(versions.length) {
610 			writer.putTag("<div class=\"versions-container\">");
611 			writer.put("version(");
612 			writer.put(versions);
613 			writer.put(")");
614 			writer.putTag("</div>");
615 		}
616 
617 		switch (protection)
618 		{
619 			case tok!"private": writer.put("private "); break;
620 			case tok!"package": writer.put("package "); break;
621 			case tok!"protected": writer.put("protected "); break;
622 			case tok!"export": writer.put("export "); break;
623 			case tok!"public": // see below
624 			default:
625 				// I'm not printing public so this is commented intentionally
626 				// public is the default state of documents so saying it outright
627 				// is kinda just a waste of time IMO.
628 				//writer.put("public ");
629 			break;
630 		}
631 
632 		void innards(const VersionOrAttribute a) {
633 			if(auto fakeAttr = cast(const MemberFakeAttribute) a) {
634 				formatter.format(fakeAttr.attr);
635 				writer.put(" ");
636 			} else {
637 				formatter.format(a.attr);
638 				writer.put(" ");
639 			}
640 		}
641 
642 		foreach (a; remainingBuiltIns)
643 			innards(a);
644 		foreach (a; remainingCustoms) {
645 			writer.putTag("<div class=\"uda\">");
646 			innards(a);
647 			writer.putTag("</div>");
648 		}
649 
650 		if(bracket) writer.putTag("</div>");
651 	}
652 
653 class VersionOrAttribute {
654 	const(Attribute) attr;
655 	this(const(Attribute) attr) {
656 		this.attr = attr;
657 	}
658 
659 	bool isBuiltIn() const {
660 		if(attr is null) return false;
661 		if(attr.atAttribute is null) return true; // any keyword can be safely assumed...
662 
663 		return phelper(attr.atAttribute);
664 	}
665 
666 	protected bool phelper(const AtAttribute at) const {
667 		string txt = toText(at);
668 
669 		if(txt == "@(nogc)") return true;
670 		if(txt == "@(disable)") return true;
671 		if(txt == "@(live)") return true;
672 		if(txt == "@(property)") return true;
673 
674 		if(txt == "@(safe)") return true;
675 		if(txt == "@(system)") return true;
676 		if(txt == "@(trusted)") return true;
677 
678 		return false;
679 	}
680 
681 	const(VersionOrAttribute) invertedClone() const {
682 		return new VersionOrAttribute(attr);
683 	}
684 }
685 
686 class FakeAttribute : VersionOrAttribute {
687 	this() { super(null); }
688 	abstract string toHTML() const;
689 }
690 
691 class MemberFakeAttribute : FakeAttribute {
692 	const(MemberFunctionAttribute) attr;
693 	this(const(MemberFunctionAttribute) attr) {
694 		this.attr = attr;
695 	}
696 
697 	override string toHTML() const {
698 		return toText(attr);
699 	}
700 
701 	override bool isBuiltIn() const {
702 		if(attr is null) return false;
703 		if(attr.atAttribute is null) return true;
704 		return phelper(attr.atAttribute);
705 	}
706 }
707 
708 class VersionFakeAttribute : FakeAttribute {
709 	string cond;
710 	bool inverted;
711 	this(string condition, bool inverted = false) {
712 		cond = condition;
713 		this.inverted = inverted;
714 	}
715 
716 	override const(VersionFakeAttribute) invertedClone() const {
717 		return new VersionFakeAttribute(cond, !inverted);
718 	}
719 
720 	override bool isBuiltIn() const {
721 		return false;
722 	}
723 
724 	override string toHTML() const {
725 		auto element = Element.make("span");
726 		element.addChild("span", "version", "lang-feature");
727 		element.appendText("(");
728 		if(inverted)
729 			element.appendText("!");
730 		element.addChild("span", cond);
731 		element.appendText(")");
732 		return element.toString;
733 	}
734 }
735 
736 void putSimplfiedReturnValue(MyOutputRange output, const FunctionDeclaration decl) {
737 	if (decl.hasAuto && decl.hasRef)
738 		output.putTag(`<span class="lang-feature">auto ref</span> `);
739 	else {
740 		if (decl.hasAuto)
741 			output.putTag(`<span class="lang-feature">auto</span> `);
742 		if (decl.hasRef)
743 			output.putTag(`<span class="lang-feature">ref</span> `);
744 	}
745 
746 	if (decl.returnType !is null)
747 		output.putTag(toHtml(decl.returnType).source);
748 }
749 
750 void putSimplfiedArgs(T)(MyOutputRange output, const T decl) {
751 	// FIXME: do NOT show default values here
752 	if(decl.parameters) {
753 		output.putTag("(");
754 		foreach(idx, p; decl.parameters.parameters) {
755 			if(idx)
756 				output.putTag(", ");
757 			output.putTag(toText(p.type));
758 			output.putTag(" ");
759 			output.putTag(toText(p.name));
760 		}
761 		if(decl.parameters.hasVarargs) {
762 			if(decl.parameters.parameters.length)
763 				output.putTag(", ");
764 			output.putTag("...");
765 		}
766 		output.putTag(")");
767 	}
768 
769 }
770 
771 string specialPreprocess(string comment, Decl decl) {
772 	switch(specialPreprocessor) {
773 		case "dwt":
774 			// translate Javadoc to adrdox
775 			// @see, @exception/@throws, @param, @return
776 			// @author, @version, @since, @deprecated
777 			// {@link thing}
778 			// one line desc is until the first <p>
779 			// html tags are allowed in javadoc
780 
781 			// links go class#member(args)
782 			// the (args) and class are optional
783 
784 			string parseIdentifier(ref string s, bool allowHash = false) {
785 				int end = 0;
786 				while(end < s.length && (
787 					(s[end] >= 'A' && s[end] <= 'Z') ||
788 					(s[end] >= 'a' && s[end] <= 'z') ||
789 					(s[end] >= '0' && s[end] <= '9') ||
790 					s[end] == '_' ||
791 					s[end] == '.' ||
792 					(allowHash && s[end] == '#')
793 				))
794 				{
795 					end++;
796 				}
797 
798 				auto i = s[0 .. end];
799 				s = s[end .. $];
800 				return i;
801 			}
802 
803 
804 
805 			// javadoc is basically html with @ stuff, so start by parsing that (presumed) tag soup
806 			auto document = new Document("<root>" ~ comment ~ "</root>");
807 
808 			string newComment;
809 
810 			string fixupJavaReference(string r) {
811 				if(r.length == 0)
812 					return r;
813 				if(r[0] == '#')
814 					r = r[1 .. $]; // local refs in adrdox need no special sigil
815 				r = r.replace("#", ".");
816 				auto idx = r.indexOf("(");
817 				if(idx != -1)
818 					r = r[0 .. idx];
819 				return r;
820 			}
821 
822 			void translate(Element element) {
823 				if(element.nodeType == NodeType.Text) {
824 					foreach(line; element.nodeValue.splitLines(KeepTerminator.yes)) {
825 						auto s = line.strip;
826 						if(s.length && s[0] == '@') {
827 							s = s[1 .. $];
828 							auto ident = parseIdentifier(s);
829 							switch(ident) {
830 								case "author":
831 								case "deprecated":
832 								case "version":
833 								case "since":
834 									line = ident ~ ": " ~ s ~ "\n";
835 								break;
836 								case "return":
837 								case "returns":
838 									line = "Returns: " ~ s ~ "\n";
839 								break;
840 								case "exception":
841 								case "throws":
842 									while(s.length && s[0] == ' ')
843 										s = s[1 .. $];
844 									auto p = parseIdentifier(s);
845 
846 									line = "Throws: [" ~ p ~ "]" ~ s ~ "\n";
847 								break;
848 								case "param":
849 									while(s.length && s[0] == ' ')
850 										s = s[1 .. $];
851 									auto p = parseIdentifier(s);
852 									line  = "Params:\n" ~ p ~ " = " ~ s ~ "\n";
853 								break;
854 								case "see":
855 									while(s.length && s[0] == ' ')
856 										s = s[1 .. $];
857 									auto p = parseIdentifier(s, true);
858 									if(p.length)
859 									line = "See_Also: [" ~ fixupJavaReference(p) ~ "]" ~ "\n";
860 									else
861 									line = "See_Also: " ~ s ~ "\n";
862 								break;
863 								default:
864 									// idk, leave it alone.
865 							}
866 
867 						}
868 
869 						newComment ~= line;
870 					}
871 				} else {
872 					if(element.tagName == "code") {
873 						newComment ~= "`";
874 						// FIXME: what about ` inside code?
875 						newComment ~= element.innerText; // .replace("`", "``");
876 						newComment ~= "`";
877 					} else if(element.tagName == "p") {
878 						newComment ~= "\n\n";
879 						foreach(child; element.children)
880 							translate(child);
881 						newComment ~= "\n\n";
882 					} else if(element.tagName == "a") {
883 						newComment ~= "${LINK2 " ~ element.href ~ ", " ~ element.innerText ~ "}";
884 					} else {
885 						newComment ~= "${" ~ element.tagName.toUpper ~ " ";
886 						foreach(child; element.children)
887 							translate(child);
888 						newComment ~= "}";
889 					}
890 				}
891 			}
892 
893 			foreach(child; document.root.children)
894 				translate(child);
895 
896 			comment = newComment;
897 		break;
898 		case "gtk":
899 			// translate gtk syntax and names to our own
900 
901 			string gtkObjectToDClass(string name) {
902 				if(name.length == 0)
903 					return null;
904 				int pkgEnd = 1;
905 				while(pkgEnd < name.length && !(name[pkgEnd] >= 'A'  && name[pkgEnd] <= 'Z'))
906 					pkgEnd++;
907 
908 				auto pkg = name[0 .. pkgEnd].toLower;
909 				auto mod = name[pkgEnd .. $];
910 
911 				auto t = pkg ~ "." ~ mod;
912 
913 				if(t in modulesByName)
914 					return t ~ "." ~ mod;
915 				if(auto c = mod in allClasses)
916 					return c.fullyQualifiedName;
917 
918 				return null;
919 			}
920 
921 			string trimFirstThing(string name) {
922 				if(name.length == 0)
923 					return null;
924 				int pkgEnd = 1;
925 				while(pkgEnd < name.length && !(name[pkgEnd] >= 'A'  && name[pkgEnd] <= 'Z'))
926 					pkgEnd++;
927 				return name[pkgEnd .. $];
928 			}
929 
930 			string formatForDisplay(string name) {
931 				auto parts = name.split(".");
932 				// gtk.Application.Application.member
933 				// we want to take out the repeated one - slot [1]
934 				string disp;
935 				if(parts.length > 2)
936 					foreach(idx, part; parts) {
937 						if(idx == 1) continue;
938 						if(idx) {
939 							disp ~= ".";
940 						}
941 						disp ~= part;
942 					}
943 				else
944 					disp = name;
945 				return disp;
946 			}
947 
948 			import std.regex : regex, replaceAll, Captures;
949 			// gtk references to adrdox reference; punt it to the search engine
950 			string magic(Captures!string m) {
951 				string s = m.hit;
952 				s = s[1 .. $]; // trim off #
953 				auto orig = s;
954 				auto name = s;
955 
956 				string displayOverride;
957 
958 				string additional;
959 
960 				auto idx = s.indexOf(":");
961 				if(idx != -1) {
962 					// colon means it is an attribute or a signal
963 					auto property = s[idx + 1 .. $];
964 					s = s[0 .. idx];
965 					if(property.length && property[0] == ':') {
966 						// is a signal
967 						property = property[1 .. $];
968 						additional = ".addOn";
969 						displayOverride = property;
970 					} else {
971 						// is a property
972 						additional = ".get";
973 						displayOverride = property;
974 					}
975 					bool justSawDash = true;
976 					foreach(ch; property) {
977 						if(justSawDash && ch >= 'a' && ch <= 'z') {
978 							additional ~= cast(char) (cast(int) ch - 32);
979 						} else if(ch == '-') {
980 							// intentionally blank
981 						} else {
982 							additional ~= ch;
983 						}
984 
985 						if(ch == '-') {
986 							justSawDash = true;
987 						} else {
988 							justSawDash = false;
989 						}
990 					}
991 				} else {
992 					idx = s.indexOf(".");
993 					if(idx != -1) {
994 						// dot is either a tailing period or a Struct.field
995 						if(idx == s.length - 1)
996 							s = s[0 .. $ - 1]; // tailing period
997 						else {
998 							auto structField = s[idx + 1 .. $];
999 							s = s[0 .. idx];
1000 
1001 							additional = "." ~ structField; // FIXME?
1002 						}
1003 					}
1004 				}
1005 
1006 				auto dClass = gtkObjectToDClass(s);
1007 				bool plural = false;
1008 				if(dClass is null && s.length && s[$-1] == 's') {
1009 					s = s[0 .. $-1];
1010 					dClass = gtkObjectToDClass(s);
1011 					plural = true;
1012 				}
1013 
1014 				if(dClass !is null)
1015 					s = dClass;
1016 
1017 				s ~= additional;
1018 
1019 				if(displayOverride.length)
1020 					return "[" ~ s ~ "|"~displayOverride~"]";
1021 				else
1022 					return "[" ~ s ~ "|"~formatForDisplay(s)~(plural ? "s" : "") ~ "]";
1023 			}
1024 
1025 			// gtk function to adrdox d ref
1026 			string magic2(Captures!string m) {
1027 				if(m.hit == "main()")
1028 					return "`"~m.hit~"`"; // special case
1029 				string s = m.hit[0 .. $-2]; // trim off the ()
1030 				auto orig = m.hit;
1031 				// these tend to go package_class_method_snake
1032 				string gtkType;
1033 				gtkType ~= s[0] | 32;
1034 				s = s[1 .. $];
1035 				bool justSawUnderscore = false;
1036 				string dType;
1037 				bool firstUnderscore = true;
1038 				while(s.length) {
1039 					if(s[0] == '_') {
1040 						justSawUnderscore = true;
1041 						if(!firstUnderscore) {
1042 							auto dc = gtkObjectToDClass(gtkType);
1043 							if(dc !is null) {
1044 								dType = dc;
1045 								s = s[1 .. $];
1046 								break;
1047 							}
1048 						}
1049 						firstUnderscore = false;
1050 					} else if(justSawUnderscore) {
1051 						gtkType ~= s[0] & ~32;
1052 						justSawUnderscore = false;
1053 					} else
1054 						gtkType ~= s[0];
1055 
1056 					s = s[1 .. $];
1057 				}
1058 
1059 				if(dType.length) {
1060 					justSawUnderscore = false;
1061 					string gtkMethod = "";
1062 					while(s.length) {
1063 						if(s[0] == '_') {
1064 							justSawUnderscore = true;
1065 						} else if(justSawUnderscore) {
1066 							gtkMethod ~= s[0] & ~32;
1067 							justSawUnderscore = false;
1068 						} else
1069 							gtkMethod ~= s[0];
1070 
1071 						s = s[1 .. $];
1072 					}
1073 
1074 					auto dispName = dType[dType.lastIndexOf(".") + 1 .. $] ~ "." ~ gtkMethod;
1075 
1076 					return "[" ~ dType ~ "." ~ gtkMethod ~ "|" ~ dispName ~ "]";
1077 				}
1078 
1079 				return "`" ~ orig ~ "`";
1080 			}
1081 
1082 			// cut off spam at the end of headers
1083 			comment = replaceAll(comment, regex(r"(## [A-Za-z0-9 ]+)##.*$", "gm"), "$1");
1084 
1085 			// translate see also header into ddoc style as a special case
1086 			comment = replaceAll(comment, regex(r"## See Also.*$", "gm"), "See_Also:\n");
1087 
1088 			// name lookup
1089 			comment = replaceAll!magic(comment, regex(r"#[A-Za-z0-9_:\-\.]+", "g"));
1090 			// gtk params to inline code
1091 			comment = replaceAll(comment, regex(r"@([A-Za-z0-9_:]+)", "g"), "`$1`");
1092 			// constants too
1093 			comment = replaceAll(comment, regex(r"%([A-Za-z0-9_:]+)", "g"), "`$1`");
1094 			// and functions
1095 			comment = replaceAll!magic2(comment, regex(r"([A-Za-z0-9_]+)\(\)", "g"));
1096 
1097 			// their code blocks
1098 			comment = replace(comment, `|[<!-- language="C" -->`, "```c\n");
1099 			comment = replace(comment, `]|`, "\n```");
1100 		break;
1101 		default:
1102 			return comment;
1103 	}
1104 
1105 	return comment;
1106 }
1107 
1108 struct HeaderLink {
1109 	string text;
1110 	string url;
1111 }
1112 
1113 string[string] pseudoFiles;
1114 bool usePseudoFiles = false;
1115 
1116 Document writeHtml(Decl decl, bool forReal, bool gzip, string headerTitle, HeaderLink[] headerLinks, bool overrideOutput = false) {
1117 	if(!decl.docsShouldBeOutputted && !overrideOutput)
1118 		return null;
1119 
1120 	if(cast(ImportDecl) decl)
1121 		return null; // never write imports, it can overwrite the actual thing
1122 
1123 	auto title = decl.name;
1124 	bool justDocs = false;
1125 	if(auto mod = cast(ModuleDecl) decl) {
1126 		if(mod.justDocsTitle !is null) {
1127 			title = mod.justDocsTitle;
1128 			justDocs = true;
1129 		}
1130 	}
1131 
1132 	if(decl.parent !is null && !decl.parent.isModule) {
1133 		title = decl.parent.name ~ "." ~ title;
1134 	}
1135 
1136 	auto document = new Document();
1137 	import std.file;
1138 	document.parseUtf8(readText(findStandardFile(skeletonFile)), true, true);
1139 
1140 	switch (texMathOpt) with (TexMathOpt) {
1141 		case KaTeX: {
1142 			import adrdox.jstex;
1143 			prepareForKaTeX(document);
1144 			break;
1145 		}
1146 		default: break;
1147 	}
1148 
1149 	document.title = title ~ " (" ~ decl.fullyQualifiedName ~ ")";
1150 
1151 	if(headerTitle.length)
1152 		document.requireSelector("#logotype span").innerText = headerTitle;
1153 	if(headerLinks.length) {
1154 		auto n = document.requireSelector("#page-header nav");
1155 		foreach(l; headerLinks)
1156 			if(l.text.length && l.url.length)
1157 				n.addChild("a", l.text, l.url);
1158 	}
1159 
1160 	auto content = document.requireElementById("page-content");
1161 
1162 	auto comment = decl.parsedDocComment;
1163 
1164 	content.addChild("h1", title);
1165 
1166 	auto breadcrumbs = content.addChild("div").addClass("breadcrumbs");
1167 
1168 	//breadcrumbs.prependChild(Element.make("a", decl.name, decl.link).addClass("current breadcrumb"));
1169 
1170 	{
1171 		auto p = decl.parent;
1172 		while(p) {
1173 			if(p.fakeDecl && p.name == "index")
1174 				break;
1175 			// cut off package names that would be repeated
1176 			auto name = (p.isModule && p.parent) ? lastDotOnly(p.name) : p.name;
1177 			breadcrumbs.prependChild(new TextNode(" "));
1178 			breadcrumbs.prependChild(Element.make("a", name, p.link(true)).addClass("breadcrumb"));
1179 			p = p.parent;
1180 		}
1181 	}
1182 
1183 	if(blogMode && decl.isArticle) {
1184 		// FIXME: kinda a hack there
1185 		auto mod = cast(ModuleDecl) decl;
1186 		if(mod.name.startsWith("Blog.Posted_"))
1187 			content.addChild("span", decl.name.replace("Blog.Posted_", "Posted ").replace("_", "-")).addClass("date-posted");
1188 	}
1189 
1190 	string s;
1191 	MyOutputRange output = MyOutputRange(&s);
1192 
1193 	comment.writeSynopsis(output);
1194 	content.addChild("div", Html(s));
1195 
1196 	s = null;
1197 	decl.getAnnotatedPrototype(output);
1198 	//import std.stdio; writeln(s);
1199 	//content.addChild("div", Html(linkUpHtml(s, decl)), "annotated-prototype");
1200 	content.addChild("div", Html(s), "annotated-prototype");
1201 
1202 	Element lastDt;
1203 	string dittoedName;
1204 	string dittoedComment;
1205 
1206 	void handleChildDecl(Element dl, Decl child, bool enableLinking = true) {
1207 		auto cc = child.parsedDocComment;
1208 		string sp;
1209 		MyOutputRange or = MyOutputRange(&sp);
1210 		child.getSimplifiedPrototype(or);
1211 
1212 		auto printableName = child.name;
1213 
1214 		if(child.isArticle) {
1215 			auto mod = cast(ModuleDecl) child;
1216 			printableName = mod.justDocsTitle;
1217 		} else {
1218 			if(child.isModule && child.parent && child.parent.isModule) {
1219 				if(printableName.startsWith(child.parent.name))
1220 					printableName = printableName[child.parent.name.length + 1 .. $];
1221 			}
1222 		}
1223 
1224 		auto newDt = Element.make("dt", Element.make("a", printableName, child.link));
1225 		auto st = newDt.addChild("div", Html(sp)).addClass("simplified-prototype");
1226 		st.style.maxWidth = to!string(st.innerText.length * 11 / 10) ~ "ch";
1227 
1228 		if(child.isDitto && child.comment == dittoedComment && lastDt !is null) {
1229 			// ditto'd names don't need to be written again
1230 			if(child.name == dittoedName) {
1231 				foreach(ldt; lastDt.parentNode.querySelectorAll("dt .simplified-prototype")) {
1232 					if(st.innerHTML == ldt.innerHTML)
1233 						return; // no need to say the same thing twice
1234 				}
1235 				// same name, but different prototype. Cut the repetition.
1236 				newDt.requireSelector("a").removeFromTree();
1237 			}
1238 			lastDt.addSibling(newDt);
1239 		} else {
1240 			dl.addChild(newDt);
1241 			auto dd = dl.addChild("dd", Html(formatDocumentationComment(enableLinking ? cc.ddocSummary : preprocessComment(child.comment, child), child)));
1242 			foreach(e; dd.querySelectorAll("h1, h2, h3, h4, h5, h6"))
1243 				e.stripOut;
1244 			dittoedComment = child.comment;
1245 		}
1246 
1247 		lastDt = newDt;
1248 		dittoedName = child.name;
1249 	}
1250 
1251 	Decl[] ctors;
1252 	Decl[] members;
1253 	ModuleDecl[] articles;
1254 	Decl[] submodules;
1255 	ImportDecl[] imports;
1256 
1257 	if(forReal)
1258 	foreach(child; decl.children) {
1259 		if(!child.docsShouldBeOutputted)
1260 			continue;
1261 		if(child.isConstructor())
1262 			ctors ~= child;
1263 		else if(child.isArticle)
1264 			articles ~= cast(ModuleDecl) child;
1265 		else if(child.isModule)
1266 			submodules ~= child;
1267 		else if(cast(DestructorDecl) child)
1268 			 {} // intentionally blank
1269 		else if(cast(PostblitDecl) child)
1270 			 {} // intentionally blank
1271 		else if(auto c = cast(ImportDecl) child)
1272 			imports ~= c;
1273 		else
1274 			members ~= child;
1275 	}
1276 
1277 	if(decl.disabledDefaultConstructor) {
1278 		content.addChild("h2", "Disabled Default Constructor").id = "disabled-default-constructor";
1279 		auto div = content.addChild("div");
1280 		div.addChild("p", "A disabled default is present on this object. To use it, use one of the other constructors or a factory function.");
1281 	}
1282 
1283 	if(ctors.length) {
1284 		content.addChild("h2", "Constructors").id = "constructors";
1285 		auto dl = content.addChild("dl").addClass("member-list constructors");
1286 
1287 		foreach(child; ctors) {
1288 			if(child is decl.disabledDefaultConstructor)
1289 				continue;
1290 			handleChildDecl(dl, child);
1291 			if(!minimalDescent)
1292 				writeHtml(child, forReal, gzip, headerTitle, headerLinks);
1293 		}
1294 	}
1295 
1296 	if(auto dtor = decl.destructor) {
1297 		content.addChild("h2", "Destructor").id = "destructor";
1298 		auto dl = content.addChild("dl").addClass("member-list");
1299 
1300 		if(dtor.isDocumented)
1301 			handleChildDecl(dl, dtor);
1302 		else
1303 			content.addChild("p", "A destructor is present on this object, but not explicitly documented in the source.");
1304 		//if(!minimalDescent)
1305 			//writeHtml(dtor, forReal, gzip, headerTitle, headerLinks);
1306 	}
1307 
1308 	if(auto postblit = decl.postblit) {
1309 		content.addChild("h2", "Postblit").id = "postblit";
1310 		auto dl = content.addChild("dl").addClass("member-list");
1311 
1312 		if(postblit.isDisabled())
1313 			content.addChild("p", "Copying this object is disabled.");
1314 
1315 		if(postblit.isDocumented)
1316 			handleChildDecl(dl, postblit);
1317 		else
1318 			content.addChild("p", "A postblit is present on this object, but not explicitly documented in the source.");
1319 		//if(!minimalDescent)
1320 			//writeHtml(dtor, forReal, gzip, headerTitle, headerLinks);
1321 	}
1322 
1323 	if(articles.length) {
1324 		content.addChild("h2", "Articles").id = "articles";
1325 		auto dl = content.addChild("dl").addClass("member-list articles");
1326 		foreach(child; articles.sort!((a,b) => (blogMode ? (b.name < a.name) : (a.name < b.name)))) {
1327 			handleChildDecl(dl, child);
1328 		}
1329 	}
1330 
1331 	if(submodules.length) {
1332 		content.addChild("h2", "Modules").id = "modules";
1333 		auto dl = content.addChild("dl").addClass("member-list native");
1334 		foreach(child; submodules.sort!((a,b) => a.name < b.name)) {
1335 			handleChildDecl(dl, child);
1336 
1337 			// i actually want submodules to be controlled on the command line too.
1338 			//if(!usePseudoFiles) // with pseudofiles, we can generate child modules on demand too, so avoid recursive everything on root request
1339 				//writeHtml(child, forReal, gzip, headerTitle, headerLinks);
1340 		}
1341 	}
1342 
1343 	if(auto at = decl.aliasThis) {
1344 		content.addChild("h2", "Alias This").id = "alias-this";
1345 		auto div = content.addChild("div");
1346 
1347 		div.addChild("a", at.name, at.link);
1348 
1349 		if(decl.aliasThisComment.length) {
1350 			auto memberComment = formatDocumentationComment(preprocessComment(decl.aliasThisComment, decl), decl);
1351 			auto dc = div.addChild("div").addClass("documentation-comment");
1352 			dc.innerHTML = memberComment;
1353 		}
1354 	}
1355 
1356 	if(imports.length) {
1357 		content.addChild("h2", "Public Imports").id = "public-imports";
1358 		auto div = content.addChild("div");
1359 
1360 		foreach(imp; imports) {
1361 			auto dl = content.addChild("dl").addClass("member-list native");
1362 			handleChildDecl(dl, imp, false);
1363 		}
1364 	}
1365 
1366 	if(members.length) {
1367 		content.addChild("h2", "Members").id = "members";
1368 
1369 		void outputMemberList(Decl[] members, string header, string idPrefix, string headerPrefix) {
1370 			Element dl;
1371 			string lastType;
1372 			foreach(child; members.sort!sorter) {
1373 				if(child.declarationType != lastType) {
1374 					auto hdr = content.addChild(header, headerPrefix ~ pluralize(child.declarationType).capitalize, "member-list-header hide-from-toc");
1375 					hdr.id = idPrefix ~ child.declarationType;
1376 					dl = content.addChild("dl").addClass("member-list native");
1377 					lastType = child.declarationType;
1378 				}
1379 
1380 				handleChildDecl(dl, child);
1381 
1382 				if(!minimalDescent)
1383 					writeHtml(child, forReal, gzip, headerTitle, headerLinks);
1384 			}
1385 		}
1386 
1387 		foreach(section; comment.symbolGroupsOrder) {
1388 			auto memberComment = formatDocumentationComment(preprocessComment(comment.symbolGroups[section], decl), decl);
1389 			string sectionPrintable = section.replace("_", " ").capitalize;
1390 			// these count as user headers to move toward TOC - section groups are user defined so it makes sense
1391 			auto hdr = content.addChild("h3", sectionPrintable, "member-list-header user-header");
1392 			hdr.id = "group-" ~ section;
1393 			auto dc = content.addChild("div").addClass("documentation-comment");
1394 			dc.innerHTML = memberComment;
1395 
1396 			if(auto hdr2 = dc.querySelector("> div:only-child > h2:first-child, > div:only-child > h3:first-child")) {
1397 				hdr.innerHTML = hdr2.innerHTML;
1398 				hdr2.removeFromTree;
1399 			}
1400 
1401 			Decl[] subList;
1402 			for(int i = 0; i < members.length; i++) {
1403 				auto member = members[i];
1404 				if(member.parsedDocComment.group == section) {
1405 					subList ~= member;
1406 					members[i] = members[$-1];
1407 					members = members[0 .. $-1];
1408 					i--;
1409 				}
1410 			}
1411 
1412 			outputMemberList(subList, "h4", section ~ "-", sectionPrintable ~ " ");
1413 		}
1414 
1415 		if(members.length) {
1416 			if(comment.symbolGroupsOrder.length) {
1417 				auto hdr = content.addChild("h3", "Other", "member-list-header");
1418 				hdr.id = "group-other";
1419 				outputMemberList(members, "h4", "other-", "Other ");
1420 			} else {
1421 				outputMemberList(members, "h3", "", "");
1422 			}
1423 		}
1424 	}
1425 
1426 	bool firstMitd = true;
1427 	foreach(d; decl.children) {
1428 		if(auto mi = cast(MixedInTemplateDecl) d) {
1429 
1430 			auto thing = decl.lookupName(toText(mi.astNode.mixinTemplateName));
1431 
1432 			if(thing) {
1433 				Element dl;
1434 				foreach(child; thing.children) {
1435 					if(mi.isPrivate && !child.isExplicitlyNonPrivate)
1436 						continue;
1437 					if(child.docsShouldBeOutputted) {
1438 						if(dl is null) {
1439 							if(firstMitd) {
1440 								auto h2 = content.addChild("h2", "Mixed In Members");
1441 								h2.id = "mixed-in-members";
1442 								firstMitd = false;
1443 							}
1444 
1445 							//mi.name
1446 
1447 							string sp;
1448 							MyOutputRange or = MyOutputRange(&sp);
1449 							mi.getSimplifiedPrototype(or);
1450 							auto h3 = content.addChild("h3", Html("From " ~ sp));
1451 
1452 							dl = content.addChild("dl").addClass("member-list native");
1453 						}
1454 						handleChildDecl(dl, child);
1455 
1456 						if(!minimalDescent)
1457 							writeHtml(child, forReal, gzip, headerTitle, headerLinks, true);
1458 					}
1459 				}
1460 			} else {
1461 
1462 			}
1463 		}
1464 	}
1465 
1466 	auto irList = decl.inheritsFrom;
1467 	if(irList.length) {
1468 		auto h2 = content.addChild("h2", "Inherited Members");
1469 		h2.id = "inherited-members";
1470 
1471 		bool hasAnyListing = false;
1472 
1473 		foreach(ir; irList) {
1474 			if(ir.decl is null) continue;
1475 			auto h3 = content.addChild("h3", "From " ~ ir.decl.name);
1476 			h3.id = "inherited-from-" ~ ir.decl.fullyQualifiedName;
1477 			auto dl = content.addChild("dl").addClass("member-list inherited");
1478 			bool hadListing = false;
1479 			foreach(child; ir.decl.children) {
1480 				if(!child.docsShouldBeOutputted)
1481 					continue;
1482 				if(!child.isConstructor()) {
1483 					handleChildDecl(dl, child);
1484 					hadListing = true;
1485 					hasAnyListing = true;
1486 				}
1487 			}
1488 
1489 			if(!hadListing) {
1490 				h3.removeFromTree();
1491 				dl.removeFromTree();
1492 			}
1493 		}
1494 
1495 		if(!hasAnyListing)
1496 			h2.removeFromTree();
1497 	}
1498 
1499 
1500 	decl.addSupplementalData(content);
1501 
1502 	s = null;
1503 
1504 	if(auto fd = cast(FunctionDeclaration) decl.getAstNode())
1505 		comment.writeDetails(output, fd, decl.getProcessedUnittests());
1506 	else if(auto fd = cast(Constructor) decl.getAstNode())
1507 		comment.writeDetails(output, fd, decl.getProcessedUnittests());
1508 	else if(auto fd = cast(TemplateDeclaration) decl.getAstNode())
1509 		comment.writeDetails(output, fd, decl.getProcessedUnittests());
1510 	else if(auto fd = cast(EponymousTemplateDeclaration) decl.getAstNode())
1511 		comment.writeDetails(output, fd, decl.getProcessedUnittests());
1512 	else if(auto fd = cast(AliasDecl) decl) {
1513 		if(fd.initializer)
1514 			comment.writeDetails(output, fd.initializer, decl.getProcessedUnittests());
1515 		else
1516 			comment.writeDetails(output, decl, decl.getProcessedUnittests());
1517 	} else
1518 		comment.writeDetails(output, decl, decl.getProcessedUnittests());
1519 
1520 	content.addChild("div", Html(s));
1521 
1522 	if(forReal) {
1523 		auto nav = document.requireElementById("page-nav");
1524 
1525 		Decl[] navArray;
1526 		string[string] inNavArray;
1527 		if(decl.parent) {
1528 			auto iterate = decl.parent.children;
1529 
1530 			if(!decl.isModule && decl.parent.isModule && decl.parent.children.length == 1) {
1531 				// we are an only child of a module, show the module's nav instead
1532 				if(decl.parent.parent !is null)
1533 					iterate = decl.parent.parent.children;
1534 			}
1535 
1536 			foreach(child; iterate) {
1537 				if(cast(ImportDecl) child) continue; // do not document public imports here, they belong only on the inside
1538 				if(child.docsShouldBeOutputted) {
1539 					// strip overloads from sidebar
1540 					if(child.name !in inNavArray) {
1541 						navArray ~= child;
1542 						inNavArray[child.name] = "";
1543 					}
1544 				}
1545 			}
1546 		} else {
1547 		/+ commented pending removal
1548 			// this is for building the module nav when doing an incremental
1549 			// rebuild. It loads the index.xml made with the special option below.
1550 			static bool attemptedXmlLoad;
1551 			static ModuleDecl[] indexedModules;
1552 			if(!attemptedXmlLoad) {
1553 				import std.file;
1554 				if(std.file.exists("index.xml")) {
1555 					auto idx = new XmlDocument(readText("index.xml"));
1556 					foreach(d; idx.querySelectorAll("listing > decl"))
1557 						indexedModules ~= new ModuleDecl(d.requireSelector("name").innerText);
1558 				}
1559 				attemptedXmlLoad = true;
1560 			}
1561 
1562 			auto tm = cast(ModuleDecl) decl;
1563 			if(tm !is null)
1564 			foreach(im; indexedModules)
1565 				if(im.packageName == tm.packageName)
1566 					navArray ~= im;
1567 		+/
1568 		}
1569 
1570 		{
1571 			auto p = decl.parent;
1572 			while(p) {
1573 				// cut off package names that would be repeated
1574 				auto name = (p.isModule && p.parent) ? lastDotOnly(p.name) : p.name;
1575 				if(name == "index" && p.fakeDecl)
1576 					break;
1577 				nav.prependChild(new TextNode(" "));
1578 				nav.prependChild(Element.make("a", name, p.link(true))).addClass("parent");
1579 				p = p.parent;
1580 			}
1581 		}
1582 
1583 		import std.algorithm;
1584 
1585 		sort!sorter(navArray);
1586 
1587 		Element list;
1588 
1589 		string lastType;
1590 		foreach(item; navArray) {
1591 			if(item.declarationType != lastType) {
1592 				nav.addChild("span", pluralize(item.declarationType)).addClass("type-separator");
1593 				list = nav.addChild("ul");
1594 				lastType = item.declarationType;
1595 			}
1596 
1597 			string name;
1598 			if(item.isArticle) {
1599 				auto mod = cast(ModuleDecl) item;
1600 				name = mod.justDocsTitle;
1601 			} else {
1602 				// cut off package names that would be repeated
1603 				name = (item.isModule && item.parent) ? lastDotOnly(item.name) : item.name;
1604 			}
1605 			auto n = list.addChild("li").addChild("a", name, item.link).addClass(item.declarationType.replace(" ", "-"));
1606 			if(item.name == decl.name || name == decl.name)
1607 				n.addClass("current");
1608 		}
1609 
1610 		if(justDocs) {
1611 			if(auto d = document.querySelector("#details"))
1612 				d.removeFromTree;
1613 		}
1614 
1615 		auto toc = Element.make("div");
1616 		toc.id = "table-of-contents";
1617 		auto current = toc;
1618 		int lastLevel;
1619 		tree: foreach(header; document.root.tree) {
1620 			int level;
1621 			switch(header.tagName) {
1622 				case "h2":
1623 					level = 2;
1624 				break;
1625 				case "h3":
1626 					level = 3;
1627 				break;
1628 				case "h4":
1629 					level = 4;
1630 				break;
1631 				case "h5:":
1632 					level = 5;
1633 				break;
1634 				case "h6":
1635 					level = 6;
1636 				break;
1637 				default: break;
1638 			}
1639 
1640 			if(level == 0) continue;
1641 
1642 			bool addToIt = true;
1643 			if(header.hasClass("hide-from-toc"))
1644 				addToIt = false;
1645 
1646 			Element addTo;
1647 			if(addToIt) {
1648 				auto parentCheck = header;
1649 				while(parentCheck) {
1650 					if(parentCheck.hasClass("adrdox-sample"))
1651 						continue tree;
1652 					parentCheck = parentCheck.parentNode;
1653 				}
1654 
1655 				if(level > lastLevel) {
1656 					current = current.addChild("ol");
1657 					current.addClass("heading-level-" ~ to!string(level));
1658 				} else if(level < lastLevel) {
1659 					while(current && !current.hasClass("heading-level-" ~ to!string(level)))
1660 						current = current.parentNode;
1661 					if(current is null) {
1662 						import std.stdio;
1663 						writeln("WARNING: TOC broken on " ~ decl.name);
1664 						goto skip_toc;
1665 					}
1666 					assert(current !is null);
1667 				}
1668 
1669 				lastLevel = level;
1670 				addTo = current;
1671 				if(addTo.tagName != "ol")
1672 					addTo = addTo.parentNode;
1673 			}
1674 
1675 			if(!header.hasAttribute("id"))
1676 				header.attrs.id = toId(header.innerText);
1677 			if(header.querySelector(" > *") is null) {
1678 				auto selfLink = Element.make("a", header.innerText, "#" ~ header.attrs.id);
1679 				selfLink.addClass("header-anchor");
1680 				header.innerHTML = selfLink.toString();
1681 			}
1682 
1683 			if(addToIt)
1684 				addTo.addChild("li", Element.make("a", header.innerText, "#" ~ header.attrs.id));
1685 		}
1686 
1687 		if(auto d = document.querySelector("#more-link")) {
1688 			if(document.querySelectorAll(".user-header:not(.hide-from-toc)").length > 2)
1689 				d.replaceWith(toc);
1690 		}
1691 
1692 		skip_toc: {}
1693 
1694 		if(auto a = document.querySelector(".annotated-prototype"))
1695 			outer: foreach(c; a.querySelectorAll(".parameters-list")) {
1696 				auto p = c.parentNode;
1697 				while(p) {
1698 					if(p.hasClass("lambda-expression"))
1699 						continue outer;
1700 					p = p.parentNode;
1701 				}
1702 				c.addClass("toplevel");
1703 			}
1704 
1705 		// for line numbering
1706 		foreach(pre; document.querySelectorAll("pre.highlighted, pre.block-code[data-language!=\"\"]")) {
1707 			addLineNumbering(pre);
1708 		}
1709 
1710 		string overloadLink;
1711 		string declLink = decl.link(true, &overloadLink);
1712 
1713 		if(usePseudoFiles) {
1714 			pseudoFiles[declLink] = document.toString();
1715 			if(overloadLink.length)
1716 				pseudoFiles[overloadLink] = redirectToOverloadHtml(declLink);
1717 		} else {
1718 			writeFile(outputFilePath(declLink), document.toString(), gzip);
1719 			if(overloadLink.length)
1720 				writeFile(outputFilePath(overloadLink), redirectToOverloadHtml(declLink), gzip);
1721 		}
1722 
1723 		import std.stdio;
1724 		writeln("WRITTEN TO ", declLink);
1725 	}
1726 
1727 	return document;
1728 }
1729 
1730 string redirectToOverloadHtml(string what) {
1731 	return `<html class="overload-redirect"><script>location.href = '`~what~`';</script> <a href="`~what~`">Continue to overload</a></html>`;
1732 }
1733 
1734 void addLineNumbering(Element pre, bool id = false) {
1735 	if(pre.hasClass("with-line-wrappers"))
1736 		return;
1737 	string html;
1738 	int count;
1739 	foreach(idx, line; pre.innerHTML.splitLines) {
1740 		auto num = to!string(idx + 1);
1741 		auto href = "L"~num;
1742 		if(id)
1743 			html ~= "<a class=\"br\""~(id ? " id=\""~href~"\"" : "")~" href=\"#"~href~"\">"~num~" </a>";
1744 		else
1745 			html ~= "<span class=\"br\">"~num~" </span>";
1746 		html ~= line;
1747 		html ~= "\n";
1748 		count++;
1749 	}
1750 	if(count < 15)
1751 		return; // no point cluttering the display with the sample is so small you can eyeball it instantly anyway
1752 	pre.innerHTML = html.stripRight;
1753 	pre.addClass("with-line-wrappers");
1754 
1755 	if(count >= 10000)
1756 		pre.addClass("ten-thousand-lines");
1757 	else if(count >= 1000)
1758 		pre.addClass("thousand-lines");
1759 }
1760 
1761 string lastDotOnly(string s) {
1762 	auto idx = s.lastIndexOf(".");
1763 	if(idx == -1) return s;
1764 	return s[idx + 1 .. $];
1765 }
1766 
1767 struct InheritanceResult {
1768 	Decl decl; // may be null
1769 	string plainText;
1770 	//const(BaseClass) ast;
1771 }
1772 
1773 Decl[] declsByUda(string uda, Decl start = null) {
1774 	if(start is null) {
1775 		assert(0); // cross-module search not implemented here
1776 	}
1777 
1778 	Decl[] list;
1779 
1780 	if(start.hasUda(uda))
1781 		list ~= start;
1782 
1783 	foreach(child; start.children)
1784 		list ~= declsByUda(uda, child);
1785 
1786 	return list;
1787 	
1788 }
1789 
1790 abstract class Decl {
1791 	private int databaseId;
1792 
1793 	bool fakeDecl = false;
1794 	bool alreadyGenerated = false;
1795 	abstract string name();
1796 	abstract string comment();
1797 	abstract string rawComment();
1798 	abstract string declarationType();
1799 	abstract const(ASTNode) getAstNode();
1800 	abstract int lineNumber();
1801 
1802 	//abstract string sourceCode();
1803 
1804 	abstract void getAnnotatedPrototype(MyOutputRange);
1805 	abstract void getSimplifiedPrototype(MyOutputRange);
1806 
1807 	final string externNote() {
1808 		bool hadABody;
1809 		if(auto f = cast(FunctionDecl) this) {
1810 			if(f.astNode && f.astNode.functionBody)
1811 				hadABody = f.astNode.functionBody.hadABody;
1812 		}
1813 
1814 		if(hadABody)
1815 			return ". Be warned that the author may not have intended to support it.";
1816 
1817 		switch(externWhat) {
1818 			case "C":
1819 			case "C++":
1820 			case "Windows":
1821 			case "Objective-C":
1822 				return " but is binding to " ~ externWhat ~ ". You might be able to learn more by searching the web for its name.";
1823 			case "System":
1824 				return " but is binding to an external library. You might be able to learn more by searching the web for its name.";
1825 			case null:
1826 			default:
1827 				return ".";
1828 		}
1829 	}
1830 
1831 	DocComment parsedDocComment_;
1832 	final @property DocComment parsedDocComment() {
1833 		if(parsedDocComment_ is DocComment.init)
1834 			parsedDocComment_ = parseDocumentationComment(comment.length ? comment : "/++\nUndocumented in source"~externNote~"\n+/", this);
1835 		return parsedDocComment_;
1836 	}
1837 
1838 	void getAggregatePrototype(MyOutputRange r) {
1839 		getSimplifiedPrototype(r);
1840 		r.put(";");
1841 	}
1842 
1843 	/* virtual */ void addSupplementalData(Element) {}
1844 
1845 	// why is this needed?!?!?!?!?
1846 	override int opCmp(Object o) {
1847 		return cast(int)cast(void*)this - cast(int)cast(void*)o;
1848 	}
1849 
1850 	Decl parentModule() {
1851 		auto p = this;
1852 		while(p) {
1853 			if(p.isModule())
1854 				return p;
1855 			p = p.parent;
1856 		}
1857 		assert(0);
1858 	}
1859 
1860 	Decl previousSibling() {
1861 		if(parent is null)
1862 			return null;
1863 
1864 		Decl prev;
1865 		foreach(child; parent.children) {
1866 			if(child is this)
1867 				return prev;
1868 			prev = child;
1869 		}
1870 
1871 		return null;
1872 	}
1873 
1874 	bool isDocumented() {
1875 		// this shouldn't be needed anymore cuz the recursive check below does a better job
1876 		//if(this.isModule)
1877 			//return true; // allow undocumented modules because then it will at least descend into documented children
1878 
1879 		// skip modules with "internal" because they are usually not meant
1880 		// to be publicly documented anyway
1881 		{
1882 		auto mod = this.parentModule.name;
1883 		if(mod.indexOf(".internal") != -1 && !documentInternal)
1884 			return false;
1885 		}
1886 
1887 		if(documentUndocumented)
1888 			return true;
1889 
1890 		if(rawComment.length) // hack
1891 		return rawComment.length > 0; // cool, not a hack
1892 
1893 		// if it has any documented children, we want to pretend this is documented too
1894 		// since then it will be possible to navigate to it
1895 		foreach(child; children)
1896 			if(child.docsShouldBeOutputted())
1897 				return true;
1898 
1899 		// what follows is all filthy hack
1900 		// the C bindings in druntime are not documented, but
1901 		// we want them to show up. So I'm gonna hack it.
1902 
1903 		/*
1904 		auto mod = this.parentModule.name;
1905 		if(mod.startsWith("core"))
1906 			return true;
1907 		*/
1908 		return false;
1909 	}
1910 
1911 	bool isStatic() {
1912 		foreach (a; attributes) {
1913 			if(a.attr && a.attr.attribute.type == tok!"static")
1914 				return true;
1915 			// gshared also implies static (though note that shared does not!)
1916 			if(a.attr && a.attr.attribute.type == tok!"__gshared")
1917 				return true;
1918 		}
1919 
1920 		return false;
1921 	}
1922 
1923 	bool isPrivate() {
1924 		IdType protection;
1925 		foreach (a; attributes) {
1926 			if (a.attr && isProtection(a.attr.attribute.type))
1927 				protection = a.attr.attribute.type;
1928 		}
1929 
1930 		return protection == tok!"private";
1931 	}
1932 
1933 	bool isExplicitlyNonPrivate() {
1934 		IdType protection;
1935 		bool hadOne;
1936 		foreach (a; attributes) {
1937 			if (a.attr && isProtection(a.attr.attribute.type)) {
1938 				protection = a.attr.attribute.type;
1939 				hadOne = true;
1940 			}
1941 		}
1942 
1943 		return hadOne && protection != tok!"private" && protection != tok!"package";
1944 
1945 	}
1946 
1947 	string externWhat() {
1948 		LinkageAttribute attr;
1949 		foreach (a; attributes) {
1950 			if(a.attr && a.attr.linkageAttribute)
1951 				attr = cast() a.attr.linkageAttribute;
1952 		}
1953 
1954 		if(attr is null)
1955 			return null;
1956 		auto text = attr.identifier.text;
1957 		if(text == "Objective")
1958 			text = "Objective-C";
1959 		else
1960 			text = text ~ (attr.hasPlusPlus ? "++" : "");
1961 
1962 		return text;
1963 	}
1964 
1965 	bool docsShouldBeOutputted() {
1966 		if(this.rawComment.indexOf("$(NEVER_DOCUMENT)") != -1)
1967 			return false;
1968 		if((!this.isPrivate || writePrivateDocs) && this.isDocumented)
1969 			return true;
1970 		else if(this.rawComment.indexOf("$(ALWAYS_DOCUMENT)") != -1)
1971 			return true;
1972 		return false;
1973 	}
1974 
1975 	final bool hasUda(string name) {
1976 		foreach(a; attributes) {
1977 			if(a.attr && a.attr.atAttribute && a.attr.atAttribute.identifier.text == name)
1978 				return true;
1979 			if(a.attr && a.attr.atAttribute && a.attr.atAttribute.argumentList)
1980 				foreach(at; a.attr.atAttribute.argumentList.items) {
1981 					if(auto e = cast(UnaryExpression) at)
1982 					if(auto pe = e.primaryExpression)
1983 					if(auto i = pe.identifierOrTemplateInstance)
1984 					if(i.identifier.text == name)
1985 						return true;
1986 				}
1987 
1988 		}
1989 		return false;
1990 	}
1991 
1992 	// FIXME: isFinal and isVirtual
1993 	// FIXME: it would be nice to inherit documentation from interfaces too.
1994 
1995 	bool isProperty() {
1996 		return hasUda("property"); // property isn't actually a UDA, but adrdox doesn't care.
1997 	}
1998 
1999 	bool isAggregateMember() {
2000 		return parent ? !parent.isModule : false; // FIXME?
2001 	}
2002 
2003 	// does NOT look for aliased overload sets, just ones right in this scope
2004 	// includes this in the return (plus eponymous check). Check if overloaded with .length > 1
2005 	Decl[] getImmediateDocumentedOverloads() {
2006 		Decl[] ret;
2007 
2008 		if(this.parent !is null) {
2009 			foreach(child; this.parent.children) {
2010 				if(((cast(ImportDecl) child) is null) && child.name == this.name && child.docsShouldBeOutputted())
2011 					ret ~= child;
2012 			}
2013 			if(auto t = cast(TemplateDecl) this.parent)
2014 			if(this is t.eponymousMember) {
2015 				foreach(i; t.getImmediateDocumentedOverloads())
2016 					if(i !is t)
2017 						ret ~= i;
2018 			}
2019 		}
2020 
2021 		return ret;
2022 	}
2023 
2024 	Decl[] getDittos() {
2025 		if(this.parent is null)
2026 			return null;
2027 
2028 		size_t lastNonDitto;
2029 
2030 		foreach(idx, child; this.parent.children) {
2031 			if(!child.isDitto())
2032 				lastNonDitto = idx;
2033 			if(child is this) {
2034 				break;
2035 			}
2036 		}
2037 
2038 		size_t stop = lastNonDitto;
2039 		foreach(idx, child; this.parent.children[lastNonDitto + 1 .. $])
2040 			if(child.isDitto())
2041 				stop = idx + lastNonDitto + 1 + 1; // one +1 is offset of begin, other is to make sure it is inclusive
2042 			else
2043 				break;
2044 
2045 		return this.parent.children[lastNonDitto .. stop];
2046 	}
2047 
2048 	string link(bool forFile = false, string* masterOverloadName = null) {
2049 		auto linkTo = this;
2050 		if(!forFile && this.isModule && this.children.length == 1) {
2051 			linkTo = this.children[0];
2052 		}
2053 
2054 		auto n = linkTo.fullyQualifiedName();
2055 
2056 		auto overloads = linkTo.getImmediateDocumentedOverloads();
2057 		if(overloads.length > 1) {
2058 			int number = 1;
2059 			int goodNumber;
2060 			foreach(overload; overloads) {
2061 				if(overload is this) {
2062 					goodNumber = number;
2063 					break;
2064 				}
2065 				number++;
2066 			}
2067 
2068 			if(goodNumber)
2069 				number = goodNumber;
2070 			else
2071 				number = 1;
2072 
2073 			if(masterOverloadName !is null)
2074 				*masterOverloadName = n.idup;
2075 
2076 			import std.conv : text;
2077 			n ~= text(".", number);
2078 		}
2079 
2080 		n ~= ".html";
2081 
2082 		if(masterOverloadName !is null)
2083 			*masterOverloadName ~= ".html";
2084 
2085 		if(!forFile) {
2086 			string d = getDirectoryForPackage(linkTo.fullyQualifiedName());
2087 			if(d.length) {
2088 				n = d ~ n;
2089 				if(masterOverloadName !is null)
2090 					*masterOverloadName = d ~ *masterOverloadName;
2091 			}
2092 		}
2093 
2094 		return n.handleCaseSensitivity();
2095 	}
2096 
2097 	string[] parentNameList() {
2098 		string[] fqn = [name()];
2099 		auto p = parent;
2100 		while(p) {
2101 			fqn = p.name() ~ fqn;
2102 			p = p.parent;
2103 		}
2104 		return fqn;
2105 
2106 	}
2107 
2108 	string fullyQualifiedName() {
2109 		string fqn = name();
2110 		if(isModule)
2111 			return fqn;
2112 		auto p = parent;
2113 		while(p) {
2114 			fqn = p.name() ~ "." ~ fqn;
2115 			if(p.isModule)
2116 				break; // do NOT want package names in here
2117 			p = p.parent;
2118 		}
2119 		return fqn;
2120 	}
2121 
2122 	final InheritanceResult[] inheritsFrom() {
2123 		if(!inheritsFromProcessed)
2124 		foreach(ref i; _inheritsFrom)
2125 			if(this.parent && i.plainText.length) {
2126 				i.decl = this.parent.lookupName(i.plainText);
2127 			}
2128 		inheritsFromProcessed = true;
2129 		return _inheritsFrom; 
2130 	}
2131 	InheritanceResult[] _inheritsFrom;
2132 	bool inheritsFromProcessed = false;
2133 
2134 	Decl[string] nameTable;
2135 	bool nameTableBuilt;
2136 	Decl[string] buildNameTable(string[] excludeModules = null) {
2137 		if(!nameTableBuilt) {
2138 			lookup: foreach(mod; this.importedModules) {
2139 				if(!mod.publicImport)
2140 					continue;
2141 				if(auto modDeclPtr = mod.name in modulesByName) {
2142 					auto modDecl = *modDeclPtr;
2143 
2144 					foreach(imod; excludeModules)
2145 						if(imod == modDeclPtr.name)
2146 							break lookup;
2147 
2148 					auto tbl = modDecl.buildNameTable(excludeModules ~ this.parentModule.name);
2149 					foreach(k, v; tbl)
2150 						nameTable[k] = v;
2151 				}
2152 			}
2153 
2154 			foreach(child; children)
2155 				nameTable[child.name] = child;
2156 
2157 			nameTableBuilt = true;
2158 		}
2159 		return nameTable;
2160 	}
2161 
2162 	// the excludeModules is meant to prevent circular lookups
2163 	Decl lookupName(string name, bool lookUp = true, string[] excludeModules = null) {
2164 		if(importedModules.length == 0 || importedModules[$-1].name != "object")
2165 			addImport("object", false);
2166 
2167 		if(name.length == 0)
2168 			return null;
2169 		string originalFullName = name;
2170 		auto subject = this;
2171 		if(name[0] == '.') {
2172 			// global scope operator
2173 			while(subject && !subject.isModule)
2174 				subject = subject.parent;
2175 			name = name[1 .. $];
2176 			originalFullName = originalFullName[1 .. $];
2177 
2178 		}
2179 
2180 		auto firstDotIdx = name.indexOf(".");
2181 		if(firstDotIdx != -1) {
2182 			subject = subject.lookupName(name[0 .. firstDotIdx]);
2183 			name = name[firstDotIdx + 1 .. $];
2184 		}
2185 
2186 		if(subject)
2187 		while(subject) {
2188 
2189 			auto table = subject.buildNameTable();
2190 			if(name in table)
2191 				return table[name];
2192 
2193 			if(lookUp)
2194 			// at the top level, we also need to check private imports
2195 			lookup: foreach(mod; subject.importedModules) {
2196 				if(mod.publicImport)
2197 					continue; // handled by the name table
2198 				auto lookupInsideModule = originalFullName;
2199 				if(auto modDeclPtr = mod.name in modulesByName) {
2200 					auto modDecl = *modDeclPtr;
2201 
2202 					foreach(imod; excludeModules)
2203 						if(imod == modDeclPtr.name)
2204 							break lookup;
2205 					//import std.stdio; writeln(modDecl.name, " ", lookupInsideModule);
2206 					auto located = modDecl.lookupName(lookupInsideModule, false, excludeModules ~ this.parentModule.name);
2207 					if(located !is null)
2208 						return located;
2209 				}
2210 			}
2211 
2212 			if(!lookUp || subject.isModule)
2213 				subject = null;
2214 			else
2215 				subject = subject.parent;
2216 		}
2217 		else {
2218 			// FIXME?
2219 			// fully qualified name from this module
2220 			subject = this;
2221 			if(originalFullName.startsWith(this.parentModule.name ~ ".")) {
2222 				// came from here!
2223 				auto located = this.parentModule.lookupName(originalFullName[this.parentModule.name.length + 1 .. $]);
2224 				if(located !is null)
2225 					return located;
2226 			} else
2227 			while(subject !is null) {
2228 				foreach(mod; subject.importedModules) {
2229 					if(originalFullName.startsWith(mod.name ~ ".")) {
2230 						// fully qualified name from this module
2231 						auto lookupInsideModule = originalFullName[mod.name.length + 1 .. $];
2232 						if(auto modDeclPtr = mod.name in modulesByName) {
2233 							auto modDecl = *modDeclPtr;
2234 							auto located = modDecl.lookupName(lookupInsideModule, mod.publicImport);
2235 							if(located !is null)
2236 								return located;
2237 						}
2238 					}
2239 				}
2240 
2241 				if(lookUp && subject.isModule)
2242 					subject = null;
2243 				else
2244 					subject = subject.parent;
2245 			}
2246 		}
2247 
2248 		return null;
2249 	}
2250 
2251 	final Decl lookupName(const IdentifierOrTemplateInstance ic, bool lookUp = true) {
2252 		auto subject = this;
2253 		if(ic.templateInstance)
2254 			return null; // FIXME
2255 
2256 		return lookupName(ic.identifier.text, lookUp);
2257 	}
2258 
2259 
2260 	final Decl lookupName(const IdentifierChain ic) {
2261 		auto subject = this;
2262 		assert(ic.identifiers.length);
2263 
2264 		// FIXME: leading dot?
2265 		foreach(idx, ident; ic.identifiers) {
2266 			subject = subject.lookupName(ident.text, idx == 0);
2267 			if(subject is null) return null;
2268 		}
2269 		return subject;
2270 	}
2271 
2272 	final Decl lookupName(const IdentifierOrTemplateChain ic) {
2273 		auto subject = this;
2274 		assert(ic.identifiersOrTemplateInstances.length);
2275 
2276 		// FIXME: leading dot?
2277 		foreach(idx, ident; ic.identifiersOrTemplateInstances) {
2278 			subject = subject.lookupName(ident, idx == 0);
2279 			if(subject is null) return null;
2280 		}
2281 		return subject;
2282 	}
2283 
2284 	final Decl lookupName(const Symbol ic) {
2285 		// FIXME dot
2286 		return lookupName(ic.identifierOrTemplateChain);
2287 	}
2288 
2289 
2290 	Decl parent;
2291 	Decl[] children;
2292 
2293 	void writeTemplateConstraint(MyOutputRange output);
2294 
2295 	const(VersionOrAttribute)[] attributes;
2296 
2297 	void addChild(Decl decl) {
2298 		decl.parent = this;
2299 		children ~= decl;
2300 	}
2301 
2302 	struct ImportedModule {
2303 		string name;
2304 		bool publicImport;
2305 	}
2306 	ImportedModule[] importedModules;
2307 	void addImport(string moduleName, bool isPublic) {
2308 		importedModules ~= ImportedModule(moduleName, isPublic);
2309 	}
2310 
2311 	struct Unittest {
2312 		const(dparse.ast.Unittest) ut;
2313 		string code;
2314 		string comment;
2315 	}
2316 
2317 	Unittest[] unittests;
2318 
2319 	void addUnittest(const(dparse.ast.Unittest) ut, const(ubyte)[] code, string comment) {
2320 		int slicePoint = 0;
2321 		foreach(idx, b; code) {
2322 			if(b == ' ' || b == '\t' || b == '\r')
2323 				slicePoint++;
2324 			else if(b == '\n') {
2325 				slicePoint++;
2326 				break;
2327 			} else {
2328 				slicePoint = 0;
2329 				break;
2330 			}
2331 		}
2332 		code = code[slicePoint .. $];
2333 		unittests ~= Unittest(ut, unittestCodeToString(code), comment);
2334 	}
2335 
2336 	string unittestCodeToString(const(ubyte)[] code) {
2337 		auto excludeString = cast(const(ubyte[])) "// exclude from docs";
2338 		bool replacementMade;
2339 
2340 		import std.algorithm.searching;
2341 
2342 		auto idx = code.countUntil(excludeString);
2343 		while(idx != -1) {
2344 			int before = cast(int) idx;
2345 			int after = cast(int) idx;
2346 			while(before > 0 && code[before] != '\n')
2347 				before--;
2348 			while(after < code.length && code[after] != '\n')
2349 				after++;
2350 
2351 			code = code[0 .. before] ~ code[after .. $];
2352 			replacementMade = true;
2353 			idx = code.countUntil(excludeString);
2354 		}
2355 
2356 		if(!replacementMade)
2357 			return (cast(char[]) code).idup; // needs to be unique
2358 		else
2359 			return cast(string) code; // already copied above, so it is unique
2360 	}
2361 
2362 	struct ProcessedUnittest {
2363 		string code;
2364 		string comment;
2365 		bool embedded;
2366 	}
2367 
2368 	bool _unittestsProcessed;
2369 	ProcessedUnittest[] _processedUnittests;
2370 
2371 	ProcessedUnittest[] getProcessedUnittests() {
2372 		if(_unittestsProcessed)
2373 			return _processedUnittests;
2374 
2375 		_unittestsProcessed = true;
2376 
2377 		// source, comment
2378 		ProcessedUnittest[] ret;
2379 
2380 		Decl start = this;
2381 		if(isDitto()) {
2382 			foreach(child; this.parent.children) {
2383 				if(child is this)
2384 					break;
2385 				if(!child.isDitto())
2386 					start = child;
2387 			}
2388 
2389 		}
2390 
2391 		bool started = false;
2392 		if(this.parent)
2393 		foreach(child; this.parent.children) {
2394 			if(started) {
2395 				if(!child.isDitto())
2396 					break;
2397 			} else {
2398 				if(child is start)
2399 					started = true;
2400 			}
2401 
2402 			if(started)
2403 				foreach(test; child.unittests)
2404 					if(test.comment.length)
2405 						ret ~= ProcessedUnittest(test.code, test.comment);
2406 		}
2407 		else
2408 			foreach(test; this.unittests)
2409 				if(test.comment.length)
2410 					ret ~= ProcessedUnittest(test.code, test.comment);
2411 		_processedUnittests = ret;
2412 		return ret;
2413 	}
2414 
2415 	override string toString() {
2416 		string s;
2417 		s ~= super.toString() ~ " " ~ this.name();
2418 		foreach(child; children) {
2419 			s ~= "\n";
2420 			auto p = parent;
2421 			while(p) {
2422 				s ~= "\t";
2423 				p = p.parent;
2424 			}
2425 			s ~= child.toString();
2426 		}
2427 		return s;
2428 	}
2429 
2430 	abstract bool isDitto();
2431 	bool isModule() { return false; }
2432 	bool isArticle() { return false; }
2433 	bool isConstructor() { return false; }
2434 
2435 	bool aliasThisPresent;
2436 	Token aliasThisToken;
2437 	string aliasThisComment;
2438 
2439 	Decl aliasThis() {
2440 		if(!aliasThisPresent)
2441 			return null;
2442 		else
2443 			return lookupName(aliasThisToken.text, false);
2444 	}
2445 
2446 	DestructorDecl destructor() {
2447 		foreach(child; children)
2448 			if(auto dd = cast(DestructorDecl) child)
2449 				return dd;
2450 		return null;
2451 	}
2452 
2453 	PostblitDecl postblit() {
2454 		foreach(child; children)
2455 			if(auto dd = cast(PostblitDecl) child)
2456 				return dd;
2457 		return null;
2458 	}
2459 
2460 
2461 	abstract bool isDisabled();
2462 
2463 	ConstructorDecl disabledDefaultConstructor() {
2464 		foreach(child; children)
2465 		if(child.isConstructor() && child.isDisabled()) {
2466 			auto ctor = cast(ConstructorDecl) child;
2467 			if(ctor.astNode.parameters || ctor.astNode.parameters.parameters.length == 0)
2468 				return ctor;
2469 		}
2470 		return null;
2471 	}
2472 }
2473 
2474 class ModuleDecl : Decl {
2475 	mixin CtorFrom!Module defaultMixins;
2476 
2477 	string justDocsTitle;
2478 
2479 	override bool isModule() { return true; }
2480 	override bool isArticle() { return justDocsTitle.length > 0; }
2481 
2482 	override bool docsShouldBeOutputted() {
2483 		if(this.justDocsTitle !is null)
2484 			return true;
2485 		return super.docsShouldBeOutputted();
2486 	}
2487 
2488 	override string declarationType() {
2489 		return isArticle() ? "Article" : "module";
2490 	}
2491 
2492 	version(none)
2493 	override void getSimplifiedPrototype(MyOutputRange r) {
2494 		if(isArticle())
2495 			r.put(justDocsTitle);
2496 		else
2497 			defaultMixins.getSimplifiedPrototype(r);
2498 	}
2499 
2500 	ubyte[] originalSource;
2501 
2502 	string packageName() {
2503 		auto it = this.name();
2504 		auto idx = it.lastIndexOf(".");
2505 		if(idx == -1)
2506 			return null;
2507 		return it[0 .. idx];
2508 	}
2509 }
2510 
2511 class AliasDecl : Decl {
2512 	mixin CtorFrom!AliasDeclaration;
2513 
2514 	this(const(AliasDeclaration) ad, const(VersionOrAttribute)[] attributes) {
2515 		this.attributes = attributes;
2516 		this.astNode = ad;
2517 		this.initializer = null;
2518 		// deal with the type and initializer list and storage classes
2519 	}
2520 
2521 	const(AliasInitializer) initializer;
2522 
2523 	this(const(AliasDeclaration) ad, const(AliasInitializer) init, const(VersionOrAttribute)[] attributes) {
2524 		this.attributes = attributes;
2525 		this.astNode = ad;
2526 		this.initializer = init;
2527 		// deal with init
2528 	}
2529 
2530 	override string name() {
2531 		if(initializer is null)
2532 			return toText(astNode.identifierList);
2533 		else
2534 			return initializer.name.text;
2535 	}
2536 
2537 	override void getAnnotatedPrototype(MyOutputRange output) {
2538 		void cool() {
2539 			output.putTag("<div class=\"declaration-prototype\">");
2540 			if(parent !is null && !parent.isModule) {
2541 				output.putTag("<div class=\"parent-prototype\">");
2542 				parent.getSimplifiedPrototype(output);
2543 				output.putTag("</div><div>");
2544 				getPrototype(output, true);
2545 				output.putTag("</div>");
2546 			} else {
2547 				getPrototype(output, true);
2548 			}
2549 			output.putTag("</div>");
2550 		}
2551 
2552 		writeOverloads!cool(this, output);
2553 	}
2554 
2555 	override void getSimplifiedPrototype(MyOutputRange output) {
2556 		getPrototype(output, false);
2557 	}
2558 
2559 	void getPrototype(MyOutputRange output, bool link) {
2560 		// FIXME: storage classes?
2561 
2562 		if(link) {
2563 			auto f = new MyFormatter!(typeof(output))(output, this);
2564 			writeAttributes(f, output, this.attributes);
2565 		}
2566 
2567 		output.putTag("<span class=\"builtin-type\">alias</span> ");
2568 
2569 		output.putTag("<span class=\"name\">");
2570 		output.put(name);
2571 		output.putTag("</span>");
2572 
2573 		if(initializer && initializer.templateParameters) {
2574 			output.putTag(toHtml(initializer.templateParameters).source);
2575 		}
2576 
2577 		output.put(" = ");
2578 
2579 		if(initializer) {
2580 			if(link)
2581 				output.putTag(toLinkedHtml(initializer.type, this).source);
2582 			else
2583 				output.putTag(toHtml(initializer.type).source);
2584 		}
2585 
2586 		if(astNode.type) {
2587 			if(link) {
2588 				auto t = toText(astNode.type);
2589 				auto decl = lookupName(t);
2590 				if(decl is null)
2591 					goto nulldecl;
2592 				output.putTag(getReferenceLink(t, decl).toString);
2593 			} else {
2594 			nulldecl:
2595 				output.putTag(toHtml(astNode.type).source);
2596 			}
2597 		}
2598 	}
2599 }
2600 
2601 class VariableDecl : Decl {
2602 	mixin CtorFrom!VariableDeclaration;
2603 
2604 	const(Declarator) declarator;
2605 	this(const(Declarator) declarator, const(VariableDeclaration) astNode, const(VersionOrAttribute)[] attributes) {
2606 		this.astNode = astNode;
2607 		this.declarator = declarator;
2608 		this.attributes = attributes;
2609 		this.ident = Token.init;
2610 		this.initializer = null;
2611 
2612 		foreach(a; astNode.attributes)
2613 			this.attributes ~= new VersionOrAttribute(a);
2614 		filterDuplicateAttributes();
2615 	}
2616 
2617 	const(Token) ident;
2618 	const(Initializer) initializer;
2619 	this(const(VariableDeclaration) astNode, const(Token) ident, const(Initializer) initializer, const(VersionOrAttribute)[] attributes, bool isEnum) {
2620 		this.declarator = null;
2621 		this.attributes = attributes;
2622 		this.astNode = astNode;
2623 		this.ident = ident;
2624 		this.isEnum = isEnum;
2625 		this.initializer = initializer;
2626 
2627 		foreach(a; astNode.attributes)
2628 			this.attributes ~= new VersionOrAttribute(a);
2629 		filterDuplicateAttributes();
2630 	}
2631 
2632 	void filterDuplicateAttributes() {
2633 		const(VersionOrAttribute)[] filtered;
2634 		foreach(idx, a; attributes) {
2635 			bool isdup;
2636 			foreach(b; attributes[idx + 1 .. $]) {
2637 				if(a is b)
2638 					continue;
2639 
2640 				if(cast(FakeAttribute) a || cast(FakeAttribute) b)
2641 					continue;
2642 
2643 				if(a.attr is b.attr)
2644 					isdup = true;
2645 				else if(toText(a.attr) == toText(b.attr))
2646 					isdup = true;
2647 			}
2648 			if(!isdup)
2649 				filtered ~= a;
2650 		}
2651 
2652 		this.attributes = filtered;
2653 	}
2654 
2655 	bool isEnum;
2656 
2657 	override string name() {
2658 		if(declarator)
2659 			return declarator.name.text;
2660 		else
2661 			return ident.text;
2662 	}
2663 
2664 	override string rawComment() {
2665 		string it = astNode.comment;
2666 		auto additional = (declarator ? declarator.comment : astNode.autoDeclaration.comment);
2667 		if(additional != it)
2668 			it ~= additional;
2669 		return it;
2670 	}
2671 
2672 	override void getAnnotatedPrototype(MyOutputRange outputFinal) {
2673 		string t;
2674 		MyOutputRange output = MyOutputRange(&t);
2675 
2676 		output.putTag("<div class=\"declaration-prototype\">");
2677 		if(parent !is null && !parent.isModule) {
2678 			output.putTag("<div class=\"parent-prototype\">");
2679 			parent.getSimplifiedPrototype(output);
2680 			output.putTag("</div><div>");
2681 			auto f = new MyFormatter!(typeof(output))(output);
2682 			writeAttributes(f, output, attributes);
2683 			getSimplifiedPrototypeInternal(output, true);
2684 			output.putTag("</div>");
2685 		} else {
2686 			auto f = new MyFormatter!(typeof(output))(output);
2687 			writeAttributes(f, output, attributes);
2688 			getSimplifiedPrototypeInternal(output, true);
2689 		}
2690 		output.putTag("</div>");
2691 
2692 		outputFinal.putTag(linkUpHtml(t, this));
2693 	}
2694 
2695 	override void getSimplifiedPrototype(MyOutputRange output) {
2696 		getSimplifiedPrototypeInternal(output, false);
2697 	}
2698 
2699 	final void getSimplifiedPrototypeInternal(MyOutputRange output, bool link) {
2700 		foreach(sc; astNode.storageClasses) {
2701 			output.putTag(toHtml(sc).source);
2702 			output.put(" ");
2703 		}
2704 
2705 		if(astNode.type) {
2706 			if(link) {
2707 				auto html = toHtml(astNode.type).source;
2708 				auto txt = toText(astNode.type);
2709 
2710 				auto typeDecl = lookupName(txt);
2711 				if(typeDecl is null || !typeDecl.docsShouldBeOutputted)
2712 					goto plain;
2713 
2714 				output.putTag("<a title=\""~typeDecl.fullyQualifiedName~"\" href=\""~typeDecl.link~"\">" ~ html ~ "</a>");
2715 			} else {
2716 				plain:
2717 				output.putTag(toHtml(astNode.type).source);
2718 			}
2719 		} else
2720 			output.putTag("<span class=\"builtin-type\">"~(isEnum ? "enum" : "auto")~"</span>");
2721 
2722 		output.put(" ");
2723 
2724 		output.putTag("<span class=\"name\">");
2725 		output.put(name);
2726 		output.putTag("</span>");
2727 
2728 		if(declarator && declarator.templateParameters)
2729 			output.putTag(toHtml(declarator.templateParameters).source);
2730 
2731 		if(link) {
2732 			if(initializer !is null) {
2733 				output.put(" = ");
2734 				output.putTag(toHtml(initializer).source);
2735 			}
2736 		}
2737 		output.put(";");
2738 	}
2739 
2740 	override void getAggregatePrototype(MyOutputRange output) {
2741 		auto f = new MyFormatter!(typeof(output))(output);
2742 		writeAttributes(f, output, attributes);
2743 		getSimplifiedPrototypeInternal(output, false);
2744 	}
2745 
2746 	override string declarationType() {
2747 		return (isStatic() ? "static variable" : (isEnum ? "manifest constant" : "variable"));
2748 	}
2749 }
2750 
2751 
2752 class FunctionDecl : Decl {
2753 	mixin CtorFrom!FunctionDeclaration;
2754 	override void getAnnotatedPrototype(MyOutputRange output) {
2755 		doFunctionDec(this, output);
2756 	}
2757 
2758 	override Decl lookupName(string name, bool lookUp = true, string[] excludeModules = null) {
2759 		// is it a param or template param? If so, return that.
2760 
2761 		foreach(param; astNode.parameters.parameters) {
2762 			if (param.name.type != tok!"")
2763 				if(param.name.text == name) {
2764 					return null; // it is local, but we don't have a decl..
2765 				}
2766 		}
2767 		if(astNode.templateParameters && astNode.templateParameters.templateParameterList && astNode.templateParameters.templateParameterList.items)
2768 		foreach(param; astNode.templateParameters.templateParameterList.items) {
2769 			auto paramName = "";
2770 
2771 			if(param.templateTypeParameter)
2772 				paramName = param.templateTypeParameter.identifier.text;
2773 			else if(param.templateValueParameter)
2774 				paramName = param.templateValueParameter.identifier.text;
2775 			else if(param.templateAliasParameter)
2776 				paramName = param.templateAliasParameter.identifier.text;
2777 			else if(param.templateTupleParameter)
2778 				paramName = param.templateTupleParameter.identifier.text;
2779 
2780 			if(paramName.length && paramName == name) {
2781 				return null; // it is local, but we don't have a decl..
2782 			}
2783 		}
2784 
2785 		if(lookUp)
2786 			return super.lookupName(name, lookUp, excludeModules);
2787 		else
2788 			return null;
2789 	}
2790 
2791 	override string declarationType() {
2792 		return isProperty() ? "property" : (isStatic() ? "static function" : "function");
2793 	}
2794 
2795 	override void getAggregatePrototype(MyOutputRange output) {
2796 		if(isStatic()) {
2797 			output.putTag("<span class=\"storage-class\">static</span> ");
2798 		}
2799 
2800 		getSimplifiedPrototype(output);
2801 		output.put(";");
2802 	}
2803 
2804 	override void getSimplifiedPrototype(MyOutputRange output) {
2805 		foreach(sc; astNode.storageClasses) {
2806 			output.putTag(toHtml(sc).source);
2807 			output.put(" ");
2808 		}
2809 
2810 		if(isProperty() && (paramCount == 0 || paramCount == 1 || (paramCount == 2 && !isAggregateMember))) {
2811 			if((paramCount == 1 && isAggregateMember()) || (paramCount == 2 && !isAggregateMember())) {
2812 				// setter
2813 				output.putTag(toHtml(astNode.parameters.parameters[0].type).source);
2814 				output.put(" ");
2815 				output.putTag("<span class=\"name\">");
2816 				output.put(name);
2817 				output.putTag("</span>");
2818 
2819 				output.put(" [@property setter]");
2820 			} else {
2821 				// getter
2822 				putSimplfiedReturnValue(output, astNode);
2823 				output.put(" ");
2824 				output.putTag("<span class=\"name\">");
2825 				output.put(name);
2826 				output.putTag("</span>");
2827 
2828 				output.put(" [@property getter]");
2829 			}
2830 		} else {
2831 			putSimplfiedReturnValue(output, astNode);
2832 			output.put(" ");
2833 			output.putTag("<span class=\"name\">");
2834 			output.put(name);
2835 			output.putTag("</span>");
2836 			putSimplfiedArgs(output, astNode);
2837 		}
2838 	}
2839 
2840 	int paramCount() {
2841 		return cast(int) astNode.parameters.parameters.length;
2842 	}
2843 }
2844 
2845 class ConstructorDecl : Decl {
2846 	mixin CtorFrom!Constructor;
2847 
2848 	override void getAnnotatedPrototype(MyOutputRange output) {
2849 		doFunctionDec(this, output);
2850 	}
2851 
2852 	override void getSimplifiedPrototype(MyOutputRange output) {
2853 		output.putTag("<span class=\"lang-feature name\">");
2854 		output.put("this");
2855 		output.putTag("</span>");
2856 		putSimplfiedArgs(output, astNode);
2857 	}
2858 
2859 	override bool isConstructor() { return true; }
2860 }
2861 
2862 class DestructorDecl : Decl {
2863 	mixin CtorFrom!Destructor;
2864 
2865 	override void getSimplifiedPrototype(MyOutputRange output) {
2866 		output.putTag("<span class=\"lang-feature name\">");
2867 		output.put("~this");
2868 		output.putTag("</span>");
2869 		output.put("()");
2870 	}
2871 }
2872 
2873 class PostblitDecl : Decl {
2874 	mixin CtorFrom!Postblit;
2875 
2876 	override void getSimplifiedPrototype(MyOutputRange output) {
2877 		if(isDisabled) {
2878 			output.putTag("<span class=\"builtin-type\">");
2879 			output.put("@disable");
2880 			output.putTag("</span>");
2881 			output.put(" ");
2882 		}
2883 		output.putTag("<span class=\"lang-feature name\">");
2884 		output.put("this(this)");
2885 		output.putTag("</span>");
2886 	}
2887 }
2888 
2889 class ImportDecl : Decl {
2890 	mixin CtorFrom!ImportDeclaration;
2891 
2892 	bool isPublic;
2893 	string newName;
2894 	string oldName;
2895 
2896 	override string link(bool forFile = false, string* useless = null) {
2897 		string d;
2898 		if(!forFile) {
2899 			d = getDirectoryForPackage(oldName);
2900 		}
2901 		return d ~ oldName ~ ".html";
2902 	}
2903 
2904 	// I also want to document undocumented public imports, since they also spam up the namespace
2905 	override bool docsShouldBeOutputted() {
2906 		return isPublic;
2907 	}
2908 
2909 	override string name() {
2910 		return newName.length ? newName : oldName;
2911 	}
2912 
2913 	override string declarationType() {
2914 		return "import";
2915 	}
2916 
2917 	override void getSimplifiedPrototype(MyOutputRange output) {
2918 		if(isPublic)
2919 			output.putTag("<span class=\"builtin-type\">public</span> ");
2920 		output.putTag(toHtml(astNode).source);
2921 	}
2922 
2923 }
2924 
2925 class MixedInTemplateDecl : Decl {
2926 	mixin CtorFrom!TemplateMixinExpression;
2927 
2928 	override string declarationType() {
2929 		return "mixin";
2930 	}
2931 
2932 	override void getSimplifiedPrototype(MyOutputRange output) {
2933 		output.putTag(toHtml(astNode).source);
2934 	}
2935 }
2936 
2937 class StructDecl : Decl {
2938 	mixin CtorFrom!StructDeclaration;
2939 	override void getAnnotatedPrototype(MyOutputRange output) {
2940 		annotatedPrototype(this, output);
2941 	}
2942 
2943 }
2944 
2945 class UnionDecl : Decl {
2946 	mixin CtorFrom!UnionDeclaration;
2947 
2948 	override void getAnnotatedPrototype(MyOutputRange output) {
2949 		annotatedPrototype(this, output);
2950 	}
2951 }
2952 
2953 class ClassDecl : Decl {
2954 	mixin CtorFrom!ClassDeclaration;
2955 
2956 	override void getAnnotatedPrototype(MyOutputRange output) {
2957 		annotatedPrototype(this, output);
2958 	}
2959 }
2960 
2961 class InterfaceDecl : Decl {
2962 	mixin CtorFrom!InterfaceDeclaration;
2963 	override void getAnnotatedPrototype(MyOutputRange output) {
2964 		annotatedPrototype(this, output);
2965 	}
2966 }
2967 
2968 class TemplateDecl : Decl {
2969 	mixin CtorFrom!TemplateDeclaration;
2970 
2971 	Decl eponymousMember() {
2972 		foreach(child; this.children)
2973 			if(child.name == this.name)
2974 				return child;
2975 		return null;
2976 	}
2977 
2978 	override void getAnnotatedPrototype(MyOutputRange output) {
2979 		annotatedPrototype(this, output);
2980 	}
2981 }
2982 
2983 class EponymousTemplateDecl : Decl {
2984 	mixin CtorFrom!EponymousTemplateDeclaration;
2985 
2986 	/*
2987 	Decl eponymousMember() {
2988 		foreach(child; this.children)
2989 			if(child.name == this.name)
2990 				return child;
2991 		return null;
2992 	}
2993 	*/
2994 
2995 	override string declarationType() {
2996 		return "enum";
2997 	}
2998 
2999 	override void getAnnotatedPrototype(MyOutputRange output) {
3000 		annotatedPrototype(this, output);
3001 	}
3002 }
3003 
3004 
3005 class MixinTemplateDecl : Decl {
3006 	mixin CtorFrom!TemplateDeclaration; // MixinTemplateDeclaration does nothing interesting except this..
3007 
3008 	override void getAnnotatedPrototype(MyOutputRange output) {
3009 		annotatedPrototype(this, output);
3010 	}
3011 
3012 	override string declarationType() {
3013 		return "mixin template";
3014 	}
3015 }
3016 
3017 class EnumDecl : Decl {
3018 	mixin CtorFrom!EnumDeclaration;
3019 
3020 	override void addSupplementalData(Element content) {
3021 		doEnumDecl(this, content);
3022 	}
3023 }
3024 
3025 class AnonymousEnumDecl : Decl {
3026 	mixin CtorFrom!AnonymousEnumDeclaration;
3027 
3028 	override string name() {
3029 		assert(astNode.members.length > 0);
3030 		auto name = astNode.members[0].name.text;
3031 		return name;
3032 	}
3033 
3034 	override void addSupplementalData(Element content) {
3035 		doEnumDecl(this, content);
3036 	}
3037 
3038 	override string declarationType() {
3039 		return "enum";
3040 	}
3041 }
3042 
3043 mixin template CtorFrom(T) {
3044 	const(T) astNode;
3045 
3046 	static if(!is(T == VariableDeclaration) && !is(T == AliasDeclaration))
3047 	this(const(T) astNode, const(VersionOrAttribute)[] attributes) {
3048 		this.astNode = astNode;
3049 		this.attributes = attributes;
3050 
3051 		static if(is(typeof(astNode.memberFunctionAttributes))) {
3052 			foreach(a; astNode.memberFunctionAttributes)
3053 				if(a !is null)
3054 				this.attributes ~= new MemberFakeAttribute(a);
3055 		}
3056 
3057 		static if(is(typeof(astNode) == const(ClassDeclaration)) || is(typeof(astNode) == const(InterfaceDeclaration))) {
3058 			if(astNode.baseClassList)
3059 			foreach(idx, baseClass; astNode.baseClassList.items) {
3060 				auto bc = toText(baseClass);
3061 				InheritanceResult ir = InheritanceResult(null, bc);
3062 				_inheritsFrom ~= ir;
3063 			}
3064 		}
3065 	}
3066 
3067 	static if(is(T == Module)) {
3068 		// this is so I can load this from the index... kinda a hack
3069 		// it should only be used in limited circumstances
3070 		private string _name;
3071 		private this(string name) {
3072 			this._name = name;
3073 			this.astNode = null;
3074 		}
3075 	}
3076 
3077 	override const(T) getAstNode() { return astNode; }
3078 	override int lineNumber() {
3079 		static if(__traits(compiles, astNode.name.line))
3080 			return cast(int) astNode.name.line;
3081 		else static if(__traits(compiles, astNode.line))
3082 			return cast(int) astNode.line;
3083 		else static if(__traits(compiles, astNode.declarators[0].name.line)) {
3084 			if(astNode.declarators.length)
3085 				return cast(int) astNode.declarators[0].name.line;
3086 		} else static if(is(typeof(astNode) == const(Module))) {
3087 			return 0;
3088 		} else static assert(0, typeof(astNode).stringof);
3089 		return 0;
3090 	}
3091 
3092 	override void writeTemplateConstraint(MyOutputRange output) {
3093 		static if(__traits(compiles, astNode.constraint)) {
3094 			if(astNode.constraint) {
3095 				auto f = new MyFormatter!(typeof(output))(output);
3096 				output.putTag("<div class=\"template-constraint\">");
3097 				f.format(astNode.constraint);
3098 				output.putTag("</div>");
3099 			}
3100 		}
3101 	}
3102 
3103 	override string name() {
3104 		static if(is(T == Constructor))
3105 			return "this";
3106 		else static if(is(T == Destructor))
3107 			return "~this";
3108 		else static if(is(T == Postblit))
3109 			return "this(this)";
3110 		else static if(is(T == Module))
3111 			return _name is null ? .format(astNode.moduleDeclaration.moduleName) : _name;
3112 		else static if(is(T == AnonymousEnumDeclaration))
3113 			{ assert(0); } // overridden above
3114 		else static if(is(T == AliasDeclaration))
3115 			{ assert(0); } // overridden above
3116 		else static if(is(T == VariableDeclaration))
3117 			{assert(0);} // not compiled, overridden above
3118 		else static if(is(T == ImportDeclaration))
3119 			{assert(0);} // not compiled, overridden above
3120 		else static if(is(T == MixinTemplateDeclaration)) {
3121 			return astNode.templateDeclaration.name.text;
3122 		} else static if(is(T == StructDeclaration) || is(T == UnionDeclaration))
3123 			if(astNode.name.text.length)
3124 				return astNode.name.text;
3125 			else
3126 				return "__anonymous";
3127 		else static if(is(T == TemplateMixinExpression)) {
3128 			return astNode.identifier.text.length ? astNode.identifier.text : "__anonymous";
3129 		} else
3130 			return astNode.name.text;
3131 	}
3132 
3133 	override string comment() {
3134 		static if(is(T == Module))
3135 			return astNode.moduleDeclaration.comment;
3136 		else {
3137 			if(isDitto()) {
3138 				auto ps = previousSibling;
3139 				while(ps && ps.rawComment.length == 0)
3140 					ps = ps.previousSibling;
3141 				return ps ? ps.comment : rawComment();
3142 			} else
3143 				return rawComment();
3144 		}
3145 	}
3146 
3147 	override void getAnnotatedPrototype(MyOutputRange) {}
3148 	override void getSimplifiedPrototype(MyOutputRange output) {
3149 		output.putTag("<span class=\"builtin-type\">");
3150 		output.put(declarationType());
3151 		output.putTag("</span>");
3152 		output.put(" ");
3153 
3154 		output.putTag("<span class=\"name\">");
3155 		output.put(this.name);
3156 		output.putTag("</span>");
3157 
3158 		static if(__traits(compiles, astNode.templateParameters)) {
3159 			if(astNode.templateParameters) {
3160 				output.putTag("<span class=\"template-params\">");
3161 				output.put(toText(astNode.templateParameters));
3162 				output.putTag("</span>");
3163 			}
3164 		}
3165 	}
3166 	override string declarationType() {
3167 		import std..string:toLower;
3168 		return toLower(typeof(this).stringof[0 .. $-4]);
3169 	}
3170 
3171 	override bool isDitto() {
3172 		static if(is(T == Module))
3173 			return false;
3174 		else {
3175 			import std..string;
3176 			auto l = strip(toLower(preprocessComment(rawComment, this)));
3177 			if(l.length && l[$-1] == '.')
3178 				l = l[0 .. $-1];
3179 			return l == "ditto";
3180 		}
3181 	}
3182 
3183 	override string rawComment() {
3184 		static if(is(T == Module))
3185 			return astNode.moduleDeclaration.comment;
3186 		else static if(is(T == MixinTemplateDeclaration))
3187 			return astNode.templateDeclaration.comment;
3188 		else
3189 			return astNode.comment;
3190 	}
3191 
3192 	override bool isDisabled() {
3193 		foreach(attribute; attributes)
3194 			if(attribute.attr && attribute.attr.atAttribute && attribute.attr.atAttribute.identifier.text == "disable")
3195 				return true;
3196 		static if(__traits(compiles, astNode.memberFunctionAttributes))
3197 		foreach(attribute; astNode.memberFunctionAttributes)
3198 			if(attribute && attribute.atAttribute && attribute.atAttribute.identifier.text == "disable")
3199 				return true;
3200 		return false;
3201 	}
3202 
3203 }
3204 
3205 ClassDecl[string] allClasses;
3206 
3207 class Looker : ASTVisitor {
3208 	alias visit = ASTVisitor.visit;
3209 
3210 	const(ubyte)[] fileBytes;
3211 	string originalFileName;
3212 	this(const(ubyte)[] fileBytes, string fileName) {
3213 		this.fileBytes = fileBytes;
3214 		this.originalFileName = fileName;
3215 	}
3216 
3217 	ModuleDecl root;
3218 
3219 
3220 	private Decl[] stack;
3221 
3222 	Decl previousSibling() {
3223 		auto s = stack[$-1];
3224 		if(s.children.length)
3225 			return s.children[$-1];
3226 		return s; // probably a documented unittest of the module itself
3227 	}
3228 
3229 	void visitInto(D, T)(const(T) t, bool keepAttributes = true) {
3230 		auto d = new D(t, attributes[$-1]);
3231 		stack[$-1].addChild(d);
3232 		stack ~= d;
3233 		if(!keepAttributes)
3234 			pushAttributes();
3235 		t.accept(this);
3236 		if(!keepAttributes)
3237 			popAttributes();
3238 		stack = stack[0 .. $-1];
3239 
3240 		static if(is(D == ClassDecl))
3241 			allClasses[d.name] = d;
3242 	}
3243 
3244 	override void visit(const Module mod) {
3245 		pushAttributes();
3246 
3247 		root = new ModuleDecl(mod, attributes[$-1]);
3248 		stack ~= root;
3249 		mod.accept(this);
3250 		assert(stack.length == 1);
3251 	}
3252 
3253 	override void visit(const FunctionDeclaration dec) {
3254 		stack[$-1].addChild(new FunctionDecl(dec, attributes[$-1]));
3255 	}
3256 	override void visit(const Constructor dec) {
3257 		stack[$-1].addChild(new ConstructorDecl(dec, attributes[$-1]));
3258 	}
3259 	override void visit(const TemplateMixinExpression dec) {
3260 		stack[$-1].addChild(new MixedInTemplateDecl(dec, attributes[$-1]));
3261 	}
3262 	override void visit(const Postblit dec) {
3263 		stack[$-1].addChild(new PostblitDecl(dec, attributes[$-1]));
3264 	}
3265 	override void visit(const Destructor dec) {
3266 		stack[$-1].addChild(new DestructorDecl(dec, attributes[$-1]));
3267 	}
3268 
3269 	override void visit(const StructDeclaration dec) {
3270 		visitInto!StructDecl(dec);
3271 	}
3272 	override void visit(const ClassDeclaration dec) {
3273 		visitInto!ClassDecl(dec);
3274 	}
3275 	override void visit(const UnionDeclaration dec) {
3276 		visitInto!UnionDecl(dec);
3277 	}
3278 	override void visit(const InterfaceDeclaration dec) {
3279 		visitInto!InterfaceDecl(dec);
3280 	}
3281 	override void visit(const TemplateDeclaration dec) {
3282 		visitInto!TemplateDecl(dec);
3283 	}
3284 	override void visit(const EponymousTemplateDeclaration dec) {
3285 		visitInto!EponymousTemplateDecl(dec);
3286 	}
3287 	override void visit(const MixinTemplateDeclaration dec) {
3288 		visitInto!MixinTemplateDecl(dec.templateDeclaration, false);
3289 	}
3290 	override void visit(const EnumDeclaration dec) {
3291 		visitInto!EnumDecl(dec);
3292 	}
3293 	override void visit(const AliasThisDeclaration dec) {
3294 		stack[$-1].aliasThisPresent = true;
3295 		stack[$-1].aliasThisToken = dec.identifier;
3296 		stack[$-1].aliasThisComment = dec.comment;
3297 	}
3298 	override void visit(const AnonymousEnumDeclaration dec) {
3299 		// we can't do anything with an empty anonymous enum, we need a name from somewhere
3300 		if(dec.members.length)
3301 			visitInto!AnonymousEnumDecl(dec);
3302 	}
3303 	override void visit(const VariableDeclaration dec) {
3304         	if (dec.autoDeclaration) {
3305 			foreach (idx, ident; dec.autoDeclaration.identifiers) {
3306 				stack[$-1].addChild(new VariableDecl(dec, ident, dec.autoDeclaration.initializers[idx], attributes[$-1], dec.isEnum));
3307 
3308 			}
3309 		} else
3310 		foreach (const Declarator d; dec.declarators) {
3311 			stack[$-1].addChild(new VariableDecl(d, dec, attributes[$-1]));
3312 
3313 			/*
3314 			if (variableDeclaration.type !is null)
3315 			{
3316 				auto f = new MyFormatter!(typeof(app))(app);
3317 				f.format(variableDeclaration.type);
3318 			}
3319 			output.putTag(app.data);
3320 			output.put(" ");
3321 			output.put(d.name.text);
3322 
3323 			comment.writeDetails(output);
3324 
3325 			writeToParentList("variable " ~ cast(string)app.data ~ " ", name, comment.synopsis, "variable");
3326 
3327 			ascendOutOf(name);
3328 			*/
3329 		}
3330 	}
3331 	override void visit(const AliasDeclaration dec) {
3332 		if(dec.initializers.length) { // alias a = b
3333 			foreach(init; dec.initializers)
3334 				stack[$-1].addChild(new AliasDecl(dec, init, attributes[$-1]));
3335 		} else { // alias b a;
3336 			// might include a type...
3337 			stack[$-1].addChild(new AliasDecl(dec, attributes[$-1]));
3338 		}
3339 	}
3340 
3341 	override void visit(const Unittest ut) {
3342 		//import std.stdio; writeln(fileBytes.length, " ", ut.blockStatement.startLocation, " ", ut.blockStatement.endLocation);
3343 		previousSibling.addUnittest(
3344 			ut,
3345 			fileBytes[ut.blockStatement.startLocation + 1 .. ut.blockStatement.endLocation], // trim off the opening and closing {}
3346 			ut.comment
3347 		);
3348 	}
3349 
3350 	override void visit(const ImportDeclaration id) {
3351 
3352 		bool isPublic = false;
3353 
3354 		foreach(a; attributes[$-1]) {
3355 			if (a.attr && isProtection(a.attr.attribute.type)) {
3356 				if(a.attr.attribute.type == tok!"public") {
3357 					isPublic = true;
3358 				} else {
3359 					isPublic = false;
3360 				}
3361 			}
3362 		}
3363 
3364 
3365 		void handleSingleImport(const SingleImport si) {
3366 			auto newName = si.rename.text;
3367 			auto oldName = "";
3368 			foreach(idx, ident; si.identifierChain.identifiers) {
3369 				if(idx)
3370 					oldName ~= ".";
3371 				oldName ~= ident.text;
3372 			}
3373 			stack[$-1].addImport(oldName, isPublic);
3374 			// FIXME: handle the rest like newName for the import lookups
3375 
3376 			auto nid = new ImportDecl(id, attributes[$-1]);
3377 			stack[$-1].addChild(nid);
3378 			nid.isPublic = isPublic;
3379 			nid.oldName = oldName;
3380 			nid.newName = newName;
3381 		}
3382 
3383 
3384 		foreach(si; id.singleImports) {
3385 			handleSingleImport(si);
3386 		}
3387 
3388 		if(id.importBindings && id.importBindings.singleImport)
3389 			handleSingleImport(id.importBindings.singleImport); // FIXME: handle bindings
3390 
3391 	}
3392 
3393 	override void visit(const StructBody sb) {
3394 		pushAttributes();
3395 		sb.accept(this);
3396 		popAttributes();
3397 	}
3398 
3399 	// FIXME ????
3400 	override void visit(const VersionCondition sb) {
3401 		import std.conv;
3402 		attributes[$-1] ~= new VersionFakeAttribute(toText(sb.token));
3403 		sb.accept(this);
3404 	}
3405 
3406 	override void visit(const BlockStatement bs) {
3407 		pushAttributes();
3408 		bs.accept(this);
3409 		popAttributes();
3410 	}
3411 
3412 	override void visit(const FunctionBody bs) {
3413 		// just skipping it hence the commented code below. not important to docs
3414 		/*
3415 		pushAttributes();
3416 		bs.accept(this);
3417 		popAttributes();
3418 		*/
3419 	}
3420 
3421 	override void visit(const ConditionalDeclaration bs) {
3422 		pushAttributes();
3423 		if(attributes.length >= 2)
3424 			attributes[$-1] = attributes[$-2]; // inherit from the previous scope here
3425 		size_t previousConditions;
3426 		if(bs.compileCondition) {
3427 			previousConditions = attributes[$-1].length;
3428 			bs.compileCondition.accept(this);
3429 		}
3430 		// WTF FIXME FIXME
3431 		// http://dpldocs.info/experimental-docs/asdf.bar.html
3432 
3433 		if(bs.trueDeclarations)
3434 			foreach(td; bs.trueDeclarations)
3435 				td.accept(this);
3436 
3437 		if(bs.falseDeclaration) {
3438 			auto slice = attributes[$-1][previousConditions .. $];
3439 			attributes[$-1] = attributes[$-1][0 .. previousConditions];
3440 			foreach(cond; slice)
3441 				attributes[$-1] ~= cond.invertedClone;
3442 			bs.falseDeclaration.accept(this);
3443 		}
3444 		popAttributes();
3445 	}
3446 
3447 	override void visit(const Declaration dec) {
3448 		auto originalAttributes = attributes[$ - 1];
3449 		foreach(a; dec.attributes)
3450 			attributes[$ - 1] ~= new VersionOrAttribute(a);
3451 		dec.accept(this);
3452 		if (dec.attributeDeclaration is null)
3453 			attributes[$ - 1] = originalAttributes;
3454 	}
3455 
3456 	override void visit(const AttributeDeclaration dec) {
3457 		attributes[$ - 1] ~= new VersionOrAttribute(dec.attribute);
3458 	}
3459 
3460 	void pushAttributes() {
3461 		attributes.length = attributes.length + 1;
3462 	}
3463 
3464 	void popAttributes() {
3465 		attributes = attributes[0 .. $ - 1];
3466 	}
3467 
3468 	const(VersionOrAttribute)[][] attributes;
3469 }
3470 
3471 
3472 string format(const IdentifierChain identifierChain) {
3473 	string r;
3474 	foreach(count, ident; identifierChain.identifiers) {
3475 		if (count) r ~= (".");
3476 		r ~= (ident.text);
3477 	}
3478 	return r;
3479 }
3480 
3481 import std.algorithm : startsWith, findSplitBefore;
3482 import std..string : strip;
3483 
3484 //Decl[][string] packages;
3485 __gshared static Object modulesByNameMonitor = new Object; // intentional CTF
3486 __gshared ModuleDecl[string] modulesByName;
3487 
3488 __gshared string specialPreprocessor;
3489 
3490 // simplified ".gitignore" processor
3491 final class GitIgnore {
3492 	string[] masks; // on each new dir, empty line is added to masks
3493 
3494 	void loadGlobalGitIgnore () {
3495 		import std.path;
3496 		import std.stdio;
3497 		try {
3498 			foreach (string s; File("~/.gitignore_global".expandTilde).byLineCopy) {
3499 				if (isComment(s)) continue;
3500 				masks ~= trim(s);
3501 			}
3502 		} catch (Exception e) {} // sorry
3503 		try {
3504 			foreach (string s; File("~/.adrdoxignore_global".expandTilde).byLineCopy) {
3505 				if (isComment(s)) continue;
3506 				masks ~= trim(s);
3507 			}
3508 		} catch (Exception e) {} // sorry
3509 	}
3510 
3511 	void loadGitIgnore (const(char)[] dir) {
3512 		import std.path;
3513 		import std.stdio;
3514 		masks ~= null;
3515 		try {
3516 			foreach (string s; File(buildPath(dir, ".gitignore").expandTilde).byLineCopy) {
3517 				if (isComment(s)) continue;
3518 				masks ~= trim(s);
3519 			}
3520 		} catch (Exception e) {} // sorry
3521 		try {
3522 			foreach (string s; File(buildPath(dir, ".adrdoxignore").expandTilde).byLineCopy) {
3523 				if (isComment(s)) continue;
3524 				masks ~= trim(s);
3525 			}
3526 		} catch (Exception e) {} // sorry
3527 	}
3528 
3529 	// unload latest gitignore
3530 	void unloadGitIgnore () {
3531 		auto ol = masks.length;
3532 		while (masks.length > 0 && masks[$-1] !is null) masks = masks[0..$-1];
3533 		if (masks.length > 0 && masks[$-1] is null) masks = masks[0..$-1];
3534 		if (masks.length != ol) {
3535 			//writeln("removed ", ol-masks.length, " lines");
3536 			masks.assumeSafeAppend; //hack!
3537 		}
3538 	}
3539 
3540 	bool match (string fname) {
3541 		import std.path;
3542 		import std.stdio;
3543 		if (masks.length == 0) return false;
3544 		//writeln("gitignore checking: <", fname, ">");
3545 
3546 		bool xmatch (string path, string mask) {
3547 			if (mask.length == 0 || path.length == 0) return false;
3548 			import std..string : indexOf;
3549 			if (mask.indexOf('/') < 0) return path.baseName.globMatch(mask);
3550 			int xpos = cast(int)path.length-1;
3551 			while (xpos >= 0) {
3552 				while (xpos > 0 && path[xpos] != '/') --xpos;
3553 				if (mask[0] == '/') {
3554 					if (xpos+1 < path.length && path[xpos+1..$].globMatch(mask)) return true;
3555 				} else {
3556 					if (path[xpos..$].globMatch(mask)) return true;
3557 				}
3558 				--xpos;
3559 			}
3560 			return false;
3561 		}
3562 
3563 		string curname = fname.baseName;
3564 		int pos = cast(int)masks.length-1;
3565 		// local dir matching
3566 		while (pos >= 0 && masks[pos] !is null) {
3567 			//writeln(" [", masks[pos], "]");
3568 			if (xmatch(curname, masks[pos])) {
3569 				//writeln("  LOCAL HIT: [", masks[pos], "]: <", curname, ">");
3570 				return true;
3571 			}
3572 			if (masks[pos][0] == '/' && xmatch(curname, masks[pos][1..$])) return true;
3573 			--pos;
3574 		}
3575 		curname = fname;
3576 		while (pos >= 0) {
3577 			if (masks[pos] !is null) {
3578 				//writeln(" [", masks[pos], "]");
3579 				if (xmatch(curname, masks[pos])) {
3580 					//writeln("  HIT: [", masks[pos], "]: <", curname, ">");
3581 					return true;
3582 				}
3583 			}
3584 			--pos;
3585 		}
3586 		return false;
3587 	}
3588 
3589 static:
3590 	inout(char)[] trim (inout(char)[] s) {
3591 		while (s.length > 0 && s[0] <= ' ') s = s[1..$];
3592 		while (s.length > 0 && s[$-1] <= ' ') s = s[0..$-1];
3593 		return s;
3594 	}
3595 
3596 	bool isComment (const(char)[] s) {
3597 		s = trim(s);
3598 		return (s.length == 0 || s[0] == '#');
3599 	}
3600 }
3601 
3602 
3603 string[] scanFiles (string basedir) {
3604 	import std.file : isDir;
3605 	import std.path;
3606 
3607 	if(basedir == "-")
3608 		return ["-"];
3609 
3610 	string[] res;
3611 
3612 	auto gi = new GitIgnore();
3613 	gi.loadGlobalGitIgnore();
3614 
3615 	void scanSubDir(bool checkdir=true) (string dir) {
3616 		import std.file;
3617 		static if (checkdir) {
3618 			string d = dir;
3619 			if (d.length > 1 && d[$-1] == '/') d = d[0..$-1];
3620 			if (gi.match(d)) {
3621 				//writeln("DIR SKIP: <", dir, ">");
3622 				return;
3623 			}
3624 		}
3625 		gi.loadGitIgnore(dir);
3626 		scope(exit) gi.unloadGitIgnore();
3627 		foreach (DirEntry de; dirEntries(dir, SpanMode.shallow)) {
3628 			try {
3629 				if (de.baseName.length == 0) continue; // just in case
3630 				if (de.baseName[0] == '.') continue; // skip hidden files
3631 				if (de.isDir) { scanSubDir(de.name); continue; }
3632 				if (!de.baseName.globMatch("*.d")) continue;
3633 				if (/*de.isFile &&*/ !gi.match(de.name)) {
3634 					//writeln(de.name);
3635 					res ~= de.name;
3636 				}
3637 			} catch (Exception e) {} // some checks (like `isDir`) can throw
3638 		}
3639 	}
3640 
3641 	basedir = basedir.expandTilde.absolutePath;
3642 	if (basedir.isDir) {
3643 		scanSubDir!false(basedir);
3644 	} else {
3645 		res ~= basedir;
3646 	}
3647 	return res;
3648 }
3649 
3650 void writeFile(string filename, string content, bool gzip) {
3651 	import std.zlib;
3652 	import std.file;
3653 
3654 	if(gzip) {
3655 		auto compress = new Compress(HeaderFormat.gzip);
3656 		auto data = compress.compress(content);
3657 		data ~= compress.flush();
3658 
3659 		std.file.write(filename ~ ".gz", data);
3660 	} else {
3661 		std.file.write(filename, content);
3662 	}
3663 }
3664 
3665 __gshared bool generatingSource;
3666 __gshared bool blogMode = false;
3667 
3668 int main(string[] args) {
3669 	import std.stdio;
3670 	import std.path : buildPath;
3671 	import std.getopt;
3672 
3673 	static import std.file;
3674 	LexerConfig config;
3675 	StringCache stringCache = StringCache(128);
3676 
3677 	config.stringBehavior = StringBehavior.source;
3678 	config.whitespaceBehavior = WhitespaceBehavior.include;
3679 
3680 	ModuleDecl[] moduleDecls;
3681 	ModuleDecl[] moduleDeclsGenerate;
3682 	ModuleDecl[string] moduleDeclsGenerateByName;
3683 
3684 	bool makeHtml = true;
3685 	bool makeSearchIndex = false;
3686 	string postgresConnectionString = null;
3687 	string postgresVersionId = null;
3688 
3689 	string[] preloadArgs;
3690 
3691 	string[] linkReferences;
3692 
3693 	bool annotateSource = false;
3694 
3695 	string locateSymbol = null;
3696 	bool gzip;
3697 	bool copyStandardFiles = true;
3698 	string headerTitle;
3699 
3700 	string texMath = "latex";
3701 
3702 	string[] headerLinks;
3703 	HeaderLink[] headerLinksParsed;
3704 
3705 	bool skipExisting = false;
3706 
3707 	string[] globPathInput;
3708 	string dataDirPath;
3709 
3710 	int jobs = 0;
3711 	
3712 	auto opt = getopt(args,
3713 		std.getopt.config.passThrough,
3714 		std.getopt.config.bundling,
3715 		"load", "Load for automatic cross-referencing, but do not generate for it", &preloadArgs,
3716 		"link-references", "A file defining global link references", &linkReferences,
3717 		"skeleton|s", "Location of the skeleton file, change to your use case, Default: skeleton.html", &skeletonFile,
3718 		"directory|o", "Output directory of the html files", &outputDirectory,
3719 		"write-private-docs|p", "Include documentation for `private` members (default: false)", &writePrivateDocs,
3720 		"write-internal-modules", "Include documentation for modules named `internal` (default: false)", &documentInternal,
3721 		"locate-symbol", "Locate a symbol in the passed file", &locateSymbol,
3722 		"genHtml|h", "Generate html, default: true", &makeHtml,
3723 		"genSource|u", "Generate annotated source", &annotateSource,
3724 		"genSearchIndex|i", "Generate search index, default: false", &makeSearchIndex,
3725 		"postgresConnectionString", "Specify the postgres database to save search index to. If specified, you must also specify postgresVersionId", &postgresConnectionString,
3726 		"postgresVersionId", "Specify the version_id to associate saved terms to. If specified, you must also specify postgresConnectionString", &postgresVersionId,
3727 		"gzip|z", "Gzip generated files as they are created", &gzip,
3728 		"copy-standard-files", "Copy standard JS/CSS files into target directory (default: true)", &copyStandardFiles,
3729 		"blog-mode", "Use adrdox as a static site generator for a blog", &blogMode,
3730 		"header-title", "Title to put on the page header", &headerTitle,
3731 		"header-link", "Link to add to the header (text=url)", &headerLinks,
3732 		"document-undocumented", "Generate documentation even for undocumented symbols", &documentUndocumented,
3733 		"minimal-descent", "Performs minimal descent into generating sub-pages", &minimalDescent,
3734 		"case-insensitive-filenames", "Adjust generated filenames for case-insensitive file systems", &caseInsensitiveFilenames,
3735 		"skip-existing", "Skip file generation for modules where the html already exists in the output dir", &skipExisting,
3736 		"tex-math", "How TeX math should be processed (latex|katex, default=latex)", &texMath,
3737 		"special-preprocessor", "Run a special preprocessor on comments. Only supported right now are gtk and dwt", &specialPreprocessor,
3738 		"jobs|j", "Number of generation jobs to run at once (default=dependent on number of cpu cores", &jobs,
3739 		"package-path", "Path to be prefixed to links for a particular D package namespace (package_pattern=link_prefix)", &globPathInput,
3740 		"data-dir", "Path to directory containing standard files (default=detect automatically)", &dataDirPath);
3741 
3742 	if (opt.helpWanted || args.length == 1) {
3743 		defaultGetoptPrinter("A better D documentation generator\nCopyright © Adam D. Ruppe 2016-2020\n" ~
3744 			"Syntax: " ~ args[0] ~ " /path/to/your/package\n", opt.options);
3745 		return 0;
3746 	}
3747 
3748 	PostgreSql searchDb;
3749 
3750 	if(postgresVersionId.length || postgresConnectionString.length) {
3751 		import std.stdio;
3752 		version(with_postgres) {
3753 			if(postgresVersionId.length == 0) {
3754 				stderr.writeln("Required command line option `postgresVersionId` not set.");
3755 				return 1;
3756 			}
3757 			if(postgresConnectionString.length == 0) {
3758 				stderr.writeln("Required command line option `postgresConnectionString` not set. It must minimally reference an existing database like \"dbname=adrdox\".");
3759 				return 1;
3760 			}
3761 
3762 			searchDb = new PostgreSql(postgresConnectionString);
3763 
3764 			try {
3765 				foreach(res; searchDb.query("SELECT schema_version FROM adrdox_schema")) {
3766 					if(res[0] != "1") {
3767 						stderr.writeln("Unsupported adrdox_schema version. Maybe update your adrdox?");
3768 						return 1;
3769 					}
3770 				}
3771 			} catch(DatabaseException e) {
3772 				// probably table not existing, let's try to create it.
3773 				try {
3774 					searchDb.query(import("db.sql"));
3775 				} catch(Exception e) {
3776 					stderr.writeln("Database schema check failed: ", e.msg);
3777 					stderr.writeln("Maybe try recreating the database and/or ensuring your user has full access.");
3778 					return 1;
3779 				}
3780 			}
3781 
3782 			if(postgresVersionId == "auto") {
3783 				// automatically determine one based on each file name; deferred for later.
3784 				// FIXME
3785 			} else {
3786 				bool found = false;
3787 				foreach(res; searchDb.query("SELECT id FROM package_version WHERE id = ?", postgresVersionId))
3788 					found = true;
3789 
3790 				if(!found) {
3791 					stderr.writeln("package_version ID ", postgresVersionId, " does not exist in the database");
3792 					return 1;
3793 				}
3794 
3795 				// delete the existing stuff so we do a full update in this run
3796 				searchDb.query("DELETE FROM auto_generated_tags WHERE package_version_id = ?", postgresVersionId);
3797 			}
3798 
3799 		} else {
3800 			stderr.writeln("PostgreSql support not compiled in. Recompile adrdox with -version=with_postgres and try again.");
3801 			return 1;
3802 		}
3803 	}
3804 
3805 	foreach(gpi; globPathInput) {
3806 		auto idx = gpi.indexOf("=");
3807 		string pathGlob;
3808 		string dir;
3809 		if(idx != -1) {
3810 			pathGlob = gpi[0 .. idx];
3811 			dir = gpi[idx + 1 .. $];
3812 		} else {
3813 			pathGlob = gpi;
3814 		}
3815 
3816 		synchronized(directoriesForPackageMonitor)
3817 		directoriesForPackage[pathGlob] = dir;
3818 	}
3819 
3820 	if (checkDataDirectory(dataDirPath)) {
3821 		// use data direcotory from command-line
3822 		dataDirectory = dataDirPath;
3823 	} else {
3824 		import std.process: environment;
3825 
3826 		if (dataDirPath.length > 0) {
3827 			writeln("Invalid data directory given from command line: " ~ dataDirPath);
3828 		}
3829 
3830 		// try get data directory from environment
3831 		dataDirPath = environment.get("ADRDOX_DATA_DIR");
3832 
3833 		if (checkDataDirectory(dataDirPath)) {
3834 			// use data directory from environment
3835 			dataDirectory = dataDirPath;
3836 		} else {
3837 			if (dataDirPath.length > 0) {
3838 				writeln("Invalid data directory given from environment variable: " ~ dataDirPath);
3839 			}
3840 
3841 			// try detect data directory automatically
3842 			if (!detectDataDirectory(dataDirectory)) {
3843 				throw new Exception("Unable to determine data directory.");
3844 			}
3845 		}
3846 	}
3847 
3848 	generatingSource = annotateSource;
3849 
3850 	if (outputDirectory[$-1] != '/')
3851 		outputDirectory ~= '/';
3852 
3853 	if (opt.helpWanted || args.length == 1) {
3854 		defaultGetoptPrinter("A better D documentation generator\nCopyright © Adam D. Ruppe 2016-2018\n" ~
3855 			"Syntax: " ~ args[0] ~ " /path/to/your/package\n", opt.options);
3856 		return 0;
3857 	}
3858 
3859 	texMathOpt = parseTexMathOpt(texMath);
3860 
3861 	foreach(l; headerLinks) {
3862 		auto idx = l.indexOf("=");
3863 		if(idx == -1)
3864 			continue;
3865 
3866 		HeaderLink lnk;
3867 		lnk.text = l[0 .. idx].strip;
3868 		lnk.url = l[idx + 1 .. $].strip;
3869 
3870 		headerLinksParsed ~= lnk;
3871 	}
3872 
3873 	if(locateSymbol is null) {
3874 		import std.file;
3875 
3876 		if (!exists(skeletonFile) && findStandardFile!false("skeleton-default.html").length)
3877 			copyStandardFileTo!false(skeletonFile, "skeleton-default.html");
3878 
3879 		if (!exists(outputDirectory))
3880 			mkdir(outputDirectory);
3881 
3882 		if(copyStandardFiles) {
3883 			copyStandardFileTo(outputFilePath("style.css"), "style.css");
3884 			copyStandardFileTo(outputFilePath("script.js"), "script.js");
3885 			copyStandardFileTo(outputFilePath("search-docs.js"), "search-docs.js");
3886 
3887 			switch (texMathOpt) with (TexMathOpt) {
3888 				case KaTeX: {
3889 					import adrdox.jstex;
3890 					foreach (file; filesForKaTeX) {
3891 						copyStandardFileTo(outputFilePath(file), "katex/" ~ file);
3892 					}
3893 					break;
3894 				}
3895 				default: break;
3896 			}
3897 		}
3898 	}
3899 
3900 	// FIXME: maybe a zeroth path just grepping for a module declaration in located files
3901 	// and making a mapping of module names, package listing, and files.
3902 	// cuz reading all of Phobos takes several seconds. Then they can parse it fully lazily.
3903 
3904 	static void generateAnnotatedSource(ModuleDecl mod, bool gzip) {
3905 		import std.file;
3906 		auto annotatedSourceDocument = new Document();
3907 		annotatedSourceDocument.parseUtf8(readText(skeletonFile), true, true);
3908 
3909 		string fixupLink(string s) {
3910 			if(!s.startsWith("http") && !s.startsWith("/"))
3911 				return "../" ~ s;
3912 			return s;
3913 		}
3914 
3915 		foreach(ele; annotatedSourceDocument.querySelectorAll("a, link, script[src], form"))
3916 			if(ele.tagName == "link")
3917 				ele.attrs.href = "../" ~ ele.attrs.href;
3918 			else if(ele.tagName == "form")
3919 				ele.attrs.action = "../" ~ ele.attrs.action;
3920 			else if(ele.tagName == "a")
3921 				ele.attrs.href = fixupLink(ele.attrs.href);
3922 			else
3923 				ele.attrs.src = "../" ~ ele.attrs.src;
3924 
3925 		auto code = Element.make("pre", Html(linkUpHtml(highlight(cast(string) mod.originalSource), mod, "../", true))).addClass("d_code highlighted");
3926 		addLineNumbering(code, true);
3927 		auto content = annotatedSourceDocument.requireElementById("page-content");
3928 		content.addChild(code);
3929 
3930 		auto nav = annotatedSourceDocument.requireElementById("page-nav");
3931 
3932 		void addDeclNav(Element nav, Decl decl) {
3933 			auto li = nav.addChild("li");
3934 			if(decl.docsShouldBeOutputted)
3935 				li.addChild("a", "[Docs] ", fixupLink(decl.link)).addClass("docs");
3936 			li.addChild("a", decl.name, "#L" ~ to!string(decl.lineNumber == 0 ? 1 : decl.lineNumber));
3937 			if(decl.children.length)
3938 				nav = li.addChild("ul");
3939 			foreach(child; decl.children)
3940 				addDeclNav(nav, child);
3941 
3942 		}
3943 
3944 		auto sn = nav.addChild("div").setAttribute("id", "source-navigation");
3945 
3946 		addDeclNav(sn.addChild("div").addClass("list-holder").addChild("ul"), mod);
3947 
3948 		annotatedSourceDocument.title = mod.name ~ " source code";
3949 
3950 		auto outputSourcePath = outputFilePath("source");
3951 		if(!usePseudoFiles && !outputSourcePath.exists)
3952 			mkdir(outputSourcePath);
3953 		if(usePseudoFiles)
3954 			pseudoFiles["source/" ~ mod.name ~ ".d.html"] = annotatedSourceDocument.toString();
3955 		else
3956 			writeFile(outputFilePath("source", mod.name ~ ".d.html"), annotatedSourceDocument.toString(), gzip);
3957 	}
3958 
3959 	void process(string arg, bool generate) {
3960 		try {
3961 			if(locateSymbol is null)
3962 			writeln("First pass processing ", arg);
3963 			import std.file;
3964 			ubyte[] b;
3965 
3966 			if(arg == "-") {
3967 				foreach(chunk; stdin.byChunk(4096))
3968 					b ~= chunk;
3969 			} else
3970 				b = cast(ubyte[]) read(arg);
3971 
3972 			config.fileName = arg;
3973 			auto tokens = getTokensForParser(b, config, &stringCache);
3974 
3975 			import std.path : baseName;
3976 			auto m = parseModule(tokens, baseName(arg));
3977 
3978 			auto sweet = new Looker(b, baseName(arg));
3979 			sweet.visit(m);
3980 
3981 			ModuleDecl existingDecl;
3982 
3983 			auto mod = cast(ModuleDecl) sweet.root;
3984 
3985 			{
3986 				mod.originalSource = b;
3987 				if(mod.astNode.moduleDeclaration is null)
3988 					throw new Exception("you must have a module declaration for this to work on it");
3989 
3990 				if(b.startsWith(cast(ubyte[])"// just docs:"))
3991 					sweet.root.justDocsTitle = (cast(string) b["// just docs:".length .. $].findSplitBefore(['\n'])[0].idup).strip;
3992 
3993 				synchronized(modulesByNameMonitor) {
3994 					if(sweet.root.name !in modulesByName) {
3995 						moduleDecls ~= mod;
3996 						existingDecl = mod;
3997 
3998 						assert(mod !is null);
3999 						modulesByName[sweet.root.name] = mod;
4000 					} else {
4001 						existingDecl = modulesByName[sweet.root.name];
4002 					}
4003 				}
4004 			}
4005 
4006 			if(generate) {
4007 
4008 				if(sweet.root.name !in moduleDeclsGenerateByName) {
4009 					moduleDeclsGenerateByName[sweet.root.name] = existingDecl;
4010 					moduleDeclsGenerate ~= existingDecl;
4011 
4012 					if(generatingSource) {
4013 						generateAnnotatedSource(mod, gzip);
4014 					}
4015 				}
4016 			}
4017 
4018 			//packages[sweet.root.packageName] ~= sweet.root;
4019 
4020 
4021 		} catch (Throwable t) {
4022 			writeln(t.toString());
4023 		}
4024 	}
4025 
4026 	args = args[1 .. $]; // remove program name
4027 
4028 	foreach(arg; linkReferences) {
4029 		import std.file;
4030 		loadGlobalLinkReferences(readText(arg));
4031 	}
4032 
4033 	string[] generateFiles;
4034 	foreach (arg; args) generateFiles ~= scanFiles(arg);
4035 	/*
4036 	foreach(argIdx, arg; args) {
4037 		if(arg != "-" && std.file.isDir(arg))
4038 			foreach(string name; std.file.dirEntries(arg, "*.d", std.file.SpanMode.breadth))
4039 				generateFiles ~= name;
4040 		else
4041 			generateFiles ~= arg;
4042 	}
4043 	*/
4044 	args = generateFiles;
4045 	//{ import std.stdio; foreach (fn; args) writeln(fn); } assert(0);
4046 
4047 
4048 	// Process them all first so name-lookups have more chance of working
4049 	foreach(argIdx, arg; preloadArgs) {
4050 		if(std.file.isDir(arg)) {
4051 			foreach(string name; std.file.dirEntries(arg, "*.d", std.file.SpanMode.breadth)) {
4052 				bool g = false;
4053 				if(locateSymbol is null)
4054 				foreach(idx, a; args) {
4055 					if(a == name) {
4056 						g = true;
4057 						args[idx] = args[$-1];
4058 						args = args[0 .. $-1];
4059 						break;
4060 					}
4061 				}
4062 
4063 				process(name, g);
4064 			}
4065 		} else {
4066 			bool g = false;
4067 
4068 			if(locateSymbol is null)
4069 			foreach(idx, a; args) {
4070 				if(a == arg) {
4071 					g = true;
4072 					args[idx] = args[$-1];
4073 					args = args[0 .. $-1];
4074 					break;
4075 				}
4076 			}
4077 
4078 			process(arg, g);
4079 		}
4080 	}
4081 
4082 	foreach(argIdx, arg; args) {
4083 		process(arg, locateSymbol is null ? true : false);
4084 	}
4085 
4086 	if(locateSymbol !is null) {
4087 		auto decl = moduleDecls[0].lookupName(locateSymbol);
4088 		if(decl is null)
4089 			writeln("not found ", locateSymbol);
4090 		else
4091 			writeln(decl.lineNumber);
4092 		return 0;
4093 	}
4094 
4095 	// create dummy packages for those not found in the source
4096 	// this makes linking far more sane, without requiring package.d
4097 	// everywhere (though I still strongly recommending you write them!)
4098 		// I'm using for instead of foreach so I can append in the loop
4099 		// and keep it going
4100 	for(size_t i = 0; i < moduleDecls.length; i++ ) {
4101 		auto decl = moduleDecls[i];
4102 		auto pkg = decl.packageName;
4103 		if(decl.name == "index")
4104 			continue; // avoid infinite recursion
4105 		if(pkg is null)
4106 			pkg = "index";//continue; // to create an index.html listing all top level things
4107 		synchronized(modulesByNameMonitor)
4108 		if(pkg !in modulesByName) {
4109 			writeln("Making FAKE package for ", pkg);
4110 			config.fileName = "dummy";
4111 			auto b = cast(ubyte[]) (`/++
4112 			+/ module `~pkg~`; `);
4113 			auto tokens = getTokensForParser(b, config, &stringCache);
4114 			auto m = parseModule(tokens, "dummy");
4115 			auto sweet = new Looker(b, "dummy");
4116 			sweet.visit(m);
4117 
4118 			auto mod = cast(ModuleDecl) sweet.root;
4119 
4120 			mod.fakeDecl = true;
4121 
4122 			moduleDecls ~= mod;
4123 			modulesByName[pkg] = mod;
4124 
4125 			// only generate a fake one if the real one isn't already there
4126 			// like perhaps the real one was generated before but just not loaded
4127 			// this time.
4128 			if(!std.file.exists(outputFilePath(mod.link)))
4129 				moduleDeclsGenerate ~= mod;
4130 		}
4131 	}
4132 
4133 	// add modules to their packages, if possible
4134 	foreach(decl; moduleDecls) {
4135 		auto pkg = decl.packageName;
4136 		if(decl.name == "index") continue; // avoid infinite recursion
4137 		if(pkg.length == 0) {
4138 			//continue;
4139 			pkg = "index";
4140 		}
4141 		synchronized(modulesByNameMonitor)
4142 		if(auto a = pkg in modulesByName) {
4143 			(*a).addChild(decl);
4144 		} else assert(0, pkg ~ " " ~ decl.toString); // it should have make a fake package above
4145 	}
4146 
4147 
4148 	version(with_http_server) {
4149 		import arsd.cgi;
4150 
4151 		void serveFiles(Cgi cgi) {
4152 
4153 			import std.file;
4154 
4155 			string file = cgi.requestUri;
4156 
4157 			auto slash = file.lastIndexOf("/");
4158 			bool wasSource = file.indexOf("source/") != -1;
4159 
4160 			if(slash != -1)
4161 				file = file[slash + 1 .. $];
4162 
4163 			if(wasSource)
4164 				file = "source/" ~ file;
4165 
4166 			if(file == "style.css") {
4167 				cgi.setResponseContentType("text/css");
4168 				cgi.write(readText(findStandardFile("style.css")), true);
4169 				return;
4170 			} else if(file == "script.js") {
4171 				cgi.setResponseContentType("text/javascript");
4172 				cgi.write(readText(findStandardFile("script.js")), true);
4173 				return;
4174 			} else if(file == "search-docs.js") {
4175 				cgi.setResponseContentType("text/javascript");
4176 				cgi.write(readText(findStandardFile("search-docs.js")), true);
4177 				return;
4178 			} else {
4179 				if(file.length == 0) {
4180 					if("index" !in pseudoFiles)
4181 						writeHtml(modulesByName["index"], true, false, headerTitle, headerLinksParsed);
4182 					cgi.write(pseudoFiles["index"], true);
4183 					return;
4184 				} else {
4185 					auto of = file;
4186 
4187 					if(file !in pseudoFiles) {
4188 						ModuleDecl* declPtr;
4189 						file = file[0 .. $-5]; // cut off ".html"
4190 						if(wasSource) {
4191 							file = file["source/".length .. $];
4192 							file = file[0 .. $-2]; // cut off ".d"
4193 						}
4194 						while((declPtr = file in modulesByName) is null) {
4195 							auto idx = file.lastIndexOf(".");
4196 							if(idx == -1)
4197 								break;
4198 							file = file[0 .. idx];
4199 						}
4200 
4201 						if(declPtr !is null) {
4202 							if(wasSource) {
4203 								generateAnnotatedSource(*declPtr, false);
4204 							} else {
4205 								if(!(*declPtr).alreadyGenerated)
4206 									writeHtml(*declPtr, true, false, headerTitle, headerLinksParsed);
4207 								(*declPtr).alreadyGenerated = true;
4208 							}
4209 						}
4210 					}
4211 
4212 					file = of;
4213 
4214 					if(file in pseudoFiles)
4215 						cgi.write(pseudoFiles[file], true);
4216 					else {
4217 						cgi.setResponseStatus("404 Not Found");
4218 						cgi.write("404 " ~ file, true);
4219 					}
4220 					return;
4221 				}
4222 			}
4223 
4224 			cgi.setResponseStatus("404 Not Found");
4225 			cgi.write("404", true);
4226 		}
4227 
4228 		mixin CustomCgiMain!(Cgi, serveFiles);
4229 
4230 		processPoolSize = 1;
4231 
4232 		usePseudoFiles = true;
4233 
4234 		writeln("\n\nListening on http port 8999....");
4235 
4236 		cgiMainImpl(["server", "--port", "8999"]);
4237 		return 0;
4238 	}
4239 
4240 	import std.parallelism;
4241 	if(jobs > 1)
4242 	defaultPoolThreads = jobs;
4243 
4244 	version(linux)
4245 	if(makeSearchIndex && makeHtml) {
4246 		import core.sys.posix.unistd;
4247 		if(fork()) {
4248 			makeSearchIndex = false; // this fork focuses on html
4249 			//mustWait = true;
4250 		} else {
4251 			makeHtml = false; // and this one does the search
4252 		}
4253 	}
4254 
4255 	if(makeHtml) {
4256 		bool[string] alreadyTried;
4257 
4258 		void helper(size_t idx, ModuleDecl decl) {
4259 			//if(decl.parent && moduleDeclsGenerate.canFind(decl.parent))
4260 				//continue; // it will be written in the list of children. actually i want to do it all here.
4261 
4262 			// FIXME: make search index in here if we can
4263 			if(!skipExisting || !std.file.exists(outputFilePath(decl.link(true) ~ (gzip ?".gz":"")))) {
4264 				if(decl.name in alreadyTried)
4265 					return;
4266 				alreadyTried[decl.name] = true;
4267 				writeln("Generating HTML for ", decl.name);
4268 				writeHtml(decl, true, gzip, headerTitle, headerLinksParsed);
4269 			}
4270 
4271 			writeln(idx + 1, "/", moduleDeclsGenerate.length, " completed");
4272 		}
4273 
4274 		if(jobs == 1)
4275 		foreach(idx, decl; moduleDeclsGenerate) {
4276 			helper(idx, decl);
4277 		}
4278 		else
4279 		foreach(idx, decl; parallel(moduleDeclsGenerate)) {
4280 			helper(idx, decl);
4281 		}
4282 	}
4283 
4284 	if(makeSearchIndex) {
4285 
4286 		// we need the listing and the search index
4287 		FileProxy index;
4288 		int id;
4289 
4290 		static import std.file;
4291 
4292 		// write out the landing page for JS search,
4293 		// see the comment in the source of that html
4294 		// for more details
4295 		auto searchDocsHtml = std.file.readText(findStandardFile("search-docs.html"));
4296 		writeFile(outputFilePath("search-docs.html"), searchDocsHtml, gzip);
4297 
4298 
4299 		// the search index is a HTML page containing some script
4300 		// and the index XML. See the source of search-docs.js for more info.
4301 		index = FileProxy(outputFilePath("search-results.html"), gzip);
4302 
4303 		auto skeletonDocument = new Document();
4304 		skeletonDocument.parseUtf8(std.file.readText(skeletonFile), true, true);
4305 		auto skeletonText = skeletonDocument.toString();
4306 
4307 		auto idx = skeletonText.indexOf("</body>");
4308 		if(idx == -1) throw new Exception("skeleton missing body element");
4309 
4310 		// write out the skeleton...
4311 		index.writeln(skeletonText[0 .. idx]);
4312 
4313 		// and then the data container for the xml
4314 		index.writeln(`<script type="text/xml" id="search-index-container">`);
4315 
4316 		index.writeln("<adrdox>");
4317 
4318 		index.writeln("<listing>");
4319 		foreach(decl; moduleDeclsGenerate) {
4320 			if(decl.fakeDecl)
4321 				continue;
4322 			writeln("Listing ", decl.name);
4323 
4324 			writeIndexXml(decl, index, id, postgresVersionId, searchDb);
4325 		}
4326 		index.writeln("</listing>");
4327 
4328 		// also making the search index
4329 		foreach(decl; moduleDeclsGenerate) {
4330 			if(decl.fakeDecl)
4331 				continue;
4332 			writeln("Generating search for ", decl.name);
4333 
4334 			generateSearchIndex(decl, searchDb);
4335 		}
4336 
4337 		writeln("Writing search...");
4338 
4339 		if(searchDb)
4340 			searchDb.flushSearchDatabase();
4341 
4342 		index.writeln("<index>");
4343 		foreach(term, arr; searchTerms) {
4344 			index.write("<term value=\""~xmlEntitiesEncode(term)~"\">");
4345 			foreach(item; arr) {
4346 				index.write("<result decl=\""~to!string(item.declId)~"\" score=\""~to!string(item.score)~"\" />");
4347 			}
4348 			index.writeln("</term>");
4349 		}
4350 		index.writeln("</index>");
4351 		index.writeln("</adrdox>");
4352 
4353 		// finish the container
4354 		index.writeln("</script>");
4355 
4356 		// write the script that runs the search
4357 		index.writeln("<script src=\"search-docs.js\"></script>");
4358 
4359 		// and close the skeleton
4360 		index.writeln("</body></html>");
4361 		index.close();
4362 	}
4363 
4364 
4365 	//import std.stdio;
4366 	//writeln("press any key to continue");
4367 	//readln();
4368 
4369 	return 0;
4370 }
4371 
4372 struct FileProxy {
4373 	import std.zlib;
4374 	File f; // it will inherit File's refcounting
4375 	Compress compress; // and compress is gc'd anyway so copying the ref means same object!
4376 	bool gzip;
4377 
4378 	this(string filename, bool gzip) {
4379 		f = File(filename ~ (gzip ? ".gz" : ""), gzip ? "wb" : "wt");
4380 		if(gzip)
4381 			compress = new Compress(HeaderFormat.gzip);
4382 		this.gzip = gzip;
4383 	}
4384 
4385 	void writeln(string s) {
4386 		if(gzip)
4387 			f.rawWrite(compress.compress(s ~ "\n"));
4388 		else
4389 			f.writeln(s);
4390 	}
4391 
4392 	void write(string s) {
4393 		if(gzip)
4394 			f.rawWrite(compress.compress(s));
4395 		else
4396 			f.write(s);
4397 	}
4398 
4399 	void close() {
4400 		if(gzip)
4401 			f.rawWrite(compress.flush());
4402 		f.close();
4403 	}
4404 }
4405 
4406 struct SearchResult {
4407 	int declId;
4408 	int score;
4409 }
4410 
4411 string[] splitIdentifier(string name) {
4412 	string[] ret;
4413 
4414 	bool isUpper(dchar c) {
4415 		return c >= 'A' && c <= 'Z';
4416 	}
4417 
4418 	bool breakOnNext;
4419 	dchar lastChar;
4420 	foreach(dchar ch; name) {
4421 		if(ch == '_') {
4422 			breakOnNext = true;
4423 			continue;
4424 		}
4425 		if(breakOnNext || ret.length == 0 || (isUpper(ch) && !isUpper(lastChar))) {
4426 			if(ret.length == 0 || ret[$-1].length)
4427 				ret ~= "";
4428 		}
4429 		breakOnNext = false;
4430 		ret[$-1] ~= ch;
4431 		lastChar = ch;
4432 	}
4433 
4434 	return ret;
4435 }
4436 
4437 SearchResult[][string] searchTerms;
4438 string searchInsertToBeFlushed;
4439 string postgresVersionIdGlobal; // total hack!!!
4440 
4441 void saveSearchTerm(PostgreSql searchDb, string term, SearchResult sr, bool isDeep = false) {
4442 	if(searchDb is null || !isDeep) {
4443 		searchTerms[term] ~= sr; // save the things for offline xml too
4444 	}
4445 	version(with_postgres) {
4446 		if(searchDb !is null) {
4447 			if(searchInsertToBeFlushed.length > 4096)
4448 				searchDb.flushSearchDatabase();
4449 
4450 			if(searchInsertToBeFlushed.length)
4451 				searchInsertToBeFlushed ~= ", ";
4452 			searchInsertToBeFlushed ~= "('";
4453 			searchInsertToBeFlushed ~= searchDb.escape(term);
4454 			searchInsertToBeFlushed ~= "', ";
4455 			searchInsertToBeFlushed ~= to!string(sr.declId);
4456 			searchInsertToBeFlushed ~= ", ";
4457 			searchInsertToBeFlushed ~= to!string(sr.score);
4458 			searchInsertToBeFlushed ~= ", ";
4459 			searchInsertToBeFlushed ~= to!string(postgresVersionIdGlobal);
4460 			searchInsertToBeFlushed ~= ")";
4461 		}
4462 	}
4463 }
4464 
4465 void flushSearchDatabase(PostgreSql searchDb) {
4466 	if(searchDb is null)
4467 		return;
4468 	else version(with_postgres) {
4469 		if(searchInsertToBeFlushed.length) {
4470 			searchDb.query("INSERT INTO auto_generated_tags (tag, d_symbols_id, score, package_version_id) VALUES " ~ searchInsertToBeFlushed);
4471 
4472 			searchInsertToBeFlushed = searchInsertToBeFlushed[$..$];
4473 			//searchInsertToBeFlushed.assumeSafeAppend;
4474 		}
4475 	}
4476 }
4477 
4478 void generateSearchIndex(Decl decl, PostgreSql searchDb) {
4479 	/*
4480 	if((*cast(void**) decl) is null)
4481 		return;
4482 	scope(exit) {
4483 		(cast(ubyte*) decl)[0 .. typeid(decl).initializer.length] = 0;
4484 	}
4485 	*/
4486 
4487 	if(decl.databaseId == 0)
4488 		return;
4489 
4490 	if(!decl.docsShouldBeOutputted)
4491 		return;
4492 	if(cast(ImportDecl) decl)
4493 		return; // never write imports, it can overwrite the actual thing
4494 
4495 	// this needs to match the id in index.xml!
4496 	const tid = decl.databaseId;
4497 
4498 // FIXME: if it is undocumented in source, give it a score penalty.
4499 
4500 	// exact match on FQL is always a great match
4501 	searchDb.saveSearchTerm(decl.fullyQualifiedName, SearchResult(tid, 50));
4502 
4503 	// names like GC.free should be a solid match too
4504 
4505 	string partialName;
4506 	if(!decl.isModule)
4507 		partialName = decl.fullyQualifiedName[decl.parentModule.name.length + 1 .. $];
4508 
4509 	if(partialName.length)
4510 	searchDb.saveSearchTerm(partialName, SearchResult(tid, 35));
4511 
4512 	if(decl.name != "this") {
4513 		// exact match on specific name is worth something too
4514 		searchDb.saveSearchTerm(decl.name, SearchResult(tid, 25));
4515 
4516 		if(decl.isModule) {
4517 			// module names like std.stdio should match stdio strongly,
4518 			// and std is ok too. I will break them by dot and give diminsihing
4519 			// returns.
4520 			int score = 25;
4521 			foreach_reverse(part; decl.name.split(".")) {
4522 				searchDb.saveSearchTerm(part, SearchResult(tid, score));
4523 				score -= 10;
4524 				if(score <= 0)
4525 					break;
4526 			}
4527 		}
4528 
4529 		// and so is fuzzy match
4530 		if(decl.name != decl.name.toLower) {
4531 			searchDb.saveSearchTerm(decl.name.toLower, SearchResult(tid, 15));
4532 		}
4533 		if(partialName.length && partialName != decl.name)
4534 		if(partialName != partialName.toLower)
4535 			searchDb.saveSearchTerm(partialName.toLower, SearchResult(tid, 20));
4536 
4537 		// and so is partial word match
4538 		auto splitNames = splitIdentifier(decl.name);
4539 		if(splitNames.length) {
4540 			foreach(name; splitNames) {
4541 				searchDb.saveSearchTerm(name, SearchResult(tid, 6));
4542 				if(name != name.toLower)
4543 					searchDb.saveSearchTerm(name.toLower, SearchResult(tid, 3));
4544 			}
4545 		}
4546 	}
4547 
4548 	// and we want to match parent names, though worth less.
4549 	version(none) {
4550 	Decl parent = decl.parent;
4551 	while(parent !is null) {
4552 		searchDb.saveSearchTerm(parent.name, SearchResult(tid, 5));
4553 		if(parent.name != parent.name.toLower)
4554 			searchDb.saveSearchTerm(parent.name.toLower, SearchResult(tid, 2));
4555 
4556 		auto splitNames = splitIdentifier(parent.name);
4557 		if(splitNames.length) {
4558 			foreach(name; splitNames) {
4559 				searchDb.saveSearchTerm(name, SearchResult(tid, 3));
4560 				if(name != name.toLower)
4561 					searchDb.saveSearchTerm(name.toLower, SearchResult(tid, 2));
4562 			}
4563 		}
4564 
4565 
4566 		parent = parent.parent;
4567 	}
4568 	}
4569 
4570 	bool deepSearch = searchDb !is null;
4571 
4572 	if(deepSearch) {
4573 		Document document;
4574 		//if(decl.fullyQualifiedName in generatedDocuments)
4575 			//document = generatedDocuments[decl.fullyQualifiedName];
4576 		//else
4577 			document = null;// writeHtml(decl, false, false, null, null);
4578 		//assert(document !is null);
4579 
4580 		// FIXME: pulling this from the generated html is a bit inefficient.
4581 
4582 		bool[const(char)[]] wordsUsed;
4583 
4584 		// tags are worth a lot
4585 		version(none)
4586 		foreach(tag; document.querySelectorAll(".tag")) {
4587 			if(tag.attrs.name in wordsUsed) continue;
4588 			wordsUsed[tag.attrs.name] = true;
4589 			searchDb.saveSearchTerm(tag.attrs.name, SearchResult(tid, to!int(tag.attrs.value.length ? tag.attrs.value : "0")), true);
4590 		}
4591 
4592 		// and other names that are referenced are worth quite a bit.
4593 		version(none)
4594 		foreach(tag; document.querySelectorAll(".xref")) {
4595 			if(tag.innerText in wordsUsed) continue;
4596 			wordsUsed[tag.innerText] = true;
4597 			searchDb.saveSearchTerm(tag.innerText, SearchResult(tid, tag.hasClass("parent-class") ? 10 : 5), true);
4598 		}
4599 		/*
4600 		foreach(tag; document.querySelectorAll("a[data-ident][title]"))
4601 			searchDb.saveSearchTerm(tag.dataset.ident, SearchResult(tid, 3), true);
4602 		foreach(tag; document.querySelectorAll("a.hid[title]"))
4603 			searchDb.saveSearchTerm(tag.innerText, SearchResult(tid, 3), true);
4604 		*/
4605 
4606 		// and full-text search. limited to first paragraph for speed reasons, hoping it is good enough for practical purposes
4607 		import ps = PorterStemmer;
4608 		ps.PorterStemmer s;
4609 		//foreach(tag; document.querySelectorAll(".documentation-comment.synopsis > p")){ //:first-of-type")) {
4610 			//foreach(word; getWords(tag.innerText)) {
4611 			foreach(word; getWords(decl.parsedDocComment.ddocSummary ~ "\n" ~ decl.parsedDocComment.synopsis)) {
4612 				auto w = s.stem(word.toLower);
4613 				if(w.length < 3) continue;
4614 				if(w.isIrrelevant())
4615 					continue;
4616 				if(w in wordsUsed)
4617 					continue;
4618 				wordsUsed[w] = true;
4619 				searchDb.saveSearchTerm(s.stem(word.toLower).idup, SearchResult(tid, 1), true);
4620 			}
4621 		//}
4622 	}
4623 
4624 	foreach(child; decl.children)
4625 		generateSearchIndex(child, searchDb);
4626 }
4627 
4628 bool isIrrelevant(in char[] s) {
4629 	switch(s) {
4630 		foreach(w; irrelevantWordList)
4631 			case w: return true;
4632 		default: return false;
4633 	}
4634 }
4635 
4636 // These are common words in English, which I'm generally
4637 // ignoring because they happen so often that they probably
4638 // aren't relevant keywords
4639 import std.meta;
4640 alias irrelevantWordList = AliasSeq!(
4641     "undocumented",
4642     "source",
4643     "intended",
4644     "author",
4645     "warned",
4646     "the",
4647     "of",
4648     "and",
4649     "a",
4650     "to",
4651     "in",
4652     "is",
4653     "you",
4654     "that",
4655     "it",
4656     "he",
4657     "was",
4658     "for",
4659     "on",
4660     "are",
4661     "as",
4662     "with",
4663     "his",
4664     "they",
4665     "I",
4666     "at",
4667     "be",
4668     "this",
4669     "have",
4670     "from",
4671     "or",
4672     "one",
4673     "had",
4674     "by",
4675     "word",
4676     "but",
4677     "not",
4678     "what",
4679     "all",
4680     "were",
4681     "we",
4682     "when",
4683     "your",
4684     "can",
4685     "said",
4686     "there",
4687     "use",
4688     "an",
4689     "each",
4690     "which",
4691     "she",
4692     "do",
4693     "how",
4694     "their",
4695     "if",
4696     "will",
4697     "up",
4698     "other",
4699     "about",
4700     "out",
4701     "many",
4702     "then",
4703     "them",
4704     "these",
4705     "so",
4706     "some",
4707     "her",
4708     "would",
4709     "make",
4710     "like",
4711     "him",
4712     "into",
4713     "time",
4714     "has",
4715     "look",
4716     "two",
4717     "more",
4718     "write",
4719     "go",
4720     "see",
4721     "number",
4722     "no",
4723     "way",
4724     "could",
4725     "people",
4726     "my",
4727     "than",
4728     "first",
4729     "water",
4730     "been",
4731     "call",
4732     "who",
4733     "its",
4734     "now",
4735     "find",
4736     "long",
4737     "down",
4738     "day",
4739     "did",
4740     "get",
4741     "come",
4742     "made",
4743     "may",
4744     "part",
4745 );
4746 
4747 string[] getWords(string text) {
4748 	string[] words;
4749 	string currentWord;
4750 
4751 	import std.uni;
4752 	foreach(dchar ch; text) {
4753 		if(!isAlpha(ch)) {
4754 			if(currentWord.length)
4755 				words ~= currentWord;
4756 			currentWord = null;
4757 		} else {
4758 			currentWord ~= ch;
4759 		}
4760 	}
4761 
4762 	return words;
4763 }
4764 
4765 import std.stdio : File;
4766 
4767 int getNestingLevel(Decl decl) {
4768 	int count = 0;
4769 	while(decl && !decl.isModule) {
4770 		decl = decl.parent;
4771 		count++;
4772 	}
4773 	return count;
4774 }
4775 
4776 void writeIndexXml(Decl decl, FileProxy index, ref int id, string postgresVersionId, PostgreSql searchDb) {
4777 //import std.stdio;writeln(decl.fullyQualifiedName, " ", decl.isPrivate, " ", decl.isDocumented);
4778 	if(!decl.docsShouldBeOutputted)
4779 		return;
4780 	if(cast(ImportDecl) decl)
4781 		return; // never write imports, it can overwrite the actual thing
4782 
4783 	auto cc = decl.parsedDocComment;
4784 
4785 	auto desc = formatDocumentationComment(cc.ddocSummary, decl);
4786 
4787 	.postgresVersionIdGlobal = postgresVersionId;
4788 
4789 	if(searchDb is null)
4790 		decl.databaseId = ++id;
4791 	else version(with_postgres) {
4792 
4793 		// this will leave stuff behind w/o the delete line but it is sooooo slow to rebuild this that reusing it is a big win for now
4794 		// searchDb.query("DELETE FROM d_symbols WHERE package_version_id = ?", postgresVersionId);
4795 		foreach(res; searchDb.query("SELECT id FROM d_symbols WHERE package_version_id = ? AND fully_qualified_name = ?", postgresVersionId, decl.fullyQualifiedName)) {
4796 			decl.databaseId = res[0].to!int;
4797 		}
4798 
4799 		if(decl.databaseId == 0)
4800 		foreach(res; searchDb.query("INSERT INTO d_symbols
4801 			(package_version_id, name, nesting_level, module_name, fully_qualified_name, url_name, summary)
4802 			VALUES
4803 			(?, ?, ?, ?, ?, ?, ?)
4804 			RETURNING id",
4805 			postgresVersionId,
4806 			decl.name,
4807 			getNestingLevel(decl),
4808 			decl.parentModule.name,
4809 			decl.fullyQualifiedName,
4810 			decl.link,
4811 			desc
4812 		))
4813 		{
4814 			decl.databaseId = res[0].to!int;
4815 		}
4816 	}
4817 
4818 	// the id needs to match the search index!
4819 	index.write("<decl id=\"" ~ to!string(decl.databaseId) ~ "\" type=\""~decl.declarationType~"\">");
4820 
4821 	index.write("<name>" ~ xmlEntitiesEncode(decl.name) ~ "</name>");
4822 	index.write("<desc>" ~ xmlEntitiesEncode(desc) ~ "</desc>");
4823 	index.write("<link>" ~ xmlEntitiesEncode(decl.link) ~ "</link>");
4824 
4825 	foreach(child; decl.children)
4826 		writeIndexXml(child, index, id, postgresVersionId, searchDb);
4827 
4828 	index.write("</decl>");
4829 }
4830 
4831 string pluralize(string word, int count = 2, string pluralWord = null) {
4832 	if(word.length == 0)
4833 		return word;
4834 
4835 	if(count == 1)
4836 		return word; // it isn't actually plural
4837 
4838 	if(pluralWord !is null)
4839 		return pluralWord;
4840 
4841 	switch(word[$ - 1]) {
4842 		case 's':
4843 		case 'a', 'i', 'o', 'u':
4844 			return word ~ "es";
4845 		case 'f':
4846 			return word[0 .. $-1] ~ "ves";
4847 		case 'y':
4848 			return word[0 .. $-1] ~ "ies";
4849 		default:
4850 			return word ~ "s";
4851 	}
4852 }
4853 
4854 Html toLinkedHtml(T)(const T t, Decl decl) {
4855 	import dparse.formatter;
4856 	string s;
4857 	struct Foo {
4858 		void put(in char[] a) {
4859 			s ~= a;
4860 		}
4861 	}
4862 	Foo output;
4863 	auto f = new MyFormatter!(typeof(output))(output);
4864 	f.format(t);
4865 
4866 	return Html(linkUpHtml(s, decl));
4867 }
4868 
4869 string linkUpHtml(string s, Decl decl, string base = "", bool linkToSource = false) {
4870 	auto document = new Document("<root>" ~ s ~ "</root>", true, true);
4871 
4872 	// additional cross referencing we weren't able to do at lower level
4873 	foreach(ident; document.querySelectorAll("*:not(a) [data-ident]:not(:has(a)), .hid")) {
4874 		// since i modify the tree in the loop, i recheck that we still match the selector
4875 		if(ident.parentNode is null)
4876 			continue;
4877 		if(ident.tagName == "a" || (ident.parentNode && ident.parentNode.tagName == "a"))
4878 			continue;
4879 		string i = ident.hasAttribute("data-ident") ? ident.dataset.ident : ident.innerText;
4880 
4881 		auto n = ident.nextSibling;
4882 		while(n && n.nodeValue == ".") {
4883 			i ~= ".";
4884 			auto txt = n;
4885 			n = n.nextSibling; // the span, ideally
4886 			if(n is null)
4887 				break;
4888 			if(n && (n.hasAttribute("data-ident") || n.hasClass("hid"))) {
4889 				txt.removeFromTree();
4890 				i ~= n.hasAttribute("data-ident") ? n.dataset.ident : n.innerText;
4891 				auto span = n;
4892 				n = n.nextSibling;
4893 				span.removeFromTree;
4894 			}
4895 		}
4896 
4897 		//ident.dataset.ident = i;
4898 		ident.innerText = i;
4899 
4900 		auto found = decl.lookupName(i);
4901 		string hash;
4902 
4903 		if(found is null) {
4904 			auto lastPieceIdx = i.lastIndexOf(".");
4905 			if(lastPieceIdx != -1) {
4906 				found = decl.lookupName(i[0 .. lastPieceIdx]);
4907 				if(found)
4908 					hash = "#" ~ i[lastPieceIdx + 1 .. $];
4909 			}
4910 		}
4911 
4912 		if(found) {
4913 			auto overloads = found.getImmediateDocumentedOverloads();
4914 			if(overloads.length)
4915 				found = overloads[0];
4916 		}
4917 
4918 		void linkToDoc() {
4919 			if(found && found.docsShouldBeOutputted) {
4920 				ident.attrs.title = found.fullyQualifiedName;
4921 				ident.tagName = "a";
4922 				ident.href = base ~ found.link ~ hash;
4923 			}
4924 		}
4925 
4926 		if(linkToSource) {
4927 			if(found && linkToSource && found.parentModule) {
4928 				ident.attrs.title = found.fullyQualifiedName;
4929 				ident.tagName = "a";
4930 				ident.href = found.parentModule.name ~ ".d.html#L" ~ to!string(found.lineNumber);
4931 			}
4932 		} else {
4933 			linkToDoc();
4934 		}
4935 	}
4936 
4937 	return document.root.innerHTML;
4938 }
4939 
4940 
4941 Html toHtml(T)(const T t) {
4942 	import dparse.formatter;
4943 	string s;
4944 	struct Foo {
4945 		void put(in char[] a) {
4946 			s ~= a;
4947 		}
4948 	}
4949 	Foo output;
4950 	auto f = new Formatter!(typeof(output))(output);
4951 	f.format(t);
4952 
4953 	return Html("<tt class=\"highlighted\">"~highlight(s)~"</tt>");
4954 }
4955 
4956 string toText(T)(const T t) {
4957 	import dparse.formatter;
4958 	string s;
4959 	struct Foo {
4960 		void put(in char[] a) {
4961 			s ~= a;
4962 		}
4963 	}
4964 	Foo output;
4965 	auto f = new Formatter!(typeof(output))(output);
4966 	f.format(t);
4967 
4968 	return s;
4969 }
4970 
4971 string toId(string txt) {
4972 	string id;
4973 	bool justSawSpace;
4974 	foreach(ch; txt) {
4975 		if(ch < 127) {
4976 			if(ch >= 'A' && ch <= 'Z') {
4977 				id ~= ch + 32;
4978 			} else if(ch == ' ') {
4979 				if(!justSawSpace)
4980 					id ~= '-';
4981 			} else {
4982 				id ~= ch;
4983 			}
4984 		} else {
4985 			id ~= ch;
4986 		}
4987 		justSawSpace = ch == ' ';
4988 	}
4989 	return id.strip;
4990 }
4991 
4992 
4993 
4994 /*
4995 	This file contains code from https://github.com/economicmodeling/harbored/
4996 
4997 	Those portions are Copyright 2014 Economic Modeling Specialists, Intl.,
4998 	written by Brian Schott, made available under the following license:
4999 
5000 	Boost Software License - Version 1.0 - August 17th, 2003
5001 
5002 	Permission is hereby granted, free of charge, to any person or organization
5003 	obtaining a copy of the software and accompanying documentation covered by
5004 	this license (the "Software") to use, reproduce, display, distribute,
5005 	execute, and transmit the Software, and to prepare derivative works of the
5006 	Software, and to permit third-parties to whom the Software is furnished to
5007 	do so, all subject to the following:
5008 
5009 	The copyright notices in the Software and this entire statement, including
5010 	the above license grant, this restriction and the following disclaimer,
5011 	must be included in all copies of the Software, in whole or in part, and
5012 	all derivative works of the Software, unless such copies or derivative
5013 	works are solely in the form of machine-executable object code generated by
5014 	a source language processor.
5015 
5016 	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
5017 	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
5018 	FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
5019 	SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
5020 	FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
5021 	ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
5022 	DEALINGS IN THE SOFTWARE.
5023 */