|
|
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
|
|
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 newLocal = 'customBlocks';
|
|
|
|
const blockTokenizers = Parser.prototype.blockTokenizers;
|
|
|
|
const blockMethods = Parser.prototype.blockMethods;
|
|
|
|
blockTokenizers.customBlocks = blockTokenizer;
|
|
|
|
blockMethods.splice(parseInt(blockMethods.indexOf(newLocal)) + 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(parseInt(interruptParagraph.indexOf('fencedCode')) + 1, 0, [newLocal]);
|
|
|
|
interruptList.splice(parseInt(interruptList.indexOf('fencedCode')) + 1, 0, ['customBlocks']);
|
|
|
|
interruptBlockquote.splice(parseInt(interruptBlockquote.indexOf('fencedCode')) + 1, 0, [
|
|
|
|
'customBlocks',
|
|
|
|
]);
|
|
|
|
};
|