chore: move doc-specific code into documentation (#18933)

This commit is contained in:
Pavel Feldman 2022-11-19 11:26:11 -08:00 committed by GitHub
parent 941090f0c4
commit 03d2b2ecbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 198 additions and 194 deletions

View File

@ -19,7 +19,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const md = require('../markdown'); const md = require('../markdown');
const Documentation = require('./documentation'); const docs = require('./documentation');
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */ /** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
/** @typedef {import('../markdown').MarkdownHeaderNode} MarkdownHeaderNode */ /** @typedef {import('../markdown').MarkdownHeaderNode} MarkdownHeaderNode */
@ -45,7 +45,7 @@ class ApiParser {
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : undefined; const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : undefined;
checkNoDuplicateParamEntries(params); checkNoDuplicateParamEntries(params);
const api = params ? applyTemplates(body, params) : body; const api = params ? applyTemplates(body, params) : body;
/** @type {Map<string, Documentation.Class>} */ /** @type {Map<string, documentation.Class>} */
this.classes = new Map(); this.classes = new Map();
md.visitAll(api, node => { md.visitAll(api, node => {
if (node.type === 'h1') if (node.type === 'h1')
@ -59,7 +59,7 @@ class ApiParser {
if (node.type === 'h3') if (node.type === 'h3')
this.parseArgument(node); this.parseArgument(node);
}); });
this.documentation = new Documentation([...this.classes.values()]); this.documentation = new docs.Documentation([...this.classes.values()]);
this.documentation.index(); this.documentation.index();
} }
@ -77,7 +77,7 @@ class ApiParser {
continue; continue;
} }
} }
const clazz = new Documentation.Class(extractMetainfo(node), name, [], extendsName, extractComments(node)); const clazz = new docs.Class(extractMetainfo(node), name, [], extendsName, extractComments(node));
this.classes.set(clazz.name, clazz); this.classes.set(clazz.name, clazz);
} }
@ -100,16 +100,16 @@ class ApiParser {
} }
} }
if (!returnType) if (!returnType)
returnType = new Documentation.Type('void'); returnType = new docs.Type('void');
const comments = extractComments(spec); const comments = extractComments(spec);
let member; let member;
if (match[1] === 'event') if (match[1] === 'event')
member = Documentation.Member.createEvent(extractMetainfo(spec), name, returnType, comments); member = docs.Member.createEvent(extractMetainfo(spec), name, returnType, comments);
if (match[1] === 'property') if (match[1] === 'property')
member = Documentation.Member.createProperty(extractMetainfo(spec), name, returnType, comments, !optional); member = docs.Member.createProperty(extractMetainfo(spec), name, returnType, comments, !optional);
if (['method', 'async method', 'optional method', 'optional async method'].includes(match[1])) { if (['method', 'async method', 'optional method', 'optional async method'].includes(match[1])) {
member = Documentation.Member.createMethod(extractMetainfo(spec), name, [], returnType, comments); member = docs.Member.createMethod(extractMetainfo(spec), name, [], returnType, comments);
if (match[1].includes('async')) if (match[1].includes('async'))
member.async = true; member.async = true;
if (match[1].includes('optional')) if (match[1].includes('optional'))
@ -118,7 +118,7 @@ class ApiParser {
if (!member) if (!member)
throw new Error('Unknown member: ' + spec.text); throw new Error('Unknown member: ' + spec.text);
const clazz = /** @type {Documentation.Class} */(this.classes.get(match[2])); const clazz = /** @type {documentation.Class} */(this.classes.get(match[2]));
const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind); const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind);
if (existingMember && isTypeOverride(existingMember, member)) { if (existingMember && isTypeOverride(existingMember, member)) {
for (const lang of member?.langs?.only || []) { for (const lang of member?.langs?.only || []) {
@ -173,8 +173,8 @@ class ApiParser {
// match[1] === 'option' // match[1] === 'option'
let options = method.argsArray.find(o => o.name === 'options'); let options = method.argsArray.find(o => o.name === 'options');
if (!options) { if (!options) {
const type = new Documentation.Type('Object', []); const type = new docs.Type('Object', []);
options = Documentation.Member.createProperty({ langs: {}, experimental: false, since: 'v1.0' }, 'options', type, undefined, false); options = docs.Member.createProperty({ langs: {}, experimental: false, since: 'v1.0' }, 'options', type, undefined, false);
method.argsArray.push(options); method.argsArray.push(options);
} }
const p = this.parseProperty(spec); const p = this.parseProperty(spec);
@ -196,12 +196,12 @@ class ApiParser {
const name = text.substring(0, typeStart).replace(/\`/g, '').trim(); const name = text.substring(0, typeStart).replace(/\`/g, '').trim();
const comments = extractComments(spec); const comments = extractComments(spec);
const { type, optional } = this.parseType(/** @type {MarkdownLiNode} */(param)); const { type, optional } = this.parseType(/** @type {MarkdownLiNode} */(param));
return Documentation.Member.createProperty(extractMetainfo(spec), name, type, comments, !optional); return docs.Member.createProperty(extractMetainfo(spec), name, type, comments, !optional);
} }
/** /**
* @param {MarkdownLiNode} spec * @param {MarkdownLiNode} spec
* @return {{ type: Documentation.Type, optional: boolean, experimental: boolean }} * @return {{ type: documentation.Type, optional: boolean, experimental: boolean }}
*/ */
parseType(spec) { parseType(spec) {
const arg = parseVariable(spec.text); const arg = parseVariable(spec.text);
@ -210,9 +210,9 @@ class ApiParser {
const { name, text } = parseVariable(/** @type {string} */(child.text)); const { name, text } = parseVariable(/** @type {string} */(child.text));
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]); const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
const childType = this.parseType(child); const childType = this.parseType(child);
properties.push(Documentation.Member.createProperty({ langs: {}, experimental: childType.experimental, since: 'v1.0' }, name, childType.type, comments, !childType.optional)); properties.push(docs.Member.createProperty({ langs: {}, experimental: childType.experimental, since: 'v1.0' }, name, childType.type, comments, !childType.optional));
} }
const type = Documentation.Type.parse(arg.type, properties); const type = docs.Type.parse(arg.type, properties);
return { type, optional: arg.optional, experimental: arg.experimental }; return { type, optional: arg.optional, experimental: arg.experimental };
} }
} }
@ -415,8 +415,8 @@ function childrenWithoutProperties(spec) {
} }
/** /**
* @param {Documentation.Member} existingMember * @param {documentation.Member} existingMember
* @param {Documentation.Member} member * @param {documentation.Member} member
* @returns {boolean} * @returns {boolean}
*/ */
function isTypeOverride(existingMember, member) { function isTypeOverride(existingMember, member) {

View File

@ -23,6 +23,7 @@ const path = require('path');
const { parseApi } = require('./api_parser'); const { parseApi } = require('./api_parser');
const missingDocs = require('./missingDocs'); const missingDocs = require('./missingDocs');
const md = require('../markdown'); const md = require('../markdown');
const docs = require('./documentation');
/** @typedef {import('./documentation').Type} Type */ /** @typedef {import('./documentation').Type} Type */
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */ /** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
@ -189,7 +190,7 @@ async function run() {
const data = fs.readFileSync(filePath, 'utf-8'); const data = fs.readFileSync(filePath, 'utf-8');
let rootNode = md.filterNodesForLanguage(md.parse(data), lang); let rootNode = md.filterNodesForLanguage(md.parse(data), lang);
// Validates code snippet groups. // Validates code snippet groups.
rootNode = md.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec)); rootNode = docs.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec));
// Renders links. // Renders links.
documentation.renderLinksInText(rootNode); documentation.renderLinksInText(rootNode);
// Validate links. // Validate links.

View File

@ -36,15 +36,15 @@ const md = require('../markdown');
* @typedef {{ * @typedef {{
* only?: string[], * only?: string[],
* aliases?: Object<string, string>, * aliases?: Object<string, string>,
* types?: Object<string, Documentation.Type>, * types?: Object<string, Type>,
* overrides?: Object<string, Documentation.Member>, * overrides?: Object<string, Member>,
* }} Langs * }} Langs
*/ */
/** /**
* @typedef {function({ * @typedef {function({
* clazz?: Documentation.Class, * clazz?: Class,
* member?: Documentation.Member, * member?: Member,
* param?: string, * param?: string,
* option?: string, * option?: string,
* href?: string, * href?: string,
@ -65,13 +65,19 @@ const md = require('../markdown');
* }} LanguageOptions * }} LanguageOptions
*/ */
/** @typedef {{
* value: string, groupId: string, spec: MarkdownNode
* }} CodeGroup */
/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */
class Documentation { class Documentation {
/** /**
* @param {!Array<!Documentation.Class>} classesArray * @param {!Array<!Class>} classesArray
*/ */
constructor(classesArray) { constructor(classesArray) {
this.classesArray = classesArray; this.classesArray = classesArray;
/** @type {!Map<string, !Documentation.Class>} */ /** @type {!Map<string, !Class>} */
this.classes = new Map(); this.classes = new Map();
this.index(); this.index();
} }
@ -147,7 +153,7 @@ class Documentation {
* @param {Renderer} linkRenderer * @param {Renderer} linkRenderer
*/ */
setLinkRenderer(linkRenderer) { setLinkRenderer(linkRenderer) {
// @type {Map<string, Documentation.Class>} // @type {Map<string, Class>}
const classesMap = new Map(); const classesMap = new Map();
const membersMap = new Map(); const membersMap = new Map();
for (const clazz of this.classesArray) { for (const clazz of this.classesArray) {
@ -156,7 +162,7 @@ class Documentation {
membersMap.set(`${member.kind}: ${clazz.name}.${member.name}`, member); membersMap.set(`${member.kind}: ${clazz.name}.${member.name}`, member);
} }
/** /**
* @param {Documentation.Class|Documentation.Member|null} classOrMember * @param {Class|Member|null} classOrMember
* @param {MarkdownNode[] | undefined} nodes * @param {MarkdownNode[] | undefined} nodes
*/ */
this._patchLinks = (classOrMember, nodes) => patchLinks(classOrMember, nodes, classesMap, membersMap, linkRenderer); this._patchLinks = (classOrMember, nodes) => patchLinks(classOrMember, nodes, classesMap, membersMap, linkRenderer);
@ -174,7 +180,7 @@ class Documentation {
/** /**
* @param {string} lang * @param {string} lang
* @param {import('../markdown').CodeGroupTransformer} transformer * @param {CodeGroupTransformer} transformer
*/ */
setCodeGroupsTransformer(lang, transformer) { setCodeGroupsTransformer(lang, transformer) {
this._codeGroupsTransformer = { lang, transformer }; this._codeGroupsTransformer = { lang, transformer };
@ -185,7 +191,7 @@ class Documentation {
clazz.visit(item => { clazz.visit(item => {
let spec = item.spec; let spec = item.spec;
if (spec && this._codeGroupsTransformer) if (spec && this._codeGroupsTransformer)
spec = md.processCodeGroups(spec, this._codeGroupsTransformer.lang, this._codeGroupsTransformer.transformer); spec = processCodeGroups(spec, this._codeGroupsTransformer.lang, this._codeGroupsTransformer.transformer);
item.comment = generateSourceCodeComment(spec); item.comment = generateSourceCodeComment(spec);
}); });
} }
@ -196,11 +202,11 @@ class Documentation {
} }
} }
Documentation.Class = class { class Class {
/** /**
* @param {Metainfo} metainfo * @param {Metainfo} metainfo
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Member>} membersArray * @param {!Array<!Member>} membersArray
* @param {?string=} extendsName * @param {?string=} extendsName
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
*/ */
@ -216,19 +222,19 @@ Documentation.Class = class {
this.index(); this.index();
const match = /** @type {string[]} */(name.match(/(API|JS|CDP|[A-Z])(.*)/)); const match = /** @type {string[]} */(name.match(/(API|JS|CDP|[A-Z])(.*)/));
this.varName = match[1].toLowerCase() + match[2]; this.varName = match[1].toLowerCase() + match[2];
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Member>} */
this.members = new Map(); this.members = new Map();
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Member>} */
this.properties = new Map(); this.properties = new Map();
/** @type {!Array<!Documentation.Member>} */ /** @type {!Array<!Member>} */
this.propertiesArray = []; this.propertiesArray = [];
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Member>} */
this.methods = new Map(); this.methods = new Map();
/** @type {!Array<!Documentation.Member>} */ /** @type {!Array<!Member>} */
this.methodsArray = []; this.methodsArray = [];
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Member>} */
this.events = new Map(); this.events = new Map();
/** @type {!Array<!Documentation.Member>} */ /** @type {!Array<!Member>} */
this.eventsArray = []; this.eventsArray = [];
} }
@ -259,7 +265,7 @@ Documentation.Class = class {
} }
clone() { clone() {
const cls = new Documentation.Class({ langs: this.langs, experimental: this.experimental, since: this.since }, this.name, this.membersArray.map(m => m.clone()), this.extends, this.spec); const cls = new Class({ langs: this.langs, experimental: this.experimental, since: this.since }, this.name, this.membersArray.map(m => m.clone()), this.extends, this.spec);
cls.comment = this.comment; cls.comment = this.comment;
return cls; return cls;
} }
@ -335,7 +341,7 @@ Documentation.Class = class {
} }
/** /**
* @param {function(Documentation.Member|Documentation.Class): void} visitor * @param {function(Member|Class): void} visitor
*/ */
visit(visitor) { visit(visitor) {
visitor(this); visitor(this);
@ -348,13 +354,13 @@ Documentation.Class = class {
} }
}; };
Documentation.Member = class { class Member {
/** /**
* @param {string} kind * @param {string} kind
* @param {Metainfo} metainfo * @param {Metainfo} metainfo
* @param {string} name * @param {string} name
* @param {?Documentation.Type} type * @param {?Type} type
* @param {!Array<!Documentation.Member>} argsArray * @param {!Array<!Member>} argsArray
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
* @param {boolean=} required * @param {boolean=} required
*/ */
@ -369,12 +375,12 @@ Documentation.Member = class {
this.argsArray = argsArray; this.argsArray = argsArray;
this.required = required; this.required = required;
this.comment = ''; this.comment = '';
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Member>} */
this.args = new Map(); this.args = new Map();
this.index(); this.index();
/** @type {!Documentation.Class | null} */ /** @type {!Class | null} */
this.clazz = null; this.clazz = null;
/** @type {Documentation.Member=} */ /** @type {Member=} */
this.enclosingMethod = undefined; this.enclosingMethod = undefined;
this.deprecated = false; this.deprecated = false;
if (spec) { if (spec) {
@ -466,7 +472,7 @@ Documentation.Member = class {
} }
clone() { clone() {
const result = new Documentation.Member(this.kind, { langs: this.langs, experimental: this.experimental, since: this.since }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required); const result = new Member(this.kind, { langs: this.langs, experimental: this.experimental, since: this.since }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required);
result.alias = this.alias; result.alias = this.alias;
result.async = this.async; result.async = this.async;
result.paramOrOption = this.paramOrOption; result.paramOrOption = this.paramOrOption;
@ -476,40 +482,40 @@ Documentation.Member = class {
/** /**
* @param {Metainfo} metainfo * @param {Metainfo} metainfo
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Member>} argsArray * @param {!Array<!Member>} argsArray
* @param {?Documentation.Type} returnType * @param {?Type} returnType
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
* @return {!Documentation.Member} * @return {!Member}
*/ */
static createMethod(metainfo, name, argsArray, returnType, spec) { static createMethod(metainfo, name, argsArray, returnType, spec) {
return new Documentation.Member('method', metainfo, name, returnType, argsArray, spec); return new Member('method', metainfo, name, returnType, argsArray, spec);
} }
/** /**
* @param {Metainfo} metainfo * @param {Metainfo} metainfo
* @param {!string} name * @param {!string} name
* @param {!Documentation.Type} type * @param {!Type} type
* @param {!MarkdownNode[]=} spec * @param {!MarkdownNode[]=} spec
* @param {boolean=} required * @param {boolean=} required
* @return {!Documentation.Member} * @return {!Member}
*/ */
static createProperty(metainfo, name, type, spec, required) { static createProperty(metainfo, name, type, spec, required) {
return new Documentation.Member('property', metainfo, name, type, [], spec, required); return new Member('property', metainfo, name, type, [], spec, required);
} }
/** /**
* @param {Metainfo} metainfo * @param {Metainfo} metainfo
* @param {string} name * @param {string} name
* @param {?Documentation.Type=} type * @param {?Type=} type
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
* @return {!Documentation.Member} * @return {!Member}
*/ */
static createEvent(metainfo, name, type = null, spec) { static createEvent(metainfo, name, type = null, spec) {
return new Documentation.Member('event', metainfo, name, type, [], spec); return new Member('event', metainfo, name, type, [], spec);
} }
/** /**
* @param {function(Documentation.Member|Documentation.Class): void} visitor * @param {function(Member|Class): void} visitor
*/ */
visit(visitor) { visit(visitor) {
visitor(this); visitor(this);
@ -520,15 +526,15 @@ Documentation.Member = class {
} }
}; };
Documentation.Type = class { class Type {
/** /**
* @param {string} expression * @param {string} expression
* @param {!Array<!Documentation.Member>=} properties * @param {!Array<!Member>=} properties
* @return {Documentation.Type} * @return {Type}
*/ */
static parse(expression, properties = []) { static parse(expression, properties = []) {
expression = expression.replace(/\\\(/g, '(').replace(/\\\)/g, ')'); expression = expression.replace(/\\\(/g, '(').replace(/\\\)/g, ')');
const type = Documentation.Type.fromParsedType(parseTypeExpression(expression)); const type = Type.fromParsedType(parseTypeExpression(expression));
type.expression = expression; type.expression = expression;
if (type.name === 'number') if (type.name === 'number')
throw new Error('Number types should be either int or float, not number in: ' + expression); throw new Error('Number types should be either int or float, not number in: ' + expression);
@ -550,7 +556,7 @@ Documentation.Type = class {
/** /**
* @param {ParsedType} parsedType * @param {ParsedType} parsedType
* @return {Documentation.Type} * @return {Type}
*/ */
static fromParsedType(parsedType, inUnion = false) { static fromParsedType(parsedType, inUnion = false) {
if (!inUnion && !parsedType.unionName && isStringUnion(parsedType) ) { if (!inUnion && !parsedType.unionName && isStringUnion(parsedType) ) {
@ -558,12 +564,12 @@ Documentation.Type = class {
} }
if (!inUnion && (parsedType.union || parsedType.unionName)) { if (!inUnion && (parsedType.union || parsedType.unionName)) {
const type = new Documentation.Type(parsedType.unionName || ''); const type = new Type(parsedType.unionName || '');
type.union = []; type.union = [];
// @ts-ignore // @ts-ignore
for (let t = parsedType; t; t = t.union) { for (let t = parsedType; t; t = t.union) {
const nestedUnion = !!t.unionName && t !== parsedType; const nestedUnion = !!t.unionName && t !== parsedType;
type.union.push(Documentation.Type.fromParsedType(t, !nestedUnion)); type.union.push(Type.fromParsedType(t, !nestedUnion));
if (nestedUnion) if (nestedUnion)
break; break;
} }
@ -571,41 +577,41 @@ Documentation.Type = class {
} }
if (parsedType.args) { if (parsedType.args) {
const type = new Documentation.Type('function'); const type = new Type('function');
type.args = []; type.args = [];
// @ts-ignore // @ts-ignore
for (let t = parsedType.args; t; t = t.next) for (let t = parsedType.args; t; t = t.next)
type.args.push(Documentation.Type.fromParsedType(t)); type.args.push(Type.fromParsedType(t));
type.returnType = parsedType.retType ? Documentation.Type.fromParsedType(parsedType.retType) : undefined; type.returnType = parsedType.retType ? Type.fromParsedType(parsedType.retType) : undefined;
return type; return type;
} }
if (parsedType.template) { if (parsedType.template) {
const type = new Documentation.Type(parsedType.name); const type = new Type(parsedType.name);
type.templates = []; type.templates = [];
// @ts-ignore // @ts-ignore
for (let t = parsedType.template; t; t = t.next) for (let t = parsedType.template; t; t = t.next)
type.templates.push(Documentation.Type.fromParsedType(t)); type.templates.push(Type.fromParsedType(t));
return type; return type;
} }
return new Documentation.Type(parsedType.name); return new Type(parsedType.name);
} }
/** /**
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Member>=} properties * @param {!Array<!Member>=} properties
*/ */
constructor(name, properties) { constructor(name, properties) {
this.name = name.replace(/^\[/, '').replace(/\]$/, ''); this.name = name.replace(/^\[/, '').replace(/\]$/, '');
/** @type {Documentation.Member[] | undefined} */ /** @type {Member[] | undefined} */
this.properties = this.name === 'Object' ? properties : undefined; this.properties = this.name === 'Object' ? properties : undefined;
/** @type {Documentation.Type[] | undefined} */ /** @type {Type[] | undefined} */
this.union; this.union;
/** @type {Documentation.Type[] | undefined} */ /** @type {Type[] | undefined} */
this.args; this.args;
/** @type {Documentation.Type | undefined} */ /** @type {Type | undefined} */
this.returnType; this.returnType;
/** @type {Documentation.Type[] | undefined} */ /** @type {Type[] | undefined} */
this.templates; this.templates;
/** @type {string | undefined} */ /** @type {string | undefined} */
this.expression; this.expression;
@ -621,7 +627,7 @@ Documentation.Type = class {
} }
clone() { clone() {
const type = new Documentation.Type(this.name, this.properties ? this.properties.map(prop => prop.clone()) : undefined); const type = new Type(this.name, this.properties ? this.properties.map(prop => prop.clone()) : undefined);
if (this.union) if (this.union)
type.union = this.union.map(type => type.clone()); type.union = this.union.map(type => type.clone());
if (this.args) if (this.args)
@ -635,7 +641,7 @@ Documentation.Type = class {
} }
/** /**
* @returns {Documentation.Member[]} * @returns {Member[]}
*/ */
deepProperties() { deepProperties() {
const types = []; const types = [];
@ -648,7 +654,7 @@ Documentation.Type = class {
} }
/** /**
* @returns {Documentation.Member[] | undefined} * @returns {Member[] | undefined}
*/ */
sortedProperties() { sortedProperties() {
if (!this.properties) if (!this.properties)
@ -689,7 +695,7 @@ Documentation.Type = class {
} }
/** /**
* @param {Documentation.Type[]} result * @param {Type[]} result
*/ */
_collectAllTypes(result) { _collectAllTypes(result) {
result.push(this); result.push(this);
@ -795,10 +801,10 @@ function matchingBracket(str, open, close) {
} }
/** /**
* @param {Documentation.Class|Documentation.Member|null} classOrMember * @param {Class|Member|null} classOrMember
* @param {MarkdownNode[]|undefined} spec * @param {MarkdownNode[]|undefined} spec
* @param {Map<string, Documentation.Class>} classesMap * @param {Map<string, Class>} classesMap
* @param {Map<string, Documentation.Member>} membersMap * @param {Map<string, Member>} membersMap
* @param {Renderer} linkRenderer * @param {Renderer} linkRenderer
*/ */
function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) { function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) {
@ -849,6 +855,8 @@ function generateSourceCodeComment(spec) {
md.visitAll(comments, node => { md.visitAll(comments, node => {
if (node.type === 'li' && node.liType === 'bullet') if (node.type === 'li' && node.liType === 'bullet')
node.liType = 'default'; node.liType = 'default';
if (node.type === 'code' && node.codeLang)
node.codeLang = parseCodeLang(node.codeLang).highlighter;
if (node.type === 'note') { if (node.type === 'note') {
// @ts-ignore // @ts-ignore
node.type = 'text'; node.type = 'text';
@ -859,7 +867,7 @@ function generateSourceCodeComment(spec) {
} }
/** /**
* @param {Documentation.Member} optionsArg * @param {Member} optionsArg
* @param {LanguageOptions=} options * @param {LanguageOptions=} options
*/ */
function patchCSharpOptionOverloads(optionsArg, options = {}) { function patchCSharpOptionOverloads(optionsArg, options = {}) {
@ -927,4 +935,88 @@ function csharpOptionOverloadSuffix(option, type) {
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`); throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
} }
module.exports = Documentation; /**
* @param {MarkdownNode[]} spec
* @param {string} language
* @param {CodeGroupTransformer} transformer
* @returns {MarkdownNode[]}
*/
function processCodeGroups(spec, language, transformer) {
/** @type {MarkdownNode[]} */
const newSpec = [];
for (let i = 0; i < spec.length; ++i) {
/** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */
const tabs = [];
for (;i < spec.length; i++) {
const codeLang = spec[i].codeLang;
if (!codeLang)
break;
let parsed;
try {
parsed = parseCodeLang(codeLang);
} catch (e) {
throw new Error(e.message + '\n while processing:\n' + md.render([spec[i]]));
}
if (!parsed.codeGroup)
break;
if (parsed.language && parsed.language !== language)
continue;
const [groupId, value] = parsed.codeGroup.split('-');
const clone = md.clone(spec[i]);
clone.codeLang = parsed.highlighter;
tabs.push({ groupId, value, spec: clone });
}
if (tabs.length) {
if (tabs.length === 1)
throw new Error(`Lonely tab "${tabs[0].spec.codeLang}". Make sure there are at least two tabs in the group.\n` + md.render([tabs[0].spec]));
// Validate group consistency.
const groupId = tabs[0].groupId;
const values = new Set();
for (const tab of tabs) {
if (tab.groupId !== groupId)
throw new Error('Mixed group ids: ' + md.render(spec));
if (values.has(tab.value))
throw new Error(`Duplicated tab "${tab.value}"\n` + md.render(tabs.map(tab => tab.spec)));
values.add(tab.value);
}
// Append transformed nodes.
newSpec.push(...transformer(tabs));
}
if (i < spec.length)
newSpec.push(spec[i]);
}
return newSpec;
}
/**
* @param {string} codeLang
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
*/
function parseCodeLang(codeLang) {
if (codeLang === 'python async')
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
if (codeLang === 'python sync')
return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' };
const [highlighter] = codeLang.split(' ');
if (!highlighter)
throw new Error(`Cannot parse code block lang: "${codeLang}"`);
const languageMatch = codeLang.match(/ lang=([\w\d]+)/);
let language = languageMatch ? languageMatch[1] : undefined;
if (!language) {
if (highlighter === 'ts')
language = 'js';
else if (highlighter === 'py')
language = 'python';
else if (['js', 'python', 'csharp', 'java'].includes(highlighter))
language = highlighter;
}
const tabMatch = codeLang.match(/ tab=([\w\d-]+)/);
return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' };
}
module.exports = { Documentation, Class, Member, Type, processCodeGroups, parseCodeLang };

View File

@ -18,7 +18,7 @@
const path = require('path'); const path = require('path');
const toKebabCase = require('lodash/kebabCase') const toKebabCase = require('lodash/kebabCase')
const devices = require('../../packages/playwright-core/lib/server/deviceDescriptors'); const devices = require('../../packages/playwright-core/lib/server/deviceDescriptors');
const Documentation = require('../doclint/documentation'); const docs = require('../doclint/documentation');
const PROJECT_DIR = path.join(__dirname, '..', '..'); const PROJECT_DIR = path.join(__dirname, '..', '..');
const fs = require('fs'); const fs = require('fs');
const { parseOverrides } = require('./parseOverrides'); const { parseOverrides } = require('./parseOverrides');
@ -30,7 +30,7 @@ Error.stackTraceLimit = 50;
class TypesGenerator { class TypesGenerator {
/** /**
* @param {{ * @param {{
* documentation: Documentation, * documentation: docs.Documentation,
* overridesToDocsClassMapping?: Map<string, string>, * overridesToDocsClassMapping?: Map<string, string>,
* ignoreMissing?: Set<string>, * ignoreMissing?: Set<string>,
* doNotExportClassNames?: Set<string>, * doNotExportClassNames?: Set<string>,
@ -39,7 +39,7 @@ class TypesGenerator {
* }} options * }} options
*/ */
constructor(options) { constructor(options) {
/** @type {Array<{name: string, properties: Documentation.Member[]}>} */ /** @type {Array<{name: string, properties: docs.Member[]}>} */
this.objectDefinitions = []; this.objectDefinitions = [];
/** @type {Set<string>} */ /** @type {Set<string>} */
this.handledMethods = new Set(); this.handledMethods = new Set();
@ -77,6 +77,8 @@ class TypesGenerator {
return `\`${option}\``; return `\`${option}\``;
if (clazz) if (clazz)
return `[${clazz.name}]`; return `[${clazz.name}]`;
if (!member || !member.clazz)
throw new Error('Internal error');
const className = member.clazz.varName === 'playwrightAssertions' ? '' : member.clazz.varName + '.'; const className = member.clazz.varName === 'playwrightAssertions' ? '' : member.clazz.varName + '.';
if (member.kind === 'method') if (member.kind === 'method')
return createMarkdownLink(member, `${className}${member.alias}(${this.renderJSSignature(member.argsArray)})`); return createMarkdownLink(member, `${className}${member.alias}(${this.renderJSSignature(member.argsArray)})`);
@ -203,7 +205,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Class} classDesc * @param {docs.Class} classDesc
*/ */
classToString(classDesc) { classToString(classDesc) {
const parts = []; const parts = [];
@ -229,7 +231,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Class} classDesc * @param {docs.Class} classDesc
*/ */
hasUniqueEvents(classDesc) { hasUniqueEvents(classDesc) {
if (!classDesc.events.size) if (!classDesc.events.size)
@ -241,7 +243,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Class} classDesc * @param {docs.Class} classDesc
*/ */
createEventDescriptions(classDesc) { createEventDescriptions(classDesc) {
if (!this.hasUniqueEvents(classDesc)) if (!this.hasUniqueEvents(classDesc))
@ -263,7 +265,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Class} classDesc * @param {docs.Class} classDesc
* @param {boolean=} exportMembersAsGlobals * @param {boolean=} exportMembersAsGlobals
*/ */
classBody(classDesc, exportMembersAsGlobals) { classBody(classDesc, exportMembersAsGlobals) {
@ -319,8 +321,8 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Class} classDesc * @param {docs.Class} classDesc
* @param {Documentation.Member} member * @param {docs.Member} member
*/ */
hasOwnMethod(classDesc, member) { hasOwnMethod(classDesc, member) {
if (this.handledMethods.has(`${classDesc.name}.${member.alias}#${member.overloadIndex}`)) if (this.handledMethods.has(`${classDesc.name}.${member.alias}#${member.overloadIndex}`))
@ -333,7 +335,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Class} classDesc * @param {docs.Class} classDesc
*/ */
parentClass(classDesc) { parentClass(classDesc) {
if (!classDesc.extends) if (!classDesc.extends)
@ -378,7 +380,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Type} type * @param {docs.Type} type
*/ */
stringifyComplexType(type, indent, ...namespace) { stringifyComplexType(type, indent, ...namespace) {
if (!type) if (!type)
@ -387,7 +389,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Member[]} properties * @param {docs.Member[]} properties
* @param {string} name * @param {string} name
* @param {string=} indent * @param {string=} indent
* @returns {string} * @returns {string}
@ -406,7 +408,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Type=} type * @param {docs.Type=} type
* @returns{string} * @returns{string}
*/ */
stringifySimpleType(type, indent = '', ...namespace) { stringifySimpleType(type, indent = '', ...namespace) {
@ -455,7 +457,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Member} member * @param {docs.Member} member
*/ */
argsFromMember(member, indent, ...namespace) { argsFromMember(member, indent, ...namespace) {
if (member.kind === 'property') if (member.kind === 'property')
@ -464,7 +466,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Member} member * @param {docs.Member} member
* @param {string} indent * @param {string} indent
*/ */
memberJSDOC(member, indent) { memberJSDOC(member, indent) {
@ -480,7 +482,7 @@ class TypesGenerator {
} }
/** /**
* @param {Documentation.Member[]} args * @param {docs.Member[]} args
*/ */
renderJSSignature(args) { renderJSSignature(args) {
const tokens = []; const tokens = [];

View File

@ -62,12 +62,6 @@
* lines: string[], * lines: string[],
* }} MarkdownPropsNode */ * }} MarkdownPropsNode */
/** @typedef {{
* value: string, groupId: string, spec: MarkdownNode
* }} CodeGroup */
/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */
/** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */ /** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */
function flattenWrappedLines(content) { function flattenWrappedLines(content) {
@ -310,10 +304,7 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) {
if (node.type === 'code') { if (node.type === 'code') {
newLine(); newLine();
if (process.env.API_JSON_MODE) result.push(`${indent}\`\`\`${node.codeLang}`);
result.push(`${indent}\`\`\`${node.codeLang}`);
else
result.push(`${indent}\`\`\`${node.codeLang ? parseCodeLang(node.codeLang).highlighter : ''}`);
for (const line of node.lines) for (const line of node.lines)
result.push(indent + line); result.push(indent + line);
result.push(`${indent}\`\`\``); result.push(`${indent}\`\`\``);
@ -473,86 +464,4 @@ function filterNodesForLanguage(nodes, language) {
return result; return result;
} }
/** module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage };
* @param {string} codeLang
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
*/
function parseCodeLang(codeLang) {
if (codeLang === 'python async')
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
if (codeLang === 'python sync')
return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' };
const [highlighter] = codeLang.split(' ');
if (!highlighter)
throw new Error(`Cannot parse code block lang: "${codeLang}"`);
const languageMatch = codeLang.match(/ lang=([\w\d]+)/);
let language = languageMatch ? languageMatch[1] : undefined;
if (!language) {
if (highlighter === 'ts')
language = 'js';
else if (highlighter === 'py')
language = 'python';
else if (['js', 'python', 'csharp', 'java'].includes(highlighter))
language = highlighter;
}
const tabMatch = codeLang.match(/ tab=([\w\d-]+)/);
return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' };
}
/**
* @param {MarkdownNode[]} spec
* @param {string} language
* @param {CodeGroupTransformer} transformer
* @returns {MarkdownNode[]}
*/
function processCodeGroups(spec, language, transformer) {
/** @type {MarkdownNode[]} */
const newSpec = [];
for (let i = 0; i < spec.length; ++i) {
/** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */
const tabs = [];
for (;i < spec.length; i++) {
const codeLang = spec[i].codeLang;
if (!codeLang)
break;
let parsed;
try {
parsed = parseCodeLang(codeLang);
} catch (e) {
throw new Error(e.message + '\n while processing:\n' + render([spec[i]]));
}
if (!parsed.codeGroup)
break;
if (parsed.language && parsed.language !== language)
continue;
const [groupId, value] = parsed.codeGroup.split('-');
tabs.push({ groupId, value, spec: spec[i] });
}
if (tabs.length) {
if (tabs.length === 1)
throw new Error(`Lonely tab "${tabs[0].spec.codeLang}". Make sure there are at least two tabs in the group.\n` + render([tabs[0].spec]));
// Validate group consistency.
const groupId = tabs[0].groupId;
const values = new Set();
for (const tab of tabs) {
if (tab.groupId !== groupId)
throw new Error('Mixed group ids: ' + render(spec));
if (values.has(tab.value))
throw new Error(`Duplicated tab "${tab.value}"\n` + render(tabs.map(tab => tab.spec)));
values.add(tab.value);
}
// Append transformed nodes.
newSpec.push(...transformer(tabs));
}
if (i < spec.length)
newSpec.push(spec[i]);
}
return newSpec;
}
module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, parseCodeLang, processCodeGroups };