Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…

module adrdox.main; | |
__gshared string skeletonFile = "skeleton.html"; | |
__gshared string outputDirectory = "generated-docs"; | |
__gshared bool writePrivateDocs = false; | |
__gshared bool documentInternal = false; | |
__gshared bool documentUndocumented = false; | |
/* | |
Glossary feature: little short links that lead somewhere else. | |
FIXME: it should be able to handle bom. consider core/thread.d the unittest example is offset. | |
FIXME: | |
* make sure there's a link to the source for everything | |
* search | |
* package index without needing to build everything at once | |
* version specifiers | |
* prettified constraints | |
*/ | |
import dparse.parser; | |
import dparse.lexer; | |
import dparse.ast; | |
import arsd.dom; | |
import arsd.docgen.comment; | |
import std.algorithm :sort, canFind; | |
import std.string; | |
import std.conv : to; | |
// returns empty string if file not found | |
string findStandardFile(bool dofail=true) (string stdfname) { | |
import std.file : exists, thisExePath; | |
import std.path : buildPath, dirName; | |
if (!stdfname.exists) { | |
if (stdfname.length && stdfname[0] != '/') { | |
string newname = buildPath(thisExePath.dirName, stdfname); | |
if (newname.exists) return newname; | |
} | |
static if (dofail) throw new Exception("standard file '" ~stdfname ~ "' not found!"); | |
} | |
return stdfname; | |
} | |
void copyStandardFileTo(bool timecheck=true) (string destname, string stdfname) { | |
import std.file; | |
if (exists(destname)) { | |
static if (timecheck) { | |
if (timeLastModified(destname) >= timeLastModified(findStandardFile(stdfname))) return; | |
} else { | |
return; | |
} | |
} | |
copy(findStandardFile(stdfname), destname); | |
} | |
__gshared string[string] directoriesForPackage; | |
string getDirectoryForPackage(string packageName) { | |
if(packageName.indexOf("/") != -1) | |
return ""; // not actually a D package! | |
if(packageName.indexOf("#") != -1) | |
return ""; // not actually a D package! | |
string bestMatch = ""; | |
int bestMatchDots = -1; | |
import std.path; | |
foreach(pkg, dir; directoriesForPackage) { | |
if(globMatch!(CaseSensitive.yes)(packageName, pkg)) { | |
int cnt; | |
foreach(ch; pkg) | |
if(ch == '.') | |
cnt++; | |
if(cnt > bestMatchDots) { | |
bestMatch = dir; | |
bestMatchDots = cnt; | |
} | |
} | |
} | |
return bestMatch; | |
} | |
// FIXME: make See Also automatically list dittos that are not overloads | |
enum skip_undocumented = true; | |
static bool sorter(Decl a, Decl b) { | |
if(a.declarationType == b.declarationType) | |
return (blogMode && a.declarationType == "Article") ? (b.name < a.name) : (a.name < b.name); | |
else if(a.declarationType == "module" || b.declarationType == "module") // always put modules up top | |
return | |
(a.declarationType == "module" ? "aaaaaa" : a.declarationType) | |
< (b.declarationType == "module" ? "aaaaaa" : b.declarationType); | |
else | |
return a.declarationType < b.declarationType; | |
} | |
void annotatedPrototype(T)(T decl, MyOutputRange output) { | |
static if(is(T == TemplateDecl)) { | |
auto td = cast(TemplateDecl) decl; | |
auto epony = td.eponymousMember; | |
if(epony) { | |
if(auto e = cast(FunctionDecl) epony) { | |
doFunctionDec(e, output); | |
return; | |
} | |
} | |
} | |
auto classDec = decl.astNode; | |
auto f = new MyFormatter!(typeof(output))(output, decl); | |
void writePrototype() { | |
output.putTag("<div class=\"aggregate-prototype\">"); | |
if(decl.parent !is null && !decl.parent.isModule) { | |
output.putTag("<div class=\"parent-prototype\">"); | |
decl.parent.getSimplifiedPrototype(output); | |
output.putTag("</div><div>"); | |
} | |
writeAttributes(f, output, decl.attributes); | |
output.putTag("<span class=\"builtin-type\">"); | |
output.put(decl.declarationType); | |
output.putTag("</span>"); | |
output.put(" "); | |
output.put(decl.name); | |
output.put(" "); | |
foreach(idx, ir; decl.inheritsFrom()) { | |
if(idx == 0) | |
output.put(" : "); | |
else | |
output.put(", "); | |
if(ir.decl is null) | |
output.put(ir.plainText); | |
else | |
output.putTag(`<a class="xref parent-class" href=`~ir.decl.link~`>`~ir.decl.name~`</a> `); | |
} | |
if(classDec.templateParameters) | |
f.format(classDec.templateParameters); | |
if(classDec.constraint) | |
f.format(classDec.constraint); | |
// FIXME: invariant | |
if(decl.children.length) { | |
output.put(" {"); | |
foreach(child; decl.children) { | |
if((child.isPrivate() && !writePrivateDocs)) | |
continue; | |
// I want to show undocumented plain data members (if not private) | |
// since they might be used as public fields or in ctors for simple | |
// structs, but I'll skip everything else undocumented. | |
if(!child.isDocumented() && (cast(VariableDecl) child) is null) | |
continue; | |
output.putTag("<div class=\"aggregate-member\">"); | |
if(child.isDocumented()) | |
output.putTag("<a href=\""~child.link~"\""); | |
child.getAggregatePrototype(output); | |
if(child.isDocumented()) | |
output.putTag("</a>"); | |
output.putTag("</div>"); | |
} | |
output.put("}"); | |
} | |
if(decl.parent !is null && !decl.parent.isModule) { | |
output.putTag("</div>"); | |
} | |
output.putTag("</div>"); | |
} | |
writeOverloads!writePrototype(decl, output); | |
} | |
void doEnumDecl(T)(T decl, Element content) | |
{ | |
auto enumDec = decl.astNode; | |
static if(is(typeof(enumDec) == const(AnonymousEnumDeclaration))) { | |
if(enumDec.members.length == 0) return; | |
auto name = enumDec.members[0].name.text; | |
auto type = enumDec.baseType; | |
auto members = enumDec.members; | |
} else { | |
auto name = enumDec.name.text; | |
auto type = enumDec.type; | |
const(EnumMember)[] members; | |
if(enumDec.enumBody) | |
members = enumDec.enumBody.enumMembers; | |
} | |
static if(is(typeof(enumDec) == const(AnonymousEnumDeclaration))) { | |
// undocumented anonymous enums get a pass if any of | |
// their members are documented because that's what dmd does... | |
// FIXME maybe | |
bool foundOne = false; | |
foreach(member; members) { | |
if(member.comment.length) { | |
foundOne = true; | |
break; | |
} | |
} | |
if(!foundOne && skip_undocumented) | |
return; | |
} else { | |
if(enumDec.comment.length == 0 && skip_undocumented) | |
return; | |
} | |
/* | |
auto f = new MyFormatter!(typeof(output))(output); | |
if(type) { | |
output.putTag("<div class=\"base-type\">"); | |
f.format(type); | |
output.putTag("</div>"); | |
} | |
*/ | |
content.addChild("h2", "Values").attrs.id = "values"; | |
auto table = content.addChild("table").addClass("enum-members"); | |
table.appendHtml("<tr><th>Value</th><th>Meaning</th></tr>"); | |
foreach(member; members) { | |
auto memberComment = formatDocumentationComment(preprocessComment(member.comment, decl), decl); | |
auto tr = table.addChild("tr"); | |
tr.addClass("enum-member"); | |
tr.attrs.id = member.name.text; | |
auto td = tr.addChild("td"); | |
if(member.isDisabled) { | |
td.addChild("span", "@disable").addClass("enum-disabled"); | |
td.addChild("br"); | |
} | |
if(member.type) { | |
td.addChild("span", toHtml(member.type)).addClass("enum-type"); | |
} | |
td.addChild("a", member.name.text, "#" ~ member.name.text).addClass("enum-member-name"); | |
if(member.assignExpression) { | |
td.addChild("span", toHtml(member.assignExpression)).addClass("enum-member-value"); | |
} | |
auto ea = td.addChild("div", "", "enum-attributes"); | |
foreach(attribute; member.atAttributes) { | |
ea.addChild("div", toHtml(attribute)); | |
} | |
// type | |
// assignExpression | |
td = tr.addChild("td"); | |
td.innerHTML = memberComment; | |
if(member.deprecated_) { | |
auto p = td.prependChild(Element.make("div", Element.make("span", member.deprecated_.stringLiterals.length ? "Deprecated: " : "Deprecated", "deprecated-label"))).addClass("enum-deprecated"); | |
foreach(sl; member.deprecated_.stringLiterals) | |
p.addChild("span", sl.text[1 .. $-1]); | |
} | |
// I might write to parent list later if I can do it all semantically inside the anonymous enum | |
// since these names are introduced into the parent scope i think it is justified to list them there | |
// maybe. | |
} | |
} | |
void doFunctionDec(T)(T decl, MyOutputRange output) | |
{ | |
auto functionDec = decl.astNode; | |
//if(!decl.isDocumented() && skip_undocumented) | |
//return; | |
string[] conceptsFound; | |
auto f = new MyFormatter!(typeof(output))(output, decl); | |
/+ | |
auto comment = parseDocumentationComment(dittoSupport(functionDec, functionDec.comment), fullyQualifiedName ~ name); | |
if(auto ptr = name in overloadChain[$-1]) { | |
*ptr += 1; | |
name ~= "." ~ to!string(*ptr); | |
} else { | |
overloadChain[$-1][name] = 1; | |
overloadNodeChain[$-1] = cast() functionDec; | |
} | |
const(ASTNode)[] overloadsList; | |
if(auto overloads = overloadNodeChain[$-1] in additionalModuleInfo.overloadsOf) { | |
overloadsList = *overloads; | |
} | |
if(dittoChain[$-1]) | |
if(auto dittos = dittoChain[$-1] in additionalModuleInfo.dittos) { | |
auto list = *dittos; | |
outer: foreach(item; list) { | |
if(item is functionDec) | |
continue; | |
// already listed as a formal overload which means we | |
// don't need to list it again under see also | |
foreach(ol; overloadsList) | |
if(ol is item) | |
continue outer; | |
string linkHtml; | |
linkHtml ~= `<a href="`~htmlEncode(linkMapping[item])~`">` ~ htmlEncode(nameMapping[item]) ~ `</a>`; | |
comment.see_alsos ~= linkHtml; | |
} | |
} | |
descendInto(name); | |
output.putTag("<h1><span class=\"entity-name\">" ~ name ~ "</span> "~typeString~"</h1>"); | |
writeBreadcrumbs(); | |
output.putTag("<div class=\"function-declaration\">"); | |
comment.writeSynopsis(output); | |
+/ | |
void writeFunctionPrototype() { | |
string outputStr; | |
auto originalOutput = output; | |
MyOutputRange output = MyOutputRange(&outputStr); | |
auto f = new MyFormatter!(typeof(output))(output); | |
output.putTag("<div class=\"function-prototype\">"); | |
//output.putTag(`<a href="http://dpldocs.info/reading-prototypes" id="help-link">?</a>`); | |
if(decl.parent !is null && !decl.parent.isModule) { | |
output.putTag("<div class=\"parent-prototype\">"); | |
decl.parent.getSimplifiedPrototype(output); | |
output.putTag("</div><div>"); | |
} | |
writeAttributes(f, output, decl.attributes); | |
static if( | |
!is(typeof(functionDec) == const(Constructor)) && | |
!is(typeof(functionDec) == const(Postblit)) && | |
!is(typeof(functionDec) == const(Destructor)) | |
) { | |
output.putTag("<div class=\"return-type\">"); | |
if (functionDec.hasAuto && functionDec.hasRef) | |
output.putTag(`<a class="lang-feature" href="http://dpldocs.info/auto-ref-function-return-prototype">auto ref</a> `); | |
else { | |
if (functionDec.hasAuto) | |
output.putTag(`<a class="lang-feature" href="http://dpldocs.info/auto-function-return-prototype">auto</a> `); | |
if (functionDec.hasRef) | |
output.putTag(`<a class="lang-feature" href="http://dpldocs.info/ref-function-return-prototype">ref</a> `); | |
} | |
if (functionDec.returnType !is null) | |
f.format(functionDec.returnType); | |
output.putTag("</div>"); | |
} | |
output.putTag("<div class=\"function-name\">"); | |
output.put(decl.name); | |
output.putTag("</div>"); | |
foreach(attr; decl.astNode.memberFunctionAttributes) { | |
f.format(attr); | |
output.put(" "); | |
} | |
output.putTag("<div class=\"template-parameters\">"); | |
if (functionDec.templateParameters !is null) | |
f.format(functionDec.templateParameters); | |
output.putTag("</div>"); | |
output.putTag("<div class=\"runtime-parameters\">"); | |
f.format(functionDec.parameters); | |
output.putTag("</div>"); | |
if(functionDec.constraint !is null) { | |
output.putTag("<div class=\"template-constraint\">"); | |
f.format(functionDec.constraint); | |
output.putTag("</div>"); | |
} | |
if(functionDec.functionBody !is null) { | |
// FIXME: list inherited contracts | |
output.putTag("<div class=\"function-contracts\">"); | |
import dparse.formatter; | |
auto f2 = new Formatter!(typeof(output))(output); | |
if(functionDec.functionBody.inStatement) { | |
output.putTag("<div class=\"in-contract\">"); | |
f2.format(functionDec.functionBody.inStatement); | |
output.putTag("</div>"); | |
} | |
if(functionDec.functionBody.outStatement) { | |
output.putTag("<div class=\"out-contract\">"); | |
f2.format(functionDec.functionBody.outStatement); | |
output.putTag("</div>"); | |
} | |
output.putTag("</div>"); | |
} | |
//output.put(" : "); | |
//output.put(to!string(functionDec.name.line)); | |
if(decl.parent !is null && !decl.parent.isModule) { | |
output.putTag("</div>"); | |
decl.parent.writeTemplateConstraint(output); | |
} | |
output.putTag("</div>"); | |
originalOutput.putTag(linkUpHtml(outputStr, decl)); | |
} | |
writeOverloads!writeFunctionPrototype(decl, output); | |
} | |
void writeOverloads(alias writePrototype, D : Decl)(D decl, ref MyOutputRange output) { | |
auto overloadsList = decl.getImmediateDocumentedOverloads(); | |
// I'm treating dittos similarly to overloads | |
if(overloadsList.length == 1) { | |
overloadsList = decl.getDittos(); | |
} else { | |
foreach(ditto; decl.getDittos()) { | |
if(!overloadsList.canFind(ditto)) | |
overloadsList ~= ditto; | |
} | |
} | |
if(overloadsList.length > 1) { | |
import std.conv; | |
output.putTag("<ol class=\"overloads\">"); | |
foreach(idx, item; overloadsList) { | |
assert(item.parent !is null); | |
string cn; | |
Decl epony; | |
if(auto t = cast(TemplateDecl) item) | |
epony = t.eponymousMember; | |
if(item !is decl && decl !is epony) | |
cn = "overload-option"; | |
else | |
cn = "active-overload-option"; | |
if(item.name != decl.name) | |
cn ~= " ditto-option"; | |
output.putTag("<li class=\""~cn~"\">"); | |
//if(item is decl) | |
//writeFunctionPrototype(); | |
//} else { | |
{ | |
if(item !is decl) | |
output.putTag(`<a href="`~item.link~`">`); | |
output.putTag("<span class=\"overload-signature\">"); | |
item.getSimplifiedPrototype(output); | |
output.putTag("</span>"); | |
if(item !is decl) | |
output.putTag(`</a>`); | |
} | |
if(item is decl || decl is epony) | |
writePrototype(); | |
output.putTag("</li>"); | |
} | |
output.putTag("</ol>"); | |
} else { | |
writePrototype(); | |
} | |
} | |
void writeAttributes(F, W)(F formatter, W writer, const(VersionOrAttribute)[] attrs) | |
{ | |
writer.putTag("<div class=\"attributes\">"); | |
IdType protection; | |
foreach (a; attrs) | |
{ | |
if (a.attr && isProtection(a.attr.attribute.type)) | |
protection = a.attr.attribute.type; | |
} | |
switch (protection) | |
{ | |
case tok!"private": writer.put("private "); break; | |
case tok!"package": writer.put("package "); break; | |
case tok!"protected": writer.put("protected "); break; | |
case tok!"export": writer.put("export "); break; | |
case tok!"public": // see below | |
default: | |
// I'm not printing public so this is commented intentionally | |
// public is the default state of documents so saying it outright | |
// is kinda just a waste of time IMO. | |
//writer.put("public "); | |
break; | |
} | |
foreach (a; attrs) | |
{ | |
if(auto fakeAttr = cast(FakeAttribute) a) { | |
writer.putTag(fakeAttr.toHTML()); | |
writer.put(" "); | |
continue; | |
} | |
// skipping auto because it is already handled as the return value | |
if (!isProtection(a.attr.attribute.type) && a.attr.attribute.type != tok!"auto") | |
{ | |
formatter.format(a.attr); | |
writer.put(" "); | |
} | |
} | |
writer.putTag("</div>"); | |
} | |
class VersionOrAttribute { | |
const(Attribute) attr; | |
this(const(Attribute) attr) { | |
this.attr = attr; | |
} | |
const(VersionOrAttribute) invertedClone() const { | |
return new VersionOrAttribute(attr); | |
} | |
} | |
class FakeAttribute : VersionOrAttribute { | |
this() { super(null); } | |
abstract string toHTML() const; | |
} | |
class VersionFakeAttribute : FakeAttribute { | |
string cond; | |
bool inverted; | |
this(string condition, bool inverted = false) { | |
cond = condition; | |
this.inverted = inverted; | |
} | |
override const(VersionFakeAttribute) invertedClone() const { | |
return new VersionFakeAttribute(cond, !inverted); | |
} | |
override string toHTML() const { | |
auto element = Element.make("span"); | |
element.addChild("span", "version", "lang-feature"); | |
element.appendText("("); | |
if(inverted) | |
element.appendText("!"); | |
element.addChild("span", cond); | |
element.appendText(")"); | |
return element.toString; | |
} | |
} | |
void putSimplfiedReturnValue(MyOutputRange output, const FunctionDeclaration decl) { | |
if (decl.hasAuto && decl.hasRef) | |
output.putTag(`<span class="lang-feature">auto ref</span> `); | |
else { | |
if (decl.hasAuto) | |
output.putTag(`<span class="lang-feature">auto</span> `); | |
if (decl.hasRef) | |
output.putTag(`<span class="lang-feature">ref</span> `); | |
} | |
if (decl.returnType !is null) | |
output.putTag(toHtml(decl.returnType).source); | |
} | |
void putSimplfiedArgs(T)(MyOutputRange output, const T decl) { | |
// FIXME: do NOT show default values here | |
if(decl.parameters) | |
output.putTag(toHtml(decl.parameters).source); | |
} | |
string specialPreprocess(string comment, Decl decl) { | |
switch(specialPreprocessor) { | |
case "dwt": | |
// translate Javadoc to adrdox | |
// @see, @exception/@throws, @param, @return | |
// @author, @version, @since, @deprecated | |
// {@link thing} | |
// one line desc is until the first <p> | |
// html tags are allowed in javadoc | |
// links go class#member(args) | |
// the (args) and class are optional | |
string parseIdentifier(ref string s, bool allowHash = false) { | |
int end = 0; | |
while(end < s.length && ( | |
(s[end] >= 'A' && s[end] <= 'Z') || | |
(s[end] >= 'a' && s[end] <= 'z') || | |
(s[end] >= '0' && s[end] <= '9') || | |
s[end] == '_' || | |
s[end] == '.' || | |
(allowHash && s[end] == '#') | |
)) | |
{ | |
end++; | |
} | |
auto i = s[0 .. end]; | |
s = s[end .. $]; | |
return i; | |
} | |
// javadoc is basically html with @ stuff, so start by parsing that (presumed) tag soup | |
auto document = new Document("<root>" ~ comment ~ "</root>"); | |
string newComment; | |
string fixupJavaReference(string r) { | |
if(r.length == 0) | |
return r; | |
if(r[0] == '#') | |
r = r[1 .. $]; // local refs in adrdox need no special sigil | |
r = r.replace("#", "."); | |
auto idx = r.indexOf("("); | |
if(idx != -1) | |
r = r[0 .. idx]; | |
return r; | |
} | |
void translate(Element element) { | |
if(element.nodeType == NodeType.Text) { | |
foreach(line; element.nodeValue.splitLines(KeepTerminator.yes)) { | |
auto s = line.strip; | |
if(s.length && s[0] == '@') { | |
s = s[1 .. $]; | |
auto ident = parseIdentifier(s); | |
switch(ident) { | |
case "author": | |
case "deprecated": | |
case "version": | |
case "since": | |
line = ident ~ ": " ~ s ~ "\n"; | |
break; | |
case "return": | |
case "returns": | |
line = "Returns: " ~ s ~ "\n"; | |
break; | |
case "exception": | |
case "throws": | |
while(s.length && s[0] == ' ') | |
s = s[1 .. $]; | |
auto p = parseIdentifier(s); | |
line = "Throws: [" ~ p ~ "]" ~ s ~ "\n"; | |
break; | |
case "param": | |
while(s.length && s[0] == ' ') | |
s = s[1 .. $]; | |
auto p = parseIdentifier(s); | |
line = "Params:\n" ~ p ~ " = " ~ s ~ "\n"; | |
break; | |
case "see": | |
while(s.length && s[0] == ' ') | |
s = s[1 .. $]; | |
auto p = parseIdentifier(s, true); | |
if(p.length) | |
line = "See_Also: [" ~ fixupJavaReference(p) ~ "]" ~ "\n"; | |
else | |
line = "See_Also: " ~ s ~ "\n"; | |
break; | |
default: | |
// idk, leave it alone. | |
} | |
} | |
newComment ~= line; | |
} | |
} else { | |
if(element.tagName == "code") { | |
newComment ~= "`"; | |
// FIXME: what about ` inside code? | |
newComment ~= element.innerText; // .replace("`", "``"); | |
newComment ~= "`"; | |
} else if(element.tagName == "p") { | |
newComment ~= "\n\n"; | |
foreach(child; element.children) | |
translate(child); | |
newComment ~= "\n\n"; | |
} else if(element.tagName == "a") { | |
newComment ~= "${LINK2 " ~ element.href ~ ", " ~ element.innerText ~ "}"; | |
} else { | |
newComment ~= "${" ~ element.tagName.toUpper ~ " "; | |
foreach(child; element.children) | |
translate(child); | |
newComment ~= "}"; | |
} | |
} | |
} | |
foreach(child; document.root.children) | |
translate(child); | |
comment = newComment; | |
break; | |
case "gtk": | |
// translate gtk syntax and names to our own | |
string gtkObjectToDClass(string name) { | |
if(name.length == 0) | |
return null; | |
int pkgEnd = 1; | |
while(pkgEnd < name.length && !(name[pkgEnd] >= 'A' && name[pkgEnd] <= 'Z')) | |
pkgEnd++; | |
auto pkg = name[0 .. pkgEnd].toLower; | |
auto mod = name[pkgEnd .. $]; | |
auto t = pkg ~ "." ~ mod; | |
if(t in modulesByName) | |
return t ~ "." ~ mod; | |
if(auto c = mod in allClasses) | |
return c.fullyQualifiedName; | |
return null; | |
} | |
string trimFirstThing(string name) { | |
if(name.length == 0) | |
return null; | |
int pkgEnd = 1; | |
while(pkgEnd < name.length && !(name[pkgEnd] >= 'A' && name[pkgEnd] <= 'Z')) | |
pkgEnd++; | |
return name[pkgEnd .. $]; | |
} | |
string formatForDisplay(string name) { | |
auto parts = name.split("."); | |
// gtk.Application.Application.member | |
// we want to take out the repeated one - slot [1] | |
string disp; | |
if(parts.length > 2) | |
foreach(idx, part; parts) { | |
if(idx == 1) continue; | |
if(idx) { | |
disp ~= "."; | |
} | |
disp ~= part; | |
} | |
else | |
disp = name; | |
return disp; | |
} | |
import std.regex : regex, replaceAll, Captures; | |
// gtk references to adrdox reference; punt it to the search engine | |
string magic(Captures!string m) { | |
string s = m.hit; | |
s = s[1 .. $]; // trim off # | |
auto orig = s; | |
auto name = s; | |
string displayOverride; | |
string additional; | |
auto idx = s.indexOf(":"); | |
if(idx != -1) { | |
// colon means it is an attribute or a signal | |
auto property = s[idx + 1 .. $]; | |
s = s[0 .. idx]; | |
if(property.length && property[0] == ':') { | |
// is a signal | |
property = property[1 .. $]; | |
additional = ".addOn"; | |
displayOverride = property; | |
} else { | |
// is a property | |
additional = ".get"; | |
displayOverride = property; | |
} | |
bool justSawDash = true; | |
foreach(ch; property) { | |
if(justSawDash && ch >= 'a' && ch <= 'z') { | |
additional ~= cast(char) (cast(int) ch - 32); | |
} else if(ch == '-') { | |
// intentionally blank | |
} else { | |
additional ~= ch; | |
} | |
if(ch == '-') { | |
justSawDash = true; | |
} else { | |
justSawDash = false; | |
} | |
} | |
} else { | |
idx = s.indexOf("."); | |
if(idx != -1) { | |
// dot is either a tailing period or a Struct.field | |
if(idx == s.length - 1) | |
s = s[0 .. $ - 1]; // tailing period | |
else { | |
auto structField = s[idx + 1 .. $]; | |
s = s[0 .. idx]; | |
additional = "." ~ structField; // FIXME? | |
} | |
} | |
} | |
auto dClass = gtkObjectToDClass(s); | |
bool plural = false; | |
if(dClass is null && s.length && s[$-1] == 's') { | |
s = s[0 .. $-1]; | |
dClass = gtkObjectToDClass(s); | |
plural = true; | |
} | |
if(dClass !is null) | |
s = dClass; | |
s ~= additional; | |
if(displayOverride.length) | |
return "[" ~ s ~ "|"~displayOverride~"]"; | |
else | |
return "[" ~ s ~ "|"~formatForDisplay(s)~(plural ? "s" : "") ~ "]"; | |
} | |
// gtk function to adrdox d ref | |
string magic2(Captures!string m) { | |
if(m.hit == "main()") | |
return "`"~m.hit~"`"; // special case | |
string s = m.hit[0 .. $-2]; // trim off the () | |
auto orig = m.hit; | |
// these tend to go package_class_method_snake | |
string gtkType; | |
gtkType ~= s[0] | 32; | |
s = s[1 .. $]; | |
bool justSawUnderscore = false; | |
string dType; | |
bool firstUnderscore = true; | |
while(s.length) { | |
if(s[0] == '_') { | |
justSawUnderscore = true; | |
if(!firstUnderscore) { | |
auto dc = gtkObjectToDClass(gtkType); | |
if(dc !is null) { | |
dType = dc; | |
s = s[1 .. $]; | |
break; | |
} | |
} | |
firstUnderscore = false; | |
} else if(justSawUnderscore) { | |
gtkType ~= s[0] & ~32; | |
justSawUnderscore = false; | |
} else | |
gtkType ~= s[0]; | |
s = s[1 .. $]; | |
} | |
if(dType.length) { | |
justSawUnderscore = false; | |
string gtkMethod = ""; | |
while(s.length) { | |
if(s[0] == '_') { | |
justSawUnderscore = true; | |
} else if(justSawUnderscore) { | |
gtkMethod ~= s[0] & ~32; | |
justSawUnderscore = false; | |
} else | |
gtkMethod ~= s[0]; | |
s = s[1 .. $]; | |
} | |
auto dispName = dType[dType.lastIndexOf(".") + 1 .. $] ~ "." ~ gtkMethod; | |
return "[" ~ dType ~ "." ~ gtkMethod ~ "|" ~ dispName ~ "]"; | |
} | |
return "`" ~ orig ~ "`"; | |
} | |
// cut off spam at the end of headers | |
comment = replaceAll(comment, regex(r"(## [A-Za-z0-9 ]+)##.*$", "gm"), "$1"); | |
// translate see also header into ddoc style as a special case | |
comment = replaceAll(comment, regex(r"## See Also.*$", "gm"), "See_Also:\n"); | |
// name lookup | |
comment = replaceAll!magic(comment, regex(r"#[A-Za-z0-9_:\-\.]+", "g")); | |
// gtk params to inline code | |
comment = replaceAll(comment, regex(r"@([A-Za-z0-9_:]+)", "g"), "`$1`"); | |
// constants too | |
comment = replaceAll(comment, regex(r"%([A-Za-z0-9_:]+)", "g"), "`$1`"); | |
// and functions | |
comment = replaceAll!magic2(comment, regex(r"([A-Za-z0-9_]+)\(\)", "g")); | |
// their code blocks | |
comment = replace(comment, `|[<!-- language="C" -->`, "```c\n"); | |
comment = replace(comment, `]|`, "\n```"); | |
break; | |
default: | |
return comment; | |
} | |
return comment; | |
} | |
struct HeaderLink { | |
string text; | |
string url; | |
} | |
string[string] pseudoFiles; | |
bool usePseudoFiles = false; | |
Document writeHtml(Decl decl, bool forReal, bool gzip, string headerTitle, HeaderLink[] headerLinks) { | |
if(!decl.docsShouldBeOutputted) | |
return null; | |
auto title = decl.name; | |
bool justDocs = false; | |
if(auto mod = cast(ModuleDecl) decl) { | |
if(mod.justDocsTitle !is null) { | |
title = mod.justDocsTitle; | |
justDocs = true; | |
} | |
} | |
if(decl.parent !is null && !decl.parent.isModule) { | |
title = decl.parent.name ~ "." ~ title; | |
} | |
auto document = new Document(); | |
import std.file; | |
document.parseUtf8(readText(findStandardFile(skeletonFile)), true, true); | |
document.title = title ~ " (" ~ decl.fullyQualifiedName ~ ")"; | |
if(headerTitle.length) | |
document.requireSelector("#logotype span").innerText = headerTitle; | |
if(headerLinks.length) { | |
auto n = document.requireSelector("#page-header nav"); | |
foreach(l; headerLinks) | |
if(l.text.length && l.url.length) | |
n.addChild("a", l.text, l.url); | |
} | |
auto content = document.requireElementById("page-content"); | |
auto comment = decl.parsedDocComment; | |
content.addChild("h1", title); | |
auto breadcrumbs = content.addChild("div").addClass("breadcrumbs"); | |
//breadcrumbs.prependChild(Element.make("a", decl.name, decl.link).addClass("current breadcrumb")); | |
{ | |
auto p = decl.parent; | |
while(p) { | |
if(p.fakeDecl && p.name == "index") | |
break; | |
// cut off package names that would be repeated | |
auto name = (p.isModule && p.parent) ? lastDotOnly(p.name) : p.name; | |
breadcrumbs.prependChild(Element.make("a", name, p.link(true)).addClass("breadcrumb")); | |
p = p.parent; | |
} | |
} | |
if(blogMode && decl.isArticle) { | |
// FIXME: kinda a hack there | |
auto mod = cast(ModuleDecl) decl; | |
if(mod.name.startsWith("Blog.Posted_")) | |
content.addChild("span", decl.name.replace("Blog.Posted_", "Posted ").replace("_", "-")).addClass("date-posted"); | |
} | |
string s; | |
MyOutputRange output = MyOutputRange(&s); | |
comment.writeSynopsis(output); | |
content.addChild("div", Html(s)); | |
s = null; | |
decl.getAnnotatedPrototype(output); | |
content.addChild("div", Html(s), "annotated-prototype"); | |
Element lastDt; | |
string dittoedName; | |
string dittoedComment; | |
void handleChildDecl(Element dl, Decl child, bool enableLinking = true) { | |
auto cc = child.parsedDocComment; | |
string sp; | |
MyOutputRange or = MyOutputRange(&sp); | |
child.getSimplifiedPrototype(or); | |
auto printableName = child.name; | |
if(child.isArticle) { | |
auto mod = cast(ModuleDecl) child; | |
printableName = mod.justDocsTitle; | |
} else { | |
if(child.isModule && child.parent && child.parent.isModule) { | |
if(printableName.startsWith(child.parent.name)) | |
printableName = printableName[child.parent.name.length + 1 .. $]; | |
} | |
} | |
auto newDt = Element.make("dt", Element.make("a", printableName, child.link)); | |
auto st = newDt.addChild("div", Html(sp)).addClass("simplified-prototype"); | |
st.style.maxWidth = to!string(st.innerText.length * 11 / 10) ~ "ch"; | |
if(child.isDitto && child.comment == dittoedComment && lastDt !is null) { | |
// ditto'd names don't need to be written again | |
if(child.name == dittoedName) { | |
if(sp == lastDt.requireSelector(".simplified-prototype").innerHTML) | |
return; // no need to say the same thing twice | |
// same name, but different prototype. Cut the repetition. | |
newDt.requireSelector("a").removeFromTree(); | |
} | |
lastDt.addSibling(newDt); | |
} else { | |
dl.addChild(newDt); | |
auto dd = dl.addChild("dd", Html(formatDocumentationComment(enableLinking ? cc.ddocSummary : preprocessComment(child.comment, child), child))); | |
foreach(e; dd.querySelectorAll("h1, h2, h3, h4, h5, h6")) | |
e.stripOut; | |
dittoedComment = child.comment; | |
} | |
lastDt = newDt; | |
dittoedName = child.name; | |
} | |
Decl[] ctors; | |
Decl[] members; | |
ModuleDecl[] articles; | |
Decl[] submodules; | |
ImportDecl[] imports; | |
if(forReal) | |
foreach(child; decl.children) { | |
if(!child.docsShouldBeOutputted) | |
continue; | |
if(child.isConstructor()) | |
ctors ~= child; | |
else if(child.isArticle) | |
articles ~= cast(ModuleDecl) child; | |
else if(child.isModule) | |
submodules ~= child; | |
else if(cast(DestructorDecl) child) | |
{} // intentionally blank | |
else if(cast(PostblitDecl) child) | |
{} // intentionally blank | |
else if(auto c = cast(ImportDecl) child) | |
imports ~= c; | |
else | |
members ~= child; | |
} | |
if(decl.disabledDefaultConstructor) { | |
content.addChild("h2", "Disabled Default Constructor").id = "disabled-default-constructor"; | |
auto div = content.addChild("div"); | |
div.addChild("p", "A disabled default is present on this object. To use it, use one of the other constructors or a factory function."); | |
} | |
if(ctors.length) { | |
content.addChild("h2", "Constructors").id = "constructors"; | |
auto dl = content.addChild("dl").addClass("member-list constructors"); | |
foreach(child; ctors) { | |
if(child is decl.disabledDefaultConstructor) | |
continue; | |
handleChildDecl(dl, child); | |
writeHtml(child, forReal, gzip, headerTitle, headerLinks); | |
} | |
} | |
if(auto dtor = decl.destructor) { | |
content.addChild("h2", "Destructor").id = "destructor"; | |
auto dl = content.addChild("dl").addClass("member-list"); | |
if(dtor.isDocumented) | |
handleChildDecl(dl, dtor); | |
else | |
content.addChild("p", "A destructor is present on this object, but not explicitly documented in the source."); | |
//writeHtml(dtor, forReal, gzip, headerTitle, headerLinks); | |
} | |
if(auto postblit = decl.postblit) { | |
content.addChild("h2", "Postblit").id = "postblit"; | |
auto dl = content.addChild("dl").addClass("member-list"); | |
if(postblit.isDisabled()) | |
content.addChild("p", "Copying this object is disabled."); | |
if(postblit.isDocumented) | |
handleChildDecl(dl, postblit); | |
else | |
content.addChild("p", "A postblit is present on this object, but not explicitly documented in the source."); | |
//writeHtml(dtor, forReal, gzip, headerTitle, headerLinks); | |
} | |
if(articles.length) { | |
content.addChild("h2", "Articles").id = "articles"; | |
auto dl = content.addChild("dl").addClass("member-list articles"); | |
foreach(child; articles.sort!((a,b) => (blogMode ? (b.name < a.name) : (a.name < b.name)))) { | |
handleChildDecl(dl, child); | |
} | |
} | |
if(submodules.length) { | |
content.addChild("h2", "Modules").id = "modules"; | |
auto dl = content.addChild("dl").addClass("member-list native"); | |
foreach(child; submodules.sort!((a,b) => a.name < b.name)) { | |
handleChildDecl(dl, child); | |
// i actually want submodules to be controlled on the command line too. | |
//if(!usePseudoFiles) // with pseudofiles, we can generate child modules on demand too, so avoid recursive everything on root request | |
//writeHtml(child, forReal, gzip, headerTitle, headerLinks); | |
} | |
} | |
if(auto at = decl.aliasThis) { | |
content.addChild("h2", "Alias This").id = "alias-this"; | |
auto div = content.addChild("div"); | |
div.addChild("a", at.name, at.link); | |
if(decl.aliasThisComment.length) { | |
auto memberComment = formatDocumentationComment(preprocessComment(decl.aliasThisComment, decl), decl); | |
auto dc = div.addChild("div").addClass("documentation-comment"); | |
dc.innerHTML = memberComment; | |
} | |
} | |
if(imports.length) { | |
content.addChild("h2", "Public Imports").id = "public-imports"; | |
auto div = content.addChild("div"); | |
foreach(imp; imports) { | |
auto dl = content.addChild("dl").addClass("member-list native"); | |
handleChildDecl(dl, imp, false); | |
} | |
} | |
if(members.length) { | |
content.addChild("h2", "Members").id = "members"; | |
void outputMemberList(Decl[] members, string header, string idPrefix, string headerPrefix) { | |
Element dl; | |
string lastType; | |
foreach(child; members.sort!sorter) { | |
if(child.declarationType != lastType) { | |
auto hdr = content.addChild(header, headerPrefix ~ pluralize(child.declarationType).capitalize, "member-list-header hide-from-toc"); | |
hdr.id = idPrefix ~ child.declarationType; | |
dl = content.addChild("dl").addClass("member-list native"); | |
lastType = child.declarationType; | |
} | |
handleChildDecl(dl, child); | |
writeHtml(child, forReal, gzip, headerTitle, headerLinks); | |
} | |
} | |
foreach(section; comment.symbolGroupsOrder) { | |
auto memberComment = formatDocumentationComment(preprocessComment(comment.symbolGroups[section], decl), decl); | |
string sectionPrintable = section.replace("_", " ").capitalize; | |
// these count as user headers to move toward TOC - section groups are user defined so it makes sense | |
auto hdr = content.addChild("h3", sectionPrintable, "member-list-header user-header"); | |
hdr.id = "group-" ~ section; | |
auto dc = content.addChild("div").addClass("documentation-comment"); | |
dc.innerHTML = memberComment; | |
if(auto hdr2 = dc.querySelector("> div:only-child > h2:first-child, > div:only-child > h3:first-child")) { | |
hdr.innerHTML = hdr2.innerHTML; | |
hdr2.removeFromTree; | |
} | |
Decl[] subList; | |
for(int i = 0; i < members.length; i++) { | |
auto member = members[i]; | |
if(member.parsedDocComment.group == section) { | |
subList ~= member; | |
members[i] = members[$-1]; | |
members = members[0 .. $-1]; | |
i--; | |
} | |
} | |
outputMemberList(subList, "h4", section ~ "-", sectionPrintable ~ " "); | |
} | |
if(members.length) { | |
if(comment.symbolGroupsOrder.length) { | |
auto hdr = content.addChild("h3", "Other", "member-list-header"); | |
hdr.id = "group-other"; | |
outputMemberList(members, "h4", "other-", "Other "); | |
} else { | |
outputMemberList(members, "h3", "", ""); | |
} | |
} | |
} | |
auto irList = decl.inheritsFrom; | |
if(irList.length) { | |
auto h2 = content.addChild("h2", "Inherited Members"); | |
h2.id = "inherited-members"; | |
bool hasAnyListing = false; | |
foreach(ir; irList) { | |
if(ir.decl is null) continue; | |
auto h3 = content.addChild("h3", "From " ~ ir.decl.name); | |
h3.id = "inherited-from-" ~ ir.decl.fullyQualifiedName; | |
auto dl = content.addChild("dl").addClass("member-list inherited"); | |
bool hadListing = false; | |
foreach(child; ir.decl.children) { | |
if(!child.docsShouldBeOutputted) | |
continue; | |
if(!child.isConstructor()) { | |
handleChildDecl(dl, child); | |
hadListing = true; | |
hasAnyListing = true; | |
} | |
} | |
if(!hadListing) { | |
h3.removeFromTree(); | |
dl.removeFromTree(); | |
} | |
} | |
if(!hasAnyListing) | |
h2.removeFromTree(); | |
} | |
decl.addSupplementalData(content); | |
s = null; | |
if(auto fd = cast(FunctionDeclaration) decl.getAstNode()) | |
comment.writeDetails(output, fd, decl.getProcessedUnittests()); | |
else if(auto fd = cast(Constructor) decl.getAstNode()) | |
comment.writeDetails(output, fd, decl.getProcessedUnittests()); | |
else if(auto fd = cast(TemplateDeclaration) decl.getAstNode()) | |
comment.writeDetails(output, fd, decl.getProcessedUnittests()); | |
else if(auto fd = cast(EponymousTemplateDeclaration) decl.getAstNode()) | |
comment.writeDetails(output, fd, decl.getProcessedUnittests()); | |
else if(auto fd = cast(AliasDecl) decl) { | |
if(fd.initializer) | |
comment.writeDetails(output, fd.initializer, decl.getProcessedUnittests()); | |
else | |
comment.writeDetails(output, decl, decl.getProcessedUnittests()); | |
} else | |
comment.writeDetails(output, decl, decl.getProcessedUnittests()); | |
content.addChild("div", Html(s)); | |
if(forReal) { | |
auto nav = document.requireElementById("page-nav"); | |
Decl[] navArray; | |
string[string] inNavArray; | |
if(decl.parent) { | |
auto iterate = decl.parent.children; | |
if(!decl.isModule && decl.parent.isModule && decl.parent.children.length == 1) { | |
// we are an only child of a module, show the module's nav instead | |
if(decl.parent.parent !is null) | |
iterate = decl.parent.parent.children; | |
} | |
foreach(child; iterate) { | |
if(cast(ImportDecl) child) continue; // do not document public imports here, they belong only on the inside | |
if(child.docsShouldBeOutputted) { | |
// strip overloads from sidebar | |
if(child.name !in inNavArray) { | |
navArray ~= child; | |
inNavArray[child.name] = ""; | |
} | |
} | |
} | |
} else { | |
/+ commented pending removal | |
// this is for building the module nav when doing an incremental | |
// rebuild. It loads the index.xml made with the special option below. | |
static bool attemptedXmlLoad; | |
static ModuleDecl[] indexedModules; | |
if(!attemptedXmlLoad) { | |
import std.file; | |
if(std.file.exists("index.xml")) { | |
auto idx = new XmlDocument(readText("index.xml")); | |
foreach(d; idx.querySelectorAll("listing > decl")) | |
indexedModules ~= new ModuleDecl(d.requireSelector("name").innerText); | |
} | |
attemptedXmlLoad = true; | |
} | |
auto tm = cast(ModuleDecl) decl; | |
if(tm !is null) | |
foreach(im; indexedModules) | |
if(im.packageName == tm.packageName) | |
navArray ~= im; | |
+/ | |
} | |
{ | |
auto p = decl.parent; | |
while(p) { | |
// cut off package names that would be repeated | |
auto name = (p.isModule && p.parent) ? lastDotOnly(p.name) : p.name; | |
if(name == "index" && p.fakeDecl) | |
break; | |
nav.prependChild(Element.make("a", name, p.link(true))).addClass("parent"); | |
p = p.parent; | |
} | |
} | |
import std.algorithm; | |
sort!sorter(navArray); | |
Element list; | |
string lastType; | |
foreach(item; navArray) { | |
if(item.declarationType != lastType) { | |
nav.addChild("span", pluralize(item.declarationType)).addClass("type-separator"); | |
list = nav.addChild("ul"); | |
lastType = item.declarationType; | |
} | |
string name; | |
if(item.isArticle) { | |
auto mod = cast(ModuleDecl) item; | |
name = mod.justDocsTitle; | |
} else { | |
// cut off package names that would be repeated | |
name = (item.isModule && item.parent) ? lastDotOnly(item.name) : item.name; | |
} | |
auto n = list.addChild("li").addChild("a", name, item.link).addClass(item.declarationType.replace(" ", "-")); | |
if(item.name == decl.name || name == decl.name) | |
n.addClass("current"); | |
} | |
if(justDocs) { | |
if(auto d = document.querySelector("#details")) | |
d.removeFromTree; | |
} | |
auto toc = Element.make("div"); | |
toc.id = "table-of-contents"; | |
auto current = toc; | |
int lastLevel; | |
tree: foreach(header; document.root.tree) { | |
int level; | |
switch(header.tagName) { | |
case "h2": | |
level = 2; | |
break; | |
case "h3": | |
level = 3; | |
break; | |
case "h4": | |
level = 4; | |
break; | |
case "h5:": | |
level = 5; | |
break; | |
case "h6": | |
level = 6; | |
break; | |
default: break; | |
} | |
if(level == 0) continue; | |
bool addToIt = true; | |
if(header.hasClass("hide-from-toc")) | |
addToIt = false; | |
Element addTo; | |
if(addToIt) { | |
auto parentCheck = header; | |
while(parentCheck) { | |
if(parentCheck.hasClass("adrdox-sample")) | |
continue tree; | |
parentCheck = parentCheck.parentNode; | |
} | |
if(level > lastLevel) { | |
current = current.addChild("ol"); | |
current.addClass("heading-level-" ~ to!string(level)); | |
} else if(level < lastLevel) { | |
while(current && !current.hasClass("heading-level-" ~ to!string(level))) | |
current = current.parentNode; | |
if(current is null) { | |
import std.stdio; | |
writeln("WARNING: TOC broken on " ~ decl.name); | |
goto skip_toc; | |
} | |
assert(current !is null); | |
} | |
lastLevel = level; | |
addTo = current; | |
if(addTo.tagName != "ol") | |
addTo = addTo.parentNode; | |
} | |
if(!header.hasAttribute("id")) | |
header.attrs.id = toId(header.innerText); | |
if(header.querySelector(" > *") is null) { | |
auto selfLink = Element.make("a", header.innerText, "#" ~ header.attrs.id); | |
selfLink.addClass("header-anchor"); | |
header.innerHTML = selfLink.toString(); | |
} | |
if(addToIt) | |
addTo.addChild("li", Element.make("a", header.innerText, "#" ~ header.attrs.id)); | |
} | |
if(auto d = document.querySelector("#more-link")) { | |
if(document.querySelectorAll(".user-header:not(.hide-from-toc)").length > 2) | |
d.replaceWith(toc); | |
} | |
skip_toc: {} | |
if(auto a = document.querySelector(".annotated-prototype")) | |
outer: foreach(c; a.querySelectorAll(".parameters-list")) { | |
auto p = c.parentNode; | |
while(p) { | |
if(p.hasClass("lambda-expression")) | |
continue outer; | |
p = p.parentNode; | |
} | |
c.addClass("toplevel"); | |
} | |
// for line numbering | |
foreach(pre; document.querySelectorAll("pre.highlighted, pre.block-code[data-language!=\"\"]")) { | |
addLineNumbering(pre); | |
} | |
string overloadLink; | |
string declLink = decl.link(true, &overloadLink); | |
if(usePseudoFiles) { | |
pseudoFiles[declLink] = document.toString(); | |
if(overloadLink.length) | |
pseudoFiles[overloadLink] = redirectToOverloadHtml(declLink); | |
} else { | |
writeFile(outputDirectory ~ declLink, document.toString(), gzip); | |
if(overloadLink.length) | |
writeFile(outputDirectory ~ overloadLink, redirectToOverloadHtml(declLink), gzip); | |
} | |
import std.stdio; | |
writeln("WRITTEN TO ", declLink); | |
} | |
return document; | |
} | |
string redirectToOverloadHtml(string what) { | |
return `<script>location.href = '`~what~`';</script> <a href="`~what~`">Continue to overload</a>`; | |
} | |
void addLineNumbering(Element pre, bool id = false) { | |
if(pre.hasClass("with-line-wrappers")) | |
return; | |
string html; | |
int count; | |
foreach(idx, line; pre.innerHTML.splitLines) { | |
auto num = to!string(idx + 1); | |
auto href = "L"~num; | |
if(id) | |
html ~= "<a class=\"br\""~(id ? " id=\""~href~"\"" : "")~" href=\"#"~href~"\">"~num~" </a>"; | |
else | |
html ~= "<span class=\"br\">"~num~" </span>"; | |
html ~= line; | |
html ~= "\n"; | |
count++; | |
} | |
if(count < 5) | |
return; // no point cluttering the display with the sample is so small you can eyeball it instantly anyway | |
pre.innerHTML = html.stripRight; | |
pre.addClass("with-line-wrappers"); | |
if(count >= 10000) | |
pre.addClass("ten-thousand-lines"); | |
else if(count >= 1000) | |
pre.addClass("thousand-lines"); | |
} | |
string lastDotOnly(string s) { | |
auto idx = s.lastIndexOf("."); | |
if(idx == -1) return s; | |
return s[idx + 1 .. $]; | |
} | |
struct InheritanceResult { | |
Decl decl; // may be null | |
string plainText; | |
//const(BaseClass) ast; | |
} | |
Decl[] declsByUda(string uda, Decl start = null) { | |
if(start is null) { | |
assert(0); // cross-module search not implemented here | |
} | |
Decl[] list; | |
if(start.hasUda(uda)) | |
list ~= start; | |
foreach(child; start.children) | |
list ~= declsByUda(uda, child); | |
return list; | |
} | |
abstract class Decl { | |
bool fakeDecl = false; | |
bool alreadyGenerated = false; | |
abstract string name(); | |
abstract string comment(); | |
abstract string rawComment(); | |
abstract string declarationType(); | |
abstract const(ASTNode) getAstNode(); | |
abstract int lineNumber(); | |
//abstract string sourceCode(); | |
abstract void getAnnotatedPrototype(MyOutputRange); | |
abstract void getSimplifiedPrototype(MyOutputRange); | |
DocComment parsedDocComment_; | |
final @property DocComment parsedDocComment() { | |
if(parsedDocComment_ is DocComment.init) | |
parsedDocComment_ = parseDocumentationComment(comment, this); | |
return parsedDocComment_; | |
} | |
void getAggregatePrototype(MyOutputRange r) { | |
getSimplifiedPrototype(r); | |
r.put(";"); | |
} | |
/* virtual */ void addSupplementalData(Element) {} | |
// why is this needed?!?!?!?!? | |
override int opCmp(Object o) { | |
return cast(int)cast(void*)this - cast(int)cast(void*)o; | |
} | |
Decl parentModule() { | |
auto p = this; | |
while(p) { | |
if(p.isModule()) | |
return p; | |
p = p.parent; | |
} | |
assert(0); | |
} | |
Decl previousSibling() { | |
if(parent is null) | |
return null; | |
Decl prev; | |
foreach(child; parent.children) { | |
if(child is this) | |
return prev; | |
prev = child; | |
} | |
return null; | |
} | |
bool isDocumented() { | |
// this shouldn't be needed anymore cuz the recursive check below does a better job | |
//if(this.isModule) | |
//return true; // allow undocumented modules because then it will at least descend into documented children | |
// skip modules with "internal" because they are usually not meant | |
// to be publicly documented anyway | |
{ | |
auto mod = this.parentModule.name; | |
if(mod.indexOf(".internal") != -1 && !documentInternal) | |
return false; | |
} | |
if(documentUndocumented) | |
return true; | |
if(comment.length) // hack | |
return comment.length > 0; // cool, not a hack | |
// if it has any documented children, we want to pretend this is documented too | |
// since then it will be possible to navigate to it | |
foreach(child; children) | |
if(child.docsShouldBeOutputted()) | |
return true; | |
// what follows is all filthy hack | |
// the C bindings in druntime are not documented, but | |
// we want them to show up. So I'm gonna hack it. | |
/* | |
auto mod = this.parentModule.name; | |
if(mod.startsWith("core")) | |
return true; | |
*/ | |
return false; | |
} | |
bool isStatic() { | |
foreach (a; attributes) { | |
if(a.attr && a.attr.attribute.type == tok!"static") | |
return true; | |
// gshared also implies static (though note that shared does not!) | |
if(a.attr && a.attr.attribute.type == tok!"__gshared") | |
return true; | |
} | |
return false; | |
} | |
bool isPrivate() { | |
IdType protection; | |
foreach (a; attributes) { | |
if (a.attr && isProtection(a.attr.attribute.type)) | |
protection = a.attr.attribute.type; | |
} | |
return protection == tok!"private"; | |
} | |
bool docsShouldBeOutputted() { | |
if((!this.isPrivate || writePrivateDocs) && this.isDocumented) | |
return true; | |
else if(this.comment.indexOf("$(ALWAYS_DOCUMENT)") != -1) | |
return true; | |
return false; | |
} | |
final bool hasUda(string name) { | |
foreach(a; attributes) | |
if(a.attr && a.attr.atAttribute && a.attr.atAttribute.identifier.text == name) | |
return true; | |
return false; | |
} | |
// FIXME: isFinal and isVirtual | |
// FIXME: it would be nice to inherit documentation from interfaces too. | |
bool isProperty() { | |
foreach (a; attributes) { | |
if(a.attr && a.attr.atAttribute && a.attr.atAttribute.identifier.text == "property") | |
return true; | |
} | |
return false; | |
} | |
bool isAggregateMember() { | |
return parent ? !parent.isModule : false; // FIXME? | |
} | |
// does NOT look for aliased overload sets, just ones right in this scope | |
// includes this in the return (plus eponymous check). Check if overloaded with .length > 1 | |
Decl[] getImmediateDocumentedOverloads() { | |
Decl[] ret; | |
if(this.parent !is null) { | |
foreach(child; this.parent.children) { | |
if(((cast(ImportDecl) child) is null) && child.name == this.name && child.docsShouldBeOutputted()) | |
ret ~= child; | |
} | |
if(auto t = cast(TemplateDecl) this.parent) | |
if(this is t.eponymousMember) { | |
foreach(i; t.getImmediateDocumentedOverloads()) | |
if(i !is t) | |
ret ~= i; | |
} | |
} | |
return ret; | |
} | |
Decl[] getDittos() { | |
if(this.parent is null) | |
return null; | |
size_t lastNonDitto; | |
foreach(idx, child; this.parent.children) { | |
if(!child.isDitto()) | |
lastNonDitto = idx; | |
if(child is this) { | |
break; | |
} | |
} | |
size_t stop = lastNonDitto; | |
foreach(idx, child; this.parent.children[lastNonDitto + 1 .. $]) | |
if(child.isDitto()) | |
stop = idx + lastNonDitto + 1 + 1; // one +1 is offset of begin, other is to make sure it is inclusive | |
else | |
break; | |
return this.parent.children[lastNonDitto .. stop]; | |
} | |
string link(bool forFile = false, string* masterOverloadName = null) { | |
auto linkTo = this; | |
if(!forFile && this.isModule && this.children.length == 1) { | |
linkTo = this.children[0]; | |
} | |
auto n = linkTo.fullyQualifiedName(); | |
auto overloads = linkTo.getImmediateDocumentedOverloads(); | |
if(overloads.length > 1) { | |
int number = 1; | |
int goodNumber; | |
foreach(overload; overloads) { | |
if(overload is this) { | |
goodNumber = number; | |
break; | |
} | |
number++; | |
} | |
if(goodNumber) | |
number = goodNumber; | |
else | |
number = 1; | |
if(masterOverloadName !is null) | |
*masterOverloadName = n.idup; | |
import std.conv : text; | |
n ~= text(".", number); | |
} | |
n ~= ".html"; | |
if(masterOverloadName !is null) | |
*masterOverloadName ~= ".html"; | |
if(!forFile) { | |
string d = getDirectoryForPackage(linkTo.fullyQualifiedName()); | |
if(d.length) { | |
n = d ~ n; | |
if(masterOverloadName !is null) | |
*masterOverloadName = d ~ *masterOverloadName; | |
} | |
} | |
return n; | |
} | |
string[] parentNameList() { | |
string[] fqn = [name()]; | |
auto p = parent; | |
while(p) { | |
fqn = p.name() ~ fqn; | |
p = p.parent; | |
} | |
return fqn; | |
} | |
string fullyQualifiedName() { | |
string fqn = name(); | |
if(isModule) | |
return fqn; | |
auto p = parent; | |
while(p) { | |
fqn = p.name() ~ "." ~ fqn; | |
if(p.isModule) | |
break; // do NOT want package names in here | |
p = p.parent; | |
} | |
return fqn; | |
} | |
final InheritanceResult[] inheritsFrom() { | |
if(!inheritsFromProcessed) | |
foreach(ref i; _inheritsFrom) | |
if(this.parent && i.plainText.length) { | |
i.decl = this.parent.lookupName(i.plainText); | |
} | |
inheritsFromProcessed = true; | |
return _inheritsFrom; | |
} | |
InheritanceResult[] _inheritsFrom; | |
bool inheritsFromProcessed = false; | |
Decl[string] nameTable; | |
bool nameTableBuilt; | |
Decl[string] buildNameTable(string[] excludeModules = null) { | |
if(!nameTableBuilt) { | |
lookup: foreach(mod; this.importedModules) { | |
if(!mod.publicImport) | |
continue; | |
if(auto modDeclPtr = mod.name in modulesByName) { | |
auto modDecl = *modDeclPtr; | |
foreach(imod; excludeModules) | |
if(imod == modDeclPtr.name) | |
break lookup; | |
auto tbl = modDecl.buildNameTable(excludeModules ~ this.parentModule.name); | |
foreach(k, v; tbl) | |
nameTable[k] = v; | |
} | |
} | |
foreach(child; children) | |
nameTable[child.name] = child; | |
nameTableBuilt = true; | |
} | |
return nameTable; | |
} | |
// the excludeModules is meant to prevent circular lookups | |
Decl lookupName(string name, bool lookUp = true, string[] excludeModules = null) { | |
if(importedModules.length == 0 || importedModules[$-1].name != "object") | |
addImport("object", false); | |
if(name.length == 0) | |
return null; | |
string originalFullName = name; | |
auto subject = this; | |
if(name[0] == '.') { | |
// global scope operator | |
while(subject && !subject.isModule) | |
subject = subject.parent; | |
name = name[1 .. $]; | |
originalFullName = originalFullName[1 .. $]; | |
} | |
auto firstDotIdx = name.indexOf("."); | |
if(firstDotIdx != -1) { | |
subject = subject.lookupName(name[0 .. firstDotIdx]); | |
name = name[firstDotIdx + 1 .. $]; | |
} | |
if(subject) | |
while(subject) { | |
auto table = subject.buildNameTable(); | |
if(name in table) | |
return table[name]; | |
if(lookUp) | |
// at the top level, we also need to check private imports | |
lookup: foreach(mod; subject.importedModules) { | |
if(mod.publicImport) | |
continue; // handled by the name table | |
auto lookupInsideModule = originalFullName; | |
if(auto modDeclPtr = mod.name in modulesByName) { | |
auto modDecl = *modDeclPtr; | |
foreach(imod; excludeModules) | |
if(imod == modDeclPtr.name) | |
break lookup; | |
//import std.stdio; writeln(modDecl.name, " ", lookupInsideModule); | |
auto located = modDecl.lookupName(lookupInsideModule, false, excludeModules ~ this.parentModule.name); | |
if(located !is null) | |
return located; | |
} | |
} | |
if(!lookUp || subject.isModule) | |
subject = null; | |
else | |
subject = subject.parent; | |
} | |
else { | |
// FIXME? | |
// fully qualified name from this module | |
subject = this; | |
if(originalFullName.startsWith(this.parentModule.name ~ ".")) { | |
// came from here! | |
auto located = this.parentModule.lookupName(originalFullName[this.parentModule.name.length + 1 .. $]); | |
if(located !is null) | |
return located; | |
} else | |
while(subject !is null) { | |
foreach(mod; subject.importedModules) { | |
if(originalFullName.startsWith(mod.name ~ ".")) { | |
// fully qualified name from this module | |
auto lookupInsideModule = originalFullName[mod.name.length + 1 .. $]; | |
if(auto modDeclPtr = mod.name in modulesByName) { | |
auto modDecl = *modDeclPtr; | |
auto located = modDecl.lookupName(lookupInsideModule, mod.publicImport); | |
if(located !is null) | |
return located; | |
} | |
} | |
} | |
if(lookUp && subject.isModule) | |
subject = null; | |
else | |
subject = subject.parent; | |
} | |
} | |
return null; | |
} | |
final Decl lookupName(const IdentifierOrTemplateInstance ic, bool lookUp = true) { | |
auto subject = this; | |
if(ic.templateInstance) | |
return null; // FIXME | |
return lookupName(ic.identifier.text, lookUp); | |
} | |
final Decl lookupName(const IdentifierChain ic) { | |
auto subject = this; | |
assert(ic.identifiers.length); | |
// FIXME: leading dot? | |
foreach(idx, ident; ic.identifiers) { | |
subject = subject.lookupName(ident.text, idx == 0); | |
if(subject is null) return null; | |
} | |
return subject; | |
} | |
final Decl lookupName(const IdentifierOrTemplateChain ic) { | |
auto subject = this; | |
assert(ic.identifiersOrTemplateInstances.length); | |
// FIXME: leading dot? | |
foreach(idx, ident; ic.identifiersOrTemplateInstances) { | |
subject = subject.lookupName(ident, idx == 0); | |
if(subject is null) return null; | |
} | |
return subject; | |
} | |
final Decl lookupName(const Symbol ic) { | |
// FIXME dot | |
return lookupName(ic.identifierOrTemplateChain); | |
} | |
Decl parent; | |
Decl[] children; | |
void writeTemplateConstraint(MyOutputRange output); | |
const(VersionOrAttribute)[] attributes; | |
void addChild(Decl decl) { | |
decl.parent = this; | |
children ~= decl; | |
} | |
struct ImportedModule { | |
string name; | |
bool publicImport; | |
} | |
ImportedModule[] importedModules; | |
void addImport(string moduleName, bool isPublic) { | |
importedModules ~= ImportedModule(moduleName, isPublic); | |
} | |
struct Unittest { | |
const(dparse.ast.Unittest) ut; | |
string code; | |
string comment; | |
} | |
Unittest[] unittests; | |
void addUnittest(const(dparse.ast.Unittest) ut, const(ubyte)[] code, string comment) { | |
int slicePoint = 0; | |
foreach(idx, b; code) { | |
if(b == ' ' || b == '\t' || b == '\r') | |
slicePoint++; | |
else if(b == '\n') { | |
slicePoint++; | |
break; | |
} else { | |
slicePoint = 0; | |
break; | |
} | |
} | |
code = code[slicePoint .. $]; | |
unittests ~= Unittest(ut, unittestCodeToString(code), comment); | |
} | |
string unittestCodeToString(const(ubyte)[] code) { | |
auto excludeString = cast(const(ubyte[])) "// exclude from docs"; | |
bool replacementMade; | |
import std.algorithm.searching; | |
auto idx = code.countUntil(excludeString); | |
while(idx != -1) { | |
int before = cast(int) idx; | |
int after = cast(int) idx; | |
while(before > 0 && code[before] != '\n') | |
before--; | |
while(after < code.length && code[after] != '\n') | |
after++; | |
code = code[0 .. before] ~ code[after .. $]; | |
replacementMade = true; | |
idx = code.countUntil(excludeString); | |
} | |
if(!replacementMade) | |
return (cast(char[]) code).idup; // needs to be unique | |
else | |
return cast(string) code; // already copied above, so it is unique | |
} | |
struct ProcessedUnittest { | |
string code; | |
string comment; | |
bool embedded; | |
} | |
bool _unittestsProcessed; | |
ProcessedUnittest[] _processedUnittests; | |
ProcessedUnittest[] getProcessedUnittests() { | |
if(_unittestsProcessed) | |
return _processedUnittests; | |
_unittestsProcessed = true; | |
// source, comment | |
ProcessedUnittest[] ret; | |
Decl start = this; | |
if(isDitto()) { | |
foreach(child; this.parent.children) { | |
if(child is this) | |
break; | |
if(!child.isDitto()) | |
start = child; | |
} | |
} | |
bool started = false; | |
if(this.parent) | |
foreach(child; this.parent.children) { | |
if(started) { | |
if(!child.isDitto()) | |
break; | |
} else { | |
if(child is start) | |
started = true; | |
} | |
if(started) | |
foreach(test; child.unittests) | |
if(test.comment.length) | |
ret ~= ProcessedUnittest(test.code, test.comment); | |
} | |
else | |
foreach(test; this.unittests) | |
if(test.comment.length) | |
ret ~= ProcessedUnittest(test.code, test.comment); | |
_processedUnittests = ret; | |
return ret; | |
} | |
override string toString() { | |
string s; | |
s ~= super.toString() ~ " " ~ this.name(); | |
foreach(child; children) { | |
s ~= "\n"; | |
auto p = parent; | |
while(p) { | |
s ~= "\t"; | |
p = p.parent; | |
} | |
s ~= child.toString(); | |
} | |
return s; | |
} | |
abstract bool isDitto(); | |
bool isModule() { return false; } | |
bool isArticle() { return false; } | |
bool isConstructor() { return false; } | |
bool aliasThisPresent; | |
Token aliasThisToken; | |
string aliasThisComment; | |
Decl aliasThis() { | |
if(!aliasThisPresent) | |
return null; | |
else | |
return lookupName(aliasThisToken.text, false); | |
} | |
DestructorDecl destructor() { | |
foreach(child; children) | |
if(auto dd = cast(DestructorDecl) child) | |
return dd; | |
return null; | |
} | |
PostblitDecl postblit() { | |
foreach(child; children) | |
if(auto dd = cast(PostblitDecl) child) | |
return dd; | |
return null; | |
} | |
abstract bool isDisabled(); | |
ConstructorDecl disabledDefaultConstructor() { | |
foreach(child; children) | |
if(child.isConstructor() && child.isDisabled()) { | |
auto ctor = cast(ConstructorDecl) child; | |
if(ctor.astNode.parameters || ctor.astNode.parameters.parameters.length == 0) | |
return ctor; | |
} | |
return null; | |
} | |
} | |
class ModuleDecl : Decl { | |
mixin CtorFrom!Module defaultMixins; | |
string justDocsTitle; | |
override bool isModule() { return true; } | |
override bool isArticle() { return justDocsTitle.length > 0; } | |
override string declarationType() { | |
return isArticle() ? "Article" : "module"; | |
} | |
version(none) | |
override void getSimplifiedPrototype(MyOutputRange r) { | |
if(isArticle()) | |
r.put(justDocsTitle); | |
else | |
defaultMixins.getSimplifiedPrototype(r); | |
} | |
ubyte[] originalSource; | |
string packageName() { | |
auto it = this.name(); | |
auto idx = it.lastIndexOf("."); | |
if(idx == -1) | |
return null; | |
return it[0 .. idx]; | |
} | |
} | |
class AliasDecl : Decl { | |
mixin CtorFrom!AliasDeclaration; | |
this(const(AliasDeclaration) ad, const(VersionOrAttribute)[] attributes) { | |
this.attributes = attributes; | |
this.astNode = ad; | |
this.initializer = null; | |
// deal with the type and initializer list and storage classes | |
} | |
const(AliasInitializer) initializer; | |
this(const(AliasDeclaration) ad, const(AliasInitializer) init, const(VersionOrAttribute)[] attributes) { | |
this.attributes = attributes; | |
this.astNode = ad; | |
this.initializer = init; | |
// deal with init | |
} | |
override string name() { | |
if(initializer is null) | |
return toText(astNode.identifierList); | |
else | |
return initializer.name.text; | |
} | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
void cool() { | |
output.putTag("<div class=\"declaration-prototype\">"); | |
if(parent !is null && !parent.isModule) { | |
output.putTag("<div class=\"parent-prototype\""); | |
parent.getSimplifiedPrototype(output); | |
output.putTag("</div><div>"); | |
getPrototype(output, true); | |
output.putTag("</div>"); | |
} else { | |
getPrototype(output, true); | |
} | |
output.putTag("</div>"); | |
} | |
writeOverloads!cool(this, output); | |
} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
getPrototype(output, false); | |
} | |
void getPrototype(MyOutputRange output, bool link) { | |
// FIXME: storage classes? | |
if(link) { | |
auto f = new MyFormatter!(typeof(output))(output, this); | |
writeAttributes(f, output, this.attributes); | |
} | |
output.putTag("<span class=\"builtin-type\">alias</span> "); | |
output.putTag("<span class=\"name\">"); | |
output.put(name); | |
output.putTag("</span>"); | |
if(initializer && initializer.templateParameters) { | |
output.putTag(toHtml(initializer.templateParameters).source); | |
} | |
output.put(" = "); | |
if(initializer) { | |
if(link) | |
output.putTag(toLinkedHtml(initializer.type, this).source); | |
else | |
output.putTag(toHtml(initializer.type).source); | |
} | |
if(astNode.type) { | |
if(link) { | |
auto t = toText(astNode.type); | |
auto decl = lookupName(t); | |
if(decl is null) | |
goto nulldecl; | |
output.putTag(getReferenceLink(t, decl).toString); | |
} else { | |
nulldecl: | |
output.putTag(toHtml(astNode.type).source); | |
} | |
} | |
} | |
} | |
class VariableDecl : Decl { | |
mixin CtorFrom!VariableDeclaration; | |
const(Declarator) declarator; | |
this(const(Declarator) declarator, const(VariableDeclaration) astNode, const(VersionOrAttribute)[] attributes) { | |
this.astNode = astNode; | |
this.declarator = declarator; | |
this.attributes = attributes; | |
this.ident = Token.init; | |
this.initializer = null; | |
} | |
const(Token) ident; | |
const(Initializer) initializer; | |
this(const(VariableDeclaration) astNode, const(Token) ident, const(Initializer) initializer, const(VersionOrAttribute)[] attributes, bool isEnum) { | |
this.declarator = null; | |
this.attributes = attributes; | |
this.astNode = astNode; | |
this.ident = ident; | |
this.isEnum = isEnum; | |
this.initializer = initializer; | |
} | |
bool isEnum; | |
override string name() { | |
if(declarator) | |
return declarator.name.text; | |
else | |
return ident.text; | |
} | |
override string rawComment() { | |
string it = astNode.comment; | |
auto additional = (declarator ? declarator.comment : astNode.autoDeclaration.comment); | |
if(additional != it) | |
it ~= additional; | |
return it; | |
} | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
output.putTag("<div class=\"declaration-prototype\">"); | |
if(parent !is null && !parent.isModule) { | |
output.putTag("<div class=\"parent-prototype\""); | |
parent.getSimplifiedPrototype(output); | |
output.putTag("</div><div>"); | |
auto f = new MyFormatter!(typeof(output))(output); | |
writeAttributes(f, output, attributes); | |
getSimplifiedPrototypeInternal(output, true); | |
output.putTag("</div>"); | |
} else { | |
auto f = new MyFormatter!(typeof(output))(output); | |
writeAttributes(f, output, attributes); | |
getSimplifiedPrototypeInternal(output, true); | |
} | |
output.putTag("</div>"); | |
} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
getSimplifiedPrototypeInternal(output, false); | |
} | |
final void getSimplifiedPrototypeInternal(MyOutputRange output, bool link) { | |
foreach(sc; astNode.storageClasses) { | |
output.putTag(toHtml(sc).source); | |
output.put(" "); | |
} | |
if(astNode.type) { | |
if(link) { | |
auto html = toHtml(astNode.type).source; | |
auto txt = toText(astNode.type); | |
auto typeDecl = lookupName(txt); | |
if(typeDecl is null || !typeDecl.docsShouldBeOutputted) | |
goto plain; | |
output.putTag("<a title=\""~typeDecl.fullyQualifiedName~"\" href=\""~typeDecl.link~"\">" ~ html ~ "</a>"); | |
} else { | |
plain: | |
output.putTag(toHtml(astNode.type).source); | |
} | |
} else | |
output.putTag("<span class=\"builtin-type\">"~(isEnum ? "enum" : "auto")~"</span>"); | |
output.put(" "); | |
output.putTag("<span class=\"name\">"); | |
output.put(name); | |
output.putTag("</span>"); | |
if(declarator && declarator.templateParameters) | |
output.putTag(toHtml(declarator.templateParameters).source); | |
if(link) { | |
if(initializer !is null) { | |
output.put(" = "); | |
output.putTag(toHtml(initializer).source); | |
} | |
} | |
output.put(";"); | |
} | |
override void getAggregatePrototype(MyOutputRange output) { | |
auto f = new MyFormatter!(typeof(output))(output); | |
writeAttributes(f, output, attributes); | |
getSimplifiedPrototypeInternal(output, false); | |
} | |
override string declarationType() { | |
return (isStatic() ? "static variable" : (isEnum ? "manifest constant" : "variable")); | |
} | |
} | |
class FunctionDecl : Decl { | |
mixin CtorFrom!FunctionDeclaration; | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
doFunctionDec(this, output); | |
} | |
override Decl lookupName(string name, bool lookUp = true, string[] excludeModules = null) { | |
// is it a param or template param? If so, return that. | |
foreach(param; astNode.parameters.parameters) { | |
if (param.name.type != tok!"") | |
if(param.name.text == name) { | |
return null; // it is local, but we don't have a decl.. | |
} | |
} | |
if(astNode.templateParameters && astNode.templateParameters.templateParameterList && astNode.templateParameters.templateParameterList.items) | |
foreach(param; astNode.templateParameters.templateParameterList.items) { | |
auto paramName = ""; | |
if(param.templateTypeParameter) | |
paramName = param.templateTypeParameter.identifier.text; | |
else if(param.templateValueParameter) | |
paramName = param.templateValueParameter.identifier.text; | |
else if(param.templateAliasParameter) | |
paramName = param.templateAliasParameter.identifier.text; | |
else if(param.templateTupleParameter) | |
paramName = param.templateTupleParameter.identifier.text; | |
if(paramName.length && paramName == name) { | |
return null; // it is local, but we don't have a decl.. | |
} | |
} | |
if(lookUp) | |
return super.lookupName(name, lookUp, excludeModules); | |
else | |
return null; | |
} | |
override string declarationType() { | |
return isProperty() ? "property" : (isStatic() ? "static function" : "function"); | |
} | |
override void getAggregatePrototype(MyOutputRange output) { | |
if(isStatic()) { | |
output.putTag("<span class=\"storage-class\">static</span> "); | |
} | |
getSimplifiedPrototype(output); | |
output.put(";"); | |
} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
foreach(sc; astNode.storageClasses) { | |
output.putTag(toHtml(sc).source); | |
output.put(" "); | |
} | |
if(isProperty() && (paramCount == 0 || paramCount == 1 || (paramCount == 2 && !isAggregateMember))) { | |
if((paramCount == 1 && isAggregateMember()) || (paramCount == 2 && !isAggregateMember())) { | |
// setter | |
output.putTag(toHtml(astNode.parameters.parameters[0].type).source); | |
output.put(" "); | |
output.putTag("<span class=\"name\">"); | |
output.put(name); | |
output.putTag("</span>"); | |
output.put(" [@property setter]"); | |
} else { | |
// getter | |
putSimplfiedReturnValue(output, astNode); | |
output.put(" "); | |
output.putTag("<span class=\"name\">"); | |
output.put(name); | |
output.putTag("</span>"); | |
output.put(" [@property getter]"); | |
} | |
} else { | |
putSimplfiedReturnValue(output, astNode); | |
output.put(" "); | |
output.putTag("<span class=\"name\">"); | |
output.put(name); | |
output.putTag("</span>"); | |
putSimplfiedArgs(output, astNode); | |
} | |
} | |
int paramCount() { | |
return cast(int) astNode.parameters.parameters.length; | |
} | |
} | |
class ConstructorDecl : Decl { | |
mixin CtorFrom!Constructor; | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
doFunctionDec(this, output); | |
} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
output.putTag("<span class=\"lang-feature name\">"); | |
output.put("this"); | |
output.putTag("</span>"); | |
putSimplfiedArgs(output, astNode); | |
} | |
override bool isConstructor() { return true; } | |
} | |
class DestructorDecl : Decl { | |
mixin CtorFrom!Destructor; | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
output.putTag("<span class=\"lang-feature name\">"); | |
output.put("~this"); | |
output.putTag("</span>"); | |
output.put("()"); | |
} | |
} | |
class PostblitDecl : Decl { | |
mixin CtorFrom!Postblit; | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
if(isDisabled) { | |
output.putTag("<span class=\"builtin-type\">"); | |
output.put("@disable"); | |
output.putTag("</span>"); | |
output.put(" "); | |
} | |
output.putTag("<span class=\"lang-feature name\">"); | |
output.put("this(this)"); | |
output.putTag("</span>"); | |
} | |
} | |
class ImportDecl : Decl { | |
mixin CtorFrom!ImportDeclaration; | |
bool isPublic; | |
string newName; | |
string oldName; | |
override string link(bool forFile = false, string* useless = null) { | |
string d; | |
if(!forFile) { | |
d = getDirectoryForPackage(oldName); | |
} | |
return d ~ oldName ~ ".html"; | |
} | |
// I also want to document undocumented public imports, since they also spam up the namespace | |
override bool docsShouldBeOutputted() { | |
return isPublic; | |
} | |
override string name() { | |
return newName.length ? newName : oldName; | |
} | |
override string declarationType() { | |
return "import"; | |
} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
if(isPublic) | |
output.putTag("<span class=\"builtin-type\">public</span> "); | |
output.putTag(toHtml(astNode).source); | |
} | |
} | |
class MixedInTemplateDecl : Decl { | |
mixin CtorFrom!TemplateMixinExpression; | |
override string declarationType() { | |
return "mixin"; | |
} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
output.putTag(toHtml(astNode).source); | |
} | |
} | |
class StructDecl : Decl { | |
mixin CtorFrom!StructDeclaration; | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
} | |
class UnionDecl : Decl { | |
mixin CtorFrom!UnionDeclaration; | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
} | |
class ClassDecl : Decl { | |
mixin CtorFrom!ClassDeclaration; | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
} | |
class InterfaceDecl : Decl { | |
mixin CtorFrom!InterfaceDeclaration; | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
} | |
class TemplateDecl : Decl { | |
mixin CtorFrom!TemplateDeclaration; | |
Decl eponymousMember() { | |
foreach(child; this.children) | |
if(child.name == this.name) | |
return child; | |
return null; | |
} | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
} | |
class EponymousTemplateDecl : Decl { | |
mixin CtorFrom!EponymousTemplateDeclaration; | |
/* | |
Decl eponymousMember() { | |
foreach(child; this.children) | |
if(child.name == this.name) | |
return child; | |
return null; | |
} | |
*/ | |
override string declarationType() { | |
return "enum"; | |
} | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
} | |
class MixinTemplateDecl : Decl { | |
mixin CtorFrom!TemplateDeclaration; // MixinTemplateDeclaration does nothing interesting except this.. | |
override void getAnnotatedPrototype(MyOutputRange output) { | |
annotatedPrototype(this, output); | |
} | |
override string declarationType() { | |
return "mixin template"; | |
} | |
} | |
class EnumDecl : Decl { | |
mixin CtorFrom!EnumDeclaration; | |
override void addSupplementalData(Element content) { | |
doEnumDecl(this, content); | |
} | |
} | |
class AnonymousEnumDecl : Decl { | |
mixin CtorFrom!AnonymousEnumDeclaration; | |
override string name() { | |
assert(astNode.members.length > 0); | |
auto name = astNode.members[0].name.text; | |
return name; | |
} | |
override void addSupplementalData(Element content) { | |
doEnumDecl(this, content); | |
} | |
override string declarationType() { | |
return "enum"; | |
} | |
} | |
mixin template CtorFrom(T) { | |
const(T) astNode; | |
static if(!is(T == VariableDeclaration) && !is(T == AliasDeclaration)) | |
this(const(T) astNode, const(VersionOrAttribute)[] attributes) { | |
this.astNode = astNode; | |
this.attributes = attributes; | |
static if(is(typeof(astNode) == const(ClassDeclaration)) || is(typeof(astNode) == const(InterfaceDeclaration))) { | |
if(astNode.baseClassList) | |
foreach(idx, baseClass; astNode.baseClassList.items) { | |
auto bc = toText(baseClass); | |
InheritanceResult ir = InheritanceResult(null, bc); | |
_inheritsFrom ~= ir; | |
} | |
} | |
} | |
static if(is(T == Module)) { | |
// this is so I can load this from the index... kinda a hack | |
// it should only be used in limited circumstances | |
private string _name; | |
private this(string name) { | |
this._name = name; | |
this.astNode = null; | |
} | |
} | |
override const(T) getAstNode() { return astNode; } | |
override int lineNumber() { | |
static if(__traits(compiles, astNode.name.line)) | |
return cast(int) astNode.name.line; | |
else static if(__traits(compiles, astNode.line)) | |
return cast(int) astNode.line; | |
else static if(__traits(compiles, astNode.declarators[0].name.line)) { | |
if(astNode.declarators.length) | |
return cast(int) astNode.declarators[0].name.line; | |
} else static if(is(typeof(astNode) == const(Module))) { | |
return 0; | |
} else static assert(0, typeof(astNode).stringof); | |
return 0; | |
} | |
override void writeTemplateConstraint(MyOutputRange output) { | |
static if(__traits(compiles, astNode.constraint)) { | |
if(astNode.constraint) { | |
auto f = new MyFormatter!(typeof(output))(output); | |
output.putTag("<div class=\"template-constraint\">"); | |
f.format(astNode.constraint); | |
output.putTag("</div>"); | |
} | |
} | |
} | |
override string name() { | |
static if(is(T == Constructor)) | |
return "this"; | |
else static if(is(T == Destructor)) | |
return "~this"; | |
else static if(is(T == Postblit)) | |
return "this(this)"; | |
else static if(is(T == Module)) | |
return _name is null ? .format(astNode.moduleDeclaration.moduleName) : _name; | |
else static if(is(T == AnonymousEnumDeclaration)) | |
{ assert(0); } // overridden above | |
else static if(is(T == AliasDeclaration)) | |
{ assert(0); } // overridden above | |
else static if(is(T == VariableDeclaration)) | |
{assert(0);} // not compiled, overridden above | |
else static if(is(T == ImportDeclaration)) | |
{assert(0);} // not compiled, overridden above | |
else static if(is(T == MixinTemplateDeclaration)) { | |
return astNode.templateDeclaration.name.text; | |
} else static if(is(T == StructDeclaration) || is(T == UnionDeclaration)) | |
if(astNode.name.text.length) | |
return astNode.name.text; | |
else | |
return "__anonymous"; | |
else static if(is(T == TemplateMixinExpression)) { | |
return astNode.identifier.text.length ? astNode.identifier.text : "__anonymous"; | |
} else | |
return astNode.name.text; | |
} | |
override string comment() { | |
static if(is(T == Module)) | |
return astNode.moduleDeclaration.comment; | |
else { | |
if(isDitto()) { | |
auto ps = previousSibling; | |
while(ps && ps.rawComment.length == 0) | |
ps = ps.previousSibling; | |
return ps ? ps.comment : rawComment(); | |
} else | |
return rawComment(); | |
} | |
} | |
override void getAnnotatedPrototype(MyOutputRange) {} | |
override void getSimplifiedPrototype(MyOutputRange output) { | |
output.putTag("<span class=\"builtin-type\">"); | |
output.put(declarationType()); | |
output.putTag("</span>"); | |
output.put(" "); | |
output.putTag("<span class=\"name\">"); | |
output.put(name); | |
output.putTag("</span>"); | |
static if(__traits(compiles, astNode.templateParameters)) { | |
if(astNode.templateParameters) { | |
output.putTag("<span class=\"template-params\">"); | |
output.put(toText(astNode.templateParameters)); | |
output.putTag("</span>"); | |
} | |
} | |
} | |
override string declarationType() { | |
import std.string:toLower; | |
return toLower(typeof(this).stringof[0 .. $-4]); | |
} | |
override bool isDitto() { | |
static if(is(T == Module)) | |
return false; | |
else { | |
import std.string; | |
auto l = strip(toLower(preprocessComment(rawComment, this))); | |
if(l.length && l[$-1] == '.') | |
l = l[0 .. $-1]; | |
return l == "ditto"; | |
} | |
} | |
override string rawComment() { | |
static if(is(T == Module)) | |
return astNode.moduleDeclaration.comment; | |
else static if(is(T == MixinTemplateDeclaration)) | |
return astNode.templateDeclaration.comment; | |
else | |
return astNode.comment; | |
} | |
override bool isDisabled() { | |
foreach(attribute; attributes) | |
if(attribute.attr && attribute.attr.atAttribute && attribute.attr.atAttribute.identifier.text == "disable") | |
return true; | |
static if(__traits(compiles, astNode.memberFunctionAttributes)) | |
foreach(attribute; astNode.memberFunctionAttributes) | |
if(attribute && attribute.atAttribute && attribute.atAttribute.identifier.text == "disable") | |
return true; | |
return false; | |
} | |
} | |
ClassDecl[string] allClasses; | |
class Looker : ASTVisitor { | |
alias visit = ASTVisitor.visit; | |
const(ubyte)[] fileBytes; | |
string originalFileName; | |
this(const(ubyte)[] fileBytes, string fileName) { | |
this.fileBytes = fileBytes; | |
this.originalFileName = fileName; | |
} | |
ModuleDecl root; | |
private Decl[] stack; | |
Decl previousSibling() { | |
auto s = stack[$-1]; | |
if(s.children.length) | |
return s.children[$-1]; | |
return s; // probably a documented unittest of the module itself | |
} | |
void visitInto(D, T)(const(T) t) { | |
auto d = new D(t, attributes[$-1]); | |
stack[$-1].addChild(d); | |
stack ~= d; | |
t.accept(this); | |
stack = stack[0 .. $-1]; | |
static if(is(D == ClassDecl)) | |
allClasses[d.name] = d; | |
} | |
override void visit(const Module mod) { | |
pushAttributes(); | |
root = new ModuleDecl(mod, attributes[$-1]); | |
stack ~= root; | |
mod.accept(this); | |
assert(stack.length == 1); | |
} | |
override void visit(const FunctionDeclaration dec) { | |
stack[$-1].addChild(new FunctionDecl(dec, attributes[$-1])); | |
} | |
override void visit(const Constructor dec) { | |
stack[$-1].addChild(new ConstructorDecl(dec, attributes[$-1])); | |
} | |
override void visit(const TemplateMixinExpression dec) { | |
stack[$-1].addChild(new MixedInTemplateDecl(dec, attributes[$-1])); | |
} | |
override void visit(const Postblit dec) { | |
stack[$-1].addChild(new PostblitDecl(dec, attributes[$-1])); | |
} | |
override void visit(const Destructor dec) { | |
stack[$-1].addChild(new DestructorDecl(dec, attributes[$-1])); | |
} | |
override void visit(const StructDeclaration dec) { | |
visitInto!StructDecl(dec); | |
} | |
override void visit(const ClassDeclaration dec) { | |
visitInto!ClassDecl(dec); | |
} | |
override void visit(const UnionDeclaration dec) { | |
visitInto!UnionDecl(dec); | |
} | |
override void visit(const InterfaceDeclaration dec) { | |
visitInto!InterfaceDecl(dec); | |
} | |
override void visit(const TemplateDeclaration dec) { | |
visitInto!TemplateDecl(dec); | |
} | |
override void visit(const EponymousTemplateDeclaration dec) { | |
visitInto!EponymousTemplateDecl(dec); | |
} | |
override void visit(const MixinTemplateDeclaration dec) { | |
visitInto!MixinTemplateDecl(dec.templateDeclaration); | |
} | |
override void visit(const EnumDeclaration dec) { | |
visitInto!EnumDecl(dec); | |
} | |
override void visit(const AliasThisDeclaration dec) { | |
stack[$-1].aliasThisPresent = true; | |
stack[$-1].aliasThisToken = dec.identifier; | |
stack[$-1].aliasThisComment = dec.comment; | |
} | |
override void visit(const AnonymousEnumDeclaration dec) { | |
// we can't do anything with an empty anonymous enum, we need a name from somewhere | |
if(dec.members.length) | |
visitInto!AnonymousEnumDecl(dec); | |
} | |
override void visit(const VariableDeclaration dec) { | |
if (dec.autoDeclaration) { | |
foreach (idx, ident; dec.autoDeclaration.identifiers) { | |
stack[$-1].addChild(new VariableDecl(dec, ident, dec.autoDeclaration.initializers[idx], attributes[$-1], dec.isEnum)); | |
} | |
} else | |
foreach (const Declarator d; dec.declarators) { | |
stack[$-1].addChild(new VariableDecl(d, dec, attributes[$-1])); | |
/* | |
if (variableDeclaration.type !is null) | |
{ | |
auto f = new MyFormatter!(typeof(app))(app); | |
f.format(variableDeclaration.type); | |
} | |
output.putTag(app.data); | |
output.put(" "); | |
output.put(d.name.text); | |
comment.writeDetails(output); | |
writeToParentList("variable " ~ cast(string)app.data ~ " ", name, comment.synopsis, "variable"); | |
ascendOutOf(name); | |
*/ | |
} | |
} | |
override void visit(const AliasDeclaration dec) { | |
if(dec.initializers.length) { // alias a = b | |
foreach(init; dec.initializers) | |
stack[$-1].addChild(new AliasDecl(dec, init, attributes[$-1])); | |
} else { // alias b a; | |
// might include a type... | |
stack[$-1].addChild(new AliasDecl(dec, attributes[$-1])); | |
} | |
} | |
override void visit(const Unittest ut) { | |
//import std.stdio; writeln(fileBytes.length, " ", ut.blockStatement.startLocation, " ", ut.blockStatement.endLocation); | |
previousSibling.addUnittest( | |
ut, | |
fileBytes[ut.blockStatement.startLocation + 1 .. ut.blockStatement.endLocation], // trim off the opening and closing {} | |
ut.comment | |
); | |
} | |
override void visit(const ImportDeclaration id) { | |
bool isPublic = false; | |
foreach(a; attributes[$-1]) { | |
if (a.attr && isProtection(a.attr.attribute.type)) | |
if(a.attr.attribute.type == tok!"public") { | |
isPublic = true; | |
break; | |
} | |
} | |
void handleSingleImport(const SingleImport si) { | |
auto newName = si.rename.text; | |
auto oldName = ""; | |
foreach(idx, ident; si.identifierChain.identifiers) { | |
if(idx) | |
oldName ~= "."; | |
oldName ~= ident.text; | |
} | |
stack[$-1].addImport(oldName, isPublic); | |
// FIXME: handle the rest like newName for the import lookups | |
auto nid = new ImportDecl(id, attributes[$-1]); | |
stack[$-1].addChild(nid); | |
nid.isPublic = isPublic; | |
nid.oldName = oldName; | |
nid.newName = newName; | |
} | |
foreach(si; id.singleImports) { | |
handleSingleImport(si); | |
} | |
if(id.importBindings && id.importBindings.singleImport) | |
handleSingleImport(id.importBindings.singleImport); // FIXME: handle bindings | |
} | |
override void visit(const StructBody sb) { | |
pushAttributes(); | |
sb.accept(this); | |
popAttributes(); | |
} | |
// FIXME ???? | |
override void visit(const VersionCondition sb) { | |
attributes[$-1] ~= new VersionFakeAttribute(toText(sb.token)); | |
sb.accept(this); | |
} | |
override void visit(const BlockStatement bs) { | |
pushAttributes(); | |
bs.accept(this); | |
popAttributes(); | |
} | |
override void visit(const ConditionalDeclaration bs) { | |
pushAttributes(); | |
size_t previousConditions; | |
if(bs.compileCondition) { | |
previousConditions = attributes[$-1].length; | |
bs.compileCondition.accept(this); | |
} | |
if(bs.trueDeclarations) | |
foreach(td; bs.trueDeclarations) | |
td.accept(this); | |
if(bs.falseDeclaration) { | |
auto slice = attributes[$-1][previousConditions .. $]; | |
attributes[$-1] = attributes[$-1][0 .. previousConditions]; | |
foreach(cond; slice) | |
attributes[$-1] ~= cond.invertedClone; | |
bs.falseDeclaration.accept(this); | |
} | |
popAttributes(); | |
} | |
override void visit(const Declaration dec) { | |
auto originalAttributes = attributes[$ - 1]; | |
foreach(a; dec.attributes) | |
attributes[$ - 1] ~= new VersionOrAttribute(a); | |
dec.accept(this); | |
if (dec.attributeDeclaration is null) | |
attributes[$ - 1] = originalAttributes; | |
} | |
override void visit(const AttributeDeclaration dec) { | |
attributes[$ - 1] ~= new VersionOrAttribute(dec.attribute); | |
} | |
void pushAttributes() { | |
attributes.length = attributes.length + 1; | |
} | |
void popAttributes() { | |
attributes = attributes[0 .. $ - 1]; | |
} | |
const(VersionOrAttribute)[][] attributes; | |
} | |
string format(const IdentifierChain identifierChain) { | |
string r; | |
foreach(count, ident; identifierChain.identifiers) { | |
if (count) r ~= ("."); | |
r ~= (ident.text); | |
} | |
return r; | |
} | |
import std.algorithm : startsWith, findSplitBefore; | |
import std.string : strip; | |
//Decl[][string] packages; | |
__gshared ModuleDecl[string] modulesByName; | |
__gshared string specialPreprocessor; | |
// simplified ".gitignore" processor | |
final class GitIgnore { | |
string[] masks; // on each new dir, empty line is added to masks | |
void loadGlobalGitIgnore () { | |
import std.path; | |
import std.stdio; | |
try { | |
foreach (string s; File("~/.gitignore_global".expandTilde).byLineCopy) { | |
if (isComment(s)) continue; | |
masks ~= trim(s); | |
} | |
} catch (Exception e) {} // sorry | |
try { | |
foreach (string s; File("~/.adrdoxignore_global".expandTilde).byLineCopy) { | |
if (isComment(s)) continue; | |
masks ~= trim(s); | |
} | |
} catch (Exception e) {} // sorry | |
} | |
void loadGitIgnore (const(char)[] dir) { | |
import std.path; | |
import std.stdio; | |
masks ~= null; | |
try { | |
foreach (string s; File(buildPath(dir, ".gitignore").expandTilde).byLineCopy) { | |
if (isComment(s)) continue; | |
masks ~= trim(s); | |
} | |
} catch (Exception e) {} // sorry | |
try { | |
foreach (string s; File(buildPath(dir, ".adrdoxignore").expandTilde).byLineCopy) { | |
if (isComment(s)) continue; | |
masks ~= trim(s); | |
} | |
} catch (Exception e) {} // sorry | |
} | |
// unload latest gitignore | |
void unloadGitIgnore () { | |
auto ol = masks.length; | |
while (masks.length > 0 && masks[$-1] !is null) masks = masks[0..$-1]; | |
if (masks.length > 0 && masks[$-1] is null) masks = masks[0..$-1]; | |
if (masks.length != ol) { | |
//writeln("removed ", ol-masks.length, " lines"); | |
masks.assumeSafeAppend; //hack! | |
} | |
} | |
bool match (string fname) { | |
import std.path; | |
import std.stdio; | |
if (masks.length == 0) return false; | |
//writeln("gitignore checking: <", fname, ">"); | |
bool xmatch (string path, string mask) { | |
if (mask.length == 0 || path.length == 0) return false; | |
import std.string : indexOf; | |
if (mask.indexOf('/') < 0) return path.baseName.globMatch(mask); | |
int xpos = cast(int)path.length-1; | |
while (xpos >= 0) { | |
while (xpos > 0 && path[xpos] != '/') --xpos; | |
if (mask[0] == '/') { | |
if (xpos+1 < path.length && path[xpos+1..$].globMatch(mask)) return true; | |
} else { | |
if (path[xpos..$].globMatch(mask)) return true; | |
} | |
--xpos; | |
} | |
return false; | |
} | |
string curname = fname.baseName; | |
int pos = cast(int)masks.length-1; | |
// local dir matching | |
while (pos >= 0 && masks[pos] !is null) { | |
//writeln(" [", masks[pos], "]"); | |
if (xmatch(curname, masks[pos])) { | |
//writeln(" LOCAL HIT: [", masks[pos], "]: <", curname, ">"); | |
return true; | |
} | |
if (masks[pos][0] == '/' && xmatch(curname, masks[pos][1..$])) return true; | |
--pos; | |
} | |
curname = fname; | |
while (pos >= 0) { | |
if (masks[pos] !is null) { | |
//writeln(" [", masks[pos], "]"); | |
if (xmatch(curname, masks[pos])) { | |
//writeln(" HIT: [", masks[pos], "]: <", curname, ">"); | |
return true; | |
} | |
} | |
--pos; | |
} | |
return false; | |
} | |
static: | |
inout(char)[] trim (inout(char)[] s) { | |
while (s.length > 0 && s[0] <= ' ') s = s[1..$]; | |
while (s.length > 0 && s[$-1] <= ' ') s = s[0..$-1]; | |
return s; | |
} | |
bool isComment (const(char)[] s) { | |
s = trim(s); | |
return (s.length == 0 || s[0] == '#'); | |
} | |
} | |
string[] scanFiles (string basedir) { | |
import std.file : isDir; | |
import std.path; | |
if(basedir == "-") | |
return ["-"]; | |
string[] res; | |
auto gi = new GitIgnore(); | |
gi.loadGlobalGitIgnore(); | |
void scanSubDir(bool checkdir=true) (string dir) { | |
import std.file; | |
static if (checkdir) { | |
string d = dir; | |
if (d.length > 1 && d[$-1] == '/') d = d[0..$-1]; | |
if (gi.match(d)) { | |
//writeln("DIR SKIP: <", dir, ">"); | |
return; | |
} | |
} | |
gi.loadGitIgnore(dir); | |
scope(exit) gi.unloadGitIgnore(); | |
foreach (DirEntry de; dirEntries(dir, SpanMode.shallow)) { | |
try { | |
if (de.isDir) { scanSubDir(de.name); continue; } | |
if (de.baseName.length == 0) continue; // just in case | |
if (de.baseName[0] == '.') continue; // skip hidden files | |
if (!de.baseName.globMatch("*.d")) continue; | |
if (/*de.isFile &&*/ !gi.match(de.name)) { | |
//writeln(de.name); | |
res ~= de.name; | |
} | |
} catch (Exception e) {} // some checks (like `isDir`) can throw | |
} | |
} | |
basedir = basedir.expandTilde.absolutePath; | |
if (basedir.isDir) { | |
scanSubDir!false(basedir); | |
} else { | |
res ~= basedir; | |
} | |
return res; | |
} | |
void writeFile(string filename, string content, bool gzip) { | |
import std.zlib; | |
import std.file; | |
if(gzip) { | |
auto compress = new Compress(HeaderFormat.gzip); | |
auto data = compress.compress(content); | |
data ~= compress.flush(); | |
std.file.write(filename ~ ".gz", data); | |
} else { | |
std.file.write(filename, content); | |
} | |
} | |
__gshared bool generatingSource; | |
__gshared bool blogMode = false; | |
void main(string[] args) { | |
import std.stdio; | |
import std.path : buildPath; | |
import std.getopt; | |
static import std.file; | |
LexerConfig config; | |
StringCache stringCache = StringCache(128); | |
config.stringBehavior = StringBehavior.source; | |
config.whitespaceBehavior = WhitespaceBehavior.include; | |
ModuleDecl[] moduleDecls; | |
ModuleDecl[] moduleDeclsGenerate; | |
ModuleDecl[string] moduleDeclsGenerateByName; | |
bool makeHtml = true; | |
bool makeSearchIndex = false; | |
string[] preloadArgs; | |
string[] linkReferences; | |
bool annotateSource = false; | |
string locateSymbol = null; | |
bool gzip; | |
bool copyStandardFiles = true; | |
string headerTitle; | |
string[] headerLinks; | |
HeaderLink[] headerLinksParsed; | |
bool skipExisting = false; | |
string[] globPathInput; | |
int jobs = 0; | |
auto opt = getopt(args, | |
std.getopt.config.passThrough, | |
std.getopt.config.bundling, | |
"load", "Load for automatic cross-referencing, but do not generate for it", &preloadArgs, | |
"link-references", "A file defining global link references", &linkReferences, | |
"skeleton|s", "Location of the skeleton file, change to your use case, Default: skeleton.html", &skeletonFile, | |
"directory|o", "Output directory of the html files", &outputDirectory, | |
"write-private-docs|p", "Include documentation for `private` members (default: false)", &writePrivateDocs, | |
"write-internal-modules", "Include documentation for modules named `internal` (default: false)", &documentInternal, | |
"locate-symbol", "Locate a symbol in the passed file", &locateSymbol, | |
"genHtml|h", "Generate html, default: true", &makeHtml, | |
"genSource|u", "Generate annotated source", &annotateSource, | |
"genSearchIndex|i", "Generate search index, default: false", &makeSearchIndex, | |
"gzip|z", "Gzip generated files as they are created", &gzip, | |
"copy-standard-files", "Copy standard JS/CSS files into target directory (default: true)", ©StandardFiles, | |
"blog-mode", "Use adrdox as a static site generator for a blog", &blogMode, | |
"header-title", "Title to put on the page header", &headerTitle, | |
"header-link", "Link to add to the header (text=url)", &headerLinks, | |
"document-undocumented", "Generate documentation even for undocumented symbols", &documentUndocumented, | |
"skip-existing", "Skip file generation for modules where the html already exists in the output dir", &skipExisting, | |
"special-preprocessor", "Run a special preprocessor on comments. Only supported right now are gtk and dwt", &specialPreprocessor, | |
"jobs|j", "Number of generation jobs to run at once (default=dependent on number of cpu cores", &jobs, | |
"package-path", "Path to be prefixed to links for a particular D package namespace (package_pattern=link_prefix)", &globPathInput); | |
foreach(gpi; globPathInput) { | |
auto idx = gpi.indexOf("="); | |
string pathGlob; | |
string dir; | |
if(idx != -1) { | |
pathGlob = gpi[0 .. idx]; | |
dir = gpi[idx + 1 .. $]; | |
} else { | |
pathGlob = gpi; | |
} | |
directoriesForPackage[pathGlob] = dir; | |
} | |
generatingSource = annotateSource; | |
if (outputDirectory[$-1] != '/') | |
outputDirectory ~= '/'; | |
if (opt.helpWanted || args.length == 1) { | |
defaultGetoptPrinter("A better D documentation generator\nCopyright © Adam D. Ruppe 2016-2018\n" ~ | |
"Syntax: " ~ args[0] ~ " /path/to/your/package\n", opt.options); | |
return; | |
} | |
foreach(l; headerLinks) { | |
auto idx = l.indexOf("="); | |
if(idx == -1) | |
continue; | |
HeaderLink lnk; | |
lnk.text = l[0 .. idx].strip; | |
lnk.url = l[idx + 1 .. $].strip; | |
headerLinksParsed ~= lnk; | |
} | |
if(locateSymbol is null) { | |
import std.file; | |
if (!exists(skeletonFile) && findStandardFile!false("skeleton-default.html").length) | |
copyStandardFileTo!false(skeletonFile, "skeleton-default.html"); | |
if (!exists(outputDirectory)) | |
mkdir(outputDirectory); | |
if(copyStandardFiles) { | |
copyStandardFileTo(outputDirectory ~ "style.css", "style.css"); | |
copyStandardFileTo(outputDirectory ~ "script.js", "script.js"); | |
copyStandardFileTo(outputDirectory ~ "search-docs.js", "search-docs.js"); | |
} | |
/* | |
if(!exists(skeletonFile) && exists("skeleton-default.html")) | |
copy("skeleton-default.html", skeletonFile); | |
if(!exists(outputDirectory)) | |
mkdir(outputDirectory); | |
if(!exists(outputDirectory ~ "style.css") || (timeLastModified(outputDirectory ~ "style.css") < timeLastModified("style.css"))) | |
copy("style.css", outputDirectory ~ "style.css"); | |
if(!exists(outputDirectory ~ "script.js") || (timeLastModified(outputDirectory ~ "script.js") < timeLastModified("script.js"))) | |
copy("script.js", outputDirectory ~ "script.js"); | |
*/ | |
} | |
// FIXME: maybe a zeroth path just grepping for a module declaration in located files | |
// and making a mapping of module names, package listing, and files. | |
// cuz reading all of Phobos takes several seconds. Then they can parse it fully lazily. | |
static void generateAnnotatedSource(ModuleDecl mod, bool gzip) { | |
import std.file; | |
auto annotatedSourceDocument = new Document(); | |
annotatedSourceDocument.parseUtf8(readText(skeletonFile), true, true); | |
string fixupLink(string s) { | |
if(!s.startsWith("http") && !s.startsWith("/")) | |
return "../" ~ s; | |
return s; | |
} | |
foreach(ele; annotatedSourceDocument.querySelectorAll("a, link, script[src], form")) | |
if(ele.tagName == "link") | |
ele.attrs.href = "../" ~ ele.attrs.href; | |
else if(ele.tagName == "form") | |
ele.attrs.action = "../" ~ ele.attrs.action; | |
else if(ele.tagName == "a") | |
ele.attrs.href = fixupLink(ele.attrs.href); | |
else | |
ele.attrs.src = "../" ~ ele.attrs.src; | |
auto code = Element.make("pre", Html(linkUpHtml(highlight(cast(string) mod.originalSource), mod, "../", true))).addClass("d_code highlighted"); | |
addLineNumbering(code.requireSelector("pre"), true); | |
auto content = annotatedSourceDocument.requireElementById("page-content"); | |
content.addChild(code); | |
auto nav = annotatedSourceDocument.requireElementById("page-nav"); | |
void addDeclNav(Element nav, Decl decl) { | |
auto li = nav.addChild("li"); | |
if(decl.docsShouldBeOutputted) | |
li.addChild("a", "[Docs] ", fixupLink(decl.link)).addClass("docs"); | |
li.addChild("a", decl.name, "#L" ~ to!string(decl.lineNumber == 0 ? 1 : decl.lineNumber)); | |
if(decl.children.length) | |
nav = li.addChild("ul"); | |
foreach(child; decl.children) | |
addDeclNav(nav, child); | |
} | |
auto sn = nav.addChild("div").setAttribute("id", "source-navigation"); | |
addDeclNav(sn.addChild("div").addClass("list-holder").addChild("ul"), mod); | |
annotatedSourceDocument.title = mod.name ~ " source code"; | |
if(!usePseudoFiles && !exists(outputDirectory ~ "source")) | |
mkdir(outputDirectory ~ "source"); | |
if(usePseudoFiles) | |
pseudoFiles["source/" ~ mod.name ~ ".d.html"] = annotatedSourceDocument.toString(); | |
else | |
writeFile(outputDirectory ~ "source/" ~ mod.name ~ ".d.html", annotatedSourceDocument.toString(), gzip); | |
} | |
void process(string arg, bool generate) { | |
try { | |
if(locateSymbol is null) | |
writeln("First pass processing ", arg); | |
import std.file; | |
ubyte[] b; | |
if(arg == "-") { | |
foreach(chunk; stdin.byChunk(4096)) | |
b ~= chunk; | |
} else | |
b = cast(ubyte[]) read(arg); | |
config.fileName = arg; | |
auto tokens = getTokensForParser(b, config, &stringCache); | |
import std.path : baseName; | |
auto m = parseModule(tokens, baseName(arg)); | |
auto sweet = new Looker(b, baseName(arg)); | |
sweet.visit(m); | |
ModuleDecl existingDecl; | |
auto mod = cast(ModuleDecl) sweet.root; | |
{ | |
mod.originalSource = b; | |
if(mod.astNode.moduleDeclaration is null) | |
throw new Exception("you must have a module declaration for this to work on it"); | |
if(b.startsWith(cast(ubyte[])"// just docs:")) | |
sweet.root.justDocsTitle = (cast(string) b["// just docs:".length .. $].findSplitBefore(['\n'])[0].idup).strip; | |
if(sweet.root.name !in modulesByName) { | |
moduleDecls ~= mod; | |
existingDecl = mod; | |
assert(mod !is null); | |
modulesByName[sweet.root.name] = mod; | |
} else { | |
existingDecl = modulesByName[sweet.root.name]; | |
} | |
} | |
if(generate) { | |
if(sweet.root.name !in moduleDeclsGenerateByName) { | |
moduleDeclsGenerateByName[sweet.root.name] = existingDecl; | |
moduleDeclsGenerate ~= existingDecl; | |
if(annotateSource) { | |
generateAnnotatedSource(mod, gzip); | |
} | |
} | |
} | |
//packages[sweet.root.packageName] ~= sweet.root; | |
} catch (Throwable t) { | |
writeln(t.toString()); | |
} | |
} | |
args = args[1 .. $]; // remove program name | |
foreach(arg; linkReferences) { | |
import std.file; | |
loadGlobalLinkReferences(readText(arg)); | |
} | |
string[] generateFiles; | |
foreach (arg; args) generateFiles ~= scanFiles(arg); | |
/* | |
foreach(argIdx, arg; args) { | |
if(arg != "-" && std.file.isDir(arg)) | |
foreach(string name; std.file.dirEntries(arg, "*.d", std.file.SpanMode.breadth)) | |
generateFiles ~= name; | |
else | |
generateFiles ~= arg; | |
} | |
*/ | |
args = generateFiles; | |
//{ import std.stdio; foreach (fn; args) writeln(fn); } assert(0); | |
// Process them all first so name-lookups have more chance of working | |
foreach(argIdx, arg; preloadArgs) { | |
if(std.file.isDir(arg)) { | |
foreach(string name; std.file.dirEntries(arg, "*.d", std.file.SpanMode.breadth)) { | |
bool g = false; | |
if(locateSymbol is null) | |
foreach(idx, a; args) { | |
if(a == name) { | |
g = true; | |
args[idx] = args[$-1]; | |
args = args[0 .. $-1]; | |
break; | |
} | |
} | |
process(name, g); | |
} | |
} else { | |
bool g = false; | |
if(locateSymbol is null) | |
foreach(idx, a; args) { | |
if(a == arg) { | |
g = true; | |
args[idx] = args[$-1]; | |
args = args[0 .. $-1]; | |
break; | |
} | |
} | |
process(arg, g); | |
} | |
} | |
foreach(argIdx, arg; args) { | |
process(arg, locateSymbol is null ? true : false); | |
} | |
if(locateSymbol !is null) { | |
auto decl = moduleDecls[0].lookupName(locateSymbol); | |
if(decl is null) | |
writeln("not found ", locateSymbol); | |
else | |
writeln(decl.lineNumber); | |
return; | |
} | |
// create dummy packages for those not found in the source | |
// this makes linking far more sane, without requiring package.d | |
// everywhere (though I still strongly recommending you write them!) | |
// I'm using for instead of foreach so I can append in the loop | |
// and keep it going | |
for(size_t i = 0; i < moduleDecls.length; i++ ) { | |
auto decl = moduleDecls[i]; | |
auto pkg = decl.packageName; | |
if(decl.name == "index") | |
continue; // avoid infinite recursion | |
if(pkg is null) | |
pkg = "index";//continue; // to create an index.html listing all top level things | |
if(pkg !in modulesByName) { | |
writeln("Making FAKE package for ", pkg); | |
config.fileName = "dummy"; | |
auto b = cast(ubyte[]) (`/++ | |
+/ module `~pkg~`; `); | |
auto tokens = getTokensForParser(b, config, &stringCache); | |
auto m = parseModule(tokens, "dummy"); | |
auto sweet = new Looker(b, "dummy"); | |
sweet.visit(m); | |
auto mod = cast(ModuleDecl) sweet.root; | |
mod.fakeDecl = true; | |
moduleDecls ~= mod; | |
modulesByName[pkg] = mod; | |
// only generate a fake one if the real one isn't already there | |
// like perhaps the real one was generated before but just not loaded | |
// this time. | |
if(!std.file.exists(outputDirectory ~ mod.link)) | |
moduleDeclsGenerate ~= mod; | |
} | |
} | |
// add modules to their packages, if possible | |
foreach(decl; moduleDecls) { | |
auto pkg = decl.packageName; | |
if(decl.name == "index") continue; // avoid infinite recursion | |
if(pkg.length == 0) { | |
//continue; | |
pkg = "index"; | |
} | |
if(auto a = pkg in modulesByName) { | |
(*a).addChild(decl); | |
} else assert(0, pkg ~ " " ~ decl.toString); // it should have make a fake package above | |
} | |
version(with_http_server) { | |
import arsd.cgi; | |
void serveFiles(Cgi cgi) { | |
import std.file; | |
string file = cgi.requestUri; | |
auto slash = file.lastIndexOf("/"); | |
bool wasSource = file.indexOf("source/") != -1; | |
if(slash != -1) | |
file = file[slash + 1 .. $]; | |
if(wasSource) | |
file = "source/" ~ file; | |
if(file == "style.css") { | |
cgi.setResponseContentType("text/css"); | |
cgi.write(readText(findStandardFile("style.css")), true); | |
return; | |
} else if(file == "script.js") { | |
cgi.setResponseContentType("text/javascript"); | |
cgi.write(readText(findStandardFile("script.js")), true); | |
return; | |
} else if(file == "search-docs.js") { | |
cgi.setResponseContentType("text/javascript"); | |
cgi.write(readText(findStandardFile("search-docs.js")), true); | |
return; | |
} else { | |
if(file.length == 0) { | |
if("index" !in pseudoFiles) | |
writeHtml(modulesByName["index"], true, false, headerTitle, headerLinksParsed); | |
cgi.write(pseudoFiles["index"], true); | |
return; | |
} else { | |
auto of = file; | |
if(file !in pseudoFiles) { | |
ModuleDecl* declPtr; | |
file = file[0 .. $-5]; // cut off ".html" | |
if(wasSource) { | |
file = file["source/".length .. $]; | |
file = file[0 .. $-2]; // cut off ".d" | |
} | |
while((declPtr = file in modulesByName) is null) { | |
auto idx = file.lastIndexOf("."); | |
if(idx == -1) | |
break; | |
file = file[0 .. idx]; | |
} | |
if(declPtr !is null) { | |
if(wasSource) { | |
generateAnnotatedSource(*declPtr, false); | |
} else { | |
if(!(*declPtr).alreadyGenerated) | |
writeHtml(*declPtr, true, false, headerTitle, headerLinksParsed); | |
(*declPtr).alreadyGenerated = true; | |
} | |
} | |
} | |
file = of; | |
if(file in pseudoFiles) | |
cgi.write(pseudoFiles[file], true); | |
else { | |
cgi.setResponseStatus("404 Not Found"); | |
cgi.write("404 " ~ file, true); | |
} | |
return; | |
} | |
} | |
cgi.setResponseStatus("404 Not Found"); | |
cgi.write("404", true); | |
} | |
mixin CustomCgiMain!(Cgi, serveFiles); | |
processPoolSize = 1; | |
usePseudoFiles = true; | |
writeln("\n\nListening on http port 8999...."); | |
cgiMainImpl(["server", "--port", "8999"]); | |
return; | |
} | |
import std.parallelism; | |
if(jobs > 1) | |
defaultPoolThreads = jobs; | |
if(makeHtml) { | |
bool[string] alreadyTried; | |
void helper(size_t idx, ModuleDecl decl) { | |
//if(decl.parent && moduleDeclsGenerate.canFind(decl.parent)) | |
//continue; // it will be written in the list of children. actually i want to do it all here. | |
// FIXME: make search index in here if we can | |
if(!skipExisting || !std.file.exists(outputDirectory ~ decl.link(true) ~ (gzip ?".gz":""))) { | |
if(decl.name in alreadyTried) | |
return; | |
alreadyTried[decl.name] = true; | |
writeln("Generating HTML for ", decl.name); | |
writeHtml(decl, true, gzip, headerTitle, headerLinksParsed); | |
} | |
writeln(idx + 1, "/", moduleDeclsGenerate.length, " completed"); | |
} | |
if(jobs == 1) | |
foreach(idx, decl; moduleDeclsGenerate) { | |
helper(idx, decl); | |
} | |
else | |