| Index: pkg/analyzer_plugin/tool/spec/to_html.dart
|
| diff --git a/pkg/analyzer_plugin/tool/spec/to_html.dart b/pkg/analyzer_plugin/tool/spec/to_html.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..85502f1a085575291618a5bb8dfc34634d59fc8b
|
| --- /dev/null
|
| +++ b/pkg/analyzer_plugin/tool/spec/to_html.dart
|
| @@ -0,0 +1,773 @@
|
| +// 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 displaying the API as HTML. This is used both for generating a
|
| + * full description of the API as a web page, and for generating doc comments
|
| + * in generated code.
|
| + */
|
| +import 'dart:convert';
|
| +
|
| +import 'package:analyzer/src/codegen/html.dart';
|
| +import 'package:analyzer/src/codegen/tools.dart';
|
| +import 'package:html/dom.dart' as dom;
|
| +
|
| +import 'api.dart';
|
| +import 'from_html.dart';
|
| +
|
| +/**
|
| + * Embedded stylesheet
|
| + */
|
| +final String stylesheet = '''
|
| +body {
|
| + font-family: 'Roboto', sans-serif;
|
| + max-width: 800px;
|
| + margin: 0 auto;
|
| + padding: 0 16px;
|
| + font-size: 16px;
|
| + line-height: 1.5;
|
| + color: #111;
|
| + background-color: #fdfdfd;
|
| + font-weight: 300;
|
| + -webkit-font-smoothing: auto;
|
| +}
|
| +
|
| +h1 {
|
| + text-align: center;
|
| +}
|
| +
|
| +h2, h3, h4, h5 {
|
| + margin-bottom: 0;
|
| +}
|
| +
|
| +h2.domain {
|
| + border-bottom: 1px solid rgb(200, 200, 200);
|
| + margin-bottom: 0.5em;
|
| +}
|
| +
|
| +h4 {
|
| + font-size: 18px;
|
| +}
|
| +
|
| +h5 {
|
| + font-size: 16px;
|
| +}
|
| +
|
| +p {
|
| + margin-top: 0;
|
| +}
|
| +
|
| +pre {
|
| + margin: 0;
|
| + font-family: 'Source Code Pro', monospace;
|
| + font-size: 15px;
|
| +}
|
| +
|
| +div.box {
|
| + background-color: rgb(240, 245, 240);
|
| + border-radius: 4px;
|
| + padding: 4px 12px;
|
| + margin: 16px 0;
|
| +}
|
| +
|
| +div.hangingIndent {
|
| + padding-left: 3em;
|
| + text-indent: -3em;
|
| +}
|
| +
|
| +dl dt {
|
| + font-weight: bold;
|
| +}
|
| +
|
| +dl dd {
|
| + margin-left: 16px;
|
| +}
|
| +
|
| +dt {
|
| + margin-top: 1em;
|
| +}
|
| +
|
| +dt.notification {
|
| + font-weight: bold;
|
| +}
|
| +
|
| +dt.refactoring {
|
| + font-weight: bold;
|
| +}
|
| +
|
| +dt.request {
|
| + font-weight: bold;
|
| +}
|
| +
|
| +dt.typeDefinition {
|
| + font-weight: bold;
|
| +}
|
| +
|
| +a {
|
| + text-decoration: none;
|
| +}
|
| +
|
| +a:focus, a:hover {
|
| + text-decoration: underline;
|
| +}
|
| +
|
| +/* Styles for index */
|
| +
|
| +.subindex {
|
| +}
|
| +
|
| +.subindex ul {
|
| + padding-left: 0;
|
| + margin-left: 0;
|
| +
|
| + -webkit-margin-before: 0;
|
| + -webkit-margin-start: 0;
|
| + -webkit-padding-start: 0;
|
| +
|
| + list-style-type: none;
|
| +}
|
| +'''
|
| + .trim();
|
| +
|
| +final GeneratedFile target =
|
| + new GeneratedFile('doc/api.html', (String pkgPath) {
|
| + ToHtmlVisitor visitor = new ToHtmlVisitor(readApi(pkgPath));
|
| + dom.Document document = new dom.Document();
|
| + document.append(new dom.DocumentType('html', null, null));
|
| + for (dom.Node node in visitor.collectHtml(visitor.visitApi)) {
|
| + document.append(node);
|
| + }
|
| + return document.outerHtml;
|
| +});
|
| +
|
| +/**
|
| + * Visitor that records the mapping from HTML elements to various kinds of API
|
| + * nodes.
|
| + */
|
| +class ApiMappings extends HierarchicalApiVisitor {
|
| + Map<dom.Element, Domain> domains = <dom.Element, Domain>{};
|
| +
|
| + ApiMappings(Api api) : super(api);
|
| +
|
| + @override
|
| + void visitDomain(Domain domain) {
|
| + domains[domain.html] = domain;
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Helper methods for creating HTML elements.
|
| + */
|
| +abstract class HtmlMixin {
|
| + void anchor(String id, void callback()) {
|
| + element('a', {'name': id}, callback);
|
| + }
|
| +
|
| + void b(void callback()) => element('b', {}, callback);
|
| + void body(void callback()) => element('body', {}, callback);
|
| + void box(void callback()) {
|
| + element('div', {'class': 'box'}, callback);
|
| + }
|
| +
|
| + void br() => element('br', {});
|
| + void dd(void callback()) => element('dd', {}, callback);
|
| + void dl(void callback()) => element('dl', {}, callback);
|
| + void dt(String cls, void callback()) =>
|
| + element('dt', {'class': cls}, callback);
|
| + void element(String name, Map<dynamic, String> attributes, [void callback()]);
|
| + void gray(void callback()) =>
|
| + element('span', {'style': 'color:#999999'}, callback);
|
| + void h1(void callback()) => element('h1', {}, callback);
|
| + void h2(String cls, void callback()) {
|
| + if (cls == null) {
|
| + return element('h2', {}, callback);
|
| + }
|
| + return element('h2', {'class': cls}, callback);
|
| + }
|
| +
|
| + void h3(void callback()) => element('h3', {}, callback);
|
| + void h4(void callback()) => element('h4', {}, callback);
|
| + void h5(void callback()) => element('h5', {}, callback);
|
| + void hangingIndent(void callback()) =>
|
| + element('div', {'class': 'hangingIndent'}, callback);
|
| + void head(void callback()) => element('head', {}, callback);
|
| + void html(void callback()) => element('html', {}, callback);
|
| + void i(void callback()) => element('i', {}, callback);
|
| + void link(String id, void callback()) {
|
| + element('a', {'href': '#$id'}, callback);
|
| + }
|
| +
|
| + void p(void callback()) => element('p', {}, callback);
|
| + void pre(void callback()) => element('pre', {}, callback);
|
| + void title(void callback()) => element('title', {}, callback);
|
| + void tt(void callback()) => element('tt', {}, callback);
|
| +}
|
| +
|
| +/**
|
| + * Visitor that generates HTML documentation of the API.
|
| + */
|
| +class ToHtmlVisitor extends HierarchicalApiVisitor
|
| + with HtmlMixin, HtmlGenerator {
|
| + /**
|
| + * Set of types defined in the API.
|
| + */
|
| + Set<String> definedTypes = new Set<String>();
|
| +
|
| + /**
|
| + * Mappings from HTML elements to API nodes.
|
| + */
|
| + ApiMappings apiMappings;
|
| +
|
| + ToHtmlVisitor(Api api)
|
| + : apiMappings = new ApiMappings(api),
|
| + super(api) {
|
| + apiMappings.visitApi();
|
| + }
|
| +
|
| + /**
|
| + * Describe the payload of request, response, notification, refactoring
|
| + * feedback, or refactoring options.
|
| + *
|
| + * If [force] is true, then a section is inserted even if the payload is
|
| + * null.
|
| + */
|
| + void describePayload(TypeObject subType, String name, {bool force: false}) {
|
| + if (force || subType != null) {
|
| + h4(() {
|
| + write(name);
|
| + });
|
| + if (subType == null) {
|
| + p(() {
|
| + write('none');
|
| + });
|
| + } else {
|
| + visitTypeDecl(subType);
|
| + }
|
| + }
|
| + }
|
| +
|
| + void generateDomainIndex(Domain domain) {
|
| + h4(() {
|
| + write(domain.name);
|
| + write(' (');
|
| + link('domain_${domain.name}', () => write('\u2191'));
|
| + write(')');
|
| + });
|
| + if (domain.requests.length > 0) {
|
| + element('div', {'class': 'subindex'}, () {
|
| + generateRequestsIndex(domain.requests);
|
| + if (domain.notifications.length > 0) {
|
| + generateNotificationsIndex(domain.notifications);
|
| + }
|
| + });
|
| + } else if (domain.notifications.length > 0) {
|
| + element('div', {'class': 'subindex'}, () {
|
| + generateNotificationsIndex(domain.notifications);
|
| + });
|
| + }
|
| + }
|
| +
|
| + void generateIndex() {
|
| + h3(() => write('Domains'));
|
| + for (var domain in api.domains) {
|
| + if (domain.experimental ||
|
| + (domain.requests.length == 0 && domain.notifications == 0)) {
|
| + continue;
|
| + }
|
| + generateDomainIndex(domain);
|
| + }
|
| +
|
| + generateTypesIndex(definedTypes);
|
| + generateRefactoringsIndex(api.refactorings);
|
| + }
|
| +
|
| + void generateNotificationsIndex(Iterable<Notification> notifications) {
|
| + h5(() => write("Notifications"));
|
| + element('div', {'class': 'subindex'}, () {
|
| + element('ul', {}, () {
|
| + for (var notification in notifications) {
|
| + element(
|
| + 'li',
|
| + {},
|
| + () => link('notification_${notification.longEvent}',
|
| + () => write(notification.event)));
|
| + }
|
| + });
|
| + });
|
| + }
|
| +
|
| + void generateRefactoringsIndex(Iterable<Refactoring> refactorings) {
|
| + if (refactorings == null) {
|
| + return;
|
| + }
|
| + h3(() {
|
| + write("Refactorings");
|
| + write(' (');
|
| + link('refactorings', () => write('\u2191'));
|
| + write(')');
|
| + });
|
| + // TODO: Individual refactorings are not yet hyperlinked.
|
| + element('div', {'class': 'subindex'}, () {
|
| + element('ul', {}, () {
|
| + for (var refactoring in refactorings) {
|
| + element(
|
| + 'li',
|
| + {},
|
| + () => link('refactoring_${refactoring.kind}',
|
| + () => write(refactoring.kind)));
|
| + }
|
| + });
|
| + });
|
| + }
|
| +
|
| + void generateRequestsIndex(Iterable<Request> requests) {
|
| + h5(() => write("Requests"));
|
| + element('ul', {}, () {
|
| + for (var request in requests) {
|
| + element(
|
| + 'li',
|
| + {},
|
| + () => link(
|
| + 'request_${request.longMethod}', () => write(request.method)));
|
| + }
|
| + });
|
| + }
|
| +
|
| + void generateTypesIndex(Set<String> types) {
|
| + h3(() {
|
| + write("Types");
|
| + write(' (');
|
| + link('types', () => write('\u2191'));
|
| + write(')');
|
| + });
|
| + element('div', {'class': 'subindex'}, () {
|
| + element('ul', {}, () {
|
| + for (var type in types) {
|
| + element('li', {}, () => link('type_$type', () => write(type)));
|
| + }
|
| + });
|
| + });
|
| + }
|
| +
|
| + void javadocParams(TypeObject typeObject) {
|
| + if (typeObject != null) {
|
| + for (TypeObjectField field in typeObject.fields) {
|
| + hangingIndent(() {
|
| + write('@param ${field.name} ');
|
| + translateHtml(field.html, squashParagraphs: true);
|
| + });
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Generate a description of [type] using [TypeVisitor].
|
| + *
|
| + * If [shortDesc] is non-null, the output is prefixed with this string
|
| + * and a colon.
|
| + *
|
| + * If [typeForBolding] is supplied, then fields in this type are shown in
|
| + * boldface.
|
| + */
|
| + void showType(String shortDesc, TypeDecl type, [TypeObject typeForBolding]) {
|
| + Set<String> fieldsToBold = new Set<String>();
|
| + if (typeForBolding != null) {
|
| + for (TypeObjectField field in typeForBolding.fields) {
|
| + fieldsToBold.add(field.name);
|
| + }
|
| + }
|
| + pre(() {
|
| + if (shortDesc != null) {
|
| + write('$shortDesc: ');
|
| + }
|
| + TypeVisitor typeVisitor =
|
| + new TypeVisitor(api, fieldsToBold: fieldsToBold);
|
| + addAll(typeVisitor.collectHtml(() {
|
| + typeVisitor.visitTypeDecl(type);
|
| + }));
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Copy the contents of the given HTML element, translating the special
|
| + * elements that define the API appropriately.
|
| + */
|
| + void translateHtml(dom.Element html, {bool squashParagraphs: false}) {
|
| + for (dom.Node node in html.nodes) {
|
| + if (node is dom.Element) {
|
| + if (squashParagraphs && node.localName == 'p') {
|
| + translateHtml(node, squashParagraphs: squashParagraphs);
|
| + continue;
|
| + }
|
| + switch (node.localName) {
|
| + case 'domain':
|
| + visitDomain(apiMappings.domains[node]);
|
| + break;
|
| + case 'head':
|
| + head(() {
|
| + translateHtml(node, squashParagraphs: squashParagraphs);
|
| + element('link', {
|
| + 'rel': 'stylesheet',
|
| + 'href':
|
| + 'https://fonts.googleapis.com/css?family=Source+Code+Pro|Roboto:500,400italic,300,400',
|
| + 'type': 'text/css'
|
| + });
|
| + element('style', {}, () {
|
| + writeln(stylesheet);
|
| + });
|
| + });
|
| + break;
|
| + case 'refactorings':
|
| + visitRefactorings(api.refactorings);
|
| + break;
|
| + case 'types':
|
| + visitTypes(api.types);
|
| + break;
|
| + case 'version':
|
| + translateHtml(node, squashParagraphs: squashParagraphs);
|
| + break;
|
| + case 'index':
|
| + generateIndex();
|
| + break;
|
| + default:
|
| + if (!specialElements.contains(node.localName)) {
|
| + element(node.localName, node.attributes, () {
|
| + translateHtml(node, squashParagraphs: squashParagraphs);
|
| + });
|
| + }
|
| + }
|
| + } else if (node is dom.Text) {
|
| + String text = node.text;
|
| + write(text);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @override
|
| + void visitApi() {
|
| + Iterable<TypeDefinition> apiTypes =
|
| + api.types.where((TypeDefinition td) => !td.experimental);
|
| + definedTypes = apiTypes.map((TypeDefinition td) => td.name).toSet();
|
| +
|
| + html(() {
|
| + translateHtml(api.html);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitDomain(Domain domain) {
|
| + if (domain.experimental) {
|
| + return;
|
| + }
|
| + h2('domain', () {
|
| + anchor('domain_${domain.name}', () {
|
| + write('${domain.name} domain');
|
| + });
|
| + });
|
| + translateHtml(domain.html);
|
| + if (domain.requests.isNotEmpty) {
|
| + h3(() {
|
| + write('Requests');
|
| + });
|
| + dl(() {
|
| + domain.requests.forEach(visitRequest);
|
| + });
|
| + }
|
| + if (domain.notifications.isNotEmpty) {
|
| + h3(() {
|
| + write('Notifications');
|
| + });
|
| + dl(() {
|
| + domain.notifications.forEach(visitNotification);
|
| + });
|
| + }
|
| + }
|
| +
|
| + @override
|
| + void visitNotification(Notification notification) {
|
| + dt('notification', () {
|
| + anchor('notification_${notification.longEvent}', () {
|
| + write(notification.longEvent);
|
| + });
|
| + write(' (');
|
| + link('notification_${notification.longEvent}', () {
|
| + write('#');
|
| + });
|
| + write(')');
|
| + });
|
| + dd(() {
|
| + box(() {
|
| + showType(
|
| + 'notification', notification.notificationType, notification.params);
|
| + });
|
| + translateHtml(notification.html);
|
| + describePayload(notification.params, 'parameters:');
|
| + });
|
| + }
|
| +
|
| + @override
|
| + visitRefactoring(Refactoring refactoring) {
|
| + dt('refactoring', () {
|
| + write(refactoring.kind);
|
| + });
|
| + dd(() {
|
| + translateHtml(refactoring.html);
|
| + describePayload(refactoring.feedback, 'Feedback:', force: true);
|
| + describePayload(refactoring.options, 'Options:', force: true);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitRefactorings(Refactorings refactorings) {
|
| + translateHtml(refactorings.html);
|
| + dl(() {
|
| + super.visitRefactorings(refactorings);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitRequest(Request request) {
|
| + dt('request', () {
|
| + anchor('request_${request.longMethod}', () {
|
| + write(request.longMethod);
|
| + });
|
| + write(' (');
|
| + link('request_${request.longMethod}', () {
|
| + write('#');
|
| + });
|
| + write(')');
|
| + });
|
| + dd(() {
|
| + box(() {
|
| + showType('request', request.requestType, request.params);
|
| + br();
|
| + showType('response', request.responseType, request.result);
|
| + });
|
| + translateHtml(request.html);
|
| + describePayload(request.params, 'parameters:');
|
| + describePayload(request.result, 'returns:');
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitTypeDefinition(TypeDefinition typeDefinition) {
|
| + if (typeDefinition.experimental) {
|
| + return;
|
| + }
|
| + dt('typeDefinition', () {
|
| + anchor('type_${typeDefinition.name}', () {
|
| + write('${typeDefinition.name}: ');
|
| + TypeVisitor typeVisitor = new TypeVisitor(api, short: true);
|
| + addAll(typeVisitor.collectHtml(() {
|
| + typeVisitor.visitTypeDecl(typeDefinition.type);
|
| + }));
|
| + });
|
| + });
|
| + dd(() {
|
| + translateHtml(typeDefinition.html);
|
| + visitTypeDecl(typeDefinition.type);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitTypeEnum(TypeEnum typeEnum) {
|
| + dl(() {
|
| + super.visitTypeEnum(typeEnum);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitTypeEnumValue(TypeEnumValue typeEnumValue) {
|
| + bool isDocumented = false;
|
| + for (dom.Node node in typeEnumValue.html.nodes) {
|
| + if ((node is dom.Element && node.localName != 'code') ||
|
| + (node is dom.Text && node.text.trim().isNotEmpty)) {
|
| + isDocumented = true;
|
| + break;
|
| + }
|
| + }
|
| + dt('value', () {
|
| + write(typeEnumValue.value);
|
| + });
|
| + if (isDocumented) {
|
| + dd(() {
|
| + translateHtml(typeEnumValue.html);
|
| + });
|
| + }
|
| + }
|
| +
|
| + @override
|
| + void visitTypeList(TypeList typeList) {
|
| + visitTypeDecl(typeList.itemType);
|
| + }
|
| +
|
| + @override
|
| + void visitTypeMap(TypeMap typeMap) {
|
| + visitTypeDecl(typeMap.valueType);
|
| + }
|
| +
|
| + @override
|
| + void visitTypeObject(TypeObject typeObject) {
|
| + dl(() {
|
| + super.visitTypeObject(typeObject);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitTypeObjectField(TypeObjectField typeObjectField) {
|
| + dt('field', () {
|
| + b(() {
|
| + write(typeObjectField.name);
|
| + if (typeObjectField.value != null) {
|
| + write(' = ${JSON.encode(typeObjectField.value)}');
|
| + } else {
|
| + write(' (');
|
| + if (typeObjectField.optional) {
|
| + gray(() {
|
| + write('optional');
|
| + });
|
| + write(' ');
|
| + }
|
| + TypeVisitor typeVisitor = new TypeVisitor(api, short: true);
|
| + addAll(typeVisitor.collectHtml(() {
|
| + typeVisitor.visitTypeDecl(typeObjectField.type);
|
| + }));
|
| + write(')');
|
| + }
|
| + });
|
| + });
|
| + dd(() {
|
| + translateHtml(typeObjectField.html);
|
| + });
|
| + }
|
| +
|
| + @override
|
| + void visitTypeReference(TypeReference typeReference) {}
|
| +
|
| + @override
|
| + void visitTypes(Types types) {
|
| + translateHtml(types.html);
|
| + dl(() {
|
| + super.visitTypes(types);
|
| + });
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Visitor that generates a compact representation of a type, such as:
|
| + *
|
| + * {
|
| + * "id": String
|
| + * "error": optional Error
|
| + * "result": {
|
| + * "version": String
|
| + * }
|
| + * }
|
| + */
|
| +class TypeVisitor extends HierarchicalApiVisitor
|
| + with HtmlMixin, HtmlCodeGenerator {
|
| + /**
|
| + * Set of fields which should be shown in boldface, or null if no field
|
| + * should be shown in boldface.
|
| + */
|
| + final Set<String> fieldsToBold;
|
| +
|
| + /**
|
| + * True if a short description should be generated. In a short description,
|
| + * objects are shown as simply "object", and enums are shown as "String".
|
| + */
|
| + final bool short;
|
| +
|
| + TypeVisitor(Api api, {this.fieldsToBold, this.short: false}) : super(api);
|
| +
|
| + @override
|
| + void visitTypeEnum(TypeEnum typeEnum) {
|
| + if (short) {
|
| + write('String');
|
| + return;
|
| + }
|
| + writeln('enum {');
|
| + indent(() {
|
| + for (TypeEnumValue value in typeEnum.values) {
|
| + writeln(value.value);
|
| + }
|
| + });
|
| + write('}');
|
| + }
|
| +
|
| + @override
|
| + void visitTypeList(TypeList typeList) {
|
| + write('List<');
|
| + visitTypeDecl(typeList.itemType);
|
| + write('>');
|
| + }
|
| +
|
| + @override
|
| + void visitTypeMap(TypeMap typeMap) {
|
| + write('Map<');
|
| + visitTypeDecl(typeMap.keyType);
|
| + write(', ');
|
| + visitTypeDecl(typeMap.valueType);
|
| + write('>');
|
| + }
|
| +
|
| + @override
|
| + void visitTypeObject(TypeObject typeObject) {
|
| + if (short) {
|
| + write('object');
|
| + return;
|
| + }
|
| + writeln('{');
|
| + indent(() {
|
| + for (TypeObjectField field in typeObject.fields) {
|
| + write('"');
|
| + if (fieldsToBold != null && fieldsToBold.contains(field.name)) {
|
| + b(() {
|
| + write(field.name);
|
| + });
|
| + } else {
|
| + write(field.name);
|
| + }
|
| + write('": ');
|
| + if (field.value != null) {
|
| + write(JSON.encode(field.value));
|
| + } else {
|
| + if (field.optional) {
|
| + gray(() {
|
| + write('optional');
|
| + });
|
| + write(' ');
|
| + }
|
| + visitTypeDecl(field.type);
|
| + }
|
| + writeln();
|
| + }
|
| + });
|
| + write('}');
|
| + }
|
| +
|
| + @override
|
| + void visitTypeReference(TypeReference typeReference) {
|
| + String displayName = typeReference.typeName;
|
| + if (api.types.containsKey(typeReference.typeName)) {
|
| + link('type_${typeReference.typeName}', () {
|
| + write(displayName);
|
| + });
|
| + } else {
|
| + write(displayName);
|
| + }
|
| + }
|
| +
|
| + @override
|
| + void visitTypeUnion(TypeUnion typeUnion) {
|
| + bool verticalBarNeeded = false;
|
| + for (TypeDecl choice in typeUnion.choices) {
|
| + if (verticalBarNeeded) {
|
| + write(' | ');
|
| + }
|
| + visitTypeDecl(choice);
|
| + verticalBarNeeded = true;
|
| + }
|
| + }
|
| +}
|
|
|