================================================
FILE: Public/index.js
================================================
"use strict";
import "./scss/default.scss";
import "codemirror/lib/codemirror.css";
import "tippy.js/dist/tippy.css";
import "./css/common.css";
import "./css/highlight.css";
import "./js/misc/icons";
import "bootstrap";
import { App } from "./js/app";
new App();
================================================
FILE: Public/js/app.js
================================================
"use strict";
import { Tooltip } from "bootstrap";
import { ExpressionField } from "./views/expression_field";
import { MatchOptions } from "./views/match_options";
import { TestEditor } from "./views/test_editor";
import { DSLView } from "./views/dsl_view";
import { DSLEditor } from "./views/dsl_editor";
import { DebuggerText } from "./views/debugger_text";
import { Runner } from "./runner";
export class App {
constructor() {
this.init();
}
init() {
[].slice
.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map((trigger) => {
return new Tooltip(trigger);
});
this.expressionField = new ExpressionField(
document.getElementById("expression-field-container"),
);
this.expressionField.addEventListener("change", () =>
this.onExpressionFieldChange(),
);
this.matchOptions = new MatchOptions();
this.matchOptions.addEventListener("change", () =>
this.onExpressionFieldChange(),
);
this.patternTestEditor = new TestEditor(
document.querySelector(".test-editor-container"),
);
this.patternTestEditor.addEventListener("change", () =>
this.onPatternTestEditorChange(),
);
this.debuggerText = new DebuggerText(
document.getElementById("debugger-text-container"),
);
this.debuggerGoStartButton = document.getElementById("debugger-go-start");
this.debuggerGoStartButton.addEventListener("click", () => {
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.value = 1;
this.onDebuggerStepChange();
});
this.debuggerStepBackwardButton = document.getElementById(
"debugger-step-backward",
);
this.debuggerStepBackwardButton.addEventListener("click", () => {
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.value = Math.max(1, parseInt(matchStepRange.value) - 1);
this.onDebuggerStepChange();
});
this.debuggerStepForwardButton = document.getElementById(
"debugger-step-forward",
);
this.debuggerStepForwardButton.addEventListener("click", () => {
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.value = Math.min(
parseInt(matchStepRange.value) + 1,
parseInt(matchStepRange.max),
);
this.onDebuggerStepChange();
});
this.debuggerGoEndButton = document.getElementById("debugger-go-end");
this.debuggerGoEndButton.addEventListener("click", () => {
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.value = matchStepRange.max;
this.onDebuggerStepChange();
});
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.addEventListener("input", () => {
this.onDebuggerStepChange();
});
this.debuggerModal = document.getElementById("debugger-modal");
this.debuggerModal.addEventListener("shown.bs.modal", () =>
this.launchDebugger(),
);
this.dslView = new DSLView(document.getElementById("dsl-view-container"));
this.runner = new Runner();
this.runner.onready = this.onRunnerReady.bind(this);
this.runner.onresponse = this.onRunnerResponse.bind(this);
this.stateProxy = {
builder: "",
text2: "",
};
if (window.Worker) {
this.stateRestorationWorker = new Worker(
new URL("./state/worker.js", import.meta.url),
);
if (window.location.search) {
this.decodeState();
} else {
this.expressionField.setDefaultValue();
this.patternTestEditor.setDefaultValue();
this.stateProxy.builder = DSLEditor.defaultValue;
this.stateProxy.text2 = TestEditor.defaultValue;
}
this.startStateRestoration();
}
}
startStateRestoration() {
if (!this.stateRestorationWorker) {
return;
}
const debounce = (() => {
const timers = {};
return function (callback, delay, id) {
delay = delay || 400;
id = id || "duplicated event";
if (timers[id]) {
clearTimeout(timers[id]);
}
timers[id] = setTimeout(callback, delay);
};
})();
this.stateRestorationWorker.onmessage = (e) => {
if (e.data && e.data.type === "encode") {
debounce(
() => {
history.replaceState(null, "", e.data.value);
},
400,
"update_location",
);
}
if (e.data && e.data.type === "decode") {
const expressionField = this.expressionField;
const matchOptions = this.matchOptions;
const patternTestEditor = this.patternTestEditor;
if (expressionField) {
expressionField.value = e.data.value.pattern;
}
if (matchOptions) {
matchOptions.value = e.data.value.options;
}
if (patternTestEditor) {
patternTestEditor.value = e.data.value.text1;
}
}
};
}
encodeState() {
if (!this.stateRestorationWorker) {
return;
}
const expressionField = this.expressionField;
const matchOptions = this.matchOptions;
const patternTestEditor = this.patternTestEditor;
this.stateRestorationWorker.postMessage({
type: "encode",
value: {
pattern: expressionField ? expressionField.value : "",
options: matchOptions ? matchOptions.value : [],
text1: patternTestEditor ? patternTestEditor.value : "",
},
});
}
decodeState() {
if (!this.stateRestorationWorker) {
return;
}
this.stateRestorationWorker.postMessage({
type: "decode",
value: window.location.search,
});
}
updateMatchCount(count, id) {
const matchCount = document.getElementById(id);
if (count > 1) {
matchCount.textContent = `${count} matches`;
} else if (count > 0) {
matchCount.textContent = "1 match";
} else {
matchCount.textContent = "no match";
}
}
launchDebugger() {
const expressionField = this.expressionField;
const patternTestEditor = this.patternTestEditor;
const expression = expressionField.value;
const text = patternTestEditor.value;
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.value = 1;
matchStepRange.min = 1;
const matchStep = document.getElementById("debugger-match-step");
matchStep.textContent = "1";
const debuggerPattern = document.getElementById("debugger-regex");
debuggerPattern.value = expression;
this.debuggerText.value = text;
this.onDebuggerStepChange();
}
onExpressionFieldChange() {
if (!this.expressionField.value) {
this.expressionField.tokens = [];
this.expressionField.error = null;
this.dslView.value = "";
this.dslView.error = null;
this.updateMatchCount(0, "match-count");
return;
}
this.run();
this.encodeState();
}
run() {
const methods = ["parseExpression", "convertToDSL", "match"];
const params = {
pattern: this.expressionField.value,
text: this.patternTestEditor.value,
matchOptions: this.matchOptions.value,
};
if (this.runner.isReady) {
for (const method of methods) {
this.runner.run({
method: method,
...params,
});
}
} else {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
for (const method of methods) {
const body = JSON.stringify({
method: method,
...params,
});
fetch(`/api/rest/${method}`, { method: "POST", headers, body })
.then((response) => {
return response.json();
})
.then((response) => {
this.onRunnerResponse(response);
});
}
}
}
onMatchOptionsChange() {
this.onPatternTestEditorChange();
}
onPatternTestEditorChange() {
const method = "match";
const params = {
method,
pattern: this.expressionField.value,
text: this.patternTestEditor.value,
matchOptions: this.matchOptions.value,
};
if (this.runner.isReady) {
this.runner.run(params);
} else {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
const body = JSON.stringify(params);
fetch(`/api/rest/${method}`, { method: "POST", headers, body })
.then((response) => {
return response.json();
})
.then((response) => {
this.onRunnerResponse(response);
});
}
this.encodeState();
}
onDebuggerStepChange() {
const method = "debug";
const params = {
method,
pattern: document.getElementById("debugger-regex").value,
text: this.debuggerText.value,
matchOptions: this.matchOptions.value,
step: document.getElementById("debugger-step-range").value,
};
if (this.runner.isReady) {
this.runner.run(params);
} else {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
const body = JSON.stringify(params);
fetch(`/api/rest/${method}`, { method: "POST", headers, body })
.then((response) => {
return response.json();
})
.then((response) => {
this.onRunnerResponse(response);
});
}
}
onRunnerReady() {
const value = this.expressionField.value;
if (value) {
this.onExpressionFieldChange();
}
}
onRunnerResponse(response) {
switch (response.method) {
case "parseExpression":
if (response.result) {
const tokens = JSON.parse(response.result);
this.expressionField.tokens = tokens;
} else {
this.expressionField.tokens = [];
}
if (response.error) {
try {
const error = JSON.parse(response.error);
if (error) {
this.expressionField.error = error;
}
} catch (e) {
this.expressionField.error = response.error;
}
} else {
this.expressionField.error = null;
}
break;
case "convertToDSL":
if (response.result) {
this.dslView.value = JSON.parse(response.result);
}
if (response.error) {
try {
const error = JSON.parse(response.error);
if (error) {
this.dslView.error = error;
}
} catch (e) {
this.dslView.error = response.error;
}
} else {
this.dslView.error = null;
}
break;
case "match":
const debuggerButton = document.getElementById("debugger-button");
if (response.result) {
const matches = JSON.parse(response.result);
this.patternTestEditor.matches = matches;
this.updateMatchCount(matches.length, "match-count");
debuggerButton.disabled = matches.length === 0;
} else {
this.patternTestEditor.matches = [];
this.updateMatchCount(0, "match-count");
debuggerButton.disabled = true;
}
this.patternTestEditor.error = response.error;
break;
case "debug":
if (response.result) {
const metrics = JSON.parse(response.result);
const matchStep = document.getElementById("debugger-match-step");
matchStep.textContent = metrics.step;
const matchStepRange = document.getElementById("debugger-step-range");
matchStepRange.max = metrics.stepCount;
const matchStepRangeMax = document.getElementById(
"debugger-step-range-max",
);
matchStepRangeMax.textContent = metrics.stepCount;
const instructions = document.getElementById("debugger-instructions");
instructions.innerHTML = "";
metrics.instructions.forEach((instruction, i) => {
const tr = document.createElement("tr");
if (i === metrics.programCounter) {
tr.classList.add("table-primary");
}
const programCounter = document.createElement("td");
programCounter.style =
"width: 1%; text-align: right; white-space: nowrap; padding-left: 1em; padding-right: 1em;";
programCounter.textContent = i + 1;
tr.appendChild(programCounter);
const inst = document.createElement("td");
inst.style = "white-space: nowrap;";
inst.textContent = instruction;
tr.appendChild(inst);
instructions.appendChild(tr);
});
const totalCycleCount = document.getElementById(
"debugger-total-cycle-count",
);
totalCycleCount.textContent = metrics.totalCycleCount;
const resets = document.getElementById("debugger-resets");
resets.textContent = metrics.resets;
const backtracks = document.getElementById("debugger-backtracks");
const previousBacktracks = Number(backtracks.textContent);
backtracks.textContent = metrics.backtracks;
this.debuggerText.highlighter.draw(
metrics.traces,
previousBacktracks < metrics.backtracks ? metrics.failure : null,
);
}
break;
}
}
}
================================================
FILE: Public/js/docs/reference.js
================================================
"use strict";
export class Reference {
static get(category, key) {
return references[category][key];
}
}
const references = {
charclasses: {
label: "Character classes",
desc: "Character classes match a character from a specific set. There are a number of predefined character classes and you can also define your own sets.",
set: {
title: "Character set",
detail: "Match any character in the set.",
},
setnot: {
title: "Negated set",
detail: "Match any character that is not in the set.",
},
range: {
title: "Range",
detail:
"Matches a character in the range {{getChar(prev)}} to {{getChar(next)}} (char code {{prev.code}} to {{next.code}}). {{getInsensitive()}}",
},
posixcharclass: {
title: "POSIX class",
detail:
"Matches any character in the specified POSIX class. Must be in a character set. For example, [[:alnum:]$] will match alphanumeric characters and $.",
},
dot: {
title: "Dot",
detail: "Matches any character {{getDotAll()}}.",
},
matchanyset: {
title: "Match any",
detail:
"A character set that can be used to match any character, including line breaks, without the dotall flag (s)." +
"
An alternative is [^], but it is not supported in all browsers.
",
},
unicodegrapheme: {
title: "Unicode grapheme",
detail: "Matches any single unicode grapheme (ie. character).",
},
word: {
title: "Word",
detail: "Matches any word character (alphanumeric & underscore).",
},
notword: {
title: "Not word",
detail:
"Matches any character that is not a word character (alphanumeric & underscore).",
},
digit: {
title: "Digit",
detail: "Matches any digit character (0-9).",
},
notdigit: {
title: "Not digit",
detail: "Matches any character that is not a digit character (0-9).",
},
whitespace: {
title: "Whitespace",
detail: "Matches any whitespace character (spaces, tabs, line breaks).",
},
notwhitespace: {
title: "Not whitespace",
detail:
"Matches any character that is not a whitespace character (spaces, tabs, line breaks).",
},
hwhitespace: {
title: "Horizontal whitespace",
detail: "Matches any horizontal whitespace character (spaces, tabs).",
},
nothwhitespace: {
title: "Not horizontal whitespace",
detail:
"Matches any character that is not a horizontal whitespace character (spaces, tabs).",
},
vwhitespace: {
title: "Vertical whitespace",
detail: "Matches any vertical whitespace character (line breaks).",
},
notvwhitespace: {
title: "Not vertical whitespace",
detail:
"Matches any character that is not a vertical whitespace character (line breaks).",
},
linebreak: {
title: "Line break",
detail:
"Matches any line break character, including the CRLF pair, and CR / LF individually.",
},
notlinebreak: {
title: "Not line break",
detail: "Matches any character that is not a line break.",
},
unicodecat: {
title: "Unicode category",
detail:
"Matches any character in the '{{getUniCat()}}' unicode category.",
},
notunicodecat: {
title: "Not unicode category",
detail:
"Matches any character that is not in the '{{getUniCat()}}' unicode category.",
},
unicodescript: {
title: "Unicode script",
detail: "Matches any character in the '{{value}}' unicode script.",
},
notunicodescript: {
title: "Not unicode script",
detail:
"Matches any character that is not in the '{{value}}' unicode script.",
},
binary: {
title: "Unicode property escapes",
detail:
"Allows for matching characters based on their Unicode properties.",
},
script: {
title: "Script",
detail: "No overview available.",
},
scriptextension: {
title: "Script extension",
detail: "No overview available.",
},
named: {
title: "Named",
detail: "No overview available.",
},
numerictype: {
title: "Numeric type",
detail: "No overview available.",
},
numericvalue: {
title: "Numeric value",
detail: "No overview available.",
},
mapping: {
title: "Mapping",
detail: "No overview available.",
},
ccc: {
title: "Custom character class",
detail: "No overview available.",
},
age: {
title: "Age",
detail: "No overview available.",
},
block: {
title: "Block",
detail: "No overview available.",
},
pcrespecial: {
title: "PCRE special",
detail: "No overview available.",
},
javaspecial: {
title: "Java special",
detail: "No overview available.",
},
graphemecluster: {
title: "Grapheme cluster",
detail: "No overview available.",
},
trueanychar: {
title: "Any character",
detail: "Equivalent to (?m:.)",
},
textsegment: {
title: "Text segment",
detail: `Equivalent to (?>\O(?:\Y\O)*)`,
},
nottextsegment: {
title: "Not text segment",
detail: "Text segment non-boundary",
},
keyboardcontrol: {
title: "Control char",
detail: "No overview available.",
},
keyboardmeta: {
title: "Meta",
detail: "No overview available.",
},
keyboardmetacontrol: {
title: "Meta control char",
detail: "No overview available.",
},
namedcharacter: {
title: "Named character",
detail: "No overview available.",
},
subpattern: {
title: "Subpattern",
detail: "No overview available.",
},
callout: {
title: "Callout",
detail: "No overview available.",
},
accept: {
title: "Backtracking control",
detail: `This verb causes the match to end successfully, skipping the remainder of the pattern. When inside a recursion, only the innermost pattern is ended immediately.`,
},
fail: {
title: "Backtracking control",
detail: `This verb causes the match to fail, forcing backtracking to occur. It is equivalent to (?!) but easier to read. The Perl documentation notes that it is probably useful only when combined with (?{}) or (??{}).`,
},
mark: {
title: "Backtracking control",
detail: "No overview available.",
},
commit: {
title: "Backtracking control",
detail: `This verb causes the whole match to fail outright if the rest of the pattern does not match. Even if the pattern is unanchored, no further attempts to find a match by advancing the start point take place. Once (*COMMIT) has been passed, re:run/3 is committed to finding a match at the current starting point, or not at all.`,
},
prune: {
title: "Backtracking control",
detail: `acktracking cannot cross (*PRUNE). In simple cases, the use of (*PRUNE) is just an alternative to an atomic group or possessive quantifier, but there are some uses of (*PRUNE) that cannot be expressed in any other way.`,
},
skip: {
title: "Backtracking control",
detail: `This verb is like (*PRUNE), except that if the pattern is unanchored, the "bumpalong" advance is not to the next character, but to the position in the subject where (*SKIP) was encountered. (*SKIP) signifies that whatever text was matched leading up to it cannot be part of a successful match.`,
},
then: {
title: "Backtracking control",
detail: `This verb causes a skip to the next alternation if the rest of the pattern does not match. That is, it cancels pending backtracking, but only within the current alternation.`,
},
},
anchors: {
label: "Anchors",
desc: "Anchors are unique in that they match a position within a string, not a character.",
bos: {
title: "Beginning of string",
detail: "Matches the beginning of the string.",
},
eos: {
title: "End of string",
detail: "Matches the end of the string.",
},
abseos: {
title: "Strict end of string",
detail:
"Matches the end of the string. Unlike $ or \\Z, it does not allow for a trailing newline.",
},
bof: {
title: "Beginning",
detail:
"Matches the beginning of the string, or the beginning of a line if the multiline flag (m) is enabled.",
},
eof: {
title: "End",
detail:
"Matches the end of the string, or the end of a line if the multiline flag (m) is enabled.",
},
wordboundary: {
title: "Word boundary",
detail:
"Matches a word boundary position between a word character and non-word character or position (start / end of string).",
},
notwordboundary: {
title: "Not word boundary",
detail: "Matches any position that is not a word boundary.",
},
prevmatchend: {
title: "Previous match end",
detail: "Matches the end position of the previous match.",
},
invalid: {
title: "Invalid character class",
detail: "No overview available.",
},
},
escchars: {
label: "Escaped characters",
desc: "Escape sequences can be used to insert reserved, special, and unicode characters. All escaped characters begin with the \\ character.",
reservedchar: {
title: "Reserved characters",
detail:
"The following character have special meaning, and should be preceded by a \\ (backslash) to represent a literal character:" +
"
{{getEscChars()}}
" +
"
Within a character set, only \\, -, and ] need to be escaped.
",
},
escoctal: {
title: "Octal escape",
detail: "Octal escaped character in the form \\000.",
},
eschexadecimal: {
title: "Hexadecimal escape",
detail: "Hexadecimal escaped character in the form \\xFF.",
},
escunicodeu: {
title: "Unicode escape",
detail: "Unicode escaped character in the form \\uFFFF",
},
escunicodeub: {
title: "Extended unicode escape",
detail: "Unicode escaped character in the form \\u{FFFF}.",
},
escunicodexb: {
title: "Unicode escape",
detail: "Unicode escaped character in the form \\x{FF}.",
},
esccontrolchar: {
title: "Control character escape",
detail: "Escaped control character in the form \\cZ.",
},
escsequence: {
title: "Escape sequence",
detail: "Matches the literal string '{{value}}'.",
},
},
groups: {
label: "Groups & References",
desc: "Groups allow you to combine a sequence of tokens to operate on them together. Capture groups can be referenced by a backreference and accessed separately in the results.",
group: {
title: "Capturing group #{{group.num}}",
detail:
"Groups multiple tokens together and creates a capture group for extracting a substring or using a backreference.",
},
namedgroup: {
title: "Named capturing group",
detail: "Creates a capturing group named '{{name}}'.",
},
namedref: {
title: "Named reference",
detail:
"Matches the results of the capture group named '{{group.name}}'.",
},
numref: {
title: "Numeric reference",
detail: "Matches the results of capture group #{{group.num}}.",
},
branchreset: {
title: "Branch reset group",
detail: "Define alternative groups that share the same group numbers.",
},
noncapgroup: {
title: "Non-capturing group",
detail:
"Groups multiple tokens together without creating a capture group.",
},
atomic: {
title: "Atomic group",
detail:
"Non-capturing group that discards backtracking positions once matched.",
},
define: {
title: "Define",
detail:
"Used to define named groups for use as subroutines without including them in the match.",
},
numsubroutine: {
title: "Numeric subroutine",
detail: "Matches the expression in capture group #{{group.num}}.",
},
namedsubroutine: {
title: "Named subroutine",
detail:
"Matches the expression in the capture group named '{{group.name}}'.",
},
balancedcapture: {
title: "Balancing group",
detail:
"This allows nested constructs to be matched, such as parentheses or HTML tags. The previously defined group to balance against is specified by previous. Captures subpattern as a named group specified by name, or name can be omitted to capture as an unnamed group.",
},
absentfunction: {
title: "Absent function",
detail: "No overview available.",
},
},
lookaround: {
label: "Lookaround",
desc:
"Lookaround lets you match a group before (lookbehind) or after (lookahead) your main pattern without including it in the result." +
"
Negative lookarounds specify a group that can NOT match before or after the pattern.
",
poslookahead: {
title: "Positive lookahead",
detail:
"Matches a group after the main expression without including it in the result.",
},
neglookahead: {
title: "Negative lookahead",
detail:
"Specifies a group that can not match after the main expression (if it matches, the result is discarded).",
},
poslookbehind: {
title: "Positive lookbehind",
detail:
"Matches a group before the main expression without including it in the result.",
},
neglookbehind: {
title: "Negative lookbehind",
detail:
"Specifies a group that can not match before the main expression (if it matches, the result is discarded).",
},
keepout: {
title: "Keep out",
detail:
"Keep text matched so far out of the returned match, essentially discarding the match up to this point.",
},
nonatomicposlookahead: {
title: "Non-atomic Positive lookahead",
detail: "No overview available.",
},
nonatomicposlookbehind: {
title: "Non-atomic Positive lookbehind",
detail: "No overview available.",
},
scriptrun: {
title: "Script run",
detail: "No overview available.",
},
atomicscriptrun: {
title: "Atomic script run",
detail: "No overview available.",
},
changematchingoptions: {
title: "Change matching options",
detail: "No overview available.",
},
},
quants: {
label: "Quantifiers & Alternation",
desc:
"Quantifiers indicate that the preceding token must be matched a certain number of times. By default, quantifiers are greedy, and will match as many characters as possible." +
"Alternation acts like a boolean OR, matching one sequence or another.",
plus: {
title: "Plus",
detail: "Matches 1 or more of the preceding token.",
},
star: {
title: "Star",
detail: "Matches 0 or more of the preceding token.",
},
quant: {
title: "Quantifier",
detail: "Match {{getQuant()}} of the preceding token.",
},
opt: {
title: "Optional",
detail:
"Matches 0 or 1 of the preceding token, effectively making it optional.",
},
lazy: {
title: "Lazy",
detail:
"Makes the preceding quantifier {{getLazy()}}, causing it to match as {{getLazyFew()}} characters as possible.",
},
possessive: {
title: "Possessive",
detail:
"Makes the preceding quantifier possessive. It will match as many characters as possible, and will not release them to match subsequent tokens.",
},
alt: {
title: "Alternation",
detail:
"Acts like a boolean OR. Matches the expression before or after the |.",
},
},
other: {
label: "Special",
desc: "Tokens that don't quite fit anywhere else.",
comment: {
title: "Comment",
detail:
"Allows you to insert a comment into your expression that is ignored when finding a match.",
},
conditional: {
title: "Conditional",
detail:
"Conditionally matches one of two options based on whether a lookaround is matched.",
},
conditionalgroup: {
title: "Group conditional",
detail:
"Conditionally matches one of two options based on whether group '{{name}}' matched.",
},
recursion: {
title: "Recursion",
detail:
"Attempts to match the full expression again at the current position.",
},
mode: {
title: "Mode modifier",
detail: "{{~getDesc()}}{{~getModes()}}",
},
interpolation: {
title: "Interpolation",
detail: "No overview available.",
},
},
subst: {
label: "Substitution",
desc: "These tokens are used in a substitution string to insert different parts of the match.",
"subst_$&match": {
title: "Match",
detail: "Inserts the matched text.",
},
subst_0match: {
title: "Match",
detail: "Inserts the matched text.",
},
subst_group: {
title: "Capture group",
detail: "Inserts the results of capture group #{{group.num}}.",
},
subst_$before: {
title: "Before match",
detail:
"Inserts the portion of the source string that precedes the match.",
},
subst_$after: {
title: "After match",
detail:
"Inserts the portion of the source string that follows the match.",
},
subst_$esc: {
title: "Escaped $",
detail: "Inserts a dollar sign character ($).",
},
subst_esc: {
title: "Escaped characters",
detail:
"For convenience, these escaped characters are supported in the Replace string in RegExr: \\n, \\r, \\t, \\\\, and unicode escapes \\uFFFF. This may vary in your deploy environment.",
},
},
flags: {
label: "Flags",
desc: "Expression flags change how the expression is interpreted. Flags follow the closing forward slash of the expression (ex. /.+/igm ).",
caseinsensitive: {
title: "Ignore case",
detail: "Makes the whole expression case-insensitive.",
},
global: {
title: "Global search",
detail:
"Retain the index of the last match, allowing iterative searches.",
},
multiline: {
title: "Multiline",
detail:
"Beginning/end anchors (^/$) will match the start/end of a line.",
},
unicode: {
title: "Unicode",
detail: "Enables \\x{FFFFF} unicode escapes.",
},
sticky: {
title: "Sticky",
detail:
"The expression will only match from its lastIndex position and ignores the global (g) flag if set.",
},
dotall: {
title: "Dot all",
detail:
"Dot (.) will match any character, including newline.",
},
extended: {
title: "Extended",
detail:
"Literal whitespace characters are ignored, except in character sets.",
},
ungreedy: {
title: "Ungreedy",
detail: "Makes quantifiers ungreedy (lazy) by default.",
},
},
misc: {
label: "Miscellaneous",
desc: "No overview available.",
ignorews: {
title: "Ignored whitespace",
detail:
"Whitespace character ignored due to the extended flag or mode.",
},
extnumref: {
title: "Numeric reference",
detail: "Matches the results of capture group #{{group.num}}.",
},
char: {
title: "Character",
detail:
"Matches a {{getChar()}} character (char code {{code}}). {{getInsensitive()}}",
},
escchar: {
title: "Escaped character",
detail: "Matches a {{getChar()}} character (char code {{code}}).",
},
open: {
title: "Open",
detail: "Indicates the start of a regular expression.",
},
close: {
title: "Close",
detail:
"Indicates the end of a regular expression and the start of expression flags.",
},
condition: {
title: "Condition",
detail:
"The lookaround to match in resolving the enclosing conditional statement. See 'conditional' in the Reference for info.",
},
conditionalelse: {
title: "Conditional else",
detail: "Delimits the 'else' portion of the conditional.",
},
ERROR: {
title: "Error",
detail:
"Errors in the expression are underlined in red. Roll over errors for more info.",
},
PREG_INTERNAL_ERROR: {
title: "Internal error",
detail: "Internal PCRE error",
},
PREG_BACKTRACK_LIMIT_ERROR: {
title: "Backtrack limit error",
detail: "Backtrack limit was exhausted.",
},
PREG_RECURSION_LIMIT_ERROR: {
title: "Recursion limit error",
detail: "Recursion limit was exhausted",
},
PREG_BAD_UTF8_ERROR: {
title: "Bad UTF8 error",
detail: "Malformed UTF-8 data",
},
PREG_BAD_UTF8_OFFSET_ERROR: {
title: "Bad UTF8 offset error",
detail: "Malformed UTF-8 data",
},
any: {
title: "Any",
detail: "No overview available.",
},
assigned: {
title: "Assigned",
detail: "No overview available.",
},
ascii: {
title: "Unicode property",
detail: "Matches any characters in the ASCII script extension.",
},
},
errors: {
groupopen: "Unmatched opening parenthesis.",
groupclose: "Unmatched closing parenthesis.",
setopen: "Unmatched opening square bracket.",
rangerev:
"Range values reversed. Start char code is greater than end char code.",
quanttarg: "Invalid target for quantifier.",
quantrev: "Quantifier minimum is greater than maximum.",
esccharopen: "Dangling backslash.",
esccharbad: "Unrecognized or malformed escape character.",
unicodebad: "Unrecognized unicode category or script.",
posixcharclassbad: "Unrecognized POSIX character class.",
posixcharclassnoset: "POSIX character class must be in a character set.",
notsupported:
'The "{{~getLabel()}}" feature is not supported in this flavor of RegEx.',
fwdslash:
"Unescaped forward slash. This may cause issues if copying/pasting this expression into code.",
esccharbad: "Invalid escape sequence.",
servercomm: "An error occurred while communicating with the server.",
extraelse: "Extra else in conditional group.",
unmatchedref: 'Reference to non-existent group "{{name}}".',
modebad: 'Unrecognized mode flag "{{errmode}}".',
badname: "Group name can not start with a digit.",
dupname: "Duplicate group name.",
branchreseterr:
"Branch Reset. Results will be ok, but RegExr's parser does not number branch reset groups correctly. Coming soon!",
timeout: "The expression took longer than 250ms to execute.", // TODO: can we couple this to the help content somehow?
// warnings:
jsfuture:
'The "{{~getLabel()}}" feature may not be supported in all browsers.',
infinite:
"The expression can return empty matches, and may match infinitely in some use cases.", // TODO: can we couple this to the help content somehow?
},
empty: {
empty: {
title: "Empty",
detail: "No overview available.",
},
},
};
================================================
FILE: Public/js/misc/icons.js
================================================
"use strict";
import { config, library, dom } from "@fortawesome/fontawesome-svg-core";
import {
faFlag,
faOctagonXmark,
faStethoscope,
faHeart,
faBackwardStep,
faForwardStep,
faCaretLeft,
faCaretRight,
} from "@fortawesome/pro-solid-svg-icons";
import {
faAt,
faCheck,
faCommentAltSmile,
} from "@fortawesome/pro-regular-svg-icons";
import { faMonitorHeartRate } from "@fortawesome/pro-light-svg-icons";
import { faSwift, faGithub } from "@fortawesome/free-brands-svg-icons";
config.searchPseudoElements = true;
library.add(
faFlag,
faOctagonXmark,
faStethoscope,
faHeart,
faBackwardStep,
faForwardStep,
faCaretLeft,
faCaretRight,
faAt,
faCheck,
faCommentAltSmile,
faMonitorHeartRate,
faSwift,
faGithub
);
dom.watch();
================================================
FILE: Public/js/misc/utils.js
================================================
"use strict";
const Utils = {};
export default Utils;
Utils.copy = function (target, source) {
for (let n in source) {
target[n] = source[n];
}
return target;
};
Utils.clone = function (o) {
// this seems hacky, but it's the fastest, easiest approach for now:
return JSON.parse(JSON.stringify(o));
};
Utils.htmlSafe = function (str) {
return str == null
? ""
: ("" + str).replace(/&/g, "&").replace(/ 0 && str.length > length;
if (b) {
str = str.substr(0, length - 1);
}
if (htmlSafe) {
str = Utils.htmlSafe(str);
}
return !b
? str
: str + (tag && "<" + tag + ">") + "\u2026" + (tag && "" + tag + ">");
};
Utils.unescSubstStr = function (str) {
if (!str) {
return "";
}
return str.replace(
Utils.SUBST_ESC_RE,
(a, b, c) =>
Utils.SUBST_ESC_CHARS[b] || String.fromCharCode(parseInt(c, 16))
);
};
Utils.getRegExp = function (str) {
// returns a JS RegExp object.
let match = str.match(/^\/(.+)\/([a-z]+)?$/),
regex = null;
try {
regex = match ? new RegExp(match[1], match[2] || "") : new RegExp(str, "g");
} catch (e) {}
return regex;
};
Utils.decomposeRegEx = function (str, delim = "/") {
let re = new RegExp("^" + delim + "(.*)" + delim + "([igmsuUxy]*)$");
let match = re.exec(str);
if (match) {
return { source: match[1], flags: match[2] };
} else {
return { source: str, flags: "g" };
}
};
Utils.isMac = function () {
return !!navigator.userAgent.match(/Mac\sOS/i);
};
Utils.getCtrlKey = function () {
return Utils.isMac() ? "cmd" : "ctrl";
};
Utils.now = function () {
return window.performance ? performance.now() : Date.now();
};
Utils.getUrlParams = function () {
let match,
re = /([^&=]+)=?([^&]*)/g,
params = {};
let url = window.location.search.substr(1).replace(/\+/g, " ");
while ((match = re.exec(url))) {
params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
}
return params;
};
let deferIds = {};
Utils.defer = function (f, id, t = 1) {
clearTimeout(deferIds[id]);
if (f === null) {
delete deferIds[id];
return;
}
deferIds[id] = setTimeout(() => {
delete deferIds[id];
f();
}, t);
};
Utils.getHashCode = function (s) {
let hash = 0,
l = s.length,
i;
for (i = 0; i < l; i++) {
hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0;
}
return hash;
};
Utils.getPatternURL = function (pattern) {
let a = Utils.isLocal ? "?id=" : "/";
let url = window.location.origin,
id = (pattern && pattern.id) || "";
return url + a + id;
};
Utils.isLocal = window.location.hostname === "localhost";
Utils.getPatternURLStr = function (pattern) {
if (!pattern || !pattern.id) {
return null;
}
let a = Utils.isLocal ? "?id=" : "/";
let url = window.location.host,
id = pattern.id;
return url + a + id;
};
Utils.SUBST_ESC_CHARS = {
// this is just the list supported in Replace. Others: b, f, ", etc.
n: "\n",
r: "\r",
t: "\t",
"\\": "\\",
};
Utils.SUBST_ESC_RE = /\\([nrt\\]|u([A-Z0-9]{4}))/gi;
================================================
FILE: Public/js/runner.js
================================================
"use strict";
import ReconnectingWebSocket from "reconnecting-websocket";
export class Runner {
constructor() {
this.connection = this.createConnection(this.endpoint());
this.onconnect = () => {};
this.onready = () => {};
this.onresponse = () => {};
}
get isReady() {
return this.connection.readyState === 1;
}
run(request) {
const encoder = new TextEncoder();
this.connection.send(encoder.encode(JSON.stringify(request)));
}
createConnection(endpoint) {
if (
this.connection &&
(this.connection.readyState === 0 || this.connection.readyState === 1)
) {
return this.connection;
}
const connection = new ReconnectingWebSocket(endpoint, [], {
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 10000,
maxRetries: Infinity,
debug: false,
});
connection.bufferType = "arraybuffer";
connection.onopen = () => {
this.onconnect();
this.onready();
};
connection.onerror = (event) => {
connection.close();
};
connection.onmessage = (event) => {
if (event.data.trim()) {
this.onresponse(JSON.parse(event.data));
}
};
return connection;
}
endpoint() {
let endpoint;
if (window.location.protocol === "https:") {
endpoint = "wss:";
} else {
endpoint = "ws:";
}
endpoint += "//" + window.location.host;
endpoint += window.location.pathname + "api/ws";
return endpoint;
}
}
================================================
FILE: Public/js/state/decoder.js
================================================
"use strict";
import { ungzip } from "pako";
export class Decoder {
static decode(string) {
const base64 = decodeURIComponent(string);
const gziped = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
const json = ungzip(gziped, { to: "string" });
return JSON.parse(json);
}
}
================================================
FILE: Public/js/state/encoder.js
================================================
"use strict";
import { gzip } from "pako";
export class Encoder {
static encode(data) {
const json = JSON.stringify(data);
const gziped = gzip(json);
const base64 = btoa(String.fromCharCode(...gziped));
return encodeURIComponent(base64);
}
}
================================================
FILE: Public/js/state/worker.js
================================================
"use strict";
import { Decoder } from "./decoder.js";
import { Encoder } from "./encoder.js";
onmessage = (e) => {
if (!e.data || !e.data.type || !e.data.value) {
return;
}
switch (e.data.type) {
case "decode": {
const searchParams = new URLSearchParams(e.data.value);
const query = Object.fromEntries(searchParams.entries());
if (!query.s) {
return;
}
try {
const data = Decoder.decode(query.s);
if (!data) {
return;
}
const pattern = data.p;
const options = data.o;
const text1 = data.t1;
const builder = data.b;
const text2 = data.t2;
postMessage({
type: e.data.type,
value: {
pattern,
options,
text1,
builder,
text2,
},
});
} catch (error) {}
break;
}
case "encode": {
postMessage({
type: e.data.type,
value: `?s=${Encoder.encode({
p: e.data.value.pattern,
o: e.data.value.options,
t1: e.data.value.text1,
b: e.data.value.builder,
t2: e.data.value.text2,
})}`,
});
break;
}
}
};
================================================
FILE: Public/js/views/debugger_highlighter.js
================================================
"use strict";
import Editor from "./editor";
export default class DebuggerHighlighter {
constructor(editor) {
this.editor = editor;
this.activeMarks = [];
this.widgets = [];
}
draw(traces, backtrack) {
this.clear();
const editor = this.editor;
editor.operation(() => {
const doc = editor.getDoc();
const marks = this.activeMarks;
const defaultTextHeight = editor.defaultTextHeight();
for (const trace of traces) {
const className = "debuggermatch";
if (trace.location.start !== trace.location.end) {
const location = Editor.calcRangePos(
this.editor,
trace.location.start,
trace.location.end - trace.location.start
);
marks.push(
doc.markText(location.startPos, location.endPos, {
className: className,
})
);
} else {
const pos = doc.posFromIndex(trace.location.start);
const widget = document.createElement("span");
widget.className = className;
widget.style.height = `${defaultTextHeight * 1.5}px`;
widget.style.width = "1px";
widget.style.zIndex = "10";
this.editor.addWidget(pos, widget);
const coords = editor.charCoords(pos, "local");
widget.style.left = `${coords.left}px`;
widget.style.top = `${coords.top + 2}px`;
this.widgets.push(widget);
}
}
if (backtrack) {
const pos = doc.posFromIndex(backtrack.start);
const widget = document.createElement("span");
widget.className = "debuggerbacktrack";
widget.style.height = `${defaultTextHeight * 1.5}px`;
widget.style.width = `${editor.defaultCharWidth()}px`;
widget.style.zIndex = "10";
this.editor.addWidget(pos, widget);
const coords = editor.charCoords(pos, "local");
widget.style.left = `${coords.left}px`;
widget.style.top = `${coords.top + 2}px`;
this.widgets.push(widget);
}
});
}
clear() {
this.editor.operation(() => {
let marks = this.activeMarks;
for (var i = 0, l = marks.length; i < l; i++) {
marks[i].clear();
}
marks.length = 0;
for (const widget of this.widgets) {
widget.parentNode.removeChild(widget);
}
this.widgets.length = 0;
});
}
}
================================================
FILE: Public/js/views/debugger_text.js
================================================
"use strict";
import Editor from "./editor";
import DebuggerHighlighter from "./debugger_highlighter";
export class DebuggerText {
constructor(container) {
this.container = container;
this.init(container);
}
get value() {
return this.editor.getValue();
}
set value(val) {
this.editor.setValue(val);
}
init(container) {
const editor = Editor.create(
container,
{
lineWrapping: true,
screenReaderLabel: "Debugger Test View",
readOnly: true,
},
"100%",
"100%"
);
this.editor = editor;
this.highlighter = new DebuggerHighlighter(editor);
}
}
================================================
FILE: Public/js/views/dsl_editor.js
================================================
"use strict";
import { EventDispatcher } from "@createjs/easeljs";
import Editor from "./editor";
import ErrorMessage from "./error_message";
import Utils from "../misc/utils";
const defaultValue = `Regex {
Capture {
ChoiceOf {
"CREDIT"
"DEBIT"
}
}
OneOrMore(.whitespace)
Capture {
Regex {
Repeat(1...2) {
One(.digit)
}
"/"
Repeat(1...2) {
One(.digit)
}
"/"
Repeat(count: 4) {
One(.digit)
}
}
}
}
`;
export class DSLEditor extends EventDispatcher {
static defaultValue = defaultValue;
constructor(container) {
super();
this.container = container;
this.init(container);
}
get value() {
return this.editor.getValue();
}
set value(val) {
this.editor.setValue(val);
}
set error(error) {
const editor = this.editor;
const widgets = this.widgets;
editor.operation(function () {
for (const widget of widgets) {
editor.removeLineWidget(widget);
}
widgets.length = 0;
if (!error) {
return;
}
widgets.push(
editor.addLineWidget(0, ErrorMessage.create(error), {
coverGutter: false,
noHScroll: true,
above: true,
})
);
});
}
init(container) {
this.editor = Editor.create(
container,
{
lineNumbers: true,
lineWrapping: false,
matchBrackets: true,
mode: "swift",
screenReaderLabel: "Build DSL Editor",
},
"100%",
"100%"
);
this.editor.setValue(defaultValue);
this.editor.setCursor(this.editor.lineCount(), 0);
this.editor.on("change", (editor, event) =>
this.onEditorChange(editor, event)
);
this.widgets = [];
}
setDefaultValue() {
this.editor.setValue(defaultValue);
}
deferUpdate() {
Utils.defer(() => this.update(), "DSLEditor.update");
}
update() {
this.dispatchEvent("change");
}
onEditorChange(editor, event) {
this.deferUpdate();
}
}
================================================
FILE: Public/js/views/dsl_highlighter.js
================================================
"use strict";
import { EventDispatcher } from "@createjs/easeljs";
import Editor from "./editor";
export class DSLHighlighter extends EventDispatcher {
constructor(editor) {
super();
this.editor = editor;
this.activeMarks = [];
}
clear() {
this.editor.operation(() => {
let marks = this.activeMarks;
for (var i = 0, l = marks.length; i < l; i++) {
marks[i].clear();
}
marks.length = 0;
});
}
draw(tokens) {
const editor = this.editor;
this.clear();
editor.operation(() => {
const doc = editor.getDoc();
const marks = this.activeMarks;
for (const token of tokens) {
const className = "highlight";
const location = Editor.calcRangePos(
this.editor,
token.sourceLocation.start,
token.sourceLocation.end - token.sourceLocation.start
);
marks.push(
doc.markText(location.startPos, location.endPos, {
className: className,
})
);
}
});
}
}
================================================
FILE: Public/js/views/dsl_view.js
================================================
"use strict";
import { EventDispatcher } from "@createjs/easeljs";
import Editor from "./editor";
import ErrorMessage from "./error_message";
export class DSLView extends EventDispatcher {
constructor(container) {
super();
this.container = container;
this.init(container);
}
get value() {
return this.editor.getValue();
}
set value(val) {
this.editor.setValue(val);
}
set error(error) {
const editor = this.editor;
const widgets = this.widgets;
editor.operation(function () {
for (const widget of widgets) {
editor.removeLineWidget(widget);
}
widgets.length = 0;
if (!error) {
return;
}
if (typeof error === "string" || error instanceof String) {
widgets.push(
editor.addLineWidget(0, ErrorMessage.create(error), {
coverGutter: false,
noHScroll: true,
above: true,
}),
);
} else {
for (const e of error) {
const message = ErrorMessage.create(e.message);
widgets.push(
editor.addLineWidget(0, message, {
coverGutter: false,
noHScroll: true,
above: true,
}),
);
}
}
});
}
init(container) {
this.editor = Editor.create(
container,
{
lineNumbers: true,
lineWrapping: false,
matchBrackets: true,
mode: "swift",
readOnly: true,
screenReaderLabel: "Build DSL View",
},
"100%",
"100%",
);
this.widgets = [];
}
}
================================================
FILE: Public/js/views/editor.js
================================================
"use strict";
import CodeMirror from "codemirror";
import "codemirror/mode/swift/swift";
import Utils from "../misc/utils";
const Editor = {};
export default Editor;
Editor.create = (target, opts = {}, width = "100%", height = "100%") => {
const keys = {};
const o = Utils.copy(
{
extraKeys: keys,
indentWithTabs: false,
lineNumbers: false,
mode: "null",
specialChars:
/[ \u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/,
specialCharPlaceholder: (ch) =>
createElement("span", ch === " " ? "cm-space" : "cm-special", " "), // needs to be a space so wrapping works
tabSize: 2,
},
opts,
);
const cm = CodeMirror(target, o);
cm.setSize(width, height);
if (cm.getOption("maxLength")) {
cm.on("beforeChange", Editor.enforceMaxLength);
}
if (cm.getOption("singleLine")) {
cm.on("beforeChange", Editor.enforceSingleLine);
}
return cm;
};
Editor.enforceMaxLength = (cm, change) => {
let maxLength = cm.getOption("maxLength");
if (maxLength && change.update) {
let str = change.text.join("\n");
let delta =
str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from));
if (delta <= 0) {
return true;
}
delta = cm.getValue().length + delta - maxLength;
if (delta > 0) {
str = str.substr(0, str.length - delta);
change.update(change.from, change.to, str.split("\n"));
}
}
return true;
};
Editor.enforceSingleLine = (cm, change) => {
if (change.update) {
let str = change.text.join("").replace(/(\n|\r)/g, "");
change.update(change.from, change.to, [str]);
}
return true;
};
Editor.selectAll = (cm) => {
cm.focus();
cm.setSelection({ ch: 0, line: 0 }, { ch: 0, line: cm.lineCount() });
};
Editor.calcRangePos = (cm, i, l = 0, o = {}) => {
let doc = cm.getDoc();
o.startPos = doc.posFromIndex(i);
o.endPos = doc.posFromIndex(i + l);
return o;
};
function createElement(type, className, content, parent) {
let element = document.createElement(type || "div");
if (className) {
element.className = className;
}
if (content) {
if (content instanceof HTMLElement) {
element.appendChild(content);
} else {
element.innerHTML = content;
}
}
if (parent) {
parent.appendChild(element);
}
return element;
}
================================================
FILE: Public/js/views/error_message.js
================================================
"use strict";
const ErrorMessage = {};
export default ErrorMessage;
ErrorMessage.create = (message) => {
const container = document.createElement("div");
container.classList.add("error-message", "d-flex", "flex-row");
const wrapper = document.createElement("div");
wrapper.classList.add("d-flex", "flex-row", "overflow-hidden", "w-100");
container.appendChild(wrapper);
const iconWrapper = document.createElement("div");
wrapper.appendChild(iconWrapper);
const icon = document.createElement("span");
icon.classList.add(
"fa-solid",
"fa-octagon-xmark",
"fa-xs",
"text-danger",
"px-2",
);
iconWrapper.appendChild(icon);
const messageWrapper = document.createElement("div");
messageWrapper.classList.add("text-nowrap");
messageWrapper.textContent = message;
wrapper.appendChild(messageWrapper);
return container;
};
================================================
FILE: Public/js/views/expression_field.js
================================================
"use strict";
import { EventDispatcher } from "@createjs/easeljs";
import tippy from "tippy.js";
import Editor from "./editor";
import ExpressionHighlighter from "./expression_highlighter";
import Utils from "../misc/utils";
export class ExpressionField extends EventDispatcher {
constructor(container) {
super();
this.container = container;
this.init(container);
}
get value() {
return this.editor.getValue();
}
set value(val) {
this.editor.setValue(val);
}
set tokens(tokens) {
this.expressionTokens = tokens;
this.highlighter.draw(tokens);
this.resetTooltips();
}
set error(error) {
if (error.length) {
let message = "";
if (typeof error === "string" || error instanceof String) {
const errorMessage = Utils.htmlSafe(error);
message = `Parse Error: ${errorMessage}`;
} else {
message = error
.map((e) => {
const errorMessage = Utils.htmlSafe(e.message);
return `${e.behavior}: ${errorMessage}`;
})
.join(" ");
this.highlighter.drawError(error);
}
this.errorMessageTooltip.setContent(message);
document
.getElementById("expression-field-error")
.classList.remove("d-none");
} else {
this.errorMessageTooltip.setContent("");
document.getElementById("expression-field-error").classList.add("d-none");
this.highlighter.clearError();
}
tippy(".exp-syntax-error", {
allowHTML: true,
animation: false,
placement: "bottom",
});
}
init(container) {
this.editor = Editor.create(
container,
{
autofocus: true,
maxLength: 2500,
singleLine: true,
screenReaderLabel: "Regular Expression Field",
},
"100%",
"100%",
);
this.editor.on("change", (editor, event) =>
this.onEditorChange(editor, event),
);
this.highlighter = new ExpressionHighlighter(this.editor);
this.expressionTokens = [];
this.activeTooltips = [];
this.errorMessageTooltip = tippy(
document.getElementById("expression-field-error"),
{
...tooltipProps,
},
);
}
setDefaultValue() {
this.editor.setValue(defaultValue);
this.editor.setCursor(this.editor.lineCount(), 0);
}
resetTooltips() {
for (const tooltip of this.activeTooltips) {
tooltip.destroy();
}
this.activeTooltips = tippy(tooltipSelector, {
...tooltipProps,
onShow: (instance) => {
const index = instance.reference.dataset.tokenIndex;
if (index === undefined) {
return false;
}
const token = this.expressionTokens[index];
this.onHover(token, instance);
return false;
},
});
}
deferUpdate() {
Utils.defer(() => this.update(), "ExpressionField.update");
}
update() {
this.dispatchEvent("change");
}
onEditorChange(editor, event) {
this.deferUpdate();
}
onHover(token, tippyInstance) {
this.hoverToken = token;
this.highlighter.drawHover(token);
this.dispatchEvent("hover");
for (const tooltip of this.activeTooltips) {
if (tooltip !== tippyInstance) {
tooltip.destroy();
}
}
this.activeTooltips = tippy(tooltipSelector, {
...tooltipProps,
onUntrigger: (instance) => {
this.highlighter.clearHover();
this.resetTooltips();
this.dispatchEvent("unhover");
},
});
}
}
const defaultValue = `(CREDIT|DEBIT)\\s+(\\d{1,2}/\\d{1,2}/\\d{4})`;
const tooltipSelector = "#expression-field-container span[data-tippy-content]";
const tooltipProps = {
allowHTML: true,
animation: false,
placement: "bottom",
};
================================================
FILE: Public/js/views/expression_highlighter.js
================================================
"use strict";
import { EventDispatcher } from "@createjs/easeljs";
import { Reference } from "../docs/reference";
import Editor from "./editor";
export default class ExpressionHighlighter extends EventDispatcher {
constructor(editor) {
super();
this.editor = editor;
this.activeMarks = [];
this.hoverMarks = [];
this.widgets = [];
}
draw(tokens) {
this.clear();
const pre = ExpressionHighlighter.CSS_PREFIX;
const editor = this.editor;
editor.operation(() => {
const doc = editor.getDoc();
const marks = this.activeMarks;
for (const [i, token] of Object.entries(tokens)) {
const location = Editor.calcRangePos(
this.editor,
token.location.start,
token.location.end - token.location.start,
);
const tooltipAttr = (() => {
if (token.tooltip) {
const reference = Reference.get(
token.tooltip.category,
token.tooltip.key,
);
let title = reference ? reference.title : token.tooltip.category;
let detail = reference ? reference.detail : token.tooltip.key;
for (const [k, v] of Object.entries(token.tooltip.substitution)) {
title = title.replaceAll(k, v);
detail = detail.replaceAll(k, v);
}
return {
"data-tippy-content": makeTooltip(title, detail),
};
} else {
return {};
}
})();
marks.push(
doc.markText(location.startPos, location.endPos, {
className: `${token.classes.map((c) => `${pre}-${c}`).join(" ")}`,
attributes: {
...tooltipAttr,
"data-token-index": i,
},
}),
);
}
});
}
drawError(errors) {
this.clearError();
const pre = ExpressionHighlighter.CSS_PREFIX;
const editor = this.editor;
editor.operation(() => {
for (const error of errors) {
const location = Editor.calcRangePos(
this.editor,
error.location.start,
error.location.end - error.location.start,
);
const widget = document.createElement("span");
widget.className = `${pre}-syntax-error`;
widget.style.height = `5px`;
widget.style.zIndex = "10";
widget.setAttribute(
"data-tippy-content",
`${error.behavior}: ${error.message}`,
);
editor.addWidget(location.startPos, widget);
const startCoords = editor.charCoords(location.startPos, "local");
const endCoords = editor.charCoords(location.endPos, "local");
widget.style.left = `${startCoords.left + 1}px`;
widget.style.top = `${startCoords.bottom - 1}px`;
if (error.location.start === error.location.end) {
widget.style.width = `${editor.defaultCharWidth()}px`;
} else {
widget.style.width = `${endCoords.left - startCoords.left - 2}px`;
}
this.widgets.push(widget);
}
});
}
clear() {
this.editor.operation(() => {
for (const mark of this.activeMarks) {
mark.clear();
}
this.activeMarks.length = 0;
});
}
clearError() {
this.editor.operation(() => {
for (const widget of this.widgets) {
widget.parentNode.removeChild(widget);
}
this.widgets.length = 0;
});
}
drawHover(token) {
const selection = token.selection;
const related = token.related;
if ((!selection && !related) || this.hoverMarks.length) {
return;
}
this.clearHover();
if (selection) {
this.drawBorder(selection, "selected");
}
if (related) {
this.drawBorder(related.location, "related");
}
}
drawBorder(range, className) {
const editor = this.editor;
const doc = editor.getDoc();
const pre = ExpressionHighlighter.CSS_PREFIX;
const left = Editor.calcRangePos(this.editor, range.start, 1);
const location = Editor.calcRangePos(
this.editor,
range.start,
range.end - range.start,
);
const right = Editor.calcRangePos(this.editor, range.end - 1, 1);
this.hoverMarks.push(
doc.markText(left.startPos, left.endPos, {
className: `${pre}-${className}-left`,
}),
);
this.hoverMarks.push(
doc.markText(location.startPos, location.endPos, {
className: `${pre}-${className}`,
}),
);
this.hoverMarks.push(
doc.markText(right.startPos, right.endPos, {
className: `${pre}-${className}-right`,
}),
);
}
clearHover() {
this.editor.operation(() => {
for (const mark of this.hoverMarks) {
mark.clear();
}
this.hoverMarks.length = 0;
});
}
}
function makeTooltip(label, desc) {
return `