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