From 8b865f649aed9c3ba273bdd33c1e0efbbda1c01e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 4 Apr 2026 21:59:55 +0000 Subject: [PATCH 1/3] fix: use DEP code as slug for deprecation headings Headings in deprecations.md use the form 'DEP0001: some description'. The old tooling generated the DEP code (e.g., 'DEP0001') as the anchor ID for these headings, producing stable links like #DEP0190. The new tooling was generating long slugs from the full heading text, breaking existing external links. When processing the 'deprecations' API doc, detect headings that match the DEP#### pattern and use just the code as the slug rather than the full text-derived slug. Fixes: https://github.com/nodejs/doc-kit/issues/750 --- src/generators/metadata/constants.mjs | 4 ++ .../metadata/utils/__tests__/parse.test.mjs | 37 +++++++++++++++++++ src/generators/metadata/utils/parse.mjs | 17 +++++++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/generators/metadata/constants.mjs b/src/generators/metadata/constants.mjs index 614b30ef..a99d7567 100644 --- a/src/generators/metadata/constants.mjs +++ b/src/generators/metadata/constants.mjs @@ -59,5 +59,9 @@ export const DOC_API_HEADING_TYPES = [ // This regex is used to match basic TypeScript generic types (e.g., Promise) export const TYPE_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/; +// This regex matches headings in the deprecations API doc (e.g., "DEP0001: some title") +// and captures the deprecation code (e.g., "DEP0001") as the first group +export const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; + // This is the base URL of the Man7 documentation export const DOC_MAN_BASE_URL = 'http://man7.org/linux/man-pages/man'; diff --git a/src/generators/metadata/utils/__tests__/parse.test.mjs b/src/generators/metadata/utils/__tests__/parse.test.mjs index b7b9d2f6..92ffe316 100644 --- a/src/generators/metadata/utils/__tests__/parse.test.mjs +++ b/src/generators/metadata/utils/__tests__/parse.test.mjs @@ -222,6 +222,43 @@ describe('parseApiDoc', () => { }); }); + describe('deprecation heading slugs', () => { + it('uses the DEP code as slug for headings in the deprecations doc', () => { + const tree = u('root', [ + h('DEP0001: `http.OutgoingMessage.prototype.flush`', 3), + ]); + const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); + + assert.strictEqual(entry.heading.data.slug, 'DEP0001'); + }); + + it('uses the DEP code as slug regardless of the heading text that follows', () => { + const tree = u('root', [ + h( + 'DEP0190: spawning .bat and .cmd files with child_process.spawn() with shell option', + 3 + ), + ]); + const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); + + assert.strictEqual(entry.heading.data.slug, 'DEP0190'); + }); + + it('does not use the DEP code shortcut for non-deprecations docs', () => { + const tree = u('root', [h('DEP0190: some section heading', 3)]); + const [entry] = parseApiDoc({ path, tree }, typeMap); + + assert.notStrictEqual(entry.heading.data.slug, 'DEP0190'); + }); + + it('uses normal slug for non-DEP headings in the deprecations doc', () => { + const tree = u('root', [h('List of deprecated APIs', 2)]); + const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); + + assert.strictEqual(entry.heading.data.slug, 'list-of-deprecated-apis'); + }); + }); + describe('document without headings', () => { it('produces one entry for content with no headings', () => { const tree = u('root', [ diff --git a/src/generators/metadata/utils/parse.mjs b/src/generators/metadata/utils/parse.mjs index 29192653..fd74a9ba 100644 --- a/src/generators/metadata/utils/parse.mjs +++ b/src/generators/metadata/utils/parse.mjs @@ -22,7 +22,10 @@ import { import { UNIST } from '../../../utils/queries/index.mjs'; import { getRemark as remark } from '../../../utils/remark.mjs'; import { relative } from '../../../utils/url.mjs'; -import { IGNORE_STABILITY_STEMS } from '../constants.mjs'; +import { + DEPRECATION_HEADING_REGEX, + IGNORE_STABILITY_STEMS, +} from '../constants.mjs'; /** * This generator generates a flattened list of metadata entries from a API doc @@ -87,8 +90,16 @@ export const parseApiDoc = ({ path, tree }, typeMap) => { heading: headingNode, }); - // Generate slug and update heading data - metadata.heading.data.slug = nodeSlugger.slug(metadata.heading.data.text); + // Generate slug and update heading data. + // For the deprecations API doc, headings like "DEP0001: some title" use + // just the deprecation code (e.g., "DEP0001") as the anchor to preserve + // compatibility with existing external links. + const depMatch = + api === 'deprecations' && + DEPRECATION_HEADING_REGEX.exec(metadata.heading.data.text); + metadata.heading.data.slug = depMatch + ? depMatch[1] + : nodeSlugger.slug(metadata.heading.data.text); // Find the next heading to determine section boundaries const nextHeadingNode = From 89b457d604bff6c01a1348b29839070c00746a2b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 4 Apr 2026 22:07:49 +0000 Subject: [PATCH 2/3] fix: use DEP code as anchor for deprecation headings in legacy-html When generating legacy slugs for the deprecations API doc, headings in the form 'DEP0001: some description' now return just the DEP code (e.g. DEP0001) as the anchor ID. This matches the behavior of the old tooling and preserves existing external links like deprecations.html#DEP0190. The fix lives in createLegacySlugger so it stays within the legacy generator, as the legacy anchor is the one exposed on the old-format HTML page. Fixes: https://github.com/nodejs/doc-kit/issues/750 --- .../utils/__tests__/slugger.test.mjs | 32 ++++++++++++++++ src/generators/legacy-html/utils/slugger.mjs | 15 ++++++++ src/generators/metadata/constants.mjs | 4 -- .../metadata/utils/__tests__/parse.test.mjs | 37 ------------------- src/generators/metadata/utils/parse.mjs | 17 ++------- 5 files changed, 50 insertions(+), 55 deletions(-) diff --git a/src/generators/legacy-html/utils/__tests__/slugger.test.mjs b/src/generators/legacy-html/utils/__tests__/slugger.test.mjs index 852aff71..a8c247a8 100644 --- a/src/generators/legacy-html/utils/__tests__/slugger.test.mjs +++ b/src/generators/legacy-html/utils/__tests__/slugger.test.mjs @@ -36,4 +36,36 @@ describe('createLegacySlugger', () => { assert.strictEqual(getLegacySlug('Hello', 'fs'), 'fs_hello_2'); assert.strictEqual(getLegacySlug('World', 'fs'), 'fs_world'); }); + + describe('deprecation headings', () => { + it('returns the DEP code for deprecation headings in the deprecations doc', () => { + const getLegacySlug = createLegacySlugger(); + assert.strictEqual( + getLegacySlug( + 'DEP0001: `http.OutgoingMessage.prototype.flush`', + 'deprecations' + ), + 'DEP0001' + ); + }); + + it('returns the DEP code regardless of the description text', () => { + const getLegacySlug = createLegacySlugger(); + assert.strictEqual( + getLegacySlug( + 'DEP0190: spawning .bat and .cmd files with child_process.spawn() with shell option', + 'deprecations' + ), + 'DEP0190' + ); + }); + + it('does not apply deprecation special-casing outside the deprecations doc', () => { + const getLegacySlug = createLegacySlugger(); + assert.notStrictEqual( + getLegacySlug('DEP0190: some heading', 'child_process'), + 'DEP0190' + ); + }); + }); }); diff --git a/src/generators/legacy-html/utils/slugger.mjs b/src/generators/legacy-html/utils/slugger.mjs index f6a79e4f..5bff71b7 100644 --- a/src/generators/legacy-html/utils/slugger.mjs +++ b/src/generators/legacy-html/utils/slugger.mjs @@ -1,16 +1,31 @@ 'use strict'; +// Matches headings in the deprecations API doc (e.g., "DEP0001: some title") +// and captures the deprecation code (e.g., "DEP0001") as the first group +const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; + /** * Creates a stateful slugger for legacy anchor links. * * Generates underscore-separated slugs in the form `{apiStem}_{text}`, * appending `_{n}` for duplicates to preserve historical anchor compatibility. * + * For the deprecations API doc, headings matching the `DEP####:` pattern use + * just the deprecation code (e.g., `DEP0001`) as the anchor, matching the + * behavior of the old tooling and preserving existing external links. + * * @returns {(text: string, apiStem: string) => string} */ export const createLegacySlugger = (counters = {}) => (text, apiStem) => { + const depMatch = + apiStem === 'deprecations' && DEPRECATION_HEADING_REGEX.exec(text); + + if (depMatch) { + return depMatch[1]; + } + const id = `${apiStem}_${text}` .toLowerCase() .replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '') diff --git a/src/generators/metadata/constants.mjs b/src/generators/metadata/constants.mjs index a99d7567..614b30ef 100644 --- a/src/generators/metadata/constants.mjs +++ b/src/generators/metadata/constants.mjs @@ -59,9 +59,5 @@ export const DOC_API_HEADING_TYPES = [ // This regex is used to match basic TypeScript generic types (e.g., Promise) export const TYPE_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/; -// This regex matches headings in the deprecations API doc (e.g., "DEP0001: some title") -// and captures the deprecation code (e.g., "DEP0001") as the first group -export const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; - // This is the base URL of the Man7 documentation export const DOC_MAN_BASE_URL = 'http://man7.org/linux/man-pages/man'; diff --git a/src/generators/metadata/utils/__tests__/parse.test.mjs b/src/generators/metadata/utils/__tests__/parse.test.mjs index 92ffe316..b7b9d2f6 100644 --- a/src/generators/metadata/utils/__tests__/parse.test.mjs +++ b/src/generators/metadata/utils/__tests__/parse.test.mjs @@ -222,43 +222,6 @@ describe('parseApiDoc', () => { }); }); - describe('deprecation heading slugs', () => { - it('uses the DEP code as slug for headings in the deprecations doc', () => { - const tree = u('root', [ - h('DEP0001: `http.OutgoingMessage.prototype.flush`', 3), - ]); - const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); - - assert.strictEqual(entry.heading.data.slug, 'DEP0001'); - }); - - it('uses the DEP code as slug regardless of the heading text that follows', () => { - const tree = u('root', [ - h( - 'DEP0190: spawning .bat and .cmd files with child_process.spawn() with shell option', - 3 - ), - ]); - const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); - - assert.strictEqual(entry.heading.data.slug, 'DEP0190'); - }); - - it('does not use the DEP code shortcut for non-deprecations docs', () => { - const tree = u('root', [h('DEP0190: some section heading', 3)]); - const [entry] = parseApiDoc({ path, tree }, typeMap); - - assert.notStrictEqual(entry.heading.data.slug, 'DEP0190'); - }); - - it('uses normal slug for non-DEP headings in the deprecations doc', () => { - const tree = u('root', [h('List of deprecated APIs', 2)]); - const [entry] = parseApiDoc({ path: '/deprecations', tree }, typeMap); - - assert.strictEqual(entry.heading.data.slug, 'list-of-deprecated-apis'); - }); - }); - describe('document without headings', () => { it('produces one entry for content with no headings', () => { const tree = u('root', [ diff --git a/src/generators/metadata/utils/parse.mjs b/src/generators/metadata/utils/parse.mjs index fd74a9ba..29192653 100644 --- a/src/generators/metadata/utils/parse.mjs +++ b/src/generators/metadata/utils/parse.mjs @@ -22,10 +22,7 @@ import { import { UNIST } from '../../../utils/queries/index.mjs'; import { getRemark as remark } from '../../../utils/remark.mjs'; import { relative } from '../../../utils/url.mjs'; -import { - DEPRECATION_HEADING_REGEX, - IGNORE_STABILITY_STEMS, -} from '../constants.mjs'; +import { IGNORE_STABILITY_STEMS } from '../constants.mjs'; /** * This generator generates a flattened list of metadata entries from a API doc @@ -90,16 +87,8 @@ export const parseApiDoc = ({ path, tree }, typeMap) => { heading: headingNode, }); - // Generate slug and update heading data. - // For the deprecations API doc, headings like "DEP0001: some title" use - // just the deprecation code (e.g., "DEP0001") as the anchor to preserve - // compatibility with existing external links. - const depMatch = - api === 'deprecations' && - DEPRECATION_HEADING_REGEX.exec(metadata.heading.data.text); - metadata.heading.data.slug = depMatch - ? depMatch[1] - : nodeSlugger.slug(metadata.heading.data.text); + // Generate slug and update heading data + metadata.heading.data.slug = nodeSlugger.slug(metadata.heading.data.text); // Find the next heading to determine section boundaries const nextHeadingNode = From 264afe5e36f47492f89c80eab816247e008d3bf0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 4 Apr 2026 22:11:55 +0000 Subject: [PATCH 3/3] refactor: move DEPRECATION_HEADING_REGEX to legacy-html constants, drop apiStem check --- src/generators/legacy-html/constants.mjs | 5 +++++ .../legacy-html/utils/__tests__/slugger.test.mjs | 10 +--------- src/generators/legacy-html/utils/slugger.mjs | 13 +++++-------- 3 files changed, 11 insertions(+), 17 deletions(-) create mode 100644 src/generators/legacy-html/constants.mjs diff --git a/src/generators/legacy-html/constants.mjs b/src/generators/legacy-html/constants.mjs new file mode 100644 index 00000000..fe07c6e1 --- /dev/null +++ b/src/generators/legacy-html/constants.mjs @@ -0,0 +1,5 @@ +'use strict'; + +// Matches deprecation headings (e.g., "DEP0001: some title") and captures +// the deprecation code (e.g., "DEP0001") as the first group +export const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; diff --git a/src/generators/legacy-html/utils/__tests__/slugger.test.mjs b/src/generators/legacy-html/utils/__tests__/slugger.test.mjs index a8c247a8..83f508ab 100644 --- a/src/generators/legacy-html/utils/__tests__/slugger.test.mjs +++ b/src/generators/legacy-html/utils/__tests__/slugger.test.mjs @@ -38,7 +38,7 @@ describe('createLegacySlugger', () => { }); describe('deprecation headings', () => { - it('returns the DEP code for deprecation headings in the deprecations doc', () => { + it('returns the DEP code for a deprecation heading', () => { const getLegacySlug = createLegacySlugger(); assert.strictEqual( getLegacySlug( @@ -59,13 +59,5 @@ describe('createLegacySlugger', () => { 'DEP0190' ); }); - - it('does not apply deprecation special-casing outside the deprecations doc', () => { - const getLegacySlug = createLegacySlugger(); - assert.notStrictEqual( - getLegacySlug('DEP0190: some heading', 'child_process'), - 'DEP0190' - ); - }); }); }); diff --git a/src/generators/legacy-html/utils/slugger.mjs b/src/generators/legacy-html/utils/slugger.mjs index 5bff71b7..7fe769b6 100644 --- a/src/generators/legacy-html/utils/slugger.mjs +++ b/src/generators/legacy-html/utils/slugger.mjs @@ -1,8 +1,6 @@ 'use strict'; -// Matches headings in the deprecations API doc (e.g., "DEP0001: some title") -// and captures the deprecation code (e.g., "DEP0001") as the first group -const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; +import { DEPRECATION_HEADING_REGEX } from '../constants.mjs'; /** * Creates a stateful slugger for legacy anchor links. @@ -10,17 +8,16 @@ const DEPRECATION_HEADING_REGEX = /^(DEP\d+):/; * Generates underscore-separated slugs in the form `{apiStem}_{text}`, * appending `_{n}` for duplicates to preserve historical anchor compatibility. * - * For the deprecations API doc, headings matching the `DEP####:` pattern use - * just the deprecation code (e.g., `DEP0001`) as the anchor, matching the - * behavior of the old tooling and preserving existing external links. + * Headings matching the `DEP####:` pattern return just the deprecation code + * (e.g., `DEP0001`) as the anchor, matching the behavior of the old tooling + * and preserving existing external links. * * @returns {(text: string, apiStem: string) => string} */ export const createLegacySlugger = (counters = {}) => (text, apiStem) => { - const depMatch = - apiStem === 'deprecations' && DEPRECATION_HEADING_REGEX.exec(text); + const depMatch = DEPRECATION_HEADING_REGEX.exec(text); if (depMatch) { return depMatch[1];