| 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 | 
|---|