@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/dist/schema.js ADDED
@@ -0,0 +1,395 @@
1
+ /**
2
+ * TipTap schema for the TreeNode vocabulary: StarterKit (paragraph,
3
+ * heading, lists, blockquote, codeBlock, horizontalRule, hardBreak,
4
+ * bold/italic/strike/code/link marks) plus the custom pieces:
5
+ * - callout — a block container
6
+ * - variable — an inline atom (the directive-chip mechanism)
7
+ * - dogsbayBlock — a block atom carrying verbatim TreeNode JSON (lossless
8
+ * passthrough for tables and anything unmodeled)
9
+ * - dogsbayInline — the inline equivalent
10
+ * - DogsbayProps — a hidden global attribute on every modeled block that
11
+ * carries the original TreeNode props/meta verbatim, so
12
+ * fields the editor doesn't model (code title/highlights,
13
+ * heading {#id .class}, ordered-list start, …) survive
14
+ * the round trip. ProseMirror drops undeclared attrs, so
15
+ * this MUST be a declared attribute.
16
+ *
17
+ * Underline is disabled: InlineText has no underline flag (markdown cannot
18
+ * express it), so anything underlined in the editor would be silently
19
+ * stripped on serialize — better not to offer it.
20
+ */
21
+ import { Extension, Node, mergeAttributes, callOrReturn, getExtensionField } from "@tiptap/core";
22
+ import StarterKit from "@tiptap/starter-kit";
23
+ import { tableEditing, goToNextCell, isInTable, addRowAfter } from "@tiptap/pm/tables";
24
+ export const DogsbayProps = Extension.create({
25
+ name: "dogsbayProps",
26
+ addGlobalAttributes() {
27
+ return [
28
+ {
29
+ types: [
30
+ "paragraph",
31
+ "heading",
32
+ "blockquote",
33
+ "bulletList",
34
+ "orderedList",
35
+ "listItem",
36
+ "codeBlock",
37
+ "horizontalRule",
38
+ "callout",
39
+ "conditional",
40
+ "include",
41
+ "varDef",
42
+ "directiveRaw",
43
+ "table",
44
+ "tableRow",
45
+ "tableHeader",
46
+ "tableCell",
47
+ ],
48
+ attributes: {
49
+ dogsbayProps: { default: null, rendered: false, keepOnSplit: false },
50
+ },
51
+ },
52
+ ];
53
+ },
54
+ });
55
+ /**
56
+ * Tables are NATIVE nodes on prosemirror-tables (via `@tiptap/pm/tables` —
57
+ * no extra dependency), not the `dogsbayBlock` passthrough: a writer edits
58
+ * cells in place, and row/column commands come from prosemirror-tables.
59
+ *
60
+ * Cells hold `block+` because a Dogsbay table cell may hold blocks (the
61
+ * serializer emits an HTML table for those); the common case is one
62
+ * paragraph, which the bridge maps back to the cell's `inline`.
63
+ *
64
+ * `align` is a first-class cell attribute — it is GFM column alignment
65
+ * (`:--`), which the toolbar edits and the serializer re-emits. Everything
66
+ * else the table carries (`caption`, `#id`, `.class`) rides `dogsbayProps`.
67
+ *
68
+ * The nodes deliberately do NOT render `colwidth`/resize handles: Dogsbay
69
+ * MD has no column-width concept, so a width set here could not survive a
70
+ * save.
71
+ */
72
+ const cellAttributes = () => ({
73
+ colspan: { default: 1 },
74
+ rowspan: { default: 1 },
75
+ align: { default: null },
76
+ });
77
+ export const Table = Node.create({
78
+ name: "table",
79
+ group: "block",
80
+ content: "tableRow+",
81
+ tableRole: "table",
82
+ isolating: true,
83
+ parseHTML() {
84
+ return [{ tag: "table" }];
85
+ },
86
+ renderHTML({ HTMLAttributes }) {
87
+ return ["table", mergeAttributes(HTMLAttributes), ["tbody", 0]];
88
+ },
89
+ });
90
+ export const TableRow = Node.create({
91
+ name: "tableRow",
92
+ content: "(tableCell | tableHeader)*",
93
+ tableRole: "row",
94
+ parseHTML() {
95
+ return [{ tag: "tr" }];
96
+ },
97
+ renderHTML({ HTMLAttributes }) {
98
+ return ["tr", mergeAttributes(HTMLAttributes), 0];
99
+ },
100
+ });
101
+ export const TableHeader = Node.create({
102
+ name: "tableHeader",
103
+ content: "block+",
104
+ tableRole: "header_cell",
105
+ isolating: true,
106
+ addAttributes: cellAttributes,
107
+ parseHTML() {
108
+ return [{ tag: "th" }];
109
+ },
110
+ renderHTML({ node, HTMLAttributes }) {
111
+ return ["th", mergeAttributes(HTMLAttributes, alignAttrs(node.attrs.align)), 0];
112
+ },
113
+ });
114
+ export const TableCell = Node.create({
115
+ name: "tableCell",
116
+ content: "block+",
117
+ tableRole: "cell",
118
+ isolating: true,
119
+ addAttributes: cellAttributes,
120
+ parseHTML() {
121
+ return [{ tag: "td" }];
122
+ },
123
+ renderHTML({ node, HTMLAttributes }) {
124
+ return ["td", mergeAttributes(HTMLAttributes, alignAttrs(node.attrs.align)), 0];
125
+ },
126
+ });
127
+ const alignAttrs = (align) => (align ? { style: `text-align: ${align}` } : {});
128
+ /** prosemirror-tables' editing plugin + cell navigation (Tab / Shift-Tab). */
129
+ export const TableEditing = Extension.create({
130
+ name: "tableEditing",
131
+ extendNodeSchema(extension) {
132
+ const context = { name: extension.name, options: extension.options, storage: extension.storage };
133
+ return { tableRole: callOrReturn(getExtensionField(extension, "tableRole", context)) };
134
+ },
135
+ addProseMirrorPlugins() {
136
+ return [tableEditing({ allowTableNodeSelection: false })];
137
+ },
138
+ addKeyboardShortcuts() {
139
+ return {
140
+ // a list inside a cell is the INNER context: Tab indents it, as it does anywhere else
141
+ Tab: () => (sinkList(this.editor) ? true : goToCell(this.editor, 1)),
142
+ "Shift-Tab": () => (liftList(this.editor) ? true : goToCell(this.editor, -1)),
143
+ };
144
+ },
145
+ });
146
+ /**
147
+ * Tab moves to the next cell, and only inside a table (outside one it falls
148
+ * through, so Tab still means what it means everywhere else). Tab in the
149
+ * last cell appends a row — the standard way to grow a table by typing.
150
+ */
151
+ function goToCell(editor, dir) {
152
+ const { state, view } = editor;
153
+ if (!isInTable(state))
154
+ return false;
155
+ if (goToNextCell(dir)(state, view.dispatch))
156
+ return true;
157
+ if (dir === -1)
158
+ return false;
159
+ if (!addRowAfter(state, view.dispatch))
160
+ return false;
161
+ // the row was added in a new state: re-read it before moving
162
+ return goToNextCell(1)(view.state, view.dispatch);
163
+ }
164
+ const sinkList = (editor) => editor.can().sinkListItem("listItem") && editor.commands.sinkListItem("listItem");
165
+ const liftList = (editor) => editor.can().liftListItem("listItem") && editor.commands.liftListItem("listItem");
166
+ export const Callout = Node.create({
167
+ name: "callout",
168
+ group: "block",
169
+ content: "block+",
170
+ defining: true,
171
+ addAttributes() {
172
+ return { tone: { default: "note" } };
173
+ },
174
+ parseHTML() {
175
+ return [{ tag: "aside[data-callout]" }];
176
+ },
177
+ renderHTML({ HTMLAttributes }) {
178
+ return ["aside", mergeAttributes(HTMLAttributes, { "data-callout": "" }), 0];
179
+ },
180
+ });
181
+ export const Variable = Node.create({
182
+ name: "variable",
183
+ group: "inline",
184
+ inline: true,
185
+ atom: true,
186
+ selectable: true,
187
+ // Atoms default to NO marks; directive chips inherit their containing
188
+ // span's formatting (InlineVariable.bold/italic/strikethrough), which
189
+ // the bridge maps to marks — so marks must be allowed here.
190
+ marks: "bold italic strike",
191
+ addAttributes() {
192
+ return { expr: { default: "" } };
193
+ },
194
+ parseHTML() {
195
+ return [{ tag: "span[data-variable]" }];
196
+ },
197
+ renderHTML({ node, HTMLAttributes }) {
198
+ return [
199
+ "span",
200
+ mergeAttributes(HTMLAttributes, { "data-variable": "", class: "variable" }),
201
+ `{{ ${node.attrs.expr} }}`,
202
+ ];
203
+ },
204
+ });
205
+ /* ── Directive nodes (directive-model Phase 5a) ─────────────────────────
206
+ * First-class editor nodes for the preserve-mode TreeNode vocabulary:
207
+ * conditional / include / var-def / directive-raw (block) and
208
+ * conditional-span / directive-raw-inline (inline). Chip UX (node views,
209
+ * attribute editing, minja preview) is Phase 5b; renderHTML below is the
210
+ * minimal semantic rendering the WYSIWYG shows meanwhile.
211
+ */
212
+ export const Conditional = Node.create({
213
+ name: "conditional",
214
+ group: "block",
215
+ // block* not block+: an empty `{% if %}{% endif %}` region is valid source
216
+ content: "block*",
217
+ defining: true,
218
+ addAttributes() {
219
+ return { test: { default: "" } };
220
+ },
221
+ parseHTML() {
222
+ return [{ tag: "section[data-conditional]" }];
223
+ },
224
+ renderHTML({ node, HTMLAttributes }) {
225
+ return [
226
+ "section",
227
+ mergeAttributes(HTMLAttributes, {
228
+ "data-conditional": "",
229
+ "data-test": String(node.attrs.test ?? ""),
230
+ class: "directive-conditional",
231
+ }),
232
+ 0,
233
+ ];
234
+ },
235
+ });
236
+ export const Include = Node.create({
237
+ name: "include",
238
+ group: "block",
239
+ atom: true,
240
+ addAttributes() {
241
+ return { src: { default: "" } };
242
+ },
243
+ parseHTML() {
244
+ return [{ tag: "div[data-include]" }];
245
+ },
246
+ renderHTML({ node, HTMLAttributes }) {
247
+ return [
248
+ "div",
249
+ mergeAttributes(HTMLAttributes, { "data-include": "", class: "directive-include" }),
250
+ `include: ${node.attrs.src}`,
251
+ ];
252
+ },
253
+ });
254
+ export const VarDef = Node.create({
255
+ name: "varDef",
256
+ group: "block",
257
+ atom: true,
258
+ addAttributes() {
259
+ return { name: { default: "" }, value: { default: "" } };
260
+ },
261
+ parseHTML() {
262
+ return [{ tag: "div[data-var-def]" }];
263
+ },
264
+ renderHTML({ node, HTMLAttributes }) {
265
+ return [
266
+ "div",
267
+ mergeAttributes(HTMLAttributes, { "data-var-def": "", class: "directive-var-def" }),
268
+ `set ${node.attrs.name} = ${node.attrs.value}`,
269
+ ];
270
+ },
271
+ });
272
+ export const DirectiveRaw = Node.create({
273
+ name: "directiveRaw",
274
+ group: "block",
275
+ atom: true,
276
+ addAttributes() {
277
+ return { source: { default: "" }, kind: { default: null } };
278
+ },
279
+ parseHTML() {
280
+ return [{ tag: "div[data-directive-raw]" }];
281
+ },
282
+ renderHTML({ node, HTMLAttributes }) {
283
+ return [
284
+ "div",
285
+ mergeAttributes(HTMLAttributes, { "data-directive-raw": "", class: "directive-raw" }),
286
+ String(node.attrs.source ?? ""),
287
+ ];
288
+ },
289
+ });
290
+ export const ConditionalSpan = Node.create({
291
+ name: "conditionalSpan",
292
+ group: "inline",
293
+ inline: true,
294
+ content: "inline*",
295
+ marks: "bold italic strike",
296
+ addAttributes() {
297
+ return { test: { default: "" } };
298
+ },
299
+ parseHTML() {
300
+ return [{ tag: "span[data-conditional-span]" }];
301
+ },
302
+ renderHTML({ node, HTMLAttributes }) {
303
+ return [
304
+ "span",
305
+ mergeAttributes(HTMLAttributes, {
306
+ "data-conditional-span": "",
307
+ "data-test": String(node.attrs.test ?? ""),
308
+ class: "directive-conditional-span",
309
+ }),
310
+ 0,
311
+ ];
312
+ },
313
+ });
314
+ export const DirectiveRawInline = Node.create({
315
+ name: "directiveRawInline",
316
+ group: "inline",
317
+ inline: true,
318
+ atom: true,
319
+ marks: "bold italic strike",
320
+ addAttributes() {
321
+ return { source: { default: "" } };
322
+ },
323
+ parseHTML() {
324
+ return [{ tag: "span[data-directive-raw-inline]" }];
325
+ },
326
+ renderHTML({ node, HTMLAttributes }) {
327
+ return [
328
+ "span",
329
+ mergeAttributes(HTMLAttributes, {
330
+ "data-directive-raw-inline": "",
331
+ class: "directive-raw-inline",
332
+ }),
333
+ String(node.attrs.source ?? ""),
334
+ ];
335
+ },
336
+ });
337
+ export const DogsbayBlock = Node.create({
338
+ name: "dogsbayBlock",
339
+ group: "block",
340
+ atom: true,
341
+ addAttributes() {
342
+ return { json: { default: "" } };
343
+ },
344
+ parseHTML() {
345
+ return [{ tag: "div[data-dogsbay-block]" }];
346
+ },
347
+ renderHTML({ node, HTMLAttributes }) {
348
+ let label = "block";
349
+ try {
350
+ label = JSON.parse(String(node.attrs.json ?? "{}")).type ?? "block";
351
+ }
352
+ catch {
353
+ /* keep default */
354
+ }
355
+ return [
356
+ "div",
357
+ mergeAttributes(HTMLAttributes, { "data-dogsbay-block": "", class: "dogsbay-block" }),
358
+ `⟨${label}⟩`,
359
+ ];
360
+ },
361
+ });
362
+ export const DogsbayInline = Node.create({
363
+ name: "dogsbayInline",
364
+ group: "inline",
365
+ inline: true,
366
+ atom: true,
367
+ addAttributes() {
368
+ return { json: { default: "" } };
369
+ },
370
+ parseHTML() {
371
+ return [{ tag: "span[data-dogsbay-inline]" }];
372
+ },
373
+ renderHTML({ HTMLAttributes }) {
374
+ return ["span", mergeAttributes(HTMLAttributes, { "data-dogsbay-inline": "" }), "⟨inline⟩"];
375
+ },
376
+ });
377
+ export const extensions = [
378
+ StarterKit.configure({ underline: false }),
379
+ DogsbayProps,
380
+ Table,
381
+ TableRow,
382
+ TableHeader,
383
+ TableCell,
384
+ TableEditing,
385
+ Callout,
386
+ Variable,
387
+ Conditional,
388
+ Include,
389
+ VarDef,
390
+ DirectiveRaw,
391
+ ConditionalSpan,
392
+ DirectiveRawInline,
393
+ DogsbayBlock,
394
+ DogsbayInline,
395
+ ];
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@dogsbay/editor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-beta.174",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "TipTap/ProseMirror editor for Dogsbay Markdown: schema, a lossless TreeNode ⇄ ProseMirror bridge, and the editor on one block of a page (./block)",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./block": {
15
+ "types": "./dist/block.d.ts",
16
+ "import": "./dist/block.js"
17
+ },
18
+ "./block-view": {
19
+ "types": "./dist/block-view.d.ts",
20
+ "import": "./dist/block-view.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.174",
29
+ "@dogsbay/minja": "0.2.0-beta.174",
30
+ "@dogsbay/tree-diff": "0.2.0-beta.174",
31
+ "@dogsbay/types": "0.2.0-beta.174"
32
+ },
33
+ "peerDependencies": {
34
+ "@tiptap/core": "^3.27.3",
35
+ "@tiptap/pm": "^3.27.3",
36
+ "@tiptap/starter-kit": "^3.27.3"
37
+ },
38
+ "devDependencies": {
39
+ "@playwright/test": "^1.61.1",
40
+ "@tiptap/core": "^3.27.3",
41
+ "@tiptap/pm": "^3.27.3",
42
+ "@tiptap/react": "^3.27.3",
43
+ "@tiptap/starter-kit": "^3.27.3",
44
+ "@types/node": "^22.20.1",
45
+ "@types/react": "^19.2.17",
46
+ "@types/react-dom": "^19.2.3",
47
+ "@vitejs/plugin-react": "^6.0.3",
48
+ "esbuild": "0.28.1",
49
+ "jsdom": "^26.1.0",
50
+ "react": "^19.2.7",
51
+ "react-dom": "^19.2.7",
52
+ "typescript": "^5.9.3",
53
+ "vite": "^8.1.4",
54
+ "vitest": "^4.1.10"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/dogsbay/dogsbay.git",
59
+ "directory": "packages/editor"
60
+ },
61
+ "homepage": "https://github.com/dogsbay/dogsbay/tree/main/packages/editor",
62
+ "bugs": {
63
+ "url": "https://github.com/dogsbay/dogsbay/issues"
64
+ },
65
+ "scripts": {
66
+ "dev": "vite",
67
+ "build": "tsc -p tsconfig.build.json",
68
+ "test": "vitest run",
69
+ "test:e2e": "playwright test"
70
+ }
6
71
  }