tor-browser

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

transform.ts (9733B)


      1 import {Node, NodeType, Mark, MarkType, ContentMatch, Slice, Fragment, NodeRange, Attrs} from "prosemirror-model"
      2 
      3 import {Mapping} from "./map"
      4 import {Step} from "./step"
      5 import {addMark, removeMark, clearIncompatible} from "./mark"
      6 import {replaceStep, replaceRange, replaceRangeWith, deleteRange} from "./replace"
      7 import {lift, wrap, setBlockType, setNodeMarkup, split, join} from "./structure"
      8 import {AttrStep, DocAttrStep} from "./attr_step"
      9 import {AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step"
     10 
     11 /// @internal
     12 export let TransformError = class extends Error {}
     13 
     14 TransformError = function TransformError(this: any, message: string) {
     15  let err = Error.call(this, message)
     16  ;(err as any).__proto__ = TransformError.prototype
     17  return err
     18 } as any
     19 
     20 TransformError.prototype = Object.create(Error.prototype)
     21 TransformError.prototype.constructor = TransformError
     22 TransformError.prototype.name = "TransformError"
     23 
     24 /// Abstraction to build up and track an array of
     25 /// [steps](#transform.Step) representing a document transformation.
     26 ///
     27 /// Most transforming methods return the `Transform` object itself, so
     28 /// that they can be chained.
     29 export class Transform {
     30  /// The steps in this transform.
     31  readonly steps: Step[] = []
     32  /// The documents before each of the steps.
     33  readonly docs: Node[] = []
     34  /// A mapping with the maps for each of the steps in this transform.
     35  readonly mapping: Mapping = new Mapping
     36 
     37  /// Create a transform that starts with the given document.
     38  constructor(
     39    /// The current document (the result of applying the steps in the
     40    /// transform).
     41    public doc: Node
     42  ) {}
     43 
     44  /// The starting document.
     45  get before() { return this.docs.length ? this.docs[0] : this.doc }
     46 
     47  /// Apply a new step in this transform, saving the result. Throws an
     48  /// error when the step fails.
     49  step(step: Step) {
     50    let result = this.maybeStep(step)
     51    if (result.failed) throw new TransformError(result.failed)
     52    return this
     53  }
     54 
     55  /// Try to apply a step in this transformation, ignoring it if it
     56  /// fails. Returns the step result.
     57  maybeStep(step: Step) {
     58    let result = step.apply(this.doc)
     59    if (!result.failed) this.addStep(step, result.doc!)
     60    return result
     61  }
     62 
     63  /// True when the document has been changed (when there are any
     64  /// steps).
     65  get docChanged() {
     66    return this.steps.length > 0
     67  }
     68 
     69  /// @internal
     70  addStep(step: Step, doc: Node) {
     71    this.docs.push(this.doc)
     72    this.steps.push(step)
     73    this.mapping.appendMap(step.getMap())
     74    this.doc = doc
     75  }
     76 
     77  /// Replace the part of the document between `from` and `to` with the
     78  /// given `slice`.
     79  replace(from: number, to = from, slice = Slice.empty): this {
     80    let step = replaceStep(this.doc, from, to, slice)
     81    if (step) this.step(step)
     82    return this
     83  }
     84 
     85  /// Replace the given range with the given content, which may be a
     86  /// fragment, node, or array of nodes.
     87  replaceWith(from: number, to: number, content: Fragment | Node | readonly Node[]): this {
     88    return this.replace(from, to, new Slice(Fragment.from(content), 0, 0))
     89  }
     90 
     91  /// Delete the content between the given positions.
     92  delete(from: number, to: number): this {
     93    return this.replace(from, to, Slice.empty)
     94  }
     95 
     96  /// Insert the given content at the given position.
     97  insert(pos: number, content: Fragment | Node | readonly Node[]): this {
     98    return this.replaceWith(pos, pos, content)
     99  }
    100 
    101  /// Replace a range of the document with a given slice, using
    102  /// `from`, `to`, and the slice's
    103  /// [`openStart`](#model.Slice.openStart) property as hints, rather
    104  /// than fixed start and end points. This method may grow the
    105  /// replaced area or close open nodes in the slice in order to get a
    106  /// fit that is more in line with WYSIWYG expectations, by dropping
    107  /// fully covered parent nodes of the replaced region when they are
    108  /// marked [non-defining as
    109  /// context](#model.NodeSpec.definingAsContext), or including an
    110  /// open parent node from the slice that _is_ marked as [defining
    111  /// its content](#model.NodeSpec.definingForContent).
    112  ///
    113  /// This is the method, for example, to handle paste. The similar
    114  /// [`replace`](#transform.Transform.replace) method is a more
    115  /// primitive tool which will _not_ move the start and end of its given
    116  /// range, and is useful in situations where you need more precise
    117  /// control over what happens.
    118  replaceRange(from: number, to: number, slice: Slice): this {
    119    replaceRange(this, from, to, slice)
    120    return this
    121  }
    122 
    123  /// Replace the given range with a node, but use `from` and `to` as
    124  /// hints, rather than precise positions. When from and to are the same
    125  /// and are at the start or end of a parent node in which the given
    126  /// node doesn't fit, this method may _move_ them out towards a parent
    127  /// that does allow the given node to be placed. When the given range
    128  /// completely covers a parent node, this method may completely replace
    129  /// that parent node.
    130  replaceRangeWith(from: number, to: number, node: Node): this {
    131    replaceRangeWith(this, from, to, node)
    132    return this
    133  }
    134 
    135  /// Delete the given range, expanding it to cover fully covered
    136  /// parent nodes until a valid replace is found.
    137  deleteRange(from: number, to: number): this {
    138    deleteRange(this, from, to)
    139    return this
    140  }
    141 
    142  /// Split the content in the given range off from its parent, if there
    143  /// is sibling content before or after it, and move it up the tree to
    144  /// the depth specified by `target`. You'll probably want to use
    145  /// [`liftTarget`](#transform.liftTarget) to compute `target`, to make
    146  /// sure the lift is valid.
    147  lift(range: NodeRange, target: number): this {
    148    lift(this, range, target)
    149    return this
    150  }
    151 
    152  /// Join the blocks around the given position. If depth is 2, their
    153  /// last and first siblings are also joined, and so on.
    154  join(pos: number, depth: number = 1): this {
    155    join(this, pos, depth)
    156    return this
    157  }
    158 
    159  /// Wrap the given [range](#model.NodeRange) in the given set of wrappers.
    160  /// The wrappers are assumed to be valid in this position, and should
    161  /// probably be computed with [`findWrapping`](#transform.findWrapping).
    162  wrap(range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]): this {
    163    wrap(this, range, wrappers)
    164    return this
    165  }
    166 
    167  /// Set the type of all textblocks (partly) between `from` and `to` to
    168  /// the given node type with the given attributes.
    169  setBlockType(from: number, to = from, type: NodeType, attrs: Attrs | null | ((oldNode: Node) => Attrs) = null): this {
    170    setBlockType(this, from, to, type, attrs)
    171    return this
    172  }
    173 
    174  /// Change the type, attributes, and/or marks of the node at `pos`.
    175  /// When `type` isn't given, the existing node type is preserved,
    176  setNodeMarkup(pos: number, type?: NodeType | null, attrs: Attrs | null = null, marks?: readonly Mark[]): this {
    177    setNodeMarkup(this, pos, type, attrs, marks)
    178    return this
    179  }
    180 
    181  /// Set a single attribute on a given node to a new value.
    182  /// The `pos` addresses the document content. Use `setDocAttribute`
    183  /// to set attributes on the document itself.
    184  setNodeAttribute(pos: number, attr: string, value: any): this {
    185    this.step(new AttrStep(pos, attr, value))
    186    return this
    187  }
    188 
    189  /// Set a single attribute on the document to a new value.
    190  setDocAttribute(attr: string, value: any): this {
    191    this.step(new DocAttrStep(attr, value))
    192    return this
    193  }
    194 
    195  /// Add a mark to the node at position `pos`.
    196  addNodeMark(pos: number, mark: Mark): this {
    197    this.step(new AddNodeMarkStep(pos, mark))
    198    return this
    199  }
    200 
    201  /// Remove a mark (or all marks of the given type) from the node at
    202  /// position `pos`.
    203  removeNodeMark(pos: number, mark: Mark | MarkType): this {
    204    let node = this.doc.nodeAt(pos)
    205    if (!node) throw new RangeError("No node at position " + pos)
    206    if (mark instanceof Mark) {
    207      if (mark.isInSet(node.marks)) this.step(new RemoveNodeMarkStep(pos, mark))
    208    } else {
    209      let set = node.marks, found, steps: Step[] = []
    210      while (found = mark.isInSet(set)) {
    211        steps.push(new RemoveNodeMarkStep(pos, found))
    212        set = found.removeFromSet(set)
    213      }
    214      for (let i = steps.length - 1; i >= 0; i--) this.step(steps[i])
    215    }
    216    return this
    217  }
    218 
    219  /// Split the node at the given position, and optionally, if `depth` is
    220  /// greater than one, any number of nodes above that. By default, the
    221  /// parts split off will inherit the node type of the original node.
    222  /// This can be changed by passing an array of types and attributes to
    223  /// use after the split (with the outermost nodes coming first).
    224  split(pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) {
    225    split(this, pos, depth, typesAfter)
    226    return this
    227  }
    228 
    229  /// Add the given mark to the inline content between `from` and `to`.
    230  addMark(from: number, to: number, mark: Mark): this {
    231    addMark(this, from, to, mark)
    232    return this
    233  }
    234 
    235  /// Remove marks from inline nodes between `from` and `to`. When
    236  /// `mark` is a single mark, remove precisely that mark. When it is
    237  /// a mark type, remove all marks of that type. When it is null,
    238  /// remove all marks of any type.
    239  removeMark(from: number, to: number, mark?: Mark | MarkType | null) {
    240    removeMark(this, from, to, mark)
    241    return this
    242  }
    243 
    244  /// Removes all marks and nodes from the content of the node at
    245  /// `pos` that don't match the given new parent node type. Accepts
    246  /// an optional starting [content match](#model.ContentMatch) as
    247  /// third argument.
    248  clearIncompatible(pos: number, parentType: NodeType, match?: ContentMatch) {
    249    clearIncompatible(this, pos, parentType, match)
    250    return this
    251  }
    252 }