UNPKG

codemirror-ot

Version:

Operational Transformation adapter for CodeMirror 6.

1,468 lines (1,417 loc) 84 kB
'use strict'; var state = require('@codemirror/state'); // 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) => { let op = []; let offset = 0; const fullDoc = doc.sliceString(0, doc.length, '\n'); changeSet.iterChanges((fromA, toA, fromB, toB, inserted) => { const deletedText = doc.sliceString(fromA, toA, '\n'); const insertedText = inserted.sliceString(0, inserted.length, '\n'); // Convert UTF-16 position (CodeMirror) to code point position (text-unicode) const codePointPos = utf16ToCodePoint(fullDoc, fromA) + offset; if (deletedText) { op.push(textUnicode.remove(codePointPos, deletedText)); } if (insertedText) { op.push(textUnicode.insert(codePointPos, insertedText)); } // Update offset in code point space offset += insertedText.length - deletedText.length; }); // Composes string deletion followed by string insertion // to produce a "new kind" of op component that represents // a string replacement (using only a single op component). if (op.length === 0) { return null; } op = op.reduce(textUnicode.type.compose); return json1.editOp(path, 'text-unicode', op); }; 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, originalDoc = null) => { if (!op) return []; const changes = []; for (const component of op) { const { es } = component; if (es !== undefined) { let position = 0; for (let i = 0; i < es.length; i++) { const component = es[i]; if (typeof component === 'number') { // It's a skip/retain operation. position += component; } else if (typeof component === '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 deletedText = typeof es[i + 1].d === 'string' ? es[i + 1].d : ''; 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 + deletedText.length, ); } else { // Fallback: assume positions are the same (ASCII-only content) utf16From = position; utf16To = position + deletedText.length; } changes.push({ from: utf16From, to: utf16To, insert: component, }); position += deletedText.length; 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: component, }); } } else if (component && component.d !== undefined) { if (typeof component.d === 'number') { // It's a deletion by count. let utf16From, utf16To; if (originalDoc) { utf16From = codePointToUtf16(originalDoc, position); utf16To = codePointToUtf16(originalDoc, position + component.d); } else { utf16From = position; utf16To = position + component.d; } changes.push({ from: utf16From, to: utf16To, }); position += component.d; } else if (typeof component.d === 'string') { // It's a deletion of a specific string. let utf16From, utf16To; if (originalDoc) { utf16From = codePointToUtf16(originalDoc, position); utf16To = codePointToUtf16( originalDoc, position + component.d.length, ); } else { utf16From = position; utf16To = position + component.d.length; } changes.push({ from: utf16From, to: utf16To, }); position += component.d.length; } } } } } 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; } const Rect0 = { left: 0, right: 0, top: 0, bottom: 0 }; function flattenRect(rect, left) { let x = left ? rect.left : rect.right; return { left: x, right: x, top: rect.top, bottom: rect.bottom }; } function scrollableParent(dom) { let doc = dom.ownerDocument; for (let cur = dom.parentNode; cur;) { if (cur == doc.body) { break; } else if (cur.nodeType == 1) { if (cur.scrollHeight > cur.clientHeight || cur.scrollWidth > cur.clientWidth) return cur; cur = cur.assignedSlot || cur.parentNode; } else if (cur.nodeType == 11) { cur = cur.host; } else { break; } } return null; } 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 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.dirty = 2 /* Dirty.Node */; } 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; } // Will return a rectangle directly before (when side < 0), after // (side > 0) or directly on (when the browser supports it) the // given position. coordsAt(_pos, _side) { return null; } sync(view, track) { if (this.dirty & 2 /* Dirty.Node */) { let parent = this.dom; let prev = null, next; for (let child of this.children) { if (child.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.dirty = 0 /* Dirty.Not */; } 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.dirty & 1 /* Dirty.Child */) { for (let child of this.children) if (child.dirty) { child.sync(view, track); child.dirty = 0 /* Dirty.Not */; } } } 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.dirty |= 2 /* Dirty.Node */; this.markParentsDirty(andParent); } markParentsDirty(childList) { for (let parent = this.parent; parent; parent = parent.parent) { if (childList) parent.dirty |= 2 /* Dirty.Node */; if (parent.dirty & 1 /* Dirty.Child */) return; parent.dirty |= 1 /* Dirty.Child */; childList = false; } } setParent(parent) { if (this.parent != parent) { this.parent = parent; if (this.dirty) this.markParentsDirty(true); } } setDOM(dom) { 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) child.destroy(); } this.children.splice(from, to - from, ...children); 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; } merge(from, to, source, hasStart, openStart, openEnd) { return false; } become(other) { return false; } canReuseDOM(other) { return other.constructor == this.constructor; } // 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() { 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) { // 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.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(navigator.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 (source && (!(source instanceof TextView) || this.length - (to - from) + source.length > MaxJoinLen)) 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(); 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; } reuseDOM(node) { if (node.nodeName == this.mark.tagName.toUpperCase()) { this.setDOM(node); this.dirty |= 4 /* Dirty.Attrs */ | 2 /* Dirty.Node */; } } sync(view, track) { if (!this.dom) this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))); else if (this.dirty & 4 /* Dirty.Attrs */) 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 : [], 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 Rect0; 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 { constructor(widget, length, side) { super(); this.widget = widget; this.length = length; this.side = side; this.prevWidget = null; } static create(widget, length, side) { return new (widget.customView || WidgetView)(widget, length, side); } 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)); 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.length == this.length && other instanceof WidgetView && other.side == this.side) { if (this.widget.constructor == other.widget.constructor) { if (!this.widget.eq(other.widget)) this.markDirty(true); if (this.dom && !this.prevWidget) this.prevWidget = this.widget; this.widget = other.widget; 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 pos == 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom, pos == this.length); } domBoundsAround() { return null; } coordsAt(pos, side) { let rects = this.dom.getClientRects(), rect = null; if (!rects.length) return Rect0; for (let i = pos > 0 ? rects.length - 1 : 0;; i += (pos > 0 ? -1 : 1)) { rect = rects[i]; if (pos > 0 ? i == 0 : i == rects.length - 1 || rect.top < rect.bottom) break; } return this.length ? rect : flattenRect(rect, this.side > 0); } get isEditable() { return false; } get isWidget() { return true; } 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 DOMPos.before(this.dom); } localPosFromDOM() { return 0; } domBoundsAround() { return null; } coordsAt(pos) { let imgRect = this.dom.getBoundingClientRect(); // Since the <img> height doesn't correspond to text height, try // to borrow the height from some sibling node. let siblingRect = inlineSiblingRect(this, this.side > 0 ? -1 : 1); return siblingRect && siblingRect.top < imgRect.bottom && siblingRect.bottom > imgRect.top ? { left: imgRect.left, right: imgRect.right, top: siblingRect.top, bottom: siblingRect.bottom } : imgRect; } get overrideDOMText() { return state.Text.empty; } } TextView.prototype.children = WidgetView.prototype.children = WidgetBufferView.prototype.children = noChildren; function inlineSiblingRect(view, side) { let parent = view.parent, index = parent ? parent.children.indexOf(view) : -1; while (parent && index >= 0) { if (side < 0 ? index > 0 : index < parent.children.length) { let next = parent.children[index + side]; if (next instanceof TextView) { let nextRect = next.coordsAt(side < 0 ? next.length : 0, side); if (nextRect) return nextRect; } index += side; } else if (parent instanceof MarkView && parent.parent) { index = parent.parent.children.indexOf(parent) + (side < 0 ? 0 : 1); parent = parent.parent; } else { let last = parent.dom.lastChild; if (last && last.nodeName == "BR") return last.getClientRects()[0]; break; } } return undefined; } 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 && (end > pos || off == end && child.getSide() > 0)) { after = child; afterPos = pos - off; } else if (off < pos || (off == end && child.getSide() < 0)) { 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 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; } function attrsEq(a, b) { if (a == b) return true; if (!a || !b) return false; let keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length != keysB.length) return false; for (let key of keysA) { if (keysB.indexOf(key) == -1 || a[key] !== b[key]) return false; } return true; } function updateAttrs(dom, prev, attrs) { let changed = null; if (prev) for (let name in prev) if (!(attrs && name in attrs)) dom.removeAttribute(changed = name); if (attrs) for (let name in attrs) if (!(prev && prev[name] == attrs[name])) dom.setAttribute(changed = 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 = spec.side || 0, block = !!spec.block; side += block ? (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) { return this == other || other instanceof MarkDecoration && this.tagName == other.tagName && this.class == other.class && attrsEq(this.attrs, other.attrs); } 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; this.mapMode = !block ? state.MapMode.TrackDel : startSide <= 0 ? state.MapMode.TrackBefore : state.MapMode.TrackAfter; } // Only relevant when this.block == true get type() { return this.startSide < this.endSide ? BlockType.WidgetRange : this.startSide <= 0 ? BlockType.WidgetBefore : BlockType.WidgetAfter; } get heightRelevant() { return this.block || !!this.widget && this.widget.estimatedHeight >= 5; } eq(other) { return other instanceof PointDecoration && widgetsEq(this.widget, other.widget) && this.block == other.block && this.startSide == other.startSide && this.endSide == other.endSide; } range(from, to = from) { if (this.isReplace && (from > to || (from == to && this.startSide > 0 && this.endSide <= 0))) throw new RangeError("Invalid range for replacement decoration"); if (!this.isReplace && to != from) throw new RangeError("Widget decorations can only have zero-length ranges"); return super.range(from, to); } } PointDecoration.prototype.point = true; function getInclusive(spec, block = false) { let { inclusiveStart: start, inclusiveEnd: end } = spec; if (start == null) start = spec.inclusive; if (end == null) end = spec.inclusive; return { start: start !== null && start !== void 0 ? start : block, end: end !== null && end !== void 0 ? end : block }; } function widgetsEq(a, b) { return a == b || !!(a && b && a.compare(b)); } class LineView extends ContentView { constructor() { super(...arguments); this.children = []; this.length = 0; this.prevAttrs = undefined; this.attrs = null; this.breakAfter = 0; } // Consumes source merge(from, to, source, hasStart, openStart, openEnd) { if (source) { if (!(source instanceof LineView)) return false; if (!this.dom) source.transferDOM(this); // Reuse source.dom when appropriate } if (hasStart) this.setDeco(source ? source.attrs : null); mergeChildrenInto(this, from, to, source ? source.children : [], openStart, openEnd); return true; } split(at) { let end = new LineView; end.breakAfter = this.breakAfter; if (this.length == 0) return end; let { i, off } = this.childPos(at); if (off) { end.append(this.children[i].split(off), 0); this.children[i].merge(off, this.children[i].length, null, false, 0, 0); i++; } for (let j = i; j < this.children.length; j++) end.append(this.children[j], 0); while (i > 0 && this.children[i - 1].length == 0) this.children[--i].destroy(); this.children.length = i; this.markDirty(); this.length = at; return end; } transferDOM(other) { if (!this.dom) return; this.markDirty(); other.s