You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

160 lines
5.0 KiB

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']);
};