Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/fix-typed-list-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@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

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.

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.
63 changes: 63 additions & 0 deletions packages/core/src/utils/queries/__tests__/index.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
8 changes: 7 additions & 1 deletion packages/core/src/utils/queries/index.mjs
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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}
Expand Down
46 changes: 35 additions & 11 deletions packages/core/src/utils/queries/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs';
import { QUERIES } from './index.mjs';

/**
* @param {import('@types/mdast').List} list
* 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').PhrasingContent | undefined} firstNode
* @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
* 0: Not a typed parameter
* 1: Loosely typed (inlineCode + valid property name)
* 2: Strongly typed (typed list starter or direct type annotation)
*/
export const isTypedList = list => {
if (!list || list.type !== 'list') {
return 0;
}

const firstNode = list.children?.[0]?.children?.[0]?.children[0];

const getTypedConfidence = firstNode => {
if (!firstNode) {
return 0;
}
Expand Down Expand Up @@ -43,3 +40,30 @@ export const isTypedList = list => {

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
*
* 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;
}

return getTypedConfidence(list.children?.[0]?.children?.[0]?.children?.[0]);
};
32 changes: 27 additions & 5 deletions packages/react/src/jsx-ast/utils/buildContent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading