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
5 changes: 5 additions & 0 deletions .changeset/documentation-index-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@doc-kit/generator-react': patch
---

Render `<!-- DOCUMENTATION_INDEX -->` with a new `DocumentationIndex` UI component
59 changes: 59 additions & 0 deletions packages/core/src/utils/__tests__/generators.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/utils/generators.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/html/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<a className={styles.entry} href={`${api}.html`}>
<span className={styles.title}>
<span className={styles.name}>{name}</span>

<Badge
size="small"
kind={STABILITY_KINDS[level] ?? 'neutral'}
aria-label={`Stability: ${index}`}
>
{label}
</Badge>
</span>

{description && <span className={styles.summary}>{description}</span>}
</a>
);
};

/**
* @param {{ entries: Array<DocumentationIndexEntry> }} props
*/
export default ({ entries = [] }) => (
<nav className={styles.documentationIndex} aria-label="Documentation index">
{entries.map(entry => (
<IndexEntry key={entry.api} {...entry} />
))}
</nav>
);
Original file line number Diff line number Diff line change
@@ -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);
}
15 changes: 5 additions & 10 deletions packages/react/src/html/ui/components/MetaBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand All @@ -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 (
<>
Expand All @@ -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]}
</Badge>
</>
);
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/html/ui/components/constants.mjs
Original file line number Diff line number Diff line change
@@ -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',
];
46 changes: 0 additions & 46 deletions packages/react/src/jsx-ast/__tests__/generate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<!-- DOCUMENTATION_INDEX -->` 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');
});
});
3 changes: 3 additions & 0 deletions packages/react/src/jsx-ast/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ export const AST_NODE_TYPES = {
},
};

// `<!-- DOCUMENTATION_INDEX -->` 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
Expand Down
Loading
Loading