| 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 CommentMap _comments; |
| 118 | 79 |
| 119 // Patch in support for [:...:]-style code to the markdown parser. | 80 /** The library that we're currently generating docs for. */ |
| 120 // TODO(rnystrom): Markdown already has syntax for this. Phase this out? | 81 Library _currentLibrary; |
| 121 md.InlineParser.syntaxes.insertRange(0, 1, | 82 |
| 122 new md.CodeSyntax(@'\[\:((?:.|\n)*?)\:\]')); | 83 /** The type that we're currently generating docs for. */ |
| 123 | 84 Type _currentType; |
| 124 md.setImplicitLinkResolver(resolveNameReference); | 85 |
| 125 } | 86 /** The member that we're currently generating docs for. */ |
| 126 | 87 Member _currentMember; |
| 127 document(String entrypoint) { | 88 |
| 128 try { | 89 int _totalLibraries = 0; |
| 129 var oldDietParse = options.dietParse; | 90 int _totalTypes = 0; |
| 130 options.dietParse = true; | 91 int _totalMembers = 0; |
| 131 | 92 |
| 132 // Handle the built-in entrypoints. | 93 Dartdoc() |
| 133 switch (entrypoint) { | 94 : _comments = new CommentMap() { |
| 134 case 'corelib': | 95 // Patch in support for [:...:]-style code to the markdown parser. |
| 135 world.getOrAddLibrary('dart:core'); | 96 // TODO(rnystrom): Markdown already has syntax for this. Phase this out? |
| 136 world.getOrAddLibrary('dart:coreimpl'); | 97 md.InlineParser.syntaxes.insertRange(0, 1, |
| 137 world.getOrAddLibrary('dart:json'); | 98 new md.CodeSyntax(@'\[\:((?:.|\n)*?)\:\]')); |
| 138 world.process(); | 99 |
| 139 break; | 100 md.setImplicitLinkResolver(resolveNameReference); |
| 140 | 101 } |
| 141 case 'dom': | 102 |
| 142 world.getOrAddLibrary('dart:core'); | 103 document(String entrypoint) { |
| 143 world.getOrAddLibrary('dart:coreimpl'); | 104 try { |
| 144 world.getOrAddLibrary('dart:json'); | 105 var oldDietParse = options.dietParse; |
| 145 world.getOrAddLibrary('dart:dom'); | 106 options.dietParse = true; |
| 146 world.process(); | 107 |
| 147 break; | 108 // Handle the built-in entrypoints. |
| 148 | 109 switch (entrypoint) { |
| 149 case 'html': | 110 case 'corelib': |
| 150 world.getOrAddLibrary('dart:core'); | 111 world.getOrAddLibrary('dart:core'); |
| 151 world.getOrAddLibrary('dart:coreimpl'); | 112 world.getOrAddLibrary('dart:coreimpl'); |
| 152 world.getOrAddLibrary('dart:json'); | 113 world.getOrAddLibrary('dart:json'); |
| 153 world.getOrAddLibrary('dart:dom'); | 114 world.process(); |
| 154 world.getOrAddLibrary('dart:html'); | 115 break; |
| 155 world.process(); | 116 |
| 156 break; | 117 case 'dom': |
| 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.getOrAddLibrary('dart:dom'); |
| 161 } | 122 world.process(); |
| 162 | 123 break; |
| 163 world.resolveAll(); | 124 |
| 164 | 125 case 'html': |
| 165 // Generate the docs. | 126 world.getOrAddLibrary('dart:core'); |
| 166 docIndex(); | 127 world.getOrAddLibrary('dart:coreimpl'); |
| 167 for (final library in world.libraries.getValues()) { | 128 world.getOrAddLibrary('dart:json'); |
| 168 docLibrary(library); | 129 world.getOrAddLibrary('dart:dom'); |
| 169 } | 130 world.getOrAddLibrary('dart:html'); |
| 170 } finally { | 131 world.process(); |
| 171 options.dietParse = oldDietParse; | 132 break; |
| 172 } | 133 |
| 173 } | 134 default: |
| 174 | 135 // Normal entrypoint script. |
| 175 printStats(num elapsed) { | 136 world.processDartScript(entrypoint); |
| 176 print('Documented $_totalLibraries libraries, $_totalTypes types, and ' + | 137 } |
| 177 '$_totalMembers members in ${elapsed}msec.'); | 138 |
| 178 } | 139 world.resolveAll(); |
| 179 | 140 |
| 180 writeHeader(String title) { | 141 // Generate the docs. |
| 181 writeln( | 142 docIndex(); |
| 182 ''' | 143 for (final library in world.libraries.getValues()) { |
| 183 <!DOCTYPE html> | 144 docLibrary(library); |
| 184 <html> | 145 } |
| 185 <head> | 146 } finally { |
| 186 <meta charset="utf-8"> | 147 options.dietParse = oldDietParse; |
| 187 <title>$title</title> | 148 } |
| 188 <link rel="stylesheet" type="text/css" | 149 } |
| 189 href="${relativePath('styles.css')}" /> | 150 |
| 190 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,8
00" rel="stylesheet" type="text/css"> | 151 writeHeader(String title) { |
| 191 <script src="${relativePath('interact.js')}"></script> | |
| 192 </head> | |
| 193 <body> | |
| 194 <div class="page"> | |
| 195 '''); | |
| 196 docNavigation(); | |
| 197 writeln('<div class="content">'); | |
| 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( | 152 writeln( |
| 219 ''' | 153 ''' |
| 220 <h4>${a(libraryUrl(library), library.name)}</h4> | 154 <!DOCTYPE html> |
| 155 <html> |
| 156 <head> |
| 157 <meta charset="utf-8"> |
| 158 <title>$title</title> |
| 159 <link rel="stylesheet" type="text/css" |
| 160 href="${relativePath('styles.css')}" /> |
| 161 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700
,800" rel="stylesheet" type="text/css"> |
| 162 <script src="${relativePath('interact.js')}"></script> |
| 163 </head> |
| 164 <body> |
| 165 <div class="page"> |
| 221 '''); | 166 '''); |
| 222 } | 167 docNavigation(); |
| 223 | 168 writeln('<div class="content">'); |
| 224 writeFooter(); | 169 } |
| 225 endFile(); | 170 |
| 226 } | 171 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( | 172 writeln( |
| 349 ''' | 173 ''' |
| 350 <div class="type"> | 174 </div> |
| 351 <h4> | 175 <div class="footer"</div> |
| 352 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} | 176 </body></html> |
| 177 '''); |
| 178 } |
| 179 |
| 180 docIndex() { |
| 181 startFile('index.html'); |
| 182 |
| 183 writeHeader('Dart Documentation'); |
| 184 |
| 185 writeln('<h1>Dart Documentation</h1>'); |
| 186 writeln('<h3>Libraries</h3>'); |
| 187 |
| 188 for (final library in orderByName(world.libraries)) { |
| 189 writeln( |
| 190 ''' |
| 191 <h4>${a(libraryUrl(library), library.name)}</h4> |
| 192 '''); |
| 193 } |
| 194 |
| 195 writeFooter(); |
| 196 endFile(); |
| 197 } |
| 198 |
| 199 docNavigation() { |
| 200 writeln( |
| 201 ''' |
| 202 <div class="nav"> |
| 203 <h1>${a("index.html", "Dart Documentation")}</h1> |
| 204 '''); |
| 205 |
| 206 for (final library in orderByName(world.libraries)) { |
| 207 write('<h2><div class="icon-library"></div>'); |
| 208 |
| 209 if ((_currentLibrary == library) && (_currentType == null)) { |
| 210 write('<strong>${library.name}</strong>'); |
| 211 } else { |
| 212 write('${a(libraryUrl(library), library.name)}'); |
| 213 } |
| 214 write('</h2>'); |
| 215 |
| 216 // Only expand classes in navigation for current library. |
| 217 if (_currentLibrary == library) docLibraryNavigation(library); |
| 218 } |
| 219 |
| 220 writeln('</div>'); |
| 221 } |
| 222 |
| 223 /** Writes the navigation for the types contained by the given library. */ |
| 224 docLibraryNavigation(Library library) { |
| 225 // Show the exception types separately. |
| 226 final types = <Type>[]; |
| 227 final exceptions = <Type>[]; |
| 228 |
| 229 for (final type in orderByName(library.types)) { |
| 230 if (type.isTop) continue; |
| 231 if (type.name.startsWith('_')) continue; |
| 232 |
| 233 if (type.name.endsWith('Exception')) { |
| 234 exceptions.add(type); |
| 235 } else { |
| 236 types.add(type); |
| 237 } |
| 238 } |
| 239 |
| 240 if ((types.length == 0) && (exceptions.length == 0)) return; |
| 241 |
| 242 writeType(String icon, Type type) { |
| 243 write('<li>'); |
| 244 if (_currentType == type) { |
| 245 write( |
| 246 '<div class="icon-$icon"></div><strong>${typeName(type)}</strong>'); |
| 247 } else { |
| 248 write(a(typeUrl(type), |
| 249 '<div class="icon-$icon"></div>${typeName(type)}')); |
| 250 } |
| 251 writeln('</li>'); |
| 252 } |
| 253 |
| 254 writeln('<ul>'); |
| 255 types.forEach((type) => writeType(type.isClass ? 'class' : 'interface', |
| 256 type)); |
| 257 exceptions.forEach((type) => writeType('exception', type)); |
| 258 writeln('</ul>'); |
| 259 } |
| 260 |
| 261 docLibrary(Library library) { |
| 262 _totalLibraries++; |
| 263 _currentLibrary = library; |
| 264 _currentType = null; |
| 265 |
| 266 startFile(libraryUrl(library)); |
| 267 writeHeader(library.name); |
| 268 writeln('<h1>Library <strong>${library.name}</strong></h1>'); |
| 269 |
| 270 // Look for a comment for the entire library. |
| 271 final comment = _comments.findLibrary(library.baseSource); |
| 272 if (comment != null) { |
| 273 final html = md.markdownToHtml(comment); |
| 274 writeln('<div class="doc">$html</div>'); |
| 275 } |
| 276 |
| 277 // Document the top-level members. |
| 278 docMembers(library.topType); |
| 279 |
| 280 // Document the types. |
| 281 final classes = <Type>[]; |
| 282 final interfaces = <Type>[]; |
| 283 final exceptions = <Type>[]; |
| 284 |
| 285 for (final type in orderByName(library.types)) { |
| 286 if (type.isTop) continue; |
| 287 if (type.name.startsWith('_')) continue; |
| 288 |
| 289 if (type.name.endsWith('Exception')) { |
| 290 exceptions.add(type); |
| 291 } else if (type.isClass) { |
| 292 classes.add(type); |
| 293 } else { |
| 294 interfaces.add(type); |
| 295 } |
| 296 } |
| 297 |
| 298 docTypes(classes, 'Classes'); |
| 299 docTypes(interfaces, 'Interfaces'); |
| 300 docTypes(exceptions, 'Exceptions'); |
| 301 |
| 302 writeFooter(); |
| 303 endFile(); |
| 304 |
| 305 for (final type in library.types.getValues()) { |
| 306 if (!type.isTop) docType(type); |
| 307 } |
| 308 } |
| 309 |
| 310 docTypes(List<Type> types, String header) { |
| 311 if (types.length == 0) return; |
| 312 |
| 313 writeln('<h3>$header</h3>'); |
| 314 |
| 315 for (final type in types) { |
| 316 writeln( |
| 317 ''' |
| 318 <div class="type"> |
| 319 <h4> |
| 320 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} |
| 321 </h4> |
| 322 </div> |
| 323 '''); |
| 324 } |
| 325 } |
| 326 |
| 327 docType(Type type) { |
| 328 _totalTypes++; |
| 329 _currentType = type; |
| 330 |
| 331 startFile(typeUrl(type)); |
| 332 |
| 333 final typeTitle = |
| 334 '${type.isClass ? "Class" : "Interface"} ${typeName(type)}'; |
| 335 writeHeader('Library ${type.library.name} / $typeTitle'); |
| 336 writeln( |
| 337 ''' |
| 338 <h1>${a(libraryUrl(type.library), |
| 339 "Library <strong>${type.library.name}</strong>")}</h1> |
| 340 <h2>${type.isClass ? "Class" : "Interface"} |
| 341 <strong>${typeName(type, showBounds: true)}</strong></h2> |
| 342 '''); |
| 343 |
| 344 docInheritance(type); |
| 345 |
| 346 docCode(type.span, getTypeComment(type)); |
| 347 docConstructors(type); |
| 348 docMembers(type); |
| 349 |
| 350 writeFooter(); |
| 351 endFile(); |
| 352 } |
| 353 |
| 354 /** Document the superclass, superinterfaces and default class of [Type]. */ |
| 355 docInheritance(Type type) { |
| 356 final isSubclass = (type.parent != null) && !type.parent.isObject; |
| 357 |
| 358 Type defaultType; |
| 359 if (type.definition is TypeDefinition) { |
| 360 TypeDefinition definition = type.definition; |
| 361 if (definition.defaultType != null) { |
| 362 defaultType = definition.defaultType.type; |
| 363 } |
| 364 } |
| 365 |
| 366 if (isSubclass || |
| 367 (type.interfaces != null && type.interfaces.length > 0) || |
| 368 (defaultType != null)) { |
| 369 writeln('<p>'); |
| 370 |
| 371 if (isSubclass) { |
| 372 write('Extends ${typeReference(type.parent)}. '); |
| 373 } |
| 374 |
| 375 if (type.interfaces != null && type.interfaces.length > 0) { |
| 376 var interfaceStr = joinWithCommas(map(type.interfaces, typeReference)); |
| 377 write('Implements ${interfaceStr}. '); |
| 378 } |
| 379 |
| 380 if (defaultType != null) { |
| 381 write('Has default class ${typeReference(defaultType)}.'); |
| 382 } |
| 383 } |
| 384 } |
| 385 |
| 386 /** Document the constructors for [Type], if any. */ |
| 387 docConstructors(Type type) { |
| 388 final names = type.constructors.getKeys().filter( |
| 389 (name) => !name.startsWith('_')); |
| 390 |
| 391 if (names.length > 0) { |
| 392 writeln('<h3>Constructors</h3>'); |
| 393 names.sort((x, y) => x.toUpperCase().compareTo(y.toUpperCase())); |
| 394 |
| 395 for (final name in names) { |
| 396 docMethod(type, type.constructors[name], constructorName: name); |
| 397 } |
| 398 } |
| 399 } |
| 400 |
| 401 void docMembers(Type type) { |
| 402 // Collect the different kinds of members. |
| 403 final methods = []; |
| 404 final fields = []; |
| 405 |
| 406 for (final member in orderByName(type.members)) { |
| 407 if (member.name.startsWith('_')) continue; |
| 408 |
| 409 if (member.isProperty) { |
| 410 if (member.canGet) methods.add(member.getter); |
| 411 if (member.canSet) methods.add(member.setter); |
| 412 } else if (member.isMethod) { |
| 413 methods.add(member); |
| 414 } else if (member.isField) { |
| 415 fields.add(member); |
| 416 } |
| 417 } |
| 418 |
| 419 if (methods.length > 0) { |
| 420 writeln('<h3>Methods</h3>'); |
| 421 for (final method in methods) docMethod(type, method); |
| 422 } |
| 423 |
| 424 if (fields.length > 0) { |
| 425 writeln('<h3>Fields</h3>'); |
| 426 for (final field in fields) docField(type, field); |
| 427 } |
| 428 } |
| 429 |
| 430 /** |
| 431 * Documents the [method] in type [type]. Handles all kinds of methods |
| 432 * including getters, setters, and constructors. |
| 433 */ |
| 434 docMethod(Type type, MethodMember method, [String constructorName = null]) { |
| 435 _totalMembers++; |
| 436 _currentMember = method; |
| 437 |
| 438 writeln('<div class="method"><h4 id="${memberAnchor(method)}">'); |
| 439 |
| 440 if (includeSource) { |
| 441 writeln('<span class="show-code">Code</span>'); |
| 442 } |
| 443 |
| 444 if (method.isStatic && !type.isTop) { |
| 445 write('static '); |
| 446 } |
| 447 |
| 448 if (method.isConstructor) { |
| 449 write(method.isConst ? 'const ' : 'new '); |
| 450 } |
| 451 |
| 452 if (constructorName == null) { |
| 453 annotateType(type, method.returnType); |
| 454 } |
| 455 |
| 456 // Translate specially-named methods: getters, setters, operators. |
| 457 var name = method.name; |
| 458 if (name.startsWith('get:')) { |
| 459 // Getter. |
| 460 name = 'get ${name.substring(4)}'; |
| 461 } else if (name.startsWith('set:')) { |
| 462 // Setter. |
| 463 name = 'set ${name.substring(4)}'; |
| 464 } else { |
| 465 // See if it's an operator. |
| 466 name = TokenKind.rawOperatorFromMethod(name); |
| 467 if (name == null) { |
| 468 name = method.name; |
| 469 } else { |
| 470 name = 'operator $name'; |
| 471 } |
| 472 } |
| 473 |
| 474 write('<strong>$name</strong>'); |
| 475 |
| 476 // Named constructors. |
| 477 if (constructorName != null && constructorName != '') { |
| 478 write('.'); |
| 479 write(constructorName); |
| 480 } |
| 481 |
| 482 docParamList(type, method); |
| 483 |
| 484 write(''' <a class="anchor-link" href="#${memberAnchor(method)}" |
| 485 title="Permalink to ${typeName(type)}.$name">#</a>'''); |
| 486 writeln('</h4>'); |
| 487 |
| 488 docCode(method.span, getMethodComment(method), showCode: true); |
| 489 |
| 490 writeln('</div>'); |
| 491 } |
| 492 |
| 493 /** Documents the field [field] of type [type]. */ |
| 494 docField(Type type, FieldMember field) { |
| 495 _totalMembers++; |
| 496 _currentMember = field; |
| 497 |
| 498 writeln('<div class="field"><h4 id="${memberAnchor(field)}">'); |
| 499 |
| 500 if (includeSource) { |
| 501 writeln('<span class="show-code">Code</span>'); |
| 502 } |
| 503 |
| 504 if (field.isStatic && !type.isTop) { |
| 505 write('static '); |
| 506 } |
| 507 |
| 508 if (field.isFinal) { |
| 509 write('final '); |
| 510 } else if (field.type.name == 'Dynamic') { |
| 511 write('var '); |
| 512 } |
| 513 |
| 514 annotateType(type, field.type); |
| 515 write( |
| 516 ''' |
| 517 <strong>${field.name}</strong> <a class="anchor-link" |
| 518 href="#${memberAnchor(field)}" |
| 519 title="Permalink to ${typeName(type)}.${field.name}">#</a> |
| 353 </h4> | 520 </h4> |
| 354 </div> | |
| 355 '''); | 521 '''); |
| 522 |
| 523 docCode(field.span, getFieldComment(field), showCode: true); |
| 524 writeln('</div>'); |
| 525 } |
| 526 |
| 527 docParamList(Type enclosingType, MethodMember member) { |
| 528 write('('); |
| 529 bool first = true; |
| 530 bool inOptionals = false; |
| 531 for (final parameter in member.parameters) { |
| 532 if (!first) write(', '); |
| 533 |
| 534 if (!inOptionals && parameter.isOptional) { |
| 535 write('['); |
| 536 inOptionals = true; |
| 537 } |
| 538 |
| 539 annotateType(enclosingType, parameter.type, parameter.name); |
| 540 |
| 541 // Show the default value for named optional parameters. |
| 542 if (parameter.isOptional && parameter.hasDefaultValue) { |
| 543 write(' = '); |
| 544 // TODO(rnystrom): Using the definition text here is a bit cheap. |
| 545 // We really should be pretty-printing the AST so that if you have: |
| 546 // foo([arg = 1 + /* comment */ 2]) |
| 547 // the docs should just show: |
| 548 // foo([arg = 1 + 2]) |
| 549 // For now, we'll assume you don't do that. |
| 550 write(parameter.definition.value.span.text); |
| 551 } |
| 552 |
| 553 first = false; |
| 554 } |
| 555 |
| 556 if (inOptionals) write(']'); |
| 557 write(')'); |
| 558 } |
| 559 |
| 560 /** |
| 561 * Documents the code contained within [span] with [comment]. If [showCode] |
| 562 * is `true` (and [includeSource] is set), also includes the source code. |
| 563 */ |
| 564 docCode(SourceSpan span, String comment, [bool showCode = false]) { |
| 565 writeln('<div class="doc">'); |
| 566 if (comment != null) { |
| 567 writeln(md.markdownToHtml(comment)); |
| 568 } |
| 569 |
| 570 if (includeSource && showCode) { |
| 571 writeln('<pre class="source">'); |
| 572 write(formatCode(span)); |
| 573 writeln('</pre>'); |
| 574 } |
| 575 |
| 576 writeln('</div>'); |
| 577 } |
| 578 |
| 579 /** Get the doc comment associated with the given type. */ |
| 580 String getTypeComment(Type type) => _comments.find(type.span); |
| 581 |
| 582 /** Get the doc comment associated with the given method. */ |
| 583 String getMethodComment(MethodMember method) => _comments.find(method.span); |
| 584 |
| 585 /** Get the doc comment associated with the given field. */ |
| 586 String getFieldComment(FieldMember field) => _comments.find(field.span); |
| 587 |
| 588 /** |
| 589 * Creates a hyperlink. Handles turning the [href] into an appropriate |
| 590 * relative path from the current file. |
| 591 */ |
| 592 String a(String href, String contents, [String class]) { |
| 593 final css = class == null ? '' : ' class="$class"'; |
| 594 return '<a href="${relativePath(href)}"$css>$contents</a>'; |
| 595 } |
| 596 |
| 597 /** |
| 598 * Writes a type annotation for the given type and (optional) parameter name. |
| 599 */ |
| 600 annotateType(Type enclosingType, Type type, [String paramName = null]) { |
| 601 // Don't bother explicitly displaying Dynamic. |
| 602 if (type.isVar) { |
| 603 if (paramName !== null) write(paramName); |
| 604 return; |
| 605 } |
| 606 |
| 607 // For parameters, handle non-typedefed function types. |
| 608 if (paramName !== null) { |
| 609 final call = type.getCallMethod(); |
| 610 if (call != null) { |
| 611 annotateType(enclosingType, call.returnType); |
| 612 write(paramName); |
| 613 |
| 614 docParamList(enclosingType, call); |
| 615 return; |
| 616 } |
| 617 } |
| 618 |
| 619 linkToType(enclosingType, type); |
| 620 |
| 621 write(' '); |
| 622 if (paramName !== null) write(paramName); |
| 623 } |
| 624 |
| 625 /** Writes a link to a human-friendly string representation for a type. */ |
| 626 linkToType(Type enclosingType, Type type) { |
| 627 if (type is ParameterType) { |
| 628 // If we're using a type parameter within the body of a generic class then |
| 629 // just link back up to the class. |
| 630 write(a(typeUrl(enclosingType), type.name)); |
| 631 return; |
| 632 } |
| 633 |
| 634 // Link to the type. |
| 635 // Use .genericType to avoid writing the <...> here. |
| 636 write(a(typeUrl(type), type.genericType.name)); |
| 637 |
| 638 // See if it's a generic type. |
| 639 if (type.isGeneric) { |
| 640 // TODO(rnystrom): This relies on a weird corner case of frog. Currently, |
| 641 // the only time we get into this case is when we have a "raw" generic |
| 642 // that's been instantiated with Dynamic for all type arguments. It's kind |
| 643 // of strange that frog works that way, but we take advantage of it to |
| 644 // show raw types without any type arguments. |
| 645 return; |
| 646 } |
| 647 |
| 648 // See if it's an instantiation of a generic type. |
| 649 final typeArgs = type.typeArgsInOrder; |
| 650 if (typeArgs != null) { |
| 651 write('<'); |
| 652 bool first = true; |
| 653 for (final arg in typeArgs) { |
| 654 if (!first) write(', '); |
| 655 first = false; |
| 656 linkToType(enclosingType, arg); |
| 657 } |
| 658 write('>'); |
| 659 } |
| 660 } |
| 661 |
| 662 /** Creates a linked cross reference to [type]. */ |
| 663 typeReference(Type type) { |
| 664 // TODO(rnystrom): Do we need to handle ParameterTypes here like |
| 665 // annotation() does? |
| 666 return a(typeUrl(type), typeName(type), class: 'crossref'); |
| 667 } |
| 668 |
| 669 /** Generates a human-friendly string representation for a type. */ |
| 670 typeName(Type type, [bool showBounds = false]) { |
| 671 // See if it's a generic type. |
| 672 if (type.isGeneric) { |
| 673 final typeParams = []; |
| 674 for (final typeParam in type.genericType.typeParameters) { |
| 675 if (showBounds && |
| 676 (typeParam.extendsType != null) && |
| 677 !typeParam.extendsType.isObject) { |
| 678 final bound = typeName(typeParam.extendsType, showBounds: true); |
| 679 typeParams.add('${typeParam.name} extends $bound'); |
| 680 } else { |
| 681 typeParams.add(typeParam.name); |
| 682 } |
| 683 } |
| 684 |
| 685 final params = Strings.join(typeParams, ', '); |
| 686 return '${type.name}<$params>'; |
| 687 } |
| 688 |
| 689 // See if it's an instantiation of a generic type. |
| 690 final typeArgs = type.typeArgsInOrder; |
| 691 if (typeArgs != null) { |
| 692 final args = Strings.join(map(typeArgs, (arg) => typeName(arg)), ', '); |
| 693 return '${type.genericType.name}<$args>'; |
| 694 } |
| 695 |
| 696 // Regular type. |
| 697 return type.name; |
| 698 } |
| 699 |
| 700 /** |
| 701 * Takes a string of Dart code and turns it into sanitized HTML. |
| 702 */ |
| 703 formatCode(SourceSpan span) { |
| 704 // Remove leading indentation to line up with first line. |
| 705 final column = getSpanColumn(span); |
| 706 final lines = span.text.split('\n'); |
| 707 // TODO(rnystrom): Dirty hack. |
| 708 for (final i = 1; i < lines.length; i++) { |
| 709 lines[i] = unindent(lines[i], column); |
| 710 } |
| 711 |
| 712 final code = Strings.join(lines, '\n'); |
| 713 |
| 714 // Syntax highlight. |
| 715 return classifySource(new SourceFile('', code)); |
| 716 } |
| 717 |
| 718 /** |
| 719 * This will be called whenever a doc comment hits a `[name]` in square |
| 720 * brackets. It will try to figure out what the name refers to and link or |
| 721 * style it appropriately. |
| 722 */ |
| 723 md.Node resolveNameReference(String name) { |
| 724 makeLink(String href) { |
| 725 final anchor = new md.Element.text('a', name); |
| 726 anchor.attributes['href'] = relativePath(href); |
| 727 anchor.attributes['class'] = 'crossref'; |
| 728 return anchor; |
| 729 } |
| 730 |
| 731 findMember(Type type) { |
| 732 final member = type.members[name]; |
| 733 if (member == null) return null; |
| 734 |
| 735 // Special case: if the member we've resolved is a property (i.e. it wraps |
| 736 // a getter and/or setter then *that* member itself won't be on the docs, |
| 737 // just the getter or setter will be. So pick one of those to link to. |
| 738 if (member.isProperty) { |
| 739 return member.canGet ? member.getter : member.setter; |
| 740 } |
| 741 |
| 742 return member; |
| 743 } |
| 744 |
| 745 // See if it's a parameter of the current method. |
| 746 if (_currentMember != null) { |
| 747 for (final parameter in _currentMember.parameters) { |
| 748 if (parameter.name == name) { |
| 749 final element = new md.Element.text('span', name); |
| 750 element.attributes['class'] = 'param'; |
| 751 return element; |
| 752 } |
| 753 } |
| 754 } |
| 755 |
| 756 // See if it's another member of the current type. |
| 757 if (_currentType != null) { |
| 758 final member = findMember(_currentType); |
| 759 if (member != null) { |
| 760 return makeLink(memberUrl(member)); |
| 761 } |
| 762 } |
| 763 |
| 764 // See if it's another type in the current library. |
| 765 if (_currentLibrary != null) { |
| 766 final type = _currentLibrary.types[name]; |
| 767 if (type != null) { |
| 768 return makeLink(typeUrl(type)); |
| 769 } |
| 770 |
| 771 // See if it's a top-level member in the current library. |
| 772 final member = findMember(_currentLibrary.topType); |
| 773 if (member != null) { |
| 774 return makeLink(memberUrl(member)); |
| 775 } |
| 776 } |
| 777 |
| 778 // TODO(rnystrom): Should also consider: |
| 779 // * Names imported by libraries this library imports. |
| 780 // * Type parameters of the enclosing type. |
| 781 |
| 782 return new md.Element.text('code', name); |
| 783 } |
| 784 |
| 785 // TODO(rnystrom): Move into SourceSpan? |
| 786 int getSpanColumn(SourceSpan span) { |
| 787 final line = span.file.getLine(span.start); |
| 788 return span.file.getColumn(line, span.start); |
| 356 } | 789 } |
| 357 } | 790 } |
| 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 |