| 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 |