diff --git a/meshchatx/src/frontend/js/MicronParser.js b/meshchatx/src/frontend/js/MicronParser.js
index 80cd198..aa92cf8 100644
--- a/meshchatx/src/frontend/js/MicronParser.js
+++ b/meshchatx/src/frontend/js/MicronParser.js
@@ -7,57 +7,139 @@
* Documentation for the Micron markdown format can be found here:
* https://raw.githubusercontent.com/markqvist/NomadNet/refs/heads/master/nomadnet/ui/textui/Guide.py
*/
+
class MicronParser {
- constructor(darkTheme = true) {
+
+ constructor(darkTheme = true, enableForceMonospace = true) {
this.darkTheme = darkTheme;
+ this.enableForceMonospace = enableForceMonospace;
this.DEFAULT_FG_DARK = "ddd";
this.DEFAULT_FG_LIGHT = "222";
this.DEFAULT_BG = "default";
- this.SELECTED_STYLES = null;
+ if (this.enableForceMonospace) {
+ this.injectMonospaceStyles();
+ }
+
+ try {
+ if (typeof DOMPurify === 'undefined') {
+ console.warn('DOMPurify is not installed. Include it above micron-parser.js or run npm install dompurify');
+ }
+ } catch (error) {
+ console.warn('DOMPurify is not installed. Include it above micron-parser.js or run npm install dompurify');
+ }
this.STYLES_DARK = {
- plain: { fg: this.DEFAULT_FG_DARK, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false },
- heading1: { fg: "222", bg: "bbb", bold: false, underline: false, italic: false },
- heading2: { fg: "111", bg: "999", bold: false, underline: false, italic: false },
- heading3: { fg: "000", bg: "777", bold: false, underline: false, italic: false },
+ "plain": {fg: this.DEFAULT_FG_DARK, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false},
+ "heading1": {fg: "222", bg: "bbb", bold: false, underline: false, italic: false},
+ "heading2": {fg: "111", bg: "999", bold: false, underline: false, italic: false},
+ "heading3": {fg: "000", bg: "777", bold: false, underline: false, italic: false}
};
this.STYLES_LIGHT = {
- plain: { fg: this.DEFAULT_FG_LIGHT, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false },
- heading1: { fg: "000", bg: "777", bold: false, underline: false, italic: false },
- heading2: { fg: "111", bg: "aaa", bold: false, underline: false, italic: false },
- heading3: { fg: "222", bg: "ccc", bold: false, underline: false, italic: false },
+ "plain": {fg: this.DEFAULT_FG_LIGHT, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false},
+ "heading1": {fg: "000", bg: "777", bold: false, underline: false, italic: false},
+ "heading2": {fg: "111", bg: "aaa", bold: false, underline: false, italic: false},
+ "heading3": {fg: "222", bg: "ccc", bold: false, underline: false, italic: false}
};
- if (this.darkTheme) {
- this.SELECTED_STYLES = this.STYLES_DARK;
- } else {
- this.SELECTED_STYLES = this.STYLES_LIGHT;
+ this.SELECTED_STYLES = this.darkTheme ? this.STYLES_DARK : this.STYLES_LIGHT;
+
+ }
+
+ injectMonospaceStyles() {
+ if (document.getElementById('micron-monospace-styles')) {
+ return;
}
+
+ const styleEl = document.createElement('style');
+ styleEl.id = 'micron-monospace-styles';
+
+ styleEl.textContent = `
+ .Mu-nl {
+ cursor: pointer;
+ }
+ .Mu-mnt {
+ display: inline-block;
+ width: 0.6em;
+ text-align: center;
+ white-space: pre;
+ text-decoration: inherit;
+ }
+ .Mu-mws {
+ text-decoration: inherit;
+ display: inline-block;
+ }
+ `;
+ document.head.appendChild(styleEl);
}
static formatNomadnetworkUrl(url) {
return `nomadnetwork://${url}`;
}
+
+ parseHeaderTags(markup) {
+ let pageFg = null;
+ let pageBg = null;
+
+ const lines = markup.split("\n");
+
+ for (let line of lines) {
+ const trimmedLine = line.trim();
+
+ if (trimmedLine.length === 0) {
+ continue;
+ }
+
+ if (!trimmedLine.startsWith("#!")) {
+ break;
+ }
+
+ if (trimmedLine.startsWith("#!fg=")) {
+ let color = trimmedLine.substring(5).trim();
+ if (color.length === 3 || color.length === 6) {
+ pageFg = color;
+ }
+ }
+
+ if (trimmedLine.startsWith("#!bg=")) {
+ let color = trimmedLine.substring(5).trim();
+ if (color.length === 3 || color.length === 6) {
+ pageBg = color;
+ }
+ }
+ }
+
+ return { fg: pageFg, bg: pageBg };
+ }
+
convertMicronToHtml(markup) {
let html = "";
+ // parse header tags for page-level color defaults
+ const headerColors = this.parseHeaderTags(markup);
+
+ const plainStyle = this.SELECTED_STYLES?.plain || {fg: this.DEFAULT_FG_DARK, bg: this.DEFAULT_BG};
+ const defaultFg = headerColors.fg || plainStyle.fg;
+ const defaultBg = headerColors.bg || this.DEFAULT_BG;
+
let state = {
literal: false,
depth: 0,
- fg_color: this.SELECTED_STYLES.plain.fg,
- bg_color: this.DEFAULT_BG,
+ fg_color: defaultFg,
+ bg_color: defaultBg,
formatting: {
bold: false,
underline: false,
italic: false,
- strikethrough: false,
+ strikethrough: false
},
default_align: "left",
align: "left",
- radio_groups: {},
+ default_fg: defaultFg,
+ default_bg: defaultBg,
+ radio_groups: {}
};
const lines = markup.split("\n");
@@ -68,42 +150,62 @@ class MicronParser {
for (let el of lineOutput) {
html += el.outerHTML;
}
+ } else if (lineOutput && lineOutput.length === 0) {
+ // skip
} else {
html += "
";
}
}
- return html;
+
+ try {
+ return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
+ } catch (error) {
+ console.warn('DOMPurify is not installed. Include it above micron-parser.js or run npm install dompurify ', error);
+ return `
⚠ DOMPurify is not installed. Include it above micron-parser.js or run npm install dompurify
`;
+ }
}
- parseToHtml(markup) {
+ convertMicronToFragment(markup) {
// Create a fragment to hold all the Micron output
const fragment = document.createDocumentFragment();
+ const headerColors = this.parseHeaderTags(markup);
+
+ const plainStyle = this.SELECTED_STYLES?.plain || {fg: this.DEFAULT_FG_DARK, bg: this.DEFAULT_BG};
+ const defaultFg = headerColors.fg || plainStyle.fg;
+ const defaultBg = headerColors.bg || this.DEFAULT_BG;
+
let state = {
literal: false,
depth: 0,
- fg_color: this.SELECTED_STYLES.plain.fg,
- bg_color: this.DEFAULT_BG,
+ fg_color: defaultFg,
+ bg_color: defaultBg,
formatting: {
bold: false,
underline: false,
italic: false,
- strikethrough: false,
+ strikethrough: false
},
default_align: "left",
align: "left",
- radio_groups: {},
+ default_fg: defaultFg,
+ default_bg: defaultBg,
+ radio_groups: {}
};
const lines = markup.split("\n");
for (let line of lines) {
+ line = DOMPurify.sanitize(line, { USE_PROFILES: { html: true } });
const lineOutput = this.parseLine(line, state);
if (lineOutput && lineOutput.length > 0) {
for (let el of lineOutput) {
+
fragment.appendChild(el);
}
+ } else if (lineOutput && lineOutput.length === 0) {
+ // skip
} else {
fragment.appendChild(document.createElement("br"));
}
@@ -120,10 +222,11 @@ class MicronParser {
return null;
}
+
if (!state.literal) {
- // Comments
+ // Comments, and header tags s
if (line[0] === "#") {
- return null;
+ return [];
}
// Reset section depth
@@ -145,10 +248,11 @@ class MicronParser {
// apply heading style if it exists
let style = null;
let wanted_style = "heading" + i;
- if (this.SELECTED_STYLES[wanted_style]) {
+ const defaultPlain = {fg: this.darkTheme ? this.DEFAULT_FG_DARK : this.DEFAULT_FG_LIGHT, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false};
+ if (this.SELECTED_STYLES?.[wanted_style]) {
style = this.SELECTED_STYLES[wanted_style];
} else {
- style = this.SELECTED_STYLES.plain;
+ style = this.SELECTED_STYLES?.plain || defaultPlain;
}
const latched_style = this.stateToStyle(state);
@@ -157,6 +261,22 @@ class MicronParser {
let outputParts = this.makeOutput(state, headingLine);
this.styleToState(latched_style, state);
+ // make outputParts full container width
+ if (outputParts && outputParts.length > 0) {
+ const outerDiv = document.createElement("div");
+ outerDiv.style.display = "inline-block";
+ outerDiv.style.width = "100%";
+ this.applyStyleToElement(outerDiv, style);
+
+ const innerDiv = document.createElement("div");
+ this.applySectionIndent(innerDiv, state);
+
+ this.appendOutput(innerDiv, outputParts, state);
+ outerDiv.appendChild(innerDiv);
+
+ const br = document.createElement("br");
+ return [outerDiv, br]
+ }
// wrap in a heading container
if (outputParts && outputParts.length > 0) {
const div = document.createElement("div");
@@ -175,28 +295,94 @@ class MicronParser {
// horizontal dividers
if (line[0] === "-") {
- const hr = document.createElement("hr");
- this.applySectionIndent(hr, state);
- return [hr];
+ // if the line is just "-", do a normal
+ if (line.length === 1) {
+ const hr = document.createElement("hr");
+ hr.style.all = "revert";
+ hr.style.borderColor = this.colorToCss(state.fg_color);
+ hr.style.margin = "0.5em 0.5em 0.5em 0.5em";
+ hr.style.boxShadow = "0 0 0 0.5em " + this.colorToCss(state.bg_color);
+ this.applySectionIndent(hr, state);
+ return [hr];
+ } else {
+ // if second char given
+ const dividerChar = line[1]; // use the following character for creating the divider
+ const repeated = dividerChar.repeat(250);
+
+ const div = document.createElement("div");
+ div.style.whiteSpace = "pre"; // needs to not wrap and ignore container formatting
+ div.textContent = repeated;
+ div.style.width = "100%";
+ div.style.whiteSpace = "nowrap";
+ div.style.overflow = "hidden";
+ div.style.color = this.colorToCss(state.fg_color);
+ div.style.backgroundColor = this.colorToCss(state.bg_color);
+ this.applySectionIndent(div, state);
+
+ return [div];
+ }
}
+
}
let outputParts = this.makeOutput(state, line);
+ // outputParts can contain text (tuple) and special objects (fields/checkbox)
if (outputParts) {
- // outputParts can contain text (tuple) and special objects (fields/checkbox)
- // construct a single line container
+
+ // create parent div container to apply proper section indent
let container = document.createElement("div");
this.applyAlignment(container, state);
this.applySectionIndent(container, state);
+
this.appendOutput(container, outputParts, state);
+
+ // if theres a background color, wrap with outer div
+ if (state.bg_color !== this.DEFAULT_BG) {
+ const outerDiv = document.createElement("div");
+ outerDiv.style.backgroundColor = this.colorToCss(state.bg_color);
+ outerDiv.style.width = "100%";
+ outerDiv.style.display = "block";
+ outerDiv.appendChild(container);
+ return [outerDiv];
+ }
return [container];
} else {
- // Just empty line
- return [document.createElement("br")];
+ // empty line but maintain background color if set
+ const br = document.createElement("br");
+ if (state.bg_color !== this.DEFAULT_BG) {
+ const outerDiv = document.createElement("div");
+ outerDiv.style.backgroundColor = this.colorToCss(state.bg_color);
+ outerDiv.style.width = "100%";
+ outerDiv.style.height = "1.2em";
+ outerDiv.style.display = "block";
+
+ const innerDiv = document.createElement("div");
+ this.applySectionIndent(innerDiv, state);
+ innerDiv.appendChild(br);
+ outerDiv.appendChild(innerDiv);
+
+ return [outerDiv];
+ }
+ return [br];
}
} else {
- // Empty line
- return [document.createElement("br")];
+ // Empty line handling for just newline background color
+ const br = document.createElement("br");
+ if (state.bg_color !== this.DEFAULT_BG) {
+ const outerDiv = document.createElement("div");
+ outerDiv.style.backgroundColor = this.colorToCss(state.bg_color);
+ outerDiv.style.width = "100%";
+ outerDiv.style.height = "1.2em";
+ outerDiv.style.display = "block";
+
+ const innerDiv = document.createElement("div");
+ this.applySectionIndent(innerDiv, state);
+ innerDiv.appendChild(br);
+ outerDiv.appendChild(innerDiv);
+
+ return [outerDiv];
+ }
+ return [br];
}
}
@@ -208,8 +394,9 @@ class MicronParser {
applySectionIndent(el, state) {
// indent by state.depth
let indent = (state.depth - 1) * 2;
- if (indent > 0) {
- el.style.marginLeft = indent * 10 + "px";
+ if (indent > 0 ) {
+ // Indent according to forceMonospace() character width
+ el.style.marginLeft = (indent * 0.6) + "em";
}
}
@@ -220,7 +407,7 @@ class MicronParser {
bg: state.bg_color,
bold: state.formatting.bold,
underline: state.formatting.underline,
- italic: state.formatting.italic,
+ italic: state.formatting.italic
};
}
@@ -232,12 +419,16 @@ class MicronParser {
if (style.italic !== undefined && style.italic !== null) state.formatting.italic = style.italic;
}
- appendOutput(container, parts) {
+ appendOutput(container, parts, state) {
+
let currentSpan = null;
let currentStyle = null;
- const flushSpan = () => {
+ const flushSpan = () => {
if (currentSpan) {
+ if (currentStyle && currentStyle.bg !== this.DEFAULT_BG) {
+ currentSpan.style.display = "inline-block";
+ }
container.appendChild(currentSpan);
currentSpan = null;
currentStyle = null;
@@ -245,9 +436,9 @@ class MicronParser {
};
for (let p of parts) {
- if (typeof p === "string") {
+ if (typeof p === 'string') {
let span = document.createElement("span");
- span.textContent = p;
+ span.innerHTML = p;
container.appendChild(span);
} else if (Array.isArray(p) && p.length === 2) {
// tuple: [styleSpec, text]
@@ -259,15 +450,15 @@ class MicronParser {
this.applyStyleToElement(currentSpan, styleSpec);
currentStyle = styleSpec;
}
- currentSpan.textContent += text;
- } else if (p && typeof p === "object") {
+ currentSpan.innerHTML += text;
+ } else if (p && typeof p === 'object') {
// field, checkbox, radio, link
flushSpan();
if (p.type === "field") {
let input = document.createElement("input");
input.type = p.masked ? "password" : "text";
input.name = p.name;
- input.setAttribute("value", p.data);
+ input.setAttribute('value', p.data);
if (p.width) {
input.size = p.width;
}
@@ -279,7 +470,7 @@ class MicronParser {
cb.type = "checkbox";
cb.name = p.name;
cb.value = p.value;
- if (p.prechecked) cb.setAttribute("checked", true);
+ if (p.prechecked) cb.setAttribute('checked', true);
label.appendChild(cb);
label.appendChild(document.createTextNode(" " + p.label));
this.applyStyleToElement(label, this.styleFromState(p.style));
@@ -290,13 +481,14 @@ class MicronParser {
rb.type = "radio";
rb.name = p.name;
rb.value = p.value;
- if (p.prechecked) rb.setAttribute("checked", true);
+ if (p.prechecked) rb.setAttribute('checked', true);
label.appendChild(rb);
label.appendChild(document.createTextNode(" " + p.label));
this.applyStyleToElement(label, this.styleFromState(p.style));
container.appendChild(label);
} else if (p.type === "link") {
- let directURL = p.url.replace("nomadnetwork://", "").replace("lxmf://", "");
+
+ let directURL = p.url.replace('nomadnetwork://', '').replace('lxmf://', '');
// use p.url as is for the href
const formattedUrl = p.url;
@@ -310,12 +502,12 @@ class MicronParser {
if (p.fields && p.fields.length > 0) {
for (const f of p.fields) {
- if (f === "*") {
+ if (f === '*') {
// submit all fields
foundAll = true;
- } else if (f.includes("=")) {
+ } else if (f.includes('=')) {
// this is a request variable (key=value)
- const [k, v] = f.split("=");
+ const [k, v] = f.split('=');
requestVars[k] = v;
} else {
// this is a field name to submit
@@ -323,38 +515,35 @@ class MicronParser {
}
}
- let fieldStr = "";
+ let fieldStr = '';
if (foundAll) {
// if '*' was found, submit all fields
- fieldStr = "*";
+ fieldStr = '*';
} else {
- fieldStr = fieldsToSubmit.join("|");
+ fieldStr = fieldsToSubmit.join('|');
}
// append request variables directly to the directURL as query parameters
const varEntries = Object.entries(requestVars);
if (varEntries.length > 0) {
- const queryString = varEntries.map(([k, v]) => `${k}=${v}`).join("|");
+ const queryString = varEntries.map(([k, v]) => `${k}=${v}`).join('|');
- directURL += directURL.includes("`") ? `|${queryString}` : `\`${queryString}`;
+ directURL += directURL.includes('`') ? `|${queryString}` : `\`${queryString}`;
}
- a.setAttribute(
- "onclick",
- `event.preventDefault(); onNodePageUrlClick('${directURL}', '${fieldStr}', false, false)`
- );
+ a.setAttribute("data-destination", `${directURL}`);
+ a.setAttribute("data-fields", `${fieldStr}`);
} else {
// no fields or request variables, just handle the direct URL
- a.setAttribute(
- "onclick",
- `event.preventDefault(); onNodePageUrlClick('${directURL}', null, false, false)`
- );
+ a.setAttribute("data-destination", `${directURL}`);
}
-
- a.textContent = p.label;
+ a.classList.add('Mu-nl');
+ a.setAttribute('data-action', "openNode");
+ a.innerHTML = p.label;
this.applyStyleToElement(a, this.styleFromState(p.style));
container.appendChild(a);
}
+
}
}
@@ -364,13 +553,7 @@ class MicronParser {
stylesEqual(s1, s2) {
if (!s1 && !s2) return true;
if (!s1 || !s2) return false;
- return (
- s1.fg === s2.fg &&
- s1.bg === s2.bg &&
- s1.bold === s2.bold &&
- s1.underline === s2.underline &&
- s1.italic === s2.italic
- );
+ return (s1.fg === s2.fg && s1.bg === s2.bg && s1.bold === s2.bold && s1.underline === s2.underline && s1.italic === s2.italic);
}
styleFromState(stateStyle) {
@@ -379,7 +562,7 @@ class MicronParser {
return stateStyle;
}
- applyStyleToElement(el, style) {
+applyStyleToElement(el, style) {
if (!style) return;
// convert style fg/bg to colors
let fgColor = this.colorToCss(style.fg);
@@ -390,13 +573,14 @@ class MicronParser {
}
if (bgColor && bgColor !== "default") {
el.style.backgroundColor = bgColor;
+ el.style.display = "inline-block";
}
if (style.bold) {
el.style.fontWeight = "bold";
}
if (style.underline) {
- el.style.textDecoration = el.style.textDecoration ? el.style.textDecoration + " underline" : "underline";
+ el.style.textDecoration = (el.style.textDecoration ? el.style.textDecoration + " underline" : "underline");
}
if (style.italic) {
el.style.fontStyle = "italic";
@@ -414,14 +598,12 @@ class MicronParser {
return "#" + c;
}
// If grayscale 'gxx'
- if (c.length === 3 && c[0] === "g") {
+ if (c.length === 3 && c[0] === 'g') {
// treat xx as a number and map to gray
let val = parseInt(c.slice(1), 10);
if (isNaN(val)) val = 50;
// map 0-99 scale to a gray hex
- let h = Math.floor(val * 2.55)
- .toString(16)
- .padStart(2, "0");
+ let h = Math.floor(val * 2.55).toString(16).padStart(2, '0');
return "#" + h + h + h;
}
@@ -435,7 +617,11 @@ class MicronParser {
if (line === "\\`=") {
line = "`=";
}
- return [[this.stateToStyle(state), line]];
+ if(this.enableForceMonospace) {
+ return [[this.stateToStyle(state), this.splitAtSpaces(line)]];
+ } else {
+ return [[this.stateToStyle(state), line]];
+ }
}
let output = [];
@@ -443,9 +629,22 @@ class MicronParser {
let mode = "text";
let escape = false;
let skip = 0;
+
+ const flushPart = () => {
+ if (part.length > 0) {
+ if(this.enableForceMonospace) {
+ output.push([this.stateToStyle(state), this.splitAtSpaces(part)]);
+ } else {
+ output.push([this.stateToStyle(state), part]);
+ }
+ part = "";
+ }
+ };
+
let i = 0;
while (i < line.length) {
let c = line[i];
+
if (skip > 0) {
skip--;
i++;
@@ -453,91 +652,85 @@ class MicronParser {
}
if (mode === "formatting") {
- // Handle formatting commands
switch (c) {
- case "_":
+ case '_':
state.formatting.underline = !state.formatting.underline;
break;
- case "!":
+ case '!':
state.formatting.bold = !state.formatting.bold;
break;
- case "*":
+ case '*':
state.formatting.italic = !state.formatting.italic;
break;
- case "F":
- // next 3 chars = fg color
+
+ case 'F':
+ // next 3 chars => fg color
if (line.length >= i + 4) {
let color = line.substr(i + 1, 3);
state.fg_color = color;
skip = 3;
}
break;
- case "f":
- // reset fg
- state.fg_color = this.SELECTED_STYLES.plain.fg;
+ case 'f':
+ // reset fg to page default
+ state.fg_color = state.default_fg;
break;
- case "B":
+ case 'B':
+ // next 3 chars => bg color
if (line.length >= i + 4) {
let color = line.substr(i + 1, 3);
state.bg_color = color;
skip = 3;
+ flushPart(); // flush current part when background color changes
}
break;
- case "b":
- // reset bg
- state.bg_color = this.DEFAULT_BG;
+ case 'b':
+ // reset bg to page default
+ state.bg_color = state.default_bg;
+ flushPart(); // flush to allow for ` tags on same line
break;
- case "`":
- // reset all formatting
+ case '`':
state.formatting.bold = false;
state.formatting.underline = false;
state.formatting.italic = false;
- state.fg_color = this.SELECTED_STYLES.plain.fg;
- state.bg_color = this.DEFAULT_BG;
+ state.fg_color = state.default_fg;
+ state.bg_color = state.default_bg;
+ state.align = state.default_align;
+ mode = "text";
+ break;
+ case 'c':
+ state.align = 'center';
+ break;
+ case 'l':
+ state.align = 'left';
+ break;
+ case 'r':
+ state.align = 'right';
+ break;
+ case 'a':
state.align = state.default_align;
break;
- case "c":
- state.align = state.align === "center" ? state.default_align : "center";
- break;
- case "l":
- state.align = state.align === "left" ? state.default_align : "left";
- break;
- case "r":
- state.align = state.align === "right" ? state.default_align : "right";
- break;
- case "a":
- state.align = state.default_align;
- break;
- case "<":
- // Flush current text first
- if (part.length > 0) {
- output.push([this.stateToStyle(state), part]);
- part = "";
- }
- {
- let fieldData = this.parseField(line, i, state);
- if (fieldData) {
- output.push(fieldData.obj);
- i += fieldData.skip;
- continue;
- }
+
+ case '<':
+ // if there's already text, flush it
+ flushPart();
+ let fieldData = this.parseField(line, i, state);
+ if (fieldData) {
+ output.push(fieldData.obj);
+ i += fieldData.skip;
+ // do not i++ here or we'll skip an extra char
+ continue;
}
break;
- case "[":
+ case '[':
// flush current text first
- if (part.length > 0) {
- output.push([this.stateToStyle(state), part]);
- part = "";
- }
- {
- let linkData = this.parseLink(line, i, state);
- if (linkData) {
- output.push(linkData.obj);
- // mode = "text";
- i += linkData.skip;
- continue;
- }
+ flushPart();
+ let linkData = this.parseLink(line, i, state);
+ if (linkData) {
+ output.push(linkData.obj);
+ i += linkData.skip;
+ continue;
}
break;
@@ -546,55 +739,55 @@ class MicronParser {
break;
}
mode = "text";
- if (part.length > 0) {
- // no flush needed, no text added
- }
+ i++;
+ continue;
+
} else {
// mode === "text"
- if (c === "\\") {
- if (escape) {
- // was escaped backslash
- part += c;
- escape = false;
+ if (escape) {
+ part += c;
+ escape = false;
+ } else if (c === '\\') {
+ escape = true;
+ } else if (c === '`') {
+ if (i + 1 < line.length && line[i + 1] === '`') {
+ flushPart();
+ state.formatting.bold = false;
+ state.formatting.underline = false;
+ state.formatting.italic = false;
+ state.fg_color = state.default_fg;
+ state.bg_color = state.default_bg;
+ state.align = state.default_align;
+ i += 2;
+ continue;
} else {
- escape = true;
- }
- } else if (c === "`") {
- if (escape) {
- // just a literal backtick
- part += c;
- escape = false;
- } else {
- // switch to formatting mode
- if (part.length > 0) {
- output.push([this.stateToStyle(state), part]);
- part = "";
- }
+ flushPart();
mode = "formatting";
+ i++;
+ continue;
}
} else {
- if (escape) {
- part += "\\";
- escape = false;
- }
+ // normal text char
part += c;
}
+ i++;
}
-
- i++;
}
-
// end of line
if (part.length > 0) {
- output.push([this.stateToStyle(state), part]);
+ if(this.enableForceMonospace) {
+ output.push([this.stateToStyle(state), this.splitAtSpaces(part)]);
+ } else {
+ output.push([this.stateToStyle(state), part]);
+ }
}
- return output.length > 0 ? output : null;
+ return output;
}
parseField(line, startIndex, state) {
let field_start = startIndex + 1;
- let backtick_pos = line.indexOf("`", field_start);
+ let backtick_pos = line.indexOf('`', field_start);
if (backtick_pos === -1) return null;
let field_content = line.substring(field_start, backtick_pos);
@@ -605,20 +798,20 @@ class MicronParser {
let field_value = "";
let field_prechecked = false;
- if (field_content.includes("|")) {
- let f_components = field_content.split("|");
+ if (field_content.includes('|')) {
+ let f_components = field_content.split('|');
let field_flags = f_components[0];
field_name = f_components[1];
- if (field_flags.includes("^")) {
+ if (field_flags.includes('^')) {
field_type = "radio";
- field_flags = field_flags.replace("^", "");
- } else if (field_flags.includes("?")) {
+ field_flags = field_flags.replace('^', '');
+ } else if (field_flags.includes('?')) {
field_type = "checkbox";
- field_flags = field_flags.replace("?", "");
- } else if (field_flags.includes("!")) {
+ field_flags = field_flags.replace('?', '');
+ } else if (field_flags.includes('!')) {
field_masked = true;
- field_flags = field_flags.replace("!", "");
+ field_flags = field_flags.replace('!', '');
}
if (field_flags.length > 0) {
@@ -633,13 +826,13 @@ class MicronParser {
}
if (f_components.length > 3) {
- if (f_components[3] === "*") {
+ if (f_components[3] === '*') {
field_prechecked = true;
}
}
}
- let field_end = line.indexOf(">", backtick_pos);
+ let field_end = line.indexOf('>', backtick_pos);
if (field_end === -1) return null;
let field_data = line.substring(backtick_pos + 1, field_end);
@@ -653,7 +846,7 @@ class MicronParser {
value: field_value || field_data,
label: field_data,
prechecked: field_prechecked,
- style: style,
+ style: style
};
} else {
obj = {
@@ -662,20 +855,20 @@ class MicronParser {
width: field_width,
masked: field_masked,
data: field_data,
- style: style,
+ style: style
};
}
- let skip = field_end - startIndex + 2;
- return { obj: obj, skip: skip };
+ let skip = (field_end - startIndex);
+ return {obj: obj, skip: skip};
}
parseLink(line, startIndex, state) {
- let endpos = line.indexOf("]", startIndex);
+ let endpos = line.indexOf(']', startIndex);
if (endpos === -1) return null;
let link_data = line.substring(startIndex + 1, endpos);
- let link_components = link_data.split("`");
+ let link_components = link_data.split('`');
let link_label = "";
let link_url = "";
let link_fields = "";
@@ -703,17 +896,41 @@ class MicronParser {
// format the URL
link_url = MicronParser.formatNomadnetworkUrl(link_url);
+ // Apply forceMonospace
+ if(this.enableForceMonospace) {
+ link_label = this.splitAtSpaces(link_label);
+ }
+
let style = this.stateToStyle(state);
let obj = {
type: "link",
url: link_url,
label: link_label,
- fields: link_fields ? link_fields.split("|") : [],
- style: style,
+ fields: (link_fields ? link_fields.split("|") : []),
+ style: style
};
- let skip = endpos - startIndex + 2;
- return { obj: obj, skip: skip };
+ let skip = (endpos - startIndex);
+ return {obj: obj, skip: skip};
+ }
+
+ splitAtSpaces(line) {
+ let out = "";
+ let wordArr = line.split(/(?<= )/g);
+ for (let i = 0; i < wordArr.length; i++) {
+ out += "" + this.forceMonospace(wordArr[i]) + "";
+ }
+ return out;
+ }
+
+ forceMonospace(line) {
+ let out = "";
+ // Properly split compount emoji, source: https://stackoverflow.com/a/71619350
+ let charArr = [...new Intl.Segmenter().segment(line)].map(x => x.segment);
+ for (let char of charArr) {
+ out += "" + char + "";
+ }
+ return out;
}
}