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