mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-23 23:35:08 +00:00
Update MicronParser
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user