comark-ui 0.0.0-stage → 0.7.1

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.
Files changed (76) hide show
  1. package/LICENSE +69 -0
  2. package/README.md +70 -2
  3. package/dist/client.d.mts +12 -0
  4. package/dist/client.mjs +26 -0
  5. package/dist/editor-BAdok66p.mjs +835 -0
  6. package/dist/editor-CNivEuMH.d.mts +131 -0
  7. package/dist/editor-vue.d.mts +57 -0
  8. package/dist/editor-vue.mjs +85 -0
  9. package/dist/editor.d.mts +2 -0
  10. package/dist/editor.mjs +1 -0
  11. package/dist/html.d.mts +7 -0
  12. package/dist/html.mjs +90 -0
  13. package/dist/index-B0YvApci.d.mts +173 -0
  14. package/dist/index.d.mts +3 -0
  15. package/dist/index.mjs +3 -0
  16. package/dist/nuxt-ui.d.mts +17 -0
  17. package/dist/nuxt-ui.mjs +488 -0
  18. package/dist/react.d.mts +8 -0
  19. package/dist/react.mjs +83 -0
  20. package/dist/skills/comark-ui/SKILL.md +138 -0
  21. package/dist/skills/comark-ui/agents/openai.yaml +3 -0
  22. package/dist/skills/comark-ui/references/annotate.md +65 -0
  23. package/dist/skills/comark-ui/references/badge.md +52 -0
  24. package/dist/skills/comark-ui/references/cards.md +57 -0
  25. package/dist/skills/comark-ui/references/carousel.md +31 -0
  26. package/dist/skills/comark-ui/references/cast.md +48 -0
  27. package/dist/skills/comark-ui/references/charts.md +80 -0
  28. package/dist/skills/comark-ui/references/chat.md +25 -0
  29. package/dist/skills/comark-ui/references/chips.md +33 -0
  30. package/dist/skills/comark-ui/references/cjk.md +23 -0
  31. package/dist/skills/comark-ui/references/code-notes.md +34 -0
  32. package/dist/skills/comark-ui/references/code.md +84 -0
  33. package/dist/skills/comark-ui/references/colors.md +33 -0
  34. package/dist/skills/comark-ui/references/critic.md +30 -0
  35. package/dist/skills/comark-ui/references/deflist.md +38 -0
  36. package/dist/skills/comark-ui/references/details.md +49 -0
  37. package/dist/skills/comark-ui/references/diagrams.md +45 -0
  38. package/dist/skills/comark-ui/references/diff.md +43 -0
  39. package/dist/skills/comark-ui/references/embed.md +40 -0
  40. package/dist/skills/comark-ui/references/fields.md +48 -0
  41. package/dist/skills/comark-ui/references/figure.md +25 -0
  42. package/dist/skills/comark-ui/references/file-tree.md +42 -0
  43. package/dist/skills/comark-ui/references/footnotes.md +26 -0
  44. package/dist/skills/comark-ui/references/github.md +44 -0
  45. package/dist/skills/comark-ui/references/icon.md +28 -0
  46. package/dist/skills/comark-ui/references/include.md +44 -0
  47. package/dist/skills/comark-ui/references/keys.md +28 -0
  48. package/dist/skills/comark-ui/references/links.md +43 -0
  49. package/dist/skills/comark-ui/references/lint.md +96 -0
  50. package/dist/skills/comark-ui/references/magic-move.md +34 -0
  51. package/dist/skills/comark-ui/references/mark.md +22 -0
  52. package/dist/skills/comark-ui/references/math.md +25 -0
  53. package/dist/skills/comark-ui/references/media.md +29 -0
  54. package/dist/skills/comark-ui/references/metrics.md +34 -0
  55. package/dist/skills/comark-ui/references/outline.md +36 -0
  56. package/dist/skills/comark-ui/references/properties.md +32 -0
  57. package/dist/skills/comark-ui/references/qr.md +20 -0
  58. package/dist/skills/comark-ui/references/quiz.md +45 -0
  59. package/dist/skills/comark-ui/references/reading-time.md +32 -0
  60. package/dist/skills/comark-ui/references/sources.md +24 -0
  61. package/dist/skills/comark-ui/references/steps.md +34 -0
  62. package/dist/skills/comark-ui/references/table.md +48 -0
  63. package/dist/skills/comark-ui/references/tabs.md +46 -0
  64. package/dist/skills/comark-ui/references/tests.md +59 -0
  65. package/dist/skills/comark-ui/references/ui.md +54 -0
  66. package/dist/skills/comark-ui/references/xref.md +48 -0
  67. package/dist/skills/comark-ui/references/zoom.md +35 -0
  68. package/dist/src-DROB05U6.mjs +119 -0
  69. package/dist/svelte/Typeset.svelte +7 -0
  70. package/dist/svelte/index.d.ts +9 -0
  71. package/dist/svelte/index.js +84 -0
  72. package/dist/typeset-css-DPB0S1bV.mjs +202 -0
  73. package/dist/typeset-css-DdOGL7WF.d.mts +10 -0
  74. package/dist/vue.d.mts +7 -0
  75. package/dist/vue.mjs +86 -0
  76. package/package.json +170 -3
@@ -0,0 +1,835 @@
1
+ import { parseMarkdown } from "comark";
2
+ import { Extension, Node } from "@tiptap/core";
3
+ import Image from "@tiptap/extension-image";
4
+ import { TaskItem, TaskList } from "@tiptap/extension-list";
5
+ import { TableKit } from "@tiptap/extension-table";
6
+ import StarterKit from "@tiptap/starter-kit";
7
+ import { renderMarkdown } from "comark/render";
8
+ import { textContent } from "comark/utils";
9
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
10
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
11
+ //#region src/editor/kit.ts
12
+ const typesWithProps = [
13
+ "paragraph",
14
+ "heading",
15
+ "blockquote",
16
+ "bulletList",
17
+ "orderedList",
18
+ "listItem",
19
+ "taskList",
20
+ "taskItem",
21
+ "codeBlock",
22
+ "horizontalRule",
23
+ "hardBreak",
24
+ "image",
25
+ "table",
26
+ "tableRow",
27
+ "tableHeader",
28
+ "tableCell",
29
+ "bold",
30
+ "italic",
31
+ "strike",
32
+ "code",
33
+ "link"
34
+ ];
35
+ /** Comark attributes the editor doesn't show, carried as they were parsed so writing the node back keeps them. */
36
+ const props = {
37
+ default: null,
38
+ rendered: false
39
+ };
40
+ const name = (attribute) => ({
41
+ default: "",
42
+ parseHTML: (element) => element.getAttribute(attribute) ?? "",
43
+ renderHTML: (attrs) => ({ [attribute]: attrs.name })
44
+ });
45
+ function mediaKind(src) {
46
+ const path = (src ?? "").replace(/[?#].*$/, "");
47
+ return /\.(?:mp4|webm|mov|m4v)$/i.test(path) ? "video" : /\.(?:mp3|wav|ogg|m4a)$/i.test(path) ? "audio" : void 0;
48
+ }
49
+ /** Markdown's image, inline as Comark parses it, drawn as a player when comark-media would play it. */
50
+ const ComarkImage = Image.extend({ renderHTML({ node, HTMLAttributes }) {
51
+ const kind = mediaKind(node.attrs.src);
52
+ if (!kind) return this.parent?.({
53
+ node,
54
+ HTMLAttributes
55
+ }) ?? ["img", HTMLAttributes];
56
+ return [kind, {
57
+ src: node.attrs.src,
58
+ title: node.attrs.title,
59
+ controls: "",
60
+ preload: "metadata"
61
+ }];
62
+ } }).configure({ inline: true });
63
+ /** A block component, `::name`, or a block of raw HTML. Its children are blocks and its `#slot`s. */
64
+ const ComarkBlock = Node.create({
65
+ name: "comarkBlock",
66
+ group: "block",
67
+ content: "(block | comarkSlot)*",
68
+ defining: true,
69
+ addAttributes: () => ({
70
+ name: name("data-comark-block"),
71
+ props
72
+ }),
73
+ parseHTML: () => [{ tag: "div[data-comark-block]" }],
74
+ renderHTML: ({ HTMLAttributes }) => [
75
+ "div",
76
+ HTMLAttributes,
77
+ 0
78
+ ]
79
+ });
80
+ /** A component's named slot, `#name`. */
81
+ const ComarkSlot = Node.create({
82
+ name: "comarkSlot",
83
+ content: "block*",
84
+ defining: true,
85
+ addAttributes: () => ({
86
+ name: name("data-comark-slot"),
87
+ props
88
+ }),
89
+ parseHTML: () => [{ tag: "div[data-comark-slot]" }],
90
+ renderHTML: ({ HTMLAttributes }) => [
91
+ "div",
92
+ HTMLAttributes,
93
+ 0
94
+ ]
95
+ });
96
+ /** An inline component, `:name[text]{props}`, or inline raw HTML. */
97
+ const ComarkInline = Node.create({
98
+ name: "comarkInline",
99
+ group: "inline",
100
+ inline: true,
101
+ content: "inline*",
102
+ addAttributes: () => ({
103
+ name: name("data-comark-inline"),
104
+ props
105
+ }),
106
+ parseHTML: () => [{ tag: "span[data-comark-inline]" }],
107
+ renderHTML: ({ HTMLAttributes }) => [
108
+ "span",
109
+ HTMLAttributes,
110
+ 0
111
+ ]
112
+ });
113
+ /** An HTML comment between blocks. */
114
+ const ComarkComment = Node.create({
115
+ name: "comarkComment",
116
+ group: "block",
117
+ atom: true,
118
+ addAttributes: () => ({ text: {
119
+ default: "",
120
+ parseHTML: (element) => element.getAttribute("data-comark-comment") ?? "",
121
+ renderHTML: (attrs) => ({ "data-comark-comment": attrs.text })
122
+ } }),
123
+ parseHTML: () => [{ tag: "div[data-comark-comment]" }],
124
+ renderHTML: ({ HTMLAttributes }) => ["div", HTMLAttributes]
125
+ });
126
+ /**
127
+ * The schema `fromMarkdown` and `toMarkdown` read and write: StarterKit's nodes and marks, task lists, inline images,
128
+ * tables, and Comark's components, slots, and comments. It needs no DOM, so a server can load it to convert a document
129
+ * for a collaborative room.
130
+ */
131
+ const ComarkKit = Extension.create({
132
+ name: "comarkKit",
133
+ addOptions: () => ({ starterKit: {} }),
134
+ addGlobalAttributes: () => [{
135
+ types: typesWithProps,
136
+ attributes: { props }
137
+ }],
138
+ addExtensions() {
139
+ return [
140
+ StarterKit.configure({
141
+ ...this.options.starterKit,
142
+ underline: false
143
+ }),
144
+ TaskList,
145
+ TaskItem.configure({ nested: true }),
146
+ ComarkImage,
147
+ TableKit.configure({ table: { resizable: false } }),
148
+ ComarkBlock,
149
+ ComarkSlot,
150
+ ComarkInline,
151
+ ComarkComment
152
+ ];
153
+ }
154
+ });
155
+ //#endregion
156
+ //#region src/editor/markdown.ts
157
+ const parseOptions = {
158
+ autoUnwrap: false,
159
+ headingIds: false
160
+ };
161
+ const marks = /* @__PURE__ */ new Map([
162
+ ["strong", "bold"],
163
+ ["em", "italic"],
164
+ ["del", "strike"],
165
+ ["code", "code"],
166
+ ["a", "link"]
167
+ ]);
168
+ const markTags = new Map([...marks].map(([tag, mark]) => [mark, tag]));
169
+ const inlineTags = /* @__PURE__ */ new Set([
170
+ ...marks.keys(),
171
+ "br",
172
+ "img",
173
+ "span",
174
+ "input"
175
+ ]);
176
+ const headings = /* @__PURE__ */ new Set([
177
+ "h1",
178
+ "h2",
179
+ "h3",
180
+ "h4",
181
+ "h5",
182
+ "h6"
183
+ ]);
184
+ const builtIn = /* @__PURE__ */ new Set([
185
+ ...headings,
186
+ "p",
187
+ "blockquote",
188
+ "hr",
189
+ "pre",
190
+ "table",
191
+ "ul",
192
+ "ol",
193
+ "li"
194
+ ]);
195
+ const isText = (node) => !Array.isArray(node);
196
+ const isElement = (node) => Array.isArray(node) && node[0] !== null;
197
+ const childrenOf = (element) => element.slice(2);
198
+ /** A prop as text. Comark props are strings, numbers, and booleans, or parsed JSON for `:prop` bindings. */
199
+ const textOf = (value) => value === void 0 || value === null || value instanceof Object ? void 0 : String(value);
200
+ const inlineHtml = ({ $: meta }) => meta instanceof Object && "block" in meta && meta.block === 0;
201
+ const isInline = (node) => isText(node) || isElement(node) && (inlineTags.has(node[0]) || inlineHtml(node[1]));
202
+ /** The props a node has no field for, or `undefined` when none are left. */
203
+ function rest(props, ...keys) {
204
+ const left = Object.fromEntries(Object.entries(props).filter(([key]) => !keys.includes(key)));
205
+ return Object.keys(left).length ? left : void 0;
206
+ }
207
+ const classes = (props) => textOf(props.class)?.split(/\s+/) ?? [];
208
+ function withoutClass(props, name) {
209
+ const left = classes(props).filter((value) => value && value !== name);
210
+ const { class: _, ...others } = props;
211
+ return left.length ? {
212
+ ...others,
213
+ class: left.join(" ")
214
+ } : others;
215
+ }
216
+ function withClass(props, name) {
217
+ return {
218
+ ...props,
219
+ class: [name, ...classes(props).filter((value) => value && value !== name)].join(" ")
220
+ };
221
+ }
222
+ const withProps = (attrs, props) => props ? {
223
+ ...attrs,
224
+ props
225
+ } : attrs;
226
+ const node = (type, attrs, content) => {
227
+ const json = { type };
228
+ if (Object.keys(attrs).length) json.attrs = attrs;
229
+ if (content?.length) json.content = content;
230
+ return json;
231
+ };
232
+ function readBlocks(children, reading) {
233
+ const out = [];
234
+ let run = [];
235
+ const flush = () => {
236
+ if (run.some((child) => !isText(child) || child.trim())) out.push(node("paragraph", {}, readInline(run, [], reading)));
237
+ run = [];
238
+ };
239
+ for (const child of children) if (isInline(child)) run.push(child);
240
+ else {
241
+ flush();
242
+ out.push(...readBlock(child, reading));
243
+ }
244
+ flush();
245
+ return out;
246
+ }
247
+ function readBlock(child, reading) {
248
+ if (isText(child)) return [];
249
+ if (!isElement(child)) return [node("comarkComment", { text: child[2] })];
250
+ const [tag, props] = child;
251
+ const children = childrenOf(child);
252
+ if (tag === "p") return [node("paragraph", withProps({}, rest(props)), readInline(children, [], reading))];
253
+ if (headings.has(tag)) return [node("heading", withProps({ level: Number(tag[1]) }, rest(props)), readInline(children, [], reading))];
254
+ if (tag === "blockquote") return [node("blockquote", withProps({}, rest(props)), readBlocks(children, reading))];
255
+ if (tag === "hr") return [node("horizontalRule", withProps({}, rest(props)))];
256
+ if (tag === "pre") return [readCode(props, children)];
257
+ if (tag === "table" && children.every((part) => isElement(part) && [
258
+ "thead",
259
+ "tbody",
260
+ "tr"
261
+ ].includes(part[0]))) return [readTable(props, children, reading)];
262
+ if (tag === "ul" || tag === "ol") return [readList(tag, props, children, reading)];
263
+ if (tag === "template" || builtIn.has(tag)) reading.lossy = true;
264
+ return [node("comarkBlock", withProps({ name: tag }, rest(props)), readComponent(children, reading))];
265
+ }
266
+ function readComponent(children, reading) {
267
+ const out = [];
268
+ let run = [];
269
+ const flush = () => {
270
+ out.push(...readBlocks(run, reading));
271
+ run = [];
272
+ };
273
+ for (const child of children) if (isElement(child) && child[0] === "template") {
274
+ flush();
275
+ const { name, ...props } = child[1];
276
+ out.push(node("comarkSlot", withProps({ name: textOf(name) ?? "default" }, rest(props)), readBlocks(childrenOf(child), reading)));
277
+ } else run.push(child);
278
+ flush();
279
+ return out;
280
+ }
281
+ function readCode(props, children) {
282
+ const code = children.find(isElement);
283
+ const text = (code ? childrenOf(code) : children).filter(isText).join("");
284
+ const language = textOf(props.language) ?? null;
285
+ return node("codeBlock", withProps(language ? { language } : {}, rest(props, "language")), text ? [{
286
+ type: "text",
287
+ text
288
+ }] : []);
289
+ }
290
+ const isTaskItem = (child) => isElement(child) && child[0] === "li" && classes(child[1]).includes("task-list-item");
291
+ const isCheckbox = (child) => isElement(child) && child[0] === "input" && child[1].type === "checkbox";
292
+ function readList(tag, props, children, reading) {
293
+ const items = children.filter(isElement);
294
+ if (tag === "ul" && items.length && items.every(isTaskItem)) return node("taskList", withProps({}, rest(withoutClass(props, "contains-task-list"))), items.map((item) => readTaskItem(item, reading)));
295
+ const listItems = items.map((item) => {
296
+ const blocks = readBlocks(childrenOf(item), reading);
297
+ if (blocks[0]?.type !== "paragraph") blocks.unshift({ type: "paragraph" });
298
+ return node("listItem", withProps({}, rest(item[1])), blocks);
299
+ });
300
+ if (tag === "ul") return node("bulletList", withProps({}, rest(props)), listItems);
301
+ const start = Number(props.start ?? 1);
302
+ return node("orderedList", withProps(start === 1 ? {} : { start }, rest(props, "start")), listItems);
303
+ }
304
+ function readTaskItem(item, reading) {
305
+ let checked = false;
306
+ const strip = (content) => {
307
+ const [box, next, ...after] = content;
308
+ if (box === void 0 || !isCheckbox(box)) return content;
309
+ checked = box[1][":checked"] === "true" || box[1].checked === true;
310
+ if (next === void 0) return after;
311
+ return isText(next) ? [next.replace(/^\s+/, ""), ...after] : [next, ...after];
312
+ };
313
+ const [first, ...others] = childrenOf(item);
314
+ let content;
315
+ if (first !== void 0 && isElement(first) && first[0] === "p") content = [[
316
+ first[0],
317
+ first[1],
318
+ ...strip(childrenOf(first))
319
+ ], ...others];
320
+ else content = strip(childrenOf(item));
321
+ const blocks = readBlocks(content, reading);
322
+ if (blocks[0]?.type !== "paragraph") blocks.unshift({ type: "paragraph" });
323
+ return node("taskItem", withProps({ checked }, rest(withoutClass(item[1], "task-list-item"))), blocks);
324
+ }
325
+ function readTable(props, children, reading) {
326
+ const rows = children.filter(isElement).flatMap((section) => section[0] === "tr" ? [section] : childrenOf(section).filter(isElement));
327
+ return node("table", withProps({}, rest(props)), rows.map((row) => node("tableRow", withProps({}, rest(row[1])), childrenOf(row).filter(isElement).map((cell) => node(cell[0] === "th" ? "tableHeader" : "tableCell", withProps({}, rest(cell[1])), [node("paragraph", {}, readInline(childrenOf(cell), [], reading))])))));
328
+ }
329
+ function readInline(children, active, reading) {
330
+ const out = [];
331
+ const marked = (json) => active.length ? {
332
+ ...json,
333
+ marks: active
334
+ } : json;
335
+ for (const child of children) {
336
+ if (isText(child)) {
337
+ if (child) out.push(marked({
338
+ type: "text",
339
+ text: child
340
+ }));
341
+ continue;
342
+ }
343
+ if (!isElement(child)) {
344
+ reading.lossy = true;
345
+ continue;
346
+ }
347
+ const [tag, props] = child;
348
+ const content = childrenOf(child);
349
+ const mark = marks.get(tag);
350
+ if (mark && !(tag === "code" && !content.every(isText))) {
351
+ const attrs = mark === "link" ? withProps({
352
+ href: props.href ?? null,
353
+ title: props.title ?? null
354
+ }, rest(props, "href", "title")) : withProps({}, rest(props));
355
+ const next = Object.keys(attrs).length ? {
356
+ type: mark,
357
+ attrs
358
+ } : { type: mark };
359
+ out.push(...readInline(content, [...active, next], reading));
360
+ } else if (tag === "br") out.push(marked(node("hardBreak", withProps({}, rest(props)))));
361
+ else if (tag === "img") out.push(marked(node("image", withProps({
362
+ src: props.src ?? null,
363
+ alt: props.alt ?? null,
364
+ title: props.title ?? null
365
+ }, rest(props, "src", "alt", "title")))));
366
+ else out.push(marked(node("comarkInline", withProps({ name: tag }, rest(props)), readInline(content, [], reading))));
367
+ }
368
+ return out;
369
+ }
370
+ const propsOf = (attrs) => attrs?.props ?? {};
371
+ const plainText = (content = []) => content.map((child) => child.text ?? "").join("");
372
+ function writeBlocks(content = []) {
373
+ return content.flatMap(writeBlock);
374
+ }
375
+ function writeBlock(json) {
376
+ const props = propsOf(json.attrs);
377
+ const attrs = json.attrs ?? {};
378
+ switch (json.type) {
379
+ case "paragraph": return json.content?.length ? [[
380
+ "p",
381
+ props,
382
+ ...writeInline(json.content)
383
+ ]] : [];
384
+ case "heading": return [[
385
+ `h${Number(attrs.level ?? 1)}`,
386
+ props,
387
+ ...writeInline(json.content)
388
+ ]];
389
+ case "blockquote": return [[
390
+ "blockquote",
391
+ props,
392
+ ...writeBlocks(json.content)
393
+ ]];
394
+ case "horizontalRule": return [["hr", props]];
395
+ case "codeBlock": {
396
+ const language = textOf(attrs.language) || void 0;
397
+ return [[
398
+ "pre",
399
+ language ? {
400
+ ...props,
401
+ language
402
+ } : props,
403
+ [
404
+ "code",
405
+ language ? { class: `language-${language}` } : {},
406
+ plainText(json.content)
407
+ ]
408
+ ]];
409
+ }
410
+ case "bulletList": return [[
411
+ "ul",
412
+ props,
413
+ ...writeBlocks(json.content)
414
+ ]];
415
+ case "orderedList": {
416
+ const start = Number(attrs.start ?? 1);
417
+ return [[
418
+ "ol",
419
+ start === 1 ? props : {
420
+ ...props,
421
+ start
422
+ },
423
+ ...writeBlocks(json.content)
424
+ ]];
425
+ }
426
+ case "listItem": return [[
427
+ "li",
428
+ props,
429
+ ...writeBlocks(json.content)
430
+ ]];
431
+ case "taskList": return [[
432
+ "ul",
433
+ withClass(props, "contains-task-list"),
434
+ ...writeBlocks(json.content)
435
+ ]];
436
+ case "taskItem": {
437
+ const [first, ...others] = json.content ?? [];
438
+ const text = writeInline(first?.content);
439
+ const [head] = text;
440
+ if (head !== void 0 && isText(head)) text[0] = ` ${head}`;
441
+ else text.unshift(" ");
442
+ const box = {
443
+ class: "task-list-item-checkbox",
444
+ type: "checkbox",
445
+ ":disabled": "true"
446
+ };
447
+ if (attrs.checked) box[":checked"] = "true";
448
+ return [[
449
+ "li",
450
+ withClass(props, "task-list-item"),
451
+ ["input", box],
452
+ ...text,
453
+ ...writeBlocks(others)
454
+ ]];
455
+ }
456
+ case "table": return [writeTable(json, props)];
457
+ case "comarkBlock": return [[
458
+ String(attrs.name),
459
+ props,
460
+ ...(json.content ?? []).flatMap((child) => child.type === "comarkSlot" ? [[
461
+ "template",
462
+ {
463
+ ...propsOf(child.attrs),
464
+ name: child.attrs?.name
465
+ },
466
+ ...writeBlocks(child.content)
467
+ ]] : writeBlock(child))
468
+ ]];
469
+ case "comarkComment": return [[
470
+ null,
471
+ {},
472
+ textOf(attrs.text) ?? ""
473
+ ]];
474
+ default: return [];
475
+ }
476
+ }
477
+ function writeTable(json, props) {
478
+ const rows = json.content ?? [];
479
+ const isHeader = (row) => row.content?.every((cell) => cell.type === "tableHeader") ?? false;
480
+ const firstBody = rows.findIndex((row) => !isHeader(row));
481
+ const split = firstBody === -1 ? rows.length : Math.max(1, firstBody);
482
+ const row = (tag) => (json) => [
483
+ "tr",
484
+ propsOf(json.attrs),
485
+ ...(json.content ?? []).map((cell) => [
486
+ tag,
487
+ propsOf(cell.attrs),
488
+ ...(cell.content ?? []).flatMap((paragraph, index) => [...index ? [["br", {}]] : [], ...writeInline(paragraph.content)])
489
+ ])
490
+ ];
491
+ const body = rows.slice(split).map(row("td"));
492
+ return [
493
+ "table",
494
+ props,
495
+ [
496
+ "thead",
497
+ {},
498
+ ...rows.slice(0, split).map(row("th"))
499
+ ],
500
+ ...body.length ? [[
501
+ "tbody",
502
+ {},
503
+ ...body
504
+ ]] : []
505
+ ];
506
+ }
507
+ const sameMark = (a, b) => a.type === b.type && JSON.stringify(a.attrs ?? {}) === JSON.stringify(b.attrs ?? {});
508
+ function writeMark(mark, children) {
509
+ const props = propsOf(mark.attrs);
510
+ if (mark.type !== "link") return [
511
+ markTags.get(mark.type) ?? "span",
512
+ props,
513
+ ...children
514
+ ];
515
+ const href = textOf(mark.attrs?.href);
516
+ const title = textOf(mark.attrs?.title);
517
+ const link = {
518
+ ...props,
519
+ href
520
+ };
521
+ if (title) link.title = title;
522
+ return [
523
+ "a",
524
+ link,
525
+ ...children
526
+ ];
527
+ }
528
+ function append(children, child) {
529
+ const last = children.at(-1);
530
+ if (last !== void 0 && isText(last) && isText(child)) children[children.length - 1] = last + child;
531
+ else children.push(child);
532
+ }
533
+ function writeInline(content = []) {
534
+ const open = [];
535
+ const root = [];
536
+ const close = (depth) => {
537
+ for (let frame = open.pop(); frame; frame = open.length > depth ? open.pop() : void 0) append(open.at(-1)?.children ?? root, writeMark(frame.mark, frame.children));
538
+ };
539
+ for (const json of content) {
540
+ const active = json.marks ?? [];
541
+ const same = (index) => {
542
+ const frame = open[index];
543
+ const mark = active[index];
544
+ return frame !== void 0 && mark !== void 0 && sameMark(frame.mark, mark);
545
+ };
546
+ let keep = 0;
547
+ while (same(keep)) keep++;
548
+ if (open.length > keep) close(keep);
549
+ for (const mark of active.slice(keep)) open.push({
550
+ mark,
551
+ children: []
552
+ });
553
+ for (const child of writeLeaf(json)) append(open.at(-1)?.children ?? root, child);
554
+ }
555
+ if (open.length) close(0);
556
+ return root;
557
+ }
558
+ function writeLeaf(json) {
559
+ const props = propsOf(json.attrs);
560
+ const attrs = json.attrs ?? {};
561
+ if (json.type === "text") return json.text ? [json.text] : [];
562
+ if (json.type === "hardBreak") return [["br", props]];
563
+ if (json.type === "image") {
564
+ const image = {
565
+ ...props,
566
+ src: attrs.src
567
+ };
568
+ if (attrs.alt) image.alt = attrs.alt;
569
+ if (attrs.title) image.title = attrs.title;
570
+ return [["img", image]];
571
+ }
572
+ if (json.type === "comarkInline") return [[
573
+ String(attrs.name),
574
+ props,
575
+ ...writeInline(json.content)
576
+ ]];
577
+ return [];
578
+ }
579
+ function equal(a, b) {
580
+ if (a === b) return true;
581
+ if (!(a instanceof Object) || !(b instanceof Object) || Array.isArray(a) !== Array.isArray(b)) return false;
582
+ const left = Object.entries(a).filter(([, value]) => value !== void 0);
583
+ const right = new Map(Object.entries(b).filter(([, value]) => value !== void 0));
584
+ return left.length === right.size && left.every(([key, value]) => right.has(key) && equal(value, right.get(key)));
585
+ }
586
+ /** Reads Markdown into the editor's document. */
587
+ async function fromMarkdown(markdown) {
588
+ const tree = await parseMarkdown(markdown, parseOptions);
589
+ const reading = { lossy: false };
590
+ const content = readBlocks(tree.nodes, reading);
591
+ return {
592
+ doc: {
593
+ type: "doc",
594
+ content: content.length ? content : [{ type: "paragraph" }]
595
+ },
596
+ frontmatter: tree.frontmatter ?? {},
597
+ lossy: reading.lossy
598
+ };
599
+ }
600
+ /** Writes the editor's document, such as `editor.getJSON()`, back to Markdown. */
601
+ async function toMarkdown(doc, options = {}) {
602
+ const markdown = await renderMarkdown({
603
+ nodes: writeBlocks(doc.content),
604
+ frontmatter: options.frontmatter ?? {},
605
+ meta: {}
606
+ }, { headingIds: false });
607
+ return markdown.trim() ? `${markdown.trimEnd()}\n` : "";
608
+ }
609
+ /**
610
+ * Whether the editor can open this Markdown and save it without changing what readers see. Pass the parse plugins the
611
+ * page renders with, so syntax only they read, such as footnote definitions, counts too.
612
+ */
613
+ async function checkMarkdown(markdown, options = {}) {
614
+ const first = await fromMarkdown(markdown);
615
+ if (first.lossy) return { editable: false };
616
+ const written = await toMarkdown(first.doc, first);
617
+ const again = await fromMarkdown(written);
618
+ if (again.lossy || !equal(first.doc, again.doc) || !equal(first.frontmatter, again.frontmatter)) return { editable: false };
619
+ const { plugins = [] } = options;
620
+ if (plugins.length) {
621
+ const read = async (source) => {
622
+ return (await parseMarkdown(source, {
623
+ ...parseOptions,
624
+ plugins: [...plugins]
625
+ })).nodes.map((child) => textContent(child)).join("").replace(/\s+/g, " ").trim();
626
+ };
627
+ if (await read(markdown) !== await read(written)) return { editable: false };
628
+ }
629
+ return {
630
+ editable: true,
631
+ markdown: written
632
+ };
633
+ }
634
+ //#endregion
635
+ //#region src/editor/upload.ts
636
+ const uploadKey = new PluginKey("comarkUpload");
637
+ const acceptMedia = (file) => /^(?:image|video|audio)\//.test(file.type);
638
+ /** Where placeholder `id` sits now, after every edit since it was added. */
639
+ function findUpload(state, id) {
640
+ return uploadKey.getState(state)?.find(void 0, void 0, (spec) => spec.id === id)[0]?.from;
641
+ }
642
+ function placeholder(pending) {
643
+ const root = document.createElement("span");
644
+ root.className = "cue-upload";
645
+ root.contentEditable = "false";
646
+ root.dataset.kind = pending.file.type.split("/")[0];
647
+ if (pending.preview && root.dataset.kind === "image") {
648
+ const image = document.createElement("img");
649
+ image.src = pending.preview;
650
+ image.alt = "";
651
+ root.append(image);
652
+ } else if (pending.preview && root.dataset.kind === "video") {
653
+ const video = document.createElement("video");
654
+ video.src = pending.preview;
655
+ video.muted = true;
656
+ root.append(video);
657
+ }
658
+ const label = document.createElement("span");
659
+ label.className = "cue-upload-label";
660
+ label.setAttribute("role", "status");
661
+ root.append(label);
662
+ const show = (fraction) => {
663
+ root.style.setProperty("--cue-upload", fraction === null ? "0%" : `${Math.round(fraction * 100)}%`);
664
+ label.textContent = fraction === null ? `Uploading ${pending.file.name}` : `Uploading ${pending.file.name}, ${Math.round(fraction * 100)}%`;
665
+ };
666
+ show(pending.fraction);
667
+ return {
668
+ root,
669
+ show
670
+ };
671
+ }
672
+ /**
673
+ * Uploads `files` into the document at `pos`. Each shows a placeholder there until its URL comes back, then becomes an
674
+ * image node, which comark-media plays when its URL ends in a video or audio extension. The placeholder is a
675
+ * decoration, so a collaborative room never sees a file that hasn't finished.
676
+ */
677
+ function startUploads(view, files, pos, options) {
678
+ const { upload } = options;
679
+ if (!upload) return [];
680
+ const batch = Symbol();
681
+ return files.flatMap((file) => options.accept(file) ? [send(view, file, pos, upload, options, batch)] : []);
682
+ }
683
+ function send(view, file, pos, upload, options, batch) {
684
+ const pending = {
685
+ id: nextId++,
686
+ file,
687
+ fraction: null
688
+ };
689
+ if (/^(?:image|video)\//.test(file.type)) pending.preview = URL.createObjectURL(file);
690
+ pendingUploads.set(pending.id, pending);
691
+ view.dispatch(view.state.tr.setMeta(uploadKey, { add: {
692
+ id: pending.id,
693
+ pos,
694
+ batch
695
+ } }));
696
+ const finish = () => {
697
+ pendingUploads.delete(pending.id);
698
+ if (pending.preview) URL.revokeObjectURL(pending.preview);
699
+ };
700
+ return upload(file, (fraction) => {
701
+ pending.fraction = Math.min(1, Math.max(0, fraction));
702
+ elements.get(pending.id)?.show(pending.fraction);
703
+ }).then((src) => {
704
+ const at = findUpload(view.state, pending.id);
705
+ const tr = view.state.tr.setMeta(uploadKey, {
706
+ remove: pending.id,
707
+ batch
708
+ });
709
+ if (at !== void 0) insertImage(tr, at, view.state.schema.nodes.image.create({
710
+ src,
711
+ alt: file.name
712
+ }));
713
+ view.dispatch(tr);
714
+ finish();
715
+ return src;
716
+ }, (error) => {
717
+ view.dispatch(view.state.tr.setMeta(uploadKey, { remove: pending.id }));
718
+ finish();
719
+ options.onError?.(error, file);
720
+ });
721
+ }
722
+ function insertImage(tr, pos, image) {
723
+ if (tr.doc.resolve(pos).parent.inlineContent) tr.insert(pos, image);
724
+ else tr.insert(pos, tr.doc.type.schema.nodes.paragraph.create(null, image));
725
+ }
726
+ let nextId = 1;
727
+ const pendingUploads = /* @__PURE__ */ new Map();
728
+ const elements = /* @__PURE__ */ new Map();
729
+ function uploadWidget(pos, id, batch) {
730
+ return Decoration.widget(pos, () => {
731
+ const element = placeholder(pendingUploads.get(id) ?? {
732
+ id,
733
+ file: new File([], ""),
734
+ fraction: null
735
+ });
736
+ elements.set(id, element);
737
+ return element.root;
738
+ }, {
739
+ id,
740
+ batch,
741
+ key: String(id),
742
+ side: -1,
743
+ destroy: () => elements.delete(id)
744
+ });
745
+ }
746
+ /** The plugin alone, for a ProseMirror editor without Tiptap. */
747
+ function uploadPlugin(options) {
748
+ const filesOf = (list) => Array.from(list ?? []).filter(options.accept);
749
+ return new Plugin({
750
+ key: uploadKey,
751
+ state: {
752
+ init: () => DecorationSet.empty,
753
+ apply(tr, set) {
754
+ let next = set.map(tr.mapping, tr.doc);
755
+ const meta = tr.getMeta(uploadKey);
756
+ if (meta && "add" in meta) {
757
+ const { id, pos, batch } = meta.add;
758
+ next = next.add(tr.doc, [uploadWidget(pos, id, batch)]);
759
+ } else if (meta && "remove" in meta) {
760
+ if (meta.batch) for (const widget of set.find(void 0, void 0, (spec) => spec.batch === meta.batch && spec.id > meta.remove)) {
761
+ next = next.remove(next.find(void 0, void 0, (spec) => spec.id === widget.spec.id));
762
+ next = next.add(tr.doc, [uploadWidget(tr.mapping.map(widget.from, 1), widget.spec.id, meta.batch)]);
763
+ }
764
+ next = next.remove(next.find(void 0, void 0, (spec) => spec.id === meta.remove));
765
+ }
766
+ return next;
767
+ }
768
+ },
769
+ props: {
770
+ decorations: (state) => uploadKey.getState(state),
771
+ handlePaste(view, event) {
772
+ const files = filesOf(event.clipboardData?.files);
773
+ if (!files.length || !options.upload) return false;
774
+ Promise.all(startUploads(view, files, view.state.selection.from, options));
775
+ return true;
776
+ },
777
+ handleDrop(view, event, _slice, moved) {
778
+ const files = filesOf(event.dataTransfer?.files);
779
+ if (moved || !files.length || !options.upload) return false;
780
+ const pos = view.posAtCoords({
781
+ left: event.clientX,
782
+ top: event.clientY
783
+ })?.pos ?? view.state.selection.from;
784
+ Promise.all(startUploads(view, files, pos, options));
785
+ return true;
786
+ }
787
+ }
788
+ });
789
+ }
790
+ /** Uploads pasted and dropped files, with a placeholder that shows the progress. Pass `upload` to turn it on. */
791
+ const Upload = Extension.create({
792
+ name: "comarkUpload",
793
+ addOptions: () => ({
794
+ upload: void 0,
795
+ accept: acceptMedia,
796
+ onError: void 0
797
+ }),
798
+ addProseMirrorPlugins() {
799
+ return [uploadPlugin(this.options)];
800
+ },
801
+ addCommands() {
802
+ return { uploadFiles: (files) => ({ view }) => {
803
+ if (!this.options.upload) return false;
804
+ Promise.all(startUploads(view, files, view.state.selection.from, this.options));
805
+ return true;
806
+ } };
807
+ }
808
+ });
809
+ //#endregion
810
+ //#region src/editor/codec.ts
811
+ function createEditorCodec(options = {}) {
812
+ const kit = ComarkKit.configure({
813
+ ...options.kit,
814
+ starterKit: {
815
+ undoRedo: false,
816
+ trailingNode: false,
817
+ ...options.kit?.starterKit
818
+ }
819
+ });
820
+ return {
821
+ extensions: () => [kit],
822
+ fromMarkdown: async (markdown) => (await fromMarkdown(markdown)).doc,
823
+ toMarkdown: (doc) => toMarkdown(doc),
824
+ async verify(markdown) {
825
+ if (Object.keys((await fromMarkdown(markdown)).frontmatter).length) return { writable: false };
826
+ const check = await checkMarkdown(markdown, { plugins: options.plugins });
827
+ return check.editable ? {
828
+ writable: true,
829
+ canonical: check.markdown
830
+ } : { writable: false };
831
+ }
832
+ };
833
+ }
834
+ //#endregion
835
+ export { uploadKey as a, fromMarkdown as c, ComarkComment as d, ComarkImage as f, ComarkSlot as h, startUploads as i, toMarkdown as l, ComarkKit as m, Upload as n, uploadPlugin as o, ComarkInline as p, findUpload as r, checkMarkdown as s, createEditorCodec as t, ComarkBlock as u };