tor-browser

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

replace.ts (9145B)


      1 import {Fragment} from "./fragment"
      2 import {Schema} from "./schema"
      3 import {Node, TextNode} from "./node"
      4 import {ResolvedPos} from "./resolvedpos"
      5 
      6 /// Error type raised by [`Node.replace`](#model.Node.replace) when
      7 /// given an invalid replacement.
      8 export class ReplaceError extends Error {}
      9 /*
     10 ReplaceError = function(this: any, message: string) {
     11  let err = Error.call(this, message)
     12  ;(err as any).__proto__ = ReplaceError.prototype
     13  return err
     14 } as any
     15 
     16 ReplaceError.prototype = Object.create(Error.prototype)
     17 ReplaceError.prototype.constructor = ReplaceError
     18 ReplaceError.prototype.name = "ReplaceError"
     19 */
     20 
     21 /// A slice represents a piece cut out of a larger document. It
     22 /// stores not only a fragment, but also the depth up to which nodes on
     23 /// both side are ‘open’ (cut through).
     24 export class Slice {
     25  /// Create a slice. When specifying a non-zero open depth, you must
     26  /// make sure that there are nodes of at least that depth at the
     27  /// appropriate side of the fragment—i.e. if the fragment is an
     28  /// empty paragraph node, `openStart` and `openEnd` can't be greater
     29  /// than 1.
     30  ///
     31  /// It is not necessary for the content of open nodes to conform to
     32  /// the schema's content constraints, though it should be a valid
     33  /// start/end/middle for such a node, depending on which sides are
     34  /// open.
     35  constructor(
     36    /// The slice's content.
     37    readonly content: Fragment,
     38    /// The open depth at the start of the fragment.
     39    readonly openStart: number,
     40    /// The open depth at the end.
     41    readonly openEnd: number
     42  ) {}
     43 
     44  /// The size this slice would add when inserted into a document.
     45  get size(): number {
     46    return this.content.size - this.openStart - this.openEnd
     47  }
     48 
     49  /// @internal
     50  insertAt(pos: number, fragment: Fragment) {
     51    let content = insertInto(this.content, pos + this.openStart, fragment)
     52    return content && new Slice(content, this.openStart, this.openEnd)
     53  }
     54 
     55  /// @internal
     56  removeBetween(from: number, to: number) {
     57    return new Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd)
     58  }
     59 
     60  /// Tests whether this slice is equal to another slice.
     61  eq(other: Slice): boolean {
     62    return this.content.eq(other.content) && this.openStart == other.openStart && this.openEnd == other.openEnd
     63  }
     64 
     65  /// @internal
     66  toString() {
     67    return this.content + "(" + this.openStart + "," + this.openEnd + ")"
     68  }
     69 
     70  /// Convert a slice to a JSON-serializable representation.
     71  toJSON(): any {
     72    if (!this.content.size) return null
     73    let json: any = {content: this.content.toJSON()}
     74    if (this.openStart > 0) json.openStart = this.openStart
     75    if (this.openEnd > 0) json.openEnd = this.openEnd
     76    return json
     77  }
     78 
     79  /// Deserialize a slice from its JSON representation.
     80  static fromJSON(schema: Schema, json: any): Slice {
     81    if (!json) return Slice.empty
     82    let openStart = json.openStart || 0, openEnd = json.openEnd || 0
     83    if (typeof openStart != "number" || typeof openEnd != "number")
     84      throw new RangeError("Invalid input for Slice.fromJSON")
     85    return new Slice(Fragment.fromJSON(schema, json.content), openStart, openEnd)
     86  }
     87 
     88  /// Create a slice from a fragment by taking the maximum possible
     89  /// open value on both side of the fragment.
     90  static maxOpen(fragment: Fragment, openIsolating = true) {
     91    let openStart = 0, openEnd = 0
     92    for (let n = fragment.firstChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.firstChild) openStart++
     93    for (let n = fragment.lastChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.lastChild) openEnd++
     94    return new Slice(fragment, openStart, openEnd)
     95  }
     96 
     97  /// The empty slice.
     98  static empty = new Slice(Fragment.empty, 0, 0)
     99 }
    100 
    101 function removeRange(content: Fragment, from: number, to: number): Fragment {
    102  let {index, offset} = content.findIndex(from), child = content.maybeChild(index)
    103  let {index: indexTo, offset: offsetTo} = content.findIndex(to)
    104  if (offset == from || child!.isText) {
    105    if (offsetTo != to && !content.child(indexTo).isText) throw new RangeError("Removing non-flat range")
    106    return content.cut(0, from).append(content.cut(to))
    107  }
    108  if (index != indexTo) throw new RangeError("Removing non-flat range")
    109  return content.replaceChild(index, child!.copy(removeRange(child!.content, from - offset - 1, to - offset - 1)))
    110 }
    111 
    112 function insertInto(content: Fragment, dist: number, insert: Fragment, parent?: Node | null): Fragment | null {
    113  let {index, offset} = content.findIndex(dist), child = content.maybeChild(index)
    114  if (offset == dist || child!.isText) {
    115    if (parent && !parent.canReplace(index, index, insert)) return null
    116    return content.cut(0, dist).append(insert).append(content.cut(dist))
    117  }
    118  let inner = insertInto(child!.content, dist - offset - 1, insert, child)
    119  return inner && content.replaceChild(index, child!.copy(inner))
    120 }
    121 
    122 export function replace($from: ResolvedPos, $to: ResolvedPos, slice: Slice) {
    123  if (slice.openStart > $from.depth)
    124    throw new ReplaceError("Inserted content deeper than insertion position")
    125  if ($from.depth - slice.openStart != $to.depth - slice.openEnd)
    126    throw new ReplaceError("Inconsistent open depths")
    127  return replaceOuter($from, $to, slice, 0)
    128 }
    129 
    130 function replaceOuter($from: ResolvedPos, $to: ResolvedPos, slice: Slice, depth: number): Node {
    131  let index = $from.index(depth), node = $from.node(depth)
    132  if (index == $to.index(depth) && depth < $from.depth - slice.openStart) {
    133    let inner = replaceOuter($from, $to, slice, depth + 1)
    134    return node.copy(node.content.replaceChild(index, inner))
    135  } else if (!slice.content.size) {
    136    return close(node, replaceTwoWay($from, $to, depth))
    137  } else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case
    138    let parent = $from.parent, content = parent.content
    139    return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset)))
    140  } else {
    141    let {start, end} = prepareSliceForReplace(slice, $from)
    142    return close(node, replaceThreeWay($from, start, end, $to, depth))
    143  }
    144 }
    145 
    146 function checkJoin(main: Node, sub: Node) {
    147  if (!sub.type.compatibleContent(main.type))
    148    throw new ReplaceError("Cannot join " + sub.type.name + " onto " + main.type.name)
    149 }
    150 
    151 function joinable($before: ResolvedPos, $after: ResolvedPos, depth: number) {
    152  let node = $before.node(depth)
    153  checkJoin(node, $after.node(depth))
    154  return node
    155 }
    156 
    157 function addNode(child: Node, target: Node[]) {
    158  let last = target.length - 1
    159  if (last >= 0 && child.isText && child.sameMarkup(target[last]))
    160    target[last] = (child as TextNode).withText(target[last].text! + child.text!)
    161  else
    162    target.push(child)
    163 }
    164 
    165 function addRange($start: ResolvedPos | null, $end: ResolvedPos | null, depth: number, target: Node[]) {
    166  let node = ($end || $start)!.node(depth)
    167  let startIndex = 0, endIndex = $end ? $end.index(depth) : node.childCount
    168  if ($start) {
    169    startIndex = $start.index(depth)
    170    if ($start.depth > depth) {
    171      startIndex++
    172    } else if ($start.textOffset) {
    173      addNode($start.nodeAfter!, target)
    174      startIndex++
    175    }
    176  }
    177  for (let i = startIndex; i < endIndex; i++) addNode(node.child(i), target)
    178  if ($end && $end.depth == depth && $end.textOffset)
    179    addNode($end.nodeBefore!, target)
    180 }
    181 
    182 function close(node: Node, content: Fragment) {
    183  node.type.checkContent(content)
    184  return node.copy(content)
    185 }
    186 
    187 function replaceThreeWay($from: ResolvedPos, $start: ResolvedPos, $end: ResolvedPos, $to: ResolvedPos, depth: number) {
    188  let openStart = $from.depth > depth && joinable($from, $start, depth + 1)
    189  let openEnd = $to.depth > depth && joinable($end, $to, depth + 1)
    190 
    191  let content: Node[] = []
    192  addRange(null, $from, depth, content)
    193  if (openStart && openEnd && $start.index(depth) == $end.index(depth)) {
    194    checkJoin(openStart, openEnd)
    195    addNode(close(openStart, replaceThreeWay($from, $start, $end, $to, depth + 1)), content)
    196  } else {
    197    if (openStart)
    198      addNode(close(openStart, replaceTwoWay($from, $start, depth + 1)), content)
    199    addRange($start, $end, depth, content)
    200    if (openEnd)
    201      addNode(close(openEnd, replaceTwoWay($end, $to, depth + 1)), content)
    202  }
    203  addRange($to, null, depth, content)
    204  return new Fragment(content)
    205 }
    206 
    207 function replaceTwoWay($from: ResolvedPos, $to: ResolvedPos, depth: number) {
    208  let content: Node[] = []
    209  addRange(null, $from, depth, content)
    210  if ($from.depth > depth) {
    211    let type = joinable($from, $to, depth + 1)
    212    addNode(close(type, replaceTwoWay($from, $to, depth + 1)), content)
    213  }
    214  addRange($to, null, depth, content)
    215  return new Fragment(content)
    216 }
    217 
    218 function prepareSliceForReplace(slice: Slice, $along: ResolvedPos) {
    219  let extra = $along.depth - slice.openStart, parent = $along.node(extra)
    220  let node = parent.copy(slice.content)
    221  for (let i = extra - 1; i >= 0; i--)
    222    node = $along.node(i).copy(Fragment.from(node))
    223  return {start: node.resolveNoCache(slice.openStart + extra),
    224          end: node.resolveNoCache(node.content.size - slice.openEnd - extra)}
    225 }