doc: generator code health (#4840)
This commit is contained in:
parent
a1232b6980
commit
70c14e6b99
|
|
@ -16,14 +16,7 @@
|
|||
|
||||
// @ts-check
|
||||
|
||||
/** @typedef {{
|
||||
* type: 'text' | 'li' | 'code' | 'gen' | 'h1' | 'h2' | 'h3' | 'h4',
|
||||
* text?: string,
|
||||
* codeLang?: string,
|
||||
* lines?: string[],
|
||||
* liType?: 'default' | 'bullet' | 'ordinal',
|
||||
* children?: MarkdownNode[]
|
||||
* }} MarkdownNode */
|
||||
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
class Documentation {
|
||||
/**
|
||||
|
|
@ -64,10 +57,6 @@ Documentation.Class = class {
|
|||
/** @type {!Array<!Documentation.Member>} */
|
||||
this.propertiesArray = [];
|
||||
/** @type {!Map<string, !Documentation.Member>} */
|
||||
this.options = new Map();
|
||||
/** @type {!Array<!Documentation.Member>} */
|
||||
this.optionsArray = [];
|
||||
/** @type {!Map<string, !Documentation.Member>} */
|
||||
this.methods = new Map();
|
||||
/** @type {!Array<!Documentation.Member>} */
|
||||
this.methodsArray = [];
|
||||
|
|
@ -135,6 +124,9 @@ Documentation.Class = class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(Documentation.Member|Documentation.Class): void} visitor
|
||||
*/
|
||||
visit(visitor) {
|
||||
visitor(this);
|
||||
for (const p of this.propertiesArray)
|
||||
|
|
@ -204,6 +196,9 @@ Documentation.Member = class {
|
|||
return new Documentation.Member('event', name, type, [], spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(Documentation.Member|Documentation.Class): void} visitor
|
||||
*/
|
||||
visit(visitor) {
|
||||
visitor(this);
|
||||
if (this.type)
|
||||
|
|
|
|||
|
|
@ -16,17 +16,22 @@
|
|||
|
||||
// @ts-check
|
||||
|
||||
const { parseArgument, renderMd, clone } = require('../parse_md');
|
||||
const fs = require('fs');
|
||||
const md = require('../markdown');
|
||||
const Documentation = require('./Documentation');
|
||||
|
||||
/** @typedef {import('./Documentation').MarkdownNode} MarkdownNode */
|
||||
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
class MDOutline {
|
||||
/**
|
||||
* @param {MarkdownNode[]} api
|
||||
* @param {string} bodyPath
|
||||
* @param {string=} paramsPath
|
||||
* @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.classes = /** @type {Map<string, Documentation.Class>} */ new Map();
|
||||
for (const clazz of api) {
|
||||
|
|
@ -42,8 +47,32 @@ class MDOutline {
|
|||
linksMap.set(new RegExp('\\[' + match[1] + '\\]', 'g'), { href: match[2], label: match[3] });
|
||||
}
|
||||
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
|
||||
*/
|
||||
|
|
@ -81,7 +110,7 @@ class MDOutline {
|
|||
}
|
||||
|
||||
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)
|
||||
clazz.visit(item => item.comment = renderCommentsForSourceCode(item.spec, linksMap));
|
||||
return signatures;
|
||||
|
|
@ -120,8 +149,8 @@ function extractComments(item) {
|
|||
* @param {Map<string, { href: string, label: string}>} 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 visit = node => {
|
||||
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})`);
|
||||
|
|
@ -133,27 +162,21 @@ function renderCommentsForSourceCode(spec, linksMap) {
|
|||
}
|
||||
if (node.liType === 'bullet')
|
||||
node.liType = 'default';
|
||||
for (const child of node.children || [])
|
||||
visit(child);
|
||||
};
|
||||
for (const node of comments)
|
||||
visit(node);
|
||||
return renderMd(comments, 10000);
|
||||
|
||||
// [`frame.waitForFunction(pageFunction[, arg, options])`](#framewaitforfunctionpagefunction-arg-options)
|
||||
});
|
||||
return md.render(comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode[]} spec
|
||||
* @param {Map<string, string>} [signatures]
|
||||
*/
|
||||
function patchSignatures(spec, signatures) {
|
||||
function patchLinks(spec, signatures) {
|
||||
for (const node of spec || []) {
|
||||
if (node.type === 'text')
|
||||
node.text = patchSignaturesInText(node.text, signatures);
|
||||
node.text = patchLinksInText(node.text, signatures);
|
||||
if (node.type === 'li') {
|
||||
node.text = patchSignaturesInText(node.text, signatures);
|
||||
patchSignatures(node.children, signatures);
|
||||
node.text = patchLinksInText(node.text, signatures);
|
||||
patchLinks(node.children, signatures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +194,7 @@ function createLink(text) {
|
|||
* @param {string} comment
|
||||
* @param {Map<string, string>} signatures
|
||||
*/
|
||||
function patchSignaturesInText(comment, 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) => {
|
||||
|
|
@ -291,36 +314,79 @@ function guessRequired(comment) {
|
|||
return required;
|
||||
}
|
||||
|
||||
module.exports =
|
||||
/**
|
||||
* @param {any} api
|
||||
* @param {boolean=} copyDocsFromSuperClasses
|
||||
* @param {MarkdownNode[]} body
|
||||
* @param {MarkdownNode[]} params
|
||||
*/
|
||||
function(api, copyDocsFromSuperClasses = false, links = '') {
|
||||
const errors = [];
|
||||
const outline = new MDOutline(api, links);
|
||||
const documentation = new Documentation(outline.classesArray);
|
||||
function applyTemplates(body, params) {
|
||||
const paramsMap = new Map();
|
||||
for (const node of params)
|
||||
paramsMap.set('%%-' + node.text + '-%%', node);
|
||||
|
||||
if (copyDocsFromSuperClasses) {
|
||||
// Push base class documentation to derived classes.
|
||||
for (const [name, clazz] of documentation.classes.entries()) {
|
||||
clazz.validateOrder(errors, clazz);
|
||||
|
||||
if (!clazz.extends || clazz.extends === 'EventEmitter' || clazz.extends === 'Error')
|
||||
continue;
|
||||
const superClass = documentation.classes.get(clazz.extends);
|
||||
if (!superClass) {
|
||||
errors.push(`Undefined superclass: ${superClass} in ${name}`);
|
||||
continue;
|
||||
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))
|
||||
});
|
||||
}
|
||||
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();
|
||||
} 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) };
|
||||
}
|
||||
return { documentation, errors, outline };
|
||||
};
|
||||
throw new Error('Should not be reached');
|
||||
}
|
||||
|
||||
module.exports = { MDOutline };
|
||||
|
|
|
|||
|
|
@ -21,13 +21,14 @@ const playwright = require('../../');
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Source = require('./Source');
|
||||
const { parseMd, renderMd, applyTemplates, clone } = require('./../parse_md');
|
||||
const md = require('../markdown');
|
||||
const { spawnSync } = require('child_process');
|
||||
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('../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
const VERSION = require(path.join(PROJECT_DIR, 'package.json')).version;
|
||||
|
|
@ -56,21 +57,20 @@ async function run() {
|
|||
let changedFiles = false;
|
||||
|
||||
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 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 apiSpec = applyTemplates(parseMd(body), parseMd(params));
|
||||
const outline = new MDOutline(path.join(PROJECT_DIR, 'docs-src', 'api-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
|
||||
|
||||
// Produce api.md
|
||||
{
|
||||
const comment = '<!-- THIS FILE IS NOW GENERATED -->';
|
||||
{
|
||||
const { outline } = mdBuilder(apiSpec, false);
|
||||
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];
|
||||
|
|
@ -81,10 +81,11 @@ async function run() {
|
|||
text: `[${clazz.name}]: #class-${clazz.name.toLowerCase()} "${clazz.name}"`
|
||||
});
|
||||
// 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) {
|
||||
// Iterate members
|
||||
/** @type {MarkdownNode} */
|
||||
const memberNode = { type: 'h4', children: [] };
|
||||
if (member.kind === 'event') {
|
||||
memberNode.text = `${varName}.on('${member.name}')`;
|
||||
|
|
@ -112,7 +113,7 @@ async function run() {
|
|||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +121,7 @@ async function run() {
|
|||
type: 'text',
|
||||
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()}`);
|
||||
|
||||
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src', 'client'), '', []);
|
||||
const missingDocs = require('./missingDocs.js');
|
||||
errors.push(...missingDocs(apiSpec, jsSources, path.join(PROJECT_DIR, 'src', 'client', 'api.ts')));
|
||||
errors.push(...missingDocs(outline, jsSources, path.join(PROJECT_DIR, 'src', 'client', 'api.ts')));
|
||||
|
||||
for (const source of mdSources) {
|
||||
if (!source.hasUpdatedText())
|
||||
|
|
@ -199,8 +199,9 @@ function renderProperty(name, type, spec) {
|
|||
if (type.properties && type.properties.length)
|
||||
children = type.properties.map(p => renderProperty(`\`${p.name}\``, p.type, p.spec))
|
||||
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 = {
|
||||
type: 'li',
|
||||
liType: 'default',
|
||||
|
|
|
|||
|
|
@ -14,17 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
// @ts-check
|
||||
|
||||
const path = require('path');
|
||||
const { parseMd, applyTemplates } = require('../parse_md');
|
||||
const mdBuilder = require('./MDBuilder');
|
||||
const { MDOutline } = require('./MDBuilder');
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
|
||||
{
|
||||
const apiBody = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-body.md')).toString());
|
||||
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 { documentation } = new MDOutline(path.join(PROJECT_DIR, 'docs-src', 'api-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
|
||||
const result = serialize(documentation);
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,18 +15,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const mdBuilder = require('./MDBuilder');
|
||||
const ts = require('typescript');
|
||||
const EventEmitter = require('events');
|
||||
const Documentation = require('./Documentation');
|
||||
|
||||
/** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
/**
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
module.exports = function lint(api, jsSources, apiFileName) {
|
||||
const documentation = mdBuilder(api, true).documentation;
|
||||
const apiMethods = listMethods(jsSources, apiFileName);
|
||||
module.exports = function lint(outline, jsSources, apiFileName) {
|
||||
const errors = [];
|
||||
const documentation = outline.documentation;
|
||||
outline.copyDocsFromSuperclasses(errors);
|
||||
const apiMethods = listMethods(jsSources, apiFileName);
|
||||
for (const [className, methods] of apiMethods) {
|
||||
const docClass = documentation.classes.get(className);
|
||||
if (!docClass) {
|
||||
|
|
@ -75,11 +77,8 @@ module.exports = function lint(api, jsSources, apiFileName) {
|
|||
*/
|
||||
function paramsForMember(member) {
|
||||
if (member.kind !== 'method')
|
||||
return [];
|
||||
const paramNames = new Set(member.argsArray.map(a => a.name));
|
||||
if (member.options)
|
||||
paramNames.add('options');
|
||||
return paramNames;
|
||||
return new Set();
|
||||
return new Set(member.argsArray.map(a => a.name));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,10 +123,10 @@ function listMethods(sources, apiFileName) {
|
|||
methods = new Map();
|
||||
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')
|
||||
continue;
|
||||
if (EventEmitter.prototype.hasOwnProperty(name))
|
||||
if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(name))
|
||||
continue;
|
||||
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
||||
const signature = signatureForType(memberType);
|
||||
|
|
@ -149,7 +148,7 @@ function listMethods(sources, apiFileName) {
|
|||
function visitMethods(node) {
|
||||
if (ts.isExportSpecifier(node)) {
|
||||
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);
|
||||
if (!classType)
|
||||
throw new Error(`Cannot parse class "${className}"`);
|
||||
|
|
|
|||
|
|
@ -20,25 +20,25 @@ const path = require('path');
|
|||
const missingDocs = require('../missingDocs');
|
||||
const Source = require('../Source');
|
||||
const { folio } = require('folio');
|
||||
const { parseMd } = require('../../parse_md');
|
||||
const { MDOutline } = require('../MDBuilder');
|
||||
|
||||
const { test, expect } = folio;
|
||||
|
||||
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 = [
|
||||
await Source.readFile(path.join(__dirname, 'test-api.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([
|
||||
'Missing documentation for "Exists.exists2.extra"',
|
||||
'Missing documentation for "Exists.exists2.options"',
|
||||
'Missing documentation for "Exists.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.options" not found is sources',
|
||||
'Documented "Exists.doesNotExist" not found is sources',
|
||||
'Documented "DoesNotExist" not found in sources',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# class: DoesNotExist
|
||||
|
||||
## method: DoesNotExist.doesNotExist
|
||||
|
||||
# class: Exists
|
||||
|
||||
## method: Exists.doesNotExist
|
||||
|
||||
## method: Exists.exists
|
||||
|
||||
### param: Exists.exists.exists
|
||||
|
|
@ -12,9 +18,3 @@
|
|||
- `option` <[number]>
|
||||
|
||||
## method: Exists.exists2
|
||||
|
||||
## method: Exists.doesNotExist
|
||||
|
||||
# class: DoesNotExist
|
||||
|
||||
## method: DoesNotExist.doesNotExist
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const PROJECT_DIR = path.join(__dirname, '..', '..');
|
|||
const fs = require('fs');
|
||||
const {parseOverrides} = require('./parseOverrides');
|
||||
const exported = require('./exported.json');
|
||||
const { parseMd, applyTemplates } = require('../parse_md');
|
||||
const { MDOutline } = require('../doclint/MDBuilder');
|
||||
|
||||
const objectDefinitions = [];
|
||||
const handledMethods = new Set();
|
||||
|
|
@ -36,11 +36,9 @@ 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 apiBody = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-body.md')).toString());
|
||||
const apiParams = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-params.md')).toString());
|
||||
const api = applyTemplates(apiBody, apiParams);
|
||||
const mdResult = require('../doclint/MDBuilder')(api, true);
|
||||
documentation = mdResult.documentation;
|
||||
const outline = new MDOutline(path.join(PROJECT_DIR, 'docs-src', 'api-body.md'), path.join(PROJECT_DIR, 'docs-src', 'api-params.md'));
|
||||
outline.copyDocsFromSuperclasses([]);
|
||||
documentation = outline.documentation;
|
||||
|
||||
// Root module types are overridden.
|
||||
const playwrightClass = documentation.classes.get('Playwright');
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@
|
|||
* 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) {
|
||||
const inLines = content.replace(/\r\n/g, '\n').split('\n');
|
||||
let inCodeBlock = false;
|
||||
|
|
@ -47,19 +58,26 @@ function normalizeLines(content) {
|
|||
return outLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} lines
|
||||
*/
|
||||
function buildTree(lines) {
|
||||
/** @type {MarkdownNode} */
|
||||
const root = {
|
||||
type: 'h0',
|
||||
value: '<root>',
|
||||
text: '<root>',
|
||||
children: []
|
||||
};
|
||||
/** @type {MarkdownNode[]} */
|
||||
const stack = [root];
|
||||
/** @type {MarkdownNode[]} */
|
||||
let liStack = null;
|
||||
|
||||
for (let i = 0; i < lines.length; ++i) {
|
||||
let line = lines[i];
|
||||
|
||||
if (line.startsWith('```')) {
|
||||
/** @type {MarkdownNode} */
|
||||
const node = {
|
||||
type: 'code',
|
||||
lines: [],
|
||||
|
|
@ -75,6 +93,7 @@ function buildTree(lines) {
|
|||
}
|
||||
|
||||
if (line.startsWith('<!-- GEN')) {
|
||||
/** @type {MarkdownNode} */
|
||||
const node = {
|
||||
type: 'gen',
|
||||
lines: [line]
|
||||
|
|
@ -91,10 +110,8 @@ function buildTree(lines) {
|
|||
|
||||
const header = line.match(/^(#+)/);
|
||||
if (header) {
|
||||
const node = { children: [] };
|
||||
const h = header[1].length;
|
||||
node.type = 'h' + h;
|
||||
node.text = line.substring(h + 1);
|
||||
const node = /** @type {MarkdownNode} */({ type: 'h' + h, text: line.substring(h + 1), children: [] });
|
||||
|
||||
while (true) {
|
||||
const lastH = +stack[0].type.substring(1);
|
||||
|
|
@ -111,7 +128,7 @@ function buildTree(lines) {
|
|||
|
||||
const list = line.match(/^(\s*)(-|1.|\*) /);
|
||||
const depth = list ? (list[1].length / 2) : 0;
|
||||
const node = {};
|
||||
const node = /** @type {MarkdownNode} */({ type: 'text', text: line });
|
||||
if (list) {
|
||||
node.type = 'li';
|
||||
node.text = line.substring(list[0].length);
|
||||
|
|
@ -121,9 +138,6 @@ function buildTree(lines) {
|
|||
node.liType = 'bullet';
|
||||
else
|
||||
node.liType = 'default';
|
||||
} else {
|
||||
node.type = 'text';
|
||||
node.text = line;
|
||||
}
|
||||
if (!liStack[depth].children)
|
||||
liStack[depth].children = [];
|
||||
|
|
@ -133,21 +147,32 @@ function buildTree(lines) {
|
|||
return root.children;
|
||||
}
|
||||
|
||||
function parseMd(content) {
|
||||
/**
|
||||
* @param {string} content
|
||||
*/
|
||||
function parse(content) {
|
||||
return buildTree(normalizeLines(content));
|
||||
}
|
||||
|
||||
function renderMd(nodes, maxColumns) {
|
||||
/**
|
||||
* @param {MarkdownNode[]} nodes
|
||||
*/
|
||||
function render(nodes) {
|
||||
const result = [];
|
||||
let lastNode;
|
||||
for (let node of nodes) {
|
||||
innerRenderMdNode(node, lastNode, result, maxColumns);
|
||||
innerRenderMdNode(node, lastNode, result);
|
||||
lastNode = node;
|
||||
}
|
||||
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 = () => {
|
||||
if (result[result.length - 1] !== '')
|
||||
result.push('');
|
||||
|
|
@ -155,11 +180,11 @@ function innerRenderMdNode(node, lastNode, result, maxColumns = 120) {
|
|||
|
||||
if (node.type.startsWith('h')) {
|
||||
newLine();
|
||||
const depth = node.type.substring(1);
|
||||
const depth = +node.type.substring(1);
|
||||
result.push(`${'#'.repeat(depth)} ${node.text}`);
|
||||
let lastNode = node;
|
||||
for (const child of node.children || []) {
|
||||
innerRenderMdNode(child, lastNode, result, maxColumns);
|
||||
innerRenderMdNode(child, lastNode, result);
|
||||
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('>');
|
||||
if (!bothComments && lastNode && lastNode.text)
|
||||
newLine();
|
||||
printText(node, result, maxColumns);
|
||||
result.push(node.text);
|
||||
}
|
||||
|
||||
if (node.type === 'code') {
|
||||
|
|
@ -203,97 +228,32 @@ function innerRenderMdNode(node, lastNode, result, maxColumns = 120) {
|
|||
}
|
||||
}
|
||||
|
||||
function printText(node, result, maxColumns) {
|
||||
let line = node.text;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode} node
|
||||
*/
|
||||
function clone(node) {
|
||||
const copy = { ...node };
|
||||
copy.children = copy.children ? copy.children.map(c => clone(c)) : undefined;
|
||||
return copy;
|
||||
}
|
||||
|
||||
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 => 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 {MarkdownNode[]} nodes
|
||||
* @param {function(MarkdownNode): void} visitor
|
||||
*/
|
||||
function visitAll(nodes, visitor) {
|
||||
for (const node of nodes)
|
||||
visit(node, visitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @returns {{ name: string, type: string, text: string }}
|
||||
* @param {MarkdownNode} node
|
||||
* @param {function(MarkdownNode): void} visitor
|
||||
*/
|
||||
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');
|
||||
function visit(node, visitor) {
|
||||
visitor(node);
|
||||
for (const n of node.children || [])
|
||||
visit(n, visitor);
|
||||
}
|
||||
|
||||
module.exports = { parseMd, renderMd, parseArgument, applyTemplates, clone };
|
||||
module.exports = { parse, render, clone, visitAll, visit };
|
||||
Loading…
Reference in a new issue