From d47c918fd7452522f602d9979b1929c1e699cf16 Mon Sep 17 00:00:00 2001
From: sverweij <sverweij@users.noreply.github.com>
Date: Thu, 10 Sep 2015 21:48:07 +0200
Subject: [PATCH] [mscgen mode] Add

---
 mode/meta.js                   |   5 +-
 mode/mscgen/index.html         |  53 ++++++++++
 mode/mscgen/index_msgenny.html |  44 ++++++++
 mode/mscgen/index_xu.html      |  70 +++++++++++++
 mode/mscgen/mscgen.js          | 186 +++++++++++++++++++++++++++++++++
 mode/mscgen/mscgen_test.js     |  75 +++++++++++++
 mode/mscgen/msgenny_test.js    |  71 +++++++++++++
 mode/mscgen/xu_test.js         |  75 +++++++++++++
 test/index.html                |   4 +
 9 files changed, 582 insertions(+), 1 deletion(-)
 create mode 100644 mode/mscgen/index.html
 create mode 100644 mode/mscgen/index_msgenny.html
 create mode 100644 mode/mscgen/index_xu.html
 create mode 100644 mode/mscgen/mscgen.js
 create mode 100644 mode/mscgen/mscgen_test.js
 create mode 100644 mode/mscgen/msgenny_test.js
 create mode 100644 mode/mscgen/xu_test.js

diff --git a/mode/meta.js b/mode/meta.js
index 55f11da8b..6e82fad6c 100644
--- a/mode/meta.js
+++ b/mode/meta.js
@@ -145,7 +145,10 @@
     {name: "XML", mimes: ["application/xml", "text/xml"], mode: "xml", ext: ["xml", "xsl", "xsd"], alias: ["rss", "wsdl", "xsd"]},
     {name: "XQuery", mime: "application/xquery", mode: "xquery", ext: ["xy", "xquery"]},
     {name: "YAML", mime: "text/x-yaml", mode: "yaml", ext: ["yaml", "yml"], alias: ["yml"]},
-    {name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]}
+    {name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]},
+    {name: "mscgen", mime: "text/x-mscgen", mode: "mscgen", ext: ["mscgen", "mscin", "msc"]},
+    {name: "xu", mime: "text/x-xu", mode: "xu", ext: ["xu"]},
+    {name: "msgenny", mime: "text/x-msgenny", mode: "msgenny", ext: ["msgenny"]}
   ];
   // Ensure all modes have a mime property for backwards compatibility
   for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
diff --git a/mode/mscgen/index.html b/mode/mscgen/index.html
new file mode 100644
index 000000000..a97b6c832
--- /dev/null
+++ b/mode/mscgen/index.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>CodeMirror: mscgen mode</title>
+    <link rel="stylesheet" href="../../lib/codemirror.css">
+    <script src="../../lib/codemirror.js"></script>
+    <script src="mscgen.js"></script>
+    <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
+  </head>
+  <body>
+    <h1>CodeMirror: MscGen mode</h1>
+
+<div><textarea id="code">
+# Sample mscgen program
+# See http://www.mcternan.me.uk/mscgen or 
+# https://sverweij.github.io/mscgen_js for more samples
+
+msc {
+  # options
+  hscale="1.2";
+
+  # entities/ lifelines
+  a [label="Entity A"],
+  b [label="Entity B", linecolor="red", arclinecolor="red", textbgcolor="pink"],
+  c [label="Entity C"];
+
+  # arcs/ messages
+  a => c [label="doSomething(args)"];
+  b => c [label="doSomething(args)"];
+  c >> * [label="everyone asked me", arcskip="1"];
+  c =>> c [label="doing something"];
+  c -x * [label="report back", arcskip="1"];
+  |||;
+  --- [label="shows's over, however ..."];
+  b => a [label="did you see c doing something?"];
+  a -> b [label="nope"];
+  b :> a [label="shall we ask again?"];
+  a => b [label="naah"];
+  ...;
+}
+</textarea></div>
+
+    <script>
+      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
+        lineNumbers: true,
+        mode: "mscgen",
+      });
+    </script>
+
+    <p><strong>MIME types defined:</strong> <code>text/x-mscgen</code></p>
+  </body>
+</html>
diff --git a/mode/mscgen/index_msgenny.html b/mode/mscgen/index_msgenny.html
new file mode 100644
index 000000000..1664f3051
--- /dev/null
+++ b/mode/mscgen/index_msgenny.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>CodeMirror: msgenny mode</title>
+    <link rel="stylesheet" href="../../lib/codemirror.css">
+    <script src="../../lib/codemirror.js"></script>
+    <script src="mscgen.js"></script>
+    <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
+  </head>
+  <body>
+    <h1>CodeMirror: msgenny mode</h1>
+
+<div><textarea id="code">
+# Sample msgenny program
+# https://sverweij.github.io/mscgen_js for more samples
+
+a -> b   : a -> b  (signal);
+a => b   : a => b  (method);
+b >> a   : b >> a  (return value);
+a =>> b  : a =>> b (callback);
+a -x b   : a -x b  (lost);
+a :> b   : a :> b  (emphasis);
+a .. b   : a .. b  (dotted);
+a -- b   : "a -- b straight line";
+a note a : a note a\n(note),
+b box b  : b box b\n(action);
+a rbox a : a rbox a\n(reference),
+b abox b : b abox b\n(state/ condition);
+|||      : ||| (empty row);
+...      : ... (omitted row);
+---      : --- (comment);
+</textarea></div>
+
+    <script>
+      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
+        lineNumbers: true,
+        mode: "msgenny",
+      });
+    </script>
+
+    <p><strong>MIME types defined:</strong> <code>text/x-msgenny</code></p>
+  </body>
+</html>
diff --git a/mode/mscgen/index_xu.html b/mode/mscgen/index_xu.html
new file mode 100644
index 000000000..2f7bf9ec0
--- /dev/null
+++ b/mode/mscgen/index_xu.html
@@ -0,0 +1,70 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>CodeMirror: xu mode</title>
+    <link rel="stylesheet" href="../../lib/codemirror.css">
+    <script src="../../lib/codemirror.js"></script>
+    <script src="mscgen.js"></script>
+    <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
+  </head>
+  <body>
+    <h1>CodeMirror: xù mode</h1>
+
+<div><textarea id="code">
+# test50 - expansions to mscgen to support inline expressions
+#          for now in a separate language: xù
+msc {
+
+  hscale="0.8",
+  width="700";
+
+  a,
+  b [label="change store"],
+  c,
+  d [label="necro queue"],
+  e [label="natalis queue"],
+  f;
+
+  a =>> b [label="get change list()"];
+  a alt f [label="changes found"] { /* alt is a xu specific keyword*/
+    b >> a [label="list of changes"];
+    a =>> c [label="cull old stuff (list of changes)"];
+    b loop e [label="for each change"] { // loop is xu specific as well...
+      /*
+       * Here interesting stuff happens.
+       * TBD
+       */
+      c =>> b [label="get change()"];
+      b >> c [label="change"];
+      c alt e [label="change too old"] {
+        c =>> d [label="queue(change)"];
+        --- [label="change newer than latest run"];
+        c =>> e [label="queue(change)"];
+        --- [label="all other cases"];
+        ||| [label="leave well alone"];
+      };
+    };
+    
+    
+    c >> a [label="done
+    processing"];
+    
+    /* shucks! nothing found ...*/
+    --- [label="nothing found"];
+    b >> a [label="nothing"];
+    a note a [label="silent exit"];
+  };
+}
+</textarea></div>
+
+    <script>
+      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
+        lineNumbers: true,
+        mode: "xu",
+      });
+    </script>
+
+    <p><strong>MIME types defined:</strong> <code>text/x-xu</code></p>
+  </body>
+</html>
diff --git a/mode/mscgen/mscgen.js b/mode/mscgen/mscgen.js
new file mode 100644
index 000000000..090a208a7
--- /dev/null
+++ b/mode/mscgen/mscgen.js
@@ -0,0 +1,186 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// mode(s) for the sequence chart dsl's mscgen, xù and msgenny
+// For more information on mscgen, see the site of the original author:
+// http://www.mcternan.me.uk/mscgen
+//
+// This mode for mscgen and the two derivative languages were
+// originally made for use in the mscgen_js interpreter
+// (https://sverweij.github.io/mscgen_js)
+
+(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.defineMode("mscgen", function() {
+    return {
+      startState : startStateFn,
+      copyState : copyStateFn,
+      token : produceTokenFunction({
+        "keywords" : ["msc"],
+        "options" : ["hscale", "width", "arcgradient", "wordwraparcs"],
+        "attributes" : ["label", "idurl", "id", "url", "linecolor", "linecolour", "textcolor", "textcolour", "textbgcolor", "textbgcolour", "arclinecolor", "arclinecolour", "arctextcolor", "arctextcolour", "arctextbgcolor", "arctextbgcolour", "arcskip"],
+        "brackets" : ["\\{", "\\}"], // [ and  ] are brackets too, but these get handled in with lists
+        "arcsWords" : ["note", "abox", "rbox", "box"],
+        "arcsOthers" : ["\\|\\|\\|", "\\.\\.\\.", "---", "--", "<->", "==", "<<=>>", "<=>", "\\.\\.", "<<>>", "::", "<:>", "->", "=>>", "=>", ">>", ":>", "<-", "<<=", "<=", "<<", "<:", "x-", "-x"],
+        "singlecomment" : ["//", "#"],
+        "operators" : ["="]
+      }),
+      lineComment : "#",
+      blockCommentStart : "/*",
+      blockCommentEnd : "*/"
+    };
+  });
+  CodeMirror.defineMIME("text/x-mscgen", "mscgen");
+
+  CodeMirror.defineMode("xu", function() {
+    return {
+      startState : startStateFn,
+      copyState : copyStateFn,
+      token : produceTokenFunction({
+        "keywords" : ["msc"],
+        "options" : ["hscale", "width", "arcgradient", "wordwraparcs", "watermark"],
+        "attributes" : ["label", "idurl", "id", "url", "linecolor", "linecolour", "textcolor", "textcolour", "textbgcolor", "textbgcolour", "arclinecolor", "arclinecolour", "arctextcolor", "arctextcolour", "arctextbgcolor", "arctextbgcolour", "arcskip"],
+        "brackets" : ["\\{", "\\}"],  // [ and  ] are brackets too, but these get handled in with lists
+        "arcsWords" : ["note", "abox", "rbox", "box", "alt", "else", "opt", "break", "par", "seq", "strict", "neg", "critical", "ignore", "consider", "assert", "loop", "ref", "exc"],
+        "arcsOthers" : ["\\|\\|\\|", "\\.\\.\\.", "---", "--", "<->", "==", "<<=>>", "<=>", "\\.\\.", "<<>>", "::", "<:>", "->", "=>>", "=>", ">>", ":>", "<-", "<<=", "<=", "<<", "<:", "x-", "-x"],
+        "singlecomment" : ["//", "#"],
+        "operators" : ["="]
+      }),
+      lineComment : "#",
+      blockCommentStart : "/*",
+      blockCommentEnd : "*/"
+    };
+  });
+  CodeMirror.defineMIME("text/x-xu", "xu");
+
+  CodeMirror.defineMode("msgenny", function() {
+    return {
+      startState : startStateFn,
+      copyState : copyStateFn,
+      token : produceTokenFunction({
+        "keywords" : null,
+        "options" : ["hscale", "width", "arcgradient", "wordwraparcs", "watermark"],
+        "attributes" : null,
+        "brackets" : ["\\{", "\\}"],
+        "arcsWords" : ["note", "abox", "rbox", "box", "alt", "else", "opt", "break", "par", "seq", "strict", "neg", "critical", "ignore", "consider", "assert", "loop", "ref", "exc"],
+        "arcsOthers" : ["\\|\\|\\|", "\\.\\.\\.", "---", "--", "<->", "==", "<<=>>", "<=>", "\\.\\.", "<<>>", "::", "<:>", "->", "=>>", "=>", ">>", ":>", "<-", "<<=", "<=", "<<", "<:", "x-", "-x"],
+        "singlecomment" : ["//", "#"],
+        "operators" : ["="]
+      }),
+      lineComment : "#",
+      blockCommentStart : "/*",
+      blockCommentEnd : "*/"
+
+    };
+  });
+  CodeMirror.defineMIME("text/x-msgenny", "msgenny");
+
+  function wordRegexpBoundary(pWords) {
+    return new RegExp("\\b((" + pWords.join(")|(") + "))\\b", "i");
+  }
+
+  function wordRegexp(pWords) {
+    return new RegExp("((" + pWords.join(")|(") + "))", "i");
+  }
+
+  function startStateFn() {
+    return {
+      inComment : false,
+      inString : false,
+      inAttributeList : false,
+      inScript : false
+    };
+  }
+
+  function copyStateFn(pState) {
+    return {
+      inComment : pState.inComment,
+      inString : pState.inString,
+      inAttributeList : pState.inAttributeList,
+      inScript : pState.inScript
+    };
+  }
+
+  function produceTokenFunction(pConfig) {
+
+    return function(pStream, pState) {
+      if (pStream.match(wordRegexp(pConfig.brackets), true, true)) {
+        return "bracket";
+      }
+      /* comments */
+      if (!pState.inComment) {
+        if (pStream.match(/\/\*[^\*\/]*/, true, true)) {
+          pState.inComment = true;
+          return "comment";
+        }
+        if (pStream.match(wordRegexp(pConfig.singlecomment), true, true)) {
+          pStream.skipToEnd();
+          return "comment";
+        }
+      }
+      if (pState.inComment) {
+        if (pStream.match(/[^\*\/]*\*\//, true, true)) {
+          pState.inComment = false;
+        } else {
+          pStream.skipToEnd();
+        }
+        return "comment";
+      }
+      /* strings */
+      if (!pState.inString && pStream.match(/\"(\\\"|[^\"])*/, true, true)) {
+        pState.inString = true;
+        return "string";
+      }
+      if (pState.inString) {
+        if (pStream.match(/[^\"]*\"/, true, true)) {
+          pState.inString = false;
+        } else {
+          pStream.skipToEnd();
+        }
+        return "string";
+      }
+      /* keywords & operators */
+      if (!!pConfig.keywords && pStream.match(wordRegexpBoundary(pConfig.keywords), true, true)) {
+        return "keyword";
+      }
+      if (pStream.match(wordRegexpBoundary(pConfig.options), true, true)) {
+        return "keyword";
+      }
+      if (pStream.match(wordRegexpBoundary(pConfig.arcsWords), true, true)) {
+        return "keyword";
+      }
+      if (pStream.match(wordRegexp(pConfig.arcsOthers), true, true)) {
+        return "keyword";
+      }
+      if (!!pConfig.operators && pStream.match(wordRegexp(pConfig.operators), true, true)) {
+        return "operator";
+      }
+      /* attribute lists */
+      if (!pConfig.inAttributeList && !!pConfig.attributes && pStream.match(/\[/, true, true)) {
+        pConfig.inAttributeList = true;
+        return "bracket";
+      }
+      if (pConfig.inAttributeList) {
+        if (pConfig.attributes !== null && pStream.match(wordRegexpBoundary(pConfig.attributes), true, true)) {
+          return "attribute";
+        }
+        if (pStream.match(/]/, true, true)) {
+          pConfig.inAttributeList = false;
+          return "bracket";
+        }
+      }
+
+      pStream.next();
+      return "base";
+    };
+  }
+
+});
diff --git a/mode/mscgen/mscgen_test.js b/mode/mscgen/mscgen_test.js
new file mode 100644
index 000000000..e319a3997
--- /dev/null
+++ b/mode/mscgen/mscgen_test.js
@@ -0,0 +1,75 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function() {
+  var mode = CodeMirror.getMode({indentUnit: 2}, "mscgen");
+  function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
+
+  MT("empty chart",
+     "[keyword msc][bracket {]",
+     "[base   ]",
+     "[bracket }]"
+   );
+
+  MT("comments",
+    "[comment // a single line comment]",
+    "[comment # another  single line comment /* and */ ignored here]",
+    "[comment /* A multi-line comment even though it contains]",
+    "[comment msc keywords and \"quoted text\"*/]");
+
+  MT("strings",
+    "[string \"// a string\"]",
+    "[string \"a string running over]",
+    "[string two lines\"]",
+    "[string \"with \\\"escaped quote\"]"
+  );
+
+  MT("xù/ msgenny keywords classify as 'base'",
+    "[base watermark]",
+    "[base alt loop opt ref else break par seq assert]"
+  );
+
+  MT("mscgen options classify as keyword",
+    "[keyword hscale]", "[keyword width]", "[keyword arcgradient]", "[keyword wordwraparcs]"
+  );
+
+  MT("mscgen arcs classify as keyword",
+    "[keyword note]","[keyword abox]","[keyword rbox]","[keyword box]",
+    "[keyword |||...---]", "[keyword ..--==::]",
+    "[keyword ->]", "[keyword <-]", "[keyword <->]",
+    "[keyword =>]", "[keyword <=]", "[keyword <=>]",
+    "[keyword =>>]", "[keyword <<=]", "[keyword <<=>>]",
+    "[keyword >>]", "[keyword <<]", "[keyword <<>>]",
+    "[keyword -x]", "[keyword x-]", "[keyword -X]", "[keyword X-]",
+    "[keyword :>]", "[keyword <:]", "[keyword <:>]"
+  );
+
+  MT("within an attribute list, attributes classify as attribute",
+    "[bracket [[][attribute label]",
+    "[attribute id]","[attribute url]","[attribute idurl]",
+    "[attribute linecolor]","[attribute linecolour]","[attribute textcolor]","[attribute textcolour]","[attribute textbgcolor]","[attribute textbgcolour]",
+    "[attribute arclinecolor]","[attribute arclinecolour]","[attribute arctextcolor]","[attribute arctextcolour]","[attribute arctextbgcolor]","[attribute arctextbgcolour]",
+    "[attribute arcskip][bracket ]]]"
+  );
+
+  MT("outside an attribute list, attributes classify as base",
+    "[base label]",
+    "[base id]","[base url]","[base idurl]",
+    "[base linecolor]","[base linecolour]","[base textcolor]","[base textcolour]","[base textbgcolor]","[base textbgcolour]",
+    "[base arclinecolor]","[base arclinecolour]","[base arctextcolor]","[base arctextcolour]","[base arctextbgcolor]","[base arctextbgcolour]",
+    "[base arcskip]"
+  );
+
+  MT("a typical program",
+    "[comment # typical mscgen program]",
+    "[keyword msc][base  ][bracket {]",
+    "[keyword wordwraparcs][operator =][string \"true\"][base , ][keyword hscale][operator =][string \"0.8\"][keyword arcgradient][operator =][base 30;]",
+    "[base   a][bracket [[][attribute label][operator =][string \"Entity A\"][bracket ]]][base ,]",
+    "[base   b][bracket [[][attribute label][operator =][string \"Entity B\"][bracket ]]][base ,]",
+    "[base   c][bracket [[][attribute label][operator =][string \"Entity C\"][bracket ]]][base ;]",
+    "[base   a ][keyword =>>][base  b][bracket [[][attribute label][operator =][string \"Hello entity B\"][bracket ]]][base ;]",
+    "[base   a ][keyword <<][base  b][bracket [[][attribute label][operator =][string \"Here's an answer dude!\"][bracket ]]][base ;]",
+    "[base   c ][keyword :>][base  *][bracket [[][attribute label][operator =][string \"What about me?\"][base , ][attribute textcolor][operator =][base red][bracket ]]][base ;]",
+    "[bracket }]"
+  );
+})();
diff --git a/mode/mscgen/msgenny_test.js b/mode/mscgen/msgenny_test.js
new file mode 100644
index 000000000..6e9ab1653
--- /dev/null
+++ b/mode/mscgen/msgenny_test.js
@@ -0,0 +1,71 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function() {
+  var mode = CodeMirror.getMode({indentUnit: 2}, "msgenny");
+  function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
+
+  MT("comments",
+    "[comment // a single line comment]",
+    "[comment # another  single line comment /* and */ ignored here]",
+    "[comment /* A multi-line comment even though it contains]",
+    "[comment msc keywords and \"quoted text\"*/]");
+
+  MT("strings",
+    "[string \"// a string\"]",
+    "[string \"a string running over]",
+    "[string two lines\"]",
+    "[string \"with \\\"escaped quote\"]"
+  );
+
+  MT("xù/ msgenny keywords classify as 'keyword'",
+    "[keyword watermark]",
+    "[keyword alt]","[keyword loop]","[keyword opt]","[keyword ref]","[keyword else]","[keyword break]","[keyword par]","[keyword seq]","[keyword assert]"
+  );
+
+  MT("mscgen options classify as keyword",
+    "[keyword hscale]", "[keyword width]", "[keyword arcgradient]", "[keyword wordwraparcs]"
+  );
+
+  MT("mscgen arcs classify as keyword",
+    "[keyword note]","[keyword abox]","[keyword rbox]","[keyword box]",
+    "[keyword |||...---]", "[keyword ..--==::]",
+    "[keyword ->]", "[keyword <-]", "[keyword <->]",
+    "[keyword =>]", "[keyword <=]", "[keyword <=>]",
+    "[keyword =>>]", "[keyword <<=]", "[keyword <<=>>]",
+    "[keyword >>]", "[keyword <<]", "[keyword <<>>]",
+    "[keyword -x]", "[keyword x-]", "[keyword -X]", "[keyword X-]",
+    "[keyword :>]", "[keyword <:]", "[keyword <:>]"
+  );
+
+  MT("within an attribute list, mscgen/ xù attributes classify as base",
+    "[base [[label]",
+    "[base idurl id url]",
+    "[base linecolor linecolour textcolor textcolour textbgcolor textbgcolour]",
+    "[base arclinecolor arclinecolour arctextcolor arctextcolour arctextbgcolor arctextbgcolour]",
+    "[base arcskip]]]"
+  );
+
+  MT("outside an attribute list, mscgen/ xù attributes classify as base",
+    "[base label]",
+    "[base idurl id url]",
+    "[base linecolor linecolour textcolor textcolour textbgcolor textbgcolour]",
+    "[base arclinecolor arclinecolour arctextcolor arctextcolour arctextbgcolor arctextbgcolour]",
+    "[base arcskip]"
+  );
+
+  MT("a typical program",
+    "[comment # typical msgenny program]",
+    "[keyword wordwraparcs][operator =][string \"true\"][base , ][keyword hscale][operator =][string \"0.8\"][base , ][keyword arcgradient][operator =][base 30;]",
+    "[base   a : ][string \"Entity A\"][base ,]",
+    "[base   b : Entity B,]",
+    "[base   c : Entity C;]",
+    "[base   a ][keyword =>>][base  b: ][string \"Hello entity B\"][base ;]",
+    "[base   a ][keyword alt][base  c][bracket {]",
+    "[base     a ][keyword <<][base  b: ][string \"Here's an answer dude!\"][base ;]",
+    "[keyword ---][base : ][string \"sorry, won't march - comm glitch\"]",
+    "[base     a ][keyword x-][base  b: ][string \"Here's an answer dude! (won't arrive...)\"][base ;]",
+    "[bracket }]",
+    "[base   c ][keyword :>][base  *: What about me?;]"
+  );
+})();
diff --git a/mode/mscgen/xu_test.js b/mode/mscgen/xu_test.js
new file mode 100644
index 000000000..ada8dd104
--- /dev/null
+++ b/mode/mscgen/xu_test.js
@@ -0,0 +1,75 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function() {
+  var mode = CodeMirror.getMode({indentUnit: 2}, "xu");
+  function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
+
+  MT("empty chart",
+     "[keyword msc][bracket {]",
+     "[base   ]",
+     "[bracket }]"
+   );
+
+  MT("comments",
+    "[comment // a single line comment]",
+    "[comment # another  single line comment /* and */ ignored here]",
+    "[comment /* A multi-line comment even though it contains]",
+    "[comment msc keywords and \"quoted text\"*/]");
+
+  MT("strings",
+    "[string \"// a string\"]",
+    "[string \"a string running over]",
+    "[string two lines\"]",
+    "[string \"with \\\"escaped quote\"]"
+  );
+
+  MT("xù/ msgenny keywords classify as 'keyword'",
+    "[keyword watermark]",
+    "[keyword alt]","[keyword loop]","[keyword opt]","[keyword ref]","[keyword else]","[keyword break]","[keyword par]","[keyword seq]","[keyword assert]"
+  );
+
+  MT("mscgen options classify as keyword",
+    "[keyword hscale]", "[keyword width]", "[keyword arcgradient]", "[keyword wordwraparcs]"
+  );
+
+  MT("mscgen arcs classify as keyword",
+    "[keyword note]","[keyword abox]","[keyword rbox]","[keyword box]",
+    "[keyword |||...---]", "[keyword ..--==::]",
+    "[keyword ->]", "[keyword <-]", "[keyword <->]",
+    "[keyword =>]", "[keyword <=]", "[keyword <=>]",
+    "[keyword =>>]", "[keyword <<=]", "[keyword <<=>>]",
+    "[keyword >>]", "[keyword <<]", "[keyword <<>>]",
+    "[keyword -x]", "[keyword x-]", "[keyword -X]", "[keyword X-]",
+    "[keyword :>]", "[keyword <:]", "[keyword <:>]"
+  );
+
+  MT("within an attribute list, attributes classify as attribute",
+    "[bracket [[][attribute label]",
+    "[attribute id]","[attribute url]","[attribute idurl]",
+    "[attribute linecolor]","[attribute linecolour]","[attribute textcolor]","[attribute textcolour]","[attribute textbgcolor]","[attribute textbgcolour]",
+    "[attribute arclinecolor]","[attribute arclinecolour]","[attribute arctextcolor]","[attribute arctextcolour]","[attribute arctextbgcolor]","[attribute arctextbgcolour]",
+    "[attribute arcskip][bracket ]]]"
+  );
+
+  MT("outside an attribute list, attributes classify as base",
+    "[base label]",
+    "[base id]","[base url]","[base idurl]",
+    "[base linecolor]","[base linecolour]","[base textcolor]","[base textcolour]","[base textbgcolor]","[base textbgcolour]",
+    "[base arclinecolor]","[base arclinecolour]","[base arctextcolor]","[base arctextcolour]","[base arctextbgcolor]","[base arctextbgcolour]",
+    "[base arcskip]"
+  );
+
+  MT("a typical program",
+    "[comment # typical mscgen program]",
+    "[keyword msc][base  ][bracket {]",
+    "[keyword wordwraparcs][operator =][string \"true\"][keyword hscale][operator =][string \"0.8\"][keyword arcgradient][operator =][base 30;]",
+    "[base   a][bracket [[][attribute label][operator =][string \"Entity A\"][bracket ]]][base ,]",
+    "[base   b][bracket [[][attribute label][operator =][string \"Entity B\"][bracket ]]][base ,]",
+    "[base   c][bracket [[][attribute label][operator =][string \"Entity C\"][bracket ]]][base ;]",
+    "[base   a ][keyword =>>][base  b][bracket [[][attribute label][operator =][string \"Hello entity B\"][bracket ]]][base ;]",
+    "[base   a ][keyword <<][base  b][bracket [[][attribute label][operator =][string \"Here's an answer dude!\"][bracket ]]][base ;]",
+    "[base   c ][keyword :>][base  *][bracket [[][attribute label][operator =][string \"What about me?\"][base , ][attribute textcolor][operator =][base red][bracket ]]][base ;]",
+    "[bracket }]"
+  );
+})();
diff --git a/test/index.html b/test/index.html
index 23d3fa5e5..b0b1fa976 100644
--- a/test/index.html
+++ b/test/index.html
@@ -38,6 +38,7 @@
 <script src="../keymap/sublime.js"></script>
 <script src="../keymap/vim.js"></script>
 <script src="../mode/rust/rust.js"></script>
+<script src="../mode/mscgen/mscgen.js"></script>
 
 <style type="text/css">
   .ok {color: #090;}
@@ -117,6 +118,9 @@
     <script src="../mode/xml/test.js"></script>
     <script src="../mode/xquery/test.js"></script>
     <script src="../mode/rust/test.js"></script>
+    <script src="../mode/mscgen/mscgen_test.js"></script>
+    <script src="../mode/mscgen/xu_test.js"></script>
+    <script src="../mode/mscgen/msgenny_test.js"></script>
     <script src="../addon/mode/multiplex_test.js"></script>
     <script src="emacs_test.js"></script>
     <script src="sql-hint-test.js"></script>
-- 
GitLab