// Copyright (c) 2014, 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:front_end/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; } 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; } .deprecated { text-decoration: line-through; } /* Styles for index */ .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) async { 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; }); String _toTitleCase(String str) { if (str.isEmpty) return str; return str.substring(0, 1).toUpperCase() + str.substring(1); } /** * Visitor that records the mapping from HTML elements to various kinds of API * nodes. */ class ApiMappings extends HierarchicalApiVisitor { Map domains = {}; 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 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 li(void callback()) => element('li', {}, callback); void link(String id, void callback(), [Map attributes]) { attributes ??= {}; attributes['href'] = '#$id'; element('a', attributes, callback); } void p(void callback()) => element('p', {}, callback); void pre(void callback()) => element('pre', {}, callback); void span(String cls, void callback()) => element('span', {'class': cls}, callback); void title(void callback()) => element('title', {}, callback); void tt(void callback()) => element('tt', {}, callback); void ul(void callback()) => element('ul', {}, callback); } /** * Visitor that generates HTML documentation of the API. */ class ToHtmlVisitor extends HierarchicalApiVisitor with HtmlMixin, HtmlGenerator { /** * Set of types defined in the API. */ Set definedTypes = new Set(); /** * 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 generateDomainsHeader() { h1(() { write('Domains'); }); } 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 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 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 requests) { h5(() => write("Requests")); element('ul', {}, () { for (var request in requests) { if (!request.experimental) { element( 'li', {}, () => link('request_${request.longMethod}', () => write(request.method))); } } }); } void generateTableOfContents() { for (var domain in api.domains.where((domain) => !domain.experimental)) { if (domain.experimental) continue; writeln(); p(() { link('domain_${domain.name}', () { write(_toTitleCase(domain.name)); }); }); ul(() { for (Request request in domain.requests) { if (request.experimental) continue; li(() { link('request_${request.longMethod}', () { write(request.longMethod); }, request.deprecated ? {'class': 'deprecated'} : null); }); writeln(); } }); writeln(); } } void generateTypesIndex(Set types) { h3(() { write("Types"); write(' ('); link('types', () => write('\u2191')); write(')'); }); List sortedTypes = types.toList(); sortedTypes.sort(); element('div', {'class': 'subindex'}, () { element('ul', {}, () { for (var type in sortedTypes) { 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 fieldsToBold = new Set(); 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 'domains': generateDomainsHeader(); break; 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 'toc': generateTableOfContents(); break; case 'index': generateIndex(); break; default: if (!ApiReader.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 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) { if (notification.experimental) { return; } dt('notification', () { anchor('notification_${notification.longEvent}', () { write(notification.longEvent); }); }); 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) { if (request.experimental) { return; } dt(request.deprecated ? 'request deprecated' : 'request', () { anchor('request_${request.longMethod}', () { write(request.longMethod); }); }); 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.deprecated ? 'typeDefinition deprecated' : '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(typeEnumValue.deprecated ? 'value deprecated' : '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(() { if (typeObjectField.deprecated) { span('deprecated', () { write(typeObjectField.name); }); } else { write(typeObjectField.name); } if (typeObjectField.value != null) { write(' = ${json.encode(typeObjectField.value)}'); } else { write(': '); TypeVisitor typeVisitor = new TypeVisitor(api, short: true); addAll(typeVisitor.collectHtml(() { typeVisitor.visitTypeDecl(typeObjectField.type); })); if (typeObjectField.optional) { gray(() => write(' (optional)')); } } }); }); dd(() { translateHtml(typeObjectField.html); }); } @override void visitTypeReference(TypeReference typeReference) {} @override void visitTypes(Types types) { translateHtml(types.html); dl(() { List sortedTypes = types.toList(); sortedTypes.sort((TypeDefinition first, TypeDefinition second) => first.name.compareTo(second.name)); sortedTypes.forEach(visitTypeDefinition); }); } } /** * 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 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; } } }