tor-browser

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

state.ts (10514B)


      1 import {Node, Mark, Schema} from "prosemirror-model"
      2 
      3 import {Selection, TextSelection} from "./selection"
      4 import {Transaction} from "./transaction"
      5 import {Plugin, StateField} from "./plugin"
      6 
      7 function bind<T extends Function>(f: T, self: any): T {
      8  return !self || !f ? f : f.bind(self)
      9 }
     10 
     11 class FieldDesc<T> {
     12  init: (config: EditorStateConfig, instance: EditorState) => T
     13  apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T
     14 
     15  constructor(readonly name: string, desc: StateField<any>, self?: any) {
     16    this.init = bind(desc.init, self)
     17    this.apply = bind(desc.apply, self)
     18  }
     19 }
     20 
     21 const baseFields = [
     22  new FieldDesc<Node>("doc", {
     23    init(config) { return config.doc || config.schema!.topNodeType.createAndFill() },
     24    apply(tr) { return tr.doc }
     25  }),
     26 
     27  new FieldDesc<Selection>("selection", {
     28    init(config, instance) { return config.selection || Selection.atStart(instance.doc) },
     29    apply(tr) { return tr.selection }
     30  }),
     31 
     32  new FieldDesc<readonly Mark[] | null>("storedMarks", {
     33    init(config) { return config.storedMarks || null },
     34    apply(tr, _marks, _old, state) { return (state.selection as TextSelection).$cursor ? tr.storedMarks : null }
     35  }),
     36 
     37  new FieldDesc<number>("scrollToSelection", {
     38    init() { return 0 },
     39    apply(tr, prev) { return tr.scrolledIntoView ? prev + 1 : prev }
     40  })
     41 ]
     42 
     43 // Object wrapping the part of a state object that stays the same
     44 // across transactions. Stored in the state's `config` property.
     45 class Configuration {
     46  fields: FieldDesc<any>[]
     47  plugins: Plugin[] = []
     48  pluginsByKey: {[key: string]: Plugin} = Object.create(null)
     49 
     50  constructor(readonly schema: Schema, plugins?: readonly Plugin[]) {
     51    this.fields = baseFields.slice()
     52    if (plugins) plugins.forEach(plugin => {
     53      if (this.pluginsByKey[plugin.key])
     54        throw new RangeError("Adding different instances of a keyed plugin (" + plugin.key + ")")
     55      this.plugins.push(plugin)
     56      this.pluginsByKey[plugin.key] = plugin
     57      if (plugin.spec.state)
     58        this.fields.push(new FieldDesc<any>(plugin.key, plugin.spec.state, plugin))
     59    })
     60  }
     61 }
     62 
     63 /// The type of object passed to
     64 /// [`EditorState.create`](#state.EditorState^create).
     65 export interface EditorStateConfig {
     66  /// The schema to use (only relevant if no `doc` is specified).
     67  schema?: Schema
     68 
     69  /// The starting document. Either this or `schema` _must_ be
     70  /// provided.
     71  doc?: Node
     72 
     73  /// A valid selection in the document.
     74  selection?: Selection
     75 
     76  /// The initial set of [stored marks](#state.EditorState.storedMarks).
     77  storedMarks?: readonly Mark[] | null
     78 
     79  /// The plugins that should be active in this state.
     80  plugins?: readonly Plugin[]
     81 }
     82 
     83 /// The state of a ProseMirror editor is represented by an object of
     84 /// this type. A state is a persistent data structure—it isn't
     85 /// updated, but rather a new state value is computed from an old one
     86 /// using the [`apply`](#state.EditorState.apply) method.
     87 ///
     88 /// A state holds a number of built-in fields, and plugins can
     89 /// [define](#state.PluginSpec.state) additional fields.
     90 export class EditorState {
     91  /// @internal
     92  constructor(
     93    /// @internal
     94    readonly config: Configuration
     95  ) {}
     96 
     97  /// The current document.
     98  declare doc: Node
     99 
    100  /// The selection.
    101  declare selection: Selection
    102 
    103  /// A set of marks to apply to the next input. Will be null when
    104  /// no explicit marks have been set.
    105  declare storedMarks: readonly Mark[] | null
    106 
    107  /// The schema of the state's document.
    108  get schema(): Schema {
    109    return this.config.schema
    110  }
    111 
    112  /// The plugins that are active in this state.
    113  get plugins(): readonly Plugin[] {
    114    return this.config.plugins
    115  }
    116 
    117  /// Apply the given transaction to produce a new state.
    118  apply(tr: Transaction): EditorState {
    119    return this.applyTransaction(tr).state
    120  }
    121 
    122  /// @internal
    123  filterTransaction(tr: Transaction, ignore = -1) {
    124    for (let i = 0; i < this.config.plugins.length; i++) if (i != ignore) {
    125      let plugin = this.config.plugins[i]
    126      if (plugin.spec.filterTransaction && !plugin.spec.filterTransaction.call(plugin, tr, this))
    127        return false
    128    }
    129    return true
    130  }
    131 
    132  /// Verbose variant of [`apply`](#state.EditorState.apply) that
    133  /// returns the precise transactions that were applied (which might
    134  /// be influenced by the [transaction
    135  /// hooks](#state.PluginSpec.filterTransaction) of
    136  /// plugins) along with the new state.
    137  applyTransaction(rootTr: Transaction): {state: EditorState, transactions: readonly Transaction[]} {
    138    if (!this.filterTransaction(rootTr)) return {state: this, transactions: []}
    139 
    140    let trs = [rootTr], newState = this.applyInner(rootTr), seen = null
    141    // This loop repeatedly gives plugins a chance to respond to
    142    // transactions as new transactions are added, making sure to only
    143    // pass the transactions the plugin did not see before.
    144    for (;;) {
    145      let haveNew = false
    146      for (let i = 0; i < this.config.plugins.length; i++) {
    147        let plugin = this.config.plugins[i]
    148        if (plugin.spec.appendTransaction) {
    149          let n = seen ? seen[i].n : 0, oldState = seen ? seen[i].state : this
    150          let tr = n < trs.length &&
    151              plugin.spec.appendTransaction.call(plugin, n ? trs.slice(n) : trs, oldState, newState)
    152          if (tr && newState.filterTransaction(tr, i)) {
    153            tr.setMeta("appendedTransaction", rootTr)
    154            if (!seen) {
    155              seen = []
    156              for (let j = 0; j < this.config.plugins.length; j++)
    157                seen.push(j < i ? {state: newState, n: trs.length} : {state: this, n: 0})
    158            }
    159            trs.push(tr)
    160            newState = newState.applyInner(tr)
    161            haveNew = true
    162          }
    163          if (seen) seen[i] = {state: newState, n: trs.length}
    164        }
    165      }
    166      if (!haveNew) return {state: newState, transactions: trs}
    167    }
    168  }
    169 
    170  /// @internal
    171  applyInner(tr: Transaction) {
    172    if (!tr.before.eq(this.doc)) throw new RangeError("Applying a mismatched transaction")
    173    let newInstance = new EditorState(this.config), fields = this.config.fields
    174    for (let i = 0; i < fields.length; i++) {
    175      let field = fields[i]
    176      ;(newInstance as any)[field.name] = field.apply(tr, (this as any)[field.name], this, newInstance)
    177    }
    178    return newInstance
    179  }
    180 
    181  /// Accessor that constructs and returns a new [transaction](#state.Transaction) from this state.
    182  get tr(): Transaction { return new Transaction(this) }
    183 
    184  /// Create a new state.
    185  static create(config: EditorStateConfig) {
    186    let $config = new Configuration(config.doc ? config.doc.type.schema : config.schema!, config.plugins)
    187    let instance = new EditorState($config)
    188    for (let i = 0; i < $config.fields.length; i++)
    189      (instance as any)[$config.fields[i].name] = $config.fields[i].init(config, instance)
    190    return instance
    191  }
    192 
    193  /// Create a new state based on this one, but with an adjusted set
    194  /// of active plugins. State fields that exist in both sets of
    195  /// plugins are kept unchanged. Those that no longer exist are
    196  /// dropped, and those that are new are initialized using their
    197  /// [`init`](#state.StateField.init) method, passing in the new
    198  /// configuration object..
    199  reconfigure(config: {
    200    /// New set of active plugins.
    201    plugins?: readonly Plugin[]    
    202  }) {
    203    let $config = new Configuration(this.schema, config.plugins)
    204    let fields = $config.fields, instance = new EditorState($config)
    205    for (let i = 0; i < fields.length; i++) {
    206      let name = fields[i].name
    207      ;(instance as any)[name] = this.hasOwnProperty(name) ? (this as any)[name] : fields[i].init(config, instance)
    208    }
    209    return instance
    210  }
    211 
    212  /// Serialize this state to JSON. If you want to serialize the state
    213  /// of plugins, pass an object mapping property names to use in the
    214  /// resulting JSON object to plugin objects. The argument may also be
    215  /// a string or number, in which case it is ignored, to support the
    216  /// way `JSON.stringify` calls `toString` methods.
    217  toJSON(pluginFields?: {[propName: string]: Plugin}): any {
    218    let result: any = {doc: this.doc.toJSON(), selection: this.selection.toJSON()}
    219    if (this.storedMarks) result.storedMarks = this.storedMarks.map(m => m.toJSON())
    220    if (pluginFields && typeof pluginFields == 'object') for (let prop in pluginFields) {
    221      if (prop == "doc" || prop == "selection")
    222        throw new RangeError("The JSON fields `doc` and `selection` are reserved")
    223      let plugin = pluginFields[prop], state = plugin.spec.state
    224      if (state && state.toJSON) result[prop] = state.toJSON.call(plugin, (this as any)[plugin.key])
    225    }
    226    return result
    227  }
    228 
    229  /// Deserialize a JSON representation of a state. `config` should
    230  /// have at least a `schema` field, and should contain array of
    231  /// plugins to initialize the state with. `pluginFields` can be used
    232  /// to deserialize the state of plugins, by associating plugin
    233  /// instances with the property names they use in the JSON object.
    234  static fromJSON(config: {
    235    /// The schema to use.
    236    schema: Schema
    237    /// The set of active plugins.
    238    plugins?: readonly Plugin[]
    239  }, json: any, pluginFields?: {[propName: string]: Plugin}) {
    240    if (!json) throw new RangeError("Invalid input for EditorState.fromJSON")
    241    if (!config.schema) throw new RangeError("Required config field 'schema' missing")
    242    let $config = new Configuration(config.schema, config.plugins)
    243    let instance = new EditorState($config)
    244    $config.fields.forEach(field => {
    245      if (field.name == "doc") {
    246        instance.doc = Node.fromJSON(config.schema, json.doc)
    247      } else if (field.name == "selection") {
    248        instance.selection = Selection.fromJSON(instance.doc, json.selection)
    249      } else if (field.name == "storedMarks") {
    250        if (json.storedMarks) instance.storedMarks = json.storedMarks.map(config.schema.markFromJSON)
    251      } else {
    252        if (pluginFields) for (let prop in pluginFields) {
    253          let plugin = pluginFields[prop], state = plugin.spec.state
    254          if (plugin.key == field.name && state && state.fromJSON &&
    255              Object.prototype.hasOwnProperty.call(json, prop)) {
    256            // This field belongs to a plugin mapped to a JSON field, read it from there.
    257            ;(instance as any)[field.name] = state.fromJSON.call(plugin, config, json[prop], instance)
    258            return
    259          }
    260        }
    261        ;(instance as any)[field.name] = field.init(config, instance)
    262      }
    263    })
    264    return instance
    265  }
    266 }