Initial
This commit is contained in:
62
resources/app/node_modules/prosemirror-model/src/README.md
generated
vendored
Normal file
62
resources/app/node_modules/prosemirror-model/src/README.md
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
This module defines ProseMirror's content model, the data structures
|
||||
used to represent and work with documents.
|
||||
|
||||
### Document Structure
|
||||
|
||||
A ProseMirror document is a tree. At each level, a [node](#model.Node)
|
||||
describes the type of the content, and holds a
|
||||
[fragment](#model.Fragment) containing its children.
|
||||
|
||||
@Node
|
||||
@Fragment
|
||||
@Mark
|
||||
@Slice
|
||||
@Attrs
|
||||
@ReplaceError
|
||||
|
||||
### Resolved Positions
|
||||
|
||||
Positions in a document can be represented as integer
|
||||
[offsets](/docs/guide/#doc.indexing). But you'll often want to use a
|
||||
more convenient representation.
|
||||
|
||||
@ResolvedPos
|
||||
@NodeRange
|
||||
|
||||
### Document Schema
|
||||
|
||||
Every ProseMirror document conforms to a
|
||||
[schema](/docs/guide/#schema), which describes the set of nodes and
|
||||
marks that it is made out of, along with the relations between those,
|
||||
such as which node may occur as a child node of which other nodes.
|
||||
|
||||
@Schema
|
||||
|
||||
@SchemaSpec
|
||||
@NodeSpec
|
||||
@MarkSpec
|
||||
@AttributeSpec
|
||||
|
||||
@NodeType
|
||||
@MarkType
|
||||
|
||||
@ContentMatch
|
||||
|
||||
### DOM Representation
|
||||
|
||||
Because representing a document as a tree of DOM nodes is central to
|
||||
the way ProseMirror operates, DOM [parsing](#model.DOMParser) and
|
||||
[serializing](#model.DOMSerializer) is integrated with the model.
|
||||
|
||||
(But note that you do _not_ need to have a DOM implementation loaded
|
||||
to use this module.)
|
||||
|
||||
@DOMParser
|
||||
@ParseOptions
|
||||
@GenericParseRule
|
||||
@TagParseRule
|
||||
@StyleParseRule
|
||||
@ParseRule
|
||||
|
||||
@DOMSerializer
|
||||
@DOMOutputSpec
|
||||
15
resources/app/node_modules/prosemirror-model/src/comparedeep.ts
generated
vendored
Normal file
15
resources/app/node_modules/prosemirror-model/src/comparedeep.ts
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export function compareDeep(a: any, b: any) {
|
||||
if (a === b) return true
|
||||
if (!(a && typeof a == "object") ||
|
||||
!(b && typeof b == "object")) return false
|
||||
let array = Array.isArray(a)
|
||||
if (Array.isArray(b) != array) return false
|
||||
if (array) {
|
||||
if (a.length != b.length) return false
|
||||
for (let i = 0; i < a.length; i++) if (!compareDeep(a[i], b[i])) return false
|
||||
} else {
|
||||
for (let p in a) if (!(p in b) || !compareDeep(a[p], b[p])) return false
|
||||
for (let p in b) if (!(p in a)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
413
resources/app/node_modules/prosemirror-model/src/content.ts
generated
vendored
Normal file
413
resources/app/node_modules/prosemirror-model/src/content.ts
generated
vendored
Normal file
@@ -0,0 +1,413 @@
|
||||
import {Fragment} from "./fragment"
|
||||
import {NodeType} from "./schema"
|
||||
|
||||
type MatchEdge = {type: NodeType, next: ContentMatch}
|
||||
|
||||
/// Instances of this class represent a match state of a node type's
|
||||
/// [content expression](#model.NodeSpec.content), and can be used to
|
||||
/// find out whether further content matches here, and whether a given
|
||||
/// position is a valid end of the node.
|
||||
export class ContentMatch {
|
||||
/// @internal
|
||||
readonly next: MatchEdge[] = []
|
||||
/// @internal
|
||||
readonly wrapCache: (NodeType | readonly NodeType[] | null)[] = []
|
||||
|
||||
/// @internal
|
||||
constructor(
|
||||
/// True when this match state represents a valid end of the node.
|
||||
readonly validEnd: boolean
|
||||
) {}
|
||||
|
||||
/// @internal
|
||||
static parse(string: string, nodeTypes: {readonly [name: string]: NodeType}): ContentMatch {
|
||||
let stream = new TokenStream(string, nodeTypes)
|
||||
if (stream.next == null) return ContentMatch.empty
|
||||
let expr = parseExpr(stream)
|
||||
if (stream.next) stream.err("Unexpected trailing text")
|
||||
let match = dfa(nfa(expr))
|
||||
checkForDeadEnds(match, stream)
|
||||
return match
|
||||
}
|
||||
|
||||
/// Match a node type, returning a match after that node if
|
||||
/// successful.
|
||||
matchType(type: NodeType): ContentMatch | null {
|
||||
for (let i = 0; i < this.next.length; i++)
|
||||
if (this.next[i].type == type) return this.next[i].next
|
||||
return null
|
||||
}
|
||||
|
||||
/// Try to match a fragment. Returns the resulting match when
|
||||
/// successful.
|
||||
matchFragment(frag: Fragment, start = 0, end = frag.childCount): ContentMatch | null {
|
||||
let cur: ContentMatch | null = this
|
||||
for (let i = start; cur && i < end; i++)
|
||||
cur = cur.matchType(frag.child(i).type)
|
||||
return cur
|
||||
}
|
||||
|
||||
/// @internal
|
||||
get inlineContent() {
|
||||
return this.next.length != 0 && this.next[0].type.isInline
|
||||
}
|
||||
|
||||
/// Get the first matching node type at this match position that can
|
||||
/// be generated.
|
||||
get defaultType(): NodeType | null {
|
||||
for (let i = 0; i < this.next.length; i++) {
|
||||
let {type} = this.next[i]
|
||||
if (!(type.isText || type.hasRequiredAttrs())) return type
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/// @internal
|
||||
compatible(other: ContentMatch) {
|
||||
for (let i = 0; i < this.next.length; i++)
|
||||
for (let j = 0; j < other.next.length; j++)
|
||||
if (this.next[i].type == other.next[j].type) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/// Try to match the given fragment, and if that fails, see if it can
|
||||
/// be made to match by inserting nodes in front of it. When
|
||||
/// successful, return a fragment of inserted nodes (which may be
|
||||
/// empty if nothing had to be inserted). When `toEnd` is true, only
|
||||
/// return a fragment if the resulting match goes to the end of the
|
||||
/// content expression.
|
||||
fillBefore(after: Fragment, toEnd = false, startIndex = 0): Fragment | null {
|
||||
let seen: ContentMatch[] = [this]
|
||||
function search(match: ContentMatch, types: readonly NodeType[]): Fragment | null {
|
||||
let finished = match.matchFragment(after, startIndex)
|
||||
if (finished && (!toEnd || finished.validEnd))
|
||||
return Fragment.from(types.map(tp => tp.createAndFill()!))
|
||||
|
||||
for (let i = 0; i < match.next.length; i++) {
|
||||
let {type, next} = match.next[i]
|
||||
if (!(type.isText || type.hasRequiredAttrs()) && seen.indexOf(next) == -1) {
|
||||
seen.push(next)
|
||||
let found = search(next, types.concat(type))
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return search(this, [])
|
||||
}
|
||||
|
||||
/// Find a set of wrapping node types that would allow a node of the
|
||||
/// given type to appear at this position. The result may be empty
|
||||
/// (when it fits directly) and will be null when no such wrapping
|
||||
/// exists.
|
||||
findWrapping(target: NodeType): readonly NodeType[] | null {
|
||||
for (let i = 0; i < this.wrapCache.length; i += 2)
|
||||
if (this.wrapCache[i] == target) return this.wrapCache[i + 1] as (readonly NodeType[] | null)
|
||||
let computed = this.computeWrapping(target)
|
||||
this.wrapCache.push(target, computed)
|
||||
return computed
|
||||
}
|
||||
|
||||
/// @internal
|
||||
computeWrapping(target: NodeType): readonly NodeType[] | null {
|
||||
type Active = {match: ContentMatch, type: NodeType | null, via: Active | null}
|
||||
let seen = Object.create(null), active: Active[] = [{match: this, type: null, via: null}]
|
||||
while (active.length) {
|
||||
let current = active.shift()!, match = current.match
|
||||
if (match.matchType(target)) {
|
||||
let result: NodeType[] = []
|
||||
for (let obj: Active = current; obj.type; obj = obj.via!)
|
||||
result.push(obj.type)
|
||||
return result.reverse()
|
||||
}
|
||||
for (let i = 0; i < match.next.length; i++) {
|
||||
let {type, next} = match.next[i]
|
||||
if (!type.isLeaf && !type.hasRequiredAttrs() && !(type.name in seen) && (!current.type || next.validEnd)) {
|
||||
active.push({match: type.contentMatch, type, via: current})
|
||||
seen[type.name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/// The number of outgoing edges this node has in the finite
|
||||
/// automaton that describes the content expression.
|
||||
get edgeCount() {
|
||||
return this.next.length
|
||||
}
|
||||
|
||||
/// Get the _n_th outgoing edge from this node in the finite
|
||||
/// automaton that describes the content expression.
|
||||
edge(n: number): MatchEdge {
|
||||
if (n >= this.next.length) throw new RangeError(`There's no ${n}th edge in this content match`)
|
||||
return this.next[n]
|
||||
}
|
||||
|
||||
/// @internal
|
||||
toString() {
|
||||
let seen: ContentMatch[] = []
|
||||
function scan(m: ContentMatch) {
|
||||
seen.push(m)
|
||||
for (let i = 0; i < m.next.length; i++)
|
||||
if (seen.indexOf(m.next[i].next) == -1) scan(m.next[i].next)
|
||||
}
|
||||
scan(this)
|
||||
return seen.map((m, i) => {
|
||||
let out = i + (m.validEnd ? "*" : " ") + " "
|
||||
for (let i = 0; i < m.next.length; i++)
|
||||
out += (i ? ", " : "") + m.next[i].type.name + "->" + seen.indexOf(m.next[i].next)
|
||||
return out
|
||||
}).join("\n")
|
||||
}
|
||||
|
||||
/// @internal
|
||||
static empty = new ContentMatch(true)
|
||||
}
|
||||
|
||||
class TokenStream {
|
||||
inline: boolean | null = null
|
||||
pos = 0
|
||||
tokens: string[]
|
||||
|
||||
constructor(
|
||||
readonly string: string,
|
||||
readonly nodeTypes: {readonly [name: string]: NodeType}
|
||||
) {
|
||||
this.tokens = string.split(/\s*(?=\b|\W|$)/)
|
||||
if (this.tokens[this.tokens.length - 1] == "") this.tokens.pop()
|
||||
if (this.tokens[0] == "") this.tokens.shift()
|
||||
}
|
||||
|
||||
get next() { return this.tokens[this.pos] }
|
||||
|
||||
eat(tok: string) { return this.next == tok && (this.pos++ || true) }
|
||||
|
||||
err(str: string): never { throw new SyntaxError(str + " (in content expression '" + this.string + "')") }
|
||||
}
|
||||
|
||||
type Expr =
|
||||
{type: "choice", exprs: Expr[]} |
|
||||
{type: "seq", exprs: Expr[]} |
|
||||
{type: "plus", expr: Expr} |
|
||||
{type: "star", expr: Expr} |
|
||||
{type: "opt", expr: Expr} |
|
||||
{type: "range", min: number, max: number, expr: Expr} |
|
||||
{type: "name", value: NodeType}
|
||||
|
||||
function parseExpr(stream: TokenStream): Expr {
|
||||
let exprs = []
|
||||
do { exprs.push(parseExprSeq(stream)) }
|
||||
while (stream.eat("|"))
|
||||
return exprs.length == 1 ? exprs[0] : {type: "choice", exprs}
|
||||
}
|
||||
|
||||
function parseExprSeq(stream: TokenStream): Expr {
|
||||
let exprs = []
|
||||
do { exprs.push(parseExprSubscript(stream)) }
|
||||
while (stream.next && stream.next != ")" && stream.next != "|")
|
||||
return exprs.length == 1 ? exprs[0] : {type: "seq", exprs}
|
||||
}
|
||||
|
||||
function parseExprSubscript(stream: TokenStream): Expr {
|
||||
let expr = parseExprAtom(stream)
|
||||
for (;;) {
|
||||
if (stream.eat("+"))
|
||||
expr = {type: "plus", expr}
|
||||
else if (stream.eat("*"))
|
||||
expr = {type: "star", expr}
|
||||
else if (stream.eat("?"))
|
||||
expr = {type: "opt", expr}
|
||||
else if (stream.eat("{"))
|
||||
expr = parseExprRange(stream, expr)
|
||||
else break
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
function parseNum(stream: TokenStream) {
|
||||
if (/\D/.test(stream.next)) stream.err("Expected number, got '" + stream.next + "'")
|
||||
let result = Number(stream.next)
|
||||
stream.pos++
|
||||
return result
|
||||
}
|
||||
|
||||
function parseExprRange(stream: TokenStream, expr: Expr): Expr {
|
||||
let min = parseNum(stream), max = min
|
||||
if (stream.eat(",")) {
|
||||
if (stream.next != "}") max = parseNum(stream)
|
||||
else max = -1
|
||||
}
|
||||
if (!stream.eat("}")) stream.err("Unclosed braced range")
|
||||
return {type: "range", min, max, expr}
|
||||
}
|
||||
|
||||
function resolveName(stream: TokenStream, name: string): readonly NodeType[] {
|
||||
let types = stream.nodeTypes, type = types[name]
|
||||
if (type) return [type]
|
||||
let result = []
|
||||
for (let typeName in types) {
|
||||
let type = types[typeName]
|
||||
if (type.groups.indexOf(name) > -1) result.push(type)
|
||||
}
|
||||
if (result.length == 0) stream.err("No node type or group '" + name + "' found")
|
||||
return result
|
||||
}
|
||||
|
||||
function parseExprAtom(stream: TokenStream): Expr {
|
||||
if (stream.eat("(")) {
|
||||
let expr = parseExpr(stream)
|
||||
if (!stream.eat(")")) stream.err("Missing closing paren")
|
||||
return expr
|
||||
} else if (!/\W/.test(stream.next)) {
|
||||
let exprs = resolveName(stream, stream.next).map(type => {
|
||||
if (stream.inline == null) stream.inline = type.isInline
|
||||
else if (stream.inline != type.isInline) stream.err("Mixing inline and block content")
|
||||
return {type: "name", value: type} as Expr
|
||||
})
|
||||
stream.pos++
|
||||
return exprs.length == 1 ? exprs[0] : {type: "choice", exprs}
|
||||
} else {
|
||||
stream.err("Unexpected token '" + stream.next + "'")
|
||||
}
|
||||
}
|
||||
|
||||
// The code below helps compile a regular-expression-like language
|
||||
// into a deterministic finite automaton. For a good introduction to
|
||||
// these concepts, see https://swtch.com/~rsc/regexp/regexp1.html
|
||||
|
||||
type Edge = {term: NodeType | undefined, to: number | undefined}
|
||||
|
||||
/// Construct an NFA from an expression as returned by the parser. The
|
||||
/// NFA is represented as an array of states, which are themselves
|
||||
/// arrays of edges, which are `{term, to}` objects. The first state is
|
||||
/// the entry state and the last node is the success state.
|
||||
///
|
||||
/// Note that unlike typical NFAs, the edge ordering in this one is
|
||||
/// significant, in that it is used to contruct filler content when
|
||||
/// necessary.
|
||||
function nfa(expr: Expr): Edge[][] {
|
||||
let nfa: Edge[][] = [[]]
|
||||
connect(compile(expr, 0), node())
|
||||
return nfa
|
||||
|
||||
function node() { return nfa.push([]) - 1 }
|
||||
function edge(from: number, to?: number, term?: NodeType) {
|
||||
let edge = {term, to}
|
||||
nfa[from].push(edge)
|
||||
return edge
|
||||
}
|
||||
function connect(edges: Edge[], to: number) {
|
||||
edges.forEach(edge => edge.to = to)
|
||||
}
|
||||
|
||||
function compile(expr: Expr, from: number): Edge[] {
|
||||
if (expr.type == "choice") {
|
||||
return expr.exprs.reduce((out, expr) => out.concat(compile(expr, from)), [] as Edge[])
|
||||
} else if (expr.type == "seq") {
|
||||
for (let i = 0;; i++) {
|
||||
let next = compile(expr.exprs[i], from)
|
||||
if (i == expr.exprs.length - 1) return next
|
||||
connect(next, from = node())
|
||||
}
|
||||
} else if (expr.type == "star") {
|
||||
let loop = node()
|
||||
edge(from, loop)
|
||||
connect(compile(expr.expr, loop), loop)
|
||||
return [edge(loop)]
|
||||
} else if (expr.type == "plus") {
|
||||
let loop = node()
|
||||
connect(compile(expr.expr, from), loop)
|
||||
connect(compile(expr.expr, loop), loop)
|
||||
return [edge(loop)]
|
||||
} else if (expr.type == "opt") {
|
||||
return [edge(from)].concat(compile(expr.expr, from))
|
||||
} else if (expr.type == "range") {
|
||||
let cur = from
|
||||
for (let i = 0; i < expr.min; i++) {
|
||||
let next = node()
|
||||
connect(compile(expr.expr, cur), next)
|
||||
cur = next
|
||||
}
|
||||
if (expr.max == -1) {
|
||||
connect(compile(expr.expr, cur), cur)
|
||||
} else {
|
||||
for (let i = expr.min; i < expr.max; i++) {
|
||||
let next = node()
|
||||
edge(cur, next)
|
||||
connect(compile(expr.expr, cur), next)
|
||||
cur = next
|
||||
}
|
||||
}
|
||||
return [edge(cur)]
|
||||
} else if (expr.type == "name") {
|
||||
return [edge(from, undefined, expr.value)]
|
||||
} else {
|
||||
throw new Error("Unknown expr type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cmp(a: number, b: number) { return b - a }
|
||||
|
||||
// Get the set of nodes reachable by null edges from `node`. Omit
|
||||
// nodes with only a single null-out-edge, since they may lead to
|
||||
// needless duplicated nodes.
|
||||
function nullFrom(nfa: Edge[][], node: number): readonly number[] {
|
||||
let result: number[] = []
|
||||
scan(node)
|
||||
return result.sort(cmp)
|
||||
|
||||
function scan(node: number): void {
|
||||
let edges = nfa[node]
|
||||
if (edges.length == 1 && !edges[0].term) return scan(edges[0].to!)
|
||||
result.push(node)
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
let {term, to} = edges[i]
|
||||
if (!term && result.indexOf(to!) == -1) scan(to!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compiles an NFA as produced by `nfa` into a DFA, modeled as a set
|
||||
// of state objects (`ContentMatch` instances) with transitions
|
||||
// between them.
|
||||
function dfa(nfa: Edge[][]): ContentMatch {
|
||||
let labeled = Object.create(null)
|
||||
return explore(nullFrom(nfa, 0))
|
||||
|
||||
function explore(states: readonly number[]) {
|
||||
let out: [NodeType, number[]][] = []
|
||||
states.forEach(node => {
|
||||
nfa[node].forEach(({term, to}) => {
|
||||
if (!term) return
|
||||
let set: number[] | undefined
|
||||
for (let i = 0; i < out.length; i++) if (out[i][0] == term) set = out[i][1]
|
||||
nullFrom(nfa, to!).forEach(node => {
|
||||
if (!set) out.push([term, set = []])
|
||||
if (set.indexOf(node) == -1) set.push(node)
|
||||
})
|
||||
})
|
||||
})
|
||||
let state = labeled[states.join(",")] = new ContentMatch(states.indexOf(nfa.length - 1) > -1)
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
let states = out[i][1].sort(cmp)
|
||||
state.next.push({type: out[i][0], next: labeled[states.join(",")] || explore(states)})
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function checkForDeadEnds(match: ContentMatch, stream: TokenStream) {
|
||||
for (let i = 0, work = [match]; i < work.length; i++) {
|
||||
let state = work[i], dead = !state.validEnd, nodes = []
|
||||
for (let j = 0; j < state.next.length; j++) {
|
||||
let {type, next} = state.next[j]
|
||||
nodes.push(type.name)
|
||||
if (dead && !(type.isText || type.hasRequiredAttrs())) dead = false
|
||||
if (work.indexOf(next) == -1) work.push(next)
|
||||
}
|
||||
if (dead) stream.err("Only non-generatable nodes (" + nodes.join(", ") + ") in a required position (see https://prosemirror.net/docs/guide/#generatable)")
|
||||
}
|
||||
}
|
||||
52
resources/app/node_modules/prosemirror-model/src/diff.ts
generated
vendored
Normal file
52
resources/app/node_modules/prosemirror-model/src/diff.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import {Fragment} from "./fragment"
|
||||
|
||||
export function findDiffStart(a: Fragment, b: Fragment, pos: number): number | null {
|
||||
for (let i = 0;; i++) {
|
||||
if (i == a.childCount || i == b.childCount)
|
||||
return a.childCount == b.childCount ? null : pos
|
||||
|
||||
let childA = a.child(i), childB = b.child(i)
|
||||
if (childA == childB) { pos += childA.nodeSize; continue }
|
||||
|
||||
if (!childA.sameMarkup(childB)) return pos
|
||||
|
||||
if (childA.isText && childA.text != childB.text) {
|
||||
for (let j = 0; childA.text![j] == childB.text![j]; j++)
|
||||
pos++
|
||||
return pos
|
||||
}
|
||||
if (childA.content.size || childB.content.size) {
|
||||
let inner = findDiffStart(childA.content, childB.content, pos + 1)
|
||||
if (inner != null) return inner
|
||||
}
|
||||
pos += childA.nodeSize
|
||||
}
|
||||
}
|
||||
|
||||
export function findDiffEnd(a: Fragment, b: Fragment, posA: number, posB: number): {a: number, b: number} | null {
|
||||
for (let iA = a.childCount, iB = b.childCount;;) {
|
||||
if (iA == 0 || iB == 0)
|
||||
return iA == iB ? null : {a: posA, b: posB}
|
||||
|
||||
let childA = a.child(--iA), childB = b.child(--iB), size = childA.nodeSize
|
||||
if (childA == childB) {
|
||||
posA -= size; posB -= size
|
||||
continue
|
||||
}
|
||||
|
||||
if (!childA.sameMarkup(childB)) return {a: posA, b: posB}
|
||||
|
||||
if (childA.isText && childA.text != childB.text) {
|
||||
let same = 0, minSize = Math.min(childA.text!.length, childB.text!.length)
|
||||
while (same < minSize && childA.text![childA.text!.length - same - 1] == childB.text![childB.text!.length - same - 1]) {
|
||||
same++; posA--; posB--
|
||||
}
|
||||
return {a: posA, b: posB}
|
||||
}
|
||||
if (childA.content.size || childB.content.size) {
|
||||
let inner = findDiffEnd(childA.content, childB.content, posA - 1, posB - 1)
|
||||
if (inner) return inner
|
||||
}
|
||||
posA -= size; posB -= size
|
||||
}
|
||||
}
|
||||
1
resources/app/node_modules/prosemirror-model/src/dom.ts
generated
vendored
Normal file
1
resources/app/node_modules/prosemirror-model/src/dom.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type DOMNode = InstanceType<typeof window.Node>
|
||||
268
resources/app/node_modules/prosemirror-model/src/fragment.ts
generated
vendored
Normal file
268
resources/app/node_modules/prosemirror-model/src/fragment.ts
generated
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
import {findDiffStart, findDiffEnd} from "./diff"
|
||||
import {Node, TextNode} from "./node"
|
||||
import {Schema} from "./schema"
|
||||
|
||||
/// A fragment represents a node's collection of child nodes.
|
||||
///
|
||||
/// Like nodes, fragments are persistent data structures, and you
|
||||
/// should not mutate them or their content. Rather, you create new
|
||||
/// instances whenever needed. The API tries to make this easy.
|
||||
export class Fragment {
|
||||
/// The size of the fragment, which is the total of the size of
|
||||
/// its content nodes.
|
||||
readonly size: number
|
||||
|
||||
/// @internal
|
||||
constructor(
|
||||
/// @internal
|
||||
readonly content: readonly Node[],
|
||||
size?: number
|
||||
) {
|
||||
this.size = size || 0
|
||||
if (size == null) for (let i = 0; i < content.length; i++)
|
||||
this.size += content[i].nodeSize
|
||||
}
|
||||
|
||||
/// Invoke a callback for all descendant nodes between the given two
|
||||
/// positions (relative to start of this fragment). Doesn't descend
|
||||
/// into a node when the callback returns `false`.
|
||||
nodesBetween(from: number, to: number,
|
||||
f: (node: Node, start: number, parent: Node | null, index: number) => boolean | void,
|
||||
nodeStart = 0,
|
||||
parent?: Node) {
|
||||
for (let i = 0, pos = 0; pos < to; i++) {
|
||||
let child = this.content[i], end = pos + child.nodeSize
|
||||
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
|
||||
let start = pos + 1
|
||||
child.nodesBetween(Math.max(0, from - start),
|
||||
Math.min(child.content.size, to - start),
|
||||
f, nodeStart + start)
|
||||
}
|
||||
pos = end
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the given callback for every descendant node. `pos` will be
|
||||
/// relative to the start of the fragment. The callback may return
|
||||
/// `false` to prevent traversal of a given node's children.
|
||||
descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => boolean | void) {
|
||||
this.nodesBetween(0, this.size, f)
|
||||
}
|
||||
|
||||
/// Extract the text between `from` and `to`. See the same method on
|
||||
/// [`Node`](#model.Node.textBetween).
|
||||
textBetween(from: number, to: number, blockSeparator?: string | null, leafText?: string | null | ((leafNode: Node) => string)) {
|
||||
let text = "", first = true
|
||||
this.nodesBetween(from, to, (node, pos) => {
|
||||
let nodeText = node.isText ? node.text!.slice(Math.max(from, pos) - pos, to - pos)
|
||||
: !node.isLeaf ? ""
|
||||
: leafText ? (typeof leafText === "function" ? leafText(node) : leafText)
|
||||
: node.type.spec.leafText ? node.type.spec.leafText(node)
|
||||
: ""
|
||||
if (node.isBlock && (node.isLeaf && nodeText || node.isTextblock) && blockSeparator) {
|
||||
if (first) first = false
|
||||
else text += blockSeparator
|
||||
}
|
||||
text += nodeText
|
||||
}, 0)
|
||||
return text
|
||||
}
|
||||
|
||||
/// Create a new fragment containing the combined content of this
|
||||
/// fragment and the other.
|
||||
append(other: Fragment) {
|
||||
if (!other.size) return this
|
||||
if (!this.size) return other
|
||||
let last = this.lastChild!, first = other.firstChild!, content = this.content.slice(), i = 0
|
||||
if (last.isText && last.sameMarkup(first)) {
|
||||
content[content.length - 1] = (last as TextNode).withText(last.text! + first.text!)
|
||||
i = 1
|
||||
}
|
||||
for (; i < other.content.length; i++) content.push(other.content[i])
|
||||
return new Fragment(content, this.size + other.size)
|
||||
}
|
||||
|
||||
/// Cut out the sub-fragment between the two given positions.
|
||||
cut(from: number, to = this.size) {
|
||||
if (from == 0 && to == this.size) return this
|
||||
let result = [], size = 0
|
||||
if (to > from) for (let i = 0, pos = 0; pos < to; i++) {
|
||||
let child = this.content[i], end = pos + child.nodeSize
|
||||
if (end > from) {
|
||||
if (pos < from || end > to) {
|
||||
if (child.isText)
|
||||
child = child.cut(Math.max(0, from - pos), Math.min(child.text!.length, to - pos))
|
||||
else
|
||||
child = child.cut(Math.max(0, from - pos - 1), Math.min(child.content.size, to - pos - 1))
|
||||
}
|
||||
result.push(child)
|
||||
size += child.nodeSize
|
||||
}
|
||||
pos = end
|
||||
}
|
||||
return new Fragment(result, size)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
cutByIndex(from: number, to: number) {
|
||||
if (from == to) return Fragment.empty
|
||||
if (from == 0 && to == this.content.length) return this
|
||||
return new Fragment(this.content.slice(from, to))
|
||||
}
|
||||
|
||||
/// Create a new fragment in which the node at the given index is
|
||||
/// replaced by the given node.
|
||||
replaceChild(index: number, node: Node) {
|
||||
let current = this.content[index]
|
||||
if (current == node) return this
|
||||
let copy = this.content.slice()
|
||||
let size = this.size + node.nodeSize - current.nodeSize
|
||||
copy[index] = node
|
||||
return new Fragment(copy, size)
|
||||
}
|
||||
|
||||
/// Create a new fragment by prepending the given node to this
|
||||
/// fragment.
|
||||
addToStart(node: Node) {
|
||||
return new Fragment([node].concat(this.content), this.size + node.nodeSize)
|
||||
}
|
||||
|
||||
/// Create a new fragment by appending the given node to this
|
||||
/// fragment.
|
||||
addToEnd(node: Node) {
|
||||
return new Fragment(this.content.concat(node), this.size + node.nodeSize)
|
||||
}
|
||||
|
||||
/// Compare this fragment to another one.
|
||||
eq(other: Fragment): boolean {
|
||||
if (this.content.length != other.content.length) return false
|
||||
for (let i = 0; i < this.content.length; i++)
|
||||
if (!this.content[i].eq(other.content[i])) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/// The first child of the fragment, or `null` if it is empty.
|
||||
get firstChild(): Node | null { return this.content.length ? this.content[0] : null }
|
||||
|
||||
/// The last child of the fragment, or `null` if it is empty.
|
||||
get lastChild(): Node | null { return this.content.length ? this.content[this.content.length - 1] : null }
|
||||
|
||||
/// The number of child nodes in this fragment.
|
||||
get childCount() { return this.content.length }
|
||||
|
||||
/// Get the child node at the given index. Raise an error when the
|
||||
/// index is out of range.
|
||||
child(index: number) {
|
||||
let found = this.content[index]
|
||||
if (!found) throw new RangeError("Index " + index + " out of range for " + this)
|
||||
return found
|
||||
}
|
||||
|
||||
/// Get the child node at the given index, if it exists.
|
||||
maybeChild(index: number): Node | null {
|
||||
return this.content[index] || null
|
||||
}
|
||||
|
||||
/// Call `f` for every child node, passing the node, its offset
|
||||
/// into this parent node, and its index.
|
||||
forEach(f: (node: Node, offset: number, index: number) => void) {
|
||||
for (let i = 0, p = 0; i < this.content.length; i++) {
|
||||
let child = this.content[i]
|
||||
f(child, p, i)
|
||||
p += child.nodeSize
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the first position at which this fragment and another
|
||||
/// fragment differ, or `null` if they are the same.
|
||||
findDiffStart(other: Fragment, pos = 0) {
|
||||
return findDiffStart(this, other, pos)
|
||||
}
|
||||
|
||||
/// Find the first position, searching from the end, at which this
|
||||
/// fragment and the given fragment differ, or `null` if they are
|
||||
/// the same. Since this position will not be the same in both
|
||||
/// nodes, an object with two separate positions is returned.
|
||||
findDiffEnd(other: Fragment, pos = this.size, otherPos = other.size) {
|
||||
return findDiffEnd(this, other, pos, otherPos)
|
||||
}
|
||||
|
||||
/// Find the index and inner offset corresponding to a given relative
|
||||
/// position in this fragment. The result object will be reused
|
||||
/// (overwritten) the next time the function is called. (Not public.)
|
||||
findIndex(pos: number, round = -1): {index: number, offset: number} {
|
||||
if (pos == 0) return retIndex(0, pos)
|
||||
if (pos == this.size) return retIndex(this.content.length, pos)
|
||||
if (pos > this.size || pos < 0) throw new RangeError(`Position ${pos} outside of fragment (${this})`)
|
||||
for (let i = 0, curPos = 0;; i++) {
|
||||
let cur = this.child(i), end = curPos + cur.nodeSize
|
||||
if (end >= pos) {
|
||||
if (end == pos || round > 0) return retIndex(i + 1, end)
|
||||
return retIndex(i, curPos)
|
||||
}
|
||||
curPos = end
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a debugging string that describes this fragment.
|
||||
toString(): string { return "<" + this.toStringInner() + ">" }
|
||||
|
||||
/// @internal
|
||||
toStringInner() { return this.content.join(", ") }
|
||||
|
||||
/// Create a JSON-serializeable representation of this fragment.
|
||||
toJSON(): any {
|
||||
return this.content.length ? this.content.map(n => n.toJSON()) : null
|
||||
}
|
||||
|
||||
/// Deserialize a fragment from its JSON representation.
|
||||
static fromJSON(schema: Schema, value: any) {
|
||||
if (!value) return Fragment.empty
|
||||
if (!Array.isArray(value)) throw new RangeError("Invalid input for Fragment.fromJSON")
|
||||
return new Fragment(value.map(schema.nodeFromJSON))
|
||||
}
|
||||
|
||||
/// Build a fragment from an array of nodes. Ensures that adjacent
|
||||
/// text nodes with the same marks are joined together.
|
||||
static fromArray(array: readonly Node[]) {
|
||||
if (!array.length) return Fragment.empty
|
||||
let joined: Node[] | undefined, size = 0
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let node = array[i]
|
||||
size += node.nodeSize
|
||||
if (i && node.isText && array[i - 1].sameMarkup(node)) {
|
||||
if (!joined) joined = array.slice(0, i)
|
||||
joined[joined.length - 1] = (node as TextNode)
|
||||
.withText((joined[joined.length - 1] as TextNode).text + (node as TextNode).text)
|
||||
} else if (joined) {
|
||||
joined.push(node)
|
||||
}
|
||||
}
|
||||
return new Fragment(joined || array, size)
|
||||
}
|
||||
|
||||
/// Create a fragment from something that can be interpreted as a
|
||||
/// set of nodes. For `null`, it returns the empty fragment. For a
|
||||
/// fragment, the fragment itself. For a node or array of nodes, a
|
||||
/// fragment containing those nodes.
|
||||
static from(nodes?: Fragment | Node | readonly Node[] | null) {
|
||||
if (!nodes) return Fragment.empty
|
||||
if (nodes instanceof Fragment) return nodes
|
||||
if (Array.isArray(nodes)) return this.fromArray(nodes)
|
||||
if ((nodes as Node).attrs) return new Fragment([nodes as Node], (nodes as Node).nodeSize)
|
||||
throw new RangeError("Can not convert " + nodes + " to a Fragment" +
|
||||
((nodes as any).nodesBetween ? " (looks like multiple versions of prosemirror-model were loaded)" : ""))
|
||||
}
|
||||
|
||||
/// An empty fragment. Intended to be reused whenever a node doesn't
|
||||
/// contain anything (rather than allocating a new empty fragment for
|
||||
/// each leaf node).
|
||||
static empty: Fragment = new Fragment([], 0)
|
||||
}
|
||||
|
||||
const found = {index: 0, offset: 0}
|
||||
function retIndex(index: number, offset: number) {
|
||||
found.index = index
|
||||
found.offset = offset
|
||||
return found
|
||||
}
|
||||
871
resources/app/node_modules/prosemirror-model/src/from_dom.ts
generated
vendored
Normal file
871
resources/app/node_modules/prosemirror-model/src/from_dom.ts
generated
vendored
Normal file
@@ -0,0 +1,871 @@
|
||||
import {Fragment} from "./fragment"
|
||||
import {Slice} from "./replace"
|
||||
import {Mark} from "./mark"
|
||||
import {Node, TextNode} from "./node"
|
||||
import {ContentMatch} from "./content"
|
||||
import {ResolvedPos} from "./resolvedpos"
|
||||
import {Schema, Attrs, NodeType, MarkType} from "./schema"
|
||||
import {DOMNode} from "./dom"
|
||||
|
||||
/// These are the options recognized by the
|
||||
/// [`parse`](#model.DOMParser.parse) and
|
||||
/// [`parseSlice`](#model.DOMParser.parseSlice) methods.
|
||||
export interface ParseOptions {
|
||||
/// By default, whitespace is collapsed as per HTML's rules. Pass
|
||||
/// `true` to preserve whitespace, but normalize newlines to
|
||||
/// spaces, and `"full"` to preserve whitespace entirely.
|
||||
preserveWhitespace?: boolean | "full"
|
||||
|
||||
/// When given, the parser will, beside parsing the content,
|
||||
/// record the document positions of the given DOM positions. It
|
||||
/// will do so by writing to the objects, adding a `pos` property
|
||||
/// that holds the document position. DOM positions that are not
|
||||
/// in the parsed content will not be written to.
|
||||
findPositions?: {node: DOMNode, offset: number, pos?: number}[]
|
||||
|
||||
/// The child node index to start parsing from.
|
||||
from?: number
|
||||
|
||||
/// The child node index to stop parsing at.
|
||||
to?: number
|
||||
|
||||
/// By default, the content is parsed into the schema's default
|
||||
/// [top node type](#model.Schema.topNodeType). You can pass this
|
||||
/// option to use the type and attributes from a different node
|
||||
/// as the top container.
|
||||
topNode?: Node
|
||||
|
||||
/// Provide the starting content match that content parsed into the
|
||||
/// top node is matched against.
|
||||
topMatch?: ContentMatch
|
||||
|
||||
/// A set of additional nodes to count as
|
||||
/// [context](#model.ParseRule.context) when parsing, above the
|
||||
/// given [top node](#model.ParseOptions.topNode).
|
||||
context?: ResolvedPos
|
||||
|
||||
/// @internal
|
||||
ruleFromNode?: (node: DOMNode) => Omit<TagParseRule, "tag"> | null
|
||||
/// @internal
|
||||
topOpen?: boolean
|
||||
}
|
||||
|
||||
/// Fields that may be present in both [tag](#model.TagParseRule) and
|
||||
/// [style](#model.StyleParseRule) parse rules.
|
||||
export interface GenericParseRule {
|
||||
/// Can be used to change the order in which the parse rules in a
|
||||
/// schema are tried. Those with higher priority come first. Rules
|
||||
/// without a priority are counted as having priority 50. This
|
||||
/// property is only meaningful in a schema—when directly
|
||||
/// constructing a parser, the order of the rule array is used.
|
||||
priority?: number
|
||||
|
||||
/// By default, when a rule matches an element or style, no further
|
||||
/// rules get a chance to match it. By setting this to `false`, you
|
||||
/// indicate that even when this rule matches, other rules that come
|
||||
/// after it should also run.
|
||||
consuming?: boolean
|
||||
|
||||
/// When given, restricts this rule to only match when the current
|
||||
/// context—the parent nodes into which the content is being
|
||||
/// parsed—matches this expression. Should contain one or more node
|
||||
/// names or node group names followed by single or double slashes.
|
||||
/// For example `"paragraph/"` means the rule only matches when the
|
||||
/// parent node is a paragraph, `"blockquote/paragraph/"` restricts
|
||||
/// it to be in a paragraph that is inside a blockquote, and
|
||||
/// `"section//"` matches any position inside a section—a double
|
||||
/// slash matches any sequence of ancestor nodes. To allow multiple
|
||||
/// different contexts, they can be separated by a pipe (`|`)
|
||||
/// character, as in `"blockquote/|list_item/"`.
|
||||
context?: string
|
||||
|
||||
/// The name of the mark type to wrap the matched content in.
|
||||
mark?: string
|
||||
|
||||
/// When true, ignore content that matches this rule.
|
||||
ignore?: boolean
|
||||
|
||||
/// When true, finding an element that matches this rule will close
|
||||
/// the current node.
|
||||
closeParent?: boolean
|
||||
|
||||
/// When true, ignore the node that matches this rule, but do parse
|
||||
/// its content.
|
||||
skip?: boolean
|
||||
|
||||
/// Attributes for the node or mark created by this rule. When
|
||||
/// `getAttrs` is provided, it takes precedence.
|
||||
attrs?: Attrs
|
||||
}
|
||||
|
||||
/// Parse rule targeting a DOM element.
|
||||
export interface TagParseRule extends GenericParseRule {
|
||||
/// A CSS selector describing the kind of DOM elements to match.
|
||||
tag: string
|
||||
|
||||
/// The namespace to match. Nodes are only matched when the
|
||||
/// namespace matches or this property is null.
|
||||
namespace?: string
|
||||
|
||||
/// The name of the node type to create when this rule matches. Each
|
||||
/// rule should have either a `node`, `mark`, or `ignore` property
|
||||
/// (except when it appears in a [node](#model.NodeSpec.parseDOM) or
|
||||
/// [mark spec](#model.MarkSpec.parseDOM), in which case the `node`
|
||||
/// or `mark` property will be derived from its position).
|
||||
node?: string
|
||||
|
||||
/// A function used to compute the attributes for the node or mark
|
||||
/// created by this rule. Can also be used to describe further
|
||||
/// conditions the DOM element or style must match. When it returns
|
||||
/// `false`, the rule won't match. When it returns null or undefined,
|
||||
/// that is interpreted as an empty/default set of attributes.
|
||||
getAttrs?: (node: HTMLElement) => Attrs | false | null
|
||||
|
||||
/// For rules that produce non-leaf nodes, by default the content of
|
||||
/// the DOM element is parsed as content of the node. If the child
|
||||
/// nodes are in a descendent node, this may be a CSS selector
|
||||
/// string that the parser must use to find the actual content
|
||||
/// element, or a function that returns the actual content element
|
||||
/// to the parser.
|
||||
contentElement?: string | HTMLElement | ((node: DOMNode) => HTMLElement)
|
||||
|
||||
/// Can be used to override the content of a matched node. When
|
||||
/// present, instead of parsing the node's child nodes, the result of
|
||||
/// this function is used.
|
||||
getContent?: (node: DOMNode, schema: Schema) => Fragment
|
||||
|
||||
/// Controls whether whitespace should be preserved when parsing the
|
||||
/// content inside the matched element. `false` means whitespace may
|
||||
/// be collapsed, `true` means that whitespace should be preserved
|
||||
/// but newlines normalized to spaces, and `"full"` means that
|
||||
/// newlines should also be preserved.
|
||||
preserveWhitespace?: boolean | "full"
|
||||
}
|
||||
|
||||
/// A parse rule targeting a style property.
|
||||
export interface StyleParseRule extends GenericParseRule {
|
||||
/// A CSS property name to match. This rule will match inline styles
|
||||
/// that list that property. May also have the form
|
||||
/// `"property=value"`, in which case the rule only matches if the
|
||||
/// property's value exactly matches the given value. (For more
|
||||
/// complicated filters, use [`getAttrs`](#model.ParseRule.getAttrs)
|
||||
/// and return false to indicate that the match failed.) Rules
|
||||
/// matching styles may only produce [marks](#model.ParseRule.mark),
|
||||
/// not nodes.
|
||||
style: string
|
||||
|
||||
/// Given to make TS see ParseRule as a tagged union @hide
|
||||
tag?: undefined
|
||||
|
||||
/// Style rules can remove marks from the set of active marks.
|
||||
clearMark?: (mark: Mark) => boolean
|
||||
|
||||
/// A function used to compute the attributes for the node or mark
|
||||
/// created by this rule. Called with the style's value.
|
||||
getAttrs?: (node: string) => Attrs | false | null
|
||||
}
|
||||
|
||||
/// A value that describes how to parse a given DOM node or inline
|
||||
/// style as a ProseMirror node or mark.
|
||||
export type ParseRule = TagParseRule | StyleParseRule
|
||||
|
||||
function isTagRule(rule: ParseRule): rule is TagParseRule { return (rule as TagParseRule).tag != null }
|
||||
function isStyleRule(rule: ParseRule): rule is StyleParseRule { return (rule as StyleParseRule).style != null }
|
||||
|
||||
/// A DOM parser represents a strategy for parsing DOM content into a
|
||||
/// ProseMirror document conforming to a given schema. Its behavior is
|
||||
/// defined by an array of [rules](#model.ParseRule).
|
||||
export class DOMParser {
|
||||
/// @internal
|
||||
tags: TagParseRule[] = []
|
||||
/// @internal
|
||||
styles: StyleParseRule[] = []
|
||||
/// @internal
|
||||
normalizeLists: boolean
|
||||
|
||||
/// Create a parser that targets the given schema, using the given
|
||||
/// parsing rules.
|
||||
constructor(
|
||||
/// The schema into which the parser parses.
|
||||
readonly schema: Schema,
|
||||
/// The set of [parse rules](#model.ParseRule) that the parser
|
||||
/// uses, in order of precedence.
|
||||
readonly rules: readonly ParseRule[]
|
||||
) {
|
||||
rules.forEach(rule => {
|
||||
if (isTagRule(rule)) this.tags.push(rule)
|
||||
else if (isStyleRule(rule)) this.styles.push(rule)
|
||||
})
|
||||
|
||||
// Only normalize list elements when lists in the schema can't directly contain themselves
|
||||
this.normalizeLists = !this.tags.some(r => {
|
||||
if (!/^(ul|ol)\b/.test(r.tag!) || !r.node) return false
|
||||
let node = schema.nodes[r.node]
|
||||
return node.contentMatch.matchType(node)
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a document from the content of a DOM node.
|
||||
parse(dom: DOMNode, options: ParseOptions = {}): Node {
|
||||
let context = new ParseContext(this, options, false)
|
||||
context.addAll(dom, options.from, options.to)
|
||||
return context.finish() as Node
|
||||
}
|
||||
|
||||
/// Parses the content of the given DOM node, like
|
||||
/// [`parse`](#model.DOMParser.parse), and takes the same set of
|
||||
/// options. But unlike that method, which produces a whole node,
|
||||
/// this one returns a slice that is open at the sides, meaning that
|
||||
/// the schema constraints aren't applied to the start of nodes to
|
||||
/// the left of the input and the end of nodes at the end.
|
||||
parseSlice(dom: DOMNode, options: ParseOptions = {}) {
|
||||
let context = new ParseContext(this, options, true)
|
||||
context.addAll(dom, options.from, options.to)
|
||||
return Slice.maxOpen(context.finish() as Fragment)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
matchTag(dom: DOMNode, context: ParseContext, after?: TagParseRule) {
|
||||
for (let i = after ? this.tags.indexOf(after) + 1 : 0; i < this.tags.length; i++) {
|
||||
let rule = this.tags[i]
|
||||
if (matches(dom, rule.tag!) &&
|
||||
(rule.namespace === undefined || (dom as HTMLElement).namespaceURI == rule.namespace) &&
|
||||
(!rule.context || context.matchesContext(rule.context))) {
|
||||
if (rule.getAttrs) {
|
||||
let result = rule.getAttrs(dom as HTMLElement)
|
||||
if (result === false) continue
|
||||
rule.attrs = result || undefined
|
||||
}
|
||||
return rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @internal
|
||||
matchStyle(prop: string, value: string, context: ParseContext, after?: StyleParseRule) {
|
||||
for (let i = after ? this.styles.indexOf(after) + 1 : 0; i < this.styles.length; i++) {
|
||||
let rule = this.styles[i], style = rule.style!
|
||||
if (style.indexOf(prop) != 0 ||
|
||||
rule.context && !context.matchesContext(rule.context) ||
|
||||
// Test that the style string either precisely matches the prop,
|
||||
// or has an '=' sign after the prop, followed by the given
|
||||
// value.
|
||||
style.length > prop.length &&
|
||||
(style.charCodeAt(prop.length) != 61 || style.slice(prop.length + 1) != value))
|
||||
continue
|
||||
if (rule.getAttrs) {
|
||||
let result = rule.getAttrs(value)
|
||||
if (result === false) continue
|
||||
rule.attrs = result || undefined
|
||||
}
|
||||
return rule
|
||||
}
|
||||
}
|
||||
|
||||
/// @internal
|
||||
static schemaRules(schema: Schema) {
|
||||
let result: ParseRule[] = []
|
||||
function insert(rule: ParseRule) {
|
||||
let priority = rule.priority == null ? 50 : rule.priority, i = 0
|
||||
for (; i < result.length; i++) {
|
||||
let next = result[i], nextPriority = next.priority == null ? 50 : next.priority
|
||||
if (nextPriority < priority) break
|
||||
}
|
||||
result.splice(i, 0, rule)
|
||||
}
|
||||
|
||||
for (let name in schema.marks) {
|
||||
let rules = schema.marks[name].spec.parseDOM
|
||||
if (rules) rules.forEach(rule => {
|
||||
insert(rule = copy(rule) as ParseRule)
|
||||
if (!(rule.mark || rule.ignore || (rule as StyleParseRule).clearMark))
|
||||
rule.mark = name
|
||||
})
|
||||
}
|
||||
for (let name in schema.nodes) {
|
||||
let rules = schema.nodes[name].spec.parseDOM
|
||||
if (rules) rules.forEach(rule => {
|
||||
insert(rule = copy(rule) as TagParseRule)
|
||||
if (!((rule as TagParseRule).node || rule.ignore || rule.mark))
|
||||
rule.node = name
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Construct a DOM parser using the parsing rules listed in a
|
||||
/// schema's [node specs](#model.NodeSpec.parseDOM), reordered by
|
||||
/// [priority](#model.ParseRule.priority).
|
||||
static fromSchema(schema: Schema) {
|
||||
return schema.cached.domParser as DOMParser ||
|
||||
(schema.cached.domParser = new DOMParser(schema, DOMParser.schemaRules(schema)))
|
||||
}
|
||||
}
|
||||
|
||||
const blockTags: {[tagName: string]: boolean} = {
|
||||
address: true, article: true, aside: true, blockquote: true, canvas: true,
|
||||
dd: true, div: true, dl: true, fieldset: true, figcaption: true, figure: true,
|
||||
footer: true, form: true, h1: true, h2: true, h3: true, h4: true, h5: true,
|
||||
h6: true, header: true, hgroup: true, hr: true, li: true, noscript: true, ol: true,
|
||||
output: true, p: true, pre: true, section: true, table: true, tfoot: true, ul: true
|
||||
}
|
||||
|
||||
const ignoreTags: {[tagName: string]: boolean} = {
|
||||
head: true, noscript: true, object: true, script: true, style: true, title: true
|
||||
}
|
||||
|
||||
const listTags: {[tagName: string]: boolean} = {ol: true, ul: true}
|
||||
|
||||
// Using a bitfield for node context options
|
||||
const OPT_PRESERVE_WS = 1, OPT_PRESERVE_WS_FULL = 2, OPT_OPEN_LEFT = 4
|
||||
|
||||
function wsOptionsFor(type: NodeType | null, preserveWhitespace: boolean | "full" | undefined, base: number) {
|
||||
if (preserveWhitespace != null) return (preserveWhitespace ? OPT_PRESERVE_WS : 0) |
|
||||
(preserveWhitespace === "full" ? OPT_PRESERVE_WS_FULL : 0)
|
||||
return type && type.whitespace == "pre" ? OPT_PRESERVE_WS | OPT_PRESERVE_WS_FULL : base & ~OPT_OPEN_LEFT
|
||||
}
|
||||
|
||||
class NodeContext {
|
||||
match: ContentMatch | null
|
||||
content: Node[] = []
|
||||
|
||||
// Marks applied to the node's children
|
||||
activeMarks: readonly Mark[] = Mark.none
|
||||
// Nested Marks with same type
|
||||
stashMarks: Mark[] = []
|
||||
|
||||
constructor(
|
||||
readonly type: NodeType | null,
|
||||
readonly attrs: Attrs | null,
|
||||
// Marks applied to this node itself
|
||||
readonly marks: readonly Mark[],
|
||||
// Marks that can't apply here, but will be used in children if possible
|
||||
public pendingMarks: readonly Mark[],
|
||||
readonly solid: boolean,
|
||||
match: ContentMatch | null,
|
||||
readonly options: number
|
||||
) {
|
||||
this.match = match || (options & OPT_OPEN_LEFT ? null : type!.contentMatch)
|
||||
}
|
||||
|
||||
findWrapping(node: Node) {
|
||||
if (!this.match) {
|
||||
if (!this.type) return []
|
||||
let fill = this.type.contentMatch.fillBefore(Fragment.from(node))
|
||||
if (fill) {
|
||||
this.match = this.type.contentMatch.matchFragment(fill)!
|
||||
} else {
|
||||
let start = this.type.contentMatch, wrap
|
||||
if (wrap = start.findWrapping(node.type)) {
|
||||
this.match = start
|
||||
return wrap
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.match.findWrapping(node.type)
|
||||
}
|
||||
|
||||
finish(openEnd?: boolean): Node | Fragment {
|
||||
if (!(this.options & OPT_PRESERVE_WS)) { // Strip trailing whitespace
|
||||
let last = this.content[this.content.length - 1], m
|
||||
if (last && last.isText && (m = /[ \t\r\n\u000c]+$/.exec(last.text!))) {
|
||||
let text = last as TextNode
|
||||
if (last.text!.length == m[0].length) this.content.pop()
|
||||
else this.content[this.content.length - 1] = text.withText(text.text.slice(0, text.text.length - m[0].length))
|
||||
}
|
||||
}
|
||||
let content = Fragment.from(this.content)
|
||||
if (!openEnd && this.match)
|
||||
content = content.append(this.match.fillBefore(Fragment.empty, true)!)
|
||||
return this.type ? this.type.create(this.attrs, content, this.marks) : content
|
||||
}
|
||||
|
||||
popFromStashMark(mark: Mark) {
|
||||
for (let i = this.stashMarks.length - 1; i >= 0; i--)
|
||||
if (mark.eq(this.stashMarks[i])) return this.stashMarks.splice(i, 1)[0]
|
||||
}
|
||||
|
||||
applyPending(nextType: NodeType) {
|
||||
for (let i = 0, pending = this.pendingMarks; i < pending.length; i++) {
|
||||
let mark = pending[i]
|
||||
if ((this.type ? this.type.allowsMarkType(mark.type) : markMayApply(mark.type, nextType)) &&
|
||||
!mark.isInSet(this.activeMarks)) {
|
||||
this.activeMarks = mark.addToSet(this.activeMarks)
|
||||
this.pendingMarks = mark.removeFromSet(this.pendingMarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inlineContext(node: DOMNode) {
|
||||
if (this.type) return this.type.inlineContent
|
||||
if (this.content.length) return this.content[0].isInline
|
||||
return node.parentNode && !blockTags.hasOwnProperty(node.parentNode.nodeName.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
class ParseContext {
|
||||
open: number = 0
|
||||
find: {node: DOMNode, offset: number, pos?: number}[] | undefined
|
||||
needsBlock: boolean
|
||||
nodes: NodeContext[]
|
||||
|
||||
constructor(
|
||||
// The parser we are using.
|
||||
readonly parser: DOMParser,
|
||||
// The options passed to this parse.
|
||||
readonly options: ParseOptions,
|
||||
readonly isOpen: boolean
|
||||
) {
|
||||
let topNode = options.topNode, topContext: NodeContext
|
||||
let topOptions = wsOptionsFor(null, options.preserveWhitespace, 0) | (isOpen ? OPT_OPEN_LEFT : 0)
|
||||
if (topNode)
|
||||
topContext = new NodeContext(topNode.type, topNode.attrs, Mark.none, Mark.none, true,
|
||||
options.topMatch || topNode.type.contentMatch, topOptions)
|
||||
else if (isOpen)
|
||||
topContext = new NodeContext(null, null, Mark.none, Mark.none, true, null, topOptions)
|
||||
else
|
||||
topContext = new NodeContext(parser.schema.topNodeType, null, Mark.none, Mark.none, true, null, topOptions)
|
||||
this.nodes = [topContext]
|
||||
this.find = options.findPositions
|
||||
this.needsBlock = false
|
||||
}
|
||||
|
||||
get top() {
|
||||
return this.nodes[this.open]
|
||||
}
|
||||
|
||||
// Add a DOM node to the content. Text is inserted as text node,
|
||||
// otherwise, the node is passed to `addElement` or, if it has a
|
||||
// `style` attribute, `addElementWithStyles`.
|
||||
addDOM(dom: DOMNode) {
|
||||
if (dom.nodeType == 3) this.addTextNode(dom as Text)
|
||||
else if (dom.nodeType == 1) this.addElement(dom as HTMLElement)
|
||||
}
|
||||
|
||||
withStyleRules(dom: HTMLElement, f: () => void) {
|
||||
let style = dom.getAttribute("style")
|
||||
if (!style) return f()
|
||||
let marks = this.readStyles(parseStyles(style))
|
||||
if (!marks) return // A style with ignore: true
|
||||
let [addMarks, removeMarks] = marks, top = this.top
|
||||
for (let i = 0; i < removeMarks.length; i++) this.removePendingMark(removeMarks[i], top)
|
||||
for (let i = 0; i < addMarks.length; i++) this.addPendingMark(addMarks[i])
|
||||
f()
|
||||
for (let i = 0; i < addMarks.length; i++) this.removePendingMark(addMarks[i], top)
|
||||
for (let i = 0; i < removeMarks.length; i++) this.addPendingMark(removeMarks[i])
|
||||
}
|
||||
|
||||
addTextNode(dom: Text) {
|
||||
let value = dom.nodeValue!
|
||||
let top = this.top
|
||||
if (top.options & OPT_PRESERVE_WS_FULL ||
|
||||
top.inlineContext(dom) ||
|
||||
/[^ \t\r\n\u000c]/.test(value)) {
|
||||
if (!(top.options & OPT_PRESERVE_WS)) {
|
||||
value = value.replace(/[ \t\r\n\u000c]+/g, " ")
|
||||
// If this starts with whitespace, and there is no node before it, or
|
||||
// a hard break, or a text node that ends with whitespace, strip the
|
||||
// leading space.
|
||||
if (/^[ \t\r\n\u000c]/.test(value) && this.open == this.nodes.length - 1) {
|
||||
let nodeBefore = top.content[top.content.length - 1]
|
||||
let domNodeBefore = dom.previousSibling
|
||||
if (!nodeBefore ||
|
||||
(domNodeBefore && domNodeBefore.nodeName == 'BR') ||
|
||||
(nodeBefore.isText && /[ \t\r\n\u000c]$/.test(nodeBefore.text!)))
|
||||
value = value.slice(1)
|
||||
}
|
||||
} else if (!(top.options & OPT_PRESERVE_WS_FULL)) {
|
||||
value = value.replace(/\r?\n|\r/g, " ")
|
||||
} else {
|
||||
value = value.replace(/\r\n?/g, "\n")
|
||||
}
|
||||
if (value) this.insertNode(this.parser.schema.text(value))
|
||||
this.findInText(dom)
|
||||
} else {
|
||||
this.findInside(dom)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find a handler for the given tag and use that to parse. If
|
||||
// none is found, the element's content nodes are added directly.
|
||||
addElement(dom: HTMLElement, matchAfter?: TagParseRule) {
|
||||
let name = dom.nodeName.toLowerCase(), ruleID: TagParseRule | undefined
|
||||
if (listTags.hasOwnProperty(name) && this.parser.normalizeLists) normalizeList(dom)
|
||||
let rule = (this.options.ruleFromNode && this.options.ruleFromNode(dom)) ||
|
||||
(ruleID = this.parser.matchTag(dom, this, matchAfter))
|
||||
if (rule ? rule.ignore : ignoreTags.hasOwnProperty(name)) {
|
||||
this.findInside(dom)
|
||||
this.ignoreFallback(dom)
|
||||
} else if (!rule || rule.skip || rule.closeParent) {
|
||||
if (rule && rule.closeParent) this.open = Math.max(0, this.open - 1)
|
||||
else if (rule && (rule.skip as any).nodeType) dom = rule.skip as any as HTMLElement
|
||||
let sync, top = this.top, oldNeedsBlock = this.needsBlock
|
||||
if (blockTags.hasOwnProperty(name)) {
|
||||
if (top.content.length && top.content[0].isInline && this.open) {
|
||||
this.open--
|
||||
top = this.top
|
||||
}
|
||||
sync = true
|
||||
if (!top.type) this.needsBlock = true
|
||||
} else if (!dom.firstChild) {
|
||||
this.leafFallback(dom)
|
||||
return
|
||||
}
|
||||
if (rule && rule.skip) this.addAll(dom)
|
||||
else this.withStyleRules(dom, () => this.addAll(dom))
|
||||
if (sync) this.sync(top)
|
||||
this.needsBlock = oldNeedsBlock
|
||||
} else {
|
||||
this.withStyleRules(dom, () => {
|
||||
this.addElementByRule(dom, rule as TagParseRule, rule!.consuming === false ? ruleID : undefined)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Called for leaf DOM nodes that would otherwise be ignored
|
||||
leafFallback(dom: DOMNode) {
|
||||
if (dom.nodeName == "BR" && this.top.type && this.top.type.inlineContent)
|
||||
this.addTextNode(dom.ownerDocument!.createTextNode("\n"))
|
||||
}
|
||||
|
||||
// Called for ignored nodes
|
||||
ignoreFallback(dom: DOMNode) {
|
||||
// Ignored BR nodes should at least create an inline context
|
||||
if (dom.nodeName == "BR" && (!this.top.type || !this.top.type.inlineContent))
|
||||
this.findPlace(this.parser.schema.text("-"))
|
||||
}
|
||||
|
||||
// Run any style parser associated with the node's styles. Either
|
||||
// return an array of marks, or null to indicate some of the styles
|
||||
// had a rule with `ignore` set.
|
||||
readStyles(styles: readonly string[]) {
|
||||
let add = Mark.none, remove = Mark.none
|
||||
for (let i = 0; i < styles.length; i += 2) {
|
||||
for (let after = undefined;;) {
|
||||
let rule = this.parser.matchStyle(styles[i], styles[i + 1], this, after)
|
||||
if (!rule) break
|
||||
if (rule.ignore) return null
|
||||
if (rule.clearMark) {
|
||||
this.top.pendingMarks.concat(this.top.activeMarks).forEach(m => {
|
||||
if (rule!.clearMark!(m)) remove = m.addToSet(remove)
|
||||
})
|
||||
} else {
|
||||
add = this.parser.schema.marks[rule.mark!].create(rule.attrs).addToSet(add)
|
||||
}
|
||||
if (rule.consuming === false) after = rule
|
||||
else break
|
||||
}
|
||||
}
|
||||
return [add, remove]
|
||||
}
|
||||
|
||||
// Look up a handler for the given node. If none are found, return
|
||||
// false. Otherwise, apply it, use its return value to drive the way
|
||||
// the node's content is wrapped, and return true.
|
||||
addElementByRule(dom: HTMLElement, rule: TagParseRule, continueAfter?: TagParseRule) {
|
||||
let sync, nodeType, mark
|
||||
if (rule.node) {
|
||||
nodeType = this.parser.schema.nodes[rule.node]
|
||||
if (!nodeType.isLeaf) {
|
||||
sync = this.enter(nodeType, rule.attrs || null, rule.preserveWhitespace)
|
||||
} else if (!this.insertNode(nodeType.create(rule.attrs))) {
|
||||
this.leafFallback(dom)
|
||||
}
|
||||
} else {
|
||||
let markType = this.parser.schema.marks[rule.mark!]
|
||||
mark = markType.create(rule.attrs)
|
||||
this.addPendingMark(mark)
|
||||
}
|
||||
let startIn = this.top
|
||||
|
||||
if (nodeType && nodeType.isLeaf) {
|
||||
this.findInside(dom)
|
||||
} else if (continueAfter) {
|
||||
this.addElement(dom, continueAfter)
|
||||
} else if (rule.getContent) {
|
||||
this.findInside(dom)
|
||||
rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node))
|
||||
} else {
|
||||
let contentDOM = dom
|
||||
if (typeof rule.contentElement == "string") contentDOM = dom.querySelector(rule.contentElement)!
|
||||
else if (typeof rule.contentElement == "function") contentDOM = rule.contentElement(dom)
|
||||
else if (rule.contentElement) contentDOM = rule.contentElement
|
||||
this.findAround(dom, contentDOM, true)
|
||||
this.addAll(contentDOM)
|
||||
}
|
||||
if (sync && this.sync(startIn)) this.open--
|
||||
if (mark) this.removePendingMark(mark, startIn)
|
||||
}
|
||||
|
||||
// Add all child nodes between `startIndex` and `endIndex` (or the
|
||||
// whole node, if not given). If `sync` is passed, use it to
|
||||
// synchronize after every block element.
|
||||
addAll(parent: DOMNode, startIndex?: number, endIndex?: number) {
|
||||
let index = startIndex || 0
|
||||
for (let dom = startIndex ? parent.childNodes[startIndex] : parent.firstChild,
|
||||
end = endIndex == null ? null : parent.childNodes[endIndex];
|
||||
dom != end; dom = dom!.nextSibling, ++index) {
|
||||
this.findAtPoint(parent, index)
|
||||
this.addDOM(dom!)
|
||||
}
|
||||
this.findAtPoint(parent, index)
|
||||
}
|
||||
|
||||
// Try to find a way to fit the given node type into the current
|
||||
// context. May add intermediate wrappers and/or leave non-solid
|
||||
// nodes that we're in.
|
||||
findPlace(node: Node) {
|
||||
let route, sync: NodeContext | undefined
|
||||
for (let depth = this.open; depth >= 0; depth--) {
|
||||
let cx = this.nodes[depth]
|
||||
let found = cx.findWrapping(node)
|
||||
if (found && (!route || route.length > found.length)) {
|
||||
route = found
|
||||
sync = cx
|
||||
if (!found.length) break
|
||||
}
|
||||
if (cx.solid) break
|
||||
}
|
||||
if (!route) return false
|
||||
this.sync(sync!)
|
||||
for (let i = 0; i < route.length; i++)
|
||||
this.enterInner(route[i], null, false)
|
||||
return true
|
||||
}
|
||||
|
||||
// Try to insert the given node, adjusting the context when needed.
|
||||
insertNode(node: Node) {
|
||||
if (node.isInline && this.needsBlock && !this.top.type) {
|
||||
let block = this.textblockFromContext()
|
||||
if (block) this.enterInner(block)
|
||||
}
|
||||
if (this.findPlace(node)) {
|
||||
this.closeExtra()
|
||||
let top = this.top
|
||||
top.applyPending(node.type)
|
||||
if (top.match) top.match = top.match.matchType(node.type)
|
||||
let marks = top.activeMarks
|
||||
for (let i = 0; i < node.marks.length; i++)
|
||||
if (!top.type || top.type.allowsMarkType(node.marks[i].type))
|
||||
marks = node.marks[i].addToSet(marks)
|
||||
top.content.push(node.mark(marks))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to start a node of the given type, adjusting the context when
|
||||
// necessary.
|
||||
enter(type: NodeType, attrs: Attrs | null, preserveWS?: boolean | "full") {
|
||||
let ok = this.findPlace(type.create(attrs))
|
||||
if (ok) this.enterInner(type, attrs, true, preserveWS)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Open a node of the given type
|
||||
enterInner(type: NodeType, attrs: Attrs | null = null, solid: boolean = false, preserveWS?: boolean | "full") {
|
||||
this.closeExtra()
|
||||
let top = this.top
|
||||
top.applyPending(type)
|
||||
top.match = top.match && top.match.matchType(type)
|
||||
let options = wsOptionsFor(type, preserveWS, top.options)
|
||||
if ((top.options & OPT_OPEN_LEFT) && top.content.length == 0) options |= OPT_OPEN_LEFT
|
||||
this.nodes.push(new NodeContext(type, attrs, top.activeMarks, top.pendingMarks, solid, null, options))
|
||||
this.open++
|
||||
}
|
||||
|
||||
// Make sure all nodes above this.open are finished and added to
|
||||
// their parents
|
||||
closeExtra(openEnd = false) {
|
||||
let i = this.nodes.length - 1
|
||||
if (i > this.open) {
|
||||
for (; i > this.open; i--) this.nodes[i - 1].content.push(this.nodes[i].finish(openEnd) as Node)
|
||||
this.nodes.length = this.open + 1
|
||||
}
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.open = 0
|
||||
this.closeExtra(this.isOpen)
|
||||
return this.nodes[0].finish(this.isOpen || this.options.topOpen)
|
||||
}
|
||||
|
||||
sync(to: NodeContext) {
|
||||
for (let i = this.open; i >= 0; i--) if (this.nodes[i] == to) {
|
||||
this.open = i
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
get currentPos() {
|
||||
this.closeExtra()
|
||||
let pos = 0
|
||||
for (let i = this.open; i >= 0; i--) {
|
||||
let content = this.nodes[i].content
|
||||
for (let j = content.length - 1; j >= 0; j--)
|
||||
pos += content[j].nodeSize
|
||||
if (i) pos++
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
findAtPoint(parent: DOMNode, offset: number) {
|
||||
if (this.find) for (let i = 0; i < this.find.length; i++) {
|
||||
if (this.find[i].node == parent && this.find[i].offset == offset)
|
||||
this.find[i].pos = this.currentPos
|
||||
}
|
||||
}
|
||||
|
||||
findInside(parent: DOMNode) {
|
||||
if (this.find) for (let i = 0; i < this.find.length; i++) {
|
||||
if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node))
|
||||
this.find[i].pos = this.currentPos
|
||||
}
|
||||
}
|
||||
|
||||
findAround(parent: DOMNode, content: DOMNode, before: boolean) {
|
||||
if (parent != content && this.find) for (let i = 0; i < this.find.length; i++) {
|
||||
if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node)) {
|
||||
let pos = content.compareDocumentPosition(this.find[i].node)
|
||||
if (pos & (before ? 2 : 4))
|
||||
this.find[i].pos = this.currentPos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findInText(textNode: Text) {
|
||||
if (this.find) for (let i = 0; i < this.find.length; i++) {
|
||||
if (this.find[i].node == textNode)
|
||||
this.find[i].pos = this.currentPos - (textNode.nodeValue!.length - this.find[i].offset)
|
||||
}
|
||||
}
|
||||
|
||||
// Determines whether the given context string matches this context.
|
||||
matchesContext(context: string) {
|
||||
if (context.indexOf("|") > -1)
|
||||
return context.split(/\s*\|\s*/).some(this.matchesContext, this)
|
||||
|
||||
let parts = context.split("/")
|
||||
let option = this.options.context
|
||||
let useRoot = !this.isOpen && (!option || option.parent.type == this.nodes[0].type)
|
||||
let minDepth = -(option ? option.depth + 1 : 0) + (useRoot ? 0 : 1)
|
||||
let match = (i: number, depth: number) => {
|
||||
for (; i >= 0; i--) {
|
||||
let part = parts[i]
|
||||
if (part == "") {
|
||||
if (i == parts.length - 1 || i == 0) continue
|
||||
for (; depth >= minDepth; depth--)
|
||||
if (match(i - 1, depth)) return true
|
||||
return false
|
||||
} else {
|
||||
let next = depth > 0 || (depth == 0 && useRoot) ? this.nodes[depth].type
|
||||
: option && depth >= minDepth ? option.node(depth - minDepth).type
|
||||
: null
|
||||
if (!next || (next.name != part && next.groups.indexOf(part) == -1))
|
||||
return false
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return match(parts.length - 1, this.open)
|
||||
}
|
||||
|
||||
textblockFromContext() {
|
||||
let $context = this.options.context
|
||||
if ($context) for (let d = $context.depth; d >= 0; d--) {
|
||||
let deflt = $context.node(d).contentMatchAt($context.indexAfter(d)).defaultType
|
||||
if (deflt && deflt.isTextblock && deflt.defaultAttrs) return deflt
|
||||
}
|
||||
for (let name in this.parser.schema.nodes) {
|
||||
let type = this.parser.schema.nodes[name]
|
||||
if (type.isTextblock && type.defaultAttrs) return type
|
||||
}
|
||||
}
|
||||
|
||||
addPendingMark(mark: Mark) {
|
||||
let found = findSameMarkInSet(mark, this.top.pendingMarks)
|
||||
if (found) this.top.stashMarks.push(found)
|
||||
this.top.pendingMarks = mark.addToSet(this.top.pendingMarks)
|
||||
}
|
||||
|
||||
removePendingMark(mark: Mark, upto: NodeContext) {
|
||||
for (let depth = this.open; depth >= 0; depth--) {
|
||||
let level = this.nodes[depth]
|
||||
let found = level.pendingMarks.lastIndexOf(mark)
|
||||
if (found > -1) {
|
||||
level.pendingMarks = mark.removeFromSet(level.pendingMarks)
|
||||
} else {
|
||||
level.activeMarks = mark.removeFromSet(level.activeMarks)
|
||||
let stashMark = level.popFromStashMark(mark)
|
||||
if (stashMark && level.type && level.type.allowsMarkType(stashMark.type))
|
||||
level.activeMarks = stashMark.addToSet(level.activeMarks)
|
||||
}
|
||||
if (level == upto) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kludge to work around directly nested list nodes produced by some
|
||||
// tools and allowed by browsers to mean that the nested list is
|
||||
// actually part of the list item above it.
|
||||
function normalizeList(dom: DOMNode) {
|
||||
for (let child = dom.firstChild, prevItem = null; child; child = child.nextSibling) {
|
||||
let name = child.nodeType == 1 ? child.nodeName.toLowerCase() : null
|
||||
if (name && listTags.hasOwnProperty(name) && prevItem) {
|
||||
prevItem.appendChild(child)
|
||||
child = prevItem
|
||||
} else if (name == "li") {
|
||||
prevItem = child
|
||||
} else if (name) {
|
||||
prevItem = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a CSS selector.
|
||||
function matches(dom: any, selector: string): boolean {
|
||||
return (dom.matches || dom.msMatchesSelector || dom.webkitMatchesSelector || dom.mozMatchesSelector).call(dom, selector)
|
||||
}
|
||||
|
||||
// Tokenize a style attribute into property/value pairs.
|
||||
function parseStyles(style: string): string[] {
|
||||
let re = /\s*([\w-]+)\s*:\s*([^;]+)/g, m, result = []
|
||||
while (m = re.exec(style)) result.push(m[1], m[2].trim())
|
||||
return result
|
||||
}
|
||||
|
||||
function copy(obj: {[prop: string]: any}) {
|
||||
let copy: {[prop: string]: any} = {}
|
||||
for (let prop in obj) copy[prop] = obj[prop]
|
||||
return copy
|
||||
}
|
||||
|
||||
// Used when finding a mark at the top level of a fragment parse.
|
||||
// Checks whether it would be reasonable to apply a given mark type to
|
||||
// a given node, by looking at the way the mark occurs in the schema.
|
||||
function markMayApply(markType: MarkType, nodeType: NodeType) {
|
||||
let nodes = nodeType.schema.nodes
|
||||
for (let name in nodes) {
|
||||
let parent = nodes[name]
|
||||
if (!parent.allowsMarkType(markType)) continue
|
||||
let seen: ContentMatch[] = [], scan = (match: ContentMatch) => {
|
||||
seen.push(match)
|
||||
for (let i = 0; i < match.edgeCount; i++) {
|
||||
let {type, next} = match.edge(i)
|
||||
if (type == nodeType) return true
|
||||
if (seen.indexOf(next) < 0 && scan(next)) return true
|
||||
}
|
||||
}
|
||||
if (scan(parent.contentMatch)) return true
|
||||
}
|
||||
}
|
||||
|
||||
function findSameMarkInSet(mark: Mark, set: readonly Mark[]) {
|
||||
for (let i = 0; i < set.length; i++) {
|
||||
if (mark.eq(set[i])) return set[i]
|
||||
}
|
||||
}
|
||||
11
resources/app/node_modules/prosemirror-model/src/index.ts
generated
vendored
Normal file
11
resources/app/node_modules/prosemirror-model/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export {Node} from "./node"
|
||||
export {ResolvedPos, NodeRange} from "./resolvedpos"
|
||||
export {Fragment} from "./fragment"
|
||||
export {Slice, ReplaceError} from "./replace"
|
||||
export {Mark} from "./mark"
|
||||
|
||||
export {Schema, NodeType, Attrs, MarkType, NodeSpec, MarkSpec, AttributeSpec, SchemaSpec} from "./schema"
|
||||
export {ContentMatch} from "./content"
|
||||
|
||||
export {DOMParser, GenericParseRule, TagParseRule, StyleParseRule, ParseRule, ParseOptions} from "./from_dom"
|
||||
export {DOMSerializer, DOMOutputSpec} from "./to_dom"
|
||||
109
resources/app/node_modules/prosemirror-model/src/mark.ts
generated
vendored
Normal file
109
resources/app/node_modules/prosemirror-model/src/mark.ts
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
import {compareDeep} from "./comparedeep"
|
||||
import {Attrs, MarkType, Schema} from "./schema"
|
||||
|
||||
/// A mark is a piece of information that can be attached to a node,
|
||||
/// such as it being emphasized, in code font, or a link. It has a
|
||||
/// type and optionally a set of attributes that provide further
|
||||
/// information (such as the target of the link). Marks are created
|
||||
/// through a `Schema`, which controls which types exist and which
|
||||
/// attributes they have.
|
||||
export class Mark {
|
||||
/// @internal
|
||||
constructor(
|
||||
/// The type of this mark.
|
||||
readonly type: MarkType,
|
||||
/// The attributes associated with this mark.
|
||||
readonly attrs: Attrs
|
||||
) {}
|
||||
|
||||
/// Given a set of marks, create a new set which contains this one as
|
||||
/// well, in the right position. If this mark is already in the set,
|
||||
/// the set itself is returned. If any marks that are set to be
|
||||
/// [exclusive](#model.MarkSpec.excludes) with this mark are present,
|
||||
/// those are replaced by this one.
|
||||
addToSet(set: readonly Mark[]): readonly Mark[] {
|
||||
let copy, placed = false
|
||||
for (let i = 0; i < set.length; i++) {
|
||||
let other = set[i]
|
||||
if (this.eq(other)) return set
|
||||
if (this.type.excludes(other.type)) {
|
||||
if (!copy) copy = set.slice(0, i)
|
||||
} else if (other.type.excludes(this.type)) {
|
||||
return set
|
||||
} else {
|
||||
if (!placed && other.type.rank > this.type.rank) {
|
||||
if (!copy) copy = set.slice(0, i)
|
||||
copy.push(this)
|
||||
placed = true
|
||||
}
|
||||
if (copy) copy.push(other)
|
||||
}
|
||||
}
|
||||
if (!copy) copy = set.slice()
|
||||
if (!placed) copy.push(this)
|
||||
return copy
|
||||
}
|
||||
|
||||
/// Remove this mark from the given set, returning a new set. If this
|
||||
/// mark is not in the set, the set itself is returned.
|
||||
removeFromSet(set: readonly Mark[]): readonly Mark[] {
|
||||
for (let i = 0; i < set.length; i++)
|
||||
if (this.eq(set[i]))
|
||||
return set.slice(0, i).concat(set.slice(i + 1))
|
||||
return set
|
||||
}
|
||||
|
||||
/// Test whether this mark is in the given set of marks.
|
||||
isInSet(set: readonly Mark[]) {
|
||||
for (let i = 0; i < set.length; i++)
|
||||
if (this.eq(set[i])) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/// Test whether this mark has the same type and attributes as
|
||||
/// another mark.
|
||||
eq(other: Mark) {
|
||||
return this == other ||
|
||||
(this.type == other.type && compareDeep(this.attrs, other.attrs))
|
||||
}
|
||||
|
||||
/// Convert this mark to a JSON-serializeable representation.
|
||||
toJSON(): any {
|
||||
let obj: any = {type: this.type.name}
|
||||
for (let _ in this.attrs) {
|
||||
obj.attrs = this.attrs
|
||||
break
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/// Deserialize a mark from JSON.
|
||||
static fromJSON(schema: Schema, json: any) {
|
||||
if (!json) throw new RangeError("Invalid input for Mark.fromJSON")
|
||||
let type = schema.marks[json.type]
|
||||
if (!type) throw new RangeError(`There is no mark type ${json.type} in this schema`)
|
||||
return type.create(json.attrs)
|
||||
}
|
||||
|
||||
/// Test whether two sets of marks are identical.
|
||||
static sameSet(a: readonly Mark[], b: readonly Mark[]) {
|
||||
if (a == b) return true
|
||||
if (a.length != b.length) return false
|
||||
for (let i = 0; i < a.length; i++)
|
||||
if (!a[i].eq(b[i])) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/// Create a properly sorted mark set from null, a single mark, or an
|
||||
/// unsorted array of marks.
|
||||
static setFrom(marks?: Mark | readonly Mark[] | null): readonly Mark[] {
|
||||
if (!marks || Array.isArray(marks) && marks.length == 0) return Mark.none
|
||||
if (marks instanceof Mark) return [marks]
|
||||
let copy = marks.slice()
|
||||
copy.sort((a, b) => a.type.rank - b.type.rank)
|
||||
return copy
|
||||
}
|
||||
|
||||
/// The empty set of marks.
|
||||
static none: readonly Mark[] = []
|
||||
}
|
||||
392
resources/app/node_modules/prosemirror-model/src/node.ts
generated
vendored
Normal file
392
resources/app/node_modules/prosemirror-model/src/node.ts
generated
vendored
Normal file
@@ -0,0 +1,392 @@
|
||||
import {Fragment} from "./fragment"
|
||||
import {Mark} from "./mark"
|
||||
import {Schema, NodeType, Attrs, MarkType} from "./schema"
|
||||
import {Slice, replace} from "./replace"
|
||||
import {ResolvedPos} from "./resolvedpos"
|
||||
import {compareDeep} from "./comparedeep"
|
||||
|
||||
const emptyAttrs: Attrs = Object.create(null)
|
||||
|
||||
/// This class represents a node in the tree that makes up a
|
||||
/// ProseMirror document. So a document is an instance of `Node`, with
|
||||
/// children that are also instances of `Node`.
|
||||
///
|
||||
/// Nodes are persistent data structures. Instead of changing them, you
|
||||
/// create new ones with the content you want. Old ones keep pointing
|
||||
/// at the old document shape. This is made cheaper by sharing
|
||||
/// structure between the old and new data as much as possible, which a
|
||||
/// tree shape like this (without back pointers) makes easy.
|
||||
///
|
||||
/// **Do not** directly mutate the properties of a `Node` object. See
|
||||
/// [the guide](/docs/guide/#doc) for more information.
|
||||
export class Node {
|
||||
/// @internal
|
||||
constructor(
|
||||
/// The type of node that this is.
|
||||
readonly type: NodeType,
|
||||
/// An object mapping attribute names to values. The kind of
|
||||
/// attributes allowed and required are
|
||||
/// [determined](#model.NodeSpec.attrs) by the node type.
|
||||
readonly attrs: Attrs,
|
||||
// A fragment holding the node's children.
|
||||
content?: Fragment | null,
|
||||
/// The marks (things like whether it is emphasized or part of a
|
||||
/// link) applied to this node.
|
||||
readonly marks = Mark.none
|
||||
) {
|
||||
this.content = content || Fragment.empty
|
||||
}
|
||||
|
||||
/// A container holding the node's children.
|
||||
readonly content: Fragment
|
||||
|
||||
/// For text nodes, this contains the node's text content.
|
||||
readonly text: string | undefined
|
||||
|
||||
/// The size of this node, as defined by the integer-based [indexing
|
||||
/// scheme](/docs/guide/#doc.indexing). For text nodes, this is the
|
||||
/// amount of characters. For other leaf nodes, it is one. For
|
||||
/// non-leaf nodes, it is the size of the content plus two (the
|
||||
/// start and end token).
|
||||
get nodeSize(): number { return this.isLeaf ? 1 : 2 + this.content.size }
|
||||
|
||||
/// The number of children that the node has.
|
||||
get childCount() { return this.content.childCount }
|
||||
|
||||
/// Get the child node at the given index. Raises an error when the
|
||||
/// index is out of range.
|
||||
child(index: number) { return this.content.child(index) }
|
||||
|
||||
/// Get the child node at the given index, if it exists.
|
||||
maybeChild(index: number) { return this.content.maybeChild(index) }
|
||||
|
||||
/// Call `f` for every child node, passing the node, its offset
|
||||
/// into this parent node, and its index.
|
||||
forEach(f: (node: Node, offset: number, index: number) => void) { this.content.forEach(f) }
|
||||
|
||||
/// Invoke a callback for all descendant nodes recursively between
|
||||
/// the given two positions that are relative to start of this
|
||||
/// node's content. The callback is invoked with the node, its
|
||||
/// position relative to the original node (method receiver),
|
||||
/// its parent node, and its child index. When the callback returns
|
||||
/// false for a given node, that node's children will not be
|
||||
/// recursed over. The last parameter can be used to specify a
|
||||
/// starting position to count from.
|
||||
nodesBetween(from: number, to: number,
|
||||
f: (node: Node, pos: number, parent: Node | null, index: number) => void | boolean,
|
||||
startPos = 0) {
|
||||
this.content.nodesBetween(from, to, f, startPos, this)
|
||||
}
|
||||
|
||||
/// Call the given callback for every descendant node. Doesn't
|
||||
/// descend into a node when the callback returns `false`.
|
||||
descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => void | boolean) {
|
||||
this.nodesBetween(0, this.content.size, f)
|
||||
}
|
||||
|
||||
/// Concatenates all the text nodes found in this fragment and its
|
||||
/// children.
|
||||
get textContent() {
|
||||
return (this.isLeaf && this.type.spec.leafText)
|
||||
? this.type.spec.leafText(this)
|
||||
: this.textBetween(0, this.content.size, "")
|
||||
}
|
||||
|
||||
/// Get all text between positions `from` and `to`. When
|
||||
/// `blockSeparator` is given, it will be inserted to separate text
|
||||
/// from different block nodes. If `leafText` is given, it'll be
|
||||
/// inserted for every non-text leaf node encountered, otherwise
|
||||
/// [`leafText`](#model.NodeSpec^leafText) will be used.
|
||||
textBetween(from: number, to: number, blockSeparator?: string | null,
|
||||
leafText?: null | string | ((leafNode: Node) => string)) {
|
||||
return this.content.textBetween(from, to, blockSeparator, leafText)
|
||||
}
|
||||
|
||||
/// Returns this node's first child, or `null` if there are no
|
||||
/// children.
|
||||
get firstChild(): Node | null { return this.content.firstChild }
|
||||
|
||||
/// Returns this node's last child, or `null` if there are no
|
||||
/// children.
|
||||
get lastChild(): Node | null { return this.content.lastChild }
|
||||
|
||||
/// Test whether two nodes represent the same piece of document.
|
||||
eq(other: Node) {
|
||||
return this == other || (this.sameMarkup(other) && this.content.eq(other.content))
|
||||
}
|
||||
|
||||
/// Compare the markup (type, attributes, and marks) of this node to
|
||||
/// those of another. Returns `true` if both have the same markup.
|
||||
sameMarkup(other: Node) {
|
||||
return this.hasMarkup(other.type, other.attrs, other.marks)
|
||||
}
|
||||
|
||||
/// Check whether this node's markup correspond to the given type,
|
||||
/// attributes, and marks.
|
||||
hasMarkup(type: NodeType, attrs?: Attrs | null, marks?: readonly Mark[]): boolean {
|
||||
return this.type == type &&
|
||||
compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
|
||||
Mark.sameSet(this.marks, marks || Mark.none)
|
||||
}
|
||||
|
||||
/// Create a new node with the same markup as this node, containing
|
||||
/// the given content (or empty, if no content is given).
|
||||
copy(content: Fragment | null = null): Node {
|
||||
if (content == this.content) return this
|
||||
return new Node(this.type, this.attrs, content, this.marks)
|
||||
}
|
||||
|
||||
/// Create a copy of this node, with the given set of marks instead
|
||||
/// of the node's own marks.
|
||||
mark(marks: readonly Mark[]): Node {
|
||||
return marks == this.marks ? this : new Node(this.type, this.attrs, this.content, marks)
|
||||
}
|
||||
|
||||
/// Create a copy of this node with only the content between the
|
||||
/// given positions. If `to` is not given, it defaults to the end of
|
||||
/// the node.
|
||||
cut(from: number, to: number = this.content.size): Node {
|
||||
if (from == 0 && to == this.content.size) return this
|
||||
return this.copy(this.content.cut(from, to))
|
||||
}
|
||||
|
||||
/// Cut out the part of the document between the given positions, and
|
||||
/// return it as a `Slice` object.
|
||||
slice(from: number, to: number = this.content.size, includeParents = false) {
|
||||
if (from == to) return Slice.empty
|
||||
|
||||
let $from = this.resolve(from), $to = this.resolve(to)
|
||||
let depth = includeParents ? 0 : $from.sharedDepth(to)
|
||||
let start = $from.start(depth), node = $from.node(depth)
|
||||
let content = node.content.cut($from.pos - start, $to.pos - start)
|
||||
return new Slice(content, $from.depth - depth, $to.depth - depth)
|
||||
}
|
||||
|
||||
/// Replace the part of the document between the given positions with
|
||||
/// the given slice. The slice must 'fit', meaning its open sides
|
||||
/// must be able to connect to the surrounding content, and its
|
||||
/// content nodes must be valid children for the node they are placed
|
||||
/// into. If any of this is violated, an error of type
|
||||
/// [`ReplaceError`](#model.ReplaceError) is thrown.
|
||||
replace(from: number, to: number, slice: Slice) {
|
||||
return replace(this.resolve(from), this.resolve(to), slice)
|
||||
}
|
||||
|
||||
/// Find the node directly after the given position.
|
||||
nodeAt(pos: number): Node | null {
|
||||
for (let node: Node | null = this;;) {
|
||||
let {index, offset} = node.content.findIndex(pos)
|
||||
node = node.maybeChild(index)
|
||||
if (!node) return null
|
||||
if (offset == pos || node.isText) return node
|
||||
pos -= offset + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the (direct) child node after the given offset, if any,
|
||||
/// and return it along with its index and offset relative to this
|
||||
/// node.
|
||||
childAfter(pos: number): {node: Node | null, index: number, offset: number} {
|
||||
let {index, offset} = this.content.findIndex(pos)
|
||||
return {node: this.content.maybeChild(index), index, offset}
|
||||
}
|
||||
|
||||
/// Find the (direct) child node before the given offset, if any,
|
||||
/// and return it along with its index and offset relative to this
|
||||
/// node.
|
||||
childBefore(pos: number): {node: Node | null, index: number, offset: number} {
|
||||
if (pos == 0) return {node: null, index: 0, offset: 0}
|
||||
let {index, offset} = this.content.findIndex(pos)
|
||||
if (offset < pos) return {node: this.content.child(index), index, offset}
|
||||
let node = this.content.child(index - 1)
|
||||
return {node, index: index - 1, offset: offset - node.nodeSize}
|
||||
}
|
||||
|
||||
/// Resolve the given position in the document, returning an
|
||||
/// [object](#model.ResolvedPos) with information about its context.
|
||||
resolve(pos: number) { return ResolvedPos.resolveCached(this, pos) }
|
||||
|
||||
/// @internal
|
||||
resolveNoCache(pos: number) { return ResolvedPos.resolve(this, pos) }
|
||||
|
||||
/// Test whether a given mark or mark type occurs in this document
|
||||
/// between the two given positions.
|
||||
rangeHasMark(from: number, to: number, type: Mark | MarkType): boolean {
|
||||
let found = false
|
||||
if (to > from) this.nodesBetween(from, to, node => {
|
||||
if (type.isInSet(node.marks)) found = true
|
||||
return !found
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
/// True when this is a block (non-inline node)
|
||||
get isBlock() { return this.type.isBlock }
|
||||
|
||||
/// True when this is a textblock node, a block node with inline
|
||||
/// content.
|
||||
get isTextblock() { return this.type.isTextblock }
|
||||
|
||||
/// True when this node allows inline content.
|
||||
get inlineContent() { return this.type.inlineContent }
|
||||
|
||||
/// True when this is an inline node (a text node or a node that can
|
||||
/// appear among text).
|
||||
get isInline() { return this.type.isInline }
|
||||
|
||||
/// True when this is a text node.
|
||||
get isText() { return this.type.isText }
|
||||
|
||||
/// True when this is a leaf node.
|
||||
get isLeaf() { return this.type.isLeaf }
|
||||
|
||||
/// True when this is an atom, i.e. when it does not have directly
|
||||
/// editable content. This is usually the same as `isLeaf`, but can
|
||||
/// be configured with the [`atom` property](#model.NodeSpec.atom)
|
||||
/// on a node's spec (typically used when the node is displayed as
|
||||
/// an uneditable [node view](#view.NodeView)).
|
||||
get isAtom() { return this.type.isAtom }
|
||||
|
||||
/// Return a string representation of this node for debugging
|
||||
/// purposes.
|
||||
toString(): string {
|
||||
if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this)
|
||||
let name = this.type.name
|
||||
if (this.content.size)
|
||||
name += "(" + this.content.toStringInner() + ")"
|
||||
return wrapMarks(this.marks, name)
|
||||
}
|
||||
|
||||
/// Get the content match in this node at the given index.
|
||||
contentMatchAt(index: number) {
|
||||
let match = this.type.contentMatch.matchFragment(this.content, 0, index)
|
||||
if (!match) throw new Error("Called contentMatchAt on a node with invalid content")
|
||||
return match
|
||||
}
|
||||
|
||||
/// Test whether replacing the range between `from` and `to` (by
|
||||
/// child index) with the given replacement fragment (which defaults
|
||||
/// to the empty fragment) would leave the node's content valid. You
|
||||
/// can optionally pass `start` and `end` indices into the
|
||||
/// replacement fragment.
|
||||
canReplace(from: number, to: number, replacement = Fragment.empty, start = 0, end = replacement.childCount) {
|
||||
let one = this.contentMatchAt(from).matchFragment(replacement, start, end)
|
||||
let two = one && one.matchFragment(this.content, to)
|
||||
if (!two || !two.validEnd) return false
|
||||
for (let i = start; i < end; i++) if (!this.type.allowsMarks(replacement.child(i).marks)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/// Test whether replacing the range `from` to `to` (by index) with
|
||||
/// a node of the given type would leave the node's content valid.
|
||||
canReplaceWith(from: number, to: number, type: NodeType, marks?: readonly Mark[]) {
|
||||
if (marks && !this.type.allowsMarks(marks)) return false
|
||||
let start = this.contentMatchAt(from).matchType(type)
|
||||
let end = start && start.matchFragment(this.content, to)
|
||||
return end ? end.validEnd : false
|
||||
}
|
||||
|
||||
/// Test whether the given node's content could be appended to this
|
||||
/// node. If that node is empty, this will only return true if there
|
||||
/// is at least one node type that can appear in both nodes (to avoid
|
||||
/// merging completely incompatible nodes).
|
||||
canAppend(other: Node) {
|
||||
if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content)
|
||||
else return this.type.compatibleContent(other.type)
|
||||
}
|
||||
|
||||
/// Check whether this node and its descendants conform to the
|
||||
/// schema, and raise error when they do not.
|
||||
check() {
|
||||
this.type.checkContent(this.content)
|
||||
let copy = Mark.none
|
||||
for (let i = 0; i < this.marks.length; i++) copy = this.marks[i].addToSet(copy)
|
||||
if (!Mark.sameSet(copy, this.marks))
|
||||
throw new RangeError(`Invalid collection of marks for node ${this.type.name}: ${this.marks.map(m => m.type.name)}`)
|
||||
this.content.forEach(node => node.check())
|
||||
}
|
||||
|
||||
/// Return a JSON-serializeable representation of this node.
|
||||
toJSON(): any {
|
||||
let obj: any = {type: this.type.name}
|
||||
for (let _ in this.attrs) {
|
||||
obj.attrs = this.attrs
|
||||
break
|
||||
}
|
||||
if (this.content.size)
|
||||
obj.content = this.content.toJSON()
|
||||
if (this.marks.length)
|
||||
obj.marks = this.marks.map(n => n.toJSON())
|
||||
return obj
|
||||
}
|
||||
|
||||
/// Deserialize a node from its JSON representation.
|
||||
static fromJSON(schema: Schema, json: any): Node {
|
||||
if (!json) throw new RangeError("Invalid input for Node.fromJSON")
|
||||
let marks = null
|
||||
if (json.marks) {
|
||||
if (!Array.isArray(json.marks)) throw new RangeError("Invalid mark data for Node.fromJSON")
|
||||
marks = json.marks.map(schema.markFromJSON)
|
||||
}
|
||||
if (json.type == "text") {
|
||||
if (typeof json.text != "string") throw new RangeError("Invalid text node in JSON")
|
||||
return schema.text(json.text, marks)
|
||||
}
|
||||
let content = Fragment.fromJSON(schema, json.content)
|
||||
return schema.nodeType(json.type).create(json.attrs, content, marks)
|
||||
}
|
||||
}
|
||||
|
||||
;(Node.prototype as any).text = undefined
|
||||
|
||||
export class TextNode extends Node {
|
||||
readonly text: string
|
||||
|
||||
/// @internal
|
||||
constructor(type: NodeType, attrs: Attrs, content: string, marks?: readonly Mark[]) {
|
||||
super(type, attrs, null, marks)
|
||||
if (!content) throw new RangeError("Empty text nodes are not allowed")
|
||||
this.text = content
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this)
|
||||
return wrapMarks(this.marks, JSON.stringify(this.text))
|
||||
}
|
||||
|
||||
get textContent() { return this.text }
|
||||
|
||||
textBetween(from: number, to: number) { return this.text.slice(from, to) }
|
||||
|
||||
get nodeSize() { return this.text.length }
|
||||
|
||||
mark(marks: readonly Mark[]) {
|
||||
return marks == this.marks ? this : new TextNode(this.type, this.attrs, this.text, marks)
|
||||
}
|
||||
|
||||
withText(text: string) {
|
||||
if (text == this.text) return this
|
||||
return new TextNode(this.type, this.attrs, text, this.marks)
|
||||
}
|
||||
|
||||
cut(from = 0, to = this.text.length) {
|
||||
if (from == 0 && to == this.text.length) return this
|
||||
return this.withText(this.text.slice(from, to))
|
||||
}
|
||||
|
||||
eq(other: Node) {
|
||||
return this.sameMarkup(other) && this.text == other.text
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
let base = super.toJSON()
|
||||
base.text = this.text
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
function wrapMarks(marks: readonly Mark[], str: string) {
|
||||
for (let i = marks.length - 1; i >= 0; i--)
|
||||
str = marks[i].type.name + "(" + str + ")"
|
||||
return str
|
||||
}
|
||||
225
resources/app/node_modules/prosemirror-model/src/replace.ts
generated
vendored
Normal file
225
resources/app/node_modules/prosemirror-model/src/replace.ts
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
import {Fragment} from "./fragment"
|
||||
import {Schema} from "./schema"
|
||||
import {Node, TextNode} from "./node"
|
||||
import {ResolvedPos} from "./resolvedpos"
|
||||
|
||||
/// Error type raised by [`Node.replace`](#model.Node.replace) when
|
||||
/// given an invalid replacement.
|
||||
export class ReplaceError extends Error {}
|
||||
/*
|
||||
ReplaceError = function(this: any, message: string) {
|
||||
let err = Error.call(this, message)
|
||||
;(err as any).__proto__ = ReplaceError.prototype
|
||||
return err
|
||||
} as any
|
||||
|
||||
ReplaceError.prototype = Object.create(Error.prototype)
|
||||
ReplaceError.prototype.constructor = ReplaceError
|
||||
ReplaceError.prototype.name = "ReplaceError"
|
||||
*/
|
||||
|
||||
/// A slice represents a piece cut out of a larger document. It
|
||||
/// stores not only a fragment, but also the depth up to which nodes on
|
||||
/// both side are ‘open’ (cut through).
|
||||
export class Slice {
|
||||
/// Create a slice. When specifying a non-zero open depth, you must
|
||||
/// make sure that there are nodes of at least that depth at the
|
||||
/// appropriate side of the fragment—i.e. if the fragment is an
|
||||
/// empty paragraph node, `openStart` and `openEnd` can't be greater
|
||||
/// than 1.
|
||||
///
|
||||
/// It is not necessary for the content of open nodes to conform to
|
||||
/// the schema's content constraints, though it should be a valid
|
||||
/// start/end/middle for such a node, depending on which sides are
|
||||
/// open.
|
||||
constructor(
|
||||
/// The slice's content.
|
||||
readonly content: Fragment,
|
||||
/// The open depth at the start of the fragment.
|
||||
readonly openStart: number,
|
||||
/// The open depth at the end.
|
||||
readonly openEnd: number
|
||||
) {}
|
||||
|
||||
/// The size this slice would add when inserted into a document.
|
||||
get size(): number {
|
||||
return this.content.size - this.openStart - this.openEnd
|
||||
}
|
||||
|
||||
/// @internal
|
||||
insertAt(pos: number, fragment: Fragment) {
|
||||
let content = insertInto(this.content, pos + this.openStart, fragment)
|
||||
return content && new Slice(content, this.openStart, this.openEnd)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
removeBetween(from: number, to: number) {
|
||||
return new Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd)
|
||||
}
|
||||
|
||||
/// Tests whether this slice is equal to another slice.
|
||||
eq(other: Slice): boolean {
|
||||
return this.content.eq(other.content) && this.openStart == other.openStart && this.openEnd == other.openEnd
|
||||
}
|
||||
|
||||
/// @internal
|
||||
toString() {
|
||||
return this.content + "(" + this.openStart + "," + this.openEnd + ")"
|
||||
}
|
||||
|
||||
/// Convert a slice to a JSON-serializable representation.
|
||||
toJSON(): any {
|
||||
if (!this.content.size) return null
|
||||
let json: any = {content: this.content.toJSON()}
|
||||
if (this.openStart > 0) json.openStart = this.openStart
|
||||
if (this.openEnd > 0) json.openEnd = this.openEnd
|
||||
return json
|
||||
}
|
||||
|
||||
/// Deserialize a slice from its JSON representation.
|
||||
static fromJSON(schema: Schema, json: any): Slice {
|
||||
if (!json) return Slice.empty
|
||||
let openStart = json.openStart || 0, openEnd = json.openEnd || 0
|
||||
if (typeof openStart != "number" || typeof openEnd != "number")
|
||||
throw new RangeError("Invalid input for Slice.fromJSON")
|
||||
return new Slice(Fragment.fromJSON(schema, json.content), openStart, openEnd)
|
||||
}
|
||||
|
||||
/// Create a slice from a fragment by taking the maximum possible
|
||||
/// open value on both side of the fragment.
|
||||
static maxOpen(fragment: Fragment, openIsolating = true) {
|
||||
let openStart = 0, openEnd = 0
|
||||
for (let n = fragment.firstChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.firstChild) openStart++
|
||||
for (let n = fragment.lastChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.lastChild) openEnd++
|
||||
return new Slice(fragment, openStart, openEnd)
|
||||
}
|
||||
|
||||
/// The empty slice.
|
||||
static empty = new Slice(Fragment.empty, 0, 0)
|
||||
}
|
||||
|
||||
function removeRange(content: Fragment, from: number, to: number): Fragment {
|
||||
let {index, offset} = content.findIndex(from), child = content.maybeChild(index)
|
||||
let {index: indexTo, offset: offsetTo} = content.findIndex(to)
|
||||
if (offset == from || child!.isText) {
|
||||
if (offsetTo != to && !content.child(indexTo).isText) throw new RangeError("Removing non-flat range")
|
||||
return content.cut(0, from).append(content.cut(to))
|
||||
}
|
||||
if (index != indexTo) throw new RangeError("Removing non-flat range")
|
||||
return content.replaceChild(index, child!.copy(removeRange(child!.content, from - offset - 1, to - offset - 1)))
|
||||
}
|
||||
|
||||
function insertInto(content: Fragment, dist: number, insert: Fragment, parent?: Node): Fragment | null {
|
||||
let {index, offset} = content.findIndex(dist), child = content.maybeChild(index)
|
||||
if (offset == dist || child!.isText) {
|
||||
if (parent && !parent.canReplace(index, index, insert)) return null
|
||||
return content.cut(0, dist).append(insert).append(content.cut(dist))
|
||||
}
|
||||
let inner = insertInto(child!.content, dist - offset - 1, insert)
|
||||
return inner && content.replaceChild(index, child!.copy(inner))
|
||||
}
|
||||
|
||||
export function replace($from: ResolvedPos, $to: ResolvedPos, slice: Slice) {
|
||||
if (slice.openStart > $from.depth)
|
||||
throw new ReplaceError("Inserted content deeper than insertion position")
|
||||
if ($from.depth - slice.openStart != $to.depth - slice.openEnd)
|
||||
throw new ReplaceError("Inconsistent open depths")
|
||||
return replaceOuter($from, $to, slice, 0)
|
||||
}
|
||||
|
||||
function replaceOuter($from: ResolvedPos, $to: ResolvedPos, slice: Slice, depth: number): Node {
|
||||
let index = $from.index(depth), node = $from.node(depth)
|
||||
if (index == $to.index(depth) && depth < $from.depth - slice.openStart) {
|
||||
let inner = replaceOuter($from, $to, slice, depth + 1)
|
||||
return node.copy(node.content.replaceChild(index, inner))
|
||||
} else if (!slice.content.size) {
|
||||
return close(node, replaceTwoWay($from, $to, depth))
|
||||
} else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case
|
||||
let parent = $from.parent, content = parent.content
|
||||
return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset)))
|
||||
} else {
|
||||
let {start, end} = prepareSliceForReplace(slice, $from)
|
||||
return close(node, replaceThreeWay($from, start, end, $to, depth))
|
||||
}
|
||||
}
|
||||
|
||||
function checkJoin(main: Node, sub: Node) {
|
||||
if (!sub.type.compatibleContent(main.type))
|
||||
throw new ReplaceError("Cannot join " + sub.type.name + " onto " + main.type.name)
|
||||
}
|
||||
|
||||
function joinable($before: ResolvedPos, $after: ResolvedPos, depth: number) {
|
||||
let node = $before.node(depth)
|
||||
checkJoin(node, $after.node(depth))
|
||||
return node
|
||||
}
|
||||
|
||||
function addNode(child: Node, target: Node[]) {
|
||||
let last = target.length - 1
|
||||
if (last >= 0 && child.isText && child.sameMarkup(target[last]))
|
||||
target[last] = (child as TextNode).withText(target[last].text! + child.text!)
|
||||
else
|
||||
target.push(child)
|
||||
}
|
||||
|
||||
function addRange($start: ResolvedPos | null, $end: ResolvedPos | null, depth: number, target: Node[]) {
|
||||
let node = ($end || $start)!.node(depth)
|
||||
let startIndex = 0, endIndex = $end ? $end.index(depth) : node.childCount
|
||||
if ($start) {
|
||||
startIndex = $start.index(depth)
|
||||
if ($start.depth > depth) {
|
||||
startIndex++
|
||||
} else if ($start.textOffset) {
|
||||
addNode($start.nodeAfter!, target)
|
||||
startIndex++
|
||||
}
|
||||
}
|
||||
for (let i = startIndex; i < endIndex; i++) addNode(node.child(i), target)
|
||||
if ($end && $end.depth == depth && $end.textOffset)
|
||||
addNode($end.nodeBefore!, target)
|
||||
}
|
||||
|
||||
function close(node: Node, content: Fragment) {
|
||||
node.type.checkContent(content)
|
||||
return node.copy(content)
|
||||
}
|
||||
|
||||
function replaceThreeWay($from: ResolvedPos, $start: ResolvedPos, $end: ResolvedPos, $to: ResolvedPos, depth: number) {
|
||||
let openStart = $from.depth > depth && joinable($from, $start, depth + 1)
|
||||
let openEnd = $to.depth > depth && joinable($end, $to, depth + 1)
|
||||
|
||||
let content: Node[] = []
|
||||
addRange(null, $from, depth, content)
|
||||
if (openStart && openEnd && $start.index(depth) == $end.index(depth)) {
|
||||
checkJoin(openStart, openEnd)
|
||||
addNode(close(openStart, replaceThreeWay($from, $start, $end, $to, depth + 1)), content)
|
||||
} else {
|
||||
if (openStart)
|
||||
addNode(close(openStart, replaceTwoWay($from, $start, depth + 1)), content)
|
||||
addRange($start, $end, depth, content)
|
||||
if (openEnd)
|
||||
addNode(close(openEnd, replaceTwoWay($end, $to, depth + 1)), content)
|
||||
}
|
||||
addRange($to, null, depth, content)
|
||||
return new Fragment(content)
|
||||
}
|
||||
|
||||
function replaceTwoWay($from: ResolvedPos, $to: ResolvedPos, depth: number) {
|
||||
let content: Node[] = []
|
||||
addRange(null, $from, depth, content)
|
||||
if ($from.depth > depth) {
|
||||
let type = joinable($from, $to, depth + 1)
|
||||
addNode(close(type, replaceTwoWay($from, $to, depth + 1)), content)
|
||||
}
|
||||
addRange($to, null, depth, content)
|
||||
return new Fragment(content)
|
||||
}
|
||||
|
||||
function prepareSliceForReplace(slice: Slice, $along: ResolvedPos) {
|
||||
let extra = $along.depth - slice.openStart, parent = $along.node(extra)
|
||||
let node = parent.copy(slice.content)
|
||||
for (let i = extra - 1; i >= 0; i--)
|
||||
node = $along.node(i).copy(Fragment.from(node))
|
||||
return {start: node.resolveNoCache(slice.openStart + extra),
|
||||
end: node.resolveNoCache(node.content.size - slice.openEnd - extra)}
|
||||
}
|
||||
279
resources/app/node_modules/prosemirror-model/src/resolvedpos.ts
generated
vendored
Normal file
279
resources/app/node_modules/prosemirror-model/src/resolvedpos.ts
generated
vendored
Normal file
@@ -0,0 +1,279 @@
|
||||
import {Mark} from "./mark"
|
||||
import {Node} from "./node"
|
||||
|
||||
/// You can [_resolve_](#model.Node.resolve) a position to get more
|
||||
/// information about it. Objects of this class represent such a
|
||||
/// resolved position, providing various pieces of context
|
||||
/// information, and some helper methods.
|
||||
///
|
||||
/// Throughout this interface, methods that take an optional `depth`
|
||||
/// parameter will interpret undefined as `this.depth` and negative
|
||||
/// numbers as `this.depth + value`.
|
||||
export class ResolvedPos {
|
||||
/// The number of levels the parent node is from the root. If this
|
||||
/// position points directly into the root node, it is 0. If it
|
||||
/// points into a top-level paragraph, 1, and so on.
|
||||
depth: number
|
||||
|
||||
/// @internal
|
||||
constructor(
|
||||
/// The position that was resolved.
|
||||
readonly pos: number,
|
||||
/// @internal
|
||||
readonly path: any[],
|
||||
/// The offset this position has into its parent node.
|
||||
readonly parentOffset: number
|
||||
) {
|
||||
this.depth = path.length / 3 - 1
|
||||
}
|
||||
|
||||
/// @internal
|
||||
resolveDepth(val: number | undefined | null) {
|
||||
if (val == null) return this.depth
|
||||
if (val < 0) return this.depth + val
|
||||
return val
|
||||
}
|
||||
|
||||
/// The parent node that the position points into. Note that even if
|
||||
/// a position points into a text node, that node is not considered
|
||||
/// the parent—text nodes are ‘flat’ in this model, and have no content.
|
||||
get parent() { return this.node(this.depth) }
|
||||
|
||||
/// The root node in which the position was resolved.
|
||||
get doc() { return this.node(0) }
|
||||
|
||||
/// The ancestor node at the given level. `p.node(p.depth)` is the
|
||||
/// same as `p.parent`.
|
||||
node(depth?: number | null): Node { return this.path[this.resolveDepth(depth) * 3] }
|
||||
|
||||
/// The index into the ancestor at the given level. If this points
|
||||
/// at the 3rd node in the 2nd paragraph on the top level, for
|
||||
/// example, `p.index(0)` is 1 and `p.index(1)` is 2.
|
||||
index(depth?: number | null): number { return this.path[this.resolveDepth(depth) * 3 + 1] }
|
||||
|
||||
/// The index pointing after this position into the ancestor at the
|
||||
/// given level.
|
||||
indexAfter(depth?: number | null): number {
|
||||
depth = this.resolveDepth(depth)
|
||||
return this.index(depth) + (depth == this.depth && !this.textOffset ? 0 : 1)
|
||||
}
|
||||
|
||||
/// The (absolute) position at the start of the node at the given
|
||||
/// level.
|
||||
start(depth?: number | null): number {
|
||||
depth = this.resolveDepth(depth)
|
||||
return depth == 0 ? 0 : this.path[depth * 3 - 1] + 1
|
||||
}
|
||||
|
||||
/// The (absolute) position at the end of the node at the given
|
||||
/// level.
|
||||
end(depth?: number | null): number {
|
||||
depth = this.resolveDepth(depth)
|
||||
return this.start(depth) + this.node(depth).content.size
|
||||
}
|
||||
|
||||
/// The (absolute) position directly before the wrapping node at the
|
||||
/// given level, or, when `depth` is `this.depth + 1`, the original
|
||||
/// position.
|
||||
before(depth?: number | null): number {
|
||||
depth = this.resolveDepth(depth)
|
||||
if (!depth) throw new RangeError("There is no position before the top-level node")
|
||||
return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1]
|
||||
}
|
||||
|
||||
/// The (absolute) position directly after the wrapping node at the
|
||||
/// given level, or the original position when `depth` is `this.depth + 1`.
|
||||
after(depth?: number | null): number {
|
||||
depth = this.resolveDepth(depth)
|
||||
if (!depth) throw new RangeError("There is no position after the top-level node")
|
||||
return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] + this.path[depth * 3].nodeSize
|
||||
}
|
||||
|
||||
/// When this position points into a text node, this returns the
|
||||
/// distance between the position and the start of the text node.
|
||||
/// Will be zero for positions that point between nodes.
|
||||
get textOffset(): number { return this.pos - this.path[this.path.length - 1] }
|
||||
|
||||
/// Get the node directly after the position, if any. If the position
|
||||
/// points into a text node, only the part of that node after the
|
||||
/// position is returned.
|
||||
get nodeAfter(): Node | null {
|
||||
let parent = this.parent, index = this.index(this.depth)
|
||||
if (index == parent.childCount) return null
|
||||
let dOff = this.pos - this.path[this.path.length - 1], child = parent.child(index)
|
||||
return dOff ? parent.child(index).cut(dOff) : child
|
||||
}
|
||||
|
||||
/// Get the node directly before the position, if any. If the
|
||||
/// position points into a text node, only the part of that node
|
||||
/// before the position is returned.
|
||||
get nodeBefore(): Node | null {
|
||||
let index = this.index(this.depth)
|
||||
let dOff = this.pos - this.path[this.path.length - 1]
|
||||
if (dOff) return this.parent.child(index).cut(0, dOff)
|
||||
return index == 0 ? null : this.parent.child(index - 1)
|
||||
}
|
||||
|
||||
/// Get the position at the given index in the parent node at the
|
||||
/// given depth (which defaults to `this.depth`).
|
||||
posAtIndex(index: number, depth?: number | null): number {
|
||||
depth = this.resolveDepth(depth)
|
||||
let node = this.path[depth * 3], pos = depth == 0 ? 0 : this.path[depth * 3 - 1] + 1
|
||||
for (let i = 0; i < index; i++) pos += node.child(i).nodeSize
|
||||
return pos
|
||||
}
|
||||
|
||||
/// Get the marks at this position, factoring in the surrounding
|
||||
/// marks' [`inclusive`](#model.MarkSpec.inclusive) property. If the
|
||||
/// position is at the start of a non-empty node, the marks of the
|
||||
/// node after it (if any) are returned.
|
||||
marks(): readonly Mark[] {
|
||||
let parent = this.parent, index = this.index()
|
||||
|
||||
// In an empty parent, return the empty array
|
||||
if (parent.content.size == 0) return Mark.none
|
||||
|
||||
// When inside a text node, just return the text node's marks
|
||||
if (this.textOffset) return parent.child(index).marks
|
||||
|
||||
let main = parent.maybeChild(index - 1), other = parent.maybeChild(index)
|
||||
// If the `after` flag is true of there is no node before, make
|
||||
// the node after this position the main reference.
|
||||
if (!main) { let tmp = main; main = other; other = tmp }
|
||||
|
||||
// Use all marks in the main node, except those that have
|
||||
// `inclusive` set to false and are not present in the other node.
|
||||
let marks = main!.marks
|
||||
for (var i = 0; i < marks.length; i++)
|
||||
if (marks[i].type.spec.inclusive === false && (!other || !marks[i].isInSet(other.marks)))
|
||||
marks = marks[i--].removeFromSet(marks)
|
||||
|
||||
return marks
|
||||
}
|
||||
|
||||
/// Get the marks after the current position, if any, except those
|
||||
/// that are non-inclusive and not present at position `$end`. This
|
||||
/// is mostly useful for getting the set of marks to preserve after a
|
||||
/// deletion. Will return `null` if this position is at the end of
|
||||
/// its parent node or its parent node isn't a textblock (in which
|
||||
/// case no marks should be preserved).
|
||||
marksAcross($end: ResolvedPos): readonly Mark[] | null {
|
||||
let after = this.parent.maybeChild(this.index())
|
||||
if (!after || !after.isInline) return null
|
||||
|
||||
let marks = after.marks, next = $end.parent.maybeChild($end.index())
|
||||
for (var i = 0; i < marks.length; i++)
|
||||
if (marks[i].type.spec.inclusive === false && (!next || !marks[i].isInSet(next.marks)))
|
||||
marks = marks[i--].removeFromSet(marks)
|
||||
return marks
|
||||
}
|
||||
|
||||
/// The depth up to which this position and the given (non-resolved)
|
||||
/// position share the same parent nodes.
|
||||
sharedDepth(pos: number): number {
|
||||
for (let depth = this.depth; depth > 0; depth--)
|
||||
if (this.start(depth) <= pos && this.end(depth) >= pos) return depth
|
||||
return 0
|
||||
}
|
||||
|
||||
/// Returns a range based on the place where this position and the
|
||||
/// given position diverge around block content. If both point into
|
||||
/// the same textblock, for example, a range around that textblock
|
||||
/// will be returned. If they point into different blocks, the range
|
||||
/// around those blocks in their shared ancestor is returned. You can
|
||||
/// pass in an optional predicate that will be called with a parent
|
||||
/// node to see if a range into that parent is acceptable.
|
||||
blockRange(other: ResolvedPos = this, pred?: (node: Node) => boolean): NodeRange | null {
|
||||
if (other.pos < this.pos) return other.blockRange(this)
|
||||
for (let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0); d >= 0; d--)
|
||||
if (other.pos <= this.end(d) && (!pred || pred(this.node(d))))
|
||||
return new NodeRange(this, other, d)
|
||||
return null
|
||||
}
|
||||
|
||||
/// Query whether the given position shares the same parent node.
|
||||
sameParent(other: ResolvedPos): boolean {
|
||||
return this.pos - this.parentOffset == other.pos - other.parentOffset
|
||||
}
|
||||
|
||||
/// Return the greater of this and the given position.
|
||||
max(other: ResolvedPos): ResolvedPos {
|
||||
return other.pos > this.pos ? other : this
|
||||
}
|
||||
|
||||
/// Return the smaller of this and the given position.
|
||||
min(other: ResolvedPos): ResolvedPos {
|
||||
return other.pos < this.pos ? other : this
|
||||
}
|
||||
|
||||
/// @internal
|
||||
toString() {
|
||||
let str = ""
|
||||
for (let i = 1; i <= this.depth; i++)
|
||||
str += (str ? "/" : "") + this.node(i).type.name + "_" + this.index(i - 1)
|
||||
return str + ":" + this.parentOffset
|
||||
}
|
||||
|
||||
/// @internal
|
||||
static resolve(doc: Node, pos: number): ResolvedPos {
|
||||
if (!(pos >= 0 && pos <= doc.content.size)) throw new RangeError("Position " + pos + " out of range")
|
||||
let path = []
|
||||
let start = 0, parentOffset = pos
|
||||
for (let node = doc;;) {
|
||||
let {index, offset} = node.content.findIndex(parentOffset)
|
||||
let rem = parentOffset - offset
|
||||
path.push(node, index, start + offset)
|
||||
if (!rem) break
|
||||
node = node.child(index)
|
||||
if (node.isText) break
|
||||
parentOffset = rem - 1
|
||||
start += offset + 1
|
||||
}
|
||||
return new ResolvedPos(pos, path, parentOffset)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
static resolveCached(doc: Node, pos: number): ResolvedPos {
|
||||
for (let i = 0; i < resolveCache.length; i++) {
|
||||
let cached = resolveCache[i]
|
||||
if (cached.pos == pos && cached.doc == doc) return cached
|
||||
}
|
||||
let result = resolveCache[resolveCachePos] = ResolvedPos.resolve(doc, pos)
|
||||
resolveCachePos = (resolveCachePos + 1) % resolveCacheSize
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let resolveCache: ResolvedPos[] = [], resolveCachePos = 0, resolveCacheSize = 12
|
||||
|
||||
/// Represents a flat range of content, i.e. one that starts and
|
||||
/// ends in the same node.
|
||||
export class NodeRange {
|
||||
/// Construct a node range. `$from` and `$to` should point into the
|
||||
/// same node until at least the given `depth`, since a node range
|
||||
/// denotes an adjacent set of nodes in a single parent node.
|
||||
constructor(
|
||||
/// A resolved position along the start of the content. May have a
|
||||
/// `depth` greater than this object's `depth` property, since
|
||||
/// these are the positions that were used to compute the range,
|
||||
/// not re-resolved positions directly at its boundaries.
|
||||
readonly $from: ResolvedPos,
|
||||
/// A position along the end of the content. See
|
||||
/// caveat for [`$from`](#model.NodeRange.$from).
|
||||
readonly $to: ResolvedPos,
|
||||
/// The depth of the node that this range points into.
|
||||
readonly depth: number
|
||||
) {}
|
||||
|
||||
/// The position at the start of the range.
|
||||
get start() { return this.$from.before(this.depth + 1) }
|
||||
/// The position at the end of the range.
|
||||
get end() { return this.$to.after(this.depth + 1) }
|
||||
|
||||
/// The parent node that the range points into.
|
||||
get parent() { return this.$from.node(this.depth) }
|
||||
/// The start index of the range in the parent node.
|
||||
get startIndex() { return this.$from.index(this.depth) }
|
||||
/// The end index of the range in the parent node.
|
||||
get endIndex() { return this.$to.indexAfter(this.depth) }
|
||||
}
|
||||
661
resources/app/node_modules/prosemirror-model/src/schema.ts
generated
vendored
Normal file
661
resources/app/node_modules/prosemirror-model/src/schema.ts
generated
vendored
Normal file
@@ -0,0 +1,661 @@
|
||||
import OrderedMap from "orderedmap"
|
||||
|
||||
import {Node, TextNode} from "./node"
|
||||
import {Fragment} from "./fragment"
|
||||
import {Mark} from "./mark"
|
||||
import {ContentMatch} from "./content"
|
||||
import {DOMOutputSpec} from "./to_dom"
|
||||
import {ParseRule, TagParseRule} from "./from_dom"
|
||||
|
||||
/// An object holding the attributes of a node.
|
||||
export type Attrs = {readonly [attr: string]: any}
|
||||
|
||||
// For node types where all attrs have a default value (or which don't
|
||||
// have any attributes), build up a single reusable default attribute
|
||||
// object, and use it for all nodes that don't specify specific
|
||||
// attributes.
|
||||
function defaultAttrs(attrs: Attrs) {
|
||||
let defaults = Object.create(null)
|
||||
for (let attrName in attrs) {
|
||||
let attr = attrs[attrName]
|
||||
if (!attr.hasDefault) return null
|
||||
defaults[attrName] = attr.default
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
function computeAttrs(attrs: Attrs, value: Attrs | null) {
|
||||
let built = Object.create(null)
|
||||
for (let name in attrs) {
|
||||
let given = value && value[name]
|
||||
if (given === undefined) {
|
||||
let attr = attrs[name]
|
||||
if (attr.hasDefault) given = attr.default
|
||||
else throw new RangeError("No value supplied for attribute " + name)
|
||||
}
|
||||
built[name] = given
|
||||
}
|
||||
return built
|
||||
}
|
||||
|
||||
function initAttrs(attrs?: {[name: string]: AttributeSpec}) {
|
||||
let result: {[name: string]: Attribute} = Object.create(null)
|
||||
if (attrs) for (let name in attrs) result[name] = new Attribute(attrs[name])
|
||||
return result
|
||||
}
|
||||
|
||||
/// Node types are objects allocated once per `Schema` and used to
|
||||
/// [tag](#model.Node.type) `Node` instances. They contain information
|
||||
/// about the node type, such as its name and what kind of node it
|
||||
/// represents.
|
||||
export class NodeType {
|
||||
/// @internal
|
||||
groups: readonly string[]
|
||||
/// @internal
|
||||
attrs: {[name: string]: Attribute}
|
||||
/// @internal
|
||||
defaultAttrs: Attrs
|
||||
|
||||
/// @internal
|
||||
constructor(
|
||||
/// The name the node type has in this schema.
|
||||
readonly name: string,
|
||||
/// A link back to the `Schema` the node type belongs to.
|
||||
readonly schema: Schema,
|
||||
/// The spec that this type is based on
|
||||
readonly spec: NodeSpec
|
||||
) {
|
||||
this.groups = spec.group ? spec.group.split(" ") : []
|
||||
this.attrs = initAttrs(spec.attrs)
|
||||
this.defaultAttrs = defaultAttrs(this.attrs)
|
||||
|
||||
// Filled in later
|
||||
;(this as any).contentMatch = null
|
||||
;(this as any).inlineContent = null
|
||||
|
||||
this.isBlock = !(spec.inline || name == "text")
|
||||
this.isText = name == "text"
|
||||
}
|
||||
|
||||
/// True if this node type has inline content.
|
||||
inlineContent!: boolean
|
||||
/// True if this is a block type
|
||||
isBlock: boolean
|
||||
/// True if this is the text node type.
|
||||
isText: boolean
|
||||
|
||||
/// True if this is an inline type.
|
||||
get isInline() { return !this.isBlock }
|
||||
|
||||
/// True if this is a textblock type, a block that contains inline
|
||||
/// content.
|
||||
get isTextblock() { return this.isBlock && this.inlineContent }
|
||||
|
||||
/// True for node types that allow no content.
|
||||
get isLeaf() { return this.contentMatch == ContentMatch.empty }
|
||||
|
||||
/// True when this node is an atom, i.e. when it does not have
|
||||
/// directly editable content.
|
||||
get isAtom() { return this.isLeaf || !!this.spec.atom }
|
||||
|
||||
/// The starting match of the node type's content expression.
|
||||
contentMatch!: ContentMatch
|
||||
|
||||
/// The set of marks allowed in this node. `null` means all marks
|
||||
/// are allowed.
|
||||
markSet: readonly MarkType[] | null = null
|
||||
|
||||
/// The node type's [whitespace](#model.NodeSpec.whitespace) option.
|
||||
get whitespace(): "pre" | "normal" {
|
||||
return this.spec.whitespace || (this.spec.code ? "pre" : "normal")
|
||||
}
|
||||
|
||||
/// Tells you whether this node type has any required attributes.
|
||||
hasRequiredAttrs() {
|
||||
for (let n in this.attrs) if (this.attrs[n].isRequired) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/// Indicates whether this node allows some of the same content as
|
||||
/// the given node type.
|
||||
compatibleContent(other: NodeType) {
|
||||
return this == other || this.contentMatch.compatible(other.contentMatch)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
computeAttrs(attrs: Attrs | null): Attrs {
|
||||
if (!attrs && this.defaultAttrs) return this.defaultAttrs
|
||||
else return computeAttrs(this.attrs, attrs)
|
||||
}
|
||||
|
||||
/// Create a `Node` of this type. The given attributes are
|
||||
/// checked and defaulted (you can pass `null` to use the type's
|
||||
/// defaults entirely, if no required attributes exist). `content`
|
||||
/// may be a `Fragment`, a node, an array of nodes, or
|
||||
/// `null`. Similarly `marks` may be `null` to default to the empty
|
||||
/// set of marks.
|
||||
create(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) {
|
||||
if (this.isText) throw new Error("NodeType.create can't construct text nodes")
|
||||
return new Node(this, this.computeAttrs(attrs), Fragment.from(content), Mark.setFrom(marks))
|
||||
}
|
||||
|
||||
/// Like [`create`](#model.NodeType.create), but check the given content
|
||||
/// against the node type's content restrictions, and throw an error
|
||||
/// if it doesn't match.
|
||||
createChecked(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) {
|
||||
content = Fragment.from(content)
|
||||
this.checkContent(content)
|
||||
return new Node(this, this.computeAttrs(attrs), content, Mark.setFrom(marks))
|
||||
}
|
||||
|
||||
/// Like [`create`](#model.NodeType.create), but see if it is
|
||||
/// necessary to add nodes to the start or end of the given fragment
|
||||
/// to make it fit the node. If no fitting wrapping can be found,
|
||||
/// return null. Note that, due to the fact that required nodes can
|
||||
/// always be created, this will always succeed if you pass null or
|
||||
/// `Fragment.empty` as content.
|
||||
createAndFill(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) {
|
||||
attrs = this.computeAttrs(attrs)
|
||||
content = Fragment.from(content)
|
||||
if (content.size) {
|
||||
let before = this.contentMatch.fillBefore(content)
|
||||
if (!before) return null
|
||||
content = before.append(content)
|
||||
}
|
||||
let matched = this.contentMatch.matchFragment(content)
|
||||
let after = matched && matched.fillBefore(Fragment.empty, true)
|
||||
if (!after) return null
|
||||
return new Node(this, attrs, (content as Fragment).append(after), Mark.setFrom(marks))
|
||||
}
|
||||
|
||||
/// Returns true if the given fragment is valid content for this node
|
||||
/// type with the given attributes.
|
||||
validContent(content: Fragment) {
|
||||
let result = this.contentMatch.matchFragment(content)
|
||||
if (!result || !result.validEnd) return false
|
||||
for (let i = 0; i < content.childCount; i++)
|
||||
if (!this.allowsMarks(content.child(i).marks)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/// Throws a RangeError if the given fragment is not valid content for this
|
||||
/// node type.
|
||||
/// @internal
|
||||
checkContent(content: Fragment) {
|
||||
if (!this.validContent(content))
|
||||
throw new RangeError(`Invalid content for node ${this.name}: ${content.toString().slice(0, 50)}`)
|
||||
}
|
||||
|
||||
/// Check whether the given mark type is allowed in this node.
|
||||
allowsMarkType(markType: MarkType) {
|
||||
return this.markSet == null || this.markSet.indexOf(markType) > -1
|
||||
}
|
||||
|
||||
/// Test whether the given set of marks are allowed in this node.
|
||||
allowsMarks(marks: readonly Mark[]) {
|
||||
if (this.markSet == null) return true
|
||||
for (let i = 0; i < marks.length; i++) if (!this.allowsMarkType(marks[i].type)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/// Removes the marks that are not allowed in this node from the given set.
|
||||
allowedMarks(marks: readonly Mark[]): readonly Mark[] {
|
||||
if (this.markSet == null) return marks
|
||||
let copy
|
||||
for (let i = 0; i < marks.length; i++) {
|
||||
if (!this.allowsMarkType(marks[i].type)) {
|
||||
if (!copy) copy = marks.slice(0, i)
|
||||
} else if (copy) {
|
||||
copy.push(marks[i])
|
||||
}
|
||||
}
|
||||
return !copy ? marks : copy.length ? copy : Mark.none
|
||||
}
|
||||
|
||||
/// @internal
|
||||
static compile<Nodes extends string>(nodes: OrderedMap<NodeSpec>, schema: Schema<Nodes>): {readonly [name in Nodes]: NodeType} {
|
||||
let result = Object.create(null)
|
||||
nodes.forEach((name, spec) => result[name] = new NodeType(name, schema, spec))
|
||||
|
||||
let topType = schema.spec.topNode || "doc"
|
||||
if (!result[topType]) throw new RangeError("Schema is missing its top node type ('" + topType + "')")
|
||||
if (!result.text) throw new RangeError("Every schema needs a 'text' type")
|
||||
for (let _ in result.text.attrs) throw new RangeError("The text node type should not have attributes")
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Attribute descriptors
|
||||
|
||||
class Attribute {
|
||||
hasDefault: boolean
|
||||
default: any
|
||||
|
||||
constructor(options: AttributeSpec) {
|
||||
this.hasDefault = Object.prototype.hasOwnProperty.call(options, "default")
|
||||
this.default = options.default
|
||||
}
|
||||
|
||||
get isRequired() {
|
||||
return !this.hasDefault
|
||||
}
|
||||
}
|
||||
|
||||
// Marks
|
||||
|
||||
/// Like nodes, marks (which are associated with nodes to signify
|
||||
/// things like emphasis or being part of a link) are
|
||||
/// [tagged](#model.Mark.type) with type objects, which are
|
||||
/// instantiated once per `Schema`.
|
||||
export class MarkType {
|
||||
/// @internal
|
||||
attrs: {[name: string]: Attribute}
|
||||
/// @internal
|
||||
excluded!: readonly MarkType[]
|
||||
/// @internal
|
||||
instance: Mark | null
|
||||
|
||||
/// @internal
|
||||
constructor(
|
||||
/// The name of the mark type.
|
||||
readonly name: string,
|
||||
/// @internal
|
||||
readonly rank: number,
|
||||
/// The schema that this mark type instance is part of.
|
||||
readonly schema: Schema,
|
||||
/// The spec on which the type is based.
|
||||
readonly spec: MarkSpec
|
||||
) {
|
||||
this.attrs = initAttrs(spec.attrs)
|
||||
;(this as any).excluded = null
|
||||
let defaults = defaultAttrs(this.attrs)
|
||||
this.instance = defaults ? new Mark(this, defaults) : null
|
||||
}
|
||||
|
||||
/// Create a mark of this type. `attrs` may be `null` or an object
|
||||
/// containing only some of the mark's attributes. The others, if
|
||||
/// they have defaults, will be added.
|
||||
create(attrs: Attrs | null = null) {
|
||||
if (!attrs && this.instance) return this.instance
|
||||
return new Mark(this, computeAttrs(this.attrs, attrs))
|
||||
}
|
||||
|
||||
/// @internal
|
||||
static compile(marks: OrderedMap<MarkSpec>, schema: Schema) {
|
||||
let result = Object.create(null), rank = 0
|
||||
marks.forEach((name, spec) => result[name] = new MarkType(name, rank++, schema, spec))
|
||||
return result
|
||||
}
|
||||
|
||||
/// When there is a mark of this type in the given set, a new set
|
||||
/// without it is returned. Otherwise, the input set is returned.
|
||||
removeFromSet(set: readonly Mark[]): readonly Mark[] {
|
||||
for (var i = 0; i < set.length; i++) if (set[i].type == this) {
|
||||
set = set.slice(0, i).concat(set.slice(i + 1))
|
||||
i--
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
/// Tests whether there is a mark of this type in the given set.
|
||||
isInSet(set: readonly Mark[]): Mark | undefined {
|
||||
for (let i = 0; i < set.length; i++)
|
||||
if (set[i].type == this) return set[i]
|
||||
}
|
||||
|
||||
/// Queries whether a given mark type is
|
||||
/// [excluded](#model.MarkSpec.excludes) by this one.
|
||||
excludes(other: MarkType) {
|
||||
return this.excluded.indexOf(other) > -1
|
||||
}
|
||||
}
|
||||
|
||||
/// An object describing a schema, as passed to the [`Schema`](#model.Schema)
|
||||
/// constructor.
|
||||
export interface SchemaSpec<Nodes extends string = any, Marks extends string = any> {
|
||||
/// The node types in this schema. Maps names to
|
||||
/// [`NodeSpec`](#model.NodeSpec) objects that describe the node type
|
||||
/// associated with that name. Their order is significant—it
|
||||
/// determines which [parse rules](#model.NodeSpec.parseDOM) take
|
||||
/// precedence by default, and which nodes come first in a given
|
||||
/// [group](#model.NodeSpec.group).
|
||||
nodes: {[name in Nodes]: NodeSpec} | OrderedMap<NodeSpec>,
|
||||
|
||||
/// The mark types that exist in this schema. The order in which they
|
||||
/// are provided determines the order in which [mark
|
||||
/// sets](#model.Mark.addToSet) are sorted and in which [parse
|
||||
/// rules](#model.MarkSpec.parseDOM) are tried.
|
||||
marks?: {[name in Marks]: MarkSpec} | OrderedMap<MarkSpec>
|
||||
|
||||
/// The name of the default top-level node for the schema. Defaults
|
||||
/// to `"doc"`.
|
||||
topNode?: string
|
||||
}
|
||||
|
||||
/// A description of a node type, used when defining a schema.
|
||||
export interface NodeSpec {
|
||||
/// The content expression for this node, as described in the [schema
|
||||
/// guide](/docs/guide/#schema.content_expressions). When not given,
|
||||
/// the node does not allow any content.
|
||||
content?: string
|
||||
|
||||
/// The marks that are allowed inside of this node. May be a
|
||||
/// space-separated string referring to mark names or groups, `"_"`
|
||||
/// to explicitly allow all marks, or `""` to disallow marks. When
|
||||
/// not given, nodes with inline content default to allowing all
|
||||
/// marks, other nodes default to not allowing marks.
|
||||
marks?: string
|
||||
|
||||
/// The group or space-separated groups to which this node belongs,
|
||||
/// which can be referred to in the content expressions for the
|
||||
/// schema.
|
||||
group?: string
|
||||
|
||||
/// Should be set to true for inline nodes. (Implied for text nodes.)
|
||||
inline?: boolean
|
||||
|
||||
/// Can be set to true to indicate that, though this isn't a [leaf
|
||||
/// node](#model.NodeType.isLeaf), it doesn't have directly editable
|
||||
/// content and should be treated as a single unit in the view.
|
||||
atom?: boolean
|
||||
|
||||
/// The attributes that nodes of this type get.
|
||||
attrs?: {[name: string]: AttributeSpec}
|
||||
|
||||
/// Controls whether nodes of this type can be selected as a [node
|
||||
/// selection](#state.NodeSelection). Defaults to true for non-text
|
||||
/// nodes.
|
||||
selectable?: boolean
|
||||
|
||||
/// Determines whether nodes of this type can be dragged without
|
||||
/// being selected. Defaults to false.
|
||||
draggable?: boolean
|
||||
|
||||
/// Can be used to indicate that this node contains code, which
|
||||
/// causes some commands to behave differently.
|
||||
code?: boolean
|
||||
|
||||
/// Controls way whitespace in this a node is parsed. The default is
|
||||
/// `"normal"`, which causes the [DOM parser](#model.DOMParser) to
|
||||
/// collapse whitespace in normal mode, and normalize it (replacing
|
||||
/// newlines and such with spaces) otherwise. `"pre"` causes the
|
||||
/// parser to preserve spaces inside the node. When this option isn't
|
||||
/// given, but [`code`](#model.NodeSpec.code) is true, `whitespace`
|
||||
/// will default to `"pre"`. Note that this option doesn't influence
|
||||
/// the way the node is rendered—that should be handled by `toDOM`
|
||||
/// and/or styling.
|
||||
whitespace?: "pre" | "normal"
|
||||
|
||||
/// Determines whether this node is considered an important parent
|
||||
/// node during replace operations (such as paste). Non-defining (the
|
||||
/// default) nodes get dropped when their entire content is replaced,
|
||||
/// whereas defining nodes persist and wrap the inserted content.
|
||||
definingAsContext?: boolean
|
||||
|
||||
/// In inserted content the defining parents of the content are
|
||||
/// preserved when possible. Typically, non-default-paragraph
|
||||
/// textblock types, and possibly list items, are marked as defining.
|
||||
definingForContent?: boolean
|
||||
|
||||
/// When enabled, enables both
|
||||
/// [`definingAsContext`](#model.NodeSpec.definingAsContext) and
|
||||
/// [`definingForContent`](#model.NodeSpec.definingForContent).
|
||||
defining?: boolean
|
||||
|
||||
/// When enabled (default is false), the sides of nodes of this type
|
||||
/// count as boundaries that regular editing operations, like
|
||||
/// backspacing or lifting, won't cross. An example of a node that
|
||||
/// should probably have this enabled is a table cell.
|
||||
isolating?: boolean
|
||||
|
||||
/// Defines the default way a node of this type should be serialized
|
||||
/// to DOM/HTML (as used by
|
||||
/// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)).
|
||||
/// Should return a DOM node or an [array
|
||||
/// structure](#model.DOMOutputSpec) that describes one, with an
|
||||
/// optional number zero (“hole”) in it to indicate where the node's
|
||||
/// content should be inserted.
|
||||
///
|
||||
/// For text nodes, the default is to create a text DOM node. Though
|
||||
/// it is possible to create a serializer where text is rendered
|
||||
/// differently, this is not supported inside the editor, so you
|
||||
/// shouldn't override that in your text node spec.
|
||||
toDOM?: (node: Node) => DOMOutputSpec
|
||||
|
||||
/// Associates DOM parser information with this node, which can be
|
||||
/// used by [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) to
|
||||
/// automatically derive a parser. The `node` field in the rules is
|
||||
/// implied (the name of this node will be filled in automatically).
|
||||
/// If you supply your own parser, you do not need to also specify
|
||||
/// parsing rules in your schema.
|
||||
parseDOM?: readonly TagParseRule[]
|
||||
|
||||
/// Defines the default way a node of this type should be serialized
|
||||
/// to a string representation for debugging (e.g. in error messages).
|
||||
toDebugString?: (node: Node) => string
|
||||
|
||||
/// Defines the default way a [leaf node](#model.NodeType.isLeaf) of
|
||||
/// this type should be serialized to a string (as used by
|
||||
/// [`Node.textBetween`](#model.Node^textBetween) and
|
||||
/// [`Node.textContent`](#model.Node^textContent)).
|
||||
leafText?: (node: Node) => string
|
||||
|
||||
/// A single inline node in a schema can be set to be a linebreak
|
||||
/// equivalent. When converting between block types that support the
|
||||
/// node and block types that don't but have
|
||||
/// [`whitespace`](#model.NodeSpec.whitespace) set to `"pre"`,
|
||||
/// [`setBlockType`](#transform.Transform.setBlockType) will convert
|
||||
/// between newline characters to or from linebreak nodes as
|
||||
/// appropriate.
|
||||
linebreakReplacement?: boolean
|
||||
|
||||
/// Node specs may include arbitrary properties that can be read by
|
||||
/// other code via [`NodeType.spec`](#model.NodeType.spec).
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/// Used to define marks when creating a schema.
|
||||
export interface MarkSpec {
|
||||
/// The attributes that marks of this type get.
|
||||
attrs?: {[name: string]: AttributeSpec}
|
||||
|
||||
/// Whether this mark should be active when the cursor is positioned
|
||||
/// at its end (or at its start when that is also the start of the
|
||||
/// parent node). Defaults to true.
|
||||
inclusive?: boolean
|
||||
|
||||
/// Determines which other marks this mark can coexist with. Should
|
||||
/// be a space-separated strings naming other marks or groups of marks.
|
||||
/// When a mark is [added](#model.Mark.addToSet) to a set, all marks
|
||||
/// that it excludes are removed in the process. If the set contains
|
||||
/// any mark that excludes the new mark but is not, itself, excluded
|
||||
/// by the new mark, the mark can not be added an the set. You can
|
||||
/// use the value `"_"` to indicate that the mark excludes all
|
||||
/// marks in the schema.
|
||||
///
|
||||
/// Defaults to only being exclusive with marks of the same type. You
|
||||
/// can set it to an empty string (or any string not containing the
|
||||
/// mark's own name) to allow multiple marks of a given type to
|
||||
/// coexist (as long as they have different attributes).
|
||||
excludes?: string
|
||||
|
||||
/// The group or space-separated groups to which this mark belongs.
|
||||
group?: string
|
||||
|
||||
/// Determines whether marks of this type can span multiple adjacent
|
||||
/// nodes when serialized to DOM/HTML. Defaults to true.
|
||||
spanning?: boolean
|
||||
|
||||
/// Defines the default way marks of this type should be serialized
|
||||
/// to DOM/HTML. When the resulting spec contains a hole, that is
|
||||
/// where the marked content is placed. Otherwise, it is appended to
|
||||
/// the top node.
|
||||
toDOM?: (mark: Mark, inline: boolean) => DOMOutputSpec
|
||||
|
||||
/// Associates DOM parser information with this mark (see the
|
||||
/// corresponding [node spec field](#model.NodeSpec.parseDOM)). The
|
||||
/// `mark` field in the rules is implied.
|
||||
parseDOM?: readonly ParseRule[]
|
||||
|
||||
/// Mark specs can include additional properties that can be
|
||||
/// inspected through [`MarkType.spec`](#model.MarkType.spec) when
|
||||
/// working with the mark.
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/// Used to [define](#model.NodeSpec.attrs) attributes on nodes or
|
||||
/// marks.
|
||||
export interface AttributeSpec {
|
||||
/// The default value for this attribute, to use when no explicit
|
||||
/// value is provided. Attributes that have no default must be
|
||||
/// provided whenever a node or mark of a type that has them is
|
||||
/// created.
|
||||
default?: any
|
||||
}
|
||||
|
||||
/// A document schema. Holds [node](#model.NodeType) and [mark
|
||||
/// type](#model.MarkType) objects for the nodes and marks that may
|
||||
/// occur in conforming documents, and provides functionality for
|
||||
/// creating and deserializing such documents.
|
||||
///
|
||||
/// When given, the type parameters provide the names of the nodes and
|
||||
/// marks in this schema.
|
||||
export class Schema<Nodes extends string = any, Marks extends string = any> {
|
||||
/// The [spec](#model.SchemaSpec) on which the schema is based,
|
||||
/// with the added guarantee that its `nodes` and `marks`
|
||||
/// properties are
|
||||
/// [`OrderedMap`](https://github.com/marijnh/orderedmap) instances
|
||||
/// (not raw objects).
|
||||
spec: {
|
||||
nodes: OrderedMap<NodeSpec>,
|
||||
marks: OrderedMap<MarkSpec>,
|
||||
topNode?: string
|
||||
}
|
||||
|
||||
/// An object mapping the schema's node names to node type objects.
|
||||
nodes: {readonly [name in Nodes]: NodeType} & {readonly [key: string]: NodeType}
|
||||
|
||||
/// A map from mark names to mark type objects.
|
||||
marks: {readonly [name in Marks]: MarkType} & {readonly [key: string]: MarkType}
|
||||
|
||||
/// The [linebreak
|
||||
/// replacement](#model.NodeSpec.linebreakReplacement) node defined
|
||||
/// in this schema, if any.
|
||||
linebreakReplacement: NodeType | null = null
|
||||
|
||||
/// Construct a schema from a schema [specification](#model.SchemaSpec).
|
||||
constructor(spec: SchemaSpec<Nodes, Marks>) {
|
||||
let instanceSpec = this.spec = {} as any
|
||||
for (let prop in spec) instanceSpec[prop] = (spec as any)[prop]
|
||||
instanceSpec.nodes = OrderedMap.from(spec.nodes),
|
||||
instanceSpec.marks = OrderedMap.from(spec.marks || {}),
|
||||
|
||||
this.nodes = NodeType.compile(this.spec.nodes, this)
|
||||
this.marks = MarkType.compile(this.spec.marks, this)
|
||||
|
||||
let contentExprCache = Object.create(null)
|
||||
for (let prop in this.nodes) {
|
||||
if (prop in this.marks)
|
||||
throw new RangeError(prop + " can not be both a node and a mark")
|
||||
let type = this.nodes[prop], contentExpr = type.spec.content || "", markExpr = type.spec.marks
|
||||
type.contentMatch = contentExprCache[contentExpr] ||
|
||||
(contentExprCache[contentExpr] = ContentMatch.parse(contentExpr, this.nodes))
|
||||
;(type as any).inlineContent = type.contentMatch.inlineContent
|
||||
if (type.spec.linebreakReplacement) {
|
||||
if (this.linebreakReplacement) throw new RangeError("Multiple linebreak nodes defined")
|
||||
if (!type.isInline || !type.isLeaf) throw new RangeError("Linebreak replacement nodes must be inline leaf nodes")
|
||||
this.linebreakReplacement = type
|
||||
}
|
||||
type.markSet = markExpr == "_" ? null :
|
||||
markExpr ? gatherMarks(this, markExpr.split(" ")) :
|
||||
markExpr == "" || !type.inlineContent ? [] : null
|
||||
}
|
||||
for (let prop in this.marks) {
|
||||
let type = this.marks[prop], excl = type.spec.excludes
|
||||
type.excluded = excl == null ? [type] : excl == "" ? [] : gatherMarks(this, excl.split(" "))
|
||||
}
|
||||
|
||||
this.nodeFromJSON = this.nodeFromJSON.bind(this)
|
||||
this.markFromJSON = this.markFromJSON.bind(this)
|
||||
this.topNodeType = this.nodes[this.spec.topNode || "doc"]
|
||||
this.cached.wrappings = Object.create(null)
|
||||
}
|
||||
|
||||
/// The type of the [default top node](#model.SchemaSpec.topNode)
|
||||
/// for this schema.
|
||||
topNodeType: NodeType
|
||||
|
||||
/// An object for storing whatever values modules may want to
|
||||
/// compute and cache per schema. (If you want to store something
|
||||
/// in it, try to use property names unlikely to clash.)
|
||||
cached: {[key: string]: any} = Object.create(null)
|
||||
|
||||
/// Create a node in this schema. The `type` may be a string or a
|
||||
/// `NodeType` instance. Attributes will be extended with defaults,
|
||||
/// `content` may be a `Fragment`, `null`, a `Node`, or an array of
|
||||
/// nodes.
|
||||
node(type: string | NodeType,
|
||||
attrs: Attrs | null = null,
|
||||
content?: Fragment | Node | readonly Node[],
|
||||
marks?: readonly Mark[]) {
|
||||
if (typeof type == "string")
|
||||
type = this.nodeType(type)
|
||||
else if (!(type instanceof NodeType))
|
||||
throw new RangeError("Invalid node type: " + type)
|
||||
else if (type.schema != this)
|
||||
throw new RangeError("Node type from different schema used (" + type.name + ")")
|
||||
|
||||
return type.createChecked(attrs, content, marks)
|
||||
}
|
||||
|
||||
/// Create a text node in the schema. Empty text nodes are not
|
||||
/// allowed.
|
||||
text(text: string, marks?: readonly Mark[] | null): Node {
|
||||
let type = this.nodes.text
|
||||
return new TextNode(type, type.defaultAttrs, text, Mark.setFrom(marks))
|
||||
}
|
||||
|
||||
/// Create a mark with the given type and attributes.
|
||||
mark(type: string | MarkType, attrs?: Attrs | null) {
|
||||
if (typeof type == "string") type = this.marks[type]
|
||||
return type.create(attrs)
|
||||
}
|
||||
|
||||
/// Deserialize a node from its JSON representation. This method is
|
||||
/// bound.
|
||||
nodeFromJSON(json: any): Node {
|
||||
return Node.fromJSON(this, json)
|
||||
}
|
||||
|
||||
/// Deserialize a mark from its JSON representation. This method is
|
||||
/// bound.
|
||||
markFromJSON(json: any): Mark {
|
||||
return Mark.fromJSON(this, json)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
nodeType(name: string) {
|
||||
let found = this.nodes[name]
|
||||
if (!found) throw new RangeError("Unknown node type: " + name)
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
function gatherMarks(schema: Schema, marks: readonly string[]) {
|
||||
let found = []
|
||||
for (let i = 0; i < marks.length; i++) {
|
||||
let name = marks[i], mark = schema.marks[name], ok = mark
|
||||
if (mark) {
|
||||
found.push(mark)
|
||||
} else {
|
||||
for (let prop in schema.marks) {
|
||||
let mark = schema.marks[prop]
|
||||
if (name == "_" || (mark.spec.group && mark.spec.group.split(" ").indexOf(name) > -1))
|
||||
found.push(ok = mark)
|
||||
}
|
||||
}
|
||||
if (!ok) throw new SyntaxError("Unknown mark type: '" + marks[i] + "'")
|
||||
}
|
||||
return found
|
||||
}
|
||||
190
resources/app/node_modules/prosemirror-model/src/to_dom.ts
generated
vendored
Normal file
190
resources/app/node_modules/prosemirror-model/src/to_dom.ts
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
import {Fragment} from "./fragment"
|
||||
import {Node} from "./node"
|
||||
import {Schema, NodeType, MarkType} from "./schema"
|
||||
import {Mark} from "./mark"
|
||||
import {DOMNode} from "./dom"
|
||||
|
||||
/// A description of a DOM structure. Can be either a string, which is
|
||||
/// interpreted as a text node, a DOM node, which is interpreted as
|
||||
/// itself, a `{dom, contentDOM}` object, or an array.
|
||||
///
|
||||
/// An array describes a DOM element. The first value in the array
|
||||
/// should be a string—the name of the DOM element, optionally prefixed
|
||||
/// by a namespace URL and a space. If the second element is plain
|
||||
/// object, it is interpreted as a set of attributes for the element.
|
||||
/// Any elements after that (including the 2nd if it's not an attribute
|
||||
/// object) are interpreted as children of the DOM elements, and must
|
||||
/// either be valid `DOMOutputSpec` values, or the number zero.
|
||||
///
|
||||
/// The number zero (pronounced “hole”) is used to indicate the place
|
||||
/// where a node's child nodes should be inserted. If it occurs in an
|
||||
/// output spec, it should be the only child element in its parent
|
||||
/// node.
|
||||
export type DOMOutputSpec = string | DOMNode | {dom: DOMNode, contentDOM?: HTMLElement} | readonly [string, ...any[]]
|
||||
|
||||
/// A DOM serializer knows how to convert ProseMirror nodes and
|
||||
/// marks of various types to DOM nodes.
|
||||
export class DOMSerializer {
|
||||
/// Create a serializer. `nodes` should map node names to functions
|
||||
/// that take a node and return a description of the corresponding
|
||||
/// DOM. `marks` does the same for mark names, but also gets an
|
||||
/// argument that tells it whether the mark's content is block or
|
||||
/// inline content (for typical use, it'll always be inline). A mark
|
||||
/// serializer may be `null` to indicate that marks of that type
|
||||
/// should not be serialized.
|
||||
constructor(
|
||||
/// The node serialization functions.
|
||||
readonly nodes: {[node: string]: (node: Node) => DOMOutputSpec},
|
||||
/// The mark serialization functions.
|
||||
readonly marks: {[mark: string]: (mark: Mark, inline: boolean) => DOMOutputSpec}
|
||||
) {}
|
||||
|
||||
/// Serialize the content of this fragment to a DOM fragment. When
|
||||
/// not in the browser, the `document` option, containing a DOM
|
||||
/// document, should be passed so that the serializer can create
|
||||
/// nodes.
|
||||
serializeFragment(fragment: Fragment, options: {document?: Document} = {}, target?: HTMLElement | DocumentFragment) {
|
||||
if (!target) target = doc(options).createDocumentFragment()
|
||||
|
||||
let top = target!, active: [Mark, HTMLElement | DocumentFragment][] = []
|
||||
fragment.forEach(node => {
|
||||
if (active.length || node.marks.length) {
|
||||
let keep = 0, rendered = 0
|
||||
while (keep < active.length && rendered < node.marks.length) {
|
||||
let next = node.marks[rendered]
|
||||
if (!this.marks[next.type.name]) { rendered++; continue }
|
||||
if (!next.eq(active[keep][0]) || next.type.spec.spanning === false) break
|
||||
keep++; rendered++
|
||||
}
|
||||
while (keep < active.length) top = active.pop()![1]
|
||||
while (rendered < node.marks.length) {
|
||||
let add = node.marks[rendered++]
|
||||
let markDOM = this.serializeMark(add, node.isInline, options)
|
||||
if (markDOM) {
|
||||
active.push([add, top])
|
||||
top.appendChild(markDOM.dom)
|
||||
top = markDOM.contentDOM || markDOM.dom as HTMLElement
|
||||
}
|
||||
}
|
||||
}
|
||||
top.appendChild(this.serializeNodeInner(node, options))
|
||||
})
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
/// @internal
|
||||
serializeNodeInner(node: Node, options: {document?: Document}) {
|
||||
let {dom, contentDOM} =
|
||||
DOMSerializer.renderSpec(doc(options), this.nodes[node.type.name](node))
|
||||
if (contentDOM) {
|
||||
if (node.isLeaf)
|
||||
throw new RangeError("Content hole not allowed in a leaf node spec")
|
||||
this.serializeFragment(node.content, options, contentDOM)
|
||||
}
|
||||
return dom
|
||||
}
|
||||
|
||||
/// Serialize this node to a DOM node. This can be useful when you
|
||||
/// need to serialize a part of a document, as opposed to the whole
|
||||
/// document. To serialize a whole document, use
|
||||
/// [`serializeFragment`](#model.DOMSerializer.serializeFragment) on
|
||||
/// its [content](#model.Node.content).
|
||||
serializeNode(node: Node, options: {document?: Document} = {}) {
|
||||
let dom = this.serializeNodeInner(node, options)
|
||||
for (let i = node.marks.length - 1; i >= 0; i--) {
|
||||
let wrap = this.serializeMark(node.marks[i], node.isInline, options)
|
||||
if (wrap) {
|
||||
;(wrap.contentDOM || wrap.dom).appendChild(dom)
|
||||
dom = wrap.dom
|
||||
}
|
||||
}
|
||||
return dom
|
||||
}
|
||||
|
||||
/// @internal
|
||||
serializeMark(mark: Mark, inline: boolean, options: {document?: Document} = {}) {
|
||||
let toDOM = this.marks[mark.type.name]
|
||||
return toDOM && DOMSerializer.renderSpec(doc(options), toDOM(mark, inline))
|
||||
}
|
||||
|
||||
/// Render an [output spec](#model.DOMOutputSpec) to a DOM node. If
|
||||
/// the spec has a hole (zero) in it, `contentDOM` will point at the
|
||||
/// node with the hole.
|
||||
static renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS: string | null = null): {
|
||||
dom: DOMNode,
|
||||
contentDOM?: HTMLElement
|
||||
} {
|
||||
if (typeof structure == "string")
|
||||
return {dom: doc.createTextNode(structure)}
|
||||
if ((structure as DOMNode).nodeType != null)
|
||||
return {dom: structure as DOMNode}
|
||||
if ((structure as any).dom && (structure as any).dom.nodeType != null)
|
||||
return structure as {dom: DOMNode, contentDOM?: HTMLElement}
|
||||
let tagName = (structure as [string])[0], space = tagName.indexOf(" ")
|
||||
if (space > 0) {
|
||||
xmlNS = tagName.slice(0, space)
|
||||
tagName = tagName.slice(space + 1)
|
||||
}
|
||||
let contentDOM: HTMLElement | undefined
|
||||
let dom = (xmlNS ? doc.createElementNS(xmlNS, tagName) : doc.createElement(tagName)) as HTMLElement
|
||||
let attrs = (structure as any)[1], start = 1
|
||||
if (attrs && typeof attrs == "object" && attrs.nodeType == null && !Array.isArray(attrs)) {
|
||||
start = 2
|
||||
for (let name in attrs) if (attrs[name] != null) {
|
||||
let space = name.indexOf(" ")
|
||||
if (space > 0) dom.setAttributeNS(name.slice(0, space), name.slice(space + 1), attrs[name])
|
||||
else dom.setAttribute(name, attrs[name])
|
||||
}
|
||||
}
|
||||
for (let i = start; i < (structure as readonly any[]).length; i++) {
|
||||
let child = (structure as any)[i] as DOMOutputSpec | 0
|
||||
if (child === 0) {
|
||||
if (i < (structure as readonly any[]).length - 1 || i > start)
|
||||
throw new RangeError("Content hole must be the only child of its parent node")
|
||||
return {dom, contentDOM: dom}
|
||||
} else {
|
||||
let {dom: inner, contentDOM: innerContent} = DOMSerializer.renderSpec(doc, child, xmlNS)
|
||||
dom.appendChild(inner)
|
||||
if (innerContent) {
|
||||
if (contentDOM) throw new RangeError("Multiple content holes")
|
||||
contentDOM = innerContent as HTMLElement
|
||||
}
|
||||
}
|
||||
}
|
||||
return {dom, contentDOM}
|
||||
}
|
||||
|
||||
/// Build a serializer using the [`toDOM`](#model.NodeSpec.toDOM)
|
||||
/// properties in a schema's node and mark specs.
|
||||
static fromSchema(schema: Schema): DOMSerializer {
|
||||
return schema.cached.domSerializer as DOMSerializer ||
|
||||
(schema.cached.domSerializer = new DOMSerializer(this.nodesFromSchema(schema), this.marksFromSchema(schema)))
|
||||
}
|
||||
|
||||
/// Gather the serializers in a schema's node specs into an object.
|
||||
/// This can be useful as a base to build a custom serializer from.
|
||||
static nodesFromSchema(schema: Schema) {
|
||||
let result = gatherToDOM(schema.nodes)
|
||||
if (!result.text) result.text = node => node.text
|
||||
return result as {[node: string]: (node: Node) => DOMOutputSpec}
|
||||
}
|
||||
|
||||
/// Gather the serializers in a schema's mark specs into an object.
|
||||
static marksFromSchema(schema: Schema) {
|
||||
return gatherToDOM(schema.marks) as {[mark: string]: (mark: Mark, inline: boolean) => DOMOutputSpec}
|
||||
}
|
||||
}
|
||||
|
||||
function gatherToDOM(obj: {[node: string]: NodeType | MarkType}) {
|
||||
let result: {[node: string]: (value: any, inline: boolean) => DOMOutputSpec} = {}
|
||||
for (let name in obj) {
|
||||
let toDOM = obj[name].spec.toDOM
|
||||
if (toDOM) result[name] = toDOM
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function doc(options: {document?: Document}) {
|
||||
return options.document || window.document
|
||||
}
|
||||
Reference in New Issue
Block a user