OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 // TODO(rnystrom): This is moving from a sample to being a real project. Right | |
6 // now, to try this out: | |
7 // 1. Compile interact.dart to JS: | |
8 // $ ./frogsh --out=docs/interact.js --compile-only docs/interact.dart | |
9 // 2. Run the doc generator: | |
10 // $ ./frogsh samples/doc.dart | |
11 // 3. Look at the results in frog/docs/ | |
12 | |
13 /** An awesome documentation generator. */ | |
14 #library('doc'); | |
15 | |
16 #import('../lang.dart'); | |
17 #import('../file_system_node.dart'); | |
18 #import('classify.dart'); | |
19 | |
20 /** Path to starting library or application. */ | |
21 // TODO(rnystrom): Make this a command-line arg. | |
22 final libPath = 'samples/doc.dart'; | |
23 | |
24 /** Path to corePath library. */ | |
25 final corePath = 'lib'; | |
26 | |
27 /** Path to generate html files into. */ | |
28 final outdir = './docs'; | |
29 | |
30 /** Special comment position used to store the library-level doc comment. */ | |
31 final _libraryDoc = -1; | |
32 | |
33 /** The file currently being written to. */ | |
34 StringBuffer _file; | |
35 | |
36 /** | |
37 * The cached lookup-table to associate doc comments with spans. The outer map | |
38 * is from filenames to doc comments in that file. The inner map maps from the | |
39 * token positions to doc comments. Each position is the starting offset of the | |
40 * next non-comment token *following* the doc comment. For example, the position | |
41 * for this comment would be the position of the "Map" token below. | |
42 */ | |
43 Map<String, Map<int, String>> _comments; | |
44 | |
45 // TODO(jimhug): This generates really ugly output with lots of holes. | |
46 | |
47 /** | |
48 * Run this from the frog/samples directory. Before running, you need | |
49 * to create a docs dir with 'mkdir docs' - since Dart currently doesn't | |
50 * support creating new directories. | |
51 */ | |
52 void main() { | |
53 // TODO(rnystrom): Get options and homedir like frog.dart does. | |
54 final files = new NodeFileSystem(); | |
55 parseOptions('.', [] /* args */, files); | |
56 | |
57 initializeWorld(files); | |
58 | |
59 world.withTiming('parsed', () { | |
60 world.processScript(libPath); | |
61 }); | |
62 | |
63 world.withTiming('resolved', () { | |
64 world.resolveAll(); | |
65 }); | |
66 | |
67 world.withTiming('generated docs', () { | |
68 _comments = <String, Map<int, String>>{}; | |
69 | |
70 for (var library in world.libraries.getValues()) { | |
71 docLibrary(library); | |
72 } | |
73 | |
74 docIndex(world.libraries.getValues()); | |
75 }); | |
76 } | |
77 | |
78 startFile() { | |
79 _file = new StringBuffer(); | |
80 } | |
81 | |
82 write(String s) { | |
83 _file.add(s); | |
84 } | |
85 | |
86 writeln(String s) { | |
87 write(s); | |
88 write('\n'); | |
89 } | |
90 | |
91 endFile(String outfile) { | |
92 world.files.writeString(outfile, _file.toString()); | |
93 _file = null; | |
94 } | |
95 | |
96 /** Turns a library name into something that's safe to use as a file name. */ | |
97 sanitize(String name) => name.replaceAll(':', '_').replaceAll('/', '_'); | |
98 | |
99 docIndex(List<Library> libraries) { | |
100 startFile(); | |
101 // TODO(rnystrom): Need to figure out what this should look like. | |
102 writeln( | |
103 ''' | |
104 <html><head> | |
105 <title>Index</title> | |
106 <link rel="stylesheet" type="text/css" href="styles.css" /> | |
107 </head> | |
108 <body> | |
109 <div class="content"> | |
110 <ul> | |
111 '''); | |
112 | |
113 var sorted = new List<Library>.from(libraries); | |
114 sorted.sort((a, b) => a.name.compareTo(b.name)); | |
115 | |
116 for (var library in sorted) { | |
117 writeln( | |
118 ''' | |
119 <li><a href="${sanitize(library.name)}.html"> | |
120 Library ${library.name}</a> | |
121 </li> | |
122 '''); | |
123 } | |
124 | |
125 writeln( | |
126 ''' | |
127 </ul> | |
128 </div> | |
129 </body></html> | |
130 '''); | |
131 | |
132 endFile('$outdir/index.html'); | |
133 } | |
134 | |
135 docLibrary(Library library) { | |
136 startFile(); | |
137 writeln( | |
138 ''' | |
139 <html> | |
140 <head> | |
141 <title>${library.name}</title> | |
142 <link rel="stylesheet" type="text/css" href="styles.css" /> | |
143 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,8
00" rel="stylesheet" type="text/css"> | |
144 <script src="interact.js"></script> | |
145 </head> | |
146 <body> | |
147 <div class="content"> | |
148 <h1>Library <strong>${library.name}</strong></h1> | |
149 '''); | |
150 | |
151 bool needsSeparator = false; | |
152 | |
153 // Look for a comment for the entire library. | |
154 final comment = findCommentInFile(library.baseSource, _libraryDoc); | |
155 if (comment != null) { | |
156 writeln('<div class="doc"><p>$comment</p></div>'); | |
157 needsSeparator = true; | |
158 } | |
159 | |
160 for (var type in library.types.getValues()) { | |
161 if (needsSeparator) writeln('<hr/>'); | |
162 if (docType(type)) needsSeparator = false; | |
163 } | |
164 | |
165 writeln( | |
166 ''' | |
167 </div> | |
168 </body></html> | |
169 '''); | |
170 | |
171 endFile('$outdir/${sanitize(library.name)}.html'); | |
172 } | |
173 | |
174 /** | |
175 * Documents [Type]. Handles top-level members if given an unnamed Type. | |
176 * Returns [:true:] if it wrote anything. | |
177 */ | |
178 bool docType(Type type) { | |
179 bool wroteSomething = false; | |
180 | |
181 if (type.name != null) { | |
182 write( | |
183 ''' | |
184 <h2 id="${type.name}"> | |
185 ${type.isClass ? "Class" : "Interface"} <strong>${type.name}</strong> | |
186 <a class="anchor-link" href="#${type.name}" | |
187 title="Permalink to ${type.name}">#</a> | |
188 </h2> | |
189 '''); | |
190 | |
191 docInheritance(type); | |
192 docCode(type.span); | |
193 docConstructors(type); | |
194 | |
195 wroteSomething = true; | |
196 } | |
197 | |
198 // Collect the different kinds of members. | |
199 var methods = []; | |
200 var fields = []; | |
201 | |
202 for (var member in orderValuesByKeys(type.members)) { | |
203 if (member.isMethod && | |
204 (member.definition != null) && | |
205 !member.name.startsWith('_')) { | |
206 methods.add(member); | |
207 } else if (member.isProperty) { | |
208 if (member.canGet) methods.add(member.getter); | |
209 if (member.canSet) methods.add(member.setter); | |
210 } else if (member.isField && !member.name.startsWith('_')) { | |
211 fields.add(member); | |
212 } | |
213 } | |
214 | |
215 if (methods.length > 0) { | |
216 writeln('<h3>Methods</h3>'); | |
217 for (var method in methods) docMethod(type.name, method); | |
218 } | |
219 | |
220 if (fields.length > 0) { | |
221 writeln('<h3>Fields</h3>'); | |
222 for (var field in fields) docField(type.name, field); | |
223 } | |
224 | |
225 return wroteSomething || methods.length > 0 || fields.length > 0; | |
226 } | |
227 | |
228 /** Document the superclass and superinterfaces of [Type]. */ | |
229 docInheritance(Type type) { | |
230 // Show the superclass and superinterface(s). | |
231 if ((type.parent != null) && (type.parent.isObject) || | |
232 (type.interfaces != null && type.interfaces.length > 0)) { | |
233 writeln('<p>'); | |
234 | |
235 if (type.parent != null) { | |
236 write('Extends ${typeRef(type.parent)}. '); | |
237 } | |
238 | |
239 if (type.interfaces != null) { | |
240 var interfaces = []; | |
241 switch (type.interfaces.length) { | |
242 case 0: | |
243 // Do nothing. | |
244 break; | |
245 | |
246 case 1: | |
247 write('Implements ${typeRef(type.interfaces[0])}.'); | |
248 break; | |
249 | |
250 case 2: | |
251 write('''Implements ${typeRef(type.interfaces[0])} and | |
252 ${typeRef(type.interfaces[1])}.'''); | |
253 break; | |
254 | |
255 default: | |
256 write('Implements '); | |
257 for (var i = 0; i < type.interfaces.length; i++) { | |
258 write('${typeRef(type.interfaces[i])}'); | |
259 if (i < type.interfaces.length - 1) { | |
260 write(', '); | |
261 } else { | |
262 write(' and '); | |
263 } | |
264 } | |
265 write('.'); | |
266 break; | |
267 } | |
268 } | |
269 } | |
270 } | |
271 | |
272 /** Document the constructors for [Type], if any. */ | |
273 docConstructors(Type type) { | |
274 if (type.constructors.length > 0) { | |
275 writeln('<h3>Constructors</h3>'); | |
276 for (var name in type.constructors.getKeys()) { | |
277 var constructor = type.constructors[name]; | |
278 docMethod(type.name, constructor, namedConstructor: name); | |
279 } | |
280 } | |
281 } | |
282 | |
283 /** | |
284 * Documents the [method] in a type named [typeName]. Handles all kinds of | |
285 * methods including getters, setters, and constructors. | |
286 */ | |
287 docMethod(String typeName, MethodMember method, | |
288 [String namedConstructor = null]) { | |
289 writeln( | |
290 ''' | |
291 <div class="method"><h4 id="$typeName.${method.name}"> | |
292 <span class="show-code">Code</span> | |
293 '''); | |
294 | |
295 // A null typeName means it's a top-level definition which is implicitly | |
296 // static so doesn't need to annotate it. | |
297 if (method.isStatic && (typeName != null)) { | |
298 write('static '); | |
299 } | |
300 | |
301 if (method.isConstructor) { | |
302 write(method.isConst ? 'const ' : 'new '); | |
303 } | |
304 | |
305 if (namedConstructor == null) { | |
306 write(optionalTypeRef(method.returnType)); | |
307 } | |
308 | |
309 // Translate specially-named methods: getters, setters, operators. | |
310 var name = method.name; | |
311 if (name.startsWith('get\$')) { | |
312 // Getter. | |
313 name = 'get ${name.substring(4)}'; | |
314 } else if (name.startsWith('set\$')) { | |
315 // Setter. | |
316 name = 'set ${name.substring(4)}'; | |
317 } else { | |
318 // See if it's an operator. | |
319 name = TokenKind.rawOperatorFromMethod(name); | |
320 if (name == null) { | |
321 name = method.name; | |
322 } else { | |
323 name = 'operator $name'; | |
324 } | |
325 } | |
326 | |
327 write('<strong>$name</strong>'); | |
328 | |
329 // Named constructors. | |
330 if (namedConstructor != null && namedConstructor != '') { | |
331 write('.'); | |
332 write(namedConstructor); | |
333 } | |
334 | |
335 write('('); | |
336 var paramList = []; | |
337 if (method.parameters == null) print(method.name); | |
338 for (var p in method.parameters) { | |
339 paramList.add('${optionalTypeRef(p.type)}${p.name}'); | |
340 } | |
341 write(Strings.join(paramList, ", ")); | |
342 write(')'); | |
343 | |
344 write(''' <a class="anchor-link" href="#$typeName.${method.name}" | |
345 title="Permalink to $typeName.$name">#</a>'''); | |
346 writeln('</h4>'); | |
347 | |
348 docCode(method.span, showCode: true); | |
349 | |
350 writeln('</div>'); | |
351 } | |
352 | |
353 /** Documents the field [field] in a type named [typeName]. */ | |
354 docField(String typeName, FieldMember field) { | |
355 writeln( | |
356 ''' | |
357 <div class="field"><h4 id="$typeName.${field.name}"> | |
358 <span class="show-code">Code</span> | |
359 '''); | |
360 | |
361 // A null typeName means it's a top-level definition which is implicitly | |
362 // static so doesn't need to annotate it. | |
363 if (field.isStatic && (typeName != null)) { | |
364 write('static '); | |
365 } | |
366 | |
367 if (field.isFinal) { | |
368 write('final '); | |
369 } else if (field.type.name == 'Dynamic') { | |
370 write('var '); | |
371 } | |
372 | |
373 write(optionalTypeRef(field.type)); | |
374 write( | |
375 ''' | |
376 <strong>${field.name}</strong> <a class="anchor-link" | |
377 href="#$typeName.${field.name}" | |
378 title="Permalink to $typeName.${field.name}">#</a> | |
379 </h4> | |
380 '''); | |
381 | |
382 docCode(field.span, showCode: true); | |
383 writeln('</div>'); | |
384 } | |
385 | |
386 /** | |
387 * Writes a type annotation for [type]. Will hyperlink it to that type's | |
388 * documentation if possible. | |
389 */ | |
390 typeRef(Type type) { | |
391 if (type.library != null) { | |
392 var library = sanitize(type.library.name); | |
393 return '<a href="${library}.html#${type.name}">${type.name}</a>'; | |
394 } else { | |
395 return type.name; | |
396 } | |
397 } | |
398 | |
399 /** | |
400 * Creates a linked string for an optional type annotation. Returns an empty | |
401 * string if the type is Dynamic. | |
402 */ | |
403 optionalTypeRef(Type type) { | |
404 if (type.name == 'Dynamic') { | |
405 return ''; | |
406 } else { | |
407 return typeRef(type) + ' '; | |
408 } | |
409 } | |
410 | |
411 /** | |
412 * Documents the code contained within [span]. Will include the previous | |
413 * Dartdoc associated with that span if found, and will include the syntax | |
414 * highlighted code itself if desired. | |
415 */ | |
416 docCode(SourceSpan span, [bool showCode = false]) { | |
417 if (span == null) return; | |
418 | |
419 writeln('<div class="doc">'); | |
420 var comment = findComment(span); | |
421 if (comment != null) { | |
422 writeln('<p>$comment</p>'); | |
423 } | |
424 | |
425 if (showCode) { | |
426 writeln('<pre class="source">'); | |
427 write(formatCode(span)); | |
428 writeln('</pre>'); | |
429 } | |
430 | |
431 writeln('</div>'); | |
432 } | |
433 | |
434 /** Finds the doc comment preceding the given source span, if there is one. */ | |
435 findComment(SourceSpan span) => findCommentInFile(span.file, span.start); | |
436 | |
437 /** Finds the doc comment preceding the given source span, if there is one. */ | |
438 findCommentInFile(SourceFile file, int position) { | |
439 // Get the doc comments for this file. | |
440 var fileComments = _comments.putIfAbsent(file.filename, | |
441 () => parseDocComments(file)); | |
442 | |
443 return fileComments[position]; | |
444 } | |
445 | |
446 parseDocComments(SourceFile file) { | |
447 var comments = <int, String>{}; | |
448 | |
449 var tokenizer = new Tokenizer(file, false); | |
450 var lastComment = null; | |
451 | |
452 while (true) { | |
453 var token = tokenizer.next(); | |
454 if (token.kind == TokenKind.END_OF_FILE) break; | |
455 | |
456 if (token.kind == TokenKind.COMMENT) { | |
457 var text = token.text; | |
458 if (text.startsWith('/**')) { | |
459 // Remember that we've encountered a doc comment. | |
460 lastComment = stripComment(token.text); | |
461 } | |
462 } else if (token.kind == TokenKind.WHITESPACE) { | |
463 // Ignore whitespace tokens. | |
464 } else if (token.kind == TokenKind.HASH) { | |
465 // Look for #library() to find the library comment. | |
466 var next = tokenizer.next(); | |
467 if ((lastComment != null) && (next.kind == TokenKind.LIBRARY)) { | |
468 comments[_libraryDoc] = lastComment; | |
469 lastComment = null; | |
470 } | |
471 } else { | |
472 if (lastComment != null) { | |
473 // We haven't attached the last doc comment to something yet, so stick | |
474 // it to this token. | |
475 comments[token.start] = lastComment; | |
476 lastComment = null; | |
477 } | |
478 } | |
479 } | |
480 | |
481 return comments; | |
482 } | |
483 | |
484 /** | |
485 * Takes a string of Dart code and turns it into sanitized HTML. | |
486 */ | |
487 formatCode(SourceSpan span) { | |
488 // Remove leading indentation to line up with first line. | |
489 var column = getSpanColumn(span); | |
490 var lines = span.text.split('\n'); | |
491 // TODO(rnystrom): Dirty hack. | |
492 for (int i = 1; i < lines.length; i++) { | |
493 lines[i] = unindent(lines[i], column); | |
494 } | |
495 | |
496 var code = Strings.join(lines, '\n'); | |
497 | |
498 // Syntax highlight. | |
499 return classifySource(new SourceFile('', code)); | |
500 } | |
501 | |
502 // TODO(rnystrom): Move into SourceSpan? | |
503 int getSpanColumn(SourceSpan span) { | |
504 var line = span.file.getLine(span.start); | |
505 return span.file.getColumn(line, span.start); | |
506 } | |
507 | |
508 /** Removes up to [indentation] leading whitespace characters from [text]. */ | |
509 unindent(String text, int indentation) { | |
510 var start; | |
511 for (start = 0; start < Math.min(indentation, text.length); start++) { | |
512 // Stop if we hit a non-whitespace character. | |
513 if (text[start] != ' ') break; | |
514 } | |
515 | |
516 return text.substring(start); | |
517 } | |
518 | |
519 /** | |
520 * Pulls the raw text out of a doc comment (i.e. removes the comment | |
521 * characters. | |
522 */ | |
523 // TODO(rnystrom): Should handle [name] and [:code:] in comments. Should also | |
524 // break empty lines into multiple paragraphs. Other formatting? | |
525 // See dart/compiler/java/com/google/dart/compiler/backend/doc for ideas. | |
526 // (/DartDocumentationVisitor.java#180) | |
527 stripComment(comment) { | |
528 StringBuffer buf = new StringBuffer(); | |
529 | |
530 for (var line in comment.split('\n')) { | |
531 line = line.trim(); | |
532 if (line.startsWith('/**')) line = line.substring(3, line.length); | |
533 if (line.endsWith('*/')) line = line.substring(0, line.length-2); | |
534 line = line.trim(); | |
535 while (line.startsWith('*')) line = line.substring(1, line.length); | |
536 line = line.trim(); | |
537 buf.add(line); | |
538 buf.add(' '); | |
539 } | |
540 | |
541 return buf.toString(); | |
542 } | |
OLD | NEW |