doc: generator code health (#4840)

This commit is contained in:
Pavel Feldman 2020-12-28 17:38:00 -08:00 committed by GitHub
parent a1232b6980
commit 70c14e6b99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 221 additions and 205 deletions

View file

@ -16,14 +16,7 @@
// @ts-check // @ts-check
/** @typedef {{ /** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
* type: 'text' | 'li' | 'code' | 'gen' | 'h1' | 'h2' | 'h3' | 'h4',
* text?: string,
* codeLang?: string,
* lines?: string[],
* liType?: 'default' | 'bullet' | 'ordinal',
* children?: MarkdownNode[]
* }} MarkdownNode */
class Documentation { class Documentation {
/** /**
@ -64,10 +57,6 @@ Documentation.Class = class {
/** @type {!Array<!Documentation.Member>} */ /** @type {!Array<!Documentation.Member>} */
this.propertiesArray = []; this.propertiesArray = [];
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Documentation.Member>} */
this.options = new Map();
/** @type {!Array<!Documentation.Member>} */
this.optionsArray = [];
/** @type {!Map<string, !Documentation.Member>} */
this.methods = new Map(); this.methods = new Map();
/** @type {!Array<!Documentation.Member>} */ /** @type {!Array<!Documentation.Member>} */
this.methodsArray = []; this.methodsArray = [];
@ -135,6 +124,9 @@ Documentation.Class = class {
} }
} }
/**
* @param {function(Documentation.Member|Documentation.Class): void} visitor
*/
visit(visitor) { visit(visitor) {
visitor(this); visitor(this);
for (const p of this.propertiesArray) for (const p of this.propertiesArray)
@ -204,6 +196,9 @@ Documentation.Member = class {
return new Documentation.Member('event', name, type, [], spec); return new Documentation.Member('event', name, type, [], spec);
} }
/**
* @param {function(Documentation.Member|Documentation.Class): void} visitor
*/
visit(visitor) { visit(visitor) {
visitor(this); visitor(this);
if (this.type) if (this.type)

View file

@ -16,17 +16,22 @@
// @ts-check // @ts-check
const { parseArgument, renderMd, clone } = require('../parse_md'); const fs = require('fs');
const md = require('../markdown');
const Documentation = require('./Documentation'); const Documentation = require('./Documentation');
/** @typedef {import('./Documentation').MarkdownNode} MarkdownNode */ /** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
class MDOutline { class MDOutline {
/** /**
* @param {MarkdownNode[]} api * @param {string} bodyPath
* @param {string=} paramsPath
* @param {string=} links * @param {string=} links
*/ */
constructor(api, links = '') { constructor(bodyPath, paramsPath, links = '') {
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;
this.classesArray = /** @type {Documentation.Class[]} */ []; this.classesArray = /** @type {Documentation.Class[]} */ [];
this.classes = /** @type {Map<string, Documentation.Class>} */ new Map(); this.classes = /** @type {Map<string, Documentation.Class>} */ new Map();
for (const clazz of api) { for (const clazz of api) {
@ -42,8 +47,32 @@ class MDOutline {
linksMap.set(new RegExp('\\[' + match[1] + '\\]', 'g'), { href: match[2], label: match[3] }); linksMap.set(new RegExp('\\[' + match[1] + '\\]', 'g'), { href: match[2], label: match[3] });
} }
this.signatures = this._generateComments(linksMap); this.signatures = this._generateComments(linksMap);
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];
clazz.index();
}
}
/** /**
* @param {Map<string, { href: string, label: string}>} linksMap * @param {Map<string, { href: string, label: string}>} linksMap
*/ */
@ -81,7 +110,7 @@ class MDOutline {
} }
for (const clazz of this.classesArray) for (const clazz of this.classesArray)
clazz.visit(item => patchSignatures(item.spec, signatures)); clazz.visit(item => patchLinks(item.spec, signatures));
for (const clazz of this.classesArray) for (const clazz of this.classesArray)
clazz.visit(item => item.comment = renderCommentsForSourceCode(item.spec, linksMap)); clazz.visit(item => item.comment = renderCommentsForSourceCode(item.spec, linksMap));
return signatures; return signatures;
@ -120,8 +149,8 @@ function extractComments(item) {
* @param {Map<string, { href: string, label: string}>} linksMap * @param {Map<string, { href: string, label: string}>} linksMap
*/ */
function renderCommentsForSourceCode(spec, linksMap) { function renderCommentsForSourceCode(spec, linksMap) {
const comments = (spec || []).filter(n => n.type !== 'gen' && !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => clone(c)); const comments = (spec || []).filter(n => n.type !== 'gen' && !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
const visit = node => { md.visitAll(comments, node => {
if (node.text) { if (node.text) {
for (const [regex, { href, label }] of linksMap) for (const [regex, { href, label }] of linksMap)
node.text = node.text.replace(regex, `[${label}](${href})`); node.text = node.text.replace(regex, `[${label}](${href})`);
@ -133,27 +162,21 @@ function renderCommentsForSourceCode(spec, linksMap) {
} }
if (node.liType === 'bullet') if (node.liType === 'bullet')
node.liType = 'default'; node.liType = 'default';
for (const child of node.children || []) });
visit(child); return md.render(comments);
};
for (const node of comments)
visit(node);
return renderMd(comments, 10000);
// [`frame.waitForFunction(pageFunction[, arg, options])`](#framewaitforfunctionpagefunction-arg-options)
} }
/** /**
* @param {MarkdownNode[]} spec * @param {MarkdownNode[]} spec
* @param {Map<string, string>} [signatures] * @param {Map<string, string>} [signatures]
*/ */
function patchSignatures(spec, signatures) { function patchLinks(spec, signatures) {
for (const node of spec || []) { for (const node of spec || []) {
if (node.type === 'text') if (node.type === 'text')
node.text = patchSignaturesInText(node.text, signatures); node.text = patchLinksInText(node.text, signatures);
if (node.type === 'li') { if (node.type === 'li') {
node.text = patchSignaturesInText(node.text, signatures); node.text = patchLinksInText(node.text, signatures);
patchSignatures(node.children, signatures); patchLinks(node.children, signatures);
} }
} }
} }
@ -171,7 +194,7 @@ function createLink(text) {
* @param {string} comment * @param {string} comment
* @param {Map<string, string>} signatures * @param {Map<string, string>} signatures
*/ */
function patchSignaturesInText(comment, signatures) { function patchLinksInText(comment, signatures) {
if (!signatures) if (!signatures)
return comment; return comment;
comment = comment.replace(/\[`(event|method|property):\s(JS|CDP|[A-Z])([^.]+)\.([^`]+)`\]\(\)/g, (match, type, clazzPrefix, clazz, name) => { comment = comment.replace(/\[`(event|method|property):\s(JS|CDP|[A-Z])([^.]+)\.([^`]+)`\]\(\)/g, (match, type, clazzPrefix, clazz, name) => {
@ -291,36 +314,79 @@ function guessRequired(comment) {
return required; return required;
} }
module.exports =
/** /**
* @param {any} api * @param {MarkdownNode[]} body
* @param {boolean=} copyDocsFromSuperClasses * @param {MarkdownNode[]} params
*/ */
function(api, copyDocsFromSuperClasses = false, links = '') { function applyTemplates(body, params) {
const errors = []; const paramsMap = new Map();
const outline = new MDOutline(api, links); for (const node of params)
const documentation = new Documentation(outline.classesArray); paramsMap.set('%%-' + node.text + '-%%', node);
if (copyDocsFromSuperClasses) { const visit = (node, parent) => {
// Push base class documentation to derived classes. if (node.text && node.text.includes('-inline- = %%')) {
for (const [name, clazz] of documentation.classes.entries()) { const [name, key] = node.text.split('-inline- = ');
clazz.validateOrder(errors, clazz); const list = paramsMap.get(key);
if (!list)
if (!clazz.extends || clazz.extends === 'EventEmitter' || clazz.extends === 'Error') throw new Error('Bad template: ' + key);
continue; for (const prop of list.children) {
const superClass = documentation.classes.get(clazz.extends); const template = paramsMap.get(prop.text);
if (!superClass) { if (!template)
errors.push(`Undefined superclass: ${superClass} in ${name}`); throw new Error('Bad template: ' + prop.text);
continue; 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))
});
} }
for (const memberName of clazz.members.keys()) { } else if (node.text && node.text.includes(' = %%')) {
if (superClass.members.has(memberName)) const [name, key] = node.text.split(' = ');
errors.push(`Member documentation overrides base: ${name}.${memberName} over ${clazz.extends}.${memberName}`); node.text = name;
} const template = paramsMap.get(key);
if (!template)
clazz.membersArray = [...clazz.membersArray, ...superClass.membersArray]; throw new Error('Bad template: ' + key);
clazz.index(); 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) };
} }
return { documentation, errors, outline }; throw new Error('Should not be reached');
}; }
module.exports = { MDOutline };

View file

@ -21,13 +21,14 @@ const playwright = require('../../');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const Source = require('./Source'); const Source = require('./Source');
const { parseMd, renderMd, applyTemplates, clone } = require('./../parse_md'); const md = require('../markdown');
const { spawnSync } = require('child_process'); const { spawnSync } = require('child_process');
const preprocessor = require('./preprocessor'); const preprocessor = require('./preprocessor');
const mdBuilder = require('./MDBuilder'); const { MDOutline } = require('./MDBuilder');
const missingDocs = require('./missingDocs');
/** @typedef {import('./Documentation').MarkdownNode} MarkdownNode */
/** @typedef {import('./Documentation').Type} Type */ /** @typedef {import('./Documentation').Type} Type */
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
const PROJECT_DIR = path.join(__dirname, '..', '..'); const PROJECT_DIR = path.join(__dirname, '..', '..');
const VERSION = require(path.join(PROJECT_DIR, 'package.json')).version; const VERSION = require(path.join(PROJECT_DIR, 'package.json')).version;
@ -56,21 +57,20 @@ async function run() {
let changedFiles = false; let changedFiles = false;
const header = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-header.md')).toString(); const header = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-header.md')).toString();
const body = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-body.md')).toString();
const footer = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-footer.md')).toString(); const footer = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-footer.md')).toString();
const links = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-links.md')).toString(); const links = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-links.md')).toString();
const params = fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-params.md')).toString(); const outline = new MDOutline(path.join(PROJECT_DIR, 'docs-src', 'api-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
const apiSpec = applyTemplates(parseMd(body), parseMd(params));
// Produce api.md // Produce api.md
{ {
const comment = '<!-- THIS FILE IS NOW GENERATED -->'; const comment = '<!-- THIS FILE IS NOW GENERATED -->';
{ {
const { outline } = mdBuilder(apiSpec, false);
const signatures = outline.signatures; const signatures = outline.signatures;
/** @type {MarkdownNode[]} */
const result = []; const result = [];
for (const clazz of outline.classesArray) { for (const clazz of outline.classesArray) {
// Iterate over classes, create header node. // Iterate over classes, create header node.
/** @type {MarkdownNode} */
const classNode = { type: 'h3', text: `class: ${clazz.name}` }; const classNode = { type: 'h3', text: `class: ${clazz.name}` };
const match = clazz.name.match(/(JS|CDP|[A-Z])(.*)/); const match = clazz.name.match(/(JS|CDP|[A-Z])(.*)/);
const varName = match[1].toLocaleLowerCase() + match[2]; const varName = match[1].toLocaleLowerCase() + match[2];
@ -81,10 +81,11 @@ async function run() {
text: `[${clazz.name}]: #class-${clazz.name.toLowerCase()} "${clazz.name}"` text: `[${clazz.name}]: #class-${clazz.name.toLowerCase()} "${clazz.name}"`
}); });
// Append class comments // Append class comments
classNode.children = (clazz.spec || []).map(c => clone(c)); classNode.children = (clazz.spec || []).map(c => md.clone(c));
for (const member of clazz.membersArray) { for (const member of clazz.membersArray) {
// Iterate members // Iterate members
/** @type {MarkdownNode} */
const memberNode = { type: 'h4', children: [] }; const memberNode = { type: 'h4', children: [] };
if (member.kind === 'event') { if (member.kind === 'event') {
memberNode.text = `${varName}.on('${member.name}')`; memberNode.text = `${varName}.on('${member.name}')`;
@ -112,7 +113,7 @@ async function run() {
} }
// Append member doc // Append member doc
memberNode.children.push(...(member.spec || []).map(c => clone(c))); memberNode.children.push(...(member.spec || []).map(c => md.clone(c)));
classNode.children.push(memberNode); classNode.children.push(memberNode);
} }
} }
@ -120,7 +121,7 @@ async function run() {
type: 'text', type: 'text',
text: links text: links
}); });
api.setText([comment, header, renderMd(result, 10000), footer].join('\n')); api.setText([comment, header, md.render(result), footer].join('\n'));
} }
} }
@ -139,8 +140,7 @@ async function run() {
errors.push(`WARN: updated ${source.projectPath()}`); errors.push(`WARN: updated ${source.projectPath()}`);
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src', 'client'), '', []); const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src', 'client'), '', []);
const missingDocs = require('./missingDocs.js'); errors.push(...missingDocs(outline, jsSources, path.join(PROJECT_DIR, 'src', 'client', 'api.ts')));
errors.push(...missingDocs(apiSpec, jsSources, path.join(PROJECT_DIR, 'src', 'client', 'api.ts')));
for (const source of mdSources) { for (const source of mdSources) {
if (!source.hasUpdatedText()) if (!source.hasUpdatedText())
@ -199,8 +199,9 @@ function renderProperty(name, type, spec) {
if (type.properties && type.properties.length) if (type.properties && type.properties.length)
children = type.properties.map(p => renderProperty(`\`${p.name}\``, p.type, p.spec)) children = type.properties.map(p => renderProperty(`\`${p.name}\``, p.type, p.spec))
else if (spec && spec.length > 1) else if (spec && spec.length > 1)
children = spec.slice(1).map(s => clone(s)); children = spec.slice(1).map(s => md.clone(s));
/** @type {MarkdownNode} */
const result = { const result = {
type: 'li', type: 'li',
liType: 'default', liType: 'default',

View file

@ -14,17 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
const fs = require('fs'); // @ts-check
const path = require('path'); const path = require('path');
const { parseMd, applyTemplates } = require('../parse_md'); const { MDOutline } = require('./MDBuilder');
const mdBuilder = require('./MDBuilder');
const PROJECT_DIR = path.join(__dirname, '..', '..'); const PROJECT_DIR = path.join(__dirname, '..', '..');
{ {
const apiBody = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-body.md')).toString()); const { documentation } = new MDOutline(path.join(PROJECT_DIR, 'docs-src', 'api-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
const apiParams = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-params.md')).toString());
const api = applyTemplates(apiBody, apiParams);
const { documentation } = mdBuilder(api, false);
const result = serialize(documentation); const result = serialize(documentation);
console.log(JSON.stringify(result)); console.log(JSON.stringify(result));
} }

View file

@ -15,18 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
const mdBuilder = require('./MDBuilder');
const ts = require('typescript'); const ts = require('typescript');
const EventEmitter = require('events'); const EventEmitter = require('events');
const Documentation = require('./Documentation'); const Documentation = require('./Documentation');
/** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
/** /**
* @return {!Array<string>} * @return {!Array<string>}
*/ */
module.exports = function lint(api, jsSources, apiFileName) { module.exports = function lint(outline, jsSources, apiFileName) {
const documentation = mdBuilder(api, true).documentation;
const apiMethods = listMethods(jsSources, apiFileName);
const errors = []; const errors = [];
const documentation = outline.documentation;
outline.copyDocsFromSuperclasses(errors);
const apiMethods = listMethods(jsSources, apiFileName);
for (const [className, methods] of apiMethods) { for (const [className, methods] of apiMethods) {
const docClass = documentation.classes.get(className); const docClass = documentation.classes.get(className);
if (!docClass) { if (!docClass) {
@ -75,11 +77,8 @@ module.exports = function lint(api, jsSources, apiFileName) {
*/ */
function paramsForMember(member) { function paramsForMember(member) {
if (member.kind !== 'method') if (member.kind !== 'method')
return []; return new Set();
const paramNames = new Set(member.argsArray.map(a => a.name)); return new Set(member.argsArray.map(a => a.name));
if (member.options)
paramNames.add('options');
return paramNames;
} }
/** /**
@ -124,10 +123,10 @@ function listMethods(sources, apiFileName) {
methods = new Map(); methods = new Map();
apiMethods.set(className, methods); apiMethods.set(className, methods);
} }
for (const [name, member] of classType.symbol.members || []) { for (const [name, member] of /** @type {any[]} */(classType.symbol.members || [])) {
if (name.startsWith('_') || name === 'T' || name === 'toString') if (name.startsWith('_') || name === 'T' || name === 'toString')
continue; continue;
if (EventEmitter.prototype.hasOwnProperty(name)) if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(name))
continue; continue;
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration); const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
const signature = signatureForType(memberType); const signature = signatureForType(memberType);
@ -149,7 +148,7 @@ function listMethods(sources, apiFileName) {
function visitMethods(node) { function visitMethods(node) {
if (ts.isExportSpecifier(node)) { if (ts.isExportSpecifier(node)) {
const className = node.name.text; const className = node.name.text;
const exportSymbol = node.name ? checker.getSymbolAtLocation(node.name) : node.symbol; const exportSymbol = node.name ? checker.getSymbolAtLocation(node.name) : /** @type {any} */ (node).symbol;
const classType = checker.getDeclaredTypeOfSymbol(exportSymbol); const classType = checker.getDeclaredTypeOfSymbol(exportSymbol);
if (!classType) if (!classType)
throw new Error(`Cannot parse class "${className}"`); throw new Error(`Cannot parse class "${className}"`);

View file

@ -20,25 +20,25 @@ const path = require('path');
const missingDocs = require('../missingDocs'); const missingDocs = require('../missingDocs');
const Source = require('../Source'); const Source = require('../Source');
const { folio } = require('folio'); const { folio } = require('folio');
const { parseMd } = require('../../parse_md'); const { MDOutline } = require('../MDBuilder');
const { test, expect } = folio; const { test, expect } = folio;
test('missing docs', async ({}) => { test('missing docs', async ({}) => {
const api = parseMd(fs.readFileSync(path.join(__dirname, 'test-api.md')).toString()); const outline = new MDOutline(path.join(__dirname, 'test-api.md'));
const tsSources = [ const tsSources = [
await Source.readFile(path.join(__dirname, 'test-api.ts')), await Source.readFile(path.join(__dirname, 'test-api.ts')),
await Source.readFile(path.join(__dirname, 'test-api-class.ts')), await Source.readFile(path.join(__dirname, 'test-api-class.ts')),
]; ];
const errors = missingDocs(api, tsSources, path.join(__dirname, 'test-api.ts')); const errors = missingDocs(outline, tsSources, path.join(__dirname, 'test-api.ts'));
expect(errors).toEqual([ expect(errors).toEqual([
'Missing documentation for "Exists.exists2.extra"', 'Missing documentation for "Exists.exists2.extra"',
'Missing documentation for "Exists.exists2.options"', 'Missing documentation for "Exists.exists2.options"',
'Missing documentation for "Exists.extra"', 'Missing documentation for "Exists.extra"',
'Missing documentation for "Extra"', 'Missing documentation for "Extra"',
'Documented "DoesNotExist" not found in sources',
'Documented "Exists.doesNotExist" not found is sources',
'Documented "Exists.exists.doesNotExist" not found is sources', 'Documented "Exists.exists.doesNotExist" not found is sources',
'Documented "Exists.exists.options" not found is sources', 'Documented "Exists.exists.options" not found is sources',
'Documented "Exists.doesNotExist" not found is sources',
'Documented "DoesNotExist" not found in sources',
]); ]);
}); });

View file

@ -1,5 +1,11 @@
# class: DoesNotExist
## method: DoesNotExist.doesNotExist
# class: Exists # class: Exists
## method: Exists.doesNotExist
## method: Exists.exists ## method: Exists.exists
### param: Exists.exists.exists ### param: Exists.exists.exists
@ -12,9 +18,3 @@
- `option` <[number]> - `option` <[number]>
## method: Exists.exists2 ## method: Exists.exists2
## method: Exists.doesNotExist
# class: DoesNotExist
## method: DoesNotExist.doesNotExist

View file

@ -22,7 +22,7 @@ const PROJECT_DIR = path.join(__dirname, '..', '..');
const fs = require('fs'); const fs = require('fs');
const {parseOverrides} = require('./parseOverrides'); const {parseOverrides} = require('./parseOverrides');
const exported = require('./exported.json'); const exported = require('./exported.json');
const { parseMd, applyTemplates } = require('../parse_md'); const { MDOutline } = require('../doclint/MDBuilder');
const objectDefinitions = []; const objectDefinitions = [];
const handledMethods = new Set(); const handledMethods = new Set();
@ -36,11 +36,9 @@ let hadChanges = false;
fs.mkdirSync(typesDir) 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, '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')); writeFile(path.join(typesDir, 'trace.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'trace', 'traceTypes.ts'), 'utf8'));
const apiBody = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-body.md')).toString()); const outline = new MDOutline(path.join(PROJECT_DIR, 'docs-src', 'api-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
const apiParams = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-params.md')).toString()); outline.copyDocsFromSuperclasses([]);
const api = applyTemplates(apiBody, apiParams); documentation = outline.documentation;
const mdResult = require('../doclint/MDBuilder')(api, true);
documentation = mdResult.documentation;
// Root module types are overridden. // Root module types are overridden.
const playwrightClass = documentation.classes.get('Playwright'); const playwrightClass = documentation.classes.get('Playwright');

View file

@ -14,6 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
// @ts-check
/** @typedef {{
* type: 'text' | 'li' | 'code' | 'gen' | 'h0' | 'h1' | 'h2' | 'h3' | 'h4',
* text?: string,
* codeLang?: string,
* lines?: string[],
* liType?: 'default' | 'bullet' | 'ordinal',
* children?: MarkdownNode[]
* }} MarkdownNode */
function normalizeLines(content) { function normalizeLines(content) {
const inLines = content.replace(/\r\n/g, '\n').split('\n'); const inLines = content.replace(/\r\n/g, '\n').split('\n');
let inCodeBlock = false; let inCodeBlock = false;
@ -47,19 +58,26 @@ function normalizeLines(content) {
return outLines; return outLines;
} }
/**
* @param {string[]} lines
*/
function buildTree(lines) { function buildTree(lines) {
/** @type {MarkdownNode} */
const root = { const root = {
type: 'h0', type: 'h0',
value: '<root>', text: '<root>',
children: [] children: []
}; };
/** @type {MarkdownNode[]} */
const stack = [root]; const stack = [root];
/** @type {MarkdownNode[]} */
let liStack = null; let liStack = null;
for (let i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
let line = lines[i]; let line = lines[i];
if (line.startsWith('```')) { if (line.startsWith('```')) {
/** @type {MarkdownNode} */
const node = { const node = {
type: 'code', type: 'code',
lines: [], lines: [],
@ -75,6 +93,7 @@ function buildTree(lines) {
} }
if (line.startsWith('<!-- GEN')) { if (line.startsWith('<!-- GEN')) {
/** @type {MarkdownNode} */
const node = { const node = {
type: 'gen', type: 'gen',
lines: [line] lines: [line]
@ -91,10 +110,8 @@ function buildTree(lines) {
const header = line.match(/^(#+)/); const header = line.match(/^(#+)/);
if (header) { if (header) {
const node = { children: [] };
const h = header[1].length; const h = header[1].length;
node.type = 'h' + h; const node = /** @type {MarkdownNode} */({ type: 'h' + h, text: line.substring(h + 1), children: [] });
node.text = line.substring(h + 1);
while (true) { while (true) {
const lastH = +stack[0].type.substring(1); const lastH = +stack[0].type.substring(1);
@ -111,7 +128,7 @@ function buildTree(lines) {
const list = line.match(/^(\s*)(-|1.|\*) /); const list = line.match(/^(\s*)(-|1.|\*) /);
const depth = list ? (list[1].length / 2) : 0; const depth = list ? (list[1].length / 2) : 0;
const node = {}; const node = /** @type {MarkdownNode} */({ type: 'text', text: line });
if (list) { if (list) {
node.type = 'li'; node.type = 'li';
node.text = line.substring(list[0].length); node.text = line.substring(list[0].length);
@ -121,9 +138,6 @@ function buildTree(lines) {
node.liType = 'bullet'; node.liType = 'bullet';
else else
node.liType = 'default'; node.liType = 'default';
} else {
node.type = 'text';
node.text = line;
} }
if (!liStack[depth].children) if (!liStack[depth].children)
liStack[depth].children = []; liStack[depth].children = [];
@ -133,21 +147,32 @@ function buildTree(lines) {
return root.children; return root.children;
} }
function parseMd(content) { /**
* @param {string} content
*/
function parse(content) {
return buildTree(normalizeLines(content)); return buildTree(normalizeLines(content));
} }
function renderMd(nodes, maxColumns) { /**
* @param {MarkdownNode[]} nodes
*/
function render(nodes) {
const result = []; const result = [];
let lastNode; let lastNode;
for (let node of nodes) { for (let node of nodes) {
innerRenderMdNode(node, lastNode, result, maxColumns); innerRenderMdNode(node, lastNode, result);
lastNode = node; lastNode = node;
} }
return result.join('\n'); return result.join('\n');
} }
function innerRenderMdNode(node, lastNode, result, maxColumns = 120) { /**
* @param {MarkdownNode} node
* @param {MarkdownNode} lastNode
* @param {string[]} result
*/
function innerRenderMdNode(node, lastNode, result) {
const newLine = () => { const newLine = () => {
if (result[result.length - 1] !== '') if (result[result.length - 1] !== '')
result.push(''); result.push('');
@ -155,11 +180,11 @@ function innerRenderMdNode(node, lastNode, result, maxColumns = 120) {
if (node.type.startsWith('h')) { if (node.type.startsWith('h')) {
newLine(); newLine();
const depth = node.type.substring(1); const depth = +node.type.substring(1);
result.push(`${'#'.repeat(depth)} ${node.text}`); result.push(`${'#'.repeat(depth)} ${node.text}`);
let lastNode = node; let lastNode = node;
for (const child of node.children || []) { for (const child of node.children || []) {
innerRenderMdNode(child, lastNode, result, maxColumns); innerRenderMdNode(child, lastNode, result);
lastNode = child; lastNode = child;
} }
} }
@ -168,7 +193,7 @@ function innerRenderMdNode(node, lastNode, result, maxColumns = 120) {
const bothComments = node.text.startsWith('>') && lastNode && lastNode.type === 'text' && lastNode.text.startsWith('>'); const bothComments = node.text.startsWith('>') && lastNode && lastNode.type === 'text' && lastNode.text.startsWith('>');
if (!bothComments && lastNode && lastNode.text) if (!bothComments && lastNode && lastNode.text)
newLine(); newLine();
printText(node, result, maxColumns); result.push(node.text);
} }
if (node.type === 'code') { if (node.type === 'code') {
@ -203,97 +228,32 @@ function innerRenderMdNode(node, lastNode, result, maxColumns = 120) {
} }
} }
function printText(node, result, maxColumns) { /**
let line = node.text; * @param {MarkdownNode} node
while (line.length > maxColumns) { */
let index = line.lastIndexOf(' ', maxColumns);
if (index === -1) {
index = line.indexOf(' ', maxColumns);
if (index === -1)
break;
}
result.push(line.substring(0, index));
line = line.substring(index + 1);
}
if (line.length)
result.push(line);
}
function clone(node) { function clone(node) {
const copy = { ...node }; const copy = { ...node };
copy.children = copy.children ? copy.children.map(c => clone(c)) : undefined; copy.children = copy.children ? copy.children.map(c => clone(c)) : undefined;
return copy; return copy;
} }
function applyTemplates(body, params) { /**
const paramsMap = new Map(); * @param {MarkdownNode[]} nodes
for (const node of params) * @param {function(MarkdownNode): void} visitor
paramsMap.set('%%-' + node.text + '-%%', node); */
function visitAll(nodes, visitor) {
const visit = (node, parent) => { for (const node of nodes)
if (node.text && node.text.includes('-inline- = %%')) { visit(node, visitor);
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 => 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 => 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 * @param {MarkdownNode} node
* @returns {{ name: string, type: string, text: string }} * @param {function(MarkdownNode): void} visitor
*/ */
function parseArgument(line) { function visit(node, visitor) {
let match = line.match(/^`([^`]+)` (.*)/); visitor(node);
if (!match) for (const n of node.children || [])
match = line.match(/^(returns): (.*)/); visit(n, visitor);
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 = { parseMd, renderMd, parseArgument, applyTemplates, clone }; module.exports = { parse, render, clone, visitAll, visit };