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 |