Index: utils/dartdoc/dartdoc.dart |
diff --git a/utils/dartdoc/dartdoc.dart b/utils/dartdoc/dartdoc.dart |
index 72b52aadd6f6fadd3c6f9e29466a9fa74ef2127a..60f4660c9fa395cb1777423172340194892d1dd7 100644 |
--- a/utils/dartdoc/dartdoc.dart |
+++ b/utils/dartdoc/dartdoc.dart |
@@ -7,10 +7,11 @@ |
* |
* $ dartdoc <path to .dart file> |
* |
- * This will create a "docs" directory with the docs for your libraries. To do |
- * so, dartdoc parses that library and every library it imports. From each |
- * library, it parses all classes and members, finds the associated doc |
- * comments and builds crosslinked docs from them. |
+ * This will create a "docs" directory with the docs for your libraries. To |
+ * create these beautiful docs, dartdoc parses your library and every library |
+ * it imports (recursively). From each library, it parses all classes and |
+ * members, finds the associated doc comments and builds crosslinked docs from |
+ * them. |
*/ |
#library('dartdoc'); |
@@ -33,6 +34,9 @@ bool includeSource = true; |
/** Special comment position used to store the library-level doc comment. */ |
final _libraryDoc = -1; |
+/** The path to the file currently being written to, relative to [outdir]. */ |
+String _filePath; |
+ |
/** The file currently being written to. */ |
StringBuffer _file; |
@@ -67,6 +71,19 @@ void main() { |
// The entrypoint of the library to generate docs for. |
final libPath = process.argv[2]; |
+ // Parse the dartdoc options. |
+ for (int i = 3; i < process.argv.length; i++) { |
+ final arg = process.argv[i]; |
+ switch (arg) { |
+ case '--no-code': |
+ includeSource = false; |
+ break; |
+ |
+ default: |
+ print('Unknown option: $arg'); |
+ } |
+ } |
+ |
files = new NodeFileSystem(); |
parseOptions('../../frog', [] /* args */, files); |
@@ -82,7 +99,7 @@ void main() { |
initializeWorld(files); |
- world.processScript(libPath); |
+ world.processDartScript(libPath); |
world.resolveAll(); |
// Clean the output directory. |
@@ -127,7 +144,8 @@ num time(callback()) { |
return watch.elapsedInMs(); |
} |
-startFile() { |
+startFile(String path) { |
+ _filePath = path; |
_file = new StringBuffer(); |
} |
@@ -140,8 +158,12 @@ writeln(String s) { |
write('\n'); |
} |
-endFile(String outfile) { |
- world.files.writeString(outfile, _file.toString()); |
+endFile() { |
+ String outPath = '$outdir/$_filePath'; |
+ files.createDirectory(dirname(outPath), recursive: true); |
+ |
+ world.files.writeString(outPath, _file.toString()); |
+ _filePath = null; |
_file = null; |
} |
@@ -149,7 +171,7 @@ endFile(String outfile) { |
sanitize(String name) => name.replaceAll(':', '_').replaceAll('/', '_'); |
docIndex(List<Library> libraries) { |
- startFile(); |
+ startFile('index.html'); |
// TODO(rnystrom): Need to figure out what this should look like. |
writeln( |
''' |
@@ -168,7 +190,7 @@ docIndex(List<Library> libraries) { |
for (final library in sorted) { |
writeln( |
''' |
- <li><a href="${libraryUrl(library)}">Library ${library.name}</a></li> |
+ <li>${a(libraryUrl(library), "Library ${library.name}")}</li> |
'''); |
} |
@@ -179,97 +201,163 @@ docIndex(List<Library> libraries) { |
</body></html> |
'''); |
- endFile('$outdir/index.html'); |
+ endFile(); |
} |
-docLibrary(Library library) { |
- _totalLibraries++; |
- _currentLibrary = library; |
+/** Returns the number of times [search] occurs in [text]. */ |
+int countOccurrences(String text, String search) { |
+ int start = 0; |
+ int count = 0; |
+ |
+ while (true) { |
+ start = text.indexOf(search, start); |
+ if (start == -1) break; |
+ count++; |
+ // Offsetting by needle length means overlapping needles are not counted. |
+ start += search.length; |
+ } |
- startFile(); |
+ return count; |
+} |
+ |
+/** Repeats [text] [count] times, separated by [separator] if given. */ |
+String repeat(String text, int count, [String separator]) { |
+ // TODO(rnystrom): Should be in corelib. |
+ final buffer = new StringBuffer(); |
+ for (int i = 0; i < count; i++) { |
+ buffer.add(text); |
+ if ((i < count - 1) && (separator !== null)) buffer.add(separator); |
+ } |
+ |
+ return buffer.toString(); |
+} |
+ |
+/** |
+ * Converts [absolute] which is understood to be a full path from the root of |
+ * the generated docs to one relative to the current file. |
+ */ |
+String relativePath(String absolute) { |
+ // TODO(rnystrom): Walks all the way up to root each time. Shouldn't do this |
+ // if the paths overlap. |
+ return repeat('../', countOccurrences(_filePath, '/')) + absolute; |
+} |
+ |
+/** |
+ * Creates a hyperlink. Handles turning the [href] into an appropriate relative |
+ * path from the current file. |
+ */ |
+String a(String href, String contents, [String class]) { |
+ final css = class == null ? '' : ' class="$class"'; |
+ return '<a href="${relativePath(href)}"$css>$contents</a>'; |
+} |
+ |
+writeHeader(String title) { |
writeln( |
''' |
+ <!DOCTYPE html> |
<html> |
<head> |
- <title>${library.name}</title> |
- <link rel="stylesheet" type="text/css" href="styles.css" /> |
+ <meta charset="utf-8"> |
+ <title>$title</title> |
+ <link rel="stylesheet" type="text/css" |
+ href="${relativePath('styles.css')}" /> |
<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800" rel="stylesheet" type="text/css"> |
- <script src="interact.js"></script> |
+ <script src="${relativePath('interact.js')}"></script> |
</head> |
<body> |
<div class="content"> |
- <h1>Library <strong>${library.name}</strong></h1> |
'''); |
+} |
- bool needsSeparator = false; |
+writeFooter() { |
+ writeln( |
+ ''' |
+ </div> |
+ </body></html> |
+ '''); |
+} |
+ |
+docLibrary(Library library) { |
+ _totalLibraries++; |
+ _currentLibrary = library; |
+ |
+ startFile(libraryUrl(library)); |
+ writeHeader(library.name); |
+ writeln('<h1>Library <strong>${library.name}</strong></h1>'); |
// Look for a comment for the entire library. |
final comment = findCommentInFile(library.baseSource, _libraryDoc); |
if (comment != null) { |
final html = md.markdownToHtml(comment); |
writeln('<div class="doc">$html</div>'); |
- needsSeparator = true; |
} |
- for (final type in orderValuesByKeys(library.types)) { |
- // Skip private types (for now at least). |
- if ((type.name != null) && type.name.startsWith('_')) continue; |
+ // Document the top-level members. |
+ docMembers(library.topType); |
+ |
+ // TODO(rnystrom): Link to types. |
+ writeln('<h3>Types</h3>'); |
- if (needsSeparator) writeln('<hr/>'); |
- if (docType(type)) needsSeparator = true; |
+ for (final type in orderValuesByKeys(library.types)) { |
+ if (type.isTop) continue; |
+ writeln( |
+ ''' |
+ <div class="type"> |
+ <h4> |
+ ${type.isClass ? "class" : "interface"} |
+ ${a(typeUrl(type), "<strong>${type.name}</strong>")} |
+ </h4> |
+ </div> |
+ '''); |
} |
- writeln( |
- ''' |
- </div> |
- </body></html> |
- '''); |
+ writeFooter(); |
+ endFile(); |
- endFile('$outdir/${sanitize(library.name)}.html'); |
+ for (final type in library.types.getValues()) { |
+ if (!type.isTop) docType(type); |
+ } |
} |
-/** |
- * Documents [type]. Handles top-level members if given an unnamed Type. |
- * Returns `true` if it wrote anything. |
- */ |
-bool docType(Type type) { |
+docType(Type type) { |
_totalTypes++; |
_currentType = type; |
- bool wroteSomething = false; |
+ startFile(typeUrl(type)); |
- if (type.name != null) { |
- final name = typeName(type); |
- |
- write( |
- ''' |
- <h2 id="${typeAnchor(type)}"> |
- ${type.isClass ? "Class" : "Interface"} <strong>$name</strong> |
- <a class="anchor-link" href="${typeUrl(type)}" |
- title="Permalink to $name">#</a> |
- </h2> |
- '''); |
+ final typeName = '${type.isClass ? "Class" : "Interface"} ${type.name}'; |
+ writeHeader('Library ${type.library.name} / $typeName'); |
+ writeln( |
+ ''' |
+ <h1>${a(libraryUrl(type.library), |
+ "Library <strong>${type.library.name}</strong>")}</h1> |
+ <h2>${type.isClass ? "Class" : "Interface"} |
+ <strong>${type.name}</strong></h2> |
+ '''); |
- docInheritance(type); |
- docCode(type.span); |
- docConstructors(type); |
+ docInheritance(type); |
+ docCode(type.span); |
+ docConstructors(type); |
+ docMembers(type); |
- wroteSomething = true; |
- } |
+ writeFooter(); |
+ endFile(); |
+} |
+void docMembers(Type type) { |
// Collect the different kinds of members. |
final methods = []; |
final fields = []; |
for (final member in orderValuesByKeys(type.members)) { |
- if (member.isMethod && |
- (member.definition != null) && |
- !member.name.startsWith('_')) { |
- methods.add(member); |
- } else if (member.isProperty) { |
+ if (member.name.startsWith('_')) continue; |
+ |
+ if (member.isProperty) { |
if (member.canGet) methods.add(member.getter); |
if (member.canSet) methods.add(member.setter); |
- } else if (member.isField && !member.name.startsWith('_')) { |
+ } else if (member.isMethod) { |
+ methods.add(member); |
+ } else if (member.isField) { |
fields.add(member); |
} |
} |
@@ -283,8 +371,6 @@ bool docType(Type type) { |
writeln('<h3>Fields</h3>'); |
for (final field in fields) docField(type, field); |
} |
- |
- return wroteSomething || methods.length > 0 || fields.length > 0; |
} |
/** Document the superclass and superinterfaces of [Type]. */ |
@@ -434,7 +520,7 @@ docField(Type type, FieldMember field) { |
write( |
''' |
<strong>${field.name}</strong> <a class="anchor-link" |
- href="#${memberUrl(field)}" |
+ href="#${memberAnchor(field)}" |
title="Permalink to ${type.name}.${field.name}">#</a> |
</h4> |
'''); |
@@ -464,38 +550,31 @@ typeName(Type type) { |
} |
/** Gets the URL to the documentation for [library]. */ |
-libraryUrl(Library library) => '${sanitize(library.name)}.html'; |
+libraryUrl(Library library) { |
+ return '${sanitize(library.name)}.html'; |
+} |
/** Gets the URL for the documentation for [type]. */ |
-typeUrl(Type type) => '${libraryUrl(type.library)}#${typeAnchor(type)}'; |
+typeUrl(Type type) { |
+ // Always get the generic type to strip off any type parameters or arguments. |
+ // If the type isn't generic, genericType returns `this`, so it works for |
+ // non-generic types too. |
+ return '${sanitize(type.library.name)}/${type.genericType.name}.html'; |
+} |
/** Gets the URL for the documentation for [member]. */ |
-memberUrl(Member member) => '${typeUrl(member.declaringType)}-${member.name}'; |
- |
-/** Gets the anchor id for the document for [type]. */ |
-typeAnchor(Type type) { |
- var name = type.name; |
- |
- // No name for the special type that contains top-level members. |
- if (type.isTop) return ''; |
- |
- // Remove any type args or params that have been mangled into the name. |
- var dollar = name.indexOf('\$', 0); |
- if (dollar != -1) name = name.substring(0, dollar); |
- |
- return name; |
+memberUrl(Member member) { |
+ return '${typeUrl(member.declaringType)}#${member.name}'; |
} |
/** Gets the anchor id for the document for [member]. */ |
-memberAnchor(Member member) { |
- return '${typeAnchor(member.declaringType)}-${member.name}'; |
-} |
+memberAnchor(Member member) => '${member.name}'; |
/** Writes a linked cross reference to [type]. */ |
typeReference(Type type) { |
// TODO(rnystrom): Do we need to handle ParameterTypes here like |
// annotation() does? |
- return '<a href="${typeUrl(type)}" class="crossref">${typeName(type)}</a>'; |
+ return a(typeUrl(type), typeName(type), class: 'crossref'); |
} |
/** |
@@ -508,12 +587,11 @@ annotation(Type enclosingType, Type type) { |
// If we're using a type parameter within the body of a generic class then |
// just link back up to the class. |
if (type is ParameterType) { |
- final library = sanitize(enclosingType.library.name); |
- return '<a href="${typeUrl(enclosingType)}">${type.name}</a> '; |
+ return '${a(typeUrl(enclosingType), type.name)} '; |
} |
// Link to the type. |
- return '<a href="${typeUrl(type)}">${typeName(type)}</a> '; |
+ return '${a(typeUrl(type), typeName(type))} '; |
} |
/** |
@@ -522,20 +600,9 @@ annotation(Type enclosingType, Type type) { |
* style it appropriately. |
*/ |
md.Node resolveNameReference(String name) { |
- if (_currentMember != null) { |
- // See if it's a parameter of the current method. |
- for (final parameter in _currentMember.parameters) { |
- if (parameter.name == name) { |
- final element = new md.Element.text('span', name); |
- element.attributes['class'] = 'param'; |
- return element; |
- } |
- } |
- } |
- |
makeLink(String href) { |
final anchor = new md.Element.text('a', name); |
- anchor.attributes['href'] = href; |
+ anchor.attributes['href'] = relativePath(href); |
anchor.attributes['class'] = 'crossref'; |
return anchor; |
} |
@@ -554,6 +621,17 @@ md.Node resolveNameReference(String name) { |
return member; |
} |
+ // See if it's a parameter of the current method. |
+ if (_currentMember != null) { |
+ for (final parameter in _currentMember.parameters) { |
+ if (parameter.name == name) { |
+ final element = new md.Element.text('span', name); |
+ element.attributes['class'] = 'param'; |
+ return element; |
+ } |
+ } |
+ } |
+ |
// See if it's another member of the current type. |
if (_currentType != null) { |
final member = findMember(_currentType); |