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

View File

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

View File

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