From fd94d8d29bae0b580f257707ee86857d8757077a Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Sun, 18 Jan 2026 15:55:26 -0600 Subject: [PATCH] Update MicronParser --- meshchatx/src/frontend/js/MicronParser.js | 593 +++++++++++++++------- 1 file changed, 405 insertions(+), 188 deletions(-) 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; } }