UNPKG

tiny-markdown-editor

Version:

TinyMDE: A tiny, ultra low dependency, embeddable HTML/JavaScript Markdown editor.

1,179 lines (1,178 loc) 64.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Editor = void 0; const grammar_1 = require("./grammar"); class Editor { constructor(props = {}) { this.e = null; this.textarea = null; this.lines = []; this.lineElements = []; this.lineTypes = []; this.lineCaptures = []; this.lineReplacements = []; this.linkLabels = []; this.lineDirty = []; this.lastCommandState = null; this.customInlineGrammar = {}; this.mergedInlineGrammar = grammar_1.inlineGrammar; this.hasFocus = true; this.listeners = { change: [], selection: [], drop: [], }; this.undoStack = []; this.redoStack = []; this.isRestoringHistory = false; this.maxHistory = 100; this.e = null; this.textarea = null; this.lines = []; this.lineElements = []; this.lineTypes = []; this.lineCaptures = []; this.lineReplacements = []; this.linkLabels = []; this.lineDirty = []; this.lastCommandState = null; this.hasFocus = true; this.customInlineGrammar = props.customInlineGrammar || {}; this.mergedInlineGrammar = (0, grammar_1.createMergedInlineGrammar)(this.customInlineGrammar); this.listeners = { change: [], selection: [], drop: [], }; let element = null; if (typeof props.element === 'string') { element = document.getElementById(props.element); } else if (props.element) { element = props.element; } if (typeof props.textarea === 'string') { this.textarea = document.getElementById(props.textarea); } else if (props.textarea) { this.textarea = props.textarea; } if (this.textarea) { if (!element) element = this.textarea; } if (!element) { element = document.getElementsByTagName("body")[0]; } if (element && element.tagName === "TEXTAREA") { this.textarea = element; element = this.textarea.parentNode; } if (this.textarea) { this.textarea.style.display = "none"; } this.undoStack = []; this.redoStack = []; this.isRestoringHistory = false; this.maxHistory = 100; this.createEditorElement(element, props); this.setContent(typeof props.content === "string" ? props.content : this.textarea ? this.textarea.value : "# Hello TinyMDE!\nEdit **here**"); this.e.addEventListener("keydown", (e) => this.handleUndoRedoKey(e)); } get canUndo() { return this.undoStack.length >= 2; } get canRedo() { return this.redoStack.length > 0; } pushHistory() { if (this.isRestoringHistory) return; this.pushCurrentState(); this.redoStack = []; } pushCurrentState() { this.undoStack.push({ content: this.getContent(), selection: this.getSelection(), anchor: this.getSelection(true), }); if (this.undoStack.length > this.maxHistory) this.undoStack.shift(); } undo() { if (this.undoStack.length < 2) return; this.isRestoringHistory = true; this.pushCurrentState(); const current = this.undoStack.pop(); this.redoStack.push(current); const prev = this.undoStack[this.undoStack.length - 1]; this.setContent(prev.content); if (prev.selection) this.setSelection(prev.selection, prev.anchor); this.undoStack.pop(); this.isRestoringHistory = false; } redo() { if (!this.redoStack.length) return; this.isRestoringHistory = true; this.pushCurrentState(); const next = this.redoStack.pop(); this.setContent(next.content); if (next.selection) this.setSelection(next.selection, next.anchor); this.isRestoringHistory = false; } handleUndoRedoKey(e) { const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); const ctrl = isMac ? e.metaKey : e.ctrlKey; if (ctrl && !e.altKey) { if (e.key === "z" || e.key === "Z") { if (e.shiftKey) { this.redo(); } else { this.undo(); } e.preventDefault(); } else if (e.key === "y" || e.key === "Y") { this.redo(); e.preventDefault(); } } } createEditorElement(element, props) { if (props && props.editor !== undefined) { if (typeof props.editor === 'string') { this.e = document.getElementById(props.editor); } else { this.e = props.editor; } } else { this.e = document.createElement("div"); } this.e.classList.add("TinyMDE"); this.e.contentEditable = "true"; this.e.style.whiteSpace = "pre-wrap"; this.e.style.webkitUserModify = "read-write-plaintext-only"; if (props.editor === undefined) { 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) => { if (this.hasFocus) { this.handleSelectionChangeEvent(e); } }); this.e.addEventListener("blur", () => this.hasFocus = false); this.e.addEventListener("focus", () => this.hasFocus = true); this.e.addEventListener("paste", (e) => this.handlePaste(e)); this.e.addEventListener("drop", (e) => this.handleDrop(e)); this.lineElements = this.e.childNodes; } 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(); if (!this.isRestoringHistory) this.pushHistory(); } getContent() { return this.lines.join("\n"); } updateFormatting() { this.updateLineTypes(); this.updateLinkLabels(); this.applyLineTypes(); } updateLinkLabels() { this.linkLabels = []; for (let l = 0; l < this.lines.length; l++) { if (this.lineTypes[l] === "TMLinkReferenceDefinition") { this.linkLabels.push(this.lineCaptures[l][grammar_1.lineGrammar.TMLinkReferenceDefinition.labelPlaceholder]); } } } replace(replacement, capture) { return replacement.replace(/(\${1,2})([0-9])/g, (str, p1, p2) => { if (p1 === "$") return (0, grammar_1.htmlescape)(capture[parseInt(p2)]); else return `<span class="TMInlineFormatted">${this.processInlineStyles(capture[parseInt(p2)])}</span>`; }); } 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 === "" ? "<br />" : contentHTML; } this.lineElements[lineNum].dataset.lineNum = lineNum.toString(); } } 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"; // Check ongoing code blocks if (codeBlockType === "TMCodeFenceBacktickOpen") { let capture = grammar_1.lineGrammar.TMCodeFenceBacktickClose.regexp.exec(this.lines[lineNum]); if (capture && capture.groups["seq"].length >= codeBlockSeqLength) { lineType = "TMCodeFenceBacktickClose"; lineReplacement = grammar_1.lineGrammar.TMCodeFenceBacktickClose.replacement; lineCapture = capture; codeBlockType = false; } else { lineType = "TMFencedCodeBacktick"; lineReplacement = '<span class="TMFencedCode">$0<br /></span>'; lineCapture = [this.lines[lineNum]]; } } else if (codeBlockType === "TMCodeFenceTildeOpen") { let capture = grammar_1.lineGrammar.TMCodeFenceTildeClose.regexp.exec(this.lines[lineNum]); if (capture && capture.groups["seq"].length >= codeBlockSeqLength) { lineType = "TMCodeFenceTildeClose"; lineReplacement = grammar_1.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 grammar_1.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 = '<span class="TMHTMLContent">$0<br /></span>'; 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_1.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 grammar_1.lineGrammar) { if (grammar_1.lineGrammar[type].regexp) { let capture = grammar_1.lineGrammar[type].regexp.exec(this.lines[lineNum]); if (capture) { lineType = type; lineReplacement = grammar_1.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")) { 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 if (lineType === "TMSetextH2Marker") { let capture = grammar_1.lineGrammar.TMUL.regexp.exec(this.lines[lineNum]); if (capture) { lineType = "TMUL"; lineReplacement = grammar_1.lineGrammar.TMUL.replacement; lineCapture = capture; } } // Setext headings are only valid if preceded by a paragraph if (lineType === "TMSetextH1Marker" || lineType === "TMSetextH2Marker") { if (lineNum === 0 || this.lineTypes[lineNum - 1] !== "TMPara") { let capture = grammar_1.lineGrammar.TMHR.regexp.exec(this.lines[lineNum]); if (capture) { lineType = "TMHR"; lineCapture = capture; lineReplacement = grammar_1.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[headingLine] !== headingLineType) { this.lineTypes[headingLine] = headingLineType; this.lineDirty[headingLine] = 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; } } getSelection(getAnchor = false) { var _a, _b; 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; let node = startNode; while (node.parentElement !== this.e) { node = node.parentElement; } let row = 0; // If the node doesn't have a previous sibling, it must be the first line if (node.previousSibling) { const currentLineNumData = (_a = node.dataset) === null || _a === void 0 ? void 0 : _a.lineNum; const previousLineNumData = (_b = node.previousSibling.dataset) === null || _b === void 0 ? void 0 : _b.lineNum; if (currentLineNumData && previousLineNumData) { const currentLineNum = parseInt(currentLineNumData); const previousLineNum = parseInt(previousLineNumData); if (currentLineNum === previousLineNum + 1) { row = currentLineNum; } else { // If the current line is NOT the previous line + 1, then either // the current line got split in two or merged with the previous line // Either way, we need to recalculate the row number while (node.previousSibling) { row++; node = node.previousSibling; } } } } return { row: row, col: col }; } setSelection(focus, anchor = null) { if (!focus) return; let { node: focusNode, offset: focusOffset } = this.computeNodeAndOffset(focus.row, focus.col, anchor ? anchor.row === focus.row && anchor.col > focus.col : false); 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; } let windowSelection = window.getSelection(); windowSelection.setBaseAndExtent(focusNode, focusOffset, anchorNode || focusNode, anchorNode ? anchorOffset : focusOffset); } paste(text, anchor = null, focus = 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(); } wrapSelection(pre, post, focus = null, anchor = null) { if (!this.isRestoringHistory) this.pushHistory(); 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); } 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); } if (type.match(/^(?:drop)$/i)) { this.listeners.drop.push(listener); } } 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.lineDirty], }); } } handleInputEvent(event) { const inputEvent = event; if (inputEvent.inputType === "insertCompositionText") return; if (!this.isRestoringHistory) this.pushHistory(); let focus = this.getSelection(); if ((inputEvent.inputType === "insertParagraph" || inputEvent.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(); } handleSelectionChangeEvent(_e) { this.fireSelection(); } handlePaste(event) { if (!this.isRestoringHistory) this.pushHistory(); event.preventDefault(); let text = event.clipboardData.getData("text/plain"); this.paste(text); } handleDrop(event) { event.preventDefault(); this.fireDrop(event.dataTransfer); } processInlineStyles(originalString) { let processed = ""; let stack = []; let offset = 0; let string = originalString; outer: while (string) { // Process simple rules (non-delimiter) for (let rule of ["escape", "code", "autolink", "html"]) { if (this.mergedInlineGrammar[rule]) { let cap = this.mergedInlineGrammar[rule].regexp.exec(string); if (cap) { string = string.substr(cap[0].length); offset += cap[0].length; processed += this.mergedInlineGrammar[rule].replacement.replace(/\$([1-9])/g, (str, p1) => (0, grammar_1.htmlescape)(cap[p1])); continue outer; } } } // Process custom inline grammar rules for (let rule in this.customInlineGrammar) { if (rule !== "escape" && rule !== "code" && rule !== "autolink" && rule !== "html" && rule !== "linkOpen" && rule !== "imageOpen" && rule !== "linkLabel" && rule !== "default") { let cap = this.mergedInlineGrammar[rule].regexp.exec(string); if (cap) { string = string.substr(cap[0].length); offset += cap[0].length; processed += this.mergedInlineGrammar[rule].replacement.replace(/\$([1-9])/g, (str, p1) => (0, grammar_1.htmlescape)(cap[p1])); continue outer; } } } // Check for links / images let potentialLink = string.match(this.mergedInlineGrammar.linkOpen.regexp); let potentialImage = string.match(this.mergedInlineGrammar.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]; 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_1.punctuationLeading); const punctuationPrecedes = preceding.match(grammar_1.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 = `<span class="TMMark">${currentDelimiter}${currentDelimiter}</span><strong class="TMStrong">${processed}</strong><span class="TMMark">${currentDelimiter}${currentDelimiter}</span>`; delimCount -= 2; stack[stackPointer].count -= 2; } else { processed = `<span class="TMMark">${currentDelimiter}</span><em class="TMEm">${processed}</em><span class="TMMark">${currentDelimiter}</span>`; 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: delimString, count: delimCount, output: processed, }); processed = ""; delimCount = 0; } 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; while (!consumed && stackPointer >= 0) { if (stack[stackPointer].delimiter === "~") { while (stackPointer < stack.length - 1) { const entry = stack.pop(); processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`; } 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 { stackPointer--; } } if (!consumed) { stack.push({ delimiter: "~", delimString: "~~", count: 2, output: processed, }); processed = ""; } offset += cap[0].length; string = string.substr(cap[0].length); continue outer; } // Process 'default' rule cap = this.mergedInlineGrammar.default.regexp.exec(string); if (cap) { string = string.substr(cap[0].length); offset += cap[0].length; processed += this.mergedInlineGrammar.default.replacement.replace(/\$([1-9])/g, (str, p1) => (0, grammar_1.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; } computeColumn(startNode, offset) { let node = startNode; let col; while (node && node.parentNode !== this.e) { node = node.parentNode; } if (node === null) return null; if (startNode.nodeType === Node.TEXT_NODE || offset === 0) { col = offset; node = startNode; } else if (offset > 0) { node = startNode.childNodes[offset - 1]; col = node.textContent.length; } else { col = 0; node = startNode; } while (node.parentNode !== this.e) { if (node.previousSibling) { node = node.previousSibling; col += node.textContent.length; } else { node = node.parentNode; } } return col; } computeNodeAndOffset(row, col, bindRight = 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: 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; } } return rv; } updateLineContentsAndFormatting() { this.clearDirtyFlag(); this.updateLineContents(); this.updateFormatting(); } clearDirtyFlag() { this.lineDirty = new Array(this.lines.length); for (let i = 0; i < this.lineDirty.length; i++) { this.lineDirty[i] = false; } } 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 && this.lineTypes[firstChangedLine] === this.lineElements[firstChangedLine].className) { 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 && this.lineTypes[this.lines.length + lastChangedLine] === this.lineElements[this.lineElements.length + lastChangedLine].className) { 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.lineTypes[line] !== e.className) { this.lines[line] = ct; this.lineTypes[line] = e.className; this.lineDirty[line] = true; } } } } 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.spliceLines(sel.row, 1, lines, true); sel.row++; sel.col = 0; } if (continuableType) { let capture = grammar_1.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) + 1).toString(); }); } 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(); } spliceLines(startLine, linesToDelete = 0, linesToInsert = [], adjustLineElements = 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); } fixNodeHierarchy() { const originalChildren = Array.from(this.e.childNodes); const replaceChild = (child, ...newChildren) => { const parent = child.parentElement; const nextSibling = child.nextSibling; parent.removeChild(child); newChildren.forEach((newChild) => nextSibling ? parent.insertBefore(newChild, nextSibling) : parent.appendChild(newChild)); }; originalChildren.forEach((child) => { if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "DIV") { const divWrapper = document.createElement("div"); replaceChild(child, divWrapper); divWrapper.appendChild(child); } else if (child.childNodes.length === 0) { 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); } } }); } 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 = []; textOuter: while (currentOffset < originalString.length && linkText === false) { let string = originalString.substr(currentOffset); // Capture any escapes and code blocks at current position for (let rule of ["escape", "code", "autolink", "html"]) { let cap = this.mergedInlineGrammar[rule].regexp.exec(string); if (cap) { currentOffset += cap[0].length; continue textOuter; } } // Check for image if (string.match(this.mergedInlineGrammar.imageOpen.regexp)) { bracketLevel++; currentOffset += 2; continue textOuter; } // Check for link if (string.match(this.mergedInlineGrammar.linkOpen.regexp)) { bracketLevel++; if (!isImage) { if (this.parseLinkOrImage(string, false)) { return false; } } currentOffset += 1; continue textOuter; } // Check for closing bracket if (string.match(/^\]/)) { bracketLevel--; if (bracketLevel === 0) { linkText = originalString.substr(textOffset, currentOffset - textOffset); currentOffset++; continue textOuter; } } // Nothing matches, proceed to next char currentOffset++; } // Did we find a link text? if (linkText === false) return false; // Check 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 = this.mergedInlineGrammar.linkLabel.regexp.exec(string); if (cap) { currentOffset += cap[0].length; linkLabel.push(cap[1], cap[2], cap[3]); if (cap[this.mergedInlineGrammar.linkLabel.labelPlaceholder]) { linkRef = cap[this.mergedInlineGrammar.linkLabel.labelPlaceholder]; } else { linkRef = linkText.trim(); } } else { return false; } } else if (nextChar !== "(") { // Shortcut ref link linkRef = linkText.trim(); } else { // INLINE LINKS 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; 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; } // Process backslash escapes cap = this.mergedInlineGrammar.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; } // Process opening angle bracket if (linkDetails.length < 2 && string.match(/^</)) { if (linkDetails.length === 0) linkDetails.push(""); linkDetails[0] = linkDetails[0].concat("<"); currentOffset++; continue inlineOuter; } // Process closing angle bracket if ((linkDetails.length === 1 || linkDetails.length === 2) && string.match(/^>/)) { if (linkDetails.length === 1) linkDetails.push(""); linkDetails.push(">"); currentOffset++; continue inlineOuter; } // Process non-parenthesis delimiter for title 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; } // Process opening parenthesis 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; } // Process closing parenthesis 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(")");