Skip to content

Commit 27a412a

Browse files
bteaavivkeller
andauthored
fix: split non-parameter items from typed lists in signature tables (#1023)
* fix: split non-parameter items from typed lists in signature tables Co-authored-by: Aviv Keller <me@aviv.sh>
1 parent bf43830 commit 27a412a

5 files changed

Lines changed: 142 additions & 17 deletions

File tree

.changeset/fix-typed-list-split.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@doc-kit/core': patch
3+
'@doc-kit/generator-react': patch
4+
---
5+
6+
Fix empty paragraphs in API docs when a typed parameter list contains trailing non-parameter items
7+
8+
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 `<section>` blocks on the built site.
9+
10+
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.

packages/core/src/utils/queries/__tests__/index.test.mjs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,67 @@ describe('UNIST', () => {
178178
});
179179
});
180180
});
181+
182+
describe('isTypedListItem', () => {
183+
it('returns false for undefined/null items', () => {
184+
strictEqual(UNIST.isTypedListItem(undefined), false);
185+
strictEqual(UNIST.isTypedListItem(null), false);
186+
});
187+
188+
it('returns false for items without children', () => {
189+
strictEqual(UNIST.isTypedListItem({}), false);
190+
});
191+
192+
const cases = [
193+
{
194+
name: 'inlineCode with valid property name',
195+
item: createTree('listItem', [
196+
createTree('paragraph', [
197+
createTree('inlineCode', 'actual'),
198+
createTree('text', ' '),
199+
createTree('typeAnnotation', 'Array|string'),
200+
]),
201+
]),
202+
expected: true,
203+
},
204+
{
205+
name: 'Returns prefix',
206+
item: createTree('listItem', [
207+
createTree('paragraph', [createTree('text', 'Returns: some value')]),
208+
]),
209+
expected: true,
210+
},
211+
{
212+
name: 'direct type annotation',
213+
item: createTree('listItem', [
214+
createTree('paragraph', [createTree('typeAnnotation', 'Type')]),
215+
]),
216+
expected: true,
217+
},
218+
{
219+
name: 'plain prose text (no typed prefix)',
220+
item: createTree('listItem', [
221+
createTree('paragraph', [
222+
createTree('text', 'Algorithm complexity: O(N*D), where:'),
223+
]),
224+
]),
225+
expected: false,
226+
},
227+
{
228+
name: 'inlineCode with invalid property name',
229+
item: createTree('listItem', [
230+
createTree('paragraph', [
231+
createTree('inlineCode', 'not a valid prop'),
232+
]),
233+
]),
234+
expected: false,
235+
},
236+
];
237+
238+
cases.forEach(({ name, item, expected }) => {
239+
it(`returns ${expected} for ${name}`, () => {
240+
strictEqual(UNIST.isTypedListItem(item), expected);
241+
});
242+
});
243+
});
181244
});

packages/core/src/utils/queries/index.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
import { transformNodesToString } from '../unist.mjs';
4-
import { isTypedList } from './utils.mjs';
4+
import { isTypedListItem, isTypedList } from './utils.mjs';
55

66
// This defines the actual REGEX Queries
77
export const QUERIES = {
@@ -71,6 +71,12 @@ export const UNIST = {
7171
*/
7272
isLooselyTypedList: list => Boolean(isTypedList(list)),
7373

74+
/**
75+
* @param {import('@types/mdast').ListItem} item
76+
* @returns {boolean}
77+
*/
78+
isTypedListItem,
79+
7480
/**
7581
* @param {import('@types/mdast').List} list
7682
* @returns {boolean}

packages/core/src/utils/queries/utils.mjs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@ import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs';
22
import { QUERIES } from './index.mjs';
33

44
/**
5-
* @param {import('@types/mdast').List} list
5+
* Inspects the first phrasing node of a paragraph and returns how confidently
6+
* it looks like the start of a typed parameter.
7+
*
8+
* @param {import('@types/mdast').PhrasingContent | undefined} firstNode
69
* @returns {0 | 1 | 2} confidence
710
*
8-
* 0: This is not a typed list
9-
* 1: This is a loosely typed list
10-
* 2: This is a strongly typed list
11+
* 0: Not a typed parameter
12+
* 1: Loosely typed (inlineCode + valid property name)
13+
* 2: Strongly typed (typed list starter or direct type annotation)
1114
*/
12-
export const isTypedList = list => {
13-
if (!list || list.type !== 'list') {
14-
return 0;
15-
}
16-
17-
const firstNode = list.children?.[0]?.children?.[0]?.children[0];
18-
15+
const getTypedConfidence = firstNode => {
1916
if (!firstNode) {
2017
return 0;
2118
}
@@ -43,3 +40,30 @@ export const isTypedList = list => {
4340

4441
return 0;
4542
};
43+
44+
/**
45+
* Checks whether a single list item looks like a typed parameter — i.e. it
46+
* starts with a property name (`inlineCode`), a Returns/Extends/Type prefix,
47+
* or a direct type annotation.
48+
*
49+
* @param {import('@types/mdast').ListItem} item
50+
* @returns {boolean}
51+
*/
52+
export const isTypedListItem = item =>
53+
Boolean(getTypedConfidence(item?.children?.[0]?.children?.[0]));
54+
55+
/**
56+
* @param {import('@types/mdast').List} list
57+
* @returns {0 | 1 | 2} confidence
58+
*
59+
* 0: This is not a typed list
60+
* 1: This is a loosely typed list
61+
* 2: This is a strongly typed list
62+
*/
63+
export const isTypedList = list => {
64+
if (!list || list.type !== 'list') {
65+
return 0;
66+
}
67+
68+
return getTypedConfidence(list.children?.[0]?.children?.[0]?.children?.[0]);
69+
};

packages/react/src/jsx-ast/utils/buildContent.mjs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,33 @@ export const processEntry = entry => {
264264
// Transform typed lists into property tables. Skipped for MDX pages, whose
265265
// lists are authored prose rather than API type signatures.
266266
if (!entry.mdx) {
267-
visit(
268-
entry.content,
269-
UNIST.isStronglyTypedList,
270-
(node, idx, parent) => (parent.children[idx] = createSignatureTable(node))
271-
);
267+
visit(entry.content, UNIST.isStronglyTypedList, (node, idx, parent) => {
268+
// A typed list may contain trailing non-parameter items (e.g. prose
269+
// bullets that happen to share the same loose list in the source
270+
// markdown). Split those off so they render as regular content instead
271+
// of being silently swallowed by the signature table.
272+
const firstNonTyped = node.children.findIndex(
273+
item => !UNIST.isTypedListItem(item)
274+
);
275+
276+
if (firstNonTyped === -1) {
277+
parent.children[idx] = createSignatureTable(node);
278+
return;
279+
}
280+
281+
const typedItems = node.children.slice(0, firstNonTyped);
282+
const restItems = node.children.slice(firstNonTyped);
283+
284+
const replacements = [];
285+
if (typedItems.length > 0) {
286+
replacements.push(
287+
createSignatureTable({ ...node, children: typedItems })
288+
);
289+
}
290+
replacements.push({ ...node, children: restItems });
291+
292+
parent.children.splice(idx, 1, ...replacements);
293+
});
272294
}
273295

274296
return entry.content;

0 commit comments

Comments
 (0)