Skip to content
Draft
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
27 changes: 27 additions & 0 deletions packages/react/src/html/ui/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,33 @@ main {
}
}

.overload-panel {
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 4);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-200);
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
padding: calc(var(--spacing) * 4);
padding-bottom: calc(var(--spacing) * 2);
}

@media (min-width: 48rem) {
Comment thread
ovflowd marked this conversation as resolved.
.overload-panel {
gap: calc(var(--spacing) * 6);
padding: calc(var(--spacing) * 6);
padding-bottom: calc(var(--spacing) * 3);
}
}

:where([data-theme='dark'], [data-theme='dark'] *) .overload-panel {
background-color: var(--color-neutral-950);
border-color: var(--color-neutral-900);
}

table {
td {
word-break: break-all;
Expand Down
104 changes: 103 additions & 1 deletion packages/react/src/jsx-ast/utils/__tests__/buildContent.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { describe, it } from 'node:test';

import { setConfig } from '@doc-kit/core/utils/configuration/index.mjs';

import { transformHeadingNode, gatherChangeEntries } from '../buildContent.mjs';
import {
transformHeadingNode,
gatherChangeEntries,
groupOverloadsIntoTabs,
} from '../buildContent.mjs';

const heading = {
type: 'heading',
Expand Down Expand Up @@ -190,3 +194,101 @@ describe('gatherChangeEntries', () => {
assert.equal(result[1].label, 'Added new feature.');
});
});

describe('groupOverloadsIntoTabs', () => {
it('groups consecutive overloads into a single OverloadTabs component', () => {
const originalEntries = [
{ heading: { data: { name: 'funcA', isOverload: false } } },
{ heading: { depth: 3, data: { name: 'funcB', isOverload: false } } },
{ heading: { depth: 3, data: { name: 'funcB', isOverload: true } } },
{ heading: { depth: 3, data: { name: 'funcB', isOverload: true } } },
{ heading: { data: { name: 'funcC', isOverload: false } } },
];

const getText = node => {
if (node.type === 'text') {
return node.value;
}
return (node.children || []).map(getText).join('');
};

const makeNode = (className, bodyText, sigText = null) => {
const children = [
{ type: 'element', tagName: 'h3', depth: 3 }, // The heading to be stripped
{ type: 'text', value: bodyText },
];

if (sigText) {
children.push({
type: 'element',
tagName: 'div',
properties: { class: 'signature', dataSignatureRaw: sigText },
});
}

return {
type: 'element',
tagName: 'div',
properties: { className },
children,
};
};

const processedChildren = [
makeNode('entry-a', 'body a'),
makeNode('entry-b1', 'body b1', 'function funcB(arg1);'),
makeNode('entry-b2', 'body b2', 'function funcB(arg1, arg2);'),
makeNode('entry-b3', 'body b3', 'function funcB(arg1, arg2, arg3);'),
makeNode('entry-c', 'body c'),
];

const result = groupOverloadsIntoTabs(processedChildren, originalEntries);

// 0: funcA, 1: funcB-heading, 2: CombinedSignatures, 3: CodeTabs(funcB), 4: funcC
assert.equal(result.length, 5);

// First element is untouched
assert.equal(result[0].properties.className, 'entry-a');

// Second element is the extracted heading
assert.equal(result[1].tagName, 'h3');

// Third element is the combined signatures block
const combinedSigBlock = result[2];
assert.deepEqual(combinedSigBlock.properties.className, ['signature']);

// Assert that the combined signatures contain the raw signatures without comments
const combinedText = getText(combinedSigBlock);
assert.match(combinedText, /function funcB\(arg1\);/);
assert.match(combinedText, /function funcB\(arg1, arg2\);/);
assert.match(combinedText, /function funcB\(arg1, arg2, arg3\);/);

// Fourth element is the CodeTabs component
const tabsComponent = result[3];
assert.equal(tabsComponent.name, 'CodeTabs');
const languagesAttr = tabsComponent.attributes.find(
a => a.name === 'languages'
);
const displayNamesAttr = tabsComponent.attributes.find(
a => a.name === 'displayNames'
);
assert.equal(languagesAttr.value, 'overload|overload|overload');
assert.equal(displayNamesAttr.value, 'Overload #1|Overload #2|Overload #3');
assert.equal(tabsComponent.children.length, 3); // 3 tab panels

// Check that the h3 was removed from the overloads and they are wrapped in overload-panel
const panel1 = tabsComponent.children[0];
const classAttr1 = panel1.attributes.find(a => a.name === 'className');
assert.equal(classAttr1.value, 'overload-panel');

// Second panel child should be the text we inserted
assert.equal(panel1.children[0].value, 'body b1');
assert.equal(result[4].properties.className, 'entry-c');

const panel2 = tabsComponent.children[1];
const classAttr2 = panel2.attributes.find(a => a.name === 'className');
assert.equal(classAttr2.value, 'overload-panel');
assert.equal(panel2.children[0].type, 'text');
assert.equal(panel2.children[0].value, 'body b2');
});
});
156 changes: 146 additions & 10 deletions packages/react/src/jsx-ast/utils/buildContent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
GITHUB_BLOB_URL,
populate,
} from '@doc-kit/core/utils/configuration/templates.mjs';
import { highlighter } from '@doc-kit/core/utils/highlighter.mjs';
import { parseInline } from '@doc-kit/core/utils/inline.mjs';
import { annotateOverloads } from '@doc-kit/core/utils/overloads.mjs';
import { splitTypedItems, UNIST } from '@doc-kit/core/utils/queries/index.mjs';
Expand All @@ -26,7 +27,7 @@
TYPES_WITH_METHOD_SIGNATURES,
TYPE_PREFIX_LENGTH,
} from '../constants.mjs';
import { createJSXElement } from './ast.mjs';
import { createJSXElement, createAttributeNode } from './ast.mjs';
import { extractHeadings, extractTextContent } from './buildBarProps.mjs';
import { getRemarkRecma as remark } from './remark.mjs';
import { renderAsJSX } from './render.mjs';
Expand Down Expand Up @@ -312,14 +313,149 @@
};

/**
* Builds a page's content: every entry processed and wrapped in one JSX
* fragment, plus the table of contents and reading time the layout needs.
*
* The layout itself (`<Layout>`) is not part of the content. The `html`
* generator wraps each page in it, which lets a page be assembled from other
* pages' content — `all.html` is the module pages concatenated — without
* building those modules a second time.
*
* Groups consecutive overloaded function API entries into a single OverloadTabs component.
* @param {Array<import('estree').Node>} processedChildren - The processed JSX AST nodes for the API entries
* @param {Array<import('@doc-kit/core/generators/metadata/types').MetadataEntry>} originalEntries - The original API metadata entries containing the overload flags
* @returns {Array<import('estree').Node>} The final array of layout children with overloads grouped
*/
export const groupOverloadsIntoTabs = (processedChildren, originalEntries) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be extracted to a dedicated overloads file, like buildOverloads.mjs?

const finalChildren = [];
let activeOverloadGroup;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how this handles in asynchronous work? no issues here, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works on the static AST arrays. That's where it is called: diff


/**
* Wraps an AST node's children in a styled panel `div` for tab rendering.
* @param {import('estree').Node} rootNode - The root node whose children will be wrapped.
* @returns {import('estree').Node} The new `div` AST node containing the children.
*/
const wrapInDiv = rootNode => {
return createJSXElement('div', {
inline: false,
className: 'overload-panel',
children: rootNode.children || [],
});
};

/**
* Extracts the raw signature string from an API entry node and removes the signature node from its children.
* @param {import('estree').Node} node - The AST node representing the API entry.
* @returns {string|undefined} The raw TypeScript signature string, or undefined if not found.
*/
const extractSignature = ({ children = [] }) => {
const signatureIndex = children.findIndex(
c =>
c.properties?.className?.includes('signature') ||
c.properties?.class === 'signature'
);

if (signatureIndex !== -1) {
const [signatureNode] = children.splice(signatureIndex, 1) ?? [];

return signatureNode.properties?.dataSignatureRaw;
}
};

/**
* Finalizes the active overload group by generating a combined signatures block
*/
const pushOverloadGroup = () => {
if (!activeOverloadGroup) {
return;
}

// Deduplicate signatures and join with a single newline
const uniqueSignatures = [...new Set(activeOverloadGroup.signatures)];
const combinedSignatureRaw = uniqueSignatures.join('\n');

const highlighted = highlighter.highlightToHast(
combinedSignatureRaw,
'typescript'
);
const combinedSignatureNode = createElement('div', { class: 'signature' }, [
highlighted,
]);

// Push combined signatures
finalChildren.push(combinedSignatureNode);

// Inject properties needed by CodeTabs component
const count = activeOverloadGroup.signatures.length;

const languagesArr = [];
const displayNamesArr = [];

for (let i = 0; i < count; i++) {
languagesArr.push('overload');
displayNamesArr.push(`Overload #${i + 1}`);
}

activeOverloadGroup.tabsNode.attributes.push(
createAttributeNode('languages', languagesArr.join('|')),
createAttributeNode('displayNames', displayNamesArr.join('|'))
);

// Push the tabs
finalChildren.push(activeOverloadGroup.tabsNode);

activeOverloadGroup = undefined;
};

/**
* Processes a single API entry node belonging to an overload group.
* It extracts its signature and pushes its remaining content into a new tab panel.
* @param {import('estree').Node} node - The AST node to process and add to the active group.
*/
const processOverloadNode = node => {
Comment thread
moshams272 marked this conversation as resolved.
const signatureRaw = extractSignature(node);

signatureRaw && activeOverloadGroup.signatures.push(signatureRaw);
activeOverloadGroup.tabsNode.children.push(wrapInDiv(node));
};

for (const [i, current] of processedChildren.entries()) {
const isOverload = originalEntries[i].heading?.data?.isOverload;

if (!isOverload) {
pushOverloadGroup();
finalChildren.push(current);
continue;
}

// Remove the heading from subsequent overloads as they are grouped under the first heading
current.children.shift();

if (activeOverloadGroup) {
processOverloadNode(current);
continue;
}

// Pop the previous node as it is the first entry of this overload group
const last = finalChildren.pop();
Comment thread
moshams272 marked this conversation as resolved.
activeOverloadGroup = {
// Shift out the first node's heading to serve as the main heading for the entire group
firstHeading: last?.children?.shift?.(),
signatures: [],
tabsNode: createJSXElement(JSX_IMPORTS.CodeTabs.name, {
inline: false,
children: [],
}),
};

processOverloadNode(last);
processOverloadNode(current);

if (activeOverloadGroup.firstHeading) {
finalChildren.push(activeOverloadGroup.firstHeading);
}
}

pushOverloadGroup();

return finalChildren;
};

/**
* Builds the overall document layout tree

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moshams272 eslint/prettier issue here


Check failure on line 458 in packages/react/src/jsx-ast/utils/buildContent.mjs

View workflow job for this annotation

GitHub Actions / Lint & Format

jsdoc-js(check-alignment)

packages/react/src/jsx-ast/utils/buildContent.mjs:458:1: Expected JSDoc block to be aligned.
* @param {Array<import('@doc-kit/core/generators/metadata/types').MetadataEntry>} entries - API documentation metadata entries
*/
export const createDocumentContent = async entries => {
Expand All @@ -337,7 +473,7 @@
root: createTree('root', [
createJSXElement(null, {
inline: false,
children: entries.map(processEntry),
children: groupOverloadsIntoTabs(entries.map(processEntry), entries),
}),
]),
};
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/jsx-ast/utils/signature.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export const createSignatureCodeBlock = (functionName, signature, heading) => {
const sig = generateSignature(functionName, signature, heading);
const highlighted = highlighter.highlightToHast(sig, 'typescript');

return createElement('div', { class: 'signature' }, [highlighted]);
return createElement('div', { class: 'signature', dataSignatureRaw: sig }, [
highlighted,
]);
};

/**
Expand Down
Loading