CyberChef/src/web/utils/htmlWidget.mjs

135 lines
3.6 KiB
JavaScript

/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2022
* @license Apache-2.0
*/
import {WidgetType, Decoration, ViewPlugin} from "@codemirror/view";
import {escapeControlChars} from "./editorUtils.mjs";
import {htmlCopyOverride} from "./copyOverride.mjs";
import Utils from "../../core/Utils.mjs";
/**
* Adds an HTML widget to the Code Mirror editor
*/
class HTMLWidget extends WidgetType {
/**
* HTMLWidget consructor
*/
constructor(html, view) {
super();
this.html = html;
this.view = view;
}
/**
* Builds the DOM node
* @returns {DOMNode}
*/
toDOM() {
const wrap = document.createElement("span");
wrap.setAttribute("id", "output-html");
wrap.innerHTML = this.html;
// Find text nodes and replace unprintable chars with control codes
this.walkTextNodes(wrap);
// Add a handler for copy events to ensure the control codes are copied correctly
wrap.addEventListener("copy", htmlCopyOverride);
return wrap;
}
/**
* Walks all text nodes in a given element
* @param {DOMNode} el
*/
walkTextNodes(el) {
for (const node of el.childNodes) {
switch (node.nodeType) {
case Node.TEXT_NODE:
this.replaceControlChars(node);
break;
default:
if (node.nodeName !== "SCRIPT" &&
node.nodeName !== "STYLE")
this.walkTextNodes(node);
break;
}
}
}
/**
* Renders control characters in text nodes
* @param {DOMNode} textNode
*/
replaceControlChars(textNode) {
// .nodeValue unencodes HTML encoding such as &lt; to "<"
// We must remember to escape any potential HTML in TextNodes as we do not
// want to render it.
const textValue = Utils.escapeHtml(textNode.nodeValue);
const val = escapeControlChars(textValue, true, this.view.state.lineBreak);
if (val.length !== textNode.nodeValue.length) {
const node = document.createElement("span");
node.innerHTML = val;
textNode.parentNode.replaceChild(node, textNode);
}
}
}
/**
* 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, view),
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;
}