chore: annotate md utils to compile with ts-check (#17600)

This commit is contained in:
Pavel Feldman 2022-09-27 08:27:23 -08:00 committed by GitHub
parent 6fe551e6ac
commit 0a3cfc1c21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 60 deletions

View File

@ -22,6 +22,9 @@ const md = require('../markdown');
const Documentation = require('./documentation');
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
/** @typedef {import('../markdown').MarkdownHeaderNode} MarkdownHeaderNode */
/** @typedef {import('../markdown').MarkdownLiNode} MarkdownLiNode */
/** @typedef {import('../markdown').MarkdownTextNode} MarkdownTextNode */
class ApiParser {
/**
@ -39,7 +42,7 @@ class ApiParser {
bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString());
}
const body = md.parse(bodyParts.join('\n'));
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : null;
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : undefined;
checkNoDuplicateParamEntries(params);
const api = params ? applyTemplates(body, params) : body;
/** @type {Map<string, Documentation.Class>} */
@ -61,7 +64,7 @@ class ApiParser {
}
/**
* @param {MarkdownNode} node
* @param {MarkdownHeaderNode} node
*/
parseClass(node) {
let extendsName = null;
@ -80,7 +83,7 @@ class ApiParser {
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
*/
parseMember(spec) {
const match = spec.text.match(/(event|method|property|async method|optional method|optional async method): ([^.]+)\.(.*)/);
@ -112,10 +115,13 @@ class ApiParser {
if (match[1].includes('optional'))
member.required = false;
}
const clazz = this.classes.get(match[2]);
if (!member)
throw new Error('Unknown member: ' + spec.text);
const clazz = /** @type {Documentation.Class} */(this.classes.get(match[2]));
const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind);
if (existingMember && isTypeOverride(existingMember, member)) {
for (const lang of member.langs.only) {
for (const lang of member?.langs?.only || []) {
existingMember.langs.types = existingMember.langs.types || {};
existingMember.langs.types[lang] = returnType;
}
@ -125,7 +131,7 @@ class ApiParser {
}
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
*/
parseArgument(spec) {
const match = spec.text.match(/(param|option): (.*)/);
@ -173,34 +179,35 @@ class ApiParser {
}
const p = this.parseProperty(spec);
p.required = false;
// @ts-ignore
options.type.properties.push(p);
}
}
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
*/
parseProperty(spec) {
const param = childrenWithoutProperties(spec)[0];
const text = param.text;
const text = /** @type {string}*/(param.text);
let typeStart = text.indexOf('<');
while ('?e'.includes(text[typeStart - 1]))
typeStart--;
const name = text.substring(0, typeStart).replace(/\`/g, '').trim();
const comments = extractComments(spec);
const { type, optional } = this.parseType(param);
const { type, optional } = this.parseType(/** @type {MarkdownLiNode} */(param));
return Documentation.Member.createProperty(extractMetainfo(spec), name, type, comments, !optional);
}
/**
* @param {MarkdownNode=} spec
* @param {MarkdownLiNode} spec
* @return {{ type: Documentation.Type, optional: boolean, experimental: boolean }}
*/
parseType(spec) {
const arg = parseVariable(spec.text);
const properties = [];
for (const child of spec.children || []) {
const { name, text } = parseVariable(child.text);
for (const child of /** @type {MarkdownLiNode[]} */ (spec.children) || []) {
const { name, text } = parseVariable(/** @type {string} */(child.text));
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
const childType = this.parseType(child);
properties.push(Documentation.Member.createProperty({ langs: {}, experimental: childType.experimental, since: 'v1.0' }, name, childType.type, comments, !childType.optional));
@ -271,7 +278,7 @@ function applyTemplates(body, params) {
if (!template)
throw new Error('Bad template: ' + prop.text);
const children = childrenWithoutProperties(template);
const { name: argName } = parseVariable(children[0].text);
const { name: argName } = parseVariable(children[0].text || '');
newChildren.push({
type: node.type,
text: name + argName,
@ -301,7 +308,7 @@ function applyTemplates(body, params) {
}
/**
* @param {MarkdownNode} item
* @param {MarkdownHeaderNode} item
* @returns {MarkdownNode[]}
*/
function extractComments(item) {
@ -323,7 +330,7 @@ function parseApi(apiDir, paramsPath) {
}
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
* @returns {import('./documentation').Metainfo}
*/
function extractMetainfo(spec) {
@ -339,7 +346,7 @@ function extractMetainfo(spec) {
* @returns {import('./documentation').Langs}
*/
function extractLangs(spec) {
for (const child of spec.children) {
for (const child of spec.children || []) {
if (child.type !== 'li' || child.liType !== 'bullet' || !child.text.startsWith('langs:'))
continue;
@ -347,7 +354,7 @@ function extractLangs(spec) {
/** @type {Object<string, string>} */
const aliases = {};
for (const p of child.children || []) {
const match = p.text.match(/alias-(\w+)[\s]*:(.*)/);
const match = /** @type {string}*/(p.text).match(/alias-(\w+)[\s]*:(.*)/);
if (match)
aliases[match[1].trim()] = match[2].trim();
}
@ -362,7 +369,7 @@ function extractLangs(spec) {
}
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
* @returns {string}
*/
function extractSince(spec) {
@ -377,7 +384,7 @@ function extractSince(spec) {
}
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
* @returns {boolean}
*/
function extractExperimental(spec) {
@ -389,12 +396,12 @@ function extractSince(spec) {
}
/**
* @param {MarkdownNode} spec
* @param {MarkdownHeaderNode} spec
* @returns {MarkdownNode[]}
*/
function childrenWithoutProperties(spec) {
return (spec.children || []).filter(c => {
const isProperty = c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text === 'experimental');
const isProperty = c.type === 'li' && c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text === 'experimental');
return !isProperty;
});
}
@ -405,11 +412,12 @@ function childrenWithoutProperties(spec) {
* @returns {boolean}
*/
function isTypeOverride(existingMember, member) {
if (!existingMember.langs.only)
if (!existingMember.langs.only || !member.langs.only)
return true;
if (member.langs.only.every(l => existingMember.langs.only.includes(l))) {
const existingOnly = existingMember.langs.only;
if (member.langs.only.every(l => existingOnly.includes(l))) {
return true;
} else if (member.langs.only.some(l => existingMember.langs.only.includes(l))) {
} else if (member.langs.only.some(l => existingOnly.includes(l))) {
throw new Error(`Ambiguous language override for: ${member.name}`);
}
return false;

View File

@ -149,19 +149,19 @@ class Documentation {
}
/**
* @param {Documentation.Class|Documentation.Member|null} classOrMember
* @param {MarkdownNode[]} nodes
* @param {MarkdownNode[] | undefined} nodes
*/
this._patchLinks = (classOrMember, nodes) => patchLinks(classOrMember, nodes, classesMap, membersMap, linkRenderer);
for (const clazz of this.classesArray)
clazz.visit(item => this._patchLinks(item, item.spec));
clazz.visit(item => this._patchLinks?.(item, item.spec));
}
/**
* @param {MarkdownNode[]} nodes
*/
renderLinksInText(nodes) {
this._patchLinks(null, nodes);
this._patchLinks?.(null, nodes);
}
generateSourceCodeComments() {
@ -192,11 +192,8 @@ Documentation.Class = class {
this.extends = extendsName;
this.comment = '';
this.index();
const match = 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];
}
index() {
/** @type {!Map<string, !Documentation.Member>} */
this.members = new Map();
/** @type {!Map<string, !Documentation.Member>} */
@ -211,6 +208,16 @@ Documentation.Class = class {
this.events = new Map();
/** @type {!Array<!Documentation.Member>} */
this.eventsArray = [];
}
index() {
this.members = new Map();
this.properties = new Map();
this.propertiesArray = [];
this.methods = new Map();
this.methodsArray = [];
this.events = new Map();
this.eventsArray = [];
for (const member of this.membersArray) {
this.members.set(member.name, member);
@ -342,7 +349,7 @@ Documentation.Member = class {
/** @type {!Map<string, !Documentation.Member>} */
this.args = new Map();
this.index();
/** @type {!Documentation.Class} */
/** @type {!Documentation.Class | null} */
this.clazz = null;
/** @type {Documentation.Member=} */
this.enclosingMethod = undefined;
@ -357,13 +364,13 @@ Documentation.Member = class {
this.alias = name;
this.overloadIndex = 0;
if (name.includes('#')) {
const match = name.match(/(.*)#(.*)/);
const match = /** @type {string[]} */(name.match(/(.*)#(.*)/));
this.alias = match[1];
this.overloadIndex = (+match[2]) - 1;
}
/**
* Param is true and option false
* @type {Boolean}
* @type {Boolean | null}
*/
this.paramOrOption = null;
}
@ -376,7 +383,9 @@ Documentation.Member = class {
this.args.set(arg.name, arg);
arg.enclosingMethod = this;
if (arg.name === 'options') {
// @ts-ignore
arg.type.properties.sort((p1, p2) => p1.name.localeCompare(p2.name));
// @ts-ignore
arg.type.properties.forEach(p => p.enclosingMethod = this);
}
}
@ -386,6 +395,8 @@ Documentation.Member = class {
* @param {string} lang
*/
filterForLanguage(lang) {
if (!this.type)
return;
if (this.langs.aliases && this.langs.aliases[lang])
this.alias = this.langs.aliases[lang];
if (this.langs.types && this.langs.types[lang])
@ -397,8 +408,10 @@ Documentation.Member = class {
continue;
const overriddenArg = (arg.langs.overrides && arg.langs.overrides[lang]) || arg;
overriddenArg.filterForLanguage(lang);
// @ts-ignore
if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length)
continue;
// @ts-ignore
overriddenArg.type.filterForLanguage(lang);
argsArray.push(overriddenArg);
}
@ -406,10 +419,12 @@ Documentation.Member = class {
}
filterOutExperimental() {
if (!this.type)
return;
this.type.filterOutExperimental();
const argsArray = [];
for (const arg of this.argsArray) {
if (arg.experimental)
if (arg.experimental || !arg.type)
continue;
arg.type.filterOutExperimental();
argsArray.push(arg);
@ -418,7 +433,7 @@ Documentation.Member = class {
}
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 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);
result.alias = this.alias;
result.async = this.async;
result.paramOrOption = this.paramOrOption;
@ -512,6 +527,7 @@ Documentation.Type = class {
if (!inUnion && (parsedType.union || parsedType.unionName)) {
const type = new Documentation.Type(parsedType.unionName || '');
type.union = [];
// @ts-ignore
for (let t = parsedType; t; t = t.union) {
const nestedUnion = !!t.unionName && t !== parsedType;
type.union.push(Documentation.Type.fromParsedType(t, !nestedUnion));
@ -524,15 +540,17 @@ Documentation.Type = class {
if (parsedType.args) {
const type = new Documentation.Type('function');
type.args = [];
// @ts-ignore
for (let t = parsedType.args; t; t = t.next)
type.args.push(Documentation.Type.fromParsedType(t));
type.returnType = parsedType.retType ? Documentation.Type.fromParsedType(parsedType.retType) : null;
type.returnType = parsedType.retType ? Documentation.Type.fromParsedType(parsedType.retType) : undefined;
return type;
}
if (parsedType.template) {
const type = new Documentation.Type(parsedType.name);
type.templates = [];
// @ts-ignore
for (let t = parsedType.template; t; t = t.next)
type.templates.push(Documentation.Type.fromParsedType(t));
return type;
@ -597,7 +615,7 @@ Documentation.Type = class {
}
/**
* @returns {Documentation.Member[]}
* @returns {Documentation.Member[] | undefined}
*/
sortedProperties() {
if (!this.properties)
@ -653,7 +671,7 @@ Documentation.Type = class {
};
/**
* @param {ParsedType} type
* @param {ParsedType | null} type
* @returns {boolean}
*/
function isStringUnion(type) {
@ -744,7 +762,7 @@ function matchingBracket(str, open, close) {
/**
* @param {Documentation.Class|Documentation.Member|null} classOrMember
* @param {MarkdownNode[]} spec
* @param {MarkdownNode[]|undefined} spec
* @param {Map<string, Documentation.Class>} classesMap
* @param {Map<string, Documentation.Member>} membersMap
* @param {Renderer} linkRenderer
@ -790,16 +808,17 @@ function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) {
}
/**
* @param {MarkdownNode[]} spec
* @param {MarkdownNode[] | undefined} spec
*/
function generateSourceCodeComment(spec) {
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
md.visitAll(comments, node => {
if (node.codeLang && node.codeLang.includes('tab=js-js'))
node.type = 'null';
if (node.liType === 'bullet')
if (node.type === 'li' && node.liType === 'bullet')
node.liType = 'default';
if (node.type === 'note') {
// @ts-ignore
node.type = 'text';
node.text = '> NOTE: ' + node.text;
}

View File

@ -17,14 +17,52 @@
// @ts-check
/** @typedef {{
* type: 'text' | 'li' | 'code' | 'properties' | 'h0' | 'h1' | 'h2' | 'h3' | 'h4' | 'note' | 'null',
* type: string,
* text?: string,
* children?: MarkdownNode[],
* codeLang?: string,
* noteType?: string,
* lines?: string[],
* liType?: 'default' | 'bullet' | 'ordinal',
* children?: MarkdownNode[]
* }} MarkdownNode */
* }} MarkdownBaseNode */
/** @typedef {MarkdownBaseNode & {
* type: 'text',
* text: string,
* }} MarkdownTextNode */
/** @typedef {MarkdownBaseNode & {
* type: 'h0' | 'h1' | 'h2' | 'h3' | 'h4',
* text: string,
* children: MarkdownNode[]
* }} MarkdownHeaderNode */
/** @typedef {MarkdownBaseNode & {
* type: 'li',
* text: string,
* liType: 'default' | 'bullet' | 'ordinal',
* children: MarkdownNode[]
* }} MarkdownLiNode */
/** @typedef {MarkdownBaseNode & {
* type: 'code',
* lines: string[],
* codeLang: string,
* }} MarkdownCodeNode */
/** @typedef {MarkdownBaseNode & {
* type: 'note',
* text: string,
* noteType: string,
* }} MarkdownNoteNode */
/** @typedef {MarkdownBaseNode & {
* type: 'null',
* }} MarkdownNullNode */
/** @typedef {MarkdownBaseNode & {
* type: 'properties',
* lines: string[],
* }} MarkdownPropsNode */
/** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */
function flattenWrappedLines(content) {
const inLines = content.replace(/\r\n/g, '\n').split('\n');
@ -110,13 +148,13 @@ function buildTree(lines) {
else
break;
}
headerStack[0].children.push(node);
/** @type {MarkdownNode[]}*/(headerStack[0].children).push(node);
headerStack.unshift(node);
continue;
}
// Remaining items respect indent-based nesting.
const [, indent, content] = line.match('^([ ]*)(.*)');
const [, indent, content] = /** @type {string[]} */ (line.match('^([ ]*)(.*)'));
if (content.startsWith('```')) {
/** @type {MarkdownNode} */
const node = {
@ -143,10 +181,10 @@ function buildTree(lines) {
if (content.startsWith(':::')) {
/** @type {MarkdownNode} */
const node = {
const node = /** @type {MarkdownNoteNode} */ ({
type: 'note',
noteType: content.substring(3)
};
});
line = lines[++i];
const tokens = [];
while (!line.trim().startsWith(':::')) {
@ -184,16 +222,17 @@ function buildTree(lines) {
const liType = content.match(/^(-|1.|\*) /);
const node = /** @type {MarkdownNode} */({ type: 'text', text: content });
if (liType) {
node.type = 'li';
node.text = content.substring(liType[0].length);
const liNode = /** @type {MarkdownLiNode} */(node);
liNode.type = 'li';
liNode.text = content.substring(liType[0].length);
if (content.startsWith('1.'))
node.liType = 'ordinal';
liNode.liType = 'ordinal';
else if (content.startsWith('*'))
node.liType = 'bullet';
liNode.liType = 'bullet';
else
node.liType = 'default';
liNode.liType = 'default';
}
const match = node.text.match(/\*\*langs: (.*)\*\*(.*)/);
const match = node.text?.match(/\*\*langs: (.*)\*\*(.*)/);
if (match) {
node.codeLang = match[1];
node.text = match[2];
@ -220,7 +259,7 @@ function render(nodes, maxColumns) {
for (let node of nodes) {
if (node.type === 'null')
continue;
innerRenderMdNode('', node, lastNode, result, maxColumns);
innerRenderMdNode('', node, /** @type {MarkdownNode} */ (lastNode), result, maxColumns);
lastNode = node;
}
return result.join('\n');
@ -240,9 +279,10 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) {
};
if (node.type.startsWith('h')) {
const headerNode = /** @type {MarkdownHeaderNode} */ (node);
newLine();
const depth = +node.type.substring(1);
result.push(`${'#'.repeat(depth)} ${node.text}`);
result.push(`${'#'.repeat(depth)} ${headerNode.text}`);
let lastNode = node;
for (const child of node.children || []) {
innerRenderMdNode('', child, lastNode, result, maxColumns);