Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add container around heading and anchor #100

Closed
nhoizey opened this issue Jul 9, 2021 · 3 comments
Closed

Add container around heading and anchor #100

nhoizey opened this issue Jul 9, 2021 · 3 comments

Comments

@nhoizey
Copy link

nhoizey commented Jul 9, 2021

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
Copy link
Author

nhoizey commented Jul 9, 2021

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
Copy link
Owner

valeriangalliat commented Aug 26, 2021

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
Copy link
Author

nhoizey commented Sep 7, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants