@xenosystem/blocks 0.20.0 → 0.21.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.
@@ -0,0 +1,688 @@
1
+ import {
2
+ DOCUMENT_MARK_TYPES,
3
+ DOCUMENT_NODE_TYPES,
4
+ SAFE_IMAGE,
5
+ SAFE_LINK,
6
+ XdocError,
7
+ emptyContent,
8
+ nodeTypesIn,
9
+ pageLinksIn,
10
+ parseXdoc,
11
+ plainText,
12
+ serializeXdoc,
13
+ validateContent,
14
+ wordCount
15
+ } from "./chunk-NFOBZJDF.js";
16
+
17
+ // src/docs/document/extensions/Callout.ts
18
+ import { Node, mergeAttributes } from "@tiptap/core";
19
+ var Callout = Node.create({
20
+ name: "callout",
21
+ group: "block",
22
+ content: "block+",
23
+ defining: true,
24
+ addAttributes() {
25
+ return {
26
+ type: {
27
+ default: "info",
28
+ parseHTML: (element) => element.getAttribute("data-callout-type") || "info",
29
+ renderHTML: (attributes) => ({
30
+ "data-callout-type": attributes.type
31
+ })
32
+ }
33
+ };
34
+ },
35
+ parseHTML() {
36
+ return [
37
+ { tag: "div[data-callout-type]" },
38
+ { tag: "div.callout" }
39
+ ];
40
+ },
41
+ renderHTML({ HTMLAttributes }) {
42
+ const type = HTMLAttributes["data-callout-type"] || "info";
43
+ return [
44
+ "div",
45
+ mergeAttributes(HTMLAttributes, {
46
+ class: `callout ${type}`,
47
+ "data-callout-type": type
48
+ }),
49
+ 0
50
+ ];
51
+ },
52
+ addCommands() {
53
+ return {
54
+ setCallout: (attrs) => ({ commands }) => {
55
+ return commands.wrapIn(this.name, attrs);
56
+ },
57
+ toggleCallout: (attrs) => ({ commands }) => {
58
+ return commands.toggleWrap(this.name, attrs);
59
+ }
60
+ };
61
+ }
62
+ });
63
+
64
+ // src/docs/document/extensions/ToggleBlock.ts
65
+ import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
66
+ var ToggleBlock = Node2.create({
67
+ name: "toggleBlock",
68
+ group: "block",
69
+ content: "block+",
70
+ defining: true,
71
+ addAttributes() {
72
+ return {
73
+ open: {
74
+ default: true,
75
+ parseHTML: (element) => element.getAttribute("data-open") !== "false",
76
+ renderHTML: (attributes) => ({
77
+ "data-open": String(attributes.open)
78
+ })
79
+ },
80
+ summary: {
81
+ default: "Toggle",
82
+ parseHTML: (element) => element.getAttribute("data-summary") || "Toggle",
83
+ renderHTML: (attributes) => ({
84
+ "data-summary": attributes.summary
85
+ })
86
+ }
87
+ };
88
+ },
89
+ parseHTML() {
90
+ return [
91
+ { tag: "div[data-toggle-block]" },
92
+ {
93
+ tag: "details",
94
+ getAttrs: (element) => ({
95
+ open: element.hasAttribute("open"),
96
+ summary: element.querySelector("summary")?.textContent || "Toggle"
97
+ })
98
+ }
99
+ ];
100
+ },
101
+ renderHTML({ HTMLAttributes }) {
102
+ return [
103
+ "details",
104
+ mergeAttributes2(HTMLAttributes, {
105
+ "data-toggle-block": "true",
106
+ ...HTMLAttributes["data-open"] === "true" ? { open: "open" } : {}
107
+ }),
108
+ ["summary", {}, HTMLAttributes["data-summary"] || "Toggle"],
109
+ ["div", { class: "toggle-content" }, 0]
110
+ ];
111
+ },
112
+ addCommands() {
113
+ return {
114
+ setToggleBlock: () => ({ commands }) => {
115
+ return commands.insertContent({
116
+ type: this.name,
117
+ attrs: { summary: "Click to expand", open: true },
118
+ content: [{ type: "paragraph", content: [{ type: "text", text: "Content here..." }] }]
119
+ });
120
+ }
121
+ };
122
+ }
123
+ });
124
+
125
+ // src/docs/document/extensions/PageBreak.ts
126
+ import { Node as Node3, mergeAttributes as mergeAttributes3 } from "@tiptap/core";
127
+ var PageBreak = Node3.create({
128
+ name: "pageBreak",
129
+ group: "block",
130
+ atom: true,
131
+ selectable: true,
132
+ draggable: false,
133
+ parseHTML() {
134
+ return [
135
+ { tag: "div[data-page-break]" },
136
+ {
137
+ tag: "div",
138
+ getAttrs: (element) => element.style.pageBreakAfter === "always" ? {} : false
139
+ }
140
+ ];
141
+ },
142
+ renderHTML({ HTMLAttributes }) {
143
+ return [
144
+ "div",
145
+ mergeAttributes3(HTMLAttributes, {
146
+ "data-page-break": "true",
147
+ class: "page-break",
148
+ contenteditable: "false",
149
+ style: "page-break-after: always;"
150
+ }),
151
+ ["div", { class: "page-break-rule" }],
152
+ ["span", { class: "page-break-label" }, "Page Break"],
153
+ ["div", { class: "page-break-rule" }]
154
+ ];
155
+ },
156
+ addCommands() {
157
+ return {
158
+ setPageBreak: () => ({ commands }) => {
159
+ return commands.insertContent({ type: this.name });
160
+ }
161
+ };
162
+ },
163
+ addKeyboardShortcuts() {
164
+ return {
165
+ "Mod-Enter": () => this.editor.commands.setPageBreak()
166
+ };
167
+ }
168
+ });
169
+
170
+ // src/docs/document/extensions/WikiLink.ts
171
+ import { Mark, mergeAttributes as mergeAttributes4 } from "@tiptap/core";
172
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
173
+ var WikiLinkMark = Mark.create({
174
+ name: "wikiLink",
175
+ priority: 1e3,
176
+ addOptions() {
177
+ return {
178
+ onClickLink: () => {
179
+ },
180
+ HTMLAttributes: {
181
+ class: "wiki-link"
182
+ }
183
+ };
184
+ },
185
+ addAttributes() {
186
+ return {
187
+ pageId: {
188
+ default: null,
189
+ parseHTML: (element) => element.getAttribute("data-page-id"),
190
+ renderHTML: (attributes) => ({
191
+ "data-page-id": attributes.pageId
192
+ })
193
+ },
194
+ pageTitle: {
195
+ default: "",
196
+ parseHTML: (element) => element.getAttribute("data-page-title"),
197
+ renderHTML: (attributes) => ({
198
+ "data-page-title": attributes.pageTitle
199
+ })
200
+ }
201
+ };
202
+ },
203
+ parseHTML() {
204
+ return [
205
+ {
206
+ tag: "span[data-wiki-link]"
207
+ }
208
+ ];
209
+ },
210
+ renderHTML({ HTMLAttributes }) {
211
+ return [
212
+ "span",
213
+ mergeAttributes4(
214
+ this.options.HTMLAttributes,
215
+ HTMLAttributes,
216
+ { "data-wiki-link": "" }
217
+ ),
218
+ 0
219
+ ];
220
+ },
221
+ addCommands() {
222
+ return {
223
+ setWikiLink: (attrs) => ({ commands }) => {
224
+ return commands.setMark(this.name, attrs);
225
+ },
226
+ unsetWikiLink: () => ({ commands }) => {
227
+ return commands.unsetMark(this.name);
228
+ }
229
+ };
230
+ },
231
+ addProseMirrorPlugins() {
232
+ const { onClickLink } = this.options;
233
+ return [
234
+ new Plugin({
235
+ key: new PluginKey("wikiLinkClick"),
236
+ props: {
237
+ handleClick(view, pos) {
238
+ const { state } = view;
239
+ const $pos = state.doc.resolve(pos);
240
+ const marks = $pos.marks();
241
+ for (const mark of marks) {
242
+ if (mark.type.name === "wikiLink") {
243
+ const { pageId, pageTitle } = mark.attrs;
244
+ if (pageId || pageTitle) {
245
+ onClickLink(pageId || "", pageTitle || "");
246
+ return true;
247
+ }
248
+ }
249
+ }
250
+ return false;
251
+ }
252
+ }
253
+ })
254
+ ];
255
+ }
256
+ });
257
+
258
+ // src/docs/document/schema.ts
259
+ import StarterKit from "@tiptap/starter-kit";
260
+ import Underline from "@tiptap/extension-underline";
261
+ import TextAlign from "@tiptap/extension-text-align";
262
+ import Placeholder from "@tiptap/extension-placeholder";
263
+ import TaskList from "@tiptap/extension-task-list";
264
+ import TaskItem from "@tiptap/extension-task-item";
265
+ import Table from "@tiptap/extension-table";
266
+ import TableRow from "@tiptap/extension-table-row";
267
+ import TableCell from "@tiptap/extension-table-cell";
268
+ import TableHeader from "@tiptap/extension-table-header";
269
+ import Image from "@tiptap/extension-image";
270
+ import Link from "@tiptap/extension-link";
271
+ import Subscript from "@tiptap/extension-subscript";
272
+ import Superscript from "@tiptap/extension-superscript";
273
+ import Highlight from "@tiptap/extension-highlight";
274
+ import Typography from "@tiptap/extension-typography";
275
+ var SafeImage = Image.extend({
276
+ parseHTML() {
277
+ return [{ tag: "img[src]", getAttrs: (el) => SAFE_IMAGE.test(el.getAttribute("src") ?? "") ? null : false }];
278
+ }
279
+ });
280
+ function createDocumentExtensions(options = {}) {
281
+ const list = [
282
+ StarterKit.configure({
283
+ ...options.codeBlock ? { codeBlock: false } : {},
284
+ heading: { levels: [1, 2, 3, 4, 5, 6] },
285
+ ...options.history === false ? { history: false } : {}
286
+ }),
287
+ Underline,
288
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
289
+ Placeholder.configure({ placeholder: options.placeholder ?? 'Start writing, or press "/" for commands...' }),
290
+ TaskList,
291
+ TaskItem.configure({ nested: true }),
292
+ Table.configure({ resizable: options.resizableTables ?? false }),
293
+ TableRow,
294
+ TableCell,
295
+ TableHeader,
296
+ SafeImage.configure({ inline: false }),
297
+ Link.configure({
298
+ openOnClick: false,
299
+ autolink: true,
300
+ HTMLAttributes: { rel: "noopener noreferrer nofollow", target: "_blank" },
301
+ // the format refuses anything else; the editor must not be able to produce what the format refuses
302
+ isAllowedUri: (url) => SAFE_LINK.test(String(url).trim())
303
+ }),
304
+ Subscript,
305
+ Superscript,
306
+ Highlight.configure({ multicolor: true }),
307
+ Typography,
308
+ Callout,
309
+ ToggleBlock,
310
+ PageBreak,
311
+ WikiLinkMark.configure({ onClickLink: options.onOpenPage ?? (() => {
312
+ }) })
313
+ ];
314
+ if (options.codeBlock) list.push(options.codeBlock);
315
+ return list;
316
+ }
317
+
318
+ // src/docs/document/surface.ts
319
+ import { Editor } from "@tiptap/core";
320
+ function runFormat(editor, command, arg, promptLink) {
321
+ const c = editor.chain().focus();
322
+ switch (command) {
323
+ case "paragraph":
324
+ return c.setParagraph().run();
325
+ case "h1":
326
+ return c.toggleHeading({ level: 1 }).run();
327
+ case "h2":
328
+ return c.toggleHeading({ level: 2 }).run();
329
+ case "h3":
330
+ return c.toggleHeading({ level: 3 }).run();
331
+ case "bold":
332
+ return c.toggleBold().run();
333
+ case "italic":
334
+ return c.toggleItalic().run();
335
+ case "underline":
336
+ return c.toggleUnderline().run();
337
+ case "strike":
338
+ return c.toggleStrike().run();
339
+ case "code":
340
+ return c.toggleCode().run();
341
+ case "highlight":
342
+ return c.toggleHighlight().run();
343
+ case "bulletList":
344
+ return c.toggleBulletList().run();
345
+ case "orderedList":
346
+ return c.toggleOrderedList().run();
347
+ case "taskList":
348
+ return c.toggleTaskList().run();
349
+ case "blockquote":
350
+ return c.toggleBlockquote().run();
351
+ case "codeBlock":
352
+ return c.toggleCodeBlock().run();
353
+ case "callout":
354
+ return c.toggleCallout({ type: "info" }).run();
355
+ case "toggle":
356
+ return c.setToggleBlock().run();
357
+ case "pageBreak":
358
+ return c.setPageBreak().run();
359
+ case "horizontalRule":
360
+ return c.setHorizontalRule().run();
361
+ case "unlink":
362
+ return c.unsetLink().run();
363
+ case "undo":
364
+ return c.undo().run();
365
+ case "redo":
366
+ return c.redo().run();
367
+ case "link": {
368
+ const current = editor.getAttributes("link").href ?? null;
369
+ const href = typeof arg === "string" ? arg : promptLink ? promptLink(current) : null;
370
+ if (href === null || href === void 0) return false;
371
+ if (href.trim() === "") return c.unsetLink().run();
372
+ if (!SAFE_LINK.test(href.trim())) return false;
373
+ return c.extendMarkRange("link").setLink({ href: href.trim(), target: "_blank" }).run();
374
+ }
375
+ default:
376
+ return false;
377
+ }
378
+ }
379
+ var ACTIVE = {
380
+ paragraph: ["paragraph"],
381
+ h1: ["heading", { level: 1 }],
382
+ h2: ["heading", { level: 2 }],
383
+ h3: ["heading", { level: 3 }],
384
+ bold: ["bold"],
385
+ italic: ["italic"],
386
+ underline: ["underline"],
387
+ strike: ["strike"],
388
+ code: ["code"],
389
+ highlight: ["highlight"],
390
+ bulletList: ["bulletList"],
391
+ orderedList: ["orderedList"],
392
+ taskList: ["taskList"],
393
+ blockquote: ["blockquote"],
394
+ codeBlock: ["codeBlock"],
395
+ callout: ["callout"],
396
+ toggle: ["toggleBlock"],
397
+ link: ["link"]
398
+ };
399
+ function editorHandle(editor, promptLink) {
400
+ return {
401
+ getJSON: () => editor.getJSON(),
402
+ setContent: (content) => {
403
+ editor.commands.setContent(content, false);
404
+ },
405
+ setEditable: (editable) => editor.setEditable(editable),
406
+ focus: () => {
407
+ editor.commands.focus();
408
+ },
409
+ run: (command, arg) => runFormat(editor, command, arg, promptLink),
410
+ isActive: (command) => {
411
+ const a = ACTIVE[command];
412
+ return a ? editor.isActive(a[0], a[1]) : false;
413
+ },
414
+ insertText: (text) => {
415
+ editor.chain().focus().insertContent({ type: "text", text }).run();
416
+ },
417
+ insertPageLink: (pageId, pageTitle, label) => {
418
+ const { from, to } = editor.state.selection;
419
+ const words = label || (from !== to ? editor.state.doc.textBetween(from, to, " ") : "") || pageTitle || "Untitled";
420
+ editor.chain().focus().insertContentAt({ from, to }, [
421
+ { type: "text", text: words, marks: [{ type: "wikiLink", attrs: { pageId, pageTitle } }] },
422
+ { type: "text", text: " " }
423
+ ]).run();
424
+ }
425
+ };
426
+ }
427
+ function mountDocumentSurface(element, controller, options = {}) {
428
+ const state = controller.getState();
429
+ const editor = new Editor({
430
+ element,
431
+ extensions: [...createDocumentExtensions({ ...options, onOpenPage: (id, title) => controller.openPage(id, title) }), ...options.extensions ?? []],
432
+ content: state.unsupported.length || state.status === "unsupported" ? "" : state.content,
433
+ editable: !state.readOnly,
434
+ editorProps: {
435
+ attributes: {
436
+ ...options.editorClass ? { class: options.editorClass } : {},
437
+ role: "textbox",
438
+ "aria-multiline": "true",
439
+ "aria-label": options.label ?? "Document text",
440
+ "data-xeno-document-surface": ""
441
+ }
442
+ },
443
+ onUpdate: () => controller.contentChanged(),
444
+ onSelectionUpdate: () => controller.selectionChanged()
445
+ });
446
+ const unbind = controller.attach(editorHandle(editor, options.promptLink));
447
+ return () => {
448
+ unbind();
449
+ editor.destroy();
450
+ };
451
+ }
452
+
453
+ // src/docs/document/controller.ts
454
+ var DOCUMENT_FORMAT_COMMANDS = Object.freeze([
455
+ "paragraph",
456
+ "h1",
457
+ "h2",
458
+ "h3",
459
+ "bold",
460
+ "italic",
461
+ "underline",
462
+ "strike",
463
+ "code",
464
+ "highlight",
465
+ "bulletList",
466
+ "orderedList",
467
+ "taskList",
468
+ "blockquote",
469
+ "codeBlock",
470
+ "callout",
471
+ "toggle",
472
+ "pageBreak",
473
+ "horizontalRule",
474
+ "link",
475
+ "unlink",
476
+ "undo",
477
+ "redo"
478
+ ]);
479
+ var KNOWN = /* @__PURE__ */ new Set([...DOCUMENT_NODE_TYPES, ...DOCUMENT_MARK_TYPES.map((m) => `mark:${m}`)]);
480
+ var DOCUMENT_TOOLBAR = Object.freeze([
481
+ { command: "h1", label: "H1" },
482
+ { command: "h2", label: "H2" },
483
+ { command: "h3", label: "H3" },
484
+ { command: "paragraph", label: "Text" },
485
+ { command: "bold", label: "Bold" },
486
+ { command: "italic", label: "Italic" },
487
+ { command: "underline", label: "Underline" },
488
+ { command: "strike", label: "Strike" },
489
+ { command: "code", label: "Code" },
490
+ { command: "bulletList", label: "List" },
491
+ { command: "orderedList", label: "Numbered" },
492
+ { command: "taskList", label: "Tasks" },
493
+ { command: "blockquote", label: "Quote" },
494
+ { command: "codeBlock", label: "Code block" },
495
+ { command: "callout", label: "Callout" },
496
+ { command: "link", label: "Link" }
497
+ ]);
498
+ var DocumentController = class {
499
+ host;
500
+ state = { id: null, versionId: null, title: "", readOnly: false, unsupported: [], dirty: false, status: "empty", message: null, content: emptyContent(), revision: 0 };
501
+ created = Date.now();
502
+ /** XENO Docs' comment threads, carried through a save untouched (this block does not edit comments). */
503
+ commentThreads;
504
+ editor = null;
505
+ listeners = /* @__PURE__ */ new Set();
506
+ constructor(options) {
507
+ this.host = options.host;
508
+ }
509
+ /** Subscribe to state changes (a view re-renders on each). */
510
+ subscribe(fn) {
511
+ this.listeners.add(fn);
512
+ return () => this.listeners.delete(fn);
513
+ }
514
+ getState() {
515
+ return this.state;
516
+ }
517
+ set(patch) {
518
+ this.state = { ...this.state, ...patch };
519
+ for (const fn of this.listeners) fn();
520
+ }
521
+ /**
522
+ * Open a document. A file the shared schema cannot render is NOT coerced: it opens read-only with
523
+ * the unknown types listed, so nothing the user wrote elsewhere is lost on a save here.
524
+ */
525
+ load(input) {
526
+ let content;
527
+ let unsupported = [];
528
+ let message = null;
529
+ try {
530
+ if (input.xdoc !== void 0) {
531
+ const f = parseXdoc(input.xdoc);
532
+ content = f.content;
533
+ this.created = f.created;
534
+ this.commentThreads = f.commentThreads;
535
+ } else {
536
+ content = input.content ?? emptyContent();
537
+ const problems = validateContent(content);
538
+ if (problems.length) throw new XdocError(problems[0].detail, problems);
539
+ this.created = input.created ?? Date.now();
540
+ this.commentThreads = void 0;
541
+ }
542
+ } catch (e) {
543
+ let raw = emptyContent();
544
+ try {
545
+ const r = typeof input.xdoc === "string" ? JSON.parse(input.xdoc) : input.xdoc ?? { content: input.content };
546
+ if (r && typeof r === "object" && r.content) raw = r.content;
547
+ } catch {
548
+ }
549
+ content = raw;
550
+ unsupported = [...nodeTypesIn(raw)].filter((t) => !KNOWN.has(t));
551
+ message = e instanceof Error ? e.message : "This document cannot be edited here";
552
+ }
553
+ this.set({
554
+ id: input.id,
555
+ versionId: input.versionId ?? null,
556
+ title: input.title,
557
+ readOnly: !!input.readOnly || unsupported.length > 0 || message !== null,
558
+ unsupported,
559
+ dirty: false,
560
+ status: message ? "unsupported" : "ready",
561
+ message,
562
+ content,
563
+ revision: this.state.revision + 1
564
+ });
565
+ if (this.editor) {
566
+ this.editor.setContent(message ? emptyContent() : content);
567
+ this.editor.setEditable(!this.state.readOnly);
568
+ }
569
+ }
570
+ /** Bind the live editor (`./surface` does this). Returns the unbind. */
571
+ attach(editor) {
572
+ this.editor = editor;
573
+ editor.setEditable(!this.state.readOnly);
574
+ return () => {
575
+ if (this.editor === editor) this.editor = null;
576
+ };
577
+ }
578
+ hasEditor() {
579
+ return this.editor !== null;
580
+ }
581
+ /** The editor reported an edit. */
582
+ contentChanged() {
583
+ if (this.state.readOnly) return;
584
+ if (!this.state.dirty) this.host.emit("dirty", { dirty: true });
585
+ this.set({ dirty: true, status: this.state.status === "saved" ? "ready" : this.state.status });
586
+ }
587
+ /** The cursor moved: a view re-reads which format commands are active. No state changes. */
588
+ selectionChanged() {
589
+ this.set({});
590
+ }
591
+ setTitle(title) {
592
+ if (this.state.readOnly || title === this.state.title) return;
593
+ if (!this.state.dirty) this.host.emit("dirty", { dirty: true });
594
+ this.set({ title, dirty: true });
595
+ }
596
+ /** The content as it stands — the live editor's when bound. */
597
+ currentContent() {
598
+ return this.editor ? this.editor.getJSON() : this.state.content;
599
+ }
600
+ /** Run a format command against the live editor. */
601
+ format(command, arg) {
602
+ if (this.state.readOnly || !this.editor) return false;
603
+ if (!DOCUMENT_FORMAT_COMMANDS.includes(command)) return false;
604
+ return this.editor.run(command, arg);
605
+ }
606
+ isActive(command) {
607
+ return this.editor ? this.editor.isActive(command) : false;
608
+ }
609
+ insertText(text) {
610
+ if (this.state.readOnly || !this.editor) return false;
611
+ this.editor.insertText(String(text));
612
+ return true;
613
+ }
614
+ insertPageLink(pageId, pageTitle, label) {
615
+ if (this.state.readOnly || !this.editor || !pageId) return false;
616
+ this.editor.insertPageLink(pageId, pageTitle, label);
617
+ return true;
618
+ }
619
+ /** A page link was clicked — the host navigates. */
620
+ openPage(pageId, pageTitle) {
621
+ if (pageId || pageTitle) this.host.emit("openPage", { pageId, pageTitle });
622
+ }
623
+ /** The `.xdoc` for the document as it stands. Throws {@link XdocError} if it would not validate. */
624
+ toXdoc() {
625
+ return serializeXdoc({ ...this.state.id ? { id: this.state.id } : {}, title: this.state.title, content: this.currentContent(), created: this.created, lastModified: Date.now(), ...this.commentThreads ? { commentThreads: this.commentThreads } : {} });
626
+ }
627
+ /** Ask the host to save. Refused (false) when read-only, when nothing is open, or mid-save. */
628
+ save() {
629
+ const s = this.state;
630
+ if (!s.id || s.readOnly || s.status === "saving") return false;
631
+ const title = s.title.trim();
632
+ if (!title) {
633
+ this.set({ status: "failed", message: "A title is required" });
634
+ return false;
635
+ }
636
+ let xdoc;
637
+ const content = this.currentContent();
638
+ try {
639
+ xdoc = this.toXdoc();
640
+ } catch (e) {
641
+ this.set({ status: "failed", message: e instanceof Error ? e.message : "The document cannot be saved" });
642
+ return false;
643
+ }
644
+ this.set({ status: "saving", message: null });
645
+ const save = { id: s.id, ...s.versionId ? { expectedVersion: s.versionId } : {}, title, xdoc, content, text: plainText(content), pageLinks: pageLinksIn(content) };
646
+ this.host.emit("save", save);
647
+ return true;
648
+ }
649
+ /** The host saved: adopt its new version id. */
650
+ saved(versionId) {
651
+ this.set({ versionId: versionId ?? this.state.versionId, dirty: false, status: "saved", message: null });
652
+ this.host.emit("dirty", { dirty: false });
653
+ }
654
+ /** The host reports an outcome that is not success (`conflict`, `failed`) or a message. */
655
+ setStatus(status, message) {
656
+ this.set({ status, message: message ?? null });
657
+ }
658
+ /** What an agent reads. The status travels with the words so "empty" is never a guess. */
659
+ describe() {
660
+ const content = this.currentContent();
661
+ const s = this.state;
662
+ return { id: s.id, title: s.title, status: s.status, readOnly: s.readOnly, dirty: s.dirty, text: plainText(content), wordCount: wordCount(content), pageLinks: pageLinksIn(content), unsupported: s.unsupported };
663
+ }
664
+ serialize() {
665
+ return { id: this.state.id, title: this.state.title };
666
+ }
667
+ deserialize(_state) {
668
+ }
669
+ dispose() {
670
+ this.listeners.clear();
671
+ this.editor = null;
672
+ }
673
+ };
674
+
675
+ export {
676
+ Callout,
677
+ ToggleBlock,
678
+ PageBreak,
679
+ WikiLinkMark,
680
+ SafeImage,
681
+ createDocumentExtensions,
682
+ runFormat,
683
+ editorHandle,
684
+ mountDocumentSurface,
685
+ DOCUMENT_FORMAT_COMMANDS,
686
+ DOCUMENT_TOOLBAR,
687
+ DocumentController
688
+ };
File without changes