import grimm from "./grimm.js";
const rubric = {
options: {
default: {
allowMedia: false,
allowRawCode: false,
allowReplies: false,
allowSidenotes: false,
allowTemplates: false,
fullParagraphs: true,
highlightCode: false,
hyperlinkHeadings: false,
minHeadingLevel: 1,
html_smallCapsClasses: { lower: "smallcaps", all: "all-sc" },
customTemplates: {}
},
risky: {
allowRawCode: true,
allowMedia: true,
allowSidenotes: true,
allowTemplates: true,
highlightCode: true,
hyperlinkHeadings: true
},
inlineRisky: {
allowRawCode: true,
inline: true
},
comments: {
allowReplies: true,
minHeadingLevel: 2,
highlightCode: true
}
},
templates: {
"-": {
html: (node, opt, lang) => "­"
}
},
patterns: {
block: {
pre: {
starts: /^``` *(\S*?)$/u,
continues: /.*/,
ends: /^```$/,
parseRule: /^``` *(\S*?)\n(.*)\n?```\s*$/su,
leaf: (opt, match, language, text) => ({
language: language,
text: text
})
},
attributeNext: {
starts: /^(?:[:.#](?:-|_|[^\s\p{P}\p{S}])+)+$/u,
parseRule: /^(?:[:.#](?:-|_|[^\s\p{P}\p{S}])+)+$/u
},
div: {
starts: /^::: *(\S*?(?:[:.#](?:-|_|[^\s\p{P}\p{S}])+)*)?$/u,
continues: /.*/,
ends: /^:::$/,
parseRule:
/^::: *(\S*?(?:[:.#](?:-|_|[^\s\p{P}\p{S}])+)*)?\n(.*)\n:::\s*$/su,
leaf: (opt, match, attrs, text) => ({
attributes: rubric.parseAttrs(attrs, opt),
text: rubric.parseBlocks(
text,
Object.assign({}, opt, { fullParagraphs: false })
)
})
},
quote: {
starts: /^> (.*?)$/u,
continues: /^>( .*?)?$/,
parseRule: /^> (.*?)$/su,
leaf: (opt, match, text) => ({
text: rubric.parseBlocks(text.replace(/^> ?/gm, ""), opt)
})
},
heading: {
starts: /^(#{1,6}) (.*?)$/u,
continues: /^#{1,6}( .*?)?$/,
parseRule: /^(#{1,6}) (.*?)$/su,
leaf: (opt, match, level, text) => ({
level: Math.max(level.length, opt.minHeadingLevel),
text: rubric.parseInline(
text.replace(/^#{1,6} ?/gm, ""),
opt
)
})
},
spoiler: {
starts: /^~{3,} .*?$/u,
continues: /.*/,
ends: /^~{3,}$/,
parseRule: /^~{3,} (.*?)\n(.*?)\n?~{3,}$/su,
nestable: true,
leaf: (opt, match, summary, details) => ({
summary: rubric.parseInline(summary, opt),
text: rubric.parseBlocks(details, opt)
})
},
media: {
starts: /^\[([>:<]?)\[(.*?(\.[a-z0-9]+?)?)((?: :\S+)+)?( .*?)?\]( .*?)?\]$/u,
parseRule:
/^\[([>:<]?)\[(.*?(\.[a-z0-9]+?)?)((?: :\S+)+)?( .*?)?\]( .*?)?\]$/u,
nestable: true,
leaf: (opt, match, align, src, format, flags, alt, caption) => {
const videoFormats = [
".webm",
".mkv",
".mov",
".avi",
".mp4"
];
const audioFormats = [
".alac",
".aac",
".flac",
".ogg",
".mp3",
".wav"
];
let alignment;
switch (align) {
case ">":
alignment = "end";
break;
case "<":
alignment = "start";
break;
case ":":
alignment = "full";
break;
default:
alignment = undefined;
}
return {
format: src.match(/^[a-z-]+\:/)
? "custom"
: videoFormats.includes(format)
? "video"
: audioFormats.includes(format)
? "audio"
: "image",
source: src,
flags:
flags === undefined
? []
: flags.split(" :").slice(1),
alt: alt === undefined ? undefined : alt.trim(),
align: alignment,
isFigure: caption !== undefined,
text:
caption === undefined
? undefined
: rubric.parseInline(caption.trim(), opt)
};
}
},
list: {
starts: /^(-|[0-9]+\.|[a-z]\.|[lxvi]+\.) .*?$/u,
continues: /^(\t+|(-|[0-9]+\.|[a-z]\.|[lxvi]+\.) )/,
parseRule: /^(-|[0-9]+\.|[a-z]\.|[lxvi]+\.) (.*?)$/su,
leaf: (opt, match, type, text) => ({
ordered: type != "-",
style:
type == "-"
? "unordered"
: type.match(/[0-9]/)
? "numbered"
: type.match(/^[abcdefghjklmnopqrstuwxyz]\.$/)
? "lettered"
: "roman",
text: rubric.parseList(`${type} ${text}`, opt)
})
},
sidenoteBody: {
starts: /^\[\^.*?\]: .*?$/u,
continues: /^\t/,
parseRule: /^\[\^(.*?)\]: (.*?)$/su,
leaf: (opt, match, label, contents) => ({
label: label,
text: rubric.parseBlocks(contents.replace(/^\t/gm, ""), opt)
})
},
division: {
starts: /^---+$/u,
parseRule: /^---+$/u,
leaf: (opt, match) => ({
kind: "division"
})
}
},
inline: {
code: {
parse: /`(.+?)`/g,
priority: 10,
leaf: (opt, buffer, match, text) => ({
text: rubric.unbuffer(text, buffer)
})
},
template: {
parse: /{{(\S+?)(?: (.*?))?}}/g,
priority: 11,
leaf: (opt, buffer, match, temp, text) => ({
template: temp,
text: text
? rubric?.templates?.[temp]?.noParse
? rubric.unbuffer(text, buffer)
: rubric.parseInline(
rubric.unbuffer(text, buffer),
opt,
"template",
false
)
: undefined
})
},
span: {
parse: /\{(\S*?(?:[:.#](?:-|_|[^\s\p{P}\p{S}])+)* )?(.*?)\}/gu,
priority: 15,
leaf: (opt, buffer, match, attrs, text) => ({
attributes: rubric.parseAttrs(attrs, opt),
text: rubric.parseInline(
rubric.unbuffer(text, buffer),
opt,
"span",
false
)
})
},
sidenoteNote: {
parse: /\[\^(.*?)\]((?:[:.#](?:-|_|[^\s\p{P}\p{S}])+)+)?/g,
priority: 17,
leaf: (opt, buffer, match, text, attrs) => ({
attributes: rubric.parseAttrs(attrs, opt),
text: rubric.unbuffer(text, buffer)
})
},
link: {
parse: /(\[([^\s⟪⟫]+?) (.+?)\])|(?<!\S)[a-z]+\:\/\/[^\s⟪⟫]+/g,
priority: 20,
leaf: (opt, buffer, match, rich, url, text) => ({
href: rich ? rubric.unescape(url) : rubric.unescape(match),
text: rich
? rubric.parseInline(
rubric.unbuffer(text, buffer),
opt,
"link",
false
)
: rubric.unescape(match)
})
},
superscript: {
parse: /(?<!\^)\^([^\^]+?)\b/g,
priority: 50,
leaf: (opt, buffer, match, text) => ({
text: rubric.parseInline(
rubric.unbuffer(text, buffer),
opt,
"superscript",
false
)
})
},
subscript: {
parse: /(?<!_)_([^_]+?)\b/g,
priority: 50,
leaf: (opt, buffer, match, text) => ({
text: rubric.parseInline(
rubric.unbuffer(text, buffer),
opt,
"subscript",
false
)
})
},
commentReply: {
parse: /@([0-9]+)\b/g,
priority: 100,
leaf: (opt, buffer, match, comment) => ({
commentID: parseInt(comment),
text: `@${comment}`
})
}
},
symmetrical: {
boldStrong: {
token: "\\*+",
leaf: (opt, length, text) => ({
kind: length >= 2 ? "strong" : "bold",
text: text
})
},
italicEmphasis: {
token: "(?<!<)\\/+",
leaf: (opt, length, text) => ({
kind: length >= 2 ? "emphasis" : "italic",
text: text
})
},
smallStrike: {
token: "~+",
leaf: (opt, length, text) => ({
kind: length >= 2 ? "strikethrough" : "small",
text: text
})
},
inserted: {
token: "(?:\\+\\+)+",
leaf: (opt, length, text) => ({
kind: "inserted",
text: text
})
},
deleted: {
token: "(?:--)+",
leaf: (opt, length, text) => ({
kind: "deleted",
text: text
})
},
superscript: {
token: "(?:\\^\\^)+",
priority: 50,
leaf: (opt, length, text) => ({
kind: "superscript",
text: text
})
},
subscript: {
token: "(?:__)+",
priority: 50,
leaf: (opt, length, text) => ({
kind: "subscript",
text: text
})
},
smallCaps: {
token: "(?:\\:\\:)+",
priority: 70,
leaf: (opt, length, text) => ({
kind: "smallCaps",
case: text == text.toUpperCase() ? "all" : "lower",
text: text
})
}
}
},
langs: {
html: {
els: {
pre: (node, opt, lang) =>
`\n<pre${lang.attribute(
node.attributes
)}><code>${lang.escape(node.text)}</code></pre>\n`,
heading: (node, opt, lang) =>
`\n<h${node.level}${lang.attribute(
node.attributes
)}>${lang.parse(node.text, opt)}</h${node.level}>\n`,
spoiler: (node, opt, lang) =>
`\n<details${lang.attribute(
node.attributes
)}>\n<summary>${lang.parse(
node.summary,
opt
)}</summary>\n${lang.parse(node.text, opt)}</details>\n`,
media: (node, opt, lang) => {
let figure = "";
const captions = node.flags
.map(x => x.match(/^(st|cc)\:(.+?)@(.+?)$/))
.filter(x => x)
.map((el, idx) =>
rubric.langs.html.writeCaption(...el, idx == 0)
)
.join("");
const alignment = node?.align
? ` class="align-${node.align}"`
: "";
switch (node.format) {
case "custom":
figure += `<iframe class="rubric-youtube" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen src="https://www.youtube-nocookie.com/embed/${
node.source.match(/:(.*?)$/)[1]
}"></iframe>`;
break;
case "audio":
figure += `<audio controls src="${lang.escapeQuotes(
rubric.langs.html.parseMediaSource(node.source)
)}">${
node.alt ? lang.escape(node.alt) : ""
}${captions}</audio>`;
break;
case "video":
figure += `<video ${
node.flags.includes("gif")
? "autoplay loop muted"
: "controls"
} src="${lang.escapeQuotes(
rubric.langs.html.parseMediaSource(node.source)
)}">${
node.alt ? lang.escape(node.alt) : ""
}${captions}</video>`;
break;
default:
figure += `<img src="${lang.escapeQuotes(
rubric.langs.html.parseMediaSource(node.source)
)}"${
node.alt
? ` alt="${lang.escapeQuotes(node.alt)}"`
: ""
}>`;
}
if (node.isFigure) {
figure = `<figure${lang.attribute(
node.attributes
)}${alignment}>\n${figure}\n<figcaption>${lang.parse(
node.text,
opt
)}</figcaption>\n</figure>`;
} else {
figure = figure.replace(
/<(video|audio|img)/,
`<$1${lang.attribute(node.attributes)}${alignment} `
);
}
return `\n${figure}\n`;
},
list: (node, opt, lang) => {
const parsed = lang.parse(node.text, opt);
switch (node.style) {
case "unordered":
return `\n<ul${lang.attribute(
node.attributes
)}>${parsed}</ul>\n`;
case "numbered":
return `\n<ol type="1"${lang.attribute(
node.attributes
)}>${parsed}</ol>\n`;
case "lettered":
return `\n<ol type="a"${lang.attribute(
node.attributes
)}>${parsed}</ol>\n`;
case "roman":
return `\n<ol type="i"${lang.attribute(
node.attributes
)}>${parsed}</ol>\n`;
default:
return `\n<ol${lang.attribute(
node.attributes
)}>${parsed}</ol>\n`;
}
},
code: (node, opt, lang) =>
`<code${lang.attribute(node.attributes)}>${lang.escape(
node.text
)}</code>`,
commentReply: (node, opt, lang) =>
`<a href="#comment-${node.commentID}"${lang.attribute(
node.attributes
)}>${node.text}</a>`,
link: (node, opt, lang) =>
`<a href="${lang.escapeQuotes(node.href)}"${lang.attribute(
node.attributes
)}>${lang.parse(node.text, opt)}</a>`,
smallCaps: (node, opt, lang) =>
`<span class="${
opt.html_smallCapsClasses?.[node.case]
}"${lang.attribute(node.attributes)}>${lang.parse(
node.text,
opt
)}</span>`,
sidenoteBody: (node, opt, lang) =>
`\n<aside class="note" id="sn-body-${lang.escapeQuotes(
node.label
)}" sn-slug="${lang.escapeQuotes(
node.label
)}"${lang.attribute(
node.attributes
)}>\n<a href="#sn-ref-${lang.escapeQuotes(
node.label
)}" class="note-number">${lang.escapeQuotes(
node.label
)}</a>\n<div>${lang.parse(
node.text,
opt
)}</div>\n</aside>\n`,
sidenoteNote: (node, opt, lang) =>
`<a class="sidenote-ref" id="sn-ref-${lang.escapeQuotes(
node.text
)}" href="#sn-body-${lang.escapeQuotes(
node.text
)}" sn-slug="${lang.escapeQuotes(
node.text
)}"${lang.attribute(
node.attributes
)}><sup class="note">${lang.escape(node.text)}</sup></a>`,
div: (node, opt, lang) =>
rubric.langs.html.element(
node?.attributes?.element ?? "div",
lang.parse(node.text, opt),
node.attributes
),
span: (node, opt, lang) =>
rubric.langs.html.element(
node?.attributes?.element ?? "span",
lang.parse(node.text, opt),
node.attributes
),
division: (node, opt, lang) =>
`<hr${lang.attribute(node.attributes)}>`,
template: (node, opt, lang) =>
node.template in rubric.templates &&
"html" in rubric?.templates?.[node.template]
? rubric.templates[node.template].html(node, opt, lang)
: `{{${node.template} ${lang.parse(node.text, opt)}}}`
},
aliases: {
para: "p",
item: "li",
italic: "i",
emphasis: "em",
bold: "b",
quote: "blockquote",
strikethrough: "s",
subscript: "sub",
superscript: "sup",
inserted: "ins",
deleted: "del",
division: "hr"
},
blockEls: [
"div",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"details",
"summary",
"li",
"ul",
"ol",
"img",
"figure",
"figcaption",
"video",
"audio",
"blockquote",
"p",
"aside",
"main",
"body"
],
attribute: list =>
typeof list != "object"
? ""
: Object.entries(list)
.filter(x => x[0] != "element")
.map(
x =>
` ${x[0]}="${rubric.langs.html.escapeQuotes(
typeof x[1] == "string"
? x[1]
: x[1].join(" ")
)}"`
)
.join(""),
element: (el, text = undefined, attrs = {}) =>
`${
rubric.langs.html.blockEls.includes(el) ? "\n" : ""
}<${el}${rubric.langs.html.attribute(attrs)}>${
text !== undefined && text !== null ? `${text}</${el}>` : ""
}${rubric.langs.html.blockEls.includes(el) ? "\n" : ""}`,
escape: text => text.replace(/</g, "<").replace(/>/g, ">"),
escapeQuotes: text =>
text
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """),
parseMediaSource: src => src,
parse: (tree, options) => {
const lang = rubric.langs.html;
if (typeof tree == "string") {
return options.allowRawCode ? tree : lang.escape(tree);
}
if (!Array.isArray(tree)) {
if (tree.kind in lang.els) {
return lang.els[tree.kind](tree, options, lang);
}
if ("text" in tree) {
return lang.element(
lang.aliases?.[tree.kind] ?? tree.kind,
lang.parse(tree.text, options),
tree.attributes
);
}
return lang.element(tree.kind, null, tree.attributes);
}
return tree.map(x => lang.parse(x, options)).join("");
},
writeCaption: (raw, type, lang, url, isDefault) => {
const grimmHasLang = !!grimm?.langs?.[lang];
return (
'<track kind="' +
(type == "cc" ? "captions" : "subtitles") +
'" srclang="' +
(grimmHasLang ? grimm.langs[lang].long : lang) +
'" label="' +
(grimmHasLang
? grimm.langs[lang].name
: new Intl.DisplayNames([lang], {
type: "language"
}).of(lang)) +
'" src="' +
rubric.langs.html.parseMediaSource(url) +
'"' +
(isDefault ? " default" : "") +
"/>"
);
}
}
},
escape: (text, backslashes = false) => {
let escapedText = text.replace(
/[⟨⟩⟪⟫]/g,
match => `⟨${match.charCodeAt(0)}⟩`
);
if (backslashes) {
escapedText = escapedText.replace(
/\\([\p{P}\p{S}])/gu,
match => `⟨${match.charCodeAt(1)}⟩`
);
}
return escapedText;
},
unescape: text =>
text.replace(/⟨([0-9]+)⟩/g, (match, code) => String.fromCharCode(code)),
unbuffer: (text, buffer) => {
const unbuffered = text
.split(/(⟪[0-9]+⟫)/g)
.map(x =>
x.match(/(⟪[0-9]+⟫)/)
? buffer[parseInt(x.slice(1, -1))]
: rubric.unescape(x)
);
return unbuffered.length == 1 ? unbuffered[0] : unbuffered;
},
parseAttrs: (text, options) => {
let attrs = { class: [] };
if ((text === undefined) | (text === null) | (text === "")) {
return {};
}
const splitArray = text
.trim()
.split(/([\.\:#][^\.\:#]*)/g)
.filter(x => x != "");
for (const attr of splitArray) {
switch (attr[0]) {
case ".":
attrs.class.push(attr.slice(1));
break;
case "#":
attrs.id = attr.slice(1);
break;
case ":":
attrs.lang = attr.slice(1);
break;
default:
attrs.element = attr;
}
}
if (attrs.class.length == 0) {
delete attrs.class;
}
if (!options.allowRawCode) {
return attrs.lang ? { lang: attrs.lang } : {};
}
return attrs;
},
parseInline: (text, options, startsFrom = undefined, escape = true) => {
const inlinePatterns = Object.entries(rubric.patterns.inline)
.filter(
x =>
(options.allowSidenotes || x[0] != "sidenoteNote") &&
(options.allowReplies || x[0] != "commentReply")
)
.map(entry => Object.assign(entry[1], { kind: entry[0] }))
.sort((a, b) =>
a.priority < b.priority ? -1 : a.priority > b.priority ? 1 : 0
);
if (startsFrom) {
while (inlinePatterns[0].kind != startsFrom) {
inlinePatterns.shift();
}
}
let leafBuffer = [];
let smooshedText = "";
if (typeof text == "object") {
for (const node of text) {
if (typeof node == "string") {
smooshedText += rubric.escape(node);
} else {
smooshedText += `⟪${leafBuffer.push(node) - 1}⟫`;
}
}
text = smooshedText;
}
if (escape) {
text = rubric.escape(text, true);
}
for (const pattern of inlinePatterns) {
text = text.replace(
pattern.parse,
(...args) =>
`⟪${
leafBuffer.push(
Object.assign(
pattern.leaf(options, leafBuffer, ...args),
{
kind: pattern.kind
}
)
) - 1
}⟫`
);
}
const symmetricalPatterns = Object.values(rubric.patterns.symmetrical);
const tokens = symmetricalPatterns.map(x => x.token);
const splitRegex = new RegExp(`(${tokens.join("|")}|⟪[0-9]+⟫)`, `g`);
const matchRegex = new RegExp(`^(${tokens.join(")|(")})$`);
const startsWithSpace = /^\s/;
const endsWithSpace = /\s$/;
let nodes = text.split(splitRegex);
let delimiters = [];
const smooshNodeList = list => {
list = list.filter(x => x !== null);
let hungryNode = null;
for (let i = 0; i < list.length; i++) {
if (typeof list[i] != "string") {
hungryNode = null;
continue;
}
if (hungryNode === null) {
hungryNode = i;
continue;
}
list[hungryNode] += list[i];
list[i] = null;
}
const filteredList = list.filter(x => x !== null);
return filteredList.length > 1 ? filteredList : filteredList[0];
};
nodes.forEach((node, i) => {
if (node.match(/⟪[0-9]+⟫/)) {
nodes[i] = leafBuffer[parseInt(node.slice(1, -1))];
return;
}
if (!node.match(matchRegex)) {
nodes[i] = rubric.unescape(node);
return;
}
const spaceLeft =
i == 0 ? true : nodes[i - 1].match(endsWithSpace) !== null;
const spaceRight =
i + 1 == nodes.length
? true
: nodes[i + 1].match(startsWithSpace) !== null;
const noSpace = !spaceLeft && !spaceRight;
if (spaceLeft && spaceRight) {
return;
}
delimiters.push({
position: i,
kind: node.match(matchRegex).indexOf(node, 1) - 1,
length: node.length,
active: true,
canStart: noSpace ? true : spaceLeft,
canEnd: noSpace ? true : spaceRight
});
});
while (delimiters.length > 0) {
if (delimiters[0].canEnd && !delimiters[0].canStart) {
delimiters.shift();
continue;
}
let closer = null;
let closerIndex = null;
for (let i = 1; i < delimiters.length; i++) {
if (delimiters[i].canEnd) {
closer = delimiters[i];
closerIndex = i;
break;
}
}
if (closer === null) {
delimiters = [];
break;
}
let opener = null;
let openerIndex = null;
for (let j = closerIndex - 1; j >= 0; j--) {
if (
delimiters[j].canStart &&
delimiters[j].kind == closer.kind
) {
opener = delimiters[j];
openerIndex = j;
break;
}
}
if (opener === null) {
if (closerIndex == delimiters.length) {
delimiters.pop();
}
if (!closer.canStart) {
delimiters = delimiters.filter(x => x != closer);
continue;
}
delimiters[closerIndex].canEnd = false;
continue;
}
const delimiterLength = Math.min(opener.length, closer.length, 2);
opener.length -= delimiterLength;
closer.length -= delimiterLength;
nodes[opener.position] =
nodes[opener.position].slice(delimiterLength) || null;
nodes[closer.position] =
nodes[closer.position].slice(delimiterLength) || null;
nodes[opener.position + 1] = symmetricalPatterns[opener.kind].leaf(
options,
delimiterLength,
smooshNodeList(
nodes.slice(opener.position + 1, closer.position)
)
);
nodes = nodes.map((node, nodeIdx) =>
nodeIdx <= opener.position + 1 || nodeIdx >= closer.position
? node
: null
);
delimiters = delimiters.filter(
(delimiter, delimiterIdx) =>
(delimiterIdx >= closerIndex ||
delimiterIdx <= openerIndex) &&
delimiter.length
);
}
return smooshNodeList(nodes);
},
parseList: (list, options) => {
return list.split(/\n(?!\t)/g).map(li => ({
kind: "item",
text: rubric.parseBlocks(
li.replace(/^.*? /, "").replace(/\n\t/g, "\n"),
Object.assign({}, options, { fullParagraphs: false })
)
}));
},
parseBlocks: (text, options = {}) => {
const lines = text.split(/(?:\n|\r|\r\n|\n\r)/g);
const blocks = rubric.patterns.block;
let allowedBlocks = Object.keys(blocks).filter(
x =>
(options.allowMedia || x != "media") &&
(options.allowSidenotes || x != "sidenoteBody") &&
(options.allowRawCode || x != "div")
);
let context = null;
let nestContext = 0;
let contextBuffer = [];
let documentTree = [];
let nextAttributes = null;
const pushToTree = () => {
if (context) {
let toSend = [contextBuffer.join("\n")];
if ("parseRule" in blocks[context]) {
toSend = toSend[0].match(blocks[context].parseRule);
}
documentTree.push(
Object.assign(
blocks[context].leaf(
Object.assign({}, options, {
fullParagraphs: true
}),
...toSend
),
{
kind: context,
attributes: nextAttributes || undefined
}
)
);
} else {
if (
contextBuffer.length == 1 &&
options.fullParagraphs == false
) {
documentTree.push(
rubric.parseInline(contextBuffer[0].trim(), options)
);
} else {
documentTree.push(
...contextBuffer
.join("\n")
.trim()
.split(/\n{2,}/g)
.map(x => x.trim())
.filter(x => x != "")
.map((x, idx) => ({
kind: "para",
attributes:
idx == 0
? nextAttributes || undefined
: undefined,
text: rubric.parseInline(x, options)
}))
);
}
}
context = null;
contextBuffer = [];
nextAttributes = null;
};
lines.forEach((line, index) => {
if (context) {
const pattern = blocks[context];
if ("ends" in pattern && line.match(pattern.ends)) {
contextBuffer.push(line);
if (nestContext == 0) {
pushToTree();
} else {
nestContext--;
}
return;
} else if (
"continues" in pattern &&
line.match(pattern.continues)
) {
contextBuffer.push(line);
if (pattern?.nestable && line.match(pattern.starts)) {
nestContext++;
}
if (index + 1 == lines.length) {
pushToTree();
}
return;
} else {
pushToTree();
}
}
for (const tag of allowedBlocks) {
if (line.match(blocks[tag].starts)) {
if (!context && contextBuffer.length > 0) {
pushToTree();
}
if (tag == "attributeNext") {
nextAttributes = rubric.parseAttrs(line, options);
return;
}
context = tag;
contextBuffer.push(line);
break;
}
}
if (!context) {
contextBuffer.push(line);
}
if (index + 1 == lines.length) {
pushToTree();
}
});
return documentTree;
},
parse: (text, target = "html", options = {}) => {
options = Object.assign({}, rubric.options.default, options);
if (options.customTemplates) {
rubric.templates = Object.assign(
rubric.templates,
options.customTemplates
);
}
if (!(target in rubric.langs)) {
return undefined;
}
return rubric.langs[target].parse(
options.inline
? rubric.parseInline(text, options)
: rubric.parseBlocks(text, options),
options
);
}
};
export default rubric;