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