I spent some time this weekend updating a remark utility I wrote a while back. I wanted to add an 'overview' section to the beginning of every post -- basically a table of contents generated from post headers. There's a great little plugin that does this called remark-toc
, which uses mdast-util-toc
to do just this. It did present a few problems for my workflow:
- It requires you to define where the table of contents went in the markdown. For other folks, I'm sure this is a useful feature, but I didn't want to have to bother.
- It removed all content between between the table of contents and the first header. Again, maybe not a problem for some folks, but I often have an introductory section that lacks a header.
Because of this, I ended up spinning up a custom implementation of mdast-util-toc
.
remark plugin
The unified
ecosystem has a fairly robust language for describing itself. At it's core, 'unified is an interface for processing text using syntax trees' (unified documentation). For our purposes, we can think of it this way:
- we have an
.mdx
or.md
file - it is run through a
parser
and converted into a syntax tree - that syntax tree can be transformed by any number of plugins or
transformers
- the new syntax tree is run through a
compiler
and output as html
We want to write a plugin that returns a transformer
that modifies our syntax tree. A really basic version would look like this:
module.exports = parse;function parse(options = {}) {return transformer;function transformer(tree) {// modify tree here}}
We can pass this module to the remarkPlugins
argument of renderToString
to transform the tree.
generating overview section
We'll update the above generic parse
function to use mdast-util-toc
, which does the heavy lifting of generating a list of headers with relevant links. mdast-util-toc
returns a node with a type
of list
that we can pass to the children
array of the syntax tree. If that node
is returned, we'll update the children
array in the syntax tree so the map comes first.
const tocUtil = require('mdast-util-toc');module.exports = parse;function parse(options = {}) {return transformer;function transformer(tree) {const { map } = tocUtil(tree);if (map) {tree.children = [].concat(map, tree.children)}}}
This works, however, I want to generate a div
with the class-name toc
that includes both this table of contents as well as an overview
heading. We'll need to generate this parent
node as well as the header node. I wrote up a really generic function for this:
const genOverview = map => {const header = {type: 'heading',depth: 3,children: [{ type: 'text', value: 'overview' }]};return {type: 'parent',children: [header, map],data: {hProperties: { className: 'toc' }}};};
And update the transformer
function:
const { map } = tocUtil(tree);if (map) {const overview = genOverview(map);tree.children = [].concat(overview, tree.children);}
While not the most exciting thing in the world, this should offer a brief overview for thinking about handling syntax tress. Looking forward to doing more in the future.
resources
up next
- templating