tor-browser

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

map.ts (10962B)


      1 /// There are several things that positions can be mapped through.
      2 /// Such objects conform to this interface.
      3 export interface Mappable {
      4  /// Map a position through this object. When given, `assoc` (should
      5  /// be -1 or 1, defaults to 1) determines with which side the
      6  /// position is associated, which determines in which direction to
      7  /// move when a chunk of content is inserted at the mapped position.
      8  map: (pos: number, assoc?: number) => number
      9 
     10  /// Map a position, and return an object containing additional
     11  /// information about the mapping. The result's `deleted` field tells
     12  /// you whether the position was deleted (completely enclosed in a
     13  /// replaced range) during the mapping. When content on only one side
     14  /// is deleted, the position itself is only considered deleted when
     15  /// `assoc` points in the direction of the deleted content.
     16  mapResult: (pos: number, assoc?: number) => MapResult
     17 }
     18 
     19 // Recovery values encode a range index and an offset. They are
     20 // represented as numbers, because tons of them will be created when
     21 // mapping, for example, a large number of decorations. The number's
     22 // lower 16 bits provide the index, the remaining bits the offset.
     23 //
     24 // Note: We intentionally don't use bit shift operators to en- and
     25 // decode these, since those clip to 32 bits, which we might in rare
     26 // cases want to overflow. A 64-bit float can represent 48-bit
     27 // integers precisely.
     28 
     29 const lower16 = 0xffff
     30 const factor16 = Math.pow(2, 16)
     31 
     32 function makeRecover(index: number, offset: number) { return index + offset * factor16 }
     33 function recoverIndex(value: number) { return value & lower16 }
     34 function recoverOffset(value: number) { return (value - (value & lower16)) / factor16 }
     35 
     36 const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8
     37 
     38 /// An object representing a mapped position with extra
     39 /// information.
     40 export class MapResult {
     41  /// @internal
     42  constructor(
     43    /// The mapped version of the position.
     44    readonly pos: number,
     45    /// @internal
     46    readonly delInfo: number,
     47    /// @internal
     48    readonly recover: number | null
     49  ) {}
     50 
     51  /// Tells you whether the position was deleted, that is, whether the
     52  /// step removed the token on the side queried (via the `assoc`)
     53  /// argument from the document.
     54  get deleted() { return (this.delInfo & DEL_SIDE) > 0 }
     55 
     56  /// Tells you whether the token before the mapped position was deleted.
     57  get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0 }
     58 
     59  /// True when the token after the mapped position was deleted.
     60  get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0 }
     61 
     62  /// Tells whether any of the steps mapped through deletes across the
     63  /// position (including both the token before and after the
     64  /// position).
     65  get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0 }
     66 }
     67 
     68 /// A map describing the deletions and insertions made by a step, which
     69 /// can be used to find the correspondence between positions in the
     70 /// pre-step version of a document and the same position in the
     71 /// post-step version.
     72 export class StepMap implements Mappable {
     73  /// Create a position map. The modifications to the document are
     74  /// represented as an array of numbers, in which each group of three
     75  /// represents a modified chunk as `[start, oldSize, newSize]`.
     76  constructor(
     77    /// @internal
     78    readonly ranges: readonly number[],
     79    /// @internal
     80    readonly inverted = false
     81  ) {
     82    if (!ranges.length && StepMap.empty) return StepMap.empty
     83  }
     84 
     85  /// @internal
     86  recover(value: number) {
     87    let diff = 0, index = recoverIndex(value)
     88    if (!this.inverted) for (let i = 0; i < index; i++)
     89      diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1]
     90    return this.ranges[index * 3] + diff + recoverOffset(value)
     91  }
     92 
     93  mapResult(pos: number, assoc = 1): MapResult { return this._map(pos, assoc, false) as MapResult }
     94 
     95  map(pos: number, assoc = 1): number { return this._map(pos, assoc, true) as number }
     96 
     97  /// @internal
     98  _map(pos: number, assoc: number, simple: boolean) {
     99    let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
    100    for (let i = 0; i < this.ranges.length; i += 3) {
    101      let start = this.ranges[i] - (this.inverted ? diff : 0)
    102      if (start > pos) break
    103      let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize
    104      if (pos <= end) {
    105        let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc
    106        let result = start + diff + (side < 0 ? 0 : newSize)
    107        if (simple) return result
    108        let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start)
    109        let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS
    110        if (assoc < 0 ? pos != start : pos != end) del |= DEL_SIDE
    111        return new MapResult(result, del, recover)
    112      }
    113      diff += newSize - oldSize
    114    }
    115    return simple ? pos + diff : new MapResult(pos + diff, 0, null)
    116  }
    117 
    118  /// @internal
    119  touches(pos: number, recover: number) {
    120    let diff = 0, index = recoverIndex(recover)
    121    let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
    122    for (let i = 0; i < this.ranges.length; i += 3) {
    123      let start = this.ranges[i] - (this.inverted ? diff : 0)
    124      if (start > pos) break
    125      let oldSize = this.ranges[i + oldIndex], end = start + oldSize
    126      if (pos <= end && i == index * 3) return true
    127      diff += this.ranges[i + newIndex] - oldSize
    128    }
    129    return false
    130  }
    131 
    132  /// Calls the given function on each of the changed ranges included in
    133  /// this map.
    134  forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void) {
    135    let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
    136    for (let i = 0, diff = 0; i < this.ranges.length; i += 3) {
    137      let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff)
    138      let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex]
    139      f(oldStart, oldStart + oldSize, newStart, newStart + newSize)
    140      diff += newSize - oldSize
    141    }
    142  }
    143 
    144  /// Create an inverted version of this map. The result can be used to
    145  /// map positions in the post-step document to the pre-step document.
    146  invert() {
    147    return new StepMap(this.ranges, !this.inverted)
    148  }
    149 
    150  /// @internal
    151  toString() {
    152    return (this.inverted ? "-" : "") + JSON.stringify(this.ranges)
    153  }
    154 
    155  /// Create a map that moves all positions by offset `n` (which may be
    156  /// negative). This can be useful when applying steps meant for a
    157  /// sub-document to a larger document, or vice-versa.
    158  static offset(n: number) {
    159    return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n])
    160  }
    161 
    162  /// A StepMap that contains no changed ranges.
    163  static empty = new StepMap([])
    164 }
    165 
    166 /// A mapping represents a pipeline of zero or more [step
    167 /// maps](#transform.StepMap). It has special provisions for losslessly
    168 /// handling mapping positions through a series of steps in which some
    169 /// steps are inverted versions of earlier steps. (This comes up when
    170 /// ‘[rebasing](/docs/guide/#transform.rebasing)’ steps for
    171 /// collaboration or history management.)
    172 export class Mapping implements Mappable {
    173  /// Create a new mapping with the given position maps.
    174  constructor(
    175    maps?: readonly StepMap[],
    176    /// @internal
    177    public mirror?: number[],
    178    /// The starting position in the `maps` array, used when `map` or
    179    /// `mapResult` is called.
    180    public from = 0,
    181    /// The end position in the `maps` array.
    182    public to = maps ? maps.length : 0
    183  ) {
    184    this._maps = (maps as StepMap[]) || []
    185    this.ownData = !(maps || mirror)
    186  }
    187 
    188  /// The step maps in this mapping.
    189  get maps(): readonly StepMap[] { return this._maps }
    190 
    191  private _maps: StepMap[]
    192  // False if maps/mirror are shared arrays that we shouldn't mutate
    193  private ownData: boolean
    194 
    195  /// Create a mapping that maps only through a part of this one.
    196  slice(from = 0, to = this.maps.length) {
    197    return new Mapping(this._maps, this.mirror, from, to)
    198  }
    199 
    200  /// Add a step map to the end of this mapping. If `mirrors` is
    201  /// given, it should be the index of the step map that is the mirror
    202  /// image of this one.
    203  appendMap(map: StepMap, mirrors?: number) {
    204    if (!this.ownData) {
    205      this._maps = this._maps.slice()
    206      this.mirror = this.mirror && this.mirror.slice()
    207      this.ownData = true
    208    }
    209    this.to = this._maps.push(map)
    210    if (mirrors != null) this.setMirror(this._maps.length - 1, mirrors)
    211  }
    212 
    213  /// Add all the step maps in a given mapping to this one (preserving
    214  /// mirroring information).
    215  appendMapping(mapping: Mapping) {
    216    for (let i = 0, startSize = this._maps.length; i < mapping._maps.length; i++) {
    217      let mirr = mapping.getMirror(i)
    218      this.appendMap(mapping._maps[i], mirr != null && mirr < i ? startSize + mirr : undefined)
    219    }
    220  }
    221 
    222  /// Finds the offset of the step map that mirrors the map at the
    223  /// given offset, in this mapping (as per the second argument to
    224  /// `appendMap`).
    225  getMirror(n: number): number | undefined {
    226    if (this.mirror) for (let i = 0; i < this.mirror.length; i++)
    227      if (this.mirror[i] == n) return this.mirror[i + (i % 2 ? -1 : 1)]
    228  }
    229 
    230  /// @internal
    231  setMirror(n: number, m: number) {
    232    if (!this.mirror) this.mirror = []
    233    this.mirror.push(n, m)
    234  }
    235 
    236  /// Append the inverse of the given mapping to this one.
    237  appendMappingInverted(mapping: Mapping) {
    238    for (let i = mapping.maps.length - 1, totalSize = this._maps.length + mapping._maps.length; i >= 0; i--) {
    239      let mirr = mapping.getMirror(i)
    240      this.appendMap(mapping._maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined)
    241    }
    242  }
    243 
    244  /// Create an inverted version of this mapping.
    245  invert() {
    246    let inverse = new Mapping
    247    inverse.appendMappingInverted(this)
    248    return inverse
    249  }
    250 
    251  /// Map a position through this mapping.
    252  map(pos: number, assoc = 1) {
    253    if (this.mirror) return this._map(pos, assoc, true) as number
    254    for (let i = this.from; i < this.to; i++)
    255      pos = this._maps[i].map(pos, assoc)
    256    return pos
    257  }
    258 
    259  /// Map a position through this mapping, returning a mapping
    260  /// result.
    261  mapResult(pos: number, assoc = 1) { return this._map(pos, assoc, false) as MapResult }
    262 
    263  /// @internal
    264  _map(pos: number, assoc: number, simple: boolean) {
    265    let delInfo = 0
    266 
    267    for (let i = this.from; i < this.to; i++) {
    268      let map = this._maps[i], result = map.mapResult(pos, assoc)
    269      if (result.recover != null) {
    270        let corr = this.getMirror(i)
    271        if (corr != null && corr > i && corr < this.to) {
    272          i = corr
    273          pos = this._maps[corr].recover(result.recover)
    274          continue
    275        }
    276      }
    277 
    278      delInfo |= result.delInfo
    279      pos = result.pos
    280    }
    281 
    282    return simple ? pos : new MapResult(pos, delInfo, null)
    283  }
    284 }