@tessera-editor/core 0.1.0

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/index.js ADDED
@@ -0,0 +1,2538 @@
1
+ // src/nodes/hint.ts
2
+ import { Node, mergeAttributes, wrappingInputRule } from "@tiptap/core";
3
+ var Hint = Node.create({
4
+ name: "hint",
5
+ group: "block",
6
+ content: "block+",
7
+ defining: true,
8
+ addOptions() {
9
+ return {
10
+ HTMLAttributes: {}
11
+ };
12
+ },
13
+ addAttributes() {
14
+ return {
15
+ variant: {
16
+ default: "info",
17
+ parseHTML: (element) => element.getAttribute("data-variant") || "info",
18
+ renderHTML: (attributes) => ({ "data-variant": String(attributes.variant) })
19
+ }
20
+ };
21
+ },
22
+ parseHTML() {
23
+ return [{ tag: 'div[data-type="hint"]' }];
24
+ },
25
+ renderHTML({ HTMLAttributes }) {
26
+ return ["div", mergeAttributes({ "data-type": "hint" }, this.options.HTMLAttributes, HTMLAttributes), 0];
27
+ },
28
+ addCommands() {
29
+ return {
30
+ setHint: (variant = "info") => ({ commands }) => commands.wrapIn(this.name, { variant }),
31
+ toggleHint: () => ({ commands }) => commands.toggleWrap(this.name)
32
+ };
33
+ },
34
+ addInputRules() {
35
+ return [
36
+ wrappingInputRule({
37
+ find: /^(!{2}) $/,
38
+ type: this.type,
39
+ keepMarks: true,
40
+ keepAttributes: true
41
+ })
42
+ ];
43
+ }
44
+ });
45
+
46
+ // src/nodes/collapsible.ts
47
+ import { Node as Node2, mergeAttributes as mergeAttributes2, InputRule } from "@tiptap/core";
48
+ import { TextSelection } from "@tiptap/pm/state";
49
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
50
+ function collapsibleJSON() {
51
+ return {
52
+ type: "collapsible",
53
+ attrs: { open: true },
54
+ content: [
55
+ { type: "collapsibleSummary" },
56
+ {
57
+ type: "collapsibleContent",
58
+ content: [{ type: "paragraph" }]
59
+ }
60
+ ]
61
+ };
62
+ }
63
+ var Collapsible = Node2.create({
64
+ name: "collapsible",
65
+ group: "block",
66
+ content: "collapsibleSummary collapsibleContent",
67
+ defining: true,
68
+ isolating: true,
69
+ addOptions() {
70
+ return {
71
+ HTMLAttributes: {}
72
+ };
73
+ },
74
+ addAttributes() {
75
+ return {
76
+ open: {
77
+ default: true,
78
+ parseHTML: (element) => element.getAttribute("data-open") !== "false",
79
+ renderHTML: (attributes) => ({ "data-open": String(attributes.open) })
80
+ }
81
+ };
82
+ },
83
+ parseHTML() {
84
+ return [{ tag: 'div[data-type="collapsible"]' }];
85
+ },
86
+ renderHTML({ HTMLAttributes }) {
87
+ return ["div", mergeAttributes2({ "data-type": "collapsible" }, this.options.HTMLAttributes, HTMLAttributes), 0];
88
+ },
89
+ addCommands() {
90
+ return {
91
+ insertCollapsible: () => ({ state, chain }) => {
92
+ const { $from } = state.selection;
93
+ if ($from.parent.type.name !== "paragraph") {
94
+ return false;
95
+ }
96
+ const from = $from.before();
97
+ const to = $from.after();
98
+ return chain().insertContentAt({ from, to }, collapsibleJSON()).command(({ tr, dispatch }) => {
99
+ if (dispatch) {
100
+ tr.setSelection(TextSelection.near(tr.doc.resolve(from + 2)));
101
+ }
102
+ return true;
103
+ }).run();
104
+ }
105
+ };
106
+ },
107
+ addInputRules() {
108
+ return [
109
+ new InputRule({
110
+ find: /^(>{2}) $/,
111
+ handler: ({ state, range, chain }) => {
112
+ const $from = state.doc.resolve(range.from);
113
+ if ($from.parent.type.name !== "paragraph") {
114
+ return;
115
+ }
116
+ const from = $from.before();
117
+ const to = $from.after();
118
+ chain().insertContentAt({ from, to }, collapsibleJSON()).command(({ tr, dispatch }) => {
119
+ if (dispatch) {
120
+ tr.setSelection(TextSelection.near(tr.doc.resolve(from + 2)));
121
+ }
122
+ return true;
123
+ }).run();
124
+ }
125
+ })
126
+ ];
127
+ },
128
+ addProseMirrorPlugins() {
129
+ const nodeName = this.name;
130
+ return [
131
+ new Plugin({
132
+ key: new PluginKey("tesseraCollapsibleToggle"),
133
+ props: {
134
+ // Click on the summary toggles open/closed; collapsed state survives reload
135
+ // because it is a node attribute, not CSS state.
136
+ handleClickOn: (view, _pos, node, nodePos, event) => {
137
+ if (node.type.name !== nodeName) {
138
+ return false;
139
+ }
140
+ const target = event.target;
141
+ if (!target?.closest('[data-type="collapsible-summary"]')) {
142
+ return false;
143
+ }
144
+ view.dispatch(
145
+ view.state.tr.setNodeMarkup(nodePos, void 0, {
146
+ ...node.attrs,
147
+ open: !node.attrs.open
148
+ })
149
+ );
150
+ return true;
151
+ }
152
+ }
153
+ })
154
+ ];
155
+ }
156
+ });
157
+ var CollapsibleSummary = Node2.create({
158
+ name: "collapsibleSummary",
159
+ group: "",
160
+ content: "inline*",
161
+ defining: true,
162
+ parseHTML() {
163
+ return [{ tag: 'div[data-type="collapsible-summary"]' }];
164
+ },
165
+ renderHTML({ HTMLAttributes }) {
166
+ return ["div", mergeAttributes2({ "data-type": "collapsible-summary" }, HTMLAttributes), 0];
167
+ }
168
+ });
169
+ var CollapsibleContent = Node2.create({
170
+ name: "collapsibleContent",
171
+ group: "",
172
+ content: "block+",
173
+ parseHTML() {
174
+ return [{ tag: 'div[data-type="collapsible-content"]' }];
175
+ },
176
+ renderHTML({ HTMLAttributes }) {
177
+ return ["div", mergeAttributes2({ "data-type": "collapsible-content" }, HTMLAttributes), 0];
178
+ }
179
+ });
180
+
181
+ // src/nodes/image.ts
182
+ import { Node as Node3, mergeAttributes as mergeAttributes3 } from "@tiptap/core";
183
+ var ImageBlock = Node3.create({
184
+ name: "imageBlock",
185
+ inline: false,
186
+ group: "block",
187
+ draggable: true,
188
+ addOptions() {
189
+ return {
190
+ HTMLAttributes: {}
191
+ };
192
+ },
193
+ addAttributes() {
194
+ return {
195
+ src: { default: null },
196
+ alt: { default: null },
197
+ title: { default: null },
198
+ width: { default: null },
199
+ align: { default: "center" }
200
+ };
201
+ },
202
+ parseHTML() {
203
+ return [
204
+ { tag: 'img[data-type="tessera-image"]' },
205
+ // plain <img> pasted from outside becomes a block image
206
+ { tag: "img[src]:not([data-type])" }
207
+ ];
208
+ },
209
+ renderHTML({ node, HTMLAttributes }) {
210
+ const style = node.attrs.width ? `width: ${Number(node.attrs.width)}px` : void 0;
211
+ return [
212
+ "img",
213
+ mergeAttributes3(this.options.HTMLAttributes, HTMLAttributes, {
214
+ "data-type": "tessera-image",
215
+ "data-align": String(node.attrs.align),
216
+ style
217
+ })
218
+ ];
219
+ },
220
+ addCommands() {
221
+ return {
222
+ setImage: (options) => ({ commands }) => commands.insertContent({ type: this.name, attrs: options })
223
+ };
224
+ }
225
+ });
226
+
227
+ // src/nodes/table.ts
228
+ import { Table, TableRow, TableCell, TableHeader } from "@tiptap/extension-table";
229
+ var TABLE_COLUMN_KINDS = [
230
+ "text",
231
+ "checkbox",
232
+ "select",
233
+ "multiSelect",
234
+ "number",
235
+ "date",
236
+ "link"
237
+ ];
238
+ function tableToCsvAt(editor) {
239
+ const table = locateTable(editor.state);
240
+ return table ? tableNodeToCsv(table.node) : null;
241
+ }
242
+ function locateTable(state) {
243
+ const { $from } = state.selection;
244
+ for (let depth = $from.depth; depth > 0; depth--) {
245
+ const node = $from.node(depth);
246
+ if (node.type.name === "table") {
247
+ return { pos: $from.before(depth), node };
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+ function columnCount(table) {
253
+ const firstRow = table.content.firstChild;
254
+ return firstRow ? firstRow.childCount : 0;
255
+ }
256
+ function normalizeTypes(types, cols) {
257
+ const list = Array.isArray(types) ? [...types] : [];
258
+ while (list.length < cols) {
259
+ list.push("text");
260
+ }
261
+ return list.slice(0, cols);
262
+ }
263
+ function cellSortValue(row, index, kind) {
264
+ const cell = row.maybeChild(index);
265
+ if (!cell) {
266
+ return null;
267
+ }
268
+ const value = cell.attrs.value;
269
+ switch (kind) {
270
+ case "checkbox":
271
+ return value === true || value === "true";
272
+ case "number": {
273
+ if (typeof value === "number") {
274
+ return value;
275
+ }
276
+ const parsed = Number(String(value ?? cell.textContent).trim());
277
+ return Number.isFinite(parsed) ? parsed : null;
278
+ }
279
+ case "date":
280
+ return typeof value === "string" && value ? value : null;
281
+ case "select":
282
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
283
+ case "multiSelect":
284
+ return Array.isArray(value) ? [...value].sort().join(" \u2027 ") : null;
285
+ default:
286
+ return cell.textContent.trim() || null;
287
+ }
288
+ }
289
+ function csvEscape(text) {
290
+ if (/[",\n]/.test(text)) {
291
+ return `"${text.replace(/"/g, '""')}"`;
292
+ }
293
+ return text;
294
+ }
295
+ function tableNodeToCsv(table) {
296
+ const types = normalizeTypes(table.attrs.types, columnCount(table));
297
+ const lines = [];
298
+ table.forEach((row) => {
299
+ const isHeaderRow = row.content.firstChild?.type.name === "tableHeader";
300
+ const cells = [];
301
+ row.forEach((cell, _offset, index) => {
302
+ const kind = isHeaderRow ? "text" : types[index] ?? "text";
303
+ const value = cell.attrs.value;
304
+ let text;
305
+ switch (kind) {
306
+ case "checkbox":
307
+ text = value === true || value === "true" ? "\u2714" : "";
308
+ break;
309
+ case "select":
310
+ case "multiSelect":
311
+ text = Array.isArray(value) ? value.join(" / ") : String(value ?? "");
312
+ break;
313
+ case "date":
314
+ text = String(value ?? "");
315
+ break;
316
+ case "number":
317
+ case "link":
318
+ text = value === null || value === void 0 ? cell.textContent : String(value);
319
+ break;
320
+ default:
321
+ text = cell.textContent;
322
+ }
323
+ cells.push(csvEscape(text.trim()));
324
+ });
325
+ lines.push(cells.join(","));
326
+ });
327
+ return lines.join("\n");
328
+ }
329
+ var AiTable = Table.extend({
330
+ name: "table",
331
+ addAttributes() {
332
+ return {
333
+ ...this.parent?.(),
334
+ types: {
335
+ default: [],
336
+ parseHTML: (element) => {
337
+ try {
338
+ const raw = element.getAttribute("data-types");
339
+ return raw ? JSON.parse(raw) : [];
340
+ } catch {
341
+ return [];
342
+ }
343
+ },
344
+ renderHTML: (attributes) => ({
345
+ "data-types": JSON.stringify(attributes.types ?? [])
346
+ })
347
+ },
348
+ freezeFirstCol: {
349
+ default: true,
350
+ parseHTML: (element) => element.getAttribute("data-freeze-first") !== "false",
351
+ renderHTML: (attributes) => ({ "data-freeze-first": String(attributes.freezeFirstCol) })
352
+ }
353
+ };
354
+ },
355
+ addCommands() {
356
+ const parent = this.parent?.() ?? {};
357
+ return {
358
+ ...parent,
359
+ insertTableTyped: (options = {}) => ({ commands }) => commands.insertTable({
360
+ rows: options.rows ?? 3,
361
+ cols: options.cols ?? 3,
362
+ withHeaderRow: options.withHeaderRow ?? true
363
+ }),
364
+ setColumnType: (index, kind) => ({ state, dispatch, tr }) => {
365
+ const table = locateTable(state);
366
+ if (!table || index < 0 || index >= columnCount(table.node)) {
367
+ return false;
368
+ }
369
+ const types = normalizeTypes(table.node.attrs.types, columnCount(table.node));
370
+ types[index] = kind;
371
+ if (dispatch) {
372
+ tr.setNodeMarkup(table.pos, void 0, { ...table.node.attrs, types });
373
+ dispatch(tr);
374
+ }
375
+ return true;
376
+ },
377
+ sortTableByColumn: (index, direction) => ({ state, dispatch, tr }) => {
378
+ const table = locateTable(state);
379
+ if (!table) {
380
+ return false;
381
+ }
382
+ const rows = [];
383
+ table.node.forEach((row) => rows.push(row));
384
+ const hasHeader = rows.length > 0 && rows[0].content.firstChild?.type.name === "tableHeader";
385
+ const header = hasHeader ? rows[0] : null;
386
+ const body = hasHeader ? rows.slice(1) : rows.slice();
387
+ if (body.length === 0 || index >= columnCount(table.node)) {
388
+ return false;
389
+ }
390
+ const types = normalizeTypes(table.node.attrs.types, columnCount(table.node));
391
+ const kind = types[index] ?? "text";
392
+ const dir = direction === "asc" ? 1 : -1;
393
+ const sorted = [...body].sort((a, b) => {
394
+ const va = cellSortValue(a, index, kind);
395
+ const vb = cellSortValue(b, index, kind);
396
+ if (va === vb) {
397
+ return 0;
398
+ }
399
+ if (va === null) {
400
+ return 1;
401
+ }
402
+ if (vb === null) {
403
+ return -1;
404
+ }
405
+ return va < vb ? -dir : dir;
406
+ });
407
+ const start = table.pos + 1 + (header ? header.nodeSize : 0);
408
+ const end = table.pos + table.node.nodeSize - 1;
409
+ if (dispatch) {
410
+ tr.replaceWith(start, end, sorted);
411
+ dispatch(tr);
412
+ }
413
+ return true;
414
+ }
415
+ };
416
+ }
417
+ });
418
+ function valueAttr() {
419
+ return {
420
+ default: null,
421
+ parseHTML: (element) => {
422
+ const raw = element.getAttribute("data-value");
423
+ if (raw === null || raw === "") {
424
+ return null;
425
+ }
426
+ try {
427
+ return JSON.parse(raw);
428
+ } catch {
429
+ return raw;
430
+ }
431
+ },
432
+ renderHTML: (attributes) => ({
433
+ "data-value": attributes.value === null || attributes.value === void 0 ? "" : JSON.stringify(attributes.value)
434
+ })
435
+ };
436
+ }
437
+ var AiTableCell = TableCell.extend({
438
+ addAttributes() {
439
+ return {
440
+ ...this.parent?.(),
441
+ value: valueAttr()
442
+ };
443
+ }
444
+ });
445
+ var AiTableHeader = TableHeader.extend({
446
+ addAttributes() {
447
+ return {
448
+ ...this.parent?.(),
449
+ value: valueAttr()
450
+ };
451
+ }
452
+ });
453
+
454
+ // src/nodes/embed.ts
455
+ import { Node as Node4, mergeAttributes as mergeAttributes4 } from "@tiptap/core";
456
+ var EmbedBlock = Node4.create({
457
+ name: "embedBlock",
458
+ group: "block",
459
+ atom: true,
460
+ draggable: true,
461
+ addAttributes() {
462
+ return {
463
+ src: { default: null },
464
+ title: { default: null },
465
+ height: { default: 360 },
466
+ allowScripts: { default: false }
467
+ };
468
+ },
469
+ parseHTML() {
470
+ return [{ tag: 'div[data-type="tessera-embed"]' }];
471
+ },
472
+ renderHTML({ node, HTMLAttributes }) {
473
+ return [
474
+ "div",
475
+ mergeAttributes4(HTMLAttributes, {
476
+ "data-type": "tessera-embed",
477
+ "data-src": String(node.attrs.src ?? ""),
478
+ "data-height": String(node.attrs.height),
479
+ "data-allow-scripts": String(node.attrs.allowScripts)
480
+ })
481
+ ];
482
+ },
483
+ addCommands() {
484
+ return {
485
+ insertEmbed: (attrs) => ({ commands }) => commands.insertContent({ type: this.name, attrs })
486
+ };
487
+ }
488
+ });
489
+
490
+ // src/nodes/toc.ts
491
+ import { Node as Node5, mergeAttributes as mergeAttributes5 } from "@tiptap/core";
492
+ var TocBlock = Node5.create({
493
+ name: "tocBlock",
494
+ group: "block",
495
+ atom: true,
496
+ draggable: true,
497
+ parseHTML() {
498
+ return [{ tag: 'div[data-type="tessera-toc"]' }];
499
+ },
500
+ renderHTML({ HTMLAttributes }) {
501
+ return ["div", mergeAttributes5(HTMLAttributes, { "data-type": "tessera-toc" })];
502
+ },
503
+ addCommands() {
504
+ return {
505
+ insertToc: () => ({ commands }) => commands.insertContent({ type: this.name })
506
+ };
507
+ }
508
+ });
509
+
510
+ // src/marks/ai.ts
511
+ import { Mark, mergeAttributes as mergeAttributes6 } from "@tiptap/core";
512
+ var AiAttribution = Mark.create({
513
+ name: "aiAttribution",
514
+ inclusive: false,
515
+ addOptions() {
516
+ return {
517
+ HTMLAttributes: {}
518
+ };
519
+ },
520
+ addAttributes() {
521
+ return {
522
+ pending: {
523
+ default: true,
524
+ parseHTML: (element) => element.getAttribute("data-pending") !== "false",
525
+ renderHTML: (attributes) => ({ "data-pending": String(attributes.pending) })
526
+ },
527
+ action: {
528
+ default: null,
529
+ parseHTML: (element) => element.getAttribute("data-action"),
530
+ renderHTML: (attributes) => attributes.action ? { "data-action": String(attributes.action) } : {}
531
+ },
532
+ agent: {
533
+ default: "ai",
534
+ parseHTML: (element) => element.getAttribute("data-agent") ?? "ai",
535
+ renderHTML: (attributes) => ({ "data-agent": String(attributes.agent) })
536
+ },
537
+ ts: { default: null }
538
+ };
539
+ },
540
+ parseHTML() {
541
+ return [{ tag: "span[data-ai]" }];
542
+ },
543
+ renderHTML({ HTMLAttributes }) {
544
+ return ["span", mergeAttributes6({ "data-ai": "" }, this.options.HTMLAttributes, HTMLAttributes)];
545
+ }
546
+ });
547
+
548
+ // src/marks/comment.ts
549
+ import { Mark as Mark2, Extension, mergeAttributes as mergeAttributes7 } from "@tiptap/core";
550
+ import { TextSelection as TextSelection2 } from "@tiptap/pm/state";
551
+ var CommentMark = Mark2.create({
552
+ name: "comment",
553
+ inclusive: false,
554
+ addAttributes() {
555
+ return {
556
+ threadId: {
557
+ default: null,
558
+ parseHTML: (element) => element.getAttribute("data-thread-id"),
559
+ renderHTML: (attributes) => ({ "data-thread-id": String(attributes.threadId ?? "") })
560
+ },
561
+ resolved: {
562
+ default: false,
563
+ parseHTML: (element) => element.getAttribute("data-resolved") === "true",
564
+ renderHTML: (attributes) => ({ "data-resolved": String(attributes.resolved) })
565
+ }
566
+ };
567
+ },
568
+ parseHTML() {
569
+ return [{ tag: "span[data-thread-id]" }];
570
+ },
571
+ renderHTML({ HTMLAttributes }) {
572
+ return ["span", mergeAttributes7({ "data-comment": "" }, HTMLAttributes)];
573
+ }
574
+ });
575
+ function listCommentRanges(state) {
576
+ const seen = /* @__PURE__ */ new Map();
577
+ state.doc.descendants((node, pos) => {
578
+ for (const m of node.marks) {
579
+ if (m.type.name === "comment" && typeof m.attrs.threadId === "string") {
580
+ const from = pos;
581
+ const to = pos + node.nodeSize;
582
+ const existing = seen.get(m.attrs.threadId);
583
+ if (!existing) {
584
+ seen.set(m.attrs.threadId, { threadId: m.attrs.threadId, from, to, resolved: Boolean(m.attrs.resolved) });
585
+ } else {
586
+ existing.to = Math.max(existing.to, to);
587
+ existing.resolved = existing.resolved && Boolean(m.attrs.resolved);
588
+ }
589
+ }
590
+ }
591
+ return true;
592
+ });
593
+ return [...seen.values()].sort((a, b) => a.from - b.from);
594
+ }
595
+ var CommentCommands = Extension.create({
596
+ name: "tesseraCommentCommands",
597
+ addCommands() {
598
+ return {
599
+ addCommentThread: (threadId) => ({ commands }) => commands.setMark("comment", { threadId, resolved: false }),
600
+ setCommentResolved: (threadId, resolved) => ({ state, tr, dispatch }) => {
601
+ let touched = false;
602
+ state.doc.descendants((node, pos) => {
603
+ node.marks.filter((m) => m.type.name === "comment" && m.attrs.threadId === threadId).forEach((m) => {
604
+ const next = state.schema.marks.comment.create({ ...m.attrs, resolved });
605
+ tr.removeMark(pos, pos + node.nodeSize, m);
606
+ tr.addMark(pos, pos + node.nodeSize, next);
607
+ touched = true;
608
+ });
609
+ return true;
610
+ });
611
+ if (touched && dispatch) {
612
+ dispatch(tr);
613
+ }
614
+ return touched;
615
+ },
616
+ removeCommentThread: (threadId) => ({ state, tr, dispatch }) => {
617
+ let touched = false;
618
+ state.doc.descendants((node, pos) => {
619
+ if (node.marks.some((m) => m.type.name === "comment" && m.attrs.threadId === threadId)) {
620
+ tr.removeMark(pos, pos + node.nodeSize, state.schema.marks.comment);
621
+ touched = true;
622
+ }
623
+ return true;
624
+ });
625
+ if (touched && dispatch) {
626
+ dispatch(tr);
627
+ }
628
+ return touched;
629
+ },
630
+ focusNextCommentThread: () => ({ state, tr, dispatch }) => {
631
+ const ranges = listCommentRanges(state);
632
+ if (ranges.length === 0) {
633
+ return false;
634
+ }
635
+ const anchor = state.selection.from;
636
+ const next = ranges.find((r) => r.from > anchor) ?? ranges[0];
637
+ if (dispatch) {
638
+ tr.setSelection(TextSelection2.create(state.doc, next.from, next.to));
639
+ dispatch(tr);
640
+ }
641
+ return true;
642
+ }
643
+ };
644
+ }
645
+ });
646
+
647
+ // src/marks/placeholder.ts
648
+ import { Mark as Mark3, mergeAttributes as mergeAttributes8, Extension as Extension2 } from "@tiptap/core";
649
+ var PlaceholderMark = Mark3.create({
650
+ name: "tesseraPlaceholder",
651
+ addAttributes() {
652
+ return {
653
+ kind: {
654
+ default: "text",
655
+ parseHTML: (element) => element.getAttribute("data-kind") ?? "text",
656
+ renderHTML: (attributes) => ({ "data-kind": String(attributes.kind) })
657
+ }
658
+ };
659
+ },
660
+ parseHTML() {
661
+ return [{ tag: 'span[data-type="tessera-placeholder"]' }];
662
+ },
663
+ renderHTML({ HTMLAttributes }) {
664
+ return ["span", mergeAttributes8({ "data-type": "tessera-placeholder" }, HTMLAttributes)];
665
+ }
666
+ });
667
+ var PlaceholderCommands = Extension2.create({
668
+ name: "tesseraPlaceholderCommands",
669
+ addCommands() {
670
+ return {
671
+ togglePlaceholderMark: (kind = "text") => ({ commands }) => commands.toggleMark("tesseraPlaceholder", { kind }),
672
+ insertPlaceholderToken: (kind, label) => ({ chain }) => chain().insertContent({
673
+ type: "text",
674
+ text: label,
675
+ marks: [{ type: "tesseraPlaceholder", attrs: { kind } }]
676
+ }).run()
677
+ };
678
+ }
679
+ });
680
+
681
+ // src/extensions/input-rules.ts
682
+ import { Extension as Extension3, wrappingInputRule as wrappingInputRule2, markInputRule } from "@tiptap/core";
683
+ var TesseraInputRules = Extension3.create({
684
+ name: "tesseraInputRules",
685
+ addInputRules() {
686
+ const taskList = this.editor.schema.nodes.taskList;
687
+ const highlight = this.editor.schema.marks.highlight;
688
+ const rules = [];
689
+ if (taskList) {
690
+ rules.push(
691
+ wrappingInputRule2({
692
+ find: /^\[\] $/,
693
+ type: taskList
694
+ })
695
+ );
696
+ }
697
+ if (highlight) {
698
+ rules.push(
699
+ markInputRule({
700
+ find: /::([^:]+)::$/,
701
+ type: highlight
702
+ })
703
+ );
704
+ }
705
+ return rules;
706
+ }
707
+ });
708
+
709
+ // src/extensions/shortcuts.ts
710
+ import { Extension as Extension4 } from "@tiptap/core";
711
+ import { TextSelection as TextSelection3 } from "@tiptap/pm/state";
712
+ var TesseraShortcuts = Extension4.create({
713
+ name: "tesseraShortcuts",
714
+ priority: 500,
715
+ addCommands() {
716
+ return {
717
+ moveBlockUp: () => ({ state, tr, dispatch }) => swapBlock(state, tr, dispatch, -1),
718
+ moveBlockDown: () => ({ state, tr, dispatch }) => swapBlock(state, tr, dispatch, 1)
719
+ };
720
+ },
721
+ addKeyboardShortcuts() {
722
+ return {
723
+ "Mod-Shift-1": () => this.editor.commands.toggleHeading({ level: 1 }),
724
+ "Mod-Shift-2": () => this.editor.commands.toggleHeading({ level: 2 }),
725
+ "Mod-Shift-3": () => this.editor.commands.toggleHeading({ level: 3 }),
726
+ "Mod-Shift-4": () => this.editor.commands.toggleHeading({ level: 4 }),
727
+ "Mod-Shift-7": () => this.editor.commands.toggleOrderedList(),
728
+ "Mod-Shift-8": () => this.editor.commands.toggleBulletList(),
729
+ "Mod-Shift-c": () => this.editor.commands.toggleTaskList(),
730
+ "Mod-Alt-h": () => this.editor.commands.toggleHint(),
731
+ "Mod-j": () => this.editor.commands.toggleCode(),
732
+ "Mod-Shift-9": () => this.editor.commands.toggleCodeBlock(),
733
+ "Mod-Shift-.": () => this.editor.commands.toggleBlockquote(),
734
+ "Mod-e": () => {
735
+ this.editor.emit("tessera:formatPanel", { kind: "color" });
736
+ return true;
737
+ },
738
+ "Mod-k": () => {
739
+ this.editor.emit("tessera:linkPanel", {});
740
+ return true;
741
+ },
742
+ "Mod-f": () => {
743
+ this.editor.emit("tessera:findPanel", {});
744
+ return true;
745
+ },
746
+ "Mod-Shift-k": () => {
747
+ this.editor.emit("tessera:askPanel", {});
748
+ return true;
749
+ },
750
+ "Mod-Alt-s": () => this.editor.commands.insertTableTyped({ withHeaderRow: true }),
751
+ "Mod-Alt-t": () => this.editor.commands.insertTableTyped({ withHeaderRow: false }),
752
+ "Mod-Alt-p": () => this.editor.commands.togglePlaceholderMark("text"),
753
+ "Mod-Alt-m": () => {
754
+ this.editor.emit("tessera:commentPanel", {});
755
+ return true;
756
+ },
757
+ "Alt-ArrowUp": () => this.editor.commands.moveBlockUp(),
758
+ "Alt-ArrowDown": () => this.editor.commands.moveBlockDown()
759
+ };
760
+ }
761
+ });
762
+ function swapBlock(state, tr, dispatch, direction) {
763
+ const { $from } = state.selection;
764
+ if ($from.depth < 1) {
765
+ return false;
766
+ }
767
+ const index = $from.index(0);
768
+ const other = index + direction;
769
+ if (other < 0 || other >= state.doc.childCount) {
770
+ return false;
771
+ }
772
+ const node = state.doc.child(index);
773
+ const otherNode = state.doc.child(other);
774
+ const pos = $from.before(1);
775
+ const nodeStart = direction === -1 ? pos - otherNode.nodeSize : pos;
776
+ const rangeEnd = nodeStart + otherNode.nodeSize + node.nodeSize;
777
+ if (dispatch) {
778
+ const content = direction === -1 ? [node, otherNode] : [otherNode, node];
779
+ tr.replaceWith(nodeStart, rangeEnd, content);
780
+ const anchor = tr.mapping.map(state.selection.anchor);
781
+ tr.setSelection(TextSelection3.near(tr.doc.resolve(Math.min(anchor, tr.doc.content.size))));
782
+ dispatch(tr);
783
+ }
784
+ return true;
785
+ }
786
+
787
+ // src/extensions/find-replace.ts
788
+ import { Extension as Extension5 } from "@tiptap/core";
789
+ import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
790
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
791
+ var findReplaceKey = new PluginKey2("tesseraFindReplace");
792
+ function computeMatches(doc, query) {
793
+ if (!query) {
794
+ return [];
795
+ }
796
+ const matches = [];
797
+ const needle = query.toLowerCase();
798
+ doc.descendants((node, pos) => {
799
+ if (node.isText && node.text) {
800
+ const haystack = node.text.toLowerCase();
801
+ let idx = haystack.indexOf(needle);
802
+ while (idx !== -1) {
803
+ matches.push({ from: pos + idx, to: pos + idx + needle.length });
804
+ idx = haystack.indexOf(needle, idx + needle.length);
805
+ }
806
+ }
807
+ return true;
808
+ });
809
+ return matches;
810
+ }
811
+ var TesseraFindReplace = Extension5.create({
812
+ name: "tesseraFindReplace",
813
+ addStorage() {
814
+ return {
815
+ query: "",
816
+ matches: [],
817
+ active: 0,
818
+ visible: false
819
+ };
820
+ },
821
+ addCommands() {
822
+ return {
823
+ openFindPanel: () => ({ tr, dispatch }) => {
824
+ if (dispatch) {
825
+ tr.setMeta(findReplaceKey, { type: "open" });
826
+ dispatch(tr);
827
+ }
828
+ return true;
829
+ },
830
+ closeFindPanel: () => ({ tr, dispatch }) => {
831
+ if (dispatch) {
832
+ tr.setMeta(findReplaceKey, { type: "close" });
833
+ dispatch(tr);
834
+ }
835
+ return true;
836
+ },
837
+ setFindQuery: (query) => ({ tr, dispatch }) => {
838
+ if (dispatch) {
839
+ tr.setMeta(findReplaceKey, { type: "query", query });
840
+ dispatch(tr);
841
+ }
842
+ return true;
843
+ },
844
+ findNext: () => ({ tr, state, dispatch }) => {
845
+ const s = findReplaceKey.getState(state);
846
+ if (!s || s.matches.length === 0) {
847
+ return false;
848
+ }
849
+ if (dispatch) {
850
+ tr.setMeta(findReplaceKey, { type: "active", active: (s.active + 1) % s.matches.length });
851
+ dispatch(tr);
852
+ }
853
+ return true;
854
+ },
855
+ findPrev: () => ({ tr, state, dispatch }) => {
856
+ const s = findReplaceKey.getState(state);
857
+ if (!s || s.matches.length === 0) {
858
+ return false;
859
+ }
860
+ if (dispatch) {
861
+ tr.setMeta(findReplaceKey, {
862
+ type: "active",
863
+ active: (s.active - 1 + s.matches.length) % s.matches.length
864
+ });
865
+ dispatch(tr);
866
+ }
867
+ return true;
868
+ },
869
+ replaceCurrent: (replacement) => ({ state, tr, dispatch }) => {
870
+ const s = findReplaceKey.getState(state);
871
+ if (!s || s.matches.length === 0) {
872
+ return false;
873
+ }
874
+ const m = s.matches[s.active];
875
+ if (!m) {
876
+ return false;
877
+ }
878
+ if (dispatch) {
879
+ tr.insertText(replacement, m.from, m.to);
880
+ tr.setMeta(findReplaceKey, { type: "requery" });
881
+ dispatch(tr);
882
+ }
883
+ return true;
884
+ },
885
+ replaceAll: (replacement) => ({ state, tr, dispatch }) => {
886
+ const s = findReplaceKey.getState(state);
887
+ if (!s || s.matches.length === 0) {
888
+ return false;
889
+ }
890
+ if (dispatch) {
891
+ for (let i = s.matches.length - 1; i >= 0; i--) {
892
+ const m = s.matches[i];
893
+ tr.insertText(replacement, m.from, m.to);
894
+ }
895
+ tr.setMeta(findReplaceKey, { type: "requery" });
896
+ dispatch(tr);
897
+ }
898
+ return true;
899
+ }
900
+ };
901
+ },
902
+ addProseMirrorPlugins() {
903
+ return [
904
+ new Plugin2({
905
+ key: findReplaceKey,
906
+ state: {
907
+ init: () => ({ query: "", matches: [], active: 0, visible: false }),
908
+ apply: (tr, prev, _oldState, newState) => {
909
+ const meta = tr.getMeta(findReplaceKey);
910
+ let next = null;
911
+ if (meta?.type === "open") {
912
+ next = { ...prev, visible: true };
913
+ } else if (meta?.type === "close") {
914
+ next = { ...prev, visible: false, matches: [], query: "", active: 0 };
915
+ } else if (meta?.type === "query") {
916
+ const matches = computeMatches(newState.doc, meta.query);
917
+ next = { query: meta.query, matches, active: 0, visible: true };
918
+ } else if (meta?.type === "active") {
919
+ next = { ...prev, active: meta.active };
920
+ } else if (meta?.type === "requery") {
921
+ next = { ...prev, matches: [], active: 0 };
922
+ }
923
+ if (!next) {
924
+ if (prev.query && prev.visible && tr.docChanged) {
925
+ const matches = computeMatches(newState.doc, prev.query);
926
+ return { ...prev, matches, active: 0 };
927
+ }
928
+ return prev;
929
+ }
930
+ this.storage.query = next.query;
931
+ this.storage.matches = next.matches;
932
+ this.storage.active = next.active;
933
+ this.storage.visible = next.visible;
934
+ return next;
935
+ }
936
+ },
937
+ props: {
938
+ decorations(state) {
939
+ const s = findReplaceKey.getState(state);
940
+ if (!s || !s.visible || s.matches.length === 0) {
941
+ return DecorationSet.empty;
942
+ }
943
+ const decorations = s.matches.map(
944
+ (m, i) => Decoration.inline(m.from, m.to, {
945
+ class: i === s.active ? "tessera-find-match tessera-find-match--active" : "tessera-find-match"
946
+ })
947
+ );
948
+ return DecorationSet.create(state.doc, decorations);
949
+ }
950
+ }
951
+ })
952
+ ];
953
+ }
954
+ });
955
+
956
+ // src/extensions/slash.ts
957
+ import { Extension as Extension6 } from "@tiptap/core";
958
+ import Suggestion from "@tiptap/suggestion";
959
+ import { PluginKey as PluginKey3 } from "@tiptap/pm/state";
960
+
961
+ // src/i18n.ts
962
+ var tesseraMessages = {
963
+ "zh-CN": {
964
+ // placeholder
965
+ placeholderEmpty: "\u8F93\u5165 / \u5524\u8D77\u83DC\u5355\uFF0C\u6216\u76F4\u63A5\u5F00\u59CB\u4E66\u5199\u2026",
966
+ // slash menu groups
967
+ groupBasic: "\u57FA\u7840\u5757",
968
+ groupAdvanced: "\u9AD8\u7EA7\u5757",
969
+ groupAi: "AI",
970
+ // slash items
971
+ itemText: "\u6587\u672C",
972
+ itemTextDesc: "\u666E\u901A\u6BB5\u843D",
973
+ itemH1: "\u6807\u9898 1",
974
+ itemH2: "\u6807\u9898 2",
975
+ itemH3: "\u6807\u9898 3",
976
+ itemH4: "\u6807\u9898 4",
977
+ itemBullet: "\u65E0\u5E8F\u5217\u8868",
978
+ itemBulletDesc: "\u7B80\u5355\u7684\u9879\u76EE\u7B26\u53F7\u5217\u8868",
979
+ itemOrdered: "\u6709\u5E8F\u5217\u8868",
980
+ itemOrderedDesc: "\u5E26\u7F16\u53F7\u7684\u5217\u8868",
981
+ itemTask: "\u4EFB\u52A1\u6E05\u5355",
982
+ itemTaskDesc: "\u7528\u590D\u9009\u6846\u8DDF\u8E2A\u5F85\u529E",
983
+ itemQuote: "\u5F15\u7528",
984
+ itemQuoteDesc: "\u5F15\u7528\u4E00\u6BB5\u6587\u5B57",
985
+ itemCode: "\u4EE3\u7801\u5757",
986
+ itemCodeDesc: "\u5E26\u8BED\u6CD5\u9AD8\u4EAE\u7684\u4EE3\u7801",
987
+ itemDivider: "\u5206\u5272\u7EBF",
988
+ itemDividerDesc: "\u6C34\u5E73\u5206\u5272\u7EBF",
989
+ itemHint: "\u63D0\u793A\u5757",
990
+ itemHintDesc: "\u9192\u76EE\u7684\u4FE1\u606F/\u8B66\u544A\u5BB9\u5668\uFF08!! + \u7A7A\u683C\uFF09",
991
+ itemCollapsible: "\u6298\u53E0\u5757",
992
+ itemCollapsibleDesc: "\u53EF\u6298\u53E0\u7684\u5185\u5BB9\u533A\uFF08>> + \u7A7A\u683C\uFF09",
993
+ itemImage: "\u56FE\u7247",
994
+ itemImageDesc: "\u4E0A\u4F20\u6216\u7C98\u8D34\u56FE\u7247",
995
+ itemImageUrl: "\u56FE\u7247\u94FE\u63A5",
996
+ // AI slash items
997
+ itemSummarize: "AI \u6458\u8981",
998
+ itemSummarizeDesc: "\u4E3A\u6574\u7BC7\u6587\u6863\u751F\u6210 TL;DR",
999
+ itemAsk: "AI \u95EE\u7B54",
1000
+ itemAskDesc: "\u9488\u5BF9\u5F53\u524D\u6587\u6863\u63D0\u95EE",
1001
+ itemImprove: "AI \u6539\u5199",
1002
+ itemImproveDesc: "\u6539\u5199\u9009\u4E2D\u7684\u6587\u5B57",
1003
+ // selection toolbar
1004
+ tooltipBold: "\u52A0\u7C97",
1005
+ tooltipItalic: "\u659C\u4F53",
1006
+ tooltipUnderline: "\u4E0B\u5212\u7EBF",
1007
+ tooltipStrike: "\u5220\u9664\u7EBF",
1008
+ tooltipCode: "\u884C\u5185\u4EE3\u7801",
1009
+ tooltipColor: "\u6587\u5B57\u989C\u8272",
1010
+ tooltipHighlight: "\u9AD8\u4EAE",
1011
+ tooltipLink: "\u94FE\u63A5",
1012
+ tooltipComment: "\u8BC4\u8BBA\uFF08v1.x\uFF09",
1013
+ tooltipTurnCollapsible: "\u8F6C\u4E3A\u6298\u53E0\u5757",
1014
+ tooltipMore: "\u66F4\u591A",
1015
+ tooltipImprove: "AI \u6539\u5199",
1016
+ tooltipCopyMarkdown: "\u590D\u5236\u4E3A Markdown",
1017
+ // color/highlight panel
1018
+ colorDefault: "\u9ED8\u8BA4",
1019
+ highlightNone: "\u65E0\u9AD8\u4EAE",
1020
+ // link panel
1021
+ linkPlaceholder: "\u94FE\u63A5\u5730\u5740\u2026",
1022
+ linkApply: "\u5E94\u7528",
1023
+ linkRemove: "\u79FB\u9664\u94FE\u63A5",
1024
+ linkOpen: "\u6253\u5F00",
1025
+ // empty-line toolbar
1026
+ emptyLineExpand: "\u5C55\u5F00\u5168\u90E8\u5757",
1027
+ // find & replace
1028
+ findPlaceholder: "\u67E5\u627E\u2026",
1029
+ replacePlaceholder: "\u66FF\u6362\u4E3A\u2026",
1030
+ findNext: "\u4E0B\u4E00\u4E2A",
1031
+ findPrev: "\u4E0A\u4E00\u4E2A",
1032
+ replaceOne: "\u66FF\u6362",
1033
+ replaceAll: "\u5168\u90E8\u66FF\u6362",
1034
+ findCount: (n) => `${n} \u4E2A\u7ED3\u679C`,
1035
+ findClose: "\u5173\u95ED",
1036
+ // image
1037
+ imageUploadFailed: "\u56FE\u7247\u4E0A\u4F20\u5931\u8D25",
1038
+ imageAlignLeft: "\u5DE6\u5BF9\u9F50",
1039
+ imageAlignCenter: "\u5C45\u4E2D",
1040
+ imageAlignFull: "\u5168\u5BBD",
1041
+ // AI panel
1042
+ aiTitle: "AI",
1043
+ aiAskPlaceholder: "\u9488\u5BF9\u672C\u6587\u6863\u63D0\u95EE\u2026",
1044
+ aiSend: "\u53D1\u9001",
1045
+ aiThinking: "\u601D\u8003\u4E2D\u2026",
1046
+ aiAccept: "\u63A5\u53D7",
1047
+ aiReject: "\u62D2\u7EDD",
1048
+ aiImprovePrompt: "\u6539\u5199\u8981\u6C42\uFF08\u53EF\u9009\uFF09\u2026",
1049
+ aiImproveRun: "\u6267\u884C",
1050
+ aiStreaming: "\u751F\u6210\u4E2D\u2026",
1051
+ aiRuntimeMissing: "\u672A\u6CE8\u5165 AI Runtime\uFF0C\u6B64\u64CD\u4F5C\u4E0D\u53EF\u7528",
1052
+ // v1.1: tables
1053
+ itemTable: "\u8868\u683C",
1054
+ itemTableDesc: "\u5E26\u7C7B\u578B\u5316\u5217\u7684\u7ED3\u6784\u5316\u8868\u683C\uFF08\u2318\u2325S\uFF09",
1055
+ itemTableSimple: "\u65E0\u8868\u5934\u8868\u683C",
1056
+ itemTableSimpleDesc: "\u7EAF\u6587\u672C\u7B80\u6613\u5E03\u5C40\u8868\uFF08\u2318\u2325T\uFF09",
1057
+ colTypeText: "\u6587\u672C",
1058
+ colTypeCheckbox: "\u590D\u9009\u6846",
1059
+ colTypeSelect: "\u5355\u9009\u6807\u7B7E",
1060
+ colTypeMultiSelect: "\u591A\u9009\u6807\u7B7E",
1061
+ colTypeNumber: "\u6570\u5B57",
1062
+ colTypeDate: "\u65E5\u671F",
1063
+ colTypeLink: "\u94FE\u63A5",
1064
+ colMenuTitle: "\u5217\u64CD\u4F5C",
1065
+ colMenuSortAsc: "\u5347\u5E8F\u6392\u5E8F",
1066
+ colMenuSortDesc: "\u964D\u5E8F\u6392\u5E8F",
1067
+ colMenuInsertLeft: "\u5DE6\u4FA7\u63D2\u5165\u5217",
1068
+ colMenuInsertRight: "\u53F3\u4FA7\u63D2\u5165\u5217",
1069
+ colMenuDelete: "\u5220\u9664\u6B64\u5217",
1070
+ colMenuType: "\u5217\u7C7B\u578B",
1071
+ rowMenuTitle: "\u884C\u64CD\u4F5C",
1072
+ rowMenuInsertAbove: "\u4E0A\u65B9\u63D2\u5165\u884C",
1073
+ rowMenuInsertBelow: "\u4E0B\u65B9\u63D2\u5165\u884C",
1074
+ rowMenuDelete: "\u5220\u9664\u6B64\u884C",
1075
+ tableDelete: "\u5220\u9664\u8868\u683C",
1076
+ tableCopyCsv: "\u590D\u5236\u4E3A CSV",
1077
+ tableToggleHeader: "\u5207\u6362\u8868\u5934",
1078
+ cellEmpty: "\u7A7A",
1079
+ // v1.1: history
1080
+ itemHistory: "\u7248\u672C\u5386\u53F2",
1081
+ historyTitle: "\u7248\u672C\u5386\u53F2",
1082
+ historyEmpty: "\u6682\u65E0\u5FEB\u7167\uFF08\u7F16\u8F91\u540E\u7A7A\u95F2\u81EA\u52A8\u4FDD\u5B58\uFF0C\u6216\u624B\u52A8\u6355\u83B7\uFF09",
1083
+ historyCapture: "\u6355\u83B7\u5FEB\u7167",
1084
+ historyRestore: "\u6062\u590D\u6B64\u7248\u672C",
1085
+ historyCurrent: "\u5F53\u524D",
1086
+ historyDiffAdded: "\u65B0\u589E",
1087
+ historyDiffRemoved: "\u5220\u9664",
1088
+ historyDiffChanged: "\u4FEE\u6539",
1089
+ historyConfirmRestore: "\u6062\u590D\u5230\u8BE5\u7248\u672C\uFF1F\u5F53\u524D\u5185\u5BB9\u5C06\u88AB\u66FF\u6362\uFF08\u53EF\u64A4\u9500\uFF09",
1090
+ // v1.1: comments
1091
+ tooltipCommentV11: "\u8BC4\u8BBA",
1092
+ commentTitle: "\u8BC4\u8BBA",
1093
+ commentEmpty: "\u6682\u65E0\u8BC4\u8BBA",
1094
+ commentPlaceholder: "\u5199\u4E0B\u8BC4\u8BBA\u2026\uFF08Shift+Enter \u6362\u884C\uFF09",
1095
+ commentSend: "\u53D1\u9001",
1096
+ commentResolve: "\u89E3\u51B3",
1097
+ commentReopen: "\u91CD\u65B0\u6253\u5F00",
1098
+ commentDelete: "\u5220\u9664",
1099
+ commentResolvedBadge: "\u5DF2\u89E3\u51B3",
1100
+ commentCount: (n) => `${n} \u6761\u8BC4\u8BBA`,
1101
+ // v1.1: embed / toc / placeholder
1102
+ itemEmbed: "\u5D4C\u5165",
1103
+ itemEmbedDesc: "\u5D4C\u5165\u5916\u90E8\u7F51\u9875\uFF08iframe \u6C99\u7BB1\uFF09",
1104
+ itemToc: "\u76EE\u5F55",
1105
+ itemTocDesc: "\u81EA\u52A8\u751F\u6210\u5168\u6587\u5927\u7EB2\uFF08H1\u2013H4\uFF09",
1106
+ embedPlaceholder: "\u7C98\u8D34\u8981\u5D4C\u5165\u7684\u94FE\u63A5\u2026",
1107
+ embedApply: "\u5D4C\u5165",
1108
+ embedInvalid: "\u65E0\u6548\u94FE\u63A5",
1109
+ embedOpen: "\u6253\u5F00\u539F\u94FE\u63A5",
1110
+ tocEmpty: "\u6682\u65E0\u6807\u9898\u2014\u2014\u6DFB\u52A0 H1\u2013H4 \u540E\u81EA\u52A8\u51FA\u73B0",
1111
+ placeholderText: "\u5F85\u8865\u5145",
1112
+ placeholderPerson: "\u5F85\u586B\u4EBA",
1113
+ placeholderDate: "\u5F85\u586B\u65E5\u671F",
1114
+ // v1.1: block context menu
1115
+ menuCopyAnchor: "\u590D\u5236\u951A\u94FE\u63A5",
1116
+ menuCopyBlockId: "\u590D\u5236\u5757 ID",
1117
+ menuDeleteBlock: "\u5220\u9664\u6B64\u5757"
1118
+ },
1119
+ "en-US": {
1120
+ placeholderEmpty: "Type / for blocks, or just start writing\u2026",
1121
+ groupBasic: "Basic blocks",
1122
+ groupAdvanced: "Advanced blocks",
1123
+ groupAi: "AI",
1124
+ itemText: "Text",
1125
+ itemTextDesc: "Plain paragraph",
1126
+ itemH1: "Heading 1",
1127
+ itemH2: "Heading 2",
1128
+ itemH3: "Heading 3",
1129
+ itemH4: "Heading 4",
1130
+ itemBullet: "Bullet list",
1131
+ itemBulletDesc: "Simple bulleted list",
1132
+ itemOrdered: "Numbered list",
1133
+ itemOrderedDesc: "List with numbering",
1134
+ itemTask: "Task list",
1135
+ itemTaskDesc: "Track to-dos with checkboxes",
1136
+ itemQuote: "Quote",
1137
+ itemQuoteDesc: "Quote a passage",
1138
+ itemCode: "Code block",
1139
+ itemCodeDesc: "Code with syntax highlighting",
1140
+ itemDivider: "Divider",
1141
+ itemDividerDesc: "Horizontal rule",
1142
+ itemHint: "Hint",
1143
+ itemHintDesc: "Callout container (!! + space)",
1144
+ itemCollapsible: "Collapsible",
1145
+ itemCollapsibleDesc: "Foldable section (>> + space)",
1146
+ itemImage: "Image",
1147
+ itemImageDesc: "Upload or paste an image",
1148
+ itemImageUrl: "Image URL",
1149
+ itemSummarize: "AI Summarize",
1150
+ itemSummarizeDesc: "Generate a TL;DR for this doc",
1151
+ itemAsk: "AI Ask",
1152
+ itemAskDesc: "Ask questions about this doc",
1153
+ itemImprove: "AI Improve",
1154
+ itemImproveDesc: "Rewrite the selection",
1155
+ tooltipBold: "Bold",
1156
+ tooltipItalic: "Italic",
1157
+ tooltipUnderline: "Underline",
1158
+ tooltipStrike: "Strikethrough",
1159
+ tooltipCode: "Inline code",
1160
+ tooltipColor: "Text color",
1161
+ tooltipHighlight: "Highlight",
1162
+ tooltipLink: "Link",
1163
+ tooltipComment: "Comment (v1.x)",
1164
+ tooltipTurnCollapsible: "Turn into collapsible",
1165
+ tooltipMore: "More",
1166
+ tooltipImprove: "AI Improve",
1167
+ tooltipCopyMarkdown: "Copy as Markdown",
1168
+ colorDefault: "Default",
1169
+ highlightNone: "No highlight",
1170
+ linkPlaceholder: "Link URL\u2026",
1171
+ linkApply: "Apply",
1172
+ linkRemove: "Remove link",
1173
+ linkOpen: "Open",
1174
+ emptyLineExpand: "Show all blocks",
1175
+ findPlaceholder: "Find\u2026",
1176
+ replacePlaceholder: "Replace with\u2026",
1177
+ findNext: "Next",
1178
+ findPrev: "Prev",
1179
+ replaceOne: "Replace",
1180
+ replaceAll: "Replace all",
1181
+ findCount: (n) => `${n} result${n === 1 ? "" : "s"}`,
1182
+ findClose: "Close",
1183
+ imageUploadFailed: "Image upload failed",
1184
+ imageAlignLeft: "Align left",
1185
+ imageAlignCenter: "Center",
1186
+ imageAlignFull: "Full width",
1187
+ aiTitle: "AI",
1188
+ aiAskPlaceholder: "Ask about this doc\u2026",
1189
+ aiSend: "Send",
1190
+ aiThinking: "Thinking\u2026",
1191
+ aiAccept: "Accept",
1192
+ aiReject: "Reject",
1193
+ aiImprovePrompt: "Instruction (optional)\u2026",
1194
+ aiImproveRun: "Run",
1195
+ aiStreaming: "Generating\u2026",
1196
+ aiRuntimeMissing: "No AI Runtime injected; action unavailable",
1197
+ // v1.1: tables
1198
+ itemTable: "Table",
1199
+ itemTableDesc: "Structured table with typed columns (\u2318\u2325S)",
1200
+ itemTableSimple: "Table (no header)",
1201
+ itemTableSimpleDesc: "Plain layout table without header (\u2318\u2325T)",
1202
+ colTypeText: "Text",
1203
+ colTypeCheckbox: "Checkbox",
1204
+ colTypeSelect: "Select tag",
1205
+ colTypeMultiSelect: "Multi-select",
1206
+ colTypeNumber: "Number",
1207
+ colTypeDate: "Date",
1208
+ colTypeLink: "Link",
1209
+ colMenuTitle: "Column",
1210
+ colMenuSortAsc: "Sort ascending",
1211
+ colMenuSortDesc: "Sort descending",
1212
+ colMenuInsertLeft: "Insert column left",
1213
+ colMenuInsertRight: "Insert column right",
1214
+ colMenuDelete: "Delete column",
1215
+ colMenuType: "Column type",
1216
+ rowMenuTitle: "Row",
1217
+ rowMenuInsertAbove: "Insert row above",
1218
+ rowMenuInsertBelow: "Insert row below",
1219
+ rowMenuDelete: "Delete row",
1220
+ tableDelete: "Delete table",
1221
+ tableCopyCsv: "Copy as CSV",
1222
+ tableToggleHeader: "Toggle header row",
1223
+ cellEmpty: "Empty",
1224
+ // v1.1: history
1225
+ itemHistory: "Version history",
1226
+ historyTitle: "Version history",
1227
+ historyEmpty: "No snapshots yet (auto-captured when idle, or capture manually)",
1228
+ historyCapture: "Capture snapshot",
1229
+ historyRestore: "Restore this version",
1230
+ historyCurrent: "Current",
1231
+ historyDiffAdded: "Added",
1232
+ historyDiffRemoved: "Removed",
1233
+ historyDiffChanged: "Changed",
1234
+ historyConfirmRestore: "Restore this version? Current content will be replaced (undoable)",
1235
+ // v1.1: comments
1236
+ tooltipCommentV11: "Comment",
1237
+ commentTitle: "Comments",
1238
+ commentEmpty: "No comments yet",
1239
+ commentPlaceholder: "Write a comment\u2026 (Shift+Enter for newline)",
1240
+ commentSend: "Send",
1241
+ commentResolve: "Resolve",
1242
+ commentReopen: "Reopen",
1243
+ commentDelete: "Delete",
1244
+ commentResolvedBadge: "Resolved",
1245
+ commentCount: (n) => `${n} comment${n === 1 ? "" : "s"}`,
1246
+ // v1.1: embed / toc / placeholder
1247
+ itemEmbed: "Embed",
1248
+ itemEmbedDesc: "Embed an external page (sandboxed iframe)",
1249
+ itemToc: "Table of contents",
1250
+ itemTocDesc: "Auto outline from H1\u2013H4",
1251
+ embedPlaceholder: "Paste a link to embed\u2026",
1252
+ embedApply: "Embed",
1253
+ embedInvalid: "Invalid URL",
1254
+ embedOpen: "Open original",
1255
+ tocEmpty: "No headings yet \u2014 add H1\u2013H4 and they appear here",
1256
+ placeholderText: "to fill in",
1257
+ placeholderPerson: "assignee",
1258
+ placeholderDate: "due date",
1259
+ // v1.1: block context menu
1260
+ menuCopyAnchor: "Copy anchor link",
1261
+ menuCopyBlockId: "Copy block ID",
1262
+ menuDeleteBlock: "Delete block"
1263
+ }
1264
+ };
1265
+ function createTesseraT(locale = "zh-CN") {
1266
+ const table = tesseraMessages[locale] ?? tesseraMessages["zh-CN"];
1267
+ return (key) => table[key] ?? key;
1268
+ }
1269
+
1270
+ // src/extensions/slash.ts
1271
+ function chainDelete(editor, range) {
1272
+ return editor.chain().focus().deleteRange(range);
1273
+ }
1274
+ function defaultSlashItems(t) {
1275
+ return [
1276
+ {
1277
+ id: "text",
1278
+ group: "basic",
1279
+ title: t("itemText"),
1280
+ description: t("itemTextDesc"),
1281
+ keywords: ["text", "paragraph", "\u6587\u672C", "\u6BB5\u843D"],
1282
+ command: ({ editor, range }) => chainDelete(editor, range).setParagraph().run()
1283
+ },
1284
+ {
1285
+ id: "h1",
1286
+ group: "basic",
1287
+ title: t("itemH1"),
1288
+ shortcut: "\u2318\u21E71",
1289
+ keywords: ["heading", "title", "\u6807\u9898"],
1290
+ command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 1 }).run()
1291
+ },
1292
+ {
1293
+ id: "h2",
1294
+ group: "basic",
1295
+ title: t("itemH2"),
1296
+ shortcut: "\u2318\u21E72",
1297
+ keywords: ["heading", "title", "\u6807\u9898"],
1298
+ command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 2 }).run()
1299
+ },
1300
+ {
1301
+ id: "h3",
1302
+ group: "basic",
1303
+ title: t("itemH3"),
1304
+ shortcut: "\u2318\u21E73",
1305
+ keywords: ["heading", "title", "\u6807\u9898"],
1306
+ command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 3 }).run()
1307
+ },
1308
+ {
1309
+ id: "h4",
1310
+ group: "basic",
1311
+ title: t("itemH4"),
1312
+ shortcut: "\u2318\u21E74",
1313
+ keywords: ["heading", "title", "\u6807\u9898"],
1314
+ command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 4 }).run()
1315
+ },
1316
+ {
1317
+ id: "bulletList",
1318
+ group: "basic",
1319
+ title: t("itemBullet"),
1320
+ description: t("itemBulletDesc"),
1321
+ shortcut: "\u2318\u21E78",
1322
+ keywords: ["bullet", "list", "unordered", "\u5217\u8868"],
1323
+ command: ({ editor, range }) => chainDelete(editor, range).toggleBulletList().run()
1324
+ },
1325
+ {
1326
+ id: "orderedList",
1327
+ group: "basic",
1328
+ title: t("itemOrdered"),
1329
+ description: t("itemOrderedDesc"),
1330
+ shortcut: "\u2318\u21E77",
1331
+ keywords: ["ordered", "list", "numbered", "\u5217\u8868"],
1332
+ command: ({ editor, range }) => chainDelete(editor, range).toggleOrderedList().run()
1333
+ },
1334
+ {
1335
+ id: "taskList",
1336
+ group: "basic",
1337
+ title: t("itemTask"),
1338
+ description: t("itemTaskDesc"),
1339
+ shortcut: "\u2318\u21E7C",
1340
+ keywords: ["task", "todo", "checkbox", "\u4EFB\u52A1", "\u6E05\u5355"],
1341
+ command: ({ editor, range }) => chainDelete(editor, range).toggleTaskList().run()
1342
+ },
1343
+ {
1344
+ id: "quote",
1345
+ group: "basic",
1346
+ title: t("itemQuote"),
1347
+ description: t("itemQuoteDesc"),
1348
+ shortcut: "\u2318\u21E7.",
1349
+ keywords: ["quote", "blockquote", "\u5F15\u7528"],
1350
+ command: ({ editor, range }) => chainDelete(editor, range).toggleBlockquote().run()
1351
+ },
1352
+ {
1353
+ id: "codeBlock",
1354
+ group: "advanced",
1355
+ title: t("itemCode"),
1356
+ description: t("itemCodeDesc"),
1357
+ shortcut: "\u2318\u21E79",
1358
+ keywords: ["code", "\u4EE3\u7801"],
1359
+ command: ({ editor, range }) => chainDelete(editor, range).toggleCodeBlock().run()
1360
+ },
1361
+ {
1362
+ id: "divider",
1363
+ group: "advanced",
1364
+ title: t("itemDivider"),
1365
+ description: t("itemDividerDesc"),
1366
+ keywords: ["divider", "hr", "rule", "\u5206\u5272\u7EBF"],
1367
+ command: ({ editor, range }) => chainDelete(editor, range).setHorizontalRule().run()
1368
+ },
1369
+ {
1370
+ id: "hint",
1371
+ group: "advanced",
1372
+ title: t("itemHint"),
1373
+ description: t("itemHintDesc"),
1374
+ shortcut: "\u2318\u2325H",
1375
+ keywords: ["hint", "callout", "info", "\u63D0\u793A"],
1376
+ command: ({ editor, range }) => chainDelete(editor, range).setHint().run()
1377
+ },
1378
+ {
1379
+ id: "collapsible",
1380
+ group: "advanced",
1381
+ title: t("itemCollapsible"),
1382
+ description: t("itemCollapsibleDesc"),
1383
+ keywords: ["collapsible", "fold", "details", "\u6298\u53E0"],
1384
+ command: ({ editor, range }) => chainDelete(editor, range).insertCollapsible().run()
1385
+ },
1386
+ {
1387
+ id: "image",
1388
+ group: "advanced",
1389
+ title: t("itemImage"),
1390
+ description: t("itemImageDesc"),
1391
+ keywords: ["image", "picture", "photo", "\u56FE\u7247"],
1392
+ command: ({ editor, range }) => {
1393
+ chainDelete(editor, range).run();
1394
+ editor.emit("tessera:insertImage", {});
1395
+ }
1396
+ },
1397
+ {
1398
+ id: "table",
1399
+ group: "advanced",
1400
+ title: t("itemTable"),
1401
+ description: t("itemTableDesc"),
1402
+ shortcut: "\u2318\u2325S",
1403
+ keywords: ["table", "\u8868\u683C"],
1404
+ command: ({ editor, range }) => chainDelete(editor, range).insertTableTyped({ withHeaderRow: true }).run()
1405
+ },
1406
+ {
1407
+ id: "table-simple",
1408
+ group: "advanced",
1409
+ title: t("itemTableSimple"),
1410
+ description: t("itemTableSimpleDesc"),
1411
+ shortcut: "\u2318\u2325T",
1412
+ keywords: ["table", "simple", "\u8868\u683C"],
1413
+ command: ({ editor, range }) => chainDelete(editor, range).insertTableTyped({ withHeaderRow: false }).run()
1414
+ },
1415
+ {
1416
+ id: "embed",
1417
+ group: "advanced",
1418
+ title: t("itemEmbed"),
1419
+ description: t("itemEmbedDesc"),
1420
+ keywords: ["embed", "iframe", "\u5D4C\u5165"],
1421
+ command: ({ editor, range }) => chainDelete(editor, range).insertEmbed({ src: "" }).run()
1422
+ },
1423
+ {
1424
+ id: "toc",
1425
+ group: "advanced",
1426
+ title: t("itemToc"),
1427
+ description: t("itemTocDesc"),
1428
+ keywords: ["toc", "outline", "\u76EE\u5F55", "\u5927\u7EB2"],
1429
+ command: ({ editor, range }) => chainDelete(editor, range).insertToc().run()
1430
+ },
1431
+ {
1432
+ id: "placeholder-person",
1433
+ group: "advanced",
1434
+ title: t("placeholderPerson"),
1435
+ keywords: ["somebody", "person", "owner", "\u5F85\u586B\u4EBA"],
1436
+ command: ({ editor, range }) => chainDelete(editor, range).insertPlaceholderToken("person", t("placeholderPerson")).run()
1437
+ },
1438
+ {
1439
+ id: "placeholder-date",
1440
+ group: "advanced",
1441
+ title: t("placeholderDate"),
1442
+ keywords: ["date", "due", "\u5F85\u586B\u65E5\u671F"],
1443
+ command: ({ editor, range }) => chainDelete(editor, range).insertPlaceholderToken("date", t("placeholderDate")).run()
1444
+ },
1445
+ {
1446
+ id: "history",
1447
+ group: "advanced",
1448
+ title: t("itemHistory"),
1449
+ keywords: ["history", "version", "snapshot", "\u5386\u53F2", "\u7248\u672C"],
1450
+ command: ({ editor, range }) => {
1451
+ chainDelete(editor, range).run();
1452
+ editor.emit("tessera:historyPanel", {});
1453
+ }
1454
+ }
1455
+ ];
1456
+ }
1457
+ var SlashMenu = Extension6.create({
1458
+ name: "tesseraSlashMenu",
1459
+ addOptions() {
1460
+ return {
1461
+ locale: "zh-CN",
1462
+ extraItems: void 0,
1463
+ includeAiItems: false,
1464
+ render: void 0
1465
+ };
1466
+ },
1467
+ addProseMirrorPlugins() {
1468
+ const editor = this.editor;
1469
+ const options = this.options;
1470
+ const t = createTesseraT(options.locale);
1471
+ const items = [
1472
+ ...defaultSlashItems(t),
1473
+ ...options.extraItems?.({ editor, t }) ?? []
1474
+ ];
1475
+ return [
1476
+ Suggestion({
1477
+ editor,
1478
+ pluginKey: new PluginKey3("tesseraSlashMenu"),
1479
+ char: "/",
1480
+ startOfLine: true,
1481
+ items: ({ query }) => {
1482
+ const q = query.toLowerCase();
1483
+ if (!q) {
1484
+ return items;
1485
+ }
1486
+ return items.filter(
1487
+ (item) => item.title.toLowerCase().includes(q) || item.id.toLowerCase().includes(q) || item.keywords?.some((k) => k.toLowerCase().includes(q))
1488
+ );
1489
+ },
1490
+ command: ({ editor: e, range, props }) => {
1491
+ ;
1492
+ props.command({ editor: e, range });
1493
+ },
1494
+ render: options.render
1495
+ })
1496
+ ];
1497
+ }
1498
+ });
1499
+
1500
+ // src/extensions/emoji.ts
1501
+ import { Extension as Extension7 } from "@tiptap/core";
1502
+ import { PluginKey as PluginKey4 } from "@tiptap/pm/state";
1503
+ import Suggestion2 from "@tiptap/suggestion";
1504
+ var EMOJI_ITEMS = [
1505
+ { char: "\u{1F600}", name: "grinning", keywords: ["face", "happy", "smile"] },
1506
+ { char: "\u{1F604}", name: "smile", keywords: ["face", "happy", "joy"] },
1507
+ { char: "\u{1F601}", name: "beaming", keywords: ["face", "grin"] },
1508
+ { char: "\u{1F602}", name: "joy", keywords: ["face", "tears", "lol"] },
1509
+ { char: "\u{1F923}", name: "rofl", keywords: ["face", "laugh", "floor"] },
1510
+ { char: "\u{1F60A}", name: "blush", keywords: ["face", "happy", "warm"] },
1511
+ { char: "\u{1F642}", name: "slight_smile", keywords: ["face", "smile"] },
1512
+ { char: "\u{1F609}", name: "wink", keywords: ["face"] },
1513
+ { char: "\u{1F60D}", name: "heart_eyes", keywords: ["face", "love"] },
1514
+ { char: "\u{1F618}", name: "kiss", keywords: ["face", "love"] },
1515
+ { char: "\u{1F61C}", name: "zany", keywords: ["face", "crazy", "tongue"] },
1516
+ { char: "\u{1F914}", name: "thinking", keywords: ["face", "hmm"] },
1517
+ { char: "\u{1F917}", name: "hug", keywords: ["face"] },
1518
+ { char: "\u{1F928}", name: "eyebrow", keywords: ["face", "suspicious"] },
1519
+ { char: "\u{1F610}", name: "neutral", keywords: ["face", "meh"] },
1520
+ { char: "\u{1F634}", name: "sleeping", keywords: ["face", "zzz"] },
1521
+ { char: "\u{1F62A}", name: "sleepy", keywords: ["face", "tired"] },
1522
+ { char: "\u{1F62B}", name: "tired", keywords: ["face", "exhausted"] },
1523
+ { char: "\u{1F973}", name: "partying", keywords: ["face", "celebrate"] },
1524
+ { char: "\u{1F60E}", name: "cool", keywords: ["face", "sunglasses"] },
1525
+ { char: "\u{1F913}", name: "nerd", keywords: ["face", "glasses"] },
1526
+ { char: "\u{1F62D}", name: "sob", keywords: ["face", "cry", "tears"] },
1527
+ { char: "\u{1F621}", name: "angry", keywords: ["face", "rage"] },
1528
+ { char: "\u{1F631}", name: "scream", keywords: ["face", "fear"] },
1529
+ { char: "\u{1F92F}", name: "exploding_head", keywords: ["face", "mind", "blown"] },
1530
+ { char: "\u{1F97A}", name: "pleading", keywords: ["face", "puppy"] },
1531
+ { char: "\u{1F607}", name: "innocent", keywords: ["face", "angel", "halo"] },
1532
+ { char: "\u{1F91D}", name: "handshake", keywords: ["hands", "deal"] },
1533
+ { char: "\u{1F44D}", name: "thumbsup", keywords: ["hand", "ok", "like", "yes"] },
1534
+ { char: "\u{1F44E}", name: "thumbsdown", keywords: ["hand", "dislike", "no"] },
1535
+ { char: "\u{1F44C}", name: "ok_hand", keywords: ["hand"] },
1536
+ { char: "\u270C\uFE0F", name: "victory", keywords: ["hand", "peace"] },
1537
+ { char: "\u{1F91E}", name: "crossed_fingers", keywords: ["hand", "luck"] },
1538
+ { char: "\u{1F44F}", name: "clap", keywords: ["hands", "praise"] },
1539
+ { char: "\u{1F64F}", name: "pray", keywords: ["hands", "thanks"] },
1540
+ { char: "\u{1F4AA}", name: "muscle", keywords: ["arm", "strong"] },
1541
+ { char: "\u2764\uFE0F", name: "heart", keywords: ["love", "red"] },
1542
+ { char: "\u{1F9E1}", name: "orange_heart", keywords: ["love"] },
1543
+ { char: "\u{1F49B}", name: "yellow_heart", keywords: ["love"] },
1544
+ { char: "\u{1F49A}", name: "green_heart", keywords: ["love"] },
1545
+ { char: "\u{1F499}", name: "blue_heart", keywords: ["love"] },
1546
+ { char: "\u{1F49C}", name: "purple_heart", keywords: ["love"] },
1547
+ { char: "\u{1F5A4}", name: "black_heart", keywords: ["love"] },
1548
+ { char: "\u{1F494}", name: "broken_heart", keywords: ["love", "sad"] },
1549
+ { char: "\u2B50", name: "star", keywords: ["favorite"] },
1550
+ { char: "\u{1F31F}", name: "glowing_star", keywords: ["star", "shine"] },
1551
+ { char: "\u2728", name: "sparkles", keywords: ["magic", "shine", "ai"] },
1552
+ { char: "\u{1F525}", name: "fire", keywords: ["hot", "flame"] },
1553
+ { char: "\u26A1", name: "zap", keywords: ["lightning", "fast"] },
1554
+ { char: "\u{1F4A1}", name: "bulb", keywords: ["idea", "light"] },
1555
+ { char: "\u2705", name: "check", keywords: ["done", "ok", "complete"] },
1556
+ { char: "\u274C", name: "x", keywords: ["wrong", "no", "cancel"] },
1557
+ { char: "\u26A0\uFE0F", name: "warning", keywords: ["caution", "alert"] },
1558
+ { char: "\u2753", name: "question", keywords: ["ask", "help"] },
1559
+ { char: "\u2757", name: "exclamation", keywords: ["important"] },
1560
+ { char: "\u{1F680}", name: "rocket", keywords: ["ship", "launch", "fast"] },
1561
+ { char: "\u{1F389}", name: "tada", keywords: ["party", "celebrate", "congrats"] },
1562
+ { char: "\u{1F38A}", name: "confetti", keywords: ["party"] },
1563
+ { char: "\u{1F3AF}", name: "dart", keywords: ["target", "goal"] },
1564
+ { char: "\u{1F4CC}", name: "pushpin", keywords: ["pin"] },
1565
+ { char: "\u{1F4CE}", name: "paperclip", keywords: ["attach"] },
1566
+ { char: "\u{1F4DD}", name: "memo", keywords: ["note", "doc", "write"] },
1567
+ { char: "\u{1F4C5}", name: "calendar", keywords: ["date", "schedule"] },
1568
+ { char: "\u23F0", name: "alarm", keywords: ["clock", "time"] },
1569
+ { char: "\u{1F4B0}", name: "moneybag", keywords: ["money", "cash"] },
1570
+ { char: "\u{1F381}", name: "gift", keywords: ["present"] },
1571
+ { char: "\u2615", name: "coffee", keywords: ["drink", "tea"] },
1572
+ { char: "\u{1F355}", name: "pizza", keywords: ["food"] },
1573
+ { char: "\u{1F308}", name: "rainbow", keywords: ["color"] },
1574
+ { char: "\u2600\uFE0F", name: "sunny", keywords: ["weather", "sun"] },
1575
+ { char: "\u{1F319}", name: "crescent", keywords: ["weather", "moon", "night"] },
1576
+ { char: "\u2601\uFE0F", name: "cloud", keywords: ["weather"] },
1577
+ { char: "\u{1F440}", name: "eyes", keywords: ["look", "watch"] },
1578
+ { char: "\u{1F916}", name: "robot", keywords: ["ai", "bot"] },
1579
+ { char: "\u{1F41E}", name: "bug", keywords: ["insect", "error"] }
1580
+ ];
1581
+ function filterEmojiItems(query, items = EMOJI_ITEMS) {
1582
+ const q = query.toLowerCase();
1583
+ if (!q) {
1584
+ return items;
1585
+ }
1586
+ return items.filter(
1587
+ (item) => item.name.toLowerCase().includes(q) || item.keywords.some((k) => k.toLowerCase().includes(q))
1588
+ );
1589
+ }
1590
+ var EmojiMenu = Extension7.create({
1591
+ name: "tesseraEmojiMenu",
1592
+ addOptions() {
1593
+ return {
1594
+ render: void 0,
1595
+ extraItems: void 0
1596
+ };
1597
+ },
1598
+ addProseMirrorPlugins() {
1599
+ const options = this.options;
1600
+ const items = [...EMOJI_ITEMS, ...options.extraItems?.() ?? []];
1601
+ return [
1602
+ Suggestion2({
1603
+ editor: this.editor,
1604
+ pluginKey: new PluginKey4("tesseraEmojiMenu"),
1605
+ char: ":",
1606
+ startOfLine: false,
1607
+ items: ({ query }) => filterEmojiItems(query, items),
1608
+ command: ({ editor: e, range, props }) => {
1609
+ e.chain().focus().insertContentAt(range, `${props.char} `).run();
1610
+ },
1611
+ render: options.render
1612
+ })
1613
+ ];
1614
+ }
1615
+ });
1616
+
1617
+ // src/extensions/history.ts
1618
+ import { Extension as Extension9 } from "@tiptap/core";
1619
+ import { Plugin as Plugin3, PluginKey as PluginKey5 } from "@tiptap/pm/state";
1620
+
1621
+ // src/services.ts
1622
+ import { Extension as Extension8 } from "@tiptap/core";
1623
+ var TesseraServices = Extension8.create({
1624
+ name: "tesseraServices",
1625
+ addStorage() {
1626
+ return {
1627
+ upload: void 0,
1628
+ storage: void 0,
1629
+ comments: void 0,
1630
+ identity: void 0
1631
+ };
1632
+ }
1633
+ });
1634
+ function servicesBag(editor) {
1635
+ return editor.storage.tesseraServices;
1636
+ }
1637
+ function getUploadService(editor) {
1638
+ return servicesBag(editor)?.upload;
1639
+ }
1640
+ function getStorageService(editor) {
1641
+ return servicesBag(editor)?.storage;
1642
+ }
1643
+ function getCommentStore(editor) {
1644
+ return servicesBag(editor)?.comments;
1645
+ }
1646
+ function getIdentityService(editor) {
1647
+ return servicesBag(editor)?.identity;
1648
+ }
1649
+
1650
+ // src/extensions/history.ts
1651
+ var historyKey = new PluginKey5("tesseraHistory");
1652
+ var TesseraHistory = Extension9.create({
1653
+ name: "tesseraHistory",
1654
+ addOptions() {
1655
+ return {
1656
+ idleMs: 5 * 60 * 1e3,
1657
+ minIntervalMs: 60 * 1e3,
1658
+ label: void 0
1659
+ };
1660
+ },
1661
+ addCommands() {
1662
+ return {
1663
+ captureSnapshot: (label) => ({ editor, state }) => {
1664
+ const storage = getStorageService(editor);
1665
+ if (!storage) {
1666
+ return false;
1667
+ }
1668
+ const doc = editor.getJSON();
1669
+ const snapshot = {
1670
+ id: `snap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
1671
+ ts: Date.now(),
1672
+ doc,
1673
+ label: label ?? this.options.label?.()
1674
+ };
1675
+ void storage.saveSnapshot(snapshot).then(() => {
1676
+ editor.emit("tessera:snapshotSaved", { snapshot });
1677
+ });
1678
+ const tr = state.tr.setMeta(historyKey, { type: "captured", ts: snapshot.ts });
1679
+ editor.view.dispatch(tr);
1680
+ return true;
1681
+ }
1682
+ };
1683
+ },
1684
+ addProseMirrorPlugins() {
1685
+ const editor = this.editor;
1686
+ const options = this.options;
1687
+ return [
1688
+ new Plugin3({
1689
+ key: historyKey,
1690
+ state: {
1691
+ init: () => ({ lastChange: 0, lastCapture: 0, timer: null }),
1692
+ apply: (tr, prev) => {
1693
+ const meta = tr.getMeta(historyKey);
1694
+ if (meta?.type === "captured") {
1695
+ return { ...prev, lastCapture: meta.ts ?? Date.now() };
1696
+ }
1697
+ if (!tr.docChanged) {
1698
+ return prev;
1699
+ }
1700
+ return { ...prev, lastChange: Date.now() };
1701
+ }
1702
+ },
1703
+ view() {
1704
+ return {
1705
+ update: (_view, prevState) => {
1706
+ const before = historyKey.getState(prevState);
1707
+ const after = historyKey.getState(editor.state);
1708
+ if (!before || !after || after.lastChange === before.lastChange) {
1709
+ return;
1710
+ }
1711
+ const storage = getStorageService(editor);
1712
+ if (!storage) {
1713
+ return;
1714
+ }
1715
+ const s = historyKey.getState(editor.state);
1716
+ if (s?.timer) {
1717
+ clearTimeout(s.timer);
1718
+ }
1719
+ const timer = setTimeout(() => {
1720
+ const now = Date.now();
1721
+ const cur = historyKey.getState(editor.state);
1722
+ if (!cur || now - cur.lastChange < options.idleMs - 50) {
1723
+ return;
1724
+ }
1725
+ if (now - cur.lastCapture < options.minIntervalMs) {
1726
+ return;
1727
+ }
1728
+ editor.commands.captureSnapshot();
1729
+ }, options.idleMs);
1730
+ const st = historyKey.getState(editor.state);
1731
+ if (st) {
1732
+ st.timer = timer;
1733
+ }
1734
+ },
1735
+ destroy() {
1736
+ const s = historyKey.getState(editor.state);
1737
+ if (s?.timer) {
1738
+ clearTimeout(s.timer);
1739
+ }
1740
+ }
1741
+ };
1742
+ }
1743
+ })
1744
+ ];
1745
+ }
1746
+ });
1747
+
1748
+ // src/extensions/context-menu.ts
1749
+ import { Extension as Extension10 } from "@tiptap/core";
1750
+ import { Plugin as Plugin4 } from "@tiptap/pm/state";
1751
+ var BlockContextMenu = Extension10.create({
1752
+ name: "tesseraBlockMenu",
1753
+ addProseMirrorPlugins() {
1754
+ const editor = this.editor;
1755
+ return [
1756
+ new Plugin4({
1757
+ props: {
1758
+ handleDOMEvents: {
1759
+ contextmenu: (view, event) => {
1760
+ const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
1761
+ if (!coords) {
1762
+ return false;
1763
+ }
1764
+ const $pos = view.state.doc.resolve(coords.pos);
1765
+ for (let depth = $pos.depth; depth >= 1; depth--) {
1766
+ const node = $pos.node(depth);
1767
+ if (typeof node.attrs.id === "string") {
1768
+ editor.emit("tessera:blockMenu", {
1769
+ blockId: node.attrs.id,
1770
+ blockType: node.type.name,
1771
+ clientX: event.clientX,
1772
+ clientY: event.clientY
1773
+ });
1774
+ event.preventDefault();
1775
+ return true;
1776
+ }
1777
+ }
1778
+ return false;
1779
+ }
1780
+ }
1781
+ }
1782
+ })
1783
+ ];
1784
+ }
1785
+ });
1786
+
1787
+ // src/extensions/gallery.ts
1788
+ import { Extension as Extension11 } from "@tiptap/core";
1789
+ import { Plugin as Plugin5, PluginKey as PluginKey6 } from "@tiptap/pm/state";
1790
+ import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
1791
+ var galleryKey = new PluginKey6("tesseraGallery");
1792
+ function findGalleryRuns(doc) {
1793
+ const runs = [];
1794
+ let start = -1;
1795
+ let size = 0;
1796
+ doc.forEach((node, offset) => {
1797
+ if (node.type.name === "imageBlock") {
1798
+ if (start < 0) {
1799
+ start = offset;
1800
+ size = 0;
1801
+ }
1802
+ size += 1;
1803
+ } else if (start >= 0) {
1804
+ if (size >= 2) {
1805
+ runs.push({ from: start, to: offset, size });
1806
+ }
1807
+ start = -1;
1808
+ size = 0;
1809
+ }
1810
+ });
1811
+ if (start >= 0 && size >= 2) {
1812
+ runs.push({ from: start, to: doc.content.size, size });
1813
+ }
1814
+ return runs;
1815
+ }
1816
+ function galleryDecorations(doc) {
1817
+ const decorations = [];
1818
+ for (const run of findGalleryRuns(doc)) {
1819
+ let index = 0;
1820
+ doc.nodesBetween(run.from, run.to, (node, pos) => {
1821
+ if (node.type.name !== "imageBlock") {
1822
+ return false;
1823
+ }
1824
+ const attrs = {
1825
+ "data-gallery": "true",
1826
+ "data-gallery-index": String(index),
1827
+ "data-gallery-size": String(run.size)
1828
+ };
1829
+ decorations.push(Decoration2.node(pos, pos + node.nodeSize, attrs));
1830
+ index += 1;
1831
+ return false;
1832
+ });
1833
+ }
1834
+ return DecorationSet2.create(doc, decorations);
1835
+ }
1836
+ var TesseraGallery = Extension11.create({
1837
+ name: "tesseraGallery",
1838
+ addProseMirrorPlugins() {
1839
+ return [
1840
+ new Plugin5({
1841
+ key: galleryKey,
1842
+ state: {
1843
+ init: (_config, state) => galleryDecorations(state.doc),
1844
+ apply: (tr, old) => tr.docChanged ? galleryDecorations(tr.doc) : old
1845
+ },
1846
+ props: {
1847
+ decorations(state) {
1848
+ return galleryKey.getState(state);
1849
+ }
1850
+ }
1851
+ })
1852
+ ];
1853
+ }
1854
+ });
1855
+
1856
+ // src/extensions/metrics.ts
1857
+ import { Extension as Extension12 } from "@tiptap/core";
1858
+
1859
+ // src/markdown.ts
1860
+ import MarkdownIt from "markdown-it";
1861
+ import { DOMParser as PMDOMParser, DOMSerializer } from "@tiptap/pm/model";
1862
+ import { MarkdownSerializer, defaultMarkdownSerializer } from "prosemirror-markdown";
1863
+ function taskListPlugin(md2) {
1864
+ md2.core.ruler.after("inline", "tessera-tasklist", (state) => {
1865
+ const tokens = state.tokens;
1866
+ const taskItems = /* @__PURE__ */ new Set();
1867
+ for (let i = 0; i < tokens.length; i++) {
1868
+ if (tokens[i].type !== "inline") continue;
1869
+ let p = -1;
1870
+ if (tokens[i - 1]?.type === "paragraph_open") p = i - 2;
1871
+ if (p === -1 || tokens[p]?.type !== "list_item_open") continue;
1872
+ const children = tokens[i].children;
1873
+ if (!children || children.length === 0) continue;
1874
+ const first = children[0];
1875
+ const m = /^\[([ xX])\]\s+/.exec(first.content ?? "");
1876
+ if (!m) continue;
1877
+ first.content = (first.content ?? "").slice(m[0].length);
1878
+ for (let j = 1; j < children.length; j++) {
1879
+ const child = children[j];
1880
+ child.content = (child.content ?? "").replace(/^\[([ xX])\]\s+/, "");
1881
+ }
1882
+ tokens[p].attrSet?.("data-type", "taskItem");
1883
+ tokens[p].attrSet?.("data-checked", m[1] === " " ? "false" : "true");
1884
+ taskItems.add(p);
1885
+ }
1886
+ for (let i = 0; i < tokens.length; i++) {
1887
+ if (tokens[i].type !== "bullet_list_open") continue;
1888
+ for (let j = i + 1; j < tokens.length; j++) {
1889
+ if (tokens[j].type === "bullet_list_close") break;
1890
+ if (tokens[j].type === "list_item_open" && taskItems.has(j)) {
1891
+ tokens[i].attrSet?.("data-type", "taskList");
1892
+ break;
1893
+ }
1894
+ }
1895
+ }
1896
+ });
1897
+ }
1898
+ var md = MarkdownIt({ html: true, linkify: true }).use(
1899
+ // structural mismatch is only in the type layer (markdown-it Token typings)
1900
+ taskListPlugin
1901
+ );
1902
+ function stripIds(json) {
1903
+ const walk = (node) => {
1904
+ const attrs = node.attrs && "id" in node.attrs ? Object.fromEntries(Object.entries(node.attrs).filter(([k]) => k !== "id")) : node.attrs;
1905
+ return {
1906
+ ...node,
1907
+ attrs,
1908
+ content: node.content?.map(walk)
1909
+ };
1910
+ };
1911
+ return walk(json);
1912
+ }
1913
+ function nodeToHtml(node) {
1914
+ const clean = node.type.schema.nodeFromJSON(stripIds(node.toJSON()));
1915
+ const dom = DOMSerializer.fromSchema(node.type.schema).serializeNode(clean);
1916
+ return dom.outerHTML;
1917
+ }
1918
+ function createMarkdownSerializer(schema) {
1919
+ const nodes = {
1920
+ ...defaultMarkdownSerializer.nodes
1921
+ };
1922
+ const rep = (s, n) => " ".repeat(n);
1923
+ nodes.bulletList = (state, node) => {
1924
+ state.renderList(node, " ", () => "- ");
1925
+ state.closeBlock(node);
1926
+ };
1927
+ nodes.orderedList = (state, node) => {
1928
+ const start = Number(node.attrs.order ?? 1);
1929
+ const maxW = String(start + node.childCount - 1).length;
1930
+ const space = rep(" ", maxW + 2);
1931
+ state.renderList(node, space, (i) => {
1932
+ const n = String(start + i);
1933
+ return rep(" ", maxW - n.length) + n + ". ";
1934
+ });
1935
+ state.closeBlock(node);
1936
+ };
1937
+ nodes.listItem = (state, node) => {
1938
+ state.renderContent(node);
1939
+ };
1940
+ nodes.codeBlock = (state, node) => {
1941
+ state.write("```" + (node.attrs.language ?? "") + "\n");
1942
+ state.write(node.textContent);
1943
+ state.write("\n```");
1944
+ state.closeBlock(node);
1945
+ };
1946
+ nodes.horizontalRule = (state, node) => {
1947
+ state.write("---");
1948
+ state.closeBlock(node);
1949
+ };
1950
+ nodes.hardBreak = (state) => {
1951
+ state.write("\\\n");
1952
+ };
1953
+ nodes.taskList = (state, node) => {
1954
+ state.renderList(node, " ", () => "- ");
1955
+ state.closeBlock(node);
1956
+ };
1957
+ nodes.taskItem = (state, node) => {
1958
+ state.write(node.attrs.checked ? "[x] " : "[ ] ");
1959
+ state.renderContent(node);
1960
+ };
1961
+ nodes.hint = (state, node) => {
1962
+ state.write(nodeToHtml(node));
1963
+ state.closeBlock(node);
1964
+ };
1965
+ nodes.collapsible = (state, node) => {
1966
+ state.write(nodeToHtml(node));
1967
+ state.closeBlock(node);
1968
+ };
1969
+ nodes.imageBlock = (state, node) => {
1970
+ state.write(nodeToHtml(node));
1971
+ state.closeBlock(node);
1972
+ };
1973
+ nodes.table = (state, node) => {
1974
+ state.write(nodeToHtml(node));
1975
+ state.closeBlock(node);
1976
+ };
1977
+ nodes.tableRow = () => {
1978
+ };
1979
+ nodes.tableCell = () => {
1980
+ };
1981
+ nodes.tableHeader = () => {
1982
+ };
1983
+ nodes.embedBlock = (state, node) => {
1984
+ state.write(nodeToHtml(node));
1985
+ state.closeBlock(node);
1986
+ };
1987
+ nodes.tocBlock = (state, node) => {
1988
+ state.write('<div data-type="tessera-toc"></div>');
1989
+ state.closeBlock(node);
1990
+ };
1991
+ const marks = {
1992
+ ...defaultMarkdownSerializer.marks,
1993
+ bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
1994
+ italic: { open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true },
1995
+ strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
1996
+ code: { open: "`", close: "`", escape: false },
1997
+ link: {
1998
+ open: "[",
1999
+ close: (state, mark) => `](${mark.attrs.href})`,
2000
+ mixable: false
2001
+ },
2002
+ underline: { open: "<u>", close: "</u>", mixable: true, expelEnclosingWhitespace: true },
2003
+ highlight: { open: "<mark>", close: "</mark>", mixable: true, expelEnclosingWhitespace: true },
2004
+ // transparent passthroughs: these marks carry no Markdown syntax.
2005
+ // Omitting them makes prosemirror-markdown THROW on export (crashes the
2006
+ // host app) — textStyle always accompanies Color, and aiAttribution rides
2007
+ // on AI-written text.
2008
+ textStyle: { open: "", close: "", mixable: true },
2009
+ color: { open: "", close: "", mixable: true },
2010
+ aiAttribution: { open: "", close: "", mixable: true },
2011
+ tesseraPlaceholder: { open: "", close: "", mixable: true },
2012
+ comment: { open: "", close: "", mixable: true }
2013
+ };
2014
+ return new MarkdownSerializer(nodes, marks);
2015
+ }
2016
+ function docToMarkdown(doc, schema) {
2017
+ const isNode = typeof doc.nodeSize === "number" && typeof doc.type === "object";
2018
+ const realDoc = isNode ? doc : schema.nodeFromJSON(doc);
2019
+ return createMarkdownSerializer(schema).serialize(realDoc);
2020
+ }
2021
+ function markdownToDoc(markdown, schema) {
2022
+ const html = md.render(markdown);
2023
+ const container = document.createElement("div");
2024
+ container.innerHTML = html;
2025
+ const parsed = PMDOMParser.fromSchema(schema).parse(container);
2026
+ return parsed.toJSON();
2027
+ }
2028
+ function stableJson(json) {
2029
+ const clone = JSON.parse(JSON.stringify(json));
2030
+ const walk = (node) => {
2031
+ if (node.attrs && "id" in node.attrs) {
2032
+ const { id: _drop, ...rest } = node.attrs;
2033
+ if (Object.keys(rest).length === 0) {
2034
+ delete node.attrs;
2035
+ } else {
2036
+ node.attrs = rest;
2037
+ }
2038
+ }
2039
+ node.marks = node.marks?.map((mark) => {
2040
+ if (mark.attrs) {
2041
+ const attrs = Object.fromEntries(
2042
+ Object.entries(mark.attrs).filter(([, v]) => v !== null && v !== "")
2043
+ );
2044
+ return { ...mark, attrs };
2045
+ }
2046
+ return mark;
2047
+ });
2048
+ if (node.type === "codeBlock" && node.content) {
2049
+ node.content = node.content.map(
2050
+ (child) => child.type === "text" ? { ...child, text: (child.text ?? "").replace(/\s+$/, "") } : child
2051
+ );
2052
+ node.content = node.content.filter((child) => !(child.type === "text" && child.text === ""));
2053
+ }
2054
+ node.content?.forEach(walk);
2055
+ };
2056
+ walk(clone);
2057
+ return clone;
2058
+ }
2059
+
2060
+ // src/extensions/metrics.ts
2061
+ function measureTesseraMetrics(editor) {
2062
+ let images = 0;
2063
+ let tables = 0;
2064
+ editor.state.doc.descendants((node) => {
2065
+ if (node.type.name === "imageBlock") images += 1;
2066
+ if (node.type.name === "table") tables += 1;
2067
+ return true;
2068
+ });
2069
+ const started = performance.now();
2070
+ docToMarkdown(editor.state.doc, editor.state.schema);
2071
+ const serializeMs = Math.round((performance.now() - started) * 100) / 100;
2072
+ return {
2073
+ blocks: editor.state.doc.childCount,
2074
+ words: editor.state.doc.textBetween(0, editor.state.doc.content.size, " ", " ").split(/\s+/).filter(Boolean).length,
2075
+ images,
2076
+ tables,
2077
+ mountedNodeViews: editor.view ? editor.view.dom.querySelectorAll("[data-node-view-wrapper]").length : 0,
2078
+ serializeMs
2079
+ };
2080
+ }
2081
+ var TesseraMetrics = Extension12.create({
2082
+ name: "tesseraMetrics"
2083
+ });
2084
+ function getTesseraMetrics(editor) {
2085
+ return measureTesseraMetrics(editor);
2086
+ }
2087
+
2088
+ // src/extensions/word-paste.ts
2089
+ import { Extension as Extension13 } from "@tiptap/core";
2090
+ import { Plugin as Plugin6, PluginKey as PluginKey7 } from "@tiptap/pm/state";
2091
+
2092
+ // src/wordpaste.ts
2093
+ function isWordHtml(html) {
2094
+ return html.includes("urn:schemas-microsoft-com:office:word") || html.includes("urn:schemas-microsoft-com:office:office") || /mso-[\w-]+/.test(html) || /\bMso\w+/.test(html) || /<w:/i.test(html) || /<o:/i.test(html);
2095
+ }
2096
+ function cleanWordHtml(html) {
2097
+ let out = html;
2098
+ out = out.replace(/<!--\[if[\s\S]*?<!\[endif\]-->/gi, "");
2099
+ out = out.replace(/<!--[\s\S]*?-->/g, "");
2100
+ out = out.replace(/<(style|script)\b[\s\S]*?<\/\1>/gi, "");
2101
+ out = out.replace(/<(meta|link)\b[^>]*>/gi, "");
2102
+ out = out.replace(/<\?xml[^>]*\?>/gi, "");
2103
+ out = out.replace(/<!DOCTYPE[^>]*>/gi, "");
2104
+ out = out.replace(/<\/?[a-z][a-z0-9]*:[^>]*>/gi, "");
2105
+ out = out.replace(/\s(class|style|lang|xml:lang|face)\s*=\s*"[^"]*"/gi, "");
2106
+ out = out.replace(/\s(class|style|lang|xml:lang|face)\s*=\s*'[^']*'/gi, "");
2107
+ out = out.replace(/&nbsp;/gi, " ");
2108
+ let prev = "";
2109
+ while (out !== prev) {
2110
+ prev = out;
2111
+ out = out.replace(/<p\b[^>]*>(\s|<br\s*\/?>)*<\/p>/gi, "");
2112
+ }
2113
+ return out.trim();
2114
+ }
2115
+
2116
+ // src/extensions/word-paste.ts
2117
+ var TesseraWordPaste = Extension13.create({
2118
+ name: "tesseraWordPaste",
2119
+ addProseMirrorPlugins() {
2120
+ return [
2121
+ new Plugin6({
2122
+ key: new PluginKey7("tesseraWordPaste"),
2123
+ props: {
2124
+ transformPastedHTML(html) {
2125
+ return isWordHtml(html) ? cleanWordHtml(html) : html;
2126
+ }
2127
+ }
2128
+ })
2129
+ ];
2130
+ }
2131
+ });
2132
+
2133
+ // src/blockmenu.ts
2134
+ function findBlockPosById(editor, id) {
2135
+ let found = null;
2136
+ editor.state.doc.forEach((node, offset) => {
2137
+ if (found === null && node.attrs.id === id) {
2138
+ found = offset;
2139
+ }
2140
+ });
2141
+ return found;
2142
+ }
2143
+ function deleteBlockById(editor, id) {
2144
+ const pos = findBlockPosById(editor, id);
2145
+ if (pos === null) {
2146
+ return false;
2147
+ }
2148
+ const node = editor.state.doc.nodeAt(pos);
2149
+ if (!node) {
2150
+ return false;
2151
+ }
2152
+ const tr = editor.state.tr.delete(pos, pos + node.nodeSize);
2153
+ editor.view.dispatch(tr);
2154
+ return true;
2155
+ }
2156
+ function blockAnchorUrl(blockId2) {
2157
+ return `${location.origin}${location.pathname}#block-${blockId2}`;
2158
+ }
2159
+
2160
+ // src/preset.ts
2161
+ import StarterKit from "@tiptap/starter-kit";
2162
+ import { TaskList, TaskItem } from "@tiptap/extension-list";
2163
+ import { TextStyle } from "@tiptap/extension-text-style";
2164
+ import { Color } from "@tiptap/extension-color";
2165
+ import { Highlight } from "@tiptap/extension-highlight";
2166
+ import { UniqueID } from "@tiptap/extension-unique-id";
2167
+ import { Placeholder } from "@tiptap/extensions";
2168
+ var ID_BLOCK_TYPES = [
2169
+ "paragraph",
2170
+ "heading",
2171
+ "bulletList",
2172
+ "orderedList",
2173
+ "taskList",
2174
+ "listItem",
2175
+ "taskItem",
2176
+ "blockquote",
2177
+ "codeBlock",
2178
+ "horizontalRule",
2179
+ "hint",
2180
+ "collapsible",
2181
+ "imageBlock",
2182
+ "table",
2183
+ "tableRow",
2184
+ "embedBlock",
2185
+ "tocBlock"
2186
+ ];
2187
+ function createTesseraExtensions(options = {}) {
2188
+ const t = createTesseraT(options.locale ?? "zh-CN");
2189
+ return [
2190
+ StarterKit.configure({
2191
+ heading: { levels: [1, 2, 3, 4] },
2192
+ link: {
2193
+ openOnClick: false,
2194
+ autolink: true,
2195
+ defaultProtocol: "https"
2196
+ }
2197
+ // undoRedo keeps defaults (newGroupDelay 500ms): streaming AI chunks
2198
+ // arriving faster than that already merge into one undo step.
2199
+ }),
2200
+ TextStyle,
2201
+ Color,
2202
+ Highlight.configure({ multicolor: true }),
2203
+ TaskList,
2204
+ TaskItem.configure({ nested: true }),
2205
+ Hint,
2206
+ Collapsible,
2207
+ CollapsibleSummary,
2208
+ CollapsibleContent,
2209
+ ImageBlock,
2210
+ AiTable,
2211
+ TableRow,
2212
+ AiTableCell,
2213
+ AiTableHeader,
2214
+ EmbedBlock,
2215
+ TocBlock,
2216
+ AiAttribution,
2217
+ CommentMark,
2218
+ CommentCommands,
2219
+ PlaceholderMark,
2220
+ PlaceholderCommands,
2221
+ UniqueID.configure({
2222
+ types: ID_BLOCK_TYPES,
2223
+ attributeName: "id"
2224
+ }),
2225
+ Placeholder.configure({
2226
+ placeholder: ({ node }) => node.type.name === "paragraph" ? t("placeholderEmpty") : "",
2227
+ showOnlyWhenEditable: true
2228
+ }),
2229
+ TesseraInputRules,
2230
+ TesseraShortcuts,
2231
+ TesseraFindReplace,
2232
+ TesseraHistory.configure({ idleMs: options.historyIdleMs }),
2233
+ BlockContextMenu,
2234
+ SlashMenu.configure({ locale: options.locale ?? "zh-CN" }),
2235
+ EmojiMenu,
2236
+ TesseraGallery,
2237
+ TesseraMetrics,
2238
+ TesseraWordPaste,
2239
+ TesseraServices
2240
+ ];
2241
+ }
2242
+
2243
+ // src/diff.ts
2244
+ function topLevel(doc) {
2245
+ return doc.content ?? [];
2246
+ }
2247
+ function blockId(block) {
2248
+ const id = block.attrs?.id;
2249
+ return typeof id === "string" ? id : null;
2250
+ }
2251
+ function collectText(node) {
2252
+ let text = "";
2253
+ if (node.text) {
2254
+ text += node.text;
2255
+ }
2256
+ for (const child of node.content ?? []) {
2257
+ text += collectText(child);
2258
+ }
2259
+ return text;
2260
+ }
2261
+ function tokenize(text) {
2262
+ return text.match(/[\u4e00-\u9fa5]|[a-zA-Z0-9]+|\s+|[^\sa-zA-Z0-9\u4e00-\u9fa5]/g) ?? [];
2263
+ }
2264
+ function wordDiff(beforeText, afterText) {
2265
+ const a = tokenize(beforeText);
2266
+ const b = tokenize(afterText);
2267
+ if (a.length * b.length > 4e6) {
2268
+ return [
2269
+ { text: beforeText, type: "del" },
2270
+ { text: afterText, type: "add" }
2271
+ ];
2272
+ }
2273
+ const dp = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
2274
+ for (let i2 = a.length - 1; i2 >= 0; i2--) {
2275
+ for (let j2 = b.length - 1; j2 >= 0; j2--) {
2276
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
2277
+ }
2278
+ }
2279
+ const parts = [];
2280
+ const push = (text, type) => {
2281
+ const last = parts[parts.length - 1];
2282
+ if (last && last.type === type) {
2283
+ last.text += text;
2284
+ } else {
2285
+ parts.push({ text, type });
2286
+ }
2287
+ };
2288
+ let i = 0;
2289
+ let j = 0;
2290
+ while (i < a.length && j < b.length) {
2291
+ if (a[i] === b[j]) {
2292
+ push(a[i], "same");
2293
+ i++;
2294
+ j++;
2295
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
2296
+ push(a[i], "del");
2297
+ i++;
2298
+ } else {
2299
+ push(b[j], "add");
2300
+ j++;
2301
+ }
2302
+ }
2303
+ while (i < a.length) {
2304
+ push(a[i++], "del");
2305
+ }
2306
+ while (j < b.length) {
2307
+ push(b[j++], "add");
2308
+ }
2309
+ return parts;
2310
+ }
2311
+ function sameBlock(a, b) {
2312
+ return JSON.stringify(a) === JSON.stringify(b);
2313
+ }
2314
+ function diffDocs(before, after) {
2315
+ const beforeBlocks = topLevel(before);
2316
+ const afterBlocks = topLevel(after);
2317
+ const afterById = /* @__PURE__ */ new Map();
2318
+ for (const block of afterBlocks) {
2319
+ const id = blockId(block);
2320
+ if (id) {
2321
+ afterById.set(id, block);
2322
+ }
2323
+ }
2324
+ const seen = /* @__PURE__ */ new Set();
2325
+ const entries = [];
2326
+ for (const block of beforeBlocks) {
2327
+ const id = blockId(block);
2328
+ if (id && afterById.has(id)) {
2329
+ seen.add(id);
2330
+ const next = afterById.get(id);
2331
+ if (sameBlock(block, next)) {
2332
+ entries.push({ kind: "unchanged", id, before: block, after: next });
2333
+ } else {
2334
+ entries.push({ kind: "changed", id, before: block, after: next, wordDiff: wordDiff(collectText(block), collectText(next)) });
2335
+ }
2336
+ } else {
2337
+ entries.push({ kind: "removed", id: id ?? void 0, before: block });
2338
+ }
2339
+ }
2340
+ for (const block of afterBlocks) {
2341
+ const id = blockId(block);
2342
+ if (id && seen.has(id)) {
2343
+ continue;
2344
+ }
2345
+ if (!id || !beforeBlocks.some((b) => blockId(b) === id)) {
2346
+ entries.push({ kind: "added", id: id ?? void 0, after: block });
2347
+ }
2348
+ }
2349
+ return entries;
2350
+ }
2351
+ function diffSummary(entries) {
2352
+ let added = 0;
2353
+ let removed = 0;
2354
+ let changed = 0;
2355
+ for (const entry of entries) {
2356
+ if (entry.kind === "added") added++;
2357
+ else if (entry.kind === "removed") removed++;
2358
+ else if (entry.kind === "changed") changed++;
2359
+ }
2360
+ return { added, removed, changed };
2361
+ }
2362
+
2363
+ // src/writeback.ts
2364
+ function getTopLevelBlocks(editor) {
2365
+ const blocks = [];
2366
+ editor.state.doc.forEach((node, offset) => {
2367
+ blocks.push({ id: node.attrs.id ?? null, type: node.type.name, pos: offset, node });
2368
+ });
2369
+ return blocks;
2370
+ }
2371
+ function findBlockPosById2(editor, id) {
2372
+ let found = null;
2373
+ editor.state.doc.forEach((node, offset) => {
2374
+ if (found === null && node.attrs.id === id) {
2375
+ found = offset;
2376
+ }
2377
+ });
2378
+ return found;
2379
+ }
2380
+ function newBlockId() {
2381
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
2382
+ return crypto.randomUUID();
2383
+ }
2384
+ return `tessera-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
2385
+ }
2386
+ function resolveBlocks(editor, blocks) {
2387
+ return blocks.map((json) => {
2388
+ const clean = { ...json };
2389
+ clean.attrs = { ...clean.attrs ?? {}, id: newBlockId() };
2390
+ return editor.state.schema.nodeFromJSON(clean);
2391
+ });
2392
+ }
2393
+ function modifyRange(editor, options) {
2394
+ const blocks = getTopLevelBlocks(editor).filter((b) => b.id);
2395
+ if (blocks.length === 0) {
2396
+ return false;
2397
+ }
2398
+ const ids = blocks.map((b) => b.id);
2399
+ const fromId = options.fromId ?? ids[0];
2400
+ const toId = options.toId ?? fromId;
2401
+ let fromIndex = ids.indexOf(fromId);
2402
+ let toIndex = ids.indexOf(toId);
2403
+ if (fromIndex === -1 || toIndex === -1) {
2404
+ return false;
2405
+ }
2406
+ if (fromIndex > toIndex) {
2407
+ ;
2408
+ [fromIndex, toIndex] = [toIndex, fromIndex];
2409
+ }
2410
+ const fromBlock = blocks[fromIndex];
2411
+ const toBlock = blocks[toIndex];
2412
+ const from = fromBlock.pos;
2413
+ const to = toBlock.pos + toBlock.node.nodeSize;
2414
+ const nodes = resolveBlocks(editor, options.content);
2415
+ const tr = editor.state.tr;
2416
+ tr.replaceWith(from, to, nodes);
2417
+ editor.view.dispatch(tr);
2418
+ return true;
2419
+ }
2420
+ function appendBlocks(editor, options) {
2421
+ const nodes = resolveBlocks(editor, options.content);
2422
+ if (nodes.length === 0) {
2423
+ return false;
2424
+ }
2425
+ let insertPos = editor.state.doc.content.size;
2426
+ if (options.afterId) {
2427
+ const pos = findBlockPosById2(editor, options.afterId);
2428
+ if (pos === null) {
2429
+ return false;
2430
+ }
2431
+ const node = editor.state.doc.nodeAt(pos);
2432
+ if (!node) {
2433
+ return false;
2434
+ }
2435
+ insertPos = pos + node.nodeSize;
2436
+ }
2437
+ const tr = editor.state.tr;
2438
+ tr.insert(insertPos, nodes);
2439
+ editor.view.dispatch(tr);
2440
+ return true;
2441
+ }
2442
+ function removeBlocks(editor, ids) {
2443
+ if (ids.length === 0) {
2444
+ return false;
2445
+ }
2446
+ const ranges = [];
2447
+ editor.state.doc.forEach((node, offset) => {
2448
+ if (node.attrs.id && ids.includes(node.attrs.id)) {
2449
+ ranges.push({ from: offset, to: offset + node.nodeSize });
2450
+ }
2451
+ });
2452
+ if (ranges.length === 0) {
2453
+ return false;
2454
+ }
2455
+ const tr = editor.state.tr;
2456
+ for (let i = ranges.length - 1; i >= 0; i--) {
2457
+ tr.delete(ranges[i].from, ranges[i].to);
2458
+ }
2459
+ editor.view.dispatch(tr);
2460
+ return true;
2461
+ }
2462
+ function getBlockJson(editor, id) {
2463
+ const pos = findBlockPosById2(editor, id);
2464
+ if (pos === null) {
2465
+ return null;
2466
+ }
2467
+ const node = editor.state.doc.nodeAt(pos);
2468
+ return node ? node.toJSON() : null;
2469
+ }
2470
+ export {
2471
+ AiAttribution,
2472
+ AiTable,
2473
+ AiTableCell,
2474
+ AiTableHeader,
2475
+ TableRow as AiTableRow,
2476
+ BlockContextMenu,
2477
+ Collapsible,
2478
+ CollapsibleContent,
2479
+ CollapsibleSummary,
2480
+ CommentCommands,
2481
+ CommentMark,
2482
+ EMOJI_ITEMS,
2483
+ EmbedBlock,
2484
+ EmojiMenu,
2485
+ Hint,
2486
+ ID_BLOCK_TYPES,
2487
+ ImageBlock,
2488
+ PlaceholderCommands,
2489
+ PlaceholderMark,
2490
+ SlashMenu,
2491
+ TABLE_COLUMN_KINDS,
2492
+ TesseraFindReplace,
2493
+ TesseraGallery,
2494
+ TesseraHistory,
2495
+ TesseraInputRules,
2496
+ TesseraMetrics,
2497
+ TesseraServices,
2498
+ TesseraShortcuts,
2499
+ TesseraWordPaste,
2500
+ TocBlock,
2501
+ appendBlocks,
2502
+ blockAnchorUrl,
2503
+ cleanWordHtml,
2504
+ createMarkdownSerializer,
2505
+ createTesseraExtensions,
2506
+ createTesseraT,
2507
+ defaultSlashItems,
2508
+ deleteBlockById,
2509
+ diffDocs,
2510
+ diffSummary,
2511
+ docToMarkdown,
2512
+ filterEmojiItems,
2513
+ findBlockPosById,
2514
+ findGalleryRuns,
2515
+ findReplaceKey,
2516
+ galleryKey,
2517
+ getBlockJson,
2518
+ getCommentStore,
2519
+ getIdentityService,
2520
+ getStorageService,
2521
+ getTesseraMetrics,
2522
+ getTopLevelBlocks,
2523
+ getUploadService,
2524
+ historyKey,
2525
+ isWordHtml,
2526
+ listCommentRanges,
2527
+ markdownToDoc,
2528
+ measureTesseraMetrics,
2529
+ modifyRange,
2530
+ normalizeTypes,
2531
+ removeBlocks,
2532
+ stableJson,
2533
+ tableNodeToCsv,
2534
+ tableToCsvAt,
2535
+ tesseraMessages,
2536
+ wordDiff
2537
+ };
2538
+ //# sourceMappingURL=index.js.map