@milkdown/transformer
Advanced tools
Comparing version 6.5.4 to 7.0.0-next.0
@@ -1,268 +0,244 @@ | ||
var A = Object.defineProperty; | ||
var C = (e, t, r) => t in e ? A(e, t, { enumerable: !0, configurable: !0, writable: !0, value: r }) : e[t] = r; | ||
var i = (e, t, r) => (C(e, typeof t != "symbol" ? t + "" : t, r), r), U = (e, t, r) => { | ||
if (!t.has(e)) | ||
throw TypeError("Cannot " + r); | ||
var W = Object.defineProperty; | ||
var X = (o, n, t) => n in o ? W(o, n, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[n] = t; | ||
var h = (o, n, t) => (X(o, typeof n != "symbol" ? n + "" : n, t), t), K = (o, n, t) => { | ||
if (!n.has(o)) | ||
throw TypeError("Cannot " + t); | ||
}; | ||
var u = (e, t, r) => { | ||
if (t.has(e)) | ||
var i = (o, n, t) => (K(o, n, "read from private field"), t ? t.call(o) : n.get(o)), c = (o, n, t) => { | ||
if (n.has(o)) | ||
throw TypeError("Cannot add the same private member more than once"); | ||
t instanceof WeakSet ? t.add(e) : t.set(e, r); | ||
}; | ||
var h = (e, t, r) => (U(e, t, "access private method"), r); | ||
import { stackOverFlow as $, createNodeInParserFail as q, parserMatchError as x, serializerMatchError as B } from "@milkdown/exception"; | ||
import { Mark as w } from "@milkdown/prose/model"; | ||
const E = () => { | ||
const e = (o) => o.elements.length, t = (o) => o.elements[e(o) - 1]; | ||
return { | ||
size: e, | ||
top: t, | ||
push: (o) => (c) => { | ||
var a; | ||
(a = t(o)) == null || a.push(c); | ||
}, | ||
open: (o) => (c) => { | ||
o.elements.push(c); | ||
}, | ||
close: (o) => { | ||
const c = o.elements.pop(); | ||
if (!c) | ||
throw $(); | ||
return c; | ||
} | ||
}; | ||
}, G = (e, t, ...r) => { | ||
e.content.push(t, ...r); | ||
}, H = (e) => e.content.pop(), K = (e, t, r) => { | ||
const s = { | ||
type: e, | ||
content: t, | ||
attrs: r, | ||
push: (...n) => G(s, ...n), | ||
pop: () => H(s) | ||
}; | ||
return s; | ||
}, { size: L, push: Q, top: V, open: W, close: X } = E(), v = (e) => e.isText, Y = (e, t, r) => { | ||
if (v(t) && v(r) && w.sameSet(t.marks, r.marks)) | ||
return e.text(t.text + r.text, t.marks); | ||
}, Z = (e) => (t, r) => W(e)(K(t, [], r)), T = (e) => (t, r, s) => { | ||
const n = t.createAndFill(r, s, e.marks); | ||
if (!n) | ||
throw q(t, r, s); | ||
return Q(e)(n), n; | ||
}, b = (e) => () => { | ||
e.marks = w.none; | ||
const t = X(e); | ||
return T(e)(t.type, t.attrs, t.content); | ||
}, _ = (e) => (t, r) => { | ||
const s = t.create(r); | ||
e.marks = s.addToSet(e.marks); | ||
}, tt = (e) => (t) => { | ||
e.marks = t.removeFromSet(e.marks); | ||
}, et = (e) => (t) => { | ||
const r = V(e); | ||
if (!r) | ||
throw $(); | ||
const s = r.pop(), n = e.schema.text(t, e.marks); | ||
if (!s) { | ||
r.push(n); | ||
return; | ||
n instanceof WeakSet ? n.add(o) : n.set(o, t); | ||
}, m = (o, n, t, e) => (K(o, n, "write to private field"), e ? e.call(o, t) : n.set(o, t), t); | ||
import { stackOverFlow as L, parserMatchError as Y, createNodeInParserFail as Z, serializerMatchError as $ } from "@milkdown/exception"; | ||
import { Mark as T } from "@milkdown/prose/model"; | ||
class Q { | ||
} | ||
class U { | ||
constructor() { | ||
h(this, "elements", []); | ||
h(this, "size", () => this.elements.length); | ||
h(this, "top", () => this.elements.at(-1)); | ||
h(this, "push", (n) => { | ||
var t; | ||
(t = this.top()) == null || t.push(n); | ||
}); | ||
h(this, "open", (n) => { | ||
this.elements.push(n); | ||
}); | ||
h(this, "close", () => { | ||
const n = this.elements.pop(); | ||
if (!n) | ||
throw L(); | ||
return n; | ||
}); | ||
} | ||
const o = Y(e.schema, s, n); | ||
if (o) { | ||
r.push(o); | ||
return; | ||
} | ||
class z extends Q { | ||
constructor(n, t, e) { | ||
super(), this.type = n, this.content = t, this.attrs = e; | ||
} | ||
r.push(s, n); | ||
}, rt = (e) => () => { | ||
let t; | ||
do | ||
t = b(e)(); | ||
while (L(e)); | ||
return t; | ||
}, st = (e) => { | ||
const t = { | ||
marks: [], | ||
elements: [], | ||
schema: e | ||
}; | ||
return { | ||
build: rt(t), | ||
openMark: _(t), | ||
closeMark: tt(t), | ||
addText: et(t), | ||
openNode: Z(t), | ||
addNode: T(t), | ||
closeNode: b(t) | ||
}; | ||
}; | ||
var m, P, f, z; | ||
class nt { | ||
constructor(t, r, s) { | ||
u(this, m); | ||
u(this, f); | ||
i(this, "run", (t, r) => { | ||
const s = t.runSync(t.parse(r), r); | ||
push(n, ...t) { | ||
this.content.push(n, ...t); | ||
} | ||
pop() { | ||
return this.content.pop(); | ||
} | ||
static create(n, t, e) { | ||
return new z(n, t, e); | ||
} | ||
} | ||
var a, v, F, A, E, w, x; | ||
const B = class extends U { | ||
constructor(t) { | ||
super(); | ||
h(this, "schema"); | ||
c(this, a, T.none); | ||
c(this, v, (t) => t.isText); | ||
c(this, F, (t, e) => { | ||
if (i(this, v).call(this, t) && i(this, v).call(this, e) && T.sameSet(t.marks, e.marks)) | ||
return this.schema.text(t.text + e.text, t.marks); | ||
}); | ||
c(this, A, (t) => { | ||
const e = Object.values({ ...this.schema.nodes, ...this.schema.marks }).find((s) => s.spec.parseMarkdown.match(t)); | ||
if (!e) | ||
throw Y(t); | ||
return e; | ||
}); | ||
c(this, E, (t) => { | ||
const e = i(this, A).call(this, t); | ||
e.spec.parseMarkdown.runner(this, t, e); | ||
}); | ||
h(this, "injectRoot", (t, e, s) => (this.openNode(e, s), this.next(t.children), this)); | ||
h(this, "openNode", (t, e) => (this.open(z.create(t, [], e)), this)); | ||
c(this, w, () => { | ||
m(this, a, T.none); | ||
const t = this.close(); | ||
return i(this, x).call(this, t.type, t.attrs, t.content); | ||
}); | ||
h(this, "closeNode", () => (i(this, w).call(this), this)); | ||
c(this, x, (t, e, s) => { | ||
const r = t.createAndFill(e, s, i(this, a)); | ||
if (!r) | ||
throw Z(t, e, s); | ||
return this.push(r), r; | ||
}); | ||
h(this, "addNode", (t, e, s) => (i(this, x).call(this, t, e, s), this)); | ||
h(this, "openMark", (t, e) => { | ||
const s = t.create(e); | ||
return m(this, a, s.addToSet(i(this, a))), this; | ||
}); | ||
h(this, "closeMark", (t) => (m(this, a, t.removeFromSet(i(this, a))), this)); | ||
h(this, "addText", (t) => { | ||
const e = this.top(); | ||
if (!e) | ||
throw L(); | ||
const s = e.pop(), r = this.schema.text(t, i(this, a)); | ||
if (!s) | ||
return e.push(r), this; | ||
const p = i(this, F).call(this, s, r); | ||
return p ? (e.push(p), this) : (e.push(s, r), this); | ||
}); | ||
h(this, "build", () => { | ||
let t; | ||
do | ||
t = i(this, w).call(this); | ||
while (this.size()); | ||
return t; | ||
}); | ||
h(this, "next", (t = []) => ([t].flat().forEach((e) => i(this, E).call(this, e)), this)); | ||
h(this, "toDoc", () => this.build()); | ||
h(this, "run", (t, e) => { | ||
const s = t.runSync(t.parse(e), e); | ||
return this.next(s), this; | ||
}); | ||
i(this, "next", (t = []) => ([t].flat().forEach((r) => h(this, f, z).call(this, r)), this)); | ||
i(this, "toDoc", () => this.stack.build()); | ||
i(this, "injectRoot", (t, r, s) => (this.stack.openNode(r, s), this.next(t.children), this)); | ||
i(this, "addText", (t = "") => (this.stack.addText(t), this)); | ||
i(this, "addNode", (...t) => (this.stack.addNode(...t), this)); | ||
i(this, "openNode", (...t) => (this.stack.openNode(...t), this)); | ||
i(this, "closeNode", (...t) => (this.stack.closeNode(...t), this)); | ||
i(this, "openMark", (...t) => (this.stack.openMark(...t), this)); | ||
i(this, "closeMark", (...t) => (this.stack.closeMark(...t), this)); | ||
this.stack = t, this.schema = r, this.specMap = s; | ||
this.schema = t; | ||
} | ||
} | ||
m = new WeakSet(), P = function(t) { | ||
const r = Object.values(this.specMap).find((s) => s.match(t)); | ||
if (!r) | ||
throw x(t); | ||
return r; | ||
}, f = new WeakSet(), z = function(t) { | ||
const { key: r, runner: s, is: n } = h(this, m, P).call(this, t), o = this.schema[n === "node" ? "nodes" : "marks"][r]; | ||
s(this, t, o); | ||
}; | ||
const vt = (e, t, r) => { | ||
const s = new nt(st(e), e, t); | ||
return (n) => (s.run(r, n), s.toDoc()); | ||
}, ot = (e, t, ...r) => { | ||
e.children || (e.children = []), e.children.push(t, ...r); | ||
}, ct = (e) => { | ||
var t; | ||
return (t = e.children) == null ? void 0 : t.pop(); | ||
}, O = (e, t, r, s = {}) => { | ||
const n = { | ||
type: e, | ||
children: t, | ||
props: s, | ||
value: r, | ||
push: (...o) => ot(n, ...o), | ||
pop: () => ct(n) | ||
}; | ||
return n; | ||
}, { size: it, push: at, open: ht, close: ut } = E(), pt = (e, t) => { | ||
var c; | ||
if (e.type === t || ((c = e.children) == null ? void 0 : c.length) !== 1) | ||
return e; | ||
const r = (a) => { | ||
var d; | ||
if (a.type === t) | ||
return a; | ||
if (((d = a.children) == null ? void 0 : d.length) !== 1) | ||
return null; | ||
const [p] = a.children; | ||
return p ? r(p) : null; | ||
}, s = r(e); | ||
if (!s) | ||
return e; | ||
const n = s.children ? [...s.children] : void 0, o = { ...e, children: n }; | ||
return o.children = n, s.children = [o], s; | ||
}, F = (e) => { | ||
const { children: t } = e; | ||
return t && (e.children = t.reduce((r, s, n) => { | ||
if (n === 0) | ||
return [s]; | ||
const o = r[r.length - 1]; | ||
if (o && o.isMark && s.isMark) { | ||
s = pt(s, o.type); | ||
const { children: c, ...a } = s, { children: p, ...d } = o; | ||
if (s.type === o.type && c && p && JSON.stringify(a) === JSON.stringify(d)) { | ||
const J = { | ||
...d, | ||
children: [...p, ...c] | ||
}; | ||
return r.slice(0, -1).concat(F(J)); | ||
} | ||
} | ||
return r.concat(s); | ||
}, [])), e; | ||
}, dt = (e) => { | ||
const t = { | ||
...e.props, | ||
type: e.type | ||
}; | ||
return e.children && (t.children = e.children), e.value && (t.value = e.value), t; | ||
}, I = (e) => (t, r, s) => ht(e)(O(t, [], r, s)), j = (e) => (t, r, s, n) => { | ||
const o = O(t, r, s, n), c = F(dt(o)); | ||
return at(e)(c), c; | ||
}, S = (e) => () => { | ||
const t = ut(e); | ||
return j(e)(t.type, t.children, t.value, t.props); | ||
}, lt = (e) => (t, r, s, n) => { | ||
t.isInSet(e.marks) || (e.marks = t.addToSet(e.marks), I(e)(r, s, { ...n, isMark: !0 })); | ||
}, kt = (e) => (t) => t.isInSet(e.marks) ? (e.marks = t.type.removeFromSet(e.marks), S(e)()) : null, mt = (e) => () => { | ||
let t = null; | ||
do | ||
t = S(e)(); | ||
while (it(e)); | ||
return t; | ||
}, ft = () => { | ||
const e = { | ||
marks: [], | ||
elements: [] | ||
}; | ||
return { | ||
build: mt(e), | ||
openMark: lt(e), | ||
closeMark: kt(e), | ||
openNode: I(e), | ||
addNode: j(e), | ||
closeNode: S(e) | ||
}; | ||
}, Nt = (e) => Object.prototype.hasOwnProperty.call(e, "size"); | ||
var l, y, N, R, M, D, k, g; | ||
class Mt { | ||
constructor(t, r, s) { | ||
u(this, l); | ||
u(this, N); | ||
u(this, M); | ||
u(this, k); | ||
i(this, "toString", (t) => t.stringify(this.stack.build())); | ||
i(this, "next", (t) => Nt(t) ? (t.forEach((r) => { | ||
h(this, k, g).call(this, r); | ||
}), this) : (h(this, k, g).call(this, t), this)); | ||
i(this, "addNode", (...t) => (this.stack.addNode(...t), this)); | ||
i(this, "openNode", (...t) => (this.stack.openNode(...t), this)); | ||
i(this, "closeNode", (...t) => (this.stack.closeNode(...t), this)); | ||
i(this, "withMark", (...t) => (this.stack.openMark(...t), this)); | ||
this.stack = t, this.schema = r, this.specMap = s; | ||
let S = B; | ||
a = new WeakMap(), v = new WeakMap(), F = new WeakMap(), A = new WeakMap(), E = new WeakMap(), w = new WeakMap(), x = new WeakMap(), h(S, "create", (t, e) => { | ||
const s = new B(t); | ||
return (r) => (s.run(e, r), s.toDoc()); | ||
}); | ||
const G = class extends Q { | ||
constructor(t, e, s, r = {}) { | ||
super(); | ||
h(this, "push", (t, ...e) => { | ||
this.children || (this.children = []), this.children.push(t, ...e); | ||
}); | ||
h(this, "pop", () => { | ||
var t; | ||
return (t = this.children) == null ? void 0 : t.pop(); | ||
}); | ||
this.type = t, this.children = e, this.value = s, this.props = r; | ||
} | ||
run(t) { | ||
return this.next(t), this; | ||
}; | ||
let M = G; | ||
h(M, "create", (t, e, s, r = {}) => new G(t, e, s, r)); | ||
const _ = (o) => Object.prototype.hasOwnProperty.call(o, "size"); | ||
var d, g, j, R, P, C, b, D, y, I, J, O; | ||
const H = class extends U { | ||
constructor(t) { | ||
super(); | ||
c(this, d, T.none); | ||
h(this, "schema"); | ||
c(this, g, (t) => { | ||
const e = Object.values({ ...this.schema.nodes, ...this.schema.marks }).find((s) => s.spec.toMarkdown.match(t)); | ||
if (!e) | ||
throw $(t.type); | ||
return e; | ||
}); | ||
c(this, j, (t) => i(this, g).call(this, t).spec.toMarkdown.runner(this, t)); | ||
c(this, R, (t, e) => i(this, g).call(this, t).spec.toMarkdown.runner(this, t, e)); | ||
c(this, P, (t) => { | ||
const { marks: e } = t, s = (u) => u.type.spec.priority ?? 50; | ||
[...e].sort((u, l) => s(u) - s(l)).every((u) => !i(this, R).call(this, u, t)) && i(this, j).call(this, t), e.forEach((u) => i(this, O).call(this, u)); | ||
}); | ||
c(this, C, (t, e) => { | ||
var l; | ||
if (t.type === e || ((l = t.children) == null ? void 0 : l.length) !== 1) | ||
return t; | ||
const s = (f) => { | ||
var k; | ||
if (f.type === e) | ||
return f; | ||
if (((k = f.children) == null ? void 0 : k.length) !== 1) | ||
return null; | ||
const [N] = f.children; | ||
return N ? s(N) : null; | ||
}, r = s(t); | ||
if (!r) | ||
return t; | ||
const p = r.children ? [...r.children] : void 0, u = { ...t, children: p }; | ||
return u.children = p, r.children = [u], r; | ||
}); | ||
c(this, b, (t) => { | ||
const { children: e } = t; | ||
return e && (t.children = e.reduce((s, r, p) => { | ||
if (p === 0) | ||
return [r]; | ||
const u = s.at(-1); | ||
if (u && u.isMark && r.isMark) { | ||
r = i(this, C).call(this, r, u.type); | ||
const { children: l, ...f } = r, { children: N, ...k } = u; | ||
if (r.type === u.type && l && N && JSON.stringify(f) === JSON.stringify(k)) { | ||
const V = { | ||
...k, | ||
children: [...N, ...l] | ||
}; | ||
return s.slice(0, -1).concat(i(this, b).call(this, V)); | ||
} | ||
} | ||
return s.concat(r); | ||
}, [])), t; | ||
}); | ||
c(this, D, (t) => { | ||
const e = { | ||
...t.props, | ||
type: t.type | ||
}; | ||
return t.children && (e.children = t.children), t.value && (e.value = t.value), e; | ||
}); | ||
h(this, "openNode", (t, e, s) => (this.open(M.create(t, void 0, e, s)), this)); | ||
c(this, y, () => { | ||
const t = this.close(); | ||
return i(this, I).call(this, t.type, t.children, t.value, t.props); | ||
}); | ||
h(this, "closeNode", () => (i(this, y).call(this), this)); | ||
c(this, I, (t, e, s, r) => { | ||
const p = M.create(t, e, s, r), u = i(this, b).call(this, i(this, D).call(this, p)); | ||
return this.push(u), u; | ||
}); | ||
h(this, "addNode", (t, e, s, r) => (i(this, I).call(this, t, e, s, r), this)); | ||
c(this, J, (t, e, s, r) => t.isInSet(i(this, d)) ? this : (m(this, d, t.addToSet(i(this, d))), this.openNode(e, s, { ...r, isMark: !0 }))); | ||
c(this, O, (t) => { | ||
!t.isInSet(i(this, d)) || (m(this, d, t.type.removeFromSet(i(this, d))), i(this, y).call(this)); | ||
}); | ||
h(this, "withMark", (t, e, s, r) => (i(this, J).call(this, t, e, s, r), this)); | ||
h(this, "closeMark", (t) => (i(this, O).call(this, t), this)); | ||
h(this, "build", () => { | ||
let t = null; | ||
do | ||
t = i(this, y).call(this); | ||
while (this.size()); | ||
return t; | ||
}); | ||
h(this, "next", (t) => _(t) ? (t.forEach((e) => { | ||
i(this, P).call(this, e); | ||
}), this) : (i(this, P).call(this, t), this)); | ||
h(this, "toString", (t) => t.stringify(this.build())); | ||
h(this, "run", (t) => (this.next(t), this)); | ||
this.schema = t; | ||
} | ||
} | ||
l = new WeakSet(), y = function(t) { | ||
const r = Object.entries(this.specMap).map(([s, n]) => ({ | ||
key: s, | ||
...n | ||
})).find((s) => s.match(t)); | ||
if (!r) | ||
throw B(t.type); | ||
return r; | ||
}, N = new WeakSet(), R = function(t) { | ||
const { runner: r } = h(this, l, y).call(this, t); | ||
r(this, t); | ||
}, M = new WeakSet(), D = function(t, r) { | ||
const { runner: s } = h(this, l, y).call(this, t); | ||
return s(this, t, r); | ||
}, k = new WeakSet(), g = function(t) { | ||
const { marks: r } = t, s = (c) => { | ||
var a; | ||
return (a = c.type.spec.priority) != null ? a : 50; | ||
}; | ||
[...r].sort((c, a) => s(c) - s(a)).every((c) => !h(this, M, D).call(this, c, t)) && h(this, N, R).call(this, t), r.forEach((c) => this.stack.closeMark(c)); | ||
}; | ||
const $t = (e, t, r) => (s) => { | ||
const n = new Mt(ft(), e, t); | ||
return n.run(s), n.toString(r); | ||
}; | ||
let q = H; | ||
d = new WeakMap(), g = new WeakMap(), j = new WeakMap(), R = new WeakMap(), P = new WeakMap(), C = new WeakMap(), b = new WeakMap(), D = new WeakMap(), y = new WeakMap(), I = new WeakMap(), J = new WeakMap(), O = new WeakMap(), h(q, "create", (t, e) => { | ||
const s = new H(t); | ||
return (r) => (s.run(r), s.toString(e)); | ||
}); | ||
export { | ||
vt as createParser, | ||
$t as createSerializer, | ||
E as getStackUtil | ||
S as ParserState, | ||
q as SerializerState, | ||
U as Stack, | ||
Q as StackElement | ||
}; | ||
//# sourceMappingURL=index.es.js.map |
@@ -1,6 +0,3 @@ | ||
import type { Node, Schema } from '@milkdown/prose/model'; | ||
import type { RemarkParser } from '../utility'; | ||
import type { InnerParserSpecMap } from './types'; | ||
export declare const createParser: (schema: Schema, specMap: InnerParserSpecMap, remark: RemarkParser) => (text: string) => Node; | ||
export * from './types'; | ||
export * from './state'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,11 +0,12 @@ | ||
import type { Node, NodeType } from '@milkdown/prose/model'; | ||
import type { Attrs } from './types'; | ||
export interface StackElement { | ||
import type { Attrs, Node, NodeType } from '@milkdown/prose/model'; | ||
import { StackElement } from '../utility'; | ||
export declare class ParserStackElement extends StackElement<Node> { | ||
type: NodeType; | ||
content: Node[]; | ||
attrs?: Attrs; | ||
push: (node: Node, ...rest: Node[]) => void; | ||
pop: () => Node | undefined; | ||
attrs?: Attrs | undefined; | ||
constructor(type: NodeType, content: Node[], attrs?: Attrs | undefined); | ||
push(node: Node, ...rest: Node[]): void; | ||
pop(): Node | undefined; | ||
static create(type: NodeType, content: Node[], attrs?: Attrs): ParserStackElement; | ||
} | ||
export declare const createElement: (type: NodeType, content: Node[], attrs?: Attrs) => StackElement; | ||
//# sourceMappingURL=stack-element.d.ts.map |
@@ -1,105 +0,23 @@ | ||
import type { MarkType, Node, NodeType, Schema } from '@milkdown/prose/model'; | ||
import type { RemarkParser } from '../utility'; | ||
import type { Stack } from './stack'; | ||
import type { Attrs, InnerParserSpecMap, MarkdownNode } from './types'; | ||
/** | ||
* State for parser. | ||
* Transform remark AST into prosemirror state. | ||
*/ | ||
export declare class State { | ||
import type { Attrs, MarkType, Node, NodeType, Schema } from '@milkdown/prose/model'; | ||
import type { MarkdownNode, RemarkParser } from '../utility'; | ||
import { Stack } from '../utility'; | ||
import { ParserStackElement } from './stack-element'; | ||
import type { Parser } from './types'; | ||
export declare class ParserState extends Stack<Node, ParserStackElement> { | ||
#private; | ||
private readonly stack; | ||
readonly schema: Schema; | ||
private readonly specMap; | ||
constructor(stack: Stack, schema: Schema, specMap: InnerParserSpecMap); | ||
/** | ||
* Transform a markdown string into prosemirror state. | ||
* | ||
* @param remark - The remark parser used. | ||
* @param markdown - The markdown string needs to be parsed. | ||
* @returns The state instance. | ||
*/ | ||
run: (remark: RemarkParser, markdown: string) => this; | ||
/** | ||
* Give the node or node list back to the state and the state will find a proper runner (by `match` method) to handle it. | ||
* | ||
* @param nodes - The node or node list needs to be handled. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
next: (nodes?: MarkdownNode | MarkdownNode[]) => this; | ||
/** | ||
* Parse current remark AST into prosemirror state. | ||
* | ||
* @returns Result prosemirror doc. | ||
*/ | ||
toDoc: () => Node; | ||
/** | ||
* Inject root node for prosemirror state. | ||
* | ||
* @param node - The target markdown node. | ||
* @param nodeType - The root prosemirror nodeType . | ||
* @param attrs - The attribute of root type. | ||
* @returns The state instance. | ||
*/ | ||
static create: (schema: Schema, remark: RemarkParser) => Parser; | ||
protected constructor(schema: Schema); | ||
injectRoot: (node: MarkdownNode, nodeType: NodeType, attrs?: Attrs) => this; | ||
/** | ||
* Add a text type prosemirror node. | ||
* | ||
* @param text - Text string. | ||
* @returns The state instance. | ||
*/ | ||
addText: (text?: string) => this; | ||
/** | ||
* Add a node without open or close it. | ||
* | ||
* @remarks | ||
* It's useful for nodes which don't have content. | ||
* | ||
* @param nodeType - Node type of this node. | ||
* @param attrs - Attributes of this node. | ||
* @param content - Content of this node. | ||
* | ||
* @returns The added node. | ||
*/ | ||
addNode: (nodeType: NodeType, attrs?: Attrs | undefined, content?: Node[] | undefined) => this; | ||
/** | ||
* Open a node, and all nodes created after this method will be set as the children of the node until a `closeNode` been called. | ||
* | ||
* @remarks | ||
* You can imagine `openNode` as the left half of parenthesis and `closeNode` as the right half. For nodes have children, your runner should just take care of the node itself and let other runners to handle the children. | ||
* | ||
* @param nodeType - Node type of this node. | ||
* @param attrs - Attributes of this node. | ||
* | ||
* @returns | ||
*/ | ||
openNode: (nodeType: NodeType, attrs?: Attrs | undefined) => this; | ||
/** | ||
* Close current node. | ||
* | ||
* @returns The node closed. | ||
*/ | ||
openNode: (nodeType: NodeType, attrs?: Attrs) => this; | ||
closeNode: () => this; | ||
/** | ||
* Open a mark, and all marks created after this method will be set as the children of the mark until a `closeMark` been called. | ||
* | ||
* @remarks | ||
* You can imagine `openMark` as the left half of parenthesis and `closeMark` as the right half. For nodes have children, your runner should just take care of the node itself and let other runners to handle the children. | ||
* | ||
* @param markType - Mark type of this mark. | ||
* @param attrs - Attributes of this mark. | ||
* | ||
* @returns | ||
*/ | ||
openMark: (markType: MarkType, attrs?: Attrs | undefined) => this; | ||
/** | ||
* Close target mark. | ||
* | ||
* @param markType - Mark type of this mark. | ||
* | ||
* @returns The mark closed. | ||
*/ | ||
addNode: (nodeType: NodeType, attrs?: Attrs, content?: Node[]) => this; | ||
openMark: (markType: MarkType, attrs?: Attrs) => this; | ||
closeMark: (markType: MarkType) => this; | ||
addText: (text: string) => this; | ||
build: () => Node; | ||
next: (nodes?: MarkdownNode | MarkdownNode[]) => this; | ||
toDoc: () => Node; | ||
run: (remark: RemarkParser, markdown: string) => this; | ||
} | ||
//# sourceMappingURL=state.d.ts.map |
@@ -1,24 +0,13 @@ | ||
import type { MarkType, NodeType } from '@milkdown/prose/model'; | ||
import type { Node } from 'unist'; | ||
import type { State } from './state'; | ||
export type Attrs = Record<string, string | number | boolean | null>; | ||
export type MarkdownNode = Node & { | ||
children?: MarkdownNode[]; | ||
[x: string]: unknown; | ||
import type { MarkType, Node, NodeType } from '@milkdown/prose/model'; | ||
import type { MarkdownNode } from '../utility/types'; | ||
import type { ParserState } from './state'; | ||
export type Parser = (text: string) => Node; | ||
export type NodeParserSpec = { | ||
match: (node: MarkdownNode) => boolean; | ||
runner: (state: ParserState, node: MarkdownNode, proseType: NodeType) => void; | ||
}; | ||
export type ParserRunner<T extends NodeType | MarkType = NodeType | MarkType> = (state: State, Node: MarkdownNode, proseType: T) => void; | ||
export interface ParserSpec<T extends NodeType | MarkType = NodeType | MarkType> { | ||
export type MarkParserSpec = { | ||
match: (node: MarkdownNode) => boolean; | ||
runner: ParserRunner<T>; | ||
} | ||
export type NodeParserSpec = ParserSpec<NodeType>; | ||
export type MarkParserSpec = ParserSpec<MarkType>; | ||
export type ParserSpecWithType = (NodeParserSpec & { | ||
is: 'node'; | ||
key: string; | ||
}) | (MarkParserSpec & { | ||
is: 'mark'; | ||
key: string; | ||
}); | ||
export type InnerParserSpecMap = Record<string, ParserSpecWithType>; | ||
runner: (state: ParserState, node: MarkdownNode, proseType: MarkType) => void; | ||
}; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,6 +0,3 @@ | ||
import type { Node, Schema } from '@milkdown/prose/model'; | ||
import type { RemarkParser } from '../utility'; | ||
import type { InnerSerializerSpecMap } from './types'; | ||
export declare const createSerializer: (schema: Schema, specMap: InnerSerializerSpecMap, remark: RemarkParser) => (content: Node) => string; | ||
export * from './types'; | ||
export * from './state'; | ||
//# sourceMappingURL=index.d.ts.map |
import type { MarkdownNode } from '..'; | ||
import type { JSONRecord } from '../utility'; | ||
export interface StackElement { | ||
import { StackElement } from '../utility'; | ||
export declare class SerializerStackElement extends StackElement<MarkdownNode> { | ||
type: string; | ||
value?: string; | ||
children?: MarkdownNode[] | undefined; | ||
value?: string | undefined; | ||
props: JSONRecord; | ||
children?: MarkdownNode[]; | ||
constructor(type: string, children?: MarkdownNode[] | undefined, value?: string | undefined, props?: JSONRecord); | ||
static create: (type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord) => SerializerStackElement; | ||
push: (node: MarkdownNode, ...rest: MarkdownNode[]) => void; | ||
pop: () => MarkdownNode | undefined; | ||
} | ||
export declare const createElement: (type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord) => StackElement; | ||
//# sourceMappingURL=stack-element.d.ts.map |
@@ -1,85 +0,23 @@ | ||
import type { Fragment, Node as ProseNode, Schema } from '@milkdown/prose/model'; | ||
import type { RemarkParser } from '../utility'; | ||
import type { Stack } from './stack'; | ||
import type { InnerSerializerSpecMap } from './types'; | ||
type StateMethod<T extends keyof Stack> = (...args: Parameters<Stack[T]>) => State; | ||
/** | ||
* State for serializer. | ||
* Transform prosemirror state into remark AST. | ||
*/ | ||
export declare class State { | ||
import type { Fragment, Node, Schema } from '@milkdown/prose/model'; | ||
import { Mark } from '@milkdown/prose/model'; | ||
import type { Root } from 'mdast'; | ||
import type { JSONRecord, MarkdownNode, RemarkParser } from '../utility'; | ||
import { Stack } from '../utility'; | ||
import { SerializerStackElement } from './stack-element'; | ||
import type { Serializer } from './types'; | ||
export declare class SerializerState extends Stack<MarkdownNode, SerializerStackElement> { | ||
#private; | ||
private readonly stack; | ||
readonly schema: Schema; | ||
private readonly specMap; | ||
constructor(stack: Stack, schema: Schema, specMap: InnerSerializerSpecMap); | ||
/** | ||
* Transform a prosemirror node tree into remark AST. | ||
* | ||
* @param tree - The prosemirror node tree needs to be transformed. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
run(tree: ProseNode): this; | ||
/** | ||
* Use a remark parser to serialize current AST stored. | ||
* | ||
* @param remark - The remark parser needs to used. | ||
* @returns Result markdown string. | ||
*/ | ||
static create: (schema: Schema, remark: RemarkParser) => Serializer; | ||
constructor(schema: Schema); | ||
openNode: (type: string, value?: string, props?: JSONRecord) => this; | ||
closeNode: () => this; | ||
addNode: (type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord) => this; | ||
withMark: (mark: Mark, type: string, value?: string, props?: JSONRecord) => this; | ||
closeMark: (mark: Mark) => this; | ||
build: () => Root; | ||
next: (nodes: Node | Fragment) => this; | ||
toString: (remark: RemarkParser) => string; | ||
/** | ||
* Give the node or node list back to the state and the state will find a proper runner (by `match` method) to handle it. | ||
* | ||
* @param nodes - The node or node list needs to be handled. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
next: (nodes: ProseNode | Fragment) => this; | ||
/** | ||
* Add a node without open or close it. | ||
* | ||
* @remarks | ||
* It's useful for nodes which don't have content. | ||
* | ||
* @param type - Type of this node. | ||
* @param children - Children of this node. | ||
* @param value - Value of this node. | ||
* @param props - Additional props of this node. | ||
* | ||
* @returns The added node. | ||
*/ | ||
addNode: StateMethod<'addNode'>; | ||
/** | ||
* Open a node, and all nodes created after this method will be set as the children of the node until a `closeNode` been called. | ||
* | ||
* @remarks | ||
* You can imagine `openNode` as the left half of parenthesis and `closeNode` as the right half. For nodes have children, your runner should just take care of the node itself and let other runners to handle the children. | ||
* | ||
* @param type - Type of this node. | ||
* @param value - Value of this node. | ||
* @param props - Additional props of this node. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
openNode: StateMethod<'openNode'>; | ||
/** | ||
* Close current node. | ||
* | ||
* @returns The node closed. | ||
*/ | ||
closeNode: StateMethod<'closeNode'>; | ||
/** | ||
* Used when current node has marks, the serializer will auto combine marks nearby. | ||
* | ||
* @param mark - The mark need to be opened. | ||
* @param type - Type of this mark. | ||
* @param value - Value of this mark. | ||
* @param props - Additional props of this mark. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
withMark: StateMethod<'openMark'>; | ||
run: (tree: Node) => this; | ||
} | ||
export {}; | ||
//# sourceMappingURL=state.d.ts.map |
@@ -1,13 +0,12 @@ | ||
import type { Mark as ProseMark, Node as ProseNode } from '@milkdown/prose/model'; | ||
import type { State } from './state'; | ||
import type { Mark, Node } from '@milkdown/prose/model'; | ||
import type { SerializerState } from './state'; | ||
export type Serializer = (content: Node) => string; | ||
export interface NodeSerializerSpec { | ||
match: (node: ProseNode) => boolean; | ||
runner: (state: State, node: ProseNode) => void; | ||
match: (node: Node) => boolean; | ||
runner: (state: SerializerState, node: Node) => void; | ||
} | ||
export interface MarkSerializerSpec { | ||
match: (mark: ProseMark) => boolean; | ||
runner: (state: State, mark: ProseMark, node: ProseNode) => void | boolean; | ||
match: (mark: Mark) => boolean; | ||
runner: (state: SerializerState, mark: Mark, node: Node) => void | boolean; | ||
} | ||
export type SerializerSpec = NodeSerializerSpec | MarkSerializerSpec; | ||
export type InnerSerializerSpecMap = Record<string, SerializerSpec>; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,15 +0,12 @@ | ||
interface ElInstance<U> { | ||
push: (node: U, ...rest: U[]) => void; | ||
export declare abstract class StackElement<Node> { | ||
abstract push(node: Node, ...rest: Node[]): void; | ||
} | ||
interface StackCtx<T extends ElInstance<U>, U> { | ||
readonly elements: T[]; | ||
export declare class Stack<Node, Element extends StackElement<Node>> { | ||
protected elements: Element[]; | ||
size: () => number; | ||
top: () => Element | undefined; | ||
push: (node: Node) => void; | ||
open: (node: Element) => void; | ||
close: () => Element; | ||
} | ||
export declare const getStackUtil: <Node_1, El extends ElInstance<Node_1>, Ctx extends StackCtx<El, Node_1>>() => { | ||
size: (ctx: Ctx) => number; | ||
top: (ctx: Ctx) => El | undefined; | ||
push: (ctx: Ctx) => (node: Node_1) => void; | ||
open: (ctx: Ctx) => (node: El) => void; | ||
close: (ctx: Ctx) => El; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=stack.d.ts.map |
@@ -0,5 +1,9 @@ | ||
import type { MarkSpec, NodeSpec } from '@milkdown/prose/model'; | ||
import type { Root } from 'mdast'; | ||
import type { remark } from 'remark'; | ||
import type { Plugin } from 'unified'; | ||
type JSONValue = string | number | boolean | null | JSONValue[] | { | ||
import type { Node } from 'unist'; | ||
import type { MarkParserSpec, NodeParserSpec } from '../parser/types'; | ||
import type { MarkSerializerSpec, NodeSerializerSpec } from '../serializer/types'; | ||
export type JSONValue = string | number | boolean | null | JSONValue[] | { | ||
[key: string]: JSONValue; | ||
@@ -10,3 +14,15 @@ }; | ||
export type RemarkParser = ReturnType<typeof remark>; | ||
export {}; | ||
export type MarkdownNode = Node & { | ||
children?: MarkdownNode[]; | ||
[x: string]: unknown; | ||
}; | ||
export interface NodeSchema extends NodeSpec { | ||
readonly toMarkdown: NodeSerializerSpec; | ||
readonly parseMarkdown: NodeParserSpec; | ||
readonly priority?: number; | ||
} | ||
export interface MarkSchema extends MarkSpec { | ||
readonly toMarkdown: MarkSerializerSpec; | ||
readonly parseMarkdown: MarkParserSpec; | ||
} | ||
//# sourceMappingURL=types.d.ts.map |
{ | ||
"name": "@milkdown/transformer", | ||
"type": "module", | ||
"version": "6.5.4", | ||
"version": "7.0.0-next.0", | ||
"license": "MIT", | ||
@@ -25,3 +25,3 @@ "repository": { | ||
"peerDependencies": { | ||
"@milkdown/prose": "^6.0.1" | ||
"@milkdown/prose": "^7.0.0-next.0" | ||
}, | ||
@@ -36,6 +36,6 @@ "dependencies": { | ||
"unified": "^10.1.0", | ||
"@milkdown/exception": "6.5.4" | ||
"@milkdown/exception": "7.0.0-next.0" | ||
}, | ||
"devDependencies": { | ||
"@milkdown/prose": "6.5.4" | ||
"@milkdown/prose": "7.0.0-next.0" | ||
}, | ||
@@ -42,0 +42,0 @@ "nx": { |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { Node, Schema } from '@milkdown/prose/model' | ||
import type { RemarkParser } from '../utility' | ||
import { createStack } from './stack' | ||
import { State } from './state' | ||
import type { InnerParserSpecMap } from './types' | ||
export const createParser = (schema: Schema, specMap: InnerParserSpecMap, remark: RemarkParser) => { | ||
const state = new State(createStack(schema), schema, specMap) | ||
return (text: string): Node => { | ||
state.run(remark, text) | ||
return state.toDoc() | ||
} | ||
} | ||
export * from './types' | ||
export * from './state' |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import { describe, expect, it } from 'vitest' | ||
import { expect, it } from 'vitest' | ||
import { ParserStackElement } from './stack-element' | ||
import { createMockNodeType } from './stack.spec' | ||
import { createElement } from './stack-element' | ||
it('parser-stack-element', () => { | ||
const type: any = {} | ||
const content = [] | ||
const textNodeType = createMockNodeType('text') | ||
const parserStackElement = ParserStackElement.create(type, content) | ||
describe('parser/stack-element', () => { | ||
it('create an element', () => { | ||
const el1 = createElement(textNodeType, []) | ||
expect(el1.type).toBe(textNodeType) | ||
expect(el1.content).toEqual([]) | ||
expect(el1.attrs).toBeUndefined() | ||
expect(parserStackElement.type).toBe(type) | ||
expect(parserStackElement.content).toBe(content) | ||
expect(parserStackElement.attrs).toBeUndefined() | ||
const content = [textNodeType.create(), textNodeType.create()] | ||
const el2 = createElement(textNodeType, content, { foo: 'bar' }) | ||
expect(el2.type).toBe(textNodeType) | ||
expect(el2.content).toBe(content) | ||
expect(el2.attrs).toEqual({ foo: 'bar' }) | ||
}) | ||
const node1: any = {} | ||
const node2: any = {} | ||
it('push & pop element', () => { | ||
const el1 = createElement(textNodeType, []) | ||
parserStackElement.push(node1, node2) | ||
expect(content).toEqual([node1, node2]) | ||
const text1 = textNodeType.create() | ||
el1.push(text1) | ||
expect(el1.content[0]).toBe(text1) | ||
const text2 = textNodeType.create() | ||
const text3 = textNodeType.create() | ||
el1.push(text2, text3) | ||
expect(el1.content).toEqual([text1, text2, text3]) | ||
expect(el1.pop()).toBe(text3) | ||
expect(el1.content).toEqual([text1, text2]) | ||
}) | ||
expect(parserStackElement.pop()).toBe(node2) | ||
expect(content).toEqual([node1]) | ||
}) |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { Node, NodeType } from '@milkdown/prose/model' | ||
import type { Attrs, Node, NodeType } from '@milkdown/prose/model' | ||
import { StackElement } from '../utility' | ||
import type { Attrs } from './types' | ||
export class ParserStackElement extends StackElement<Node> { | ||
constructor(public type: NodeType, public content: Node[], public attrs?: Attrs) { | ||
super() | ||
} | ||
export interface StackElement { | ||
type: NodeType | ||
content: Node[] | ||
attrs?: Attrs | ||
push: (node: Node, ...rest: Node[]) => void | ||
pop: () => Node | undefined | ||
} | ||
push(node: Node, ...rest: Node[]) { | ||
this.content.push(node, ...rest) | ||
} | ||
const pushElement = (element: StackElement, node: Node, ...rest: Node[]) => { | ||
element.content.push(node, ...rest) | ||
} | ||
pop(): Node | undefined { | ||
return this.content.pop() | ||
} | ||
const popElement = (element: StackElement): Node | undefined => element.content.pop() | ||
export const createElement = (type: NodeType, content: Node[], attrs?: Attrs): StackElement => { | ||
const element: StackElement = { | ||
type, | ||
content, | ||
attrs, | ||
push: (...args) => pushElement(element, ...args), | ||
pop: () => popElement(element), | ||
static create(type: NodeType, content: Node[], attrs?: Attrs) { | ||
return new ParserStackElement(type, content, attrs) | ||
} | ||
return element | ||
} |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { NodeType, Schema } from '@milkdown/prose/model' | ||
import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
import type { MarkType, NodeType, Schema } from '@milkdown/prose/model' | ||
import { describe, expect, it, vi } from 'vitest' | ||
import { ParserState } from './state' | ||
import type { RemarkParser } from '../utility' | ||
import type { Stack } from './stack' | ||
import { State } from './state' | ||
import type { InnerParserSpecMap } from './types' | ||
const docNodeType = { | ||
createAndFill: vi.fn().mockImplementation((attrs, content, marks) => ({ name: 'docNode', content, attrs, marks })), | ||
} as unknown as NodeType | ||
const paragraphNodeType = { | ||
createAndFill: vi.fn().mockImplementation((attrs, content, marks) => ({ name: 'paragraphNode', content, attrs, marks })), | ||
} as unknown as NodeType | ||
const blockquoteNodeType = { | ||
createAndFill: vi.fn().mockImplementation((attrs, content, marks) => ({ name: 'blockquoteNode', content, attrs, marks })), | ||
} as unknown as NodeType | ||
const boldType = { | ||
create: vi.fn().mockImplementation(attrs => ({ | ||
name: 'boldMark', | ||
attrs, | ||
addToSet: arr => arr.concat('bold'), | ||
removeFromSet: arr => arr.filter(x => x !== 'bold'), | ||
})), | ||
addToSet: arr => arr.concat('bold'), | ||
removeFromSet: arr => arr.filter(x => x !== 'bold'), | ||
} as unknown as MarkType | ||
class MockStack implements Stack { | ||
build = vi.fn() | ||
const schema = { | ||
nodes: { | ||
paragraph: { | ||
spec: { | ||
parseMarkdown: { | ||
match: node => node.type === 'paragraphNode', | ||
runner: (state, node) => { | ||
state.addText(node.value) | ||
}, | ||
}, | ||
}, | ||
}, | ||
blockquote: { | ||
spec: { | ||
parseMarkdown: { | ||
match: node => node.type === 'blockquoteNode', | ||
runner: (state, node) => { | ||
state.openNode(blockquoteNodeType) | ||
state.next(node.children) | ||
state.closeNode() | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
text: (text, marks) => ({ text, marks, isText: true }), | ||
} as unknown as Schema | ||
openMark = vi.fn() | ||
describe('parser-state', () => { | ||
it('node', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
closeMark = vi.fn() | ||
state | ||
.openNode(blockquoteNodeType, { id: 'blockquote' }) | ||
.addNode(paragraphNodeType, { id: 1 }) | ||
.addNode(paragraphNodeType, { id: 2 }) | ||
.closeNode() | ||
addText = vi.fn() | ||
openNode = vi.fn() | ||
addNode = vi.fn() | ||
closeNode = vi.fn() | ||
} | ||
const stack = new MockStack() | ||
const schema = { nodes: {}, marks: {}, text: vi.fn() } as unknown as Schema | ||
const textRunner = vi.fn() | ||
const boldRunner = vi.fn() | ||
const specMap: InnerParserSpecMap = { | ||
text: { | ||
key: 'text', | ||
is: 'node', | ||
match: n => n.type === 'text', | ||
runner: textRunner, | ||
}, | ||
bold: { | ||
key: 'bold', | ||
is: 'mark', | ||
match: n => n.type === 'bold', | ||
runner: boldRunner, | ||
}, | ||
} | ||
describe('parser/state', () => { | ||
let state: State | ||
beforeEach(() => { | ||
state = new State(stack, schema, specMap) | ||
expect(state.top()).toMatchObject({ | ||
content: [ | ||
{ | ||
name: 'blockquoteNode', | ||
content: [ | ||
{ | ||
name: 'paragraphNode', | ||
attrs: { | ||
id: 1, | ||
}, | ||
}, | ||
{ | ||
name: 'paragraphNode', | ||
attrs: { | ||
id: 2, | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('run', async () => { | ||
vi.spyOn(state, 'next') | ||
const result: unknown[] = [] | ||
const parse = vi.fn(() => result) | ||
const runSync = vi.fn(() => result) | ||
const mockRemark = { parse, runSync } as unknown as RemarkParser | ||
state.run(mockRemark, 'markdown') | ||
it('mark', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
state | ||
.openMark(boldType) | ||
.addNode(paragraphNodeType) | ||
.closeMark(boldType) | ||
expect(parse).toHaveBeenCalledWith('markdown') | ||
expect(state.next).toHaveBeenCalledWith(result) | ||
expect(state.top()).toMatchObject({ | ||
content: [ | ||
{ | ||
name: 'paragraphNode', | ||
marks: ['bold'], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('next', () => { | ||
const textNode = { type: 'text' } | ||
const boldNode = { type: 'bold' } | ||
const errorNode = { type: 'error' } | ||
it('merge text for no mark', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
state.next(textNode) | ||
state | ||
.openNode(paragraphNodeType) | ||
.addText('The lunatic is on the grass.\n') | ||
.addText('I\'ll see you on the dark side of the moon.') | ||
.closeNode() | ||
expect(textRunner).toBeCalledTimes(1) | ||
expect(boldRunner).toBeCalledTimes(0) | ||
state.next([textNode, boldNode]) | ||
expect(textRunner).toBeCalledTimes(2) | ||
expect(boldRunner).toBeCalledTimes(1) | ||
expect(() => state.next(errorNode)).toThrow() | ||
expect(state.top()).toMatchObject({ | ||
content: [ | ||
{ | ||
name: 'paragraphNode', | ||
content: [ | ||
{ | ||
text: 'The lunatic is on the grass.\nI\'ll see you on the dark side of the moon.', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('injectRoot', () => { | ||
vi.spyOn(state, 'next') | ||
const children = [] as never[] | ||
const textNode = { type: 'text', children } | ||
const mockNodeType = {} as NodeType | ||
const mockAttr = {} | ||
state.injectRoot(textNode, mockNodeType, mockAttr) | ||
it('merge text for same mark', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
expect(stack.openNode).toBeCalledWith(mockNodeType, mockAttr) | ||
expect(state.next).toBeCalledWith(children) | ||
}) | ||
state | ||
.openNode(paragraphNodeType) | ||
.openMark(boldType) | ||
.addText('The lunatic is on the grass.\n') | ||
.addText('I\'ll see you on the dark side of the moon.') | ||
.closeMark(boldType) | ||
.closeNode() | ||
it('addText', () => { | ||
state.addText() | ||
expect(stack.addText).toBeCalledTimes(1) | ||
state.addText('foo') | ||
expect(stack.addText).toBeCalledTimes(2) | ||
expect(state.top()).toMatchObject({ | ||
content: [ | ||
{ | ||
name: 'paragraphNode', | ||
content: [ | ||
{ | ||
text: 'The lunatic is on the grass.\nI\'ll see you on the dark side of the moon.', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('addNode', () => { | ||
const xs = [null, null, null] as [never, never, never] | ||
state.addNode(...xs) | ||
it('not merge text for different marks', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
expect(stack.addNode).toBeCalledWith(...xs) | ||
}) | ||
state | ||
.openNode(paragraphNodeType) | ||
.openMark(boldType) | ||
.addText('The lunatic is on the grass.\n') | ||
.closeMark(boldType) | ||
.addText('I\'ll see you on the dark side of the moon.') | ||
.closeNode() | ||
it('openNode', () => { | ||
const xs = [null, null] as [never, never] | ||
state.openNode(...xs) | ||
expect(stack.openNode).toBeCalledWith(...xs) | ||
expect(state.top()).toMatchObject({ | ||
content: [ | ||
{ | ||
name: 'paragraphNode', | ||
content: [ | ||
{ | ||
text: 'The lunatic is on the grass.\n', | ||
}, | ||
{ | ||
text: 'I\'ll see you on the dark side of the moon.', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('closeNode', () => { | ||
state.closeNode() | ||
it('build', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
expect(stack.closeNode).toBeCalledWith() | ||
}) | ||
state | ||
.openNode(blockquoteNodeType, { id: 'blockquote' }) | ||
.addNode(paragraphNodeType, { id: 1 }) | ||
.addNode(paragraphNodeType, { id: 2 }) | ||
.closeNode() | ||
it('openMark', () => { | ||
const xs = [null, null] as [never, never] | ||
state.openMark(...xs) | ||
expect(stack.openMark).toBeCalledWith(...xs) | ||
const node = state.build() | ||
expect(node).toMatchObject({ | ||
name: 'docNode', | ||
content: [ | ||
{ | ||
name: 'blockquoteNode', | ||
content: [ | ||
{ | ||
name: 'paragraphNode', | ||
attrs: { | ||
id: 1, | ||
}, | ||
}, | ||
{ | ||
name: 'paragraphNode', | ||
attrs: { | ||
id: 2, | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('closeMark', () => { | ||
state.closeMark(null as never) | ||
it('next', () => { | ||
const state = new ParserState(schema) | ||
state.openNode(docNodeType) | ||
state.next([ | ||
{ | ||
type: 'blockquoteNode', | ||
children: [ | ||
{ | ||
type: 'paragraphNode', | ||
value: 'The lunatic is on the grass.', | ||
}, | ||
], | ||
}, | ||
]) | ||
expect(stack.closeMark).toBeCalledWith(null) | ||
const node = state.build() | ||
expect(node).toMatchObject({ | ||
name: 'docNode', | ||
content: [ | ||
{ | ||
name: 'blockquoteNode', | ||
content: [ | ||
{ | ||
text: 'The lunatic is on the grass.', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('toDoc', () => { | ||
state.toDoc() | ||
expect(stack.build).toBeCalledTimes(1) | ||
}) | ||
}) |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import { parserMatchError } from '@milkdown/exception' | ||
import type { MarkType, Node, NodeType, Schema } from '@milkdown/prose/model' | ||
import type { Attrs, MarkType, Node, NodeType, Schema } from '@milkdown/prose/model' | ||
import { createNodeInParserFail, parserMatchError, stackOverFlow } from '@milkdown/exception' | ||
import { Mark } from '@milkdown/prose/model' | ||
import type { MarkSchema, MarkdownNode, NodeSchema, RemarkParser } from '../utility' | ||
import { Stack } from '../utility' | ||
import type { RemarkParser } from '../utility' | ||
import type { Stack } from './stack' | ||
import type { Attrs, InnerParserSpecMap, MarkdownNode, ParserSpecWithType } from './types' | ||
import { ParserStackElement } from './stack-element' | ||
import type { Parser } from './types' | ||
type PS<T extends keyof Stack> = Parameters<Stack[T]> | ||
/// A state machine for parser. Transform remark AST into prosemirror state. | ||
export class ParserState extends Stack<Node, ParserStackElement> { | ||
/// The schema in current editor. | ||
readonly schema: Schema | ||
/** | ||
* State for parser. | ||
* Transform remark AST into prosemirror state. | ||
*/ | ||
export class State { | ||
constructor( | ||
private readonly stack: Stack, | ||
public readonly schema: Schema, | ||
private readonly specMap: InnerParserSpecMap, | ||
) {} | ||
/// @internal | ||
#marks: readonly Mark[] = Mark.none | ||
#matchTarget(node: MarkdownNode): ParserSpecWithType { | ||
const result = Object.values(this.specMap).find(x => x.match(node)) | ||
/// Create a parser from schema and remark instance. | ||
/// | ||
/// ```typescript | ||
/// const parser = ParserState.create(schema, remark) | ||
/// const prosemirrorNode = parser(SomeMarkdownText) | ||
/// ``` | ||
static create = (schema: Schema, remark: RemarkParser): Parser => { | ||
const state = new this(schema) | ||
return (text) => { | ||
state.run(remark, text) | ||
return state.toDoc() | ||
} | ||
} | ||
/// @internal | ||
protected constructor(schema: Schema) { | ||
super() | ||
this.schema = schema | ||
} | ||
/// @internal | ||
#hasText = (node: Node): node is Node & { text: string } => node.isText | ||
/// @internal | ||
#maybeMerge = (a: Node, b: Node): Node | undefined => { | ||
if (this.#hasText(a) && this.#hasText(b) && Mark.sameSet(a.marks, b.marks)) | ||
return this.schema.text(a.text + b.text, a.marks) | ||
return undefined | ||
} | ||
/// @internal | ||
#matchTarget = (node: MarkdownNode): NodeType | MarkType => { | ||
const result = Object.values({ ...this.schema.nodes, ...this.schema.marks }) | ||
.find((x): x is (NodeType | MarkType) => { | ||
const spec = x.spec as NodeSchema | MarkSchema | ||
return spec.parseMarkdown.match(node) | ||
}) | ||
if (!result) | ||
@@ -31,22 +64,14 @@ throw parserMatchError(node) | ||
#runNode(node: MarkdownNode) { | ||
const { key, runner, is } = this.#matchTarget(node) | ||
/// @internal | ||
#runNode = (node: MarkdownNode) => { | ||
const type = this.#matchTarget(node) | ||
const spec = type.spec as NodeSchema | MarkSchema | ||
const proseType: NodeType | MarkType = this.schema[is === 'node' ? 'nodes' : 'marks'][key] as | ||
| NodeType | ||
| MarkType | ||
runner(this, node, proseType as NodeType & MarkType) | ||
spec.parseMarkdown.runner(this, node, type as NodeType & MarkType) | ||
} | ||
/** | ||
* Transform a markdown string into prosemirror state. | ||
* | ||
* @param remark - The remark parser used. | ||
* @param markdown - The markdown string needs to be parsed. | ||
* @returns The state instance. | ||
*/ | ||
run = (remark: RemarkParser, markdown: string) => { | ||
const tree = remark.runSync(remark.parse(markdown), markdown) as MarkdownNode | ||
this.next(tree) | ||
/// Inject root node for prosemirror state. | ||
injectRoot = (node: MarkdownNode, nodeType: NodeType, attrs?: Attrs) => { | ||
this.openNode(nodeType, attrs) | ||
this.next(node.children) | ||
@@ -56,117 +81,105 @@ return this | ||
/** | ||
* Give the node or node list back to the state and the state will find a proper runner (by `match` method) to handle it. | ||
* | ||
* @param nodes - The node or node list needs to be handled. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
next = (nodes: MarkdownNode | MarkdownNode[] = []) => { | ||
[nodes].flat().forEach(node => this.#runNode(node)) | ||
/// Open a new node, the next operations will | ||
/// add nodes into that new node until `closeNode` is called. | ||
openNode = (nodeType: NodeType, attrs?: Attrs) => { | ||
this.open(ParserStackElement.create(nodeType, [], attrs)) | ||
return this | ||
} | ||
/** | ||
* Parse current remark AST into prosemirror state. | ||
* | ||
* @returns Result prosemirror doc. | ||
*/ | ||
toDoc = (): Node => this.stack.build() | ||
/// @internal | ||
#closeNodeAndPush = (): Node => { | ||
this.#marks = Mark.none | ||
const element = this.close() | ||
/** | ||
* Inject root node for prosemirror state. | ||
* | ||
* @param node - The target markdown node. | ||
* @param nodeType - The root prosemirror nodeType . | ||
* @param attrs - The attribute of root type. | ||
* @returns The state instance. | ||
*/ | ||
injectRoot = (node: MarkdownNode, nodeType: NodeType, attrs?: Attrs) => { | ||
this.stack.openNode(nodeType, attrs) | ||
this.next(node.children) | ||
return this.#addNodeAndPush(element.type, element.attrs, element.content) | ||
} | ||
/// Close the current node and push it into the parent node. | ||
closeNode = () => { | ||
this.#closeNodeAndPush() | ||
return this | ||
} | ||
/** | ||
* Add a text type prosemirror node. | ||
* | ||
* @param text - Text string. | ||
* @returns The state instance. | ||
*/ | ||
addText = (text = '') => { | ||
this.stack.addText(text) | ||
/// @internal | ||
#addNodeAndPush = (nodeType: NodeType, attrs?: Attrs, content?: Node[]): Node => { | ||
const node = nodeType.createAndFill(attrs, content, this.#marks) | ||
if (!node) | ||
throw createNodeInParserFail(nodeType, attrs, content) | ||
this.push(node) | ||
return node | ||
} | ||
/// Add a node into current node. | ||
addNode = (nodeType: NodeType, attrs?: Attrs, content?: Node[]) => { | ||
this.#addNodeAndPush(nodeType, attrs, content) | ||
return this | ||
} | ||
/** | ||
* Add a node without open or close it. | ||
* | ||
* @remarks | ||
* It's useful for nodes which don't have content. | ||
* | ||
* @param nodeType - Node type of this node. | ||
* @param attrs - Attributes of this node. | ||
* @param content - Content of this node. | ||
* | ||
* @returns The added node. | ||
*/ | ||
addNode = (...args: PS<'addNode'>) => { | ||
this.stack.addNode(...args) | ||
/// Open a new mark, the next nodes added will have that mark. | ||
openMark = (markType: MarkType, attrs?: Attrs) => { | ||
const mark = markType.create(attrs) | ||
this.#marks = mark.addToSet(this.#marks) | ||
return this | ||
} | ||
/** | ||
* Open a node, and all nodes created after this method will be set as the children of the node until a `closeNode` been called. | ||
* | ||
* @remarks | ||
* You can imagine `openNode` as the left half of parenthesis and `closeNode` as the right half. For nodes have children, your runner should just take care of the node itself and let other runners to handle the children. | ||
* | ||
* @param nodeType - Node type of this node. | ||
* @param attrs - Attributes of this node. | ||
* | ||
* @returns | ||
*/ | ||
openNode = (...args: PS<'openNode'>) => { | ||
this.stack.openNode(...args) | ||
/// Close a opened mark. | ||
closeMark = (markType: MarkType) => { | ||
this.#marks = markType.removeFromSet(this.#marks) | ||
return this | ||
} | ||
/** | ||
* Close current node. | ||
* | ||
* @returns The node closed. | ||
*/ | ||
closeNode = (...args: PS<'closeNode'>) => { | ||
this.stack.closeNode(...args) | ||
/// Add a text node into current node. | ||
addText = (text: string) => { | ||
const topElement = this.top() | ||
if (!topElement) | ||
throw stackOverFlow() | ||
const prevNode = topElement.pop() | ||
const currNode = this.schema.text(text, this.#marks) | ||
if (!prevNode) { | ||
topElement.push(currNode) | ||
return this | ||
} | ||
const merged = this.#maybeMerge(prevNode, currNode) | ||
if (merged) { | ||
topElement.push(merged) | ||
return this | ||
} | ||
topElement.push(prevNode, currNode) | ||
return this | ||
} | ||
/** | ||
* Open a mark, and all marks created after this method will be set as the children of the mark until a `closeMark` been called. | ||
* | ||
* @remarks | ||
* You can imagine `openMark` as the left half of parenthesis and `closeMark` as the right half. For nodes have children, your runner should just take care of the node itself and let other runners to handle the children. | ||
* | ||
* @param markType - Mark type of this mark. | ||
* @param attrs - Attributes of this mark. | ||
* | ||
* @returns | ||
*/ | ||
openMark = (...args: PS<'openMark'>) => { | ||
this.stack.openMark(...args) | ||
/// @internal | ||
build = (): Node => { | ||
let doc: Node | undefined | ||
do | ||
doc = this.#closeNodeAndPush() | ||
while (this.size()) | ||
return doc | ||
} | ||
/// Give the node or node list back to the state and | ||
/// the state will find a proper runner (by `match` method in parser spec) to handle it. | ||
next = (nodes: MarkdownNode | MarkdownNode[] = []) => { | ||
[nodes].flat().forEach(node => this.#runNode(node)) | ||
return this | ||
} | ||
/** | ||
* Close target mark. | ||
* | ||
* @param markType - Mark type of this mark. | ||
* | ||
* @returns The mark closed. | ||
*/ | ||
closeMark = (...args: PS<'closeMark'>) => { | ||
this.stack.closeMark(...args) | ||
/// Build the current state into a [prosemirror document](https://prosemirror.net/docs/ref/#model.Document_Structure). | ||
toDoc = () => this.build() | ||
/// Transform a markdown string into prosemirror state. | ||
run = (remark: RemarkParser, markdown: string) => { | ||
const tree = remark.runSync(remark.parse(markdown), markdown) as MarkdownNode | ||
this.next(tree) | ||
return this | ||
} | ||
} |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { MarkType, NodeType } from '@milkdown/prose/model' | ||
import type { Node } from 'unist' | ||
import type { MarkType, Node, NodeType } from '@milkdown/prose/model' | ||
import type { MarkdownNode } from '../utility/types' | ||
import type { State } from './state' | ||
import type { ParserState } from './state' | ||
export type Attrs = Record<string, string | number | boolean | null> | ||
export type MarkdownNode = Node & { children?: MarkdownNode[]; [x: string]: unknown } | ||
/// The parser type which is used to transform markdown text into prosemirror node. | ||
export type Parser = (text: string) => Node | ||
export type ParserRunner<T extends NodeType | MarkType = NodeType | MarkType> = ( | ||
state: State, | ||
Node: MarkdownNode, | ||
proseType: T, | ||
) => void | ||
export interface ParserSpec<T extends NodeType | MarkType = NodeType | MarkType> { | ||
/// The spec for node parser in schema. | ||
export type NodeParserSpec = { | ||
/// The match function to check if the node is the target node. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// match: (node) => node.type === 'paragraph' | ||
/// ``` | ||
match: (node: MarkdownNode) => boolean | ||
runner: ParserRunner<T> | ||
/// The runner function to transform the node into prosemirror node. | ||
/// Generally, you should call methods in `state` to add node to state. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// runner: (state, node, type) => { | ||
/// state | ||
/// .openNode(type) | ||
/// .next(node.children) | ||
/// .closeNode(); | ||
/// } | ||
/// ``` | ||
runner: (state: ParserState, node: MarkdownNode, proseType: NodeType) => void | ||
} | ||
export type NodeParserSpec = ParserSpec<NodeType> | ||
export type MarkParserSpec = ParserSpec<MarkType> | ||
export type ParserSpecWithType = | ||
| (NodeParserSpec & { is: 'node'; key: string }) | ||
| (MarkParserSpec & { is: 'mark'; key: string }) | ||
export type InnerParserSpecMap = Record<string, ParserSpecWithType> | ||
/// The spec for mark parser in schema. | ||
export type MarkParserSpec = { | ||
/// The match function to check if the node is the target mark. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// match: (mark) => mark.type === 'emphasis' | ||
/// ``` | ||
match: (node: MarkdownNode) => boolean | ||
/// The runner function to transform the node into prosemirror mark. | ||
/// Generally, you should call methods in `state` to add mark to state. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// runner: (state, node, type) => { | ||
/// state | ||
/// .openMark(type) | ||
/// .next(node.children) | ||
/// .closeMark(type) | ||
/// } | ||
/// ``` | ||
runner: (state: ParserState, node: MarkdownNode, proseType: MarkType) => void | ||
} |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { Node, Schema } from '@milkdown/prose/model' | ||
import type { RemarkParser } from '../utility' | ||
import { createStack } from './stack' | ||
import { State } from './state' | ||
import type { InnerSerializerSpecMap } from './types' | ||
export const createSerializer | ||
= (schema: Schema, specMap: InnerSerializerSpecMap, remark: RemarkParser) => (content: Node) => { | ||
const state = new State(createStack(), schema, specMap) | ||
state.run(content) | ||
return state.toString(remark) | ||
} | ||
export * from './types' | ||
export * from './state' |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import { describe, expect, it } from 'vitest' | ||
import { expect, it } from 'vitest' | ||
import { SerializerStackElement } from './stack-element' | ||
import { createElement } from './stack-element' | ||
it('serializer-stack-element', () => { | ||
const element = SerializerStackElement.create('type') | ||
describe('serializer/stack-element', () => { | ||
it('create an element', () => { | ||
const el1 = createElement('root') | ||
expect(el1.type).toBe('root') | ||
expect(element.props).toEqual({}) | ||
const el2 = createElement('text', [el1], 'value', { | ||
foo: 'bar', | ||
}) | ||
expect(el2.type).toBe('text') | ||
expect(el2.children).toEqual([el1]) | ||
expect(el2.value).toBe('value') | ||
expect(el2.props.foo).toBe('bar') | ||
}) | ||
element.push({ type: 'text' }) | ||
it('push & pop element', () => { | ||
const el1 = createElement('root') | ||
const el2 = createElement('text') | ||
expect(element.children).toEqual([{ type: 'text' }]) | ||
el1.push(el2) | ||
expect(el1.children).toHaveLength(1) | ||
expect(el1.pop()).toBe(el2) | ||
const node = element.pop() | ||
const el3 = createElement('text') | ||
const el4 = createElement('text') | ||
el1.push(el3, el4) | ||
expect(el1.children).toHaveLength(2) | ||
expect(el1.pop()).toBe(el4) | ||
expect(el1.children).toHaveLength(1) | ||
expect(el1.pop()).toBe(el3) | ||
}) | ||
expect(node).toEqual({ type: 'text' }) | ||
}) |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { MarkdownNode } from '..' | ||
import type { JSONRecord } from '../utility' | ||
import { StackElement } from '../utility' | ||
export interface StackElement { | ||
type: string | ||
value?: string | ||
props: JSONRecord | ||
children?: MarkdownNode[] | ||
push: (node: MarkdownNode, ...rest: MarkdownNode[]) => void | ||
pop: () => MarkdownNode | undefined | ||
} | ||
export class SerializerStackElement extends StackElement<MarkdownNode> { | ||
constructor( | ||
public type: string, | ||
public children?: MarkdownNode[], | ||
public value?: string, | ||
public props: JSONRecord = {}, | ||
) { | ||
super() | ||
} | ||
const pushElement = (element: StackElement, node: MarkdownNode, ...rest: MarkdownNode[]) => { | ||
if (!element.children) | ||
element.children = [] | ||
static create = ( | ||
type: string, | ||
children?: MarkdownNode[], | ||
value?: string, | ||
props: JSONRecord = {}, | ||
) => new SerializerStackElement(type, children, value, props) | ||
element.children.push(node, ...rest) | ||
} | ||
push = (node: MarkdownNode, ...rest: MarkdownNode[]) => { | ||
if (!this.children) | ||
this.children = [] | ||
const popElement = (element: StackElement): MarkdownNode | undefined => element.children?.pop() | ||
this.children.push(node, ...rest) | ||
} | ||
export const createElement = ( | ||
type: string, | ||
children?: MarkdownNode[], | ||
value?: string, | ||
props: JSONRecord = {}, | ||
): StackElement => { | ||
const element: StackElement = { | ||
type, | ||
children, | ||
props, | ||
value, | ||
push: (...args) => pushElement(element, ...args), | ||
pop: () => popElement(element), | ||
} | ||
return element | ||
pop = (): MarkdownNode | undefined => this.children?.pop() | ||
} |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { Mark as ProseMark, Node as ProseNode, Schema } from '@milkdown/prose/model' | ||
import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
import type { Mark, Schema } from '@milkdown/prose/model' | ||
import { describe, expect, it } from 'vitest' | ||
import { SerializerState } from './state' | ||
import type { InnerSerializerSpecMap } from '..' | ||
import { createMockMarkType, createMockNodeType } from '../parser/stack.spec' | ||
import type { RemarkParser } from '../utility' | ||
import type { Stack } from './stack' | ||
import { State } from './state' | ||
const boldMark = { | ||
isInSet: arr => arr.includes('bold'), | ||
addToSet: arr => arr.concat('bold'), | ||
type: { | ||
removeFromSet: arr => arr.filter(x => x !== 'bold'), | ||
}, | ||
} as unknown as Mark | ||
class MockStack implements Stack { | ||
build = vi.fn() | ||
openMark = vi.fn() | ||
closeMark = vi.fn() | ||
openNode = vi.fn() | ||
addNode = vi.fn() | ||
closeNode = vi.fn() | ||
top = vi.fn() | ||
} | ||
const stack = new MockStack() | ||
const schema = { nodes: {}, marks: {}, text: vi.fn() } as unknown as Schema | ||
const textRunner = vi.fn() | ||
const boldRunner = vi.fn() | ||
const italicRunner = vi.fn() | ||
const specMap: InnerSerializerSpecMap = { | ||
text: { | ||
match: (n: ProseNode) => n.type.name === 'text', | ||
runner: textRunner, | ||
const schema = { | ||
nodes: { | ||
paragraph: { | ||
spec: { | ||
toMarkdown: { | ||
match: node => node.type === 'paragraph', | ||
runner: (state, node) => { | ||
state.addNode('text', [], node.value) | ||
}, | ||
}, | ||
}, | ||
}, | ||
blockquote: { | ||
spec: { | ||
toMarkdown: { | ||
match: node => node.type === 'blockquote', | ||
runner: (state, node) => { | ||
state.openNode('blockquote') | ||
state.next(node.content) | ||
state.closeNode() | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
bold: { | ||
match: (n: ProseMark) => n.type.name === 'bold', | ||
runner: boldRunner, | ||
}, | ||
italic: { | ||
match: (n: ProseMark) => n.type.name === 'italic', | ||
runner: italicRunner, | ||
}, | ||
} | ||
marks: {}, | ||
text: (text, marks) => ({ text, marks, isText: true }), | ||
} as unknown as Schema | ||
const textType = createMockNodeType('text') | ||
const boldType = createMockMarkType('bold') | ||
const italicType = createMockMarkType('italic') | ||
describe('serializer-state', () => { | ||
it('node', () => { | ||
const state = new SerializerState(schema) | ||
state.openNode('doc') | ||
state.openNode('paragraph', 'paragraph node value', { foo: 'bar' }) | ||
state.addNode('text', [], 'text node value') | ||
state.closeNode() | ||
describe('serializer/state', () => { | ||
let state: State | ||
beforeEach(() => { | ||
state = new State(stack, schema, specMap) | ||
expect(state.top()).toMatchObject({ | ||
type: 'doc', | ||
children: [ | ||
{ | ||
type: 'paragraph', | ||
value: 'paragraph node value', | ||
children: [ | ||
{ | ||
type: 'text', | ||
value: 'text node value', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('run', () => { | ||
vi.spyOn(state, 'next') | ||
const text = textType.create() | ||
state.run(text) | ||
it('maybe merge children for same mark', () => { | ||
const state = new SerializerState(schema) | ||
state.openNode('doc') | ||
state.openNode('paragraph') | ||
state.withMark(boldMark, 'bold') | ||
state.addNode('text', [], 'The lunatic is on the grass.') | ||
state.closeMark(boldMark) | ||
state.withMark(boldMark, 'bold') | ||
state.addNode('text', [], 'The lunatic is in the hell.') | ||
state.closeMark(boldMark) | ||
state.closeNode() | ||
expect(state.next).toHaveBeenCalledWith(text) | ||
expect(state.top()).toMatchObject({ | ||
type: 'doc', | ||
children: [ | ||
{ | ||
type: 'paragraph', | ||
children: [ | ||
{ | ||
type: 'bold', | ||
isMark: true, | ||
children: [ | ||
{ | ||
type: 'text', | ||
value: 'The lunatic is on the grass.', | ||
}, | ||
{ | ||
type: 'text', | ||
value: 'The lunatic is in the hell.', | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('toString', () => { | ||
const stringify = vi.fn() | ||
state.toString({ stringify } as unknown as RemarkParser) | ||
expect(stack.build).toBeCalledTimes(1) | ||
expect(stringify).toBeCalledTimes(1) | ||
it('build', () => { | ||
const state = new SerializerState(schema) | ||
state.openNode('doc') | ||
state.openNode('paragraph', 'paragraph node value', { foo: 'bar' }) | ||
state.addNode('text', [], 'text node value') | ||
state.closeNode() | ||
expect(state.build()).toMatchObject({ | ||
type: 'doc', | ||
children: [ | ||
{ | ||
type: 'paragraph', | ||
value: 'paragraph node value', | ||
children: [ | ||
{ | ||
type: 'text', | ||
value: 'text node value', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('next', () => { | ||
const text = textType.create() | ||
const strong = boldType.create() | ||
const italic = italicType.create() | ||
text.marks = [strong, italic] | ||
const state = new SerializerState(schema) | ||
state.openNode('doc') | ||
state.next({ | ||
type: 'blockquote', | ||
marks: [], | ||
content: { | ||
type: 'paragraph', | ||
marks: [], | ||
value: 'The lunatic is on the grass.', | ||
}, | ||
} as any) | ||
textRunner.mockClear() | ||
state.next(text) | ||
expect(boldRunner).toBeCalledTimes(1) | ||
expect(italicRunner).toBeCalledTimes(1) | ||
expect(textRunner).toBeCalledTimes(1) | ||
expect(state.build()).toMatchObject({ | ||
type: 'doc', | ||
children: [ | ||
{ | ||
type: 'blockquote', | ||
children: [ | ||
{ | ||
type: 'text', | ||
value: 'The lunatic is on the grass.', | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
}) | ||
it('addNode', () => { | ||
state.addNode('node') | ||
expect(stack.addNode).toBeCalledWith('node') | ||
}) | ||
it('openNode', () => { | ||
state.openNode('node') | ||
expect(stack.openNode).toBeCalledWith('node') | ||
}) | ||
it('closeNode', () => { | ||
state.closeNode() | ||
expect(stack.closeNode).toBeCalledWith() | ||
}) | ||
it('withMark', () => { | ||
const bold = boldType.create() | ||
state.withMark(bold, 'bold') | ||
expect(stack.openMark).toBeCalledWith(bold, 'bold') | ||
}) | ||
}) |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import { serializerMatchError } from '@milkdown/exception' | ||
import type { Fragment, Mark as ProseMark, Node as ProseNode, Schema } from '@milkdown/prose/model' | ||
import type { Fragment, MarkType, Node, NodeType, Schema } from '@milkdown/prose/model' | ||
import { Mark } from '@milkdown/prose/model' | ||
import type { RemarkParser } from '../utility' | ||
import type { Stack } from './stack' | ||
import type { InnerSerializerSpecMap, MarkSerializerSpec, NodeSerializerSpec } from './types' | ||
import type { Root } from 'mdast' | ||
import type { JSONRecord, MarkSchema, MarkdownNode, NodeSchema, RemarkParser } from '../utility' | ||
import { Stack } from '../utility' | ||
import { SerializerStackElement } from './stack-element' | ||
import type { Serializer } from './types' | ||
const isFragment = (x: ProseNode | Fragment): x is Fragment => Object.prototype.hasOwnProperty.call(x, 'size') | ||
const isFragment = (x: Node | Fragment): x is Fragment => Object.prototype.hasOwnProperty.call(x, 'size') | ||
type StateMethod<T extends keyof Stack> = (...args: Parameters<Stack[T]>) => State | ||
/// State for serializer. | ||
/// Transform prosemirror state into remark AST. | ||
export class SerializerState extends Stack<MarkdownNode, SerializerStackElement> { | ||
/// @internal | ||
#marks: readonly Mark[] = Mark.none | ||
/// Get the schema of state. | ||
readonly schema: Schema | ||
/** | ||
* State for serializer. | ||
* Transform prosemirror state into remark AST. | ||
*/ | ||
export class State { | ||
constructor( | ||
private readonly stack: Stack, | ||
public readonly schema: Schema, | ||
private readonly specMap: InnerSerializerSpecMap, | ||
) {} | ||
/// Create a serializer from schema and remark instance. | ||
/// | ||
/// ```typescript | ||
/// const serializer = SerializerState.create(schema, remark) | ||
/// const markdown = parser(prosemirrorDoc) | ||
/// ``` | ||
static create = (schema: Schema, remark: RemarkParser): Serializer => { | ||
const state = new this(schema) | ||
return (content: Node) => { | ||
state.run(content) | ||
return state.toString(remark) | ||
} | ||
} | ||
#matchTarget<T extends ProseMark | ProseNode>( | ||
node: T, | ||
): (T extends ProseNode ? NodeSerializerSpec : MarkSerializerSpec) & { key: string } { | ||
const result = Object.entries(this.specMap) | ||
.map(([key, spec]) => ({ | ||
key, | ||
...spec, | ||
})) | ||
.find(x => x.match(node as ProseMark & ProseNode)) | ||
/// @internal | ||
constructor(schema: Schema) { | ||
super() | ||
this.schema = schema | ||
} | ||
/// @internal | ||
#matchTarget = (node: Node | Mark): NodeType | MarkType => { | ||
const result = Object.values({ ...this.schema.nodes, ...this.schema.marks }) | ||
.find((x): x is (NodeType | MarkType) => { | ||
const spec = x.spec as NodeSchema | MarkSchema | ||
return spec.toMarkdown.match(node as Node & Mark) | ||
}) | ||
if (!result) | ||
throw serializerMatchError(node.type) | ||
return result as never | ||
return result | ||
} | ||
#runProseNode(node: ProseNode) { | ||
const { runner } = this.#matchTarget(node) | ||
runner(this, node) | ||
/// @internal | ||
#runProseNode = (node: Node) => { | ||
const type = this.#matchTarget(node) | ||
const spec = type.spec as NodeSchema | ||
return spec.toMarkdown.runner(this, node) | ||
} | ||
#runProseMark(mark: ProseMark, node: ProseNode) { | ||
const { runner } = this.#matchTarget(mark) | ||
return runner(this, mark, node) | ||
/// @internal | ||
#runProseMark = (mark: Mark, node: Node) => { | ||
const type = this.#matchTarget(mark) | ||
const spec = type.spec as MarkSchema | ||
return spec.toMarkdown.runner(this, mark, node) | ||
} | ||
#runNode(node: ProseNode) { | ||
/// @internal | ||
#runNode = (node: Node) => { | ||
const { marks } = node | ||
const getPriority = (x: ProseMark) => x.type.spec.priority ?? 50 | ||
const getPriority = (x: Mark) => x.type.spec.priority ?? 50 | ||
const tmp = [...marks].sort((a, b) => getPriority(a) - getPriority(b)) | ||
@@ -58,34 +79,175 @@ const unPreventNext = tmp.every(mark => !this.#runProseMark(mark, node)) | ||
marks.forEach(mark => this.stack.closeMark(mark)) | ||
marks.forEach(mark => this.#closeMark(mark)) | ||
} | ||
/** | ||
* Transform a prosemirror node tree into remark AST. | ||
* | ||
* @param tree - The prosemirror node tree needs to be transformed. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
run(tree: ProseNode) { | ||
this.next(tree) | ||
/// @internal | ||
#searchType = (child: MarkdownNode, type: string): MarkdownNode => { | ||
if (child.type === type) | ||
return child | ||
if (child.children?.length !== 1) | ||
return child | ||
const searchNode = (node: MarkdownNode): MarkdownNode | null => { | ||
if (node.type === type) | ||
return node | ||
if (node.children?.length !== 1) | ||
return null | ||
const [firstChild] = node.children | ||
if (!firstChild) | ||
return null | ||
return searchNode(firstChild) | ||
} | ||
const target = searchNode(child) | ||
if (!target) | ||
return child | ||
const tmp = target.children ? [...target.children] : undefined | ||
const node = { ...child, children: tmp } | ||
node.children = tmp | ||
target.children = [node] | ||
return target | ||
} | ||
/// @internal | ||
#maybeMergeChildren = (node: MarkdownNode): MarkdownNode => { | ||
const { children } = node | ||
if (!children) | ||
return node | ||
node.children = children.reduce((nextChildren, child, index) => { | ||
if (index === 0) | ||
return [child] | ||
const last = nextChildren.at(-1) | ||
if (last && last.isMark && child.isMark) { | ||
child = this.#searchType(child, last.type) | ||
const { children: currChildren, ...currRest } = child | ||
const { children: prevChildren, ...prevRest } = last | ||
if ( | ||
child.type === last.type | ||
&& currChildren | ||
&& prevChildren | ||
&& JSON.stringify(currRest) === JSON.stringify(prevRest) | ||
) { | ||
const next = { | ||
...prevRest, | ||
children: [...prevChildren, ...currChildren], | ||
} | ||
return nextChildren | ||
.slice(0, -1) | ||
.concat(this.#maybeMergeChildren(next)) | ||
} | ||
} | ||
return nextChildren.concat(child) | ||
}, [] as MarkdownNode[]) | ||
return node | ||
} | ||
/// @internal | ||
#createMarkdownNode = (element: SerializerStackElement) => { | ||
const node: MarkdownNode = { | ||
...element.props, | ||
type: element.type, | ||
} | ||
if (element.children) | ||
node.children = element.children | ||
if (element.value) | ||
node.value = element.value | ||
return node | ||
} | ||
/// Open a new node, the next operations will | ||
/// add nodes into that new node until `closeNode` is called. | ||
openNode = (type: string, value?: string, props?: JSONRecord) => { | ||
this.open(SerializerStackElement.create(type, undefined, value, props)) | ||
return this | ||
} | ||
/** | ||
* Use a remark parser to serialize current AST stored. | ||
* | ||
* @param remark - The remark parser needs to used. | ||
* @returns Result markdown string. | ||
*/ | ||
toString = (remark: RemarkParser): string => remark.stringify(this.stack.build()) as string | ||
/// @internal | ||
#closeNodeAndPush = (): MarkdownNode => { | ||
const element = this.close() | ||
return this.#addNodeAndPush(element.type, element.children, element.value, element.props) | ||
} | ||
/** | ||
* Give the node or node list back to the state and the state will find a proper runner (by `match` method) to handle it. | ||
* | ||
* @param nodes - The node or node list needs to be handled. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
next = (nodes: ProseNode | Fragment) => { | ||
/// Close the current node and push it into the parent node. | ||
closeNode = () => { | ||
this.#closeNodeAndPush() | ||
return this | ||
} | ||
/// @internal | ||
#addNodeAndPush = (type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord): MarkdownNode => { | ||
const element = SerializerStackElement.create(type, children, value, props) | ||
const node: MarkdownNode = this.#maybeMergeChildren(this.#createMarkdownNode(element)) | ||
this.push(node) | ||
return node | ||
} | ||
/// Add a node into current node. | ||
addNode = (type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord) => { | ||
this.#addNodeAndPush(type, children, value, props) | ||
return this | ||
} | ||
/// @internal | ||
#openMark = (mark: Mark, type: string, value?: string, props?: JSONRecord) => { | ||
const isIn = mark.isInSet(this.#marks) | ||
if (isIn) | ||
return this | ||
this.#marks = mark.addToSet(this.#marks) | ||
return this.openNode(type, value, { ...props, isMark: true }) | ||
} | ||
/// @internal | ||
#closeMark = (mark: Mark): void => { | ||
const isIn = mark.isInSet(this.#marks) | ||
if (!isIn) | ||
return | ||
this.#marks = mark.type.removeFromSet(this.#marks) | ||
this.#closeNodeAndPush() | ||
} | ||
/// Open a new mark, the next nodes added will have that mark. | ||
/// The mark will be closed automatically. | ||
withMark = (mark: Mark, type: string, value?: string, props?: JSONRecord) => { | ||
this.#openMark(mark, type, value, props) | ||
return this | ||
} | ||
/// Close a opened mark. | ||
/// In most cases you don't need this because | ||
/// marks will be closed automatically. | ||
closeMark = (mark: Mark) => { | ||
this.#closeMark(mark) | ||
return this | ||
} | ||
/// @internal | ||
build = (): Root => { | ||
let doc: Root | null = null | ||
do | ||
doc = this.#closeNodeAndPush() as Root | ||
while (this.size()) | ||
return doc | ||
} | ||
/// Give the node or node list back to the state and | ||
/// the state will find a proper runner (by `match` method in serializer spec) to handle it. | ||
next = (nodes: Node | Fragment) => { | ||
if (isFragment(nodes)) { | ||
@@ -101,61 +263,11 @@ nodes.forEach((node) => { | ||
/** | ||
* Add a node without open or close it. | ||
* | ||
* @remarks | ||
* It's useful for nodes which don't have content. | ||
* | ||
* @param type - Type of this node. | ||
* @param children - Children of this node. | ||
* @param value - Value of this node. | ||
* @param props - Additional props of this node. | ||
* | ||
* @returns The added node. | ||
*/ | ||
addNode: StateMethod<'addNode'> = (...args) => { | ||
this.stack.addNode(...args) | ||
return this | ||
} | ||
/// Use a remark parser to serialize current AST stored. | ||
override toString = (remark: RemarkParser): string => remark.stringify(this.build()) as string | ||
/** | ||
* Open a node, and all nodes created after this method will be set as the children of the node until a `closeNode` been called. | ||
* | ||
* @remarks | ||
* You can imagine `openNode` as the left half of parenthesis and `closeNode` as the right half. For nodes have children, your runner should just take care of the node itself and let other runners to handle the children. | ||
* | ||
* @param type - Type of this node. | ||
* @param value - Value of this node. | ||
* @param props - Additional props of this node. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
openNode: StateMethod<'openNode'> = (...args) => { | ||
this.stack.openNode(...args) | ||
return this | ||
} | ||
/// Transform a prosemirror node tree into remark AST. | ||
run = (tree: Node) => { | ||
this.next(tree) | ||
/** | ||
* Close current node. | ||
* | ||
* @returns The node closed. | ||
*/ | ||
closeNode: StateMethod<'closeNode'> = (...args) => { | ||
this.stack.closeNode(...args) | ||
return this | ||
} | ||
/** | ||
* Used when current node has marks, the serializer will auto combine marks nearby. | ||
* | ||
* @param mark - The mark need to be opened. | ||
* @param type - Type of this mark. | ||
* @param value - Value of this mark. | ||
* @param props - Additional props of this mark. | ||
* | ||
* @returns The state instance. | ||
*/ | ||
withMark: StateMethod<'openMark'> = (...args) => { | ||
this.stack.openMark(...args) | ||
return this | ||
} | ||
} |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { Mark as ProseMark, Node as ProseNode } from '@milkdown/prose/model' | ||
import type { Mark, Node } from '@milkdown/prose/model' | ||
import type { State } from './state' | ||
import type { SerializerState } from './state' | ||
/// The serializer type which is used to transform prosemirror node into markdown text. | ||
export type Serializer = (content: Node) => string | ||
/// The spec for node serializer in schema. | ||
export interface NodeSerializerSpec { | ||
match: (node: ProseNode) => boolean | ||
runner: (state: State, node: ProseNode) => void | ||
/// The match function to check if the node is the target node. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// match: (node) => node.type.name === 'paragraph' | ||
/// ``` | ||
match: (node: Node) => boolean | ||
/// The runner function to transform the node into markdown text. | ||
/// Generally, you should call methods in `state` to add node to state. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// runner: (state, node) => { | ||
/// state | ||
/// .openNode(node.type.name) | ||
/// .next(node.content) | ||
/// .closeNode(); | ||
/// } | ||
/// ``` | ||
runner: (state: SerializerState, node: Node) => void | ||
} | ||
/// The spec for mark serializer in schema. | ||
export interface MarkSerializerSpec { | ||
match: (mark: ProseMark) => boolean | ||
runner: (state: State, mark: ProseMark, node: ProseNode) => void | boolean | ||
/// The match function to check if the node is the target mark. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// match: (mark) => mark.type.name === 'emphasis' | ||
/// ``` | ||
match: (mark: Mark) => boolean | ||
/// The runner function to transform the node into markdown text. | ||
/// Generally, you should call methods in `state` to add mark to state. | ||
/// For example: | ||
/// | ||
/// ```typescript | ||
/// runner: (state, mark, node) => { | ||
/// state.withMark(mark, 'emphasis'); | ||
/// } | ||
/// ``` | ||
runner: (state: SerializerState, mark: Mark, node: Node) => void | boolean | ||
} | ||
export type SerializerSpec = NodeSerializerSpec | MarkSerializerSpec | ||
export type InnerSerializerSpecMap = Record<string, SerializerSpec> |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import { stackOverFlow } from '@milkdown/exception' | ||
interface ElInstance<U> { | ||
push: (node: U, ...rest: U[]) => void | ||
/// The element of the stack, which holds an array of nodes. | ||
export abstract class StackElement<Node> { | ||
/// A method that can `push` a node into the element. | ||
abstract push(node: Node, ...rest: Node[]): void | ||
} | ||
interface StackCtx<T extends ElInstance<U>, U> { | ||
readonly elements: T[] | ||
} | ||
/// The stack that is used to store the elements. | ||
/// | ||
/// > Generally, you don't need to use this class directly. | ||
/// | ||
/// When using the stack, users can call `stack.open` to push a new element into the stack. | ||
/// And use `stack.push` to push a node into the top element. | ||
/// Then use `stack.close` to close the top element and pop it. | ||
/// | ||
/// For example: `stack.open(A).push(B).push(C).close()` will generate a structure like `A(B, C)`. | ||
export class Stack<Node, Element extends StackElement<Node>> { | ||
protected elements: Element[] = [] | ||
export const getStackUtil = <Node, El extends ElInstance<Node>, Ctx extends StackCtx<El, Node>>() => { | ||
const size = (ctx: Ctx): number => ctx.elements.length | ||
/// Get the size of the stack. | ||
size = (): number => { | ||
return this.elements.length | ||
} | ||
const top = (ctx: Ctx): El | undefined => ctx.elements[size(ctx) - 1] | ||
/// Get the top element of the stack. | ||
top = (): Element | undefined => { | ||
return this.elements.at(-1) | ||
} | ||
const push | ||
= (ctx: Ctx) => | ||
(node: Node): void => { | ||
top(ctx)?.push(node) | ||
} | ||
/// Push a node into the top element. | ||
push = (node: Node): void => { | ||
this.top()?.push(node) | ||
} | ||
const open | ||
= (ctx: Ctx) => | ||
(node: El): void => { | ||
ctx.elements.push(node) | ||
} | ||
/// Push a new element. | ||
open = (node: Element): void => { | ||
this.elements.push(node) | ||
} | ||
const close = (ctx: Ctx): El => { | ||
const el = ctx.elements.pop() | ||
/// Close the top element and pop it. | ||
close = (): Element => { | ||
const el = this.elements.pop() | ||
if (!el) | ||
@@ -36,10 +50,2 @@ throw stackOverFlow() | ||
} | ||
return { | ||
size, | ||
top, | ||
push, | ||
open, | ||
close, | ||
} | ||
} |
/* Copyright 2021, Milkdown by Mirone. */ | ||
import type { MarkSpec, NodeSpec } from '@milkdown/prose/model' | ||
import type { Root } from 'mdast' | ||
import type { remark } from 'remark' | ||
import type { Plugin } from 'unified' | ||
import type { Node } from 'unist' | ||
import type { MarkParserSpec, NodeParserSpec } from '../parser/types' | ||
import type { MarkSerializerSpec, NodeSerializerSpec } from '../serializer/types' | ||
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue } | ||
/// @internal | ||
export type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue } | ||
/// @internal | ||
export type JSONRecord = Record<string, JSONValue> | ||
/// The universal type of a [remark plugin](https://github.com/remarkjs/remark/blob/main/doc/plugins.md). | ||
export type RemarkPlugin = Plugin<never[], Root> | ||
/// The type of [remark instance](https://github.com/remarkjs/remark/tree/main/packages/remark#remark-1). | ||
export type RemarkParser = ReturnType<typeof remark> | ||
/// The universal type of a node in [mdast](https://github.com/syntax-tree/mdast). | ||
export type MarkdownNode = Node & { children?: MarkdownNode[]; [x: string]: unknown } | ||
/// Schema spec for node. It is a super set of [NodeSpec](https://prosemirror.net/docs/ref/#model.NodeSpec). | ||
export interface NodeSchema extends NodeSpec { | ||
/// To markdown serializer spec. | ||
readonly toMarkdown: NodeSerializerSpec | ||
/// Parse markdown serializer spec. | ||
readonly parseMarkdown: NodeParserSpec | ||
/// The priority of the node, by default it's 50. | ||
readonly priority?: number | ||
} | ||
/// Schema spec for mark. It is a super set of [MarkSpec](https://prosemirror.net/docs/ref/#model.MarkSpec). | ||
export interface MarkSchema extends MarkSpec { | ||
/// To markdown serializer spec. | ||
readonly toMarkdown: MarkSerializerSpec | ||
/// Parse markdown serializer spec. | ||
readonly parseMarkdown: MarkParserSpec | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
91711
47
1652
2
1
+ Added@milkdown/exception@7.0.0-next.07.5.9(transitive)
+ Added@milkdown/prose@7.5.9(transitive)
+ Addedprosemirror-changeset@2.2.1(transitive)
+ Addedprosemirror-tables@1.6.1(transitive)
- Removed@milkdown/exception@6.5.4(transitive)
- Removed@milkdown/prose@6.5.4(transitive)