| 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 html_to_json; | |
| 26 | |
| 27 import 'dart:json'; | |
| 28 import 'dart:io'; | |
| 29 import 'dart:async'; | |
| 30 | |
| 31 | |
| 32 /// True if any errors were triggered through the conversion. | |
| 33 bool _anyErrors = false; | |
| 34 | |
| 35 | |
| 36 /** | |
| 37 * Convert files on [htmlPath] and write JSON to [jsonPath]. | |
| 38 */ | |
| 39 Future<bool> convert(Path htmlPath, Path jsonPath) { | |
| 40 var completer = new Completer(); | |
| 41 | |
| 42 // TODO(amouravski): make this transform once I know what I want this file to | |
| 43 // return. | |
| 44 _convertFiles(htmlPath).then((convertedJson) { | |
| 45 final jsonFile = new File.fromPath(jsonPath); | |
| 46 var writeJson = convertedJson; | |
| 47 | |
| 48 if (jsonFile.existsSync()) { | |
| 49 writeJson = _mergeJsonAndFile(convertedJson, jsonFile); | |
| 50 } | |
| 51 | |
| 52 var outputStream = jsonFile.openOutputStream(); | |
| 53 outputStream.writeString(prettyPrintJson(writeJson)); | |
| 54 | |
| 55 outputStream.onNoPendingWrites = () { | |
| 56 completer.complete(_anyErrors); | |
| 57 }; | |
| 58 | |
| 59 outputStream.onClosed = () { | |
| 60 completer.complete(_anyErrors); | |
| 61 }; | |
| 62 | |
| 63 outputStream.onError = completer.completeError; | |
| 64 }); | |
| 65 | |
| 66 return completer.future; | |
| 67 } | |
| 68 | |
| 69 | |
| 70 /** | |
| 71 * Convert all files on [htmlPath]. | |
| 72 * | |
| 73 * Returns a future that completes to the converted JSON object. | |
| 74 */ | |
| 75 Future<Object> _convertFiles(Path htmlPath) { | |
| 76 var completer = new Completer(); | |
| 77 | |
| 78 List<Future> fileFutures = []; | |
| 79 | |
| 80 // Get a list of all HTML dart files. | |
| 81 // TODO(amouravski): discriminate .dart files. | |
| 82 final htmlDir = new Directory.fromPath(htmlPath); | |
| 83 final lister = htmlDir.list(recursive: false); | |
| 84 | |
| 85 lister.onFile = (String path) { | |
| 86 final name = new Path(path).filename; | |
| 87 | |
| 88 // Ignore private classes. | |
| 89 if (name.startsWith('_')) return; | |
| 90 | |
| 91 // Ignore non-dart files. | |
| 92 if (!name.endsWith('.dart')) return; | |
| 93 | |
| 94 File file = new File(path); | |
| 95 | |
| 96 // TODO(amouravski): Handle missing file. | |
| 97 if (!file.existsSync()) { | |
| 98 print('ERROR: cannot find file $path'); | |
| 99 _anyErrors = true; | |
| 100 return; | |
| 101 } | |
| 102 | |
| 103 fileFutures.add(_convertFile(file)); | |
| 104 }; | |
| 105 | |
| 106 | |
| 107 // Combine all JSON objects | |
| 108 lister.onDone = (_) { | |
| 109 Futures.wait(fileFutures).then((jsonList) { | |
| 110 var convertedJson = {}; | |
| 111 jsonList.forEach((json) { | |
| 112 final k = json.keys[0]; | |
| 113 convertedJson.putIfAbsent(k, () => json[k]); | |
| 114 }); | |
| 115 completer.complete(convertedJson); | |
| 116 }); | |
| 117 }; | |
| 118 | |
| 119 // TODO(amouravski): add more error handling. | |
| 120 | |
| 121 return completer.future; | |
| 122 } | |
| 123 | |
| 124 | |
| 125 /** | |
| 126 * Convert a single file to JSON docs. | |
| 127 * | |
| 128 * Returns a map with one entry whose key is the file name and whose value is | |
| 129 * the list of comment lines. | |
| 130 */ | |
| 131 Future<Map> _convertFile(File file) { | |
| 132 var completer = new Completer(); | |
| 133 | |
| 134 var comments = {}; | |
| 135 | |
| 136 // Find all /// @docsEditable annotations. | |
| 137 InputStream file_stream = file.openInputStream(); | |
| 138 StringInputStream inputLines = new StringInputStream(file_stream); | |
| 139 | |
| 140 // TODO(amouravski): Re-write as file.readAsLine().thin((lines) {...} | |
| 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(); | |
| 154 | |
| 155 if (nextLine == null) return false; | |
| 156 | |
| 157 var lineObject = {}; | |
| 158 | |
| 159 if (comments[nextLine] != null) { | |
| 160 print('WARNING: duplicate line ${nextLine} found in' | |
| 161 '${new Path(file.fullPathSync()).filename}'); | |
| 162 } | |
| 163 comments.putIfAbsent(nextLine, () => comment); | |
| 164 } | |
| 165 | |
| 166 // Reset. | |
| 167 docCommentFound = false; | |
| 168 comment = <String>[]; | |
| 169 } else if ( // Start a comment block. | |
| 170 trimmedLine.startsWith('/**') || | |
| 171 trimmedLine.startsWith('///')) { | |
| 172 docCommentFound = true; | |
| 173 comment.add(line); | |
| 174 } else if (docCommentFound && | |
| 175 // TODO(amouravski): This will barf on: | |
| 176 // /// blah | |
| 177 // * | |
| 178 (trimmedLine.startsWith('*') || trimmedLine.startsWith('///'))) { | |
| 179 comment.add(line); | |
| 180 } else { | |
| 181 // Reset if we're not in a comment. | |
| 182 docCommentFound = false; | |
| 183 comment = <String>[]; | |
| 184 } | |
| 185 } | |
| 186 }; | |
| 187 | |
| 188 inputLines.onClosed = () { | |
| 189 var jsonObject = {}; | |
| 190 jsonObject[new Path(file.fullPathSync()).filename] = comments; | |
| 191 completer.complete(jsonObject); | |
| 192 }; | |
| 193 | |
| 194 // TODO(amouravski): better error handling. | |
| 195 | |
| 196 return completer.future; | |
| 197 } | |
| 198 | |
| 199 | |
| 200 /** | |
| 201 * Merge the new JSON object and the existing file. | |
| 202 */ | |
| 203 Object _mergeJsonAndFile(Object json, File file) { | |
| 204 var completer = new Completer(); | |
| 205 | |
| 206 var fileJson = {}; | |
| 207 var jsonRead = file.readAsStringSync(); | |
| 208 | |
| 209 if (jsonRead == '') { | |
| 210 print('WARNING: no data read from ' | |
| 211 '${new Path(file.fullPathSync()).filename}'); | |
| 212 _anyErrors = true; | |
| 213 } else { | |
| 214 fileJson = JSON.parse(jsonRead); | |
| 215 } | |
| 216 return _mergeJson(json, fileJson); | |
| 217 } | |
| 218 | |
| 219 | |
| 220 /** | |
| 221 * Merge two JSON objects, such that the returned JSON object is the | |
| 222 * union of both. | |
| 223 * | |
| 224 * Each JSON must be a map, with each value being a map. | |
| 225 */ | |
| 226 Object _mergeJson(Object json1, Object json2) { | |
| 227 if (json1 is Map && json2 is Map) { | |
| 228 // Then check if [json2] contains any key form [json1], in which case | |
| 229 // add all of the values from [json2] to the values of [json1]. | |
| 230 json2.forEach((k, v) { | |
| 231 if (json1.containsKey(k)) { | |
| 232 v.forEach((vk, vv) { | |
| 233 if (json1[k].containsKey(vk) && | |
| 234 !_listsEqual(json1[k][vk],vv)) { | |
| 235 // Assume that json1 is more current and take its data as opposed | |
| 236 // to json2's. | |
| 237 // TODO(amouravski): add better warning message and only if there's | |
| 238 // a conflict. | |
| 239 print('INFO: duplicate keys.'); | |
| 240 _anyErrors = false; | |
| 241 } else { | |
| 242 json1[k].putIfAbsent(vk, () => vv); | |
| 243 } | |
| 244 }); | |
| 245 } else { | |
| 246 json1.putIfAbsent(k, () => v); | |
| 247 } | |
| 248 }); | |
| 249 } else { | |
| 250 throw new ArgumentError('JSON objects must both be Maps'); | |
| 251 } | |
| 252 | |
| 253 // TODO(amouravski): more error handling. | |
| 254 | |
| 255 return json1; | |
| 256 } | |
| 257 | |
| 258 | |
| 259 /** | |
| 260 * Tests for equality between two lists. | |
| 261 * | |
| 262 * This checks the first level of depth, so does not work for nested lists. | |
| 263 */ | |
| 264 bool _listsEqual(List list1, List list2) { | |
| 265 return list1.every((e) => list2.contains(e)) && | |
| 266 list2.every((e) => list1.contains(e)); | |
| 267 } | |
| 268 | |
| 269 | |
| 270 /** | |
| 271 * Print JSON in a much nicer format. | |
| 272 * | |
| 273 * For example: | |
| 274 * | |
| 275 * {"foo":["bar","baz"],"boo":{"far:"faz"}} | |
| 276 * | |
| 277 * becomes: | |
| 278 * | |
| 279 * { | |
| 280 * "foo": | |
| 281 * [ | |
| 282 * "bar", | |
| 283 * "baz" | |
| 284 * ], | |
| 285 * "boo": | |
| 286 * { | |
| 287 * "far": | |
| 288 * "faz" | |
| 289 * } | |
| 290 * } | |
| 291 */ | |
| 292 String prettyPrintJson(Object json, [String indentation = '']) { | |
| 293 var output; | |
| 294 | |
| 295 if (json is List) { | |
| 296 var recursiveOutput = | |
| 297 Strings.join(json.map((e) => | |
| 298 prettyPrintJson(e, '$indentation ')), ',\n'); | |
| 299 output = '$indentation[\n' | |
| 300 '$recursiveOutput' | |
| 301 '\n$indentation]'; | |
| 302 } else if (json is Map) { | |
| 303 var keys = json.keys | |
| 304 ..sort(); | |
| 305 | |
| 306 // TODO(amouravski): No newline after : | |
| 307 var mapList = keys.map((key) => | |
| 308 '$indentation${JSON.stringify(key)}:\n' | |
| 309 '${prettyPrintJson(json[key], '$indentation ')}'); | |
| 310 var recursiveOutput = Strings.join(mapList, ',\n'); | |
| 311 output = '$indentation{\n' | |
| 312 '$recursiveOutput' | |
| 313 '\n$indentation}'; | |
| 314 } else { | |
| 315 output = '$indentation${JSON.stringify(json)}'; | |
| 316 } | |
| 317 return output; | |
| 318 } | |
| OLD | NEW |