OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, 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 import 'dart:io'; |
| 9 |
| 10 import 'package:analyzer/src/codegen/html.dart'; |
| 11 import 'package:html/dom.dart' as dom; |
| 12 import 'package:html/parser.dart' as parser; |
| 13 import 'package:path/path.dart'; |
| 14 |
| 15 import 'api.dart'; |
| 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 /** |
| 44 * Create an [Api] object from an HTML representation such as: |
| 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. |
| 294 */ |
| 295 Api readApi(String pkgPath) { |
| 296 File htmlFile = new File(join(pkgPath, 'tool', 'spec', 'plugin_spec.html')); |
| 297 String htmlContents = htmlFile.readAsStringSync(); |
| 298 dom.Document document = parser.parse(htmlContents); |
| 299 dom.Element htmlElement = document.children |
| 300 .singleWhere((element) => element.localName.toLowerCase() == 'html'); |
| 301 return apiFromHtml(htmlElement); |
| 302 } |
| 303 |
| 304 void recurse(dom.Element parent, String context, |
| 305 Map<String, ElementProcessor> elementProcessors) { |
| 306 for (String key in elementProcessors.keys) { |
| 307 if (!specialElements.contains(key)) { |
| 308 throw new Exception('$context: $key is not a special element'); |
| 309 } |
| 310 } |
| 311 for (dom.Node node in parent.nodes) { |
| 312 if (node is dom.Element) { |
| 313 if (elementProcessors.containsKey(node.localName)) { |
| 314 elementProcessors[node.localName](node); |
| 315 } else if (specialElements.contains(node.localName)) { |
| 316 throw new Exception('$context: Unexpected use of <${node.localName}>'); |
| 317 } else { |
| 318 recurse(node, context, elementProcessors); |
| 319 } |
| 320 } |
| 321 } |
| 322 } |
| 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 TypeDecl params; |
| 397 TypeDecl result; |
| 398 recurse(html, context, { |
| 399 'params': (dom.Element child) { |
| 400 params = typeObjectFromHtml(child, '$context.params'); |
| 401 }, |
| 402 'result': (dom.Element child) { |
| 403 result = typeObjectFromHtml(child, '$context.result'); |
| 404 } |
| 405 }); |
| 406 return new Request(domainName, method, params, result, html); |
| 407 } |
| 408 |
| 409 /** |
| 410 * Create a [TypeDefinition] object from an HTML representation such as: |
| 411 * |
| 412 * <type name="typeName"> |
| 413 * TYPE |
| 414 * </type> |
| 415 * |
| 416 * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml]. |
| 417 * |
| 418 * Child elements can occur in any order. |
| 419 */ |
| 420 TypeDefinition typeDefinitionFromHtml(dom.Element html) { |
| 421 checkName(html, 'type'); |
| 422 String name = html.attributes['name']; |
| 423 String context = name != null ? name : 'type'; |
| 424 checkAttributes(html, ['name'], context, |
| 425 optionalAttributes: ['experimental']); |
| 426 TypeDecl type = processContentsAsType(html, context); |
| 427 bool experimental = html.attributes['experimental'] == 'true'; |
| 428 return new TypeDefinition(name, type, html, experimental: experimental); |
| 429 } |
| 430 |
| 431 /** |
| 432 * Create a [TypeEnum] from an HTML description. |
| 433 */ |
| 434 TypeEnum typeEnumFromHtml(dom.Element html, String context) { |
| 435 checkName(html, 'enum', context); |
| 436 checkAttributes(html, [], context); |
| 437 List<TypeEnumValue> values = <TypeEnumValue>[]; |
| 438 recurse(html, context, { |
| 439 'value': (dom.Element child) { |
| 440 values.add(typeEnumValueFromHtml(child, context)); |
| 441 } |
| 442 }); |
| 443 return new TypeEnum(values, html); |
| 444 } |
| 445 |
| 446 /** |
| 447 * Create a [TypeEnumValue] from an HTML description such as: |
| 448 * |
| 449 * <enum> |
| 450 * <code>VALUE</code> |
| 451 * </enum> |
| 452 * |
| 453 * Where VALUE is the text of the enumerated value. |
| 454 * |
| 455 * Child elements can occur in any order. |
| 456 */ |
| 457 TypeEnumValue typeEnumValueFromHtml(dom.Element html, String context) { |
| 458 checkName(html, 'value', context); |
| 459 checkAttributes(html, [], context); |
| 460 List<String> values = <String>[]; |
| 461 recurse(html, context, { |
| 462 'code': (dom.Element child) { |
| 463 String text = innerText(child).trim(); |
| 464 values.add(text); |
| 465 } |
| 466 }); |
| 467 if (values.length != 1) { |
| 468 throw new Exception('$context: Exactly one value must be specified'); |
| 469 } |
| 470 return new TypeEnumValue(values[0], html); |
| 471 } |
| 472 |
| 473 /** |
| 474 * Create a [TypeObjectField] from an HTML description such as: |
| 475 * |
| 476 * <field name="fieldName"> |
| 477 * TYPE |
| 478 * </field> |
| 479 * |
| 480 * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml]. |
| 481 * |
| 482 * In addition, the attribute optional="true" may be used to specify that the |
| 483 * field is optional, and the attribute value="..." may be used to specify that |
| 484 * the field is required to have a certain value. |
| 485 * |
| 486 * Child elements can occur in any order. |
| 487 */ |
| 488 TypeObjectField typeObjectFieldFromHtml(dom.Element html, String context) { |
| 489 checkName(html, 'field', context); |
| 490 String name = html.attributes['name']; |
| 491 context = '$context.${name != null ? name : 'field'}'; |
| 492 checkAttributes(html, ['name'], context, |
| 493 optionalAttributes: ['optional', 'value']); |
| 494 bool optional = false; |
| 495 String optionalString = html.attributes['optional']; |
| 496 if (optionalString != null) { |
| 497 switch (optionalString) { |
| 498 case 'true': |
| 499 optional = true; |
| 500 break; |
| 501 case 'false': |
| 502 optional = false; |
| 503 break; |
| 504 default: |
| 505 throw new Exception( |
| 506 '$context: field contains invalid "optional" attribute: "$optionalSt
ring"'); |
| 507 } |
| 508 } |
| 509 String value = html.attributes['value']; |
| 510 TypeDecl type = processContentsAsType(html, context); |
| 511 return new TypeObjectField(name, type, html, |
| 512 optional: optional, value: value); |
| 513 } |
| 514 |
| 515 /** |
| 516 * Create a [TypeObject] from an HTML description. |
| 517 */ |
| 518 TypeObject typeObjectFromHtml(dom.Element html, String context) { |
| 519 checkAttributes(html, [], context, optionalAttributes: ['experimental']); |
| 520 List<TypeObjectField> fields = <TypeObjectField>[]; |
| 521 recurse(html, context, { |
| 522 'field': (dom.Element child) { |
| 523 fields.add(typeObjectFieldFromHtml(child, context)); |
| 524 } |
| 525 }); |
| 526 bool experimental = html.attributes['experimental'] == 'true'; |
| 527 return new TypeObject(fields, html, experimental: experimental); |
| 528 } |
| 529 |
| 530 /** |
| 531 * Create a [Types] object from an HTML representation such as: |
| 532 * |
| 533 * <types> |
| 534 * <type name="...">...</type> <!-- zero or more --> |
| 535 * </types> |
| 536 */ |
| 537 Types typesFromHtml(dom.Element html) { |
| 538 checkName(html, 'types'); |
| 539 String context = 'types'; |
| 540 checkAttributes(html, [], context); |
| 541 Map<String, TypeDefinition> types = <String, TypeDefinition>{}; |
| 542 recurse(html, context, { |
| 543 'type': (dom.Element child) { |
| 544 TypeDefinition typeDefinition = typeDefinitionFromHtml(child); |
| 545 types[typeDefinition.name] = typeDefinition; |
| 546 } |
| 547 }); |
| 548 return new Types(types, html); |
| 549 } |
| 550 |
| 551 typedef void ElementProcessor(dom.Element element); |
| 552 |
| 553 typedef void TextProcessor(dom.Text text); |
OLD | NEW |