const spaceSeparated = require('space-separated-tokens'); function escapeRegExp(str) { return str.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, 'g'), '\\$&'); } const C_NEWLINE = '\n'; const C_FENCE = '|'; function compilerFactory(nodeType) { let text; let title; return { blockHeading(node) { title = this.all(node).join(''); return ''; }, blockBody(node) { text = this.all(node) .map(s => s.replace(/\n/g, '\n| ')) .join('\n|\n| '); return text; }, block(node) { text = ''; title = ''; this.all(node); if (title) { return `[[${nodeType} | ${title}]]\n| ${text}`; } else { return `[[${nodeType}]]\n| ${text}`; } }, }; } module.exports = function blockPlugin(availableBlocks = {}) { const pattern = Object.keys(availableBlocks).map(escapeRegExp).join('|'); if (!pattern) { throw new Error('remark-custom-blocks needs to be passed a configuration object as option'); } const regex = new RegExp(`\\[\@(${pattern})(?: *\\| *(.*))?\]\n`); function blockTokenizer(eat, value, silent) { const now = eat.now(); const keep = regex.exec(value); if (!keep) return; if (keep.index !== 0) return; const [eaten, blockType, blockTitle] = keep; /* istanbul ignore if - never used (yet) */ if (silent) return true; const linesToEat = []; const content = []; let idx = 0; while ((idx = value.indexOf(C_NEWLINE)) !== -1) { const next = value.indexOf(C_NEWLINE, idx + 1); // either slice until next NEWLINE or slice until end of string const lineToEat = next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1); if (lineToEat[0] !== C_FENCE) break; // remove leading `FENCE ` or leading `FENCE` const line = lineToEat.slice(lineToEat.startsWith(`${C_FENCE} `) ? 2 : 1); linesToEat.push(lineToEat); content.push(line); value = value.slice(idx + 1); } const contentString = content.join(C_NEWLINE); const stringToEat = eaten + linesToEat.join(C_NEWLINE); const potentialBlock = availableBlocks[blockType]; const titleAllowed = potentialBlock.title && ['optional', 'required'].includes(potentialBlock.title); const titleRequired = potentialBlock.title && potentialBlock.title === 'required'; if (titleRequired && !blockTitle) return; if (!titleAllowed && blockTitle) return; const add = eat(stringToEat); if (potentialBlock.details) { potentialBlock.containerElement = 'details'; potentialBlock.titleElement = 'summary'; } const exit = this.enterBlock(); const contents = { type: `${blockType}CustomBlockBody`, data: { hName: potentialBlock.contentsElement ? potentialBlock.contentsElement : 'div', hProperties: { className: 'custom-block-body', }, }, children: this.tokenizeBlock(contentString, now), }; exit(); const blockChildren = [contents]; if (titleAllowed && blockTitle) { const titleElement = potentialBlock.titleElement ? potentialBlock.titleElement : 'div'; const titleNode = { type: `${blockType}CustomBlockHeading`, data: { hName: titleElement, hProperties: { className: 'custom-block-heading', }, }, children: this.tokenizeInline(blockTitle, now), }; blockChildren.unshift(titleNode); } const classList = spaceSeparated.parse(potentialBlock.classes || ''); return add({ type: `${blockType}CustomBlock`, children: blockChildren, data: { hName: potentialBlock.containerElement ? potentialBlock.containerElement : 'div', hProperties: { className: ['custom-block', ...classList], }, }, }); } const Parser = this.Parser; // Inject blockTokenizer const blockTokenizers = Parser.prototype.blockTokenizers; const blockMethods = Parser.prototype.blockMethods; blockTokenizers.customBlocks = blockTokenizer; blockMethods.splice(blockMethods.indexOf('fencedCode') + 1, 0, 'customBlocks'); const Compiler = this.Compiler; if (Compiler) { const visitors = Compiler.prototype.visitors; if (!visitors) return; Object.keys(availableBlocks).forEach(key => { const compiler = compilerFactory(key); visitors[`${key}CustomBlock`] = compiler.block; visitors[`${key}CustomBlockHeading`] = compiler.blockHeading; visitors[`${key}CustomBlockBody`] = compiler.blockBody; }); } // Inject into interrupt rules const interruptParagraph = Parser.prototype.interruptParagraph; const interruptList = Parser.prototype.interruptList; const interruptBlockquote = Parser.prototype.interruptBlockquote; interruptParagraph.splice(interruptParagraph.indexOf('fencedCode') + 1, 0, ['customBlocks']); interruptList.splice(interruptList.indexOf('fencedCode') + 1, 0, ['customBlocks']); interruptBlockquote.splice(interruptBlockquote.indexOf('fencedCode') + 1, 0, ['customBlocks']); };