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 |