├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.d.ts ├── index.js ├── lib ├── index.js ├── types.d.ts └── types.js ├── license ├── package.json ├── readme.md ├── test ├── fixtures │ ├── behavior-after │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── behavior-append │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── behavior-before │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── behavior-prepend │ │ ├── input.html │ │ └── output.html │ ├── behavior-wrap-content-multiple │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── behavior-wrap-content-one │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── behavior-wrap │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── content-multiple │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── content-one │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── default │ │ ├── input.html │ │ └── output.html │ ├── group │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── heading-properties │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ ├── properties │ │ ├── config.json │ │ ├── input.html │ │ └── output.html │ └── test │ │ ├── config.json │ │ ├── input.html │ │ └── output.html └── index.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v4 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | *.d.ts.map 4 | *.d.ts 5 | *.log 6 | *.tsbuildinfo 7 | .DS_Store 8 | yarn.lock 9 | !/lib/types.d.ts 10 | !/index.d.ts 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.html 3 | *.md 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type {Behavior, BuildProperties, Build, Options} from './lib/types.js' 2 | 3 | export {default} from './lib/index.js' 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {ElementContent, Element, Properties, Root} from 'hast' 3 | * @import {Visitor} from 'unist-util-visit' 4 | * @import {BuildProperties, Build, Cloneable, Options} from './types.js' 5 | */ 6 | 7 | import structuredClone from '@ungap/structured-clone' 8 | import {headingRank} from 'hast-util-heading-rank' 9 | import {convertElement} from 'hast-util-is-element' 10 | import {SKIP, visit} from 'unist-util-visit' 11 | 12 | /** @type {Element} */ 13 | const contentDefaults = { 14 | type: 'element', 15 | tagName: 'span', 16 | properties: {className: ['icon', 'icon-link']}, 17 | children: [] 18 | } 19 | 20 | /** @type {Options} */ 21 | const emptyOptions = {} 22 | 23 | /** 24 | * Add links from headings back to themselves. 25 | * 26 | * ###### Notes 27 | * 28 | * This plugin only applies to headings with `id`s. 29 | * Use `rehype-slug` to generate `id`s for headings that don’t have them. 30 | * 31 | * Several behaviors are supported: 32 | * 33 | * * `'prepend'` (default) — inject link before the heading text 34 | * * `'append'` — inject link after the heading text 35 | * * `'wrap'` — wrap the whole heading text with the link 36 | * * `'before'` — insert link before the heading 37 | * * `'after'` — insert link after the heading 38 | * 39 | * @param {Readonly | null | undefined} [options] 40 | * Configuration (optional). 41 | * @returns 42 | * Transform. 43 | */ 44 | export default function rehypeAutolinkHeadings(options) { 45 | const settings = options || emptyOptions 46 | let properties = settings.properties 47 | const headingOroperties = settings.headingProperties 48 | const behavior = settings.behavior || 'prepend' 49 | const content = settings.content 50 | const group = settings.group 51 | const is = convertElement(settings.test) 52 | 53 | /** @type {Visitor} */ 54 | let method 55 | 56 | if (behavior === 'after' || behavior === 'before') { 57 | method = around 58 | } else if (behavior === 'wrap') { 59 | method = wrap 60 | } else { 61 | method = inject 62 | 63 | if (!properties) { 64 | properties = {ariaHidden: 'true', tabIndex: -1} 65 | } 66 | } 67 | 68 | /** 69 | * Transform. 70 | * 71 | * @param {Root} tree 72 | * Tree. 73 | * @returns {undefined} 74 | * Nothing. 75 | */ 76 | return function (tree) { 77 | visit(tree, 'element', function (node, index, parent) { 78 | if (headingRank(node) && node.properties.id && is(node, index, parent)) { 79 | Object.assign(node.properties, toProperties(headingOroperties, node)) 80 | return method(node, index, parent) 81 | } 82 | }) 83 | } 84 | 85 | /** @type {Visitor} */ 86 | function inject(node) { 87 | const children = toChildren(content || contentDefaults, node) 88 | node.children[behavior === 'prepend' ? 'unshift' : 'push']( 89 | create(node, toProperties(properties, node), children) 90 | ) 91 | 92 | return [SKIP] 93 | } 94 | 95 | /** @type {Visitor} */ 96 | function around(node, index, parent) { 97 | /* c8 ignore next -- uncommon */ 98 | if (typeof index !== 'number' || !parent) return 99 | 100 | const children = toChildren(content || contentDefaults, node) 101 | const link = create(node, toProperties(properties, node), children) 102 | let nodes = behavior === 'before' ? [link, node] : [node, link] 103 | 104 | if (group) { 105 | const grouping = toNode(group, node) 106 | 107 | if (grouping && !Array.isArray(grouping) && grouping.type === 'element') { 108 | grouping.children = nodes 109 | nodes = [grouping] 110 | } 111 | } 112 | 113 | parent.children.splice(index, 1, ...nodes) 114 | 115 | return [SKIP, index + nodes.length] 116 | } 117 | 118 | /** @type {Visitor} */ 119 | function wrap(node) { 120 | /** @type {Array} */ 121 | let before = node.children 122 | /** @type {Array | ElementContent} */ 123 | let after = [] 124 | 125 | if (typeof content === 'function') { 126 | before = [] 127 | after = content(node) 128 | } else if (content) { 129 | after = clone(content) 130 | } 131 | 132 | node.children = [ 133 | create( 134 | node, 135 | toProperties(properties, node), 136 | Array.isArray(after) ? [...before, ...after] : [...before, after] 137 | ) 138 | ] 139 | 140 | return [SKIP] 141 | } 142 | } 143 | 144 | /** 145 | * Deep clone. 146 | * 147 | * @template T 148 | * Kind. 149 | * @param {T} thing 150 | * Thing to clone. 151 | * @returns {Cloneable} 152 | * Cloned thing. 153 | */ 154 | function clone(thing) { 155 | // Cast because it’s mutable now. 156 | return /** @type {Cloneable} */ (structuredClone(thing)) 157 | } 158 | 159 | /** 160 | * Create an `a`. 161 | * 162 | * @param {Readonly} node 163 | * Related heading. 164 | * @param {Properties | undefined} properties 165 | * Properties to set on the link. 166 | * @param {Array} children 167 | * Content. 168 | * @returns {Element} 169 | * Link. 170 | */ 171 | function create(node, properties, children) { 172 | return { 173 | type: 'element', 174 | tagName: 'a', 175 | properties: {...properties, href: '#' + node.properties.id}, 176 | children 177 | } 178 | } 179 | 180 | /** 181 | * Turn into children. 182 | * 183 | * @param {Readonly | ReadonlyArray | Build} value 184 | * Content. 185 | * @param {Readonly} node 186 | * Related heading. 187 | * @returns {Array} 188 | * Children. 189 | */ 190 | function toChildren(value, node) { 191 | const result = toNode(value, node) 192 | return Array.isArray(result) ? result : [result] 193 | } 194 | 195 | /** 196 | * Turn into a node. 197 | * 198 | * @param {Readonly | ReadonlyArray | Build} value 199 | * Content. 200 | * @param {Readonly} node 201 | * Related heading. 202 | * @returns {Array | ElementContent} 203 | * Node. 204 | */ 205 | function toNode(value, node) { 206 | if (typeof value === 'function') return value(node) 207 | return clone(value) 208 | } 209 | 210 | /** 211 | * Turn into properties. 212 | * 213 | * @param {Readonly | BuildProperties | null | undefined} value 214 | * Properties. 215 | * @param {Readonly} node 216 | * Related heading. 217 | * @returns {Properties} 218 | * Properties. 219 | */ 220 | function toProperties(value, node) { 221 | if (typeof value === 'function') return value(node) 222 | return value ? clone(value) : {} 223 | } 224 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Test} from 'hast-util-is-element' 2 | import type {ElementContent, Element, Properties} from 'hast' 3 | 4 | /** 5 | * Behavior. 6 | */ 7 | export type Behavior = 'after' | 'append' | 'before' | 'prepend' | 'wrap' 8 | 9 | /** 10 | * Generate properties. 11 | * 12 | * @param element 13 | * Heading. 14 | * @returns 15 | * Properties to set on the link. 16 | */ 17 | export type BuildProperties = (element: Readonly) => Properties 18 | 19 | /** 20 | * Generate content. 21 | * 22 | * @param element 23 | * Heading. 24 | * @returns 25 | * Content. 26 | */ 27 | export type Build = ( 28 | element: Readonly 29 | ) => Array | ElementContent 30 | 31 | /** 32 | * Deep clone. 33 | * 34 | * See: 35 | */ 36 | export type Cloneable = 37 | T extends Record ? {-readonly [k in keyof T]: Cloneable} : T 38 | 39 | /** 40 | * Configuration. 41 | */ 42 | export interface Options { 43 | /** 44 | * How to create links (default: `'prepend'`). 45 | */ 46 | behavior?: Behavior | null | undefined 47 | /** 48 | * Content to insert in the link 49 | * (default: if `'wrap'` then `undefined`, 50 | * otherwise ``); 51 | * if `behavior` is `'wrap'` and `Build` is passed, 52 | * its result replaces the existing content, 53 | * otherwise the content is added after existing content. 54 | */ 55 | content?: 56 | | ReadonlyArray 57 | | Readonly 58 | | Build 59 | | null 60 | | undefined 61 | /** 62 | * Content to wrap the heading and link with, 63 | * if `behavior` is `'after'` or `'before'` (optional). 64 | */ 65 | group?: 66 | | ReadonlyArray 67 | | Readonly 68 | | Build 69 | | null 70 | | undefined 71 | /** 72 | * Extra properties to set on the heading (optional). 73 | */ 74 | headingProperties?: Readonly | BuildProperties | null | undefined 75 | /** 76 | * Extra properties to set on the link when injecting 77 | * (default: `{ariaHidden: true, tabIndex: -1}` if `'append'` or `'prepend'`, 78 | * otherwise `undefined`). 79 | */ 80 | properties?: Readonly | BuildProperties | null | undefined 81 | /** 82 | * Extra test for which headings are linked (optional). 83 | */ 84 | test?: Test | null | undefined 85 | } 86 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rehype-autolink-headings", 3 | "version": "7.1.1", 4 | "description": "rehype plugin to add links to headings", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unified", 8 | "rehype", 9 | "rehype-plugin", 10 | "plugin", 11 | "heading", 12 | "link", 13 | "html" 14 | ], 15 | "repository": "rehypejs/rehype-autolink-headings", 16 | "bugs": "https://github.com/rehypejs/rehype-autolink-headings/issues", 17 | "funding": { 18 | "type": "opencollective", 19 | "url": "https://opencollective.com/unified" 20 | }, 21 | "author": "Titus Wormer (https://wooorm.com)", 22 | "contributors": [ 23 | "Titus Wormer (https://wooorm.com)" 24 | ], 25 | "sideEffects": false, 26 | "type": "module", 27 | "exports": "./index.js", 28 | "files": [ 29 | "lib/", 30 | "index.d.ts", 31 | "index.js" 32 | ], 33 | "dependencies": { 34 | "@types/hast": "^3.0.0", 35 | "@ungap/structured-clone": "^1.0.0", 36 | "hast-util-heading-rank": "^3.0.0", 37 | "hast-util-is-element": "^3.0.0", 38 | "unist-util-visit": "^5.0.0" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^22.0.0", 42 | "@types/ungap__structured-clone": "^1.0.0", 43 | "c8": "^10.0.0", 44 | "is-hidden": "^2.0.0", 45 | "prettier": "^3.0.0", 46 | "rehype": "^13.0.0", 47 | "remark-cli": "^12.0.0", 48 | "remark-preset-wooorm": "^10.0.0", 49 | "type-coverage": "^2.0.0", 50 | "typescript": "^5.0.0", 51 | "xo": "^0.59.0" 52 | }, 53 | "scripts": { 54 | "build": "tsc --build --clean && tsc --build && type-coverage", 55 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 56 | "prepack": "npm run build && npm run format", 57 | "test": "npm run build && npm run format && npm run test-coverage", 58 | "test-api": "node --conditions development test/index.js", 59 | "test-coverage": "c8 --100 --check-coverage --reporter lcov npm run test-api" 60 | }, 61 | "prettier": { 62 | "bracketSpacing": false, 63 | "singleQuote": true, 64 | "semi": false, 65 | "tabWidth": 2, 66 | "trailingComma": "none", 67 | "useTabs": false 68 | }, 69 | "remarkConfig": { 70 | "plugins": [ 71 | "remark-preset-wooorm" 72 | ] 73 | }, 74 | "typeCoverage": { 75 | "atLeast": 100, 76 | "detail": true, 77 | "ignoreCatch": true, 78 | "strict": true 79 | }, 80 | "xo": { 81 | "overrides": [ 82 | { 83 | "files": [ 84 | "**/*.d.ts" 85 | ], 86 | "rules": { 87 | "@typescript-eslint/array-type": [ 88 | "error", 89 | { 90 | "default": "generic" 91 | } 92 | ], 93 | "@typescript-eslint/ban-types": [ 94 | "error", 95 | { 96 | "extendDefaults": true 97 | } 98 | ], 99 | "@typescript-eslint/consistent-type-definitions": [ 100 | "error", 101 | "interface" 102 | ] 103 | } 104 | }, 105 | { 106 | "files": [ 107 | "test/**/*.js" 108 | ], 109 | "rules": { 110 | "no-await-in-loop": "off" 111 | } 112 | } 113 | ], 114 | "prettier": true, 115 | "rules": { 116 | "logical-assignment-operators": "off" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rehype-autolink-headings 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | **[rehype][]** plugin to add links from headings back to themselves. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When should I use this?](#when-should-i-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`unified().use(rehypeAutolinkHeadings[, options])`](#unifieduserehypeautolinkheadings-options) 21 | * [`Behavior`](#behavior) 22 | * [`Build`](#build) 23 | * [`BuildProperties`](#buildproperties) 24 | * [`Options`](#options) 25 | * [Examples](#examples) 26 | * [Example: different behaviors](#example-different-behaviors) 27 | * [Example: building content with `hastscript`](#example-building-content-with-hastscript) 28 | * [Example: passing content from a string of HTML](#example-passing-content-from-a-string-of-html) 29 | * [Example: group](#example-group) 30 | * [Types](#types) 31 | * [Compatibility](#compatibility) 32 | * [Security](#security) 33 | * [Related](#related) 34 | * [Contribute](#contribute) 35 | * [License](#license) 36 | 37 | ## What is this? 38 | 39 | This package is a [unified][] ([rehype][]) plugin to add links from headings 40 | back to themselves. 41 | It looks for headings (so `

` through `

`) that have `id` properties, 42 | and injects a link to themselves. 43 | Similar functionality is applied by many places that render markdown. 44 | For example, when browsing this readme on GitHub or npm, an anchor is added 45 | to headings, which you can share to point people to a particular place in a 46 | document. 47 | 48 | **unified** is a project that transforms content with abstract syntax trees 49 | (ASTs). 50 | **rehype** adds support for HTML to unified. 51 | **hast** is the HTML AST that rehype uses. 52 | This is a rehype plugin that adds links to headings in the AST. 53 | 54 | ## When should I use this? 55 | 56 | This plugin is useful when you have relatively long documents, where you want 57 | users to be able to link to particular sections, and you already have `id` 58 | properties set on all (or certain?) headings. 59 | 60 | A different plugin, [`rehype-slug`][rehype-slug], adds `id`s to headings. 61 | When a heading doesn’t already have an `id` property, it creates a slug from 62 | it, and adds that as the `id` property. 63 | When using both plugins together, all headings (whether explicitly with a 64 | certain `id` or automatically with a generate one) will get a link back to 65 | themselves. 66 | 67 | ## Install 68 | 69 | This package is [ESM only][esm]. 70 | In Node.js (version 16+), install with [npm][]: 71 | 72 | ```sh 73 | npm install rehype-autolink-headings 74 | ``` 75 | 76 | In Deno with [`esm.sh`][esmsh]: 77 | 78 | ```js 79 | import rehypeAutolinkHeadings from 'https://esm.sh/rehype-autolink-headings@7' 80 | ``` 81 | 82 | In browsers with [`esm.sh`][esmsh]: 83 | 84 | ```html 85 | 88 | ``` 89 | 90 | ## Use 91 | 92 | Say we have the following file `example.html`: 93 | 94 | ```html 95 |

Solar System

96 |

Formation and evolution

97 |

Structure and composition

98 |

Orbits

99 |

Composition

100 |

Distances and scales

101 |

Interplanetary environment

102 |

103 | ``` 104 | 105 | …and our module `example.js` contains: 106 | 107 | ```js 108 | import {rehype} from 'rehype' 109 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 110 | import rehypeSlug from 'rehype-slug' 111 | import {read} from 'to-vfile' 112 | 113 | const file = await rehype() 114 | .data('settings', {fragment: true}) 115 | .use(rehypeSlug) 116 | .use(rehypeAutolinkHeadings) 117 | .process(await read('example.html')) 118 | 119 | console.log(String(file)) 120 | ``` 121 | 122 | …then running `node example.js` yields: 123 | 124 | ```html 125 |

Solar System

126 |

Formation and evolution

127 |

Structure and composition

128 |

Orbits

129 |

Composition

130 |

Distances and scales

131 |

Interplanetary environment

132 |

133 | ``` 134 | 135 | ## API 136 | 137 | This package exports no identifiers. 138 | The default export is [`rehypeAutolinkHeadings`][api-rehype-autolink-headings]. 139 | 140 | ### `unified().use(rehypeAutolinkHeadings[, options])` 141 | 142 | Add links from headings back to themselves. 143 | 144 | ###### Parameters 145 | 146 | * `options` ([`Options`][api-options], optional) 147 | — configuration 148 | 149 | ###### Returns 150 | 151 | Transform ([`Transformer`][unified-transformer]). 152 | 153 | ###### Notes 154 | 155 | This plugin only applies to headings with `id`s. 156 | Use `rehype-slug` to generate `id`s for headings that don’t have them. 157 | 158 | Several behaviors are supported: 159 | 160 | * `'prepend'` (default) — inject link before the heading text 161 | * `'append'` — inject link after the heading text 162 | * `'wrap'` — wrap the whole heading text with the link 163 | * `'before'` — insert link before the heading 164 | * `'after'` — insert link after the heading 165 | 166 | ### `Behavior` 167 | 168 | Behavior (TypeScript type). 169 | 170 | ###### Type 171 | 172 | ```ts 173 | type Behavior = 'after' | 'append' | 'before' | 'prepend' | 'wrap' 174 | ``` 175 | 176 | ### `Build` 177 | 178 | Generate content (TypeScript type). 179 | 180 | ###### Parameters 181 | 182 | * `element` ([`Element`][hast-element]) 183 | — current heading 184 | 185 | ###### Returns 186 | 187 | Content ([`Array`][hast-node] or `Node`). 188 | 189 | ### `BuildProperties` 190 | 191 | Generate properties (TypeScript type). 192 | 193 | ###### Parameters 194 | 195 | * `element` ([`Element`][hast-element]) 196 | — current heading 197 | 198 | ###### Returns 199 | 200 | Properties ([`Properties`][hast-properties]). 201 | 202 | ### `Options` 203 | 204 | Configuration (TypeScript type). 205 | 206 | ###### Fields 207 | 208 | * `behavior` ([`Behavior`][api-behavior], default: `'prepend'`) 209 | — how to create links 210 | * `content` ([`Array`][hast-node], `Node`, or [`Build`][api-build], 211 | default: if `'wrap'` then `undefined`, otherwise equivalent of 212 | ``) 213 | — content to insert in the link; 214 | if `behavior` is `'wrap'` and `Build` is passed, its result replaces the 215 | existing content, otherwise the content is added after existing content 216 | * `group` ([`Array`][hast-node], `Node`, or [`Build`][api-build], 217 | optional) 218 | — content to wrap the heading and link with, if `behavior` is `'after'` or 219 | `'before'` 220 | * `headingProperties` ([`BuildProperties`][api-build-properties] or 221 | [`Properties`][hast-properties], optional) 222 | — extra properties to set on the heading 223 | * `properties` ([`BuildProperties`][api-build-properties] or 224 | [`Properties`][hast-properties], default: 225 | `{ariaHidden: true, tabIndex: -1}` if `'append'` or `'prepend'`, otherwise 226 | `undefined`) 227 | — extra properties to set on the link when injecting 228 | * `test` ([`Test`][hast-util-is-element-test], optional) 229 | — extra test for which headings are linked 230 | 231 | ## Examples 232 | 233 | ### Example: different behaviors 234 | 235 | This example shows what each behavior generates by default. 236 | 237 | ```js 238 | import {rehype} from 'rehype' 239 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 240 | 241 | const behaviors = ['after', 'append', 'before', 'prepend', 'wrap'] 242 | let index = -1 243 | while (++index < behaviors.length) { 244 | const behavior = behaviors[index] 245 | console.log( 246 | String( 247 | await rehype() 248 | .data('settings', {fragment: true}) 249 | .use(rehypeAutolinkHeadings, {behavior}) 250 | .process('

' + behavior + '

') 251 | ) 252 | ) 253 | } 254 | ``` 255 | 256 | Yields: 257 | 258 | ```html 259 |

after

260 |

append

261 |

before

262 |

prepend

263 |

wrap

264 | ``` 265 | 266 | ### Example: building content with `hastscript` 267 | 268 | The following example passes `options.content` as a function, to generate an 269 | accessible description specific to each link. 270 | It uses [`hastscript`][hastscript] to build nodes. 271 | 272 | ```js 273 | import {h} from 'hastscript' 274 | import {toString} from 'hast-util-to-string' 275 | import {rehype} from 'rehype' 276 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 277 | 278 | const file = await rehype() 279 | .data('settings', {fragment: true}) 280 | .use(rehypeAutolinkHeadings, { 281 | content(node) { 282 | return [ 283 | h('span.visually-hidden', 'Read the “', toString(node), '” section'), 284 | h('span.icon.icon-link', {ariaHidden: 'true'}) 285 | ] 286 | } 287 | }) 288 | .process('

Pluto

') 289 | 290 | console.log(String(file)) 291 | ``` 292 | 293 | Yields: 294 | 295 | ```html 296 |

Pluto

297 | ``` 298 | 299 | ### Example: passing content from a string of HTML 300 | 301 | The following example passes `content` as nodes. 302 | It uses [`hast-util-from-html-isomorphic`][hast-util-from-html-isomorphic] to 303 | build nodes from a string of HTML. 304 | 305 | ```js 306 | /** 307 | * @import {ElementContent} from 'hast' 308 | */ 309 | 310 | import {fromHtmlIsomorphic} from 'hast-util-from-html-isomorphic' 311 | import {rehype} from 'rehype' 312 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 313 | 314 | const file = await rehype() 315 | .data('settings', {fragment: true}) 316 | .use(rehypeAutolinkHeadings, { 317 | content: /** @type {Array} */ ( 318 | fromHtmlIsomorphic( 319 | '', 320 | {fragment: true} 321 | ).children 322 | ) 323 | }) 324 | .process('

Makemake

') 325 | 326 | console.log(String(file)) 327 | ``` 328 | 329 | Yields: 330 | 331 | ```html 332 |

Makemake

333 | ``` 334 | 335 | ### Example: group 336 | 337 | The following example passes `group` as a function, to dynamically generate a 338 | differing element that wraps the heading. 339 | It uses [`hastscript`][hastscript] to build nodes. 340 | 341 | ```js 342 | import {h} from 'hastscript' 343 | import {rehype} from 'rehype' 344 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 345 | 346 | const file = await rehype() 347 | .data('settings', {fragment: true}) 348 | .use(rehypeAutolinkHeadings, { 349 | behavior: 'before', 350 | group(node) { 351 | return h('.heading-' + node.tagName.charAt(1) + '-group') 352 | } 353 | }) 354 | .process('

Ceres

') 355 | 356 | console.log(String(file)) 357 | ``` 358 | 359 | Yields: 360 | 361 | ```html 362 |

Ceres

363 | ``` 364 | 365 | ## Types 366 | 367 | This package is fully typed with [TypeScript][]. 368 | It exports the additional types 369 | [`Behavior`][api-behavior], 370 | [`Build`][api-build], 371 | [`BuildProperties`][api-build-properties], and 372 | [`Options`][api-options]. 373 | 374 | ## Compatibility 375 | 376 | Projects maintained by the unified collective are compatible with maintained 377 | versions of Node.js. 378 | 379 | When we cut a new major release, we drop support for unmaintained versions of 380 | Node. 381 | This means we try to keep the current release line, 382 | `rehype-autolink-headings@^7`, compatible with Node.js 16. 383 | 384 | This plugin works with `rehype-parse` version 1+, `rehype-stringify` version 1+, 385 | `rehype` version 1+, and `unified` version 4+. 386 | 387 | ## Security 388 | 389 | Use of `rehype-autolink-headings` can open you up to a 390 | [cross-site scripting (XSS)][xss] attack if you pass user provided content in 391 | `content`, `group`, or `properties`. 392 | 393 | Always be wary of user input and use [`rehype-sanitize`][rehype-sanitize]. 394 | 395 | ## Related 396 | 397 | * [`rehype-slug`][rehype-slug] 398 | — add `id`s to headings 399 | * [`rehype-highlight`](https://github.com/rehypejs/rehype-highlight) 400 | — apply syntax highlighting to code blocks 401 | * [`rehype-toc`](https://github.com/JS-DevTools/rehype-toc) 402 | — add a table of contents (TOC) 403 | 404 | ## Contribute 405 | 406 | See [`contributing.md`][contributing] in [`rehypejs/.github`][health] for ways 407 | to get started. 408 | See [`support.md`][support] for ways to get help. 409 | 410 | This project has a [code of conduct][coc]. 411 | By interacting with this repository, organization, or community you agree to 412 | abide by its terms. 413 | 414 | ## License 415 | 416 | [MIT][license] © [Titus Wormer][author] 417 | 418 | 419 | 420 | [build-badge]: https://github.com/rehypejs/rehype-autolink-headings/workflows/main/badge.svg 421 | 422 | [build]: https://github.com/rehypejs/rehype-autolink-headings/actions 423 | 424 | [coverage-badge]: https://img.shields.io/codecov/c/github/rehypejs/rehype-autolink-headings.svg 425 | 426 | [coverage]: https://codecov.io/github/rehypejs/rehype-autolink-headings 427 | 428 | [downloads-badge]: https://img.shields.io/npm/dm/rehype-autolink-headings.svg 429 | 430 | [downloads]: https://www.npmjs.com/package/rehype-autolink-headings 431 | 432 | [size-badge]: https://img.shields.io/bundlejs/size/rehype-autolink-headings 433 | 434 | [size]: https://bundlejs.com/?q=rehype-autolink-headings 435 | 436 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 437 | 438 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 439 | 440 | [collective]: https://opencollective.com/unified 441 | 442 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 443 | 444 | [chat]: https://github.com/rehypejs/rehype/discussions 445 | 446 | [npm]: https://docs.npmjs.com/cli/install 447 | 448 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 449 | 450 | [esmsh]: https://esm.sh 451 | 452 | [health]: https://github.com/rehypejs/.github 453 | 454 | [contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md 455 | 456 | [support]: https://github.com/rehypejs/.github/blob/main/support.md 457 | 458 | [coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md 459 | 460 | [license]: license 461 | 462 | [author]: https://wooorm.com 463 | 464 | [hast-element]: https://github.com/syntax-tree/hast#element 465 | 466 | [hast-node]: https://github.com/syntax-tree/hast#nodes 467 | 468 | [hast-util-is-element-test]: https://github.com/syntax-tree/hast-util-is-element#test 469 | 470 | [hast-properties]: https://github.com/syntax-tree/hast#properties 471 | 472 | [hastscript]: https://github.com/syntax-tree/hastscript 473 | 474 | [hast-util-from-html-isomorphic]: https://github.com/syntax-tree/hast-util-from-html-isomorphic 475 | 476 | [rehype]: https://github.com/rehypejs/rehype 477 | 478 | [rehype-sanitize]: https://github.com/rehypejs/rehype-sanitize 479 | 480 | [typescript]: https://www.typescriptlang.org 481 | 482 | [unified]: https://github.com/unifiedjs/unified 483 | 484 | [unified-transformer]: https://github.com/unifiedjs/unified#transformer 485 | 486 | [xss]: https://en.wikipedia.org/wiki/Cross-site_scripting 487 | 488 | [rehype-slug]: https://github.com/rehypejs/rehype-slug 489 | 490 | [api-behavior]: #behavior 491 | 492 | [api-build]: #build 493 | 494 | [api-build-properties]: #buildproperties 495 | 496 | [api-options]: #options 497 | 498 | [api-rehype-autolink-headings]: #unifieduserehypeautolinkheadings-options 499 | -------------------------------------------------------------------------------- /test/fixtures/behavior-after/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "after" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/behavior-after/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-after/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-append/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "append" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/behavior-append/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-append/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-before/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "before" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/behavior-before/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-before/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-prepend/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-prepend/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap-content-multiple/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "wrap", 3 | "content": [ 4 | { 5 | "type": "element", 6 | "tagName": "span", 7 | "properties": {"className": ["icon", "icon-link"]}, 8 | "children": [] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap-content-multiple/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap-content-multiple/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap-content-one/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "wrap", 3 | "content": { 4 | "type": "element", 5 | "tagName": "span", 6 | "properties": {"className": ["icon", "icon-link"]}, 7 | "children": [] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap-content-one/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap-content-one/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "wrap" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/behavior-wrap/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/content-multiple/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | {"type": "comment", "value": "Foo "}, 4 | {"type": "text", "value": "bar"}, 5 | {"type": "comment", "value": " baz"} 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/content-multiple/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/content-multiple/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/content-one/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": {"type": "comment", "value": "Tada!"} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/content-one/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/content-one/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/default/input.html: -------------------------------------------------------------------------------- 1 |

Alpha

2 |

Charlie

3 |

Delta

4 | -------------------------------------------------------------------------------- /test/fixtures/default/output.html: -------------------------------------------------------------------------------- 1 |

Alpha

2 |

Charlie

3 |

Delta

4 | -------------------------------------------------------------------------------- /test/fixtures/group/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "behavior": "before", 3 | "group": { 4 | "type": "element", 5 | "tagName": "div", 6 | "properties": { 7 | "className": ["heading-group"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/group/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/group/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/heading-properties/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "headingProperties": { 3 | "className": ["foo", "bar", "baz"], 4 | "dataQux": "quux" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/heading-properties/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/heading-properties/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/properties/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "className": ["foo", "bar", "baz"], 4 | "dataQux": "quux" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/properties/input.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/properties/output.html: -------------------------------------------------------------------------------- 1 |

Bar

2 | -------------------------------------------------------------------------------- /test/fixtures/test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": ["h1", "h2"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/test/input.html: -------------------------------------------------------------------------------- 1 |

Alpha

2 |

Bravo

3 |

Charlie

4 |

Delta

5 |
Echo
6 |
Foxtrot
7 | -------------------------------------------------------------------------------- /test/fixtures/test/output.html: -------------------------------------------------------------------------------- 1 |

Alpha

2 |

Bravo

3 |

Charlie

4 |

Delta

5 |
Echo
6 |
Foxtrot
7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Options} from 'rehype-autolink-headings' 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import fs from 'node:fs/promises' 7 | import test from 'node:test' 8 | import {isHidden} from 'is-hidden' 9 | import {rehype} from 'rehype' 10 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 11 | 12 | test('rehypeAutolinkHeadings', async function (t) { 13 | await t.test('should expose the public api', async function () { 14 | assert.deepEqual( 15 | Object.keys(await import('rehype-autolink-headings')).sort(), 16 | ['default'] 17 | ) 18 | }) 19 | 20 | await t.test('should support functions', async function () { 21 | const file = await rehype() 22 | .data('settings', {fragment: true}) 23 | .use(rehypeAutolinkHeadings, { 24 | behavior: 'after', 25 | content(node) { 26 | assert.equal(node.properties.id, 'a') 27 | return {type: 'element', tagName: 'i', properties: {}, children: []} 28 | }, 29 | group(node) { 30 | assert.equal(node.properties.id, 'a') 31 | return {type: 'element', tagName: 'div', properties: {}, children: []} 32 | }, 33 | headingProperties(node) { 34 | assert.equal(node.properties.id, 'a') 35 | return {dataA: 'b'} 36 | }, 37 | properties(node) { 38 | assert.equal(node.properties.id, 'a') 39 | return {dataX: 'y'} 40 | } 41 | }) 42 | .process('

b

') 43 | 44 | assert.deepEqual( 45 | String(file), 46 | '

b

' 47 | ) 48 | }) 49 | 50 | await t.test('should `content` as a function w/ `wrap`', async function () { 51 | assert.deepEqual( 52 | String( 53 | await rehype() 54 | .data('settings', {fragment: true}) 55 | .use(rehypeAutolinkHeadings, { 56 | behavior: 'wrap', 57 | content(node) { 58 | assert.equal(node.properties.id, 'a') 59 | return [ 60 | {type: 'element', tagName: 'i', properties: {}, children: []}, 61 | ...node.children 62 | ] 63 | } 64 | }) 65 | .process('

b

') 66 | ), 67 | '

b

' 68 | ) 69 | }) 70 | }) 71 | 72 | test('fixtures', async function (t) { 73 | const root = new URL('fixtures/', import.meta.url) 74 | const folders = await fs.readdir(root) 75 | let index = -1 76 | 77 | while (++index < folders.length) { 78 | const folder = folders[index] 79 | 80 | if (isHidden(folder)) { 81 | continue 82 | } 83 | 84 | await t.test(folder, async function () { 85 | const folderUrl = new URL(folder + '/', root) 86 | const input = await fs.readFile(new URL('input.html', folderUrl)) 87 | const output = String( 88 | await fs.readFile(new URL('output.html', folderUrl)) 89 | ) 90 | /** @type {Options | undefined} */ 91 | let config 92 | 93 | try { 94 | config = JSON.parse( 95 | String(await fs.readFile(new URL('config.json', folderUrl))) 96 | ) 97 | } catch {} 98 | 99 | const file = await rehype() 100 | .data('settings', {fragment: true}) 101 | .use(rehypeAutolinkHeadings, config) 102 | .process(input) 103 | 104 | assert.equal(String(file), output) 105 | }) 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js", "lib/types.d.ts", "index.d.ts"] 16 | } 17 | --------------------------------------------------------------------------------