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('files.dart'); |
| 25 #source('utils.dart'); |
24 | 26 |
25 /** Path to corePath library. */ | 27 /** Path to corePath library. */ |
26 final corePath = 'lib'; | 28 final corePath = 'lib'; |
27 | 29 |
28 /** Path to generate html files into. */ | 30 /** Path to generate html files into. */ |
29 final outdir = 'docs'; | 31 final outdir = 'docs'; |
30 | 32 |
31 /** Set to `true` to include the source code in the generated docs. */ | 33 /** Set to `true` to include the source code in the generated docs. */ |
32 bool includeSource = true; | 34 bool includeSource = false; |
33 | 35 |
34 /** Special comment position used to store the library-level doc comment. */ | 36 /** Special comment position used to store the library-level doc comment. */ |
35 final _libraryDoc = -1; | 37 final _libraryDoc = -1; |
36 | 38 |
37 /** The path to the file currently being written to, relative to [outdir]. */ | |
38 String _filePath; | |
39 | |
40 /** The file currently being written to. */ | |
41 StringBuffer _file; | |
42 | |
43 /** The library that we're currently generating docs for. */ | 39 /** The library that we're currently generating docs for. */ |
44 Library _currentLibrary; | 40 Library _currentLibrary; |
45 | 41 |
46 /** The type that we're currently generating docs for. */ | 42 /** The type that we're currently generating docs for. */ |
47 Type _currentType; | 43 Type _currentType; |
48 | 44 |
49 /** The member that we're currently generating docs for. */ | 45 /** The member that we're currently generating docs for. */ |
50 Member _currentMember; | 46 Member _currentMember; |
51 | 47 |
52 /** | 48 /** |
53 * The cached lookup-table to associate doc comments with spans. The outer map | 49 * The cached lookup-table to associate doc comments with spans. The outer map |
54 * is from filenames to doc comments in that file. The inner map maps from the | 50 * is from filenames to doc comments in that file. The inner map maps from the |
55 * token positions to doc comments. Each position is the starting offset of the | 51 * token positions to doc comments. Each position is the starting offset of the |
56 * next non-comment token *following* the doc comment. For example, the position | 52 * next non-comment token *following* the doc comment. For example, the position |
57 * for this comment would be the position of the "Map" token below. | 53 * for this comment would be the position of the "Map" token below. |
58 */ | 54 */ |
59 Map<String, Map<int, String>> _comments; | 55 Map<String, Map<int, String>> _comments; |
60 | 56 |
61 int _totalLibraries = 0; | 57 int _totalLibraries = 0; |
62 int _totalTypes = 0; | 58 int _totalTypes = 0; |
63 int _totalMembers = 0; | 59 int _totalMembers = 0; |
64 | 60 |
65 FileSystem files; | |
66 | |
67 /** | 61 /** |
68 * Run this from the `utils/dartdoc` directory. | 62 * Run this from the `utils/dartdoc` directory. |
69 */ | 63 */ |
70 void main() { | 64 void main() { |
71 // The entrypoint of the library to generate docs for. | 65 // The entrypoint of the library to generate docs for. |
72 final libPath = process.argv[2]; | 66 final libPath = process.argv[2]; |
73 | 67 |
74 // Parse the dartdoc options. | 68 // Parse the dartdoc options. |
75 for (int i = 3; i < process.argv.length; i++) { | 69 for (int i = 3; i < process.argv.length; i++) { |
76 final arg = process.argv[i]; | 70 final arg = process.argv[i]; |
(...skipping 18 matching lines...) Expand all Loading... |
95 md.setImplicitLinkResolver(resolveNameReference); | 89 md.setImplicitLinkResolver(resolveNameReference); |
96 | 90 |
97 final elapsed = time(() { | 91 final elapsed = time(() { |
98 initializeDartDoc(); | 92 initializeDartDoc(); |
99 | 93 |
100 initializeWorld(files); | 94 initializeWorld(files); |
101 | 95 |
102 world.processDartScript(libPath); | 96 world.processDartScript(libPath); |
103 world.resolveAll(); | 97 world.resolveAll(); |
104 | 98 |
105 // Clean the output directory. | |
106 if (files.fileExists(outdir)) { | |
107 files.removeDirectory(outdir, recursive: true); | |
108 } | |
109 files.createDirectory(outdir, recursive: true); | |
110 | |
111 // Copy over the static files. | |
112 for (final file in ['interact.js', 'styles.css']) { | |
113 copyStatic(file); | |
114 } | |
115 | |
116 // Generate the docs. | 99 // Generate the docs. |
117 for (final library in world.libraries.getValues()) { | 100 for (final library in world.libraries.getValues()) { |
118 docLibrary(library); | 101 docLibrary(library); |
119 } | 102 } |
120 | 103 |
121 docIndex(world.libraries.getValues()); | 104 docIndex(world.libraries.getValues()); |
122 }); | 105 }); |
123 | 106 |
124 print('Documented $_totalLibraries libraries, $_totalTypes types, and ' + | 107 print('Documented $_totalLibraries libraries, $_totalTypes types, and ' + |
125 '$_totalMembers members in ${elapsed}msec.'); | 108 '$_totalMembers members in ${elapsed}msec.'); |
126 } | 109 } |
127 | 110 |
128 void initializeDartDoc() { | 111 void initializeDartDoc() { |
129 _comments = <String, Map<int, String>>{}; | 112 _comments = <String, Map<int, String>>{}; |
130 } | 113 } |
131 | 114 |
132 /** Copies the static file at 'static/file' to the output directory. */ | |
133 copyStatic(String file) { | |
134 var contents = files.readAll(joinPaths('static', file)); | |
135 files.writeString(joinPaths(outdir, file), contents); | |
136 } | |
137 | |
138 num time(callback()) { | |
139 // Unlike world.withTiming, returns the elapsed time. | |
140 final watch = new Stopwatch(); | |
141 watch.start(); | |
142 callback(); | |
143 watch.stop(); | |
144 return watch.elapsedInMs(); | |
145 } | |
146 | |
147 startFile(String path) { | |
148 _filePath = path; | |
149 _file = new StringBuffer(); | |
150 } | |
151 | |
152 write(String s) { | |
153 _file.add(s); | |
154 } | |
155 | |
156 writeln(String s) { | |
157 write(s); | |
158 write('\n'); | |
159 } | |
160 | |
161 endFile() { | |
162 String outPath = '$outdir/$_filePath'; | |
163 files.createDirectory(dirname(outPath), recursive: true); | |
164 | |
165 world.files.writeString(outPath, _file.toString()); | |
166 _filePath = null; | |
167 _file = null; | |
168 } | |
169 | |
170 /** Turns a library name into something that's safe to use as a file name. */ | |
171 sanitize(String name) => name.replaceAll(':', '_').replaceAll('/', '_'); | |
172 | |
173 docIndex(List<Library> libraries) { | 115 docIndex(List<Library> libraries) { |
174 startFile('index.html'); | 116 startFile('index.html'); |
175 // TODO(rnystrom): Need to figure out what this should look like. | 117 // TODO(rnystrom): Need to figure out what this should look like. |
176 writeln( | 118 writeln( |
177 ''' | 119 ''' |
178 <html><head> | 120 <html><head> |
179 <title>Index</title> | 121 <title>Index</title> |
180 <link rel="stylesheet" type="text/css" href="styles.css" /> | 122 <link rel="stylesheet" type="text/css" href="styles.css" /> |
181 </head> | 123 </head> |
182 <body> | 124 <body> |
(...skipping 14 matching lines...) Expand all Loading... |
197 writeln( | 139 writeln( |
198 ''' | 140 ''' |
199 </ul> | 141 </ul> |
200 </div> | 142 </div> |
201 </body></html> | 143 </body></html> |
202 '''); | 144 '''); |
203 | 145 |
204 endFile(); | 146 endFile(); |
205 } | 147 } |
206 | 148 |
207 /** Returns the number of times [search] occurs in [text]. */ | |
208 int countOccurrences(String text, String search) { | |
209 int start = 0; | |
210 int count = 0; | |
211 | |
212 while (true) { | |
213 start = text.indexOf(search, start); | |
214 if (start == -1) break; | |
215 count++; | |
216 // Offsetting by needle length means overlapping needles are not counted. | |
217 start += search.length; | |
218 } | |
219 | |
220 return count; | |
221 } | |
222 | |
223 /** Repeats [text] [count] times, separated by [separator] if given. */ | |
224 String repeat(String text, int count, [String separator]) { | |
225 // TODO(rnystrom): Should be in corelib. | |
226 final buffer = new StringBuffer(); | |
227 for (int i = 0; i < count; i++) { | |
228 buffer.add(text); | |
229 if ((i < count - 1) && (separator !== null)) buffer.add(separator); | |
230 } | |
231 | |
232 return buffer.toString(); | |
233 } | |
234 | |
235 /** | |
236 * Converts [absolute] which is understood to be a full path from the root of | |
237 * the generated docs to one relative to the current file. | |
238 */ | |
239 String relativePath(String absolute) { | |
240 // TODO(rnystrom): Walks all the way up to root each time. Shouldn't do this | |
241 // if the paths overlap. | |
242 return repeat('../', countOccurrences(_filePath, '/')) + absolute; | |
243 } | |
244 | |
245 /** | |
246 * Creates a hyperlink. Handles turning the [href] into an appropriate relative | |
247 * path from the current file. | |
248 */ | |
249 String a(String href, String contents, [String class]) { | |
250 final css = class == null ? '' : ' class="$class"'; | |
251 return '<a href="${relativePath(href)}"$css>$contents</a>'; | |
252 } | |
253 | |
254 writeHeader(String title) { | 149 writeHeader(String title) { |
255 writeln( | 150 writeln( |
256 ''' | 151 ''' |
257 <!DOCTYPE html> | 152 <!DOCTYPE html> |
258 <html> | 153 <html> |
259 <head> | 154 <head> |
260 <meta charset="utf-8"> | 155 <meta charset="utf-8"> |
261 <title>$title</title> | 156 <title>$title</title> |
262 <link rel="stylesheet" type="text/css" | 157 <link rel="stylesheet" type="text/css" |
263 href="${relativePath('styles.css')}" /> | 158 href="${relativePath('styles.css')}" /> |
264 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,8
00" rel="stylesheet" type="text/css"> | 159 <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,8
00" rel="stylesheet" type="text/css"> |
265 <script src="${relativePath('interact.js')}"></script> | 160 <script src="${relativePath('interact.js')}"></script> |
266 </head> | 161 </head> |
267 <body> | 162 <body> |
268 <div class="content"> | 163 <div class="page"> |
269 '''); | 164 '''); |
| 165 docNavigation(); |
| 166 writeln('<div class="content">'); |
270 } | 167 } |
271 | 168 |
272 writeFooter() { | 169 writeFooter() { |
273 writeln( | 170 writeln( |
274 ''' | 171 ''' |
275 </div> | 172 </div> |
| 173 <div class="footer"</div> |
276 </body></html> | 174 </body></html> |
277 '''); | 175 '''); |
278 } | 176 } |
279 | 177 |
| 178 docNavigation() { |
| 179 writeln( |
| 180 ''' |
| 181 <div class="nav"> |
| 182 <h1>Libraries</h1> |
| 183 '''); |
| 184 |
| 185 for (final library in orderValuesByKeys(world.libraries)) { |
| 186 write('<h2><div class="icon-library"></div> '); |
| 187 |
| 188 if ((_currentLibrary == library) && (_currentType == null)) { |
| 189 write('<strong>${library.name}</strong>'); |
| 190 } else { |
| 191 write('${a(libraryUrl(library), library.name)}'); |
| 192 } |
| 193 write('</h2>'); |
| 194 |
| 195 final types = orderValuesByKeys(library.types); |
| 196 if (types.length > 0) { |
| 197 writeln('<ul>'); |
| 198 for (final type in types) { |
| 199 if (type.isTop) continue; |
| 200 if (type.name.startsWith('_')) continue; |
| 201 |
| 202 var icon = type.isClass ? 'icon-class' : 'icon-interface'; |
| 203 write('<li><div class="$icon"></div> '); |
| 204 |
| 205 if (_currentType == type) { |
| 206 write('<strong>${type.name}</strong>'); |
| 207 } else { |
| 208 write('${a(typeUrl(type), type.name)}'); |
| 209 } |
| 210 |
| 211 writeln('</li>'); |
| 212 } |
| 213 |
| 214 writeln('</ul>'); |
| 215 } |
| 216 } |
| 217 |
| 218 writeln('</div>'); |
| 219 } |
| 220 |
280 docLibrary(Library library) { | 221 docLibrary(Library library) { |
281 _totalLibraries++; | 222 _totalLibraries++; |
282 _currentLibrary = library; | 223 _currentLibrary = library; |
| 224 _currentType = null; |
283 | 225 |
284 startFile(libraryUrl(library)); | 226 startFile(libraryUrl(library)); |
285 writeHeader(library.name); | 227 writeHeader(library.name); |
286 writeln('<h1>Library <strong>${library.name}</strong></h1>'); | 228 writeln('<h1>Library <strong>${library.name}</strong></h1>'); |
287 | 229 |
288 // Look for a comment for the entire library. | 230 // Look for a comment for the entire library. |
289 final comment = findCommentInFile(library.baseSource, _libraryDoc); | 231 final comment = findCommentInFile(library.baseSource, _libraryDoc); |
290 if (comment != null) { | 232 if (comment != null) { |
291 final html = md.markdownToHtml(comment); | 233 final html = md.markdownToHtml(comment); |
292 writeln('<div class="doc">$html</div>'); | 234 writeln('<div class="doc">$html</div>'); |
293 } | 235 } |
294 | 236 |
295 // Document the top-level members. | 237 // Document the top-level members. |
296 docMembers(library.topType); | 238 docMembers(library.topType); |
297 | 239 |
298 // TODO(rnystrom): Link to types. | 240 // TODO(rnystrom): Link to types. |
299 writeln('<h3>Types</h3>'); | 241 writeln('<h3>Types</h3>'); |
300 | 242 |
301 for (final type in orderValuesByKeys(library.types)) { | 243 for (final type in orderValuesByKeys(library.types)) { |
302 if (type.isTop) continue; | 244 if (type.isTop) continue; |
| 245 if (type.name.startsWith('_')) continue; |
303 writeln( | 246 writeln( |
304 ''' | 247 ''' |
305 <div class="type"> | 248 <div class="type"> |
306 <h4> | 249 <h4> |
307 ${type.isClass ? "class" : "interface"} | 250 ${type.isClass ? "class" : "interface"} |
308 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} | 251 ${a(typeUrl(type), "<strong>${typeName(type)}</strong>")} |
309 </h4> | 252 </h4> |
310 </div> | 253 </div> |
311 '''); | 254 '''); |
312 } | 255 } |
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
462 if (method.isConstructor) { | 405 if (method.isConstructor) { |
463 write(method.isConst ? 'const ' : 'new '); | 406 write(method.isConst ? 'const ' : 'new '); |
464 } | 407 } |
465 | 408 |
466 if (constructorName == null) { | 409 if (constructorName == null) { |
467 write(annotation(type, method.returnType)); | 410 write(annotation(type, method.returnType)); |
468 } | 411 } |
469 | 412 |
470 // Translate specially-named methods: getters, setters, operators. | 413 // Translate specially-named methods: getters, setters, operators. |
471 var name = method.name; | 414 var name = method.name; |
472 if (name.startsWith('get\$')) { | 415 if (name.startsWith('get:')) { |
473 // Getter. | 416 // Getter. |
474 name = 'get ${name.substring(4)}'; | 417 name = 'get ${name.substring(4)}'; |
475 } else if (name.startsWith('set\$')) { | 418 } else if (name.startsWith('set:')) { |
476 // Setter. | 419 // Setter. |
477 name = 'set ${name.substring(4)}'; | 420 name = 'set ${name.substring(4)}'; |
478 } else { | 421 } else { |
479 // See if it's an operator. | 422 // See if it's an operator. |
480 name = TokenKind.rawOperatorFromMethod(name); | 423 name = TokenKind.rawOperatorFromMethod(name); |
481 if (name == null) { | 424 if (name == null) { |
482 name = method.name; | 425 name = method.name; |
483 } else { | 426 } else { |
484 name = 'operator $name'; | 427 name = 'operator $name'; |
485 } | 428 } |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
535 <strong>${field.name}</strong> <a class="anchor-link" | 478 <strong>${field.name}</strong> <a class="anchor-link" |
536 href="#${memberAnchor(field)}" | 479 href="#${memberAnchor(field)}" |
537 title="Permalink to ${typeName(type)}.${field.name}">#</a> | 480 title="Permalink to ${typeName(type)}.${field.name}">#</a> |
538 </h4> | 481 </h4> |
539 '''); | 482 '''); |
540 | 483 |
541 docCode(field.span, showCode: true); | 484 docCode(field.span, showCode: true); |
542 writeln('</div>'); | 485 writeln('</div>'); |
543 } | 486 } |
544 | 487 |
| 488 /** |
| 489 * Creates a hyperlink. Handles turning the [href] into an appropriate relative |
| 490 * path from the current file. |
| 491 */ |
| 492 String a(String href, String contents, [String class]) { |
| 493 final css = class == null ? '' : ' class="$class"'; |
| 494 return '<a href="${relativePath(href)}"$css>$contents</a>'; |
| 495 } |
| 496 |
545 /** Generates a human-friendly string representation for a type. */ | 497 /** Generates a human-friendly string representation for a type. */ |
546 typeName(Type type) { | 498 typeName(Type type) { |
547 // See if it's a generic type. | 499 // See if it's a generic type. |
548 if (type.isGeneric) { | 500 if (type.isGeneric) { |
549 final typeParams = type.genericType.typeParameters; | 501 final typeParams = type.genericType.typeParameters; |
550 final params = Strings.join(map(typeParams, (p) => p.name), ', '); | 502 final params = Strings.join(map(typeParams, (p) => p.name), ', '); |
551 return '${type.name}<$params>'; | 503 return '${type.name}<$params>'; |
552 } | 504 } |
553 | 505 |
554 // See if it's an instantiation of a generic type. | 506 // See if it's an instantiation of a generic type. |
555 final typeArgs = type.typeArgsInOrder; | 507 final typeArgs = type.typeArgsInOrder; |
556 if (typeArgs != null) { | 508 if (typeArgs != null) { |
557 final args = Strings.join(map(typeArgs, typeName), ', '); | 509 final args = Strings.join(map(typeArgs, typeName), ', '); |
558 return '${type.genericType.name}<$args>'; | 510 return '${type.genericType.name}<$args>'; |
559 } | 511 } |
560 | 512 |
561 // Regular type. | 513 // Regular type. |
562 return type.name; | 514 return type.name; |
563 } | 515 } |
564 | 516 |
565 /** Gets the URL to the documentation for [library]. */ | |
566 libraryUrl(Library library) { | |
567 return '${sanitize(library.name)}.html'; | |
568 } | |
569 | |
570 /** Gets the URL for the documentation for [type]. */ | |
571 typeUrl(Type type) { | |
572 // Always get the generic type to strip off any type parameters or arguments. | |
573 // If the type isn't generic, genericType returns `this`, so it works for | |
574 // non-generic types too. | |
575 return '${sanitize(type.library.name)}/${type.genericType.name}.html'; | |
576 } | |
577 | |
578 /** Gets the URL for the documentation for [member]. */ | |
579 memberUrl(Member member) { | |
580 return '${typeUrl(member.declaringType)}#${member.name}'; | |
581 } | |
582 | |
583 /** Gets the anchor id for the document for [member]. */ | |
584 memberAnchor(Member member) => '${member.name}'; | |
585 | |
586 /** Writes a linked cross reference to [type]. */ | 517 /** Writes a linked cross reference to [type]. */ |
587 typeReference(Type type) { | 518 typeReference(Type type) { |
588 // TODO(rnystrom): Do we need to handle ParameterTypes here like | 519 // TODO(rnystrom): Do we need to handle ParameterTypes here like |
589 // annotation() does? | 520 // annotation() does? |
590 return a(typeUrl(type), typeName(type), class: 'crossref'); | 521 return a(typeUrl(type), typeName(type), class: 'crossref'); |
591 } | 522 } |
592 | 523 |
593 /** | 524 /** |
594 * Creates a linked string for an optional type annotation. Returns an empty | 525 * Creates a linked string for an optional type annotation. Returns an empty |
595 * string if the type is Dynamic. | 526 * string if the type is Dynamic. |
(...skipping 177 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
773 // Syntax highlight. | 704 // Syntax highlight. |
774 return classifySource(new SourceFile('', code)); | 705 return classifySource(new SourceFile('', code)); |
775 } | 706 } |
776 | 707 |
777 // TODO(rnystrom): Move into SourceSpan? | 708 // TODO(rnystrom): Move into SourceSpan? |
778 int getSpanColumn(SourceSpan span) { | 709 int getSpanColumn(SourceSpan span) { |
779 final line = span.file.getLine(span.start); | 710 final line = span.file.getLine(span.start); |
780 return span.file.getColumn(line, span.start); | 711 return span.file.getColumn(line, span.start); |
781 } | 712 } |
782 | 713 |
783 /** Removes up to [indentation] leading whitespace characters from [text]. */ | |
784 unindent(String text, int indentation) { | |
785 var start; | |
786 for (start = 0; start < Math.min(indentation, text.length); start++) { | |
787 // Stop if we hit a non-whitespace character. | |
788 if (text[start] != ' ') break; | |
789 } | |
790 | |
791 return text.substring(start); | |
792 } | |
793 | |
794 /** | 714 /** |
795 * Pulls the raw text out of a doc comment (i.e. removes the comment | 715 * Pulls the raw text out of a doc comment (i.e. removes the comment |
796 * characters). | 716 * characters). |
797 */ | 717 */ |
798 stripComment(comment) { | 718 stripComment(comment) { |
799 StringBuffer buf = new StringBuffer(); | 719 StringBuffer buf = new StringBuffer(); |
800 | 720 |
801 for (final line in comment.split('\n')) { | 721 for (final line in comment.split('\n')) { |
802 line = line.trim(); | 722 line = line.trim(); |
803 if (line.startsWith('/**')) line = line.substring(3, line.length); | 723 if (line.startsWith('/**')) line = line.substring(3, line.length); |
804 if (line.endsWith('*/')) line = line.substring(0, line.length - 2); | 724 if (line.endsWith('*/')) line = line.substring(0, line.length - 2); |
805 line = line.trim(); | 725 line = line.trim(); |
806 if (line.startsWith('* ')) { | 726 if (line.startsWith('* ')) { |
807 line = line.substring(2, line.length); | 727 line = line.substring(2, line.length); |
808 } else if (line.startsWith('*')) { | 728 } else if (line.startsWith('*')) { |
809 line = line.substring(1, line.length); | 729 line = line.substring(1, line.length); |
810 } | 730 } |
811 | 731 |
812 buf.add(line); | 732 buf.add(line); |
813 buf.add('\n'); | 733 buf.add('\n'); |
814 } | 734 } |
815 | 735 |
816 return buf.toString(); | 736 return buf.toString(); |
817 } | 737 } |
OLD | NEW |