Giter VIP home page Giter VIP logo

eleventy-plugin-toc's Introduction

eleventy-plugin-toc

This Eleventy plugin will generate a TOC from page content using an Eleventy filter.

Default Options

{
  tags: ['h2', 'h3', 'h4'], // which heading tags are selected headings must each have an ID attribute
  wrapper: 'nav',           // element to put around the root `ol`/`ul`
  wrapperClass: 'toc',      // class for the element around the root `ol`/`ul`
  ul: false,                // if to use `ul` instead of `ol`
  flat: false,              // if subheadings should appear as child of parent or as a sibling
}

Usage

1. Install the plugin

npm i --save eleventy-plugin-toc

2. Make sure your headings have anchor IDs

Your heading elements must have ids before this plugin will create a TOC. If there aren't ids on your headings, there will be no anchors for this plugin to link to.

I use markdown-it-anchor to add those ids to the headings: Eleventy config example

// .eleventy.js

const markdownIt = require('markdown-it')
const markdownItAnchor = require('markdown-it-anchor')

module.exports = eleventyConfig => {
  // Markdown
  eleventyConfig.setLibrary(
    'md',
    markdownIt().use(markdownItAnchor)
  )
  // ... your other Eleventy config options
}

3. Add this plugin to your Eleventy config

// .eleventy.js

const pluginTOC = require('eleventy-plugin-toc')

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(pluginTOC)
}

3.1 You can override the default plugin options

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(pluginTOC, {
    tags: ['h2', 'h3'],
    wrapper: 'div'
  })
}

4. Use the filter in your layout template(s)

Because Eleventy only provides the content variable to layout templates (not to content files), you'll need to put this markup in a layout template:

<article>
  {{ content }}
</article>
<aside>
  {{ content | toc }}
</aside>

If you're using Nunjucks, include the safe filter:

<article>
  {{ content | safe }}
</article>
<aside>
  {{ content | toc | safe }}
</aside>

If you want to conditionally render a wrapper element, the filter will return undefined when no markup is generated:

{% if content | toc %}
  <aside>
    {{ content | toc }}
  </aside>
{% endif %}

5. Override default options if necessary

Pass a stringified JSON object (must be JSON.parse()-able) as an option for in your template. Because this is an object, you only need to include the key-value pairs you need to override; defaults will be preserved.

<aside>
  {{ content | toc: '{"tags":["h2","h3"],"wrapper":"div","wrapperClass":"content-tableau"}' }}
</aside>

Options

Name Default Value Type Purpose
tags ['h2', 'h3', 'h4'] array of strings which heading tags are used to generate the table of contents
wrapper 'nav' string tag of element wrapping toc lists; '' removes wrapper element
wrapperClass 'toc' string class on element wrapping toc lists
wrapperLabel undefined string aria-label on element wrapping toc lists
ul false boolean lists are ul if true, ol if false
flat false boolean use flat list if true; use nested lists if false

Roadmap

  • Some tests would be nice

eleventy-plugin-toc's People

Contributors

jdsteinbach avatar michaelsolati avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

eleventy-plugin-toc's Issues

Stringified override options don't parse

Hi!
So far so good, except when I pass the JSON string in your example, the template no longer parses (Having trouble writing template: docs/components/card/index.html (TemplateWriterWriteError)).

Any idea what that might be? Thanks.

Add ARIA patterns to the wrapper element

Thanks for writing this plugin! I'm currently working on a design system using 11ty and this has been very useful.

I have two accessibility related suggestions.

  1. Add aria-label and aria-labelledby to help identify the navigation landmark.
  2. Add role="navigation" for any wrapper other than "nav".

4.3.6 Navigation | WAI-ARIA Authoring Practices 1.1

aria-label

The aria-label attribute could be included by default with a value of "Table of contents". A new option of wrapperLabel could be added to replace the default value.

/* Config */

{
	wrapperLabel: 'Article contents'
}

/* Result */

<nav aria-label="Article contents">
	<ol>
		…
	</ol>
</nav>

aria-labelledby

If the author wants visible text to describe the table of contents, they could set a wrapperLabelHeading option to "true".

That would add aria-labelledby="toc-label" to the <nav> element, and a <h2> with an id of "toc-label" with the wrapperLabel option providing the text.

/* Config */

{
	wrapperLabel: 'Article contents',
	wrapperLabelHeading: true
}

/* Result */

<nav aria-labelledby="toc-label">
	<h2 id="toc-label">Table of contents</h2>

	<ol>
		…
	</ol>
</nav>

role="navigation"

If the wrapper is anything other then <nav>, add role="navigation".

/* Config */

{
	wrapper: 'div'
}

/* Result */

<div role="navigation" aria-label="Table of contents">
	<ol>
		…
	</ol>
</div>

Navigation Landmark: ARIA Landmark Example

IDs on headings otherwise it doesn't work

Great plugin but I had a pretty hard time getting this to work because I didn't feel that the following was very obvious in the docs

Note: you'll need to make sure you have ids on heading elements before this plugin will create a TOC. If there aren't ids, there will be nothing for links in this TOC to link to. (I recommend using markdown-it-anchor to add those ids to the headings: Eleventy config example)

I think that this needs highlighting stronger in the installation steps and it would be nice if you could provide some example code to render suitable ids to work well with the plugin.

I ended up pulling the markdown-it config code from the eleventy.js file in your blog repo here on Github after some digging

  /* Markdown */
  let markdownIt = require('markdown-it')
  let markdownItAnchor = require('markdown-it-anchor')
  let options = {
    html: true,
    breaks: true,
    linkify: true,
    typographer: true
  }
  let opts = {
    permalink: true,
    permalinkClass: 'anchor-link',
    permalinkSymbol: '#',
    level: [1, 2, 3, 4]
  }

  eleventyConfig.setLibrary('md', markdownIt(options).use(markdownItAnchor, opts))

Option to preserve inline formatting in heading content

The generated TOC currently drops any inline formatting from the headings, which in some cases is ok, and in others makes them read very weird (as an extreme example, imagine <h2><s>deleted</s> something new</h2> which would be displayed as "deleted something new").

It would be nice if there was an option to preserve some or all formatting. An MVP would be just a boolean, but the setting could in the future take an array/set of tags too.

Error: filter not found: toc (via Template render error)

Environment

Node version: 19.8.1
OS/version: macOS 13.4

Eleventy

Eleventy version: 2.0.1
Template engine: njk
Sample input file: what does it mean?

Plugin

Plugin module version: 1.1.5
Plugin settings object (if used): tags: ['h2', 'h3'], wrapper: 'div'
Plugin use in template:

{% if content | toc %}
  <aside class="md:sticky md:top-10 p-4 bg-rose-50 rounded-xl">
      {{ content | toc | safe }}
  </aside>
{% endif %}

Description of the issue

Followed through the instructions as closely as I could but on build 11ty throws an error.

I was able to fix it with some trial and error (I'm no developer) but it sure feels like a hack.

In .eleventy.js added globalOpts to module.export and copy pasted the filter from this repo's .eleventy.js like this:

module.exports = function (eleventyConfig, globalOpts) {
    globalOpts = globalOpts || {}
    eleventyConfig.namespace(globalOpts, () => {
        eleventyConfig.addFilter('toc', (content, localOpts) => {
        return buildTOC(content, parseOptions(localOpts, globalOpts))
        })
    });
}

I have `markdown-it-anchor' installed and working.

Desired output

I expected to see a list of links in the <aside> of the layout.

Actual output

[11ty] 1. Having trouble writing to "_site/blog/2020-review-koltozes-az-elso-covid-19-hullam-kellos-kozepen/index.html" from "./blog/2020-review-koltozes-az-elso-covid-19-hullam-kellos-kozepen.md" (via EleventyTemplateError)
[11ty] 2. (./_includes/post-layout.njk)
[11ty]   Error: filter not found: toc (via Template render error)
[11ty]
[11ty] Original error stack trace: Template render error: (./_includes/post-layout.njk)
[11ty]   Error: filter not found: toc
[11ty]     at Object._prettifyError (/opt/homebrew/lib/node_modules/@11ty/eleventy/node_modules/nunjucks/src/lib.js:32:11)
[11ty]     at /opt/homebrew/lib/node_modules/@11ty/eleventy/node_modules/nunjucks/src/environment.js:464:19
[11ty]     at Template.root [as rootRenderFunc] (eval at _compile (/opt/homebrew/lib/node_modules/@11ty/eleventy/node_modules/nunjucks/src/environment.js:527:18), <anonymous>:32:3)
[11ty]     at Template.render (/opt/homebrew/lib/node_modules/@11ty/eleventy/node_modules/nunjucks/src/environment.js:454:10)
[11ty]     at /opt/homebrew/lib/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js:411:14
[11ty]     at new Promise (<anonymous>)
[11ty]     at /opt/homebrew/lib/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js:410:14
[11ty]     at TemplateLayout.render (/opt/homebrew/lib/node_modules/@11ty/eleventy/src/TemplateLayout.js:236:31)
[11ty]     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
[11ty]     at async Template.renderPageEntry (/opt/homebrew/lib/node_modules/@11ty/eleventy/src/Template.js:793:17)

<h2> TOCs rendering, but not <h3> or <h4>

As the title states, <h2> renders fine, but <h3> and <h4> tags do not, even though they are after <h2>, and correspondingly have their own IDs.

Using the default settings:

const pluginTOC = require('eleventy-plugin-toc', {
  tags: ['h2', 'h3', 'h4'], // which heading tags are selected headings must each have an ID attribute
  wrapper: 'nav',           // element to put around the root `ol`/`ul`
  wrapperClass: 'toc',      // class for the element around the root `ol`/`ul`
  ul: false,                // if to use `ul` instead of `ol`
  flat: false,              // if subheadings should appear as child of parent or as a sibling
});

Minimum reproducible example:

---
title: Title
layout: post.njk
---
<h2 id="header-test1">test1</h2>
<h3 id="header-test2">test2</h3>
<h4 id="header-test3">test3</h4>

Feature request: Ability to append or prepend TOC items

Thanks for making this plugin!

I tried to use it today to replace some in-house code that generated TOCs client-side. The issue I run into was that we are also adding sections with headings in the same template, which of course are not reflected in the TOC as they are not part of content. It would be nice to be able to add TOC items for these manually.

One possible API for this would be to deprecate the ul option and replace it with a listElement option that takes values like ul or 'ol, but could also take the empty string for no list. Then the consumer of this plugin can add the <ol>/<ul> manually and prepend/append any additional items.

clarify documentation

I misunderstood the step "4. Use the filter in your template".
After a while I figured out that that HTML goes in a layout, not in a markdown file.
Maybe it would be more clear to say "4. Use the filter in your layout".

Position TOC within article content - After first paragraph

Is your feature request related to a problem? Please describe.

It's common to find the table of contents after one or two introductory paragraphs.
I believe this is yet not possible with this plugin.

Describe the solution you'd like

It'd be nice to be able to add on our markdown files some indicator of where we want the table to get added.
Something like

paragraph
paragraph

{{ TOC }}

paragraph
paragraph

`TemplateContentRenderError` was thrown > cheerio.load() expects a string

Hi, getting this error

`TemplateContentRenderError` was thrown
> cheerio.load() expects a string, file:./modules.md, line:9

`RenderError` was thrown
> cheerio.load() expects a string

`Error` was thrown:
    Error: cheerio.load() expects a string
        at Function.exports.load (/usr/local/lib/node_modules/eleventy-plugin-toc/node_modules/cheerio/lib/static.js:30:11)
        at BuildTOC (/usr/local/lib/node_modules/eleventy-plugin-toc/src/BuildTOC.js:18:21)
        at /usr/local/lib/node_modules/eleventy-plugin-toc/.eleventy.js:8:14

with a simple .md file

        <h2>{{ page.title }}</h2>
        <div class="line-shape"></div>

{{ content | toc : '{"tags":["h2","h3"],"wrapper":"div","wrapperClass":"content-tableau"}' }}

HTML entities are not escaped

If you have a heading such as:

# `<div>`

The < and > will be successfully escaped when rendering the HTML, but however this plugin extracts it and outputs it to the TOC, retains the < and > which produces invalid HTML.

Allow wrapper to take a function

Hi! 👋

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch [email protected] for the project I'm working on.

Users may need finer control over the wrapping element. In this patch, I allow wrapper to be a binary function taking content and label, which returns HTML. the wrapping element must apply aria-label="${label}"

Here is the diff that solved my problem:

diff --git a/node_modules/eleventy-plugin-toc/.eleventy.js b/node_modules/eleventy-plugin-toc/.eleventy.js
index 6a926b8..f87651d 100644
--- a/node_modules/eleventy-plugin-toc/.eleventy.js
+++ b/node_modules/eleventy-plugin-toc/.eleventy.js
@@ -4,7 +4,7 @@ const parseOptions = require('./src/ParseOptions')
 module.exports = (eleventyConfig, globalOpts) => {
   globalOpts = globalOpts || {}
   eleventyConfig.namespace(globalOpts, () => {
-    eleventyConfig.addFilter('toc', (content, localOpts) => {
+    eleventyConfig.addFilter('toc', function (content, localOpts) {
       return buildTOC(content, parseOptions(localOpts, globalOpts))
     })
   })
diff --git a/node_modules/eleventy-plugin-toc/src/BuildTOC.js b/node_modules/eleventy-plugin-toc/src/BuildTOC.js
index 50ced4a..00e3e60 100644
--- a/node_modules/eleventy-plugin-toc/src/BuildTOC.js
+++ b/node_modules/eleventy-plugin-toc/src/BuildTOC.js
@@ -29,11 +29,12 @@ const BuildTOC = (text, opts) => {
 
   const label = wrapperLabel ? `aria-label="${wrapperLabel}"` : ''
 
-  return wrapper
-    ? `<${wrapper} class="${wrapperClass}" ${label}>
-        ${BuildList(headings, ul, flat)}
-      </${wrapper}>`
-    : BuildList(headings, ul, flat)
+  const content = BuildList(headings, ul, flat);
+  return (
+      typeof wrapper === 'function' ? wrapper(content, label)
+    : wrapper ? `<${wrapper} class="${wrapperClass}" ${label}>${content}</${wrapper}>`
+    : content
+  );
 }
 
 module.exports = BuildTOC

This issue body was partially generated by patch-package.

PluginTOC renders permalink symbol from markdown-it

Environment

Node version: v17.0.1
OS/version: Mac OS 11.6 Big Sur

Eleventy

Eleventy version: 0.12.1
Template engine: Nunjucks

Plugin

Plugin module version: 1.1.0


Description of the issue

PluginTOC renders everything inside the h(n) tag. Markdown-it generates permalinks by placing an a tag inside the h(n) tag.

Desired output

PluginTOC renders the text content of the h(n) tag and ignores any other markup inside it.

Make wrapper element optional

Currently, there is no option to completely remove the wrapper element. It would be nice to pass something like { wrapper: null } and/or { wrapper: undefined } to render only the ul/ol part.

Layout.html specific custom anchors

Big fan of this plugin. I use it for the table of contents on my Tech blog https://blog.frost.kiwi/

I have hacked in a feature I need with the implementation of that blog. Source code is here: https://github.com/FrostKiwi/treasurechest

I have a comments section and I want the Post title to be in the table of contents. Both of these are in the layout.html though, not the markdown posts themselves. Eg., here are is the header and anchor for the comments:

<h2 id="comments" class="site_title">Comment via GitHub <a href="#comments" class="anchor-link">#</a></h2>

I need these reflected in the table of contents. I found no feature to insert those, so I hacked in this feature via this function in this line of my .eleventy.js via rather fragile string manipulation.

eleventyConfig.addFilter("modifyTOC", function (tocHtml, postTitle) {
	if (!tocHtml)
		tocHtml = `<nav class="toc"><ul></ul></nav>`;
	/* Clear whitespace before string matching */
	tocHtml = tocHtml.replace(/>\s+</g, '><');
	/* Header */
	tocHtml = tocHtml.replace('<ul>', `<ul><li><a href="#${postTitle}">${postTitle}</a><ul>`);
	/* Comments */
	tocHtml = tocHtml.replace('</ul></nav>', '</ul></li><li><a href="#comments">Comments</a></li></ul></nav>');
	return tocHtml;
});

There is also some extra shenanigans going on with the nesting. I want the H1 Post title and Comments to be the outer most bullet points, with the rest being nested under the title bullet point. However, only the title is H1. All other points and also the comments are H2. So heading level and nesting don't quite correspond.

So this is the feature I needed for my blog: Being able to cross reference custom tags in the layout.html, with a specific placement in the Table of Contents hierarchy. I think it would make a good QoL feature, considering that I have not found such a feature in all the other markdown-it based table of content plugins.

ToC is generated out of order

Say, I have the following HTML:

<h1>Style Guide</h1>
  <h2 id="a">a</h2>
    <h3 id="aa">aa</h3>
    <p>aa foo bar</p>
       <h4 id="aaa">aaa</h4>
       <p>blah blah</p>
  <h2 id="b">b</h2>
  <p>bbb</p>
  <h2 id="c">c</h2>
    <h3 id="cc">cc</h3>
    <p>cc cc cc</p>

The generated ToC puts cc h3 into the first a group, instead of putting it under c group:

Screen Shot 2021-01-17 at 10 51 49 AM

Add Custom CSS Classes

So far to add a custom CSS class to the TOC is to use the following workaround:

  eleventyConfig.addPlugin(toc, {
    wrapper: undefined,
  });

  eleventyConfig.addFilter("bootstrapToc", (tocHtml) => {
    if (tocHtml) {
      return tocHtml
        .replace(/<ol>/g, `<ol class="list-group">`)
        .replace(/<li>/g, `<li class="list-group-item">`);
    }
    return tocHtml;
  });

Would be nice to have a built-in feature to achieve the same results.

Output is escaped

Using...

{{ content | toc }}

... I get

&lt;nav class=&quot;toc&quot;&gt;&lt;ol&gt;&lt;li&gt;&lt;a href=&quot;#here-be-a-heading&quot;&gt;Here be a heading&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#aaaaaaaaand-another&quot;&gt;Aaaaaaaaand another&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;

Any ideas? I tried {{content | safe | toc}} but that errored.

Thanks.

Use the table of content in another page

Hi there!
thanks for this amazing plugin.

i’m trying to use the generated table of content on another page than the one used,
is it possible to add a preprend to the configuration to preprend the link in the url?

{{ templateContent | toc | safe }}

could then accept something along those line

{{ item.templaceContent | toc: '{"prepend": "{{item.permalink}}/"} | safe' }}

where item would be a page inside a loop.

I’m gonna have a look at the code and see if that could work, but i don’t know if the question was asked before.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.