From 02bd3073bdde42a49d1bea4a4578ba50a83b57bc Mon Sep 17 00:00:00 2001
From: Owen Leonard <oleonard@myacpl.org>
Date: Fri, 5 Feb 2021 18:14:55 +0000
Subject: [PATCH] Bug 27643: Add CodeMirror custom syntax highlighting for SQL
 runtime parameters

This patch adds some additional configuration to CodeMirror so that
runtime parameters have a distinct color in the CodeMirror SQL editor.

To test, apply the patch and create or edit an SQL report which contains
one or more runtime parameters, e.g. <<branches>>, <<categorycode>>,
etc.

Confirm that when editing the SQL, these parameters should appear as
green text. Save your report and view it. The syntax highlighting should
be updated in this view too.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
---
 koha-tmpl/intranet-tmpl/lib/codemirror/overlay.js  | 90 ++++++++++++++++++++++
 .../intranet-tmpl/lib/codemirror/overlay.min.js    |  1 +
 .../en/modules/reports/guided_reports_start.tt     | 30 +++++++-
 3 files changed, 118 insertions(+), 3 deletions(-)
 create mode 100644 koha-tmpl/intranet-tmpl/lib/codemirror/overlay.js
 create mode 100644 koha-tmpl/intranet-tmpl/lib/codemirror/overlay.min.js

diff --git a/koha-tmpl/intranet-tmpl/lib/codemirror/overlay.js b/koha-tmpl/intranet-tmpl/lib/codemirror/overlay.js
new file mode 100644
index 0000000000..839d9e552e
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/lib/codemirror/overlay.js
@@ -0,0 +1,90 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: https://codemirror.net/LICENSE
+
+// Utility function that allows modes to be combined. The mode given
+// as the base argument takes care of most of the normal mode
+// functionality, but a second (typically simple) mode is used, which
+// can override the style of text. Both modes get to parse all of the
+// text, but when both assign a non-null style to a piece of code, the
+// overlay wins, unless the combine argument was true and not overridden,
+// or state.overlay.combineTokens was true, in which case the styles are
+// combined.
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.overlayMode = function(base, overlay, combine) {
+  return {
+    startState: function() {
+      return {
+        base: CodeMirror.startState(base),
+        overlay: CodeMirror.startState(overlay),
+        basePos: 0, baseCur: null,
+        overlayPos: 0, overlayCur: null,
+        streamSeen: null
+      };
+    },
+    copyState: function(state) {
+      return {
+        base: CodeMirror.copyState(base, state.base),
+        overlay: CodeMirror.copyState(overlay, state.overlay),
+        basePos: state.basePos, baseCur: null,
+        overlayPos: state.overlayPos, overlayCur: null
+      };
+    },
+
+    token: function(stream, state) {
+      if (stream != state.streamSeen ||
+          Math.min(state.basePos, state.overlayPos) < stream.start) {
+        state.streamSeen = stream;
+        state.basePos = state.overlayPos = stream.start;
+      }
+
+      if (stream.start == state.basePos) {
+        state.baseCur = base.token(stream, state.base);
+        state.basePos = stream.pos;
+      }
+      if (stream.start == state.overlayPos) {
+        stream.pos = stream.start;
+        state.overlayCur = overlay.token(stream, state.overlay);
+        state.overlayPos = stream.pos;
+      }
+      stream.pos = Math.min(state.basePos, state.overlayPos);
+
+      // state.overlay.combineTokens always takes precedence over combine,
+      // unless set to null
+      if (state.overlayCur == null) return state.baseCur;
+      else if (state.baseCur != null &&
+               state.overlay.combineTokens ||
+               combine && state.overlay.combineTokens == null)
+        return state.baseCur + " " + state.overlayCur;
+      else return state.overlayCur;
+    },
+
+    indent: base.indent && function(state, textAfter) {
+      return base.indent(state.base, textAfter);
+    },
+    electricChars: base.electricChars,
+
+    innerMode: function(state) { return {state: state.base, mode: base}; },
+
+    blankLine: function(state) {
+      var baseToken, overlayToken;
+      if (base.blankLine) baseToken = base.blankLine(state.base);
+      if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay);
+
+      return overlayToken == null ?
+        baseToken :
+        (combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken);
+    }
+  };
+};
+
+});
diff --git a/koha-tmpl/intranet-tmpl/lib/codemirror/overlay.min.js b/koha-tmpl/intranet-tmpl/lib/codemirror/overlay.min.js
new file mode 100644
index 0000000000..6e0f9b3466
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/lib/codemirror/overlay.min.js
@@ -0,0 +1 @@
+!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.overlayMode=function(o,r,a){return{startState:function(){return{base:e.startState(o),overlay:e.startState(r),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(a){return{base:e.copyState(o,a.base),overlay:e.copyState(r,a.overlay),basePos:a.basePos,baseCur:null,overlayPos:a.overlayPos,overlayCur:null}},token:function(e,n){return(e!=n.streamSeen||Math.min(n.basePos,n.overlayPos)<e.start)&&(n.streamSeen=e,n.basePos=n.overlayPos=e.start),e.start==n.basePos&&(n.baseCur=o.token(e,n.base),n.basePos=e.pos),e.start==n.overlayPos&&(e.pos=e.start,n.overlayCur=r.token(e,n.overlay),n.overlayPos=e.pos),e.pos=Math.min(n.basePos,n.overlayPos),null==n.overlayCur?n.baseCur:null!=n.baseCur&&n.overlay.combineTokens||a&&null==n.overlay.combineTokens?n.baseCur+" "+n.overlayCur:n.overlayCur},indent:o.indent&&function(e,r){return o.indent(e.base,r)},electricChars:o.electricChars,innerMode:function(e){return{state:e.base,mode:o}},blankLine:function(e){var n,t;return o.blankLine&&(n=o.blankLine(e.base)),r.blankLine&&(t=r.blankLine(e.overlay)),null==t?n:a&&null!=n?n+" "+t:t}}}});
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt
index c04ffef6a4..ada04bb135 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt
@@ -53,6 +53,9 @@
     .CodeMirror {
         resize:  vertical;
     }
+    .cm-sqlParams {
+        color: #11917B;
+    }
     #mana_search_errortext { font-family: monospace; font-weight: bold; }
 </style>
 [% Asset.css("css/reports.css") | $raw %]
@@ -1287,7 +1290,7 @@
                 [% INCLUDE 'guided-reports-view.inc' %]
             </aside>
         </div> <!-- /.col-sm-2.col-sm-pull-10 -->
-     </div> <!-- /.row -->
+    </div> <!-- /.row -->
 
 [% MACRO jsinclude BLOCK %]
     [% Asset.js("js/charts.js") | $raw %]
@@ -1299,6 +1302,7 @@
         [% INCLUDE 'columns_settings.inc' %]
     [% END %]
     [% Asset.js( "lib/codemirror/codemirror.min.js" ) | $raw %]
+    [% Asset.js( "lib/codemirror/overlay.min.js" ) | $raw %]
     [% Asset.js( "lib/codemirror/sql.min.js" ) | $raw %]
     [% Asset.js( "js/mana.js" ) | $raw %]
     <script>
@@ -1331,6 +1335,25 @@
             }
         }
 
+        /* overlay a syntax-highlighting definition on top of the existing sql one */
+        CodeMirror.defineMode("sqlParams", function(config, parserConfig) {
+            var sqlParamsOverlay = {
+                token: function(stream, state) {
+                var ch;
+                if (stream.match("<<")) {
+                    while ((ch = stream.next()) != null)
+                    if (ch == ">" && stream.next() == ">") {
+                        stream.eat(">");
+                        return "sqlParams";
+                    }
+                }
+                while (stream.next() != null && !stream.match("<<", false)) {}
+                return null;
+                }
+            };
+            return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/x-sql"), sqlParamsOverlay);
+        });
+
         var MSG_CONFIRM_DELETE = _("Are you sure you want to delete this report? This cannot be undone.");
         var group_subgroups = {};
         [% FOREACH group IN groups_with_subgroups %]
@@ -1344,9 +1367,10 @@
         [% END %]
 
         [% IF ( create || editsql || save ) %]
+
             var editor = CodeMirror.fromTextArea(sql, {
                 lineNumbers: true,
-                mode: "text/x-sql",
+                mode: "sqlParams", /* text/x-sql plus custom sqlParams configuration */
                 lineWrapping: true,
                 smartIndent: false
             });
@@ -1368,7 +1392,7 @@
         [% IF ( showsql ) %]
             var editor = CodeMirror.fromTextArea(sql, {
                 lineNumbers: false,
-                mode: "text/x-sql",
+                mode: "sqlParams", /* text/x-sql plus custom sqlParams configuration */
                 lineWrapping: true,
                 readOnly: true
             });
-- 
2.11.0