From 3a4b27b3b9be8b4071604f3ca1fa2f5e9d409fb1 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 1/8] feat(json): add legacy JSON generator Co-Authored-By: flakey5 <73616808+flakey5@users.noreply.github.com> --- README.md | 2 +- shiki.config.mjs | 4 +- src/constants.mjs | 15 +- src/generators/index.mjs | 4 + src/generators/legacy-html/assets/api.js | 2 - src/generators/legacy-json-all/index.mjs | 54 +++ src/generators/legacy-json-all/types.d.ts | 14 + src/generators/legacy-json/constants.mjs | 18 + src/generators/legacy-json/index.mjs | 67 ++++ src/generators/legacy-json/types.d.ts | 83 +++++ .../legacy-json/utils/buildHierarchy.mjs | 78 +++++ .../legacy-json/utils/buildSection.mjs | 314 ++++++++++++++++++ .../legacy-json/utils/parseSignature.mjs | 206 ++++++++++++ src/metadata.mjs | 16 +- src/parser.mjs | 4 +- src/queries.mjs | 19 +- src/test/metadata.test.mjs | 2 + src/test/queries.test.mjs | 31 +- src/types.d.ts | 4 + src/utils/parser.mjs | 13 +- 20 files changed, 920 insertions(+), 30 deletions(-) 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/constants.mjs 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/buildHierarchy.mjs create mode 100644 src/generators/legacy-json/utils/buildSection.mjs create mode 100644 src/generators/legacy-json/utils/parseSignature.mjs diff --git a/README.md b/README.md index 8f02bf02..1b623e04 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,6 @@ Options: -o, --output Specify the relative or absolute output directory -v, --version Specify the target version of Node.js, semver compliant (default: "v22.6.0") -c, --changelog Specify the path (file: or https://) to the CHANGELOG.md file (default: "https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md") - -t, --target [mode...] Set the processing target modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page") + -t, --target [mode...] Set the processing target modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page", "legacy-json", "legacy-json-all") -h, --help display help for command ``` diff --git a/shiki.config.mjs b/shiki.config.mjs index d53dddcc..aab14243 100644 --- a/shiki.config.mjs +++ b/shiki.config.mjs @@ -30,7 +30,7 @@ export default { // Only register the languages that the API docs use // and override the JavaScript language with the aliases langs: [ - { ...javaScriptLanguage[0], aliases: ['mjs', 'cjs', 'js'] }, + ...httpLanguage, ...jsonLanguage, ...typeScriptLanguage, ...shellScriptLanguage, @@ -40,7 +40,7 @@ export default { ...diffLanguage, ...cLanguage, ...cPlusPlusLanguage, - ...httpLanguage, ...coffeeScriptLanguage, + { ...javaScriptLanguage[0], aliases: ['mjs', 'cjs', 'js'] }, ], }; diff --git a/src/constants.mjs b/src/constants.mjs index a44763f5..5bb7f0b9 100644 --- a/src/constants.mjs +++ b/src/constants.mjs @@ -58,7 +58,14 @@ export const DOC_API_SLUGS_REPLACEMENTS = [ // is a specific type of API Doc entry (e.g., Event, Class, Method, etc) // and to extract the inner content of said Heading to be used as the API doc entry name export const DOC_API_HEADING_TYPES = [ - { type: 'method', regex: /^`?([A-Z]\w+(?:\.[A-Z]\w+)*\.\w+)\([^)]*\)`?$/i }, + { + type: 'method', + regex: + // Group 1: foo[bar]() + // Group 2: foo.bar() + // Group 3: foobar() + /^`?(?:\w*(?:(\[[^\]]+\])|(?:\.(\w+)))|(\w+))\([^)]*\)`?$/i, + }, { type: 'event', regex: /^Event: +`?['"]?([^'"]+)['"]?`?$/i }, { type: 'class', @@ -71,11 +78,13 @@ export const DOC_API_HEADING_TYPES = [ }, { type: 'classMethod', - regex: /^Static method: +`?([A-Z]\w+(?:\.[A-Z]\w+)*\.\w+)\([^)]*\)`?$/i, + regex: + /^Static method: +`?[A-Z]\w+(?:\.[A-Z]\w+)*(?:(\[\w+\.\w+\])|\.(\w+))\([^)]*\)`?$/i, }, { type: 'property', - regex: /^(?:Class property: +)?`?([A-Z]\w+(?:\.[A-Z]\w+)*\.\w+)`?$/i, + regex: + /^(?:Class property: +)?`?[A-Z]\w+(?:\.[A-Z]\w+)*(?:(\[\w+\.\w+\])|\.(\w+))`?$/i, }, ]; diff --git a/src/generators/index.mjs b/src/generators/index.mjs index 6c8c835e..f787a2b2 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -4,10 +4,14 @@ import jsonSimple from './json-simple/index.mjs'; import legacyHtml from './legacy-html/index.mjs'; import legacyHtmlAll from './legacy-html-all/index.mjs'; import manPage from './man-page/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, 'man-page': manPage, + 'legacy-json': legacyJson, + 'legacy-json-all': legacyJsonAll, }; diff --git a/src/generators/legacy-html/assets/api.js b/src/generators/legacy-html/assets/api.js index a2e3c5fb..7bb67a21 100644 --- a/src/generators/legacy-html/assets/api.js +++ b/src/generators/legacy-html/assets/api.js @@ -165,8 +165,6 @@ let code = ''; - console.log(parentNode); - if (flavorToggle) { if (flavorToggle.checked) { code = parentNode.querySelector('.mjs').textContent; diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs new file mode 100644 index 00000000..7dbc8c54 --- /dev/null +++ b/src/generators/legacy-json-all/index.mjs @@ -0,0 +1,54 @@ +'use strict'; + +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +/** + * @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, { output }) { + /** + * @type {import('./types.d.ts').Output} + */ + const generatedValue = { + miscs: [], + modules: [], + classes: [], + globals: [], + methods: [], + }; + + const propertiesToCopy = [ + 'miscs', + 'modules', + 'classes', + 'globals', + 'methods', + ]; + + input.forEach(section => { + // Copy the relevant properties from each section into our output + propertiesToCopy.forEach(property => { + if (section[property]) { + generatedValue[property].push(...section[property]); + } + }); + }); + + await writeFile(join(output, 'all.json'), JSON.stringify(generatedValue)); + + return generatedValue; + }, +}; 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 00000000..0748a319 --- /dev/null +++ b/src/generators/legacy-json-all/types.d.ts @@ -0,0 +1,14 @@ +import { + MiscSection, + Section, + SignatureSection, + ModuleSection, +} from '../legacy-json/types'; + +export interface Output { + miscs: Array; + modules: Array
; + classes: Array; + globals: Array; + methods: Array; +} diff --git a/src/generators/legacy-json/constants.mjs b/src/generators/legacy-json/constants.mjs new file mode 100644 index 00000000..b999dea7 --- /dev/null +++ b/src/generators/legacy-json/constants.mjs @@ -0,0 +1,18 @@ +// Grabs a method's return value +export const RETURN_EXPRESSION = /^returns?\s*:?\s*/i; + +// Grabs a method's name +export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; + +// Denotes a method's type +export const TYPE_EXPRESSION = /^\{([^}]+)\}\s*/; + +// Checks if there's a leading hyphen +export const LEADING_HYPHEN = /^-\s*/; + +// Grabs the default value if present +export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; + +// 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 = /\((.+)\);?$/; diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs new file mode 100644 index 00000000..d3b6c058 --- /dev/null +++ b/src/generators/legacy-json/index.mjs @@ -0,0 +1,67 @@ +'use strict'; + +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { groupNodesByModule } from '../../utils/generators.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} + */ +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" + 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 section = buildSection(head, nodes); + generatedValues.push(section); + + 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) + ); + }) + ); + + 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 00000000..95180150 --- /dev/null +++ b/src/generators/legacy-json/types.d.ts @@ -0,0 +1,83 @@ +import { ListItem } from 'mdast'; + +export interface HierarchizedEntry extends ApiDocMetadataEntry { + hierarchyChildren: Array; +} + +export interface Meta { + changes: Array; + added?: Array; + napiVersion?: Array; + deprecated?: Array; + removed?: Array; +} + +export interface SectionBase { + type: string; + name: string; + textRaw: string; + displayName?: string; + desc: string; + shortDesc?: string; + stability?: number; + stabilityText?: string; + meta?: Meta; +} + +export interface ModuleSection extends SectionBase { + type: 'module'; + source: string; + miscs?: Array; + modules?: Array; + classes?: Array; + methods?: Array; + properties?: Array; + globals?: ModuleSection | { type: 'global' }; + signatures?: Array; +} + +export interface SignatureSection extends SectionBase { + type: 'class' | 'ctor' | 'classMethod' | 'method'; + signatures: Array; +} + +export type Section = + | SignatureSection + | PropertySection + | EventSection + | MiscSection; + +export interface Parameter { + name: string; + optional?: boolean; + default?: string; +} + +export interface MethodSignature { + params: Array; + return?: string; +} + +export interface PropertySection extends SectionBase { + type: 'property'; + [key: string]: string | undefined; +} + +export interface EventSection extends SectionBase { + type: 'event'; + params: Array; +} + +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 new file mode 100644 index 00000000..340ad7ec --- /dev/null +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -0,0 +1,78 @@ +/** + * Recursively finds the most suitable parent entry for a given `entry` based on heading depth. + * + * @param {ApiDocMetadataEntry} entry + * @param {ApiDocMetadataEntry[]} entry + * @param {number} startIdx + * @returns {import('../types.d.ts').HierarchizedEntry} + */ +function findParent(entry, entries, startIdx) { + // Base case: if we're at the beginning of the list, no valid parent exists. + if (startIdx < 0) { + throw new Error( + `Cannot find a suitable parent for entry at index ${startIdx + 1}` + ); + } + + const candidateParent = entries[startIdx]; + const candidateDepth = candidateParent.heading.depth; + + // If we find a suitable parent, return it. + if (candidateDepth < entry.heading.depth) { + candidateParent.hierarchyChildren ??= []; + return candidateParent; + } + + // Recurse upwards to find a suitable parent. + return findParent(entry, entries, startIdx - 1); +} + +/** + * 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 + * 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 = []; + + // Main loop to construct the hierarchy. + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const currentDepth = entry.heading.depth; + + // Top-level entries are added directly to roots. + if (currentDepth <= 1) { + roots.push(entry); + continue; + } + + // For non-root entries, find the appropriate parent. + const previousEntry = entries[i - 1]; + const previousDepth = previousEntry.heading.depth; + + if (currentDepth > previousDepth) { + previousEntry.hierarchyChildren ??= []; + previousEntry.hierarchyChildren.push(entry); + } else { + // Use recursive helper to find the nearest valid parent. + const parent = findParent(entry, entries, i - 2); + parent.hierarchyChildren.push(entry); + } + } + + return roots; +} diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs new file mode 100644 index 00000000..e1424227 --- /dev/null +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -0,0 +1,314 @@ +import { + DEFAULT_EXPRESSION, + LEADING_HYPHEN, + NAME_EXPRESSION, + RETURN_EXPRESSION, + TYPE_EXPRESSION, +} from '../constants.mjs'; +import { buildHierarchy } from './buildHierarchy.mjs'; +import parseSignature from './parseSignature.mjs'; +import { getRemarkRehype } from '../../../utils/remark.mjs'; +import { transformNodesToString } from '../../../utils/unist.mjs'; + +const sectionTypePlurals = { + module: 'modules', + misc: 'miscs', + class: 'classes', + method: 'methods', + property: 'properties', + global: 'globals', + example: 'examples', + ctor: 'signatures', + classMethod: 'classMethods', + event: 'events', + var: 'vars', +}; + +/** + * Converts a value to an array. + * @template T + * @param {T | T[]} val - The value to convert. + * @returns {T[]} The value as an array. + */ +const enforceArray = val => (Array.isArray(val) ? val : [val]); + +/** + * Creates metadata from a hierarchized entry. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to create metadata from. + * @returns {import('../types.d.ts').Meta} The created metadata. + */ +function createMeta(entry) { + const { + added_in = [], + n_api_version = [], + deprecated_in = [], + removed_in = [], + changes, + } = entry; + + return { + changes, + added: enforceArray(added_in), + napiVersion: enforceArray(n_api_version), + deprecated: enforceArray(deprecated_in), + removed: enforceArray(removed_in), + }; +} + +/** + * Creates a section from an entry and its heading. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The AST entry. + * @param {HeadingMetadataParent} head - The head node of the entry. + * @returns {import('../types.d.ts').Section} The created section. + */ +function createSection(entry, head) { + return { + textRaw: transformNodesToString(head.children), + name: head.data.name, + type: head.data.type, + meta: createMeta(entry), + introduced_in: entry.introduced_in, + }; +} + +/** + * Parses a list item to extract properties. + * @param {import('mdast').ListItem} child - The list item node. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry containing raw content. + * @returns {import('../types.d.ts').List} The parsed list. + */ +function parseListItem(child, entry) { + const current = {}; + + /** + * Extracts raw content from a node based on its position. + * @param {import('mdast').BlockContent} node + * @returns {string} + */ + const getRawContent = node => + entry.rawContent.slice( + node.position.start.offset, + node.position.end.offset + ); + + /** + * Extracts a pattern from text and assigns it to the current object. + * @param {string} text + * @param {RegExp} pattern + * @param {string} key + * @returns {string} + */ + const extractPattern = (text, pattern, key) => { + const [, match] = text.match(pattern) || []; + if (match) { + current[key] = match.trim().replace(/\.$/, ''); + return text.replace(pattern, ''); + } + return text; + }; + + // Combine and clean text from child nodes, excluding nested lists + current.textRaw = child.children + .filter(node => node.type !== 'list') + .map(getRawContent) + .join('') + .replace(/\s+/g, ' ') + .replace(//gs, ''); + + let text = current.textRaw; + + // Determine if the current item is a return statement + if (RETURN_EXPRESSION.test(text)) { + current.name = 'return'; + text = text.replace(RETURN_EXPRESSION, ''); + } else { + text = extractPattern(text, NAME_EXPRESSION, 'name'); + } + + // Extract type and default values if present + text = extractPattern(text, TYPE_EXPRESSION, 'type'); + text = extractPattern(text, DEFAULT_EXPRESSION, 'default'); + + // Assign the remaining text as the description after removing leading hyphens + current.desc = text.replace(LEADING_HYPHEN, '').trim() || undefined; + + // Recursively parse nested options if a list is found within the list item + const optionsNode = child.children.find(child => child.type === 'list'); + if (optionsNode) { + current.options = optionsNode.children.map(child => + parseListItem(child, entry) + ); + } + + return current; +} + +/** + * Parses stability metadata and adds it to the section. + * @param {import('../types.d.ts').Section} section - The section to add stability to. + * @param {Array} nodes - The AST nodes. + */ +function parseStability(section, nodes) { + nodes.forEach((node, i) => { + if ( + node.type === 'blockquote' && + node.children.length === 1 && + node.children[0].type === 'paragraph' && + nodes.slice(0, i).every(n => n.type === 'list') + ) { + const text = transformNodesToString(node.children[0].children); + const stabilityMatch = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s.exec(text); + if (stabilityMatch) { + section.stability = Number(stabilityMatch[1]); + section.stabilityText = stabilityMatch[2].replace(/\n/g, ' ').trim(); + nodes.splice(i, 1); // Remove the matched stability node to prevent further processing + } + } + }); +} + +/** + * Parses a list and updates the section accordingly. + * @param {import('../types.d.ts').Section} section - The section to update. + * @param {Array} nodes - The AST nodes. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The associated entry. + */ +function parseList(section, nodes, entry) { + const list = nodes[0]?.type === 'list' ? nodes.shift() : null; + const values = list + ? list.children.map(child => parseListItem(child, entry)) + : []; + + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': + section.signatures = [parseSignature(section.textRaw, values)]; + break; + case 'property': + if (values.length) { + const { type, ...rest } = values[0]; + if (type) section.propertySigType = type; + Object.assign(section, rest); + section.textRaw = `\`${section.name}\` ${section.textRaw}`; + } + break; + case 'event': + section.params = values; + break; + default: + if (list) nodes.unshift(list); // If the list wasn't processed, add it back for further processing + } +} + +/** + * Adds a description to the section. + * @param {import('../types.d.ts').Section} section - The section to add description to. + * @param {Array} nodes - The AST nodes. + */ +function addDescription(section, nodes) { + if (!nodes.length) return; + + if (section.desc) { + section.shortDesc = section.desc; + } + + const html = getRemarkRehype(); + const rendered = html.stringify( + html.runSync({ type: 'root', children: nodes }) + ); + section.desc = rendered || undefined; +} + +/** + * Adds additional metadata to the section based on its type. + * @param {import('../types.d.ts').Section} section - The section to update. + * @param {import('../types.d.ts').Section} parentSection - The parent section. + * @param {import('../../types.d.ts').NodeWithData} headingNode - The heading node. + */ +function addAdditionalMetadata(section, parentSection, headingNode) { + if (!section.type) { + section.name = section.textRaw.toLowerCase().trim().replace(/\s+/g, '_'); + section.displayName = headingNode.data.name; + section.type = parentSection.type === 'misc' ? 'misc' : 'module'; + } +} + +/** + * Adds the section to its parent section. + * @param {import('../types.d.ts').Section} section - The section to add. + * @param {import('../types.d.ts').Section} parentSection - The parent section. + */ +function addToParent(section, parentSection) { + const pluralType = sectionTypePlurals[section.type]; + parentSection[pluralType] = parentSection[pluralType] || []; + parentSection[pluralType].push(section); +} + +/** + * Promotes children to top-level if the section type is 'misc'. + * @param {import('../types.d.ts').Section} section - The section to promote. + * @param {import('../types.d.ts').Section} parentSection - The parent section. + */ +const makeChildrenTopLevelIfMisc = (section, parentSection) => { + if (section.type !== 'misc' || parentSection.type === 'misc') { + return; + } + + Object.keys(section).forEach(key => { + if (['textRaw', 'name', 'type', 'desc', 'miscs'].includes(key)) { + return; + } + if (parentSection[key]) { + parentSection[key] = Array.isArray(parentSection[key]) + ? parentSection[key].concat(section[key]) + : section[key]; + } else { + parentSection[key] = section[key]; + } + }); +}; + +/** + * Handles an entry and updates the parent section. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to handle. + * @param {import('../types.d.ts').Section} parentSection - The parent section. + */ +function handleEntry(entry, parentSection) { + const [headingNode, ...nodes] = structuredClone(entry.content.children); + const section = createSection(entry, headingNode); + + parseStability(section, nodes); + parseList(section, nodes, entry); + addDescription(section, nodes); + entry.hierarchyChildren?.forEach(child => handleEntry(child, section)); + addAdditionalMetadata(section, parentSection, headingNode); + addToParent(section, parentSection); + makeChildrenTopLevelIfMisc(section, parentSection); + + if (section.type === 'property') { + if (section.propertySigType) { + section.type = section.propertySigType; + delete section.propertySigType; + } else { + delete section.type; + } + } +} + +/** + * Builds the module section from head and entries. + * @param {ApiDocMetadataEntry} head - The head metadata entry. + * @param {Array} entries - The list of metadata entries. + * @returns {import('../types.d.ts').ModuleSection} The constructed module section. + */ +export default (head, entries) => { + const rootModule = { + type: 'module', + source: head.api_doc_source, + }; + + buildHierarchy(entries).forEach(entry => handleEntry(entry, rootModule)); + + return rootModule; +}; diff --git a/src/generators/legacy-json/utils/parseSignature.mjs b/src/generators/legacy-json/utils/parseSignature.mjs new file mode 100644 index 00000000..a28983fb --- /dev/null +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -0,0 +1,206 @@ +'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[endingIdx]]; + if (!levelChange) { + break; + } + + optionalDepth += levelChange; + } + + 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} + */ + let isParameterOptional; + [parameterName, optionalDepth, isParameterOptional] = + parseNameAndOptionalStatus(parameterName, optionalDepth); + + /** + * 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 = { params: [] }; + + // 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.substring(1, textRaw.length - 1).match(PARAM_EXPRESSION) || []; + + if (!declaredParameters) { + return signature; + } + + /** + * @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 35358691..167aaf2f 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -113,6 +113,7 @@ const createMetadata = slugger => { const { type, + introduced_in, added, deprecated, removed, @@ -137,11 +138,14 @@ 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, + api_doc_source: `doc/api/${apiDoc.basename}`, added_in: added, deprecated_in: deprecated, removed_in: removed, @@ -152,7 +156,15 @@ const createMetadata = slugger => { stability: internalMetadata.stability, content: section, tags, + rawContent: apiDoc.toString(), }; + + if (introduced_in) { + value.introduced_in = introduced_in; + } + + // Returns the Metadata entry for the API doc + return value; }, }; }; diff --git a/src/parser.mjs b/src/parser.mjs index 3c05b64f..c4c34347 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -140,8 +140,8 @@ const createParser = () => { // Visits all Text nodes from the current subtree and if there's any that matches // any API doc type reference and then updates the type reference to be a Markdown link - visit(subTree, createQueries.UNIST.isTextWithType, node => - updateTypeReference(node) + visit(subTree, createQueries.UNIST.isTextWithType, (node, _, parent) => + updateTypeReference(node, parent) ); // Removes already parsed items from the subtree so that they aren't included in the final content diff --git a/src/queries.mjs b/src/queries.mjs index 8937539c..74185eff 100644 --- a/src/queries.mjs +++ b/src/queries.mjs @@ -1,7 +1,7 @@ 'use strict'; import { u as createTree } from 'unist-builder'; -import { SKIP } from 'unist-util-visit'; +import { SKIP, visit } from 'unist-util-visit'; import { DOC_API_STABILITY_SECTION_REF_URL } from './constants.mjs'; @@ -12,12 +12,14 @@ import { parseYAMLIntoMetadata, transformTypeToReferenceLink, } from './utils/parser.mjs'; +import { getRemark } from './utils/remark.mjs'; /** * Creates an instance of the Query Manager, which allows to do multiple sort * of metadata and content metadata manipulation within an API Doc */ const createQueries = () => { + const remark = getRemark(); /** * Sanitizes the YAML source by returning the inner YAML content * and then parsing it into an API Metadata object and updating the current Metadata @@ -71,15 +73,24 @@ const createQueries = () => { * into a Markdown link referencing to the correct API docs * * @param {import('mdast').Text} node A Markdown link node + * @param {import('mdast').Parent} parent The parent node */ - const updateTypeReference = node => { + const updateTypeReference = (node, parent) => { const replacedTypes = node.value.replace( createQueries.QUERIES.normalizeTypes, transformTypeToReferenceLink ); - node.type = 'html'; - node.value = replacedTypes; + const { + children: [newNode], + } = remark.parse(replacedTypes); + const index = parent.children.indexOf(node); + const originalPosition = node.position; + visit(newNode, node => { + (node.position.start += originalPosition.start), + (node.position.end += originalPosition.end); + }); + parent.children.splice(index, 1, ...newNode.children); return [SKIP]; }; diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index 3612f126..b89d82b6 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -66,11 +66,13 @@ describe('createMetadata', () => { const expected = { added_in: undefined, api: 'test', + api_doc_source: 'doc/api/test.md', changes: [], content: section, deprecated_in: undefined, heading, n_api_version: undefined, + rawContent: '', removed_in: undefined, slug: 'test-heading', source_link: 'test.com', diff --git a/src/test/queries.test.mjs b/src/test/queries.test.mjs index b8de7ca2..398b190d 100644 --- a/src/test/queries.test.mjs +++ b/src/test/queries.test.mjs @@ -17,21 +17,32 @@ describe('createQueries', () => { // valid type it('should update type to reference correctly', () => { const queries = createQueries(); - const node = { value: 'This is a {string} type.' }; - queries.updateTypeReference(node); - strictEqual(node.type, 'html'); - strictEqual( - node.value, - 'This is a [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) type.' + const node = { + value: 'This is a {string} type.', + position: { start: 0, end: 0 }, + }; + const parent = { children: [node] }; + queries.updateTypeReference(node, parent); + deepStrictEqual( + parent.children.map(c => c.value), + [ + 'This is a ', + undefined, // link + ' type.', + ] ); }); it('should update type to reference not correctly if no match', () => { const queries = createQueries(); - const node = { value: 'This is a {test} type.' }; - queries.updateTypeReference(node); - strictEqual(node.type, 'html'); - strictEqual(node.value, 'This is a {test} type.'); + const node = { + value: 'This is a {test} type.', + position: { start: 0, end: 0 }, + }; + const parent = { children: [node] }; + queries.updateTypeReference(node, parent); + strictEqual(parent.children[0].type, 'text'); + strictEqual(parent.children[0].value, 'This is a {test} type.'); }); it('should add heading metadata correctly', () => { diff --git a/src/types.d.ts b/src/types.d.ts index 1391750b..e372cce7 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; + // 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; // When a said API section got removed (in which version(s) of Node.js) @@ -96,6 +98,8 @@ declare global { // Extra YAML section entries that are stringd and serve // to provide additional metadata about the API doc entry tags: Array; + // The raw file content + rawContent: string; } export interface ApiDocReleaseEntry { diff --git a/src/utils/parser.mjs b/src/utils/parser.mjs index dea0dbb1..e0ad23d6 100644 --- a/src/utils/parser.mjs +++ b/src/utils/parser.mjs @@ -123,10 +123,15 @@ export const parseHeadingIntoMetadata = (heading, depth) => { // Attempts to get a match from one of the heading types, if a match is found // we use that type as the heading type, and extract the regex expression match group // which should be the inner "plain" heading content (or the title of the heading for navigation) - const [, innerHeading] = heading.match(regex) ?? []; - - if (innerHeading && innerHeading.length) { - return { text: heading, type, name: innerHeading, depth }; + const [, ...matches] = heading.match(regex) ?? []; + + if (matches?.length) { + return { + text: heading, + type, + name: matches.filter(Boolean).at(-1), + depth, + }; } } From 35e6f58b1bdfe996acac9e449ed1c13d86514e3c Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:16:33 -0500 Subject: [PATCH 2/8] code review (1) --- src/generators/legacy-json-all/index.mjs | 6 ++ .../legacy-json/utils/buildSection.mjs | 71 ++++++++------ .../legacy-json/utils/parseSignature.mjs | 95 ++++++++++--------- src/metadata.mjs | 14 +-- src/queries.mjs | 3 + src/test/metadata.test.mjs | 1 + src/utils/parser.mjs | 1 + 7 files changed, 102 insertions(+), 89 deletions(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index 7dbc8c54..9624b1fd 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -4,6 +4,9 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; /** + * This generator consolidates data from the `legacy-json` generator into a single + * JSON file (`all.json`). + * * @typedef {Array} Input * * @type {import('../types.d.ts').GeneratorMetadata} @@ -20,6 +23,9 @@ export default { async generate(input, { output }) { /** + * The consolidated output object that will contain + * combined data from all sections in the input. + * * @type {import('./types.d.ts').Output} */ const generatedValue = { diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index e1424227..277d1ca0 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -134,6 +134,7 @@ function parseListItem(child, entry) { // Recursively parse nested options if a list is found within the list item const optionsNode = child.children.find(child => child.type === 'list'); + if (optionsNode) { current.options = optionsNode.children.map(child => parseListItem(child, entry) @@ -175,6 +176,7 @@ function parseStability(section, nodes) { */ function parseList(section, nodes, entry) { const list = nodes[0]?.type === 'list' ? nodes.shift() : null; + const values = list ? list.children.map(child => parseListItem(child, entry)) : []; @@ -188,7 +190,8 @@ function parseList(section, nodes, entry) { case 'property': if (values.length) { const { type, ...rest } = values[0]; - if (type) section.propertySigType = type; + + section.type = type; Object.assign(section, rest); section.textRaw = `\`${section.name}\` ${section.textRaw}`; } @@ -197,26 +200,35 @@ function parseList(section, nodes, entry) { section.params = values; break; default: - if (list) nodes.unshift(list); // If the list wasn't processed, add it back for further processing + // If the list wasn't processed, add it back for further processing + if (list) { + nodes.unshift(list); + } } } +let lazyHTML; + /** * Adds a description to the section. * @param {import('../types.d.ts').Section} section - The section to add description to. * @param {Array} nodes - The AST nodes. */ function addDescription(section, nodes) { - if (!nodes.length) return; + if (!nodes.length) { + return; + } if (section.desc) { section.shortDesc = section.desc; } - const html = getRemarkRehype(); - const rendered = html.stringify( - html.runSync({ type: 'root', children: nodes }) + lazyHTML ??= getRemarkRehype(); + + const rendered = lazyHTML.stringify( + lazyHTML.runSync({ type: 'root', children: nodes }) ); + section.desc = rendered || undefined; } @@ -241,32 +253,38 @@ function addAdditionalMetadata(section, parentSection, headingNode) { */ function addToParent(section, parentSection) { const pluralType = sectionTypePlurals[section.type]; + parentSection[pluralType] = parentSection[pluralType] || []; parentSection[pluralType].push(section); } +const notTransferredKeys = ['textRaw', 'name', 'type', 'desc', 'miscs']; + /** - * Promotes children to top-level if the section type is 'misc'. + * Promotes children properties to the parent level if the section type is 'misc'. + * * @param {import('../types.d.ts').Section} section - The section to promote. * @param {import('../types.d.ts').Section} parentSection - The parent section. */ const makeChildrenTopLevelIfMisc = (section, parentSection) => { - if (section.type !== 'misc' || parentSection.type === 'misc') { - return; + // Only promote if the current section is of type 'misc' and the parent is not 'misc' + if (section.type === 'misc' && parentSection.type !== 'misc') { + Object.entries(section).forEach(([key, value]) => { + // Skip keys that should not be transferred + if (notTransferredKeys.includes(key)) return; + + // Merge the section's properties into the parent section + parentSection[key] = parentSection[key] + ? // If the parent already has this key, concatenate the values + [].concat(parentSection[key], value) + : // Otherwise, directly assign the section's value to the parent + value; + }); } +}; - Object.keys(section).forEach(key => { - if (['textRaw', 'name', 'type', 'desc', 'miscs'].includes(key)) { - return; - } - if (parentSection[key]) { - parentSection[key] = Array.isArray(parentSection[key]) - ? parentSection[key].concat(section[key]) - : section[key]; - } else { - parentSection[key] = section[key]; - } - }); +const handleChildren = (entry, section) => { + entry.hierarchyChildren?.forEach(child => handleEntry(child, section)); }; /** @@ -281,19 +299,10 @@ function handleEntry(entry, parentSection) { parseStability(section, nodes); parseList(section, nodes, entry); addDescription(section, nodes); - entry.hierarchyChildren?.forEach(child => handleEntry(child, section)); + handleChildren(entry, section); addAdditionalMetadata(section, parentSection, headingNode); addToParent(section, parentSection); makeChildrenTopLevelIfMisc(section, parentSection); - - if (section.type === 'property') { - if (section.propertySigType) { - section.type = section.propertySigType; - delete section.propertySigType; - } else { - delete section.type; - } - } } /** diff --git a/src/generators/legacy-json/utils/parseSignature.mjs b/src/generators/legacy-json/utils/parseSignature.mjs index a28983fb..c5c303fe 100644 --- a/src/generators/legacy-json/utils/parseSignature.mjs +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -4,6 +4,14 @@ import { PARAM_EXPRESSION } from '../constants.mjs'; const OPTIONAL_LEVEL_CHANGES = { '[': 1, ']': -1, ' ': 0 }; +/** + * @param {String} char + * @param {Number} depth + * @returns {Number} + */ +const updateDepth = (char, depth) => + depth + (OPTIONAL_LEVEL_CHANGES[char] || 0); + /** * @param {string} parameterName * @param {number} optionalDepth @@ -14,36 +22,34 @@ function parseNameAndOptionalStatus(parameterName, optionalDepth) { // 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; - } + // Find the starting index where the name begins + const startingIdx = [...parameterName].findIndex( + char => !OPTIONAL_LEVEL_CHANGES[char] + ); + + // Update optionalDepth based on leading brackets + optionalDepth = [...parameterName.slice(0, startingIdx)].reduce( + updateDepth, + optionalDepth + ); + + // Find the ending index where the name ends + const endingIdx = [...parameterName].findLastIndex( + char => !OPTIONAL_LEVEL_CHANGES[char] + ); + + // Update optionalDepth based on trailing brackets + optionalDepth = [...parameterName.slice(endingIdx + 1)].reduce( + updateDepth, + optionalDepth + ); + + // Extract the actual parameter name + const actualName = parameterName.slice(startingIdx, endingIdx + 1); 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[endingIdx]]; - if (!levelChange) { - break; - } - - optionalDepth += levelChange; - } - - return [ - parameterName.substring(startingIdx, endingIdx + 1), - optionalDepth, - isParameterOptional, - ]; + return [actualName, optionalDepth, isParameterOptional]; } /** @@ -55,8 +61,8 @@ 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(); @@ -75,34 +81,29 @@ function parseDefaultValue(parameterName) { * @returns {import('../types.d.ts').Parameter} */ function findParameter(parameterName, index, markdownParameters) { - let parameter = markdownParameters[index]; - if (parameter && parameter.name === parameterName) { + const parameter = markdownParameters[index]; + if (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); - } - } + for (const property of markdownParameters) { + if (property.name === parameterName) { + return property; } - } - // At this point, we couldn't find a shared signature - if (parameterName.startsWith('...')) { - return { name: parameterName }; - } else { - throw new Error(`Invalid param "${parameterName}"`); + const matchingOption = property.options?.find( + option => option.name === parameterName + ); + if (matchingOption) { + return { ...matchingOption }; + } } + + // Default return if no matches are found + return { name: parameterName }; } /** diff --git a/src/metadata.mjs b/src/metadata.mjs index 167aaf2f..ae006353 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -138,10 +138,8 @@ const createMetadata = slugger => { internalMetadata.stability.toJSON = () => internalMetadata.stability.children.map(node => node.data); - /** - * @type {ApiDocMetadataEntry} - */ - const value = { + // Returns the Metadata entry for the API doc + return { api: apiDoc.stem, slug: sectionSlug, source_link, @@ -156,15 +154,9 @@ const createMetadata = slugger => { stability: internalMetadata.stability, content: section, tags, + introduced_in, rawContent: apiDoc.toString(), }; - - if (introduced_in) { - value.introduced_in = introduced_in; - } - - // Returns the Metadata entry for the API doc - return value; }, }; }; diff --git a/src/queries.mjs b/src/queries.mjs index 74185eff..1db3f23c 100644 --- a/src/queries.mjs +++ b/src/queries.mjs @@ -81,6 +81,9 @@ const createQueries = () => { transformTypeToReferenceLink ); + // This changes the type into a link by splitting it up + // into several nodes, and adding those nodes to the + // parent. const { children: [newNode], } = remark.parse(replacedTypes); diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index b89d82b6..cee82400 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -72,6 +72,7 @@ describe('createMetadata', () => { deprecated_in: undefined, heading, n_api_version: undefined, + introduced_in: undefined, rawContent: '', removed_in: undefined, slug: 'test-heading', diff --git a/src/utils/parser.mjs b/src/utils/parser.mjs index e0ad23d6..42458064 100644 --- a/src/utils/parser.mjs +++ b/src/utils/parser.mjs @@ -129,6 +129,7 @@ export const parseHeadingIntoMetadata = (heading, depth) => { return { text: heading, type, + // The highest match group should be used. name: matches.filter(Boolean).at(-1), depth, }; From 96c0c2665ac15c548e3222b4c4c5b53f6cf6c5ae Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:56:00 -0500 Subject: [PATCH 3/8] code review (2) --- src/generators/legacy-json-all/index.mjs | 4 +- src/generators/legacy-json/index.mjs | 10 ++- .../legacy-json/utils/buildSection.mjs | 78 ++++++++----------- src/metadata.mjs | 1 - src/queries.mjs | 31 ++++---- src/test/metadata.test.mjs | 1 - src/types.d.ts | 2 - 7 files changed, 58 insertions(+), 69 deletions(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index 9624b1fd..d7d4a3cb 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -53,7 +53,9 @@ export default { }); }); - await writeFile(join(output, 'all.json'), JSON.stringify(generatedValue)); + if (output) { + await writeFile(join(output, 'all.json'), JSON.stringify(generatedValue)); + } return generatedValue; }, diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index d3b6c058..08165151 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -55,10 +55,12 @@ export default { const section = processModuleNodes(node); // Write it to the output file - await writeFile( - join(output, `${node.api}.json`), - JSON.stringify(section) - ); + if (output) { + await writeFile( + join(output, `${node.api}.json`), + JSON.stringify(section) + ); + } }) ); diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 277d1ca0..9d21e912 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -71,26 +71,24 @@ function createSection(entry, head) { }; } +/** + * + * @param {String} string + * @returns {String} + */ +function transformTypeReferences(string) { + // console.log(string) + return string.replaceAll(/`<([^>]+)>`/g, '{$1}').replaceAll('} | {', '|'); +} + /** * Parses a list item to extract properties. * @param {import('mdast').ListItem} child - The list item node. - * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry containing raw content. * @returns {import('../types.d.ts').List} The parsed list. */ -function parseListItem(child, entry) { +function parseListItem(child) { const current = {}; - /** - * Extracts raw content from a node based on its position. - * @param {import('mdast').BlockContent} node - * @returns {string} - */ - const getRawContent = node => - entry.rawContent.slice( - node.position.start.offset, - node.position.end.offset - ); - /** * Extracts a pattern from text and assigns it to the current object. * @param {string} text @@ -108,12 +106,14 @@ function parseListItem(child, entry) { }; // Combine and clean text from child nodes, excluding nested lists - current.textRaw = child.children - .filter(node => node.type !== 'list') - .map(getRawContent) - .join('') - .replace(/\s+/g, ' ') - .replace(//gs, ''); + current.textRaw = transformTypeReferences( + transformNodesToString( + child.children.filter(node => node.type !== 'list'), + true + ) + .replace(/\s+/g, ' ') + .replace(//gs, '') + ); let text = current.textRaw; @@ -136,9 +136,7 @@ function parseListItem(child, entry) { const optionsNode = child.children.find(child => child.type === 'list'); if (optionsNode) { - current.options = optionsNode.children.map(child => - parseListItem(child, entry) - ); + current.options = optionsNode.children.map(parseListItem); } return current; @@ -148,38 +146,26 @@ function parseListItem(child, entry) { * Parses stability metadata and adds it to the section. * @param {import('../types.d.ts').Section} section - The section to add stability to. * @param {Array} nodes - The AST nodes. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to handle. */ -function parseStability(section, nodes) { - nodes.forEach((node, i) => { - if ( - node.type === 'blockquote' && - node.children.length === 1 && - node.children[0].type === 'paragraph' && - nodes.slice(0, i).every(n => n.type === 'list') - ) { - const text = transformNodesToString(node.children[0].children); - const stabilityMatch = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s.exec(text); - if (stabilityMatch) { - section.stability = Number(stabilityMatch[1]); - section.stabilityText = stabilityMatch[2].replace(/\n/g, ' ').trim(); - nodes.splice(i, 1); // Remove the matched stability node to prevent further processing - } - } - }); +function parseStability(section, nodes, entry) { + const json = entry.stability.toJSON()[0]; + if (json) { + section.stability = json.index; + section.stabilityText = json.description; + nodes.splice(0, 1); + } } /** * Parses a list and updates the section accordingly. * @param {import('../types.d.ts').Section} section - The section to update. * @param {Array} nodes - The AST nodes. - * @param {import('../types.d.ts').HierarchizedEntry} entry - The associated entry. */ -function parseList(section, nodes, entry) { +function parseList(section, nodes) { const list = nodes[0]?.type === 'list' ? nodes.shift() : null; - const values = list - ? list.children.map(child => parseListItem(child, entry)) - : []; + const values = list ? list.children.map(parseListItem) : []; switch (section.type) { case 'ctor': @@ -296,8 +282,8 @@ function handleEntry(entry, parentSection) { const [headingNode, ...nodes] = structuredClone(entry.content.children); const section = createSection(entry, headingNode); - parseStability(section, nodes); - parseList(section, nodes, entry); + parseStability(section, nodes, entry); + parseList(section, nodes); addDescription(section, nodes); handleChildren(entry, section); addAdditionalMetadata(section, parentSection, headingNode); diff --git a/src/metadata.mjs b/src/metadata.mjs index ae006353..ff1a705c 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -155,7 +155,6 @@ const createMetadata = slugger => { content: section, tags, introduced_in, - rawContent: apiDoc.toString(), }; }, }; diff --git a/src/queries.mjs b/src/queries.mjs index 1db3f23c..79e40030 100644 --- a/src/queries.mjs +++ b/src/queries.mjs @@ -1,7 +1,7 @@ 'use strict'; import { u as createTree } from 'unist-builder'; -import { SKIP, visit } from 'unist-util-visit'; +import { SKIP } from 'unist-util-visit'; import { DOC_API_STABILITY_SECTION_REF_URL } from './constants.mjs'; @@ -76,23 +76,26 @@ const createQueries = () => { * @param {import('mdast').Parent} parent The parent node */ const updateTypeReference = (node, parent) => { - const replacedTypes = node.value.replace( - createQueries.QUERIES.normalizeTypes, - transformTypeToReferenceLink - ); - - // This changes the type into a link by splitting it up - // into several nodes, and adding those nodes to the - // parent. + const replacedTypes = node.value + .replace( + createQueries.QUERIES.normalizeTypes, + transformTypeToReferenceLink + ) + // Remark doesn't handle leading / trailing spaces, so replace them with + // HTML entities. + .replace(/^\s/, ' ') + .replace(/\s$/, ' '); + + // This changes the type into a link by splitting it up into several nodes, + // and adding those nodes to the parent. const { children: [newNode], } = remark.parse(replacedTypes); + + // Find the index of the original node in the parent const index = parent.children.indexOf(node); - const originalPosition = node.position; - visit(newNode, node => { - (node.position.start += originalPosition.start), - (node.position.end += originalPosition.end); - }); + + // Replace the original node with the new node(s) parent.children.splice(index, 1, ...newNode.children); return [SKIP]; diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index cee82400..96c20819 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -73,7 +73,6 @@ describe('createMetadata', () => { heading, n_api_version: undefined, introduced_in: undefined, - rawContent: '', removed_in: undefined, slug: 'test-heading', source_link: 'test.com', diff --git a/src/types.d.ts b/src/types.d.ts index e372cce7..f40eb7fe 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -98,8 +98,6 @@ declare global { // Extra YAML section entries that are stringd and serve // to provide additional metadata about the API doc entry tags: Array; - // The raw file content - rawContent: string; } export interface ApiDocReleaseEntry { From 8becb05b54b9a96bf4cc51723cc7913c0c959b57 Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:12:18 -0500 Subject: [PATCH 4/8] code review (3) --- src/generators/legacy-json/types.d.ts | 228 ++++++++++++++++-- .../legacy-json/utils/buildSection.mjs | 116 +-------- .../legacy-json/utils/parseList.mjs | 114 +++++++++ .../legacy-json/utils/parseSignature.mjs | 6 +- 4 files changed, 328 insertions(+), 136 deletions(-) create mode 100644 src/generators/legacy-json/utils/parseList.mjs diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts index 95180150..85f83d75 100644 --- a/src/generators/legacy-json/types.d.ts +++ b/src/generators/legacy-json/types.d.ts @@ -1,83 +1,275 @@ import { ListItem } from 'mdast'; +/** + * Represents an entry in a hierarchical structure, extending from ApiDocMetadataEntry. + * It includes children entries organized in a hierarchy. + */ export interface HierarchizedEntry extends ApiDocMetadataEntry { - hierarchyChildren: Array; + /** + * List of child entries that are part of this entry's hierarchy. + */ + hierarchyChildren: ApiDocMetadataEntry[]; } +/** + * Contains metadata related to changes, additions, removals, and deprecated statuses of an entry. + */ export interface Meta { - changes: Array; - added?: Array; - napiVersion?: Array; - deprecated?: Array; - removed?: Array; + /** + * A list of changes associated with the entry. + */ + changes: ApiDocMetadataChange[]; + + /** + * A list of added versions or entities for the entry. + */ + added: string[]; + + /** + * A list of NAPI (Node API) versions related to the entry. + */ + napiVersion: string[]; + + /** + * A list of versions where the entry was deprecated. + */ + deprecated: string[]; + + /** + * A list of versions where the entry was removed. + */ + removed: string[]; } +/** + * Base interface for sections in the API documentation, representing common properties. + */ export interface SectionBase { + /** + * The type of section (e.g., 'module', 'method', 'property'). + */ type: string; + + /** + * The name of the section. + */ name: string; + + /** + * Raw text content associated with the section. + */ textRaw: string; + + /** + * Display name of the section. + */ displayName?: string; + + /** + * A detailed description of the section. + */ desc: string; + + /** + * A brief description of the section. + */ shortDesc?: string; + + /** + * Stability index of the section. + */ stability?: number; + + /** + * Descriptive text related to the stability of the section (E.G. "Experimental"). + */ stabilityText?: string; - meta?: Meta; + + /** + * Metadata associated with the section. + */ + meta: Meta; } +/** + * Represents a module section, which can contain other modules, classes, methods, properties, and other sections. + */ export interface ModuleSection extends SectionBase { + /** + * The type of section. Always 'module' for this interface. + */ type: 'module'; + + /** + * Source of the module (File path). + */ source: string; - miscs?: Array; - modules?: Array; - classes?: Array; - methods?: Array; - properties?: Array; + + /** + * Miscellaneous sections associated with the module. + */ + miscs?: MiscSection[]; + + /** + * Submodules within this module. + */ + modules?: ModuleSection[]; + + /** + * Classes within this module. + */ + classes?: SignatureSection[]; + + /** + * Methods within this module. + */ + methods?: MethodSignature[]; + + /** + * Properties within this module. + */ + properties?: PropertySection[]; + + /** + * Global definitions associated with the module. + */ globals?: ModuleSection | { type: 'global' }; - signatures?: Array; + + /** + * Signatures (e.g., functions, methods) associated with this module. + */ + signatures?: SignatureSection[]; } +/** + * Represents a signature section for methods, constructors, or classes. + */ export interface SignatureSection extends SectionBase { + /** + * The type of section. It can be one of 'class', 'ctor' (constructor), 'classMethod', or 'method'. + */ type: 'class' | 'ctor' | 'classMethod' | 'method'; - signatures: Array; + + /** + * A list of method signatures within this section. + */ + signatures: MethodSignature[]; } +/** + * All possible types of sections. + */ export type Section = | SignatureSection | PropertySection | EventSection | MiscSection; +/** + * Represents a parameter for methods or functions. + */ export interface Parameter { + /** + * The name of the parameter. + */ name: string; + + /** + * Indicates if the parameter is optional. + */ optional?: boolean; + + /** + * The default value for the parameter. + */ default?: string; } +/** + * Represents a method signature, including its parameters and return type. + */ export interface MethodSignature { - params: Array; + /** + * A list of parameters for the method. + */ + params: Parameter[]; + + /** + * The return type of the method. + */ return?: string; } +/** + * Represents a property section in the API documentation. + */ export interface PropertySection extends SectionBase { + /** + * The type of section. Always 'property' for this interface. + */ type: 'property'; + + /** + * Arbitrary key-value pairs for the property. + */ [key: string]: string | undefined; } +/** + * Represents an event section, typically containing event parameters. + */ export interface EventSection extends SectionBase { + /** + * The type of section. Always 'event' for this interface. + */ type: 'event'; - params: Array; + + /** + * A list of parameters associated with the event. + */ + params: ListItem[]; } +/** + * Represents a miscellaneous section with arbitrary content. + */ export interface MiscSection extends SectionBase { + /** + * The type of section. Always 'misc' for this interface. + */ type: 'misc'; + [key: string]: string | undefined; } -export interface List { +/** + * Represents a list of parameters. + */ +export interface ParameterList { + /** + * Raw parameter description + */ textRaw: string; + + /** + * A short description of the parameter. + */ desc?: string; + + /** + * The name of the parameter. + */ name: string; + + /** + * The type of the parameter (E.G. string, boolean). + */ type?: string; + + /** + * The default value. + */ default?: string; - options?: List; + + options?: ParameterList; } diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 9d21e912..9a3690db 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -1,14 +1,7 @@ -import { - DEFAULT_EXPRESSION, - LEADING_HYPHEN, - NAME_EXPRESSION, - RETURN_EXPRESSION, - TYPE_EXPRESSION, -} from '../constants.mjs'; import { buildHierarchy } from './buildHierarchy.mjs'; -import parseSignature from './parseSignature.mjs'; import { getRemarkRehype } from '../../../utils/remark.mjs'; import { transformNodesToString } from '../../../utils/unist.mjs'; +import { parseList } from './parseList.mjs'; const sectionTypePlurals = { module: 'modules', @@ -71,77 +64,6 @@ function createSection(entry, head) { }; } -/** - * - * @param {String} string - * @returns {String} - */ -function transformTypeReferences(string) { - // console.log(string) - return string.replaceAll(/`<([^>]+)>`/g, '{$1}').replaceAll('} | {', '|'); -} - -/** - * Parses a list item to extract properties. - * @param {import('mdast').ListItem} child - The list item node. - * @returns {import('../types.d.ts').List} The parsed list. - */ -function parseListItem(child) { - const current = {}; - - /** - * Extracts a pattern from text and assigns it to the current object. - * @param {string} text - * @param {RegExp} pattern - * @param {string} key - * @returns {string} - */ - const extractPattern = (text, pattern, key) => { - const [, match] = text.match(pattern) || []; - if (match) { - current[key] = match.trim().replace(/\.$/, ''); - return text.replace(pattern, ''); - } - return text; - }; - - // Combine and clean text from child nodes, excluding nested lists - current.textRaw = transformTypeReferences( - transformNodesToString( - child.children.filter(node => node.type !== 'list'), - true - ) - .replace(/\s+/g, ' ') - .replace(//gs, '') - ); - - let text = current.textRaw; - - // Determine if the current item is a return statement - if (RETURN_EXPRESSION.test(text)) { - current.name = 'return'; - text = text.replace(RETURN_EXPRESSION, ''); - } else { - text = extractPattern(text, NAME_EXPRESSION, 'name'); - } - - // Extract type and default values if present - text = extractPattern(text, TYPE_EXPRESSION, 'type'); - text = extractPattern(text, DEFAULT_EXPRESSION, 'default'); - - // Assign the remaining text as the description after removing leading hyphens - current.desc = text.replace(LEADING_HYPHEN, '').trim() || undefined; - - // Recursively parse nested options if a list is found within the list item - const optionsNode = child.children.find(child => child.type === 'list'); - - if (optionsNode) { - current.options = optionsNode.children.map(parseListItem); - } - - return current; -} - /** * Parses stability metadata and adds it to the section. * @param {import('../types.d.ts').Section} section - The section to add stability to. @@ -157,42 +79,6 @@ function parseStability(section, nodes, entry) { } } -/** - * Parses a list and updates the section accordingly. - * @param {import('../types.d.ts').Section} section - The section to update. - * @param {Array} nodes - The AST nodes. - */ -function parseList(section, nodes) { - const list = nodes[0]?.type === 'list' ? nodes.shift() : null; - - const values = list ? list.children.map(parseListItem) : []; - - switch (section.type) { - case 'ctor': - case 'classMethod': - case 'method': - section.signatures = [parseSignature(section.textRaw, values)]; - break; - case 'property': - if (values.length) { - const { type, ...rest } = values[0]; - - section.type = type; - Object.assign(section, rest); - section.textRaw = `\`${section.name}\` ${section.textRaw}`; - } - break; - case 'event': - section.params = values; - break; - default: - // If the list wasn't processed, add it back for further processing - if (list) { - nodes.unshift(list); - } - } -} - let lazyHTML; /** diff --git a/src/generators/legacy-json/utils/parseList.mjs b/src/generators/legacy-json/utils/parseList.mjs new file mode 100644 index 00000000..12d9611f --- /dev/null +++ b/src/generators/legacy-json/utils/parseList.mjs @@ -0,0 +1,114 @@ +import { + DEFAULT_EXPRESSION, + LEADING_HYPHEN, + NAME_EXPRESSION, + RETURN_EXPRESSION, + TYPE_EXPRESSION, +} from '../constants.mjs'; +import parseSignature from './parseSignature.mjs'; +import { transformNodesToString } from '../../../utils/unist.mjs'; + +/** + * Transforms type references in a string by replacing template syntax with curly braces and cleaning up. + * @param {String} string + * @returns {String} + */ +function transformTypeReferences(string) { + return string.replace(/`<([^>]+)>`/g, '{$1}').replaceAll('} | {', '|'); +} + +/** + * Extracts a matching pattern from a text and assigns it to the current object. + * @param {string} text + * @param {RegExp} pattern + * @param {string} key + * @param {Object} current + * @returns {string} + */ +const extractPattern = (text, pattern, key, current) => { + const match = text.match(pattern)?.[1]?.trim().replace(/\.$/, ''); + if (match) { + current[key] = match; + return text.replace(pattern, ''); + } + return text; +}; + +/** + * Parses a list item node to extract key properties. + * @param {import('mdast').ListItem} child - The list item node. + * @returns {import('../types').ParameterList} The parsed list item. + */ +function parseListItem(child) { + const current = {}; + + // Clean up and transform the raw text + current.textRaw = transformTypeReferences( + transformNodesToString( + child.children.filter(node => node.type !== 'list'), + true + ) + .replace(/\s+/g, ' ') + .replace(//gs, '') + ); + + let text = current.textRaw; + + // Determine the item type and extract relevant details + if (RETURN_EXPRESSION.test(text)) { + current.name = 'return'; + text = text.replace(RETURN_EXPRESSION, ''); + } else { + text = extractPattern(text, NAME_EXPRESSION, 'name', current); + } + + text = extractPattern(text, TYPE_EXPRESSION, 'type', current); + text = extractPattern(text, DEFAULT_EXPRESSION, 'default', current); + + // Assign the remaining text as description after removing any leading hyphen + current.desc = text.replace(LEADING_HYPHEN, '').trim() || undefined; + + // Recursively parse nested options if a list exists + const optionsNode = child.children.find(node => node.type === 'list'); + if (optionsNode) { + current.options = optionsNode.children.map(parseListItem); + } + + return current; +} + +/** + * Parses a list of nodes and updates the section accordingly. + * @param {import('../types').Section} section - The section to update. + * @param {Array} nodes - The AST nodes. + */ +export function parseList(section, nodes) { + const list = nodes[0]?.type === 'list' ? nodes.shift() : null; + + const values = list ? list.children.map(parseListItem) : []; + + // Handle different section types based on parsed values + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': + section.signatures = [parseSignature(section.textRaw, values)]; + break; + case 'property': + if (values.length) { + const { type, ...rest } = values[0]; + section.type = type; + Object.assign(section, rest); + section.textRaw = `\`${section.name}\` ${section.textRaw}`; + } + break; + case 'event': + section.params = values; + break; + default: + // Re-add list for further processing if not handled + if (list) { + nodes.unshift(list); + } + } +} diff --git a/src/generators/legacy-json/utils/parseSignature.mjs b/src/generators/legacy-json/utils/parseSignature.mjs index c5c303fe..fc3f1acf 100644 --- a/src/generators/legacy-json/utils/parseSignature.mjs +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -77,7 +77,7 @@ function parseDefaultValue(parameterName) { /** * @param {string} parameterName * @param {number} index - * @param {Array} markdownParameters + * @param {Array} markdownParameters * @returns {import('../types.d.ts').Parameter} */ function findParameter(parameterName, index, markdownParameters) { @@ -108,7 +108,7 @@ function findParameter(parameterName, index, markdownParameters) { /** * @param {string[]} declaredParameters - * @param {Array} parameters + * @param {Array} parameters */ function parseParameters(declaredParameters, markdownParameters) { /** @@ -165,7 +165,7 @@ function parseParameters(declaredParameters, markdownParameters) { /** * @param {string} textRaw Something like `new buffer.Blob([sources[, options]])` - * @param {Array { From 541977397805f0f190ff7516b532251f9e5d538f Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:19:55 -0500 Subject: [PATCH 5/8] code review (4) --- src/generators/legacy-json/index.mjs | 4 +- .../legacy-json/utils/buildSection.mjs | 274 +++++++++--------- .../legacy-json/utils/parseList.mjs | 56 ++-- 3 files changed, 168 insertions(+), 166 deletions(-) diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index 08165151..bbaf163c 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -3,7 +3,7 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { groupNodesByModule } from '../../utils/generators.mjs'; -import buildSection from './utils/buildSection.mjs'; +import { createSectionBuilder } from './utils/buildSection.mjs'; /** * This generator is responsible for generating the legacy JSON files for the @@ -28,6 +28,8 @@ export default { dependsOn: 'ast', async generate(input, { output }) { + const buildSection = createSectionBuilder(); + // This array holds all the generated values for each module const generatedValues = []; diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 9a3690db..9b0f9b39 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -17,6 +17,8 @@ const sectionTypePlurals = { var: 'vars', }; +const unpromotedKeys = ['textRaw', 'name', 'type', 'desc', 'miscs']; + /** * Converts a value to an array. * @template T @@ -25,171 +27,161 @@ const sectionTypePlurals = { */ const enforceArray = val => (Array.isArray(val) ? val : [val]); -/** - * Creates metadata from a hierarchized entry. - * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to create metadata from. - * @returns {import('../types.d.ts').Meta} The created metadata. - */ -function createMeta(entry) { - const { +export const createSectionBuilder = () => { + const html = getRemarkRehype(); + + /** + * Creates metadata from a hierarchized entry. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to create metadata from. + * @returns {import('../types.d.ts').Meta} The created metadata. + */ + const createMeta = ({ added_in = [], n_api_version = [], deprecated_in = [], removed_in = [], changes, - } = entry; - - return { + }) => ({ changes, added: enforceArray(added_in), napiVersion: enforceArray(n_api_version), deprecated: enforceArray(deprecated_in), removed: enforceArray(removed_in), - }; -} - -/** - * Creates a section from an entry and its heading. - * @param {import('../types.d.ts').HierarchizedEntry} entry - The AST entry. - * @param {HeadingMetadataParent} head - The head node of the entry. - * @returns {import('../types.d.ts').Section} The created section. - */ -function createSection(entry, head) { - return { + }); + + /** + * Creates a section from an entry and its heading. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The AST entry. + * @param {HeadingMetadataParent} head - The head node of the entry. + * @returns {import('../types.d.ts').Section} The created section. + */ + const createSection = (entry, head) => ({ textRaw: transformNodesToString(head.children), name: head.data.name, type: head.data.type, meta: createMeta(entry), introduced_in: entry.introduced_in, + }); + + /** + * Parses stability metadata and adds it to the section. + * @param {import('../types.d.ts').Section} section - The section to update. + * @param {Array} nodes - The remaining AST nodes. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry providing stability information. + */ + const parseStability = (section, nodes, { stability }) => { + const stabilityInfo = stability.toJSON()?.[0]; + + if (stabilityInfo) { + section.stability = stabilityInfo.index; + section.stabilityText = stabilityInfo.description; + nodes.shift(); // Remove stability node from processing + } }; -} - -/** - * Parses stability metadata and adds it to the section. - * @param {import('../types.d.ts').Section} section - The section to add stability to. - * @param {Array} nodes - The AST nodes. - * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to handle. - */ -function parseStability(section, nodes, entry) { - const json = entry.stability.toJSON()[0]; - if (json) { - section.stability = json.index; - section.stabilityText = json.description; - nodes.splice(0, 1); - } -} - -let lazyHTML; - -/** - * Adds a description to the section. - * @param {import('../types.d.ts').Section} section - The section to add description to. - * @param {Array} nodes - The AST nodes. - */ -function addDescription(section, nodes) { - if (!nodes.length) { - return; - } - - if (section.desc) { - section.shortDesc = section.desc; - } - - lazyHTML ??= getRemarkRehype(); - - const rendered = lazyHTML.stringify( - lazyHTML.runSync({ type: 'root', children: nodes }) - ); - section.desc = rendered || undefined; -} + /** + * Adds a description to the section. + * @param {import('../types.d.ts').Section} section - The section to update. + * @param {Array} nodes - The remaining AST nodes. + */ + const addDescription = (section, nodes) => { + if (!nodes.length) { + return; + } + + const rendered = html.stringify( + html.runSync({ type: 'root', children: nodes }) + ); + + section.shortDesc = section.desc || undefined; + section.desc = rendered || undefined; + }; -/** - * Adds additional metadata to the section based on its type. - * @param {import('../types.d.ts').Section} section - The section to update. - * @param {import('../types.d.ts').Section} parentSection - The parent section. - * @param {import('../../types.d.ts').NodeWithData} headingNode - The heading node. - */ -function addAdditionalMetadata(section, parentSection, headingNode) { - if (!section.type) { - section.name = section.textRaw.toLowerCase().trim().replace(/\s+/g, '_'); - section.displayName = headingNode.data.name; - section.type = parentSection.type === 'misc' ? 'misc' : 'module'; - } -} + /** + * Adds additional metadata to the section based on its type. + * @param {import('../types.d.ts').Section} section - The section to update. + * @param {import('../types.d.ts').Section} parent - The parent section. + * @param {import('../../types.d.ts').NodeWithData} heading - The heading node of the section. + */ + const addAdditionalMetadata = (section, parent, heading) => { + if (!section.type) { + section.name = section.textRaw.toLowerCase().trim().replace(/\s+/g, '_'); + section.displayName = heading.data.name; + section.type = parent.type === 'misc' ? 'misc' : 'module'; + } + }; -/** - * Adds the section to its parent section. - * @param {import('../types.d.ts').Section} section - The section to add. - * @param {import('../types.d.ts').Section} parentSection - The parent section. - */ -function addToParent(section, parentSection) { - const pluralType = sectionTypePlurals[section.type]; + /** + * Adds the section to its parent section. + * @param {import('../types.d.ts').Section} section - The section to add. + * @param {import('../types.d.ts').Section} parent - The parent section. + */ + const addToParent = (section, parent) => { + const key = sectionTypePlurals[section.type] || 'miscs'; - parentSection[pluralType] = parentSection[pluralType] || []; - parentSection[pluralType].push(section); -} + parent[key] ??= []; + parent[key].push(section); + }; -const notTransferredKeys = ['textRaw', 'name', 'type', 'desc', 'miscs']; + /** + * Promotes children properties to the parent level if the section type is 'misc'. + * @param {import('../types.d.ts').Section} section - The section to promote. + * @param {import('../types.d.ts').Section} parent - The parent section. + */ + const promoteMiscChildren = (section, parent) => { + // Only promote if the current section is of type 'misc' and the parent is not 'misc' + if (section.type === 'misc' && parent.type !== 'misc') { + Object.entries(section).forEach(([key, value]) => { + // Only promote certain keys + if (!unpromotedKeys.includes(key)) { + // Merge the section's properties into the parent section + parent[key] = parent[key] + ? // If the parent already has this key, concatenate the values + [].concat(parent[key], value) + : // Otherwise, directly assign the section's value to the parent + []; + } + }); + } + }; -/** - * Promotes children properties to the parent level if the section type is 'misc'. - * - * @param {import('../types.d.ts').Section} section - The section to promote. - * @param {import('../types.d.ts').Section} parentSection - The parent section. - */ -const makeChildrenTopLevelIfMisc = (section, parentSection) => { - // Only promote if the current section is of type 'misc' and the parent is not 'misc' - if (section.type === 'misc' && parentSection.type !== 'misc') { - Object.entries(section).forEach(([key, value]) => { - // Skip keys that should not be transferred - if (notTransferredKeys.includes(key)) return; - - // Merge the section's properties into the parent section - parentSection[key] = parentSection[key] - ? // If the parent already has this key, concatenate the values - [].concat(parentSection[key], value) - : // Otherwise, directly assign the section's value to the parent - value; - }); - } -}; + /** + * Processes children of a given entry and updates the section. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The current entry. + * @param {import('../types.d.ts').Section} section - The current section. + */ + const handleChildren = ({ hierarchyChildren }, section) => + hierarchyChildren?.forEach(child => handleEntry(child, section)); + + /** + * Handles an entry and updates the parent section. + * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to process. + * @param {import('../types.d.ts').Section} parent - The parent section. + */ + const handleEntry = (entry, parent) => { + const [headingNode, ...nodes] = structuredClone(entry.content.children); + const section = createSection(entry, headingNode); + + parseStability(section, nodes, entry); + parseList(section, nodes); + addDescription(section, nodes); + handleChildren(entry, section); + addAdditionalMetadata(section, parent, headingNode); + addToParent(section, parent); + promoteMiscChildren(section, parent); + }; -const handleChildren = (entry, section) => { - entry.hierarchyChildren?.forEach(child => handleEntry(child, section)); -}; + /** + * Builds the module section from head metadata and entries. + * @param {ApiDocMetadataEntry} head - The head metadata entry. + * @param {Array} entries - The list of metadata entries. + * @returns {import('../types.d.ts').ModuleSection} The constructed module section. + */ + return (head, entries) => { + const rootModule = { type: 'module', source: head.api_doc_source }; -/** - * Handles an entry and updates the parent section. - * @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to handle. - * @param {import('../types.d.ts').Section} parentSection - The parent section. - */ -function handleEntry(entry, parentSection) { - const [headingNode, ...nodes] = structuredClone(entry.content.children); - const section = createSection(entry, headingNode); - - parseStability(section, nodes, entry); - parseList(section, nodes); - addDescription(section, nodes); - handleChildren(entry, section); - addAdditionalMetadata(section, parentSection, headingNode); - addToParent(section, parentSection); - makeChildrenTopLevelIfMisc(section, parentSection); -} + buildHierarchy(entries).forEach(entry => handleEntry(entry, rootModule)); -/** - * Builds the module section from head and entries. - * @param {ApiDocMetadataEntry} head - The head metadata entry. - * @param {Array} entries - The list of metadata entries. - * @returns {import('../types.d.ts').ModuleSection} The constructed module section. - */ -export default (head, entries) => { - const rootModule = { - type: 'module', - source: head.api_doc_source, + return rootModule; }; - - buildHierarchy(entries).forEach(entry => handleEntry(entry, rootModule)); - - return rootModule; }; diff --git a/src/generators/legacy-json/utils/parseList.mjs b/src/generators/legacy-json/utils/parseList.mjs index 12d9611f..c4884ff0 100644 --- a/src/generators/legacy-json/utils/parseList.mjs +++ b/src/generators/legacy-json/utils/parseList.mjs @@ -9,16 +9,17 @@ import parseSignature from './parseSignature.mjs'; import { transformNodesToString } from '../../../utils/unist.mjs'; /** - * Transforms type references in a string by replacing template syntax with curly braces and cleaning up. - * @param {String} string - * @returns {String} + * Modifies type references in a string by replacing template syntax (`<...>`) with curly braces `{...}` + * and normalizing formatting. + * @param {string} string + * @returns {string} */ function transformTypeReferences(string) { return string.replace(/`<([^>]+)>`/g, '{$1}').replaceAll('} | {', '|'); } /** - * Extracts a matching pattern from a text and assigns it to the current object. + * Extracts and removes a specific pattern from a text string while storing the result in a key of the `current` object. * @param {string} text * @param {RegExp} pattern * @param {string} key @@ -27,34 +28,34 @@ function transformTypeReferences(string) { */ const extractPattern = (text, pattern, key, current) => { const match = text.match(pattern)?.[1]?.trim().replace(/\.$/, ''); - if (match) { - current[key] = match; - return text.replace(pattern, ''); + + if (!match) { + return text; } - return text; + + current[key] = match; + return text.replace(pattern, ''); }; /** - * Parses a list item node to extract key properties. - * @param {import('mdast').ListItem} child - The list item node. - * @returns {import('../types').ParameterList} The parsed list item. + * Parses an individual list item node to extract its properties + * + * @param {import('mdast').ListItem} child + * @returns {import('../types').ParameterList} */ function parseListItem(child) { const current = {}; - // Clean up and transform the raw text + // Extract and clean raw text from the node, excluding nested lists current.textRaw = transformTypeReferences( - transformNodesToString( - child.children.filter(node => node.type !== 'list'), - true - ) + transformNodesToString(child.children.filter(node => node.type !== 'list')) .replace(/\s+/g, ' ') .replace(//gs, '') ); let text = current.textRaw; - // Determine the item type and extract relevant details + // Identify return items or extract key properties (name, type, default) from the text if (RETURN_EXPRESSION.test(text)) { current.name = 'return'; text = text.replace(RETURN_EXPRESSION, ''); @@ -65,10 +66,10 @@ function parseListItem(child) { text = extractPattern(text, TYPE_EXPRESSION, 'type', current); text = extractPattern(text, DEFAULT_EXPRESSION, 'default', current); - // Assign the remaining text as description after removing any leading hyphen + // Set the remaining text as the description, removing any leading hyphen current.desc = text.replace(LEADING_HYPHEN, '').trim() || undefined; - // Recursively parse nested options if a list exists + // Parse nested lists (options) recursively if present const optionsNode = child.children.find(node => node.type === 'list'); if (optionsNode) { current.options = optionsNode.children.map(parseListItem); @@ -78,23 +79,27 @@ function parseListItem(child) { } /** - * Parses a list of nodes and updates the section accordingly. - * @param {import('../types').Section} section - The section to update. - * @param {Array} nodes - The AST nodes. + * Parses a list of nodes and updates the corresponding section object with the extracted information. + * Handles different section types such as methods, properties, and events differently. + * @param {import('../types').Section} section + * @param {import('mdast').RootContent[]} nodes */ export function parseList(section, nodes) { const list = nodes[0]?.type === 'list' ? nodes.shift() : null; const values = list ? list.children.map(parseListItem) : []; - // Handle different section types based on parsed values + // Update the section based on its type and parsed values switch (section.type) { case 'ctor': case 'classMethod': case 'method': + // For methods and constructors, parse and attach signatures section.signatures = [parseSignature(section.textRaw, values)]; break; + case 'property': + // For properties, update type and other details if values exist if (values.length) { const { type, ...rest } = values[0]; section.type = type; @@ -102,11 +107,14 @@ export function parseList(section, nodes) { section.textRaw = `\`${section.name}\` ${section.textRaw}`; } break; + case 'event': + // For events, assign parsed values as parameters section.params = values; break; + default: - // Re-add list for further processing if not handled + // If no specific handling, re-add the list for further processing if (list) { nodes.unshift(list); } From fb9da69038bebc8b3ad4ce9855fd066d4677b5b3 Mon Sep 17 00:00:00 2001 From: RedYetiDev Date: Mon, 25 Nov 2024 07:55:11 -0500 Subject: [PATCH 6/8] resolve review --- src/generators/legacy-json-all/index.mjs | 6 +++++ src/generators/legacy-json/constants.mjs | 18 ++++++++++++++ src/generators/legacy-json/index.mjs | 12 +++++++--- .../legacy-json/utils/buildHierarchy.mjs | 15 ++++++------ .../legacy-json/utils/buildSection.mjs | 24 +++++-------------- .../legacy-json/utils/parseSignature.mjs | 1 + 6 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index d7d4a3cb..fac03d61 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -21,6 +21,12 @@ export default { dependsOn: 'legacy-json', + /** + * + * @param input + * @param root0 + * @param root0.output + */ async generate(input, { output }) { /** * The consolidated output object that will contain diff --git a/src/generators/legacy-json/constants.mjs b/src/generators/legacy-json/constants.mjs index b999dea7..cdfd3647 100644 --- a/src/generators/legacy-json/constants.mjs +++ b/src/generators/legacy-json/constants.mjs @@ -16,3 +16,21 @@ export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; // 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 = /\((.+)\);?$/; + +// The plurals associated with each section type. +export const SECTION_TYPE_PLURALS = { + module: 'modules', + misc: 'miscs', + class: 'classes', + method: 'methods', + property: 'properties', + global: 'globals', + example: 'examples', + ctor: 'signatures', + classMethod: 'classMethods', + event: 'events', + var: 'vars', +}; + +// The keys to not promote when promoting children. +export const UNPROMOTED_KEYS = ['textRaw', 'name', 'type', 'desc', 'miscs']; diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index bbaf163c..e55dde34 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -7,12 +7,12 @@ import { createSectionBuilder } 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. + * 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. + * config. * * @typedef {Array} Input * @@ -27,6 +27,12 @@ export default { dependsOn: 'ast', + /** + * + * @param input + * @param root0 + * @param root0.output + */ async generate(input, { output }) { const buildSection = createSectionBuilder(); diff --git a/src/generators/legacy-json/utils/buildHierarchy.mjs b/src/generators/legacy-json/utils/buildHierarchy.mjs index 340ad7ec..b4ef2d19 100644 --- a/src/generators/legacy-json/utils/buildHierarchy.mjs +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -3,6 +3,7 @@ * * @param {ApiDocMetadataEntry} entry * @param {ApiDocMetadataEntry[]} entry + * @param entries * @param {number} startIdx * @returns {import('../types.d.ts').HierarchizedEntry} */ @@ -29,19 +30,19 @@ function findParent(entry, entries, startIdx) { /** * 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. + * 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. + * 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. + * 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} diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 9b0f9b39..c284286b 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -2,22 +2,7 @@ import { buildHierarchy } from './buildHierarchy.mjs'; import { getRemarkRehype } from '../../../utils/remark.mjs'; import { transformNodesToString } from '../../../utils/unist.mjs'; import { parseList } from './parseList.mjs'; - -const sectionTypePlurals = { - module: 'modules', - misc: 'miscs', - class: 'classes', - method: 'methods', - property: 'properties', - global: 'globals', - example: 'examples', - ctor: 'signatures', - classMethod: 'classMethods', - event: 'events', - var: 'vars', -}; - -const unpromotedKeys = ['textRaw', 'name', 'type', 'desc', 'miscs']; +import { SECTION_TYPE_PLURALS, UNPROMOTED_KEYS } from '../constants.mjs'; /** * Converts a value to an array. @@ -27,6 +12,9 @@ const unpromotedKeys = ['textRaw', 'name', 'type', 'desc', 'miscs']; */ const enforceArray = val => (Array.isArray(val) ? val : [val]); +/** + * + */ export const createSectionBuilder = () => { const html = getRemarkRehype(); @@ -117,7 +105,7 @@ export const createSectionBuilder = () => { * @param {import('../types.d.ts').Section} parent - The parent section. */ const addToParent = (section, parent) => { - const key = sectionTypePlurals[section.type] || 'miscs'; + const key = SECTION_TYPE_PLURALS[section.type] || 'miscs'; parent[key] ??= []; parent[key].push(section); @@ -133,7 +121,7 @@ export const createSectionBuilder = () => { if (section.type === 'misc' && parent.type !== 'misc') { Object.entries(section).forEach(([key, value]) => { // Only promote certain keys - if (!unpromotedKeys.includes(key)) { + if (!UNPROMOTED_KEYS.includes(key)) { // Merge the section's properties into the parent section parent[key] = parent[key] ? // If the parent already has this key, concatenate the values diff --git a/src/generators/legacy-json/utils/parseSignature.mjs b/src/generators/legacy-json/utils/parseSignature.mjs index fc3f1acf..415bb176 100644 --- a/src/generators/legacy-json/utils/parseSignature.mjs +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -109,6 +109,7 @@ function findParameter(parameterName, index, markdownParameters) { /** * @param {string[]} declaredParameters * @param {Array} parameters + * @param markdownParameters */ function parseParameters(declaredParameters, markdownParameters) { /** From 158869e9ca021dcf0452c6d67556dd332e9fae69 Mon Sep 17 00:00:00 2001 From: RedYetiDev Date: Mon, 25 Nov 2024 08:12:51 -0500 Subject: [PATCH 7/8] lint jsdoc --- src/generators/legacy-json-all/index.mjs | 6 +++--- src/generators/legacy-json/index.mjs | 6 +++--- src/generators/legacy-json/utils/parseSignature.mjs | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs index fac03d61..9bb36310 100644 --- a/src/generators/legacy-json-all/index.mjs +++ b/src/generators/legacy-json-all/index.mjs @@ -22,10 +22,10 @@ export default { dependsOn: 'legacy-json', /** + * Generates the legacy JSON `all.json` file. * - * @param input - * @param root0 - * @param root0.output + * @param {Input} input + * @param {Partial} options */ async generate(input, { output }) { /** diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index e55dde34..8cf70e88 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -28,10 +28,10 @@ export default { dependsOn: 'ast', /** + * Generates a legacy JSON file. * - * @param input - * @param root0 - * @param root0.output + * @param {Input} input + * @param {Partial} options */ async generate(input, { output }) { const buildSection = createSectionBuilder(); diff --git a/src/generators/legacy-json/utils/parseSignature.mjs b/src/generators/legacy-json/utils/parseSignature.mjs index 415bb176..9c17289b 100644 --- a/src/generators/legacy-json/utils/parseSignature.mjs +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -108,8 +108,7 @@ function findParameter(parameterName, index, markdownParameters) { /** * @param {string[]} declaredParameters - * @param {Array} parameters - * @param markdownParameters + * @param {Array} markdownParameters */ function parseParameters(declaredParameters, markdownParameters) { /** From 420d7c5fa843e9582fbcd74ed9505b88c039c6aa Mon Sep 17 00:00:00 2001 From: RedYetiDev Date: Mon, 25 Nov 2024 08:13:42 -0500 Subject: [PATCH 8/8] fixup! lint jsdoc --- src/generators/legacy-json/utils/buildHierarchy.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/generators/legacy-json/utils/buildHierarchy.mjs b/src/generators/legacy-json/utils/buildHierarchy.mjs index b4ef2d19..ed2b4143 100644 --- a/src/generators/legacy-json/utils/buildHierarchy.mjs +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -2,8 +2,7 @@ * Recursively finds the most suitable parent entry for a given `entry` based on heading depth. * * @param {ApiDocMetadataEntry} entry - * @param {ApiDocMetadataEntry[]} entry - * @param entries + * @param {ApiDocMetadataEntry[]} entries * @param {number} startIdx * @returns {import('../types.d.ts').HierarchizedEntry} */