@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
|
@@ -1,419 +1,415 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List generation flow extracted from ReconciliationPipeline.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { preprocessMarkdown } from './markdown-processor.js';
|
|
6
|
-
import { matchListMarker, stripListMarker } from './list-markers.js';
|
|
7
|
-
import { serializeToOoxml } from './serialization.js';
|
|
8
|
-
import { generateTableOoxml } from '../services/table-reconciliation.js';
|
|
9
|
-
import { parseTable } from './content-analysis.js';
|
|
10
|
-
import { log } from '../adapters/logger.js';
|
|
11
|
-
import {
|
|
12
|
-
import { parseOoxmlSafe, serializeXml } from '../adapters/xml-adapter.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Executes list generation when paragraph content expands into list/table blocks.
|
|
17
|
-
*
|
|
18
|
-
* @param {Object} options - Generation options
|
|
19
|
-
* @param {string} options.cleanText - Preprocessed target text
|
|
20
|
-
* @param {Object|null} options.numberingContext - Existing numbering context
|
|
21
|
-
* @param {Array} [options.originalRunModel=[]] - Original run model
|
|
22
|
-
* @param {string} [options.originalText=''] - Original text fallback
|
|
23
|
-
* @param {boolean} [options.generateRedlines=true] - Track-change toggle
|
|
24
|
-
* @param {string} [options.author='AI'] - Author metadata
|
|
25
|
-
* @param {string|null} [options.font=null] - Optional font
|
|
26
|
-
* @param {import('../services/numbering-service.js').NumberingService} options.numberingService - Numbering service
|
|
27
|
-
* @returns {Promise<import('../core/types.js').ReconciliationResult>}
|
|
28
|
-
*/
|
|
29
|
-
export async function executeListGeneration(options) {
|
|
30
|
-
const {
|
|
31
|
-
cleanText,
|
|
32
|
-
numberingContext,
|
|
33
|
-
originalRunModel = [],
|
|
34
|
-
originalText = '',
|
|
35
|
-
generateRedlines = true,
|
|
36
|
-
author = 'AI',
|
|
37
|
-
font = null,
|
|
38
|
-
revisionIdAllocator = null,
|
|
39
|
-
numberingService
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
42
|
-
const normalizedListText = normalizeCompositeListMarkers(cleanText);
|
|
43
|
-
const lineMetadata = buildLineMetadata(normalizedListText);
|
|
44
|
-
const rawLines = lineMetadata.map(line => line.raw);
|
|
45
|
-
const results = [];
|
|
46
|
-
const sourcePPr = getSourceParagraphProperties(originalRunModel);
|
|
47
|
-
const inheritedTypographyRPrXml = extractInheritedTypographyRPrXml(originalRunModel, sourcePPr);
|
|
48
|
-
const inheritedHeadingRPrXml = extractInheritedTypographyRPrXml(
|
|
49
|
-
originalRunModel,
|
|
50
|
-
sourcePPr,
|
|
51
|
-
['rFonts', 'kern', 'position', 'rtl', 'cs', 'lang']
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
let deletionRuns = [];
|
|
55
|
-
if (generateRedlines) {
|
|
56
|
-
if (originalRunModel && originalRunModel.length > 0) {
|
|
57
|
-
deletionRuns = originalRunModel
|
|
58
|
-
.filter(run => run.kind === 'text' || run.kind === 'run'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// paragraph
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
marker.setAttribute('w:date', metadata.date);
|
|
417
|
-
rPr.appendChild(marker);
|
|
418
|
-
return serializeXml(pPr);
|
|
419
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* List generation flow extracted from ReconciliationPipeline.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { preprocessMarkdown } from './markdown-processor.js';
|
|
6
|
+
import { matchListMarker, stripListMarker } from './list-markers.js';
|
|
7
|
+
import { serializeToOoxml } from './serialization.js';
|
|
8
|
+
import { generateTableOoxml } from '../services/table-reconciliation.js';
|
|
9
|
+
import { parseTable } from './content-analysis.js';
|
|
10
|
+
import { log } from '../adapters/logger.js';
|
|
11
|
+
import { NS_W, RunKind } from '../core/types.js';
|
|
12
|
+
import { parseOoxmlSafe, serializeXml } from '../adapters/xml-adapter.js';
|
|
13
|
+
import { setParagraphPropertiesMarkRevision } from '../engine/paragraph-mark-revisions.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes list generation when paragraph content expands into list/table blocks.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options - Generation options
|
|
19
|
+
* @param {string} options.cleanText - Preprocessed target text
|
|
20
|
+
* @param {Object|null} options.numberingContext - Existing numbering context
|
|
21
|
+
* @param {Array} [options.originalRunModel=[]] - Original run model
|
|
22
|
+
* @param {string} [options.originalText=''] - Original text fallback
|
|
23
|
+
* @param {boolean} [options.generateRedlines=true] - Track-change toggle
|
|
24
|
+
* @param {string} [options.author='AI'] - Author metadata
|
|
25
|
+
* @param {string|null} [options.font=null] - Optional font
|
|
26
|
+
* @param {import('../services/numbering-service.js').NumberingService} options.numberingService - Numbering service
|
|
27
|
+
* @returns {Promise<import('../core/types.js').ReconciliationResult>}
|
|
28
|
+
*/
|
|
29
|
+
export async function executeListGeneration(options) {
|
|
30
|
+
const {
|
|
31
|
+
cleanText,
|
|
32
|
+
numberingContext,
|
|
33
|
+
originalRunModel = [],
|
|
34
|
+
originalText = '',
|
|
35
|
+
generateRedlines = true,
|
|
36
|
+
author = 'AI',
|
|
37
|
+
font = null,
|
|
38
|
+
revisionIdAllocator = null,
|
|
39
|
+
numberingService
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const normalizedListText = normalizeCompositeListMarkers(cleanText);
|
|
43
|
+
const lineMetadata = buildLineMetadata(normalizedListText);
|
|
44
|
+
const rawLines = lineMetadata.map(line => line.raw);
|
|
45
|
+
const results = [];
|
|
46
|
+
const sourcePPr = getSourceParagraphProperties(originalRunModel);
|
|
47
|
+
const inheritedTypographyRPrXml = extractInheritedTypographyRPrXml(originalRunModel, sourcePPr);
|
|
48
|
+
const inheritedHeadingRPrXml = extractInheritedTypographyRPrXml(
|
|
49
|
+
originalRunModel,
|
|
50
|
+
sourcePPr,
|
|
51
|
+
['rFonts', 'kern', 'position', 'rtl', 'cs', 'lang']
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let deletionRuns = [];
|
|
55
|
+
if (generateRedlines) {
|
|
56
|
+
if (originalRunModel && originalRunModel.length > 0) {
|
|
57
|
+
deletionRuns = originalRunModel
|
|
58
|
+
.filter(run => run.kind === 'text' || run.kind === 'run'
|
|
59
|
+
|| run.kind === RunKind.CONTAINER_START || run.kind === RunKind.CONTAINER_END)
|
|
60
|
+
.map(run => (run.kind === RunKind.CONTAINER_START || run.kind === RunKind.CONTAINER_END
|
|
61
|
+
? run
|
|
62
|
+
: { ...run, kind: 'deletion', author }));
|
|
63
|
+
} else if (originalText && originalText.trim().length > 0) {
|
|
64
|
+
const trimmed = originalText.trim();
|
|
65
|
+
deletionRuns = [{
|
|
66
|
+
kind: 'deletion',
|
|
67
|
+
text: trimmed,
|
|
68
|
+
author,
|
|
69
|
+
startOffset: 0,
|
|
70
|
+
endOffset: trimmed.length
|
|
71
|
+
}];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sourceParagraphCount = (originalRunModel || [])
|
|
76
|
+
.filter(run => run.kind === RunKind.PARAGRAPH_START).length;
|
|
77
|
+
if (generateRedlines && sourceParagraphCount > 0 && (sourceParagraphCount > 1 || deletionRuns.length === 0)) {
|
|
78
|
+
// Each source paragraph keeps its own deleted paragraph (content and tracked-deleted mark), so
|
|
79
|
+
// Reject All restores every original paragraph boundary instead of merging them into one.
|
|
80
|
+
// Serialize each source paragraph on its own so empty paragraphs are emitted too (an empty
|
|
81
|
+
// paragraph carrying only its tracked-deleted mark).
|
|
82
|
+
let segment = null;
|
|
83
|
+
const flushSegment = () => {
|
|
84
|
+
if (!segment) return;
|
|
85
|
+
results.push(serializeToOoxml(segment, null, [], {
|
|
86
|
+
author,
|
|
87
|
+
generateRedlines,
|
|
88
|
+
revisionIdAllocator
|
|
89
|
+
}));
|
|
90
|
+
segment = null;
|
|
91
|
+
};
|
|
92
|
+
for (const run of originalRunModel) {
|
|
93
|
+
if (run.kind === RunKind.PARAGRAPH_START) {
|
|
94
|
+
flushSegment();
|
|
95
|
+
segment = [{
|
|
96
|
+
...run,
|
|
97
|
+
pPrElement: null,
|
|
98
|
+
pPrXml: addParagraphMarkRevision(
|
|
99
|
+
run.pPrElement || run.pPrXml || null,
|
|
100
|
+
'del',
|
|
101
|
+
author,
|
|
102
|
+
revisionIdAllocator
|
|
103
|
+
)
|
|
104
|
+
}];
|
|
105
|
+
} else if (segment && (run.kind === 'text' || run.kind === 'run')) {
|
|
106
|
+
segment.push({ ...run, kind: 'deletion', author });
|
|
107
|
+
} else if (segment && (run.kind === RunKind.CONTAINER_START || run.kind === RunKind.CONTAINER_END)) {
|
|
108
|
+
// Keep the hyperlink / content-control wrapper around the deleted runs so Reject All restores
|
|
109
|
+
// the text inside it, not as loose runs next to it.
|
|
110
|
+
segment.push(run);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
flushSegment();
|
|
114
|
+
} else if (generateRedlines && deletionRuns.length > 0) {
|
|
115
|
+
const deletedPPr = addParagraphMarkRevision(sourcePPr, 'del', author, revisionIdAllocator);
|
|
116
|
+
const deletedParagraph = serializeToOoxml(deletionRuns, deletedPPr, [], {
|
|
117
|
+
author,
|
|
118
|
+
generateRedlines,
|
|
119
|
+
revisionIdAllocator
|
|
120
|
+
});
|
|
121
|
+
results.push(deletedParagraph);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const indentStep = detectIndentationStep(rawLines);
|
|
125
|
+
log(`[ListGen] Detected indentation step: ${indentStep} spaces/chars`);
|
|
126
|
+
|
|
127
|
+
const firstMarker = lineMetadata.find(line => line.marker)?.marker || '';
|
|
128
|
+
const { format: defaultFormat } = numberingService.detectNumberingFormat(firstMarker);
|
|
129
|
+
log(`[ListGen] Detected primary marker: "${firstMarker}", format: ${defaultFormat}`);
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < lineMetadata.length; i++) {
|
|
132
|
+
const tableBlock = collectMarkdownTableBlock(lineMetadata, i);
|
|
133
|
+
if (tableBlock) {
|
|
134
|
+
const tableData = parseTable(tableBlock.tableText);
|
|
135
|
+
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
136
|
+
results.push(generateTableOoxml(tableData, {
|
|
137
|
+
generateRedlines,
|
|
138
|
+
author,
|
|
139
|
+
revisionIdAllocator,
|
|
140
|
+
trackAsBlock: true
|
|
141
|
+
}));
|
|
142
|
+
i = tableBlock.endIndex;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const line = lineMetadata[i];
|
|
148
|
+
const entry = buildListEntry(
|
|
149
|
+
line,
|
|
150
|
+
indentStep,
|
|
151
|
+
numberingContext,
|
|
152
|
+
numberingService,
|
|
153
|
+
generateRedlines,
|
|
154
|
+
author,
|
|
155
|
+
font,
|
|
156
|
+
revisionIdAllocator,
|
|
157
|
+
inheritedTypographyRPrXml,
|
|
158
|
+
inheritedHeadingRPrXml
|
|
159
|
+
);
|
|
160
|
+
results.push(entry.ooxml);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const numberingXml = numberingService.generateNumberingXml();
|
|
164
|
+
const finalOoxml = results.join('');
|
|
165
|
+
|
|
166
|
+
log(`[ListGen] ✅ Generated OOXML for ${results.length} paragraphs, total length: ${finalOoxml.length}`);
|
|
167
|
+
log(`[ListGen] First 200 chars: ${finalOoxml.substring(0, 200)}...`);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
ooxml: finalOoxml,
|
|
171
|
+
isValid: true,
|
|
172
|
+
warnings: ['Paragraph expanded to list fragment'],
|
|
173
|
+
type: 'fragment',
|
|
174
|
+
includeNumbering: true,
|
|
175
|
+
numberingXml
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Heuristically detects indentation step (spaces/tabs per level).
|
|
181
|
+
*
|
|
182
|
+
* @param {string[]} lines - Raw lines
|
|
183
|
+
* @returns {number}
|
|
184
|
+
*/
|
|
185
|
+
export function detectIndentationStep(lines) {
|
|
186
|
+
const indentations = lines
|
|
187
|
+
.map(line => line.match(/^(\s*)/)[0].length)
|
|
188
|
+
.filter(length => length > 0)
|
|
189
|
+
.sort((a, b) => a - b);
|
|
190
|
+
|
|
191
|
+
if (indentations.length === 0) return 2;
|
|
192
|
+
|
|
193
|
+
let minJump = indentations[0];
|
|
194
|
+
for (let i = 1; i < indentations.length; i++) {
|
|
195
|
+
const jump = indentations[i] - indentations[i - 1];
|
|
196
|
+
if (jump > 0 && jump < minJump) {
|
|
197
|
+
minJump = jump;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return minJump || 2;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildLineMetadata(cleanText) {
|
|
205
|
+
return cleanText
|
|
206
|
+
.split('\n')
|
|
207
|
+
.filter(line => line.trim().length > 0)
|
|
208
|
+
.map(raw => {
|
|
209
|
+
const markerMatch = matchListMarker(raw);
|
|
210
|
+
const headerMatch = raw.match(/^\s*(#{1,9})\s+(.*)/);
|
|
211
|
+
return {
|
|
212
|
+
raw,
|
|
213
|
+
marker: markerMatch ? markerMatch[2].trim() : '',
|
|
214
|
+
headerMatch,
|
|
215
|
+
indentSize: (raw.match(/^(\s*)/)?.[1].length) || 0,
|
|
216
|
+
isTableLine: /^\s*\|/.test(raw),
|
|
217
|
+
isTableSeparator: /^\s*\|?[\s:-]*-[-\s|:]*\|?\s*$/.test(raw)
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeCompositeListMarkers(text) {
|
|
223
|
+
const lines = String(text || '').split('\n');
|
|
224
|
+
const nonEmptyIndexes = lines
|
|
225
|
+
.map((line, index) => ({ line, index }))
|
|
226
|
+
.filter(entry => entry.line.trim().length > 0);
|
|
227
|
+
if (nonEmptyIndexes.length < 2) return text;
|
|
228
|
+
|
|
229
|
+
const rewrites = [];
|
|
230
|
+
let rewrittenCount = 0;
|
|
231
|
+
|
|
232
|
+
for (const { line, index } of nonEmptyIndexes) {
|
|
233
|
+
const outerMarkerMatch = matchListMarker(line);
|
|
234
|
+
if (!outerMarkerMatch) return text;
|
|
235
|
+
|
|
236
|
+
const stripped = stripListMarker(line);
|
|
237
|
+
const innerMarkerMatch = matchListMarker(stripped);
|
|
238
|
+
if (!innerMarkerMatch) return text;
|
|
239
|
+
|
|
240
|
+
const outerMarker = (outerMarkerMatch[2] || '').trim();
|
|
241
|
+
const innerMarker = (innerMarkerMatch[2] || '').trim();
|
|
242
|
+
if (!outerMarker || !innerMarker || outerMarker === innerMarker) return text;
|
|
243
|
+
|
|
244
|
+
const indent = outerMarkerMatch[1] || '';
|
|
245
|
+
const rewritten = `${indent}${stripped.trimStart()}`;
|
|
246
|
+
rewrites.push({ index, rewritten });
|
|
247
|
+
rewrittenCount++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (rewrittenCount < 2) return text;
|
|
251
|
+
|
|
252
|
+
const updated = lines.slice();
|
|
253
|
+
for (const rewrite of rewrites) {
|
|
254
|
+
updated[rewrite.index] = rewrite.rewritten;
|
|
255
|
+
}
|
|
256
|
+
log(`[ListGen] Normalized ${rewrittenCount} composite list markers (e.g., "- A." -> "A.").`);
|
|
257
|
+
return updated.join('\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function collectMarkdownTableBlock(lineMetadata, index) {
|
|
261
|
+
const current = lineMetadata[index];
|
|
262
|
+
const next = lineMetadata[index + 1];
|
|
263
|
+
|
|
264
|
+
if (!current?.isTableLine || !next?.isTableLine || !next?.isTableSeparator) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tableLines = [];
|
|
269
|
+
let cursor = index;
|
|
270
|
+
while (cursor < lineMetadata.length && lineMetadata[cursor].isTableLine) {
|
|
271
|
+
tableLines.push(lineMetadata[cursor].raw);
|
|
272
|
+
cursor++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
tableText: tableLines.join('\n'),
|
|
277
|
+
endIndex: cursor - 1
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildListEntry(
|
|
282
|
+
line,
|
|
283
|
+
indentStep,
|
|
284
|
+
numberingContext,
|
|
285
|
+
numberingService,
|
|
286
|
+
generateRedlines,
|
|
287
|
+
author,
|
|
288
|
+
font,
|
|
289
|
+
revisionIdAllocator,
|
|
290
|
+
inheritedTypographyRPrXml,
|
|
291
|
+
inheritedHeadingRPrXml
|
|
292
|
+
) {
|
|
293
|
+
let pPrXml = '';
|
|
294
|
+
let segmentText = '';
|
|
295
|
+
let insertedRPrXml = inheritedTypographyRPrXml;
|
|
296
|
+
|
|
297
|
+
if (line.headerMatch) {
|
|
298
|
+
const level = Math.min(line.headerMatch[1].length, 9);
|
|
299
|
+
const outlineLevel = Math.min(level - 1, 8);
|
|
300
|
+
const headingSizes = [32, 28, 26, 24, 22, 20, 20, 20, 20];
|
|
301
|
+
const headingSize = headingSizes[level - 1] || headingSizes[headingSizes.length - 1];
|
|
302
|
+
segmentText = line.headerMatch[2].trim();
|
|
303
|
+
pPrXml = `<w:pPr><w:pStyle w:val="Heading${level}"/><w:outlineLvl w:val="${outlineLevel}"/><w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr></w:pPr>`;
|
|
304
|
+
insertedRPrXml = mergeRunProperties(
|
|
305
|
+
inheritedHeadingRPrXml,
|
|
306
|
+
`<w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr>`
|
|
307
|
+
);
|
|
308
|
+
} else if (line.marker) {
|
|
309
|
+
const lineFormat = numberingService.detectNumberingFormat(line.marker);
|
|
310
|
+
const indentLevel = indentStep > 0 ? Math.floor(line.indentSize / indentStep) : 0;
|
|
311
|
+
const contextLevel = numberingContext?.ilvl || 0;
|
|
312
|
+
const ilvl = lineFormat.format === 'outline'
|
|
313
|
+
? Math.min(8, lineFormat.depth)
|
|
314
|
+
: Math.min(8, indentLevel + contextLevel);
|
|
315
|
+
|
|
316
|
+
segmentText = stripListMarker(line.raw);
|
|
317
|
+
const numId = numberingService.getOrCreateNumId({ type: lineFormat.format }, numberingContext);
|
|
318
|
+
pPrXml = numberingService.buildListPPr(numId, ilvl);
|
|
319
|
+
} else {
|
|
320
|
+
segmentText = line.raw;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (generateRedlines) {
|
|
324
|
+
pPrXml = addParagraphMarkRevision(
|
|
325
|
+
pPrXml || '<w:pPr/>',
|
|
326
|
+
'ins',
|
|
327
|
+
author,
|
|
328
|
+
revisionIdAllocator
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { cleanText, formatHints } = preprocessMarkdown(segmentText);
|
|
333
|
+
const runModel = [];
|
|
334
|
+
|
|
335
|
+
runModel.push({
|
|
336
|
+
kind: generateRedlines ? 'insertion' : 'run',
|
|
337
|
+
text: cleanText,
|
|
338
|
+
rPrXml: insertedRPrXml,
|
|
339
|
+
author,
|
|
340
|
+
startOffset: 0,
|
|
341
|
+
endOffset: cleanText.length
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
ooxml: serializeToOoxml(runModel, pPrXml, formatHints, {
|
|
346
|
+
author,
|
|
347
|
+
generateRedlines,
|
|
348
|
+
font,
|
|
349
|
+
revisionIdAllocator
|
|
350
|
+
})
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getSourceParagraphProperties(originalRunModel) {
|
|
355
|
+
const paragraphStart = (originalRunModel || []).find(run => run.kind === RunKind.PARAGRAPH_START);
|
|
356
|
+
return paragraphStart?.pPrElement || paragraphStart?.pPrXml || null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function extractInheritedTypographyRPrXml(
|
|
360
|
+
originalRunModel,
|
|
361
|
+
sourcePPr,
|
|
362
|
+
tagOrder = ['rFonts', 'kern', 'position', 'sz', 'szCs', 'rtl', 'cs', 'lang']
|
|
363
|
+
) {
|
|
364
|
+
const sources = (originalRunModel || [])
|
|
365
|
+
.filter(run => (run.kind === RunKind.TEXT || run.kind === 'text') && run.rPrXml)
|
|
366
|
+
.map(run => run.rPrXml);
|
|
367
|
+
|
|
368
|
+
if (sourcePPr) {
|
|
369
|
+
sources.push(typeof sourcePPr === 'string' ? sourcePPr : serializeXml(sourcePPr));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const inherited = [];
|
|
373
|
+
for (const tagName of tagOrder) {
|
|
374
|
+
let match = null;
|
|
375
|
+
for (const source of sources) {
|
|
376
|
+
match = String(source || '').match(new RegExp(`<w:${tagName}\\b[^>]*(?:\\/>|>[\\s\\S]*?<\\/w:${tagName}>)`));
|
|
377
|
+
if (match) break;
|
|
378
|
+
}
|
|
379
|
+
if (match) inherited.push(match[0]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return inherited.length > 0 ? `<w:rPr>${inherited.join('')}</w:rPr>` : '';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function mergeRunProperties(...sources) {
|
|
386
|
+
const properties = sources
|
|
387
|
+
.map(source => String(source || '').replace(/^\s*<w:rPr[^>]*>|<\/w:rPr>\s*$/g, ''))
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
.join('');
|
|
390
|
+
return properties ? `<w:rPr>${properties}</w:rPr>` : '';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function addParagraphMarkRevision(xml, type, author, revisionIdAllocator) {
|
|
394
|
+
let pPr;
|
|
395
|
+
let ownerDoc;
|
|
396
|
+
|
|
397
|
+
if (xml?.nodeType === 1) {
|
|
398
|
+
pPr = xml.cloneNode(true);
|
|
399
|
+
ownerDoc = pPr.ownerDocument;
|
|
400
|
+
} else {
|
|
401
|
+
const pPrXml = typeof xml === 'string' && xml.trim() ? xml : '<w:pPr/>';
|
|
402
|
+
const parsed = parseOoxmlSafe(`<w:root xmlns:w="${NS_W}">${pPrXml}</w:root>`);
|
|
403
|
+
if (!parsed.doc) {
|
|
404
|
+
throw new Error(parsed.error?.message || 'Could not parse paragraph properties for list revision');
|
|
405
|
+
}
|
|
406
|
+
ownerDoc = parsed.doc;
|
|
407
|
+
pPr = Array.from(ownerDoc.documentElement.childNodes || []).find(node => (
|
|
408
|
+
node.nodeType === 1 && node.localName === 'pPr'
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!pPr || !ownerDoc) throw new Error('List paragraph properties are unavailable');
|
|
413
|
+
setParagraphPropertiesMarkRevision(pPr, type === 'del' ? 'del' : 'ins', { author, revisionIdAllocator });
|
|
414
|
+
return serializeXml(pPr);
|
|
415
|
+
}
|