Project init

This commit is contained in:
rfnx
2024-12-27 17:19:45 -05:00
commit 3c91feec8a
3 changed files with 1164 additions and 0 deletions

View File

Binary file not shown.

450
index.html Normal file
View File

@@ -0,0 +1,450 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Micron Playground</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module" src="js/micron-parser.js"></script>
<style>
@font-face {
font-family: 'Roboto Mono Nerd Font';
src: url('/fonts/RobotoMonoNerdFont-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
pre {
font-family: Roboto Mono Nerd Font, monospace;
line-height: normal;
}
</style>
</head>
<body class="bg-[#000] text-white h-screen overflow-hidden">
<div class="flex h-full">
<div class="w-1/2 border-r border-gray-700 ">
<textarea
id="text-editor"
class="w-full h-full bg-[#000] text-white p-4 resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="">
-\u223f
<
`c`!Hello!`! This is output from `*micron`*
Micron generates formatted text for your terminal
`a
-\u223f
<
Nomad Network supports a simple and functional markup language called `*micron`*. If you are familiar with `*markdown`* or `*HTML`*, you will feel right at home writing pages with micron.
With micron you can easily create structured documents and pages with formatting, colors, glyphs and icons, ideal for display in terminals.
>>Recommendations and Requirements
While micron can output formatted text to even the most basic terminal, there's a few capabilities your terminal `*must`* support to display micron output correctly, and some that, while not strictly necessary, make the experience a lot better.
Formatting such as `_underline`_, `!bold`! or `*italics`* will be displayed if your terminal supports it.
If you are having trouble getting micron output to display correctly, try using `*gnome-terminal`* or `*alacritty`*, which should work with all formatting options out of the box. Most other terminals will work fine as well, but you might have to change some settings to get certain formatting to display correctly.
>>>Encoding
All micron sources are intepreted as UTF-8, and micron assumes it can output UTF-8 characters to the terminal. If your terminal does not support UTF-8, output will be faulty.
>>>Colors
Shading and coloring text and backgrounds is integral to micron output, and while micron will attempt to gracefully degrade output even to 1-bit terminals, you will get the best output with terminals supporting at least 256 colors. True-color support is recommended.
>>>Terminal Font
While any unicode capable font can be used with micron, it's highly recommended to use a `*"Nerd Font"`* (see https://www.nerdfonts.com/), which will add a lot of extra glyphs and icons to your output.
> A Few Demo Outputs
`F222`Bddd
`cWith micron, you can control layout and presentation
`a
``
`B33f
You can change background ...
``
`B393
`r`F320... and foreground colors`f
`a
`b
If you want to make a break, horizontal dividers can be inserted. They can be plain, like the one below this text, or you can style them with unicode characters and glyphs, like the wavy divider in the beginning of this document.
-
`cText can be `_underlined`_, `!bold`! or `*italic`*.
You can also `_`*`!`B5d5`F222combine`f`b`_ `_`Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg`` for some fabulous effects.
`a
>>>Sections and Headings
You can define an arbitrary number of sections and sub sections, each with their own named headings. Text inside sections will be automatically indented.
-
If you place a divider inside a section, it will adhere to the section indents.
>>>>>
If no heading text is defined, the section will appear as a sub-section without a header. This can be useful for creating indented blocks of text, like this one.
>Micron tags
Tags are used to format text with micron. Some tags can appear anywhere in text, and some must appear at the beginning of a line. If you need to write text that contains a sequence that would be interpreted as a tag, you can escape it with the character \\.
In the following sections, the different tags will be introduced. Any styling set within micron can be reset to the default style by using the special \\`\\` tag anywhere in the markup, which will immediately remove any formatting previously specified.
>>Alignment
To control text alignment use the tag \\`c to center text, \\`l to left-align, \\`r to right-align, and \\`a to return to the default alignment of the document. Alignment tags must appear at the beginning of a line. Here is an example:
`Faaa
`=
`cThis line will be centered.
So will this.
`aThe alignment has now been returned to default.
`rThis will be aligned to the right
``
`=
``
The above markup produces the following output:
`Faaa`B333
`cThis line will be centered.
So will this.
`aThe alignment has now been returned to default.
`rThis will be aligned to the right
``
>>Formatting
Text can be formatted as `!bold`! by using the \\`! tag, `_underline`_ by using the \\`_ tag and `*italic`* by using the \\`* tag.
Here's an example of formatting text:
`Faaa
`=
We shall soon see `!bold`! paragraphs of text decorated with `_underlines`_ and `*italics`*. Some even dare `!`*`_combine`` them!
`=
``
The above markup produces the following output:
`Faaa`B333
We shall soon see `!bold`! paragraphs of text decorated with `_underlines`_ and `*italics`*. Some even dare `!`*`_combine`!`*`_ them!
``
>>Sections
To create sections and subsections, use the > tag. This tag must be placed at the beginning of a line. To specify a sub-section of any level, use any number of > tags. If text is placed after a > tag, it will be used as a heading.
Here is an example of sections:
`Faaa
`=
>High Level Stuff
This is a section. It contains this text.
>>Another Level
This is a sub section.
>>>Going deeper
A sub sub section. We could continue, but you get the point.
>>>>
Wait! It's worth noting that we can also create sections without headings. They look like this.
`=
``
The above markup produces the following output:
`Faaa`B333
>High Level Stuff
This is a section. It contains this text.
>>Another Level
This is a sub section.
>>>Going deeper
A sub sub section. We could continue, but you get the point.
>>>>
Wait! It's worth noting that we can also create sections without headings. They look like this.
``
>Colors
Foreground colors can be specified with the \\`F tag, followed by three hexadecimal characters. To return to the default foreground color, use the \\`f tag. Background color is specified in the same way, but by using the \\`B and \\`b tags.
Here's a few examples:
`Faaa
`=
You can use `B5d5`F222 color `f`b `Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg`f for some fabulous effects.
`=
``
The above markup produces the following output:
`Faaa`B333
You can use `B5d5`F222 color `f`B333 `Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`F43fi`F70fn`Fe0fg`f for some fabulous effects.
``
>Links
Links to pages, files or other resources can be created with the \\`[ tag, which should always be terminated with a closing ]. You can create links with and without labels, it is up to you to control the formatting of links with other tags. Although not strictly necessary, it is good practice to at least format links with underlining.
Here's a few examples:
`Faaa
`=
Here is a link without any label: `[72914442a3689add83a09a767963f57c:/page/index.mu]
This is a `[labeled link`72914442a3689add83a09a767963f57c:/page/index.mu] to the same page, but it's hard to see if you don't know it
Here is `F00a`_`[a more visible link`72914442a3689add83a09a767963f57c:/page/index.mu]`_`f
`=
``
The above markup produces the following output:
`Faaa`B333
Here is a link without any label: `[72914442a3689add83a09a767963f57c:/page/index.mu]
This is a `[labeled link`72914442a3689add83a09a767963f57c:/page/index.mu] to the same page, but it's hard to see if you don't know it
Here is `F00f`_`[a more visible link`72914442a3689add83a09a767963f57c:/page/index.mu]`_`f
``
When links like these are displayed in the built-in browser, clicking on them or activating them using the keyboard will cause the browser to load the specified URL.
>Fields & Requests
Nomad Network let's you use simple input fields for submitting data to node-side applications. Submitted data, along with other session variables will be available to the node-side script / program as environment variables.
>>Request Links
Links can contain request variables and a list of fields to submit to the node-side application. You can include all fields on the page, only specific ones, and any number of request variables. To simply submit all fields on a page to a specified node-side page, create a link like this:
`Faaa
`=
`[Submit Fields`:/page/fields.mu`*]
`=
``
Note the `!*`! following the extra `!\\``! at the end of the path. This `!*`! denotes `*all fields`*. You can also specify a list of fields to include:
`Faaa
`=
`[Submit Fields`:/page/fields.mu`username|auth_token]
`=
``
If you want to include pre-set variables, you can do it like this:
`Faaa
`=
`[Query the System`:/page/fields.mu`username|auth_token|action=view|amount=64]
`=
``
>> Fields
Here's an example of creating a field. We'll create a field named `!user_input`! and fill it with the text `!Pre-defined data`!. Note that we are using background color tags to make the field more visible to the user:
`Faaa
`=
A simple input field: `B444`<user_input`Pre-defined data>`b
`=
``
You must always set a field `*name`*, but you can of course omit the pre-defined value of the field:
`Faaa
`=
An empty input field: `B444`<demo_empty`>`b
`=
``
You can set the size of the field like this:
`Faaa
`=
A sized input field: `B444`<16|with_size`>`b
`=
``
It is possible to mask fields, for example for use with passwords and similar:
`Faaa
`=
A masked input field: `B444`<!|masked_demo`hidden text>`b
`=
``
And you can of course control all parameters at the same time:
`Faaa
`=
Full control: `B444`<!32|all_options`hidden text>`b
`=
``
Collecting the above markup produces the following output:
`Faaa`B333
A simple input field: `B444`<user_input`Pre-defined data>`B333
An empty input field: `B444`<demo_empty`>`B333
A sized input field: `B444`<16|with_size`>`B333
A masked input field: `B444`<!|masked_demo`hidden text>`B333
Full control: `B444`<!32|all_options`hidden text>`B333
`b
>>> Checkboxes
In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options.
`Faaa
`=
`<?|field_name|value`>`b Label Text`
`=
When the checkbox is checked, it's field will be set to the provided value. If there are multiple checkboxes that share the same field name, the checked values will be concatenated when they are sent to the node by a comma.
``
`B444`<?|sign_up|1`>`b Sign me up`
You can also pre-check both checkboxes and radio groups by appending a |* after the field value.
`B444`<?|checkbox|1|*`>`b Pre-checked checkbox`
>>> Radio groups
Radio groups are another input that lets the user chose from a set of options. Unlike checkboxes, radio buttons with the same field name are mutually exclusive.
Example:
`=
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
`=
will render:
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
In this example, when the data is submitted, `B444` field_color`b will be set to whichever value from the list was selected.
``
>Comments
You can insert comments that will not be displayed in the output by starting a line with the # character.
Here's an example:
`Faaa
`=
# This line will not be displayed
This line will
`=
``
The above markup produces the following output:
`Faaa`B333
# This line will not be displayed
This line will
``
>Literals
To display literal content, for example source-code, or blocks of text that should not be interpreted by micron, you can use literal blocks, specified by the \\`= tag. Below is the source code of this entire document, presented as a literal block.
-
`=
</textarea>
</div>
<div class="w-1/2 ">
<div id="output" class="bg-[#000] text-white p-4 h-full overflow-auto">
</div>
</div>
</div>
<script type="module">
import MicronParser from './js/micron-parser.js';
const parser = new MicronParser(true);
const textEditor = document.getElementById('text-editor');
const output = document.getElementById('output');
const init = () => {
renderOutput();
}
const renderOutput = () => {
const inputText = textEditor.value;
const parsedHtml = parser.convertMicronToHtml(inputText);
output.innerHTML = parsedHtml;
};
init();
textEditor.addEventListener('input', renderOutput);
</script>
</body>
</html>

714
js/micron-parser.js Normal file
View File

@@ -0,0 +1,714 @@
/**
* Micron Parser JavaScript implementation
*
* micron-parser.js is based on MicronParser.py from NomadNet:
* https://raw.githubusercontent.com/markqvist/NomadNet/refs/heads/master/nomadnet/ui/textui/MicronParser.py
*
* 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) {
this.darkTheme = darkTheme;
this.DEFAULT_FG_DARK = "ddd";
this.DEFAULT_FG_LIGHT = "222";
this.DEFAULT_BG = "default";
this.SELECTED_STYLES = null;
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 }
};
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 }
};
if (this.darkTheme) {
this.SELECTED_STYLES = this.STYLES_DARK;
} else {
this.SELECTED_STYLES = this.STYLES_LIGHT;
}
}
static formatNomadnetworkUrl(url) {
return `nomadnetwork://${url}`;
}
convertMicronToHtml(markup) {
let html = "";
let state = {
literal: false,
depth: 0,
fg_color: this.SELECTED_STYLES.plain.fg,
bg_color: this.DEFAULT_BG,
formatting: {
bold: false,
underline: false,
italic: false,
strikethrough: false
},
default_align: "left",
align: "left",
radio_groups: {}
};
const lines = markup.split("\n");
for (let line of lines) {
const lineOutput = this.parseLine(line, state);
if (lineOutput && lineOutput.length > 0) {
for (let el of lineOutput) {
html += el.outerHTML;
}
} else {
html += "<br>";
}
}
return html;
}
parseToHtml(markup) {
// Create a fragment to hold all the Micron output
const fragment = document.createDocumentFragment();
let state = {
literal: false,
depth: 0,
fg_color: this.SELECTED_STYLES.plain.fg,
bg_color: this.DEFAULT_BG,
formatting: {
bold: false,
underline: false,
italic: false,
strikethrough: false
},
default_align: "left",
align: "left",
radio_groups: {}
};
const lines = markup.split("\n");
for (let line of lines) {
const lineOutput = this.parseLine(line, state);
if (lineOutput && lineOutput.length > 0) {
for (let el of lineOutput) {
fragment.appendChild(el);
}
} else {
fragment.appendChild(document.createElement("br"));
}
}
return fragment;
}
parseLine(line, state) {
if (line.length > 0) {
// Check literals toggle
if (line === "`=") {
state.literal = !state.literal;
return null;
}
if (!state.literal) {
// Comments
if (line[0] === "#") {
return null;
}
// Reset section depth
if (line[0] === "<") {
state.depth = 0;
return this.parseLine(line.slice(1), state);
}
// Section headings
if (line[0] === ">") {
let i = 0;
while (i < line.length && line[i] === ">") {
i++;
}
state.depth = i;
let headingLine = line.slice(i);
if (headingLine.length > 0) {
// apply heading style if it exists
let style = null;
let wanted_style = "heading" + i;
if (this.SELECTED_STYLES[wanted_style]) {
style = this.SELECTED_STYLES[wanted_style];
} else {
style = this.SELECTED_STYLES.plain;
}
const latched_style = this.stateToStyle(state);
this.styleToState(style, state);
let outputParts = this.makeOutput(state, headingLine);
this.styleToState(latched_style, state);
// wrap in a heading container
if (outputParts && outputParts.length > 0) {
const div = document.createElement("div");
this.applyAlignment(div, state);
this.applySectionIndent(div, state);
// merge text nodes
this.appendOutput(div, outputParts, state);
return [div];
} else {
return null;
}
} else {
return null;
}
}
// horizontal dividers
if (line[0] === "-") {
let dividerChar = "\u2500";
// if second char given
if (line.length > 1) {
dividerChar = line[1];
}
const hr = document.createElement("hr");
this.applySectionIndent(hr, state);
return [hr];
}
}
let outputParts = this.makeOutput(state, line);
if (outputParts) {
// outputParts can contain text (tuple) and special objects (fields/checkbox)
// construct a single line container
let container = document.createElement("div");
this.applyAlignment(container, state);
this.applySectionIndent(container, state);
this.appendOutput(container, outputParts, state);
return [container];
} else {
// Just empty line
return [document.createElement("br")];
}
} else {
// Empty line
return [document.createElement("br")];
}
}
applyAlignment(el, state) {
// use CSS text-align for alignment
el.style.textAlign = state.align || "left";
}
applySectionIndent(el, state) {
// indent by state.depth
let indent = (state.depth - 1) * 2;
if (indent > 0) {
el.style.marginLeft = (indent * 10) + "px";
}
}
// convert current state to a style object
stateToStyle(state) {
return {
fg: state.fg_color,
bg: state.bg_color,
bold: state.formatting.bold,
underline: state.formatting.underline,
italic: state.formatting.italic
};
}
styleToState(style, state) {
if (style.fg !== undefined && style.fg !== null) state.fg_color = style.fg;
if (style.bg !== undefined && style.bg !== null) state.bg_color = style.bg;
if (style.bold !== undefined && style.bold !== null) state.formatting.bold = style.bold;
if (style.underline !== undefined && style.underline !== null) state.formatting.underline = style.underline;
if (style.italic !== undefined && style.italic !== null) state.formatting.italic = style.italic;
}
appendOutput(container, parts, state) {
let currentSpan = null;
let currentStyle = null;
const flushSpan = () => {
if (currentSpan) {
container.appendChild(currentSpan);
currentSpan = null;
currentStyle = null;
}
};
for (let p of parts) {
if (typeof p === 'string') {
let span = document.createElement("span");
span.textContent = p;
container.appendChild(span);
} else if (Array.isArray(p) && p.length === 2) {
// tuple: [styleSpec, text]
let [styleSpec, text] = p;
// if different style, flush currentSpan
if (!this.stylesEqual(styleSpec, currentStyle)) {
flushSpan();
currentSpan = document.createElement("span");
this.applyStyleToElement(currentSpan, styleSpec);
currentStyle = styleSpec;
}
currentSpan.textContent += 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.value = p.data;
if (p.width) {
input.size = p.width;
}
this.applyStyleToElement(input, this.styleFromState(p.style));
container.appendChild(input);
} else if (p.type === "checkbox") {
let label = document.createElement("label");
let cb = document.createElement("input");
cb.type = "checkbox";
cb.name = p.name;
cb.value = p.value;
if (p.prechecked) cb.checked = true;
label.appendChild(cb);
label.appendChild(document.createTextNode(" " + p.label));
this.applyStyleToElement(label, this.styleFromState(p.style));
container.appendChild(label);
} else if (p.type === "radio") {
let label = document.createElement("label");
let rb = document.createElement("input");
rb.type = "radio";
rb.name = p.name;
rb.value = p.value;
if (p.prechecked) rb.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://', '');
// use p.url as is for the href
const formattedUrl = p.url;
let a = document.createElement("a");
a.href = formattedUrl;
a.title = formattedUrl;
let fieldsToSubmit = [];
let requestVars = {};
let foundAll = false;
if (p.fields && p.fields.length > 0) {
for (const f of p.fields) {
if (f === '*') {
// submit all fields
foundAll = true;
} else if (f.includes('=')) {
// this is a request variable (key=value)
const [k, v] = f.split('=');
requestVars[k] = v;
} else {
// this is a field name to submit
fieldsToSubmit.push(f);
}
}
let fieldStr = '';
if (foundAll) {
// if '*' was found, submit all fields
fieldStr = '*';
} else {
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('|');
directURL += directURL.includes('`') ? `|${queryString}` : `\`${queryString}`;
}
a.setAttribute("onclick", `event.preventDefault(); onNodePageUrlClick('${directURL}', '${fieldStr}', false, false)`);
} else {
// no fields or request variables, just handle the direct URL
a.setAttribute("onclick", `event.preventDefault(); onNodePageUrlClick('${directURL}', null, false, false)`);
}
a.textContent = p.label;
this.applyStyleToElement(a, this.styleFromState(p.style));
container.appendChild(a);
}
}
}
flushSpan();
}
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);
}
styleFromState(stateStyle) {
// stateStyle is a name of a style or a style object
// in this code, p.style is actually a style name. j,ust return that
return stateStyle;
}
applyStyleToElement(el, style) {
if (!style) return;
// convert style fg/bg to colors
let fgColor = this.colorToCss(style.fg);
let bgColor = this.colorToCss(style.bg);
if (fgColor && fgColor !== "default") {
el.style.color = fgColor;
}
if (bgColor && bgColor !== "default") {
el.style.backgroundColor = bgColor;
}
if (style.bold) {
el.style.fontWeight = "bold";
}
if (style.underline) {
el.style.textDecoration = (el.style.textDecoration ? el.style.textDecoration + " underline" : "underline");
}
if (style.italic) {
el.style.fontStyle = "italic";
}
}
colorToCss(c) {
if (!c || c === "default") return null;
// if 3 hex chars (like '222') => expand to #222
if (c.length === 3 && /^[0-9a-fA-F]{3}$/.test(c)) {
return "#" + c;
}
// If 6 hex chars
if (c.length === 6 && /^[0-9a-fA-F]{6}$/.test(c)) {
return "#" + c;
}
// If grayscale 'gxx'
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');
return "#" + h + h + h;
}
// fallback: just return a known CSS color or tailwind class if not known
return null;
}
makeOutput(state, line) {
if (state.literal) {
// literal mode: output as is, except if `= line
if (line === "\\`=") {
line = "`=";
}
return [[this.stateToStyle(state), line]];
}
let output = [];
let part = "";
let mode = "text";
let escape = false;
let skip = 0;
let i = 0;
while (i < line.length) {
let c = line[i];
if (skip > 0) {
skip--;
i++;
continue;
}
if (mode === "formatting") {
// Handle formatting commands
switch(c) {
case '_':
state.formatting.underline = !state.formatting.underline;
break;
case '!':
state.formatting.bold = !state.formatting.bold;
break;
case '*':
state.formatting.italic = !state.formatting.italic;
break;
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;
break;
case 'B':
if (line.length >= i+4) {
let color = line.substr(i+1,3);
state.bg_color = color;
skip = 3;
}
break;
case 'b':
// reset bg
state.bg_color = this.DEFAULT_BG;
break;
case '`':
// reset all formatting
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.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;
}
break;
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;
}
break;
default:
// unknown formatting char, ignore
break;
}
mode = "text";
if (part.length > 0) {
// no flush needed, no text added
}
} else {
// mode === "text"
if (c === '\\') {
if (escape) {
// was escaped backslash
part += c;
escape = false;
} 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 = "";
}
mode = "formatting";
}
} else {
if (escape) {
part += '\\';
escape = false;
}
part += c;
}
}
i++;
}
// end of line
if (part.length > 0) {
output.push([this.stateToStyle(state), part]);
}
return (output.length > 0) ? output : null;
}
parseField(line, startIndex, state) {
let field_start = startIndex + 1;
let backtick_pos = line.indexOf('`', field_start);
if (backtick_pos === -1) return null;
let field_content = line.substring(field_start, backtick_pos);
let field_masked = false;
let field_width = 24;
let field_type = "field";
let field_name = field_content;
let field_value = "";
let field_prechecked = false;
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('^')) {
field_type = "radio";
field_flags = field_flags.replace('^','');
} else if (field_flags.includes('?')) {
field_type = "checkbox";
field_flags = field_flags.replace('?','');
} else if (field_flags.includes('!')) {
field_masked = true;
field_flags = field_flags.replace('!','');
}
if (field_flags.length > 0) {
let w = parseInt(field_flags,10);
if (!isNaN(w)) {
field_width = Math.min(w,256);
}
}
if (f_components.length > 2) {
field_value = f_components[2];
}
if (f_components.length > 3) {
if (f_components[3] === '*') {
field_prechecked = true;
}
}
}
let field_end = line.indexOf('>', backtick_pos);
if (field_end === -1) return null;
let field_data = line.substring(backtick_pos+1, field_end);
let style = this.stateToStyle(state);
let obj = null;
if (field_type === "checkbox" || field_type === "radio") {
obj = {
type: field_type,
name: field_name,
value: field_value || field_data,
label: field_data,
prechecked: field_prechecked,
style: style
};
} else {
obj = {
type: "field",
name: field_name,
width: field_width,
masked: field_masked,
data: field_data,
style: style
};
}
let skip = (field_end - startIndex) + 2;
return { obj: obj, skip: skip };
}
parseLink(line, startIndex, state) {
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_label = "";
let link_url = "";
let link_fields = "";
if (link_components.length === 1) {
link_label = "";
link_url = link_data;
} else if (link_components.length === 2) {
link_label = link_components[0];
link_url = link_components[1];
} else if (link_components.length === 3) {
link_label = link_components[0];
link_url = link_components[1];
link_fields = link_components[2];
}
if (link_url.length === 0) {
return null;
}
if (link_label === "") {
link_label = link_url;
}
// format the URL
link_url = MicronParser.formatNomadnetworkUrl(link_url);
let style = this.stateToStyle(state);
let obj = {
type: "link",
url: link_url,
label: link_label,
fields: (link_fields ? link_fields.split("|") : []),
style: style
};
let skip = (endpos - startIndex) + 2;
return { obj: obj, skip: skip };
}
}
export default MicronParser;