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