diff --git a/.changeset/documentation-index-component.md b/.changeset/documentation-index-component.md
new file mode 100644
index 000000000..ddfdf3ef7
--- /dev/null
+++ b/.changeset/documentation-index-component.md
@@ -0,0 +1,5 @@
+---
+'@doc-kit/generator-react': patch
+---
+
+Render `` with a new `DocumentationIndex` UI component
diff --git a/packages/core/src/utils/__tests__/generators.test.mjs b/packages/core/src/utils/__tests__/generators.test.mjs
index d6ef025be..6fe290dba 100644
--- a/packages/core/src/utils/__tests__/generators.test.mjs
+++ b/packages/core/src/utils/__tests__/generators.test.mjs
@@ -2,12 +2,71 @@ import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
+ getEntryDescription,
groupNodesByModule,
getVersionFromSemVer,
coerceSemVer,
getCompatibleVersions,
} from '../generators.mjs';
+describe('getEntryDescription', () => {
+ it('returns llm_description when available', () => {
+ const entry = {
+ llm_description: 'LLM generated description',
+ content: { children: [] },
+ };
+
+ const result = getEntryDescription(entry);
+ assert.equal(result, 'LLM generated description');
+ });
+
+ it('extracts first paragraph when no llm_description', () => {
+ const entry = {
+ content: {
+ children: [
+ {
+ type: 'paragraph',
+ children: [{ type: 'text', value: 'First paragraph' }],
+ },
+ ],
+ },
+ };
+
+ const result = getEntryDescription(entry);
+ assert.ok(result.length > 0);
+ });
+
+ it('returns empty string when no paragraph found', () => {
+ const entry = {
+ content: {
+ children: [
+ { type: 'heading', children: [{ type: 'text', value: 'Title' }] },
+ ],
+ },
+ };
+
+ const result = getEntryDescription(entry);
+ assert.equal(result, '');
+ });
+
+ it('removes newlines from description', () => {
+ const entry = {
+ content: {
+ children: [
+ {
+ type: 'paragraph',
+ children: [{ type: 'text', value: 'Line 1\nLine 2\r\nLine 3' }],
+ },
+ ],
+ },
+ };
+
+ const result = getEntryDescription(entry);
+ assert.equal(result.includes('\n'), false);
+ assert.equal(result.includes('\r'), false);
+ });
+});
+
describe('groupNodesByModule', () => {
it('groups nodes by api property', () => {
const nodes = [
diff --git a/packages/core/src/utils/generators.mjs b/packages/core/src/utils/generators.mjs
index ea2d97dfb..516113bca 100644
--- a/packages/core/src/utils/generators.mjs
+++ b/packages/core/src/utils/generators.mjs
@@ -2,6 +2,36 @@
import { coerce, major } from 'semver';
+import { transformNodeToString } from './unist.mjs';
+
+/**
+ * Retrieves the description of a given API doc entry. It first checks whether
+ * the entry has a llm_description property. If not, it extracts the first
+ * paragraph from the entry's content.
+ *
+ * @param {import('../generators/metadata/types').MetadataEntry} entry
+ * @returns {string}
+ */
+export const getEntryDescription = entry => {
+ if (entry.llm_description) {
+ return entry.llm_description.trim();
+ }
+
+ const descriptionNode = entry.content.children.find(
+ child => child.type === 'paragraph'
+ );
+
+ if (!descriptionNode) {
+ return '';
+ }
+
+ return (
+ transformNodeToString(descriptionNode)
+ // Remove newlines and extra spaces
+ .replace(/[\r\n]+/g, '')
+ );
+};
+
/**
* Groups all the API metadata nodes by module (`api` property) so that we can process each different file
* based on the module it belongs to.
diff --git a/packages/react/src/html/constants.mjs b/packages/react/src/html/constants.mjs
index 4ca3e783a..3850a2a38 100644
--- a/packages/react/src/html/constants.mjs
+++ b/packages/react/src/html/constants.mjs
@@ -26,6 +26,10 @@ export const JSX_IMPORTS = {
name: 'CodeTabs',
source: resolve(ROOT, './ui/components/CodeTabs'),
},
+ DocumentationIndex: {
+ name: 'DocumentationIndex',
+ source: resolve(ROOT, './ui/components/DocumentationIndex'),
+ },
MDXTooltip: {
name: 'MDXTooltip',
isDefaultExport: false,
diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx
new file mode 100644
index 000000000..70d3d0d42
--- /dev/null
+++ b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx
@@ -0,0 +1,49 @@
+import Badge from '@node-core/ui-components/Common/Badge';
+
+import styles from './index.module.css';
+import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs';
+
+/**
+ * @typedef {Object} DocumentationIndexEntry
+ * @property {string} api - Basename of the document, linked as `${api}.html`
+ * @property {string} name - Human-readable name from the document's heading
+ * @property {string} index - Stability index (e.g. `'2'` or `'1.1'`)
+ * @property {string} [description] - The document's `llm_description`, or its first paragraph
+ */
+
+/**
+ * @param {DocumentationIndexEntry} props
+ */
+const IndexEntry = ({ api, name, index, description }) => {
+ const level = parseInt(index, 10);
+ const label = STABILITY_LABELS[level] ?? index;
+
+ return (
+
+
+ {name}
+
+
+ {label}
+
+
+
+ {description && {description}}
+
+ );
+};
+
+/**
+ * @param {{ entries: Array }} props
+ */
+export default ({ entries = [] }) => (
+
+);
diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.module.css b/packages/react/src/html/ui/components/DocumentationIndex/index.module.css
new file mode 100644
index 000000000..a9a29c8e8
--- /dev/null
+++ b/packages/react/src/html/ui/components/DocumentationIndex/index.module.css
@@ -0,0 +1,61 @@
+.documentationIndex {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
+ gap: 1rem;
+ margin-block: 1.5rem;
+}
+
+.entry {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+ padding: 1rem;
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 0.75rem;
+ color: inherit;
+ text-decoration: none;
+ transition:
+ border-color 0.15s ease,
+ background-color 0.15s ease;
+}
+
+.entry:hover,
+.entry:focus-visible {
+ border-color: var(--color-neutral-400);
+ background-color: var(--color-neutral-100);
+}
+
+:where([data-theme='dark'], [data-theme='dark'] *) .entry {
+ border-color: var(--color-neutral-900);
+}
+
+:where([data-theme='dark'], [data-theme='dark'] *) .entry:hover,
+:where([data-theme='dark'], [data-theme='dark'] *) .entry:focus-visible {
+ border-color: var(--color-neutral-700);
+ background-color: var(--color-neutral-950);
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.name {
+ font-weight: 600;
+ color: var(--color-neutral-900);
+}
+
+:where([data-theme='dark'], [data-theme='dark'] *) .name {
+ color: var(--color-white);
+}
+
+.summary {
+ font-size: 0.875rem;
+ color: var(--color-neutral-800);
+}
+
+:where([data-theme='dark'], [data-theme='dark'] *) .summary {
+ color: var(--color-neutral-600);
+}
diff --git a/packages/react/src/html/ui/components/MetaBar/index.jsx b/packages/react/src/html/ui/components/MetaBar/index.jsx
index 5f705efd3..1a09aa8de 100644
--- a/packages/react/src/html/ui/components/MetaBar/index.jsx
+++ b/packages/react/src/html/ui/components/MetaBar/index.jsx
@@ -4,6 +4,7 @@ import MetaBar from '@node-core/ui-components/Containers/MetaBar';
import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub';
import styles from './index.module.css';
+import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs';
import { editURL } from '#theme/config';
@@ -12,10 +13,6 @@ const iconMap = {
MD: DocumentIcon,
};
-const STABILITY_KINDS = ['error', 'warning', null, 'info'];
-const STABILITY_LABELS = ['D', 'E', null, 'L'];
-const STABILITY_TOOLTIPS = ['Deprecated', 'Experimental', null, 'Legacy'];
-
/**
* Renders a heading value with an optional stability badge
* @param {{ value: string, stability: number }} props
@@ -25,9 +22,7 @@ const HeadingValue = ({ value, stability }) => {
return value;
}
- const ariaLabel = STABILITY_TOOLTIPS[stability]
- ? `Stability: ${STABILITY_TOOLTIPS[stability]}`
- : undefined;
+ const label = STABILITY_LABELS[stability];
return (
<>
@@ -37,11 +32,11 @@ const HeadingValue = ({ value, stability }) => {
size="small"
className={styles.badge}
kind={STABILITY_KINDS[stability]}
- data-tooltip={STABILITY_TOOLTIPS[stability]}
- aria-label={ariaLabel}
+ data-tooltip={label}
+ aria-label={label ? `Stability: ${label}` : undefined}
tabIndex={0}
>
- {STABILITY_LABELS[stability]}
+ {label?.[0]}
>
);
diff --git a/packages/react/src/html/ui/components/constants.mjs b/packages/react/src/html/ui/components/constants.mjs
new file mode 100644
index 000000000..e6fccd249
--- /dev/null
+++ b/packages/react/src/html/ui/components/constants.mjs
@@ -0,0 +1,12 @@
+/**
+ * UI badge kinds and labels for Node.js API stability levels
+ *
+ * @see https://nodejs.org/api/documentation.html#stability-index
+ */
+export const STABILITY_KINDS = ['error', 'warning', 'default', 'info'];
+export const STABILITY_LABELS = [
+ 'Deprecated',
+ 'Experimental',
+ 'Stable',
+ 'Legacy',
+];
diff --git a/packages/react/src/jsx-ast/__tests__/generate.test.mjs b/packages/react/src/jsx-ast/__tests__/generate.test.mjs
index a3050b09e..7d09db400 100644
--- a/packages/react/src/jsx-ast/__tests__/generate.test.mjs
+++ b/packages/react/src/jsx-ast/__tests__/generate.test.mjs
@@ -94,50 +94,4 @@ describe('jsx-ast generate', () => {
['index', 'fs']
);
});
-
- it('only generates an index page when an index document is an input', async () => {
- await setConfig({ target: ['jsx-ast'] });
-
- const jsxAstConfig = getConfig('jsx-ast');
- jsxAstConfig.generateAllPage = false;
- jsxAstConfig.generateNotFoundPage = false;
-
- const seenItems = [];
- await collect(
- generate([createEntry('fs', 'File system')], createWorker(seenItems))
- );
-
- assert.deepEqual(
- seenItems.map(({ head }) => head.api),
- ['fs']
- );
- });
-
- it('places the stability overview at the DOCUMENTATION_INDEX comment', async () => {
- await setConfig({ target: ['jsx-ast'] });
-
- const jsxAstConfig = getConfig('jsx-ast');
- jsxAstConfig.generateAllPage = false;
- jsxAstConfig.generateNotFoundPage = false;
-
- const index = createEntry('index', 'Index', { stabilityIndex: null });
- // The metadata parser turns a `` comment into
- // this tag on the entry of the section containing it.
- index.tags = ['DOCUMENTATION_INDEX'];
-
- const seenItems = [];
- await collect(
- generate(
- [index, createEntry('fs', 'File system')],
- createWorker(seenItems)
- )
- );
-
- const [{ entries }] = seenItems;
- const table = entries[0].content.children.at(-1);
-
- assert.equal(table.tagName, 'table');
- const [row] = table.children.at(-1).children;
- assert.equal(row.children[0].children[0].properties.href, 'fs.html');
- });
});
diff --git a/packages/react/src/jsx-ast/constants.mjs b/packages/react/src/jsx-ast/constants.mjs
index ffeec38c8..26b5afcc1 100644
--- a/packages/react/src/jsx-ast/constants.mjs
+++ b/packages/react/src/jsx-ast/constants.mjs
@@ -189,6 +189,9 @@ export const AST_NODE_TYPES = {
},
};
+// `` comment in a source document = a stability index
+export const DOCUMENTATION_INDEX_TAG = 'DOCUMENTATION_INDEX';
+
// These positions are explicity before anything else
export const OVERRIDDEN_POSITIONS = [
'index', // https://github.com/nodejs/node/blob/main/doc/api/index.md
diff --git a/packages/react/src/jsx-ast/generate.mjs b/packages/react/src/jsx-ast/generate.mjs
index 9f6969543..6a1a2f733 100644
--- a/packages/react/src/jsx-ast/generate.mjs
+++ b/packages/react/src/jsx-ast/generate.mjs
@@ -1,13 +1,36 @@
import getConfig from '@doc-kit/core/utils/configuration/index.mjs';
-import { groupNodesByModule } from '@doc-kit/core/utils/generators.mjs';
+import {
+ getEntryDescription,
+ groupNodesByModule,
+} from '@doc-kit/core/utils/generators.mjs';
import { jsx, toJs } from 'estree-util-to-js';
+import { DOCUMENTATION_INDEX_TAG } from './constants.mjs';
+import { createJSXElement } from './utils/ast.mjs';
import buildContent from './utils/buildContent.mjs';
-import { injectDocumentationIndex } from './utils/documentationIndex.mjs';
import { getSortedHeadNodes } from './utils/getSortedHeadNodes.mjs';
+import { JSX_IMPORTS } from '../html/constants.mjs';
import { buildNotFoundPage } from './utils/synthetic/404.mjs';
import { buildAllPage } from './utils/synthetic/all.mjs';
+/**
+ * Builds the `` element
+ *
+ * @param {Array} moduleEntries
+ */
+const buildDocumentationIndex = moduleEntries =>
+ createJSXElement(JSX_IMPORTS.DocumentationIndex.name, {
+ inline: false,
+ entries: getSortedHeadNodes(moduleEntries)
+ .filter(entry => entry.stability)
+ .map(entry => ({
+ api: entry.api,
+ name: entry.heading.data.name,
+ index: entry.stability.data.index,
+ description: getEntryDescription(entry),
+ })),
+ });
+
/**
* Builds the `{ head, entries }` page descriptors for all configured synthetic
* pages. The descriptors are cheap to build; the expensive `buildContent` step
@@ -60,13 +83,16 @@ export async function processChunk(slicedInput, itemIndices) {
*/
export async function* generate(input, worker) {
// The `index` page is only generated when an `index` document is part of
- // the input; the module list for the synthetic pages and the stability
- // overview excludes it.
+ // the input; the module list for the synthetic pages and the documentation
+ // index excludes it.
const moduleInput = input.filter(entry => entry.api !== 'index');
- // Sections tagged with a `` comment (e.g. in
- // the `index` document) receive the Stability Overview of all modules.
- injectDocumentationIndex(input, moduleInput);
+ // Sections tagged with a `` build an index
+ for (const entry of input) {
+ if (entry.tags?.includes(DOCUMENTATION_INDEX_TAG)) {
+ entry.content.children.push(buildDocumentationIndex(moduleInput));
+ }
+ }
// Create sliced input: each item contains head + its module's entries
// This avoids sending all 4700+ entries to every worker
diff --git a/packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs
deleted file mode 100644
index 6817f6349..000000000
--- a/packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs
+++ /dev/null
@@ -1,144 +0,0 @@
-import assert from 'node:assert/strict';
-import { describe, it } from 'node:test';
-
-import {
- buildStabilityOverview,
- injectDocumentationIndex,
-} from '../documentationIndex.mjs';
-
-const fakeHead = (api, name, stabilityIndex, depth = 1) => ({
- api,
- heading: { depth, data: { name, text: name, slug: api } },
- stability:
- stabilityIndex == null
- ? null
- : {
- data: {
- index: String(stabilityIndex),
- description: `${name} stable. Long-form description.`,
- },
- },
-});
-
-const findChild = (node, tagName) =>
- node.children.find(child => child.tagName === tagName);
-
-describe('injectDocumentationIndex', () => {
- const createEntry = tags => ({
- ...fakeHead('index', 'Index', null),
- tags,
- content: { type: 'root', children: [] },
- });
-
- it('appends the overview to entries tagged DOCUMENTATION_INDEX', () => {
- const tagged = createEntry(['DOCUMENTATION_INDEX']);
- const untagged = createEntry(undefined);
-
- injectDocumentationIndex(
- [tagged, untagged],
- [fakeHead('fs', 'fs', 2), fakeHead('assert', 'assert', 2)]
- );
-
- const table = findChild(tagged.content, 'table');
- assert.equal(findChild(table, 'tbody').children.length, 2);
- assert.equal(untagged.content.children.length, 0);
- });
-
- it('sorts the stability overview rows alphabetically by API name', () => {
- const entry = createEntry(['DOCUMENTATION_INDEX']);
-
- injectDocumentationIndex(
- [entry],
- [
- fakeHead('fs', 'fs', 2),
- fakeHead('assert', 'assert', 2),
- fakeHead('crypto', 'crypto', 2),
- ]
- );
-
- const table = findChild(entry.content, 'table');
- const rows = findChild(table, 'tbody').children;
- const names = rows.map(
- row => row.children[0].children[0].children[0].value
- );
-
- assert.deepEqual(names, ['assert', 'crypto', 'fs']);
- });
-
- it('excludes module heads without a stability index', () => {
- const entry = createEntry(['DOCUMENTATION_INDEX']);
-
- injectDocumentationIndex(
- [entry],
- [fakeHead('fs', 'fs', 2), fakeHead('synopsis', 'Usage', null)]
- );
-
- const table = findChild(entry.content, 'table');
- assert.equal(findChild(table, 'tbody').children.length, 1);
- });
-});
-
-describe('buildStabilityOverview', () => {
- it('renders a header row and one body row per entry', () => {
- const table = buildStabilityOverview([
- fakeHead('fs', 'fs', 2),
- fakeHead('crypto', 'crypto', 1),
- ]);
-
- assert.equal(table.tagName, 'table');
- const headerRow = findChild(findChild(table, 'thead'), 'tr');
- assert.deepEqual(
- headerRow.children.map(c => c.children[0].value),
- ['API', 'Stability']
- );
-
- assert.equal(findChild(table, 'tbody').children.length, 2);
- });
-
- it('formats the stability cell with a colored badge and first sentence', () => {
- const table = buildStabilityOverview([fakeHead('fs', 'fs', 1)]);
-
- const row = findChild(table, 'tbody').children[0];
- const stabilityCell = row.children[1];
- const badge = stabilityCell.children[0];
-
- assert.equal(badge.name, 'Badge');
- assert.deepEqual(
- badge.attributes.map(({ name, value }) => [name, value]),
- [
- ['size', 'small'],
- ['kind', 'warning'],
- ['aria-label', 'Stability: 1'],
- ]
- );
- assert.equal(badge.children[0].value, '1');
- assert.equal(stabilityCell.children[1].value, ' fs stable');
- });
-
- it('uses a default badge for stable entries', () => {
- const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]);
-
- const row = findChild(table, 'tbody').children[0];
- const badge = row.children[1].children[0];
- const kind = badge.attributes.find(attr => attr.name === 'kind');
-
- assert.equal(kind.value, 'default');
- });
-
- it('builds a relative link to the module HTML page', () => {
- const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]);
-
- const row = findChild(table, 'tbody').children[0];
- const link = row.children[0].children[0];
-
- assert.equal(link.tagName, 'a');
- assert.equal(link.properties.href, 'fs.html');
- assert.equal(link.children[0].value, 'fs');
- });
-
- it('renders an empty body when no entries are passed', () => {
- const table = buildStabilityOverview([]);
-
- assert.equal(findChild(table, 'tbody').children.length, 0);
- });
-});
diff --git a/packages/react/src/jsx-ast/utils/documentationIndex.mjs b/packages/react/src/jsx-ast/utils/documentationIndex.mjs
deleted file mode 100644
index c12ce0bf7..000000000
--- a/packages/react/src/jsx-ast/utils/documentationIndex.mjs
+++ /dev/null
@@ -1,86 +0,0 @@
-'use strict';
-
-import { h as createElement } from 'hastscript';
-
-import { createJSXElement } from './ast.mjs';
-import { getSortedHeadNodes } from './getSortedHeadNodes.mjs';
-import { JSX_IMPORTS } from '../../html/constants.mjs';
-
-// The metadata parser turns bare HTML comments into entry tags, so a
-// `` comment in a source document surfaces as
-// this tag on the entry for the section containing it.
-export const DOCUMENTATION_INDEX_TAG = 'DOCUMENTATION_INDEX';
-
-const STABILITY_BADGE_KINDS = [
- 'error',
- 'warning',
- 'default',
- 'info',
- 'neutral',
- 'neutral',
-];
-
-/**
- * Maps a Node.js stability index to a UI badge kind.
- *
- * @param {string} index
- */
-const getStabilityBadgeKind = index =>
- STABILITY_BADGE_KINDS[parseInt(index, 10)] ?? 'neutral';
-
-/**
- * Builds the Stability Overview table from module heads that declare a
- * top-level stability index, mirroring the `legacy-html-all` overview.
- *
- * @param {Array} headEntries
- */
-export const buildStabilityOverview = headEntries =>
- createElement('table', [
- createElement('thead', [
- createElement('tr', [
- createElement('th', 'API'),
- createElement('th', 'Stability'),
- ]),
- ]),
- createElement(
- 'tbody',
- headEntries.map(({ heading, api, stability }) =>
- createElement('tr', [
- createElement(
- 'td',
- createElement('a', { href: `${api}.html` }, heading.data.name)
- ),
- createElement(
- 'td',
- createJSXElement(JSX_IMPORTS.Badge.name, {
- size: 'small',
- kind: getStabilityBadgeKind(stability.data.index),
- 'aria-label': `Stability: ${stability.data.index}`,
- children: stability.data.index,
- }),
- ` ${stability.data.description.split('. ')[0]}`
- ),
- ])
- )
- ),
- ]);
-
-/**
- * Places the Stability Overview into every entry whose source section
- * contains a `` comment. The parser strips the
- * comment itself, so the table lands at the end of the tagged section.
- *
- * @param {Array} entries - Entries to scan for the tag
- * @param {Array} moduleEntries - Entries providing the module heads for the overview
- */
-export const injectDocumentationIndex = (entries, moduleEntries) => {
- const headEntries = getSortedHeadNodes(moduleEntries).filter(
- entry => entry.stability
- );
-
- for (const entry of entries) {
- if (entry.tags?.includes(DOCUMENTATION_INDEX_TAG)) {
- entry.content.children.push(buildStabilityOverview(headEntries));
- }
- }
-};
diff --git a/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs b/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs
index 6120f93e1..0e8e4f202 100644
--- a/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs
+++ b/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs
@@ -1,65 +1,7 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
-import { getEntryDescription, buildApiDocLink } from '../buildApiDocLink.mjs';
-
-describe('getEntryDescription', () => {
- it('returns llm_description when available', () => {
- const entry = {
- llm_description: 'LLM generated description',
- content: { children: [] },
- };
-
- const result = getEntryDescription(entry);
- assert.equal(result, 'LLM generated description');
- });
-
- it('extracts first paragraph when no llm_description', () => {
- const entry = {
- content: {
- children: [
- {
- type: 'paragraph',
- children: [{ type: 'text', value: 'First paragraph' }],
- },
- ],
- },
- };
-
- const result = getEntryDescription(entry);
- assert.ok(result.length > 0);
- });
-
- it('returns empty string when no paragraph found', () => {
- const entry = {
- content: {
- children: [
- { type: 'heading', children: [{ type: 'text', value: 'Title' }] },
- ],
- },
- };
-
- const result = getEntryDescription(entry);
- assert.equal(result, '');
- });
-
- it('removes newlines from description', () => {
- const entry = {
- content: {
- children: [
- {
- type: 'paragraph',
- children: [{ type: 'text', value: 'Line 1\nLine 2\r\nLine 3' }],
- },
- ],
- },
- };
-
- const result = getEntryDescription(entry);
- assert.equal(result.includes('\n'), false);
- assert.equal(result.includes('\r'), false);
- });
-});
+import { buildApiDocLink } from '../buildApiDocLink.mjs';
describe('buildApiDocLink', () => {
it('builds markdown link with description', () => {
diff --git a/packages/react/src/llms-txt/utils/buildApiDocLink.mjs b/packages/react/src/llms-txt/utils/buildApiDocLink.mjs
index 7d17d04f0..d81e84976 100644
--- a/packages/react/src/llms-txt/utils/buildApiDocLink.mjs
+++ b/packages/react/src/llms-txt/utils/buildApiDocLink.mjs
@@ -1,33 +1,5 @@
import { populate } from '@doc-kit/core/utils/configuration/templates.mjs';
-import { transformNodeToString } from '@doc-kit/core/utils/unist.mjs';
-
-/**
- * Retrieves the description of a given API doc entry. It first checks whether
- * the entry has a llm_description property. If not, it extracts the first
- * paragraph from the entry's content.
- *
- * @param {import('@doc-kit/core/generators/metadata/types').MetadataEntry} entry
- * @returns {string}
- */
-export const getEntryDescription = entry => {
- if (entry.llm_description) {
- return entry.llm_description.trim();
- }
-
- const descriptionNode = entry.content.children.find(
- child => child.type === 'paragraph'
- );
-
- if (!descriptionNode) {
- return '';
- }
-
- return (
- transformNodeToString(descriptionNode)
- // Remove newlines and extra spaces
- .replace(/[\r\n]+/g, '')
- );
-};
+import { getEntryDescription } from '@doc-kit/core/utils/generators.mjs';
/**
* Builds a markdown link for an API doc entry