From a982a702f7416ce26c61eb59c832b5366155afb0 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 11 Aug 2026 12:01:26 +0800 Subject: [PATCH 1/3] fix: split non-parameter items from typed lists in signature tables When a loose markdown list in API docs starts with typed parameters (e.g. `actual`, `expected`, `Returns`) but also contains plain prose bullets (e.g. algorithm complexity notes in `util.diff`), the entire list was treated as a parameter signature table. The non-parameter items had no name or type, causing them to render as empty sections. Split the list at the first non-parameter item: typed items become the FunctionSignature table, and remaining items render as regular markdown. Affected pages: util, fs, quic (31 mixed lists across 4 files). --- .changeset/fix-typed-list-split.md | 9 +++ .../utils/queries/__tests__/index.test.mjs | 63 +++++++++++++++++++ packages/core/src/utils/queries/index.mjs | 8 ++- packages/core/src/utils/queries/utils.mjs | 38 +++++++++++ .../react/src/jsx-ast/utils/buildContent.mjs | 32 ++++++++-- 5 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-typed-list-split.md diff --git a/.changeset/fix-typed-list-split.md b/.changeset/fix-typed-list-split.md new file mode 100644 index 00000000..fbd97009 --- /dev/null +++ b/.changeset/fix-typed-list-split.md @@ -0,0 +1,9 @@ +--- +'doc-kit': patch +--- + +Fix empty paragraphs in API docs when a typed parameter list contains trailing non-parameter items + +When a loose markdown list in the API docs starts with typed parameters (e.g. `actual`, `expected`, `Returns`) but also contains plain prose bullets (e.g. algorithm complexity notes), the entire list was previously treated as a parameter signature table. The non-parameter items had no name or type, causing them to render as empty `
` blocks on the built site. + +The fix splits the list at the first non-parameter item: typed items become the `FunctionSignature` table, and the remaining items render as regular markdown content. diff --git a/packages/core/src/utils/queries/__tests__/index.test.mjs b/packages/core/src/utils/queries/__tests__/index.test.mjs index a57762a0..5bca0411 100644 --- a/packages/core/src/utils/queries/__tests__/index.test.mjs +++ b/packages/core/src/utils/queries/__tests__/index.test.mjs @@ -178,4 +178,67 @@ describe('UNIST', () => { }); }); }); + + describe('isTypedListItem', () => { + it('returns false for undefined/null items', () => { + strictEqual(UNIST.isTypedListItem(undefined), false); + strictEqual(UNIST.isTypedListItem(null), false); + }); + + it('returns false for items without children', () => { + strictEqual(UNIST.isTypedListItem({}), false); + }); + + const cases = [ + { + name: 'inlineCode with valid property name', + item: createTree('listItem', [ + createTree('paragraph', [ + createTree('inlineCode', 'actual'), + createTree('text', ' '), + createTree('typeAnnotation', 'Array|string'), + ]), + ]), + expected: true, + }, + { + name: 'Returns prefix', + item: createTree('listItem', [ + createTree('paragraph', [createTree('text', 'Returns: some value')]), + ]), + expected: true, + }, + { + name: 'direct type annotation', + item: createTree('listItem', [ + createTree('paragraph', [createTree('typeAnnotation', 'Type')]), + ]), + expected: true, + }, + { + name: 'plain prose text (no typed prefix)', + item: createTree('listItem', [ + createTree('paragraph', [ + createTree('text', 'Algorithm complexity: O(N*D), where:'), + ]), + ]), + expected: false, + }, + { + name: 'inlineCode with invalid property name', + item: createTree('listItem', [ + createTree('paragraph', [ + createTree('inlineCode', 'not a valid prop'), + ]), + ]), + expected: false, + }, + ]; + + cases.forEach(({ name, item, expected }) => { + it(`returns ${expected} for ${name}`, () => { + strictEqual(UNIST.isTypedListItem(item), expected); + }); + }); + }); }); diff --git a/packages/core/src/utils/queries/index.mjs b/packages/core/src/utils/queries/index.mjs index abe289fb..488ce6f5 100644 --- a/packages/core/src/utils/queries/index.mjs +++ b/packages/core/src/utils/queries/index.mjs @@ -1,7 +1,7 @@ 'use strict'; import { transformNodesToString } from '../unist.mjs'; -import { isTypedList } from './utils.mjs'; +import { isTypedListItem, isTypedList } from './utils.mjs'; // This defines the actual REGEX Queries export const QUERIES = { @@ -71,6 +71,12 @@ export const UNIST = { */ isLooselyTypedList: list => Boolean(isTypedList(list)), + /** + * @param {import('@types/mdast').ListItem} item + * @returns {boolean} + */ + isTypedListItem, + /** * @param {import('@types/mdast').List} list * @returns {boolean} diff --git a/packages/core/src/utils/queries/utils.mjs b/packages/core/src/utils/queries/utils.mjs index 698294f1..750e7630 100644 --- a/packages/core/src/utils/queries/utils.mjs +++ b/packages/core/src/utils/queries/utils.mjs @@ -1,6 +1,44 @@ import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs'; import { QUERIES } from './index.mjs'; +/** + * Checks whether a single list item looks like a typed parameter — i.e. it + * starts with a property name (`inlineCode`), a Returns/Extends/Type prefix, + * or a direct type annotation. + * + * @param {import('@types/mdast').ListItem} item + * @returns {boolean} + */ +export const isTypedListItem = item => { + const firstNode = item?.children?.[0]?.children?.[0]; + if (!firstNode) { + return false; + } + + const value = firstNode?.value?.trimStart(); + + // Typed list starters (strong signal) + if (value && QUERIES.typedListStarters.test(value)) { + return true; + } + + // Direct type annotation: {Type} + if (firstNode.type === 'typeAnnotation') { + return true; + } + + // inlineCode + space (weaker signal) + if ( + firstNode.type === 'inlineCode' && + value && + VALID_JAVASCRIPT_PROPERTY.test(value) + ) { + return true; + } + + return false; +}; + /** * @param {import('@types/mdast').List} list * @returns {0 | 1 | 2} confidence diff --git a/packages/react/src/jsx-ast/utils/buildContent.mjs b/packages/react/src/jsx-ast/utils/buildContent.mjs index 5d10ad9a..bc92b7be 100644 --- a/packages/react/src/jsx-ast/utils/buildContent.mjs +++ b/packages/react/src/jsx-ast/utils/buildContent.mjs @@ -264,11 +264,33 @@ export const processEntry = entry => { // Transform typed lists into property tables. Skipped for MDX pages, whose // lists are authored prose rather than API type signatures. if (!entry.mdx) { - visit( - entry.content, - UNIST.isStronglyTypedList, - (node, idx, parent) => (parent.children[idx] = createSignatureTable(node)) - ); + visit(entry.content, UNIST.isStronglyTypedList, (node, idx, parent) => { + // A typed list may contain trailing non-parameter items (e.g. prose + // bullets that happen to share the same loose list in the source + // markdown). Split those off so they render as regular content instead + // of being silently swallowed by the signature table. + const firstNonTyped = node.children.findIndex( + item => !UNIST.isTypedListItem(item) + ); + + if (firstNonTyped === -1) { + parent.children[idx] = createSignatureTable(node); + return; + } + + const typedItems = node.children.slice(0, firstNonTyped); + const restItems = node.children.slice(firstNonTyped); + + const replacements = []; + if (typedItems.length > 0) { + replacements.push( + createSignatureTable({ ...node, children: typedItems }) + ); + } + replacements.push({ ...node, children: restItems }); + + parent.children.splice(idx, 1, ...replacements); + }); } return entry.content; From e338385f0566dc47af25e35958d6e23808f75600 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 11 Aug 2026 21:01:05 +0800 Subject: [PATCH 2/3] fix: update --- packages/core/src/utils/queries/utils.mjs | 66 +++++++++-------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/packages/core/src/utils/queries/utils.mjs b/packages/core/src/utils/queries/utils.mjs index 750e7630..3dea1436 100644 --- a/packages/core/src/utils/queries/utils.mjs +++ b/packages/core/src/utils/queries/utils.mjs @@ -2,29 +2,31 @@ import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs'; import { QUERIES } from './index.mjs'; /** - * Checks whether a single list item looks like a typed parameter — i.e. it - * starts with a property name (`inlineCode`), a Returns/Extends/Type prefix, - * or a direct type annotation. + * Inspects the first phrasing node of a paragraph and returns how confidently + * it looks like the start of a typed parameter. * - * @param {import('@types/mdast').ListItem} item - * @returns {boolean} + * @param {import('@types/mdast').PhrasingContent | undefined} firstNode + * @returns {0 | 1 | 2} confidence + * + * 0: Not a typed parameter + * 1: Loosely typed (inlineCode + valid property name) + * 2: Strongly typed (typed list starter or direct type annotation) */ -export const isTypedListItem = item => { - const firstNode = item?.children?.[0]?.children?.[0]; +const getTypedConfidence = firstNode => { if (!firstNode) { - return false; + return 0; } const value = firstNode?.value?.trimStart(); // Typed list starters (strong signal) if (value && QUERIES.typedListStarters.test(value)) { - return true; + return 2; } // Direct type annotation: {Type} if (firstNode.type === 'typeAnnotation') { - return true; + return 2; } // inlineCode + space (weaker signal) @@ -33,12 +35,23 @@ export const isTypedListItem = item => { value && VALID_JAVASCRIPT_PROPERTY.test(value) ) { - return true; + return 1; } - return false; + return 0; }; +/** + * Checks whether a single list item looks like a typed parameter — i.e. it + * starts with a property name (`inlineCode`), a Returns/Extends/Type prefix, + * or a direct type annotation. + * + * @param {import('@types/mdast').ListItem} item + * @returns {boolean} + */ +export const isTypedListItem = item => + Boolean(getTypedConfidence(item?.children?.[0]?.children?.[0])); + /** * @param {import('@types/mdast').List} list * @returns {0 | 1 | 2} confidence @@ -52,32 +65,5 @@ export const isTypedList = 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 && QUERIES.typedListStarters.test(value)) { - return 2; - } - - // Direct type annotation: {Type} - if (firstNode.type === 'typeAnnotation') { - return 2; - } - - // inlineCode + space (weaker signal) - if ( - firstNode.type === 'inlineCode' && - value && - VALID_JAVASCRIPT_PROPERTY.test(value) - ) { - return 1; - } - - return 0; + return getTypedConfidence(list.children?.[0]?.children?.[0]?.children?.[0]); }; From 23c7d8f64b6b08b743ef354813f659a4fd839f63 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Wed, 12 Aug 2026 06:20:52 +0800 Subject: [PATCH 3/3] Update .changeset/fix-typed-list-split.md Co-authored-by: Aviv Keller --- .changeset/fix-typed-list-split.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/fix-typed-list-split.md b/.changeset/fix-typed-list-split.md index fbd97009..ced5172d 100644 --- a/.changeset/fix-typed-list-split.md +++ b/.changeset/fix-typed-list-split.md @@ -1,5 +1,6 @@ --- -'doc-kit': patch +'@doc-kit/core': patch +'@doc-kit/generator-react': patch --- Fix empty paragraphs in API docs when a typed parameter list contains trailing non-parameter items