| Index: utils/dartdoc/dartdoc.dart
|
| diff --git a/utils/dartdoc/dartdoc.dart b/utils/dartdoc/dartdoc.dart
|
| index 51027c0470fa056e3c081ebdde52eba7a8c4f823..2cadadf47cbf38a3c5ae92dc4650a69d4fd4cf65 100644
|
| --- a/utils/dartdoc/dartdoc.dart
|
| +++ b/utils/dartdoc/dartdoc.dart
|
| @@ -2,12 +2,22 @@
|
| // for details. All rights reserved. Use of this source code is governed by a
|
| // BSD-style license that can be found in the LICENSE file.
|
|
|
| -/** An awesome documentation generator. */
|
| +/**
|
| + * To use it, from this directory, run:
|
| + *
|
| + * $ 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.
|
| + */
|
| #library('dartdoc');
|
|
|
| #import('../../frog/lang.dart');
|
| #import('../../frog/file_system.dart');
|
| #import('../../frog/file_system_node.dart');
|
| +#import('../markdown/lib.dart', prefix: 'md');
|
|
|
| #source('classify.dart');
|
|
|
| @@ -17,12 +27,24 @@ final corePath = 'lib';
|
| /** Path to generate html files into. */
|
| final outdir = 'docs';
|
|
|
| +/** Set to `true` to include the source code in the generated docs. */
|
| +bool includeSource = true;
|
| +
|
| /** Special comment position used to store the library-level doc comment. */
|
| final _libraryDoc = -1;
|
|
|
| /** The file currently being written to. */
|
| StringBuffer _file;
|
|
|
| +/** The library that we're currently generating docs for. */
|
| +Library _currentLibrary;
|
| +
|
| +/** The type that we're currently generating docs for. */
|
| +Type _currentType;
|
| +
|
| +/** The member that we're currently generating docs for. */
|
| +Member _currentMember;
|
| +
|
| /**
|
| * The cached lookup-table to associate doc comments with spans. The outer map
|
| * is from filenames to doc comments in that file. The inner map maps from the
|
| @@ -39,9 +61,7 @@ int _totalMembers = 0;
|
| FileSystem files;
|
|
|
| /**
|
| - * Run this from the frog/samples directory. Before running, you need
|
| - * to create a docs dir with 'mkdir docs' - since Dart currently doesn't
|
| - * support creating new directories.
|
| + * Run this from the `utils/dartdoc` directory.
|
| */
|
| void main() {
|
| // The entrypoint of the library to generate docs for.
|
| @@ -50,6 +70,13 @@ void main() {
|
| files = new NodeFileSystem();
|
| parseOptions('../../frog', [] /* args */, files);
|
|
|
| + // Patch in support for [:...:]-style code to the markdown parser.
|
| + // TODO(rnystrom): Markdown already has syntax for this. Phase this out?
|
| + md.InlineParser.syntaxes.insertRange(0, 1,
|
| + new md.CodeSyntax(@'\[\:((?:.|\n)*?)\:\]'));
|
| +
|
| + md.setImplicitLinkResolver(resolveNameReference);
|
| +
|
| final elapsed = time(() {
|
| initializeDartDoc();
|
|
|
| @@ -141,9 +168,7 @@ docIndex(List<Library> libraries) {
|
| for (final library in sorted) {
|
| writeln(
|
| '''
|
| - <li><a href="${sanitize(library.name)}.html">
|
| - Library ${library.name}</a>
|
| - </li>
|
| + <li><a href="${libraryUrl(library)}">Library ${library.name}</a></li>
|
| ''');
|
| }
|
|
|
| @@ -159,6 +184,7 @@ docIndex(List<Library> libraries) {
|
|
|
| docLibrary(Library library) {
|
| _totalLibraries++;
|
| + _currentLibrary = library;
|
|
|
| startFile();
|
| writeln(
|
| @@ -180,11 +206,15 @@ docLibrary(Library library) {
|
| // Look for a comment for the entire library.
|
| final comment = findCommentInFile(library.baseSource, _libraryDoc);
|
| if (comment != null) {
|
| - writeln('<div class="doc"><p>$comment</p></div>');
|
| + final html = md.markdownToHtml(comment);
|
| + writeln('<div class="doc">$html</div>');
|
| needsSeparator = true;
|
| }
|
|
|
| - for (final type in library.types.getValues()) {
|
| + for (final type in orderValuesByKeys(library.types)) {
|
| + // Skip private types (for now at least).
|
| + if ((type.name != null) && type.name.startsWith('_')) continue;
|
| +
|
| if (needsSeparator) writeln('<hr/>');
|
| if (docType(type)) needsSeparator = true;
|
| }
|
| @@ -199,21 +229,24 @@ docLibrary(Library library) {
|
| }
|
|
|
| /**
|
| - * Documents [Type]. Handles top-level members if given an unnamed Type.
|
| - * Returns [:true:] if it wrote anything.
|
| + * Documents [type]. Handles top-level members if given an unnamed Type.
|
| + * Returns `true` if it wrote anything.
|
| */
|
| bool docType(Type type) {
|
| _totalTypes++;
|
| + _currentType = type;
|
|
|
| bool wroteSomething = false;
|
|
|
| if (type.name != null) {
|
| + final name = typeName(type);
|
| +
|
| write(
|
| '''
|
| - <h2 id="${type.name}">
|
| - ${type.isClass ? "Class" : "Interface"} <strong>${type.name}</strong>
|
| - <a class="anchor-link" href="#${type.name}"
|
| - title="Permalink to ${type.name}">#</a>
|
| + <h2 id="${typeAnchor(type)}">
|
| + ${type.isClass ? "Class" : "Interface"} <strong>$name</strong>
|
| + <a class="anchor-link" href="${typeUrl(type)}"
|
| + title="Permalink to $name">#</a>
|
| </h2>
|
| ''');
|
|
|
| @@ -243,12 +276,12 @@ bool docType(Type type) {
|
|
|
| if (methods.length > 0) {
|
| writeln('<h3>Methods</h3>');
|
| - for (final method in methods) docMethod(type.name, method);
|
| + for (final method in methods) docMethod(type, method);
|
| }
|
|
|
| if (fields.length > 0) {
|
| writeln('<h3>Fields</h3>');
|
| - for (final field in fields) docField(type.name, field);
|
| + for (final field in fields) docField(type, field);
|
| }
|
|
|
| return wroteSomething || methods.length > 0 || fields.length > 0;
|
| @@ -257,38 +290,38 @@ bool docType(Type type) {
|
| /** Document the superclass and superinterfaces of [Type]. */
|
| docInheritance(Type type) {
|
| // Show the superclass and superinterface(s).
|
| - if ((type.parent != null) && (type.parent.isObject) ||
|
| - (type.interfaces != null && type.interfaces.length > 0)) {
|
| + final isSubclass = (type.parent != null) && !type.parent.isObject;
|
| +
|
| + if (isSubclass || (type.interfaces != null && type.interfaces.length > 0)) {
|
| writeln('<p>');
|
|
|
| - if (type.parent != null) {
|
| - write('Extends ${typeRef(type.parent)}. ');
|
| + if (isSubclass) {
|
| + write('Extends ${typeReference(type.parent)}. ');
|
| }
|
|
|
| if (type.interfaces != null) {
|
| - final interfaces = [];
|
| switch (type.interfaces.length) {
|
| case 0:
|
| // Do nothing.
|
| break;
|
|
|
| case 1:
|
| - write('Implements ${typeRef(type.interfaces[0])}.');
|
| + write('Implements ${typeReference(type.interfaces[0])}.');
|
| break;
|
|
|
| case 2:
|
| - write('''Implements ${typeRef(type.interfaces[0])} and
|
| - ${typeRef(type.interfaces[1])}.''');
|
| + write('''Implements ${typeReference(type.interfaces[0])} and
|
| + ${typeReference(type.interfaces[1])}.''');
|
| break;
|
|
|
| default:
|
| write('Implements ');
|
| for (final i = 0; i < type.interfaces.length; i++) {
|
| - write('${typeRef(type.interfaces[i])}');
|
| - if (i < type.interfaces.length - 1) {
|
| + write('${typeReference(type.interfaces[i])}');
|
| + if (i < type.interfaces.length - 2) {
|
| write(', ');
|
| - } else {
|
| - write(' and ');
|
| + } else if (i < type.interfaces.length - 1) {
|
| + write(', and ');
|
| }
|
| }
|
| write('.');
|
| @@ -304,28 +337,26 @@ docConstructors(Type type) {
|
| writeln('<h3>Constructors</h3>');
|
| for (final name in type.constructors.getKeys()) {
|
| final constructor = type.constructors[name];
|
| - docMethod(type.name, constructor, namedConstructor: name);
|
| + docMethod(type, constructor, constructorName: name);
|
| }
|
| }
|
| }
|
|
|
| /**
|
| - * Documents the [method] in a type named [typeName]. Handles all kinds of
|
| - * methods including getters, setters, and constructors.
|
| + * Documents the [method] in type [type]. Handles all kinds of methods
|
| + * including getters, setters, and constructors.
|
| */
|
| -docMethod(String typeName, MethodMember method,
|
| - [String namedConstructor = null]) {
|
| +docMethod(Type type, MethodMember method, [String constructorName = null]) {
|
| _totalMembers++;
|
| + _currentMember = method;
|
|
|
| - writeln(
|
| - '''
|
| - <div class="method"><h4 id="$typeName.${method.name}">
|
| - <span class="show-code">Code</span>
|
| - ''');
|
| + writeln('<div class="method"><h4 id="${memberAnchor(method)}">');
|
| +
|
| + if (includeSource) {
|
| + writeln('<span class="show-code">Code</span>');
|
| + }
|
|
|
| - // A null typeName means it's a top-level definition which is implicitly
|
| - // static so doesn't need to annotate it.
|
| - if (method.isStatic && (typeName != null)) {
|
| + if (method.isStatic && !type.isTop) {
|
| write('static ');
|
| }
|
|
|
| @@ -333,8 +364,8 @@ docMethod(String typeName, MethodMember method,
|
| write(method.isConst ? 'const ' : 'new ');
|
| }
|
|
|
| - if (namedConstructor == null) {
|
| - write(optionalTypeRef(method.returnType));
|
| + if (constructorName == null) {
|
| + write(annotation(type, method.returnType));
|
| }
|
|
|
| // Translate specially-named methods: getters, setters, operators.
|
| @@ -358,22 +389,19 @@ docMethod(String typeName, MethodMember method,
|
| write('<strong>$name</strong>');
|
|
|
| // Named constructors.
|
| - if (namedConstructor != null && namedConstructor != '') {
|
| + if (constructorName != null && constructorName != '') {
|
| write('.');
|
| - write(namedConstructor);
|
| + write(constructorName);
|
| }
|
|
|
| write('(');
|
| - final paramList = [];
|
| - if (method.parameters == null) print(method.name);
|
| - for (final p in method.parameters) {
|
| - paramList.add('${optionalTypeRef(p.type)}${p.name}');
|
| - }
|
| - write(Strings.join(paramList, ", "));
|
| + final parameters = map(method.parameters,
|
| + (p) => '${annotation(type, p.type)}${p.name}');
|
| + write(Strings.join(parameters, ', '));
|
| write(')');
|
|
|
| - write(''' <a class="anchor-link" href="#$typeName.${method.name}"
|
| - title="Permalink to $typeName.$name">#</a>''');
|
| + write(''' <a class="anchor-link" href="#${memberAnchor(method)}"
|
| + title="Permalink to ${type.name}.$name">#</a>''');
|
| writeln('</h4>');
|
|
|
| docCode(method.span, showCode: true);
|
| @@ -381,19 +409,18 @@ docMethod(String typeName, MethodMember method,
|
| writeln('</div>');
|
| }
|
|
|
| -/** Documents the field [field] in a type named [typeName]. */
|
| -docField(String typeName, FieldMember field) {
|
| +/** Documents the field [field] of type [type]. */
|
| +docField(Type type, FieldMember field) {
|
| _totalMembers++;
|
| + _currentMember = field;
|
|
|
| - writeln(
|
| - '''
|
| - <div class="field"><h4 id="$typeName.${field.name}">
|
| - <span class="show-code">Code</span>
|
| - ''');
|
| + writeln('<div class="field"><h4 id="${memberAnchor(field)}">');
|
|
|
| - // A null typeName means it's a top-level definition which is implicitly
|
| - // static so doesn't need to annotate it.
|
| - if (field.isStatic && (typeName != null)) {
|
| + if (includeSource) {
|
| + writeln('<span class="show-code">Code</span>');
|
| + }
|
| +
|
| + if (field.isStatic && !type.isTop) {
|
| write('static ');
|
| }
|
|
|
| @@ -403,12 +430,12 @@ docField(String typeName, FieldMember field) {
|
| write('var ');
|
| }
|
|
|
| - write(optionalTypeRef(field.type));
|
| + write(annotation(type, field.type));
|
| write(
|
| '''
|
| <strong>${field.name}</strong> <a class="anchor-link"
|
| - href="#$typeName.${field.name}"
|
| - title="Permalink to $typeName.${field.name}">#</a>
|
| + href="#${memberUrl(field)}"
|
| + title="Permalink to ${type.name}.${field.name}">#</a>
|
| </h4>
|
| ''');
|
|
|
| @@ -416,29 +443,135 @@ docField(String typeName, FieldMember field) {
|
| writeln('</div>');
|
| }
|
|
|
| -/**
|
| - * Writes a type annotation for [type]. Will hyperlink it to that type's
|
| - * documentation if possible.
|
| - */
|
| -typeRef(Type type) {
|
| - if (type.library != null) {
|
| - final library = sanitize(type.library.name);
|
| - return '<a href="${library}.html#${type.name}">${type.name}</a>';
|
| - } else {
|
| - return type.name;
|
| +/** Generates a human-friendly string representation for a type. */
|
| +typeName(Type type) {
|
| + // See if it's a generic type.
|
| + if (type.isGeneric) {
|
| + final typeParams = type.genericType.typeParameters;
|
| + final params = Strings.join(map(typeParams, (p) => p.name), ', ');
|
| + return '${type.name}<$params>';
|
| + }
|
| +
|
| + // See if it's an instantiation of a generic type.
|
| + final typeArgs = type.typeArgsInOrder;
|
| + if (typeArgs != null) {
|
| + final args = Strings.join(map(typeArgs, typeName), ', ');
|
| + return '${type.genericType.name}<$args>';
|
| }
|
| +
|
| + // Regular type.
|
| + return type.name;
|
| +}
|
| +
|
| +/** Gets the URL to the documentation for [library]. */
|
| +libraryUrl(Library library) => '${sanitize(library.name)}.html';
|
| +
|
| +/** Gets the URL for the documentation for [type]. */
|
| +typeUrl(Type type) => '${libraryUrl(type.library)}#${typeAnchor(type)}';
|
| +
|
| +/** 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;
|
| +}
|
| +
|
| +/** Gets the anchor id for the document for [member]. */
|
| +memberAnchor(Member member) {
|
| + return '${typeAnchor(member.declaringType)}-${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>';
|
| }
|
|
|
| /**
|
| * Creates a linked string for an optional type annotation. Returns an empty
|
| * string if the type is Dynamic.
|
| */
|
| -optionalTypeRef(Type type) {
|
| - if (type.name == 'Dynamic') {
|
| - return '';
|
| - } else {
|
| - return typeRef(type) + ' ';
|
| +annotation(Type enclosingType, Type type) {
|
| + if (type.name == 'Dynamic') return '';
|
| +
|
| + // 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> ';
|
| + }
|
| +
|
| + // Link to the type.
|
| + return '<a href="${typeUrl(type)}">${typeName(type)}</a> ';
|
| +}
|
| +
|
| +/**
|
| + * This will be called whenever a doc comment hits a `[name]` in square
|
| + * brackets. It will try to figure out what the name refers to and link or
|
| + * 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['class'] = 'crossref';
|
| + return anchor;
|
| + }
|
| +
|
| + // See if it's another member of the current type.
|
| + if (_currentType != null) {
|
| + var member = _currentType.members[name];
|
| + if (member != null) {
|
| + // Special case: if the member we've resolved is a property (i.e. it wraps
|
| + // a getter and/or setter then *that* member itself won't be on the docs,
|
| + // just the getter or setter will be. So pick one of those to link to.
|
| + if (member.isProperty) {
|
| + if (member.canGet) {
|
| + member = member.getter;
|
| + } else {
|
| + member = member.setter;
|
| + }
|
| + }
|
| +
|
| + return makeLink(memberUrl(member));
|
| + }
|
| }
|
| +
|
| + // See if it's another type in the current library.
|
| + if (_currentLibrary != null) {
|
| + final type = _currentLibrary.types[name];
|
| + if (type != null) {
|
| + return makeLink(typeUrl(type));
|
| + }
|
| + }
|
| +
|
| + // TODO(rnystrom): Should also consider:
|
| + // * Names imported by libraries this library imports.
|
| + // * Type parameters of the enclosing type.
|
| +
|
| + return new md.Element.text('code', name);
|
| }
|
|
|
| /**
|
| @@ -452,10 +585,10 @@ docCode(SourceSpan span, [bool showCode = false]) {
|
| writeln('<div class="doc">');
|
| final comment = findComment(span);
|
| if (comment != null) {
|
| - writeln('<p>$comment</p>');
|
| + writeln(md.markdownToHtml(comment));
|
| }
|
|
|
| - if (showCode) {
|
| + if (includeSource && showCode) {
|
| writeln('<pre class="source">');
|
| write(formatCode(span));
|
| writeln('</pre>');
|
| @@ -491,6 +624,15 @@ parseDocComments(SourceFile file) {
|
| if (text.startsWith('/**')) {
|
| // Remember that we've encountered a doc comment.
|
| lastComment = stripComment(token.text);
|
| + } else if (text.startsWith('///')) {
|
| + var line = text.substring(3, text.length);
|
| + // Allow a leading space.
|
| + if (line.startsWith(' ')) line = line.substring(1, text.length);
|
| + if (lastComment == null) {
|
| + lastComment = line;
|
| + } else {
|
| + lastComment = '$lastComment$line';
|
| + }
|
| }
|
| } else if (token.kind == TokenKind.WHITESPACE) {
|
| // Ignore whitespace tokens.
|
| @@ -551,24 +693,24 @@ unindent(String text, int indentation) {
|
|
|
| /**
|
| * Pulls the raw text out of a doc comment (i.e. removes the comment
|
| - * characters.
|
| + * characters).
|
| */
|
| -// TODO(rnystrom): Should handle [name] and [:code:] in comments. Should also
|
| -// break empty lines into multiple paragraphs. Other formatting?
|
| -// See dart/compiler/java/com/google/dart/compiler/backend/doc for ideas.
|
| -// (/DartDocumentationVisitor.java#180)
|
| stripComment(comment) {
|
| StringBuffer buf = new StringBuffer();
|
|
|
| for (final line in comment.split('\n')) {
|
| line = line.trim();
|
| if (line.startsWith('/**')) line = line.substring(3, line.length);
|
| - if (line.endsWith('*/')) line = line.substring(0, line.length-2);
|
| - line = line.trim();
|
| - while (line.startsWith('*')) line = line.substring(1, line.length);
|
| + if (line.endsWith('*/')) line = line.substring(0, line.length - 2);
|
| line = line.trim();
|
| + if (line.startsWith('* ')) {
|
| + line = line.substring(2, line.length);
|
| + } else if (line.startsWith('*')) {
|
| + line = line.substring(1, line.length);
|
| + }
|
| +
|
| buf.add(line);
|
| - buf.add(' ');
|
| + buf.add('\n');
|
| }
|
|
|
| return buf.toString();
|
|
|