@x12i/editor 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -16
- package/dist/chunk-FYXMOAJT.js +1890 -0
- package/dist/chunk-FYXMOAJT.js.map +1 -0
- package/dist/index.d.ts +207 -171
- package/dist/index.js +1321 -7
- package/dist/index.js.map +1 -1
- package/dist/outline-Drc3E0C1.d.ts +339 -0
- package/dist/react.d.ts +546 -0
- package/dist/react.js +4439 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +1219 -0
- package/package.json +57 -8
|
@@ -0,0 +1,1890 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
var DOCUMENT_VERSION = 1;
|
|
3
|
+
function createEmptyDocument(id = "root") {
|
|
4
|
+
return {
|
|
5
|
+
version: DOCUMENT_VERSION,
|
|
6
|
+
blocks: [{ id, type: "paragraph", content: [] }]
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
var BLOCK_TYPES = [
|
|
10
|
+
"paragraph",
|
|
11
|
+
"heading",
|
|
12
|
+
"quote",
|
|
13
|
+
"callout",
|
|
14
|
+
"toggle",
|
|
15
|
+
"divider",
|
|
16
|
+
"bulleted-list",
|
|
17
|
+
"numbered-list",
|
|
18
|
+
"task-list",
|
|
19
|
+
"code",
|
|
20
|
+
"equation",
|
|
21
|
+
"table",
|
|
22
|
+
"image",
|
|
23
|
+
"video",
|
|
24
|
+
"audio",
|
|
25
|
+
"file",
|
|
26
|
+
"bookmark",
|
|
27
|
+
"columns",
|
|
28
|
+
"page-break",
|
|
29
|
+
"table-of-contents",
|
|
30
|
+
"chart",
|
|
31
|
+
"diagram",
|
|
32
|
+
"data-view",
|
|
33
|
+
"ai"
|
|
34
|
+
];
|
|
35
|
+
var MARK_TYPES = [
|
|
36
|
+
"bold",
|
|
37
|
+
"italic",
|
|
38
|
+
"underline",
|
|
39
|
+
"strike",
|
|
40
|
+
"code",
|
|
41
|
+
"link",
|
|
42
|
+
"color",
|
|
43
|
+
"highlight"
|
|
44
|
+
];
|
|
45
|
+
var CALLOUT_VARIANTS = [
|
|
46
|
+
"info",
|
|
47
|
+
"success",
|
|
48
|
+
"warning",
|
|
49
|
+
"danger",
|
|
50
|
+
"neutral"
|
|
51
|
+
];
|
|
52
|
+
var blockTypeSet = new Set(BLOCK_TYPES);
|
|
53
|
+
function isKnownBlockType(type) {
|
|
54
|
+
return typeof type === "string" && blockTypeSet.has(type);
|
|
55
|
+
}
|
|
56
|
+
function createParagraph(id, text = "") {
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
type: "paragraph",
|
|
60
|
+
content: text === "" ? [] : [{ type: "text", text }]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/ids.ts
|
|
65
|
+
var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
66
|
+
var ID_LENGTH = 12;
|
|
67
|
+
function generateBlockId() {
|
|
68
|
+
const bytes = new Uint8Array(ID_LENGTH);
|
|
69
|
+
const webCrypto = globalThis.crypto;
|
|
70
|
+
if (webCrypto?.getRandomValues) {
|
|
71
|
+
webCrypto.getRandomValues(bytes);
|
|
72
|
+
} else {
|
|
73
|
+
for (let index = 0; index < ID_LENGTH; index += 1) bytes[index] = Math.floor(Math.random() * 256);
|
|
74
|
+
}
|
|
75
|
+
let id = "";
|
|
76
|
+
for (const byte of bytes) id += ALPHABET.charAt(byte % ALPHABET.length);
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
function createSeededRandom(seed) {
|
|
80
|
+
let state = seed >>> 0;
|
|
81
|
+
return () => {
|
|
82
|
+
state = state + 1831565813 >>> 0;
|
|
83
|
+
let value = state;
|
|
84
|
+
value = Math.imul(value ^ value >>> 15, value | 1);
|
|
85
|
+
value ^= value + Math.imul(value ^ value >>> 7, value | 61);
|
|
86
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function createSequentialIdGenerator(prefix = "b", start = 1) {
|
|
90
|
+
let counter = start;
|
|
91
|
+
return () => {
|
|
92
|
+
const id = `${prefix}${counter}`;
|
|
93
|
+
counter += 1;
|
|
94
|
+
return id;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function walkListItems(items, visit) {
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
visit(item.id);
|
|
100
|
+
if (item.children) walkListItems(item.children, visit);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function walkBlocks(blocks, visit, parent = null) {
|
|
104
|
+
for (const block of blocks) {
|
|
105
|
+
visit(block, parent);
|
|
106
|
+
switch (block.type) {
|
|
107
|
+
case "toggle":
|
|
108
|
+
walkBlocks(block.children ?? [], visit, block);
|
|
109
|
+
break;
|
|
110
|
+
case "columns":
|
|
111
|
+
for (const column of block.columns ?? []) walkBlocks(column, visit, block);
|
|
112
|
+
break;
|
|
113
|
+
case "ai":
|
|
114
|
+
if (block.result) walkBlocks(block.result, visit, block);
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function collectIds(doc) {
|
|
122
|
+
const ids = [];
|
|
123
|
+
walkBlocks(doc.blocks, (block) => {
|
|
124
|
+
ids.push(block.id);
|
|
125
|
+
if (block.type === "bulleted-list" || block.type === "numbered-list" || block.type === "task-list") {
|
|
126
|
+
walkListItems(block.items ?? [], (id) => ids.push(id));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return ids;
|
|
130
|
+
}
|
|
131
|
+
function findDuplicateIds(doc) {
|
|
132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
133
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const id of collectIds(doc)) {
|
|
135
|
+
if (seen.has(id)) duplicates.add(id);
|
|
136
|
+
seen.add(id);
|
|
137
|
+
}
|
|
138
|
+
return [...duplicates];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/url.ts
|
|
142
|
+
var DEFAULT_PROTOCOLS = ["http", "https", "mailto", "tel"];
|
|
143
|
+
var INVISIBLE = /[\u0000-\u0020\u007f-\u00a0\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u3000\ufeff]/g;
|
|
144
|
+
var SCHEME = /^([a-z][a-z0-9+.-]*):/i;
|
|
145
|
+
var DATA_IMAGE = /^data:image\/[a-z0-9.+-]+(?:;[a-z0-9=.+-]+)*,/i;
|
|
146
|
+
function sanitizeUrl(input, options = {}) {
|
|
147
|
+
if (typeof input !== "string") return null;
|
|
148
|
+
const trimmed = input.trim();
|
|
149
|
+
if (trimmed === "") return null;
|
|
150
|
+
const compact = trimmed.replace(INVISIBLE, "");
|
|
151
|
+
const scheme = SCHEME.exec(compact);
|
|
152
|
+
if (scheme === null) {
|
|
153
|
+
return trimmed;
|
|
154
|
+
}
|
|
155
|
+
const protocol = (scheme[1] ?? "").toLowerCase();
|
|
156
|
+
if (protocol === "data") {
|
|
157
|
+
return options.allowDataImage === true && DATA_IMAGE.test(compact) ? trimmed : null;
|
|
158
|
+
}
|
|
159
|
+
const allowed = options.allowedProtocols ?? DEFAULT_PROTOCOLS;
|
|
160
|
+
return allowed.includes(protocol) ? trimmed : null;
|
|
161
|
+
}
|
|
162
|
+
function isSafeUrl(input, options) {
|
|
163
|
+
return sanitizeUrl(input, options) !== null;
|
|
164
|
+
}
|
|
165
|
+
function sanitizeMediaUrl(input) {
|
|
166
|
+
return sanitizeUrl(input, { allowDataImage: true });
|
|
167
|
+
}
|
|
168
|
+
var DOMAIN_LIKE = /^(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:[/?#]|$)/i;
|
|
169
|
+
var EMAIL_LIKE = /^[^\s@/:]+@[^\s@/:]+\.[^\s@/:]+$/;
|
|
170
|
+
function normalizeUrlInput(input) {
|
|
171
|
+
const trimmed = input.trim();
|
|
172
|
+
if (trimmed === "") return "";
|
|
173
|
+
if (EMAIL_LIKE.test(trimmed)) return `mailto:${trimmed}`;
|
|
174
|
+
if (DOMAIN_LIKE.test(trimmed)) return `https://${trimmed}`;
|
|
175
|
+
return trimmed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/tokens.ts
|
|
179
|
+
var TEXT_COLOR_TOKENS = [
|
|
180
|
+
"color.text.secondary",
|
|
181
|
+
"color.text.link",
|
|
182
|
+
"color.accent.default",
|
|
183
|
+
"color.status.success.foreground",
|
|
184
|
+
"color.status.warning.foreground",
|
|
185
|
+
"color.status.danger.foreground",
|
|
186
|
+
"color.status.info.foreground",
|
|
187
|
+
"color.chart.categorical.4"
|
|
188
|
+
];
|
|
189
|
+
var HIGHLIGHT_TOKENS = [
|
|
190
|
+
"color.highlight",
|
|
191
|
+
"color.accent.subtle",
|
|
192
|
+
"color.status.success.background",
|
|
193
|
+
"color.status.warning.background",
|
|
194
|
+
"color.status.danger.background",
|
|
195
|
+
"color.status.info.background"
|
|
196
|
+
];
|
|
197
|
+
var textColorSet = new Set(TEXT_COLOR_TOKENS);
|
|
198
|
+
var highlightSet = new Set(HIGHLIGHT_TOKENS);
|
|
199
|
+
function isTextColorToken(token) {
|
|
200
|
+
return typeof token === "string" && textColorSet.has(token);
|
|
201
|
+
}
|
|
202
|
+
function isHighlightToken(token) {
|
|
203
|
+
return typeof token === "string" && highlightSet.has(token);
|
|
204
|
+
}
|
|
205
|
+
var toKebab = (segment) => segment.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
206
|
+
function tokenToCssVarName(token) {
|
|
207
|
+
return `--kit-${token.split(".").map(toKebab).join("-")}`;
|
|
208
|
+
}
|
|
209
|
+
function textColorToCss(token) {
|
|
210
|
+
return isTextColorToken(token) ? `var(${tokenToCssVarName(token)})` : null;
|
|
211
|
+
}
|
|
212
|
+
function highlightToCss(token) {
|
|
213
|
+
return isHighlightToken(token) ? `var(${tokenToCssVarName(token)})` : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/tiptap-json.ts
|
|
217
|
+
var TIPTAP_NODES = {
|
|
218
|
+
doc: "doc",
|
|
219
|
+
text: "text",
|
|
220
|
+
paragraph: "paragraph",
|
|
221
|
+
heading: "heading",
|
|
222
|
+
blockquote: "blockquote",
|
|
223
|
+
callout: "callout",
|
|
224
|
+
toggle: "toggle",
|
|
225
|
+
toggleSummary: "toggleSummary",
|
|
226
|
+
horizontalRule: "horizontalRule",
|
|
227
|
+
bulletList: "bulletList",
|
|
228
|
+
orderedList: "orderedList",
|
|
229
|
+
listItem: "listItem",
|
|
230
|
+
taskList: "taskList",
|
|
231
|
+
taskItem: "taskItem",
|
|
232
|
+
codeBlock: "codeBlock",
|
|
233
|
+
equation: "equation",
|
|
234
|
+
table: "table",
|
|
235
|
+
tableRow: "tableRow",
|
|
236
|
+
tableCell: "tableCell",
|
|
237
|
+
tableHeader: "tableHeader",
|
|
238
|
+
image: "image",
|
|
239
|
+
media: "media",
|
|
240
|
+
file: "file",
|
|
241
|
+
bookmark: "bookmark",
|
|
242
|
+
columns: "columns",
|
|
243
|
+
column: "column",
|
|
244
|
+
pageBreak: "pageBreak",
|
|
245
|
+
tableOfContents: "tableOfContents",
|
|
246
|
+
chart: "chart",
|
|
247
|
+
diagram: "diagram",
|
|
248
|
+
dataView: "dataView",
|
|
249
|
+
aiBlock: "aiBlock",
|
|
250
|
+
unknownBlock: "unknownBlock",
|
|
251
|
+
hardBreak: "hardBreak",
|
|
252
|
+
mention: "mention",
|
|
253
|
+
emoji: "emoji"
|
|
254
|
+
};
|
|
255
|
+
var TIPTAP_MARKS = {
|
|
256
|
+
bold: "bold",
|
|
257
|
+
italic: "italic",
|
|
258
|
+
underline: "underline",
|
|
259
|
+
strike: "strike",
|
|
260
|
+
code: "code",
|
|
261
|
+
link: "link",
|
|
262
|
+
color: "color",
|
|
263
|
+
highlight: "highlight"
|
|
264
|
+
};
|
|
265
|
+
var BLOCK_ID_NODES = [
|
|
266
|
+
TIPTAP_NODES.paragraph,
|
|
267
|
+
TIPTAP_NODES.heading,
|
|
268
|
+
TIPTAP_NODES.blockquote,
|
|
269
|
+
TIPTAP_NODES.callout,
|
|
270
|
+
TIPTAP_NODES.toggle,
|
|
271
|
+
TIPTAP_NODES.horizontalRule,
|
|
272
|
+
TIPTAP_NODES.bulletList,
|
|
273
|
+
TIPTAP_NODES.orderedList,
|
|
274
|
+
TIPTAP_NODES.taskList,
|
|
275
|
+
TIPTAP_NODES.codeBlock,
|
|
276
|
+
TIPTAP_NODES.equation,
|
|
277
|
+
TIPTAP_NODES.table,
|
|
278
|
+
TIPTAP_NODES.image,
|
|
279
|
+
TIPTAP_NODES.media,
|
|
280
|
+
TIPTAP_NODES.file,
|
|
281
|
+
TIPTAP_NODES.bookmark,
|
|
282
|
+
TIPTAP_NODES.columns,
|
|
283
|
+
TIPTAP_NODES.pageBreak,
|
|
284
|
+
TIPTAP_NODES.tableOfContents,
|
|
285
|
+
TIPTAP_NODES.chart,
|
|
286
|
+
TIPTAP_NODES.diagram,
|
|
287
|
+
TIPTAP_NODES.dataView,
|
|
288
|
+
TIPTAP_NODES.aiBlock,
|
|
289
|
+
TIPTAP_NODES.unknownBlock,
|
|
290
|
+
TIPTAP_NODES.listItem,
|
|
291
|
+
TIPTAP_NODES.taskItem
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
// src/convert.ts
|
|
295
|
+
function createContext(options) {
|
|
296
|
+
return {
|
|
297
|
+
warnings: options?.warnings,
|
|
298
|
+
generateId: options?.generateId ?? generateBlockId,
|
|
299
|
+
seenIds: /* @__PURE__ */ new Set()
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function warn(ctx, code, message, extra = {}) {
|
|
303
|
+
if (!ctx.warnings) return;
|
|
304
|
+
const warning = { code, message };
|
|
305
|
+
if (extra.blockId !== void 0) warning.blockId = extra.blockId;
|
|
306
|
+
if (extra.nodeType !== void 0) warning.nodeType = extra.nodeType;
|
|
307
|
+
ctx.warnings.push(warning);
|
|
308
|
+
}
|
|
309
|
+
function isRecord(value) {
|
|
310
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
311
|
+
}
|
|
312
|
+
function str(value, fallback = "") {
|
|
313
|
+
return typeof value === "string" ? value : fallback;
|
|
314
|
+
}
|
|
315
|
+
function optionalString(value) {
|
|
316
|
+
return typeof value === "string" ? value : void 0;
|
|
317
|
+
}
|
|
318
|
+
function safeUrl(value, ctx, media) {
|
|
319
|
+
const url = str(value);
|
|
320
|
+
if (url === "") return "";
|
|
321
|
+
const safe = media ? sanitizeMediaUrl(url) : sanitizeUrl(url);
|
|
322
|
+
if (safe === null) {
|
|
323
|
+
warn(ctx, "invalid-value", "An unsafe URL was replaced by an empty value.");
|
|
324
|
+
return "";
|
|
325
|
+
}
|
|
326
|
+
return url;
|
|
327
|
+
}
|
|
328
|
+
function optionalNumber(value) {
|
|
329
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
330
|
+
}
|
|
331
|
+
function cloneJson(value) {
|
|
332
|
+
return JSON.parse(JSON.stringify(value));
|
|
333
|
+
}
|
|
334
|
+
function nullable(value) {
|
|
335
|
+
return value === void 0 ? null : value;
|
|
336
|
+
}
|
|
337
|
+
function node(type, attrs2, content) {
|
|
338
|
+
const result = { type };
|
|
339
|
+
if (attrs2 !== void 0) result.attrs = attrs2;
|
|
340
|
+
if (content !== void 0 && content.length > 0) result.content = content;
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
var markOrder = new Map(MARK_TYPES.map((type, index) => [type, index]));
|
|
344
|
+
function markToTiptap(mark, ctx) {
|
|
345
|
+
switch (mark.type) {
|
|
346
|
+
case "bold":
|
|
347
|
+
case "italic":
|
|
348
|
+
case "underline":
|
|
349
|
+
case "strike":
|
|
350
|
+
case "code":
|
|
351
|
+
return { type: mark.type };
|
|
352
|
+
case "link":
|
|
353
|
+
if (typeof mark.href !== "string" || mark.href === "") {
|
|
354
|
+
warn(ctx, "invalid-mark", "A link mark without an href was dropped.", { nodeType: "link" });
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (sanitizeUrl(mark.href) === null) {
|
|
358
|
+
warn(ctx, "invalid-mark", "A link with an unsafe URL was dropped.", { nodeType: "link" });
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return { type: TIPTAP_MARKS.link, attrs: { href: mark.href } };
|
|
362
|
+
case "color":
|
|
363
|
+
case "highlight":
|
|
364
|
+
if (typeof mark.token !== "string" || mark.token === "") {
|
|
365
|
+
warn(ctx, "invalid-mark", `A ${mark.type} mark without a token was dropped.`, { nodeType: mark.type });
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
return { type: mark.type, attrs: { token: mark.token } };
|
|
369
|
+
default: {
|
|
370
|
+
const unknown = mark;
|
|
371
|
+
warn(ctx, "unknown-mark", `The mark "${String(unknown.type)}" is not supported and was dropped.`, {
|
|
372
|
+
nodeType: String(unknown.type)
|
|
373
|
+
});
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function sortMarks(marks) {
|
|
379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
380
|
+
const unique = marks.filter((mark) => {
|
|
381
|
+
if (seen.has(mark.type)) return false;
|
|
382
|
+
seen.add(mark.type);
|
|
383
|
+
return true;
|
|
384
|
+
});
|
|
385
|
+
return unique.map((mark, index) => ({ mark, index })).sort((a, b) => (markOrder.get(a.mark.type) ?? 99) - (markOrder.get(b.mark.type) ?? 99) || a.index - b.index).map((entry) => entry.mark);
|
|
386
|
+
}
|
|
387
|
+
function marksToTiptap(marks, ctx) {
|
|
388
|
+
if (!marks || marks.length === 0) return void 0;
|
|
389
|
+
const converted = [];
|
|
390
|
+
for (const mark of marks) {
|
|
391
|
+
const result = markToTiptap(mark, ctx);
|
|
392
|
+
if (result) converted.push(result);
|
|
393
|
+
}
|
|
394
|
+
const sorted = sortMarks(converted);
|
|
395
|
+
return sorted.length > 0 ? sorted : void 0;
|
|
396
|
+
}
|
|
397
|
+
function withMarks(base, marks) {
|
|
398
|
+
if (marks) base.marks = marks.map((mark) => ({ ...mark, ...mark.attrs ? { attrs: { ...mark.attrs } } : {} }));
|
|
399
|
+
return base;
|
|
400
|
+
}
|
|
401
|
+
function inlineToTiptap(inline, ctx) {
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const item of inline ?? []) {
|
|
404
|
+
if (!isRecord(item)) continue;
|
|
405
|
+
switch (item.type) {
|
|
406
|
+
case "text": {
|
|
407
|
+
const marks = marksToTiptap(item.marks, ctx);
|
|
408
|
+
const parts = str(item.text).split("\n");
|
|
409
|
+
parts.forEach((part, index) => {
|
|
410
|
+
if (index > 0) out.push(withMarks({ type: TIPTAP_NODES.hardBreak }, marks));
|
|
411
|
+
if (part !== "") out.push(withMarks({ type: TIPTAP_NODES.text, text: part }, marks));
|
|
412
|
+
});
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case "mention":
|
|
416
|
+
out.push({
|
|
417
|
+
type: TIPTAP_NODES.mention,
|
|
418
|
+
attrs: { kind: str(item.kind, "person"), id: str(item.id), label: str(item.label) }
|
|
419
|
+
});
|
|
420
|
+
break;
|
|
421
|
+
case "emoji":
|
|
422
|
+
out.push({ type: TIPTAP_NODES.emoji, attrs: { shortcode: str(item.shortcode) } });
|
|
423
|
+
break;
|
|
424
|
+
default:
|
|
425
|
+
warn(ctx, "unknown-inline", `The inline node "${String(item.type)}" was dropped.`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
function marksFromTiptap(marks, ctx) {
|
|
431
|
+
if (!marks || marks.length === 0) return [];
|
|
432
|
+
const converted = [];
|
|
433
|
+
for (const mark of marks) {
|
|
434
|
+
if (!isRecord(mark)) continue;
|
|
435
|
+
switch (mark.type) {
|
|
436
|
+
case "bold":
|
|
437
|
+
case "italic":
|
|
438
|
+
case "underline":
|
|
439
|
+
case "strike":
|
|
440
|
+
case "code":
|
|
441
|
+
converted.push({ type: mark.type });
|
|
442
|
+
break;
|
|
443
|
+
case "link": {
|
|
444
|
+
const href = optionalString(mark.attrs?.href);
|
|
445
|
+
if (href === void 0 || href === "") {
|
|
446
|
+
warn(ctx, "invalid-mark", "A link mark without an href was dropped.", { nodeType: "link" });
|
|
447
|
+
} else if (sanitizeUrl(href) === null) {
|
|
448
|
+
warn(ctx, "invalid-mark", "A link with an unsafe URL was dropped.", { nodeType: "link" });
|
|
449
|
+
} else {
|
|
450
|
+
converted.push({ type: "link", attrs: { href } });
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case "color":
|
|
455
|
+
case "highlight": {
|
|
456
|
+
const token = optionalString(mark.attrs?.token);
|
|
457
|
+
if (token === void 0 || token === "") {
|
|
458
|
+
warn(ctx, "invalid-mark", `A ${mark.type} mark without a token path was dropped.`, { nodeType: mark.type });
|
|
459
|
+
} else {
|
|
460
|
+
converted.push({ type: mark.type, attrs: { token } });
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
default:
|
|
465
|
+
warn(ctx, "unknown-mark", `The mark "${String(mark.type)}" is not supported and was dropped.`, {
|
|
466
|
+
nodeType: String(mark.type)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return sortMarks(converted).map((mark) => {
|
|
471
|
+
switch (mark.type) {
|
|
472
|
+
case "link":
|
|
473
|
+
return { type: "link", href: str(mark.attrs?.href) };
|
|
474
|
+
case "color":
|
|
475
|
+
return { type: "color", token: str(mark.attrs?.token) };
|
|
476
|
+
case "highlight":
|
|
477
|
+
return { type: "highlight", token: str(mark.attrs?.token) };
|
|
478
|
+
default:
|
|
479
|
+
return { type: mark.type };
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
function marksEqual(a, b) {
|
|
484
|
+
const left = a ?? [];
|
|
485
|
+
if (left.length !== b.length) return false;
|
|
486
|
+
return left.every((mark, index) => JSON.stringify(mark) === JSON.stringify(b[index]));
|
|
487
|
+
}
|
|
488
|
+
var MENTION_KINDS = ["person", "page", "date"];
|
|
489
|
+
function inlineFromTiptap(nodes, ctx) {
|
|
490
|
+
const out = [];
|
|
491
|
+
const pushText = (text, marks) => {
|
|
492
|
+
if (text === "") return;
|
|
493
|
+
const last = out[out.length - 1];
|
|
494
|
+
if (last && last.type === "text" && marksEqual(last.marks, marks)) {
|
|
495
|
+
last.text += text;
|
|
496
|
+
} else {
|
|
497
|
+
out.push(marks.length > 0 ? { type: "text", text, marks } : { type: "text", text });
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
for (const item of nodes ?? []) {
|
|
501
|
+
if (!isRecord(item)) continue;
|
|
502
|
+
switch (item.type) {
|
|
503
|
+
case TIPTAP_NODES.text:
|
|
504
|
+
pushText(str(item.text), marksFromTiptap(item.marks, ctx));
|
|
505
|
+
break;
|
|
506
|
+
case TIPTAP_NODES.hardBreak:
|
|
507
|
+
pushText("\n", marksFromTiptap(item.marks, ctx));
|
|
508
|
+
break;
|
|
509
|
+
case TIPTAP_NODES.mention: {
|
|
510
|
+
const kind = MENTION_KINDS.find((candidate) => candidate === item.attrs?.kind) ?? "person";
|
|
511
|
+
out.push({ type: "mention", kind, id: str(item.attrs?.id), label: str(item.attrs?.label) });
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case TIPTAP_NODES.emoji:
|
|
515
|
+
out.push({ type: "emoji", shortcode: str(item.attrs?.shortcode) });
|
|
516
|
+
break;
|
|
517
|
+
default:
|
|
518
|
+
warn(ctx, "unknown-inline", `The inline node "${item.type}" was dropped.`, { nodeType: item.type });
|
|
519
|
+
if (typeof item.text === "string") pushText(item.text, marksFromTiptap(item.marks, ctx));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
function normalizeInline(inline) {
|
|
525
|
+
const ctx = createContext(void 0);
|
|
526
|
+
return inlineFromTiptap(inlineToTiptap(inline, ctx), ctx);
|
|
527
|
+
}
|
|
528
|
+
function joinInlines(parts) {
|
|
529
|
+
const joined = [];
|
|
530
|
+
parts.forEach((part, index) => {
|
|
531
|
+
if (index > 0) joined.push({ type: "text", text: "\n" });
|
|
532
|
+
joined.push(...part);
|
|
533
|
+
});
|
|
534
|
+
return joined;
|
|
535
|
+
}
|
|
536
|
+
function plainTextOf(tiptapNode) {
|
|
537
|
+
if (typeof tiptapNode.text === "string") return tiptapNode.text;
|
|
538
|
+
if (tiptapNode.type === TIPTAP_NODES.hardBreak) return "\n";
|
|
539
|
+
const children = tiptapNode.content ?? [];
|
|
540
|
+
const separator = children.some((child) => !(child.type === TIPTAP_NODES.text || child.type === TIPTAP_NODES.hardBreak)) ? "\n" : "";
|
|
541
|
+
return children.map(plainTextOf).join(separator);
|
|
542
|
+
}
|
|
543
|
+
var EMPTY_PARAGRAPH = () => ({ type: TIPTAP_NODES.paragraph });
|
|
544
|
+
function paragraphOf(inline, ctx) {
|
|
545
|
+
return node(TIPTAP_NODES.paragraph, void 0, inlineToTiptap(inline, ctx));
|
|
546
|
+
}
|
|
547
|
+
function listItemToTiptap(item, listType, itemType, ctx) {
|
|
548
|
+
const attrs2 = { id: item.id };
|
|
549
|
+
if (itemType === TIPTAP_NODES.taskItem) attrs2.checked = item.checked === true;
|
|
550
|
+
const content = [paragraphOf(item.content, ctx)];
|
|
551
|
+
if (item.children && item.children.length > 0) {
|
|
552
|
+
content.push(
|
|
553
|
+
node(
|
|
554
|
+
listType,
|
|
555
|
+
void 0,
|
|
556
|
+
item.children.map((child) => listItemToTiptap(child, listType, itemType, ctx))
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return node(itemType, attrs2, content);
|
|
561
|
+
}
|
|
562
|
+
function listToTiptap(block, listType, itemType, ctx) {
|
|
563
|
+
const items = block.items && block.items.length > 0 ? block.items : [{ id: ctx.generateId(), content: [] }];
|
|
564
|
+
return node(
|
|
565
|
+
listType,
|
|
566
|
+
{ id: block.id },
|
|
567
|
+
items.map((item) => listItemToTiptap(item, listType, itemType, ctx))
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
function blocksToTiptap(blocks, ctx) {
|
|
571
|
+
return (blocks ?? []).map((block) => blockToTiptap(block, ctx));
|
|
572
|
+
}
|
|
573
|
+
function blockToTiptap(block, ctx) {
|
|
574
|
+
const id = block.id;
|
|
575
|
+
switch (block.type) {
|
|
576
|
+
case "paragraph":
|
|
577
|
+
return node(TIPTAP_NODES.paragraph, { id }, inlineToTiptap(block.content, ctx));
|
|
578
|
+
case "heading":
|
|
579
|
+
return node(TIPTAP_NODES.heading, { id, level: block.level }, inlineToTiptap(block.content, ctx));
|
|
580
|
+
case "quote":
|
|
581
|
+
return node(TIPTAP_NODES.blockquote, { id }, [paragraphOf(block.content, ctx)]);
|
|
582
|
+
case "callout":
|
|
583
|
+
return node(TIPTAP_NODES.callout, { id, variant: block.variant }, inlineToTiptap(block.content, ctx));
|
|
584
|
+
case "toggle":
|
|
585
|
+
return node(TIPTAP_NODES.toggle, { id }, [
|
|
586
|
+
node(TIPTAP_NODES.toggleSummary, void 0, inlineToTiptap(block.summary, ctx)),
|
|
587
|
+
...blocksToTiptap(block.children, ctx)
|
|
588
|
+
]);
|
|
589
|
+
case "divider":
|
|
590
|
+
return node(TIPTAP_NODES.horizontalRule, { id });
|
|
591
|
+
case "bulleted-list":
|
|
592
|
+
return listToTiptap(block, TIPTAP_NODES.bulletList, TIPTAP_NODES.listItem, ctx);
|
|
593
|
+
case "numbered-list":
|
|
594
|
+
return listToTiptap(block, TIPTAP_NODES.orderedList, TIPTAP_NODES.listItem, ctx);
|
|
595
|
+
case "task-list":
|
|
596
|
+
return listToTiptap(block, TIPTAP_NODES.taskList, TIPTAP_NODES.taskItem, ctx);
|
|
597
|
+
case "code":
|
|
598
|
+
return node(
|
|
599
|
+
TIPTAP_NODES.codeBlock,
|
|
600
|
+
{ id, language: nullable(block.language) },
|
|
601
|
+
block.code ? [{ type: TIPTAP_NODES.text, text: block.code }] : []
|
|
602
|
+
);
|
|
603
|
+
case "equation":
|
|
604
|
+
return node(
|
|
605
|
+
TIPTAP_NODES.equation,
|
|
606
|
+
{ id },
|
|
607
|
+
block.latex ? [{ type: TIPTAP_NODES.text, text: block.latex }] : []
|
|
608
|
+
);
|
|
609
|
+
case "table":
|
|
610
|
+
return tableToTiptap(block, ctx);
|
|
611
|
+
case "image":
|
|
612
|
+
return {
|
|
613
|
+
type: TIPTAP_NODES.image,
|
|
614
|
+
attrs: {
|
|
615
|
+
id,
|
|
616
|
+
src: block.src,
|
|
617
|
+
alt: block.alt ?? "",
|
|
618
|
+
caption: block.caption === void 0 ? null : normalizeInline(block.caption),
|
|
619
|
+
align: nullable(block.align),
|
|
620
|
+
width: nullable(block.width)
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
case "video":
|
|
624
|
+
case "audio":
|
|
625
|
+
return {
|
|
626
|
+
type: TIPTAP_NODES.media,
|
|
627
|
+
attrs: {
|
|
628
|
+
id,
|
|
629
|
+
kind: block.type,
|
|
630
|
+
src: block.src,
|
|
631
|
+
caption: block.caption === void 0 ? null : normalizeInline(block.caption)
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
case "file":
|
|
635
|
+
return { type: TIPTAP_NODES.file, attrs: { id, src: block.src, name: block.name, size: nullable(block.size) } };
|
|
636
|
+
case "bookmark":
|
|
637
|
+
return {
|
|
638
|
+
type: TIPTAP_NODES.bookmark,
|
|
639
|
+
attrs: { id, url: block.url, title: nullable(block.title), description: nullable(block.description) }
|
|
640
|
+
};
|
|
641
|
+
case "columns": {
|
|
642
|
+
const columns = block.columns && block.columns.length > 0 ? block.columns : [[]];
|
|
643
|
+
return node(
|
|
644
|
+
TIPTAP_NODES.columns,
|
|
645
|
+
{ id },
|
|
646
|
+
columns.map((column) => {
|
|
647
|
+
const content = blocksToTiptap(column, ctx);
|
|
648
|
+
return node(TIPTAP_NODES.column, void 0, content.length > 0 ? content : [EMPTY_PARAGRAPH()]);
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
case "page-break":
|
|
653
|
+
return { type: TIPTAP_NODES.pageBreak, attrs: { id } };
|
|
654
|
+
case "table-of-contents":
|
|
655
|
+
return { type: TIPTAP_NODES.tableOfContents, attrs: { id } };
|
|
656
|
+
case "chart":
|
|
657
|
+
return { type: TIPTAP_NODES.chart, attrs: { id, spec: cloneJson(block.spec) } };
|
|
658
|
+
case "diagram":
|
|
659
|
+
return {
|
|
660
|
+
type: TIPTAP_NODES.diagram,
|
|
661
|
+
attrs: {
|
|
662
|
+
id,
|
|
663
|
+
engine: block.engine,
|
|
664
|
+
diagram: block.diagram === void 0 ? null : cloneJson(block.diagram),
|
|
665
|
+
source: nullable(block.source)
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
case "data-view":
|
|
669
|
+
return { type: TIPTAP_NODES.dataView, attrs: { id, dataset: block.dataset, view: cloneJson(block.view) } };
|
|
670
|
+
case "ai":
|
|
671
|
+
return {
|
|
672
|
+
type: TIPTAP_NODES.aiBlock,
|
|
673
|
+
attrs: { id, prompt: block.prompt, result: block.result === void 0 ? null : cloneJson(block.result) }
|
|
674
|
+
};
|
|
675
|
+
default: {
|
|
676
|
+
const unknown = block;
|
|
677
|
+
const blockType = String(unknown.type);
|
|
678
|
+
const blockId = optionalString(unknown.id);
|
|
679
|
+
warn(ctx, "unknown-block", `The block type "${blockType}" is not supported; it is kept as an opaque block.`, {
|
|
680
|
+
blockId,
|
|
681
|
+
nodeType: blockType
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
type: TIPTAP_NODES.unknownBlock,
|
|
685
|
+
attrs: { id: nullable(blockId), blockType, payload: cloneJson(unknown) }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function tableToTiptap(block, ctx) {
|
|
691
|
+
const rows = block.rows && block.rows.length > 0 ? block.rows : [[{ content: [] }]];
|
|
692
|
+
const headerRow = block.headerRow;
|
|
693
|
+
const headerColumn = block.headerColumn;
|
|
694
|
+
return node(
|
|
695
|
+
TIPTAP_NODES.table,
|
|
696
|
+
{ id: block.id, headerRow: nullable(headerRow), headerColumn: nullable(headerColumn) },
|
|
697
|
+
rows.map(
|
|
698
|
+
(row, rowIndex) => node(
|
|
699
|
+
TIPTAP_NODES.tableRow,
|
|
700
|
+
void 0,
|
|
701
|
+
row.map((cell, columnIndex) => {
|
|
702
|
+
const isHeader = cell.header === true || headerRow === true && rowIndex === 0 || headerColumn === true && columnIndex === 0;
|
|
703
|
+
return node(
|
|
704
|
+
isHeader ? TIPTAP_NODES.tableHeader : TIPTAP_NODES.tableCell,
|
|
705
|
+
{ colspan: cell.colSpan ?? 1, rowspan: cell.rowSpan ?? 1, colwidth: null },
|
|
706
|
+
[paragraphOf(cell.content, ctx)]
|
|
707
|
+
);
|
|
708
|
+
})
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function readId(source, ctx) {
|
|
714
|
+
const candidate = source.attrs?.id;
|
|
715
|
+
let id = typeof candidate === "string" && candidate !== "" ? candidate : "";
|
|
716
|
+
if (id !== "" && ctx.seenIds.has(id)) {
|
|
717
|
+
warn(ctx, "duplicate-id", `The id "${id}" was used twice; the second block got a new id.`, {
|
|
718
|
+
blockId: id,
|
|
719
|
+
nodeType: source.type
|
|
720
|
+
});
|
|
721
|
+
id = "";
|
|
722
|
+
}
|
|
723
|
+
if (id === "") {
|
|
724
|
+
do {
|
|
725
|
+
id = ctx.generateId();
|
|
726
|
+
} while (ctx.seenIds.has(id));
|
|
727
|
+
}
|
|
728
|
+
ctx.seenIds.add(id);
|
|
729
|
+
return id;
|
|
730
|
+
}
|
|
731
|
+
function inlineOfTextblock(textblock, ctx) {
|
|
732
|
+
return inlineFromTiptap(textblock.content, ctx);
|
|
733
|
+
}
|
|
734
|
+
function inlineOfContainer(container, ctx, extraBlocks) {
|
|
735
|
+
const runs = [];
|
|
736
|
+
for (const child of container.content ?? []) {
|
|
737
|
+
if (child.type === TIPTAP_NODES.paragraph || child.type === TIPTAP_NODES.heading) {
|
|
738
|
+
runs.push(inlineOfTextblock(child, ctx));
|
|
739
|
+
} else if (extraBlocks) {
|
|
740
|
+
blockFromTiptap(child, ctx, extraBlocks);
|
|
741
|
+
} else {
|
|
742
|
+
const text = plainTextOf(child);
|
|
743
|
+
warn(ctx, "lossy", `A "${child.type}" inside a ${container.type} was reduced to plain text.`, {
|
|
744
|
+
nodeType: child.type
|
|
745
|
+
});
|
|
746
|
+
if (text !== "") runs.push([{ type: "text", text }]);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return normalizeInline(joinInlines(runs));
|
|
750
|
+
}
|
|
751
|
+
function listItemFromTiptap(source, parentListType, isTask, ctx) {
|
|
752
|
+
const id = readId(source, ctx);
|
|
753
|
+
const runs = [];
|
|
754
|
+
const children = [];
|
|
755
|
+
for (const child of source.content ?? []) {
|
|
756
|
+
if (child.type === TIPTAP_NODES.paragraph) {
|
|
757
|
+
runs.push(inlineOfTextblock(child, ctx));
|
|
758
|
+
} else if (child.type === TIPTAP_NODES.bulletList || child.type === TIPTAP_NODES.orderedList || child.type === TIPTAP_NODES.taskList) {
|
|
759
|
+
if (child.type !== parentListType) {
|
|
760
|
+
warn(ctx, "lossy", `A nested ${child.type} inside a ${parentListType} took the type of its parent list.`, {
|
|
761
|
+
nodeType: child.type
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
for (const nested of child.content ?? []) {
|
|
765
|
+
if (nested.type === TIPTAP_NODES.listItem || nested.type === TIPTAP_NODES.taskItem) {
|
|
766
|
+
children.push(listItemFromTiptap(nested, parentListType, isTask, ctx));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
const text = plainTextOf(child);
|
|
771
|
+
warn(ctx, "lossy", `A "${child.type}" inside a list item was reduced to plain text.`, { nodeType: child.type });
|
|
772
|
+
if (text !== "") runs.push([{ type: "text", text }]);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const content = normalizeInline(joinInlines(runs));
|
|
776
|
+
const result = isTask ? { id, content, checked: source.attrs?.checked === true } : { id, content };
|
|
777
|
+
if (children.length > 0) result.children = children;
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
function listFromTiptap(source, type, ctx) {
|
|
781
|
+
const id = readId(source, ctx);
|
|
782
|
+
const isTask = type === "task-list";
|
|
783
|
+
const items = [];
|
|
784
|
+
for (const child of source.content ?? []) {
|
|
785
|
+
if (child.type === TIPTAP_NODES.listItem || child.type === TIPTAP_NODES.taskItem) {
|
|
786
|
+
items.push(listItemFromTiptap(child, source.type, isTask, ctx));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return { id, type, items };
|
|
790
|
+
}
|
|
791
|
+
var HEADER_ATTR_VALUES = [true, false];
|
|
792
|
+
function readHint(value) {
|
|
793
|
+
return HEADER_ATTR_VALUES.find((candidate) => candidate === value) ?? null;
|
|
794
|
+
}
|
|
795
|
+
function resolveHeaderFlag(hint, allHeader, ambiguous) {
|
|
796
|
+
if (hint === true) return allHeader;
|
|
797
|
+
if (hint === false) return false;
|
|
798
|
+
return allHeader && !ambiguous ? true : void 0;
|
|
799
|
+
}
|
|
800
|
+
function tableFromTiptap(source, ctx) {
|
|
801
|
+
const id = readId(source, ctx);
|
|
802
|
+
const rowNodes = (source.content ?? []).filter((row) => row.type === TIPTAP_NODES.tableRow);
|
|
803
|
+
const grid = rowNodes.map(
|
|
804
|
+
(row) => (row.content ?? []).filter(
|
|
805
|
+
(cell) => cell.type === TIPTAP_NODES.tableCell || cell.type === TIPTAP_NODES.tableHeader
|
|
806
|
+
)
|
|
807
|
+
);
|
|
808
|
+
const isHeader = (cell) => cell.type === TIPTAP_NODES.tableHeader;
|
|
809
|
+
const firstRow = grid[0] ?? [];
|
|
810
|
+
const rowAll = firstRow.length > 0 && firstRow.every(isHeader);
|
|
811
|
+
const columnAll = grid.length > 0 && grid.every((row) => row.length > 0 && isHeader(row[0]));
|
|
812
|
+
const headerRow = resolveHeaderFlag(
|
|
813
|
+
readHint(source.attrs?.headerRow),
|
|
814
|
+
rowAll,
|
|
815
|
+
firstRow.length === 1 && columnAll
|
|
816
|
+
);
|
|
817
|
+
const headerColumn = resolveHeaderFlag(
|
|
818
|
+
readHint(source.attrs?.headerColumn),
|
|
819
|
+
columnAll,
|
|
820
|
+
grid.length === 1 && rowAll
|
|
821
|
+
);
|
|
822
|
+
const rows = grid.map(
|
|
823
|
+
(row, rowIndex) => row.map((cellNode, columnIndex) => {
|
|
824
|
+
const cell = { content: inlineOfContainer(cellNode, ctx) };
|
|
825
|
+
const colSpan = optionalNumber(cellNode.attrs?.colspan);
|
|
826
|
+
const rowSpan = optionalNumber(cellNode.attrs?.rowspan);
|
|
827
|
+
if (colSpan !== void 0 && colSpan > 1) cell.colSpan = colSpan;
|
|
828
|
+
if (rowSpan !== void 0 && rowSpan > 1) cell.rowSpan = rowSpan;
|
|
829
|
+
const coveredByRow = headerRow === true && rowIndex === 0;
|
|
830
|
+
const coveredByColumn = headerColumn === true && columnIndex === 0;
|
|
831
|
+
if (isHeader(cellNode) && !coveredByRow && !coveredByColumn) cell.header = true;
|
|
832
|
+
return cell;
|
|
833
|
+
})
|
|
834
|
+
);
|
|
835
|
+
if (rows.length === 0) rows.push([{ content: [] }]);
|
|
836
|
+
const block = { id, type: "table", rows };
|
|
837
|
+
if (headerRow !== void 0) block.headerRow = headerRow;
|
|
838
|
+
if (headerColumn !== void 0) block.headerColumn = headerColumn;
|
|
839
|
+
return block;
|
|
840
|
+
}
|
|
841
|
+
function codeTextOf(source) {
|
|
842
|
+
return (source.content ?? []).map((child) => child.type === TIPTAP_NODES.hardBreak ? "\n" : str(child.text)).join("");
|
|
843
|
+
}
|
|
844
|
+
function captionOf(value, ctx) {
|
|
845
|
+
if (!Array.isArray(value)) return void 0;
|
|
846
|
+
const inline = inlineFromTiptap(inlineToTiptap(value, ctx), ctx);
|
|
847
|
+
return inline;
|
|
848
|
+
}
|
|
849
|
+
var ALIGNMENTS = ["start", "center", "end"];
|
|
850
|
+
var DIAGRAM_ENGINES = ["mermaid", "canvas", "excalidraw"];
|
|
851
|
+
function blockFromTiptap(source, ctx, out) {
|
|
852
|
+
if (!isRecord(source)) return;
|
|
853
|
+
switch (source.type) {
|
|
854
|
+
case TIPTAP_NODES.paragraph:
|
|
855
|
+
out.push({ id: readId(source, ctx), type: "paragraph", content: inlineOfTextblock(source, ctx) });
|
|
856
|
+
return;
|
|
857
|
+
case TIPTAP_NODES.heading: {
|
|
858
|
+
const id = readId(source, ctx);
|
|
859
|
+
const raw = optionalNumber(source.attrs?.level) ?? 1;
|
|
860
|
+
const level = Math.min(4, Math.max(1, Math.round(raw)));
|
|
861
|
+
if (level !== raw) {
|
|
862
|
+
warn(ctx, "invalid-value", `Heading level ${raw} was clamped to ${level}.`, { blockId: id, nodeType: "heading" });
|
|
863
|
+
}
|
|
864
|
+
out.push({ id, type: "heading", level, content: inlineOfTextblock(source, ctx) });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
case TIPTAP_NODES.blockquote: {
|
|
868
|
+
const id = readId(source, ctx);
|
|
869
|
+
const trailing = [];
|
|
870
|
+
const content = inlineOfContainer(source, ctx, trailing);
|
|
871
|
+
const hasText = (source.content ?? []).some(
|
|
872
|
+
(child) => child.type === TIPTAP_NODES.paragraph || child.type === TIPTAP_NODES.heading
|
|
873
|
+
);
|
|
874
|
+
if (hasText || trailing.length === 0) out.push({ id, type: "quote", content });
|
|
875
|
+
out.push(...trailing);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
case TIPTAP_NODES.callout: {
|
|
879
|
+
const id = readId(source, ctx);
|
|
880
|
+
const raw = source.attrs?.variant;
|
|
881
|
+
const variant = CALLOUT_VARIANTS.find((candidate) => candidate === raw) ?? "info";
|
|
882
|
+
if (variant !== raw) {
|
|
883
|
+
warn(ctx, "invalid-value", `Callout variant "${String(raw)}" became "info".`, { blockId: id, nodeType: "callout" });
|
|
884
|
+
}
|
|
885
|
+
out.push({ id, type: "callout", variant, content: inlineOfTextblock(source, ctx) });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
case TIPTAP_NODES.toggle: {
|
|
889
|
+
const id = readId(source, ctx);
|
|
890
|
+
const [first, ...rest] = source.content ?? [];
|
|
891
|
+
const hasSummary = first?.type === TIPTAP_NODES.toggleSummary;
|
|
892
|
+
const children = [];
|
|
893
|
+
for (const child of hasSummary ? rest : source.content ?? []) blockFromTiptap(child, ctx, children);
|
|
894
|
+
out.push({
|
|
895
|
+
id,
|
|
896
|
+
type: "toggle",
|
|
897
|
+
summary: hasSummary && first ? inlineOfTextblock(first, ctx) : [],
|
|
898
|
+
children
|
|
899
|
+
});
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
case TIPTAP_NODES.horizontalRule:
|
|
903
|
+
out.push({ id: readId(source, ctx), type: "divider" });
|
|
904
|
+
return;
|
|
905
|
+
case TIPTAP_NODES.bulletList:
|
|
906
|
+
out.push(listFromTiptap(source, "bulleted-list", ctx));
|
|
907
|
+
return;
|
|
908
|
+
case TIPTAP_NODES.orderedList:
|
|
909
|
+
out.push(listFromTiptap(source, "numbered-list", ctx));
|
|
910
|
+
return;
|
|
911
|
+
case TIPTAP_NODES.taskList:
|
|
912
|
+
out.push(listFromTiptap(source, "task-list", ctx));
|
|
913
|
+
return;
|
|
914
|
+
case TIPTAP_NODES.codeBlock: {
|
|
915
|
+
const block = {
|
|
916
|
+
id: readId(source, ctx),
|
|
917
|
+
type: "code",
|
|
918
|
+
code: codeTextOf(source)
|
|
919
|
+
};
|
|
920
|
+
const language = optionalString(source.attrs?.language);
|
|
921
|
+
if (language !== void 0) block.language = language;
|
|
922
|
+
out.push(block);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
case TIPTAP_NODES.equation:
|
|
926
|
+
out.push({ id: readId(source, ctx), type: "equation", latex: codeTextOf(source) });
|
|
927
|
+
return;
|
|
928
|
+
case TIPTAP_NODES.table:
|
|
929
|
+
out.push(tableFromTiptap(source, ctx));
|
|
930
|
+
return;
|
|
931
|
+
case TIPTAP_NODES.image: {
|
|
932
|
+
const block = {
|
|
933
|
+
id: readId(source, ctx),
|
|
934
|
+
type: "image",
|
|
935
|
+
src: safeUrl(source.attrs?.src, ctx, true),
|
|
936
|
+
alt: str(source.attrs?.alt)
|
|
937
|
+
};
|
|
938
|
+
const caption = captionOf(source.attrs?.caption, ctx);
|
|
939
|
+
if (caption !== void 0) block.caption = caption;
|
|
940
|
+
const align = ALIGNMENTS.find((candidate) => candidate === source.attrs?.align);
|
|
941
|
+
if (align !== void 0) block.align = align;
|
|
942
|
+
const width = optionalNumber(source.attrs?.width);
|
|
943
|
+
if (width !== void 0) block.width = width;
|
|
944
|
+
out.push(block);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
case TIPTAP_NODES.media: {
|
|
948
|
+
const block = {
|
|
949
|
+
id: readId(source, ctx),
|
|
950
|
+
type: source.attrs?.kind === "audio" ? "audio" : "video",
|
|
951
|
+
src: safeUrl(source.attrs?.src, ctx, true)
|
|
952
|
+
};
|
|
953
|
+
const caption = captionOf(source.attrs?.caption, ctx);
|
|
954
|
+
if (caption !== void 0) block.caption = caption;
|
|
955
|
+
out.push(block);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
case TIPTAP_NODES.file: {
|
|
959
|
+
const block = {
|
|
960
|
+
id: readId(source, ctx),
|
|
961
|
+
type: "file",
|
|
962
|
+
src: safeUrl(source.attrs?.src, ctx, false),
|
|
963
|
+
name: str(source.attrs?.name)
|
|
964
|
+
};
|
|
965
|
+
const size = optionalNumber(source.attrs?.size);
|
|
966
|
+
if (size !== void 0) block.size = size;
|
|
967
|
+
out.push(block);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
case TIPTAP_NODES.bookmark: {
|
|
971
|
+
const block = {
|
|
972
|
+
id: readId(source, ctx),
|
|
973
|
+
type: "bookmark",
|
|
974
|
+
url: safeUrl(source.attrs?.url, ctx, false)
|
|
975
|
+
};
|
|
976
|
+
const title = optionalString(source.attrs?.title);
|
|
977
|
+
const description = optionalString(source.attrs?.description);
|
|
978
|
+
if (title !== void 0) block.title = title;
|
|
979
|
+
if (description !== void 0) block.description = description;
|
|
980
|
+
out.push(block);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
case TIPTAP_NODES.columns: {
|
|
984
|
+
const id = readId(source, ctx);
|
|
985
|
+
const columns = [];
|
|
986
|
+
for (const column of source.content ?? []) {
|
|
987
|
+
if (column.type !== TIPTAP_NODES.column) continue;
|
|
988
|
+
const blocks = [];
|
|
989
|
+
for (const child of column.content ?? []) blockFromTiptap(child, ctx, blocks);
|
|
990
|
+
columns.push(blocks);
|
|
991
|
+
}
|
|
992
|
+
out.push({ id, type: "columns", columns });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
case TIPTAP_NODES.pageBreak:
|
|
996
|
+
out.push({ id: readId(source, ctx), type: "page-break" });
|
|
997
|
+
return;
|
|
998
|
+
case TIPTAP_NODES.tableOfContents:
|
|
999
|
+
out.push({ id: readId(source, ctx), type: "table-of-contents" });
|
|
1000
|
+
return;
|
|
1001
|
+
case TIPTAP_NODES.chart:
|
|
1002
|
+
out.push({
|
|
1003
|
+
id: readId(source, ctx),
|
|
1004
|
+
type: "chart",
|
|
1005
|
+
spec: cloneJson(source.attrs?.spec)
|
|
1006
|
+
});
|
|
1007
|
+
return;
|
|
1008
|
+
case TIPTAP_NODES.diagram: {
|
|
1009
|
+
const block = {
|
|
1010
|
+
id: readId(source, ctx),
|
|
1011
|
+
type: "diagram",
|
|
1012
|
+
engine: DIAGRAM_ENGINES.find((candidate) => candidate === source.attrs?.engine) ?? "mermaid"
|
|
1013
|
+
};
|
|
1014
|
+
if (isRecord(source.attrs?.diagram)) block.diagram = cloneJson(source.attrs?.diagram);
|
|
1015
|
+
const diagramSource = optionalString(source.attrs?.source);
|
|
1016
|
+
if (diagramSource !== void 0) block.source = diagramSource;
|
|
1017
|
+
out.push(block);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
case TIPTAP_NODES.dataView:
|
|
1021
|
+
out.push({
|
|
1022
|
+
id: readId(source, ctx),
|
|
1023
|
+
type: "data-view",
|
|
1024
|
+
dataset: str(source.attrs?.dataset),
|
|
1025
|
+
view: cloneJson(source.attrs?.view)
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
case TIPTAP_NODES.aiBlock: {
|
|
1029
|
+
const block = {
|
|
1030
|
+
id: readId(source, ctx),
|
|
1031
|
+
type: "ai",
|
|
1032
|
+
prompt: str(source.attrs?.prompt)
|
|
1033
|
+
};
|
|
1034
|
+
if (Array.isArray(source.attrs?.result)) block.result = cloneJson(source.attrs?.result);
|
|
1035
|
+
out.push(block);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
case TIPTAP_NODES.unknownBlock: {
|
|
1039
|
+
const payload = source.attrs?.payload;
|
|
1040
|
+
if (isRecord(payload)) {
|
|
1041
|
+
const copy = cloneJson(payload);
|
|
1042
|
+
const id = optionalString(copy.id) ?? optionalString(source.attrs?.id) ?? ctx.generateId();
|
|
1043
|
+
copy.id = id;
|
|
1044
|
+
ctx.seenIds.add(id);
|
|
1045
|
+
warn(ctx, "unknown-block", `The block type "${String(copy.type)}" is not supported; it was kept as is.`, {
|
|
1046
|
+
blockId: id,
|
|
1047
|
+
nodeType: String(copy.type)
|
|
1048
|
+
});
|
|
1049
|
+
out.push(copy);
|
|
1050
|
+
}
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
case TIPTAP_NODES.text:
|
|
1054
|
+
case TIPTAP_NODES.hardBreak:
|
|
1055
|
+
case TIPTAP_NODES.mention:
|
|
1056
|
+
case TIPTAP_NODES.emoji:
|
|
1057
|
+
out.push({ id: ctx.generateId(), type: "paragraph", content: inlineFromTiptap([source], ctx) });
|
|
1058
|
+
return;
|
|
1059
|
+
default: {
|
|
1060
|
+
const children = source.content ?? [];
|
|
1061
|
+
const hasInlineOnly = children.length > 0 && children.every(
|
|
1062
|
+
(child) => child.type === TIPTAP_NODES.text || child.type === TIPTAP_NODES.hardBreak || child.type === TIPTAP_NODES.mention || child.type === TIPTAP_NODES.emoji
|
|
1063
|
+
);
|
|
1064
|
+
warn(ctx, "unknown-node", `The node "${source.type}" is not supported and was converted to plain blocks.`, {
|
|
1065
|
+
nodeType: source.type
|
|
1066
|
+
});
|
|
1067
|
+
if (hasInlineOnly) {
|
|
1068
|
+
out.push({ id: readId(source, ctx), type: "paragraph", content: inlineOfTextblock(source, ctx) });
|
|
1069
|
+
} else {
|
|
1070
|
+
for (const child of children) blockFromTiptap(child, ctx, out);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function documentToTiptap(doc, options) {
|
|
1076
|
+
const ctx = createContext(options);
|
|
1077
|
+
const content = blocksToTiptap(doc.blocks, ctx);
|
|
1078
|
+
const result = {
|
|
1079
|
+
type: "doc",
|
|
1080
|
+
content: content.length > 0 ? content : [{ type: TIPTAP_NODES.paragraph, attrs: { id: ctx.generateId() } }]
|
|
1081
|
+
};
|
|
1082
|
+
if (doc.title !== void 0) result.attrs = { title: doc.title };
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
function tiptapToDocument(json, options) {
|
|
1086
|
+
const ctx = createContext(options);
|
|
1087
|
+
const blocks = [];
|
|
1088
|
+
const top = json.type === "doc" ? json.content ?? [] : [json];
|
|
1089
|
+
for (const child of top) blockFromTiptap(child, ctx, blocks);
|
|
1090
|
+
const doc = { version: DOCUMENT_VERSION, blocks };
|
|
1091
|
+
const title = optionalString(json.attrs?.title);
|
|
1092
|
+
if (title !== void 0) doc.title = title;
|
|
1093
|
+
return doc;
|
|
1094
|
+
}
|
|
1095
|
+
function normalizeDocument(doc, options) {
|
|
1096
|
+
const ctx = { ...options };
|
|
1097
|
+
return tiptapToDocument(documentToTiptap(doc, ctx), ctx);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/emoji.ts
|
|
1101
|
+
var EMOJI_SHORTCODES = {
|
|
1102
|
+
smile: "\u{1F604}",
|
|
1103
|
+
grin: "\u{1F601}",
|
|
1104
|
+
joy: "\u{1F602}",
|
|
1105
|
+
wink: "\u{1F609}",
|
|
1106
|
+
heart: "\u2764\uFE0F",
|
|
1107
|
+
thumbsup: "\u{1F44D}",
|
|
1108
|
+
"+1": "\u{1F44D}",
|
|
1109
|
+
thumbsdown: "\u{1F44E}",
|
|
1110
|
+
"-1": "\u{1F44E}",
|
|
1111
|
+
clap: "\u{1F44F}",
|
|
1112
|
+
pray: "\u{1F64F}",
|
|
1113
|
+
fire: "\u{1F525}",
|
|
1114
|
+
rocket: "\u{1F680}",
|
|
1115
|
+
tada: "\u{1F389}",
|
|
1116
|
+
eyes: "\u{1F440}",
|
|
1117
|
+
thinking: "\u{1F914}",
|
|
1118
|
+
sparkles: "\u2728",
|
|
1119
|
+
star: "\u2B50",
|
|
1120
|
+
warning: "\u26A0\uFE0F",
|
|
1121
|
+
check: "\u2705",
|
|
1122
|
+
white_check_mark: "\u2705",
|
|
1123
|
+
x: "\u274C",
|
|
1124
|
+
bulb: "\u{1F4A1}",
|
|
1125
|
+
memo: "\u{1F4DD}",
|
|
1126
|
+
book: "\u{1F4D6}",
|
|
1127
|
+
calendar: "\u{1F4C5}",
|
|
1128
|
+
lock: "\u{1F512}",
|
|
1129
|
+
link: "\u{1F517}",
|
|
1130
|
+
pushpin: "\u{1F4CC}",
|
|
1131
|
+
hourglass: "\u231B",
|
|
1132
|
+
zap: "\u26A1",
|
|
1133
|
+
bug: "\u{1F41B}",
|
|
1134
|
+
wave: "\u{1F44B}",
|
|
1135
|
+
muscle: "\u{1F4AA}",
|
|
1136
|
+
ok_hand: "\u{1F44C}",
|
|
1137
|
+
coffee: "\u2615",
|
|
1138
|
+
sunny: "\u2600\uFE0F",
|
|
1139
|
+
question: "\u2753",
|
|
1140
|
+
exclamation: "\u2757"
|
|
1141
|
+
};
|
|
1142
|
+
function emojiForShortcode(shortcode) {
|
|
1143
|
+
const key = shortcode.replace(/^:|:$/g, "").toLowerCase();
|
|
1144
|
+
return EMOJI_SHORTCODES[key] ?? null;
|
|
1145
|
+
}
|
|
1146
|
+
function emojiToText(shortcode) {
|
|
1147
|
+
return emojiForShortcode(shortcode) ?? `:${shortcode.replace(/^:|:$/g, "")}:`;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/text.ts
|
|
1151
|
+
function inlineToPlainText(inline) {
|
|
1152
|
+
let text = "";
|
|
1153
|
+
for (const node2 of inline ?? []) {
|
|
1154
|
+
if (node2.type === "text") text += node2.text;
|
|
1155
|
+
else if (node2.type === "mention") text += `@${node2.label}`;
|
|
1156
|
+
else if (node2.type === "emoji") text += emojiToText(node2.shortcode);
|
|
1157
|
+
}
|
|
1158
|
+
return text;
|
|
1159
|
+
}
|
|
1160
|
+
function indent(depth) {
|
|
1161
|
+
return " ".repeat(depth);
|
|
1162
|
+
}
|
|
1163
|
+
function listLines(items, kind, markers, depth) {
|
|
1164
|
+
const lines = [];
|
|
1165
|
+
items.forEach((item, index) => {
|
|
1166
|
+
let marker = "";
|
|
1167
|
+
if (markers) {
|
|
1168
|
+
if (kind === "bulleted") marker = "- ";
|
|
1169
|
+
else if (kind === "numbered") marker = `${index + 1}. `;
|
|
1170
|
+
else marker = `[${item.checked ? "x" : " "}] `;
|
|
1171
|
+
}
|
|
1172
|
+
lines.push(`${markers ? indent(depth) : ""}${marker}${inlineToPlainText(item.content)}`);
|
|
1173
|
+
if (item.children) lines.push(...listLines(item.children, kind, markers, depth + 1));
|
|
1174
|
+
});
|
|
1175
|
+
return lines;
|
|
1176
|
+
}
|
|
1177
|
+
function blockText(block, options) {
|
|
1178
|
+
switch (block.type) {
|
|
1179
|
+
case "paragraph":
|
|
1180
|
+
case "heading":
|
|
1181
|
+
case "quote":
|
|
1182
|
+
case "callout":
|
|
1183
|
+
return inlineToPlainText(block.content);
|
|
1184
|
+
case "toggle": {
|
|
1185
|
+
const parts = [inlineToPlainText(block.summary), ...blocksText(block.children ?? [], options)];
|
|
1186
|
+
return parts.filter((part) => part !== "").join(options.blockSeparator);
|
|
1187
|
+
}
|
|
1188
|
+
case "divider":
|
|
1189
|
+
case "page-break":
|
|
1190
|
+
case "table-of-contents":
|
|
1191
|
+
return "";
|
|
1192
|
+
case "bulleted-list":
|
|
1193
|
+
return listLines(block.items ?? [], "bulleted", options.markers, 0).join("\n");
|
|
1194
|
+
case "numbered-list":
|
|
1195
|
+
return listLines(block.items ?? [], "numbered", options.markers, 0).join("\n");
|
|
1196
|
+
case "task-list":
|
|
1197
|
+
return listLines(block.items ?? [], "task", options.markers, 0).join("\n");
|
|
1198
|
+
case "code":
|
|
1199
|
+
return block.code;
|
|
1200
|
+
case "equation":
|
|
1201
|
+
return block.latex;
|
|
1202
|
+
case "table":
|
|
1203
|
+
return (block.rows ?? []).map((row) => row.map((cell) => inlineToPlainText(cell.content)).join(" ")).join("\n");
|
|
1204
|
+
case "image":
|
|
1205
|
+
return [block.alt, inlineToPlainText(block.caption)].filter((part) => part !== "").join("\n");
|
|
1206
|
+
case "video":
|
|
1207
|
+
case "audio":
|
|
1208
|
+
return inlineToPlainText(block.caption);
|
|
1209
|
+
case "file":
|
|
1210
|
+
return block.name;
|
|
1211
|
+
case "bookmark":
|
|
1212
|
+
return [block.title, block.description, block.url].filter((part) => !!part).join("\n");
|
|
1213
|
+
case "columns":
|
|
1214
|
+
return (block.columns ?? []).flatMap((column) => blocksText(column, options)).join(options.blockSeparator);
|
|
1215
|
+
case "chart":
|
|
1216
|
+
return [block.spec?.title, block.spec?.subtitle].filter((part) => !!part).join("\n");
|
|
1217
|
+
case "diagram":
|
|
1218
|
+
return block.diagram?.title ?? block.source ?? "";
|
|
1219
|
+
case "data-view":
|
|
1220
|
+
return [block.view?.name, block.dataset].filter((part) => !!part).join("\n");
|
|
1221
|
+
case "ai":
|
|
1222
|
+
return [block.prompt, ...blocksText(block.result ?? [], options)].filter((part) => part !== "").join(options.blockSeparator);
|
|
1223
|
+
default:
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function blocksText(blocks, options) {
|
|
1228
|
+
return blocks.map((block) => blockText(block, options)).filter((text) => text !== "");
|
|
1229
|
+
}
|
|
1230
|
+
function documentToPlainText(doc, options = {}) {
|
|
1231
|
+
const resolved = {
|
|
1232
|
+
blockSeparator: options.blockSeparator ?? "\n\n",
|
|
1233
|
+
markers: options.markers ?? true
|
|
1234
|
+
};
|
|
1235
|
+
return blocksText(doc.blocks ?? [], resolved).join(resolved.blockSeparator);
|
|
1236
|
+
}
|
|
1237
|
+
function getSegmenter() {
|
|
1238
|
+
return Intl.Segmenter;
|
|
1239
|
+
}
|
|
1240
|
+
function countWords(text, locale) {
|
|
1241
|
+
if (text.trim() === "") return 0;
|
|
1242
|
+
const Segmenter = getSegmenter();
|
|
1243
|
+
if (Segmenter) {
|
|
1244
|
+
const segmenter = new Segmenter(locale, { granularity: "word" });
|
|
1245
|
+
let count = 0;
|
|
1246
|
+
for (const part of segmenter.segment(text)) if (part.isWordLike) count += 1;
|
|
1247
|
+
return count;
|
|
1248
|
+
}
|
|
1249
|
+
return text.trim().split(/\s+/).length;
|
|
1250
|
+
}
|
|
1251
|
+
function countCharacters(text, options = {}) {
|
|
1252
|
+
const source = options.includeSpaces === false ? text.replace(/\s+/g, "") : text;
|
|
1253
|
+
const Segmenter = getSegmenter();
|
|
1254
|
+
if (Segmenter) {
|
|
1255
|
+
let count = 0;
|
|
1256
|
+
for (const _ of new Segmenter(void 0, { granularity: "grapheme" }).segment(source)) {
|
|
1257
|
+
void _;
|
|
1258
|
+
count += 1;
|
|
1259
|
+
}
|
|
1260
|
+
return count;
|
|
1261
|
+
}
|
|
1262
|
+
return [...source].length;
|
|
1263
|
+
}
|
|
1264
|
+
function getDocumentStats(doc, options = {}) {
|
|
1265
|
+
const text = documentToPlainText(doc, { markers: false, blockSeparator: "\n" });
|
|
1266
|
+
const words = countWords(text, options.locale);
|
|
1267
|
+
return {
|
|
1268
|
+
words,
|
|
1269
|
+
characters: countCharacters(text.replace(/\n/g, " ")),
|
|
1270
|
+
charactersNoSpaces: countCharacters(text, { includeSpaces: false }),
|
|
1271
|
+
blocks: doc.blocks?.length ?? 0,
|
|
1272
|
+
readingMinutes: words === 0 ? 0 : Math.max(1, Math.ceil(words / 200))
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/outline.ts
|
|
1277
|
+
function getOutline(doc, options = {}) {
|
|
1278
|
+
const items = [];
|
|
1279
|
+
const maxLevel = options.maxLevel ?? 4;
|
|
1280
|
+
walkBlocks(doc.blocks ?? [], (block) => {
|
|
1281
|
+
if (block.type !== "heading" || block.level > maxLevel) return;
|
|
1282
|
+
const text = inlineToPlainText(block.content).trim();
|
|
1283
|
+
if (text === "" && options.includeEmpty !== true) return;
|
|
1284
|
+
items.push({ id: block.id, level: block.level, text });
|
|
1285
|
+
});
|
|
1286
|
+
return items;
|
|
1287
|
+
}
|
|
1288
|
+
function buildOutlineTree(items) {
|
|
1289
|
+
const roots = [];
|
|
1290
|
+
const stack = [];
|
|
1291
|
+
for (const item of items) {
|
|
1292
|
+
const entry = { ...item, children: [] };
|
|
1293
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= entry.level) stack.pop();
|
|
1294
|
+
const parent = stack[stack.length - 1];
|
|
1295
|
+
if (parent) parent.children.push(entry);
|
|
1296
|
+
else roots.push(entry);
|
|
1297
|
+
stack.push(entry);
|
|
1298
|
+
}
|
|
1299
|
+
return roots;
|
|
1300
|
+
}
|
|
1301
|
+
function slugifyHeading(text) {
|
|
1302
|
+
return text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}\s-]+/gu, "").trim().replace(/[\s-]+/g, "-");
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/html.ts
|
|
1306
|
+
var defaultExportLabels = {
|
|
1307
|
+
tableOfContents: "Table of contents",
|
|
1308
|
+
chart: "Chart",
|
|
1309
|
+
diagram: "Diagram",
|
|
1310
|
+
dataView: "Data view",
|
|
1311
|
+
ai: "AI",
|
|
1312
|
+
file: "File"
|
|
1313
|
+
};
|
|
1314
|
+
var ESCAPES = {
|
|
1315
|
+
"&": "&",
|
|
1316
|
+
"<": "<",
|
|
1317
|
+
">": ">",
|
|
1318
|
+
'"': """,
|
|
1319
|
+
"'": "'"
|
|
1320
|
+
};
|
|
1321
|
+
function escapeHtml(value) {
|
|
1322
|
+
return value.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
1323
|
+
}
|
|
1324
|
+
function attrs(values) {
|
|
1325
|
+
let out = "";
|
|
1326
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1327
|
+
if (value === void 0 || value === null || value === false) continue;
|
|
1328
|
+
out += value === true ? ` ${name}` : ` ${name}="${escapeHtml(String(value))}"`;
|
|
1329
|
+
}
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
function safeToken(value) {
|
|
1333
|
+
return value.replace(/[^a-zA-Z0-9_+.-]/g, "");
|
|
1334
|
+
}
|
|
1335
|
+
function applyMark(html, mark) {
|
|
1336
|
+
switch (mark.type) {
|
|
1337
|
+
case "bold":
|
|
1338
|
+
return `<strong>${html}</strong>`;
|
|
1339
|
+
case "italic":
|
|
1340
|
+
return `<em>${html}</em>`;
|
|
1341
|
+
case "underline":
|
|
1342
|
+
return `<u>${html}</u>`;
|
|
1343
|
+
case "strike":
|
|
1344
|
+
return `<s>${html}</s>`;
|
|
1345
|
+
case "code":
|
|
1346
|
+
return `<code>${html}</code>`;
|
|
1347
|
+
case "link": {
|
|
1348
|
+
const href = sanitizeUrl(mark.href);
|
|
1349
|
+
return href === null ? html : `<a${attrs({ href, rel: "noopener noreferrer" })}>${html}</a>`;
|
|
1350
|
+
}
|
|
1351
|
+
case "color": {
|
|
1352
|
+
const css = textColorToCss(mark.token);
|
|
1353
|
+
return css === null ? html : `<span${attrs({ "data-kit": "editor-text-color", "data-token": mark.token, style: `color: ${css}` })}>${html}</span>`;
|
|
1354
|
+
}
|
|
1355
|
+
case "highlight": {
|
|
1356
|
+
const css = highlightToCss(mark.token);
|
|
1357
|
+
return css === null ? html : `<mark${attrs({ "data-kit": "editor-highlight", "data-token": mark.token, style: `background-color: ${css}` })}>${html}</mark>`;
|
|
1358
|
+
}
|
|
1359
|
+
default:
|
|
1360
|
+
return html;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
var APPLY_ORDER = ["code", "strike", "underline", "italic", "bold", "highlight", "color", "link"];
|
|
1364
|
+
function renderInline(inline) {
|
|
1365
|
+
let html = "";
|
|
1366
|
+
for (const node2 of inline ?? []) {
|
|
1367
|
+
if (node2.type === "text") {
|
|
1368
|
+
let part = escapeHtml(node2.text).replace(/\n/g, "<br>");
|
|
1369
|
+
const marks = node2.marks ?? [];
|
|
1370
|
+
for (const type of APPLY_ORDER) {
|
|
1371
|
+
const mark = marks.find((candidate) => candidate.type === type);
|
|
1372
|
+
if (mark) part = applyMark(part, mark);
|
|
1373
|
+
}
|
|
1374
|
+
html += part;
|
|
1375
|
+
} else if (node2.type === "mention") {
|
|
1376
|
+
html += `<span${attrs({ "data-kit": "editor-mention", "data-kind": node2.kind, "data-id": node2.id })}>@${escapeHtml(node2.label)}</span>`;
|
|
1377
|
+
} else if (node2.type === "emoji") {
|
|
1378
|
+
html += `<span${attrs({ "data-kit": "editor-emoji", "data-shortcode": node2.shortcode })}>${escapeHtml(emojiToText(node2.shortcode))}</span>`;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return html;
|
|
1382
|
+
}
|
|
1383
|
+
function renderItems(items, kind, ctx) {
|
|
1384
|
+
const tag = kind === "ol" ? "ol" : "ul";
|
|
1385
|
+
const open = kind === "task" ? `<ul${attrs({ "data-kit": "editor-task-list" })}>` : `<${tag}>`;
|
|
1386
|
+
let html = open;
|
|
1387
|
+
for (const item of items) {
|
|
1388
|
+
const nested = item.children && item.children.length > 0 ? renderItems(item.children, kind, ctx) : "";
|
|
1389
|
+
if (kind === "task") {
|
|
1390
|
+
const checked = item.checked === true;
|
|
1391
|
+
const labelId = `${ctx.idPrefix}${item.id}-label`;
|
|
1392
|
+
html += `<li${attrs({ "data-kit": "editor-task-item", "data-checked": checked ? "true" : "false" })}>`;
|
|
1393
|
+
html += `<input${attrs({ type: "checkbox", disabled: true, checked, "aria-labelledby": labelId })}>`;
|
|
1394
|
+
html += `<span${attrs({ id: labelId, dir: "auto" })}>${renderInline(item.content)}</span>${nested}</li>`;
|
|
1395
|
+
} else {
|
|
1396
|
+
html += `<li${attrs({ dir: "auto" })}>${renderInline(item.content)}${nested}</li>`;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return `${html}</${tag}>`;
|
|
1400
|
+
}
|
|
1401
|
+
function formatBytes(size, locale) {
|
|
1402
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
1403
|
+
let value = size;
|
|
1404
|
+
let unit = 0;
|
|
1405
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1406
|
+
value /= 1024;
|
|
1407
|
+
unit += 1;
|
|
1408
|
+
}
|
|
1409
|
+
const formatted = new Intl.NumberFormat(locale, { maximumFractionDigits: unit === 0 ? 0 : 1 }).format(value);
|
|
1410
|
+
return `${formatted} ${units[unit]}`;
|
|
1411
|
+
}
|
|
1412
|
+
function renderCaption(caption) {
|
|
1413
|
+
return caption && caption.length > 0 ? `<figcaption dir="auto">${renderInline(caption)}</figcaption>` : "";
|
|
1414
|
+
}
|
|
1415
|
+
function renderOutline(nodes, ctx) {
|
|
1416
|
+
if (nodes.length === 0) return "";
|
|
1417
|
+
let html = "<ol>";
|
|
1418
|
+
for (const node2 of nodes) {
|
|
1419
|
+
html += `<li><a href="#${escapeHtml(ctx.idPrefix + node2.id)}">${escapeHtml(node2.text)}</a>${renderOutline(node2.children, ctx)}</li>`;
|
|
1420
|
+
}
|
|
1421
|
+
return `${html}</ol>`;
|
|
1422
|
+
}
|
|
1423
|
+
function renderEmbed(blockId, type, title, subtitle, label, body = "") {
|
|
1424
|
+
const heading = `<figcaption><span data-kit="editor-embed-kind">${escapeHtml(label)}</span> ${escapeHtml(title)}${subtitle ? ` <small>${escapeHtml(subtitle)}</small>` : ""}</figcaption>`;
|
|
1425
|
+
return `<figure${attrs({ "data-block-id": blockId, "data-kit": "editor-embed", "data-block-type": type, role: "group", "aria-label": title || label })}>${heading}${body}</figure>`;
|
|
1426
|
+
}
|
|
1427
|
+
function renderBlock(block, ctx) {
|
|
1428
|
+
const id = attrs({ "data-block-id": block.id });
|
|
1429
|
+
switch (block.type) {
|
|
1430
|
+
case "paragraph":
|
|
1431
|
+
return `<p${id}${attrs({ dir: "auto" })}>${renderInline(block.content)}</p>`;
|
|
1432
|
+
case "heading": {
|
|
1433
|
+
const level = Math.min(4, Math.max(1, Math.round(block.level)));
|
|
1434
|
+
return `<h${level}${attrs({ id: ctx.idPrefix + block.id, dir: "auto" })}${id}>${renderInline(block.content)}</h${level}>`;
|
|
1435
|
+
}
|
|
1436
|
+
case "quote":
|
|
1437
|
+
return `<blockquote${id}${attrs({ dir: "auto" })}>${renderInline(block.content)}</blockquote>`;
|
|
1438
|
+
case "callout":
|
|
1439
|
+
return `<div${id}${attrs({ "data-kit": "editor-callout", "data-variant": safeToken(block.variant), role: "note", dir: "auto" })}>${renderInline(block.content)}</div>`;
|
|
1440
|
+
case "toggle":
|
|
1441
|
+
return `<details${id}${attrs({ "data-kit": "editor-toggle" })}><summary${attrs({ dir: "auto" })}>${renderInline(block.summary)}</summary>${renderBlocks(block.children ?? [], ctx)}</details>`;
|
|
1442
|
+
case "divider":
|
|
1443
|
+
return `<hr${id}>`;
|
|
1444
|
+
case "bulleted-list":
|
|
1445
|
+
return renderListBlock(block, "ul", ctx);
|
|
1446
|
+
case "numbered-list":
|
|
1447
|
+
return renderListBlock(block, "ol", ctx);
|
|
1448
|
+
case "task-list":
|
|
1449
|
+
return renderListBlock(block, "task", ctx);
|
|
1450
|
+
case "code": {
|
|
1451
|
+
const language = block.language ? safeToken(block.language) : "";
|
|
1452
|
+
return `<pre${id}${attrs({ "data-kit": "editor-code-block", "data-language": language || void 0, dir: "ltr" })}><code${attrs({ class: language ? `language-${language}` : void 0 })}>${escapeHtml(block.code)}</code></pre>`;
|
|
1453
|
+
}
|
|
1454
|
+
case "equation":
|
|
1455
|
+
return `<pre${id}${attrs({ "data-kit": "editor-equation", dir: "ltr" })}><code>${escapeHtml(block.latex)}</code></pre>`;
|
|
1456
|
+
case "table":
|
|
1457
|
+
return renderTable(block);
|
|
1458
|
+
case "image": {
|
|
1459
|
+
const src = sanitizeMediaUrl(block.src);
|
|
1460
|
+
const img = src === null ? "" : `<img${attrs({ src, alt: block.alt ?? "", loading: "lazy", decoding: "async", width: block.width })}>`;
|
|
1461
|
+
return `<figure${id}${attrs({ "data-kit": "editor-image", "data-align": block.align })}>${img}${renderCaption(block.caption)}</figure>`;
|
|
1462
|
+
}
|
|
1463
|
+
case "video":
|
|
1464
|
+
case "audio": {
|
|
1465
|
+
const src = sanitizeMediaUrl(block.src);
|
|
1466
|
+
const media = src === null ? "" : `<${block.type}${attrs({ src, controls: true, preload: "none" })}></${block.type}>`;
|
|
1467
|
+
return `<figure${id}${attrs({ "data-kit": "editor-media", "data-kind": block.type })}>${media}${renderCaption(block.caption)}</figure>`;
|
|
1468
|
+
}
|
|
1469
|
+
case "file": {
|
|
1470
|
+
const href = sanitizeUrl(block.src);
|
|
1471
|
+
const name = escapeHtml(block.name);
|
|
1472
|
+
const link = href === null ? name : `<a${attrs({ href, download: true, rel: "noopener noreferrer" })}>${name}</a>`;
|
|
1473
|
+
const size = typeof block.size === "number" ? ` <span data-kit="editor-file-size">(${escapeHtml(formatBytes(block.size, ctx.locale))})</span>` : "";
|
|
1474
|
+
return `<p${id}${attrs({ "data-kit": "editor-file", dir: "auto" })}>${link}${size}</p>`;
|
|
1475
|
+
}
|
|
1476
|
+
case "bookmark": {
|
|
1477
|
+
const href = sanitizeUrl(block.url);
|
|
1478
|
+
const title = block.title ? `<strong>${escapeHtml(block.title)}</strong>` : escapeHtml(block.url);
|
|
1479
|
+
const description = block.description ? `<span>${escapeHtml(block.description)}</span>` : "";
|
|
1480
|
+
const inner = `${title}${description}`;
|
|
1481
|
+
return href === null ? `<div${id}${attrs({ "data-kit": "editor-bookmark" })}>${inner}</div>` : `<a${id}${attrs({ "data-kit": "editor-bookmark", href, rel: "noopener noreferrer" })}>${inner}</a>`;
|
|
1482
|
+
}
|
|
1483
|
+
case "columns": {
|
|
1484
|
+
const columns = block.columns ?? [];
|
|
1485
|
+
return `<div${id}${attrs({ "data-kit": "editor-columns", "data-count": columns.length })}>${columns.map((column) => `<div data-kit="editor-column">${renderBlocks(column, ctx)}</div>`).join("")}</div>`;
|
|
1486
|
+
}
|
|
1487
|
+
case "page-break":
|
|
1488
|
+
return `<hr${id}${attrs({ "data-kit": "editor-page-break" })}>`;
|
|
1489
|
+
case "table-of-contents":
|
|
1490
|
+
return `<nav${id}${attrs({ "data-kit": "editor-toc", "aria-label": ctx.labels.tableOfContents })}>${renderOutline(ctx.outline, ctx)}</nav>`;
|
|
1491
|
+
case "chart":
|
|
1492
|
+
return renderEmbed(
|
|
1493
|
+
block.id,
|
|
1494
|
+
"chart",
|
|
1495
|
+
block.spec?.title ?? "",
|
|
1496
|
+
block.spec?.subtitle,
|
|
1497
|
+
ctx.labels.chart,
|
|
1498
|
+
block.spec?.a11y?.description ? `<p dir="auto">${escapeHtml(block.spec.a11y.description)}</p>` : ""
|
|
1499
|
+
);
|
|
1500
|
+
case "diagram": {
|
|
1501
|
+
const title = block.diagram?.title ?? "";
|
|
1502
|
+
const body = block.source ? `<pre dir="ltr"><code>${escapeHtml(block.source)}</code></pre>` : "";
|
|
1503
|
+
return renderEmbed(block.id, "diagram", title, void 0, ctx.labels.diagram, body);
|
|
1504
|
+
}
|
|
1505
|
+
case "data-view":
|
|
1506
|
+
return renderEmbed(block.id, "data-view", block.view?.name ?? block.dataset, block.view?.name ? block.dataset : void 0, ctx.labels.dataView);
|
|
1507
|
+
case "ai":
|
|
1508
|
+
return renderEmbed(block.id, "ai", block.prompt, void 0, ctx.labels.ai, renderBlocks(block.result ?? [], ctx));
|
|
1509
|
+
default: {
|
|
1510
|
+
const unknownType = String(block.type);
|
|
1511
|
+
return `<div${id}${attrs({ "data-kit": "editor-unknown", "data-block-type": unknownType, hidden: true })}></div>`;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function renderListBlock(block, kind, ctx) {
|
|
1516
|
+
const html = renderItems(block.items ?? [], kind, ctx);
|
|
1517
|
+
return html.replace(/^<(ul|ol)/, `<$1${attrs({ "data-block-id": block.id })}`);
|
|
1518
|
+
}
|
|
1519
|
+
function renderTable(block) {
|
|
1520
|
+
const rows = block.rows ?? [];
|
|
1521
|
+
const renderRow = (row, rowIndex) => `<tr>${row.map((cell, columnIndex) => {
|
|
1522
|
+
const header = cell.header === true || block.headerRow === true && rowIndex === 0 || block.headerColumn === true && columnIndex === 0;
|
|
1523
|
+
const tag = header ? "th" : "td";
|
|
1524
|
+
let scope;
|
|
1525
|
+
if (header && block.headerRow === true && rowIndex === 0) scope = "col";
|
|
1526
|
+
else if (header && block.headerColumn === true && columnIndex === 0) scope = "row";
|
|
1527
|
+
return `<${tag}${attrs({
|
|
1528
|
+
scope,
|
|
1529
|
+
colspan: cell.colSpan && cell.colSpan > 1 ? cell.colSpan : void 0,
|
|
1530
|
+
rowspan: cell.rowSpan && cell.rowSpan > 1 ? cell.rowSpan : void 0,
|
|
1531
|
+
dir: "auto"
|
|
1532
|
+
})}>${renderInline(cell.content)}</${tag}>`;
|
|
1533
|
+
}).join("")}</tr>`;
|
|
1534
|
+
const headRow = block.headerRow === true ? rows[0] : void 0;
|
|
1535
|
+
const bodyRows = headRow ? rows.slice(1) : rows;
|
|
1536
|
+
const thead = headRow ? `<thead>${renderRow(headRow, 0)}</thead>` : "";
|
|
1537
|
+
const tbody = `<tbody>${bodyRows.map((row, index) => renderRow(row, headRow ? index + 1 : index)).join("")}</tbody>`;
|
|
1538
|
+
return `<div${attrs({ "data-kit": "editor-table-wrapper", "data-block-id": block.id })}><table${attrs({ "data-kit": "editor-table" })}>${thead}${tbody}</table></div>`;
|
|
1539
|
+
}
|
|
1540
|
+
function renderBlocks(blocks, ctx) {
|
|
1541
|
+
return blocks.map((block) => renderBlock(block, ctx)).join("");
|
|
1542
|
+
}
|
|
1543
|
+
function documentToHtml(doc, options = {}) {
|
|
1544
|
+
const ctx = {
|
|
1545
|
+
idPrefix: options.idPrefix ?? "kit-",
|
|
1546
|
+
labels: { ...defaultExportLabels, ...options.labels },
|
|
1547
|
+
locale: options.locale,
|
|
1548
|
+
outline: buildOutlineTree(getOutline(doc))
|
|
1549
|
+
};
|
|
1550
|
+
const html = renderBlocks(doc.blocks ?? [], ctx);
|
|
1551
|
+
return options.wrap === true ? `<div data-kit="editor-view">${html}</div>` : html;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/markdown.ts
|
|
1555
|
+
function escapeMarkdown(text) {
|
|
1556
|
+
return text.replace(/([\\`*_[\]<>~|])/g, "\\$1");
|
|
1557
|
+
}
|
|
1558
|
+
function oneLine(text) {
|
|
1559
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1560
|
+
}
|
|
1561
|
+
function escapeLineStart(line) {
|
|
1562
|
+
return line.replace(/^(\s*)([#>+-])/, "$1\\$2").replace(/^(\s*)(\d+)([.)])(\s|$)/, "$1$2\\$3$4").replace(/^(\s*)([=]{2,}|[-]{2,})\s*$/, "$1\\$2").replace(/^(\s*)(:{1}[a-z0-9_+-]+:)/i, "$1\\$2");
|
|
1563
|
+
}
|
|
1564
|
+
function codeSpan(text) {
|
|
1565
|
+
const runs = text.match(/`+/g) ?? [];
|
|
1566
|
+
const longest = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
1567
|
+
const fence2 = "`".repeat(longest + 1);
|
|
1568
|
+
const pad = text.startsWith("`") || text.endsWith("`") || text.startsWith(" ") && text.endsWith(" ") && text.trim() !== "" ? " " : "";
|
|
1569
|
+
return `${fence2}${pad}${text}${pad}${fence2}`;
|
|
1570
|
+
}
|
|
1571
|
+
function encodeDestination(href) {
|
|
1572
|
+
return href.replace(/ /g, "%20").replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/</g, "%3C").replace(/>/g, "%3E");
|
|
1573
|
+
}
|
|
1574
|
+
function markKey(mark) {
|
|
1575
|
+
return JSON.stringify(mark);
|
|
1576
|
+
}
|
|
1577
|
+
function openToken(mark, ctx) {
|
|
1578
|
+
switch (mark.type) {
|
|
1579
|
+
case "bold":
|
|
1580
|
+
return "**";
|
|
1581
|
+
case "italic":
|
|
1582
|
+
return "*";
|
|
1583
|
+
case "strike":
|
|
1584
|
+
return "~~";
|
|
1585
|
+
case "underline":
|
|
1586
|
+
return ctx.inlineHtml ? "<u>" : "";
|
|
1587
|
+
case "link":
|
|
1588
|
+
return sanitizeUrl(mark.href) === null ? "" : "[";
|
|
1589
|
+
case "color":
|
|
1590
|
+
return ctx.inlineHtml && isTextColorToken(mark.token) ? `<span data-color-token="${escapeHtml(mark.token)}">` : "";
|
|
1591
|
+
case "highlight":
|
|
1592
|
+
return ctx.inlineHtml && isHighlightToken(mark.token) ? `<mark data-token="${escapeHtml(mark.token)}">` : "";
|
|
1593
|
+
default:
|
|
1594
|
+
return "";
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function closeToken(mark, ctx) {
|
|
1598
|
+
switch (mark.type) {
|
|
1599
|
+
case "bold":
|
|
1600
|
+
return "**";
|
|
1601
|
+
case "italic":
|
|
1602
|
+
return "*";
|
|
1603
|
+
case "strike":
|
|
1604
|
+
return "~~";
|
|
1605
|
+
case "underline":
|
|
1606
|
+
return ctx.inlineHtml ? "</u>" : "";
|
|
1607
|
+
case "link": {
|
|
1608
|
+
const href = sanitizeUrl(mark.href);
|
|
1609
|
+
return href === null ? "" : `](${encodeDestination(href)})`;
|
|
1610
|
+
}
|
|
1611
|
+
case "color":
|
|
1612
|
+
return ctx.inlineHtml && isTextColorToken(mark.token) ? "</span>" : "";
|
|
1613
|
+
case "highlight":
|
|
1614
|
+
return ctx.inlineHtml && isHighlightToken(mark.token) ? "</mark>" : "";
|
|
1615
|
+
default:
|
|
1616
|
+
return "";
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function renderInline2(inline, ctx, escapeLines = true) {
|
|
1620
|
+
const nodes = normalizeInline(inline);
|
|
1621
|
+
let out = "";
|
|
1622
|
+
let active = [];
|
|
1623
|
+
const closeFrom = (keep) => {
|
|
1624
|
+
for (let index = active.length - 1; index >= keep; index -= 1) {
|
|
1625
|
+
const mark = active[index];
|
|
1626
|
+
if (mark.type === "code") continue;
|
|
1627
|
+
const token = closeToken(mark, ctx);
|
|
1628
|
+
if (token === "") continue;
|
|
1629
|
+
const trailing = /\s+$/.exec(out)?.[0] ?? "";
|
|
1630
|
+
if (trailing !== "" && mark.type !== "link") out = out.slice(0, out.length - trailing.length);
|
|
1631
|
+
out += token + (mark.type !== "link" ? trailing : "");
|
|
1632
|
+
}
|
|
1633
|
+
active = active.slice(0, keep);
|
|
1634
|
+
};
|
|
1635
|
+
for (const node2 of nodes) {
|
|
1636
|
+
if (node2.type !== "text") {
|
|
1637
|
+
closeFrom(0);
|
|
1638
|
+
if (node2.type === "mention") {
|
|
1639
|
+
out += `[@${escapeMarkdown(node2.label)}](kit:mention/${node2.kind}/${encodeURIComponent(node2.id)})`;
|
|
1640
|
+
} else {
|
|
1641
|
+
const code = node2.shortcode.replace(/^:|:$/g, "");
|
|
1642
|
+
out += emojiForShortcode(code) !== null ? `:${code}:` : escapeMarkdown(`:${code}:`);
|
|
1643
|
+
}
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
const marks = (node2.marks ?? []).filter((mark) => mark.type !== "code");
|
|
1647
|
+
const hasCode = (node2.marks ?? []).some((mark) => mark.type === "code");
|
|
1648
|
+
let keep = 0;
|
|
1649
|
+
while (keep < active.length && keep < marks.length && markKey(active[keep]) === markKey(marks[keep])) keep += 1;
|
|
1650
|
+
closeFrom(keep);
|
|
1651
|
+
let text = node2.text;
|
|
1652
|
+
let lead = "";
|
|
1653
|
+
if (marks.length > keep) {
|
|
1654
|
+
const match = /^\s+/.exec(text);
|
|
1655
|
+
if (match && text.trim() !== "") {
|
|
1656
|
+
lead = match[0];
|
|
1657
|
+
text = text.slice(lead.length);
|
|
1658
|
+
}
|
|
1659
|
+
if (text.trim() === "") {
|
|
1660
|
+
out += node2.text.replace(/\n/g, "\\\n");
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
out += lead;
|
|
1665
|
+
for (let index = keep; index < marks.length; index += 1) {
|
|
1666
|
+
const mark = marks[index];
|
|
1667
|
+
out += openToken(mark, ctx) ?? "";
|
|
1668
|
+
active.push(mark);
|
|
1669
|
+
}
|
|
1670
|
+
if (hasCode && !text.includes("\n")) out += codeSpan(text);
|
|
1671
|
+
else
|
|
1672
|
+
out += text.split("\n").map((part, position) => position === 0 || !escapeLines ? escapeMarkdown(part) : escapeLineStart(escapeMarkdown(part))).join("\\\n");
|
|
1673
|
+
}
|
|
1674
|
+
closeFrom(0);
|
|
1675
|
+
return out;
|
|
1676
|
+
}
|
|
1677
|
+
function renderInlineLine(inline, ctx, newline) {
|
|
1678
|
+
return renderInline2(inline, ctx, false).replace(/\\\n/g, newline).replace(/\n/g, newline);
|
|
1679
|
+
}
|
|
1680
|
+
var CALLOUT_LABEL = {
|
|
1681
|
+
info: "INFO",
|
|
1682
|
+
success: "SUCCESS",
|
|
1683
|
+
warning: "WARNING",
|
|
1684
|
+
danger: "DANGER",
|
|
1685
|
+
neutral: "NEUTRAL"
|
|
1686
|
+
};
|
|
1687
|
+
function fence(content, info) {
|
|
1688
|
+
const runs = content.match(/`+/g) ?? [];
|
|
1689
|
+
const longest = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
1690
|
+
const marker = "`".repeat(Math.max(3, longest + 1));
|
|
1691
|
+
return `${marker}${info}
|
|
1692
|
+
${content}
|
|
1693
|
+
${marker}`;
|
|
1694
|
+
}
|
|
1695
|
+
function safeInfo(value) {
|
|
1696
|
+
return value.replace(/[^a-zA-Z0-9_+.#-]/g, "");
|
|
1697
|
+
}
|
|
1698
|
+
function prefixLines(text, first, rest) {
|
|
1699
|
+
return text.split("\n").map((line, index) => (index === 0 ? first : rest) + line).join("\n");
|
|
1700
|
+
}
|
|
1701
|
+
function renderItems2(items, kind, ctx) {
|
|
1702
|
+
const lines = [];
|
|
1703
|
+
items.forEach((item, index) => {
|
|
1704
|
+
const marker = kind === "numbered" ? `${index + 1}. ` : "- ";
|
|
1705
|
+
const checkbox = kind === "task" ? `[${item.checked ? "x" : " "}] ` : "";
|
|
1706
|
+
const body = escapeLineStart(renderInline2(item.content, ctx));
|
|
1707
|
+
const pad = " ".repeat(marker.length);
|
|
1708
|
+
lines.push(prefixLines(`${checkbox}${body}`, marker, pad).replace(/ +$/, ""));
|
|
1709
|
+
if (item.children && item.children.length > 0) {
|
|
1710
|
+
const nested = renderItems2(item.children, kind, ctx).join("\n");
|
|
1711
|
+
lines.push(prefixLines(nested, pad, pad));
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
return lines;
|
|
1715
|
+
}
|
|
1716
|
+
function renderTable2(block, ctx) {
|
|
1717
|
+
const rows = block.rows ?? [];
|
|
1718
|
+
const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
1719
|
+
if (rows.length === 0 || width === 0) return "";
|
|
1720
|
+
const cell = (inline) => renderInlineLine(inline, ctx, "<br>").replace(/\|/g, "\\|") || " ";
|
|
1721
|
+
const toLine = (row) => `| ${Array.from({ length: width }, (_, index) => row[index] ? cell(row[index]?.content) : " ").join(" | ")} |`;
|
|
1722
|
+
const [head, ...body] = rows;
|
|
1723
|
+
const delimiter = `| ${Array.from({ length: width }, () => "---").join(" | ")} |`;
|
|
1724
|
+
return [toLine(head ?? []), delimiter, ...body.map(toLine)].join("\n");
|
|
1725
|
+
}
|
|
1726
|
+
function renderBlock2(block, ctx) {
|
|
1727
|
+
switch (block.type) {
|
|
1728
|
+
case "paragraph": {
|
|
1729
|
+
const text = renderInline2(block.content, ctx);
|
|
1730
|
+
return text === "" ? "" : escapeLineStart(text);
|
|
1731
|
+
}
|
|
1732
|
+
case "heading": {
|
|
1733
|
+
const level = Math.min(4, Math.max(1, Math.round(block.level)));
|
|
1734
|
+
return `${"#".repeat(level)} ${renderInlineLine(block.content, ctx, " ")}`.trimEnd();
|
|
1735
|
+
}
|
|
1736
|
+
case "quote":
|
|
1737
|
+
return prefixLines(escapeLineStart(renderInline2(block.content, ctx)), "> ", "> ");
|
|
1738
|
+
case "callout": {
|
|
1739
|
+
const label = CALLOUT_LABEL[block.variant] ?? "INFO";
|
|
1740
|
+
const body = escapeLineStart(renderInline2(block.content, ctx));
|
|
1741
|
+
return prefixLines(`[!${label}]${body === "" ? "" : `
|
|
1742
|
+
${body}`}`, "> ", "> ");
|
|
1743
|
+
}
|
|
1744
|
+
case "toggle": {
|
|
1745
|
+
const summary = escapeHtml(
|
|
1746
|
+
normalizeInline(block.summary).map((node2) => node2.type === "text" ? node2.text : node2.type === "mention" ? `@${node2.label}` : emojiToText(node2.shortcode)).join("").replace(/\n/g, " ")
|
|
1747
|
+
);
|
|
1748
|
+
const body = renderBlocks2(block.children ?? [], ctx);
|
|
1749
|
+
return `<details>
|
|
1750
|
+
<summary>${summary}</summary>
|
|
1751
|
+
${body === "" ? "" : `
|
|
1752
|
+
${body}
|
|
1753
|
+
`}
|
|
1754
|
+
</details>`;
|
|
1755
|
+
}
|
|
1756
|
+
case "divider":
|
|
1757
|
+
return "---";
|
|
1758
|
+
case "bulleted-list":
|
|
1759
|
+
return renderItems2(block.items ?? [], "bulleted", ctx).join("\n");
|
|
1760
|
+
case "numbered-list":
|
|
1761
|
+
return renderItems2(block.items ?? [], "numbered", ctx).join("\n");
|
|
1762
|
+
case "task-list":
|
|
1763
|
+
return renderItems2(block.items ?? [], "task", ctx).join("\n");
|
|
1764
|
+
case "code":
|
|
1765
|
+
return fence(block.code, safeInfo(block.language ?? ""));
|
|
1766
|
+
case "equation":
|
|
1767
|
+
return `$$
|
|
1768
|
+
${block.latex}
|
|
1769
|
+
$$`;
|
|
1770
|
+
case "table":
|
|
1771
|
+
return renderTable2(block, ctx);
|
|
1772
|
+
case "image": {
|
|
1773
|
+
const src = sanitizeMediaUrl(block.src);
|
|
1774
|
+
const alt = escapeMarkdown(oneLine(block.alt ?? ""));
|
|
1775
|
+
if (src === null) return alt;
|
|
1776
|
+
const caption = normalizeInline(block.caption).map((node2) => node2.type === "text" ? node2.text : "").join("").replace(/\s+/g, " ").trim();
|
|
1777
|
+
const title = caption === "" ? "" : ` "${caption.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1778
|
+
return `}${title})`;
|
|
1779
|
+
}
|
|
1780
|
+
case "video":
|
|
1781
|
+
case "audio": {
|
|
1782
|
+
const src = sanitizeMediaUrl(block.src);
|
|
1783
|
+
if (src === null) return "";
|
|
1784
|
+
const caption = normalizeInline(block.caption).map((node2) => node2.type === "text" ? node2.text : "").join("").replace(/\s+/g, " ").trim();
|
|
1785
|
+
return `<${block.type} src="${escapeHtml(src)}"${caption === "" ? "" : ` title="${escapeHtml(caption)}"`} controls></${block.type}>`;
|
|
1786
|
+
}
|
|
1787
|
+
case "file": {
|
|
1788
|
+
const href = sanitizeUrl(block.src);
|
|
1789
|
+
const name = escapeMarkdown(oneLine(block.name));
|
|
1790
|
+
return href === null ? name : `[${name}](${encodeDestination(href)})`;
|
|
1791
|
+
}
|
|
1792
|
+
case "bookmark": {
|
|
1793
|
+
const href = sanitizeUrl(block.url);
|
|
1794
|
+
const label = escapeMarkdown(oneLine(block.title || block.url));
|
|
1795
|
+
return href === null ? label : `[${label}](${encodeDestination(href)})`;
|
|
1796
|
+
}
|
|
1797
|
+
case "columns":
|
|
1798
|
+
return (block.columns ?? []).map((column) => renderBlocks2(column, ctx)).filter((text) => text !== "").join("\n\n");
|
|
1799
|
+
case "page-break":
|
|
1800
|
+
return "<!-- page-break -->";
|
|
1801
|
+
case "table-of-contents":
|
|
1802
|
+
return "[TOC]";
|
|
1803
|
+
case "chart":
|
|
1804
|
+
return fence(JSON.stringify(block.spec, null, 2), "kit-chart");
|
|
1805
|
+
case "diagram":
|
|
1806
|
+
if (block.engine === "mermaid" && typeof block.source === "string" && block.diagram === void 0) {
|
|
1807
|
+
return fence(block.source, "mermaid");
|
|
1808
|
+
}
|
|
1809
|
+
return fence(JSON.stringify({ engine: block.engine, diagram: block.diagram, source: block.source }, null, 2), "kit-diagram");
|
|
1810
|
+
case "data-view":
|
|
1811
|
+
return fence(JSON.stringify({ dataset: block.dataset, view: block.view }, null, 2), "kit-data-view");
|
|
1812
|
+
case "ai":
|
|
1813
|
+
return fence(JSON.stringify({ prompt: block.prompt, result: block.result }, null, 2), "kit-ai");
|
|
1814
|
+
default:
|
|
1815
|
+
return fence(JSON.stringify(block, null, 2), "kit-block");
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
var LIST_TYPES = /* @__PURE__ */ new Set(["bulleted-list", "numbered-list", "task-list"]);
|
|
1819
|
+
function flattenColumns(blocks) {
|
|
1820
|
+
return blocks.flatMap((block) => block.type === "columns" ? flattenColumns((block.columns ?? []).flat()) : [block]);
|
|
1821
|
+
}
|
|
1822
|
+
function renderBlocks2(blocks, ctx) {
|
|
1823
|
+
const parts = [];
|
|
1824
|
+
let previous;
|
|
1825
|
+
for (const block of flattenColumns(blocks)) {
|
|
1826
|
+
const text = renderBlock2(block, ctx);
|
|
1827
|
+
if (text === "") continue;
|
|
1828
|
+
if (previous && block.type === previous.type && LIST_TYPES.has(block.type)) parts.push("<!-- -->");
|
|
1829
|
+
parts.push(text);
|
|
1830
|
+
previous = block;
|
|
1831
|
+
}
|
|
1832
|
+
return parts.join("\n\n");
|
|
1833
|
+
}
|
|
1834
|
+
function documentToMarkdown(doc, options = {}) {
|
|
1835
|
+
const ctx = { inlineHtml: options.inlineHtml ?? true };
|
|
1836
|
+
const body = renderBlocks2(doc.blocks ?? [], ctx);
|
|
1837
|
+
return body === "" ? "" : `${body}
|
|
1838
|
+
`;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
export {
|
|
1842
|
+
DOCUMENT_VERSION,
|
|
1843
|
+
createEmptyDocument,
|
|
1844
|
+
BLOCK_TYPES,
|
|
1845
|
+
MARK_TYPES,
|
|
1846
|
+
CALLOUT_VARIANTS,
|
|
1847
|
+
isKnownBlockType,
|
|
1848
|
+
createParagraph,
|
|
1849
|
+
generateBlockId,
|
|
1850
|
+
createSeededRandom,
|
|
1851
|
+
createSequentialIdGenerator,
|
|
1852
|
+
walkBlocks,
|
|
1853
|
+
collectIds,
|
|
1854
|
+
findDuplicateIds,
|
|
1855
|
+
sanitizeUrl,
|
|
1856
|
+
isSafeUrl,
|
|
1857
|
+
sanitizeMediaUrl,
|
|
1858
|
+
normalizeUrlInput,
|
|
1859
|
+
TEXT_COLOR_TOKENS,
|
|
1860
|
+
HIGHLIGHT_TOKENS,
|
|
1861
|
+
isTextColorToken,
|
|
1862
|
+
isHighlightToken,
|
|
1863
|
+
tokenToCssVarName,
|
|
1864
|
+
textColorToCss,
|
|
1865
|
+
highlightToCss,
|
|
1866
|
+
TIPTAP_NODES,
|
|
1867
|
+
TIPTAP_MARKS,
|
|
1868
|
+
BLOCK_ID_NODES,
|
|
1869
|
+
normalizeInline,
|
|
1870
|
+
documentToTiptap,
|
|
1871
|
+
tiptapToDocument,
|
|
1872
|
+
normalizeDocument,
|
|
1873
|
+
EMOJI_SHORTCODES,
|
|
1874
|
+
emojiForShortcode,
|
|
1875
|
+
emojiToText,
|
|
1876
|
+
inlineToPlainText,
|
|
1877
|
+
documentToPlainText,
|
|
1878
|
+
countWords,
|
|
1879
|
+
countCharacters,
|
|
1880
|
+
getDocumentStats,
|
|
1881
|
+
getOutline,
|
|
1882
|
+
buildOutlineTree,
|
|
1883
|
+
slugifyHeading,
|
|
1884
|
+
defaultExportLabels,
|
|
1885
|
+
escapeHtml,
|
|
1886
|
+
documentToHtml,
|
|
1887
|
+
escapeMarkdown,
|
|
1888
|
+
documentToMarkdown
|
|
1889
|
+
};
|
|
1890
|
+
//# sourceMappingURL=chunk-FYXMOAJT.js.map
|