@ansonlai/docx-redline-js 0.8.3 → 0.8.4

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.
@@ -4,14 +4,14 @@
4
4
  * Converts patched run model back to OOXML with track changes.
5
5
  */
6
6
 
7
- import { RunKind, escapeXml, createRevisionMetadata } from '../core/types.js';
8
- import { getApplicableFormatHints } from './markdown-processor.js';
9
- import { serializeXml } from '../adapters/xml-adapter.js';
10
- import { warn } from '../adapters/logger.js';
11
- import { buildDocumentFragmentPackage } from '../services/package-builder.js';
12
- import { getDefaultAuthor } from '../adapters/config.js';
13
-
14
- const XMLNS_ATTR_REGEX = /\s+xmlns:[^=]+="[^"]*"/g;
7
+ import { RunKind, escapeXml, createRevisionMetadata } from '../core/types.js';
8
+ import { getApplicableFormatHints } from './markdown-processor.js';
9
+ import { serializeXml } from '../adapters/xml-adapter.js';
10
+ import { warn } from '../adapters/logger.js';
11
+ import { buildDocumentFragmentPackage } from '../services/package-builder.js';
12
+ import { getDefaultAuthor } from '../adapters/config.js';
13
+
14
+ const XMLNS_ATTR_REGEX = /\s+xmlns:[^=]+="[^"]*"/g;
15
15
 
16
16
  /**
17
17
  * Serializes a patched run model to OOXML.
@@ -22,48 +22,48 @@ const XMLNS_ATTR_REGEX = /\s+xmlns:[^=]+="[^"]*"/g;
22
22
  * @param {import('../core/types.js').SerializationOptions} [options={}] - Serialization options
23
23
  * @returns {string} OOXML paragraph string (WITHOUT namespace - added by wrapper)
24
24
  */
25
- export function serializeToOoxml(patchedModel, pPr, formatHints = [], options = {}) {
26
- const serializationOptions = normalizeSerializationOptions(options);
27
- const { generateRedlines } = serializationOptions;
28
- const paragraphs = [];
29
- let currentPPrXml = '';
30
- let currentPPrElement = null;
31
- let currentRuns = [];
25
+ export function serializeToOoxml(patchedModel, pPr, formatHints = [], options = {}) {
26
+ const serializationOptions = normalizeSerializationOptions(options);
27
+ const { generateRedlines } = serializationOptions;
28
+ const paragraphs = [];
29
+ let currentPPrXml = '';
30
+ let currentPPrElement = null;
31
+ let currentRuns = [];
32
32
 
33
33
  // Helper to flush accumulated runs into a paragraph
34
34
  function flushParagraph() {
35
35
  if (currentRuns.length > 0 || paragraphs.length === 0) {
36
36
  // Build paragraph properties - handle both string and DOM element
37
- let pPrContent = '';
38
- if (currentPPrXml) {
39
- pPrContent = stripNamespaceDeclarations(currentPPrXml);
40
- } else if (currentPPrElement) {
41
- currentPPrXml = stripNamespaceDeclarations(serializeXml(currentPPrElement));
42
- pPrContent = currentPPrXml;
43
- } else if (pPr) {
44
- // Fallback to legacy pPr if no PARAGRAPH_START was seen
45
- if (typeof pPr === 'string') {
46
- pPrContent = pPr;
47
- } else {
48
- pPrContent = serializeXml(pPr);
49
- }
50
- pPrContent = stripNamespaceDeclarations(pPrContent);
51
- }
52
- paragraphs.push(`<w:p>${pPrContent}${currentRuns.join('')}</w:p>`);
53
- currentRuns = [];
54
- }
37
+ let pPrContent = '';
38
+ if (currentPPrXml) {
39
+ pPrContent = stripNamespaceDeclarations(currentPPrXml);
40
+ } else if (currentPPrElement) {
41
+ currentPPrXml = stripNamespaceDeclarations(serializeXml(currentPPrElement));
42
+ pPrContent = currentPPrXml;
43
+ } else if (pPr) {
44
+ // Fallback to legacy pPr if no PARAGRAPH_START was seen
45
+ if (typeof pPr === 'string') {
46
+ pPrContent = pPr;
47
+ } else {
48
+ pPrContent = serializeXml(pPr);
49
+ }
50
+ pPrContent = stripNamespaceDeclarations(pPrContent);
51
+ }
52
+ paragraphs.push(`<w:p>${pPrContent}${currentRuns.join('')}</w:p>`);
53
+ currentRuns = [];
54
+ }
55
55
  }
56
56
 
57
57
  for (const item of patchedModel) {
58
58
  switch (item.kind) {
59
59
  case RunKind.PARAGRAPH_START:
60
60
  // Flush previous paragraph before starting a new one
61
- if (currentRuns.length > 0 || paragraphs.length > 0) {
62
- flushParagraph();
63
- }
64
- currentPPrXml = item.pPrXml || '';
65
- currentPPrElement = item.pPrElement || null;
66
- break;
61
+ if (currentRuns.length > 0 || paragraphs.length > 0) {
62
+ flushParagraph();
63
+ }
64
+ currentPPrXml = item.pPrXml || '';
65
+ currentPPrElement = item.pPrElement || null;
66
+ break;
67
67
 
68
68
  case RunKind.TEXT:
69
69
  currentRuns.push(buildRunXmlWithHints(item, formatHints, serializationOptions));
@@ -87,11 +87,11 @@ export function serializeToOoxml(patchedModel, pPr, formatHints = [], options =
87
87
 
88
88
  case RunKind.BOOKMARK:
89
89
  case RunKind.HYPERLINK:
90
- // Pass through original XML - but strip any namespace declarations
91
- if (item.nodeXml) {
92
- currentRuns.push(stripNamespaceDeclarations(item.nodeXml));
93
- }
94
- break;
90
+ // Pass through original XML - but strip any namespace declarations
91
+ if (item.nodeXml) {
92
+ currentRuns.push(stripNamespaceDeclarations(item.nodeXml));
93
+ }
94
+ break;
95
95
 
96
96
  case RunKind.CONTAINER_START:
97
97
  if (item.containerKind === 'sdt') {
@@ -134,29 +134,29 @@ export function serializeToOoxml(patchedModel, pPr, formatHints = [], options =
134
134
  * @param {import('../core/types.js').SerializationOptions|string|undefined|null} options - Raw options
135
135
  * @returns {import('../core/types.js').SerializationOptions}
136
136
  */
137
- function normalizeSerializationOptions(options) {
138
- // Backward compatibility: accept legacy `font` string signature.
139
- if (typeof options === 'string') {
140
- return {
141
- author: getDefaultAuthor(),
142
- generateRedlines: true,
143
- font: options,
144
- revisionIdAllocator: null
145
- };
146
- }
147
-
148
- const normalized = options && typeof options === 'object' ? options : {};
149
- const resolvedAuthor = typeof normalized.author === 'string' && normalized.author.trim()
150
- ? normalized.author.trim()
151
- : getDefaultAuthor();
152
-
153
- return {
154
- author: resolvedAuthor,
155
- generateRedlines: normalized.generateRedlines ?? true,
156
- font: normalized.font ?? null,
157
- revisionIdAllocator: normalized.revisionIdAllocator ?? null
158
- };
159
- }
137
+ function normalizeSerializationOptions(options) {
138
+ // Backward compatibility: accept legacy `font` string signature.
139
+ if (typeof options === 'string') {
140
+ return {
141
+ author: getDefaultAuthor(),
142
+ generateRedlines: true,
143
+ font: options,
144
+ revisionIdAllocator: null
145
+ };
146
+ }
147
+
148
+ const normalized = options && typeof options === 'object' ? options : {};
149
+ const resolvedAuthor = typeof normalized.author === 'string' && normalized.author.trim()
150
+ ? normalized.author.trim()
151
+ : getDefaultAuthor();
152
+
153
+ return {
154
+ author: resolvedAuthor,
155
+ generateRedlines: normalized.generateRedlines ?? true,
156
+ font: normalized.font ?? null,
157
+ revisionIdAllocator: normalized.revisionIdAllocator ?? null
158
+ };
159
+ }
160
160
 
161
161
  /**
162
162
  * Builds a run XML element, applying format hints if applicable.
@@ -166,10 +166,10 @@ function normalizeSerializationOptions(options) {
166
166
  * @param {import('../core/types.js').SerializationOptions} options - Serialization options
167
167
  * @returns {string}
168
168
  */
169
- function buildRunXmlWithHints(item, formatHints, options = {}) {
170
- const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
171
- const font = options?.font ?? null;
172
- let cleanRPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
169
+ function buildRunXmlWithHints(item, formatHints, options = {}) {
170
+ const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
171
+ const font = options?.font ?? null;
172
+ let cleanRPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
173
173
 
174
174
  if (font) {
175
175
  cleanRPr = applyFont(cleanRPr, font);
@@ -219,7 +219,24 @@ function buildRunXmlWithHints(item, formatHints, options = {}) {
219
219
  function buildSimpleRun(text, rPrXml) {
220
220
  if (!text) return '';
221
221
  const rPr = rPrXml || '';
222
- return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
222
+ return `<w:r>${rPr}${buildRunContent(text, 'w:t')}</w:r>`;
223
+ }
224
+
225
+ /**
226
+ * Builds run content, restoring in-run line breaks and tabs (ingested from
227
+ * w:br/w:cr/w:tab as "\n"/"\t") as elements; raw control characters inside
228
+ * w:t or w:delText render as spaces in Word.
229
+ *
230
+ * @param {string} text - Run text
231
+ * @param {'w:t'|'w:delText'} textTag - Text element name
232
+ * @returns {string}
233
+ */
234
+ function buildRunContent(text, textTag) {
235
+ return String(text).split(/(\r\n|[\n\r\v\t])/).map(part => {
236
+ if (part === '\t') return '<w:tab/>';
237
+ if (part === '\n' || part === '\r' || part === '\r\n' || part === '\v') return '<w:br/>';
238
+ return part ? `<${textTag} xml:space="preserve">${escapeXml(part)}</${textTag}>` : '';
239
+ }).join('');
223
240
  }
224
241
 
225
242
  /**
@@ -229,21 +246,21 @@ function buildSimpleRun(text, rPrXml) {
229
246
  * @param {import('../core/types.js').SerializationOptions} options - Serialization options
230
247
  * @returns {string}
231
248
  */
232
- function buildDeletionXml(item, options = {}) {
233
- const metadata = createRevisionMetadata(
234
- options.author ?? getDefaultAuthor(),
235
- options.revisionIdAllocator,
236
- 'del'
237
- );
238
- const font = options.font ?? null;
239
- let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
249
+ function buildDeletionXml(item, options = {}) {
250
+ const metadata = createRevisionMetadata(
251
+ options.author ?? getDefaultAuthor(),
252
+ options.revisionIdAllocator,
253
+ 'del'
254
+ );
255
+ const font = options.font ?? null;
256
+ let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
240
257
 
241
258
  if (font) {
242
259
  rPr = applyFont(rPr, font);
243
260
  }
244
261
 
245
262
  return `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}">` +
246
- `<w:r>${rPr}<w:delText xml:space="preserve">${escapeXml(item.text)}</w:delText></w:r>` +
263
+ `<w:r>${rPr}${buildRunContent(item.text, 'w:delText')}</w:r>` +
247
264
  `</w:del>`;
248
265
  }
249
266
 
@@ -255,18 +272,18 @@ function buildDeletionXml(item, options = {}) {
255
272
  * @param {import('../core/types.js').SerializationOptions} options - Serialization options
256
273
  * @returns {string}
257
274
  */
258
- function buildInsertionXml(item, formatHints, options = {}) {
259
- const metadata = createRevisionMetadata(
260
- options.author ?? getDefaultAuthor(),
261
- options.revisionIdAllocator,
262
- 'ins'
263
- );
275
+ function buildInsertionXml(item, formatHints, options = {}) {
276
+ const metadata = createRevisionMetadata(
277
+ options.author ?? getDefaultAuthor(),
278
+ options.revisionIdAllocator,
279
+ 'ins'
280
+ );
264
281
  const font = options.font ?? null;
265
282
 
266
283
  // Build the inner run content with format hints
267
284
  const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
268
285
  let innerContent = '';
269
- let cleanRPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
286
+ let cleanRPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
270
287
 
271
288
  if (font) {
272
289
  cleanRPr = applyFont(cleanRPr, font);
@@ -383,7 +400,7 @@ export function wrapInDocumentFragment(paragraphXml, options = {}) {
383
400
  * @param {import('../core/types.js').DocumentFragmentOptions|boolean|undefined|null} options - Raw options
384
401
  * @returns {import('../core/types.js').DocumentFragmentOptions}
385
402
  */
386
- function normalizeFragmentOptions(options) {
403
+ function normalizeFragmentOptions(options) {
387
404
  if (typeof options === 'boolean') {
388
405
  return { includeNumbering: options };
389
406
  }
@@ -397,9 +414,9 @@ function normalizeFragmentOptions(options) {
397
414
  numberingXml: options.numberingXml ?? null,
398
415
  appendTrailingParagraph: options.appendTrailingParagraph ?? true
399
416
  };
400
- }
401
-
402
- function stripNamespaceDeclarations(xml) {
403
- return xml ? xml.replace(XMLNS_ATTR_REGEX, '') : '';
404
- }
417
+ }
418
+
419
+ function stripNamespaceDeclarations(xml) {
420
+ return xml ? xml.replace(XMLNS_ATTR_REGEX, '') : '';
421
+ }
405
422
 
@@ -24,6 +24,7 @@ import {
24
24
  reconcileReceiptsAgainstOutput
25
25
  } from './receipt-collector.js';
26
26
  import { compileOperationBatch } from './operation-batch-compiler.js';
27
+ import { createDynamicNumberingIdState } from './numbering-helpers.js';
27
28
  import { createRetryPlan } from './error-recovery.js';
28
29
 
29
30
  const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
@@ -368,6 +369,11 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
368
369
 
369
370
  const atomic = options.atomic === true;
370
371
  const continueOnError = options.continueOnError !== false;
372
+ // Generated list definitions must not collide with the supplied source numbering part.
373
+ if (runtimeContext && typeof runtimeContext === 'object' && !runtimeContext.numberingIdState
374
+ && typeof runtimeContext.numberingXml === 'string' && runtimeContext.numberingXml.trim()) {
375
+ runtimeContext.numberingIdState = createDynamicNumberingIdState(runtimeContext.numberingXml);
376
+ }
371
377
  const context = cloneBatchRuntimeContext(runtimeContext);
372
378
  if (!(context.targetRefSnapshot instanceof Map)) {
373
379
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
@@ -216,9 +216,11 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
216
216
  ...(operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {}),
217
217
  structuredContent: typeof operation.structuredContent === 'boolean' ? operation.structuredContent : (options.structuredContent !== false),
218
218
  explicitStructuredContent: operation.structuredContent === true,
219
+ ...(operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {}),
219
220
  pairReplacements: typeof operation.pairReplacements === 'boolean' ? operation.pairReplacements : (options.pairReplacements !== false),
220
221
  ...(operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {}),
221
222
  ...(operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {}),
223
+ ...(operation.operationKind === 'format' && Number.isInteger(operation.textOccurrence) ? { textOccurrence: operation.textOccurrence } : {}),
222
224
  targetDescriptor: operation.targetDescriptor,
223
225
  targetEndDescriptor: operation.targetEndDescriptor,
224
226
  _compiledSourceId: operation._compiledSourceId || null,