tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

viewdesc.ts (63650B)


      1 import {DOMSerializer, Fragment, Mark, Node, TagParseRule} from "prosemirror-model"
      2 import {TextSelection} from "prosemirror-state"
      3 
      4 import {domIndex, isEquivalentPosition, DOMNode} from "./dom"
      5 import * as browser from "./browser"
      6 import {Decoration, DecorationSource, WidgetConstructor, WidgetType, NodeType} from "./decoration"
      7 import {EditorView} from "./index"
      8 
      9 declare global {
     10  interface Node { 
     11    /// @internal
     12    pmViewDesc?: ViewDesc 
     13  }
     14 }
     15 
     16 /// A ViewMutationRecord represents a DOM
     17 /// [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
     18 /// or a selection change happens within the view. When the change is
     19 /// a selection change, the record will have a `type` property of
     20 /// `"selection"` (which doesn't occur for native mutation records).
     21 export type ViewMutationRecord = MutationRecord | { type: "selection", target: DOMNode }
     22 
     23 /// By default, document nodes are rendered using the result of the
     24 /// [`toDOM`](#model.NodeSpec.toDOM) method of their spec, and managed
     25 /// entirely by the editor. For some use cases, such as embedded
     26 /// node-specific editing interfaces, you want more control over
     27 /// the behavior of a node's in-editor representation, and need to
     28 /// [define](#view.EditorProps.nodeViews) a custom node view.
     29 ///
     30 /// Objects returned as node views must conform to this interface.
     31 export interface NodeView {
     32  /// The outer DOM node that represents the document node.
     33  dom: DOMNode
     34 
     35  /// The DOM node that should hold the node's content. Only meaningful
     36  /// if the node view also defines a `dom` property and if its node
     37  /// type is not a leaf node type. When this is present, ProseMirror
     38  /// will take care of rendering the node's children into it. When it
     39  /// is not present, the node view itself is responsible for rendering
     40  /// (or deciding not to render) its child nodes.
     41  contentDOM?: HTMLElement | null
     42 
     43  /// When given, this will be called when the view is updating
     44  /// itself. It will be given a node, an array of active decorations
     45  /// around the node (which are automatically drawn, and the node
     46  /// view may ignore if it isn't interested in them), and a
     47  /// [decoration source](#view.DecorationSource) that represents any
     48  /// decorations that apply to the content of the node (which again
     49  /// may be ignored). It should return true if it was able to update
     50  /// to that node, and false otherwise. If the node view has a
     51  /// `contentDOM` property (or no `dom` property), updating its child
     52  /// nodes will be handled by ProseMirror.
     53  update?: (node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource) => boolean
     54 
     55  /// By default, `update` will only be called when a node of the same
     56  /// node type appears in this view's position. When you set this to
     57  /// true, it will be called for any node, making it possible to have
     58  /// a node view that representsmultiple types of nodes. You will
     59  /// need to check the type of the nodes you get in `update` and
     60  /// return `false` for types you cannot handle.
     61  multiType?: boolean
     62 
     63  /// Can be used to override the way the node's selected status (as a
     64  /// node selection) is displayed.
     65  selectNode?: () => void
     66 
     67  /// When defining a `selectNode` method, you should also provide a
     68  /// `deselectNode` method to remove the effect again.
     69  deselectNode?: () => void
     70 
     71  /// This will be called to handle setting the selection inside the
     72  /// node. The `anchor` and `head` positions are relative to the start
     73  /// of the node. By default, a DOM selection will be created between
     74  /// the DOM positions corresponding to those positions, but if you
     75  /// override it you can do something else.
     76  setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void
     77 
     78  /// Can be used to prevent the editor view from trying to handle some
     79  /// or all DOM events that bubble up from the node view. Events for
     80  /// which this returns true are not handled by the editor.
     81  stopEvent?: (event: Event) => boolean
     82 
     83  /// Called when a [mutation](#view.ViewMutationRecord) happens within the
     84  /// view. Return false if the editor should re-read the selection or re-parse
     85  /// the range around the mutation, true if it can safely be ignored.
     86  ignoreMutation?: (mutation: ViewMutationRecord) => boolean
     87 
     88  /// Called when the node view is removed from the editor or the whole
     89  /// editor is destroyed.
     90  destroy?: () => void
     91 }
     92 
     93 /// By default, document marks are rendered using the result of the
     94 /// [`toDOM`](#model.MarkSpec.toDOM) method of their spec, and managed entirely
     95 /// by the editor. For some use cases, you want more control over the behavior
     96 /// of a mark's in-editor representation, and need to
     97 /// [define](#view.EditorProps.markViews) a custom mark view.
     98 ///
     99 /// Objects returned as mark views must conform to this interface.
    100 export interface MarkView {
    101  /// The outer DOM node that represents the document node.
    102  dom: DOMNode
    103 
    104  /// The DOM node that should hold the mark's content. When this is not
    105  /// present, the `dom` property is used as the content DOM.
    106  contentDOM?: HTMLElement | null
    107 
    108  /// Called when a [mutation](#view.ViewMutationRecord) happens within the
    109  /// view. Return false if the editor should re-read the selection or re-parse
    110  /// the range around the mutation, true if it can safely be ignored.
    111  ignoreMutation?: (mutation: ViewMutationRecord) => boolean
    112 
    113  
    114  /// Called when the mark view is removed from the editor or the whole
    115  /// editor is destroyed.
    116  destroy?: () => void
    117 }
    118 
    119 // View descriptions are data structures that describe the DOM that is
    120 // used to represent the editor's content. They are used for:
    121 //
    122 // - Incremental redrawing when the document changes
    123 //
    124 // - Figuring out what part of the document a given DOM position
    125 //   corresponds to
    126 //
    127 // - Wiring in custom implementations of the editing interface for a
    128 //   given node
    129 //
    130 // They form a doubly-linked mutable tree, starting at `view.docView`.
    131 
    132 const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3
    133 
    134 // Superclass for the various kinds of descriptions. Defines their
    135 // basic structure and shared methods.
    136 export class ViewDesc {
    137  dirty = NOT_DIRTY
    138  declare node: Node | null
    139 
    140  constructor(
    141    public parent: ViewDesc | undefined,
    142    public children: ViewDesc[],
    143    public dom: DOMNode,
    144    // This is the node that holds the child views. It may be null for
    145    // descs that don't have children.
    146    public contentDOM: HTMLElement | null
    147  ) {
    148    // An expando property on the DOM node provides a link back to its
    149    // description.
    150    dom.pmViewDesc = this
    151  }
    152 
    153  // Used to check whether a given description corresponds to a
    154  // widget/mark/node.
    155  matchesWidget(widget: Decoration) { return false }
    156  matchesMark(mark: Mark) { return false }
    157  matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource) { return false }
    158  matchesHack(nodeName: string) { return false }
    159 
    160  // When parsing in-editor content (in domchange.js), we allow
    161  // descriptions to determine the parse rules that should be used to
    162  // parse them.
    163  parseRule(): Omit<TagParseRule, "tag"> | null { return null }
    164 
    165  // Used by the editor's event handler to ignore events that come
    166  // from certain descs.
    167  stopEvent(event: Event) { return false }
    168 
    169  // The size of the content represented by this desc.
    170  get size() {
    171    let size = 0
    172    for (let i = 0; i < this.children.length; i++) size += this.children[i].size
    173    return size
    174  }
    175 
    176  // For block nodes, this represents the space taken up by their
    177  // start/end tokens.
    178  get border() { return 0 }
    179 
    180  destroy() {
    181    this.parent = undefined
    182    if (this.dom.pmViewDesc == this) this.dom.pmViewDesc = undefined
    183    for (let i = 0; i < this.children.length; i++)
    184      this.children[i].destroy()
    185  }
    186 
    187  posBeforeChild(child: ViewDesc): number {
    188    for (let i = 0, pos = this.posAtStart;; i++) {
    189      let cur = this.children[i]
    190      if (cur == child) return pos
    191      pos += cur.size
    192    }
    193  }
    194 
    195  get posBefore() {
    196    return this.parent!.posBeforeChild(this)
    197  }
    198 
    199  get posAtStart() {
    200    return this.parent ? this.parent.posBeforeChild(this) + this.border : 0
    201  }
    202 
    203  get posAfter() {
    204    return this.posBefore + this.size
    205  }
    206 
    207  get posAtEnd() {
    208    return this.posAtStart + this.size - 2 * this.border
    209  }
    210 
    211  localPosFromDOM(dom: DOMNode, offset: number, bias: number): number {
    212    // If the DOM position is in the content, use the child desc after
    213    // it to figure out a position.
    214    if (this.contentDOM && this.contentDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode)) {
    215      if (bias < 0) {
    216        let domBefore, desc: ViewDesc | undefined
    217        if (dom == this.contentDOM) {
    218          domBefore = dom.childNodes[offset - 1]
    219        } else {
    220          while (dom.parentNode != this.contentDOM) dom = dom.parentNode!
    221          domBefore = dom.previousSibling
    222        }
    223        while (domBefore && !((desc = domBefore.pmViewDesc) && desc.parent == this)) domBefore = domBefore.previousSibling
    224        return domBefore ? this.posBeforeChild(desc!) + desc!.size : this.posAtStart
    225      } else {
    226        let domAfter, desc: ViewDesc | undefined
    227        if (dom == this.contentDOM) {
    228          domAfter = dom.childNodes[offset]
    229        } else {
    230          while (dom.parentNode != this.contentDOM) dom = dom.parentNode!
    231          domAfter = dom.nextSibling
    232        }
    233        while (domAfter && !((desc = domAfter.pmViewDesc) && desc.parent == this)) domAfter = domAfter.nextSibling
    234        return domAfter ? this.posBeforeChild(desc!) : this.posAtEnd
    235      }
    236    }
    237    // Otherwise, use various heuristics, falling back on the bias
    238    // parameter, to determine whether to return the position at the
    239    // start or at the end of this view desc.
    240    let atEnd
    241    if (dom == this.dom && this.contentDOM) {
    242      atEnd = offset > domIndex(this.contentDOM)
    243    } else if (this.contentDOM && this.contentDOM != this.dom && this.dom.contains(this.contentDOM)) {
    244      atEnd = dom.compareDocumentPosition(this.contentDOM) & 2
    245    } else if (this.dom.firstChild) {
    246      if (offset == 0) for (let search = dom;; search = search.parentNode!) {
    247        if (search == this.dom) { atEnd = false; break }
    248        if (search.previousSibling) break
    249      }
    250      if (atEnd == null && offset == dom.childNodes.length) for (let search = dom;; search = search.parentNode!) {
    251        if (search == this.dom) { atEnd = true; break }
    252        if (search.nextSibling) break
    253      }
    254    }
    255    return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart
    256  }
    257 
    258  // Scan up the dom finding the first desc that is a descendant of
    259  // this one.
    260  nearestDesc(dom: DOMNode): ViewDesc | undefined
    261  nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined
    262  nearestDesc(dom: DOMNode, onlyNodes: boolean = false) {
    263    for (let first = true, cur: DOMNode | null = dom; cur; cur = cur.parentNode) {
    264      let desc = this.getDesc(cur), nodeDOM
    265      if (desc && (!onlyNodes || desc.node)) {
    266        // If dom is outside of this desc's nodeDOM, don't count it.
    267        if (first && (nodeDOM = (desc as NodeViewDesc).nodeDOM) &&
    268            !(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom))
    269          first = false
    270        else
    271          return desc
    272      }
    273    }
    274  }
    275 
    276  getDesc(dom: DOMNode) {
    277    let desc = dom.pmViewDesc
    278    for (let cur: ViewDesc | undefined = desc; cur; cur = cur.parent) if (cur == this) return desc
    279  }
    280 
    281  posFromDOM(dom: DOMNode, offset: number, bias: number) {
    282    for (let scan: DOMNode | null = dom; scan; scan = scan.parentNode) {
    283      let desc = this.getDesc(scan)
    284      if (desc) return desc.localPosFromDOM(dom, offset, bias)
    285    }
    286    return -1
    287  }
    288 
    289  // Find the desc for the node after the given pos, if any. (When a
    290  // parent node overrode rendering, there might not be one.)
    291  descAt(pos: number): ViewDesc | undefined {
    292    for (let i = 0, offset = 0; i < this.children.length; i++) {
    293      let child = this.children[i], end = offset + child.size
    294      if (offset == pos && end != offset) {
    295        while (!child.border && child.children.length) {
    296          for (let i = 0; i < child.children.length; i++) {
    297            let inner = child.children[i]
    298            if (inner.size) { child = inner; break }
    299          }
    300        }
    301        return child
    302      }
    303      if (pos < end) return child.descAt(pos - offset - child.border)
    304      offset = end
    305    }
    306  }
    307 
    308  domFromPos(pos: number, side: number): {node: DOMNode, offset: number, atom?: number} {
    309    if (!this.contentDOM) return {node: this.dom, offset: 0, atom: pos + 1}
    310    // First find the position in the child array
    311    let i = 0, offset = 0
    312    for (let curPos = 0; i < this.children.length; i++) {
    313      let child = this.children[i], end = curPos + child.size
    314      if (end > pos || child instanceof TrailingHackViewDesc) { offset = pos - curPos; break }
    315      curPos = end
    316    }
    317    // If this points into the middle of a child, call through
    318    if (offset) return this.children[i].domFromPos(offset - this.children[i].border, side)
    319    // Go back if there were any zero-length widgets with side >= 0 before this point
    320    for (let prev; i && !(prev = this.children[i - 1]).size && prev instanceof WidgetViewDesc && prev.side >= 0; i--) {}
    321    // Scan towards the first useable node
    322    if (side <= 0) {
    323      let prev, enter = true
    324      for (;; i--, enter = false) {
    325        prev = i ? this.children[i - 1] : null
    326        if (!prev || prev.dom.parentNode == this.contentDOM) break
    327      }
    328      if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side)
    329      return {node: this.contentDOM, offset: prev ? domIndex(prev.dom) + 1 : 0}
    330    } else {
    331      let next, enter = true
    332      for (;; i++, enter = false) {
    333        next = i < this.children.length ? this.children[i] : null
    334        if (!next || next.dom.parentNode == this.contentDOM) break
    335      }
    336      if (next && enter && !next.border && !next.domAtom) return next.domFromPos(0, side)
    337      return {node: this.contentDOM, offset: next ? domIndex(next.dom) : this.contentDOM.childNodes.length}
    338    }
    339  }
    340 
    341  // Used to find a DOM range in a single parent for a given changed
    342  // range.
    343  parseRange(
    344    from: number, to: number, base = 0
    345  ): {node: DOMNode, from: number, to: number, fromOffset: number, toOffset: number} {
    346    if (this.children.length == 0)
    347      return {node: this.contentDOM!, from, to, fromOffset: 0, toOffset: this.contentDOM!.childNodes.length}
    348 
    349    let fromOffset = -1, toOffset = -1
    350    for (let offset = base, i = 0;; i++) {
    351      let child = this.children[i], end = offset + child.size
    352      if (fromOffset == -1 && from <= end) {
    353        let childBase = offset + child.border
    354        // FIXME maybe descend mark views to parse a narrower range?
    355        if (from >= childBase && to <= end - child.border && child.node &&
    356            child.contentDOM && this.contentDOM!.contains(child.contentDOM))
    357          return child.parseRange(from, to, childBase)
    358 
    359        from = offset
    360        for (let j = i; j > 0; j--) {
    361          let prev = this.children[j - 1]
    362          if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) {
    363            fromOffset = domIndex(prev.dom) + 1
    364            break
    365          }
    366          from -= prev.size
    367        }
    368        if (fromOffset == -1) fromOffset = 0
    369      }
    370      if (fromOffset > -1 && (end > to || i == this.children.length - 1)) {
    371        to = end
    372        for (let j = i + 1; j < this.children.length; j++) {
    373          let next = this.children[j]
    374          if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) {
    375            toOffset = domIndex(next.dom)
    376            break
    377          }
    378          to += next.size
    379        }
    380        if (toOffset == -1) toOffset = this.contentDOM!.childNodes.length
    381        break
    382      }
    383      offset = end
    384    }
    385    return {node: this.contentDOM!, from, to, fromOffset, toOffset}
    386  }
    387 
    388  emptyChildAt(side: number): boolean {
    389    if (this.border || !this.contentDOM || !this.children.length) return false
    390    let child = this.children[side < 0 ? 0 : this.children.length - 1]
    391    return child.size == 0 || child.emptyChildAt(side)
    392  }
    393 
    394  domAfterPos(pos: number): DOMNode {
    395    let {node, offset} = this.domFromPos(pos, 0)
    396    if (node.nodeType != 1 || offset == node.childNodes.length)
    397      throw new RangeError("No node after pos " + pos)
    398    return node.childNodes[offset]
    399  }
    400 
    401  // View descs are responsible for setting any selection that falls
    402  // entirely inside of them, so that custom implementations can do
    403  // custom things with the selection. Note that this falls apart when
    404  // a selection starts in such a node and ends in another, in which
    405  // case we just use whatever domFromPos produces as a best effort.
    406  setSelection(anchor: number, head: number, view: EditorView, force = false): void {
    407    // If the selection falls entirely in a child, give it to that child
    408    let from = Math.min(anchor, head), to = Math.max(anchor, head)
    409    for (let i = 0, offset = 0; i < this.children.length; i++) {
    410      let child = this.children[i], end = offset + child.size
    411      if (from > offset && to < end)
    412        return child.setSelection(anchor - offset - child.border, head - offset - child.border, view, force)
    413      offset = end
    414    }
    415 
    416    let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1)
    417    let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1)
    418    let domSel = (view.root as Document).getSelection()!
    419    let selRange = view.domSelectionRange()
    420 
    421    let brKludge = false
    422    // On Firefox, using Selection.collapse to put the cursor after a
    423    // BR node for some reason doesn't always work (#1073). On Safari,
    424    // the cursor sometimes inexplicable visually lags behind its
    425    // reported position in such situations (#1092).
    426    if ((browser.gecko || browser.safari) && anchor == head) {
    427      let {node, offset} = anchorDOM
    428      if (node.nodeType == 3) {
    429        brKludge = !!(offset && node.nodeValue![offset - 1] == "\n")
    430        // Issue #1128
    431        if (brKludge && offset == node.nodeValue!.length) {
    432           for (let scan: DOMNode | null = node, after; scan; scan = scan.parentNode) {
    433            if (after = scan.nextSibling) {
    434              if (after.nodeName == "BR")
    435                anchorDOM = headDOM = {node: after.parentNode!, offset: domIndex(after) + 1}
    436              break
    437            }
    438            let desc = scan.pmViewDesc
    439            if (desc && desc.node && desc.node.isBlock) break
    440          }
    441        }
    442      } else {
    443        let prev = node.childNodes[offset - 1]
    444        brKludge = prev && (prev.nodeName == "BR" || (prev as HTMLElement).contentEditable == "false")
    445      }
    446    }
    447    // Firefox can act strangely when the selection is in front of an
    448    // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536
    449    if (browser.gecko && selRange.focusNode && selRange.focusNode != headDOM.node && selRange.focusNode.nodeType == 1) {
    450      let after = selRange.focusNode.childNodes[selRange.focusOffset]
    451      if (after && (after as HTMLElement).contentEditable == "false") force = true
    452    }
    453 
    454    if (!(force || brKludge && browser.safari) &&
    455        isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode!, selRange.anchorOffset) &&
    456        isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode!, selRange.focusOffset))
    457      return
    458 
    459    // Selection.extend can be used to create an 'inverted' selection
    460    // (one where the focus is before the anchor), but not all
    461    // browsers support it yet.
    462    let domSelExtended = false
    463    if ((domSel.extend || anchor == head) && !(brKludge && browser.gecko)) {
    464      domSel.collapse(anchorDOM.node, anchorDOM.offset)
    465      try {
    466        if (anchor != head)
    467          domSel.extend(headDOM.node, headDOM.offset)
    468        domSelExtended = true
    469      } catch (_) {
    470        // In some cases with Chrome the selection is empty after calling
    471        // collapse, even when it should be valid. This appears to be a bug, but
    472        // it is difficult to isolate. If this happens fallback to the old path
    473        // without using extend.
    474        // Similarly, this could crash on Safari if the editor is hidden, and
    475        // there was no selection.
    476      }
    477    }
    478    if (!domSelExtended) {
    479      if (anchor > head) { let tmp = anchorDOM; anchorDOM = headDOM; headDOM = tmp }
    480      let range = document.createRange()
    481      range.setEnd(headDOM.node, headDOM.offset)
    482      range.setStart(anchorDOM.node, anchorDOM.offset)
    483      domSel.removeAllRanges()
    484      domSel.addRange(range)
    485    }
    486  }
    487 
    488  ignoreMutation(mutation: ViewMutationRecord): boolean {
    489    return !this.contentDOM && mutation.type != "selection"
    490  }
    491 
    492  get contentLost() {
    493    return this.contentDOM && this.contentDOM != this.dom && !this.dom.contains(this.contentDOM)
    494  }
    495 
    496  // Remove a subtree of the element tree that has been touched
    497  // by a DOM change, so that the next update will redraw it.
    498  markDirty(from: number, to: number) {
    499    for (let offset = 0, i = 0; i < this.children.length; i++) {
    500      let child = this.children[i], end = offset + child.size
    501      if (offset == end ? from <= end && to >= offset : from < end && to > offset) {
    502        let startInside = offset + child.border, endInside = end - child.border
    503        if (from >= startInside && to <= endInside) {
    504          this.dirty = from == offset || to == end ? CONTENT_DIRTY : CHILD_DIRTY
    505          if (from == startInside && to == endInside &&
    506              (child.contentLost || child.dom.parentNode != this.contentDOM)) child.dirty = NODE_DIRTY
    507          else child.markDirty(from - startInside, to - startInside)
    508          return
    509        } else {
    510          child.dirty = child.dom == child.contentDOM && child.dom.parentNode == this.contentDOM && !child.children.length
    511            ? CONTENT_DIRTY : NODE_DIRTY
    512        }
    513      }
    514      offset = end
    515    }
    516    this.dirty = CONTENT_DIRTY
    517  }
    518 
    519  markParentsDirty() {
    520    let level = 1
    521    for (let node = this.parent; node; node = node.parent, level++) {
    522      let dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY
    523      if (node.dirty < dirty) node.dirty = dirty
    524    }
    525  }
    526 
    527  get domAtom() { return false }
    528 
    529  get ignoreForCoords() { return false }
    530 
    531  get ignoreForSelection() { return false }
    532 
    533  isText(text: string) { return false }
    534 }
    535 
    536 // A widget desc represents a widget decoration, which is a DOM node
    537 // drawn between the document nodes.
    538 class WidgetViewDesc extends ViewDesc {
    539  constructor(parent: ViewDesc, readonly widget: Decoration, view: EditorView, pos: number) {
    540    let self: WidgetViewDesc, dom = (widget.type as any).toDOM as WidgetConstructor
    541    if (typeof dom == "function") dom = dom(view, () => {
    542      if (!self) return pos
    543      if (self.parent) return self.parent.posBeforeChild(self)
    544    })
    545    if (!widget.type.spec.raw) {
    546      if (dom.nodeType != 1) {
    547        let wrap = document.createElement("span")
    548        wrap.appendChild(dom)
    549        dom = wrap
    550      }
    551      ;(dom as HTMLElement).contentEditable = "false"
    552      ;(dom as HTMLElement).classList.add("ProseMirror-widget")
    553    }
    554    super(parent, [], dom, null)
    555    this.widget = widget
    556    self = this
    557  }
    558 
    559  matchesWidget(widget: Decoration) {
    560    return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type)
    561  }
    562 
    563  parseRule() { return {ignore: true} }
    564 
    565  stopEvent(event: Event) {
    566    let stop = this.widget.spec.stopEvent
    567    return stop ? stop(event) : false
    568  }
    569 
    570  ignoreMutation(mutation: ViewMutationRecord) {
    571    return mutation.type != "selection" || this.widget.spec.ignoreSelection
    572  }
    573 
    574  destroy() {
    575    this.widget.type.destroy(this.dom)
    576    super.destroy()
    577  }
    578 
    579  get domAtom() { return true }
    580 
    581  get ignoreForSelection() { return !!this.widget.type.spec.relaxedSide }
    582 
    583  get side() { return (this.widget.type as any).side as number }
    584 }
    585 
    586 class CompositionViewDesc extends ViewDesc {
    587  constructor(parent: ViewDesc, dom: DOMNode, readonly textDOM: Text, readonly text: string) {
    588    super(parent, [], dom, null)
    589  }
    590 
    591  get size() { return this.text.length }
    592 
    593  localPosFromDOM(dom: DOMNode, offset: number) {
    594    if (dom != this.textDOM) return this.posAtStart + (offset ? this.size : 0)
    595    return this.posAtStart + offset
    596  }
    597 
    598  domFromPos(pos: number) {
    599    return {node: this.textDOM, offset: pos}
    600  }
    601 
    602  ignoreMutation(mut: ViewMutationRecord) {
    603    return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue
    604   }
    605 }
    606 
    607 // A mark desc represents a mark. May have multiple children,
    608 // depending on how the mark is split. Note that marks are drawn using
    609 // a fixed nesting order, for simplicity and predictability, so in
    610 // some cases they will be split more often than would appear
    611 // necessary.
    612 class MarkViewDesc extends ViewDesc {
    613  constructor(parent: ViewDesc, readonly mark: Mark, dom: DOMNode, contentDOM: HTMLElement, readonly spec: MarkView) {
    614    super(parent, [], dom, contentDOM)
    615  }
    616 
    617  static create(parent: ViewDesc, mark: Mark, inline: boolean, view: EditorView) {
    618    let custom = view.nodeViews[mark.type.name]
    619    let spec: {dom: HTMLElement, contentDOM?: HTMLElement} = custom && (custom as any)(mark, view, inline)
    620    if (!spec || !spec.dom)
    621      spec = (DOMSerializer.renderSpec as any)(document, mark.type.spec.toDOM!(mark, inline), null, mark.attrs) as any
    622    return new MarkViewDesc(parent, mark, spec.dom, spec.contentDOM || spec.dom as HTMLElement, spec)
    623  }
    624 
    625  parseRule() {
    626    if ((this.dirty & NODE_DIRTY) || this.mark.type.spec.reparseInView) return null
    627    return {mark: this.mark.type.name, attrs: this.mark.attrs, contentElement: this.contentDOM!}
    628  }
    629 
    630  matchesMark(mark: Mark) { return this.dirty != NODE_DIRTY && this.mark.eq(mark) }
    631 
    632  markDirty(from: number, to: number) {
    633    super.markDirty(from, to)
    634    // Move dirty info to nearest node view
    635    if (this.dirty != NOT_DIRTY) {
    636      let parent = this.parent!
    637      while (!parent.node) parent = parent.parent!
    638      if (parent.dirty < this.dirty) parent.dirty = this.dirty
    639      this.dirty = NOT_DIRTY
    640    }
    641  }
    642 
    643  slice(from: number, to: number, view: EditorView) {
    644    let copy = MarkViewDesc.create(this.parent!, this.mark, true, view)
    645    let nodes = this.children, size = this.size
    646    if (to < size) nodes = replaceNodes(nodes, to, size, view)
    647    if (from > 0) nodes = replaceNodes(nodes, 0, from, view)
    648    for (let i = 0; i < nodes.length; i++) nodes[i].parent = copy
    649    copy.children = nodes
    650    return copy
    651  }
    652 
    653  ignoreMutation(mutation: ViewMutationRecord) {
    654    return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation)
    655  }
    656 
    657  destroy() {
    658    if (this.spec.destroy) this.spec.destroy()
    659    super.destroy()
    660  }
    661 }
    662 
    663 // Node view descs are the main, most common type of view desc, and
    664 // correspond to an actual node in the document. Unlike mark descs,
    665 // they populate their child array themselves.
    666 export class NodeViewDesc extends ViewDesc {
    667  constructor(
    668    parent: ViewDesc | undefined,
    669    public node: Node,
    670    public outerDeco: readonly Decoration[],
    671    public innerDeco: DecorationSource,
    672    dom: DOMNode,
    673    contentDOM: HTMLElement | null,
    674    readonly nodeDOM: DOMNode,
    675    view: EditorView,
    676    pos: number
    677  ) {
    678    super(parent, [], dom, contentDOM)
    679  }
    680 
    681  // By default, a node is rendered using the `toDOM` method from the
    682  // node type spec. But client code can use the `nodeViews` spec to
    683  // supply a custom node view, which can influence various aspects of
    684  // the way the node works.
    685  //
    686  // (Using subclassing for this was intentionally decided against,
    687  // since it'd require exposing a whole slew of finicky
    688  // implementation details to the user code that they probably will
    689  // never need.)
    690  static create(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[],
    691                innerDeco: DecorationSource, view: EditorView, pos: number) {
    692    let custom = view.nodeViews[node.type.name], descObj: ViewDesc
    693    let spec: NodeView | undefined = custom && (custom as any)(node, view, () => {
    694      // (This is a function that allows the custom view to find its
    695      // own position)
    696      if (!descObj) return pos
    697      if (descObj.parent) return descObj.parent.posBeforeChild(descObj)
    698    }, outerDeco, innerDeco)
    699 
    700    let dom = spec && spec.dom, contentDOM = spec && spec.contentDOM
    701    if (node.isText) {
    702      if (!dom) dom = document.createTextNode(node.text!)
    703      else if (dom.nodeType != 3) throw new RangeError("Text must be rendered as a DOM text node")
    704    } else if (!dom) {
    705      let spec = (DOMSerializer.renderSpec as any)(document, node.type.spec.toDOM!(node), null, node.attrs)
    706      ;({dom, contentDOM} = spec as {dom: DOMNode, contentDOM?: HTMLElement})
    707    }
    708    if (!contentDOM && !node.isText && dom.nodeName != "BR") { // Chrome gets confused by <br contenteditable=false>
    709      if (!(dom as HTMLElement).hasAttribute("contenteditable")) (dom as HTMLElement).contentEditable = "false"
    710      if (node.type.spec.draggable) (dom as HTMLElement).draggable = true
    711    }
    712 
    713    let nodeDOM = dom
    714    dom = applyOuterDeco(dom, outerDeco, node)
    715 
    716    if (spec)
    717      return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM,
    718                                              spec, view, pos + 1)
    719    else if (node.isText)
    720      return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view)
    721    else
    722      return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, view, pos + 1)
    723  }
    724 
    725  parseRule() {
    726    // Experimental kludge to allow opt-in re-parsing of nodes
    727    if (this.node.type.spec.reparseInView) return null
    728    // FIXME the assumption that this can always return the current
    729    // attrs means that if the user somehow manages to change the
    730    // attrs in the dom, that won't be picked up. Not entirely sure
    731    // whether this is a problem
    732    let rule: Omit<TagParseRule, "tag"> = {node: this.node.type.name, attrs: this.node.attrs}
    733    if (this.node.type.whitespace == "pre") rule.preserveWhitespace = "full"
    734    if (!this.contentDOM) {
    735      rule.getContent = () => this.node.content
    736    } else if (!this.contentLost) {
    737      rule.contentElement = this.contentDOM
    738    } else {
    739      // Chrome likes to randomly recreate parent nodes when
    740      // backspacing things. When that happens, this tries to find the
    741      // new parent.
    742      for (let i = this.children.length - 1; i >= 0; i--) {
    743        let child = this.children[i]
    744        if (this.dom.contains(child.dom.parentNode)) {
    745          rule.contentElement = child.dom.parentNode as HTMLElement
    746          break
    747        }
    748      }
    749      if (!rule.contentElement) rule.getContent = () => Fragment.empty
    750    }
    751    return rule
    752  }
    753 
    754  matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource) {
    755    return this.dirty == NOT_DIRTY && node.eq(this.node) &&
    756      sameOuterDeco(outerDeco, this.outerDeco) && innerDeco.eq(this.innerDeco)
    757  }
    758 
    759  get size() { return this.node.nodeSize }
    760 
    761  get border() { return this.node.isLeaf ? 0 : 1 }
    762 
    763  // Syncs `this.children` to match `this.node.content` and the local
    764  // decorations, possibly introducing nesting for marks. Then, in a
    765  // separate step, syncs the DOM inside `this.contentDOM` to
    766  // `this.children`.
    767  updateChildren(view: EditorView, pos: number) {
    768    let inline = this.node.inlineContent, off = pos
    769    let composition = view.composing ? this.localCompositionInfo(view, pos) : null
    770    let localComposition = composition && composition.pos > -1 ? composition : null
    771    let compositionInChild = composition && composition.pos < 0
    772    let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view)
    773    iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
    774      if (widget.spec.marks)
    775        updater.syncToMarks(widget.spec.marks, inline, view)
    776      else if ((widget.type as WidgetType).side >= 0 && !insideNode)
    777        updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view)
    778      // If the next node is a desc matching this widget, reuse it,
    779      // otherwise insert the widget as a new view desc.
    780      updater.placeWidget(widget, view, off)
    781    }, (child, outerDeco, innerDeco, i) => {
    782      // Make sure the wrapping mark descs match the node's marks.
    783      updater.syncToMarks(child.marks, inline, view)
    784      // Try several strategies for drawing this node
    785      let compIndex
    786      if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) {
    787        // Found precise match with existing node view
    788      } else if (compositionInChild && view.state.selection.from > off &&
    789                 view.state.selection.to < off + child.nodeSize &&
    790                 (compIndex = updater.findIndexWithChild(composition!.node)) > -1 &&
    791                 updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) {
    792        // Updated the specific node that holds the composition
    793      } else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i, off)) {
    794        // Could update an existing node to reflect this node
    795      } else {
    796        // Add it as a new view
    797        updater.addNode(child, outerDeco, innerDeco, view, off)
    798      }
    799      off += child.nodeSize
    800    })
    801    // Drop all remaining descs after the current position.
    802    updater.syncToMarks([], inline, view)
    803    if (this.node.isTextblock) updater.addTextblockHacks()
    804    updater.destroyRest()
    805 
    806    // Sync the DOM if anything changed
    807    if (updater.changed || this.dirty == CONTENT_DIRTY) {
    808      // May have to protect focused DOM from being changed if a composition is active
    809      if (localComposition) this.protectLocalComposition(view, localComposition)
    810      renderDescs(this.contentDOM!, this.children, view)
    811      if (browser.ios) iosHacks(this.dom as HTMLElement)
    812    }
    813  }
    814 
    815  localCompositionInfo(view: EditorView, pos: number): {node: Text, pos: number, text: string} | null {
    816    // Only do something if both the selection and a focused text node
    817    // are inside of this node
    818    let {from, to} = view.state.selection
    819    if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return null
    820    let textNode = view.input.compositionNode
    821    if (!textNode || !this.dom.contains(textNode.parentNode)) return null
    822 
    823    if (this.node.inlineContent) {
    824      // Find the text in the focused node in the node, stop if it's not
    825      // there (may have been modified through other means, in which
    826      // case it should overwritten)
    827      let text = textNode.nodeValue!
    828      let textPos = findTextInFragment(this.node.content, text, from - pos, to - pos)
    829      return textPos < 0 ? null : {node: textNode, pos: textPos, text}
    830    } else {
    831      return {node: textNode, pos: -1, text: ""}
    832    }
    833  }
    834 
    835  protectLocalComposition(view: EditorView, {node, pos, text}: {node: Text, pos: number, text: string}) {
    836    // The node is already part of a local view desc, leave it there
    837    if (this.getDesc(node)) return
    838 
    839    // Create a composition view for the orphaned nodes
    840    let topNode: DOMNode = node
    841    for (;; topNode = topNode.parentNode!) {
    842      if (topNode.parentNode == this.contentDOM) break
    843      while (topNode.previousSibling) topNode.parentNode!.removeChild(topNode.previousSibling)
    844      while (topNode.nextSibling) topNode.parentNode!.removeChild(topNode.nextSibling)
    845      if (topNode.pmViewDesc) topNode.pmViewDesc = undefined
    846    }
    847    let desc = new CompositionViewDesc(this, topNode, node, text)
    848    view.input.compositionNodes.push(desc)
    849 
    850    // Patch up this.children to contain the composition view
    851    this.children = replaceNodes(this.children, pos, pos + text.length, view, desc)
    852  }
    853 
    854  // If this desc must be updated to match the given node decoration,
    855  // do so and return true.
    856  update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) {
    857    if (this.dirty == NODE_DIRTY ||
    858        !node.sameMarkup(this.node)) return false
    859    this.updateInner(node, outerDeco, innerDeco, view)
    860    return true
    861  }
    862 
    863  updateInner(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) {
    864    this.updateOuterDeco(outerDeco)
    865    this.node = node
    866    this.innerDeco = innerDeco
    867    if (this.contentDOM) this.updateChildren(view, this.posAtStart)
    868    this.dirty = NOT_DIRTY
    869  }
    870 
    871  updateOuterDeco(outerDeco: readonly Decoration[]) {
    872    if (sameOuterDeco(outerDeco, this.outerDeco)) return
    873    let needsWrap = this.nodeDOM.nodeType != 1
    874    let oldDOM = this.dom
    875    this.dom = patchOuterDeco(this.dom, this.nodeDOM,
    876                              computeOuterDeco(this.outerDeco, this.node, needsWrap),
    877                              computeOuterDeco(outerDeco, this.node, needsWrap))
    878    if (this.dom != oldDOM) {
    879      oldDOM.pmViewDesc = undefined
    880      this.dom.pmViewDesc = this
    881    }
    882    this.outerDeco = outerDeco
    883  }
    884 
    885  // Mark this node as being the selected node.
    886  selectNode() {
    887    if (this.nodeDOM.nodeType == 1) {
    888      ;(this.nodeDOM as HTMLElement).classList.add("ProseMirror-selectednode")
    889      if (this.contentDOM || !this.node.type.spec.draggable) (this.nodeDOM as HTMLElement).draggable = true
    890    }
    891  }
    892 
    893  // Remove selected node marking from this node.
    894  deselectNode() {
    895    if (this.nodeDOM.nodeType == 1) {
    896      ;(this.nodeDOM as HTMLElement).classList.remove("ProseMirror-selectednode")
    897      if (this.contentDOM || !this.node.type.spec.draggable) (this.nodeDOM as HTMLElement).removeAttribute("draggable")
    898    }
    899  }
    900 
    901  get domAtom() { return this.node.isAtom }
    902 }
    903 
    904 // Create a view desc for the top-level document node, to be exported
    905 // and used by the view class.
    906 export function docViewDesc(doc: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
    907                            dom: HTMLElement, view: EditorView): NodeViewDesc {
    908  applyOuterDeco(dom, outerDeco, doc)
    909  let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom, view, 0)
    910  if (docView.contentDOM) docView.updateChildren(view, 0)
    911  return docView
    912 }
    913 
    914 class TextViewDesc extends NodeViewDesc {
    915  constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[],
    916              innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode, view: EditorView) {
    917    super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view, 0)
    918  }
    919 
    920  parseRule() {
    921    let skip = this.nodeDOM.parentNode
    922    while (skip && skip != this.dom && !(skip as any).pmIsDeco) skip = skip.parentNode
    923    return {skip: (skip || true) as any}
    924  }
    925 
    926  update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) {
    927    if (this.dirty == NODE_DIRTY || (this.dirty != NOT_DIRTY && !this.inParent()) ||
    928        !node.sameMarkup(this.node)) return false
    929    this.updateOuterDeco(outerDeco)
    930    if ((this.dirty != NOT_DIRTY || node.text != this.node.text) && node.text != this.nodeDOM.nodeValue) {
    931      this.nodeDOM.nodeValue = node.text!
    932      if (view.trackWrites == this.nodeDOM) view.trackWrites = null
    933    }
    934    this.node = node
    935    this.dirty = NOT_DIRTY
    936    return true
    937  }
    938 
    939  inParent() {
    940    let parentDOM = this.parent!.contentDOM
    941    for (let n: DOMNode | null = this.nodeDOM; n; n = n.parentNode) if (n == parentDOM) return true
    942    return false
    943  }
    944 
    945  domFromPos(pos: number) {
    946    return {node: this.nodeDOM, offset: pos}
    947  }
    948 
    949  localPosFromDOM(dom: DOMNode, offset: number, bias: number) {
    950    if (dom == this.nodeDOM) return this.posAtStart + Math.min(offset, this.node.text!.length)
    951    return super.localPosFromDOM(dom, offset, bias)
    952  }
    953 
    954  ignoreMutation(mutation: ViewMutationRecord) {
    955    return mutation.type != "characterData" && mutation.type != "selection"
    956  }
    957 
    958  slice(from: number, to: number, view: EditorView) {
    959    let node = this.node.cut(from, to), dom = document.createTextNode(node.text!)
    960    return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom, view)
    961  }
    962 
    963  markDirty(from: number, to: number) {
    964    super.markDirty(from, to)
    965    if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue!.length))
    966      this.dirty = NODE_DIRTY
    967  }
    968 
    969  get domAtom() { return false }
    970 
    971  isText(text: string) { return this.node.text == text }
    972 }
    973 
    974 // A dummy desc used to tag trailing BR or IMG nodes created to work
    975 // around contentEditable terribleness.
    976 class TrailingHackViewDesc extends ViewDesc {
    977  parseRule() { return {ignore: true} }
    978  matchesHack(nodeName: string) { return this.dirty == NOT_DIRTY && this.dom.nodeName == nodeName }
    979  get domAtom() { return true }
    980  get ignoreForCoords() { return this.dom.nodeName == "IMG" }
    981 }
    982 
    983 // A separate subclass is used for customized node views, so that the
    984 // extra checks only have to be made for nodes that are actually
    985 // customized.
    986 class CustomNodeViewDesc extends NodeViewDesc {
    987  constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
    988              dom: DOMNode, contentDOM: HTMLElement | null, nodeDOM: DOMNode, readonly spec: NodeView,
    989              view: EditorView, pos: number) {
    990    super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos)
    991  }
    992 
    993  // A custom `update` method gets to decide whether the update goes
    994  // through. If it does, and there's a `contentDOM` node, our logic
    995  // updates the children.
    996  update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) {
    997    if (this.dirty == NODE_DIRTY) return false
    998    if (this.spec.update && (this.node.type == node.type || this.spec.multiType)) {
    999      let result = this.spec.update(node, outerDeco, innerDeco)
   1000      if (result) this.updateInner(node, outerDeco, innerDeco, view)
   1001      return result
   1002    } else if (!this.contentDOM && !node.isLeaf) {
   1003      return false
   1004    } else {
   1005      return super.update(node, outerDeco, innerDeco, view)
   1006    }
   1007  }
   1008 
   1009  selectNode() {
   1010    this.spec.selectNode ? this.spec.selectNode() : super.selectNode()
   1011  }
   1012 
   1013  deselectNode() {
   1014    this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode()
   1015  }
   1016 
   1017  setSelection(anchor: number, head: number, view: EditorView, force: boolean) {
   1018    this.spec.setSelection ? this.spec.setSelection(anchor, head, view.root)
   1019      : super.setSelection(anchor, head, view, force)
   1020  }
   1021 
   1022  destroy() {
   1023    if (this.spec.destroy) this.spec.destroy()
   1024    super.destroy()
   1025  }
   1026 
   1027  stopEvent(event: Event) {
   1028    return this.spec.stopEvent ? this.spec.stopEvent(event) : false
   1029  }
   1030 
   1031  ignoreMutation(mutation: ViewMutationRecord) {
   1032    return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation)
   1033  }
   1034 }
   1035 
   1036 // Sync the content of the given DOM node with the nodes associated
   1037 // with the given array of view descs, recursing into mark descs
   1038 // because this should sync the subtree for a whole node at a time.
   1039 function renderDescs(parentDOM: HTMLElement, descs: readonly ViewDesc[], view: EditorView) {
   1040  let dom = parentDOM.firstChild, written = false
   1041  for (let i = 0; i < descs.length; i++) {
   1042    let desc = descs[i], childDOM = desc.dom
   1043    if (childDOM.parentNode == parentDOM) {
   1044      while (childDOM != dom) { dom = rm(dom!); written = true }
   1045      dom = dom.nextSibling
   1046    } else {
   1047      written = true
   1048      parentDOM.insertBefore(childDOM, dom)
   1049    }
   1050    if (desc instanceof MarkViewDesc) {
   1051      let pos = dom ? dom.previousSibling : parentDOM.lastChild
   1052      renderDescs(desc.contentDOM!, desc.children, view)
   1053      dom = pos ? pos.nextSibling : parentDOM.firstChild
   1054    }
   1055  }
   1056  while (dom) { dom = rm(dom); written = true }
   1057  if (written && view.trackWrites == parentDOM) view.trackWrites = null
   1058 }
   1059 
   1060 type OuterDecoLevel = {[attr: string]: string}
   1061 
   1062 const OuterDecoLevel: {new (nodeName?: string): OuterDecoLevel} = function(this: any, nodeName?: string) {
   1063  if (nodeName) this.nodeName = nodeName
   1064 } as any
   1065 OuterDecoLevel.prototype = Object.create(null)
   1066 
   1067 const noDeco = [new OuterDecoLevel]
   1068 
   1069 function computeOuterDeco(outerDeco: readonly Decoration[], node: Node, needsWrap: boolean) {
   1070  if (outerDeco.length == 0) return noDeco
   1071 
   1072  let top = needsWrap ? noDeco[0] : new OuterDecoLevel, result = [top]
   1073 
   1074  for (let i = 0; i < outerDeco.length; i++) {
   1075    let attrs = (outerDeco[i].type as NodeType).attrs
   1076    if (!attrs) continue
   1077    if (attrs.nodeName)
   1078      result.push(top = new OuterDecoLevel(attrs.nodeName))
   1079 
   1080    for (let name in attrs) {
   1081      let val = attrs[name]
   1082      if (val == null) continue
   1083      if (needsWrap && result.length == 1)
   1084        result.push(top = new OuterDecoLevel(node.isInline ? "span" : "div"))
   1085      if (name == "class") top.class = (top.class ? top.class + " " : "") + val
   1086      else if (name == "style") top.style = (top.style ? top.style + ";" : "") + val
   1087      else if (name != "nodeName") top[name] = val
   1088    }
   1089  }
   1090 
   1091  return result
   1092 }
   1093 
   1094 function patchOuterDeco(outerDOM: DOMNode, nodeDOM: DOMNode,
   1095                        prevComputed: readonly OuterDecoLevel[], curComputed: readonly OuterDecoLevel[]) {
   1096  // Shortcut for trivial case
   1097  if (prevComputed == noDeco && curComputed == noDeco) return nodeDOM
   1098 
   1099  let curDOM = nodeDOM
   1100  for (let i = 0; i < curComputed.length; i++) {
   1101    let deco = curComputed[i], prev = prevComputed[i]
   1102    if (i) {
   1103      let parent: DOMNode | null
   1104      if (prev && prev.nodeName == deco.nodeName && curDOM != outerDOM &&
   1105          (parent = curDOM.parentNode) && parent.nodeName!.toLowerCase() == deco.nodeName) {
   1106        curDOM = parent
   1107      } else {
   1108        parent = document.createElement(deco.nodeName)
   1109        ;(parent as any).pmIsDeco = true
   1110        parent.appendChild(curDOM)
   1111        prev = noDeco[0]
   1112        curDOM = parent
   1113      }
   1114    }
   1115    patchAttributes(curDOM as HTMLElement, prev || noDeco[0], deco)
   1116  }
   1117  return curDOM
   1118 }
   1119 
   1120 function patchAttributes(dom: HTMLElement, prev: {[name: string]: string}, cur: {[name: string]: string}) {
   1121  for (let name in prev)
   1122    if (name != "class" && name != "style" && name != "nodeName" && !(name in cur))
   1123      dom.removeAttribute(name)
   1124  for (let name in cur)
   1125    if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name])
   1126      dom.setAttribute(name, cur[name])
   1127  if (prev.class != cur.class) {
   1128    let prevList = prev.class ? prev.class.split(" ").filter(Boolean) : []
   1129    let curList = cur.class ? cur.class.split(" ").filter(Boolean) : []
   1130    for (let i = 0; i < prevList.length; i++) if (curList.indexOf(prevList[i]) == -1)
   1131      dom.classList.remove(prevList[i])
   1132    for (let i = 0; i < curList.length; i++) if (prevList.indexOf(curList[i]) == -1)
   1133      dom.classList.add(curList[i])
   1134    if (dom.classList.length == 0)
   1135      dom.removeAttribute("class")
   1136  }
   1137  if (prev.style != cur.style) {
   1138    if (prev.style) {
   1139      let prop = /\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g, m
   1140      while (m = prop.exec(prev.style))
   1141        dom.style.removeProperty(m[1])
   1142    }
   1143    if (cur.style)
   1144      dom.style.cssText += cur.style
   1145  }
   1146 }
   1147 
   1148 function applyOuterDeco(dom: DOMNode, deco: readonly Decoration[], node: Node) {
   1149  return patchOuterDeco(dom, dom, noDeco, computeOuterDeco(deco, node, dom.nodeType != 1))
   1150 }
   1151 
   1152 function sameOuterDeco(a: readonly Decoration[], b: readonly Decoration[]) {
   1153  if (a.length != b.length) return false
   1154  for (let i = 0; i < a.length; i++) if (!a[i].type.eq(b[i].type)) return false
   1155  return true
   1156 }
   1157 
   1158 // Remove a DOM node and return its next sibling.
   1159 function rm(dom: DOMNode) {
   1160  let next = dom.nextSibling
   1161  dom.parentNode!.removeChild(dom)
   1162  return next
   1163 }
   1164 
   1165 // Helper class for incrementally updating a tree of mark descs and
   1166 // the widget and node descs inside of them.
   1167 class ViewTreeUpdater {
   1168  // Index into `this.top`'s child array, represents the current
   1169  // update position.
   1170  index = 0
   1171  // When entering a mark, the current top and index are pushed
   1172  // onto this.
   1173  stack: (ViewDesc | number)[] = []
   1174  // Tracks whether anything was changed
   1175  changed = false
   1176  preMatch: {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]}
   1177  top: ViewDesc
   1178 
   1179  constructor(top: NodeViewDesc, readonly lock: DOMNode | null, private readonly view: EditorView) {
   1180    this.top = top
   1181    this.preMatch = preMatch(top.node.content, top)
   1182  }
   1183 
   1184  // Destroy and remove the children between the given indices in
   1185  // `this.top`.
   1186  destroyBetween(start: number, end: number) {
   1187    if (start == end) return
   1188    for (let i = start; i < end; i++) this.top.children[i].destroy()
   1189    this.top.children.splice(start, end - start)
   1190    this.changed = true
   1191  }
   1192 
   1193  // Destroy all remaining children in `this.top`.
   1194  destroyRest() {
   1195    this.destroyBetween(this.index, this.top.children.length)
   1196  }
   1197 
   1198  // Sync the current stack of mark descs with the given array of
   1199  // marks, reusing existing mark descs when possible.
   1200  syncToMarks(marks: readonly Mark[], inline: boolean, view: EditorView) {
   1201    let keep = 0, depth = this.stack.length >> 1
   1202    let maxKeep = Math.min(depth, marks.length)
   1203    while (keep < maxKeep &&
   1204           (keep == depth - 1 ? this.top : this.stack[(keep + 1) << 1] as ViewDesc)
   1205             .matchesMark(marks[keep]) && marks[keep].type.spec.spanning !== false)
   1206      keep++
   1207 
   1208    while (keep < depth) {
   1209      this.destroyRest()
   1210      this.top.dirty = NOT_DIRTY
   1211      this.index = this.stack.pop() as number
   1212      this.top = this.stack.pop() as ViewDesc
   1213      depth--
   1214    }
   1215    while (depth < marks.length) {
   1216      this.stack.push(this.top, this.index + 1)
   1217      let found = -1
   1218      for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
   1219        let next = this.top.children[i]
   1220        if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) { found = i; break }
   1221      }
   1222      if (found > -1) {
   1223        if (found > this.index) {
   1224          this.changed = true
   1225          this.destroyBetween(this.index, found)
   1226        }
   1227        this.top = this.top.children[this.index]
   1228      } else {
   1229        let markDesc = MarkViewDesc.create(this.top, marks[depth], inline, view)
   1230        this.top.children.splice(this.index, 0, markDesc)
   1231        this.top = markDesc
   1232        this.changed = true
   1233      }
   1234      this.index = 0
   1235      depth++
   1236    }
   1237  }
   1238 
   1239  // Try to find a node desc matching the given data. Skip over it and
   1240  // return true when successful.
   1241  findNodeMatch(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number): boolean {
   1242    let found = -1, targetDesc
   1243    if (index >= this.preMatch.index &&
   1244        (targetDesc = this.preMatch.matches[index - this.preMatch.index]).parent == this.top &&
   1245        targetDesc.matchesNode(node, outerDeco, innerDeco)) {
   1246      found = this.top.children.indexOf(targetDesc, this.index)
   1247    } else {
   1248      for (let i = this.index, e = Math.min(this.top.children.length, i + 5); i < e; i++) {
   1249        let child = this.top.children[i]
   1250        if (child.matchesNode(node, outerDeco, innerDeco) && !this.preMatch.matched.has(child)) {
   1251          found = i
   1252          break
   1253        }
   1254      }
   1255    }
   1256    if (found < 0) return false
   1257    this.destroyBetween(this.index, found)
   1258    this.index++
   1259    return true
   1260  }
   1261 
   1262  updateNodeAt(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number, view: EditorView) {
   1263    let child = this.top.children[index] as NodeViewDesc
   1264    if (child.dirty == NODE_DIRTY && child.dom == child.contentDOM) child.dirty = CONTENT_DIRTY
   1265    if (!child.update(node, outerDeco, innerDeco, view)) return false
   1266    this.destroyBetween(this.index, index)
   1267    this.index++
   1268    return true
   1269  }
   1270 
   1271  findIndexWithChild(domNode: DOMNode) {
   1272    for (;;) {
   1273      let parent = domNode.parentNode
   1274      if (!parent) return -1
   1275      if (parent == this.top.contentDOM) {
   1276        let desc = domNode.pmViewDesc
   1277        if (desc) for (let i = this.index; i < this.top.children.length; i++) {
   1278          if (this.top.children[i] == desc) return i
   1279        }
   1280        return -1
   1281      }
   1282      domNode = parent
   1283    }
   1284  }
   1285 
   1286  // Try to update the next node, if any, to the given data. Checks
   1287  // pre-matches to avoid overwriting nodes that could still be used.
   1288  updateNextNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
   1289                 view: EditorView, index: number, pos: number): boolean {
   1290    for (let i = this.index; i < this.top.children.length; i++) {
   1291      let next = this.top.children[i]
   1292      if (next instanceof NodeViewDesc) {
   1293        let preMatch = this.preMatch.matched.get(next)
   1294        if (preMatch != null && preMatch != index) return false
   1295        let nextDOM = next.dom, updated
   1296 
   1297        // Can't update if nextDOM is or contains this.lock, except if
   1298        // it's a text node whose content already matches the new text
   1299        // and whose decorations match the new ones.
   1300        let locked = this.isLocked(nextDOM) &&
   1301            !(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text &&
   1302              next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco))
   1303        if (!locked && next.update(node, outerDeco, innerDeco, view)) {
   1304          this.destroyBetween(this.index, i)
   1305          if (next.dom != nextDOM) this.changed = true
   1306          this.index++
   1307          return true
   1308        } else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) {
   1309          this.destroyBetween(this.index, i)
   1310          this.top.children[this.index] = updated
   1311          if (updated.contentDOM) {
   1312            updated.dirty = CONTENT_DIRTY
   1313            updated.updateChildren(view, pos + 1)
   1314            updated.dirty = NOT_DIRTY
   1315          }
   1316          this.changed = true
   1317          this.index++
   1318          return true
   1319        }
   1320        break
   1321      }
   1322    }
   1323    return false
   1324  }
   1325 
   1326  // When a node with content is replaced by a different node with
   1327  // identical content, move over its children.
   1328  recreateWrapper(next: NodeViewDesc, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
   1329                  view: EditorView, pos: number) {
   1330    if (next.dirty || node.isAtom || !next.children.length ||
   1331        !next.node.content.eq(node.content) ||
   1332        !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco)) return null
   1333    let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos)
   1334    if (wrapper.contentDOM) {
   1335      wrapper.children = next.children
   1336      next.children = []
   1337      for (let ch of wrapper.children) ch.parent = wrapper
   1338    }
   1339    next.destroy()
   1340    return wrapper
   1341  }
   1342 
   1343  // Insert the node as a newly created node desc.
   1344  addNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView, pos: number) {
   1345    let desc = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos)
   1346    if (desc.contentDOM) desc.updateChildren(view, pos + 1)
   1347    this.top.children.splice(this.index++, 0, desc)
   1348    this.changed = true
   1349  }
   1350 
   1351  placeWidget(widget: Decoration, view: EditorView, pos: number) {
   1352    let next = this.index < this.top.children.length ? this.top.children[this.index] : null
   1353    if (next && next.matchesWidget(widget) &&
   1354        (widget == (next as WidgetViewDesc).widget || !(next as any).widget.type.toDOM.parentNode)) {
   1355      this.index++
   1356    } else {
   1357      let desc = new WidgetViewDesc(this.top, widget, view, pos)
   1358      this.top.children.splice(this.index++, 0, desc)
   1359      this.changed = true
   1360    }
   1361  }
   1362 
   1363  // Make sure a textblock looks and behaves correctly in
   1364  // contentEditable.
   1365  addTextblockHacks() {
   1366    let lastChild = this.top.children[this.index - 1], parent = this.top
   1367    while (lastChild instanceof MarkViewDesc) {
   1368      parent = lastChild
   1369      lastChild = parent.children[parent.children.length - 1]
   1370    }
   1371 
   1372    if (!lastChild || // Empty textblock
   1373        !(lastChild instanceof TextViewDesc) ||
   1374        /\n$/.test(lastChild.node.text!) ||
   1375        (this.view.requiresGeckoHackNode && /\s$/.test(lastChild.node.text!))) {
   1376      // Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152)
   1377      if ((browser.safari || browser.chrome) && lastChild && (lastChild.dom as HTMLElement).contentEditable == "false")
   1378        this.addHackNode("IMG", parent)
   1379      this.addHackNode("BR", this.top)
   1380    }
   1381  }
   1382 
   1383  addHackNode(nodeName: string, parent: ViewDesc) {
   1384    if (parent == this.top && this.index < parent.children.length && parent.children[this.index].matchesHack(nodeName)) {
   1385      this.index++
   1386    } else {
   1387      let dom = document.createElement(nodeName)
   1388      if (nodeName == "IMG") {
   1389        dom.className = "ProseMirror-separator"
   1390        ;(dom as HTMLImageElement).alt = ""
   1391      }
   1392      if (nodeName == "BR") dom.className = "ProseMirror-trailingBreak"
   1393      let hack = new TrailingHackViewDesc(this.top, [], dom, null)
   1394      if (parent != this.top) parent.children.push(hack)
   1395      else parent.children.splice(this.index++, 0, hack)
   1396      this.changed = true
   1397    }
   1398  }
   1399 
   1400  isLocked(node: DOMNode) {
   1401    return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode))
   1402  }
   1403 }
   1404 
   1405 // Iterate from the end of the fragment and array of descs to find
   1406 // directly matching ones, in order to avoid overeagerly reusing those
   1407 // for other nodes. Returns the fragment index of the first node that
   1408 // is part of the sequence of matched nodes at the end of the
   1409 // fragment.
   1410 function preMatch(
   1411  frag: Fragment, parentDesc: ViewDesc
   1412 ): {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]} {
   1413  let curDesc = parentDesc, descI = curDesc.children.length
   1414  let fI = frag.childCount, matched = new Map, matches = []
   1415  outer: while (fI > 0) {
   1416    let desc
   1417    for (;;) {
   1418      if (descI) {
   1419        let next = curDesc.children[descI - 1]
   1420        if (next instanceof MarkViewDesc) {
   1421          curDesc = next
   1422          descI = next.children.length
   1423        } else {
   1424          desc = next
   1425          descI--
   1426          break
   1427        }
   1428      } else if (curDesc == parentDesc) {
   1429        break outer
   1430      } else {
   1431        // FIXME
   1432        descI = curDesc.parent!.children.indexOf(curDesc)
   1433        curDesc = curDesc.parent!
   1434      }
   1435    }
   1436    let node = desc.node
   1437    if (!node) continue
   1438    if (node != frag.child(fI - 1)) break
   1439    --fI
   1440    matched.set(desc, fI)
   1441    matches.push(desc)
   1442  }
   1443  return {index: fI, matched, matches: matches.reverse()}
   1444 }
   1445 
   1446 function compareSide(a: Decoration, b: Decoration) {
   1447  return (a.type as WidgetType).side - (b.type as WidgetType).side
   1448 }
   1449 
   1450 // This function abstracts iterating over the nodes and decorations in
   1451 // a fragment. Calls `onNode` for each node, with its local and child
   1452 // decorations. Splits text nodes when there is a decoration starting
   1453 // or ending inside of them. Calls `onWidget` for each widget.
   1454 function iterDeco(
   1455  parent: Node,
   1456  deco: DecorationSource,
   1457  onWidget: (widget: Decoration, index: number, insideNode: boolean) => void,
   1458  onNode: (node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number) => void
   1459 ) {
   1460  let locals = deco.locals(parent), offset = 0
   1461  // Simple, cheap variant for when there are no local decorations
   1462  if (locals.length == 0) {
   1463    for (let i = 0; i < parent.childCount; i++) {
   1464      let child = parent.child(i)
   1465      onNode(child, locals, deco.forChild(offset, child), i)
   1466      offset += child.nodeSize
   1467    }
   1468    return
   1469  }
   1470 
   1471  let decoIndex = 0, active = [], restNode = null
   1472  for (let parentIndex = 0;;) {
   1473    let widget, widgets
   1474    while (decoIndex < locals.length && locals[decoIndex].to == offset) {
   1475      let next = locals[decoIndex++]
   1476      if (next.widget) {
   1477        if (!widget) widget = next
   1478        else (widgets || (widgets = [widget])).push(next)
   1479      }
   1480    }
   1481    if (widget) {
   1482      if (widgets) {
   1483        widgets.sort(compareSide)
   1484        for (let i = 0; i < widgets.length; i++) onWidget(widgets[i], parentIndex, !!restNode)
   1485      } else {
   1486        onWidget(widget, parentIndex, !!restNode)
   1487      }
   1488    }
   1489 
   1490    let child, index
   1491    if (restNode) {
   1492      index = -1
   1493      child = restNode
   1494      restNode = null
   1495    } else if (parentIndex < parent.childCount) {
   1496      index = parentIndex
   1497      child = parent.child(parentIndex++)
   1498    } else {
   1499      break
   1500    }
   1501 
   1502    for (let i = 0; i < active.length; i++) if (active[i].to <= offset) active.splice(i--, 1)
   1503    while (decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset)
   1504      active.push(locals[decoIndex++])
   1505 
   1506    let end = offset + child.nodeSize
   1507    if (child.isText) {
   1508      let cutAt = end
   1509      if (decoIndex < locals.length && locals[decoIndex].from < cutAt) cutAt = locals[decoIndex].from
   1510      for (let i = 0; i < active.length; i++) if (active[i].to < cutAt) cutAt = active[i].to
   1511      if (cutAt < end) {
   1512        restNode = child.cut(cutAt - offset)
   1513        child = child.cut(0, cutAt - offset)
   1514        end = cutAt
   1515        index = -1
   1516      }
   1517    } else {
   1518      while (decoIndex < locals.length && locals[decoIndex].to < end) decoIndex++
   1519    }
   1520 
   1521    let outerDeco = child.isInline && !child.isLeaf ? active.filter(d => !d.inline) : active.slice()
   1522    onNode(child, outerDeco, deco.forChild(offset, child), index)
   1523    offset = end
   1524  }
   1525 }
   1526 
   1527 // List markers in Mobile Safari will mysteriously disappear
   1528 // sometimes. This works around that.
   1529 function iosHacks(dom: HTMLElement) {
   1530  if (dom.nodeName == "UL" || dom.nodeName == "OL") {
   1531    let oldCSS = dom.style.cssText
   1532    dom.style.cssText = oldCSS + "; list-style: square !important"
   1533    window.getComputedStyle(dom).listStyle
   1534    dom.style.cssText = oldCSS
   1535  }
   1536 }
   1537 
   1538 // Find a piece of text in an inline fragment, overlapping from-to
   1539 function findTextInFragment(frag: Fragment, text: string, from: number, to: number) {
   1540  for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) {
   1541    let child = frag.child(i++), childStart = pos
   1542    pos += child.nodeSize
   1543    if (!child.isText) continue
   1544    let str = child.text!
   1545    while (i < frag.childCount) {
   1546      let next = frag.child(i++)
   1547      pos += next.nodeSize
   1548      if (!next.isText) break
   1549      str += next.text
   1550    }
   1551    if (pos >= from) {
   1552      if (pos >= to && str.slice(to - text.length - childStart, to - childStart) == text)
   1553        return to - text.length
   1554      let found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1
   1555      if (found >= 0 && found + text.length + childStart >= from)
   1556        return childStart + found
   1557      if (from == to && str.length >= (to + text.length) - childStart &&
   1558          str.slice(to - childStart, to - childStart + text.length) == text)
   1559        return to
   1560    }
   1561  }
   1562  return -1
   1563 }
   1564 
   1565 // Replace range from-to in an array of view descs with replacement
   1566 // (may be null to just delete). This goes very much against the grain
   1567 // of the rest of this code, which tends to create nodes with the
   1568 // right shape in one go, rather than messing with them after
   1569 // creation, but is necessary in the composition hack.
   1570 function replaceNodes(nodes: readonly ViewDesc[], from: number, to: number, view: EditorView, replacement?: ViewDesc) {
   1571  let result = []
   1572  for (let i = 0, off = 0; i < nodes.length; i++) {
   1573    let child = nodes[i], start = off, end = off += child.size
   1574    if (start >= to || end <= from) {
   1575      result.push(child)
   1576    } else {
   1577      if (start < from) result.push((child as MarkViewDesc | TextViewDesc).slice(0, from - start, view))
   1578      if (replacement) {
   1579        result.push(replacement)
   1580        replacement = undefined
   1581      }
   1582      if (end > to) result.push((child as MarkViewDesc | TextViewDesc).slice(to - start, child.size, view))
   1583    }
   1584  }
   1585  return result
   1586 }