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