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