export class TextPostProcessRuler { constructor() { this.rules = []; } getRules() { return this.rules; } // TODO error handling getMatcher() { if (this.matcher) { return this.matcher; } this.matcherIndex = []; const rules = []; const flags = new Set("g"); this.rules.forEach((r) => { const matcher = r.rule.matcher; rules.push(`(${matcher.source})`); matcher.flags.split("").forEach((f) => flags.add(f)); }); let i; let regexString = ""; let last = 1; // this code is a bit tricky, our matcher may have multiple capture groups // we want to dynamically determine how many for (i = 0; i < rules.length; i++) { this.matcherIndex[i] = last; if (i === rules.length - 1) { break; } if (i > 0) { regexString = regexString + "|"; } regexString = regexString + rules[i]; let regex = new RegExp(regexString + "|(x)"); last = "x".match(regex).length - 1; } this.matcher = new RegExp(rules.join("|"), [...flags].join("")); return this.matcher; } applyRule(buffer, match, state) { let i; for (i = 0; i < this.rules.length; i++) { let index = this.matcherIndex[i]; if (match[index]) { this.rules[i].rule.onMatch( buffer, match.slice(index, this.matcherIndex[i + 1]), state ); break; } } } // TODO validate inputs push(name, rule) { this.rules.push({ name, rule }); this.matcher = null; } } function allowedBoundary(content, index, utils) { let code = content.charCodeAt(index); return ( utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code)) ); } function textPostProcess(content, state, ruler) { let result = null; let match; let pos = 0; const matcher = ruler.getMatcher(); while ((match = matcher.exec(content))) { // something is wrong if (match.index < pos) { break; } // check boundary if (match.index > 0) { if (!allowedBoundary(content, match.index - 1, state.md.utils)) { continue; } } // check forward boundary as well if (match.index + match[0].length < content.length) { if ( !allowedBoundary(content, match.index + match[0].length, state.md.utils) ) { continue; } } result = result || []; if (match.index > pos) { let token = new state.Token("text", "", 0); token.content = content.slice(pos, match.index); result.push(token); } ruler.applyRule(result, match, state); pos = match.index + match[0].length; } if (result && pos < content.length) { let token = new state.Token("text", "", 0); token.content = content.slice(pos); result.push(token); } return result; } export function setup(helper) { helper.registerPlugin((md) => { const ruler = md.core.textPostProcess.ruler; const replacer = (content, state) => textPostProcess(content, state, ruler); md.core.ruler.push("text-post-process", (state) => md.options.discourse.helpers.textReplace(state, replacer, true) ); }); }