Skip to content

Commit 584b0e9

Browse files
feat: restore legacy anchor alias to preserve old links (#699)
* feat: restore legacy anchor alias to preserve old links Fixes: #697 * refactor(legacy-html): adopt functional closure for legacy slugger Co-authored-by: Aviv Keller <me@aviv.sh> --------- Co-authored-by: Aviv Keller <me@aviv.sh>
1 parent f0bc8d4 commit 584b0e9

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
import assert from 'node:assert/strict';
4+
import { describe, it } from 'node:test';
5+
6+
import { createLegacySlugger } from '../slugger.mjs';
7+
8+
describe('createLegacySlugger', () => {
9+
it('prefixes with api stem and uses underscores', () => {
10+
const getLegacySlug = createLegacySlugger();
11+
assert.strictEqual(getLegacySlug('File System', 'fs'), 'fs_file_system');
12+
});
13+
14+
it('replaces special characters with underscores', () => {
15+
const getLegacySlug = createLegacySlugger();
16+
assert.strictEqual(
17+
getLegacySlug('fs.readFile(path)', 'fs'),
18+
'fs_fs_readfile_path'
19+
);
20+
});
21+
22+
it('strips leading and trailing underscores', () => {
23+
const getLegacySlug = createLegacySlugger();
24+
assert.strictEqual(getLegacySlug('Hello', 'fs'), 'fs_hello');
25+
});
26+
27+
it('prefixes with underscore when result starts with non-alpha', () => {
28+
const getLegacySlug = createLegacySlugger();
29+
assert.strictEqual(getLegacySlug('123 test', '0num'), '_0num_123_test');
30+
});
31+
32+
it('deduplicates with a counter for identical titles', () => {
33+
const getLegacySlug = createLegacySlugger();
34+
assert.strictEqual(getLegacySlug('Hello', 'fs'), 'fs_hello');
35+
assert.strictEqual(getLegacySlug('Hello', 'fs'), 'fs_hello_1');
36+
assert.strictEqual(getLegacySlug('Hello', 'fs'), 'fs_hello_2');
37+
assert.strictEqual(getLegacySlug('World', 'fs'), 'fs_world');
38+
});
39+
});

src/generators/legacy-html/utils/buildContent.mjs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { u as createTree } from 'unist-builder';
55
import { SKIP, visit } from 'unist-util-visit';
66

77
import buildExtraContent from './buildExtraContent.mjs';
8+
import { createLegacySlugger } from './slugger.mjs';
89
import getConfig from '../../../utils/configuration/index.mjs';
910
import {
1011
GITHUB_BLOB_URL,
@@ -20,12 +21,14 @@ import { QUERIES, UNIST } from '../../../utils/queries/index.mjs';
2021
* @param {import('unist').Parent} parent The parent node of the current node
2122
* @returns {import('hast').Element} The HTML AST tree of the heading content
2223
*/
23-
const buildHeading = ({ data, children, depth }, index, parent) => {
24+
const buildHeading = ({ data, children, depth }, index, parent, legacySlug) => {
2425
// Creates the heading element with the heading text and the link to the heading
2526
const headingElement = createElement(`h${depth + 1}`, [
2627
// The inner Heading markdown content is still using Remark nodes, and they need
2728
// to be converted into Rehype nodes
2829
...children,
30+
// Legacy anchor alias to preserve old external links
31+
createElement('span', createElement(`a#${legacySlug}`)),
2932
// Creates the element that references the link to the heading
3033
// (The `#` anchor on the right of each Heading section)
3134
createElement(
@@ -220,6 +223,8 @@ const buildMetadataElement = (node, remark) => {
220223
* @param {import('unified').Processor} remark The Remark instance to be used to process
221224
*/
222225
export default (headNodes, metadataEntries, remark) => {
226+
const getLegacySlug = createLegacySlugger();
227+
223228
// Creates the root node for the content
224229
const parsedNodes = createTree(
225230
'root',
@@ -229,7 +234,14 @@ export default (headNodes, metadataEntries, remark) => {
229234
const content = structuredClone(entry.content);
230235

231236
// Parses the Heading nodes into Heading elements
232-
visit(content, UNIST.isHeading, buildHeading);
237+
visit(content, UNIST.isHeading, (node, index, parent) =>
238+
buildHeading(
239+
node,
240+
index,
241+
parent,
242+
getLegacySlug(node.data.text, entry.api)
243+
)
244+
);
233245

234246
// Parses the Blockquotes into Stability elements
235247
// This is treated differently as we want to preserve the position of a Stability Index
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
/**
4+
* Creates a stateful slugger for legacy anchor links.
5+
*
6+
* Generates underscore-separated slugs in the form `{apiStem}_{text}`,
7+
* appending `_{n}` for duplicates to preserve historical anchor compatibility.
8+
*
9+
* @returns {(text: string, apiStem: string) => string}
10+
*/
11+
export const createLegacySlugger =
12+
(counters = {}) =>
13+
(text, apiStem) => {
14+
const id = `${apiStem}_${text}`
15+
.toLowerCase()
16+
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '')
17+
.replace(/[^a-z0-9]+/g, '_')
18+
.replace(/^\d/, '_$&');
19+
20+
counters[id] ??= -1;
21+
const count = ++counters[id];
22+
return count > 0 ? `${id}_${count}` : id;
23+
};

0 commit comments

Comments
 (0)