from_dom.ts (33457B)
1 import {Fragment} from "./fragment" 2 import {Slice} from "./replace" 3 import {Mark} from "./mark" 4 import {Node, TextNode} from "./node" 5 import {ContentMatch} from "./content" 6 import {ResolvedPos} from "./resolvedpos" 7 import {Schema, Attrs, NodeType, MarkType} from "./schema" 8 import {DOMNode} from "./dom" 9 10 /// These are the options recognized by the 11 /// [`parse`](#model.DOMParser.parse) and 12 /// [`parseSlice`](#model.DOMParser.parseSlice) methods. 13 export interface ParseOptions { 14 /// By default, whitespace is collapsed as per HTML's rules. Pass 15 /// `true` to preserve whitespace, but normalize newlines to 16 /// spaces or, if available, [line break replacements](#model.NodeSpec.linebreakReplacement), 17 /// and `"full"` to preserve whitespace entirely. 18 preserveWhitespace?: boolean | "full" 19 20 /// When given, the parser will, beside parsing the content, 21 /// record the document positions of the given DOM positions. It 22 /// will do so by writing to the objects, adding a `pos` property 23 /// that holds the document position. DOM positions that are not 24 /// in the parsed content will not be written to. 25 findPositions?: {node: DOMNode, offset: number, pos?: number}[] 26 27 /// The child node index to start parsing from. 28 from?: number 29 30 /// The child node index to stop parsing at. 31 to?: number 32 33 /// By default, the content is parsed into the schema's default 34 /// [top node type](#model.Schema.topNodeType). You can pass this 35 /// option to use the type and attributes from a different node 36 /// as the top container. 37 topNode?: Node 38 39 /// Provide the starting content match that content parsed into the 40 /// top node is matched against. 41 topMatch?: ContentMatch 42 43 /// A set of additional nodes to count as 44 /// [context](#model.GenericParseRule.context) when parsing, above the 45 /// given [top node](#model.ParseOptions.topNode). 46 context?: ResolvedPos 47 48 /// @internal 49 ruleFromNode?: (node: DOMNode) => Omit<TagParseRule, "tag"> | null 50 /// @internal 51 topOpen?: boolean 52 } 53 54 /// Fields that may be present in both [tag](#model.TagParseRule) and 55 /// [style](#model.StyleParseRule) parse rules. 56 export interface GenericParseRule { 57 /// Can be used to change the order in which the parse rules in a 58 /// schema are tried. Those with higher priority come first. Rules 59 /// without a priority are counted as having priority 50. This 60 /// property is only meaningful in a schema—when directly 61 /// constructing a parser, the order of the rule array is used. 62 priority?: number 63 64 /// By default, when a rule matches an element or style, no further 65 /// rules get a chance to match it. By setting this to `false`, you 66 /// indicate that even when this rule matches, other rules that come 67 /// after it should also run. 68 consuming?: boolean 69 70 /// When given, restricts this rule to only match when the current 71 /// context—the parent nodes into which the content is being 72 /// parsed—matches this expression. Should contain one or more node 73 /// names or node group names followed by single or double slashes. 74 /// For example `"paragraph/"` means the rule only matches when the 75 /// parent node is a paragraph, `"blockquote/paragraph/"` restricts 76 /// it to be in a paragraph that is inside a blockquote, and 77 /// `"section//"` matches any position inside a section—a double 78 /// slash matches any sequence of ancestor nodes. To allow multiple 79 /// different contexts, they can be separated by a pipe (`|`) 80 /// character, as in `"blockquote/|list_item/"`. 81 context?: string 82 83 /// The name of the mark type to wrap the matched content in. 84 mark?: string 85 86 /// When true, ignore content that matches this rule. 87 ignore?: boolean 88 89 /// When true, finding an element that matches this rule will close 90 /// the current node. 91 closeParent?: boolean 92 93 /// When true, ignore the node that matches this rule, but do parse 94 /// its content. 95 skip?: boolean 96 97 /// Attributes for the node or mark created by this rule. When 98 /// `getAttrs` is provided, it takes precedence. 99 attrs?: Attrs 100 } 101 102 /// Parse rule targeting a DOM element. 103 export interface TagParseRule extends GenericParseRule { 104 /// A CSS selector describing the kind of DOM elements to match. 105 tag: string 106 107 /// The namespace to match. Nodes are only matched when the 108 /// namespace matches or this property is null. 109 namespace?: string 110 111 /// The name of the node type to create when this rule matches. Each 112 /// rule should have either a `node`, `mark`, or `ignore` property 113 /// (except when it appears in a [node](#model.NodeSpec.parseDOM) or 114 /// [mark spec](#model.MarkSpec.parseDOM), in which case the `node` 115 /// or `mark` property will be derived from its position). 116 node?: string 117 118 /// A function used to compute the attributes for the node or mark 119 /// created by this rule. Can also be used to describe further 120 /// conditions the DOM element or style must match. When it returns 121 /// `false`, the rule won't match. When it returns null or undefined, 122 /// that is interpreted as an empty/default set of attributes. 123 getAttrs?: (node: HTMLElement) => Attrs | false | null 124 125 /// For rules that produce non-leaf nodes, by default the content of 126 /// the DOM element is parsed as content of the node. If the child 127 /// nodes are in a descendent node, this may be a CSS selector 128 /// string that the parser must use to find the actual content 129 /// element, or a function that returns the actual content element 130 /// to the parser. 131 contentElement?: string | HTMLElement | ((node: HTMLElement) => HTMLElement) 132 133 /// Can be used to override the content of a matched node. When 134 /// present, instead of parsing the node's child nodes, the result of 135 /// this function is used. 136 getContent?: (node: DOMNode, schema: Schema) => Fragment 137 138 /// Controls whether whitespace should be preserved when parsing the 139 /// content inside the matched element. `false` means whitespace may 140 /// be collapsed, `true` means that whitespace should be preserved 141 /// but newlines normalized to spaces, and `"full"` means that 142 /// newlines should also be preserved. 143 preserveWhitespace?: boolean | "full" 144 } 145 146 /// A parse rule targeting a style property. 147 export interface StyleParseRule extends GenericParseRule { 148 /// A CSS property name to match. This rule will match inline styles 149 /// that list that property. May also have the form 150 /// `"property=value"`, in which case the rule only matches if the 151 /// property's value exactly matches the given value. (For more 152 /// complicated filters, use [`getAttrs`](#model.StyleParseRule.getAttrs) 153 /// and return false to indicate that the match failed.) Rules 154 /// matching styles may only produce [marks](#model.GenericParseRule.mark), 155 /// not nodes. 156 style: string 157 158 /// Given to make TS see ParseRule as a tagged union @hide 159 tag?: undefined 160 161 /// Style rules can remove marks from the set of active marks. 162 clearMark?: (mark: Mark) => boolean 163 164 /// A function used to compute the attributes for the node or mark 165 /// created by this rule. Called with the style's value. 166 getAttrs?: (node: string) => Attrs | false | null 167 } 168 169 /// A value that describes how to parse a given DOM node or inline 170 /// style as a ProseMirror node or mark. 171 export type ParseRule = TagParseRule | StyleParseRule 172 173 function isTagRule(rule: ParseRule): rule is TagParseRule { return (rule as TagParseRule).tag != null } 174 function isStyleRule(rule: ParseRule): rule is StyleParseRule { return (rule as StyleParseRule).style != null } 175 176 /// A DOM parser represents a strategy for parsing DOM content into a 177 /// ProseMirror document conforming to a given schema. Its behavior is 178 /// defined by an array of [rules](#model.ParseRule). 179 export class DOMParser { 180 /// @internal 181 tags: TagParseRule[] = [] 182 /// @internal 183 styles: StyleParseRule[] = [] 184 /// @internal 185 matchedStyles: readonly string[] 186 /// @internal 187 normalizeLists: boolean 188 189 /// Create a parser that targets the given schema, using the given 190 /// parsing rules. 191 constructor( 192 /// The schema into which the parser parses. 193 readonly schema: Schema, 194 /// The set of [parse rules](#model.ParseRule) that the parser 195 /// uses, in order of precedence. 196 readonly rules: readonly ParseRule[] 197 ) { 198 let matchedStyles: string[] = this.matchedStyles = [] 199 rules.forEach(rule => { 200 if (isTagRule(rule)) { 201 this.tags.push(rule) 202 } else if (isStyleRule(rule)) { 203 let prop = /[^=]*/.exec(rule.style)![0] 204 if (matchedStyles.indexOf(prop) < 0) matchedStyles.push(prop) 205 this.styles.push(rule) 206 } 207 }) 208 209 // Only normalize list elements when lists in the schema can't directly contain themselves 210 this.normalizeLists = !this.tags.some(r => { 211 if (!/^(ul|ol)\b/.test(r.tag!) || !r.node) return false 212 let node = schema.nodes[r.node] 213 return node.contentMatch.matchType(node) 214 }) 215 } 216 217 /// Parse a document from the content of a DOM node. 218 parse(dom: DOMNode, options: ParseOptions = {}): Node { 219 let context = new ParseContext(this, options, false) 220 context.addAll(dom, Mark.none, options.from, options.to) 221 return context.finish() as Node 222 } 223 224 /// Parses the content of the given DOM node, like 225 /// [`parse`](#model.DOMParser.parse), and takes the same set of 226 /// options. But unlike that method, which produces a whole node, 227 /// this one returns a slice that is open at the sides, meaning that 228 /// the schema constraints aren't applied to the start of nodes to 229 /// the left of the input and the end of nodes at the end. 230 parseSlice(dom: DOMNode, options: ParseOptions = {}) { 231 let context = new ParseContext(this, options, true) 232 context.addAll(dom, Mark.none, options.from, options.to) 233 return Slice.maxOpen(context.finish() as Fragment) 234 } 235 236 /// @internal 237 matchTag(dom: DOMNode, context: ParseContext, after?: TagParseRule) { 238 for (let i = after ? this.tags.indexOf(after) + 1 : 0; i < this.tags.length; i++) { 239 let rule = this.tags[i] 240 if (matches(dom, rule.tag!) && 241 (rule.namespace === undefined || (dom as HTMLElement).namespaceURI == rule.namespace) && 242 (!rule.context || context.matchesContext(rule.context))) { 243 if (rule.getAttrs) { 244 let result = rule.getAttrs(dom as HTMLElement) 245 if (result === false) continue 246 rule.attrs = result || undefined 247 } 248 return rule 249 } 250 } 251 } 252 253 /// @internal 254 matchStyle(prop: string, value: string, context: ParseContext, after?: StyleParseRule) { 255 for (let i = after ? this.styles.indexOf(after) + 1 : 0; i < this.styles.length; i++) { 256 let rule = this.styles[i], style = rule.style! 257 if (style.indexOf(prop) != 0 || 258 rule.context && !context.matchesContext(rule.context) || 259 // Test that the style string either precisely matches the prop, 260 // or has an '=' sign after the prop, followed by the given 261 // value. 262 style.length > prop.length && 263 (style.charCodeAt(prop.length) != 61 || style.slice(prop.length + 1) != value)) 264 continue 265 if (rule.getAttrs) { 266 let result = rule.getAttrs(value) 267 if (result === false) continue 268 rule.attrs = result || undefined 269 } 270 return rule 271 } 272 } 273 274 /// @internal 275 static schemaRules(schema: Schema) { 276 let result: ParseRule[] = [] 277 function insert(rule: ParseRule) { 278 let priority = rule.priority == null ? 50 : rule.priority, i = 0 279 for (; i < result.length; i++) { 280 let next = result[i], nextPriority = next.priority == null ? 50 : next.priority 281 if (nextPriority < priority) break 282 } 283 result.splice(i, 0, rule) 284 } 285 286 for (let name in schema.marks) { 287 let rules = schema.marks[name].spec.parseDOM 288 if (rules) rules.forEach(rule => { 289 insert(rule = copy(rule) as ParseRule) 290 if (!(rule.mark || rule.ignore || (rule as StyleParseRule).clearMark)) 291 rule.mark = name 292 }) 293 } 294 for (let name in schema.nodes) { 295 let rules = schema.nodes[name].spec.parseDOM 296 if (rules) rules.forEach(rule => { 297 insert(rule = copy(rule) as TagParseRule) 298 if (!((rule as TagParseRule).node || rule.ignore || rule.mark)) 299 rule.node = name 300 }) 301 } 302 return result 303 } 304 305 /// Construct a DOM parser using the parsing rules listed in a 306 /// schema's [node specs](#model.NodeSpec.parseDOM), reordered by 307 /// [priority](#model.GenericParseRule.priority). 308 static fromSchema(schema: Schema) { 309 return schema.cached.domParser as DOMParser || 310 (schema.cached.domParser = new DOMParser(schema, DOMParser.schemaRules(schema))) 311 } 312 } 313 314 const blockTags: {[tagName: string]: boolean} = { 315 address: true, article: true, aside: true, blockquote: true, canvas: true, 316 dd: true, div: true, dl: true, fieldset: true, figcaption: true, figure: true, 317 footer: true, form: true, h1: true, h2: true, h3: true, h4: true, h5: true, 318 h6: true, header: true, hgroup: true, hr: true, li: true, noscript: true, ol: true, 319 output: true, p: true, pre: true, section: true, table: true, tfoot: true, ul: true 320 } 321 322 const ignoreTags: {[tagName: string]: boolean} = { 323 head: true, noscript: true, object: true, script: true, style: true, title: true 324 } 325 326 const listTags: {[tagName: string]: boolean} = {ol: true, ul: true} 327 328 // Using a bitfield for node context options 329 const OPT_PRESERVE_WS = 1, OPT_PRESERVE_WS_FULL = 2, OPT_OPEN_LEFT = 4 330 331 function wsOptionsFor(type: NodeType | null, preserveWhitespace: boolean | "full" | undefined, base: number) { 332 if (preserveWhitespace != null) return (preserveWhitespace ? OPT_PRESERVE_WS : 0) | 333 (preserveWhitespace === "full" ? OPT_PRESERVE_WS_FULL : 0) 334 return type && type.whitespace == "pre" ? OPT_PRESERVE_WS | OPT_PRESERVE_WS_FULL : base & ~OPT_OPEN_LEFT 335 } 336 337 class NodeContext { 338 match: ContentMatch | null 339 content: Node[] = [] 340 341 // Marks applied to the node's children 342 activeMarks: readonly Mark[] = Mark.none 343 344 constructor( 345 readonly type: NodeType | null, 346 readonly attrs: Attrs | null, 347 readonly marks: readonly Mark[], 348 readonly solid: boolean, 349 match: ContentMatch | null, 350 public options: number 351 ) { 352 this.match = match || (options & OPT_OPEN_LEFT ? null : type!.contentMatch) 353 } 354 355 findWrapping(node: Node) { 356 if (!this.match) { 357 if (!this.type) return [] 358 let fill = this.type.contentMatch.fillBefore(Fragment.from(node)) 359 if (fill) { 360 this.match = this.type.contentMatch.matchFragment(fill)! 361 } else { 362 let start = this.type.contentMatch, wrap 363 if (wrap = start.findWrapping(node.type)) { 364 this.match = start 365 return wrap 366 } else { 367 return null 368 } 369 } 370 } 371 return this.match.findWrapping(node.type) 372 } 373 374 finish(openEnd: boolean): Node | Fragment { 375 if (!(this.options & OPT_PRESERVE_WS)) { // Strip trailing whitespace 376 let last = this.content[this.content.length - 1], m 377 if (last && last.isText && (m = /[ \t\r\n\u000c]+$/.exec(last.text!))) { 378 let text = last as TextNode 379 if (last.text!.length == m[0].length) this.content.pop() 380 else this.content[this.content.length - 1] = text.withText(text.text.slice(0, text.text.length - m[0].length)) 381 } 382 } 383 let content = Fragment.from(this.content) 384 if (!openEnd && this.match) 385 content = content.append(this.match.fillBefore(Fragment.empty, true)!) 386 return this.type ? this.type.create(this.attrs, content, this.marks) : content 387 } 388 389 inlineContext(node: DOMNode) { 390 if (this.type) return this.type.inlineContent 391 if (this.content.length) return this.content[0].isInline 392 return node.parentNode && !blockTags.hasOwnProperty(node.parentNode.nodeName.toLowerCase()) 393 } 394 } 395 396 class ParseContext { 397 open: number = 0 398 find: {node: DOMNode, offset: number, pos?: number}[] | undefined 399 needsBlock: boolean 400 nodes: NodeContext[] 401 localPreserveWS = false 402 403 constructor( 404 // The parser we are using. 405 readonly parser: DOMParser, 406 // The options passed to this parse. 407 readonly options: ParseOptions, 408 readonly isOpen: boolean 409 ) { 410 let topNode = options.topNode, topContext: NodeContext 411 let topOptions = wsOptionsFor(null, options.preserveWhitespace, 0) | (isOpen ? OPT_OPEN_LEFT : 0) 412 if (topNode) 413 topContext = new NodeContext(topNode.type, topNode.attrs, Mark.none, true, 414 options.topMatch || topNode.type.contentMatch, topOptions) 415 else if (isOpen) 416 topContext = new NodeContext(null, null, Mark.none, true, null, topOptions) 417 else 418 topContext = new NodeContext(parser.schema.topNodeType, null, Mark.none, true, null, topOptions) 419 this.nodes = [topContext] 420 this.find = options.findPositions 421 this.needsBlock = false 422 } 423 424 get top() { 425 return this.nodes[this.open] 426 } 427 428 // Add a DOM node to the content. Text is inserted as text node, 429 // otherwise, the node is passed to `addElement` or, if it has a 430 // `style` attribute, `addElementWithStyles`. 431 addDOM(dom: DOMNode, marks: readonly Mark[]) { 432 if (dom.nodeType == 3) this.addTextNode(dom as Text, marks) 433 else if (dom.nodeType == 1) this.addElement(dom as HTMLElement, marks) 434 } 435 436 addTextNode(dom: Text, marks: readonly Mark[]) { 437 let value = dom.nodeValue! 438 let top = this.top, preserveWS = (top.options & OPT_PRESERVE_WS_FULL) ? "full" 439 : this.localPreserveWS || (top.options & OPT_PRESERVE_WS) > 0 440 let {schema} = this.parser 441 if (preserveWS === "full" || 442 top.inlineContext(dom) || 443 /[^ \t\r\n\u000c]/.test(value)) { 444 if (!preserveWS) { 445 value = value.replace(/[ \t\r\n\u000c]+/g, " ") 446 // If this starts with whitespace, and there is no node before it, or 447 // a hard break, or a text node that ends with whitespace, strip the 448 // leading space. 449 if (/^[ \t\r\n\u000c]/.test(value) && this.open == this.nodes.length - 1) { 450 let nodeBefore = top.content[top.content.length - 1] 451 let domNodeBefore = dom.previousSibling 452 if (!nodeBefore || 453 (domNodeBefore && domNodeBefore.nodeName == 'BR') || 454 (nodeBefore.isText && /[ \t\r\n\u000c]$/.test(nodeBefore.text!))) 455 value = value.slice(1) 456 } 457 } else if (preserveWS === "full") { 458 value = value.replace(/\r\n?/g, "\n") 459 } else if (schema.linebreakReplacement && /[\r\n]/.test(value) && this.top.findWrapping(schema.linebreakReplacement.create())) { 460 let lines = value.split(/\r?\n|\r/) 461 for (let i = 0; i < lines.length; i++) { 462 if (i) this.insertNode(schema.linebreakReplacement.create(), marks, true) 463 if (lines[i]) this.insertNode(schema.text(lines[i]), marks, !/\S/.test(lines[i])) 464 } 465 value = "" 466 } else { 467 value = value.replace(/\r?\n|\r/g, " ") 468 } 469 if (value) this.insertNode(schema.text(value), marks, !/\S/.test(value)) 470 this.findInText(dom) 471 } else { 472 this.findInside(dom) 473 } 474 } 475 476 // Try to find a handler for the given tag and use that to parse. If 477 // none is found, the element's content nodes are added directly. 478 addElement(dom: HTMLElement, marks: readonly Mark[], matchAfter?: TagParseRule) { 479 let outerWS = this.localPreserveWS, top = this.top 480 if (dom.tagName == "PRE" || /pre/.test(dom.style && dom.style.whiteSpace)) 481 this.localPreserveWS = true 482 let name = dom.nodeName.toLowerCase(), ruleID: TagParseRule | undefined 483 if (listTags.hasOwnProperty(name) && this.parser.normalizeLists) normalizeList(dom) 484 let rule = (this.options.ruleFromNode && this.options.ruleFromNode(dom)) || 485 (ruleID = this.parser.matchTag(dom, this, matchAfter)) 486 out: 487 if (rule ? rule.ignore : ignoreTags.hasOwnProperty(name)) { 488 this.findInside(dom) 489 this.ignoreFallback(dom, marks) 490 } else if (!rule || rule.skip || rule.closeParent) { 491 if (rule && rule.closeParent) this.open = Math.max(0, this.open - 1) 492 else if (rule && (rule.skip as any).nodeType) dom = rule.skip as any as HTMLElement 493 let sync, oldNeedsBlock = this.needsBlock 494 if (blockTags.hasOwnProperty(name)) { 495 if (top.content.length && top.content[0].isInline && this.open) { 496 this.open-- 497 top = this.top 498 } 499 sync = true 500 if (!top.type) this.needsBlock = true 501 } else if (!dom.firstChild) { 502 this.leafFallback(dom, marks) 503 break out 504 } 505 let innerMarks = rule && rule.skip ? marks : this.readStyles(dom, marks) 506 if (innerMarks) this.addAll(dom, innerMarks) 507 if (sync) this.sync(top) 508 this.needsBlock = oldNeedsBlock 509 } else { 510 let innerMarks = this.readStyles(dom, marks) 511 if (innerMarks) 512 this.addElementByRule(dom, rule as TagParseRule, innerMarks, rule!.consuming === false ? ruleID : undefined) 513 } 514 this.localPreserveWS = outerWS 515 } 516 517 // Called for leaf DOM nodes that would otherwise be ignored 518 leafFallback(dom: DOMNode, marks: readonly Mark[]) { 519 if (dom.nodeName == "BR" && this.top.type && this.top.type.inlineContent) 520 this.addTextNode(dom.ownerDocument!.createTextNode("\n"), marks) 521 } 522 523 // Called for ignored nodes 524 ignoreFallback(dom: DOMNode, marks: readonly Mark[]) { 525 // Ignored BR nodes should at least create an inline context 526 if (dom.nodeName == "BR" && (!this.top.type || !this.top.type.inlineContent)) 527 this.findPlace(this.parser.schema.text("-"), marks, true) 528 } 529 530 // Run any style parser associated with the node's styles. Either 531 // return an updated array of marks, or null to indicate some of the 532 // styles had a rule with `ignore` set. 533 readStyles(dom: HTMLElement, marks: readonly Mark[]) { 534 let styles = dom.style 535 // Because many properties will only show up in 'normalized' form 536 // in `style.item` (i.e. text-decoration becomes 537 // text-decoration-line, text-decoration-color, etc), we directly 538 // query the styles mentioned in our rules instead of iterating 539 // over the items. 540 if (styles && styles.length) for (let i = 0; i < this.parser.matchedStyles.length; i++) { 541 let name = this.parser.matchedStyles[i], value = styles.getPropertyValue(name) 542 if (value) for (let after: StyleParseRule | undefined = undefined;;) { 543 let rule = this.parser.matchStyle(name, value, this, after) 544 if (!rule) break 545 if (rule.ignore) return null 546 if (rule.clearMark) 547 marks = marks.filter(m => !rule!.clearMark!(m)) 548 else 549 marks = marks.concat(this.parser.schema.marks[rule.mark!].create(rule.attrs)) 550 if (rule.consuming === false) after = rule 551 else break 552 } 553 } 554 return marks 555 } 556 557 // Look up a handler for the given node. If none are found, return 558 // false. Otherwise, apply it, use its return value to drive the way 559 // the node's content is wrapped, and return true. 560 addElementByRule(dom: HTMLElement, rule: TagParseRule, marks: readonly Mark[], continueAfter?: TagParseRule) { 561 let sync, nodeType 562 if (rule.node) { 563 nodeType = this.parser.schema.nodes[rule.node] 564 if (!nodeType.isLeaf) { 565 let inner = this.enter(nodeType, rule.attrs || null, marks, rule.preserveWhitespace) 566 if (inner) { 567 sync = true 568 marks = inner 569 } 570 } else if (!this.insertNode(nodeType.create(rule.attrs), marks, dom.nodeName == "BR")) { 571 this.leafFallback(dom, marks) 572 } 573 } else { 574 let markType = this.parser.schema.marks[rule.mark!] 575 marks = marks.concat(markType.create(rule.attrs)) 576 } 577 let startIn = this.top 578 579 if (nodeType && nodeType.isLeaf) { 580 this.findInside(dom) 581 } else if (continueAfter) { 582 this.addElement(dom, marks, continueAfter) 583 } else if (rule.getContent) { 584 this.findInside(dom) 585 rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node, marks, false)) 586 } else { 587 let contentDOM = dom 588 if (typeof rule.contentElement == "string") contentDOM = dom.querySelector(rule.contentElement)! 589 else if (typeof rule.contentElement == "function") contentDOM = rule.contentElement(dom) 590 else if (rule.contentElement) contentDOM = rule.contentElement 591 this.findAround(dom, contentDOM, true) 592 this.addAll(contentDOM, marks) 593 this.findAround(dom, contentDOM, false) 594 } 595 if (sync && this.sync(startIn)) this.open-- 596 } 597 598 // Add all child nodes between `startIndex` and `endIndex` (or the 599 // whole node, if not given). If `sync` is passed, use it to 600 // synchronize after every block element. 601 addAll(parent: DOMNode, marks: readonly Mark[], startIndex?: number, endIndex?: number) { 602 let index = startIndex || 0 603 for (let dom = startIndex ? parent.childNodes[startIndex] : parent.firstChild, 604 end = endIndex == null ? null : parent.childNodes[endIndex]; 605 dom != end; dom = dom!.nextSibling, ++index) { 606 this.findAtPoint(parent, index) 607 this.addDOM(dom!, marks) 608 } 609 this.findAtPoint(parent, index) 610 } 611 612 // Try to find a way to fit the given node type into the current 613 // context. May add intermediate wrappers and/or leave non-solid 614 // nodes that we're in. 615 findPlace(node: Node, marks: readonly Mark[], cautious: boolean) { 616 let route, sync: NodeContext | undefined 617 for (let depth = this.open, penalty = 0; depth >= 0; depth--) { 618 let cx = this.nodes[depth] 619 let found = cx.findWrapping(node) 620 if (found && (!route || route.length > found.length + penalty)) { 621 route = found 622 sync = cx 623 if (!found.length) break 624 } 625 if (cx.solid) { 626 if (cautious) break 627 penalty += 2 628 } 629 } 630 if (!route) return null 631 this.sync(sync!) 632 for (let i = 0; i < route.length; i++) 633 marks = this.enterInner(route[i], null, marks, false) 634 return marks 635 } 636 637 // Try to insert the given node, adjusting the context when needed. 638 insertNode(node: Node, marks: readonly Mark[], cautious: boolean) { 639 if (node.isInline && this.needsBlock && !this.top.type) { 640 let block = this.textblockFromContext() 641 if (block) marks = this.enterInner(block, null, marks) 642 } 643 let innerMarks = this.findPlace(node, marks, cautious) 644 if (innerMarks) { 645 this.closeExtra() 646 let top = this.top 647 if (top.match) top.match = top.match.matchType(node.type) 648 let nodeMarks = Mark.none 649 for (let m of innerMarks.concat(node.marks)) 650 if (top.type ? top.type.allowsMarkType(m.type) : markMayApply(m.type, node.type)) 651 nodeMarks = m.addToSet(nodeMarks) 652 top.content.push(node.mark(nodeMarks)) 653 return true 654 } 655 return false 656 } 657 658 // Try to start a node of the given type, adjusting the context when 659 // necessary. 660 enter(type: NodeType, attrs: Attrs | null, marks: readonly Mark[], preserveWS?: boolean | "full") { 661 let innerMarks = this.findPlace(type.create(attrs), marks, false) 662 if (innerMarks) innerMarks = this.enterInner(type, attrs, marks, true, preserveWS) 663 return innerMarks 664 } 665 666 // Open a node of the given type 667 enterInner(type: NodeType, attrs: Attrs | null, marks: readonly Mark[], 668 solid: boolean = false, preserveWS?: boolean | "full") { 669 this.closeExtra() 670 let top = this.top 671 top.match = top.match && top.match.matchType(type) 672 let options = wsOptionsFor(type, preserveWS, top.options) 673 if ((top.options & OPT_OPEN_LEFT) && top.content.length == 0) options |= OPT_OPEN_LEFT 674 let applyMarks = Mark.none 675 marks = marks.filter(m => { 676 if (top.type ? top.type.allowsMarkType(m.type) : markMayApply(m.type, type)) { 677 applyMarks = m.addToSet(applyMarks) 678 return false 679 } 680 return true 681 }) 682 this.nodes.push(new NodeContext(type, attrs, applyMarks, solid, null, options)) 683 this.open++ 684 return marks 685 } 686 687 // Make sure all nodes above this.open are finished and added to 688 // their parents 689 closeExtra(openEnd = false) { 690 let i = this.nodes.length - 1 691 if (i > this.open) { 692 for (; i > this.open; i--) this.nodes[i - 1].content.push(this.nodes[i].finish(openEnd) as Node) 693 this.nodes.length = this.open + 1 694 } 695 } 696 697 finish() { 698 this.open = 0 699 this.closeExtra(this.isOpen) 700 return this.nodes[0].finish(!!(this.isOpen || this.options.topOpen)) 701 } 702 703 sync(to: NodeContext) { 704 for (let i = this.open; i >= 0; i--) { 705 if (this.nodes[i] == to) { 706 this.open = i 707 return true 708 } else if (this.localPreserveWS) { 709 this.nodes[i].options |= OPT_PRESERVE_WS 710 } 711 } 712 return false 713 } 714 715 get currentPos() { 716 this.closeExtra() 717 let pos = 0 718 for (let i = this.open; i >= 0; i--) { 719 let content = this.nodes[i].content 720 for (let j = content.length - 1; j >= 0; j--) 721 pos += content[j].nodeSize 722 if (i) pos++ 723 } 724 return pos 725 } 726 727 findAtPoint(parent: DOMNode, offset: number) { 728 if (this.find) for (let i = 0; i < this.find.length; i++) { 729 if (this.find[i].node == parent && this.find[i].offset == offset) 730 this.find[i].pos = this.currentPos 731 } 732 } 733 734 findInside(parent: DOMNode) { 735 if (this.find) for (let i = 0; i < this.find.length; i++) { 736 if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node)) 737 this.find[i].pos = this.currentPos 738 } 739 } 740 741 findAround(parent: DOMNode, content: DOMNode, before: boolean) { 742 if (parent != content && this.find) for (let i = 0; i < this.find.length; i++) { 743 if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node)) { 744 let pos = content.compareDocumentPosition(this.find[i].node) 745 if (pos & (before ? 2 : 4)) 746 this.find[i].pos = this.currentPos 747 } 748 } 749 } 750 751 findInText(textNode: Text) { 752 if (this.find) for (let i = 0; i < this.find.length; i++) { 753 if (this.find[i].node == textNode) 754 this.find[i].pos = this.currentPos - (textNode.nodeValue!.length - this.find[i].offset) 755 } 756 } 757 758 // Determines whether the given context string matches this context. 759 matchesContext(context: string) { 760 if (context.indexOf("|") > -1) 761 return context.split(/\s*\|\s*/).some(this.matchesContext, this) 762 763 let parts = context.split("/") 764 let option = this.options.context 765 let useRoot = !this.isOpen && (!option || option.parent.type == this.nodes[0].type) 766 let minDepth = -(option ? option.depth + 1 : 0) + (useRoot ? 0 : 1) 767 let match = (i: number, depth: number) => { 768 for (; i >= 0; i--) { 769 let part = parts[i] 770 if (part == "") { 771 if (i == parts.length - 1 || i == 0) continue 772 for (; depth >= minDepth; depth--) 773 if (match(i - 1, depth)) return true 774 return false 775 } else { 776 let next = depth > 0 || (depth == 0 && useRoot) ? this.nodes[depth].type 777 : option && depth >= minDepth ? option.node(depth - minDepth).type 778 : null 779 if (!next || (next.name != part && !next.isInGroup(part))) 780 return false 781 depth-- 782 } 783 } 784 return true 785 } 786 return match(parts.length - 1, this.open) 787 } 788 789 textblockFromContext() { 790 let $context = this.options.context 791 if ($context) for (let d = $context.depth; d >= 0; d--) { 792 let deflt = $context.node(d).contentMatchAt($context.indexAfter(d)).defaultType 793 if (deflt && deflt.isTextblock && deflt.defaultAttrs) return deflt 794 } 795 for (let name in this.parser.schema.nodes) { 796 let type = this.parser.schema.nodes[name] 797 if (type.isTextblock && type.defaultAttrs) return type 798 } 799 } 800 } 801 802 // Kludge to work around directly nested list nodes produced by some 803 // tools and allowed by browsers to mean that the nested list is 804 // actually part of the list item above it. 805 function normalizeList(dom: DOMNode) { 806 for (let child = dom.firstChild, prevItem: ChildNode | null = null; child; child = child.nextSibling) { 807 let name = child.nodeType == 1 ? child.nodeName.toLowerCase() : null 808 if (name && listTags.hasOwnProperty(name) && prevItem) { 809 prevItem.appendChild(child) 810 child = prevItem 811 } else if (name == "li") { 812 prevItem = child 813 } else if (name) { 814 prevItem = null 815 } 816 } 817 } 818 819 // Apply a CSS selector. 820 function matches(dom: any, selector: string): boolean { 821 return (dom.matches || dom.msMatchesSelector || dom.webkitMatchesSelector || dom.mozMatchesSelector).call(dom, selector) 822 } 823 824 function copy(obj: {[prop: string]: any}) { 825 let copy: {[prop: string]: any} = {} 826 for (let prop in obj) copy[prop] = obj[prop] 827 return copy 828 } 829 830 // Used when finding a mark at the top level of a fragment parse. 831 // Checks whether it would be reasonable to apply a given mark type to 832 // a given node, by looking at the way the mark occurs in the schema. 833 function markMayApply(markType: MarkType, nodeType: NodeType) { 834 let nodes = nodeType.schema.nodes 835 for (let name in nodes) { 836 let parent = nodes[name] 837 if (!parent.allowsMarkType(markType)) continue 838 let seen: ContentMatch[] = [], scan = (match: ContentMatch) => { 839 seen.push(match) 840 for (let i = 0; i < match.edgeCount; i++) { 841 let {type, next} = match.edge(i) 842 if (type == nodeType) return true 843 if (seen.indexOf(next) < 0 && scan(next)) return true 844 } 845 } 846 if (scan(parent.contentMatch)) return true 847 } 848 }