| Index: pkg/analyzer_plugin/tool/spec/from_html.dart
|
| diff --git a/pkg/analyzer_plugin/tool/spec/from_html.dart b/pkg/analyzer_plugin/tool/spec/from_html.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..11aa5ad4113431d52257a9f4abded777a8618ceb
|
| --- /dev/null
|
| +++ b/pkg/analyzer_plugin/tool/spec/from_html.dart
|
| @@ -0,0 +1,553 @@
|
| +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +/**
|
| + * Code for reading an HTML API description.
|
| + */
|
| +import 'dart:io';
|
| +
|
| +import 'package:analyzer/src/codegen/html.dart';
|
| +import 'package:html/dom.dart' as dom;
|
| +import 'package:html/parser.dart' as parser;
|
| +import 'package:path/path.dart';
|
| +
|
| +import 'api.dart';
|
| +
|
| +const List<String> specialElements = const [
|
| + 'domain',
|
| + 'feedback',
|
| + 'object',
|
| + 'refactorings',
|
| + 'refactoring',
|
| + 'type',
|
| + 'types',
|
| + 'request',
|
| + 'notification',
|
| + 'params',
|
| + 'result',
|
| + 'field',
|
| + 'list',
|
| + 'map',
|
| + 'enum',
|
| + 'key',
|
| + 'value',
|
| + 'options',
|
| + 'ref',
|
| + 'code',
|
| + 'version',
|
| + 'union',
|
| + 'index'
|
| +];
|
| +
|
| +/**
|
| + * Create an [Api] object from an HTML representation such as:
|
| + *
|
| + * <html>
|
| + * ...
|
| + * <body>
|
| + * ... <version>1.0</version> ...
|
| + * <domain name="...">...</domain> <!-- zero or more -->
|
| + * <types>...</types>
|
| + * <refactorings>...</refactorings>
|
| + * </body>
|
| + * </html>
|
| + *
|
| + * Child elements of <api> can occur in any order.
|
| + */
|
| +Api apiFromHtml(dom.Element html) {
|
| + Api api;
|
| + List<String> versions = <String>[];
|
| + List<Domain> domains = <Domain>[];
|
| + Types types = null;
|
| + Refactorings refactorings = null;
|
| + recurse(html, 'api', {
|
| + 'domain': (dom.Element element) {
|
| + domains.add(domainFromHtml(element));
|
| + },
|
| + 'refactorings': (dom.Element element) {
|
| + refactorings = refactoringsFromHtml(element);
|
| + },
|
| + 'types': (dom.Element element) {
|
| + types = typesFromHtml(element);
|
| + },
|
| + 'version': (dom.Element element) {
|
| + versions.add(innerText(element));
|
| + },
|
| + 'index': (dom.Element element) {
|
| + /* Ignore; generated dynamically. */
|
| + }
|
| + });
|
| + if (versions.length != 1) {
|
| + throw new Exception('The API must contain exactly one <version> element');
|
| + }
|
| + api = new Api(versions[0], domains, types, refactorings, html);
|
| + return api;
|
| +}
|
| +
|
| +/**
|
| + * Check that the given [element] has all of the attributes in
|
| + * [requiredAttributes], possibly some of the attributes in
|
| + * [optionalAttributes], and no others.
|
| + */
|
| +void checkAttributes(
|
| + dom.Element element, List<String> requiredAttributes, String context,
|
| + {List<String> optionalAttributes: const []}) {
|
| + Set<String> attributesFound = new Set<String>();
|
| + element.attributes.forEach((String name, String value) {
|
| + if (!requiredAttributes.contains(name) &&
|
| + !optionalAttributes.contains(name)) {
|
| + throw new Exception(
|
| + '$context: Unexpected attribute in ${element.localName}: $name');
|
| + }
|
| + attributesFound.add(name);
|
| + });
|
| + for (String expectedAttribute in requiredAttributes) {
|
| + if (!attributesFound.contains(expectedAttribute)) {
|
| + throw new Exception(
|
| + '$context: ${element.localName} must contain attribute $expectedAttribute');
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Check that the given [element] has the given [expectedName].
|
| + */
|
| +void checkName(dom.Element element, String expectedName, [String context]) {
|
| + if (element.localName != expectedName) {
|
| + if (context == null) {
|
| + context = element.localName;
|
| + }
|
| + throw new Exception(
|
| + '$context: Expected $expectedName, found ${element.localName}');
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Create a [Domain] object from an HTML representation such as:
|
| + *
|
| + * <domain name="domainName">
|
| + * <request method="...">...</request> <!-- zero or more -->
|
| + * <notification event="...">...</notification> <!-- zero or more -->
|
| + * </domain>
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +Domain domainFromHtml(dom.Element html) {
|
| + checkName(html, 'domain');
|
| + String name = html.attributes['name'];
|
| + String context = name ?? 'domain';
|
| + bool experimental = html.attributes['experimental'] == 'true';
|
| + checkAttributes(html, ['name'], context,
|
| + optionalAttributes: ['experimental']);
|
| + List<Request> requests = <Request>[];
|
| + List<Notification> notifications = <Notification>[];
|
| + recurse(html, context, {
|
| + 'request': (dom.Element child) {
|
| + requests.add(requestFromHtml(child, context));
|
| + },
|
| + 'notification': (dom.Element child) {
|
| + notifications.add(notificationFromHtml(child, context));
|
| + }
|
| + });
|
| + return new Domain(name, requests, notifications, html,
|
| + experimental: experimental);
|
| +}
|
| +
|
| +dom.Element getAncestor(dom.Element html, String name, String context) {
|
| + dom.Element ancestor = html.parent;
|
| + while (ancestor != null) {
|
| + if (ancestor.localName == name) {
|
| + return ancestor;
|
| + }
|
| + ancestor = ancestor.parent;
|
| + }
|
| + throw new Exception(
|
| + '$context: <${html.localName}> must be nested within <$name>');
|
| +}
|
| +
|
| +/**
|
| + * Create a [Notification] object from an HTML representation such as:
|
| + *
|
| + * <notification event="methodName">
|
| + * <params>...</params> <!-- optional -->
|
| + * </notification>
|
| + *
|
| + * Note that the event name should not include the domain name.
|
| + *
|
| + * <params> has the same form as <object>, as described in [typeDeclFromHtml].
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +Notification notificationFromHtml(dom.Element html, String context) {
|
| + String domainName = getAncestor(html, 'domain', context).attributes['name'];
|
| + checkName(html, 'notification', context);
|
| + String event = html.attributes['event'];
|
| + context = '$context.${event != null ? event : 'event'}';
|
| + checkAttributes(html, ['event'], context);
|
| + TypeDecl params;
|
| + recurse(html, context, {
|
| + 'params': (dom.Element child) {
|
| + params = typeObjectFromHtml(child, '$context.params');
|
| + }
|
| + });
|
| + return new Notification(domainName, event, params, html);
|
| +}
|
| +
|
| +/**
|
| + * Create a single of [TypeDecl] corresponding to the type defined inside the
|
| + * given HTML element.
|
| + */
|
| +TypeDecl processContentsAsType(dom.Element html, String context) {
|
| + List<TypeDecl> types = processContentsAsTypes(html, context);
|
| + if (types.length != 1) {
|
| + throw new Exception('$context: Exactly one type must be specified');
|
| + }
|
| + return types[0];
|
| +}
|
| +
|
| +/**
|
| + * Create a list of [TypeDecl]s corresponding to the types defined inside the
|
| + * given HTML element. The following forms are supported.
|
| + *
|
| + * To refer to a type declared elsewhere (or a built-in type):
|
| + *
|
| + * <ref>typeName</ref>
|
| + *
|
| + * For a list: <list>ItemType</list>
|
| + *
|
| + * For a map: <map><key>KeyType</key><value>ValueType</value></map>
|
| + *
|
| + * For a JSON object:
|
| + *
|
| + * <object>
|
| + * <field name="...">...</field> <!-- zero or more -->
|
| + * </object>
|
| + *
|
| + * For an enum:
|
| + *
|
| + * <enum>
|
| + * <value>...</value> <!-- zero or more -->
|
| + * </enum>
|
| + *
|
| + * For a union type:
|
| + * <union>
|
| + * TYPE <!-- zero or more -->
|
| + * </union>
|
| + */
|
| +List<TypeDecl> processContentsAsTypes(dom.Element html, String context) {
|
| + List<TypeDecl> types = <TypeDecl>[];
|
| + recurse(html, context, {
|
| + 'object': (dom.Element child) {
|
| + types.add(typeObjectFromHtml(child, context));
|
| + },
|
| + 'list': (dom.Element child) {
|
| + checkAttributes(child, [], context);
|
| + types.add(new TypeList(processContentsAsType(child, context), child));
|
| + },
|
| + 'map': (dom.Element child) {
|
| + checkAttributes(child, [], context);
|
| + TypeDecl keyType;
|
| + TypeDecl valueType;
|
| + recurse(child, context, {
|
| + 'key': (dom.Element child) {
|
| + if (keyType != null) {
|
| + throw new Exception('$context: Key type already specified');
|
| + }
|
| + keyType = processContentsAsType(child, '$context.key');
|
| + },
|
| + 'value': (dom.Element child) {
|
| + if (valueType != null) {
|
| + throw new Exception('$context: Value type already specified');
|
| + }
|
| + valueType = processContentsAsType(child, '$context.value');
|
| + }
|
| + });
|
| + if (keyType == null) {
|
| + throw new Exception('$context: Key type not specified');
|
| + }
|
| + if (valueType == null) {
|
| + throw new Exception('$context: Value type not specified');
|
| + }
|
| + types.add(new TypeMap(keyType, valueType, child));
|
| + },
|
| + 'enum': (dom.Element child) {
|
| + types.add(typeEnumFromHtml(child, context));
|
| + },
|
| + 'ref': (dom.Element child) {
|
| + checkAttributes(child, [], context);
|
| + types.add(new TypeReference(innerText(child), child));
|
| + },
|
| + 'union': (dom.Element child) {
|
| + checkAttributes(child, ['field'], context);
|
| + String field = child.attributes['field'];
|
| + types.add(
|
| + new TypeUnion(processContentsAsTypes(child, context), field, child));
|
| + }
|
| + });
|
| + return types;
|
| +}
|
| +
|
| +/**
|
| + * Read the API description from the file 'spec_input.html'. [pkgPath] is the
|
| + * path to the current package.
|
| + */
|
| +Api readApi(String pkgPath) {
|
| + File htmlFile = new File(join(pkgPath, 'tool', 'spec', 'plugin_spec.html'));
|
| + String htmlContents = htmlFile.readAsStringSync();
|
| + dom.Document document = parser.parse(htmlContents);
|
| + dom.Element htmlElement = document.children
|
| + .singleWhere((element) => element.localName.toLowerCase() == 'html');
|
| + return apiFromHtml(htmlElement);
|
| +}
|
| +
|
| +void recurse(dom.Element parent, String context,
|
| + Map<String, ElementProcessor> elementProcessors) {
|
| + for (String key in elementProcessors.keys) {
|
| + if (!specialElements.contains(key)) {
|
| + throw new Exception('$context: $key is not a special element');
|
| + }
|
| + }
|
| + for (dom.Node node in parent.nodes) {
|
| + if (node is dom.Element) {
|
| + if (elementProcessors.containsKey(node.localName)) {
|
| + elementProcessors[node.localName](node);
|
| + } else if (specialElements.contains(node.localName)) {
|
| + throw new Exception('$context: Unexpected use of <${node.localName}>');
|
| + } else {
|
| + recurse(node, context, elementProcessors);
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Create a [Refactoring] object from an HTML representation such as:
|
| + *
|
| + * <refactoring kind="refactoringKind">
|
| + * <feedback>...</feedback> <!-- optional -->
|
| + * <options>...</options> <!-- optional -->
|
| + * </refactoring>
|
| + *
|
| + * <feedback> and <options> have the same form as <object>, as described in
|
| + * [typeDeclFromHtml].
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +Refactoring refactoringFromHtml(dom.Element html) {
|
| + checkName(html, 'refactoring');
|
| + String kind = html.attributes['kind'];
|
| + String context = kind != null ? kind : 'refactoring';
|
| + checkAttributes(html, ['kind'], context);
|
| + TypeDecl feedback;
|
| + TypeDecl options;
|
| + recurse(html, context, {
|
| + 'feedback': (dom.Element child) {
|
| + feedback = typeObjectFromHtml(child, '$context.feedback');
|
| + },
|
| + 'options': (dom.Element child) {
|
| + options = typeObjectFromHtml(child, '$context.options');
|
| + }
|
| + });
|
| + return new Refactoring(kind, feedback, options, html);
|
| +}
|
| +
|
| +/**
|
| + * Create a [Refactorings] object from an HTML representation such as:
|
| + *
|
| + * <refactorings>
|
| + * <refactoring kind="...">...</refactoring> <!-- zero or more -->
|
| + * </refactorings>
|
| + */
|
| +Refactorings refactoringsFromHtml(dom.Element html) {
|
| + checkName(html, 'refactorings');
|
| + String context = 'refactorings';
|
| + checkAttributes(html, [], context);
|
| + List<Refactoring> refactorings = <Refactoring>[];
|
| + recurse(html, context, {
|
| + 'refactoring': (dom.Element child) {
|
| + refactorings.add(refactoringFromHtml(child));
|
| + }
|
| + });
|
| + return new Refactorings(refactorings, html);
|
| +}
|
| +
|
| +/**
|
| + * Create a [Request] object from an HTML representation such as:
|
| + *
|
| + * <request method="methodName">
|
| + * <params>...</params> <!-- optional -->
|
| + * <result>...</result> <!-- optional -->
|
| + * </request>
|
| + *
|
| + * Note that the method name should not include the domain name.
|
| + *
|
| + * <params> and <result> have the same form as <object>, as described in
|
| + * [typeDeclFromHtml].
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +Request requestFromHtml(dom.Element html, String context) {
|
| + String domainName = getAncestor(html, 'domain', context).attributes['name'];
|
| + checkName(html, 'request', context);
|
| + String method = html.attributes['method'];
|
| + context = '$context.${method != null ? method : 'method'}';
|
| + checkAttributes(html, ['method'], context);
|
| + TypeDecl params;
|
| + TypeDecl result;
|
| + recurse(html, context, {
|
| + 'params': (dom.Element child) {
|
| + params = typeObjectFromHtml(child, '$context.params');
|
| + },
|
| + 'result': (dom.Element child) {
|
| + result = typeObjectFromHtml(child, '$context.result');
|
| + }
|
| + });
|
| + return new Request(domainName, method, params, result, html);
|
| +}
|
| +
|
| +/**
|
| + * Create a [TypeDefinition] object from an HTML representation such as:
|
| + *
|
| + * <type name="typeName">
|
| + * TYPE
|
| + * </type>
|
| + *
|
| + * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml].
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +TypeDefinition typeDefinitionFromHtml(dom.Element html) {
|
| + checkName(html, 'type');
|
| + String name = html.attributes['name'];
|
| + String context = name != null ? name : 'type';
|
| + checkAttributes(html, ['name'], context,
|
| + optionalAttributes: ['experimental']);
|
| + TypeDecl type = processContentsAsType(html, context);
|
| + bool experimental = html.attributes['experimental'] == 'true';
|
| + return new TypeDefinition(name, type, html, experimental: experimental);
|
| +}
|
| +
|
| +/**
|
| + * Create a [TypeEnum] from an HTML description.
|
| + */
|
| +TypeEnum typeEnumFromHtml(dom.Element html, String context) {
|
| + checkName(html, 'enum', context);
|
| + checkAttributes(html, [], context);
|
| + List<TypeEnumValue> values = <TypeEnumValue>[];
|
| + recurse(html, context, {
|
| + 'value': (dom.Element child) {
|
| + values.add(typeEnumValueFromHtml(child, context));
|
| + }
|
| + });
|
| + return new TypeEnum(values, html);
|
| +}
|
| +
|
| +/**
|
| + * Create a [TypeEnumValue] from an HTML description such as:
|
| + *
|
| + * <enum>
|
| + * <code>VALUE</code>
|
| + * </enum>
|
| + *
|
| + * Where VALUE is the text of the enumerated value.
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +TypeEnumValue typeEnumValueFromHtml(dom.Element html, String context) {
|
| + checkName(html, 'value', context);
|
| + checkAttributes(html, [], context);
|
| + List<String> values = <String>[];
|
| + recurse(html, context, {
|
| + 'code': (dom.Element child) {
|
| + String text = innerText(child).trim();
|
| + values.add(text);
|
| + }
|
| + });
|
| + if (values.length != 1) {
|
| + throw new Exception('$context: Exactly one value must be specified');
|
| + }
|
| + return new TypeEnumValue(values[0], html);
|
| +}
|
| +
|
| +/**
|
| + * Create a [TypeObjectField] from an HTML description such as:
|
| + *
|
| + * <field name="fieldName">
|
| + * TYPE
|
| + * </field>
|
| + *
|
| + * Where TYPE is any HTML that can be parsed by [typeDeclFromHtml].
|
| + *
|
| + * In addition, the attribute optional="true" may be used to specify that the
|
| + * field is optional, and the attribute value="..." may be used to specify that
|
| + * the field is required to have a certain value.
|
| + *
|
| + * Child elements can occur in any order.
|
| + */
|
| +TypeObjectField typeObjectFieldFromHtml(dom.Element html, String context) {
|
| + checkName(html, 'field', context);
|
| + String name = html.attributes['name'];
|
| + context = '$context.${name != null ? name : 'field'}';
|
| + checkAttributes(html, ['name'], context,
|
| + optionalAttributes: ['optional', 'value']);
|
| + bool optional = false;
|
| + String optionalString = html.attributes['optional'];
|
| + if (optionalString != null) {
|
| + switch (optionalString) {
|
| + case 'true':
|
| + optional = true;
|
| + break;
|
| + case 'false':
|
| + optional = false;
|
| + break;
|
| + default:
|
| + throw new Exception(
|
| + '$context: field contains invalid "optional" attribute: "$optionalString"');
|
| + }
|
| + }
|
| + String value = html.attributes['value'];
|
| + TypeDecl type = processContentsAsType(html, context);
|
| + return new TypeObjectField(name, type, html,
|
| + optional: optional, value: value);
|
| +}
|
| +
|
| +/**
|
| + * Create a [TypeObject] from an HTML description.
|
| + */
|
| +TypeObject typeObjectFromHtml(dom.Element html, String context) {
|
| + checkAttributes(html, [], context, optionalAttributes: ['experimental']);
|
| + List<TypeObjectField> fields = <TypeObjectField>[];
|
| + recurse(html, context, {
|
| + 'field': (dom.Element child) {
|
| + fields.add(typeObjectFieldFromHtml(child, context));
|
| + }
|
| + });
|
| + bool experimental = html.attributes['experimental'] == 'true';
|
| + return new TypeObject(fields, html, experimental: experimental);
|
| +}
|
| +
|
| +/**
|
| + * Create a [Types] object from an HTML representation such as:
|
| + *
|
| + * <types>
|
| + * <type name="...">...</type> <!-- zero or more -->
|
| + * </types>
|
| + */
|
| +Types typesFromHtml(dom.Element html) {
|
| + checkName(html, 'types');
|
| + String context = 'types';
|
| + checkAttributes(html, [], context);
|
| + Map<String, TypeDefinition> types = <String, TypeDefinition>{};
|
| + recurse(html, context, {
|
| + 'type': (dom.Element child) {
|
| + TypeDefinition typeDefinition = typeDefinitionFromHtml(child);
|
| + types[typeDefinition.name] = typeDefinition;
|
| + }
|
| + });
|
| + return new Types(types, html);
|
| +}
|
| +
|
| +typedef void ElementProcessor(dom.Element element);
|
| +
|
| +typedef void TextProcessor(dom.Text text);
|
|
|