blob: 1f94af616f8a5b2963bb768fe21453b96ebf0b50 [file] [log] [blame]
// Copyright (c) 2013, 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.
/**
* To generate docs for a library, run this script with the path to an
* entrypoint .dart file, like:
*
* $ dart dartdoc.dart foo.dart
*
* 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;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:path/path.dart' as path;
import 'classify.dart';
import 'markdown.dart' as md;
import 'universe_serializer.dart';
import 'src/dartdoc/nav.dart';
import 'src/dartdoc/utils.dart';
import 'src/export_map.dart';
import 'src/json_serializer.dart' as json_serializer;
// TODO(rnystrom): Use "package:" URL (#4968).
import 'src/dart2js_mirrors.dart' as dart2js;
import '../../compiler/implementation/mirrors/mirrors.dart';
import '../../compiler/implementation/mirrors/mirrors_util.dart';
import '../../libraries.dart';
/**
* Generates completely static HTML containing everything you need to browse
* the docs. The only client side behavior is trivial stuff like syntax
* highlighting code.
*/
const MODE_STATIC = 0;
/**
* Generated docs do not include baked HTML navigation. Instead, a single
* `nav.json` file is created and the appropriate navigation is generated
* client-side by parsing that and building HTML.
*
* This dramatically reduces the generated size of the HTML since a large
* fraction of each static page is just redundant navigation links.
*
* In this mode, the browser will do a XHR for nav.json which means that to
* preview docs locally, you will need to enable requesting file:// links in
* your browser or run a little local server like `python -m SimpleHTTPServer`.
*/
const MODE_LIVE_NAV = 1;
const API_LOCATION = 'http://api.dartlang.org/';
/**
* Gets the full path to the directory containing the entrypoint of the current
* script. In other words, if you invoked dartdoc, directly, it will be the
* path to the directory containing `dartdoc.dart`. If you're running a script
* that imports dartdoc, it will be the path to that script.
*/
// TODO(johnniwinther): Convert to final (lazily initialized) variables when
// the feature is supported.
String get scriptDir => path.dirname(Platform.script);
/**
* Deletes and recreates the output directory at [path] if it exists.
*/
void cleanOutputDirectory(String path) {
final outputDir = new Directory(path);
if (outputDir.existsSync()) {
outputDir.deleteSync(recursive: true);
}
try {
// TODO(3914): Hack to avoid 'file already exists' exception thrown
// due to invalid result from dir.existsSync() (probably due to race
// conditions).
outputDir.createSync();
} on DirectoryException catch (e) {
// Ignore.
}
}
/**
* Copies all of the files in the directory [from] to [to]. Does *not*
* recursively copy subdirectories.
*
* Note: runs asynchronously, so you won't see any files copied until after the
* event loop has had a chance to pump (i.e. after `main()` has returned).
*/
Future copyDirectory(String from, String to) {
print('Copying static files...');
final completer = new Completer();
final fromDir = new Directory(from);
var futureList = [];
fromDir.list(recursive: false).listen(
(FileSystemEntity entity) {
if (entity is File) {
final name = path.basename(entity.path);
// TODO(rnystrom): Hackish. Ignore 'hidden' files like .DS_Store.
if (name.startsWith('.')) return;
File fromFile = entity;
File toFile = new File(path.join(to, name));
futureList.add(fromFile.openRead().pipe(toFile.openWrite()));
}
},
onDone: () => Future.wait(futureList).then((_) => completer.complete()),
onError: (e) => completer.completeError(e));
return completer.future;
}
/**
* Compiles the dartdoc client-side code to JavaScript using Dart2js.
*/
Future compileScript(int mode, String outputDir, String libPath, String tmpPath) {
print('Compiling client JavaScript...');
var clientScript = (mode == MODE_STATIC) ? 'static' : 'live-nav';
var dartdocLibPath = path.join(libPath, 'lib', '_internal', 'dartdoc', 'lib');
var dartPath = mode == MODE_STATIC ?
path.join(tmpPath, 'client.dart') :
path.join(dartdocLibPath, 'src', 'client', 'client-live-nav.dart');
var jsPath = path.join(outputDir, 'client-$clientScript.js');
// dart2js takes a String, but it expects that to be a Uri, not a file
// system path.
libPath = path.toUri(libPath).toString();
dartPath = path.toUri(dartPath).toString();
return dart2js.compile(
dartPath, libPath,
options: const <String>['--categories=Client,Server', '--minify'])
.then((jsCode) {
if (jsCode == null) throw new StateError("No javascript was generated.");
writeString(new File(jsPath), jsCode);
});
}
/**
* Package manifest containing all information required to render the main page
* for a package.
*
* The manifest specifies where to load the [LibraryElement]s describing each
* library rather than including them directly.
* For our purposes we treat the core Dart libraries as a package.
*/
class PackageManifest {
/** Package name. */
final name;
/** Package description */
final description;
/** Libraries contained in this package. */
final List<Reference> libraries = <Reference>[];
/**
* Descriptive string describing the version# of the package.
*
* The current format for dart-sdk versions is
* $MAJOR.$MINOR.$BUILD.$PATCH$revisionString$userNameString
* For example: 0.1.2.0_r18233_johndoe
*/
final String fullVersion;
/**
* Source control revision number for the package. For SVN this is a number
* while for GIT it is a hash.
*/
final String revision;
/**
* Path to the directory containing data files for each library.
*
* Currently this is the serialized json version of the LibraryElement for
* the library.
*/
String location;
/**
* Packages depended on by this package.
* We currently store the entire manifest for the depency here the manifests
* are small. We may want to change this to a reference in the future.
*/
final List<PackageManifest> dependencies = <PackageManifest>[];
PackageManifest(this.name, this.description, this.fullVersion, this.revision);
}
class Dartdoc {
/** Set to `false` to not include the source code in the generated docs. */
bool includeSource = true;
/**
* Dartdoc can generate docs in a few different ways based on how dynamic you
* want the client-side behavior to be. The value for this should be one of
* the `MODE_` constants.
*/
int mode = MODE_LIVE_NAV;
/**
* Generates the App Cache manifest file, enabling offline doc viewing.
*/
bool generateAppCache = false;
/** Path to the dartdoc directory. */
String dartdocPath;
/** Path to generate HTML files into. */
String outputDir = 'docs';
/**
* The title used for the overall generated output. Set this to change it.
*/
String mainTitle = 'Dart Documentation';
/**
* The URL that the Dart logo links to. Defaults "index.html", the main
* page for the generated docs, but can be anything.
*/
String mainUrl = 'index.html';
/**
* The Google Custom Search ID that should be used for the search box. If
* this is `null` then no search box will be shown.
*/
String searchEngineId = null;
/* The URL that the embedded search results should be displayed on. */
String searchResultsUrl = 'results.html';
/** Set this to add footer text to each generated page. */
String footerText = null;
/** Set this to omit generation timestamp from output */
bool omitGenerationTime = false;
/** Set by Dartdoc user to print extra information during generation. */
bool verbose = false;
/** Set this to include API libraries in the documentation. */
bool includeApi = false;
/** Set this to generate links to the online API. */
bool linkToApi = false;
/** Set this to generate docs for private types and members. */
bool showPrivate = false;
/** Set this to inherit from Object. */
bool inheritFromObject = false;
/** Version of the sdk or package docs are being generated for. */
String version;
/** Set this to select the libraries to include in the documentation. */
List<String> includedLibraries = const <String>[];
/** Set this to select the libraries to exclude from the documentation. */
List<String> excludedLibraries = const <String>[];
/** The package root for `package:` imports. */
String _packageRoot;
/** The map containing all the exports for each library. */
ExportMap _exports;
/** The path to a temporary directory used by Dartdoc. */
String tmpPath;
/**
* This list contains the libraries sorted in by the library name.
*/
List<LibraryMirror> _sortedLibraries;
/** A map from absolute paths of libraries to the libraries at those paths. */
Map<String, LibraryMirror> _librariesByPath;
/**
* A map from absolute paths of hidden libraries to lists of [Export]s that
* export those libraries from visible libraries. This is used to determine
* what public library any given entity belongs to.
*
* The lists of exports are sorted so that exports that hide the fewest number
* of members come first.
*/
Map<String, List<Export>> _hiddenLibraryExports;
/** The library that we're currently generating docs for. */
LibraryMirror _currentLibrary;
/** The type that we're currently generating docs for. */
ClassMirror _currentType;
/** The member that we're currently generating docs for. */
MemberMirror _currentMember;
/** The path to the file currently being written to, relative to [outdir]. */
String _filePath;
/** The file currently being written to. */
StringBuffer _file;
int _totalLibraries = 0;
int _totalTypes = 0;
int _totalMembers = 0;
int get totalLibraries => _totalLibraries;
int get totalTypes => _totalTypes;
int get totalMembers => _totalMembers;
// Check if the compilation has started and finished.
bool _started = false;
bool _finished = false;
/**
* Prints the status of dartdoc.
*
* Prints whether dartdoc is running, whether dartdoc has finished
* succesfully or not, and how many libraries, types, and members were
* documented.
*/
String get status {
// TODO(amouravski): Make this more full featured and remove all other
// prints and put them under verbose flag.
if (!_started) {
return 'Documentation has not yet started.';
} else if (!_finished) {
return 'Documentation in progress -- documented $_statisticsSummary so far.';
} else {
if (totals == 0) {
return 'Documentation complete -- warning: nothing was documented!';
} else {
return 'Documentation complete -- documented $_statisticsSummary.';
}
}
}
int get totals => totalLibraries + totalTypes + totalMembers;
String get _statisticsSummary =>
'${totalLibraries} libraries, ${totalTypes} types, and '
'${totalMembers} members';
static const List<String> COMPILER_OPTIONS =
const <String>['--preserve-comments', '--categories=Client,Server'];
/// Resolves Dart links to the correct Node.
md.Resolver dartdocResolver;
// Add support for [:...:]-style code to the markdown parser.
List<md.InlineSyntax> dartdocSyntaxes =
[new md.CodeSyntax(r'\[:\s?((?:.|\n)*?)\s?:\]')];
Dartdoc() {
tmpPath = new Directory('').createTempSync().path;
dartdocResolver = (String name) => resolveNameReference(name,
currentLibrary: _currentLibrary, currentType: _currentType,
currentMember: _currentMember);
}
/**
* Returns `true` if [library] is included in the generated documentation.
*/
bool shouldIncludeLibrary(LibraryMirror library) {
if (shouldLinkToPublicApi(library)) {
return false;
}
var includeByDefault = true;
String libraryName = displayName(library);
if (excludedLibraries.contains(libraryName)) {
return false;
}
if (!includedLibraries.isEmpty) {
includeByDefault = false;
if (includedLibraries.contains(libraryName)) {
return true;
}
}
Uri uri = library.uri;
if (uri.scheme == 'dart') {
String suffix = uri.path;
LibraryInfo info = LIBRARIES[suffix];
if (info != null) {
return info.documented && includeApi;
}
}
return includeByDefault;
}
/**
* Returns `true` if links to the public API should be generated for
* [library].
*/
bool shouldLinkToPublicApi(LibraryMirror library) {
if (linkToApi) {
Uri uri = library.uri;
if (uri.scheme == 'dart') {
String suffix = uri.path;
LibraryInfo info = LIBRARIES[suffix];
if (info != null) {
return info.documented;
}
}
}
return false;
}
String get footerContent{
var footerItems = [];
if (!omitGenerationTime) {
footerItems.add("This page was generated at ${new DateTime.now()}");
}
if (footerText != null) {
footerItems.add(footerText);
}
var content = '';
for (int i = 0; i < footerItems.length; i++) {
if (i > 0) {
content += '\n';
}
content += '<div>${footerItems[i]}</div>';
}
return content;
}
Future documentLibraries(List<Uri> libraryList, String libPath,
String packageRoot) {
_packageRoot = packageRoot;
_exports = new ExportMap.parse(libraryList, packageRoot);
var librariesToAnalyze = _exports.allExportedFiles.toList();
librariesToAnalyze.addAll(libraryList.map((uri) => uri.toString()));
// dart2js takes a String, but it expects that to be a Uri, not a file
// system path.
libPath = path.toUri(libPath).toString();
if (packageRoot != null) {
packageRoot = path.toUri(packageRoot).toString();
}
// TODO(amouravski): make all of these print statements into logging
// statements.
print('Analyzing libraries...');
return dart2js.analyze(
librariesToAnalyze.toList(), libPath,
packageRoot: packageRoot, options: COMPILER_OPTIONS)
.then((MirrorSystem mirrors) {
print('Generating documentation...');
_document(mirrors);
});
}
void _document(MirrorSystem mirrors) {
_started = true;
// Remove duplicated libraries. This is a hack because libraries can
// get picked up multiple times (dartbug.com/11826) which will go away
// with the new docgen. The reason we hit this issue is that we attempt
// to dart2js.analyze all packages in the repo together, which results
// in packages getting referenced with different URI's (../../pkg versus
// ../../out/ReleaseIA32/packages versus package:).
_sortedLibraries = new Map.fromIterable(
mirrors.libraries.values.where(shouldIncludeLibrary),
key: displayName).values.toList();
// Sort the libraries by name (not key).
_sortedLibraries.sort((x, y) {
return displayName(x).toUpperCase().compareTo(
displayName(y).toUpperCase());
});
_librariesByPath = <String, LibraryMirror>{};
for (var library in mirrors.libraries.values) {
var libraryPath = _libraryPath(library);
if (libraryPath == null) continue;
libraryPath = path.normalize(path.absolute(libraryPath));
_librariesByPath[libraryPath] = library;
}
_hiddenLibraryExports = _generateHiddenLibraryExports();
// Generate the docs.
if (mode == MODE_LIVE_NAV) {
docNavigationJson();
} else {
docNavigationDart();
}
docIndex();
for (final library in _sortedLibraries) {
docLibrary(library);
}
if (generateAppCache) {
generateAppCacheManifest();
}
// TODO(nweiz): properly handle exports when generating JSON.
// TODO(jacobr): handle arbitrary pub packages rather than just the system
// libraries.
var revision = '0';
if (version != null) {
var match = new RegExp(r"_r(\d+)").firstMatch(version);
if (match != null) {
revision = match.group(1);
} else {
print("Warning: could not parse version: $version");
}
}
var packageManifest = new PackageManifest('dart:', 'Dart System Libraries',
version, revision);
for (final lib in _sortedLibraries) {
var libraryElement = new LibraryElement(
lib, lookupMdnComment: lookupMdnComment)
..stripDuplicateUris(null, null);
packageManifest.libraries.add(new Reference.fromElement(libraryElement));
startFile("$revision/${libraryElement.id}.json");
write(json_serializer.serialize(libraryElement));
endFile();
}
startFile("$revision/apidoc.json");
write(json_serializer.serialize(packageManifest));
endFile();
// Write out top level json file with a relative path to the library json
// files.
startFile("apidoc.json");
packageManifest.location = revision;
write(json_serializer.serialize(packageManifest));
endFile();
_finished = true;
}
/**
* Generate [_hiddenLibraryExports] from [_exports] and [_librariesByPath].
*/
Map<String, List<Export>> _generateHiddenLibraryExports() {
// First generate a map `exported path => exporter path => Export`. The
// inner map makes it easier to merge multiple exports of the same library
// by the same exporter.
var hiddenLibraryExportMaps = <String, Map<String, Export>>{};
_exports.exports.forEach((exporter, exports) {
var library = _librariesByPath[exporter];
// TODO(nweiz): remove this check when issue 9645 is fixed.
if (library == null) return;
if (!shouldIncludeLibrary(library)) return;
for (var export in exports) {
var library = _librariesByPath[export.path];
// TODO(nweiz): remove this check when issue 9645 is fixed.
if (library == null) continue;
if (shouldIncludeLibrary(library)) continue;
var hiddenExports = _exports.transitiveExports(export.path)
.map((transitiveExport) => export.compose(transitiveExport))
.toList();
hiddenExports.add(export);
for (var hiddenExport in hiddenExports) {
var exportsByExporterPath = hiddenLibraryExportMaps
.putIfAbsent(hiddenExport.path, () => <String, Export>{});
addOrMergeExport(exportsByExporterPath, exporter, hiddenExport);
}
}
});
// Now sort the values of the inner maps of `hiddenLibraryExportMaps` to get
// the final value of `_hiddenLibraryExports`.
var hiddenLibraryExports = <String, List<Export>>{};
hiddenLibraryExportMaps.forEach((exporteePath, exportsByExporterPath) {
int rank(Export export) {
if (export.show.isEmpty && export.hide.isEmpty) return 0;
if (export.show.isEmpty) return export.hide.length;
// Multiply by 1000 to ensure this sorts after an export with hides.
return 1000 * export.show.length;
}
var exports = exportsByExporterPath.values.toList();
exports.sort((export1, export2) {
var comparison = Comparable.compare(rank(export1), rank(export2));
if (comparison != 0) return comparison;
var library1 = _librariesByPath[export1.exporter];
var library2 = _librariesByPath[export2.exporter];
return Comparable.compare(displayName(library1), displayName(library2));
});
hiddenLibraryExports[exporteePath] = exports;
});
return hiddenLibraryExports;
}
MdnComment lookupMdnComment(Mirror mirror) => null;
void startFile(String path) {
_filePath = path;
_file = new StringBuffer();
}
void endFile() {
final outPath = path.join(outputDir, _filePath);
final dir = new Directory(path.dirname(outPath));
if (!dir.existsSync()) {
// TODO(3914): Hack to avoid 'file already exists' exception
// thrown due to invalid result from dir.existsSync() (probably due to
// race conditions).
try {
dir.createSync();
} on DirectoryException catch (e) {
// Ignore.
}
}
writeString(new File(outPath), _file.toString());
_filePath = null;
_file = null;
}
void write(String s) {
_file.write(s);
}
void writeln(String s) {
write(s);
write('\n');
}
/**
* Writes the page header with the given [title] and [breadcrumbs]. The
* breadcrumbs are an interleaved list of links and titles. If a link is null,
* then no link will be generated. For example, given:
*
* ['foo', 'foo.html', 'bar', null]
*
* It will output:
*
* <a href="foo.html">foo</a> &rsaquo; bar
*/
void writeHeader(String title, List<String> breadcrumbs) {
final htmlAttributes = generateAppCache ?
'manifest="/appcache.manifest"' : '';
write(
'''
<!DOCTYPE html>
<html${htmlAttributes == '' ? '' : ' $htmlAttributes'}>
<head>
''');
writeHeadContents(title);
// Add data attributes describing what the page documents.
var data = '';
if (_currentLibrary != null) {
data = '$data data-library='
'"${md.escapeHtml(displayName(_currentLibrary))}"';
}
if (_currentType != null) {
data = '$data data-type="${md.escapeHtml(typeName(_currentType))}"';
}
write(
'''
</head>
<body$data>
<div class="page">
<div class="header">
${a(mainUrl, '<div class="logo"></div>')}
${a('index.html', mainTitle)}
''');
// Write the breadcrumb trail.
for (int i = 0; i < breadcrumbs.length; i += 2) {
if (breadcrumbs[i + 1] == null) {
write(' &rsaquo; ${breadcrumbs[i]}');
} else {
write(' &rsaquo; ${a(breadcrumbs[i + 1], breadcrumbs[i])}');
}
}
if (searchEngineId != null) {
writeln(
'''
<form action="$searchResultsUrl" id="search-box">
<input type="hidden" name="cx" value="$searchEngineId">
<input type="hidden" name="ie" value="UTF-8">
<input type="hidden" name="hl" value="en">
<input type="search" name="q" id="q" autocomplete="off"
class="search-input" placeholder="Search API">
</form>
''');
} else {
writeln(
'''
<div id="search-box">
<input type="search" name="q" id="q" autocomplete="off"
class="search-input" placeholder="Search API">
</div>
''');
}
writeln(
'''
</div>
<div class="drop-down" id="drop-down"></div>
''');
docNavigation();
writeln('<div class="content">');
}
String get clientScript {
switch (mode) {
case MODE_STATIC: return 'client-static';
case MODE_LIVE_NAV: return 'client-live-nav';
default: throw 'Unknown mode $mode.';
}
}
void writeHeadContents(String title) {
writeln(
'''
<meta charset="utf-8">
<title>$title / $mainTitle</title>
<link rel="stylesheet" type="text/css"
href="${relativePath('styles.css')}">
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,600,700,800" rel="stylesheet" type="text/css">
<link rel="shortcut icon" href="${relativePath('favicon.ico')}">
''');
}
void writeFooter() {
writeln(
'''
</div>
<div class="clear"></div>
</div>
<div class="footer">
$footerContent
</div>
<script async src="${relativePath('$clientScript.js')}"></script>
</body></html>
''');
}
void docIndex() {
startFile('index.html');
writeHeader(mainTitle, []);
writeln('<h2>$mainTitle</h2>');
writeln('<h3>Libraries</h3>');
for (final library in _sortedLibraries) {
docIndexLibrary(library);
}
writeFooter();
endFile();
}
void docIndexLibrary(LibraryMirror library) {
writeln('<h4>${a(libraryUrl(library), displayName(library))}</h4>');
}
/**
* Walks the libraries and creates a JSON object containing the data needed
* to generate navigation for them.
*/
void docNavigationJson() {
startFile('nav.json');
writeln(JSON.encode(createNavigationInfo()));
endFile();
}
/// Whether dartdoc is running from within the Dart SDK or the
/// Dart source repository.
bool get runningFromSdk =>
path.extension(Platform.script) == '.snapshot';
/// Gets the path to the root directory of the SDK.
String get sdkDir =>
path.dirname(path.dirname(Platform.executable));
/// Gets the path to the dartdoc directory normalized for running in different
/// places.
String get normalizedDartdocPath => path.normalize(
path.absolute(runningFromSdk ?
path.join(sdkDir, 'lib', '_internal', 'dartdoc') :
dartdocPath.toString()));
void docNavigationDart() {
var tmpDir = new Directory(tmpPath);
if (!tmpDir.existsSync()) {
tmpDir.createSync();
}
String jsonString = JSON.encode(createNavigationInfo());
String dartString = jsonString.replaceAll(r"$", r"\$");
var filePath = path.join(tmpPath, 'client.dart');
var clientDir = path.join(normalizedDartdocPath,'lib', 'src', 'client');
writeString(new File(filePath),
'''library client;
import 'dart:html';
import 'dart:json';
import r'${path.toUri(path.join(clientDir, 'client-shared.dart'))}';
import r'${path.toUri(path.join(clientDir, 'dropdown.dart'))}';
main() {
setup();
setupSearch(json);
}
get json => $dartString;''');
}
void cleanup() {
var tmpDir = new Directory(tmpPath);
if (tmpDir.existsSync()) {
tmpDir.deleteSync(recursive: true);
}
tmpPath = null;
}
List createNavigationInfo() {
final libraryList = [];
for (final library in _sortedLibraries) {
docLibraryNavigationJson(library, libraryList);
}
return libraryList;
}
void docLibraryNavigationJson(LibraryMirror library, List libraryList) {
var libraryInfo = {};
libraryInfo[NAME] = displayName(library);
final List members = docMembersJson(library.members);
if (!members.isEmpty) {
libraryInfo[MEMBERS] = members;
}
final types = [];
for (ClassMirror type in orderByName(library.classes.values)) {
if (!showPrivate && type.isPrivate) continue;
var typeInfo = {};
typeInfo[NAME] = displayName(type);
if (type.isClass) {
typeInfo[KIND] = CLASS;
} else {
assert(type.isTypedef);
typeInfo[KIND] = TYPEDEF;
}
final List typeMembers = docMembersJson(type.members);
if (!typeMembers.isEmpty) {
typeInfo[MEMBERS] = typeMembers;
}
if (!type.originalDeclaration.typeVariables.isEmpty) {
final typeVariables = [];
for (final typeVariable in type.originalDeclaration.typeVariables) {
typeVariables.add(displayName(typeVariable));
}
typeInfo[ARGS] = typeVariables.join(', ');
}
types.add(typeInfo);
}
if (!types.isEmpty) {
libraryInfo[TYPES] = types;
}
libraryList.add(libraryInfo);
}
List docMembersJson(Map<Object,MemberMirror> memberMap) {
final members = [];
for (MemberMirror member in orderByName(memberMap.values)) {
if (!showPrivate && member.isPrivate) continue;
var memberInfo = {};
if (member.isVariable) {
memberInfo[KIND] = FIELD;
} else {
MethodMirror method = member;
if (method.isConstructor) {
memberInfo[KIND] = CONSTRUCTOR;
} else if (method.isSetter) {
memberInfo[KIND] = SETTER;
} else if (method.isGetter) {
memberInfo[KIND] = GETTER;
} else {
memberInfo[KIND] = METHOD;
}
if (method.parameters.isEmpty) {
memberInfo[NO_PARAMS] = true;
}
}
memberInfo[NAME] = displayName(member);
var anchor = memberAnchor(member);
if (anchor != memberInfo[NAME]) {
memberInfo[LINK_NAME] = anchor;
}
members.add(memberInfo);
}
return members;
}
void docNavigation() {
writeln(
'''
<div class="nav">
''');
if (mode == MODE_STATIC) {
for (final library in _sortedLibraries) {
write('<h2><div class="icon-library"></div>');
if ((_currentLibrary == library) && (_currentType == null)) {
write('<strong>${displayName(library)}</strong>');
} else {
write('${a(libraryUrl(library), displayName(library))}');
}
write('</h2>');
// Only expand classes in navigation for current library.
if (_currentLibrary == library) docLibraryNavigation(library);
}
}
writeln('</div>');
}
/** Writes the navigation for the types contained by the given library. */
void docLibraryNavigation(LibraryMirror library) {
// Show the exception types separately.
final types = <ClassMirror>[];
final exceptions = <ClassMirror>[];
for (ClassMirror type in orderByName(library.classes.values)) {
if (!showPrivate && type.isPrivate) continue;
if (isException(type)) {
exceptions.add(type);
} else {
types.add(type);
}
}
if ((types.length == 0) && (exceptions.length == 0)) return;
writeln('<ul class="icon">');
types.forEach(docTypeNavigation);
exceptions.forEach(docTypeNavigation);
writeln('</ul>');
}
/** Writes a linked navigation list item for the given type. */
void docTypeNavigation(ClassMirror type) {
var icon = 'interface';
if (isException(type)) {
icon = 'exception';
} else if (type.isClass) {
icon = 'class';
}
write('<li>');
if (_currentType == type) {
write(
'<div class="icon-$icon"></div><strong>${typeName(type)}</strong>');
} else {
write(a(typeUrl(type),
'<div class="icon-$icon"></div>${typeName(type)}'));
}
writeln('</li>');
}
void docLibrary(LibraryMirror library) {
if (verbose) {
print('Library \'${displayName(library)}\':');
}
_totalLibraries++;
_currentLibrary = library;
startFile(libraryUrl(library));
writeHeader('${displayName(library)} Library',
[displayName(library), libraryUrl(library)]);
writeln('<h2><strong>${displayName(library)}</strong> library</h2>');
// Look for a comment for the entire library.
final comment = getLibraryComment(library);
if (comment != null) {
writeln('<div class="doc">${comment.html}</div>');
}
// Document the visible libraries exported by this library.
docExports(library);
// Document the top-level members.
docMembers(library);
// Document the types.
final abstractClasses = <ClassMirror>[];
final classes = <ClassMirror>[];
final typedefs = <TypedefMirror>[];
final exceptions = <ClassMirror>[];
var allClasses = _libraryClasses(library);
for (ClassMirror type in orderByName(allClasses)) {
if (!showPrivate && type.isPrivate) continue;
if (isException(type)) {
exceptions.add(type);
} else if (type.isClass) {
if (type.isAbstract) {
abstractClasses.add(type);
} else {
classes.add(type);
}
} else if (type is TypedefMirror) {
typedefs.add(type);
} else {
throw new InternalError("internal error: unknown type $type.");
}
}
docTypes(abstractClasses, 'Abstract Classes');
docTypes(classes, 'Classes');
docTypes(typedefs, 'Typedefs');
docTypes(exceptions, 'Exceptions');
writeFooter();
endFile();
for (final type in allClasses) {
if (!showPrivate && type.isPrivate) continue;
docType(type);
}
_currentLibrary = null;
}
void docTypes(List types, String header) {
if (types.length == 0) return;
writeln('<div>');
writeln('<h3>$header</h3>');
for (final type in types) {
writeln(
'''
<div class="type">
<h4>
${a(typeUrl(type), "<strong>${typeName(type)}</strong>")}
</h4>
</div>
''');
}
writeln('</div>');
}
void docType(ClassMirror type) {
if (verbose) {
print('- ${type.simpleName}');
}
_totalTypes++;
_currentType = type;
startFile(typeUrl(type));
var kind;
if (type.isTypedef) {
kind = 'typedef';
} else {
assert(type.isClass);
if (type.isAbstract) {
kind = 'abstract class';
} else {
kind = 'class';
}
}
final typeTitle =
'${typeName(type)} ${kind}';
var library = _libraryFor(type);
writeHeader('$typeTitle / ${displayName(library)} Library',
[displayName(library), libraryUrl(library),
typeName(type), typeUrl(type)]);
writeln(
'''
<h2><strong>${typeName(type, showBounds: true)}</strong>
$kind
</h2>
''');
writeln('<button id="show-inherited" class="show-inherited">'
'Hide inherited</button>');
writeln('<div class="doc">');
docComment(type, getTypeComment(type));
docCode(type.location);
writeln('</div>');
docInheritance(type);
docTypedef(type);
docMembers(type);
writeTypeFooter();
writeFooter();
endFile();
_currentType = null;
}
/** Override this to write additional content at the end of a type's page. */
void writeTypeFooter() {
// Do nothing.
}
/**
* Writes an inline type span for the given type. This is a little box with
* an icon and the type's name. It's similar to how types appear in the
* navigation, but is suitable for inline (as opposed to in a `<ul>`) use.
*/
void typeSpan(ClassMirror type) {
var icon = 'interface';
if (isException(type)) {
icon = 'exception';
} else if (type.isClass) {
icon = 'class';
}
write('<span class="type-box"><span class="icon-$icon"></span>');
if (_currentType == type) {
write('<strong>${typeName(type)}</strong>');
} else {
write(a(typeUrl(type), typeName(type)));
}
write('</span>');
}
/**
* Document the other types that touch [Type] in the inheritance hierarchy:
* subclasses, superclasses, subinterfaces, superinferfaces, and default
* class.
*/
void docInheritance(ClassMirror type) {
// Don't show the inheritance details for Object. It doesn't have any base
// class (obviously) and it has too many subclasses to be useful.
if (type.isObject) return;
if (type.isTypedef) return;
// Writes an unordered list of references to types with an optional header.
listTypes(types, header) {
if (types == null) return;
// Filter out injected types. (JavaScriptIndexingBehavior)
types = new List.from(types.where((t) => t.library != null));
var publicTypes;
if (showPrivate) {
publicTypes = types;
} else {
// Skip private types.
publicTypes = new List.from(types.where((t) => !t.isPrivate));
}
if (publicTypes.length == 0) return;
writeln('<h3>$header</h3>');
writeln('<p>');
bool first = true;
for (final t in publicTypes) {
if (!first) write(', ');
typeSpan(t);
first = false;
}
writeln('</p>');
}
final subtypes = [];
for (final subtype in computeSubdeclarations(type)) {
subtypes.add(subtype);
}
subtypes.sort((x, y) => x.simpleName.compareTo(y.simpleName));
// Show the chain of superclasses.
if (!type.superclass.isObject) {
final supertypes = [];
var thisType = type.superclass;
do {
supertypes.add(thisType);
thisType = thisType.superclass;
} while (!thisType.isObject);
writeln('<h3>Extends</h3>');
writeln('<p>');
for (var i = supertypes.length - 1; i >= 0; i--) {
typeSpan(supertypes[i]);
write('&nbsp;&gt;&nbsp;');
}
// Write this class.
typeSpan(type);
writeln('</p>');
}
listTypes(subtypes, 'Subclasses');
listTypes(type.superinterfaces, 'Implements');
}
/**
* Documents the definition of [type] if it is a typedef.
*/
void docTypedef(TypeMirror type) {
if (type is! TypedefMirror) {
return;
}
writeln('<div class="method"><h4 id="${type.simpleName}">');
if (includeSource) {
writeln('<button class="show-code">Code</button>');
}
write('typedef ');
annotateType(type, type.value, type.simpleName);
write(''' <a class="anchor-link" href="#${type.simpleName}"
title="Permalink to ${type.simpleName}">#</a>''');
writeln('</h4>');
writeln('<div class="doc">');
docCode(type.location);
writeln('</div>');
writeln('</div>');
}
static const operatorOrder = const <String>[
'[]', '[]=', // Indexing.
'+', Mirror.UNARY_MINUS, '-', '*', '/', '~/', '%', // Arithmetic.
'&', '|', '^', '~', // Bitwise.
'<<', '>>', // Shift.
'<', '<=', '>', '>=', // Relational.
'==', // Equality.
];
static final Map<String, int> operatorOrderMap = (){
var map = new Map<String, int>();
var index = 0;
for (String operator in operatorOrder) {
map[operator] = index++;
}
return map;
}();
void docExports(LibraryMirror library) {
// TODO(nweiz): show `dart:` library exports.
var exportLinks = _exports.transitiveExports(_libraryPath(library))
.map((export) {
var library = _librariesByPath[export.path];
// TODO(nweiz): remove this check when issue 9645 is fixed.
if (library == null) return null;
// Only link to publically visible libraries.
if (!shouldIncludeLibrary(library)) return null;
var memberNames = export.show.isEmpty ? export.hide : export.show;
var memberLinks = memberNames.map((name) {
return md.renderToHtml([resolveNameReference(
name, currentLibrary: library)]);
}).join(', ');
var combinator = '';
if (!export.show.isEmpty) {
combinator = ' show $memberLinks';
} else if (!export.hide.isEmpty) {
combinator = ' hide $memberLinks';
}
return '<ul>${a(libraryUrl(library), displayName(library))}'
'$combinator</ul>';
}).where((link) => link != null);
if (!exportLinks.isEmpty) {
writeln('<h3>Exports</h3>');
writeln('<ul>');
writeln(exportLinks.join('\n'));
writeln('</ul>');
}
}
void docMembers(ContainerMirror host) {
// Collect the different kinds of members.
final staticMethods = [];
final staticGetters = new Map<String,MemberMirror>();
final staticSetters = new Map<String,MemberMirror>();
final memberMap = new Map<String,MemberMirror>();
final instanceMethods = [];
final instanceOperators = [];
final instanceGetters = new Map<String,MemberMirror>();
final instanceSetters = new Map<String,MemberMirror>();
final constructors = [];
var hostMembers = host is LibraryMirror ?
_libraryMembers(host) : host.members.values;
for (var member in hostMembers) {
if (!showPrivate && member.isPrivate) continue;
if (host is LibraryMirror || member.isStatic) {
if (member is MethodMirror) {
if (member.isGetter) {
staticGetters[displayName(member)] = member;
} else if (member.isSetter) {
staticSetters[displayName(member)] = member;
} else {
staticMethods.add(member);
}
} else if (member is VariableMirror) {
staticGetters[displayName(member)] = member;
}
}
}
if (host is ClassMirror) {
var iterable = new HierarchyIterable(host, includeType: true);
for (ClassMirror type in iterable) {
if (!host.isObject && !inheritFromObject && type.isObject) continue;
type.members.forEach((_, MemberMirror member) {
if (member.isStatic) return;
if (!showPrivate && member.isPrivate) return;
bool inherit = true;
if (type != host) {
if (member.isPrivate) {
// Don't inherit private members.
inherit = false;
}
if (member.isConstructor) {
// Don't inherit constructors.
inherit = false;
}
}
if (!inherit) return;
if (member.isVariable) {
// Fields override both getters and setters.
memberMap.putIfAbsent(member.simpleName, () => member);
memberMap.putIfAbsent('${member.simpleName}=', () => member);
} else if (member.isConstructor) {
constructors.add(member);
} else {
memberMap.putIfAbsent(member.simpleName, () => member);
}
});
}
}
bool allMethodsInherited = true;
bool allPropertiesInherited = true;
bool allOperatorsInherited = true;
memberMap.forEach((_, MemberMirror member) {
if (member is MethodMirror) {
if (member.isGetter) {
instanceGetters[displayName(member)] = member;
if (_ownerFor(member) == host) {
allPropertiesInherited = false;
}
} else if (member.isSetter) {
instanceSetters[displayName(member)] = member;
if (_ownerFor(member) == host) {
allPropertiesInherited = false;
}
} else if (member.isOperator) {
instanceOperators.add(member);
if (_ownerFor(member) == host) {
allOperatorsInherited = false;
}
} else {
instanceMethods.add(member);
if (_ownerFor(member) == host) {
allMethodsInherited = false;
}
}
} else if (member is VariableMirror) {
instanceGetters[displayName(member)] = member;
if (_ownerFor(member) == host) {
allPropertiesInherited = false;
}
}
});
instanceOperators.sort((MethodMirror a, MethodMirror b) {
return operatorOrderMap[a.simpleName].compareTo(
operatorOrderMap[b.simpleName]);
});
docProperties(host,
host is LibraryMirror ? 'Properties' : 'Static Properties',
staticGetters, staticSetters, allInherited: false);
docMethods(host,
host is LibraryMirror ? 'Functions' : 'Static Methods',
staticMethods, allInherited: false);
docMethods(host, 'Constructors', orderByName(constructors),
allInherited: false);
docProperties(host, 'Properties', instanceGetters, instanceSetters,
allInherited: allPropertiesInherited);
docMethods(host, 'Operators', instanceOperators,
allInherited: allOperatorsInherited);
docMethods(host, 'Methods', orderByName(instanceMethods),
allInherited: allMethodsInherited);
}
/**
* Documents fields, getters, and setters as properties.
*/
void docProperties(ContainerMirror host, String title,
Map<String,MemberMirror> getters,
Map<String,MemberMirror> setters,
{bool allInherited}) {
if (getters.isEmpty && setters.isEmpty) return;
var nameSet = new Set<String>.from(getters.keys);
nameSet.addAll(setters.keys);
var nameList = new List<String>.from(nameSet);
nameList.sort((String a, String b) {
return a.toLowerCase().compareTo(b.toLowerCase());
});
writeln('<div${allInherited ? ' class="inherited"' : ''}>');
writeln('<h3>$title</h3>');
for (String name in nameList) {
MemberMirror getter = getters[name];
MemberMirror setter = setters[name];
if (setter == null) {
if (getter is VariableMirror) {
// We have a field.
docField(host, getter);
} else {
// We only have a getter.
assert(getter is MethodMirror);
docProperty(host, getter, null);
}
} else if (getter == null) {
// We only have a setter => Document as a method.
assert(setter is MethodMirror);
docMethod(host, setter);
} else {
DocComment getterComment = getMemberComment(getter);
DocComment setterComment = getMemberComment(setter);
if (_ownerFor(getter) != _ownerFor(setter) ||
getterComment != null && setterComment != null) {
// Both have comments or are not declared in the same class
// => Documents separately.
if (getter is VariableMirror) {
// Document field as a getter (setter is inherited).
docField(host, getter, asGetter: true);
} else {
docMethod(host, getter);
}
if (setter is VariableMirror) {
// Document field as a setter (getter is inherited).
docField(host, setter, asSetter: true);
} else {
docMethod(host, setter);
}
} else {
// Document as field.
docProperty(host, getter, setter);
}
}
}
writeln('</div>');
}
void docMethods(ContainerMirror host, String title, List<Mirror> methods,
{bool allInherited}) {
if (methods.length > 0) {
writeln('<div${allInherited ? ' class="inherited"' : ''}>');
writeln('<h3>$title</h3>');
for (MethodMirror method in methods) {
docMethod(host, method);
}
writeln('</div>');
}
}
/**
* Documents the [member] declared in [host]. Handles all kinds of members
* including getters, setters, and constructors. If [member] is a
* [FieldMirror] it is documented as a getter or setter depending upon the
* value of [asGetter].
*/
void docMethod(ContainerMirror host, MemberMirror member,
{bool asGetter: false}) {
_totalMembers++;
_currentMember = member;
bool isAbstract = false;
String name = displayName(member);
if (member is VariableMirror) {
if (asGetter) {
// Getter.
name = 'get $name';
} else {
// Setter.
name = 'set $name';
}
} else {
assert(member is MethodMirror);
isAbstract = member.isAbstract;
if (member.isGetter) {
// Getter.
name = 'get $name';
} else if (member.isSetter) {
// Setter.
name = 'set $name';
}
}
bool showCode = includeSource && !isAbstract;
bool inherited = host != member.owner && member.owner is! LibraryMirror;
writeln('<div class="method${inherited ? ' inherited': ''}">'
'<h4 id="${memberAnchor(member)}">');
if (showCode) {
writeln('<button class="show-code">Code</button>');
}
if (member is MethodMirror) {
if (member.isConstructor) {
if (member.isFactoryConstructor) {
write('factory ');
} else {
write(member.isConstConstructor ? 'const ' : 'new ');
}
} else if (member.isAbstract) {
write('abstract ');
}
if (!member.isConstructor) {
annotateDynamicType(host, member.returnType);
}
} else {
assert(member is VariableMirror);
if (asGetter) {
annotateDynamicType(host, member.type);
} else {
write('void ');
}
}
write('<strong>$name</strong>');
if (member is MethodMirror) {
if (!member.isGetter) {
docParamList(host, member.parameters);
}
} else {
assert(member is VariableMirror);
if (!asGetter) {
write('(');
annotateType(host, member.type);
write(' value)');
}
}
var prefix = host is LibraryMirror ? '' : '${typeName(host)}.';
write(''' <a class="anchor-link" href="#${memberAnchor(member)}"
title="Permalink to $prefix$name">#</a>''');
writeln('</h4>');
if (inherited) {
write('<div class="inherited-from">inherited from ');
annotateType(host, member.owner);
write('</div>');
}
writeln('<div class="doc">');
docComment(host, getMemberComment(member));
if (showCode) {
docCode(member.location);
}
writeln('</div>');
writeln('</div>');
_currentMember = null;
}
void docField(ContainerMirror host, VariableMirror field,
{bool asGetter: false, bool asSetter: false}) {
if (asGetter) {
docMethod(host, field, asGetter: true);
} else if (asSetter) {
docMethod(host, field, asGetter: false);
} else {
docProperty(host, field, null);
}
}
/**
* Documents the property defined by [getter] and [setter] of declared in
* [host]. If [getter] is a [FieldMirror], [setter] must be [:null:].
* Otherwise, if [getter] is a [MethodMirror], the property is considered
* final if [setter] is [:null:].
*/
void docProperty(ContainerMirror host,
MemberMirror getter, MemberMirror setter) {
assert(getter != null);
_totalMembers++;
_currentMember = getter;
bool inherited = host != getter.owner && getter.owner is! LibraryMirror;
writeln('<div class="field${inherited ? ' inherited' : ''}">'
'<h4 id="${memberAnchor(getter)}">');
if (includeSource) {
writeln('<button class="show-code">Code</button>');
}
bool isConst = false;
bool isFinal;
TypeMirror type;
if (getter is VariableMirror) {
assert(setter == null);
isConst = getter.isConst;
isFinal = getter.isFinal;
type = getter.type;
} else {
assert(getter is MethodMirror);
isFinal = setter == null;
type = getter.returnType;
}
if (isConst) {
write('const ');
} else if (isFinal) {
write('final ');
} else if (type.isDynamic) {
write('var ');
}
annotateType(host, type);
var prefix = host is LibraryMirror ? '' : '${typeName(host)}.';
write(
'''
<strong>${getter.simpleName}</strong> <a class="anchor-link"
href="#${memberAnchor(getter)}"
title="Permalink to $prefix${getter.simpleName}">#</a>
</h4>
''');
if (inherited) {
write('<div class="inherited-from">inherited from ');
annotateType(host, getter.owner);
write('</div>');
}
DocComment comment = getMemberComment(getter);
if (comment == null && setter != null) {
comment = getMemberComment(setter);
}
writeln('<div class="doc">');
docComment(host, comment);
docCode(getter.location);
if (setter != null) {
docCode(setter.location);
}
writeln('</div>');
writeln('</div>');
_currentMember = null;
}
void docParamList(ContainerMirror enclosingType,
List<ParameterMirror> parameters) {
write('(');
bool first = true;
bool inOptionals = false;
bool isNamed = false;
for (final parameter in parameters) {
if (!first) write(', ');
if (!inOptionals && parameter.isOptional) {
isNamed = parameter.isNamed;
write(isNamed ? '{' : '[');
inOptionals = true;
}
annotateType(enclosingType, parameter.type, parameter.simpleName);
// Show the default value for named optional parameters.
if (parameter.isOptional && parameter.hasDefaultValue) {
write(isNamed ? ': ' : ' = ');
write(parameter.defaultValue);
}
first = false;
}
if (inOptionals) {
write(isNamed ? '}' : ']');
}
write(')');
}
void docComment(ContainerMirror host, DocComment comment) {
if (comment != null) {
var html = comment.html;
if (comment.inheritedFrom != null) {
writeln('<div class="inherited">');
writeln(html);
write('<div class="docs-inherited-from">docs inherited from ');
annotateType(host, comment.inheritedFrom);
write('</div>');
writeln('</div>');
} else {
writeln(html);
}
}
}
/**
* Documents the source code contained within [location].
*/
void docCode(SourceLocation location) {
if (includeSource) {
writeln('<pre class="source">');
writeln(md.escapeHtml(unindentCode(location)));
writeln('</pre>');
}
}
/** Get the doc comment associated with the given library. */
DocComment getLibraryComment(LibraryMirror library) => getComment(library);
/** Get the doc comment associated with the given type. */
DocComment getTypeComment(TypeMirror type) => getComment(type);
/**
* Get the doc comment associated with the given member.
*
* If no comment was found on the member, the hierarchy is traversed to find
* an inherited comment, favouring comments inherited from classes over
* comments inherited from interfaces.
*/
DocComment getMemberComment(MemberMirror member) => getComment(member);
/**
* Get the doc comment associated with the given declaration.
*
* If no comment was found on a member, the hierarchy is traversed to find
* an inherited comment, favouring comments inherited from classes over
* comments inherited from interfaces.
*/
DocComment getComment(DeclarationMirror mirror) {
String comment = computeComment(mirror);
ClassMirror inheritedFrom = null;
if (comment == null) {
if (mirror.owner is ClassMirror) {
var iterable =
new HierarchyIterable(mirror.owner,
includeType: false);
for (ClassMirror type in iterable) {
var inheritedMember = type.members[mirror.simpleName];
if (inheritedMember is MemberMirror) {
comment = computeComment(inheritedMember);
if (comment != null) {
inheritedFrom = type;
break;
}
}
}
}
}
if (comment == null) return null;
return new DocComment(comment, inheritedFrom, dartdocSyntaxes,
dartdocResolver);
}
/**
* Converts [fullPath] 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 fullPath) {
// Don't make it relative if it's an absolute path.
if (isAbsolute(fullPath)) return fullPath;
// TODO(rnystrom): Walks all the way up to root each time. Shouldn't do
// this if the paths overlap.
return '${repeat('../',
countOccurrences(_filePath.toString(), '/'))}$fullPath';
}
/** Gets whether or not the given URL is absolute or relative. */
bool isAbsolute(String url) {
// TODO(rnystrom): Why don't we have a nice type in the platform for this?
// TODO(rnystrom): This is a bit hackish. We consider any URL that lacks
// a scheme to be relative.
return new RegExp(r'^\w+:').hasMatch(url);
}
/** Gets the URL to the documentation for [library]. */
String libraryUrl(LibraryMirror library) {
return '${sanitize(displayName(library))}.html';
}
/** Gets the URL for the documentation for [type]. */
String typeUrl(ContainerMirror type) {
if (type is LibraryMirror) {
return '${sanitize(type.simpleName)}.html';
}
if (type.library == null) {
return '';
}
// 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(displayName(_libraryFor(type)))}/'
'${type.originalDeclaration.simpleName}.html';
}
/** Gets the URL for the documentation for [member]. */
String memberUrl(MemberMirror member) {
String url = typeUrl(_ownerFor(member));
return '$url#${memberAnchor(member)}';
}
/** Gets the anchor id for the document for [member]. */
String memberAnchor(MemberMirror member) {
return member.simpleName;
}
/**
* Creates a hyperlink. Handles turning the [href] into an appropriate
* relative path from the current file.
*/
String a(String href, String contents, [String css]) {
// Mark outgoing external links, mainly so we can style them.
final rel = isAbsolute(href) ? ' ref="external"' : '';
final cssClass = css == null ? '' : ' class="$css"';
return '<a href="${relativePath(href)}"$cssClass$rel>$contents</a>';
}
/**
* Writes a type annotation, preferring to print dynamic.
*/
annotateDynamicType(ContainerMirror enclosingType,
TypeMirror type) {
annotateType(enclosingType, type, type.isDynamic ? 'dynamic ' : null);
}
/**
* Writes a type annotation for the given type and (optional) parameter name.
*/
annotateType(ContainerMirror enclosingType,
TypeMirror type,
[String paramName = null]) {
// Don't bother explicitly displaying dynamic.
if (type.isDynamic) {
if (paramName != null) write(paramName);
return;
}
// For parameters, handle non-typedefed function types.
if (paramName != null && type is FunctionTypeMirror) {
annotateType(enclosingType, type.returnType);
write(paramName);
docParamList(enclosingType, type.parameters);
return;
}
linkToType(enclosingType, type);
write(' ');
if (paramName != null) write(paramName);
}
/** Writes a link to a human-friendly string representation for a type. */
linkToType(ContainerMirror enclosingType, TypeMirror type) {
if (type.isVoid) {
// Do not generate links for void.
// TODO(johnniwinter): Generate span for specific style?
write('void');
return;
}
if (type.isDynamic) {
// Do not generate links for dynamic.
write('dynamic');
return;
}
if (type.isTypeVariable) {
// If we're using a type parameter within the body of a generic class then
// just link back up to the class.
write(a(typeUrl(enclosingType), type.simpleName));
return;
}
assert(type is ClassMirror);
// Link to the type.
var library = _libraryFor(type);
if (shouldLinkToPublicApi(library)) {
write('<a href="$API_LOCATION${typeUrl(type)}">${type.simpleName}</a>');
} else if (shouldIncludeLibrary(library)) {
write(a(typeUrl(type), type.simpleName));
} else {
write(type.simpleName);
}
if (type.isOriginalDeclaration) {
// Avoid calling [:typeArguments():] on a declaration.
return;
}
// See if it's an instantiation of a generic type.
final typeArgs = type.typeArguments;
if (typeArgs.length > 0) {
write('&lt;');
bool first = true;
for (final arg in typeArgs) {
if (!first) write(', ');
first = false;
linkToType(enclosingType, arg);
}
write('&gt;');
}
}
/** Creates a linked cross reference to [type]. */
typeReference(ClassMirror type) {
// TODO(rnystrom): Do we need to handle ParameterTypes here like
// annotation() does?
return a(typeUrl(type), typeName(type), 'crossref');
}
/** Generates a human-friendly string representation for a type. */
typeName(TypeMirror type, {bool showBounds: false}) {
if (type.isVoid) {
return 'void';
}
if (type.isDynamic) {
return 'dynamic';
}
if (type is TypeVariableMirror) {
return type.simpleName;
}
assert(type is ClassMirror);
// See if it's a generic type.
if (type.isOriginalDeclaration) {
final typeParams = [];
for (final typeParam in type.originalDeclaration.typeVariables) {
if (showBounds &&
(typeParam.upperBound != null) &&
!typeParam.upperBound.isObject) {
final bound = typeName(typeParam.upperBound, showBounds: true);
typeParams.add('${typeParam.simpleName} extends $bound');
} else {
typeParams.add(typeParam.simpleName);
}
}
if (typeParams.isEmpty) {
return type.simpleName;
}
final params = typeParams.join(', ');
return '${type.simpleName}&lt;$params&gt;';
}
// See if it's an instantiation of a generic type.
final typeArgs = type.typeArguments;
if (typeArgs.length > 0) {
final args = typeArgs.map((arg) => typeName(arg)).join(', ');
return '${type.originalDeclaration.simpleName}&lt;$args&gt;';
}
// Regular type.
return type.simpleName;
}
/**
* Remove leading indentation to line up with first line.
*/
unindentCode(SourceLocation span) {
final column = span.column;
final lines = span.text.split('\n');
// TODO(rnystrom): Dirty hack.
for (var i = 1; i < lines.length; i++) {
lines[i] = unindent(lines[i], column);
}
final code = lines.join('\n');
return code;
}
/**
* Takes a string of Dart code and turns it into sanitized HTML.
*/
formatCode(SourceLocation span) {
final code = unindentCode(span);
// Syntax highlight.
return classifySource(code);
}
/**
* 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,
{MemberMirror currentMember,
ContainerMirror currentType,
LibraryMirror currentLibrary}) {
makeLink(String href) {
final anchor = new md.Element.text('a', name);
anchor.attributes['href'] = relativePath(href);
anchor.attributes['class'] = 'crossref';
return anchor;
}
DeclarationMirror declaration = currentMember;
if (declaration == null) declaration = currentType;
if (declaration == null) declaration = currentLibrary;
if (declaration != null) {
declaration = lookupQualifiedInScope(declaration, name);
}
if (declaration != null) {
if (declaration is TypeVariableMirror) {
return makeLink(typeUrl(declaration.owner));
} if (declaration is TypeMirror) {
return makeLink(typeUrl(declaration));
} else if (declaration is MethodMirror) {
return makeLink(memberUrl(declaration));
}
}
// See if it's a parameter of the current method.
if (currentMember is MethodMirror) {
for (final parameter in currentMember.parameters) {
if (parameter.simpleName == 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 foundMember = currentType.members[name];
if (foundMember != null) {
return makeLink(memberUrl(foundMember));
}
}
// See if it's another type or a member of another type in the current
// library.
if (currentLibrary != null) {
// See if it's a constructor
final constructorLink = (() {
final match =
new RegExp(r'new ([\w$]+)(?:\.([\w$]+))?').firstMatch(name);
if (match == null) return;
String typeName = match[1];
ClassMirror foundtype = currentLibrary.classes[typeName];
if (foundtype == null) return;
String constructorName =
(match[2] == null) ? typeName : '$typeName.${match[2]}';
final constructor =
foundtype.constructors[constructorName];
if (constructor == null) return;
return makeLink(memberUrl(constructor));
})();
if (constructorLink != null) return constructorLink;
// See if it's a member of another type
final foreignMemberLink = (() {
final match = new RegExp(r'([\w$]+)\.([\w$]+)').firstMatch(name);
if (match == null) return;
ClassMirror foundtype = currentLibrary.classes[match[1]];
if (foundtype == null) return;
MemberMirror foundMember = foundtype.members[match[2]];
if (foundMember == null) return;
return makeLink(memberUrl(foundMember));
})();
if (foreignMemberLink != null) return foreignMemberLink;
ClassMirror foundType = currentLibrary.classes[name];
if (foundType != null) {
return makeLink(typeUrl(foundType));
}
// See if it's a top-level member in the current library.
MemberMirror foundMember = currentLibrary.members[name];
if (foundMember != null) {
return makeLink(memberUrl(foundMember));
}
}
// 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);
}
generateAppCacheManifest() {
if (verbose) {
print('Generating app cache manifest from output $outputDir');
}
startFile('appcache.manifest');
write("CACHE MANIFEST\n\n");
write("# VERSION: ${new DateTime.now()}\n\n");
write("NETWORK:\n*\n\n");
write("CACHE:\n");
var toCache = new Directory(outputDir);
toCache.list(recursive: true).listen(
(FileSystemEntity entity) {
if (entity is File) {
var filename = entity.path;
if (filename.endsWith('appcache.manifest')) {
return;
}
String relativeFilePath = path.relative(filename, from: outputDir);
write("$relativeFilePath\n");
}
},
onDone: () => endFile());
}
/**
* Returns [:true:] if [type] should be regarded as an exception.
*/
bool isException(TypeMirror type) {
return type.simpleName.endsWith('Exception') ||
type.simpleName.endsWith('Error');
}
/**
* Returns the absolute path to [library] on the filesystem, or `null` if the
* library doesn't exist on the local filesystem.
*/
String _libraryPath(LibraryMirror library) =>
importUriToPath(library.uri, packageRoot: _packageRoot);
/**
* Returns a list of classes in [library], including classes it exports from
* hidden libraries.
*/
List<ClassMirror> _libraryClasses(LibraryMirror library) =>
_libraryContents(library, (lib) => lib.classes.values);
/**
* Returns a list of top-level members in [library], including members it
* exports from hidden libraries.
*/
List<MemberMirror> _libraryMembers(LibraryMirror library) =>
_libraryContents(library, (lib) => lib.members.values);
/**
* Returns a list of elements in [library], including elements it exports from
* hidden libraries. [fn] should return the element list for a single library,
* which will then be merged across all exported libraries.
*/
List<DeclarationMirror> _libraryContents(LibraryMirror library,
List<DeclarationMirror> fn(LibraryMirror)) {
var contents = fn(library).toList();
var path = _libraryPath(library);
if (path == null || _exports.exports[path] == null) return contents;
contents.addAll(_exports.exports[path].expand((export) {
var exportedLibrary = _librariesByPath[export.path];
// TODO(nweiz): remove this check when issue 9645 is fixed.
if (exportedLibrary == null) return [];
if (shouldIncludeLibrary(exportedLibrary)) return [];
return fn(exportedLibrary).where((declaration) =>
export.isMemberVisible(displayName(declaration)));
}));
return contents;
}
/**
* Returns the library in which [type] was defined. If [type] was defined in a
* hidden library that was exported by another library, this returns the
* exporter.
*/
LibraryMirror _libraryFor(TypeMirror type) =>
_visibleLibrary(type.library, displayName(type));
/**
* Returns the owner of [declaration]. If [declaration]'s owner is a hidden
* library that was exported by another library, this returns the exporter.
*/
DeclarationMirror _ownerFor(DeclarationMirror declaration) {
var owner = declaration.owner;
if (owner is! LibraryMirror) return owner;
return _visibleLibrary(owner, displayName(declaration));
}
/**
* Returns the best visible library that exports [name] from [library]. If
* [library] is public, it will be returned.
*/
LibraryMirror _visibleLibrary(LibraryMirror library, String name) {
if (library == null) return null;
var exports = _hiddenLibraryExports[_libraryPath(library)];
if (exports == null) return library;
var export = exports.firstWhere(
(exp) => exp.isMemberVisible(name),
orElse: () => null);
if (export == null) return library;
return _librariesByPath[export.exporter];
}
}
/**
* Used to report an unexpected error in the DartDoc tool or the
* underlying data
*/
class InternalError {
final String message;
const InternalError(this.message);
String toString() => "InternalError: '$message'";
}
/**
* Computes the doc comment for the declaration mirror.
*
* Multiple comments are concatenated with newlines in between.
*/
String computeComment(DeclarationMirror mirror) {
String text;
for (InstanceMirror metadata in mirror.metadata) {
if (metadata is CommentInstanceMirror) {
CommentInstanceMirror comment = metadata;
if (comment.isDocComment) {
if (text == null) {
text = comment.trimmedText;
} else {
text = '$text\n${comment.trimmedText}';
}
}
}
}
return text;
}
/**
* Computes the doc comment for the declaration mirror as a list.
*/
List<String> computeUntrimmedCommentAsList(DeclarationMirror mirror) {
var text = <String>[];
for (InstanceMirror metadata in mirror.metadata) {
if (metadata is CommentInstanceMirror) {
CommentInstanceMirror comment = metadata;
if (comment.isDocComment) {
text.add(comment.text);
}
}
}
return text;
}
class DocComment {
final String text;
md.Resolver dartdocResolver;
List<md.InlineSyntax> dartdocSyntaxes;
/**
* Non-null if the comment is inherited from another declaration.
*/
final ClassMirror inheritedFrom;
DocComment(this.text, [this.inheritedFrom = null, this.dartdocSyntaxes,
this.dartdocResolver]) {
assert(text != null && !text.trim().isEmpty);
}
String toString() => text;
String get html {
return md.markdownToHtml(text,
inlineSyntaxes: dartdocSyntaxes,
linkResolver: dartdocResolver);
}
}
class MdnComment implements DocComment {
final String mdnComment;
final String mdnUrl;
MdnComment(String this.mdnComment, String this.mdnUrl);
String get text => mdnComment;
ClassMirror get inheritedFrom => null;
String get html {
// Wrap the mdn comment so we can highlight it and so we handle MDN scraped
// content that lacks a top-level block tag.
return '''
<div class="mdn">
$mdnComment
<div class="mdn-note"><a href="$mdnUrl">from MDN</a></div>
</div>
''';
}
String toString() => mdnComment;
}