@xenosystem/blocks 0.19.1 → 0.20.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/agent/index.d.ts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/data/index.d.ts +4 -4
- package/dist/docs/index.d.ts +559 -0
- package/dist/docs/index.js +1599 -0
- package/dist/edit/index.d.ts +4 -4
- package/dist/ops/index.d.ts +3 -3
- package/dist/time/index.d.ts +2 -2
- package/dist/trust/index.d.ts +1 -1
- package/package.json +100 -5
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
import "../chunk-2KG3PWR4.js";
|
|
2
|
+
|
|
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 ``;
|
|
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 ``;
|
|
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
|
+
// src/docs/document/panel.ts
|
|
1225
|
+
import { createElement } from "react";
|
|
1226
|
+
import { createRoot } from "react-dom/client";
|
|
1227
|
+
import { bindConfig } from "@xenosystem/block-sdk";
|
|
1228
|
+
|
|
1229
|
+
// src/docs/document/manifest.ts
|
|
1230
|
+
var DOCUMENT_PANEL_ID = "xeno.core.document";
|
|
1231
|
+
var documentManifest = {
|
|
1232
|
+
id: DOCUMENT_PANEL_ID,
|
|
1233
|
+
version: "0.1.0",
|
|
1234
|
+
title: "Document",
|
|
1235
|
+
icon: "file-text",
|
|
1236
|
+
description: "XENO Docs\u2019 editor over the shared .xdoc format: rich text, lists, tasks, tables, callouts, toggles, page links. The host stores and loads; the block edits and asks. A document containing a block this schema does not know opens read-only, never coerced.",
|
|
1237
|
+
defaultSlot: "center",
|
|
1238
|
+
inputs: [
|
|
1239
|
+
{
|
|
1240
|
+
id: "document",
|
|
1241
|
+
name: "Document",
|
|
1242
|
+
type: "object",
|
|
1243
|
+
description: "{id, versionId?, title, xdoc | content, readOnly?}. `xdoc` is the stored file (text or parsed); `versionId` is round-tripped as `expectedVersion` on save so a stale save is refused, not merged.",
|
|
1244
|
+
multiple: false
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
id: "saved",
|
|
1248
|
+
name: "Saved",
|
|
1249
|
+
type: "object",
|
|
1250
|
+
description: "{versionId} \u2014 the host wrote the version the last `save` asked for.",
|
|
1251
|
+
multiple: false
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
id: "status",
|
|
1255
|
+
name: "Status",
|
|
1256
|
+
type: "object",
|
|
1257
|
+
description: '{status: "conflict" | "failed", message?} \u2014 the save did not happen, and why.',
|
|
1258
|
+
multiple: false
|
|
1259
|
+
}
|
|
1260
|
+
],
|
|
1261
|
+
outputs: [
|
|
1262
|
+
{
|
|
1263
|
+
id: "save",
|
|
1264
|
+
name: "Save",
|
|
1265
|
+
type: "object",
|
|
1266
|
+
description: "{id, expectedVersion?, title, xdoc, content, text, pageLinks}. An INTENT \u2014 the host writes `xdoc` (already validated against the format) as the next version and answers on `saved` or `status`."
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
id: "openPage",
|
|
1270
|
+
name: "Open page",
|
|
1271
|
+
type: "object",
|
|
1272
|
+
description: "{pageId, pageTitle} \u2014 a page link (the `wikiLink` mark) was followed. The host navigates."
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
id: "dirty",
|
|
1276
|
+
name: "Dirty",
|
|
1277
|
+
type: "object",
|
|
1278
|
+
description: "{dirty} \u2014 the document has unsaved changes (true) or was just saved (false)."
|
|
1279
|
+
}
|
|
1280
|
+
],
|
|
1281
|
+
commands: [
|
|
1282
|
+
{
|
|
1283
|
+
id: "get_document",
|
|
1284
|
+
title: "Get Document",
|
|
1285
|
+
description: "Return {id, title, status, readOnly, dirty, text, wordCount, pageLinks, unsupported}. `status` travels with the text so an empty string is never read as an empty document when it was an unsupported one.",
|
|
1286
|
+
parameters: {}
|
|
1287
|
+
},
|
|
1288
|
+
{ id: "set_title", title: "Set Title", description: "Rename the document (unsaved until `save`).", parameters: { title: { type: "string", description: "The new title", required: true } } },
|
|
1289
|
+
{ id: "insert_text", title: "Insert Text", description: "Insert plain text at the cursor.", parameters: { text: { type: "string", description: "The text", required: true } } },
|
|
1290
|
+
{
|
|
1291
|
+
id: "format",
|
|
1292
|
+
title: "Format",
|
|
1293
|
+
description: "Apply one format command: paragraph, h1\u2013h3, bold, italic, underline, strike, code, highlight, bulletList, orderedList, taskList, blockquote, codeBlock, callout, toggle, pageBreak, horizontalRule, link (with href), unlink, undo, redo.",
|
|
1294
|
+
parameters: {
|
|
1295
|
+
command: { type: "string", description: "The command", required: true },
|
|
1296
|
+
href: { type: "string", description: "For link: an http(s) or mailto address" }
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
id: "insert_page_link",
|
|
1301
|
+
title: "Link a Page",
|
|
1302
|
+
description: "Insert a link to another document (the `wikiLink` mark) at the cursor.",
|
|
1303
|
+
parameters: {
|
|
1304
|
+
pageId: { type: "string", description: "The linked document\u2019s id", required: true },
|
|
1305
|
+
pageTitle: { type: "string", description: "Its title, shown as the link text", required: true }
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
{ id: "save", title: "Save", description: "Ask the host to save. Refused while read-only or already saving.", parameters: {} }
|
|
1309
|
+
],
|
|
1310
|
+
config: [
|
|
1311
|
+
{ key: "placeholder", label: "Placeholder", type: "text", defaultValue: "Start writing\u2026", description: "The empty-document hint.", placeholder: "Start writing\u2026" },
|
|
1312
|
+
{ key: "showTitle", label: "Show title field", type: "boolean", defaultValue: true, description: "Edit the title in the block. Off when the host owns the title." }
|
|
1313
|
+
],
|
|
1314
|
+
capabilities: ["storage.local"],
|
|
1315
|
+
sdk: "^1.1.0"
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
// src/docs/document/react/DocumentPanelView.tsx
|
|
1319
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
1320
|
+
import { Column, EmptyState, Input, Note, StatusBar, TextButton, Toolbar, ToolbarGroup } from "@xenosystem/components/chrome";
|
|
1321
|
+
|
|
1322
|
+
// 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
|
+
var STATUS = { empty: "", ready: "", saving: "Saving\u2026", saved: "Saved", conflict: "Someone saved a newer version", failed: "Not saved", unsupported: "Read only" };
|
|
1342
|
+
function documentViewState(c, options = {}) {
|
|
1343
|
+
const s = c.getState();
|
|
1344
|
+
const has = s.id !== null;
|
|
1345
|
+
const editable = has && !s.readOnly;
|
|
1346
|
+
const words = wordCount(c.currentContent());
|
|
1347
|
+
const note = s.status === "unsupported" ? `${s.message ?? "This document cannot be edited here."}${s.unsupported.length ? ` Open it in XENO Docs to edit it.` : ""}` : s.status === "failed" || s.status === "conflict" ? s.message ?? STATUS[s.status] : null;
|
|
1348
|
+
return {
|
|
1349
|
+
hasDocument: has,
|
|
1350
|
+
editable,
|
|
1351
|
+
readOnly: has && s.readOnly,
|
|
1352
|
+
title: s.title,
|
|
1353
|
+
titleShown: has && options.showTitle !== false,
|
|
1354
|
+
tools: DOCUMENT_TOOLBAR.map((t) => ({ key: t.command, command: t.command, label: t.label, active: c.isActive(t.command) })),
|
|
1355
|
+
toolsShown: editable,
|
|
1356
|
+
unsupported: s.status === "unsupported",
|
|
1357
|
+
note,
|
|
1358
|
+
canSave: editable && s.status !== "saving",
|
|
1359
|
+
statusLabel: s.dirty && s.status !== "saving" ? "Unsaved changes" : STATUS[s.status] ?? "",
|
|
1360
|
+
wordsLabel: `${words} ${words === 1 ? "word" : "words"}`
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function applyDocumentUi(c, intent) {
|
|
1364
|
+
switch (intent.kind) {
|
|
1365
|
+
case "format":
|
|
1366
|
+
if (DOCUMENT_FORMAT_COMMANDS.includes(intent.command)) c.format(intent.command);
|
|
1367
|
+
break;
|
|
1368
|
+
case "title":
|
|
1369
|
+
c.setTitle(intent.value);
|
|
1370
|
+
break;
|
|
1371
|
+
case "save":
|
|
1372
|
+
c.save();
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
var documentViewManifest = {
|
|
1377
|
+
...documentManifest,
|
|
1378
|
+
capabilities: [],
|
|
1379
|
+
inputs: [{ id: "view", name: "View state", type: "object", description: "The DocumentViewModel the runtime derives from the controller on every draw." }],
|
|
1380
|
+
outputs: [{ id: "ui", name: "UI intent", type: "object", description: "{ kind, command?, value? } \u2014 applied by the runtime to the controller." }],
|
|
1381
|
+
commands: []
|
|
1382
|
+
};
|
|
1383
|
+
var EMPTY = { hasDocument: false, editable: false, readOnly: false, title: "", titleShown: false, tools: [], toolsShown: false, unsupported: false, note: null, canSave: false, statusLabel: "", wordsLabel: "0 words" };
|
|
1384
|
+
var BEHAVIOURS = {
|
|
1385
|
+
"docs/documentPanelView": (inputs) => inputs.view ?? EMPTY
|
|
1386
|
+
};
|
|
1387
|
+
var DOCUMENT_RUNTIME_UNITS = ["document/Surface"];
|
|
1388
|
+
var text = (b) => ({ component: "text", props: { text: b } });
|
|
1389
|
+
var ui = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
|
|
1390
|
+
var DOCUMENT_DECLARATION = {
|
|
1391
|
+
format: "xeno.block@1",
|
|
1392
|
+
manifest: documentViewManifest,
|
|
1393
|
+
behaviour: "docs/documentPanelView",
|
|
1394
|
+
composition: {
|
|
1395
|
+
component: "fragment",
|
|
1396
|
+
children: [
|
|
1397
|
+
{ component: "chrome/EmptyState", when: { not: { input: "view.hasDocument" } }, props: { title: { value: "No document open" }, hint: { value: "Choose a document, or start a new one." } } },
|
|
1398
|
+
{
|
|
1399
|
+
component: "chrome/Column",
|
|
1400
|
+
when: { input: "view.hasDocument" },
|
|
1401
|
+
children: [
|
|
1402
|
+
{
|
|
1403
|
+
component: "chrome/Toolbar",
|
|
1404
|
+
when: { input: "view.toolsShown" },
|
|
1405
|
+
props: {
|
|
1406
|
+
left: {
|
|
1407
|
+
node: {
|
|
1408
|
+
component: "chrome/ToolbarGroup",
|
|
1409
|
+
children: [
|
|
1410
|
+
{
|
|
1411
|
+
component: "chrome/TextButton",
|
|
1412
|
+
each: { input: "view.tools", key: "key" },
|
|
1413
|
+
props: { strong: { item: "active" } },
|
|
1414
|
+
children: [text({ item: "label" })],
|
|
1415
|
+
on: { onClick: ui("format", { command: { item: "command" } }) }
|
|
1416
|
+
}
|
|
1417
|
+
]
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
right: {
|
|
1421
|
+
node: {
|
|
1422
|
+
component: "chrome/ToolbarGroup",
|
|
1423
|
+
props: { end: { value: true } },
|
|
1424
|
+
children: [{ component: "chrome/TextButton", props: { strong: { value: true }, disabled: { not: { input: "view.canSave" } } }, children: [text({ value: "Save" })], on: { onClick: ui("save") } }]
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
{ component: "chrome/Input", when: { input: "view.titleShown" }, props: { value: { input: "view.title" }, label: { value: "Title" }, readOnly: { input: "view.readOnly" } }, on: { onChange: ui("title", { value: { arg: "" } }) } },
|
|
1430
|
+
{ component: "chrome/Note", when: { input: "view.note" }, children: [text({ input: "view.note" })] },
|
|
1431
|
+
{ component: "document/Surface" },
|
|
1432
|
+
{ component: "chrome/StatusBar", props: { left: { input: "view.wordsLabel" }, right: { input: "view.statusLabel" } } }
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
]
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/docs/document/react/DocumentPanelView.tsx
|
|
1440
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1441
|
+
function DocumentSurface({ controller, surface }) {
|
|
1442
|
+
const ref = useRef(null);
|
|
1443
|
+
useEffect(() => {
|
|
1444
|
+
const el = ref.current;
|
|
1445
|
+
if (!el) return;
|
|
1446
|
+
return mountDocumentSurface(el, controller, surface);
|
|
1447
|
+
}, [controller]);
|
|
1448
|
+
return /* @__PURE__ */ jsx("div", { ref, "data-xeno-document-surface-host": "" });
|
|
1449
|
+
}
|
|
1450
|
+
var documentRuntimeComponents = (controller, surface) => ({
|
|
1451
|
+
"document/Surface": () => /* @__PURE__ */ jsx(DocumentSurface, { controller, surface })
|
|
1452
|
+
});
|
|
1453
|
+
function DocumentPanelView({ controller, surface, ...options }) {
|
|
1454
|
+
useSyncExternalStore((fn) => controller.subscribe(fn), () => controller.getState(), () => controller.getState());
|
|
1455
|
+
const view = documentViewState(controller, options);
|
|
1456
|
+
const ui2 = (intent) => applyDocumentUi(controller, intent);
|
|
1457
|
+
if (!view.hasDocument) return /* @__PURE__ */ jsx(EmptyState, { title: "No document open", hint: "Choose a document, or start a new one." });
|
|
1458
|
+
return /* @__PURE__ */ jsxs(Column, { children: [
|
|
1459
|
+
view.toolsShown ? /* @__PURE__ */ jsx(
|
|
1460
|
+
Toolbar,
|
|
1461
|
+
{
|
|
1462
|
+
left: /* @__PURE__ */ jsx(ToolbarGroup, { children: view.tools.map((t) => /* @__PURE__ */ jsx(TextButton, { strong: t.active, onClick: () => ui2({ kind: "format", command: t.command }), children: t.label }, t.key)) }),
|
|
1463
|
+
right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(TextButton, { strong: true, disabled: !view.canSave, onClick: () => ui2({ kind: "save" }), children: "Save" }) })
|
|
1464
|
+
}
|
|
1465
|
+
) : null,
|
|
1466
|
+
view.titleShown ? /* @__PURE__ */ jsx(Input, { value: view.title, label: "Title", readOnly: view.readOnly, onChange: (value) => ui2({ kind: "title", value }) }) : null,
|
|
1467
|
+
view.note ? /* @__PURE__ */ jsx(Note, { children: view.note }) : null,
|
|
1468
|
+
/* @__PURE__ */ jsx(DocumentSurface, { controller, surface }),
|
|
1469
|
+
/* @__PURE__ */ jsx(StatusBar, { left: view.wordsLabel, right: view.statusLabel })
|
|
1470
|
+
] });
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/docs/document/panel.ts
|
|
1474
|
+
function createDocumentPanel(options = {}) {
|
|
1475
|
+
return {
|
|
1476
|
+
manifest: documentManifest,
|
|
1477
|
+
activate(host) {
|
|
1478
|
+
const controller = new DocumentController({ host: { emit: (portId, value) => host.emit(portId, value) } });
|
|
1479
|
+
const resolve = (config) => ({
|
|
1480
|
+
showTitle: config.showTitle !== false,
|
|
1481
|
+
placeholder: typeof config.placeholder === "string" ? config.placeholder : "Start writing\u2026"
|
|
1482
|
+
});
|
|
1483
|
+
let renderConfig = resolve(host.config ?? {});
|
|
1484
|
+
let unrender = null;
|
|
1485
|
+
let root = null;
|
|
1486
|
+
let currentEl = null;
|
|
1487
|
+
const draw = (el) => {
|
|
1488
|
+
unrender?.();
|
|
1489
|
+
if (options.render) {
|
|
1490
|
+
unrender = options.render(el, { controller, config: renderConfig });
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
root = createRoot(el);
|
|
1494
|
+
root.render(createElement(DocumentPanelView, { controller, showTitle: renderConfig.showTitle, surface: { placeholder: renderConfig.placeholder, ...options.surface } }));
|
|
1495
|
+
unrender = () => {
|
|
1496
|
+
root?.unmount();
|
|
1497
|
+
root = null;
|
|
1498
|
+
};
|
|
1499
|
+
};
|
|
1500
|
+
const unbindConfig = bindConfig(host, (config) => {
|
|
1501
|
+
renderConfig = resolve(config);
|
|
1502
|
+
if (currentEl) draw(currentEl);
|
|
1503
|
+
});
|
|
1504
|
+
return {
|
|
1505
|
+
render(el) {
|
|
1506
|
+
currentEl = el;
|
|
1507
|
+
draw(el);
|
|
1508
|
+
},
|
|
1509
|
+
onInput(portId, value) {
|
|
1510
|
+
if (portId === "document" && value && typeof value === "object") controller.load(value);
|
|
1511
|
+
else if (portId === "saved") controller.saved(value?.versionId);
|
|
1512
|
+
else if (portId === "status") {
|
|
1513
|
+
const p = value;
|
|
1514
|
+
if (p?.status) controller.setStatus(p.status, p.message);
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
async onCommand(commandId, params) {
|
|
1518
|
+
switch (commandId) {
|
|
1519
|
+
case "get_document":
|
|
1520
|
+
return controller.describe();
|
|
1521
|
+
case "set_title":
|
|
1522
|
+
controller.setTitle(String(params.title ?? ""));
|
|
1523
|
+
return true;
|
|
1524
|
+
case "insert_text":
|
|
1525
|
+
return controller.insertText(String(params.text ?? ""));
|
|
1526
|
+
case "format":
|
|
1527
|
+
return controller.format(String(params.command ?? ""), params.href);
|
|
1528
|
+
case "insert_page_link":
|
|
1529
|
+
return controller.insertPageLink(String(params.pageId ?? ""), String(params.pageTitle ?? ""));
|
|
1530
|
+
case "save":
|
|
1531
|
+
return controller.save();
|
|
1532
|
+
default:
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
serialize() {
|
|
1537
|
+
return controller.serialize();
|
|
1538
|
+
},
|
|
1539
|
+
deserialize(state) {
|
|
1540
|
+
controller.deserialize(state);
|
|
1541
|
+
},
|
|
1542
|
+
dispose() {
|
|
1543
|
+
unbindConfig();
|
|
1544
|
+
currentEl = null;
|
|
1545
|
+
unrender?.();
|
|
1546
|
+
unrender = null;
|
|
1547
|
+
controller.dispose();
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
var documentPanel = createDocumentPanel();
|
|
1554
|
+
export {
|
|
1555
|
+
Callout,
|
|
1556
|
+
BEHAVIOURS as DOCUMENT_BEHAVIOURS,
|
|
1557
|
+
DOCUMENT_DECLARATION,
|
|
1558
|
+
DOCUMENT_FORMAT_COMMANDS,
|
|
1559
|
+
DOCUMENT_MARK_TYPES,
|
|
1560
|
+
DOCUMENT_NODE_TYPES,
|
|
1561
|
+
DOCUMENT_PANEL_ID,
|
|
1562
|
+
DOCUMENT_RUNTIME_UNITS,
|
|
1563
|
+
DOCUMENT_TOOLBAR,
|
|
1564
|
+
DocumentController,
|
|
1565
|
+
DocumentPanelView,
|
|
1566
|
+
DocumentSurface,
|
|
1567
|
+
LIMITS,
|
|
1568
|
+
PageBreak,
|
|
1569
|
+
SAFE_IMAGE,
|
|
1570
|
+
SAFE_LINK,
|
|
1571
|
+
SafeImage,
|
|
1572
|
+
ToggleBlock,
|
|
1573
|
+
WikiLinkMark,
|
|
1574
|
+
XDOC_EXTENSION,
|
|
1575
|
+
XDOC_FORMAT,
|
|
1576
|
+
XDOC_VERSION,
|
|
1577
|
+
XdocError,
|
|
1578
|
+
applyDocumentUi,
|
|
1579
|
+
createDocumentExtensions,
|
|
1580
|
+
createDocumentPanel,
|
|
1581
|
+
documentManifest,
|
|
1582
|
+
documentPanel,
|
|
1583
|
+
documentRuntimeComponents,
|
|
1584
|
+
documentToMarkdown,
|
|
1585
|
+
documentViewManifest,
|
|
1586
|
+
documentViewState,
|
|
1587
|
+
editorHandle,
|
|
1588
|
+
emptyContent,
|
|
1589
|
+
markdownToDocument,
|
|
1590
|
+
mountDocumentSurface,
|
|
1591
|
+
nodeTypesIn,
|
|
1592
|
+
pageLinksIn,
|
|
1593
|
+
parseXdoc,
|
|
1594
|
+
plainText,
|
|
1595
|
+
runFormat,
|
|
1596
|
+
serializeXdoc,
|
|
1597
|
+
validateContent,
|
|
1598
|
+
wordCount
|
|
1599
|
+
};
|