Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(341)

Side by Side Diff: pkg/analysis_server/tool/spec/codegen_tools.dart

Issue 725143004: Format and sort analyzer and analysis_server packages. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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
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
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 }
OLDNEW
« no previous file with comments | « pkg/analysis_server/tool/spec/codegen_matchers.dart ('k') | pkg/analysis_server/tool/spec/from_html.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698