| 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 * Code for reading an HTML API description. | 6 * Code for reading an HTML API description. |
| 7 */ | 7 */ |
| 8 import 'dart:io'; | 8 import 'dart:io'; |
| 9 | 9 |
| 10 import 'package:analyzer/src/codegen/html.dart'; | 10 import 'package:analyzer/src/codegen/html.dart'; |
| 11 import 'package:html/dom.dart' as dom; | 11 import 'package:html/dom.dart' as dom; |
| 12 import 'package:html/parser.dart' as parser; | 12 import 'package:html/parser.dart' as parser; |
| 13 import 'package:path/path.dart'; | 13 import 'package:path/path.dart'; |
| 14 | 14 |
| 15 import 'api.dart'; | 15 import 'api.dart'; |
| 16 | 16 |
| 17 const List<String> specialElements = const [ | |
| 18 'domain', | |
| 19 'feedback', | |
| 20 'object', | |
| 21 'refactorings', | |
| 22 'refactoring', | |
| 23 'type', | |
| 24 'types', | |
| 25 'request', | |
| 26 'notification', | |
| 27 'params', | |
| 28 'result', | |
| 29 'field', | |
| 30 'list', | |
| 31 'map', | |
| 32 'enum', | |
| 33 'key', | |
| 34 'value', | |
| 35 'options', | |
| 36 'ref', | |
| 37 'code', | |
| 38 'version', | |
| 39 'union', | |
| 40 'index' | |
| 41 ]; | |
| 42 | |
| 43 /** | 17 /** |
| 44 * Create an [Api] object from an HTML representation such as: | 18 * Read the API description from the file 'plugin_spec.html'. [pkgPath] is the |
| 45 * | |
| 46 * <html> | |
| 47 * ... | |
| 48 * <body> | |
| 49 * ... <version>1.0</version> ... | |
| 50 * <domain name="...">...</domain> <!-- zero or more --> | |
| 51 * <types>...</types> | |
| 52 * <refactorings>...</refactorings> | |
| 53 * </body> | |
| 54 * </html> | |
| 55 * | |
| 56 * Child elements of <api> can occur in any order. | |
| 57 */ | |
| 58 Api apiFromHtml(dom.Element html) { | |
| 59 Api api; | |
| 60 List<String> versions = <String>[]; | |
| 61 List<Domain> domains = <Domain>[]; | |
| 62 Types types = null; | |
| 63 Refactorings refactorings = null; | |
| 64 recurse(html, 'api', { | |
| 65 'domain': (dom.Element element) { | |
| 66 domains.add(domainFromHtml(element)); | |
| 67 }, | |
| 68 'refactorings': (dom.Element element) { | |
| 69 refactorings = refactoringsFromHtml(element); | |
| 70 }, | |
| 71 'types': (dom.Element element) { | |
| 72 types = typesFromHtml(element); | |
| 73 }, | |
| 74 'version': (dom.Element element) { | |
| 75 versions.add(innerText(element)); | |
| 76 }, | |
| 77 'index': (dom.Element element) { | |
| 78 /* Ignore; generated dynamically. */ | |
| 79 } | |
| 80 }); | |
| 81 if (versions.length != 1) { | |
| 82 throw new Exception('The API must contain exactly one <version> element'); | |
| 83 } | |
| 84 api = new Api(versions[0], domains, types, refactorings, html); | |
| 85 return api; | |
| 86 } | |
| 87 | |
| 88 /** | |
| 89 * Check that the given [element] has all of the attributes in | |
| 90 * [requiredAttributes], possibly some of the attributes in | |
| 91 * [optionalAttributes], and no others. | |
| 92 */ | |
| 93 void checkAttributes( | |
| 94 dom.Element element, List<String> requiredAttributes, String context, | |
| 95 {List<String> optionalAttributes: const []}) { | |
| 96 Set<String> attributesFound = new Set<String>(); | |
| 97 element.attributes.forEach((String name, String value) { | |
| 98 if (!requiredAttributes.contains(name) && | |
| 99 !optionalAttributes.contains(name)) { | |
| 100 throw new Exception( | |
| 101 '$context: Unexpected attribute in ${element.localName}: $name'); | |
| 102 } | |
| 103 attributesFound.add(name); | |
| 104 }); | |
| 105 for (String expectedAttribute in requiredAttributes) { | |
| 106 if (!attributesFound.contains(expectedAttribute)) { | |
| 107 throw new Exception( | |
| 108 '$context: ${element.localName} must contain attribute $expectedAttrib
ute'); | |
| 109 } | |
| 110 } | |
| 111 } | |
| 112 | |
| 113 /** | |
| 114 * Check that the given [element] has the given [expectedName]. | |
| 115 */ | |
| 116 void checkName(dom.Element element, String expectedName, [String context]) { | |
| 117 if (element.localName != expectedName) { | |
| 118 if (context == null) { | |
| 119 context = element.localName; | |
| 120 } | |
| 121 throw new Exception( | |
| 122 '$context: Expected $expectedName, found ${element.localName}'); | |
| 123 } | |
| 124 } | |
| 125 | |
| 126 /** | |
| 127 * Create a [Domain] object from an HTML representation such as: | |
| 128 * | |
| 129 * <domain name="domainName"> | |
| 130 * <request method="...">...</request> <!-- zero or more --> | |
| 131 * <notification event="...">...</notification> <!-- zero or more --> | |
| 132 * </domain> | |
| 133 * | |
| 134 * Child elements can occur in any order. | |
| 135 */ | |
| 136 Domain domainFromHtml(dom.Element html) { | |
| 137 checkName(html, 'domain'); | |
| 138 String name = html.attributes['name']; | |
| 139 String context = name ?? 'domain'; | |
| 140 bool experimental = html.attributes['experimental'] == 'true'; | |
| 141 checkAttributes(html, ['name'], context, | |
| 142 optionalAttributes: ['experimental']); | |
| 143 List<Request> requests = <Request>[]; | |
| 144 List<Notification> notifications = <Notification>[]; | |
| 145 recurse(html, context, { | |
| 146 'request': (dom.Element child) { | |
| 147 requests.add(requestFromHtml(child, context)); | |
| 148 }, | |
| 149 'notification': (dom.Element child) { | |
| 150 notifications.add(notificationFromHtml(child, context)); | |
| 151 } | |
| 152 }); | |
| 153 return new Domain(name, requests, notifications, html, | |
| 154 experimental: experimental); | |
| 155 } | |
| 156 | |
| 157 dom.Element getAncestor(dom.Element html, String name, String context) { | |
| 158 dom.Element ancestor = html.parent; | |
| 159 while (ancestor != null) { | |
| 160 if (ancestor.localName == name) { | |
| 161 return ancestor; | |
| 162 } | |
| 163 ancestor = ancestor.parent; | |
| 164 } | |
| 165 throw new Exception( | |
| 166 '$context: <${html.localName}> must be nested within <$name>'); | |
| 167 } | |
| 168 | |
| 169 /** | |
| 170 * Create a [Notification] object from an HTML representation such as: | |
| 171 * | |
| 172 * <notification event="methodName"> | |
| 173 * <params>...</params> <!-- optional --> | |
| 174 * </notification> | |
| 175 * | |
| 176 * Note that the event name should not include the domain name. | |
| 177 * | |
| 178 * <params> has the same form as <object>, as described in [typeDeclFromHtml]. | |
| 179 * | |
| 180 * Child elements can occur in any order. | |
| 181 */ | |
| 182 Notification notificationFromHtml(dom.Element html, String context) { | |
| 183 String domainName = getAncestor(html, 'domain', context).attributes['name']; | |
| 184 checkName(html, 'notification', context); | |
| 185 String event = html.attributes['event']; | |
| 186 context = '$context.${event != null ? event : 'event'}'; | |
| 187 checkAttributes(html, ['event'], context); | |
| 188 TypeDecl params; | |
| 189 recurse(html, context, { | |
| 190 'params': (dom.Element child) { | |
| 191 params = typeObjectFromHtml(child, '$context.params'); | |
| 192 } | |
| 193 }); | |
| 194 return new Notification(domainName, event, params, html); | |
| 195 } | |
| 196 | |
| 197 /** | |
| 198 * Create a single of [TypeDecl] corresponding to the type defined inside the | |
| 199 * given HTML element. | |
| 200 */ | |
| 201 TypeDecl processContentsAsType(dom.Element html, String context) { | |
| 202 List<TypeDecl> types = processContentsAsTypes(html, context); | |
| 203 if (types.length != 1) { | |
| 204 throw new Exception('$context: Exactly one type must be specified'); | |
| 205 } | |
| 206 return types[0]; | |
| 207 } | |
| 208 | |
| 209 /** | |
| 210 * Create a list of [TypeDecl]s corresponding to the types defined inside the | |
| 211 * given HTML element. The following forms are supported. | |
| 212 * | |
| 213 * To refer to a type declared elsewhere (or a built-in type): | |
| 214 * | |
| 215 * <ref>typeName</ref> | |
| 216 * | |
| 217 * For a list: <list>ItemType</list> | |
| 218 * | |
| 219 * For a map: <map><key>KeyType</key><value>ValueType</value></map> | |
| 220 * | |
| 221 * For a JSON object: | |
| 222 * | |
| 223 * <object> | |
| 224 * <field name="...">...</field> <!-- zero or more --> | |
| 225 * </object> | |
| 226 * | |
| 227 * For an enum: | |
| 228 * | |
| 229 * <enum> | |
| 230 * <value>...</value> <!-- zero or more --> | |
| 231 * </enum> | |
| 232 * | |
| 233 * For a union type: | |
| 234 * <union> | |
| 235 * TYPE <!-- zero or more --> | |
| 236 * </union> | |
| 237 */ | |
| 238 List<TypeDecl> processContentsAsTypes(dom.Element html, String context) { | |
| 239 List<TypeDecl> types = <TypeDecl>[]; | |
| 240 recurse(html, context, { | |
| 241 'object': (dom.Element child) { | |
| 242 types.add(typeObjectFromHtml(child, context)); | |
| 243 }, | |
| 244 'list': (dom.Element child) { | |
| 245 checkAttributes(child, [], context); | |
| 246 types.add(new TypeList(processContentsAsType(child, context), child)); | |
| 247 }, | |
| 248 'map': (dom.Element child) { | |
| 249 checkAttributes(child, [], context); | |
| 250 TypeDecl keyType; | |
| 251 TypeDecl valueType; | |
| 252 recurse(child, context, { | |
| 253 'key': (dom.Element child) { | |
| 254 if (keyType != null) { | |
| 255 throw new Exception('$context: Key type already specified'); | |
| 256 } | |
| 257 keyType = processContentsAsType(child, '$context.key'); | |
| 258 }, | |
| 259 'value': (dom.Element child) { | |
| 260 if (valueType != null) { | |
| 261 throw new Exception('$context: Value type already specified'); | |
| 262 } | |
| 263 valueType = processContentsAsType(child, '$context.value'); | |
| 264 } | |
| 265 }); | |
| 266 if (keyType == null) { | |
| 267 throw new Exception('$context: Key type not specified'); | |
| 268 } | |
| 269 if (valueType == null) { | |
| 270 throw new Exception('$context: Value type not specified'); | |
| 271 } | |
| 272 types.add(new TypeMap(keyType, valueType, child)); | |
| 273 }, | |
| 274 'enum': (dom.Element child) { | |
| 275 types.add(typeEnumFromHtml(child, context)); | |
| 276 }, | |
| 277 'ref': (dom.Element child) { | |
| 278 checkAttributes(child, [], context); | |
| 279 types.add(new TypeReference(innerText(child), child)); | |
| 280 }, | |
| 281 'union': (dom.Element child) { | |
| 282 checkAttributes(child, ['field'], context); | |
| 283 String field = child.attributes['field']; | |
| 284 types.add( | |
| 285 new TypeUnion(processContentsAsTypes(child, context), field, child)); | |
| 286 } | |
| 287 }); | |
| 288 return types; | |
| 289 } | |
| 290 | |
| 291 /** | |
| 292 * Read the API description from the file 'spec_input.html'. [pkgPath] is the | |
| 293 * path to the current package. | 19 * path to the current package. |
| 294 */ | 20 */ |
| 295 Api readApi(String pkgPath) { | 21 Api readApi(String pkgPath) { |
| 296 File htmlFile = new File(join(pkgPath, 'tool', 'spec', 'spec_input.html')); | 22 ApiReader reader = |
| 297 String htmlContents = htmlFile.readAsStringSync(); | 23 new ApiReader(join(pkgPath, 'tool', 'spec', 'spec_input.html')); |
| 298 dom.Document document = parser.parse(htmlContents); | 24 return reader.readApi(); |
| 299 dom.Element htmlElement = document.children | |
| 300 .singleWhere((element) => element.localName.toLowerCase() == 'html'); | |
| 301 return apiFromHtml(htmlElement); | |
| 302 } | 25 } |
| 303 | 26 |
| 304 void recurse(dom.Element parent, String context, | 27 typedef void ElementProcessor(dom.Element element); |
| 305 Map<String, ElementProcessor> elementProcessors) { | 28 |
| 306 for (String key in elementProcessors.keys) { | 29 typedef void TextProcessor(dom.Text text); |
| 307 if (!specialElements.contains(key)) { | 30 |
| 308 throw new Exception('$context: $key is not a special element'); | 31 class ApiReader { |
| 309 } | 32 static const List<String> specialElements = const [ |
| 310 } | 33 'domain', |
| 311 for (dom.Node node in parent.nodes) { | 34 'feedback', |
| 312 if (node is dom.Element) { | 35 'object', |
| 313 if (elementProcessors.containsKey(node.localName)) { | 36 'refactorings', |
| 314 elementProcessors[node.localName](node); | 37 'refactoring', |
| 315 } else if (specialElements.contains(node.localName)) { | 38 'type', |
| 316 throw new Exception('$context: Unexpected use of <${node.localName}>'); | 39 'types', |
| 317 } else { | 40 'request', |
| 318 recurse(node, context, elementProcessors); | 41 'notification', |
| 319 } | 42 'params', |
| 320 } | 43 'result', |
| 44 'field', |
| 45 'list', |
| 46 'map', |
| 47 'enum', |
| 48 'key', |
| 49 'value', |
| 50 'options', |
| 51 'ref', |
| 52 'code', |
| 53 'version', |
| 54 'union', |
| 55 'index', |
| 56 'include' |
| 57 ]; |
| 58 |
| 59 /** |
| 60 * The absolute and normalized path to the file being read. |
| 61 */ |
| 62 final String filePath; |
| 63 |
| 64 /** |
| 65 * Initialize a newly created API reader to read from the file with the given |
| 66 * [filePath]. |
| 67 */ |
| 68 ApiReader(this.filePath); |
| 69 |
| 70 /** |
| 71 * Create an [Api] object from an HTML representation such as: |
| 72 * |
| 73 * <html> |
| 74 * ... |
| 75 * <body> |
| 76 * ... <version>1.0</version> ... |
| 77 * <domain name="...">...</domain> <!-- zero or more --> |
| 78 * <types>...</types> |
| 79 * <refactorings>...</refactorings> |
| 80 * </body> |
| 81 * </html> |
| 82 * |
| 83 * Child elements of <api> can occur in any order. |
| 84 */ |
| 85 Api apiFromHtml(dom.Element html) { |
| 86 Api api; |
| 87 List<String> versions = <String>[]; |
| 88 List<Domain> domains = <Domain>[]; |
| 89 Types types = null; |
| 90 Refactorings refactorings = null; |
| 91 recurse(html, 'api', { |
| 92 'domain': (dom.Element element) { |
| 93 domains.add(domainFromHtml(element)); |
| 94 }, |
| 95 'refactorings': (dom.Element element) { |
| 96 refactorings = refactoringsFromHtml(element); |
| 97 }, |
| 98 'types': (dom.Element element) { |
| 99 types = typesFromHtml(element); |
| 100 }, |
| 101 'version': (dom.Element element) { |
| 102 versions.add(innerText(element)); |
| 103 }, |
| 104 'index': (dom.Element element) { |
| 105 /* Ignore; generated dynamically. */ |
| 106 } |
| 107 }); |
| 108 if (versions.length != 1) { |
| 109 throw new Exception('The API must contain exactly one <version> element'); |
| 110 } |
| 111 api = new Api(versions[0], domains, types, refactorings, html); |
| 112 return api; |
| 113 } |
| 114 |
| 115 /** |
| 116 * Check that the given [element] has all of the attributes in |
| 117 * [requiredAttributes], possibly some of the attributes in |
| 118 * [optionalAttributes], and no others. |
| 119 */ |
| 120 void checkAttributes( |
| 121 dom.Element element, List<String> requiredAttributes, String context, |
| 122 {List<String> optionalAttributes: const []}) { |
| 123 Set<String> attributesFound = new Set<String>(); |
| 124 element.attributes.forEach((String name, String value) { |
| 125 if (!requiredAttributes.contains(name) && |
| 126 !optionalAttributes.contains(name)) { |
| 127 throw new Exception( |
| 128 '$context: Unexpected attribute in ${element.localName}: $name'); |
| 129 } |
| 130 attributesFound.add(name); |
| 131 }); |
| 132 for (String expectedAttribute in requiredAttributes) { |
| 133 if (!attributesFound.contains(expectedAttribute)) { |
| 134 throw new Exception( |
| 135 '$context: ${element.localName} must contain attribute $expectedAttr
ibute'); |
| 136 } |
| 137 } |
| 138 } |
| 139 |
| 140 /** |
| 141 * Check that the given [element] has the given [expectedName]. |
| 142 */ |
| 143 void checkName(dom.Element element, String expectedName, [String context]) { |
| 144 if (element.localName != expectedName) { |
| 145 if (context == null) { |
| 146 context = element.localName; |
| 147 } |
| 148 throw new Exception( |
| 149 '$context: Expected $expectedName, found ${element.localName}'); |
| 150 } |
| 151 } |
| 152 |
| 153 /** |
| 154 * Create a [Domain] object from an HTML representation such as: |
| 155 * |
| 156 * <domain name="domainName"> |
| 157 * <request method="...">...</request> <!-- zero or more --> |
| 158 * <notification event="...">...</notification> <!-- zero or more --> |
| 159 * </domain> |
| 160 * |
| 161 * Child elements can occur in any order. |
| 162 */ |
| 163 Domain domainFromHtml(dom.Element html) { |
| 164 checkName(html, 'domain'); |
| 165 String name = html.attributes['name']; |
| 166 String context = name ?? 'domain'; |
| 167 bool experimental = html.attributes['experimental'] == 'true'; |
| 168 checkAttributes(html, ['name'], context, |
| 169 optionalAttributes: ['experimental']); |
| 170 List<Request> requests = <Request>[]; |
| 171 List<Notification> notifications = <Notification>[]; |
| 172 recurse(html, context, { |
| 173 'request': (dom.Element child) { |
| 174 requests.add(requestFromHtml(child, context)); |
| 175 }, |
| 176 'notification': (dom.Element child) { |
| 177 notifications.add(notificationFromHtml(child, context)); |
| 178 } |
| 179 }); |
| 180 return new Domain(name, requests, notifications, html, |
| 181 experimental: experimental); |
| 182 } |
| 183 |
| 184 dom.Element getAncestor(dom.Element html, String name, String context) { |
| 185 dom.Element ancestor = html.parent; |
| 186 while (ancestor != null) { |
| 187 if (ancestor.localName == name) { |
| 188 return ancestor; |
| 189 } |
| 190 ancestor = ancestor.parent; |
| 191 } |
| 192 throw new Exception( |
| 193 '$context: <${html.localName}> must be nested within <$name>'); |
| 194 } |
| 195 |
| 196 /** |
| 197 * Create a [Notification] object from an HTML representation such as: |
| 198 * |
| 199 * <notification event="methodName"> |
| 200 * <params>...</params> <!-- optional --> |
| 201 * </notification> |
| 202 * |
| 203 * Note that the event name should not include the domain name. |
| 204 * |
| 205 * <params> has the same form as <object>, as described in [typeDeclFromHtml]. |
| 206 * |
| 207 * Child elements can occur in any order. |
| 208 */ |
| 209 Notification notificationFromHtml(dom.Element html, String context) { |
| 210 String domainName = getAncestor(html, 'domain', context).attributes['name']; |
| 211 checkName(html, 'notification', context); |
| 212 String event = html.attributes['event']; |
| 213 context = '$context.${event != null ? event : 'event'}'; |
| 214 checkAttributes(html, ['event'], context); |
| 215 TypeDecl params; |
| 216 recurse(html, context, { |
| 217 'params': (dom.Element child) { |
| 218 params = typeObjectFromHtml(child, '$context.params'); |
| 219 } |
| 220 }); |
| 221 return new Notification(domainName, event, params, html); |
| 222 } |
| 223 |
| 224 /** |
| 225 * Create a single of [TypeDecl] corresponding to the type defined inside the |
| 226 * given HTML element. |
| 227 */ |
| 228 TypeDecl processContentsAsType(dom.Element html, String context) { |
| 229 List<TypeDecl> types = processContentsAsTypes(html, context); |
| 230 if (types.length != 1) { |
| 231 throw new Exception('$context: Exactly one type must be specified'); |
| 232 } |
| 233 return types[0]; |
| 234 } |
| 235 |
| 236 /** |
| 237 * Create a list of [TypeDecl]s corresponding to the types defined inside the |
| 238 * given HTML element. The following forms are supported. |
| 239 * |
| 240 * To refer to a type declared elsewhere (or a built-in type): |
| 241 * |
| 242 * <ref>typeName</ref> |
| 243 * |
| 244 * For a list: <list>ItemType</list> |
| 245 * |
| 246 * For a map: <map><key>KeyType</key><value>ValueType</value></map> |
| 247 * |
| 248 * For a JSON object: |
| 249 * |
| 250 * <object> |
| 251 * <field name="...">...</field> <!-- zero or more --> |
| 252 * </object> |
| 253 * |
| 254 * For an enum: |
| 255 * |
| 256 * <enum> |
| 257 * <value>...</value> <!-- zero or more --> |
| 258 * </enum> |
| 259 * |
| 260 * For a union type: |
| 261 * <union> |
| 262 * TYPE <!-- zero or more --> |
| 263 * </union> |
| 264 */ |
| 265 List<TypeDecl> processContentsAsTypes(dom.Element html, String context) { |
| 266 List<TypeDecl> types = <TypeDecl>[]; |
| 267 recurse(html, context, { |
| 268 'object': (dom.Element child) { |
| 269 types.add(typeObjectFromHtml(child, context)); |
| 270 }, |
| 271 'list': (dom.Element child) { |
| 272 checkAttributes(child, [], context); |
| 273 types.add(new TypeList(processContentsAsType(child, context), child)); |
| 274 }, |
| 275 'map': (dom.Element child) { |
| 276 checkAttributes(child, [], context); |
| 277 TypeDecl keyType; |
| 278 TypeDecl valueType; |
| 279 recurse(child, context, { |
| 280 'key': (dom.Element child) { |
| 281 if (keyType != null) { |
| 282 throw new Exception('$context: Key type already specified'); |
| 283 } |
| 284 keyType = processContentsAsType(child, '$context.key'); |
| 285 }, |
| 286 'value': (dom.Element child) { |
| 287 if (valueType != null) { |
| 288 throw new Exception('$context: Value type already specified'); |
| 289 } |
| 290 valueType = processContentsAsType(child, '$context.value'); |
| 291 } |
| 292 }); |
| 293 if (keyType == null) { |
| 294 throw new Exception('$context: Key type not specified'); |
| 295 } |
| 296 if (valueType == null) { |
| 297 throw new Exception('$context: Value type not specified'); |
| 298 } |
| 299 types.add(new TypeMap(keyType, valueType, child)); |
| 300 }, |
| 301 'enum': (dom.Element child) { |
| 302 types.add(typeEnumFromHtml(child, context)); |
| 303 }, |
| 304 'ref': (dom.Element child) { |
| 305 checkAttributes(child, [], context); |
| 306 types.add(new TypeReference(innerText(child), child)); |
| 307 }, |
| 308 'union': (dom.Element child) { |
| 309 checkAttributes(child, ['field'], context); |
| 310 String field = child.attributes['field']; |
| 311 types.add(new TypeUnion( |
| 312 processContentsAsTypes(child, context), field, child)); |
| 313 } |
| 314 }); |
| 315 return types; |
| 316 } |
| 317 |
| 318 /** |
| 319 * Read the API description from file with the given [filePath]. |
| 320 */ |
| 321 Api readApi() { |
| 322 String htmlContents = new File(filePath).readAsStringSync(); |
| 323 dom.Document document = parser.parse(htmlContents); |
| 324 dom.Element htmlElement = document.children |
| 325 .singleWhere((element) => element.localName.toLowerCase() == 'html'); |
| 326 return apiFromHtml(htmlElement); |
| 327 } |
| 328 |
| 329 void recurse(dom.Element parent, String context, |
| 330 Map<String, ElementProcessor> elementProcessors) { |
| 331 for (String key in elementProcessors.keys) { |
| 332 if (!specialElements.contains(key)) { |
| 333 throw new Exception('$context: $key is not a special element'); |
| 334 } |
| 335 } |
| 336 for (dom.Node node in parent.nodes) { |
| 337 if (node is dom.Element) { |
| 338 if (elementProcessors.containsKey(node.localName)) { |
| 339 elementProcessors[node.localName](node); |
| 340 } else if (specialElements.contains(node.localName)) { |
| 341 throw new Exception( |
| 342 '$context: Unexpected use of <${node.localName}>'); |
| 343 } else { |
| 344 recurse(node, context, elementProcessors); |
| 345 } |
| 346 } |
| 347 } |
| 348 } |
| 349 |
| 350 /** |
| 351 * Create a [Refactoring] object from an HTML representation such as: |
| 352 * |
| 353 * <refactoring kind="refactoringKind"> |
| 354 * <feedback>...</feedback> <!-- optional --> |
| 355 * <options>...</options> <!-- optional --> |
| 356 * </refactoring> |
| 357 * |
| 358 * <feedback> and <options> have the same form as <object>, as described in |
| 359 * [typeDeclFromHtml]. |
| 360 * |
| 361 * Child elements can occur in any order. |
| 362 */ |
| 363 Refactoring refactoringFromHtml(dom.Element html) { |
| 364 checkName(html, 'refactoring'); |
| 365 String kind = html.attributes['kind']; |
| 366 String context = kind != null ? kind : 'refactoring'; |
| 367 checkAttributes(html, ['kind'], context); |
| 368 TypeDecl feedback; |
| 369 TypeDecl options; |
| 370 recurse(html, context, { |
| 371 'feedback': (dom.Element child) { |
| 372 feedback = typeObjectFromHtml(child, '$context.feedback'); |
| 373 }, |
| 374 'options': (dom.Element child) { |
| 375 options = typeObjectFromHtml(child, '$context.options'); |
| 376 } |
| 377 }); |
| 378 return new Refactoring(kind, feedback, options, html); |
| 379 } |
| 380 |
| 381 /** |
| 382 * Create a [Refactorings] object from an HTML representation such as: |
| 383 * |
| 384 * <refactorings> |
| 385 * <refactoring kind="...">...</refactoring> <!-- zero or more --> |
| 386 * </refactorings> |
| 387 */ |
| 388 Refactorings refactoringsFromHtml(dom.Element html) { |
| 389 checkName(html, 'refactorings'); |
| 390 String context = 'refactorings'; |
| 391 checkAttributes(html, [], context); |
| 392 List<Refactoring> refactorings = <Refactoring>[]; |
| 393 recurse(html, context, { |
| 394 'refactoring': (dom.Element child) { |
| 395 refactorings.add(refactoringFromHtml(child)); |
| 396 } |
| 397 }); |
| 398 return new Refactorings(refactorings, html); |
| 399 } |
| 400 |
| 401 /** |
| 402 * Create a [Request] object from an HTML representation such as: |
| 403 * |
| 404 * <request method="methodName"> |
| 405 * <params>...</params> <!-- optional --> |
| 406 * <result>...</result> <!-- optional --> |
| 407 * </request> |
| 408 * |
| 409 * Note that the method name should not include the domain name. |
| 410 * |
| 411 * <params> and <result> have the same form as <object>, as described in |
| 412 * [typeDeclFromHtml]. |
| 413 * |
| 414 * Child elements can occur in any order. |
| 415 */ |
| 416 Request requestFromHtml(dom.Element html, String context) { |
| 417 String domainName = getAncestor(html, 'domain', context).attributes['name']; |
| 418 checkName(html, 'request', context); |
| 419 String method = html.attributes['method']; |
| 420 context = '$context.${method != null ? method : 'method'}'; |
| 421 checkAttributes(html, ['method'], context, |
| 422 optionalAttributes: ['experimental', 'deprecated']); |
| 423 bool experimental = html.attributes['experimental'] == 'true'; |
| 424 bool deprecated = html.attributes['deprecated'] == 'true'; |
| 425 TypeDecl params; |
| 426 TypeDecl result; |
| 427 recurse(html, context, { |
| 428 'params': (dom.Element child) { |
| 429 params = typeObjectFromHtml(child, '$context.params'); |
| 430 }, |
| 431 'result': (dom.Element child) { |
| 432 result = typeObjectFromHtml(child, '$context.result'); |
| 433 } |
| 434 }); |
| 435 return new Request(domainName, method, params, result, html, |
| 436 experimental: experimental, deprecated: deprecated); |
| 437 } |
| 438 |
| 439 /** |
| 440 * Create a [TypeDefinition] object from an HTML representation such as: |
| 441 * |
| 442 * <type name="typeName"> |
| 443 * TYPE |
| 444 * </type> |
| 445 * |
| 446 * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml]. |
| 447 * |
| 448 * Child elements can occur in any order. |
| 449 */ |
| 450 TypeDefinition typeDefinitionFromHtml(dom.Element html) { |
| 451 checkName(html, 'type'); |
| 452 String name = html.attributes['name']; |
| 453 String context = name != null ? name : 'type'; |
| 454 checkAttributes(html, ['name'], context, |
| 455 optionalAttributes: ['experimental', 'deprecated']); |
| 456 TypeDecl type = processContentsAsType(html, context); |
| 457 bool experimental = html.attributes['experimental'] == 'true'; |
| 458 bool deprecated = html.attributes['deprecated'] == 'true'; |
| 459 return new TypeDefinition(name, type, html, |
| 460 experimental: experimental, deprecated: deprecated); |
| 461 } |
| 462 |
| 463 /** |
| 464 * Create a [TypeEnum] from an HTML description. |
| 465 */ |
| 466 TypeEnum typeEnumFromHtml(dom.Element html, String context) { |
| 467 checkName(html, 'enum', context); |
| 468 checkAttributes(html, [], context); |
| 469 List<TypeEnumValue> values = <TypeEnumValue>[]; |
| 470 recurse(html, context, { |
| 471 'value': (dom.Element child) { |
| 472 values.add(typeEnumValueFromHtml(child, context)); |
| 473 } |
| 474 }); |
| 475 return new TypeEnum(values, html); |
| 476 } |
| 477 |
| 478 /** |
| 479 * Create a [TypeEnumValue] from an HTML description such as: |
| 480 * |
| 481 * <enum> |
| 482 * <code>VALUE</code> |
| 483 * </enum> |
| 484 * |
| 485 * Where VALUE is the text of the enumerated value. |
| 486 * |
| 487 * Child elements can occur in any order. |
| 488 */ |
| 489 TypeEnumValue typeEnumValueFromHtml(dom.Element html, String context) { |
| 490 checkName(html, 'value', context); |
| 491 checkAttributes(html, [], context, optionalAttributes: ['deprecated']); |
| 492 bool deprecated = html.attributes['deprecated'] == 'true'; |
| 493 List<String> values = <String>[]; |
| 494 recurse(html, context, { |
| 495 'code': (dom.Element child) { |
| 496 String text = innerText(child).trim(); |
| 497 values.add(text); |
| 498 } |
| 499 }); |
| 500 if (values.length != 1) { |
| 501 throw new Exception('$context: Exactly one value must be specified'); |
| 502 } |
| 503 return new TypeEnumValue(values[0], html, deprecated: deprecated); |
| 504 } |
| 505 |
| 506 /** |
| 507 * Create a [TypeObjectField] from an HTML description such as: |
| 508 * |
| 509 * <field name="fieldName"> |
| 510 * TYPE |
| 511 * </field> |
| 512 * |
| 513 * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml]. |
| 514 * |
| 515 * In addition, the attribute optional="true" may be used to specify that the |
| 516 * field is optional, and the attribute value="..." may be used to specify tha
t |
| 517 * the field is required to have a certain value. |
| 518 * |
| 519 * Child elements can occur in any order. |
| 520 */ |
| 521 TypeObjectField typeObjectFieldFromHtml(dom.Element html, String context) { |
| 522 checkName(html, 'field', context); |
| 523 String name = html.attributes['name']; |
| 524 context = '$context.${name != null ? name : 'field'}'; |
| 525 checkAttributes(html, ['name'], context, |
| 526 optionalAttributes: ['optional', 'value', 'deprecated']); |
| 527 bool deprecated = html.attributes['deprecated'] == 'true'; |
| 528 bool optional = false; |
| 529 String optionalString = html.attributes['optional']; |
| 530 if (optionalString != null) { |
| 531 switch (optionalString) { |
| 532 case 'true': |
| 533 optional = true; |
| 534 break; |
| 535 case 'false': |
| 536 optional = false; |
| 537 break; |
| 538 default: |
| 539 throw new Exception( |
| 540 '$context: field contains invalid "optional" attribute: "$optional
String"'); |
| 541 } |
| 542 } |
| 543 String value = html.attributes['value']; |
| 544 TypeDecl type = processContentsAsType(html, context); |
| 545 return new TypeObjectField(name, type, html, |
| 546 optional: optional, value: value, deprecated: deprecated); |
| 547 } |
| 548 |
| 549 /** |
| 550 * Create a [TypeObject] from an HTML description. |
| 551 */ |
| 552 TypeObject typeObjectFromHtml(dom.Element html, String context) { |
| 553 checkAttributes(html, [], context, optionalAttributes: ['experimental']); |
| 554 List<TypeObjectField> fields = <TypeObjectField>[]; |
| 555 recurse(html, context, { |
| 556 'field': (dom.Element child) { |
| 557 fields.add(typeObjectFieldFromHtml(child, context)); |
| 558 } |
| 559 }); |
| 560 bool experimental = html.attributes['experimental'] == 'true'; |
| 561 return new TypeObject(fields, html, experimental: experimental); |
| 562 } |
| 563 |
| 564 /** |
| 565 * Create a [Types] object from an HTML representation such as: |
| 566 * |
| 567 * <types> |
| 568 * <type name="...">...</type> <!-- zero or more --> |
| 569 * </types> |
| 570 */ |
| 571 Types typesFromHtml(dom.Element html) { |
| 572 checkName(html, 'types'); |
| 573 String context = 'types'; |
| 574 checkAttributes(html, [], context); |
| 575 List<String> importUris = <String>[]; |
| 576 Map<String, TypeDefinition> typeMap = <String, TypeDefinition>{}; |
| 577 List<dom.Element> childElements = <dom.Element>[]; |
| 578 recurse(html, context, { |
| 579 'include': (dom.Element child) { |
| 580 String importUri = child.attributes['import']; |
| 581 if (importUri != null) { |
| 582 importUris.add(importUri); |
| 583 } |
| 584 String relativePath = child.attributes['path']; |
| 585 String path = normalize(join(dirname(filePath), relativePath)); |
| 586 ApiReader reader = new ApiReader(path); |
| 587 Api api = reader.readApi(); |
| 588 for (TypeDefinition typeDefinition in api.types) { |
| 589 typeDefinition.isExternal = true; |
| 590 childElements.add(typeDefinition.html); |
| 591 typeMap[typeDefinition.name] = typeDefinition; |
| 592 } |
| 593 }, |
| 594 'type': (dom.Element child) { |
| 595 TypeDefinition typeDefinition = typeDefinitionFromHtml(child); |
| 596 typeMap[typeDefinition.name] = typeDefinition; |
| 597 } |
| 598 }); |
| 599 for (dom.Element element in childElements) { |
| 600 html.append(element); |
| 601 } |
| 602 Types types = new Types(typeMap, html); |
| 603 types.importUris.addAll(importUris); |
| 604 return types; |
| 321 } | 605 } |
| 322 } | 606 } |
| 323 | |
| 324 /** | |
| 325 * Create a [Refactoring] object from an HTML representation such as: | |
| 326 * | |
| 327 * <refactoring kind="refactoringKind"> | |
| 328 * <feedback>...</feedback> <!-- optional --> | |
| 329 * <options>...</options> <!-- optional --> | |
| 330 * </refactoring> | |
| 331 * | |
| 332 * <feedback> and <options> have the same form as <object>, as described in | |
| 333 * [typeDeclFromHtml]. | |
| 334 * | |
| 335 * Child elements can occur in any order. | |
| 336 */ | |
| 337 Refactoring refactoringFromHtml(dom.Element html) { | |
| 338 checkName(html, 'refactoring'); | |
| 339 String kind = html.attributes['kind']; | |
| 340 String context = kind != null ? kind : 'refactoring'; | |
| 341 checkAttributes(html, ['kind'], context); | |
| 342 TypeDecl feedback; | |
| 343 TypeDecl options; | |
| 344 recurse(html, context, { | |
| 345 'feedback': (dom.Element child) { | |
| 346 feedback = typeObjectFromHtml(child, '$context.feedback'); | |
| 347 }, | |
| 348 'options': (dom.Element child) { | |
| 349 options = typeObjectFromHtml(child, '$context.options'); | |
| 350 } | |
| 351 }); | |
| 352 return new Refactoring(kind, feedback, options, html); | |
| 353 } | |
| 354 | |
| 355 /** | |
| 356 * Create a [Refactorings] object from an HTML representation such as: | |
| 357 * | |
| 358 * <refactorings> | |
| 359 * <refactoring kind="...">...</refactoring> <!-- zero or more --> | |
| 360 * </refactorings> | |
| 361 */ | |
| 362 Refactorings refactoringsFromHtml(dom.Element html) { | |
| 363 checkName(html, 'refactorings'); | |
| 364 String context = 'refactorings'; | |
| 365 checkAttributes(html, [], context); | |
| 366 List<Refactoring> refactorings = <Refactoring>[]; | |
| 367 recurse(html, context, { | |
| 368 'refactoring': (dom.Element child) { | |
| 369 refactorings.add(refactoringFromHtml(child)); | |
| 370 } | |
| 371 }); | |
| 372 return new Refactorings(refactorings, html); | |
| 373 } | |
| 374 | |
| 375 /** | |
| 376 * Create a [Request] object from an HTML representation such as: | |
| 377 * | |
| 378 * <request method="methodName"> | |
| 379 * <params>...</params> <!-- optional --> | |
| 380 * <result>...</result> <!-- optional --> | |
| 381 * </request> | |
| 382 * | |
| 383 * Note that the method name should not include the domain name. | |
| 384 * | |
| 385 * <params> and <result> have the same form as <object>, as described in | |
| 386 * [typeDeclFromHtml]. | |
| 387 * | |
| 388 * Child elements can occur in any order. | |
| 389 */ | |
| 390 Request requestFromHtml(dom.Element html, String context) { | |
| 391 String domainName = getAncestor(html, 'domain', context).attributes['name']; | |
| 392 checkName(html, 'request', context); | |
| 393 String method = html.attributes['method']; | |
| 394 context = '$context.${method != null ? method : 'method'}'; | |
| 395 checkAttributes(html, ['method'], context, | |
| 396 optionalAttributes: ['experimental', 'deprecated']); | |
| 397 bool experimental = html.attributes['experimental'] == 'true'; | |
| 398 bool deprecated = html.attributes['deprecated'] == 'true'; | |
| 399 TypeDecl params; | |
| 400 TypeDecl result; | |
| 401 recurse(html, context, { | |
| 402 'params': (dom.Element child) { | |
| 403 params = typeObjectFromHtml(child, '$context.params'); | |
| 404 }, | |
| 405 'result': (dom.Element child) { | |
| 406 result = typeObjectFromHtml(child, '$context.result'); | |
| 407 } | |
| 408 }); | |
| 409 return new Request(domainName, method, params, result, html, | |
| 410 experimental: experimental, deprecated: deprecated); | |
| 411 } | |
| 412 | |
| 413 /** | |
| 414 * Create a [TypeDefinition] object from an HTML representation such as: | |
| 415 * | |
| 416 * <type name="typeName"> | |
| 417 * TYPE | |
| 418 * </type> | |
| 419 * | |
| 420 * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml]. | |
| 421 * | |
| 422 * Child elements can occur in any order. | |
| 423 */ | |
| 424 TypeDefinition typeDefinitionFromHtml(dom.Element html) { | |
| 425 checkName(html, 'type'); | |
| 426 String name = html.attributes['name']; | |
| 427 String context = name != null ? name : 'type'; | |
| 428 checkAttributes(html, ['name'], context, | |
| 429 optionalAttributes: ['experimental', 'deprecated']); | |
| 430 TypeDecl type = processContentsAsType(html, context); | |
| 431 bool experimental = html.attributes['experimental'] == 'true'; | |
| 432 bool deprecated = html.attributes['deprecated'] == 'true'; | |
| 433 return new TypeDefinition(name, type, html, | |
| 434 experimental: experimental, deprecated: deprecated); | |
| 435 } | |
| 436 | |
| 437 /** | |
| 438 * Create a [TypeEnum] from an HTML description. | |
| 439 */ | |
| 440 TypeEnum typeEnumFromHtml(dom.Element html, String context) { | |
| 441 checkName(html, 'enum', context); | |
| 442 checkAttributes(html, [], context); | |
| 443 List<TypeEnumValue> values = <TypeEnumValue>[]; | |
| 444 recurse(html, context, { | |
| 445 'value': (dom.Element child) { | |
| 446 values.add(typeEnumValueFromHtml(child, context)); | |
| 447 } | |
| 448 }); | |
| 449 return new TypeEnum(values, html); | |
| 450 } | |
| 451 | |
| 452 /** | |
| 453 * Create a [TypeEnumValue] from an HTML description such as: | |
| 454 * | |
| 455 * <enum> | |
| 456 * <code>VALUE</code> | |
| 457 * </enum> | |
| 458 * | |
| 459 * Where VALUE is the text of the enumerated value. | |
| 460 * | |
| 461 * Child elements can occur in any order. | |
| 462 */ | |
| 463 TypeEnumValue typeEnumValueFromHtml(dom.Element html, String context) { | |
| 464 checkName(html, 'value', context); | |
| 465 checkAttributes(html, [], context, optionalAttributes: ['deprecated']); | |
| 466 bool deprecated = html.attributes['deprecated'] == 'true'; | |
| 467 List<String> values = <String>[]; | |
| 468 recurse(html, context, { | |
| 469 'code': (dom.Element child) { | |
| 470 String text = innerText(child).trim(); | |
| 471 values.add(text); | |
| 472 } | |
| 473 }); | |
| 474 if (values.length != 1) { | |
| 475 throw new Exception('$context: Exactly one value must be specified'); | |
| 476 } | |
| 477 return new TypeEnumValue(values[0], html, deprecated: deprecated); | |
| 478 } | |
| 479 | |
| 480 /** | |
| 481 * Create a [TypeObjectField] from an HTML description such as: | |
| 482 * | |
| 483 * <field name="fieldName"> | |
| 484 * TYPE | |
| 485 * </field> | |
| 486 * | |
| 487 * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml]. | |
| 488 * | |
| 489 * In addition, the attribute optional="true" may be used to specify that the | |
| 490 * field is optional, and the attribute value="..." may be used to specify that | |
| 491 * the field is required to have a certain value. | |
| 492 * | |
| 493 * Child elements can occur in any order. | |
| 494 */ | |
| 495 TypeObjectField typeObjectFieldFromHtml(dom.Element html, String context) { | |
| 496 checkName(html, 'field', context); | |
| 497 String name = html.attributes['name']; | |
| 498 context = '$context.${name != null ? name : 'field'}'; | |
| 499 checkAttributes(html, ['name'], context, | |
| 500 optionalAttributes: ['optional', 'value', 'deprecated']); | |
| 501 bool deprecated = html.attributes['deprecated'] == 'true'; | |
| 502 bool optional = false; | |
| 503 String optionalString = html.attributes['optional']; | |
| 504 if (optionalString != null) { | |
| 505 switch (optionalString) { | |
| 506 case 'true': | |
| 507 optional = true; | |
| 508 break; | |
| 509 case 'false': | |
| 510 optional = false; | |
| 511 break; | |
| 512 default: | |
| 513 throw new Exception( | |
| 514 '$context: field contains invalid "optional" attribute: "$optionalSt
ring"'); | |
| 515 } | |
| 516 } | |
| 517 String value = html.attributes['value']; | |
| 518 TypeDecl type = processContentsAsType(html, context); | |
| 519 return new TypeObjectField(name, type, html, | |
| 520 optional: optional, value: value, deprecated: deprecated); | |
| 521 } | |
| 522 | |
| 523 /** | |
| 524 * Create a [TypeObject] from an HTML description. | |
| 525 */ | |
| 526 TypeObject typeObjectFromHtml(dom.Element html, String context) { | |
| 527 checkAttributes(html, [], context, optionalAttributes: ['experimental']); | |
| 528 List<TypeObjectField> fields = <TypeObjectField>[]; | |
| 529 recurse(html, context, { | |
| 530 'field': (dom.Element child) { | |
| 531 fields.add(typeObjectFieldFromHtml(child, context)); | |
| 532 } | |
| 533 }); | |
| 534 bool experimental = html.attributes['experimental'] == 'true'; | |
| 535 return new TypeObject(fields, html, experimental: experimental); | |
| 536 } | |
| 537 | |
| 538 /** | |
| 539 * Create a [Types] object from an HTML representation such as: | |
| 540 * | |
| 541 * <types> | |
| 542 * <type name="...">...</type> <!-- zero or more --> | |
| 543 * </types> | |
| 544 */ | |
| 545 Types typesFromHtml(dom.Element html) { | |
| 546 checkName(html, 'types'); | |
| 547 String context = 'types'; | |
| 548 checkAttributes(html, [], context); | |
| 549 Map<String, TypeDefinition> types = <String, TypeDefinition>{}; | |
| 550 recurse(html, context, { | |
| 551 'type': (dom.Element child) { | |
| 552 TypeDefinition typeDefinition = typeDefinitionFromHtml(child); | |
| 553 types[typeDefinition.name] = typeDefinition; | |
| 554 } | |
| 555 }); | |
| 556 return new Types(types, html); | |
| 557 } | |
| 558 | |
| 559 typedef void ElementProcessor(dom.Element element); | |
| 560 | |
| 561 typedef void TextProcessor(dom.Text text); | |
| OLD | NEW |