mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: language-specific members api (#4936)
This commit is contained in:
parent
8d649949c0
commit
e56832b646
18
docs/src/api/class-playwright~python.md
Normal file
18
docs/src/api/class-playwright~python.md
Normal 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()
|
||||
```
|
@ -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
284
utils/doclint/api_parser.js
Normal 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 };
|
@ -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:');
|
||||
|
@ -31,6 +31,15 @@ const md = require('../markdown');
|
||||
* }} ParsedType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {function({
|
||||
* clazz?: Documentation.Class,
|
||||
* member?: Documentation.Member,
|
||||
* param?: string,
|
||||
* option?: string
|
||||
* }): string} Renderer
|
||||
*/
|
||||
|
||||
class Documentation {
|
||||
/**
|
||||
* @param {!Array<!Documentation.Class>} classesArray
|
||||
@ -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;
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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"',
|
||||
|
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user