chore: language-specific members api (#4936)

This commit is contained in:
Pavel Feldman 2021-01-07 15:00:04 -08:00 committed by GitHub
parent 8d649949c0
commit e56832b646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 521 additions and 406 deletions

View File

@ -0,0 +1,18 @@
## async method: Playwright.stop
* langs: python
Terminates this instance of Playwright in case it was created bypassing the Python context manager. This is useful in REPL applications.
```py
>>> from playwright import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.chromium.launch()
>>> page = browser.newPage()
>>> page.goto("http://whatsmyuseragent.org/")
>>> page.screenshot(path="example.png")
>>> browser.close()
>>> playwright.stop()
```

View File

@ -1,358 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
const fs = require('fs');
const path = require('path');
const md = require('../markdown');
const Documentation = require('./Documentation');
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
/**
* @typedef {function({
* clazz?: Documentation.Class,
* member?: Documentation.Member,
* param?: string,
* option?: string
* }): string} Renderer
*/
class MDOutline {
/**
* @param {string} apiDir
*/
constructor(apiDir) {
let bodyParts = [];
let paramsPath;
for (const name of fs.readdirSync(apiDir)) {
if (name.startsWith('class-'))
bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString());
if (name === 'params.md')
paramsPath = path.join(apiDir, name);
}
const body = md.parse(bodyParts.join('\n'));
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : null;
const api = params ? applyTemplates(body, params) : body;
this.classesArray = /** @type {Documentation.Class[]} */ [];
this.classes = /** @type {Map<string, Documentation.Class>} */ new Map();
for (const clazz of api) {
const c = parseClass(clazz);
this.classesArray.push(c);
this.classes.set(c.name, c);
}
this.documentation = new Documentation(this.classesArray);
}
/**
* @param {string[]} errors
*/
copyDocsFromSuperclasses(errors) {
for (const [name, clazz] of this.documentation.classes.entries()) {
clazz.validateOrder(errors, clazz);
if (!clazz.extends || clazz.extends === 'EventEmitter' || clazz.extends === 'Error')
continue;
const superClass = this.documentation.classes.get(clazz.extends);
if (!superClass) {
errors.push(`Undefined superclass: ${superClass} in ${name}`);
continue;
}
for (const memberName of clazz.members.keys()) {
if (superClass.members.has(memberName))
errors.push(`Member documentation overrides base: ${name}.${memberName} over ${clazz.extends}.${memberName}`);
}
clazz.membersArray = [...clazz.membersArray, ...superClass.membersArray.map(c => c.clone())];
clazz.index();
}
}
/**
* @param {Renderer} linkRenderer
*/
setLinkRenderer(linkRenderer) {
// @type {Map<string, Documentation.Class>}
const classesMap = new Map();
const membersMap = new Map();
for (const clazz of this.classesArray) {
classesMap.set(clazz.name, clazz);
for (const member of clazz.membersArray)
membersMap.set(`${member.kind}: ${clazz.name}.${member.name}`, member);
}
this._patchLinks = nodes => patchLinks(nodes, classesMap, membersMap, linkRenderer);
for (const clazz of this.classesArray)
clazz.visit(item => this._patchLinks(item.spec));
}
/**
* @param {MarkdownNode[]} nodes
*/
renderLinksInText(nodes) {
this._patchLinks(nodes);
}
generateSourceCodeComments() {
for (const clazz of this.classesArray)
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
}
}
/**
* @param {MarkdownNode} node
* @returns {Documentation.Class}
*/
function parseClass(node) {
const members = [];
let extendsName = null;
const name = node.text.substring('class: '.length);
for (const member of node.children) {
if (member.type === 'li' && member.text.startsWith('extends: [')) {
extendsName = member.text.substring('extends: ['.length, member.text.indexOf(']'));
continue;
}
if (member.type === 'h2')
members.push(parseMember(member));
}
return new Documentation.Class(name, members, extendsName, extractComments(node));
}
/**
* @param {MarkdownNode} item
* @returns {MarkdownNode[]}
*/
function extractComments(item) {
return (item.children || []).filter(c => !c.type.startsWith('h') && (c.type !== 'li' || c.liType !== 'default'));
}
/**
* @param {MarkdownNode[]} spec
*/
function generateSourceCodeComment(spec) {
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
md.visitAll(comments, node => {
if (node.liType === 'bullet')
node.liType = 'default';
});
return md.render(comments, 120);
}
/**
* @param {MarkdownNode[]} spec
* @param {Map<string, Documentation.Class>} classesMap
* @param {Map<string, Documentation.Member>} membersMap
* @param {Renderer} linkRenderer
*/
function patchLinks(spec, classesMap, membersMap, linkRenderer) {
if (!spec)
return;
md.visitAll(spec, node => {
if (!node.text)
return;
node.text = node.text.replace(/\[`((?:event|method|property): [^\]]+)`\]/g, (match, p1) => {
const member = membersMap.get(p1);
if (!member)
throw new Error('Undefined member references: ' + match);
return linkRenderer({ member }) || match;
});
node.text = node.text.replace(/\[`(param|option): ([^\]]+)`\]/g, (match, p1, p2) => {
if (p1 === 'param')
return linkRenderer({ param: p2 }) || match;
if (p1 === 'option')
return linkRenderer({ option: p2 }) || match;
});
node.text = node.text.replace(/\[([\w]+)\]/, (match, p1) => {
const clazz = classesMap.get(p1);
if (clazz)
return linkRenderer({ clazz }) || match;
return match;
});
});
}
/**
* @param {MarkdownNode} member
* @returns {Documentation.Member}
*/
function parseMember(member) {
const args = [];
const match = member.text.match(/(event|method|property|async method): (JS|CDP|[A-Z])([^.]+)\.(.*)/);
const name = match[4];
let returnType = null;
const options = [];
for (const item of member.children || []) {
if (item.type === 'li' && item.liType === 'default')
returnType = parseType(item);
}
if (!returnType)
returnType = new Documentation.Type('void');
if (match[1] === 'async method') {
const templates = [ returnType ];
returnType = new Documentation.Type('Promise');
returnType.templates = templates;
}
if (match[1] === 'event')
return Documentation.Member.createEvent(name, returnType, extractComments(member));
if (match[1] === 'property')
return Documentation.Member.createProperty(name, returnType, extractComments(member), true);
for (const item of member.children || []) {
if (item.type === 'h3' && item.text.startsWith('param:'))
args.push(parseProperty(item));
if (item.type === 'h3' && item.text.startsWith('option:'))
options.push(parseProperty(item));
}
if (options.length) {
options.sort((o1, o2) => o1.name.localeCompare(o2.name));
for (const option of options)
option.required = false;
const type = new Documentation.Type('Object', options);
args.push(Documentation.Member.createProperty('options', type, undefined, false));
}
return Documentation.Member.createMethod(name, args, returnType, extractComments(member));
}
/**
* @param {MarkdownNode} spec
* @return {Documentation.Member}
*/
function parseProperty(spec) {
const param = spec.children[0];
const text = param.text;
const name = text.substring(0, text.indexOf('<')).replace(/\`/g, '').trim();
const comments = extractComments(spec);
return Documentation.Member.createProperty(name, parseType(param), comments, guessRequired(md.render(comments)));
}
/**
* @param {MarkdownNode=} spec
* @return {Documentation.Type}
*/
function parseType(spec) {
const arg = parseArgument(spec.text);
const properties = [];
for (const child of spec.children || []) {
const { name, text } = parseArgument(child.text);
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
properties.push(Documentation.Member.createProperty(name, parseType(child), comments, guessRequired(text)));
}
return Documentation.Type.parse(arg.type, properties);
}
/**
* @param {string} comment
*/
function guessRequired(comment) {
let required = true;
if (comment.toLowerCase().includes('defaults to '))
required = false;
if (comment.startsWith('Optional'))
required = false;
if (comment.endsWith('Optional.'))
required = false;
if (comment.toLowerCase().includes('if set'))
required = false;
if (comment.toLowerCase().includes('if applicable'))
required = false;
if (comment.toLowerCase().includes('if available'))
required = false;
if (comment.includes('**required**'))
required = true;
return required;
}
/**
* @param {MarkdownNode[]} body
* @param {MarkdownNode[]} params
*/
function applyTemplates(body, params) {
const paramsMap = new Map();
for (const node of params)
paramsMap.set('%%-' + node.text + '-%%', node);
const visit = (node, parent) => {
if (node.text && node.text.includes('-inline- = %%')) {
const [name, key] = node.text.split('-inline- = ');
const list = paramsMap.get(key);
if (!list)
throw new Error('Bad template: ' + key);
for (const prop of list.children) {
const template = paramsMap.get(prop.text);
if (!template)
throw new Error('Bad template: ' + prop.text);
const { name: argName } = parseArgument(template.children[0].text);
parent.children.push({
type: node.type,
text: name + argName,
children: template.children.map(c => md.clone(c))
});
}
} else if (node.text && node.text.includes(' = %%')) {
const [name, key] = node.text.split(' = ');
node.text = name;
const template = paramsMap.get(key);
if (!template)
throw new Error('Bad template: ' + key);
node.children.push(...template.children.map(c => md.clone(c)));
}
for (const child of node.children || [])
visit(child, node);
if (node.children)
node.children = node.children.filter(child => !child.text || !child.text.includes('-inline- = %%'));
};
for (const node of body)
visit(node, null);
return body;
}
/**
* @param {string} line
* @returns {{ name: string, type: string, text: string }}
*/
function parseArgument(line) {
let match = line.match(/^`([^`]+)` (.*)/);
if (!match)
match = line.match(/^(returns): (.*)/);
if (!match)
match = line.match(/^(type): (.*)/);
if (!match)
throw new Error('Invalid argument: ' + line);
const name = match[1];
const remainder = match[2];
if (!remainder.startsWith('<'))
throw new Error('Bad argument: ' + remainder);
let depth = 0;
for (let i = 0; i < remainder.length; ++i) {
const c = remainder.charAt(i);
if (c === '<')
++depth;
if (c === '>')
--depth;
if (depth === 0)
return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2) };
}
throw new Error('Should not be reached');
}
module.exports = { MDOutline };

284
utils/doclint/api_parser.js Normal file
View File

@ -0,0 +1,284 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
const fs = require('fs');
const path = require('path');
const md = require('../markdown');
const Documentation = require('./documentation');
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
class ApiParser {
/**
* @param {string} apiDir
*/
constructor(apiDir) {
let bodyParts = [];
let paramsPath;
for (const name of fs.readdirSync(apiDir)) {
if (name.startsWith('class-'))
bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString());
if (name === 'params.md')
paramsPath = path.join(apiDir, name);
}
const body = md.parse(bodyParts.join('\n'));
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : null;
const api = params ? applyTemplates(body, params) : body;
/** @type {Map<string, Documentation.Class>} */
this.classes = new Map();
md.visitAll(api, node => {
if (node.type === 'h1')
this.parseClass(node);
if (node.type === 'h2')
this.parseMember(node);
if (node.type === 'h3')
this.parseArgument(node);
});
this.documentation = new Documentation([...this.classes.values()]);
this.documentation.index();
}
/**
* @param {MarkdownNode} node
*/
parseClass(node) {
let extendsName = null;
let langs = null;
const name = node.text.substring('class: '.length);
for (const member of node.children) {
if (member.type.startsWith('h'))
continue;
if (member.type === 'li' && member.text.startsWith('extends: [')) {
extendsName = member.text.substring('extends: ['.length, member.text.indexOf(']'));
continue;
}
if (member.type === 'li' && member.text.startsWith('langs: ')) {
langs = new Set(member.text.substring('langs: '.length).split(',').map(l => l.trim()));
continue;
}
}
const clazz = new Documentation.Class(langs, name, [], extendsName, extractComments(node));
this.classes.set(clazz.name, clazz);
}
/**
* @param {MarkdownNode} spec
*/
parseMember(spec) {
const match = spec.text.match(/(event|method|property|async method): ([^.]+)\.(.*)/);
const name = match[3];
let returnType = null;
let langs = null;
for (const item of spec.children || []) {
if (item.type === 'li' && item.liType === 'default')
returnType = this.parseType(item);
if (item.type === 'li' && item.liType === 'bullet' && item.text.startsWith('langs: '))
langs = new Set(item.text.substring('langs: '.length).split(',').map(l => l.trim()));
}
if (!returnType)
returnType = new Documentation.Type('void');
if (match[1] === 'async method') {
const templates = [ returnType ];
returnType = new Documentation.Type('Promise');
returnType.templates = templates;
}
let member;
if (match[1] === 'event')
member = Documentation.Member.createEvent(langs, name, returnType, extractComments(spec));
if (match[1] === 'property')
member = Documentation.Member.createProperty(langs, name, returnType, extractComments(spec));
if (match[1] === 'method' || match[1] === 'async method')
member = Documentation.Member.createMethod(langs, name, [], returnType, extractComments(spec));
const clazz = this.classes.get(match[2]);
clazz.membersArray.push(member);
}
/**
* @param {MarkdownNode} spec
*/
parseArgument(spec) {
const match = spec.text.match(/(param|option): ([^.]+)\.([^.]+)\.(.*)/);
const clazz = this.classes.get(match[2]);
const method = clazz.membersArray.find(m => m.kind === 'method' && m.name === match[3]);
if (match[1] === 'param') {
method.argsArray.push(this.parseProperty(spec));
} else {
let options = method.argsArray.find(o => o.name === 'options');
if (!options) {
const type = new Documentation.Type('Object', []);
options = Documentation.Member.createProperty(null, 'options', type, undefined, false);
method.argsArray.push(options);
}
const p = this.parseProperty(spec);
p.required = false;
options.type.properties.push(p);
}
}
/**
* @param {MarkdownNode} spec
*/
parseProperty(spec) {
const param = spec.children[0];
const text = param.text;
const name = text.substring(0, text.indexOf('<')).replace(/\`/g, '').trim();
const comments = extractComments(spec);
return Documentation.Member.createProperty(null, name, this.parseType(param), comments, guessRequired(md.render(comments)));
}
/**
* @param {MarkdownNode=} spec
* @return {Documentation.Type}
*/
parseType(spec) {
const arg = parseArgument(spec.text);
const properties = [];
for (const child of spec.children || []) {
const { name, text } = parseArgument(child.text);
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
properties.push(Documentation.Member.createProperty(null, name, this.parseType(child), comments, guessRequired(text)));
}
return Documentation.Type.parse(arg.type, properties);
}
}
/**
* @param {string} line
* @returns {{ name: string, type: string, text: string }}
*/
function parseArgument(line) {
let match = line.match(/^`([^`]+)` (.*)/);
if (!match)
match = line.match(/^(returns): (.*)/);
if (!match)
match = line.match(/^(type): (.*)/);
if (!match)
throw new Error('Invalid argument: ' + line);
const name = match[1];
const remainder = match[2];
if (!remainder.startsWith('<'))
throw new Error('Bad argument: ' + remainder);
let depth = 0;
for (let i = 0; i < remainder.length; ++i) {
const c = remainder.charAt(i);
if (c === '<')
++depth;
if (c === '>')
--depth;
if (depth === 0)
return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2) };
}
throw new Error('Should not be reached');
}
/**
* @param {MarkdownNode[]} body
* @param {MarkdownNode[]} params
*/
function applyTemplates(body, params) {
const paramsMap = new Map();
for (const node of params)
paramsMap.set('%%-' + node.text + '-%%', node);
const visit = (node, parent) => {
if (node.text && node.text.includes('-inline- = %%')) {
const [name, key] = node.text.split('-inline- = ');
const list = paramsMap.get(key);
if (!list)
throw new Error('Bad template: ' + key);
for (const prop of list.children) {
const template = paramsMap.get(prop.text);
if (!template)
throw new Error('Bad template: ' + prop.text);
const { name: argName } = parseArgument(template.children[0].text);
parent.children.push({
type: node.type,
text: name + argName,
children: template.children.map(c => md.clone(c))
});
}
} else if (node.text && node.text.includes(' = %%')) {
const [name, key] = node.text.split(' = ');
node.text = name;
const template = paramsMap.get(key);
if (!template)
throw new Error('Bad template: ' + key);
node.children.push(...template.children.map(c => md.clone(c)));
}
for (const child of node.children || [])
visit(child, node);
if (node.children)
node.children = node.children.filter(child => !child.text || !child.text.includes('-inline- = %%'));
};
for (const node of body)
visit(node, null);
return body;
}
/**
* @param {MarkdownNode} item
* @returns {MarkdownNode[]}
*/
function extractComments(item) {
return (item.children || []).filter(c => {
if (c.type.startsWith('h'))
return false;
if (c.type === 'li' && c.liType === 'default')
return false;
if (c.type === 'li' && c.text.startsWith('langs:'))
return false;
return true;
});
}
/**
* @param {string} comment
*/
function guessRequired(comment) {
let required = true;
if (comment.toLowerCase().includes('defaults to '))
required = false;
if (comment.startsWith('Optional'))
required = false;
if (comment.endsWith('Optional.'))
required = false;
if (comment.toLowerCase().includes('if set'))
required = false;
if (comment.toLowerCase().includes('if applicable'))
required = false;
if (comment.toLowerCase().includes('if available'))
required = false;
if (comment.includes('**required**'))
required = true;
return required;
}
/**
* @param {string} apiDir
*/
function parseApi(apiDir) {
return new ApiParser(apiDir).documentation;
}
module.exports = { parseApi };

View File

@ -20,16 +20,14 @@
const playwright = require('../../');
const fs = require('fs');
const path = require('path');
const { MDOutline } = require('./MDBuilder');
const { parseApi } = require('./api_parser');
const missingDocs = require('./missingDocs');
/** @typedef {import('./Documentation').Type} Type */
/** @typedef {import('./documentation').Type} Type */
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
const PROJECT_DIR = path.join(__dirname, '..', '..');
const links = new Map();
const rLinks = new Map();
const dirtyFiles = new Set();
run().catch(e => {
@ -38,9 +36,10 @@ run().catch(e => {
});;
async function run() {
const outline = new MDOutline(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
const documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
// This validates member links.
outline.setLinkRenderer(() => undefined);
documentation.setLinkRenderer(() => undefined);
documentation.filterForLanguage('js');
// Patch README.md
{
@ -69,7 +68,7 @@ async function run() {
{
const srcClient = path.join(PROJECT_DIR, 'src', 'client');
const sources = fs.readdirSync(srcClient).map(n => path.join(srcClient, n));
const errors = missingDocs(outline, sources, path.join(srcClient, 'api.ts'));
const errors = missingDocs(documentation, sources, path.join(srcClient, 'api.ts'));
if (errors.length) {
console.log('============================');
console.log('ERROR: missing documentation:');

View File

@ -30,6 +30,15 @@ const md = require('../markdown');
* next: ParsedType | null,
* }} ParsedType
*/
/**
* @typedef {function({
* clazz?: Documentation.Class,
* member?: Documentation.Member,
* param?: string,
* option?: string
* }): string} Renderer
*/
class Documentation {
/**
@ -39,25 +48,101 @@ class Documentation {
this.classesArray = classesArray;
/** @type {!Map<string, !Documentation.Class>} */
this.classes = new Map();
for (const cls of classesArray)
this.classes.set(cls.name, cls);
this.index();
}
/**
* @param {string[]} errors
*/
copyDocsFromSuperclasses(errors) {
for (const [name, clazz] of this.classes.entries()) {
clazz.validateOrder(errors, clazz);
if (!clazz.extends || clazz.extends === 'EventEmitter' || clazz.extends === 'Error')
continue;
const superClass = this.classes.get(clazz.extends);
if (!superClass) {
errors.push(`Undefined superclass: ${superClass} in ${name}`);
continue;
}
for (const memberName of clazz.members.keys()) {
if (superClass.members.has(memberName))
errors.push(`Member documentation overrides base: ${name}.${memberName} over ${clazz.extends}.${memberName}`);
}
clazz.membersArray = [...clazz.membersArray, ...superClass.membersArray.map(c => c.clone())];
clazz.index();
}
}
/**
* @param {string} lang
*/
filterForLanguage(lang) {
const classesArray = [];
for (const clazz of this.classesArray) {
if (clazz.langs && !clazz.langs.has(lang))
continue;
clazz.filterForLanguage(lang);
classesArray.push(clazz);
}
this.classesArray = classesArray;
this.index();
}
index() {
for (const cls of this.classesArray) {
this.classes.set(cls.name, cls);
cls.index();
}
}
/**
* @param {Renderer} linkRenderer
*/
setLinkRenderer(linkRenderer) {
// @type {Map<string, Documentation.Class>}
const classesMap = new Map();
const membersMap = new Map();
for (const clazz of this.classesArray) {
classesMap.set(clazz.name, clazz);
for (const member of clazz.membersArray)
membersMap.set(`${member.kind}: ${clazz.name}.${member.name}`, member);
}
this._patchLinks = nodes => patchLinks(nodes, classesMap, membersMap, linkRenderer);
for (const clazz of this.classesArray)
clazz.visit(item => this._patchLinks(item.spec));
}
/**
* @param {MarkdownNode[]} nodes
*/
renderLinksInText(nodes) {
this._patchLinks(nodes);
}
generateSourceCodeComments() {
for (const clazz of this.classesArray)
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
}
}
Documentation.Class = class {
/**
* @param {?Set<string>} langs
* @param {string} name
* @param {!Array<!Documentation.Member>} membersArray
* @param {?string=} extendsName
* @param {MarkdownNode[]=} spec
* @param {string[]=} templates
*/
constructor(name, membersArray, extendsName = null, spec = undefined, templates = []) {
constructor(langs, name, membersArray, extendsName = null, spec = undefined) {
this.langs = langs;
this.name = name;
this.membersArray = membersArray;
this.spec = spec;
this.extends = extendsName;
this.templates = templates;
this.comment = '';
this.index();
const match = name.match(/(JS|CDP|[A-Z])(.*)/);
@ -93,9 +178,24 @@ Documentation.Class = class {
this.eventsArray.push(member);
}
member.clazz = this;
member.index();
}
}
/**
* @param {string} lang
*/
filterForLanguage(lang) {
const membersArray = [];
for (const member of this.membersArray) {
if (member.langs && !member.langs.has(lang))
continue;
member.filterForLanguage(lang);
membersArray.push(member);
}
this.membersArray = membersArray;
}
validateOrder(errors, cls) {
const members = this.membersArray;
// Events should go first.
@ -157,6 +257,7 @@ Documentation.Class = class {
Documentation.Member = class {
/**
* @param {string} kind
* @param {?Set<string>} langs
* @param {string} name
* @param {?Documentation.Type} type
* @param {!Array<!Documentation.Member>} argsArray
@ -164,19 +265,18 @@ Documentation.Member = class {
* @param {boolean=} required
* @param {string[]=} templates
*/
constructor(kind, name, type, argsArray, spec = undefined, required = true, templates = []) {
constructor(kind, langs, name, type, argsArray, spec = undefined, required = true, templates = []) {
this.kind = kind;
this.langs = langs;
this.name = name;
this.type = type;
this.spec = spec;
this.argsArray = argsArray;
this.required = required;
this.templates = templates;
this.comment = '';
/** @type {!Map<string, !Documentation.Member>} */
this.args = new Map();
for (const arg of argsArray)
this.args.set(arg.name, arg);
this.index();
/** @type {!Documentation.Class} */
this.clazz = null;
this.deprecated = false;
@ -188,41 +288,66 @@ Documentation.Member = class {
}
}
index() {
this.args = new Map();
for (const arg of this.argsArray) {
this.args.set(arg.name, arg);
if (arg.name === 'options')
arg.type.properties.sort((p1, p2) => p1.name.localeCompare(p2.name));
}
}
/**
* @param {string} lang
*/
filterForLanguage(lang) {
const argsArray = [];
for (const arg of this.argsArray) {
if (arg.langs && !arg.langs.has(lang))
continue;
arg.filterForLanguage(lang);
argsArray.push(arg);
}
this.argsArray = argsArray;
}
clone() {
return new Documentation.Member(this.kind, this.name, this.type, this.argsArray, this.spec, this.required, this.templates);
return new Documentation.Member(this.kind, this.langs, this.name, this.type, this.argsArray, this.spec, this.required);
}
/**
* @param {?Set<string>} langs
* @param {string} name
* @param {!Array<!Documentation.Member>} argsArray
* @param {?Documentation.Type} returnType
* @param {MarkdownNode[]=} spec
* @param {string[]=} templates
* @return {!Documentation.Member}
*/
static createMethod(name, argsArray, returnType, spec, templates) {
return new Documentation.Member('method', name, returnType, argsArray, spec, undefined, templates);
static createMethod(langs, name, argsArray, returnType, spec) {
return new Documentation.Member('method', langs, name, returnType, argsArray, spec);
}
/**
* @param {?Set<string>} langs
* @param {string} name
* @param {!Documentation.Type} type
* @param {MarkdownNode[]=} spec
* @param {boolean=} required
* @return {!Documentation.Member}
*/
static createProperty(name, type, spec, required) {
return new Documentation.Member('property', name, type, [], spec, required);
static createProperty(langs, name, type, spec, required) {
return new Documentation.Member('property', langs, name, type, [], spec, required);
}
/**
* @param {?Set<string>} langs
* @param {string} name
* @param {?Documentation.Type=} type
* @param {MarkdownNode[]=} spec
* @return {!Documentation.Member}
*/
static createEvent(name, type = null, spec) {
return new Documentation.Member('event', name, type, [], spec);
static createEvent(langs, name, type = null, spec) {
return new Documentation.Member('event', langs, name, type, [], spec);
}
/**
@ -425,4 +550,49 @@ function matchingBracket(str, open, close) {
return i;
}
/**
* @param {MarkdownNode[]} spec
* @param {Map<string, Documentation.Class>} classesMap
* @param {Map<string, Documentation.Member>} membersMap
* @param {Renderer} linkRenderer
*/
function patchLinks(spec, classesMap, membersMap, linkRenderer) {
if (!spec)
return;
md.visitAll(spec, node => {
if (!node.text)
return;
node.text = node.text.replace(/\[`((?:event|method|property): [^\]]+)`\]/g, (match, p1) => {
const member = membersMap.get(p1);
if (!member)
throw new Error('Undefined member references: ' + match);
return linkRenderer({ member }) || match;
});
node.text = node.text.replace(/\[`(param|option): ([^\]]+)`\]/g, (match, p1, p2) => {
if (p1 === 'param')
return linkRenderer({ param: p2 }) || match;
if (p1 === 'option')
return linkRenderer({ option: p2 }) || match;
});
node.text = node.text.replace(/\[([\w]+)\]/, (match, p1) => {
const clazz = classesMap.get(p1);
if (clazz)
return linkRenderer({ clazz }) || match;
return match;
});
});
}
/**
* @param {MarkdownNode[]} spec
*/
function generateSourceCodeComment(spec) {
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
md.visitAll(comments, node => {
if (node.liType === 'bullet')
node.liType = 'default';
});
return md.render(comments, 120);
}
module.exports = Documentation;

View File

@ -18,14 +18,14 @@
const path = require('path');
const fs = require('fs');
const Documentation = require('./Documentation');
const { MDOutline } = require('./MDBuilder');
const Documentation = require('./documentation');
const { parseApi } = require('./api_parser');
const PROJECT_DIR = path.join(__dirname, '..', '..');
{
const outline = new MDOutline(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
outline.setLinkRenderer(item => {
const { clazz, member, param, option } = item;
const documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
documentation.setLinkRenderer(item => {
const { clazz, param, option } = item;
if (param)
return `\`${param}\``;
if (option)
@ -33,8 +33,8 @@ const PROJECT_DIR = path.join(__dirname, '..', '..');
if (clazz)
return `\`${clazz.name}\``;
});
outline.generateSourceCodeComments();
const result = serialize(outline);
documentation.generateSourceCodeComments();
const result = serialize(documentation);
fs.writeFileSync(path.join(PROJECT_DIR, 'api.json'), JSON.stringify(result));
}
@ -52,6 +52,8 @@ function serializeClass(clazz) {
const result = { name: clazz.name };
if (clazz.extends)
result.extends = clazz.extends;
if (clazz.langs)
result.langs = [...clazz.langs];
if (clazz.comment)
result.comment = clazz.comment;
result.members = clazz.membersArray.map(serializeMember);
@ -64,6 +66,8 @@ function serializeClass(clazz) {
function serializeMember(member) {
const result = /** @type {any} */ ({ ...member });
sanitize(result);
if (member.langs)
result.langs = [...member.langs];
result.args = member.argsArray.map(serializeProperty);
if (member.type)
result.type = serializeType(member.type)
@ -73,6 +77,8 @@ function serializeMember(member) {
function serializeProperty(arg) {
const result = { ...arg };
sanitize(result);
if (arg.langs)
result.langs = [...arg.langs];
if (arg.type)
result.type = serializeType(arg.type)
return result;

View File

@ -17,17 +17,13 @@
const ts = require('typescript');
const EventEmitter = require('events');
const Documentation = require('./Documentation');
const Documentation = require('./documentation');
/** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
/**
* @return {!Array<string>}
*/
module.exports = function lint(outline, jsSources, apiFileName) {
module.exports = function lint(documentation, jsSources, apiFileName) {
const errors = [];
const documentation = outline.documentation;
outline.copyDocsFromSuperclasses(errors);
documentation.copyDocsFromSuperclasses(errors);
const apiMethods = listMethods(jsSources, apiFileName);
for (const [className, methods] of apiMethods) {
const docClass = documentation.classes.get(className);

View File

@ -19,17 +19,17 @@ const fs = require('fs');
const path = require('path');
const missingDocs = require('../missingDocs');
const { folio } = require('folio');
const { MDOutline } = require('../MDBuilder');
const { parseApi } = require('../api_parser');
const { test, expect } = folio;
test('missing docs', async ({}) => {
const outline = new MDOutline(path.join(__dirname));
const documentation = parseApi(path.join(__dirname));
const tsSources = [
path.join(__dirname, 'test-api.ts'),
path.join(__dirname, 'test-api-class.ts'),
];
const errors = missingDocs(outline, tsSources, path.join(__dirname, 'test-api.ts'));
const errors = missingDocs(documentation, tsSources, path.join(__dirname, 'test-api.ts'));
expect(errors).toEqual([
'Missing documentation for "Exists.exists2.extra"',
'Missing documentation for "Exists.exists2.options"',

View File

@ -18,12 +18,12 @@
const path = require('path');
const os = require('os');
const {devices} = require('../..');
const Documentation = require('../doclint/Documentation');
const Documentation = require('../doclint/documentation');
const PROJECT_DIR = path.join(__dirname, '..', '..');
const fs = require('fs');
const {parseOverrides} = require('./parseOverrides');
const exported = require('./exported.json');
const { MDOutline } = require('../doclint/MDBuilder');
const { parseApi } = require('../doclint/api_parser');
const objectDefinitions = [];
const handledMethods = new Set();
@ -37,13 +37,14 @@ let hadChanges = false;
fs.mkdirSync(typesDir)
writeFile(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'server', 'chromium', 'protocol.ts'), 'utf8'));
writeFile(path.join(typesDir, 'trace.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'trace', 'traceTypes.ts'), 'utf8'));
const outline = new MDOutline(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
outline.copyDocsFromSuperclasses([]);
documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
documentation.filterForLanguage('js');
documentation.copyDocsFromSuperclasses([]);
const createMemberLink = (text) => {
const anchor = text.toLowerCase().split(',').map(c => c.replace(/[^a-z]/g, '')).join('-');
return `[${text}](https://github.com/microsoft/playwright/blob/master/docs/api.md#${anchor})`;
};
outline.setLinkRenderer(item => {
documentation.setLinkRenderer(item => {
const { clazz, member, param, option } = item;
if (param)
return `\`${param}\``;
@ -59,8 +60,7 @@ let hadChanges = false;
return createMemberLink(`${member.clazz.varName}.${member.name}`);
throw new Error('Unknown member kind ' + member.kind);
});
outline.generateSourceCodeComments();
documentation = outline.documentation;
documentation.generateSourceCodeComments();
// Root module types are overridden.
const playwrightClass = documentation.classes.get('Playwright');