tor-browser

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

fragment.ts (10443B)


      1 import {findDiffStart, findDiffEnd} from "./diff"
      2 import {Node, TextNode} from "./node"
      3 import {Schema} from "./schema"
      4 
      5 /// A fragment represents a node's collection of child nodes.
      6 ///
      7 /// Like nodes, fragments are persistent data structures, and you
      8 /// should not mutate them or their content. Rather, you create new
      9 /// instances whenever needed. The API tries to make this easy.
     10 export class Fragment {
     11  /// The size of the fragment, which is the total of the size of
     12  /// its content nodes.
     13  readonly size: number
     14 
     15  /// @internal
     16  constructor(
     17    /// The child nodes in this fragment.
     18    readonly content: readonly Node[],
     19    size?: number
     20  ) {
     21    this.size = size || 0
     22    if (size == null) for (let i = 0; i < content.length; i++)
     23      this.size += content[i].nodeSize
     24  }
     25 
     26  /// Invoke a callback for all descendant nodes between the given two
     27  /// positions (relative to start of this fragment). Doesn't descend
     28  /// into a node when the callback returns `false`.
     29  nodesBetween(from: number, to: number,
     30               f: (node: Node, start: number, parent: Node | null, index: number) => boolean | void,
     31               nodeStart = 0,
     32               parent?: Node) {
     33    for (let i = 0, pos = 0; pos < to; i++) {
     34      let child = this.content[i], end = pos + child.nodeSize
     35      if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
     36        let start = pos + 1
     37        child.nodesBetween(Math.max(0, from - start),
     38                           Math.min(child.content.size, to - start),
     39                           f, nodeStart + start)
     40      }
     41      pos = end
     42    }
     43  }
     44 
     45  /// Call the given callback for every descendant node. `pos` will be
     46  /// relative to the start of the fragment. The callback may return
     47  /// `false` to prevent traversal of a given node's children.
     48  descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => boolean | void) {
     49    this.nodesBetween(0, this.size, f)
     50  }
     51 
     52  /// Extract the text between `from` and `to`. See the same method on
     53  /// [`Node`](#model.Node.textBetween).
     54  textBetween(from: number, to: number, blockSeparator?: string | null, leafText?: string | null | ((leafNode: Node) => string)) {
     55    let text = "", first = true
     56    this.nodesBetween(from, to, (node, pos) => {
     57      let nodeText = node.isText ? node.text!.slice(Math.max(from, pos) - pos, to - pos)
     58        : !node.isLeaf ? ""
     59        : leafText ? (typeof leafText === "function" ? leafText(node) : leafText)
     60        : node.type.spec.leafText ? node.type.spec.leafText(node)
     61        : ""
     62      if (node.isBlock && (node.isLeaf && nodeText || node.isTextblock) && blockSeparator) {
     63        if (first) first = false
     64        else text += blockSeparator
     65      }
     66      text += nodeText
     67    }, 0)
     68    return text
     69  }
     70 
     71  /// Create a new fragment containing the combined content of this
     72  /// fragment and the other.
     73  append(other: Fragment) {
     74    if (!other.size) return this
     75    if (!this.size) return other
     76    let last = this.lastChild!, first = other.firstChild!, content = this.content.slice(), i = 0
     77    if (last.isText && last.sameMarkup(first)) {
     78      content[content.length - 1] = (last as TextNode).withText(last.text! + first.text!)
     79      i = 1
     80    }
     81    for (; i < other.content.length; i++) content.push(other.content[i])
     82    return new Fragment(content, this.size + other.size)
     83  }
     84 
     85  /// Cut out the sub-fragment between the two given positions.
     86  cut(from: number, to = this.size) {
     87    if (from == 0 && to == this.size) return this
     88    let result: Node[] = [], size = 0
     89    if (to > from) for (let i = 0, pos = 0; pos < to; i++) {
     90      let child = this.content[i], end = pos + child.nodeSize
     91      if (end > from) {
     92        if (pos < from || end > to) {
     93          if (child.isText)
     94            child = child.cut(Math.max(0, from - pos), Math.min(child.text!.length, to - pos))
     95          else
     96            child = child.cut(Math.max(0, from - pos - 1), Math.min(child.content.size, to - pos - 1))
     97        }
     98        result.push(child)
     99        size += child.nodeSize
    100      }
    101      pos = end
    102    }
    103    return new Fragment(result, size)
    104  }
    105 
    106  /// @internal
    107  cutByIndex(from: number, to: number) {
    108    if (from == to) return Fragment.empty
    109    if (from == 0 && to == this.content.length) return this
    110    return new Fragment(this.content.slice(from, to))
    111  }
    112 
    113  /// Create a new fragment in which the node at the given index is
    114  /// replaced by the given node.
    115  replaceChild(index: number, node: Node) {
    116    let current = this.content[index]
    117    if (current == node) return this
    118    let copy = this.content.slice()
    119    let size = this.size + node.nodeSize - current.nodeSize
    120    copy[index] = node
    121    return new Fragment(copy, size)
    122  }
    123 
    124  /// Create a new fragment by prepending the given node to this
    125  /// fragment.
    126  addToStart(node: Node) {
    127    return new Fragment([node].concat(this.content), this.size + node.nodeSize)
    128  }
    129 
    130  /// Create a new fragment by appending the given node to this
    131  /// fragment.
    132  addToEnd(node: Node) {
    133    return new Fragment(this.content.concat(node), this.size + node.nodeSize)
    134  }
    135 
    136  /// Compare this fragment to another one.
    137  eq(other: Fragment): boolean {
    138    if (this.content.length != other.content.length) return false
    139    for (let i = 0; i < this.content.length; i++)
    140      if (!this.content[i].eq(other.content[i])) return false
    141    return true
    142  }
    143 
    144  /// The first child of the fragment, or `null` if it is empty.
    145  get firstChild(): Node | null { return this.content.length ? this.content[0] : null }
    146 
    147  /// The last child of the fragment, or `null` if it is empty.
    148  get lastChild(): Node | null { return this.content.length ? this.content[this.content.length - 1] : null }
    149 
    150  /// The number of child nodes in this fragment.
    151  get childCount() { return this.content.length }
    152 
    153  /// Get the child node at the given index. Raise an error when the
    154  /// index is out of range.
    155  child(index: number) {
    156    let found = this.content[index]
    157    if (!found) throw new RangeError("Index " + index + " out of range for " + this)
    158    return found
    159  }
    160 
    161  /// Get the child node at the given index, if it exists.
    162  maybeChild(index: number): Node | null {
    163    return this.content[index] || null
    164  }
    165 
    166  /// Call `f` for every child node, passing the node, its offset
    167  /// into this parent node, and its index.
    168  forEach(f: (node: Node, offset: number, index: number) => void) {
    169    for (let i = 0, p = 0; i < this.content.length; i++) {
    170      let child = this.content[i]
    171      f(child, p, i)
    172      p += child.nodeSize
    173    }
    174  }
    175 
    176  /// Find the first position at which this fragment and another
    177  /// fragment differ, or `null` if they are the same.
    178  findDiffStart(other: Fragment, pos = 0) {
    179    return findDiffStart(this, other, pos)
    180  }
    181 
    182  /// Find the first position, searching from the end, at which this
    183  /// fragment and the given fragment differ, or `null` if they are
    184  /// the same. Since this position will not be the same in both
    185  /// nodes, an object with two separate positions is returned.
    186  findDiffEnd(other: Fragment, pos = this.size, otherPos = other.size) {
    187    return findDiffEnd(this, other, pos, otherPos)
    188  }
    189 
    190  /// Find the index and inner offset corresponding to a given relative
    191  /// position in this fragment. The result object will be reused
    192  /// (overwritten) the next time the function is called. @internal
    193  findIndex(pos: number): {index: number, offset: number} {
    194    if (pos == 0) return retIndex(0, pos)
    195    if (pos == this.size) return retIndex(this.content.length, pos)
    196    if (pos > this.size || pos < 0) throw new RangeError(`Position ${pos} outside of fragment (${this})`)
    197    for (let i = 0, curPos = 0;; i++) {
    198      let cur = this.child(i), end = curPos + cur.nodeSize
    199      if (end >= pos) {
    200        if (end == pos) return retIndex(i + 1, end)
    201        return retIndex(i, curPos)
    202      }
    203      curPos = end
    204    }
    205  }
    206 
    207  /// Return a debugging string that describes this fragment.
    208  toString(): string { return "<" + this.toStringInner() + ">" }
    209 
    210  /// @internal
    211  toStringInner() { return this.content.join(", ") }
    212 
    213  /// Create a JSON-serializeable representation of this fragment.
    214  toJSON(): any {
    215    return this.content.length ? this.content.map(n => n.toJSON()) : null
    216  }
    217 
    218  /// Deserialize a fragment from its JSON representation.
    219  static fromJSON(schema: Schema, value: any) {
    220    if (!value) return Fragment.empty
    221    if (!Array.isArray(value)) throw new RangeError("Invalid input for Fragment.fromJSON")
    222    return new Fragment(value.map(schema.nodeFromJSON))
    223  }
    224 
    225  /// Build a fragment from an array of nodes. Ensures that adjacent
    226  /// text nodes with the same marks are joined together.
    227  static fromArray(array: readonly Node[]) {
    228    if (!array.length) return Fragment.empty
    229    let joined: Node[] | undefined, size = 0
    230    for (let i = 0; i < array.length; i++) {
    231      let node = array[i]
    232      size += node.nodeSize
    233      if (i && node.isText && array[i - 1].sameMarkup(node)) {
    234        if (!joined) joined = array.slice(0, i)
    235        joined[joined.length - 1] = (node as TextNode)
    236                                      .withText((joined[joined.length - 1] as TextNode).text + (node as TextNode).text)
    237      } else if (joined) {
    238        joined.push(node)
    239      }
    240    }
    241    return new Fragment(joined || array, size)
    242  }
    243 
    244  /// Create a fragment from something that can be interpreted as a
    245  /// set of nodes. For `null`, it returns the empty fragment. For a
    246  /// fragment, the fragment itself. For a node or array of nodes, a
    247  /// fragment containing those nodes.
    248  static from(nodes?: Fragment | Node | readonly Node[] | null) {
    249    if (!nodes) return Fragment.empty
    250    if (nodes instanceof Fragment) return nodes
    251    if (Array.isArray(nodes)) return this.fromArray(nodes)
    252    if ((nodes as Node).attrs) return new Fragment([nodes as Node], (nodes as Node).nodeSize)
    253    throw new RangeError("Can not convert " + nodes + " to a Fragment" +
    254      ((nodes as any).nodesBetween ? " (looks like multiple versions of prosemirror-model were loaded)" : ""))
    255  }
    256 
    257  /// An empty fragment. Intended to be reused whenever a node doesn't
    258  /// contain anything (rather than allocating a new empty fragment for
    259  /// each leaf node).
    260  static empty: Fragment = new Fragment([], 0)
    261 }
    262 
    263 const found = {index: 0, offset: 0}
    264 function retIndex(index: number, offset: number) {
    265  found.index = index
    266  found.offset = offset
    267  return found
    268 }