UNPKG

codemirror-ot

Version:

Operational Transformation adapter for CodeMirror 6.

1,465 lines (1,411 loc) 103 kB
'use strict'; var state = require('@codemirror/state'); const reconstructOp = (op, path) => { if (op && op[0] === 'files' && Array.isArray(op[1])) { const filesIndex = path.indexOf('files'); if (filesIndex !== -1) { const fileId = path[filesIndex + 1]; const textProperty = path[filesIndex + 2]; if (fileId && textProperty === 'text') { const fileOpPart = op .slice(1) .find((c) => Array.isArray(c) && c[0] === fileId && c[1] === 'text'); if (fileOpPart) { // Reconstruct the op for a single file. return [...path, fileOpPart[2]]; } } } // If it's a special multi-file op but not for this file, return null to ignore. return null; } return op; }; // This module is able to translate from CodeMirror ChangeSet to OT ops // and back, for both json0 and json1 OT types. // // Inspired by https://github.com/yjs/y-codemirror.next/blob/main/src/y-sync.js // Convert UTF-16 position (used by CodeMirror) to Unicode code point position (used by text-unicode) const utf16ToCodePoint = (str, utf16Pos) => { let codePointPos = 0; let utf16Index = 0; while (utf16Index < utf16Pos && utf16Index < str.length) { const codePoint = str.codePointAt(utf16Index); if (codePoint > 0xffff) { utf16Index += 2; // Surrogate pair takes 2 UTF-16 code units } else { utf16Index += 1; } codePointPos++; } return codePointPos; }; // Convert Unicode code point position (used by text-unicode) to UTF-16 position (used by CodeMirror) const codePointToUtf16 = (str, codePointPos) => { let utf16Index = 0; let currentCodePointPos = 0; while (currentCodePointPos < codePointPos && utf16Index < str.length) { const codePoint = str.codePointAt(utf16Index); if (codePoint > 0xffff) { utf16Index += 2; // Surrogate pair takes 2 UTF-16 code units } else { utf16Index += 1; } currentCodePointPos++; } return utf16Index; }; // Converts a CodeMirror ChangeSet to a json0 OT op. const changesToOpJSON0 = (path, changeSet, doc) => { const op = []; let offset = 0; // Used to track the position offset based on previous operations changeSet.iterChanges((fromA, toA, fromB, toB, inserted) => { const deletedText = doc.sliceString(fromA, toA, '\n'); const insertedText = inserted.sliceString(0, inserted.length, '\n'); const p = path.concat([fromA + offset]); if (deletedText) { op.push({ p, sd: deletedText }); } if (insertedText) { op.push({ p, si: insertedText }); } offset += insertedText.length; offset -= deletedText.length; }); return op; }; // Converts a CodeMirror ChangeSet to a json1 OT op. // Iterate over all changes in the ChangeSet. // See https://codemirror.net/docs/ref/#state.ChangeSet.iterChanges // See https://codemirror.net/docs/ref/#state.Text.sliceString // This was also the approach taken in the YJS CodeMirror integration. // See https://github.com/yjs/y-codemirror.next/blob/main/src/y-sync.js#L141 const changesToOpJSON1 = (path, changeSet, doc, json1, textUnicode) => { const op = []; let lastPos = 0; const fullDoc = doc.sliceString(0, doc.length, '\n'); changeSet.iterChanges((fromA, toA, fromB, toB, inserted) => { const codePointFrom = utf16ToCodePoint(fullDoc, fromA); const deletedText = doc.sliceString(fromA, toA, '\n'); const insertedText = inserted.sliceString(0, inserted.length, '\n'); if (codePointFrom > lastPos) { op.push(codePointFrom - lastPos); } if (insertedText.length > 0) { op.push(insertedText); } if (deletedText.length > 0) { op.push({ d: deletedText }); } lastPos = utf16ToCodePoint(fullDoc, toA); }); const docCodePointLength = [...fullDoc].length; if (docCodePointLength > lastPos) { op.push(docCodePointLength - lastPos); } if (op.length === 0) { return null; } const normalized = textUnicode.type.normalize(op); return json1.editOp(path, 'text-unicode', normalized); }; const opToChangesJSON0 = (op) => { const changes = []; let offset = 0; // Used to track the position offset based on previous operations for (let i = 0; i < op.length; i++) { const component = op[i]; const originalPosition = component.p[component.p.length - 1]; const adjustedPosition = originalPosition + offset; // String insert if (component.si !== undefined) { // String replacement if ( i > 0 && op[i - 1].sd !== undefined && JSON.stringify(op[i - 1].p) === JSON.stringify(component.p) ) { // Modify the previous change to be a replacement instead of an insertion if (changes[i - 1]) { changes[i - 1].insert = component.si; } // Undo the offset added by the previous change offset -= op[i - 1].sd.length; // Adjust the offset based on the length of the inserted string // offset += component.si.length; } else { changes.push({ from: adjustedPosition, to: adjustedPosition, insert: component.si, }); // offset += component.si.length; // Adjust offset for inserted string } } // String deletion (not part of a replacement) if ( component.sd !== undefined && (i === 0 || JSON.stringify(op[i - 1].p) !== JSON.stringify(component.p)) ) { changes.push({ from: adjustedPosition, to: adjustedPosition + component.sd.length, }); offset += component.sd.length; // Adjust offset for deleted string } } return changes; }; // Converts a json1 OT op to a CodeMirror ChangeSet. const opToChangesJSON1 = (op, path, originalDoc = null) => { op = reconstructOp(op, path); if (!op) return []; const changes = []; // Check if this is a move operation // Move ops have form: [prefix..., [dest_key, {d: ...}], [src_key, {p: ...}]] if (op.length >= 2) { const secondLast = op[op.length - 2]; // destination const last = op[op.length - 1]; // source if ( Array.isArray(secondLast) && Array.isArray(last) && secondLast.length === 2 && last.length === 2 && typeof secondLast[1] === 'object' && secondLast[1].d !== undefined && typeof last[1] === 'object' && last[1].p !== undefined ) { // It's a move operation - the document at this path is being moved away // So we need to delete all content from this editor changes.push({ from: 0, to: originalDoc ? originalDoc.length : 0 }); return changes; } } // The op for a text document is of the form [...path, textOp]. // The textOp is the last element. const component = op[op.length - 1]; if (typeof component === 'object' && component !== null) { const { es, r, i, p } = component; if (es !== undefined) { let position = 0; for (let i = 0; i < es.length; i++) { const subComponent = es[i]; if (typeof subComponent === 'number') { // It's a skip/retain operation. position += subComponent; } else if (typeof subComponent === 'string') { // Check if the next component is a deletion, indicating a replacement. if ( es[i + 1] && typeof es[i + 1] === 'object' && es[i + 1].d !== undefined ) { let deletedLength; if (typeof es[i + 1].d === 'string') { deletedLength = [...es[i + 1].d].length; } else if (typeof es[i + 1].d === 'number') { deletedLength = es[i + 1].d; } else { deletedLength = 0; } let utf16From, utf16To; if (originalDoc) { // Convert from Unicode code point positions to UTF-16 positions using original document utf16From = codePointToUtf16(originalDoc, position); utf16To = codePointToUtf16(originalDoc, position + deletedLength); } else { // Fallback: assume positions are the same (ASCII-only content) utf16From = position; utf16To = position + deletedLength; } changes.push({ from: utf16From, to: utf16To, insert: subComponent, }); position += deletedLength; i++; // Skip the next component since we've already handled it. } else { // It's a regular insertion. let utf16Position; if (originalDoc) { utf16Position = codePointToUtf16(originalDoc, position); } else { utf16Position = position; } changes.push({ from: utf16Position, to: utf16Position, insert: subComponent, }); } } else if (subComponent && subComponent.d !== undefined) { let deletedLength; if (typeof subComponent.d === 'number') { deletedLength = subComponent.d; } else if (typeof subComponent.d === 'string') { deletedLength = [...subComponent.d].length; } else { deletedLength = 0; } // It's a deletion. // For deletions, we can't rely on originalDoc for position conversion // because the op was generated against a different document state. // Instead, assume the positions are already correct for the current state. const utf16From = position; const utf16To = position + deletedLength; changes.push({ from: utf16From, to: utf16To, }); position += deletedLength; } } } else if (r !== undefined && i !== undefined) { // Replacement changes.push({ from: 0, to: originalDoc ? originalDoc.length : 0, insert: i, }); } else if (r !== undefined) { // Deletion changes.push({ from: 0, to: originalDoc ? originalDoc.length : 0 }); } else if (p !== undefined) { // Move (path remove) changes.push({ from: 0, to: originalDoc ? originalDoc.length : 0 }); } } return changes; }; var base = { 8: "Backspace", 9: "Tab", 10: "Enter", 12: "NumLock", 13: "Enter", 16: "Shift", 17: "Control", 18: "Alt", 20: "CapsLock", 27: "Escape", 32: " ", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 37: "ArrowLeft", 38: "ArrowUp", 39: "ArrowRight", 40: "ArrowDown", 44: "PrintScreen", 45: "Insert", 46: "Delete", 59: ";", 61: "=", 91: "Meta", 92: "Meta", 106: "*", 107: "+", 108: ",", 109: "-", 110: ".", 111: "/", 144: "NumLock", 145: "ScrollLock", 160: "Shift", 161: "Shift", 162: "Control", 163: "Control", 164: "Alt", 165: "Alt", 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" }; var shift = { 48: ")", 49: "!", 50: "@", 51: "#", 52: "$", 53: "%", 54: "^", 55: "&", 56: "*", 57: "(", 59: ":", 61: "+", 173: "_", 186: ":", 187: "+", 188: "<", 189: "_", 190: ">", 191: "?", 192: "~", 219: "{", 220: "|", 221: "}", 222: "\"" }; var chrome$1 = typeof navigator != "undefined" && /Chrome\/(\d+)/.exec(navigator.userAgent); var mac = typeof navigator != "undefined" && /Mac/.test(navigator.platform); mac || chrome$1 && +chrome$1[1] < 57; // Fill in the digit keys for (var i = 0; i < 10; i++) base[48 + i] = base[96 + i] = String(i); // The function keys for (var i = 1; i <= 24; i++) base[i + 111] = "F" + i; // And the alphabetic keys for (var i = 65; i <= 90; i++) { base[i] = String.fromCharCode(i + 32); shift[i] = String.fromCharCode(i); } // For each code that doesn't have a shift-equivalent, copy the base name for (var code in base) if (!shift.hasOwnProperty(code)) shift[code] = base[code]; function getSelection(root) { let target; // Browsers differ on whether shadow roots have a getSelection // method. If it exists, use that, otherwise, call it on the // document. if (root.nodeType == 11) { // Shadow root target = root.getSelection ? root : root.ownerDocument; } else { target = root; } return target.getSelection(); } function clientRectsFor(dom) { if (dom.nodeType == 3) return textRange(dom, 0, dom.nodeValue.length).getClientRects(); else if (dom.nodeType == 1) return dom.getClientRects(); else return []; } function domIndex(node) { for (var index = 0;; index++) { node = node.previousSibling; if (!node) return index; } } function maxOffset(node) { return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length; } function flattenRect(rect, left) { let x = left ? rect.left : rect.right; return { left: x, right: x, top: rect.top, bottom: rect.bottom }; } function scrollableParents(dom) { let doc = dom.ownerDocument, x, y; for (let cur = dom.parentNode; cur;) { if (cur == doc.body || (x && y)) { break; } else if (cur.nodeType == 1) { if (!y && cur.scrollHeight > cur.clientHeight) y = cur; if (!x && cur.scrollWidth > cur.clientWidth) x = cur; cur = cur.assignedSlot || cur.parentNode; } else if (cur.nodeType == 11) { cur = cur.host; } else { break; } } return { x, y }; } let preventScrollSupported = null; // Feature-detects support for .focus({preventScroll: true}), and uses // a fallback kludge when not supported. function focusPreventScroll(dom) { if (dom.setActive) return dom.setActive(); // in IE if (preventScrollSupported) return dom.focus(preventScrollSupported); let stack = []; for (let cur = dom; cur; cur = cur.parentNode) { stack.push(cur, cur.scrollTop, cur.scrollLeft); if (cur == cur.ownerDocument) break; } dom.focus(preventScrollSupported == null ? { get preventScroll() { preventScrollSupported = { preventScroll: true }; return true; } } : undefined); if (!preventScrollSupported) { preventScrollSupported = false; for (let i = 0; i < stack.length;) { let elt = stack[i++], top = stack[i++], left = stack[i++]; if (elt.scrollTop != top) elt.scrollTop = top; if (elt.scrollLeft != left) elt.scrollLeft = left; } } } let scratchRange; function textRange(node, from, to = from) { let range = scratchRange || (scratchRange = document.createRange()); range.setEnd(node, to); range.setStart(node, from); return range; } function dispatchKey(elt, name, code, mods) { let options = { key: name, code: name, keyCode: code, which: code, cancelable: true }; let down = new KeyboardEvent("keydown", options); down.synthetic = true; elt.dispatchEvent(down); let up = new KeyboardEvent("keyup", options); up.synthetic = true; elt.dispatchEvent(up); return down.defaultPrevented || up.defaultPrevented; } function clearAttributes(node) { while (node.attributes.length) node.removeAttributeNode(node.attributes[0]); } class DOMPos { constructor(node, offset, precise = true) { this.node = node; this.offset = offset; this.precise = precise; } static before(dom, precise) { return new DOMPos(dom.parentNode, domIndex(dom), precise); } static after(dom, precise) { return new DOMPos(dom.parentNode, domIndex(dom) + 1, precise); } } const noChildren = []; class ContentView { constructor() { this.parent = null; this.dom = null; this.flags = 2 /* ViewFlag.NodeDirty */; } get overrideDOMText() { return null; } get posAtStart() { return this.parent ? this.parent.posBefore(this) : 0; } get posAtEnd() { return this.posAtStart + this.length; } posBefore(view) { let pos = this.posAtStart; for (let child of this.children) { if (child == view) return pos; pos += child.length + child.breakAfter; } throw new RangeError("Invalid child in posBefore"); } posAfter(view) { return this.posBefore(view) + view.length; } sync(view, track) { if (this.flags & 2 /* ViewFlag.NodeDirty */) { let parent = this.dom; let prev = null, next; for (let child of this.children) { if (child.flags & 7 /* ViewFlag.Dirty */) { if (!child.dom && (next = prev ? prev.nextSibling : parent.firstChild)) { let contentView = ContentView.get(next); if (!contentView || !contentView.parent && contentView.canReuseDOM(child)) child.reuseDOM(next); } child.sync(view, track); child.flags &= -8 /* ViewFlag.Dirty */; } next = prev ? prev.nextSibling : parent.firstChild; if (track && !track.written && track.node == parent && next != child.dom) track.written = true; if (child.dom.parentNode == parent) { while (next && next != child.dom) next = rm$1(next); } else { parent.insertBefore(child.dom, next); } prev = child.dom; } next = prev ? prev.nextSibling : parent.firstChild; if (next && track && track.node == parent) track.written = true; while (next) next = rm$1(next); } else if (this.flags & 1 /* ViewFlag.ChildDirty */) { for (let child of this.children) if (child.flags & 7 /* ViewFlag.Dirty */) { child.sync(view, track); child.flags &= -8 /* ViewFlag.Dirty */; } } } reuseDOM(_dom) { } localPosFromDOM(node, offset) { let after; if (node == this.dom) { after = this.dom.childNodes[offset]; } else { let bias = maxOffset(node) == 0 ? 0 : offset == 0 ? -1 : 1; for (;;) { let parent = node.parentNode; if (parent == this.dom) break; if (bias == 0 && parent.firstChild != parent.lastChild) { if (node == parent.firstChild) bias = -1; else bias = 1; } node = parent; } if (bias < 0) after = node; else after = node.nextSibling; } if (after == this.dom.firstChild) return 0; while (after && !ContentView.get(after)) after = after.nextSibling; if (!after) return this.length; for (let i = 0, pos = 0;; i++) { let child = this.children[i]; if (child.dom == after) return pos; pos += child.length + child.breakAfter; } } domBoundsAround(from, to, offset = 0) { let fromI = -1, fromStart = -1, toI = -1, toEnd = -1; for (let i = 0, pos = offset, prevEnd = offset; i < this.children.length; i++) { let child = this.children[i], end = pos + child.length; if (pos < from && end > to) return child.domBoundsAround(from, to, pos); if (end >= from && fromI == -1) { fromI = i; fromStart = pos; } if (pos > to && child.dom.parentNode == this.dom) { toI = i; toEnd = prevEnd; break; } prevEnd = end; pos = end + child.breakAfter; } return { from: fromStart, to: toEnd < 0 ? offset + this.length : toEnd, startDOM: (fromI ? this.children[fromI - 1].dom.nextSibling : null) || this.dom.firstChild, endDOM: toI < this.children.length && toI >= 0 ? this.children[toI].dom : null }; } markDirty(andParent = false) { this.flags |= 2 /* ViewFlag.NodeDirty */; this.markParentsDirty(andParent); } markParentsDirty(childList) { for (let parent = this.parent; parent; parent = parent.parent) { if (childList) parent.flags |= 2 /* ViewFlag.NodeDirty */; if (parent.flags & 1 /* ViewFlag.ChildDirty */) return; parent.flags |= 1 /* ViewFlag.ChildDirty */; childList = false; } } setParent(parent) { if (this.parent != parent) { this.parent = parent; if (this.flags & 7 /* ViewFlag.Dirty */) this.markParentsDirty(true); } } setDOM(dom) { if (this.dom == dom) return; if (this.dom) this.dom.cmView = null; this.dom = dom; dom.cmView = this; } get rootView() { for (let v = this;;) { let parent = v.parent; if (!parent) return v; v = parent; } } replaceChildren(from, to, children = noChildren) { this.markDirty(); for (let i = from; i < to; i++) { let child = this.children[i]; if (child.parent == this && children.indexOf(child) < 0) child.destroy(); } if (children.length < 250) this.children.splice(from, to - from, ...children); else this.children = [].concat(this.children.slice(0, from), children, this.children.slice(to)); for (let i = 0; i < children.length; i++) children[i].setParent(this); } ignoreMutation(_rec) { return false; } ignoreEvent(_event) { return false; } childCursor(pos = this.length) { return new ChildCursor(this.children, pos, this.children.length); } childPos(pos, bias = 1) { return this.childCursor().findPos(pos, bias); } toString() { let name = this.constructor.name.replace("View", ""); return name + (this.children.length ? "(" + this.children.join() + ")" : this.length ? "[" + (name == "Text" ? this.text : this.length) + "]" : "") + (this.breakAfter ? "#" : ""); } static get(node) { return node.cmView; } get isEditable() { return true; } get isWidget() { return false; } get isHidden() { return false; } merge(from, to, source, hasStart, openStart, openEnd) { return false; } become(other) { return false; } canReuseDOM(other) { return other.constructor == this.constructor && !((this.flags | other.flags) & 8 /* ViewFlag.Composition */); } // When this is a zero-length view with a side, this should return a // number <= 0 to indicate it is before its position, or a // number > 0 when after its position. getSide() { return 0; } destroy() { for (let child of this.children) if (child.parent == this) child.destroy(); this.parent = null; } } ContentView.prototype.breakAfter = 0; // Remove a DOM node and return its next sibling. function rm$1(dom) { let next = dom.nextSibling; dom.parentNode.removeChild(dom); return next; } class ChildCursor { constructor(children, pos, i) { this.children = children; this.pos = pos; this.i = i; this.off = 0; } findPos(pos, bias = 1) { for (;;) { if (pos > this.pos || pos == this.pos && (bias > 0 || this.i == 0 || this.children[this.i - 1].breakAfter)) { this.off = pos - this.pos; return this; } let next = this.children[--this.i]; this.pos -= next.length + next.breakAfter; } } } function replaceRange(parent, fromI, fromOff, toI, toOff, insert, breakAtStart, openStart, openEnd) { let { children } = parent; let before = children.length ? children[fromI] : null; let last = insert.length ? insert[insert.length - 1] : null; let breakAtEnd = last ? last.breakAfter : breakAtStart; // Change within a single child if (fromI == toI && before && !breakAtStart && !breakAtEnd && insert.length < 2 && before.merge(fromOff, toOff, insert.length ? last : null, fromOff == 0, openStart, openEnd)) return; if (toI < children.length) { let after = children[toI]; // Make sure the end of the child after the update is preserved in `after` if (after && (toOff < after.length || after.breakAfter && (last === null || last === void 0 ? void 0 : last.breakAfter))) { // If we're splitting a child, separate part of it to avoid that // being mangled when updating the child before the update. if (fromI == toI) { after = after.split(toOff); toOff = 0; } // If the element after the replacement should be merged with // the last replacing element, update `content` if (!breakAtEnd && last && after.merge(0, toOff, last, true, 0, openEnd)) { insert[insert.length - 1] = after; } else { // Remove the start of the after element, if necessary, and // add it to `content`. if (toOff || after.children.length && !after.children[0].length) after.merge(0, toOff, null, false, 0, openEnd); insert.push(after); } } else if (after === null || after === void 0 ? void 0 : after.breakAfter) { // The element at `toI` is entirely covered by this range. // Preserve its line break, if any. if (last) last.breakAfter = 1; else breakAtStart = 1; } // Since we've handled the next element from the current elements // now, make sure `toI` points after that. toI++; } if (before) { before.breakAfter = breakAtStart; if (fromOff > 0) { if (!breakAtStart && insert.length && before.merge(fromOff, before.length, insert[0], false, openStart, 0)) { before.breakAfter = insert.shift().breakAfter; } else if (fromOff < before.length || before.children.length && before.children[before.children.length - 1].length == 0) { before.merge(fromOff, before.length, null, false, openStart, 0); } fromI++; } } // Try to merge widgets on the boundaries of the replacement while (fromI < toI && insert.length) { if (children[toI - 1].become(insert[insert.length - 1])) { toI--; insert.pop(); openEnd = insert.length ? 0 : openStart; } else if (children[fromI].become(insert[0])) { fromI++; insert.shift(); openStart = insert.length ? 0 : openEnd; } else { break; } } if (!insert.length && fromI && toI < children.length && !children[fromI - 1].breakAfter && children[toI].merge(0, 0, children[fromI - 1], false, openStart, openEnd)) fromI--; if (fromI < toI || insert.length) parent.replaceChildren(fromI, toI, insert); } function mergeChildrenInto(parent, from, to, insert, openStart, openEnd) { let cur = parent.childCursor(); let { i: toI, off: toOff } = cur.findPos(to, 1); let { i: fromI, off: fromOff } = cur.findPos(from, -1); let dLen = from - to; for (let view of insert) dLen += view.length; parent.length += dLen; replaceRange(parent, fromI, fromOff, toI, toOff, insert, 0, openStart, openEnd); } let nav = typeof navigator != "undefined" ? navigator : { userAgent: "", vendor: "", platform: "" }; let doc = typeof document != "undefined" ? document : { documentElement: { style: {} } }; const ie_edge = /*@__PURE__*//Edge\/(\d+)/.exec(nav.userAgent); const ie_upto10 = /*@__PURE__*//MSIE \d/.test(nav.userAgent); const ie_11up = /*@__PURE__*//Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(nav.userAgent); const ie = !!(ie_upto10 || ie_11up || ie_edge); const gecko = !ie && /*@__PURE__*//gecko\/(\d+)/i.test(nav.userAgent); const chrome = !ie && /*@__PURE__*//Chrome\/(\d+)/.exec(nav.userAgent); const webkit = "webkitFontSmoothing" in doc.documentElement.style; const safari = !ie && /*@__PURE__*//Apple Computer/.test(nav.vendor); const ios = safari && (/*@__PURE__*//Mobile\/\w+/.test(nav.userAgent) || nav.maxTouchPoints > 2); var browser = { mac: ios || /*@__PURE__*//Mac/.test(nav.platform), ie, ie_version: ie_upto10 ? doc.documentMode || 6 : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0, gecko, gecko_version: gecko ? +(/*@__PURE__*//Firefox\/(\d+)/.exec(nav.userAgent) || [0, 0])[1] : 0, chrome: !!chrome, chrome_version: chrome ? +chrome[1] : 0, ios, android: /*@__PURE__*//Android\b/.test(nav.userAgent), safari, webkit_version: webkit ? +(/*@__PURE__*//\bAppleWebKit\/(\d+)/.exec(nav.userAgent) || [0, 0])[1] : 0, tabSize: doc.documentElement.style.tabSize != null ? "tab-size" : "-moz-tab-size" }; const MaxJoinLen = 256; class TextView extends ContentView { constructor(text) { super(); this.text = text; } get length() { return this.text.length; } createDOM(textDOM) { this.setDOM(textDOM || document.createTextNode(this.text)); } sync(view, track) { if (!this.dom) this.createDOM(); if (this.dom.nodeValue != this.text) { if (track && track.node == this.dom) track.written = true; this.dom.nodeValue = this.text; } } reuseDOM(dom) { if (dom.nodeType == 3) this.createDOM(dom); } merge(from, to, source) { if ((this.flags & 8 /* ViewFlag.Composition */) || source && (!(source instanceof TextView) || this.length - (to - from) + source.length > MaxJoinLen || (source.flags & 8 /* ViewFlag.Composition */))) return false; this.text = this.text.slice(0, from) + (source ? source.text : "") + this.text.slice(to); this.markDirty(); return true; } split(from) { let result = new TextView(this.text.slice(from)); this.text = this.text.slice(0, from); this.markDirty(); result.flags |= this.flags & 8 /* ViewFlag.Composition */; return result; } localPosFromDOM(node, offset) { return node == this.dom ? offset : offset ? this.text.length : 0; } domAtPos(pos) { return new DOMPos(this.dom, pos); } domBoundsAround(_from, _to, offset) { return { from: offset, to: offset + this.length, startDOM: this.dom, endDOM: this.dom.nextSibling }; } coordsAt(pos, side) { return textCoords(this.dom, pos, side); } } class MarkView extends ContentView { constructor(mark, children = [], length = 0) { super(); this.mark = mark; this.children = children; this.length = length; for (let ch of children) ch.setParent(this); } setAttrs(dom) { clearAttributes(dom); if (this.mark.class) dom.className = this.mark.class; if (this.mark.attrs) for (let name in this.mark.attrs) dom.setAttribute(name, this.mark.attrs[name]); return dom; } canReuseDOM(other) { return super.canReuseDOM(other) && !((this.flags | other.flags) & 8 /* ViewFlag.Composition */); } reuseDOM(node) { if (node.nodeName == this.mark.tagName.toUpperCase()) { this.setDOM(node); this.flags |= 4 /* ViewFlag.AttrsDirty */ | 2 /* ViewFlag.NodeDirty */; } } sync(view, track) { if (!this.dom) this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))); else if (this.flags & 4 /* ViewFlag.AttrsDirty */) this.setAttrs(this.dom); super.sync(view, track); } merge(from, to, source, _hasStart, openStart, openEnd) { if (source && (!(source instanceof MarkView && source.mark.eq(this.mark)) || (from && openStart <= 0) || (to < this.length && openEnd <= 0))) return false; mergeChildrenInto(this, from, to, source ? source.children.slice() : [], openStart - 1, openEnd - 1); this.markDirty(); return true; } split(from) { let result = [], off = 0, detachFrom = -1, i = 0; for (let elt of this.children) { let end = off + elt.length; if (end > from) result.push(off < from ? elt.split(from - off) : elt); if (detachFrom < 0 && off >= from) detachFrom = i; off = end; i++; } let length = this.length - from; this.length = from; if (detachFrom > -1) { this.children.length = detachFrom; this.markDirty(); } return new MarkView(this.mark, result, length); } domAtPos(pos) { return inlineDOMAtPos(this, pos); } coordsAt(pos, side) { return coordsInChildren(this, pos, side); } } function textCoords(text, pos, side) { let length = text.nodeValue.length; if (pos > length) pos = length; let from = pos, to = pos, flatten = 0; if (pos == 0 && side < 0 || pos == length && side >= 0) { if (!(browser.chrome || browser.gecko)) { // These browsers reliably return valid rectangles for empty ranges if (pos) { from--; flatten = 1; } // FIXME this is wrong in RTL text else if (to < length) { to++; flatten = -1; } } } else { if (side < 0) from--; else if (to < length) to++; } let rects = textRange(text, from, to).getClientRects(); if (!rects.length) return null; let rect = rects[(flatten ? flatten < 0 : side >= 0) ? 0 : rects.length - 1]; if (browser.safari && !flatten && rect.width == 0) rect = Array.prototype.find.call(rects, r => r.width) || rect; return flatten ? flattenRect(rect, flatten < 0) : rect || null; } // Also used for collapsed ranges that don't have a placeholder widget! class WidgetView extends ContentView { static create(widget, length, side) { return new WidgetView(widget, length, side); } constructor(widget, length, side) { super(); this.widget = widget; this.length = length; this.side = side; this.prevWidget = null; } split(from) { let result = WidgetView.create(this.widget, this.length - from, this.side); this.length -= from; return result; } sync(view) { if (!this.dom || !this.widget.updateDOM(this.dom, view)) { if (this.dom && this.prevWidget) this.prevWidget.destroy(this.dom); this.prevWidget = null; this.setDOM(this.widget.toDOM(view)); if (!this.widget.editable) this.dom.contentEditable = "false"; } } getSide() { return this.side; } merge(from, to, source, hasStart, openStart, openEnd) { if (source && (!(source instanceof WidgetView) || !this.widget.compare(source.widget) || from > 0 && openStart <= 0 || to < this.length && openEnd <= 0)) return false; this.length = from + (source ? source.length : 0) + (this.length - to); return true; } become(other) { if (other instanceof WidgetView && other.side == this.side && this.widget.constructor == other.widget.constructor) { if (!this.widget.compare(other.widget)) this.markDirty(true); if (this.dom && !this.prevWidget) this.prevWidget = this.widget; this.widget = other.widget; this.length = other.length; return true; } return false; } ignoreMutation() { return true; } ignoreEvent(event) { return this.widget.ignoreEvent(event); } get overrideDOMText() { if (this.length == 0) return state.Text.empty; let top = this; while (top.parent) top = top.parent; let { view } = top, text = view && view.state.doc, start = this.posAtStart; return text ? text.slice(start, start + this.length) : state.Text.empty; } domAtPos(pos) { return (this.length ? pos == 0 : this.side > 0) ? DOMPos.before(this.dom) : DOMPos.after(this.dom, pos == this.length); } domBoundsAround() { return null; } coordsAt(pos, side) { let custom = this.widget.coordsAt(this.dom, pos, side); if (custom) return custom; let rects = this.dom.getClientRects(), rect = null; if (!rects.length) return null; let fromBack = this.side ? this.side < 0 : pos > 0; for (let i = fromBack ? rects.length - 1 : 0;; i += (fromBack ? -1 : 1)) { rect = rects[i]; if (pos > 0 ? i == 0 : i == rects.length - 1 || rect.top < rect.bottom) break; } return flattenRect(rect, !fromBack); } get isEditable() { return false; } get isWidget() { return true; } get isHidden() { return this.widget.isHidden; } destroy() { super.destroy(); if (this.dom) this.widget.destroy(this.dom); } } // These are drawn around uneditable widgets to avoid a number of // browser bugs that show up when the cursor is directly next to // uneditable inline content. class WidgetBufferView extends ContentView { constructor(side) { super(); this.side = side; } get length() { return 0; } merge() { return false; } become(other) { return other instanceof WidgetBufferView && other.side == this.side; } split() { return new WidgetBufferView(this.side); } sync() { if (!this.dom) { let dom = document.createElement("img"); dom.className = "cm-widgetBuffer"; dom.setAttribute("aria-hidden", "true"); this.setDOM(dom); } } getSide() { return this.side; } domAtPos(pos) { return this.side > 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom); } localPosFromDOM() { return 0; } domBoundsAround() { return null; } coordsAt(pos) { return this.dom.getBoundingClientRect(); } get overrideDOMText() { return state.Text.empty; } get isHidden() { return true; } } TextView.prototype.children = WidgetView.prototype.children = WidgetBufferView.prototype.children = noChildren; function inlineDOMAtPos(parent, pos) { let dom = parent.dom, { children } = parent, i = 0; for (let off = 0; i < children.length; i++) { let child = children[i], end = off + child.length; if (end == off && child.getSide() <= 0) continue; if (pos > off && pos < end && child.dom.parentNode == dom) return child.domAtPos(pos - off); if (pos <= off) break; off = end; } for (let j = i; j > 0; j--) { let prev = children[j - 1]; if (prev.dom.parentNode == dom) return prev.domAtPos(prev.length); } for (let j = i; j < children.length; j++) { let next = children[j]; if (next.dom.parentNode == dom) return next.domAtPos(0); } return new DOMPos(dom, 0); } // Assumes `view`, if a mark view, has precisely 1 child. function joinInlineInto(parent, view, open) { let last, { children } = parent; if (open > 0 && view instanceof MarkView && children.length && (last = children[children.length - 1]) instanceof MarkView && last.mark.eq(view.mark)) { joinInlineInto(last, view.children[0], open - 1); } else { children.push(view); view.setParent(parent); } parent.length += view.length; } function coordsInChildren(view, pos, side) { let before = null, beforePos = -1, after = null, afterPos = -1; function scan(view, pos) { for (let i = 0, off = 0; i < view.children.length && off <= pos; i++) { let child = view.children[i], end = off + child.length; if (end >= pos) { if (child.children.length) { scan(child, pos - off); } else if ((!after || after.isHidden && (side > 0 || onSameLine(after, child))) && (end > pos || off == end && child.getSide() > 0)) { after = child; afterPos = pos - off; } else if (off < pos || (off == end && child.getSide() < 0) && !child.isHidden) { before = child; beforePos = pos - off; } } off = end; } } scan(view, pos); let target = (side < 0 ? before : after) || before || after; if (target) return target.coordsAt(Math.max(0, target == before ? beforePos : afterPos), side); return fallbackRect(view); } function fallbackRect(view) { let last = view.dom.lastChild; if (!last) return view.dom.getBoundingClientRect(); let rects = clientRectsFor(last); return rects[rects.length - 1] || null; } function onSameLine(a, b) { let posA = a.coordsAt(0, 1), posB = b.coordsAt(0, 1); return posA && posB && posB.top < posA.bottom; } function combineAttrs(source, target) { for (let name in source) { if (name == "class" && target.class) target.class += " " + source.class; else if (name == "style" && target.style) target.style += ";" + source.style; else target[name] = source[name]; } return target; } const noAttrs = /*@__PURE__*/Object.create(null); function attrsEq(a, b, ignore) { if (a == b) return true; if (!a) a = noAttrs; if (!b) b = noAttrs; let keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length - (ignore && keysA.indexOf(ignore) > -1 ? 1 : 0) != keysB.length - (ignore && keysB.indexOf(ignore) > -1 ? 1 : 0)) return false; for (let key of keysA) { if (key != ignore && (keysB.indexOf(key) == -1 || a[key] !== b[key])) return false; } return true; } function updateAttrs(dom, prev, attrs) { let changed = false; if (prev) for (let name in prev) if (!(attrs && name in attrs)) { changed = true; if (name == "style") dom.style.cssText = ""; else dom.removeAttribute(name); } if (attrs) for (let name in attrs) if (!(prev && prev[name] == attrs[name])) { changed = true; if (name == "style") dom.style.cssText = attrs[name]; else dom.setAttribute(name, attrs[name]); } return changed; } /** The different types of blocks that can occur in an editor view. */ var BlockType = /*@__PURE__*/(function (BlockType) { /** A line of text. */ BlockType[BlockType["Text"] = 0] = "Text"; /** A block widget associated with the position after it. */ BlockType[BlockType["WidgetBefore"] = 1] = "WidgetBefore"; /** A block widget associated with the position before it. */ BlockType[BlockType["WidgetAfter"] = 2] = "WidgetAfter"; /** A block widget [replacing](https://codemirror.net/6/docs/ref/#view.Decoration^replace) a range of content. */ BlockType[BlockType["WidgetRange"] = 3] = "WidgetRange"; return BlockType})(BlockType || (BlockType = {})); /** A decoration provides information on how to draw or style a piece of content. You'll usually use it wrapped in a [`Range`](https://codemirror.net/6/docs/ref/#state.Range), which adds a start and end position. @nonabstract */ class Decoration extends state.RangeValue { constructor( /** @internal */ startSide, /** @internal */ endSide, /** @internal */ widget, /** The config object used to create this decoration. You can include additional properties in there to store metadata about your decoration. */ spec) { super(); this.startSide = startSide; this.endSide = endSide; this.widget = widget; this.spec = spec; } /** @internal */ get heightRelevant() { return false; } /** Create a mark decoration, which influences the styling of the content in its range. Nested mark decorations will cause nested DOM elements to be created. Nesting order is determined by precedence of the [facet](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), with the higher-precedence decorations creating the inner DOM nodes. Such elements are split on line boundaries and on the boundaries of lower-precedence decorations. */ static mark(spec) { return new MarkDecoration(spec); } /** Create a widget decoration, which displays a DOM element at the given position. */ static widget(spec) { let side = Math.max(-1e4, Math.min(10000, spec.side || 0)), block = !!spec.block; side += (block && !spec.inlineOrder) ? (side > 0 ? 300000000 /* Side.BlockAfter */ : -4e8 /* Side.BlockBefore */) : (side > 0 ? 100000000 /* Side.InlineAfter */ : -1e8 /* Side.InlineBefore */); return new PointDecoration(spec, side, side, block, spec.widget || null, false); } /** Create a replace decoration which replaces the given range with a widget, or simply hides it. */ static replace(spec) { let block = !!spec.block, startSide, endSide; if (spec.isBlockGap) { startSide = -5e8 /* Side.GapStart */; endSide = 400000000 /* Side.GapEnd */; } else { let { start, end } = getInclusive(spec, block); startSide = (start ? (block ? -3e8 /* Side.BlockIncStart */ : -1 /* Side.InlineIncStart */) : 500000000 /* Side.NonIncStart */) - 1; endSide = (end ? (block ? 200000000 /* Side.BlockIncEnd */ : 1 /* Side.InlineIncEnd */) : -6e8 /* Side.NonIncEnd */) + 1; } return new PointDecoration(spec, startSide, endSide, block, spec.widget || null, true); } /** Create a line decoration, which can add DOM attributes to the line starting at the given position. */ static line(spec) { return new LineDecoration(spec); } /** Build a [`DecorationSet`](https://codemirror.net/6/docs/ref/#view.DecorationSet) from the given decorated range or ranges. If the ranges aren't already sorted, pass `true` for `sort` to make the library sort them for you. */ static set(of, sort = false) { return state.RangeSet.of(of, sort); } /** @internal */ hasHeight() { return this.widget ? this.widget.estimatedHeight > -1 : false; } } /** The empty set of decorations. */ Decoration.none = state.RangeSet.empty; class MarkDecoration extends Decoration { constructor(spec) { let { start, end } = getInclusive(spec); super(start ? -1 /* Side.InlineIncStart */ : 500000000 /* Side.NonIncStart */, end ? 1 /* Side.InlineIncEnd */ : -6e8 /* Side.NonIncEnd */, null, spec); this.tagName = spec.tagName || "span"; this.class = spec.class || ""; this.attrs = spec.attributes || null; } eq(other) { var _a, _b; return this == other || other instanceof MarkDecoration && this.tagName == other.tagName && (this.class || ((_a = this.attrs) === null || _a === void 0 ? void 0 : _a.class)) == (other.class || ((_b = other.attrs) === null || _b === void 0 ? void 0 : _b.class)) && attrsEq(this.attrs, other.attrs, "class"); } range(from, to = from) { if (from >= to) throw new RangeError("Mark decorations may not be empty"); return super.range(from, to); } } MarkDecoration.prototype.point = false; class LineDecoration extends Decoration { constructor(spec) { super(-2e8 /* Side.Line */, -2e8 /* Side.Line */, null, spec); } eq(other) { return other instanceof LineDecoration && this.spec.class == other.spec.class && attrsEq(this.spec.attributes, other.spec.attributes); } range(from, to = from) { if (to != from) throw new RangeError("Line decoration ranges must be zero-length"); return super.range(from, to); } } LineDecoration.prototype.mapMode = state.MapMode.TrackBefore; LineDecoration.prototype.point = true; class PointDecoration extends Decoration { constructor(spec, startSide, endSide, block, widget, isReplace) { super(startSide, endSide, widget, spec); this.block = block; this.isReplace = isReplace;