@dogsbay/editor 0.2.0-beta.174 → 0.2.0-beta.175
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 +15 -4
- package/dist/block-view.js +3 -2
- package/dist/block.d.ts +3 -3
- package/dist/block.js +4 -4
- package/dist/bridge.js +118 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/schema.d.ts +15 -0
- package/dist/schema.js +74 -4
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -45,14 +45,25 @@ const { text, changed } = block.close(doc); // splice `text` over the block's
|
|
|
45
45
|
pass `page`. With `page`, reference links are resolved from the page's definitions and an edited
|
|
46
46
|
block writes them as inline links.
|
|
47
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.
|
|
49
|
-
|
|
48
|
+
edit restyles its other lines too. Rare: the block is parsed with format-dogsbay-md's
|
|
49
|
+
`asWritten`, so a paragraph wrapped over several lines keeps its lines (the editor draws each
|
|
50
|
+
break as a space), inline code across a line too, a table keeps its delimiter row (`|---|`), a
|
|
51
|
+
list stays tight or loose and each callout keeps its form (`:::note` or `> [!NOTE]`). Measured
|
|
52
|
+
on Dogsbay's `docs/`: 3,536 of 3,561 editable blocks exact with the docs' list style (3,353 with
|
|
53
|
+
the default `listIndent: 4`: pass the project's `style`).
|
|
54
|
+
- **What typing gives is written as Markdown reads it**: a space typed before or after a bold,
|
|
55
|
+
italic, struck-out or code word goes outside its marks (`** bold**` is not bold).
|
|
56
|
+
- **The block editor adds no empty paragraph** under the block (TipTap's `TrailingNode` is off in
|
|
57
|
+
`./block-view`; `editorExtensions({ trailingNode: false })` for a host building its own).
|
|
58
|
+
- **A callout is drawn with the site's markup** (render-html's classes, `data-variant`,
|
|
59
|
+
`data-type`, icon and title, from `@dogsbay/render-html/callout-markup`), so the page's own
|
|
60
|
+
styles show it as the page does.
|
|
50
61
|
- **Templating, includes and conditions** are parsed in preserve mode: they stay as written, as
|
|
51
62
|
first-class nodes or verbatim parts. A host may open a block with `verbatim > 0` as Markdown
|
|
52
63
|
instead.
|
|
53
64
|
- The two entries are split so that each side loads only what it needs; the documents pass
|
|
54
65
|
between them as JSON (`postMessage` into a sandboxed frame works). Gzipped, `./block-view` is
|
|
55
|
-
about
|
|
66
|
+
about 138 KB (TipTap and ProseMirror) and `./block` about 163 KB, nearly all of it the
|
|
56
67
|
format-dogsbay-md parser and serializer a host already has. Neither imports the change-review
|
|
57
68
|
view or `@dogsbay/tree-diff`.
|
|
58
69
|
|
|
@@ -75,5 +86,5 @@ The package has no view layer of its own: bring `@tiptap/react` or plain `@tipta
|
|
|
75
86
|
`@tiptap/core`, `@tiptap/pm` and `@tiptap/starter-kit` are peer dependencies, so the host and this
|
|
76
87
|
package share one copy of ProseMirror. ESM only.
|
|
77
88
|
|
|
78
|
-
What Markdown cannot hold, the bridge does not carry back:
|
|
89
|
+
What Markdown cannot hold, the bridge does not carry back: a link's `target`
|
|
79
90
|
or `rel`, a table column's width. An edit that only changes these is no change.
|
package/dist/block-view.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* here and back as JSON.
|
|
6
6
|
*/
|
|
7
7
|
import { Editor } from "@tiptap/core";
|
|
8
|
-
import {
|
|
8
|
+
import { editorExtensions } from "./schema.js";
|
|
9
9
|
import { pmDocToTree } from "./bridge.js";
|
|
10
10
|
/** JSON with sorted keys, so two readings of one tree compare equal. */
|
|
11
11
|
const stable = (value) => JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v)
|
|
@@ -13,7 +13,8 @@ const stable = (value) => JSON.stringify(value, (_key, v) => v && typeof v === "
|
|
|
13
13
|
: v);
|
|
14
14
|
/** Mount the editor on `element` for a block's document (`openBlock(text).doc`). */
|
|
15
15
|
export function mountBlock(element, doc, options = {}) {
|
|
16
|
-
|
|
16
|
+
// No trailing empty paragraph: the block is edited where it stands on its page
|
|
17
|
+
const editor = new Editor({ element, extensions: editorExtensions({ trailingNode: false }), content: doc });
|
|
17
18
|
// The editor's own reading of the document (default attributes filled in) is what "unchanged" means
|
|
18
19
|
const opened = stable(pmDocToTree(editor.getJSON()));
|
|
19
20
|
const read = () => {
|
package/dist/block.d.ts
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* (undoing every change counts as no change). A changed block is written by format-dogsbay-md's
|
|
12
12
|
* serializer in the host's style, re-indented and with the original line ends; `exact` says
|
|
13
13
|
* beforehand whether that serializer gives back this block as written (when not, saving an edit
|
|
14
|
-
* also restyles the block's untouched lines).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* also restyles the block's untouched lines). The block is parsed `asWritten`: a paragraph wrapped
|
|
15
|
+
* over several lines keeps them (inline code across a line too), and a table its delimiter row, so
|
|
16
|
+
* an edit changes only the lines it touches.
|
|
17
17
|
*
|
|
18
18
|
* Some blocks cannot be edited this way, and `editable` is false with the reason: indentation that
|
|
19
19
|
* does not start every line the same (a lazy continuation line, tabs on one line and spaces on the
|
package/dist/block.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* (undoing every change counts as no change). A changed block is written by format-dogsbay-md's
|
|
12
12
|
* serializer in the host's style, re-indented and with the original line ends; `exact` says
|
|
13
13
|
* beforehand whether that serializer gives back this block as written (when not, saving an edit
|
|
14
|
-
* also restyles the block's untouched lines).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* also restyles the block's untouched lines). The block is parsed `asWritten`: a paragraph wrapped
|
|
15
|
+
* over several lines keeps them (inline code across a line too), and a table its delimiter row, so
|
|
16
|
+
* an edit changes only the lines it touches.
|
|
17
17
|
*
|
|
18
18
|
* Some blocks cannot be edited this way, and `editable` is false with the reason: indentation that
|
|
19
19
|
* does not start every line the same (a lazy continuation line, tabs on one line and spaces on the
|
|
@@ -89,7 +89,7 @@ export function openBlock(text, options = {}) {
|
|
|
89
89
|
const l = layout(text);
|
|
90
90
|
// A leading line end: a block starting `---` is a rule, never front matter
|
|
91
91
|
const defs = options.page !== undefined ? definitions(options.page) : "";
|
|
92
|
-
const tree = dogsbayMdToTree(`\n${l.inner}${defs ? `\n\n${defs}\n` : ""}`, { directives: "preserve" }).tree;
|
|
92
|
+
const tree = dogsbayMdToTree(`\n${l.inner}${defs ? `\n\n${defs}\n` : ""}`, { directives: "preserve", asWritten: true }).tree;
|
|
93
93
|
const opts = { ...options.serialize, ...options.style };
|
|
94
94
|
const doc = treeToPmDoc(tree);
|
|
95
95
|
const opened = stable(pmDocToTree(doc));
|
package/dist/bridge.js
CHANGED
|
@@ -62,13 +62,30 @@ function inlineToPm(nodes) {
|
|
|
62
62
|
marks.push({ type: "diffIns" });
|
|
63
63
|
if (diffOp === "del")
|
|
64
64
|
marks.push({ type: "diffDel" });
|
|
65
|
-
|
|
65
|
+
// A soft line break kept by the parser (`softBreaks`) is its own atom, drawn as a space: the
|
|
66
|
+
// paragraph reads as the page shows it, and its lines come back as written
|
|
67
|
+
n.text.split("\n").forEach((part, i) => {
|
|
68
|
+
if (i > 0)
|
|
69
|
+
out.push(marks.length ? { type: "softBreak", marks } : { type: "softBreak" });
|
|
70
|
+
if (part)
|
|
71
|
+
out.push(marks.length ? { type: "text", text: part, marks } : { type: "text", text: part });
|
|
72
|
+
});
|
|
66
73
|
continue;
|
|
67
74
|
}
|
|
68
75
|
if (n.type === "code") {
|
|
69
76
|
if (n.text === "")
|
|
70
77
|
continue;
|
|
71
|
-
out
|
|
78
|
+
// Bold, italic or struck-out code (`**\`kubectl\`**`: the parser puts the flags on the code)
|
|
79
|
+
// keeps them as marks beside the code mark; they were dropped, so an edit anywhere in the
|
|
80
|
+
// paragraph lost them
|
|
81
|
+
const marks = [...flagMarks(n), n.minja ? { type: "code", attrs: { minja: true } } : { type: "code" }];
|
|
82
|
+
// A line break kept in the code (`asWritten`): a soft break with the code mark
|
|
83
|
+
n.text.split("\n").forEach((part, i) => {
|
|
84
|
+
if (i > 0)
|
|
85
|
+
out.push({ type: "softBreak", marks });
|
|
86
|
+
if (part)
|
|
87
|
+
out.push({ type: "text", text: part, marks });
|
|
88
|
+
});
|
|
72
89
|
continue;
|
|
73
90
|
}
|
|
74
91
|
if (n.type === "break") {
|
|
@@ -95,7 +112,7 @@ function inlineToPm(nodes) {
|
|
|
95
112
|
continue;
|
|
96
113
|
}
|
|
97
114
|
for (const child of inner) {
|
|
98
|
-
if (child.type === "text") {
|
|
115
|
+
if (child.type === "text" || child.type === "softBreak") {
|
|
99
116
|
out.push({ ...child, marks: [...(child.marks ?? []), linkMark] });
|
|
100
117
|
}
|
|
101
118
|
else {
|
|
@@ -144,8 +161,9 @@ function inlineToPm(nodes) {
|
|
|
144
161
|
function textSpanFromPm(n) {
|
|
145
162
|
const markTypes = new Set((n.marks ?? []).map((m) => m.type));
|
|
146
163
|
if (markTypes.has("code")) {
|
|
147
|
-
//
|
|
148
|
-
|
|
164
|
+
// The parser's shape: the formats around the code as flags on it
|
|
165
|
+
const minja = (n.marks ?? []).some((m) => m.type === "code" && m.attrs?.minja === true);
|
|
166
|
+
return { type: "code", text: n.text ?? "", ...(minja ? { minja: true } : {}), ...marksToFlags(n.marks) };
|
|
149
167
|
}
|
|
150
168
|
const out = { type: "text", text: n.text ?? "" };
|
|
151
169
|
if (markTypes.has("bold"))
|
|
@@ -156,7 +174,95 @@ function textSpanFromPm(n) {
|
|
|
156
174
|
out.strikethrough = true;
|
|
157
175
|
return out;
|
|
158
176
|
}
|
|
177
|
+
/** Adjacent text with the same formats as one node, as the parser gives it (a soft break rejoins its line). */
|
|
178
|
+
function joinText(nodes) {
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const n of nodes) {
|
|
181
|
+
const prev = out[out.length - 1];
|
|
182
|
+
// A code span's line break (a soft break with the code mark) joins its two halves again
|
|
183
|
+
const flags = (x) => { const f = x; return `${!!f.bold}${!!f.italic}${!!f.strikethrough}`; };
|
|
184
|
+
if (n.type === "code" && prev?.type === "code" && (n.text.startsWith("\n") || prev.text.endsWith("\n")) && !!prev.minja === !!n.minja && flags(prev) === flags(n)) {
|
|
185
|
+
out[out.length - 1] = { ...prev, text: prev.text + n.text };
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (n.type === "text" &&
|
|
189
|
+
prev?.type === "text" &&
|
|
190
|
+
!!prev.bold === !!n.bold &&
|
|
191
|
+
!!prev.italic === !!n.italic &&
|
|
192
|
+
!!prev.strikethrough === !!n.strikethrough &&
|
|
193
|
+
prev.diffOp === n.diffOp &&
|
|
194
|
+
(n.text.startsWith("\n") || prev.text.endsWith("\n"))) {
|
|
195
|
+
out[out.length - 1] = { ...prev, text: prev.text + n.text };
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (n.type === "link") {
|
|
199
|
+
out.push({ ...n, children: joinText(n.children) });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
out.push(n);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Inline code typed with a space before or after the word (`code` on, a space, the word) keeps the
|
|
208
|
+
* space outside the span: `` ` word` `` shows a space in the code. The other marks are moved by
|
|
209
|
+
* the serializer; a code span is a node of its own here, so it is done on the way out.
|
|
210
|
+
*/
|
|
211
|
+
function codeSpans(span) {
|
|
212
|
+
if (span.type !== "code" || !span.text.trim())
|
|
213
|
+
return [span];
|
|
214
|
+
// Lengths, not `\s+$`: that regex is quadratic on a long run of spaces inside the text
|
|
215
|
+
const lead = span.text.slice(0, span.text.length - span.text.trimStart().length);
|
|
216
|
+
const trail = span.text.slice(span.text.trimEnd().length);
|
|
217
|
+
if (!lead && !trail)
|
|
218
|
+
return [span];
|
|
219
|
+
return [
|
|
220
|
+
...(lead ? [{ type: "text", text: lead }] : []),
|
|
221
|
+
{ ...span, text: span.text.slice(lead.length, span.text.length - trail.length) },
|
|
222
|
+
...(trail ? [{ type: "text", text: trail }] : []),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
159
225
|
function pmInlineToTree(content) {
|
|
226
|
+
return closeBeforePlainCode(joinText(pmInlineSpans(soleBreaks(content))));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The serializer lets code with no formats bridge an open bold or italic (`*foo \`x\` bar*`, an
|
|
230
|
+
* importer's shape), so plain code straight after formatted text or code would be written inside
|
|
231
|
+
* it (`**\`a\`**\`b\`` came back `**\`a\`\`b\`**`, and `b` bold). The parser marks that boundary
|
|
232
|
+
* with an empty text; so does the bridge.
|
|
233
|
+
*/
|
|
234
|
+
function closeBeforePlainCode(nodes) {
|
|
235
|
+
const formatted = (n) => {
|
|
236
|
+
const f = n;
|
|
237
|
+
return !!n && (n.type === "text" || n.type === "code") && !!(f.bold || f.italic || f.strikethrough);
|
|
238
|
+
};
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const n of nodes) {
|
|
241
|
+
if (n.type === "code" && !formatted(n) && formatted(out[out.length - 1]))
|
|
242
|
+
out.push({ type: "text", text: "" });
|
|
243
|
+
out.push(n.type === "link" ? { ...n, children: closeBeforePlainCode(n.children) } : n);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Soft breaks as Markdown can hold them: never two in a row (a blank line would end the paragraph:
|
|
249
|
+
* deleting a wrapped line's words leaves its two breaks side by side) and never at the start or
|
|
250
|
+
* end, where the line break would add a blank line or be lost.
|
|
251
|
+
*/
|
|
252
|
+
function soleBreaks(content) {
|
|
253
|
+
if (!content?.some((n) => n.type === "softBreak"))
|
|
254
|
+
return content;
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const n of content) {
|
|
257
|
+
if (n.type === "softBreak" && (out.length === 0 || out[out.length - 1].type === "softBreak" || out[out.length - 1].type === "hardBreak"))
|
|
258
|
+
continue;
|
|
259
|
+
out.push(n);
|
|
260
|
+
}
|
|
261
|
+
while (out.length && out[out.length - 1].type === "softBreak")
|
|
262
|
+
out.pop();
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
function pmInlineSpans(content) {
|
|
160
266
|
const out = [];
|
|
161
267
|
for (const n of content ?? []) {
|
|
162
268
|
if (n.type === "variable") {
|
|
@@ -194,21 +300,24 @@ function pmInlineToTree(content) {
|
|
|
194
300
|
}
|
|
195
301
|
// text node — a link mark wraps the span in an InlineLink; consecutive
|
|
196
302
|
// spans of the SAME link are grouped back into one link with children.
|
|
303
|
+
// A soft break is a line break in the text, with the formats around it.
|
|
197
304
|
const linkMark = (n.marks ?? []).find((m) => m.type === "link");
|
|
198
|
-
const
|
|
305
|
+
const spans = n.type === "softBreak"
|
|
306
|
+
? [textSpanFromPm({ type: "text", text: "\n", marks: n.marks ?? [] })]
|
|
307
|
+
: codeSpans(textSpanFromPm(n));
|
|
199
308
|
if (linkMark) {
|
|
200
309
|
const href = String(linkMark.attrs?.href ?? "");
|
|
201
310
|
const title = linkMark.attrs?.title == null ? undefined : String(linkMark.attrs.title);
|
|
202
311
|
const prev = out[out.length - 1];
|
|
203
312
|
if (prev && prev.type === "link" && prev.href === href && prev.title === title) {
|
|
204
|
-
prev.children.push(
|
|
313
|
+
prev.children.push(...spans);
|
|
205
314
|
}
|
|
206
315
|
else {
|
|
207
|
-
out.push({ type: "link", href, ...(title !== undefined ? { title } : {}), children:
|
|
316
|
+
out.push({ type: "link", href, ...(title !== undefined ? { title } : {}), children: spans });
|
|
208
317
|
}
|
|
209
318
|
continue;
|
|
210
319
|
}
|
|
211
|
-
out.push(
|
|
320
|
+
out.push(...spans);
|
|
212
321
|
}
|
|
213
322
|
return out;
|
|
214
323
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Consumers bring their own view layer (@tiptap/react etc.). The dev
|
|
12
12
|
* harness in main.tsx / index.html is not part of the published surface.
|
|
13
13
|
*/
|
|
14
|
-
export { extensions, Callout, Variable, Conditional, Include, VarDef, DirectiveRaw, ConditionalSpan, DirectiveRawInline, DogsbayBlock, DogsbayInline, DogsbayProps, } from "./schema.js";
|
|
14
|
+
export { extensions, editorExtensions, type EditorExtensionOptions, SoftBreak, Callout, Variable, Conditional, Include, VarDef, DirectiveRaw, ConditionalSpan, DirectiveRawInline, DogsbayBlock, DogsbayInline, DogsbayProps, } from "./schema.js";
|
|
15
15
|
export { treeToPmDoc, pmDocToTree, treeToPmBlocks } from "./bridge.js";
|
|
16
16
|
export type { PMNode, PMMark } from "./bridge.js";
|
|
17
17
|
export { diffTreeToTree, diffTreeToPmDoc, diffExtensions, DiffMarks, DiffIns, DiffDel, type DiffMeta, type DiffKind, } from "./diff.js";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Consumers bring their own view layer (@tiptap/react etc.). The dev
|
|
12
12
|
* harness in main.tsx / index.html is not part of the published surface.
|
|
13
13
|
*/
|
|
14
|
-
export { extensions, Callout, Variable, Conditional, Include, VarDef, DirectiveRaw, ConditionalSpan, DirectiveRawInline, DogsbayBlock, DogsbayInline, DogsbayProps, } from "./schema.js";
|
|
14
|
+
export { extensions, editorExtensions, SoftBreak, Callout, Variable, Conditional, Include, VarDef, DirectiveRaw, ConditionalSpan, DirectiveRawInline, DogsbayBlock, DogsbayInline, DogsbayProps, } from "./schema.js";
|
|
15
15
|
export { treeToPmDoc, pmDocToTree, treeToPmBlocks } from "./bridge.js";
|
|
16
16
|
export { diffTreeToTree, diffTreeToPmDoc, diffExtensions, DiffMarks, DiffIns, DiffDel, } from "./diff.js";
|
|
17
17
|
export { evaluateTest, resolveExpr, formatValue, collectDocVarDefs, } from "./directive-context.js";
|
package/dist/schema.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export declare const TableCell: Node<any, any>;
|
|
|
42
42
|
/** prosemirror-tables' editing plugin + cell navigation (Tab / Shift-Tab). */
|
|
43
43
|
export declare const TableEditing: Extension<any, any>;
|
|
44
44
|
export declare const Callout: Node<any, any>;
|
|
45
|
+
/**
|
|
46
|
+
* A soft line break kept from the source (a paragraph wrapped over several lines): drawn as the
|
|
47
|
+
* space a reader sees, written back as the line break it was (`@dogsbay/editor/block`).
|
|
48
|
+
*/
|
|
49
|
+
export declare const SoftBreak: Node<any, any>;
|
|
45
50
|
export declare const Variable: Node<any, any>;
|
|
46
51
|
export declare const Conditional: Node<any, any>;
|
|
47
52
|
export declare const Include: Node<any, any>;
|
|
@@ -51,4 +56,14 @@ export declare const ConditionalSpan: Node<any, any>;
|
|
|
51
56
|
export declare const DirectiveRawInline: Node<any, any>;
|
|
52
57
|
export declare const DogsbayBlock: Node<any, any>;
|
|
53
58
|
export declare const DogsbayInline: Node<any, any>;
|
|
59
|
+
export interface EditorExtensionOptions {
|
|
60
|
+
/**
|
|
61
|
+
* TipTap's trailing empty paragraph (StarterKit's `TrailingNode`), so the caret can always get
|
|
62
|
+
* below the last block. On by default; the block editor turns it off, since a block shown on its
|
|
63
|
+
* page has no "below" and the writer saw an empty line under every block opened.
|
|
64
|
+
*/
|
|
65
|
+
trailingNode?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** The editor's extensions (`extensions` is this with the defaults). */
|
|
68
|
+
export declare function editorExtensions(options?: EditorExtensionOptions): (Node<any, any> | Extension<any, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any>)[];
|
|
54
69
|
export declare const extensions: (Node<any, any> | Extension<any, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any>)[];
|
package/dist/schema.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { Extension, Node, mergeAttributes, callOrReturn, getExtensionField } from "@tiptap/core";
|
|
22
22
|
import StarterKit from "@tiptap/starter-kit";
|
|
23
|
+
import { calloutShell } from "@dogsbay/render-html/callout-markup";
|
|
23
24
|
import { tableEditing, goToNextCell, isInTable, addRowAfter } from "@tiptap/pm/tables";
|
|
24
25
|
export const DogsbayProps = Extension.create({
|
|
25
26
|
name: "dogsbayProps",
|
|
@@ -49,6 +50,14 @@ export const DogsbayProps = Extension.create({
|
|
|
49
50
|
dogsbayProps: { default: null, rendered: false, keepOnSplit: false },
|
|
50
51
|
},
|
|
51
52
|
},
|
|
53
|
+
{
|
|
54
|
+
// `\`x\`{minja}`: the span's build-processing opt-in (format-dogsbay-md #030), carried on
|
|
55
|
+
// the code mark so an edit elsewhere in the paragraph keeps it
|
|
56
|
+
types: ["code"],
|
|
57
|
+
attributes: {
|
|
58
|
+
minja: { default: null, rendered: false },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
52
61
|
];
|
|
53
62
|
},
|
|
54
63
|
});
|
|
@@ -172,10 +181,66 @@ export const Callout = Node.create({
|
|
|
172
181
|
return { tone: { default: "note" } };
|
|
173
182
|
},
|
|
174
183
|
parseHTML() {
|
|
175
|
-
return [{ tag: "
|
|
184
|
+
return [{ tag: "[data-callout]", contentElement: (el) => el.querySelector(":scope > div:not([data-part])") ?? el }];
|
|
185
|
+
},
|
|
186
|
+
// Drawn as the site draws a callout (render-html's markup and classes, its icon and title), so a
|
|
187
|
+
// host shows it with the site's own styles. The tone stays `data-callout` for parseHTML.
|
|
188
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
189
|
+
const shell = calloutShell(String(node.attrs.tone ?? "note"));
|
|
190
|
+
return [
|
|
191
|
+
"div",
|
|
192
|
+
mergeAttributes(HTMLAttributes, { "data-callout": "", class: `${shell.className} my-4`, "data-variant": shell.variant, "data-type": shell.type, role: "note" }),
|
|
193
|
+
["div", { "data-part": "title", class: shell.titleClass, contenteditable: "false" }, shell.label],
|
|
194
|
+
["div", { class: shell.bodyClass }, 0],
|
|
195
|
+
];
|
|
196
|
+
},
|
|
197
|
+
addNodeView() {
|
|
198
|
+
return ({ node, HTMLAttributes }) => {
|
|
199
|
+
const shell = calloutShell(String(node.attrs.tone ?? "note"));
|
|
200
|
+
const dom = document.createElement("div");
|
|
201
|
+
const attrs = mergeAttributes(HTMLAttributes, { "data-callout": "", class: `${shell.className} my-4`, "data-variant": shell.variant, "data-type": shell.type, role: "note" });
|
|
202
|
+
for (const [k, v] of Object.entries(attrs))
|
|
203
|
+
if (v != null && v !== false)
|
|
204
|
+
dom.setAttribute(k, String(v));
|
|
205
|
+
// The icon as the site's first child (its classes place it: `[&>svg]:absolute`); our own
|
|
206
|
+
// constant SVG, never document text
|
|
207
|
+
if (shell.icon) {
|
|
208
|
+
const holder = document.createElement("template");
|
|
209
|
+
holder.innerHTML = shell.icon;
|
|
210
|
+
const svg = holder.content.firstElementChild;
|
|
211
|
+
if (svg)
|
|
212
|
+
dom.appendChild(svg);
|
|
213
|
+
}
|
|
214
|
+
const title = document.createElement("div");
|
|
215
|
+
title.setAttribute("data-part", "title");
|
|
216
|
+
title.className = shell.titleClass;
|
|
217
|
+
title.contentEditable = "false";
|
|
218
|
+
title.textContent = shell.label;
|
|
219
|
+
const body = document.createElement("div");
|
|
220
|
+
body.className = shell.bodyClass;
|
|
221
|
+
dom.append(title, body);
|
|
222
|
+
return { dom, contentDOM: body };
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
/**
|
|
227
|
+
* A soft line break kept from the source (a paragraph wrapped over several lines): drawn as the
|
|
228
|
+
* space a reader sees, written back as the line break it was (`@dogsbay/editor/block`).
|
|
229
|
+
*/
|
|
230
|
+
export const SoftBreak = Node.create({
|
|
231
|
+
name: "softBreak",
|
|
232
|
+
group: "inline",
|
|
233
|
+
inline: true,
|
|
234
|
+
atom: true,
|
|
235
|
+
selectable: false,
|
|
236
|
+
parseHTML() {
|
|
237
|
+
return [{ tag: "span[data-soft-break]" }];
|
|
176
238
|
},
|
|
177
239
|
renderHTML({ HTMLAttributes }) {
|
|
178
|
-
return ["
|
|
240
|
+
return ["span", mergeAttributes(HTMLAttributes, { "data-soft-break": "" }), " "];
|
|
241
|
+
},
|
|
242
|
+
renderText() {
|
|
243
|
+
return " ";
|
|
179
244
|
},
|
|
180
245
|
});
|
|
181
246
|
export const Variable = Node.create({
|
|
@@ -374,8 +439,12 @@ export const DogsbayInline = Node.create({
|
|
|
374
439
|
return ["span", mergeAttributes(HTMLAttributes, { "data-dogsbay-inline": "" }), "⟨inline⟩"];
|
|
375
440
|
},
|
|
376
441
|
});
|
|
377
|
-
|
|
378
|
-
|
|
442
|
+
/** The editor's extensions (`extensions` is this with the defaults). */
|
|
443
|
+
export function editorExtensions(options = {}) {
|
|
444
|
+
return [StarterKit.configure({ underline: false, ...(options.trailingNode === false ? { trailingNode: false } : {}) }), ...SCHEMA];
|
|
445
|
+
}
|
|
446
|
+
const SCHEMA = [
|
|
447
|
+
SoftBreak,
|
|
379
448
|
DogsbayProps,
|
|
380
449
|
Table,
|
|
381
450
|
TableRow,
|
|
@@ -393,3 +462,4 @@ export const extensions = [
|
|
|
393
462
|
DogsbayBlock,
|
|
394
463
|
DogsbayInline,
|
|
395
464
|
];
|
|
465
|
+
export const extensions = editorExtensions();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/editor",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.175",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "TipTap/ProseMirror editor for Dogsbay Markdown: schema, a lossless TreeNode ⇄ ProseMirror bridge, and the editor on one block of a page (./block)",
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@dogsbay/
|
|
29
|
-
"@dogsbay/
|
|
30
|
-
"@dogsbay/
|
|
31
|
-
"@dogsbay/
|
|
28
|
+
"@dogsbay/render-html": "0.2.0-beta.175",
|
|
29
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.175",
|
|
30
|
+
"@dogsbay/minja": "0.2.0-beta.175",
|
|
31
|
+
"@dogsbay/tree-diff": "0.2.0-beta.175",
|
|
32
|
+
"@dogsbay/types": "0.2.0-beta.175"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"@tiptap/core": "^3.27.3",
|