~~~~*))(\s*)$/,
+ replacement: '$1$3'
+ },
+ TMBlankLine: {
+ regexp: /^([ \t]*)$/,
+ replacement: "$0"
+ },
+ TMSetextH1Marker: {
+ regexp: /^ {0,3}=+\s*$/,
+ replacement: '$0'
+ },
+ TMSetextH2Marker: {
+ regexp: /^ {0,3}-+\s*$/,
+ replacement: '$0'
+ },
+ TMHR: {
+ regexp: /^( {0,3}(\*[ \t]*\*[ \t]*\*[ \t*]*)|(-[ \t]*-[ \t]*-[ \t-]*)|(_[ \t]*_[ \t]*_[ \t_]*))$/,
+ replacement: '$0'
+ },
+ TMUL: {
+ regexp: /^( {0,3}[+*-] {1,4})(.*)$/,
+ replacement: '$1$$2'
+ },
+ TMOL: {
+ regexp: /^( {0,3}\d{1,9}[.)] {1,4})(.*)$/,
+ replacement: '$1$$2'
+ },
+ // TODO: This is currently preventing sublists (and any content within list items, really) from working
+ TMIndentedCode: {
+ regexp: /^( {4}|\t)(.*)$/,
+ replacement: '$1$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: '$1$2$3$4$5$6$7',
+ labelPlaceholder: 2
+ // this defines which placeholder in the above regex is the link "label"
+ }
+ };
+ var htmlBlockGrammar = exports.htmlBlockGrammar = [{
+ start: /^ {0,3}<(?:script|pre|style)(?:\s|>|$)/i,
+ end: /(?:<\/script>|<\/pre>|<\/style>)/i,
+ paraInterrupt: true
+ }, {
+ start: /^ {0,3}/,
+ paraInterrupt: true
+ }, {
+ start: /^ {0,3}<\?/,
+ end: /\?>/,
+ paraInterrupt: true
+ }, {
+ start: /^ {0,3}/,
+ paraInterrupt: true
+ }, {
+ start: /^ {0,3}/,
+ paraInterrupt: true
+ }, {
+ start: /^ {0,3}(?:<|<\/)(?:KnownTag)(?:\s|>|\/>|$)/i,
+ end: false,
+ paraInterrupt: true
+ }, {
+ start: /^ {0,3}(?:HTMLOpenTag|HTMLCloseTag)\s*$/,
+ end: false,
+ paraInterrupt: false
+ }];
+ var inlineGrammar = exports.inlineGrammar = {
+ escape: {
+ regexp: /^\\(ASCIIPunctuation)/,
+ replacement: '\\$1'
+ },
+ code: {
+ regexp: /^(`+)((?:[^`])|(?:[^`].*?[^`]))(\1)/,
+ replacement: '$1$2
$3'
+ },
+ autolink: {
+ regexp: /^<((?:Scheme:[^\s<>]*)|(?:Email))>/,
+ replacement: '<$1>'
+ },
+ html: {
+ regexp: /^((?:HTMLOpenTag)|(?:HTMLCloseTag)|(?:HTMLComment)|(?:HTMLPI)|(?:HTMLDeclaration)|(?:HTMLCDATA))/,
+ replacement: '$1'
+ },
+ linkOpen: {
+ regexp: /^\[/,
+ replacement: ""
+ },
+ imageOpen: {
+ regexp: /^!\[/,
+ replacement: ""
+ },
+ linkLabel: {
+ regexp: /^(\[\s*)([^\]]*?)(\s*\])/,
+ replacement: "",
+ labelPlaceholder: 2
+ },
+ default: {
+ regexp: /^(.|(?:NotTriggerChar+))/,
+ replacement: "$1"
+ }
+ };
+ var replacementRegexp = new RegExp(Object.keys(replacements).join("|"));
+ var inlineRules = [...Object.keys(inlineGrammar)];
+ for (let rule of inlineRules) {
+ let re = inlineGrammar[rule].regexp.source;
+ while (re.match(replacementRegexp)) {
+ re = re.replace(replacementRegexp, (string) => {
+ return replacements[string].source;
+ });
+ }
+ inlineGrammar[rule].regexp = new RegExp(re, inlineGrammar[rule].regexp.flags);
+ }
+ for (let rule of htmlBlockGrammar) {
+ let re = rule.start.source;
+ while (re.match(replacementRegexp)) {
+ re = re.replace(replacementRegexp, (string) => {
+ return replacements[string].source;
+ });
+ }
+ rule.start = new RegExp(re, rule.start.flags);
+ }
+ function htmlescape(string) {
+ return (string ? string : "").replace(/&/g, "&").replace(//g, ">");
+ }
+ var commands = exports.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"
+ }
+ }
+ };
+ }
+ });
+
+ // node_modules/tiny-markdown-editor/lib/TinyMDE.js
+ var require_TinyMDE = __commonJS({
+ "node_modules/tiny-markdown-editor/lib/TinyMDE.js"(exports) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ exports.default = void 0;
+ var _grammar = require_grammar();
+ var Editor = class {
+ constructor() {
+ let props = arguments.length > 0 && arguments[0] !== void 0 ? 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 && !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);
+ this.setContent(props.content || (this.textarea ? this.textarea.value : false) || "# 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;
+ this.e.style.whiteSpace = "pre-wrap";
+ 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));
+ document.addEventListener("selectionchange", (e) => this.handleSelectionChangeEvent(e));
+ this.e.addEventListener("paste", (e) => this.handlePaste(e));
+ this.lineElements = this.e.childNodes;
+ }
+ /**
+ * Sets the editor content.
+ * @param {string} content The new Markdown content
+ */
+ setContent(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() {
+ this.updateLineTypes();
+ this.updateLinkLabels();
+ 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][_grammar.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 (0, _grammar.htmlescape)(capture[p2]);
+ else
+ return `${this.processInlineStyles(capture[p2])}`;
+ });
+ }
+ /**
+ * 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.lineElements[lineNum].className = this.lineTypes[lineNum];
+ this.lineElements[lineNum].removeAttribute("style");
+ this.lineElements[lineNum].innerHTML = contentHTML == "" ? "
" : contentHTML;
+ }
+ 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";
+ if (codeBlockType == "TMCodeFenceBacktickOpen") {
+ let capture = _grammar.lineGrammar.TMCodeFenceBacktickClose.regexp.exec(this.lines[lineNum]);
+ if (capture && capture.groups["seq"].length >= codeBlockSeqLength) {
+ lineType = "TMCodeFenceBacktickClose";
+ lineReplacement = _grammar.lineGrammar.TMCodeFenceBacktickClose.replacement;
+ lineCapture = capture;
+ codeBlockType = false;
+ } else {
+ lineType = "TMFencedCodeBacktick";
+ lineReplacement = "$0";
+ lineCapture = [this.lines[lineNum]];
+ }
+ } else if (codeBlockType == "TMCodeFenceTildeOpen") {
+ let capture = _grammar.lineGrammar.TMCodeFenceTildeClose.regexp.exec(this.lines[lineNum]);
+ if (capture && capture.groups["seq"].length >= codeBlockSeqLength) {
+ lineType = "TMCodeFenceTildeClose";
+ lineReplacement = _grammar.lineGrammar.TMCodeFenceTildeClose.replacement;
+ lineCapture = capture;
+ codeBlockType = false;
+ } else {
+ lineType = "TMFencedCodeTilde";
+ lineReplacement = "$0";
+ lineCapture = [this.lines[lineNum]];
+ }
+ }
+ if (lineType == "TMPara" && htmlBlock === false) {
+ for (let htmlBlockType of _grammar.htmlBlockGrammar) {
+ if (this.lines[lineNum].match(htmlBlockType.start)) {
+ 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 = "$0";
+ lineCapture = [this.lines[lineNum]];
+ if (htmlBlock.end) {
+ if (this.lines[lineNum].match(htmlBlock.end)) {
+ htmlBlock = false;
+ }
+ } else {
+ if (lineNum == this.lines.length - 1 || this.lines[lineNum + 1].match(_grammar.lineGrammar.TMBlankLine.regexp)) {
+ htmlBlock = false;
+ }
+ }
+ }
+ if (lineType == "TMPara") {
+ for (let type in _grammar.lineGrammar) {
+ if (_grammar.lineGrammar[type].regexp) {
+ let capture = _grammar.lineGrammar[type].regexp.exec(this.lines[lineNum]);
+ if (capture) {
+ lineType = type;
+ lineReplacement = _grammar.lineGrammar[type].replacement;
+ lineCapture = capture;
+ break;
+ }
+ }
+ }
+ }
+ if (lineType == "TMCodeFenceBacktickOpen" || lineType == "TMCodeFenceTildeOpen") {
+ codeBlockType = lineType;
+ codeBlockSeqLength = lineCapture.groups["seq"].length;
+ }
+ 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")) {
+ lineType = "TMPara";
+ lineCapture = [this.lines[lineNum]];
+ lineReplacement = "$$0";
+ }
+ if (lineType == "TMSetextH2Marker") {
+ let capture = _grammar.lineGrammar.TMUL.regexp.exec(this.lines[lineNum]);
+ if (capture) {
+ lineType = "TMUL";
+ lineReplacement = _grammar.lineGrammar.TMUL.replacement;
+ lineCapture = capture;
+ }
+ }
+ if (lineType == "TMSetextH1Marker" || lineType == "TMSetextH2Marker") {
+ if (lineNum == 0 || this.lineTypes[lineNum - 1] != "TMPara") {
+ let capture = _grammar.lineGrammar.TMHR.regexp.exec(this.lines[lineNum]);
+ if (capture) {
+ lineType = "TMHR";
+ lineCapture = capture;
+ lineReplacement = _grammar.lineGrammar.TMHR.replacement;
+ } else {
+ lineType = "TMPara";
+ lineCapture = [this.lines[lineNum]];
+ lineReplacement = "$$0";
+ }
+ } else {
+ 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");
+ }
+ }
+ 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) {
+ 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 = [];
+ textOuter:
+ while (currentOffset < originalString.length && linkText === false) {
+ let string = originalString.substr(currentOffset);
+ for (let rule of ["escape", "code", "autolink", "html"]) {
+ let cap = _grammar.inlineGrammar[rule].regexp.exec(string);
+ if (cap) {
+ currentOffset += cap[0].length;
+ continue textOuter;
+ }
+ }
+ if (string.match(_grammar.inlineGrammar.imageOpen.regexp)) {
+ bracketLevel++;
+ currentOffset += 2;
+ continue textOuter;
+ }
+ if (string.match(_grammar.inlineGrammar.linkOpen.regexp)) {
+ bracketLevel++;
+ if (!isImage) {
+ if (this.parseLinkOrImage(string, false)) {
+ return false;
+ }
+ }
+ currentOffset += 1;
+ continue textOuter;
+ }
+ if (string.match(/^\]/)) {
+ bracketLevel--;
+ if (bracketLevel == 0) {
+ linkText = originalString.substr(textOffset, currentOffset - textOffset);
+ currentOffset++;
+ continue textOuter;
+ }
+ }
+ currentOffset++;
+ }
+ if (linkText === false)
+ return false;
+ let nextChar = currentOffset < originalString.length ? originalString.substr(currentOffset, 1) : "";
+ if (nextChar == "[") {
+ let string = originalString.substr(currentOffset);
+ let cap = _grammar.inlineGrammar.linkLabel.regexp.exec(string);
+ if (cap) {
+ currentOffset += cap[0].length;
+ linkLabel.push(cap[1], cap[2], cap[3]);
+ if (cap[_grammar.inlineGrammar.linkLabel.labelPlaceholder]) {
+ linkRef = cap[_grammar.inlineGrammar.linkLabel.labelPlaceholder];
+ } else {
+ linkRef = linkText.trim();
+ }
+ } else {
+ return false;
+ }
+ } else if (nextChar != "(") {
+ linkRef = linkText.trim();
+ } else {
+ currentOffset++;
+ let parenthesisLevel = 1;
+ inlineOuter:
+ while (currentOffset < originalString.length && parenthesisLevel > 0) {
+ let string = originalString.substr(currentOffset);
+ let cap = /^\s+/.exec(string);
+ if (cap) {
+ switch (linkDetails.length) {
+ case 0:
+ linkDetails.push(cap[0]);
+ break;
+ case 1:
+ linkDetails.push(cap[0]);
+ break;
+ case 2:
+ if (linkDetails[0].match(/)) {
+ linkDetails[1] = linkDetails[1].concat(cap[0]);
+ } else {
+ if (parenthesisLevel != 1)
+ return false;
+ linkDetails.push("");
+ linkDetails.push(cap[0]);
+ }
+ break;
+ case 3:
+ linkDetails.push(cap[0]);
+ break;
+ case 4:
+ return false;
+ case 5:
+ linkDetails.push("");
+ case 6:
+ linkDetails[5] = linkDetails[5].concat(cap[0]);
+ break;
+ case 7:
+ linkDetails[6] = linkDetails[6].concat(cap[0]);
+ break;
+ default:
+ return false;
+ }
+ currentOffset += cap[0].length;
+ continue inlineOuter;
+ }
+ cap = _grammar.inlineGrammar.escape.regexp.exec(string);
+ if (cap) {
+ switch (linkDetails.length) {
+ case 0:
+ linkDetails.push("");
+ case 1:
+ linkDetails.push(cap[0]);
+ break;
+ case 2:
+ linkDetails[1] = linkDetails[1].concat(cap[0]);
+ break;
+ case 3:
+ return false;
+ case 4:
+ return false;
+ case 5:
+ linkDetails.push("");
+ case 6:
+ linkDetails[5] = linkDetails[5].concat(cap[0]);
+ break;
+ default:
+ return false;
+ }
+ currentOffset += cap[0].length;
+ continue inlineOuter;
+ }
+ if (linkDetails.length < 2 && string.match(/^)) {
+ if (linkDetails.length == 0)
+ linkDetails.push("");
+ linkDetails[0] = linkDetails[0].concat("<");
+ currentOffset++;
+ continue inlineOuter;
+ }
+ if ((linkDetails.length == 1 || linkDetails.length == 2) && string.match(/^>/)) {
+ if (linkDetails.length == 1)
+ linkDetails.push("");
+ linkDetails.push(">");
+ currentOffset++;
+ continue inlineOuter;
+ }
+ cap = /^["']/.exec(string);
+ if (cap && (linkDetails.length == 0 || linkDetails.length == 1 || linkDetails.length == 4)) {
+ while (linkDetails.length < 4)
+ linkDetails.push("");
+ linkDetails.push(cap[0]);
+ currentOffset++;
+ continue inlineOuter;
+ }
+ if (cap && (linkDetails.length == 5 || linkDetails.length == 6) && linkDetails[4] == cap[0]) {
+ if (linkDetails.length == 5)
+ linkDetails.push("");
+ linkDetails.push(cap[0]);
+ currentOffset++;
+ continue inlineOuter;
+ }
+ if (string.match(/^\(/)) {
+ switch (linkDetails.length) {
+ case 0:
+ linkDetails.push("");
+ case 1:
+ linkDetails.push("");
+ case 2:
+ linkDetails[1] = linkDetails[1].concat("(");
+ if (!linkDetails[0].match(/<$/))
+ parenthesisLevel++;
+ break;
+ case 3:
+ linkDetails.push("");
+ case 4:
+ linkDetails.push("(");
+ break;
+ case 5:
+ linkDetails.push("");
+ case 6:
+ if (linkDetails[4] == "(")
+ return false;
+ linkDetails[5] = linkDetails[5].concat("(");
+ break;
+ default:
+ return false;
+ }
+ currentOffset++;
+ continue inlineOuter;
+ }
+ if (string.match(/^\)/)) {
+ if (linkDetails.length <= 2) {
+ 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) {
+ if (linkDetails[4] == "(") {
+ if (linkDetails.length == 5)
+ linkDetails.push("");
+ linkDetails.push(")");
+ } else {
+ if (linkDetails.length == 5)
+ linkDetails.push(")");
+ else
+ linkDetails[5] = linkDetails[5].concat(")");
+ }
+ } else {
+ parenthesisLevel--;
+ }
+ if (parenthesisLevel == 0) {
+ while (linkDetails.length < 7)
+ linkDetails.push("");
+ }
+ currentOffset++;
+ continue inlineOuter;
+ }
+ cap = /^./.exec(string);
+ if (cap) {
+ switch (linkDetails.length) {
+ case 0:
+ linkDetails.push("");
+ case 1:
+ linkDetails.push(cap[0]);
+ break;
+ case 2:
+ linkDetails[1] = linkDetails[1].concat(cap[0]);
+ break;
+ case 3:
+ return false;
+ case 4:
+ return false;
+ case 5:
+ linkDetails.push("");
+ case 6:
+ linkDetails[5] = linkDetails[5].concat(cap[0]);
+ break;
+ default:
+ return false;
+ }
+ currentOffset += cap[0].length;
+ continue inlineOuter;
+ }
+ throw "Infinite loop";
+ }
+ if (parenthesisLevel > 0)
+ return false;
+ }
+ if (linkRef !== false) {
+ let valid = false;
+ for (let label2 of this.linkLabels) {
+ if (label2 == linkRef) {
+ valid = true;
+ break;
+ }
+ }
+ let label = valid ? "TMLinkLabel TMLinkLabel_Valid" : "TMLinkLabel TMLinkLabel_Invalid";
+ let output = `${opener}${this.processInlineStyles(linkText)}]`;
+ if (linkLabel.length >= 3) {
+ output = output.concat(`${linkLabel[0]}`, `${linkLabel[1]}`, `${linkLabel[2]}`);
+ }
+ return {
+ output,
+ charCount: currentOffset
+ };
+ } else if (linkDetails) {
+ while (linkDetails.length < 7) {
+ linkDetails.push("");
+ }
+ return {
+ output: `${opener}${this.processInlineStyles(linkText)}](${linkDetails[0]}${linkDetails[1]}${linkDetails[2]}${linkDetails[3]}${linkDetails[4]}${linkDetails[5]}${linkDetails[6]})`,
+ 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 = [];
+ let offset = 0;
+ let string = originalString;
+ outer:
+ while (string) {
+ for (let rule of ["escape", "code", "autolink", "html"]) {
+ let cap2 = _grammar.inlineGrammar[rule].regexp.exec(string);
+ if (cap2) {
+ string = string.substr(cap2[0].length);
+ offset += cap2[0].length;
+ processed += _grammar.inlineGrammar[rule].replacement.replace(/\$([1-9])/g, (str, p1) => (0, _grammar.htmlescape)(cap2[p1]));
+ continue outer;
+ }
+ }
+ let potentialLink = string.match(_grammar.inlineGrammar.linkOpen.regexp);
+ let potentialImage = string.match(_grammar.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;
+ }
+ }
+ let cap = /(^\*+)|(^_+)/.exec(string);
+ if (cap) {
+ let delimCount = cap[0].length;
+ const delimString = cap[0];
+ const currentDelimiter = cap[0][0];
+ string = string.substr(cap[0].length);
+ const preceding = offset > 0 ? originalString.substr(0, offset) : " ";
+ const following = offset + cap[0].length < originalString.length ? string : " ";
+ const punctuationFollows = following.match(_grammar.punctuationLeading);
+ const punctuationPrecedes = preceding.match(_grammar.punctuationTrailing);
+ const whitespaceFollows = following.match(/^\s/);
+ const whitespacePrecedes = preceding.match(/\s$/);
+ let canOpen = !whitespaceFollows && (!punctuationFollows || !!whitespacePrecedes || !!punctuationPrecedes);
+ let canClose = !whitespacePrecedes && (!punctuationPrecedes || !!whitespaceFollows || !!punctuationFollows);
+ if (currentDelimiter == "_" && canOpen && canClose) {
+ canOpen = punctuationPrecedes;
+ canClose = punctuationFollows;
+ }
+ if (canClose) {
+ let stackPointer = stack.length - 1;
+ while (delimCount && stackPointer >= 0) {
+ if (stack[stackPointer].delimiter == currentDelimiter) {
+ while (stackPointer < stack.length - 1) {
+ const entry = stack.pop();
+ processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
+ }
+ if (delimCount >= 2 && stack[stackPointer].count >= 2) {
+ processed = `${currentDelimiter}${currentDelimiter}${processed}${currentDelimiter}${currentDelimiter}`;
+ delimCount -= 2;
+ stack[stackPointer].count -= 2;
+ } else {
+ processed = `${currentDelimiter}${processed}${currentDelimiter}`;
+ delimCount -= 1;
+ stack[stackPointer].count -= 1;
+ }
+ if (stack[stackPointer].count == 0) {
+ let entry = stack.pop();
+ processed = `${entry.output}${processed}`;
+ stackPointer--;
+ }
+ } else {
+ stackPointer--;
+ }
+ }
+ }
+ if (delimCount && canOpen) {
+ stack.push({
+ delimiter: currentDelimiter,
+ delimString,
+ count: delimCount,
+ output: processed
+ });
+ processed = "";
+ delimCount = 0;
+ }
+ if (delimCount) {
+ processed = `${processed}${delimString.substr(0, delimCount)}`;
+ }
+ offset += cap[0].length;
+ continue outer;
+ }
+ cap = /^~~/.exec(string);
+ if (cap) {
+ let consumed = false;
+ let stackPointer = stack.length - 1;
+ while (!consumed && stackPointer >= 0) {
+ if (stack[stackPointer].delimiter == "~") {
+ while (stackPointer < stack.length - 1) {
+ const entry2 = stack.pop();
+ processed = `${entry2.output}${entry2.delimString.substr(0, entry2.count)}${processed}`;
+ }
+ processed = `~~${processed}~~`;
+ let entry = stack.pop();
+ processed = `${entry.output}${processed}`;
+ consumed = true;
+ } else {
+ stackPointer--;
+ }
+ }
+ if (!consumed) {
+ stack.push({
+ delimiter: "~",
+ delimString: "~~",
+ count: 2,
+ output: processed
+ });
+ processed = "";
+ }
+ offset += cap[0].length;
+ string = string.substr(cap[0].length);
+ continue outer;
+ }
+ cap = _grammar.inlineGrammar.default.regexp.exec(string);
+ if (cap) {
+ string = string.substr(cap[0].length);
+ offset += cap[0].length;
+ processed += _grammar.inlineGrammar.default.replacement.replace(/\$([1-9])/g, (str, p1) => (0, _grammar.htmlescape)(cap[p1]));
+ continue outer;
+ }
+ throw "Infinite loop!";
+ }
+ 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() {
+ let lineDelta = this.e.childElementCount - this.lines.length;
+ if (lineDelta) {
+ let firstChangedLine = 0;
+ while (firstChangedLine <= this.lines.length && firstChangedLine <= this.lineElements.length && this.lineElements[firstChangedLine] && this.lines[firstChangedLine] == this.lineElements[firstChangedLine].textContent) {
+ firstChangedLine++;
+ }
+ 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 {
+ for (let line = 0; line < this.lineElements.length; line++) {
+ let e = this.lineElements[line];
+ let ct = e.textContent;
+ if (this.lines[line] !== ct) {
+ this.lines[line] = ct;
+ this.lineDirty[line] = true;
+ }
+ }
+ }
+ }
+ /**
+ * Processes a new paragraph.
+ * @param sel The current selection
+ */
+ processNewParagraph(sel) {
+ if (!sel)
+ return;
+ this.updateLineContents();
+ let continuableType = false;
+ 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) {
+ this.updateFormatting();
+ return;
+ }
+ this.spliceLines(sel.row, 1, lines, true);
+ sel.row++;
+ sel.col = 0;
+ if (continuableType) {
+ let capture = _grammar.lineGrammar[continuableType].regexp.exec(this.lines[sel.row - 1]);
+ if (capture) {
+ if (capture[2]) {
+ 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 {
+ 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] !== void 0 ? arguments[0] : false;
+ const selection = window.getSelection();
+ let startNode = getAnchor ? selection.anchorNode : selection.focusNode;
+ if (!startNode)
+ return null;
+ let offset = startNode.nodeType === Node.TEXT_NODE ? getAnchor ? selection.anchorOffset : selection.focusOffset : 0;
+ if (startNode == this.e) {
+ return {
+ row: 0,
+ col: offset
+ };
+ }
+ let col = this.computeColumn(startNode, offset);
+ if (col === null)
+ return null;
+ let node = startNode;
+ while (node.parentElement != this.e) {
+ node = node.parentElement;
+ }
+ let row = 0;
+ 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,
+ 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 = offset;
+ while (node && node.parentNode != this.e) {
+ node = node.parentNode;
+ }
+ if (node == null)
+ return null;
+ node = startNode;
+ 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] !== void 0 ? arguments[2] : false;
+ if (row >= this.lineElements.length) {
+ 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;
+ 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) {
+ rv = {
+ node,
+ offset: col
+ };
+ col = 0;
+ } else {
+ return {
+ 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;
+ }
+ }
+ 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.
+ */
+ setSelection(focus) {
+ let anchor = arguments.length > 1 && arguments[1] !== void 0 ? 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);
+ 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) {
+ 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 = '
';
+ } else {
+ for (let childNode = this.e.firstChild; childNode; childNode = childNode.nextSibling) {
+ if (childNode.nodeType != 3 || childNode.tagName != "DIV") {
+ let divWrapper = document.createElement("div");
+ this.e.insertBefore(divWrapper, childNode);
+ this.e.removeChild(childNode);
+ divWrapper.appendChild(childNode);
+ }
+ }
+ }
+ this.updateLineContentsAndFormatting();
+ }
+ if (focus)
+ this.setSelection(focus);
+ this.fireChange();
+ }
+ /**
+ * 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 elements are also inserted in the DOM at the respective position
+ */
+ spliceLines(startLine) {
+ let linesToDelete = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0;
+ let linesToInsert = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : [];
+ let adjustLineElements = arguments.length > 3 && arguments[3] !== void 0 ? 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();
+ let text = (event.originalEvent || event).clipboardData.getData("text/plain");
+ 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] !== void 0 ? arguments[1] : null;
+ let focus = arguments.length > 2 && arguments[2] !== void 0 ? 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
+ };
+ }
+ 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 ancestry3 = [];
+ while (node) {
+ ancestry3.unshift(node);
+ node = node.parentNode;
+ }
+ return ancestry3;
+ };
+ 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;
+ }
+ return null;
+ }
+ /**
+ * Returns the state (true / false) of all commands.
+ * @param focus Focus of the selection. If not given, assumes the current focus.
+ */
+ getCommandState() {
+ let focus = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : null;
+ let anchor = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : null;
+ let commandState = {};
+ if (!focus)
+ focus = this.getSelection(false);
+ if (!anchor)
+ anchor = this.getSelection(true);
+ if (!focus) {
+ for (let cmd in _grammar.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;
+ }
+ for (let cmd in _grammar.commands) {
+ if (_grammar.commands[cmd].type == "inline") {
+ if (!focus || focus.row != anchor.row || !this.isInlineFormattingAllowed(focus, anchor)) {
+ commandState[cmd] = null;
+ } else {
+ commandState[cmd] = !!this.computeEnclosingMarkupNode(focus, anchor, _grammar.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(_grammar.commands[cmd].unset.prePattern) && !!this.lines[focus.row].substr(focus.col).match(_grammar.commands[cmd].unset.postPattern);
+ }
+ }
+ if (_grammar.commands[cmd].type == "line") {
+ if (!focus) {
+ commandState[cmd] = null;
+ } else {
+ let state = this.lineTypes[start.row] == _grammar.commands[cmd].className;
+ for (let line = start.row; line <= end.row; line++) {
+ if (this.lineTypes[line] == _grammar.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 (_grammar.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, _grammar.commands[command].className);
+ this.clearDirtyFlag();
+ 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(_grammar.commands[command].unset.prePattern, "");
+ const mid = this.lines[focus.row].substr(startCol, len);
+ const right = this.lines[focus.row].substr(startCol + len).replace(_grammar.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);
+ } else if (focus.col == anchor.col && !!this.lines[focus.row].substr(0, focus.col).match(_grammar.commands[command].unset.prePattern) && !!this.lines[focus.row].substr(focus.col).match(_grammar.commands[command].unset.postPattern)) {
+ this.lineDirty[focus.row] = true;
+ const left = this.lines[focus.row].substr(0, focus.col).replace(_grammar.commands[command].unset.prePattern, "");
+ const right = this.lines[focus.row].substr(focus.col).replace(_grammar.commands[command].unset.postPattern, "");
+ this.lines[focus.row] = left.concat(right);
+ focus.col = anchor.col = left.length;
+ this.updateFormatting();
+ this.setSelection(focus, anchor);
+ } else {
+ 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(/^(?\s*).*\S(?\s*)$/);
+ if (match) {
+ startCol += match.groups.leading.length;
+ endCol -= match.groups.trailing.length;
+ }
+ focus.col = startCol;
+ anchor.col = endCol;
+ this.wrapSelection(_grammar.commands[command].set.pre, _grammar.commands[command].set.post, focus, anchor);
+ }
+ } else if (_grammar.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] != _grammar.commands[command].className) {
+ this.lines[line] = this.lines[line].replace(_grammar.commands[command].set.pattern, _grammar.commands[command].set.replacement.replace("$#", line - start.row + 1));
+ this.lineDirty[line] = true;
+ }
+ if (!state && this.lineTypes[line] == _grammar.commands[command].className) {
+ this.lines[line] = this.lines[line].replace(_grammar.commands[command].unset.pattern, _grammar.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
+ });
+ }
+ }
+ /**
+ * Returns whether or not inline formatting is allowed at the current focus
+ * @param {object} focus The current focus
+ */
+ isInlineFormattingAllowed() {
+ const sel = window.getSelection();
+ if (!sel)
+ return false;
+ 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;
+ }
+ let ancestor = this.computeCommonAncestor(sel.focusNode, sel.anchorNode);
+ if (!ancestor)
+ return false;
+ 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] !== void 0 ? arguments[2] : null;
+ let anchor = arguments.length > 3 && arguments[3] !== void 0 ? 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,
+ 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,
+ 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);
+ }
+ }
+ };
+ var _default = exports.default = Editor;
+ }
+ });
+
+ // node_modules/tiny-markdown-editor/lib/index.js
+ var require_lib = __commonJS({
+ "node_modules/tiny-markdown-editor/lib/index.js"(exports) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ Object.defineProperty(exports, "CommandBar", {
+ enumerable: true,
+ get: function() {
+ return _TinyMDECommandBar.default;
+ }
+ });
+ Object.defineProperty(exports, "Editor", {
+ enumerable: true,
+ get: function() {
+ return _TinyMDE.default;
+ }
+ });
+ var _TinyMDECommandBar = _interopRequireDefault(require_TinyMDECommandBar());
+ var _TinyMDE = _interopRequireDefault(require_TinyMDE());
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : { default: obj };
+ }
+ }
+ });
+
+ // node_modules/@icelab/defo/dist/index.bundle.js
+ var require_index_bundle = __commonJS({
+ "node_modules/@icelab/defo/dist/index.bundle.js"(exports, module) {
+ !function(e, t) {
+ "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = e || self).Defo = t();
+ }(exports, function() {
+ "use strict";
+ function e(e2, t2) {
+ return function(e3, t3) {
+ return Object.keys(e3.dataset).filter((e4) => 0 === e4.indexOf(t3));
+ }(e2, t2).length > 0;
+ }
+ function t(e2) {
+ return e2.charAt(0).toUpperCase() + e2.slice(1);
+ }
+ function r(e2) {
+ try {
+ return JSON.parse(e2);
+ } catch (e3) {
+ }
+ return e2;
+ }
+ function o({ prefix: e2, scope: t2, views: r2 }) {
+ Object.keys(r2).forEach((o2) => {
+ const a2 = `data-${e2}-${s = o2, s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
+ var s;
+ let i = Array.prototype.slice.call(t2.querySelectorAll(`[${a2}]`));
+ t2.hasAttribute(a2) && (i = [t2].concat(i)), i.forEach((t3) => {
+ n(t3, e2, r2, o2);
+ });
+ });
+ }
+ function n(e2, o2, n2, a2) {
+ if (e2._defoUpdate && e2._defoUpdate[a2])
+ return;
+ const s = n2[a2], i = function(e3, r2) {
+ return `${o3 = e3, o3.replace(/^[_.\- ]+/, "").toLowerCase().replace(/[_.\- ]+(\w|$)/g, (e4, t2) => t2.toUpperCase()).replace(/\d+(\w|$)/g, (e4) => e4.toUpperCase())}${t(r2)}`;
+ var o3;
+ }(o2, a2);
+ if (!s || !(i in e2.dataset))
+ return;
+ const c = s(e2, r(e2.dataset[i]));
+ e2._defoUpdate = e2._defoUpdate || {}, e2._defoDestroy = e2._defoDestroy || {}, e2._defoUpdate[a2] = function(e3) {
+ return function(t2, o3) {
+ t2 = t2 ? r(t2) : t2, o3 = o3 ? r(o3) : o3, Promise.resolve(e3).then((e4) => {
+ e4.update && e4.update(t2, o3);
+ });
+ };
+ }(c), e2._defoDestroy[a2] = function(e3, t2, r2) {
+ return function() {
+ Promise.resolve(e3).then((e4) => {
+ e4.destroy && (e4.destroy(), delete t2._defoUpdate[r2], delete t2._defoDestroy[r2]);
+ });
+ };
+ }(c, e2, a2);
+ }
+ function a({ prefix: r2, scope: a2, views: s }) {
+ const i = new MutationObserver((a3) => {
+ a3.forEach((a4) => {
+ const i2 = a4.target;
+ if ("attributes" === a4.type && function(e2, t2) {
+ return 0 === e2.indexOf(`data-${t2}-`);
+ }(a4.attributeName, r2)) {
+ const e2 = a4.attributeName.split("-").slice(2).map((e3, r3) => r3 > 0 ? t(e3) : e3).join("");
+ i2.hasAttribute(a4.attributeName) ? null !== a4.oldValue ? i2._defoUpdate[e2](i2.getAttribute(a4.attributeName), a4.oldValue) : n(i2, r2, s, e2) : i2._defoDestroy[e2]();
+ } else
+ "childList" === a4.type && (Array.prototype.slice.call(a4.removedNodes).filter((e2) => e2.nodeType === e2.ELEMENT_NODE).filter((t2) => e(t2, r2)).forEach((e2) => {
+ Object.keys(e2._defoDestroy).forEach((t2) => {
+ e2._defoDestroy[t2]();
+ });
+ }), Array.prototype.slice.call(a4.addedNodes).filter((e2) => e2.nodeType === e2.ELEMENT_NODE).forEach((e2) => {
+ Promise.resolve(e2).then((e3) => {
+ o({ prefix: r2, scope: e3, views: s });
+ });
+ }));
+ });
+ });
+ return i.observe(a2, { attributes: true, attributeOldValue: true, childList: true, characterData: false, subtree: true }), o({ prefix: r2, scope: a2, views: s }), i;
+ }
+ return function({ prefix: e2 = "defo", scope: t2 = document.documentElement, views: r2 = {} } = {}) {
+ const o2 = a({ prefix: e2, scope: t2, views: r2 });
+ return { destroy: () => {
+ o2.disconnect();
+ } };
+ };
+ });
+ }
+ });
+
+ // slices/admin/assets/js/app.js
+ var import_tiny_markdown_editor = __toESM(require_lib(), 1);
+ var import_defo = __toESM(require_index_bundle(), 1);
+ (function() {
+ document.addEventListener("alpine:init", () => {
+ Alpine.magic("clipboard", () => {
+ return (subject) => navigator.clipboard.writeText(subject);
+ });
+ });
+ document.addEventListener("DOMContentLoaded", function() {
+ const views = {
+ markdown: (el, attrs) => {
+ var tinyMDE = new import_tiny_markdown_editor.default.Editor({ textarea: attrs.editorId });
+ return {
+ update: (newName, oldName) => {
+ },
+ destroy: () => {
+ }
+ };
+ }
+ };
+ (0, import_defo.default)({ views });
+ });
+ })();
+})();
diff --git a/slices/admin/assets/js/app.js b/slices/admin/assets/js/app.js
index 6309fe9..6cf671c 100644
--- a/slices/admin/assets/js/app.js
+++ b/slices/admin/assets/js/app.js
@@ -5,6 +5,12 @@ import TinyMDE from "tiny-markdown-editor";
import defo from "@icelab/defo";
(function() {
+ document.addEventListener('alpine:init', () => {
+ Alpine.magic('clipboard', () => {
+ return subject => navigator.clipboard.writeText(subject)
+ })
+ })
+
document.addEventListener("DOMContentLoaded", function () {
const views = {
markdown: (el, attrs) => {
diff --git a/slices/admin/templates/photos/index.html.slim b/slices/admin/templates/photos/index.html.slim
index 6f48026..1b998ad 100644
--- a/slices/admin/templates/photos/index.html.slim
+++ b/slices/admin/templates/photos/index.html.slim
@@ -14,6 +14,9 @@ div class="mb-4 max-w-prose mx-auto prose dark:prose-invert"
- photos.each_with_index do |photo, idx|
- next if photo.match(/small/)
div class="rounded max-w-xs" x-data="" id="photo-#{idx}"
- img class="rounded object-cover hover:opacity-80 h-48 w-48" src="/#{photo.gsub("public/", "")}"
- button class="hover:text-blue-400 p-2 bg-blue-100 rounded text-blue-600 mr-4 no-underline" @click="$clipboard('#{Hanami.app.settings.micropub_site_url}/#{photo.gsub("public/", "")}')" Copy URL
- button class="text-red-600 hover:text-red-400" hx-delete="/admin/media/#{photo}" hx-target="#photo-#{idx}" Delete
\ No newline at end of file
+ img class="rounded object-cover hover:opacity-80 h-48 w-48 mb-2" src="/#{photo.gsub("public/", "")}"
+ div class="grid grid-cols-2 gap-2"
+ button class="hover:text-blue-400 p-1 bg-blue-100 rounded text-blue-600 no-underline" @click="$clipboard('#{Hanami.app.settings.micropub_site_url}/#{photo.gsub("public/", "")}')" Copy URL
+ button class="hover:text-blue-400 p-1 bg-blue-100 rounded text-blue-600 no-underline" @click="$clipboard('})')" Copy .md
+ div class="grid grid-cols-1"
+ button class="text-red-600 p-1 bg-red-50 rounded hover:text-red-400 mt-2" hx-delete="/admin/media/#{photo}" hx-target="#photo-#{idx}" Delete
\ No newline at end of file