@xenosystem/blocks 0.20.0 → 0.22.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.
@@ -1,1226 +1,40 @@
1
+ import {
2
+ Callout,
3
+ DOCUMENT_FORMAT_COMMANDS,
4
+ DOCUMENT_TOOLBAR,
5
+ DocumentController,
6
+ PageBreak,
7
+ SafeImage,
8
+ ToggleBlock,
9
+ WikiLinkMark,
10
+ createDocumentExtensions,
11
+ editorHandle,
12
+ mountDocumentSurface,
13
+ runFormat
14
+ } from "../chunk-5VOA3OSG.js";
15
+ import {
16
+ DOCUMENT_MARK_TYPES,
17
+ DOCUMENT_NODE_TYPES,
18
+ LIMITS,
19
+ SAFE_IMAGE,
20
+ SAFE_LINK,
21
+ XDOC_EXTENSION,
22
+ XDOC_FORMAT,
23
+ XDOC_VERSION,
24
+ XdocError,
25
+ documentToMarkdown,
26
+ emptyContent,
27
+ markdownToDocument,
28
+ nodeTypesIn,
29
+ pageLinksIn,
30
+ parseXdoc,
31
+ plainText,
32
+ serializeXdoc,
33
+ validateContent,
34
+ wordCount
35
+ } from "../chunk-NFOBZJDF.js";
1
36
  import "../chunk-2KG3PWR4.js";
2
37
 
3
- // src/docs/document/format.ts
4
- var XDOC_FORMAT = "xeno.xdoc";
5
- var XDOC_VERSION = 1;
6
- var XDOC_EXTENSION = ".xdoc";
7
- var DOCUMENT_NODE_TYPES = Object.freeze([
8
- "doc",
9
- "paragraph",
10
- "text",
11
- "heading",
12
- "blockquote",
13
- "bulletList",
14
- "orderedList",
15
- "listItem",
16
- "taskList",
17
- "taskItem",
18
- "codeBlock",
19
- "horizontalRule",
20
- "hardBreak",
21
- "table",
22
- "tableRow",
23
- "tableCell",
24
- "tableHeader",
25
- "image",
26
- "callout",
27
- "toggleBlock",
28
- "pageBreak"
29
- ]);
30
- var DOCUMENT_MARK_TYPES = Object.freeze([
31
- "bold",
32
- "italic",
33
- "strike",
34
- "code",
35
- "underline",
36
- "link",
37
- "subscript",
38
- "superscript",
39
- "highlight",
40
- "wikiLink"
41
- ]);
42
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
43
- var SAFE_LINK = /^(https?:\/\/|mailto:)/i;
44
- var SAFE_IMAGE = /^(https:\/\/|data:image\/(png|jpe?g|gif|webp);base64,)/i;
45
- var CALLOUT_TYPES = /* @__PURE__ */ new Set(["info", "warning", "error", "success", "tip"]);
46
- var ALIGN = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
47
- var COLOUR_VALUE = /^(#[0-9a-fA-F]{3,8}|[a-zA-Z]{3,20}|var\(--[a-zA-Z0-9-]{1,60}\))$/;
48
- var LANGUAGE = /^[a-zA-Z0-9_+#.-]{0,40}$/;
49
- var LIMITS = Object.freeze({ depth: 64, nodes: 5e4, text: 2e6 });
50
- var isNull = (v) => v === null || v === void 0;
51
- var optString = (max) => (v) => isNull(v) || typeof v === "string" && v.length <= max;
52
- var align = (v) => isNull(v) || typeof v === "string" && ALIGN.has(v);
53
- var int = (lo, hi) => (v) => isNull(v) || Number.isInteger(v) && v >= lo && v <= hi;
54
- var bool = (v) => isNull(v) || typeof v === "boolean";
55
- var colwidth = (v) => isNull(v) || Array.isArray(v) && v.length <= 64 && v.every((n) => Number.isInteger(n) && n >= 0 && n <= 1e4);
56
- var dims = (v) => isNull(v) || typeof v === "number" && v >= 0 && v <= 1e5 || typeof v === "string" && /^\d{1,6}(px|%)?$/.test(v);
57
- var NODE_ATTRS = {
58
- paragraph: { textAlign: align },
59
- heading: { level: int(1, 6), textAlign: align },
60
- orderedList: { start: int(0, 1e6), type: optString(8) },
61
- taskItem: { checked: bool },
62
- codeBlock: { language: (v) => isNull(v) || typeof v === "string" && LANGUAGE.test(v) },
63
- tableCell: { colspan: int(1, 1e3), rowspan: int(1, 1e3), colwidth },
64
- tableHeader: { colspan: int(1, 1e3), rowspan: int(1, 1e3), colwidth },
65
- image: { src: (v) => typeof v === "string" && v.length <= 8e6 && SAFE_IMAGE.test(v), alt: optString(1e3), title: optString(1e3), width: dims, height: dims },
66
- callout: { type: (v) => isNull(v) || typeof v === "string" && CALLOUT_TYPES.has(v) },
67
- toggleBlock: { open: bool, summary: optString(500) }
68
- };
69
- var MARK_ATTRS = {
70
- link: {
71
- href: (v) => typeof v === "string" && v.length <= 4e3 && SAFE_LINK.test(v.trim()),
72
- target: (v) => isNull(v) || v === "_blank",
73
- rel: optString(200),
74
- class: (v) => isNull(v)
75
- },
76
- highlight: { color: (v) => isNull(v) || typeof v === "string" && COLOUR_VALUE.test(v) },
77
- wikiLink: { pageId: (v) => isNull(v) || typeof v === "string" && (UUID.test(v) || /^[A-Za-z0-9_-]{1,128}$/.test(v)), pageTitle: optString(500) }
78
- };
79
- function validateContent(content, options = {}) {
80
- const nodes = /* @__PURE__ */ new Set([...DOCUMENT_NODE_TYPES, ...options.extraNodes ?? []]);
81
- const marks = /* @__PURE__ */ new Set([...DOCUMENT_MARK_TYPES, ...options.extraMarks ?? []]);
82
- const extraN = new Set(options.extraNodes ?? []);
83
- const extraM = new Set(options.extraMarks ?? []);
84
- const problems = [];
85
- let count = 0;
86
- let text2 = 0;
87
- const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
88
- const attrs = (where, kind, value, rules) => {
89
- if (isNull(value)) return;
90
- if (!isObj(value)) {
91
- problems.push({ path: where, code: "bad_shape", detail: `${kind}.attrs is not an object` });
92
- return;
93
- }
94
- for (const [k, v] of Object.entries(value)) {
95
- const rule = rules?.[k];
96
- if (!rule) {
97
- if (!isNull(v)) problems.push({ path: where, code: "bad_attr", detail: `${kind} does not take "${k}"` });
98
- continue;
99
- }
100
- if (!rule(v)) problems.push({ path: where, code: "bad_attr", detail: `${kind}.${k} is not allowed` });
101
- }
102
- };
103
- const walk = (n, where, depth) => {
104
- if (problems.length > 50) return;
105
- if (depth > LIMITS.depth) {
106
- problems.push({ path: where, code: "too_deep", detail: `deeper than ${LIMITS.depth}` });
107
- return;
108
- }
109
- if (++count > LIMITS.nodes) {
110
- if (count === LIMITS.nodes + 1) problems.push({ path: where, code: "too_large", detail: `more than ${LIMITS.nodes} nodes` });
111
- return;
112
- }
113
- if (!isObj(n) || typeof n.type !== "string") {
114
- problems.push({ path: where, code: "bad_shape", detail: "a node needs a string type" });
115
- return;
116
- }
117
- if (!nodes.has(n.type)) {
118
- problems.push({ path: where, code: "unknown_node", detail: n.type });
119
- return;
120
- }
121
- if (!extraN.has(n.type)) attrs(where, n.type, n.attrs, NODE_ATTRS[n.type]);
122
- if (n.type === "text") {
123
- if (typeof n.text !== "string" || !n.text) problems.push({ path: where, code: "bad_shape", detail: "a text node needs text" });
124
- else if ((text2 += n.text.length) > LIMITS.text) problems.push({ path: where, code: "too_large", detail: `more than ${LIMITS.text} characters` });
125
- } else if (n.text !== void 0) problems.push({ path: where, code: "bad_shape", detail: `${n.type} cannot carry text` });
126
- if (n.marks !== void 0) {
127
- if (!Array.isArray(n.marks)) problems.push({ path: where, code: "bad_shape", detail: "marks is not a list" });
128
- else n.marks.forEach((m, i) => {
129
- if (!isObj(m) || typeof m.type !== "string") problems.push({ path: `${where}.marks[${i}]`, code: "bad_shape", detail: "a mark needs a string type" });
130
- else if (!marks.has(m.type)) problems.push({ path: `${where}.marks[${i}]`, code: "unknown_mark", detail: m.type });
131
- else if (!extraM.has(m.type)) attrs(`${where}.marks[${i}]`, m.type, m.attrs, MARK_ATTRS[m.type]);
132
- });
133
- }
134
- if (n.content !== void 0) {
135
- if (!Array.isArray(n.content)) problems.push({ path: where, code: "bad_shape", detail: "content is not a list" });
136
- else n.content.forEach((c, i) => walk(c, `${where}.content[${i}]`, depth + 1));
137
- }
138
- };
139
- if (!isObj(content) || content.type !== "doc") return [{ path: "content", code: "bad_shape", detail: "the root must be a doc node" }];
140
- walk(content, "content", 0);
141
- return problems;
142
- }
143
- var XdocError = class extends Error {
144
- problems;
145
- constructor(message, problems = []) {
146
- super(message);
147
- this.name = "XdocError";
148
- this.problems = problems;
149
- }
150
- };
151
- var emptyContent = () => ({ type: "doc", content: [{ type: "paragraph" }] });
152
- function parseXdoc(input, options = {}) {
153
- let raw = input;
154
- if (typeof input === "string") {
155
- try {
156
- raw = JSON.parse(input);
157
- } catch {
158
- throw new XdocError("Not a XENO document: the file is not JSON");
159
- }
160
- }
161
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new XdocError("Not a XENO document");
162
- const r = raw;
163
- if (r.format !== void 0 && r.format !== XDOC_FORMAT) throw new XdocError(`Not a XENO document (format "${String(r.format)}")`);
164
- if (r.version !== void 0 && (!Number.isInteger(r.version) || r.version > XDOC_VERSION)) {
165
- throw new XdocError(`This document was written by a newer XENO Docs (version ${String(r.version)})`);
166
- }
167
- const problems = validateContent(r.content, options);
168
- if (problems.length) throw new XdocError(`The document contains content this editor cannot show (${problems[0].detail})`, problems);
169
- const now = Date.now();
170
- const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
171
- const content = r.content;
172
- return {
173
- format: XDOC_FORMAT,
174
- version: XDOC_VERSION,
175
- ...typeof r.id === "string" ? { id: r.id } : {},
176
- title: typeof r.title === "string" ? r.title : "",
177
- content,
178
- created: num(r.created, now),
179
- lastModified: num(r.lastModified, now),
180
- wordCount: num(r.wordCount, wordCount(content)),
181
- ...r.commentThreads && typeof r.commentThreads === "object" ? { commentThreads: r.commentThreads } : {}
182
- };
183
- }
184
- function serializeXdoc(doc, options = {}) {
185
- const problems = validateContent(doc.content, options);
186
- if (problems.length) throw new XdocError(`The document contains content that cannot be saved (${problems[0].detail})`, problems);
187
- const file = {
188
- format: XDOC_FORMAT,
189
- version: XDOC_VERSION,
190
- ...doc.id ? { id: doc.id } : {},
191
- title: doc.title,
192
- content: doc.content,
193
- created: doc.created,
194
- lastModified: doc.lastModified,
195
- wordCount: doc.wordCount ?? wordCount(doc.content),
196
- ...doc.commentThreads && typeof doc.commentThreads === "object" ? { commentThreads: doc.commentThreads } : {}
197
- };
198
- return JSON.stringify(file, null, options.pretty === false ? void 0 : 2);
199
- }
200
- var BLOCKS = /* @__PURE__ */ new Set(["paragraph", "heading", "blockquote", "listItem", "taskItem", "codeBlock", "tableCell", "tableHeader", "callout", "toggleBlock"]);
201
- function plainText(content) {
202
- const out = [];
203
- const walk = (n) => {
204
- if (!n) return;
205
- if (n.type === "text" && typeof n.text === "string") out.push(n.text);
206
- else if (n.type === "hardBreak") out.push(" ");
207
- if (n.type === "toggleBlock" && typeof n.attrs?.summary === "string") out.push(` ${n.attrs.summary} `);
208
- for (const c of n.content ?? []) walk(c);
209
- if (BLOCKS.has(n.type)) out.push(" ");
210
- };
211
- walk(content);
212
- return out.join("").replace(/\s+/g, " ").trim();
213
- }
214
- function wordCount(content) {
215
- const t = plainText(content);
216
- return t ? t.split(" ").length : 0;
217
- }
218
- function pageLinksIn(content) {
219
- const seen = /* @__PURE__ */ new Set();
220
- const walk = (n) => {
221
- if (!n) return;
222
- for (const m of n.marks ?? []) {
223
- const id = m.type === "wikiLink" ? m.attrs?.pageId : null;
224
- if (typeof id === "string" && id) seen.add(id.toLowerCase());
225
- }
226
- for (const c of n.content ?? []) walk(c);
227
- };
228
- walk(content);
229
- return [...seen];
230
- }
231
- function nodeTypesIn(content) {
232
- const seen = /* @__PURE__ */ new Set();
233
- const walk = (n) => {
234
- if (!n) return;
235
- seen.add(n.type);
236
- for (const m of n.marks ?? []) seen.add(`mark:${m.type}`);
237
- for (const c of n.content ?? []) walk(c);
238
- };
239
- walk(content);
240
- return seen;
241
- }
242
-
243
- // src/docs/document/markdown.ts
244
- var PAGE_LINK = /^xeno-doc:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
245
- function documentToMarkdown(doc) {
246
- if (!doc.content) return "";
247
- return doc.content.map((node) => nodeToMarkdown(node, 0)).join("\n\n");
248
- }
249
- function nodeToMarkdown(node, depth) {
250
- switch (node.type) {
251
- case "heading":
252
- return `${"#".repeat(node.attrs?.level || 1)} ${inlineToMarkdown(node.content)}`;
253
- case "paragraph":
254
- return inlineToMarkdown(node.content);
255
- case "bulletList":
256
- return (node.content || []).map((item) => listItemToMarkdown(item, depth, "-")).join("\n");
257
- case "orderedList":
258
- return (node.content || []).map((item, i) => listItemToMarkdown(item, depth, `${i + 1}.`)).join("\n");
259
- case "taskList":
260
- return (node.content || []).map((item) => {
261
- const checked = item.attrs?.checked ? "x" : " ";
262
- const content = (item.content || []).map((c) => nodeToMarkdown(c, depth + 1)).join("\n");
263
- return `${" ".repeat(depth)}- [${checked}] ${content}`;
264
- }).join("\n");
265
- case "blockquote":
266
- return (node.content || []).map((c) => `> ${nodeToMarkdown(c, depth)}`).join("\n");
267
- case "codeBlock": {
268
- const lang = node.attrs?.language || "";
269
- const code = node.content?.map((c) => c.text || "").join("") || "";
270
- return `\`\`\`${lang}
271
- ${code}
272
- \`\`\``;
273
- }
274
- case "horizontalRule":
275
- return "---";
276
- case "image": {
277
- const alt = node.attrs?.alt || "";
278
- const src = node.attrs?.src || "";
279
- const title = node.attrs?.title ? ` "${node.attrs.title}"` : "";
280
- return `![${alt}](${src}${title})`;
281
- }
282
- case "table":
283
- return tableToMarkdown(node);
284
- case "callout": {
285
- const type = node.attrs?.type || "info";
286
- const content = (node.content || []).map((c) => nodeToMarkdown(c, depth)).join("\n");
287
- return `> **${type.charAt(0).toUpperCase() + type.slice(1)}:** ${content}`;
288
- }
289
- case "mathBlock":
290
- return `$$
291
- ${node.attrs?.content || ""}
292
- $$`;
293
- case "toggleBlock": {
294
- const summary = node.attrs?.summary || "Toggle";
295
- const content = (node.content || []).map((c) => nodeToMarkdown(c, depth)).join("\n");
296
- return `<details>
297
- <summary>${summary}</summary>
298
-
299
- ${content}
300
- </details>`;
301
- }
302
- case "multiColumn": {
303
- return (node.content || []).map((col, i) => {
304
- const colContent = (col.content || []).map((c) => nodeToMarkdown(c, depth)).join("\n\n");
305
- return `<!-- Column ${i + 1} -->
306
- ${colContent}`;
307
- }).join("\n\n");
308
- }
309
- default:
310
- return inlineToMarkdown(node.content);
311
- }
312
- }
313
- function listItemToMarkdown(item, depth, bullet) {
314
- const content = (item.content || []).map((c, i) => {
315
- const text2 = nodeToMarkdown(c, depth + 1);
316
- if (i === 0) return text2;
317
- return " ".repeat(depth + 1) + text2;
318
- }).join("\n");
319
- return `${" ".repeat(depth)}${bullet} ${content}`;
320
- }
321
- function inlineToMarkdown(content) {
322
- if (!content) return "";
323
- return content.map((node) => {
324
- let text2 = node.text || "";
325
- if (node.type === "hardBreak") return " \n";
326
- if (node.type === "image") {
327
- return `![${node.attrs?.alt || ""}](${node.attrs?.src || ""})`;
328
- }
329
- if (!node.marks) return text2;
330
- for (const mark of node.marks) {
331
- switch (mark.type) {
332
- case "bold":
333
- text2 = `**${text2}**`;
334
- break;
335
- case "italic":
336
- text2 = `*${text2}*`;
337
- break;
338
- case "strike":
339
- text2 = `~~${text2}~~`;
340
- break;
341
- case "code":
342
- text2 = `\`${text2}\``;
343
- break;
344
- case "link":
345
- text2 = `[${text2}](${mark.attrs?.href || ""})`;
346
- break;
347
- case "wikiLink":
348
- text2 = `[${text2}](xeno-doc:${mark.attrs?.pageId || ""})`;
349
- break;
350
- case "underline":
351
- text2 = `<u>${text2}</u>`;
352
- break;
353
- case "subscript":
354
- text2 = `<sub>${text2}</sub>`;
355
- break;
356
- case "superscript":
357
- text2 = `<sup>${text2}</sup>`;
358
- break;
359
- }
360
- }
361
- return text2;
362
- }).join("");
363
- }
364
- function tableToMarkdown(node) {
365
- if (!node.content) return "";
366
- const rows = node.content.filter((r) => r.type === "tableRow");
367
- if (rows.length === 0) return "";
368
- const lines = [];
369
- for (let i = 0; i < rows.length; i++) {
370
- const cells = rows[i].content || [];
371
- const line = cells.map((cell) => {
372
- const text2 = (cell.content || []).map((c) => inlineToMarkdown(c.content)).join(" ");
373
- return ` ${text2} `;
374
- }).join("|");
375
- lines.push(`|${line}|`);
376
- if (i === 0) {
377
- const sep = cells.map(() => " --- ").join("|");
378
- lines.push(`|${sep}|`);
379
- }
380
- }
381
- return lines.join("\n");
382
- }
383
- function markdownToDocument(md) {
384
- let input = md;
385
- const frontmatterMatch = input.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
386
- if (frontmatterMatch) {
387
- input = input.slice(frontmatterMatch[0].length);
388
- }
389
- const lines = input.split("\n");
390
- const content = [];
391
- let i = 0;
392
- while (i < lines.length) {
393
- const line = lines[i];
394
- if (line.trim() === "") {
395
- i++;
396
- continue;
397
- }
398
- const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
399
- if (headingMatch) {
400
- content.push({
401
- type: "heading",
402
- attrs: { level: headingMatch[1].length },
403
- content: parseInlineMarkdown(headingMatch[2])
404
- });
405
- i++;
406
- continue;
407
- }
408
- if (/^(---|\*\*\*|___)/.test(line.trim())) {
409
- content.push({ type: "horizontalRule" });
410
- i++;
411
- continue;
412
- }
413
- if (line.trim().startsWith("```")) {
414
- const lang = line.trim().slice(3).trim();
415
- const codeLines = [];
416
- i++;
417
- while (i < lines.length && !lines[i].trim().startsWith("```")) {
418
- codeLines.push(lines[i]);
419
- i++;
420
- }
421
- i++;
422
- content.push({
423
- type: "codeBlock",
424
- attrs: { language: lang || null },
425
- content: [{ type: "text", text: codeLines.join("\n") }]
426
- });
427
- continue;
428
- }
429
- if (line.trim() === "$$") {
430
- const mathLines = [];
431
- i++;
432
- while (i < lines.length && lines[i].trim() !== "$$") {
433
- mathLines.push(lines[i]);
434
- i++;
435
- }
436
- i++;
437
- content.push({
438
- type: "mathBlock",
439
- attrs: { content: mathLines.join("\n") }
440
- });
441
- continue;
442
- }
443
- if (line.startsWith(">")) {
444
- const quoteLines = [];
445
- while (i < lines.length && lines[i].startsWith(">")) {
446
- quoteLines.push(lines[i].replace(/^>\s?/, ""));
447
- i++;
448
- }
449
- content.push({
450
- type: "blockquote",
451
- content: quoteLines.map((ql) => ({
452
- type: "paragraph",
453
- content: parseInlineMarkdown(ql)
454
- }))
455
- });
456
- continue;
457
- }
458
- if (/^\s*[-*+]\s/.test(line)) {
459
- const listItems = [];
460
- while (i < lines.length && /^\s*[-*+]\s/.test(lines[i])) {
461
- const itemText = lines[i].replace(/^\s*[-*+]\s/, "");
462
- const taskMatch = itemText.match(/^\[([ xX])\]\s*(.*)$/);
463
- if (taskMatch) {
464
- listItems.push({
465
- type: "taskItem",
466
- attrs: { checked: taskMatch[1] !== " " },
467
- content: [{ type: "paragraph", content: parseInlineMarkdown(taskMatch[2]) }]
468
- });
469
- } else {
470
- listItems.push({
471
- type: "listItem",
472
- content: [{ type: "paragraph", content: parseInlineMarkdown(itemText) }]
473
- });
474
- }
475
- i++;
476
- }
477
- if (listItems[0]?.type === "taskItem") {
478
- content.push({ type: "taskList", content: listItems });
479
- } else {
480
- content.push({ type: "bulletList", content: listItems });
481
- }
482
- continue;
483
- }
484
- if (/^\s*\d+\.\s/.test(line)) {
485
- const listItems = [];
486
- while (i < lines.length && /^\s*\d+\.\s/.test(lines[i])) {
487
- const itemText = lines[i].replace(/^\s*\d+\.\s/, "");
488
- listItems.push({
489
- type: "listItem",
490
- content: [{ type: "paragraph", content: parseInlineMarkdown(itemText) }]
491
- });
492
- i++;
493
- }
494
- content.push({ type: "orderedList", content: listItems });
495
- continue;
496
- }
497
- if (line.includes("|") && line.trim().startsWith("|")) {
498
- const tableRows = [];
499
- let isFirst = true;
500
- while (i < lines.length && lines[i].includes("|")) {
501
- const trimmed = lines[i].trim();
502
- if (/^\|[\s\-:|]+\|$/.test(trimmed)) {
503
- i++;
504
- continue;
505
- }
506
- const cells = trimmed.split("|").filter((_, idx, arr) => idx > 0 && idx < arr.length - 1).map((cell) => cell.trim());
507
- const cellType = isFirst ? "tableHeader" : "tableCell";
508
- tableRows.push({
509
- type: "tableRow",
510
- content: cells.map((cellText) => ({
511
- type: cellType,
512
- content: [{ type: "paragraph", content: parseInlineMarkdown(cellText) }]
513
- }))
514
- });
515
- isFirst = false;
516
- i++;
517
- }
518
- content.push({ type: "table", content: tableRows });
519
- continue;
520
- }
521
- const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
522
- if (imgMatch) {
523
- content.push({
524
- type: "image",
525
- attrs: { src: imgMatch[2], alt: imgMatch[1] }
526
- });
527
- i++;
528
- continue;
529
- }
530
- const paraLines = [line];
531
- i++;
532
- while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|```|>|\s*[-*+]\s|\s*\d+\.\s|---|\*\*\*|___|!\[|\|)/.test(lines[i])) {
533
- paraLines.push(lines[i]);
534
- i++;
535
- }
536
- content.push({
537
- type: "paragraph",
538
- content: parseInlineMarkdown(paraLines.join(" "))
539
- });
540
- }
541
- return { type: "doc", content: content.length > 0 ? content : [{ type: "paragraph" }] };
542
- }
543
- function parseInlineMarkdown(text2) {
544
- if (!text2) return [];
545
- const result = [];
546
- const inlineRegex = /(\*\*(.+?)\*\*|\*(.+?)\*|~~(.+?)~~|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|<u>(.+?)<\/u>|<sub>(.+?)<\/sub>|<sup>(.+?)<\/sup>)/g;
547
- let lastIndex = 0;
548
- let match;
549
- while ((match = inlineRegex.exec(text2)) !== null) {
550
- if (match.index > lastIndex) {
551
- result.push({ type: "text", text: text2.slice(lastIndex, match.index) });
552
- }
553
- if (match[2]) {
554
- result.push({ type: "text", text: match[2], marks: [{ type: "bold" }] });
555
- } else if (match[3]) {
556
- result.push({ type: "text", text: match[3], marks: [{ type: "italic" }] });
557
- } else if (match[4]) {
558
- result.push({ type: "text", text: match[4], marks: [{ type: "strike" }] });
559
- } else if (match[5]) {
560
- result.push({ type: "text", text: match[5], marks: [{ type: "code" }] });
561
- } else if (match[6] && match[7]) {
562
- const page = PAGE_LINK.exec(match[7]);
563
- if (page) result.push({ type: "text", text: match[6], marks: [{ type: "wikiLink", attrs: { pageId: page[1].toLowerCase(), pageTitle: match[6] } }] });
564
- else if (SAFE_LINK.test(match[7].trim())) result.push({ type: "text", text: match[6], marks: [{ type: "link", attrs: { href: match[7] } }] });
565
- else result.push({ type: "text", text: match[6] });
566
- } else if (match[8]) {
567
- result.push({ type: "text", text: match[8], marks: [{ type: "underline" }] });
568
- } else if (match[9]) {
569
- result.push({ type: "text", text: match[9], marks: [{ type: "subscript" }] });
570
- } else if (match[10]) {
571
- result.push({ type: "text", text: match[10], marks: [{ type: "superscript" }] });
572
- }
573
- lastIndex = match.index + match[0].length;
574
- }
575
- if (lastIndex < text2.length) {
576
- result.push({ type: "text", text: text2.slice(lastIndex) });
577
- }
578
- const kept = result.filter((n) => n.type !== "text" || (n.text ?? "") !== "");
579
- return kept.length > 0 ? kept : text2 ? [{ type: "text", text: text2 }] : [];
580
- }
581
-
582
- // src/docs/document/schema.ts
583
- import StarterKit from "@tiptap/starter-kit";
584
- import Underline from "@tiptap/extension-underline";
585
- import TextAlign from "@tiptap/extension-text-align";
586
- import Placeholder from "@tiptap/extension-placeholder";
587
- import TaskList from "@tiptap/extension-task-list";
588
- import TaskItem from "@tiptap/extension-task-item";
589
- import Table from "@tiptap/extension-table";
590
- import TableRow from "@tiptap/extension-table-row";
591
- import TableCell from "@tiptap/extension-table-cell";
592
- import TableHeader from "@tiptap/extension-table-header";
593
- import Image from "@tiptap/extension-image";
594
- import Link from "@tiptap/extension-link";
595
- import Subscript from "@tiptap/extension-subscript";
596
- import Superscript from "@tiptap/extension-superscript";
597
- import Highlight from "@tiptap/extension-highlight";
598
- import Typography from "@tiptap/extension-typography";
599
-
600
- // src/docs/document/extensions/Callout.ts
601
- import { Node, mergeAttributes } from "@tiptap/core";
602
- var Callout = Node.create({
603
- name: "callout",
604
- group: "block",
605
- content: "block+",
606
- defining: true,
607
- addAttributes() {
608
- return {
609
- type: {
610
- default: "info",
611
- parseHTML: (element) => element.getAttribute("data-callout-type") || "info",
612
- renderHTML: (attributes) => ({
613
- "data-callout-type": attributes.type
614
- })
615
- }
616
- };
617
- },
618
- parseHTML() {
619
- return [
620
- { tag: "div[data-callout-type]" },
621
- { tag: "div.callout" }
622
- ];
623
- },
624
- renderHTML({ HTMLAttributes }) {
625
- const type = HTMLAttributes["data-callout-type"] || "info";
626
- return [
627
- "div",
628
- mergeAttributes(HTMLAttributes, {
629
- class: `callout ${type}`,
630
- "data-callout-type": type
631
- }),
632
- 0
633
- ];
634
- },
635
- addCommands() {
636
- return {
637
- setCallout: (attrs) => ({ commands }) => {
638
- return commands.wrapIn(this.name, attrs);
639
- },
640
- toggleCallout: (attrs) => ({ commands }) => {
641
- return commands.toggleWrap(this.name, attrs);
642
- }
643
- };
644
- }
645
- });
646
-
647
- // src/docs/document/extensions/ToggleBlock.ts
648
- import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
649
- var ToggleBlock = Node2.create({
650
- name: "toggleBlock",
651
- group: "block",
652
- content: "block+",
653
- defining: true,
654
- addAttributes() {
655
- return {
656
- open: {
657
- default: true,
658
- parseHTML: (element) => element.getAttribute("data-open") !== "false",
659
- renderHTML: (attributes) => ({
660
- "data-open": String(attributes.open)
661
- })
662
- },
663
- summary: {
664
- default: "Toggle",
665
- parseHTML: (element) => element.getAttribute("data-summary") || "Toggle",
666
- renderHTML: (attributes) => ({
667
- "data-summary": attributes.summary
668
- })
669
- }
670
- };
671
- },
672
- parseHTML() {
673
- return [
674
- { tag: "div[data-toggle-block]" },
675
- {
676
- tag: "details",
677
- getAttrs: (element) => ({
678
- open: element.hasAttribute("open"),
679
- summary: element.querySelector("summary")?.textContent || "Toggle"
680
- })
681
- }
682
- ];
683
- },
684
- renderHTML({ HTMLAttributes }) {
685
- return [
686
- "details",
687
- mergeAttributes2(HTMLAttributes, {
688
- "data-toggle-block": "true",
689
- ...HTMLAttributes["data-open"] === "true" ? { open: "open" } : {}
690
- }),
691
- ["summary", {}, HTMLAttributes["data-summary"] || "Toggle"],
692
- ["div", { class: "toggle-content" }, 0]
693
- ];
694
- },
695
- addCommands() {
696
- return {
697
- setToggleBlock: () => ({ commands }) => {
698
- return commands.insertContent({
699
- type: this.name,
700
- attrs: { summary: "Click to expand", open: true },
701
- content: [{ type: "paragraph", content: [{ type: "text", text: "Content here..." }] }]
702
- });
703
- }
704
- };
705
- }
706
- });
707
-
708
- // src/docs/document/extensions/PageBreak.ts
709
- import { Node as Node3, mergeAttributes as mergeAttributes3 } from "@tiptap/core";
710
- var PageBreak = Node3.create({
711
- name: "pageBreak",
712
- group: "block",
713
- atom: true,
714
- selectable: true,
715
- draggable: false,
716
- parseHTML() {
717
- return [
718
- { tag: "div[data-page-break]" },
719
- {
720
- tag: "div",
721
- getAttrs: (element) => element.style.pageBreakAfter === "always" ? {} : false
722
- }
723
- ];
724
- },
725
- renderHTML({ HTMLAttributes }) {
726
- return [
727
- "div",
728
- mergeAttributes3(HTMLAttributes, {
729
- "data-page-break": "true",
730
- class: "page-break",
731
- contenteditable: "false",
732
- style: "page-break-after: always;"
733
- }),
734
- ["div", { class: "page-break-rule" }],
735
- ["span", { class: "page-break-label" }, "Page Break"],
736
- ["div", { class: "page-break-rule" }]
737
- ];
738
- },
739
- addCommands() {
740
- return {
741
- setPageBreak: () => ({ commands }) => {
742
- return commands.insertContent({ type: this.name });
743
- }
744
- };
745
- },
746
- addKeyboardShortcuts() {
747
- return {
748
- "Mod-Enter": () => this.editor.commands.setPageBreak()
749
- };
750
- }
751
- });
752
-
753
- // src/docs/document/extensions/WikiLink.ts
754
- import { Mark, mergeAttributes as mergeAttributes4 } from "@tiptap/core";
755
- import { Plugin, PluginKey } from "@tiptap/pm/state";
756
- var WikiLinkMark = Mark.create({
757
- name: "wikiLink",
758
- priority: 1e3,
759
- addOptions() {
760
- return {
761
- onClickLink: () => {
762
- },
763
- HTMLAttributes: {
764
- class: "wiki-link"
765
- }
766
- };
767
- },
768
- addAttributes() {
769
- return {
770
- pageId: {
771
- default: null,
772
- parseHTML: (element) => element.getAttribute("data-page-id"),
773
- renderHTML: (attributes) => ({
774
- "data-page-id": attributes.pageId
775
- })
776
- },
777
- pageTitle: {
778
- default: "",
779
- parseHTML: (element) => element.getAttribute("data-page-title"),
780
- renderHTML: (attributes) => ({
781
- "data-page-title": attributes.pageTitle
782
- })
783
- }
784
- };
785
- },
786
- parseHTML() {
787
- return [
788
- {
789
- tag: "span[data-wiki-link]"
790
- }
791
- ];
792
- },
793
- renderHTML({ HTMLAttributes }) {
794
- return [
795
- "span",
796
- mergeAttributes4(
797
- this.options.HTMLAttributes,
798
- HTMLAttributes,
799
- { "data-wiki-link": "" }
800
- ),
801
- 0
802
- ];
803
- },
804
- addCommands() {
805
- return {
806
- setWikiLink: (attrs) => ({ commands }) => {
807
- return commands.setMark(this.name, attrs);
808
- },
809
- unsetWikiLink: () => ({ commands }) => {
810
- return commands.unsetMark(this.name);
811
- }
812
- };
813
- },
814
- addProseMirrorPlugins() {
815
- const { onClickLink } = this.options;
816
- return [
817
- new Plugin({
818
- key: new PluginKey("wikiLinkClick"),
819
- props: {
820
- handleClick(view, pos) {
821
- const { state } = view;
822
- const $pos = state.doc.resolve(pos);
823
- const marks = $pos.marks();
824
- for (const mark of marks) {
825
- if (mark.type.name === "wikiLink") {
826
- const { pageId, pageTitle } = mark.attrs;
827
- if (pageId || pageTitle) {
828
- onClickLink(pageId || "", pageTitle || "");
829
- return true;
830
- }
831
- }
832
- }
833
- return false;
834
- }
835
- }
836
- })
837
- ];
838
- }
839
- });
840
-
841
- // src/docs/document/schema.ts
842
- var SafeImage = Image.extend({
843
- parseHTML() {
844
- return [{ tag: "img[src]", getAttrs: (el) => SAFE_IMAGE.test(el.getAttribute("src") ?? "") ? null : false }];
845
- }
846
- });
847
- function createDocumentExtensions(options = {}) {
848
- const list = [
849
- StarterKit.configure({
850
- ...options.codeBlock ? { codeBlock: false } : {},
851
- heading: { levels: [1, 2, 3, 4, 5, 6] },
852
- ...options.history === false ? { history: false } : {}
853
- }),
854
- Underline,
855
- TextAlign.configure({ types: ["heading", "paragraph"] }),
856
- Placeholder.configure({ placeholder: options.placeholder ?? 'Start writing, or press "/" for commands...' }),
857
- TaskList,
858
- TaskItem.configure({ nested: true }),
859
- Table.configure({ resizable: options.resizableTables ?? false }),
860
- TableRow,
861
- TableCell,
862
- TableHeader,
863
- SafeImage.configure({ inline: false }),
864
- Link.configure({
865
- openOnClick: false,
866
- autolink: true,
867
- HTMLAttributes: { rel: "noopener noreferrer nofollow", target: "_blank" },
868
- // the format refuses anything else; the editor must not be able to produce what the format refuses
869
- isAllowedUri: (url) => SAFE_LINK.test(String(url).trim())
870
- }),
871
- Subscript,
872
- Superscript,
873
- Highlight.configure({ multicolor: true }),
874
- Typography,
875
- Callout,
876
- ToggleBlock,
877
- PageBreak,
878
- WikiLinkMark.configure({ onClickLink: options.onOpenPage ?? (() => {
879
- }) })
880
- ];
881
- if (options.codeBlock) list.push(options.codeBlock);
882
- return list;
883
- }
884
-
885
- // src/docs/document/surface.ts
886
- import { Editor } from "@tiptap/core";
887
- function runFormat(editor, command, arg, promptLink) {
888
- const c = editor.chain().focus();
889
- switch (command) {
890
- case "paragraph":
891
- return c.setParagraph().run();
892
- case "h1":
893
- return c.toggleHeading({ level: 1 }).run();
894
- case "h2":
895
- return c.toggleHeading({ level: 2 }).run();
896
- case "h3":
897
- return c.toggleHeading({ level: 3 }).run();
898
- case "bold":
899
- return c.toggleBold().run();
900
- case "italic":
901
- return c.toggleItalic().run();
902
- case "underline":
903
- return c.toggleUnderline().run();
904
- case "strike":
905
- return c.toggleStrike().run();
906
- case "code":
907
- return c.toggleCode().run();
908
- case "highlight":
909
- return c.toggleHighlight().run();
910
- case "bulletList":
911
- return c.toggleBulletList().run();
912
- case "orderedList":
913
- return c.toggleOrderedList().run();
914
- case "taskList":
915
- return c.toggleTaskList().run();
916
- case "blockquote":
917
- return c.toggleBlockquote().run();
918
- case "codeBlock":
919
- return c.toggleCodeBlock().run();
920
- case "callout":
921
- return c.toggleCallout({ type: "info" }).run();
922
- case "toggle":
923
- return c.setToggleBlock().run();
924
- case "pageBreak":
925
- return c.setPageBreak().run();
926
- case "horizontalRule":
927
- return c.setHorizontalRule().run();
928
- case "unlink":
929
- return c.unsetLink().run();
930
- case "undo":
931
- return c.undo().run();
932
- case "redo":
933
- return c.redo().run();
934
- case "link": {
935
- const current = editor.getAttributes("link").href ?? null;
936
- const href = typeof arg === "string" ? arg : promptLink ? promptLink(current) : null;
937
- if (href === null || href === void 0) return false;
938
- if (href.trim() === "") return c.unsetLink().run();
939
- if (!SAFE_LINK.test(href.trim())) return false;
940
- return c.extendMarkRange("link").setLink({ href: href.trim(), target: "_blank" }).run();
941
- }
942
- default:
943
- return false;
944
- }
945
- }
946
- var ACTIVE = {
947
- paragraph: ["paragraph"],
948
- h1: ["heading", { level: 1 }],
949
- h2: ["heading", { level: 2 }],
950
- h3: ["heading", { level: 3 }],
951
- bold: ["bold"],
952
- italic: ["italic"],
953
- underline: ["underline"],
954
- strike: ["strike"],
955
- code: ["code"],
956
- highlight: ["highlight"],
957
- bulletList: ["bulletList"],
958
- orderedList: ["orderedList"],
959
- taskList: ["taskList"],
960
- blockquote: ["blockquote"],
961
- codeBlock: ["codeBlock"],
962
- callout: ["callout"],
963
- toggle: ["toggleBlock"],
964
- link: ["link"]
965
- };
966
- function editorHandle(editor, promptLink) {
967
- return {
968
- getJSON: () => editor.getJSON(),
969
- setContent: (content) => {
970
- editor.commands.setContent(content, false);
971
- },
972
- setEditable: (editable) => editor.setEditable(editable),
973
- focus: () => {
974
- editor.commands.focus();
975
- },
976
- run: (command, arg) => runFormat(editor, command, arg, promptLink),
977
- isActive: (command) => {
978
- const a = ACTIVE[command];
979
- return a ? editor.isActive(a[0], a[1]) : false;
980
- },
981
- insertText: (text2) => {
982
- editor.chain().focus().insertContent({ type: "text", text: text2 }).run();
983
- },
984
- insertPageLink: (pageId, pageTitle, label) => {
985
- const { from, to } = editor.state.selection;
986
- const words = label || (from !== to ? editor.state.doc.textBetween(from, to, " ") : "") || pageTitle || "Untitled";
987
- editor.chain().focus().insertContentAt({ from, to }, [
988
- { type: "text", text: words, marks: [{ type: "wikiLink", attrs: { pageId, pageTitle } }] },
989
- { type: "text", text: " " }
990
- ]).run();
991
- }
992
- };
993
- }
994
- function mountDocumentSurface(element, controller, options = {}) {
995
- const state = controller.getState();
996
- const editor = new Editor({
997
- element,
998
- extensions: [...createDocumentExtensions({ ...options, onOpenPage: (id, title) => controller.openPage(id, title) }), ...options.extensions ?? []],
999
- content: state.unsupported.length || state.status === "unsupported" ? "" : state.content,
1000
- editable: !state.readOnly,
1001
- editorProps: {
1002
- attributes: {
1003
- ...options.editorClass ? { class: options.editorClass } : {},
1004
- role: "textbox",
1005
- "aria-multiline": "true",
1006
- "aria-label": options.label ?? "Document text",
1007
- "data-xeno-document-surface": ""
1008
- }
1009
- },
1010
- onUpdate: () => controller.contentChanged(),
1011
- onSelectionUpdate: () => controller.selectionChanged()
1012
- });
1013
- const unbind = controller.attach(editorHandle(editor, options.promptLink));
1014
- return () => {
1015
- unbind();
1016
- editor.destroy();
1017
- };
1018
- }
1019
-
1020
- // src/docs/document/controller.ts
1021
- var DOCUMENT_FORMAT_COMMANDS = Object.freeze([
1022
- "paragraph",
1023
- "h1",
1024
- "h2",
1025
- "h3",
1026
- "bold",
1027
- "italic",
1028
- "underline",
1029
- "strike",
1030
- "code",
1031
- "highlight",
1032
- "bulletList",
1033
- "orderedList",
1034
- "taskList",
1035
- "blockquote",
1036
- "codeBlock",
1037
- "callout",
1038
- "toggle",
1039
- "pageBreak",
1040
- "horizontalRule",
1041
- "link",
1042
- "unlink",
1043
- "undo",
1044
- "redo"
1045
- ]);
1046
- var KNOWN = /* @__PURE__ */ new Set([...DOCUMENT_NODE_TYPES, ...DOCUMENT_MARK_TYPES.map((m) => `mark:${m}`)]);
1047
- var DocumentController = class {
1048
- host;
1049
- state = { id: null, versionId: null, title: "", readOnly: false, unsupported: [], dirty: false, status: "empty", message: null, content: emptyContent(), revision: 0 };
1050
- created = Date.now();
1051
- /** XENO Docs' comment threads, carried through a save untouched (this block does not edit comments). */
1052
- commentThreads;
1053
- editor = null;
1054
- listeners = /* @__PURE__ */ new Set();
1055
- constructor(options) {
1056
- this.host = options.host;
1057
- }
1058
- /** Subscribe to state changes (a view re-renders on each). */
1059
- subscribe(fn) {
1060
- this.listeners.add(fn);
1061
- return () => this.listeners.delete(fn);
1062
- }
1063
- getState() {
1064
- return this.state;
1065
- }
1066
- set(patch) {
1067
- this.state = { ...this.state, ...patch };
1068
- for (const fn of this.listeners) fn();
1069
- }
1070
- /**
1071
- * Open a document. A file the shared schema cannot render is NOT coerced: it opens read-only with
1072
- * the unknown types listed, so nothing the user wrote elsewhere is lost on a save here.
1073
- */
1074
- load(input) {
1075
- let content;
1076
- let unsupported = [];
1077
- let message = null;
1078
- try {
1079
- if (input.xdoc !== void 0) {
1080
- const f = parseXdoc(input.xdoc);
1081
- content = f.content;
1082
- this.created = f.created;
1083
- this.commentThreads = f.commentThreads;
1084
- } else {
1085
- content = input.content ?? emptyContent();
1086
- const problems = validateContent(content);
1087
- if (problems.length) throw new XdocError(problems[0].detail, problems);
1088
- this.created = input.created ?? Date.now();
1089
- this.commentThreads = void 0;
1090
- }
1091
- } catch (e) {
1092
- let raw = emptyContent();
1093
- try {
1094
- const r = typeof input.xdoc === "string" ? JSON.parse(input.xdoc) : input.xdoc ?? { content: input.content };
1095
- if (r && typeof r === "object" && r.content) raw = r.content;
1096
- } catch {
1097
- }
1098
- content = raw;
1099
- unsupported = [...nodeTypesIn(raw)].filter((t) => !KNOWN.has(t));
1100
- message = e instanceof Error ? e.message : "This document cannot be edited here";
1101
- }
1102
- this.set({
1103
- id: input.id,
1104
- versionId: input.versionId ?? null,
1105
- title: input.title,
1106
- readOnly: !!input.readOnly || unsupported.length > 0 || message !== null,
1107
- unsupported,
1108
- dirty: false,
1109
- status: message ? "unsupported" : "ready",
1110
- message,
1111
- content,
1112
- revision: this.state.revision + 1
1113
- });
1114
- if (this.editor) {
1115
- this.editor.setContent(message ? emptyContent() : content);
1116
- this.editor.setEditable(!this.state.readOnly);
1117
- }
1118
- }
1119
- /** Bind the live editor (`./surface` does this). Returns the unbind. */
1120
- attach(editor) {
1121
- this.editor = editor;
1122
- editor.setEditable(!this.state.readOnly);
1123
- return () => {
1124
- if (this.editor === editor) this.editor = null;
1125
- };
1126
- }
1127
- hasEditor() {
1128
- return this.editor !== null;
1129
- }
1130
- /** The editor reported an edit. */
1131
- contentChanged() {
1132
- if (this.state.readOnly) return;
1133
- if (!this.state.dirty) this.host.emit("dirty", { dirty: true });
1134
- this.set({ dirty: true, status: this.state.status === "saved" ? "ready" : this.state.status });
1135
- }
1136
- /** The cursor moved: a view re-reads which format commands are active. No state changes. */
1137
- selectionChanged() {
1138
- this.set({});
1139
- }
1140
- setTitle(title) {
1141
- if (this.state.readOnly || title === this.state.title) return;
1142
- if (!this.state.dirty) this.host.emit("dirty", { dirty: true });
1143
- this.set({ title, dirty: true });
1144
- }
1145
- /** The content as it stands — the live editor's when bound. */
1146
- currentContent() {
1147
- return this.editor ? this.editor.getJSON() : this.state.content;
1148
- }
1149
- /** Run a format command against the live editor. */
1150
- format(command, arg) {
1151
- if (this.state.readOnly || !this.editor) return false;
1152
- if (!DOCUMENT_FORMAT_COMMANDS.includes(command)) return false;
1153
- return this.editor.run(command, arg);
1154
- }
1155
- isActive(command) {
1156
- return this.editor ? this.editor.isActive(command) : false;
1157
- }
1158
- insertText(text2) {
1159
- if (this.state.readOnly || !this.editor) return false;
1160
- this.editor.insertText(String(text2));
1161
- return true;
1162
- }
1163
- insertPageLink(pageId, pageTitle, label) {
1164
- if (this.state.readOnly || !this.editor || !pageId) return false;
1165
- this.editor.insertPageLink(pageId, pageTitle, label);
1166
- return true;
1167
- }
1168
- /** A page link was clicked — the host navigates. */
1169
- openPage(pageId, pageTitle) {
1170
- if (pageId || pageTitle) this.host.emit("openPage", { pageId, pageTitle });
1171
- }
1172
- /** The `.xdoc` for the document as it stands. Throws {@link XdocError} if it would not validate. */
1173
- toXdoc() {
1174
- 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 } : {} });
1175
- }
1176
- /** Ask the host to save. Refused (false) when read-only, when nothing is open, or mid-save. */
1177
- save() {
1178
- const s = this.state;
1179
- if (!s.id || s.readOnly || s.status === "saving") return false;
1180
- const title = s.title.trim();
1181
- if (!title) {
1182
- this.set({ status: "failed", message: "A title is required" });
1183
- return false;
1184
- }
1185
- let xdoc;
1186
- const content = this.currentContent();
1187
- try {
1188
- xdoc = this.toXdoc();
1189
- } catch (e) {
1190
- this.set({ status: "failed", message: e instanceof Error ? e.message : "The document cannot be saved" });
1191
- return false;
1192
- }
1193
- this.set({ status: "saving", message: null });
1194
- const save = { id: s.id, ...s.versionId ? { expectedVersion: s.versionId } : {}, title, xdoc, content, text: plainText(content), pageLinks: pageLinksIn(content) };
1195
- this.host.emit("save", save);
1196
- return true;
1197
- }
1198
- /** The host saved: adopt its new version id. */
1199
- saved(versionId) {
1200
- this.set({ versionId: versionId ?? this.state.versionId, dirty: false, status: "saved", message: null });
1201
- this.host.emit("dirty", { dirty: false });
1202
- }
1203
- /** The host reports an outcome that is not success (`conflict`, `failed`) or a message. */
1204
- setStatus(status, message) {
1205
- this.set({ status, message: message ?? null });
1206
- }
1207
- /** What an agent reads. The status travels with the words so "empty" is never a guess. */
1208
- describe() {
1209
- const content = this.currentContent();
1210
- const s = this.state;
1211
- 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 };
1212
- }
1213
- serialize() {
1214
- return { id: this.state.id, title: this.state.title };
1215
- }
1216
- deserialize(_state) {
1217
- }
1218
- dispose() {
1219
- this.listeners.clear();
1220
- this.editor = null;
1221
- }
1222
- };
1223
-
1224
38
  // src/docs/document/panel.ts
1225
39
  import { createElement } from "react";
1226
40
  import { createRoot } from "react-dom/client";
@@ -1320,24 +134,6 @@ import { useEffect, useRef, useSyncExternalStore } from "react";
1320
134
  import { Column, EmptyState, Input, Note, StatusBar, TextButton, Toolbar, ToolbarGroup } from "@xenosystem/components/chrome";
1321
135
 
1322
136
  // src/docs/document/declaration.ts
1323
- var DOCUMENT_TOOLBAR = Object.freeze([
1324
- { command: "h1", label: "H1" },
1325
- { command: "h2", label: "H2" },
1326
- { command: "h3", label: "H3" },
1327
- { command: "paragraph", label: "Text" },
1328
- { command: "bold", label: "Bold" },
1329
- { command: "italic", label: "Italic" },
1330
- { command: "underline", label: "Underline" },
1331
- { command: "strike", label: "Strike" },
1332
- { command: "code", label: "Code" },
1333
- { command: "bulletList", label: "List" },
1334
- { command: "orderedList", label: "Numbered" },
1335
- { command: "taskList", label: "Tasks" },
1336
- { command: "blockquote", label: "Quote" },
1337
- { command: "codeBlock", label: "Code block" },
1338
- { command: "callout", label: "Callout" },
1339
- { command: "link", label: "Link" }
1340
- ]);
1341
137
  var STATUS = { empty: "", ready: "", saving: "Saving\u2026", saved: "Saved", conflict: "Someone saved a newer version", failed: "Not saved", unsupported: "Read only" };
1342
138
  function documentViewState(c, options = {}) {
1343
139
  const s = c.getState();