tor-browser

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

structure.ts (15448B)


      1 import {Slice, Fragment, NodeRange, NodeType, Node, Mark, Attrs, ContentMatch} from "prosemirror-model"
      2 
      3 import {Transform} from "./transform"
      4 import {ReplaceStep, ReplaceAroundStep} from "./replace_step"
      5 import {clearIncompatible} from "./mark"
      6 
      7 function canCut(node: Node, start: number, end: number) {
      8  return (start == 0 || node.canReplace(start, node.childCount)) &&
      9    (end == node.childCount || node.canReplace(0, end))
     10 }
     11 
     12 /// Try to find a target depth to which the content in the given range
     13 /// can be lifted. Will not go across
     14 /// [isolating](#model.NodeSpec.isolating) parent nodes.
     15 export function liftTarget(range: NodeRange): number | null {
     16  let parent = range.parent
     17  let content = parent.content.cutByIndex(range.startIndex, range.endIndex)
     18  for (let depth = range.depth, contentBefore = 0, contentAfter = 0;; --depth) {
     19    let node = range.$from.node(depth)
     20    let index = range.$from.index(depth) + contentBefore, endIndex = range.$to.indexAfter(depth) - contentAfter
     21    if (depth < range.depth && node.canReplace(index, endIndex, content))
     22      return depth
     23    if (depth == 0 || node.type.spec.isolating || !canCut(node, index, endIndex)) break
     24    if (index) contentBefore = 1
     25    if (endIndex < node.childCount) contentAfter = 1
     26  }
     27  return null
     28 }
     29 
     30 export function lift(tr: Transform, range: NodeRange, target: number) {
     31  let {$from, $to, depth} = range
     32 
     33  let gapStart = $from.before(depth + 1), gapEnd = $to.after(depth + 1)
     34  let start = gapStart, end = gapEnd
     35 
     36  let before = Fragment.empty, openStart = 0
     37  for (let d = depth, splitting = false; d > target; d--)
     38    if (splitting || $from.index(d) > 0) {
     39      splitting = true
     40      before = Fragment.from($from.node(d).copy(before))
     41      openStart++
     42    } else {
     43      start--
     44    }
     45  let after = Fragment.empty, openEnd = 0
     46  for (let d = depth, splitting = false; d > target; d--)
     47    if (splitting || $to.after(d + 1) < $to.end(d)) {
     48      splitting = true
     49      after = Fragment.from($to.node(d).copy(after))
     50      openEnd++
     51    } else {
     52      end++
     53    }
     54 
     55  tr.step(new ReplaceAroundStep(start, end, gapStart, gapEnd,
     56                                new Slice(before.append(after), openStart, openEnd),
     57                                before.size - openStart, true))
     58 }
     59 
     60 /// Try to find a valid way to wrap the content in the given range in a
     61 /// node of the given type. May introduce extra nodes around and inside
     62 /// the wrapper node, if necessary. Returns null if no valid wrapping
     63 /// could be found. When `innerRange` is given, that range's content is
     64 /// used as the content to fit into the wrapping, instead of the
     65 /// content of `range`.
     66 export function findWrapping(
     67  range: NodeRange,
     68  nodeType: NodeType,
     69  attrs: Attrs | null = null,
     70  innerRange = range
     71 ): {type: NodeType, attrs: Attrs | null}[] | null {
     72  let around = findWrappingOutside(range, nodeType)
     73  let inner = around && findWrappingInside(innerRange, nodeType)
     74  if (!inner) return null
     75  return (around!.map(withAttrs) as {type: NodeType, attrs: Attrs | null}[])
     76    .concat({type: nodeType, attrs}).concat(inner.map(withAttrs))
     77 }
     78 
     79 function withAttrs(type: NodeType) { return {type, attrs: null} }
     80 
     81 function findWrappingOutside(range: NodeRange, type: NodeType) {
     82  let {parent, startIndex, endIndex} = range
     83  let around = parent.contentMatchAt(startIndex).findWrapping(type)
     84  if (!around) return null
     85  let outer = around.length ? around[0] : type
     86  return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
     87 }
     88 
     89 function findWrappingInside(range: NodeRange, type: NodeType) {
     90  let {parent, startIndex, endIndex} = range
     91  let inner = parent.child(startIndex)
     92  let inside = type.contentMatch.findWrapping(inner.type)
     93  if (!inside) return null
     94  let lastType = inside.length ? inside[inside.length - 1] : type
     95  let innerMatch: ContentMatch | null = lastType.contentMatch
     96  for (let i = startIndex; innerMatch && i < endIndex; i++)
     97    innerMatch = innerMatch.matchType(parent.child(i).type)
     98  if (!innerMatch || !innerMatch.validEnd) return null
     99  return inside
    100 }
    101 
    102 export function wrap(tr: Transform, range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]) {
    103  let content = Fragment.empty
    104  for (let i = wrappers.length - 1; i >= 0; i--) {
    105    if (content.size) {
    106      let match = wrappers[i].type.contentMatch.matchFragment(content)
    107      if (!match || !match.validEnd)
    108        throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")
    109    }
    110    content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content))
    111  }
    112 
    113  let start = range.start, end = range.end
    114  tr.step(new ReplaceAroundStep(start, end, start, end, new Slice(content, 0, 0), wrappers.length, true))
    115 }
    116 
    117 export function setBlockType(tr: Transform, from: number, to: number,
    118                             type: NodeType, attrs: Attrs | null | ((oldNode: Node) => Attrs)) {
    119  if (!type.isTextblock) throw new RangeError("Type given to setBlockType should be a textblock")
    120  let mapFrom = tr.steps.length
    121  tr.doc.nodesBetween(from, to, (node, pos) => {
    122    let attrsHere = typeof attrs == "function" ? attrs(node) : attrs
    123    if (node.isTextblock && !node.hasMarkup(type, attrsHere) &&
    124        canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) {
    125      let convertNewlines = null
    126      if (type.schema.linebreakReplacement) {
    127        let pre = type.whitespace == "pre", supportLinebreak = !!type.contentMatch.matchType(type.schema.linebreakReplacement)
    128        if (pre && !supportLinebreak) convertNewlines = false
    129        else if (!pre && supportLinebreak) convertNewlines = true
    130      }
    131      // Ensure all markup that isn't allowed in the new node type is cleared
    132      if (convertNewlines === false) replaceLinebreaks(tr, node, pos, mapFrom)
    133      clearIncompatible(tr, tr.mapping.slice(mapFrom).map(pos, 1), type, undefined, convertNewlines === null)
    134      let mapping = tr.mapping.slice(mapFrom)
    135      let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1)
    136      tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1,
    137                                    new Slice(Fragment.from(type.create(attrsHere, null, node.marks)), 0, 0), 1, true))
    138      if (convertNewlines === true) replaceNewlines(tr, node, pos, mapFrom)
    139      return false
    140    }
    141  })
    142 }
    143 
    144 function replaceNewlines(tr: Transform, node: Node, pos: number, mapFrom: number) {
    145  node.forEach((child, offset) => {
    146    if (child.isText) {
    147      let m, newline = /\r?\n|\r/g
    148      while (m = newline.exec(child.text!)) {
    149        let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset + m.index)
    150        tr.replaceWith(start, start + 1, node.type.schema.linebreakReplacement!.create())
    151      }
    152    }
    153  })
    154 }
    155 
    156 function replaceLinebreaks(tr: Transform, node: Node, pos: number, mapFrom: number) {
    157  node.forEach((child, offset) => {
    158    if (child.type == child.type.schema.linebreakReplacement) {
    159      let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset)
    160      tr.replaceWith(start, start + 1, node.type.schema.text("\n"))
    161    }
    162  })
    163 }
    164 
    165 function canChangeType(doc: Node, pos: number, type: NodeType) {
    166  let $pos = doc.resolve(pos), index = $pos.index()
    167  return $pos.parent.canReplaceWith(index, index + 1, type)
    168 }
    169 
    170 /// Change the type, attributes, and/or marks of the node at `pos`.
    171 /// When `type` isn't given, the existing node type is preserved,
    172 export function setNodeMarkup(tr: Transform, pos: number, type: NodeType | undefined | null,
    173                              attrs: Attrs | null, marks: readonly Mark[] | undefined) {
    174  let node = tr.doc.nodeAt(pos)
    175  if (!node) throw new RangeError("No node at given position")
    176  if (!type) type = node.type
    177  let newNode = type.create(attrs, null, marks || node.marks)
    178  if (node.isLeaf)
    179    return tr.replaceWith(pos, pos + node.nodeSize, newNode)
    180 
    181  if (!type.validContent(node.content))
    182    throw new RangeError("Invalid content for node type " + type.name)
    183 
    184  tr.step(new ReplaceAroundStep(pos, pos + node.nodeSize, pos + 1, pos + node.nodeSize - 1,
    185                                new Slice(Fragment.from(newNode), 0, 0), 1, true))
    186 }
    187 
    188 /// Check whether splitting at the given position is allowed.
    189 export function canSplit(doc: Node, pos: number, depth = 1,
    190                         typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]): boolean {
    191  let $pos = doc.resolve(pos), base = $pos.depth - depth
    192  let innerType = (typesAfter && typesAfter[typesAfter.length - 1]) || $pos.parent
    193  if (base < 0 || $pos.parent.type.spec.isolating ||
    194      !$pos.parent.canReplace($pos.index(), $pos.parent.childCount) ||
    195      !innerType.type.validContent($pos.parent.content.cutByIndex($pos.index(), $pos.parent.childCount)))
    196    return false
    197  for (let d = $pos.depth - 1, i = depth - 2; d > base; d--, i--) {
    198    let node = $pos.node(d), index = $pos.index(d)
    199    if (node.type.spec.isolating) return false
    200    let rest = node.content.cutByIndex(index, node.childCount)
    201    let overrideChild = typesAfter && typesAfter[i + 1]
    202    if (overrideChild)
    203      rest = rest.replaceChild(0, overrideChild.type.create(overrideChild.attrs))
    204    let after = (typesAfter && typesAfter[i]) || node
    205    if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
    206      return false
    207  }
    208  let index = $pos.indexAfter(base)
    209  let baseType = typesAfter && typesAfter[0]
    210  return $pos.node(base).canReplaceWith(index, index, baseType ? baseType.type : $pos.node(base + 1).type)
    211 }
    212 
    213 export function split(tr: Transform, pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) {
    214  let $pos = tr.doc.resolve(pos), before = Fragment.empty, after = Fragment.empty
    215  for (let d = $pos.depth, e = $pos.depth - depth, i = depth - 1; d > e; d--, i--) {
    216    before = Fragment.from($pos.node(d).copy(before))
    217    let typeAfter = typesAfter && typesAfter[i]
    218    after = Fragment.from(typeAfter ? typeAfter.type.create(typeAfter.attrs, after) : $pos.node(d).copy(after))
    219  }
    220  tr.step(new ReplaceStep(pos, pos, new Slice(before.append(after), depth, depth), true))
    221 }
    222 
    223 /// Test whether the blocks before and after a given position can be
    224 /// joined.
    225 export function canJoin(doc: Node, pos: number): boolean {
    226  let $pos = doc.resolve(pos), index = $pos.index()
    227  return joinable($pos.nodeBefore, $pos.nodeAfter) &&
    228    $pos.parent.canReplace(index, index + 1)
    229 }
    230 
    231 function canAppendWithSubstitutedLinebreaks(a: Node, b: Node) {
    232  if (!b.content.size) a.type.compatibleContent(b.type)
    233  let match: ContentMatch | null = a.contentMatchAt(a.childCount)
    234  let {linebreakReplacement} = a.type.schema
    235  for (let i = 0; i < b.childCount; i++) {
    236    let child = b.child(i)
    237    let type = child.type == linebreakReplacement ? a.type.schema.nodes.text : child.type
    238    match = match.matchType(type)
    239    if (!match) return false
    240    if (!a.type.allowsMarks(child.marks)) return false
    241  }
    242  return match.validEnd
    243 }
    244 
    245 function joinable(a: Node | null, b: Node | null) {
    246  return !!(a && b && !a.isLeaf && canAppendWithSubstitutedLinebreaks(a, b))
    247 }
    248 
    249 /// Find an ancestor of the given position that can be joined to the
    250 /// block before (or after if `dir` is positive). Returns the joinable
    251 /// point, if any.
    252 export function joinPoint(doc: Node, pos: number, dir = -1) {
    253  let $pos = doc.resolve(pos)
    254  for (let d = $pos.depth;; d--) {
    255    let before, after, index = $pos.index(d)
    256    if (d == $pos.depth) {
    257      before = $pos.nodeBefore
    258      after = $pos.nodeAfter
    259    } else if (dir > 0) {
    260      before = $pos.node(d + 1)
    261      index++
    262      after = $pos.node(d).maybeChild(index)
    263    } else {
    264      before = $pos.node(d).maybeChild(index - 1)
    265      after = $pos.node(d + 1)
    266    }
    267    if (before && !before.isTextblock && joinable(before, after) &&
    268        $pos.node(d).canReplace(index, index + 1)) return pos
    269    if (d == 0) break
    270    pos = dir < 0 ? $pos.before(d) : $pos.after(d)
    271  }
    272 }
    273 
    274 export function join(tr: Transform, pos: number, depth: number) {
    275  let convertNewlines = null
    276  let {linebreakReplacement} = tr.doc.type.schema
    277  let $before = tr.doc.resolve(pos - depth), beforeType = $before.node().type
    278  if (linebreakReplacement && beforeType.inlineContent) {
    279    let pre = beforeType.whitespace == "pre"
    280    let supportLinebreak = !!beforeType.contentMatch.matchType(linebreakReplacement)
    281    if (pre && !supportLinebreak) convertNewlines = false
    282    else if (!pre && supportLinebreak) convertNewlines = true
    283  }
    284  let mapFrom = tr.steps.length
    285  if (convertNewlines === false) {
    286    let $after = tr.doc.resolve(pos + depth)
    287    replaceLinebreaks(tr, $after.node(), $after.before(), mapFrom)
    288  }
    289  if (beforeType.inlineContent)
    290    clearIncompatible(tr, pos + depth - 1, beforeType,
    291                      $before.node().contentMatchAt($before.index()), convertNewlines == null)
    292  let mapping = tr.mapping.slice(mapFrom), start = mapping.map(pos - depth)
    293  tr.step(new ReplaceStep(start, mapping.map(pos + depth, - 1), Slice.empty, true))
    294  if (convertNewlines === true) {
    295    let $full = tr.doc.resolve(start)
    296    replaceNewlines(tr, $full.node(), $full.before(), tr.steps.length)
    297  }
    298  return tr
    299 }
    300 
    301 /// Try to find a point where a node of the given type can be inserted
    302 /// near `pos`, by searching up the node hierarchy when `pos` itself
    303 /// isn't a valid place but is at the start or end of a node. Return
    304 /// null if no position was found.
    305 export function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null {
    306  let $pos = doc.resolve(pos)
    307  if ($pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType)) return pos
    308 
    309  if ($pos.parentOffset == 0)
    310    for (let d = $pos.depth - 1; d >= 0; d--) {
    311      let index = $pos.index(d)
    312      if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.before(d + 1)
    313      if (index > 0) return null
    314    }
    315  if ($pos.parentOffset == $pos.parent.content.size)
    316    for (let d = $pos.depth - 1; d >= 0; d--) {
    317      let index = $pos.indexAfter(d)
    318      if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.after(d + 1)
    319      if (index < $pos.node(d).childCount) return null
    320    }
    321  return null
    322 }
    323 
    324 /// Finds a position at or around the given position where the given
    325 /// slice can be inserted. Will look at parent nodes' nearest boundary
    326 /// and try there, even if the original position wasn't directly at the
    327 /// start or end of that node. Returns null when no position was found.
    328 export function dropPoint(doc: Node, pos: number, slice: Slice): number | null {
    329  let $pos = doc.resolve(pos)
    330  if (!slice.content.size) return pos
    331  let content = slice.content
    332  for (let i = 0; i < slice.openStart; i++) content = content.firstChild!.content
    333  for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) {
    334    for (let d = $pos.depth; d >= 0; d--) {
    335      let bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1
    336      let insertPos = $pos.index(d) + (bias > 0 ? 1 : 0)
    337      let parent = $pos.node(d), fits: boolean | null = false
    338      if (pass == 1) {
    339        fits = parent.canReplace(insertPos, insertPos, content)
    340      } else {
    341        let wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild!.type)
    342        fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0])
    343      }
    344      if (fits)
    345        return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1)
    346    }
    347  }
    348  return null
    349 }