Create separate HTML pages for each type.

This also writes everything to an index.html inside an appropriate
directory. This keeps the URLs nice and pretty like: core_lib/String.
The downside is that you'll need to run a little local web server
to get them to be served up right instead of just opening them as
files in your browser.

Review URL: http://codereview.chromium.org//8758010

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@2031 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
rnystrom@google.com 2011-12-02 19:41:08 +00:00
parent d8d90a89de
commit cafe727361
4 changed files with 293 additions and 112 deletions

View file

@ -4,10 +4,15 @@ 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.
This will create a "docs" directory with the docs for your libraries.
How docs are generated
----------------------
To make beautiful docs from your library, dartdoc parses it 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.
"Doc comments" can be in one of a few forms:

View file

@ -1,12 +1,14 @@
#!/bin/bash
# To generate docs for a library, run this script with the path to an entrypoint
# .dart file as the only argument, like:
# .dart file, like:
#
# $ dartdoc foo.dart
# Get the .dart lib file the user wants to generate docs for.
entrypoint=$PWD/$1
# Run from dartdoc directory to get correct relative paths.
startdir=$PWD
pushd `dirname "$0"` >>/dev/null
# Generate the client-side .js file from interact.dart if we haven't already or
@ -18,7 +20,10 @@ if [ "interact.dart" -nt "static/interact.js" ]
echo "Compiled interact.dart."
fi
# Ditch the first arg so we can pass any extra arguments to dartdoc.
shift
# Generate the user's docs.
../../frog/frogsh --libdir=../../frog/lib dartdoc.dart "$startdir/$1"
../../frog/frogsh --libdir=../../frog/lib dartdoc.dart "$entrypoint" $@
popd >>/dev/null

View file

@ -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();
}
/** 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;
}
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>
<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="${relativePath('interact.js')}"></script>
</head>
<body>
<div class="content">
''');
}
writeFooter() {
writeln(
'''
</div>
</body></html>
''');
}
docLibrary(Library library) {
_totalLibraries++;
_currentLibrary = library;
startFile();
writeln(
'''
<html>
<head>
<title>${library.name}</title>
<link rel="stylesheet" type="text/css" href="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>
</head>
<body>
<div class="content">
<h1>Library <strong>${library.name}</strong></h1>
''');
bool needsSeparator = false;
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;
}
// Document the top-level members.
docMembers(library.topType);
// TODO(rnystrom): Link to types.
writeln('<h3>Types</h3>');
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;
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);
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>
''');
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>
''');
docInheritance(type);
docCode(type.span);
docConstructors(type);
docMembers(type);
docInheritance(type);
docCode(type.span);
docConstructors(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);

View file

@ -0,0 +1,93 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// 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.
/// Unit tests for dartdoc.
#library('dartdoc_tests');
#import('../dartdoc.dart');
// TODO(rnystrom): Better path to unittest.
#import('../../../client/testing/unittest/unittest_node.dart');
main() {
group('countOccurrences', () {
test('empty text returns 0', () {
expect(countOccurrences('', 'needle')).equals(0);
});
test('one occurrence', () {
expect(countOccurrences('bananarama', 'nara')).equals(1);
});
test('multiple occurrences', () {
expect(countOccurrences('bananarama', 'a')).equals(5);
});
test('overlapping matches do not count', () {
expect(countOccurrences('bananarama', 'ana')).equals(1);
});
});
group('repeat', () {
test('zero times returns an empty string', () {
expect(repeat('ba', 0)).equals('');
});
test('one time returns the string', () {
expect(repeat('ba', 1)).equals('ba');
});
test('multiple times', () {
expect(repeat('ba', 3)).equals('bababa');
});
test('multiple times with a separator', () {
expect(repeat('ba', 3, separator: ' ')).equals('ba ba ba');
});
});
group('relativePath', () {
test('from root to root', () {
startFile('root.html');
expect(relativePath('other.html')).equals('other.html');
});
test('from root to directory', () {
startFile('root.html');
expect(relativePath('dir/file.html')).equals('dir/file.html');
});
test('from root to nested', () {
startFile('root.html');
expect(relativePath('dir/sub/file.html')).equals('dir/sub/file.html');
});
test('from directory to root', () {
startFile('dir/file.html');
expect(relativePath('root.html')).equals('../root.html');
});
test('from nested to root', () {
startFile('dir/sub/file.html');
expect(relativePath('root.html')).equals('../../root.html');
});
test('from dir to dir with different path', () {
startFile('dir/file.html');
expect(relativePath('other/file.html')).equals('../other/file.html');
});
test('from nested to nested with different path', () {
startFile('dir/sub/file.html');
expect(relativePath('other/sub/file.html')).equals(
'../../other/sub/file.html');
});
test('from nested to directory with different path', () {
startFile('dir/sub/file.html');
expect(relativePath('other/file.html')).equals(
'../../other/file.html');
});
});
}