diff --git a/package-lock.json b/package-lock.json index 21681eb..0f1063b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "html-minifier-terser": "^7.2.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", + "remark-html": "^16.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-stringify": "^11.0.0", @@ -1388,6 +1389,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-sanitize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz", + "integrity": "sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.2.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", @@ -3255,6 +3271,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-html": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-16.0.1.tgz", + "integrity": "sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "hast-util-sanitize": "^5.0.0", + "hast-util-to-html": "^9.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", diff --git a/package.json b/package.json index ad2e1d2..19f703e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "html-minifier-terser": "^7.2.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", + "remark-html": "^16.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-stringify": "^11.0.0", diff --git a/src/generators/index.mjs b/src/generators/index.mjs index 5b916d8..ddd4d73 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -3,9 +3,13 @@ import jsonSimple from './json-simple/index.mjs'; import legacyHtml from './legacy-html/index.mjs'; import legacyHtmlAll from './legacy-html-all/index.mjs'; +import legacyJson from './legacy-json/index.mjs'; +import legacyJsonAll from './legacy-json-all/index.mjs'; export default { 'json-simple': jsonSimple, 'legacy-html': legacyHtml, 'legacy-html-all': legacyHtmlAll, + 'legacy-json': legacyJson, + 'legacy-json-all': legacyJsonAll, }; diff --git a/src/generators/legacy-json-all/index.mjs b/src/generators/legacy-json-all/index.mjs new file mode 100644 index 0000000..f596ef8 --- /dev/null +++ b/src/generators/legacy-json-all/index.mjs @@ -0,0 +1,58 @@ +'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), + 'utf8' + ); + + 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 0000000..0748a31 --- /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 0000000..b999dea --- /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 0000000..8ed7e61 --- /dev/null +++ b/src/generators/legacy-json/index.mjs @@ -0,0 +1,68 @@ +'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), + 'utf8' + ); + }) + ); + + return generatedValues; + }, +}; diff --git a/src/generators/legacy-json/types.d.ts b/src/generators/legacy-json/types.d.ts new file mode 100644 index 0000000..9518015 --- /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 0000000..020f1d3 --- /dev/null +++ b/src/generators/legacy-json/utils/buildHierarchy.mjs @@ -0,0 +1,63 @@ +/** + * 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 = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const currentDepth = entry.heading.depth; + + if (currentDepth <= 1) { + // We're a top-level entry + roots.push(entry); + continue; + } + + const previousEntry = entries[i - 1]; + + const previousDepth = previousEntry.heading.depth; + if (currentDepth > previousDepth) { + // We're a child of the previous one + if (previousEntry.hierarchyChildren === undefined) { + previousEntry.hierarchyChildren = []; + } + + previousEntry.hierarchyChildren.push(entry); + } else { + if (i < 2) { + throw new Error(`can't find parent since i < 2 (${i})`); + } + + // Loop to find the entry we're a child of + for (let j = i - 2; j >= 0; j--) { + const jEntry = entries[j]; + const jDepth = jEntry.heading.depth; + + if (currentDepth > jDepth) { + // Found it + jEntry.hierarchyChildren.push(entry); + break; + } + } + } + } + + 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 0000000..128df02 --- /dev/null +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -0,0 +1,392 @@ +import { unified } from 'unified'; +import html from 'remark-html'; +import { + DEFAULT_EXPRESSION, + LEADING_HYPHEN, + NAME_EXPRESSION, + PARAM_EXPRESSION, + RETURN_EXPRESSION, + TYPE_EXPRESSION, +} from '../constants.mjs'; +import { buildHierarchy } from './buildHierarchy.mjs'; +import parseSignature from './parseSignature.mjs'; + +const sectionTypePlurals = { + module: 'modules', + misc: 'miscs', + class: 'classes', + method: 'methods', + property: 'properties', + global: 'globals', + example: 'examples', + // Constructors should go under a class sections' signatures property + ctor: 'signatures', + classMethod: 'classMethods', + event: 'events', + var: 'vars', +}; + +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @returns {import('../types.d.ts').Meta | undefined} + */ +function createMeta(entry) { + const makeArrayIfNotAlready = val => (Array.isArray(val) ? val : [val]); + + const { added_in, n_api_version, deprecated_in, removed_in, changes } = entry; + return { + changes, + added: added_in ? makeArrayIfNotAlready(added_in) : undefined, + napiVersion: n_api_version + ? makeArrayIfNotAlready(n_api_version) + : undefined, + deprecated: deprecated_in + ? makeArrayIfNotAlready(deprecated_in) + : undefined, + removed: removed_in ? makeArrayIfNotAlready(removed_in) : undefined, + }; +} + +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry Section's AST entry + * @param {HeadingMetadataParent} head Head node of the entry + * @returns {import('../types.d.ts').Section} + */ +function createSection(entry, head) { + const text = textJoin(head.children); + + return { + textRaw: text, + type: head.data.type, + name: text.toLowerCase().replaceAll(' ', '_'), + displayName: head.data.name, + meta: createMeta(entry), + introduced_in: entry.introduced_in, + }; +} + +/** + * @param {Array} nodes + */ +function textJoin(nodes) { + return nodes + .map(node => { + switch (node.type) { + case 'strong': + return `**${textJoin(node.children)}**`; + case 'emphasis': + return `_${textJoin(node.children)}_`; + case 'link': { + return `[${node.label}][]`; + } + default: + if (node.children) { + return textJoin(node.children); + } + + return node.value; + } + }) + .join(''); +} + +/** + * Find name, type, default, desc properties + * @param {import('mdast').ListItem} child + * @returns {import('../types.d.ts').List} + */ +function parseListItem(child) { + /** + * @type {import('../types.d.ts').List} + */ + const current = {}; + + current.textRaw = textJoin( + child.children.filter(node => node.type !== 'list') + ) + .replace(/\s+/g, ' ') + .replaceAll(//gs, ''); + + if (!current.textRaw) { + throw new Error(`empty list item: ${JSON.stringify(child)}`); + } + + let text = current.textRaw; + + // Extract name + if (RETURN_EXPRESSION.test(text)) { + current.name = 'return'; + + let matchResult = text.match(/`(.*?)`/); + if (matchResult) { + let returnType = matchResult[1]; + returnType = returnType.substring(1, returnType.length - 1); + current.type = returnType; + } + + text = text.replace(RETURN_EXPRESSION, ''); + } else { + const [, name] = text.match(NAME_EXPRESSION) || []; + if (name) { + current.name = name; + text = text.replace(NAME_EXPRESSION, ''); + } + + // Extract type (if provided) + const [, type] = text.match(TYPE_EXPRESSION) || []; + if (type) { + current.type = type; + text = text.replace(TYPE_EXPRESSION, ''); + } + } + + // Remove leading hyphens + text = text.replace(LEADING_HYPHEN, ''); + + // Extract default value (if exists) + const [, defaultValue] = text.match(DEFAULT_EXPRESSION) || []; + if (defaultValue) { + current.default = defaultValue.replace(/\.$/, ''); + text = text.replace(DEFAULT_EXPRESSION, ''); + } + + // Add remaining text to the desc + if (text) { + current.desc = text; + } + + const options = child.children.find(child => child.type === 'list'); + if (options) { + current.options = options.children.map(child => parseListItem(child)); + } + + return current; +} + +/** + * @param {import('../types.d.ts').HierarchizedEntry} entry + * @param {import('../types.d.ts').Section} parentSection + */ +function handleEntry(entry, parentSection) { + // Clone the children so we don't mess with any other generators + let [headingNode, ...nodes] = structuredClone(entry.content.children); + + /** + * @returns {import('../types.d.ts').Section} + */ + const setupSection = () => { + // Create the section object with base data we know now + const section = createSection(entry, headingNode); + + // Get the plural type of the section (e.g. 'modules' for type 'module') + const pluralType = sectionTypePlurals[section.type]; + + // Check if our parent section has a array property with the plural type + // already, create it if not + if (!(pluralType in parentSection)) { + parentSection[pluralType] = []; + } + + // Add this section to our parent + parentSection[sectionTypePlurals[section.type]].push(section); + + return section; + }; + + /** + * Grabs stability number & text and adds it to the section + * @param {import('../types.d.ts').Section} section + */ + const parseStability = section => { + let needsCompressing = false; + + // Remove metadata not directly the inferable from the markdown + nodes.forEach((node, i) => { + if ( + node.type === 'blockquote' && + node.children.length === 1 && + node.children[0].type === 'paragraph' && + nodes.slice(0, i).every(node => node.type === 'list') + ) { + const text = textJoin(node.children[0].children); + const stability = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s.exec(text); + if (stability) { + section.stability = parseInt(stability[1], 10); + section.stabilityText = stability[2].replaceAll('\n', ' ').trim(); + + delete nodes[i]; + needsCompressing = true; + } + } + }); + + if (needsCompressing) { + // Compress to remove the holes left by deletions + nodes = nodes.filter(() => true); + } + }; + + /** + * + * @param {import('../types.d.ts').Section} section + */ + const parseListIfThereIsOne = section => { + const list = + nodes.length && nodes[0].type === 'list' ? nodes.shift() : null; + if (!list) { + return; + } + + /** + * @type {Array} + */ + const values = list ? list.children.map(child => parseListItem(child)) : []; + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': { + section.signatures = [parseSignature(section.textRaw, values)]; + + break; + } + + case 'property': { + if (!values.length) { + break; + } + + const signature = values[0]; + signature.textRaw = `\`${section.name}\` ${signature.textRaw}`; + + for (const key in signature) { + if (!signature[key]) { + continue; + } + + if (key === 'type') { + // We'll set propertySigType to type at the end since we still need the + // original type for a few more checks + section.propertySigType = signature.type; + } else { + section[key] = signature[key]; + } + } + + break; + } + + case 'event': + section.params = values; + break; + + default: + // List wasn't consumed, add it back + nodes.unshift(list); + } + }; + + /** + * @param {import('../types.d.ts').Section} section + */ + const addDescription = section => { + if (nodes.length === 0) { + return; + } + + if (section.desc) { + section.shortDesc = section.desc; + } + + // Render the description as if it was html + section.desc = unified() + .use(function () { + this.Parser = () => ({ type: 'root', children: nodes }); + }) + .use(html, { sanitize: false }) + .processSync('') + .toString() + .trim(); + + if (!section.desc) { + // Rendering returned nothing + delete section.desc; + } + }; + + /** + * Creates the sections for the children of this entry + * @param {import('../types.d.ts').Section} section + */ + const handleChildren = section => { + if (!entry.hierarchyChildren) { + return; + } + + entry.hierarchyChildren.forEach(child => handleEntry(child, section)); + }; + + /** + * @param {import('../types.d.ts').Section} section + * @param {import('../types.d.ts').Section} parentSection + */ + const makeChildrenTopLevelIfMisc = (section, parentSection) => { + if (parentSection.type !== 'misc') { + return; + } + + for (const key of Object.keys(section)) { + if (['textRaw', 'name', 'type', 'desc', 'miscs'].includes(key)) { + continue; + } + + if (parentSection[key]) { + if (Array.isArray(parentSection[key])) { + parentSection[key] = parentSection[key].concat(section[key]); + } + } else { + parentSection[key] = section[key]; + } + } + }; + + const section = setupSection(); + + parseStability(section); + + parseListIfThereIsOne(section); + + addDescription(section); + + handleChildren(section); + + makeChildrenTopLevelIfMisc(section, parentSection); + + if (section.type === 'property') { + if (section.propertySigType) { + section.type = section.propertySigType; + section.propertySigType = undefined; + } else { + // Delete the type here because ??? + section.type = undefined; + } + } +} + +/** + * @param {ApiDocMetadataEntry} head + * @param {Array} entries + * @returns {import('../types.d.ts').ModuleSection} + */ +export default (head, entries) => { + /** + * @type {import('../types.d.ts').ModuleSection} + */ + 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 0000000..3508ede --- /dev/null +++ b/src/generators/legacy-json/utils/parseSignature.mjs @@ -0,0 +1,210 @@ +'use strict'; + +import { PARAM_EXPRESSION } from '../constants.mjs'; + +const OPTIONAL_LEVEL_CHANGES = { '[': 1, ']': -1, ' ': 0 }; + +/** + * @param {string} parameterName + * @param {number} optionalDepth + * @returns {[string, number, boolean]} + */ +function parseNameAndOptionalStatus(parameterName, optionalDepth) { + // Let's check if the parameter is optional & grab its name at the same time. + // We need to see if there's any leading brackets in front of the parameter + // name. While we're doing that, we can also get the index where the + // parameter's name actually starts at. + let startingIdx = 0; + for (; startingIdx < parameterName.length; startingIdx++) { + const levelChange = OPTIONAL_LEVEL_CHANGES[parameterName[startingIdx]]; + + if (!levelChange) { + break; + } + + optionalDepth += levelChange; + } + + const isParameterOptional = optionalDepth > 0; + + // Now let's check for any trailing brackets at the end of the parameter's + // name. This will tell us where the parameter's name ends. + let endingIdx = parameterName.length - 1; + for (; endingIdx >= 0; endingIdx--) { + const levelChange = OPTIONAL_LEVEL_CHANGES[parameterName[startingIdx]]; + + if (!levelChange) { + break; + } + + optionalDepth += levelChange; + } + console.log('', startingIdx, endingIdx) + return [ + parameterName.substring(startingIdx, endingIdx + 1), + optionalDepth, + isParameterOptional + ]; +} + +/** + * @param {string} parameterName + * @returns {[string, string | undefined]} + */ +function parseDefaultValue(parameterName) { + /** + * @type {string | undefined} + */ + let defaultValue + + const equalSignPos = parameterName.indexOf('='); + if (equalSignPos !== -1) { + // We do have a default value, let's extract it + defaultValue = parameterName.substring(equalSignPos).trim(); + + // Let's remove the default value from the parameter name + parameterName = parameterName.substring(0, equalSignPos); + } + + return [parameterName, defaultValue] +} + +/** + * @param {string} parameterName + * @param {number} index + * @param {Array} markdownParameters + * @returns {import('../types.d.ts').Parameter} + */ +function findParameter(parameterName, index, markdownParameters) { + let parameter = markdownParameters[index] + if (parameter && parameter.name === parameterName) { + return parameter + } + + // Method likely has multiple signatures, something like + // `new Console(stdout[, stderr][, ignoreErrors])` and `new Console(options)` + // Try to find the parameter that this is being shared with + for (const markdownProperty of markdownParameters) { + if (markdownProperty.name === parameterName) { + // Found it + return markdownParameters + } else if (markdownProperty.options) { + for (const option of markdownProperty.options) { + if (option.name === parameterName) { + // Found a matching one in the parameter's options + return Object.assign({}, option); + } + } + } + } + + // At this point, we couldn't find a shared signature + if (parameterName.startsWith('...')) { + return { name: parameterName }; + } else { + throw new Error( + `Invalid param "${parameterName}"` + ); + } +} + +/** + * @param {string[]} declaredParameters + * @param {Array} parameters + */ +function parseParameters(declaredParameters, markdownParameters) { + /** + * @type {Array} + */ + let parameters = []; + + let optionalDepth = 0; + + declaredParameters.forEach((parameterName, i) => { + /** + * @example 'length]]' + * @example 'arrayBuffer[' + * @example '[sources[' + * @example 'end' + */ + parameterName = parameterName.trim(); + + // We need to do three things here: + // 1. Determine the declared parameters' name + // 2. Determine if the parameter is optional + // 3. Determine if the parameter has a default value + + /** + * This will handle the first and second thing for us + * @type {boolean} + */ + console.log(parameterName) + let isParameterOptional; + [parameterName, optionalDepth, isParameterOptional] = + parseNameAndOptionalStatus(parameterName, optionalDepth); + + console.log('', parameterName) + /** + * Now let's work on the third thing + * @type {string | undefined} + */ + let defaultValue; + [parameterName, defaultValue] = parseDefaultValue(parameterName) + + const parameter = findParameter(parameterName, i, markdownParameters) + + if (isParameterOptional) { + parameter.optional = true + } + + if (defaultValue) { + parameter.default = defaultValue + } + + parameters.push(parameter) + }); + + return parameters; +} + +/** + * @param {string} textRaw Something like `new buffer.Blob([sources[, options]])` + * @param {Array { + /** + * @type {import('../types.d.ts').MethodSignature} + */ + const signature = {}; + + // Find the return value & filter it out + markdownParameters = markdownParameters.filter(value => { + if (value.name === 'return') { + signature.return = value; + return false; + } + + return true; + }); + + /** + * Extract the parameters from the method's declaration + * @example `[sources[, options]]` + */ + let [, declaredParameters] = `\`${textRaw}\``.match(PARAM_EXPRESSION) || []; + + if (!declaredParameters) { + return undefined; + } + + /** + * @type {string[]} + * @example ['sources[,', 'options]]'] + */ + declaredParameters = declaredParameters.split(','); + + signature.params = parseParameters(declaredParameters, markdownParameters); + + return signature; +} diff --git a/src/metadata.mjs b/src/metadata.mjs index 3535869..389a3e5 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, @@ -153,6 +157,13 @@ const createMetadata = slugger => { content: section, tags, }; + + if (introduced_in) { + value.introduced_in = introduced_in; + } + + // Returns the Metadata entry for the API doc + return value; }, }; }; diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs index 3612f12..9c9e25d 100644 --- a/src/test/metadata.test.mjs +++ b/src/test/metadata.test.mjs @@ -66,6 +66,7 @@ describe('createMetadata', () => { const expected = { added_in: undefined, api: 'test', + api_doc_source: 'doc/api/test.md', changes: [], content: section, deprecated_in: undefined, diff --git a/src/types.d.ts b/src/types.d.ts index 1391750..f40eb7f 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)