Skip to content

Commit e834f65

Browse files
committed
feat: export slugs and getHeaderNodeId functions
1 parent 08e55ff commit e834f65

File tree

5 files changed

+87
-70
lines changed

5 files changed

+87
-70
lines changed

index.js

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,70 @@
22
* @typedef {import('hast').Root} Root
33
*/
44

5+
/**
6+
* @typedef {import('hast').Node} Node
7+
*/
8+
59
import Slugger from 'github-slugger'
610
import {hasProperty} from 'hast-util-has-property'
711
import {headingRank} from 'hast-util-heading-rank'
812
import {toString} from 'hast-util-to-string'
913
import {visit} from 'unist-util-visit'
1014
import deburr from 'lodash/deburr.js'
1115

12-
const slugs = new Slugger()
16+
export const slugs = new Slugger()
17+
18+
/**
19+
* Exported function to get a Node's ID
20+
*
21+
* @param {Node} node
22+
* @param {{
23+
* enableCustomId?: boolean,
24+
* maintainCase?: boolean,
25+
* removeAccents?: boolean
26+
* }} props
27+
*/
28+
export function getHeaderNodeId(node, props = {}) {
29+
const {
30+
enableCustomId = false,
31+
maintainCase = false,
32+
removeAccents = false
33+
} = props
34+
35+
/**
36+
* @type {Node & HTMLElement}
37+
*/
38+
// @ts-ignore
39+
const headerNode = node
40+
41+
let id
42+
let isCustomId = false
43+
if (enableCustomId && headerNode.children.length > 0) {
44+
const last = headerNode.children[headerNode.children.length - 1]
45+
// This regex matches to preceding spaces and {#custom-id} at the end of a string.
46+
// Also, checks the text of node won't be empty after the removal of {#custom-id}.
47+
// @ts-ignore
48+
const match = /^(.*?)\s*{#([\w-]+)}$/.exec(toString(last))
49+
if (match && (match[1] || headerNode.children.length > 1)) {
50+
id = match[2]
51+
// Remove the custom ID from the original text.
52+
if (match[1]) {
53+
// @ts-ignore
54+
last.value = match[1]
55+
} else {
56+
isCustomId = true
57+
}
58+
}
59+
}
60+
61+
if (!id) {
62+
// @ts-ignore
63+
const slug = slugs.slug(toString(headerNode), maintainCase)
64+
id = removeAccents ? deburr(slug) : slug
65+
}
66+
67+
return {id, isCustomId}
68+
}
1369

1470
/**
1571
* Plugin to add `id`s to headings.
@@ -20,39 +76,16 @@ const slugs = new Slugger()
2076
* removeAccents?: boolean
2177
* }], Root>}
2278
*/
23-
export default function rehypeSlug({
24-
enableCustomId = false,
25-
maintainCase = false,
26-
removeAccents = false
27-
} = {}) {
79+
export default function rehypeSlug(props = {}) {
2880
return (tree) => {
2981
slugs.reset()
3082

3183
visit(tree, 'element', (node) => {
3284
if (headingRank(node) && node.properties && !hasProperty(node, 'id')) {
85+
const {id, isCustomId} = getHeaderNodeId(node, props)
3386

34-
let id
35-
if (enableCustomId && node.children.length > 0) {
36-
const last = node.children[node.children.length - 1]
37-
// This regex matches to preceding spaces and {#custom-id} at the end of a string.
38-
// Also, checks the text of node won't be empty after the removal of {#custom-id}.
39-
const match = /^(.*?)\s*\{#([\w-]+)\}$/.exec(toString(last))
40-
if (match && (match[1] || node.children.length > 1)) {
41-
id = match[2]
42-
// Remove the custom ID from the original text.
43-
if (match[1]) {
44-
// @ts-ignore
45-
last.value = match[1]
46-
} else {
47-
node.children.pop()
48-
}
49-
}
50-
}
51-
if (!id) {
52-
const slug = slugs.slug(toString(node), maintainCase)
53-
id = removeAccents ? deburr(slug) : slug
54-
}
55-
node.properties.id = id;
87+
if (isCustomId) node.children.pop()
88+
node.properties.id = id
5689
}
5790
})
5891
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@
5050
},
5151
"scripts": {
5252
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
53-
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
53+
"format": "remark . -qo && prettier . -w --loglevel warn && xo --fix",
5454
"test-api": "node --conditions development test.js",
55-
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
55+
"test-coverage": "c8 --check-coverage --branches 80 --functions 90 --lines 90 --statements 90 --reporter lcov npm run test-api",
5656
"test": "npm run build && npm run format && npm run test-coverage"
5757
},
5858
"prettier": {

readme.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ Uses [**github-slugger**][ghslug] to create GitHub style `id`s, or a custom ID i
7676

7777
We support the following options for the plugin:
7878

79-
- `enableCustomId`: `Boolean`. Enable custom header IDs with {#id} (optional)
80-
- `maintainCase`: `Boolean`. Maintains the case for markdown header (optional)
81-
- `removeAccents`: `Boolean`. Remove accents from generated headings IDs (optional)
79+
* `enableCustomId`: `Boolean`. Enable custom header IDs with {#id} (optional)
80+
* `maintainCase`: `Boolean`. Maintains the case for markdown header (optional)
81+
* `removeAccents`: `Boolean`. Remove accents from generated headings IDs (optional)
8282

8383
## Security
8484

@@ -92,11 +92,11 @@ Always be wary with user input and use [`rehype-sanitize`][sanitize].
9292

9393
## Related
9494

95-
* [`rehype-slug`](https://github.com/rehypejs/rehype-slug)
96-
— Add slugs to headings in html
97-
* [`remark-slug`](https://github.com/wooorm/remark-slug)
95+
* [`rehype-slug`](https://github.com/rehypejs/rehype-slug)
96+
— Add slugs to headings in html
97+
* [`remark-slug`](https://github.com/wooorm/remark-slug)
9898
— Add slugs to headings in markdown
99-
* [`gatsby-remark-autolink-headers`](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-remark-autolink-headers)
99+
* [`gatsby-remark-autolink-headers`](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-remark-autolink-headers)
100100
— Add slugs to headings in markdown for Gatsby
101101

102102
<!-- Definitions -->

test.js

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,42 +44,26 @@ test('rehypeSlug', (t) => {
4444
.use(rehypeSlug, {
4545
enableCustomId: true
4646
})
47-
.process(
48-
[
49-
'<h2>Test {#testing}</h2>',
50-
].join('\n'),
51-
(error, file) => {
52-
t.equal(
53-
String(file),
54-
[
55-
'<h2 id="testing">Test</h2>'
56-
].join('\n'),
57-
'should match with custom ID'
58-
)
59-
}
60-
)
47+
.process(['<h2>Test {#testing}</h2>'].join('\n'), (error, file) => {
48+
t.equal(
49+
String(file),
50+
['<h2 id="testing">Test</h2>'].join('\n'),
51+
'should match with custom ID'
52+
)
53+
})
6154

6255
rehype()
6356
.data('settings', {fragment: true})
6457
.use(rehypeSlug, {
6558
maintainCase: true
6659
})
67-
.process(
68-
[
69-
'<h1>Test</h1>',
70-
'<h2>hello</h2>',
71-
].join('\n'),
72-
(error, file) => {
73-
t.equal(
74-
String(file),
75-
[
76-
'<h1 id="Test">Test</h1>',
77-
'<h2 id="hello">hello</h2>',
78-
].join('\n'),
79-
'should maintain casing'
80-
)
81-
}
82-
)
60+
.process(['<h1>Test</h1>', '<h2>hello</h2>'].join('\n'), (error, file) => {
61+
t.equal(
62+
String(file),
63+
['<h1 id="Test">Test</h1>', '<h2 id="hello">hello</h2>'].join('\n'),
64+
'should maintain casing'
65+
)
66+
})
8367

8468
rehype()
8569
.data('settings', {fragment: true})
@@ -90,15 +74,15 @@ test('rehypeSlug', (t) => {
9074
[
9175
'<h1>Héading One</h1>',
9276
'<h2>Héading Two</h2>',
93-
'<h3>Héading Three</h3>',
77+
'<h3>Héading Three</h3>'
9478
].join('\n'),
9579
(error, file) => {
9680
t.equal(
9781
String(file),
9882
[
9983
'<h1 id="heading-one">Héading One</h1>',
10084
'<h2 id="heading-two">Héading Two</h2>',
101-
'<h3 id="heading-three">Héading Three</h3>',
85+
'<h3 id="heading-three">Héading Three</h3>'
10286
].join('\n'),
10387
'should remove accents'
10488
)

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"include": ["*.js"],
33
"compilerOptions": {
44
"target": "ES2020",
5-
"lib": ["ES2020"],
5+
"lib": ["ES2020", "DOM"],
66
"module": "ES2020",
77
"moduleResolution": "node",
88
"allowJs": true,

0 commit comments

Comments
 (0)