Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

markdown-it-anchor

valeriangalliat9.2mUnlicense9.2.0TypeScript support: included

Header anchors for markdown-it.

markdown, markdown-it, markdown-it-plugin, header, anchor

readme

markdown-it-anchor npm version

A markdown-it plugin that adds an id attribute to headings and optionally permalinks.

English | 中文 (v7.0.1)

Overview

This plugin adds an id attribute to headings, e.g. ## Foo becomes <h2 id="foo">Foo</h2>.

Optionally it can also include permalinks, e.g. <h2 id="foo"><a class="header-anchor" href="#foo">Foo</a></h2> and a bunch of other variants!

Usage

const md = require('markdown-it')()
  .use(require('markdown-it-anchor'), opts)

See a demo as JSFiddle.

The opts object can contain:

Name Description Default
level Minimum level to apply anchors, or array of selected levels. 1
permalink A function to render permalinks, see permalinks below. undefined
slugify A custom slugification function. See index.js
callback Called with token and info after rendering. undefined
getTokensText A custom function to get the text contents of the title from its tokens. See index.js
tabIndex Value of the tabindex attribute on headings, set to false to disable. -1
uniqueSlugStartIndex Index to start with when making duplicate slugs unique. 1

All headers greater than the minimum level will have an id attribute with a slug of their content. For example, you can set level to 2 to add anchors to all headers but h1. You can also pass an array of header levels to apply the anchor, like [2, 3] to have an anchor on only level 2 and 3 headers.

If a permalink renderer is given, it will be called for each matching header to add a permalink. See permalinks below.

If a slugify function is given, you can decide how to transform a heading text to a URL slug. See user-friendly URLs.

The callback option is a function that will be called at the end of rendering with the token and an info object. The info object has title and slug properties with the token content and the slug used for the identifier.

We set by default tabindex="-1" on headers. This marks the headers as focusable elements that are not reachable by keyboard navigation. The effect is that screen readers will read the title content when it's being jumped to. Outside of screen readers, the experience is the same as not setting that attribute. You can override this behavior with the tabIndex option. Set it to false to remove the attribute altogether, otherwise the value will be used as attribute value.

Finally, you can customize how the title text is extracted from the markdown-it tokens (to later generate the slug). See user-friendly URLs.

User-friendly URLs

Starting from v5.0.0, markdown-it-anchor dropped the string package to retain our core value of being an impartial and secure library. Nevertheless, users looking for backward compatibility may want the old slugify function:

npm install string
const string = require('string')
const slugify = s => string(s).slugify().toString()

const md = require('markdown-it')()
  .use(require('markdown-it-anchor'), { slugify })

Another popular library for this is @sindresorhus/slugify, which have better Unicode support and other cool features:

npm install @sindresorhus/slugify
const slugify = require('@sindresorhus/slugify')

const md = require('markdown-it')()
  .use(require('markdown-it-anchor'), { slugify: s => slugify(s) })

Custimizing the slugify input

Additionally, if you want to further customize the title that gets passed to the slugify function, you can do so by customizing the getTokensText function, that gets the plain text from a list of markdown-it inline tokens:

function getTokensText (tokens) {
  return tokens
    .filter(token => !['html_inline', 'image'].includes(token.type))
    .map(t => t.content)
    .join('')
}

const md = require('markdown-it')()
  .use(require('markdown-it-anchor'), { getTokensText })

By default we include only text and code_inline tokens, which appeared to be a sensible approach for the vast majority of use cases.

An alternative approach is to include every token's content except for html_inline and image tokens, which yields the exact same results as the previous approach with a stock markdown-it, but would also include custom tokens added by any of your markdown-it plugins, which might or might not be desirable for you. Now you have the option!

Slugifying with state

If you need access to the markdown-it state from the slugify function, e.g. to access state.env, you can use slugifyWithState instead.

const md = require('markdown-it')()
  .use(require('markdown-it-anchor'), {
    slugifyWithState: (title, state) => `${state.env.id}-${slugify(title)}`
  })

Manually setting the id attribute

You might want to explicitly set the id attribute of your headings from the Markdown document, for example to keep them consistent across translations.

markdown-it-anchor is designed to reuse any existing id, making markdown-it-attrs a perfect fit for this use case. Make sure to load it before markdown-it-anchor!

Then you can do something like this:

# Your title {#your-custom-id}

The anchor link will reuse the id that you explicitly defined.

Compatible table of contents plugin

Looking for an automatic table of contents (TOC) generator? Take a look at markdown-it-toc-done-right it's made from the ground to be a great companion of this plugin.

Parsing headings from HTML blocks

markdown-it-anchor doesn't parse HTML blocks, so headings defined in HTML blocks will be ignored. If you need to add anchors to both HTML headings and Markdown headings, the easiest way would be to do it on the final HTML rather than during the Markdown parsing phase:

const { parse } = require('node-html-parser')

const root = parse(html)

for (const h of root.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
  const slug = h.getAttribute('id') || slugify(h.textContent)
  h.setAttribute('id', slug)
  h.innerHTML = `<a href="#${slug}">${h.innerHTML}</a>`
}

console.log(root.toString())

Or with a (not accessible) GitHub-style anchor, replace the h.innerHTML part with:

h.insertAdjacentHTML('afterbegin', `<a class="anchor" aria-hidden="true" href="#${slug}">🔗</a> `)

While this still needs extra work like handling duplicated slugs and IDs, this should give you a solid base.

That said if you really want to use markdown-it-anchor for this even though it's not designed to, you can do like npm does with their marky-markdown parser, and transform the html_block tokens into a sequence of heading_open, inline, and heading_close tokens that can be handled by markdown-it-anchor:

const md = require('markdown-it')()
  .use(require('@npmcorp/marky-markdown/lib/plugin/html-heading'))
  .use(require('markdown-it-anchor'), opts)

While they use regexes to parse the HTML and it won't gracefully handle any arbitrary HTML, it should work okay for the happy path, which might be good enough for you.

You might also want to check this implementation which uses Cheerio for a more solid parsing, including support for HTML attributes.

The only edge cases I see it failing with are multiple headings defined in the same HTML block with arbitrary content between them, or headings where the opening and closing tag are defined in separate html_block tokens, both which should very rarely happen.

If you need a bulletproof implementation, I would recommend the first HTML parser approach I documented instead.

Browser example

See example.html.

Version 8.0.0 completely reworked the way permalinks work in order to offer more accessible options out of the box. You can also make your own permalink.

Instead of a single default way of rendering permalinks (which used to have a poor UX on screen readers), we now have multiple styles of permalinks for you to chose from.

const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink[styleOfPermalink](permalinkOpts)
})

Here, styleOfPermalink is one of the available styles documented below, and permalinkOpts is an options object.

All renderers share a common set of options:

Name Description Default
class The class of the permalink anchor. header-anchor
symbol The symbol in the permalink anchor. #
renderHref A custom permalink href rendering function. See permalink.js
renderAttrs A custom permalink attributes rendering function. See permalink.js

For the symbol, you may want to use the link symbol, or a symbol from your favorite web font.

This style wraps the header itself in an anchor link. It doesn't use the symbol option as there's no symbol needed in the markup (though you could add it with CSS using ::before if you like).

It's so simple it doesn't have any behaviour to custom, and it's also accessible out of the box without any further configuration, hence it doesn't have other options than the common ones described above.

You can find this style on the MDN as well as HTTP Archive and their Web Almanac, which to me is a good sign that this is a thoughtful way of implementing permalinks. This is also the style that I chose for my own blog.

Name Description Default
safariReaderFix Add a span inside the link so Safari shows headings in reader view. false (for backwards compatibility)
| See common options.
const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.headerLink()
})
<h2 id="title"><a class="header-anchor" href="#title">Title</a></h2>

The main caveat of this approach is that you can't include links inside headers. If you do, consider the other styles.

Also note that this pattern breaks reader mode in Safari, an issue you can also notice on the referenced websites above. This was already reported to Apple but their bug tracker is not public. In the meantime, a fix mentioned in the article above is to insert a span inside the link. You can use the safariReaderFix option to enable it.

const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.headerLink({ safariReaderFix: true })
})
<h2 id="title"><a class="header-anchor" href="#title"><span>Title</span></a></h2>

If you want to customize further the screen reader experience of your permalinks, this style gives you much more freedom than the header link.

It works by leaving the header itself alone, and adding the permalink after it, giving you different methods of customizing the assistive text. It makes the permalink symbol aria-hidden to not pollute the experience, and leverages a visuallyHiddenClass to hide the assistive text from the visual experience.

Name Description Default
style The (sub) style of link, one of visually-hidden, aria-label, aria-describedby or aria-labelledby. visually-hidden
assistiveText A function that takes the title and returns the assistive text. undefined, required for visually-hidden and aria-label styles
visuallyHiddenClass The class you use to make an element visually hidden. undefined, required for visually-hidden style
space Add a space between the assistive text and the permalink symbol. true
placement Placement of the permalink symbol relative to the assistive text, can be before or after the header. after
wrapper Opening and closing wrapper string, e.g. ['<div class="wrapper">', '</div>']. null
| See common options.
const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.linkAfterHeader({
    style: 'visually-hidden',
    assistiveText: title => `Permalink to “${title}”`,
    visuallyHiddenClass: 'visually-hidden',
    wrapper: ['<div class="wrapper">', '</div>']
  })
})
<div class="wrapper">
  <h2 id="title">Title</h2>
  <a class="header-anchor" href="#title">
    <span class="visually-hidden">Permalink to “Title”</span>
    <span aria-hidden="true">#</span>
  </a>
</div>

By using a visually hidden element for the assistive text, we make sure that the assistive text can be picked up by translation services, as most of the popular translation services (including Google Translate) currently ignore aria-label.

If you prefer an alternative method for the assistive text, see other styles:

<summary>aria-label variant</summary>

This removes the need from a visually hidden span, but will likely hurt the permalink experience when using a screen reader through a translation service.

const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.linkAfterHeader({
    style: 'aria-label'
    assistiveText: title => `Permalink to “${title}”`
  })
})
<h2 id="title">Title</h2>
<a class="header-anchor" href="#title" aria-label="Permalink to “Title”">#</a>
<summary>aria-describedby and aria-labelledby variants</summary>

This removes the need to customize the assistive text to your locale and doesn't need a visually hidden span either, but since the anchor will be described by just the text of the title without any context, it might be confusing.

const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.linkAfterHeader({
    style: 'aria-describedby' // Or `aria-labelledby`
  })
})
<h2 id="title">Title</h2>
<a class="header-anchor" href="#title" aria-describedby="title">#</a>

This is the equivalent of the default permalink in previous versions. The reason it's not the first one in the list is because this method has accessibility issues.

If you use a symbol like just # without adding any markup around, screen readers will read it as part of every heading (in the case of #, it could be read "pound", "number" or "number sign") meaning that if you title is "my beautiful title", it will read "number sign my beautiful title" for example. For other common symbols, 🔗 is usually read as "link symbol" and as "pilcrow".

Additionally, screen readers users commonly request the list of all links in the page, so they'll be flooded with "number sign, number sign, number sign" for each of your headings.

I would highly recommend using one of the markups above which have a better experience, but if you really want to use this markup, make sure to pass accessible HTML as symbol to make things usable, like in the example below, but even that has some flaws.

With that said, this permalink allows the following options:

Name Description Default
space Add a space between the header text and the permalink symbol. Set it to a string to customize the space (e.g. &nbsp;). true
placement Placement of the permalink, can be before or after the header. This option used to be called permalinkBefore. after
ariaHidden Whether to add aria-hidden="true", see ARIA hidden. false
| See common options.
const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.linkInsideHeader({
    symbol: `
      <span class="visually-hidden">Jump to heading</span>
      <span aria-hidden="true">#</span>
    `,
    placement: 'before'
  })
})
<h2 id="title">
  <a class="header-anchor" href="#title">
    <span class="visually-hidden">Jump to heading</span>
    <span aria-hidden="true">#</span>
  </a>
  Title
</h2>

While this example allows more accessible anchors with the same markup as previous versions of markdown-it-anchor, it's still not ideal. The assistive text for permalinks will be read as part of the heading when listing all the titles of the page, e.g. "jump to heading title 1, jump to heading title 2" and so on. Also that assistive text is not very useful when listing the links in the page (which will read "jump to heading, jump to heading, jump to heading" for each of your permalinks).

ARIA hidden

This is just an alias for linkInsideHeader with ariaHidden: true by default, to mimic GitHub's way of rendering permalinks.

Setting aria-hidden="true" makes the permalink explicitly inaccessible instead of having the permalink and its symbol being read by screen readers as part of every single headings (which was a pretty terrible experience).

const anchor = require('markdown-it-anchor')
const md = require('markdown-it')()

md.use(anchor, {
  permalink: anchor.permalink.ariaHidden({
    placement: 'before'
  })
})
<h2 id="title"><a class="header-anchor" href="#title" aria-hidden="true">#</a> Title</h2>

While no experience might be arguably better than a bad experience, I would instead recommend using one of the above renderers to provide an accessible experience. My favorite one is the header link, which is also the simplest one.

If none of those options suit you, you can always make your own renderer! Take inspiration from the code behind all permalinks.

The signature of the function you pass in the permalink option is the following:

function renderPermalink (slug, opts, state, idx) {}

Where opts are the markdown-it-anchor options, state is a markdown-it StateCore instance, and idx is the index of the heading_open token in the state.tokens array. That array contains Token objects.

To make sense of the "token stream" and the way token objects are organized, you will probably want to read the markdown-it design principles page.

This function can freely modify the token stream (state.tokens), usually around the given idx, to construct the anchor.

Because of the way the token stream works, a heading_open token is usually followed by a inline token that contains the actual text (and inline markup) of the heading, and finally a heading_close token. This is why you'll see most built-in permalink renderers touch state.tokens[idx + 1], because they update the contents of the inline token that follows a heading_open.

Debugging

If you want to debug this library more easily, we support source maps.

Use the source-map-support module to enable it with Node.js.

node -r source-map-support/register your-script.js

Development

# Build the library in the `dist/` directory.
npm run build

# Watch file changes to update `dist/`.
npm run dev

# Run tests, will use the build version so make sure to build after
# making changes.
npm test

changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

Unreleased

9.2.0 - 2024-09-07

  • Re-export Token and State types from markdown-it. (#135)

9.1.0 - 2024-08-24

  • Introduce a slugifyWithState option. (#125, #134)

9.0.2 - 2024-08-24

  • Support duplicated class attributes. (#133)

9.0.1 - 2024-05-17

  • Fix types following upgrade. (#131)

9.0.0 - 2024-05-16

  • Support markdown-it 14. This may be breaking hence the major. (#129, #128)

8.6.7 - 2023-02-16

  • Fix link in deprecation warning. (#123)

8.6.6 - 2022-12-18

  • npm audit fix. (#121)

8.6.5 - 2022-09-12

  • Support native ESM modules with TypeScript. (#118)

8.6.4 - 2021-05-11

  • Fix linkAfterHeader type to support wrapper tuple. (#116)

8.6.3 - 2021-05-05

  • Fix permalink generator type to return void instead of string. (#115)

8.6.2 - 2021-04-08

  • Fix types to reflect optional permalink arguments and properties. (#114)

8.6.1 - 2021-04-08

  • Fix type for headerLink permalink options. (#108)
  • Allow to customize the space text (e.g. using &nbsp;). (#88)

8.6.0 - 2021-04-08

  • Add a getTokensText option to customize how we extract the title text from the heading tokens. (#112)

8.5.0 - 2021-04-04

  • Support wrapping output of linkAfterHeader. (#100, #110)

8.4.1 - 2021-10-11

  • Attempt to fix npm publish that didn't publish previous version.

8.4.0 - 2021-10-11

  • Add a fix for Safari reader view in headerLink. (#107)

8.3.1 - 2021-09-15

  • Update TypeScript types to properly reflect the export style of @types/markdown-it. Also use export default anchor in type declaration instead of export = anchor so that TypeScript allows both import anchor from 'markdown-it-anchor' and import anchor = require('markdown-it-anchor') syntaxes instead of being forced to the latter. (#106)
  • Added a hack to make TypeScript work with the modern import syntax when not being used with a bundler. (6fcc502)

8.3.0 - 2021-08-26

  • Make core loop resilient to permalink renderers mutating the token stream with splice. (#100)

8.2.0 - 2021-08-26

  • Introduce a linkInsideHeader permalink option, which is the closest to the permalink in previous versions. (#101)
  • Refactor tests using AVA.

8.1.3 - 2021-08-24

  • Fix tabIndex type. (#103)

8.1.2 - 2021-07-23

  • Fix prepublish script not being run on npm publish anymore.
  • Update the dist code.

8.1.1 - 2021-07-23

  • Fix ReferenceError with process.emitWarning in the browser. (#102)

8.1.0 - 2021-07-01

  • Add token.meta.isPermalinkSymbol to help other plugins (e.g. TOC) to identify and ignore permalink symbols. (#99)

8.0.5 - 2021-07-01

  • Revert html_inline to html_block in legacy permalink. (#98)

8.0.4 - 2021-06-25

  • Fix level option TypeScript type. (#97)

8.0.3 - 2021-06-20

  • Update TypeScript types compatible with 8.0.0 release. (#95)

8.0.2 - 2021-06-19

  • Fix bug with linkAfterHeader permalink renderer. (#93)
  • Also fix regression where symbol wasn't allowed to be HTML anymore in new renderers.

8.0.1 - 2021-06-15

  • Fix permalink option typo in readme. (#91)

8.0.0 - 2021-06-14

  • Set tabindex="-1" on headers. (#85, #86)
  • Change the way to configure a permalink, allowing for more accessible choices. (#82, #89)
  • Show a deprecation warning for the old permalink option.

7.1.0 - 2021-03-06

  • Update TypeScript types. (#83)

7.0.2 - 2021-02-06

  • Optimize token parsing. (#80)

7.0.1 - 2021-01-28

  • Add a Chinese readme. (#79)

7.0.0 - 2021-01-04

  • Depend on any markdown-it version. (#76)

6.0.1 - 2020-11-19

  • Added example.html test case.
  • Added uniqueSlugStartIndex test case.
  • Fix equal -> strictEqual.
  • Updated dependencies -> found 0 vulnerabilities.

6.0.0 - 2020-09-29

  • Allow to configure unique slug start index, and make it 1 instead of 2 to mimic what markdown-toc, github-slugger, and GitHub itself does by default. This should improve out of the box compatibility with other packages. (#74)

5.3.0 - 2020-05-12

  • Fix support for user defined ids by using markdown-it-attrs.
  • Updated dependencies -> found 0 vulnerabilities.

5.2.7 - 2020-04-01

  • Forgot to build before pushing to npm.

5.2.6 - 2020-02-05

  • Support arbitrary permalink attributes with permalinkAttrs. (#63)

5.2.5 - 2019-10-16

  • Removing aria-hidden from links. (#58)

5.2.4 - 2019-06-03

  • Rolled back to ...linkTokens.
  • Executed npm audit fix to fix dependencies vulnerabilities.

5.2.3 - 2019-05-28

  • ...linkTokens -> (...).apply(null, linkTokens) IE doesn't support spread syntax.

5.2.2 - 2019-05-28

  • ...linkTokens -> [].concat(linkTokens) makes IE compatible.

5.2.1 - 2019-05-28

  • Fix typo.

5.2.0 - 2019-05-28

  • Added support for unpkg
  • Added support for mjs
  • Fix Babel issue, support ES modules. (#40, #46)
  • New option permalinkSpace makes possible to suppress the whitespace between the permalink and the header text value. Defaults to true. (#52)
  • Fix duplicate ID edge case. (#35)

5.0.1 - 2018-06-14

  • trim() before toLowerCase() to prevent dashes as prefixes and suffixes.

5.0.0 - 2018-06-14

4.0.0 - 2017-02-26

  • Drop Babel. This drops support for Node.js versions that doesn't support ES6.
  • Support code in titles. (#27)
  • Support individual header level selection. (#27)

3.0.0 - 2017-02-06

  • Use existing ID as slug if present. This drops the support for markdown-it 5 and lower, hence the major bump. (#22)

2.5.1 - 2016-11-19

  • Patch for supporting "Constructor" title. (#18)

2.5.0 - 2016-03-22

  • Test against markdown-it 6.
  • Support anchors with HTML in header.

2.4.0 - 2016-02-12

  • Add a callback option. (#16)

2.3.3 - 2015-12-21

  • Add a live example. (#13)

2.3.2 - 2015-11-29

  • Test against markdown-it 5.
  • Keep assigning module.exports after Babel 6 upgrade (that assigns exports.default only instead), using babel-plugin-add-module-exports. (#12)

2.3.1 - 2015-11-29

  • Remove hard dependency on markdown-it and replace lodash.assign with Object.assign. (#11)
  • Move to Babel 6.
  • Use babel-plugin-transform-object-assign to have Object.assign work in ES5 environments.
  • Add the permalink during compilation instead of rendering.

2.3.0 - 2015-08-13

  • Allow to pass HTML as permalink symbol. (#8)

2.2.1 - 2015-08-13

  • Do not crash when permalink is enabled and headers below specified level are present. (#7)

2.2.0 - 2015-07-20

  • Use core.ruler to add attributes so other plugins can reuse them. (#5)

2.1.0 - 2015-06-22

  • Set aria-hidden on permalink anchor.

2.0.0 - 2015-05-28

  • Place the permalink after the header by default. (#3)

    If you want to keep the old behavior, set the permalinkBefore option to true:

    const md = require('markdown-it')
      .use(require('markdown-it-anchor'), {
        permalink: true,
        permalinkBefore: true
      })

1.1.2 - 2015-05-23

  • Fix a code example in the readme.

1.1.1 - 2015-05-20

  • Slight tweaks in package.json.
  • Upgrade Babel.

1.1.0 - 2015-04-24

  • Allow to customize the permalink symbol. (#1)
  • Handle duplicate slugs by appending a number.

1.0.0 - 2015-03-18

  • Initial release.