@pixerate/editor 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +20 -11
  2. package/dist/agent/index.cjs +8 -8
  3. package/dist/agent/index.d.cts +2 -2
  4. package/dist/agent/index.d.ts +2 -2
  5. package/dist/agent/index.js +2 -2
  6. package/dist/canvas/index.cjs +1269 -13
  7. package/dist/canvas/index.cjs.map +1 -1
  8. package/dist/canvas/index.d.cts +432 -6
  9. package/dist/canvas/index.d.ts +432 -6
  10. package/dist/canvas/index.js +1264 -8
  11. package/dist/canvas/index.js.map +1 -1
  12. package/dist/{chunk-U6NDQFOK.cjs → chunk-6M3BCTBD.cjs} +427 -138
  13. package/dist/chunk-6M3BCTBD.cjs.map +1 -0
  14. package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
  15. package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
  16. package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
  17. package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
  18. package/dist/{chunk-A7DKIA72.js → chunk-DENSCKFC.js} +642 -69
  19. package/dist/chunk-DENSCKFC.js.map +1 -0
  20. package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
  21. package/dist/chunk-GVSMRPHB.cjs.map +1 -0
  22. package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
  23. package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
  24. package/dist/chunk-MJZBNWG7.js.map +1 -0
  25. package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
  26. package/dist/chunk-QGHGJ56O.js.map +1 -0
  27. package/dist/{chunk-GVW4P7BG.js → chunk-RU52I7QO.js} +305 -16
  28. package/dist/chunk-RU52I7QO.js.map +1 -0
  29. package/dist/{chunk-JYRIOFEN.cjs → chunk-SAZRENEZ.cjs} +647 -74
  30. package/dist/chunk-SAZRENEZ.cjs.map +1 -0
  31. package/dist/{controller-OUdxPY-X.d.ts → controller-B7GIybc4.d.cts} +139 -7
  32. package/dist/{controller-OUdxPY-X.d.cts → controller-B7GIybc4.d.ts} +139 -7
  33. package/dist/extensions/index.cjs +14 -4
  34. package/dist/extensions/index.cjs.map +1 -1
  35. package/dist/extensions/index.d.cts +3 -3
  36. package/dist/extensions/index.d.ts +3 -3
  37. package/dist/extensions/index.js +13 -3
  38. package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
  39. package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
  40. package/dist/index.cjs +250 -16
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +294 -8
  43. package/dist/index.d.ts +294 -8
  44. package/dist/index.js +241 -7
  45. package/dist/index.js.map +1 -1
  46. package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
  47. package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
  48. package/dist/serializers/index.cjs +3 -3
  49. package/dist/serializers/index.d.cts +1 -1
  50. package/dist/serializers/index.d.ts +1 -1
  51. package/dist/serializers/index.js +2 -2
  52. package/dist/spreadsheet/index.cjs +7 -3
  53. package/dist/spreadsheet/index.cjs.map +1 -1
  54. package/dist/spreadsheet/index.d.cts +33 -4
  55. package/dist/spreadsheet/index.d.ts +33 -4
  56. package/dist/spreadsheet/index.js +6 -2
  57. package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
  58. package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
  59. package/dist/{types-DMe0vFB1.d.cts → types-CJ4U-HHI.d.cts} +1 -1
  60. package/dist/{types-DMe0vFB1.d.ts → types-CJ4U-HHI.d.ts} +1 -1
  61. package/package.json +1 -1
  62. package/dist/chunk-3TYMNULT.js.map +0 -1
  63. package/dist/chunk-4QPLEUL2.cjs.map +0 -1
  64. package/dist/chunk-A7DKIA72.js.map +0 -1
  65. package/dist/chunk-CYFJCKC4.js.map +0 -1
  66. package/dist/chunk-GVW4P7BG.js.map +0 -1
  67. package/dist/chunk-JYRIOFEN.cjs.map +0 -1
  68. package/dist/chunk-TVRC4KVT.cjs.map +0 -1
  69. package/dist/chunk-U6NDQFOK.cjs.map +0 -1
  70. /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
@@ -245,11 +245,32 @@ import {
245
245
  } from "@tiptap/pm/state";
246
246
  import {
247
247
  Fragment,
248
+ Mark as ProsemirrorMark,
249
+ NodeRange,
248
250
  Slice
249
251
  } from "@tiptap/pm/model";
250
- import { Mapping, ReplaceStep, Transform } from "@tiptap/pm/transform";
252
+ import {
253
+ AddMarkStep,
254
+ AttrStep,
255
+ Mapping,
256
+ RemoveMarkStep,
257
+ ReplaceAroundStep,
258
+ ReplaceStep,
259
+ Transform,
260
+ canJoin,
261
+ liftTarget
262
+ } from "@tiptap/pm/transform";
251
263
  var INSERTION_MARK_NAME = "insertion";
252
264
  var DELETION_MARK_NAME = "deletion";
265
+ var FORMAT_CHANGE_MARK_NAME = "formatChange";
266
+ var BLOCK_CHANGE_ATTRIBUTE = "blockChange";
267
+ var REVIEW_MARK_NAMES = /* @__PURE__ */ new Set([
268
+ INSERTION_MARK_NAME,
269
+ DELETION_MARK_NAME,
270
+ FORMAT_CHANGE_MARK_NAME,
271
+ "comment",
272
+ "bookmark"
273
+ ]);
253
274
  var TRACK_CHANGES_SKIP_META = "trackChangesSkip";
254
275
  var TrackChangesStore = class {
255
276
  snapshot;
@@ -281,6 +302,14 @@ var trackChangesPluginKey = new PluginKey("trackChanges");
281
302
  function changeAttributes(mark) {
282
303
  return mark.attrs;
283
304
  }
305
+ function parseJSON(value, fallback) {
306
+ if (!value) return fallback;
307
+ try {
308
+ return JSON.parse(value);
309
+ } catch {
310
+ return fallback;
311
+ }
312
+ }
284
313
  var sharedAttributes = () => ({
285
314
  changeId: {
286
315
  default: null,
@@ -367,6 +396,172 @@ var TrackedDeletion = Mark.create({
367
396
  ];
368
397
  }
369
398
  });
399
+ var TrackedFormatChange = Mark.create({
400
+ name: FORMAT_CHANGE_MARK_NAME,
401
+ inclusive: false,
402
+ addOptions() {
403
+ return {
404
+ HTMLAttributes: {
405
+ class: "pixerate-format-change underline decoration-dotted decoration-2 decoration-sky-500 underline-offset-4"
406
+ }
407
+ };
408
+ },
409
+ addStorage() {
410
+ return { markdown: { serialize: { open: "", close: "" }, parse: {} } };
411
+ },
412
+ addAttributes() {
413
+ return {
414
+ ...sharedAttributes(),
415
+ before: {
416
+ default: [],
417
+ parseHTML: (el) => parseJSON(el.getAttribute("data-format-before"), []),
418
+ renderHTML: (attrs) => ({ "data-format-before": JSON.stringify(attrs.before ?? []) })
419
+ },
420
+ after: {
421
+ default: [],
422
+ parseHTML: (el) => parseJSON(el.getAttribute("data-format-after"), []),
423
+ renderHTML: (attrs) => ({ "data-format-after": JSON.stringify(attrs.after ?? []) })
424
+ }
425
+ };
426
+ },
427
+ parseHTML() {
428
+ return [{ tag: 'span[data-change-type="format"]', priority: 100 }];
429
+ },
430
+ renderHTML({ HTMLAttributes }) {
431
+ return [
432
+ "span",
433
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
434
+ "data-change-type": "format"
435
+ }),
436
+ 0
437
+ ];
438
+ }
439
+ });
440
+ var MARK_LABELS = {
441
+ bold: "Bold",
442
+ italic: "Italic",
443
+ underline: "Underline",
444
+ strike: "Strikethrough",
445
+ code: "Code",
446
+ link: "Link",
447
+ highlight: "Highlight",
448
+ subscript: "Subscript",
449
+ superscript: "Superscript",
450
+ textStyle: "Text style"
451
+ };
452
+ var TEXT_STYLE_LABELS = {
453
+ fontSize: "Font size",
454
+ fontFamily: "Font",
455
+ color: "Color",
456
+ backgroundColor: "Background color",
457
+ lineHeight: "Line height"
458
+ };
459
+ var BLOCK_LABELS = {
460
+ paragraph: "Paragraph",
461
+ codeBlock: "Code block",
462
+ blockquote: "Quote",
463
+ bulletList: "Bulleted list",
464
+ orderedList: "Numbered list",
465
+ taskList: "Task list",
466
+ listItem: "List item",
467
+ taskItem: "Task item"
468
+ };
469
+ function humanize(name) {
470
+ const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").toLowerCase();
471
+ return words.charAt(0).toUpperCase() + words.slice(1);
472
+ }
473
+ function stableStringify(value) {
474
+ if (value === void 0) return "null";
475
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
476
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
477
+ const obj = value;
478
+ return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
479
+ }
480
+ var sameValue = (a, b) => stableStringify(a) === stableStringify(b);
481
+ function markLabels(mark, other) {
482
+ if (mark.type === "textStyle") {
483
+ const attrs = mark.attrs ?? {};
484
+ const otherAttrs = other?.attrs ?? {};
485
+ const keys = Array.from(/* @__PURE__ */ new Set([...Object.keys(attrs), ...Object.keys(otherAttrs)])).filter(
486
+ (key) => other ? !sameValue(attrs[key], otherAttrs[key]) : attrs[key] != null
487
+ );
488
+ return keys.length ? keys.map((key) => TEXT_STYLE_LABELS[key] ?? humanize(key)) : ["Text style"];
489
+ }
490
+ return [MARK_LABELS[mark.type] ?? humanize(mark.type)];
491
+ }
492
+ function diffFormatting(before, after) {
493
+ const summary = { added: [], removed: [], changed: [] };
494
+ const push = (list, labels) => labels.forEach((label) => {
495
+ if (!list.includes(label)) list.push(label);
496
+ });
497
+ after.forEach((mark) => {
498
+ const old = before.find((m) => m.type === mark.type);
499
+ if (!old) push(summary.added, markLabels(mark));
500
+ else if (!sameValue(old.attrs ?? {}, mark.attrs ?? {})) push(summary.changed, markLabels(mark, old));
501
+ });
502
+ before.forEach((mark) => {
503
+ if (!after.some((m) => m.type === mark.type)) push(summary.removed, markLabels(mark));
504
+ });
505
+ return summary;
506
+ }
507
+ function mergeSummary(target, source) {
508
+ ["added", "removed", "changed"].forEach(
509
+ (key) => source[key].forEach((label) => {
510
+ if (!target[key].includes(label)) target[key].push(label);
511
+ })
512
+ );
513
+ }
514
+ function describeFormatChange(summary) {
515
+ const parts = [];
516
+ if (summary.added.length) parts.push(`Formatted: ${summary.added.join(", ")}`);
517
+ if (summary.removed.length) parts.push(`Removed formatting: ${summary.removed.join(", ")}`);
518
+ if (summary.changed.length) parts.push(`Changed formatting: ${summary.changed.join(", ")}`);
519
+ return parts.join("; ") || "Formatted";
520
+ }
521
+ function blockLabel(type, attrs) {
522
+ if (type === "heading" && attrs?.level) return `Heading ${attrs.level}`;
523
+ return BLOCK_LABELS[type] ?? humanize(type);
524
+ }
525
+ var containerLabels = (wrappers) => wrappers.filter((w) => w.type !== "listItem" && w.type !== "taskItem").map((w) => blockLabel(w.type, w.attrs));
526
+ var isPrefix = (a, b) => a.every((label, i) => b[i] === label);
527
+ function describeBlockChange(previous, current, ignoredAttributes = ["id"]) {
528
+ const parts = [];
529
+ const before = blockLabel(previous.type, previous.attrs);
530
+ const after = blockLabel(current.type, current.attrs);
531
+ if (before !== after) {
532
+ parts.push(`Changed ${before} to ${after}`);
533
+ } else {
534
+ const keys = Array.from(
535
+ /* @__PURE__ */ new Set([...Object.keys(previous.attrs ?? {}), ...Object.keys(current.attrs ?? {})])
536
+ ).filter(
537
+ (key) => key !== BLOCK_CHANGE_ATTRIBUTE && !ignoredAttributes.includes(key) && !(current.type === "heading" && key === "level") && !sameValue(previous.attrs?.[key], current.attrs?.[key])
538
+ );
539
+ keys.forEach((key) => {
540
+ if (key === "textAlign") {
541
+ parts.push(`Changed alignment to ${humanize(String(current.attrs?.textAlign ?? "default"))}`);
542
+ } else {
543
+ parts.push(`Changed ${humanize(key).toLowerCase()} of ${after}`);
544
+ }
545
+ });
546
+ }
547
+ const oldTypes = previous.wrappers.map((w) => w.type).join("/");
548
+ const newTypes = current.wrappers.map((w) => w.type).join("/");
549
+ if (oldTypes !== newTypes) {
550
+ const was = containerLabels(previous.wrappers);
551
+ const now = containerLabels(current.wrappers);
552
+ const last = (list) => list[list.length - 1];
553
+ if (was.length === 0 && now.length > 0) parts.push(`Wrapped in ${last(now)}`);
554
+ else if (now.length === 0 && was.length > 0) parts.push(`Removed from ${last(was)}`);
555
+ else if (now.length > was.length && isPrefix(was, now)) parts.push(`Indented in ${last(now)}`);
556
+ else if (now.length < was.length && isPrefix(now, was)) parts.push(`Outdented from ${last(was)}`);
557
+ else if (was.length && now.length) parts.push(`Changed ${last(was)} to ${last(now)}`);
558
+ else parts.push("Changed nesting");
559
+ }
560
+ return parts.join("; ") || `Changed ${after}`;
561
+ }
562
+ function truncate(text, max = 40) {
563
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
564
+ }
370
565
  function toDoc(source) {
371
566
  if (!source) return null;
372
567
  if (source.state?.doc) return source.state.doc;
@@ -374,55 +569,136 @@ function toDoc(source) {
374
569
  if (typeof source.descendants === "function") return source;
375
570
  return null;
376
571
  }
572
+ function stripBlockChange(attrs) {
573
+ const copy = { ...attrs };
574
+ delete copy[BLOCK_CHANGE_ATTRIBUTE];
575
+ return copy;
576
+ }
577
+ function blockSnapshot(doc, pos) {
578
+ const $pos = doc.resolve(pos);
579
+ const node = $pos.nodeAfter;
580
+ const wrappers = [];
581
+ for (let depth = 1; depth <= $pos.depth; depth++) {
582
+ const wrapper = $pos.node(depth);
583
+ wrappers.push({ type: wrapper.type.name, attrs: stripBlockChange(wrapper.attrs) });
584
+ }
585
+ return { type: node.type.name, attrs: stripBlockChange(node.attrs), wrappers };
586
+ }
587
+ function sameBlock(a, b, ignored) {
588
+ if (a.type !== b.type) return false;
589
+ const filter = (attrs) => Object.fromEntries(
590
+ Object.entries(attrs ?? {}).filter(
591
+ ([key]) => key !== BLOCK_CHANGE_ATTRIBUTE && !ignored.has(key)
592
+ )
593
+ );
594
+ if (!sameValue(filter(a.attrs), filter(b.attrs))) return false;
595
+ if (a.wrappers.length !== b.wrappers.length) return false;
596
+ return a.wrappers.every((w, i) => w.type === b.wrappers[i].type);
597
+ }
598
+ var isMarkChange = (type) => type === INSERTION_MARK_NAME || type === DELETION_MARK_NAME || type === FORMAT_CHANGE_MARK_NAME;
377
599
  function getTrackedChanges(source) {
378
600
  const doc = toDoc(source);
379
601
  if (!doc) return [];
380
602
  const byKey = /* @__PURE__ */ new Map();
603
+ const add = (key, type, attrs, from, to, text) => {
604
+ let change = byKey.get(key);
605
+ if (!change) {
606
+ change = {
607
+ id: attrs.changeId,
608
+ type,
609
+ author: {
610
+ id: attrs.authorId,
611
+ name: attrs.authorName,
612
+ ...attrs.authorColor ? { color: attrs.authorColor } : {}
613
+ },
614
+ createdAt: attrs.createdAt,
615
+ from,
616
+ to,
617
+ ranges: [],
618
+ text: "",
619
+ description: ""
620
+ };
621
+ byKey.set(key, change);
622
+ }
623
+ const last = change.ranges[change.ranges.length - 1];
624
+ if (last && last.to === from) {
625
+ last.to = to;
626
+ } else {
627
+ change.ranges.push({ from, to });
628
+ }
629
+ change.to = to;
630
+ change.text += text;
631
+ return change;
632
+ };
381
633
  doc.descendants((node, pos) => {
634
+ const block = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE];
635
+ if (block?.changeId && node.isTextblock) {
636
+ const change = add(
637
+ `block:${block.changeId}`,
638
+ "block",
639
+ block,
640
+ pos,
641
+ pos + node.nodeSize,
642
+ node.textContent
643
+ );
644
+ if (!change.block) {
645
+ change.block = { previous: block.previous, current: blockSnapshot(doc, pos) };
646
+ }
647
+ }
382
648
  if (!node.isInline) return;
383
649
  node.marks.forEach((mark) => {
384
- const type = mark.type.name;
385
- if (type !== INSERTION_MARK_NAME && type !== DELETION_MARK_NAME) return;
650
+ const markType = mark.type.name;
651
+ if (!isMarkChange(markType)) return;
386
652
  const attrs = changeAttributes(mark);
387
653
  if (!attrs.changeId) return;
388
- const key = `${type}:${attrs.changeId}`;
389
- const end = pos + node.nodeSize;
390
- const text = node.isText ? node.text ?? "" : "";
391
- let change = byKey.get(key);
392
- if (!change) {
393
- change = {
394
- id: attrs.changeId,
395
- type,
396
- author: {
397
- id: attrs.authorId,
398
- name: attrs.authorName,
399
- ...attrs.authorColor ? { color: attrs.authorColor } : {}
400
- },
401
- createdAt: attrs.createdAt,
402
- from: pos,
403
- to: end,
404
- ranges: [],
405
- text: ""
406
- };
407
- byKey.set(key, change);
654
+ const type = markType === FORMAT_CHANGE_MARK_NAME ? "format" : markType;
655
+ const change = add(
656
+ `${type}:${attrs.changeId}`,
657
+ type,
658
+ attrs,
659
+ pos,
660
+ pos + node.nodeSize,
661
+ node.isText ? node.text ?? "" : ""
662
+ );
663
+ if (type === "format") {
664
+ const summary = diffFormatting(mark.attrs.before ?? [], mark.attrs.after ?? []);
665
+ if (!change.format) change.format = { added: [], removed: [], changed: [] };
666
+ mergeSummary(change.format, summary);
408
667
  }
409
- const last = change.ranges[change.ranges.length - 1];
410
- if (last && last.to === pos) {
411
- last.to = end;
412
- } else {
413
- change.ranges.push({ from: pos, to: end });
414
- }
415
- change.to = end;
416
- change.text += text;
417
668
  });
418
669
  });
419
- return Array.from(byKey.values()).sort((a, b) => a.from - b.from || a.to - b.to);
670
+ const changes = Array.from(byKey.values());
671
+ changes.forEach((change) => {
672
+ switch (change.type) {
673
+ case "insertion":
674
+ change.description = change.text ? `Inserted "${truncate(change.text)}"` : "Inserted content";
675
+ break;
676
+ case "deletion":
677
+ change.description = change.text ? `Deleted "${truncate(change.text)}"` : "Deleted content";
678
+ break;
679
+ case "format":
680
+ change.description = describeFormatChange(change.format);
681
+ break;
682
+ case "block":
683
+ change.description = describeBlockChange(change.block.previous, change.block.current);
684
+ break;
685
+ }
686
+ });
687
+ return changes.sort((a, b) => a.from - b.from || a.to - b.to);
420
688
  }
421
689
  function getTrackedChangeAt(state, pos = state.selection.from, changes = getTrackedChanges(state.doc)) {
422
690
  const { from, to } = state.selection;
423
- const exact = changes.find((c) => c.from === from && c.to === to);
691
+ const exact = changes.find((c) => {
692
+ const [start, end] = selectionRange(c);
693
+ return start === from && end === to;
694
+ });
424
695
  if (exact) return exact;
425
- return changes.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ?? changes.find((c) => c.ranges.some((r) => r.from <= pos && pos <= r.to)) ?? null;
696
+ const inline = changes.filter((c) => c.type !== "block");
697
+ const blocks = changes.filter((c) => c.type === "block");
698
+ return inline.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ?? inline.find((c) => c.ranges.some((r) => r.from <= pos && pos <= r.to)) ?? blocks.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ?? null;
699
+ }
700
+ function selectionRange(change) {
701
+ return change.type === "block" ? [change.from + 1, change.to - 1] : [change.from, change.to];
426
702
  }
427
703
  function isTrackChangesEnabled(source) {
428
704
  const state = source?.state ?? source;
@@ -432,10 +708,225 @@ function isTrackChangesEnabled(source) {
432
708
  function getTrackChangesStore(editor) {
433
709
  return editor?.storage?.trackChanges?.store ?? null;
434
710
  }
711
+ function marksFromJSON(schema, marks) {
712
+ const result = [];
713
+ (marks ?? []).forEach((json) => {
714
+ try {
715
+ if (schema.marks[json.type]) result.push(schema.markFromJSON(json));
716
+ } catch {
717
+ }
718
+ });
719
+ return result;
720
+ }
721
+ function decideFormat(tr, changes, decision) {
722
+ const schema = tr.doc.type.schema;
723
+ const formatType = schema.marks[FORMAT_CHANGE_MARK_NAME];
724
+ if (!formatType) return;
725
+ const ids = new Set(changes.map((c) => c.id));
726
+ const segments = [];
727
+ changes.forEach(
728
+ (change) => change.ranges.forEach(
729
+ ({ from, to }) => tr.doc.nodesBetween(from, to, (node, pos) => {
730
+ if (!node.isInline) return true;
731
+ const mark = formatType.isInSet(node.marks);
732
+ if (mark && ids.has(mark.attrs.changeId)) {
733
+ segments.push({ from: Math.max(pos, from), to: Math.min(pos + node.nodeSize, to), node, mark });
734
+ }
735
+ return false;
736
+ })
737
+ )
738
+ );
739
+ segments.forEach(({ from, to, node, mark }) => {
740
+ tr.removeMark(from, to, formatType);
741
+ if (decision === "accept") return;
742
+ const before = mark.attrs.before ?? [];
743
+ const after = mark.attrs.after ?? [];
744
+ const involved = new Set([...before, ...after].map((m) => m.type));
745
+ node.marks.forEach((current) => {
746
+ if (involved.has(current.type.name)) tr.removeMark(from, to, current);
747
+ });
748
+ marksFromJSON(schema, before).forEach((restored) => tr.addMark(from, to, restored));
749
+ });
750
+ }
751
+ var LIST_ITEM_TYPES = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
752
+ function listItemAt(tr, contentPos) {
753
+ const $pos = tr.doc.resolve(contentPos);
754
+ const itemDepth = $pos.depth - 1;
755
+ if (itemDepth < 2) return null;
756
+ if (!LIST_ITEM_TYPES.has($pos.node(itemDepth).type.name) || $pos.index(itemDepth) !== 0) {
757
+ return null;
758
+ }
759
+ return { $pos, itemDepth };
760
+ }
761
+ function sinkItem(tr, contentPos) {
762
+ const info = listItemAt(tr, contentPos);
763
+ if (!info) return false;
764
+ const { $pos, itemDepth } = info;
765
+ const list = $pos.node(itemDepth - 1);
766
+ const index = $pos.index(itemDepth - 1);
767
+ const itemType = $pos.node(itemDepth).type;
768
+ if (index === 0 || list.child(index - 1).type !== itemType) return false;
769
+ const nodeBefore = list.child(index - 1);
770
+ const nestedBefore = nodeBefore.lastChild?.type === list.type;
771
+ const inner = Fragment.from(nestedBefore ? itemType.create() : null);
772
+ const slice = new Slice(
773
+ Fragment.from(itemType.create(null, Fragment.from(list.type.create(null, inner)))),
774
+ nestedBefore ? 3 : 1,
775
+ 0
776
+ );
777
+ const before = $pos.before(itemDepth);
778
+ const after = $pos.after(itemDepth);
779
+ tr.step(
780
+ new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true)
781
+ );
782
+ return true;
783
+ }
784
+ function outdentItem(tr, contentPos) {
785
+ const info = listItemAt(tr, contentPos);
786
+ if (!info) return false;
787
+ const { $pos, itemDepth } = info;
788
+ const listDepth = itemDepth - 1;
789
+ if (listDepth < 2 || !LIST_ITEM_TYPES.has($pos.node(listDepth - 1).type.name)) return false;
790
+ const itemType = $pos.node(itemDepth).type;
791
+ let range = new NodeRange(
792
+ tr.doc.resolve($pos.before(itemDepth)),
793
+ tr.doc.resolve($pos.after(itemDepth)),
794
+ listDepth
795
+ );
796
+ const end = range.end;
797
+ const endOfList = range.$to.end(range.depth);
798
+ if (end < endOfList) {
799
+ tr.step(
800
+ new ReplaceAroundStep(
801
+ end - 1,
802
+ endOfList,
803
+ end,
804
+ endOfList,
805
+ new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0),
806
+ 1,
807
+ true
808
+ )
809
+ );
810
+ range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
811
+ }
812
+ const target = liftTarget(range);
813
+ if (target == null) return false;
814
+ tr.lift(range, target);
815
+ return true;
816
+ }
817
+ function restoreWrappers(tr, contentPos, wrappers) {
818
+ const schema = tr.doc.type.schema;
819
+ const step = (fn) => {
820
+ const index = tr.steps.length;
821
+ const result = fn();
822
+ contentPos = tr.mapping.slice(index).map(contentPos);
823
+ return result !== false && tr.steps.length > index;
824
+ };
825
+ const chain = () => {
826
+ const $pos = tr.doc.resolve(contentPos);
827
+ const types = [];
828
+ for (let depth = 1; depth < $pos.depth; depth++) types.push($pos.node(depth).type.name);
829
+ return types;
830
+ };
831
+ const target = wrappers.map((w) => w.type);
832
+ for (let guard = 0; guard < 24; guard++) {
833
+ const current = chain();
834
+ if (current.join("/") === target.join("/")) return;
835
+ if (current.length === target.length) {
836
+ const index = current.findIndex((type2, i) => type2 !== target[i]);
837
+ const type = schema.nodes[target[index]];
838
+ if (type && LIST_ITEM_TYPES.has(type.name) === LIST_ITEM_TYPES.has(current[index])) {
839
+ const $pos = tr.doc.resolve(contentPos);
840
+ if (step(() => tr.setNodeMarkup($pos.before(index + 1), type, wrappers[index].attrs))) {
841
+ continue;
842
+ }
843
+ }
844
+ }
845
+ let common = 0;
846
+ while (common < current.length && common < target.length && current[common] === target[common]) {
847
+ common++;
848
+ }
849
+ if (current.length > common) {
850
+ if (step(() => outdentItem(tr, contentPos))) continue;
851
+ const $from2 = tr.doc.resolve(contentPos);
852
+ const range2 = $from2.blockRange(tr.doc.resolve($from2.end()));
853
+ const lift = range2 ? liftTarget(range2) : null;
854
+ if (!range2 || lift == null || !step(() => tr.lift(range2, lift))) return;
855
+ continue;
856
+ }
857
+ const missing = wrappers.slice(current.length);
858
+ if (missing.length >= 2 && LIST_ITEM_TYPES.has(missing[1].type) && step(() => sinkItem(tr, contentPos))) {
859
+ continue;
860
+ }
861
+ const types = missing.map((w) => ({ type: schema.nodes[w.type], attrs: w.attrs }));
862
+ if (types.some((t) => !t.type)) return;
863
+ const $from = tr.doc.resolve(contentPos);
864
+ const range = $from.blockRange(tr.doc.resolve($from.end()));
865
+ if (!range) return;
866
+ const start = range.start;
867
+ step(() => tr.wrap(range, types));
868
+ const wrapper = tr.doc.nodeAt(start);
869
+ if (!wrapper) return;
870
+ const end = start + wrapper.nodeSize;
871
+ if (tr.doc.resolve(end).nodeAfter?.type === wrapper.type && canJoin(tr.doc, end)) {
872
+ step(() => tr.join(end));
873
+ }
874
+ if (tr.doc.resolve(start).nodeBefore?.type === wrapper.type && canJoin(tr.doc, start)) {
875
+ step(() => tr.join(start));
876
+ }
877
+ return;
878
+ }
879
+ }
880
+ function decideBlocks(tr, changes, decision) {
881
+ const ids = new Set(changes.map((c) => c.id));
882
+ const items = [];
883
+ tr.doc.descendants((node, pos) => {
884
+ const record = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE];
885
+ if (node.isTextblock && record?.changeId && ids.has(record.changeId)) {
886
+ items.push({ contentPos: pos + 1, record });
887
+ }
888
+ return !node.isTextblock;
889
+ });
890
+ const schema = tr.doc.type.schema;
891
+ const startIndex = tr.steps.length;
892
+ items.reverse().forEach(({ contentPos: original, record }) => {
893
+ const contentPos = tr.mapping.slice(startIndex).map(original);
894
+ const $pos = tr.doc.resolve(contentPos);
895
+ if (!$pos.parent.isTextblock || $pos.parentOffset !== 0) return;
896
+ const pos = $pos.before();
897
+ if (decision === "accept") {
898
+ tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);
899
+ return;
900
+ }
901
+ const previous = record.previous;
902
+ const type = previous && schema.nodes[previous.type];
903
+ if (!type) {
904
+ tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);
905
+ return;
906
+ }
907
+ const attrs = { ...previous.attrs, [BLOCK_CHANGE_ATTRIBUTE]: null };
908
+ try {
909
+ tr.setNodeMarkup(pos, type, attrs);
910
+ } catch {
911
+ try {
912
+ tr.setBlockType(pos + 1, pos + 1, type, attrs);
913
+ } catch {
914
+ tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);
915
+ }
916
+ }
917
+ try {
918
+ restoreWrappers(tr, tr.mapping.slice(startIndex).map(original), previous.wrappers ?? []);
919
+ } catch {
920
+ }
921
+ });
922
+ }
435
923
  function applyDecision(tr, changes, decision) {
436
924
  const schema = tr.doc.type.schema;
925
+ const format = changes.filter((c) => c.type === "format");
926
+ if (format.length) decideFormat(tr, format, decision);
437
927
  const deletions = [];
438
928
  changes.forEach((change) => {
929
+ if (change.type !== "insertion" && change.type !== "deletion") return;
439
930
  const markType = schema.marks[change.type];
440
931
  if (!markType) return;
441
932
  const keep = change.type === "insertion" === (decision === "accept");
@@ -450,6 +941,8 @@ function applyDecision(tr, changes, decision) {
450
941
  deletions.sort((a, b) => b.from - a.from).forEach(({ from, to }) => {
451
942
  tr.delete(tr.mapping.map(from), tr.mapping.map(to));
452
943
  });
944
+ const blocks = changes.filter((c) => c.type === "block");
945
+ if (blocks.length) decideBlocks(tr, blocks, decision);
453
946
  }
454
947
  function resolveTrackedChanges(doc, view = "accepted") {
455
948
  if (view === "all") return doc;
@@ -468,14 +961,27 @@ function hasInlineContent(fragment) {
468
961
  });
469
962
  return found;
470
963
  }
471
- function acceptSlice(slice, insertion, deletion) {
472
- if (!insertion && !deletion) return slice;
964
+ function acceptSlice(slice, schema) {
965
+ const insertion = schema.marks[INSERTION_MARK_NAME];
966
+ const deletion = schema.marks[DELETION_MARK_NAME];
967
+ const format = schema.marks[FORMAT_CHANGE_MARK_NAME];
473
968
  const mapFragment = (fragment) => {
474
969
  const children = [];
475
970
  fragment.forEach((child) => {
476
971
  if (child.isInline) {
477
972
  if (deletion && deletion.isInSet(child.marks)) return;
478
- children.push(insertion ? child.mark(insertion.removeFromSet(child.marks)) : child);
973
+ let marks = child.marks;
974
+ if (insertion) marks = insertion.removeFromSet(marks);
975
+ if (format) marks = format.removeFromSet(marks);
976
+ children.push(marks === child.marks ? child : child.mark(marks));
977
+ } else if (child.attrs?.[BLOCK_CHANGE_ATTRIBUTE]) {
978
+ children.push(
979
+ child.type.create(
980
+ { ...child.attrs, [BLOCK_CHANGE_ATTRIBUTE]: null },
981
+ mapFragment(child.content),
982
+ child.marks
983
+ )
984
+ );
479
985
  } else {
480
986
  children.push(child.copy(mapFragment(child.content)));
481
987
  }
@@ -484,6 +990,13 @@ function acceptSlice(slice, insertion, deletion) {
484
990
  };
485
991
  return new Slice(mapFragment(slice.content), slice.openStart, slice.openEnd);
486
992
  }
993
+ function stripMarks(fragment) {
994
+ const children = [];
995
+ fragment.forEach((child) => {
996
+ children.push(child.isInline ? child.mark(ProsemirrorMark.none) : child.copy(stripMarks(child.content)));
997
+ });
998
+ return Fragment.fromArray(children);
999
+ }
487
1000
  function newAttributes(ctx) {
488
1001
  return {
489
1002
  changeId: ctx.generateId(),
@@ -493,6 +1006,15 @@ function newAttributes(ctx) {
493
1006
  createdAt: ctx.now()
494
1007
  };
495
1008
  }
1009
+ function baseAttributes(attrs) {
1010
+ return {
1011
+ changeId: attrs.changeId,
1012
+ authorId: attrs.authorId,
1013
+ authorName: attrs.authorName,
1014
+ authorColor: attrs.authorColor ?? null,
1015
+ createdAt: attrs.createdAt
1016
+ };
1017
+ }
496
1018
  function adjacentAttributes(doc, from, to, type, ctx) {
497
1019
  const candidates = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];
498
1020
  for (const node of candidates) {
@@ -540,27 +1062,191 @@ function shouldSkip(tr) {
540
1062
  !!tr.getMeta("y-sync$") || // Housekeeping appended by other plugins (e.g. trailing nodes).
541
1063
  !!tr.getMeta("appendedTransaction");
542
1064
  }
1065
+ function formattingOf(marks, ctx) {
1066
+ return marks.filter((m) => !REVIEW_MARK_NAMES.has(m.type.name) && !ctx.ignoredMarks.has(m.type.name)).map((m) => m.toJSON());
1067
+ }
1068
+ function mergeRanges(ranges) {
1069
+ const sorted = ranges.filter((r) => r.to > r.from).sort((a, b) => a.from - b.from);
1070
+ const merged = [];
1071
+ sorted.forEach((range) => {
1072
+ const last = merged[merged.length - 1];
1073
+ if (last && range.from <= last.to) last.to = Math.max(last.to, range.to);
1074
+ else merged.push({ ...range });
1075
+ });
1076
+ return merged;
1077
+ }
1078
+ function trackFormatting(tr, ranges, oldDoc, back, inserted, ctx) {
1079
+ const format = ctx.format;
1080
+ const doc = tr.doc;
1081
+ let changed = false;
1082
+ let batch = null;
1083
+ mergeRanges(ranges).forEach(({ from: a, to: b }) => {
1084
+ const segments = [];
1085
+ doc.nodesBetween(a, b, (node, pos, parent) => {
1086
+ if (!node.isInline) return true;
1087
+ const s = Math.max(a, pos);
1088
+ const e = Math.min(b, pos + node.nodeSize);
1089
+ if (e <= s || !parent || !parent.type.allowsMarkType(format)) return false;
1090
+ if (ctx.insertion.isInSet(node.marks)) return false;
1091
+ if (inserted.some((r) => s < r.to && e > r.from)) return false;
1092
+ const os = back.map(s, 1);
1093
+ const oe = back.map(e, -1);
1094
+ if (oe - os !== e - s) return false;
1095
+ oldDoc.nodesBetween(os, oe, (oldNode, oldPos) => {
1096
+ if (!oldNode.isInline) return true;
1097
+ const ss = Math.max(os, oldPos);
1098
+ const ee = Math.min(oe, oldPos + oldNode.nodeSize);
1099
+ if (ee > ss && !ctx.insertion.isInSet(oldNode.marks)) {
1100
+ segments.push({ from: s + (ss - os), to: s + (ee - os), oldNode, node });
1101
+ }
1102
+ return false;
1103
+ });
1104
+ return false;
1105
+ });
1106
+ segments.forEach(({ from, to, oldNode, node }) => {
1107
+ const existing = format.isInSet(oldNode.marks);
1108
+ const original = existing ? existing.attrs.before ?? [] : formattingOf(oldNode.marks, ctx);
1109
+ const current = formattingOf(node.marks, ctx);
1110
+ if (sameValue(original, current)) {
1111
+ if (existing || format.isInSet(node.marks)) {
1112
+ tr.removeMark(from, to, format);
1113
+ changed = true;
1114
+ }
1115
+ return;
1116
+ }
1117
+ let attrs;
1118
+ if (existing && existing.attrs.authorId === ctx.author.id && existing.attrs.changeId) {
1119
+ attrs = baseAttributes(changeAttributes(existing));
1120
+ } else {
1121
+ if (!batch) {
1122
+ const description = describeFormatChange(diffFormatting(original, current));
1123
+ const neighbours = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];
1124
+ for (const neighbour of neighbours) {
1125
+ const mark = neighbour && format.isInSet(neighbour.marks);
1126
+ if (mark && mark.attrs.authorId === ctx.author.id && mark.attrs.changeId && describeFormatChange(diffFormatting(mark.attrs.before ?? [], mark.attrs.after ?? [])) === description) {
1127
+ batch = baseAttributes(changeAttributes(mark));
1128
+ break;
1129
+ }
1130
+ }
1131
+ batch = batch ?? newAttributes(ctx);
1132
+ }
1133
+ attrs = batch;
1134
+ }
1135
+ tr.addMark(from, to, format.create({ ...attrs, before: original, after: current }));
1136
+ changed = true;
1137
+ });
1138
+ });
1139
+ return changed;
1140
+ }
1141
+ function trackBlocks(tr, pending, ctx) {
1142
+ let changed = false;
1143
+ let batch = null;
1144
+ pending.forEach(({ previous, existing }, contentPos) => {
1145
+ if (contentPos < 0 || contentPos > tr.doc.content.size) return;
1146
+ const $pos = tr.doc.resolve(contentPos);
1147
+ const node = $pos.parent;
1148
+ if (!node.isTextblock || $pos.parentOffset !== 0 || $pos.depth === 0) return;
1149
+ if (!node.type.spec.attrs || !(BLOCK_CHANGE_ATTRIBUTE in node.type.spec.attrs)) return;
1150
+ const pos = $pos.before();
1151
+ const original = existing?.previous ?? previous;
1152
+ const current = blockSnapshot(tr.doc, pos);
1153
+ if (sameBlock(original, current, ctx.ignoredAttributes)) {
1154
+ if (node.attrs[BLOCK_CHANGE_ATTRIBUTE]) {
1155
+ tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);
1156
+ changed = true;
1157
+ }
1158
+ return;
1159
+ }
1160
+ const base = existing && existing.authorId === ctx.author.id && existing.changeId ? baseAttributes(existing) : batch = batch ?? newAttributes(ctx);
1161
+ const value = { ...base, previous: original };
1162
+ if (!sameValue(node.attrs[BLOCK_CHANGE_ATTRIBUTE], value)) {
1163
+ tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, value);
1164
+ changed = true;
1165
+ }
1166
+ });
1167
+ return changed;
1168
+ }
1169
+ function collectBlockChanges(step, docBefore, docAfter, rest, pending, ctx) {
1170
+ const record = (oldPos, newPos) => {
1171
+ const key = rest.map(newPos + 1);
1172
+ if (pending.has(key)) return;
1173
+ const oldNode = docBefore.nodeAt(oldPos);
1174
+ const previous = blockSnapshot(docBefore, oldPos);
1175
+ if (sameBlock(previous, blockSnapshot(docAfter, newPos), ctx.ignoredAttributes)) return;
1176
+ pending.set(key, {
1177
+ previous,
1178
+ existing: oldNode.attrs[BLOCK_CHANGE_ATTRIBUTE] ?? null
1179
+ });
1180
+ };
1181
+ if (step instanceof AttrStep) {
1182
+ const node = docBefore.nodeAt(step.pos);
1183
+ if (node?.isTextblock && step.attr !== BLOCK_CHANGE_ATTRIBUTE && !ctx.ignoredAttributes.has(step.attr)) {
1184
+ record(step.pos, step.pos);
1185
+ }
1186
+ return;
1187
+ }
1188
+ const map = step.getMap();
1189
+ const back = map.invert();
1190
+ const start = map.map(step.from, -1);
1191
+ const end = map.map(step.to, 1);
1192
+ docAfter.nodesBetween(start, end, (node, pos) => {
1193
+ if (!node.isTextblock) return true;
1194
+ const oldContent = back.map(pos + 1);
1195
+ if (oldContent < 1 || oldContent > docBefore.content.size) return false;
1196
+ const $old = docBefore.resolve(oldContent);
1197
+ if ($old.parent.isTextblock && $old.parentOffset === 0 && $old.parent.content.size === node.content.size) {
1198
+ record($old.before(), pos);
1199
+ }
1200
+ return false;
1201
+ });
1202
+ }
543
1203
  function trackTransactions(transactions, oldState, newState, ctx) {
544
1204
  const maps = [];
545
1205
  transactions.forEach((t) => maps.push(...t.mapping.maps));
546
1206
  const inserted = [];
547
1207
  const restores = [];
1208
+ const formatRanges = [];
1209
+ const blocks = /* @__PURE__ */ new Map();
548
1210
  let mapIndex = 0;
549
1211
  const single = transactions.length === 1 && transactions[0].steps.length === 1;
550
1212
  transactions.forEach((t) => {
551
1213
  const skip = shouldSkip(t);
552
1214
  t.steps.forEach((step, i) => {
553
1215
  const index = mapIndex + i;
554
- if (skip || !(step instanceof ReplaceStep)) return;
555
- const { from, to, slice } = step;
1216
+ if (skip) return;
556
1217
  const rest = new Mapping(maps.slice(index + 1));
1218
+ const docBefore = t.docs[i];
1219
+ if (step instanceof AddMarkStep || step instanceof RemoveMarkStep) {
1220
+ const name = step.mark.type.name;
1221
+ if (ctx.format && !REVIEW_MARK_NAMES.has(name) && !ctx.ignoredMarks.has(name)) {
1222
+ formatRanges.push({ from: rest.map(step.from, 1), to: rest.map(step.to, -1) });
1223
+ }
1224
+ return;
1225
+ }
1226
+ if (step instanceof ReplaceAroundStep || step instanceof AttrStep) {
1227
+ if (ctx.trackBlocks) {
1228
+ const docAfter = t.docs[i + 1] ?? t.doc;
1229
+ collectBlockChanges(step, docBefore, docAfter, rest, blocks, ctx);
1230
+ }
1231
+ return;
1232
+ }
1233
+ if (!(step instanceof ReplaceStep)) return;
1234
+ const { from, to, slice } = step;
1235
+ if (to > from && slice.size === to - from) {
1236
+ const deleted = docBefore.slice(from, to);
1237
+ if (slice.openStart === deleted.openStart && slice.openEnd === deleted.openEnd && stripMarks(slice.content).eq(stripMarks(deleted.content))) {
1238
+ if (ctx.format && !slice.content.eq(deleted.content)) {
1239
+ formatRanges.push({ from: rest.map(from, 1), to: rest.map(to, -1) });
1240
+ }
1241
+ return;
1242
+ }
1243
+ }
557
1244
  if (slice.size > 0 && hasInlineContent(slice.content)) {
558
1245
  const start = rest.map(from, 1);
559
1246
  const end = rest.map(from + slice.size, -1);
560
1247
  if (end > start) inserted.push({ from: start, to: end });
561
1248
  }
562
1249
  if (to > from) {
563
- const docBefore = t.docs[i];
564
1250
  const deleted = docBefore.slice(from, to);
565
1251
  if (!hasInlineContent(deleted.content)) return;
566
1252
  const $from = docBefore.resolve(from);
@@ -577,12 +1263,22 @@ function trackTransactions(transactions, oldState, newState, ctx) {
577
1263
  });
578
1264
  mapIndex += t.steps.length;
579
1265
  });
580
- if (inserted.length === 0 && restores.length === 0) return null;
1266
+ if (inserted.length === 0 && restores.length === 0 && formatRanges.length === 0 && blocks.size === 0) {
1267
+ return null;
1268
+ }
581
1269
  const tr = newState.tr;
582
1270
  let changed = false;
1271
+ if (formatRanges.length && ctx.format) {
1272
+ const back = new Mapping(maps).invert();
1273
+ changed = trackFormatting(tr, formatRanges, oldState.doc, back, inserted, ctx) || changed;
1274
+ }
1275
+ if (blocks.size) {
1276
+ changed = trackBlocks(tr, blocks, ctx) || changed;
1277
+ }
583
1278
  inserted.forEach(({ from, to }) => {
584
1279
  tr.removeMark(from, to, ctx.deletion);
585
1280
  tr.removeMark(from, to, ctx.insertion);
1281
+ if (ctx.format) tr.removeMark(from, to, ctx.format);
586
1282
  const attrs = adjacentAttributes(tr.doc, from, to, ctx.insertion, ctx) ?? newAttributes(ctx);
587
1283
  tr.addMark(from, to, ctx.insertion.create(attrs));
588
1284
  changed = true;
@@ -624,6 +1320,19 @@ function buildSnapshot(state, author) {
624
1320
  currentChangeId: getTrackedChangeAt(state, state.selection.from, changes)?.id ?? null
625
1321
  };
626
1322
  }
1323
+ function allowTrackingInCodeBlocks(schema) {
1324
+ const marks = [schema.marks[INSERTION_MARK_NAME], schema.marks[DELETION_MARK_NAME]].filter(
1325
+ Boolean
1326
+ );
1327
+ Object.values(schema.nodes).forEach((type) => {
1328
+ const markSet = type.markSet;
1329
+ if (!type.isTextblock || !type.spec.code || markSet === null) return;
1330
+ const missing = marks.filter((mark) => !markSet.includes(mark));
1331
+ if (missing.length) {
1332
+ type.markSet = [...markSet, ...missing];
1333
+ }
1334
+ });
1335
+ }
627
1336
  var TrackChanges = Extension.create({
628
1337
  name: "trackChanges",
629
1338
  addOptions() {
@@ -633,7 +1342,15 @@ var TrackChanges = Extension.create({
633
1342
  now: () => Date.now(),
634
1343
  generateId: () => createReviewId("change"),
635
1344
  insertionHTMLAttributes: {},
636
- deletionHTMLAttributes: {}
1345
+ deletionHTMLAttributes: {},
1346
+ formatChangeHTMLAttributes: {},
1347
+ blockChangeClass: "pixerate-block-change border-l-2 border-amber-400 pl-2 dark:border-amber-500",
1348
+ trackFormatting: true,
1349
+ trackBlocks: true,
1350
+ trackCodeBlocks: true,
1351
+ blockTypes: ["paragraph", "heading", "codeBlock"],
1352
+ ignoredMarks: [],
1353
+ ignoredBlockAttributes: ["id"]
637
1354
  };
638
1355
  },
639
1356
  addStorage() {
@@ -646,14 +1363,43 @@ var TrackChanges = Extension.create({
646
1363
  })
647
1364
  };
648
1365
  },
1366
+ addGlobalAttributes() {
1367
+ if (this.options?.trackBlocks === false) return [];
1368
+ const blockClass = this.options?.blockChangeClass ?? "";
1369
+ return [
1370
+ {
1371
+ types: this.options?.blockTypes ?? ["paragraph", "heading", "codeBlock"],
1372
+ attributes: {
1373
+ [BLOCK_CHANGE_ATTRIBUTE]: {
1374
+ default: null,
1375
+ keepOnSplit: false,
1376
+ parseHTML: (el) => parseJSON(el.getAttribute("data-block-change"), null),
1377
+ renderHTML: (attrs) => {
1378
+ const value = attrs[BLOCK_CHANGE_ATTRIBUTE];
1379
+ if (!value) return {};
1380
+ return {
1381
+ "data-block-change": JSON.stringify(value),
1382
+ ...blockClass ? { class: blockClass } : {}
1383
+ };
1384
+ }
1385
+ }
1386
+ }
1387
+ }
1388
+ ];
1389
+ },
649
1390
  addExtensions() {
650
1391
  const insertionAttrs = this.options?.insertionHTMLAttributes ?? {};
651
1392
  const deletionAttrs = this.options?.deletionHTMLAttributes ?? {};
1393
+ const formatAttrs = this.options?.formatChangeHTMLAttributes ?? {};
652
1394
  return [
653
1395
  Object.keys(insertionAttrs).length ? TrackedInsertion.configure({ HTMLAttributes: insertionAttrs }) : TrackedInsertion,
654
- Object.keys(deletionAttrs).length ? TrackedDeletion.configure({ HTMLAttributes: deletionAttrs }) : TrackedDeletion
1396
+ Object.keys(deletionAttrs).length ? TrackedDeletion.configure({ HTMLAttributes: deletionAttrs }) : TrackedDeletion,
1397
+ Object.keys(formatAttrs).length ? TrackedFormatChange.configure({ HTMLAttributes: formatAttrs }) : TrackedFormatChange
655
1398
  ];
656
1399
  },
1400
+ onBeforeCreate() {
1401
+ if (this.options.trackCodeBlocks) allowTrackingInCodeBlocks(this.editor.schema);
1402
+ },
657
1403
  addCommands() {
658
1404
  const decide = (decision, id) => ({ state, tr, dispatch }) => {
659
1405
  const all = getTrackedChanges(state.doc);
@@ -669,9 +1415,11 @@ var TrackChanges = Extension.create({
669
1415
  const changes = getTrackedChanges(state.doc);
670
1416
  if (changes.length === 0) return false;
671
1417
  const { from } = state.selection;
672
- const target = direction === 1 ? changes.find((c) => c.from > from) ?? changes[0] : [...changes].reverse().find((c) => c.from < from) ?? changes[changes.length - 1];
1418
+ const start = (c) => selectionRange(c)[0];
1419
+ const target = direction === 1 ? changes.find((c) => start(c) > from) ?? changes[0] : [...changes].reverse().find((c) => start(c) < from) ?? changes[changes.length - 1];
673
1420
  if (dispatch) {
674
- tr.setSelection(TextSelection.create(tr.doc, target.from, target.to)).scrollIntoView();
1421
+ const [anchor, head] = selectionRange(target);
1422
+ tr.setSelection(TextSelection.create(tr.doc, anchor, head)).scrollIntoView();
675
1423
  }
676
1424
  return true;
677
1425
  };
@@ -702,6 +1450,8 @@ var TrackChanges = Extension.create({
702
1450
  addProseMirrorPlugins() {
703
1451
  const options = this.options;
704
1452
  const store = this.storage.store;
1453
+ const ignoredMarks = new Set(options.ignoredMarks ?? []);
1454
+ const ignoredAttributes = new Set(options.ignoredBlockAttributes ?? []);
705
1455
  let editorView = null;
706
1456
  let pending = null;
707
1457
  let flushTimer = null;
@@ -757,15 +1507,15 @@ var TrackChanges = Extension.create({
757
1507
  now: options.now,
758
1508
  generateId: options.generateId,
759
1509
  insertion: newState.schema.marks[INSERTION_MARK_NAME],
760
- deletion: newState.schema.marks[DELETION_MARK_NAME]
1510
+ deletion: newState.schema.marks[DELETION_MARK_NAME],
1511
+ format: options.trackFormatting !== false ? newState.schema.marks[FORMAT_CHANGE_MARK_NAME] : void 0,
1512
+ trackBlocks: options.trackBlocks !== false,
1513
+ ignoredMarks,
1514
+ ignoredAttributes
761
1515
  });
762
1516
  },
763
1517
  props: {
764
- transformPasted: (slice, view) => acceptSlice(
765
- slice,
766
- view.state.schema.marks[INSERTION_MARK_NAME],
767
- view.state.schema.marks[DELETION_MARK_NAME]
768
- ),
1518
+ transformPasted: (slice, view) => acceptSlice(slice, view.state.schema),
769
1519
  handleDOMEvents: {
770
1520
  compositionend: () => {
771
1521
  if (pending) scheduleFlush();
@@ -1336,11 +2086,16 @@ export {
1336
2086
  formatReviewTime,
1337
2087
  INSERTION_MARK_NAME,
1338
2088
  DELETION_MARK_NAME,
2089
+ FORMAT_CHANGE_MARK_NAME,
2090
+ BLOCK_CHANGE_ATTRIBUTE,
1339
2091
  TRACK_CHANGES_SKIP_META,
1340
2092
  TrackChangesStore,
1341
2093
  trackChangesPluginKey,
1342
2094
  TrackedInsertion,
1343
2095
  TrackedDeletion,
2096
+ TrackedFormatChange,
2097
+ describeFormatChange,
2098
+ describeBlockChange,
1344
2099
  getTrackedChanges,
1345
2100
  getTrackedChangeAt,
1346
2101
  isTrackChangesEnabled,
@@ -1364,4 +2119,4 @@ export {
1364
2119
  markdownToTipTapHtml,
1365
2120
  getEditorMarkdown
1366
2121
  };
1367
- //# sourceMappingURL=chunk-3TYMNULT.js.map
2122
+ //# sourceMappingURL=chunk-MJZBNWG7.js.map