#- I am a level 1 subtitle
##- I am a level 2 subtitle
######- Up to six levels are supported corresponding to heading levels
<h1>I am a level 1 heading</h1> | |
<p class="subtitle subtitle--1">I am a level 1 subtitle</p> | |
<p class="subtitle subtitle--2">I am a level 2 subtitle</p> | |
<p class="subtitle subtitle--6"> | |
Up to six levels are supported corresponding to heading levels | |
</p> |
// These `unist-util-*` utilities are super useful when working with unist | |
// syntax trees | |
const is = require("unist-util-is"); | |
const visit = require("unist-util-visit"); | |
// We're going to need this to convert some mdast nodes to hast nodes later on | |
const mdastToHast = require("mdast-util-to-hast"); | |
// Our plugin's constructor function. This would receive configuration options. | |
module.exports = function subtitlePlugin() { | |
// Plugins need to return a transform function that takes a unified compatable | |
// AST and manipulate or walk it. | |
return async function transform(tree) { | |
// Go through the Markdown document (in mdast form) and call my callback | |
// whenever you see paragraph nodes. | |
visit(tree, "paragraph", (paragraphNode) => { | |
const { children } = paragraphNode; | |
// Get the first child node under the paragraph and make sure it's a text | |
// node. If it's not, skip processing this paragraph node. | |
const textNode = children && children[0]; | |
if (!is(textNode, "text")) { | |
return; | |
} | |
// Does this text node start with a sequence of hash ('#') signs followed | |
// by a dash ('-')? | |
const text = | |
typeof textNode.value === "string" ? textNode.value.trimLeft() : ""; | |
const re = /^(#{1,6})-\s+/; | |
const matches = text.match(re); | |
if (typeof text === "string" && !matches) { | |
return; | |
} | |
// If it did let's count the number of '#'s as that will be our subtitle | |
// depth | |
const depth = matches[1].length; | |
// Once we have what we need, let's make a copy of this text node without | |
// the leading subtitle syntax. | |
// i.e. '##- hello world' becomes 'hello world' | |
const newValue = text.replace(re, ""); | |
// We can now attach some metadata to an mdast node. If the node is being | |
// serialized to html by a hast-compatible library, it will know to use | |
// these overrides instead of the default behaviour of rendering a plain | |
// <p> tag. | |
paragraphNode.data = { | |
// we could use a different html tag but "p" is semantically correct for | |
// the subtitle | |
hName: "p", | |
// The <p> tag will have the following attributes added to it. | |
// Note that we need to use "className" for the html "class" attribute. | |
hProperties: { | |
className: `subtitle subtitle--${depth}`, | |
"data-remark-subtype": "subtitle", | |
"data-subtitle": depth, | |
}, | |
// When we are passing custom children, it is our responsibility to make | |
// sure they are in hast format instead of mdast. We use the library, | |
// mdast-util-to-hast, to do this conversion. | |
hChildren: [ | |
// We pass in a modified text node without the leading subtitle | |
// characters | |
{ | |
...textNode, | |
value: newValue, | |
}, | |
// Then we pass in the rest of the children under this paragraph node | |
...children.slice(1), | |
].map(mdastToHast), // Finally convert it all to hast | |
}; | |
}); | |
}; | |
}; |
// our Markdown parser that spits out mdast | |
const remark = require("remark"); | |
// an mdast to html serializer | |
const html = require("remark-html"); | |
// the plugin we'll write | |
const subtitlePlugin = require("./remark-subtitles"); | |
const text = ` | |
# Hello | |
###- How are __you__? | |
Great!`; | |
remark() | |
.use(subtitlePlugin) | |
.use(html) | |
.process(text /* Markdown in */, function (err, file) { | |
if (err) throw err; | |
console.log(String(file)); /* HTML out */ | |
}); | |
}); |