#- 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 */ | |
| }); | |
| }); |