From 842c8f6a7b29644715c284ee14a13a76df61edab Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 22 Dec 2025 13:09:12 -0500 Subject: [PATCH 1/2] fix(legacy-json): more identical output --- src/generators/jsx-ast/utils/buildContent.mjs | 2 +- .../jsx-ast/utils/buildPropertyTable.mjs | 4 +- .../jsx-ast/utils/buildSignature.mjs | 2 +- src/generators/legacy-json/constants.mjs | 2 +- .../utils/__tests__/parseList.test.mjs | 29 +++++---- .../legacy-json/utils/buildSection.mjs | 23 ++++--- .../legacy-json/utils/parseList.mjs | 23 ++++--- src/utils/parser/constants.mjs | 9 ++- src/utils/queries/__tests__/index.test.mjs | 14 +++-- src/utils/queries/constants.mjs | 2 +- src/utils/queries/index.mjs | 62 +++++++------------ src/utils/queries/utils.mjs | 48 ++++++++++++++ 12 files changed, 138 insertions(+), 82 deletions(-) create mode 100644 src/utils/queries/utils.mjs diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index a8096498..1e852dc3 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -229,7 +229,7 @@ export const processEntry = (entry, remark) => { // Transform typed lists into property tables visit( content, - createQueries.UNIST.isTypedList, + createQueries.UNIST.isStronglyTypedList, (node, idx, parent) => (parent.children[idx] = createPropertyTable(node)) ); diff --git a/src/generators/jsx-ast/utils/buildPropertyTable.mjs b/src/generators/jsx-ast/utils/buildPropertyTable.mjs index e5d41c9d..b132d38b 100644 --- a/src/generators/jsx-ast/utils/buildPropertyTable.mjs +++ b/src/generators/jsx-ast/utils/buildPropertyTable.mjs @@ -124,7 +124,7 @@ export const parseListIntoProperties = node => { // Clean up leading whitespace in remaining description if (children[0]?.type === 'text') { - children[0].value = children[0].value.trimStart(); + children[0].value = children[0].value.replace(/^[\s:]+/, ''); } properties.push({ @@ -133,7 +133,7 @@ export const parseListIntoProperties = node => { // The remaining children are the description desc: children, // Is there a list within this list? - sublist: sublists.find(createQueries.UNIST.isTypedList), + sublist: sublists.find(createQueries.UNIST.isLooselyTypedList), }); } diff --git a/src/generators/jsx-ast/utils/buildSignature.mjs b/src/generators/jsx-ast/utils/buildSignature.mjs index b6ba238a..781f6a87 100644 --- a/src/generators/jsx-ast/utils/buildSignature.mjs +++ b/src/generators/jsx-ast/utils/buildSignature.mjs @@ -89,7 +89,7 @@ export const getFullName = ({ name, text }, fallback = name) => { */ export default ({ children }, { data }, idx) => { // Try to locate the parameter list immediately following the heading - const listIdx = children.findIndex(createQueries.UNIST.isTypedList); + const listIdx = children.findIndex(createQueries.UNIST.isStronglyTypedList); // Parse parameters from the list, if found const params = diff --git a/src/generators/legacy-json/constants.mjs b/src/generators/legacy-json/constants.mjs index 3fc7a524..3b017d4d 100644 --- a/src/generators/legacy-json/constants.mjs +++ b/src/generators/legacy-json/constants.mjs @@ -12,7 +12,7 @@ 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 = /\((.+)\);?$/; +export const PARAM_EXPRESSION = /\(([^)]+)\);?$/; // The plurals associated with each section type. export const SECTION_TYPE_PLURALS = { diff --git a/src/generators/legacy-json/utils/__tests__/parseList.test.mjs b/src/generators/legacy-json/utils/__tests__/parseList.test.mjs index 11b2fee3..c88324fd 100644 --- a/src/generators/legacy-json/utils/__tests__/parseList.test.mjs +++ b/src/generators/legacy-json/utils/__tests__/parseList.test.mjs @@ -8,6 +8,16 @@ import { parseList, } from '../parseList.mjs'; +const validTypedList = [ + { type: 'inlineCode', value: 'option' }, // inline code + { type: 'text', value: ' ' }, // space + { + type: 'link', + children: [{ type: 'text', value: '' }], // link with < value + }, + { type: 'text', value: ' option description' }, +]; + describe('transformTypeReferences', () => { it('replaces template syntax with curly braces', () => { const result = transformTypeReferences('``'); @@ -90,7 +100,7 @@ describe('parseList', () => { children: [ { type: 'paragraph', - children: [{ type: 'text', value: '{string} description' }], + children: validTypedList, }, ], }, @@ -134,9 +144,7 @@ describe('parseList', () => { children: [ { type: 'paragraph', - children: [ - { type: 'text', value: 'param1 {string} first parameter' }, - ], + children: validTypedList, }, // This is a nested typed list { @@ -146,15 +154,7 @@ describe('parseList', () => { children: [ { type: 'paragraph', - children: [ - { type: 'inlineCode', value: 'option' }, // inline code - { type: 'text', value: ' ' }, // space - { - type: 'link', - children: [{ type: 'text', value: '' }], // link with < value - }, - { type: 'text', value: ' option description' }, - ], + children: validTypedList, }, ], }, @@ -167,6 +167,9 @@ describe('parseList', () => { ]; parseList(section, nodes); + + console.log(section); + assert.equal(section.params[0].options.length, 1); }); }); diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 7f842f7d..963ccba9 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -53,7 +53,7 @@ export const createSectionBuilder = () => { meta.changes = changes; - if (n_api_version?.length) { + if (typeof n_api_version === 'number' || n_api_version?.length) { meta.napiVersion = enforceArray(n_api_version); } @@ -102,13 +102,17 @@ export const createSectionBuilder = () => { * @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.children.map(node => node.data)?.[0]; + const parseStability = (section, nodes, { stability, content }) => { + const stabilityNode = stability.children[0]; - if (stabilityInfo) { - section.stability = Number(stabilityInfo.index); - section.stabilityText = stabilityInfo.description; - nodes.shift(); // Remove stability node from processing + if (stabilityNode) { + section.stability = Number(stabilityNode.data.index); + section.stabilityText = stabilityNode.data.description; + + const nodeToRemove = content.children.findIndex( + ({ data }) => data === stabilityNode.data + ); + nodes.splice(nodeToRemove - 1, 1); } }; @@ -153,7 +157,10 @@ export const createSectionBuilder = () => { * @param {import('../types.d.ts').Section} parent - The parent section. */ const addToParent = (section, parent) => { - const key = SECTION_TYPE_PLURALS[section.type] || 'miscs'; + const key = + SECTION_TYPE_PLURALS[section.__promote ?? section.type] || 'miscs'; + + delete section.__promote; parent[key] ??= []; parent[key].push(section); diff --git a/src/generators/legacy-json/utils/parseList.mjs b/src/generators/legacy-json/utils/parseList.mjs index 6f62bf66..6126db08 100644 --- a/src/generators/legacy-json/utils/parseList.mjs +++ b/src/generators/legacy-json/utils/parseList.mjs @@ -47,7 +47,7 @@ export const extractPattern = (text, pattern, key, current) => { export function parseListItem(child) { const current = {}; - const subList = child.children.find(createQueries.UNIST.isTypedList); + const subList = child.children.find(createQueries.UNIST.isLooselyTypedList); // Extract and clean raw text from the node, excluding nested lists current.textRaw = transformTypeReferences( @@ -89,10 +89,13 @@ export function parseListItem(child) { * @param {import('@types/mdast').RootContent[]} nodes */ export function parseList(section, nodes) { - const list = nodes[0]?.type === 'list' ? nodes.shift() : null; + const listIdx = nodes.findIndex(createQueries.UNIST.isStronglyTypedList); + const list = nodes[listIdx]; const values = list ? list.children.map(parseListItem) : []; + let removeList = true; + // Update the section based on its type and parsed values switch (section.type) { case 'ctor': @@ -109,7 +112,12 @@ export function parseList(section, nodes) { case 'property': // For properties, update type and other details if values exist if (values.length) { - leftHandAssign(section, values[0]); + delete values[0].name; + + Object.assign(section, values[0]); + + // TODO(@avivkeller): There is probably a better way to do this. + section.__promote = 'property'; } break; @@ -119,9 +127,10 @@ export function parseList(section, nodes) { break; default: - // If no specific handling, re-add the list for further processing - if (list) { - nodes.unshift(list); - } + removeList = false; + } + + if (removeList && list) { + nodes.splice(listIdx, 1); } } diff --git a/src/utils/parser/constants.mjs b/src/utils/parser/constants.mjs index 80e0c946..9684785c 100644 --- a/src/utils/parser/constants.mjs +++ b/src/utils/parser/constants.mjs @@ -37,7 +37,7 @@ const FUNCTION_CALL = '\\([^)]*\\)'; // Matches "bar": // Group 1: foo[bar] // Group 2: foo.bar -const PROPERTY = `${CAMEL_CASE}(?:(\\[${CAMEL_CASE}\\])|\\.(\\w+))`; +const PROPERTY = `${CAMEL_CASE}(?:(\\[[^\\]]+\\])|\\.(\\w+))`; // An array of objects defining the different types of API doc headings we want to // capture and their respective regex to match against the heading text. @@ -45,9 +45,12 @@ const PROPERTY = `${CAMEL_CASE}(?:(\\[${CAMEL_CASE}\\])|\\.(\\w+))`; export const DOC_API_HEADING_TYPES = [ { type: 'method', - regex: new RegExp(`^\`${PROPERTY}${FUNCTION_CALL}\`$`, 'i'), + regex: new RegExp( + `^\`(?:${PROPERTY}|(${CAMEL_CASE}))${FUNCTION_CALL}\`$`, + 'i' + ), }, - { type: 'event', regex: /^Event: +`'?([^']+)'`$/i }, + { type: 'event', regex: /^Event: +`'?([^`]*?)'?`$/i }, { type: 'class', regex: new RegExp( diff --git a/src/utils/queries/__tests__/index.test.mjs b/src/utils/queries/__tests__/index.test.mjs index 60ab257a..07c2be1e 100644 --- a/src/utils/queries/__tests__/index.test.mjs +++ b/src/utils/queries/__tests__/index.test.mjs @@ -108,17 +108,23 @@ describe('createQueries', () => { }); describe('UNIST', () => { - describe('isTypedList', () => { + describe('isStronglyTypedList', () => { it('returns false for non-list nodes', () => { strictEqual( - createQueries.UNIST.isTypedList({ type: 'paragraph', children: [] }), + createQueries.UNIST.isStronglyTypedList({ + type: 'paragraph', + children: [], + }), false ); }); it('returns false for empty lists', () => { strictEqual( - createQueries.UNIST.isTypedList({ type: 'list', children: [] }), + createQueries.UNIST.isStronglyTypedList({ + type: 'list', + children: [], + }), false ); }); @@ -211,7 +217,7 @@ describe('createQueries', () => { cases.forEach(({ name, node, expected }) => { it(`returns ${expected} for ${name}`, () => { - strictEqual(createQueries.UNIST.isTypedList(node), expected); + strictEqual(createQueries.UNIST.isStronglyTypedList(node), expected); }); }); }); diff --git a/src/utils/queries/constants.mjs b/src/utils/queries/constants.mjs index 88537e2f..90965202 100644 --- a/src/utils/queries/constants.mjs +++ b/src/utils/queries/constants.mjs @@ -4,4 +4,4 @@ export const DOC_API_STABILITY_SECTION_REF_URL = 'documentation.html#stability-index'; -export const VALID_JAVASCRIPT_PROPERTY = /^[.a-z0-9$_]+$/i; +export const VALID_JAVASCRIPT_PROPERTY = /^[.a-z0-9$_'-]+$/i; diff --git a/src/utils/queries/index.mjs b/src/utils/queries/index.mjs index 7f21cb85..35ed6b3b 100644 --- a/src/utils/queries/index.mjs +++ b/src/utils/queries/index.mjs @@ -3,10 +3,7 @@ import { u as createTree } from 'unist-builder'; import { SKIP } from 'unist-util-visit'; -import { - DOC_API_STABILITY_SECTION_REF_URL, - VALID_JAVASCRIPT_PROPERTY, -} from './constants.mjs'; +import { DOC_API_STABILITY_SECTION_REF_URL } from './constants.mjs'; import { extractYamlContent, parseHeadingIntoMetadata, @@ -16,6 +13,7 @@ import { } from '../parser/index.mjs'; import { getRemark } from '../remark.mjs'; import { transformNodesToString } from '../unist.mjs'; +import { isTypedList } from './utils.mjs'; /** * Creates an instance of the Query Manager, which allows to do multiple sort @@ -272,46 +270,28 @@ createQueries.UNIST = { * @param {import('@types/mdast').List} list * @returns {boolean} */ - isTypedList: list => { - // Exit early if not a list node - if (list.type !== 'list') { - return false; - } - - // Get the content nodes of the first list item's paragraph - const [node, ...contentNodes] = - list?.children?.[0]?.children?.[0]?.children ?? []; - - // Exit if no node - if (!node) { - return false; - } - - const possibleProperty = node?.value?.trimStart(); - - // Check for other starters - if (possibleProperty?.match(createQueries.QUERIES.typedListStarters)) { - return true; - } + isLooselyTypedList: list => Boolean(isTypedList(list)), - // Check for direct type link pattern (starts with '<') - if (node.type === 'link' && node.children?.[0]?.value?.[0] === '<') { - return true; - } - - // Check for inline code + space + type link pattern - if ( - node.type === 'inlineCode' && - possibleProperty?.match(VALID_JAVASCRIPT_PROPERTY) && - contentNodes[0]?.value?.trim() === '' && - contentNodes[1]?.type === 'link' && - contentNodes[1]?.children?.[0]?.value?.[0] === '<' - ) { - return true; + /** + * @param {import('@types/mdast').List} list + * @returns {boolean} + */ + isStronglyTypedList: list => { + const confidence = isTypedList(list); + + if (confidence === 1) { + // This is a loosely typed list, but we can still check if it is strongly typed. + const [, secondNode, thirdNode] = + list.children?.[0]?.children?.[0]?.children ?? []; + + return ( + secondNode?.value?.trim() === '' && + thirdNode?.type === 'link' && + thirdNode?.children?.[0]?.value?.[0] === '<' + ); } - // Not a typed list - return false; + return Boolean(confidence); }, }; diff --git a/src/utils/queries/utils.mjs b/src/utils/queries/utils.mjs new file mode 100644 index 00000000..df85d797 --- /dev/null +++ b/src/utils/queries/utils.mjs @@ -0,0 +1,48 @@ +import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs'; +import createQueries from './index.mjs'; + +/** + * @param {import('@types/mdast').List} list + * @returns {0 | 1 | 2} confidence + * + * 0: This is not a typed list + * 1: This is a loosely typed list + * 2: This is a strongly typed list + */ +export const isTypedList = list => { + if (!list || list.type !== 'list') { + return 0; + } + + const firstNode = list.children?.[0]?.children?.[0]?.children[0]; + + if (!firstNode) { + return 0; + } + + const value = firstNode?.value?.trimStart(); + + // Typed list starters (strong signal) + if (value && createQueries.QUERIES.typedListStarters.test(value)) { + return 2; + } + + // Direct type link: + if ( + firstNode.type === 'link' && + firstNode.children?.[0]?.value?.startsWith('<') + ) { + return 2; + } + + // inlineCode + space (weaker signal) + if ( + firstNode.type === 'inlineCode' && + value && + VALID_JAVASCRIPT_PROPERTY.test(value) + ) { + return 1; + } + + return 0; +}; From 809c5ad439fa5f1b5db0fa0260a6386b6a71f309 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 23 Dec 2025 11:11:02 -0500 Subject: [PATCH 2/2] fixup! --- src/generators/legacy-json/utils/__tests__/parseList.test.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/generators/legacy-json/utils/__tests__/parseList.test.mjs b/src/generators/legacy-json/utils/__tests__/parseList.test.mjs index c88324fd..648b2723 100644 --- a/src/generators/legacy-json/utils/__tests__/parseList.test.mjs +++ b/src/generators/legacy-json/utils/__tests__/parseList.test.mjs @@ -167,9 +167,6 @@ describe('parseList', () => { ]; parseList(section, nodes); - - console.log(section); - assert.equal(section.params[0].options.length, 1); }); });