doc: generator code health (2) (#4843)

This commit is contained in:
Pavel Feldman 2020-12-28 23:42:51 -08:00 committed by GitHub
parent 8fbb984f64
commit 722db85e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 562 additions and 534 deletions

File diff suppressed because it is too large Load diff

View file

@ -11,23 +11,23 @@ When to consider operation succeeded, defaults to `load`. Events can be either:
Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout.
The default value can be changed by using the
[`method: BrowserContext.setDefaultNavigationTimeout`](),
[`method: BrowserContext.setDefaultTimeout`](),
[`method: Page.setDefaultNavigationTimeout`]() or
[`method: Page.setDefaultTimeout`]() methods.
[`method: BrowserContext.setDefaultNavigationTimeout`],
[`method: BrowserContext.setDefaultTimeout`],
[`method: Page.setDefaultNavigationTimeout`] or
[`method: Page.setDefaultTimeout`] methods.
## wait-for-timeout
- `timeout` <[number]>
maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
value can be changed by using the [`method: BrowserContext.setDefaultTimeout`]().
value can be changed by using the [`method: BrowserContext.setDefaultTimeout`].
## input-timeout
- `timeout` <[number]>
Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by
using the [`method: BrowserContext.setDefaultTimeout`]() or
[`method: Page.setDefaultTimeout`]() methods.
using the [`method: BrowserContext.setDefaultTimeout`] or
[`method: Page.setDefaultTimeout`] methods.
## input-no-wait-after
- `noWaitAfter` <[boolean]>
@ -117,7 +117,7 @@ Defaults to `'visible'`. Can be either:
- `value` <[string]>
Populates context with given storage state. This method can be used to initialize context with logged-in information
obtained via [`method: BrowserContext.storageState`](). Either a path to the file with saved storage, or an object with the following fields:
obtained via [`method: BrowserContext.storageState`]. Either a path to the file with saved storage, or an object with the following fields:
## context-option-acceptdownloads
- `acceptDownloads` <[boolean]>
@ -189,7 +189,7 @@ request header value as well as number and date formatting rules.
- `permissions` <[Array]<[string]>>
A list of permissions to grant to all pages in this context. See
[`method: BrowserContext.grantPermissions`]() for more details.
[`method: BrowserContext.grantPermissions`] for more details.
## context-option-extrahttpheaders
- `extraHTTPHeaders` <[Object]<[string], [string]>>
@ -212,7 +212,7 @@ Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/W
- `colorScheme` <"light"|"dark"|"no-preference">
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
[`method: Page.emulateMedia`]() for more details. Defaults to '`light`'.
[`method: Page.emulateMedia`] for more details. Defaults to '`light`'.
## context-option-logger
- `logger` <[Logger]>
@ -222,17 +222,16 @@ Logger sink for Playwright logging.
## context-option-videospath
- `videosPath` <[string]>
**NOTE** Use [`param: recordVideo`]() instead, it takes precedence over `videosPath`. Enables video recording for all pages to
`videosPath` directory. If not specified, videos are not recorded. Make sure to await
[`method: BrowserContext.close`]() for videos to be saved.
**NOTE** Use [`option: recordVideo`] instead, it takes precedence over [`option: videosPath`]. Enables video recording for all pages to [`option: videosPath`] directory. If not specified, videos are not recorded. Make sure to await
[`method: BrowserContext.close`] for videos to be saved.
## context-option-videosize
- `videoSize` <[Object]>
- `width` <[number]> Video frame width.
- `height` <[number]> Video frame height.
**NOTE** Use [`param: recordVideo`]() instead, it takes precedence over `videoSize`. Specifies dimensions of the automatically
recorded video. Can only be used if `videosPath` is set. If not specified the size will be equal to `viewport`. If
**NOTE** Use [`option: recordVideo`] instead, it takes precedence over [`option: videoSize`]. Specifies dimensions of the automatically
recorded video. Can only be used if [`option: videosPath`] is set. If not specified the size will be equal to `viewport`. If
`viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled
down if necessary to fit specified size.
@ -243,7 +242,7 @@ down if necessary to fit specified size.
- `path` <[string]> Path on the filesystem to write the HAR file to.
Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not
specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.close`]() for the HAR to be
specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.close`] for the HAR to be
saved.
## context-option-recordvideo
@ -256,7 +255,7 @@ saved.
- `height` <[number]> Video frame height.
Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. Make
sure to await [`method: BrowserContext.close`]() for videos to be saved.
sure to await [`method: BrowserContext.close`] for videos to be saved.
## context-option-proxy
- `proxy` <[Object]>

View file

@ -746,7 +746,7 @@ Removes a route created with [`browserContext.route(url, handler)`](#browsercont
- `event` <[string]> Event name, same one would pass into `browserContext.on(event)`.
- `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object. Optional.
- `predicate` <[Function]> receives the event data and resolves to truthy value when the waiting should resolve.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout).
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [`browserContext.setDefaultTimeout(timeout)`](#browsercontextsetdefaulttimeouttimeout).
- returns: <[Promise]<[Object]>>
Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy value. Will throw an error if the context closes before the event is fired. Returns the event data value.
@ -2041,7 +2041,7 @@ Video object associated with this page.
- `event` <[string]> Event name, same one would pass into `page.on(event)`.
- `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object. Optional.
- `predicate` <[Function]> receives the event data and resolves to truthy value when the waiting should resolve.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout).
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [`browserContext.setDefaultTimeout(timeout)`](#browsercontextsetdefaulttimeouttimeout).
- returns: <[Promise]<[Object]>>
Returns the event data value.
@ -4421,7 +4421,7 @@ Contains the URL of the WebSocket.
- `event` <[string]> Event name, same one would pass into `webSocket.on(event)`.
- `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object. Optional.
- `predicate` <[Function]> receives the event data and resolves to truthy value when the waiting should resolve.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout).
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [`browserContext.setDefaultTimeout(timeout)`](#browsercontextsetdefaulttimeouttimeout).
- returns: <[Promise]<[Object]>>
Returns the event data value.

View file

@ -47,6 +47,8 @@ Documentation.Class = class {
this.templates = templates;
this.comment = '';
this.index();
const match = name.match(/(JS|CDP|[A-Z])(.*)/);
this.varName = match[1].toLowerCase() + match[2];
}
index() {
@ -77,6 +79,7 @@ Documentation.Class = class {
this.events.set(member.name, member);
this.eventsArray.push(member);
}
member.clazz = this;
}
}
@ -161,6 +164,13 @@ Documentation.Member = class {
this.args = new Map();
for (const arg of argsArray)
this.args.set(arg.name, arg);
/** @type {!Documentation.Class} */
this.clazz = null;
this.signature = this._createSignature();
}
clone() {
return new Documentation.Member(this.kind, this.name, this.type, this.argsArray, this.spec, this.required, this.templates);
}
/**
@ -206,6 +216,29 @@ Documentation.Member = class {
for (const arg of this.argsArray)
arg.visit(visitor);
}
_createSignature() {
const tokens = [];
let hasOptional = false;
for (const arg of this.argsArray) {
const optional = !arg.required;
if (tokens.length) {
if (optional && !hasOptional)
tokens.push(`[, ${arg.name}`);
else
tokens.push(`, ${arg.name}`);
} else {
if (optional && !hasOptional)
tokens.push(`[${arg.name}`);
else
tokens.push(`${arg.name}`);
}
hasOptional = hasOptional || optional;
}
if (hasOptional)
tokens.push(']');
return tokens.join('');
}
};
Documentation.Type = class {

View file

@ -22,13 +22,19 @@ 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} bodyPath
* @param {string=} paramsPath
* @param {string=} links
*/
constructor(bodyPath, paramsPath, links = '') {
constructor(bodyPath, paramsPath) {
const body = md.parse(fs.readFileSync(bodyPath).toString());
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : null;
const api = params ? applyTemplates(body, params) : body;
@ -39,14 +45,6 @@ class MDOutline {
this.classesArray.push(c);
this.classes.set(c.name, c);
}
const linksMap = new Map();
for (const link of links.replace(/\r\n/g, '\n').split('\n')) {
if (!link)
continue;
const match = link.match(/\[([^\]]+)\]: ([^"]+) "([^"]+)"/);
linksMap.set(new RegExp('\\[' + match[1] + '\\]', 'g'), { href: match[2], label: match[3] });
}
this.signatures = this._generateComments(linksMap);
this.documentation = new Documentation(this.classesArray);
}
@ -69,51 +67,33 @@ class MDOutline {
errors.push(`Member documentation overrides base: ${name}.${memberName} over ${clazz.extends}.${memberName}`);
}
clazz.membersArray = [...clazz.membersArray, ...superClass.membersArray];
clazz.membersArray = [...clazz.membersArray, ...superClass.membersArray.map(c => c.clone())];
clazz.index();
}
}
/**
* @param {Map<string, { href: string, label: string}>} linksMap
*/
_generateComments(linksMap) {
/**
* @type {Map<string, string>}
*/
const signatures = new Map();
/**
* @param {Renderer} linkRenderer
*/
renderLinks(linkRenderer) {
const externalLinksMap = new Map();
// @type {Map<string, Documentation.Class>}
const classesMap = new Map();
const membersMap = new Map();
for (const clazz of this.classesArray) {
for (const method of clazz.methodsArray) {
const tokens = [];
let hasOptional = false;
for (const arg of method.argsArray) {
const optional = !arg.required;
if (tokens.length) {
if (optional && !hasOptional)
tokens.push(`[, ${arg.name}`);
else
tokens.push(`, ${arg.name}`);
} else {
if (optional && !hasOptional)
tokens.push(`[${arg.name}`);
else
tokens.push(`${arg.name}`);
}
hasOptional = hasOptional || optional;
}
if (hasOptional)
tokens.push(']');
const signature = tokens.join('');
const methodName = `${clazz.name}.${method.name}`;
signatures.set(methodName, signature);
}
classesMap.set(clazz.name, clazz);
for (const member of clazz.membersArray)
membersMap.set(`${member.kind}: ${clazz.name}.${member.name}`, member);
}
for (const clazz of this.classesArray)
clazz.visit(item => patchLinks(item.spec, signatures));
clazz.visit(item => patchLinks(item, item.spec, classesMap, membersMap, linkRenderer));
}
renderComments() {
for (const clazz of this.classesArray)
clazz.visit(item => item.comment = renderCommentsForSourceCode(item.spec, linksMap));
return signatures;
clazz.visit(item => item.comment = renderLinksForSourceCode(item.spec));
}
}
@ -146,20 +126,10 @@ function extractComments(item) {
/**
* @param {MarkdownNode[]} spec
* @param {Map<string, { href: string, label: string}>} linksMap
*/
function renderCommentsForSourceCode(spec, linksMap) {
function renderLinksForSourceCode(spec) {
const comments = (spec || []).filter(n => n.type !== 'gen' && !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
md.visitAll(comments, node => {
if (node.text) {
for (const [regex, { href, label }] of linksMap)
node.text = node.text.replace(regex, `[${label}](${href})`);
// Those with in `` can have nested [], hence twice twice.
node.text = node.text.replace(/\[`([^`]+)`\]\(#([^\)]+)\)/g, '[`$1`](https://github.com/microsoft/playwright/blob/master/docs/api.md#$2)');
node.text = node.text.replace(/\[([^\]]+)\]\(#([^\)]+)\)/g, '[$1](https://github.com/microsoft/playwright/blob/master/docs/api.md#$2)');
node.text = node.text.replace(/\[`([^`]+)`\]\(\.\/([^\)]+)\)/g, '[`$1`](https://github.com/microsoft/playwright/blob/master/docs/$2)');
node.text = node.text.replace(/\[([^\]]+)\]\(\.\/([^\)]+)\)/g, '[$1](https://github.com/microsoft/playwright/blob/master/docs/$2)');
}
if (node.liType === 'bullet')
node.liType = 'default';
});
@ -167,47 +137,39 @@ function renderCommentsForSourceCode(spec, linksMap) {
}
/**
* @param {Documentation.Class|Documentation.Member} item
* @param {MarkdownNode[]} spec
* @param {Map<string, string>} [signatures]
* @param {Map<string, Documentation.Class>} classesMap
* @param {Map<string, Documentation.Member>} membersMap
* @param {Renderer} linkRenderer
*/
function patchLinks(spec, signatures) {
for (const node of spec || []) {
if (node.type === 'text')
node.text = patchLinksInText(node.text, signatures);
if (node.type === 'li') {
node.text = patchLinksInText(node.text, signatures);
patchLinks(node.children, signatures);
}
}
}
/**
* @param {string} text
* @returns {string}
*/
function createLink(text) {
const anchor = text.toLowerCase().split(',').map(c => c.replace(/[^a-z]/g, '')).join('-');
return `[\`${text}\`](#${anchor})`;
}
/**
* @param {string} comment
* @param {Map<string, string>} signatures
*/
function patchLinksInText(comment, signatures) {
if (!signatures)
return comment;
comment = comment.replace(/\[`(event|method|property):\s(JS|CDP|[A-Z])([^.]+)\.([^`]+)`\]\(\)/g, (match, type, clazzPrefix, clazz, name) => {
const className = `${clazzPrefix.toLowerCase()}${clazz}`;
if (type === 'event')
return createLink(`${className}.on('${name}')`);
if (type === 'method') {
const signature = signatures.get(`${clazzPrefix}${clazz}.${name}`) || '';
return createLink(`${className}.${name}(${signature})`);
}
return createLink(`${className}.${name}`);
function patchLinks(item, spec, classesMap, membersMap, linkRenderer) {
if (!spec)
return;
md.visitAll(spec, node => {
if (!node.text)
return;
node.text = node.text.replace(/\[`((?:event|method|property): [^\]]+)`\]/g, (_, p1) => {
const member = membersMap.get(p1);
return linkRenderer({ member });
});
node.text = node.text.replace(/\[`(param|option): ([^\]]+)`\]/g, (_, p1, p2) => {
const context = {
clazz: item instanceof Documentation.Class ? item : undefined,
member: item instanceof Documentation.Member ? item : undefined,
};
if (p1 === 'param')
return linkRenderer({ ...context, param: p2 });
if (p1 === 'option')
return linkRenderer({ ...context, option: p2 });
});
node.text = node.text.replace(/\[([\w]+)\]/, (match, p1) => {
const clazz = classesMap.get(p1);
if (clazz)
return linkRenderer({ clazz });
return match;
});
});
return comment.replace(/\[`(?:param|option):\s([^`]+)`\]\(\)/g, '`$1`');
}
/**
@ -216,7 +178,7 @@ function patchLinksInText(comment, signatures) {
*/
function parseMember(member) {
const args = [];
const match = member.text.match(/(event|method|property|async method|): (JS|CDP|[A-Z])([^.]+)\.(.*)/);
const match = member.text.match(/(event|method|property|async method): (JS|CDP|[A-Z])([^.]+)\.(.*)/);
const name = match[4];
let returnType = null;
const options = [];
@ -261,7 +223,7 @@ function parseProperty(spec) {
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(renderCommentsForSourceCode(comments, new Map())));
return Documentation.Member.createProperty(name, parseType(param), comments, guessRequired(md.render(comments)));
}
/**

View file

@ -63,17 +63,36 @@ async function run() {
// Produce api.md
{
const createMemberLink = (text) => {
const anchor = text.toLowerCase().split(',').map(c => c.replace(/[^a-z]/g, '')).join('-');
return `[\`${text}\`](#${anchor})`;
};
outline.renderLinks(item => {
const { clazz, member, param, option } = item;
if (param)
return `\`${param}\``;
if (option)
return `\`${option}\``;
if (clazz)
return `[${clazz.name}]`;
if (member.kind === 'method')
return createMemberLink(`${member.clazz.varName}.${member.name}(${member.signature})`);
if (member.kind === 'event')
return createMemberLink(`${member.clazz.varName}.on('${member.name}')`);
if (member.kind === 'property')
return createMemberLink(`${member.clazz.varName}.${member.name}`);
throw new Error('Unknown member kind ' + member.kind);
});
const comment = '<!-- THIS FILE IS NOW GENERATED -->';
{
const signatures = outline.signatures;
/** @type {MarkdownNode[]} */
const result = [];
for (const clazz of outline.classesArray) {
// Iterate over classes, create header node.
/** @type {MarkdownNode} */
const classNode = { type: 'h3', text: `class: ${clazz.name}` };
const match = clazz.name.match(/(JS|CDP|[A-Z])(.*)/);
const varName = match[1].toLocaleLowerCase() + match[2];
result.push(classNode);
// Append link shortcut to resolve text like [Browser]
result.push({
@ -88,13 +107,12 @@ async function run() {
/** @type {MarkdownNode} */
const memberNode = { type: 'h4', children: [] };
if (member.kind === 'event') {
memberNode.text = `${varName}.on('${member.name}')`;
memberNode.text = `${clazz.varName}.on('${member.name}')`;
} else if (member.kind === 'property') {
memberNode.text = `${varName}.${member.name}`;
memberNode.text = `${clazz.varName}.${member.name}`;
} else if (member.kind === 'method') {
// Patch method signatures
const signature = signatures.get(clazz.name + '.' + member.name);
memberNode.text = `${varName}.${member.name}(${signature})`;
memberNode.text = `${clazz.varName}.${member.name}(${member.signature})`;
for (const arg of member.argsArray) {
if (arg.type)
memberNode.children.push(renderProperty(`\`${arg.name}\``, arg.type, arg.spec));

View file

@ -62,6 +62,7 @@ function serializeMember(member) {
const result = { ...member };
sanitize(result);
result.args = {};
delete member.clazz;
for (const arg of member.argsArray)
result.args[arg.name] = serializeProperty(arg);
if (member.type)

View file

@ -38,6 +38,27 @@ let hadChanges = false;
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-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
outline.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.renderLinks(item => {
const { clazz, member, param, option } = item;
if (param)
return `\`${param}\``;
if (option)
return `\`${option}\``;
if (clazz)
return `[${clazz.name}]`;
if (member.kind === 'method')
return createMemberLink(`${member.clazz.varName}.${member.name}(${member.signature})`);
if (member.kind === 'event')
return createMemberLink(`${member.clazz.varName}.on('${member.name}')`);
if (member.kind === 'property')
return createMemberLink(`${member.clazz.varName}.${member.name}`);
throw new Error('Unknown member kind ' + member.kind);
});
outline.renderComments();
documentation = outline.documentation;
// Root module types are overridden.
@ -239,6 +260,12 @@ function parentClass(classDesc) {
function writeComment(comment, indent = '') {
const parts = [];
comment = comment.replace(/\[`([^`]+)`\]\(#([^\)]+)\)/g, '[`$1`](https://github.com/microsoft/playwright/blob/master/docs/api.md#$2)');
comment = comment.replace(/\[([^\]]+)\]\(#([^\)]+)\)/g, '[$1](https://github.com/microsoft/playwright/blob/master/docs/api.md#$2)');
comment = comment.replace(/\[`([^`]+)`\]\(\.\/([^\)]+)\)/g, '[`$1`](https://github.com/microsoft/playwright/blob/master/docs/$2)');
comment = comment.replace(/\[([^\]]+)\]\(\.\/([^\)]+)\)/g, '[$1](https://github.com/microsoft/playwright/blob/master/docs/$2)');
parts.push(indent + '/**');
parts.push(...comment.split('\n').map(line => indent + ' * ' + line.replace(/\*\//g, '*\\/')));
parts.push(indent + ' */');
@ -411,18 +438,6 @@ function memberJSDOC(member, indent) {
return writeComment(lines.join('\n'), indent) + '\n' + indent;
}
/**
* @param {Documentation.Class} mdClass
* @param {Documentation.Class} jsClass
* @return {Documentation.Class}
*/
function mergeClasses(mdClass, jsClass) {
mdClass.templates = jsClass.templates;
for (const member of mdClass.membersArray)
member.templates = jsClass.members.get(member.name).templates;
return mdClass;
}
function generateDevicesTypes() {
const namedDevices =
Object.keys(devices)