UNPKG

gridsome-plugin-remark-container

Version:
216 lines (190 loc) 6.01 kB
const { element } = require('./element') const { types } = require('./types') // handles different types of whitespace const unified = require('unified') const html = require('rehype-parse') const visit = require('unist-util-visit') const NEWLINE = '\n' // default options for plugin const defaultOptions = { customTypes: [], //custom types here. useDefaultTypes: true, //set to false if you don't want to use default types tag: ':::', icons: 'svg', //can be 'emoji' or 'none' classMaster: 'admonition' //generate admonition-content, admonition-icon, admonition-heading } // override default options const configure = options => { const { customTypes, ...baseOptions } = { ...defaultOptions, ...options } return { ...baseOptions, types: baseOptions.useDefaultTypes ? { ...types, ...customTypes } : { ...customTypes } } } // escape regex special characters function escapeRegExp(s) { return s.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, 'g'), '\\$&') } // helper: recursively replace nodes const _nodes = ({ tagName: hName, properties: hProperties, position, children }) => { return { type: 'admonitionHTML', data: { hName, hProperties }, position, children: children.map(_nodes) } } // convert HTML to MDAST (must be a single root element) const nodes = markup => { return _nodes( unified() .use(html) .parse(markup).children[0].children[1].children[0] ) } // create a simple text node const text = value => { return { type: 'text', value } } // passed to unified.use() // you have to use a named function for access to `this` :( module.exports = function attacher(options) { const config = configure(options) // match to determine if the line is an opening tag const keywords = Object.keys(config.types) .map(escapeRegExp) .join('|') const tag = escapeRegExp(config.tag) const regex = new RegExp(`${tag}(${keywords})(?: *(.*))?\n`) const escapeTag = new RegExp(escapeRegExp(`\\${config.tag}`), 'g') // the tokenizer is called on blocks to determine if there is an admonition present and create tags for it function blockTokenizer(eat, value, silent) { // stop if no match or match does not start at beginning of line const match = regex.exec(value) if (!match || match.index !== 0) return false // if silent return the match if (silent) return true const now = eat.now() const [opening, keyword, title] = match const food = [] const content = [] // consume lines until a closing tag let idx = 0 while ((idx = value.indexOf(NEWLINE)) !== -1) { // grab this line and eat it const next = value.indexOf(NEWLINE, idx + 1) const line = next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1) food.push(line) value = value.slice(idx + 1) // the closing tag is NOT part of the content if (line.startsWith(config.tag)) break content.push(line) } // consume the processed tag and replace escape sequences const contentString = content.join(NEWLINE).replace(escapeTag, config.tag) const add = eat(opening + food.join(NEWLINE)) // create settings const entry = config.types[keyword] const settings = typeof entry === 'string' ? config.types[entry] : entry // parse the content in block mode const exit = this.enterBlock() const contentNodes = element( 'div', `${config.classMaster}-content`, this.tokenizeBlock(contentString, now) ) exit() // parse the title in inline mode const titleNodes = this.tokenizeInline( title || settings.defaultTitle || keyword, now ) // create the nodes for the icon let iconContainerNodes = [] let iconNodes = '' if (settings.svg && settings.emoji) { iconNodes = config.icons === 'svg' ? nodes(settings.svg) : text(settings.emoji) iconContainerNodes = config.icons === 'none' ? [] : [element('div', `${config.classMaster}-icon`, [iconNodes])] } // build the nodes for the full markup let admonition = element( 'div', [`${config.classMaster}`, `${config.classMaster}-${keyword}`], [ element('div', `${config.classMaster}-heading`, [ element('h5', '', iconContainerNodes.concat(titleNodes)) ]), contentNodes ] ) if (settings.customBlock) { admonition = element( (tagName = settings.tagName), (classes = []), (children = settings.children ? eval(settings.children) : [contentNodes]), (properties = settings.properties ? settings.properties : {}) ) } return add(admonition) } // add tokenizer to parser after fenced code blocks const Parser = this.Parser.prototype Parser.blockTokenizers.admonition = blockTokenizer Parser.blockMethods.splice( Parser.blockMethods.indexOf('fencedCode') + 1, 0, 'admonition' ) Parser.interruptParagraph.splice( Parser.interruptParagraph.indexOf('fencedCode') + 1, 0, ['admonition'] ) Parser.interruptList.splice( Parser.interruptList.indexOf('fencedCode') + 1, 0, ['admonition'] ) Parser.interruptBlockquote.splice( Parser.interruptBlockquote.indexOf('fencedCode') + 1, 0, ['admonition'] ) // TODO: add compiler rules for converting back to markdown return function transformer(tree) { // escape everything except admonitionHTML nodes visit( tree, node => { return node.type !== 'admonitionHTML' }, function visitor(node) { if (node.value) node.value = node.value.replace(escapeTag, config.tag) return node } ) } }