tor-browser

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

to_dom.ts (9865B)


      1 import {Fragment} from "./fragment"
      2 import {Node} from "./node"
      3 import {Schema, NodeType, MarkType} from "./schema"
      4 import {Mark} from "./mark"
      5 import {DOMNode} from "./dom"
      6 
      7 /// A description of a DOM structure. Can be either a string, which is
      8 /// interpreted as a text node, a DOM node, which is interpreted as
      9 /// itself, a `{dom, contentDOM}` object, or an array.
     10 ///
     11 /// An array describes a DOM element. The first value in the array
     12 /// should be a string—the name of the DOM element, optionally prefixed
     13 /// by a namespace URL and a space. If the second element is plain
     14 /// object, it is interpreted as a set of attributes for the element.
     15 /// Any elements after that (including the 2nd if it's not an attribute
     16 /// object) are interpreted as children of the DOM elements, and must
     17 /// either be valid `DOMOutputSpec` values, or the number zero.
     18 ///
     19 /// The number zero (pronounced “hole”) is used to indicate the place
     20 /// where a node's child nodes should be inserted. If it occurs in an
     21 /// output spec, it should be the only child element in its parent
     22 /// node.
     23 export type DOMOutputSpec = string | DOMNode | {dom: DOMNode, contentDOM?: HTMLElement} | readonly [string, ...any[]]
     24 
     25 /// A DOM serializer knows how to convert ProseMirror nodes and
     26 /// marks of various types to DOM nodes.
     27 export class DOMSerializer {
     28  /// Create a serializer. `nodes` should map node names to functions
     29  /// that take a node and return a description of the corresponding
     30  /// DOM. `marks` does the same for mark names, but also gets an
     31  /// argument that tells it whether the mark's content is block or
     32  /// inline content (for typical use, it'll always be inline). A mark
     33  /// serializer may be `null` to indicate that marks of that type
     34  /// should not be serialized.
     35  constructor(
     36    /// The node serialization functions.
     37    readonly nodes: {[node: string]: (node: Node) => DOMOutputSpec},
     38    /// The mark serialization functions.
     39    readonly marks: {[mark: string]: (mark: Mark, inline: boolean) => DOMOutputSpec}
     40  ) {}
     41 
     42  /// Serialize the content of this fragment to a DOM fragment. When
     43  /// not in the browser, the `document` option, containing a DOM
     44  /// document, should be passed so that the serializer can create
     45  /// nodes.
     46  serializeFragment(fragment: Fragment, options: {document?: Document} = {}, target?: HTMLElement | DocumentFragment) {
     47    if (!target) target = doc(options).createDocumentFragment()
     48 
     49    let top = target!, active: [Mark, HTMLElement | DocumentFragment][] = []
     50    fragment.forEach(node => {
     51      if (active.length || node.marks.length) {
     52        let keep = 0, rendered = 0
     53        while (keep < active.length && rendered < node.marks.length) {
     54          let next = node.marks[rendered]
     55          if (!this.marks[next.type.name]) { rendered++; continue }
     56          if (!next.eq(active[keep][0]) || next.type.spec.spanning === false) break
     57          keep++; rendered++
     58        }
     59        while (keep < active.length) top = active.pop()![1]
     60        while (rendered < node.marks.length) {
     61          let add = node.marks[rendered++]
     62          let markDOM = this.serializeMark(add, node.isInline, options)
     63          if (markDOM) {
     64            active.push([add, top])
     65            top.appendChild(markDOM.dom)
     66            top = markDOM.contentDOM || markDOM.dom as HTMLElement
     67          }
     68        }
     69      }
     70      top.appendChild(this.serializeNodeInner(node, options))
     71    })
     72 
     73    return target
     74  }
     75 
     76  /// @internal
     77  serializeNodeInner(node: Node, options: {document?: Document}) {
     78    let {dom, contentDOM} =
     79      renderSpec(doc(options), this.nodes[node.type.name](node), null, node.attrs)
     80    if (contentDOM) {
     81      if (node.isLeaf)
     82        throw new RangeError("Content hole not allowed in a leaf node spec")
     83      this.serializeFragment(node.content, options, contentDOM)
     84    }
     85    return dom
     86  }
     87 
     88  /// Serialize this node to a DOM node. This can be useful when you
     89  /// need to serialize a part of a document, as opposed to the whole
     90  /// document. To serialize a whole document, use
     91  /// [`serializeFragment`](#model.DOMSerializer.serializeFragment) on
     92  /// its [content](#model.Node.content).
     93  serializeNode(node: Node, options: {document?: Document} = {}) {
     94    let dom = this.serializeNodeInner(node, options)
     95    for (let i = node.marks.length - 1; i >= 0; i--) {
     96      let wrap = this.serializeMark(node.marks[i], node.isInline, options)
     97      if (wrap) {
     98        ;(wrap.contentDOM || wrap.dom).appendChild(dom)
     99        dom = wrap.dom
    100      }
    101    }
    102    return dom
    103  }
    104 
    105  /// @internal
    106  serializeMark(mark: Mark, inline: boolean, options: {document?: Document} = {}) {
    107    let toDOM = this.marks[mark.type.name]
    108    return toDOM && renderSpec(doc(options), toDOM(mark, inline), null, mark.attrs)
    109  }
    110 
    111  /// Render an [output spec](#model.DOMOutputSpec) to a DOM node. If
    112  /// the spec has a hole (zero) in it, `contentDOM` will point at the
    113  /// node with the hole.
    114  static renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS?: string | null): {
    115    dom: DOMNode,
    116    contentDOM?: HTMLElement
    117  }
    118  static renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS: string | null = null,
    119                    blockArraysIn?: {[name: string]: any}): {
    120    dom: DOMNode,
    121    contentDOM?: HTMLElement
    122  } {
    123    return renderSpec(doc, structure, xmlNS, blockArraysIn)
    124  }
    125 
    126  /// Build a serializer using the [`toDOM`](#model.NodeSpec.toDOM)
    127  /// properties in a schema's node and mark specs.
    128  static fromSchema(schema: Schema): DOMSerializer {
    129    return schema.cached.domSerializer as DOMSerializer ||
    130      (schema.cached.domSerializer = new DOMSerializer(this.nodesFromSchema(schema), this.marksFromSchema(schema)))
    131  }
    132 
    133  /// Gather the serializers in a schema's node specs into an object.
    134  /// This can be useful as a base to build a custom serializer from.
    135  static nodesFromSchema(schema: Schema) {
    136    let result = gatherToDOM(schema.nodes)
    137    if (!result.text) result.text = node => node.text
    138    return result as {[node: string]: (node: Node) => DOMOutputSpec}
    139  }
    140 
    141  /// Gather the serializers in a schema's mark specs into an object.
    142  static marksFromSchema(schema: Schema) {
    143    return gatherToDOM(schema.marks) as {[mark: string]: (mark: Mark, inline: boolean) => DOMOutputSpec}
    144  }
    145 }
    146 
    147 function gatherToDOM(obj: {[node: string]: NodeType | MarkType}) {
    148  let result: {[node: string]: (value: any, inline: boolean) => DOMOutputSpec} = {}
    149  for (let name in obj) {
    150    let toDOM = obj[name].spec.toDOM
    151    if (toDOM) result[name] = toDOM
    152  }
    153  return result
    154 }
    155 
    156 function doc(options: {document?: Document}) {
    157  return options.document || window.document
    158 }
    159 
    160 const suspiciousAttributeCache = new WeakMap<any, readonly any[] | null>()
    161 
    162 function suspiciousAttributes(attrs: {[name: string]: any}): readonly any[] | null {
    163  let value = suspiciousAttributeCache.get(attrs)
    164  if (value === undefined)
    165    suspiciousAttributeCache.set(attrs, value = suspiciousAttributesInner(attrs))
    166  return value
    167 }
    168 
    169 function suspiciousAttributesInner(attrs: {[name: string]: any}): readonly any[] | null {
    170  let result: any[] | null = null
    171  function scan(value: any) {
    172    if (value && typeof value == "object") {
    173      if (Array.isArray(value)) {
    174        if (typeof value[0] == "string") {
    175          if (!result) result = []
    176          result.push(value)
    177        } else {
    178          for (let i = 0; i < value.length; i++) scan(value[i])
    179        }
    180      } else {
    181        for (let prop in value) scan(value[prop])
    182      }
    183    }
    184  }
    185  scan(attrs)
    186  return result
    187 }
    188 
    189 function renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS: string | null,
    190                    blockArraysIn?: {[name: string]: any}): {
    191  dom: DOMNode,
    192  contentDOM?: HTMLElement
    193 } {
    194  if (typeof structure == "string")
    195    return {dom: doc.createTextNode(structure)}
    196  if ((structure as DOMNode).nodeType != null)
    197    return {dom: structure as DOMNode}
    198  if ((structure as any).dom && (structure as any).dom.nodeType != null)
    199    return structure as {dom: DOMNode, contentDOM?: HTMLElement}
    200  let tagName = (structure as [string])[0], suspicious
    201  if (typeof tagName != "string") throw new RangeError("Invalid array passed to renderSpec")
    202  if (blockArraysIn && (suspicious = suspiciousAttributes(blockArraysIn)) &&
    203      suspicious.indexOf(structure) > -1)
    204    throw new RangeError("Using an array from an attribute object as a DOM spec. This may be an attempted cross site scripting attack.")
    205  let space = tagName.indexOf(" ")
    206  if (space > 0) {
    207    xmlNS = tagName.slice(0, space)
    208    tagName = tagName.slice(space + 1)
    209  }
    210  let contentDOM: HTMLElement | undefined
    211  let dom = (xmlNS ? doc.createElementNS(xmlNS, tagName) : doc.createElement(tagName)) as HTMLElement
    212  let attrs = (structure as any)[1], start = 1
    213  if (attrs && typeof attrs == "object" && attrs.nodeType == null && !Array.isArray(attrs)) {
    214    start = 2
    215    for (let name in attrs) if (attrs[name] != null) {
    216      let space = name.indexOf(" ")
    217      if (space > 0) dom.setAttributeNS(name.slice(0, space), name.slice(space + 1), attrs[name])
    218      else if (name == "style" && dom.style) dom.style.cssText = attrs[name]
    219      else dom.setAttribute(name, attrs[name])
    220    }
    221  }
    222  for (let i = start; i < (structure as readonly any[]).length; i++) {
    223    let child = (structure as any)[i] as DOMOutputSpec | 0
    224    if (child === 0) {
    225      if (i < (structure as readonly any[]).length - 1 || i > start)
    226        throw new RangeError("Content hole must be the only child of its parent node")
    227      return {dom, contentDOM: dom}
    228    } else {
    229      let {dom: inner, contentDOM: innerContent} = renderSpec(doc, child, xmlNS, blockArraysIn)
    230      dom.appendChild(inner)
    231      if (innerContent) {
    232        if (contentDOM) throw new RangeError("Multiple content holes")
    233        contentDOM = innerContent as HTMLElement
    234      }
    235    }
    236  }
    237  return {dom, contentDOM}
    238 }