tiny-markdown-editor
Version:
TinyMDE: A tiny, ultra low dependency, embeddable HTML/JavaScript Markdown editor.
1,179 lines (1,178 loc) • 64.5 kB
JavaScript
"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(")");