Update MicronParser

This commit is contained in:
Sudo-Ivan
2026-01-18 15:55:26 -06:00
parent aa8575e367
commit fd94d8d29b
+405 -188
View File
@@ -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 += "<br>";
}
}
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 `<p style="color: red;"> ⚠ DOMPurify is not installed. Include it above micron-parser.js or run npm install dompurify </p>`;
}
}
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 <hr>
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 += "<span class='Mu-mws'>" + this.forceMonospace(wordArr[i]) + "</span>";
}
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 += "<span class='Mu-mnt'>" + char + "</span>";
}
return out;
}
}