comark-ui 0.7.2 → 0.7.5

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 (47) hide show
  1. package/README.md +2 -2
  2. package/dist/{editor-BAdok66p.mjs → editor-DmxNPK7K.mjs} +522 -68
  3. package/dist/{editor-CNivEuMH.d.mts → editor-KD97dL_1.d.mts} +21 -3
  4. package/dist/editor-vue.d.mts +1 -1
  5. package/dist/editor-vue.mjs +1 -1
  6. package/dist/editor.d.mts +2 -2
  7. package/dist/editor.mjs +1 -1
  8. package/dist/html.d.mts +1 -1
  9. package/dist/html.mjs +3 -1
  10. package/dist/{index-jkwx6SyG.d.mts → index-Dbqxt4Qg.d.mts} +10 -2
  11. package/dist/index.d.mts +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/react.d.mts +2 -2
  14. package/dist/react.mjs +2 -2
  15. package/dist/skills/before-after-evidence/SKILL.md +72 -0
  16. package/dist/skills/before-after-evidence/agents/openai.yaml +3 -0
  17. package/dist/skills/before-after-evidence/references/blocks.md +54 -0
  18. package/dist/skills/before-after-evidence/references/example.md +37 -0
  19. package/dist/skills/before-after-evidence/references/measuring.md +50 -0
  20. package/dist/skills/comark-ui/SKILL.md +8 -1
  21. package/dist/skills/comark-ui/references/annotate.md +5 -3
  22. package/dist/skills/comark-ui/references/comments.md +43 -0
  23. package/dist/skills/comark-ui/references/lint.md +1 -1
  24. package/dist/skills/comark-ui/references/media.md +2 -2
  25. package/dist/skills/comark-ui/references/quiz.md +1 -1
  26. package/dist/skills/comark-ui/references/ui.md +3 -2
  27. package/dist/skills/comark-ui/references/zoom.md +1 -1
  28. package/dist/skills/review-document/SKILL.md +64 -0
  29. package/dist/skills/review-document/agents/openai.yaml +3 -0
  30. package/dist/skills/review-document/references/blocks.md +69 -0
  31. package/dist/skills/review-document/references/example.md +51 -0
  32. package/dist/skills/teach-lesson/SKILL.md +78 -0
  33. package/dist/skills/teach-lesson/agents/openai.yaml +3 -0
  34. package/dist/skills/teach-lesson/references/blocks.md +91 -0
  35. package/dist/skills/teach-lesson/references/example.md +70 -0
  36. package/dist/skills/visual-explanation/SKILL.md +67 -0
  37. package/dist/skills/visual-explanation/agents/openai.yaml +3 -0
  38. package/dist/skills/visual-explanation/references/blocks.md +89 -0
  39. package/dist/skills/visual-explanation/references/example.md +25 -0
  40. package/dist/skills.d.mts +18 -0
  41. package/dist/skills.mjs +326 -0
  42. package/dist/{src-DROB05U6.mjs → src-PkmYCSNu.mjs} +1 -0
  43. package/dist/svelte/index.d.ts +1 -1
  44. package/dist/svelte/index.js +1 -1
  45. package/dist/vue.d.mts +2 -2
  46. package/dist/vue.mjs +2 -2
  47. package/package.json +46 -44
@@ -1,11 +1,15 @@
1
- import { parseMarkdown } from "comark";
2
- import { Extension, Node } from "@tiptap/core";
1
+ import cjk from "comark-cjk";
2
+ import critic from "comark-critic";
3
+ import deflist from "comark-deflist";
4
+ import footnotes from "comark-footnotes";
5
+ import { defineComarkPlugin, parseMarkdown } from "comark";
6
+ import { Extension, Mark, Node } from "@tiptap/core";
3
7
  import Image from "@tiptap/extension-image";
4
8
  import { TaskItem, TaskList } from "@tiptap/extension-list";
5
9
  import { TableKit } from "@tiptap/extension-table";
6
10
  import StarterKit from "@tiptap/starter-kit";
7
11
  import { renderMarkdown } from "comark/render";
8
- import { textContent } from "comark/utils";
12
+ import { indent, textContent } from "comark/utils";
9
13
  import { Plugin, PluginKey } from "@tiptap/pm/state";
10
14
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
11
15
  //#region src/editor/kit.ts
@@ -30,12 +34,26 @@ const typesWithProps = [
30
34
  "italic",
31
35
  "strike",
32
36
  "code",
33
- "link"
37
+ "link",
38
+ "definitionList",
39
+ "definitionTerm",
40
+ "definitionDescription"
34
41
  ];
35
- /** Comark attributes the editor doesn't show, carried as they were parsed so writing the node back keeps them. */
42
+ function readProps(element) {
43
+ try {
44
+ return JSON.parse(element.getAttribute("data-comark-props") ?? "null");
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Comark attributes the editor doesn't show, carried as they were parsed so writing the node back keeps them. They're
51
+ * in the HTML too, since the clipboard copies nodes as HTML: a pasted `::screenshot` keeps its `src`.
52
+ */
36
53
  const props = {
37
54
  default: null,
38
- rendered: false
55
+ parseHTML: readProps,
56
+ renderHTML: (attrs) => attrs.props ? { "data-comark-props": JSON.stringify(attrs.props) } : {}
39
57
  };
40
58
  const name = (attribute) => ({
41
59
  default: "",
@@ -123,9 +141,157 @@ const ComarkComment = Node.create({
123
141
  parseHTML: () => [{ tag: "div[data-comark-comment]" }],
124
142
  renderHTML: ({ HTMLAttributes }) => ["div", HTMLAttributes]
125
143
  });
144
+ const label = {
145
+ default: "",
146
+ parseHTML: (element) => element.getAttribute("data-label") ?? "",
147
+ renderHTML: (attrs) => ({ "data-label": attrs.label })
148
+ };
149
+ /** A footnote reference, `[^label]`. */
150
+ const FootnoteReference = Node.create({
151
+ name: "footnoteReference",
152
+ group: "inline",
153
+ inline: true,
154
+ atom: true,
155
+ addAttributes: () => ({ label }),
156
+ parseHTML: () => [{ tag: "sup[data-footnote-reference]" }],
157
+ renderHTML: ({ node, HTMLAttributes }) => [
158
+ "sup",
159
+ {
160
+ ...HTMLAttributes,
161
+ "data-footnote-reference": ""
162
+ },
163
+ node.attrs.label
164
+ ]
165
+ });
166
+ /** A note written where it applies, `^[text]`. */
167
+ const InlineFootnote = Node.create({
168
+ name: "inlineFootnote",
169
+ group: "inline",
170
+ inline: true,
171
+ content: "inline*",
172
+ parseHTML: () => [{ tag: "span[data-inline-footnote]" }],
173
+ renderHTML: ({ HTMLAttributes }) => [
174
+ "span",
175
+ {
176
+ ...HTMLAttributes,
177
+ "data-inline-footnote": ""
178
+ },
179
+ 0
180
+ ]
181
+ });
182
+ /** A footnote's text, `[^label]: text`, with the blocks indented under it. */
183
+ const FootnoteDefinition = Node.create({
184
+ name: "footnoteDefinition",
185
+ group: "block",
186
+ content: "block+",
187
+ defining: true,
188
+ addAttributes: () => ({ label }),
189
+ parseHTML: () => [{ tag: "div[data-footnote-definition]" }],
190
+ renderHTML: ({ HTMLAttributes }) => [
191
+ "div",
192
+ {
193
+ ...HTMLAttributes,
194
+ "data-footnote-definition": ""
195
+ },
196
+ 0
197
+ ]
198
+ });
199
+ /** A definition list, `Term` then `: Definition`. */
200
+ const DefinitionList = Node.create({
201
+ name: "definitionList",
202
+ group: "block",
203
+ content: "(definitionTerm definitionDescription+)+",
204
+ parseHTML: () => [{ tag: "dl" }],
205
+ renderHTML: ({ HTMLAttributes }) => [
206
+ "dl",
207
+ HTMLAttributes,
208
+ 0
209
+ ]
210
+ });
211
+ const DefinitionTerm = Node.create({
212
+ name: "definitionTerm",
213
+ content: "inline*",
214
+ defining: true,
215
+ parseHTML: () => [{ tag: "dt" }],
216
+ renderHTML: ({ HTMLAttributes }) => [
217
+ "dt",
218
+ HTMLAttributes,
219
+ 0
220
+ ]
221
+ });
222
+ const DefinitionDescription = Node.create({
223
+ name: "definitionDescription",
224
+ content: "block+",
225
+ defining: true,
226
+ parseHTML: () => [{ tag: "dd" }],
227
+ renderHTML: ({ HTMLAttributes }) => [
228
+ "dd",
229
+ HTMLAttributes,
230
+ 0
231
+ ]
232
+ });
233
+ const substitution$2 = {
234
+ default: false,
235
+ parseHTML: (element) => element.hasAttribute("data-substitution"),
236
+ renderHTML: (attrs) => attrs.substitution ? { "data-substitution": "" } : {}
237
+ };
238
+ /** CriticMarkup's suggested insertion, `{++text++}`. */
239
+ const CriticInsertion = Mark.create({
240
+ name: "criticInsertion",
241
+ addAttributes: () => ({ substitution: substitution$2 }),
242
+ parseHTML: () => [{ tag: "ins" }],
243
+ renderHTML: ({ HTMLAttributes }) => [
244
+ "ins",
245
+ HTMLAttributes,
246
+ 0
247
+ ]
248
+ });
249
+ /** CriticMarkup's suggested deletion, `{--text--}`. */
250
+ const CriticDeletion = Mark.create({
251
+ name: "criticDeletion",
252
+ addAttributes: () => ({ substitution: substitution$2 }),
253
+ parseHTML: () => [{
254
+ tag: "del[data-critic]",
255
+ priority: 60
256
+ }],
257
+ renderHTML: ({ HTMLAttributes }) => [
258
+ "del",
259
+ {
260
+ ...HTMLAttributes,
261
+ "data-critic": ""
262
+ },
263
+ 0
264
+ ]
265
+ });
266
+ /** CriticMarkup's highlight, `{==text==}`. */
267
+ const CriticHighlight = Mark.create({
268
+ name: "criticHighlight",
269
+ parseHTML: () => [{ tag: "mark" }],
270
+ renderHTML: ({ HTMLAttributes }) => [
271
+ "mark",
272
+ HTMLAttributes,
273
+ 0
274
+ ]
275
+ });
276
+ /** CriticMarkup's comment, `{>>text<<}`. */
277
+ const CriticComment = Node.create({
278
+ name: "criticComment",
279
+ group: "inline",
280
+ inline: true,
281
+ content: "inline*",
282
+ parseHTML: () => [{ tag: "span[data-critic-comment]" }],
283
+ renderHTML: ({ HTMLAttributes }) => [
284
+ "span",
285
+ {
286
+ ...HTMLAttributes,
287
+ "data-critic-comment": ""
288
+ },
289
+ 0
290
+ ]
291
+ });
126
292
  /**
127
293
  * 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
294
+ * tables, footnotes, definition lists, CriticMarkup, and Comark's components, slots, and comments. It needs no DOM, so a server can load it to convert a document
129
295
  * for a collaborative room.
130
296
  */
131
297
  const ComarkKit = Extension.create({
@@ -137,7 +303,9 @@ const ComarkKit = Extension.create({
137
303
  }],
138
304
  addExtensions() {
139
305
  return [
140
- StarterKit.configure({
306
+ StarterKit.extend({ addExtensions() {
307
+ return (this.parent?.() ?? []).map((extension) => extension.name === "code" ? extension.extend({ excludes: "" }) : extension);
308
+ } }).configure({
141
309
  ...this.options.starterKit,
142
310
  underline: false
143
311
  }),
@@ -148,16 +316,196 @@ const ComarkKit = Extension.create({
148
316
  ComarkBlock,
149
317
  ComarkSlot,
150
318
  ComarkInline,
151
- ComarkComment
319
+ ComarkComment,
320
+ FootnoteReference,
321
+ InlineFootnote,
322
+ FootnoteDefinition,
323
+ DefinitionList,
324
+ DefinitionTerm,
325
+ DefinitionDescription,
326
+ CriticInsertion,
327
+ CriticDeletion,
328
+ CriticHighlight,
329
+ CriticComment
152
330
  ];
153
331
  }
154
332
  });
155
333
  //#endregion
334
+ //#region src/editor/handlers.ts
335
+ const handlers = /* @__PURE__ */ new WeakMap();
336
+ const isElement$1 = (node) => Array.isArray(node) && node[0] !== null;
337
+ /** Marks `node` to be written by `handler`. */
338
+ function own(node, handler) {
339
+ handlers.set(node, handler);
340
+ return node;
341
+ }
342
+ const writeOptions = {
343
+ headingIds: false,
344
+ maxInlineAttributes: Number.POSITIVE_INFINITY,
345
+ components: { owned: {
346
+ match: (node) => handlers.has(node),
347
+ handler: (node, state, parent) => handlers.get(node)?.(node, state, parent) ?? ""
348
+ } }
349
+ };
350
+ const TABLE = "comark-editor-table";
351
+ /**
352
+ * A block component, `::name`. Written with no parent, since Comark indents a component inside another, and authors
353
+ * write them flush: the colons already say how deep each one is.
354
+ */
355
+ const blockComponent = async (node, state) => {
356
+ const [tag, attrs, ...children] = node;
357
+ if (tag !== "table") return state.handlers.mdc(node, state);
358
+ return (await state.handlers.mdc([
359
+ TABLE,
360
+ attrs,
361
+ ...children
362
+ ], state)).replace(TABLE, "table");
363
+ };
364
+ /** An inline component, `:name[text]{props}`, which Comark would write as a block when it's alone in its paragraph. */
365
+ const inlineComponent = async (node, state, parent) => {
366
+ const revert = state.applyContext({ inline: true });
367
+ try {
368
+ return await state.handlers.mdc(node, state, parent);
369
+ } finally {
370
+ state.applyContext(revert);
371
+ }
372
+ };
373
+ /**
374
+ * A task, written as a list item with its check box added after the marker. Comark's own writer indents the blocks
375
+ * under a task by the width of `- [ ] `, which reads them back as code.
376
+ */
377
+ const task = (checked) => async (node, state, parent) => (await state.handlers.li(node, state, parent)).replace(/^(- |\d+\. )/, `$1[${checked ? "x" : " "}] `);
378
+ const END = "";
379
+ /** A code block that ends in blank lines, which Comark's writer trims off. */
380
+ const trailingLines = async ([tag, attrs, ...children], state) => {
381
+ const written = await state.handlers.pre([
382
+ tag,
383
+ attrs,
384
+ ...children,
385
+ END
386
+ ], state);
387
+ const at = written.lastIndexOf(END);
388
+ return written.slice(0, at) + written.slice(at + 1);
389
+ };
390
+ /** Inline content between two delimiters, such as `{++` and `++}`. */
391
+ const between = (open, close) => async (node, state) => `${open}${await state.flow(node, state)}${close}`;
392
+ /** `{~~old~>new~~}`, from its `del` and `ins` halves. */
393
+ const substitution$1 = async ([, , removed, added], state) => {
394
+ const flow = async (half) => isElement$1(half) ? state.flow(half, state) : "";
395
+ return `{~~${await flow(removed)}~>${await flow(added)}~~}`;
396
+ };
397
+ const footnoteReference = (label) => () => `[^${label}]`;
398
+ /** `[^label]: text`, with the rest of its blocks indented under it. */
399
+ const footnoteDefinition = (label) => async (node, state) => {
400
+ const content = (await state.flow(node, state)).trim();
401
+ return `[^${label}]: ${indent(content, {
402
+ width: 4,
403
+ ignoreFirstLine: true
404
+ })}${state.context.blockSeparator}`;
405
+ };
406
+ /** Each term on its line, each definition after `: ` with its blocks indented under it, and a blank line before the next term. */
407
+ const definitionList = async (node, state) => {
408
+ let written = "";
409
+ for (const item of node.slice(2)) {
410
+ if (!isElement$1(item)) continue;
411
+ const content = (await state.flow(item, state)).trim();
412
+ if (item[0] === "dt") written += `${written ? "\n" : ""}${content}\n`;
413
+ else written += `: ${indent(content, {
414
+ width: 4,
415
+ ignoreFirstLine: true
416
+ })}\n`;
417
+ }
418
+ return written.trimEnd() + state.context.blockSeparator;
419
+ };
420
+ //#endregion
421
+ //#region ../../shared/markdown-it.ts
422
+ /**
423
+ * An inline element and a block element, by the prefix of their `_open` and `_close` types. Not `mdc_block_slot` for
424
+ * blocks: directly inside a `::component` Comark reads those as named slots and splits the block apart.
425
+ */
426
+ const SPAN = "mdc_inline_span";
427
+ /**
428
+ * Adds a rule before or after the first of `anchors` the parser has, else last. Comark's own rules exist only with
429
+ * its default plugins, and markdown-exit throws on a missing anchor, which would make every parse throw.
430
+ */
431
+ function addRule(ruler, place, anchors, name, fn, options) {
432
+ for (const anchor of anchors) try {
433
+ ruler[place](anchor, name, fn, options);
434
+ return;
435
+ } catch {}
436
+ ruler.push(name, fn, options);
437
+ }
438
+ //#endregion
439
+ //#region src/editor/syntax.ts
440
+ /** The element around the two halves of `{~~old~>new~~}`, which comark-critic writes as a `del` beside an `ins`. */
441
+ const SUBSTITUTION = "critic-sub";
442
+ let inside = false;
443
+ function substitution(state, silent) {
444
+ if (silent || inside || !state.src.startsWith("{~~", state.pos)) return false;
445
+ const start = state.pos;
446
+ const max = state.posMax;
447
+ const rules = state.md.inline.ruler.getRules("");
448
+ const criticRule = rules[rules.indexOf(substitution) + 1];
449
+ if (!criticRule?.(state, true)) return false;
450
+ const end = state.pos;
451
+ state.pos = start;
452
+ state.push(`${SPAN}_open`, SUBSTITUTION, 1);
453
+ state.posMax = end;
454
+ inside = true;
455
+ try {
456
+ state.md.inline.tokenize(state);
457
+ } finally {
458
+ inside = false;
459
+ }
460
+ state.posMax = max;
461
+ state.push(`${SPAN}_close`, SUBSTITUTION, -1);
462
+ return true;
463
+ }
464
+ const substitutions = defineComarkPlugin(() => ({
465
+ name: "editor-critic-substitutions",
466
+ markdownItPlugins: [(md) => addRule(md.inline.ruler, "before", ["critic"], "critic_substitution", substitution)]
467
+ }));
468
+ const footnoteMarkers = defineComarkPlugin(() => ({
469
+ name: "editor-footnotes",
470
+ markdownItPlugins: footnotes().markdownItPlugins
471
+ }));
472
+ const noAlert = defineComarkPlugin(() => ({ name: "alert" }));
473
+ /** The prop on a component written `:name{props}` alone on its line, which Comark reads like an empty `::name`. */
474
+ const SHORTHAND = "data-comark-editor-shorthand";
475
+ function markShorthands(tokens) {
476
+ for (const token of tokens) {
477
+ if (token.type === "mdc_block_shorthand" || token.type === "mdc_block_shorthand_open") token.attrSet(SHORTHAND, "");
478
+ if (token.children) markShorthands(token.children);
479
+ }
480
+ }
481
+ const shorthands = defineComarkPlugin(() => ({
482
+ name: "editor-shorthands",
483
+ markdownItPost: (state) => markShorthands(state.tokens)
484
+ }));
485
+ /**
486
+ * The syntax the codec reads beyond Comark's own: CriticMarkup, definition lists, footnotes, and emphasis next to CJK
487
+ * punctuation. Each would otherwise reach the tree as text, or not at all, and be written back escaped or dropped.
488
+ * It also marks the components written `:name` alone on their line, so they're written back that way.
489
+ */
490
+ const syntax = [
491
+ noAlert(),
492
+ critic(),
493
+ substitutions(),
494
+ deflist(),
495
+ cjk(),
496
+ footnoteMarkers(),
497
+ shorthands()
498
+ ];
499
+ //#endregion
156
500
  //#region src/editor/markdown.ts
157
501
  const parseOptions = {
158
502
  autoUnwrap: false,
159
503
  headingIds: false
160
504
  };
505
+ const readOptions = {
506
+ ...parseOptions,
507
+ plugins: syntax
508
+ };
161
509
  const marks = /* @__PURE__ */ new Map([
162
510
  ["strong", "bold"],
163
511
  ["em", "italic"],
@@ -165,13 +513,20 @@ const marks = /* @__PURE__ */ new Map([
165
513
  ["code", "code"],
166
514
  ["a", "link"]
167
515
  ]);
516
+ const DEFINITION = "cfn-def:";
517
+ const REFERENCE = "cfn-ref:";
518
+ const NOTE = "cfn-note";
168
519
  const markTags = new Map([...marks].map(([tag, mark]) => [mark, tag]));
169
520
  const inlineTags = /* @__PURE__ */ new Set([
170
521
  ...marks.keys(),
171
522
  "br",
172
523
  "img",
173
524
  "span",
174
- "input"
525
+ "input",
526
+ "ins",
527
+ "mark",
528
+ SUBSTITUTION,
529
+ NOTE
175
530
  ]);
176
531
  const headings = /* @__PURE__ */ new Set([
177
532
  "h1",
@@ -181,24 +536,14 @@ const headings = /* @__PURE__ */ new Set([
181
536
  "h5",
182
537
  "h6"
183
538
  ]);
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
539
  const isText = (node) => !Array.isArray(node);
196
540
  const isElement = (node) => Array.isArray(node) && node[0] !== null;
197
541
  const childrenOf = (element) => element.slice(2);
198
542
  /** A prop as text. Comark props are strings, numbers, and booleans, or parsed JSON for `:prop` bindings. */
199
543
  const textOf = (value) => value === void 0 || value === null || value instanceof Object ? void 0 : String(value);
200
544
  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]));
545
+ const isInline = (node) => isText(node) || isElement(node) && (inlineTags.has(node[0]) || node[0].startsWith(REFERENCE) || inlineHtml(node[1]));
546
+ const isHtml = (props) => props.$ instanceof Object;
202
547
  /** The props a node has no field for, or `undefined` when none are left. */
203
548
  function rest(props, ...keys) {
204
549
  const left = Object.fromEntries(Object.entries(props).filter(([key]) => !keys.includes(key)));
@@ -249,20 +594,38 @@ function readBlock(child, reading) {
249
594
  if (!isElement(child)) return [node("comarkComment", { text: child[2] })];
250
595
  const [tag, props] = child;
251
596
  const children = childrenOf(child);
597
+ if ("data-comark-editor-shorthand" in props) {
598
+ const { [SHORTHAND]: _, ...attrs } = props;
599
+ return [node("paragraph", {}, readInline([[
600
+ tag,
601
+ attrs,
602
+ ...children
603
+ ]], [], reading))];
604
+ }
252
605
  if (tag === "p") return [node("paragraph", withProps({}, rest(props)), readInline(children, [], reading))];
253
606
  if (headings.has(tag)) return [node("heading", withProps({ level: Number(tag[1]) }, rest(props)), readInline(children, [], reading))];
254
607
  if (tag === "blockquote") return [node("blockquote", withProps({}, rest(props)), readBlocks(children, reading))];
255
608
  if (tag === "hr") return [node("horizontalRule", withProps({}, rest(props)))];
256
609
  if (tag === "pre") return [readCode(props, children)];
257
- if (tag === "table" && children.every((part) => isElement(part) && [
610
+ if (tag === "table" && children.length && children.every((part) => isElement(part) && [
258
611
  "thead",
259
612
  "tbody",
260
613
  "tr"
261
614
  ].includes(part[0]))) return [readTable(props, children, reading)];
262
615
  if (tag === "ul" || tag === "ol") return [readList(tag, props, children, reading)];
263
- if (tag === "template" || builtIn.has(tag)) reading.lossy = true;
616
+ if (tag.startsWith(DEFINITION)) return [node("footnoteDefinition", { label: tag.slice(8) }, filled(readBlocks(children, reading)))];
617
+ if (tag === "dl") return [readDefinitions(props, children, reading)];
618
+ if (tag === "template") reading.lossy = true;
264
619
  return [node("comarkBlock", withProps({ name: tag }, rest(props)), readComponent(children, reading))];
265
620
  }
621
+ /** Blocks that need at least one, as an empty item or definition writes nothing. */
622
+ const filled = (blocks) => blocks.length ? blocks : [{ type: "paragraph" }];
623
+ function readDefinitions(props, children, reading) {
624
+ return node("definitionList", withProps({}, rest(props)), children.flatMap((item) => {
625
+ if (!isElement(item)) return [];
626
+ return item[0] === "dt" ? [node("definitionTerm", withProps({}, rest(item[1])), readInline(childrenOf(item), [], reading))] : [node("definitionDescription", withProps({}, rest(item[1])), filled(readBlocks(childrenOf(item), reading)))];
627
+ }));
628
+ }
266
629
  function readComponent(children, reading) {
267
630
  const out = [];
268
631
  let run = [];
@@ -326,6 +689,29 @@ function readTable(props, children, reading) {
326
689
  const rows = children.filter(isElement).flatMap((section) => section[0] === "tr" ? [section] : childrenOf(section).filter(isElement));
327
690
  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
691
  }
692
+ const critics = /* @__PURE__ */ new Map([
693
+ ["ins", {
694
+ className: "ccm-ins",
695
+ type: "criticInsertion"
696
+ }],
697
+ ["del", {
698
+ className: "ccm-del",
699
+ type: "criticDeletion"
700
+ }],
701
+ ["mark", {
702
+ className: "ccm-mark",
703
+ type: "criticHighlight"
704
+ }]
705
+ ]);
706
+ const COMMENT = "ccm-comment";
707
+ function criticMark([tag, props]) {
708
+ const critic = critics.get(tag);
709
+ if (!critic || !classes(props).includes(critic.className)) return;
710
+ return {
711
+ className: critic.className,
712
+ mark: { type: critic.type }
713
+ };
714
+ }
329
715
  function readInline(children, active, reading) {
330
716
  const out = [];
331
717
  const marked = (json) => active.length ? {
@@ -346,8 +732,24 @@ function readInline(children, active, reading) {
346
732
  }
347
733
  const [tag, props] = child;
348
734
  const content = childrenOf(child);
735
+ const critic = criticMark(child);
349
736
  const mark = marks.get(tag);
350
- if (mark && !(tag === "code" && !content.every(isText))) {
737
+ if (critic) {
738
+ if (rest(withoutClass(props, critic.className))) reading.lossy = true;
739
+ out.push(...readInline(content, [...active, critic.mark], reading));
740
+ } else if (tag === "critic-sub") {
741
+ const [removed, added] = content.filter(isElement);
742
+ const half = (element, type) => element ? readInline(childrenOf(element), [...active, {
743
+ type,
744
+ attrs: { substitution: true }
745
+ }], reading) : [];
746
+ out.push(...half(removed, "criticDeletion"), ...half(added, "criticInsertion"));
747
+ } else if (tag === "span" && classes(props).includes(COMMENT)) {
748
+ if (rest(withoutClass(props, COMMENT))) reading.lossy = true;
749
+ out.push(marked(node("criticComment", {}, readInline(content, [], reading))));
750
+ } else if (tag.startsWith(REFERENCE)) out.push(marked(node("footnoteReference", { label: tag.slice(8) })));
751
+ else if (tag === NOTE) out.push(marked(node("inlineFootnote", {}, readInline(content, [], reading))));
752
+ else if (mark && !(tag === "code" && !content.every(isText))) {
351
753
  const attrs = mark === "link" ? withProps({
352
754
  href: props.href ?? null,
353
755
  title: props.title ?? null
@@ -394,7 +796,8 @@ function writeBlock(json) {
394
796
  case "horizontalRule": return [["hr", props]];
395
797
  case "codeBlock": {
396
798
  const language = textOf(attrs.language) || void 0;
397
- return [[
799
+ const code = plainText(json.content);
800
+ const pre = [
398
801
  "pre",
399
802
  language ? {
400
803
  ...props,
@@ -403,9 +806,10 @@ function writeBlock(json) {
403
806
  [
404
807
  "code",
405
808
  language ? { class: `language-${language}` } : {},
406
- plainText(json.content)
809
+ code
407
810
  ]
408
- ]];
811
+ ];
812
+ return [code.trimEnd() === code ? pre : own(pre, trailingLines)];
409
813
  }
410
814
  case "bulletList": return [[
411
815
  "ul",
@@ -433,39 +837,48 @@ function writeBlock(json) {
433
837
  withClass(props, "contains-task-list"),
434
838
  ...writeBlocks(json.content)
435
839
  ]];
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
- }
840
+ case "taskItem": return [own([
841
+ "li",
842
+ props,
843
+ ...writeBlocks(json.content)
844
+ ], task(Boolean(attrs.checked)))];
456
845
  case "table": return [writeTable(json, props)];
457
- case "comarkBlock": return [[
458
- String(attrs.name),
846
+ case "comarkBlock": {
847
+ const component = [
848
+ String(attrs.name),
849
+ props,
850
+ ...(json.content ?? []).flatMap((child) => child.type === "comarkSlot" ? [[
851
+ "template",
852
+ {
853
+ ...propsOf(child.attrs),
854
+ name: child.attrs?.name
855
+ },
856
+ ...writeBlocks(child.content)
857
+ ]] : writeBlock(child))
858
+ ];
859
+ return [isHtml(props) ? component : own(component, blockComponent)];
860
+ }
861
+ case "footnoteDefinition": {
862
+ const label = textOf(attrs.label) ?? "";
863
+ return [own([
864
+ "footnote",
865
+ {},
866
+ ...writeBlocks(json.content)
867
+ ], footnoteDefinition(label))];
868
+ }
869
+ case "definitionList": return [own([
870
+ "dl",
459
871
  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
- ]];
872
+ ...(json.content ?? []).map((item) => item.type === "definitionTerm" ? [
873
+ "dt",
874
+ propsOf(item.attrs),
875
+ ...writeInline(item.content)
876
+ ] : [
877
+ "dd",
878
+ propsOf(item.attrs),
879
+ ...writeBlocks(item.content)
880
+ ])
881
+ ], definitionList)];
469
882
  case "comarkComment": return [[
470
883
  null,
471
884
  {},
@@ -505,7 +918,18 @@ function writeTable(json, props) {
505
918
  ];
506
919
  }
507
920
  const sameMark = (a, b) => a.type === b.type && JSON.stringify(a.attrs ?? {}) === JSON.stringify(b.attrs ?? {});
921
+ const criticDelimiters = /* @__PURE__ */ new Map([
922
+ ["criticInsertion", ["{++", "++}"]],
923
+ ["criticDeletion", ["{--", "--}"]],
924
+ ["criticHighlight", ["{==", "==}"]]
925
+ ]);
508
926
  function writeMark(mark, children) {
927
+ const delimiters = criticDelimiters.get(mark.type);
928
+ if (delimiters) return own([
929
+ mark.attrs?.substitution ? mark.type : "critic",
930
+ {},
931
+ ...children
932
+ ], between(...delimiters));
509
933
  const props = propsOf(mark.attrs);
510
934
  if (mark.type !== "link") return [
511
935
  markTags.get(mark.type) ?? "span",
@@ -537,7 +961,7 @@ function writeInline(content = []) {
537
961
  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
962
  };
539
963
  for (const json of content) {
540
- const active = json.marks ?? [];
964
+ const active = [...json.marks ?? []].sort((left, right) => markOrder(left) - markOrder(right));
541
965
  const same = (index) => {
542
966
  const frame = open[index];
543
967
  const mark = active[index];
@@ -553,7 +977,23 @@ function writeInline(content = []) {
553
977
  for (const child of writeLeaf(json)) append(open.at(-1)?.children ?? root, child);
554
978
  }
555
979
  if (open.length) close(0);
556
- return root;
980
+ return pairSubstitutions(root);
981
+ }
982
+ const markOrder = (mark) => criticDelimiters.has(mark.type) ? -1 : mark.type === "code" ? 1 : 0;
983
+ /** Joins a deletion and the insertion right after it, both from one `{~~old~>new~~}`, back into one. */
984
+ function pairSubstitutions(children) {
985
+ const out = [];
986
+ for (const child of children) {
987
+ const last = out.at(-1);
988
+ if (isElement(child) && child[0] === "criticInsertion" && last !== void 0 && isElement(last) && last[0] === "criticDeletion") out[out.length - 1] = own([
989
+ SUBSTITUTION,
990
+ {},
991
+ last,
992
+ child
993
+ ], substitution$1);
994
+ else out.push(child);
995
+ }
996
+ return out;
557
997
  }
558
998
  function writeLeaf(json) {
559
999
  const props = propsOf(json.attrs);
@@ -569,11 +1009,25 @@ function writeLeaf(json) {
569
1009
  if (attrs.title) image.title = attrs.title;
570
1010
  return [["img", image]];
571
1011
  }
572
- if (json.type === "comarkInline") return [[
573
- String(attrs.name),
574
- props,
1012
+ if (json.type === "comarkInline") {
1013
+ const component = [
1014
+ String(attrs.name),
1015
+ props,
1016
+ ...writeInline(json.content)
1017
+ ];
1018
+ return [isHtml(props) ? component : own(component, inlineComponent)];
1019
+ }
1020
+ if (json.type === "footnoteReference") return [own(["footnote", {}], footnoteReference(textOf(attrs.label) ?? ""))];
1021
+ if (json.type === "inlineFootnote") return [own([
1022
+ "footnote",
1023
+ {},
1024
+ ...writeInline(json.content)
1025
+ ], between("^[", "]"))];
1026
+ if (json.type === "criticComment") return [own([
1027
+ "span",
1028
+ {},
575
1029
  ...writeInline(json.content)
576
- ]];
1030
+ ], between("{>>", "<<}"))];
577
1031
  return [];
578
1032
  }
579
1033
  function equal(a, b) {
@@ -585,7 +1039,7 @@ function equal(a, b) {
585
1039
  }
586
1040
  /** Reads Markdown into the editor's document. */
587
1041
  async function fromMarkdown(markdown) {
588
- const tree = await parseMarkdown(markdown, parseOptions);
1042
+ const tree = await parseMarkdown(markdown, readOptions);
589
1043
  const reading = { lossy: false };
590
1044
  const content = readBlocks(tree.nodes, reading);
591
1045
  return {
@@ -603,7 +1057,7 @@ async function toMarkdown(doc, options = {}) {
603
1057
  nodes: writeBlocks(doc.content),
604
1058
  frontmatter: options.frontmatter ?? {},
605
1059
  meta: {}
606
- }, { headingIds: false });
1060
+ }, writeOptions);
607
1061
  return markdown.trim() ? `${markdown.trimEnd()}\n` : "";
608
1062
  }
609
1063
  /**
@@ -832,4 +1286,4 @@ function createEditorCodec(options = {}) {
832
1286
  };
833
1287
  }
834
1288
  //#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 };
1289
+ export { FootnoteDefinition as C, DefinitionTerm as S, InlineFootnote as T, CriticDeletion as _, uploadKey as a, DefinitionDescription as b, fromMarkdown as c, ComarkComment as d, ComarkImage as f, CriticComment as g, 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, CriticHighlight as v, FootnoteReference as w, DefinitionList as x, CriticInsertion as y };