@stll/folio-core 0.47.0 → 0.47.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.
@@ -57,6 +57,8 @@ type ApplyFolioAIEditOperationsOptions = {
57
57
  * still describes a table by its cell texts.
58
58
  */
59
59
  tableTemplates?: FolioTableTemplates;
60
+ /** What a replacement does with a background the text it replaces carries. */
61
+ replacementBackground?: FolioReplacementBackground;
60
62
  };
61
63
  /**
62
64
  * How an apply batch cuts the redline for a replacement. Word-level by
@@ -68,6 +70,20 @@ type ApplyFolioAIEditOperationsOptions = {
68
70
  type FolioWordDiffOptions = {
69
71
  granularity?: WordDiffGranularity;
70
72
  };
73
+ /**
74
+ * What a replacement does with a highlight or `w:shd` the text it replaces
75
+ * carries.
76
+ *
77
+ * `clear` is the authoring default: text typed over a highlighted placeholder
78
+ * is new text, and keeping the marker that said "fill this in" would carry it
79
+ * into the finished document. `keep` is for a caller that states the
80
+ * replacement's own run properties rather than letting it inherit them — a
81
+ * comparison reproducing a revised document holds the properties that document
82
+ * has, and clearing them first records a run-property change it takes straight
83
+ * back, which reaches the reader as a revision whose before and after are the
84
+ * same.
85
+ */
86
+ type FolioReplacementBackground = "clear" | "keep";
71
87
  /**
72
88
  * An apply result plus where the batch left the revision-id counter.
73
89
  *
@@ -88,4 +104,4 @@ type FolioAIEditApplyOutcome = FolioAIEditApplyResult & {
88
104
  declare const applyFolioAIEditOperations: (options: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyOutcome;
89
105
  declare const previewFolioAIEditOperations: (options: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyOutcome;
90
106
  //#endregion
91
- export { FolioAIEditApplyOutcome, FolioAIEditView, FolioRevisionStamp, FolioWordDiffOptions, applyFolioAIEditOperations, previewFolioAIEditOperations };
107
+ export { FolioAIEditApplyOutcome, FolioAIEditView, FolioReplacementBackground, FolioRevisionStamp, FolioWordDiffOptions, applyFolioAIEditOperations, previewFolioAIEditOperations };
@@ -578,8 +578,8 @@ const formattingChangesForAuthoredFormatting = (authoredFormatting, formatting)
578
578
  }
579
579
  return changes;
580
580
  };
581
- const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
582
- if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return {
581
+ const clearReplacementBackground = ({ tr, schema, from, to, mode, replacementBackground, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
582
+ if (!(replacementBackground === "clear" && [schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType)))) return {
583
583
  type: "applied",
584
584
  transaction: tr,
585
585
  revisionIds: [],
@@ -1241,7 +1241,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1241
1241
  nextRevisionId
1242
1242
  };
1243
1243
  };
1244
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates }) => {
1244
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear" }) => {
1245
1245
  const applied = [];
1246
1246
  const skipped = [];
1247
1247
  const normalizations = [];
@@ -1532,6 +1532,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1532
1532
  from: item.from,
1533
1533
  to: item.to,
1534
1534
  mode,
1535
+ replacementBackground,
1535
1536
  revisionIdSeed: revisionIdBackgroundSeed,
1536
1537
  author,
1537
1538
  date,
@@ -1659,6 +1660,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1659
1660
  from: item.from,
1660
1661
  to: item.to,
1661
1662
  mode,
1663
+ replacementBackground,
1662
1664
  revisionIdSeed: revisionIdBackgroundSeed,
1663
1665
  author,
1664
1666
  date,
@@ -1,7 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { FolioTableTemplates } from "./table-template.js";
3
3
  import { FolioAIBlock, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js";
4
- import { FolioRevisionStamp, FolioWordDiffOptions } from "./apply.js";
4
+ import { FolioReplacementBackground, FolioRevisionStamp, FolioWordDiffOptions } from "./apply.js";
5
5
  import { ImportReferencedStyleDefinitionsResult } from "../compare/style-resources.js";
6
6
  import { InlineProvenanceTargetOptions } from "../compare/inline-provenance.js";
7
7
  import { MatchInlineAtomsOptions } from "../compare/inline-atoms.js";
@@ -124,6 +124,13 @@ type FolioApplyDocumentOperationsToStoryOptions = FolioApplyDocumentOperationsOp
124
124
  * every table, row and cell property on the way.
125
125
  */
126
126
  tableTemplates?: FolioTableTemplates;
127
+ /**
128
+ * What a replacement in the batch does with a highlight or `w:shd` the text
129
+ * it replaces carries. Clearing it is the authoring default; a caller that
130
+ * states the replacement's own run properties — `compareDocx` reproducing
131
+ * the target document — keeps them instead.
132
+ */
133
+ replacementBackground?: FolioReplacementBackground;
127
134
  };
128
135
  type FolioHeaderFooterStoryHandle = Extract<FolioEditableDocumentStoryHandle, {
129
136
  type: "header" | "footer";
@@ -382,7 +389,7 @@ declare class FolioDocxReviewer {
382
389
  */
383
390
  applyDocumentOperations(batch: FolioDocumentOperationBatch, options?: FolioApplyDocumentOperationsOptions): FolioDocumentOperationResult;
384
391
  /** Apply a versioned operation batch to one editable document story. */
385
- applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates }: FolioApplyDocumentOperationsToStoryOptions): FolioDocumentOperationResult;
392
+ applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates, replacementBackground }: FolioApplyDocumentOperationsToStoryOptions): FolioDocumentOperationResult;
386
393
  private applyDocumentOperationsInternal;
387
394
  /** Undo the latest unchanged document-operation batch and its created comments. */
388
395
  undoDocumentOperations(undoHandle: FolioDocumentOperationUndoHandle): FolioDocumentOperationUndoResult;
@@ -782,7 +782,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
782
782
  });
783
783
  }
784
784
  /** Apply a versioned operation batch to one editable document story. */
785
- applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates }) {
785
+ applyDocumentOperationsToStory({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates, replacementBackground }) {
786
786
  return this.applyDocumentOperationsInternal({
787
787
  story,
788
788
  batch,
@@ -790,10 +790,11 @@ var FolioDocxReviewer = class FolioDocxReviewer {
790
790
  ...revisionStamp !== void 0 && { revisionStamp },
791
791
  ...wordDiff !== void 0 && { wordDiff },
792
792
  ...tableTemplates !== void 0 && { tableTemplates },
793
+ ...replacementBackground !== void 0 && { replacementBackground },
793
794
  createUndoEntry: true
794
795
  });
795
796
  }
796
- applyDocumentOperationsInternal({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates, createUndoEntry }) {
797
+ applyDocumentOperationsInternal({ story, batch, snapshot, revisionStamp, wordDiff, tableTemplates, replacementBackground, createUndoEntry }) {
797
798
  const beforeState = this.requireEditableStoryState(story);
798
799
  const createdCommentsLengthBefore = this.createdComments.length;
799
800
  const view = {
@@ -811,6 +812,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
811
812
  ...revisionStamp !== void 0 && { revisionStamp },
812
813
  ...wordDiff !== void 0 && { wordDiff },
813
814
  ...tableTemplates !== void 0 && { tableTemplates },
815
+ ...replacementBackground !== void 0 && { replacementBackground },
814
816
  createCommentId: (text) => {
815
817
  const comment = createReviewerComment(this.nextCommentId(), text, this.author);
816
818
  this.createdComments.push(comment);
@@ -573,7 +573,8 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, revisionFormat,
573
573
  mode: "tracked-changes",
574
574
  operations: plan.operations
575
575
  },
576
- tableTemplates: resolveTableTemplates(targetTables, plan.tableTemplates)
576
+ tableTemplates: resolveTableTemplates(targetTables, plan.tableTemplates),
577
+ replacementBackground: "keep"
577
578
  });
578
579
  if (nextRevisionId === void 0) panic("The applier did not report where it left the revision-id counter", { story: pair.baseStory });
579
580
  idSeed = nextRevisionId;
@@ -482,6 +482,37 @@ const groupFolioContentTableRows = (blocks) => {
482
482
  }
483
483
  return [...rows.values()];
484
484
  };
485
+ /**
486
+ * Which uninterrupted run of its own cell each block belongs to.
487
+ *
488
+ * A cell holds paragraphs and nested tables, and the nested table splits the
489
+ * cell's paragraphs into runs: the ones before it and the ones after it are
490
+ * separate sequences, because nothing moves a block from one side of a table
491
+ * to the other. Aligned as one sequence they pair across it, and the plan that
492
+ * follows can neither move the base paragraph past the nested table nor delete
493
+ * the one it left behind when that paragraph ends the cell. So the runs are
494
+ * aligned separately, the way rows and cells already are.
495
+ *
496
+ * Document order is the whole input: a cell's blocks are contiguous until a
497
+ * block of another cell interrupts them, and inside one table segment the only
498
+ * cell that can interrupt another is one of a table nested in it.
499
+ */
500
+ const cellRunOrdinals = (blocks) => {
501
+ const ordinals = /* @__PURE__ */ new Map();
502
+ const runByCell = /* @__PURE__ */ new Map();
503
+ let previousKey = null;
504
+ for (const block of blocks) {
505
+ const { table } = block;
506
+ if (!table) continue;
507
+ const key = `${String(table.tableIndex)}:${String(table.rowIndex)}:${String(table.cellIndex)}`;
508
+ const seen = runByCell.get(key) ?? -1;
509
+ const run = key === previousKey ? seen : seen + 1;
510
+ runByCell.set(key, run);
511
+ ordinals.set(block, run);
512
+ previousKey = key;
513
+ }
514
+ return ordinals;
515
+ };
485
516
  const groupTables = (blocks) => {
486
517
  const tables = /* @__PURE__ */ new Map();
487
518
  for (const block of blocks) {
@@ -1013,7 +1044,7 @@ const alignProfiledContentSequence = ({ base, revised, workSession, canPair = ()
1013
1044
  return aligned;
1014
1045
  };
1015
1046
  const rowLocation = (row) => row.at(0)?.table ?? null;
1016
- const alignRowCells = ({ baseRow, revisedRow, workSession, moveScopeContext, stableIdMismatch, idStability, baseColumnKeys, revisedColumnKeys }) => {
1047
+ const alignRowCells = ({ baseRow, revisedRow, workSession, moveScopeContext, stableIdMismatch, idStability, baseColumnKeys, revisedColumnKeys, cellRuns }) => {
1017
1048
  const byCell = (row, columnKeys) => {
1018
1049
  const cells = /* @__PURE__ */ new Map();
1019
1050
  for (const block of row) {
@@ -1024,47 +1055,62 @@ const alignRowCells = ({ baseRow, revisedRow, workSession, moveScopeContext, sta
1024
1055
  if (alignedColumn === void 0) return panic("A paired table cell has no aligned grid column", { gridColumnIndex: table.gridColumnIndex });
1025
1056
  cellIndex = alignedColumn;
1026
1057
  }
1027
- const blocks = cells.get(cellIndex);
1058
+ const runs = cells.get(cellIndex) ?? /* @__PURE__ */ new Map();
1059
+ cells.set(cellIndex, runs);
1060
+ const run = cellRuns.get(block) ?? 0;
1061
+ const blocks = runs.get(run);
1028
1062
  if (blocks) blocks.push(block);
1029
- else cells.set(cellIndex, [block]);
1063
+ else runs.set(run, [block]);
1030
1064
  }
1031
1065
  return cells;
1032
1066
  };
1067
+ const ascending = (left, right) => left - right;
1033
1068
  const baseCells = byCell(baseRow, baseColumnKeys);
1034
1069
  const revisedCells = byCell(revisedRow, revisedColumnKeys);
1035
- const cellIndexes = [.../* @__PURE__ */ new Set([...baseCells.keys(), ...revisedCells.keys()])].toSorted((left, right) => left - right);
1070
+ const cellIndexes = [.../* @__PURE__ */ new Set([...baseCells.keys(), ...revisedCells.keys()])].toSorted(ascending);
1036
1071
  const steps = [];
1037
1072
  for (const cellIndex of cellIndexes) {
1038
- const baseBlocks = baseCells.get(cellIndex) ?? [];
1039
- const revisedBlocks = revisedCells.get(cellIndex) ?? [];
1040
- const aligned = alignFolioContentBlocksInScope(baseBlocks, revisedBlocks, {
1073
+ const baseRuns = baseCells.get(cellIndex) ?? /* @__PURE__ */ new Map();
1074
+ const revisedRuns = revisedCells.get(cellIndex) ?? /* @__PURE__ */ new Map();
1075
+ const runOrdinals = [.../* @__PURE__ */ new Set([...baseRuns.keys(), ...revisedRuns.keys()])].toSorted(ascending);
1076
+ for (const run of runOrdinals) steps.push(...alignCellRun({
1077
+ baseBlocks: baseRuns.get(run) ?? [],
1078
+ revisedBlocks: revisedRuns.get(run) ?? [],
1041
1079
  workSession,
1042
- idStability,
1080
+ moveScopeContext,
1043
1081
  stableIdMismatch,
1044
- structuralScope: "pairedTableCell"
1045
- }).flatMap((event) => {
1046
- if (event.type === "pair" && !contentBlocksShareContainerPath(event.baseBlock, event.revisedBlock)) return [{
1047
- type: "baseOnly",
1048
- block: event.baseBlock
1049
- }, {
1050
- type: "revisedOnly",
1051
- block: event.revisedBlock
1052
- }];
1053
- return [event];
1054
- });
1055
- const bucketByContainerPath = /* @__PURE__ */ new Map();
1056
- const bucketForBlock = (block) => {
1057
- const path = containerPathKeyOf(block);
1058
- const existing = bucketByContainerPath.get(path);
1059
- if (existing !== void 0) return existing;
1060
- const bucket = moveScopeContext.nextTableCellBucket++;
1061
- bucketByContainerPath.set(path, bucket);
1062
- return bucket;
1063
- };
1064
- steps.push(...scopedAlignmentSteps(aligned, moveScopeContext, bucketForBlock));
1082
+ idStability
1083
+ }));
1065
1084
  }
1066
1085
  return steps;
1067
1086
  };
1087
+ const alignCellRun = ({ baseBlocks, revisedBlocks, workSession, moveScopeContext, stableIdMismatch, idStability }) => {
1088
+ const aligned = alignFolioContentBlocksInScope(baseBlocks, revisedBlocks, {
1089
+ workSession,
1090
+ idStability,
1091
+ stableIdMismatch,
1092
+ structuralScope: "pairedTableCell"
1093
+ }).flatMap((event) => {
1094
+ if (event.type === "pair" && !contentBlocksShareContainerPath(event.baseBlock, event.revisedBlock)) return [{
1095
+ type: "baseOnly",
1096
+ block: event.baseBlock
1097
+ }, {
1098
+ type: "revisedOnly",
1099
+ block: event.revisedBlock
1100
+ }];
1101
+ return [event];
1102
+ });
1103
+ const bucketByContainerPath = /* @__PURE__ */ new Map();
1104
+ const bucketForBlock = (block) => {
1105
+ const path = containerPathKeyOf(block);
1106
+ const existing = bucketByContainerPath.get(path);
1107
+ if (existing !== void 0) return existing;
1108
+ const bucket = moveScopeContext.nextTableCellBucket++;
1109
+ bucketByContainerPath.set(path, bucket);
1110
+ return bucket;
1111
+ };
1112
+ return scopedAlignmentSteps(aligned, moveScopeContext, bucketForBlock);
1113
+ };
1068
1114
  const pairTableRows = ({ baseRows, revisedRows, workSession, idStability }) => {
1069
1115
  const profile = (row) => ({
1070
1116
  item: row,
@@ -1097,7 +1143,7 @@ const pairTableRows = ({ baseRows, revisedRows, workSession, idStability }) => {
1097
1143
  }
1098
1144
  });
1099
1145
  };
1100
- const alignTableRows = ({ rows, workSession, moveScopeContext, stableIdMismatch, idStability, baseColumnKeys, revisedColumnKeys }) => {
1146
+ const alignTableRows = ({ rows, workSession, moveScopeContext, stableIdMismatch, idStability, baseColumnKeys, revisedColumnKeys, cellRuns }) => {
1101
1147
  const steps = [];
1102
1148
  const pushRow = (row, side) => {
1103
1149
  const location = rowLocation(row);
@@ -1123,7 +1169,8 @@ const alignTableRows = ({ rows, workSession, moveScopeContext, stableIdMismatch,
1123
1169
  stableIdMismatch,
1124
1170
  idStability,
1125
1171
  baseColumnKeys,
1126
- revisedColumnKeys
1172
+ revisedColumnKeys,
1173
+ cellRuns
1127
1174
  }));
1128
1175
  break;
1129
1176
  default: panic("Unhandled table row alignment", { alignment });
@@ -1148,7 +1195,7 @@ const rowCellSpansEqual = (baseRow, revisedRow) => {
1148
1195
  });
1149
1196
  };
1150
1197
  const rowHasVerticalSpan = (row) => row.some(({ table }) => table !== void 0 && table.rowSpan > 1);
1151
- const buildTablePlan = ({ baseBlocks, revisedBlocks, workSession, moveScopeContext, stableIdMismatch, idStability }) => {
1198
+ const buildTablePlan = ({ baseBlocks, revisedBlocks, workSession, moveScopeContext, stableIdMismatch, idStability, cellRuns }) => {
1152
1199
  const resolveIdStability = idStability ?? folioContentIdStability;
1153
1200
  const columns = alignTableColumns(baseBlocks, revisedBlocks);
1154
1201
  const rows = pairTableRows({
@@ -1169,13 +1216,15 @@ const buildTablePlan = ({ baseBlocks, revisedBlocks, workSession, moveScopeConte
1169
1216
  stableIdMismatch,
1170
1217
  idStability: resolveIdStability,
1171
1218
  baseColumnKeys: columns?.baseColumnKeys,
1172
- revisedColumnKeys: columns?.revisedColumnKeys
1219
+ revisedColumnKeys: columns?.revisedColumnKeys,
1220
+ cellRuns
1173
1221
  })]
1174
1222
  };
1175
1223
  };
1176
1224
  const buildTableSegmentPlan = ({ baseBlocks, revisedBlocks, workSession, moveScopeContext, stableIdMismatch, idStability }) => {
1177
1225
  const baseTables = groupTables(baseBlocks);
1178
1226
  const revisedTables = groupTables(revisedBlocks);
1227
+ const cellRuns = new Map([...cellRunOrdinals(baseBlocks), ...cellRunOrdinals(revisedBlocks)]);
1179
1228
  const resolveIdStability = idStability ?? folioContentIdStability;
1180
1229
  const profile = (blocks) => ({
1181
1230
  item: blocks,
@@ -1197,7 +1246,8 @@ const buildTableSegmentPlan = ({ baseBlocks, revisedBlocks, workSession, moveSco
1197
1246
  workSession,
1198
1247
  moveScopeContext,
1199
1248
  stableIdMismatch,
1200
- idStability: resolveIdStability
1249
+ idStability: resolveIdStability,
1250
+ cellRuns
1201
1251
  });
1202
1252
  steps.push(...table.steps);
1203
1253
  representable &&= table.representable;
@@ -1,7 +1,7 @@
1
1
  import { document_d_exports } from "./types/document.js";
2
2
  import { FolioTableTemplates } from "./ai-edits/table-template.js";
3
3
  import { FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditNormalization, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAITextRangeHandle } from "./ai-edits/types.js";
4
- import { FolioAIEditView, FolioRevisionStamp, FolioWordDiffOptions } from "./ai-edits/apply.js";
4
+ import { FolioAIEditView, FolioReplacementBackground, FolioRevisionStamp, FolioWordDiffOptions } from "./ai-edits/apply.js";
5
5
  //#region src/document-operations.d.ts
6
6
  declare const FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION: 1;
7
7
  /** Direct paragraph-alignment values accepted by the operation contract. */
@@ -282,7 +282,14 @@ type ApplyFolioDocumentOperationsOptions = {
282
282
  * copying the target document's — hands the whole thing over instead.
283
283
  */
284
284
  tableTemplates?: FolioTableTemplates;
285
+ /**
286
+ * What a replacement in the batch does with a highlight or `w:shd` the text
287
+ * it replaces carries. Clearing it is the authoring default; a caller that
288
+ * states the replacement's own run properties — a comparison reproducing the
289
+ * revised document — keeps them instead.
290
+ */
291
+ replacementBackground?: FolioReplacementBackground;
285
292
  };
286
- declare const applyFolioDocumentOperations: ({ view, snapshot, batch, story, author, createCommentId, createUndoHandle, revisionStamp, wordDiff, tableTemplates }: ApplyFolioDocumentOperationsOptions) => FolioDocumentOperationResult;
293
+ declare const applyFolioDocumentOperations: ({ view, snapshot, batch, story, author, createCommentId, createUndoHandle, revisionStamp, wordDiff, tableTemplates, replacementBackground }: ApplyFolioDocumentOperationsOptions) => FolioDocumentOperationResult;
287
294
  //#endregion
288
295
  export { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_BATCH_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_KEYS_BY_TYPE, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_LINE_SPACING_RULE_VALUES, FOLIO_PAGE_BREAK_CLEAR_VALUES, FOLIO_PARAGRAPH_ALIGNMENT_VALUES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationBatchPrecondition, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationQueuedOperation, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationResultBase, FolioDocumentOperationStatus, FolioDocumentOperationStory, FolioDocumentOperationType, FolioDocumentOperationUndoFailureReason, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch };
@@ -1183,7 +1183,7 @@ const getFolioDocumentOperationReceiptsForStory = ({ operations, applied, story
1183
1183
  });
1184
1184
  return receipts;
1185
1185
  };
1186
- const applyFolioDocumentOperations = ({ view, snapshot, batch, story = "main", author, createCommentId, createUndoHandle, revisionStamp, wordDiff, tableTemplates }) => {
1186
+ const applyFolioDocumentOperations = ({ view, snapshot, batch, story = "main", author, createCommentId, createUndoHandle, revisionStamp, wordDiff, tableTemplates, replacementBackground }) => {
1187
1187
  const parsedBatch = parseFolioDocumentOperationBatch(batch);
1188
1188
  const apply = ({ targetView, targetCreateCommentId = createCommentId, preview = false }) => {
1189
1189
  return (preview ? previewFolioAIEditOperations : applyFolioAIEditOperations)({
@@ -1195,7 +1195,8 @@ const applyFolioDocumentOperations = ({ view, snapshot, batch, story = "main", a
1195
1195
  ...targetCreateCommentId !== void 0 && { createCommentId: targetCreateCommentId },
1196
1196
  ...revisionStamp !== void 0 && { revisionStamp },
1197
1197
  ...wordDiff !== void 0 && { wordDiff },
1198
- ...tableTemplates !== void 0 && { tableTemplates }
1198
+ ...tableTemplates !== void 0 && { tableTemplates },
1199
+ ...replacementBackground !== void 0 && { replacementBackground }
1199
1200
  });
1200
1201
  };
1201
1202
  const preview = () => apply({
@@ -52,7 +52,11 @@ function parseFootnoteProperties(element) {
52
52
  const numFmtEl = findChild(element, "w", "numFmt");
53
53
  if (numFmtEl) {
54
54
  const numFmt = parseNumberFormat(getAttribute(numFmtEl, "w", "val"));
55
- if (numFmt !== void 0) props.numFmt = numFmt;
55
+ if (numFmt !== void 0) {
56
+ props.numFmt = numFmt;
57
+ const format = getAttribute(numFmtEl, "w", "format");
58
+ if (format !== null) props.numFmtFormat = format;
59
+ }
56
60
  }
57
61
  const numStartEl = findChild(element, "w", "numStart");
58
62
  if (numStartEl) {
@@ -81,7 +85,11 @@ function parseEndnoteProperties(element) {
81
85
  const numFmtEl = findChild(element, "w", "numFmt");
82
86
  if (numFmtEl) {
83
87
  const numFmt = parseNumberFormat(getAttribute(numFmtEl, "w", "val"));
84
- if (numFmt !== void 0) props.numFmt = numFmt;
88
+ if (numFmt !== void 0) {
89
+ props.numFmt = numFmt;
90
+ const format = getAttribute(numFmtEl, "w", "format");
91
+ if (format !== null) props.numFmtFormat = format;
92
+ }
85
93
  }
86
94
  const numStartEl = findChild(element, "w", "numStart");
87
95
  if (numStartEl) {
@@ -24,7 +24,7 @@ type NumberingMap = {
24
24
  * @returns NumberingMap with definitions and helper functions
25
25
  */
26
26
  declare function parseNumbering(numberingXml: string | null): NumberingMap;
27
- /** A `w:numFmt` as the model holds it: the token, plus what `custom` defers to. */
27
+ /** A `w:numFmt` as the model holds it: its token and authored format metadata. */
28
28
  type ResolvedNumFmt = Pick<document_d_exports.ListLevel, "numFmt" | "numFmtFormat">;
29
29
  /**
30
30
  * The width a `custom` format's zero-padded first token counts to, or
@@ -35,14 +35,6 @@ type ResolvedNumFmt = Pick<document_d_exports.ListLevel, "numFmt" | "numFmtForma
35
35
  * way; the width is the digit count of the first token, clamped to five.
36
36
  */
37
37
  declare const customNumberFormatPadWidth: (format: string | undefined) => number | undefined;
38
- /**
39
- * The `@w:format` a level's `w:numFmt` writes, which only `custom` carries.
40
- *
41
- * `custom` is the reserved member that defers to another attribute, so the
42
- * comparison lives here, in the module the reserved-value registry names as
43
- * its owner, rather than in the serializer that needs the answer.
44
- */
45
- declare const customNumberFormatOf: (level: ResolvedNumFmt) => string | undefined;
46
38
  /** What the marker renderer counts a level in; `custom` decides by pad width. */
47
39
  declare const counterFormatOf: (level: ResolvedNumFmt) => document_d_exports.CounterFormat;
48
40
  /** {@link MARKER_ALIGNMENT} for a level that may not state a justification. */
@@ -92,4 +84,4 @@ declare function isBulletLevel(level: document_d_exports.ListLevel): boolean;
92
84
  /** Whether a numbering level reserves horizontal space for a visible marker. */
93
85
  declare const numberingLevelHasMarkerSlot: (level: Pick<document_d_exports.ListLevel, "numFmt">) => boolean;
94
86
  //#endregion
95
- export { NumberingMap, computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatOf, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
87
+ export { NumberingMap, computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
@@ -248,16 +248,16 @@ function parseListLevel(element) {
248
248
  * Resolve a `<w:numFmt>` element, or null when it is absent or carries a token
249
249
  * outside `ST_NumberFormat` (an mc:Fallback can then supply the rendering).
250
250
  *
251
- * `custom` stays `custom`, with its `@w:format` beside it. It used to be
252
- * decoded here into a synthetic `decimalZero{3,4,5}` the enumeration does not
253
- * declare, which the serializer wrote back as a `w:val` no consumer can read.
254
- * The pad width belongs to the renderer, not to the model.
251
+ * `@w:format` stays beside every admitted token exactly as authored. `custom`
252
+ * used to be decoded here into a synthetic `decimalZero{3,4,5}` the
253
+ * enumeration does not declare, which the serializer wrote back as a `w:val`
254
+ * no consumer can read. The pad-width interpretation belongs to the renderer,
255
+ * not to the model; preservation belongs to every schema-valid `CT_NumFmt`.
255
256
  */
256
257
  function resolveNumFmt(numFmtEl) {
257
258
  if (!numFmtEl) return null;
258
259
  const numFmt = narrowEnum(getAttribute(numFmtEl, "w", "val"), NumberFormatSchema);
259
260
  if (numFmt === void 0) return null;
260
- if (numFmt !== "custom") return { numFmt };
261
261
  const format = getAttribute(numFmtEl, "w", "format");
262
262
  return format === null ? { numFmt } : {
263
263
  numFmt,
@@ -293,14 +293,6 @@ const customNumberFormatPadWidth = (format) => {
293
293
  if (!/^0+1$/u.test(firstToken)) return;
294
294
  return Math.min(firstToken.length, 5);
295
295
  };
296
- /**
297
- * The `@w:format` a level's `w:numFmt` writes, which only `custom` carries.
298
- *
299
- * `custom` is the reserved member that defers to another attribute, so the
300
- * comparison lives here, in the module the reserved-value registry names as
301
- * its owner, rather than in the serializer that needs the answer.
302
- */
303
- const customNumberFormatOf = (level) => level.numFmt === "custom" ? level.numFmtFormat : void 0;
304
296
  /** What the marker renderer counts a level in; `custom` decides by pad width. */
305
297
  const counterFormatOf = (level) => {
306
298
  if (level.numFmt !== "custom") return level.numFmt;
@@ -511,4 +503,4 @@ function isBulletLevel(level) {
511
503
  /** Whether a numbering level reserves horizontal space for a visible marker. */
512
504
  const numberingLevelHasMarkerSlot = (level) => level.numFmt !== "none";
513
505
  //#endregion
514
- export { computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatOf, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
506
+ export { computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
@@ -16,7 +16,7 @@ import { resolveThemeFontRef } from "./themeParser.js";
16
16
  import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
17
17
  import { captureVerbatimXml } from "./verbatimCapture.js";
18
18
  import { parseVmlImageContent, shouldPreserveRawVmlPict } from "./vmlImageParser.js";
19
- import { WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getNamespaceUri, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, selectAlternateContentBranch } from "./xmlParser.js";
19
+ import { WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getNamespaceUri, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, selectAlternateContentBranch } from "./xmlParser.js";
20
20
  import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
21
21
  //#region src/docx/runParser.ts
22
22
  /**
@@ -486,19 +486,25 @@ function parseSymbolContent(element) {
486
486
  * Parse footnote reference (w:footnoteReference)
487
487
  */
488
488
  function parseFootnoteReference(element) {
489
- return {
489
+ const reference = {
490
490
  type: "footnoteRef",
491
491
  id: parseNumericAttribute(element, "w", "id") ?? 0
492
492
  };
493
+ const customMarkFollows = parseOnOffAttribute(element, "w", "customMarkFollows");
494
+ if (customMarkFollows !== void 0) reference.customMarkFollows = customMarkFollows;
495
+ return reference;
493
496
  }
494
497
  /**
495
498
  * Parse endnote reference (w:endnoteReference)
496
499
  */
497
500
  function parseEndnoteReference(element) {
498
- return {
501
+ const reference = {
499
502
  type: "endnoteRef",
500
503
  id: parseNumericAttribute(element, "w", "id") ?? 0
501
504
  };
505
+ const customMarkFollows = parseOnOffAttribute(element, "w", "customMarkFollows");
506
+ if (customMarkFollows !== void 0) reference.customMarkFollows = customMarkFollows;
507
+ return reference;
502
508
  }
503
509
  /**
504
510
  * Parse field character (w:fldChar)
@@ -670,7 +676,7 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
670
676
  case "cr":
671
677
  contents.push({
672
678
  type: "break",
673
- breakType: "textWrapping"
679
+ sourceElement: "cr"
674
680
  });
675
681
  break;
676
682
  case "AlternateContent": {
@@ -1,6 +1,5 @@
1
1
  import { serializeParagraphPropertySet } from "../../internal/paragraphFormattingSerialization.js";
2
2
  import { serializePreservedAttributes } from "../attributeRemainder.js";
3
- import { customNumberFormatOf } from "../numberingParser.js";
4
3
  import { serializePartElement } from "./partNamespaces.js";
5
4
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
6
5
  import { intAttr } from "./xmlUtils.js";
@@ -9,8 +8,8 @@ import { serializeSequenceChildren } from "@stll/docx-core/schema";
9
8
  //#region src/docx/serializer/numberingSerializer.ts
10
9
  /** Serialize one `w:lvl`. */
11
10
  function serializeLevel(level) {
12
- const customFormat = customNumberFormatOf(level);
13
- const numFmtFormat = customFormat === void 0 ? "" : ` w:format="${escapeXmlAttribute(customFormat)}"`;
11
+ let numFmtFormat = "";
12
+ if (level.numFmtFormat !== void 0) numFmtFormat = ` w:format="${escapeXmlAttribute(level.numFmtFormat)}"`;
14
13
  const pPr = level.pPr === void 0 ? "" : serializeParagraphPropertySet({ formatting: level.pPr }) || "<w:pPr/>";
15
14
  const rPr = level.rPr === void 0 ? "" : serializeTextFormatting(level.rPr) || "<w:rPr/>";
16
15
  const lvlTextAttrs = [`w:val="${escapeXmlAttribute(level.lvlText)}"`];
@@ -76,6 +76,7 @@ function serializeTabContent(content) {
76
76
  * Serialize break content (w:br)
77
77
  */
78
78
  function serializeBreakContent(content) {
79
+ if (content.sourceElement === "cr") return "<w:cr/>";
79
80
  const attrs = [];
80
81
  if (content.breakType === "page") attrs.push("w:type=\"page\"");
81
82
  else if (content.breakType === "column") attrs.push("w:type=\"column\"");
@@ -94,8 +95,10 @@ function serializeSymbolContent(content) {
94
95
  * Serialize footnote/endnote reference
95
96
  */
96
97
  function serializeNoteReference(content) {
97
- if (content.type === "footnoteRef") return `<w:footnoteReference w:id="${content.id}"/>`;
98
- return `<w:endnoteReference w:id="${content.id}"/>`;
98
+ let customMarkFollows = "";
99
+ if (content.customMarkFollows !== void 0) customMarkFollows = ` w:customMarkFollows="${content.customMarkFollows ? "1" : "0"}"`;
100
+ if (content.type === "footnoteRef") return `<w:footnoteReference w:id="${content.id}"${customMarkFollows}/>`;
101
+ return `<w:endnoteReference w:id="${content.id}"${customMarkFollows}/>`;
99
102
  }
100
103
  /**
101
104
  * Serialize field character (w:fldChar)
@@ -13,7 +13,11 @@ function serializeFootnoteProperties(props) {
13
13
  if (!props) return "";
14
14
  const parts = [];
15
15
  if (props.position) parts.push(`<w:pos w:val="${props.position}"/>`);
16
- if (props.numFmt) parts.push(`<w:numFmt w:val="${props.numFmt}"/>`);
16
+ if (props.numFmt) {
17
+ let format = "";
18
+ if (props.numFmtFormat !== void 0) format = ` w:format="${escapeXmlAttribute(props.numFmtFormat)}"`;
19
+ parts.push(`<w:numFmt w:val="${props.numFmt}"${format}/>`);
20
+ }
17
21
  if (props.numStart !== void 0) parts.push(`<w:numStart w:val="${intAttr(props.numStart)}"/>`);
18
22
  if (props.numRestart) parts.push(`<w:numRestart w:val="${props.numRestart}"/>`);
19
23
  return parts.length > 0 ? `<w:footnotePr>${parts.join("")}</w:footnotePr>` : "";
@@ -25,7 +29,11 @@ function serializeEndnoteProperties(props) {
25
29
  if (!props) return "";
26
30
  const parts = [];
27
31
  if (props.position) parts.push(`<w:pos w:val="${props.position}"/>`);
28
- if (props.numFmt) parts.push(`<w:numFmt w:val="${props.numFmt}"/>`);
32
+ if (props.numFmt) {
33
+ let format = "";
34
+ if (props.numFmtFormat !== void 0) format = ` w:format="${escapeXmlAttribute(props.numFmtFormat)}"`;
35
+ parts.push(`<w:numFmt w:val="${props.numFmt}"${format}/>`);
36
+ }
29
37
  if (props.numStart !== void 0) parts.push(`<w:numStart w:val="${intAttr(props.numStart)}"/>`);
30
38
  if (props.numRestart) parts.push(`<w:numRestart w:val="${props.numRestart}"/>`);
31
39
  return parts.length > 0 ? `<w:endnotePr>${parts.join("")}</w:endnotePr>` : "";
@@ -529,10 +529,11 @@ function serializeCellContent(content, serializeParagraph) {
529
529
  */
530
530
  function serializeTableCell(cell, serializeParagraph) {
531
531
  const parts = [];
532
+ const id = cell.id === void 0 ? "" : ` w:id="${escapeXmlAttribute(cell.id)}"`;
532
533
  const tcPrXml = serializeTableCellFormatting(cell.formatting, cell.propertyChanges, cell.structuralChange);
533
534
  if (tcPrXml) parts.push(tcPrXml);
534
535
  parts.push(serializeCellContent(cell.content, serializeParagraph));
535
- return `<w:tc>${parts.join("")}</w:tc>`;
536
+ return `<w:tc${id}>${parts.join("")}</w:tc>`;
536
537
  }
537
538
  /**
538
539
  * A container's verbatim sink and its bookmark markers, as one positioned list.
@@ -12,7 +12,7 @@ import { TABLE_LOOK_FLAGS } from "./tableLook.js";
12
12
  import { parsePropertyChangeInfo, parseTrackedChangeInfo } from "./trackedChangeInfo.js";
13
13
  import { percentageSpelling, transitionalSlotEncoding } from "./transitionalSpelling.js";
14
14
  import { captureVerbatimXml } from "./verbatimCapture.js";
15
- import { cloneElement, findChild, findChildren, findWordprocessingChild, getAttribute, getLocalName, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, parseTableMeasurementValue, selectAlternateContentBranch } from "./xmlParser.js";
15
+ import { WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findChild, findChildren, findWordprocessingChild, getAttribute, getAttributeByNamespaceUri, getLocalName, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, parseTableMeasurementValue, selectAlternateContentBranch } from "./xmlParser.js";
16
16
  //#region src/docx/tableParser.ts
17
17
  /**
18
18
  * Sanity cap on `w:gridSpan` (and the derived table column count). Word's
@@ -833,7 +833,7 @@ function parseCellContent(tcElement, styles, theme, numbering, rels, media, opti
833
833
  if (preserved?.children) captured.push(...preserved.children);
834
834
  };
835
835
  dispatchCellChildren(tcElement, options);
836
- if (!modelled.some((block) => block.type === "paragraph" || block.type === "table")) modelled.push({
836
+ if (modelled.findLast((block) => block.type === "paragraph" || block.type === "table")?.type !== "paragraph") modelled.push({
837
837
  type: "paragraph",
838
838
  content: []
839
839
  });
@@ -858,6 +858,8 @@ function parseTableCell(tcElement, styles, theme, numbering, rels, media, option
858
858
  type: "tableCell",
859
859
  content: []
860
860
  };
861
+ const id = getAttributeByNamespaceUri(tcElement, WORDPROCESSINGML_NAMESPACE_URIS, "id");
862
+ if (id !== null) cell.id = id;
861
863
  const tcPrElement = findChild(tcElement, "w", "tcPr");
862
864
  const formatting = parseTableCellProperties(tcPrElement);
863
865
  if (formatting) cell.formatting = formatting;
@@ -89,6 +89,7 @@ const TEXT_BOX_TRACKED_CHANGE_TYPES = [
89
89
  "moveTo"
90
90
  ];
91
91
  const HARD_BREAK_TYPES = ["column", "textWrapping"];
92
+ const HARD_BREAK_SOURCE_ELEMENTS = ["br", "cr"];
92
93
  const PAGE_BREAK_CLEAR_VALUES = [
93
94
  "none",
94
95
  "left",
@@ -269,8 +270,19 @@ const readHardBreakAttrs = (node) => {
269
270
  const attrs = attrsRecord(node.attrs);
270
271
  const issues = [];
271
272
  expectNodeType(node, "hardBreak", issues);
273
+ optionalOneOf(attrs, "sourceElement", "hardBreak.attrs.sourceElement", issues, HARD_BREAK_SOURCE_ELEMENTS);
272
274
  optionalOneOf(attrs, "breakType", "hardBreak.attrs.breakType", issues, HARD_BREAK_TYPES);
273
275
  optionalOneOf(attrs, "clear", "hardBreak.attrs.clear", issues, PAGE_BREAK_CLEAR_VALUES);
276
+ if (attrs["sourceElement"] === "cr") {
277
+ if (attrs["breakType"] !== void 0 && attrs["breakType"] !== null) issues.push({
278
+ path: "hardBreak.attrs.breakType",
279
+ message: "Not allowed on a carriage-return element."
280
+ });
281
+ if (attrs["clear"] !== void 0 && attrs["clear"] !== null) issues.push({
282
+ path: "hardBreak.attrs.clear",
283
+ message: "Not allowed on a carriage-return element."
284
+ });
285
+ }
274
286
  return attrsResult(attrs, issues);
275
287
  };
276
288
  const expectHardBreakAttrs = (node) => expectCachedNodeAttrs(node, hardBreakAttrsCache, readHardBreakAttrs, "hard break attrs");
@@ -386,6 +398,7 @@ const readTableCellAttrs = (node) => {
386
398
  const attrs = attrsRecord(node.attrs);
387
399
  const issues = [];
388
400
  expectNodeTypeOneOf(node, ["tableCell", "tableHeader"], issues);
401
+ optionalString(attrs, "_docxCellId", "tableCell.attrs._docxCellId", issues);
389
402
  requiredNumber(attrs, "colspan", "tableCell.attrs.colspan", issues);
390
403
  requiredNumber(attrs, "rowspan", "tableCell.attrs.rowspan", issues);
391
404
  optionalOneOf(attrs, "_omittedGridSlot", "tableCell.attrs._omittedGridSlot", issues, OMITTED_GRID_SLOT_VALUES);
@@ -767,6 +780,7 @@ const readFootnoteRefMarkAttrs = (mark) => {
767
780
  requiredStringOrNumber(attrs, "id", "footnoteRef.attrs.id", issues);
768
781
  optionalOneOf(attrs, "noteType", "footnoteRef.attrs.noteType", issues, NOTE_TYPES);
769
782
  optionalOneOf(attrs, "vertAlign", "footnoteRef.attrs.vertAlign", issues, NOTE_REF_VERT_ALIGNS);
783
+ optionalBoolean(attrs, "customMarkFollows", "footnoteRef.attrs.customMarkFollows", issues);
770
784
  return attrsResult(attrs, issues);
771
785
  };
772
786
  const expectFootnoteRefMarkAttrs = (mark) => expectCachedMarkAttrs(mark, footnoteRefAttrsCache, readFootnoteRefMarkAttrs, "footnote reference attrs");
@@ -1018,6 +1032,9 @@ const readHyperlinkMarkAttrs = (mark) => {
1018
1032
  requiredString(attrs, "href", "hyperlink.attrs.href", issues);
1019
1033
  optionalString(attrs, "tooltip", "hyperlink.attrs.tooltip", issues);
1020
1034
  optionalString(attrs, "rId", "hyperlink.attrs.rId", issues);
1035
+ optionalString(attrs, "target", "hyperlink.attrs.target", issues);
1036
+ optionalBoolean(attrs, "history", "hyperlink.attrs.history", issues);
1037
+ optionalString(attrs, "docLocation", "hyperlink.attrs.docLocation", issues);
1021
1038
  validateNonNegativeInteger(attrs["_docxHyperlinkIndex"], "hyperlink.attrs._docxHyperlinkIndex", issues);
1022
1039
  return attrsResult(attrs, issues);
1023
1040
  };
@@ -2612,6 +2629,9 @@ const optionalEmptyHyperlinkArray = (value, issues) => {
2612
2629
  optionalString(item, "anchor", `${itemPath}.anchor`, issues);
2613
2630
  optionalString(item, "tooltip", `${itemPath}.tooltip`, issues);
2614
2631
  optionalString(item, "rId", `${itemPath}.rId`, issues);
2632
+ optionalString(item, "target", `${itemPath}.target`, issues);
2633
+ optionalBoolean(item, "history", `${itemPath}.history`, issues);
2634
+ optionalString(item, "docLocation", `${itemPath}.docLocation`, issues);
2615
2635
  }
2616
2636
  };
2617
2637
  //#endregion
@@ -1643,6 +1643,9 @@ function createEmptyHyperlink(attrs) {
1643
1643
  if (attrs.anchor !== void 0) hyperlink.anchor = attrs.anchor;
1644
1644
  if (attrs.tooltip !== void 0) hyperlink.tooltip = attrs.tooltip;
1645
1645
  if (attrs.rId !== void 0) hyperlink.rId = attrs.rId;
1646
+ if (attrs.target !== void 0) hyperlink.target = attrs.target;
1647
+ if (attrs.history !== void 0) hyperlink.history = attrs.history;
1648
+ if (attrs.docLocation !== void 0) hyperlink.docLocation = attrs.docLocation;
1646
1649
  return hyperlink;
1647
1650
  }
1648
1651
  function getCommentMarkIds(marks) {
@@ -1684,6 +1687,9 @@ function getLinkKey(mark) {
1684
1687
  attrs.href,
1685
1688
  attrs.rId ?? "",
1686
1689
  attrs.tooltip ?? "",
1690
+ attrs.target ?? "",
1691
+ attrs.history === void 0 ? "" : String(attrs.history),
1692
+ attrs.docLocation ?? "",
1687
1693
  attrs._docxHyperlinkIndex ?? ""
1688
1694
  ].join("\0");
1689
1695
  }
@@ -1701,21 +1707,24 @@ function getMarksKey(marks) {
1701
1707
  function createHyperlink(linkMark) {
1702
1708
  const attrs = expectHyperlinkMarkAttrs(linkMark);
1703
1709
  const href = attrs.href;
1704
- if (href.startsWith("#")) {
1705
- const hyperlink = {
1706
- type: "hyperlink",
1707
- anchor: href.slice(1),
1708
- children: []
1709
- };
1710
- if (attrs.tooltip) hyperlink.tooltip = attrs.tooltip;
1711
- return hyperlink;
1712
- }
1710
+ const metadata = {
1711
+ ...attrs.tooltip === void 0 ? {} : { tooltip: attrs.tooltip },
1712
+ ...attrs.target === void 0 ? {} : { target: attrs.target },
1713
+ ...attrs.history === void 0 ? {} : { history: attrs.history },
1714
+ ...attrs.docLocation === void 0 ? {} : { docLocation: attrs.docLocation }
1715
+ };
1716
+ if (href.startsWith("#")) return {
1717
+ type: "hyperlink",
1718
+ anchor: href.slice(1),
1719
+ ...metadata,
1720
+ children: []
1721
+ };
1713
1722
  const hyperlink = {
1714
1723
  type: "hyperlink",
1715
1724
  href,
1725
+ ...metadata,
1716
1726
  children: []
1717
1727
  };
1718
- if (attrs.tooltip) hyperlink.tooltip = attrs.tooltip;
1719
1728
  if (attrs.rId) hyperlink.rId = attrs.rId;
1720
1729
  return hyperlink;
1721
1730
  }
@@ -1831,12 +1840,14 @@ function getNoteReferenceVertAlign(noteAttrs, marks) {
1831
1840
  }
1832
1841
  function createNoteReferenceRun(noteRefMark, marks, formattingContext) {
1833
1842
  const noteAttrs = expectFootnoteRefMarkAttrs(noteRefMark);
1843
+ const noteRef = {
1844
+ type: noteAttrs.noteType === "endnote" ? "endnoteRef" : "footnoteRef",
1845
+ id: typeof noteAttrs.id === "string" ? Number.parseInt(noteAttrs.id, 10) || 0 : noteAttrs.id
1846
+ };
1847
+ if (noteAttrs.customMarkFollows !== void 0) noteRef.customMarkFollows = noteAttrs.customMarkFollows;
1834
1848
  const run = {
1835
1849
  type: "run",
1836
- content: [{
1837
- type: noteAttrs.noteType === "endnote" ? "endnoteRef" : "footnoteRef",
1838
- id: typeof noteAttrs.id === "string" ? Number.parseInt(noteAttrs.id, 10) || 0 : noteAttrs.id
1839
- }]
1850
+ content: [noteRef]
1840
1851
  };
1841
1852
  const formatting = getAtomRunFormattingFromMarks(marks, formattingContext);
1842
1853
  const vertAlign = getNoteReferenceVertAlign(noteAttrs, marks);
@@ -2045,8 +2056,12 @@ function createPageBreakCarrierRun({ baseParagraphFormatting, clear, inheritedFo
2045
2056
  function createBreakRun(attrs, marks, formattingContext) {
2046
2057
  const run = {
2047
2058
  type: "run",
2048
- content: [{
2059
+ content: [attrs.sourceElement === "cr" ? {
2060
+ type: "break",
2061
+ sourceElement: "cr"
2062
+ } : {
2049
2063
  type: "break",
2064
+ ...attrs.sourceElement !== void 0 ? { sourceElement: attrs.sourceElement } : {},
2050
2065
  ...attrs.breakType !== void 0 ? { breakType: attrs.breakType } : {},
2051
2066
  ...attrs.clear !== void 0 ? { clear: attrs.clear } : {}
2052
2067
  }]
@@ -3305,6 +3320,7 @@ function convertPMTableCell(node, documentCounts, styleResolver = null) {
3305
3320
  type: "tableCell",
3306
3321
  content
3307
3322
  };
3323
+ if (attrs._docxCellId !== void 0) cell.id = attrs._docxCellId;
3308
3324
  const cellFormatting = tableCellAttrsToFormatting(attrs);
3309
3325
  if (cellFormatting) cell.formatting = cellFormatting;
3310
3326
  if (Array.isArray(attrs.tcPrChange) && attrs.tcPrChange.length > 0) cell.propertyChanges = [...attrs.tcPrChange];
@@ -492,7 +492,10 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
492
492
  ...content.href !== void 0 ? { href: content.href } : {},
493
493
  ...content.anchor !== void 0 ? { anchor: content.anchor } : {},
494
494
  ...content.tooltip !== void 0 ? { tooltip: content.tooltip } : {},
495
- ...content.rId !== void 0 ? { rId: content.rId } : {}
495
+ ...content.rId !== void 0 ? { rId: content.rId } : {},
496
+ ...content.target !== void 0 ? { target: content.target } : {},
497
+ ...content.history !== void 0 ? { history: content.history } : {},
498
+ ...content.docLocation !== void 0 ? { docLocation: content.docLocation } : {}
496
499
  });
497
500
  break;
498
501
  }
@@ -1325,6 +1328,7 @@ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPer
1325
1328
  colspan: formatting?.gridSpan ?? 1,
1326
1329
  rowspan
1327
1330
  };
1331
+ if (cell.id !== void 0) attrs._docxCellId = cell.id;
1328
1332
  if (effectiveFormatting.width.type === "value") {
1329
1333
  attrs.width = effectiveFormatting.width.value;
1330
1334
  if (effectiveFormatting.width.widthType) attrs.widthType = effectiveFormatting.width.widthType;
@@ -2064,6 +2068,7 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2064
2068
  case "break":
2065
2069
  if (content.breakType === "textWrapping" || !content.breakType) {
2066
2070
  const attrs = {
2071
+ ...content.sourceElement !== void 0 ? { sourceElement: content.sourceElement } : {},
2067
2072
  ...content.breakType !== void 0 ? { breakType: content.breakType } : {},
2068
2073
  ...content.clear !== void 0 ? { clear: content.clear } : {}
2069
2074
  };
@@ -2095,7 +2100,8 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2095
2100
  const footnoteMark = schema.mark("footnoteRef", {
2096
2101
  id: content.id.toString(),
2097
2102
  noteType: "footnote",
2098
- vertAlign: noteReferenceVertAlign(formatting?.vertAlign)
2103
+ vertAlign: noteReferenceVertAlign(formatting?.vertAlign),
2104
+ customMarkFollows: content.customMarkFollows
2099
2105
  });
2100
2106
  return [schema.text(content.id.toString(), [...marks, footnoteMark])];
2101
2107
  }
@@ -2103,7 +2109,8 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2103
2109
  const endnoteMark = schema.mark("footnoteRef", {
2104
2110
  id: content.id.toString(),
2105
2111
  noteType: "endnote",
2106
- vertAlign: noteReferenceVertAlign(formatting?.vertAlign)
2112
+ vertAlign: noteReferenceVertAlign(formatting?.vertAlign),
2113
+ customMarkFollows: content.customMarkFollows
2107
2114
  });
2108
2115
  return [schema.text(content.id.toString(), [...marks, endnoteMark])];
2109
2116
  }
@@ -2284,6 +2291,9 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
2284
2291
  href,
2285
2292
  tooltip: hyperlink.tooltip,
2286
2293
  rId: hyperlink.rId,
2294
+ target: hyperlink.target,
2295
+ history: hyperlink.history,
2296
+ docLocation: hyperlink.docLocation,
2287
2297
  _docxHyperlinkIndex: hyperlinkIndex
2288
2298
  });
2289
2299
  for (const { content: child, stack } of withInlineWrapperStacks(hyperlink.children)) {
@@ -20,7 +20,8 @@ const FootnoteRefExtension = createMarkExtension({
20
20
  attrs: {
21
21
  id: {},
22
22
  noteType: { default: "footnote" },
23
- vertAlign: { default: null }
23
+ vertAlign: { default: null },
24
+ customMarkFollows: { default: null }
24
25
  },
25
26
  parseDOM: [
26
27
  {
@@ -82,6 +82,9 @@ const HyperlinkExtension = createMarkExtension({
82
82
  href: {},
83
83
  tooltip: { default: null },
84
84
  rId: { default: null },
85
+ target: { default: null },
86
+ history: { default: null },
87
+ docLocation: { default: null },
85
88
  _docxHyperlinkIndex: { default: null }
86
89
  },
87
90
  inclusive: false,
@@ -89,14 +92,15 @@ const HyperlinkExtension = createMarkExtension({
89
92
  tag: "a[href]",
90
93
  getAttrs: (dom) => ({
91
94
  href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
92
- tooltip: dom.getAttribute("title") ?? void 0
95
+ tooltip: dom.getAttribute("title") ?? void 0,
96
+ target: dom.getAttribute("target") ?? void 0
93
97
  })
94
98
  }],
95
99
  toDOM(mark) {
96
- const { href, tooltip } = expectHyperlinkMarkAttrs(mark);
100
+ const { href, target, tooltip } = expectHyperlinkMarkAttrs(mark);
97
101
  const domAttrs = {
98
102
  href: sanitizeStoredHref(href),
99
- ...anchorTargetAttrs(void 0)
103
+ ...anchorTargetAttrs(target)
100
104
  };
101
105
  if (tooltip) domAttrs["title"] = tooltip;
102
106
  return [
@@ -12,6 +12,7 @@ const HardBreakExtension = createNodeExtension({
12
12
  inline: true,
13
13
  group: "inline",
14
14
  attrs: {
15
+ sourceElement: { default: null },
15
16
  breakType: { default: null },
16
17
  clear: { default: null }
17
18
  },
@@ -22,15 +23,18 @@ const HardBreakExtension = createNodeExtension({
22
23
  if (!(node instanceof HTMLElement)) return null;
23
24
  const breakType = node.dataset["docxBreakType"];
24
25
  const clear = node.dataset["docxBreakClear"];
26
+ const sourceElement = node.dataset["docxSourceElement"];
25
27
  const attrs = {};
28
+ if (sourceElement === "br" || sourceElement === "cr") attrs["sourceElement"] = sourceElement;
26
29
  if (breakType === "column" || breakType === "textWrapping") attrs["breakType"] = breakType;
27
30
  if (clear === "none" || clear === "left" || clear === "right" || clear === "all") attrs["clear"] = clear;
28
31
  return Object.keys(attrs).length > 0 ? attrs : null;
29
32
  }
30
33
  }],
31
34
  toDOM(node) {
32
- const { breakType, clear } = expectHardBreakAttrs(node);
35
+ const { sourceElement, breakType, clear } = expectHardBreakAttrs(node);
33
36
  const attrs = {};
37
+ if (sourceElement !== void 0) attrs["data-docx-source-element"] = sourceElement;
34
38
  if (breakType !== void 0) attrs["data-docx-break-type"] = breakType;
35
39
  if (clear !== void 0) attrs["data-docx-break-clear"] = clear;
36
40
  return Object.keys(attrs).length > 0 ? ["br", attrs] : ["br"];
@@ -39,6 +39,43 @@ const preserveOmittedGridSlots = (command) => (state, dispatch, view) => {
39
39
  if (table && hasOmittedGridSlots(table)) return false;
40
40
  return command(state, dispatch, view);
41
41
  };
42
+ const preserveSourceCellIdentity = (command) => (state, dispatch, view) => {
43
+ if (!command(state, void 0, view)) return false;
44
+ if (!dispatch) return true;
45
+ const rect = selectedRect(state);
46
+ const sourceRelativePosition = rect.map.map.at(rect.top * rect.map.width + rect.left);
47
+ if (sourceRelativePosition === void 0) return panic("The selected table cell is absent from its table map");
48
+ const sourceCell = rect.table.nodeAt(sourceRelativePosition);
49
+ if (!sourceCell) return panic("The selected table cell is absent from its table map");
50
+ const sourceId = expectTableCellAttrs(sourceCell)._docxCellId;
51
+ return command(state, (tr) => {
52
+ if (sourceId === void 0 || sourceId === null) {
53
+ dispatch(tr);
54
+ return;
55
+ }
56
+ const mappedTablePosition = tr.mapping.map(rect.tableStart - 1, -1);
57
+ const table = tr.doc.nodeAt(mappedTablePosition);
58
+ if (!table) return panic("The split-cell transaction lost its table");
59
+ const tableStart = mappedTablePosition + 1;
60
+ const map = TableMap.get(table);
61
+ const survivingRelativePosition = map.map.at(rect.top * map.width + rect.left);
62
+ if (survivingRelativePosition === void 0) return panic("The surviving table cell is absent from its table map");
63
+ const cleared = /* @__PURE__ */ new Set();
64
+ for (let row = rect.top; row < rect.bottom; row++) for (let column = rect.left; column < rect.right; column++) {
65
+ const relativePosition = map.map.at(row * map.width + column);
66
+ if (relativePosition === void 0) return panic("A split table cell is absent from its table map");
67
+ if (relativePosition === survivingRelativePosition || cleared.has(relativePosition)) continue;
68
+ cleared.add(relativePosition);
69
+ const cell = table.nodeAt(relativePosition);
70
+ if (!cell) return panic("A split table cell is absent from its table map");
71
+ tr.setNodeMarkup(tableStart + relativePosition, null, {
72
+ ...cell.attrs,
73
+ _docxCellId: null
74
+ });
75
+ }
76
+ dispatch(tr);
77
+ }, view);
78
+ };
42
79
  const isOneOf = (value, values) => value !== null && value !== void 0 && values.some((allowed) => allowed === value);
43
80
  /** Map CSS border-style to OOXML border style. */
44
81
  function cssBorderStyleToOoxml(cssStyle) {
@@ -334,6 +371,7 @@ const tableCellSpec = {
334
371
  tableRole: "cell",
335
372
  isolating: true,
336
373
  attrs: {
374
+ _docxCellId: { default: null },
337
375
  colspan: { default: 1 },
338
376
  rowspan: { default: 1 },
339
377
  _omittedGridSlot: { default: null },
@@ -402,6 +440,7 @@ const tableHeaderSpec = {
402
440
  tableRole: "header_cell",
403
441
  isolating: true,
404
442
  attrs: {
443
+ _docxCellId: { default: null },
405
444
  colspan: { default: 1 },
406
445
  rowspan: { default: 1 },
407
446
  _omittedGridSlot: { default: null },
@@ -2004,7 +2043,7 @@ const TablePluginExtension = createExtension({
2004
2043
  selectRow: () => selectRow,
2005
2044
  selectColumn: () => selectColumn,
2006
2045
  mergeCells: () => preserveOmittedGridSlots(mergeCells),
2007
- splitCell: () => preserveOmittedGridSlots(splitCell),
2046
+ splitCell: () => preserveOmittedGridSlots(preserveSourceCellIdentity(splitCell)),
2008
2047
  setCellBorder: (side, spec, clearOthers) => setCellBorder(side, spec, clearOthers),
2009
2048
  setTableBorders: (preset, borderSpec) => setTableBorders(preset, borderSpec),
2010
2049
  setTableBorderPreset: (preset) => setTableBorderPreset(preset),
@@ -1,5 +1,6 @@
1
1
  import { decodeOoxmlSymbolCharacter } from "../utils/ooxmlSymbol.js";
2
2
  import { expectFieldAttrs, expectPreservedXmlAttrs, expectSymbolAttrs } from "./attrs/index.js";
3
+ import { PRESERVED_XML_LEVELS } from "./schema/nodes.js";
3
4
  import { panic } from "better-result";
4
5
  import { Mark } from "prosemirror-model";
5
6
  //#region src/prosemirror/runFormattingInlineCarriers.ts
@@ -31,6 +32,22 @@ const RUN_FORMATTING_INLINE_ATOM_DISPOSITIONS = Object.freeze({
31
32
  textBoxAnchor: "not-a-run"
32
33
  });
33
34
  const dispositionsByName = new Map(Object.entries(RUN_FORMATTING_INLINE_ATOM_DISPOSITIONS));
35
+ /**
36
+ * One preserved capture is a `w:r` child and the other is a `w:p` child, and
37
+ * the map above can only name the node type.
38
+ *
39
+ * Only the run-level capture serializes inside a `w:r`, so only it has a
40
+ * `w:rPr` to carry; the save path already branches on the same level. A
41
+ * paragraph-level capture is the markup Word writes BETWEEN runs — `w:proofErr`
42
+ * above all — and treating it as a carrier makes every consumer of this map
43
+ * wrong in the same way: formatting marks put on it are dropped on save, and a
44
+ * comparison is asked to line up the base's proofing annotations with the
45
+ * revised document's own, which no redline can do.
46
+ */
47
+ const PRESERVED_XML_DISPOSITIONS = {
48
+ [PRESERVED_XML_LEVELS.run]: "preserved-xml-run",
49
+ [PRESERVED_XML_LEVELS.inline]: "not-a-run"
50
+ };
34
51
  const CONTROL_CHARACTER_BY_DISPOSITION = Object.freeze({
35
52
  "break-run": "\n",
36
53
  "field-run": null,
@@ -73,7 +90,7 @@ const runFormattingInlineAtomDisposition = (node) => {
73
90
  if (!node.isInline || !node.isAtom) return null;
74
91
  const disposition = dispositionsByName.get(node.type.name);
75
92
  if (!disposition) return panic(`Inline atom ${JSON.stringify(node.type.name)} has no run-formatting disposition`);
76
- return disposition;
93
+ return disposition === "preserved-xml-run" ? PRESERVED_XML_DISPOSITIONS[expectPreservedXmlAttrs(node).level] : disposition;
77
94
  };
78
95
  /** Apply a mark set without changing a carrier's text or serialized ownership. */
79
96
  const applyMarksToRunFormattingRepresentation = ({ tr, representation, marks }) => {
@@ -81,6 +81,7 @@ type FootnoteRefAttrs = {
81
81
  id: string | number;
82
82
  noteType?: "footnote" | "endnote";
83
83
  vertAlign?: "baseline" | "superscript";
84
+ customMarkFollows?: document_d_exports.NoteReferenceContent["customMarkFollows"];
84
85
  };
85
86
  type CommentAttrs = {
86
87
  commentId: number;
@@ -271,6 +272,9 @@ type HyperlinkAttrs = {
271
272
  href: string;
272
273
  tooltip?: string;
273
274
  rId?: string;
275
+ target?: document_d_exports.Hyperlink["target"];
276
+ history?: document_d_exports.Hyperlink["history"];
277
+ docLocation?: document_d_exports.Hyperlink["docLocation"];
274
278
  _docxHyperlinkIndex?: number;
275
279
  };
276
280
  //#endregion
@@ -7,6 +7,12 @@ import { ParagraphNumberingAttr } from "../numberingAttr.js";
7
7
  import { ParagraphDirection } from "../paragraphDirection.js";
8
8
  //#region src/prosemirror/schema/nodes.d.ts
9
9
  type HardBreakAttrs = {
10
+ sourceElement: "cr";
11
+ breakType?: never;
12
+ clear?: never;
13
+ } | {
14
+ /** Absent and `br` both serialize as the standard break element. */
15
+ sourceElement?: "br";
10
16
  /** Absent means the source `w:br` omitted `w:type`. */
11
17
  breakType?: "column" | "textWrapping";
12
18
  clear?: "none" | "left" | "right" | "all";
@@ -216,6 +222,9 @@ type ParagraphAttrs = {
216
222
  anchor?: document_d_exports.Hyperlink["anchor"];
217
223
  tooltip?: document_d_exports.Hyperlink["tooltip"];
218
224
  rId?: document_d_exports.Hyperlink["rId"];
225
+ target?: document_d_exports.Hyperlink["target"];
226
+ history?: document_d_exports.Hyperlink["history"];
227
+ docLocation?: document_d_exports.Hyperlink["docLocation"];
219
228
  }[];
220
229
  /**
221
230
  * Run-in heading flag: this paragraph's mark carries
@@ -1056,6 +1065,8 @@ type SuggestedStructuralMarker = {
1056
1065
  * Table cell attributes
1057
1066
  */
1058
1067
  type TableCellAttrs = {
1068
+ /** Authored OOXML cell identifier. */
1069
+ _docxCellId?: document_d_exports.TableCell["id"];
1059
1070
  /** Column span */
1060
1071
  colspan: number;
1061
1072
  /** Row span */
@@ -5,7 +5,7 @@ type TablePropertiesCommand = {
5
5
  widthType?: document_d_exports.TableWidthType | null;
6
6
  justification?: document_d_exports.TableAlignment | null;
7
7
  };
8
- declare const TABLE_PROPERTY_JUSTIFICATIONS: readonly ("start" | "center" | "end" | "left" | "right")[];
8
+ declare const TABLE_PROPERTY_JUSTIFICATIONS: readonly document_d_exports.TableAlignment[];
9
9
  declare const toTablePropertyJustification: (value: string | null | undefined) => NonNullable<TablePropertiesCommand["justification"]>;
10
10
  type TableAction = "addRowAbove" | "addRowBelow" | "addColumnLeft" | "addColumnRight" | "deleteRow" | "deleteColumn" | "mergeCells" | "splitCell" | "deleteTable" | "selectTable" | "selectRow" | "selectColumn" | "borderAll" | "borderOutside" | "borderInside" | "borderNone" | "borderTop" | "borderBottom" | "borderLeft" | "borderRight" | {
11
11
  type: "cellFillColor";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.47.0",
3
+ "version": "0.47.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",
@@ -128,7 +128,7 @@
128
128
  "perf": "bun scripts/profile-editor.ts"
129
129
  },
130
130
  "dependencies": {
131
- "@stll/docx-core": "^0.25.0",
131
+ "@stll/docx-core": "^0.25.1",
132
132
  "@stll/docx-utils": "^0.1.0",
133
133
  "@stll/template-conditions": ">=0.4.0 <1.0.0",
134
134
  "better-result": "3.0.1",