diff --git a/docs/src/api/class-playwright~python.md b/docs/src/api/class-playwright~python.md new file mode 100644 index 0000000000..10fe2ccb33 --- /dev/null +++ b/docs/src/api/class-playwright~python.md @@ -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() +``` diff --git a/utils/doclint/MDBuilder.js b/utils/doclint/MDBuilder.js deleted file mode 100644 index 6544c6f27c..0000000000 --- a/utils/doclint/MDBuilder.js +++ /dev/null @@ -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} */ 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} - 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} classesMap - * @param {Map} 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 }; diff --git a/utils/doclint/api_parser.js b/utils/doclint/api_parser.js new file mode 100644 index 0000000000..e1478a35ba --- /dev/null +++ b/utils/doclint/api_parser.js @@ -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} */ + 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 }; diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index 3880b92510..2002ffb606 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -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:'); diff --git a/utils/doclint/Documentation.js b/utils/doclint/documentation.js similarity index 68% rename from utils/doclint/Documentation.js rename to utils/doclint/documentation.js index 94b1a029bb..459e69e978 100644 --- a/utils/doclint/Documentation.js +++ b/utils/doclint/documentation.js @@ -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} */ 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} + 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} langs * @param {string} name * @param {!Array} 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} langs * @param {string} name * @param {?Documentation.Type} type * @param {!Array} 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} */ 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} langs * @param {string} name * @param {!Array} 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} 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} 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} classesMap + * @param {Map} 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; diff --git a/utils/doclint/generateApiJson.js b/utils/doclint/generateApiJson.js index 81814e002a..b3e39ad5da 100644 --- a/utils/doclint/generateApiJson.js +++ b/utils/doclint/generateApiJson.js @@ -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; diff --git a/utils/doclint/missingDocs.js b/utils/doclint/missingDocs.js index 8263a76b80..1dddfc5a2c 100644 --- a/utils/doclint/missingDocs.js +++ b/utils/doclint/missingDocs.js @@ -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} - */ -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); diff --git a/utils/doclint/test/missingDocs.spec.js b/utils/doclint/test/missingDocs.spec.js index 6be9a5e511..348fbada65 100644 --- a/utils/doclint/test/missingDocs.spec.js +++ b/utils/doclint/test/missingDocs.spec.js @@ -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"', diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index d012a5c402..52046b386c 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -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');