tor-browser

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

resolvedpos.ts (11616B)


      1 import {Mark} from "./mark"
      2 import {Node} from "./node"
      3 
      4 /// You can [_resolve_](#model.Node.resolve) a position to get more
      5 /// information about it. Objects of this class represent such a
      6 /// resolved position, providing various pieces of context
      7 /// information, and some helper methods.
      8 ///
      9 /// Throughout this interface, methods that take an optional `depth`
     10 /// parameter will interpret undefined as `this.depth` and negative
     11 /// numbers as `this.depth + value`.
     12 export class ResolvedPos {
     13  /// The number of levels the parent node is from the root. If this
     14  /// position points directly into the root node, it is 0. If it
     15  /// points into a top-level paragraph, 1, and so on.
     16  depth: number
     17 
     18  /// @internal
     19  constructor(
     20    /// The position that was resolved.
     21    readonly pos: number,
     22    /// @internal
     23    readonly path: any[],
     24    /// The offset this position has into its parent node.
     25    readonly parentOffset: number
     26  ) {
     27    this.depth = path.length / 3 - 1
     28  }
     29 
     30  /// @internal
     31  resolveDepth(val: number | undefined | null) {
     32    if (val == null) return this.depth
     33    if (val < 0) return this.depth + val
     34    return val
     35  }
     36 
     37  /// The parent node that the position points into. Note that even if
     38  /// a position points into a text node, that node is not considered
     39  /// the parent—text nodes are ‘flat’ in this model, and have no content.
     40  get parent() { return this.node(this.depth) }
     41 
     42  /// The root node in which the position was resolved.
     43  get doc() { return this.node(0) }
     44 
     45  /// The ancestor node at the given level. `p.node(p.depth)` is the
     46  /// same as `p.parent`.
     47  node(depth?: number | null): Node { return this.path[this.resolveDepth(depth) * 3] }
     48 
     49  /// The index into the ancestor at the given level. If this points
     50  /// at the 3rd node in the 2nd paragraph on the top level, for
     51  /// example, `p.index(0)` is 1 and `p.index(1)` is 2.
     52  index(depth?: number | null): number { return this.path[this.resolveDepth(depth) * 3 + 1] }
     53 
     54  /// The index pointing after this position into the ancestor at the
     55  /// given level.
     56  indexAfter(depth?: number | null): number {
     57    depth = this.resolveDepth(depth)
     58    return this.index(depth) + (depth == this.depth && !this.textOffset ? 0 : 1)
     59  }
     60 
     61  /// The (absolute) position at the start of the node at the given
     62  /// level.
     63  start(depth?: number | null): number {
     64    depth = this.resolveDepth(depth)
     65    return depth == 0 ? 0 : this.path[depth * 3 - 1] + 1
     66  }
     67 
     68  /// The (absolute) position at the end of the node at the given
     69  /// level.
     70  end(depth?: number | null): number {
     71    depth = this.resolveDepth(depth)
     72    return this.start(depth) + this.node(depth).content.size
     73  }
     74 
     75  /// The (absolute) position directly before the wrapping node at the
     76  /// given level, or, when `depth` is `this.depth + 1`, the original
     77  /// position.
     78  before(depth?: number | null): number {
     79    depth = this.resolveDepth(depth)
     80    if (!depth) throw new RangeError("There is no position before the top-level node")
     81    return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1]
     82  }
     83 
     84  /// The (absolute) position directly after the wrapping node at the
     85  /// given level, or the original position when `depth` is `this.depth + 1`.
     86  after(depth?: number | null): number {
     87    depth = this.resolveDepth(depth)
     88    if (!depth) throw new RangeError("There is no position after the top-level node")
     89    return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] + this.path[depth * 3].nodeSize
     90  }
     91 
     92  /// When this position points into a text node, this returns the
     93  /// distance between the position and the start of the text node.
     94  /// Will be zero for positions that point between nodes.
     95  get textOffset(): number { return this.pos - this.path[this.path.length - 1] }
     96 
     97  /// Get the node directly after the position, if any. If the position
     98  /// points into a text node, only the part of that node after the
     99  /// position is returned.
    100  get nodeAfter(): Node | null {
    101    let parent = this.parent, index = this.index(this.depth)
    102    if (index == parent.childCount) return null
    103    let dOff = this.pos - this.path[this.path.length - 1], child = parent.child(index)
    104    return dOff ? parent.child(index).cut(dOff) : child
    105  }
    106 
    107  /// Get the node directly before the position, if any. If the
    108  /// position points into a text node, only the part of that node
    109  /// before the position is returned.
    110  get nodeBefore(): Node | null {
    111    let index = this.index(this.depth)
    112    let dOff = this.pos - this.path[this.path.length - 1]
    113    if (dOff) return this.parent.child(index).cut(0, dOff)
    114    return index == 0 ? null : this.parent.child(index - 1)
    115  }
    116 
    117  /// Get the position at the given index in the parent node at the
    118  /// given depth (which defaults to `this.depth`).
    119  posAtIndex(index: number, depth?: number | null): number {
    120    depth = this.resolveDepth(depth)
    121    let node = this.path[depth * 3], pos = depth == 0 ? 0 : this.path[depth * 3 - 1] + 1
    122    for (let i = 0; i < index; i++) pos += node.child(i).nodeSize
    123    return pos
    124  }
    125 
    126  /// Get the marks at this position, factoring in the surrounding
    127  /// marks' [`inclusive`](#model.MarkSpec.inclusive) property. If the
    128  /// position is at the start of a non-empty node, the marks of the
    129  /// node after it (if any) are returned.
    130  marks(): readonly Mark[] {
    131    let parent = this.parent, index = this.index()
    132 
    133    // In an empty parent, return the empty array
    134    if (parent.content.size == 0) return Mark.none
    135 
    136    // When inside a text node, just return the text node's marks
    137    if (this.textOffset) return parent.child(index).marks
    138 
    139    let main = parent.maybeChild(index - 1), other = parent.maybeChild(index)
    140    // If the `after` flag is true of there is no node before, make
    141    // the node after this position the main reference.
    142    if (!main) { let tmp = main; main = other; other = tmp }
    143 
    144    // Use all marks in the main node, except those that have
    145    // `inclusive` set to false and are not present in the other node.
    146    let marks = main!.marks
    147    for (var i = 0; i < marks.length; i++)
    148      if (marks[i].type.spec.inclusive === false && (!other || !marks[i].isInSet(other.marks)))
    149        marks = marks[i--].removeFromSet(marks)
    150 
    151    return marks
    152  }
    153 
    154  /// Get the marks after the current position, if any, except those
    155  /// that are non-inclusive and not present at position `$end`. This
    156  /// is mostly useful for getting the set of marks to preserve after a
    157  /// deletion. Will return `null` if this position is at the end of
    158  /// its parent node or its parent node isn't a textblock (in which
    159  /// case no marks should be preserved).
    160  marksAcross($end: ResolvedPos): readonly Mark[] | null {
    161    let after = this.parent.maybeChild(this.index())
    162    if (!after || !after.isInline) return null
    163 
    164    let marks = after.marks, next = $end.parent.maybeChild($end.index())
    165    for (var i = 0; i < marks.length; i++)
    166      if (marks[i].type.spec.inclusive === false && (!next || !marks[i].isInSet(next.marks)))
    167        marks = marks[i--].removeFromSet(marks)
    168    return marks
    169  }
    170 
    171  /// The depth up to which this position and the given (non-resolved)
    172  /// position share the same parent nodes.
    173  sharedDepth(pos: number): number {
    174    for (let depth = this.depth; depth > 0; depth--)
    175      if (this.start(depth) <= pos && this.end(depth) >= pos) return depth
    176    return 0
    177  }
    178 
    179  /// Returns a range based on the place where this position and the
    180  /// given position diverge around block content. If both point into
    181  /// the same textblock, for example, a range around that textblock
    182  /// will be returned. If they point into different blocks, the range
    183  /// around those blocks in their shared ancestor is returned. You can
    184  /// pass in an optional predicate that will be called with a parent
    185  /// node to see if a range into that parent is acceptable.
    186  blockRange(other: ResolvedPos = this, pred?: (node: Node) => boolean): NodeRange | null {
    187    if (other.pos < this.pos) return other.blockRange(this)
    188    for (let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0); d >= 0; d--)
    189      if (other.pos <= this.end(d) && (!pred || pred(this.node(d))))
    190        return new NodeRange(this, other, d)
    191    return null
    192  }
    193 
    194  /// Query whether the given position shares the same parent node.
    195  sameParent(other: ResolvedPos): boolean {
    196    return this.pos - this.parentOffset == other.pos - other.parentOffset
    197  }
    198 
    199  /// Return the greater of this and the given position.
    200  max(other: ResolvedPos): ResolvedPos {
    201    return other.pos > this.pos ? other : this
    202  }
    203 
    204  /// Return the smaller of this and the given position.
    205  min(other: ResolvedPos): ResolvedPos {
    206    return other.pos < this.pos ? other : this
    207  }
    208 
    209  /// @internal
    210  toString() {
    211    let str = ""
    212    for (let i = 1; i <= this.depth; i++)
    213      str += (str ? "/" : "") + this.node(i).type.name + "_" + this.index(i - 1)
    214    return str + ":" + this.parentOffset
    215  }
    216 
    217  /// @internal
    218  static resolve(doc: Node, pos: number): ResolvedPos {
    219    if (!(pos >= 0 && pos <= doc.content.size)) throw new RangeError("Position " + pos + " out of range")
    220    let path: Array<Node | number> = []
    221    let start = 0, parentOffset = pos
    222    for (let node = doc;;) {
    223      let {index, offset} = node.content.findIndex(parentOffset)
    224      let rem = parentOffset - offset
    225      path.push(node, index, start + offset)
    226      if (!rem) break
    227      node = node.child(index)
    228      if (node.isText) break
    229      parentOffset = rem - 1
    230      start += offset + 1
    231    }
    232    return new ResolvedPos(pos, path, parentOffset)
    233  }
    234 
    235  /// @internal
    236  static resolveCached(doc: Node, pos: number): ResolvedPos {
    237    let cache = resolveCache.get(doc)
    238    if (cache) {
    239      for (let i = 0; i < cache.elts.length; i++) {
    240        let elt = cache.elts[i]
    241        if (elt.pos == pos) return elt
    242      }
    243    } else {
    244      resolveCache.set(doc, cache = new ResolveCache)
    245    }
    246    let result = cache.elts[cache.i] = ResolvedPos.resolve(doc, pos)
    247    cache.i = (cache.i + 1) % resolveCacheSize
    248    return result
    249  }
    250 }
    251 
    252 class ResolveCache {
    253  elts: ResolvedPos[] = []
    254  i = 0
    255 }
    256 
    257 const resolveCacheSize = 12, resolveCache = new WeakMap<Node, ResolveCache>()
    258 
    259 /// Represents a flat range of content, i.e. one that starts and
    260 /// ends in the same node.
    261 export class NodeRange {
    262  /// Construct a node range. `$from` and `$to` should point into the
    263  /// same node until at least the given `depth`, since a node range
    264  /// denotes an adjacent set of nodes in a single parent node.
    265  constructor(
    266    /// A resolved position along the start of the content. May have a
    267    /// `depth` greater than this object's `depth` property, since
    268    /// these are the positions that were used to compute the range,
    269    /// not re-resolved positions directly at its boundaries.
    270    readonly $from: ResolvedPos,
    271    /// A position along the end of the content. See
    272    /// caveat for [`$from`](#model.NodeRange.$from).
    273    readonly $to: ResolvedPos,
    274    /// The depth of the node that this range points into.
    275    readonly depth: number
    276  ) {}
    277 
    278  /// The position at the start of the range.
    279  get start() { return this.$from.before(this.depth + 1) }
    280  /// The position at the end of the range.
    281  get end() { return this.$to.after(this.depth + 1) }
    282 
    283  /// The parent node that the range points into.
    284  get parent() { return this.$from.node(this.depth) }
    285  /// The start index of the range in the parent node.
    286  get startIndex() { return this.$from.index(this.depth) }
    287  /// The end index of the range in the parent node.
    288  get endIndex() { return this.$to.indexAfter(this.depth) }
    289 }