Chromium Code Reviews| 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 |