OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, 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 * Tools for code generation. | 6 * Tools for code generation. |
7 */ | 7 */ |
8 library codegen.tools; | 8 library codegen.tools; |
9 | 9 |
10 import 'dart:io'; | 10 import 'dart:io'; |
11 | 11 |
12 import 'package:html5lib/dom.dart' as dom; | 12 import 'package:html5lib/dom.dart' as dom; |
13 import 'package:path/path.dart'; | 13 import 'package:path/path.dart'; |
14 | 14 |
| 15 import 'html_tools.dart'; |
15 import 'text_formatter.dart'; | 16 import 'text_formatter.dart'; |
16 import 'html_tools.dart'; | 17 |
| 18 final RegExp trailingWhitespaceRegExp = new RegExp(r' +$', multiLine: true); |
17 | 19 |
18 /** | 20 /** |
19 * Join the given strings using camelCase. If [doCapitalize] is true, the first | 21 * Join the given strings using camelCase. If [doCapitalize] is true, the first |
20 * part will be capitalized as well. | 22 * part will be capitalized as well. |
21 */ | 23 */ |
22 String camelJoin(List<String> parts, {bool doCapitalize: false}) { | 24 String camelJoin(List<String> parts, {bool doCapitalize: false}) { |
23 List<String> upcasedParts = <String>[]; | 25 List<String> upcasedParts = <String>[]; |
24 for (int i = 0; i < parts.length; i++) { | 26 for (int i = 0; i < parts.length; i++) { |
25 if (i == 0 && !doCapitalize) { | 27 if (i == 0 && !doCapitalize) { |
26 upcasedParts.add(parts[i]); | 28 upcasedParts.add(parts[i]); |
27 } else { | 29 } else { |
28 upcasedParts.add(capitalize(parts[i])); | 30 upcasedParts.add(capitalize(parts[i])); |
29 } | 31 } |
30 } | 32 } |
31 return upcasedParts.join(); | 33 return upcasedParts.join(); |
32 } | 34 } |
33 | 35 |
34 /** | 36 /** |
35 * Capitalize and return the passed String. | 37 * Capitalize and return the passed String. |
36 */ | 38 */ |
37 String capitalize(String string) { | 39 String capitalize(String string) { |
38 return string[0].toUpperCase() + string.substring(1); | 40 return string[0].toUpperCase() + string.substring(1); |
39 } | 41 } |
40 | 42 |
41 final RegExp trailingWhitespaceRegExp = new RegExp(r' +$', multiLine: true); | 43 /** |
| 44 * Type of functions used to compute the contents of a set of generated files. |
| 45 */ |
| 46 typedef Map<String, FileContentsComputer> DirectoryContentsComputer(); |
| 47 |
| 48 /** |
| 49 * Type of functions used to compute the contents of a generated file. |
| 50 */ |
| 51 typedef String FileContentsComputer(); |
42 | 52 |
43 /** | 53 /** |
44 * Mixin class for generating code. | 54 * Mixin class for generating code. |
45 */ | 55 */ |
46 class CodeGenerator { | 56 class CodeGenerator { |
47 _CodeGeneratorState _state; | 57 _CodeGeneratorState _state; |
48 | 58 |
49 /** | 59 /** |
| 60 * Measure the width of the current indentation level. |
| 61 */ |
| 62 int get indentWidth => _state.nextIndent.length; |
| 63 |
| 64 /** |
50 * Execute [callback], collecting any code that is output using [write] | 65 * Execute [callback], collecting any code that is output using [write] |
51 * or [writeln], and return the result as a string. | 66 * or [writeln], and return the result as a string. |
52 */ | 67 */ |
53 String collectCode(void callback()) { | 68 String collectCode(void callback()) { |
54 _CodeGeneratorState oldState = _state; | 69 _CodeGeneratorState oldState = _state; |
55 try { | 70 try { |
56 _state = new _CodeGeneratorState(); | 71 _state = new _CodeGeneratorState(); |
57 callback(); | 72 callback(); |
58 return _state.buffer.toString().replaceAll(trailingWhitespaceRegExp, ''); | 73 return _state.buffer.toString().replaceAll(trailingWhitespaceRegExp, ''); |
59 } finally { | 74 } finally { |
60 _state = oldState; | 75 _state = oldState; |
61 } | 76 } |
62 } | 77 } |
63 | 78 |
64 /** | 79 /** |
65 * Output text without ending the current line. | 80 * Generate a doc comment based on the HTML in [docs]. |
| 81 * |
| 82 * If [javadocStyle] is true, then the output is compatable with Javadoc, |
| 83 * which understands certain HTML constructs. |
66 */ | 84 */ |
67 void write(Object obj) { | 85 void docComment(List<dom.Node> docs, {int width: 79, bool javadocStyle: |
68 _state.write(obj.toString()); | 86 false}) { |
| 87 if (containsOnlyWhitespace(docs)) { |
| 88 return; |
| 89 } |
| 90 writeln('/**'); |
| 91 indentBy(' * ', () { |
| 92 write(nodesToText(docs, width - _state.indent.length, javadocStyle)); |
| 93 }); |
| 94 writeln(' */'); |
69 } | 95 } |
70 | 96 |
71 /** | 97 /** |
72 * Output text, ending the current line. | |
73 */ | |
74 void writeln([Object obj = '']) { | |
75 _state.write('$obj\n'); | |
76 } | |
77 | |
78 /** | |
79 * Execute [callback], indenting any code it outputs by two spaces. | 98 * Execute [callback], indenting any code it outputs by two spaces. |
80 */ | 99 */ |
81 void indent(void callback()) => indentSpecial(' ', ' ', callback); | 100 void indent(void callback()) => indentSpecial(' ', ' ', callback); |
82 | 101 |
83 /** | 102 /** |
84 * Execute [callback], using [additionalIndent] to indent any code it outputs. | 103 * Execute [callback], using [additionalIndent] to indent any code it outputs. |
85 */ | 104 */ |
86 void indentBy(String additionalIndent, void callback()) => | 105 void indentBy(String additionalIndent, void callback()) => |
87 indentSpecial(additionalIndent, additionalIndent, callback); | 106 indentSpecial(additionalIndent, additionalIndent, callback); |
88 | 107 |
89 /** | 108 /** |
90 * Execute [callback], using [additionalIndent] to indent any code it outputs. | 109 * Execute [callback], using [additionalIndent] to indent any code it outputs. |
91 * The first line of output is indented by [firstAdditionalIndent] instead of | 110 * The first line of output is indented by [firstAdditionalIndent] instead of |
92 * [additionalIndent]. | 111 * [additionalIndent]. |
93 */ | 112 */ |
94 void indentSpecial(String firstAdditionalIndent, String additionalIndent, void | 113 void indentSpecial(String firstAdditionalIndent, String additionalIndent, void |
95 callback()) { | 114 callback()) { |
96 String oldNextIndent = _state.nextIndent; | 115 String oldNextIndent = _state.nextIndent; |
97 String oldIndent = _state.indent; | 116 String oldIndent = _state.indent; |
98 try { | 117 try { |
99 _state.nextIndent += firstAdditionalIndent; | 118 _state.nextIndent += firstAdditionalIndent; |
100 _state.indent += additionalIndent; | 119 _state.indent += additionalIndent; |
101 callback(); | 120 callback(); |
102 } finally { | 121 } finally { |
103 _state.nextIndent = oldNextIndent; | 122 _state.nextIndent = oldNextIndent; |
104 _state.indent = oldIndent; | 123 _state.indent = oldIndent; |
105 } | 124 } |
106 } | 125 } |
107 | 126 |
108 /** | |
109 * Measure the width of the current indentation level. | |
110 */ | |
111 int get indentWidth => _state.nextIndent.length; | |
112 | |
113 /** | |
114 * Generate a doc comment based on the HTML in [docs]. | |
115 * | |
116 * If [javadocStyle] is true, then the output is compatable with Javadoc, | |
117 * which understands certain HTML constructs. | |
118 */ | |
119 void docComment(List<dom.Node> docs, {int width: 79, bool javadocStyle: | |
120 false}) { | |
121 if (containsOnlyWhitespace(docs)) { | |
122 return; | |
123 } | |
124 writeln('/**'); | |
125 indentBy(' * ', () { | |
126 write(nodesToText(docs, width - _state.indent.length, javadocStyle)); | |
127 }); | |
128 writeln(' */'); | |
129 } | |
130 | |
131 void outputHeader({bool javaStyle: false}) { | 127 void outputHeader({bool javaStyle: false}) { |
132 String header; | 128 String header; |
133 if (javaStyle) { | 129 if (javaStyle) { |
134 header = ''' | 130 header = ''' |
135 /* | 131 /* |
136 * Copyright (c) 2014, the Dart project authors. | 132 * Copyright (c) 2014, the Dart project authors. |
137 * | 133 * |
138 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not u
se this file except | 134 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not u
se this file except |
139 * in compliance with the License. You may obtain a copy of the License at | 135 * in compliance with the License. You may obtain a copy of the License at |
140 * | 136 * |
(...skipping 13 matching lines...) Expand all Loading... |
154 // for details. All rights reserved. Use of this source code is governed by a | 150 // for details. All rights reserved. Use of this source code is governed by a |
155 // BSD-style license that can be found in the LICENSE file. | 151 // BSD-style license that can be found in the LICENSE file. |
156 // | 152 // |
157 // This file has been automatically generated. Please do not edit it manually. | 153 // This file has been automatically generated. Please do not edit it manually. |
158 // To regenerate the file, use the script | 154 // To regenerate the file, use the script |
159 // "pkg/analysis_server/tool/spec/generate_files". | 155 // "pkg/analysis_server/tool/spec/generate_files". |
160 '''; | 156 '''; |
161 } | 157 } |
162 writeln(header.trim()); | 158 writeln(header.trim()); |
163 } | 159 } |
164 } | |
165 | |
166 /** | |
167 * State used by [CodeGenerator]. | |
168 */ | |
169 class _CodeGeneratorState { | |
170 StringBuffer buffer = new StringBuffer(); | |
171 String nextIndent = ''; | |
172 String indent = ''; | |
173 bool indentNeeded = true; | |
174 | |
175 void write(String text) { | |
176 List<String> lines = text.split('\n'); | |
177 for (int i = 0; i < lines.length; i++) { | |
178 if (i == lines.length - 1 && lines[i].isEmpty) { | |
179 break; | |
180 } | |
181 if (indentNeeded) { | |
182 buffer.write(nextIndent); | |
183 nextIndent = indent; | |
184 } | |
185 indentNeeded = false; | |
186 buffer.write(lines[i]); | |
187 if (i != lines.length - 1) { | |
188 buffer.writeln(); | |
189 indentNeeded = true; | |
190 } | |
191 } | |
192 } | |
193 } | |
194 | |
195 /** | |
196 * Mixin class for generating HTML representations of code that are suitable | |
197 * for enclosing inside a <pre> element. | |
198 */ | |
199 abstract class HtmlCodeGenerator { | |
200 _HtmlCodeGeneratorState _state; | |
201 | |
202 /** | |
203 * Execute [callback], collecting any code that is output using [write], | |
204 * [writeln], [add], or [addAll], and return the result as a list of DOM | |
205 * nodes. | |
206 */ | |
207 List<dom.Node> collectHtml(void callback()) { | |
208 _HtmlCodeGeneratorState oldState = _state; | |
209 try { | |
210 _state = new _HtmlCodeGeneratorState(); | |
211 if (callback != null) { | |
212 callback(); | |
213 } | |
214 return _state.buffer; | |
215 } finally { | |
216 _state = oldState; | |
217 } | |
218 } | |
219 | |
220 /** | |
221 * Add the given [node] to the HTML output. | |
222 */ | |
223 void add(dom.Node node) { | |
224 _state.add(node); | |
225 } | |
226 | |
227 /** | |
228 * Add the given [nodes] to the HTML output. | |
229 */ | |
230 void addAll(Iterable<dom.Node> nodes) { | |
231 for (dom.Node node in nodes) { | |
232 _state.add(node); | |
233 } | |
234 } | |
235 | 160 |
236 /** | 161 /** |
237 * Output text without ending the current line. | 162 * Output text without ending the current line. |
238 */ | 163 */ |
239 void write(Object obj) { | 164 void write(Object obj) { |
240 _state.write(obj.toString()); | 165 _state.write(obj.toString()); |
241 } | 166 } |
242 | 167 |
243 /** | 168 /** |
244 * Output text, ending the current line. | 169 * Output text, ending the current line. |
245 */ | 170 */ |
246 void writeln([Object obj = '']) { | 171 void writeln([Object obj = '']) { |
247 _state.write('$obj\n'); | 172 _state.write('$obj\n'); |
248 } | 173 } |
249 | |
250 /** | |
251 * Execute [callback], indenting any code it outputs by two spaces. | |
252 */ | |
253 void indent(void callback()) { | |
254 String oldIndent = _state.indent; | |
255 try { | |
256 _state.indent += ' '; | |
257 callback(); | |
258 } finally { | |
259 _state.indent = oldIndent; | |
260 } | |
261 } | |
262 | |
263 /** | |
264 * Execute [callback], wrapping its output in an element with the given | |
265 * [name] and [attributes]. | |
266 */ | |
267 void element(String name, Map<String, String> attributes, [void callback()]) { | |
268 add(makeElement(name, attributes, collectHtml(callback))); | |
269 } | |
270 } | 174 } |
271 | 175 |
272 /** | |
273 * State used by [HtmlCodeGenerator]. | |
274 */ | |
275 class _HtmlCodeGeneratorState { | |
276 List<dom.Node> buffer = <dom.Node>[]; | |
277 String indent = ''; | |
278 bool indentNeeded = true; | |
279 | |
280 void add(dom.Node node) { | |
281 if (node is dom.Text) { | |
282 write(node.text); | |
283 } else { | |
284 buffer.add(node); | |
285 } | |
286 } | |
287 | |
288 void write(String text) { | |
289 if (text.isEmpty) { | |
290 return; | |
291 } | |
292 if (indentNeeded) { | |
293 buffer.add(new dom.Text(indent)); | |
294 } | |
295 List<String> lines = text.split('\n'); | |
296 if (lines.last.isEmpty) { | |
297 lines.removeLast(); | |
298 buffer.add(new dom.Text(lines.join('\n$indent') + '\n')); | |
299 indentNeeded = true; | |
300 } else { | |
301 buffer.add(new dom.Text(lines.join('\n$indent'))); | |
302 indentNeeded = false; | |
303 } | |
304 } | |
305 } | |
306 | |
307 /** | |
308 * Type of functions used to compute the contents of a generated file. | |
309 */ | |
310 typedef String FileContentsComputer(); | |
311 | |
312 /** | |
313 * Type of functions used to compute the contents of a set of generated files. | |
314 */ | |
315 typedef Map<String, FileContentsComputer> DirectoryContentsComputer(); | |
316 | |
317 abstract class GeneratedContent { | 176 abstract class GeneratedContent { |
318 FileSystemEntity get outputFile; | 177 FileSystemEntity get outputFile; |
319 bool check(); | 178 bool check(); |
320 void generate(); | 179 void generate(); |
321 } | 180 } |
322 | 181 |
323 /** | 182 /** |
| 183 * Class representing a single output directory (either generated code or |
| 184 * generated HTML). No other content should exisit in the directory. |
| 185 */ |
| 186 class GeneratedDirectory extends GeneratedContent { |
| 187 |
| 188 /** |
| 189 * The path to the directory that will have the generated content. |
| 190 */ |
| 191 final String outputDirPath; |
| 192 |
| 193 /** |
| 194 * Callback function which computes the directory contents. |
| 195 */ |
| 196 final DirectoryContentsComputer directoryContentsComputer; |
| 197 |
| 198 GeneratedDirectory(this.outputDirPath, this.directoryContentsComputer); |
| 199 |
| 200 /** |
| 201 * Get a Directory object representing the output directory. |
| 202 */ |
| 203 Directory get outputFile => |
| 204 new Directory(joinAll(posix.split(outputDirPath))); |
| 205 |
| 206 /** |
| 207 * Check whether the directory has the correct contents, and return true if it |
| 208 * does. |
| 209 */ |
| 210 @override |
| 211 bool check() { |
| 212 Map<String, FileContentsComputer> map = directoryContentsComputer(); |
| 213 try { |
| 214 map.forEach((String file, FileContentsComputer fileContentsComputer) { |
| 215 String expectedContents = fileContentsComputer(); |
| 216 File outputFile = |
| 217 new File(joinAll(posix.split(posix.join(outputDirPath, file)))); |
| 218 if (expectedContents != outputFile.readAsStringSync()) { |
| 219 return false; |
| 220 } |
| 221 }); |
| 222 int nonHiddenFileCount = 0; |
| 223 outputFile.listSync( |
| 224 recursive: false, |
| 225 followLinks: false).forEach((FileSystemEntity fileSystemEntity) { |
| 226 if (fileSystemEntity is File && |
| 227 !basename(fileSystemEntity.path).startsWith('.')) { |
| 228 nonHiddenFileCount++; |
| 229 } |
| 230 }); |
| 231 if (nonHiddenFileCount != map.length) { |
| 232 // The number of files generated doesn't match the number we expected to |
| 233 // generate. |
| 234 return false; |
| 235 } |
| 236 } catch (e) { |
| 237 // There was a problem reading the file (most likely because it didn't |
| 238 // exist). Treat that the same as if the file doesn't have the expected |
| 239 // contents. |
| 240 return false; |
| 241 } |
| 242 return true; |
| 243 } |
| 244 |
| 245 /** |
| 246 * Replace the directory with the correct contents. [spec] is the "tool/spec" |
| 247 * directory. If [spec] is unspecified, it is assumed to be the directory |
| 248 * containing Platform.executable. |
| 249 */ |
| 250 @override |
| 251 void generate() { |
| 252 try { |
| 253 // delete the contents of the directory (and the directory itself) |
| 254 outputFile.deleteSync(recursive: true); |
| 255 } catch (e) { |
| 256 // Error caught while trying to delete the directory, this can happen if |
| 257 // it didn't yet exist. |
| 258 } |
| 259 // re-create the empty directory |
| 260 outputFile.createSync(recursive: true); |
| 261 |
| 262 // generate all of the files in the directory |
| 263 Map<String, FileContentsComputer> map = directoryContentsComputer(); |
| 264 map.forEach((String file, FileContentsComputer fileContentsComputer) { |
| 265 File outputFile = new File(joinAll(posix.split(outputDirPath + file))); |
| 266 outputFile.writeAsStringSync(fileContentsComputer()); |
| 267 }); |
| 268 } |
| 269 } |
| 270 |
| 271 /** |
324 * Class representing a single output file (either generated code or generated | 272 * Class representing a single output file (either generated code or generated |
325 * HTML). | 273 * HTML). |
326 */ | 274 */ |
327 class GeneratedFile extends GeneratedContent { | 275 class GeneratedFile extends GeneratedContent { |
328 /** | 276 /** |
329 * The output file to which generated output should be written, relative to | 277 * The output file to which generated output should be written, relative to |
330 * the "tool/spec" directory. This filename uses the posix path separator | 278 * the "tool/spec" directory. This filename uses the posix path separator |
331 * ('/') regardless of the OS. | 279 * ('/') regardless of the OS. |
332 */ | 280 */ |
333 final String outputPath; | 281 final String outputPath; |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
365 * Replace the file with the correct contents. [spec] is the "tool/spec" | 313 * Replace the file with the correct contents. [spec] is the "tool/spec" |
366 * directory. If [spec] is unspecified, it is assumed to be the directory | 314 * directory. If [spec] is unspecified, it is assumed to be the directory |
367 * containing Platform.executable. | 315 * containing Platform.executable. |
368 */ | 316 */ |
369 void generate() { | 317 void generate() { |
370 outputFile.writeAsStringSync(computeContents()); | 318 outputFile.writeAsStringSync(computeContents()); |
371 } | 319 } |
372 } | 320 } |
373 | 321 |
374 /** | 322 /** |
375 * Class representing a single output directory (either generated code or | 323 * Mixin class for generating HTML representations of code that are suitable |
376 * generated HTML). No other content should exisit in the directory. | 324 * for enclosing inside a <pre> element. |
377 */ | 325 */ |
378 class GeneratedDirectory extends GeneratedContent { | 326 abstract class HtmlCodeGenerator { |
| 327 _HtmlCodeGeneratorState _state; |
379 | 328 |
380 /** | 329 /** |
381 * The path to the directory that will have the generated content. | 330 * Add the given [node] to the HTML output. |
382 */ | 331 */ |
383 final String outputDirPath; | 332 void add(dom.Node node) { |
384 | 333 _state.add(node); |
385 /** | |
386 * Callback function which computes the directory contents. | |
387 */ | |
388 final DirectoryContentsComputer directoryContentsComputer; | |
389 | |
390 GeneratedDirectory(this.outputDirPath, this.directoryContentsComputer); | |
391 | |
392 /** | |
393 * Get a Directory object representing the output directory. | |
394 */ | |
395 Directory get outputFile => | |
396 new Directory(joinAll(posix.split(outputDirPath))); | |
397 | |
398 /** | |
399 * Check whether the directory has the correct contents, and return true if it | |
400 * does. | |
401 */ | |
402 @override | |
403 bool check() { | |
404 Map<String, FileContentsComputer> map = directoryContentsComputer(); | |
405 try { | |
406 map.forEach((String file, FileContentsComputer fileContentsComputer) { | |
407 String expectedContents = fileContentsComputer(); | |
408 File outputFile = | |
409 new File(joinAll(posix.split(posix.join(outputDirPath, file)))); | |
410 if (expectedContents != outputFile.readAsStringSync()) { | |
411 return false; | |
412 } | |
413 }); | |
414 int nonHiddenFileCount = 0; | |
415 outputFile.listSync( | |
416 recursive: false, | |
417 followLinks: false).forEach((FileSystemEntity fileSystemEntity) { | |
418 if(fileSystemEntity is File && !basename(fileSystemEntity.path).startsW
ith('.')) { | |
419 nonHiddenFileCount++; | |
420 } | |
421 }); | |
422 if (nonHiddenFileCount != map.length) { | |
423 // The number of files generated doesn't match the number we expected to | |
424 // generate. | |
425 return false; | |
426 } | |
427 } catch (e) { | |
428 // There was a problem reading the file (most likely because it didn't | |
429 // exist). Treat that the same as if the file doesn't have the expected | |
430 // contents. | |
431 return false; | |
432 } | |
433 return true; | |
434 } | 334 } |
435 | 335 |
436 /** | 336 /** |
437 * Replace the directory with the correct contents. [spec] is the "tool/spec" | 337 * Add the given [nodes] to the HTML output. |
438 * directory. If [spec] is unspecified, it is assumed to be the directory | |
439 * containing Platform.executable. | |
440 */ | 338 */ |
441 @override | 339 void addAll(Iterable<dom.Node> nodes) { |
442 void generate() { | 340 for (dom.Node node in nodes) { |
| 341 _state.add(node); |
| 342 } |
| 343 } |
| 344 |
| 345 /** |
| 346 * Execute [callback], collecting any code that is output using [write], |
| 347 * [writeln], [add], or [addAll], and return the result as a list of DOM |
| 348 * nodes. |
| 349 */ |
| 350 List<dom.Node> collectHtml(void callback()) { |
| 351 _HtmlCodeGeneratorState oldState = _state; |
443 try { | 352 try { |
444 // delete the contents of the directory (and the directory itself) | 353 _state = new _HtmlCodeGeneratorState(); |
445 outputFile.deleteSync(recursive: true); | 354 if (callback != null) { |
446 } catch (e) { | 355 callback(); |
447 // Error caught while trying to delete the directory, this can happen if | 356 } |
448 // it didn't yet exist. | 357 return _state.buffer; |
| 358 } finally { |
| 359 _state = oldState; |
449 } | 360 } |
450 // re-create the empty directory | 361 } |
451 outputFile.createSync(recursive: true); | |
452 | 362 |
453 // generate all of the files in the directory | 363 /** |
454 Map<String, FileContentsComputer> map = directoryContentsComputer(); | 364 * Execute [callback], wrapping its output in an element with the given |
455 map.forEach((String file, FileContentsComputer fileContentsComputer) { | 365 * [name] and [attributes]. |
456 File outputFile = new File(joinAll(posix.split(outputDirPath + file))); | 366 */ |
457 outputFile.writeAsStringSync(fileContentsComputer()); | 367 void element(String name, Map<String, String> attributes, [void callback()]) { |
458 }); | 368 add(makeElement(name, attributes, collectHtml(callback))); |
| 369 } |
| 370 |
| 371 /** |
| 372 * Execute [callback], indenting any code it outputs by two spaces. |
| 373 */ |
| 374 void indent(void callback()) { |
| 375 String oldIndent = _state.indent; |
| 376 try { |
| 377 _state.indent += ' '; |
| 378 callback(); |
| 379 } finally { |
| 380 _state.indent = oldIndent; |
| 381 } |
| 382 } |
| 383 |
| 384 /** |
| 385 * Output text without ending the current line. |
| 386 */ |
| 387 void write(Object obj) { |
| 388 _state.write(obj.toString()); |
| 389 } |
| 390 |
| 391 /** |
| 392 * Output text, ending the current line. |
| 393 */ |
| 394 void writeln([Object obj = '']) { |
| 395 _state.write('$obj\n'); |
459 } | 396 } |
460 } | 397 } |
| 398 |
| 399 /** |
| 400 * State used by [CodeGenerator]. |
| 401 */ |
| 402 class _CodeGeneratorState { |
| 403 StringBuffer buffer = new StringBuffer(); |
| 404 String nextIndent = ''; |
| 405 String indent = ''; |
| 406 bool indentNeeded = true; |
| 407 |
| 408 void write(String text) { |
| 409 List<String> lines = text.split('\n'); |
| 410 for (int i = 0; i < lines.length; i++) { |
| 411 if (i == lines.length - 1 && lines[i].isEmpty) { |
| 412 break; |
| 413 } |
| 414 if (indentNeeded) { |
| 415 buffer.write(nextIndent); |
| 416 nextIndent = indent; |
| 417 } |
| 418 indentNeeded = false; |
| 419 buffer.write(lines[i]); |
| 420 if (i != lines.length - 1) { |
| 421 buffer.writeln(); |
| 422 indentNeeded = true; |
| 423 } |
| 424 } |
| 425 } |
| 426 } |
| 427 |
| 428 /** |
| 429 * State used by [HtmlCodeGenerator]. |
| 430 */ |
| 431 class _HtmlCodeGeneratorState { |
| 432 List<dom.Node> buffer = <dom.Node>[]; |
| 433 String indent = ''; |
| 434 bool indentNeeded = true; |
| 435 |
| 436 void add(dom.Node node) { |
| 437 if (node is dom.Text) { |
| 438 write(node.text); |
| 439 } else { |
| 440 buffer.add(node); |
| 441 } |
| 442 } |
| 443 |
| 444 void write(String text) { |
| 445 if (text.isEmpty) { |
| 446 return; |
| 447 } |
| 448 if (indentNeeded) { |
| 449 buffer.add(new dom.Text(indent)); |
| 450 } |
| 451 List<String> lines = text.split('\n'); |
| 452 if (lines.last.isEmpty) { |
| 453 lines.removeLast(); |
| 454 buffer.add(new dom.Text(lines.join('\n$indent') + '\n')); |
| 455 indentNeeded = true; |
| 456 } else { |
| 457 buffer.add(new dom.Text(lines.join('\n$indent'))); |
| 458 indentNeeded = false; |
| 459 } |
| 460 } |
| 461 } |
OLD | NEW |