prosemirror-model
Advanced tools
Comparing version 0.22.0 to 0.23.0
@@ -16,3 +16,3 @@ # How to contribute | ||
[GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues). | ||
Before reporting a bug, read these pointers. | ||
Before reporting a bug, please read these pointers. | ||
@@ -26,6 +26,7 @@ - The issue tracker is for *bugs*, not requests for help. Questions | ||
- Mention very precisely what went wrong. "X is broken" is not a good bug | ||
report. What did you expect to happen? What happened instead? Describe the | ||
exact steps a maintainer has to take to make the problem occur. We can not | ||
fix something that we can not observe. | ||
- Mention very precisely what went wrong. "X is broken" is not a good | ||
bug report. What did you expect to happen? What happened instead? | ||
Describe the exact steps a maintainer has to take to make the | ||
problem occur. A screencast can be useful, but is no substitute for | ||
a textual description. | ||
@@ -50,4 +51,4 @@ - A great way to make it easy to reproduce your problem, if it can not | ||
- Follow the code style of the rest of the project (see below). Run | ||
`npm run lint` (in the main repository checkout) that the linter is | ||
happy. | ||
`npm run lint` (in the main repository checkout) to make sure that | ||
the linter is happy. | ||
@@ -54,0 +55,0 @@ - If your changes are easy to test or likely to regress, add tests in |
{ | ||
"name": "prosemirror-model", | ||
"version": "0.22.0", | ||
"version": "0.23.0", | ||
"description": "ProseMirror's document model", | ||
@@ -22,14 +22,15 @@ "main": "dist/index.js", | ||
"devDependencies": { | ||
"buble": "^0.15.1", | ||
"mocha": "^3.0.2", | ||
"ist": "^1.0.0", | ||
"jsdom": "^10.1.0", | ||
"rimraf": "^2.5.4", | ||
"prosemirror-test-builder": "^0.22.0" | ||
"prosemirror-test-builder": "^0.23.0", | ||
"rollup": "^0.49.0", | ||
"rollup-plugin-buble": "^0.15.0" | ||
}, | ||
"scripts": { | ||
"test": "mocha test/test-*.js", | ||
"build": "rimraf dist && buble -i src -o dist --no-named-function-expr", | ||
"prepublish": "npm run build" | ||
"build": "rollup -c", | ||
"watch": "rollup -c -w", | ||
"prepare": "npm run build" | ||
} | ||
} |
@@ -9,15 +9,11 @@ # prosemirror-model | ||
This [module](http://prosemirror.net/ref.html#model) implements | ||
ProseMirror's [document model](http://prosemirror.net/guide/doc.html), | ||
This [module](http://prosemirror.net/docs/ref/#model) implements | ||
ProseMirror's [document model](http://prosemirror\.net/docs/guide/#doc), | ||
along with the mechanisms needed to support | ||
[schemas](http://prosemirror.net/guide/schema.html). | ||
[schemas](http://prosemirror\.net/docs/guide/#schema). | ||
The [project page](http://prosemirror.net) has more information, a | ||
number of [demos](http://prosemirror.net/#demos) and the | ||
[documentation](http://prosemirror.net/docs.html). | ||
number of [examples](http://prosemirror.net/examples/) and the | ||
[documentation](http://prosemirror.net/docs/). | ||
**NOTE:** This project is in *BETA* stage. It isn't thoroughly tested, | ||
and the API might still change across `0.x` releases. You are welcome | ||
to use it, but don't expect it to be very stable yet. | ||
This code is released under an | ||
@@ -24,0 +20,0 @@ [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). |
@@ -1,2 +0,2 @@ | ||
function compareDeep(a, b) { | ||
export function compareDeep(a, b) { | ||
if (a === b) return true | ||
@@ -16,2 +16,1 @@ if (!(a && typeof a == "object") || | ||
} | ||
exports.compareDeep = compareDeep |
@@ -1,456 +0,362 @@ | ||
const {Fragment} = require("./fragment") | ||
const {Mark} = require("./mark") | ||
import {Fragment} from "./fragment" | ||
class ContentExpr { | ||
constructor(nodeType, elements, inlineContent) { | ||
this.nodeType = nodeType | ||
this.elements = elements | ||
this.inlineContent = inlineContent | ||
// ::- 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 { | ||
constructor(validEnd) { | ||
// :: bool | ||
// True when this match state represents a valid end of the node. | ||
this.validEnd = validEnd | ||
this.next = [] | ||
this.wrapCache = [] | ||
} | ||
get isLeaf() { | ||
return this.elements.length == 0 | ||
static parse(string, nodeTypes) { | ||
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 | ||
} | ||
// : (?Object) → ContentMatch | ||
// The content match at the start of this expression. | ||
start(attrs) { | ||
return new ContentMatch(this, attrs, 0, 0) | ||
// :: (NodeType) → ?ContentMatch | ||
// Match a node type and marks, returning a match after that node | ||
// if successful. | ||
matchType(type) { | ||
for (let i = 0; i < this.next.length; i += 2) | ||
if (this.next[i] == type) return this.next[i + 1] | ||
return null | ||
} | ||
// : (NodeType, ?Object, ?Object) → ?ContentMatch | ||
// Try to find a match that matches the given node, anywhere in the | ||
// expression. (Useful when synthesizing a match for a node that's | ||
// open to the left.) | ||
atType(parentAttrs, type, attrs, marks = Mark.none) { | ||
for (let i = 0; i < this.elements.length; i++) | ||
if (this.elements[i].matchesType(type, attrs, marks, parentAttrs, this)) | ||
return new ContentMatch(this, parentAttrs, i, 0) | ||
// :: (Fragment, ?number, ?number) → ?ContentMatch | ||
// Try to match a fragment. Returns the resulting match when | ||
// successful. | ||
matchFragment(frag, start = 0, end = frag.childCount) { | ||
let cur = this | ||
for (let i = start; cur && i < end; i++) | ||
cur = cur.matchType(frag.child(i).type) | ||
return cur | ||
} | ||
matches(attrs, fragment, from, to) { | ||
return this.start(attrs).matchToEnd(fragment, from, to) | ||
get inlineContent() { | ||
let first = this.next[0] | ||
return first ? first.isInline : false | ||
} | ||
// Get a position in a known-valid fragment. If this is a simple | ||
// (single-element) expression, we don't have to do any matching, | ||
// and can simply skip to the position with count `index`. | ||
getMatchAt(attrs, fragment, index = fragment.childCount) { | ||
if (this.elements.length == 1) | ||
return new ContentMatch(this, attrs, 0, index) | ||
else | ||
return this.start(attrs).matchFragment(fragment, 0, index) | ||
get defaultType() { | ||
return this.next[0] | ||
} | ||
checkReplace(attrs, content, from, to, replacement = Fragment.empty, start = 0, end = replacement.childCount) { | ||
// Check for simple case, where the expression only has a single element | ||
// (Optimization to avoid matching more than we need) | ||
if (this.elements.length == 1) { | ||
let elt = this.elements[0] | ||
if (!checkCount(elt, content.childCount - (to - from) + (end - start), attrs, this)) return false | ||
for (let i = start; i < end; i++) if (!elt.matches(replacement.child(i), attrs, this)) return false | ||
return true | ||
} | ||
let match = this.getMatchAt(attrs, content, from).matchFragment(replacement, start, end) | ||
return match ? match.matchToEnd(content, to) : false | ||
compatible(other) { | ||
for (let i = 0; i < this.next.length; i += 2) | ||
for (let j = 0; j < other.next.length; j += 2) | ||
if (this.next[i] == other.next[j]) return true | ||
return false | ||
} | ||
checkReplaceWith(attrs, content, from, to, type, typeAttrs, marks) { | ||
if (this.elements.length == 1) { | ||
let elt = this.elements[0] | ||
if (!checkCount(elt, content.childCount - (to - from) + 1, attrs, this)) return false | ||
return elt.matchesType(type, typeAttrs, marks, attrs, this) | ||
// :: (Fragment, bool, ?number) → ?Fragment | ||
// 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, toEnd = false, startIndex = 0) { | ||
let seen = [this] | ||
function search(match, types) { | ||
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 += 2) { | ||
let type = match.next[i], next = match.next[i + 1] | ||
if (!type.hasRequiredAttrs() && seen.indexOf(next) == -1) { | ||
seen.push(next) | ||
let found = search(next, types.concat(type)) | ||
if (found) return found | ||
} | ||
} | ||
} | ||
let match = this.getMatchAt(attrs, content, from).matchType(type, typeAttrs, marks) | ||
return match ? match.matchToEnd(content, to) : false | ||
return search(this, []) | ||
} | ||
compatible(other) { | ||
for (let i = 0; i < this.elements.length; i++) { | ||
let elt = this.elements[i] | ||
for (let j = 0; j < other.elements.length; j++) | ||
if (other.elements[j].compatible(elt)) return true | ||
} | ||
return false | ||
// :: (NodeType) → ?[NodeType] | ||
// 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) { | ||
for (let i = 0; i < this.wrapCache.length; i += 2) | ||
if (this.wrapCache[i] == target) return this.wrapCache[i + 1] | ||
let computed = this.computeWrapping(target) | ||
this.wrapCache.push(target, computed) | ||
return computed | ||
} | ||
generateContent(attrs) { | ||
return this.start(attrs).fillBefore(Fragment.empty, true) | ||
} | ||
static parse(nodeType, expr) { | ||
let elements = [], pos = 0, inline = null | ||
for (;;) { | ||
pos += /^\s*/.exec(expr.slice(pos))[0].length | ||
if (pos == expr.length) break | ||
let types = /^(?:(\w+)|\(\s*(\w+(?:\s*\|\s*\w+)*)\s*\))/.exec(expr.slice(pos)) | ||
if (!types) throw new SyntaxError("Invalid content expression '" + expr + "' at " + pos) | ||
pos += types[0].length | ||
let attrs = /^\[([^\]]+)\]/.exec(expr.slice(pos)) | ||
if (attrs) pos += attrs[0].length | ||
let marks = /^<(?:(_)|\s*(\w+(?:\s+\w+)*)\s*)>/.exec(expr.slice(pos)) | ||
if (marks) pos += marks[0].length | ||
let repeat = /^(?:([+*?])|\{\s*(\d+|\.\w+)\s*(,\s*(\d+|\.\w+)?)?\s*\})/.exec(expr.slice(pos)) | ||
if (repeat) pos += repeat[0].length | ||
let nodeTypes = expandTypes(nodeType.schema, types[1] ? [types[1]] : types[2].split(/\s*\|\s*/)) | ||
for (let i = 0; i < nodeTypes.length; i++) { | ||
if (inline == null) inline = nodeTypes[i].isInline | ||
else if (inline != nodeTypes[i].isInline) throw new SyntaxError("Mixing inline and block content in a single node") | ||
computeWrapping(target) { | ||
let seen = Object.create(null), active = [{match: this, type: null, via: null}] | ||
while (active.length) { | ||
let current = active.shift(), match = current.match | ||
if (match.matchType(target)) { | ||
let result = [] | ||
for (let obj = current; obj.type; obj = obj.via) | ||
result.push(obj.type) | ||
return result.reverse() | ||
} | ||
let attrSet = !attrs ? null : parseAttrs(nodeType, attrs[1]) | ||
let markSet = !marks ? false : marks[1] ? true : this.gatherMarks(nodeType.schema, marks[2].split(/\s+/)) | ||
let {min, max} = parseRepeat(nodeType, repeat) | ||
if (min != 0 && (nodeTypes[0].hasRequiredAttrs(attrSet) || nodeTypes[0].isText)) | ||
throw new SyntaxError("Node type " + types[0] + " in type " + nodeType.name + | ||
" is required, but has non-optional attributes") | ||
let newElt = new ContentElement(nodeTypes, attrSet, markSet, min, max) | ||
for (let i = elements.length - 1; i >= 0; i--) { | ||
let prev = elements[i] | ||
if (prev.min != prev.max && prev.overlaps(newElt)) | ||
throw new SyntaxError("Possibly ambiguous overlapping adjacent content expressions in '" + expr + "'") | ||
if (prev.min != 0) break | ||
for (let i = 0; i < match.next.length; i += 2) { | ||
let type = match.next[i] | ||
if (!type.isLeaf && !(type.name in seen) && (!current.type || match.next[i + 1].validEnd)) { | ||
active.push({match: type.contentMatch, type, via: current}) | ||
seen[type.name] = true | ||
} | ||
} | ||
elements.push(newElt) | ||
} | ||
return new ContentExpr(nodeType, elements, !!inline) | ||
} | ||
static gatherMarks(schema, marks) { | ||
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] + "'") | ||
toString() { | ||
let seen = [] | ||
function scan(m) { | ||
seen.push(m) | ||
for (let i = 1; i < m.next.length; i += 2) | ||
if (seen.indexOf(m.next[i]) == -1) scan(m.next[i]) | ||
} | ||
return found | ||
scan(this) | ||
return seen.map((m, i) => { | ||
let out = i + (m.validEnd ? "*" : " ") + " " | ||
for (let i = 0; i < m.next.length; i += 2) | ||
out += (i ? ", " : "") + m.next[i].name + "->" + seen.indexOf(m.next[i + 1]) | ||
return out | ||
}).join("\n") | ||
} | ||
} | ||
exports.ContentExpr = ContentExpr | ||
class ContentElement { | ||
constructor(nodeTypes, attrs, marks, min, max) { | ||
ContentMatch.empty = new ContentMatch(true) | ||
class TokenStream { | ||
constructor(string, nodeTypes) { | ||
this.string = string | ||
this.nodeTypes = nodeTypes | ||
this.attrs = attrs | ||
this.marks = marks | ||
this.min = min | ||
this.max = max | ||
this.inline = null | ||
this.pos = 0 | ||
this.tokens = string.split(/\s*(?=\b|\W|$)/) | ||
if (this.tokens[this.tokens.length - 1] == "") this.tokens.pop() | ||
if (this.tokens[0] == "") this.tokens.unshift() | ||
} | ||
matchesType(type, attrs, marks, parentAttrs, parentExpr) { | ||
if (this.nodeTypes.indexOf(type) == -1) return false | ||
if (this.attrs) { | ||
if (!attrs) return false | ||
for (let prop in this.attrs) | ||
if (attrs[prop] != resolveValue(this.attrs[prop], parentAttrs, parentExpr)) return false | ||
} | ||
if (this.marks === true) return true | ||
if (this.marks === false) return marks.length == 0 | ||
for (let i = 0; i < marks.length; i++) | ||
if (this.marks.indexOf(marks[i].type) == -1) return false | ||
return true | ||
} | ||
get next() { return this.tokens[this.pos] } | ||
matches(node, parentAttrs, parentExpr) { | ||
return this.matchesType(node.type, node.attrs, node.marks, parentAttrs, parentExpr) | ||
} | ||
eat(tok) { return this.next == tok && (this.pos++ || true) } | ||
compatible(other) { | ||
for (let i = 0; i < this.nodeTypes.length; i++) | ||
if (other.nodeTypes.indexOf(this.nodeTypes[i]) != -1) return true | ||
return false | ||
} | ||
err(str) { throw new SyntaxError(str + " (in content expression '" + this.string + "')") } | ||
} | ||
constrainedAttrs(parentAttrs, expr) { | ||
if (!this.attrs) return null | ||
let attrs = Object.create(null) | ||
for (let prop in this.attrs) | ||
attrs[prop] = resolveValue(this.attrs[prop], parentAttrs, expr) | ||
return attrs | ||
} | ||
function parseExpr(stream) { | ||
let exprs = [] | ||
do { exprs.push(parseExprSeq(stream)) } | ||
while (stream.eat("|")) | ||
return exprs.length == 1 ? exprs[0] : {type: "choice", exprs} | ||
} | ||
createFiller(parentAttrs, expr) { | ||
let type = this.nodeTypes[0], attrs = type.computeAttrs(this.constrainedAttrs(parentAttrs, expr)) | ||
return type.create(attrs, type.contentExpr.generateContent(attrs)) | ||
} | ||
function parseExprSeq(stream) { | ||
let exprs = [] | ||
do { exprs.push(parseExprSubscript(stream)) } | ||
while (stream.next && stream.next != ")" && stream.next != "|") | ||
return exprs.length == 1 ? exprs[0] : {type: "seq", exprs} | ||
} | ||
defaultType() { | ||
let first = this.nodeTypes[0] | ||
if (!(first.hasRequiredAttrs() || first.isText)) return first | ||
function parseExprSubscript(stream) { | ||
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 | ||
} | ||
overlaps(other) { | ||
return this.nodeTypes.some(t => other.nodeTypes.indexOf(t) > -1) | ||
} | ||
function parseNum(stream) { | ||
if (/\D/.test(stream.next)) stream.err("Expected number, got '" + stream.next + "'") | ||
let result = Number(stream.next) | ||
stream.pos++ | ||
return result | ||
} | ||
allowsMark(markType) { | ||
return this.marks === true || this.marks && this.marks.indexOf(markType) > -1 | ||
function parseExprRange(stream, 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} | ||
} | ||
// ::- Represents a partial match of a node type's [content | ||
// expression](#model.NodeSpec), and can be used to find out whether further | ||
// content matches here, and whether a given position is a valid end | ||
// of the parent node. | ||
class ContentMatch { | ||
constructor(expr, attrs, index, count) { | ||
this.expr = expr | ||
this.attrs = attrs | ||
this.index = index | ||
this.count = count | ||
function resolveName(stream, name) { | ||
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 | ||
} | ||
get element() { return this.expr.elements[this.index] } | ||
get nextElement() { | ||
for (let i = this.index, count = this.count; i < this.expr.elements.length; i++) { | ||
let element = this.expr.elements[i] | ||
if (this.resolveValue(element.max) > count) return element | ||
count = 0 | ||
} | ||
function parseExprAtom(stream) { | ||
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} | ||
}) | ||
stream.pos++ | ||
return exprs.length == 1 ? exprs[0] : {type: "choice", exprs} | ||
} else { | ||
stream.err("Unexpected token '" + stream.next + "'") | ||
} | ||
} | ||
move(index, count) { | ||
return new ContentMatch(this.expr, this.attrs, index, count) | ||
} | ||
// 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 | ||
resolveValue(value) { | ||
return value instanceof AttrValue ? resolveValue(value, this.attrs, this.expr) : value | ||
} | ||
// : (Object) → [[{term: ?any, to: number}]] | ||
// 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) { | ||
let nfa = [[]] | ||
connect(compile(expr, 0), node()) | ||
return nfa | ||
// :: (Node) → ?ContentMatch | ||
// Match a node, returning a new match after the node if successful. | ||
matchNode(node) { | ||
return this.matchType(node.type, node.attrs, node.marks) | ||
function node() { return nfa.push([]) - 1 } | ||
function edge(from, to, term) { | ||
let edge = {term, to} | ||
nfa[from].push(edge) | ||
return edge | ||
} | ||
function connect(edges, to) { edges.forEach(edge => edge.to = to) } | ||
// :: (NodeType, ?Object, [Mark]) → ?ContentMatch | ||
// Match a node type and marks, returning an match after that node | ||
// if successful. | ||
matchType(type, attrs, marks = Mark.none) { | ||
for (let {index, count} = this; index < this.expr.elements.length; index++, count = 0) { | ||
let elt = this.expr.elements[index], max = this.resolveValue(elt.max) | ||
if (count < max && elt.matchesType(type, attrs, marks, this.attrs, this.expr)) { | ||
count++ | ||
return this.move(index, count) | ||
function compile(expr, from) { | ||
if (expr.type == "choice") { | ||
return expr.exprs.reduce((out, expr) => out.concat(compile(expr, from)), []) | ||
} 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()) | ||
} | ||
if (count < this.resolveValue(elt.min)) return null | ||
} | ||
} | ||
// :: (Fragment, ?number, ?number) → ?union<ContentMatch, bool> | ||
// Try to match a fragment. Returns a new match when successful, | ||
// `null` when it ran into a required element it couldn't fit, and | ||
// `false` if it reached the end of the expression without | ||
// matching all nodes. | ||
matchFragment(fragment, from = 0, to = fragment.childCount) { | ||
if (from == to) return this | ||
let fragPos = from, end = this.expr.elements.length | ||
for (var {index, count} = this; index < end; index++, count = 0) { | ||
let elt = this.expr.elements[index], max = this.resolveValue(elt.max) | ||
while (count < max && fragPos < to) { | ||
if (elt.matches(fragment.child(fragPos), this.attrs, this.expr)) { | ||
count++ | ||
if (++fragPos == to) return this.move(index, count) | ||
} else { | ||
break | ||
} | ||
} 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 (count < this.resolveValue(elt.min)) return null | ||
} | ||
return false | ||
} | ||
// :: (Fragment, ?number, ?number) → bool | ||
// Returns true only if the fragment matches here, and reaches all | ||
// the way to the end of the content expression. | ||
matchToEnd(fragment, start, end) { | ||
let matched = this.matchFragment(fragment, start, end) | ||
return matched && matched.validEnd() || false | ||
} | ||
// :: () → bool | ||
// Returns true if this position represents a valid end of the | ||
// expression (no required content follows after it). | ||
validEnd() { | ||
for (let i = this.index, count = this.count; i < this.expr.elements.length; i++, count = 0) | ||
if (count < this.resolveValue(this.expr.elements[i].min)) return false | ||
return true | ||
} | ||
// :: (Fragment, bool, ?number) → ?Fragment | ||
// 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, toEnd, startIndex) { | ||
let added = [], match = this, index = startIndex || 0, end = this.expr.elements.length | ||
for (;;) { | ||
let fits = match.matchFragment(after, index) | ||
if (fits && (!toEnd || fits.validEnd())) return Fragment.from(added) | ||
if (fits === false) return null // Matched to end with content remaining | ||
let elt = match.element | ||
if (match.count < this.resolveValue(elt.min)) { | ||
added.push(elt.createFiller(this.attrs, this.expr)) | ||
match = match.move(match.index, match.count + 1) | ||
} else if (match.index < end) { | ||
match = match.move(match.index + 1, 0) | ||
} else if (after.childCount > index) { | ||
return null | ||
if (expr.max == -1) { | ||
connect(compile(expr.expr, cur), cur) | ||
} else { | ||
return Fragment.from(added) | ||
} | ||
} | ||
} | ||
possibleContent() { | ||
let found = [] | ||
for (let i = this.index, count = this.count; i < this.expr.elements.length; i++, count = 0) { | ||
let elt = this.expr.elements[i], attrs = elt.constrainedAttrs(this.attrs, this.expr) | ||
if (count < this.resolveValue(elt.max)) for (let j = 0; j < elt.nodeTypes.length; j++) { | ||
let type = elt.nodeTypes[j] | ||
if (!type.hasRequiredAttrs(attrs) && !type.isText) found.push({type, attrs}) | ||
} | ||
if (this.resolveValue(elt.min) > count) break | ||
} | ||
return found | ||
} | ||
// :: (MarkType) → bool | ||
// Check whether a node with the given mark type is allowed after | ||
// this position. | ||
allowsMark(markType) { | ||
return this.element.allowsMark(markType) | ||
} | ||
// :: (NodeType, ?Object, ?[Mark]) → ?[{type: NodeType, attrs: Object}] | ||
// Find a set of wrapping node types that would allow a node of type | ||
// `target` with attributes `targetAttrs` 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, targetAttrs, targetMarks) { | ||
let seen = Object.create(null), first = {match: this, via: null}, active = [first] | ||
while (active.length) { | ||
let current = active.shift(), match = current.match | ||
if (match.matchType(target, targetAttrs, targetMarks)) { | ||
let result = [] | ||
for (let obj = current; obj != first; obj = obj.via) | ||
result.push({type: obj.match.expr.nodeType, attrs: obj.match.attrs}) | ||
return result.reverse() | ||
} | ||
let possible = match.possibleContent() | ||
for (let i = 0; i < possible.length; i++) { | ||
let {type, attrs} = possible[i], fullAttrs = type.computeAttrs(attrs) | ||
if (!type.isLeaf && !(type.name in seen) && | ||
(current == first || match.matchType(type, fullAttrs).validEnd())) { | ||
active.push({match: type.contentExpr.start(fullAttrs), via: current}) | ||
seen[type.name] = true | ||
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, null, expr.value)] | ||
} | ||
} | ||
// :: (Node) → ?[{type: NodeType, attrs: Object}] | ||
// Call [`findWrapping`](#model.ContentMatch.findWrapping) with the | ||
// properties of the given node. | ||
findWrappingFor(node) { | ||
return this.findWrapping(node.type, node.attrs, node.marks) | ||
} | ||
} | ||
exports.ContentMatch = ContentMatch | ||
class AttrValue { | ||
constructor(attr) { this.attr = attr } | ||
} | ||
function cmp(a, b) { return a - b } | ||
function parseValue(nodeType, value) { | ||
if (value.charAt(0) == ".") { | ||
let attr = value.slice(1) | ||
if (!nodeType.attrs[attr]) throw new SyntaxError("Node type " + nodeType.name + " has no attribute " + attr) | ||
return new AttrValue(attr) | ||
} else { | ||
return JSON.parse(value) | ||
} | ||
} | ||
function nullFrom(nfa, node) { | ||
let result = [] | ||
scan(node) | ||
return result.sort(cmp) | ||
function resolveValue(value, attrs, expr) { | ||
if (!(value instanceof AttrValue)) return value | ||
let attrVal = attrs && attrs[value.attr] | ||
return attrVal !== undefined ? attrVal : expr.nodeType.defaultAttrs[value.attr] | ||
} | ||
function checkCount(elt, count, attrs, expr) { | ||
return count >= resolveValue(elt.min, attrs, expr) && | ||
count <= resolveValue(elt.max, attrs, expr) | ||
} | ||
function expandTypes(schema, types) { | ||
let result = [] | ||
types.forEach(type => { | ||
let found = schema.nodes[type] | ||
if (found) { | ||
if (result.indexOf(found) == -1) result.push(found) | ||
} else { | ||
for (let name in schema.nodes) { | ||
let nodeType = schema.nodes[name] | ||
if (nodeType.groups.indexOf(type) > -1 && result.indexOf(nodeType) == -1) | ||
found = result.push(nodeType) | ||
} | ||
function scan(node) { | ||
result.push(node) | ||
for (let a = nfa[node], i = 0; i < a.length; i++) { | ||
let {term, to} = a[i] | ||
if (!term && result.indexOf(to) == -1) scan(to) | ||
} | ||
if (!found) | ||
throw new SyntaxError("Node type or group '" + type + "' does not exist") | ||
}) | ||
return result | ||
} | ||
} | ||
const many = 2e9 // Big number representable as a 32-bit int | ||
// : ([[{term: ?any, to: number}]]) → ContentMatch | ||
// 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) { | ||
let labeled = Object.create(null) | ||
return explore(nullFrom(nfa, 0)) | ||
function parseRepeat(nodeType, match) { | ||
let min = 1, max = 1 | ||
if (match) { | ||
if (match[1] == "+") { | ||
max = many | ||
} else if (match[1] == "*") { | ||
min = 0 | ||
max = many | ||
} else if (match[1] == "?") { | ||
min = 0 | ||
} else if (match[2]) { | ||
min = parseValue(nodeType, match[2]) | ||
if (match[3]) | ||
max = match[4] ? parseValue(nodeType, match[4]) : many | ||
else | ||
max = min | ||
function explore(states) { | ||
let out = [] | ||
states.forEach(node => { | ||
nfa[node].forEach(({term, to}) => { | ||
if (!term) return | ||
let known = out.indexOf(term), set = known > -1 && out[known + 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 += 2) { | ||
let states = out[i + 1].sort(cmp) | ||
state.next.push(out[i], labeled[states.join(",")] || explore(states)) | ||
} | ||
if (max == 0 || min > max) | ||
throw new SyntaxError("Invalid repeat count in '" + match[0] + "'") | ||
return state | ||
} | ||
return {min, max} | ||
} | ||
function parseAttrs(nodeType, expr) { | ||
let parts = expr.split(/\s*,\s*/) | ||
let attrs = Object.create(null) | ||
for (let i = 0; i < parts.length; i++) { | ||
let match = /^(\w+)=(\w+|\"(?:\\.|[^\\])*\"|\.\w+)$/.exec(parts[i]) | ||
if (!match) throw new SyntaxError("Invalid attribute syntax: " + parts[i]) | ||
attrs[match[1]] = parseValue(nodeType, match[2]) | ||
function checkForDeadEnds(match, stream) { | ||
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 += 2) { | ||
let node = state.next[j], next = state.next[j + 1] | ||
nodes.push(node.name) | ||
if (dead && !state.next[j].hasRequiredAttrs()) dead = false | ||
if (work.indexOf(next) == -1) work.push(next) | ||
} | ||
if (dead) stream.err("Only non-generatable nodes (" + nodes.join(", ") + ") after a match state") | ||
} | ||
return attrs | ||
} |
@@ -1,2 +0,2 @@ | ||
function findDiffStart(a, b, pos) { | ||
export function findDiffStart(a, b, pos) { | ||
for (let i = 0;; i++) { | ||
@@ -23,5 +23,4 @@ if (i == a.childCount || i == b.childCount) | ||
} | ||
exports.findDiffStart = findDiffStart | ||
function findDiffEnd(a, b, posA, posB) { | ||
export function findDiffEnd(a, b, posA, posB) { | ||
for (let iA = a.childCount, iB = b.childCount;;) { | ||
@@ -53,2 +52,1 @@ if (iA == 0 || iB == 0) | ||
} | ||
exports.findDiffEnd = findDiffEnd |
@@ -1,12 +0,14 @@ | ||
const {findDiffStart, findDiffEnd} = require("./diff") | ||
import {findDiffStart, findDiffEnd} from "./diff" | ||
// ::- Fragment is the type used to represent a node's collection of | ||
// child nodes. | ||
// ::- A fragment represents a node's collection of child nodes. | ||
// | ||
// Fragments are persistent data structures. That means you should | ||
// _not_ mutate them or their content, but create new instances | ||
// whenever needed. The API tries to make this easy. | ||
class Fragment { | ||
// 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 { | ||
constructor(content, size) { | ||
this.content = content | ||
// :: number | ||
// The size of the fragment, which is the total of the size of its | ||
// content nodes. | ||
this.size = size || 0 | ||
@@ -36,3 +38,3 @@ if (size == null) for (let i = 0; i < content.length; i++) | ||
// Call the given callback for every descendant node. The callback | ||
// may return `false` to prevent traversal of its child nodes. | ||
// may return `false` to prevent traversal of a given node's children. | ||
descendants(f) { | ||
@@ -61,4 +63,4 @@ this.nodesBetween(0, this.size, f) | ||
// :: (Fragment) → Fragment | ||
// Create a new fragment containing the content of this fragment and | ||
// `other`. | ||
// Create a new fragment containing the combined content of this | ||
// fragment and the other. | ||
append(other) { | ||
@@ -161,10 +163,2 @@ if (!other.size) return this | ||
// :: (number) → number | ||
// Get the offset at (size of children before) the given index. | ||
offsetAt(index) { | ||
let offset = 0 | ||
for (let i = 0; i < index; i++) offset += this.content[i].nodeSize | ||
return offset | ||
} | ||
// :: (number) → ?Node | ||
@@ -241,3 +235,3 @@ // Get the child node at the given index, if it exists. | ||
// Build a fragment from an array of nodes. Ensures that adjacent | ||
// text nodes with the same style are joined together. | ||
// text nodes with the same marks are joined together. | ||
static fromArray(array) { | ||
@@ -271,3 +265,2 @@ if (!array.length) return Fragment.empty | ||
} | ||
exports.Fragment = Fragment | ||
@@ -274,0 +267,0 @@ const found = {index: 0, offset: 0} |
@@ -1,7 +0,9 @@ | ||
const {Fragment} = require("./fragment") | ||
const {Slice} = require("./replace") | ||
const {Mark} = require("./mark") | ||
import {Fragment} from "./fragment" | ||
import {Slice} from "./replace" | ||
import {Mark} from "./mark" | ||
// ParseOptions:: interface | ||
// Set of options for parsing a DOM node. | ||
// These are the options recognized by the | ||
// [`parse`](#model.DOMParser.parse) and | ||
// [`parseSlice`](#model.DOMParser.parseSlice) methods. | ||
// | ||
@@ -32,11 +34,10 @@ // preserveWhitespace:: ?union<bool, "full"> | ||
// | ||
// topStart:: ?number | ||
// Can be used to influence the content match at the start of | ||
// the topnode. When given, should be a valid index into | ||
// `topNode`. | ||
// topMatch:: ?ContentMatch | ||
// Provide the starting content match that content parsed into the | ||
// top node is matched against. | ||
// | ||
// context:: ?ResolvedPos | ||
// A set of additional node names to count as | ||
// A set of additional nodes to count as | ||
// [context](#model.ParseRule.context) when parsing, above the | ||
// given [top node](#model.DOMParser.parse^options.topNode). | ||
// given [top node](#model.ParseOptions.topNode). | ||
@@ -58,4 +59,15 @@ // ParseRule:: interface | ||
// A CSS property name to match. When given, this rule matches | ||
// inline styles that list that property. | ||
// inline styles that list that property. May also have the form | ||
// `"property=value"`, in which case the rule only matches if the | ||
// propery's value exactly matches the given value. (For more | ||
// complicated filters, use [`getAttrs`](#model.ParseRule.getAttrs) | ||
// and return undefined to indicate that the match failed.) | ||
// | ||
// priority:: ?number | ||
// 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. | ||
// | ||
// context:: ?string | ||
@@ -83,9 +95,2 @@ // When given, restricts this rule to only match when the current | ||
// | ||
// priority:: ?number | ||
// 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. | ||
// | ||
// ignore:: ?bool | ||
@@ -102,3 +107,3 @@ // When true, ignore content that matches this rule. | ||
// | ||
// getAttrs:: ?(union<dom.Node, string>) → ?union<bool, Object> | ||
// getAttrs:: ?(union<dom.Node, string>) → ?union<Object, false> | ||
// A function used to compute the attributes for the node or mark | ||
@@ -113,3 +118,3 @@ // created by this rule. Can also be used to describe further | ||
// | ||
// contentElement:: ?string | ||
// contentElement:: ?union<string, (dom.Node) → dom.Node> | ||
// For `tag` rules that produce non-leaf nodes or marks, by default | ||
@@ -119,8 +124,9 @@ // the content of the DOM element is parsed as content of the mark | ||
// a CSS selector string that the parser must use to find the actual | ||
// content element. | ||
// content element, or a function that returns the actual content | ||
// element to the parser. | ||
// | ||
// getContent:: ?(dom.Node) → Fragment | ||
// Can be used to override the content of a matched node. Will be | ||
// called, and its result used, instead of parsing the node's child | ||
// nodes. | ||
// 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. | ||
// | ||
@@ -137,3 +143,3 @@ // preserveWhitespace:: ?union<bool, "full"> | ||
// is defined by an array of [rules](#model.ParseRule). | ||
class DOMParser { | ||
export class DOMParser { | ||
// :: (Schema, [ParseRule]) | ||
@@ -144,4 +150,7 @@ // Create a parser that targets the given schema, using the given | ||
// :: Schema | ||
// The schema into which the parser parses. | ||
this.schema = schema | ||
// :: [ParseRule] | ||
// The set of [parse rules](#model.ParseRule) that the parser | ||
// uses, in order of precedence. | ||
this.rules = rules | ||
@@ -182,3 +191,3 @@ this.tags = [] | ||
if (matches(dom, rule.tag) && | ||
(!rule.namespace || dom.namespaceURI == rule.namespace) && | ||
(rule.namespace === undefined || dom.namespaceURI == rule.namespace) && | ||
(!rule.context || context.matchesContext(rule.context))) { | ||
@@ -198,16 +207,20 @@ if (rule.getAttrs) { | ||
let rule = this.styles[i] | ||
if (rule.style == prop && (!rule.context || context.matchesContext(rule.context))) { | ||
if (rule.getAttrs) { | ||
let result = rule.getAttrs(value) | ||
if (result === false) continue | ||
rule.attrs = result | ||
} | ||
return rule | ||
if (rule.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. | ||
rule.style.length > prop.length && | ||
(rule.style.charCodeAt(prop.length) != 61 || rule.style.slice(prop.length + 1) != value)) | ||
continue | ||
if (rule.getAttrs) { | ||
let result = rule.getAttrs(value) | ||
if (result === false) continue | ||
rule.attrs = result | ||
} | ||
return rule | ||
} | ||
} | ||
// :: (Schema) → [ParseRule] | ||
// Extract the parse rules listed in a schema's [node | ||
// specs](#model.NodeSpec.parseDOM). | ||
// : (Schema) → [ParseRule] | ||
static schemaRules(schema) { | ||
@@ -243,3 +256,4 @@ let result = [] | ||
// Construct a DOM parser using the parsing rules listed in a | ||
// schema's [node specs](#model.NodeSpec.parseDOM). | ||
// schema's [node specs](#model.NodeSpec.parseDOM), reordered by | ||
// [priority](#model.ParseRule.priority). | ||
static fromSchema(schema) { | ||
@@ -250,3 +264,2 @@ return schema.cached.domParser || | ||
} | ||
exports.DOMParser = DOMParser | ||
@@ -282,3 +295,3 @@ // : Object<bool> The block-level tags in HTML5 | ||
this.solid = solid | ||
this.match = match || (options & OPT_OPEN_LEFT ? null : type.contentExpr.start(attrs)) | ||
this.match = match || (options & OPT_OPEN_LEFT ? null : type.contentMatch) | ||
this.options = options | ||
@@ -288,17 +301,19 @@ this.content = [] | ||
findWrapping(type, attrs) { | ||
findWrapping(node) { | ||
if (!this.match) { | ||
if (!this.type) return [] | ||
let found = this.type.contentExpr.atType(this.attrs, type, attrs) | ||
if (!found) { | ||
let start = this.type.contentExpr.start(this.attrs), wrap | ||
if (wrap = start.findWrapping(type, attrs)) { | ||
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 | ||
} | ||
} | ||
if (found) this.match = found | ||
else return null | ||
} | ||
return this.match.findWrapping(type, attrs) | ||
return this.match.findWrapping(node.type) | ||
} | ||
@@ -333,3 +348,3 @@ | ||
topContext = new NodeContext(topNode.type, topNode.attrs, true, | ||
topNode.contentMatchAt(options.topStart || 0), topOptions) | ||
options.topMatch || topNode.type.contentMatch, topOptions) | ||
else if (open) | ||
@@ -376,3 +391,3 @@ topContext = new NodeContext(null, null, true, null, topOptions) | ||
let top = this.top | ||
if ((top.type && top.type.inlineContent) || /\S/.test(value)) { | ||
if ((top.type ? top.type.inlineContent : top.content.length && top.content[0].isInline) || /\S/.test(value)) { | ||
if (!(top.options & OPT_PRESERVE_WS)) { | ||
@@ -460,2 +475,3 @@ value = value.replace(/\s+/g, " ") | ||
if (typeof contentDOM == "string") contentDOM = dom.querySelector(contentDOM) | ||
else if (typeof contentDOM == "function") contentDOM = contentDOM(dom) | ||
if (!contentDOM) contentDOM = dom | ||
@@ -490,13 +506,13 @@ this.findAround(dom, contentDOM, true) | ||
// nodes that we're in. | ||
findPlace(type, attrs) { | ||
findPlace(node) { | ||
let route, sync | ||
for (let depth = this.open; depth >= 0; depth--) { | ||
let node = this.nodes[depth] | ||
let found = node.findWrapping(type, attrs) | ||
let cx = this.nodes[depth] | ||
let found = cx.findWrapping(node) | ||
if (found && (!route || route.length > found.length)) { | ||
route = found | ||
sync = node | ||
sync = cx | ||
if (!found.length) break | ||
} | ||
if (node.solid) break | ||
if (cx.solid) break | ||
} | ||
@@ -506,3 +522,3 @@ if (!route) return false | ||
for (let i = 0; i < route.length; i++) | ||
this.enterInner(route[i].type, route[i].attrs, false) | ||
this.enterInner(route[i], null, false) | ||
return true | ||
@@ -518,12 +534,8 @@ } | ||
} | ||
if (this.findPlace(node.type, node.attrs)) { | ||
if (this.findPlace(node)) { | ||
this.closeExtra() | ||
let top = this.top | ||
if (top.match) { | ||
let match = top.match.matchNode(node) | ||
if (!match) { | ||
node = node.mark(node.marks.filter(mark => top.match.allowsMark(mark.type))) | ||
match = top.match.matchNode(node) | ||
} | ||
top.match = match | ||
top.match = top.match.matchType(node.type) | ||
if (top.type) node = node.mark(top.type.allowedMarks(node.marks)) | ||
} | ||
@@ -538,3 +550,3 @@ top.content.push(node) | ||
enter(type, attrs, preserveWS) { | ||
let ok = this.findPlace(type, attrs) | ||
let ok = this.findPlace(type.create(attrs)) | ||
if (ok) this.enterInner(type, attrs, true, preserveWS) | ||
@@ -541,0 +553,0 @@ return ok |
@@ -1,11 +0,11 @@ | ||
exports.Node = require("./node").Node | ||
;({ResolvedPos: exports.ResolvedPos, NodeRange: exports.NodeRange} = require("./resolvedpos")) | ||
exports.Fragment = require("./fragment").Fragment | ||
;({Slice: exports.Slice, ReplaceError: exports.ReplaceError} = require("./replace")) | ||
exports.Mark = require("./mark").Mark | ||
export {Node} from "./node" | ||
export {ResolvedPos, NodeRange} from "./resolvedpos" | ||
export {Fragment} from "./fragment" | ||
export {Slice, ReplaceError} from "./replace" | ||
export {Mark} from "./mark" | ||
;({Schema: exports.Schema, NodeType: exports.NodeType, MarkType: exports.MarkType} = require("./schema")) | ||
;({ContentMatch: exports.ContentMatch} = require("./content")) | ||
export {Schema, NodeType, MarkType} from "./schema" | ||
export {ContentMatch} from "./content" | ||
exports.DOMParser = require("./from_dom").DOMParser | ||
exports.DOMSerializer = require("./to_dom").DOMSerializer | ||
export {DOMParser} from "./from_dom" | ||
export {DOMSerializer} from "./to_dom" |
@@ -1,2 +0,2 @@ | ||
const {compareDeep} = require("./comparedeep") | ||
import {compareDeep} from "./comparedeep" | ||
@@ -9,3 +9,3 @@ // ::- A mark is a piece of information that can be attached to a node, | ||
// attributes they have. | ||
class Mark { | ||
export class Mark { | ||
constructor(type, attrs) { | ||
@@ -23,5 +23,5 @@ // :: MarkType | ||
// well, in the right position. If this mark is already in the set, | ||
// the set itself is returned. If a mark of this type with different | ||
// attributes is already in the set, a set in which it is replaced | ||
// by this one is returned. | ||
// 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) { | ||
@@ -115,5 +115,4 @@ let copy, placed = false | ||
} | ||
exports.Mark = Mark | ||
// :: [Mark] The empty set of marks. | ||
Mark.none = [] |
@@ -1,6 +0,6 @@ | ||
const {Fragment} = require("./fragment") | ||
const {Mark} = require("./mark") | ||
const {Slice, replace} = require("./replace") | ||
const {ResolvedPos} = require("./resolvedpos") | ||
const {compareDeep} = require("./comparedeep") | ||
import {Fragment} from "./fragment" | ||
import {Mark} from "./mark" | ||
import {Slice, replace} from "./replace" | ||
import {ResolvedPos} from "./resolvedpos" | ||
import {compareDeep} from "./comparedeep" | ||
@@ -19,5 +19,5 @@ const emptyAttrs = Object.create(null) | ||
// | ||
// **Never** directly mutate the properties of a `Node` object. See | ||
// [this guide](/docs/guides/doc/) for more information. | ||
class Node { | ||
// **Do not** directly mutate the properties of a `Node` object. See | ||
// [the guide](/docs/guide/#doc) for more information. | ||
export class Node { | ||
constructor(type, attrs, content, marks) { | ||
@@ -30,4 +30,4 @@ // :: NodeType | ||
// An object mapping attribute names to values. The kind of | ||
// attributes allowed and required are determined by the node | ||
// type. | ||
// attributes allowed and required are | ||
// [determined](#model.NodeSpec.attrs) by the node type. | ||
this.attrs = attrs | ||
@@ -41,3 +41,3 @@ | ||
// The marks (things like whether it is emphasized or part of a | ||
// link) associated with this node. | ||
// link) applied to this node. | ||
this.marks = marks || Mark.none | ||
@@ -51,4 +51,4 @@ } | ||
// The size of this node, as defined by the integer-based [indexing | ||
// scheme](/docs/guides/doc/#indexing). For text nodes, this is the | ||
// amount of characters. For other leaf nodes, it is one. And for | ||
// 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 | ||
@@ -78,6 +78,7 @@ // and end token). | ||
// 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 parent-relative position, | ||
// its parent node, and its child index. If the callback returns false, | ||
// the current node's children will not be recursed over. | ||
// the given two positions that are relative to start of this node's | ||
// content. The callback is invoked with the node, its | ||
// parent-relative position, 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. | ||
nodesBetween(from, to, f, pos = 0) { | ||
@@ -88,4 +89,4 @@ this.content.nodesBetween(from, to, f, pos, this) | ||
// :: ((node: Node, pos: number, parent: Node) → ?bool) | ||
// Call the given callback for every descendant node. If doesn't | ||
// descend into a child node when the callback returns `false`. | ||
// Call the given callback for every descendant node. Doesn't | ||
// descend into a node when the callback returns `false`. | ||
descendants(f) { | ||
@@ -120,3 +121,3 @@ this.nodesBetween(0, this.content.size, f) | ||
// :: (Node) → bool | ||
// Test whether two nodes represent the same content. | ||
// Test whether two nodes represent the same piece of document. | ||
eq(other) { | ||
@@ -159,3 +160,3 @@ return this == other || (this.sameMarkup(other) && this.content.eq(other.content)) | ||
// Create a copy of this node with only the content between the | ||
// given offsets. If `to` is not given, it defaults to the end of | ||
// given positions. If `to` is not given, it defaults to the end of | ||
// the node. | ||
@@ -192,3 +193,3 @@ cut(from, to) { | ||
// :: (number) → ?Node | ||
// Find the node after the given position. | ||
// Find the node starting at the given position. | ||
nodeAt(pos) { | ||
@@ -226,4 +227,4 @@ for (let node = this;;) { | ||
// :: (number) → ResolvedPos | ||
// Resolve the given position in the document, returning an object | ||
// describing its path through the document. | ||
// Resolve the given position in the document, returning an | ||
// [object](#model.ResolvedPos) with information about its context. | ||
resolve(pos) { return ResolvedPos.resolveCached(this, pos) } | ||
@@ -274,4 +275,4 @@ | ||
// editable content. This is usually the same as `isLeaf`, but can | ||
// be configured with the [`leaf` property](#model.NodeSpec.leaf) on | ||
// a node's spec (typically when the node is displayed as an | ||
// 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)). | ||
@@ -293,13 +294,17 @@ get isAtom() { return this.type.isAtom } | ||
contentMatchAt(index) { | ||
return this.type.contentExpr.getMatchAt(this.attrs, this.content, index) | ||
return this.type.contentMatch.matchFragment(this.content, 0, index) | ||
} | ||
// :: (number, number, ?Fragment, ?number, ?number) → bool | ||
// Test whether replacing the range `from` to `to` (by 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, to, replacement, start, end) { | ||
return this.type.contentExpr.checkReplace(this.attrs, this.content, from, to, replacement, start, end) | ||
// 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, to, 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 | ||
} | ||
@@ -309,6 +314,8 @@ | ||
// Test whether replacing the range `from` to `to` (by index) with a | ||
// node of the given type with the given attributes and marks would | ||
// be valid. | ||
canReplaceWith(from, to, type, attrs, marks) { | ||
return this.type.contentExpr.checkReplaceWith(this.attrs, this.content, from, to, type, attrs, marks || Mark.none) | ||
// node of the given type. | ||
canReplaceWith(from, to, type, marks) { | ||
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 | ||
} | ||
@@ -327,4 +334,3 @@ | ||
defaultContentType(at) { | ||
let elt = this.contentMatchAt(at).nextElement | ||
return elt && elt.defaultType() | ||
return this.contentMatchAt(at).defaultType | ||
} | ||
@@ -336,3 +342,3 @@ | ||
check() { | ||
if (!this.type.validContent(this.content, this.attrs)) | ||
if (!this.type.validContent(this.content)) | ||
throw new RangeError(`Invalid content for node ${this.type.name}: ${this.content.toString().slice(0, 50)}`) | ||
@@ -367,5 +373,4 @@ this.content.forEach(node => node.check()) | ||
} | ||
exports.Node = Node | ||
class TextNode extends Node { | ||
export class TextNode extends Node { | ||
constructor(type, attrs, content, marks) { | ||
@@ -388,3 +393,3 @@ super(type, attrs, null, marks) | ||
mark(marks) { | ||
return new TextNode(this.type, this.attrs, this.text, marks) | ||
return marks == this.marks ? this : new TextNode(this.type, this.attrs, this.text, marks) | ||
} | ||
@@ -412,3 +417,2 @@ | ||
} | ||
exports.TextNode = TextNode | ||
@@ -415,0 +419,0 @@ function wrapMarks(marks, str) { |
This module defines ProseMirror's content model, the data structures | ||
used to represent and manipulate documents. | ||
used to represent and work with documents. | ||
@@ -7,3 +7,3 @@ ### Document Structure | ||
A ProseMirror document is a tree. At each level, a [node](#model.Node) | ||
tags the type of content that's there, and holds a | ||
describes the type of the content, and holds a | ||
[fragment](#model.Fragment) containing its children. | ||
@@ -28,6 +28,6 @@ | ||
The schema to which a document must conform is another data structure. | ||
It describes the [nodes](#model.NodeSpec) and [marks](#model.MarkSpec) | ||
that may appear in a document, and the places at which they may | ||
appear. | ||
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. | ||
@@ -53,3 +53,3 @@ @Schema | ||
(But note that you do _not_ need to have a DOM implementation loaded | ||
to load this module.) | ||
to use this module.) | ||
@@ -56,0 +56,0 @@ @DOMParser |
@@ -1,6 +0,6 @@ | ||
const {Fragment} = require("./fragment") | ||
import {Fragment} from "./fragment" | ||
// ::- Error type raised by [`Node.replace`](#model.Node.replace) when | ||
// given an invalid replacement. | ||
class ReplaceError extends Error { | ||
export class ReplaceError extends Error { | ||
constructor(message) { | ||
@@ -12,11 +12,20 @@ super(message) | ||
} | ||
exports.ReplaceError = 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. | ||
class Slice { | ||
// both side are ‘open’ (cut through). | ||
export class Slice { | ||
// :: (Fragment, number, number) | ||
// 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(content, openStart, openEnd) { | ||
// :: Fragment The slice's content nodes. | ||
// :: Fragment The slice's content. | ||
this.content = content | ||
@@ -81,3 +90,2 @@ // :: number The open depth at the start. | ||
} | ||
exports.Slice = Slice | ||
@@ -109,3 +117,3 @@ function removeRange(content, from, to) { | ||
function replace($from, $to, slice) { | ||
export function replace($from, $to, slice) { | ||
if (slice.openStart > $from.depth) | ||
@@ -117,3 +125,2 @@ throw new ReplaceError("Inserted content deeper than insertion position") | ||
} | ||
exports.replace = replace | ||
@@ -173,3 +180,3 @@ function replaceOuter($from, $to, slice, depth) { | ||
function close(node, content) { | ||
if (!node.type.validContent(content, node.attrs)) | ||
if (!node.type.validContent(content)) | ||
throw new ReplaceError("Invalid content for node " + node.type.name) | ||
@@ -176,0 +183,0 @@ return node.copy(content) |
@@ -1,7 +0,7 @@ | ||
const {Mark} = require("./mark") | ||
import {Mark} from "./mark" | ||
// ::- You'll often have to '[resolve](#model.Node.resolve)' a | ||
// position to get the context you need. Objects of this class | ||
// represent such a resolved position, providing various pieces of | ||
// context information and helper methods. | ||
// ::- 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. | ||
// | ||
@@ -11,3 +11,3 @@ // Throughout this interface, methods that take an optional `depth` | ||
// numbers as `this.depth + value`. | ||
class ResolvedPos { | ||
export class ResolvedPos { | ||
constructor(pos, path, parentOffset) { | ||
@@ -19,4 +19,4 @@ // :: number The position that was resolved. | ||
// The number of levels the parent node is from the root. If this | ||
// position points directly into the root, it is 0. If it points | ||
// into a top-level paragraph, 1, and so on. | ||
// position points directly into the root node, it is 0. If it | ||
// points into a top-level paragraph, 1, and so on. | ||
this.depth = path.length / 3 - 1 | ||
@@ -36,3 +36,3 @@ // :: number The offset this position has into its parent node. | ||
// a position points into a text node, that node is not considered | ||
// the parent—text nodes are 'flat' in this model. | ||
// the parent—text nodes are ‘flat’ in this model, and have no content. | ||
get parent() { return this.node(this.depth) } | ||
@@ -80,4 +80,4 @@ | ||
// :: (?number) → number | ||
// The (absolute) position directly before the node at the given | ||
// level, or, when `level` is `this.depth + 1`, the original | ||
// The (absolute) position directly before the wrapping node at the | ||
// given level, or, when `level` is `this.depth + 1`, the original | ||
// position. | ||
@@ -91,5 +91,4 @@ before(depth) { | ||
// :: (?number) → number | ||
// The (absolute) position directly after the node at the given | ||
// level, or, when `level` is `this.depth + 1`, the original | ||
// position. | ||
// The (absolute) position directly after the wrapping node at the | ||
// given level, or the original position when `level` is `this.depth + 1`. | ||
after(depth) { | ||
@@ -129,8 +128,8 @@ depth = this.resolveDepth(depth) | ||
// :: (?bool) → [Mark] | ||
// :: () → [Mark] | ||
// 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, or `after` is true, | ||
// the marks of the node after it (if any) are returned. | ||
marks(after) { | ||
// position is at the start of a non-empty node, the marks of the | ||
// node after it (if any) are returned. | ||
marks() { | ||
let parent = this.parent, index = this.index() | ||
@@ -147,3 +146,3 @@ | ||
// the node after this position the main reference. | ||
if ((after && other) || !main) { let tmp = main; main = other; other = tmp } | ||
if (!main) { let tmp = main; main = other; other = tmp } | ||
@@ -160,2 +159,20 @@ // Use all marks in the main node, except those that have | ||
// :: () → ?[Mark] | ||
// 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) { | ||
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 | ||
} | ||
// :: (number) → number | ||
@@ -175,6 +192,5 @@ // The depth up to which this position and the given (non-resolved) | ||
// will be returned. If they point into different blocks, the range | ||
// around those blocks or their ancestors in their common 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. | ||
// 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 = this, pred) { | ||
@@ -239,8 +255,8 @@ if (other.pos < this.pos) return other.blockRange(this) | ||
} | ||
exports.ResolvedPos = ResolvedPos | ||
let resolveCache = [], resolveCachePos = 0, resolveCacheSize = 6 | ||
// ::- Represents a flat range of content. | ||
class NodeRange { | ||
// ::- Represents a flat range of content, i.e. one that starts and | ||
// ends in the same node. | ||
export class NodeRange { | ||
// :: (ResolvedPos, ResolvedPos, number) | ||
@@ -276,2 +292,1 @@ // Construct a node range. `$from` and `$to` should point into the | ||
} | ||
exports.NodeRange = NodeRange |
@@ -1,7 +0,7 @@ | ||
const OrderedMap = require("orderedmap") | ||
import OrderedMap from "orderedmap" | ||
const {Node, TextNode} = require("./node") | ||
const {Fragment} = require("./fragment") | ||
const {Mark} = require("./mark") | ||
const {ContentExpr} = require("./content") | ||
import {Node, TextNode} from "./node" | ||
import {Fragment} from "./fragment" | ||
import {Mark} from "./mark" | ||
import {ContentMatch} from "./content" | ||
@@ -16,3 +16,3 @@ // For node types where all attrs have a default value (or which don't | ||
let attr = attrs[attrName] | ||
if (attr.default === undefined) return null | ||
if (!attr.hasDefault) return null | ||
defaults[attrName] = attr.default | ||
@@ -27,10 +27,6 @@ } | ||
let given = value && value[name] | ||
if (given == null) { | ||
if (given === undefined) { | ||
let attr = attrs[name] | ||
if (attr.default !== undefined) | ||
given = attr.default | ||
else if (attr.compute) | ||
given = attr.compute() | ||
else | ||
throw new RangeError("No value supplied for attribute " + name) | ||
if (attr.hasDefault) given = attr.default | ||
else throw new RangeError("No value supplied for attribute " + name) | ||
} | ||
@@ -49,6 +45,6 @@ built[name] = given | ||
// ::- Node types are objects allocated once per `Schema` and used to | ||
// tag `Node` instances with a type. They contain information about | ||
// the node type, such as its name and what kind of node it | ||
// [tag](#model.Node.type) `Node` instances. They contain information | ||
// about the node type, such as its name and what kind of node it | ||
// represents. | ||
class NodeType { | ||
export class NodeType { | ||
constructor(name, schema, spec) { | ||
@@ -71,5 +67,17 @@ // :: string | ||
this.defaultAttrs = defaultAttrs(this.attrs) | ||
this.contentExpr = null | ||
// :: ContentMatch | ||
// The starting match of the node type's content expression. | ||
this.contentMatch = null | ||
// : ?[MarkType] | ||
// The set of marks allowed in this node. `null` means all marks | ||
// are allowed. | ||
this.markSet = null | ||
// :: bool | ||
// True if this node type has inline content. | ||
this.inlineContent = null | ||
// :: bool | ||
// True if this is a block type | ||
@@ -90,11 +98,7 @@ this.isBlock = !(spec.inline || name == "text") | ||
// content. | ||
get isTextblock() { return this.isBlock && this.contentExpr.inlineContent } | ||
get isTextblock() { return this.isBlock && this.inlineContent } | ||
// :: bool | ||
// True if this node type has inline content. | ||
get inlineContent() { return this.contentExpr.inlineContent } | ||
// :: bool | ||
// True for node types that allow no content. | ||
get isLeaf() { return this.contentExpr.isLeaf } | ||
get isLeaf() { return this.contentMatch == ContentMatch.empty } | ||
@@ -113,3 +117,3 @@ // :: bool | ||
compatibleContent(other) { | ||
return this == other || this.contentExpr.compatible(other.contentExpr) | ||
return this == other || this.contentMatch.compatible(other.contentMatch) | ||
} | ||
@@ -140,7 +144,6 @@ | ||
createChecked(attrs, content, marks) { | ||
attrs = this.computeAttrs(attrs) | ||
content = Fragment.from(content) | ||
if (!this.validContent(content, attrs)) | ||
if (!this.validContent(content)) | ||
throw new RangeError("Invalid content for node " + this.name) | ||
return new Node(this, attrs, content, Mark.setFrom(marks)) | ||
return new Node(this, this.computeAttrs(attrs), content, Mark.setFrom(marks)) | ||
} | ||
@@ -159,7 +162,7 @@ | ||
if (content.size) { | ||
let before = this.contentExpr.start(attrs).fillBefore(content) | ||
let before = this.contentMatch.fillBefore(content) | ||
if (!before) return null | ||
content = before.append(content) | ||
} | ||
let after = this.contentExpr.getMatchAt(attrs, content).fillBefore(Fragment.empty, true) | ||
let after = this.contentMatch.matchFragment(content).fillBefore(Fragment.empty, true) | ||
if (!after) return null | ||
@@ -169,9 +172,42 @@ return new Node(this, attrs, content.append(after), Mark.setFrom(marks)) | ||
// :: (Fragment, ?Object) → bool | ||
// :: (Fragment) → bool | ||
// Returns true if the given fragment is valid content for this node | ||
// type with the given attributes. | ||
validContent(content, attrs) { | ||
return this.contentExpr.matches(attrs, content) | ||
validContent(content) { | ||
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 | ||
} | ||
// :: (MarkType) → bool | ||
// Check whether the given mark type is allowed in this node. | ||
allowsMarkType(markType) { | ||
return this.markSet == null || this.markSet.indexOf(markType) > -1 | ||
} | ||
// :: ([Mark]) → bool | ||
// Test whether the given set of marks are allowed in this node. | ||
allowsMarks(marks) { | ||
if (this.markSet == null) return true | ||
for (let i = 0; i < marks.length; i++) if (!this.allowsMarkType(marks[i])) return false | ||
return true | ||
} | ||
// :: ([Mark]) → [Mark] | ||
// Removes the marks that are not allowed in this node from the given set. | ||
allowedMarks(marks) { | ||
if (this.markSet == null) return marks | ||
let copy | ||
for (let i = 0; i < marks.length; i++) { | ||
if (!this.allowsMarkType(marks[i])) { | ||
if (!copy) copy = marks.slice(0, i) | ||
} else if (copy) { | ||
copy.push(marks[i]) | ||
} | ||
} | ||
return !copy ? marks : copy.length ? copy : Mark.empty | ||
} | ||
static compile(nodes, schema) { | ||
@@ -189,3 +225,2 @@ let result = Object.create(null) | ||
} | ||
exports.NodeType = NodeType | ||
@@ -196,8 +231,8 @@ // Attribute descriptors | ||
constructor(options) { | ||
this.hasDefault = Object.prototype.hasOwnProperty.call(options, "default") | ||
this.default = options.default | ||
this.compute = options.compute | ||
} | ||
get isRequired() { | ||
return this.default === undefined && !this.compute | ||
return !this.hasDefault | ||
} | ||
@@ -209,5 +244,6 @@ } | ||
// ::- Like nodes, marks (which are associated with nodes to signify | ||
// things like emphasis or being part of a link) are tagged with type | ||
// objects, which are instantiated once per `Schema`. | ||
class MarkType { | ||
// 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 { | ||
constructor(name, rank, schema, spec) { | ||
@@ -273,15 +309,20 @@ // :: string | ||
} | ||
exports.MarkType = MarkType | ||
// SchemaSpec:: interface | ||
// An object describing a schema, as passed to the `Schema` | ||
// An object describing a schema, as passed to the [`Schema`](#model.Schema) | ||
// constructor. | ||
// | ||
// nodes:: union<Object<NodeSpec>, OrderedMap<NodeSpec>> | ||
// The node types in this schema. Maps names to `NodeSpec` objects | ||
// describing the node to be associated with that name. Their order | ||
// is significant | ||
// 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). | ||
// | ||
// marks:: ?union<Object<MarkSpec>, OrderedMap<MarkSpec>> | ||
// The mark types that exist in this schema. | ||
// 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. | ||
// | ||
@@ -296,12 +337,19 @@ // topNode:: ?string | ||
// The content expression for this node, as described in the [schema | ||
// guide](/docs/guides/schema/). When not given, the node does not allow | ||
// any content. | ||
// guide](/docs/guide/#schema.content_expressions). When not given, | ||
// the node does not allow any content. | ||
// | ||
// marks:: ?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. | ||
// | ||
// group:: ?string | ||
// The group or space-separated groups to which this node belongs, as | ||
// referred to in the content expressions for the schema. | ||
// The group or space-separated groups to which this node belongs, | ||
// which can be referred to in the content expressions for the | ||
// schema. | ||
// | ||
// inline:: ?bool | ||
// Should be set to a truthy value for inline nodes. (Implied for | ||
// text nodes.) | ||
// Should be set to true for inline nodes. (Implied for text nodes.) | ||
// | ||
@@ -317,4 +365,4 @@ // atom:: ?bool | ||
// selectable:: ?bool | ||
// Controls whether nodes of this type can be selected (as a [node | ||
// selection](#state.NodeSelection)). Defaults to true for non-text | ||
// Controls whether nodes of this type can be selected as a [node | ||
// selection](#state.NodeSelection). Defaults to true for non-text | ||
// nodes. | ||
@@ -335,6 +383,6 @@ // | ||
// whereas defining nodes persist and wrap the inserted content. | ||
// Likewise, the the _inserted_ content, when not inserting into a | ||
// textblock, the defining parents of the content are preserved. | ||
// Typically, non-default-paragraph textblock types, and possible | ||
// list items, are marked as defining. | ||
// Likewise, 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. | ||
// | ||
@@ -345,3 +393,3 @@ // isolating:: ?bool | ||
// backspacing or lifting, won't cross. An example of a node that | ||
// should probably have this set is a table cell. | ||
// should probably have this enabled is a table cell. | ||
// | ||
@@ -352,6 +400,6 @@ // toDOM:: ?(node: Node) → DOMOutputSpec | ||
// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)). | ||
// Should return an [array structure](#model.DOMOutputSpec) that | ||
// describes the resulting DOM structure, with an optional number | ||
// zero (“hole”) in it to indicate where the node's content should | ||
// be inserted. | ||
// 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. | ||
// | ||
@@ -378,3 +426,4 @@ // For text nodes, the default is to create a text DOM node. Though | ||
// Whether this mark should be active when the cursor is positioned | ||
// at the start or end boundary of the mark. Defaults to true. | ||
// at its end (or at its start when that is also the start of the | ||
// parent node). Defaults to true. | ||
// | ||
@@ -384,3 +433,3 @@ // excludes:: ?string | ||
// 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 | ||
// 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 | ||
@@ -398,3 +447,3 @@ // any mark that excludes the new mark but is not, itself, excluded | ||
// group:: ?string | ||
// The group or space-separated groups to which this node belongs. | ||
// The group or space-separated groups to which this mark belongs. | ||
// | ||
@@ -412,19 +461,20 @@ // toDOM:: ?(mark: Mark, inline: bool) → DOMOutputSpec | ||
// | ||
// Used to define attributes. Attributes that have no default or | ||
// compute property must be provided whenever a node or mark of a type | ||
// that has them is created. | ||
// Used to [define](#model.NodeSpec.attrs) attributes on nodes or | ||
// marks. | ||
// | ||
// The following fields are supported: | ||
// | ||
// default:: ?any | ||
// The default value for this attribute, to choose when no | ||
// explicit value is provided. | ||
// | ||
// compute:: ?() → any | ||
// A function that computes a default value for the attribute. | ||
// 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. | ||
// ::- A document schema. | ||
class Schema { | ||
let warnedAboutMarkSyntax = false | ||
// ::- 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. | ||
export class Schema { | ||
// :: (SchemaSpec) | ||
// Construct a schema from a specification. | ||
// Construct a schema from a schema [specification](#model.SchemaSpec). | ||
constructor(spec) { | ||
@@ -436,3 +486,3 @@ // :: SchemaSpec | ||
// [`OrderedMap`](https://github.com/marijnh/orderedmap) instances | ||
// (not raw objects or null). | ||
// (not raw objects). | ||
this.spec = {} | ||
@@ -454,17 +504,23 @@ for (let prop in spec) this.spec[prop] = spec[prop] | ||
throw new RangeError(prop + " can not be both a node and a mark") | ||
let type = this.nodes[prop] | ||
type.contentExpr = ContentExpr.parse(type, this.spec.nodes.get(prop).content || "") | ||
let type = this.nodes[prop], contentExpr = type.spec.content || "", markExpr = type.spec.marks | ||
let oldStyle = /<(.*?)>/.test(contentExpr) | ||
if (oldStyle) { | ||
if (!warnedAboutMarkSyntax && typeof console != "undefined" && console.warn) { | ||
warnedAboutMarkSyntax = true | ||
console.warn("Angle-bracket syntax for marks in content expressions is deprecated. Use the `marks` spec property instead.") | ||
} | ||
markExpr = oldStyle[1] | ||
contentExpr = contentExpr.replace(/<(.*?)>/g, "") | ||
} | ||
type.contentMatch = ContentMatch.parse(contentExpr, this.nodes) | ||
type.inlineContent = type.contentMatch.inlineContent | ||
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 == "" ? [] : ContentExpr.gatherMarks(this, excl.split(" ")) | ||
type.excluded = excl == null ? [type] : excl == "" ? [] : gatherMarks(this, excl.split(" ")) | ||
} | ||
// :: Object | ||
// 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.) | ||
this.cached = Object.create(null) | ||
this.cached.wrappings = Object.create(null) | ||
this.nodeFromJSON = this.nodeFromJSON.bind(this) | ||
@@ -477,2 +533,9 @@ this.markFromJSON = this.markFromJSON.bind(this) | ||
this.topNodeType = this.nodes[this.spec.topNode || "doc"] | ||
// :: Object | ||
// 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.) | ||
this.cached = Object.create(null) | ||
this.cached.wrappings = Object.create(null) | ||
} | ||
@@ -531,2 +594,19 @@ | ||
} | ||
exports.Schema = Schema | ||
function gatherMarks(schema, marks) { | ||
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 | ||
} |
@@ -6,16 +6,18 @@ // DOMOutputSpec:: interface | ||
// | ||
// An array describes a DOM element. The first element in the array | ||
// should be a string, and is the name of the DOM element. If the | ||
// second element is a non-Array, non-DOM node object, it is | ||
// interpreted as an object providing the DOM element's attributes. | ||
// 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. | ||
// An array describes a DOM element. The first value in the array | ||
// should be a string—the name of the DOM element. If the second | ||
// element is plain object object, it is interpreted as an 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 ProseMirror node's content should be inserted. | ||
// where a node's child nodes should be inserted. It it occurs in an | ||
// output spec, it should be the only child element in its parent | ||
// node. | ||
// ::- A DOM serializer knows how to convert ProseMirror nodes and | ||
// marks of various types to DOM nodes. | ||
class DOMSerializer { | ||
export class DOMSerializer { | ||
// :: (Object<(node: Node) → DOMOutputSpec>, Object<?(mark: Mark, inline: bool) → DOMOutputSpec>) | ||
@@ -31,4 +33,6 @@ // Create a serializer. `nodes` should map node names to functions | ||
// :: Object<(node: Node) → DOMOutputSpec> | ||
// The node serialization functions. | ||
this.nodes = nodes || {} | ||
// :: Object<(mark: Mark) → DOMOutputSpec> | ||
// :: Object<?(mark: Mark, inline: bool) → DOMOutputSpec> | ||
// The mark serialization functions. | ||
this.marks = marks || {} | ||
@@ -74,3 +78,3 @@ } | ||
// [`serializeFragment`](#model.DOMSerializer.serializeFragment) on | ||
// its [`content`](#model.Node.content). | ||
// its [content](#model.Node.content). | ||
serializeNode(node, options = {}) { | ||
@@ -98,3 +102,5 @@ return this.renderStructure(this.nodes[node.type.name](node), node, options) | ||
// :: (dom.Document, DOMOutputSpec) → {dom: dom.Node, contentDOM: ?dom.Node} | ||
// Render an [output spec](#model.DOMOutputSpec). | ||
// 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, structure) { | ||
@@ -153,3 +159,3 @@ if (typeof structure == "string") | ||
// :: (Schema) → Object<(node: Node) → DOMOutputSpec> | ||
// : (Schema) → Object<(node: Node) → DOMOutputSpec> | ||
// Gather the serializers in a schema's node specs into an object. | ||
@@ -163,3 +169,3 @@ // This can be useful as a base to build a custom serializer from. | ||
// :: (Schema) → Object<(mark: Mark) → DOMOutputSpec> | ||
// : (Schema) → Object<(mark: Mark) → DOMOutputSpec> | ||
// Gather the serializers in a schema's mark specs into an object. | ||
@@ -170,3 +176,2 @@ static marksFromSchema(schema) { | ||
} | ||
exports.DOMSerializer = DOMSerializer | ||
@@ -173,0 +178,0 @@ function gatherToDOM(obj) { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
581794
32
8726
24