let isWhiteSpace; function trailingSpaceOnly(src, start, max) { let i; for (i = start; i < max; i++) { let code = src.charCodeAt(i); if (code === 0x0a) { return true; } if (!isWhiteSpace(code)) { return false; } } return true; } const ATTR_REGEX = /^\s*=(.+)$|((([a-z0-9]*)\s*)=)([“”"][^“”"]*[“”"]|['][^']*[']|[^"'“”]\S*)/gi; // parse a tag [test a=1 b=2] to a data structure // {tag: "test", attrs={a: "1", b: "2"} export function parseBBCodeTag(src, start, max, multiline) { let i; let tag; let attrs = {}; let closed = false; let length = 0; let closingTag = false; // closing tag if (src.charCodeAt(start + 1) === 47) { closingTag = true; start += 1; } for (i = start + 1; i < max; i++) { let letter = src[i]; if ( !((letter >= "a" && letter <= "z") || (letter >= "A" && letter <= "Z")) ) { break; } } tag = src.slice(start + 1, i); if (!tag) { return; } if (closingTag) { if (src[i] === "]") { if (multiline && !trailingSpaceOnly(src, i + 1, max)) { return; } tag = tag.toLowerCase(); return { tag, length: tag.length + 3, closing: true }; } return; } for (; i < max; i++) { let letter = src[i]; if (letter === "]") { closed = true; break; } } if (closed) { length = i - start + 1; let raw = src.slice(start + tag.length + 1, i); // trivial parser that is going to have to be rewritten at some point if (raw) { let match, key, val; while ((match = ATTR_REGEX.exec(raw))) { if (match[1]) { key = "_default"; } else { key = match[4]; } val = match[1] || match[5]; if (val) { val = val.trim(); val = val.replace(/^["'“”](.*)["'“”]$/, "$1"); attrs[key] = val; } } } if (multiline && !trailingSpaceOnly(src, start + length, max)) { return; } tag = tag.toLowerCase(); return { tag, attrs, length }; } } function findBlockCloseTag(state, openTag, startLine, endLine) { let nesting = 0, line = startLine - 1, start, closeTag, max; for (;;) { line++; if (line >= endLine) { // unclosed bbcode block should not be autoclosed by end of document. return; } start = state.bMarks[line] + state.tShift[line]; max = state.eMarks[line]; if (start < max && state.sCount[line] < state.blkIndent) { // non-empty line with negative indent should stop the list: // - ``` // test break; } // bbcode close [ === 91 if (91 !== state.src.charCodeAt(start)) { continue; } if (state.sCount[line] - state.blkIndent >= 4) { // closing bbcode less than 4 spaces continue; } closeTag = parseBBCodeTag(state.src, start, max, true); if (closeTag && closeTag.closing && closeTag.tag === openTag.tag) { if (nesting === 0) { closeTag.line = line; closeTag.block = true; break; } nesting--; } if (closeTag && !closeTag.closing && closeTag.tag === openTag.tag) { nesting++; } closeTag = null; } return closeTag; } function findInlineCloseTag(state, openTag, start, max) { let closeTag; let possibleTag = false; for (let j = max - 1; j > start; j--) { if (!possibleTag) { if (state.src.charCodeAt(j) === 93 /* ] */) { possibleTag = true; continue; } if (!isWhiteSpace(state.src.charCodeAt(j))) { break; } } else { if (state.src.charCodeAt(j) === 91 /* [ */) { closeTag = parseBBCodeTag(state.src, j, max); if (!closeTag || closeTag.tag !== openTag.tag || !closeTag.closing) { closeTag = null; } else { closeTag.start = j; break; } } } } return closeTag; } function applyBBCode(state, startLine, endLine, silent, md) { let nextLine, oldParent, oldLineMax, rule, start = state.bMarks[startLine] + state.tShift[startLine], initial = start, max = state.eMarks[startLine]; // [ === 91 if (91 !== state.src.charCodeAt(start)) { return false; } let info = parseBBCodeTag(state.src, start, max); if (!info || info.closing) { return false; } let ruleInfo = md.block.bbcode.ruler.getRuleForTag(info.tag); if (!ruleInfo) { return false; } rule = ruleInfo.rule; // Since start is found, we can report success here in validation mode if (silent) { return true; } // Search for the end of the block nextLine = startLine; // We might have a single inline bbcode let closeTag = findInlineCloseTag(state, info, start + info.length, max); if (!closeTag) { if (!trailingSpaceOnly(state.src, start + info.length, max)) { return false; } closeTag = findBlockCloseTag(state, info, nextLine + 1, endLine); } if (!closeTag) { return false; } nextLine = closeTag.line || startLine; oldParent = state.parentType; oldLineMax = state.lineMax; // this will prevent lazy continuations from ever going past our end marker // which can happen if we are parsing a bbcode block state.lineMax = nextLine; if (rule.replace) { let content; if (startLine === nextLine) { content = state.src.slice(start + info.length, closeTag.start); } else { content = state.src.slice( state.bMarks[startLine + 1], state.eMarks[nextLine - 1] ); } if (!rule.replace.call(this, state, info, content)) { return false; } } else { if (rule.before) { rule.before.call( this, state, info, state.src.slice(initial, initial + info.length + 1) ); } let wrapTag; if (rule.wrap) { let token; if (typeof rule.wrap === "function") { token = new state.Token("wrap_bbcode", "div", 1); token.level = state.level + 1; if (!rule.wrap(token, info)) { return false; } state.tokens.push(token); state.level = token.level; wrapTag = token.tag; } else { let split = rule.wrap.split("."); wrapTag = split[0]; let className = split.slice(1).join(" "); token = state.push("wrap_bbcode", wrapTag, 1); if (className) { token.attrs = [["class", className]]; } } } let lastToken = state.tokens[state.tokens.length - 1]; lastToken.map = [startLine, nextLine]; if (closeTag.block) { state.md.block.tokenize(state, startLine + 1, nextLine); } else { let token = state.push("paragraph_open", "p", 1); token.map = [startLine, startLine]; token = state.push("inline", "", 0); token.children = []; token.map = [startLine, startLine]; token.content = state.src.slice(start + info.length, closeTag.start); state.push("paragraph_close", "p", -1); } if (rule.wrap) { state.push("wrap_bbcode", wrapTag, -1); } if (rule.after) { rule.after.call( this, state, lastToken, state.src.slice(start - 2, start + closeTag.length - 1) ); } } state.parentType = oldParent; state.lineMax = oldLineMax; state.line = nextLine + 1; return true; } export function setup(helper) { helper.registerPlugin((md) => { const ruler = md.block.bbcode.ruler; ruler.push("excerpt", { tag: "excerpt", wrap: "div.excerpt", }); ruler.push("code", { tag: "code", replace(state, tagInfo, content) { let token; token = state.push("fence", "code", 0); token.content = content; return true; }, }); isWhiteSpace = md.utils.isWhiteSpace; md.block.ruler.after( "fence", "bbcode", (state, startLine, endLine, silent) => { return applyBBCode(state, startLine, endLine, silent, md); }, { alt: ["paragraph", "reference", "blockquote", "list"] } ); }); }