@dogsbay/editor 0.0.0-stage → 0.2.0-beta.174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -2
- package/dist/block-view.d.ts +25 -0
- package/dist/block-view.js +28 -0
- package/dist/block.d.ts +69 -0
- package/dist/block.js +114 -0
- package/dist/bridge.d.ts +37 -0
- package/dist/bridge.js +626 -0
- package/dist/diff.d.ts +38 -0
- package/dist/diff.js +136 -0
- package/dist/directive-context.d.ts +16 -0
- package/dist/directive-context.js +87 -0
- package/dist/directive-fields.d.ts +8 -0
- package/dist/directive-fields.js +60 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +18 -0
- package/dist/schema.d.ts +54 -0
- package/dist/schema.js +395 -0
- package/package.json +68 -3
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dogsbay/editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TipTap/ProseMirror editor for Dogsbay Markdown: the schema, a lossless bridge between Dogsbay's
|
|
4
|
+
block tree (`TreeNode[]`, from `@dogsbay/format-dogsbay-md`) and ProseMirror documents, and the
|
|
5
|
+
editor on one block of a page. MIT.
|
|
6
|
+
|
|
7
|
+
## Edit one block of a page
|
|
8
|
+
|
|
9
|
+
For a host that shows the real rendered page and edits a block in place. The host gives the
|
|
10
|
+
block's source lines (from the page's `data-line` / `data-end-line`), and splices what comes back
|
|
11
|
+
over those lines. Nothing else in the file is touched, so a commit holds only the edited block.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// Where the Markdown parser already is (the host's own page):
|
|
15
|
+
import { openBlock } from "@dogsbay/editor/block";
|
|
16
|
+
|
|
17
|
+
const block = openBlock(lines, { style: project.style, page }); // style: dogsbay.config.yml's `style:`
|
|
18
|
+
if (!block.editable) openAsMarkdown(block.reason);
|
|
19
|
+
block.doc; // the block as a ProseMirror document (JSON)
|
|
20
|
+
block.verbatim; // parts the editor shows verbatim (unmodelled blocks, raw directives)
|
|
21
|
+
block.exact; // false: saving an edit will also restyle this block's untouched lines
|
|
22
|
+
|
|
23
|
+
// In the page the block is shown in (TipTap, no Markdown parser):
|
|
24
|
+
import { mountBlock } from "@dogsbay/editor/block-view";
|
|
25
|
+
|
|
26
|
+
const view = mountBlock(element, block.doc, { onUpdate: ({ changed }) => markDirty(changed) });
|
|
27
|
+
// …the host adds its toolbar, keys and paste…
|
|
28
|
+
const { doc } = view.read();
|
|
29
|
+
view.destroy();
|
|
30
|
+
|
|
31
|
+
// Back where the parser is:
|
|
32
|
+
const { text, changed } = block.close(doc); // splice `text` over the block's lines
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **No change, no bytes changed.** A block opened and closed without a change (or with every change
|
|
36
|
+
undone) gives back its lines exactly; the edited tree is compared with the opened one rather than
|
|
37
|
+
re-serialized. Checked on every block of the Dogsbay Markdown feature examples, through a real
|
|
38
|
+
editor.
|
|
39
|
+
- **A changed block** is written by format-dogsbay-md's serializer in the host's style, with the
|
|
40
|
+
block's indentation, the blank lines around it as written, and its line ends. An edit that
|
|
41
|
+
writes the same text (an empty paragraph added and dropped) is no change either.
|
|
42
|
+
- **`editable` is false**, with a `reason`, when a block cannot be taken apart safely: lines not
|
|
43
|
+
indented alike (a lazy continuation line, tabs on one line and spaces on another), a footnote
|
|
44
|
+
reference (its definition is elsewhere on the page), or a reference link when the host did not
|
|
45
|
+
pass `page`. With `page`, reference links are resolved from the page's definitions and an edited
|
|
46
|
+
block writes them as inline links.
|
|
47
|
+
- **`exact` is false** when the serializer would not write the block as it is written now, so an
|
|
48
|
+
edit restyles its other lines too. The common case: the parser reads a soft line break as a
|
|
49
|
+
space, so an edited paragraph wrapped over several lines comes back on one line.
|
|
50
|
+
- **Templating, includes and conditions** are parsed in preserve mode: they stay as written, as
|
|
51
|
+
first-class nodes or verbatim parts. A host may open a block with `verbatim > 0` as Markdown
|
|
52
|
+
instead.
|
|
53
|
+
- The two entries are split so that each side loads only what it needs; the documents pass
|
|
54
|
+
between them as JSON (`postMessage` into a sandboxed frame works). Gzipped, `./block-view` is
|
|
55
|
+
about 136 KB (TipTap and ProseMirror) and `./block` about 163 KB, nearly all of it the
|
|
56
|
+
format-dogsbay-md parser and serializer a host already has. Neither imports the change-review
|
|
57
|
+
view or `@dogsbay/tree-diff`.
|
|
58
|
+
|
|
59
|
+
## The whole document
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { extensions, treeToPmDoc, pmDocToTree } from "@dogsbay/editor";
|
|
63
|
+
import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md/browser";
|
|
64
|
+
|
|
65
|
+
const doc = treeToPmDoc(dogsbayMdToTree(markdown, { directives: "preserve" }).tree);
|
|
66
|
+
// new Editor({ extensions, content: doc }) … pmDocToTree(editor.getJSON())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The bridge is total over the schema, and never drops what it cannot model: tree content without a
|
|
70
|
+
first-class editor node rides a verbatim atom, and props the editor does not edit ride a declared
|
|
71
|
+
attribute. The main entry also has the change-review marks (`diffTreeToPmDoc`, over
|
|
72
|
+
`@dogsbay/tree-diff`) and the directive preview helpers (`evaluateTest`, `validateDirectiveField`).
|
|
73
|
+
|
|
74
|
+
The package has no view layer of its own: bring `@tiptap/react` or plain `@tiptap/core`.
|
|
75
|
+
`@tiptap/core`, `@tiptap/pm` and `@tiptap/starter-kit` are peer dependencies, so the host and this
|
|
76
|
+
package share one copy of ProseMirror. ESM only.
|
|
77
|
+
|
|
78
|
+
What Markdown cannot hold, the bridge does not carry back: bold on inline code, a link's `target`
|
|
79
|
+
or `rel`, a table column's width. An edit that only changes these is no change.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dogsbay/editor/block-view`: the editor on one block's document (from `./block`'s `openBlock`),
|
|
3
|
+
* for the page the block is shown in. TipTap and the schema, no Markdown parser: the host opens
|
|
4
|
+
* and closes the block's text with `./block` where its parser already is, and passes the document
|
|
5
|
+
* here and back as JSON.
|
|
6
|
+
*/
|
|
7
|
+
import { Editor } from "@tiptap/core";
|
|
8
|
+
import { type PMNode } from "./bridge.js";
|
|
9
|
+
export interface MountedBlock {
|
|
10
|
+
/** The TipTap editor; the host adds its own toolbar and keys. */
|
|
11
|
+
editor: Editor;
|
|
12
|
+
/** The document now, and whether it differs from the one mounted (an undone edit does not). */
|
|
13
|
+
read(): {
|
|
14
|
+
doc: PMNode;
|
|
15
|
+
changed: boolean;
|
|
16
|
+
};
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
19
|
+
/** Mount the editor on `element` for a block's document (`openBlock(text).doc`). */
|
|
20
|
+
export declare function mountBlock(element: HTMLElement, doc: PMNode, options?: {
|
|
21
|
+
onUpdate?: (block: {
|
|
22
|
+
doc: PMNode;
|
|
23
|
+
changed: boolean;
|
|
24
|
+
}) => void;
|
|
25
|
+
}): MountedBlock;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dogsbay/editor/block-view`: the editor on one block's document (from `./block`'s `openBlock`),
|
|
3
|
+
* for the page the block is shown in. TipTap and the schema, no Markdown parser: the host opens
|
|
4
|
+
* and closes the block's text with `./block` where its parser already is, and passes the document
|
|
5
|
+
* here and back as JSON.
|
|
6
|
+
*/
|
|
7
|
+
import { Editor } from "@tiptap/core";
|
|
8
|
+
import { extensions } from "./schema.js";
|
|
9
|
+
import { pmDocToTree } from "./bridge.js";
|
|
10
|
+
/** JSON with sorted keys, so two readings of one tree compare equal. */
|
|
11
|
+
const stable = (value) => JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v)
|
|
12
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]))
|
|
13
|
+
: v);
|
|
14
|
+
/** Mount the editor on `element` for a block's document (`openBlock(text).doc`). */
|
|
15
|
+
export function mountBlock(element, doc, options = {}) {
|
|
16
|
+
const editor = new Editor({ element, extensions, content: doc });
|
|
17
|
+
// The editor's own reading of the document (default attributes filled in) is what "unchanged" means
|
|
18
|
+
const opened = stable(pmDocToTree(editor.getJSON()));
|
|
19
|
+
const read = () => {
|
|
20
|
+
const now = editor.getJSON();
|
|
21
|
+
return { doc: now, changed: stable(pmDocToTree(now)) !== opened };
|
|
22
|
+
};
|
|
23
|
+
if (options.onUpdate) {
|
|
24
|
+
const onUpdate = options.onUpdate;
|
|
25
|
+
editor.on("update", () => onUpdate(read()));
|
|
26
|
+
}
|
|
27
|
+
return { editor, read, destroy: () => editor.destroy() };
|
|
28
|
+
}
|
package/dist/block.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dogsbay/editor/block`: the editor on ONE block of a page, for a host that shows the real
|
|
3
|
+
* rendered page and edits a block in place (the playground's preview).
|
|
4
|
+
*
|
|
5
|
+
* The host gives the block's source lines (a `data-line`–`data-end-line` range of the page), the
|
|
6
|
+
* editor works on them, and the host splices what comes back over those lines. Nothing else in the
|
|
7
|
+
* file is read or written, so git sees only the block that was edited.
|
|
8
|
+
*
|
|
9
|
+
* Opened and closed with no change, a block gives back its lines byte for byte: the edited tree is
|
|
10
|
+
* compared with the opened one, and an equal tree returns the original text, not a re-serialization
|
|
11
|
+
* (undoing every change counts as no change). A changed block is written by format-dogsbay-md's
|
|
12
|
+
* serializer in the host's style, re-indented and with the original line ends; `exact` says
|
|
13
|
+
* beforehand whether that serializer gives back this block as written (when not, saving an edit
|
|
14
|
+
* also restyles the block's untouched lines). One such case is common: the parser reads a soft
|
|
15
|
+
* line break as a space, so an edited paragraph that was wrapped over several lines comes back
|
|
16
|
+
* on one line.
|
|
17
|
+
*
|
|
18
|
+
* Some blocks cannot be edited this way, and `editable` is false with the reason: indentation that
|
|
19
|
+
* does not start every line the same (a lazy continuation line, tabs on one line and spaces on the
|
|
20
|
+
* next — taking it apart would turn the block into code), a footnote reference (its definition is
|
|
21
|
+
* elsewhere on the page), or a block the parser makes nothing of. Reference links are resolved with
|
|
22
|
+
* the page's definitions when the host passes the page (`page`), and written back as inline links.
|
|
23
|
+
*
|
|
24
|
+
* Directives are parsed in preserve mode: templating, includes and conditions stay first-class
|
|
25
|
+
* nodes or ride verbatim atoms; `verbatim` counts the atoms, which a host may prefer to open as
|
|
26
|
+
* Markdown instead.
|
|
27
|
+
*
|
|
28
|
+
* Two entries, so each side loads only what it needs: this one (`openBlock`: text in, document
|
|
29
|
+
* out, document in, text back) needs the Markdown parser and no DOM, and runs where the host's
|
|
30
|
+
* source editor already has the parser; `./block-view` (`mountBlock`) is the editor on that
|
|
31
|
+
* document, with TipTap and no parser, for the page the block is shown in (the host passes the
|
|
32
|
+
* document between them as JSON). Neither imports the change-review view (`diff.ts`,
|
|
33
|
+
* `@dogsbay/tree-diff`).
|
|
34
|
+
*/
|
|
35
|
+
import { type MarkdownStyle, type SerializeOptions } from "@dogsbay/format-dogsbay-md/browser";
|
|
36
|
+
import { type PMNode } from "./bridge.js";
|
|
37
|
+
export interface BlockOptions {
|
|
38
|
+
/** The project's `style:` (dogsbay.config.yml), used when a changed block is written. */
|
|
39
|
+
style?: MarkdownStyle;
|
|
40
|
+
/** Other serializer options (callout form, nested indentation). */
|
|
41
|
+
serialize?: Omit<SerializeOptions, keyof MarkdownStyle>;
|
|
42
|
+
/**
|
|
43
|
+
* The whole page, for its reference-link definitions (`[name]: url`): without them a block's
|
|
44
|
+
* `[text][name]` reads as plain text, and an edit would write it back escaped. Without `page`, a
|
|
45
|
+
* block using a reference link is not editable.
|
|
46
|
+
*/
|
|
47
|
+
page?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ClosedBlock {
|
|
50
|
+
/** The block's new lines; the original text, byte for byte, when nothing changed. */
|
|
51
|
+
text: string;
|
|
52
|
+
/** False when the text is the original's, byte for byte (an edit undone, or one that writes the same). */
|
|
53
|
+
changed: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface OpenedBlock {
|
|
56
|
+
/** The block as a ProseMirror document (JSON) for `@dogsbay/editor`'s schema. */
|
|
57
|
+
doc: PMNode;
|
|
58
|
+
/** Whether the block can be edited here; when false, open it as Markdown (`reason` says why). */
|
|
59
|
+
editable: boolean;
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** Nodes the editor shows verbatim (unmodelled blocks, raw directives). */
|
|
62
|
+
verbatim: number;
|
|
63
|
+
/** Whether the serializer writes this block exactly as it is written now. */
|
|
64
|
+
exact: boolean;
|
|
65
|
+
/** The block's text for an edited document (the opened `doc` when none is given). */
|
|
66
|
+
close(doc?: PMNode): ClosedBlock;
|
|
67
|
+
}
|
|
68
|
+
/** Open a block's source lines for editing (no DOM: the document, and how to write it back). */
|
|
69
|
+
export declare function openBlock(text: string, options?: BlockOptions): OpenedBlock;
|
package/dist/block.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dogsbay/editor/block`: the editor on ONE block of a page, for a host that shows the real
|
|
3
|
+
* rendered page and edits a block in place (the playground's preview).
|
|
4
|
+
*
|
|
5
|
+
* The host gives the block's source lines (a `data-line`–`data-end-line` range of the page), the
|
|
6
|
+
* editor works on them, and the host splices what comes back over those lines. Nothing else in the
|
|
7
|
+
* file is read or written, so git sees only the block that was edited.
|
|
8
|
+
*
|
|
9
|
+
* Opened and closed with no change, a block gives back its lines byte for byte: the edited tree is
|
|
10
|
+
* compared with the opened one, and an equal tree returns the original text, not a re-serialization
|
|
11
|
+
* (undoing every change counts as no change). A changed block is written by format-dogsbay-md's
|
|
12
|
+
* serializer in the host's style, re-indented and with the original line ends; `exact` says
|
|
13
|
+
* beforehand whether that serializer gives back this block as written (when not, saving an edit
|
|
14
|
+
* also restyles the block's untouched lines). One such case is common: the parser reads a soft
|
|
15
|
+
* line break as a space, so an edited paragraph that was wrapped over several lines comes back
|
|
16
|
+
* on one line.
|
|
17
|
+
*
|
|
18
|
+
* Some blocks cannot be edited this way, and `editable` is false with the reason: indentation that
|
|
19
|
+
* does not start every line the same (a lazy continuation line, tabs on one line and spaces on the
|
|
20
|
+
* next — taking it apart would turn the block into code), a footnote reference (its definition is
|
|
21
|
+
* elsewhere on the page), or a block the parser makes nothing of. Reference links are resolved with
|
|
22
|
+
* the page's definitions when the host passes the page (`page`), and written back as inline links.
|
|
23
|
+
*
|
|
24
|
+
* Directives are parsed in preserve mode: templating, includes and conditions stay first-class
|
|
25
|
+
* nodes or ride verbatim atoms; `verbatim` counts the atoms, which a host may prefer to open as
|
|
26
|
+
* Markdown instead.
|
|
27
|
+
*
|
|
28
|
+
* Two entries, so each side loads only what it needs: this one (`openBlock`: text in, document
|
|
29
|
+
* out, document in, text back) needs the Markdown parser and no DOM, and runs where the host's
|
|
30
|
+
* source editor already has the parser; `./block-view` (`mountBlock`) is the editor on that
|
|
31
|
+
* document, with TipTap and no parser, for the page the block is shown in (the host passes the
|
|
32
|
+
* document between them as JSON). Neither imports the change-review view (`diff.ts`,
|
|
33
|
+
* `@dogsbay/tree-diff`).
|
|
34
|
+
*/
|
|
35
|
+
import { dogsbayMdToTree, treeToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
|
|
36
|
+
import { pmDocToTree, treeToPmDoc } from "./bridge.js";
|
|
37
|
+
const VERBATIM = new Set(["dogsbayBlock", "dogsbayInline", "directiveRaw", "directiveRawInline"]);
|
|
38
|
+
function countVerbatim(node) {
|
|
39
|
+
return (VERBATIM.has(node.type) ? 1 : 0) + (node.content ?? []).reduce((n, c) => n + countVerbatim(c), 0);
|
|
40
|
+
}
|
|
41
|
+
/** JSON with sorted keys: two trees built by different paths compare by content. */
|
|
42
|
+
function stable(value) {
|
|
43
|
+
return JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v)
|
|
44
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]))
|
|
45
|
+
: v);
|
|
46
|
+
}
|
|
47
|
+
/** A blank line as CommonMark has it: spaces and tabs only (a no-break space is text). */
|
|
48
|
+
const BLANK = /^[ \t]*$/;
|
|
49
|
+
function layout(text) {
|
|
50
|
+
const bom = text.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
51
|
+
const rest = text.slice(bom.length);
|
|
52
|
+
const parts = rest.match(/[^\r\n]*(?:\r\n|\r|\n|$)/g).filter((p, i, all) => p !== "" || i < all.length - 1);
|
|
53
|
+
const line = (p) => p.replace(/(?:\r\n|\r|\n)$/, "");
|
|
54
|
+
let first = 0;
|
|
55
|
+
while (first < parts.length && BLANK.test(line(parts[first])))
|
|
56
|
+
first++;
|
|
57
|
+
let last = parts.length - 1;
|
|
58
|
+
while (last >= first && BLANK.test(line(parts[last])))
|
|
59
|
+
last--;
|
|
60
|
+
if (first > last)
|
|
61
|
+
return { lead: bom + rest, tail: "", eol: "\n", indent: "", inner: "" };
|
|
62
|
+
const middle = parts.slice(first, last + 1);
|
|
63
|
+
const lead = bom + parts.slice(0, first).join("");
|
|
64
|
+
const lastEnd = /(?:\r\n|\r|\n)$/.exec(middle[middle.length - 1])?.[0] ?? "";
|
|
65
|
+
const tail = lastEnd + parts.slice(last + 1).join("");
|
|
66
|
+
const eol = /\r\n|\r|\n/.exec(rest)?.[0] ?? "\n";
|
|
67
|
+
const lines = middle.map(line);
|
|
68
|
+
const indents = lines.filter((l) => !BLANK.test(l)).map((l) => /^[ \t]*/.exec(l)[0]);
|
|
69
|
+
let indent = indents[0];
|
|
70
|
+
for (const i of indents)
|
|
71
|
+
while (!i.startsWith(indent))
|
|
72
|
+
indent = indent.slice(0, -1);
|
|
73
|
+
const uneven = indent !== indents[0]
|
|
74
|
+
? "its lines are not indented alike (a lazy continuation line, or tabs and spaces mixed)"
|
|
75
|
+
: undefined;
|
|
76
|
+
const inner = lines.map((l) => (l.startsWith(indent) ? l.slice(indent.length) : l.replace(/^[ \t]*/, ""))).join("\n");
|
|
77
|
+
return { lead, tail, eol, indent, inner, ...(uneven ? { uneven } : {}) };
|
|
78
|
+
}
|
|
79
|
+
function assemble(md, l) {
|
|
80
|
+
const lines = md.replace(/\n+$/, "").split("\n").map((line) => (line === "" ? "" : l.indent + line));
|
|
81
|
+
return l.lead + lines.join(l.eol) + l.tail;
|
|
82
|
+
}
|
|
83
|
+
/** `[name]: destination` lines of a page (reference-link definitions; not footnotes). */
|
|
84
|
+
function definitions(page) {
|
|
85
|
+
return (page.match(/^ {0,3}\[(?!\^)(?:[^\]\\]|\\.)+\]:[ \t]*\S.*$/gm) ?? []).join("\n");
|
|
86
|
+
}
|
|
87
|
+
/** Open a block's source lines for editing (no DOM: the document, and how to write it back). */
|
|
88
|
+
export function openBlock(text, options = {}) {
|
|
89
|
+
const l = layout(text);
|
|
90
|
+
// A leading line end: a block starting `---` is a rule, never front matter
|
|
91
|
+
const defs = options.page !== undefined ? definitions(options.page) : "";
|
|
92
|
+
const tree = dogsbayMdToTree(`\n${l.inner}${defs ? `\n\n${defs}\n` : ""}`, { directives: "preserve" }).tree;
|
|
93
|
+
const opts = { ...options.serialize, ...options.style };
|
|
94
|
+
const doc = treeToPmDoc(tree);
|
|
95
|
+
const opened = stable(pmDocToTree(doc));
|
|
96
|
+
const reason = l.uneven ??
|
|
97
|
+
(/\[\^[^\]]+\]/.test(l.inner) ? "it has a footnote reference, whose definition is elsewhere on the page" : undefined) ??
|
|
98
|
+
(options.page === undefined && /\]\[|\]:/.test(l.inner) ? "it may use the page's reference links: pass `page`" : undefined) ??
|
|
99
|
+
(l.inner.trim() && !tree.length ? "the parser makes no block of it" : undefined);
|
|
100
|
+
return {
|
|
101
|
+
doc,
|
|
102
|
+
editable: !reason,
|
|
103
|
+
...(reason ? { reason } : {}),
|
|
104
|
+
verbatim: countVerbatim(doc),
|
|
105
|
+
exact: treeToDogsbayMd(tree, opts).replace(/\n+$/, "") === l.inner,
|
|
106
|
+
close(edited = doc) {
|
|
107
|
+
const now = pmDocToTree(edited);
|
|
108
|
+
if (stable(now) === opened)
|
|
109
|
+
return { text, changed: false };
|
|
110
|
+
const written = assemble(treeToDogsbayMd(now, opts), l);
|
|
111
|
+
return { text: written === text ? text : written, changed: written !== text };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreeNode ⇄ TipTap(ProseMirror) JSON bridge.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions over plain JSON (no editor runtime), so the round-trip is
|
|
5
|
+
* unit-testable without a browser. Two invariants:
|
|
6
|
+
*
|
|
7
|
+
* 1. TOTALITY over the schema — pmBlockToTree/pmInlineToTree handle every
|
|
8
|
+
* node and mark the schema in schema.ts can produce (including
|
|
9
|
+
* StarterKit's blockquote, horizontalRule, hardBreak and link input
|
|
10
|
+
* rules). The throwing default exists only to catch future schema
|
|
11
|
+
* additions loudly instead of corrupting documents silently.
|
|
12
|
+
* 2. "Never drop what you can't model" — TreeNode content the editor has
|
|
13
|
+
* no first-class node for rides a `dogsbayBlock`/`dogsbayInline` atom
|
|
14
|
+
* carrying the original JSON verbatim, and TreeNode props/meta the
|
|
15
|
+
* editor doesn't edit ride the declared `dogsbayProps` attribute
|
|
16
|
+
* (ProseMirror drops undeclared attrs — see schema.ts).
|
|
17
|
+
*
|
|
18
|
+
* Known vocabulary limit (matches the parser, not an editor regression):
|
|
19
|
+
* InlineCode carries no formatting flags, so `code` combined with other
|
|
20
|
+
* marks keeps only the code — same as parsing `**` + backtick markdown.
|
|
21
|
+
*/
|
|
22
|
+
import type { TreeNode } from "@dogsbay/types";
|
|
23
|
+
export interface PMMark {
|
|
24
|
+
type: string;
|
|
25
|
+
attrs?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface PMNode {
|
|
28
|
+
type: string;
|
|
29
|
+
attrs?: Record<string, unknown>;
|
|
30
|
+
content?: PMNode[];
|
|
31
|
+
text?: string;
|
|
32
|
+
marks?: PMMark[];
|
|
33
|
+
}
|
|
34
|
+
export declare function treeToPmDoc(tree: TreeNode[]): PMNode;
|
|
35
|
+
/** Block-level mapping without the doc wrapper — for replacing ONE block with what a snippet parses to. */
|
|
36
|
+
export declare function treeToPmBlocks(tree: TreeNode[]): PMNode[];
|
|
37
|
+
export declare function pmDocToTree(doc: PMNode): TreeNode[];
|