@stll/folio-core 0.54.0 → 0.54.2
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/apply.d.ts +1 -1
- package/dist/ai-edits/apply.js +43 -31
- package/dist/ai-edits/headless.d.ts +1 -2
- package/dist/ai-edits/headless.js +12 -27
- package/dist/ai-edits/read.d.ts +1 -0
- package/dist/ai-edits/read.js +7 -2
- package/dist/ai-edits/table-mutation-plan.d.ts +2 -1
- package/dist/ai-edits/table-mutation-plan.js +39 -1
- package/dist/ai-suggestions/apply.d.ts +1 -1
- package/dist/ai-suggestions/apply.js +3 -2
- package/dist/compare/compare.js +2 -1
- package/dist/docx/bookmarkIds.d.ts +11 -0
- package/dist/docx/bookmarkIds.js +33 -0
- package/dist/docx/documentParser.d.ts +14 -1
- package/dist/docx/documentParser.js +17 -4
- package/dist/docx/drawingIdNormalization.js +23 -8
- package/dist/docx/ensureParaIds.d.ts +5 -0
- package/dist/docx/ensureParaIds.js +10 -2
- package/dist/docx/listNumberingInstances.js +11 -9
- package/dist/docx/noteIds.d.ts +4 -0
- package/dist/docx/noteIds.js +13 -0
- package/dist/docx/numberingIds.d.ts +14 -0
- package/dist/docx/numberingIds.js +16 -0
- package/dist/docx/numericIdAllocator.d.ts +12 -0
- package/dist/docx/numericIdAllocator.js +33 -0
- package/dist/docx/numericIdNormalization.d.ts +16 -0
- package/dist/docx/numericIdNormalization.js +174 -0
- package/dist/docx/paragraphPropertySource.js +1 -1
- package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
- package/dist/docx/parser.js +17 -5
- package/dist/docx/rasterMime.d.ts +64 -0
- package/dist/docx/rasterMime.js +116 -0
- package/dist/docx/replyToComment.js +13 -6
- package/dist/docx/rezip.js +14 -7
- package/dist/docx/selectiveSave.js +10 -0
- package/dist/docx/serializer/partNamespaces.js +4 -2
- package/dist/docx/serializer/runSerializer.js +5 -4
- package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
- package/dist/docx/server/build.js +14 -3
- package/dist/docx/server/createBilingualDocument.js +10 -9
- package/dist/docx/streamingXmlParser.d.ts +10 -1
- package/dist/docx/streamingXmlParser.js +88 -19
- package/dist/docx/unzip.d.ts +3 -1
- package/dist/docx/unzip.js +36 -16
- package/dist/docx/verticalMergeProjection.d.ts +17 -0
- package/dist/docx/verticalMergeProjection.js +120 -0
- package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
- package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
- package/dist/layout-bridge/convert/tableConversion.js +4 -1
- package/dist/layout-painter/index.js +2 -5
- package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
- package/dist/paged-layout/editorScrollRoot.js +72 -0
- package/dist/paged-layout/scrollToPmPosition.js +8 -22
- package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
- package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
- package/dist/prosemirror/commands/comments.js +6 -1
- package/dist/prosemirror/commands/propertyChangeScope.js +4 -1
- package/dist/prosemirror/commands/resolveAllTableChanges.js +18 -24
- package/dist/prosemirror/commands/resolveParagraphProperties.js +13 -2
- package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +8 -9
- package/dist/prosemirror/commands/tableCellMergeResolution.js +28 -25
- package/dist/prosemirror/commands/tableMergeFoldDecisions.d.ts +61 -0
- package/dist/prosemirror/commands/tableMergeFoldDecisions.js +79 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
- package/dist/prosemirror/commentIdAllocator.js +50 -25
- package/dist/prosemirror/conversion/toProseDoc.d.ts +1 -6
- package/dist/prosemirror/conversion/toProseDoc.js +2 -117
- package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -1
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
- package/dist/prosemirror/listNumbering.js +14 -14
- package/dist/prosemirror/listRendering.d.ts +4 -2
- package/dist/prosemirror/listRendering.js +3 -4
- package/dist/prosemirror/paragraphPropertyCarry.js +2 -5
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
- package/dist/prosemirror/plugins/revisionIds.js +110 -29
- package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
- package/dist/prosemirror/plugins/suggestionMode.js +197 -45
- package/dist/prosemirror/plugins/templateDirectives.d.ts +6 -0
- package/dist/prosemirror/plugins/templateDirectives.js +21 -7
- package/dist/prosemirror/plugins/templatePreviewValues.js +14 -21
- package/dist/prosemirror/storyListNumbering.js +11 -9
- package/dist/prosemirror/textInput.d.ts +6 -1
- package/dist/prosemirror/textInput.js +9 -2
- package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
- package/dist/utils/mergeDocumentContent.d.ts +2 -2
- package/dist/utils/mergeDocumentContent.js +11 -8
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mintBookmarkId, reserveProseBookmarkIds } from "../../../docx/bookmarkIds.js";
|
|
1
2
|
import { BUILT_IN_STYLE_NAME } from "../../../docx/builtInStyles.js";
|
|
2
3
|
import { PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR } from "../../../docx/paragraphPropertySource.js";
|
|
3
4
|
import { isStyleSourcedParagraphNumbering } from "../../../internal/paragraphFormattingSerialization.js";
|
|
@@ -671,6 +672,7 @@ const ParagraphExtension = createNodeExtension({
|
|
|
671
672
|
if (!dispatch) return true;
|
|
672
673
|
const { schema: s } = state;
|
|
673
674
|
const tr = state.tr;
|
|
675
|
+
reserveProseBookmarkIds(state.doc);
|
|
674
676
|
const bookmarkEntries = [];
|
|
675
677
|
for (const h of headings) {
|
|
676
678
|
const bookmarkName = `_Toc${Math.floor(1e8 + Math.random() * 9e8)}`;
|
|
@@ -683,7 +685,7 @@ const ParagraphExtension = createNodeExtension({
|
|
|
683
685
|
const paragraphNode = tr.doc.resolve(mappedPos).nodeAfter;
|
|
684
686
|
if (paragraphNode && paragraphNode.type.name === "paragraph") {
|
|
685
687
|
const newBookmarks = [...(paragraphNode.attrs["bookmarks"] ?? []).filter((b) => !b.name.startsWith("_Toc")), {
|
|
686
|
-
id:
|
|
688
|
+
id: mintBookmarkId(),
|
|
687
689
|
name: bookmarkName
|
|
688
690
|
}];
|
|
689
691
|
tr.setNodeMarkup(mappedPos, void 0, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mergeTextFormatting } from "../../../utils/textFormattingMerge.js";
|
|
2
2
|
import { expectCharacterStyleMarkAttrs, expectRunFormattingOverrideMarkAttrs } from "../../attrs/index.js";
|
|
3
|
+
import { clearIndentOnBackspace } from "../../commands/clearParagraphIndent.js";
|
|
3
4
|
import { keepSectionBreaksOnSurvivingMarks } from "../../commands/sectionBreak.js";
|
|
4
5
|
import { getDocumentStyleResolver } from "../../plugins/documentStyleState.js";
|
|
5
6
|
import { RUN_FORMATTING_MARK_NAMES } from "../../runFormattingMarkNames.js";
|
|
@@ -8,6 +9,7 @@ import { paragraphAttrsFromResolvedStyle } from "../../styles/resolvedStyleAttrs
|
|
|
8
9
|
import { createExtension } from "../create.js";
|
|
9
10
|
import { marksToTextFormatting, textFormattingToMarks } from "../marks/markUtils.js";
|
|
10
11
|
import { Priority } from "../types.js";
|
|
12
|
+
import { EditorState, TextSelection } from "prosemirror-state";
|
|
11
13
|
import { baseKeymap, deleteSelection, joinBackward, joinForward, selectAll, selectParentNode, splitBlock } from "prosemirror-commands";
|
|
12
14
|
//#region src/prosemirror/extensions/features/BaseKeymapExtension.ts
|
|
13
15
|
/**
|
|
@@ -22,31 +24,6 @@ function chainCommands(...commands) {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
|
-
* Backspace at the start of a paragraph clears first-line indent / hanging indent
|
|
26
|
-
* before joining with the previous paragraph (matches Word behavior).
|
|
27
|
-
*/
|
|
28
|
-
const clearIndentOnBackspace = (state, dispatch) => {
|
|
29
|
-
const { $cursor } = state.selection;
|
|
30
|
-
if (!$cursor) return false;
|
|
31
|
-
if ($cursor.parentOffset !== 0) return false;
|
|
32
|
-
if ($cursor.parent.type.name !== "paragraph") return false;
|
|
33
|
-
const attrs = $cursor.parent.attrs;
|
|
34
|
-
const hasFirstLine = attrs["indentFirstLine"] !== null && attrs["indentFirstLine"] > 0;
|
|
35
|
-
const hasHanging = !!attrs["hangingIndent"];
|
|
36
|
-
const hasIndentLeft = attrs["indentLeft"] !== null && attrs["indentLeft"] > 0;
|
|
37
|
-
if (!hasFirstLine && !hasHanging && !hasIndentLeft) return false;
|
|
38
|
-
if (dispatch) {
|
|
39
|
-
const pos = $cursor.before();
|
|
40
|
-
dispatch(state.tr.setNodeMarkup(pos, void 0, {
|
|
41
|
-
...attrs,
|
|
42
|
-
indentFirstLine: null,
|
|
43
|
-
hangingIndent: null,
|
|
44
|
-
indentLeft: null
|
|
45
|
-
}).scrollIntoView());
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
27
|
* Custom Enter handler: splits the block, inherits style-related attrs,
|
|
51
28
|
* clears paragraph borders, and preserves run formatting on the new paragraph.
|
|
52
29
|
*
|
|
@@ -117,16 +94,33 @@ function applyNextParagraphStyle(tr, sourcePara, newPara, resolver) {
|
|
|
117
94
|
tr.setStoredMarks(resolved.runFormatting ? textFormattingToMarks(resolved.runFormatting, tr.doc.type.schema) : []);
|
|
118
95
|
return true;
|
|
119
96
|
}
|
|
97
|
+
const blockBoundaryDeletion = (state) => {
|
|
98
|
+
if (!(state.selection instanceof TextSelection) || state.selection.empty) return null;
|
|
99
|
+
const tr = state.tr.deleteSelection();
|
|
100
|
+
return tr.doc.resolve(tr.mapping.map(state.selection.from)).parent.isTextblock ? null : tr;
|
|
101
|
+
};
|
|
120
102
|
const splitBlockClearBorders = (state, dispatch, view) => {
|
|
121
|
-
const
|
|
103
|
+
const deletion = blockBoundaryDeletion(state);
|
|
104
|
+
const splitState = deletion ? EditorState.create({
|
|
105
|
+
doc: deletion.doc,
|
|
106
|
+
selection: deletion.selection,
|
|
107
|
+
storedMarks: deletion.storedMarks
|
|
108
|
+
}) : state;
|
|
109
|
+
const { $from: preSplitFrom } = splitState.selection;
|
|
122
110
|
const sourcePara = preSplitFrom.parent.type.name === "paragraph" ? preSplitFrom.parent : null;
|
|
123
|
-
const caretFormattingMarks = (
|
|
111
|
+
const caretFormattingMarks = (splitState.storedMarks || preSplitFrom.marks()).filter((mark) => RUN_FORMATTING_MARK_NAMES.has(mark.type.name));
|
|
124
112
|
const resolver = getDocumentStyleResolver(state);
|
|
125
113
|
const splitResult = { tr: null };
|
|
126
|
-
if (!splitBlock(
|
|
114
|
+
if (!splitBlock(splitState, dispatch ? (tr) => {
|
|
127
115
|
splitResult.tr = tr;
|
|
128
116
|
} : void 0, view)) return false;
|
|
129
117
|
if (dispatch && splitResult.tr !== null) {
|
|
118
|
+
if (deletion) {
|
|
119
|
+
for (const step of splitResult.tr.steps) deletion.step(step);
|
|
120
|
+
deletion.setSelection(splitResult.tr.selection.getBookmark().resolve(deletion.doc));
|
|
121
|
+
deletion.setStoredMarks(splitResult.tr.storedMarks);
|
|
122
|
+
splitResult.tr = deletion;
|
|
123
|
+
}
|
|
130
124
|
const tr = splitResult.tr;
|
|
131
125
|
const { $from } = tr.selection;
|
|
132
126
|
const newPara = $from.parent;
|
|
@@ -268,9 +268,9 @@ function createShapeElement(type, w, h, adjustments) {
|
|
|
268
268
|
case "straightConnector1": {
|
|
269
269
|
const el = document.createElementNS(SVG_NS, "line");
|
|
270
270
|
setNum(el, "x1", 0);
|
|
271
|
-
setNum(el, "y1",
|
|
271
|
+
setNum(el, "y1", 0);
|
|
272
272
|
setNum(el, "x2", w);
|
|
273
|
-
setNum(el, "y2", h
|
|
273
|
+
setNum(el, "y2", h);
|
|
274
274
|
return el;
|
|
275
275
|
}
|
|
276
276
|
case "rightBrace": {
|
|
@@ -537,9 +537,11 @@ const ShapeExtension = createNodeExtension({
|
|
|
537
537
|
const strokeWidth = finiteNumber(attrs.outlineWidth) ?? 1;
|
|
538
538
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
539
539
|
svg.setAttribute("xmlns", SVG_NS);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
svg
|
|
540
|
+
const viewportWidth = w === 0 ? 1 : w;
|
|
541
|
+
const viewportHeight = h === 0 ? 1 : h;
|
|
542
|
+
setNum(svg, "width", viewportWidth);
|
|
543
|
+
setNum(svg, "height", viewportHeight);
|
|
544
|
+
svg.setAttribute("viewBox", `0 0 ${viewportWidth} ${viewportHeight}`);
|
|
543
545
|
let gradient = null;
|
|
544
546
|
let fillAttr;
|
|
545
547
|
if (attrs.fillType === "gradient" && attrs.gradientStops) {
|
|
@@ -548,7 +550,7 @@ const ShapeExtension = createNodeExtension({
|
|
|
548
550
|
fillAttr = gradient ? `url(#${gradId})` : safeFillColor;
|
|
549
551
|
} else fillAttr = attrs.fillType === "none" ? "none" : safeFillColor;
|
|
550
552
|
const strokeAttr = attrs.outlineStyle === "none" ? "none" : safeStrokeColor;
|
|
551
|
-
svg.setAttribute("style", `fill:${fillAttr};stroke:${strokeAttr};stroke-width:${strokeWidth}`);
|
|
553
|
+
svg.setAttribute("style", `fill:${fillAttr};stroke:${strokeAttr};stroke-width:${strokeWidth};overflow:visible`);
|
|
552
554
|
if (gradient) {
|
|
553
555
|
const defs = document.createElementNS(SVG_NS, "defs");
|
|
554
556
|
defs.append(gradient);
|
|
@@ -7,6 +7,7 @@ import { listRenderingFor, recordsListRendering } from "./listRendering.js";
|
|
|
7
7
|
import { LIST_RENDERING_ATTR_KEYS } from "./listRenderingAttrs.js";
|
|
8
8
|
import { directParagraphIndentation } from "./paragraphIndentation.js";
|
|
9
9
|
import { getDocumentNumbering } from "./plugins/documentNumbering.js";
|
|
10
|
+
import { getDocumentStyleResolver } from "./plugins/documentStyleState.js";
|
|
10
11
|
import { SUGGESTION_META, makeRevisionInfo } from "./plugins/suggestionMode.js";
|
|
11
12
|
import { listAttrsFromNumbering, listLevelIndentAttrPatch } from "./styles/resolvedStyleAttrs.js";
|
|
12
13
|
//#region src/prosemirror/listNumbering.ts
|
|
@@ -20,22 +21,15 @@ const PARAGRAPH_NODE = "paragraph";
|
|
|
20
21
|
*/
|
|
21
22
|
const listChangeSnapshot = (node, attrs) => {
|
|
22
23
|
const previousFormatting = paragraphPropertiesSnapshot(node.type.create(attrs, node.content, node.marks));
|
|
23
|
-
previousFormatting["numPr"]
|
|
24
|
+
previousFormatting["numPr"] ??= null;
|
|
24
25
|
for (const key of LIST_RENDERING_ATTR_KEYS) previousFormatting[key] = attrs[key] ?? null;
|
|
25
26
|
return previousFormatting;
|
|
26
27
|
};
|
|
27
|
-
|
|
28
|
-
* The original state a pending record describes, completed with the list
|
|
29
|
-
* attrs it may not state. A record written by a list command states them all.
|
|
30
|
-
* One written for another property (or read from a file) states the scope
|
|
31
|
-
* only: its numbering is the original's, and an absent one means the original
|
|
32
|
-
* had none, so the rendering is recomputed from it rather than copied from
|
|
33
|
-
* the live paragraph, whose list may since have changed.
|
|
34
|
-
*/
|
|
35
|
-
const originalListFormatting = (record, numbering) => {
|
|
28
|
+
const originalListFormatting = ({ record, numbering, styleResolver }) => {
|
|
36
29
|
const original = { ...record };
|
|
37
30
|
if (recordsListRendering(record)) return original;
|
|
38
|
-
|
|
31
|
+
const inherited = styleResolver?.resolveParagraphStyle(record.styleId ?? void 0).paragraphFormatting?.numPr;
|
|
32
|
+
Object.assign(original, listRenderingFor(record.numPr ?? inherited, numbering));
|
|
39
33
|
original["numPr"] = record.numPr ?? null;
|
|
40
34
|
return original;
|
|
41
35
|
};
|
|
@@ -54,12 +48,16 @@ const canonicalJson = (value) => JSON.stringify(value, (_key, nested) => nested
|
|
|
54
48
|
* the file) keeps its author, id and date: the change joins that revision
|
|
55
49
|
* rather than re-attributing it, and it is never dropped.
|
|
56
50
|
*/
|
|
57
|
-
const trackListChange = ({ current, next, rev, numbering }) => {
|
|
51
|
+
const trackListChange = ({ current, next, rev, numbering, styleResolver }) => {
|
|
58
52
|
const currentAttrs = expectParagraphAttrs(current);
|
|
59
53
|
const existing = currentAttrs._propertyChanges ?? [];
|
|
60
54
|
const pending = existing.find(({ info }) => info.provenance !== "suggested");
|
|
61
55
|
const retained = existing.filter((change) => change !== pending);
|
|
62
|
-
const previousFormatting = pending?.previousFormatting ? originalListFormatting(
|
|
56
|
+
const previousFormatting = pending?.previousFormatting ? originalListFormatting({
|
|
57
|
+
record: pending.previousFormatting,
|
|
58
|
+
numbering,
|
|
59
|
+
styleResolver
|
|
60
|
+
}) : listChangeSnapshot(current, currentAttrs);
|
|
63
61
|
const ownPending = pending !== void 0 && pending.info.author === rev.author;
|
|
64
62
|
if (ownPending && canonicalJson(previousFormatting) === canonicalJson(listChangeSnapshot(current, next))) return {
|
|
65
63
|
...next,
|
|
@@ -198,12 +196,14 @@ const listItemAttrs = (attrs, { numId, ilvl }, numbering) => ({
|
|
|
198
196
|
const applyParagraphUpdates = ({ tr, state, updates }) => {
|
|
199
197
|
const rev = makeRevisionInfo(state);
|
|
200
198
|
const numbering = getDocumentNumbering(state);
|
|
199
|
+
const styleResolver = getDocumentStyleResolver(state);
|
|
201
200
|
for (const { pos, node, next } of updates) {
|
|
202
201
|
const attrs = rev ? trackListChange({
|
|
203
202
|
current: node,
|
|
204
203
|
next,
|
|
205
204
|
rev,
|
|
206
|
-
numbering
|
|
205
|
+
numbering,
|
|
206
|
+
styleResolver
|
|
207
207
|
}) : next;
|
|
208
208
|
tr.setNodeMarkup(pos, void 0, attrs);
|
|
209
209
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ParagraphAttrs, ParagraphPropertyChangeAttrs } from "./schema/nodes.js";
|
|
2
2
|
import { NumberingMap } from "../docx/numberingParser.js";
|
|
3
|
+
import { ParagraphNumberingOverride } from "../docx/numberingReference.js";
|
|
3
4
|
//#region src/prosemirror/listRendering.d.ts
|
|
4
5
|
type PreviousFormatting = NonNullable<ParagraphPropertyChangeAttrs["previousFormatting"]>;
|
|
5
6
|
/** Whether a `w:pPrChange` record states the list rendering it had (a list command's does). */
|
|
6
7
|
declare const recordsListRendering: (record: PreviousFormatting) => boolean;
|
|
7
8
|
/** Every list-rendering attr for `numPr`: its level's, or all cleared when it numbers nothing. */
|
|
8
|
-
declare const listRenderingFor: (numPr:
|
|
9
|
+
declare const listRenderingFor: (numPr: ParagraphNumberingOverride | null | undefined, numbering: NumberingMap | null) => Record<string, unknown>;
|
|
9
10
|
/**
|
|
10
11
|
* The list rendering a paragraph takes when a `w:pPrChange` is rejected. A
|
|
11
12
|
* reject restores the recorded numbering, but the rendering attrs sit outside
|
|
@@ -21,7 +22,8 @@ type RejectedListRenderingOptions = {
|
|
|
21
22
|
/** The previous state the rejected record restores. */
|
|
22
23
|
previousFormatting: PreviousFormatting | null | undefined;
|
|
23
24
|
numbering: NumberingMap | null;
|
|
25
|
+
restoredNumbering: ParagraphAttrs["numPr"] | null;
|
|
24
26
|
};
|
|
25
|
-
declare const rejectedListRenderingPatch: ({ current, previousFormatting, numbering }: RejectedListRenderingOptions) => Record<string, unknown>;
|
|
27
|
+
declare const rejectedListRenderingPatch: ({ current, previousFormatting, numbering, restoredNumbering }: RejectedListRenderingOptions) => Record<string, unknown>;
|
|
26
28
|
//#endregion
|
|
27
29
|
export { RejectedListRenderingOptions, listRenderingFor, recordsListRendering, rejectedListRenderingPatch };
|
|
@@ -16,10 +16,9 @@ const listRenderingFor = (numPr, numbering) => {
|
|
|
16
16
|
for (const key of LIST_RENDERING_ATTR_KEYS) attrs[key] = rendering[key] ?? null;
|
|
17
17
|
return attrs;
|
|
18
18
|
};
|
|
19
|
-
const rejectedListRenderingPatch = ({ current, previousFormatting, numbering }) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const restored = record.numPr ?? void 0;
|
|
19
|
+
const rejectedListRenderingPatch = ({ current, previousFormatting, numbering, restoredNumbering }) => {
|
|
20
|
+
if (recordsListRendering(previousFormatting ?? {})) return {};
|
|
21
|
+
const restored = restoredNumbering ?? void 0;
|
|
23
22
|
if (sameStatedParagraphNumbering(restored, current.numPr ?? void 0)) return {};
|
|
24
23
|
return listRenderingFor(restored, numbering);
|
|
25
24
|
};
|
|
@@ -86,7 +86,7 @@ const carryParagraphProperties = ({ tr, position, source, styleResolver, numberi
|
|
|
86
86
|
};
|
|
87
87
|
/**
|
|
88
88
|
* Whether the carried numbering is the paragraph's own or its style's: the
|
|
89
|
-
* record holds
|
|
89
|
+
* record holds only stated numbering, and a style's numbering
|
|
90
90
|
* stays the style's rather than becoming the paragraph's own `w:numPr`.
|
|
91
91
|
*/
|
|
92
92
|
const carriedNumberingProvenance = (source, target, keep) => ({ numPrFromStyle: expectParagraphAttrs(keep?.has("numPr") ? target : source).numPrFromStyle });
|
|
@@ -112,10 +112,7 @@ const paragraphLeftAfter = ({ doc, paragraphPos, removedBreakPositions }) => {
|
|
|
112
112
|
node = next;
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
|
-
const recordedFormatting =
|
|
116
|
-
...paragraphPropertiesSnapshot(node),
|
|
117
|
-
numPr: expectParagraphAttrs(node).numPr ?? null
|
|
118
|
-
});
|
|
115
|
+
const recordedFormatting = paragraphPropertiesSnapshot;
|
|
119
116
|
/**
|
|
120
117
|
* A paragraph now holds the break another one had (a split, or a paste into
|
|
121
118
|
* it, leaves that break on the last part) but reads with other properties:
|
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
import { Node } from "prosemirror-model";
|
|
2
2
|
//#region src/prosemirror/plugins/revisionIds.d.ts
|
|
3
|
+
declare const RevisionIdAllocationError_base: import("better-result").TaggedErrorClass<"RevisionIdAllocationError">;
|
|
4
|
+
declare class RevisionIdAllocationError extends RevisionIdAllocationError_base<{
|
|
5
|
+
message: string;
|
|
6
|
+
first: number;
|
|
7
|
+
next: number;
|
|
8
|
+
}> {}
|
|
9
|
+
/** Deterministic package-local batches prefer ids above loaded revisions, then a free interval. */
|
|
10
|
+
declare const revisionIdSeedAbove: (existingIds: Iterable<number>) => number;
|
|
11
|
+
/** The next free id, including after the signed 32-bit boundary wraps. */
|
|
12
|
+
declare const nextRevisionId: () => number;
|
|
3
13
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
14
|
+
* A shared batch increments its seed locally, so a single free id is not
|
|
15
|
+
* enough: start in the largest free interval. Every contiguous batch that
|
|
16
|
+
* can fit anywhere fits here, including after loaded ids shorten the tail.
|
|
17
|
+
* Continue within this interval until another producer changes the cursor.
|
|
6
18
|
*/
|
|
19
|
+
declare const nextRevisionIdRange: () => number;
|
|
20
|
+
/** Reserve a deterministic batch without changing its caller-supplied ids. */
|
|
21
|
+
declare const reserveRevisionIds: (first: number, next: number) => void;
|
|
22
|
+
/** Shared producers must never cross a loaded or already issued id on wrap. */
|
|
23
|
+
declare const claimRevisionIds: (first: number, next: number) => void;
|
|
24
|
+
/** Mint a bounded id without reusing loaded or previously issued ids. */
|
|
7
25
|
declare function mintRevisionId(): number;
|
|
8
|
-
/**
|
|
9
|
-
* Raise the counter above `maxId` so freshly minted ids cannot collide with
|
|
10
|
-
* ids already present in a loaded document. Only ever raises.
|
|
11
|
-
*
|
|
12
|
-
* `maxId` is derived from file content, so it is untrusted: a malformed or
|
|
13
|
-
* out-of-range value is ignored outright rather than folded into range.
|
|
14
|
-
*/
|
|
26
|
+
/** Reserve a valid loaded id and prefer the range above it. */
|
|
15
27
|
declare function seedRevisionIdsAbove(maxId: number): void;
|
|
16
|
-
|
|
17
|
-
* The highest id in `doc` from any part of the annotation space.
|
|
18
|
-
*
|
|
19
|
-
* A revision, a comment and a bookmark draw their `w:id` from one space, so a
|
|
20
|
-
* counter seeded above its own kind alone still mints over another kind's id.
|
|
21
|
-
* Both allocators seed from this, and the save-time normalization avoids the
|
|
22
|
-
* same space, so the three agree on what is taken.
|
|
23
|
-
*/
|
|
24
|
-
declare function maxAnnotationIdInDoc(doc: Node): number;
|
|
28
|
+
declare const maxAnnotationIdInDoc: (doc: Node) => number;
|
|
25
29
|
/**
|
|
26
30
|
* Raise the counter above every annotation id already present in `doc`.
|
|
27
31
|
* Called from the suggestion-mode plugin's `state.init`.
|
|
28
32
|
*/
|
|
29
33
|
declare function seedRevisionIdsFromDoc(doc: Node): void;
|
|
30
34
|
//#endregion
|
|
31
|
-
export { maxAnnotationIdInDoc, mintRevisionId, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
35
|
+
export { RevisionIdAllocationError, claimRevisionIds, maxAnnotationIdInDoc, mintRevisionId, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, revisionIdSeedAbove, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TaggedError } from "better-result";
|
|
1
2
|
import { MAX_REVISION_ID } from "@stll/docx-core/model";
|
|
2
3
|
//#region src/prosemirror/plugins/revisionIds.ts
|
|
3
4
|
/**
|
|
@@ -9,23 +10,98 @@ import { MAX_REVISION_ID } from "@stll/docx-core/model";
|
|
|
9
10
|
* int consumers read `w:id` into.
|
|
10
11
|
*/
|
|
11
12
|
let counter = 1;
|
|
13
|
+
const occupiedIds = /* @__PURE__ */ new Set();
|
|
14
|
+
var RevisionIdAllocationError = class extends TaggedError("RevisionIdAllocationError") {};
|
|
15
|
+
/** Batches use contiguous ids; on rollover prefer the largest unused interval. */
|
|
16
|
+
const largestFreeIntervalStart = (reservedIds) => {
|
|
17
|
+
let first = 1;
|
|
18
|
+
let bestFirst = 1;
|
|
19
|
+
let bestSize = 0;
|
|
20
|
+
const boundaries = [...reservedIds].filter((id) => id > 0).toSorted((a, b) => a - b);
|
|
21
|
+
boundaries.push(MAX_REVISION_ID + 1);
|
|
22
|
+
for (const boundary of boundaries) {
|
|
23
|
+
const size = boundary - first;
|
|
24
|
+
if (size > bestSize) {
|
|
25
|
+
bestFirst = first;
|
|
26
|
+
bestSize = size;
|
|
27
|
+
}
|
|
28
|
+
first = boundary + 1;
|
|
29
|
+
}
|
|
30
|
+
if (bestSize === 0) throw new RevisionIdAllocationError({
|
|
31
|
+
message: "Revision id space exhausted",
|
|
32
|
+
first: 1,
|
|
33
|
+
next: 1
|
|
34
|
+
});
|
|
35
|
+
return bestFirst;
|
|
36
|
+
};
|
|
37
|
+
/** Deterministic package-local batches prefer ids above loaded revisions, then a free interval. */
|
|
38
|
+
const revisionIdSeedAbove = (existingIds) => {
|
|
39
|
+
const reservedIds = /* @__PURE__ */ new Set();
|
|
40
|
+
let maxId = 0;
|
|
41
|
+
for (const id of existingIds) {
|
|
42
|
+
if (!Number.isInteger(id) || id < 0 || id > MAX_REVISION_ID) continue;
|
|
43
|
+
reservedIds.add(id);
|
|
44
|
+
maxId = Math.max(maxId, id);
|
|
45
|
+
}
|
|
46
|
+
return maxId < MAX_REVISION_ID ? maxId + 1 : largestFreeIntervalStart(reservedIds);
|
|
47
|
+
};
|
|
48
|
+
/** The next free id, including after the signed 32-bit boundary wraps. */
|
|
49
|
+
const nextRevisionId = () => {
|
|
50
|
+
if (counter > MAX_REVISION_ID) counter = largestFreeIntervalStart(occupiedIds);
|
|
51
|
+
while (occupiedIds.has(counter)) {
|
|
52
|
+
counter += 1;
|
|
53
|
+
if (counter > MAX_REVISION_ID) counter = largestFreeIntervalStart(occupiedIds);
|
|
54
|
+
if (occupiedIds.size >= MAX_REVISION_ID) throw new RevisionIdAllocationError({
|
|
55
|
+
message: "Revision id space exhausted",
|
|
56
|
+
first: counter,
|
|
57
|
+
next: counter
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return counter;
|
|
61
|
+
};
|
|
12
62
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
63
|
+
* A shared batch increments its seed locally, so a single free id is not
|
|
64
|
+
* enough: start in the largest free interval. Every contiguous batch that
|
|
65
|
+
* can fit anywhere fits here, including after loaded ids shorten the tail.
|
|
66
|
+
* Continue within this interval until another producer changes the cursor.
|
|
15
67
|
*/
|
|
68
|
+
const nextRevisionIdRange = () => {
|
|
69
|
+
counter = largestFreeIntervalStart(occupiedIds);
|
|
70
|
+
return counter;
|
|
71
|
+
};
|
|
72
|
+
const validateRevisionIdRange = (first, next) => {
|
|
73
|
+
if (!Number.isInteger(first) || !Number.isInteger(next) || first < 0 || next < first || next > MAX_REVISION_ID + 1) throw new RevisionIdAllocationError({
|
|
74
|
+
message: "Revision id range exceeds the OOXML limit",
|
|
75
|
+
first,
|
|
76
|
+
next
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
/** Reserve a deterministic batch without changing its caller-supplied ids. */
|
|
80
|
+
const reserveRevisionIds = (first, next) => {
|
|
81
|
+
validateRevisionIdRange(first, next);
|
|
82
|
+
for (let id = first; id < next; id += 1) occupiedIds.add(id);
|
|
83
|
+
if (next > counter) counter = next;
|
|
84
|
+
};
|
|
85
|
+
/** Shared producers must never cross a loaded or already issued id on wrap. */
|
|
86
|
+
const claimRevisionIds = (first, next) => {
|
|
87
|
+
validateRevisionIdRange(first, next);
|
|
88
|
+
for (let id = first; id < next && id <= MAX_REVISION_ID; id += 1) if (occupiedIds.has(id)) throw new RevisionIdAllocationError({
|
|
89
|
+
message: "Revision id range overlaps an occupied id",
|
|
90
|
+
first,
|
|
91
|
+
next
|
|
92
|
+
});
|
|
93
|
+
reserveRevisionIds(first, next);
|
|
94
|
+
};
|
|
95
|
+
/** Mint a bounded id without reusing loaded or previously issued ids. */
|
|
16
96
|
function mintRevisionId() {
|
|
17
|
-
|
|
18
|
-
|
|
97
|
+
const id = nextRevisionId();
|
|
98
|
+
claimRevisionIds(id, id + 1);
|
|
99
|
+
return id;
|
|
19
100
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Raise the counter above `maxId` so freshly minted ids cannot collide with
|
|
22
|
-
* ids already present in a loaded document. Only ever raises.
|
|
23
|
-
*
|
|
24
|
-
* `maxId` is derived from file content, so it is untrusted: a malformed or
|
|
25
|
-
* out-of-range value is ignored outright rather than folded into range.
|
|
26
|
-
*/
|
|
101
|
+
/** Reserve a valid loaded id and prefer the range above it. */
|
|
27
102
|
function seedRevisionIdsAbove(maxId) {
|
|
28
|
-
if (!Number.isInteger(maxId) || maxId < 0 || maxId
|
|
103
|
+
if (!Number.isInteger(maxId) || maxId < 0 || maxId > MAX_REVISION_ID) return;
|
|
104
|
+
occupiedIds.add(maxId);
|
|
29
105
|
if (maxId >= counter) counter = maxId + 1;
|
|
30
106
|
}
|
|
31
107
|
/** Node attrs that carry a revision id directly (`revisionId` field). */
|
|
@@ -33,40 +109,45 @@ const DIRECT_REVISION_ATTR_KEYS = ["trIns", "trDel"];
|
|
|
33
109
|
/**
|
|
34
110
|
* The highest id in `doc` from any part of the annotation space.
|
|
35
111
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Both allocators seed from this, and the save-time normalization avoids the
|
|
39
|
-
* same space, so the three agree on what is taken.
|
|
112
|
+
* Preserve the editor's conservative annotation seeding: revisions reserve
|
|
113
|
+
* comment and bookmark ids too, although OOXML defines separate id spaces.
|
|
40
114
|
*/
|
|
41
|
-
|
|
115
|
+
const visitAnnotationIds = (doc, visit) => {
|
|
42
116
|
let max = 0;
|
|
43
117
|
const consider = (id) => {
|
|
44
|
-
if (typeof id === "number" && Number.isInteger(id) && id
|
|
118
|
+
if (typeof id === "number" && Number.isInteger(id) && id >= 0 && id <= MAX_REVISION_ID) {
|
|
119
|
+
visit(id);
|
|
120
|
+
max = Math.max(max, id);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const considerMetadata = (value) => {
|
|
124
|
+
if (typeof value !== "object" || value === null) return;
|
|
125
|
+
for (const [key, child] of Object.entries(value)) if (key === "id" || key === "revisionId" || key === "commentId") consider(child);
|
|
126
|
+
else considerMetadata(child);
|
|
45
127
|
};
|
|
46
128
|
doc.descendants((node) => {
|
|
47
129
|
for (const mark of node.marks) {
|
|
48
130
|
consider(mark.attrs["revisionId"]);
|
|
49
131
|
consider(mark.attrs["commentId"]);
|
|
132
|
+
considerMetadata(mark.attrs["_docxRevisionAncestors"]);
|
|
133
|
+
considerMetadata(mark.attrs["changes"]);
|
|
50
134
|
}
|
|
51
135
|
if (node.type.name === "bookmarkBoundary") consider(node.attrs["id"]);
|
|
52
136
|
const attrs = node.attrs;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const key of DIRECT_REVISION_ATTR_KEYS)
|
|
56
|
-
|
|
57
|
-
if (value) consider(value.revisionId);
|
|
58
|
-
}
|
|
59
|
-
const cellMarker = attrs["cellMarker"];
|
|
60
|
-
if (cellMarker?.info) consider(cellMarker.info.revisionId);
|
|
137
|
+
considerMetadata(attrs["_propertyChanges"]);
|
|
138
|
+
considerMetadata(attrs["pPrMark"]);
|
|
139
|
+
for (const key of DIRECT_REVISION_ATTR_KEYS) considerMetadata(attrs[key]);
|
|
140
|
+
considerMetadata(attrs["cellMarker"]);
|
|
61
141
|
});
|
|
62
142
|
return max;
|
|
63
|
-
}
|
|
143
|
+
};
|
|
144
|
+
const maxAnnotationIdInDoc = (doc) => visitAnnotationIds(doc, () => {});
|
|
64
145
|
/**
|
|
65
146
|
* Raise the counter above every annotation id already present in `doc`.
|
|
66
147
|
* Called from the suggestion-mode plugin's `state.init`.
|
|
67
148
|
*/
|
|
68
149
|
function seedRevisionIdsFromDoc(doc) {
|
|
69
|
-
|
|
150
|
+
visitAnnotationIds(doc, seedRevisionIdsAbove);
|
|
70
151
|
}
|
|
71
152
|
//#endregion
|
|
72
|
-
export { maxAnnotationIdInDoc, mintRevisionId, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
153
|
+
export { RevisionIdAllocationError, claimRevisionIds, maxAnnotationIdInDoc, mintRevisionId, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, revisionIdSeedAbove, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
@@ -66,8 +66,8 @@ declare function handleSuggestionEnter(view: EditorView, pluginState: Suggestion
|
|
|
66
66
|
* - Delete-at-paragraph-end → current paragraph.
|
|
67
67
|
*
|
|
68
68
|
* Returns `null` when there is no adjacent sibling paragraph (the join would
|
|
69
|
-
* not produce paragraph-mark merging
|
|
70
|
-
*
|
|
69
|
+
* not produce paragraph-mark merging, e.g. doc start, doc end, or a table).
|
|
70
|
+
* Bookmark markers at an edge do not hide the adjacent paragraph break.
|
|
71
71
|
*/
|
|
72
72
|
declare function paragraphBoundaryTarget(state: EditorState, direction: "backward" | "forward"): number | null;
|
|
73
73
|
/** Apply the editor's selected-content deletion path for a host cut action. */
|