diff --git a/evennia/web/static/webclient/js/plugins/html.js b/evennia/web/static/webclient/js/plugins/html.js
new file mode 100644
index 0000000000..3ae2666cbf
--- /dev/null
+++ b/evennia/web/static/webclient/js/plugins/html.js
@@ -0,0 +1,63 @@
+/*
+ * Evennia server-side-generated "raw" HTML message handler plugin
+ *
+ * PLUGIN ORDER PREREQS:
+ * loaded after:
+ * webclient_gui.js
+ * option2.js
+ * loaded before:
+ *
+ *
+ * To use, at minimum, in evennia python code:
+ * target.msg( html="
...etc...
" )
+ *
+ * or, if you prefer tagged routing (RECOMMENDED):
+ * target.msg( html=("...etc...
",{'type':'tag'}) )
+ *
+ */
+let html_plugin = (function () {
+ //
+ var html = function (args, kwargs) {
+ let options = window.options;
+ if( !("html" in options) || options["html"] === false ) { return; }
+
+ var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
+ mwins.forEach( function (mwin) {
+ mwin.append(args[0]);
+ mwin.scrollTop(mwin[0].scrollHeight);
+ });
+ }
+
+ //
+ var onOptionsUI = function (parentdiv) {
+ let options = window.options;
+ var checked;
+
+ checked = options["html"] ? "checked='checked'" : "";
+ var mmHtml = $( [ ""
+ ].join("") );
+ mmHtml.on("change", window.plugins["options2"].onOptionCheckboxChanged);
+
+ parentdiv.append(mmHtml);
+ }
+
+ //
+ // Mandatory plugin init function
+ var init = function () {
+ let options = window.options;
+ options["html"] = true;
+
+ let Evennia = window.Evennia;
+ Evennia.emitter.on("html", html); // capture "image" commands
+ console.log('HTML plugin initialized');
+ }
+
+ return {
+ init: init,
+ onOptionsUI: onOptionsUI,
+ }
+})();
+plugin_handler.add("html_plugin", html_plugin);
diff --git a/evennia/web/static/webclient/js/plugins/text2html.js b/evennia/web/static/webclient/js/plugins/text2html.js
new file mode 100644
index 0000000000..f0593b1d46
--- /dev/null
+++ b/evennia/web/static/webclient/js/plugins/text2html.js
@@ -0,0 +1,348 @@
+
+/*
+ *
+ * Evennia Webclient text2html component
+ *
+ * This is used in conjunction with the main evennia.js library, which
+ * handles all the communication with the Server.
+ *
+ */
+
+//
+// Global Parser for text2html
+//
+var text2html_plugin = (function () {
+ "use strict"
+
+ let asciiESC = String.fromCharCode(27); // string literal for ASCII ESC bytes
+
+ let foreground = "color-102"; // state tracker for foreground css
+ let background = ""; // state tracker for background css
+ let underline = ""; // state tracker for underlines css
+
+ let pipecodes = /^(\|\[?[0-5][0-5][0-5])|(\|\[[0-9][0-9]?m)|(\|\[?=[a-z])|(\|[![]?[unrgybmcwxhRGYBMCWXH_/>*^-])/;
+ // example ^|000 or ^|[22m or ^|=a or ^|_
+
+ let csslookup = {
+ "|n": "normal",
+ "|r": "color-009",
+ "|g": "color-010",
+ "|y": "color-011",
+ "|b": "color-012",
+ "|m": "color-013",
+ "|c": "color-014",
+ "|w": "color-015",
+ "|x": "color-008",
+ "|R": "color-001",
+ "|G": "color-002",
+ "|Y": "color-003",
+ "|B": "color-004",
+ "|M": "color-005",
+ "|C": "color-006",
+ "|W": "color-007",
+ "|X": "color-000",
+ "|[r": "bgcolor-196",
+ "|[g": "bgcolor-046",
+ "|[y": "bgcolor-226",
+ "|[b": "bgcolor-021",
+ "|[m": "bgcolor-201",
+ "|[c": "bgcolor-051",
+ "|[w": "bgcolor-231",
+ "|[x": "bgcolor-102",
+ "|[R": "bgcolor-001",
+ "|[G": "bgcolor-002",
+ "|[Y": "bgcolor-003",
+ "|[B": "bgcolor-004",
+ "|[M": "bgcolor-005",
+ "|[C": "bgcolor-006",
+ "|[W": "bgcolor-007",
+ "|[X": "bgcolor-000",
+ "|=a": "color-016",
+ "|=b": "color-232",
+ "|=c": "color-233",
+ "|=d": "color-234",
+ "|=e": "color-235",
+ "|=f": "color-236",
+ "|=g": "color-237",
+ "|=h": "color-238",
+ "|=i": "color-239",
+ "|=j": "color-240",
+ "|=k": "color-241",
+ "|=l": "color-242",
+ "|=m": "color-243",
+ "|=n": "color-244",
+ "|=o": "color-245",
+ "|=p": "color-246",
+ "|=q": "color-247",
+ "|=r": "color-248",
+ "|=s": "color-249",
+ "|=t": "color-250",
+ "|=u": "color-251",
+ "|=v": "color-252",
+ "|=w": "color-253",
+ "|=x": "color-254",
+ "|=y": "color-255",
+ "|=z": "color-231",
+ "|[=a": "bgcolor-016",
+ "|[=b": "bgcolor-232",
+ "|[=c": "bgcolor-233",
+ "|[=d": "bgcolor-234",
+ "|[=e": "bgcolor-235",
+ "|[=f": "bgcolor-236",
+ "|[=g": "bgcolor-237",
+ "|[=h": "bgcolor-238",
+ "|[=i": "bgcolor-239",
+ "|[=j": "bgcolor-240",
+ "|[=k": "bgcolor-241",
+ "|[=l": "bgcolor-242",
+ "|[=m": "bgcolor-243",
+ "|[=n": "bgcolor-244",
+ "|[=o": "bgcolor-245",
+ "|[=p": "bgcolor-246",
+ "|[=q": "bgcolor-247",
+ "|[=r": "bgcolor-248",
+ "|[=s": "bgcolor-249",
+ "|[=t": "bgcolor-250",
+ "|[=u": "bgcolor-251",
+ "|[=v": "bgcolor-252",
+ "|[=w": "bgcolor-253",
+ "|[=x": "bgcolor-254",
+ "|[=y": "bgcolor-255",
+ "|[=z": "bgcolor-231",
+ // not sure what these nexts ones are actually supposed to map to
+ "|[0m": "normal",
+ "|[1m": "normal",
+ "|[22m": "normal",
+ "|[36m": "color-006",
+ "|[37m": "color-015",
+ }
+
+ function ascii (l) {
+ let a = new String(l); // force string
+ return a.charCodeAt(0);
+ }
+
+ // dumb convert any leading or trailing spaces/tabs to sequences
+ var handleSpaces = function (text) {
+ // TODO should probably get smart about replacing spaces inside "normal" text
+ return text.replace( /\t/g, " ").replace( / /g, " ");
+ }
+
+
+ // javascript doesn't have a native sprintf-like function
+ function zfill(string, size) {
+ while (string.length < size) string = "0" + string;
+ return string;
+ }
+
+
+ // given string starting with a pipecode |xx
+ // return the css (if any) and text remainder
+ var pipe2css = function(pipecode) {
+ let regx = "";
+ let css = "color-102";
+
+ regx = /^(\|\[?[nrgybmcwxhRGYBMCWX])/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return csslookup[ css[1] ];
+ }
+
+ regx = /^(\|\[?=[a-z])/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return csslookup[ css[1] ];
+ }
+
+ regx = /^(\|\[[0-9][0-9]?m)/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return csslookup[ css[1] ];
+ }
+
+ regx = /^(\|n)/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return "normal";
+ }
+
+ regx = /^(\|u)/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return "underline";
+ }
+
+ regx = /^\|([0-5][0-5][0-5])/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return "color-" + zfill( (parseInt(css[1], 6) + 16).toString(), 3);
+ }
+
+ regx = /^\|\[([0-5][0-5][0-5])/;
+ css = pipecode.match( regx );
+ if( css != null ) {
+ return "bgcolor-" + zfill( (parseInt(css[1], 6) + 16).toString(), 3);
+ }
+
+ return css;
+ }
+
+
+ // convert any HTML sensitive characters to &code; format
+ var htmlEscape = function (text) {
+ text = text.replace(/&/g, "&");
+ text = text.replace(//g, ">");
+ text = text.replace(/"/g, """);
+ text = text.replace(/'/g, "'");
+ text = handleSpaces(text);
+ return text;
+ }
+
+
+ // track stateful CSS
+ var trackCSS = function (css) {
+ if( (typeof css !== 'string') && ! (css instanceof String) ) {
+ css = "";
+ }
+
+ if( css.startsWith( "color-" ) ) {
+ foreground = css;
+ }
+
+ if( css.startsWith( "bgcolor-" ) ) {
+ background = css;
+ }
+
+ if( css === "underline" ) {
+ underline = css;
+ }
+
+ if( css === "normal" ) {
+ foreground = "color-102";
+ background = "";
+ underline = "";
+ }
+
+ return foreground + " " + background + " " + underline ;
+ }
+
+
+ /*
+ *
+ */
+ var parse2HTML = function (text) {
+ let html = "";
+ foreground = "color-102"; // state tracker for foreground css
+ background = ""; // state tracker for background css
+ underline = ""; // state tracker for underlines css
+
+ // HACK: parse TELNET ASCII byte-by-byte, convert ESC's to |'s -- serverside "raw" bug?
+ // Bug is further proven out by the fact that |'s don't come through as doubles.
+ let hack = new RegExp( String.fromCharCode(27) );
+ if( text.match( hack ) ) {
+ let chars = text.split(''); // to characters
+ for( let n=0; n's with no HTML height
+ if( string === "" ) {
+ html += "
";
+ continue;
+ }
+
+ html += "";
+
+ let spans = string.split( "|" );
+
+ // this split means there are 2 possible cases
+
+ // possibly-0-length leading span with "default" styling (may be the ONLY span)
+ let span = spans[0].replaceAll("|", "|"); // avoid htmlEscape mangling literal |'s
+ if( span.length > 0 ) {
+ html += "" + htmlEscape(span) + "";
+ }
+
+ // "standard" span array of [xxxtext1, xxtext2, xxtext3], where x's are pipe-codes
+ for( let n=1; n" + htmlEscape(remainder) + "";
+ }
+
+ html += "
";
+ }
+
+ return html;
+ }
+
+ // The Main text2html message capture fuction -- calls parse2HTML()
+ var text2html = function (args, kwargs) {
+ let options = window.options;
+ if( !("text2html" in options) || options["text2html"] === false ) { return; }
+
+ var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
+ mwins.forEach( function (mwin) {
+ mwin.append( parse2HTML( args[0]) ); // the heavy workload
+ mwin.scrollTop(mwin[0].scrollHeight);
+ });
+ }
+
+ //
+ var onOptionsUI = function (parentdiv) {
+ let options = window.options;
+ var checked;
+
+ checked = options["text2html"] ? "checked='checked'" : "";
+ var text2html = $( [ ""
+ ].join("") );
+ text2html.on("change", window.plugins["options2"].onOptionCheckboxChanged);
+
+ parentdiv.append(text2html);
+ }
+
+ //
+ // Mandatory plugin init function
+ var init = function () {
+ let options = window.options;
+ options["text2html"] = true;
+
+ let Evennia = window.Evennia;
+ Evennia.emitter.on("text2html", text2html); // capture "text2html" outfunc events
+
+ console.log('Text2Html plugin initialized');
+ }
+
+ return {
+ init: init,
+ onOptionsUI: onOptionsUI,
+ parse2HTML: parse2HTML,
+ }
+})();
+plugin_handler.add("text2html_plugin", text2html_plugin);
diff --git a/evennia/web/templates/webclient/base.html b/evennia/web/templates/webclient/base.html
index 96af22340c..ca8460c242 100644
--- a/evennia/web/templates/webclient/base.html
+++ b/evennia/web/templates/webclient/base.html
@@ -100,6 +100,8 @@ JQuery available.
+
+
{% endblock %}