diff --git a/package-lock.json b/package-lock.json index eff2cd84..eb6ed2b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@node-core/doc-kit", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@node-core/doc-kit", - "version": "1.4.2", + "version": "1.4.3", "dependencies": { "@actions/core": "^3.0.0", "@heroicons/react": "^2.2.0", @@ -48,10 +48,8 @@ "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find-after": "^5.0.0", "unist-util-position": "^5.0.0", "unist-util-remove": "^4.0.0", - "unist-util-select": "^5.1.0", "unist-util-visit": "^5.1.0", "yaml": "^2.9.0" }, @@ -4126,12 +4124,6 @@ "node": ">=4" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" - }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -4555,22 +4547,6 @@ "node": ">= 8" } }, - "node_modules/css-selector-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", - "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -8530,18 +8506,6 @@ } } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-deep-merge": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", @@ -10624,20 +10588,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -10692,23 +10642,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz", - "integrity": "sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "css-selector-parser": "^3.0.0", - "devlop": "^1.1.0", - "nth-check": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", diff --git a/package.json b/package.json index 715add8a..f624f81e 100644 --- a/package.json +++ b/package.json @@ -101,10 +101,8 @@ "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find-after": "^5.0.0", "unist-util-position": "^5.0.0", "unist-util-remove": "^4.0.0", - "unist-util-select": "^5.1.0", "unist-util-visit": "^5.1.0", "yaml": "^2.9.0" } diff --git a/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs b/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs index 4b7df072..7854b1e5 100644 --- a/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs +++ b/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs @@ -39,4 +39,76 @@ describe('jsx-ast transformer', () => { assert.equal(tree.children.includes(footnotes), false); assert.equal(layout.children.at(-1), footnotes); }); + + it('wraps tables in an overflow container and adds responsive labels', () => { + const table = { + type: 'element', + tagName: 'table', + properties: {}, + children: [ + { + type: 'element', + tagName: 'thead', + properties: {}, + children: [ + { + type: 'element', + tagName: 'tr', + properties: {}, + children: [ + { + type: 'element', + tagName: 'th', + properties: {}, + children: [{ type: 'text', value: 'Name' }], + }, + ], + }, + ], + }, + { + type: 'element', + tagName: 'tbody', + properties: {}, + children: [ + { + type: 'element', + tagName: 'tr', + properties: {}, + children: [ + { + type: 'element', + tagName: 'td', + properties: {}, + children: [{ type: 'text', value: 'Alice' }], + }, + ], + }, + ], + }, + ], + }; + + const tree = { + type: 'root', + children: [table], + }; + + transformer()(tree); + + const wrapper = tree.children[0]; + + assert.equal(wrapper.tagName, 'div'); + assert.deepEqual(wrapper.properties.className, ['overflow-container']); + + const transformedTable = wrapper.children[0]; + + assert.equal(transformedTable.tagName, 'table'); + assert.equal( + transformedTable.children[1].children[0].children[0].properties[ + 'data-label' + ], + 'Name' + ); + }); }); diff --git a/src/generators/jsx-ast/utils/plugins/alerts.mjs b/src/generators/jsx-ast/utils/plugins/alerts.mjs index 1abe1940..7fd90b7e 100644 --- a/src/generators/jsx-ast/utils/plugins/alerts.mjs +++ b/src/generators/jsx-ast/utils/plugins/alerts.mjs @@ -7,8 +7,13 @@ import { ALERT_MARKER, GITHUB_ALERT_TYPES } from '../../constants.mjs'; import { createJSXElement } from '../ast.mjs'; /** - * Converts a marker keyword into a human-readable title (e.g. `NOTE` -> `Note`). - * @param {string} type - The uppercase alert keyword + * Converts a marker keyword into a human-readable title. + * + * Example: + * NOTE -> Note + * + * @param {string} type + * @returns {string} */ const toTitle = type => type[0] + type.slice(1).toLowerCase(); @@ -17,46 +22,80 @@ const toTitle = type => type[0] + type.slice(1).toLowerCase(); */ const transformer = tree => { visit(tree, 'blockquote', (node, index, parent) => { - // The marker must be the leading text of the blockquote's first paragraph - const paragraph = node.children[0]; + /** + * Only root-level replacements need a parent. + */ + if (!parent || index === undefined) { + return; + } + + const children = node.children; + const paragraph = children[0]; + /** + * GitHub alerts must start with a paragraph. + */ if (paragraph?.type !== 'paragraph') { return; } - const text = paragraph.children[0]; + const paragraphChildren = paragraph.children; + const firstChild = paragraphChildren[0]; - if (text?.type !== 'text') { + /** + * The marker must be plain text. + */ + if (firstChild?.type !== 'text') { return; } - const match = text.value.match(ALERT_MARKER); + const match = ALERT_MARKER.exec(firstChild.value); if (!match) { return; } - // Strip the marker (and its trailing line break) from the leading text, - // dropping the now-empty text node — and its paragraph — if nothing remains. - text.value = text.value.slice(match[0].length); + /** + * Remove the alert marker. + * + * Example: + * + * "[!NOTE]\nhello" + * + * becomes: + * + * "hello" + */ + firstChild.value = firstChild.value.slice(match[0].length); - if (text.value === '') { - paragraph.children.shift(); + /** + * Remove empty nodes created by stripping the marker. + */ + if (firstChild.value === '') { + paragraphChildren.shift(); } - if (paragraph.children.length === 0) { - node.children.shift(); + if (paragraphChildren.length === 0) { + children.shift(); } + /** + * Replace the blockquote with AlertBox JSX. + */ parent.children[index] = createJSXElement(JSX_IMPORTS.AlertBox.name, { inline: false, - children: node.children, + children, level: GITHUB_ALERT_TYPES[match[1]], title: toTitle(match[1]), }); - // Skip the (now detached) blockquote's children, but revisit this index so - // the new AlertBox is descended into, allowing nested alerts to transform. + /** + * Skip the detached blockquote. + * + * Returning index causes the new AlertBox at this + * position to be visited again, allowing nested + * alerts to transform. + */ return [SKIP, index]; }); }; @@ -69,6 +108,6 @@ const transformer = tree => { * > [!NOTE] * > Highlights information that users should take into account. * - * @see https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts + * @see https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-syntax#alerts */ export default () => transformer; diff --git a/src/generators/jsx-ast/utils/plugins/transformer.mjs b/src/generators/jsx-ast/utils/plugins/transformer.mjs index 7d46e3ab..0d1a89b2 100644 --- a/src/generators/jsx-ast/utils/plugins/transformer.mjs +++ b/src/generators/jsx-ast/utils/plugins/transformer.mjs @@ -4,8 +4,9 @@ import { visit } from 'unist-util-visit'; import { TAG_TRANSFORMS } from '../../constants.mjs'; /** - * Checks whether a HAST node is the generated GFM footnotes section. - * @param {import('hast').Element} node + * Checks whether a node is the generated GFM footnotes section. + * + * @param {import('hast').Node} node */ const isFootnotesSection = node => node?.type === 'element' && @@ -14,11 +15,53 @@ const isFootnotesSection = node => node.properties?.className?.includes('footnotes')); /** - * Finds the generated page Layout node. - * @param {import('hast').Root} tree + * Checks whether a node is the generated Layout component. + * + * @param {import('unist').Node} node + */ +const isLayout = node => + node?.name === 'Layout' && Array.isArray(node.children); + +/** + * Adds responsive labels and wraps tables. + * + * @param {import('hast').Element} table + * @param {import('unist').Parent | undefined} parent + * @param {number | undefined} index */ -const findLayout = tree => - tree.children.find(node => node.name === 'Layout' && node.children); +const transformTable = (table, parent, index) => { + const thead = table.children.find(node => node.tagName === 'thead'); + + if (thead?.children?.[0]?.children) { + const headers = thead.children[0].children.map(toString); + const tbody = table.children.find(node => node.tagName === 'tbody'); + + if (tbody?.children) { + for (const row of tbody.children) { + for (const [cellIndex, cell] of (row.children ?? []).entries()) { + if (cell.tagName === 'td') { + cell.properties ??= {}; + cell.properties['data-label'] = headers[cellIndex]; + } + } + } + } + } + + /** + * Wrap in a
. + * + * Site styles rely on this wrapper for horizontal scrolling. + */ + if (parent && index !== undefined) { + parent.children[index] = { + type: 'element', + tagName: 'div', + properties: { className: ['overflow-container'] }, + children: [table], + }; + } +}; /** * @template {import('unist').Node} T @@ -26,50 +69,62 @@ const findLayout = tree => * @returns {T} */ const transformer = tree => { - visit(tree, 'element', (node, index, parent) => { - node.tagName = TAG_TRANSFORMS[node.tagName] || node.tagName; + let layout = null; - // Wrap
in a
, and apply responsive - // data attributes - if (node.tagName === 'table') { - if (parent) { - parent.children[index] = { - type: 'element', - tagName: 'div', - properties: { className: ['overflow-container'] }, - children: [node], - }; - } + /** + * Transform element nodes and locate Layout. + * + * We intentionally visit every node because MDX JSX nodes + * are not HAST "element" nodes. + */ + visit(tree, (node, index, parent) => { + /** + * Find Layout regardless of node type. + */ + if (isLayout(node)) { + layout = node; + } - // Not every table will have a header, so only do this on tables - // with them. - const thead = node.children.find(el => el.tagName === 'thead'); - - if (thead) { - // TODO(@avivkeller): These are only strings afaict, so a `toString` dependency - // might not actually be needed. - const headers = thead.children[0].children.map(toString); - const tbody = node.children.find(el => el.tagName === 'tbody'); - - visit( - tbody, - node => node.tagName === 'td', - (node, index) => (node.properties['data-label'] = headers[index]) - ); - } + /** + * Only HAST elements have tagName. + */ + if (node.type !== 'element') { + return; + } + + /** + * Normalize HTML tags. + */ + const transformedTag = TAG_TRANSFORMS[node.tagName]; + + if (transformedTag) { + node.tagName = transformedTag; + } + + /** + * Tables need special handling. + */ + if (node.tagName === 'table') { + transformTable(node, parent, index); } }); - const index = tree.children.findLastIndex(isFootnotesSection); + /** + * Find generated footnotes directly among root children. + * + * This is faster and more reliable than looking during + * the recursive traversal because footnotes are always + * generated at the document root. + */ + const footnotesIndex = tree.children.findLastIndex(isFootnotesSection); - if (index !== -1) { - const [section] = tree.children.splice(index, 1); - const layout = findLayout(tree); + if (footnotesIndex !== -1) { + const [footnotes] = tree.children.splice(footnotesIndex, 1); if (layout) { - layout.children.push(section); + layout.children.push(footnotes); } else { - tree.children.push(section); + tree.children.push(footnotes); } } }; @@ -77,6 +132,6 @@ const transformer = tree => { /** * Transforms elements in a syntax tree by replacing tag names according to the mapping. * - * Also moves any generated root section into its proper location in the AST. + * Also moves generated footnotes sections into Layout. */ export default () => transformer; diff --git a/src/generators/metadata/utils/parse.mjs b/src/generators/metadata/utils/parse.mjs index 9f49e0cc..e1be8597 100644 --- a/src/generators/metadata/utils/parse.mjs +++ b/src/generators/metadata/utils/parse.mjs @@ -4,10 +4,8 @@ import { basename, sep } from 'node:path/posix'; import { slug } from 'github-slugger'; import { u as createTree } from 'unist-builder'; -import { findAfter } from 'unist-util-find-after'; import { remove } from 'unist-util-remove'; -import { selectAll } from 'unist-util-select'; -import { SKIP, visit } from 'unist-util-visit'; +import { visit } from 'unist-util-visit'; import createNodeSlugger from './slugger.mjs'; import { transformNodeToHeading } from './transformers.mjs'; @@ -25,7 +23,22 @@ import { IGNORE_STABILITY_STEMS } from '../constants.mjs'; import { resolveTypeAnnotations } from './resolveTypes.mjs'; /** - * This generator generates a flattened list of metadata entries from a API doc + * Creates relative links for all known type references. + * + * This is executed once per document instead of repeatedly + * while processing individual sections. + * + * @param {Record} typeMap + * @param {string} path + * @returns {Record} + */ +const createRelativeTypeMap = (typeMap, path) => + Object.fromEntries( + Object.entries(typeMap).map(([type, url]) => [type, relative(url, path)]) + ); + +/** + * This generator generates a flattened list of metadata entries from an API doc * * @param {{ tree: import('mdast.Root'), mdx?: boolean } & import('../types').MetadataEntry} input * @param {Record} typeMap @@ -33,114 +46,191 @@ import { resolveTypeAnnotations } from './resolveTypes.mjs'; */ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => { /** - * Collection of metadata entries for the file + * Collection of generated metadata entries. + * * @type {Array} */ const metadataCollection = []; - // Creates a new Slugger instance for the current API doc file + /** + * Slug generator for headings in this document. + */ const nodeSlugger = createNodeSlugger(); - // Slug the API (We use a non-class slugger, since we are fairly certain that `path` is unique) + /** + * Values reused throughout parsing. + */ const api = slug(path.slice(1).replace(sep, '-')); + const fileName = basename(path); - // Get all Markdown Footnote definitions from the tree - const markdownDefinitions = selectAll('definition', tree); + /** + * Collect definitions and headings in a single AST traversal. + * + * The heading index is stored because we need the original + * position inside tree.children for section slicing. + * + * This avoids: + * - selectAll() traversals + * - findAfter() searches + * - indexOf() lookups later + */ + const markdownDefinitions = []; + const headingNodes = []; + + visit(tree, (node, index, parent) => { + if (UNIST.isDefinition(node)) { + markdownDefinitions.push(node); + } - // Get all Markdown Heading entries from the tree - const headingNodes = selectAll('heading', tree); + /** + * Only headings directly under the root define sections. + * + * Nested headings should not split the document. + */ + if (UNIST.isHeading(node) && parent === tree) { + headingNodes.push({ + node, + index, + }); + } + }); - // Handles Markdown link references and updates them to be plain links + /** + * Resolve Markdown reference links. + */ visit(tree, UNIST.isLinkReference, node => visitLinkReference(node, markdownDefinitions) ); - // Removes all the original definitions from the tree as they are not needed anymore + /** + * Definitions are no longer needed after references + * have been converted. + */ remove(tree, markdownDefinitions); - // Make all the typeMap links relative to us - const relativeTypeMap = Object.fromEntries( - Object.entries(typeMap).map(([type, url]) => [type, relative(url, path)]) - ); - - // Resolve every type annotation in the file at once (one TypeScript parse - // per file). MDX trees never contain `typeAnnotation` nodes — there, - // `{...}` is a real expression — so this is skipped. + /** + * Resolve type annotation links once per document. + * + * MDX skips this because typeAnnotation nodes are not + * real syntax there. + */ if (!mdx) { - resolveTypeAnnotations(tree, relativeTypeMap, path); + resolveTypeAnnotations(tree, createRelativeTypeMap(typeMap, path), path); } - // Handles the normalisation URLs that reference to API doc files with .md extension + /** + * Normalize markdown API links. + */ visit(tree, UNIST.isMarkdownUrl, node => visitMarkdownLink(node)); - // If the document has no headings but it has content, we add a fake heading to the top + /** + * Documents without headings still need a section + * if they contain content. + */ if (headingNodes.length === 0 && tree.children.length > 0) { - tree.children.unshift(createTree('heading', { depth: 1 }, [])); + const fakeHeading = createTree('heading', { depth: 1 }, []); + + tree.children.unshift(fakeHeading); + + headingNodes.push({ + node: fakeHeading, + index: 0, + }); } - // On "About this Documentation", we define the stability indices, and thus - // we don't need to check it for stability references + /** + * Documentation files define their own stability index. + */ const ignoreStability = IGNORE_STABILITY_STEMS.includes(api); - // Process each heading and create metadata entries - visit(tree, UNIST.isHeading, (headingNode, index) => { - // Initialize heading + /** + * Process each section heading. + */ + for (let i = 0; i < headingNodes.length; i++) { + const { node: headingNode, index: startHeadingIndex } = headingNodes[i]; + + /** + * Initialize heading metadata. + */ headingNode.data = transformNodeToHeading(headingNode); - // Initialize the metadata - const metadata = /** @type {import('../types').MetadataEntry} */ ({ - api, - path, - mdx, - basename: basename(path), - heading: headingNode, - }); - // Generate slug and update heading data + const metadata = + /** @type {import('../types').MetadataEntry} */ + ({ + api, + path, + mdx, + basename: fileName, + heading: headingNode, + }); + + /** + * Generate a unique slug for this heading. + */ metadata.heading.data.slug = nodeSlugger.slug(metadata.heading.data.text); - // Find the next heading to determine section boundaries - const nextHeadingNode = - findAfter(tree, index, UNIST.isHeading) ?? headingNode; - - const stop = - headingNode === nextHeadingNode - ? tree.children.length - : tree.children.indexOf(nextHeadingNode); - - // Create subtree for this section. If it's the first entry, we start from the - // beginning of the tree to ensure top-level nodes (like YAML) are captured. - const startIndex = metadataCollection.length === 0 ? 0 : index; + /** + * Find the end of this section. + * + * Using the stored heading indexes avoids + * findAfter() and repeated searching. + */ + const stop = headingNodes[i + 1]?.index ?? tree.children.length; + + /** + * The first section includes document-level + * nodes such as YAML/frontmatter. + */ + const startIndex = metadataCollection.length === 0 ? 0 : startHeadingIndex; + + /** + * Create a temporary tree containing only + * this section's content. + */ const subTree = createTree('root', tree.children.slice(startIndex, stop)); + /** + * Extract stability metadata. + */ visit(subTree, UNIST.isStabilityNode, node => visitStability(node, ignoreStability ? undefined : metadata) ); - // Process YAML nodes directly - merge all YAML data + /** + * Extract YAML metadata. + */ visit(subTree, UNIST.isYamlNode, node => visitYAML(node, metadata)); - // If YAML data contains a 'type', use it to override heading type + /** + * YAML type overrides heading type. + */ if (metadata.type) { metadata.heading.data.type = metadata.type; } - // Process Unix manual references + /** + * Convert Unix manual references. + */ visit(subTree, UNIST.isTextWithUnixManual, (node, _, parent) => visitTextWithUnixManualNode(node, parent) ); - // Remove processed YAML nodes from the content + /** + * Remove YAML metadata nodes from the + * final rendered content. + */ remove(subTree, [UNIST.isYamlNode]); - // Apply AST transformations - const parsedSubTree = remark().runSync(subTree); - metadata.content = parsedSubTree; + /** + * Apply remark transformations. + */ + metadata.content = remark().runSync(subTree); - // Add to collection + /** + * Store final metadata entry. + */ metadataCollection.push(metadata); - - return SKIP; - }); + } return metadataCollection; }; diff --git a/src/utils/highlighter.mjs b/src/utils/highlighter.mjs index 9e8fb6e2..4de3b1be 100644 --- a/src/utils/highlighter.mjs +++ b/src/utils/highlighter.mjs @@ -4,19 +4,17 @@ import { endianness } from 'node:os'; import createHighlighter from '@node-core/rehype-shiki'; import { h as createElement } from 'hastscript'; -import { SKIP, visit } from 'unist-util-visit'; +import { visit } from 'unist-util-visit'; import shikiConfig from '../../shiki.config.mjs'; -// This is what Remark will use as prefix within a
 className
-// to attribute the current language of the 
 element
 const languagePrefix = 'language-';
 
 /**
  * Creates a toolbar element with a language label and copy button.
  *
- * @param {string} languageId - The language identifier for the code block.
- * @returns {import('hast').Element} The toolbar element.
+ * @param {string} languageId
+ * @returns {import('hast').Element}
  */
 function createToolbarElement(languageId) {
   return createElement('div', { class: 'code-toolbar' }, [
@@ -26,173 +24,173 @@ function createToolbarElement(languageId) {
 }
 
 /**
- * Checks if the given node is a valid code element.
+ * Checks if a node is a code block.
  *
- * @param {import('unist').Node} node - The node to be verified.
- *
- * @return {boolean} - True when it is a valid code element, false otherwise.
+ * @param {import('unist').Node} node
+ * @returns {boolean}
  */
 function isCodeBlock(node) {
   return Boolean(
-    node?.tagName === 'pre' && node?.children[0].tagName === 'code'
+    node?.type === 'element' &&
+    node.tagName === 'pre' &&
+    node.children?.[0]?.type === 'element' &&
+    node.children[0].tagName === 'code'
   );
 }
 
-export const highlighter = await createHighlighter({
-  // riscv64 with sv39 has limited virtual memory space, where creating
-  // too many (>20) wasm memory instances fails.
-  // https://github.com/nodejs/node/pull/60591
-  //
-  // The wasm highlighter is currently not compatible with big endian.
-  // https://github.com/nodejs/node/pull/62512#issuecomment-4243469950
-  wasm: process.arch !== 'riscv64' && endianness() === 'LE',
-});
-
 /**
- * Creates a HAST transformer for Shiki which is used for transforming our codeboxes
- *
- * @deprecated This is used only for the legacy-html generator, please use `@node-core/rehype-shiki` directly instead.
+ * Converts a markdown code block into a Shiki highlighted block.
  *
- * @type {import('unified').Plugin}
+ * @param {import('hast').Element} node
+ * @returns {import('hast').Element[] | null}
  */
-export default function rehypeShikiji() {
-  /**
-   * @param {import('hast').Root} tree - The HAST tree to be transformed.
-   */
-  return function (tree) {
-    visit(tree, 'element', (node, index, parent) => {
-      // We only want to process 
...
elements - if (!parent || index == null || node.tagName !== 'pre') { - return; - } +function highlightCodeBlock(node) { + const codeElement = node.children[0]; - // We want the contents of the
 element, hence we attempt to get the first child
-      const preElement = node.children[0];
+  const classNames = codeElement.properties?.className;
 
-      // If thereÄs nothing inside the 
 element... What are we doing here?
-      if (!preElement || !preElement.properties) {
-        return;
-      }
+  if (!Array.isArray(classNames) || classNames.length === 0) {
+    return null;
+  }
 
-      // Ensure that we're not visiting a  element but it's inner contents
-      // (keep iterating further down until we reach where we want)
-      if (preElement.type !== 'element' || preElement.tagName !== 'code') {
-        return;
-      }
+  const languageClass = classNames.find(
+    c => typeof c === 'string' && c.startsWith(languagePrefix)
+  );
 
-      // Get the 
 element class names
-      const preClassNames = preElement.properties.className;
+  if (!languageClass) {
+    return null;
+  }
+
+  const languageId = languageClass.slice(languagePrefix.length);
+
+  const { children } = highlighter.shiki.codeToHast(
+    codeElement.children[0]?.value ?? '',
+    {
+      lang: languageId,
+      themes: {
+        light: shikiConfig.themes[0],
+        dark: shikiConfig.themes[1],
+      },
+    }
+  );
 
-      // The current classnames should be an array and it should have a length
-      if (typeof preClassNames !== 'object' || preClassNames.length === 0) {
-        return;
-      }
+  children[0].properties.class = `${children[0].properties.class} ${languageClass}`;
 
-      // We want to retrieve the language class name from the class names
-      const codeLanguage = preClassNames.find(
-        c => typeof c === 'string' && c.startsWith(languagePrefix)
-      );
+  children[0].children.push(createToolbarElement(languageId));
 
-      // If we didn't find any `language-` classname then we shouldn't highlight
-      if (typeof codeLanguage !== 'string') {
-        return;
-      }
+  return children;
+}
+
+/**
+ * Converts adjacent CJS/MJS blocks into a switchable block.
+ *
+ * @param {import('hast').Element[]} children
+ */
+function createCodeTabs(children) {
+  for (let index = 0; index < children.length - 1; index++) {
+    const first = children[index];
+    const second = children[index + 1];
+
+    if (!isCodeBlock(first) || !isCodeBlock(second)) {
+      continue;
+    }
+
+    const firstLanguage = getLanguage(first);
+    const secondLanguage = getLanguage(second);
+
+    if (
+      !isSupportedFlavor(firstLanguage) ||
+      !isSupportedFlavor(secondLanguage)
+    ) {
+      continue;
+    }
+
+    if (firstLanguage === secondLanguage) {
+      continue;
+    }
+
+    const firstCode = first.children[0];
+    const secondCode = second.children[0];
+
+    firstCode.properties.class = `${first.properties.class} ${firstLanguage}`;
+
+    secondCode.properties.class = `${second.properties.class} ${secondLanguage}`;
+
+    firstCode.properties.language = firstLanguage;
+    secondCode.properties.language = secondLanguage;
+
+    const switchable = createElement(
+      'pre',
+      {
+        class: 'shiki',
+        style: first.properties.style,
+      },
+      [
+        createElement('input', {
+          class: 'js-flavor-toggle',
+          type: 'checkbox',
+          checked: firstLanguage === 'cjs',
+        }),
+        firstCode,
+        secondCode,
+        createToolbarElement('javascript'),
+      ]
+    );
+
+    children.splice(index, 2, switchable);
+  }
+}
 
-      // Grabs the relevant alias/name of the language
-      const languageId = codeLanguage.slice(languagePrefix.length);
+/**
+ * Gets the language from a highlighted pre element.
+ *
+ * @param {import('hast').Element} node
+ * @returns {string}
+ */
+function getLanguage(node) {
+  const className = node.properties?.class;
 
-      // Parses the 
 contents and returns a HAST tree with the highlighted code
-      const { children } = highlighter.shiki.codeToHast(
-        preElement.children[0].value,
-        {
-          lang: languageId,
-          // Allows support for dual themes (light, dark) for Shiki
-          themes: { light: shikiConfig.themes[0], dark: shikiConfig.themes[1] },
-        }
-      );
+  if (typeof className !== 'string') {
+    return 'text';
+  }
 
-      // Adds the original language back to the 
 element
-      children[0].properties.class = `${children[0].properties.class} ${codeLanguage}`;
+  return (
+    className.match(/language-(?.*)/)?.groups?.language ?? 'text'
+  );
+}
 
-      // Adds the toolbar (language label + copy button) to the 
 element
-      children[0].children.push(createToolbarElement(languageId));
+/**
+ * @param {string} language
+ */
+function isSupportedFlavor(language) {
+  return language === 'cjs' || language === 'mjs';
+}
 
-      // Replaces the 
 element with the updated one
-      parent.children.splice(index, 1, ...children);
-    });
+export const highlighter = await createHighlighter({
+  wasm: process.arch !== 'riscv64' && endianness() === 'LE',
+});
 
-    visit(tree, 'element', (_, index, parent) => {
-      const codeElements = [];
-
-      let currentIndex = index;
-
-      while (isCodeBlock(parent?.children[currentIndex])) {
-        const preElement = parent?.children[currentIndex];
-        const codeElement = preElement.children[0];
-
-        // We should get the language name from the class name
-        if (preElement.properties.class?.length) {
-          const className = preElement.properties.class;
-          const matches = className.match(/language-(?.*)/);
-          const language = matches?.groups.language ?? 'text';
-
-          // For this iteration of our code, we only support multi-code tab for
-          // JavaScript languages for Node.js (CJS / MJS)
-          if (language === 'cjs' || language === 'mjs') {
-            // We patch up Shiki's `pre` element properties into the `code` element
-            // since we want to keep the original properties
-            codeElement.properties.class = `${className} ${language}`;
-            codeElement.properties.style = preElement.properties.style;
-            codeElement.properties.language = language;
-
-            // Add the code element to the pre children
-            codeElements.push(codeElement);
-          }
-        }
+/**
+ * @deprecated Use @node-core/rehype-shiki directly instead.
+ *
+ * @type {import('unified').Plugin}
+ */
+export default function rehypeShikiji() {
+  return function (tree) {
+    visit(tree, 'element', node => {
+      if (!node.children?.length) {
+        return;
+      }
 
-        currentIndex += 1;
-
-        // Since we only support CJS/MJS switch, we should have exactly 2 elements
-        // with different languages in order to create a switchable code tab
-        if (
-          codeElements.length === 2 &&
-          codeElements[0].properties?.language !==
-            codeElements[1].properties?.language
-        ) {
-          const switchablePreElement = createElement(
-            'pre',
-            {
-              // We grab Shiki's styling from the code tag
-              // back to the pre element tag to ensure consistency
-              style: codeElements[0].properties.style,
-              class: 'shiki',
-            },
-            [
-              createElement('input', {
-                class: 'js-flavor-toggle',
-                type: 'checkbox',
-                // If the CJS code block is the first one, then we should keep
-                // the checkbox checked so that it highglit the CJS by default
-                checked: codeElements[0].properties.language === 'cjs',
-              }),
-              ...codeElements,
-              createToolbarElement('javascript'),
-            ]
-          );
-
-          // This removes all the original code Elements and adds a new CodeTab Element
-          // at the original start of the first Code Element
-          parent.children.splice(
-            index,
-            currentIndex - index,
-            switchablePreElement
-          );
-
-          // Prevent visiting the code block children and for the next N Elements
-          // since all of them belong to this CodeTabs Element
-          return [SKIP];
+      node.children = node.children.flatMap(child => {
+        if (child.type !== 'element' || child.tagName !== 'pre') {
+          return [child];
         }
-      }
+
+        return highlightCodeBlock(child) ?? [child];
+      });
+
+      createCodeTabs(node.children);
     });
   };
 }
diff --git a/src/utils/queries/index.mjs b/src/utils/queries/index.mjs
index abe289fb..c1ae5977 100644
--- a/src/utils/queries/index.mjs
+++ b/src/utils/queries/index.mjs
@@ -22,6 +22,12 @@ export const QUERIES = {
 };
 
 export const UNIST = {
+  /**
+   * @param {import('@types/mdast').Definition} definition
+   * @returns {boolean}
+   */
+  isDefinition: ({ type }) => type === 'definition',
+
   /**
    * @param {import('@types/mdast').Blockquote} blockquote
    * @returns {boolean}
diff --git a/src/utils/type-annotations/__tests__/remark.test.mjs b/src/utils/type-annotations/__tests__/remark.test.mjs
index 60070c84..bc606d15 100644
--- a/src/utils/type-annotations/__tests__/remark.test.mjs
+++ b/src/utils/type-annotations/__tests__/remark.test.mjs
@@ -5,7 +5,7 @@ import remarkGfm from 'remark-gfm';
 import remarkParse from 'remark-parse';
 import remarkStringify from 'remark-stringify';
 import { unified } from 'unified';
-import { selectAll } from 'unist-util-select';
+import { visit } from 'unist-util-visit';
 
 import remarkTypeAnnotations from '../remark.mjs';
 
@@ -15,8 +15,16 @@ const processor = unified()
   .use(remarkGfm)
   .use(remarkStringify);
 
-const annotationsIn = markdown =>
-  selectAll('typeAnnotation', processor.parse(markdown));
+const annotationsIn = markdown => {
+  const tree = processor.parse(markdown);
+  const annotations = [];
+
+  visit(tree, 'typeAnnotation', node => {
+    annotations.push(node);
+  });
+
+  return annotations;
+};
 
 const valuesIn = markdown => annotationsIn(markdown).map(node => node.value);
 
@@ -86,13 +94,17 @@ describe('remarkTypeAnnotations', () => {
   it('leaves an unbalanced brace as literal text', () => {
     const tree = processor.parse('An { unclosed brace.');
 
-    assert.deepEqual(selectAll('typeAnnotation', tree), []);
-    assert.match(
-      selectAll('text', tree)
-        .map(node => node.value)
-        .join(''),
-      /\{ unclosed brace\./
-    );
+    const annotations = [];
+    visit(tree, 'typeAnnotation', node => {
+      annotations.push(node);
+    });
+    const textNodes = [];
+    visit(tree, 'text', node => {
+      textNodes.push(node.value);
+    });
+
+    assert.deepEqual(annotations, []);
+    assert.match(textNodes.join(''), /\{ unclosed brace\./);
   });
 
   it('does not match inside code spans', () => {
diff --git a/src/utils/type-annotations/hast.mjs b/src/utils/type-annotations/hast.mjs
index 603201cc..b47b005a 100644
--- a/src/utils/type-annotations/hast.mjs
+++ b/src/utils/type-annotations/hast.mjs
@@ -2,13 +2,53 @@
 
 import { highlighter } from '../highlighter.mjs';
 
+const TYPE_THEME = highlighter.shiki.getLoadedThemes()[0];
+
+/**
+ * Creates a HAST text node.
+ *
+ * @param {string} value
+ * @returns {import('hast').Text}
+ */
+const text = value => ({
+  type: 'text',
+  value,
+});
+
+/**
+ * Creates a HAST element.
+ *
+ * @param {string} tagName
+ * @param {import('hast').Properties} properties
+ * @param {import('hast').ElementContent[]} children
+ * @returns {import('hast').Element}
+ */
+const element = (tagName, properties, children) => ({
+  type: 'element',
+  tagName,
+  properties,
+  children,
+});
+
+/**
+ * Applies mdast metadata to a generated HAST node.
+ *
+ * @param {import('mdast-util-to-hast').State} state
+ * @param {import('mdast').Node} node
+ * @param {import('hast').Element} result
+ * @returns {import('hast').Element}
+ */
+const finalize = (state, node, result) => {
+  state.patch(node, result);
+  return state.applyData(node, result);
+};
+
 /**
- * Slices a type's text by its resolved link ranges into hast children —
- * plain text segments interleaved with `` anchors.
+ * Slices a type's text by its resolved link ranges into HAST children.
  *
- * @param {string} value The type text
- * @param {Array<{ start: number, end: number, href: string }>} links Sorted, disjoint ranges
- * @returns {Array}
+ * @param {string} value
+ * @param {Array<{ start: number, end: number, href: string }>} links
+ * @returns {import('hast').ElementContent[]}
  */
 const buildLinkedChildren = (value, links) => {
   const children = [];
@@ -16,56 +56,54 @@ const buildLinkedChildren = (value, links) => {
 
   for (const { start, end, href } of links) {
     if (start > cursor) {
-      children.push({ type: 'text', value: value.slice(cursor, start) });
+      children.push(text(value.slice(cursor, start)));
     }
 
-    children.push({
-      type: 'element',
-      tagName: 'a',
-      properties: { href, className: ['type-link'] },
-      children: [{ type: 'text', value: value.slice(start, end) }],
-    });
+    children.push(
+      element(
+        'a',
+        {
+          href,
+          class: 'type-link',
+        },
+        [text(value.slice(start, end))]
+      )
+    );
 
     cursor = end;
   }
 
   if (cursor < value.length) {
-    children.push({ type: 'text', value: value.slice(cursor) });
+    children.push(text(value.slice(cursor)));
   }
 
   return children;
 };
 
 /**
- * Minimal mdast→hast handler for `typeAnnotation` nodes: one
- * `` whose resolved identifiers are plain `` links.
- * No syntax highlighting — used by the legacy generators.
+ * Minimal mdast→hast handler for `typeAnnotation` nodes.
  *
  * @param {import('mdast-util-to-hast').State} state
  * @param {import('mdast').Node} node
  * @returns {import('hast').Element}
  */
-export const typeAnnotationToHast = (state, node) => {
-  const result = {
-    type: 'element',
-    tagName: 'code',
-    properties: { className: ['type'] },
-    children: buildLinkedChildren(node.value, node.data?.links ?? []),
-  };
-
-  state.patch(node, result);
-
-  return state.applyData(node, result);
-};
+export const typeAnnotationToHast = (state, node) =>
+  finalize(
+    state,
+    node,
+    element(
+      'code',
+      {
+        class: 'type',
+      },
+      buildLinkedChildren(node.value, node.data?.links ?? [])
+    )
+  );
 
 /**
- * Syntax-highlighted mdast→hast handler for `typeAnnotation` nodes, used by
- * the web (JSX) pipeline. The whole type is highlighted as one inline
- * TypeScript fragment, and each resolved identifier's exact character range
- * is wrapped in an `` via Shiki decorations.
+ * Syntax-highlighted mdast→hast handler for `typeAnnotation` nodes.
  *
- * Falls back to the minimal handler when the type failed to parse or nothing
- * resolved (no point paying for highlighting then).
+ * Falls back to the minimal handler when parsing failed or nothing resolved.
  *
  * @param {import('mdast-util-to-hast').State} state
  * @param {import('mdast').Node} node
@@ -80,34 +118,32 @@ export const typeAnnotationToHighlightedHast = (state, node) => {
 
   const root = highlighter.shiki.codeToHast(node.value, {
     lang: 'typescript',
-    theme: highlighter.shiki.getLoadedThemes()[0],
+    theme: TYPE_THEME,
     decorations: links.map(({ start, end, href }) => ({
       start,
       end,
       tagName: 'a',
-      properties: { href, class: 'type-link' },
+      properties: {
+        href,
+        class: 'type-link',
+      },
       alwaysWrap: true,
     })),
   });
 
-  // codeToHast wraps the highlighted line in 
; re-shape that into
-  // a single inline  element ("only the outermost type opens/closes
-  // the code fragment") carrying Shiki's theme styling. The 
's tabindex
-  // is dropped — an inline fragment is no scroll container.
-  const [preElement] = root.children;
-  const [codeElement] = preElement.children;
-
-  const result = {
-    type: 'element',
-    tagName: 'code',
-    properties: {
-      class: `${preElement.properties.class} type`,
-      style: preElement.properties.style,
-    },
-    children: codeElement.children,
-  };
-
-  state.patch(node, result);
-
-  return state.applyData(node, result);
+  const preElement = root.children[0];
+  const codeElement = preElement.children[0];
+
+  return finalize(
+    state,
+    node,
+    element(
+      'code',
+      {
+        class: `${preElement.properties.class} type`,
+        style: preElement.properties.style,
+      },
+      codeElement.children
+    )
+  );
 };
diff --git a/src/utils/type-annotations/mdast.mjs b/src/utils/type-annotations/mdast.mjs
index 203a6b21..3cdcb4a2 100644
--- a/src/utils/type-annotations/mdast.mjs
+++ b/src/utils/type-annotations/mdast.mjs
@@ -5,6 +5,18 @@
 // and the mandatory `\|` inside GFM table cells must be decoded here.
 const CHARACTER_ESCAPE = /\\([!-/:-@[-`{-~])/g;
 
+// Interior line endings are normalized to a single space.
+const WHITESPACE = /\s+/g;
+
+/**
+ * Normalizes the parsed type text into its canonical form.
+ *
+ * @param {string} value
+ * @returns {string}
+ */
+const normalizeTypeAnnotation = value =>
+  value.replace(WHITESPACE, ' ').trim().replace(CHARACTER_ESCAPE, '$1');
+
 /**
  * Creates the mdast-util-from-markdown extension that compiles
  * `typeAnnotation` tokens into `{ type: 'typeAnnotation', value }` nodes.
@@ -21,9 +33,16 @@ export const typeAnnotationFromMarkdown = () => ({
      * @param {import('micromark-util-types').Token} token
      */
     typeAnnotation(token) {
-      this.enter({ type: 'typeAnnotation', value: '' }, token);
+      this.enter(
+        {
+          type: 'typeAnnotation',
+          value: '',
+        },
+        token
+      );
     },
   },
+
   exit: {
     /**
      * @this {import('mdast-util-from-markdown').CompileContext}
@@ -33,9 +52,9 @@ export const typeAnnotationFromMarkdown = () => ({
       const node = this.stack.at(-1);
       const chunk = this.sliceSerialize(token);
 
-      // Chunks are split by interior line endings; re-join with a space
-      node.value = node.value ? `${node.value} ${chunk}` : chunk;
+      node.value += node.value ? ` ${chunk}` : chunk;
     },
+
     /**
      * @this {import('mdast-util-from-markdown').CompileContext}
      * @param {import('micromark-util-types').Token} token
@@ -43,10 +62,7 @@ export const typeAnnotationFromMarkdown = () => ({
     typeAnnotation(token) {
       const node = this.stack.at(-1);
 
-      node.value = node.value
-        .replace(/\s+/g, ' ')
-        .trim()
-        .replace(CHARACTER_ESCAPE, '$1');
+      node.value = normalizeTypeAnnotation(node.value);
 
       this.exit(token);
     },
@@ -63,7 +79,8 @@ export const typeAnnotationToMarkdown = () => ({
   handlers: {
     /**
      * @param {{ value: string }} node
+     * @returns {string}
      */
-    typeAnnotation: node => `{${node.value}}`,
+    typeAnnotation: ({ value }) => `{${value}}`,
   },
 });
diff --git a/src/utils/type-annotations/syntax.mjs b/src/utils/type-annotations/syntax.mjs
index 86b8c1d3..2a30bc41 100644
--- a/src/utils/type-annotations/syntax.mjs
+++ b/src/utils/type-annotations/syntax.mjs
@@ -9,35 +9,34 @@ const DOLLAR_SIGN = '$'.charCodeAt(0);
  * (and represents EOF as `null`).
  *
  * @param {import('micromark-util-types').Code} code
+ * @returns {boolean}
  */
 const isLineEnding = code => code !== null && code < -2;
 
 /**
- * A `{` directly after `$` is prose about template literals, not a type
- * annotation.
+ * A `{` immediately following `$` belongs to a template literal (`${...}`),
+ * not a type annotation.
  *
  * @param {import('micromark-util-types').Code} code
+ * @returns {boolean}
  */
-const previous = code => code !== DOLLAR_SIGN;
+const previousNotDollar = code => code !== DOLLAR_SIGN;
 
 /**
- * Tokenizes a balanced `{...}` span as a single `typeAnnotation` token.
+ * Tokenizes a balanced `{...}` span as a `typeAnnotation`.
  *
  * The outer braces become `typeAnnotationMarker` tokens; everything between
- * them becomes `typeAnnotationValue` chunks (one per line). An unbalanced or
- * empty span fails the construct, so the `{` falls back to literal text.
+ * them becomes one or more `typeAnnotationValue` tokens (split on line
+ * endings). Empty (`{}`) and unbalanced spans fail the construct so the `{`
+ * falls back to plain text.
  *
  * @type {import('micromark-util-types').Tokenizer}
  */
 function tokenizeTypeAnnotation(effects, ok, nok) {
-  // Nesting depth of inner `{`/`}` pairs (the wrapping pair is not counted)
   let depth = 0;
-  // `{}` (no content at all) must not become an annotation
   let hasContent = false;
 
   /**
-   * At the opening `{`.
-   *
    * @type {import('micromark-util-types').State}
    */
   const start = code => {
@@ -46,16 +45,13 @@ function tokenizeTypeAnnotation(effects, ok, nok) {
     effects.consume(code);
     effects.exit('typeAnnotationMarker');
 
-    return between;
+    return beforeValue;
   };
 
   /**
-   * At a position where a value chunk may start: right after the opening
-   * brace or after an interior line ending.
-   *
    * @type {import('micromark-util-types').State}
    */
-  const between = code => {
+  const beforeValue = code => {
     if (code === null) {
       return nok(code);
     }
@@ -69,7 +65,7 @@ function tokenizeTypeAnnotation(effects, ok, nok) {
       effects.consume(code);
       effects.exit('lineEnding');
 
-      return between;
+      return beforeValue;
     }
 
     effects.enter('typeAnnotationValue');
@@ -78,8 +74,6 @@ function tokenizeTypeAnnotation(effects, ok, nok) {
   };
 
   /**
-   * Inside a value chunk.
-   *
    * @type {import('micromark-util-types').State}
    */
   const value = code => {
@@ -89,14 +83,16 @@ function tokenizeTypeAnnotation(effects, ok, nok) {
       (code === RIGHT_CURLY_BRACE && depth === 0)
     ) {
       effects.exit('typeAnnotationValue');
-
-      return between(code);
+      return beforeValue(code);
     }
 
-    if (code === LEFT_CURLY_BRACE) {
-      depth++;
-    } else if (code === RIGHT_CURLY_BRACE) {
-      depth--;
+    switch (code) {
+      case LEFT_CURLY_BRACE:
+        depth++;
+        break;
+      case RIGHT_CURLY_BRACE:
+        depth--;
+        break;
     }
 
     hasContent = true;
@@ -106,8 +102,6 @@ function tokenizeTypeAnnotation(effects, ok, nok) {
   };
 
   /**
-   * At the closing `}`.
-   *
    * @type {import('micromark-util-types').State}
    */
   const finish = code => {
@@ -133,7 +127,7 @@ export const typeAnnotationSyntax = () => ({
     [LEFT_CURLY_BRACE]: {
       name: 'typeAnnotation',
       tokenize: tokenizeTypeAnnotation,
-      previous,
+      previous: previousNotDollar,
     },
   },
 });
diff --git a/src/utils/unist.mjs b/src/utils/unist.mjs
index 51f222d6..a244038b 100644
--- a/src/utils/unist.mjs
+++ b/src/utils/unist.mjs
@@ -3,73 +3,88 @@
 import { pointEnd, pointStart } from 'unist-util-position';
 
 /**
- * Escapes HTML entities ("<" and ">") in a string
- * @param {string} string The string
+ * Escapes HTML entities in a string.
+ *
+ * @param {string} value
+ * @returns {string}
  */
-const escapeHTMLEntities = string =>
-  string.replace(//g, '>');
+const escapeHTMLEntities = value =>
+  value.replace(/[<>]/g, character => (character === '<' ? '<' : '>'));
 
 /**
- * Extracts text content from a node recursively
+ * Converts a node tree back into its source-like string representation.
  *
- * @param {import('unist').Node} node The Node to be transformed into a string
- * @param {boolean} [escape] Escape HTML entities ("<", ">")?
- * @returns {string} The transformed Node as a string
+ * @param {import('unist').Node} node
+ * @param {boolean} [escape]
+ * @returns {string}
  */
 export const transformNodeToString = (node, escape) => {
   switch (node.type) {
     case 'inlineCode':
       return `\`${escape ? escapeHTMLEntities(node.value) : node.value}\``;
+
     case 'typeAnnotation':
       return `{${escape ? escapeHTMLEntities(node.value) : node.value}}`;
+
     case 'strong':
       return `**${transformNodesToString(node.children, escape)}**`;
+
     case 'emphasis':
       return `_${transformNodesToString(node.children, escape)}_`;
-    default: {
+
+    default:
       if (node.children) {
         return transformNodesToString(node.children, escape);
       }
 
-      const string = node.value?.replace(/\n/g, ' ') || '';
+      if (!node.value) {
+        return '';
+      }
+
+      // eslint-disable-next-line no-case-declarations
+      const value = node.value.replace(/\n/g, ' ');
 
-      // Replace line breaks (\n) with spaces to keep text in a single line
-      return escape ? escapeHTMLEntities(string) : string;
-    }
+      return escape ? escapeHTMLEntities(value) : value;
   }
 };
 
 /**
- * This utility allows us to join children Nodes into one
- * and transfor them back to what their source would look like
+ * Joins child nodes into their source-like string representation.
  *
- * @param {Array} nodes Nodes to parsed and joined
- * @param {boolean} [escape] Escape HTML entities ("<", ">")?
- * @returns {string} The parsed and joined nodes as a string
+ * @param {Array} nodes
+ * @param {boolean} [escape]
+ * @returns {string}
  */
 export const transformNodesToString = (nodes, escape) => {
-  const mappedChildren = nodes.map(node => transformNodeToString(node, escape));
+  let result = '';
 
-  return mappedChildren.join('');
+  for (const node of nodes) {
+    result += transformNodeToString(node, escape);
+  }
+
+  return result;
 };
 
 /**
- * This method is an utility that allows us to conditionally invoke/call a callback
- * based on test conditions related to a Node's position relative to another one
- * being before or not the other Node
+ * Calls a callback when nodeA appears after nodeB.
  *
- * NOTE: Not yet used, but probably going to be used by the JSON generator.
- *
- * @param {import('unist').Node | undefined} nodeA The Node to be used as a position reference to check against
- * the other Node. If the other Node is before this one, the callback will be called.
- * @param {import('unist').Node | undefined} nodeB The Node to be checked against the position of the first Node
- * @param {(nodeA: import('unist').Node, nodeB: import('unist').Node) => void} callback The callback to be called
+ * @param {import('unist').Node | undefined} nodeA
+ * @param {import('unist').Node | undefined} nodeB
+ * @param {(nodeA: import('unist').Node, nodeB: import('unist').Node) => void} callback
  */
 export const callIfBefore = (nodeA, nodeB, callback) => {
+  if (!nodeA || !nodeB) {
+    return;
+  }
+
   const positionA = pointEnd(nodeA);
   const positionB = pointStart(nodeB);
 
-  if (positionA && positionB && positionA.line > positionB.line) {
+  if (!positionA || !positionB) {
+    return;
+  }
+
+  if (positionA.line > positionB.line) {
     callback(nodeA, nodeB);
   }
 };