diff --git a/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs index 82484d18..0cc00a9d 100644 --- a/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs +++ b/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs @@ -65,11 +65,76 @@ describe('extractHeadings', () => { assert.equal(result.length, 2); assert.equal(result[0].slug, 'fs-readfile'); + assert.equal(result[0].value, 'fs.readFile()'); assert.equal(result[0].depth, 2); assert.equal(result[0].stability, 2); assert.equal(result[1].stability, 2); }); + it('keeps method table of contents labels compact', () => { + const entries = [ + { + heading: { + depth: 3, + data: { + text: '`crypto.createHash(algorithm[, options])`', + name: 'createHash', + slug: 'crypto-createhash', + type: 'method', + }, + }, + }, + ]; + + const [result] = extractHeadings(entries); + + assert.equal(result.value, 'crypto.createHash()'); + }); + + it('keeps full method labels when compact labels would collide', () => { + const entries = [ + { + heading: { + depth: 3, + data: { + text: '`url.format(urlObject)`', + name: 'format', + slug: 'url-format-urlobject', + type: 'method', + }, + }, + }, + { + heading: { + depth: 3, + data: { + text: '`url.format(urlString)`', + name: 'format', + slug: 'url-format-urlstring', + type: 'method', + }, + }, + }, + { + heading: { + depth: 3, + data: { + text: '`url.parse(urlString)`', + name: 'parse', + slug: 'url-parse-urlstring', + type: 'method', + }, + }, + }, + ]; + + const result = extractHeadings(entries); + + assert.equal(result[0].value, 'url.format(urlObject)'); + assert.equal(result[1].value, 'url.format(urlString)'); + assert.equal(result[2].value, 'url.parse()'); + }); + it('filters out entries with empty heading text', () => { const entries = [ { diff --git a/src/generators/jsx-ast/utils/buildBarProps.mjs b/src/generators/jsx-ast/utils/buildBarProps.mjs index 6f21b183..9d90a4a3 100644 --- a/src/generators/jsx-ast/utils/buildBarProps.mjs +++ b/src/generators/jsx-ast/utils/buildBarProps.mjs @@ -4,6 +4,36 @@ import { visit } from 'unist-util-visit'; import { TOC_MAX_HEADING_DEPTH } from '../constants.mjs'; +const SHORT_SIGNATURE_TYPES = new Set(['classMethod', 'ctor', 'method']); + +/** + * Formats a full heading label for the table of contents. + * @param {import('../../metadata/types').HeadingData} data + */ +const formatHeading = data => + data.text + // Remove any containing code blocks + .replace(/`/g, '') + // Remove any prefixes (i.e. 'Class:') + .replace(/^[^:]+:/, '') + // Trim the remaining whitespace + .trim(); + +/** + * Shortens method-like headings so the table of contents remains scannable. + * @param {import('../../metadata/types').HeadingData} data + */ +const formatCodeHeading = data => { + const code = data.text.match(/`([^`]+)`/)?.[1] ?? data.text; + const signatureStart = code.indexOf('('); + + if (signatureStart === -1) { + return code.replace(/`/g, '').trim(); + } + + return `${code.slice(0, signatureStart).replace(/^new\s+/, '')}()`; +}; + /** * Generate a combined plain text string from all MDAST entries for estimating reading time. * @@ -31,23 +61,11 @@ const shouldIncludeEntryInToC = ({ heading }) => /** * Extracts and formats heading information from an API documentation entry. * @param {import('../../metadata/types').MetadataEntry} entry + * @param {string} heading */ -const extractHeading = entry => { +const extractHeading = (entry, heading) => { const data = entry.heading.data; - const cliFlagOrEnv = [...data.text.matchAll(/`(-[\w-]+|[A-Z0-9_]+=)/g)]; - - const heading = - cliFlagOrEnv.length > 0 - ? cliFlagOrEnv.at(-1)[1] - : data.text - // Remove any containing code blocks - .replace(/`/g, '') - // Remove any prefixes (i.e. 'Class:') - .replace(/^[^:]+:/, '') - // Trim the remaining whitespace - .trim(); - return { depth: entry.heading.depth, value: heading, @@ -57,10 +75,45 @@ const extractHeading = entry => { }; }; +/** + * Extracts both the full heading and the optional compact heading candidate. + * @param {import('../../metadata/types').MetadataEntry} entry + */ +const prepareHeading = entry => { + const data = entry.heading.data; + const cliFlagOrEnv = [...data.text.matchAll(/`(-[\w-]+|[A-Z0-9_]+=)/g)]; + + if (cliFlagOrEnv.length > 0) { + return { entry, heading: cliFlagOrEnv.at(-1)[1], compactHeading: null }; + } + + return { + entry, + heading: formatHeading(data), + compactHeading: SHORT_SIGNATURE_TYPES.has(data.type) + ? formatCodeHeading(data) + : null, + }; +}; + /** * Build the list of heading metadata for sidebar navigation. * * @param {Array} entries - All API metadata entries */ -export const extractHeadings = entries => - entries.filter(shouldIncludeEntryInToC).map(extractHeading); +export const extractHeadings = entries => { + const headings = entries.filter(shouldIncludeEntryInToC).map(prepareHeading); + const compactHeadingCounts = Map.groupBy( + headings.filter(({ compactHeading }) => compactHeading), + ({ compactHeading }) => compactHeading + ); + + return headings.map(({ entry, heading, compactHeading }) => + extractHeading( + entry, + compactHeading && compactHeadingCounts.get(compactHeading).length === 1 + ? compactHeading + : heading + ) + ); +};