@dogsbay/editor 0.2.0-beta.173 → 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 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. 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.
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 136 KB (TipTap and ProseMirror) and `./block` about 163 KB, nearly all of it the
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: bold on inline code, a link's `target`
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.
@@ -5,7 +5,7 @@
5
5
  * here and back as JSON.
6
6
  */
7
7
  import { Editor } from "@tiptap/core";
8
- import { extensions } from "./schema.js";
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
- const editor = new Editor({ element, extensions, content: doc });
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). 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.
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). 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.
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
- out.push(marks.length ? { type: "text", text: n.text, marks } : { type: "text", text: n.text });
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.push({ type: "text", text: n.text, marks: [{ type: "code" }] });
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
- // Vocabulary limit: InlineCode has no formatting flags (parser parity).
148
- return { type: "code", text: n.text ?? "" };
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 span = textSpanFromPm(n);
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(span);
313
+ prev.children.push(...spans);
205
314
  }
206
315
  else {
207
- out.push({ type: "link", href, ...(title !== undefined ? { title } : {}), children: [span] });
316
+ out.push({ type: "link", href, ...(title !== undefined ? { title } : {}), children: spans });
208
317
  }
209
318
  continue;
210
319
  }
211
- out.push(span);
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: "aside[data-callout]" }];
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 ["aside", mergeAttributes(HTMLAttributes, { "data-callout": "" }), 0];
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
- export const extensions = [
378
- StarterKit.configure({ underline: false }),
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.173",
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/minja": "0.2.0-beta.173",
29
- "@dogsbay/tree-diff": "0.2.0-beta.173",
30
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.173",
31
- "@dogsbay/types": "0.2.0-beta.173"
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",