| // 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:io'; |
| import 'dart:isolate'; |
| import 'dart:json' as json; |
| import 'dart:math'; |
| import 'dart:uri'; |
| |
| import 'package:pathos/path.dart' as pathos; |
| |
| 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 '../../compiler/implementation/mirrors/dart2js_mirror.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. |
| Path get scriptDir => |
| new Path(new Options().script).directoryPath; |
| |
| /** |
| * Deletes and recreates the output directory at [path] if it exists. |
| */ |
| void cleanOutputDirectory(Path path) { |
| final outputDir = new Directory.fromPath(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 DirectoryIOException catch (e) { |
| // Ignore. |
| } |
| } |
| |
| /** |
| * Returns the display name of the library. This is necessary to account for |
| * dart: libraries. |
| */ |
| String displayName(LibraryMirror library) { |
| var uri = library.uri.toString(); |
| return uri.startsWith('dart:') ? uri.toString() : library.simpleName; |
| } |
| |
| /** |
| * 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(Path from, Path to) { |
| print('Copying static files...'); |
| final completer = new Completer(); |
| final fromDir = new Directory.fromPath(from); |
| fromDir.list(recursive: false).listen( |
| (FileSystemEntity entity) { |
| if (entity is File) { |
| final name = new Path(entity.path).filename; |
| // TODO(rnystrom): Hackish. Ignore 'hidden' files like .DS_Store. |
| if (name.startsWith('.')) return; |
| |
| File fromFile = entity; |
| File toFile = new File.fromPath(to.append(name)); |
| fromFile.openRead().pipe(toFile.openWrite()); |
| } |
| }, |
| onDone: () => completer.complete(), |
| onError: (e) => completer.completeError(e)); |
| return completer.future; |
| } |
| |
| /** |
| * Compiles the dartdoc client-side code to JavaScript using Dart2js. |
| */ |
| Future compileScript(int mode, Path outputDir, Path libPath) { |
| print('Compiling client JavaScript...'); |
| |
| // TODO(nweiz): don't run this in an isolate when issue 9815 is fixed. |
| return spawnFunction(_compileScript).call({ |
| 'mode': mode, |
| 'outputDir': outputDir.toNativePath(), |
| 'libPath': libPath.toNativePath() |
| }).then((result) { |
| if (result.first == 'success') return; |
| throw result[1]; |
| }); |
| } |
| |
| void _compileScript() { |
| port.receive((message, replyTo) { |
| new Future.sync(() { |
| var clientScript = (message['mode'] == MODE_STATIC) ? |
| 'static' : 'live-nav'; |
| var dartPath = pathos.join(message['libPath'], 'lib', '_internal', |
| 'dartdoc', 'lib', 'src', 'client', 'client-$clientScript.dart'); |
| var jsPath = pathos.join(message['outputDir'], 'client-$clientScript.js'); |
| |
| return dart2js.compile( |
| new Path(dartPath), new Path(message['libPath']), |
| options: const <String>['--categories=Client,Server']).then((jsCode) { |
| writeString(new File(jsPath), jsCode); |
| }); |
| }).then((_) { |
| replyTo.send(['success']); |
| }).catchError((error) { |
| var trace = getAttachedStackTrace(error); |
| var traceString = trace == null ? "" : trace.toString(); |
| replyTo.send(['error', error.toString(), traceString]); |
| }); |
| }); |
| } |
| |
| /** |
| * 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. */ |
| Path dartdocPath; |
| |
| /** Path to generate HTML files into. */ |
| Path outputDir = new Path('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; |
| |
| /** |
| * 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]. */ |
| Path _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() { |
| 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; |
| } |
| } |
| if (libraryName.startsWith('dart:')) { |
| String suffix = libraryName.substring('dart:'.length); |
| 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) { |
| String libraryName = displayName(library); |
| if (libraryName.startsWith('dart:')) { |
| String suffix = libraryName.substring('dart:'.length); |
| 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, Path libPath, |
| String packageRoot) { |
| _packageRoot = packageRoot; |
| _exports = new ExportMap.parse(libraryList, packageRoot); |
| var librariesToAnalyze = _exports.allExportedFiles.toList(); |
| librariesToAnalyze.addAll(libraryList.map((uri) { |
| if (uri.scheme == 'file') return fileUriToPath(uri); |
| // dart2js takes "dart:*" URIs as Path objects for some reason. |
| return uri.toString(); |
| })); |
| |
| var packageRootPath = packageRoot == null ? null : new Path(packageRoot); |
| |
| // TODO(amouravski): make all of these print statements into logging |
| // statements. |
| print('Analyzing libraries...'); |
| return dart2js.analyze( |
| librariesToAnalyze.map((path) => new Path(path)).toList(), libPath, |
| packageRoot: packageRootPath, options: COMPILER_OPTIONS) |
| .then((MirrorSystem mirrors) { |
| print('Generating documentation...'); |
| _document(mirrors); |
| }); |
| } |
| |
| void _document(MirrorSystem mirrors) { |
| _started = true; |
| |
| // Sort the libraries by name (not key). |
| _sortedLibraries = new List<LibraryMirror>.from( |
| mirrors.libraries.values.where(shouldIncludeLibrary)); |
| _sortedLibraries.sort((x, y) { |
| return displayName(x).toUpperCase().compareTo( |
| displayName(y).toUpperCase()); |
| }); |
| |
| _librariesByPath = <String, LibraryMirror>{}; |
| for (var library in mirrors.libraries.values) { |
| var path = _libraryPath(library); |
| if (path == null) continue; |
| path = pathos.normalize(pathos.absolute(path)); |
| _librariesByPath[path] = 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(library1.displayName, library2.displayName); |
| }); |
| hiddenLibraryExports[exporteePath] = exports; |
| }); |
| return hiddenLibraryExports; |
| } |
| |
| MdnComment lookupMdnComment(Mirror mirror) => null; |
| |
| void startFile(String path) { |
| _filePath = new Path(path); |
| _file = new StringBuffer(); |
| } |
| |
| void endFile() { |
| final outPath = outputDir.join(_filePath); |
| final dir = new Directory.fromPath(outPath.directoryPath); |
| 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 DirectoryIOException catch (e) { |
| // Ignore. |
| } |
| } |
| |
| writeString(new File.fromPath(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> › 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(' › ${breadcrumbs[i]}'); |
| } else { |
| write(' › ${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.stringify(createNavigationInfo())); |
| endFile(); |
| } |
| |
| void docNavigationDart() { |
| final dir = new Directory.fromPath(tmpPath); |
| 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 DirectoryIOException catch (e) { |
| // Ignore. |
| } |
| } |
| String jsonString = json.stringify(createNavigationInfo()); |
| String dartString = jsonString.replaceAll(r"$", r"\$"); |
| final filePath = tmpPath.append('nav.dart'); |
| writeString(new File.fromPath(filePath), |
| '''part of client; |
| get json => $dartString;'''); |
| } |
| |
| Path get tmpPath => dartdocPath.append('tmp'); |
| |
| void cleanup() { |
| final dir = new Directory.fromPath(tmpPath); |
| if (dir.existsSync()) { |
| dir.deleteSync(recursive: true); |
| } |
| } |
| |
| 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] = type.displayName; |
| if (type.isClass) { |
| typeInfo[KIND] = CLASS; |
| } else if (type.isInterface) { |
| typeInfo[KIND] = INTERFACE; |
| } 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(typeVariable.displayName); |
| } |
| 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] = member.displayName; |
| 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; |
| _currentType = null; |
| |
| 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 interfaces = <ClassMirror>[]; |
| 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.isInterface){ |
| interfaces.add(type); |
| } else if (type is TypedefMirror) { |
| typedefs.add(type); |
| } else { |
| throw new InternalError("internal error: unknown type $type."); |
| } |
| } |
| |
| docTypes(interfaces, 'Interfaces'); |
| 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); |
| } |
| } |
| |
| 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 = 'interface'; |
| if (type.isTypedef) { |
| kind = 'typedef'; |
| } else if (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(); |
| } |
| |
| /** 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; |
| |
| // 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)); |
| if (type.isClass) { |
| // Show the chain of superclasses. |
| if (!type.superclass.isObject) { |
| final supertypes = []; |
| var thisType = type.superclass; |
| // As a sanity check, only show up to five levels of nesting, otherwise |
| // the box starts to get hideous. |
| 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(' > '); |
| } |
| |
| // Write this class. |
| typeSpan(type); |
| writeln('</p>'); |
| } |
| |
| listTypes(subtypes, 'Subclasses'); |
| listTypes(type.superinterfaces, 'Implements'); |
| } else { |
| // Show the default class. |
| if (type.defaultFactory != null) { |
| listTypes([type.defaultFactory], 'Default class'); |
| } |
| |
| // List extended interfaces. |
| listTypes(type.superinterfaces, 'Extends'); |
| |
| // List subinterfaces and implementing classes. |
| final subinterfaces = []; |
| final implementing = []; |
| |
| for (final subtype in subtypes) { |
| if (subtype.isClass) { |
| implementing.add(subtype); |
| } else { |
| subinterfaces.add(subtype); |
| } |
| } |
| |
| listTypes(subinterfaces, 'Subinterfaces'); |
| listTypes(implementing, 'Implemented by'); |
| } |
| } |
| |
| /** |
| * 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[member.displayName] = member; |
| } else if (member.isSetter) { |
| staticSetters[member.displayName] = member; |
| } else { |
| staticMethods.add(member); |
| } |
| } else if (member is VariableMirror) { |
| staticGetters[member.displayName] = 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[member.displayName] = member; |
| if (_ownerFor(member) == host) { |
| allPropertiesInherited = false; |
| } |
| } else if (member.isSetter) { |
| instanceSetters[member.displayName] = 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[member.displayName] = 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 = member.displayName; |
| 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>'); |
| } |
| |
| 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>'); |
| } |
| |
| 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('<'); |
| bool first = true; |
| for (final arg in typeArgs) { |
| if (!first) write(', '); |
| first = false; |
| linkToType(enclosingType, arg); |
| } |
| write('>'); |
| } |
| } |
| |
| /** 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}<$params>'; |
| } |
| |
| // 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}<$args>'; |
| } |
| |
| // 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; |
| } |
| |
| // 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.fromPath(outputDir); |
| toCache.list(recursive: true).listen( |
| (FileSystemEntity entity) { |
| if (entity.isFile) { |
| var filename = entity.path; |
| if (filename.endsWith('appcache.manifest')) { |
| return; |
| } |
| Path relativeFilePath = new Path(filename).relativeTo(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(declaration.displayName)); |
| })); |
| 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, type.displayName); |
| |
| /** |
| * 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, declaration.displayName); |
| } |
| |
| /** |
| * 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; |
| } |