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 |