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