@stll/folio-core 0.37.0 → 0.37.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.
@@ -1,7 +1,15 @@
1
- import { FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
1
+ import { FolioAIEditOperation, FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
2
2
  import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
3
3
  import { Result } from "better-result";
4
4
  //#region src/compare/scenario.d.ts
5
+ type InsertTableRowOperation = Extract<FolioAIEditOperation, {
6
+ type: "insertTableRow";
7
+ }>;
8
+ type TableRowPosition = NonNullable<InsertTableRowOperation["position"]>;
9
+ declare const EDIT_SCRIPT_TABLE_ROW_POSITION: {
10
+ readonly after: "after";
11
+ readonly before: "before";
12
+ };
5
13
  type EditScriptStep = {
6
14
  type: "insertParagraphAfter";
7
15
  blockIndex: number;
@@ -46,6 +54,7 @@ type EditScriptStep = {
46
54
  } | {
47
55
  type: "insertTableRow";
48
56
  blockIndex: number;
57
+ position?: TableRowPosition;
49
58
  cellTexts: readonly string[];
50
59
  } | {
51
60
  type: "deleteTableRow";
@@ -74,4 +83,4 @@ type EditScriptResult = {
74
83
  */
75
84
  declare const applyEditScript: (base: ArrayBuffer, script: EditScript) => Promise<Result<EditScriptResult, CompareDocxParseError | CompareDocxSerializeError>>;
76
85
  //#endregion
77
- export { EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
86
+ export { EDIT_SCRIPT_TABLE_ROW_POSITION, EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
@@ -24,6 +24,10 @@ import { Result, panic } from "better-result";
24
24
  * dropped: a script silently doing less than it says would weaken every
25
25
  * property built on it.
26
26
  */
27
+ const EDIT_SCRIPT_TABLE_ROW_POSITION = {
28
+ after: "after",
29
+ before: "before"
30
+ };
27
31
  const EDIT_SCRIPT_UNRESOLVED_REASONS = Object.freeze([
28
32
  "block-out-of-range",
29
33
  "block-not-in-table",
@@ -160,7 +164,7 @@ const planStep = ({ step, blocks, nextOperationId }) => {
160
164
  id: nextOperationId(),
161
165
  type: "insertTableRow",
162
166
  blockId: block.id,
163
- position: "after",
167
+ position: step.position ?? EDIT_SCRIPT_TABLE_ROW_POSITION.after,
164
168
  cellTexts: [...step.cellTexts]
165
169
  }]
166
170
  } : {
@@ -260,4 +264,4 @@ const applyEditScript = async (base, script) => {
260
264
  });
261
265
  };
262
266
  //#endregion
263
- export { EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
267
+ export { EDIT_SCRIPT_TABLE_ROW_POSITION, EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
@@ -15,7 +15,7 @@ import { hasComplexScript, isComplexScriptCodePoint } from "../utils/scriptSegme
15
15
  * advances rightward from its origin whatever the paragraph does.
16
16
  */
17
17
  declare const glyphCellOffsetsPx: (run: DisplayGlyphRun) => readonly number[];
18
- declare const DISPLAY_PRIMITIVE_KINDS: ("image" | "line" | "rect" | "glyphRun" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
18
+ declare const DISPLAY_PRIMITIVE_KINDS: ("glyphRun" | "rect" | "line" | "image" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
19
19
  /**
20
20
  * Dash geometry of each stroke pattern, as multiples of the stroke thickness.
21
21
  *
@@ -101,8 +101,8 @@ declare const FOLIO_DOCUMENT_OPERATION_KEYS_BY_TYPE: Readonly<{
101
101
  readonly insertBeforeBlock: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "text", "inheritFormatting", "alignment", "spacing", "listLevel", "moveId", "pageBreakBefore", "styleId", "comment"];
102
102
  readonly replaceBlock: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "text", "preserveFormatting", "styleId", "comment"];
103
103
  readonly deleteBlock: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "moveId", "comment"];
104
- readonly splitBlock: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "offset", "separator"];
105
- readonly mergeBlockWithNext: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "separator"];
104
+ readonly splitBlock: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "offset", "separator", "firstParagraphProperties", "secondParagraphProperties"];
105
+ readonly mergeBlockWithNext: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "separator", "mergedParagraphProperties"];
106
106
  readonly setBlockParagraphProperties: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "properties"];
107
107
  readonly insertTable: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId", "position", "rows"];
108
108
  readonly deleteTable: readonly ["id", "type", "blockId", "severity", "area", "precondition", "suggestionId"];
@@ -262,9 +262,9 @@ const readTableRows = (value, path) => {
262
262
  if (parsed.some((row) => row.length !== width)) return invalidBatch(rowsPath, "expected every row to hold the same number of cells");
263
263
  return parsed;
264
264
  };
265
- const readParagraphProperties = (value, path) => {
266
- const candidate = value["properties"];
267
- const propertiesPath = `${path}.properties`;
265
+ const readParagraphProperties = ({ value, key, path }) => {
266
+ const candidate = value[key];
267
+ const propertiesPath = `${path}.${key}`;
268
268
  if (!isPlainObject(candidate)) return invalidBatch(propertiesPath, "expected an object");
269
269
  assertAllowedKeys(candidate, propertiesPath, [
270
270
  "styleId",
@@ -463,9 +463,15 @@ const FOLIO_DOCUMENT_OPERATION_KEYS_BY_TYPE = Object.freeze({
463
463
  splitBlock: [
464
464
  ...COMMON_OPERATION_KEYS,
465
465
  "offset",
466
- "separator"
466
+ "separator",
467
+ "firstParagraphProperties",
468
+ "secondParagraphProperties"
469
+ ],
470
+ mergeBlockWithNext: [
471
+ ...COMMON_OPERATION_KEYS,
472
+ "separator",
473
+ "mergedParagraphProperties"
467
474
  ],
468
- mergeBlockWithNext: [...COMMON_OPERATION_KEYS, "separator"],
469
475
  setBlockParagraphProperties: [...COMMON_OPERATION_KEYS, "properties"],
470
476
  insertTable: [
471
477
  ...COMMON_OPERATION_KEYS,
@@ -588,24 +594,48 @@ const parseDocumentOperation = (value, index) => {
588
594
  id,
589
595
  type,
590
596
  blockId,
591
- properties: readParagraphProperties(value, path)
597
+ properties: readParagraphProperties({
598
+ value,
599
+ key: "properties",
600
+ path
601
+ })
592
602
  };
593
603
  if (type === "splitBlock" || type === "mergeBlockWithNext") {
594
604
  const separator = readOptionalString(value, "separator", path);
595
- if (type === "mergeBlockWithNext") return {
596
- ...operationMeta,
597
- id,
598
- type,
599
- blockId,
600
- ...separator !== void 0 && { separator }
601
- };
605
+ if (type === "mergeBlockWithNext") {
606
+ const mergedParagraphProperties = value["mergedParagraphProperties"] === void 0 ? void 0 : readParagraphProperties({
607
+ value,
608
+ key: "mergedParagraphProperties",
609
+ path
610
+ });
611
+ return {
612
+ ...operationMeta,
613
+ id,
614
+ type,
615
+ blockId,
616
+ ...separator !== void 0 && { separator },
617
+ ...mergedParagraphProperties !== void 0 && { mergedParagraphProperties }
618
+ };
619
+ }
620
+ const firstParagraphProperties = value["firstParagraphProperties"] === void 0 ? void 0 : readParagraphProperties({
621
+ value,
622
+ key: "firstParagraphProperties",
623
+ path
624
+ });
625
+ const secondParagraphProperties = value["secondParagraphProperties"] === void 0 ? void 0 : readParagraphProperties({
626
+ value,
627
+ key: "secondParagraphProperties",
628
+ path
629
+ });
602
630
  return {
603
631
  ...operationMeta,
604
632
  id,
605
633
  type,
606
634
  blockId,
607
635
  offset: readNonNegativeInteger(value, "offset", path),
608
- ...separator !== void 0 && { separator }
636
+ ...separator !== void 0 && { separator },
637
+ ...firstParagraphProperties !== void 0 && { firstParagraphProperties },
638
+ ...secondParagraphProperties !== void 0 && { secondParagraphProperties }
609
639
  };
610
640
  }
611
641
  if (type === "replaceInBlock") return {
@@ -3,12 +3,12 @@ import { Transaction } from "prosemirror-state";
3
3
  import { MarkType } from "prosemirror-model";
4
4
  //#region src/internal/headlessRevisionResolution.d.ts
5
5
  type HeadlessRevisionResolutionMode = "accept" | "reject";
6
- type HeadlessDeleteRange = {
6
+ type HeadlessRange = {
7
7
  from: number;
8
8
  to: number;
9
9
  };
10
10
  type HeadlessInlineChangeTracking = {
11
- ranges: readonly HeadlessDeleteRange[];
11
+ ranges: readonly HeadlessRange[];
12
12
  mappingFrom: number;
13
13
  };
14
14
  type AppendHeadlessInlineResolutionOptions = {
@@ -76,6 +76,7 @@ const resolveInlineNode = ({ node, context, paragraphScope }) => {
76
76
  };
77
77
  const resolveInlineContent = ({ node, position, context, inheritedParagraphScope }) => {
78
78
  const paragraphScope = node.type.name === "paragraph" && context.mode === "reject" ? { paragraph: node } : inheritedParagraphScope;
79
+ const replacementRangeStart = context.replacementRanges.length;
79
80
  let resolvedNode = node;
80
81
  if (node.isInline) {
81
82
  const resolved = resolveInlineNode({
@@ -84,9 +85,10 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
84
85
  paragraphScope
85
86
  });
86
87
  if (resolved === null) {
87
- context.deleteRanges.push({
88
+ context.replacementRanges.push({
88
89
  from: position,
89
- to: position + node.nodeSize
90
+ to: position + node.nodeSize,
91
+ newSize: 0
90
92
  });
91
93
  return null;
92
94
  }
@@ -109,17 +111,36 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
109
111
  } else contentChanged = true;
110
112
  });
111
113
  if (!contentChanged) return resolvedNode;
114
+ let resolvedContent = Fragment.fromArray(children);
115
+ if (!resolvedNode.type.validContent(resolvedContent)) {
116
+ const fitted = resolvedNode.type.createAndFill(resolvedNode.attrs, resolvedContent, resolvedNode.marks);
117
+ if (!fitted || !resolvedNode.type.validContent(fitted.content)) {
118
+ if (!resolvedNode.isInline) return panic(`Headless inline resolution invalidated ${resolvedNode.type.name} content`);
119
+ context.replacementRanges.splice(replacementRangeStart, context.replacementRanges.length - replacementRangeStart, {
120
+ from: position,
121
+ to: position + node.nodeSize,
122
+ newSize: 0
123
+ });
124
+ return null;
125
+ }
126
+ context.replacementRanges.splice(replacementRangeStart, context.replacementRanges.length - replacementRangeStart, {
127
+ from: contentStart,
128
+ to: contentStart + node.content.size,
129
+ newSize: fitted.content.size
130
+ });
131
+ resolvedContent = fitted.content;
132
+ }
112
133
  if (resolvedNode.type.name === "paragraph") context.changedParagraphRanges.push({
113
134
  from: position,
114
135
  to: position + resolvedNode.nodeSize
115
136
  });
116
- return rebuildNode(node, resolvedNode.attrs, Fragment.fromArray(children), resolvedNode.marks);
137
+ return rebuildNode(node, resolvedNode.attrs, resolvedContent, resolvedNode.marks);
117
138
  };
118
- const coalesceDeleteRanges = (deleteRanges) => {
139
+ const coalesceReplacementRanges = (replacementRanges) => {
119
140
  const coalesced = [];
120
- for (const range of deleteRanges) {
141
+ for (const range of replacementRanges) {
121
142
  const previous = coalesced.at(-1);
122
- if (previous && range.from <= previous.to) previous.to = Math.max(previous.to, range.to);
143
+ if (previous && previous.newSize === 0 && range.newSize === 0 && range.from <= previous.to) previous.to = Math.max(previous.to, range.to);
123
144
  else coalesced.push({ ...range });
124
145
  }
125
146
  return coalesced;
@@ -129,7 +150,7 @@ const appendHeadlessInlineResolution = ({ tr, mode, keepType, removeType, styleR
129
150
  mode,
130
151
  keepType,
131
152
  removeType,
132
- deleteRanges: [],
153
+ replacementRanges: [],
133
154
  changedParagraphRanges: [],
134
155
  styleResolver
135
156
  };
@@ -139,10 +160,10 @@ const appendHeadlessInlineResolution = ({ tr, mode, keepType, removeType, styleR
139
160
  context
140
161
  });
141
162
  if (!resolved || resolved.eq(tr.doc)) return null;
142
- const positionMap = new StepMap(coalesceDeleteRanges(context.deleteRanges).flatMap(({ from, to }) => [
163
+ const positionMap = new StepMap(coalesceReplacementRanges(context.replacementRanges).flatMap(({ from, to, newSize }) => [
143
164
  from,
144
165
  to - from,
145
- 0
166
+ newSize
146
167
  ]));
147
168
  const mappingFrom = tr.steps.length;
148
169
  tr.step(new HeadlessInlineResolutionStep(0, tr.doc.content.size, new Slice(resolved.content, 0, 0), positionMap));
@@ -50,7 +50,6 @@ function removeCommentMark(commentId) {
50
50
  return true;
51
51
  };
52
52
  }
53
- const BULK_INLINE_RESOLUTION_THRESHOLD = 256;
54
53
  /**
55
54
  * Resolve a tracked change: accept or reject.
56
55
  * - Accept: keep insertions (remove mark), delete deletions (remove text)
@@ -80,8 +79,6 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
80
79
  const pPrMarkOps = [];
81
80
  const tableRowStructuralOps = [];
82
81
  const tableCellStructuralOps = [];
83
- const deferredRunPropertyChanges = [];
84
- let bulkInlineCarrierCount = 0;
85
82
  let removedSectionEndpointCount = 0;
86
83
  const removedSectionReferences = [];
87
84
  state.doc.nodesBetween(from, to, (node, pos) => {
@@ -156,31 +153,12 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
156
153
  return true;
157
154
  }
158
155
  if (!node.isInline) return true;
156
+ if (canBulkInlineResolution) return true;
159
157
  const nodeEnd = pos + node.nodeSize;
160
158
  const rangeFrom = Math.max(from, pos);
161
159
  const rangeTo = Math.min(to, nodeEnd);
162
160
  const runPropertyChangeMark = node.marks.find((mark) => mark.type.name === "runPropertyChange");
163
- const resolvesRunPropertyChange = runPropertyChangeMark !== void 0 && expectRunPropertyChangeMarkAttrs(runPropertyChangeMark).changes.length > 0;
164
161
  const removesNode = removeType !== void 0 && node.marks.some((mark) => mark.type === removeType && matchesRevision(mark));
165
- const removesKeptMark = keepType !== void 0 && node.marks.some((mark) => mark.type === keepType && matchesRevision(mark));
166
- if (canBulkInlineResolution) {
167
- if (resolvesRunPropertyChange) deferredRunPropertyChanges.push({
168
- tr,
169
- node,
170
- from: rangeFrom,
171
- to: rangeTo,
172
- mark: runPropertyChangeMark,
173
- mode,
174
- revisionSet,
175
- styleResolver
176
- });
177
- if (removesNode) deleteRanges.push({
178
- from: rangeFrom,
179
- to: rangeTo
180
- });
181
- if (resolvesRunPropertyChange || removesNode || removesKeptMark) bulkInlineCarrierCount++;
182
- return true;
183
- }
184
162
  if (runPropertyChangeMark) resolveRunPropertyChange({
185
163
  tr,
186
164
  node,
@@ -201,7 +179,7 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
201
179
  return true;
202
180
  });
203
181
  let bulkInlineChangeTracking = null;
204
- if (canBulkInlineResolution && bulkInlineCarrierCount >= BULK_INLINE_RESOLUTION_THRESHOLD) bulkInlineChangeTracking = appendHeadlessInlineResolution({
182
+ if (canBulkInlineResolution) bulkInlineChangeTracking = appendHeadlessInlineResolution({
205
183
  tr,
206
184
  mode,
207
185
  keepType,
@@ -209,7 +187,6 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
209
187
  styleResolver
210
188
  });
211
189
  else {
212
- for (const deferred of deferredRunPropertyChanges) resolveRunPropertyChange(deferred);
213
190
  if (removeKeptMarksInBulk) tr.removeMark(from, to, keepType);
214
191
  let rangesToDelete = deleteRanges;
215
192
  if (revisionSet === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.37.0",
3
+ "version": "0.37.2",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",