@stll/folio-core 0.1.3 → 0.3.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/dist/ai-edits/clean-text.d.ts +21 -1
- package/dist/ai-edits/clean-text.js +69 -1
- package/dist/ai-edits/headless.d.ts +49 -22
- package/dist/ai-edits/headless.js +101 -77
- package/dist/ai-edits/index.d.ts +3 -1
- package/dist/ai-edits/index.js +3 -1
- package/dist/ai-edits/read.d.ts +43 -0
- package/dist/ai-edits/read.js +91 -0
- package/dist/docx/index.d.ts +2 -0
- package/dist/docx/index.js +2 -0
- package/dist/docx/parser.js +1 -1
- package/dist/docx/wrapTypes.d.ts +8 -1
- package/dist/docx/wrapTypes.js +0 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +9899 -0
- package/dist/i18n/messages/catalogs.gen.js +9831 -0
- package/dist/i18n/messages/messages.gen.d.ts +585 -0
- package/dist/i18n/messages/messages.gen.js +0 -0
- package/dist/i18n/messages.d.ts +13 -0
- package/dist/i18n/messages.js +39 -0
- package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
- package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
- package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
- package/dist/layout-bridge/headerFooterLayout.js +216 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
- package/dist/layout-bridge/sectionColumns.d.ts +11 -0
- package/dist/layout-bridge/sectionColumns.js +21 -0
- package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
- package/dist/layout-bridge/tableInsertHover.js +102 -0
- package/dist/layout-engine/index.d.ts +2 -1
- package/dist/layout-engine/index.js +2 -1
- package/dist/layout-engine/measure/measureParagraph.js +40 -5
- package/dist/layout-engine/pmPageIndex.d.ts +24 -0
- package/dist/layout-engine/pmPageIndex.js +37 -0
- package/dist/layout-painter/imageLayout.d.ts +101 -0
- package/dist/layout-painter/imageLayout.js +161 -0
- package/dist/layout-painter/renderPage.d.ts +16 -1
- package/dist/layout-painter/renderPage.js +28 -1
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/sdtBoundary.d.ts +24 -1
- package/dist/layout-painter/sdtBoundary.js +40 -1
- package/dist/managers/AutoSaveManager.d.ts +89 -0
- package/dist/managers/AutoSaveManager.js +279 -0
- package/dist/managers/ClipboardManager.d.ts +31 -0
- package/dist/managers/ClipboardManager.js +147 -0
- package/dist/paged-layout/rangeProjection.d.ts +23 -1
- package/dist/paged-layout/rangeProjection.js +34 -1
- package/dist/prosemirror/cellDragSelection.d.ts +34 -0
- package/dist/prosemirror/cellDragSelection.js +69 -0
- package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
- package/dist/prosemirror/commands/sectionBreak.js +54 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
- package/dist/prosemirror/commentIdAllocator.js +35 -0
- package/dist/prosemirror/commentOps.d.ts +35 -0
- package/dist/prosemirror/commentOps.js +104 -0
- package/dist/prosemirror/conversion/toProseDoc.js +29 -6
- package/dist/prosemirror/extensions/index.d.ts +3 -0
- package/dist/prosemirror/extensions/index.js +3 -0
- package/dist/prosemirror/imageCommit.d.ts +42 -0
- package/dist/prosemirror/imageCommit.js +118 -0
- package/dist/prosemirror/paraText.d.ts +26 -0
- package/dist/prosemirror/paraText.js +73 -0
- package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
- package/dist/prosemirror/plugins/templateDirectives.js +40 -1
- package/dist/prosemirror/queries.d.ts +46 -0
- package/dist/prosemirror/queries.js +74 -0
- package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
- package/dist/prosemirror/styles/styleResolver.js +34 -0
- package/dist/prosemirror/tableResize.d.ts +49 -0
- package/dist/prosemirror/tableResize.js +162 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
- package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
- package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
- package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
- package/dist/server.d.ts +2 -1
- package/dist/style-engine/index.d.ts +2 -2
- package/dist/style-engine/styleEngine.d.ts +14 -1
- package/dist/style-engine/styleEngine.js +15 -0
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +12 -1
- package/dist/utils/findVerticalScrollParent.d.ts +20 -0
- package/dist/utils/findVerticalScrollParent.js +30 -0
- package/dist/utils/fontResolver.d.ts +22 -3
- package/dist/utils/fontResolver.js +216 -31
- package/package.json +10 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
//#region src/prosemirror/utils/extractTrackedChanges.ts
|
|
2
|
+
const EMPTY_RESULT = {
|
|
3
|
+
entries: [],
|
|
4
|
+
commentToRevision: /* @__PURE__ */ new Map()
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Walk the PM doc and extract every tracked change as a flat list of
|
|
8
|
+
* `TrackedChangeEntry` plus a comment→revision overlap map. Adjacent
|
|
9
|
+
* inline marks coalesce by `(type, revisionId, author, date)`; a
|
|
10
|
+
* deletion immediately followed by an insertion (same author + same
|
|
11
|
+
* date) collapses into a single `replacement` entry; paragraph-mark
|
|
12
|
+
* cards (`paragraphMarkInsertion` / `paragraphMarkDeletion`) are
|
|
13
|
+
* hidden when an inline entry already covers their revision triple
|
|
14
|
+
* (one Accept clears every site of one conceptual change).
|
|
15
|
+
*
|
|
16
|
+
* Pure and deterministic. Returns `EMPTY_RESULT` on null state.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
function extractTrackedChanges(state) {
|
|
21
|
+
if (!state) return EMPTY_RESULT;
|
|
22
|
+
const { doc, schema } = state;
|
|
23
|
+
const insertionType = schema.marks["insertion"];
|
|
24
|
+
const deletionType = schema.marks["deletion"];
|
|
25
|
+
const commentType = schema.marks["comment"];
|
|
26
|
+
if (!insertionType && !deletionType) return EMPTY_RESULT;
|
|
27
|
+
const raw = [];
|
|
28
|
+
const commentToRevision = /* @__PURE__ */ new Map();
|
|
29
|
+
const commentMetadata = /* @__PURE__ */ new Map();
|
|
30
|
+
doc.descendants((node, pos) => {
|
|
31
|
+
if (node.type.name === "paragraph") {
|
|
32
|
+
const ins = node.attrs["pPrIns"];
|
|
33
|
+
const del = node.attrs["pPrDel"];
|
|
34
|
+
if (ins) raw.push({
|
|
35
|
+
type: "paragraphMarkInsertion",
|
|
36
|
+
text: node.textContent || "",
|
|
37
|
+
author: ins.author || "",
|
|
38
|
+
date: ins.date ?? void 0,
|
|
39
|
+
from: pos,
|
|
40
|
+
to: pos + node.nodeSize,
|
|
41
|
+
revisionId: ins.revisionId
|
|
42
|
+
});
|
|
43
|
+
if (del) raw.push({
|
|
44
|
+
type: "paragraphMarkDeletion",
|
|
45
|
+
text: node.textContent || "",
|
|
46
|
+
author: del.author || "",
|
|
47
|
+
date: del.date ?? void 0,
|
|
48
|
+
from: pos,
|
|
49
|
+
to: pos + node.nodeSize,
|
|
50
|
+
revisionId: del.revisionId
|
|
51
|
+
});
|
|
52
|
+
const pPrChange = node.attrs["pPrChange"];
|
|
53
|
+
if (Array.isArray(pPrChange)) for (const entry of pPrChange) {
|
|
54
|
+
if (!entry?.info || typeof entry.info.id !== "number") continue;
|
|
55
|
+
raw.push({
|
|
56
|
+
type: "paragraphPropertiesChanged",
|
|
57
|
+
text: node.textContent || "",
|
|
58
|
+
author: entry.info.author || "",
|
|
59
|
+
date: entry.info.date ?? void 0,
|
|
60
|
+
from: pos,
|
|
61
|
+
to: pos + node.nodeSize,
|
|
62
|
+
revisionId: entry.info.id
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (node.type.name === "tableRow") {
|
|
67
|
+
const trIns = node.attrs["trIns"];
|
|
68
|
+
const trDel = node.attrs["trDel"];
|
|
69
|
+
if (trIns) raw.push({
|
|
70
|
+
type: "rowInserted",
|
|
71
|
+
text: node.textContent || "",
|
|
72
|
+
author: trIns.author || "",
|
|
73
|
+
date: trIns.date ?? void 0,
|
|
74
|
+
from: pos,
|
|
75
|
+
to: pos + node.nodeSize,
|
|
76
|
+
revisionId: trIns.revisionId
|
|
77
|
+
});
|
|
78
|
+
if (trDel) raw.push({
|
|
79
|
+
type: "rowDeleted",
|
|
80
|
+
text: node.textContent || "",
|
|
81
|
+
author: trDel.author || "",
|
|
82
|
+
date: trDel.date ?? void 0,
|
|
83
|
+
from: pos,
|
|
84
|
+
to: pos + node.nodeSize,
|
|
85
|
+
revisionId: trDel.revisionId
|
|
86
|
+
});
|
|
87
|
+
const trPrChange = node.attrs["trPrChange"];
|
|
88
|
+
if (Array.isArray(trPrChange)) for (const entry of trPrChange) {
|
|
89
|
+
if (!entry?.info || typeof entry.info.id !== "number") continue;
|
|
90
|
+
raw.push({
|
|
91
|
+
type: "rowPropertiesChanged",
|
|
92
|
+
text: node.textContent || "",
|
|
93
|
+
author: entry.info.author || "",
|
|
94
|
+
date: entry.info.date ?? void 0,
|
|
95
|
+
from: pos,
|
|
96
|
+
to: pos + node.nodeSize,
|
|
97
|
+
revisionId: entry.info.id
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
|
|
102
|
+
const cellMarker = node.attrs["cellMarker"];
|
|
103
|
+
if (cellMarker?.info && typeof cellMarker.info.revisionId === "number") {
|
|
104
|
+
const resolvedType = {
|
|
105
|
+
ins: "cellInserted",
|
|
106
|
+
del: "cellDeleted",
|
|
107
|
+
merge: "cellMerged"
|
|
108
|
+
}[cellMarker.kind];
|
|
109
|
+
if (resolvedType) raw.push({
|
|
110
|
+
type: resolvedType,
|
|
111
|
+
text: node.textContent || "",
|
|
112
|
+
author: cellMarker.info.author || "",
|
|
113
|
+
date: cellMarker.info.date ?? void 0,
|
|
114
|
+
from: pos,
|
|
115
|
+
to: pos + node.nodeSize,
|
|
116
|
+
revisionId: cellMarker.info.revisionId
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const tcPrChange = node.attrs["tcPrChange"];
|
|
120
|
+
if (Array.isArray(tcPrChange)) for (const entry of tcPrChange) {
|
|
121
|
+
if (!entry?.info || typeof entry.info.id !== "number") continue;
|
|
122
|
+
raw.push({
|
|
123
|
+
type: "cellPropertiesChanged",
|
|
124
|
+
text: node.textContent || "",
|
|
125
|
+
author: entry.info.author || "",
|
|
126
|
+
date: entry.info.date ?? void 0,
|
|
127
|
+
from: pos,
|
|
128
|
+
to: pos + node.nodeSize,
|
|
129
|
+
revisionId: entry.info.id
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (node.type.name === "table") {
|
|
134
|
+
const tblPrChange = node.attrs["tblPrChange"];
|
|
135
|
+
if (Array.isArray(tblPrChange)) for (const entry of tblPrChange) {
|
|
136
|
+
if (!entry?.info || typeof entry.info.id !== "number") continue;
|
|
137
|
+
raw.push({
|
|
138
|
+
type: "tablePropertiesChanged",
|
|
139
|
+
text: "",
|
|
140
|
+
author: entry.info.author || "",
|
|
141
|
+
date: entry.info.date ?? void 0,
|
|
142
|
+
from: pos,
|
|
143
|
+
to: pos + node.nodeSize,
|
|
144
|
+
revisionId: entry.info.id
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const firstRow = node.firstChild;
|
|
148
|
+
const firstIns = firstRow?.attrs["trIns"];
|
|
149
|
+
const firstDel = firstRow?.attrs["trDel"];
|
|
150
|
+
let sharedAttr = null;
|
|
151
|
+
if (firstIns) sharedAttr = "trIns";
|
|
152
|
+
else if (firstDel) sharedAttr = "trDel";
|
|
153
|
+
if (sharedAttr) {
|
|
154
|
+
const sharedRev = firstIns ?? firstDel;
|
|
155
|
+
let allShare = true;
|
|
156
|
+
const rowRevIds = [];
|
|
157
|
+
node.forEach((row) => {
|
|
158
|
+
if (row.type.name !== "tableRow") {
|
|
159
|
+
allShare = false;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const v = row.attrs[sharedAttr];
|
|
163
|
+
if (!v || (v.author ?? "") !== (sharedRev.author ?? "") || (v.date ?? null) !== (sharedRev.date ?? null)) {
|
|
164
|
+
allShare = false;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
rowRevIds.push(v.revisionId);
|
|
168
|
+
});
|
|
169
|
+
if (allShare) {
|
|
170
|
+
let visibleText = "";
|
|
171
|
+
if (deletionType) node.descendants((child) => {
|
|
172
|
+
if (child.isText && !child.marks.some((m) => m.type === deletionType)) visibleText += child.text || "";
|
|
173
|
+
});
|
|
174
|
+
else visibleText = node.textContent || "";
|
|
175
|
+
const primaryId = sharedRev.revisionId;
|
|
176
|
+
const extraIds = rowRevIds.filter((id, idx) => id !== primaryId && rowRevIds.indexOf(id) === idx);
|
|
177
|
+
raw.push({
|
|
178
|
+
type: sharedAttr === "trIns" ? "tableInserted" : "tableDeleted",
|
|
179
|
+
text: visibleText,
|
|
180
|
+
author: sharedRev.author || "",
|
|
181
|
+
date: sharedRev.date ?? void 0,
|
|
182
|
+
from: pos,
|
|
183
|
+
to: pos + node.nodeSize,
|
|
184
|
+
revisionId: primaryId,
|
|
185
|
+
...extraIds.length > 0 ? { coalescedRevisionIds: extraIds } : {}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!node.isInline) return;
|
|
191
|
+
let inlineText;
|
|
192
|
+
if (node.isText) inlineText = node.text || "";
|
|
193
|
+
else if (node.type.name === "image") inlineText = node.attrs["alt"] || "image";
|
|
194
|
+
else inlineText = node.type.name;
|
|
195
|
+
let tcMark = null;
|
|
196
|
+
for (const mark of node.marks) if (mark.type === insertionType || mark.type === deletionType) {
|
|
197
|
+
raw.push({
|
|
198
|
+
type: mark.type === insertionType ? "insertion" : "deletion",
|
|
199
|
+
text: inlineText,
|
|
200
|
+
author: mark.attrs["author"] || "",
|
|
201
|
+
date: mark.attrs["date"],
|
|
202
|
+
from: pos,
|
|
203
|
+
to: pos + node.nodeSize,
|
|
204
|
+
revisionId: mark.attrs["revisionId"]
|
|
205
|
+
});
|
|
206
|
+
tcMark = mark;
|
|
207
|
+
}
|
|
208
|
+
if (commentType) {
|
|
209
|
+
const commentMark = node.marks.find((m) => m.type === commentType);
|
|
210
|
+
if (commentMark) {
|
|
211
|
+
const cid = commentMark.attrs["commentId"];
|
|
212
|
+
let meta = commentMetadata.get(cid);
|
|
213
|
+
if (!meta) {
|
|
214
|
+
meta = {
|
|
215
|
+
revisions: /* @__PURE__ */ new Set(),
|
|
216
|
+
hasCleanText: false
|
|
217
|
+
};
|
|
218
|
+
commentMetadata.set(cid, meta);
|
|
219
|
+
}
|
|
220
|
+
if (tcMark) meta.revisions.add(tcMark.attrs["revisionId"]);
|
|
221
|
+
else meta.hasCleanText = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
for (const [cid, meta] of commentMetadata.entries()) if (!meta.hasCleanText && meta.revisions.size === 1) {
|
|
226
|
+
const rid = meta.revisions.values().next().value;
|
|
227
|
+
commentToRevision.set(cid, rid);
|
|
228
|
+
}
|
|
229
|
+
const STRUCTURAL_PRIORITY = {
|
|
230
|
+
tableInserted: 6,
|
|
231
|
+
tableDeleted: 6,
|
|
232
|
+
tablePropertiesChanged: 5,
|
|
233
|
+
rowInserted: 4,
|
|
234
|
+
rowDeleted: 4,
|
|
235
|
+
rowPropertiesChanged: 4,
|
|
236
|
+
cellInserted: 3,
|
|
237
|
+
cellDeleted: 3,
|
|
238
|
+
cellMerged: 3,
|
|
239
|
+
cellPropertiesChanged: 3,
|
|
240
|
+
paragraphMarkInsertion: 2,
|
|
241
|
+
paragraphMarkDeletion: 2,
|
|
242
|
+
paragraphPropertiesChanged: 2
|
|
243
|
+
};
|
|
244
|
+
const isStructuralType = (t) => t in STRUCTURAL_PRIORITY;
|
|
245
|
+
const slotByKey = /* @__PURE__ */ new Map();
|
|
246
|
+
const ordered = [];
|
|
247
|
+
const mergeIds = (survivor, dropped) => {
|
|
248
|
+
const ids = new Set(survivor.coalescedRevisionIds ?? []);
|
|
249
|
+
for (const id of dropped.coalescedRevisionIds ?? []) ids.add(id);
|
|
250
|
+
ids.add(dropped.revisionId);
|
|
251
|
+
ids.delete(survivor.revisionId);
|
|
252
|
+
return [...ids];
|
|
253
|
+
};
|
|
254
|
+
for (const entry of raw) {
|
|
255
|
+
if (!isStructuralType(entry.type)) {
|
|
256
|
+
ordered.push(entry);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const key = `${entry.author}|${entry.date ?? ""}`;
|
|
260
|
+
const slot = slotByKey.get(key);
|
|
261
|
+
if (slot === void 0) {
|
|
262
|
+
slotByKey.set(key, ordered.push(entry) - 1);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const existing = ordered[slot];
|
|
266
|
+
if ((STRUCTURAL_PRIORITY[entry.type] ?? 0) > (STRUCTURAL_PRIORITY[existing.type] ?? 0)) ordered[slot] = {
|
|
267
|
+
...entry,
|
|
268
|
+
coalescedRevisionIds: mergeIds(entry, existing)
|
|
269
|
+
};
|
|
270
|
+
else ordered[slot] = {
|
|
271
|
+
...existing,
|
|
272
|
+
coalescedRevisionIds: mergeIds(existing, entry)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const inlineGroups = /* @__PURE__ */ new Map();
|
|
276
|
+
const merged = [];
|
|
277
|
+
for (const entry of ordered) {
|
|
278
|
+
if (!(entry.type === "insertion" || entry.type === "deletion")) {
|
|
279
|
+
merged.push({ ...entry });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const key = `${entry.type}|${entry.author}|${entry.date ?? ""}`;
|
|
283
|
+
const group = inlineGroups.get(key);
|
|
284
|
+
if (group) {
|
|
285
|
+
const sep = group.to === entry.from ? "" : " ";
|
|
286
|
+
group.text += sep + entry.text;
|
|
287
|
+
group.to = entry.to;
|
|
288
|
+
if (entry.revisionId !== group.revisionId) {
|
|
289
|
+
const ids = new Set(group.coalescedRevisionIds ?? []);
|
|
290
|
+
for (const id of entry.coalescedRevisionIds ?? []) ids.add(id);
|
|
291
|
+
ids.add(entry.revisionId);
|
|
292
|
+
ids.delete(group.revisionId);
|
|
293
|
+
group.coalescedRevisionIds = ids.size > 0 ? [...ids] : void 0;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
const copy = { ...entry };
|
|
297
|
+
inlineGroups.set(key, copy);
|
|
298
|
+
merged.push(copy);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const final = [];
|
|
302
|
+
for (let i = 0; i < merged.length; i++) {
|
|
303
|
+
const curr = merged[i];
|
|
304
|
+
const next = merged[i + 1];
|
|
305
|
+
if (curr.type === "deletion" && next && next.type === "insertion" && curr.author === next.author && curr.date === next.date && curr.to === next.from) {
|
|
306
|
+
final.push({
|
|
307
|
+
type: "replacement",
|
|
308
|
+
text: next.text,
|
|
309
|
+
deletedText: curr.text,
|
|
310
|
+
author: curr.author,
|
|
311
|
+
date: curr.date,
|
|
312
|
+
from: curr.from,
|
|
313
|
+
to: next.to,
|
|
314
|
+
revisionId: curr.revisionId,
|
|
315
|
+
insertionRevisionId: next.revisionId
|
|
316
|
+
});
|
|
317
|
+
i++;
|
|
318
|
+
} else final.push(curr);
|
|
319
|
+
}
|
|
320
|
+
const inlineByKey = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const e of final) if (e.type === "insertion" || e.type === "deletion" || e.type === "replacement") {
|
|
322
|
+
const k = `${e.author}|${e.date ?? ""}`;
|
|
323
|
+
if (!inlineByKey.has(k)) inlineByKey.set(k, e);
|
|
324
|
+
}
|
|
325
|
+
for (const e of final) {
|
|
326
|
+
if (e.type !== "paragraphMarkInsertion" && e.type !== "paragraphMarkDeletion") continue;
|
|
327
|
+
const survivor = inlineByKey.get(`${e.author}|${e.date ?? ""}`);
|
|
328
|
+
if (!survivor) continue;
|
|
329
|
+
const ids = new Set(survivor.coalescedRevisionIds ?? []);
|
|
330
|
+
for (const id of e.coalescedRevisionIds ?? []) ids.add(id);
|
|
331
|
+
ids.add(e.revisionId);
|
|
332
|
+
ids.delete(survivor.revisionId);
|
|
333
|
+
if (survivor.type === "replacement" && survivor.insertionRevisionId != null) ids.delete(survivor.insertionRevisionId);
|
|
334
|
+
survivor.coalescedRevisionIds = ids.size > 0 ? [...ids] : void 0;
|
|
335
|
+
}
|
|
336
|
+
const emptyTableEntries = /* @__PURE__ */ new Set();
|
|
337
|
+
const tableByKey = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const e of final) {
|
|
339
|
+
if (e.type !== "tableInserted" && e.type !== "tableDeleted") continue;
|
|
340
|
+
const key = `${e.author}|${e.date ?? ""}`;
|
|
341
|
+
if (e.text.trim().length > 0) continue;
|
|
342
|
+
emptyTableEntries.add(e);
|
|
343
|
+
if (!tableByKey.has(key)) tableByKey.set(key, e);
|
|
344
|
+
}
|
|
345
|
+
for (const e of final) {
|
|
346
|
+
if (e.type !== "paragraphMarkInsertion" && e.type !== "paragraphMarkDeletion") continue;
|
|
347
|
+
const survivor = tableByKey.get(`${e.author}|${e.date ?? ""}`);
|
|
348
|
+
if (!survivor) continue;
|
|
349
|
+
const ids = new Set(survivor.coalescedRevisionIds ?? []);
|
|
350
|
+
for (const id of e.coalescedRevisionIds ?? []) ids.add(id);
|
|
351
|
+
ids.add(e.revisionId);
|
|
352
|
+
ids.delete(survivor.revisionId);
|
|
353
|
+
survivor.coalescedRevisionIds = ids.size > 0 ? [...ids] : void 0;
|
|
354
|
+
}
|
|
355
|
+
const inlineHosts = final.filter((e) => e.type === "insertion" || e.type === "replacement");
|
|
356
|
+
const foldedPropChanges = /* @__PURE__ */ new Set();
|
|
357
|
+
for (const e of final) {
|
|
358
|
+
if (e.type !== "paragraphPropertiesChanged") continue;
|
|
359
|
+
const host = inlineHosts.find((c) => c.author === e.author && c.from < e.to && e.from < c.to);
|
|
360
|
+
if (!host) continue;
|
|
361
|
+
const ids = new Set(mergeIds(host, e));
|
|
362
|
+
if (host.type === "replacement" && host.insertionRevisionId != null) ids.delete(host.insertionRevisionId);
|
|
363
|
+
host.coalescedRevisionIds = ids.size > 0 ? [...ids] : void 0;
|
|
364
|
+
foldedPropChanges.add(e);
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
entries: final.filter((e) => {
|
|
368
|
+
const key = `${e.author}|${e.date ?? ""}`;
|
|
369
|
+
if (e.type === "tableInserted" || e.type === "tableDeleted") return emptyTableEntries.has(e);
|
|
370
|
+
if (e.type === "paragraphMarkInsertion" || e.type === "paragraphMarkDeletion") return !inlineByKey.has(key) && !tableByKey.has(key);
|
|
371
|
+
if (e.type === "paragraphPropertiesChanged") return !foldedPropChanges.has(e);
|
|
372
|
+
if (e.type === "insertion" || e.type === "deletion" || e.type === "replacement") return !tableByKey.has(key);
|
|
373
|
+
return true;
|
|
374
|
+
}),
|
|
375
|
+
commentToRevision
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
export { extractTrackedChanges };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EditorView } from "prosemirror-view";
|
|
2
|
+
|
|
3
|
+
//#region src/prosemirror/utils/visualLineNavigation.d.ts
|
|
4
|
+
/** @internal */
|
|
5
|
+
type VisualLineState = {
|
|
6
|
+
stickyX: number | null;
|
|
7
|
+
lastVisualLineIndex: number;
|
|
8
|
+
};
|
|
9
|
+
/** @internal */
|
|
10
|
+
declare function createVisualLineState(): VisualLineState;
|
|
11
|
+
/** @internal */
|
|
12
|
+
declare function getCaretClientX(container: HTMLElement, pmPos: number): number | null;
|
|
13
|
+
/** @internal */
|
|
14
|
+
declare function findLineElementAtPosition(container: HTMLElement, pmPos: number): HTMLElement | null;
|
|
15
|
+
/** @internal */
|
|
16
|
+
declare function findPositionOnLineAtClientX(lineEl: HTMLElement, clientX: number): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* Handle PM ArrowUp / ArrowDown with visual-line awareness + sticky
|
|
19
|
+
* X. Returns true if the event was handled and PM should not run
|
|
20
|
+
* its default behaviour. Mutates `state` so consecutive presses
|
|
21
|
+
* keep the same sticky X.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
declare function handleVisualLineKeyDown(state: VisualLineState, view: EditorView, event: KeyboardEvent, container: HTMLElement | null): boolean;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { VisualLineState, createVisualLineState, findLineElementAtPosition, findPositionOnLineAtClientX, getCaretClientX, handleVisualLineKeyDown };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { findVerticalScrollParent } from "../../utils/findVerticalScrollParent.js";
|
|
2
|
+
import { TextSelection } from "prosemirror-state";
|
|
3
|
+
//#region src/prosemirror/utils/visualLineNavigation.ts
|
|
4
|
+
/**
|
|
5
|
+
* Visual line navigation helpers — implements Word/Google-Docs-style
|
|
6
|
+
* ArrowUp / ArrowDown with sticky X across visual lines (not just
|
|
7
|
+
* paragraphs). Lifted from packages/react/src/paged-editor/
|
|
8
|
+
* useVisualLineNavigation.ts so both adapters share the algorithm.
|
|
9
|
+
*
|
|
10
|
+
* Frontend-agnostic: takes a `getContainer: () => HTMLElement | null`
|
|
11
|
+
* callback and a mutable sticky-state object, returns the same
|
|
12
|
+
* function quartet React's hook returns.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
const CONTENT_LINE_SELECTOR = ".layout-page-content .layout-line";
|
|
18
|
+
/** @internal */
|
|
19
|
+
function createVisualLineState() {
|
|
20
|
+
return {
|
|
21
|
+
stickyX: null,
|
|
22
|
+
lastVisualLineIndex: -1
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function scrollIntoViewIfNeeded(el) {
|
|
26
|
+
const container = findVerticalScrollParent(el);
|
|
27
|
+
if (!container) return;
|
|
28
|
+
const elRect = el.getBoundingClientRect();
|
|
29
|
+
const containerRect = container.getBoundingClientRect();
|
|
30
|
+
const margin = 40;
|
|
31
|
+
if (elRect.bottom > containerRect.bottom - margin) container.scrollTop += elRect.bottom - containerRect.bottom + margin;
|
|
32
|
+
else if (elRect.top < containerRect.top + margin) container.scrollTop -= containerRect.top - elRect.top + margin;
|
|
33
|
+
}
|
|
34
|
+
/** @internal */
|
|
35
|
+
function getCaretClientX(container, pmPos) {
|
|
36
|
+
const spans = container.querySelectorAll("span[data-pm-start][data-pm-end]");
|
|
37
|
+
for (const span of Array.from(spans)) {
|
|
38
|
+
const spanEl = span;
|
|
39
|
+
const pmStart = Number(spanEl.dataset["pmStart"]);
|
|
40
|
+
const pmEnd = Number(spanEl.dataset["pmEnd"]);
|
|
41
|
+
if (spanEl.classList.contains("layout-run-tab")) {
|
|
42
|
+
if (pmPos >= pmStart && pmPos < pmEnd) return spanEl.getBoundingClientRect().left;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (pmPos >= pmStart && pmPos <= pmEnd && span.firstChild?.nodeType === Node.TEXT_NODE) {
|
|
46
|
+
const textNode = span.firstChild;
|
|
47
|
+
const charIndex = Math.min(pmPos - pmStart, textNode.length);
|
|
48
|
+
const ownerDoc = spanEl.ownerDocument;
|
|
49
|
+
if (!ownerDoc) continue;
|
|
50
|
+
const range = ownerDoc.createRange();
|
|
51
|
+
range.setStart(textNode, charIndex);
|
|
52
|
+
range.setEnd(textNode, charIndex);
|
|
53
|
+
return range.getBoundingClientRect().left;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const emptyRuns = container.querySelectorAll(".layout-empty-run");
|
|
57
|
+
for (const emptyRun of Array.from(emptyRuns)) {
|
|
58
|
+
const paragraph = emptyRun.closest(".layout-paragraph");
|
|
59
|
+
if (!paragraph) continue;
|
|
60
|
+
const pmStart = Number(paragraph.dataset["pmStart"]);
|
|
61
|
+
const pmEnd = Number(paragraph.dataset["pmEnd"]);
|
|
62
|
+
if (pmPos >= pmStart && pmPos <= pmEnd) return emptyRun.getBoundingClientRect().left;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** @internal */
|
|
67
|
+
function findLineElementAtPosition(container, pmPos) {
|
|
68
|
+
const allLines = container.querySelectorAll(CONTENT_LINE_SELECTOR);
|
|
69
|
+
for (const line of Array.from(allLines)) {
|
|
70
|
+
const lineEl = line;
|
|
71
|
+
const spans = lineEl.querySelectorAll("span[data-pm-start][data-pm-end]");
|
|
72
|
+
for (const span of Array.from(spans)) {
|
|
73
|
+
const s = span;
|
|
74
|
+
const start = Number(s.dataset["pmStart"]);
|
|
75
|
+
const end = Number(s.dataset["pmEnd"]);
|
|
76
|
+
if (pmPos >= start && pmPos <= end) return lineEl;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const line of Array.from(allLines)) {
|
|
80
|
+
const lineEl = line;
|
|
81
|
+
const paragraph = lineEl.closest(".layout-paragraph");
|
|
82
|
+
if (!paragraph) continue;
|
|
83
|
+
const pStart = Number(paragraph.dataset["pmStart"]);
|
|
84
|
+
const pEnd = Number(paragraph.dataset["pmEnd"]);
|
|
85
|
+
if (pmPos >= pStart && pmPos <= pEnd) {
|
|
86
|
+
if (paragraph.querySelector(".layout-line") === lineEl) return lineEl;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/** @internal */
|
|
92
|
+
function findPositionOnLineAtClientX(lineEl, clientX) {
|
|
93
|
+
const spans = lineEl.querySelectorAll("span[data-pm-start][data-pm-end]");
|
|
94
|
+
if (spans.length === 0) {
|
|
95
|
+
const paragraph = lineEl.closest(".layout-paragraph");
|
|
96
|
+
if (paragraph?.dataset["pmStart"]) return Number(paragraph.dataset["pmStart"]) + 1;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
for (const span of Array.from(spans)) {
|
|
100
|
+
const spanEl = span;
|
|
101
|
+
const rect = spanEl.getBoundingClientRect();
|
|
102
|
+
const pmStart = Number(spanEl.dataset["pmStart"]);
|
|
103
|
+
const pmEnd = Number(spanEl.dataset["pmEnd"]);
|
|
104
|
+
if (spanEl.classList.contains("layout-run-tab")) {
|
|
105
|
+
if (clientX >= rect.left && clientX <= rect.right) return clientX < (rect.left + rect.right) / 2 ? pmStart : pmEnd;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (clientX >= rect.left && clientX <= rect.right) {
|
|
109
|
+
const textNode = spanEl.firstChild;
|
|
110
|
+
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return pmStart;
|
|
111
|
+
const text = textNode;
|
|
112
|
+
const ownerDoc = spanEl.ownerDocument;
|
|
113
|
+
if (!ownerDoc) return pmStart;
|
|
114
|
+
let lo = 0;
|
|
115
|
+
let hi = text.length;
|
|
116
|
+
while (lo < hi) {
|
|
117
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
118
|
+
const r = ownerDoc.createRange();
|
|
119
|
+
r.setStart(text, mid);
|
|
120
|
+
r.setEnd(text, mid);
|
|
121
|
+
if (clientX < r.getBoundingClientRect().left) hi = mid;
|
|
122
|
+
else lo = mid + 1;
|
|
123
|
+
}
|
|
124
|
+
if (lo > 0 && lo <= text.length) {
|
|
125
|
+
const r = ownerDoc.createRange();
|
|
126
|
+
r.setStart(text, lo - 1);
|
|
127
|
+
r.setEnd(text, lo - 1);
|
|
128
|
+
const leftX = r.getBoundingClientRect().left;
|
|
129
|
+
r.setStart(text, Math.min(lo, text.length));
|
|
130
|
+
r.setEnd(text, Math.min(lo, text.length));
|
|
131
|
+
const rightX = r.getBoundingClientRect().left;
|
|
132
|
+
if (Math.abs(clientX - leftX) < Math.abs(clientX - rightX)) return pmStart + (lo - 1);
|
|
133
|
+
}
|
|
134
|
+
return pmStart + Math.min(lo, pmEnd - pmStart);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let closestSpan = null;
|
|
138
|
+
let closestDist = Infinity;
|
|
139
|
+
for (const span of Array.from(spans)) {
|
|
140
|
+
const spanEl = span;
|
|
141
|
+
const rect = spanEl.getBoundingClientRect();
|
|
142
|
+
const dist = clientX < rect.left ? rect.left - clientX : clientX - rect.right;
|
|
143
|
+
if (dist < closestDist) {
|
|
144
|
+
closestDist = dist;
|
|
145
|
+
closestSpan = spanEl;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!closestSpan) return null;
|
|
149
|
+
return clientX < closestSpan.getBoundingClientRect().left ? Number(closestSpan.dataset["pmStart"]) : Number(closestSpan.dataset["pmEnd"]);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Handle PM ArrowUp / ArrowDown with visual-line awareness + sticky
|
|
153
|
+
* X. Returns true if the event was handled and PM should not run
|
|
154
|
+
* its default behaviour. Mutates `state` so consecutive presses
|
|
155
|
+
* keep the same sticky X.
|
|
156
|
+
*
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
function handleVisualLineKeyDown(state, view, event, container) {
|
|
160
|
+
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") {
|
|
161
|
+
if ([
|
|
162
|
+
"ArrowLeft",
|
|
163
|
+
"ArrowRight",
|
|
164
|
+
"Home",
|
|
165
|
+
"End"
|
|
166
|
+
].includes(event.key) || event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
167
|
+
state.stickyX = null;
|
|
168
|
+
state.lastVisualLineIndex = -1;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (event.ctrlKey || event.metaKey) {
|
|
173
|
+
state.stickyX = null;
|
|
174
|
+
state.lastVisualLineIndex = -1;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (!container) return false;
|
|
178
|
+
const allLines = Array.from(container.querySelectorAll(CONTENT_LINE_SELECTOR));
|
|
179
|
+
if (allLines.length === 0) return false;
|
|
180
|
+
const { from, anchor } = view.state.selection;
|
|
181
|
+
if (state.stickyX === null) {
|
|
182
|
+
const clientX = getCaretClientX(container, from);
|
|
183
|
+
if (clientX === null) return false;
|
|
184
|
+
state.stickyX = clientX;
|
|
185
|
+
}
|
|
186
|
+
let currentIndex;
|
|
187
|
+
if (state.lastVisualLineIndex >= 0 && state.lastVisualLineIndex < allLines.length) currentIndex = state.lastVisualLineIndex;
|
|
188
|
+
else {
|
|
189
|
+
const currentLine = findLineElementAtPosition(container, from);
|
|
190
|
+
if (!currentLine) return false;
|
|
191
|
+
currentIndex = allLines.indexOf(currentLine);
|
|
192
|
+
if (currentIndex === -1) return false;
|
|
193
|
+
}
|
|
194
|
+
const targetIndex = event.key === "ArrowUp" ? currentIndex - 1 : currentIndex + 1;
|
|
195
|
+
if (targetIndex < 0 || targetIndex >= allLines.length) {
|
|
196
|
+
state.lastVisualLineIndex = -1;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const targetLine = allLines[targetIndex];
|
|
200
|
+
const newPos = findPositionOnLineAtClientX(targetLine, state.stickyX);
|
|
201
|
+
if (newPos === null) return false;
|
|
202
|
+
state.lastVisualLineIndex = targetIndex;
|
|
203
|
+
const { state: pmState, dispatch } = view;
|
|
204
|
+
const clampedPos = Math.max(0, Math.min(newPos, pmState.doc.content.size));
|
|
205
|
+
try {
|
|
206
|
+
const sel = event.shiftKey ? TextSelection.create(pmState.doc, anchor, clampedPos) : TextSelection.create(pmState.doc, clampedPos);
|
|
207
|
+
dispatch(pmState.tr.setSelection(sel));
|
|
208
|
+
} catch {
|
|
209
|
+
const $newPos = pmState.doc.resolve(clampedPos);
|
|
210
|
+
const sel = event.shiftKey ? TextSelection.between(pmState.doc.resolve(anchor), $newPos) : TextSelection.near($newPos);
|
|
211
|
+
dispatch(pmState.tr.setSelection(sel));
|
|
212
|
+
}
|
|
213
|
+
scrollIntoViewIfNeeded(targetLine);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
export { createVisualLineState, findLineElementAtPosition, findPositionOnLineAtClientX, getCaretClientX, handleVisualLineKeyDown };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./ai-edits/types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { FolioReviewChange, FolioReviewChangeKind } from "./ai-edits/read.js";
|
|
3
|
+
import { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioReviewChangeFilter, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js";
|
|
3
4
|
import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
|
|
4
5
|
import { CreateCommentReplyInput, replyToComment } from "./docx/replyToComment.js";
|
|
5
6
|
import { createDocx } from "./docx/rezip.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { ResolvedParagraphStyle } from "../prosemirror/styles/styleResolver.js";
|
|
1
|
+
import { ResolvedParagraphStyle, TableCellParagraphSpacingOverlay } from "../prosemirror/styles/styleResolver.js";
|
|
2
2
|
import { StyleEngine, StyleEngineCacheStats, StyleEngineOptions, createStyleEngine } from "./styleEngine.js";
|
|
3
|
-
export { type ResolvedParagraphStyle, type StyleEngine, type StyleEngineCacheStats, type StyleEngineOptions, createStyleEngine };
|
|
3
|
+
export { type ResolvedParagraphStyle, type StyleEngine, type StyleEngineCacheStats, type StyleEngineOptions, type TableCellParagraphSpacingOverlay, createStyleEngine };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { document_d_exports } from "../types/document.js";
|
|
2
|
-
import { ResolvedParagraphStyle } from "../prosemirror/styles/styleResolver.js";
|
|
2
|
+
import { ResolvedParagraphStyle, TableCellParagraphSpacingOverlay } from "../prosemirror/styles/styleResolver.js";
|
|
3
3
|
|
|
4
4
|
//#region src/style-engine/styleEngine.d.ts
|
|
5
5
|
/** Engine construction options. */
|
|
@@ -41,6 +41,19 @@ type StyleEngine = {
|
|
|
41
41
|
* Returned object is cached and must not be mutated.
|
|
42
42
|
*/
|
|
43
43
|
resolveParagraphStyle: (styleId: string | undefined | null) => ResolvedParagraphStyle;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the paragraph cascade for a paragraph inside a table cell,
|
|
46
|
+
* layering the enclosing table style's paragraph-spacing fields between
|
|
47
|
+
* docDefaults and the paragraph's own style chain — see
|
|
48
|
+
* {@link StyleResolver.resolveParagraphStyleInTable}.
|
|
49
|
+
*
|
|
50
|
+
* Pass the *same* `tableParagraphOverlay` object reference for every
|
|
51
|
+
* paragraph governed by the same table style region (folio's table
|
|
52
|
+
* conversion already computes it once per region) — the cache below keys
|
|
53
|
+
* on object identity, so a fresh object per call defeats memoization.
|
|
54
|
+
* Returned object is cached and must not be mutated.
|
|
55
|
+
*/
|
|
56
|
+
resolveParagraphStyleInTable: (styleId: string | undefined | null, tableParagraphOverlay: TableCellParagraphSpacingOverlay | undefined) => ResolvedParagraphStyle;
|
|
44
57
|
/**
|
|
45
58
|
* Resolve a run/character style with docDefaults applied.
|
|
46
59
|
* Returns `undefined` when the cascade yields nothing.
|