@stll/folio-core 0.37.0 → 0.37.1

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.
@@ -135,7 +135,12 @@ const hasRepresentableCommentAnchor = (item, mode) => {
135
135
  default: return item.from < item.to;
136
136
  }
137
137
  };
138
- const writesParagraphPropertyChange = (item) => item.operation.type === "setBlockParagraphProperties" || isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
138
+ const writesParagraphPropertyChange = (item) => {
139
+ if (item.operation.type === "setBlockParagraphProperties") return true;
140
+ if (item.operation.type === "splitBlock") return item.operation.firstParagraphProperties !== void 0 || item.operation.secondParagraphProperties !== void 0;
141
+ if (item.operation.type === "mergeBlockWithNext") return item.operation.mergedParagraphProperties !== void 0;
142
+ return isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
143
+ };
139
144
  const resolveReplaceBlockImpact = ({ changesText, changesStyle }) => {
140
145
  if (changesText) return changesStyle ? "textAndStyle" : "text";
141
146
  if (changesStyle) return "style";
@@ -266,6 +271,50 @@ const resolveFormattingFromStyle = ({ attrs, styleId, styleResolver }) => {
266
271
  if (attrs.alignmentFromStyle !== void 0) fallback.alignment = attrs.alignmentFromStyle;
267
272
  return Object.keys(fallback).length > 0 ? fallback : void 0;
268
273
  };
274
+ /**
275
+ * Apply one paragraph-property patch to a live node. A tracked patch stores
276
+ * the complete previous pPr: a partial snapshot cannot distinguish a property
277
+ * the change left alone from one it explicitly cleared when rejecting it.
278
+ */
279
+ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleResolver, numbering = null, revisionInfo }) => {
280
+ const attrs = expectParagraphAttrs(node);
281
+ const resolvedFormattingFromStyle = properties.styleId === void 0 ? resolveFormattingFromStyle({
282
+ attrs,
283
+ styleId: attrs.styleId,
284
+ styleResolver
285
+ }) : resolveFormattingFromStyle({
286
+ attrs,
287
+ styleId: properties.styleId,
288
+ styleResolver
289
+ });
290
+ const patch = paragraphPropertiesPatch({
291
+ node,
292
+ properties,
293
+ resolvedFormattingFromStyle,
294
+ numbering
295
+ });
296
+ if (patch === null) return {
297
+ tr,
298
+ changed: false,
299
+ revisionId: null
300
+ };
301
+ const changeInfo = revisionInfo?.();
302
+ const change = changeInfo ? {
303
+ type: "paragraphPropertyChange",
304
+ info: changeInfo,
305
+ previousFormatting: paragraphPropertiesSnapshot(node)
306
+ } : null;
307
+ const existing = attrs._propertyChanges;
308
+ return {
309
+ tr: tr.setNodeMarkup(position, void 0, {
310
+ ...node.attrs,
311
+ ...patch,
312
+ ...change ? { _propertyChanges: [...Array.isArray(existing) ? existing : [], change] } : {}
313
+ }),
314
+ changed: true,
315
+ revisionId: change?.info.id ?? null
316
+ };
317
+ };
269
318
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
270
319
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
271
320
  tr,
@@ -1291,7 +1340,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1291
1340
  const provisionalCommentId = item.commentText !== void 0 ? reserveProvisionalCommentId() : void 0;
1292
1341
  const commentMark = provisionalCommentId !== void 0 && commentType ? commentType.create({ commentId: provisionalCommentId }) : null;
1293
1342
  const stepsBefore = tr.steps.length;
1294
- let appliedRevisionIds;
1343
+ let appliedRevisionIds = [];
1295
1344
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
1296
1345
  skipped.push({
1297
1346
  id: item.operation.id,
@@ -1900,85 +1949,89 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1900
1949
  case "setBlockParagraphProperties": {
1901
1950
  const blockPosition = tr.mapping.map(item.blockFrom);
1902
1951
  const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
1903
- const liveAttrs = expectParagraphAttrs(liveBlock);
1904
- const resolvedFormattingFromStyle = item.operation.properties.styleId === void 0 ? resolveFormattingFromStyle({
1905
- attrs: liveAttrs,
1906
- styleId: liveAttrs.styleId,
1907
- styleResolver
1908
- }) : resolveFormattingFromStyle({
1909
- attrs: liveAttrs,
1910
- styleId: item.operation.properties.styleId,
1911
- styleResolver
1912
- });
1913
- const patch = paragraphPropertiesPatch({
1952
+ const appliedProperties = applyBlockParagraphProperties({
1953
+ tr,
1954
+ position: blockPosition,
1914
1955
  node: liveBlock,
1915
1956
  properties: item.operation.properties,
1916
- resolvedFormattingFromStyle,
1917
- numbering
1957
+ styleResolver,
1958
+ numbering,
1959
+ ...mode === "direct" ? {} : { revisionInfo: () => ({
1960
+ id: operationRevisionSeed++,
1961
+ author,
1962
+ date,
1963
+ ...trackedRevisionExtras
1964
+ }) }
1918
1965
  });
1919
- if (patch === null) {
1966
+ if (!appliedProperties.changed) {
1920
1967
  skipped.push({
1921
1968
  id: item.operation.id,
1922
1969
  reason: "noopOperation"
1923
1970
  });
1924
1971
  continue;
1925
1972
  }
1926
- if (mode === "direct") {
1927
- tr = tr.setNodeMarkup(blockPosition, void 0, {
1928
- ...liveBlock.attrs,
1929
- ...patch
1930
- });
1931
- break;
1932
- }
1933
- const revisionId = operationRevisionSeed++;
1934
- const change = {
1935
- type: "paragraphPropertyChange",
1936
- info: {
1937
- id: revisionId,
1938
- author,
1939
- date,
1940
- ...trackedRevisionExtras
1941
- },
1942
- previousFormatting: paragraphPropertiesSnapshot(liveBlock)
1943
- };
1944
- const existing = expectParagraphAttrs(liveBlock)._propertyChanges;
1945
- tr = tr.setNodeMarkup(blockPosition, void 0, {
1946
- ...liveBlock.attrs,
1947
- ...patch,
1948
- _propertyChanges: [...Array.isArray(existing) ? existing : [], change]
1949
- });
1950
- appliedRevisionIds = [revisionId];
1973
+ tr = appliedProperties.tr;
1974
+ if (appliedProperties.revisionId !== null) appliedRevisionIds = [appliedProperties.revisionId];
1951
1975
  break;
1952
1976
  }
1953
1977
  case "splitBlock": {
1954
1978
  if (mode === "direct") {
1955
1979
  if (item.to > item.from) tr = tr.delete(item.from, item.to);
1956
1980
  tr = tr.split(item.from);
1957
- break;
1958
- }
1959
- const revisionIdMark = operationRevisionSeed++;
1960
- const info = {
1961
- id: revisionIdMark,
1962
- author,
1963
- date,
1964
- ...trackedRevisionExtras
1965
- };
1966
- appliedRevisionIds = [revisionIdMark];
1967
- if (item.to > item.from && deletionType) {
1968
- const revisionIdSeparator = operationRevisionSeed++;
1969
- tr = tr.addMark(item.from, item.to, deletionType.create({
1970
- revisionId: revisionIdSeparator,
1981
+ } else {
1982
+ const revisionIdMark = operationRevisionSeed++;
1983
+ const info = {
1984
+ id: revisionIdMark,
1971
1985
  author,
1972
1986
  date,
1973
1987
  ...trackedRevisionExtras
1974
- }));
1975
- appliedRevisionIds = [revisionIdMark, revisionIdSeparator];
1988
+ };
1989
+ appliedRevisionIds = [revisionIdMark];
1990
+ if (item.to > item.from && deletionType) {
1991
+ const revisionIdSeparator = operationRevisionSeed++;
1992
+ tr = tr.addMark(item.from, item.to, deletionType.create({
1993
+ revisionId: revisionIdSeparator,
1994
+ author,
1995
+ date,
1996
+ ...trackedRevisionExtras
1997
+ }));
1998
+ appliedRevisionIds.push(revisionIdSeparator);
1999
+ }
2000
+ tr = tr.split(item.from);
2001
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2002
+ kind: "ins",
2003
+ info
2004
+ });
1976
2005
  }
1977
- tr = tr.split(item.from);
1978
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
1979
- kind: "ins",
1980
- info
2006
+ const allocateSplitParagraphPropertyRevisionInfo = () => ({
2007
+ id: operationRevisionSeed++,
2008
+ author,
2009
+ date,
2010
+ ...trackedRevisionExtras
1981
2011
  });
2012
+ const paragraphPropertyTargets = [{
2013
+ position: item.blockFrom,
2014
+ properties: item.operation.firstParagraphProperties
2015
+ }, {
2016
+ position: item.from + 1,
2017
+ properties: item.operation.secondParagraphProperties
2018
+ }];
2019
+ for (const { position, properties } of paragraphPropertyTargets) {
2020
+ if (!properties) continue;
2021
+ const paragraph = tr.doc.nodeAt(position);
2022
+ if (!paragraph) panic("A resolved split did not produce two paragraphs");
2023
+ const appliedProperties = applyBlockParagraphProperties({
2024
+ tr,
2025
+ position,
2026
+ node: paragraph,
2027
+ properties,
2028
+ styleResolver,
2029
+ numbering,
2030
+ ...mode === "direct" ? {} : { revisionInfo: allocateSplitParagraphPropertyRevisionInfo }
2031
+ });
2032
+ tr = appliedProperties.tr;
2033
+ if (appliedProperties.revisionId !== null) appliedRevisionIds.push(appliedProperties.revisionId);
2034
+ }
1982
2035
  break;
1983
2036
  }
1984
2037
  case "mergeBlockWithNext": {
@@ -1987,30 +2040,50 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1987
2040
  if (mode === "direct") {
1988
2041
  if (separator.length > 0) tr = tr.insertText(separator, insertAt);
1989
2042
  tr = tr.join(item.blockTo + separator.length);
1990
- break;
2043
+ } else {
2044
+ const revisionIdMark = operationRevisionSeed++;
2045
+ appliedRevisionIds = [revisionIdMark];
2046
+ if (separator.length > 0 && insertionType) {
2047
+ const revisionIdSeparator = operationRevisionSeed++;
2048
+ tr = tr.insertText(separator, insertAt);
2049
+ tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
2050
+ revisionId: revisionIdSeparator,
2051
+ author,
2052
+ date,
2053
+ ...trackedRevisionExtras
2054
+ }));
2055
+ appliedRevisionIds.push(revisionIdSeparator);
2056
+ }
2057
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2058
+ kind: "del",
2059
+ info: {
2060
+ id: revisionIdMark,
2061
+ author,
2062
+ date,
2063
+ ...trackedRevisionExtras
2064
+ }
2065
+ });
1991
2066
  }
1992
- const revisionIdMark = operationRevisionSeed++;
1993
- appliedRevisionIds = [revisionIdMark];
1994
- if (separator.length > 0 && insertionType) {
1995
- const revisionIdSeparator = operationRevisionSeed++;
1996
- tr = tr.insertText(separator, insertAt);
1997
- tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
1998
- revisionId: revisionIdSeparator,
1999
- author,
2000
- date,
2001
- ...trackedRevisionExtras
2002
- }));
2003
- appliedRevisionIds = [revisionIdMark, revisionIdSeparator];
2067
+ if (item.operation.mergedParagraphProperties) {
2068
+ const mergedParagraph = tr.doc.nodeAt(item.blockFrom);
2069
+ if (!mergedParagraph) panic("A resolved merge did not produce a paragraph");
2070
+ const appliedProperties = applyBlockParagraphProperties({
2071
+ tr,
2072
+ position: item.blockFrom,
2073
+ node: mergedParagraph,
2074
+ properties: item.operation.mergedParagraphProperties,
2075
+ styleResolver,
2076
+ numbering,
2077
+ ...mode === "direct" ? {} : { revisionInfo: () => ({
2078
+ id: operationRevisionSeed++,
2079
+ author,
2080
+ date,
2081
+ ...trackedRevisionExtras
2082
+ }) }
2083
+ });
2084
+ tr = appliedProperties.tr;
2085
+ if (appliedProperties.revisionId !== null) appliedRevisionIds.push(appliedProperties.revisionId);
2004
2086
  }
2005
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2006
- kind: "del",
2007
- info: {
2008
- id: revisionIdMark,
2009
- author,
2010
- date,
2011
- ...trackedRevisionExtras
2012
- }
2013
- });
2014
2087
  break;
2015
2088
  }
2016
2089
  case "commentOnBlock":
@@ -2043,7 +2116,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2043
2116
  applied.push({
2044
2117
  id: item.operation.id,
2045
2118
  ...committedCommentId !== void 0 && { commentId: committedCommentId },
2046
- ...appliedRevisionIds !== void 0 && appliedRevisionIds[0] !== void 0 && {
2119
+ ...appliedRevisionIds[0] !== void 0 && {
2047
2120
  revisionId: appliedRevisionIds[0],
2048
2121
  revisionIds: appliedRevisionIds
2049
2122
  },
@@ -312,6 +312,16 @@ type FolioAIEditOperation = FolioAIEditReviewMeta & {
312
312
  */
313
313
  separator?: string;
314
314
  blockId: string;
315
+ /**
316
+ * Paragraph properties for the first result. Omitted properties keep
317
+ * the source paragraph's value.
318
+ */
319
+ firstParagraphProperties?: FolioAIBlockParagraphProperties;
320
+ /**
321
+ * Paragraph properties for the second result. Omitted properties keep
322
+ * the source paragraph's value.
323
+ */
324
+ secondParagraphProperties?: FolioAIBlockParagraphProperties;
315
325
  } |
316
326
  /**
317
327
  * Add a whole table next to the anchor block, its rows marked inserted in
@@ -377,6 +387,11 @@ type FolioAIEditOperation = FolioAIEditReviewMeta & {
377
387
  */
378
388
  separator?: string;
379
389
  blockId: string;
390
+ /**
391
+ * Paragraph properties for the joined result. Omitted properties keep
392
+ * the first paragraph's value.
393
+ */
394
+ mergedParagraphProperties?: FolioAIBlockParagraphProperties;
380
395
  } | {
381
396
  id: string;
382
397
  type: "commentOnBlock";
@@ -612,6 +612,10 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
612
612
  const paragraphMarkPlan = paragraphMarkPlans.get(stepIndex);
613
613
  if (paragraphMarkPlan?.type === "split") {
614
614
  const { baseBlock, revisedBlocks: splitInto, offset, separator } = paragraphMarkPlan;
615
+ const firstParagraphFormatting = changedFolioContentParagraphFormatting(baseBlock, splitInto[0]);
616
+ const secondParagraphFormatting = changedFolioContentParagraphFormatting(baseBlock, splitInto[1]);
617
+ const firstParagraphProperties = firstParagraphFormatting ? toFolioAIBlockParagraphProperties(firstParagraphFormatting) : void 0;
618
+ const secondParagraphProperties = secondParagraphFormatting ? toFolioAIBlockParagraphProperties(secondParagraphFormatting) : void 0;
615
619
  changes.push({
616
620
  kind: "split",
617
621
  location: locationOf(story, baseBlock),
@@ -619,17 +623,35 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
619
623
  targetBlockIds: splitInto.map(({ id }) => id),
620
624
  text: baseBlock.text
621
625
  });
626
+ if (firstParagraphProperties) changes.push({
627
+ kind: "paragraph-format",
628
+ location: locationOf(story, baseBlock),
629
+ baseBlockId: baseBlock.id,
630
+ targetBlockId: splitInto[0].id,
631
+ properties: firstParagraphProperties
632
+ });
633
+ if (secondParagraphProperties) changes.push({
634
+ kind: "paragraph-format",
635
+ location: locationOf(story, baseBlock),
636
+ baseBlockId: baseBlock.id,
637
+ targetBlockId: splitInto[1].id,
638
+ properties: secondParagraphProperties
639
+ });
622
640
  operations.push({
623
641
  id: nextOperationId(),
624
642
  type: "splitBlock",
625
643
  blockId: baseBlock.id,
626
644
  offset,
627
- ...separator.length > 0 && { separator }
645
+ ...separator.length > 0 && { separator },
646
+ ...firstParagraphProperties && { firstParagraphProperties },
647
+ ...secondParagraphProperties && { secondParagraphProperties }
628
648
  });
629
649
  continue;
630
650
  }
631
651
  if (paragraphMarkPlan?.type === "merge") {
632
652
  const { baseBlocks, revisedBlock: targetBlock, separator } = paragraphMarkPlan;
653
+ const mergedParagraphFormatting = changedFolioContentParagraphFormatting(baseBlocks[0], targetBlock);
654
+ const mergedParagraphProperties = mergedParagraphFormatting ? toFolioAIBlockParagraphProperties(mergedParagraphFormatting) : void 0;
633
655
  changes.push({
634
656
  kind: "merge",
635
657
  location: locationOf(story, baseBlocks[0]),
@@ -637,11 +659,19 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
637
659
  targetBlockId: targetBlock.id,
638
660
  text: targetBlock.text
639
661
  });
662
+ if (mergedParagraphProperties) changes.push({
663
+ kind: "paragraph-format",
664
+ location: locationOf(story, baseBlocks[0]),
665
+ baseBlockId: baseBlocks[0].id,
666
+ targetBlockId: targetBlock.id,
667
+ properties: mergedParagraphProperties
668
+ });
640
669
  operations.push({
641
670
  id: nextOperationId(),
642
671
  type: "mergeBlockWithNext",
643
672
  blockId: baseBlocks[0].id,
644
- ...separator.length > 0 && { separator }
673
+ ...separator.length > 0 && { separator },
674
+ ...mergedParagraphProperties && { mergedParagraphProperties }
645
675
  });
646
676
  continue;
647
677
  }
@@ -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: ("image" | "glyphRun" | "rect" | "line" | "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.1",
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",