Giter Club home page Giter Club logo

rehype-toc's Introduction

Table of Contents plugin for Rehype

A rehype plugin that adds a table of contents (TOC) to the page

Cross-Platform Compatibility Build Status

Coverage Status Dependencies

npm License Buy us a tree

Features

  • Adds a <nav> and <ol> list outlining all headings on the page
  • Combine with rehype-slug to create links to each heading
  • Ignores headings outside of <main> if it exists
  • You can customize which headings are included (defaults to <h1> - <h6>)
  • You can customize the CSS classes on every TOC element
  • Hooks give you complete customization of the generated HTML

Example

input.html
Here's the original HTML file. There are three levels of headings (<h1> - <h3>), and none of them have IDs.

<html>
  <body>
    <h1>Apple Pie Recipe</h1>
    <p>This is the world's best apple pie recipe...</p>

    <div>
      <h2>Filling</h2>
      <p>The filling is the best part...</p>

      <h3>Preparing the apples</h3>
      <p>Cut the apples into 1/4 inch slices...</p>

      <h3>Preparing the spice mix</h3>
      <p>In a mixing bowl, combine sugar, cinnamon...</p>
    </div>

    <div>
      <h2>Crust</h2>
      <p>How to make the perfect flaky crust...</p>

      <h3>Preparing the dough</h3>
      <p>Combine flour, sugar, salt...</p>

      <h3>The criss-cross top</h3>
      <p>Cut the top crust into 1/2 inch strips...</p>
    </div>
  </body>
</html>

example.js
This script reads the input.html file above writes the results to output.html (shown below). The script uses unified, rehype-parse, rehype-slug, and rehype-stringify.

const unified = require("unified");
const parse = require("rehype-parse");
const slug = require("rehype-slug");
const toc = require("@jsdevtools/rehype-toc");
const stringify = require("rehype-stringify");
const fs = require("fs");

async function example() {
  // Create a Rehype processor with the TOC plugin
  const processor = unified()
    .use(parse)
    .use(slug)
    .use(toc)
    .use(stringify);

  // Read the original HTML file
  let inputHTML = await fs.promises.readFile("input.html");

  // Process the HTML, adding heading IDs and Table of Contents
  let outputHTML = await processor.process(inputHTML);

  // Save the new HTML
  await fs.promises.writeFile("output.html", outputHTML);
}

output.html
Here's the HTML that gets created by the above script. Notice that a table of contents has been added at the top of the <body>, with links to each of the headings on the page. The headings also now have IDs, thanks to rehype-slug.

<html>
  <body>
    <nav class="toc">
      <ol class="toc-level toc-level-1">
        <li class="toc-item toc-item-h1">
          <a class="toc-link toc-link-h1" href="#apple-pie-recipe">
            Apple Pie Recipe
          </a>

          <ol class="toc-level toc-level-2">
            <li class="toc-item toc-item-h2">
              <a class="toc-link toc-link-h2" href="#filling">
                Filling
              </a>

              <ol class="toc-level toc-level-3">
                <li class="toc-item toc-item-h3">
                  <a class="toc-link toc-link-h3" href="#preparing-the-apples">
                    Preparing the apples
                  </a>
                </li>
                <li class="toc-item toc-item-h3">
                  <a class="toc-link toc-link-h3" href="#preparing-the-spice-mix">
                    Preparing the spice mix
                  </a>
                </li>
              </ol>
            </li>

            <li class="toc-item toc-item-h2">
              <a class="toc-link toc-link-h2" href="#crust">
                Crust
              </a>

              <ol class="toc-level toc-level-3">
                <li class="toc-item toc-item-h3">
                  <a class="toc-link toc-link-h3" href="#preparing-the-dough">
                    Preparing the dough
                  </a>
                </li>
                <li class="toc-item toc-item-h3">
                  <a class="toc-link toc-link-h3" href="#the-criss-cross-top">
                    The criss-cross top
                  </a>
                </li>
              </ol>
            </li>
          </ol>
        </li>
      </ol>
    </nav>

    <h1 id="apple-pie-recipe">Apple Pie Recipe</h1>
    <p>This is the world's best apple pie recipe...</p>

    <div>
      <h2 id="filling">Filling</h2>
      <p>The filling is the best part...</p>

      <h3 id="preparing-the-apples">Preparing the apples</h3>
      <p>Cut the apples into 1/4 inch slices...</p>

      <h3 id="preparing-the-spice-mix">Preparing the spice mix</h3>
      <p>In a mixing bowl, combine sugar, cinnamon...</p>
    </div>

    <div>
      <h2 id="crust">Crust</h2>
      <p>How to make the perfect flaky crust...</p>

      <h3 id="preparing-the-dough">Preparing the dough</h3>
      <p>Combine flour, sugar, salt...</p>

      <h3 id="the-criss-cross-top">The criss-cross top</h3>
      <p>Cut the top crust into 1/2 inch strips...</p>
    </div>
  </body>
</html>

Installation

You can install Rehype TOC via npm.

npm install @jsdevtools/rehype-toc

You'll probably want to install unified, rehype-parse, rehype-stringify, and rehype-slug as well.

npm install unified rehype-parse rehype-stringify rehype-slug

Usage

Using the Rehype TOC plugin requires an understanding of how to use Unified and Rehype. Here is an excelleng guide to learn the basics.

The Rehype TOC plugin works just like any other Rehype plugin. Pass it to the .use() method, optionally with an options object.

const unified = require("unified");
const toc = require("@jsdevtools/rehype-toc");

// Use the Rehype TOC plugin with its default options
unified().use(toc);

// Use the Rehype TOC plugin with custom options
unified().use(toc, {
  headings: ["h1", "h2"],     // Only include <h1> and <h2> headings in the TOC
  cssClasses: {
    toc: "page-outline",      // Change the CSS class for the TOC
    link: "page-link",        // Change the CSS class for links in the TOC
  }
});

Options

The Rehype TOC plugin supports the following options:

Option Type Default Description
nav boolean true Determines whether the table of contents is wrapped in a <nav> element.
position string "afterbegin" The position at which the table of contents should be inserted, relative to the <main> or <body> element. Can be "beforebegin", "afterbegin", "beforeend", or "afterend". See the insertAdjacentElement() docs for an explanation of each value.
headings array of strings h1, h2, h3, h4, h5, h6 The HTML heading tags to include in the table of contents
cssClasses.toc string toc The CSS class name for the top-level <nav> or <ol> element that contains the whole table of contents.
cssClasses.list string toc-level The CSS class name for all <ol> elements in the table of contents, including the top-level one.
cssClasses.listItem string toc-item The CSS class name for all <li> elements in the table of contents.
cssClasses.link string toc-link The CSS class name for all <a> elements in the table of contents.
customizeTOC function(toc) Allows you to customize the table of contents before it is added to the page.

The function receives the TOC node tree and can modify it in any way you want. Or you can return a new node tree to use instead. Or return false to prevent the the TOC from being added to the page.
customizeTOCItem function(toc, heading) Allows you to customize each item in the table of contents before it is added to the page.

The function receives the TOC item's node tree and the heading node that it refers to. You can modify the nodes in any way you want. Or you can return a new node tree to use instead. Or return false to prevent the the TOC from being added to the page.

Contributing

Contributions, enhancements, and bug-fixes are welcome! Open an issue on GitHub and submit a pull request.

Building

To build the project locally on your computer:

  1. Clone this repo
    git clone https://github.com/JS-DevTools/rehype-toc.git

  2. Install dependencies
    npm install

  3. Build the code
    npm run build

  4. Run the tests
    npm test

License

Rehype TOC is 100% free and open-source, under the MIT license. Use it however you want.

This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest youโ€™ll be creating employment for local families and restoring wildlife habitats.

Big Thanks To

Thanks to these awesome companies for their support of Open Source developers โค

Travis CI SauceLabs Coveralls

rehype-toc's People

Contributors

danlaush avatar jamesmessinger 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

rehype-toc's Issues

Unexpected table structure

I'm wondering if I'm doing something wrong here. The output of rehype-toc seems to be structured in a weird way.

Using rehype-toc, given the input .svx file:

# First Header

Hello world!

## Second Header

This is some text.

### Third header

Hey friends! ๐Ÿ‘‹

```ts
function greet(name: string) {
	console.log(`Hey ${name}! ๐Ÿ‘‹`)
}

I get the output:

<nav class="toc">
    <ol class="toc-level toc-level-1">
        <li class="toc-item toc-item-h1">
            <a class="toc-link toc-link-h1" href="#first-header">First Header</a>
            <ol class="toc-level toc-level-2">
                <li class="toc-item toc-item-h2">
                    <a class="toc-link toc-link-h2" href="#second-header">Second Header</a>
                    <ol class="toc-level toc-level-3">
                        <li class="toc-item toc-item-h3">
                            <a class="toc-link toc-link-h3" href="#third-header">Third header</a>
                        </li>
                    </ol>
                </li>
            </ol>
        </li>
    </ol>
</nav>

To me, this html looks weird. Why does the output contain nested lists?
If, for example, I wanted to apply a border to a li, I could target it with .toc li or .toc toc-item, and set a border.
But because each li includes the other levels (for some reason I don't understand?), the border would extend all the way down, causing unexpected effects:

.toc-item {
	@apply border-2  border-black/20;
}

yields:

image

Similar issues occur when you try to make ol a list-decimal:

.toc ol {
        @apply list-decimal
}

yields

image

As each ol just contains one item!

A structure like this would make more sense to me:

<nav class="toc">
    <ol>
        <li class="toc-item toc-item-h1">
            <a class="toc-link toc-link-h1" href="#first-header">First Header</a>
        </li>
	<li class="toc-item toc-item-h2">
		<a class="toc-link toc-link-h2" href="#second-header">Second Header</a>
	</li>
	<li class="toc-item toc-item-h3">
		<a class="toc-link toc-link-h3" href="#third-header">Third header</a>
	</li>
    </ol>
</nav>

As you could target each level with the nth child selector.

Have I done something weird with my config to cause this behaviour? Or is this expected behaviour? And if so, what is the reasoning to not just use the intrinsic capabilities of lists?

Possibility to put the TOC in a specific element

Is there a way to specify a selector within which I can put the TOC?

I have a use-case where I want to make the TOC position: sticky. But because the sticky elements still remain in the flow of the document I have a weird spacing.

This could be a good use-case where I'd like to place the TOC in a pre-determined position in the document.

Happy to elaborate more if needed.

Choosable placement of TOC suported?

I don't like table of contents at the very beginning of a document. Rather it should be possible to place it at a specific position - e.g. following an introduction or at the very end of a document.

remark-toc allows to place the table of content at some arbitrary position by specifying the heading's title underneath which it should be placed. I couldn't figure out how to do this with rehype-toc. Is that supported at all? If so: how?

`Node` Shape Passed to `customizeTOC` Not `unified` Standard

The shape of the node object passed to customizeTOC doesn't match the Node interface as specified by the latest version of unified. This is what the plugin is passing:

{
    type: 'element',
    tagName: 'ol',
    properties: { className: 'toc toc-level toc-level-1' },
    children: [ [Object] ]
  }

This is what unified expects:

interface Node {
  type: string
  data: Data?
  position: Position?
}

As a result, we've no way to export the TOC using that method to stringify it, store the AST separately, etc.

Hide empty table of contents by default

Currently you can customize the installation to hide the table of contents if there are no items by doing this:

customizeTOC: (toc) => {
  if (toc.children[0].children.length > 0) {
    return toc;
  }
  return false;
},

I think this should be default functionality! ๐Ÿ™‚

Related implementation

For people who want logic similar to this project, but want the ability to further customize how the table is rendered, here is a simple ReScript implementation of similar heading gathering logic based on docusaurus v2 implementation: https://framagit.org/-/snippets/6208

Feel free to close this issue.

[Feature Request] Add Extract ToC

I would like to extract the TOC without injecting it back so I can have a bit more control of what to do with it.

Primarily the rendering of such content will be completely out of the element that renders the mdx that I have set up and is able to read the ToC on the server-side so I can create filtering.

[Feature Request] Option to Toggle List Type

Hi! Thank you for your work on this library, I just implemented it in the next version of my personal website/blog to generate a TOC for my blog posts since my previous library of choice was coupled to the Gatsby/Remark ecosystem.

I wanted to utilize un-ordered list instead of ordered list for my use case, but it did not appear there was a built-in option to change this and it required writing a custom pre-parser to change the tagType in the HAST before generating the HTML. I think this is a pretty straight forward use case that would be valuable as an optional toggle value in the library options.

Would you be open to this change? I am happy to create a PR to add this functionality if so.

NextJS MDX: rehype-toc does not work as expected

I'm creating a blog using NextJs13 and MDX, I have added a table of contents to my pages using rehype-toc.

I want the table (so element) to be outside the article element, to do this I tried to use the position attribute in the plugin config, but it not worked as expected.

next.config.mjs

const withMDX = createMDX({
   extension: /\.mdx?$/,
   options: {
        ...
        [rehypeToc, {
            headings: ["h1"],
            // I suppose that TOC will be inserted after the article element
            // (so to be a child of the `<main>` element)
            position: "afterend"
            cssClasses: {
                toc: "toc not-prose",
                link: "toc-link"
            },
        }]
   }},
})

page.tsx

const Index: NextPage = ({ params }: any) => {
const { post } = params

const PostFile = dynamic(() => import(`../posts/${post}.mdx`).
   catch(() => notFound())
)

 return (
   <main>
     <article className='prose prose-slate lg:prose-xl'>
       <PostFile />
     </article>
   </main>
  )
}

If you need more information code is public on my GitHub (blog code is located in /app/blog).

How should return Jsx.Element for customizeTOC ?

Thanks for this awesome plugin, and I'm trying to wrap a <motion.div> for created TOC, but for some reason, It seems does not support the feature. Here is my related code:

.....
interface NodeTree {
  type: string;
  properties: {
    [key: string]: any;
  };
  children: (NodeTree | string)[];
}
function renderNodeTree(nodeTree: NodeTree): JSX.Element {
  const { type, properties, children } = nodeTree;

  const childElements = (children || []).map((child) => {
    if (typeof child === 'string') {
      return child;
    } else {
      return renderNodeTree(child);
    }
  });

  return React.createElement(type, properties, ...childElements);
}

function customizeTOC(toc:NodeTree){
  const tocJsx = renderNodeTree(toc)
  // return <motion.div>{tocJsx}</motion.div>
  return "Hello"
  // console.log('[toc]: ',tocJsx)
}
.....
      <ReactMarkdown
        className="markdown-body"
        children={contentStr || ''}
        rehypePlugins={[rehypeRaw,[rehypeToc,{customizeTOC,cssClasses:{toc:tocStyle['markdown-toc']}}]]}
        remarkPlugins={[remarkEmoji,remarkGfm,remarkMermaidjs]}
.....

Thanks again!

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.