diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css
index 625b81f7..ba670f3d 100755
--- a/src/web/stylesheets/layout/_io.css
+++ b/src/web/stylesheets/layout/_io.css
@@ -6,7 +6,8 @@
* @license Apache-2.0
*/
-#input-text {
+#input-text,
+#output-text {
position: relative;
width: 100%;
height: 100%;
@@ -24,23 +25,6 @@
color: var(--fixed-width-font-colour);
}
-#output-text,
-#output-html {
- position: relative;
- width: 100%;
- height: 100%;
- margin: 0;
- padding: 3px;
- -moz-padding-start: 3px;
- -moz-padding-end: 3px;
- border: none;
- border-width: 0px;
- resize: none;
- background-color: transparent;
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
#output-wrapper{
margin: 0;
padding: 0;
@@ -54,13 +38,6 @@
pointer-events: auto;
}
-
-#output-html {
- display: none;
- overflow-y: auto;
- -moz-padding-start: 1px; /* Fixes bug in Firefox */
-}
-
#input-tabs-wrapper #input-tabs,
#output-tabs-wrapper #output-tabs {
list-style: none;
@@ -179,25 +156,15 @@
}
#input-wrapper,
-#output-wrapper,
-#input-wrapper > :not(#input-text),
-#output-wrapper > .textarea-wrapper > div,
-#output-wrapper > .textarea-wrapper > textarea {
+#output-wrapper {
height: calc(100% - var(--title-height));
}
#input-wrapper.show-tabs,
-#input-wrapper.show-tabs > :not(#input-text),
-#output-wrapper.show-tabs,
-#output-wrapper.show-tabs > .textarea-wrapper > div,
-#output-wrapper.show-tabs > .textarea-wrapper > textarea {
+#output-wrapper.show-tabs {
height: calc(100% - var(--tab-height) - var(--title-height));
}
-#output-wrapper > .textarea-wrapper > #output-html {
- height: 100%;
-}
-
#show-file-overlay {
height: 32px;
}
@@ -211,7 +178,6 @@
.textarea-wrapper textarea,
.textarea-wrapper #output-text,
-.textarea-wrapper #output-html,
.textarea-wrapper #output-highlighter {
font-family: var(--fixed-width-font-family);
font-size: var(--fixed-width-font-size);
@@ -477,6 +443,12 @@
/* Status bar */
+.ΝΌ2 .cm-panels {
+ background-color: var(--secondary-background-colour);
+ border-color: var(--secondary-border-colour);
+ color: var(--primary-font-colour);
+}
+
.cm-status-bar {
font-family: var(--fixed-width-font-family);
font-weight: normal;
diff --git a/src/web/utils/editorUtils.mjs b/src/web/utils/editorUtils.mjs
new file mode 100644
index 00000000..fe6b83d4
--- /dev/null
+++ b/src/web/utils/editorUtils.mjs
@@ -0,0 +1,28 @@
+/**
+ * CodeMirror utilities that are relevant to both the input and output
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+
+
+/**
+ * Override for rendering special characters.
+ * Should mirror the toDOM function in
+ * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150
+ * But reverts the replacement of line feeds with newline control pictures.
+ * @param {number} code
+ * @param {string} desc
+ * @param {string} placeholder
+ * @returns {element}
+ */
+export function renderSpecialChar(code, desc, placeholder) {
+ const s = document.createElement("span");
+ // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back.
+ s.textContent = code === 0x0a ? "\u240a" : placeholder;
+ s.title = desc;
+ s.setAttribute("aria-label", desc);
+ s.className = "cm-specialChar";
+ return s;
+}
diff --git a/src/web/utils/htmlWidget.mjs b/src/web/utils/htmlWidget.mjs
new file mode 100644
index 00000000..fbce9b49
--- /dev/null
+++ b/src/web/utils/htmlWidget.mjs
@@ -0,0 +1,87 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+
+import {WidgetType, Decoration, ViewPlugin} from "@codemirror/view";
+
+/**
+ * Adds an HTML widget to the Code Mirror editor
+ */
+class HTMLWidget extends WidgetType {
+
+ /**
+ * HTMLWidget consructor
+ */
+ constructor(html) {
+ super();
+ this.html = html;
+ }
+
+ /**
+ * Builds the DOM node
+ * @returns {DOMNode}
+ */
+ toDOM() {
+ const wrap = document.createElement("span");
+ wrap.setAttribute("id", "output-html");
+ wrap.innerHTML = this.html;
+ return wrap;
+ }
+
+}
+
+/**
+ * Decorator function to provide a set of widgets for the editor DOM
+ * @param {EditorView} view
+ * @param {string} html
+ * @returns {DecorationSet}
+ */
+function decorateHTML(view, html) {
+ const widgets = [];
+ if (html.length) {
+ const deco = Decoration.widget({
+ widget: new HTMLWidget(html),
+ side: 1
+ });
+ widgets.push(deco.range(0));
+ }
+ return Decoration.set(widgets);
+}
+
+
+/**
+ * An HTML Plugin builder
+ * @param {Object} htmlOutput
+ * @returns {ViewPlugin}
+ */
+export function htmlPlugin(htmlOutput) {
+ const plugin = ViewPlugin.fromClass(
+ class {
+ /**
+ * Plugin constructor
+ * @param {EditorView} view
+ */
+ constructor(view) {
+ this.htmlOutput = htmlOutput;
+ this.decorations = decorateHTML(view, this.htmlOutput.html);
+ }
+
+ /**
+ * Editor update listener
+ * @param {ViewUpdate} update
+ */
+ update(update) {
+ if (this.htmlOutput.changed) {
+ this.decorations = decorateHTML(update.view, this.htmlOutput.html);
+ this.htmlOutput.changed = false;
+ }
+ }
+ }, {
+ decorations: v => v.decorations
+ }
+ );
+
+ return plugin;
+}
diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs
new file mode 100644
index 00000000..431d8a3d
--- /dev/null
+++ b/src/web/utils/statusBar.mjs
@@ -0,0 +1,271 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+
+import {showPanel} from "@codemirror/view";
+
+/**
+ * A Status bar extension for CodeMirror
+ */
+class StatusBarPanel {
+
+ /**
+ * StatusBarPanel constructor
+ * @param {Object} opts
+ */
+ constructor(opts) {
+ this.label = opts.label;
+ this.bakeStats = opts.bakeStats ? opts.bakeStats : null;
+ this.eolHandler = opts.eolHandler;
+
+ this.dom = this.buildDOM();
+ }
+
+ /**
+ * Builds the status bar DOM tree
+ * @returns {DOMNode}
+ */
+ buildDOM() {
+ const dom = document.createElement("div");
+ const lhs = document.createElement("div");
+ const rhs = document.createElement("div");
+
+ dom.className = "cm-status-bar";
+ lhs.innerHTML = this.constructLHS();
+ rhs.innerHTML = this.constructRHS();
+
+ dom.appendChild(lhs);
+ dom.appendChild(rhs);
+
+ // Event listeners
+ dom.addEventListener("click", this.eolSelectClick.bind(this), false);
+
+ return dom;
+ }
+
+ /**
+ * Handler for EOL Select clicks
+ * Sets the line separator
+ * @param {Event} e
+ */
+ eolSelectClick(e) {
+ e.preventDefault();
+
+ const eolLookup = {
+ "LF": "\u000a",
+ "VT": "\u000b",
+ "FF": "\u000c",
+ "CR": "\u000d",
+ "CRLF": "\u000d\u000a",
+ "NEL": "\u0085",
+ "LS": "\u2028",
+ "PS": "\u2029"
+ };
+ const eolval = eolLookup[e.target.getAttribute("data-val")];
+
+ // Call relevant EOL change handler
+ this.eolHandler(eolval);
+ }
+
+ /**
+ * Counts the stats of a document
+ * @param {Text} doc
+ */
+ updateStats(doc) {
+ const length = this.dom.querySelector(".stats-length-value"),
+ lines = this.dom.querySelector(".stats-lines-value");
+ length.textContent = doc.length;
+ lines.textContent = doc.lines;
+ }
+
+ /**
+ * Gets the current selection info
+ * @param {EditorState} state
+ * @param {boolean} selectionSet
+ */
+ updateSelection(state, selectionSet) {
+ const selLen = state.selection && state.selection.main ?
+ state.selection.main.to - state.selection.main.from :
+ 0;
+
+ const selInfo = this.dom.querySelector(".sel-info"),
+ curOffsetInfo = this.dom.querySelector(".cur-offset-info");
+
+ if (!selectionSet) {
+ selInfo.style.display = "none";
+ curOffsetInfo.style.display = "none";
+ return;
+ }
+
+ if (selLen > 0) { // Range
+ const start = this.dom.querySelector(".sel-start-value"),
+ end = this.dom.querySelector(".sel-end-value"),
+ length = this.dom.querySelector(".sel-length-value");
+
+ selInfo.style.display = "inline-block";
+ curOffsetInfo.style.display = "none";
+
+ start.textContent = state.selection.main.from;
+ end.textContent = state.selection.main.to;
+ length.textContent = state.selection.main.to - state.selection.main.from;
+ } else { // Position
+ const offset = this.dom.querySelector(".cur-offset-value");
+
+ selInfo.style.display = "none";
+ curOffsetInfo.style.display = "inline-block";
+
+ offset.textContent = state.selection.main.from;
+ }
+ }
+
+ /**
+ * Gets the current character encoding of the document
+ * @param {EditorState} state
+ */
+ updateCharEnc(state) {
+ // const charenc = this.dom.querySelector("#char-enc-value");
+ // TODO
+ // charenc.textContent = "TODO";
+ }
+
+ /**
+ * Returns what the current EOL separator is set to
+ * @param {EditorState} state
+ */
+ updateEOL(state) {
+ const eolLookup = {
+ "\u000a": "LF",
+ "\u000b": "VT",
+ "\u000c": "FF",
+ "\u000d": "CR",
+ "\u000d\u000a": "CRLF",
+ "\u0085": "NEL",
+ "\u2028": "LS",
+ "\u2029": "PS"
+ };
+
+ const val = this.dom.querySelector(".eol-value");
+ val.textContent = eolLookup[state.lineBreak];
+ }
+
+ /**
+ * Sets the latest bake duration
+ */
+ updateBakeStats() {
+ const bakingTime = this.dom.querySelector(".baking-time-value");
+ const bakingTimeInfo = this.dom.querySelector(".baking-time-info");
+
+ if (this.label === "Output" &&
+ this.bakeStats &&
+ typeof this.bakeStats.duration === "number" &&
+ this.bakeStats.duration >= 0) {
+ bakingTimeInfo.style.display = "inline-block";
+ bakingTime.textContent = this.bakeStats.duration;
+ } else {
+ bakingTimeInfo.style.display = "none";
+ }
+ }
+
+ /**
+ * Builds the Left-hand-side widgets
+ * @returns {string}
+ */
+ constructLHS() {
+ return `
+
+ abc
+
+
+
+ sort
+
+
+
+
+ highlight_alt
+ \u279E
+ ( selected)
+
+
+ location_on
+
+ `;
+ }
+
+ /**
+ * Builds the Right-hand-side widgets
+ * Event listener set up in Manager
+ * @returns {string}
+ */
+ constructRHS() {
+ return `
+
+ schedule
+ ms
+
+
+
+ language
+ UTF-16
+
+
+
+
+ keyboard_return
+
+
+
`;
+ }
+
+}
+
+/**
+ * A panel constructor factory building a panel that re-counts the stats every time the document changes.
+ * @param {Object} opts
+ * @returns {Function
}
+ */
+function makePanel(opts) {
+ const sbPanel = new StatusBarPanel(opts);
+
+ return (view) => {
+ sbPanel.updateEOL(view.state);
+ sbPanel.updateCharEnc(view.state);
+ sbPanel.updateBakeStats();
+ sbPanel.updateStats(view.state.doc);
+ sbPanel.updateSelection(view.state, false);
+
+ return {
+ "dom": sbPanel.dom,
+ update(update) {
+ sbPanel.updateEOL(update.state);
+ sbPanel.updateSelection(update.state, update.selectionSet);
+ sbPanel.updateCharEnc(update.state);
+ sbPanel.updateBakeStats();
+ if (update.docChanged) {
+ sbPanel.updateStats(update.state.doc);
+ }
+ }
+ };
+ };
+}
+
+/**
+ * A function that build the extension that enables the panel in an editor.
+ * @param {Object} opts
+ * @returns {Extension}
+ */
+export function statusBar(opts) {
+ const panelMaker = makePanel(opts);
+ return showPanel.of(panelMaker);
+}
diff --git a/src/web/waiters/HighlighterWaiter.mjs b/src/web/waiters/HighlighterWaiter.mjs
index 9f83b55c..d1340165 100755
--- a/src/web/waiters/HighlighterWaiter.mjs
+++ b/src/web/waiters/HighlighterWaiter.mjs
@@ -176,34 +176,16 @@ class HighlighterWaiter {
this.mouseTarget = OUTPUT;
this.removeHighlights();
- const el = e.target;
- const start = el.selectionStart;
- const end = el.selectionEnd;
+ const sel = document.getSelection();
+ const start = sel.baseOffset;
+ const end = sel.extentOffset;
if (start !== 0 || end !== 0) {
- document.getElementById("output-selection-info").innerHTML = this.selectionInfo(start, end);
this.highlightInput([{start: start, end: end}]);
}
}
- /**
- * Handler for output HTML mousedown events.
- * Calculates the current selection info.
- *
- * @param {event} e
- */
- outputHtmlMousedown(e) {
- this.mouseButtonDown = true;
- this.mouseTarget = OUTPUT;
-
- const sel = this._getOutputHtmlSelectionOffsets();
- if (sel.start !== 0 || sel.end !== 0) {
- document.getElementById("output-selection-info").innerHTML = this.selectionInfo(sel.start, sel.end);
- }
- }
-
-
/**
* Handler for input mouseup events.
*
@@ -224,16 +206,6 @@ class HighlighterWaiter {
}
- /**
- * Handler for output HTML mouseup events.
- *
- * @param {event} e
- */
- outputHtmlMouseup(e) {
- this.mouseButtonDown = false;
- }
-
-
/**
* Handler for input mousemove events.
* Calculates the current selection info, and highlights the corresponding data in the output.
@@ -270,37 +242,16 @@ class HighlighterWaiter {
this.mouseTarget !== OUTPUT)
return;
- const el = e.target;
- const start = el.selectionStart;
- const end = el.selectionEnd;
+ const sel = document.getSelection();
+ const start = sel.baseOffset;
+ const end = sel.extentOffset;
if (start !== 0 || end !== 0) {
- document.getElementById("output-selection-info").innerHTML = this.selectionInfo(start, end);
this.highlightInput([{start: start, end: end}]);
}
}
- /**
- * Handler for output HTML mousemove events.
- * Calculates the current selection info.
- *
- * @param {event} e
- */
- outputHtmlMousemove(e) {
- // Check that the left mouse button is pressed
- if (!this.mouseButtonDown ||
- e.which !== 1 ||
- this.mouseTarget !== OUTPUT)
- return;
-
- const sel = this._getOutputHtmlSelectionOffsets();
- if (sel.start !== 0 || sel.end !== 0) {
- document.getElementById("output-selection-info").innerHTML = this.selectionInfo(sel.start, sel.end);
- }
- }
-
-
/**
* Given start and end offsets, writes the HTML for the selection info element with the correct
* padding.
@@ -326,7 +277,6 @@ class HighlighterWaiter {
removeHighlights() {
document.getElementById("input-highlighter").innerHTML = "";
document.getElementById("output-highlighter").innerHTML = "";
- document.getElementById("output-selection-info").innerHTML = "";
}
@@ -379,7 +329,8 @@ class HighlighterWaiter {
const io = direction === "forward" ? "output" : "input";
- document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);
+ // TODO
+ // document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);
this.highlight(
document.getElementById(io + "-text"),
document.getElementById(io + "-highlighter"),
@@ -398,67 +349,67 @@ class HighlighterWaiter {
* @param {number} pos.end - The end offset.
*/
async highlight(textarea, highlighter, pos) {
- if (!this.app.options.showHighlighter) return false;
- if (!this.app.options.attemptHighlight) return false;
+ // if (!this.app.options.showHighlighter) return false;
+ // if (!this.app.options.attemptHighlight) return false;
- // Check if there is a carriage return in the output dish as this will not
- // be displayed by the HTML textarea and will mess up highlighting offsets.
- if (await this.manager.output.containsCR()) return false;
+ // // Check if there is a carriage return in the output dish as this will not
+ // // be displayed by the HTML textarea and will mess up highlighting offsets.
+ // if (await this.manager.output.containsCR()) return false;
- const startPlaceholder = "[startHighlight]";
- const startPlaceholderRegex = /\[startHighlight\]/g;
- const endPlaceholder = "[endHighlight]";
- const endPlaceholderRegex = /\[endHighlight\]/g;
- let text = textarea.value;
+ // const startPlaceholder = "[startHighlight]";
+ // const startPlaceholderRegex = /\[startHighlight\]/g;
+ // const endPlaceholder = "[endHighlight]";
+ // const endPlaceholderRegex = /\[endHighlight\]/g;
+ // // let text = textarea.value; // TODO
- // Put placeholders in position
- // If there's only one value, select that
- // If there are multiple, ignore the first one and select all others
- if (pos.length === 1) {
- if (pos[0].end < pos[0].start) return;
- text = text.slice(0, pos[0].start) +
- startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder +
- text.slice(pos[0].end, text.length);
- } else {
- // O(n^2) - Can anyone improve this without overwriting placeholders?
- let result = "",
- endPlaced = true;
+ // // Put placeholders in position
+ // // If there's only one value, select that
+ // // If there are multiple, ignore the first one and select all others
+ // if (pos.length === 1) {
+ // if (pos[0].end < pos[0].start) return;
+ // text = text.slice(0, pos[0].start) +
+ // startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder +
+ // text.slice(pos[0].end, text.length);
+ // } else {
+ // // O(n^2) - Can anyone improve this without overwriting placeholders?
+ // let result = "",
+ // endPlaced = true;
- for (let i = 0; i < text.length; i++) {
- for (let j = 1; j < pos.length; j++) {
- if (pos[j].end < pos[j].start) continue;
- if (pos[j].start === i) {
- result += startPlaceholder;
- endPlaced = false;
- }
- if (pos[j].end === i) {
- result += endPlaceholder;
- endPlaced = true;
- }
- }
- result += text[i];
- }
- if (!endPlaced) result += endPlaceholder;
- text = result;
- }
+ // for (let i = 0; i < text.length; i++) {
+ // for (let j = 1; j < pos.length; j++) {
+ // if (pos[j].end < pos[j].start) continue;
+ // if (pos[j].start === i) {
+ // result += startPlaceholder;
+ // endPlaced = false;
+ // }
+ // if (pos[j].end === i) {
+ // result += endPlaceholder;
+ // endPlaced = true;
+ // }
+ // }
+ // result += text[i];
+ // }
+ // if (!endPlaced) result += endPlaceholder;
+ // text = result;
+ // }
- const cssClass = "hl1";
+ // const cssClass = "hl1";
- // Remove HTML tags
- text = text
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/\n/g, "
")
- // Convert placeholders to tags
- .replace(startPlaceholderRegex, "")
- .replace(endPlaceholderRegex, "") + " ";
+ // // Remove HTML tags
+ // text = text
+ // .replace(/&/g, "&")
+ // .replace(//g, ">")
+ // .replace(/\n/g, "
")
+ // // Convert placeholders to tags
+ // .replace(startPlaceholderRegex, "")
+ // .replace(endPlaceholderRegex, "") + " ";
- // Adjust width to allow for scrollbars
- highlighter.style.width = textarea.clientWidth + "px";
- highlighter.innerHTML = text;
- highlighter.scrollTop = textarea.scrollTop;
- highlighter.scrollLeft = textarea.scrollLeft;
+ // // Adjust width to allow for scrollbars
+ // highlighter.style.width = textarea.clientWidth + "px";
+ // highlighter.innerHTML = text;
+ // highlighter.scrollTop = textarea.scrollTop;
+ // highlighter.scrollLeft = textarea.scrollLeft;
}
}
diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs
index e8e71b12..0dc44dbe 100644
--- a/src/web/waiters/InputWaiter.mjs
+++ b/src/web/waiters/InputWaiter.mjs
@@ -19,7 +19,8 @@ import {defaultKeymap, insertTab, insertNewline, history, historyKeymap} from "@
import {bracketMatching} from "@codemirror/language";
import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search";
-import {statusBar} from "../extensions/statusBar.mjs";
+import {statusBar} from "../utils/statusBar.mjs";
+import {renderSpecialChar} from "../utils/editorUtils.mjs";
/**
@@ -87,14 +88,17 @@ class InputWaiter {
doc: null,
extensions: [
history(),
- highlightSpecialChars({render: this.renderSpecialChar}),
+ highlightSpecialChars({render: renderSpecialChar}),
drawSelection(),
rectangularSelection(),
crosshairCursor(),
bracketMatching(),
highlightSelectionMatches(),
search({top: true}),
- statusBar(this.inputEditorConf),
+ statusBar({
+ label: "Input",
+ eolHandler: this.eolChange.bind(this)
+ }),
this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping),
this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")),
EditorState.allowMultipleSelections.of(true),
@@ -118,44 +122,10 @@ class InputWaiter {
}
/**
- * Override for rendering special characters.
- * Should mirror the toDOM function in
- * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150
- * But reverts the replacement of line feeds with newline control pictures.
- * @param {number} code
- * @param {string} desc
- * @param {string} placeholder
- * @returns {element}
- */
- renderSpecialChar(code, desc, placeholder) {
- const s = document.createElement("span");
- // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back.
- s.textContent = code === 0x0a ? "\u240a" : placeholder;
- s.title = desc;
- s.setAttribute("aria-label", desc);
- s.className = "cm-specialChar";
- return s;
- }
-
- /**
- * Handler for EOL Select clicks
+ * Handler for EOL change events
* Sets the line separator
- * @param {Event} e
*/
- eolSelectClick(e) {
- e.preventDefault();
-
- const eolLookup = {
- "LF": "\u000a",
- "VT": "\u000b",
- "FF": "\u000c",
- "CR": "\u000d",
- "CRLF": "\u000d\u000a",
- "NEL": "\u0085",
- "LS": "\u2028",
- "PS": "\u2029"
- };
- const eolval = eolLookup[e.target.getAttribute("data-val")];
+ eolChange(eolval) {
const oldInputVal = this.getInput();
// Update the EOL value
diff --git a/src/web/waiters/OptionsWaiter.mjs b/src/web/waiters/OptionsWaiter.mjs
index 52b81ab4..7d9a3e2d 100755
--- a/src/web/waiters/OptionsWaiter.mjs
+++ b/src/web/waiters/OptionsWaiter.mjs
@@ -140,14 +140,11 @@ class OptionsWaiter {
*/
setWordWrap() {
this.manager.input.setWordWrap(this.app.options.wordWrap);
- document.getElementById("output-text").classList.remove("word-wrap");
- document.getElementById("output-html").classList.remove("word-wrap");
+ this.manager.output.setWordWrap(this.app.options.wordWrap);
document.getElementById("input-highlighter").classList.remove("word-wrap");
document.getElementById("output-highlighter").classList.remove("word-wrap");
if (!this.app.options.wordWrap) {
- document.getElementById("output-text").classList.add("word-wrap");
- document.getElementById("output-html").classList.add("word-wrap");
document.getElementById("input-highlighter").classList.add("word-wrap");
document.getElementById("output-highlighter").classList.add("word-wrap");
}
diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs
index 8996edb0..496b0ac5 100755
--- a/src/web/waiters/OutputWaiter.mjs
+++ b/src/web/waiters/OutputWaiter.mjs
@@ -10,6 +10,18 @@ import Dish from "../../core/Dish.mjs";
import FileSaver from "file-saver";
import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs";
+import {
+ EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor
+} from "@codemirror/view";
+import {EditorState, Compartment} from "@codemirror/state";
+import {defaultKeymap} from "@codemirror/commands";
+import {bracketMatching} from "@codemirror/language";
+import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search";
+
+import {statusBar} from "../utils/statusBar.mjs";
+import {renderSpecialChar} from "../utils/editorUtils.mjs";
+import {htmlPlugin} from "../utils/htmlWidget.mjs";
+
/**
* Waiter to handle events related to the output
*/
@@ -25,12 +37,155 @@ class OutputWaiter {
this.app = app;
this.manager = manager;
+ this.outputTextEl = document.getElementById("output-text");
+ // Object to contain bake statistics - used by statusBar extension
+ this.bakeStats = {
+ duration: 0
+ };
+ // Object to handle output HTML state - used by htmlWidget extension
+ this.htmlOutput = {
+ html: "",
+ changed: false
+ };
+ this.initEditor();
+
this.outputs = {};
this.zipWorker = null;
this.maxTabs = this.manager.tabs.calcMaxTabs();
this.tabTimeout = null;
}
+ /**
+ * Sets up the CodeMirror Editor and returns the view
+ */
+ initEditor() {
+ this.outputEditorConf = {
+ eol: new Compartment,
+ lineWrapping: new Compartment
+ };
+
+ const initialState = EditorState.create({
+ doc: null,
+ extensions: [
+ EditorState.readOnly.of(true),
+ htmlPlugin(this.htmlOutput),
+ highlightSpecialChars({render: renderSpecialChar}),
+ drawSelection(),
+ rectangularSelection(),
+ crosshairCursor(),
+ bracketMatching(),
+ highlightSelectionMatches(),
+ search({top: true}),
+ statusBar({
+ label: "Output",
+ bakeStats: this.bakeStats,
+ eolHandler: this.eolChange.bind(this)
+ }),
+ this.outputEditorConf.lineWrapping.of(EditorView.lineWrapping),
+ this.outputEditorConf.eol.of(EditorState.lineSeparator.of("\n")),
+ EditorState.allowMultipleSelections.of(true),
+ keymap.of([
+ ...defaultKeymap,
+ ...searchKeymap
+ ]),
+ ]
+ });
+
+ this.outputEditorView = new EditorView({
+ state: initialState,
+ parent: this.outputTextEl
+ });
+ }
+
+ /**
+ * Handler for EOL change events
+ * Sets the line separator
+ */
+ eolChange(eolval) {
+ const oldOutputVal = this.getOutput();
+
+ // Update the EOL value
+ this.outputEditorView.dispatch({
+ effects: this.outputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval))
+ });
+
+ // Reset the output so that lines are recalculated, preserving the old EOL values
+ this.setOutput(oldOutputVal);
+ }
+
+ /**
+ * Sets word wrap on the output editor
+ * @param {boolean} wrap
+ */
+ setWordWrap(wrap) {
+ this.outputEditorView.dispatch({
+ effects: this.outputEditorConf.lineWrapping.reconfigure(
+ wrap ? EditorView.lineWrapping : []
+ )
+ });
+ }
+
+ /**
+ * Gets the value of the current output
+ * @returns {string}
+ */
+ getOutput() {
+ const doc = this.outputEditorView.state.doc;
+ const eol = this.outputEditorView.state.lineBreak;
+ return doc.sliceString(0, doc.length, eol);
+ }
+
+ /**
+ * Sets the value of the current output
+ * @param {string} data
+ */
+ setOutput(data) {
+ this.outputEditorView.dispatch({
+ changes: {
+ from: 0,
+ to: this.outputEditorView.state.doc.length,
+ insert: data
+ }
+ });
+ }
+
+ /**
+ * Sets the value of the current output to a rendered HTML value
+ * @param {string} html
+ */
+ setHTMLOutput(html) {
+ this.htmlOutput.html = html;
+ this.htmlOutput.changed = true;
+ // This clears the text output, but also fires a View update which
+ // triggers the htmlWidget to render the HTML.
+ this.setOutput("");
+
+ // Execute script sections
+ const scriptElements = document.getElementById("output-html").querySelectorAll("script");
+ for (let i = 0; i < scriptElements.length; i++) {
+ try {
+ eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
+ } catch (err) {
+ log.error(err);
+ }
+ }
+ }
+
+ /**
+ * Clears the HTML output
+ */
+ clearHTMLOutput() {
+ this.htmlOutput.html = "";
+ this.htmlOutput.changed = true;
+ // Fire a blank change to force the htmlWidget to update and remove any HTML
+ this.outputEditorView.dispatch({
+ changes: {
+ from: 0,
+ insert: ""
+ }
+ });
+ }
+
/**
* Calculates the maximum number of tabs to display
*/
@@ -245,8 +400,6 @@ class OutputWaiter {
activeTab = this.manager.tabs.getActiveOutputTab();
if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10);
- const outputText = document.getElementById("output-text");
- const outputHtml = document.getElementById("output-html");
const outputFile = document.getElementById("output-file");
const outputHighlighter = document.getElementById("output-highlighter");
const inputHighlighter = document.getElementById("input-highlighter");
@@ -278,95 +431,68 @@ class OutputWaiter {
} else if (output.status === "error") {
// style the tab if it's being shown
this.toggleLoader(false);
- outputText.style.display = "block";
- outputText.classList.remove("blur");
- outputHtml.style.display = "none";
+ this.outputTextEl.style.display = "block";
+ this.outputTextEl.classList.remove("blur");
outputFile.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
+ this.clearHTMLOutput();
if (output.error) {
- outputText.value = output.error;
+ this.setOutput(output.error);
} else {
- outputText.value = output.data.result;
+ this.setOutput(output.data.result);
}
- outputHtml.innerHTML = "";
} else if (output.status === "baked" || output.status === "inactive") {
document.querySelector("#output-loader .loading-msg").textContent = `Loading output ${inputNum}`;
this.closeFile();
- let scriptElements, lines, length;
if (output.data === null) {
- outputText.style.display = "block";
- outputHtml.style.display = "none";
+ this.outputTextEl.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
- outputText.value = "";
- outputHtml.innerHTML = "";
+ this.clearHTMLOutput();
+ this.setOutput("");
this.toggleLoader(false);
return;
}
+ this.bakeStats.duration = output.data.duration;
+
switch (output.data.type) {
case "html":
- outputText.style.display = "none";
- outputHtml.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.style.display = "none";
inputHighlighter.style.display = "none";
- outputText.value = "";
- outputHtml.innerHTML = output.data.result;
-
- // Execute script sections
- scriptElements = outputHtml.querySelectorAll("script");
- for (let i = 0; i < scriptElements.length; i++) {
- try {
- eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
- } catch (err) {
- log.error(err);
- }
- }
+ this.setHTMLOutput(output.data.result);
break;
case "ArrayBuffer":
- outputText.style.display = "block";
- outputHtml.style.display = "none";
+ this.outputTextEl.style.display = "block";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
- outputText.value = "";
- outputHtml.innerHTML = "";
+ this.clearHTMLOutput();
+ this.setOutput("");
- length = output.data.result.byteLength;
this.setFile(await this.getDishBuffer(output.data.dish), activeTab);
break;
case "string":
default:
- outputText.style.display = "block";
- outputHtml.style.display = "none";
+ this.outputTextEl.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
- outputText.value = Utils.printable(output.data.result, true);
- outputHtml.innerHTML = "";
-
- lines = output.data.result.count("\n") + 1;
- length = output.data.result.length;
+ this.clearHTMLOutput();
+ this.setOutput(output.data.result);
break;
}
this.toggleLoader(false);
- if (output.data.type === "html") {
- const dishStr = await this.getDishStr(output.data.dish);
- length = dishStr.length;
- lines = dishStr.count("\n") + 1;
- }
-
- this.setOutputInfo(length, lines, output.data.duration);
debounce(this.backgroundMagic, 50, "backgroundMagic", this, [])();
}
}.bind(this));
@@ -383,14 +509,13 @@ class OutputWaiter {
// Display file overlay in output area with details
const fileOverlay = document.getElementById("output-file"),
fileSize = document.getElementById("output-file-size"),
- outputText = document.getElementById("output-text"),
fileSlice = buf.slice(0, 4096);
fileOverlay.style.display = "block";
fileSize.textContent = buf.byteLength.toLocaleString() + " bytes";
- outputText.classList.add("blur");
- outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
+ this.outputTextEl.classList.add("blur");
+ this.setOutput(Utils.arrayBufferToStr(fileSlice));
}
/**
@@ -398,7 +523,7 @@ class OutputWaiter {
*/
closeFile() {
document.getElementById("output-file").style.display = "none";
- document.getElementById("output-text").classList.remove("blur");
+ this.outputTextEl.classList.remove("blur");
}
/**
@@ -466,7 +591,6 @@ class OutputWaiter {
clearTimeout(this.outputLoaderTimeout);
const outputLoader = document.getElementById("output-loader"),
- outputElement = document.getElementById("output-text"),
animation = document.getElementById("output-loader-animation");
if (value) {
@@ -483,7 +607,6 @@ class OutputWaiter {
// Show the loading screen
this.outputLoaderTimeout = setTimeout(function() {
- outputElement.disabled = true;
outputLoader.style.visibility = "visible";
outputLoader.style.opacity = 1;
}, 200);
@@ -494,7 +617,6 @@ class OutputWaiter {
animation.removeChild(this.bombeEl);
} catch (err) {}
}.bind(this), 500);
- outputElement.disabled = false;
outputLoader.style.opacity = 0;
outputLoader.style.visibility = "hidden";
}
@@ -717,8 +839,7 @@ class OutputWaiter {
debounce(this.set, 50, "setOutput", this, [inputNum])();
- document.getElementById("output-html").scroll(0, 0);
- document.getElementById("output-text").scroll(0, 0);
+ this.outputTextEl.scroll(0, 0); // TODO
if (changeInput) {
this.manager.input.changeTab(inputNum, false);
@@ -996,32 +1117,6 @@ class OutputWaiter {
}
}
- /**
- * Displays information about the output.
- *
- * @param {number} length - The length of the current output string
- * @param {number} lines - The number of the lines in the current output string
- * @param {number} duration - The length of time (ms) it took to generate the output
- */
- setOutputInfo(length, lines, duration) {
- if (!length) return;
- let width = length.toString().length;
- width = width < 4 ? 4 : width;
-
- const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
- const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " ");
-
- let msg = "time: " + timeStr + "
length: " + lengthStr;
-
- if (typeof lines === "number") {
- const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
- msg += "
lines: " + linesStr;
- }
-
- document.getElementById("output-info").innerHTML = msg;
- document.getElementById("output-selection-info").innerHTML = "";
- }
-
/**
* Triggers the BackgroundWorker to attempt Magic on the current output.
*/
@@ -1111,9 +1206,7 @@ class OutputWaiter {
async displayFileSlice() {
document.querySelector("#output-loader .loading-msg").textContent = "Loading file slice...";
this.toggleLoader(true);
- const outputText = document.getElementById("output-text"),
- outputHtml = document.getElementById("output-html"),
- outputFile = document.getElementById("output-file"),
+ const outputFile = document.getElementById("output-file"),
outputHighlighter = document.getElementById("output-highlighter"),
inputHighlighter = document.getElementById("input-highlighter"),
showFileOverlay = document.getElementById("show-file-overlay"),
@@ -1130,12 +1223,12 @@ class OutputWaiter {
str = Utils.arrayBufferToStr(await this.getDishBuffer(output.dish).slice(sliceFrom, sliceTo));
}
- outputText.classList.remove("blur");
+ this.outputTextEl.classList.remove("blur");
showFileOverlay.style.display = "block";
- outputText.value = Utils.printable(str, true);
+ this.clearHTMLOutput();
+ this.setOutput(str);
- outputText.style.display = "block";
- outputHtml.style.display = "none";
+ this.outputTextEl.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
@@ -1149,9 +1242,7 @@ class OutputWaiter {
async showAllFile() {
document.querySelector("#output-loader .loading-msg").textContent = "Loading entire file at user instruction. This may cause a crash...";
this.toggleLoader(true);
- const outputText = document.getElementById("output-text"),
- outputHtml = document.getElementById("output-html"),
- outputFile = document.getElementById("output-file"),
+ const outputFile = document.getElementById("output-file"),
outputHighlighter = document.getElementById("output-highlighter"),
inputHighlighter = document.getElementById("input-highlighter"),
showFileOverlay = document.getElementById("show-file-overlay"),
@@ -1164,12 +1255,12 @@ class OutputWaiter {
str = Utils.arrayBufferToStr(await this.getDishBuffer(output.dish));
}
- outputText.classList.remove("blur");
+ this.outputTextEl.classList.remove("blur");
showFileOverlay.style.display = "none";
- outputText.value = Utils.printable(str, true);
+ this.clearHTMLOutput();
+ this.setOutput(str);
- outputText.style.display = "block";
- outputHtml.style.display = "none";
+ this.outputTextEl.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
@@ -1185,7 +1276,7 @@ class OutputWaiter {
showFileOverlayClick(e) {
const showFileOverlay = e.target;
- document.getElementById("output-text").classList.add("blur");
+ this.outputTextEl.classList.add("blur");
showFileOverlay.style.display = "none";
this.set(this.manager.tabs.getActiveOutputTab());
}
@@ -1212,7 +1303,7 @@ class OutputWaiter {
* Handler for copy click events.
* Copies the output to the clipboard
*/
- async copyClick() {
+ async copyClick() { // TODO - do we need this?
const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab());
if (dish === null) {
this.app.alert("Could not find data to copy. Has this output been baked yet?", 3000);
diff --git a/tests/browser/nightwatch.js b/tests/browser/nightwatch.js
index ba6f5204..e63a8036 100644
--- a/tests/browser/nightwatch.js
+++ b/tests/browser/nightwatch.js
@@ -90,7 +90,7 @@ module.exports = {
browser
.useCss()
.waitForElementNotVisible("#stale-indicator", 1000)
- .expect.element("#output-text").to.have.property("value").that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e");
+ .expect.element("#output-text").to.have.property("value").that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e"); // TODO
// Clear recipe
browser
@@ -206,7 +206,7 @@ module.exports = {
.useCss()
.waitForElementVisible(".operation .op-title", 1000)
.waitForElementNotVisible("#stale-indicator", 1000)
- .expect.element("#output-text").to.have.property("value").which.matches(/[\da-f-]{36}/);
+ .expect.element("#output-text").to.have.property("value").which.matches(/[\da-f-]{36}/); // TODO
browser.click("#clr-recipe");
},
diff --git a/tests/browser/ops.js b/tests/browser/ops.js
index d0933bb6..64f8e036 100644
--- a/tests/browser/ops.js
+++ b/tests/browser/ops.js
@@ -443,9 +443,9 @@ function testOp(browser, opName, input, output, args=[]) {
bakeOp(browser, opName, input, args);
if (typeof output === "string") {
- browser.expect.element("#output-text").to.have.property("value").that.equals(output);
+ browser.expect.element("#output-text").to.have.property("value").that.equals(output); // TODO
} else if (output instanceof RegExp) {
- browser.expect.element("#output-text").to.have.property("value").that.matches(output);
+ browser.expect.element("#output-text").to.have.property("value").that.matches(output); // TODO
}
}
@@ -463,8 +463,8 @@ function testOpHtml(browser, opName, input, cssSelector, output, args=[]) {
bakeOp(browser, opName, input, args);
if (typeof output === "string") {
- browser.expect.element("#output-html " + cssSelector).text.that.equals(output);
+ browser.expect.element("#output-html " + cssSelector).text.that.equals(output); // TODO
} else if (output instanceof RegExp) {
- browser.expect.element("#output-html " + cssSelector).text.that.matches(output);
+ browser.expect.element("#output-html " + cssSelector).text.that.matches(output); // TODO
}
}