@pixerate/editor 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -2
- package/dist/agent/index.cjs +625 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +286 -2
- package/dist/agent/index.d.ts +286 -2
- package/dist/agent/index.js +624 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +1240 -22
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +515 -3
- package/dist/canvas/index.d.ts +515 -3
- package/dist/canvas/index.js +1219 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-3TYMNULT.js +1367 -0
- package/dist/chunk-3TYMNULT.js.map +1 -0
- package/dist/chunk-4QPLEUL2.cjs +4480 -0
- package/dist/chunk-4QPLEUL2.cjs.map +1 -0
- package/dist/chunk-4SUNJZWB.cjs +553 -0
- package/dist/chunk-4SUNJZWB.cjs.map +1 -0
- package/dist/chunk-A7DKIA72.js +2657 -0
- package/dist/chunk-A7DKIA72.js.map +1 -0
- package/dist/chunk-AXV2ZW34.cjs +118 -0
- package/dist/chunk-AXV2ZW34.cjs.map +1 -0
- package/dist/chunk-B2ZRLN4S.js +118 -0
- package/dist/chunk-B2ZRLN4S.js.map +1 -0
- package/dist/chunk-CYFJCKC4.js +4480 -0
- package/dist/chunk-CYFJCKC4.js.map +1 -0
- package/dist/chunk-GVW4P7BG.js +2706 -0
- package/dist/chunk-GVW4P7BG.js.map +1 -0
- package/dist/chunk-JACER6TE.cjs +1 -0
- package/dist/chunk-JACER6TE.cjs.map +1 -0
- package/dist/chunk-JYRIOFEN.cjs +2657 -0
- package/dist/chunk-JYRIOFEN.cjs.map +1 -0
- package/dist/chunk-KYRZDWM4.js +299 -0
- package/dist/chunk-KYRZDWM4.js.map +1 -0
- package/dist/chunk-LA7PEGU3.cjs +299 -0
- package/dist/chunk-LA7PEGU3.cjs.map +1 -0
- package/dist/chunk-N7FG6CBO.js +1 -0
- package/dist/chunk-N7FG6CBO.js.map +1 -0
- package/dist/chunk-OYXSKQFR.js +553 -0
- package/dist/chunk-OYXSKQFR.js.map +1 -0
- package/dist/chunk-TVRC4KVT.cjs +1367 -0
- package/dist/chunk-TVRC4KVT.cjs.map +1 -0
- package/dist/chunk-U6NDQFOK.cjs +2706 -0
- package/dist/chunk-U6NDQFOK.cjs.map +1 -0
- package/dist/controller-OUdxPY-X.d.cts +754 -0
- package/dist/controller-OUdxPY-X.d.ts +754 -0
- package/dist/extensions/index.cjs +144 -3
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +9 -144
- package/dist/extensions/index.d.ts +9 -144
- package/dist/extensions/index.js +145 -4
- package/dist/grammar/index.cjs +31 -3
- package/dist/grammar/index.cjs.map +1 -1
- package/dist/grammar/index.d.cts +239 -19
- package/dist/grammar/index.d.ts +239 -19
- package/dist/grammar/index.js +31 -3
- package/dist/history/index.d.cts +2 -41
- package/dist/history/index.d.ts +2 -41
- package/dist/index-Ng1pG7JE.d.ts +899 -0
- package/dist/index-k3mFbPx_.d.cts +899 -0
- package/dist/index.cjs +457 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -12
- package/dist/index.d.ts +99 -12
- package/dist/index.js +457 -20
- package/dist/index.js.map +1 -1
- package/dist/registry-iAHbqkYt.d.cts +128 -0
- package/dist/registry-iAHbqkYt.d.ts +128 -0
- package/dist/serializers/index.cjs +26 -3
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +119 -6
- package/dist/serializers/index.d.ts +119 -6
- package/dist/serializers/index.js +24 -1
- package/dist/spreadsheet/index.cjs +122 -2
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +371 -313
- package/dist/spreadsheet/index.d.ts +371 -313
- package/dist/spreadsheet/index.js +127 -7
- package/dist/tokens-CyLZmIei.d.cts +76 -0
- package/dist/tokens-CyLZmIei.d.ts +76 -0
- package/dist/track-changes-G8JcWB8b.d.cts +306 -0
- package/dist/track-changes-G8JcWB8b.d.ts +306 -0
- package/dist/types-BZgcDsC5.d.cts +42 -0
- package/dist/types-BZgcDsC5.d.ts +42 -0
- package/dist/{types-JV3S8VeR.d.cts → types-DMe0vFB1.d.cts} +1 -1
- package/dist/{types-JV3S8VeR.d.ts → types-DMe0vFB1.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-7FWEWE6T.js +0 -1888
- package/dist/chunk-7FWEWE6T.js.map +0 -1
- package/dist/chunk-AIZUKWTL.js +0 -1715
- package/dist/chunk-AIZUKWTL.js.map +0 -1
- package/dist/chunk-GLSIZKOX.js +0 -17
- package/dist/chunk-GLSIZKOX.js.map +0 -1
- package/dist/chunk-LDES3EOS.cjs +0 -1888
- package/dist/chunk-LDES3EOS.cjs.map +0 -1
- package/dist/chunk-LLUAKTLW.cjs +0 -532
- package/dist/chunk-LLUAKTLW.cjs.map +0 -1
- package/dist/chunk-MPPXH7U5.cjs +0 -336
- package/dist/chunk-MPPXH7U5.cjs.map +0 -1
- package/dist/chunk-O7LOPQMH.js +0 -336
- package/dist/chunk-O7LOPQMH.js.map +0 -1
- package/dist/chunk-QGVMBCT5.js +0 -532
- package/dist/chunk-QGVMBCT5.js.map +0 -1
- package/dist/chunk-SEDR75VH.cjs +0 -17
- package/dist/chunk-SEDR75VH.cjs.map +0 -1
- package/dist/chunk-YQCI4CCN.cjs +0 -1715
- package/dist/chunk-YQCI4CCN.cjs.map +0 -1
- package/dist/mention-Dcevh3ff.d.cts +0 -32
- package/dist/mention-Dcevh3ff.d.ts +0 -32
- package/dist/tokens-7kRTEtPb.d.cts +0 -39
- package/dist/tokens-7kRTEtPb.d.ts +0 -39
|
@@ -0,0 +1,1367 @@
|
|
|
1
|
+
// src/extensions/comment-store.ts
|
|
2
|
+
var idCounter = 0;
|
|
3
|
+
function createReviewId(prefix = "c") {
|
|
4
|
+
idCounter = (idCounter + 1) % 1e6;
|
|
5
|
+
return `${prefix}-${Date.now().toString(36)}-${idCounter.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6
|
+
}
|
|
7
|
+
function sameAnchor(a, b) {
|
|
8
|
+
if (a === b) return true;
|
|
9
|
+
if (!a || !b) return false;
|
|
10
|
+
return a.from === b.from && a.to === b.to && a.quote === b.quote;
|
|
11
|
+
}
|
|
12
|
+
function normalizeThread(raw) {
|
|
13
|
+
return {
|
|
14
|
+
id: String(raw.id),
|
|
15
|
+
anchor: raw.anchor ?? null,
|
|
16
|
+
author: raw.author ?? { id: "unknown", name: "Unknown" },
|
|
17
|
+
createdAt: Number(raw.createdAt ?? 0),
|
|
18
|
+
body: String(raw.body ?? ""),
|
|
19
|
+
...raw.editedAt !== void 0 ? { editedAt: raw.editedAt } : {},
|
|
20
|
+
replies: Array.isArray(raw.replies) ? raw.replies.map((r) => ({ ...r })) : [],
|
|
21
|
+
resolved: !!raw.resolved,
|
|
22
|
+
...raw.resolvedAt !== void 0 ? { resolvedAt: raw.resolvedAt } : {},
|
|
23
|
+
...raw.resolvedBy !== void 0 ? { resolvedBy: raw.resolvedBy } : {},
|
|
24
|
+
orphaned: !!raw.orphaned,
|
|
25
|
+
...raw.lastQuote !== void 0 ? { lastQuote: String(raw.lastQuote) } : {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var CommentStore = class _CommentStore {
|
|
29
|
+
threads = [];
|
|
30
|
+
activeThreadId = null;
|
|
31
|
+
snapshot;
|
|
32
|
+
listeners = /* @__PURE__ */ new Set();
|
|
33
|
+
/** Deleted threads, kept so undoing a "Delete Comment" can restore them. */
|
|
34
|
+
deleted = /* @__PURE__ */ new Map();
|
|
35
|
+
generateId;
|
|
36
|
+
now;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.generateId = options.generateId ?? (() => createReviewId("comment"));
|
|
39
|
+
this.now = options.now ?? (() => Date.now());
|
|
40
|
+
if (options.threads) {
|
|
41
|
+
this.threads = _CommentStore.parse(options.threads);
|
|
42
|
+
}
|
|
43
|
+
this.snapshot = this.computeSnapshot();
|
|
44
|
+
}
|
|
45
|
+
/** Parses `toJSON()` output (or a plain thread array). */
|
|
46
|
+
static parse(json) {
|
|
47
|
+
const value = typeof json === "string" ? JSON.parse(json) : json;
|
|
48
|
+
const list = Array.isArray(value) ? value : value?.threads;
|
|
49
|
+
if (!Array.isArray(list)) return [];
|
|
50
|
+
return list.filter((t) => t && t.id != null).map(normalizeThread);
|
|
51
|
+
}
|
|
52
|
+
subscribe = (listener) => {
|
|
53
|
+
this.listeners.add(listener);
|
|
54
|
+
listener(this.snapshot);
|
|
55
|
+
return () => {
|
|
56
|
+
this.listeners.delete(listener);
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
getSnapshot = () => this.snapshot;
|
|
60
|
+
getThread(id) {
|
|
61
|
+
return this.threads.find((t) => t.id === id);
|
|
62
|
+
}
|
|
63
|
+
createThread(input) {
|
|
64
|
+
const thread = {
|
|
65
|
+
id: input.id ?? this.generateId(),
|
|
66
|
+
anchor: input.anchor ?? null,
|
|
67
|
+
author: input.author,
|
|
68
|
+
createdAt: input.createdAt ?? this.now(),
|
|
69
|
+
body: input.body,
|
|
70
|
+
replies: [],
|
|
71
|
+
resolved: false,
|
|
72
|
+
orphaned: false
|
|
73
|
+
};
|
|
74
|
+
this.threads = [...this.threads.filter((t) => t.id !== thread.id), thread];
|
|
75
|
+
this.deleted.delete(thread.id);
|
|
76
|
+
this.emit();
|
|
77
|
+
return thread;
|
|
78
|
+
}
|
|
79
|
+
/** Edits the thread's body, or one of its replies when `replyId` is given. */
|
|
80
|
+
editComment(threadId, body, replyId) {
|
|
81
|
+
const editedAt = this.now();
|
|
82
|
+
return this.updateThread(threadId, (t) => {
|
|
83
|
+
if (!replyId) return { ...t, body, editedAt };
|
|
84
|
+
if (!t.replies.some((r) => r.id === replyId)) return t;
|
|
85
|
+
return {
|
|
86
|
+
...t,
|
|
87
|
+
replies: t.replies.map((r) => r.id === replyId ? { ...r, body, editedAt } : r)
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
addReply(threadId, input) {
|
|
92
|
+
const reply = {
|
|
93
|
+
id: input.id ?? this.generateId(),
|
|
94
|
+
author: input.author,
|
|
95
|
+
body: input.body,
|
|
96
|
+
createdAt: this.now()
|
|
97
|
+
};
|
|
98
|
+
const ok = this.updateThread(threadId, (t) => ({
|
|
99
|
+
...t,
|
|
100
|
+
replies: [...t.replies, reply],
|
|
101
|
+
// Replying to a resolved thread re-opens it, as in Word and Pages.
|
|
102
|
+
resolved: false,
|
|
103
|
+
resolvedAt: void 0,
|
|
104
|
+
resolvedBy: void 0
|
|
105
|
+
}));
|
|
106
|
+
return ok ? reply : null;
|
|
107
|
+
}
|
|
108
|
+
deleteReply(threadId, replyId) {
|
|
109
|
+
return this.updateThread(
|
|
110
|
+
threadId,
|
|
111
|
+
(t) => t.replies.some((r) => r.id === replyId) ? { ...t, replies: t.replies.filter((r) => r.id !== replyId) } : t
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
resolveThread(id, resolved = true, by) {
|
|
115
|
+
return this.updateThread(
|
|
116
|
+
id,
|
|
117
|
+
(t) => t.resolved === resolved ? t : resolved ? { ...t, resolved, resolvedAt: this.now(), ...by ? { resolvedBy: by } : {} } : { ...t, resolved, resolvedAt: void 0, resolvedBy: void 0 }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
deleteThread(id) {
|
|
121
|
+
const thread = this.getThread(id);
|
|
122
|
+
if (!thread) return false;
|
|
123
|
+
this.deleted.set(id, thread);
|
|
124
|
+
this.threads = this.threads.filter((t) => t.id !== id);
|
|
125
|
+
if (this.activeThreadId === id) this.activeThreadId = null;
|
|
126
|
+
this.emit();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/** Deletes every thread whose text is no longer in the document. */
|
|
130
|
+
removeOrphanedThreads() {
|
|
131
|
+
const ids = this.threads.filter((t) => t.orphaned).map((t) => t.id);
|
|
132
|
+
if (ids.length === 0) return ids;
|
|
133
|
+
ids.forEach((id) => this.deleted.set(id, this.getThread(id)));
|
|
134
|
+
this.threads = this.threads.filter((t) => !t.orphaned);
|
|
135
|
+
if (this.activeThreadId && ids.includes(this.activeThreadId)) {
|
|
136
|
+
this.activeThreadId = null;
|
|
137
|
+
}
|
|
138
|
+
this.emit();
|
|
139
|
+
return ids;
|
|
140
|
+
}
|
|
141
|
+
setActiveThread(id) {
|
|
142
|
+
const next = id && this.getThread(id) ? id : null;
|
|
143
|
+
if (next === this.activeThreadId) return;
|
|
144
|
+
this.activeThreadId = next;
|
|
145
|
+
this.emit();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Updates anchors from the document. Threads missing from `anchors` become
|
|
149
|
+
* orphaned. Threads that were deleted but whose mark re-appeared (e.g. after
|
|
150
|
+
* undoing "Delete Comment") are restored. Returns true if anything changed.
|
|
151
|
+
*/
|
|
152
|
+
syncAnchors(anchors) {
|
|
153
|
+
let changed = false;
|
|
154
|
+
const restored = [];
|
|
155
|
+
anchors.forEach((_anchor, id) => {
|
|
156
|
+
if (!this.getThread(id) && this.deleted.has(id)) {
|
|
157
|
+
restored.push(this.deleted.get(id));
|
|
158
|
+
this.deleted.delete(id);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const next = [...this.threads, ...restored].map((t) => {
|
|
162
|
+
const anchor = anchors.get(t.id) ?? null;
|
|
163
|
+
const orphaned = anchor === null;
|
|
164
|
+
if (sameAnchor(t.anchor, anchor) && t.orphaned === orphaned) return t;
|
|
165
|
+
changed = true;
|
|
166
|
+
return orphaned ? { ...t, anchor: null, orphaned, ...t.anchor ? { lastQuote: t.anchor.quote } : {} } : { ...t, anchor, orphaned };
|
|
167
|
+
});
|
|
168
|
+
if (!changed && restored.length === 0) return false;
|
|
169
|
+
this.threads = next;
|
|
170
|
+
this.emit();
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
/** Replaces all threads, e.g. with data loaded from a server. */
|
|
174
|
+
load(json) {
|
|
175
|
+
this.threads = _CommentStore.parse(json);
|
|
176
|
+
this.deleted.clear();
|
|
177
|
+
if (this.activeThreadId && !this.getThread(this.activeThreadId)) {
|
|
178
|
+
this.activeThreadId = null;
|
|
179
|
+
}
|
|
180
|
+
this.emit();
|
|
181
|
+
}
|
|
182
|
+
clear() {
|
|
183
|
+
this.threads = [];
|
|
184
|
+
this.deleted.clear();
|
|
185
|
+
this.activeThreadId = null;
|
|
186
|
+
this.emit();
|
|
187
|
+
}
|
|
188
|
+
toJSON() {
|
|
189
|
+
return {
|
|
190
|
+
version: 1,
|
|
191
|
+
threads: this.threads.map((t) => ({ ...t, replies: t.replies.map((r) => ({ ...r })) }))
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
updateThread(id, fn) {
|
|
195
|
+
const index = this.threads.findIndex((t) => t.id === id);
|
|
196
|
+
if (index === -1) return false;
|
|
197
|
+
const updated = fn(this.threads[index]);
|
|
198
|
+
if (updated === this.threads[index]) return true;
|
|
199
|
+
this.threads = this.threads.map((t, i) => i === index ? updated : t);
|
|
200
|
+
this.emit();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
computeSnapshot() {
|
|
204
|
+
const threads = [...this.threads].sort((a, b) => {
|
|
205
|
+
if (a.anchor && b.anchor) {
|
|
206
|
+
return a.anchor.from - b.anchor.from || a.createdAt - b.createdAt;
|
|
207
|
+
}
|
|
208
|
+
if (a.anchor) return -1;
|
|
209
|
+
if (b.anchor) return 1;
|
|
210
|
+
return a.createdAt - b.createdAt;
|
|
211
|
+
});
|
|
212
|
+
return { threads, activeThreadId: this.activeThreadId };
|
|
213
|
+
}
|
|
214
|
+
emit() {
|
|
215
|
+
this.snapshot = this.computeSnapshot();
|
|
216
|
+
const snapshot = this.snapshot;
|
|
217
|
+
this.listeners.forEach((listener) => {
|
|
218
|
+
try {
|
|
219
|
+
listener(snapshot);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error("[CommentStore] Error in listener:", err);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function createCommentStore(options) {
|
|
227
|
+
return new CommentStore(options);
|
|
228
|
+
}
|
|
229
|
+
function formatReviewTime(timestamp, now = Date.now()) {
|
|
230
|
+
const seconds = Math.round((now - timestamp) / 1e3);
|
|
231
|
+
if (seconds < 45) return "just now";
|
|
232
|
+
const minutes = Math.round(seconds / 60);
|
|
233
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
234
|
+
const hours = Math.round(minutes / 60);
|
|
235
|
+
if (hours < 24) return `${hours}h ago`;
|
|
236
|
+
return new Date(timestamp).toLocaleDateString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/extensions/track-changes.ts
|
|
240
|
+
import { Extension, Mark, mergeAttributes } from "@tiptap/core";
|
|
241
|
+
import {
|
|
242
|
+
Plugin,
|
|
243
|
+
PluginKey,
|
|
244
|
+
TextSelection
|
|
245
|
+
} from "@tiptap/pm/state";
|
|
246
|
+
import {
|
|
247
|
+
Fragment,
|
|
248
|
+
Slice
|
|
249
|
+
} from "@tiptap/pm/model";
|
|
250
|
+
import { Mapping, ReplaceStep, Transform } from "@tiptap/pm/transform";
|
|
251
|
+
var INSERTION_MARK_NAME = "insertion";
|
|
252
|
+
var DELETION_MARK_NAME = "deletion";
|
|
253
|
+
var TRACK_CHANGES_SKIP_META = "trackChangesSkip";
|
|
254
|
+
var TrackChangesStore = class {
|
|
255
|
+
snapshot;
|
|
256
|
+
listeners = /* @__PURE__ */ new Set();
|
|
257
|
+
constructor(initial) {
|
|
258
|
+
this.snapshot = initial;
|
|
259
|
+
}
|
|
260
|
+
subscribe = (listener) => {
|
|
261
|
+
this.listeners.add(listener);
|
|
262
|
+
listener(this.snapshot);
|
|
263
|
+
return () => {
|
|
264
|
+
this.listeners.delete(listener);
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
getSnapshot = () => this.snapshot;
|
|
268
|
+
/** @internal Called by the extension's plugin view. */
|
|
269
|
+
set(snapshot) {
|
|
270
|
+
this.snapshot = snapshot;
|
|
271
|
+
this.listeners.forEach((listener) => {
|
|
272
|
+
try {
|
|
273
|
+
listener(snapshot);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error("[TrackChangesStore] Error in listener:", err);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var trackChangesPluginKey = new PluginKey("trackChanges");
|
|
281
|
+
function changeAttributes(mark) {
|
|
282
|
+
return mark.attrs;
|
|
283
|
+
}
|
|
284
|
+
var sharedAttributes = () => ({
|
|
285
|
+
changeId: {
|
|
286
|
+
default: null,
|
|
287
|
+
parseHTML: (el) => el.getAttribute("data-change-id"),
|
|
288
|
+
renderHTML: (attrs) => ({ "data-change-id": attrs.changeId })
|
|
289
|
+
},
|
|
290
|
+
authorId: {
|
|
291
|
+
default: "",
|
|
292
|
+
parseHTML: (el) => el.getAttribute("data-author-id") ?? "",
|
|
293
|
+
renderHTML: (attrs) => ({ "data-author-id": attrs.authorId })
|
|
294
|
+
},
|
|
295
|
+
authorName: {
|
|
296
|
+
default: "",
|
|
297
|
+
parseHTML: (el) => el.getAttribute("data-author-name") ?? "",
|
|
298
|
+
renderHTML: (attrs) => ({ "data-author-name": attrs.authorName })
|
|
299
|
+
},
|
|
300
|
+
authorColor: {
|
|
301
|
+
default: null,
|
|
302
|
+
parseHTML: (el) => el.getAttribute("data-author-color"),
|
|
303
|
+
renderHTML: (attrs) => attrs.authorColor ? { "data-author-color": attrs.authorColor } : {}
|
|
304
|
+
},
|
|
305
|
+
createdAt: {
|
|
306
|
+
default: 0,
|
|
307
|
+
parseHTML: (el) => Number(el.getAttribute("data-created-at") ?? 0),
|
|
308
|
+
renderHTML: (attrs) => ({ "data-created-at": String(attrs.createdAt) })
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
var TrackedInsertion = Mark.create({
|
|
312
|
+
name: INSERTION_MARK_NAME,
|
|
313
|
+
inclusive: false,
|
|
314
|
+
addOptions() {
|
|
315
|
+
return {
|
|
316
|
+
HTMLAttributes: {
|
|
317
|
+
class: "pixerate-insertion underline decoration-emerald-500 text-emerald-700 dark:text-emerald-400"
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
addStorage() {
|
|
322
|
+
return { markdown: { serialize: { open: "", close: "" }, parse: {} } };
|
|
323
|
+
},
|
|
324
|
+
addAttributes: sharedAttributes,
|
|
325
|
+
parseHTML() {
|
|
326
|
+
return [{ tag: 'ins[data-change-type="insertion"]', priority: 100 }];
|
|
327
|
+
},
|
|
328
|
+
renderHTML({ HTMLAttributes }) {
|
|
329
|
+
return [
|
|
330
|
+
"ins",
|
|
331
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
332
|
+
"data-change-type": "insertion"
|
|
333
|
+
}),
|
|
334
|
+
0
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
var TrackedDeletion = Mark.create({
|
|
339
|
+
name: DELETION_MARK_NAME,
|
|
340
|
+
inclusive: false,
|
|
341
|
+
addOptions() {
|
|
342
|
+
return {
|
|
343
|
+
HTMLAttributes: {
|
|
344
|
+
class: "pixerate-deletion line-through decoration-red-500 text-red-600 dark:text-red-400"
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
addStorage() {
|
|
349
|
+
return {
|
|
350
|
+
markdown: {
|
|
351
|
+
serialize: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
352
|
+
parse: {}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
addAttributes: sharedAttributes,
|
|
357
|
+
parseHTML() {
|
|
358
|
+
return [{ tag: 'del[data-change-type="deletion"]', priority: 100 }];
|
|
359
|
+
},
|
|
360
|
+
renderHTML({ HTMLAttributes }) {
|
|
361
|
+
return [
|
|
362
|
+
"del",
|
|
363
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
364
|
+
"data-change-type": "deletion"
|
|
365
|
+
}),
|
|
366
|
+
0
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
function toDoc(source) {
|
|
371
|
+
if (!source) return null;
|
|
372
|
+
if (source.state?.doc) return source.state.doc;
|
|
373
|
+
if (source.doc && typeof source.doc.descendants === "function") return source.doc;
|
|
374
|
+
if (typeof source.descendants === "function") return source;
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
function getTrackedChanges(source) {
|
|
378
|
+
const doc = toDoc(source);
|
|
379
|
+
if (!doc) return [];
|
|
380
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
381
|
+
doc.descendants((node, pos) => {
|
|
382
|
+
if (!node.isInline) return;
|
|
383
|
+
node.marks.forEach((mark) => {
|
|
384
|
+
const type = mark.type.name;
|
|
385
|
+
if (type !== INSERTION_MARK_NAME && type !== DELETION_MARK_NAME) return;
|
|
386
|
+
const attrs = changeAttributes(mark);
|
|
387
|
+
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);
|
|
408
|
+
}
|
|
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
|
+
});
|
|
418
|
+
});
|
|
419
|
+
return Array.from(byKey.values()).sort((a, b) => a.from - b.from || a.to - b.to);
|
|
420
|
+
}
|
|
421
|
+
function getTrackedChangeAt(state, pos = state.selection.from, changes = getTrackedChanges(state.doc)) {
|
|
422
|
+
const { from, to } = state.selection;
|
|
423
|
+
const exact = changes.find((c) => c.from === from && c.to === to);
|
|
424
|
+
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;
|
|
426
|
+
}
|
|
427
|
+
function isTrackChangesEnabled(source) {
|
|
428
|
+
const state = source?.state ?? source;
|
|
429
|
+
if (!state || typeof state.plugins === "undefined") return false;
|
|
430
|
+
return trackChangesPluginKey.getState(state)?.enabled ?? false;
|
|
431
|
+
}
|
|
432
|
+
function getTrackChangesStore(editor) {
|
|
433
|
+
return editor?.storage?.trackChanges?.store ?? null;
|
|
434
|
+
}
|
|
435
|
+
function applyDecision(tr, changes, decision) {
|
|
436
|
+
const schema = tr.doc.type.schema;
|
|
437
|
+
const deletions = [];
|
|
438
|
+
changes.forEach((change) => {
|
|
439
|
+
const markType = schema.marks[change.type];
|
|
440
|
+
if (!markType) return;
|
|
441
|
+
const keep = change.type === "insertion" === (decision === "accept");
|
|
442
|
+
change.ranges.forEach(({ from, to }) => {
|
|
443
|
+
if (keep) {
|
|
444
|
+
tr.removeMark(from, to, markType);
|
|
445
|
+
} else {
|
|
446
|
+
deletions.push({ from, to });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
deletions.sort((a, b) => b.from - a.from).forEach(({ from, to }) => {
|
|
451
|
+
tr.delete(tr.mapping.map(from), tr.mapping.map(to));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function resolveTrackedChanges(doc, view = "accepted") {
|
|
455
|
+
if (view === "all") return doc;
|
|
456
|
+
const changes = getTrackedChanges(doc);
|
|
457
|
+
if (changes.length === 0) return doc;
|
|
458
|
+
const tr = new Transform(doc);
|
|
459
|
+
applyDecision(tr, changes, view === "accepted" ? "accept" : "reject");
|
|
460
|
+
return tr.doc;
|
|
461
|
+
}
|
|
462
|
+
function hasInlineContent(fragment) {
|
|
463
|
+
let found = false;
|
|
464
|
+
fragment.descendants((node) => {
|
|
465
|
+
if (found) return false;
|
|
466
|
+
if (node.isInline) found = true;
|
|
467
|
+
return !found;
|
|
468
|
+
});
|
|
469
|
+
return found;
|
|
470
|
+
}
|
|
471
|
+
function acceptSlice(slice, insertion, deletion) {
|
|
472
|
+
if (!insertion && !deletion) return slice;
|
|
473
|
+
const mapFragment = (fragment) => {
|
|
474
|
+
const children = [];
|
|
475
|
+
fragment.forEach((child) => {
|
|
476
|
+
if (child.isInline) {
|
|
477
|
+
if (deletion && deletion.isInSet(child.marks)) return;
|
|
478
|
+
children.push(insertion ? child.mark(insertion.removeFromSet(child.marks)) : child);
|
|
479
|
+
} else {
|
|
480
|
+
children.push(child.copy(mapFragment(child.content)));
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
return Fragment.from(children);
|
|
484
|
+
};
|
|
485
|
+
return new Slice(mapFragment(slice.content), slice.openStart, slice.openEnd);
|
|
486
|
+
}
|
|
487
|
+
function newAttributes(ctx) {
|
|
488
|
+
return {
|
|
489
|
+
changeId: ctx.generateId(),
|
|
490
|
+
authorId: ctx.author.id,
|
|
491
|
+
authorName: ctx.author.name,
|
|
492
|
+
authorColor: ctx.author.color ?? null,
|
|
493
|
+
createdAt: ctx.now()
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function adjacentAttributes(doc, from, to, type, ctx) {
|
|
497
|
+
const candidates = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];
|
|
498
|
+
for (const node of candidates) {
|
|
499
|
+
const mark = node && type.isInSet(node.marks);
|
|
500
|
+
if (mark && mark.attrs.authorId === ctx.author.id && mark.attrs.changeId) {
|
|
501
|
+
return changeAttributes(mark);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function markDeletedSlice(slice, parentType, attrs, ctx) {
|
|
507
|
+
let ok = true;
|
|
508
|
+
let hasContent = false;
|
|
509
|
+
const deletionMark = ctx.deletion.create(attrs);
|
|
510
|
+
const mapFragment = (fragment, parent) => {
|
|
511
|
+
const children = [];
|
|
512
|
+
fragment.forEach((child) => {
|
|
513
|
+
if (child.isInline) {
|
|
514
|
+
const insertion = ctx.insertion.isInSet(child.marks);
|
|
515
|
+
if (insertion && insertion.attrs.authorId === ctx.author.id) return;
|
|
516
|
+
hasContent = true;
|
|
517
|
+
if (ctx.deletion.isInSet(child.marks)) {
|
|
518
|
+
children.push(child);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (!parent.allowsMarkType(ctx.deletion)) {
|
|
522
|
+
ok = false;
|
|
523
|
+
children.push(child);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
children.push(child.mark(deletionMark.addToSet(child.marks)));
|
|
527
|
+
} else {
|
|
528
|
+
children.push(child.copy(mapFragment(child.content, child.type)));
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
return Fragment.from(children);
|
|
532
|
+
};
|
|
533
|
+
const content = mapFragment(slice.content, parentType);
|
|
534
|
+
if (!ok) return null;
|
|
535
|
+
return { slice: new Slice(content, slice.openStart, slice.openEnd), hasContent };
|
|
536
|
+
}
|
|
537
|
+
function shouldSkip(tr) {
|
|
538
|
+
return !tr.docChanged || tr.getMeta(TRACK_CHANGES_SKIP_META) === true || !!tr.getMeta(trackChangesPluginKey) || // Undo/redo replays already-tracked history.
|
|
539
|
+
!!tr.getMeta("history$") || // Remote collaborative changes were tracked by their author.
|
|
540
|
+
!!tr.getMeta("y-sync$") || // Housekeeping appended by other plugins (e.g. trailing nodes).
|
|
541
|
+
!!tr.getMeta("appendedTransaction");
|
|
542
|
+
}
|
|
543
|
+
function trackTransactions(transactions, oldState, newState, ctx) {
|
|
544
|
+
const maps = [];
|
|
545
|
+
transactions.forEach((t) => maps.push(...t.mapping.maps));
|
|
546
|
+
const inserted = [];
|
|
547
|
+
const restores = [];
|
|
548
|
+
let mapIndex = 0;
|
|
549
|
+
const single = transactions.length === 1 && transactions[0].steps.length === 1;
|
|
550
|
+
transactions.forEach((t) => {
|
|
551
|
+
const skip = shouldSkip(t);
|
|
552
|
+
t.steps.forEach((step, i) => {
|
|
553
|
+
const index = mapIndex + i;
|
|
554
|
+
if (skip || !(step instanceof ReplaceStep)) return;
|
|
555
|
+
const { from, to, slice } = step;
|
|
556
|
+
const rest = new Mapping(maps.slice(index + 1));
|
|
557
|
+
if (slice.size > 0 && hasInlineContent(slice.content)) {
|
|
558
|
+
const start = rest.map(from, 1);
|
|
559
|
+
const end = rest.map(from + slice.size, -1);
|
|
560
|
+
if (end > start) inserted.push({ from: start, to: end });
|
|
561
|
+
}
|
|
562
|
+
if (to > from) {
|
|
563
|
+
const docBefore = t.docs[i];
|
|
564
|
+
const deleted = docBefore.slice(from, to);
|
|
565
|
+
if (!hasInlineContent(deleted.content)) return;
|
|
566
|
+
const $from = docBefore.resolve(from);
|
|
567
|
+
const before = i === 0 ? t.before : null;
|
|
568
|
+
const sel = before === oldState.doc ? oldState.selection : null;
|
|
569
|
+
restores.push({
|
|
570
|
+
pos: rest.map(from, -1),
|
|
571
|
+
slice: deleted,
|
|
572
|
+
parentType: $from.node(Math.max(0, $from.depth - deleted.openStart)).type,
|
|
573
|
+
pureDelete: slice.size === 0,
|
|
574
|
+
backspace: single && slice.size === 0 && !!sel && sel.empty && sel.from === to
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
mapIndex += t.steps.length;
|
|
579
|
+
});
|
|
580
|
+
if (inserted.length === 0 && restores.length === 0) return null;
|
|
581
|
+
const tr = newState.tr;
|
|
582
|
+
let changed = false;
|
|
583
|
+
inserted.forEach(({ from, to }) => {
|
|
584
|
+
tr.removeMark(from, to, ctx.deletion);
|
|
585
|
+
tr.removeMark(from, to, ctx.insertion);
|
|
586
|
+
const attrs = adjacentAttributes(tr.doc, from, to, ctx.insertion, ctx) ?? newAttributes(ctx);
|
|
587
|
+
tr.addMark(from, to, ctx.insertion.create(attrs));
|
|
588
|
+
changed = true;
|
|
589
|
+
});
|
|
590
|
+
let cursor = null;
|
|
591
|
+
restores.sort((a, b) => b.pos - a.pos).forEach((restore) => {
|
|
592
|
+
const pos = tr.mapping.map(restore.pos, -1);
|
|
593
|
+
const attrs = adjacentAttributes(tr.doc, pos, pos, ctx.deletion, ctx) ?? newAttributes(ctx);
|
|
594
|
+
const marked = markDeletedSlice(restore.slice, restore.parentType, attrs, ctx);
|
|
595
|
+
if (!marked || !marked.hasContent) return;
|
|
596
|
+
const sizeBefore = tr.doc.content.size;
|
|
597
|
+
try {
|
|
598
|
+
tr.replace(pos, pos, marked.slice);
|
|
599
|
+
} catch {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
changed = true;
|
|
603
|
+
if (restores.length === 1 && restore.pureDelete && newState.selection.empty) {
|
|
604
|
+
cursor = restore.backspace ? pos : pos + (tr.doc.content.size - sizeBefore);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
if (!changed) return null;
|
|
608
|
+
if (cursor !== null) {
|
|
609
|
+
try {
|
|
610
|
+
tr.setSelection(TextSelection.create(tr.doc, cursor));
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
tr.setMeta(TRACK_CHANGES_SKIP_META, true);
|
|
615
|
+
return tr;
|
|
616
|
+
}
|
|
617
|
+
function buildSnapshot(state, author) {
|
|
618
|
+
const plugin = trackChangesPluginKey.getState(state);
|
|
619
|
+
const changes = getTrackedChanges(state.doc);
|
|
620
|
+
return {
|
|
621
|
+
enabled: plugin?.enabled ?? false,
|
|
622
|
+
author: plugin?.author ?? author,
|
|
623
|
+
changes,
|
|
624
|
+
currentChangeId: getTrackedChangeAt(state, state.selection.from, changes)?.id ?? null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
var TrackChanges = Extension.create({
|
|
628
|
+
name: "trackChanges",
|
|
629
|
+
addOptions() {
|
|
630
|
+
return {
|
|
631
|
+
enabled: false,
|
|
632
|
+
author: { id: "anonymous", name: "Anonymous" },
|
|
633
|
+
now: () => Date.now(),
|
|
634
|
+
generateId: () => createReviewId("change"),
|
|
635
|
+
insertionHTMLAttributes: {},
|
|
636
|
+
deletionHTMLAttributes: {}
|
|
637
|
+
};
|
|
638
|
+
},
|
|
639
|
+
addStorage() {
|
|
640
|
+
return {
|
|
641
|
+
store: new TrackChangesStore({
|
|
642
|
+
enabled: this.options.enabled,
|
|
643
|
+
author: this.options.author,
|
|
644
|
+
changes: [],
|
|
645
|
+
currentChangeId: null
|
|
646
|
+
})
|
|
647
|
+
};
|
|
648
|
+
},
|
|
649
|
+
addExtensions() {
|
|
650
|
+
const insertionAttrs = this.options?.insertionHTMLAttributes ?? {};
|
|
651
|
+
const deletionAttrs = this.options?.deletionHTMLAttributes ?? {};
|
|
652
|
+
return [
|
|
653
|
+
Object.keys(insertionAttrs).length ? TrackedInsertion.configure({ HTMLAttributes: insertionAttrs }) : TrackedInsertion,
|
|
654
|
+
Object.keys(deletionAttrs).length ? TrackedDeletion.configure({ HTMLAttributes: deletionAttrs }) : TrackedDeletion
|
|
655
|
+
];
|
|
656
|
+
},
|
|
657
|
+
addCommands() {
|
|
658
|
+
const decide = (decision, id) => ({ state, tr, dispatch }) => {
|
|
659
|
+
const all = getTrackedChanges(state.doc);
|
|
660
|
+
const changes = id === null ? all : all.filter((c) => c.id === id);
|
|
661
|
+
if (changes.length === 0) return false;
|
|
662
|
+
if (dispatch) {
|
|
663
|
+
applyDecision(tr, changes, decision);
|
|
664
|
+
tr.setMeta(TRACK_CHANGES_SKIP_META, true);
|
|
665
|
+
}
|
|
666
|
+
return true;
|
|
667
|
+
};
|
|
668
|
+
const select = (direction) => ({ state, tr, dispatch }) => {
|
|
669
|
+
const changes = getTrackedChanges(state.doc);
|
|
670
|
+
if (changes.length === 0) return false;
|
|
671
|
+
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];
|
|
673
|
+
if (dispatch) {
|
|
674
|
+
tr.setSelection(TextSelection.create(tr.doc, target.from, target.to)).scrollIntoView();
|
|
675
|
+
}
|
|
676
|
+
return true;
|
|
677
|
+
};
|
|
678
|
+
return {
|
|
679
|
+
setTrackChanges: (enabled) => ({ tr, dispatch }) => {
|
|
680
|
+
if (dispatch) {
|
|
681
|
+
tr.setMeta(trackChangesPluginKey, { type: "setEnabled", enabled });
|
|
682
|
+
tr.setMeta("addToHistory", false);
|
|
683
|
+
}
|
|
684
|
+
return true;
|
|
685
|
+
},
|
|
686
|
+
toggleTrackChanges: () => ({ state, commands }) => commands.setTrackChanges(!isTrackChangesEnabled(state)),
|
|
687
|
+
setTrackChangesAuthor: (author) => ({ tr, dispatch }) => {
|
|
688
|
+
if (dispatch) {
|
|
689
|
+
tr.setMeta(trackChangesPluginKey, { type: "setAuthor", author });
|
|
690
|
+
tr.setMeta("addToHistory", false);
|
|
691
|
+
}
|
|
692
|
+
return true;
|
|
693
|
+
},
|
|
694
|
+
acceptChange: (id) => decide("accept", id),
|
|
695
|
+
rejectChange: (id) => decide("reject", id),
|
|
696
|
+
acceptAllChanges: () => decide("accept", null),
|
|
697
|
+
rejectAllChanges: () => decide("reject", null),
|
|
698
|
+
selectNextChange: () => select(1),
|
|
699
|
+
selectPreviousChange: () => select(-1)
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
addProseMirrorPlugins() {
|
|
703
|
+
const options = this.options;
|
|
704
|
+
const store = this.storage.store;
|
|
705
|
+
let editorView = null;
|
|
706
|
+
let pending = null;
|
|
707
|
+
let flushTimer = null;
|
|
708
|
+
const scheduleFlush = () => {
|
|
709
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
710
|
+
flushTimer = setTimeout(() => {
|
|
711
|
+
flushTimer = null;
|
|
712
|
+
const view = editorView;
|
|
713
|
+
if (!view || !pending) return;
|
|
714
|
+
if (view.composing) {
|
|
715
|
+
scheduleFlush();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
view.dispatch(view.state.tr.setMeta("trackChangesFlush", true));
|
|
719
|
+
}, 0);
|
|
720
|
+
};
|
|
721
|
+
return [
|
|
722
|
+
new Plugin({
|
|
723
|
+
key: trackChangesPluginKey,
|
|
724
|
+
state: {
|
|
725
|
+
init: () => ({ enabled: options.enabled, author: options.author }),
|
|
726
|
+
apply: (tr, value) => {
|
|
727
|
+
const meta = tr.getMeta(trackChangesPluginKey);
|
|
728
|
+
if (!meta) return value;
|
|
729
|
+
if (meta.type === "setEnabled") {
|
|
730
|
+
return meta.enabled === value.enabled ? value : { ...value, enabled: meta.enabled };
|
|
731
|
+
}
|
|
732
|
+
return { ...value, author: meta.author };
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
appendTransaction: (transactions, oldState, newState) => {
|
|
736
|
+
const plugin = trackChangesPluginKey.getState(oldState);
|
|
737
|
+
const tracking = !!plugin?.enabled;
|
|
738
|
+
if (!tracking && !pending) return null;
|
|
739
|
+
if (editorView?.composing && transactions.some((t) => t.docChanged)) {
|
|
740
|
+
if (tracking) {
|
|
741
|
+
if (!pending) pending = { oldState, transactions: [] };
|
|
742
|
+
pending.transactions.push(...transactions);
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
let trs = transactions;
|
|
747
|
+
let fromState = oldState;
|
|
748
|
+
if (pending) {
|
|
749
|
+
trs = [...pending.transactions, ...transactions];
|
|
750
|
+
fromState = pending.oldState;
|
|
751
|
+
pending = null;
|
|
752
|
+
}
|
|
753
|
+
if (!tracking && trs === transactions) return null;
|
|
754
|
+
const author = plugin?.author ?? options.author;
|
|
755
|
+
return trackTransactions(trs, fromState, newState, {
|
|
756
|
+
author,
|
|
757
|
+
now: options.now,
|
|
758
|
+
generateId: options.generateId,
|
|
759
|
+
insertion: newState.schema.marks[INSERTION_MARK_NAME],
|
|
760
|
+
deletion: newState.schema.marks[DELETION_MARK_NAME]
|
|
761
|
+
});
|
|
762
|
+
},
|
|
763
|
+
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
|
+
),
|
|
769
|
+
handleDOMEvents: {
|
|
770
|
+
compositionend: () => {
|
|
771
|
+
if (pending) scheduleFlush();
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
view: (view) => {
|
|
777
|
+
editorView = view;
|
|
778
|
+
store.set(buildSnapshot(view.state, options.author));
|
|
779
|
+
return {
|
|
780
|
+
update(view2, prevState) {
|
|
781
|
+
if (prevState.doc !== view2.state.doc || !prevState.selection.eq(view2.state.selection) || trackChangesPluginKey.getState(prevState) !== trackChangesPluginKey.getState(view2.state)) {
|
|
782
|
+
store.set(buildSnapshot(view2.state, options.author));
|
|
783
|
+
}
|
|
784
|
+
if (pending && !view2.composing) scheduleFlush();
|
|
785
|
+
},
|
|
786
|
+
destroy() {
|
|
787
|
+
editorView = null;
|
|
788
|
+
pending = null;
|
|
789
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
];
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// src/serializers/plain-text.ts
|
|
799
|
+
import { Slice as Slice2, Fragment as Fragment2, Node as ProsemirrorNode } from "@tiptap/pm/model";
|
|
800
|
+
var TRACKED_DELETION_REGEX = /<del\b(?=[^>]*\bdata-change-type=["']deletion["'])[^>]*>[\s\S]*?<\/del>/gi;
|
|
801
|
+
var TRACKED_INSERTION_REGEX = /<ins\b(?=[^>]*\bdata-change-type=["']insertion["'])[^>]*>[\s\S]*?<\/ins>/gi;
|
|
802
|
+
function stripTrackedChanges(html, view) {
|
|
803
|
+
if (view === "all" || !html.includes("data-change-type")) return html;
|
|
804
|
+
return html.replace(
|
|
805
|
+
view === "accepted" ? TRACKED_DELETION_REGEX : TRACKED_INSERTION_REGEX,
|
|
806
|
+
""
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
function escapeHtml(text) {
|
|
810
|
+
if (!text) return "";
|
|
811
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
812
|
+
}
|
|
813
|
+
var PARAGRAPH_HTML_REGEX = /^\s*<p[\s>][\s\S]*<\/p>\s*$/i;
|
|
814
|
+
function plainTextToTipTapHtml(text) {
|
|
815
|
+
if (!text) return "<p></p>";
|
|
816
|
+
if (PARAGRAPH_HTML_REGEX.test(text)) {
|
|
817
|
+
return text;
|
|
818
|
+
}
|
|
819
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => `<p>${escapeHtml(line)}</p>`).join("");
|
|
820
|
+
}
|
|
821
|
+
function plainTextToSlice(text, schema) {
|
|
822
|
+
const nodes = [];
|
|
823
|
+
text.replace(/\r\n?/g, "\n").split("\n").forEach((line) => {
|
|
824
|
+
const nodeJson = { type: "paragraph" };
|
|
825
|
+
if (line.length > 0) {
|
|
826
|
+
nodeJson.content = [{ type: "text", text: line }];
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
nodes.push(ProsemirrorNode.fromJSON(schema, nodeJson));
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
return Slice2.maxOpen(Fragment2.fromArray(nodes));
|
|
834
|
+
}
|
|
835
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
836
|
+
"P",
|
|
837
|
+
"DIV",
|
|
838
|
+
"LI",
|
|
839
|
+
"UL",
|
|
840
|
+
"OL",
|
|
841
|
+
"PRE",
|
|
842
|
+
"BLOCKQUOTE",
|
|
843
|
+
"TABLE",
|
|
844
|
+
"TR",
|
|
845
|
+
"H1",
|
|
846
|
+
"H2",
|
|
847
|
+
"H3",
|
|
848
|
+
"H4",
|
|
849
|
+
"H5",
|
|
850
|
+
"H6",
|
|
851
|
+
"HR"
|
|
852
|
+
]);
|
|
853
|
+
var MENTION_SPAN_REGEX = /<span\b(?=[^>]*\bdata-type=["']mention["'])([^>]*)>[\s\S]*?<\/span>/gi;
|
|
854
|
+
function getHtmlAttribute(attrs, name) {
|
|
855
|
+
const match = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`, "i").exec(
|
|
856
|
+
attrs
|
|
857
|
+
);
|
|
858
|
+
return match ? match[1] ?? match[2] : void 0;
|
|
859
|
+
}
|
|
860
|
+
function parseHtmlFragment(html) {
|
|
861
|
+
const template = document.createElement("template");
|
|
862
|
+
template.innerHTML = html;
|
|
863
|
+
return template.content;
|
|
864
|
+
}
|
|
865
|
+
function decodeHtmlEntities(text) {
|
|
866
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/�*39;/g, "'").replace(/ /g, "\xA0").replace(/&/g, "&");
|
|
867
|
+
}
|
|
868
|
+
function htmlToPlainText(html, options = {}) {
|
|
869
|
+
if (!html) return "";
|
|
870
|
+
const mentionNormalized = stripTrackedChanges(
|
|
871
|
+
html,
|
|
872
|
+
options.trackedChanges ?? "accepted"
|
|
873
|
+
).replace(MENTION_SPAN_REGEX, (match, attrs) => {
|
|
874
|
+
const label = getHtmlAttribute(attrs, "data-label") || getHtmlAttribute(attrs, "data-id");
|
|
875
|
+
return label ? `@${label}` : match;
|
|
876
|
+
});
|
|
877
|
+
if (typeof document !== "undefined") {
|
|
878
|
+
const fragment = parseHtmlFragment(mentionNormalized);
|
|
879
|
+
fragment.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
|
|
880
|
+
const lines = [];
|
|
881
|
+
let current = null;
|
|
882
|
+
const flush = () => {
|
|
883
|
+
if (current !== null) {
|
|
884
|
+
lines.push(current);
|
|
885
|
+
current = null;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
const visit = (node) => {
|
|
889
|
+
if (node.nodeType === 3) {
|
|
890
|
+
const value = node.nodeValue ?? "";
|
|
891
|
+
if (current === null && value.trim() === "") return;
|
|
892
|
+
current = (current ?? "") + value;
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (node.nodeType !== 1) return;
|
|
896
|
+
const tag = node.tagName.toUpperCase();
|
|
897
|
+
if (tag === "BR") {
|
|
898
|
+
current = (current ?? "") + "\n";
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (!BLOCK_TAGS.has(tag)) {
|
|
902
|
+
node.childNodes.forEach(visit);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
flush();
|
|
906
|
+
const linesBefore = lines.length;
|
|
907
|
+
node.childNodes.forEach(visit);
|
|
908
|
+
flush();
|
|
909
|
+
if (lines.length === linesBefore && tag !== "HR") {
|
|
910
|
+
lines.push("");
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
fragment.childNodes.forEach(visit);
|
|
914
|
+
flush();
|
|
915
|
+
return lines.join("\n");
|
|
916
|
+
}
|
|
917
|
+
const text = mentionNormalized.replace(/<br\s*\/?>/gi, "\n").replace(
|
|
918
|
+
/<\/(?:p|div|li|pre|blockquote|h[1-6])>(?:\s*<\/(?:li|ul|ol|blockquote|div)>)*/gi,
|
|
919
|
+
"\n"
|
|
920
|
+
).replace(/<[^>]+>/g, "");
|
|
921
|
+
return decodeHtmlEntities(text).replace(/\n$/, "");
|
|
922
|
+
}
|
|
923
|
+
function getEditorText(editor, options = {}) {
|
|
924
|
+
if (!editor) return "";
|
|
925
|
+
const html = typeof editor.getHTML === "function" ? editor.getHTML() : "";
|
|
926
|
+
if (html) {
|
|
927
|
+
return htmlToPlainText(html, options);
|
|
928
|
+
}
|
|
929
|
+
if (typeof editor.getText === "function") {
|
|
930
|
+
try {
|
|
931
|
+
return editor.getText({ blockSeparator: "\n" });
|
|
932
|
+
} catch {
|
|
933
|
+
return editor.getText();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return "";
|
|
937
|
+
}
|
|
938
|
+
function extractMentionsFromDoc(docOrEditor) {
|
|
939
|
+
if (!docOrEditor) return [];
|
|
940
|
+
const mentions = [];
|
|
941
|
+
const doc = docOrEditor?.state?.doc || (typeof docOrEditor?.descendants === "function" ? docOrEditor : null);
|
|
942
|
+
if (doc && typeof doc.descendants === "function") {
|
|
943
|
+
doc.descendants((node) => {
|
|
944
|
+
if (node.type?.name === "mention" && node.attrs) {
|
|
945
|
+
mentions.push({
|
|
946
|
+
id: node.attrs.id || "",
|
|
947
|
+
label: node.attrs.label || "",
|
|
948
|
+
type: node.attrs.type,
|
|
949
|
+
avatarUrl: node.attrs.avatarUrl,
|
|
950
|
+
color: node.attrs.color,
|
|
951
|
+
...node.attrs
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
return mentions;
|
|
956
|
+
}
|
|
957
|
+
if (typeof docOrEditor === "object" && docOrEditor.content) {
|
|
958
|
+
const walk = (nodes) => {
|
|
959
|
+
for (const node of nodes) {
|
|
960
|
+
if (node.type === "mention" && node.attrs) {
|
|
961
|
+
mentions.push({
|
|
962
|
+
id: node.attrs.id || "",
|
|
963
|
+
label: node.attrs.label || "",
|
|
964
|
+
type: node.attrs.type,
|
|
965
|
+
avatarUrl: node.attrs.avatarUrl,
|
|
966
|
+
color: node.attrs.color,
|
|
967
|
+
...node.attrs
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
if (Array.isArray(node.content)) {
|
|
971
|
+
walk(node.content);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
walk(docOrEditor.content);
|
|
976
|
+
return mentions;
|
|
977
|
+
}
|
|
978
|
+
if (typeof docOrEditor === "string") {
|
|
979
|
+
if (typeof document !== "undefined") {
|
|
980
|
+
parseHtmlFragment(docOrEditor).querySelectorAll('span[data-type="mention"]').forEach((el) => {
|
|
981
|
+
mentions.push({
|
|
982
|
+
id: el.getAttribute("data-id") || "",
|
|
983
|
+
label: el.getAttribute("data-label") || "",
|
|
984
|
+
type: el.getAttribute("data-type-name") || void 0,
|
|
985
|
+
avatarUrl: el.getAttribute("data-avatar-url") || void 0,
|
|
986
|
+
color: el.getAttribute("data-color") || void 0
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
} else {
|
|
990
|
+
const spanRegex = /<span([^>]*data-type=["']mention["'][^>]*)>/gi;
|
|
991
|
+
let match;
|
|
992
|
+
while ((match = spanRegex.exec(docOrEditor)) !== null) {
|
|
993
|
+
const attrStr = match[1];
|
|
994
|
+
const getAttr = (name) => {
|
|
995
|
+
const m = new RegExp(`${name}=["']([^"']*)["']`, "i").exec(attrStr);
|
|
996
|
+
return m ? m[1] : void 0;
|
|
997
|
+
};
|
|
998
|
+
mentions.push({
|
|
999
|
+
id: getAttr("data-id") || "",
|
|
1000
|
+
label: getAttr("data-label") || "",
|
|
1001
|
+
type: getAttr("data-type-name") || getAttr("data-entity-type"),
|
|
1002
|
+
avatarUrl: getAttr("data-avatar-url"),
|
|
1003
|
+
color: getAttr("data-color")
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (mentions.length === 0) {
|
|
1008
|
+
const rawHandles = extractMentions(docOrEditor);
|
|
1009
|
+
for (const handle of rawHandles) {
|
|
1010
|
+
mentions.push({
|
|
1011
|
+
id: handle,
|
|
1012
|
+
label: handle,
|
|
1013
|
+
type: "user"
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return mentions;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/serializers/mentions.ts
|
|
1022
|
+
var MENTION_REGEX = /(?:^|[\s>(*_"';\\/[\]{}~`:]|"|>|<|&)@(?:"([^"]+)"|"([^&]+)"|([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*))/g;
|
|
1023
|
+
var MARKDOWN_MENTION_SPLIT_REGEX = /(```[\s\S]*?```|~~~[\s\S]*?~~~|<pre[\s\S]*?<\/pre>|<code[\s\S]*?<\/code>|`[^`\n]+`|<span\b[^>]*\b(?:data-type=["']mention["']|class=["'][^"']*pixerate-mention-node[^"']*["']|data-mention=["'][^"']*["'])[^>]*>[\s\S]*?<\/span>|<[^>]+>|\]\([^)]+\)|(?:https?|file|vscode|cursor|mailto):\/\/[^\s<)]+)/gi;
|
|
1024
|
+
function stripMarkdownCode(content) {
|
|
1025
|
+
if (!content || typeof content !== "string") return "";
|
|
1026
|
+
return content.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/<pre[\s\S]*?<\/pre>/gi, "").replace(/<code[\s\S]*?<\/code>/gi, "").replace(/`[^`\n]*`/g, "");
|
|
1027
|
+
}
|
|
1028
|
+
function extractMentions(content) {
|
|
1029
|
+
if (!content || typeof content !== "string") return [];
|
|
1030
|
+
const normalizedMentionNodes = content.replace(
|
|
1031
|
+
/<span\b[^>]*\b(?:data-type=["']mention["']|class=["'][^"']*pixerate-mention-node[^"']*["'])[^>]*>([\s\S]*?)<\/span>/gi,
|
|
1032
|
+
(match2, inner) => {
|
|
1033
|
+
const labelMatch = match2.match(/\bdata-label=["']([^"']+)["']/i);
|
|
1034
|
+
if (labelMatch) return ` @${labelMatch[1]} `;
|
|
1035
|
+
const idMatch = match2.match(/\bdata-id=["']([^"']+)["']/i);
|
|
1036
|
+
if (idMatch) return ` @${idMatch[1]} `;
|
|
1037
|
+
return ` ${inner} `;
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
const cleanContent = stripMarkdownCode(normalizedMentionNodes);
|
|
1041
|
+
const withoutUrls = cleanContent.replace(/(?:https?|ftp|file|vscode|cursor|mailto):\/\/[^\s<>()]+/gi, "").replace(/\]\([^)]*\)/g, "]");
|
|
1042
|
+
const textOnly = withoutUrls.replace(/<[^>]+>/g, " ");
|
|
1043
|
+
const mentions = [];
|
|
1044
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1045
|
+
const regex = new RegExp(MENTION_REGEX.source, "g");
|
|
1046
|
+
let match;
|
|
1047
|
+
while ((match = regex.exec(textOnly)) !== null) {
|
|
1048
|
+
const handle = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
1049
|
+
if (handle && !seen.has(handle)) {
|
|
1050
|
+
seen.add(handle);
|
|
1051
|
+
mentions.push(handle);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return mentions;
|
|
1055
|
+
}
|
|
1056
|
+
function parseMentionSegments(content) {
|
|
1057
|
+
if (!content) return [];
|
|
1058
|
+
const segments = [];
|
|
1059
|
+
const regex = new RegExp(MENTION_REGEX.source, "g");
|
|
1060
|
+
let lastIndex = 0;
|
|
1061
|
+
let match;
|
|
1062
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1063
|
+
const fullMatch = match[0];
|
|
1064
|
+
const atIndex = fullMatch.indexOf("@");
|
|
1065
|
+
const prefix = atIndex > 0 ? fullMatch.slice(0, atIndex) : "";
|
|
1066
|
+
const mentionToken = fullMatch.slice(atIndex);
|
|
1067
|
+
const handle = match[1] ?? match[2] ?? match[3];
|
|
1068
|
+
const matchStart = match.index + prefix.length;
|
|
1069
|
+
if (matchStart > lastIndex) {
|
|
1070
|
+
segments.push({
|
|
1071
|
+
type: "text",
|
|
1072
|
+
value: content.slice(lastIndex, matchStart)
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
segments.push({
|
|
1076
|
+
type: "mention",
|
|
1077
|
+
value: mentionToken,
|
|
1078
|
+
handle
|
|
1079
|
+
});
|
|
1080
|
+
lastIndex = matchStart + mentionToken.length;
|
|
1081
|
+
}
|
|
1082
|
+
if (lastIndex < content.length) {
|
|
1083
|
+
segments.push({
|
|
1084
|
+
type: "text",
|
|
1085
|
+
value: content.slice(lastIndex)
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
return segments;
|
|
1089
|
+
}
|
|
1090
|
+
function preprocessMarkdownMentions(content, optionsOrMentionables = []) {
|
|
1091
|
+
if (!content || typeof content !== "string") return "";
|
|
1092
|
+
const options = Array.isArray(optionsOrMentionables) ? { mentionables: optionsOrMentionables } : optionsOrMentionables;
|
|
1093
|
+
const allMentionables = options.mentionables || [];
|
|
1094
|
+
const defaultType = options.defaultType || "user";
|
|
1095
|
+
const resolveMention = options.resolveMention;
|
|
1096
|
+
const parts = content.split(MARKDOWN_MENTION_SPLIT_REGEX);
|
|
1097
|
+
return parts.map((part) => {
|
|
1098
|
+
if (!part) return "";
|
|
1099
|
+
if (part.startsWith("```") || part.startsWith("~~~") || part.startsWith("`") || part.startsWith("](") || /^(?:https?|file|vscode|cursor|mailto):\/\//i.test(part) || /^<pre[\s>]/i.test(part) || /^<code[\s>]/i.test(part) || /^<[^>]+>$/i.test(part) || /^<span\b[^>]*\b(?:data-type=["']mention["']|class=["'][^"']*pixerate-mention-node[^"']*["']|data-mention=["'][^"']*["'])/i.test(
|
|
1100
|
+
part
|
|
1101
|
+
)) {
|
|
1102
|
+
return part;
|
|
1103
|
+
}
|
|
1104
|
+
const regex = new RegExp(MENTION_REGEX.source, "g");
|
|
1105
|
+
return part.replace(regex, (match, quotedHandle, escapedQuotedHandle, standardHandle) => {
|
|
1106
|
+
const atIndex = match.indexOf("@");
|
|
1107
|
+
const prefix = atIndex > 0 ? match.slice(0, atIndex) : "";
|
|
1108
|
+
const rawHandle = (quotedHandle || escapedQuotedHandle || standardHandle || "").trim();
|
|
1109
|
+
if (!rawHandle) return match;
|
|
1110
|
+
const lowerHandle = rawHandle.toLowerCase();
|
|
1111
|
+
const entity = allMentionables.find((e) => {
|
|
1112
|
+
if (e.label?.toLowerCase() === lowerHandle) return true;
|
|
1113
|
+
if (e.handle?.toLowerCase() === lowerHandle) return true;
|
|
1114
|
+
if (e.name?.toLowerCase() === lowerHandle) return true;
|
|
1115
|
+
if (e.id?.toLowerCase() === lowerHandle) return true;
|
|
1116
|
+
if (e.type === "agent" && `agent${(e.handle || e.label || "").toLowerCase()}` === lowerHandle) return true;
|
|
1117
|
+
if (e.aliases?.some(
|
|
1118
|
+
(a) => a.toLowerCase() === lowerHandle || e.type === "agent" && `agent${a.toLowerCase()}` === lowerHandle
|
|
1119
|
+
)) {
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
return false;
|
|
1123
|
+
});
|
|
1124
|
+
let id = entity?.id;
|
|
1125
|
+
let label = entity?.label || entity?.handle || rawHandle;
|
|
1126
|
+
let type = entity?.type;
|
|
1127
|
+
let avatarUrl = entity?.avatarUrl;
|
|
1128
|
+
let color = entity?.color || entity?.hexColor;
|
|
1129
|
+
if (!entity) {
|
|
1130
|
+
const customResolved = resolveMention?.(rawHandle);
|
|
1131
|
+
if (customResolved) {
|
|
1132
|
+
id = customResolved.id ?? id;
|
|
1133
|
+
label = customResolved.label ?? customResolved.handle ?? label;
|
|
1134
|
+
type = customResolved.type ?? type;
|
|
1135
|
+
avatarUrl = customResolved.avatarUrl ?? avatarUrl;
|
|
1136
|
+
color = customResolved.color ?? customResolved.hexColor ?? color;
|
|
1137
|
+
} else {
|
|
1138
|
+
const stripped = lowerHandle.replace(/^agent[\s\-_]*/i, "");
|
|
1139
|
+
if (["supervisor", "coordinator", "planner", "validator", "executor"].includes(stripped)) {
|
|
1140
|
+
type = "agent";
|
|
1141
|
+
id = stripped;
|
|
1142
|
+
label = stripped;
|
|
1143
|
+
} else if (lowerHandle === "agents") {
|
|
1144
|
+
type = "team";
|
|
1145
|
+
id = "agents";
|
|
1146
|
+
label = "agents";
|
|
1147
|
+
} else {
|
|
1148
|
+
type = defaultType;
|
|
1149
|
+
id = rawHandle;
|
|
1150
|
+
label = rawHandle;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const avatarAttr = avatarUrl ? ` data-avatar-url="${escapeHtml(avatarUrl)}"` : "";
|
|
1155
|
+
const colorAttr = color ? ` data-color="${escapeHtml(color)}"` : "";
|
|
1156
|
+
return `${prefix}<span class="pixerate-mention-node" data-type="mention" data-id="${escapeHtml(
|
|
1157
|
+
id || label
|
|
1158
|
+
)}" data-label="${escapeHtml(label)}" data-mention-suggestion-char="@" data-type-name="${escapeHtml(
|
|
1159
|
+
type || defaultType
|
|
1160
|
+
)}"${avatarAttr}${colorAttr}>@${escapeHtml(label)}</span>`;
|
|
1161
|
+
});
|
|
1162
|
+
}).join("");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/serializers/markdown.ts
|
|
1166
|
+
import MarkdownIt from "markdown-it";
|
|
1167
|
+
import taskLists from "markdown-it-task-lists";
|
|
1168
|
+
var md = new MarkdownIt({
|
|
1169
|
+
html: true,
|
|
1170
|
+
linkify: true,
|
|
1171
|
+
breaks: true
|
|
1172
|
+
}).use(taskLists);
|
|
1173
|
+
var defaultValidateLink = md.validateLink;
|
|
1174
|
+
md.validateLink = (url) => {
|
|
1175
|
+
if (url.startsWith("file://") || url.startsWith("vscode://") || url.startsWith("cursor://")) {
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
return defaultValidateLink.call(md, url);
|
|
1179
|
+
};
|
|
1180
|
+
var ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
1181
|
+
"a",
|
|
1182
|
+
"abbr",
|
|
1183
|
+
"b",
|
|
1184
|
+
"blockquote",
|
|
1185
|
+
"br",
|
|
1186
|
+
"code",
|
|
1187
|
+
"del",
|
|
1188
|
+
"details",
|
|
1189
|
+
"div",
|
|
1190
|
+
"em",
|
|
1191
|
+
"h1",
|
|
1192
|
+
"h2",
|
|
1193
|
+
"h3",
|
|
1194
|
+
"h4",
|
|
1195
|
+
"h5",
|
|
1196
|
+
"h6",
|
|
1197
|
+
"hr",
|
|
1198
|
+
"i",
|
|
1199
|
+
"img",
|
|
1200
|
+
"input",
|
|
1201
|
+
"kbd",
|
|
1202
|
+
"li",
|
|
1203
|
+
"mark",
|
|
1204
|
+
"ol",
|
|
1205
|
+
"p",
|
|
1206
|
+
"pre",
|
|
1207
|
+
"s",
|
|
1208
|
+
"span",
|
|
1209
|
+
"strong",
|
|
1210
|
+
"sub",
|
|
1211
|
+
"summary",
|
|
1212
|
+
"sup",
|
|
1213
|
+
"table",
|
|
1214
|
+
"tbody",
|
|
1215
|
+
"td",
|
|
1216
|
+
"tfoot",
|
|
1217
|
+
"th",
|
|
1218
|
+
"thead",
|
|
1219
|
+
"tr",
|
|
1220
|
+
"u",
|
|
1221
|
+
"ul"
|
|
1222
|
+
]);
|
|
1223
|
+
var ALLOWED_HTML_ATTRS = /* @__PURE__ */ new Set([
|
|
1224
|
+
"alt",
|
|
1225
|
+
"align",
|
|
1226
|
+
"checked",
|
|
1227
|
+
"class",
|
|
1228
|
+
"colspan",
|
|
1229
|
+
"disabled",
|
|
1230
|
+
"height",
|
|
1231
|
+
"href",
|
|
1232
|
+
"rel",
|
|
1233
|
+
"rowspan",
|
|
1234
|
+
"src",
|
|
1235
|
+
"start",
|
|
1236
|
+
"target",
|
|
1237
|
+
"title",
|
|
1238
|
+
"type",
|
|
1239
|
+
"width"
|
|
1240
|
+
]);
|
|
1241
|
+
var HTML_TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^\s"'>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
|
|
1242
|
+
var HTML_ATTR_REGEX = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
1243
|
+
function sanitizeTag(tagText, isClosing, rawTagName, rawAttrs, stripDisallowed) {
|
|
1244
|
+
const tagName = rawTagName.toLowerCase();
|
|
1245
|
+
const reject = () => stripDisallowed ? "" : escapeHtml(tagText);
|
|
1246
|
+
if (!ALLOWED_HTML_TAGS.has(tagName)) return reject();
|
|
1247
|
+
if (isClosing) return `</${tagName}>`;
|
|
1248
|
+
if (tagName === "input" && !/\stype\s*=\s*["']?checkbox["']?[\s>/]/i.test(tagText)) {
|
|
1249
|
+
return reject();
|
|
1250
|
+
}
|
|
1251
|
+
let attrs = "";
|
|
1252
|
+
let attrMatch;
|
|
1253
|
+
HTML_ATTR_REGEX.lastIndex = 0;
|
|
1254
|
+
while ((attrMatch = HTML_ATTR_REGEX.exec(rawAttrs)) !== null) {
|
|
1255
|
+
const name = attrMatch[1].toLowerCase();
|
|
1256
|
+
if (!ALLOWED_HTML_ATTRS.has(name) && !/^data-[a-z0-9_.-]+$/.test(name)) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
1260
|
+
if (rawValue === void 0) {
|
|
1261
|
+
attrs += ` ${name}`;
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const value = md.utils.unescapeAll(rawValue);
|
|
1265
|
+
if ((name === "href" || name === "src") && // Browsers ignore control characters and whitespace inside schemes.
|
|
1266
|
+
!md.validateLink(value.replace(/[\u0000-\u0020\u007f]/g, ""))) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
attrs += ` ${name}="${escapeHtml(value)}"`;
|
|
1270
|
+
}
|
|
1271
|
+
return `<${tagName}${attrs}>`;
|
|
1272
|
+
}
|
|
1273
|
+
function sanitizeHtml(html, options = {}) {
|
|
1274
|
+
return sanitizeEmbeddedHtml(html, options.disallowedTags === "strip");
|
|
1275
|
+
}
|
|
1276
|
+
function sanitizeEmbeddedHtml(html, stripDisallowed = false) {
|
|
1277
|
+
let result = "";
|
|
1278
|
+
let lastIndex = 0;
|
|
1279
|
+
let match;
|
|
1280
|
+
HTML_TAG_REGEX.lastIndex = 0;
|
|
1281
|
+
while ((match = HTML_TAG_REGEX.exec(html)) !== null) {
|
|
1282
|
+
result += html.slice(lastIndex, match.index).replace(/</g, "<");
|
|
1283
|
+
result += sanitizeTag(match[0], match[1], match[2], match[3], stripDisallowed);
|
|
1284
|
+
lastIndex = match.index + match[0].length;
|
|
1285
|
+
}
|
|
1286
|
+
return result + html.slice(lastIndex).replace(/</g, "<");
|
|
1287
|
+
}
|
|
1288
|
+
var defaultHtmlInline = md.renderer.rules.html_inline;
|
|
1289
|
+
var defaultHtmlBlock = md.renderer.rules.html_block;
|
|
1290
|
+
md.renderer.rules.html_inline = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlInline(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
|
|
1291
|
+
md.renderer.rules.html_block = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlBlock(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
|
|
1292
|
+
function preprocessMarkdownFileLinks(content) {
|
|
1293
|
+
if (!content) return "";
|
|
1294
|
+
return content.replace(
|
|
1295
|
+
/\[([^\]]+)\]\(((?:file|vscode|cursor):\/\/[^)]+)\)/g,
|
|
1296
|
+
(_match, rawLabel, href) => {
|
|
1297
|
+
const trimmed = rawLabel.trim();
|
|
1298
|
+
const codeMatch = trimmed.match(/^`([^`]+)`$/);
|
|
1299
|
+
const labelContent = codeMatch ? `<code>${escapeHtml(codeMatch[1])}</code>` : escapeHtml(trimmed);
|
|
1300
|
+
return `<a href="${escapeHtml(href)}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
|
|
1301
|
+
}
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
function markdownToTipTapHtml(markdown, options) {
|
|
1305
|
+
if (!markdown) return "<p></p>";
|
|
1306
|
+
let content = markdown;
|
|
1307
|
+
if (options?.preprocessFileLinks) {
|
|
1308
|
+
content = preprocessMarkdownFileLinks(content);
|
|
1309
|
+
}
|
|
1310
|
+
if (options?.preprocessMentions || options?.mentionables || options?.preprocessOptions) {
|
|
1311
|
+
const mentionOpts = options.preprocessOptions || {
|
|
1312
|
+
mentionables: options.mentionables
|
|
1313
|
+
};
|
|
1314
|
+
content = preprocessMarkdownMentions(content, mentionOpts);
|
|
1315
|
+
}
|
|
1316
|
+
return md.render(content, { allowUnsafeHtml: options?.allowUnsafeHtml });
|
|
1317
|
+
}
|
|
1318
|
+
function getEditorMarkdown(editor, options = {}) {
|
|
1319
|
+
if (!editor) return "";
|
|
1320
|
+
const markdown = editor.storage?.markdown;
|
|
1321
|
+
if (typeof markdown?.getMarkdown === "function") {
|
|
1322
|
+
const doc = editor.state?.doc;
|
|
1323
|
+
if (doc && typeof markdown.serializer?.serialize === "function") {
|
|
1324
|
+
const resolved = resolveTrackedChanges(doc, options.trackedChanges ?? "accepted");
|
|
1325
|
+
if (resolved !== doc) return markdown.serializer.serialize(resolved);
|
|
1326
|
+
}
|
|
1327
|
+
return markdown.getMarkdown();
|
|
1328
|
+
}
|
|
1329
|
+
return getEditorText(editor, options);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
export {
|
|
1333
|
+
createReviewId,
|
|
1334
|
+
CommentStore,
|
|
1335
|
+
createCommentStore,
|
|
1336
|
+
formatReviewTime,
|
|
1337
|
+
INSERTION_MARK_NAME,
|
|
1338
|
+
DELETION_MARK_NAME,
|
|
1339
|
+
TRACK_CHANGES_SKIP_META,
|
|
1340
|
+
TrackChangesStore,
|
|
1341
|
+
trackChangesPluginKey,
|
|
1342
|
+
TrackedInsertion,
|
|
1343
|
+
TrackedDeletion,
|
|
1344
|
+
getTrackedChanges,
|
|
1345
|
+
getTrackedChangeAt,
|
|
1346
|
+
isTrackChangesEnabled,
|
|
1347
|
+
getTrackChangesStore,
|
|
1348
|
+
resolveTrackedChanges,
|
|
1349
|
+
TrackChanges,
|
|
1350
|
+
MENTION_REGEX,
|
|
1351
|
+
MARKDOWN_MENTION_SPLIT_REGEX,
|
|
1352
|
+
stripMarkdownCode,
|
|
1353
|
+
extractMentions,
|
|
1354
|
+
parseMentionSegments,
|
|
1355
|
+
preprocessMarkdownMentions,
|
|
1356
|
+
escapeHtml,
|
|
1357
|
+
plainTextToTipTapHtml,
|
|
1358
|
+
plainTextToSlice,
|
|
1359
|
+
htmlToPlainText,
|
|
1360
|
+
getEditorText,
|
|
1361
|
+
extractMentionsFromDoc,
|
|
1362
|
+
sanitizeHtml,
|
|
1363
|
+
preprocessMarkdownFileLinks,
|
|
1364
|
+
markdownToTipTapHtml,
|
|
1365
|
+
getEditorMarkdown
|
|
1366
|
+
};
|
|
1367
|
+
//# sourceMappingURL=chunk-3TYMNULT.js.map
|