@ansonlai/docx-redline-js 0.8.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +679 -460
- package/CHANGELOG.md +208 -1
- package/README.md +152 -16
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +4338 -796
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4463 -921
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4463 -921
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
- package/docs/SKILL_AUTHORING.md +169 -150
- package/docs/TESTING.md +1146 -1059
- package/docs/schemas/document-operations.schema.json +55 -2
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +21 -22
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +53 -2
- package/index.js +13 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +21 -3
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/markdown-processor.js +5 -2
- package/pipeline/pipeline.js +206 -204
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +23 -3
- package/services/capture-engine.js +228 -226
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +214 -0
- package/services/document-operation-contract.js +76 -0
- package/services/document-operation-mutations.js +688 -51
- package/services/error-recovery.js +17 -0
- package/services/inspection-comparison.js +82 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +183 -11
- package/services/operation-preflight.js +43 -33
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +484 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- package/services/table-reconciliation.js +59 -7
|
@@ -42,6 +42,43 @@ export type HeaderFooterPartSelector =
|
|
|
42
42
|
| string
|
|
43
43
|
| { kind: 'header' | 'footer'; type?: 'default' | 'first' | 'even'; section?: number };
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Structured list-conversion diagnostic (`list-conversion-diagnostics-v1`). Warnings appear in
|
|
47
|
+
* `diagnostics` (and as `CODE: message` strings in `warnings`) on an operation that succeeded;
|
|
48
|
+
* the same details appear on `error` when the strict policy refuses the operation. They are
|
|
49
|
+
* unrelated to `REVISION_FIDELITY_VIOLATION`.
|
|
50
|
+
*/
|
|
51
|
+
export interface ListConversionDiagnostic {
|
|
52
|
+
code:
|
|
53
|
+
| 'LIST_MARKER_NOT_BOUND'
|
|
54
|
+
| 'UNSUPPORTED_LIST_CONVERSION'
|
|
55
|
+
| 'LIST_FORMAT_NOT_APPLIED'
|
|
56
|
+
| 'UNSUPPORTED_LIST_FORMAT_CHANGE'
|
|
57
|
+
| 'UNKNOWN_PARAGRAPH_FORMAT_PROPERTY';
|
|
58
|
+
severity: 'warning' | 'error';
|
|
59
|
+
message: string;
|
|
60
|
+
/** The literal marker text (for example `A.`) the edit introduced. */
|
|
61
|
+
listMarker?: string;
|
|
62
|
+
requestedList?: { kind: 'bullet' | 'numbered'; format: string };
|
|
63
|
+
/** The numbering definition the list item keeps (format-change diagnostics). */
|
|
64
|
+
sourceList?: { numId: string; level: number; kind: 'bullet' | 'numbered'; format: string };
|
|
65
|
+
targetExcerpt?: string;
|
|
66
|
+
operationIndex?: number;
|
|
67
|
+
/** Rejected `paragraph-format.properties` keys (UNKNOWN_PARAGRAPH_FORMAT_PROPERTY). */
|
|
68
|
+
properties?: string[];
|
|
69
|
+
recovery?: {
|
|
70
|
+
action: string;
|
|
71
|
+
hint?: string;
|
|
72
|
+
field?: string;
|
|
73
|
+
allowedValue?: string;
|
|
74
|
+
/**
|
|
75
|
+
* The list-format operation to send (add a target): convert-plain for LIST_MARKER_NOT_BOUND /
|
|
76
|
+
* UNSUPPORTED_LIST_CONVERSION, change-format (scope "list") for LIST_FORMAT_NOT_APPLIED / UNSUPPORTED_LIST_FORMAT_CHANGE.
|
|
77
|
+
*/
|
|
78
|
+
suggestedOperation?: { type: 'list-format'; list: ListFormatRequest };
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
45
82
|
export interface DocumentOperationBase {
|
|
46
83
|
operationId?: string;
|
|
47
84
|
/** Target a header or footer instead of the body. Comments are not supported there. */
|
|
@@ -54,6 +91,11 @@ export interface DocumentOperationBase {
|
|
|
54
91
|
existingRevisions?: ExistingRevisionsPolicy;
|
|
55
92
|
pairReplacements?: boolean;
|
|
56
93
|
insertionAffinity?: InsertionAffinity;
|
|
94
|
+
/**
|
|
95
|
+
* `'literal'` accepts list-marker text as literal text and suppresses LIST_MARKER_NOT_BOUND,
|
|
96
|
+
* UNSUPPORTED_LIST_CONVERSION, LIST_FORMAT_NOT_APPLIED and UNSUPPORTED_LIST_FORMAT_CHANGE for this operation.
|
|
97
|
+
*/
|
|
98
|
+
listMarkers?: 'literal';
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
interface RedlineDocumentOperationCommon extends DocumentOperationBase {
|
|
@@ -148,12 +190,19 @@ export interface CharacterFormatProperties {
|
|
|
148
190
|
fontFamily?: string | null;
|
|
149
191
|
}
|
|
150
192
|
|
|
193
|
+
/** Closed set. Unknown keys are rejected with INVALID_OPERATION (field `properties.<key>`) since 0.9.0. */
|
|
151
194
|
export interface ParagraphFormatProperties {
|
|
152
195
|
alignment?: 'left' | 'center' | 'right' | 'both';
|
|
153
196
|
keepNext?: boolean;
|
|
154
197
|
keepLines?: boolean;
|
|
155
198
|
pageBreakBefore?: boolean;
|
|
156
199
|
style?: string | null;
|
|
200
|
+
/**
|
|
201
|
+
* Binds the paragraph to an EXISTING list (`numId` and `level` as reported by inspect), or `null` to remove its direct
|
|
202
|
+
* list binding. Tracked as `w:pPrChange`; refuses `PARAGRAPH_NUMBERING_NOT_FOUND` when the numId/level is not defined.
|
|
203
|
+
* Capability `paragraph-numbering-binding-v1`.
|
|
204
|
+
*/
|
|
205
|
+
numbering?: { numId: number | string; level?: number } | null;
|
|
157
206
|
}
|
|
158
207
|
|
|
159
208
|
export interface HighlightDocumentOperation extends DocumentOperationBase {
|
|
@@ -176,6 +225,108 @@ export interface ParagraphFormatDocumentOperation extends DocumentOperationBase
|
|
|
176
225
|
properties: ParagraphFormatProperties;
|
|
177
226
|
}
|
|
178
227
|
|
|
228
|
+
export type ListNumberFormat = 'decimal' | 'upperLetter' | 'lowerLetter' | 'upperRoman' | 'lowerRoman' | 'bullet';
|
|
229
|
+
|
|
230
|
+
/** `list-format` request. `convert-plain`: `list-format-v1` (CLI contract version 9); `change-format` and `join`: `list-format-change-v1`. */
|
|
231
|
+
export type ListFormatRequest = ListFormatConvertPlainRequest | ListFormatChangeFormatRequest | ListFormatJoinRequest;
|
|
232
|
+
|
|
233
|
+
/** `convert-plain` (`list-format-v1`): bind plain paragraphs to a NEW list. */
|
|
234
|
+
export interface ListFormatConvertPlainRequest {
|
|
235
|
+
mode: 'convert-plain';
|
|
236
|
+
kind?: 'bullet' | 'numbered';
|
|
237
|
+
/** Required with `strip: 'none'`; with `strip: 'marker'` it must match the typed markers (and disambiguates `I.`/`C.`). */
|
|
238
|
+
format?: 'decimal' | 'upperLetter' | 'lowerLetter' | 'upperRoman' | 'lowerRoman' | 'bullet';
|
|
239
|
+
/** 0-8, default 0. */
|
|
240
|
+
level?: number;
|
|
241
|
+
/** `'marker'` (default) removes the typed marker as a tracked deletion; `'none'` keeps the text and only binds. */
|
|
242
|
+
strip?: 'marker' | 'none';
|
|
243
|
+
/** `strip: 'none'` only. Defaults to continuing the batch's previous converted list of the same format, else 1. */
|
|
244
|
+
start?: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* `change-format` (`list-format-change-v1`): change the numbering format of the target's list. Allocates a NEW
|
|
249
|
+
* definition (copying the source levels and geometry, one level redefined) and rebinds the items with tracked
|
|
250
|
+
* `w:pPrChange`s; the shared definition is never edited, so Reject All restores the original labels.
|
|
251
|
+
*/
|
|
252
|
+
export interface ListFormatChangeFormatRequest {
|
|
253
|
+
mode: 'change-format';
|
|
254
|
+
kind?: 'bullet' | 'numbered';
|
|
255
|
+
/** The new format of the level (`kind: 'numbered'` alone is refused: name the format). */
|
|
256
|
+
format?: ListNumberFormat;
|
|
257
|
+
/** Level text: numbered `%1`..`%9` with the level's own placeholder (`"%1."`, `"(%2)"`); bullet: the bullet character. */
|
|
258
|
+
lvlText?: string;
|
|
259
|
+
/** Restart value of the level (the new definition's `w:start`); scope `'list'` only. */
|
|
260
|
+
start?: number;
|
|
261
|
+
/** The level to redefine; default: the one level the target paragraphs use. */
|
|
262
|
+
level?: number;
|
|
263
|
+
/** `'list'` (default): every live item of the list; `'range'`: the target range, which must run to the end of the list. */
|
|
264
|
+
scope?: 'list' | 'range';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `join` (`list-format-change-v1`): the target list (or a range running to its end) continues an earlier list. */
|
|
268
|
+
export interface ListFormatJoinRequest {
|
|
269
|
+
mode: 'join';
|
|
270
|
+
/** An item of the earlier list to continue; bound at batch start like `target`. */
|
|
271
|
+
continueFrom: ParagraphTargetDescriptor;
|
|
272
|
+
scope?: 'list' | 'range';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Binds plain paragraphs (target, or target..targetEnd in one container) to a NEW list definition in place. The
|
|
277
|
+
* binding is a tracked `w:pPrChange`; the stripped marker is a tracked `w:del`. Each typed marker's value is the
|
|
278
|
+
* item's value, so Accept All renders the labels the text showed. Body only (no `part`); text changes belong in a
|
|
279
|
+
* separate single-paragraph `redline` of the same paragraph that keeps the typed marker exactly: the batch compiler
|
|
280
|
+
* lets that pair share one batch and runs the redline first ("1. Old" -> "1. New": redline the text, convert-plain
|
|
281
|
+
* strips "1. "); other pairs fail LIST_FORMAT_COMPOSITION_CONFLICT. Refusals: LIST_MARKER_NOT_FOUND, LIST_MARKER_AMBIGUOUS, LIST_MARKER_UNSUPPORTED,
|
|
282
|
+
* LIST_FORMAT_TARGET_IS_LIST, LIST_FORMAT_STYLE_NUMBERED, PARAGRAPH_FORMAT_REVISION_CONFLICT.
|
|
283
|
+
*
|
|
284
|
+
* `change-format` / `join` (`list-format-change-v1`) rebind existing list items with tracked `w:pPrChange`s. With
|
|
285
|
+
* `scope: 'list'` every rebound item counts as a write of the operation in the batch compiler: only a single-paragraph
|
|
286
|
+
* `redline` per item composes with it (LIST_FORMAT_COMPOSITION_CONFLICT `reason: list-rebind` otherwise).
|
|
287
|
+
* Refusals: LIST_FORMAT_TARGET_NOT_LIST, LIST_FORMAT_MIXED_RANGE, LIST_FORMAT_STYLE_LINKED, LIST_FORMAT_STYLE_NUMBERED,
|
|
288
|
+
* LIST_FORMAT_SPLIT_UNSUPPORTED, LIST_FORMAT_DEFINITION_UNSUPPORTED, LIST_FORMAT_JOIN_INVALID,
|
|
289
|
+
* PARAGRAPH_FORMAT_REVISION_CONFLICT; a no-op is `ok` with a LIST_FORMAT_UNCHANGED warning.
|
|
290
|
+
*/
|
|
291
|
+
export interface ListFormatDocumentOperation extends Omit<DocumentOperationBase, 'part'> {
|
|
292
|
+
type: 'list-format';
|
|
293
|
+
list: ListFormatRequest;
|
|
294
|
+
targetEnd?: ParagraphTargetDescriptor;
|
|
295
|
+
targetEndRef?: number | string | null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** What a `list-format` operation bound or rebound (`result.list`). */
|
|
299
|
+
export type ListFormatResult = ListFormatConvertPlainResult | ListFormatRebindResult;
|
|
300
|
+
|
|
301
|
+
/** `convert-plain` result. */
|
|
302
|
+
export interface ListFormatConvertPlainResult {
|
|
303
|
+
mode: 'convert-plain';
|
|
304
|
+
format: 'decimal' | 'upperLetter' | 'lowerLetter' | 'upperRoman' | 'lowerRoman' | 'bullet';
|
|
305
|
+
level: number;
|
|
306
|
+
strip: 'marker' | 'none';
|
|
307
|
+
items: Array<{ paragraphIndex: number; paragraphId: string | null; marker: string | null; value: number | null; numId: string }>;
|
|
308
|
+
/** Paragraphs of the range left as they were: empty, or (strip 'marker') body text without a marker. */
|
|
309
|
+
skipped: Array<{ paragraphIndex: number; reason: 'empty' | 'no-marker' }>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** `change-format` / `join` result: every paragraph rebound (scope `'list'` includes items outside the target). */
|
|
313
|
+
export interface ListFormatRebindResult {
|
|
314
|
+
mode: 'change-format' | 'join';
|
|
315
|
+
scope: 'list' | 'range';
|
|
316
|
+
sourceNumId: string;
|
|
317
|
+
/** change-format: the newly allocated numId; join: the continued list's numId. */
|
|
318
|
+
numId: string;
|
|
319
|
+
format?: ListNumberFormat;
|
|
320
|
+
level?: number;
|
|
321
|
+
lvlText?: string;
|
|
322
|
+
/** change-format: per-level `w:start` values of the new definition (carried restarts, split continuation, `start`). */
|
|
323
|
+
levelStarts?: Record<string, number>;
|
|
324
|
+
continueFrom?: { paragraphIndex: number | null; numId: string };
|
|
325
|
+
items: Array<{ paragraphIndex: number | null; paragraphId: string | null; level: number; numId: string; sourceNumId: string; inTarget: boolean }>;
|
|
326
|
+
/** Tracked-deleted paragraphs in the range keep their binding. */
|
|
327
|
+
skipped: Array<{ paragraphIndex: number | null; reason: 'tracked-deleted' }>;
|
|
328
|
+
}
|
|
329
|
+
|
|
179
330
|
export type DocumentOperation =
|
|
180
331
|
| RedlineDocumentOperation
|
|
181
332
|
| RestoreDocumentOperation
|
|
@@ -185,7 +336,8 @@ export type DocumentOperation =
|
|
|
185
336
|
| CommentResolveDocumentOperation
|
|
186
337
|
| HighlightDocumentOperation
|
|
187
338
|
| CharacterFormatDocumentOperation
|
|
188
|
-
| ParagraphFormatDocumentOperation
|
|
339
|
+
| ParagraphFormatDocumentOperation
|
|
340
|
+
| ListFormatDocumentOperation;
|
|
189
341
|
|
|
190
342
|
export interface ResolvedDocumentTarget {
|
|
191
343
|
index: number;
|
|
@@ -221,6 +373,20 @@ export interface StandaloneRunnerOptions {
|
|
|
221
373
|
generateRedlines?: boolean;
|
|
222
374
|
existingRevisions?: ExistingRevisionsPolicy;
|
|
223
375
|
strictTargets?: boolean;
|
|
376
|
+
/**
|
|
377
|
+
* Make list-conversion diagnostics errors (UNSUPPORTED_LIST_CONVERSION, UNSUPPORTED_LIST_FORMAT_CHANGE)
|
|
378
|
+
* instead of warnings. Defaults to `atomic`; the CLI agent profile sets it. An operation's
|
|
379
|
+
* `listMarkers: 'literal'` always wins.
|
|
380
|
+
*/
|
|
381
|
+
strictListConversion?: boolean;
|
|
382
|
+
/**
|
|
383
|
+
* Post-apply check that Word's Reject All of each tracked operation's output equals the source
|
|
384
|
+
* (paragraph count, text, properties, numbering, wrapper membership). Default `'enforce'` refuses
|
|
385
|
+
* the operation with `REVISION_FIDELITY_VIOLATION` (per-invariant `violations[]` and
|
|
386
|
+
* `recovery.retryGuidance`); `'warn'` applies it and appends the same message to `warnings`;
|
|
387
|
+
* `'off'` is for diagnosis only.
|
|
388
|
+
*/
|
|
389
|
+
verifyRevisionFidelity?: 'enforce' | 'warn' | 'off';
|
|
224
390
|
expectedRevision?: RevisionToken;
|
|
225
391
|
pairReplacements?: boolean;
|
|
226
392
|
insertionAffinity?: InsertionAffinity;
|
|
@@ -288,30 +454,36 @@ export interface DocumentOperationResult {
|
|
|
288
454
|
commentsXmlMode?: 'merge' | 'replace';
|
|
289
455
|
commentsExtendedXmlMode?: 'merge' | 'replace';
|
|
290
456
|
warnings?: string[];
|
|
457
|
+
diagnostics?: ListConversionDiagnostic[];
|
|
291
458
|
status?: RedlineStatus;
|
|
292
459
|
error?: RedlineError;
|
|
293
|
-
operationType: 'redline' | 'comment' | 'comment_reply' | 'comment_resolve' | 'highlight' | 'format' | 'paragraph-format';
|
|
460
|
+
operationType: 'redline' | 'comment' | 'comment_reply' | 'comment_resolve' | 'highlight' | 'format' | 'paragraph-format' | 'list-format';
|
|
294
461
|
authorUsed: string;
|
|
295
462
|
resolvedBy?: string;
|
|
296
463
|
resolvedTarget?: ResolvedDocumentTarget;
|
|
297
464
|
resolvedAnchor?: ResolvedCommentAnchor;
|
|
298
465
|
receipt?: MutationReceipt;
|
|
299
466
|
change?: LocalizedReplacementChange;
|
|
467
|
+
/** `list-format` operations: what was bound. */
|
|
468
|
+
list?: ListFormatResult;
|
|
300
469
|
}
|
|
301
470
|
|
|
302
471
|
export interface BatchOperationItemResult {
|
|
303
472
|
index: number;
|
|
304
473
|
type: string;
|
|
305
|
-
operationType: 'redline' | 'comment' | 'comment_reply' | 'comment_resolve' | 'highlight' | 'format' | 'paragraph-format';
|
|
474
|
+
operationType: 'redline' | 'comment' | 'comment_reply' | 'comment_resolve' | 'highlight' | 'format' | 'paragraph-format' | 'list-format';
|
|
306
475
|
status: 'applied' | 'no_change' | 'error';
|
|
307
476
|
authorUsed: string;
|
|
308
477
|
resolvedBy?: string;
|
|
309
478
|
resolvedTarget?: ResolvedDocumentTarget;
|
|
310
479
|
resolvedAnchor?: ResolvedCommentAnchor;
|
|
311
480
|
warnings?: string[];
|
|
481
|
+
diagnostics?: ListConversionDiagnostic[];
|
|
312
482
|
error?: RedlineError;
|
|
313
483
|
receipt?: MutationReceipt;
|
|
314
484
|
change?: LocalizedReplacementChange;
|
|
485
|
+
/** `list-format` operations: what was bound. */
|
|
486
|
+
list?: ListFormatResult;
|
|
315
487
|
}
|
|
316
488
|
|
|
317
489
|
export interface DocumentOperationBatchResult {
|
|
@@ -347,7 +519,7 @@ export interface MutationRetryPlan {
|
|
|
347
519
|
export interface OperationPreflightItemResult {
|
|
348
520
|
index: number;
|
|
349
521
|
type: string;
|
|
350
|
-
operationType: 'redline' | 'comment' | 'comment_reply' | 'comment_resolve' | 'highlight' | 'format' | 'paragraph-format';
|
|
522
|
+
operationType: 'redline' | 'comment' | 'comment_reply' | 'comment_resolve' | 'highlight' | 'format' | 'paragraph-format' | 'list-format';
|
|
351
523
|
status: 'ready' | 'deferred' | 'error';
|
|
352
524
|
authorUsed: string;
|
|
353
525
|
resolvedBy?: string;
|
|
@@ -375,10 +547,15 @@ export interface OperationPreflightItemResult {
|
|
|
375
547
|
}
|
|
376
548
|
|
|
377
549
|
export interface OperationConflict {
|
|
378
|
-
code: 'OVERLAPPING_SOURCE_TARGETS' | 'OVERLAPPING_TEXT_EDITS' | 'REVISION_ORDER_CONFLICT' | 'CAPTURE_FANOUT_CONFLICT' | string;
|
|
550
|
+
code: 'OVERLAPPING_SOURCE_TARGETS' | 'OVERLAPPING_TEXT_EDITS' | 'REVISION_ORDER_CONFLICT' | 'LIST_FORMAT_COMPOSITION_CONFLICT' | 'CAPTURE_FANOUT_CONFLICT' | string;
|
|
379
551
|
message: string;
|
|
380
552
|
operationIndexes: number[];
|
|
381
553
|
target: ResolvedDocumentTarget;
|
|
554
|
+
/** LIST_FORMAT_COMPOSITION_CONFLICT: why the redline and the list-format do not compose. */
|
|
555
|
+
reason?: 'marker-changed' | 'redline-unchanged' | 'redline-shape' | 'list-rebind' | string;
|
|
556
|
+
sourceMarker?: string | null;
|
|
557
|
+
modifiedMarker?: string | null;
|
|
558
|
+
rebindingOperationIndexes?: number[];
|
|
382
559
|
recoveryVersion?: number;
|
|
383
560
|
category?: string;
|
|
384
561
|
recovery?: RedlineError['recovery'];
|
|
@@ -8,7 +8,8 @@ import { computeWordLevelDiffOps } from '../pipeline/diff-engine.js';
|
|
|
8
8
|
import { splitRunsAtDiffBoundaries, applyPatches } from '../pipeline/patching.js';
|
|
9
9
|
import { serializeToOoxml } from '../pipeline/serialization.js';
|
|
10
10
|
import { createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
|
|
11
|
-
import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
11
|
+
import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
12
|
+
import { paragraphMarkPPrXml } from '../engine/paragraph-mark-revisions.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Generates a new w:tbl OOXML structure from Markdown table data.
|
|
@@ -75,10 +76,7 @@ export function generateTableOoxml(tableData, options = {}) {
|
|
|
75
76
|
endOffset: cleanText.length
|
|
76
77
|
}];
|
|
77
78
|
|
|
78
|
-
const
|
|
79
|
-
const cellPPr = cellMarkMeta
|
|
80
|
-
? `<w:pPr><w:rPr><w:ins w:id="${cellMarkMeta.id}" w:author="${escapeXml(cellMarkMeta.author)}" w:date="${cellMarkMeta.date}"/></w:rPr></w:pPr>`
|
|
81
|
-
: null;
|
|
79
|
+
const cellPPr = trackRows ? paragraphMarkPPrXml('ins', { author, revisionIdAllocator }) : null;
|
|
82
80
|
const runsOoxml = serializeToOoxml(runModel, cellPPr, formatHints, {
|
|
83
81
|
author,
|
|
84
82
|
generateRedlines,
|
|
@@ -153,8 +151,9 @@ export function diffTablesWithVirtualGrid(oldGrid, newTableData) {
|
|
|
153
151
|
if (col > oldCell.gridCol) continue;
|
|
154
152
|
|
|
155
153
|
if (newCellText !== undefined) {
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Markdown cannot represent a cell's trailing empty paragraphs, so they do not count as a change.
|
|
155
|
+
const oldText = trimTrailingNewlines(oldCell.getText());
|
|
156
|
+
if (oldText !== trimTrailingNewlines(newCellText)) {
|
|
158
157
|
operations.push({
|
|
159
158
|
type: 'cell_modify',
|
|
160
159
|
gridRow: row,
|
|
@@ -311,7 +310,60 @@ function buildTableOperationIndex(operations) {
|
|
|
311
310
|
};
|
|
312
311
|
}
|
|
313
312
|
|
|
313
|
+
function trimTrailingNewlines(text) {
|
|
314
|
+
return typeof text === 'string' ? text.replace(/\n+$/, '') : text;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Reconciles a multi-paragraph cell paragraph by paragraph so unchanged paragraphs (including
|
|
319
|
+
* empty ones the Markdown cell text cannot express) are preserved exactly.
|
|
320
|
+
*/
|
|
321
|
+
function reconcileMultiBlockCellContent(cell, newText, options) {
|
|
322
|
+
const { generateRedlines, author, revisionIdAllocator = null } = options;
|
|
323
|
+
const lines = String(newText ?? '').split('\n');
|
|
324
|
+
// Trailing blank lines carry no content; never create inserted paragraphs from them.
|
|
325
|
+
while (lines.length > cell.blocks.length && lines[lines.length - 1] === '') lines.pop();
|
|
326
|
+
let out = '';
|
|
327
|
+
|
|
328
|
+
cell.blocks.forEach((block, index) => {
|
|
329
|
+
const line = lines[index];
|
|
330
|
+
if (line === undefined || line === block.acceptedText) {
|
|
331
|
+
if (line === undefined && block.acceptedText !== '') {
|
|
332
|
+
// Markdown dropped this paragraph's text: track the text deletion.
|
|
333
|
+
const diffOps = computeWordLevelDiffOps(block.acceptedText, '');
|
|
334
|
+
const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
|
|
335
|
+
generateRedlines, author, formatHints: []
|
|
336
|
+
});
|
|
337
|
+
out += serializeToOoxml(model, block.pPr, [], { author, generateRedlines, revisionIdAllocator });
|
|
338
|
+
} else {
|
|
339
|
+
out += serializeToOoxml(block.runModel, block.pPr, [], {});
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const { cleanText, formatHints } = preprocessMarkdown(line);
|
|
344
|
+
const diffOps = computeWordLevelDiffOps(block.acceptedText, cleanText);
|
|
345
|
+
const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
|
|
346
|
+
generateRedlines, author, formatHints
|
|
347
|
+
});
|
|
348
|
+
out += serializeToOoxml(model, block.pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
for (let i = cell.blocks.length; i < lines.length; i++) {
|
|
352
|
+
const { cleanText, formatHints } = preprocessMarkdown(lines[i]);
|
|
353
|
+
const runModel = [{
|
|
354
|
+
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
355
|
+
text: cleanText, rPrXml: '', author, startOffset: 0, endOffset: cleanText.length
|
|
356
|
+
}];
|
|
357
|
+
const pPr = generateRedlines ? paragraphMarkPPrXml('ins', { author, revisionIdAllocator }) : null;
|
|
358
|
+
out += serializeToOoxml(runModel, pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
314
363
|
function reconcileCellContent(cell, newText, options) {
|
|
364
|
+
if (cell.blocks.length > 1) {
|
|
365
|
+
return reconcileMultiBlockCellContent(cell, newText, options);
|
|
366
|
+
}
|
|
315
367
|
const { generateRedlines, author, revisionIdAllocator = null } = options;
|
|
316
368
|
const { cleanText, formatHints } = preprocessMarkdown(newText);
|
|
317
369
|
|