Browse Source

feat: admin rendering (wip)

Nicolas Giard 2 years ago
parent
commit
9b2e0f79d5
35 changed files with 77 additions and 3140 deletions
  1. 0 8
      server/modules/rendering/markdown-abbr/definition.yml
  2. 0 11
      server/modules/rendering/markdown-abbr/renderer.js
  3. 0 63
      server/modules/rendering/markdown-core/definition.yml
  4. 0 53
      server/modules/rendering/markdown-core/renderer.js
  5. 0 12
      server/modules/rendering/markdown-core/underline.js
  6. 0 8
      server/modules/rendering/markdown-emoji/definition.yml
  7. 0 20
      server/modules/rendering/markdown-emoji/renderer.js
  8. 0 13
      server/modules/rendering/markdown-expandtabs/definition.yml
  9. 0 14
      server/modules/rendering/markdown-expandtabs/renderer.js
  10. 0 8
      server/modules/rendering/markdown-footnotes/definition.yml
  11. 0 11
      server/modules/rendering/markdown-footnotes/renderer.js
  12. 0 8
      server/modules/rendering/markdown-imsize/definition.yml
  13. 0 11
      server/modules/rendering/markdown-imsize/renderer.js
  14. 0 20
      server/modules/rendering/markdown-katex/definition.yml
  15. 0 1677
      server/modules/rendering/markdown-katex/mhchem.js
  16. 0 193
      server/modules/rendering/markdown-katex/renderer.js
  17. 0 29
      server/modules/rendering/markdown-kroki/definition.yml
  18. 0 143
      server/modules/rendering/markdown-kroki/renderer.js
  19. 0 20
      server/modules/rendering/markdown-mathjax/definition.yml
  20. 0 205
      server/modules/rendering/markdown-mathjax/renderer.js
  21. 0 23
      server/modules/rendering/markdown-multi-table/definition.yml
  22. 0 11
      server/modules/rendering/markdown-multi-table/renderer.js
  23. 0 41
      server/modules/rendering/markdown-plantuml/definition.yml
  24. 0 190
      server/modules/rendering/markdown-plantuml/renderer.js
  25. 0 18
      server/modules/rendering/markdown-supsub/definition.yml
  26. 0 17
      server/modules/rendering/markdown-supsub/renderer.js
  27. 0 8
      server/modules/rendering/markdown-tasklists/definition.yml
  28. 0 11
      server/modules/rendering/markdown-tasklists/renderer.js
  29. 0 8
      server/modules/rendering/openapi-core/definition.yml
  30. 0 14
      server/modules/rendering/openapi-core/renderer.js
  31. 1 0
      ux/public/_assets/icons/ultraviolet-brick.svg
  32. 1 1
      ux/src/i18n/locales/en.json
  33. 1 1
      ux/src/layouts/AdminLayout.vue
  34. 73 269
      ux/src/pages/AdminRendering.vue
  35. 1 1
      ux/src/router/routes.js

+ 0 - 8
server/modules/rendering/markdown-abbr/definition.yml

@@ -1,8 +0,0 @@
-key: markdownAbbr
-title: Abbreviations
-description: Parse abbreviations into abbr tags
-author: requarks.io
-icon: mdi-contain-start
-enabledDefault: true
-dependsOn: markdown-core
-props: {}

+ 0 - 11
server/modules/rendering/markdown-abbr/renderer.js

@@ -1,11 +0,0 @@
-const mdAbbr = require('markdown-it-abbr')
-
-// ------------------------------------
-// Markdown - Abbreviations
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    md.use(mdAbbr)
-  }
-}

+ 0 - 63
server/modules/rendering/markdown-core/definition.yml

@@ -1,63 +0,0 @@
-key: markdown-core
-title: Core
-description: Basic Markdown Parser
-author: requarks.io
-input: markdown
-output: html
-icon: mdi-language-markdown
-props:
-  allowHTML:
-    type: Boolean
-    default: true
-    title: Allow HTML
-    hint: Enable HTML tags in content.
-    order: 1
-    public: true
-  linkify:
-    type: Boolean
-    default: true
-    title: Automatically convert links
-    hint: Links will automatically be converted to clickable links.
-    order: 2
-    public: true
-  linebreaks:
-    type: Boolean
-    default: true
-    title: Automatically convert line breaks
-    hint: Add linebreaks within paragraphs.
-    order: 3
-    public: true
-  underline:
-    type: Boolean
-    default: false
-    title: Underline Emphasis
-    hint: Enable text underlining by using _underline_ syntax.
-    order: 4
-    public: true
-  typographer:
-    type: Boolean
-    default: false
-    title: Typographer
-    hint: Enable some language-neutral replacement + quotes beautification.
-    order: 5
-    public: true
-  quotes:
-    type: String
-    default: English
-    title: Quotes style
-    hint: When typographer is enabled. Double + single quotes replacement pairs. e.g. «»„“ for Russian, „“‚‘ for German, etc.
-    order: 6
-    enum:
-      - Chinese
-      - English
-      - French
-      - German
-      - Greek
-      - Japanese
-      - Hungarian
-      - Polish
-      - Portuguese
-      - Russian
-      - Spanish
-      - Swedish
-    public: true

+ 0 - 53
server/modules/rendering/markdown-core/renderer.js

@@ -1,53 +0,0 @@
-const md = require('markdown-it')
-const mdAttrs = require('markdown-it-attrs')
-const _ = require('lodash')
-const underline = require('./underline')
-
-const quoteStyles = {
-  Chinese: '””‘’',
-  English: '“”‘’',
-  French: ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'],
-  German: '„“‚‘',
-  Greek: '«»‘’',
-  Japanese: '「」「」',
-  Hungarian: '„”’’',
-  Polish: '„”‚‘',
-  Portuguese: '«»‘’',
-  Russian: '«»„“',
-  Spanish: '«»‘’',
-  Swedish: '””’’'
-}
-
-module.exports = {
-  async render() {
-    const mkdown = md({
-      html: this.config.allowHTML,
-      breaks: this.config.linebreaks,
-      linkify: this.config.linkify,
-      typographer: this.config.typographer,
-      quotes: _.get(quoteStyles, this.config.quotes, quoteStyles.English),
-      highlight(str, lang) {
-        if (lang === 'diagram') {
-          return `<pre class="diagram">` + Buffer.from(str, 'base64').toString() + `</pre>`
-        } else {
-          return `<pre><code class="language-${lang}">${_.escape(str)}</code></pre>`
-        }
-      }
-    })
-
-    if (this.config.underline) {
-      mkdown.use(underline)
-    }
-
-    mkdown.use(mdAttrs, {
-      allowedAttributes: ['id', 'class', 'target']
-    })
-
-    for (let child of this.children) {
-      const renderer = require(`../${child.key}/renderer.js`)
-      await renderer.init(mkdown, child.config)
-    }
-
-    return mkdown.render(this.input)
-  }
-}

+ 0 - 12
server/modules/rendering/markdown-core/underline.js

@@ -1,12 +0,0 @@
-const renderEm = (tokens, idx, opts, env, slf) => {
-  const token = tokens[idx]
-  if (token.markup === '_') {
-    token.tag = 'u'
-  }
-  return slf.renderToken(tokens, idx, opts)
-}
-
-module.exports = (md) => {
-  md.renderer.rules.em_open = renderEm
-  md.renderer.rules.em_close = renderEm
-}

+ 0 - 8
server/modules/rendering/markdown-emoji/definition.yml

@@ -1,8 +0,0 @@
-key: markdownEmoji
-title: Emoji
-description: Convert tags to emojis
-author: requarks.io
-icon: mdi-sticker-emoji
-enabledDefault: true
-dependsOn: markdown-core
-props: {}

+ 0 - 20
server/modules/rendering/markdown-emoji/renderer.js

@@ -1,20 +0,0 @@
-const mdEmoji = require('markdown-it-emoji')
-const twemoji = require('twemoji')
-
-// ------------------------------------
-// Markdown - Emoji
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    md.use(mdEmoji)
-
-    md.renderer.rules.emoji = (token, idx) => {
-      return twemoji.parse(token[idx].content, {
-        callback (icon, opts) {
-          return `/_assets/svg/twemoji/${icon}.svg`
-        }
-      })
-    }
-  }
-}

+ 0 - 13
server/modules/rendering/markdown-expandtabs/definition.yml

@@ -1,13 +0,0 @@
-key: markdownExpandtabs
-title: Expand Tabs
-description: Replace tabs with spaces in code blocks
-author: requarks.io
-icon: mdi-arrow-expand-horizontal
-enabledDefault: true
-dependsOn: markdown-core
-props:
-  tabWidth:
-    type: Number
-    title: Tab Width
-    hint: Amount of spaces for each tab
-    default: 4

+ 0 - 14
server/modules/rendering/markdown-expandtabs/renderer.js

@@ -1,14 +0,0 @@
-const mdExpandTabs = require('markdown-it-expand-tabs')
-const _ = require('lodash')
-
-// ------------------------------------
-// Markdown - Expand Tabs
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    md.use(mdExpandTabs, {
-      tabWidth: _.toInteger(conf.tabWidth || 4)
-    })
-  }
-}

+ 0 - 8
server/modules/rendering/markdown-footnotes/definition.yml

@@ -1,8 +0,0 @@
-key: markdownFootnotes
-title: Footnotes
-description: Parse footnotes references
-author: requarks.io
-icon: mdi-page-layout-footer
-enabledDefault: true
-dependsOn: markdown-core
-props: {}

+ 0 - 11
server/modules/rendering/markdown-footnotes/renderer.js

@@ -1,11 +0,0 @@
-const mdFootnote = require('markdown-it-footnote')
-
-// ------------------------------------
-// Markdown - Footnotes
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    md.use(mdFootnote)
-  }
-}

+ 0 - 8
server/modules/rendering/markdown-imsize/definition.yml

@@ -1,8 +0,0 @@
-key: markdownImsize
-title: Image Size
-description: Adds dimensions attributes to images
-author: requarks.io
-icon: mdi-image-size-select-large
-enabledDefault: true
-dependsOn: markdown-core
-props: {}

+ 0 - 11
server/modules/rendering/markdown-imsize/renderer.js

@@ -1,11 +0,0 @@
-const mdImsize = require('markdown-it-imsize')
-
-// ------------------------------------
-// Markdown - Image Size
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    md.use(mdImsize)
-  }
-}

+ 0 - 20
server/modules/rendering/markdown-katex/definition.yml

@@ -1,20 +0,0 @@
-key: markdownKatex
-title: Katex
-description: LaTeX Math + Chemical Expression Typesetting Renderer
-author: requarks.io
-icon: mdi-math-integral
-enabledDefault: true
-dependsOn: markdown-core
-props:
-  useInline:
-    type: Boolean
-    default: true
-    title: Inline TeX
-    hint: Process inline TeX expressions surrounded by $ symbols.
-    order: 1
-  useBlocks:
-    type: Boolean
-    default: true
-    title: TeX Blocks
-    hint: Process TeX blocks enclosed by $$ symbols.
-    order: 2

+ 0 - 1677
server/modules/rendering/markdown-katex/mhchem.js

@@ -1,1677 +0,0 @@
-/* eslint-disable */
-/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80: */
-
-/*************************************************************
- *
- *  KaTeX mhchem.js
- *
- *  This file implements a KaTeX version of mhchem version 3.3.0.
- *  It is adapted from MathJax/extensions/TeX/mhchem.js
- *  It differs from the MathJax version as follows:
- *    1. The interface is changed so that it can be called from KaTeX, not MathJax.
- *    2. \rlap and \llap are replaced with \mathrlap and \mathllap.
- *    3. Four lines of code are edited in order to use \raisebox instead of \raise.
- *    4. The reaction arrow code is simplified. All reaction arrows are rendered
- *       using KaTeX extensible arrows instead of building non-extensible arrows.
- *    5. \tripledash vertical alignment is slightly adjusted.
- *
- *    This code, as other KaTeX code, is released under the MIT license.
- *
- * /*************************************************************
- *
- *  MathJax/extensions/TeX/mhchem.js
- *
- *  Implements the \ce command for handling chemical formulas
- *  from the mhchem LaTeX package.
- *
- *  ---------------------------------------------------------------------
- *
- *  Copyright (c) 2011-2015 The MathJax Consortium
- *  Copyright (c) 2015-2018 Martin Hensel
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-//
-// Coding Style
-//   - use '' for identifiers that can by minified/uglified
-//   - use "" for strings that need to stay untouched
-
-// version: "3.3.0" for MathJax and KaTeX
-
-
-  //
-  //  This is the main function for handing the \ce and \pu commands.
-  //  It takes the argument to \ce or \pu and returns the corresponding TeX string.
-  //
-
-  module.exports = function (tokens, stateMachine) {
-    // Recreate the argument string from KaTeX's array of tokens.
-    var str = "";
-    var expectedLoc = tokens[tokens.length - 1].loc.start
-    for (var i = tokens.length - 1; i >= 0; i--) {
-      if(tokens[i].loc.start > expectedLoc) {
-        // context.consumeArgs has eaten a space.
-        str += " ";
-        expectedLoc = tokens[i].loc.start;
-      }
-      str += tokens[i].text;
-      expectedLoc += tokens[i].text.length;
-    }
-    var tex = texify.go(mhchemParser.go(str, stateMachine));
-    return tex;
-  };
-
-  //
-  // Core parser for mhchem syntax  (recursive)
-  //
-  /** @type {MhchemParser} */
-  var mhchemParser = {
-    //
-    // Parses mchem \ce syntax
-    //
-    // Call like
-    //   go("H2O");
-    //
-    go: function (input, stateMachine) {
-      if (!input) { return []; }
-      if (stateMachine === undefined) { stateMachine = 'ce'; }
-      var state = '0';
-
-      //
-      // String buffers for parsing:
-      //
-      // buffer.a == amount
-      // buffer.o == element
-      // buffer.b == left-side superscript
-      // buffer.p == left-side subscript
-      // buffer.q == right-side subscript
-      // buffer.d == right-side superscript
-      //
-      // buffer.r == arrow
-      // buffer.rdt == arrow, script above, type
-      // buffer.rd == arrow, script above, content
-      // buffer.rqt == arrow, script below, type
-      // buffer.rq == arrow, script below, content
-      //
-      // buffer.text_
-      // buffer.rm
-      // etc.
-      //
-      // buffer.parenthesisLevel == int, starting at 0
-      // buffer.sb == bool, space before
-      // buffer.beginsWithBond == bool
-      //
-      // These letters are also used as state names.
-      //
-      // Other states:
-      // 0 == begin of main part (arrow/operator unlikely)
-      // 1 == next entity
-      // 2 == next entity (arrow/operator unlikely)
-      // 3 == next atom
-      // c == macro
-      //
-      /** @type {Buffer} */
-      var buffer = {};
-      buffer['parenthesisLevel'] = 0;
-
-      input = input.replace(/\n/g, " ");
-      input = input.replace(/[\u2212\u2013\u2014\u2010]/g, "-");
-      input = input.replace(/[\u2026]/g, "...");
-
-      //
-      // Looks through mhchemParser.transitions, to execute a matching action
-      // (recursive)
-      //
-      var lastInput;
-      var watchdog = 10;
-      /** @type {ParserOutput[]} */
-      var output = [];
-      while (true) {
-        if (lastInput !== input) {
-          watchdog = 10;
-          lastInput = input;
-        } else {
-          watchdog--;
-        }
-        //
-        // Find actions in transition table
-        //
-        var machine = mhchemParser.stateMachines[stateMachine];
-        var t = machine.transitions[state] || machine.transitions['*'];
-        iterateTransitions:
-        for (var i=0; i<t.length; i++) {
-          var matches = mhchemParser.patterns.match_(t[i].pattern, input);
-          if (matches) {
-            //
-            // Execute actions
-            //
-            var task = t[i].task;
-            for (var iA=0; iA<task.action_.length; iA++) {
-              var o;
-              //
-              // Find and execute action
-              //
-              if (machine.actions[task.action_[iA].type_]) {
-                o = machine.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option);
-              } else if (mhchemParser.actions[task.action_[iA].type_]) {
-                o = mhchemParser.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option);
-              } else {
-                throw ["MhchemBugA", "mhchem bug A. Please report. (" + task.action_[iA].type_ + ")"];  // Trying to use non-existing action
-              }
-              //
-              // Add output
-              //
-              mhchemParser.concatArray(output, o);
-            }
-            //
-            // Set next state,
-            // Shorten input,
-            // Continue with next character
-            //   (= apply only one transition per position)
-            //
-            state = task.nextState || state;
-            if (input.length > 0) {
-              if (!task.revisit) {
-                input = matches.remainder;
-              }
-              if (!task.toContinue) {
-                break iterateTransitions;
-              }
-            } else {
-              return output;
-            }
-          }
-        }
-        //
-        // Prevent infinite loop
-        //
-        if (watchdog <= 0) {
-          throw ["MhchemBugU", "mhchem bug U. Please report."];  // Unexpected character
-        }
-      }
-    },
-    concatArray: function (a, b) {
-      if (b) {
-        if (Array.isArray(b)) {
-          for (var iB=0; iB<b.length; iB++) {
-            a.push(b[iB]);
-          }
-        } else {
-          a.push(b);
-        }
-      }
-    },
-
-    patterns: {
-      //
-      // Matching patterns
-      // either regexps or function that return null or {match_:"a", remainder:"bc"}
-      //
-      patterns: {
-        // property names must not look like integers ("2") for correct property traversal order, later on
-        'empty': /^$/,
-        'else': /^./,
-        'else2': /^./,
-        'space': /^\s/,
-        'space A': /^\s(?=[A-Z\\$])/,
-        'space$': /^\s$/,
-        'a-z': /^[a-z]/,
-        'x': /^x/,
-        'x$': /^x$/,
-        'i$': /^i$/,
-        'letters': /^(?:[a-zA-Z\u03B1-\u03C9\u0391-\u03A9?@]|(?:\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\s+|\{\}|(?![a-zA-Z]))))+/,
-        '\\greek': /^\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\s+|\{\}|(?![a-zA-Z]))/,
-        'one lowercase latin letter $': /^(?:([a-z])(?:$|[^a-zA-Z]))$/,
-        '$one lowercase latin letter$ $': /^\$(?:([a-z])(?:$|[^a-zA-Z]))\$$/,
-        'one lowercase greek letter $': /^(?:\$?[\u03B1-\u03C9]\$?|\$?\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)\s*\$?)(?:\s+|\{\}|(?![a-zA-Z]))$/,
-        'digits': /^[0-9]+/,
-        '-9.,9': /^[+\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))/,
-        '-9.,9 no missing 0': /^[+\-]?[0-9]+(?:[.,][0-9]+)?/,
-        '(-)(9.,9)(e)(99)': function (input) {
-          var m = input.match(/^(\+\-|\+\/\-|\+|\-|\\pm\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))?(\((?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))\))?(?:([eE]|\s*(\*|x|\\times|\u00D7)\s*10\^)([+\-]?[0-9]+|\{[+\-]?[0-9]+\}))?/);
-          if (m && m[0]) {
-            return { match_: m.splice(1), remainder: input.substr(m[0].length) };
-          }
-          return null;
-        },
-        '(-)(9)^(-9)': function (input) {
-          var m = input.match(/^(\+\-|\+\/\-|\+|\-|\\pm\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+)?)\^([+\-]?[0-9]+|\{[+\-]?[0-9]+\})/);
-          if (m && m[0]) {
-            return { match_: m.splice(1), remainder: input.substr(m[0].length) };
-          }
-          return null;
-        },
-        'state of aggregation $': function (input) {  // ... or crystal system
-          var a = mhchemParser.patterns.findObserveGroups(input, "", /^\([a-z]{1,3}(?=[\),])/, ")", "");  // (aq), (aq,$\infty$), (aq, sat)
-          if (a  &&  a.remainder.match(/^($|[\s,;\)\]\}])/)) { return a; }  //  AND end of 'phrase'
-          var m = input.match(/^(?:\((?:\\ca\s?)?\$[amothc]\$\))/);  // OR crystal system ($o$) (\ca$c$)
-          if (m) {
-            return { match_: m[0], remainder: input.substr(m[0].length) };
-          }
-          return null;
-        },
-        '_{(state of aggregation)}$': /^_\{(\([a-z]{1,3}\))\}/,
-        '{[(': /^(?:\\\{|\[|\()/,
-        ')]}': /^(?:\)|\]|\\\})/,
-        ', ': /^[,;]\s*/,
-        ',': /^[,;]/,
-        '.': /^[.]/,
-        '. ': /^([.\u22C5\u00B7\u2022])\s*/,
-        '...': /^\.\.\.(?=$|[^.])/,
-        '* ': /^([*])\s*/,
-        '^{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "^{", "", "", "}"); },
-        '^($...$)': function (input) { return mhchemParser.patterns.findObserveGroups(input, "^", "$", "$", ""); },
-        '^a': /^\^([0-9]+|[^\\_])/,
-        '^\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "^", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); },
-        '^\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "^", /^\\[a-zA-Z]+\{/, "}", ""); },
-        '^\\x': /^\^(\\[a-zA-Z]+)\s*/,
-        '^(-1)': /^\^(-?\d+)/,
-        '\'': /^'/,
-        '_{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "_{", "", "", "}"); },
-        '_($...$)': function (input) { return mhchemParser.patterns.findObserveGroups(input, "_", "$", "$", ""); },
-        '_9': /^_([+\-]?[0-9]+|[^\\])/,
-        '_\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "_", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); },
-        '_\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "_", /^\\[a-zA-Z]+\{/, "}", ""); },
-        '_\\x': /^_(\\[a-zA-Z]+)\s*/,
-        '^_': /^(?:\^(?=_)|\_(?=\^)|[\^_]$)/,
-        '{}': /^\{\}/,
-        '{...}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "", "{", "}", ""); },
-        '{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "{", "", "", "}"); },
-        '$...$': function (input) { return mhchemParser.patterns.findObserveGroups(input, "", "$", "$", ""); },
-        '${(...)}$': function (input) { return mhchemParser.patterns.findObserveGroups(input, "${", "", "", "}$"); },
-        '$(...)$': function (input) { return mhchemParser.patterns.findObserveGroups(input, "$", "", "", "$"); },
-        '=<>': /^[=<>]/,
-        '#': /^[#\u2261]/,
-        '+': /^\+/,
-        '-$': /^-(?=[\s_},;\]/]|$|\([a-z]+\))/,  // -space -, -; -] -/ -$ -state-of-aggregation
-        '-9': /^-(?=[0-9])/,
-        '- orbital overlap': /^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/,
-        '-': /^-/,
-        'pm-operator': /^(?:\\pm|\$\\pm\$|\+-|\+\/-)/,
-        'operator': /^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/,
-        'arrowUpDown': /^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/,
-        '\\bond{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\bond{", "", "", "}"); },
-        '->': /^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/,
-        'CMT': /^[CMT](?=\[)/,
-        '[(...)]': function (input) { return mhchemParser.patterns.findObserveGroups(input, "[", "", "", "]"); },
-        '1st-level escape': /^(&|\\\\|\\hline)\s*/,
-        '\\,': /^(?:\\[,\ ;:])/,  // \\x - but output no space before
-        '\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); },
-        '\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", ""); },
-        '\\ca': /^\\ca(?:\s+|(?![a-zA-Z]))/,
-        '\\x': /^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/,
-        'orbital': /^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/,  // only those with numbers in front, because the others will be formatted correctly anyway
-        'others': /^[\/~|]/,
-        '\\frac{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\frac{", "", "", "}", "{", "", "", "}"); },
-        '\\overset{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\overset{", "", "", "}", "{", "", "", "}"); },
-        '\\underset{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\underset{", "", "", "}", "{", "", "", "}"); },
-        '\\underbrace{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\underbrace{", "", "", "}_", "{", "", "", "}"); },
-        '\\color{(...)}0': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}"); },
-        '\\color{(...)}{(...)}1': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}", "{", "", "", "}"); },
-        '\\color(...){(...)}2': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\color", "\\", "", /^(?=\{)/, "{", "", "", "}"); },
-        '\\ce{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\ce{", "", "", "}"); },
-        'oxidation$': /^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,
-        'd-oxidation$': /^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,  // 0 could be oxidation or charge
-        'roman numeral': /^[IVX]+/,
-        '1/2$': /^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/,
-        'amount': function (input) {
-          var match;
-          // e.g. 2, 0.5, 1/2, -2, n/2, +;  $a$ could be added later in parsing
-          match = input.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/);
-          if (match) {
-            return { match_: match[0], remainder: input.substr(match[0].length) };
-          }
-          var a = mhchemParser.patterns.findObserveGroups(input, "", "$", "$", "");
-          if (a) {  // e.g. $2n-1$, $-$
-            match = a.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/);
-            if (match) {
-              return { match_: match[0], remainder: input.substr(match[0].length) };
-            }
-          }
-          return null;
-        },
-        'amount2': function (input) { return this['amount'](input); },
-        '(KV letters),': /^(?:[A-Z][a-z]{0,2}|i)(?=,)/,
-        'formula$': function (input) {
-          if (input.match(/^\([a-z]+\)$/)) { return null; }  // state of aggregation = no formula
-          var match = input.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/);
-          if (match) {
-            return { match_: match[0], remainder: input.substr(match[0].length) };
-          }
-          return null;
-        },
-        'uprightEntities': /^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,
-        '/': /^\s*(\/)\s*/,
-        '//': /^\s*(\/\/)\s*/,
-        '*': /^\s*[*.]\s*/
-      },
-      findObserveGroups: function (input, begExcl, begIncl, endIncl, endExcl, beg2Excl, beg2Incl, end2Incl, end2Excl, combine) {
-        /** @type {{(input: string, pattern: string | RegExp): string | string[] | null;}} */
-        var _match = function (input, pattern) {
-          if (typeof pattern === "string") {
-            if (input.indexOf(pattern) !== 0) { return null; }
-            return pattern;
-          } else {
-            var match = input.match(pattern);
-            if (!match) { return null; }
-            return match[0];
-          }
-        };
-        /** @type {{(input: string, i: number, endChars: string | RegExp): {endMatchBegin: number, endMatchEnd: number} | null;}} */
-        var _findObserveGroups = function (input, i, endChars) {
-          var braces = 0;
-          while (i < input.length) {
-            var a = input.charAt(i);
-            var match = _match(input.substr(i), endChars);
-            if (match !== null  &&  braces === 0) {
-              return { endMatchBegin: i, endMatchEnd: i + match.length };
-            } else if (a === "{") {
-              braces++;
-            } else if (a === "}") {
-              if (braces === 0) {
-                throw ["ExtraCloseMissingOpen", "Extra close brace or missing open brace"];
-              } else {
-                braces--;
-              }
-            }
-            i++;
-          }
-          if (braces > 0) {
-            return null;
-          }
-          return null;
-        };
-        var match = _match(input, begExcl);
-        if (match === null) { return null; }
-        input = input.substr(match.length);
-        match = _match(input, begIncl);
-        if (match === null) { return null; }
-        var e = _findObserveGroups(input, match.length, endIncl || endExcl);
-        if (e === null) { return null; }
-        var match1 = input.substring(0, (endIncl ? e.endMatchEnd : e.endMatchBegin));
-        if (!(beg2Excl || beg2Incl)) {
-          return {
-            match_: match1,
-            remainder: input.substr(e.endMatchEnd)
-          };
-        } else {
-          var group2 = this.findObserveGroups(input.substr(e.endMatchEnd), beg2Excl, beg2Incl, end2Incl, end2Excl);
-          if (group2 === null) { return null; }
-          /** @type {string[]} */
-          var matchRet = [match1, group2.match_];
-          return {
-            match_: (combine ? matchRet.join("") : matchRet),
-            remainder: group2.remainder
-          };
-        }
-      },
-
-      //
-      // Matching function
-      // e.g. match("a", input) will look for the regexp called "a" and see if it matches
-      // returns null or {match_:"a", remainder:"bc"}
-      //
-      match_: function (m, input) {
-        var pattern = mhchemParser.patterns.patterns[m];
-        if (pattern === undefined) {
-          throw ["MhchemBugP", "mhchem bug P. Please report. (" + m + ")"];  // Trying to use non-existing pattern
-        } else if (typeof pattern === "function") {
-          return mhchemParser.patterns.patterns[m](input);  // cannot use cached var pattern here, because some pattern functions need this===mhchemParser
-        } else {  // RegExp
-          var match = input.match(pattern);
-          if (match) {
-            var mm;
-            if (match[2]) {
-              mm = [ match[1], match[2] ];
-            } else if (match[1]) {
-              mm = match[1];
-            } else {
-              mm = match[0];
-            }
-            return { match_: mm, remainder: input.substr(match[0].length) };
-          }
-          return null;
-        }
-      }
-    },
-
-    //
-    // Generic state machine actions
-    //
-    actions: {
-      'a=': function (buffer, m) { buffer.a = (buffer.a || "") + m; },
-      'b=': function (buffer, m) { buffer.b = (buffer.b || "") + m; },
-      'p=': function (buffer, m) { buffer.p = (buffer.p || "") + m; },
-      'o=': function (buffer, m) { buffer.o = (buffer.o || "") + m; },
-      'q=': function (buffer, m) { buffer.q = (buffer.q || "") + m; },
-      'd=': function (buffer, m) { buffer.d = (buffer.d || "") + m; },
-      'rm=': function (buffer, m) { buffer.rm = (buffer.rm || "") + m; },
-      'text=': function (buffer, m) { buffer.text_ = (buffer.text_ || "") + m; },
-      'insert': function (buffer, m, a) { return { type_: a }; },
-      'insert+p1': function (buffer, m, a) { return { type_: a, p1: m }; },
-      'insert+p1+p2': function (buffer, m, a) { return { type_: a, p1: m[0], p2: m[1] }; },
-      'copy': function (buffer, m) { return m; },
-      'rm': function (buffer, m) { return { type_: 'rm', p1: m || ""}; },
-      'text': function (buffer, m) { return mhchemParser.go(m, 'text'); },
-      '{text}': function (buffer, m) {
-        var ret = [ "{" ];
-        mhchemParser.concatArray(ret, mhchemParser.go(m, 'text'));
-        ret.push("}");
-        return ret;
-      },
-      'tex-math': function (buffer, m) { return mhchemParser.go(m, 'tex-math'); },
-      'tex-math tight': function (buffer, m) { return mhchemParser.go(m, 'tex-math tight'); },
-      'bond': function (buffer, m, k) { return { type_: 'bond', kind_: k || m }; },
-      'color0-output': function (buffer, m) { return { type_: 'color0', color: m[0] }; },
-      'ce': function (buffer, m) { return mhchemParser.go(m); },
-      '1/2': function (buffer, m) {
-        /** @type {ParserOutput[]} */
-        var ret = [];
-        if (m.match(/^[+\-]/)) {
-          ret.push(m.substr(0, 1));
-          m = m.substr(1);
-        }
-        var n = m.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/);
-        n[1] = n[1].replace(/\$/g, "");
-        ret.push({ type_: 'frac', p1: n[1], p2: n[2] });
-        if (n[3]) {
-          n[3] = n[3].replace(/\$/g, "");
-          ret.push({ type_: 'tex-math', p1: n[3] });
-        }
-        return ret;
-      },
-      '9,9': function (buffer, m) { return mhchemParser.go(m, '9,9'); }
-    },
-    //
-    // createTransitions
-    // convert  { 'letter': { 'state': { action_: 'output' } } }  to  { 'state' => [ { pattern: 'letter', task: { action_: [{type_: 'output'}] } } ] }
-    // with expansion of 'a|b' to 'a' and 'b' (at 2 places)
-    //
-    createTransitions: function (o) {
-      var pattern, state;
-      /** @type {string[]} */
-      var stateArray;
-      var i;
-      //
-      // 1. Collect all states
-      //
-      /** @type {Transitions} */
-      var transitions = {};
-      for (pattern in o) {
-        for (state in o[pattern]) {
-          stateArray = state.split("|");
-          o[pattern][state].stateArray = stateArray;
-          for (i=0; i<stateArray.length; i++) {
-            transitions[stateArray[i]] = [];
-          }
-        }
-      }
-      //
-      // 2. Fill states
-      //
-      for (pattern in o) {
-        for (state in o[pattern]) {
-          stateArray = o[pattern][state].stateArray || [];
-          for (i=0; i<stateArray.length; i++) {
-            //
-            // 2a. Normalize actions into array:  'text=' ==> [{type_:'text='}]
-            // (Note to myself: Resolving the function here would be problematic. It would need .bind (for *this*) and currying (for *option*).)
-            //
-            /** @type {any} */
-            var p = o[pattern][state];
-            if (p.action_) {
-              p.action_ = [].concat(p.action_);
-              for (var k=0; k<p.action_.length; k++) {
-                if (typeof p.action_[k] === "string") {
-                  p.action_[k] = { type_: p.action_[k] };
-                }
-              }
-            } else {
-              p.action_ = [];
-            }
-            //
-            // 2.b Multi-insert
-            //
-            var patternArray = pattern.split("|");
-            for (var j=0; j<patternArray.length; j++) {
-              if (stateArray[i] === '*') {  // insert into all
-                for (var t in transitions) {
-                  transitions[t].push({ pattern: patternArray[j], task: p });
-                }
-              } else {
-                transitions[stateArray[i]].push({ pattern: patternArray[j], task: p });
-              }
-            }
-          }
-        }
-      }
-      return transitions;
-    },
-    stateMachines: {}
-  };
-
-  //
-  // Definition of state machines
-  //
-  mhchemParser.stateMachines = {
-    //
-    // \ce state machines
-    //
-    //#region ce
-    'ce': {  // main parser
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': { action_: 'output' } },
-        'else':  {
-          '0|1|2': { action_: 'beginsWithBond=false', revisit: true, toContinue: true } },
-        'oxidation$': {
-          '0': { action_: 'oxidation-output' } },
-        'CMT': {
-          'r': { action_: 'rdt=', nextState: 'rt' },
-          'rd': { action_: 'rqt=', nextState: 'rdt' } },
-        'arrowUpDown': {
-          '0|1|2|as': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '1' } },
-        'uprightEntities': {
-          '0|1|2': { action_: [ 'o=', 'output' ], nextState: '1' } },
-        'orbital': {
-          '0|1|2|3': { action_: 'o=', nextState: 'o' } },
-        '->': {
-          '0|1|2|3': { action_: 'r=', nextState: 'r' },
-          'a|as': { action_: [ 'output', 'r=' ], nextState: 'r' },
-          '*': { action_: [ 'output', 'r=' ], nextState: 'r' } },
-        '+': {
-          'o': { action_: 'd= kv',  nextState: 'd' },
-          'd|D': { action_: 'd=', nextState: 'd' },
-          'q': { action_: 'd=',  nextState: 'qd' },
-          'qd|qD': { action_: 'd=', nextState: 'qd' },
-          'dq': { action_: [ 'output', 'd=' ], nextState: 'd' },
-          '3': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '0' } },
-        'amount': {
-          '0|2': { action_: 'a=', nextState: 'a' } },
-        'pm-operator': {
-          '0|1|2|a|as': { action_: [ 'sb=false', 'output', { type_: 'operator', option: '\\pm' } ], nextState: '0' } },
-        'operator': {
-          '0|1|2|a|as': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '0' } },
-        '-$': {
-          'o|q': { action_: [ 'charge or bond', 'output' ],  nextState: 'qd' },
-          'd': { action_: 'd=', nextState: 'd' },
-          'D': { action_: [ 'output', { type_: 'bond', option: "-" } ], nextState: '3' },
-          'q': { action_: 'd=',  nextState: 'qd' },
-          'qd': { action_: 'd=', nextState: 'qd' },
-          'qD|dq': { action_: [ 'output', { type_: 'bond', option: "-" } ], nextState: '3' } },
-        '-9': {
-          '3|o': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '3' } },
-        '- orbital overlap': {
-          'o': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' },
-          'd': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' } },
-        '-': {
-          '0|1|2': { action_: [ { type_: 'output', option: 1 }, 'beginsWithBond=true', { type_: 'bond', option: "-" } ], nextState: '3' },
-          '3': { action_: { type_: 'bond', option: "-" } },
-          'a': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' },
-          'as': { action_: [ { type_: 'output', option: 2 }, { type_: 'bond', option: "-" } ], nextState: '3' },
-          'b': { action_: 'b=' },
-          'o': { action_: { type_: '- after o/d', option: false }, nextState: '2' },
-          'q': { action_: { type_: '- after o/d', option: false }, nextState: '2' },
-          'd|qd|dq': { action_: { type_: '- after o/d', option: true }, nextState: '2' },
-          'D|qD|p': { action_: [ 'output', { type_: 'bond', option: "-" } ], nextState: '3' } },
-        'amount2': {
-          '1|3': { action_: 'a=', nextState: 'a' } },
-        'letters': {
-          '0|1|2|3|a|as|b|p|bp|o': { action_: 'o=', nextState: 'o' },
-          'q|dq': { action_: ['output', 'o='], nextState: 'o' },
-          'd|D|qd|qD': { action_: 'o after d', nextState: 'o' } },
-        'digits': {
-          'o': { action_: 'q=', nextState: 'q' },
-          'd|D': { action_: 'q=', nextState: 'dq' },
-          'q': { action_: [ 'output', 'o=' ], nextState: 'o' },
-          'a': { action_: 'o=', nextState: 'o' } },
-        'space A': {
-          'b|p|bp': {} },
-        'space': {
-          'a': { nextState: 'as' },
-          '0': { action_: 'sb=false' },
-          '1|2': { action_: 'sb=true' },
-          'r|rt|rd|rdt|rdq': { action_: 'output', nextState: '0' },
-          '*': { action_: [ 'output', 'sb=true' ], nextState: '1'} },
-        '1st-level escape': {
-          '1|2': { action_: [ 'output', { type_: 'insert+p1', option: '1st-level escape' } ] },
-          '*': { action_: [ 'output', { type_: 'insert+p1', option: '1st-level escape' } ], nextState: '0' } },
-        '[(...)]': {
-          'r|rt': { action_: 'rd=', nextState: 'rd' },
-          'rd|rdt': { action_: 'rq=', nextState: 'rdq' } },
-        '...': {
-          'o|d|D|dq|qd|qD': { action_: [ 'output', { type_: 'bond', option: "..." } ], nextState: '3' },
-          '*': { action_: [ { type_: 'output', option: 1 }, { type_: 'insert', option: 'ellipsis' } ], nextState: '1' } },
-        '. |* ': {
-          '*': { action_: [ 'output', { type_: 'insert', option: 'addition compound' } ], nextState: '1' } },
-        'state of aggregation $': {
-          '*': { action_: [ 'output', 'state of aggregation' ], nextState: '1' } },
-        '{[(': {
-          'a|as|o': { action_: [ 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' },
-          '0|1|2|3': { action_: [ 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' },
-          '*': { action_: [ 'output', 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' } },
-        ')]}': {
-          '0|1|2|3|b|p|bp|o': { action_: [ 'o=', 'parenthesisLevel--' ], nextState: 'o' },
-          'a|as|d|D|q|qd|qD|dq': { action_: [ 'output', 'o=', 'parenthesisLevel--' ], nextState: 'o' } },
-        ', ': {
-          '*': { action_: [ 'output', 'comma' ], nextState: '0' } },
-        '^_': {  // ^ and _ without a sensible argument
-          '*': { } },
-        '^{(...)}|^($...$)': {
-          '0|1|2|as': { action_: 'b=', nextState: 'b' },
-          'p': { action_: 'b=', nextState: 'bp' },
-          '3|o': { action_: 'd= kv', nextState: 'D' },
-          'q': { action_: 'd=', nextState: 'qD' },
-          'd|D|qd|qD|dq': { action_: [ 'output', 'd=' ], nextState: 'D' } },
-        '^a|^\\x{}{}|^\\x{}|^\\x|\'': {
-          '0|1|2|as': { action_: 'b=', nextState: 'b' },
-          'p': { action_: 'b=', nextState: 'bp' },
-          '3|o': { action_: 'd= kv', nextState: 'd' },
-          'q': { action_: 'd=', nextState: 'qd' },
-          'd|qd|D|qD': { action_: 'd=' },
-          'dq': { action_: [ 'output', 'd=' ], nextState: 'd' } },
-        '_{(state of aggregation)}$': {
-          'd|D|q|qd|qD|dq': { action_: [ 'output', 'q=' ], nextState: 'q' } },
-        '_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x': {
-          '0|1|2|as': { action_: 'p=', nextState: 'p' },
-          'b': { action_: 'p=', nextState: 'bp' },
-          '3|o': { action_: 'q=', nextState: 'q' },
-          'd|D': { action_: 'q=', nextState: 'dq' },
-          'q|qd|qD|dq': { action_: [ 'output', 'q=' ], nextState: 'q' } },
-        '=<>': {
-          '0|1|2|3|a|as|o|q|d|D|qd|qD|dq': { action_: [ { type_: 'output', option: 2 }, 'bond' ], nextState: '3' } },
-        '#': {
-          '0|1|2|3|a|as|o': { action_: [ { type_: 'output', option: 2 }, { type_: 'bond', option: "#" } ], nextState: '3' } },
-        '{}': {
-          '*': { action_: { type_: 'output', option: 1 },  nextState: '1' } },
-        '{...}': {
-          '0|1|2|3|a|as|b|p|bp': { action_: 'o=', nextState: 'o' },
-          'o|d|D|q|qd|qD|dq': { action_: [ 'output', 'o=' ], nextState: 'o' } },
-        '$...$': {
-          'a': { action_: 'a=' },  // 2$n$
-          '0|1|2|3|as|b|p|bp|o': { action_: 'o=', nextState: 'o' },  // not 'amount'
-          'as|o': { action_: 'o=' },
-          'q|d|D|qd|qD|dq': { action_: [ 'output', 'o=' ], nextState: 'o' } },
-        '\\bond{(...)}': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'bond' ], nextState: "3" } },
-        '\\frac{(...)}': {
-          '*': { action_: [ { type_: 'output', option: 1 }, 'frac-output' ], nextState: '3' } },
-        '\\overset{(...)}': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'overset-output' ], nextState: '3' } },
-        '\\underset{(...)}': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'underset-output' ], nextState: '3' } },
-        '\\underbrace{(...)}': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'underbrace-output' ], nextState: '3' } },
-        '\\color{(...)}{(...)}1|\\color(...){(...)}2': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'color-output' ], nextState: '3' } },
-        '\\color{(...)}0': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'color0-output' ] } },
-        '\\ce{(...)}': {
-          '*': { action_: [ { type_: 'output', option: 2 }, 'ce' ], nextState: '3' } },
-        '\\,': {
-          '*': { action_: [ { type_: 'output', option: 1 }, 'copy' ], nextState: '1' } },
-        '\\x{}{}|\\x{}|\\x': {
-          '0|1|2|3|a|as|b|p|bp|o|c0': { action_: [ 'o=', 'output' ], nextState: '3' },
-          '*': { action_: ['output', 'o=', 'output' ], nextState: '3' } },
-        'others': {
-          '*': { action_: [ { type_: 'output', option: 1 }, 'copy' ], nextState: '3' } },
-        'else2': {
-          'a': { action_: 'a to o', nextState: 'o', revisit: true },
-          'as': { action_: [ 'output', 'sb=true' ], nextState: '1', revisit: true },
-          'r|rt|rd|rdt|rdq': { action_: [ 'output' ], nextState: '0', revisit: true },
-          '*': { action_: [ 'output', 'copy' ], nextState: '3' } }
-      }),
-      actions: {
-        'o after d': function (buffer, m) {
-          var ret;
-          if ((buffer.d || "").match(/^[0-9]+$/)) {
-            var tmp = buffer.d;
-            buffer.d = undefined;
-            ret = this['output'](buffer);
-            buffer.b = tmp;
-          } else {
-            ret = this['output'](buffer);
-          }
-          mhchemParser.actions['o='](buffer, m);
-          return ret;
-        },
-        'd= kv': function (buffer, m) {
-          buffer.d = m;
-          buffer.dType = 'kv';
-        },
-        'charge or bond': function (buffer, m) {
-          if (buffer['beginsWithBond']) {
-            /** @type {ParserOutput[]} */
-            var ret = [];
-            mhchemParser.concatArray(ret, this['output'](buffer));
-            mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-"));
-            return ret;
-          } else {
-            buffer.d = m;
-          }
-        },
-        '- after o/d': function (buffer, m, isAfterD) {
-          var c1 = mhchemParser.patterns.match_('orbital', buffer.o || "");
-          var c2 = mhchemParser.patterns.match_('one lowercase greek letter $', buffer.o || "");
-          var c3 = mhchemParser.patterns.match_('one lowercase latin letter $', buffer.o || "");
-          var c4 = mhchemParser.patterns.match_('$one lowercase latin letter$ $', buffer.o || "");
-          var hyphenFollows =  m==="-" && ( c1 && c1.remainder===""  ||  c2  ||  c3  ||  c4 );
-          if (hyphenFollows && !buffer.a && !buffer.b && !buffer.p && !buffer.d && !buffer.q && !c1 && c3) {
-            buffer.o = '$' + buffer.o + '$';
-          }
-          /** @type {ParserOutput[]} */
-          var ret = [];
-          if (hyphenFollows) {
-            mhchemParser.concatArray(ret, this['output'](buffer));
-            ret.push({ type_: 'hyphen' });
-          } else {
-            c1 = mhchemParser.patterns.match_('digits', buffer.d || "");
-            if (isAfterD && c1 && c1.remainder==='') {
-              mhchemParser.concatArray(ret, mhchemParser.actions['d='](buffer, m));
-              mhchemParser.concatArray(ret, this['output'](buffer));
-            } else {
-              mhchemParser.concatArray(ret, this['output'](buffer));
-              mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-"));
-            }
-          }
-          return ret;
-        },
-        'a to o': function (buffer) {
-          buffer.o = buffer.a;
-          buffer.a = undefined;
-        },
-        'sb=true': function (buffer) { buffer.sb = true; },
-        'sb=false': function (buffer) { buffer.sb = false; },
-        'beginsWithBond=true': function (buffer) { buffer['beginsWithBond'] = true; },
-        'beginsWithBond=false': function (buffer) { buffer['beginsWithBond'] = false; },
-        'parenthesisLevel++': function (buffer) { buffer['parenthesisLevel']++; },
-        'parenthesisLevel--': function (buffer) { buffer['parenthesisLevel']--; },
-        'state of aggregation': function (buffer, m) {
-          return { type_: 'state of aggregation', p1: mhchemParser.go(m, 'o') };
-        },
-        'comma': function (buffer, m) {
-          var a = m.replace(/\s*$/, '');
-          var withSpace = (a !== m);
-          if (withSpace  &&  buffer['parenthesisLevel'] === 0) {
-            return { type_: 'comma enumeration L', p1: a };
-          } else {
-            return { type_: 'comma enumeration M', p1: a };
-          }
-        },
-        'output': function (buffer, m, entityFollows) {
-          // entityFollows:
-          //   undefined = if we have nothing else to output, also ignore the just read space (buffer.sb)
-          //   1 = an entity follows, never omit the space if there was one just read before (can only apply to state 1)
-          //   2 = 1 + the entity can have an amount, so output a\, instead of converting it to o (can only apply to states a|as)
-          /** @type {ParserOutput | ParserOutput[]} */
-          var ret;
-          if (!buffer.r) {
-            ret = [];
-            if (!buffer.a && !buffer.b && !buffer.p && !buffer.o && !buffer.q && !buffer.d && !entityFollows) {
-              //ret = [];
-            } else {
-              if (buffer.sb) {
-                ret.push({ type_: 'entitySkip' });
-              }
-              if (!buffer.o && !buffer.q && !buffer.d && !buffer.b && !buffer.p && entityFollows!==2) {
-                buffer.o = buffer.a;
-                buffer.a = undefined;
-              } else if (!buffer.o && !buffer.q && !buffer.d && (buffer.b || buffer.p)) {
-                buffer.o = buffer.a;
-                buffer.d = buffer.b;
-                buffer.q = buffer.p;
-                buffer.a = buffer.b = buffer.p = undefined;
-              } else {
-                if (buffer.o && buffer.dType==='kv' && mhchemParser.patterns.match_('d-oxidation$', buffer.d || "")) {
-                  buffer.dType = 'oxidation';
-                } else if (buffer.o && buffer.dType==='kv' && !buffer.q) {
-                  buffer.dType = undefined;
-                }
-              }
-              ret.push({
-                type_: 'chemfive',
-                a: mhchemParser.go(buffer.a, 'a'),
-                b: mhchemParser.go(buffer.b, 'bd'),
-                p: mhchemParser.go(buffer.p, 'pq'),
-                o: mhchemParser.go(buffer.o, 'o'),
-                q: mhchemParser.go(buffer.q, 'pq'),
-                d: mhchemParser.go(buffer.d, (buffer.dType === 'oxidation' ? 'oxidation' : 'bd')),
-                dType: buffer.dType
-              });
-            }
-          } else {  // r
-            /** @type {ParserOutput[]} */
-            var rd;
-            if (buffer.rdt === 'M') {
-              rd = mhchemParser.go(buffer.rd, 'tex-math');
-            } else if (buffer.rdt === 'T') {
-              rd = [ { type_: 'text', p1: buffer.rd || "" } ];
-            } else {
-              rd = mhchemParser.go(buffer.rd);
-            }
-            /** @type {ParserOutput[]} */
-            var rq;
-            if (buffer.rqt === 'M') {
-              rq = mhchemParser.go(buffer.rq, 'tex-math');
-            } else if (buffer.rqt === 'T') {
-              rq = [ { type_: 'text', p1: buffer.rq || ""} ];
-            } else {
-              rq = mhchemParser.go(buffer.rq);
-            }
-            ret = {
-              type_: 'arrow',
-              r: buffer.r,
-              rd: rd,
-              rq: rq
-            };
-          }
-          for (var p in buffer) {
-            if (p !== 'parenthesisLevel'  &&  p !== 'beginsWithBond') {
-              delete buffer[p];
-            }
-          }
-          return ret;
-        },
-        'oxidation-output': function (buffer, m) {
-          var ret = [ "{" ];
-          mhchemParser.concatArray(ret, mhchemParser.go(m, 'oxidation'));
-          ret.push("}");
-          return ret;
-        },
-        'frac-output': function (buffer, m) {
-          return { type_: 'frac-ce', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };
-        },
-        'overset-output': function (buffer, m) {
-          return { type_: 'overset', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };
-        },
-        'underset-output': function (buffer, m) {
-          return { type_: 'underset', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };
-        },
-        'underbrace-output': function (buffer, m) {
-          return { type_: 'underbrace', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) };
-        },
-        'color-output': function (buffer, m) {
-          return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1]) };
-        },
-        'r=': function (buffer, m) { buffer.r = m; },
-        'rdt=': function (buffer, m) { buffer.rdt = m; },
-        'rd=': function (buffer, m) { buffer.rd = m; },
-        'rqt=': function (buffer, m) { buffer.rqt = m; },
-        'rq=': function (buffer, m) { buffer.rq = m; },
-        'operator': function (buffer, m, p1) { return { type_: 'operator', kind_: (p1 || m) }; }
-      }
-    },
-    'a': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': {} },
-        '1/2$': {
-          '0': { action_: '1/2' } },
-        'else': {
-          '0': { nextState: '1', revisit: true } },
-        '$(...)$': {
-          '*': { action_: 'tex-math tight', nextState: '1' } },
-        ',': {
-          '*': { action_: { type_: 'insert', option: 'commaDecimal' } } },
-        'else2': {
-          '*': { action_: 'copy' } }
-      }),
-      actions: {}
-    },
-    'o': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': {} },
-        '1/2$': {
-          '0': { action_: '1/2' } },
-        'else': {
-          '0': { nextState: '1', revisit: true } },
-        'letters': {
-          '*': { action_: 'rm' } },
-        '\\ca': {
-          '*': { action_: { type_: 'insert', option: 'circa' } } },
-        '\\x{}{}|\\x{}|\\x': {
-          '*': { action_: 'copy' } },
-        '${(...)}$|$(...)$': {
-          '*': { action_: 'tex-math' } },
-        '{(...)}': {
-          '*': { action_: '{text}' } },
-        'else2': {
-          '*': { action_: 'copy' } }
-      }),
-      actions: {}
-    },
-    'text': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': { action_: 'output' } },
-        '{...}': {
-          '*': { action_: 'text=' } },
-        '${(...)}$|$(...)$': {
-          '*': { action_: 'tex-math' } },
-        '\\greek': {
-          '*': { action_: [ 'output', 'rm' ] } },
-        '\\,|\\x{}{}|\\x{}|\\x': {
-          '*': { action_: [ 'output', 'copy' ] } },
-        'else': {
-          '*': { action_: 'text=' } }
-      }),
-      actions: {
-        'output': function (buffer) {
-          if (buffer.text_) {
-            /** @type {ParserOutput} */
-            var ret = { type_: 'text', p1: buffer.text_ };
-            for (var p in buffer) { delete buffer[p]; }
-            return ret;
-          }
-        }
-      }
-    },
-    'pq': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': {} },
-        'state of aggregation $': {
-          '*': { action_: 'state of aggregation' } },
-        'i$': {
-          '0': { nextState: '!f', revisit: true } },
-        '(KV letters),': {
-          '0': { action_: 'rm', nextState: '0' } },
-        'formula$': {
-          '0': { nextState: 'f', revisit: true } },
-        '1/2$': {
-          '0': { action_: '1/2' } },
-        'else': {
-          '0': { nextState: '!f', revisit: true } },
-        '${(...)}$|$(...)$': {
-          '*': { action_: 'tex-math' } },
-        '{(...)}': {
-          '*': { action_: 'text' } },
-        'a-z': {
-          'f': { action_: 'tex-math' } },
-        'letters': {
-          '*': { action_: 'rm' } },
-        '-9.,9': {
-          '*': { action_: '9,9'  } },
-        ',': {
-          '*': { action_: { type_: 'insert+p1', option: 'comma enumeration S' } } },
-        '\\color{(...)}{(...)}1|\\color(...){(...)}2': {
-          '*': { action_: 'color-output' } },
-        '\\color{(...)}0': {
-          '*': { action_: 'color0-output' } },
-        '\\ce{(...)}': {
-          '*': { action_: 'ce' } },
-        '\\,|\\x{}{}|\\x{}|\\x': {
-          '*': { action_: 'copy' } },
-        'else2': {
-          '*': { action_: 'copy' } }
-      }),
-      actions: {
-        'state of aggregation': function (buffer, m) {
-          return { type_: 'state of aggregation subscript', p1: mhchemParser.go(m, 'o') };
-        },
-        'color-output': function (buffer, m) {
-          return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1], 'pq') };
-        }
-      }
-    },
-    'bd': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': {} },
-        'x$': {
-          '0': { nextState: '!f', revisit: true } },
-        'formula$': {
-          '0': { nextState: 'f', revisit: true } },
-        'else': {
-          '0': { nextState: '!f', revisit: true } },
-        '-9.,9 no missing 0': {
-          '*': { action_: '9,9' } },
-        '.': {
-          '*': { action_: { type_: 'insert', option: 'electron dot' } } },
-        'a-z': {
-          'f': { action_: 'tex-math' } },
-        'x': {
-          '*': { action_: { type_: 'insert', option: 'KV x' } } },
-        'letters': {
-          '*': { action_: 'rm' } },
-        '\'': {
-          '*': { action_: { type_: 'insert', option: 'prime' } } },
-        '${(...)}$|$(...)$': {
-          '*': { action_: 'tex-math' } },
-        '{(...)}': {
-          '*': { action_: 'text' } },
-        '\\color{(...)}{(...)}1|\\color(...){(...)}2': {
-          '*': { action_: 'color-output' } },
-        '\\color{(...)}0': {
-          '*': { action_: 'color0-output' } },
-        '\\ce{(...)}': {
-          '*': { action_: 'ce' } },
-        '\\,|\\x{}{}|\\x{}|\\x': {
-          '*': { action_: 'copy' } },
-        'else2': {
-          '*': { action_: 'copy' } }
-      }),
-      actions: {
-        'color-output': function (buffer, m) {
-          return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1], 'bd') };
-        }
-      }
-    },
-    'oxidation': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': {} },
-        'roman numeral': {
-          '*': { action_: 'roman-numeral' } },
-        '${(...)}$|$(...)$': {
-          '*': { action_: 'tex-math' } },
-        'else': {
-          '*': { action_: 'copy' } }
-      }),
-      actions: {
-        'roman-numeral': function (buffer, m) { return { type_: 'roman numeral', p1: m || "" }; }
-      }
-    },
-    'tex-math': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': { action_: 'output' } },
-        '\\ce{(...)}': {
-          '*': { action_: [ 'output', 'ce' ] } },
-        '{...}|\\,|\\x{}{}|\\x{}|\\x': {
-          '*': { action_: 'o=' } },
-        'else': {
-          '*': { action_: 'o=' } }
-      }),
-      actions: {
-        'output': function (buffer) {
-          if (buffer.o) {
-            /** @type {ParserOutput} */
-            var ret = { type_: 'tex-math', p1: buffer.o };
-            for (var p in buffer) { delete buffer[p]; }
-            return ret;
-          }
-        }
-      }
-    },
-    'tex-math tight': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': { action_: 'output' } },
-        '\\ce{(...)}': {
-          '*': { action_: [ 'output', 'ce' ] } },
-        '{...}|\\,|\\x{}{}|\\x{}|\\x': {
-          '*': { action_: 'o=' } },
-        '-|+': {
-          '*': { action_: 'tight operator' } },
-        'else': {
-          '*': { action_: 'o=' } }
-      }),
-      actions: {
-        'tight operator': function (buffer, m) { buffer.o = (buffer.o || "") + "{"+m+"}"; },
-        'output': function (buffer) {
-          if (buffer.o) {
-            /** @type {ParserOutput} */
-            var ret = { type_: 'tex-math', p1: buffer.o };
-            for (var p in buffer) { delete buffer[p]; }
-            return ret;
-          }
-        }
-      }
-    },
-    '9,9': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': {} },
-        ',': {
-          '*': { action_: 'comma' } },
-        'else': {
-          '*': { action_: 'copy' } }
-      }),
-      actions: {
-        'comma': function () { return { type_: 'commaDecimal' }; }
-      }
-    },
-    //#endregion
-    //
-    // \pu state machines
-    //
-    //#region pu
-    'pu': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': { action_: 'output' } },
-        'space$': {
-          '*': { action_: [ 'output', 'space' ] } },
-        '{[(|)]}': {
-          '0|a': { action_: 'copy' } },
-        '(-)(9)^(-9)': {
-          '0': { action_: 'number^', nextState: 'a' } },
-        '(-)(9.,9)(e)(99)': {
-          '0': { action_: 'enumber', nextState: 'a' } },
-        'space': {
-          '0|a': {} },
-        'pm-operator': {
-          '0|a': { action_: { type_: 'operator', option: '\\pm' }, nextState: '0' } },
-        'operator': {
-          '0|a': { action_: 'copy', nextState: '0' } },
-        '//': {
-          'd': { action_: 'o=', nextState: '/' } },
-        '/': {
-          'd': { action_: 'o=', nextState: '/' } },
-        '{...}|else': {
-          '0|d': { action_: 'd=', nextState: 'd' },
-          'a': { action_: [ 'space', 'd=' ], nextState: 'd' },
-          '/|q': { action_: 'q=', nextState: 'q' } }
-      }),
-      actions: {
-        'enumber': function (buffer, m) {
-          /** @type {ParserOutput[]} */
-          var ret = [];
-          if (m[0] === "+-"  ||  m[0] === "+/-") {
-            ret.push("\\pm ");
-          } else if (m[0]) {
-            ret.push(m[0]);
-          }
-          if (m[1]) {
-            mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9'));
-            if (m[2]) {
-              if (m[2].match(/[,.]/)) {
-                mhchemParser.concatArray(ret, mhchemParser.go(m[2], 'pu-9,9'));
-              } else {
-                ret.push(m[2]);
-              }
-            }
-            m[3] = m[4] || m[3];
-            if (m[3]) {
-              m[3] = m[3].trim();
-              if (m[3] === "e"  ||  m[3].substr(0, 1) === "*") {
-                ret.push({ type_: 'cdot' });
-              } else {
-                ret.push({ type_: 'times' });
-              }
-            }
-          }
-          if (m[3]) {
-            ret.push("10^{"+m[5]+"}");
-          }
-          return ret;
-        },
-        'number^': function (buffer, m) {
-          /** @type {ParserOutput[]} */
-          var ret = [];
-          if (m[0] === "+-"  ||  m[0] === "+/-") {
-            ret.push("\\pm ");
-          } else if (m[0]) {
-            ret.push(m[0]);
-          }
-          mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9'));
-          ret.push("^{"+m[2]+"}");
-          return ret;
-        },
-        'operator': function (buffer, m, p1) { return { type_: 'operator', kind_: (p1 || m) }; },
-        'space': function () { return { type_: 'pu-space-1' }; },
-        'output': function (buffer) {
-          /** @type {ParserOutput | ParserOutput[]} */
-          var ret;
-          var md = mhchemParser.patterns.match_('{(...)}', buffer.d || "");
-          if (md  &&  md.remainder === '') { buffer.d = md.match_; }
-          var mq = mhchemParser.patterns.match_('{(...)}', buffer.q || "");
-          if (mq  &&  mq.remainder === '') { buffer.q = mq.match_; }
-          if (buffer.d) {
-            buffer.d = buffer.d.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C");
-            buffer.d = buffer.d.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F");
-          }
-          if (buffer.q) {  // fraction
-            buffer.q = buffer.q.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C");
-            buffer.q = buffer.q.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F");
-            var b5 = {
-              d: mhchemParser.go(buffer.d, 'pu'),
-              q: mhchemParser.go(buffer.q, 'pu')
-            };
-            if (buffer.o === '//') {
-              ret = { type_: 'pu-frac', p1: b5.d, p2: b5.q };
-            } else {
-              ret = b5.d;
-              if (b5.d.length > 1  ||  b5.q.length > 1) {
-                ret.push({ type_: ' / ' });
-              } else {
-                ret.push({ type_: '/' });
-              }
-              mhchemParser.concatArray(ret, b5.q);
-            }
-          } else {  // no fraction
-            ret = mhchemParser.go(buffer.d, 'pu-2');
-          }
-          for (var p in buffer) { delete buffer[p]; }
-          return ret;
-        }
-      }
-    },
-    'pu-2': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '*': { action_: 'output' } },
-        '*': {
-          '*': { action_: [ 'output', 'cdot' ], nextState: '0' } },
-        '\\x': {
-          '*': { action_: 'rm=' } },
-        'space': {
-          '*': { action_: [ 'output', 'space' ], nextState: '0' } },
-        '^{(...)}|^(-1)': {
-          '1': { action_: '^(-1)' } },
-        '-9.,9': {
-          '0': { action_: 'rm=', nextState: '0' },
-          '1': { action_: '^(-1)', nextState: '0' } },
-        '{...}|else': {
-          '*': { action_: 'rm=', nextState: '1' } }
-      }),
-      actions: {
-        'cdot': function () { return { type_: 'tight cdot' }; },
-        '^(-1)': function (buffer, m) { buffer.rm += "^{"+m+"}"; },
-        'space': function () { return { type_: 'pu-space-2' }; },
-        'output': function (buffer) {
-          /** @type {ParserOutput | ParserOutput[]} */
-          var ret = [];
-          if (buffer.rm) {
-            var mrm = mhchemParser.patterns.match_('{(...)}', buffer.rm || "");
-            if (mrm  &&  mrm.remainder === '') {
-              ret = mhchemParser.go(mrm.match_, 'pu');
-            } else {
-              ret = { type_: 'rm', p1: buffer.rm };
-            }
-          }
-          for (var p in buffer) { delete buffer[p]; }
-          return ret;
-        }
-      }
-    },
-    'pu-9,9': {
-      transitions: mhchemParser.createTransitions({
-        'empty': {
-          '0': { action_: 'output-0' },
-          'o': { action_: 'output-o' } },
-        ',': {
-          '0': { action_: [ 'output-0', 'comma' ], nextState: 'o' } },
-        '.': {
-          '0': { action_: [ 'output-0', 'copy' ], nextState: 'o' } },
-        'else': {
-          '*': { action_: 'text=' } }
-      }),
-      actions: {
-        'comma': function () { return { type_: 'commaDecimal' }; },
-        'output-0': function (buffer) {
-          /** @type {ParserOutput[]} */
-          var ret = [];
-          buffer.text_ = buffer.text_ || "";
-          if (buffer.text_.length > 4) {
-            var a = buffer.text_.length % 3;
-            if (a === 0) { a = 3; }
-            for (var i=buffer.text_.length-3; i>0; i-=3) {
-              ret.push(buffer.text_.substr(i, 3));
-              ret.push({ type_: '1000 separator' });
-            }
-            ret.push(buffer.text_.substr(0, a));
-            ret.reverse();
-          } else {
-            ret.push(buffer.text_);
-          }
-          for (var p in buffer) { delete buffer[p]; }
-          return ret;
-        },
-        'output-o': function (buffer) {
-          /** @type {ParserOutput[]} */
-          var ret = [];
-          buffer.text_ = buffer.text_ || "";
-          if (buffer.text_.length > 4) {
-            var a = buffer.text_.length - 3;
-            for (var i=0; i<a; i+=3) {
-              ret.push(buffer.text_.substr(i, 3));
-              ret.push({ type_: '1000 separator' });
-            }
-            ret.push(buffer.text_.substr(i));
-          } else {
-            ret.push(buffer.text_);
-          }
-          for (var p in buffer) { delete buffer[p]; }
-          return ret;
-        }
-      }
-    }
-    //#endregion
-  };
-
-  //
-  // texify: Take MhchemParser output and convert it to TeX
-  //
-  /** @type {Texify} */
-  var texify = {
-    go: function (input, isInner) {  // (recursive, max 4 levels)
-      if (!input) { return ""; }
-      var res = "";
-      var cee = false;
-      for (var i=0; i < input.length; i++) {
-        var inputi = input[i];
-        if (typeof inputi === "string") {
-          res += inputi;
-        } else {
-          res += texify._go2(inputi);
-          if (inputi.type_ === '1st-level escape') { cee = true; }
-        }
-      }
-      if (!isInner && !cee && res) {
-        res = "{" + res + "}";
-      }
-      return res;
-    },
-    _goInner: function (input) {
-      if (!input) { return input; }
-      return texify.go(input, true);
-    },
-    _go2: function (buf) {
-      /** @type {undefined | string} */
-      var res;
-      switch (buf.type_) {
-        case 'chemfive':
-          res = "";
-          var b5 = {
-            a: texify._goInner(buf.a),
-            b: texify._goInner(buf.b),
-            p: texify._goInner(buf.p),
-            o: texify._goInner(buf.o),
-            q: texify._goInner(buf.q),
-            d: texify._goInner(buf.d)
-          };
-          //
-          // a
-          //
-          if (b5.a) {
-            if (b5.a.match(/^[+\-]/)) { b5.a = "{"+b5.a+"}"; }
-            res += b5.a + "\\,";
-          }
-          //
-          // b and p
-          //
-          if (b5.b || b5.p) {
-            res += "{\\vphantom{X}}";
-            res += "^{\\hphantom{"+(b5.b||"")+"}}_{\\hphantom{"+(b5.p||"")+"}}";
-            res += "{\\vphantom{X}}";
-            res += "^{\\smash[t]{\\vphantom{2}}\\mathllap{"+(b5.b||"")+"}}";
-            res += "_{\\vphantom{2}\\mathllap{\\smash[t]{"+(b5.p||"")+"}}}";
-          }
-          //
-          // o
-          //
-          if (b5.o) {
-            if (b5.o.match(/^[+\-]/)) { b5.o = "{"+b5.o+"}"; }
-            res += b5.o;
-          }
-          //
-          // q and d
-          //
-          if (buf.dType === 'kv') {
-            if (b5.d || b5.q) {
-              res += "{\\vphantom{X}}";
-            }
-            if (b5.d) {
-              res += "^{"+b5.d+"}";
-            }
-            if (b5.q) {
-              res += "_{\\smash[t]{"+b5.q+"}}";
-            }
-          } else if (buf.dType === 'oxidation') {
-            if (b5.d) {
-              res += "{\\vphantom{X}}";
-              res += "^{"+b5.d+"}";
-            }
-            if (b5.q) {
-              res += "{\\vphantom{X}}";
-              res += "_{\\smash[t]{"+b5.q+"}}";
-            }
-          } else {
-            if (b5.q) {
-              res += "{\\vphantom{X}}";
-              res += "_{\\smash[t]{"+b5.q+"}}";
-            }
-            if (b5.d) {
-              res += "{\\vphantom{X}}";
-              res += "^{"+b5.d+"}";
-            }
-          }
-          break;
-        case 'rm':
-          res = "\\mathrm{"+buf.p1+"}";
-          break;
-        case 'text':
-          if (buf.p1.match(/[\^_]/)) {
-            buf.p1 = buf.p1.replace(" ", "~").replace("-", "\\text{-}");
-            res = "\\mathrm{"+buf.p1+"}";
-          } else {
-            res = "\\text{"+buf.p1+"}";
-          }
-          break;
-        case 'roman numeral':
-          res = "\\mathrm{"+buf.p1+"}";
-          break;
-        case 'state of aggregation':
-          res = "\\mskip2mu "+texify._goInner(buf.p1);
-          break;
-        case 'state of aggregation subscript':
-          res = "\\mskip1mu "+texify._goInner(buf.p1);
-          break;
-        case 'bond':
-          res = texify._getBond(buf.kind_);
-          if (!res) {
-            throw ["MhchemErrorBond", "mhchem Error. Unknown bond type (" + buf.kind_ + ")"];
-          }
-          break;
-        case 'frac':
-          var c = "\\frac{" + buf.p1 + "}{" + buf.p2 + "}";
-          res = "\\mathchoice{\\textstyle"+c+"}{"+c+"}{"+c+"}{"+c+"}";
-          break;
-        case 'pu-frac':
-          var d = "\\frac{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}";
-          res = "\\mathchoice{\\textstyle"+d+"}{"+d+"}{"+d+"}{"+d+"}";
-          break;
-        case 'tex-math':
-          res = buf.p1 + " ";
-          break;
-        case 'frac-ce':
-          res = "\\frac{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}";
-          break;
-        case 'overset':
-          res = "\\overset{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}";
-          break;
-        case 'underset':
-          res = "\\underset{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}";
-          break;
-        case 'underbrace':
-          res =  "\\underbrace{" + texify._goInner(buf.p1) + "}_{" + texify._goInner(buf.p2) + "}";
-          break;
-        case 'color':
-          res = "{\\color{" + buf.color1 + "}{" + texify._goInner(buf.color2) + "}}";
-          break;
-        case 'color0':
-          res = "\\color{" + buf.color + "}";
-          break;
-        case 'arrow':
-          var b6 = {
-            rd: texify._goInner(buf.rd),
-            rq: texify._goInner(buf.rq)
-          };
-          var arrow = "\\x" + texify._getArrow(buf.r);
-          if (b6.rq) { arrow += "[{" + b6.rq + "}]"; }
-          if (b6.rd) {
-            arrow += "{" + b6.rd + "}";
-          } else {
-            arrow += "{}";
-          }
-          res = arrow;
-          break;
-        case 'operator':
-          res = texify._getOperator(buf.kind_);
-          break;
-        case '1st-level escape':
-          res = buf.p1+" ";  // &, \\\\, \\hlin
-          break;
-        case 'space':
-          res = " ";
-          break;
-        case 'entitySkip':
-          res = "~";
-          break;
-        case 'pu-space-1':
-          res = "~";
-          break;
-        case 'pu-space-2':
-          res = "\\mkern3mu ";
-          break;
-        case '1000 separator':
-          res = "\\mkern2mu ";
-          break;
-        case 'commaDecimal':
-          res = "{,}";
-          break;
-          case 'comma enumeration L':
-          res = "{"+buf.p1+"}\\mkern6mu ";
-          break;
-        case 'comma enumeration M':
-          res = "{"+buf.p1+"}\\mkern3mu ";
-          break;
-        case 'comma enumeration S':
-          res = "{"+buf.p1+"}\\mkern1mu ";
-          break;
-        case 'hyphen':
-          res = "\\text{-}";
-          break;
-        case 'addition compound':
-          res = "\\,{\\cdot}\\,";
-          break;
-        case 'electron dot':
-          res = "\\mkern1mu \\bullet\\mkern1mu ";
-          break;
-        case 'KV x':
-          res = "{\\times}";
-          break;
-        case 'prime':
-          res = "\\prime ";
-          break;
-        case 'cdot':
-          res = "\\cdot ";
-          break;
-        case 'tight cdot':
-          res = "\\mkern1mu{\\cdot}\\mkern1mu ";
-          break;
-        case 'times':
-          res = "\\times ";
-          break;
-        case 'circa':
-          res = "{\\sim}";
-          break;
-        case '^':
-          res = "uparrow";
-          break;
-        case 'v':
-          res = "downarrow";
-          break;
-        case 'ellipsis':
-          res = "\\ldots ";
-          break;
-        case '/':
-          res = "/";
-          break;
-        case ' / ':
-          res = "\\,/\\,";
-          break;
-        default:
-          assertNever(buf);
-          throw ["MhchemBugT", "mhchem bug T. Please report."];  // Missing texify rule or unknown MhchemParser output
-      }
-      assertString(res);
-      return res;
-    },
-    _getArrow: function (a) {
-      switch (a) {
-        case "->": return "rightarrow";
-        case "\u2192": return "rightarrow";
-        case "\u27F6": return "rightarrow";
-        case "<-": return "leftarrow";
-        case "<->": return "leftrightarrow";
-        case "<-->": return "rightleftarrows";
-        case "<=>": return "rightleftharpoons";
-        case "\u21CC": return "rightleftharpoons";
-        case "<=>>": return "rightequilibrium";
-        case "<<=>": return "leftequilibrium";
-        default:
-          assertNever(a);
-          throw ["MhchemBugT", "mhchem bug T. Please report."];
-      }
-    },
-    _getBond: function (a) {
-      switch (a) {
-        case "-": return "{-}";
-        case "1": return "{-}";
-        case "=": return "{=}";
-        case "2": return "{=}";
-        case "#": return "{\\equiv}";
-        case "3": return "{\\equiv}";
-        case "~": return "{\\tripledash}";
-        case "~-": return "{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}";
-        case "~=": return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";
-        case "~--": return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";
-        case "-~-": return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}";
-        case "...": return "{{\\cdot}{\\cdot}{\\cdot}}";
-        case "....": return "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";
-        case "->": return "{\\rightarrow}";
-        case "<-": return "{\\leftarrow}";
-        case "<": return "{<}";
-        case ">": return "{>}";
-        default:
-          assertNever(a);
-          throw ["MhchemBugT", "mhchem bug T. Please report."];
-      }
-    },
-    _getOperator: function (a) {
-      switch (a) {
-        case "+": return " {}+{} ";
-        case "-": return " {}-{} ";
-        case "=": return " {}={} ";
-        case "<": return " {}<{} ";
-        case ">": return " {}>{} ";
-        case "<<": return " {}\\ll{} ";
-        case ">>": return " {}\\gg{} ";
-        case "\\pm": return " {}\\pm{} ";
-        case "\\approx": return " {}\\approx{} ";
-        case "$\\approx$": return " {}\\approx{} ";
-        case "v": return " \\downarrow{} ";
-        case "(v)": return " \\downarrow{} ";
-        case "^": return " \\uparrow{} ";
-        case "(^)": return " \\uparrow{} ";
-        default:
-          assertNever(a);
-          throw ["MhchemBugT", "mhchem bug T. Please report."];
-      }
-    }
-  };
-
-  //
-  // Helpers for code anaylsis
-  // Will show type error at calling position
-  //
-  /** @param {number} a */
-  function assertNever(a) {}
-  /** @param {string} a */
-  function assertString(a) {}

+ 0 - 193
server/modules/rendering/markdown-katex/renderer.js

@@ -1,193 +0,0 @@
-const katex = require('katex')
-const chemParse = require('./mhchem')
-
-// ------------------------------------
-// Markdown - KaTeX Renderer
-// ------------------------------------
-//
-// Includes code from https://github.com/liradb2000/markdown-it-katex
-
-// Add \ce, \pu, and \tripledash to the KaTeX macros.
-katex.__defineMacro('\\ce', function(context) {
-  return chemParse(context.consumeArgs(1)[0], 'ce')
-})
-katex.__defineMacro('\\pu', function(context) {
-  return chemParse(context.consumeArgs(1)[0], 'pu')
-})
-
-//  Needed for \bond for the ~ forms
-//  Raise by 2.56mu, not 2mu. We're raising a hyphen-minus, U+002D, not
-//  a mathematical minus, U+2212. So we need that extra 0.56.
-katex.__defineMacro('\\tripledash', '{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu' + '\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}')
-
-module.exports = {
-  init (mdinst, conf) {
-    if (conf.useInline) {
-      mdinst.inline.ruler.after('escape', 'katex_inline', katexInline)
-      mdinst.renderer.rules.katex_inline = (tokens, idx) => {
-        try {
-          return katex.renderToString(tokens[idx].content, {
-            displayMode: false
-          })
-        } catch (err) {
-          WIKI.logger.warn(err)
-          return tokens[idx].content
-        }
-      }
-    }
-    if (conf.useBlocks) {
-      mdinst.block.ruler.after('blockquote', 'katex_block', katexBlock, {
-        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
-      })
-      mdinst.renderer.rules.katex_block = (tokens, idx) => {
-        try {
-          return `<p>` + katex.renderToString(tokens[idx].content, {
-            displayMode: true
-          }) + `</p>`
-        } catch (err) {
-          WIKI.logger.warn(err)
-          return tokens[idx].content
-        }
-      }
-    }
-  }
-}
-
-// Test if potential opening or closing delimieter
-// Assumes that there is a "$" at state.src[pos]
-function isValidDelim (state, pos) {
-  let prevChar
-  let nextChar
-  let max = state.posMax
-  let canOpen = true
-  let canClose = true
-
-  prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
-  nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
-
-  // Check non-whitespace conditions for opening and closing, and
-  // check that closing delimeter isn't followed by a number
-  if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
-          (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
-    canClose = false
-  }
-  if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
-    canOpen = false
-  }
-
-  return {
-    canOpen: canOpen,
-    canClose: canClose
-  }
-}
-
-function katexInline (state, silent) {
-  let start, match, token, res, pos
-
-  if (state.src[state.pos] !== '$') { return false }
-
-  res = isValidDelim(state, state.pos)
-  if (!res.canOpen) {
-    if (!silent) { state.pending += '$' }
-    state.pos += 1
-    return true
-  }
-
-  // First check for and bypass all properly escaped delimieters
-  // This loop will assume that the first leading backtick can not
-  // be the first character in state.src, which is known since
-  // we have found an opening delimieter already.
-  start = state.pos + 1
-  match = start
-  while ((match = state.src.indexOf('$', match)) !== -1) {
-    // Found potential $, look for escapes, pos will point to
-    // first non escape when complete
-    pos = match - 1
-    while (state.src[pos] === '\\') { pos -= 1 }
-
-    // Even number of escapes, potential closing delimiter found
-    if (((match - pos) % 2) === 1) { break }
-    match += 1
-  }
-
-  // No closing delimter found.  Consume $ and continue.
-  if (match === -1) {
-    if (!silent) { state.pending += '$' }
-    state.pos = start
-    return true
-  }
-
-  // Check if we have empty content, ie: $$.  Do not parse.
-  if (match - start === 0) {
-    if (!silent) { state.pending += '$$' }
-    state.pos = start + 1
-    return true
-  }
-
-  // Check for valid closing delimiter
-  res = isValidDelim(state, match)
-  if (!res.canClose) {
-    if (!silent) { state.pending += '$' }
-    state.pos = start
-    return true
-  }
-
-  if (!silent) {
-    token = state.push('katex_inline', 'math', 0)
-    token.markup = '$'
-    token.content = state.src.slice(start, match)
-  }
-
-  state.pos = match + 1
-  return true
-}
-
-function katexBlock (state, start, end, silent) {
-  let firstLine; let lastLine; let next; let lastPos; let found = false; let token
-  let pos = state.bMarks[start] + state.tShift[start]
-  let max = state.eMarks[start]
-
-  if (pos + 2 > max) { return false }
-  if (state.src.slice(pos, pos + 2) !== '$$') { return false }
-
-  pos += 2
-  firstLine = state.src.slice(pos, max)
-
-  if (silent) { return true }
-  if (firstLine.trim().slice(-2) === '$$') {
-    // Single line expression
-    firstLine = firstLine.trim().slice(0, -2)
-    found = true
-  }
-
-  for (next = start; !found;) {
-    next++
-
-    if (next >= end) { break }
-
-    pos = state.bMarks[next] + state.tShift[next]
-    max = state.eMarks[next]
-
-    if (pos < max && state.tShift[next] < state.blkIndent) {
-      // non-empty line with negative indent should stop the list:
-      break
-    }
-
-    if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
-      lastPos = state.src.slice(0, max).lastIndexOf('$$')
-      lastLine = state.src.slice(pos, lastPos)
-      found = true
-    }
-  }
-
-  state.line = next + 1
-
-  token = state.push('katex_block', 'math', 0)
-  token.block = true
-  token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
-  state.getLines(start + 1, next, state.tShift[start], true) +
-  (lastLine && lastLine.trim() ? lastLine : '')
-  token.map = [ start, state.line ]
-  token.markup = '$$'
-  return true
-}

+ 0 - 29
server/modules/rendering/markdown-kroki/definition.yml

@@ -1,29 +0,0 @@
-key: markdownKroki
-title: Kroki
-description: Kroki Diagrams Parser
-author: rlanyi (based on PlantUML renderer)
-icon: mdi-sitemap
-enabledDefault: false
-dependsOn: markdown-core
-props:
-  server:
-    type: String
-    default: https://kroki.io
-    title: Kroki Server
-    hint: Kroki server used for image generation
-    order: 1
-    public: true
-  openMarker:
-    type: String
-    default: "```kroki"
-    title: Open Marker
-    hint: String to use as opening delimiter. Diagram type must be put in the next line in lowercase.
-    order: 2
-    public: true
-  closeMarker:
-    type: String
-    default: "```"
-    title: Close Marker
-    hint: String to use as closing delimiter
-    order: 3
-    public: true

+ 0 - 143
server/modules/rendering/markdown-kroki/renderer.js

@@ -1,143 +0,0 @@
-const zlib = require('zlib')
-
-// ------------------------------------
-// Markdown - Kroki Preprocessor
-// ------------------------------------
-
-module.exports = {
-  init (mdinst, conf) {
-    mdinst.use((md, opts) => {
-      const openMarker = opts.openMarker || '```kroki'
-      const openChar = openMarker.charCodeAt(0)
-      const closeMarker = opts.closeMarker || '```'
-      const closeChar = closeMarker.charCodeAt(0)
-      const server = opts.server || 'https://kroki.io'
-
-      md.block.ruler.before('fence', 'kroki', (state, startLine, endLine, silent) => {
-        let nextLine
-        let markup
-        let params
-        let token
-        let i
-        let autoClosed = false
-        let start = state.bMarks[startLine] + state.tShift[startLine]
-        let max = state.eMarks[startLine]
-
-        // Check out the first character quickly,
-        // this should filter out most of non-uml blocks
-        //
-        if (openChar !== state.src.charCodeAt(start)) { return false }
-
-        // Check out the rest of the marker string
-        //
-        for (i = 0; i < openMarker.length; ++i) {
-          if (openMarker[i] !== state.src[start + i]) { return false }
-        }
-
-        markup = state.src.slice(start, start + i)
-        params = state.src.slice(start + i, max)
-
-        // Since start is found, we can report success here in validation mode
-        //
-        if (silent) { return true }
-
-        // Search for the end of the block
-        //
-        nextLine = startLine
-
-        for (;;) {
-          nextLine++
-          if (nextLine >= endLine) {
-            // unclosed block should be autoclosed by end of document.
-            // also block seems to be autoclosed by end of parent
-            break
-          }
-
-          start = state.bMarks[nextLine] + state.tShift[nextLine]
-          max = state.eMarks[nextLine]
-
-          if (start < max && state.sCount[nextLine] < state.blkIndent) {
-            // non-empty line with negative indent should stop the list:
-            // - ```
-            //  test
-            break
-          }
-
-          if (closeChar !== state.src.charCodeAt(start)) {
-            // didn't find the closing fence
-            continue
-          }
-
-          if (state.sCount[nextLine] > state.sCount[startLine]) {
-            // closing fence should not be indented with respect of opening fence
-            continue
-          }
-
-          let closeMarkerMatched = true
-          for (i = 0; i < closeMarker.length; ++i) {
-            if (closeMarker[i] !== state.src[start + i]) {
-              closeMarkerMatched = false
-              break
-            }
-          }
-
-          if (!closeMarkerMatched) {
-            continue
-          }
-
-          // make sure tail has spaces only
-          if (state.skipSpaces(start + i) < max) {
-            continue
-          }
-
-          // found!
-          autoClosed = true
-          break
-        }
-
-        let contents = state.src
-          .split('\n')
-          .slice(startLine + 1, nextLine)
-          .join('\n')
-
-        // We generate a token list for the alt property, to mimic what the image parser does.
-        let altToken = []
-        // Remove leading space if any.
-        let alt = params ? params.slice(1) : 'uml diagram'
-        state.md.inline.parse(
-          alt,
-          state.md,
-          state.env,
-          altToken
-        )
-
-        let firstlf = contents.indexOf('\n')
-        if (firstlf === -1) firstlf = undefined
-        let diagramType = contents.substring(0, firstlf)
-        contents = contents.substring(firstlf + 1)
-
-        let result = zlib.deflateSync(contents).toString('base64').replace(/\+/g, '-').replace(/\//g, '_')
-
-        token = state.push('kroki', 'img', 0)
-        // alt is constructed from children. No point in populating it here.
-        token.attrs = [ [ 'src', `${server}/${diagramType}/svg/${result}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ]
-        token.block = true
-        token.children = altToken
-        token.info = params
-        token.map = [ startLine, nextLine ]
-        token.markup = markup
-
-        state.line = nextLine + (autoClosed ? 1 : 0)
-
-        return true
-      }, {
-        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
-      })
-      md.renderer.rules.kroki = md.renderer.rules.image
-    }, {
-      openMarker: conf.openMarker,
-      closeMarker: conf.closeMarker,
-      server: conf.server
-    })
-  }
-}

+ 0 - 20
server/modules/rendering/markdown-mathjax/definition.yml

@@ -1,20 +0,0 @@
-key: markdownMathjax
-title: Mathjax
-description: LaTeX Math + Chemical Expression Typesetting Renderer
-author: requarks.io
-icon: mdi-math-integral
-enabledDefault: false
-dependsOn: markdown-core
-props:
-  useInline:
-    type: Boolean
-    default: true
-    title: Inline TeX
-    hint: Process inline TeX expressions surrounded by $ symbols.
-    order: 1
-  useBlocks:
-    type: Boolean
-    default: true
-    title: TeX Blocks
-    hint: Process TeX blocks enclosed by $$ symbols.
-    order: 2

+ 0 - 205
server/modules/rendering/markdown-mathjax/renderer.js

@@ -1,205 +0,0 @@
-const mjax = require('mathjax')
-
-// ------------------------------------
-// Markdown - MathJax Renderer
-// ------------------------------------
-
-const extensions = [
-  'bbox',
-  'boldsymbol',
-  'braket',
-  'color',
-  'extpfeil',
-  'mhchem',
-  'newcommand',
-  'unicode',
-  'verb'
-]
-
-module.exports = {
-  async init (mdinst, conf) {
-    const MathJax = await mjax.init({
-      loader: {
-        require: require,
-        paths: { mathjax: 'mathjax/es5' },
-        load: [
-          'input/tex',
-          'output/svg',
-          ...extensions.map(e => `[tex]/${e}`)
-        ]
-      },
-      tex: {
-        packages: {'[+]': extensions}
-      }
-    })
-    if (conf.useInline) {
-      mdinst.inline.ruler.after('escape', 'mathjax_inline', mathjaxInline)
-      mdinst.renderer.rules.mathjax_inline = (tokens, idx) => {
-        try {
-          const result = MathJax.tex2svg(tokens[idx].content, {
-            display: false
-          })
-          return MathJax.startup.adaptor.innerHTML(result)
-        } catch (err) {
-          WIKI.logger.warn(err)
-          return tokens[idx].content
-        }
-      }
-    }
-    if (conf.useBlocks) {
-      mdinst.block.ruler.after('blockquote', 'mathjax_block', mathjaxBlock, {
-        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
-      })
-      mdinst.renderer.rules.mathjax_block = (tokens, idx) => {
-        try {
-          const result = MathJax.tex2svg(tokens[idx].content, {
-            display: true
-          })
-          return `<p>` + MathJax.startup.adaptor.innerHTML(result) + `</p>`
-        } catch (err) {
-          WIKI.logger.warn(err)
-          return tokens[idx].content
-        }
-      }
-    }
-  }
-}
-
-// Test if potential opening or closing delimieter
-// Assumes that there is a "$" at state.src[pos]
-function isValidDelim (state, pos) {
-  let prevChar
-  let nextChar
-  let max = state.posMax
-  let canOpen = true
-  let canClose = true
-
-  prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
-  nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
-
-  // Check non-whitespace conditions for opening and closing, and
-  // check that closing delimeter isn't followed by a number
-  if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
-  (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
-    canClose = false
-  }
-  if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
-    canOpen = false
-  }
-
-  return {
-    canOpen: canOpen,
-    canClose: canClose
-  }
-}
-
-function mathjaxInline (state, silent) {
-  let start, match, token, res, pos
-
-  if (state.src[state.pos] !== '$') { return false }
-
-  res = isValidDelim(state, state.pos)
-  if (!res.canOpen) {
-    if (!silent) { state.pending += '$' }
-    state.pos += 1
-    return true
-  }
-
-  // First check for and bypass all properly escaped delimieters
-  // This loop will assume that the first leading backtick can not
-  // be the first character in state.src, which is known since
-  // we have found an opening delimieter already.
-  start = state.pos + 1
-  match = start
-  while ((match = state.src.indexOf('$', match)) !== -1) {
-    // Found potential $, look for escapes, pos will point to
-    // first non escape when complete
-    pos = match - 1
-    while (state.src[pos] === '\\') { pos -= 1 }
-
-    // Even number of escapes, potential closing delimiter found
-    if (((match - pos) % 2) === 1) { break }
-    match += 1
-  }
-
-  // No closing delimter found.  Consume $ and continue.
-  if (match === -1) {
-    if (!silent) { state.pending += '$' }
-    state.pos = start
-    return true
-  }
-
-  // Check if we have empty content, ie: $$.  Do not parse.
-  if (match - start === 0) {
-    if (!silent) { state.pending += '$$' }
-    state.pos = start + 1
-    return true
-  }
-
-  // Check for valid closing delimiter
-  res = isValidDelim(state, match)
-  if (!res.canClose) {
-    if (!silent) { state.pending += '$' }
-    state.pos = start
-    return true
-  }
-
-  if (!silent) {
-    token = state.push('mathjax_inline', 'math', 0)
-    token.markup = '$'
-    token.content = state.src.slice(start, match)
-  }
-
-  state.pos = match + 1
-  return true
-}
-
-function mathjaxBlock (state, start, end, silent) {
-  let firstLine; let lastLine; let next; let lastPos; let found = false; let token
-  let pos = state.bMarks[start] + state.tShift[start]
-  let max = state.eMarks[start]
-
-  if (pos + 2 > max) { return false }
-  if (state.src.slice(pos, pos + 2) !== '$$') { return false }
-
-  pos += 2
-  firstLine = state.src.slice(pos, max)
-
-  if (silent) { return true }
-  if (firstLine.trim().slice(-2) === '$$') {
-    // Single line expression
-    firstLine = firstLine.trim().slice(0, -2)
-    found = true
-  }
-
-  for (next = start; !found;) {
-    next++
-
-    if (next >= end) { break }
-
-    pos = state.bMarks[next] + state.tShift[next]
-    max = state.eMarks[next]
-
-    if (pos < max && state.tShift[next] < state.blkIndent) {
-      // non-empty line with negative indent should stop the list:
-      break
-    }
-
-    if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
-      lastPos = state.src.slice(0, max).lastIndexOf('$$')
-      lastLine = state.src.slice(pos, lastPos)
-      found = true
-    }
-  }
-
-  state.line = next + 1
-
-  token = state.push('mathjax_block', 'math', 0)
-  token.block = true
-  token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
-  state.getLines(start + 1, next, state.tShift[start], true) +
-  (lastLine && lastLine.trim() ? lastLine : '')
-  token.map = [ start, state.line ]
-  token.markup = '$$'
-  return true
-}

+ 0 - 23
server/modules/rendering/markdown-multi-table/definition.yml

@@ -1,23 +0,0 @@
-key: markdownMultiTable
-title: MultiMarkdown Table
-description: Add MultiMarkdown table support
-author: requarks.io
-icon: mdi-table
-enabledDefault: false
-dependsOn: markdown-core
-props:
-  multilineEnabled:
-    type: Boolean
-    title: Multiline
-    hint: Enable multiple lines rows
-    default: true
-  headerlessEnabled:
-    type: Boolean
-    title: Headerless
-    hint: Enable ommited table headers
-    default: true
-  rowspanEnabled:
-    type: Boolean
-    title: Rowspan
-    hint: Enable table row spans
-    default: true

+ 0 - 11
server/modules/rendering/markdown-multi-table/renderer.js

@@ -1,11 +0,0 @@
-const multiTable = require('markdown-it-multimd-table')
-
-module.exports = {
-  init (md, conf) {
-    md.use(multiTable, {
-      multiline: conf.multilineEnabled,
-      rowspan: conf.rowspanEnabled,
-      headerless: conf.headerlessEnabled
-    })
-  }
-}

+ 0 - 41
server/modules/rendering/markdown-plantuml/definition.yml

@@ -1,41 +0,0 @@
-key: markdownPlantuml
-title: PlantUML
-description: PlantUML Markdown Parser
-author: ethanmdavidson
-icon: mdi-sitemap
-enabledDefault: true
-dependsOn: markdown-core
-props:
-  server:
-    type: String
-    default: https://plantuml.requarks.io
-    title: PlantUML Server
-    hint: PlantUML server used for image generation
-    order: 1
-    public: true
-  openMarker:
-    type: String
-    default: "```plantuml"
-    title: Open Marker
-    hint: String to use as opening delimiter
-    order: 2
-    public: true
-  closeMarker:
-    type: String
-    default: "```"
-    title: Close Marker
-    hint: String to use as closing delimiter
-    order: 3
-    public: true
-  imageFormat:
-    type: String
-    default: svg
-    title: Image Format
-    hint: Format to use for rendered PlantUML images
-    enum:
-      - svg
-      - png
-      - latex
-      - ascii
-    order: 4
-    public: true

+ 0 - 190
server/modules/rendering/markdown-plantuml/renderer.js

@@ -1,190 +0,0 @@
-const zlib = require('zlib')
-
-// ------------------------------------
-// Markdown - PlantUML Preprocessor
-// ------------------------------------
-
-module.exports = {
-  init (mdinst, conf) {
-    mdinst.use((md, opts) => {
-      const openMarker = opts.openMarker || '```plantuml'
-      const openChar = openMarker.charCodeAt(0)
-      const closeMarker = opts.closeMarker || '```'
-      const closeChar = closeMarker.charCodeAt(0)
-      const imageFormat = opts.imageFormat || 'svg'
-      const server = opts.server || 'https://plantuml.requarks.io'
-
-      md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {
-        let nextLine
-        let markup
-        let params
-        let token
-        let i
-        let autoClosed = false
-        let start = state.bMarks[startLine] + state.tShift[startLine]
-        let max = state.eMarks[startLine]
-
-        // Check out the first character quickly,
-        // this should filter out most of non-uml blocks
-        //
-        if (openChar !== state.src.charCodeAt(start)) { return false }
-
-        // Check out the rest of the marker string
-        //
-        for (i = 0; i < openMarker.length; ++i) {
-          if (openMarker[i] !== state.src[start + i]) { return false }
-        }
-
-        markup = state.src.slice(start, start + i)
-        params = state.src.slice(start + i, max)
-
-        // Since start is found, we can report success here in validation mode
-        //
-        if (silent) { return true }
-
-        // Search for the end of the block
-        //
-        nextLine = startLine
-
-        for (;;) {
-          nextLine++
-          if (nextLine >= endLine) {
-            // unclosed block should be autoclosed by end of document.
-            // also block seems to be autoclosed by end of parent
-            break
-          }
-
-          start = state.bMarks[nextLine] + state.tShift[nextLine]
-          max = state.eMarks[nextLine]
-
-          if (start < max && state.sCount[nextLine] < state.blkIndent) {
-            // non-empty line with negative indent should stop the list:
-            // - ```
-            //  test
-            break
-          }
-
-          if (closeChar !== state.src.charCodeAt(start)) {
-            // didn't find the closing fence
-            continue
-          }
-
-          if (state.sCount[nextLine] > state.sCount[startLine]) {
-            // closing fence should not be indented with respect of opening fence
-            continue
-          }
-
-          let closeMarkerMatched = true
-          for (i = 0; i < closeMarker.length; ++i) {
-            if (closeMarker[i] !== state.src[start + i]) {
-              closeMarkerMatched = false
-              break
-            }
-          }
-
-          if (!closeMarkerMatched) {
-            continue
-          }
-
-          // make sure tail has spaces only
-          if (state.skipSpaces(start + i) < max) {
-            continue
-          }
-
-          // found!
-          autoClosed = true
-          break
-        }
-
-        const contents = state.src
-          .split('\n')
-          .slice(startLine + 1, nextLine)
-          .join('\n')
-
-        // We generate a token list for the alt property, to mimic what the image parser does.
-        let altToken = []
-        // Remove leading space if any.
-        let alt = params ? params.slice(1) : 'uml diagram'
-        state.md.inline.parse(
-          alt,
-          state.md,
-          state.env,
-          altToken
-        )
-
-        const zippedCode = encode64(zlib.deflateRawSync('@startuml\n' + contents + '\n@enduml').toString('binary'))
-
-        token = state.push('uml_diagram', 'img', 0)
-        // alt is constructed from children. No point in populating it here.
-        token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ]
-        token.block = true
-        token.children = altToken
-        token.info = params
-        token.map = [ startLine, nextLine ]
-        token.markup = markup
-
-        state.line = nextLine + (autoClosed ? 1 : 0)
-
-        return true
-      }, {
-        alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
-      })
-      md.renderer.rules.uml_diagram = md.renderer.rules.image
-    }, {
-      openMarker: conf.openMarker,
-      closeMarker: conf.closeMarker,
-      imageFormat: conf.imageFormat,
-      server: conf.server
-    })
-  }
-}
-
-function encode64 (data) {
-  let r = ''
-  for (let i = 0; i < data.length; i += 3) {
-    if (i + 2 === data.length) {
-      r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)
-    } else if (i + 1 === data.length) {
-      r += append3bytes(data.charCodeAt(i), 0, 0)
-    } else {
-      r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))
-    }
-  }
-  return r
-}
-
-function append3bytes (b1, b2, b3) {
-  let c1 = b1 >> 2
-  let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
-  let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
-  let c4 = b3 & 0x3F
-  let r = ''
-  r += encode6bit(c1 & 0x3F)
-  r += encode6bit(c2 & 0x3F)
-  r += encode6bit(c3 & 0x3F)
-  r += encode6bit(c4 & 0x3F)
-  return r
-}
-
-function encode6bit(raw) {
-  let b = raw
-  if (b < 10) {
-    return String.fromCharCode(48 + b)
-  }
-  b -= 10
-  if (b < 26) {
-    return String.fromCharCode(65 + b)
-  }
-  b -= 26
-  if (b < 26) {
-    return String.fromCharCode(97 + b)
-  }
-  b -= 26
-  if (b === 0) {
-    return '-'
-  }
-  if (b === 1) {
-    return '_'
-  }
-  return '?'
-}

+ 0 - 18
server/modules/rendering/markdown-supsub/definition.yml

@@ -1,18 +0,0 @@
-key: markdownSupsub
-title: Subscript/Superscript
-description: Parse subscript and superscript tags
-author: requarks.io
-icon: mdi-format-superscript
-enabledDefault: true
-dependsOn: markdown-core
-props:
-  subEnabled:
-    type: Boolean
-    title: Subscript
-    hint: Enable subscript tags
-    default: true
-  supEnabled:
-    type: Boolean
-    title: Superscript
-    hint: Enable superscript tags
-    default: true

+ 0 - 17
server/modules/rendering/markdown-supsub/renderer.js

@@ -1,17 +0,0 @@
-const mdSub = require('markdown-it-sub')
-const mdSup = require('markdown-it-sup')
-
-// ------------------------------------
-// Markdown - Subscript / Superscript
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    if (conf.subEnabled) {
-      md.use(mdSub)
-    }
-    if (conf.supEnabled) {
-      md.use(mdSup)
-    }
-  }
-}

+ 0 - 8
server/modules/rendering/markdown-tasklists/definition.yml

@@ -1,8 +0,0 @@
-key: markdownTasklists
-title: Task Lists
-description: Parse task lists to checkboxes
-author: requarks.io
-icon: mdi-format-list-checks
-enabledDefault: true
-dependsOn: markdown-core
-props: {}

+ 0 - 11
server/modules/rendering/markdown-tasklists/renderer.js

@@ -1,11 +0,0 @@
-const mdTaskLists = require('markdown-it-task-lists')
-
-// ------------------------------------
-// Markdown - Task Lists
-// ------------------------------------
-
-module.exports = {
-  init (md, conf) {
-    md.use(mdTaskLists, { label: false, labelAfter: false })
-  }
-}

+ 0 - 8
server/modules/rendering/openapi-core/definition.yml

@@ -1,8 +0,0 @@
-key: openapiCore
-title: Core
-description: Basic OpenAPI Parser
-author: requarks.io
-input: openapi
-output: html
-icon: mdi-api
-props: {}

+ 0 - 14
server/modules/rendering/openapi-core/renderer.js

@@ -1,14 +0,0 @@
-const _ = require('lodash')
-
-module.exports = {
-  async render() {
-    let output = this.input
-
-    for (let child of this.children) {
-      const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)
-      output = await renderer.init(output, child.config)
-    }
-
-    return output
-  }
-}

+ 1 - 0
ux/public/_assets/icons/ultraviolet-brick.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#98ccfd" d="M2.5 30.658L2.5 7.38 20 2.519 37.5 7.38 37.5 30.658 20 37.463z"/><path fill="#4788c7" d="M20,3.038L37,7.76v22.556l-17,6.611L3,30.316V7.76L20,3.038 M20,2L2,7v24l18,7l18-7V7L20,2L20,2z"/><path fill="#b6dcfe" d="M2.5 7.62L2.5 7.38 20 2.519 37.5 7.38 37.5 7.62 20 12.481z"/><path fill="#4788c7" d="M20,3.038L36.064,7.5L20,11.962L3.936,7.5L20,3.038 M20,2L2,7v1l18,5l18-5V7L20,2L20,2z"/><path fill="#98ccfd" d="M20.5 12.38L37.5 7.658 37.5 30.658 20.5 37.269z"/><path fill="#4788c7" d="M37,8.316v22l-16,6.222V12.76L37,8.316 M38,7l-18,5v26l18-7V7L38,7z"/><path fill="#fff" d="M16.408 31.227l-2.739-.923-1.008-3.471L9.03 25.646 8.25 28.466l-2.413-.803 3.715-12.818 2.957.781L16.408 31.227zM12.036 24.225l-1.176-5.15c-.087-.386-.149-.836-.185-1.351l-.061-.017c-.025.416-.088.818-.189 1.204l-1.161 4.457L12.036 24.225zM25.41 31.018V15.831l4.166-1.298c1.238-.386 2.178-.379 2.83.014.646.39.968 1.123.968 2.203 0 .782-.195 1.534-.588 2.256-.394.726-.9 1.313-1.519 1.761v.041c.776-.143 1.391.023 1.849.496.455.471.682 1.159.682 2.067 0 1.326-.35 2.512-1.053 3.564-.709 1.062-1.687 1.842-2.943 2.34L25.41 31.018zM28.011 17.505v3.548l1.12-.387c.521-.18.929-.488 1.225-.923.296-.434.443-.941.443-1.523 0-1.082-.607-1.425-1.833-1.026L28.011 17.505zM28.011 23.558v3.945l1.378-.527c.581-.222 1.034-.574 1.36-1.055.325-.479.487-1.025.487-1.64 0-.587-.159-.99-.479-1.209-.321-.22-.771-.226-1.352-.016L28.011 23.558zM19.52 10.459c-1.103-.149-2.278-.44-3.528-.872-1.629-.563-2.493-1.157-2.59-1.782s.55-1.189 1.942-1.692c1.482-.536 3.209-.789 5.18-.76 1.971.029 3.794.333 5.467.911 1.037.358 1.797.702 2.279 1.03l-2.01.727c-.34-.38-.965-.727-1.873-1.04-.997-.345-2.076-.524-3.235-.538s-2.207.149-3.141.487c-.896.324-1.321.682-1.276 1.074s.557.757 1.535 1.095c.933.322 1.985.546 3.157.67L19.52 10.459z"/></svg>

+ 1 - 1
ux/src/i18n/locales/en.json

@@ -449,7 +449,7 @@
   "admin.navigation.visibilityMode.all": "Visible to everyone",
   "admin.navigation.visibilityMode.restricted": "Visible to select groups...",
   "admin.pages.title": "Pages",
-  "admin.rendering.subtitle": "Configure the page rendering pipeline",
+  "admin.rendering.subtitle": "Configure the content rendering pipeline",
   "admin.rendering.title": "Rendering",
   "admin.scheduler.active": "Active",
   "admin.scheduler.activeNone": "There are no active jobs at the moment.",

+ 1 - 1
ux/src/layouts/AdminLayout.vue

@@ -157,7 +157,7 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section {{ t('admin.mail.title') }}
           q-item-section(side)
             status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`')
-        q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white', disabled, v-if='flagsStore.experimental')
+        q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg')
           q-item-section {{ t('admin.rendering.title') }}

+ 73 - 269
ux/src/pages/AdminRendering.vue

@@ -11,7 +11,7 @@ q-page.admin-mail
         icon='las la-question-circle'
         flat
         color='grey'
-        href='https://docs.js.wiki/admin/rendering'
+        :href='siteStore.docsBase + `/system/rendering`'
         target='_blank'
         type='a'
         )
@@ -19,7 +19,7 @@ q-page.admin-mail
         icon='las la-redo-alt'
         flat
         color='secondary'
-        :loading='loading > 0'
+        :loading='state.loading > 0'
         @click='load'
         )
       q-btn(
@@ -28,284 +28,88 @@ q-page.admin-mail
         :label='$t(`common.actions.apply`)'
         color='secondary'
         @click='save'
-        :disabled='loading > 0'
+        :disabled='state.loading > 0'
       )
   q-separator(inset)
-  //- v-container(fluid, grid-list-lg)
-  //-   v-layout(row, wrap)
-  //-     v-flex(xs12)
-  //-       .admin-header
-  //-         img.animated.fadeInUp(src='/_assets/svg/icon-process.svg', alt='Rendering', style='width: 80px;')
-  //-         .admin-header-title
-  //-           .headline.primary--text.animated.fadeInLeft {{ $t('admin.rendering.title') }}
-  //-           .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin.rendering.subtitle') }}
-  //-         v-spacer
-  //-         v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/rendering', target='_blank')
-  //-           v-icon mdi-help-circle
-  //-         v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
-  //-           v-icon mdi-refresh
-  //-         v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-  //-           v-icon(left) mdi-check
-  //-           span {{$t('common.actions.apply')}}
 
-  //-     v-flex.animated.fadeInUp(lg3, xs12)
-  //-       v-toolbar(
-  //-         color='blue darken-2'
-  //-         dense
-  //-         flat
-  //-         dark
-  //-         )
-  //-         .subtitle-1 Pipeline
-  //-       v-expansion-panels.adm-rendering-pipeline(
-  //-         v-model='selectedCore'
-  //-         accordion
-  //-         mandatory
-  //-         )
-  //-         v-expansion-panel(
-  //-           v-for='core in renderers'
-  //-           :key='core.key'
-  //-           )
-  //-           v-expansion-panel-header(
-  //-             hide-actions
-  //-             ripple
-  //-           )
-  //-             v-toolbar(
-  //-               color='blue'
-  //-               dense
-  //-               dark
-  //-               flat
-  //-               )
-  //-               v-spacer
-  //-               .body-2 {{core.input}}
-  //-               v-icon.mx-2 mdi-arrow-right-circle
-  //-               .caption {{core.output}}
-  //-               v-spacer
-  //-           v-expansion-panel-content
-  //-             v-list.py-0(two-line, dense)
-  //-               template(v-for='(rdr, n) in core.children')
-  //-                 v-list-item(
-  //-                   :key='rdr.key'
-  //-                   @click='selectRenderer(rdr.key)'
-  //-                   :class='currentRenderer.key === rdr.key ? ($vuetify.theme.dark ? `grey darken-4-l4` : `blue lighten-5`) : ``'
-  //-                   )
-  //-                   v-list-item-avatar(size='24', tile)
-  //-                     v-icon(:color='currentRenderer.key === rdr.key ? "primary" : "grey"') {{rdr.icon}}
-  //-                   v-list-item-content
-  //-                     v-list-item-title {{rdr.title}}
-  //-                     v-list-item-subtitle: .caption {{rdr.description}}
-  //-                   v-list-item-avatar(size='24')
-  //-                     status-indicator(v-if='rdr.isEnabled', positive, pulse)
-  //-                     status-indicator(v-else, negative, pulse)
-  //-                 v-divider.my-0(v-if='n < core.children.length - 1')
+  .row.q-pa-md.q-col-gutter-md
+    .col-auto
+      q-card.rounded-borders.bg-dark
+        q-list(
+          style='min-width: 300px;'
+          padding
+          dark
+          )
+          q-item(
+            v-for='rdr of state.renderers'
+            :key='rdr.key'
+            active-class='bg-primary text-white'
+            :active='state.selectedRenderer === rdr.id'
+            @click='state.selectedRenderer = rdr.id'
+            clickable
+            )
+            q-item-section(side)
+              q-icon(:name='`img:` + rdr.icon')
+            q-item-section
+              q-item-label {{rdr.title}}
+              q-item-label(caption) {{rdr.description}}
+            q-item-section(side)
+              status-light(:color='rdr.isEnabled ? `positive` : `negative`', :pulse='rdr.isEnabled')
+    .col
+      .row.q-col-gutter-md
+        .col-12.col-lg
 
-  //-     v-flex(lg9, xs12)
-  //-       v-card.wiki-form.animated.fadeInUp
-  //-         v-toolbar(
-  //-           color='indigo'
-  //-           dark
-  //-           flat
-  //-           dense
-  //-           )
-  //-           v-icon.mr-2 {{currentRenderer.icon}}
-  //-           .subtitle-1 {{currentRenderer.title}}
-  //-           v-spacer
-  //-           v-switch(
-  //-             dark
-  //-             color='white'
-  //-             label='Enabled'
-  //-             v-model='currentRenderer.isEnabled'
-  //-             hide-details
-  //-             inset
-  //-             )
-  //-         v-card-info(color='blue')
-  //-           div
-  //-             div {{currentRenderer.description}}
-  //-             span.caption: a(href='https://docs.requarks.io/en/rendering', target='_blank') Documentation
-  //-         v-card-text.pb-4.pl-4
-  //-           .overline.mb-5 Rendering Module Configuration
-  //-           .body-2.ml-3(v-if='!currentRenderer.config || currentRenderer.config.length < 1'): em This rendering module has no configuration options you can modify.
-  //-           template(v-else, v-for='(cfg, idx) in currentRenderer.config')
-  //-             v-select(
-  //-               v-if='cfg.value.type === "string" && cfg.value.enum'
-  //-               outlined
-  //-               :items='cfg.value.enum'
-  //-               :key='cfg.key'
-  //-               :label='cfg.value.title'
-  //-               v-model='cfg.value.value'
-  //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-  //-               persistent-hint
-  //-               :class='cfg.value.hint ? "mb-2" : ""'
-  //-               color='indigo'
-  //-             )
-  //-             v-switch(
-  //-               v-else-if='cfg.value.type === "boolean"'
-  //-               :key='cfg.key'
-  //-               :label='cfg.value.title'
-  //-               v-model='cfg.value.value'
-  //-               color='indigo'
-  //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-  //-               persistent-hint
-  //-               inset
-  //-               )
-  //-             v-text-field(
-  //-               v-else
-  //-               outlined
-  //-               :key='cfg.key'
-  //-               :label='cfg.value.title'
-  //-               v-model='cfg.value.value'
-  //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-  //-               persistent-hint
-  //-               :class='cfg.value.hint ? "mb-2" : ""'
-  //-               color='indigo'
-  //-               )
-  //-             v-divider.my-5(v-if='idx < currentRenderer.config.length - 1')
-  //-         v-card-chin
-  //-           v-spacer
-  //-           .caption.pr-3.grey--text Module: {{ currentRenderer.key }}
 </template>
 
-<script>
+<script setup>
 import { cloneDeep, concat, filter, find, findIndex, reduce, reverse, sortBy } from 'lodash-es'
 import { DepGraph } from 'dependency-graph'
 import gql from 'graphql-tag'
 
-export default {
-  data () {
-    return {
-      selectedCore: -1,
-      renderers: [],
-      currentRenderer: {}
-    }
-  },
-  watch: {
-    renderers (newValue, oldValue) {
-      setTimeout(() => {
-        this.selectedCore = findIndex(newValue, ['key', 'markdownCore'])
-        this.selectRenderer('markdownCore')
-      }, 500)
-    }
-  },
-  methods: {
-    async load () {
-      this.loading++
-      try {
-        const resp = await this.$apollo.query({
-          query: gql`
-            query getRenderingConfig {
-              mailConfig {
-                senderName
-                senderEmail
-                host
-                port
-                secure
-                verifySSL
-                user
-                pass
-                useDKIM
-                dkimDomainName
-                dkimKeySelector
-                dkimPrivateKey
-              }
-            }
-          `,
-          fetchPolicy: 'no-cache'
-        })
-        if (!resp?.data?.mailConfig) {
-          throw new Error('Failed to fetch mail config.')
-        }
-        const renderers = cloneDeep(resp.data.rendering.renderers).map(str => ({
-          ...str,
-          config: sortBy(str.config.map(cfg => ({
-            ...cfg,
-            value: JSON.parse(cfg.value)
-          })), [t => t.value.order])
-        }))
-        // Build tree
-        const graph = new DepGraph({ circular: true })
-        const rawCores = filter(renderers, ['dependsOn', null]).map(core => {
-          core.children = concat([cloneDeep(core)], filter(renderers, ['dependsOn', core.key]))
-          return core
-        })
-        // Build dependency graph
-        rawCores.forEach(core => { graph.addNode(core.key) })
-        rawCores.forEach(core => {
-          rawCores.forEach(coreTarget => {
-            if (core.key !== coreTarget.key) {
-              if (core.output === coreTarget.input) {
-                graph.addDependency(core.key, coreTarget.key)
-              }
-            }
-          })
-        })
-        // Reorder cores in reverse dependency order
-        const orderedCores = []
-        reverse(graph.overallOrder()).forEach(coreKey => {
-          orderedCores.push(find(rawCores, ['key', coreKey]))
-        })
-        this.renderers = orderedCores
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: 'Failed to fetch mail config',
-          caption: err.message
-        })
-      }
-      this.loading--
-    },
-    selectRenderer (key) {
-      this.renderers.forEach(rdr => {
-        if (rdr.children.some(c => c.key === key)) {
-          this.currentRenderer = find(rdr.children, ['key', key])
-        }
-      })
-    },
-    async refresh () {
-      await this.$apollo.queries.renderers.refetch()
-      this.$store.commit('showNotification', {
-        message: 'Rendering active configuration has been reloaded.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save () {
-      this.$store.commit('loadingStart', 'admin-rendering-saverenderers')
-      await this.$apollo.mutate({
-        mutation: null,
-        variables: {
-          renderers: reduce(this.renderers, (result, core) => {
-            result.push(...core.children.map(rd => ({
-              key: rd.key,
-              isEnabled: rd.isEnabled,
-              config: rd.config.map(cfg => ({ key: cfg.key, value: JSON.stringify({ v: cfg.value.value }) }))
-            })))
-            return result
-          }, [])
-        }
-      })
-      this.$store.commit('showNotification', {
-        message: 'Rendering configuration saved successfully.',
-        style: 'success',
-        icon: 'check'
-      })
-      this.$store.commit('loadingStop', 'admin-rendering-saverenderers')
-    }
-  }
-}
-</script>
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onMounted, reactive, watch } from 'vue'
+
+import { useAdminStore } from 'src/stores/admin'
+import { useSiteStore } from 'src/stores/site'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
 
-<style lang='scss'>
-.adm-rendering-pipeline {
-  .v-expansion-panel--active .v-expansion-panel-header {
-    min-height: 0;
-  }
+// META
 
-  .v-expansion-panel-header {
-    padding: 0;
-    margin-top: 1px;
-  }
+useMeta({
+  title: t('admin.rendering.title')
+})
+
+// DATA
+
+const state = reactive({
+  renderers: [
+    { id: '123', title: 'Core', description: 'Base HTML Transformer', isEnabled: true, icon: '/_assets/icons/ultraviolet-brick.svg' }
+  ],
+  selectedRenderer: '',
+  loading: 0
+})
+
+// METHODS
+
+async function load () {
 
-  .v-expansion-panel-content__wrap {
-    padding: 0;
-  }
 }
-</style>
+
+async function save () {
+
+}
+
+</script>

+ 1 - 1
ux/src/router/routes.js

@@ -51,7 +51,7 @@ const routes = [
       { path: 'icons', component: () => import('pages/AdminIcons.vue') },
       { path: 'instances', component: () => import('pages/AdminInstances.vue') },
       { path: 'mail', component: () => import('pages/AdminMail.vue') },
-      // { path: 'rendering', component: () => import('pages/AdminRendering.vue') },
+      { path: 'rendering', component: () => import('pages/AdminRendering.vue') },
       { path: 'scheduler', component: () => import('pages/AdminScheduler.vue') },
       { path: 'security', component: () => import('pages/AdminSecurity.vue') },
       { path: 'system', component: () => import('pages/AdminSystem.vue') },