OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012, 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 /** | |
6 * Library for extracting the documentation comments from files generated by | |
7 * the HTML library. The comments are stored in a JSON file. | |
8 * | |
9 * Comments must be in either the block style with leading *s: | |
10 * | |
11 * /** | |
12 * * Comment here. | |
13 * */ | |
14 * | |
15 * Or the triple-slash style: | |
16 * | |
17 * /// Docs go here. | |
18 * /// And here. | |
19 * | |
20 * Each member that is to be documented should be preceeded by a meta-comment | |
21 * containing the string `@docsEditable` such as: | |
22 * | |
23 * /// @docsEditable | |
24 */ | |
25 library htmlToJson; | |
26 | |
27 import 'dart:json'; | |
28 import 'dart:io'; | |
29 | |
30 | |
31 /// True if any errors were triggered through the conversion. | |
32 bool _anyErrors = false; | |
33 | |
34 | |
35 /** | |
36 * Convert files on [htmlPath] and write JSON to [jsonPath]. | |
Bob Nystrom
2012/11/26 22:00:19
You do a mixture of /// and /** */ style doc comme
Andrei Mouravski
2012/11/27 03:11:45
Because nothing forces me to.
| |
37 */ | |
38 Future<bool> convert(Path htmlPath, Path jsonPath) { | |
39 var completer = new Completer(); | |
40 | |
41 var futureConvert = _convertFiles(htmlPath); | |
42 | |
43 // TODO(amouravski): make this transform once I know what I want this file to | |
44 // return. | |
45 futureConvert.then((convertedJson) { | |
Bob Nystrom
2012/11/26 22:00:19
You don't actually use this variable for anything
Andrei Mouravski
2012/11/27 03:11:45
Done.
| |
46 final jsonFile = new File.fromPath(jsonPath); | |
47 var writeJson = convertedJson; | |
48 | |
49 if (jsonFile.existsSync()) { | |
50 var writeJson = _mergeJsonAndFile(convertedJson, jsonFile); | |
Bob Nystrom
2012/11/26 22:00:19
Do you intend to shadow writeJson here?
Andrei Mouravski
2012/11/27 03:11:45
Wow. Nope.
| |
51 } else { | |
52 jsonFile.createSync(); | |
Bob Nystrom
2012/11/26 22:00:19
I don't think this is necessary. openOutputStream
Andrei Mouravski
2012/11/27 03:11:45
Done.
| |
53 } | |
54 | |
55 var outputStream = jsonFile.openOutputStream(); | |
56 outputStream.writeString(prettyPrintJson(writeJson)); | |
57 | |
58 outputStream.onNoPendingWrites = () { | |
59 completer.complete(_anyErrors); | |
60 }; | |
61 | |
62 outputStream.onClosed = () { | |
63 completer.complete(_anyErrors); | |
64 }; | |
Bob Nystrom
2012/11/26 22:00:19
Also add:
outputStream.onError = completer.comple
Andrei Mouravski
2012/11/27 03:11:45
Done.
| |
65 }); | |
66 | |
67 return completer.future; | |
68 } | |
69 | |
70 | |
71 /** | |
72 * Convert all files on [htmlPath]. | |
73 * | |
74 * Returns a future that completes to the converted JSON object. | |
75 */ | |
76 Future<Object> _convertFiles(Path htmlPath) { | |
77 var completer = new Completer(); | |
78 | |
79 List<Future> fileFutures = []; | |
80 | |
81 // Get a list of all HTML dart files. | |
82 // TODO(amouravski): discriminate .dart files. | |
83 final htmlDir = new Directory.fromPath(htmlPath); | |
84 final lister = htmlDir.list(recursive: false); | |
85 | |
86 lister.onFile = (String path) { | |
87 final name = new Path.fromNative(path).filename; | |
88 | |
89 // Ignore private classes. | |
90 if (name.startsWith('_')) return; | |
91 | |
92 // Ignore non-dart files. | |
93 if (!name.endsWith('.dart')) return; | |
94 | |
95 File file = new File(path); | |
96 | |
97 // TODO(amouravski): Handle missing file. | |
98 if (!file.existsSync()) { | |
99 print('ERROR: cannot find file $path'); | |
100 _anyErrors = true; | |
101 return; | |
102 } | |
103 | |
104 fileFutures.add(_convertFile(file)); | |
105 }; | |
106 | |
107 | |
108 // Combine all JSON objects | |
109 lister.onDone = (_) { | |
110 Futures.wait(fileFutures).then((jsonList) { | |
111 var convertedJson = {}; | |
112 jsonList.forEach((json) { | |
113 final k = json.keys[0]; | |
114 convertedJson.putIfAbsent(k, () => json[k]); | |
115 }); | |
116 completer.complete(convertedJson); | |
117 }); | |
118 }; | |
119 | |
120 // TODO(amouravski): add more error handling. | |
121 | |
122 return completer.future; | |
123 } | |
124 | |
125 | |
126 /** | |
127 * Convert a single file to JSON docs. | |
128 * | |
129 * Output is a single object which with one element that maps the file name | |
Bob Nystrom
2012/11/26 22:00:19
How about "Returns a map with one entry whose key
Andrei Mouravski
2012/11/27 03:11:45
Done.
| |
130 * to the list of comment lines. | |
131 */ | |
132 Future<Object> _convertFile(File file) { | |
Bob Nystrom
2012/11/26 22:00:19
Future<Map>
Andrei Mouravski
2012/11/27 03:11:45
Done.
| |
133 var completer = new Completer(); | |
134 | |
135 var comments = {}; | |
136 | |
137 // Find all /// @docsEditable annotations. | |
138 InputStream file_stream = file.openInputStream(); | |
139 StringInputStream inputLines = new StringInputStream(file_stream); | |
140 | |
141 inputLines.onLine = () { | |
142 var comment = <String>[]; | |
143 | |
144 var docCommentFound = false; | |
145 String line; | |
146 while ((line = inputLines.readLine()) != null) { | |
147 var trimmedLine = line.trim(); | |
148 | |
149 // Sentinel found. Process the comment block. | |
150 if (trimmedLine.startsWith('///') && | |
151 trimmedLine.contains('@docsEditable')) { | |
152 if (docCommentFound == true) { | |
153 var nextLine = inputLines.readLine(); | |
Bob Nystrom
2012/11/26 22:00:19
I don't think you can do this. readLine() can fail
Andrei Mouravski
2012/11/27 03:11:45
Will get back to this if possible.
| |
154 | |
155 if (nextLine == null) return false; | |
156 | |
157 var lineObject = {}; | |
158 //lineObject[nextLine] = comment; | |
159 comments.putIfAbsent(nextLine, () => comment); | |
160 } | |
161 | |
162 // Reset. | |
163 docCommentFound = false; | |
164 comment = <String>[]; | |
165 } else if ( // Start a comment block. | |
166 trimmedLine.startsWith('/**') || | |
167 trimmedLine.startsWith('///')) { | |
168 docCommentFound = true; | |
169 comment.add(line); | |
170 } else if (docCommentFound && | |
171 (trimmedLine.startsWith('*') || trimmedLine.startsWith('///'))) { | |
Bob Nystrom
2012/11/26 22:00:19
This would barf if you did:
/// A doc comment
* n
Andrei Mouravski
2012/11/27 03:11:45
I will barf, yes. In the interest of time, I'm goi
| |
172 comment.add(line); | |
173 } else { | |
174 // Reset if we're not in a comment. | |
175 docCommentFound = false; | |
176 comment = <String>[]; | |
177 } | |
178 } | |
179 }; | |
180 | |
181 inputLines.onClosed = () { | |
182 var jsonObject = {}; | |
183 jsonObject[new Path(file.fullPathSync()).filename] = comments; | |
184 completer.complete(jsonObject); | |
185 }; | |
186 | |
187 // TODO(amouravski): better error handling. | |
188 | |
189 return completer.future; | |
190 } | |
191 | |
192 | |
193 /** | |
194 * Merge the new JSON object and the existing file. | |
195 */ | |
196 Object _mergeJsonAndFile(Object json, File file) { | |
197 var completer = new Completer(); | |
198 | |
199 var fileJson = {}; | |
200 var jsonRead = Strings.join(file.readAsLinesSync(), '\n'); | |
Bob Nystrom
2012/11/26 22:00:19
file.readAsStringSync();
Andrei Mouravski
2012/11/27 03:11:45
I wrote this before I knew that method existed. :]
| |
201 | |
202 if (jsonRead == '') { | |
203 print('WARNING: no data read from ' | |
204 '${new Path(file.fullPathSync()).filename}'); | |
205 _anyErrors = true; | |
206 } else { | |
207 fileJson = JSON.parse(jsonRead); | |
208 } | |
209 return _mergeJson(json, fileJson); | |
210 } | |
211 | |
212 | |
213 /** | |
214 * Merge two JSON objects, such that the next JSON object is the | |
215 * union of both. | |
216 * | |
217 * Each JSON must be a map, with each value being a map. | |
218 */ | |
219 Object _mergeJson(Object json1, Object json2) { | |
220 if (json1 is Map && json2 is Map) { | |
221 // Then check if [json2] contains any key form [json1], in which case | |
222 // add all of the values from [json] to the values of [json1]. | |
223 json2.forEach((k, v) { | |
224 if (json1.containsKey(k)) { | |
225 v.forEach((vk, vv) { | |
226 if (json1[k].containsKey(vk) && | |
227 !_listsEqual(json1[k][vk],vv)) { | |
228 // TODO(amouravski): add better warning message and only if there's | |
229 // a conflict. | |
230 print('WARNING: duplicate keys.'); | |
231 _anyErrors = true; | |
232 } else { | |
233 json1[k].putIfAbsent(vk, () => vv); | |
234 } | |
235 }); | |
236 } else { | |
237 json1.putIfAbsent(k, () => v); | |
238 } | |
Bob Nystrom
2012/11/26 22:00:19
This is a bit hard for me to read. What do you thi
Andrei Mouravski
2012/11/27 03:11:45
Will return to this, too.
| |
239 }); | |
240 } else { | |
241 throw new ArgumentError('JSON objects must both be Maps'); | |
242 } | |
243 | |
244 // TODO(amouravski): more error handling. | |
245 | |
246 return json1; | |
247 } | |
248 | |
249 | |
250 /** | |
251 * Tests for equality between two lists. | |
252 * | |
253 * This checks the first level of depth, so does not work for nested lists. | |
254 */ | |
255 bool _listsEqual(List list1, List list2) { | |
256 return list1.every((e) => list2.contains(e)) && | |
257 list2.every((e) => list1.contains(e)); | |
258 } | |
259 | |
260 | |
261 /** | |
262 * Print JSON in a much nicer format. | |
263 * | |
264 * For example: | |
265 * {"foo":["bar","baz"],"boo":{"far:"faz"}} | |
266 * | |
267 * becomes: | |
268 * | |
269 * { | |
270 * "foo": | |
271 * [ | |
272 * "bar", | |
273 * "baz" | |
274 * ], | |
275 * "boo": | |
276 * { | |
277 * "far": | |
278 * "faz" | |
279 * } | |
280 * } | |
281 */ | |
282 String prettyPrintJson(Object json, [depth = 0]) { | |
283 var output; | |
284 | |
285 var printSpaces = Strings.join( | |
286 new List<String>(depth + 1).map((e) => ''), ' '); | |
Bob Nystrom
2012/11/26 22:00:19
This is a bit gratuitiously inefficient. How about
Andrei Mouravski
2012/11/27 03:11:45
Done.
Gratuitious amounts of inefficiency. Powert
| |
287 | |
288 if (json is List) { | |
289 var recursiveOutput = | |
290 Strings.join(json.map((e) => prettyPrintJson(e, depth + 1)), ',\n'); | |
291 output = '${printSpaces}[\n' | |
Bob Nystrom
2012/11/26 22:00:19
If it's a single identifier, you can omit the {},
Andrei Mouravski
2012/11/27 03:11:45
Done.
| |
292 '${recursiveOutput}' | |
293 '\n${printSpaces}]'; | |
294 } else if (json is Map) { | |
295 var mapList = json.keys.map((key) => | |
296 '${printSpaces}"${key}":\n${prettyPrintJson(json[key], depth + 1)}'); | |
Bob Nystrom
2012/11/26 22:00:19
Nit, but how about getting rid of the newline afte
Andrei Mouravski
2012/11/27 03:11:45
I'd like to, but that'd mean editing a lot of file
| |
297 var recursiveOutput = | |
298 Strings.join(mapList, ',\n'); | |
Bob Nystrom
2012/11/26 22:00:19
Nit: this can fit on one line.
Andrei Mouravski
2012/11/27 03:11:45
At one point it didn't.
| |
299 output = '${printSpaces}{\n' | |
300 '${recursiveOutput}' | |
301 '\n${printSpaces}}'; | |
302 } else { | |
303 output = '${printSpaces}"${json}"'; | |
304 } | |
305 return output; | |
306 } | |
OLD | NEW |