22 * @typedef {import('hast').Root } Root
33 */
44
5+ /**
6+ * @typedef {import('hast').Node } Node
7+ */
8+
59import Slugger from 'github-slugger'
610import { hasProperty } from 'hast-util-has-property'
711import { headingRank } from 'hast-util-heading-rank'
812import { toString } from 'hast-util-to-string'
913import { visit } from 'unist-util-visit'
1014import 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 }
0 commit comments