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 }