From 46e2c4b2a672b396fb97e758016b51ca96b81ee5 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:32:43 -0700 Subject: [PATCH 01/18] feat: legacy json generator Closes #57 Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/index.mjs | 4 + src/generators/legacy-json-all/index.mjs | 19 ++ src/generators/legacy-json-all/types.d.ts | 9 + src/generators/legacy-json/index.mjs | 57 +++++ src/generators/legacy-json/types.d.ts | 63 ++++++ .../legacy-json/utils/buildContent.mjs | 202 ++++++++++++++++++ .../legacy-json/utils/buildHierarchy.mjs | 42 ++++ src/metadata.mjs | 1 + src/types.d.ts | 2 + 9 files changed, 399 insertions(+) create mode 100644 src/generators/legacy-json-all/index.mjs create mode 100644 src/generators/legacy-json-all/types.d.ts create mode 100644 src/generators/legacy-json/index.mjs create mode 100644 src/generators/legacy-json/types.d.ts create mode 100644 src/generators/legacy-json/utils/buildContent.mjs create mode 100644 src/generators/legacy-json/utils/buildHierarchy.mjs diff --git a/src/generators/index.mjs b/src/generators/index.mjs index 5b916d8..ddd4d73 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -3,9 +3,13 @@ import jsonSimple from './json-simple/index.mjs'; import legacyHtml from './legacy-html/index.mjs'; import legacyHtmlAll from './legacy-html-all/index.mjs'; +import legacyJson from './legacy-json/index.mjs'; +import legacyJsonAll from './legacy-json-all/index.mjs'; export default { 'json-simple': jsonSimple, 'legacy-html': legacyHtml, 'legacy-html-all': legacyHtmlAll, + 'legacy-json': legacyJson, + 'legacy-json-all': legacyJsonAll, }; diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs new file mode 100644 index 0000000..37a51d5 --- /dev/null +++ b/src/generators/legacy-json-all/index.mjs @@ -0,0 +1,19 @@ +'use strict'; + +/** + * @typedef {Array} Input + * + * @type {import('../types.d.ts').GeneratorMetadata} + */ +export default { + name: 'legacy-json-all', + + version: '1.0.0', + + description: + 'Generates the `all.json` file from the `legacy-json` generator, which includes all the modules in one single file.', + + dependsOn: 'legacy-json', + + async generate(input) {}, +}; diff --git a/src/generators/legacy-json-all/types.d.ts b/src/generators/legacy-json-all/types.d.ts new file mode 100644 index 0000000..daab11d --- /dev/null +++ b/src/generators/legacy-json-all/types.d.ts @@ -0,0 +1,9 @@ +import { MiscSection, Section, SignatureSection } from '../legacy-json/types'; + +export interface Output { + miscs: MiscSection[]; + modules: Section[]; + classes: SignatureSection[]; + globals: object[]; // TODO + methods: SignatureSection[]; +} diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs new file mode 100644 index 0000000..a44171b --- /dev/null +++ b/src/generators/legacy-json/index.mjs @@ -0,0 +1,57 @@ +'use strict'; + +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { groupNodesByModule } from '../../utils/generators.mjs'; +import buildContent from './utils/buildContent.mjs'; + +/** + * @typedef {Array} Input + * + * @type {import('../types.d.ts').GeneratorMetadata} + */ +export default { + name: 'legacy-json', + + version: '1.0.0', + + description: 'Generates the legacy version of the JSON API docs.', + + dependsOn: 'ast', + + async generate(input, { output }) { + // This array holds all the generated values for each module + const generatedValues = []; + + const groupedModules = groupNodesByModule(input); + + // Gets the first nodes of each module, which is considered the "head" of the module + const headNodes = input.filter(node => node.heading.depth === 1); + + /** + * @param {ApiDocMetadataEntry} head + * @returns {import('./types.d.ts').ModuleSection} + */ + const processModuleNodes = head => { + const nodes = groupedModules.get(head.api); + + const parsedContent = buildContent(head, nodes); + generatedValues.push(parsedContent); + + return parsedContent; + }; + + for (const node of headNodes) { + const result = processModuleNodes(node); + // console.log(result) + + await writeFile( + join(output, `${node.api}.json`), + JSON.stringify(result), + 'utf8' + ); + } + + return generatedValues; + }, +}; diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts new file mode 100644 index 0000000..3c1308d --- /dev/null +++ b/src/generators/legacy-json/types.d.ts @@ -0,0 +1,63 @@ +export interface HierarchizedEntry extends ApiDocMetadataEntry { + hierarchyChildren: ApiDocMetadataEntry[]; +} + +export type Section = + | SignatureSection + | PropertySection + | EventSection + | MiscSection; + +export interface Meta { + changes: ApiDocMetadataChange[]; + added?: string[]; + napiVersion?: string[]; + deprecated?: string[]; + removed?: string[]; +} + +export interface SectionBase { + type: string; + name: string; + textRaw: string; + displayName?: string; + desc: string; + shortDesc?: string; + stability?: number; + stabilityText?: string; // TODO find the type for this + meta?: Meta; +} + +export interface ModuleSection extends SectionBase { + type: 'module'; + source: string; + miscs: MiscSection[]; + modules: ModuleSection[]; + // TODO ... +} + +export interface SignatureSection extends SectionBase { + type: 'class' | 'ctor' | 'classMethod' | 'method'; + signatures: MethodSignature[]; +} + +export interface MethodSignature { + params: object[]; // TODO + return?: object; // TODO +} + +export interface PropertySection extends SectionBase { + type: 'property'; + typeof?: string; + [key: string]: string | undefined; +} + +export interface EventSection extends SectionBase { + type: 'event'; + params: object[]; // TODO +} + +export interface MiscSection extends SectionBase { + type: 'misc'; + [key: string]: string | undefined; +} diff --git a/src/generators/legacy-json/utils/buildContent.mjs b/src/generators/legacy-json/utils/buildContent.mjs new file mode 100644 index 0000000..661e575 --- /dev/null +++ b/src/generators/legacy-json/utils/buildContent.mjs @@ -0,0 +1,202 @@ +import { buildHierarchy } from './buildHierarchy.mjs'; + +const sectionTypePlurals = { + module: 'modules', + misc: 'miscs', + class: 'classes', + method: 'methods', + property: 'properties', + global: 'globals', + example: 'examples', + ctor: 'ctors', + classMethod: 'classMethods', + event: 'events', + var: 'vars', +}; + +/** + * + * @param {HeadingMetadataParent} entry + * @returns {import('../types.d.ts').Section} + */ +function createSection(entry) { + const text = textJoin(entry.children); + + // TODO check if type or name can be undefined + return { + textRaw: text, + type: entry.data.type, + name: entry.data.name, + }; +} + +/** + * + * @param {*} values TODO type + * @returns {import('../types.d.ts').MethodSignature} + */ +function parseSignature(values) { + /** + * @type {import('../types.d.ts').MethodSignature} + */ + const signature = {}; + + signature.params = values.filter(value => { + if (value.name === 'return') { + signature.return = value; + return false; + } + + return true; + }); + + return signature; +} + +/** + * + * @param {import('mdast').PhrasingContent[]} nodes + */ +function textJoin(nodes) { + return nodes + .map(node => { + switch (node.type) { + case 'linkReference': + console.error(`todo link reference`); + return `TODO`; + case `strong`: + return `**${textJoin(node.children)}**`; + case `emphasis`: + return `_${textJoin(node.children)}_`; + default: + if (node.children) { + return textJoin(node.children); + } + + return node.value; + } + }) + .join(''); +} + +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @returns {import('../types.d.ts').Meta | undefined} + */ +function createMeta(entry) { + const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); + + const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; + if (added_in || n_api_version || deprecated_in || removed_in) { + return { + changes, + added: makeArrayIfNotAlready(added_in), + napiVersion: makeArrayIfNotAlready(n_api_version), + deprecated: makeArrayIfNotAlready(deprecated_in), + removed: makeArrayIfNotAlready(removed_in), + }; + } + + return undefined; +} + +function parseListItem() { + return { type: 'asd' }; +} + +/** + * + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @param {import('../types.d.ts').Section} parentSection + */ +function handleEntry(entry, parentSection) { + let [headingNode, ...nodes] = structuredClone(entry.content.children); + + const section = createSection(headingNode); + section.meta = createMeta(entry); + + const pluralType = sectionTypePlurals[section.type]; + if (!(pluralType in parentSection)) { + parentSection[pluralType] = []; + } + parentSection[sectionTypePlurals[section.type]].push(section); + + // Remove metadata not directly inferable from the markdown + nodes.forEach((node, i) => { + if ( + node.type === 'blockquote' && + node.children.length === 1 && + node.children[0].type === 'paragraph' && + nodes.slice(0, i).every(node => node.type === 'list') + ) { + const text = textJoin(node.children[0].children); + const stability = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s.exec(text); + if (stability) { + section.stability = parseInt(stability[1], 10); + section.stabilityText = stability[2].replaceAll('\n', ' ').trim(); + delete nodes[i]; + } + } + }); + + // Compress the node array. + nodes = nodes.filter(() => true); + + const list = nodes.length && nodes[0].type === 'list' ? nodes.shift() : null; + + if (list) { + const values = list ? list.children.map(child => parseListItem(child)) : []; + + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': { + const signature = parseSignature(values); + section.signatures = [signature]; + break; + } + case 'property': + break; + case 'event': + section.params = values; + break; + default: + // List wasn't consumed, add it back + nodes.unshift(list); + } + } + + if (nodes.length) { + if (section.desc) { + section.shortDesc = section.desc; + } + + // TODO: parse definitions + } + + // Handle our children if we have them + if (entry.hierarchyChildren) { + entry.hierarchyChildren.forEach(child => handleEntry(child, section)); + } + + // TODO: the cleanup work the current parser does +} + +/** + * @param {ApiDocMetadataEntry} head + * @param {Array} entries + * @returns {import('../types.d.ts').ModuleSection} + */ +export default (head, entries) => { + /** + * @type {import('../types.d.ts').ModuleSection} + */ + const module = { + type: 'module', + source: `doc/api/${head.api_doc_source}`, + }; + + buildHierarchy(entries).forEach(entry => handleEntry(entry, module)); + + return module; +}; diff --git a/src/generators/legacy-json/utils/buildHierarchy.mjs b/src/generators/legacy-json/utils/buildHierarchy.mjs new file mode 100644 index 0000000..bc063c9 --- /dev/null +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -0,0 +1,42 @@ +/** + * So we need the files to be in a hierarchy based off of depth, but they're + * given to us flattened. So, let's fix that in a way that's incredibly + * unfortunate but works! + * @param {ApiDocMetadataEntry[]} entries + * @returns {import('../types.d.ts').HierarchizedEntry[]} + */ +export function buildHierarchy(entries) { + const roots = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const currentDepth = entry.heading.depth; + + if (currentDepth === 1) { + roots.push(entry); + continue; + } + + const previousEntry = entries[i - 1]; + + const previousDepth = previousEntry.heading.depth; + if (currentDepth > previousDepth) { + // We're a child of the previous one + if (previousEntry.hierarchyChildren === undefined) { + previousEntry.hierarchyChildren = []; + } + previousEntry.hierarchyChildren.push(entry); + } else { + for (let j = i - 2; j >= 0; j--) { + const jEntry = entries[j]; + const jDepth = jEntry.heading.depth; + if (currentDepth > jDepth) { + jEntry.hierarchyChildren.push(entry); + break; + } + } + } + } + + return roots; +} diff --git a/src/metadata.mjs b/src/metadata.mjs index 3535869..4064b8e 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -142,6 +142,7 @@ const createMetadata = slugger => { api: apiDoc.stem, slug: sectionSlug, source_link, + api_doc_source: apiDoc.basename, added_in: added, deprecated_in: deprecated, removed_in: removed, diff --git a/src/types.d.ts b/src/types.d.ts index 1391750..34a8cb0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -70,6 +70,8 @@ declare global { slug: string; // The GitHub URL to the source of the API entry source_link: string | Array | undefined; + // Base name of the api doc file (ex/ `addons.md`) + api_doc_source: string; // When a said API section got added (in which version(s) of Node.js) added_in: string | Array | undefined; // When a said API section got removed (in which version(s) of Node.js) From a51a4926ff00ac1bab98dc960d7d975286aa46b8 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:09:21 -0700 Subject: [PATCH 02/18] legacy-json-all, some cleanup Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json-all/index.mjs | 41 ++- src/generators/legacy-json/index.mjs | 27 +- .../legacy-json/utils/buildContent.mjs | 202 -------------- .../legacy-json/utils/buildSection.mjs | 259 ++++++++++++++++++ 4 files changed, 316 insertions(+), 213 deletions(-) delete mode 100644 src/generators/legacy-json/utils/buildContent.mjs create mode 100644 src/generators/legacy-json/utils/buildSection.mjs diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index 37a51d5..6327085 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -1,7 +1,10 @@ 'use strict'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; + /** - * @typedef {Array} Input + * @typedef {Array} Input * * @type {import('../types.d.ts').GeneratorMetadata} */ @@ -15,5 +18,39 @@ export default { dependsOn: 'legacy-json', - async generate(input) {}, + async generate(input, { output }) { + /** + * @type {import('./types.d.ts').Output} + */ + const generatedValue = { + miscs: [], + modules: [], + classes: [], + globals: [], + methods: [], + }; + + for (const section of input) { + // Copy the relevant properties from each section into our output + for (const property of [ + 'miscs', + 'modules', + 'classes', + 'globals', + 'methods', + ]) { + if (section[property]) { + generatedValue[property].push(...section[property]); + } + } + } + + await writeFile( + join(output, 'all.json'), + JSON.stringify(generatedValue), + 'utf8' + ); + + return generatedValue; + }, }; diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index a44171b..a55aa84 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -3,12 +3,20 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { groupNodesByModule } from '../../utils/generators.mjs'; -import buildContent from './utils/buildContent.mjs'; +import buildSection from './utils/buildSection.mjs'; /** + * This generator is responsible for generating the legacy JSON files for the + * legacy API docs for retro-compatibility. It is to be replaced while we work + * on the new schema for this file. + * + * This is a top-level generator, intaking the raw AST tree of the api docs. + * It generates JSON files to the specified output directory given by the + * config. + * * @typedef {Array} Input * - * @type {import('../types.d.ts').GeneratorMetadata} + * @type {import('../types.d.ts').GeneratorMetadata} */ export default { name: 'legacy-json', @@ -25,7 +33,7 @@ export default { const groupedModules = groupNodesByModule(input); - // Gets the first nodes of each module, which is considered the "head" of the module + // Gets the first nodes of each module, which is considered the "head" const headNodes = input.filter(node => node.heading.depth === 1); /** @@ -35,19 +43,20 @@ export default { const processModuleNodes = head => { const nodes = groupedModules.get(head.api); - const parsedContent = buildContent(head, nodes); - generatedValues.push(parsedContent); + const section = buildSection(head, nodes); + generatedValues.push(section); - return parsedContent; + return section; }; for (const node of headNodes) { - const result = processModuleNodes(node); - // console.log(result) + // Get the json for the node's section + const section = processModuleNodes(node); + // Write it to the output file await writeFile( join(output, `${node.api}.json`), - JSON.stringify(result), + JSON.stringify(section), 'utf8' ); } diff --git a/src/generators/legacy-json/utils/buildContent.mjs b/src/generators/legacy-json/utils/buildContent.mjs deleted file mode 100644 index 661e575..0000000 --- a/src/generators/legacy-json/utils/buildContent.mjs +++ /dev/null @@ -1,202 +0,0 @@ -import { buildHierarchy } from './buildHierarchy.mjs'; - -const sectionTypePlurals = { - module: 'modules', - misc: 'miscs', - class: 'classes', - method: 'methods', - property: 'properties', - global: 'globals', - example: 'examples', - ctor: 'ctors', - classMethod: 'classMethods', - event: 'events', - var: 'vars', -}; - -/** - * - * @param {HeadingMetadataParent} entry - * @returns {import('../types.d.ts').Section} - */ -function createSection(entry) { - const text = textJoin(entry.children); - - // TODO check if type or name can be undefined - return { - textRaw: text, - type: entry.data.type, - name: entry.data.name, - }; -} - -/** - * - * @param {*} values TODO type - * @returns {import('../types.d.ts').MethodSignature} - */ -function parseSignature(values) { - /** - * @type {import('../types.d.ts').MethodSignature} - */ - const signature = {}; - - signature.params = values.filter(value => { - if (value.name === 'return') { - signature.return = value; - return false; - } - - return true; - }); - - return signature; -} - -/** - * - * @param {import('mdast').PhrasingContent[]} nodes - */ -function textJoin(nodes) { - return nodes - .map(node => { - switch (node.type) { - case 'linkReference': - console.error(`todo link reference`); - return `TODO`; - case `strong`: - return `**${textJoin(node.children)}**`; - case `emphasis`: - return `_${textJoin(node.children)}_`; - default: - if (node.children) { - return textJoin(node.children); - } - - return node.value; - } - }) - .join(''); -} - -/** - * @param {import('../types.d.ts').HierarchizedEntry} entry - * @returns {import('../types.d.ts').Meta | undefined} - */ -function createMeta(entry) { - const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); - - const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; - if (added_in || n_api_version || deprecated_in || removed_in) { - return { - changes, - added: makeArrayIfNotAlready(added_in), - napiVersion: makeArrayIfNotAlready(n_api_version), - deprecated: makeArrayIfNotAlready(deprecated_in), - removed: makeArrayIfNotAlready(removed_in), - }; - } - - return undefined; -} - -function parseListItem() { - return { type: 'asd' }; -} - -/** - * - * @param {import('../types.d.ts').HierarchizedEntry} entry - * @param {import('../types.d.ts').Section} parentSection - */ -function handleEntry(entry, parentSection) { - let [headingNode, ...nodes] = structuredClone(entry.content.children); - - const section = createSection(headingNode); - section.meta = createMeta(entry); - - const pluralType = sectionTypePlurals[section.type]; - if (!(pluralType in parentSection)) { - parentSection[pluralType] = []; - } - parentSection[sectionTypePlurals[section.type]].push(section); - - // Remove metadata not directly inferable from the markdown - nodes.forEach((node, i) => { - if ( - node.type === 'blockquote' && - node.children.length === 1 && - node.children[0].type === 'paragraph' && - nodes.slice(0, i).every(node => node.type === 'list') - ) { - const text = textJoin(node.children[0].children); - const stability = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s.exec(text); - if (stability) { - section.stability = parseInt(stability[1], 10); - section.stabilityText = stability[2].replaceAll('\n', ' ').trim(); - delete nodes[i]; - } - } - }); - - // Compress the node array. - nodes = nodes.filter(() => true); - - const list = nodes.length && nodes[0].type === 'list' ? nodes.shift() : null; - - if (list) { - const values = list ? list.children.map(child => parseListItem(child)) : []; - - switch (section.type) { - case 'ctor': - case 'classMethod': - case 'method': { - const signature = parseSignature(values); - section.signatures = [signature]; - break; - } - case 'property': - break; - case 'event': - section.params = values; - break; - default: - // List wasn't consumed, add it back - nodes.unshift(list); - } - } - - if (nodes.length) { - if (section.desc) { - section.shortDesc = section.desc; - } - - // TODO: parse definitions - } - - // Handle our children if we have them - if (entry.hierarchyChildren) { - entry.hierarchyChildren.forEach(child => handleEntry(child, section)); - } - - // TODO: the cleanup work the current parser does -} - -/** - * @param {ApiDocMetadataEntry} head - * @param {Array} entries - * @returns {import('../types.d.ts').ModuleSection} - */ -export default (head, entries) => { - /** - * @type {import('../types.d.ts').ModuleSection} - */ - const module = { - type: 'module', - source: `doc/api/${head.api_doc_source}`, - }; - - buildHierarchy(entries).forEach(entry => handleEntry(entry, module)); - - return module; -}; diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs new file mode 100644 index 0000000..a788382 --- /dev/null +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -0,0 +1,259 @@ +import { buildHierarchy } from './buildHierarchy.mjs'; + +const sectionTypePlurals = { + module: 'modules', + misc: 'miscs', + class: 'classes', + method: 'methods', + property: 'properties', + global: 'globals', + example: 'examples', + ctor: 'ctors', + classMethod: 'classMethods', + event: 'events', + var: 'vars', +}; + +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry Section's AST entry + * @param {HeadingMetadataParent} head Head node of the entry + * @returns {import('../types.d.ts').Section} + */ +function createSection(entry, head) { + const text = textJoin(head.children); + + // TODO check if type or name can be undefined + return { + textRaw: text, + type: head.data.type, + name: head.data.name, + meta: createMeta(entry), + }; +} + +/** + * + * @param {*} values TODO type + * @returns {import('../types.d.ts').MethodSignature} + */ +function parseSignature(values) { + /** + * @type {import('../types.d.ts').MethodSignature} + */ + const signature = {}; + + signature.params = values.filter(value => { + if (value.name === 'return') { + signature.return = value; + return false; + } + + return true; + }); + + return signature; +} + +/** + * + * @param {import('mdast').PhrasingContent[]} nodes + */ +function textJoin(nodes) { + return nodes + .map(node => { + switch (node.type) { + case 'linkReference': + console.error(`todo link reference`); + return `TODO`; + case `strong`: + return `**${textJoin(node.children)}**`; + case `emphasis`: + return `_${textJoin(node.children)}_`; + default: + if (node.children) { + return textJoin(node.children); + } + + return node.value; + } + }) + .join(''); +} + +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @returns {import('../types.d.ts').Meta | undefined} + */ +function createMeta(entry) { + const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); + + const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; + if (added_in || n_api_version || deprecated_in || removed_in) { + return { + changes, + added: makeArrayIfNotAlready(added_in), + napiVersion: makeArrayIfNotAlready(n_api_version), + deprecated: makeArrayIfNotAlready(deprecated_in), + removed: makeArrayIfNotAlready(removed_in), + }; + } + + return undefined; +} + +function parseListItem() { + return { type: 'asd' }; +} + +/** + * + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @param {import('../types.d.ts').Section} parentSection + */ +function handleEntry(entry, parentSection) { + // Clone the children so we don't mess with any other generators + let [headingNode, ...nodes] = structuredClone(entry.content.children); + + /** + * @returns {import('../types.d.ts').Section} + */ + const setupSection = () => { + // Create the section object with base data we know now + const section = createSection(entry, headingNode); + + // Get the plural type of the section (e.g. 'modules' for type 'module') + const pluralType = sectionTypePlurals[section.type]; + + // Check if our parent section has a array property with the plural type + // already, create it if not + if (!(pluralType in parentSection)) { + parentSection[pluralType] = []; + } + + // Add this section to our parent + parentSection[sectionTypePlurals[section.type]].push(section); + + return section; + }; + + /** + * + * @param {import('../types.d.ts').Section} section + */ + const manageMetadata = section => { + let needsCompressing = false; + + // Remove metadata not directly the inferable from the markdown + nodes.forEach((node, i) => { + if ( + node.type === 'blockquote' && + node.children.length === 1 && + node.children[0].type === 'paragraph' && + nodes.slice(0, i).every(node => node.type === 'list') + ) { + const text = textJoin(node.children[0].children); + const stability = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s.exec(text); + if (stability) { + section.stability = parseInt(stability[1], 10); + section.stabilityText = stability[2].replaceAll('\n', ' ').trim(); + delete nodes[i]; + needsCompressing = true; + } + } + }); + + if (needsCompressing) { + // Compress to remove the holes left by deletions + nodes = nodes.filter(() => true); + } + }; + + /** + * + * @param {import('../types.d.ts').Section} section + */ + const handleFrontingLists = section => { + const list = + nodes.length && nodes[0].type === 'list' ? nodes.shift() : null; + + if (list) { + const values = list + ? list.children.map(child => parseListItem(child)) + : []; + + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': { + const signature = parseSignature(values); + section.signatures = [signature]; + break; + } + case 'property': + break; + case 'event': + section.params = values; + break; + default: + // List wasn't consumed, add it back + nodes.unshift(list); + } + } + }; + + /** + * + * @param {import('../types.d.ts').Section} section + */ + const addDescription = section => { + if (nodes.length === 0) { + return; + } + + if (section.desc) { + section.shortDesc = section.desc; + } + + // TODO parse definitoons + }; + + /** + * + * @param {import('../types.d.ts').Section} section + */ + const handleChildren = section => { + if (!entry.hierarchyChildren) { + return; + } + + entry.hierarchyChildren.forEach(child => handleEntry(child, section)); + }; + + const section = setupSection(); + + manageMetadata(section); + handleFrontingLists(section); + addDescription(section); + handleChildren(section); + + // TODO: the cleanup work the current parser does +} + +/** + * @param {ApiDocMetadataEntry} head + * @param {Array} entries + * @returns {import('../types.d.ts').ModuleSection} + */ +export default (head, entries) => { + /** + * @type {import('../types.d.ts').ModuleSection} + */ + const module = { + type: 'module', + source: `doc/api/${head.api_doc_source}`, + }; + + buildHierarchy(entries).forEach(entry => handleEntry(entry, module)); + + return module; +}; From 6a0d6fbe246f86c54c1463761f53a95024a78a78 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:24:03 -0700 Subject: [PATCH 03/18] Update src/generators/legacy-json-all/index.mjs Co-authored-by: Augustin Mauroy --- src/generators/legacy-json-all/index.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index 6327085..540f9ce 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -1,7 +1,7 @@ 'use strict'; -import { writeFile } from 'fs/promises'; -import { join } from 'path'; +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; /** * @typedef {Array} Input From 08c13655c68563c42cd1bd680365dec41b439c31 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sun, 8 Sep 2024 20:55:45 -0700 Subject: [PATCH 04/18] list item stuff Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json/constants.mjs | 24 ++ src/generators/legacy-json/types.d.ts | 11 + .../legacy-json/utils/buildHierarchy.mjs | 25 +- .../legacy-json/utils/buildSection.mjs | 233 ++++++++++++++---- 4 files changed, 238 insertions(+), 55 deletions(-) create mode 100644 src/generators/legacy-json/constants.mjs diff --git a/src/generators/legacy-json/constants.mjs b/src/generators/legacy-json/constants.mjs new file mode 100644 index 0000000..69bce67 --- /dev/null +++ b/src/generators/legacy-json/constants.mjs @@ -0,0 +1,24 @@ +/** + * Denotes a method's return value + */ +export const RETURN_EXPRESSION = /^returns?\s*:?\s*/i; + +/** + * Denotes a method's name + */ +export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; + +/** + * Denotes a method's type + */ +export const TYPE_EXPRESSION = /^\{([^}]+)\}\s*/; + +/** + * Is there a leading hyphen + */ +export const LEADING_HYPHEN = /^-\s*/; + +/** + * Denotes a default value + */ +export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts index 3c1308d..0f70d81 100644 --- a/src/generators/legacy-json/types.d.ts +++ b/src/generators/legacy-json/types.d.ts @@ -1,3 +1,5 @@ +import { ListItem } from 'mdast'; + export interface HierarchizedEntry extends ApiDocMetadataEntry { hierarchyChildren: ApiDocMetadataEntry[]; } @@ -61,3 +63,12 @@ export interface MiscSection extends SectionBase { type: 'misc'; [key: string]: string | undefined; } + +export interface List { + textRaw: string; + desc?: string; + name: string; + type?: string; + default?: string; + options?: List; +} diff --git a/src/generators/legacy-json/utils/buildHierarchy.mjs b/src/generators/legacy-json/utils/buildHierarchy.mjs index bc063c9..b1ade5e 100644 --- a/src/generators/legacy-json/utils/buildHierarchy.mjs +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -1,9 +1,21 @@ /** * So we need the files to be in a hierarchy based off of depth, but they're - * given to us flattened. So, let's fix that in a way that's incredibly - * unfortunate but works! - * @param {ApiDocMetadataEntry[]} entries - * @returns {import('../types.d.ts').HierarchizedEntry[]} + * given to us flattened. So, let's fix that. + * + * Assuming that {@link entries} is in the same order as the elements are in + * the markdown, we can use the entry's depth property to reassemble the + * hierarchy. + * + * If depth <= 1, it's a top-level element (aka a root). + * + * If it's depth is greater than the previous entry's depth, it's a child of + * the previous entry. Otherwise (if it's less than or equal to the previous + * entry's depth), we need to find the entry that it was the greater than. We + * can do this by just looping through entries in reverse starting at the + * current index - 1. + * + * @param {Array} entries + * @returns {Array} */ export function buildHierarchy(entries) { const roots = []; @@ -12,7 +24,8 @@ export function buildHierarchy(entries) { const entry = entries[i]; const currentDepth = entry.heading.depth; - if (currentDepth === 1) { + // We're a top-level entry + if (currentDepth <= 1) { roots.push(entry); continue; } @@ -27,10 +40,12 @@ export function buildHierarchy(entries) { } previousEntry.hierarchyChildren.push(entry); } else { + // Loop to find the entry we're a child of for (let j = i - 2; j >= 0; j--) { const jEntry = entries[j]; const jDepth = jEntry.heading.depth; if (currentDepth > jDepth) { + // Found it jEntry.hierarchyChildren.push(entry); break; } diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index a788382..1318172 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -1,3 +1,10 @@ +import { + DEFAULT_EXPRESSION, + LEADING_HYPHEN, + NAME_EXPRESSION, + RETURN_EXPRESSION, + TYPE_EXPRESSION, +} from '../constants.mjs'; import { buildHierarchy } from './buildHierarchy.mjs'; const sectionTypePlurals = { @@ -8,12 +15,34 @@ const sectionTypePlurals = { property: 'properties', global: 'globals', example: 'examples', - ctor: 'ctors', + // Constructors should go under a class sections' signatures property + ctor: 'signatures', classMethod: 'classMethods', event: 'events', var: 'vars', }; +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @returns {import('../types.d.ts').Meta | undefined} + */ +function createMeta(entry) { + const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); + + const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; + if (added_in || n_api_version || deprecated_in || removed_in) { + return { + changes, + added: makeArrayIfNotAlready(added_in), + napiVersion: makeArrayIfNotAlready(n_api_version), + deprecated: makeArrayIfNotAlready(deprecated_in), + removed: makeArrayIfNotAlready(removed_in), + }; + } + + return undefined; +} + /** * @param {import('../types.d.ts').HierarchizedEntry} entry Section's AST entry * @param {HeadingMetadataParent} head Head node of the entry @@ -33,7 +62,7 @@ function createSection(entry, head) { /** * - * @param {*} values TODO type + * @param {Array} nodes */ function textJoin(nodes) { return nodes @@ -81,28 +112,67 @@ function textJoin(nodes) { } /** - * @param {import('../types.d.ts').HierarchizedEntry} entry - * @returns {import('../types.d.ts').Meta | undefined} + * Find name, type, default, desc properties + * @param {import('mdast').ListItem} child + * @returns {import('../types.d.ts').List} */ -function createMeta(entry) { - const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); +function parseListItem(child) { + /** + * @type {import('../types.d.ts').List} + */ + const current = {}; - const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; - if (added_in || n_api_version || deprecated_in || removed_in) { - return { - changes, - added: makeArrayIfNotAlready(added_in), - napiVersion: makeArrayIfNotAlready(n_api_version), - deprecated: makeArrayIfNotAlready(deprecated_in), - removed: makeArrayIfNotAlready(removed_in), - }; + // TODO this + // current.textRaw = child.children + // .filter(node => node.type !== 'list') + // .map(node => node.) + + if (current.textRaw) { + throw new Error(`empty list item: ${JSON.stringify(child)}`); } - return undefined; -} + let text = current.textRaw; + + // Extract name + if (RETURN_EXPRESSION.test(text)) { + current.name = 'return'; + text = text.replace(RETURN_EXPRESSION, ''); + } else { + const [, name] = text.match(NAME_EXPRESSION) || []; + if (name) { + current.name = name; + text = text.replace(NAME_EXPRESSION, ''); + } + } + + // Extract type (if provided) + const [, type] = text.match(TYPE_EXPRESSION) || []; + if (type) { + current.type = type; + text = text.replace(TYPE_EXPRESSION, ''); + } + + // Remove leading hyphens + text = text.replace(LEADING_HYPHEN, ''); -function parseListItem() { - return { type: 'asd' }; + // Extract default value (if exists) + const [, defaultValue] = text.match(DEFAULT_EXPRESSION) || []; + if (defaultValue) { + current.default = defaultValue.replace(/\.$/, ''); + text = text.parseListItem(DEFAULT_EXPRESSION, ''); + } + + // Add remaining text to the desc + if (text) { + current.desc = text; + } + + const options = child.children.find(child => child.type === 'list'); + if (options) { + current.options = options.children.map(child => parseListItem(child)); + } + + return current; } /** @@ -137,10 +207,10 @@ function handleEntry(entry, parentSection) { }; /** - * + * Grabs stability number & text and adds it to the section * @param {import('../types.d.ts').Section} section */ - const manageMetadata = section => { + const parseStability = section => { let needsCompressing = false; // Remove metadata not directly the inferable from the markdown @@ -172,37 +242,63 @@ function handleEntry(entry, parentSection) { * * @param {import('../types.d.ts').Section} section */ - const handleFrontingLists = section => { + const parseListIfThereIsOne = section => { const list = nodes.length && nodes[0].type === 'list' ? nodes.shift() : null; + if (!list) { + return; + } + + /** + * @type {Array} + */ + const values = list ? list.children.map(child => parseListItem(child)) : []; + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': { + const signature = parseSignature(values); + section.signatures = [signature]; + + break; + } - if (list) { - const values = list - ? list.children.map(child => parseListItem(child)) - : []; - - switch (section.type) { - case 'ctor': - case 'classMethod': - case 'method': { - const signature = parseSignature(values); - section.signatures = [signature]; + case 'property': { + if (!values.length) { break; } - case 'property': - break; - case 'event': - section.params = values; - break; - default: - // List wasn't consumed, add it back - nodes.unshift(list); + + const signature = values[0]; + signature.textRaw = `\`${section.name}\` ${signature.textRaw}`; + + for (const key in signature) { + if (!signature[key]) { + continue; + } + + if (key === 'type') { + // We'll set propertySigType to type at the end since we still need the + // original type for a few more checks + section.propertySigType = signature.type; + } else { + section[key] = signature[key]; + } + } + + break; } + + case 'event': + section.params = values; + break; + + default: + // List wasn't consumed, add it back + nodes.unshift(list); } }; /** - * * @param {import('../types.d.ts').Section} section */ const addDescription = section => { @@ -218,7 +314,7 @@ function handleEntry(entry, parentSection) { }; /** - * + * Creates the sections for the children of this entry * @param {import('../types.d.ts').Section} section */ const handleChildren = section => { @@ -229,14 +325,51 @@ function handleEntry(entry, parentSection) { entry.hierarchyChildren.forEach(child => handleEntry(child, section)); }; + /** + * @param {import('../types.d.ts').Section} section + * @param {import('../types.d.ts').Section} parentSection + */ + const makeChildrenTopLevelIfMisc = (section, parentSection) => { + if (parentSection.type === 'misc') { + return; + } + + for (const key of Object.keys(section)) { + if (key in ['textRaw', 'name', 'type', 'desc', 'miscs']) { + continue; + } + + if (parentSection[key]) { + if (Array.isArray(parentSection[key])) { + parentSection[key] = parentSection[key].concat(section[key]); + } + } else { + parentSection[key] = section[key]; + } + } + }; + const section = setupSection(); - manageMetadata(section); - handleFrontingLists(section); + parseStability(section); + + parseListIfThereIsOne(section); + addDescription(section); + handleChildren(section); - // TODO: the cleanup work the current parser does + makeChildrenTopLevelIfMisc(section, parentSection); + + if (section.type === 'property') { + if (section.propertySigType) { + section.type = section.propertySigType; + section.propertySigType = undefined; + } else { + // Delete the type here because ??? + section.type = undefined; + } + } } /** @@ -248,12 +381,12 @@ export default (head, entries) => { /** * @type {import('../types.d.ts').ModuleSection} */ - const module = { + const rootModule = { type: 'module', source: `doc/api/${head.api_doc_source}`, }; - buildHierarchy(entries).forEach(entry => handleEntry(entry, module)); + buildHierarchy(entries).forEach(entry => handleEntry(entry, rootModule)); - return module; + return rootModule; }; From e3c8640083d01caf0e7a980d9e556edefa377740 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sun, 8 Sep 2024 21:04:52 -0700 Subject: [PATCH 05/18] some types Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json/types.d.ts | 27 +++++++++++++------ .../legacy-json/utils/buildSection.mjs | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts index 0f70d81..47f8ba6 100644 --- a/src/generators/legacy-json/types.d.ts +++ b/src/generators/legacy-json/types.d.ts @@ -26,16 +26,22 @@ export interface SectionBase { desc: string; shortDesc?: string; stability?: number; - stabilityText?: string; // TODO find the type for this + stabilityText?: string; meta?: Meta; } export interface ModuleSection extends SectionBase { type: 'module'; source: string; - miscs: MiscSection[]; - modules: ModuleSection[]; - // TODO ... + miscs?: MiscSection[]; + modules?: ModuleSection[]; + classes?: SignatureSection[]; + methods?: MethodSignature[]; + properties?: PropertySection[]; + globals?: object[]; // TODO + examples?: object[]; // TODO + signatures?: SignatureSection[]; + // TODO the rest } export interface SignatureSection extends SectionBase { @@ -43,20 +49,25 @@ export interface SignatureSection extends SectionBase { signatures: MethodSignature[]; } +export interface Parameter { + name: string; + optional?: boolean; + default?: string; +} + export interface MethodSignature { - params: object[]; // TODO - return?: object; // TODO + params: Parameter[]; + return?: string; } export interface PropertySection extends SectionBase { type: 'property'; - typeof?: string; [key: string]: string | undefined; } export interface EventSection extends SectionBase { type: 'event'; - params: object[]; // TODO + params: ListItem[]; } export interface MiscSection extends SectionBase { diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 1318172..06f45b0 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -127,7 +127,7 @@ function parseListItem(child) { // .filter(node => node.type !== 'list') // .map(node => node.) - if (current.textRaw) { + if (!current.textRaw) { throw new Error(`empty list item: ${JSON.stringify(child)}`); } From b95dafbd091b042ace768045f8dbe0492e830984 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sun, 8 Sep 2024 21:06:44 -0700 Subject: [PATCH 06/18] fix tests Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/test/metadata.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index 3612f12..948993e 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -66,6 +66,7 @@ describe('createMetadata', () => { const expected = { added_in: undefined, api: 'test', + api_doc_source: 'test.md', changes: [], content: section, deprecated_in: undefined, From 0fa285d87a341015614cd478ab535102e91f3865 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:23:45 -0700 Subject: [PATCH 07/18] parseSignature impl Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json-all/types.d.ts | 2 +- src/generators/legacy-json/constants.mjs | 6 + src/generators/legacy-json/types.d.ts | 4 +- .../legacy-json/utils/buildHierarchy.mjs | 6 +- .../legacy-json/utils/buildSection.mjs | 169 +++++++++++++++--- 5 files changed, 158 insertions(+), 29 deletions(-) diff --git a/src/generators/legacy-json-all/types.d.ts b/src/generators/legacy-json-all/types.d.ts index daab11d..181eabc 100644 --- a/src/generators/legacy-json-all/types.d.ts +++ b/src/generators/legacy-json-all/types.d.ts @@ -4,6 +4,6 @@ export interface Output { miscs: MiscSection[]; modules: Section[]; classes: SignatureSection[]; - globals: object[]; // TODO + globals: (ModuleSection | { type: 'global' })[]; methods: SignatureSection[]; } diff --git a/src/generators/legacy-json/constants.mjs b/src/generators/legacy-json/constants.mjs index 69bce67..ebde3d9 100644 --- a/src/generators/legacy-json/constants.mjs +++ b/src/generators/legacy-json/constants.mjs @@ -22,3 +22,9 @@ export const LEADING_HYPHEN = /^-\s*/; * Denotes a default value */ export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; + +/** + * Grabs the parameters from a method's signature + * @example 'new buffer.Blob([sources[, options]])'.match(PARAM_EXPRESSION) = ['([sources[, options]])', '[sources[, options]]'] + */ +export const PARAM_EXPRESSION = /\((.+)\);?$/; diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts index 47f8ba6..5fb174d 100644 --- a/src/generators/legacy-json/types.d.ts +++ b/src/generators/legacy-json/types.d.ts @@ -38,10 +38,8 @@ export interface ModuleSection extends SectionBase { classes?: SignatureSection[]; methods?: MethodSignature[]; properties?: PropertySection[]; - globals?: object[]; // TODO - examples?: object[]; // TODO + globals?: ModuleSection | { type: 'global' }; signatures?: SignatureSection[]; - // TODO the rest } export interface SignatureSection extends SectionBase { diff --git a/src/generators/legacy-json/utils/buildHierarchy.mjs b/src/generators/legacy-json/utils/buildHierarchy.mjs index b1ade5e..eb90371 100644 --- a/src/generators/legacy-json/utils/buildHierarchy.mjs +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -1,5 +1,5 @@ /** - * So we need the files to be in a hierarchy based off of depth, but they're + * We need the files to be in a hierarchy based off of depth, but they're * given to us flattened. So, let's fix that. * * Assuming that {@link entries} is in the same order as the elements are in @@ -24,8 +24,8 @@ export function buildHierarchy(entries) { const entry = entries[i]; const currentDepth = entry.heading.depth; - // We're a top-level entry if (currentDepth <= 1) { + // We're a top-level entry roots.push(entry); continue; } @@ -38,12 +38,14 @@ export function buildHierarchy(entries) { if (previousEntry.hierarchyChildren === undefined) { previousEntry.hierarchyChildren = []; } + previousEntry.hierarchyChildren.push(entry); } else { // Loop to find the entry we're a child of for (let j = i - 2; j >= 0; j--) { const jEntry = entries[j]; const jDepth = jEntry.heading.depth; + if (currentDepth > jDepth) { // Found it jEntry.hierarchyChildren.push(entry); diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 06f45b0..0e37d17 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -2,6 +2,7 @@ import { DEFAULT_EXPRESSION, LEADING_HYPHEN, NAME_EXPRESSION, + PARAM_EXPRESSION, RETURN_EXPRESSION, TYPE_EXPRESSION, } from '../constants.mjs'; @@ -33,10 +34,14 @@ function createMeta(entry) { if (added_in || n_api_version || deprecated_in || removed_in) { return { changes, - added: makeArrayIfNotAlready(added_in), - napiVersion: makeArrayIfNotAlready(n_api_version), - deprecated: makeArrayIfNotAlready(deprecated_in), - removed: makeArrayIfNotAlready(removed_in), + added: added_in ? makeArrayIfNotAlready(added_in) : undefined, + napiVersion: n_api_version + ? makeArrayIfNotAlready(n_api_version) + : undefined, + deprecated: deprecated_in + ? makeArrayIfNotAlready(deprecated_in) + : undefined, + removed: removed_in ? makeArrayIfNotAlready(removed_in) : undefined, }; } @@ -51,27 +56,30 @@ function createMeta(entry) { function createSection(entry, head) { const text = textJoin(head.children); - // TODO check if type or name can be undefined return { textRaw: text, type: head.data.type, - name: head.data.name, + name: text.toLowerCase().replaceAll(' ', '_'), + displayName: head.data.name, meta: createMeta(entry), + introduced_in: entry.introduced_in, }; } /** - * - * @param {Array { + const rawParameters = values.filter(value => { if (value.name === 'return') { signature.return = value; return false; @@ -80,7 +88,127 @@ function parseSignature(values) { return true; }); - // TODO the unfortunate logic + /** + * Extract a list of the signatures from the method's declaration + * @example `[sources[, options]]` + */ + let [, declaredParameters] = textRaw.match(PARAM_EXPRESSION) || []; + + if (!declaredParameters) { + return; + } + + /** + * @type {string[]} + * @example ['sources[,', 'options]]'] + */ + declaredParameters = declaredParameters.split(','); + + let optionalDepth = 0; + const optionalCharDict = { '[': 1, ' ': 0, ']': -1 }; + + declaredParameters.forEach((declaredParameter, i) => { + /** + * @example 'length]]' + * @example 'arrayBuffer[' + * @example '[sources[' + * @example 'end' + */ + declaredParameter = declaredParameter.trim(); + + if (!declaredParameter) { + throw new Error(`Empty parameter slot: ${textRaw}`); + } + + // We need to find out if this parameter is optional or not. We can tell this + // if we're wrapped in brackets, so let's look for them. + + let pos; + for (pos = 0; pos < declaredParameter.length; pos++) { + const levelChange = optionalCharDict[declaredParameter[pos]]; + + if (levelChange === undefined) { + break; + } + + optionalDepth += levelChange; + } + + // Cut off any trailing brackets + declaredParameter = declaredParameter.substring(pos); + + const isParameterOptional = optionalDepth > 0; + + for (pos = declaredParameter.length - 1; pos >= 0; pos--) { + const levelChange = optionalCharDict[declaredParameter[pos]]; + + if (levelChange === undefined) { + break; + } + + optionalDepth += levelChange; + } + + // Cut off any leading brackets + declaredParameter = declaredParameter.substring(0, pos + 1); + + // Default value of this parameter in the method's declaration + let defaultValue; + + const equalSignPos = declaredParameter.indexOf('='); + if (equalSignPos !== -1) { + // We have a default value, save it and then cut it off of the signature + defaultValue = declaredParameter.substring(equalSignPos, 1).trim(); + declaredParameter = declaredParameter.substring(0, equalSignPos); + console.log('eq', declaredParameter); + } + + let parameter = rawParameters[i]; + if (!parameter || declaredParameter !== parameter.name) { + // If we're here then the method likely has shared signatures + // Something like, `new Console(stdout[, stderr][, ignoreErrors])` and + // `new Console(options)` + parameter = undefined; + + // Try finding a parameter this is being shared with + for (const otherParam of rawParameters) { + if (declaredParameter === otherParam.name) { + // Found a matching one + // TODO break? + parameter = otherParam; + } else if (otherParam.options) { + // Found a matching one in the parameter's options + for (const option of otherParam.options) { + if (declaredParameter === option.name) { + parameter = Object.assign({}, option); + break; + } + } + } + } + + if (!parameter) { + // Couldn't find the shared one, I have no idea what this case is but we'll see + if (declaredParameter.startsWith('...')) { + parameter = { name: declaredParameter }; + } else { + throw new Error( + `Invalid param "${declaredParameter}"\n` + ` > ${textRaw}` + ); + } + } + } + + if (isParameterOptional) { + parameter.optional = true; + } + + if (defaultValue) { + parameter.default = defaultValue; + } + + signature.params.push(parameter); + }); return signature; } @@ -93,9 +221,6 @@ function textJoin(nodes) { return nodes .map(node => { switch (node.type) { - case 'linkReference': - console.error(`todo link reference`); - return `TODO`; case `strong`: return `**${textJoin(node.children)}**`; case `emphasis`: @@ -122,10 +247,9 @@ function parseListItem(child) { */ const current = {}; - // TODO this - // current.textRaw = child.children - // .filter(node => node.type !== 'list') - // .map(node => node.) + current.textRaw = textJoin( + child.children.filter(node => node.type !== 'list') + ); if (!current.textRaw) { throw new Error(`empty list item: ${JSON.stringify(child)}`); @@ -159,7 +283,7 @@ function parseListItem(child) { const [, defaultValue] = text.match(DEFAULT_EXPRESSION) || []; if (defaultValue) { current.default = defaultValue.replace(/\.$/, ''); - text = text.parseListItem(DEFAULT_EXPRESSION, ''); + text = text.replace(DEFAULT_EXPRESSION, ''); } // Add remaining text to the desc @@ -257,8 +381,7 @@ function handleEntry(entry, parentSection) { case 'ctor': case 'classMethod': case 'method': { - const signature = parseSignature(values); - section.signatures = [signature]; + section.signatures = [parseSignature(section.textRaw, values)]; break; } @@ -330,7 +453,7 @@ function handleEntry(entry, parentSection) { * @param {import('../types.d.ts').Section} parentSection */ const makeChildrenTopLevelIfMisc = (section, parentSection) => { - if (parentSection.type === 'misc') { + if (parentSection.type !== 'misc') { return; } From 14440563ef0440be088730e46d3e30797973c9b5 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:25:23 -0700 Subject: [PATCH 08/18] descriptions Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- package-lock.json | 33 +++++++++++++ package.json | 1 + .../legacy-json/utils/buildSection.mjs | 49 ++++++++++++++----- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21681eb..0f1063b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "html-minifier-terser": "^7.2.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", + "remark-html": "^16.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-stringify": "^11.0.0", @@ -1388,6 +1389,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-sanitize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz", + "integrity": "sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.2.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", @@ -3255,6 +3271,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-html": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-16.0.1.tgz", + "integrity": "sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "hast-util-sanitize": "^5.0.0", + "hast-util-to-html": "^9.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", diff --git a/package.json b/package.json index ad2e1d2..19f703e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "html-minifier-terser": "^7.2.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", + "remark-html": "^16.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-stringify": "^11.0.0", diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 0e37d17..8a43fd2 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -1,3 +1,5 @@ +import { unified } from 'unified'; +import html from 'remark-html'; import { DEFAULT_EXPRESSION, LEADING_HYPHEN, @@ -160,7 +162,6 @@ function parseSignature(textRaw, values) { // We have a default value, save it and then cut it off of the signature defaultValue = declaredParameter.substring(equalSignPos, 1).trim(); declaredParameter = declaredParameter.substring(0, equalSignPos); - console.log('eq', declaredParameter); } let parameter = rawParameters[i]; @@ -188,7 +189,7 @@ function parseSignature(textRaw, values) { } if (!parameter) { - // Couldn't find the shared one, I have no idea what this case is but we'll see + // Couldn't find the shared one if (declaredParameter.startsWith('...')) { parameter = { name: declaredParameter }; } else { @@ -221,10 +222,13 @@ function textJoin(nodes) { return nodes .map(node => { switch (node.type) { - case `strong`: + case 'strong': return `**${textJoin(node.children)}**`; - case `emphasis`: + case 'emphasis': return `_${textJoin(node.children)}_`; + case 'link': { + return `[${node.label}][]`; + } default: if (node.children) { return textJoin(node.children); @@ -249,7 +253,9 @@ function parseListItem(child) { current.textRaw = textJoin( child.children.filter(node => node.type !== 'list') - ); + ) + .replace(/\s+/g, ' ') + .replace(//gs, ''); if (!current.textRaw) { throw new Error(`empty list item: ${JSON.stringify(child)}`); @@ -260,6 +266,11 @@ function parseListItem(child) { // Extract name if (RETURN_EXPRESSION.test(text)) { current.name = 'return'; + + let [, returnType] = text.match(/`(.*?)`/); + returnType = returnType.substring(1, returnType.length - 1); + current.type = returnType; + text = text.replace(RETURN_EXPRESSION, ''); } else { const [, name] = text.match(NAME_EXPRESSION) || []; @@ -267,13 +278,13 @@ function parseListItem(child) { current.name = name; text = text.replace(NAME_EXPRESSION, ''); } - } - // Extract type (if provided) - const [, type] = text.match(TYPE_EXPRESSION) || []; - if (type) { - current.type = type; - text = text.replace(TYPE_EXPRESSION, ''); + // Extract type (if provided) + const [, type] = text.match(TYPE_EXPRESSION) || []; + if (type) { + current.type = type; + text = text.replace(TYPE_EXPRESSION, ''); + } } // Remove leading hyphens @@ -350,6 +361,7 @@ function handleEntry(entry, parentSection) { if (stability) { section.stability = parseInt(stability[1], 10); section.stabilityText = stability[2].replaceAll('\n', ' ').trim(); + delete nodes[i]; needsCompressing = true; } @@ -433,7 +445,20 @@ function handleEntry(entry, parentSection) { section.shortDesc = section.desc; } - // TODO parse definitoons + // Render the description as if it was html + section.desc = unified() + .use(function () { + this.Parser = () => ({ type: 'root', children: nodes }); + }) + .use(html, { sanitize: false }) + .processSync('') + .toString() + .trim(); + + if (!section.desc) { + // Rendering returned nothing + delete section.desc; + } }; /** From 5a61eb77b48ec57ab71476418eb3440f9387f6dc Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:17:43 -0700 Subject: [PATCH 09/18] fix sanitization alert Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json/utils/buildSection.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 8a43fd2..3323b1a 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -255,7 +255,7 @@ function parseListItem(child) { child.children.filter(node => node.type !== 'list') ) .replace(/\s+/g, ' ') - .replace(//gs, ''); + .replaceAll(//gs, ''); if (!current.textRaw) { throw new Error(`empty list item: ${JSON.stringify(child)}`); From 5d084b48ae1634abf2d5a31a66c4913eb372da5f Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:55:35 -0700 Subject: [PATCH 10/18] code review Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json-all/index.mjs | 16 +++++++++------- .../legacy-json/utils/buildHierarchy.mjs | 4 ++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index 540f9ce..5b7d1e6 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -30,15 +30,17 @@ export default { methods: [], }; + const propertiesToCopy = [ + 'miscs', + 'modules', + 'classes', + 'globals', + 'methods', + ]; + for (const section of input) { // Copy the relevant properties from each section into our output - for (const property of [ - 'miscs', - 'modules', - 'classes', - 'globals', - 'methods', - ]) { + for (const property of propertiesToCopy) { if (section[property]) { generatedValue[property].push(...section[property]); } diff --git a/src/generators/legacy-json/utils/buildHierarchy.mjs b/src/generators/legacy-json/utils/buildHierarchy.mjs index eb90371..020f1d3 100644 --- a/src/generators/legacy-json/utils/buildHierarchy.mjs +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -41,6 +41,10 @@ export function buildHierarchy(entries) { previousEntry.hierarchyChildren.push(entry); } else { + if (i < 2) { + throw new Error(`can't find parent since i < 2 (${i})`); + } + // Loop to find the entry we're a child of for (let j = i - 2; j >= 0; j--) { const jEntry = entries[j]; From 5418f0195bd984f271d5fb574de2927f38fb521a Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:21:43 -0700 Subject: [PATCH 11/18] Apply suggestions from code review Co-authored-by: Caner Akdas --- src/generators/legacy-json-all/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index 5b7d1e6..f85350b 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -4,7 +4,7 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; /** - * @typedef {Array} Input + * @typedef {Array} Input * * @type {import('../types.d.ts').GeneratorMetadata} */ From 8e500473900c44cba36623f2e85f1e1a472634f1 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:25:33 -0700 Subject: [PATCH 12/18] change comment style Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json/constants.mjs | 26 +++++++----------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/generators/legacy-json/constants.mjs b/src/generators/legacy-json/constants.mjs index ebde3d9..b999dea 100644 --- a/src/generators/legacy-json/constants.mjs +++ b/src/generators/legacy-json/constants.mjs @@ -1,30 +1,18 @@ -/** - * Denotes a method's return value - */ +// Grabs a method's return value export const RETURN_EXPRESSION = /^returns?\s*:?\s*/i; -/** - * Denotes a method's name - */ +// Grabs a method's name export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; -/** - * Denotes a method's type - */ +// Denotes a method's type export const TYPE_EXPRESSION = /^\{([^}]+)\}\s*/; -/** - * Is there a leading hyphen - */ +// Checks if there's a leading hyphen export const LEADING_HYPHEN = /^-\s*/; -/** - * Denotes a default value - */ +// Grabs the default value if present export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; -/** - * Grabs the parameters from a method's signature - * @example 'new buffer.Blob([sources[, options]])'.match(PARAM_EXPRESSION) = ['([sources[, options]])', '[sources[, options]]'] - */ +// Grabs the parameters from a method's signature +// ex/ 'new buffer.Blob([sources[, options]])'.match(PARAM_EXPRESSION) === ['([sources[, options]])', '[sources[, options]]'] export const PARAM_EXPRESSION = /\((.+)\);?$/; From ee13845d3fd918cbd87b3914aa94b1509c41dd17 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:41:19 -0700 Subject: [PATCH 13/18] cleanup comment and a regex error Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json/utils/buildSection.mjs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 3323b1a..c35283a 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -215,7 +215,6 @@ function parseSignature(textRaw, values) { } /** - * * @param {Array} nodes */ function textJoin(nodes) { @@ -266,10 +265,13 @@ function parseListItem(child) { // Extract name if (RETURN_EXPRESSION.test(text)) { current.name = 'return'; - - let [, returnType] = text.match(/`(.*?)`/); - returnType = returnType.substring(1, returnType.length - 1); - current.type = returnType; + // console.log(text) + let matchResult = text.match(/`(.*?)`/); + if (matchResult) { + let returnType = matchResult[1]; + returnType = returnType.substring(1, returnType.length - 1); + current.type = returnType; + } text = text.replace(RETURN_EXPRESSION, ''); } else { @@ -311,7 +313,6 @@ function parseListItem(child) { } /** - * * @param {import('../types.d.ts').HierarchizedEntry} entry * @param {import('../types.d.ts').Section} parentSection */ From ffdfe066a6a0c47fbddd0651f4beb32a9311617c Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:04:08 -0700 Subject: [PATCH 14/18] Update src/generators/legacy-json/index.mjs Co-authored-by: Augustin Mauroy --- src/generators/legacy-json/index.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index a55aa84..df102a2 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -49,17 +49,17 @@ export default { return section; }; - for (const node of headNodes) { + await Promise.all(headNodes.map(async node => { // Get the json for the node's section const section = processModuleNodes(node); - + // Write it to the output file await writeFile( join(output, `${node.api}.json`), JSON.stringify(section), 'utf8' ); - } + })); return generatedValues; }, From 360786da817e2e0ab1a16b1c102994f6ba5d9341 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:19:51 -0700 Subject: [PATCH 15/18] some fixes Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json/index.mjs | 24 ++++++++++--------- .../legacy-json/utils/buildSection.mjs | 10 ++++---- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index df102a2..8ed7e61 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -49,17 +49,19 @@ export default { return section; }; - await Promise.all(headNodes.map(async node => { - // Get the json for the node's section - const section = processModuleNodes(node); - - // Write it to the output file - await writeFile( - join(output, `${node.api}.json`), - JSON.stringify(section), - 'utf8' - ); - })); + await Promise.all( + headNodes.map(async node => { + // Get the json for the node's section + const section = processModuleNodes(node); + + // Write it to the output file + await writeFile( + join(output, `${node.api}.json`), + JSON.stringify(section), + 'utf8' + ); + }) + ); return generatedValues; }, diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index c35283a..e6105a6 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -94,10 +94,12 @@ function parseSignature(textRaw, values) { * Extract a list of the signatures from the method's declaration * @example `[sources[, options]]` */ - let [, declaredParameters] = textRaw.match(PARAM_EXPRESSION) || []; + let [, declaredParameters] = `\`${textRaw}\``.match(PARAM_EXPRESSION) || []; if (!declaredParameters) { - return; + signature.params = rawParameters; + + return signature; } /** @@ -175,8 +177,8 @@ function parseSignature(textRaw, values) { for (const otherParam of rawParameters) { if (declaredParameter === otherParam.name) { // Found a matching one - // TODO break? parameter = otherParam; + break; } else if (otherParam.options) { // Found a matching one in the parameter's options for (const option of otherParam.options) { @@ -265,7 +267,7 @@ function parseListItem(child) { // Extract name if (RETURN_EXPRESSION.test(text)) { current.name = 'return'; - // console.log(text) + let matchResult = text.match(/`(.*?)`/); if (matchResult) { let returnType = matchResult[1]; From 858d77f50072af4a5f68873f5cd40eb6d57141b9 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:28:12 -0700 Subject: [PATCH 16/18] expose introduced_in in metadata create Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/metadata.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/metadata.mjs b/src/metadata.mjs index 4064b8e..dab402a 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -113,6 +113,7 @@ const createMetadata = slugger => { const { type, + introduced_in, added, deprecated, removed, @@ -137,8 +138,10 @@ const createMetadata = slugger => { internalMetadata.stability.toJSON = () => internalMetadata.stability.children.map(node => node.data); - // Returns the Metadata entry for the API doc - return { + /** + * @type {ApiDocMetadataEntry} + */ + const value = { api: apiDoc.stem, slug: sectionSlug, source_link, @@ -154,6 +157,13 @@ const createMetadata = slugger => { content: section, tags, }; + + if (introduced_in) { + value.introduced_in = introduced_in; + } + + // Returns the Metadata entry for the API doc + return value; }, }; }; From a27d69b50f65a405e0dc89e70ebce151ef23a847 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:07:57 -0700 Subject: [PATCH 17/18] tmp Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators/legacy-json-all/index.mjs | 8 +- src/generators/legacy-json-all/types.d.ts | 17 +- src/generators/legacy-json/types.d.ts | 42 ++-- .../legacy-json/utils/buildSection.mjs | 177 ++------------- .../legacy-json/utils/parseSignature.mjs | 210 ++++++++++++++++++ src/metadata.mjs | 2 +- src/test/metadata.test.mjs | 2 +- src/types.d.ts | 2 +- 8 files changed, 262 insertions(+), 198 deletions(-) create mode 100644 src/generators/legacy-json/utils/parseSignature.mjs diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index f85350b..f596ef8 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -38,14 +38,14 @@ export default { 'methods', ]; - for (const section of input) { + input.forEach(section => { // Copy the relevant properties from each section into our output - for (const property of propertiesToCopy) { + propertiesToCopy.forEach(property => { if (section[property]) { generatedValue[property].push(...section[property]); } - } - } + }); + }); await writeFile( join(output, 'all.json'), diff --git a/src/generators/legacy-json-all/types.d.ts b/src/generators/legacy-json-all/types.d.ts index 181eabc..0748a31 100644 --- a/src/generators/legacy-json-all/types.d.ts +++ b/src/generators/legacy-json-all/types.d.ts @@ -1,9 +1,14 @@ -import { MiscSection, Section, SignatureSection } from '../legacy-json/types'; +import { + MiscSection, + Section, + SignatureSection, + ModuleSection, +} from '../legacy-json/types'; export interface Output { - miscs: MiscSection[]; - modules: Section[]; - classes: SignatureSection[]; - globals: (ModuleSection | { type: 'global' })[]; - methods: SignatureSection[]; + miscs: Array; + modules: Array
; + classes: Array; + globals: Array; + methods: Array; } diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts index 5fb174d..9518015 100644 --- a/src/generators/legacy-json/types.d.ts +++ b/src/generators/legacy-json/types.d.ts @@ -1,21 +1,15 @@ import { ListItem } from 'mdast'; export interface HierarchizedEntry extends ApiDocMetadataEntry { - hierarchyChildren: ApiDocMetadataEntry[]; + hierarchyChildren: Array; } -export type Section = - | SignatureSection - | PropertySection - | EventSection - | MiscSection; - export interface Meta { - changes: ApiDocMetadataChange[]; - added?: string[]; - napiVersion?: string[]; - deprecated?: string[]; - removed?: string[]; + changes: Array; + added?: Array; + napiVersion?: Array; + deprecated?: Array; + removed?: Array; } export interface SectionBase { @@ -33,20 +27,26 @@ export interface SectionBase { export interface ModuleSection extends SectionBase { type: 'module'; source: string; - miscs?: MiscSection[]; - modules?: ModuleSection[]; - classes?: SignatureSection[]; - methods?: MethodSignature[]; - properties?: PropertySection[]; + miscs?: Array; + modules?: Array; + classes?: Array; + methods?: Array; + properties?: Array; globals?: ModuleSection | { type: 'global' }; - signatures?: SignatureSection[]; + signatures?: Array; } export interface SignatureSection extends SectionBase { type: 'class' | 'ctor' | 'classMethod' | 'method'; - signatures: MethodSignature[]; + signatures: Array; } +export type Section = + | SignatureSection + | PropertySection + | EventSection + | MiscSection; + export interface Parameter { name: string; optional?: boolean; @@ -54,7 +54,7 @@ export interface Parameter { } export interface MethodSignature { - params: Parameter[]; + params: Array; return?: string; } @@ -65,7 +65,7 @@ export interface PropertySection extends SectionBase { export interface EventSection extends SectionBase { type: 'event'; - params: ListItem[]; + params: Array; } export interface MiscSection extends SectionBase { diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index e6105a6..d92840a 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -9,6 +9,7 @@ import { TYPE_EXPRESSION, } from '../constants.mjs'; import { buildHierarchy } from './buildHierarchy.mjs'; +import parseSignature from './parseSignature.mjs'; const sectionTypePlurals = { module: 'modules', @@ -33,21 +34,17 @@ function createMeta(entry) { const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; - if (added_in || n_api_version || deprecated_in || removed_in) { - return { - changes, - added: added_in ? makeArrayIfNotAlready(added_in) : undefined, - napiVersion: n_api_version - ? makeArrayIfNotAlready(n_api_version) - : undefined, - deprecated: deprecated_in - ? makeArrayIfNotAlready(deprecated_in) - : undefined, - removed: removed_in ? makeArrayIfNotAlready(removed_in) : undefined, - }; - } - - return undefined; + return { + changes, + added: added_in ? makeArrayIfNotAlready(added_in) : undefined, + napiVersion: n_api_version + ? makeArrayIfNotAlready(n_api_version) + : undefined, + deprecated: deprecated_in + ? makeArrayIfNotAlready(deprecated_in) + : undefined, + removed: removed_in ? makeArrayIfNotAlready(removed_in) : undefined, + }; } /** @@ -68,154 +65,6 @@ function createSection(entry, head) { }; } -/** - * @param {string} textRaw Something like `new buffer.Blob([sources[, options]])` - * @param {Array { - if (value.name === 'return') { - signature.return = value; - return false; - } - - return true; - }); - - /** - * Extract a list of the signatures from the method's declaration - * @example `[sources[, options]]` - */ - let [, declaredParameters] = `\`${textRaw}\``.match(PARAM_EXPRESSION) || []; - - if (!declaredParameters) { - signature.params = rawParameters; - - return signature; - } - - /** - * @type {string[]} - * @example ['sources[,', 'options]]'] - */ - declaredParameters = declaredParameters.split(','); - - let optionalDepth = 0; - const optionalCharDict = { '[': 1, ' ': 0, ']': -1 }; - - declaredParameters.forEach((declaredParameter, i) => { - /** - * @example 'length]]' - * @example 'arrayBuffer[' - * @example '[sources[' - * @example 'end' - */ - declaredParameter = declaredParameter.trim(); - - if (!declaredParameter) { - throw new Error(`Empty parameter slot: ${textRaw}`); - } - - // We need to find out if this parameter is optional or not. We can tell this - // if we're wrapped in brackets, so let's look for them. - - let pos; - for (pos = 0; pos < declaredParameter.length; pos++) { - const levelChange = optionalCharDict[declaredParameter[pos]]; - - if (levelChange === undefined) { - break; - } - - optionalDepth += levelChange; - } - - // Cut off any trailing brackets - declaredParameter = declaredParameter.substring(pos); - - const isParameterOptional = optionalDepth > 0; - - for (pos = declaredParameter.length - 1; pos >= 0; pos--) { - const levelChange = optionalCharDict[declaredParameter[pos]]; - - if (levelChange === undefined) { - break; - } - - optionalDepth += levelChange; - } - - // Cut off any leading brackets - declaredParameter = declaredParameter.substring(0, pos + 1); - - // Default value of this parameter in the method's declaration - let defaultValue; - - const equalSignPos = declaredParameter.indexOf('='); - if (equalSignPos !== -1) { - // We have a default value, save it and then cut it off of the signature - defaultValue = declaredParameter.substring(equalSignPos, 1).trim(); - declaredParameter = declaredParameter.substring(0, equalSignPos); - } - - let parameter = rawParameters[i]; - if (!parameter || declaredParameter !== parameter.name) { - // If we're here then the method likely has shared signatures - // Something like, `new Console(stdout[, stderr][, ignoreErrors])` and - // `new Console(options)` - parameter = undefined; - - // Try finding a parameter this is being shared with - for (const otherParam of rawParameters) { - if (declaredParameter === otherParam.name) { - // Found a matching one - parameter = otherParam; - break; - } else if (otherParam.options) { - // Found a matching one in the parameter's options - for (const option of otherParam.options) { - if (declaredParameter === option.name) { - parameter = Object.assign({}, option); - break; - } - } - } - } - - if (!parameter) { - // Couldn't find the shared one - if (declaredParameter.startsWith('...')) { - parameter = { name: declaredParameter }; - } else { - throw new Error( - `Invalid param "${declaredParameter}"\n` + ` > ${textRaw}` - ); - } - } - } - - if (isParameterOptional) { - parameter.optional = true; - } - - if (defaultValue) { - parameter.default = defaultValue; - } - - signature.params.push(parameter); - }); - - return signature; -} - /** * @param {Array} nodes */ @@ -534,7 +383,7 @@ export default (head, entries) => { */ const rootModule = { type: 'module', - source: `doc/api/${head.api_doc_source}`, + source: head.api_doc_source, }; buildHierarchy(entries).forEach(entry => handleEntry(entry, rootModule)); diff --git a/src/generators/legacy-json/utils/parseSignature.mjs b/src/generators/legacy-json/utils/parseSignature.mjs new file mode 100644 index 0000000..3508ede --- /dev/null +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -0,0 +1,210 @@ +'use strict'; + +import { PARAM_EXPRESSION } from '../constants.mjs'; + +const OPTIONAL_LEVEL_CHANGES = { '[': 1, ']': -1, ' ': 0 }; + +/** + * @param {string} parameterName + * @param {number} optionalDepth + * @returns {[string, number, boolean]} + */ +function parseNameAndOptionalStatus(parameterName, optionalDepth) { + // Let's check if the parameter is optional & grab its name at the same time. + // We need to see if there's any leading brackets in front of the parameter + // name. While we're doing that, we can also get the index where the + // parameter's name actually starts at. + let startingIdx = 0; + for (; startingIdx < parameterName.length; startingIdx++) { + const levelChange = OPTIONAL_LEVEL_CHANGES[parameterName[startingIdx]]; + + if (!levelChange) { + break; + } + + optionalDepth += levelChange; + } + + const isParameterOptional = optionalDepth > 0; + + // Now let's check for any trailing brackets at the end of the parameter's + // name. This will tell us where the parameter's name ends. + let endingIdx = parameterName.length - 1; + for (; endingIdx >= 0; endingIdx--) { + const levelChange = OPTIONAL_LEVEL_CHANGES[parameterName[startingIdx]]; + + if (!levelChange) { + break; + } + + optionalDepth += levelChange; + } + console.log('', startingIdx, endingIdx) + return [ + parameterName.substring(startingIdx, endingIdx + 1), + optionalDepth, + isParameterOptional + ]; +} + +/** + * @param {string} parameterName + * @returns {[string, string | undefined]} + */ +function parseDefaultValue(parameterName) { + /** + * @type {string | undefined} + */ + let defaultValue + + const equalSignPos = parameterName.indexOf('='); + if (equalSignPos !== -1) { + // We do have a default value, let's extract it + defaultValue = parameterName.substring(equalSignPos).trim(); + + // Let's remove the default value from the parameter name + parameterName = parameterName.substring(0, equalSignPos); + } + + return [parameterName, defaultValue] +} + +/** + * @param {string} parameterName + * @param {number} index + * @param {Array} markdownParameters + * @returns {import('../types.d.ts').Parameter} + */ +function findParameter(parameterName, index, markdownParameters) { + let parameter = markdownParameters[index] + if (parameter && parameter.name === parameterName) { + return parameter + } + + // Method likely has multiple signatures, something like + // `new Console(stdout[, stderr][, ignoreErrors])` and `new Console(options)` + // Try to find the parameter that this is being shared with + for (const markdownProperty of markdownParameters) { + if (markdownProperty.name === parameterName) { + // Found it + return markdownParameters + } else if (markdownProperty.options) { + for (const option of markdownProperty.options) { + if (option.name === parameterName) { + // Found a matching one in the parameter's options + return Object.assign({}, option); + } + } + } + } + + // At this point, we couldn't find a shared signature + if (parameterName.startsWith('...')) { + return { name: parameterName }; + } else { + throw new Error( + `Invalid param "${parameterName}"` + ); + } +} + +/** + * @param {string[]} declaredParameters + * @param {Array} parameters + */ +function parseParameters(declaredParameters, markdownParameters) { + /** + * @type {Array} + */ + let parameters = []; + + let optionalDepth = 0; + + declaredParameters.forEach((parameterName, i) => { + /** + * @example 'length]]' + * @example 'arrayBuffer[' + * @example '[sources[' + * @example 'end' + */ + parameterName = parameterName.trim(); + + // We need to do three things here: + // 1. Determine the declared parameters' name + // 2. Determine if the parameter is optional + // 3. Determine if the parameter has a default value + + /** + * This will handle the first and second thing for us + * @type {boolean} + */ + console.log(parameterName) + let isParameterOptional; + [parameterName, optionalDepth, isParameterOptional] = + parseNameAndOptionalStatus(parameterName, optionalDepth); + + console.log('', parameterName) + /** + * Now let's work on the third thing + * @type {string | undefined} + */ + let defaultValue; + [parameterName, defaultValue] = parseDefaultValue(parameterName) + + const parameter = findParameter(parameterName, i, markdownParameters) + + if (isParameterOptional) { + parameter.optional = true + } + + if (defaultValue) { + parameter.default = defaultValue + } + + parameters.push(parameter) + }); + + return parameters; +} + +/** + * @param {string} textRaw Something like `new buffer.Blob([sources[, options]])` + * @param {Array { + /** + * @type {import('../types.d.ts').MethodSignature} + */ + const signature = {}; + + // Find the return value & filter it out + markdownParameters = markdownParameters.filter(value => { + if (value.name === 'return') { + signature.return = value; + return false; + } + + return true; + }); + + /** + * Extract the parameters from the method's declaration + * @example `[sources[, options]]` + */ + let [, declaredParameters] = `\`${textRaw}\``.match(PARAM_EXPRESSION) || []; + + if (!declaredParameters) { + return undefined; + } + + /** + * @type {string[]} + * @example ['sources[,', 'options]]'] + */ + declaredParameters = declaredParameters.split(','); + + signature.params = parseParameters(declaredParameters, markdownParameters); + + return signature; +} diff --git a/src/metadata.mjs b/src/metadata.mjs index dab402a..389a3e5 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -145,7 +145,7 @@ const createMetadata = slugger => { api: apiDoc.stem, slug: sectionSlug, source_link, - api_doc_source: apiDoc.basename, + api_doc_source: `doc/api/${apiDoc.basename}`, added_in: added, deprecated_in: deprecated, removed_in: removed, diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index 948993e..9c9e25d 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -66,7 +66,7 @@ describe('createMetadata', () => { const expected = { added_in: undefined, api: 'test', - api_doc_source: 'test.md', + api_doc_source: 'doc/api/test.md', changes: [], content: section, deprecated_in: undefined, diff --git a/src/types.d.ts b/src/types.d.ts index 34a8cb0..f40eb7f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -70,7 +70,7 @@ declare global { slug: string; // The GitHub URL to the source of the API entry source_link: string | Array | undefined; - // Base name of the api doc file (ex/ `addons.md`) + // Path to the api doc file relative to the root of the nodejs repo root (ex/ `doc/api/addons.md`) api_doc_source: string; // When a said API section got added (in which version(s) of Node.js) added_in: string | Array | undefined; From 4a44ab7f2dc6edd7475485a5500b8aa6ade755e9 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:49:24 -0800 Subject: [PATCH 18/18] Apply suggestions from code review Co-authored-by: Aviv Keller --- src/generators/legacy-json/utils/buildSection.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index d92840a..128df02 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -335,7 +335,7 @@ function handleEntry(entry, parentSection) { } for (const key of Object.keys(section)) { - if (key in ['textRaw', 'name', 'type', 'desc', 'miscs']) { + if (['textRaw', 'name', 'type', 'desc', 'miscs'].includes(key)) { continue; }