(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TinyMDE = {}));
})(this, (function (exports) { 'use strict';

  const svg = {
    blockquote: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM320-500q25 0 42.5-17.5T380-560q0-25-17.5-42.5T320-620q-25 0-42.5 17.5T260-560q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm0-60Zm-360 0Z"/></svg>`,
    bold: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z"/></svg>`,
    clear_formatting: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m528-546-93-93-121-121h486v120H568l-40 94ZM792-56 460-388l-80 188H249l119-280L56-792l56-56 736 736-56 56Z"/></svg>`,
    code: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z"/></svg>`,
    h1: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M200-280v-400h80v160h160v-160h80v400h-80v-160H280v160h-80Zm480 0v-320h-80v-80h160v400h-80Z"/></svg>`,
    h2: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M120-280v-400h80v160h160v-160h80v400h-80v-160H200v160h-80Zm400 0v-160q0-33 23.5-56.5T600-520h160v-80H520v-80h240q33 0 56.5 23.5T840-600v80q0 33-23.5 56.5T760-440H600v80h240v80H520Z"/></svg>`,
    hr: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M160-440v-80h640v80H160Z"/></svg>`,
    image: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm40-80h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/></svg>`,
    italic: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M200-200v-100h160l120-360H320v-100h400v100H580L460-300h140v100H200Z"/></svg>`,
    link: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>`,
    ol: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M120-80v-60h100v-30h-60v-60h60v-30H120v-60h120q17 0 28.5 11.5T280-280v40q0 17-11.5 28.5T240-200q17 0 28.5 11.5T280-160v40q0 17-11.5 28.5T240-80H120Zm0-280v-110q0-17 11.5-28.5T160-510h60v-30H120v-60h120q17 0 28.5 11.5T280-560v70q0 17-11.5 28.5T240-450h-60v30h100v60H120Zm60-280v-180h-60v-60h120v240h-60Zm180 440v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360Z"/></svg>`,
    strikethrough: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M80-400v-80h800v80H80Zm340-160v-120H200v-120h560v120H540v120H420Zm0 400v-160h120v160H420Z"/></svg>`,
    ul: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M360-200v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360ZM200-160q-33 0-56.5-23.5T120-240q0-33 23.5-56.5T200-320q33 0 56.5 23.5T280-240q0 33-23.5 56.5T200-160Zm0-240q-33 0-56.5-23.5T120-480q0-33 23.5-56.5T200-560q33 0 56.5 23.5T280-480q0 33-23.5 56.5T200-400Zm0-240q-33 0-56.5-23.5T120-720q0-33 23.5-56.5T200-800q33 0 56.5 23.5T280-720q0 33-23.5 56.5T200-640Z"/></svg>`
  };

  const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(typeof navigator !== "undefined" ? navigator.platform : "");
  const DefaultCommands = {
    'bold': {
      name: 'bold',
      action: 'bold',
      innerHTML: svg.bold,
      title: 'Bold',
      hotkey: 'Mod-B'
    },
    'italic': {
      name: 'italic',
      action: 'italic',
      innerHTML: svg.italic,
      title: 'Italic',
      hotkey: 'Mod-I'
    },
    'strikethrough': {
      name: 'strikethrough',
      action: 'strikethrough',
      innerHTML: svg.strikethrough,
      title: 'Strikethrough',
      hotkey: 'Mod2-Shift-5'
    },
    'code': {
      name: 'code',
      action: 'code',
      innerHTML: svg.code,
      title: 'Format as code'
    },
    'h1': {
      name: 'h1',
      action: 'h1',
      innerHTML: svg.h1,
      title: 'Level 1 heading',
      hotkey: 'Mod-Shift-1'
    },
    'h2': {
      name: 'h2',
      action: 'h2',
      innerHTML: svg.h2,
      title: 'Level 2 heading',
      hotkey: 'Mod-Shift-2'
    },
    'ul': {
      name: 'ul',
      action: 'ul',
      innerHTML: svg.ul,
      title: 'Bulleted list'
    },
    'ol': {
      name: 'ol',
      action: 'ol',
      innerHTML: svg.ol,
      title: 'Numbered list'
    },
    'blockquote': {
      name: 'blockquote',
      action: 'blockquote',
      innerHTML: svg.blockquote,
      title: 'Quote',
      hotkey: 'Mod2-Shift-Q'
    },
    'insertLink': {
      name: 'insertLink',
      action: editor => {
        if (editor.isInlineFormattingAllowed()) editor.wrapSelection('[', ']()');
      },
      enabled: (editor, focus, anchor) => editor.isInlineFormattingAllowed(focus, anchor) ? false : null,
      innerHTML: svg.link,
      title: 'Insert link',
      hotkey: 'Mod-K'
    },
    'insertImage': {
      name: 'insertImage',
      action: editor => {
        if (editor.isInlineFormattingAllowed()) editor.wrapSelection('![', ']()');
      },
      enabled: (editor, focus, anchor) => editor.isInlineFormattingAllowed(focus, anchor) ? false : null,
      innerHTML: svg.image,
      title: 'Insert image',
      hotkey: 'Mod2-Shift-I'
    },
    'hr': {
      name: 'hr',
      action: editor => editor.paste('\n***\n'),
      enabled: () => false,
      innerHTML: svg.hr,
      title: 'Insert horizontal line',
      hotkey: 'Mod2-Shift-L'
    }
  };
  class CommandBar {
    constructor(props) {
      this.e = null;
      this.editor = null;
      this.commands = [];
      this.buttons = {};
      this.state = {};
      this.hotkeys = [];
      let element = props.element;
      if (element && !element.tagName) {
        element = document.getElementById(props.element);
      }
      if (!element) {
        element = document.body;
      }
      this.createCommandBarElement(element, props.commands || ['bold', 'italic', 'strikethrough', '|', 'code', '|', 'h1', 'h2', '|', 'ul', 'ol', '|', 'blockquote', 'hr', '|', 'insertLink', 'insertImage']);
      document.addEventListener('keydown', e => this.handleKeydown(e));
      if (props.editor) this.setEditor(props.editor);
    }
    createCommandBarElement(parentElement, commands) {
      this.e = document.createElement('div');
      this.e.className = 'TMCommandBar';
      for (let command of commands) {
        if (command == '|') {
          let el = document.createElement('div');
          el.className = 'TMCommandDivider';
          this.e.appendChild(el);
        } else {
          let commandName;
          if (typeof command == "string") {
            // Reference to default command

            if (DefaultCommands[command]) {
              commandName = command;
              this.commands[commandName] = DefaultCommands[commandName];
            } else {
              continue;
            }
          } else if (typeof command == "object" && command.name) {
            commandName = command.name;
            this.commands[commandName] = {};
            if (DefaultCommands[commandName]) Object.assign(this.commands[commandName], DefaultCommands[commandName]);
            Object.assign(this.commands[commandName], command);
          } else {
            continue;
          }
          let title = this.commands[commandName].title || commandName;
          if (this.commands[commandName].hotkey) {
            const keys = this.commands[commandName].hotkey.split('-');
            // construct modifiers
            let modifiers = [];
            let modifierexplanation = [];
            for (let i = 0; i < keys.length - 1; i++) {
              switch (keys[i]) {
                case 'Ctrl':
                  modifiers.push('ctrlKey');
                  modifierexplanation.push('Ctrl');
                  break;
                case 'Cmd':
                  modifiers.push('metaKey');
                  modifierexplanation.push('⌘');
                  break;
                case 'Alt':
                  modifiers.push('altKey');
                  modifierexplanation.push('Alt');
                  break;
                case 'Option':
                  modifiers.push('altKey');
                  modifierexplanation.push('⌥');
                  break;
                case 'Win':
                  modifiers.push('metaKey');
                  modifierexplanation.push('⊞ Win');
                  break;
                case 'Shift':
                  modifiers.push('shiftKey');
                  modifierexplanation.push('⇧');
                  break;
                case 'Mod':
                  // Mod is a convenience mechanism: Ctrl on Windows, Cmd on Mac
                  if (isMacLike) {
                    modifiers.push('metaKey');
                    modifierexplanation.push('⌘');
                  } else {
                    modifiers.push('ctrlKey');
                    modifierexplanation.push('Ctrl');
                  }
                  break;
                case 'Mod2':
                  modifiers.push('altKey');
                  if (isMacLike) modifierexplanation.push('⌥');else modifierexplanation.push('Alt');
                  break;
                // Mod2 is a convenience mechanism: Alt on Windows, Option on Mac
              }
            }

            modifierexplanation.push(keys[keys.length - 1]);
            let hotkey = {
              modifiers: modifiers,
              command: commandName
            };
            // TODO Right now this is working only for letters and numbers
            if (keys[keys.length - 1].match(/^[0-9]$/)) {
              hotkey.code = `Digit${keys[keys.length - 1]}`;
            } else {
              hotkey.key = keys[keys.length - 1].toLowerCase();
            }
            this.hotkeys.push(hotkey);
            title = title.concat(` (${modifierexplanation.join('+')})`);
          }
          this.buttons[commandName] = document.createElement('div');
          this.buttons[commandName].className = 'TMCommandButton TMCommandButton_Disabled';
          this.buttons[commandName].title = title;
          this.buttons[commandName].innerHTML = this.commands[commandName].innerHTML;
          this.buttons[commandName].addEventListener('mousedown', e => this.handleClick(commandName, e));
          this.e.appendChild(this.buttons[commandName]);
        }
      }
      parentElement.appendChild(this.e);
    }
    handleClick(commandName, event) {
      if (!this.editor) return;
      event.preventDefault();
      if (typeof this.commands[commandName].action == "string") {
        if (this.state[commandName] === false) this.editor.setCommandState(commandName, true);else this.editor.setCommandState(commandName, false);
      } else if (typeof this.commands[commandName].action == "function") {
        this.commands[commandName].action(this.editor);
      }
    }
    setEditor(editor) {
      this.editor = editor;
      editor.addEventListener('selection', e => this.handleSelection(e));
    }
    handleSelection(event) {
      if (event.commandState) {
        for (let command in this.commands) {
          if (event.commandState[command] === undefined) {
            if (this.commands[command].enabled) this.state[command] = this.commands[command].enabled(this.editor, event.focus, event.anchor);else this.state[command] = event.focus ? false : null;
          } else {
            this.state[command] = event.commandState[command];
          }
          if (this.state[command] === true) {
            this.buttons[command].className = 'TMCommandButton TMCommandButton_Active';
          } else if (this.state[command] === false) {
            this.buttons[command].className = 'TMCommandButton TMCommandButton_Inactive';
          } else {
            this.buttons[command].className = 'TMCommandButton TMCommandButton_Disabled';
          }
        }
      }
    }
    handleKeydown(event) {
      outer: for (let hotkey of this.hotkeys) {
        if (hotkey.key && event.key.toLowerCase() == hotkey.key || hotkey.code && event.code == hotkey.code) {
          // Key matches hotkey. Look for any required modifier that wasn't pressed
          for (let modifier of hotkey.modifiers) {
            if (!event[modifier]) continue outer;
          }
          // Everything matches.
          this.handleClick(hotkey.command, event);
          return;
        }
      }
    }
  }

  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

  var check = function (it) {
    return it && it.Math === Math && it;
  };

  // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
  var global$b =
  // eslint-disable-next-line es/no-global-this -- safe
  check(typeof globalThis == 'object' && globalThis) || check(typeof window == 'object' && window) ||
  // eslint-disable-next-line no-restricted-globals -- safe
  check(typeof self == 'object' && self) || check(typeof commonjsGlobal == 'object' && commonjsGlobal) ||
  // eslint-disable-next-line no-new-func -- fallback
  function () {
    return this;
  }() || commonjsGlobal || Function('return this')();

  var fails$7 = function (exec) {
    try {
      return !!exec();
    } catch (error) {
      return true;
    }
  };

  var fails$6 = fails$7;

  // Detect IE8's incomplete defineProperty implementation
  var descriptors = !fails$6(function () {
    // eslint-disable-next-line es/no-object-defineproperty -- required for testing
    return Object.defineProperty({}, 1, {
      get: function () {
        return 7;
      }
    })[1] !== 7;
  });

  var makeBuiltInExports = {};
  var makeBuiltIn$2 = {
    get exports(){ return makeBuiltInExports; },
    set exports(v){ makeBuiltInExports = v; },
  };

  var fails$5 = fails$7;
  var functionBindNative = !fails$5(function () {
    // eslint-disable-next-line es/no-function-prototype-bind -- safe
    var test = function () {/* empty */}.bind();
    // eslint-disable-next-line no-prototype-builtins -- safe
    return typeof test != 'function' || test.hasOwnProperty('prototype');
  });

  var NATIVE_BIND$1 = functionBindNative;
  var FunctionPrototype$1 = Function.prototype;
  var call$3 = FunctionPrototype$1.call;
  var uncurryThisWithBind = NATIVE_BIND$1 && FunctionPrototype$1.bind.bind(call$3, call$3);
  var functionUncurryThis = NATIVE_BIND$1 ? uncurryThisWithBind : function (fn) {
    return function () {
      return call$3.apply(fn, arguments);
    };
  };

  var documentAll$2 = typeof document == 'object' && document.all;

  // https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
  // eslint-disable-next-line unicorn/no-typeof-undefined -- required for testing
  var IS_HTMLDDA = typeof documentAll$2 == 'undefined' && documentAll$2 !== undefined;
  var documentAll_1 = {
    all: documentAll$2,
    IS_HTMLDDA: IS_HTMLDDA
  };

  var $documentAll$1 = documentAll_1;
  var documentAll$1 = $documentAll$1.all;

  // `IsCallable` abstract operation
  // https://tc39.es/ecma262/#sec-iscallable
  var isCallable$8 = $documentAll$1.IS_HTMLDDA ? function (argument) {
    return typeof argument == 'function' || argument === documentAll$1;
  } : function (argument) {
    return typeof argument == 'function';
  };

  // we can't use just `it == null` since of `document.all` special case
  // https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot-aec
  var isNullOrUndefined$2 = function (it) {
    return it === null || it === undefined;
  };

  var isNullOrUndefined$1 = isNullOrUndefined$2;
  var $TypeError$5 = TypeError;

  // `RequireObjectCoercible` abstract operation
  // https://tc39.es/ecma262/#sec-requireobjectcoercible
  var requireObjectCoercible$1 = function (it) {
    if (isNullOrUndefined$1(it)) throw new $TypeError$5("Can't call method on " + it);
    return it;
  };

  var requireObjectCoercible = requireObjectCoercible$1;
  var $Object$1 = Object;

  // `ToObject` abstract operation
  // https://tc39.es/ecma262/#sec-toobject
  var toObject$1 = function (argument) {
    return $Object$1(requireObjectCoercible(argument));
  };

  var uncurryThis$4 = functionUncurryThis;
  var toObject = toObject$1;
  var hasOwnProperty = uncurryThis$4({}.hasOwnProperty);

  // `HasOwnProperty` abstract operation
  // https://tc39.es/ecma262/#sec-hasownproperty
  // eslint-disable-next-line es/no-object-hasown -- safe
  var hasOwnProperty_1 = Object.hasOwn || function hasOwn(it, key) {
    return hasOwnProperty(toObject(it), key);
  };

  var DESCRIPTORS$6 = descriptors;
  var hasOwn$3 = hasOwnProperty_1;
  var FunctionPrototype = Function.prototype;
  // eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
  var getDescriptor = DESCRIPTORS$6 && Object.getOwnPropertyDescriptor;
  var EXISTS$1 = hasOwn$3(FunctionPrototype, 'name');
  // additional protection from minified / mangled / dropped function names
  var PROPER = EXISTS$1 && function something() {/* empty */}.name === 'something';
  var CONFIGURABLE$1 = EXISTS$1 && (!DESCRIPTORS$6 || DESCRIPTORS$6 && getDescriptor(FunctionPrototype, 'name').configurable);
  var functionName = {
    EXISTS: EXISTS$1,
    PROPER: PROPER,
    CONFIGURABLE: CONFIGURABLE$1
  };

  var global$a = global$b;

  // eslint-disable-next-line es/no-object-defineproperty -- safe
  var defineProperty$2 = Object.defineProperty;
  var defineGlobalProperty$1 = function (key, value) {
    try {
      defineProperty$2(global$a, key, {
        value: value,
        configurable: true,
        writable: true
      });
    } catch (error) {
      global$a[key] = value;
    }
    return value;
  };

  var global$9 = global$b;
  var defineGlobalProperty = defineGlobalProperty$1;
  var SHARED = '__core-js_shared__';
  var store$3 = global$9[SHARED] || defineGlobalProperty(SHARED, {});
  var sharedStore = store$3;

  var uncurryThis$3 = functionUncurryThis;
  var isCallable$7 = isCallable$8;
  var store$2 = sharedStore;
  var functionToString = uncurryThis$3(Function.toString);

  // this helper broken in `core-js@3.4.1-3.4.4`, so we can't use `shared` helper
  if (!isCallable$7(store$2.inspectSource)) {
    store$2.inspectSource = function (it) {
      return functionToString(it);
    };
  }
  var inspectSource$1 = store$2.inspectSource;

  var global$8 = global$b;
  var isCallable$6 = isCallable$8;
  var WeakMap$1 = global$8.WeakMap;
  var weakMapBasicDetection = isCallable$6(WeakMap$1) && /native code/.test(String(WeakMap$1));

  var isCallable$5 = isCallable$8;
  var $documentAll = documentAll_1;
  var documentAll = $documentAll.all;
  var isObject$5 = $documentAll.IS_HTMLDDA ? function (it) {
    return typeof it == 'object' ? it !== null : isCallable$5(it) || it === documentAll;
  } : function (it) {
    return typeof it == 'object' ? it !== null : isCallable$5(it);
  };

  var objectDefineProperty = {};

  var global$7 = global$b;
  var isObject$4 = isObject$5;
  var document$1 = global$7.document;
  // typeof document.createElement is 'object' in old IE
  var EXISTS = isObject$4(document$1) && isObject$4(document$1.createElement);
  var documentCreateElement = function (it) {
    return EXISTS ? document$1.createElement(it) : {};
  };

  var DESCRIPTORS$5 = descriptors;
  var fails$4 = fails$7;
  var createElement = documentCreateElement;

  // Thanks to IE8 for its funny defineProperty
  var ie8DomDefine = !DESCRIPTORS$5 && !fails$4(function () {
    // eslint-disable-next-line es/no-object-defineproperty -- required for testing
    return Object.defineProperty(createElement('div'), 'a', {
      get: function () {
        return 7;
      }
    }).a !== 7;
  });

  var DESCRIPTORS$4 = descriptors;
  var fails$3 = fails$7;

  // V8 ~ Chrome 36-
  // https://bugs.chromium.org/p/v8/issues/detail?id=3334
  var v8PrototypeDefineBug = DESCRIPTORS$4 && fails$3(function () {
    // eslint-disable-next-line es/no-object-defineproperty -- required for testing
    return Object.defineProperty(function () {/* empty */}, 'prototype', {
      value: 42,
      writable: false
    }).prototype !== 42;
  });

  var isObject$3 = isObject$5;
  var $String$3 = String;
  var $TypeError$4 = TypeError;

  // `Assert: Type(argument) is Object`
  var anObject$2 = function (argument) {
    if (isObject$3(argument)) return argument;
    throw new $TypeError$4($String$3(argument) + ' is not an object');
  };

  var NATIVE_BIND = functionBindNative;
  var call$2 = Function.prototype.call;
  var functionCall = NATIVE_BIND ? call$2.bind(call$2) : function () {
    return call$2.apply(call$2, arguments);
  };

  var global$6 = global$b;
  var isCallable$4 = isCallable$8;
  var aFunction = function (argument) {
    return isCallable$4(argument) ? argument : undefined;
  };
  var getBuiltIn$1 = function (namespace, method) {
    return arguments.length < 2 ? aFunction(global$6[namespace]) : global$6[namespace] && global$6[namespace][method];
  };

  var uncurryThis$2 = functionUncurryThis;
  var objectIsPrototypeOf = uncurryThis$2({}.isPrototypeOf);

  var engineUserAgent = typeof navigator != 'undefined' && String(navigator.userAgent) || '';

  var global$5 = global$b;
  var userAgent = engineUserAgent;
  var process = global$5.process;
  var Deno = global$5.Deno;
  var versions = process && process.versions || Deno && Deno.version;
  var v8 = versions && versions.v8;
  var match, version;
  if (v8) {
    match = v8.split('.');
    // in old Chrome, versions of V8 isn't V8 = Chrome / 10
    // but their correct versions are not interesting for us
    version = match[0] > 0 && match[0] < 4 ? 1 : +(match[0] + match[1]);
  }

  // BrowserFS NodeJS `process` polyfill incorrectly set `.v8` to `0.0`
  // so check `userAgent` even if `.v8` exists, but 0
  if (!version && userAgent) {
    match = userAgent.match(/Edge\/(\d+)/);
    if (!match || match[1] >= 74) {
      match = userAgent.match(/Chrome\/(\d+)/);
      if (match) version = +match[1];
    }
  }
  var engineV8Version = version;

  /* eslint-disable es/no-symbol -- required for testing */
  var V8_VERSION = engineV8Version;
  var fails$2 = fails$7;
  var global$4 = global$b;
  var $String$2 = global$4.String;

  // eslint-disable-next-line es/no-object-getownpropertysymbols -- required for testing
  var symbolConstructorDetection = !!Object.getOwnPropertySymbols && !fails$2(function () {
    var symbol = Symbol('symbol detection');
    // Chrome 38 Symbol has incorrect toString conversion
    // `get-own-property-symbols` polyfill symbols converted to object are not Symbol instances
    // nb: Do not call `String` directly to avoid this being optimized out to `symbol+''` which will,
    // of course, fail.
    return !$String$2(symbol) || !(Object(symbol) instanceof Symbol) ||
    // Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances
    !Symbol.sham && V8_VERSION && V8_VERSION < 41;
  });

  /* eslint-disable es/no-symbol -- required for testing */
  var NATIVE_SYMBOL$1 = symbolConstructorDetection;
  var useSymbolAsUid = NATIVE_SYMBOL$1 && !Symbol.sham && typeof Symbol.iterator == 'symbol';

  var getBuiltIn = getBuiltIn$1;
  var isCallable$3 = isCallable$8;
  var isPrototypeOf = objectIsPrototypeOf;
  var USE_SYMBOL_AS_UID$1 = useSymbolAsUid;
  var $Object = Object;
  var isSymbol$2 = USE_SYMBOL_AS_UID$1 ? function (it) {
    return typeof it == 'symbol';
  } : function (it) {
    var $Symbol = getBuiltIn('Symbol');
    return isCallable$3($Symbol) && isPrototypeOf($Symbol.prototype, $Object(it));
  };

  var $String$1 = String;
  var tryToString$1 = function (argument) {
    try {
      return $String$1(argument);
    } catch (error) {
      return 'Object';
    }
  };

  var isCallable$2 = isCallable$8;
  var tryToString = tryToString$1;
  var $TypeError$3 = TypeError;

  // `Assert: IsCallable(argument) is true`
  var aCallable$1 = function (argument) {
    if (isCallable$2(argument)) return argument;
    throw new $TypeError$3(tryToString(argument) + ' is not a function');
  };

  var aCallable = aCallable$1;
  var isNullOrUndefined = isNullOrUndefined$2;

  // `GetMethod` abstract operation
  // https://tc39.es/ecma262/#sec-getmethod
  var getMethod$1 = function (V, P) {
    var func = V[P];
    return isNullOrUndefined(func) ? undefined : aCallable(func);
  };

  var call$1 = functionCall;
  var isCallable$1 = isCallable$8;
  var isObject$2 = isObject$5;
  var $TypeError$2 = TypeError;

  // `OrdinaryToPrimitive` abstract operation
  // https://tc39.es/ecma262/#sec-ordinarytoprimitive
  var ordinaryToPrimitive$1 = function (input, pref) {
    var fn, val;
    if (pref === 'string' && isCallable$1(fn = input.toString) && !isObject$2(val = call$1(fn, input))) return val;
    if (isCallable$1(fn = input.valueOf) && !isObject$2(val = call$1(fn, input))) return val;
    if (pref !== 'string' && isCallable$1(fn = input.toString) && !isObject$2(val = call$1(fn, input))) return val;
    throw new $TypeError$2("Can't convert object to primitive value");
  };

  var sharedExports = {};
  var shared$3 = {
    get exports(){ return sharedExports; },
    set exports(v){ sharedExports = v; },
  };

  var store$1 = sharedStore;
  (shared$3.exports = function (key, value) {
    return store$1[key] || (store$1[key] = value !== undefined ? value : {});
  })('versions', []).push({
    version: '3.33.0',
    mode: 'global',
    copyright: '© 2014-2023 Denis Pushkarev (zloirock.ru)',
    license: 'https://github.com/zloirock/core-js/blob/v3.33.0/LICENSE',
    source: 'https://github.com/zloirock/core-js'
  });

  var uncurryThis$1 = functionUncurryThis;
  var id = 0;
  var postfix = Math.random();
  var toString = uncurryThis$1(1.0.toString);
  var uid$2 = function (key) {
    return 'Symbol(' + (key === undefined ? '' : key) + ')_' + toString(++id + postfix, 36);
  };

  var global$3 = global$b;
  var shared$2 = sharedExports;
  var hasOwn$2 = hasOwnProperty_1;
  var uid$1 = uid$2;
  var NATIVE_SYMBOL = symbolConstructorDetection;
  var USE_SYMBOL_AS_UID = useSymbolAsUid;
  var Symbol$1 = global$3.Symbol;
  var WellKnownSymbolsStore = shared$2('wks');
  var createWellKnownSymbol = USE_SYMBOL_AS_UID ? Symbol$1['for'] || Symbol$1 : Symbol$1 && Symbol$1.withoutSetter || uid$1;
  var wellKnownSymbol$1 = function (name) {
    if (!hasOwn$2(WellKnownSymbolsStore, name)) {
      WellKnownSymbolsStore[name] = NATIVE_SYMBOL && hasOwn$2(Symbol$1, name) ? Symbol$1[name] : createWellKnownSymbol('Symbol.' + name);
    }
    return WellKnownSymbolsStore[name];
  };

  var call = functionCall;
  var isObject$1 = isObject$5;
  var isSymbol$1 = isSymbol$2;
  var getMethod = getMethod$1;
  var ordinaryToPrimitive = ordinaryToPrimitive$1;
  var wellKnownSymbol = wellKnownSymbol$1;
  var $TypeError$1 = TypeError;
  var TO_PRIMITIVE = wellKnownSymbol('toPrimitive');

  // `ToPrimitive` abstract operation
  // https://tc39.es/ecma262/#sec-toprimitive
  var toPrimitive$1 = function (input, pref) {
    if (!isObject$1(input) || isSymbol$1(input)) return input;
    var exoticToPrim = getMethod(input, TO_PRIMITIVE);
    var result;
    if (exoticToPrim) {
      if (pref === undefined) pref = 'default';
      result = call(exoticToPrim, input, pref);
      if (!isObject$1(result) || isSymbol$1(result)) return result;
      throw new $TypeError$1("Can't convert object to primitive value");
    }
    if (pref === undefined) pref = 'number';
    return ordinaryToPrimitive(input, pref);
  };

  var toPrimitive = toPrimitive$1;
  var isSymbol = isSymbol$2;

  // `ToPropertyKey` abstract operation
  // https://tc39.es/ecma262/#sec-topropertykey
  var toPropertyKey$1 = function (argument) {
    var key = toPrimitive(argument, 'string');
    return isSymbol(key) ? key : key + '';
  };

  var DESCRIPTORS$3 = descriptors;
  var IE8_DOM_DEFINE = ie8DomDefine;
  var V8_PROTOTYPE_DEFINE_BUG = v8PrototypeDefineBug;
  var anObject$1 = anObject$2;
  var toPropertyKey = toPropertyKey$1;
  var $TypeError = TypeError;
  // eslint-disable-next-line es/no-object-defineproperty -- safe
  var $defineProperty = Object.defineProperty;
  // eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
  var $getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
  var ENUMERABLE = 'enumerable';
  var CONFIGURABLE = 'configurable';
  var WRITABLE = 'writable';

  // `Object.defineProperty` method
  // https://tc39.es/ecma262/#sec-object.defineproperty
  objectDefineProperty.f = DESCRIPTORS$3 ? V8_PROTOTYPE_DEFINE_BUG ? function defineProperty(O, P, Attributes) {
    anObject$1(O);
    P = toPropertyKey(P);
    anObject$1(Attributes);
    if (typeof O === 'function' && P === 'prototype' && 'value' in Attributes && WRITABLE in Attributes && !Attributes[WRITABLE]) {
      var current = $getOwnPropertyDescriptor(O, P);
      if (current && current[WRITABLE]) {
        O[P] = Attributes.value;
        Attributes = {
          configurable: CONFIGURABLE in Attributes ? Attributes[CONFIGURABLE] : current[CONFIGURABLE],
          enumerable: ENUMERABLE in Attributes ? Attributes[ENUMERABLE] : current[ENUMERABLE],
          writable: false
        };
      }
    }
    return $defineProperty(O, P, Attributes);
  } : $defineProperty : function defineProperty(O, P, Attributes) {
    anObject$1(O);
    P = toPropertyKey(P);
    anObject$1(Attributes);
    if (IE8_DOM_DEFINE) try {
      return $defineProperty(O, P, Attributes);
    } catch (error) {/* empty */}
    if ('get' in Attributes || 'set' in Attributes) throw new $TypeError('Accessors not supported');
    if ('value' in Attributes) O[P] = Attributes.value;
    return O;
  };

  var createPropertyDescriptor$1 = function (bitmap, value) {
    return {
      enumerable: !(bitmap & 1),
      configurable: !(bitmap & 2),
      writable: !(bitmap & 4),
      value: value
    };
  };

  var DESCRIPTORS$2 = descriptors;
  var definePropertyModule = objectDefineProperty;
  var createPropertyDescriptor = createPropertyDescriptor$1;
  var createNonEnumerableProperty$1 = DESCRIPTORS$2 ? function (object, key, value) {
    return definePropertyModule.f(object, key, createPropertyDescriptor(1, value));
  } : function (object, key, value) {
    object[key] = value;
    return object;
  };

  var shared$1 = sharedExports;
  var uid = uid$2;
  var keys = shared$1('keys');
  var sharedKey$1 = function (key) {
    return keys[key] || (keys[key] = uid(key));
  };

  var NATIVE_WEAK_MAP = weakMapBasicDetection;
  var global$2 = global$b;
  var isObject = isObject$5;
  var createNonEnumerableProperty = createNonEnumerableProperty$1;
  var hasOwn$1 = hasOwnProperty_1;
  var shared = sharedStore;
  var sharedKey = sharedKey$1;
  var OBJECT_ALREADY_INITIALIZED = 'Object already initialized';
  var TypeError$1 = global$2.TypeError;
  var WeakMap = global$2.WeakMap;
  var set, get, has;
  var enforce = function (it) {
    return has(it) ? get(it) : set(it, {});
  };
  var getterFor = function (TYPE) {
    return function (it) {
      var state;
      if (!isObject(it) || (state = get(it)).type !== TYPE) {
        throw new TypeError$1('Incompatible receiver, ' + TYPE + ' required');
      }
      return state;
    };
  };
  if (NATIVE_WEAK_MAP || shared.state) {
    var store = shared.state || (shared.state = new WeakMap());
    /* eslint-disable no-self-assign -- prototype methods protection */
    store.get = store.get;
    store.has = store.has;
    store.set = store.set;
    /* eslint-enable no-self-assign -- prototype methods protection */
    set = function (it, metadata) {
      if (store.has(it)) throw new TypeError$1(OBJECT_ALREADY_INITIALIZED);
      metadata.facade = it;
      store.set(it, metadata);
      return metadata;
    };
    get = function (it) {
      return store.get(it) || {};
    };
    has = function (it) {
      return store.has(it);
    };
  } else {
    var STATE = sharedKey('state');
    set = function (it, metadata) {
      if (hasOwn$1(it, STATE)) throw new TypeError$1(OBJECT_ALREADY_INITIALIZED);
      metadata.facade = it;
      createNonEnumerableProperty(it, STATE, metadata);
      return metadata;
    };
    get = function (it) {
      return hasOwn$1(it, STATE) ? it[STATE] : {};
    };
    has = function (it) {
      return hasOwn$1(it, STATE);
    };
  }
  var internalState = {
    set: set,
    get: get,
    has: has,
    enforce: enforce,
    getterFor: getterFor
  };

  var uncurryThis = functionUncurryThis;
  var fails$1 = fails$7;
  var isCallable = isCallable$8;
  var hasOwn = hasOwnProperty_1;
  var DESCRIPTORS$1 = descriptors;
  var CONFIGURABLE_FUNCTION_NAME = functionName.CONFIGURABLE;
  var inspectSource = inspectSource$1;
  var InternalStateModule = internalState;
  var enforceInternalState = InternalStateModule.enforce;
  var getInternalState = InternalStateModule.get;
  var $String = String;
  // eslint-disable-next-line es/no-object-defineproperty -- safe
  var defineProperty$1 = Object.defineProperty;
  var stringSlice = uncurryThis(''.slice);
  var replace = uncurryThis(''.replace);
  var join = uncurryThis([].join);
  var CONFIGURABLE_LENGTH = DESCRIPTORS$1 && !fails$1(function () {
    return defineProperty$1(function () {/* empty */}, 'length', {
      value: 8
    }).length !== 8;
  });
  var TEMPLATE = String(String).split('String');
  var makeBuiltIn$1 = makeBuiltIn$2.exports = function (value, name, options) {
    if (stringSlice($String(name), 0, 7) === 'Symbol(') {
      name = '[' + replace($String(name), /^Symbol\(([^)]*)\)/, '$1') + ']';
    }
    if (options && options.getter) name = 'get ' + name;
    if (options && options.setter) name = 'set ' + name;
    if (!hasOwn(value, 'name') || CONFIGURABLE_FUNCTION_NAME && value.name !== name) {
      if (DESCRIPTORS$1) defineProperty$1(value, 'name', {
        value: name,
        configurable: true
      });else value.name = name;
    }
    if (CONFIGURABLE_LENGTH && options && hasOwn(options, 'arity') && value.length !== options.arity) {
      defineProperty$1(value, 'length', {
        value: options.arity
      });
    }
    try {
      if (options && hasOwn(options, 'constructor') && options.constructor) {
        if (DESCRIPTORS$1) defineProperty$1(value, 'prototype', {
          writable: false
        });
        // in V8 ~ Chrome 53, prototypes of some methods, like `Array.prototype.values`, are non-writable
      } else if (value.prototype) value.prototype = undefined;
    } catch (error) {/* empty */}
    var state = enforceInternalState(value);
    if (!hasOwn(state, 'source')) {
      state.source = join(TEMPLATE, typeof name == 'string' ? name : '');
    }
    return value;
  };

  // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative
  // eslint-disable-next-line no-extend-native -- required
  Function.prototype.toString = makeBuiltIn$1(function toString() {
    return isCallable(this) && getInternalState(this).source || inspectSource(this);
  }, 'toString');

  var makeBuiltIn = makeBuiltInExports;
  var defineProperty = objectDefineProperty;
  var defineBuiltInAccessor$1 = function (target, name, descriptor) {
    if (descriptor.get) makeBuiltIn(descriptor.get, name, {
      getter: true
    });
    if (descriptor.set) makeBuiltIn(descriptor.set, name, {
      setter: true
    });
    return defineProperty.f(target, name, descriptor);
  };

  var anObject = anObject$2;

  // `RegExp.prototype.flags` getter implementation
  // https://tc39.es/ecma262/#sec-get-regexp.prototype.flags
  var regexpFlags = function () {
    var that = anObject(this);
    var result = '';
    if (that.hasIndices) result += 'd';
    if (that.global) result += 'g';
    if (that.ignoreCase) result += 'i';
    if (that.multiline) result += 'm';
    if (that.dotAll) result += 's';
    if (that.unicode) result += 'u';
    if (that.unicodeSets) result += 'v';
    if (that.sticky) result += 'y';
    return result;
  };

  var global$1 = global$b;
  var DESCRIPTORS = descriptors;
  var defineBuiltInAccessor = defineBuiltInAccessor$1;
  var regExpFlags = regexpFlags;
  var fails = fails$7;

  // babel-minify and Closure Compiler transpiles RegExp('.', 'd') -> /./d and it causes SyntaxError
  var RegExp$1 = global$1.RegExp;
  var RegExpPrototype = RegExp$1.prototype;
  var FORCED = DESCRIPTORS && fails(function () {
    var INDICES_SUPPORT = true;
    try {
      RegExp$1('.', 'd');
    } catch (error) {
      INDICES_SUPPORT = false;
    }
    var O = {};
    // modern V8 bug
    var calls = '';
    var expected = INDICES_SUPPORT ? 'dgimsy' : 'gimsy';
    var addGetter = function (key, chr) {
      // eslint-disable-next-line es/no-object-defineproperty -- safe
      Object.defineProperty(O, key, {
        get: function () {
          calls += chr;
          return true;
        }
      });
    };
    var pairs = {
      dotAll: 's',
      global: 'g',
      ignoreCase: 'i',
      multiline: 'm',
      sticky: 'y'
    };
    if (INDICES_SUPPORT) pairs.hasIndices = 'd';
    for (var key in pairs) addGetter(key, pairs[key]);

    // eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe
    var result = Object.getOwnPropertyDescriptor(RegExpPrototype, 'flags').get.call(O);
    return result !== expected || calls !== expected;
  });

  // `RegExp.prototype.flags` getter
  // https://tc39.es/ecma262/#sec-get-regexp.prototype.flags
  if (FORCED) defineBuiltInAccessor(RegExpPrototype, 'flags', {
    configurable: true,
    get: regExpFlags
  });

  // const replacements = {
  //   ASCIIPunctuation: '!"#$%&\'()*+,\\-./:;<=>?@\\[\\]^_`{|}~',
  //   TriggerChars: '`_\*\[\]\(\)',
  //   Scheme: `[A-Za-z][A-Za-z0-9\+\.\-]{1,31}`,
  //   Email: `[a-zA-Z0-9.!#$%&'*+/=?^_\`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*`, // From CommonMark spec

  // }
  const replacements = {
    ASCIIPunctuation: /[!"#$%&'()*+,\-./:;<=>?@[\]^_`{|}~\\]/,
    NotTriggerChar: /[^`_*[\]()<>!~]/,
    Scheme: /[A-Za-z][A-Za-z0-9+.-]{1,31}/,
    Email: /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/,
    // From CommonMark spec
    HTMLOpenTag: /<HTMLTagName(?:HTMLAttribute)*\s*\/?>/,
    HTMLCloseTag: /<\/HTMLTagName\s*>/,
    HTMLTagName: /[A-Za-z][A-Za-z0-9-]*/,
    HTMLComment: /<!--(?:[^>-]|(?:[^>-](?:[^-]|-[^-])*[^-]))-->/,
    HTMLPI: /<\?(?:|.|(?:[^?]|\?[^>])*)\?>/,
    HTMLDeclaration: /<![A-Z]+\s[^>]*>/,
    HTMLCDATA: /<!\[CDATA\[.*?\]\]>/,
    HTMLAttribute: /\s+[A-Za-z_:][A-Za-z0-9_.:-]*(?:HTMLAttValue)?/,
    HTMLAttValue: /\s*=\s*(?:(?:'[^']*')|(?:"[^"]*")|(?:[^\s"'=<>`]+))/,
    KnownTag: /address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul/
  };

  // From CommonMark.js. 
  const punctuationLeading = new RegExp(/^(?:[!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B])/);
  const punctuationTrailing = new RegExp(/(?:[!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B])$/);

  // export const inlineTriggerChars = new RegExp(`[${replacements.TriggerChars}]`);

  /**
   * This is CommonMark's block grammar, but we're ignoring nested blocks here.  
   */
  const lineGrammar = {
    TMH1: {
      regexp: /^( {0,3}#\s)(.*?)((?:\s+#+\s*)?)$/,
      replacement: '<span class="TMMark TMMark_TMH1">$1</span>$$2<span class="TMMark TMMark_TMH1">$3</span>'
    },
    TMH2: {
      regexp: /^( {0,3}##\s)(.*?)((?:\s+#+\s*)?)$/,
      replacement: '<span class="TMMark TMMark_TMH2">$1</span>$$2<span class="TMMark TMMark_TMH2">$3</span>'
    },
    TMH3: {
      regexp: /^( {0,3}###\s)(.*?)((?:\s+#+\s*)?)$/,
      replacement: '<span class="TMMark TMMark_TMH3">$1</span>$$2<span class="TMMark TMMark_TMH3">$3</span>'
    },
    TMH4: {
      regexp: /^( {0,3}####\s)(.*?)((?:\s+#+\s*)?)$/,
      replacement: '<span class="TMMark TMMark_TMH4">$1</span>$$2<span class="TMMark TMMark_TMH4">$3</span>'
    },
    TMH5: {
      regexp: /^( {0,3}#####\s)(.*?)((?:\s+#+\s*)?)$/,
      replacement: '<span class="TMMark TMMark_TMH5">$1</span>$$2<span class="TMMark TMMark_TMH5">$3</span>'
    },
    TMH6: {
      regexp: /^( {0,3}######\s)(.*?)((?:\s+#+\s*)?)$/,
      replacement: '<span class="TMMark TMMark_TMH6">$1</span>$$2<span class="TMMark TMMark_TMH6">$3</span>'
    },
    TMBlockquote: {
      regexp: /^( {0,3}>[ ]?)(.*)$/,
      replacement: '<span class="TMMark TMMark_TMBlockquote">$1</span>$$2'
    },
    TMCodeFenceBacktickOpen: {
      regexp: /^( {0,3}(?<seq>````*)\s*)([^`]*?)(\s*)$/,
      replacement: '<span class="TMMark TMMark_TMCodeFenceBacktick">$1</span><span class="TMInfoString">$3</span>$4'
    },
    TMCodeFenceTildeOpen: {
      regexp: /^( {0,3}(?<seq>~~~~*)\s*)(.*?)(\s*)$/,
      replacement: '<span class="TMMark TMMark_TMCodeFenceTilde">$1</span><span class="TMInfoString">$3</span>$4'
    },
    TMCodeFenceBacktickClose: {
      regexp: /^( {0,3}(?<seq>````*))(\s*)$/,
      replacement: '<span class="TMMark TMMark_TMCodeFenceBacktick">$1</span>$3'
    },
    TMCodeFenceTildeClose: {
      regexp: /^( {0,3}(?<seq>~~~~*))(\s*)$/,
      replacement: '<span class="TMMark TMMark_TMCodeFenceTilde">$1</span>$3'
    },
    TMBlankLine: {
      regexp: /^([ \t]*)$/,
      replacement: '$0'
    },
    TMSetextH1Marker: {
      regexp: /^ {0,3}=+\s*$/,
      replacement: '<span class="TMMark TMMark_TMSetextH1Marker">$0</span>'
    },
    TMSetextH2Marker: {
      regexp: /^ {0,3}-+\s*$/,
      replacement: '<span class="TMMark TMMark_TMSetextH1Marker">$0</span>'
    },
    TMHR: {
      regexp: /^( {0,3}(\*[ \t]*\*[ \t]*\*[ \t*]*)|(-[ \t]*-[ \t]*-[ \t-]*)|(_[ \t]*_[ \t]*_[ \t_]*))$/,
      replacement: '<span class="TMMark TMMark_TMHR">$0</span>'
    },
    TMUL: {
      regexp: /^( {0,3}[+*-] {1,4})(.*)$/,
      replacement: '<span class="TMMark TMMark_TMUL">$1</span>$$2'
    },
    TMOL: {
      regexp: /^( {0,3}\d{1,9}[.)] {1,4})(.*)$/,
      replacement: '<span class="TMMark TMMark_TMOL">$1</span>$$2'
    },
    // TODO: This is currently preventing sublists (and any content within list items, really) from working
    TMIndentedCode: {
      regexp: /^( {4}|\t)(.*)$/,
      replacement: '<span class="TMMark TMMark_TMIndentedCode">$1</span>$2'
    },
    TMLinkReferenceDefinition: {
      // TODO: Link destination can't include unbalanced parantheses, but we just ignore that here 
      regexp: /^( {0,3}\[\s*)([^\s\]](?:[^\]]|\\\])*?)(\s*\]:\s*)((?:[^\s<>]+)|(?:<(?:[^<>\\]|\\.)*>))?(\s*)((?:\((?:[^()\\]|\\.)*\))|(?:"(?:[^"\\]|\\.)*")|(?:'(?:[^'\\]|\\.)*'))?(\s*)$/,
      replacement: '<span class="TMMark TMMark_TMLinkReferenceDefinition">$1</span><span class="TMLinkLabel TMLinkLabel_Definition">$2</span><span class="TMMark TMMark_TMLinkReferenceDefinition">$3</span><span class="TMLinkDestination">$4</span>$5<span class="TMLinkTitle">$6</span>$7',
      labelPlaceholder: 2 // this defines which placeholder in the above regex is the link "label"
    }
  };

  /**
   * HTML blocks have multiple different classes of opener and closer. This array defines all the cases
   */
  var htmlBlockGrammar = [{
    start: /^ {0,3}<(?:script|pre|style)(?:\s|>|$)/i,
    end: /(?:<\/script>|<\/pre>|<\/style>)/i,
    paraInterrupt: true
  }, {
    start: /^ {0,3}<!--/,
    end: /-->/,
    paraInterrupt: true
  }, {
    start: /^ {0,3}<\?/,
    end: /\?>/,
    paraInterrupt: true
  }, {
    start: /^ {0,3}<![A-Z]/,
    end: />/,
    paraInterrupt: true
  }, {
    start: /^ {0,3}<!\[CDATA\[/,
    end: /\]\]>/,
    paraInterrupt: true
  }, {
    start: /^ {0,3}(?:<|<\/)(?:KnownTag)(?:\s|>|\/>|$)/i,
    end: false,
    paraInterrupt: true
  }, {
    start: /^ {0,3}(?:HTMLOpenTag|HTMLCloseTag)\s*$/,
    end: false,
    paraInterrupt: false
  }];

  /**
   * Structure of the object:
   * Top level entries are rules, each consisting of a regular expressions (in string format) as well as a replacement.
   * In the regular expressions, replacements from the object 'replacements' will be processed before compiling into the property regexp.
   */
  var inlineGrammar = {
    escape: {
      regexp: /^\\(ASCIIPunctuation)/,
      replacement: '<span class="TMMark TMMark_TMEscape">\\</span>$1'
    },
    code: {
      regexp: /^(`+)((?:[^`])|(?:[^`].*?[^`]))(\1)/,
      replacement: '<span class="TMMark TMMark_TMCode">$1</span><code class="TMCode">$2</code><span class="TMMark TMMark_TMCode">$3</span>'
    },
    autolink: {
      regexp: /^<((?:Scheme:[^\s<>]*)|(?:Email))>/,
      replacement: '<span class="TMMark TMMark_TMAutolink">&lt;</span><span class="TMAutolink">$1</span><span class="TMMark TMMark_TMAutolink">&gt;</span>'
    },
    html: {
      regexp: /^((?:HTMLOpenTag)|(?:HTMLCloseTag)|(?:HTMLComment)|(?:HTMLPI)|(?:HTMLDeclaration)|(?:HTMLCDATA))/,
      replacement: '<span class="TMHTML">$1</span>'
    },
    linkOpen: {
      regexp: /^\[/,
      replacement: ''
    },
    imageOpen: {
      regexp: /^!\[/,
      replacement: ''
    },
    linkLabel: {
      regexp: /^(\[\s*)([^\]]*?)(\s*\])/,
      replacement: '',
      labelPlaceholder: 2
    },
    default: {
      regexp: /^(.|(?:NotTriggerChar+))/,
      replacement: '$1'
    }
  };

  // Process replacements in regexps
  const replacementRegexp = new RegExp(Object.keys(replacements).join('|'));

  // Inline
  const inlineRules = [...Object.keys(inlineGrammar)];
  for (let rule of inlineRules) {
    let re = inlineGrammar[rule].regexp.source;
    // Replace while there is something to replace. This means it also works over multiple levels (replacements containing replacements)
    while (re.match(replacementRegexp)) {
      re = re.replace(replacementRegexp, string => {
        return replacements[string].source;
      });
    }
    inlineGrammar[rule].regexp = new RegExp(re, inlineGrammar[rule].regexp.flags);
  }

  // HTML Block (only opening rule is processed currently)
  for (let rule of htmlBlockGrammar) {
    let re = rule.start.source;
    // Replace while there is something to replace. This means it also works over multiple levels (replacements containing replacements)
    while (re.match(replacementRegexp)) {
      re = re.replace(replacementRegexp, string => {
        return replacements[string].source;
      });
    }
    rule.start = new RegExp(re, rule.start.flags);
  }

  /**
   * Escapes HTML special characters (<, >, and &) in the string.
   * @param {string} string The raw string to be escaped
   * @returns {string} The string, ready to be used in HTML
   */
  function htmlescape(string) {
    return (string ? string : '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }
  /**
   * Contains the commands that can be sent to the editor. Contains objects with a name representing the name of the command.
   * Each of the objects contains the following keys:
   * 
   *   - type: Can be either inline (for inline formatting) or line (for block / line formatting).
   *   - className: Used to determine whether the command is active at a given position. 
   *     For line formatting, this looks at the class of the line element. For inline elements, tries to find an enclosing element with that class.
   *   - set / unset: Contain instructions how to set and unset the command. For line type commands, both consist of a pattern and replacement that 
   *     will be applied to each line (using String.replace). For inline type commands, the set object contains a pre and post string which will
   *     be inserted before and after the selection. The unset object contains a prePattern and a postPattern. Both should be regular expressions and 
   *     they will be applied to the portion of the line before and after the selection (using String.replace, with an empty replacement string).
   */
  const commands = {
    // Replacements for unset for inline commands are '' by default
    bold: {
      type: 'inline',
      className: 'TMStrong',
      set: {
        pre: '**',
        post: '**'
      },
      unset: {
        prePattern: /(?:\*\*|__)$/,
        postPattern: /^(?:\*\*|__)/
      }
    },
    italic: {
      type: 'inline',
      className: 'TMEm',
      set: {
        pre: '*',
        post: '*'
      },
      unset: {
        prePattern: /(?:\*|_)$/,
        postPattern: /^(?:\*|_)/
      }
    },
    code: {
      type: 'inline',
      className: 'TMCode',
      set: {
        pre: '`',
        post: '`'
      },
      unset: {
        prePattern: /`+$/,
        postPattern: /^`+/
      } // FIXME this doesn't ensure balanced backticks right now
    },

    strikethrough: {
      type: 'inline',
      className: 'TMStrikethrough',
      set: {
        pre: '~~',
        post: '~~'
      },
      unset: {
        prePattern: /~~$/,
        postPattern: /^~~/
      }
    },
    h1: {
      type: 'line',
      className: 'TMH1',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '# $2'
      },
      unset: {
        pattern: /^( {0,3}#\s+)(.*?)((?:\s+#+\s*)?)$/,
        replacement: '$2'
      }
    },
    h2: {
      type: 'line',
      className: 'TMH2',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '## $2'
      },
      unset: {
        pattern: /^( {0,3}##\s+)(.*?)((?:\s+#+\s*)?)$/,
        replacement: '$2'
      }
    },
    h3: {
      type: 'line',
      className: 'TMH3',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '### $2'
      },
      unset: {
        pattern: /^( {0,3}###\s+)(.*?)((?:\s+#+\s*)?)$/,
        replacement: '$2'
      }
    },
    h4: {
      type: 'line',
      className: 'TMH4',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '#### $2'
      },
      unset: {
        pattern: /^( {0,3}####\s+)(.*?)((?:\s+#+\s*)?)$/,
        replacement: '$2'
      }
    },
    h5: {
      type: 'line',
      className: 'TMH5',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '##### $2'
      },
      unset: {
        pattern: /^( {0,3}#####\s+)(.*?)((?:\s+#+\s*)?)$/,
        replacement: '$2'
      }
    },
    h6: {
      type: 'line',
      className: 'TMH6',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '###### $2'
      },
      unset: {
        pattern: /^( {0,3}######\s+)(.*?)((?:\s+#+\s*)?)$/,
        replacement: '$2'
      }
    },
    ul: {
      type: 'line',
      className: 'TMUL',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '- $2'
      },
      unset: {
        pattern: /^( {0,3}[+*-] {1,4})(.*)$/,
        replacement: '$2'
      }
    },
    ol: {
      type: 'line',
      className: 'TMOL',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '$#. $2'
      },
      unset: {
        pattern: /^( {0,3}\d{1,9}[.)] {1,4})(.*)$/,
        replacement: '$2'
      }
    },
    blockquote: {
      type: 'line',
      className: 'TMBlockquote',
      set: {
        pattern: /^( {0,3}(?:(?:#+|[0-9]{1,9}[).]|[>\-*+])\s+)?)(.*)$/,
        replacement: '> $2'
      },
      unset: {
        pattern: /^( {0,3}>[ ]?)(.*)$/,
        replacement: '$2'
      }
    }
  };

  class Editor {
    constructor() {
      let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
      this.e = null;
      this.textarea = null;
      this.lines = [];
      this.lineElements = [];
      this.lineTypes = [];
      this.lineCaptures = [];
      this.lineReplacements = [];
      this.linkLabels = [];
      this.lineDirty = [];
      this.lastCommandState = null;
      this.listeners = {
        change: [],
        selection: []
      };
      let element = props.element;
      this.textarea = props.textarea;
      if (this.textarea) {
        if (!this.textarea.tagName) {
          this.textarea = document.getElementById(this.textarea);
        }
        if (!element) element = this.textarea;
      }
      if (element && !element.tagName) {
        element = document.getElementById(props.element);
      }
      if (!element) {
        element = document.getElementsByTagName("body")[0];
      }
      if (element.tagName == "TEXTAREA") {
        this.textarea = element;
        element = this.textarea.parentNode;
      }
      if (this.textarea) {
        this.textarea.style.display = "none";
      }
      this.createEditorElement(element);
      // TODO Placeholder for empty content
      this.setContent(props.content || (this.textarea ? this.textarea.value : "# Hello TinyMDE!\nEdit **here**"));
    }

    /**
     * Creates the editor element inside the target element of the DOM tree
     * @param element The target element of the DOM tree
     */
    createEditorElement(element) {
      this.e = document.createElement("div");
      this.e.className = "TinyMDE";
      this.e.contentEditable = true;
      // The following is important for formatting purposes, but also since otherwise the browser replaces subsequent spaces with  &nbsp; &nbsp;
      // That breaks a lot of stuff, so we do this here and not in CSS—therefore, you don't have to remember to put this in the CSS file
      this.e.style.whiteSpace = "pre-wrap";
      // Avoid formatting (B / I / U) popping up on iOS
      this.e.style.webkitUserModify = "read-write-plaintext-only";
      if (this.textarea && this.textarea.parentNode == element && this.textarea.nextSibling) {
        element.insertBefore(this.e, this.textarea.nextSibling);
      } else {
        element.appendChild(this.e);
      }
      this.e.addEventListener("input", e => this.handleInputEvent(e));
      this.e.addEventListener("compositionend", e => this.handleInputEvent(e));
      document.addEventListener("selectionchange", e => this.handleSelectionChangeEvent(e));
      this.e.addEventListener("paste", e => this.handlePaste(e));
      this.lineElements = this.e.childNodes; // this will automatically update
    }

    /**
     * Sets the editor content.
     * @param {string} content The new Markdown content
     */
    setContent(content) {
      // Delete any existing content
      while (this.e.firstChild) {
        this.e.removeChild(this.e.firstChild);
      }
      this.lines = content.split(/(?:\r\n|\r|\n)/);
      this.lineDirty = [];
      for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
        let le = document.createElement("div");
        this.e.appendChild(le);
        this.lineDirty.push(true);
      }
      this.lineTypes = new Array(this.lines.length);
      this.updateFormatting();
      this.fireChange();
    }

    /**
     * Gets the editor content as a Markdown string.
     * @returns {string} The editor content as a markdown string
     */
    getContent() {
      return this.lines.join("\n");
    }

    /**
     * This is the main method to update the formatting (from this.lines to HTML output)
     */
    updateFormatting() {
      // First, parse line types. This will update this.lineTypes, this.lineReplacements, and this.lineCaptures
      // We don't apply the formatting yet
      this.updateLineTypes();
      // Collect any valid link labels from link reference definitions—we need that for formatting to determine what's a valid link
      this.updateLinkLabels();
      // Now, apply the formatting
      this.applyLineTypes();
    }

    /**
     * Updates this.linkLabels: For every link reference definition (line type TMLinkReferenceDefinition), we collect the label
     */
    updateLinkLabels() {
      this.linkLabels = [];
      for (let l = 0; l < this.lines.length; l++) {
        if (this.lineTypes[l] == "TMLinkReferenceDefinition") {
          this.linkLabels.push(this.lineCaptures[l][lineGrammar.TMLinkReferenceDefinition.labelPlaceholder]);
        }
      }
    }

    /**
     * Helper function to replace placeholders from a RegExp capture. The replacement string can contain regular dollar placeholders (e.g., $1),
     * which are interpreted like in String.replace(), but also double dollar placeholders ($$1). In the case of double dollar placeholders,
     * Markdown inline grammar is applied on the content of the captured subgroup, i.e., $$1 processes inline Markdown grammar in the content of the
     * first captured subgroup, and replaces `$$1` with the result.
     *
     * @param {string} replacement The replacement string, including placeholders.
     * @param  capture The result of a RegExp.exec() call
     * @returns The replacement string, with placeholders replaced from the capture result.
     */
    replace(replacement, capture) {
      return replacement.replace(/(\${1,2})([0-9])/g, (str, p1, p2) => {
        if (p1 == "$") return htmlescape(capture[p2]);else return `<span class="TMInlineFormatted">${this.processInlineStyles(capture[p2])}</span>`;
      });
    }

    /**
     * Applies the line types (from this.lineTypes as well as the capture result in this.lineReplacements and this.lineCaptures)
     * and processes inline formatting for all lines.
     */
    applyLineTypes() {
      for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
        if (this.lineDirty[lineNum]) {
          let contentHTML = this.replace(this.lineReplacements[lineNum], this.lineCaptures[lineNum]);
          // this.lineHTML[lineNum] = (contentHTML == '' ? '<br />' : contentHTML); // Prevent empty elements which can't be selected etc.
          this.lineElements[lineNum].className = this.lineTypes[lineNum];
          this.lineElements[lineNum].removeAttribute("style");
          this.lineElements[lineNum].innerHTML = contentHTML == "" ? "<br />" : contentHTML; // Prevent empty elements which can't be selected etc.
        }

        this.lineElements[lineNum].dataset.lineNum = lineNum;
      }
    }

    /**
     * Determines line types for all lines based on the line / block grammar. Captures the results of the respective line
     * grammar regular expressions.
     * Updates this.lineTypes, this.lineCaptures, and this.lineReplacements.
     */
    updateLineTypes() {
      let codeBlockType = false;
      let codeBlockSeqLength = 0;
      let htmlBlock = false;
      for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
        let lineType = "TMPara";
        let lineCapture = [this.lines[lineNum]];
        let lineReplacement = "$$0"; // Default replacement for paragraph: Inline format the entire line

        // Check ongoing code blocks
        // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceBacktickOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeBacktick')) {
        if (codeBlockType == "TMCodeFenceBacktickOpen") {
          // We're in a backtick-fenced code block, check if the current line closes it
          let capture = lineGrammar.TMCodeFenceBacktickClose.regexp.exec(this.lines[lineNum]);
          if (capture && capture.groups["seq"].length >= codeBlockSeqLength) {
            lineType = "TMCodeFenceBacktickClose";
            lineReplacement = lineGrammar.TMCodeFenceBacktickClose.replacement;
            lineCapture = capture;
            codeBlockType = false;
          } else {
            lineType = "TMFencedCodeBacktick";
            lineReplacement = '<span class="TMFencedCode">$0<br /></span>';
            lineCapture = [this.lines[lineNum]];
          }
        }
        // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceTildeOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeTilde')) {
        else if (codeBlockType == "TMCodeFenceTildeOpen") {
          // We're in a tilde-fenced code block
          let capture = lineGrammar.TMCodeFenceTildeClose.regexp.exec(this.lines[lineNum]);
          if (capture && capture.groups["seq"].length >= codeBlockSeqLength) {
            lineType = "TMCodeFenceTildeClose";
            lineReplacement = lineGrammar.TMCodeFenceTildeClose.replacement;
            lineCapture = capture;
            codeBlockType = false;
          } else {
            lineType = "TMFencedCodeTilde";
            lineReplacement = '<span class="TMFencedCode">$0<br /></span>';
            lineCapture = [this.lines[lineNum]];
          }
        }

        // Check HTML block types
        if (lineType == "TMPara" && htmlBlock === false) {
          for (let htmlBlockType of htmlBlockGrammar) {
            if (this.lines[lineNum].match(htmlBlockType.start)) {
              // Matching start condition. Check if this tag can start here (not all start conditions allow breaking a paragraph).
              if (htmlBlockType.paraInterrupt || lineNum == 0 || !(this.lineTypes[lineNum - 1] == "TMPara" || this.lineTypes[lineNum - 1] == "TMUL" || this.lineTypes[lineNum - 1] == "TMOL" || this.lineTypes[lineNum - 1] == "TMBlockquote")) {
                htmlBlock = htmlBlockType;
                break;
              }
            }
          }
        }
        if (htmlBlock !== false) {
          lineType = "TMHTMLBlock";
          lineReplacement = '<span class="TMHTMLContent">$0<br /></span>'; // No formatting in TMHTMLBlock
          lineCapture = [this.lines[lineNum]]; // This should already be set but better safe than sorry

          // Check if HTML block should be closed
          if (htmlBlock.end) {
            // Specific end condition
            if (this.lines[lineNum].match(htmlBlock.end)) {
              htmlBlock = false;
            }
          } else {
            // No specific end condition, ends with blank line
            if (lineNum == this.lines.length - 1 || this.lines[lineNum + 1].match(lineGrammar.TMBlankLine.regexp)) {
              htmlBlock = false;
            }
          }
        }

        // Check all regexps if we haven't applied one of the code block types
        if (lineType == "TMPara") {
          for (let type in lineGrammar) {
            if (lineGrammar[type].regexp) {
              let capture = lineGrammar[type].regexp.exec(this.lines[lineNum]);
              if (capture) {
                lineType = type;
                lineReplacement = lineGrammar[type].replacement;
                lineCapture = capture;
                break;
              }
            }
          }
        }

        // If we've opened a code block, remember that
        if (lineType == "TMCodeFenceBacktickOpen" || lineType == "TMCodeFenceTildeOpen") {
          codeBlockType = lineType;
          codeBlockSeqLength = lineCapture.groups["seq"].length;
        }

        // Link reference definition and indented code can't interrupt a paragraph
        if ((lineType == "TMIndentedCode" || lineType == "TMLinkReferenceDefinition") && lineNum > 0 && (this.lineTypes[lineNum - 1] == "TMPara" || this.lineTypes[lineNum - 1] == "TMUL" || this.lineTypes[lineNum - 1] == "TMOL" || this.lineTypes[lineNum - 1] == "TMBlockquote")) {
          // Fall back to TMPara
          lineType = "TMPara";
          lineCapture = [this.lines[lineNum]];
          lineReplacement = "$$0";
        }

        // Setext H2 markers that can also be interpreted as an empty list item should be regarded as such (as per CommonMark spec)
        if (lineType == "TMSetextH2Marker") {
          let capture = lineGrammar.TMUL.regexp.exec(this.lines[lineNum]);
          if (capture) {
            lineType = "TMUL";
            lineReplacement = lineGrammar.TMUL.replacement;
            lineCapture = capture;
          }
        }

        // Setext headings are only valid if preceded by a paragraph (and if so, they change the type of the previous paragraph)
        if (lineType == "TMSetextH1Marker" || lineType == "TMSetextH2Marker") {
          if (lineNum == 0 || this.lineTypes[lineNum - 1] != "TMPara") {
            // Setext marker is invalid. However, a H2 marker might still be a valid HR, so let's check that
            let capture = lineGrammar.TMHR.regexp.exec(this.lines[lineNum]);
            if (capture) {
              // Valid HR
              lineType = "TMHR";
              lineCapture = capture;
              lineReplacement = lineGrammar.TMHR.replacement;
            } else {
              // Not valid HR, format as TMPara
              lineType = "TMPara";
              lineCapture = [this.lines[lineNum]];
              lineReplacement = "$$0";
            }
          } else {
            // Valid setext marker. Change types of preceding para lines
            let headingLine = lineNum - 1;
            const headingLineType = lineType == "TMSetextH1Marker" ? "TMSetextH1" : "TMSetextH2";
            do {
              if (this.lineTypes[headingLineType] != headingLineType) {
                this.lineTypes[headingLine] = headingLineType;
                this.lineDirty[headingLineType] = true;
              }
              this.lineReplacements[headingLine] = "$$0";
              this.lineCaptures[headingLine] = [this.lines[headingLine]];
              headingLine--;
            } while (headingLine >= 0 && this.lineTypes[headingLine] == "TMPara");
          }
        }
        // Lastly, save the line style to be applied later
        if (this.lineTypes[lineNum] != lineType) {
          this.lineTypes[lineNum] = lineType;
          this.lineDirty[lineNum] = true;
        }
        this.lineReplacements[lineNum] = lineReplacement;
        this.lineCaptures[lineNum] = lineCapture;
      }
    }

    /**
     * Updates all line contents from the HTML, then re-applies formatting.
     */
    updateLineContentsAndFormatting() {
      this.clearDirtyFlag();
      this.updateLineContents();
      this.updateFormatting();
    }

    /**
     * Attempts to parse a link or image at the current position. This assumes that the opening [ or ![ has already been matched.
     * Returns false if this is not a valid link, image. See below for more information
     * @param {string} originalString The original string, starting at the opening marker ([ or ![)
     * @param {boolean} isImage Whether or not this is an image (opener == ![)
     * @returns false if not a valid link / image.
     * Otherwise returns an object with two properties: output is the string to be included in the processed output,
     * charCount is the number of input characters (from originalString) consumed.
     */
    parseLinkOrImage(originalString, isImage) {
      // Skip the opening bracket
      let textOffset = isImage ? 2 : 1;
      let opener = originalString.substr(0, textOffset);
      let type = isImage ? "TMImage" : "TMLink";
      let currentOffset = textOffset;
      let bracketLevel = 1;
      let linkText = false;
      let linkRef = false;
      let linkLabel = [];
      let linkDetails = []; // If matched, this will be an array: [whitespace + link destination delimiter, link destination, link destination delimiter, whitespace, link title delimiter, link title, link title delimiter + whitespace]. All can be empty strings.

      textOuter: while (currentOffset < originalString.length && linkText === false /* empty string is okay */) {
        let string = originalString.substr(currentOffset);

        // Capture any escapes and code blocks at current position, they bind more strongly than links
        // We don't have to actually process them here, that'll be done later in case the link / image is valid, but we need to skip over them.
        for (let rule of ["escape", "code", "autolink", "html"]) {
          let cap = inlineGrammar[rule].regexp.exec(string);
          if (cap) {
            currentOffset += cap[0].length;
            continue textOuter;
          }
        }

        // Check for image. It's okay for an image to be included in a link or image
        if (string.match(inlineGrammar.imageOpen.regexp)) {
          // Opening image. It's okay if this is a matching pair of brackets
          bracketLevel++;
          currentOffset += 2;
          continue textOuter;
        }

        // Check for link (not an image because that would have been captured and skipped over above)
        if (string.match(inlineGrammar.linkOpen.regexp)) {
          // Opening bracket. Two things to do:
          // 1) it's okay if this part of a pair of brackets.
          // 2) If we are currently trying to parse a link, this nested bracket musn't start a valid link (no nested links allowed)
          bracketLevel++;
          // if (bracketLevel >= 2) return false; // Nested unescaped brackets, this doesn't qualify as a link / image
          if (!isImage) {
            if (this.parseLinkOrImage(string, false)) {
              // Valid link inside this possible link, which makes this link invalid (inner links beat outer ones)
              return false;
            }
          }
          currentOffset += 1;
          continue textOuter;
        }

        // Check for closing bracket
        if (string.match(/^\]/)) {
          bracketLevel--;
          if (bracketLevel == 0) {
            // Found matching bracket and haven't found anything disqualifying this as link / image.
            linkText = originalString.substr(textOffset, currentOffset - textOffset);
            currentOffset++;
            continue textOuter;
          }
        }

        // Nothing matches, proceed to next char
        currentOffset++;
      }

      // Did we find a link text (i.e., find a matching closing bracket?)
      if (linkText === false) return false; // Nope

      // So far, so good. We've got a valid link text. Let's see what type of link this is
      let nextChar = currentOffset < originalString.length ? originalString.substr(currentOffset, 1) : "";

      // REFERENCE LINKS
      if (nextChar == "[") {
        let string = originalString.substr(currentOffset);
        let cap = inlineGrammar.linkLabel.regexp.exec(string);
        if (cap) {
          // Valid link label
          currentOffset += cap[0].length;
          linkLabel.push(cap[1], cap[2], cap[3]);
          if (cap[inlineGrammar.linkLabel.labelPlaceholder]) {
            // Full reference link
            linkRef = cap[inlineGrammar.linkLabel.labelPlaceholder];
          } else {
            // Collapsed reference link
            linkRef = linkText.trim();
          }
        } else {
          // Not a valid link label
          return false;
        }
      } else if (nextChar != "(") {
        // Shortcut ref link
        linkRef = linkText.trim();

        // INLINE LINKS
      } else {
        // nextChar == '('

        // Potential inline link
        currentOffset++;
        let parenthesisLevel = 1;
        inlineOuter: while (currentOffset < originalString.length && parenthesisLevel > 0) {
          let string = originalString.substr(currentOffset);

          // Process whitespace
          let cap = /^\s+/.exec(string);
          if (cap) {
            switch (linkDetails.length) {
              case 0:
                linkDetails.push(cap[0]);
                break;
              // Opening whitespace
              case 1:
                linkDetails.push(cap[0]);
                break;
              // Open destination, but not a destination yet; desination opened with <
              case 2:
                // Open destination with content in it. Whitespace only allowed if opened by angle bracket, otherwise this closes the destination
                if (linkDetails[0].match(/</)) {
                  linkDetails[1] = linkDetails[1].concat(cap[0]);
                } else {
                  if (parenthesisLevel != 1) return false; // Unbalanced parenthesis
                  linkDetails.push(""); // Empty end delimiter for destination
                  linkDetails.push(cap[0]); // Whitespace in between destination and title
                }

                break;
              case 3:
                linkDetails.push(cap[0]);
                break;
              // Whitespace between destination and title
              case 4:
                return false;
              // This should never happen (no opener for title yet, but more whitespace to capture)
              case 5:
                linkDetails.push("");
              // Whitespace at beginning of title, push empty title and continue
              case 6:
                linkDetails[5] = linkDetails[5].concat(cap[0]);
                break;
              // Whitespace in title
              case 7:
                linkDetails[6] = linkDetails[6].concat(cap[0]);
                break;
              // Whitespace after closing delimiter
              default:
                return false;
              // We should never get here
            }

            currentOffset += cap[0].length;
            continue inlineOuter;
          }

          // Process backslash escapes
          cap = inlineGrammar.escape.regexp.exec(string);
          if (cap) {
            switch (linkDetails.length) {
              case 0:
                linkDetails.push("");
              // this opens the link destination, add empty opening delimiter and proceed to next case
              case 1:
                linkDetails.push(cap[0]);
                break;
              // This opens the link destination, append it
              case 2:
                linkDetails[1] = linkDetails[1].concat(cap[0]);
                break;
              // Part of the link destination
              case 3:
                return false;
              // Lacking opening delimiter for link title
              case 4:
                return false;
              // Lcaking opening delimiter for link title
              case 5:
                linkDetails.push("");
              // This opens the link title
              case 6:
                linkDetails[5] = linkDetails[5].concat(cap[0]);
                break;
              // Part of the link title
              default:
                return false;
              // After link title was closed, without closing parenthesis
            }

            currentOffset += cap[0].length;
            continue inlineOuter;
          }

          // Process opening angle bracket as deilimiter of destination
          if (linkDetails.length < 2 && string.match(/^</)) {
            if (linkDetails.length == 0) linkDetails.push("");
            linkDetails[0] = linkDetails[0].concat("<");
            currentOffset++;
            continue inlineOuter;
          }

          // Process closing angle bracket as delimiter of destination
          if ((linkDetails.length == 1 || linkDetails.length == 2) && string.match(/^>/)) {
            if (linkDetails.length == 1) linkDetails.push(""); // Empty link destination
            linkDetails.push(">");
            currentOffset++;
            continue inlineOuter;
          }

          // Process  non-parenthesis delimiter for title.
          cap = /^["']/.exec(string);
          // For this to be a valid opener, we have to either have no destination, only whitespace so far,
          // or a destination with trailing whitespace.
          if (cap && (linkDetails.length == 0 || linkDetails.length == 1 || linkDetails.length == 4)) {
            while (linkDetails.length < 4) linkDetails.push("");
            linkDetails.push(cap[0]);
            currentOffset++;
            continue inlineOuter;
          }

          // For this to be a valid closer, we have to have an opener and some or no title, and this has to match the opener
          if (cap && (linkDetails.length == 5 || linkDetails.length == 6) && linkDetails[4] == cap[0]) {
            if (linkDetails.length == 5) linkDetails.push(""); // Empty link title
            linkDetails.push(cap[0]);
            currentOffset++;
            continue inlineOuter;
          }
          // Other cases (linkDetails.length == 2, 3, 7) will be handled with the "default" case below.

          // Process opening parenthesis
          if (string.match(/^\(/)) {
            switch (linkDetails.length) {
              case 0:
                linkDetails.push("");
              // this opens the link destination, add empty opening delimiter and proceed to next case
              case 1:
                linkDetails.push("");
              // This opens the link destination
              case 2:
                // Part of the link destination
                linkDetails[1] = linkDetails[1].concat("(");
                if (!linkDetails[0].match(/<$/)) parenthesisLevel++;
                break;
              case 3:
                linkDetails.push("");
              //  opening delimiter for link title
              case 4:
                linkDetails.push("(");
                break;
              // opening delimiter for link title
              case 5:
                linkDetails.push("");
              // opens the link title, add empty title content and proceed to next case
              case 6:
                // Part of the link title. Un-escaped parenthesis only allowed in " or ' delimited title
                if (linkDetails[4] == "(") return false;
                linkDetails[5] = linkDetails[5].concat("(");
                break;
              default:
                return false;
              // After link title was closed, without closing parenthesis
            }

            currentOffset++;
            continue inlineOuter;
          }

          // Process closing parenthesis
          if (string.match(/^\)/)) {
            if (linkDetails.length <= 2) {
              // We are inside the link destination. Parentheses have to be matched if not in angle brackets
              while (linkDetails.length < 2) linkDetails.push("");
              if (!linkDetails[0].match(/<$/)) parenthesisLevel--;
              if (parenthesisLevel > 0) {
                linkDetails[1] = linkDetails[1].concat(")");
              }
            } else if (linkDetails.length == 5 || linkDetails.length == 6) {
              // We are inside the link title.
              if (linkDetails[4] == "(") {
                // This closes the link title
                if (linkDetails.length == 5) linkDetails.push("");
                linkDetails.push(")");
              } else {
                // Just regular ol' content
                if (linkDetails.length == 5) linkDetails.push(")");else linkDetails[5] = linkDetails[5].concat(")");
              }
            } else {
              parenthesisLevel--; // This should decrease it from 1 to 0...
            }

            if (parenthesisLevel == 0) {
              // No invalid condition, let's make sure the linkDetails array is complete
              while (linkDetails.length < 7) linkDetails.push("");
            }
            currentOffset++;
            continue inlineOuter;
          }

          // Any old character
          cap = /^./.exec(string);
          if (cap) {
            switch (linkDetails.length) {
              case 0:
                linkDetails.push("");
              // this opens the link destination, add empty opening delimiter and proceed to next case
              case 1:
                linkDetails.push(cap[0]);
                break;
              // This opens the link destination, append it
              case 2:
                linkDetails[1] = linkDetails[1].concat(cap[0]);
                break;
              // Part of the link destination
              case 3:
                return false;
              // Lacking opening delimiter for link title
              case 4:
                return false;
              // Lcaking opening delimiter for link title
              case 5:
                linkDetails.push("");
              // This opens the link title
              case 6:
                linkDetails[5] = linkDetails[5].concat(cap[0]);
                break;
              // Part of the link title
              default:
                return false;
              // After link title was closed, without closing parenthesis
            }

            currentOffset += cap[0].length;
            continue inlineOuter;
          }
          throw "Infinite loop"; // we should never get here since the last test matches any character
        }

        if (parenthesisLevel > 0) return false; // Parenthes(es) not closed
      }

      if (linkRef !== false) {
        // Ref link; check that linkRef is valid
        let valid = false;
        for (let label of this.linkLabels) {
          if (label == linkRef) {
            valid = true;
            break;
          }
        }
        let label = valid ? "TMLinkLabel TMLinkLabel_Valid" : "TMLinkLabel TMLinkLabel_Invalid";
        let output = `<span class="TMMark TMMark_${type}">${opener}</span><span class="${type} ${linkLabel.length < 3 || !linkLabel[1] ? label : ""}">${this.processInlineStyles(linkText)}</span><span class="TMMark TMMark_${type}">]</span>`;
        if (linkLabel.length >= 3) {
          output = output.concat(`<span class="TMMark TMMark_${type}">${linkLabel[0]}</span>`, `<span class="${label}">${linkLabel[1]}</span>`, `<span class="TMMark TMMark_${type}">${linkLabel[2]}</span>`);
        }
        return {
          output: output,
          charCount: currentOffset
        };
      } else if (linkDetails) {
        // Inline link

        // This should never happen, but better safe than sorry.
        while (linkDetails.length < 7) {
          linkDetails.push("");
        }
        return {
          output: `<span class="TMMark TMMark_${type}">${opener}</span><span class="${type}">${this.processInlineStyles(linkText)}</span><span class="TMMark TMMark_${type}">](${linkDetails[0]}</span><span class="${type}Destination">${linkDetails[1]}</span><span class="TMMark TMMark_${type}">${linkDetails[2]}${linkDetails[3]}${linkDetails[4]}</span><span class="${type}Title">${linkDetails[5]}</span><span class="TMMark TMMark_${type}">${linkDetails[6]})</span>`,
          charCount: currentOffset
        };
      }
      return false;
    }

    /**
     * Formats a markdown string as HTML, using Markdown inline formatting.
     * @param {string} originalString The input (markdown inline formatted) string
     * @returns {string} The HTML formatted output
     */
    processInlineStyles(originalString) {
      let processed = "";
      let stack = []; // Stack is an array of objects of the format: {delimiter, delimString, count, output}
      let offset = 0;
      let string = originalString;
      outer: while (string) {
        // Process simple rules (non-delimiter)
        for (let rule of ["escape", "code", "autolink", "html"]) {
          let cap = inlineGrammar[rule].regexp.exec(string);
          if (cap) {
            string = string.substr(cap[0].length);
            offset += cap[0].length;
            processed += inlineGrammar[rule].replacement
            // .replace(/\$\$([1-9])/g, (str, p1) => processInlineStyles(cap[p1])) // todo recursive calling
            .replace(/\$([1-9])/g, (str, p1) => htmlescape(cap[p1]));
            continue outer;
          }
        }

        // Check for links / images
        let potentialLink = string.match(inlineGrammar.linkOpen.regexp);
        let potentialImage = string.match(inlineGrammar.imageOpen.regexp);
        if (potentialImage || potentialLink) {
          let result = this.parseLinkOrImage(string, potentialImage);
          if (result) {
            processed = `${processed}${result.output}`;
            string = string.substr(result.charCount);
            offset += result.charCount;
            continue outer;
          }
        }

        // Check for em / strong delimiters
        let cap = /(^\*+)|(^_+)/.exec(string);
        if (cap) {
          let delimCount = cap[0].length;
          const delimString = cap[0];
          const currentDelimiter = cap[0][0]; // This should be * or _

          string = string.substr(cap[0].length);

          // We have a delimiter run. Let's check if it can open or close an emphasis.

          const preceding = offset > 0 ? originalString.substr(0, offset) : " "; // beginning and end of line count as whitespace
          const following = offset + cap[0].length < originalString.length ? string : " ";
          const punctuationFollows = following.match(punctuationLeading);
          const punctuationPrecedes = preceding.match(punctuationTrailing);
          const whitespaceFollows = following.match(/^\s/);
          const whitespacePrecedes = preceding.match(/\s$/);

          // These are the rules for right-flanking and left-flanking delimiter runs as per CommonMark spec
          let canOpen = !whitespaceFollows && (!punctuationFollows || !!whitespacePrecedes || !!punctuationPrecedes);
          let canClose = !whitespacePrecedes && (!punctuationPrecedes || !!whitespaceFollows || !!punctuationFollows);

          // Underscores have more detailed rules than just being part of left- or right-flanking run:
          if (currentDelimiter == "_" && canOpen && canClose) {
            canOpen = punctuationPrecedes;
            canClose = punctuationFollows;
          }

          // If the delimiter can close, check the stack if there's something it can close
          if (canClose) {
            let stackPointer = stack.length - 1;
            // See if we can find a matching opening delimiter, move down through the stack
            while (delimCount && stackPointer >= 0) {
              if (stack[stackPointer].delimiter == currentDelimiter) {
                // We found a matching delimiter, let's construct the formatted string

                // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
                while (stackPointer < stack.length - 1) {
                  const entry = stack.pop();
                  processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
                }

                // Then, format the string
                if (delimCount >= 2 && stack[stackPointer].count >= 2) {
                  // Strong
                  processed = `<span class="TMMark">${currentDelimiter}${currentDelimiter}</span><strong class="TMStrong">${processed}</strong><span class="TMMark">${currentDelimiter}${currentDelimiter}</span>`;
                  delimCount -= 2;
                  stack[stackPointer].count -= 2;
                } else {
                  // Em
                  processed = `<span class="TMMark">${currentDelimiter}</span><em class="TMEm">${processed}</em><span class="TMMark">${currentDelimiter}</span>`;
                  delimCount -= 1;
                  stack[stackPointer].count -= 1;
                }

                // If that stack level is empty now, pop it
                if (stack[stackPointer].count == 0) {
                  let entry = stack.pop();
                  processed = `${entry.output}${processed}`;
                  stackPointer--;
                }
              } else {
                // This stack level's delimiter type doesn't match the current delimiter type
                // Go down one level in the stack
                stackPointer--;
              }
            }
          }
          // If there are still delimiters left, and the delimiter run can open, push it on the stack
          if (delimCount && canOpen) {
            stack.push({
              delimiter: currentDelimiter,
              delimString: delimString,
              count: delimCount,
              output: processed
            });
            processed = ""; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
            delimCount = 0;
          }

          // Any delimiters that are left (closing unmatched) are appended to the output.
          if (delimCount) {
            processed = `${processed}${delimString.substr(0, delimCount)}`;
          }
          offset += cap[0].length;
          continue outer;
        }

        // Check for strikethrough delimiter
        cap = /^~~/.exec(string);
        if (cap) {
          let consumed = false;
          let stackPointer = stack.length - 1;
          // See if we can find a matching opening delimiter, move down through the stack
          while (!consumed && stackPointer >= 0) {
            if (stack[stackPointer].delimiter == "~") {
              // We found a matching delimiter, let's construct the formatted string

              // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
              while (stackPointer < stack.length - 1) {
                const entry = stack.pop();
                processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
              }

              // Then, format the string
              processed = `<span class="TMMark">~~</span><del class="TMStrikethrough">${processed}</del><span class="TMMark">~~</span>`;
              let entry = stack.pop();
              processed = `${entry.output}${processed}`;
              consumed = true;
            } else {
              // This stack level's delimiter type doesn't match the current delimiter type
              // Go down one level in the stack
              stackPointer--;
            }
          }

          // If there are still delimiters left, and the delimiter run can open, push it on the stack
          if (!consumed) {
            stack.push({
              delimiter: "~",
              delimString: "~~",
              count: 2,
              output: processed
            });
            processed = ""; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
          }

          offset += cap[0].length;
          string = string.substr(cap[0].length);
          continue outer;
        }

        // Process 'default' rule
        cap = inlineGrammar.default.regexp.exec(string);
        if (cap) {
          string = string.substr(cap[0].length);
          offset += cap[0].length;
          processed += inlineGrammar.default.replacement.replace(/\$([1-9])/g, (str, p1) => htmlescape(cap[p1]));
          continue outer;
        }
        throw "Infinite loop!";
      }

      // Empty the stack, any opening delimiters are unused
      while (stack.length) {
        const entry = stack.pop();
        processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
      }
      return processed;
    }

    /**
     * Clears the line dirty flag (resets it to an array of false)
     */
    clearDirtyFlag() {
      this.lineDirty = new Array(this.lines.length);
      for (let i = 0; i < this.lineDirty.length; i++) {
        this.lineDirty[i] = false;
      }
    }

    /**
     * Updates the class properties (lines, lineElements) from the DOM.
     * @returns true if contents changed
     */
    updateLineContents() {
      // this.lineDirty = [];
      // Check if we have changed anything about the number of lines (inserted or deleted a paragraph)
      // < 0 means line(s) removed; > 0 means line(s) added
      let lineDelta = this.e.childElementCount - this.lines.length;
      if (lineDelta) {
        // yup. Let's try how much we can salvage (find out which lines from beginning and end were unchanged)
        // Find lines from the beginning that haven't changed...
        let firstChangedLine = 0;
        while (firstChangedLine <= this.lines.length && firstChangedLine <= this.lineElements.length && this.lineElements[firstChangedLine] &&
        // Check that the line element hasn't been deleted
        this.lines[firstChangedLine] == this.lineElements[firstChangedLine].textContent) {
          firstChangedLine++;
        }

        // End also from the end
        let lastChangedLine = -1;
        while (-lastChangedLine < this.lines.length && -lastChangedLine < this.lineElements.length && this.lines[this.lines.length + lastChangedLine] == this.lineElements[this.lineElements.length + lastChangedLine].textContent) {
          lastChangedLine--;
        }
        let linesToDelete = this.lines.length + lastChangedLine + 1 - firstChangedLine;
        if (linesToDelete < -lineDelta) linesToDelete = -lineDelta;
        if (linesToDelete < 0) linesToDelete = 0;
        let linesToAdd = [];
        for (let l = 0; l < linesToDelete + lineDelta; l++) {
          linesToAdd.push(this.lineElements[firstChangedLine + l].textContent);
        }
        this.spliceLines(firstChangedLine, linesToDelete, linesToAdd, false);
      } else {
        // No lines added or removed
        for (let line = 0; line < this.lineElements.length; line++) {
          let e = this.lineElements[line];
          let ct = e.textContent;
          if (this.lines[line] !== ct) {
            // Line changed, update it
            this.lines[line] = ct;
            this.lineDirty[line] = true;
          }
        }
      }
    }

    /**
     * Processes a new paragraph.
     * @param sel The current selection
     */
    processNewParagraph(sel) {
      if (!sel) return;

      // Update lines from content
      this.updateLineContents();
      let continuableType = false;
      // Let's see if we need to continue a list

      let checkLine = sel.col > 0 ? sel.row : sel.row - 1;
      switch (this.lineTypes[checkLine]) {
        case "TMUL":
          continuableType = "TMUL";
          break;
        case "TMOL":
          continuableType = "TMOL";
          break;
        case "TMIndentedCode":
          continuableType = "TMIndentedCode";
          break;
      }
      let lines = this.lines[sel.row].replace(/\n\n$/, "\n").split(/(?:\r\n|\n|\r)/);
      if (lines.length == 1) {
        // No new line
        this.updateFormatting();
        return;
      }
      this.spliceLines(sel.row, 1, lines, true);
      sel.row++;
      sel.col = 0;
      if (continuableType) {
        // Check if the previous line was non-empty
        let capture = lineGrammar[continuableType].regexp.exec(this.lines[sel.row - 1]);
        if (capture) {
          // Convention: capture[1] is the line type marker, capture[2] is the content
          if (capture[2]) {
            // Previous line has content, continue the continuable type

            // Hack for OL: increment number
            if (continuableType == "TMOL") {
              capture[1] = capture[1].replace(/\d{1,9}/, result => {
                return parseInt(result[0]) + 1;
              });
            }
            this.lines[sel.row] = `${capture[1]}${this.lines[sel.row]}`;
            this.lineDirty[sel.row] = true;
            sel.col = capture[1].length;
          } else {
            // Previous line has no content, remove the continuable type from the previous row
            this.lines[sel.row - 1] = "";
            this.lineDirty[sel.row - 1] = true;
          }
        }
      }
      this.updateFormatting();
    }

    // /**
    //  * Processes a "delete" input action.
    //  * @param {object} focus The selection
    //  * @param {boolean} forward If true, performs a forward delete, otherwise performs a backward delete
    //  */
    // processDelete(focus, forward) {
    //   if (!focus) return;
    //   let anchor = this.getSelection(true);
    //   // Do we have a non-empty selection?
    //   if (focus.col != anchor.col || focus.row != anchor.row) {
    //     // non-empty. direction doesn't matter.
    //     this.paste('', anchor, focus);
    //   } else {
    //     if (forward) {
    //       if (focus.col < this.lines[focus.row].length) this.paste('', {row: focus.row, col: focus.col + 1}, focus);
    //       else if (focus.col < this.lines.length) this.paste('', {row: focus.row + 1, col: 0}, focus);
    //       // Otherwise, we're at the very end and can't delete forward
    //     } else {
    //       if (focus.col > 0) this.paste('', {row: focus.row, col: focus.col - 1}, focus);
    //       else if (focus.row > 0) this.paste('', {row: focus.row - 1, col: this.lines[focus.row - 1].length - 1}, focus);
    //       // Otherwise, we're at the very beginning and can't delete backwards
    //     }
    //   }

    // }

    /**
     * Gets the current position of the selection counted by row and column of the editor Markdown content (as opposed to the position in the DOM).
     *
     * @param {boolean} getAnchor if set to true, gets the selection anchor (start point of the selection), otherwise gets the focus (end point).
     * @return {object} An object representing the selection, with properties col and row.
     */
    getSelection() {
      let getAnchor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
      const selection = window.getSelection();
      let startNode = getAnchor ? selection.anchorNode : selection.focusNode;
      if (!startNode) return null;
      let offset = getAnchor ? selection.anchorOffset : selection.focusOffset;
      if (startNode == this.e) {
        if (offset < this.lines.length) return {
          row: offset,
          col: 0
        };
        return {
          row: offset - 1,
          col: this.lines[offset - 1].length
        };
      }
      let col = this.computeColumn(startNode, offset);
      if (col === null) return null; // We are outside of the editor

      // Find the row node
      let node = startNode;
      while (node.parentElement != this.e) {
        node = node.parentElement;
      }
      let row = 0;
      // Check if we can read a line number from the data-line-num attribute.
      // The last condition is a security measure since inserting a new paragraph copies the previous rows' line number
      if (node.dataset && node.dataset.lineNum && (!node.previousSibling || node.previousSibling.dataset.lineNum != node.dataset.lineNum)) {
        row = parseInt(node.dataset.lineNum);
      } else {
        while (node.previousSibling) {
          row++;
          node = node.previousSibling;
        }
      }
      return {
        row: row,
        col: col,
        node: startNode
      };
    }

    /**
     * Computes a column within an editor line from a node and offset within that node.
     * @param {Node} startNode The node
     * @param {int} offset THe selection
     * @returns {int} the column, or null if the node is not inside the editor
     */
    computeColumn(startNode, offset) {
      let node = startNode;
      let col;
      // First, make sure we're actually in the editor.
      while (node && node.parentNode != this.e) {
        node = node.parentNode;
      }
      if (node == null) return null;

      // There are two ways that offset can be defined:
      // - Either, the node is a text node, in which case it is the offset within the text
      // - Or, the node is an element with child notes, in which case the offset refers to the
      //   child node after which the selection is located
      if (startNode.nodeType === Node.TEXT_NODE || offset === 0) {
        // In the case that the node is non-text node but the offset is 0,
        // The selection is at the beginning of that element so we
        // can simply use the same approach as if it were at the beginning
        // of a text node.
        col = offset;
        node = startNode;
      } else if (offset > 0) {
        node = startNode.childNodes[offset - 1];
        col = node.textContent.length;
      }
      while (node.parentNode != this.e) {
        if (node.previousSibling) {
          node = node.previousSibling;
          col += node.textContent.length;
        } else {
          node = node.parentNode;
        }
      }
      return col;
    }

    /**
     * Computes DOM node and offset within that node from a position expressed as row and column.
     * @param {int} row Row (line number)
     * @param {int} col Column
     * @returns An object with two properties: node and offset. offset may be null;
     */
    computeNodeAndOffset(row, col) {
      let bindRight = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
      if (row >= this.lineElements.length) {
        // Selection past the end of text, set selection to end of text
        row = this.lineElements.length - 1;
        col = this.lines[row].length;
      }
      if (col > this.lines[row].length) {
        col = this.lines[row].length;
      }
      const parentNode = this.lineElements[row];
      let node = parentNode.firstChild;
      let childrenComplete = false;
      // default return value
      let rv = {
        node: parentNode.firstChild ? parentNode.firstChild : parentNode,
        offset: 0
      };
      while (node != parentNode) {
        if (!childrenComplete && node.nodeType === Node.TEXT_NODE) {
          if (node.nodeValue.length >= col) {
            if (bindRight && node.nodeValue.length == col) {
              // Selection is at the end of this text node, but we are binding right (prefer an offset of 0 in the next text node)
              // Remember return value in case we don't find another text node
              rv = {
                node: node,
                offset: col
              };
              col = 0;
            } else {
              return {
                node: node,
                offset: col
              };
            }
          } else {
            col -= node.nodeValue.length;
          }
        }
        if (!childrenComplete && node.firstChild) {
          node = node.firstChild;
        } else if (node.nextSibling) {
          childrenComplete = false;
          node = node.nextSibling;
        } else {
          childrenComplete = true;
          node = node.parentNode;
        }
      }

      // Either, the position was invalid and we just return the default return value
      // Or we are binding right and the selection is at the end of the line
      return rv;
    }

    /**
     * Sets the selection based on rows and columns within the editor Markdown content.
     * @param {object} focus Object representing the selection, needs to have properties row and col.
     * @param anchor Anchor of the selection. If not given, assumes the current anchor.
     */
    setSelection(focus) {
      let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
      if (!focus) return;
      let range = document.createRange();
      let {
        node: focusNode,
        offset: focusOffset
      } = this.computeNodeAndOffset(focus.row, focus.col, anchor && anchor.row == focus.row && anchor.col > focus.col); // Bind selection right if anchor is in the same row and behind the focus
      let anchorNode = null,
        anchorOffset = null;
      if (anchor && (anchor.row != focus.row || anchor.col != focus.col)) {
        let {
          node,
          offset
        } = this.computeNodeAndOffset(anchor.row, anchor.col, focus.row == anchor.row && focus.col > anchor.col);
        anchorNode = node;
        anchorOffset = offset;
      }
      if (anchorNode) range.setStart(anchorNode, anchorOffset);else range.setStart(focusNode, focusOffset);
      range.setEnd(focusNode, focusOffset);
      let windowSelection = window.getSelection();
      windowSelection.removeAllRanges();
      windowSelection.addRange(range);
    }

    /**
     * Event handler for input events
     */
    handleInputEvent(event) {
      // For composition input, we are only updating the text after we have received
      // a compositionend event, so we return upon insertCompositionText.
      // Otherwise, the DOM changes break the text input.
      if (event.inputType == "insertCompositionText") return;
      let focus = this.getSelection();
      if ((event.inputType == "insertParagraph" || event.inputType == "insertLineBreak") && focus) {
        this.clearDirtyFlag();
        this.processNewParagraph(focus);
      } else {
        if (!this.e.firstChild) {
          this.e.innerHTML = '<div class="TMBlankLine"><br></div>';
        } else {
          this.fixNodeHierarchy();
        }
        this.updateLineContentsAndFormatting();
      }
      if (focus) {
        this.setSelection(focus);
      }
      this.fireChange();
    }

    /**
     * Fixes the node hierarchy – makes sure that each line is in a div, and there are no nested divs
     */
    fixNodeHierarchy() {
      const originalChildren = Array.from(this.e.childNodes);
      const replaceChild = function (child) {
        const parent = child.parentElement;
        const nextSibling = child.nextSibling;
        parent.removeChild(child);
        for (var _len = arguments.length, newChildren = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
          newChildren[_key - 1] = arguments[_key];
        }
        newChildren.forEach(newChild => nextSibling ? parent.insertBefore(newChild, nextSibling) : parent.appendChild(newChild));
      };
      originalChildren.forEach(child => {
        if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "DIV") {
          // Found a child node that's either not an element or not a div. Wrap it in a div.
          const divWrapper = document.createElement("div");
          replaceChild(child, divWrapper);
          divWrapper.appendChild(child);
        } else if (child.childNodes.length == 0) {
          // Empty div child node, include at least a <br />
          child.appendChild(document.createElement("br"));
        } else {
          const grandChildren = Array.from(child.childNodes);
          if (grandChildren.some(grandChild => grandChild.nodeType === Node.ELEMENT_NODE && grandChild.tagName === "DIV")) {
            return replaceChild(child, grandChildren);
          }
        }
      });
    }

    /**
     * Event handler for "selectionchange" events.
     */
    handleSelectionChangeEvent() {
      this.fireSelection();
    }

    /**
     * Convenience function to "splice" new lines into the arrays this.lines, this.lineDirty, this.lineTypes, and the DOM elements
     * underneath the editor element.
     * This method is essentially Array.splice, only that the third parameter takes an un-spread array (and the forth parameter)
     * determines whether the DOM should also be adjusted.
     *
     * @param {int} startLine Position at which to start changing the array of lines
     * @param {int} linesToDelete Number of lines to delete
     * @param {array} linesToInsert Array of strings representing the lines to be inserted
     * @param {boolean} adjustLineElements If true, then <div> elements are also inserted in the DOM at the respective position
     */
    spliceLines(startLine) {
      let linesToDelete = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
      let linesToInsert = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
      let adjustLineElements = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
      if (adjustLineElements) {
        for (let i = 0; i < linesToDelete; i++) {
          this.e.removeChild(this.e.childNodes[startLine]);
        }
      }
      let insertedBlank = [];
      let insertedDirty = [];
      for (let i = 0; i < linesToInsert.length; i++) {
        insertedBlank.push("");
        insertedDirty.push(true);
        if (adjustLineElements) {
          if (this.e.childNodes[startLine]) this.e.insertBefore(document.createElement("div"), this.e.childNodes[startLine]);else this.e.appendChild(document.createElement("div"));
        }
      }
      this.lines.splice(startLine, linesToDelete, ...linesToInsert);
      this.lineTypes.splice(startLine, linesToDelete, ...insertedBlank);
      this.lineDirty.splice(startLine, linesToDelete, ...insertedDirty);
    }

    /**
     * Event handler for the "paste" event
     */
    handlePaste(event) {
      event.preventDefault();

      // get text representation of clipboard
      let text = (event.originalEvent || event).clipboardData.getData("text/plain");

      // insert text manually
      this.paste(text);
    }

    /**
     * Pastes the text at the current selection (or at the end, if no current selection)
     * @param {string} text
     */
    paste(text) {
      let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
      let focus = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
      if (!anchor) anchor = this.getSelection(true);
      if (!focus) focus = this.getSelection(false);
      let beginning, end;
      if (!focus) {
        focus = {
          row: this.lines.length - 1,
          col: this.lines[this.lines.length - 1].length
        }; // Insert at end
      }

      if (!anchor) {
        anchor = focus;
      }
      if (anchor.row < focus.row || anchor.row == focus.row && anchor.col <= focus.col) {
        beginning = anchor;
        end = focus;
      } else {
        beginning = focus;
        end = anchor;
      }
      let insertedLines = text.split(/(?:\r\n|\r|\n)/);
      let lineBefore = this.lines[beginning.row].substr(0, beginning.col);
      let lineEnd = this.lines[end.row].substr(end.col);
      insertedLines[0] = lineBefore.concat(insertedLines[0]);
      let endColPos = insertedLines[insertedLines.length - 1].length;
      insertedLines[insertedLines.length - 1] = insertedLines[insertedLines.length - 1].concat(lineEnd);
      this.spliceLines(beginning.row, 1 + end.row - beginning.row, insertedLines);
      focus.row = beginning.row + insertedLines.length - 1;
      focus.col = endColPos;
      this.updateFormatting();
      this.setSelection(focus);
      this.fireChange();
    }

    /**
     * Computes the (lowest in the DOM tree) common ancestor of two DOM nodes.
     * @param {Node} node1 the first node
     * @param {Node} node2 the second node
     * @returns {Node} The commen ancestor node, or null if there is no common ancestor
     */
    computeCommonAncestor(node1, node2) {
      if (!node1 || !node2) return null;
      if (node1 == node2) return node1;
      const ancestry = node => {
        let ancestry = [];
        while (node) {
          ancestry.unshift(node);
          node = node.parentNode;
        }
        return ancestry;
      };
      const ancestry1 = ancestry(node1);
      const ancestry2 = ancestry(node2);
      if (ancestry1[0] != ancestry2[0]) return null;
      let i;
      for (i = 0; ancestry1[i] == ancestry2[i]; i++);
      return ancestry1[i - 1];
    }

    /**
     * Finds the (lowest in the DOM tree) enclosing DOM node with a given class.
     * @param {object} focus The focus selection object
     * @param {object} anchor The anchor selection object
     * @param {string} className The class name to find
     * @returns {Node} The enclosing DOM node with the respective class (inside the editor), if there is one; null otherwise.
     */
    computeEnclosingMarkupNode(focus, anchor, className) {
      let node = null;
      if (!focus) return null;
      if (!anchor) {
        node = focus.node;
      } else {
        if (focus.row != anchor.row) return null;
        node = this.computeCommonAncestor(focus.node, anchor.node);
      }
      if (!node) return null;
      while (node != this.e) {
        if (node.className && node.className.includes(className)) return node;
        node = node.parentNode;
      }
      // Ascended all the way to the editor element
      return null;
    }

    /**
     * Returns the state (true / false) of all commands.
     * @param focus Focus of the selection. If not given, assumes the current focus.
     * @param anchor Anchor of the selection. If not given, assumes the current anchor.
     */
    getCommandState() {
      let focus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
      let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
      let commandState = {};
      if (!focus) focus = this.getSelection(false);
      if (!anchor) anchor = this.getSelection(true);
      if (!focus) {
        for (let cmd in commands) {
          commandState[cmd] = null;
        }
        return commandState;
      }
      if (!anchor) anchor = focus;
      let start, end;
      if (anchor.row < focus.row || anchor.row == focus.row && anchor.col < focus.col) {
        start = anchor;
        end = focus;
      } else {
        start = focus;
        end = anchor;
      }
      if (end.row > start.row && end.col == 0) {
        end.row--;
        end.col = this.lines[end.row].length; // Selection to beginning of next line is said to end at the beginning of the last line
      }

      for (let cmd in commands) {
        if (commands[cmd].type == "inline") {
          if (!focus || focus.row != anchor.row || !this.isInlineFormattingAllowed(focus, anchor)) {
            commandState[cmd] = null;
          } else {
            // The command state is true if there is a respective enclosing markup node (e.g., the selection is enclosed in a <b>..</b>) ...
            commandState[cmd] = !!this.computeEnclosingMarkupNode(focus, anchor, commands[cmd].className) ||
            // ... or if it's an empty string preceded by and followed by formatting markers, e.g. **|** where | is the cursor
            focus.col == anchor.col && !!this.lines[focus.row].substr(0, focus.col).match(commands[cmd].unset.prePattern) && !!this.lines[focus.row].substr(focus.col).match(commands[cmd].unset.postPattern);
          }
        }
        if (commands[cmd].type == "line") {
          if (!focus) {
            commandState[cmd] = null;
          } else {
            let state = this.lineTypes[start.row] == commands[cmd].className;
            for (let line = start.row; line <= end.row; line++) {
              if (this.lineTypes[line] == commands[cmd].className != state) {
                state = null;
                break;
              }
            }
            commandState[cmd] = state;
          }
        }
      }
      return commandState;
    }

    /**
     * Sets a command state
     * @param {string} command
     * @param {boolean} state
     */
    setCommandState(command, state) {
      if (commands[command].type == "inline") {
        let anchor = this.getSelection(true);
        let focus = this.getSelection(false);
        if (!anchor) anchor = focus;
        if (!anchor) return;
        if (anchor.row != focus.row) return;
        if (!this.isInlineFormattingAllowed(focus, anchor)) return;
        let markupNode = this.computeEnclosingMarkupNode(focus, anchor, commands[command].className);
        this.clearDirtyFlag();

        // First case: There's an enclosing markup node, remove the markers around that markup node
        if (markupNode) {
          this.lineDirty[focus.row] = true;
          const startCol = this.computeColumn(markupNode, 0);
          const len = markupNode.textContent.length;
          const left = this.lines[focus.row].substr(0, startCol).replace(commands[command].unset.prePattern, "");
          const mid = this.lines[focus.row].substr(startCol, len);
          const right = this.lines[focus.row].substr(startCol + len).replace(commands[command].unset.postPattern, "");
          this.lines[focus.row] = left.concat(mid, right);
          anchor.col = left.length;
          focus.col = anchor.col + len;
          this.updateFormatting();
          this.setSelection(focus, anchor);
          this.fireChange();

          // Second case: Empty selection with surrounding formatting markers, remove those
        } else if (focus.col == anchor.col && !!this.lines[focus.row].substr(0, focus.col).match(commands[command].unset.prePattern) && !!this.lines[focus.row].substr(focus.col).match(commands[command].unset.postPattern)) {
          this.lineDirty[focus.row] = true;
          const left = this.lines[focus.row].substr(0, focus.col).replace(commands[command].unset.prePattern, "");
          const right = this.lines[focus.row].substr(focus.col).replace(commands[command].unset.postPattern, "");
          this.lines[focus.row] = left.concat(right);
          focus.col = anchor.col = left.length;
          this.updateFormatting();
          this.setSelection(focus, anchor);
          this.fireChange();

          // Not currently formatted, insert formatting markers
        } else {
          // Trim any spaces from the selection
          let {
            startCol,
            endCol
          } = focus.col < anchor.col ? {
            startCol: focus.col,
            endCol: anchor.col
          } : {
            startCol: anchor.col,
            endCol: focus.col
          };
          let match = this.lines[focus.row].substr(startCol, endCol - startCol).match(/^(?<leading>\s*).*\S(?<trailing>\s*)$/);
          if (match) {
            startCol += match.groups.leading.length;
            endCol -= match.groups.trailing.length;
          }
          focus.col = startCol;
          anchor.col = endCol;

          // Just insert markup before and after and hope for the best.
          this.wrapSelection(commands[command].set.pre, commands[command].set.post, focus, anchor);
          this.fireChange();
          // TODO clean this up so that markup remains properly nested
        }
      } else if (commands[command].type == "line") {
        let anchor = this.getSelection(true);
        let focus = this.getSelection(false);
        if (!anchor) anchor = focus;
        if (!focus) return;
        this.clearDirtyFlag();
        let start = anchor.row > focus.row ? focus : anchor;
        let end = anchor.row > focus.row ? anchor : focus;
        if (end.row > start.row && end.col == 0) {
          end.row--;
        }
        for (let line = start.row; line <= end.row; line++) {
          if (state && this.lineTypes[line] != commands[command].className) {
            this.lines[line] = this.lines[line].replace(commands[command].set.pattern, commands[command].set.replacement.replace("$#", line - start.row + 1));
            this.lineDirty[line] = true;
          }
          if (!state && this.lineTypes[line] == commands[command].className) {
            this.lines[line] = this.lines[line].replace(commands[command].unset.pattern, commands[command].unset.replacement);
            this.lineDirty[line] = true;
          }
        }
        this.updateFormatting();
        this.setSelection({
          row: end.row,
          col: this.lines[end.row].length
        }, {
          row: start.row,
          col: 0
        });
        this.fireChange();
      }
    }

    /**
     * Returns whether or not inline formatting is allowed at the current focus
     * @param {object} focus The current focus
     */
    isInlineFormattingAllowed() {
      // TODO Remove parameters from all calls
      const sel = window.getSelection();
      if (!sel || !sel.focusNode || !sel.anchorNode) return false;

      // Check if we can find a common ancestor with the class `TMInlineFormatted`

      // Special case: Empty selection right before `TMInlineFormatted`
      if (sel.isCollapsed && sel.focusNode.nodeType == 3 && sel.focusOffset == sel.focusNode.nodeValue.length) {
        let node;
        for (node = sel.focusNode; node && node.nextSibling == null; node = node.parentNode);
        if (node && node.nextSibling.className && node.nextSibling.className.includes("TMInlineFormatted")) return true;
      }

      // Look for a common ancestor
      let ancestor = this.computeCommonAncestor(sel.focusNode, sel.anchorNode);
      if (!ancestor) return false;

      // Check if there's an ancestor of class 'TMInlineFormatted' or 'TMBlankLine'
      while (ancestor && ancestor != this.e) {
        if (ancestor.className && (ancestor.className.includes("TMInlineFormatted") || ancestor.className.includes("TMBlankLine"))) return true;
        ancestor = ancestor.parentNode;
      }
      return false;
    }

    /**
     * Wraps the current selection in the strings pre and post. If the selection is not on one line, returns.
     * @param {string} pre The string to insert before the selection.
     * @param {string} post The string to insert after the selection.
     * @param {object} focus The current selection focus. If null, selection will be computed.
     * @param {object} anchor The current selection focus. If null, selection will be computed.
     */
    wrapSelection(pre, post) {
      let focus = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
      let anchor = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
      if (!focus) focus = this.getSelection(false);
      if (!anchor) anchor = this.getSelection(true);
      if (!focus || !anchor || focus.row != anchor.row) return;
      this.lineDirty[focus.row] = true;
      const startCol = focus.col < anchor.col ? focus.col : anchor.col;
      const endCol = focus.col < anchor.col ? anchor.col : focus.col;
      const left = this.lines[focus.row].substr(0, startCol).concat(pre);
      const mid = endCol == startCol ? "" : this.lines[focus.row].substr(startCol, endCol - startCol);
      const right = post.concat(this.lines[focus.row].substr(endCol));
      this.lines[focus.row] = left.concat(mid, right);
      anchor.col = left.length;
      focus.col = anchor.col + mid.length;
      this.updateFormatting();
      this.setSelection(focus, anchor);
    }

    /**
     * Toggles the command state for a command (true <-> false)
     * @param {string} command The editor command
     */
    toggleCommandState(command) {
      if (!this.lastCommandState) this.lastCommandState = this.getCommandState();
      this.setCommandState(command, !this.lastCommandState[command]);
    }

    /**
     * Fires a change event. Updates the linked textarea and notifies any event listeners.
     */
    fireChange() {
      if (!this.textarea && !this.listeners.change.length) return;
      const content = this.getContent();
      if (this.textarea) this.textarea.value = content;
      for (let listener of this.listeners.change) {
        listener({
          content: content,
          linesDirty: this.linesDirty
        });
      }
    }

    /**
     * Fires a "selection changed" event.
     */
    fireSelection() {
      if (this.listeners.selection && this.listeners.selection.length) {
        let focus = this.getSelection(false);
        let anchor = this.getSelection(true);
        let commandState = this.getCommandState(focus, anchor);
        if (this.lastCommandState) {
          Object.assign(this.lastCommandState, commandState);
        } else {
          this.lastCommandState = Object.assign({}, commandState);
        }
        for (let listener of this.listeners.selection) {
          listener({
            focus: focus,
            anchor: anchor,
            commandState: this.lastCommandState
          });
        }
      }
    }

    /**
     * Adds an event listener.
     * @param {string} type The type of event to listen to. Can be 'change' or 'selection'
     * @param {*} listener Function of the type (event) => {} to be called when the event occurs.
     */
    addEventListener(type, listener) {
      if (type.match(/^(?:change|input)$/i)) {
        this.listeners.change.push(listener);
      }
      if (type.match(/^(?:selection|selectionchange)$/i)) {
        this.listeners.selection.push(listener);
      }
    }
  }

  exports.CommandBar = CommandBar;
  exports.Editor = Editor;

  Object.defineProperty(exports, '__esModule', { value: true });

}));

//# sourceMappingURL=data:application/json;charset=utf-8;base64,[encoded data, 325592 characters]