From 903b0905432ca4e160e482c4c9c3157f74762b1a Mon Sep 17 00:00:00 2001 From: moisseev Date: Tue, 14 Jul 2020 11:47:12 +0300 Subject: [PATCH] [WebUI] Add map editor (requires a modern browser) --- .stylelintrc.json | 3 +- interface/css/prism.css | 161 ++++++++++++++++++++++++++++ interface/css/rspamd.css | 32 +++++- interface/index.html | 1 + interface/js/app/config.js | 44 ++++++-- interface/js/lib/codejar.min.js | 5 + interface/js/lib/linenumbers.min.js | 5 + interface/js/lib/prism.js | 5 + interface/js/main.js | 8 +- package.json | 3 +- 10 files changed, 254 insertions(+), 13 deletions(-) create mode 100644 interface/css/prism.css create mode 100644 interface/js/lib/codejar.min.js create mode 100644 interface/js/lib/linenumbers.min.js create mode 100644 interface/js/lib/prism.js diff --git a/.stylelintrc.json b/.stylelintrc.json index b25642b8f..be1ea613f 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -4,7 +4,8 @@ "**/*.min.css", "**/*.min.js", "interface/css/d3evolution.css", - "interface/css/nprogress.css" + "interface/css/nprogress.css", + "interface/css/prism.css" ], "rules": { "at-rule-empty-line-before": null, diff --git a/interface/css/prism.css b/interface/css/prism.css new file mode 100644 index 000000000..93e23c3b6 --- /dev/null +++ b/interface/css/prism.css @@ -0,0 +1,161 @@ +/* PrismJS 1.20.0 +https://prismjs.com/download.html#themes=prism-okaidia&languages=clike&plugins=show-invisibles */ +/** + * okaidia theme for JavaScript, CSS and HTML + * Loosely based on Monokai textmate theme by http://www.monokai.nl/ + * @author ocodia + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #f8f8f2; + background: none; + text-shadow: 0 1px rgba(0, 0, 0, 0.3); + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + border-radius: 0.3em; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #272822; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #8292a2; +} + +.token.punctuation { + color: #f8f8f2; +} + +.token.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.constant, +.token.symbol, +.token.deleted { + color: #f92672; +} + +.token.boolean, +.token.number { + color: #ae81ff; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #a6e22e; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + color: #f8f8f2; +} + +.token.atrule, +.token.attr-value, +.token.function, +.token.class-name { + color: #e6db74; +} + +.token.keyword { + color: #66d9ef; +} + +.token.regex, +.token.important { + color: #fd971f; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.tab:not(:empty), +.token.cr, +.token.lf, +.token.space { + position: relative; +} + +.token.tab:not(:empty):before, +.token.cr:before, +.token.lf:before, +.token.space:before { + color: #808080; + opacity: 0.6; + position: absolute; +} + +.token.tab:not(:empty):before { + content: '\21E5'; +} + +.token.cr:before { + content: '\240D'; +} + +.token.crlf:before { + content: '\240D\240A'; +} +.token.lf:before { + content: '\240A'; +} + +.token.space:before { + content: '\00B7'; +} + diff --git a/interface/css/rspamd.css b/interface/css/rspamd.css index 4330962b0..5fefb4b9a 100644 --- a/interface/css/rspamd.css +++ b/interface/css/rspamd.css @@ -285,9 +285,6 @@ table#symbolsTable input[type="number"] { background-color: #cddbff; } -#map-textarea { - height: 360px; -} td.maps-cell { vertical-align: middle; } @@ -536,3 +533,32 @@ td.maps-cell { #clusterTable tr:last-child td:last-child { border-radius: 0 0 calc(.25rem - 1px) 0; } + +textarea#editor { + height: calc(100vh - 178px); +} +.codejar-wrap { + background: rgb(0, 47, 79); + border-radius: 6px; + max-height: calc(100vh - 178px); + overflow-y: auto; +} +.codejar-linenumbers { + background: rgba(255, 255, 255, 0.07) !important; + bottom: unset !important; + color: rgba(120, 120, 120, 1) !important; + mix-blend-mode: unset !important; + text-align: right; + overflow: unset !important; +} +.editor { + color: #fff; + font-family: monospace; + font-size: 14px; + font-weight: 400; + letter-spacing: normal; + min-height: 1.5em; + resize: unset !important; + tab-size: 4; + overflow-y: visible !important; +} diff --git a/interface/index.html b/interface/index.html index 312a1956e..c7b941c7e 100644 --- a/interface/index.html +++ b/interface/index.html @@ -21,6 +21,7 @@ + diff --git a/interface/js/app/config.js b/interface/js/app/config.js index 986db5748..2c65bfff1 100644 --- a/interface/js/app/config.js +++ b/interface/js/app/config.js @@ -22,8 +22,8 @@ THE SOFTWARE. */ -define(["jquery"], - function ($) { +define(["jquery", "codejar", "linenumbers", "prism"], + function ($, CodeJar, withLineNumbers, Prism) { "use strict"; var ui = {}; @@ -155,6 +155,22 @@ define(["jquery"], }; ui.setup = function (rspamd) { + var jar = {}; + // CodeJar requires ES6 + var editor = window.CodeJar && + // Required to restore cursor position + (typeof window.getSelection().setBaseAndExtent === "function") + ? { + codejar: true, + elt: "div", + class: "editor language-clike", + } + // Fallback to textarea if the browser does not support ES6 + : { + elt: "textarea", + class: "form-control map-textarea", + }; + // Modal form for maps $(document).on("click", "[data-toggle=\"modal\"]", function () { var checked_server = rspamd.getSelector("selSrv"); @@ -176,10 +192,19 @@ define(["jquery"], } $("#modalDialog .modal-header").find("[data-fa-i2svg]").addClass(icon); $("#modalTitle").html(item.uri); - $('").appendTo("#modalBody"); + "").appendTo("#modalBody"); + + if (editor.codejar) { + jar = new CodeJar( + document.querySelector("#editor"), + withLineNumbers(Prism.highlightElement) + ); + } + $("#modalDialog").modal("show"); }, errorMessage: "Cannot receive maps data", @@ -188,7 +213,12 @@ define(["jquery"], return false; }); $("#modalDialog").on("hidden.bs.modal", function () { - $("#map-textarea").remove(); + if (editor.codejar) { + jar.destroy(); + $(".codejar-wrap").remove(); + } else { + $("#editor").remove(); + } }); $("#saveActionsBtn").on("click", function () { @@ -207,10 +237,10 @@ define(["jquery"], errorMessage: "Save map error", method: "POST", headers: { - Map: $("#map-textarea").data("id"), + Map: $("#editor").data("id"), }, params: { - data: $("#map-textarea").val(), + data: editor.codejar ? jar.toString() : $("#editor").val(), dataType: "text", }, server: server diff --git a/interface/js/lib/codejar.min.js b/interface/js/lib/codejar.min.js new file mode 100644 index 000000000..bbd637879 --- /dev/null +++ b/interface/js/lib/codejar.min.js @@ -0,0 +1,5 @@ +/*! + * CodeJar 3.1.0 (https://github.com/antonmedv/codejar) + * Copyright (c) 2020, Anton Medvedev, MIT + */ +function CodeJar(t,e,n={}){const o=Object.assign({tab:"\t",indentOn:/{$/},n);let r,s,i=[],c=[],d=-1,a=!1,f=navigator.userAgent.toLowerCase().indexOf("firefox")>-1;t.setAttribute("contentEditable",f?"true":"plaintext-only"),t.setAttribute("spellcheck","false"),t.style.outline="none",t.style.overflowWrap="break-word",t.style.overflowY="auto",t.style.resize="vertical",t.style.whiteSpace="pre-wrap",e(t);const l=E(()=>{const n=y();e(t),m(n)},30);let u=!1;const p=t=>!T(t)&&!v(t)&&"Meta"!==t.key&&"Control"!==t.key&&"Alt"!==t.key&&!t.key.startsWith("Arrow"),g=E(t=>{p(t)&&(O(),u=!1)},300),h=(e,n)=>{i.push([e,n]),t.addEventListener(e,n)};function y(){const e=window.getSelection(),n={start:0,end:0,dir:void 0};return C(t,t=>{if(t===e.anchorNode&&t===e.focusNode)return n.start+=e.anchorOffset,n.end+=e.focusOffset,n.dir=e.anchorOffset<=e.focusOffset?"->":"<-","stop";if(t===e.anchorNode){if(n.start+=e.anchorOffset,n.dir)return"stop";n.dir="->"}else if(t===e.focusNode){if(n.end+=e.focusOffset,n.dir)return"stop";n.dir="<-"}t.nodeType===Node.TEXT_NODE&&("->"!=n.dir&&(n.start+=t.nodeValue.length),"<-"!=n.dir&&(n.end+=t.nodeValue.length))}),n}function m(e){const n=window.getSelection();let o,r,s=0,i=0;if(e.dir||(e.dir="->"),e.start<0&&(e.start=0),e.end<0&&(e.end=0),"<-"==e.dir){const{start:t,end:n}=e;e.start=n,e.end=t}let c=0;C(t,t=>{if(t.nodeType!==Node.TEXT_NODE)return;const n=(t.nodeValue||"").length;if(c+n>=e.start&&(o||(o=t,s=e.start-c),c+n>=e.end))return r=t,i=e.end-c,"stop";c+=n}),o||(o=t),r||(r=t),"<-"==e.dir&&([o,s,r,i]=[r,i,o,s]),n.setBaseAndExtent(o,s,r,i)}function b(){const e=window.getSelection().getRangeAt(0),n=document.createRange();return n.selectNodeContents(t),n.setEnd(e.startContainer,e.startOffset),n.toString()}function w(){const e=window.getSelection().getRangeAt(0),n=document.createRange();return n.selectNodeContents(t),n.setStart(e.endContainer,e.endOffset),n.toString()}function O(){if(!a)return;const e=t.innerHTML,n=y(),o=c[d];if(o&&o.html===e&&o.pos.start===n.start&&o.pos.end===n.end)return;c[++d]={html:e,pos:n},c.splice(d+1);d>300&&(d=300,c.splice(0,1))}function C(t,e){const n=[];t.firstChild&&n.push(t.firstChild);let o=n.pop();for(;o&&"stop"!==e(o);)o.nextSibling&&n.push(o.nextSibling),o.firstChild&&n.push(o.firstChild),o=n.pop()}function k(t){return t.metaKey||t.ctrlKey}function T(t){return k(t)&&!t.shiftKey&&"KeyZ"===t.code}function v(t){return k(t)&&t.shiftKey&&"KeyZ"===t.code}function x(t){t=t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"),document.execCommand("insertHTML",!1,t)}function E(t,e){let n=0;return(...o)=>{clearTimeout(n),n=window.setTimeout(()=>t(...o),e)}}function S(t){let e=t.length-1;for(;e>=0&&"\n"!==t[e];)e--;let n=++e;for(;n{e.defaultPrevented||(s=N(),function(t){if("Enter"===t.key){const e=b(),n=w();let[r]=S(e),s=r;if(o.indentOn.test(e)&&(s+=o.tab),f?(A(t),x("\n"+s)):s.length>0&&(A(t),x("\n"+s)),s!==r&&"}"===n[0]){const t=y();x("\n"+r),m(t)}}}(e),function(t){if("Tab"===t.key)if(A(t),t.shiftKey){const t=b();let[e,n]=S(t);if(e.length>0){const t=y(),r=Math.min(o.tab.length,e.length);m({start:n,end:n+r}),document.execCommand("delete"),t.start-=r,t.end-=r,m(t)}}else x(o.tab)}(e),function(t){const e="([{'\"",n=")]}'\"",o=w();if(n.includes(t.key)&&o.substr(0,1)===t.key){const e=y();A(t),e.start=++e.end,m(e)}else if(e.includes(t.key)){const o=y();A(t);const r=t.key+n[e.indexOf(t.key)];x(r),o.start=++o.end,m(o)}}(e),function(e){if(T(e)){A(e);const n=c[--d];n&&(t.innerHTML=n.html,m(n.pos)),d<0&&(d=0)}if(v(e)){A(e);const n=c[++d];n&&(t.innerHTML=n.html,m(n.pos)),d>=c.length&&d--}}(e),p(e)&&!u&&(O(),u=!0))}),h("keyup",t=>{t.defaultPrevented||t.isComposing||(s!==N()&&l(),g(t),r&&r(N()))}),h("focus",t=>{a=!0}),h("blur",t=>{a=!1}),h("paste",n=>{O(),function(n){A(n);const o=(n.originalEvent||n).clipboardData.getData("text/plain"),r=y();x(o),e(t),m({start:r.end+o.length,end:r.end+o.length})}(n),O(),r&&r(N())}),{updateOptions(t){t=Object.assign(Object.assign({},t),t)},updateCode(n){t.textContent=n,e(t)},onUpdate(t){r=t},toString:N,destroy(){for(let[e,n]of i)t.removeEventListener(e,n)}}} \ No newline at end of file diff --git a/interface/js/lib/linenumbers.min.js b/interface/js/lib/linenumbers.min.js new file mode 100644 index 000000000..1b8312009 --- /dev/null +++ b/interface/js/lib/linenumbers.min.js @@ -0,0 +1,5 @@ +/*! + * CodeJar 3.1.0 helper: lineNumbers (https://github.com/antonmedv/codejar) + * Copyright (c) 2020, Anton Medvedev, MIT + */ +function withLineNumbers(e,t={}){const o=Object.assign({class:"codejar-linenumbers",wrapClass:"codejar-wrap",width:"35px",backgroundColor:"rgba(128, 128, 128, 0.15)",color:""},t);let l;return function(t){e(t),l||(l=init(t,o));const n=(t.textContent||"").replace(/\n+$/,"\n").split("\n").length+1;let s="";for(let e=1;e=l.reach);k+=y.value.length,y=y.next){var b=y.value;if(t.length>n.length)return;if(!(b instanceof W)){var x=1;if(h&&y!=t.tail.prev){m.lastIndex=k;var w=m.exec(n);if(!w)break;var A=w.index+(f&&w[1]?w[1].length:0),P=w.index+w[0].length,S=k;for(S+=y.value.length;S<=A;)y=y.next,S+=y.value.length;if(S-=y.value.length,k=S,y.value instanceof W)continue;for(var E=y;E!==t.tail&&(Sl.reach&&(l.reach=j);var C=y.prev;L&&(C=I(t,C,L),k+=L.length),z(t,C,x);var _=new W(o,g?M.tokenize(O,g):O,v,O);y=I(t,C,_),N&&I(t,y,N),1"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var e=M.util.currentScript();function t(){M.manual||M.highlightAll()}if(e&&(M.filename=e.src,e.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var r=document.readyState;"loading"===r||"interactive"===r&&e&&e.defer?document.addEventListener("DOMContentLoaded",t):window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(t,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; +!function(){if(("undefined"==typeof self||self.Prism)&&("undefined"==typeof global||global.Prism)){var i={tab:/\t/,crlf:/\r\n/,lf:/\n/,cr:/\r/,space:/ /};Prism.hooks.add("before-highlight",function(r){s(r.grammar)})}function f(r,e){var i=r[e];switch(Prism.util.type(i)){case"RegExp":var a={};r[e]={pattern:i,inside:a},s(a);break;case"Array":for(var n=0,t=i.length;n