valeriangalliat / markdown-it-anchor

A markdown-it plugin that adds an `id` attribute to headings and optionally permalinks.
The Unlicense
291 stars 72 forks source link

Add container around heading and anchor #100

Closed nhoizey closed 3 years ago

nhoizey commented 3 years ago

I'm trying to migrate to v8 and couldn't find how to add a container around the heading and the anchor.

Here are my options:

const options = {
    level: [2, 3, 4],
    slugify: function (s) {
      return slugify(s);
    },
    permalink: markdownItAnchor.permalink.linkAfterHeader({
      class: 'deeplink',
      symbol:
        '<svg class="icon" role="img" focusable="false" viewBox="0 0 24 24" width="1em" height="1em"><use xlink:href="#symbol-anchor" /></svg>',
      style: 'visually-hidden',
      visuallyHiddenClass: 'visually-hidden',
      assistiveText: (title) => `Permalink to heading ${title}`,
    }),
  };

I get this HTML:

<h2 id="were-my-anchor-links-accessible" tabindex="-1">Were my Anchor Links Accessible?</h2>
<a class="deeplink" href="#were-my-anchor-links-accessible">
  <span class="visually-hidden">Permalink to heading Were my Anchor Links Accessible?</span>
  <span aria-hidden="true"><svg class="icon" role="img" focusable="false" viewBox="0 0 24 24" width="1em" height="1em"><use xlink:href="#symbol-anchor"></use></svg></span>
</a>

I want to position the anchor link relative to the heading, so I need a container for both. It would be great to have an option to add this container with a class:

<div class="heading-container">
  <h2 id="were-my-anchor-links-accessible" tabindex="-1">Were my Anchor Links Accessible?</h2>
  <a class="deeplink" href="#were-my-anchor-links-accessible">
    <span class="visually-hidden">Permalink to heading Were my Anchor Links Accessible?</span>
    <span aria-hidden="true"><svg class="icon" role="img" focusable="false" viewBox="0 0 24 24" width="1em" height="1em"><use xlink:href="#symbol-anchor"></use></svg></span>
  </a>
</div>
nhoizey commented 3 years ago

This is how I get this result with markdown-it-anchor v7 (and Eleventy):

https://github.com/nhoizey/nicolas-hoizey.com/blob/main/.eleventy.js#L102-L177

const markdownItAnchorOptions = {
    permalink: true,
    permalinkClass: 'deeplink',
    permalinkSymbol:
      '<svg class="icon" role="img" focusable="false" viewBox="0 0 24 24" width="1em" height="1em"><use xlink:href="#symbol-anchor" /></svg>',
    level: [2, 3, 4],
    slugify: function (s) {
      return slugify(s);
    },
    renderPermalink: (slug, opts, state, idx) => {
      // based on fifth version in
      // https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
      const linkContent = state.tokens[idx + 1].children[0].content;

      // Create the openning <div> for the wrapper
      const headingWrapperTokenOpen = Object.assign(
        new state.Token('div_open', 'div', 1),
        {
          attrs: [['class', 'heading-wrapper']],
        }
      );
      // Create the closing </div> for the wrapper
      const headingWrapperTokenClose = Object.assign(
        new state.Token('div_close', 'div', -1),
        {
          attrs: [['class', 'heading-wrapper']],
        }
      );

      // Create the tokens for the full accessible anchor link
      // <a class="deeplink" href="#your-own-platform-is-the-nearest-you-can-get-help-to-setup">
      //   <span aria-hidden="true">
      //     ${opts.permalinkSymbol}
      //   </span>
      //   <span class="visually-hidden">
      //     Section titled Your "own" platform is the nearest you can(get help to) setup
      //   </span>
      // </a >
      const anchorTokens = [
        Object.assign(new state.Token('link_open', 'a', 1), {
          attrs: [
            ...(opts.permalinkClass ? [['class', opts.permalinkClass]] : []),
            ['href', opts.permalinkHref(slug, state)],
            ...Object.entries(opts.permalinkAttrs(slug, state)),
          ],
        }),
        Object.assign(new state.Token('span_open', 'span', 1), {
          attrs: [['aria-hidden', 'true']],
        }),
        Object.assign(new state.Token('html_block', '', 0), {
          content: opts.permalinkSymbol,
        }),
        Object.assign(new state.Token('span_close', 'span', -1), {}),
        Object.assign(new state.Token('span_open', 'span', 1), {
          attrs: [['class', 'visually-hidden']],
        }),
        Object.assign(new state.Token('html_block', '', 0), {
          content: `Section titled ${linkContent}`,
        }),
        Object.assign(new state.Token('span_close', 'span', -1), {}),
        new state.Token('link_close', 'a', -1),
      ];

      // idx is the index of the heading's first token
      // insert the wrapper opening before the heading
      state.tokens.splice(idx, 0, headingWrapperTokenOpen);
      // insert the anchor link tokens after the wrapper opening and the 3 tokens of the heading
      state.tokens.splice(idx + 3 + 1, 0, ...anchorTokens);
      // insert the wrapper closing after all these
      state.tokens.splice(
        idx + 3 + 1 + anchorTokens.length,
        0,
        headingWrapperTokenClose
      );
    },
  };
valeriangalliat commented 3 years ago

Hey! Sorry for the late reply.

I was about to suggest the following:

const linkAfterHeader = anchor.permalink.linkAfterHeader({
  class: 'deeplink',
  symbol: '<svg class="icon" role="img" focusable="false" viewBox="0 0 24 24" width="1em" height="1em"><use xlink:href="#symbol-anchor" /></svg>',
  style: 'visually-hidden',
  visuallyHiddenClass: 'visually-hidden',
  assistiveText: (title) => `Permalink to heading ${title}`,
})

const options = {
  level: [2, 3, 4],
  slugify,
  permalink (slug, opts, state, idx) {
    state.tokens.splice(idx, 0, Object.assign(new state.Token('div_open', 'div', 1), {
      attrs: [['class', 'heading-wrapper']],
      block: true
    }))

    state.tokens.splice(idx + 4, 0, Object.assign(new state.Token('div_close', 'div', -1), {
      block: true
    }))

    linkAfterHeader(slug, opts, state, idx + 1)
  }
}

But sadly this doesn't work because the fact we splice before the current header causes an infinite loop (the "next" token from markdown-it-anchor's perspective is the header we just processed, so we call the permalink on it again and so on). By default it just throws on the second iteration of the same header because the id is now explicit and not unique...

I just made a quick fix so that markdown-it-anchor is more friendly to renderers that use splice on the top-level, the example above should now work with 8.3.0!

nhoizey commented 3 years ago

Hi @valeriangalliat, it works indeed, thanks a lot! 🙏