tor-browser

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

node.ts (15895B)


      1 import {Fragment} from "./fragment"
      2 import {Mark} from "./mark"
      3 import {Schema, NodeType, Attrs, MarkType} from "./schema"
      4 import {Slice, replace} from "./replace"
      5 import {ResolvedPos} from "./resolvedpos"
      6 import {compareDeep} from "./comparedeep"
      7 
      8 const emptyAttrs: Attrs = Object.create(null)
      9 
     10 /// This class represents a node in the tree that makes up a
     11 /// ProseMirror document. So a document is an instance of `Node`, with
     12 /// children that are also instances of `Node`.
     13 ///
     14 /// Nodes are persistent data structures. Instead of changing them, you
     15 /// create new ones with the content you want. Old ones keep pointing
     16 /// at the old document shape. This is made cheaper by sharing
     17 /// structure between the old and new data as much as possible, which a
     18 /// tree shape like this (without back pointers) makes easy.
     19 ///
     20 /// **Do not** directly mutate the properties of a `Node` object. See
     21 /// [the guide](/docs/guide/#doc) for more information.
     22 export class Node {
     23  /// @internal
     24  constructor(
     25    /// The type of node that this is.
     26    readonly type: NodeType,
     27    /// An object mapping attribute names to values. The kind of
     28    /// attributes allowed and required are
     29    /// [determined](#model.NodeSpec.attrs) by the node type.
     30    readonly attrs: Attrs,
     31    // A fragment holding the node's children.
     32    content?: Fragment | null,
     33    /// The marks (things like whether it is emphasized or part of a
     34    /// link) applied to this node.
     35    readonly marks = Mark.none
     36  ) {
     37    this.content = content || Fragment.empty
     38  }
     39 
     40  /// A container holding the node's children.
     41  readonly content: Fragment
     42 
     43  /// The array of this node's child nodes.
     44  get children() { return this.content.content }
     45 
     46  /// For text nodes, this contains the node's text content.
     47  readonly text: string | undefined
     48 
     49  /// The size of this node, as defined by the integer-based [indexing
     50  /// scheme](/docs/guide/#doc.indexing). For text nodes, this is the
     51  /// amount of characters. For other leaf nodes, it is one. For
     52  /// non-leaf nodes, it is the size of the content plus two (the
     53  /// start and end token).
     54  get nodeSize(): number { return this.isLeaf ? 1 : 2 + this.content.size }
     55 
     56  /// The number of children that the node has.
     57  get childCount() { return this.content.childCount }
     58 
     59  /// Get the child node at the given index. Raises an error when the
     60  /// index is out of range.
     61  child(index: number) { return this.content.child(index) }
     62 
     63  /// Get the child node at the given index, if it exists.
     64  maybeChild(index: number) { return this.content.maybeChild(index) }
     65 
     66  /// Call `f` for every child node, passing the node, its offset
     67  /// into this parent node, and its index.
     68  forEach(f: (node: Node, offset: number, index: number) => void) { this.content.forEach(f) }
     69 
     70  /// Invoke a callback for all descendant nodes recursively between
     71  /// the given two positions that are relative to start of this
     72  /// node's content. The callback is invoked with the node, its
     73  /// position relative to the original node (method receiver), 
     74  /// its parent node, and its child index. When the callback returns
     75  /// false for a given node, that node's children will not be
     76  /// recursed over. The last parameter can be used to specify a 
     77  /// starting position to count from.
     78  nodesBetween(from: number, to: number,
     79               f: (node: Node, pos: number, parent: Node | null, index: number) => void | boolean,
     80               startPos = 0) {
     81    this.content.nodesBetween(from, to, f, startPos, this)
     82  }
     83 
     84  /// Call the given callback for every descendant node. Doesn't
     85  /// descend into a node when the callback returns `false`.
     86  descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => void | boolean) {
     87    this.nodesBetween(0, this.content.size, f)
     88  }
     89 
     90  /// Concatenates all the text nodes found in this fragment and its
     91  /// children.
     92  get textContent() {
     93    return (this.isLeaf && this.type.spec.leafText)
     94      ? this.type.spec.leafText(this)
     95      : this.textBetween(0, this.content.size, "")
     96  }
     97 
     98  /// Get all text between positions `from` and `to`. When
     99  /// `blockSeparator` is given, it will be inserted to separate text
    100  /// from different block nodes. If `leafText` is given, it'll be
    101  /// inserted for every non-text leaf node encountered, otherwise
    102  /// [`leafText`](#model.NodeSpec.leafText) will be used.
    103  textBetween(from: number, to: number, blockSeparator?: string | null,
    104              leafText?: null | string | ((leafNode: Node) => string)) {
    105    return this.content.textBetween(from, to, blockSeparator, leafText)
    106  }
    107 
    108  /// Returns this node's first child, or `null` if there are no
    109  /// children.
    110  get firstChild(): Node | null { return this.content.firstChild }
    111 
    112  /// Returns this node's last child, or `null` if there are no
    113  /// children.
    114  get lastChild(): Node | null { return this.content.lastChild }
    115 
    116  /// Test whether two nodes represent the same piece of document.
    117  eq(other: Node) {
    118    return this == other || (this.sameMarkup(other) && this.content.eq(other.content))
    119  }
    120 
    121  /// Compare the markup (type, attributes, and marks) of this node to
    122  /// those of another. Returns `true` if both have the same markup.
    123  sameMarkup(other: Node) {
    124    return this.hasMarkup(other.type, other.attrs, other.marks)
    125  }
    126 
    127  /// Check whether this node's markup correspond to the given type,
    128  /// attributes, and marks.
    129  hasMarkup(type: NodeType, attrs?: Attrs | null, marks?: readonly Mark[]): boolean {
    130    return this.type == type &&
    131      compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
    132      Mark.sameSet(this.marks, marks || Mark.none)
    133  }
    134 
    135  /// Create a new node with the same markup as this node, containing
    136  /// the given content (or empty, if no content is given).
    137  copy(content: Fragment | null = null): Node {
    138    if (content == this.content) return this
    139    return new Node(this.type, this.attrs, content, this.marks)
    140  }
    141 
    142  /// Create a copy of this node, with the given set of marks instead
    143  /// of the node's own marks.
    144  mark(marks: readonly Mark[]): Node {
    145    return marks == this.marks ? this : new Node(this.type, this.attrs, this.content, marks)
    146  }
    147 
    148  /// Create a copy of this node with only the content between the
    149  /// given positions. If `to` is not given, it defaults to the end of
    150  /// the node.
    151  cut(from: number, to: number = this.content.size): Node {
    152    if (from == 0 && to == this.content.size) return this
    153    return this.copy(this.content.cut(from, to))
    154  }
    155 
    156  /// Cut out the part of the document between the given positions, and
    157  /// return it as a `Slice` object.
    158  slice(from: number, to: number = this.content.size, includeParents = false) {
    159    if (from == to) return Slice.empty
    160 
    161    let $from = this.resolve(from), $to = this.resolve(to)
    162    let depth = includeParents ? 0 : $from.sharedDepth(to)
    163    let start = $from.start(depth), node = $from.node(depth)
    164    let content = node.content.cut($from.pos - start, $to.pos - start)
    165    return new Slice(content, $from.depth - depth, $to.depth - depth)
    166  }
    167 
    168  /// Replace the part of the document between the given positions with
    169  /// the given slice. The slice must 'fit', meaning its open sides
    170  /// must be able to connect to the surrounding content, and its
    171  /// content nodes must be valid children for the node they are placed
    172  /// into. If any of this is violated, an error of type
    173  /// [`ReplaceError`](#model.ReplaceError) is thrown.
    174  replace(from: number, to: number, slice: Slice) {
    175    return replace(this.resolve(from), this.resolve(to), slice)
    176  }
    177 
    178  /// Find the node directly after the given position.
    179  nodeAt(pos: number): Node | null {
    180    for (let node: Node | null = this;;) {
    181      let {index, offset} = node.content.findIndex(pos)
    182      node = node.maybeChild(index)
    183      if (!node) return null
    184      if (offset == pos || node.isText) return node
    185      pos -= offset + 1
    186    }
    187  }
    188 
    189  /// Find the (direct) child node after the given offset, if any,
    190  /// and return it along with its index and offset relative to this
    191  /// node.
    192  childAfter(pos: number): {node: Node | null, index: number, offset: number} {
    193    let {index, offset} = this.content.findIndex(pos)
    194    return {node: this.content.maybeChild(index), index, offset}
    195  }
    196 
    197  /// Find the (direct) child node before the given offset, if any,
    198  /// and return it along with its index and offset relative to this
    199  /// node.
    200  childBefore(pos: number): {node: Node | null, index: number, offset: number} {
    201    if (pos == 0) return {node: null, index: 0, offset: 0}
    202    let {index, offset} = this.content.findIndex(pos)
    203    if (offset < pos) return {node: this.content.child(index), index, offset}
    204    let node = this.content.child(index - 1)
    205    return {node, index: index - 1, offset: offset - node.nodeSize}
    206  }
    207 
    208  /// Resolve the given position in the document, returning an
    209  /// [object](#model.ResolvedPos) with information about its context.
    210  resolve(pos: number) { return ResolvedPos.resolveCached(this, pos) }
    211 
    212  /// @internal
    213  resolveNoCache(pos: number) { return ResolvedPos.resolve(this, pos) }
    214 
    215  /// Test whether a given mark or mark type occurs in this document
    216  /// between the two given positions.
    217  rangeHasMark(from: number, to: number, type: Mark | MarkType): boolean {
    218    let found = false
    219    if (to > from) this.nodesBetween(from, to, node => {
    220      if (type.isInSet(node.marks)) found = true
    221      return !found
    222    })
    223    return found
    224  }
    225 
    226  /// True when this is a block (non-inline node)
    227  get isBlock() { return this.type.isBlock }
    228 
    229  /// True when this is a textblock node, a block node with inline
    230  /// content.
    231  get isTextblock() { return this.type.isTextblock }
    232 
    233  /// True when this node allows inline content.
    234  get inlineContent() { return this.type.inlineContent }
    235 
    236  /// True when this is an inline node (a text node or a node that can
    237  /// appear among text).
    238  get isInline() { return this.type.isInline }
    239 
    240  /// True when this is a text node.
    241  get isText() { return this.type.isText }
    242 
    243  /// True when this is a leaf node.
    244  get isLeaf() { return this.type.isLeaf }
    245 
    246  /// True when this is an atom, i.e. when it does not have directly
    247  /// editable content. This is usually the same as `isLeaf`, but can
    248  /// be configured with the [`atom` property](#model.NodeSpec.atom)
    249  /// on a node's spec (typically used when the node is displayed as
    250  /// an uneditable [node view](#view.NodeView)).
    251  get isAtom() { return this.type.isAtom }
    252 
    253  /// Return a string representation of this node for debugging
    254  /// purposes.
    255  toString(): string {
    256    if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this)
    257    let name = this.type.name
    258    if (this.content.size)
    259      name += "(" + this.content.toStringInner() + ")"
    260    return wrapMarks(this.marks, name)
    261  }
    262 
    263  /// Get the content match in this node at the given index.
    264  contentMatchAt(index: number) {
    265    let match = this.type.contentMatch.matchFragment(this.content, 0, index)
    266    if (!match) throw new Error("Called contentMatchAt on a node with invalid content")
    267    return match
    268  }
    269 
    270  /// Test whether replacing the range between `from` and `to` (by
    271  /// child index) with the given replacement fragment (which defaults
    272  /// to the empty fragment) would leave the node's content valid. You
    273  /// can optionally pass `start` and `end` indices into the
    274  /// replacement fragment.
    275  canReplace(from: number, to: number, replacement = Fragment.empty, start = 0, end = replacement.childCount) {
    276    let one = this.contentMatchAt(from).matchFragment(replacement, start, end)
    277    let two = one && one.matchFragment(this.content, to)
    278    if (!two || !two.validEnd) return false
    279    for (let i = start; i < end; i++) if (!this.type.allowsMarks(replacement.child(i).marks)) return false
    280    return true
    281  }
    282 
    283  /// Test whether replacing the range `from` to `to` (by index) with
    284  /// a node of the given type would leave the node's content valid.
    285  canReplaceWith(from: number, to: number, type: NodeType, marks?: readonly Mark[]) {
    286    if (marks && !this.type.allowsMarks(marks)) return false
    287    let start = this.contentMatchAt(from).matchType(type)
    288    let end = start && start.matchFragment(this.content, to)
    289    return end ? end.validEnd : false
    290  }
    291 
    292  /// Test whether the given node's content could be appended to this
    293  /// node. If that node is empty, this will only return true if there
    294  /// is at least one node type that can appear in both nodes (to avoid
    295  /// merging completely incompatible nodes).
    296  canAppend(other: Node) {
    297    if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content)
    298    else return this.type.compatibleContent(other.type)
    299  }
    300 
    301  /// Check whether this node and its descendants conform to the
    302  /// schema, and raise an exception when they do not.
    303  check() {
    304    this.type.checkContent(this.content)
    305    this.type.checkAttrs(this.attrs)
    306    let copy = Mark.none
    307    for (let i = 0; i < this.marks.length; i++) {
    308      let mark = this.marks[i]
    309      mark.type.checkAttrs(mark.attrs)
    310      copy = mark.addToSet(copy)
    311    }
    312    if (!Mark.sameSet(copy, this.marks))
    313      throw new RangeError(`Invalid collection of marks for node ${this.type.name}: ${this.marks.map(m => m.type.name)}`)
    314    this.content.forEach(node => node.check())
    315  }
    316 
    317  /// Return a JSON-serializeable representation of this node.
    318  toJSON(): any {
    319    let obj: any = {type: this.type.name}
    320    for (let _ in this.attrs) {
    321      obj.attrs = this.attrs
    322      break
    323    }
    324    if (this.content.size)
    325      obj.content = this.content.toJSON()
    326    if (this.marks.length)
    327      obj.marks = this.marks.map(n => n.toJSON())
    328    return obj
    329  }
    330 
    331  /// Deserialize a node from its JSON representation.
    332  static fromJSON(schema: Schema, json: any): Node {
    333    if (!json) throw new RangeError("Invalid input for Node.fromJSON")
    334    let marks: Mark[] | undefined = undefined
    335    if (json.marks) {
    336      if (!Array.isArray(json.marks)) throw new RangeError("Invalid mark data for Node.fromJSON")
    337      marks = json.marks.map(schema.markFromJSON)
    338    }
    339    if (json.type == "text") {
    340      if (typeof json.text != "string") throw new RangeError("Invalid text node in JSON")
    341      return schema.text(json.text, marks)
    342    }
    343    let content = Fragment.fromJSON(schema, json.content)
    344    let node = schema.nodeType(json.type).create(json.attrs, content, marks)
    345    node.type.checkAttrs(node.attrs)
    346    return node
    347  }
    348 }
    349 
    350 ;(Node.prototype as any).text = undefined
    351 
    352 export class TextNode extends Node {
    353  readonly text: string
    354 
    355  /// @internal
    356  constructor(type: NodeType, attrs: Attrs, content: string, marks?: readonly Mark[]) {
    357    super(type, attrs, null, marks)
    358    if (!content) throw new RangeError("Empty text nodes are not allowed")
    359    this.text = content
    360  }
    361 
    362  toString() {
    363    if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this)
    364    return wrapMarks(this.marks, JSON.stringify(this.text))
    365  }
    366 
    367  get textContent() { return this.text }
    368 
    369  textBetween(from: number, to: number) { return this.text.slice(from, to) }
    370 
    371  get nodeSize() { return this.text.length }
    372 
    373  mark(marks: readonly Mark[]) {
    374    return marks == this.marks ? this : new TextNode(this.type, this.attrs, this.text, marks)
    375  }
    376 
    377  withText(text: string) {
    378    if (text == this.text) return this
    379    return new TextNode(this.type, this.attrs, text, this.marks)
    380  }
    381 
    382  cut(from = 0, to = this.text.length) {
    383    if (from == 0 && to == this.text.length) return this
    384    return this.withText(this.text.slice(from, to))
    385  }
    386 
    387  eq(other: Node) {
    388    return this.sameMarkup(other) && this.text == other.text
    389  }
    390 
    391  toJSON() {
    392    let base = super.toJSON()
    393    base.text = this.text
    394    return base
    395  }
    396 }
    397 
    398 function wrapMarks(marks: readonly Mark[], str: string) {
    399  for (let i = marks.length - 1; i >= 0; i--)
    400    str = marks[i].type.name + "(" + str + ")"
    401  return str
    402 }