Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * To use it, from this directory, run: | 6 * To use it, from this directory, run: |
| 7 * | 7 * |
| 8 * $ dartdoc <path to .dart file> | 8 * $ dartdoc <path to .dart file> |
| 9 * | 9 * |
| 10 * This will create a "docs" directory with the docs for your libraries. To | 10 * This will create a "docs" directory with the docs for your libraries. To |
| 11 * create these beautiful docs, dartdoc parses your library and every library | 11 * create these beautiful docs, dartdoc parses your library and every library |
| 12 * it imports (recursively). From each library, it parses all classes and | 12 * it imports (recursively). From each library, it parses all classes and |
| 13 * members, finds the associated doc comments and builds crosslinked docs from | 13 * members, finds the associated doc comments and builds crosslinked docs from |
| 14 * them. | 14 * them. |
| 15 */ | 15 */ |
| 16 #library('dartdoc'); | 16 #library('dartdoc'); |
| 17 | 17 |
| 18 #import('../../frog/lang.dart'); | 18 #import('../../frog/lang.dart'); |
| 19 #import('../../frog/file_system.dart'); | 19 #import('../../frog/file_system.dart'); |
| 20 #import('../../frog/file_system_node.dart'); | 20 #import('../../frog/file_system_node.dart'); |
| 21 #import('../markdown/lib.dart', prefix: 'md'); | 21 #import('../markdown/lib.dart', prefix: 'md'); |
| 22 | 22 |
| 23 #source('classify.dart'); | 23 #source('classify.dart'); |
| 24 #source('comment_map.dart'); | |
| 24 #source('files.dart'); | 25 #source('files.dart'); |
| 25 #source('utils.dart'); | 26 #source('utils.dart'); |
| 26 | 27 |
| 27 /** Path to corePath library. */ | 28 /** Path to corePath library. */ |
| 28 final corePath = 'lib'; | 29 final corePath = 'lib'; |
| 29 | 30 |
| 30 /** Path to generate html files into. */ | 31 /** Path to generate html files into. */ |
| 31 final outdir = 'docs'; | 32 final outdir = 'docs'; |
| 32 | 33 |
| 33 /** Set to `false` to not include the source code in the generated docs. */ | |
| 34 bool includeSource = true; | |
| 35 | |
| 36 FileSystem files; | 34 FileSystem files; |
| 37 | 35 |
| 38 /** Special comment position used to store the library-level doc comment. */ | |
| 39 final _libraryDoc = -1; | |
| 40 | |
| 41 /** The library that we're currently generating docs for. */ | |
| 42 Library _currentLibrary; | |
| 43 | |
| 44 /** The type that we're currently generating docs for. */ | |
| 45 Type _currentType; | |
| 46 | |
| 47 /** The member that we're currently generating docs for. */ | |
| 48 Member _currentMember; | |
| 49 | |
| 50 /** | |
| 51 * The cached lookup-table to associate doc comments with spans. The outer map | |
| 52 * is from filenames to doc comments in that file. The inner map maps from the | |
| 53 * token positions to doc comments. Each position is the starting offset of the | |
| 54 * next non-comment token *following* the doc comment. For example, the position | |
| 55 * for this comment would be the position of the "Map" token below. | |
| 56 */ | |
| 57 Map<String, Map<int, String>> _comments; | |
| 58 | |
| 59 /** A callback that returns additional Markdown documentation for a type. */ | |
| 60 typedef String TypeDocumenter(Type type); | |
| 61 | |
| 62 /** A list of callbacks registered for documenting types. */ | |
| 63 List<TypeDocumenter> _typeDocumenters; | |
| 64 | |
| 65 /** A callback that returns additional Markdown documentation for a method. */ | |
| 66 typedef String MethodDocumenter(MethodMember method); | |
| 67 | |
| 68 /** A list of callbacks registered for documenting methods. */ | |
| 69 List<MethodDocumenter> _methodDocumenters; | |
| 70 | |
| 71 /** A callback that returns additional Markdown documentation for a field. */ | |
| 72 typedef String FieldDocumenter(FieldMember field); | |
| 73 | |
| 74 /** A list of callbacks registered for documenting fields. */ | |
| 75 List<FieldDocumenter> _fieldDocumenters; | |
| 76 | |
| 77 int _totalLibraries = 0; | |
| 78 int _totalTypes = 0; | |
| 79 int _totalMembers = 0; | |
| 80 | |
| 81 /** | 36 /** |
| 82 * Run this from the `utils/dartdoc` directory. | 37 * Run this from the `utils/dartdoc` directory. |
| 83 */ | 38 */ |
| 84 void main() { | 39 void main() { |
| 85 // The entrypoint of the library to generate docs for. | 40 // The entrypoint of the library to generate docs for. |
| 86 final entrypoint = process.argv[2]; | 41 final entrypoint = process.argv[process.argv.length - 1]; |
| 87 | 42 |
| 88 // Parse the dartdoc options. | 43 // Parse the dartdoc options. |
| 89 for (int i = 3; i < process.argv.length; i++) { | 44 bool includeSource = true; |
| 45 | |
| 46 for (int i = 2; i < process.argv.length - 1; i++) { | |
| 90 final arg = process.argv[i]; | 47 final arg = process.argv[i]; |
| 91 switch (arg) { | 48 switch (arg) { |
| 92 case '--no-code': | 49 case '--no-code': |
| 93 includeSource = false; | 50 includeSource = false; |
| 94 break; | 51 break; |
| 95 | 52 |
| 96 default: | 53 default: |
| 97 print('Unknown option: $arg'); | 54 print('Unknown option: $arg'); |
| 98 } | 55 } |
| 99 } | 56 } |
| 100 | 57 |
| 101 files = new NodeFileSystem(); | 58 files = new NodeFileSystem(); |
| 102 parseOptions('../../frog', [] /* args */, files); | 59 parseOptions('../../frog', [] /* args */, files); |
| 103 initializeWorld(files); | 60 initializeWorld(files); |
| 104 | 61 |
| 62 var dartdoc; | |
| 105 final elapsed = time(() { | 63 final elapsed = time(() { |
| 106 initializeDartDoc(); | 64 dartdoc = new Dartdoc(); |
| 107 document(entrypoint); | 65 dartdoc.includeSource = includeSource; |
| 66 dartdoc.document(entrypoint); | |
| 108 }); | 67 }); |
| 109 | 68 |
| 110 printStats(elapsed); | 69 print('Documented ${dartdoc._totalLibraries} libraries, ' + |
| 70 '${dartdoc._totalTypes} types, and ' + | |
| 71 '${dartdoc._totalMembers} members in ${elapsed}msec.'); | |
| 111 } | 72 } |
| 112 | 73 |
| 113 void initializeDartDoc() { | 74 class Dartdoc { |
| 114 _comments = <Map<int, String>>{}; | 75 /** Set to `false` to not include the source code in the generated docs. */ |
| 115 _typeDocumenters = <TypeDocumenter>[]; | 76 bool includeSource = true; |
| 116 _methodDocumenters = <MethodDocumenter>[]; | 77 |
| 117 _fieldDocumenters = <FieldDocumenter>[]; | 78 /** |
| 118 | 79 * The cached lookup-table to associate doc comments with spans. The outer map |
| 119 // Patch in support for [:...:]-style code to the markdown parser. | 80 * is from filenames to doc comments in that file. The inner map maps from the |
| 120 // TODO(rnystrom): Markdown already has syntax for this. Phase this out? | 81 * token positions to doc comments. Each position is the starting offset of |
| 121 md.InlineParser.syntaxes.insertRange(0, 1, | 82 * the next non-comment token *following* the doc comment. For example, the |
| 122 new md.CodeSyntax(@'\[\:((?:.|\n)*?)\:\]')); | 83 * position for this comment would be the position of the "Map" token below. |
| 123 | 84 */ |
| 124 md.setImplicitLinkResolver(resolveNameReference); | 85 CommentMap _comments; |
|
nweiz
2011/12/15 19:39:04
This long doc comment seems redundant with the Com
Bob Nystrom
2011/12/15 20:20:11
Done.
| |
| 125 } | 86 |
| 126 | 87 /** The library that we're currently generating docs for. */ |
| 127 document(String entrypoint) { | 88 Library _currentLibrary; |
| 128 try { | 89 |
| 129 var oldDietParse = options.dietParse; | 90 /** The type that we're currently generating docs for. */ |
| 130 options.dietParse = true; | 91 Type _currentType; |
| 131 | 92 |
| 132 // Handle the built-in entrypoints. | 93 /** The member that we're currently generating docs for. */ |
| 133 switch (entrypoint) { | 94 Member _currentMember; |
| 134 case 'corelib': | 95 |
| 135 world.getOrAddLibrary('dart:core'); | 96 int _totalLibraries = 0; |
| 136 world.getOrAddLibrary('dart:coreimpl'); | 97 int _totalTypes = 0; |
| 137 world.getOrAddLibrary('dart:json'); | 98 int _totalMembers = 0; |
| 138 world.process(); | 99 |
| 139 break; | 100 Dartdoc() |
| 140 | 101 : _comments = new CommentMap() { |
|
nweiz
2011/12/15 19:39:04
I thought initializer lists were supposed to be on
Bob Nystrom
2011/12/15 20:20:11
I don't think we specify one way or the other. I t
| |
| 141 case 'dom': | 102 // Patch in support for [:...:]-style code to the markdown parser. |
| 142 world.getOrAddLibrary('dart:core'); | 103 // TODO(rnystrom): Markdown already has syntax for this. Phase this out? |
| 143 world.getOrAddLibrary('dart:coreimpl'); | 104 md.InlineParser.syntaxes.insertRange(0, 1, |
| 144 world.getOrAddLibrary('dart:json'); | 105 new md.CodeSyntax(@'\[\:((?:.|\n)*?)\:\]')); |
|
nweiz
2011/12/15 19:39:04
Won't this add a new syntax entry to the markdown
Bob Nystrom
2011/12/15 20:20:11
Hmm, yes. It shouldn't cause any problems but it i
| |
| 145 world.getOrAddLibrary('dart:dom'); | 106 |
| 146 world.process(); | 107 md.setImplicitLinkResolver(resolveNameReference); |
|
nweiz
2011/12/15 19:39:04
This also seems potentially troublesome if someone
Bob Nystrom
2011/12/15 20:20:11
Yeah... :(
| |
| 147 break; | 108 } |
| 148 | 109 |
| 149 case 'html': | 110 document(String entrypoint) { |
| 150 world.getOrAddLibrary('dart:core'); | 111 try { |
| 151 world.getOrAddLibrary('dart:coreimpl'); | 112 var oldDietParse = options.dietParse; |
| 152 world.getOrAddLibrary('dart:json'); | 113 options.dietParse = true; |
| 153 world.getOrAddLibrary('dart:dom'); | 114 |
| 154 world.getOrAddLibrary('dart:html'); | 115 // Handle the built-in entrypoints. |
| 155 world.process(); | 116 switch (entrypoint) { |
| 156 break; | 117 case 'corelib': |
| 157 | 118 world.getOrAddLibrary('dart:core'); |
| 158 default: | 119 world.getOrAddLibrary('dart:coreimpl'); |
| 159 // Normal entrypoint script. | 120 world.getOrAddLibrary('dart:json'); |
| 160 world.processDartScript(entrypoint); | 121 world.process(); |
| 161 } | 122 break; |
| 162 | 123 |
| 163 world.resolveAll(); | 124 case 'dom': |
| 164 | 125 world.getOrAddLibrary('dart:core'); |
| 165 // Generate the docs. | 126 world.getOrAddLibrary('dart:coreimpl'); |
| 166 docIndex(); | 127 world.getOrAddLibrary('dart:json'); |
| 167 for (final library in world.libraries.getValues()) { | 128 world.getOrAddLibrary('dart:dom'); |
| 168 docLibrary(library); | 129 world.process(); |
| 169 } | 130 break; |
| 170 } finally { | 131 |
| 171 options.dietParse = oldDietParse; | 132 case 'html': |
| 172 } | 133 world.getOrAddLibrary('dart:core'); |
| 173 } | 134 world.getOrAddLibrary('dart:coreimpl'); |
| 174 | 135 world.getOrAddLibrary('dart:json'); |
| 175 printStats(num elapsed) { | 136 world.getOrAddLibrary('dart:dom'); |
| 176 print('Documented $_totalLibraries libraries, $_totalTypes types, and ' + | 137 world.getOrAddLibrary('dart:html'); |
| 177 '$_totalMembers members in ${elapsed}msec.'); | 138 world.process(); |
| 178 } | 139 break; |
| 179 | 140 |
| 180 writeHeader(String title) { | 141 default: |
| 181 writeln( | 142 // Normal entrypoint script. |
| 182 ''' | 143 world.processDartScript(entrypoint); |
| 183 <!DOCTYPE html> | 144 } |
| 184 <html> | 145 |
| 185 <head> | 146 world.resolveAll(); |
| 186 <meta charset="utf-8"> | 147 |
| 187 <title>$title</title> | 148 // Generate the docs. |
| 188 <link rel="stylesheet" type="text/css" | 149 docIndex(); |
| 189 href="${relativePath('styles.css')}" /> | 150 for (final library in world.libraries.getValues()) { |
| 190 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,8 00" rel="stylesheet" type="text/css"> | 151 docLibrary(library); |
| 191 <script src="${relativePath('interact.js')}"></script> | 152 } |
| 192 </head> | 153 } finally { |
| 193 <body> | 154 options.dietParse = oldDietParse; |
| 194 <div class="page"> | 155 } |
| 195 '''); | 156 } |
| 196 docNavigation(); | 157 |
| 197 writeln('<div class="content">'); | 158 writeHeader(String title) { |
| 198 } | |
| 199 | |
| 200 writeFooter() { | |
| 201 writeln( | |
| 202 ''' | |
| 203 </div> | |
| 204 <div class="footer"</div> | |
| 205 </body></html> | |
| 206 '''); | |
| 207 } | |
| 208 | |
| 209 docIndex() { | |
| 210 startFile('index.html'); | |
| 211 | |
| 212 writeHeader('Dart Documentation'); | |
| 213 | |
| 214 writeln('<h1>Dart Documentation</h1>'); | |
| 215 writeln('<h3>Libraries</h3>'); | |
| 216 | |
| 217 for (final library in orderByName(world.libraries)) { | |
| 218 writeln( | 159 writeln( |
| 219 ''' | 160 ''' |
| 220 <h4>${a(libraryUrl(library), library.name)}</h4> | 161 <!DOCTYPE html> |
| 162 <html> | |
| 163 <head> | |
| 164 <meta charset="utf-8"> | |
| 165 <title>$title</title> | |
| 166 <link rel="stylesheet" type="text/css" | |
| 167 href="${relativePath('styles.css')}" /> | |
| 168 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700 ,800" rel="stylesheet" type="text/css"> | |
| 169 <script src="${relativePath('interact.js')}"></script> | |
| 170 </head> | |
| 171 <body> | |
| 172 <div class="page"> | |
| 221 '''); | 173 '''); |
| 222 } | 174 docNavigation(); |
| 223 | 175 writeln('<div class="content">'); |
| 224 writeFooter(); | 176 } |
| 225 endFile(); | 177 |
| 226 } | 178 writeFooter() { |
| 227 | |
| 228 docNavigation() { | |
| 229 writeln( | |
| 230 ''' | |
| 231 <div class="nav"> | |
| 232 <h1>${a("index.html", "Dart Documentation")}</h1> | |
| 233 '''); | |
| 234 | |
| 235 for (final library in orderByName(world.libraries)) { | |
| 236 write('<h2><div class="icon-library"></div>'); | |
| 237 | |
| 238 if ((_currentLibrary == library) && (_currentType == null)) { | |
| 239 write('<strong>${library.name}</strong>'); | |
| 240 } else { | |
| 241 write('${a(libraryUrl(library), library.name)}'); | |
| 242 } | |
| 243 write('</h2>'); | |
| 244 | |
| 245 // Only expand classes in navigation for current library. | |
| 246 if (_currentLibrary == library) docLibraryNavigation(library); | |
| 247 } | |
| 248 | |
| 249 writeln('</div>'); | |
| 250 } | |
| 251 | |
| 252 /** Writes the navigation for the types contained by the given library. */ | |
| 253 docLibraryNavigation(Library library) { | |
| 254 // Show the exception types separately. | |
| 255 final types = <Type>[]; | |
| 256 final exceptions = <Type>[]; | |
| 257 | |
| 258 for (final type in orderByName(library.types)) { | |
| 259 if (type.isTop) continue; | |
| 260 if (type.name.startsWith('_')) continue; | |
| 261 | |
| 262 if (type.name.endsWith('Exception')) { | |
| 263 exceptions.add(type); | |
| 264 } else { | |
| 265 types.add(type); | |
| 266 } | |
| 267 } | |
| 268 | |
| 269 if ((types.length == 0) && (exceptions.length == 0)) return; | |
| 270 | |
| 271 writeType(String icon, Type type) { | |
| 272 write('<li>'); | |
| 273 if (_currentType == type) { | |
| 274 write( | |
| 275 '<div class="icon-$icon"></div><strong>${typeName(type)}</strong>'); | |
| 276 } else { | |
| 277 write(a(typeUrl(type), | |
| 278 '<div class="icon-$icon"></div>${typeName(type)}')); | |
| 279 } | |
| 280 writeln('</li>'); | |
| 281 } | |
| 282 | |
| 283 writeln('<ul>'); | |
| 284 types.forEach((type) => writeType(type.isClass ? 'class' : 'interface', | |
| 285 type)); | |
| 286 exceptions.forEach((type) => writeType('exception', type)); | |
| 287 writeln('</ul>'); | |
| 288 } | |
| 289 | |
| 290 String _runDocumenters(var item, List<Function> documenters) => | |
| 291 Strings.join(map(documenters, (doc) => doc(item)), '\n\n'); | |
| 292 | |
| 293 docLibrary(Library library) { | |
| 294 _totalLibraries++; | |
| 295 _currentLibrary = library; | |
| 296 _currentType = null; | |
| 297 | |
| 298 startFile(libraryUrl(library)); | |
| 299 writeHeader(library.name); | |
| 300 writeln('<h1>Library <strong>${library.name}</strong></h1>'); | |
| 301 | |
| 302 // Look for a comment for the entire library. | |
| 303 final comment = findCommentInFile(library.baseSource, _libraryDoc); | |
| 304 if (comment != null) { | |
| 305 final html = md.markdownToHtml(comment); | |
| 306 writeln('<div class="doc">$html</div>'); | |
| 307 } | |
| 308 | |
| 309 // Document the top-level members. | |
| 310 docMembers(library.topType); | |
| 311 | |
| 312 // Document the types. | |
| 313 final classes = <Type>[]; | |
| 314 final interfaces = <Type>[]; | |
| 315 final exceptions = <Type>[]; | |
| 316 | |
| 317 for (final type in orderByName(library.types)) { | |
| 318 if (type.isTop) continue; | |
| 319 if (type.name.startsWith('_')) continue; | |
| 320 | |
| 321 if (type.name.endsWith('Exception')) { | |
| 322 exceptions.add(type); | |
| 323 } else if (type.isClass) { | |
| 324 classes.add(type); | |
| 325 } else { | |
| 326 interfaces.add(type); | |
| 327 } | |
| 328 } | |
| 329 | |
| 330 docTypes(classes, 'Classes'); | |
| 331 docTypes(interfaces, 'Interfaces'); | |
| 332 docTypes(exceptions, 'Exceptions'); | |
| 333 | |
| 334 writeFooter(); | |
| 335 endFile(); | |
| 336 | |
| 337 for (final type in library.types.getValues()) { | |
| 338 if (!type.isTop) docType(type); | |
| 339 } | |
| 340 } | |
| 341 | |
| 342 docTypes(List<Type> types, String header) { | |
| 343 if (types.length == 0) return; | |
| 344 | |
| 345 writeln('<h3>$header</h3>'); | |
| 346 | |
| 347 for (final type in types) { | |
| 348 writeln( | 179 writeln( |
| 349 ''' | 180 ''' |
| 350 <div class="type"> | 181 </div> |
| 351 <h4> | 182 <div class="footer"</div> |
| 352 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} | 183 </body></html> |
| 184 '''); | |
| 185 } | |
| 186 | |
| 187 docIndex() { | |
| 188 startFile('index.html'); | |
| 189 | |
| 190 writeHeader('Dart Documentation'); | |
| 191 | |
| 192 writeln('<h1>Dart Documentation</h1>'); | |
| 193 writeln('<h3>Libraries</h3>'); | |
| 194 | |
| 195 for (final library in orderByName(world.libraries)) { | |
| 196 writeln( | |
| 197 ''' | |
| 198 <h4>${a(libraryUrl(library), library.name)}</h4> | |
| 199 '''); | |
| 200 } | |
| 201 | |
| 202 writeFooter(); | |
| 203 endFile(); | |
| 204 } | |
| 205 | |
| 206 docNavigation() { | |
| 207 writeln( | |
| 208 ''' | |
| 209 <div class="nav"> | |
| 210 <h1>${a("index.html", "Dart Documentation")}</h1> | |
| 211 '''); | |
| 212 | |
| 213 for (final library in orderByName(world.libraries)) { | |
| 214 write('<h2><div class="icon-library"></div>'); | |
| 215 | |
| 216 if ((_currentLibrary == library) && (_currentType == null)) { | |
| 217 write('<strong>${library.name}</strong>'); | |
| 218 } else { | |
| 219 write('${a(libraryUrl(library), library.name)}'); | |
| 220 } | |
| 221 write('</h2>'); | |
| 222 | |
| 223 // Only expand classes in navigation for current library. | |
| 224 if (_currentLibrary == library) docLibraryNavigation(library); | |
| 225 } | |
| 226 | |
| 227 writeln('</div>'); | |
| 228 } | |
| 229 | |
| 230 /** Writes the navigation for the types contained by the given library. */ | |
| 231 docLibraryNavigation(Library library) { | |
| 232 // Show the exception types separately. | |
| 233 final types = <Type>[]; | |
| 234 final exceptions = <Type>[]; | |
| 235 | |
| 236 for (final type in orderByName(library.types)) { | |
| 237 if (type.isTop) continue; | |
| 238 if (type.name.startsWith('_')) continue; | |
| 239 | |
| 240 if (type.name.endsWith('Exception')) { | |
| 241 exceptions.add(type); | |
| 242 } else { | |
| 243 types.add(type); | |
| 244 } | |
| 245 } | |
| 246 | |
| 247 if ((types.length == 0) && (exceptions.length == 0)) return; | |
| 248 | |
| 249 writeType(String icon, Type type) { | |
| 250 write('<li>'); | |
| 251 if (_currentType == type) { | |
| 252 write( | |
| 253 '<div class="icon-$icon"></div><strong>${typeName(type)}</strong>'); | |
| 254 } else { | |
| 255 write(a(typeUrl(type), | |
| 256 '<div class="icon-$icon"></div>${typeName(type)}')); | |
| 257 } | |
| 258 writeln('</li>'); | |
| 259 } | |
| 260 | |
| 261 writeln('<ul>'); | |
| 262 types.forEach((type) => writeType(type.isClass ? 'class' : 'interface', | |
| 263 type)); | |
| 264 exceptions.forEach((type) => writeType('exception', type)); | |
| 265 writeln('</ul>'); | |
| 266 } | |
| 267 | |
| 268 docLibrary(Library library) { | |
| 269 _totalLibraries++; | |
| 270 _currentLibrary = library; | |
| 271 _currentType = null; | |
| 272 | |
| 273 startFile(libraryUrl(library)); | |
| 274 writeHeader(library.name); | |
| 275 writeln('<h1>Library <strong>${library.name}</strong></h1>'); | |
| 276 | |
| 277 // Look for a comment for the entire library. | |
| 278 final comment = _comments.findLibrary(library.baseSource); | |
| 279 if (comment != null) { | |
| 280 final html = md.markdownToHtml(comment); | |
| 281 writeln('<div class="doc">$html</div>'); | |
| 282 } | |
| 283 | |
| 284 // Document the top-level members. | |
| 285 docMembers(library.topType); | |
| 286 | |
| 287 // Document the types. | |
| 288 final classes = <Type>[]; | |
| 289 final interfaces = <Type>[]; | |
| 290 final exceptions = <Type>[]; | |
| 291 | |
| 292 for (final type in orderByName(library.types)) { | |
| 293 if (type.isTop) continue; | |
| 294 if (type.name.startsWith('_')) continue; | |
| 295 | |
| 296 if (type.name.endsWith('Exception')) { | |
| 297 exceptions.add(type); | |
| 298 } else if (type.isClass) { | |
| 299 classes.add(type); | |
| 300 } else { | |
| 301 interfaces.add(type); | |
| 302 } | |
| 303 } | |
| 304 | |
| 305 docTypes(classes, 'Classes'); | |
| 306 docTypes(interfaces, 'Interfaces'); | |
| 307 docTypes(exceptions, 'Exceptions'); | |
| 308 | |
| 309 writeFooter(); | |
| 310 endFile(); | |
| 311 | |
| 312 for (final type in library.types.getValues()) { | |
| 313 if (!type.isTop) docType(type); | |
| 314 } | |
| 315 } | |
| 316 | |
| 317 docTypes(List<Type> types, String header) { | |
| 318 if (types.length == 0) return; | |
| 319 | |
| 320 writeln('<h3>$header</h3>'); | |
| 321 | |
| 322 for (final type in types) { | |
| 323 writeln( | |
| 324 ''' | |
| 325 <div class="type"> | |
| 326 <h4> | |
| 327 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} | |
| 328 </h4> | |
| 329 </div> | |
| 330 '''); | |
| 331 } | |
| 332 } | |
| 333 | |
| 334 docType(Type type) { | |
| 335 _totalTypes++; | |
| 336 _currentType = type; | |
| 337 | |
| 338 startFile(typeUrl(type)); | |
| 339 | |
| 340 final typeTitle = | |
| 341 '${type.isClass ? "Class" : "Interface"} ${typeName(type)}'; | |
| 342 writeHeader('Library ${type.library.name} / $typeTitle'); | |
| 343 writeln( | |
| 344 ''' | |
| 345 <h1>${a(libraryUrl(type.library), | |
| 346 "Library <strong>${type.library.name}</strong>")}</h1> | |
| 347 <h2>${type.isClass ? "Class" : "Interface"} | |
| 348 <strong>${typeName(type, showBounds: true)}</strong></h2> | |
| 349 '''); | |
| 350 | |
| 351 docInheritance(type); | |
| 352 | |
| 353 docCode(type.span, getTypeComment(type)); | |
| 354 docConstructors(type); | |
| 355 docMembers(type); | |
| 356 | |
| 357 writeFooter(); | |
| 358 endFile(); | |
| 359 } | |
| 360 | |
| 361 /** Document the superclass, superinterfaces and factory of [Type]. */ | |
| 362 docInheritance(Type type) { | |
| 363 final isSubclass = (type.parent != null) && !type.parent.isObject; | |
| 364 | |
| 365 Type factory; | |
| 366 if (type.definition is TypeDefinition) { | |
| 367 TypeDefinition definition = type.definition; | |
| 368 if (definition.factoryType != null) { | |
| 369 factory = definition.factoryType.type; | |
| 370 } | |
| 371 } | |
| 372 | |
| 373 if (isSubclass || | |
| 374 (type.interfaces != null && type.interfaces.length > 0) || | |
| 375 (factory != null)) { | |
| 376 writeln('<p>'); | |
| 377 | |
| 378 if (isSubclass) { | |
| 379 write('Extends ${typeReference(type.parent)}. '); | |
| 380 } | |
| 381 | |
| 382 if (type.interfaces != null && type.interfaces.length > 0) { | |
| 383 var interfaceStr = joinWithCommas(map(type.interfaces, typeReference)); | |
| 384 write('Implements ${interfaceStr}. '); | |
| 385 } | |
| 386 | |
| 387 if (factory != null) { | |
| 388 write('Has factory class ${typeReference(factory)}.'); | |
| 389 } | |
| 390 } | |
| 391 } | |
| 392 | |
| 393 /** Document the constructors for [Type], if any. */ | |
| 394 docConstructors(Type type) { | |
| 395 final names = type.constructors.getKeys().filter( | |
| 396 (name) => !name.startsWith('_')); | |
| 397 | |
| 398 if (names.length > 0) { | |
| 399 writeln('<h3>Constructors</h3>'); | |
| 400 names.sort((x, y) => x.toUpperCase().compareTo(y.toUpperCase())); | |
| 401 | |
| 402 for (final name in names) { | |
| 403 docMethod(type, type.constructors[name], constructorName: name); | |
| 404 } | |
| 405 } | |
| 406 } | |
| 407 | |
| 408 void docMembers(Type type) { | |
| 409 // Collect the different kinds of members. | |
| 410 final methods = []; | |
| 411 final fields = []; | |
| 412 | |
| 413 for (final member in orderByName(type.members)) { | |
| 414 if (member.name.startsWith('_')) continue; | |
| 415 | |
| 416 if (member.isProperty) { | |
| 417 if (member.canGet) methods.add(member.getter); | |
| 418 if (member.canSet) methods.add(member.setter); | |
| 419 } else if (member.isMethod) { | |
| 420 methods.add(member); | |
| 421 } else if (member.isField) { | |
| 422 fields.add(member); | |
| 423 } | |
| 424 } | |
| 425 | |
| 426 if (methods.length > 0) { | |
| 427 writeln('<h3>Methods</h3>'); | |
| 428 for (final method in methods) docMethod(type, method); | |
| 429 } | |
| 430 | |
| 431 if (fields.length > 0) { | |
| 432 writeln('<h3>Fields</h3>'); | |
| 433 for (final field in fields) docField(type, field); | |
| 434 } | |
| 435 } | |
| 436 | |
| 437 /** | |
| 438 * Documents the [method] in type [type]. Handles all kinds of methods | |
| 439 * including getters, setters, and constructors. | |
| 440 */ | |
| 441 docMethod(Type type, MethodMember method, [String constructorName = null]) { | |
| 442 _totalMembers++; | |
| 443 _currentMember = method; | |
| 444 | |
| 445 writeln('<div class="method"><h4 id="${memberAnchor(method)}">'); | |
| 446 | |
| 447 if (includeSource) { | |
| 448 writeln('<span class="show-code">Code</span>'); | |
| 449 } | |
| 450 | |
| 451 if (method.isStatic && !type.isTop) { | |
| 452 write('static '); | |
| 453 } | |
| 454 | |
| 455 if (method.isConstructor) { | |
| 456 write(method.isConst ? 'const ' : 'new '); | |
| 457 } | |
| 458 | |
| 459 if (constructorName == null) { | |
| 460 annotateType(type, method.returnType); | |
| 461 } | |
| 462 | |
| 463 // Translate specially-named methods: getters, setters, operators. | |
| 464 var name = method.name; | |
| 465 if (name.startsWith('get:')) { | |
| 466 // Getter. | |
| 467 name = 'get ${name.substring(4)}'; | |
| 468 } else if (name.startsWith('set:')) { | |
| 469 // Setter. | |
| 470 name = 'set ${name.substring(4)}'; | |
| 471 } else { | |
| 472 // See if it's an operator. | |
| 473 name = TokenKind.rawOperatorFromMethod(name); | |
| 474 if (name == null) { | |
| 475 name = method.name; | |
| 476 } else { | |
| 477 name = 'operator $name'; | |
| 478 } | |
| 479 } | |
| 480 | |
| 481 write('<strong>$name</strong>'); | |
| 482 | |
| 483 // Named constructors. | |
| 484 if (constructorName != null && constructorName != '') { | |
| 485 write('.'); | |
| 486 write(constructorName); | |
| 487 } | |
| 488 | |
| 489 docParamList(type, method); | |
| 490 | |
| 491 write(''' <a class="anchor-link" href="#${memberAnchor(method)}" | |
| 492 title="Permalink to ${typeName(type)}.$name">#</a>'''); | |
| 493 writeln('</h4>'); | |
| 494 | |
| 495 docCode(method.span, getMethodComment(method), showCode: true); | |
| 496 | |
| 497 writeln('</div>'); | |
| 498 } | |
| 499 | |
| 500 /** Documents the field [field] of type [type]. */ | |
| 501 docField(Type type, FieldMember field) { | |
| 502 _totalMembers++; | |
| 503 _currentMember = field; | |
| 504 | |
| 505 writeln('<div class="field"><h4 id="${memberAnchor(field)}">'); | |
| 506 | |
| 507 if (includeSource) { | |
| 508 writeln('<span class="show-code">Code</span>'); | |
| 509 } | |
| 510 | |
| 511 if (field.isStatic && !type.isTop) { | |
| 512 write('static '); | |
| 513 } | |
| 514 | |
| 515 if (field.isFinal) { | |
| 516 write('final '); | |
| 517 } else if (field.type.name == 'Dynamic') { | |
| 518 write('var '); | |
| 519 } | |
| 520 | |
| 521 annotateType(type, field.type); | |
| 522 write( | |
| 523 ''' | |
| 524 <strong>${field.name}</strong> <a class="anchor-link" | |
| 525 href="#${memberAnchor(field)}" | |
| 526 title="Permalink to ${typeName(type)}.${field.name}">#</a> | |
| 353 </h4> | 527 </h4> |
| 354 </div> | |
| 355 '''); | 528 '''); |
| 529 | |
| 530 docCode(field.span, getFieldComment(field), showCode: true); | |
| 531 writeln('</div>'); | |
| 532 } | |
| 533 | |
| 534 docParamList(Type enclosingType, MethodMember member) { | |
| 535 write('('); | |
| 536 bool first = true; | |
| 537 bool inOptionals = false; | |
| 538 for (final parameter in member.parameters) { | |
| 539 if (!first) write(', '); | |
| 540 | |
| 541 if (!inOptionals && parameter.isOptional) { | |
| 542 write('['); | |
| 543 inOptionals = true; | |
| 544 } | |
| 545 | |
| 546 annotateType(enclosingType, parameter.type, parameter.name); | |
| 547 | |
| 548 // Show the default value for named optional parameters. | |
| 549 if (parameter.isOptional && parameter.hasDefaultValue) { | |
| 550 write(' = '); | |
| 551 // TODO(rnystrom): Using the definition text here is a bit cheap. | |
| 552 // We really should be pretty-printing the AST so that if you have: | |
| 553 // foo([arg = 1 + /* comment */ 2]) | |
| 554 // the docs should just show: | |
| 555 // foo([arg = 1 + 2]) | |
| 556 // For now, we'll assume you don't do that. | |
| 557 write(parameter.definition.value.span.text); | |
| 558 } | |
| 559 | |
| 560 first = false; | |
| 561 } | |
| 562 | |
| 563 if (inOptionals) write(']'); | |
| 564 write(')'); | |
| 565 } | |
| 566 | |
| 567 /** | |
| 568 * Documents the code contained within [span] with [comment]. If [showCode] | |
| 569 * is `true` (and [includeSource] is set), also includes the source code. | |
| 570 */ | |
| 571 docCode(SourceSpan span, String comment, [bool showCode = false]) { | |
| 572 writeln('<div class="doc">'); | |
| 573 if (comment != null) { | |
| 574 writeln(md.markdownToHtml(comment)); | |
| 575 } | |
| 576 | |
| 577 if (includeSource && showCode) { | |
| 578 writeln('<pre class="source">'); | |
| 579 write(formatCode(span)); | |
| 580 writeln('</pre>'); | |
| 581 } | |
| 582 | |
| 583 writeln('</div>'); | |
| 584 } | |
| 585 | |
| 586 /** Get the doc comment associated with the given type. */ | |
| 587 String getTypeComment(Type type) => _comments.find(type.span); | |
| 588 | |
| 589 /** Get the doc comment associated with the given method. */ | |
| 590 String getMethodComment(MethodMember method) => _comments.find(method.span); | |
| 591 | |
| 592 /** Get the doc comment associated with the given field. */ | |
| 593 String getFieldComment(FieldMember field) => _comments.find(field.span); | |
| 594 | |
| 595 /** | |
| 596 * Creates a hyperlink. Handles turning the [href] into an appropriate | |
| 597 * relative path from the current file. | |
| 598 */ | |
| 599 String a(String href, String contents, [String class]) { | |
| 600 final css = class == null ? '' : ' class="$class"'; | |
| 601 return '<a href="${relativePath(href)}"$css>$contents</a>'; | |
| 602 } | |
| 603 | |
| 604 /** | |
| 605 * Writes a type annotation for the given type and (optional) parameter name. | |
| 606 */ | |
| 607 annotateType(Type enclosingType, Type type, [String paramName = null]) { | |
| 608 // Don't bother explicitly displaying Dynamic. | |
| 609 if (type.isVar) { | |
| 610 if (paramName !== null) write(paramName); | |
| 611 return; | |
| 612 } | |
| 613 | |
| 614 // For parameters, handle non-typedefed function types. | |
| 615 if (paramName !== null) { | |
| 616 final call = type.getCallMethod(); | |
| 617 if (call != null) { | |
| 618 annotateType(enclosingType, call.returnType); | |
| 619 write(paramName); | |
| 620 | |
| 621 docParamList(enclosingType, call); | |
| 622 return; | |
| 623 } | |
| 624 } | |
| 625 | |
| 626 linkToType(enclosingType, type); | |
| 627 | |
| 628 write(' '); | |
| 629 if (paramName !== null) write(paramName); | |
| 630 } | |
| 631 | |
| 632 /** Writes a link to a human-friendly string representation for a type. */ | |
| 633 linkToType(Type enclosingType, Type type) { | |
| 634 if (type is ParameterType) { | |
| 635 // If we're using a type parameter within the body of a generic class then | |
| 636 // just link back up to the class. | |
| 637 write(a(typeUrl(enclosingType), type.name)); | |
| 638 return; | |
| 639 } | |
| 640 | |
| 641 // Link to the type. | |
| 642 // Use .genericType to avoid writing the <...> here. | |
| 643 write(a(typeUrl(type), type.genericType.name)); | |
| 644 | |
| 645 // See if it's a generic type. | |
| 646 if (type.isGeneric) { | |
| 647 // TODO(rnystrom): This relies on a weird corner case of frog. Currently, | |
| 648 // the only time we get into this case is when we have a "raw" generic | |
| 649 // that's been instantiated with Dynamic for all type arguments. It's kind | |
| 650 // of strange that frog works that way, but we take advantage of it to | |
| 651 // show raw types without any type arguments. | |
| 652 return; | |
| 653 } | |
| 654 | |
| 655 // See if it's an instantiation of a generic type. | |
| 656 final typeArgs = type.typeArgsInOrder; | |
| 657 if (typeArgs != null) { | |
| 658 write('<'); | |
| 659 bool first = true; | |
| 660 for (final arg in typeArgs) { | |
| 661 if (!first) write(', '); | |
| 662 first = false; | |
| 663 linkToType(enclosingType, arg); | |
| 664 } | |
| 665 write('>'); | |
| 666 } | |
| 667 } | |
| 668 | |
| 669 /** Creates a linked cross reference to [type]. */ | |
| 670 typeReference(Type type) { | |
| 671 // TODO(rnystrom): Do we need to handle ParameterTypes here like | |
| 672 // annotation() does? | |
| 673 return a(typeUrl(type), typeName(type), class: 'crossref'); | |
| 674 } | |
| 675 | |
| 676 /** Generates a human-friendly string representation for a type. */ | |
| 677 typeName(Type type, [bool showBounds = false]) { | |
| 678 // See if it's a generic type. | |
| 679 if (type.isGeneric) { | |
| 680 final typeParams = []; | |
| 681 for (final typeParam in type.genericType.typeParameters) { | |
| 682 if (showBounds && | |
| 683 (typeParam.extendsType != null) && | |
| 684 !typeParam.extendsType.isObject) { | |
| 685 final bound = typeName(typeParam.extendsType, showBounds: true); | |
| 686 typeParams.add('${typeParam.name} extends $bound'); | |
| 687 } else { | |
| 688 typeParams.add(typeParam.name); | |
| 689 } | |
| 690 } | |
| 691 | |
| 692 final params = Strings.join(typeParams, ', '); | |
| 693 return '${type.name}<$params>'; | |
| 694 } | |
| 695 | |
| 696 // See if it's an instantiation of a generic type. | |
| 697 final typeArgs = type.typeArgsInOrder; | |
| 698 if (typeArgs != null) { | |
| 699 final args = Strings.join(map(typeArgs, (arg) => typeName(arg)), ', '); | |
| 700 return '${type.genericType.name}<$args>'; | |
| 701 } | |
| 702 | |
| 703 // Regular type. | |
| 704 return type.name; | |
| 705 } | |
| 706 | |
| 707 /** | |
| 708 * Takes a string of Dart code and turns it into sanitized HTML. | |
| 709 */ | |
| 710 formatCode(SourceSpan span) { | |
| 711 // Remove leading indentation to line up with first line. | |
| 712 final column = getSpanColumn(span); | |
| 713 final lines = span.text.split('\n'); | |
| 714 // TODO(rnystrom): Dirty hack. | |
| 715 for (final i = 1; i < lines.length; i++) { | |
| 716 lines[i] = unindent(lines[i], column); | |
| 717 } | |
| 718 | |
| 719 final code = Strings.join(lines, '\n'); | |
| 720 | |
| 721 // Syntax highlight. | |
| 722 return classifySource(new SourceFile('', code)); | |
| 723 } | |
| 724 | |
| 725 /** | |
| 726 * This will be called whenever a doc comment hits a `[name]` in square | |
| 727 * brackets. It will try to figure out what the name refers to and link or | |
| 728 * style it appropriately. | |
| 729 */ | |
| 730 md.Node resolveNameReference(String name) { | |
| 731 makeLink(String href) { | |
| 732 final anchor = new md.Element.text('a', name); | |
| 733 anchor.attributes['href'] = relativePath(href); | |
| 734 anchor.attributes['class'] = 'crossref'; | |
| 735 return anchor; | |
| 736 } | |
| 737 | |
| 738 findMember(Type type) { | |
| 739 final member = type.members[name]; | |
| 740 if (member == null) return null; | |
| 741 | |
| 742 // Special case: if the member we've resolved is a property (i.e. it wraps | |
| 743 // a getter and/or setter then *that* member itself won't be on the docs, | |
| 744 // just the getter or setter will be. So pick one of those to link to. | |
| 745 if (member.isProperty) { | |
| 746 return member.canGet ? member.getter : member.setter; | |
| 747 } | |
| 748 | |
| 749 return member; | |
| 750 } | |
| 751 | |
| 752 // See if it's a parameter of the current method. | |
| 753 if (_currentMember != null) { | |
| 754 for (final parameter in _currentMember.parameters) { | |
| 755 if (parameter.name == name) { | |
| 756 final element = new md.Element.text('span', name); | |
| 757 element.attributes['class'] = 'param'; | |
| 758 return element; | |
| 759 } | |
| 760 } | |
| 761 } | |
| 762 | |
| 763 // See if it's another member of the current type. | |
| 764 if (_currentType != null) { | |
| 765 final member = findMember(_currentType); | |
| 766 if (member != null) { | |
| 767 return makeLink(memberUrl(member)); | |
| 768 } | |
| 769 } | |
| 770 | |
| 771 // See if it's another type in the current library. | |
| 772 if (_currentLibrary != null) { | |
| 773 final type = _currentLibrary.types[name]; | |
| 774 if (type != null) { | |
| 775 return makeLink(typeUrl(type)); | |
| 776 } | |
| 777 | |
| 778 // See if it's a top-level member in the current library. | |
| 779 final member = findMember(_currentLibrary.topType); | |
| 780 if (member != null) { | |
| 781 return makeLink(memberUrl(member)); | |
| 782 } | |
| 783 } | |
| 784 | |
| 785 // TODO(rnystrom): Should also consider: | |
| 786 // * Names imported by libraries this library imports. | |
| 787 // * Type parameters of the enclosing type. | |
| 788 | |
| 789 return new md.Element.text('code', name); | |
| 790 } | |
| 791 | |
| 792 // TODO(rnystrom): Move into SourceSpan? | |
| 793 int getSpanColumn(SourceSpan span) { | |
| 794 final line = span.file.getLine(span.start); | |
| 795 return span.file.getColumn(line, span.start); | |
| 356 } | 796 } |
| 357 } | 797 } |
| 358 | |
| 359 docType(Type type) { | |
| 360 _totalTypes++; | |
| 361 _currentType = type; | |
| 362 | |
| 363 startFile(typeUrl(type)); | |
| 364 | |
| 365 final typeTitle = '${type.isClass ? "Class" : "Interface"} ${typeName(type)}'; | |
| 366 writeHeader('Library ${type.library.name} / $typeTitle'); | |
| 367 writeln( | |
| 368 ''' | |
| 369 <h1>${a(libraryUrl(type.library), | |
| 370 "Library <strong>${type.library.name}</strong>")}</h1> | |
| 371 <h2>${type.isClass ? "Class" : "Interface"} | |
| 372 <strong>${typeName(type, showBounds: true)}</strong></h2> | |
| 373 '''); | |
| 374 | |
| 375 docInheritance(type); | |
| 376 docCode(type.span, _runDocumenters(type, _typeDocumenters)); | |
| 377 docConstructors(type); | |
| 378 docMembers(type); | |
| 379 | |
| 380 writeFooter(); | |
| 381 endFile(); | |
| 382 } | |
| 383 | |
| 384 void docMembers(Type type) { | |
| 385 // Collect the different kinds of members. | |
| 386 final methods = []; | |
| 387 final fields = []; | |
| 388 | |
| 389 for (final member in orderByName(type.members)) { | |
| 390 if (member.name.startsWith('_')) continue; | |
| 391 | |
| 392 if (member.isProperty) { | |
| 393 if (member.canGet) methods.add(member.getter); | |
| 394 if (member.canSet) methods.add(member.setter); | |
| 395 } else if (member.isMethod) { | |
| 396 methods.add(member); | |
| 397 } else if (member.isField) { | |
| 398 fields.add(member); | |
| 399 } | |
| 400 } | |
| 401 | |
| 402 if (methods.length > 0) { | |
| 403 writeln('<h3>Methods</h3>'); | |
| 404 for (final method in methods) docMethod(type, method); | |
| 405 } | |
| 406 | |
| 407 if (fields.length > 0) { | |
| 408 writeln('<h3>Fields</h3>'); | |
| 409 for (final field in fields) docField(type, field); | |
| 410 } | |
| 411 } | |
| 412 | |
| 413 /** Document the superclass, superinterfaces and factory of [Type]. */ | |
| 414 docInheritance(Type type) { | |
| 415 final isSubclass = (type.parent != null) && !type.parent.isObject; | |
| 416 | |
| 417 Type factory; | |
| 418 if (type.definition is TypeDefinition) { | |
| 419 TypeDefinition definition = type.definition; | |
| 420 if (definition.factoryType != null) { | |
| 421 factory = definition.factoryType.type; | |
| 422 } | |
| 423 } | |
| 424 | |
| 425 if (isSubclass || | |
| 426 (type.interfaces != null && type.interfaces.length > 0) || | |
| 427 (factory != null)) { | |
| 428 writeln('<p>'); | |
| 429 | |
| 430 if (isSubclass) { | |
| 431 write('Extends ${typeReference(type.parent)}. '); | |
| 432 } | |
| 433 | |
| 434 if (type.interfaces != null && type.interfaces.length > 0) { | |
| 435 var interfaceStr = joinWithCommas(map(type.interfaces, typeReference)); | |
| 436 write('Implements ${interfaceStr}. '); | |
| 437 } | |
| 438 | |
| 439 if (factory != null) { | |
| 440 write('Has factory class ${typeReference(factory)}.'); | |
| 441 } | |
| 442 } | |
| 443 } | |
| 444 | |
| 445 /** Document the constructors for [Type], if any. */ | |
| 446 docConstructors(Type type) { | |
| 447 final names = type.constructors.getKeys().filter( | |
| 448 (name) => !name.startsWith('_')); | |
| 449 | |
| 450 if (names.length > 0) { | |
| 451 writeln('<h3>Constructors</h3>'); | |
| 452 names.sort((x, y) => x.toUpperCase().compareTo(y.toUpperCase())); | |
| 453 | |
| 454 for (final name in names) { | |
| 455 docMethod(type, type.constructors[name], constructorName: name); | |
| 456 } | |
| 457 } | |
| 458 } | |
| 459 | |
| 460 /** | |
| 461 * Documents the [method] in type [type]. Handles all kinds of methods | |
| 462 * including getters, setters, and constructors. | |
| 463 */ | |
| 464 docMethod(Type type, MethodMember method, [String constructorName = null]) { | |
| 465 _totalMembers++; | |
| 466 _currentMember = method; | |
| 467 | |
| 468 writeln('<div class="method"><h4 id="${memberAnchor(method)}">'); | |
| 469 | |
| 470 if (includeSource) { | |
| 471 writeln('<span class="show-code">Code</span>'); | |
| 472 } | |
| 473 | |
| 474 if (method.isStatic && !type.isTop) { | |
| 475 write('static '); | |
| 476 } | |
| 477 | |
| 478 if (method.isConstructor) { | |
| 479 write(method.isConst ? 'const ' : 'new '); | |
| 480 } | |
| 481 | |
| 482 if (constructorName == null) { | |
| 483 annotateType(type, method.returnType); | |
| 484 } | |
| 485 | |
| 486 // Translate specially-named methods: getters, setters, operators. | |
| 487 var name = method.name; | |
| 488 if (name.startsWith('get:')) { | |
| 489 // Getter. | |
| 490 name = 'get ${name.substring(4)}'; | |
| 491 } else if (name.startsWith('set:')) { | |
| 492 // Setter. | |
| 493 name = 'set ${name.substring(4)}'; | |
| 494 } else { | |
| 495 // See if it's an operator. | |
| 496 name = TokenKind.rawOperatorFromMethod(name); | |
| 497 if (name == null) { | |
| 498 name = method.name; | |
| 499 } else { | |
| 500 name = 'operator $name'; | |
| 501 } | |
| 502 } | |
| 503 | |
| 504 write('<strong>$name</strong>'); | |
| 505 | |
| 506 // Named constructors. | |
| 507 if (constructorName != null && constructorName != '') { | |
| 508 write('.'); | |
| 509 write(constructorName); | |
| 510 } | |
| 511 | |
| 512 docParamList(type, method); | |
| 513 | |
| 514 write(''' <a class="anchor-link" href="#${memberAnchor(method)}" | |
| 515 title="Permalink to ${typeName(type)}.$name">#</a>'''); | |
| 516 writeln('</h4>'); | |
| 517 | |
| 518 docCode(method.span, _runDocumenters(method, _methodDocumenters), | |
| 519 showCode: true); | |
| 520 | |
| 521 writeln('</div>'); | |
| 522 } | |
| 523 | |
| 524 docParamList(Type enclosingType, MethodMember member) { | |
| 525 write('('); | |
| 526 bool first = true; | |
| 527 bool inOptionals = false; | |
| 528 for (final parameter in member.parameters) { | |
| 529 if (!first) write(', '); | |
| 530 | |
| 531 if (!inOptionals && parameter.isOptional) { | |
| 532 write('['); | |
| 533 inOptionals = true; | |
| 534 } | |
| 535 | |
| 536 annotateType(enclosingType, parameter.type, parameter.name); | |
| 537 | |
| 538 // Show the default value for named optional parameters. | |
| 539 if (parameter.isOptional && parameter.hasDefaultValue) { | |
| 540 write(' = '); | |
| 541 // TODO(rnystrom): Using the definition text here is a bit cheap. | |
| 542 // We really should be pretty-printing the AST so that if you have: | |
| 543 // foo([arg = 1 + /* comment */ 2]) | |
| 544 // the docs should just show: | |
| 545 // foo([arg = 1 + 2]) | |
| 546 // For now, we'll assume you don't do that. | |
| 547 write(parameter.definition.value.span.text); | |
| 548 } | |
| 549 | |
| 550 first = false; | |
| 551 } | |
| 552 | |
| 553 if (inOptionals) write(']'); | |
| 554 write(')'); | |
| 555 } | |
| 556 | |
| 557 /** Documents the field [field] of type [type]. */ | |
| 558 docField(Type type, FieldMember field) { | |
| 559 _totalMembers++; | |
| 560 _currentMember = field; | |
| 561 | |
| 562 writeln('<div class="field"><h4 id="${memberAnchor(field)}">'); | |
| 563 | |
| 564 if (includeSource) { | |
| 565 writeln('<span class="show-code">Code</span>'); | |
| 566 } | |
| 567 | |
| 568 if (field.isStatic && !type.isTop) { | |
| 569 write('static '); | |
| 570 } | |
| 571 | |
| 572 if (field.isFinal) { | |
| 573 write('final '); | |
| 574 } else if (field.type.name == 'Dynamic') { | |
| 575 write('var '); | |
| 576 } | |
| 577 | |
| 578 annotateType(type, field.type); | |
| 579 write( | |
| 580 ''' | |
| 581 <strong>${field.name}</strong> <a class="anchor-link" | |
| 582 href="#${memberAnchor(field)}" | |
| 583 title="Permalink to ${typeName(type)}.${field.name}">#</a> | |
| 584 </h4> | |
| 585 '''); | |
| 586 | |
| 587 docCode(field.span, _runDocumenters(field, _fieldDocumenters), | |
| 588 showCode: true); | |
| 589 writeln('</div>'); | |
| 590 } | |
| 591 | |
| 592 /** | |
| 593 * Creates a hyperlink. Handles turning the [href] into an appropriate relative | |
| 594 * path from the current file. | |
| 595 */ | |
| 596 String a(String href, String contents, [String class]) { | |
| 597 final css = class == null ? '' : ' class="$class"'; | |
| 598 return '<a href="${relativePath(href)}"$css>$contents</a>'; | |
| 599 } | |
| 600 | |
| 601 /** Generates a human-friendly string representation for a type. */ | |
| 602 typeName(Type type, [bool showBounds = false]) { | |
| 603 // See if it's a generic type. | |
| 604 if (type.isGeneric) { | |
| 605 final typeParams = []; | |
| 606 for (final typeParam in type.genericType.typeParameters) { | |
| 607 if (showBounds && | |
| 608 (typeParam.extendsType != null) && | |
| 609 !typeParam.extendsType.isObject) { | |
| 610 final bound = typeName(typeParam.extendsType, showBounds: true); | |
| 611 typeParams.add('${typeParam.name} extends $bound'); | |
| 612 } else { | |
| 613 typeParams.add(typeParam.name); | |
| 614 } | |
| 615 } | |
| 616 | |
| 617 final params = Strings.join(typeParams, ', '); | |
| 618 return '${type.name}<$params>'; | |
| 619 } | |
| 620 | |
| 621 // See if it's an instantiation of a generic type. | |
| 622 final typeArgs = type.typeArgsInOrder; | |
| 623 if (typeArgs != null) { | |
| 624 final args = Strings.join(map(typeArgs, typeName), ', '); | |
| 625 return '${type.genericType.name}<$args>'; | |
| 626 } | |
| 627 | |
| 628 // Regular type. | |
| 629 return type.name; | |
| 630 } | |
| 631 | |
| 632 /** Writes a link to a human-friendly string representation for a type. */ | |
| 633 linkToType(Type enclosingType, Type type) { | |
| 634 if (type is ParameterType) { | |
| 635 // If we're using a type parameter within the body of a generic class then | |
| 636 // just link back up to the class. | |
| 637 write(a(typeUrl(enclosingType), type.name)); | |
| 638 return; | |
| 639 } | |
| 640 | |
| 641 // Link to the type. | |
| 642 // Use .genericType to avoid writing the <...> here. | |
| 643 write(a(typeUrl(type), type.genericType.name)); | |
| 644 | |
| 645 // See if it's a generic type. | |
| 646 if (type.isGeneric) { | |
| 647 // TODO(rnystrom): This relies on a weird corner case of frog. Currently, | |
| 648 // the only time we get into this case is when we have a "raw" generic | |
| 649 // that's been instantiated with Dynamic for all type arguments. It's kind | |
| 650 // of strange that frog works that way, but we take advantage of it to | |
| 651 // show raw types without any type arguments. | |
| 652 return; | |
| 653 } | |
| 654 | |
| 655 // See if it's an instantiation of a generic type. | |
| 656 final typeArgs = type.typeArgsInOrder; | |
| 657 if (typeArgs != null) { | |
| 658 write('<'); | |
| 659 bool first = true; | |
| 660 for (final arg in typeArgs) { | |
| 661 if (!first) write(', '); | |
| 662 first = false; | |
| 663 linkToType(enclosingType, arg); | |
| 664 } | |
| 665 write('>'); | |
| 666 } | |
| 667 } | |
| 668 | |
| 669 /** Creates a linked cross reference to [type]. */ | |
| 670 typeReference(Type type) { | |
| 671 // TODO(rnystrom): Do we need to handle ParameterTypes here like | |
| 672 // annotation() does? | |
| 673 return a(typeUrl(type), typeName(type), class: 'crossref'); | |
| 674 } | |
| 675 | |
| 676 /** | |
| 677 * Writes a type annotation for the given type and (optional) parameter name. | |
| 678 */ | |
| 679 annotateType(Type enclosingType, Type type, [String paramName = null]) { | |
| 680 // Don't bother explicitly displaying Dynamic. | |
| 681 if (type.isVar) { | |
| 682 if (paramName !== null) write(paramName); | |
| 683 return; | |
| 684 } | |
| 685 | |
| 686 // For parameters, handle non-typedefed function types. | |
| 687 if (paramName !== null) { | |
| 688 final call = type.getCallMethod(); | |
| 689 if (call != null) { | |
| 690 annotateType(enclosingType, call.returnType); | |
| 691 write(paramName); | |
| 692 | |
| 693 docParamList(enclosingType, call); | |
| 694 return; | |
| 695 } | |
| 696 } | |
| 697 | |
| 698 linkToType(enclosingType, type); | |
| 699 | |
| 700 write(' '); | |
| 701 if (paramName !== null) write(paramName); | |
| 702 } | |
| 703 | |
| 704 | |
| 705 /** | |
| 706 * This will be called whenever a doc comment hits a `[name]` in square | |
| 707 * brackets. It will try to figure out what the name refers to and link or | |
| 708 * style it appropriately. | |
| 709 */ | |
| 710 md.Node resolveNameReference(String name) { | |
| 711 makeLink(String href) { | |
| 712 final anchor = new md.Element.text('a', name); | |
| 713 anchor.attributes['href'] = relativePath(href); | |
| 714 anchor.attributes['class'] = 'crossref'; | |
| 715 return anchor; | |
| 716 } | |
| 717 | |
| 718 findMember(Type type) { | |
| 719 final member = type.members[name]; | |
| 720 if (member == null) return null; | |
| 721 | |
| 722 // Special case: if the member we've resolved is a property (i.e. it wraps | |
| 723 // a getter and/or setter then *that* member itself won't be on the docs, | |
| 724 // just the getter or setter will be. So pick one of those to link to. | |
| 725 if (member.isProperty) { | |
| 726 return member.canGet ? member.getter : member.setter; | |
| 727 } | |
| 728 | |
| 729 return member; | |
| 730 } | |
| 731 | |
| 732 // See if it's a parameter of the current method. | |
| 733 if (_currentMember != null) { | |
| 734 for (final parameter in _currentMember.parameters) { | |
| 735 if (parameter.name == name) { | |
| 736 final element = new md.Element.text('span', name); | |
| 737 element.attributes['class'] = 'param'; | |
| 738 return element; | |
| 739 } | |
| 740 } | |
| 741 } | |
| 742 | |
| 743 // See if it's another member of the current type. | |
| 744 if (_currentType != null) { | |
| 745 final member = findMember(_currentType); | |
| 746 if (member != null) { | |
| 747 return makeLink(memberUrl(member)); | |
| 748 } | |
| 749 } | |
| 750 | |
| 751 // See if it's another type in the current library. | |
| 752 if (_currentLibrary != null) { | |
| 753 final type = _currentLibrary.types[name]; | |
| 754 if (type != null) { | |
| 755 return makeLink(typeUrl(type)); | |
| 756 } | |
| 757 | |
| 758 // See if it's a top-level member in the current library. | |
| 759 final member = findMember(_currentLibrary.topType); | |
| 760 if (member != null) { | |
| 761 return makeLink(memberUrl(member)); | |
| 762 } | |
| 763 } | |
| 764 | |
| 765 // TODO(rnystrom): Should also consider: | |
| 766 // * Names imported by libraries this library imports. | |
| 767 // * Type parameters of the enclosing type. | |
| 768 | |
| 769 return new md.Element.text('code', name); | |
| 770 } | |
| 771 | |
| 772 /** | |
| 773 * Documents the code contained within [span]. Will include the previous | |
| 774 * Dartdoc associated with that span if found, and will include the syntax | |
| 775 * highlighted code itself if desired. | |
| 776 */ | |
| 777 docCode(SourceSpan span, String extraMarkdown, [bool showCode = false]) { | |
| 778 if (span == null) return; | |
| 779 | |
| 780 writeln('<div class="doc">'); | |
| 781 final comment = findComment(span); | |
| 782 if (comment != null) { | |
| 783 writeln(md.markdownToHtml('${comment}\n\n${extraMarkdown}')); | |
| 784 } else { | |
| 785 writeln(md.markdownToHtml(extraMarkdown)); | |
| 786 } | |
| 787 | |
| 788 if (includeSource && showCode) { | |
| 789 writeln('<pre class="source">'); | |
| 790 write(formatCode(span)); | |
| 791 writeln('</pre>'); | |
| 792 } | |
| 793 | |
| 794 writeln('</div>'); | |
| 795 } | |
| 796 | |
| 797 /** Finds the doc comment preceding the given source span, if there is one. */ | |
| 798 findComment(SourceSpan span) => findCommentInFile(span.file, span.start); | |
| 799 | |
| 800 /** Finds the doc comment preceding the given source span, if there is one. */ | |
| 801 findCommentInFile(SourceFile file, int position) { | |
| 802 // Get the doc comments for this file. | |
| 803 final fileComments = _comments.putIfAbsent(file.filename, | |
| 804 () => parseDocComments(file)); | |
| 805 | |
| 806 return fileComments[position]; | |
| 807 } | |
| 808 | |
| 809 parseDocComments(SourceFile file) { | |
| 810 final comments = new Map<int, String>(); | |
| 811 | |
| 812 final tokenizer = new Tokenizer(file, false); | |
| 813 var lastComment = null; | |
| 814 | |
| 815 while (true) { | |
| 816 final token = tokenizer.next(); | |
| 817 if (token.kind == TokenKind.END_OF_FILE) break; | |
| 818 | |
| 819 if (token.kind == TokenKind.COMMENT) { | |
| 820 final text = token.text; | |
| 821 if (text.startsWith('/**')) { | |
| 822 // Remember that we've encountered a doc comment. | |
| 823 lastComment = stripComment(token.text); | |
| 824 } else if (text.startsWith('///')) { | |
| 825 var line = text.substring(3, text.length); | |
| 826 // Allow a leading space. | |
| 827 if (line.startsWith(' ')) line = line.substring(1, text.length); | |
| 828 if (lastComment == null) { | |
| 829 lastComment = line; | |
| 830 } else { | |
| 831 lastComment = '$lastComment$line'; | |
| 832 } | |
| 833 } | |
| 834 } else if (token.kind == TokenKind.WHITESPACE) { | |
| 835 // Ignore whitespace tokens. | |
| 836 } else if (token.kind == TokenKind.HASH) { | |
| 837 // Look for #library() to find the library comment. | |
| 838 final next = tokenizer.next(); | |
| 839 if ((lastComment != null) && (next.kind == TokenKind.LIBRARY)) { | |
| 840 comments[_libraryDoc] = lastComment; | |
| 841 lastComment = null; | |
| 842 } | |
| 843 } else { | |
| 844 if (lastComment != null) { | |
| 845 // We haven't attached the last doc comment to something yet, so stick | |
| 846 // it to this token. | |
| 847 comments[token.start] = lastComment; | |
| 848 lastComment = null; | |
| 849 } | |
| 850 } | |
| 851 } | |
| 852 | |
| 853 return comments; | |
| 854 } | |
| 855 | |
| 856 /** | |
| 857 * Takes a string of Dart code and turns it into sanitized HTML. | |
| 858 */ | |
| 859 formatCode(SourceSpan span) { | |
| 860 // Remove leading indentation to line up with first line. | |
| 861 final column = getSpanColumn(span); | |
| 862 final lines = span.text.split('\n'); | |
| 863 // TODO(rnystrom): Dirty hack. | |
| 864 for (final i = 1; i < lines.length; i++) { | |
| 865 lines[i] = unindent(lines[i], column); | |
| 866 } | |
| 867 | |
| 868 final code = Strings.join(lines, '\n'); | |
| 869 | |
| 870 // Syntax highlight. | |
| 871 return classifySource(new SourceFile('', code)); | |
| 872 } | |
| 873 | |
| 874 // TODO(rnystrom): Move into SourceSpan? | |
| 875 int getSpanColumn(SourceSpan span) { | |
| 876 final line = span.file.getLine(span.start); | |
| 877 return span.file.getColumn(line, span.start); | |
| 878 } | |
| 879 | |
| 880 /** | |
| 881 * Pulls the raw text out of a doc comment (i.e. removes the comment | |
| 882 * characters). | |
| 883 */ | |
| 884 stripComment(comment) { | |
| 885 StringBuffer buf = new StringBuffer(); | |
| 886 | |
| 887 for (final line in comment.split('\n')) { | |
| 888 line = line.trim(); | |
| 889 if (line.startsWith('/**')) line = line.substring(3, line.length); | |
| 890 if (line.endsWith('*/')) line = line.substring(0, line.length - 2); | |
| 891 line = line.trim(); | |
| 892 if (line.startsWith('* ')) { | |
| 893 line = line.substring(2, line.length); | |
| 894 } else if (line.startsWith('*')) { | |
| 895 line = line.substring(1, line.length); | |
| 896 } | |
| 897 | |
| 898 buf.add(line); | |
| 899 buf.add('\n'); | |
| 900 } | |
| 901 | |
| 902 return buf.toString(); | |
| 903 } | |
| 904 | |
| 905 /** Register a callback to add additional documentation to a type. */ | |
| 906 addTypeDocumenter(TypeDocumenter fn) => _typeDocumenters.add(fn); | |
| 907 | |
| 908 /** Register a callback to add additional documentation to a method. */ | |
| 909 addMethodDocumenter(MethodDocumenter fn) => _methodDocumenters.add(fn); | |
| 910 | |
| 911 /** Register a callback to add additional documentation to a field. */ | |
| 912 addFieldDocumenter(FieldDocumenter fn) => _fieldDocumenters.add(fn); | |
| OLD | NEW |