discourse/app/assets/javascripts/pretty-text/engines/discourse-markdown/text-post-process.js.es6

142 lines
3.1 KiB
JavaScript

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