tor-browser

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

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 }