tor-browser

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

schema.ts (27591B)


      1 import OrderedMap from "orderedmap"
      2 
      3 import {Node, TextNode} from "./node"
      4 import {Fragment} from "./fragment"
      5 import {Mark} from "./mark"
      6 import {ContentMatch} from "./content"
      7 import {DOMOutputSpec} from "./to_dom"
      8 import {ParseRule, TagParseRule} from "./from_dom"
      9 
     10 /// An object holding the attributes of a node.
     11 export type Attrs = {readonly [attr: string]: any}
     12 
     13 // For node types where all attrs have a default value (or which don't
     14 // have any attributes), build up a single reusable default attribute
     15 // object, and use it for all nodes that don't specify specific
     16 // attributes.
     17 function defaultAttrs(attrs: {[name: string]: Attribute}) {
     18  let defaults = Object.create(null)
     19  for (let attrName in attrs) {
     20    let attr = attrs[attrName]
     21    if (!attr.hasDefault) return null
     22    defaults[attrName] = attr.default
     23  }
     24  return defaults
     25 }
     26 
     27 function computeAttrs(attrs: {[name: string]: Attribute}, value: Attrs | null) {
     28  let built = Object.create(null)
     29  for (let name in attrs) {
     30    let given = value && value[name]
     31    if (given === undefined) {
     32      let attr = attrs[name]
     33      if (attr.hasDefault) given = attr.default
     34      else throw new RangeError("No value supplied for attribute " + name)
     35    }
     36    built[name] = given
     37  }
     38  return built
     39 }
     40 
     41 export function checkAttrs(attrs: {[name: string]: Attribute}, values: Attrs, type: string, name: string) {
     42  for (let name in values)
     43    if (!(name in attrs)) throw new RangeError(`Unsupported attribute ${name} for ${type} of type ${name}`)
     44  for (let name in attrs) {
     45    let attr = attrs[name]
     46    if (attr.validate) attr.validate(values[name])
     47  }
     48 }
     49 
     50 function initAttrs(typeName: string, attrs?: {[name: string]: AttributeSpec}) {
     51  let result: {[name: string]: Attribute} = Object.create(null)
     52  if (attrs) for (let name in attrs) result[name] = new Attribute(typeName, name, attrs[name])
     53  return result
     54 }
     55 
     56 /// Node types are objects allocated once per `Schema` and used to
     57 /// [tag](#model.Node.type) `Node` instances. They contain information
     58 /// about the node type, such as its name and what kind of node it
     59 /// represents.
     60 export class NodeType {
     61  /// @internal
     62  groups: readonly string[]
     63  /// @internal
     64  attrs: {[name: string]: Attribute}
     65  /// @internal
     66  defaultAttrs: Attrs
     67 
     68  /// @internal
     69  constructor(
     70    /// The name the node type has in this schema.
     71    readonly name: string,
     72    /// A link back to the `Schema` the node type belongs to.
     73    readonly schema: Schema,
     74    /// The spec that this type is based on
     75    readonly spec: NodeSpec
     76  ) {
     77    this.groups = spec.group ? spec.group.split(" ") : []
     78    this.attrs = initAttrs(name, spec.attrs)
     79    this.defaultAttrs = defaultAttrs(this.attrs)
     80 
     81    // Filled in later
     82    ;(this as any).contentMatch = null
     83    ;(this as any).inlineContent = null
     84 
     85    this.isBlock = !(spec.inline || name == "text")
     86    this.isText = name == "text"
     87  }
     88 
     89  /// True if this node type has inline content.
     90  declare inlineContent: boolean
     91  /// True if this is a block type
     92  isBlock: boolean
     93  /// True if this is the text node type.
     94  isText: boolean
     95 
     96  /// True if this is an inline type.
     97  get isInline() { return !this.isBlock }
     98 
     99  /// True if this is a textblock type, a block that contains inline
    100  /// content.
    101  get isTextblock() { return this.isBlock && this.inlineContent }
    102 
    103  /// True for node types that allow no content.
    104  get isLeaf() { return this.contentMatch == ContentMatch.empty }
    105 
    106  /// True when this node is an atom, i.e. when it does not have
    107  /// directly editable content.
    108  get isAtom() { return this.isLeaf || !!this.spec.atom }
    109 
    110  /// Return true when this node type is part of the given
    111  /// [group](#model.NodeSpec.group).
    112  isInGroup(group: string) {
    113    return this.groups.indexOf(group) > -1
    114  }
    115 
    116  /// The starting match of the node type's content expression.
    117  declare contentMatch: ContentMatch
    118 
    119  /// The set of marks allowed in this node. `null` means all marks
    120  /// are allowed.
    121  markSet: readonly MarkType[] | null = null
    122 
    123  /// The node type's [whitespace](#model.NodeSpec.whitespace) option.
    124  get whitespace(): "pre" | "normal" {
    125    return this.spec.whitespace || (this.spec.code ? "pre" : "normal")
    126  }
    127 
    128  /// Tells you whether this node type has any required attributes.
    129  hasRequiredAttrs() {
    130    for (let n in this.attrs) if (this.attrs[n].isRequired) return true
    131    return false
    132  }
    133 
    134  /// Indicates whether this node allows some of the same content as
    135  /// the given node type.
    136  compatibleContent(other: NodeType) {
    137    return this == other || this.contentMatch.compatible(other.contentMatch)
    138  }
    139 
    140  /// @internal
    141  computeAttrs(attrs: Attrs | null): Attrs {
    142    if (!attrs && this.defaultAttrs) return this.defaultAttrs
    143    else return computeAttrs(this.attrs, attrs)
    144  }
    145 
    146  /// Create a `Node` of this type. The given attributes are
    147  /// checked and defaulted (you can pass `null` to use the type's
    148  /// defaults entirely, if no required attributes exist). `content`
    149  /// may be a `Fragment`, a node, an array of nodes, or
    150  /// `null`. Similarly `marks` may be `null` to default to the empty
    151  /// set of marks.
    152  create(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) {
    153    if (this.isText) throw new Error("NodeType.create can't construct text nodes")
    154    return new Node(this, this.computeAttrs(attrs), Fragment.from(content), Mark.setFrom(marks))
    155  }
    156 
    157  /// Like [`create`](#model.NodeType.create), but check the given content
    158  /// against the node type's content restrictions, and throw an error
    159  /// if it doesn't match.
    160  createChecked(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) {
    161    content = Fragment.from(content)
    162    this.checkContent(content)
    163    return new Node(this, this.computeAttrs(attrs), content, Mark.setFrom(marks))
    164  }
    165 
    166  /// Like [`create`](#model.NodeType.create), but see if it is
    167  /// necessary to add nodes to the start or end of the given fragment
    168  /// to make it fit the node. If no fitting wrapping can be found,
    169  /// return null. Note that, due to the fact that required nodes can
    170  /// always be created, this will always succeed if you pass null or
    171  /// `Fragment.empty` as content.
    172  createAndFill(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) {
    173    attrs = this.computeAttrs(attrs)
    174    content = Fragment.from(content)
    175    if (content.size) {
    176      let before = this.contentMatch.fillBefore(content)
    177      if (!before) return null
    178      content = before.append(content)
    179    }
    180    let matched = this.contentMatch.matchFragment(content)
    181    let after = matched && matched.fillBefore(Fragment.empty, true)
    182    if (!after) return null
    183    return new Node(this, attrs, (content as Fragment).append(after), Mark.setFrom(marks))
    184  }
    185 
    186  /// Returns true if the given fragment is valid content for this node
    187  /// type.
    188  validContent(content: Fragment) {
    189    let result = this.contentMatch.matchFragment(content)
    190    if (!result || !result.validEnd) return false
    191    for (let i = 0; i < content.childCount; i++)
    192      if (!this.allowsMarks(content.child(i).marks)) return false
    193    return true
    194  }
    195 
    196  /// Throws a RangeError if the given fragment is not valid content for this
    197  /// node type.
    198  /// @internal
    199  checkContent(content: Fragment) {
    200    if (!this.validContent(content))
    201      throw new RangeError(`Invalid content for node ${this.name}: ${content.toString().slice(0, 50)}`)
    202  }
    203 
    204  /// @internal
    205  checkAttrs(attrs: Attrs) {
    206    checkAttrs(this.attrs, attrs, "node", this.name)
    207  }
    208 
    209  /// Check whether the given mark type is allowed in this node.
    210  allowsMarkType(markType: MarkType) {
    211    return this.markSet == null || this.markSet.indexOf(markType) > -1
    212  }
    213 
    214  /// Test whether the given set of marks are allowed in this node.
    215  allowsMarks(marks: readonly Mark[]) {
    216    if (this.markSet == null) return true
    217    for (let i = 0; i < marks.length; i++) if (!this.allowsMarkType(marks[i].type)) return false
    218    return true
    219  }
    220 
    221  /// Removes the marks that are not allowed in this node from the given set.
    222  allowedMarks(marks: readonly Mark[]): readonly Mark[] {
    223    if (this.markSet == null) return marks
    224    let copy
    225    for (let i = 0; i < marks.length; i++) {
    226      if (!this.allowsMarkType(marks[i].type)) {
    227        if (!copy) copy = marks.slice(0, i)
    228      } else if (copy) {
    229        copy.push(marks[i])
    230      }
    231    }
    232    return !copy ? marks : copy.length ? copy : Mark.none
    233  }
    234 
    235  /// @internal
    236  static compile<Nodes extends string>(nodes: OrderedMap<NodeSpec>, schema: Schema<Nodes>): {readonly [name in Nodes]: NodeType} {
    237    let result = Object.create(null)
    238    nodes.forEach((name, spec) => result[name] = new NodeType(name, schema, spec))
    239 
    240    let topType = schema.spec.topNode || "doc"
    241    if (!result[topType]) throw new RangeError("Schema is missing its top node type ('" + topType + "')")
    242    if (!result.text) throw new RangeError("Every schema needs a 'text' type")
    243    for (let _ in result.text.attrs) throw new RangeError("The text node type should not have attributes")
    244 
    245    return result
    246  }
    247 }
    248 
    249 function validateType(typeName: string, attrName: string, type: string) {
    250  let types = type.split("|")
    251  return (value: any) => {
    252    let name = value === null ? "null" : typeof value
    253    if (types.indexOf(name) < 0) throw new RangeError(`Expected value of type ${types} for attribute ${attrName} on type ${typeName}, got ${name}`)
    254  }
    255 }
    256 
    257 // Attribute descriptors
    258 
    259 class Attribute {
    260  hasDefault: boolean
    261  default: any
    262  validate: undefined | ((value: any) => void)
    263 
    264  constructor(typeName: string, attrName: string, options: AttributeSpec) {
    265    this.hasDefault = Object.prototype.hasOwnProperty.call(options, "default")
    266    this.default = options.default
    267    this.validate = typeof options.validate == "string" ? validateType(typeName, attrName, options.validate) : options.validate
    268  }
    269 
    270  get isRequired() {
    271    return !this.hasDefault
    272  }
    273 }
    274 
    275 // Marks
    276 
    277 /// Like nodes, marks (which are associated with nodes to signify
    278 /// things like emphasis or being part of a link) are
    279 /// [tagged](#model.Mark.type) with type objects, which are
    280 /// instantiated once per `Schema`.
    281 export class MarkType {
    282  /// @internal
    283  attrs: {[name: string]: Attribute}
    284  /// @internal
    285  declare excluded: readonly MarkType[]
    286  /// @internal
    287  instance: Mark | null
    288 
    289  /// @internal
    290  constructor(
    291    /// The name of the mark type.
    292    readonly name: string,
    293    /// @internal
    294    readonly rank: number,
    295    /// The schema that this mark type instance is part of.
    296    readonly schema: Schema,
    297    /// The spec on which the type is based.
    298    readonly spec: MarkSpec
    299  ) {
    300    this.attrs = initAttrs(name, spec.attrs)
    301    ;(this as any).excluded = null
    302    let defaults = defaultAttrs(this.attrs)
    303    this.instance = defaults ? new Mark(this, defaults) : null
    304  }
    305 
    306  /// Create a mark of this type. `attrs` may be `null` or an object
    307  /// containing only some of the mark's attributes. The others, if
    308  /// they have defaults, will be added.
    309  create(attrs: Attrs | null = null) {
    310    if (!attrs && this.instance) return this.instance
    311    return new Mark(this, computeAttrs(this.attrs, attrs))
    312  }
    313 
    314  /// @internal
    315  static compile(marks: OrderedMap<MarkSpec>, schema: Schema) {
    316    let result = Object.create(null), rank = 0
    317    marks.forEach((name, spec) => result[name] = new MarkType(name, rank++, schema, spec))
    318    return result
    319  }
    320 
    321  /// When there is a mark of this type in the given set, a new set
    322  /// without it is returned. Otherwise, the input set is returned.
    323  removeFromSet(set: readonly Mark[]): readonly Mark[] {
    324    for (var i = 0; i < set.length; i++) if (set[i].type == this) {
    325      set = set.slice(0, i).concat(set.slice(i + 1))
    326      i--
    327    }
    328    return set
    329  }
    330 
    331  /// Tests whether there is a mark of this type in the given set.
    332  isInSet(set: readonly Mark[]): Mark | undefined {
    333    for (let i = 0; i < set.length; i++)
    334      if (set[i].type == this) return set[i]
    335  }
    336 
    337  /// @internal
    338  checkAttrs(attrs: Attrs) {
    339    checkAttrs(this.attrs, attrs, "mark", this.name)
    340  }
    341 
    342  /// Queries whether a given mark type is
    343  /// [excluded](#model.MarkSpec.excludes) by this one.
    344  excludes(other: MarkType) {
    345    return this.excluded.indexOf(other) > -1
    346  }
    347 }
    348 
    349 /// An object describing a schema, as passed to the [`Schema`](#model.Schema)
    350 /// constructor.
    351 export interface SchemaSpec<Nodes extends string = any, Marks extends string = any> {
    352  /// The node types in this schema. Maps names to
    353  /// [`NodeSpec`](#model.NodeSpec) objects that describe the node type
    354  /// associated with that name. Their order is significant—it
    355  /// determines which [parse rules](#model.NodeSpec.parseDOM) take
    356  /// precedence by default, and which nodes come first in a given
    357  /// [group](#model.NodeSpec.group).
    358  nodes: {[name in Nodes]: NodeSpec} | OrderedMap<NodeSpec>,
    359 
    360  /// The mark types that exist in this schema. The order in which they
    361  /// are provided determines the order in which [mark
    362  /// sets](#model.Mark.addToSet) are sorted and in which [parse
    363  /// rules](#model.MarkSpec.parseDOM) are tried.
    364  marks?: {[name in Marks]: MarkSpec} | OrderedMap<MarkSpec>
    365 
    366  /// The name of the default top-level node for the schema. Defaults
    367  /// to `"doc"`.
    368  topNode?: string
    369 }
    370 
    371 /// A description of a node type, used when defining a schema.
    372 export interface NodeSpec {
    373  /// The content expression for this node, as described in the [schema
    374  /// guide](/docs/guide/#schema.content_expressions). When not given,
    375  /// the node does not allow any content.
    376  content?: string
    377 
    378  /// The marks that are allowed inside of this node. May be a
    379  /// space-separated string referring to mark names or groups, `"_"`
    380  /// to explicitly allow all marks, or `""` to disallow marks. When
    381  /// not given, nodes with inline content default to allowing all
    382  /// marks, other nodes default to not allowing marks.
    383  marks?: string
    384 
    385  /// The group or space-separated groups to which this node belongs,
    386  /// which can be referred to in the content expressions for the
    387  /// schema.
    388  group?: string
    389 
    390  /// Should be set to true for inline nodes. (Implied for text nodes.)
    391  inline?: boolean
    392 
    393  /// Can be set to true to indicate that, though this isn't a [leaf
    394  /// node](#model.NodeType.isLeaf), it doesn't have directly editable
    395  /// content and should be treated as a single unit in the view.
    396  atom?: boolean
    397 
    398  /// The attributes that nodes of this type get.
    399  attrs?: {[name: string]: AttributeSpec}
    400 
    401  /// Controls whether nodes of this type can be selected as a [node
    402  /// selection](#state.NodeSelection). Defaults to true for non-text
    403  /// nodes.
    404  selectable?: boolean
    405 
    406  /// Determines whether nodes of this type can be dragged without
    407  /// being selected. Defaults to false.
    408  draggable?: boolean
    409 
    410  /// Can be used to indicate that this node contains code, which
    411  /// causes some commands to behave differently.
    412  code?: boolean
    413 
    414  /// Controls way whitespace in this a node is parsed. The default is
    415  /// `"normal"`, which causes the [DOM parser](#model.DOMParser) to
    416  /// collapse whitespace in normal mode, and normalize it (replacing
    417  /// newlines and such with spaces) otherwise. `"pre"` causes the
    418  /// parser to preserve spaces inside the node. When this option isn't
    419  /// given, but [`code`](#model.NodeSpec.code) is true, `whitespace`
    420  /// will default to `"pre"`. Note that this option doesn't influence
    421  /// the way the node is rendered—that should be handled by `toDOM`
    422  /// and/or styling.
    423  whitespace?: "pre" | "normal"
    424 
    425  /// Determines whether this node is considered an important parent
    426  /// node during replace operations (such as paste). Non-defining (the
    427  /// default) nodes get dropped when their entire content is replaced,
    428  /// whereas defining nodes persist and wrap the inserted content.
    429  definingAsContext?: boolean
    430 
    431  /// In inserted content the defining parents of the content are
    432  /// preserved when possible. Typically, non-default-paragraph
    433  /// textblock types, and possibly list items, are marked as defining.
    434  definingForContent?: boolean
    435 
    436  /// When enabled, enables both
    437  /// [`definingAsContext`](#model.NodeSpec.definingAsContext) and
    438  /// [`definingForContent`](#model.NodeSpec.definingForContent).
    439  defining?: boolean
    440 
    441  /// When enabled (default is false), the sides of nodes of this type
    442  /// count as boundaries that regular editing operations, like
    443  /// backspacing or lifting, won't cross. An example of a node that
    444  /// should probably have this enabled is a table cell.
    445  isolating?: boolean
    446 
    447  /// Defines the default way a node of this type should be serialized
    448  /// to DOM/HTML (as used by
    449  /// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)).
    450  /// Should return a DOM node or an [array
    451  /// structure](#model.DOMOutputSpec) that describes one, with an
    452  /// optional number zero (“hole”) in it to indicate where the node's
    453  /// content should be inserted.
    454  ///
    455  /// For text nodes, the default is to create a text DOM node. Though
    456  /// it is possible to create a serializer where text is rendered
    457  /// differently, this is not supported inside the editor, so you
    458  /// shouldn't override that in your text node spec.
    459  toDOM?: (node: Node) => DOMOutputSpec
    460 
    461  /// Associates DOM parser information with this node, which can be
    462  /// used by [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) to
    463  /// automatically derive a parser. The `node` field in the rules is
    464  /// implied (the name of this node will be filled in automatically).
    465  /// If you supply your own parser, you do not need to also specify
    466  /// parsing rules in your schema.
    467  parseDOM?: readonly TagParseRule[]
    468 
    469  /// Defines the default way a node of this type should be serialized
    470  /// to a string representation for debugging (e.g. in error messages).
    471  toDebugString?: (node: Node) => string
    472 
    473  /// Defines the default way a [leaf node](#model.NodeType.isLeaf) of
    474  /// this type should be serialized to a string (as used by
    475  /// [`Node.textBetween`](#model.Node.textBetween) and
    476  /// [`Node.textContent`](#model.Node.textContent)).
    477  leafText?: (node: Node) => string
    478 
    479  /// A single inline node in a schema can be set to be a linebreak
    480  /// equivalent. When converting between block types that support the
    481  /// node and block types that don't but have
    482  /// [`whitespace`](#model.NodeSpec.whitespace) set to `"pre"`,
    483  /// [`setBlockType`](#transform.Transform.setBlockType) will convert
    484  /// between newline characters to or from linebreak nodes as
    485  /// appropriate.
    486  linebreakReplacement?: boolean
    487 
    488  /// Node specs may include arbitrary properties that can be read by
    489  /// other code via [`NodeType.spec`](#model.NodeType.spec).
    490  [key: string]: any
    491 }
    492 
    493 /// Used to define marks when creating a schema.
    494 export interface MarkSpec {
    495  /// The attributes that marks of this type get.
    496  attrs?: {[name: string]: AttributeSpec}
    497 
    498  /// Whether this mark should be active when the cursor is positioned
    499  /// at its end (or at its start when that is also the start of the
    500  /// parent node). Defaults to true.
    501  inclusive?: boolean
    502 
    503  /// Determines which other marks this mark can coexist with. Should
    504  /// be a space-separated strings naming other marks or groups of marks.
    505  /// When a mark is [added](#model.Mark.addToSet) to a set, all marks
    506  /// that it excludes are removed in the process. If the set contains
    507  /// any mark that excludes the new mark but is not, itself, excluded
    508  /// by the new mark, the mark can not be added an the set. You can
    509  /// use the value `"_"` to indicate that the mark excludes all
    510  /// marks in the schema.
    511  ///
    512  /// Defaults to only being exclusive with marks of the same type. You
    513  /// can set it to an empty string (or any string not containing the
    514  /// mark's own name) to allow multiple marks of a given type to
    515  /// coexist (as long as they have different attributes).
    516  excludes?: string
    517 
    518  /// The group or space-separated groups to which this mark belongs.
    519  group?: string
    520 
    521  /// Determines whether marks of this type can span multiple adjacent
    522  /// nodes when serialized to DOM/HTML. Defaults to true.
    523  spanning?: boolean
    524 
    525  /// Marks the content of this span as being code, which causes some
    526  /// commands and extensions to treat it differently.
    527  code?: boolean
    528 
    529  /// Defines the default way marks of this type should be serialized
    530  /// to DOM/HTML. When the resulting spec contains a hole, that is
    531  /// where the marked content is placed. Otherwise, it is appended to
    532  /// the top node.
    533  toDOM?: (mark: Mark, inline: boolean) => DOMOutputSpec
    534 
    535  /// Associates DOM parser information with this mark (see the
    536  /// corresponding [node spec field](#model.NodeSpec.parseDOM)). The
    537  /// `mark` field in the rules is implied.
    538  parseDOM?: readonly ParseRule[]
    539 
    540  /// Mark specs can include additional properties that can be
    541  /// inspected through [`MarkType.spec`](#model.MarkType.spec) when
    542  /// working with the mark.
    543  [key: string]: any
    544 }
    545 
    546 /// Used to [define](#model.NodeSpec.attrs) attributes on nodes or
    547 /// marks.
    548 export interface AttributeSpec {
    549  /// The default value for this attribute, to use when no explicit
    550  /// value is provided. Attributes that have no default must be
    551  /// provided whenever a node or mark of a type that has them is
    552  /// created.
    553  default?: any
    554  /// A function or type name used to validate values of this
    555  /// attribute. This will be used when deserializing the attribute
    556  /// from JSON, and when running [`Node.check`](#model.Node.check).
    557  /// When a function, it should raise an exception if the value isn't
    558  /// of the expected type or shape. When a string, it should be a
    559  /// `|`-separated string of primitive types (`"number"`, `"string"`,
    560  /// `"boolean"`, `"null"`, and `"undefined"`), and the library will
    561  /// raise an error when the value is not one of those types.
    562  validate?: string | ((value: any) => void)
    563 }
    564 
    565 /// A document schema. Holds [node](#model.NodeType) and [mark
    566 /// type](#model.MarkType) objects for the nodes and marks that may
    567 /// occur in conforming documents, and provides functionality for
    568 /// creating and deserializing such documents.
    569 ///
    570 /// When given, the type parameters provide the names of the nodes and
    571 /// marks in this schema.
    572 export class Schema<Nodes extends string = any, Marks extends string = any> {
    573  /// The [spec](#model.SchemaSpec) on which the schema is based,
    574  /// with the added guarantee that its `nodes` and `marks`
    575  /// properties are
    576  /// [`OrderedMap`](https://github.com/marijnh/orderedmap) instances
    577  /// (not raw objects).
    578  spec: {
    579    nodes: OrderedMap<NodeSpec>,
    580    marks: OrderedMap<MarkSpec>,
    581    topNode?: string
    582  }
    583 
    584  /// An object mapping the schema's node names to node type objects.
    585  nodes: {readonly [name in Nodes]: NodeType} & {readonly [key: string]: NodeType}
    586 
    587  /// A map from mark names to mark type objects.
    588  marks: {readonly [name in Marks]: MarkType} & {readonly [key: string]: MarkType}
    589 
    590  /// The [linebreak
    591  /// replacement](#model.NodeSpec.linebreakReplacement) node defined
    592  /// in this schema, if any.
    593  linebreakReplacement: NodeType | null = null
    594 
    595  /// Construct a schema from a schema [specification](#model.SchemaSpec).
    596  constructor(spec: SchemaSpec<Nodes, Marks>) {
    597    let instanceSpec = this.spec = {} as any
    598    for (let prop in spec) instanceSpec[prop] = (spec as any)[prop]
    599    instanceSpec.nodes = OrderedMap.from(spec.nodes),
    600    instanceSpec.marks = OrderedMap.from(spec.marks || {}),
    601 
    602    this.nodes = NodeType.compile(this.spec.nodes, this)
    603    this.marks = MarkType.compile(this.spec.marks, this)
    604 
    605    let contentExprCache = Object.create(null)
    606    for (let prop in this.nodes) {
    607      if (prop in this.marks)
    608        throw new RangeError(prop + " can not be both a node and a mark")
    609      let type = this.nodes[prop], contentExpr = type.spec.content || "", markExpr = type.spec.marks
    610      type.contentMatch = contentExprCache[contentExpr] ||
    611        (contentExprCache[contentExpr] = ContentMatch.parse(contentExpr, this.nodes))
    612      ;(type as any).inlineContent = type.contentMatch.inlineContent
    613      if (type.spec.linebreakReplacement) {
    614        if (this.linebreakReplacement) throw new RangeError("Multiple linebreak nodes defined")
    615        if (!type.isInline || !type.isLeaf) throw new RangeError("Linebreak replacement nodes must be inline leaf nodes")
    616        this.linebreakReplacement = type
    617      }
    618      type.markSet = markExpr == "_" ? null :
    619        markExpr ? gatherMarks(this, markExpr.split(" ")) :
    620        markExpr == "" || !type.inlineContent ? [] : null
    621    }
    622    for (let prop in this.marks) {
    623      let type = this.marks[prop], excl = type.spec.excludes
    624      type.excluded = excl == null ? [type] : excl == "" ? [] : gatherMarks(this, excl.split(" "))
    625    }
    626 
    627    this.nodeFromJSON = json => Node.fromJSON(this, json)
    628    this.markFromJSON = json => Mark.fromJSON(this, json)
    629    this.topNodeType = this.nodes[this.spec.topNode || "doc"]
    630    this.cached.wrappings = Object.create(null)
    631  }
    632 
    633  /// The type of the [default top node](#model.SchemaSpec.topNode)
    634  /// for this schema.
    635  topNodeType: NodeType
    636 
    637  /// An object for storing whatever values modules may want to
    638  /// compute and cache per schema. (If you want to store something
    639  /// in it, try to use property names unlikely to clash.)
    640  cached: {[key: string]: any} = Object.create(null)
    641 
    642  /// Create a node in this schema. The `type` may be a string or a
    643  /// `NodeType` instance. Attributes will be extended with defaults,
    644  /// `content` may be a `Fragment`, `null`, a `Node`, or an array of
    645  /// nodes.
    646  node(type: string | NodeType,
    647       attrs: Attrs | null = null,
    648       content?: Fragment | Node | readonly Node[],
    649       marks?: readonly Mark[]) {
    650    if (typeof type == "string")
    651      type = this.nodeType(type)
    652    else if (!(type instanceof NodeType))
    653      throw new RangeError("Invalid node type: " + type)
    654    else if (type.schema != this)
    655      throw new RangeError("Node type from different schema used (" + type.name + ")")
    656 
    657    return type.createChecked(attrs, content, marks)
    658  }
    659 
    660  /// Create a text node in the schema. Empty text nodes are not
    661  /// allowed.
    662  text(text: string, marks?: readonly Mark[] | null): Node {
    663    let type = this.nodes.text
    664    return new TextNode(type, type.defaultAttrs, text, Mark.setFrom(marks))
    665  }
    666 
    667  /// Create a mark with the given type and attributes.
    668  mark(type: string | MarkType, attrs?: Attrs | null) {
    669    if (typeof type == "string") type = this.marks[type]
    670    return type.create(attrs)
    671  }
    672 
    673  /// Deserialize a node from its JSON representation. This method is
    674  /// bound.
    675  nodeFromJSON: (json: any) => Node
    676 
    677  /// Deserialize a mark from its JSON representation. This method is
    678  /// bound.
    679  markFromJSON: (json: any) => Mark
    680 
    681  /// @internal
    682  nodeType(name: string) {
    683    let found = this.nodes[name]
    684    if (!found) throw new RangeError("Unknown node type: " + name)
    685    return found
    686  }
    687 }
    688 
    689 function gatherMarks(schema: Schema, marks: readonly string[]) {
    690  let found: MarkType[] = []
    691  for (let i = 0; i < marks.length; i++) {
    692    let name = marks[i], mark = schema.marks[name], ok = mark
    693    if (mark) {
    694      found.push(mark)
    695    } else {
    696      for (let prop in schema.marks) {
    697        let mark = schema.marks[prop]
    698        if (name == "_" || (mark.spec.group && mark.spec.group.split(" ").indexOf(name) > -1))
    699          found.push(ok = mark)
    700      }
    701    }
    702    if (!ok) throw new SyntaxError("Unknown mark type: '" + marks[i] + "'")
    703  }
    704  return found
    705 }