@ansonlai/docx-redline-js 0.8.2 → 0.8.3
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/CHANGELOG.md +586 -553
- package/README.md +901 -890
- package/core/list-targeting.js +12 -10
- package/dist/docx-redline-js.esm.js +154 -28
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +86 -86
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/dist/docx-redline.bundle.cjs +154 -28
- package/dist/docx-redline.bundle.cjs.map +3 -3
- package/dist/docx-redline.bundle.js +154 -28
- package/dist/docx-redline.bundle.js.map +3 -3
- package/orchestration/list-structural-fallback.js +530 -518
- package/package.json +1 -1
- package/pipeline/list-generation.js +203 -168
- package/services/document-inspection.js +10 -7
- package/services/document-operation-mutations.js +1597 -1586
- package/services/numbering-helpers.js +108 -0
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List generation flow extracted from ReconciliationPipeline.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { preprocessMarkdown } from './markdown-processor.js';
|
|
1
|
+
/**
|
|
2
|
+
* List generation flow extracted from ReconciliationPipeline.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { preprocessMarkdown } from './markdown-processor.js';
|
|
6
6
|
import { matchListMarker, stripListMarker } from './list-markers.js';
|
|
7
|
-
import { serializeToOoxml } from './serialization.js';
|
|
8
|
-
import { generateTableOoxml } from '../services/table-reconciliation.js';
|
|
7
|
+
import { serializeToOoxml } from './serialization.js';
|
|
8
|
+
import { generateTableOoxml } from '../services/table-reconciliation.js';
|
|
9
9
|
import { parseTable } from './content-analysis.js';
|
|
10
10
|
import { log } from '../adapters/logger.js';
|
|
11
11
|
import { createRevisionMetadata, NS_W, RunKind } from '../core/types.js';
|
|
12
12
|
import { parseOoxmlSafe, serializeXml } from '../adapters/xml-adapter.js';
|
|
13
13
|
import { createWordElement } from '../core/word-xml.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
|
-
*/
|
|
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
29
|
export async function executeListGeneration(options) {
|
|
30
|
-
const {
|
|
31
|
-
cleanText,
|
|
32
|
-
numberingContext,
|
|
33
|
-
originalRunModel = [],
|
|
34
|
-
originalText = '',
|
|
35
|
-
generateRedlines = true,
|
|
30
|
+
const {
|
|
31
|
+
cleanText,
|
|
32
|
+
numberingContext,
|
|
33
|
+
originalRunModel = [],
|
|
34
|
+
originalText = '',
|
|
35
|
+
generateRedlines = true,
|
|
36
36
|
author = 'AI',
|
|
37
37
|
font = null,
|
|
38
38
|
revisionIdAllocator = null,
|
|
39
39
|
numberingService
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
42
|
const normalizedListText = normalizeCompositeListMarkers(cleanText);
|
|
43
43
|
const lineMetadata = buildLineMetadata(normalizedListText);
|
|
44
44
|
const rawLines = lineMetadata.map(line => line.raw);
|
|
@@ -50,26 +50,61 @@ export async function executeListGeneration(options) {
|
|
|
50
50
|
sourcePPr,
|
|
51
51
|
['rFonts', 'kern', 'position', 'rtl', 'cs', 'lang']
|
|
52
52
|
);
|
|
53
|
-
|
|
54
|
-
let deletionRuns = [];
|
|
53
|
+
|
|
54
|
+
let deletionRuns = [];
|
|
55
55
|
if (generateRedlines) {
|
|
56
|
-
if (originalRunModel && originalRunModel.length > 0) {
|
|
57
|
-
deletionRuns = originalRunModel
|
|
58
|
-
.filter(run => run.kind === 'text' || run.kind === 'run')
|
|
59
|
-
.map(run => ({ ...run, kind: 'deletion', author }));
|
|
60
|
-
} else if (originalText && originalText.trim().length > 0) {
|
|
61
|
-
const trimmed = originalText.trim();
|
|
62
|
-
deletionRuns = [{
|
|
63
|
-
kind: 'deletion',
|
|
64
|
-
text: trimmed,
|
|
65
|
-
author,
|
|
66
|
-
startOffset: 0,
|
|
67
|
-
endOffset: trimmed.length
|
|
68
|
-
}];
|
|
56
|
+
if (originalRunModel && originalRunModel.length > 0) {
|
|
57
|
+
deletionRuns = originalRunModel
|
|
58
|
+
.filter(run => run.kind === 'text' || run.kind === 'run')
|
|
59
|
+
.map(run => ({ ...run, kind: 'deletion', author }));
|
|
60
|
+
} else if (originalText && originalText.trim().length > 0) {
|
|
61
|
+
const trimmed = originalText.trim();
|
|
62
|
+
deletionRuns = [{
|
|
63
|
+
kind: 'deletion',
|
|
64
|
+
text: trimmed,
|
|
65
|
+
author,
|
|
66
|
+
startOffset: 0,
|
|
67
|
+
endOffset: trimmed.length
|
|
68
|
+
}];
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
const sourceParagraphCount = (originalRunModel || [])
|
|
73
|
+
.filter(run => run.kind === RunKind.PARAGRAPH_START).length;
|
|
74
|
+
if (generateRedlines && sourceParagraphCount > 0 && (sourceParagraphCount > 1 || deletionRuns.length === 0)) {
|
|
75
|
+
// Each source paragraph keeps its own deleted paragraph (content and tracked-deleted mark), so
|
|
76
|
+
// Reject All restores every original paragraph boundary instead of merging them into one.
|
|
77
|
+
// Serialize each source paragraph on its own so empty paragraphs are emitted too (an empty
|
|
78
|
+
// paragraph carrying only its tracked-deleted mark).
|
|
79
|
+
let segment = null;
|
|
80
|
+
const flushSegment = () => {
|
|
81
|
+
if (!segment) return;
|
|
82
|
+
results.push(serializeToOoxml(segment, null, [], {
|
|
83
|
+
author,
|
|
84
|
+
generateRedlines,
|
|
85
|
+
revisionIdAllocator
|
|
86
|
+
}));
|
|
87
|
+
segment = null;
|
|
88
|
+
};
|
|
89
|
+
for (const run of originalRunModel) {
|
|
90
|
+
if (run.kind === RunKind.PARAGRAPH_START) {
|
|
91
|
+
flushSegment();
|
|
92
|
+
segment = [{
|
|
93
|
+
...run,
|
|
94
|
+
pPrElement: null,
|
|
95
|
+
pPrXml: addParagraphMarkRevision(
|
|
96
|
+
run.pPrElement || run.pPrXml || null,
|
|
97
|
+
'del',
|
|
98
|
+
author,
|
|
99
|
+
revisionIdAllocator
|
|
100
|
+
)
|
|
101
|
+
}];
|
|
102
|
+
} else if (segment && (run.kind === 'text' || run.kind === 'run')) {
|
|
103
|
+
segment.push({ ...run, kind: 'deletion', author });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
flushSegment();
|
|
107
|
+
} else if (generateRedlines && deletionRuns.length > 0) {
|
|
73
108
|
const deletedPPr = addParagraphMarkRevision(sourcePPr, 'del', author, revisionIdAllocator);
|
|
74
109
|
const deletedParagraph = serializeToOoxml(deletionRuns, deletedPPr, [], {
|
|
75
110
|
author,
|
|
@@ -78,102 +113,102 @@ export async function executeListGeneration(options) {
|
|
|
78
113
|
});
|
|
79
114
|
results.push(deletedParagraph);
|
|
80
115
|
}
|
|
81
|
-
|
|
82
|
-
const indentStep = detectIndentationStep(rawLines);
|
|
83
|
-
log(`[ListGen] Detected indentation step: ${indentStep} spaces/chars`);
|
|
84
|
-
|
|
116
|
+
|
|
117
|
+
const indentStep = detectIndentationStep(rawLines);
|
|
118
|
+
log(`[ListGen] Detected indentation step: ${indentStep} spaces/chars`);
|
|
119
|
+
|
|
85
120
|
const firstMarker = lineMetadata.find(line => line.marker)?.marker || '';
|
|
86
|
-
const { format: defaultFormat } = numberingService.detectNumberingFormat(firstMarker);
|
|
87
|
-
log(`[ListGen] Detected primary marker: "${firstMarker}", format: ${defaultFormat}`);
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < lineMetadata.length; i++) {
|
|
90
|
-
const tableBlock = collectMarkdownTableBlock(lineMetadata, i);
|
|
91
|
-
if (tableBlock) {
|
|
92
|
-
const tableData = parseTable(tableBlock.tableText);
|
|
93
|
-
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
121
|
+
const { format: defaultFormat } = numberingService.detectNumberingFormat(firstMarker);
|
|
122
|
+
log(`[ListGen] Detected primary marker: "${firstMarker}", format: ${defaultFormat}`);
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < lineMetadata.length; i++) {
|
|
125
|
+
const tableBlock = collectMarkdownTableBlock(lineMetadata, i);
|
|
126
|
+
if (tableBlock) {
|
|
127
|
+
const tableData = parseTable(tableBlock.tableText);
|
|
128
|
+
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
94
129
|
results.push(generateTableOoxml(tableData, {
|
|
95
130
|
generateRedlines,
|
|
96
131
|
author,
|
|
97
132
|
revisionIdAllocator,
|
|
98
133
|
trackAsBlock: true
|
|
99
134
|
}));
|
|
100
|
-
i = tableBlock.endIndex;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const line = lineMetadata[i];
|
|
135
|
+
i = tableBlock.endIndex;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const line = lineMetadata[i];
|
|
106
141
|
const entry = buildListEntry(
|
|
107
142
|
line,
|
|
108
143
|
indentStep,
|
|
109
|
-
numberingContext,
|
|
110
|
-
numberingService,
|
|
111
|
-
generateRedlines,
|
|
144
|
+
numberingContext,
|
|
145
|
+
numberingService,
|
|
146
|
+
generateRedlines,
|
|
112
147
|
author,
|
|
113
148
|
font,
|
|
114
149
|
revisionIdAllocator,
|
|
115
150
|
inheritedTypographyRPrXml,
|
|
116
151
|
inheritedHeadingRPrXml
|
|
117
152
|
);
|
|
118
|
-
results.push(entry.ooxml);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const numberingXml = numberingService.generateNumberingXml();
|
|
122
|
-
const finalOoxml = results.join('');
|
|
153
|
+
results.push(entry.ooxml);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const numberingXml = numberingService.generateNumberingXml();
|
|
157
|
+
const finalOoxml = results.join('');
|
|
123
158
|
|
|
124
159
|
log(`[ListGen] ✅ Generated OOXML for ${results.length} paragraphs, total length: ${finalOoxml.length}`);
|
|
125
160
|
log(`[ListGen] First 200 chars: ${finalOoxml.substring(0, 200)}...`);
|
|
126
|
-
|
|
127
|
-
return {
|
|
161
|
+
|
|
162
|
+
return {
|
|
128
163
|
ooxml: finalOoxml,
|
|
129
|
-
isValid: true,
|
|
130
|
-
warnings: ['Paragraph expanded to list fragment'],
|
|
131
|
-
type: 'fragment',
|
|
132
|
-
includeNumbering: true,
|
|
133
|
-
numberingXml
|
|
134
|
-
};
|
|
164
|
+
isValid: true,
|
|
165
|
+
warnings: ['Paragraph expanded to list fragment'],
|
|
166
|
+
type: 'fragment',
|
|
167
|
+
includeNumbering: true,
|
|
168
|
+
numberingXml
|
|
169
|
+
};
|
|
135
170
|
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Heuristically detects indentation step (spaces/tabs per level).
|
|
139
|
-
*
|
|
140
|
-
* @param {string[]} lines - Raw lines
|
|
141
|
-
* @returns {number}
|
|
142
|
-
*/
|
|
143
|
-
export function detectIndentationStep(lines) {
|
|
144
|
-
const indentations = lines
|
|
145
|
-
.map(line => line.match(/^(\s*)/)[0].length)
|
|
146
|
-
.filter(length => length > 0)
|
|
147
|
-
.sort((a, b) => a - b);
|
|
148
|
-
|
|
149
|
-
if (indentations.length === 0) return 2;
|
|
150
|
-
|
|
151
|
-
let minJump = indentations[0];
|
|
152
|
-
for (let i = 1; i < indentations.length; i++) {
|
|
153
|
-
const jump = indentations[i] - indentations[i - 1];
|
|
154
|
-
if (jump > 0 && jump < minJump) {
|
|
155
|
-
minJump = jump;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return minJump || 2;
|
|
160
|
-
}
|
|
161
|
-
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Heuristically detects indentation step (spaces/tabs per level).
|
|
174
|
+
*
|
|
175
|
+
* @param {string[]} lines - Raw lines
|
|
176
|
+
* @returns {number}
|
|
177
|
+
*/
|
|
178
|
+
export function detectIndentationStep(lines) {
|
|
179
|
+
const indentations = lines
|
|
180
|
+
.map(line => line.match(/^(\s*)/)[0].length)
|
|
181
|
+
.filter(length => length > 0)
|
|
182
|
+
.sort((a, b) => a - b);
|
|
183
|
+
|
|
184
|
+
if (indentations.length === 0) return 2;
|
|
185
|
+
|
|
186
|
+
let minJump = indentations[0];
|
|
187
|
+
for (let i = 1; i < indentations.length; i++) {
|
|
188
|
+
const jump = indentations[i] - indentations[i - 1];
|
|
189
|
+
if (jump > 0 && jump < minJump) {
|
|
190
|
+
minJump = jump;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return minJump || 2;
|
|
195
|
+
}
|
|
196
|
+
|
|
162
197
|
function buildLineMetadata(cleanText) {
|
|
163
|
-
return cleanText
|
|
164
|
-
.split('\n')
|
|
165
|
-
.filter(line => line.trim().length > 0)
|
|
166
|
-
.map(raw => {
|
|
167
|
-
const markerMatch = matchListMarker(raw);
|
|
168
|
-
const headerMatch = raw.match(/^\s*(#{1,9})\s+(.*)/);
|
|
169
|
-
return {
|
|
170
|
-
raw,
|
|
171
|
-
marker: markerMatch ? markerMatch[2].trim() : '',
|
|
172
|
-
headerMatch,
|
|
173
|
-
indentSize: (raw.match(/^(\s*)/)?.[1].length) || 0,
|
|
174
|
-
isTableLine: /^\s*\|/.test(raw),
|
|
175
|
-
isTableSeparator: /^\s*\|?[\s:-]*-[-\s|:]*\|?\s*$/.test(raw)
|
|
176
|
-
};
|
|
198
|
+
return cleanText
|
|
199
|
+
.split('\n')
|
|
200
|
+
.filter(line => line.trim().length > 0)
|
|
201
|
+
.map(raw => {
|
|
202
|
+
const markerMatch = matchListMarker(raw);
|
|
203
|
+
const headerMatch = raw.match(/^\s*(#{1,9})\s+(.*)/);
|
|
204
|
+
return {
|
|
205
|
+
raw,
|
|
206
|
+
marker: markerMatch ? markerMatch[2].trim() : '',
|
|
207
|
+
headerMatch,
|
|
208
|
+
indentSize: (raw.match(/^(\s*)/)?.[1].length) || 0,
|
|
209
|
+
isTableLine: /^\s*\|/.test(raw),
|
|
210
|
+
isTableSeparator: /^\s*\|?[\s:-]*-[-\s|:]*\|?\s*$/.test(raw)
|
|
211
|
+
};
|
|
177
212
|
});
|
|
178
213
|
}
|
|
179
214
|
|
|
@@ -214,34 +249,34 @@ function normalizeCompositeListMarkers(text) {
|
|
|
214
249
|
log(`[ListGen] Normalized ${rewrittenCount} composite list markers (e.g., "- A." -> "A.").`);
|
|
215
250
|
return updated.join('\n');
|
|
216
251
|
}
|
|
217
|
-
|
|
218
|
-
function collectMarkdownTableBlock(lineMetadata, index) {
|
|
219
|
-
const current = lineMetadata[index];
|
|
220
|
-
const next = lineMetadata[index + 1];
|
|
221
|
-
|
|
222
|
-
if (!current?.isTableLine || !next?.isTableLine || !next?.isTableSeparator) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const tableLines = [];
|
|
227
|
-
let cursor = index;
|
|
228
|
-
while (cursor < lineMetadata.length && lineMetadata[cursor].isTableLine) {
|
|
229
|
-
tableLines.push(lineMetadata[cursor].raw);
|
|
230
|
-
cursor++;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
tableText: tableLines.join('\n'),
|
|
235
|
-
endIndex: cursor - 1
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
252
|
+
|
|
253
|
+
function collectMarkdownTableBlock(lineMetadata, index) {
|
|
254
|
+
const current = lineMetadata[index];
|
|
255
|
+
const next = lineMetadata[index + 1];
|
|
256
|
+
|
|
257
|
+
if (!current?.isTableLine || !next?.isTableLine || !next?.isTableSeparator) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const tableLines = [];
|
|
262
|
+
let cursor = index;
|
|
263
|
+
while (cursor < lineMetadata.length && lineMetadata[cursor].isTableLine) {
|
|
264
|
+
tableLines.push(lineMetadata[cursor].raw);
|
|
265
|
+
cursor++;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
tableText: tableLines.join('\n'),
|
|
270
|
+
endIndex: cursor - 1
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
239
274
|
function buildListEntry(
|
|
240
275
|
line,
|
|
241
276
|
indentStep,
|
|
242
|
-
numberingContext,
|
|
243
|
-
numberingService,
|
|
244
|
-
generateRedlines,
|
|
277
|
+
numberingContext,
|
|
278
|
+
numberingService,
|
|
279
|
+
generateRedlines,
|
|
245
280
|
author,
|
|
246
281
|
font,
|
|
247
282
|
revisionIdAllocator,
|
|
@@ -251,11 +286,11 @@ function buildListEntry(
|
|
|
251
286
|
let pPrXml = '';
|
|
252
287
|
let segmentText = '';
|
|
253
288
|
let insertedRPrXml = inheritedTypographyRPrXml;
|
|
254
|
-
|
|
255
|
-
if (line.headerMatch) {
|
|
256
|
-
const level = Math.min(line.headerMatch[1].length, 9);
|
|
257
|
-
const outlineLevel = Math.min(level - 1, 8);
|
|
258
|
-
const headingSizes = [32, 28, 26, 24, 22, 20, 20, 20, 20];
|
|
289
|
+
|
|
290
|
+
if (line.headerMatch) {
|
|
291
|
+
const level = Math.min(line.headerMatch[1].length, 9);
|
|
292
|
+
const outlineLevel = Math.min(level - 1, 8);
|
|
293
|
+
const headingSizes = [32, 28, 26, 24, 22, 20, 20, 20, 20];
|
|
259
294
|
const headingSize = headingSizes[level - 1] || headingSizes[headingSizes.length - 1];
|
|
260
295
|
segmentText = line.headerMatch[2].trim();
|
|
261
296
|
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>`;
|
|
@@ -263,17 +298,17 @@ function buildListEntry(
|
|
|
263
298
|
inheritedHeadingRPrXml,
|
|
264
299
|
`<w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr>`
|
|
265
300
|
);
|
|
266
|
-
} else if (line.marker) {
|
|
267
|
-
const lineFormat = numberingService.detectNumberingFormat(line.marker);
|
|
268
|
-
const indentLevel = indentStep > 0 ? Math.floor(line.indentSize / indentStep) : 0;
|
|
269
|
-
const contextLevel = numberingContext?.ilvl || 0;
|
|
270
|
-
const ilvl = lineFormat.format === 'outline'
|
|
271
|
-
? Math.min(8, lineFormat.depth)
|
|
272
|
-
: Math.min(8, indentLevel + contextLevel);
|
|
273
|
-
|
|
274
|
-
segmentText = stripListMarker(line.raw);
|
|
275
|
-
const numId = numberingService.getOrCreateNumId({ type: lineFormat.format }, numberingContext);
|
|
276
|
-
pPrXml = numberingService.buildListPPr(numId, ilvl);
|
|
301
|
+
} else if (line.marker) {
|
|
302
|
+
const lineFormat = numberingService.detectNumberingFormat(line.marker);
|
|
303
|
+
const indentLevel = indentStep > 0 ? Math.floor(line.indentSize / indentStep) : 0;
|
|
304
|
+
const contextLevel = numberingContext?.ilvl || 0;
|
|
305
|
+
const ilvl = lineFormat.format === 'outline'
|
|
306
|
+
? Math.min(8, lineFormat.depth)
|
|
307
|
+
: Math.min(8, indentLevel + contextLevel);
|
|
308
|
+
|
|
309
|
+
segmentText = stripListMarker(line.raw);
|
|
310
|
+
const numId = numberingService.getOrCreateNumId({ type: lineFormat.format }, numberingContext);
|
|
311
|
+
pPrXml = numberingService.buildListPPr(numId, ilvl);
|
|
277
312
|
} else {
|
|
278
313
|
segmentText = line.raw;
|
|
279
314
|
}
|
|
@@ -294,13 +329,13 @@ function buildListEntry(
|
|
|
294
329
|
kind: generateRedlines ? 'insertion' : 'run',
|
|
295
330
|
text: cleanText,
|
|
296
331
|
rPrXml: insertedRPrXml,
|
|
297
|
-
author,
|
|
298
|
-
startOffset: 0,
|
|
299
|
-
endOffset: cleanText.length
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
ooxml: serializeToOoxml(runModel, pPrXml, formatHints, {
|
|
332
|
+
author,
|
|
333
|
+
startOffset: 0,
|
|
334
|
+
endOffset: cleanText.length
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
ooxml: serializeToOoxml(runModel, pPrXml, formatHints, {
|
|
304
339
|
author,
|
|
305
340
|
generateRedlines,
|
|
306
341
|
font,
|
|
@@ -8,6 +8,9 @@ import { extractDocumentPartsEntries, computeRevisionTokenSync } from './revisio
|
|
|
8
8
|
const attr = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || '';
|
|
9
9
|
const descendants = (node, name) => Array.from(node?.getElementsByTagNameNS?.(NS_W, name) || []);
|
|
10
10
|
const first = (node, name) => descendants(node, name)[0] || null;
|
|
11
|
+
const directChild = (node, name) => Array.from(node?.childNodes || []).find(child => child?.nodeType === 1 && child.localName === name && (!child.namespaceURI || child.namespaceURI === NS_W)) || null;
|
|
12
|
+
/** Current paragraph properties: the paragraph's direct w:pPr only (never the historical w:pPrChange/w:pPr). */
|
|
13
|
+
const currentPPr = paragraph => directChild(paragraph, 'pPr');
|
|
11
14
|
function hasAncestor(node, localName) { let cursor = node?.parentNode; while (cursor) { if (cursor.localName === localName && (!cursor.namespaceURI || cursor.namespaceURI === NS_W)) return true; cursor = cursor.parentNode; } return false; }
|
|
12
15
|
|
|
13
16
|
function parseXml(xml, partName, required = false) {
|
|
@@ -18,11 +21,11 @@ function parseXml(xml, partName, required = false) {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
function paragraphListProperties(paragraph) {
|
|
21
|
-
const numPr =
|
|
24
|
+
const numPr = directChild(currentPPr(paragraph), 'numPr');
|
|
22
25
|
if (!numPr) return null;
|
|
23
|
-
const numId = attr(
|
|
26
|
+
const numId = attr(directChild(numPr, 'numId'), 'val');
|
|
24
27
|
if (!numId || numId === '0') return null;
|
|
25
|
-
return { numId, level: Number.parseInt(attr(
|
|
28
|
+
return { numId, level: Number.parseInt(attr(directChild(numPr, 'ilvl'), 'val') || '0', 10) || 0 };
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function parseNumbering(numberingDoc) {
|
|
@@ -100,11 +103,11 @@ function createNumberingResolver(numberingDoc) {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
function headingLevel(paragraph) {
|
|
103
|
-
const pPr =
|
|
104
|
-
const style = attr(
|
|
106
|
+
const pPr = currentPPr(paragraph);
|
|
107
|
+
const style = attr(directChild(pPr, 'pStyle'), 'val');
|
|
105
108
|
const match = style.match(/^heading\s*([1-9])$/i);
|
|
106
109
|
if (match) return Math.min(Number(match[1]), 6);
|
|
107
|
-
const outline = Number.parseInt(attr(
|
|
110
|
+
const outline = Number.parseInt(attr(directChild(pPr, 'outlineLvl'), 'val'), 10);
|
|
108
111
|
return Number.isInteger(outline) ? Math.min(outline + 1, 6) : null;
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -283,7 +286,7 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
283
286
|
const ids = [...new Set([...descendants(paragraph, 'commentRangeStart'), ...descendants(paragraph, 'commentReference')].map(node => attr(node, 'id')).filter(Boolean))];
|
|
284
287
|
const authors = revisionAuthors(paragraph);
|
|
285
288
|
const list = resolveNumbering(paragraphListProperties(paragraph));
|
|
286
|
-
const styleId = attr(
|
|
289
|
+
const styleId = attr(directChild(currentPPr(paragraph), 'pStyle'), 'val') || null;
|
|
287
290
|
const structure = structuralContext(paragraph, text);
|
|
288
291
|
const index = zeroIndex + 1;
|
|
289
292
|
const provision = list?.label && list.format !== 'bullet' ? list.label : null;
|