| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * To generate docs for a library, run this script with the path to an | |
| 7 * entrypoint .dart file, like: | |
| 8 * | |
| 9 * $ dart dartdoc.dart foo.dart | |
| 10 * | |
| 11 * This will create a "docs" directory with the docs for your libraries. To | |
| 12 * create these beautiful docs, dartdoc parses your library and every library | |
| 13 * it imports (recursively). From each library, it parses all classes and | |
| 14 * members, finds the associated doc comments and builds crosslinked docs from | |
| 15 * them. | |
| 16 */ | |
| 17 #library('dartdoc'); | |
| 18 | |
| 19 #import('dart:io'); | |
| 20 #import('dart:math'); | |
| 21 #import('dart:uri'); | |
| 22 #import('dart:json'); | |
| 23 #import('mirrors/mirrors.dart'); | |
| 24 #import('mirrors/mirrors_util.dart'); | |
| 25 #import('mirrors/dart2js_mirror.dart', prefix: 'dart2js'); | |
| 26 #import('classify.dart'); | |
| 27 #import('markdown.dart', prefix: 'md'); | |
| 28 #import('../../lib/compiler/implementation/scanner/scannerlib.dart', | |
| 29 prefix: 'dart2js'); | |
| 30 #import('../../lib/_internal/libraries.dart'); | |
| 31 | |
| 32 #source('comment_map.dart'); | |
| 33 #source('nav.dart'); | |
| 34 #source('utils.dart'); | |
| 35 | |
| 36 // TODO(johnniwinther): Note that [IN_SDK] gets initialized to true when this | |
| 37 // file is modified by the SDK deployment script. If you change, be sure to test | |
| 38 // that dartdoc still works when run from the built SDK directory. | |
| 39 const bool IN_SDK = false; | |
| 40 | |
| 41 /** | |
| 42 * Generates completely static HTML containing everything you need to browse | |
| 43 * the docs. The only client side behavior is trivial stuff like syntax | |
| 44 * highlighting code. | |
| 45 */ | |
| 46 const MODE_STATIC = 0; | |
| 47 | |
| 48 /** | |
| 49 * Generated docs do not include baked HTML navigation. Instead, a single | |
| 50 * `nav.json` file is created and the appropriate navigation is generated | |
| 51 * client-side by parsing that and building HTML. | |
| 52 * | |
| 53 * This dramatically reduces the generated size of the HTML since a large | |
| 54 * fraction of each static page is just redundant navigation links. | |
| 55 * | |
| 56 * In this mode, the browser will do a XHR for nav.json which means that to | |
| 57 * preview docs locally, you will need to enable requesting file:// links in | |
| 58 * your browser or run a little local server like `python -m SimpleHTTPServer`. | |
| 59 */ | |
| 60 const MODE_LIVE_NAV = 1; | |
| 61 | |
| 62 const API_LOCATION = 'http://api.dartlang.org/'; | |
| 63 | |
| 64 /** | |
| 65 * Run this from the `pkg/dartdoc` directory. | |
| 66 */ | |
| 67 void main() { | |
| 68 final args = new Options().arguments; | |
| 69 | |
| 70 final dartdoc = new Dartdoc(); | |
| 71 | |
| 72 if (args.isEmpty()) { | |
| 73 print('No arguments provided.'); | |
| 74 printUsage(); | |
| 75 return; | |
| 76 } | |
| 77 | |
| 78 final entrypoints = <Path>[]; | |
| 79 | |
| 80 var i = 0; | |
| 81 while (i < args.length) { | |
| 82 final arg = args[i]; | |
| 83 if (!arg.startsWith('--')) { | |
| 84 // The remaining arguments must be entry points. | |
| 85 break; | |
| 86 } | |
| 87 | |
| 88 switch (arg) { | |
| 89 case '--no-code': | |
| 90 dartdoc.includeSource = false; | |
| 91 break; | |
| 92 | |
| 93 case '--mode=static': | |
| 94 dartdoc.mode = MODE_STATIC; | |
| 95 break; | |
| 96 | |
| 97 case '--mode=live-nav': | |
| 98 dartdoc.mode = MODE_LIVE_NAV; | |
| 99 break; | |
| 100 | |
| 101 case '--generate-app-cache': | |
| 102 case '--generate-app-cache=true': | |
| 103 dartdoc.generateAppCache = true; | |
| 104 break; | |
| 105 | |
| 106 case '--omit-generation-time': | |
| 107 dartdoc.omitGenerationTime = true; | |
| 108 break; | |
| 109 case '--verbose': | |
| 110 dartdoc.verbose = true; | |
| 111 break; | |
| 112 case '--include-api': | |
| 113 dartdoc.includeApi = true; | |
| 114 break; | |
| 115 case '--link-api': | |
| 116 dartdoc.linkToApi = true; | |
| 117 break; | |
| 118 | |
| 119 default: | |
| 120 if (arg.startsWith('--out=')) { | |
| 121 dartdoc.outputDir = | |
| 122 new Path.fromNative(arg.substring('--out='.length)); | |
| 123 } else if (arg.startsWith('--include-lib=')) { | |
| 124 dartdoc.includedLibraries = | |
| 125 arg.substring('--include-lib='.length).split(','); | |
| 126 } else if (arg.startsWith('--exclude-lib=')) { | |
| 127 dartdoc.excludedLibraries = | |
| 128 arg.substring('--exclude-lib='.length).split(','); | |
| 129 } else { | |
| 130 print('Unknown option: $arg'); | |
| 131 printUsage(); | |
| 132 return; | |
| 133 } | |
| 134 break; | |
| 135 } | |
| 136 i++; | |
| 137 } | |
| 138 while (i < args.length) { | |
| 139 final arg = args[i]; | |
| 140 entrypoints.add(new Path.fromNative(arg)); | |
| 141 i++; | |
| 142 } | |
| 143 | |
| 144 if (entrypoints.isEmpty()) { | |
| 145 print('No entrypoints provided.'); | |
| 146 printUsage(); | |
| 147 return; | |
| 148 } | |
| 149 | |
| 150 cleanOutputDirectory(dartdoc.outputDir); | |
| 151 | |
| 152 dartdoc.documentLibraries(entrypoints, libPath); | |
| 153 | |
| 154 // Compile the client-side code to JS. | |
| 155 final clientScript = (dartdoc.mode == MODE_STATIC) ? 'static' : 'live-nav'; | |
| 156 Future compiled = compileScript( | |
| 157 scriptDir.append('client-$clientScript.dart'), | |
| 158 dartdoc.outputDir.append('client-$clientScript.js')); | |
| 159 | |
| 160 Future filesCopied = copyDirectory(scriptDir.append('static'), | |
| 161 dartdoc.outputDir); | |
| 162 | |
| 163 Futures.wait([compiled, filesCopied]).then((_) { | |
| 164 dartdoc.cleanup(); | |
| 165 print('Documented ${dartdoc._totalLibraries} libraries, ' | |
| 166 '${dartdoc._totalTypes} types, and ' | |
| 167 '${dartdoc._totalMembers} members.'); | |
| 168 }); | |
| 169 } | |
| 170 | |
| 171 void printUsage() { | |
| 172 print(''' | |
| 173 Usage dartdoc [options] <entrypoint(s)> | |
| 174 [options] include | |
| 175 --no-code Do not include source code in the documentation. | |
| 176 | |
| 177 --mode=static Generates completely static HTML containing | |
| 178 everything you need to browse the docs. The only | |
| 179 client side behavior is trivial stuff like syntax | |
| 180 highlighting code. | |
| 181 | |
| 182 --mode=live-nav (default) Generated docs do not include baked HTML | |
| 183 navigation. Instead, a single `nav.json` file is | |
| 184 created and the appropriate navigation is generated | |
| 185 client-side by parsing that and building HTML. | |
| 186 This dramatically reduces the generated size of | |
| 187 the HTML since a large fraction of each static page | |
| 188 is just redundant navigation links. | |
| 189 In this mode, the browser will do a XHR for | |
| 190 nav.json which means that to preview docs locally, | |
| 191 you will need to enable requesting file:// links in | |
| 192 your browser or run a little local server like | |
| 193 `python -m SimpleHTTPServer`. | |
| 194 | |
| 195 --generate-app-cache Generates the App Cache manifest file, enabling | |
| 196 offline doc viewing. | |
| 197 | |
| 198 --out=<dir> Generates files into directory <dir>. If omitted | |
| 199 the files are generated into ./docs/ | |
| 200 | |
| 201 --link-api Link to the online language API in the generated | |
| 202 documentation. The option overrides inclusion | |
| 203 through --include-api or --include-lib. | |
| 204 | |
| 205 --include-api Include the used API libraries in the generated | |
| 206 documentation. If the --link-api option is used, | |
| 207 this option is ignored. | |
| 208 | |
| 209 --include-lib=<libs> Use this option to explicitly specify which | |
| 210 libraries to include in the documentation. If | |
| 211 omitted, all used libraries are included by | |
| 212 default. <libs> is comma-separated list of library | |
| 213 names. | |
| 214 | |
| 215 --exclude-lib=<libs> Use this option to explicitly specify which | |
| 216 libraries to exclude from the documentation. If | |
| 217 omitted, no libraries are excluded. <libs> is | |
| 218 comma-separated list of library names. | |
| 219 | |
| 220 --verbose Print verbose information during generation. | |
| 221 '''); | |
| 222 } | |
| 223 | |
| 224 /** | |
| 225 * Gets the full path to the directory containing the entrypoint of the current | |
| 226 * script. In other words, if you invoked dartdoc, directly, it will be the | |
| 227 * path to the directory containing `dartdoc.dart`. If you're running a script | |
| 228 * that imports dartdoc, it will be the path to that script. | |
| 229 */ | |
| 230 // TODO(johnniwinther): Convert to final (lazily initialized) variables when | |
| 231 // the feature is supported. | |
| 232 Path get scriptDir => | |
| 233 new Path.fromNative(new Options().script).directoryPath; | |
| 234 | |
| 235 // TODO(johnniwinther): Trailing slashes matter due to the use of [libPath] as | |
| 236 // a base URI with [Uri.resolve]. | |
| 237 /// Relative path to the library in which dart2js resides. | |
| 238 Path get libPath => IN_SDK | |
| 239 ? scriptDir.append('../../lib/dart2js/') | |
| 240 : scriptDir.append('../../'); | |
| 241 | |
| 242 /** | |
| 243 * Deletes and recreates the output directory at [path] if it exists. | |
| 244 */ | |
| 245 void cleanOutputDirectory(Path path) { | |
| 246 final outputDir = new Directory.fromPath(path); | |
| 247 if (outputDir.existsSync()) { | |
| 248 outputDir.deleteRecursivelySync(); | |
| 249 } | |
| 250 | |
| 251 try { | |
| 252 // TODO(3914): Hack to avoid 'file already exists' exception thrown | |
| 253 // due to invalid result from dir.existsSync() (probably due to race | |
| 254 // conditions). | |
| 255 outputDir.createSync(); | |
| 256 } on DirectoryIOException catch (e) { | |
| 257 // Ignore. | |
| 258 } | |
| 259 } | |
| 260 | |
| 261 /** | |
| 262 * Copies all of the files in the directory [from] to [to]. Does *not* | |
| 263 * recursively copy subdirectories. | |
| 264 * | |
| 265 * Note: runs asynchronously, so you won't see any files copied until after the | |
| 266 * event loop has had a chance to pump (i.e. after `main()` has returned). | |
| 267 */ | |
| 268 Future copyDirectory(Path from, Path to) { | |
| 269 final completer = new Completer(); | |
| 270 final fromDir = new Directory.fromPath(from); | |
| 271 final lister = fromDir.list(recursive: false); | |
| 272 | |
| 273 lister.onFile = (String path) { | |
| 274 final name = new Path.fromNative(path).filename; | |
| 275 // TODO(rnystrom): Hackish. Ignore 'hidden' files like .DS_Store. | |
| 276 if (name.startsWith('.')) return; | |
| 277 | |
| 278 File fromFile = new File(path); | |
| 279 File toFile = new File.fromPath(to.append(name)); | |
| 280 fromFile.openInputStream().pipe(toFile.openOutputStream()); | |
| 281 }; | |
| 282 lister.onDone = (done) => completer.complete(true); | |
| 283 return completer.future; | |
| 284 } | |
| 285 | |
| 286 /** | |
| 287 * Compiles the given Dart script to a JavaScript file at [jsPath] using the | |
| 288 * Dart2js compiler. | |
| 289 */ | |
| 290 Future<bool> compileScript(Path dartPath, Path jsPath) { | |
| 291 var completer = new Completer<bool>(); | |
| 292 var compilation = new Compilation(dartPath, libPath); | |
| 293 Future<String> result = compilation.compileToJavaScript(); | |
| 294 result.then((jsCode) { | |
| 295 writeString(new File.fromPath(jsPath), jsCode); | |
| 296 completer.complete(true); | |
| 297 }); | |
| 298 result.handleException((e) => completer.completeException(e)); | |
| 299 return completer.future; | |
| 300 } | |
| 301 | |
| 302 class Dartdoc { | |
| 303 | |
| 304 /** Set to `false` to not include the source code in the generated docs. */ | |
| 305 bool includeSource = true; | |
| 306 | |
| 307 /** | |
| 308 * Dartdoc can generate docs in a few different ways based on how dynamic you | |
| 309 * want the client-side behavior to be. The value for this should be one of | |
| 310 * the `MODE_` constants. | |
| 311 */ | |
| 312 int mode = MODE_LIVE_NAV; | |
| 313 | |
| 314 /** | |
| 315 * Generates the App Cache manifest file, enabling offline doc viewing. | |
| 316 */ | |
| 317 bool generateAppCache = false; | |
| 318 | |
| 319 /** Path to the dartdoc directory. */ | |
| 320 Path dartdocPath; | |
| 321 | |
| 322 /** Path to generate HTML files into. */ | |
| 323 Path outputDir = const Path('docs'); | |
| 324 | |
| 325 /** | |
| 326 * The title used for the overall generated output. Set this to change it. | |
| 327 */ | |
| 328 String mainTitle = 'Dart Documentation'; | |
| 329 | |
| 330 /** | |
| 331 * The URL that the Dart logo links to. Defaults "index.html", the main | |
| 332 * page for the generated docs, but can be anything. | |
| 333 */ | |
| 334 String mainUrl = 'index.html'; | |
| 335 | |
| 336 /** | |
| 337 * The Google Custom Search ID that should be used for the search box. If | |
| 338 * this is `null` then no search box will be shown. | |
| 339 */ | |
| 340 String searchEngineId = null; | |
| 341 | |
| 342 /* The URL that the embedded search results should be displayed on. */ | |
| 343 String searchResultsUrl = 'results.html'; | |
| 344 | |
| 345 /** Set this to add footer text to each generated page. */ | |
| 346 String footerText = null; | |
| 347 | |
| 348 /** Set this to add content before the footer */ | |
| 349 String preFooterText = ''; | |
| 350 | |
| 351 /** Set this to omit generation timestamp from output */ | |
| 352 bool omitGenerationTime = false; | |
| 353 | |
| 354 /** Set by Dartdoc user to print extra information during generation. */ | |
| 355 bool verbose = false; | |
| 356 | |
| 357 /** Set this to include API libraries in the documentation. */ | |
| 358 bool includeApi = false; | |
| 359 | |
| 360 /** Set this to generate links to the online API. */ | |
| 361 bool linkToApi = false; | |
| 362 | |
| 363 /** Set this to select the libraries to include in the documentation. */ | |
| 364 List<String> includedLibraries = const <String>[]; | |
| 365 | |
| 366 /** Set this to select the libraries to exclude from the documentation. */ | |
| 367 List<String> excludedLibraries = const <String>[]; | |
| 368 | |
| 369 /** | |
| 370 * This list contains the libraries sorted in by the library name. | |
| 371 */ | |
| 372 List<LibraryMirror> _sortedLibraries; | |
| 373 | |
| 374 CommentMap _comments; | |
| 375 | |
| 376 /** The library that we're currently generating docs for. */ | |
| 377 LibraryMirror _currentLibrary; | |
| 378 | |
| 379 /** The type that we're currently generating docs for. */ | |
| 380 InterfaceMirror _currentType; | |
| 381 | |
| 382 /** The member that we're currently generating docs for. */ | |
| 383 MemberMirror _currentMember; | |
| 384 | |
| 385 /** The path to the file currently being written to, relative to [outdir]. */ | |
| 386 Path _filePath; | |
| 387 | |
| 388 /** The file currently being written to. */ | |
| 389 StringBuffer _file; | |
| 390 | |
| 391 int _totalLibraries = 0; | |
| 392 int _totalTypes = 0; | |
| 393 int _totalMembers = 0; | |
| 394 | |
| 395 Dartdoc() | |
| 396 : _comments = new CommentMap(), | |
| 397 dartdocPath = scriptDir { | |
| 398 // Patch in support for [:...:]-style code to the markdown parser. | |
| 399 // TODO(rnystrom): Markdown already has syntax for this. Phase this out? | |
| 400 md.InlineParser.syntaxes.insertRange(0, 1, | |
| 401 new md.CodeSyntax(@'\[\:((?:.|\n)*?)\:\]')); | |
| 402 | |
| 403 md.setImplicitLinkResolver((name) => resolveNameReference(name, | |
| 404 currentLibrary: _currentLibrary, currentType: _currentType, | |
| 405 currentMember: _currentMember)); | |
| 406 } | |
| 407 | |
| 408 /** | |
| 409 * Returns `true` if [library] is included in the generated documentation. | |
| 410 */ | |
| 411 bool shouldIncludeLibrary(LibraryMirror library) { | |
| 412 if (shouldLinkToPublicApi(library)) { | |
| 413 return false; | |
| 414 } | |
| 415 var includeByDefault = true; | |
| 416 String libraryName = library.simpleName; | |
| 417 if (!includedLibraries.isEmpty()) { | |
| 418 includeByDefault = false; | |
| 419 if (includedLibraries.indexOf(libraryName) != -1) { | |
| 420 return true; | |
| 421 } | |
| 422 } | |
| 423 if (excludedLibraries.indexOf(libraryName) != -1) { | |
| 424 return false; | |
| 425 } | |
| 426 if (libraryName.startsWith('dart:')) { | |
| 427 String suffix = libraryName.substring('dart:'.length); | |
| 428 LibraryInfo info = LIBRARIES[suffix]; | |
| 429 if (info != null) { | |
| 430 return info.documented && includeApi; | |
| 431 } | |
| 432 } | |
| 433 return includeByDefault; | |
| 434 } | |
| 435 | |
| 436 /** | |
| 437 * Returns `true` if links to the public API should be generated for | |
| 438 * [library]. | |
| 439 */ | |
| 440 bool shouldLinkToPublicApi(LibraryMirror library) { | |
| 441 if (linkToApi) { | |
| 442 String libraryName = library.simpleName; | |
| 443 if (libraryName.startsWith('dart:')) { | |
| 444 String suffix = libraryName.substring('dart:'.length); | |
| 445 LibraryInfo info = LIBRARIES[suffix]; | |
| 446 if (info != null) { | |
| 447 return info.documented; | |
| 448 } | |
| 449 } | |
| 450 } | |
| 451 return false; | |
| 452 } | |
| 453 | |
| 454 String get footerContent{ | |
| 455 var footerItems = []; | |
| 456 if (!omitGenerationTime) { | |
| 457 footerItems.add("This page was generated at ${new Date.now()}"); | |
| 458 } | |
| 459 if (footerText != null) { | |
| 460 footerItems.add(footerText); | |
| 461 } | |
| 462 var content = ''; | |
| 463 for (int i = 0; i < footerItems.length; i++) { | |
| 464 if (i > 0) { | |
| 465 content = content.concat('\n'); | |
| 466 } | |
| 467 content = content.concat('<div>${footerItems[i]}</div>'); | |
| 468 } | |
| 469 return content; | |
| 470 } | |
| 471 | |
| 472 void documentEntryPoint(Path entrypoint, Path libPath) { | |
| 473 final compilation = new Compilation(entrypoint, libPath); | |
| 474 _document(compilation); | |
| 475 } | |
| 476 | |
| 477 void documentLibraries(List<Path> libraryList, Path libPath) { | |
| 478 final compilation = new Compilation.library(libraryList, libPath); | |
| 479 _document(compilation); | |
| 480 } | |
| 481 | |
| 482 void _document(Compilation compilation) { | |
| 483 // Sort the libraries by name (not key). | |
| 484 _sortedLibraries = new List<LibraryMirror>.from( | |
| 485 compilation.mirrors.libraries.getValues().filter( | |
| 486 shouldIncludeLibrary)); | |
| 487 _sortedLibraries.sort((x, y) { | |
| 488 return x.simpleName.toUpperCase().compareTo( | |
| 489 y.simpleName.toUpperCase()); | |
| 490 }); | |
| 491 | |
| 492 // Generate the docs. | |
| 493 if (mode == MODE_LIVE_NAV) { | |
| 494 docNavigationJson(); | |
| 495 } else { | |
| 496 docNavigationDart(); | |
| 497 } | |
| 498 | |
| 499 docIndex(); | |
| 500 for (final library in _sortedLibraries) { | |
| 501 docLibrary(library); | |
| 502 } | |
| 503 | |
| 504 if (generateAppCache) { | |
| 505 generateAppCacheManifest(); | |
| 506 } | |
| 507 } | |
| 508 | |
| 509 void startFile(String path) { | |
| 510 _filePath = new Path(path); | |
| 511 _file = new StringBuffer(); | |
| 512 } | |
| 513 | |
| 514 void endFile() { | |
| 515 final outPath = outputDir.join(_filePath); | |
| 516 final dir = new Directory.fromPath(outPath.directoryPath); | |
| 517 if (!dir.existsSync()) { | |
| 518 // TODO(3914): Hack to avoid 'file already exists' exception | |
| 519 // thrown due to invalid result from dir.existsSync() (probably due to | |
| 520 // race conditions). | |
| 521 try { | |
| 522 dir.createSync(); | |
| 523 } on DirectoryIOException catch (e) { | |
| 524 // Ignore. | |
| 525 } | |
| 526 } | |
| 527 | |
| 528 writeString(new File.fromPath(outPath), _file.toString()); | |
| 529 _filePath = null; | |
| 530 _file = null; | |
| 531 } | |
| 532 | |
| 533 void write(String s) { | |
| 534 _file.add(s); | |
| 535 } | |
| 536 | |
| 537 void writeln(String s) { | |
| 538 write(s); | |
| 539 write('\n'); | |
| 540 } | |
| 541 | |
| 542 /** | |
| 543 * Writes the page header with the given [title] and [breadcrumbs]. The | |
| 544 * breadcrumbs are an interleaved list of links and titles. If a link is null, | |
| 545 * then no link will be generated. For example, given: | |
| 546 * | |
| 547 * ['foo', 'foo.html', 'bar', null] | |
| 548 * | |
| 549 * It will output: | |
| 550 * | |
| 551 * <a href="foo.html">foo</a> › bar | |
| 552 */ | |
| 553 void writeHeader(String title, List<String> breadcrumbs) { | |
| 554 final htmlAttributes = generateAppCache ? | |
| 555 'manifest="/appcache.manifest"' : ''; | |
| 556 | |
| 557 write( | |
| 558 ''' | |
| 559 <!DOCTYPE html> | |
| 560 <html${htmlAttributes == '' ? '' : ' $htmlAttributes'}> | |
| 561 <head> | |
| 562 '''); | |
| 563 writeHeadContents(title); | |
| 564 | |
| 565 // Add data attributes describing what the page documents. | |
| 566 var data = ''; | |
| 567 if (_currentLibrary != null) { | |
| 568 data = '$data data-library=' | |
| 569 '"${md.escapeHtml(_currentLibrary.simpleName)}"'; | |
| 570 } | |
| 571 | |
| 572 if (_currentType != null) { | |
| 573 data = '$data data-type="${md.escapeHtml(typeName(_currentType))}"'; | |
| 574 } | |
| 575 | |
| 576 write( | |
| 577 ''' | |
| 578 </head> | |
| 579 <body$data> | |
| 580 <div class="page"> | |
| 581 <div class="header"> | |
| 582 ${a(mainUrl, '<div class="logo"></div>')} | |
| 583 ${a('index.html', mainTitle)} | |
| 584 '''); | |
| 585 | |
| 586 // Write the breadcrumb trail. | |
| 587 for (int i = 0; i < breadcrumbs.length; i += 2) { | |
| 588 if (breadcrumbs[i + 1] == null) { | |
| 589 write(' › ${breadcrumbs[i]}'); | |
| 590 } else { | |
| 591 write(' › ${a(breadcrumbs[i + 1], breadcrumbs[i])}'); | |
| 592 } | |
| 593 } | |
| 594 | |
| 595 if (searchEngineId != null) { | |
| 596 writeln( | |
| 597 ''' | |
| 598 <form action="$searchResultsUrl" id="search-box"> | |
| 599 <input type="hidden" name="cx" value="$searchEngineId"> | |
| 600 <input type="hidden" name="ie" value="UTF-8"> | |
| 601 <input type="hidden" name="hl" value="en"> | |
| 602 <input type="search" name="q" id="q" autocomplete="off" | |
| 603 class="search-input" placeholder="Search API"> | |
| 604 </form> | |
| 605 '''); | |
| 606 } else { | |
| 607 writeln( | |
| 608 ''' | |
| 609 <div id="search-box"> | |
| 610 <input type="search" name="q" id="q" autocomplete="off" | |
| 611 class="search-input" placeholder="Search API"> | |
| 612 </div> | |
| 613 '''); | |
| 614 } | |
| 615 | |
| 616 writeln( | |
| 617 ''' | |
| 618 </div> | |
| 619 <div class="drop-down" id="drop-down"></div> | |
| 620 '''); | |
| 621 | |
| 622 docNavigation(); | |
| 623 writeln('<div class="content">'); | |
| 624 } | |
| 625 | |
| 626 String get clientScript { | |
| 627 switch (mode) { | |
| 628 case MODE_STATIC: return 'client-static'; | |
| 629 case MODE_LIVE_NAV: return 'client-live-nav'; | |
| 630 default: throw 'Unknown mode $mode.'; | |
| 631 } | |
| 632 } | |
| 633 | |
| 634 void writeHeadContents(String title) { | |
| 635 writeln( | |
| 636 ''' | |
| 637 <meta charset="utf-8"> | |
| 638 <title>$title / $mainTitle</title> | |
| 639 <link rel="stylesheet" type="text/css" | |
| 640 href="${relativePath('styles.css')}"> | |
| 641 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700
,800" rel="stylesheet" type="text/css"> | |
| 642 <link rel="shortcut icon" href="${relativePath('favicon.ico')}"> | |
| 643 '''); | |
| 644 } | |
| 645 | |
| 646 void writeFooter() { | |
| 647 writeln( | |
| 648 ''' | |
| 649 </div> | |
| 650 <div class="clear"></div> | |
| 651 </div> | |
| 652 ${preFooterText} | |
| 653 <div class="footer"> | |
| 654 $footerContent | |
| 655 </div> | |
| 656 <script async src="${relativePath('$clientScript.js')}"></script> | |
| 657 </body></html> | |
| 658 '''); | |
| 659 } | |
| 660 | |
| 661 void docIndex() { | |
| 662 startFile('index.html'); | |
| 663 | |
| 664 writeHeader(mainTitle, []); | |
| 665 | |
| 666 writeln('<h2>$mainTitle</h2>'); | |
| 667 writeln('<h3>Libraries</h3>'); | |
| 668 | |
| 669 for (final library in _sortedLibraries) { | |
| 670 docIndexLibrary(library); | |
| 671 } | |
| 672 | |
| 673 writeFooter(); | |
| 674 endFile(); | |
| 675 } | |
| 676 | |
| 677 void docIndexLibrary(LibraryMirror library) { | |
| 678 writeln('<h4>${a(libraryUrl(library), library.simpleName)}</h4>'); | |
| 679 } | |
| 680 | |
| 681 /** | |
| 682 * Walks the libraries and creates a JSON object containing the data needed | |
| 683 * to generate navigation for them. | |
| 684 */ | |
| 685 void docNavigationJson() { | |
| 686 startFile('nav.json'); | |
| 687 writeln(JSON.stringify(createNavigationInfo())); | |
| 688 endFile(); | |
| 689 } | |
| 690 | |
| 691 void docNavigationDart() { | |
| 692 final dir = new Directory.fromPath(tmpPath); | |
| 693 if (!dir.existsSync()) { | |
| 694 // TODO(3914): Hack to avoid 'file already exists' exception | |
| 695 // thrown due to invalid result from dir.existsSync() (probably due to | |
| 696 // race conditions). | |
| 697 try { | |
| 698 dir.createSync(); | |
| 699 } on DirectoryIOException catch (e) { | |
| 700 // Ignore. | |
| 701 } | |
| 702 } | |
| 703 String jsonString = JSON.stringify(createNavigationInfo()); | |
| 704 String dartString = jsonString.replaceAll(@"$", @"\$"); | |
| 705 final filePath = tmpPath.append('nav.dart'); | |
| 706 writeString(new File.fromPath(filePath), | |
| 707 'get json => $dartString;'); | |
| 708 } | |
| 709 | |
| 710 Path get tmpPath => dartdocPath.append('tmp'); | |
| 711 | |
| 712 void cleanup() { | |
| 713 final dir = new Directory.fromPath(tmpPath); | |
| 714 if (dir.existsSync()) { | |
| 715 dir.deleteRecursivelySync(); | |
| 716 } | |
| 717 } | |
| 718 | |
| 719 List createNavigationInfo() { | |
| 720 final libraryList = []; | |
| 721 for (final library in _sortedLibraries) { | |
| 722 docLibraryNavigationJson(library, libraryList); | |
| 723 } | |
| 724 return libraryList; | |
| 725 } | |
| 726 | |
| 727 void docLibraryNavigationJson(LibraryMirror library, List libraryList) { | |
| 728 var libraryInfo = {}; | |
| 729 libraryInfo[NAME] = library.simpleName; | |
| 730 final List members = docMembersJson(library.declaredMembers); | |
| 731 if (!members.isEmpty()) { | |
| 732 libraryInfo[MEMBERS] = members; | |
| 733 } | |
| 734 | |
| 735 final types = []; | |
| 736 for (InterfaceMirror type in orderByName(library.types.getValues())) { | |
| 737 if (type.isPrivate) continue; | |
| 738 | |
| 739 var typeInfo = {}; | |
| 740 typeInfo[NAME] = type.simpleName; | |
| 741 if (type.isClass) { | |
| 742 typeInfo[KIND] = CLASS; | |
| 743 } else if (type.isInterface) { | |
| 744 typeInfo[KIND] = INTERFACE; | |
| 745 } else { | |
| 746 assert(type.isTypedef); | |
| 747 typeInfo[KIND] = TYPEDEF; | |
| 748 } | |
| 749 final List typeMembers = docMembersJson(type.declaredMembers); | |
| 750 if (!typeMembers.isEmpty()) { | |
| 751 typeInfo[MEMBERS] = typeMembers; | |
| 752 } | |
| 753 | |
| 754 if (!type.declaration.typeVariables.isEmpty()) { | |
| 755 final typeVariables = []; | |
| 756 for (final typeVariable in type.declaration.typeVariables) { | |
| 757 typeVariables.add(typeVariable.simpleName); | |
| 758 } | |
| 759 typeInfo[ARGS] = Strings.join(typeVariables, ', '); | |
| 760 } | |
| 761 types.add(typeInfo); | |
| 762 } | |
| 763 if (!types.isEmpty()) { | |
| 764 libraryInfo[TYPES] = types; | |
| 765 } | |
| 766 | |
| 767 libraryList.add(libraryInfo); | |
| 768 } | |
| 769 | |
| 770 List docMembersJson(Map<Object,MemberMirror> memberMap) { | |
| 771 final members = []; | |
| 772 for (MemberMirror member in orderByName(memberMap.getValues())) { | |
| 773 if (member.isPrivate) continue; | |
| 774 | |
| 775 var memberInfo = {}; | |
| 776 if (member.isField) { | |
| 777 memberInfo[NAME] = member.simpleName; | |
| 778 memberInfo[KIND] = FIELD; | |
| 779 } else { | |
| 780 MethodMirror method = member; | |
| 781 if (method.isConstructor) { | |
| 782 if (method.constructorName != '') { | |
| 783 memberInfo[NAME] = '${method.simpleName}.${method.constructorName}'; | |
| 784 memberInfo[KIND] = CONSTRUCTOR; | |
| 785 } else { | |
| 786 memberInfo[NAME] = member.simpleName; | |
| 787 memberInfo[KIND] = CONSTRUCTOR; | |
| 788 } | |
| 789 } else if (method.isOperator) { | |
| 790 memberInfo[NAME] = '${method.simpleName} ${method.operatorName}'; | |
| 791 memberInfo[KIND] = METHOD; | |
| 792 } else if (method.isSetter) { | |
| 793 memberInfo[NAME] = member.simpleName; | |
| 794 memberInfo[KIND] = SETTER; | |
| 795 } else if (method.isGetter) { | |
| 796 memberInfo[NAME] = member.simpleName; | |
| 797 memberInfo[KIND] = GETTER; | |
| 798 } else { | |
| 799 memberInfo[NAME] = member.simpleName; | |
| 800 memberInfo[KIND] = METHOD; | |
| 801 } | |
| 802 } | |
| 803 var anchor = memberAnchor(member); | |
| 804 if (anchor != memberInfo[NAME]) { | |
| 805 memberInfo[LINK_NAME] = anchor; | |
| 806 } | |
| 807 members.add(memberInfo); | |
| 808 } | |
| 809 return members; | |
| 810 } | |
| 811 | |
| 812 void docNavigation() { | |
| 813 writeln( | |
| 814 ''' | |
| 815 <div class="nav"> | |
| 816 '''); | |
| 817 | |
| 818 if (mode == MODE_STATIC) { | |
| 819 for (final library in _sortedLibraries) { | |
| 820 write('<h2><div class="icon-library"></div>'); | |
| 821 | |
| 822 if ((_currentLibrary == library) && (_currentType == null)) { | |
| 823 write('<strong>${library.simpleName}</strong>'); | |
| 824 } else { | |
| 825 write('${a(libraryUrl(library), library.simpleName)}'); | |
| 826 } | |
| 827 write('</h2>'); | |
| 828 | |
| 829 // Only expand classes in navigation for current library. | |
| 830 if (_currentLibrary == library) docLibraryNavigation(library); | |
| 831 } | |
| 832 } | |
| 833 | |
| 834 writeln('</div>'); | |
| 835 } | |
| 836 | |
| 837 /** Writes the navigation for the types contained by the given library. */ | |
| 838 void docLibraryNavigation(LibraryMirror library) { | |
| 839 // Show the exception types separately. | |
| 840 final types = <InterfaceMirror>[]; | |
| 841 final exceptions = <InterfaceMirror>[]; | |
| 842 | |
| 843 for (InterfaceMirror type in orderByName(library.types.getValues())) { | |
| 844 if (type.isPrivate) continue; | |
| 845 | |
| 846 if (isException(type)) { | |
| 847 exceptions.add(type); | |
| 848 } else { | |
| 849 types.add(type); | |
| 850 } | |
| 851 } | |
| 852 | |
| 853 if ((types.length == 0) && (exceptions.length == 0)) return; | |
| 854 | |
| 855 writeln('<ul class="icon">'); | |
| 856 types.forEach(docTypeNavigation); | |
| 857 exceptions.forEach(docTypeNavigation); | |
| 858 writeln('</ul>'); | |
| 859 } | |
| 860 | |
| 861 /** Writes a linked navigation list item for the given type. */ | |
| 862 void docTypeNavigation(InterfaceMirror type) { | |
| 863 var icon = 'interface'; | |
| 864 if (type.simpleName.endsWith('Exception')) { | |
| 865 icon = 'exception'; | |
| 866 } else if (type.isClass) { | |
| 867 icon = 'class'; | |
| 868 } | |
| 869 | |
| 870 write('<li>'); | |
| 871 if (_currentType == type) { | |
| 872 write( | |
| 873 '<div class="icon-$icon"></div><strong>${typeName(type)}</strong>'); | |
| 874 } else { | |
| 875 write(a(typeUrl(type), | |
| 876 '<div class="icon-$icon"></div>${typeName(type)}')); | |
| 877 } | |
| 878 writeln('</li>'); | |
| 879 } | |
| 880 | |
| 881 void docLibrary(LibraryMirror library) { | |
| 882 if (verbose) { | |
| 883 print('Library \'${library.simpleName}\':'); | |
| 884 } | |
| 885 _totalLibraries++; | |
| 886 _currentLibrary = library; | |
| 887 _currentType = null; | |
| 888 | |
| 889 startFile(libraryUrl(library)); | |
| 890 writeHeader('${library.simpleName} Library', | |
| 891 [library.simpleName, libraryUrl(library)]); | |
| 892 writeln('<h2><strong>${library.simpleName}</strong> library</h2>'); | |
| 893 | |
| 894 // Look for a comment for the entire library. | |
| 895 final comment = getLibraryComment(library); | |
| 896 if (comment != null) { | |
| 897 writeln('<div class="doc">$comment</div>'); | |
| 898 } | |
| 899 | |
| 900 // Document the top-level members. | |
| 901 docMembers(library); | |
| 902 | |
| 903 // Document the types. | |
| 904 final classes = <InterfaceMirror>[]; | |
| 905 final interfaces = <InterfaceMirror>[]; | |
| 906 final typedefs = <TypedefMirror>[]; | |
| 907 final exceptions = <InterfaceMirror>[]; | |
| 908 | |
| 909 for (InterfaceMirror type in orderByName(library.types.getValues())) { | |
| 910 if (type.isPrivate) continue; | |
| 911 | |
| 912 if (isException(type)) { | |
| 913 exceptions.add(type); | |
| 914 } else if (type.isClass) { | |
| 915 classes.add(type); | |
| 916 } else if (type.isInterface){ | |
| 917 interfaces.add(type); | |
| 918 } else if (type is TypedefMirror) { | |
| 919 typedefs.add(type); | |
| 920 } else { | |
| 921 throw new InternalError("internal error: unknown type $type."); | |
| 922 } | |
| 923 } | |
| 924 | |
| 925 docTypes(classes, 'Classes'); | |
| 926 docTypes(interfaces, 'Interfaces'); | |
| 927 docTypes(typedefs, 'Typedefs'); | |
| 928 docTypes(exceptions, 'Exceptions'); | |
| 929 | |
| 930 writeFooter(); | |
| 931 endFile(); | |
| 932 | |
| 933 for (final type in library.types.getValues()) { | |
| 934 if (type.isPrivate) continue; | |
| 935 | |
| 936 docType(type); | |
| 937 } | |
| 938 } | |
| 939 | |
| 940 void docTypes(List types, String header) { | |
| 941 if (types.length == 0) return; | |
| 942 | |
| 943 writeln('<h3>$header</h3>'); | |
| 944 | |
| 945 for (final type in types) { | |
| 946 writeln( | |
| 947 ''' | |
| 948 <div class="type"> | |
| 949 <h4> | |
| 950 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} | |
| 951 </h4> | |
| 952 </div> | |
| 953 '''); | |
| 954 } | |
| 955 } | |
| 956 | |
| 957 void docType(InterfaceMirror type) { | |
| 958 if (verbose) { | |
| 959 print('- ${type.simpleName}'); | |
| 960 } | |
| 961 _totalTypes++; | |
| 962 _currentType = type; | |
| 963 | |
| 964 startFile(typeUrl(type)); | |
| 965 | |
| 966 var kind = 'Interface'; | |
| 967 if (type.isTypedef) { | |
| 968 kind = 'Typedef'; | |
| 969 } else if (type.isClass) { | |
| 970 kind = 'Class'; | |
| 971 } | |
| 972 | |
| 973 final typeTitle = | |
| 974 '${typeName(type)} ${kind}'; | |
| 975 writeHeader('$typeTitle / ${type.library.simpleName} Library', | |
| 976 [type.library.simpleName, libraryUrl(type.library), | |
| 977 typeName(type), typeUrl(type)]); | |
| 978 writeln( | |
| 979 ''' | |
| 980 <h2><strong>${typeName(type, showBounds: true)}</strong> | |
| 981 $kind | |
| 982 </h2> | |
| 983 '''); | |
| 984 | |
| 985 docCode(type.location, getTypeComment(type)); | |
| 986 docInheritance(type); | |
| 987 docTypedef(type); | |
| 988 docConstructors(type); | |
| 989 docMembers(type); | |
| 990 | |
| 991 writeTypeFooter(); | |
| 992 writeFooter(); | |
| 993 endFile(); | |
| 994 } | |
| 995 | |
| 996 /** Override this to write additional content at the end of a type's page. */ | |
| 997 void writeTypeFooter() { | |
| 998 // Do nothing. | |
| 999 } | |
| 1000 | |
| 1001 /** | |
| 1002 * Writes an inline type span for the given type. This is a little box with | |
| 1003 * an icon and the type's name. It's similar to how types appear in the | |
| 1004 * navigation, but is suitable for inline (as opposed to in a `<ul>`) use. | |
| 1005 */ | |
| 1006 void typeSpan(InterfaceMirror type) { | |
| 1007 var icon = 'interface'; | |
| 1008 if (type.simpleName.endsWith('Exception')) { | |
| 1009 icon = 'exception'; | |
| 1010 } else if (type.isClass) { | |
| 1011 icon = 'class'; | |
| 1012 } | |
| 1013 | |
| 1014 write('<span class="type-box"><span class="icon-$icon"></span>'); | |
| 1015 if (_currentType == type) { | |
| 1016 write('<strong>${typeName(type)}</strong>'); | |
| 1017 } else { | |
| 1018 write(a(typeUrl(type), typeName(type))); | |
| 1019 } | |
| 1020 write('</span>'); | |
| 1021 } | |
| 1022 | |
| 1023 /** | |
| 1024 * Document the other types that touch [Type] in the inheritance hierarchy: | |
| 1025 * subclasses, superclasses, subinterfaces, superinferfaces, and default | |
| 1026 * class. | |
| 1027 */ | |
| 1028 void docInheritance(InterfaceMirror type) { | |
| 1029 // Don't show the inheritance details for Object. It doesn't have any base | |
| 1030 // class (obviously) and it has too many subclasses to be useful. | |
| 1031 if (type.isObject) return; | |
| 1032 | |
| 1033 // Writes an unordered list of references to types with an optional header. | |
| 1034 listTypes(types, header) { | |
| 1035 if (types == null) return; | |
| 1036 | |
| 1037 // Skip private types. | |
| 1038 final publicTypes = new List.from(types.filter((t) => !t.isPrivate)); | |
| 1039 if (publicTypes.length == 0) return; | |
| 1040 | |
| 1041 writeln('<h3>$header</h3>'); | |
| 1042 writeln('<p>'); | |
| 1043 bool first = true; | |
| 1044 for (final t in publicTypes) { | |
| 1045 if (!first) write(', '); | |
| 1046 typeSpan(t); | |
| 1047 first = false; | |
| 1048 } | |
| 1049 writeln('</p>'); | |
| 1050 } | |
| 1051 | |
| 1052 final subtypes = []; | |
| 1053 for (final subtype in computeSubdeclarations(type)) { | |
| 1054 subtypes.add(subtype); | |
| 1055 } | |
| 1056 subtypes.sort((x, y) => x.simpleName.compareTo(y.simpleName)); | |
| 1057 if (type.isClass) { | |
| 1058 // Show the chain of superclasses. | |
| 1059 if (!type.superclass.isObject) { | |
| 1060 final supertypes = []; | |
| 1061 var thisType = type.superclass; | |
| 1062 // As a sanity check, only show up to five levels of nesting, otherwise | |
| 1063 // the box starts to get hideous. | |
| 1064 do { | |
| 1065 supertypes.add(thisType); | |
| 1066 thisType = thisType.superclass; | |
| 1067 } while (!thisType.isObject); | |
| 1068 | |
| 1069 writeln('<h3>Extends</h3>'); | |
| 1070 writeln('<p>'); | |
| 1071 for (var i = supertypes.length - 1; i >= 0; i--) { | |
| 1072 typeSpan(supertypes[i]); | |
| 1073 write(' > '); | |
| 1074 } | |
| 1075 | |
| 1076 // Write this class. | |
| 1077 typeSpan(type); | |
| 1078 writeln('</p>'); | |
| 1079 } | |
| 1080 | |
| 1081 listTypes(subtypes, 'Subclasses'); | |
| 1082 listTypes(type.interfaces.getValues(), 'Implements'); | |
| 1083 } else { | |
| 1084 // Show the default class. | |
| 1085 if (type.defaultType != null) { | |
| 1086 listTypes([type.defaultType], 'Default class'); | |
| 1087 } | |
| 1088 | |
| 1089 // List extended interfaces. | |
| 1090 listTypes(type.interfaces.getValues(), 'Extends'); | |
| 1091 | |
| 1092 // List subinterfaces and implementing classes. | |
| 1093 final subinterfaces = []; | |
| 1094 final implementing = []; | |
| 1095 | |
| 1096 for (final subtype in subtypes) { | |
| 1097 if (subtype.isClass) { | |
| 1098 implementing.add(subtype); | |
| 1099 } else { | |
| 1100 subinterfaces.add(subtype); | |
| 1101 } | |
| 1102 } | |
| 1103 | |
| 1104 listTypes(subinterfaces, 'Subinterfaces'); | |
| 1105 listTypes(implementing, 'Implemented by'); | |
| 1106 } | |
| 1107 } | |
| 1108 | |
| 1109 /** | |
| 1110 * Documents the definition of [type] if it is a typedef. | |
| 1111 */ | |
| 1112 void docTypedef(TypeMirror type) { | |
| 1113 if (type is! TypedefMirror) { | |
| 1114 return; | |
| 1115 } | |
| 1116 writeln('<div class="method"><h4 id="${type.simpleName}">'); | |
| 1117 | |
| 1118 if (includeSource) { | |
| 1119 writeln('<span class="show-code">Code</span>'); | |
| 1120 } | |
| 1121 | |
| 1122 write('typedef '); | |
| 1123 annotateType(type, type.definition, type.simpleName); | |
| 1124 | |
| 1125 write(''' <a class="anchor-link" href="#${type.simpleName}" | |
| 1126 title="Permalink to ${type.simpleName}">#</a>'''); | |
| 1127 writeln('</h4>'); | |
| 1128 | |
| 1129 docCode(type.location, null, showCode: true); | |
| 1130 | |
| 1131 writeln('</div>'); | |
| 1132 } | |
| 1133 | |
| 1134 /** Document the constructors for [Type], if any. */ | |
| 1135 void docConstructors(InterfaceMirror type) { | |
| 1136 final constructors = <MethodMirror>[]; | |
| 1137 for (var constructor in type.constructors.getValues()) { | |
| 1138 if (!constructor.isPrivate) { | |
| 1139 constructors.add(constructor); | |
| 1140 } | |
| 1141 } | |
| 1142 | |
| 1143 if (constructors.length > 0) { | |
| 1144 writeln('<h3>Constructors</h3>'); | |
| 1145 constructors.sort((x, y) => x.simpleName.toUpperCase().compareTo( | |
| 1146 y.simpleName.toUpperCase())); | |
| 1147 | |
| 1148 for (final constructor in constructors) { | |
| 1149 docMethod(type, constructor); | |
| 1150 } | |
| 1151 } | |
| 1152 } | |
| 1153 | |
| 1154 void docMembers(ObjectMirror host) { | |
| 1155 // Collect the different kinds of members. | |
| 1156 final staticMethods = []; | |
| 1157 final staticFields = []; | |
| 1158 final instanceMethods = []; | |
| 1159 final instanceFields = []; | |
| 1160 | |
| 1161 for (MemberMirror member in orderByName(host.declaredMembers.getValues())) { | |
| 1162 if (member.isPrivate) continue; | |
| 1163 | |
| 1164 final methods = member.isStatic ? staticMethods : instanceMethods; | |
| 1165 final fields = member.isStatic ? staticFields : instanceFields; | |
| 1166 | |
| 1167 if (member.isMethod) { | |
| 1168 methods.add(member); | |
| 1169 } else if (member.isField) { | |
| 1170 fields.add(member); | |
| 1171 } | |
| 1172 } | |
| 1173 | |
| 1174 if (staticMethods.length > 0) { | |
| 1175 final title = host is LibraryMirror ? 'Functions' : 'Static Methods'; | |
| 1176 writeln('<h3>$title</h3>'); | |
| 1177 for (final method in orderByName(staticMethods)) { | |
| 1178 docMethod(host, method); | |
| 1179 } | |
| 1180 } | |
| 1181 | |
| 1182 if (staticFields.length > 0) { | |
| 1183 final title = host is LibraryMirror ? 'Variables' : 'Static Fields'; | |
| 1184 writeln('<h3>$title</h3>'); | |
| 1185 for (final field in orderByName(staticFields)) { | |
| 1186 docField(host, field); | |
| 1187 } | |
| 1188 } | |
| 1189 | |
| 1190 if (instanceMethods.length > 0) { | |
| 1191 writeln('<h3>Methods</h3>'); | |
| 1192 for (final method in orderByName(instanceMethods)) { | |
| 1193 docMethod(host, method); | |
| 1194 } | |
| 1195 } | |
| 1196 | |
| 1197 if (instanceFields.length > 0) { | |
| 1198 writeln('<h3>Fields</h3>'); | |
| 1199 for (final field in orderByName(instanceFields)) { | |
| 1200 docField(host, field); | |
| 1201 } | |
| 1202 } | |
| 1203 } | |
| 1204 | |
| 1205 /** | |
| 1206 * Documents the [method] in type [type]. Handles all kinds of methods | |
| 1207 * including getters, setters, and constructors. | |
| 1208 */ | |
| 1209 void docMethod(ObjectMirror host, MethodMirror method) { | |
| 1210 _totalMembers++; | |
| 1211 _currentMember = method; | |
| 1212 | |
| 1213 writeln('<div class="method"><h4 id="${memberAnchor(method)}">'); | |
| 1214 | |
| 1215 if (includeSource) { | |
| 1216 writeln('<span class="show-code">Code</span>'); | |
| 1217 } | |
| 1218 | |
| 1219 if (method.isConstructor) { | |
| 1220 if (method.isFactory) { | |
| 1221 write('factory '); | |
| 1222 } else { | |
| 1223 write(method.isConst ? 'const ' : 'new '); | |
| 1224 } | |
| 1225 } | |
| 1226 | |
| 1227 if (method.constructorName == null) { | |
| 1228 annotateType(host, method.returnType); | |
| 1229 } | |
| 1230 | |
| 1231 var name = method.simpleName; | |
| 1232 // Translate specially-named methods: getters, setters, operators. | |
| 1233 if (method.isGetter) { | |
| 1234 // Getter. | |
| 1235 name = 'get $name'; | |
| 1236 } else if (method.isSetter) { | |
| 1237 // Setter. | |
| 1238 name = 'set $name'; | |
| 1239 } else if (method.isOperator) { | |
| 1240 name = 'operator ${method.operatorName}'; | |
| 1241 } | |
| 1242 | |
| 1243 write('<strong>$name</strong>'); | |
| 1244 | |
| 1245 // Named constructors. | |
| 1246 if (method.constructorName != null && method.constructorName != '') { | |
| 1247 write('.'); | |
| 1248 write(method.constructorName); | |
| 1249 } | |
| 1250 | |
| 1251 docParamList(host, method.parameters); | |
| 1252 | |
| 1253 var prefix = host is LibraryMirror ? '' : '${typeName(host)}.'; | |
| 1254 write(''' <a class="anchor-link" href="#${memberAnchor(method)}" | |
| 1255 title="Permalink to $prefix$name">#</a>'''); | |
| 1256 writeln('</h4>'); | |
| 1257 | |
| 1258 docCode(method.location, getMethodComment(method), showCode: true); | |
| 1259 | |
| 1260 writeln('</div>'); | |
| 1261 } | |
| 1262 | |
| 1263 /** Documents the field [field] of type [type]. */ | |
| 1264 void docField(ObjectMirror host, FieldMirror field) { | |
| 1265 _totalMembers++; | |
| 1266 _currentMember = field; | |
| 1267 | |
| 1268 writeln('<div class="field"><h4 id="${memberAnchor(field)}">'); | |
| 1269 | |
| 1270 if (includeSource) { | |
| 1271 writeln('<span class="show-code">Code</span>'); | |
| 1272 } | |
| 1273 | |
| 1274 if (field.isFinal) { | |
| 1275 write('final '); | |
| 1276 } else if (field.type.isDynamic) { | |
| 1277 write('var '); | |
| 1278 } | |
| 1279 | |
| 1280 annotateType(host, field.type); | |
| 1281 var prefix = host is LibraryMirror ? '' : '${typeName(host)}.'; | |
| 1282 write( | |
| 1283 ''' | |
| 1284 <strong>${field.simpleName}</strong> <a class="anchor-link" | |
| 1285 href="#${memberAnchor(field)}" | |
| 1286 title="Permalink to $prefix${field.simpleName}">#</a> | |
| 1287 </h4> | |
| 1288 '''); | |
| 1289 | |
| 1290 docCode(field.location, getFieldComment(field), showCode: true); | |
| 1291 writeln('</div>'); | |
| 1292 } | |
| 1293 | |
| 1294 void docParamList(ObjectMirror enclosingType, | |
| 1295 List<ParameterMirror> parameters) { | |
| 1296 write('('); | |
| 1297 bool first = true; | |
| 1298 bool inOptionals = false; | |
| 1299 for (final parameter in parameters) { | |
| 1300 if (!first) write(', '); | |
| 1301 | |
| 1302 if (!inOptionals && parameter.isOptional) { | |
| 1303 write('['); | |
| 1304 inOptionals = true; | |
| 1305 } | |
| 1306 | |
| 1307 annotateType(enclosingType, parameter.type, parameter.simpleName); | |
| 1308 | |
| 1309 // Show the default value for named optional parameters. | |
| 1310 if (parameter.isOptional && parameter.hasDefaultValue) { | |
| 1311 write(' = '); | |
| 1312 write(parameter.defaultValue); | |
| 1313 } | |
| 1314 | |
| 1315 first = false; | |
| 1316 } | |
| 1317 | |
| 1318 if (inOptionals) write(']'); | |
| 1319 write(')'); | |
| 1320 } | |
| 1321 | |
| 1322 /** | |
| 1323 * Documents the code contained within [span] with [comment]. If [showCode] | |
| 1324 * is `true` (and [includeSource] is set), also includes the source code. | |
| 1325 */ | |
| 1326 void docCode(Location location, String comment, [bool showCode = false]) { | |
| 1327 writeln('<div class="doc">'); | |
| 1328 if (comment != null) { | |
| 1329 writeln(comment); | |
| 1330 } | |
| 1331 | |
| 1332 if (includeSource && showCode) { | |
| 1333 writeln('<pre class="source">'); | |
| 1334 writeln(md.escapeHtml(unindentCode(location))); | |
| 1335 writeln('</pre>'); | |
| 1336 } | |
| 1337 | |
| 1338 writeln('</div>'); | |
| 1339 } | |
| 1340 | |
| 1341 | |
| 1342 /** Get the doc comment associated with the given library. */ | |
| 1343 String getLibraryComment(LibraryMirror library) { | |
| 1344 // Look for a comment for the entire library. | |
| 1345 final comment = _comments.findLibrary(library.location.source); | |
| 1346 if (comment != null) { | |
| 1347 return md.markdownToHtml(comment); | |
| 1348 } | |
| 1349 return null; | |
| 1350 } | |
| 1351 | |
| 1352 /** Get the doc comment associated with the given type. */ | |
| 1353 String getTypeComment(TypeMirror type) { | |
| 1354 String comment = _comments.find(type.location); | |
| 1355 if (comment == null) return null; | |
| 1356 return commentToHtml(comment); | |
| 1357 } | |
| 1358 | |
| 1359 /** Get the doc comment associated with the given method. */ | |
| 1360 String getMethodComment(MethodMirror method) { | |
| 1361 String comment = _comments.find(method.location); | |
| 1362 if (comment == null) return null; | |
| 1363 return commentToHtml(comment); | |
| 1364 } | |
| 1365 | |
| 1366 /** Get the doc comment associated with the given field. */ | |
| 1367 String getFieldComment(FieldMirror field) { | |
| 1368 String comment = _comments.find(field.location); | |
| 1369 if (comment == null) return null; | |
| 1370 return commentToHtml(comment); | |
| 1371 } | |
| 1372 | |
| 1373 String commentToHtml(String comment) => md.markdownToHtml(comment); | |
| 1374 | |
| 1375 /** | |
| 1376 * Converts [fullPath] which is understood to be a full path from the root of | |
| 1377 * the generated docs to one relative to the current file. | |
| 1378 */ | |
| 1379 String relativePath(String fullPath) { | |
| 1380 // Don't make it relative if it's an absolute path. | |
| 1381 if (isAbsolute(fullPath)) return fullPath; | |
| 1382 | |
| 1383 // TODO(rnystrom): Walks all the way up to root each time. Shouldn't do | |
| 1384 // this if the paths overlap. | |
| 1385 return '${repeat('../', | |
| 1386 countOccurrences(_filePath.toString(), '/'))}$fullPath'; | |
| 1387 } | |
| 1388 | |
| 1389 /** Gets whether or not the given URL is absolute or relative. */ | |
| 1390 bool isAbsolute(String url) { | |
| 1391 // TODO(rnystrom): Why don't we have a nice type in the platform for this? | |
| 1392 // TODO(rnystrom): This is a bit hackish. We consider any URL that lacks | |
| 1393 // a scheme to be relative. | |
| 1394 return const RegExp(@'^\w+:').hasMatch(url); | |
| 1395 } | |
| 1396 | |
| 1397 /** Gets the URL to the documentation for [library]. */ | |
| 1398 String libraryUrl(LibraryMirror library) { | |
| 1399 return '${sanitize(library.simpleName)}.html'; | |
| 1400 } | |
| 1401 | |
| 1402 /** Gets the URL for the documentation for [type]. */ | |
| 1403 String typeUrl(ObjectMirror type) { | |
| 1404 if (type is LibraryMirror) { | |
| 1405 return '${sanitize(type.simpleName)}.html'; | |
| 1406 } | |
| 1407 assert (type is TypeMirror); | |
| 1408 // Always get the generic type to strip off any type parameters or | |
| 1409 // arguments. If the type isn't generic, genericType returns `this`, so it | |
| 1410 // works for non-generic types too. | |
| 1411 return '${sanitize(type.library.simpleName)}/' | |
| 1412 '${type.declaration.simpleName}.html'; | |
| 1413 } | |
| 1414 | |
| 1415 /** Gets the URL for the documentation for [member]. */ | |
| 1416 String memberUrl(MemberMirror member) { | |
| 1417 String url = typeUrl(member.surroundingDeclaration); | |
| 1418 return '$url#${memberAnchor(member)}'; | |
| 1419 } | |
| 1420 | |
| 1421 /** Gets the anchor id for the document for [member]. */ | |
| 1422 String memberAnchor(MemberMirror member) { | |
| 1423 if (member.isField) { | |
| 1424 return member.simpleName; | |
| 1425 } | |
| 1426 MethodMirror method = member; | |
| 1427 if (method.isConstructor) { | |
| 1428 if (method.constructorName == '') { | |
| 1429 return method.simpleName; | |
| 1430 } else { | |
| 1431 return '${method.simpleName}.${method.constructorName}'; | |
| 1432 } | |
| 1433 } else if (method.isOperator) { | |
| 1434 return '${method.simpleName} ${method.operatorName}'; | |
| 1435 } else if (method.isSetter) { | |
| 1436 return '${method.simpleName}='; | |
| 1437 } else { | |
| 1438 return method.simpleName; | |
| 1439 } | |
| 1440 } | |
| 1441 | |
| 1442 /** | |
| 1443 * Creates a hyperlink. Handles turning the [href] into an appropriate | |
| 1444 * relative path from the current file. | |
| 1445 */ | |
| 1446 String a(String href, String contents, [String css]) { | |
| 1447 // Mark outgoing external links, mainly so we can style them. | |
| 1448 final rel = isAbsolute(href) ? ' ref="external"' : ''; | |
| 1449 final cssClass = css == null ? '' : ' class="$css"'; | |
| 1450 return '<a href="${relativePath(href)}"$cssClass$rel>$contents</a>'; | |
| 1451 } | |
| 1452 | |
| 1453 /** | |
| 1454 * Writes a type annotation for the given type and (optional) parameter name. | |
| 1455 */ | |
| 1456 annotateType(ObjectMirror enclosingType, | |
| 1457 TypeMirror type, | |
| 1458 [String paramName = null]) { | |
| 1459 // Don't bother explicitly displaying Dynamic. | |
| 1460 if (type.isDynamic) { | |
| 1461 if (paramName !== null) write(paramName); | |
| 1462 return; | |
| 1463 } | |
| 1464 | |
| 1465 // For parameters, handle non-typedefed function types. | |
| 1466 if (paramName !== null && type is FunctionTypeMirror) { | |
| 1467 annotateType(enclosingType, type.returnType); | |
| 1468 write(paramName); | |
| 1469 | |
| 1470 docParamList(enclosingType, type.parameters); | |
| 1471 return; | |
| 1472 } | |
| 1473 | |
| 1474 linkToType(enclosingType, type); | |
| 1475 | |
| 1476 write(' '); | |
| 1477 if (paramName !== null) write(paramName); | |
| 1478 } | |
| 1479 | |
| 1480 /** Writes a link to a human-friendly string representation for a type. */ | |
| 1481 linkToType(ObjectMirror enclosingType, TypeMirror type) { | |
| 1482 if (type.isVoid) { | |
| 1483 // Do not generate links for void. | |
| 1484 // TODO(johnniwinter): Generate span for specific style? | |
| 1485 write('void'); | |
| 1486 return; | |
| 1487 } | |
| 1488 if (type.isDynamic) { | |
| 1489 // Do not generate links for Dynamic. | |
| 1490 write('Dynamic'); | |
| 1491 return; | |
| 1492 } | |
| 1493 | |
| 1494 if (type.isTypeVariable) { | |
| 1495 // If we're using a type parameter within the body of a generic class then | |
| 1496 // just link back up to the class. | |
| 1497 write(a(typeUrl(enclosingType), type.simpleName)); | |
| 1498 return; | |
| 1499 } | |
| 1500 | |
| 1501 assert(type is InterfaceMirror); | |
| 1502 | |
| 1503 // Link to the type. | |
| 1504 if (shouldLinkToPublicApi(type.library)) { | |
| 1505 write('<a href="$API_LOCATION${typeUrl(type)}">${type.simpleName}</a>'); | |
| 1506 } else if (shouldIncludeLibrary(type.library)) { | |
| 1507 write(a(typeUrl(type), type.simpleName)); | |
| 1508 } else { | |
| 1509 write(type.simpleName); | |
| 1510 } | |
| 1511 | |
| 1512 if (type.isDeclaration) { | |
| 1513 // Avoid calling [:typeArguments():] on a declaration. | |
| 1514 return; | |
| 1515 } | |
| 1516 | |
| 1517 // See if it's an instantiation of a generic type. | |
| 1518 final typeArgs = type.typeArguments; | |
| 1519 if (typeArgs.length > 0) { | |
| 1520 write('<'); | |
| 1521 bool first = true; | |
| 1522 for (final arg in typeArgs) { | |
| 1523 if (!first) write(', '); | |
| 1524 first = false; | |
| 1525 linkToType(enclosingType, arg); | |
| 1526 } | |
| 1527 write('>'); | |
| 1528 } | |
| 1529 } | |
| 1530 | |
| 1531 /** Creates a linked cross reference to [type]. */ | |
| 1532 typeReference(InterfaceMirror type) { | |
| 1533 // TODO(rnystrom): Do we need to handle ParameterTypes here like | |
| 1534 // annotation() does? | |
| 1535 return a(typeUrl(type), typeName(type), css: 'crossref'); | |
| 1536 } | |
| 1537 | |
| 1538 /** Generates a human-friendly string representation for a type. */ | |
| 1539 typeName(TypeMirror type, [bool showBounds = false]) { | |
| 1540 if (type.isVoid) { | |
| 1541 return 'void'; | |
| 1542 } | |
| 1543 if (type is TypeVariableMirror) { | |
| 1544 return type.simpleName; | |
| 1545 } | |
| 1546 assert(type is InterfaceMirror); | |
| 1547 | |
| 1548 // See if it's a generic type. | |
| 1549 if (type.isDeclaration) { | |
| 1550 final typeParams = []; | |
| 1551 for (final typeParam in type.declaration.typeVariables) { | |
| 1552 if (showBounds && | |
| 1553 (typeParam.bound != null) && | |
| 1554 !typeParam.bound.isObject) { | |
| 1555 final bound = typeName(typeParam.bound, showBounds: true); | |
| 1556 typeParams.add('${typeParam.simpleName} extends $bound'); | |
| 1557 } else { | |
| 1558 typeParams.add(typeParam.simpleName); | |
| 1559 } | |
| 1560 } | |
| 1561 if (typeParams.isEmpty()) { | |
| 1562 return type.simpleName; | |
| 1563 } | |
| 1564 final params = Strings.join(typeParams, ', '); | |
| 1565 return '${type.simpleName}<$params>'; | |
| 1566 } | |
| 1567 | |
| 1568 // See if it's an instantiation of a generic type. | |
| 1569 final typeArgs = type.typeArguments; | |
| 1570 if (typeArgs.length > 0) { | |
| 1571 final args = Strings.join(typeArgs.map((arg) => typeName(arg)), ', '); | |
| 1572 return '${type.declaration.simpleName}<$args>'; | |
| 1573 } | |
| 1574 | |
| 1575 // Regular type. | |
| 1576 return type.simpleName; | |
| 1577 } | |
| 1578 | |
| 1579 /** | |
| 1580 * Remove leading indentation to line up with first line. | |
| 1581 */ | |
| 1582 unindentCode(Location span) { | |
| 1583 final column = getLocationColumn(span); | |
| 1584 final lines = span.text.split('\n'); | |
| 1585 // TODO(rnystrom): Dirty hack. | |
| 1586 for (var i = 1; i < lines.length; i++) { | |
| 1587 lines[i] = unindent(lines[i], column); | |
| 1588 } | |
| 1589 | |
| 1590 final code = Strings.join(lines, '\n'); | |
| 1591 return code; | |
| 1592 } | |
| 1593 | |
| 1594 /** | |
| 1595 * Takes a string of Dart code and turns it into sanitized HTML. | |
| 1596 */ | |
| 1597 formatCode(Location span) { | |
| 1598 final code = unindentCode(span); | |
| 1599 | |
| 1600 // Syntax highlight. | |
| 1601 return classifySource(code); | |
| 1602 } | |
| 1603 | |
| 1604 /** | |
| 1605 * This will be called whenever a doc comment hits a `[name]` in square | |
| 1606 * brackets. It will try to figure out what the name refers to and link or | |
| 1607 * style it appropriately. | |
| 1608 */ | |
| 1609 md.Node resolveNameReference(String name, | |
| 1610 [MemberMirror currentMember = null, | |
| 1611 ObjectMirror currentType = null, | |
| 1612 LibraryMirror currentLibrary = null]) { | |
| 1613 makeLink(String href) { | |
| 1614 final anchor = new md.Element.text('a', name); | |
| 1615 anchor.attributes['href'] = relativePath(href); | |
| 1616 anchor.attributes['class'] = 'crossref'; | |
| 1617 return anchor; | |
| 1618 } | |
| 1619 | |
| 1620 // See if it's a parameter of the current method. | |
| 1621 if (currentMember is MethodMirror) { | |
| 1622 for (final parameter in currentMember.parameters) { | |
| 1623 if (parameter.simpleName == name) { | |
| 1624 final element = new md.Element.text('span', name); | |
| 1625 element.attributes['class'] = 'param'; | |
| 1626 return element; | |
| 1627 } | |
| 1628 } | |
| 1629 } | |
| 1630 | |
| 1631 // See if it's another member of the current type. | |
| 1632 if (currentType != null) { | |
| 1633 final foundMember = findMirror(currentType.declaredMembers, name); | |
| 1634 if (foundMember != null) { | |
| 1635 return makeLink(memberUrl(foundMember)); | |
| 1636 } | |
| 1637 } | |
| 1638 | |
| 1639 // See if it's another type or a member of another type in the current | |
| 1640 // library. | |
| 1641 if (currentLibrary != null) { | |
| 1642 // See if it's a constructor | |
| 1643 final constructorLink = (() { | |
| 1644 final match = | |
| 1645 new RegExp(@'new ([\w$]+)(?:\.([\w$]+))?').firstMatch(name); | |
| 1646 if (match == null) return; | |
| 1647 InterfaceMirror foundtype = findMirror(currentLibrary.types, match[1]); | |
| 1648 if (foundtype == null) return; | |
| 1649 final constructor = | |
| 1650 findMirror(foundtype.constructors, | |
| 1651 match[2] == null ? '' : match[2]); | |
| 1652 if (constructor == null) return; | |
| 1653 return makeLink(memberUrl(constructor)); | |
| 1654 })(); | |
| 1655 if (constructorLink != null) return constructorLink; | |
| 1656 | |
| 1657 // See if it's a member of another type | |
| 1658 final foreignMemberLink = (() { | |
| 1659 final match = new RegExp(@'([\w$]+)\.([\w$]+)').firstMatch(name); | |
| 1660 if (match == null) return; | |
| 1661 InterfaceMirror foundtype = findMirror(currentLibrary.types, match[1]); | |
| 1662 if (foundtype == null) return; | |
| 1663 MemberMirror foundMember = findMirror(foundtype.declaredMembers, match[2
]); | |
| 1664 if (foundMember == null) return; | |
| 1665 return makeLink(memberUrl(foundMember)); | |
| 1666 })(); | |
| 1667 if (foreignMemberLink != null) return foreignMemberLink; | |
| 1668 | |
| 1669 InterfaceMirror foundType = findMirror(currentLibrary.types, name); | |
| 1670 if (foundType != null) { | |
| 1671 return makeLink(typeUrl(foundType)); | |
| 1672 } | |
| 1673 | |
| 1674 // See if it's a top-level member in the current library. | |
| 1675 MemberMirror foundMember = findMirror(currentLibrary.declaredMembers, name
); | |
| 1676 if (foundMember != null) { | |
| 1677 return makeLink(memberUrl(foundMember)); | |
| 1678 } | |
| 1679 } | |
| 1680 | |
| 1681 // TODO(rnystrom): Should also consider: | |
| 1682 // * Names imported by libraries this library imports. | |
| 1683 // * Type parameters of the enclosing type. | |
| 1684 | |
| 1685 return new md.Element.text('code', name); | |
| 1686 } | |
| 1687 | |
| 1688 generateAppCacheManifest() { | |
| 1689 if (verbose) { | |
| 1690 print('Generating app cache manifest from output $outputDir'); | |
| 1691 } | |
| 1692 startFile('appcache.manifest'); | |
| 1693 write("CACHE MANIFEST\n\n"); | |
| 1694 write("# VERSION: ${new Date.now()}\n\n"); | |
| 1695 write("NETWORK:\n*\n\n"); | |
| 1696 write("CACHE:\n"); | |
| 1697 var toCache = new Directory.fromPath(outputDir); | |
| 1698 var toCacheLister = toCache.list(recursive: true); | |
| 1699 toCacheLister.onFile = (filename) { | |
| 1700 if (filename.endsWith('appcache.manifest')) { | |
| 1701 return; | |
| 1702 } | |
| 1703 // TODO(johnniwinther): If [outputDir] has trailing slashes, [filename] | |
| 1704 // contains double (back)slashes for files in the immediate [toCache] | |
| 1705 // directory. These are not handled by [relativeTo] thus | |
| 1706 // wrongfully producing the path `/foo.html` for a file `foo.html` in | |
| 1707 // [toCache]. | |
| 1708 // | |
| 1709 // This can be handled in two ways. 1) By ensuring that | |
| 1710 // [Directory.fromPath] does not receive a path with a trailing slash, or | |
| 1711 // better, by making [Directory.fromPath] handle such trailing slashes. | |
| 1712 // 2) By ensuring that [filePath] does not have double slashes before | |
| 1713 // calling [relativeTo], or better, by making [relativeTo] handle double | |
| 1714 // slashes correctly. | |
| 1715 Path filePath = new Path.fromNative(filename).canonicalize(); | |
| 1716 Path relativeFilePath = filePath.relativeTo(outputDir); | |
| 1717 write("$relativeFilePath\n"); | |
| 1718 }; | |
| 1719 toCacheLister.onDone = (done) => endFile(); | |
| 1720 } | |
| 1721 | |
| 1722 /** | |
| 1723 * Returns [:true:] if [type] should be regarded as an exception. | |
| 1724 */ | |
| 1725 bool isException(TypeMirror type) { | |
| 1726 return type.simpleName.endsWith('Exception'); | |
| 1727 } | |
| 1728 } | |
| 1729 | |
| 1730 /** | |
| 1731 * Used to report an unexpected error in the DartDoc tool or the | |
| 1732 * underlying data | |
| 1733 */ | |
| 1734 class InternalError { | |
| 1735 final String message; | |
| 1736 const InternalError(this.message); | |
| 1737 String toString() => "InternalError: '$message'"; | |
| 1738 } | |
| OLD | NEW |