@ansonlai/docx-redline-js 0.1.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/AGENTS.md +176 -0
- package/ARCHITECTURE.md +121 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/adapters/config.js +43 -0
- package/adapters/logger.js +89 -0
- package/adapters/xml-adapter.js +74 -0
- package/core/list-targeting.js +398 -0
- package/core/ooxml-identifiers.js +15 -0
- package/core/paragraph-offset-policy.js +50 -0
- package/core/paragraph-targeting.js +501 -0
- package/core/table-targeting.js +233 -0
- package/core/types.js +204 -0
- package/core/xml-query.js +99 -0
- package/dist/docx-redline-js.esm.js +8801 -0
- package/dist/docx-redline-js.esm.js.map +7 -0
- package/dist/docx-redline-js.esm.min.js +195 -0
- package/dist/docx-redline-js.esm.min.js.map +7 -0
- package/engine/format-application.js +358 -0
- package/engine/format-extraction.js +232 -0
- package/engine/format-paragraph-targeting.js +208 -0
- package/engine/format-span-application.js +178 -0
- package/engine/formatting-removal.js +330 -0
- package/engine/oxml-engine.js +279 -0
- package/engine/reconstruction-mapper.js +270 -0
- package/engine/reconstruction-mode.js +38 -0
- package/engine/reconstruction-writer.js +276 -0
- package/engine/rpr-helpers.js +194 -0
- package/engine/run-builders.js +235 -0
- package/engine/surgical-mode.js +520 -0
- package/engine/table-cell-context.js +151 -0
- package/engine/table-mode.js +172 -0
- package/index.js +308 -0
- package/orchestration/list-markdown.js +141 -0
- package/orchestration/list-parsing.js +73 -0
- package/orchestration/list-structural-fallback.js +530 -0
- package/orchestration/redline-operation-converter.js +141 -0
- package/orchestration/route-plan.js +160 -0
- package/package.json +76 -0
- package/pipeline/content-analysis.js +107 -0
- package/pipeline/diff-engine.js +204 -0
- package/pipeline/ingestion-export.js +255 -0
- package/pipeline/ingestion-paragraph.js +351 -0
- package/pipeline/ingestion-table.js +169 -0
- package/pipeline/ingestion-xml.js +39 -0
- package/pipeline/ingestion.js +8 -0
- package/pipeline/list-generation.js +280 -0
- package/pipeline/list-markers.js +77 -0
- package/pipeline/markdown-processor.js +160 -0
- package/pipeline/patching.js +408 -0
- package/pipeline/pipeline.js +326 -0
- package/pipeline/serialization.js +395 -0
- package/services/browser-demo-prompt-context.js +345 -0
- package/services/comment-builders.js +60 -0
- package/services/comment-engine.js +248 -0
- package/services/comment-locator.js +197 -0
- package/services/comment-package.js +113 -0
- package/services/numbering-helpers.js +416 -0
- package/services/numbering-service.js +290 -0
- package/services/package-builder.js +147 -0
- package/services/standalone-docx-plumbing.js +443 -0
- package/services/standalone-operation-runner.js +1169 -0
- package/services/table-reconciliation.js +344 -0
- package/standalone.js +5 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OOXML Reconciliation Pipeline - Numbering Service
|
|
3
|
+
*
|
|
4
|
+
* Manages list numbering to ensure continuation and consistent formatting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NS_W, NumberFormat, NumberSuffix } from '../core/types.js';
|
|
8
|
+
|
|
9
|
+
export class NumberingService {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.contextMap = new Map(); // Cache for numIds found in the document
|
|
12
|
+
this.nextNumId = 1000;
|
|
13
|
+
this.customConfigs = []; // Track custom configs needed for current run
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Preserves an existing numId for a given format signature.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} signature - Format signature (e.g., 'bullet' or 'decimal')
|
|
20
|
+
* @param {string} numId - Existing numId from Word
|
|
21
|
+
*/
|
|
22
|
+
registerExistingNumId(signature, numId) {
|
|
23
|
+
this.contextMap.set(signature, numId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the best numId to use for a requested list format.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} formatConfig - Requested format (type, depth)
|
|
30
|
+
* @param {Object} existingContext - Context from adjacent paragraph
|
|
31
|
+
* @returns {string} The numId to use
|
|
32
|
+
*/
|
|
33
|
+
getOrCreateNumId(formatConfig, existingContext = null) {
|
|
34
|
+
const requestedType = formatConfig.type || NumberFormat.BULLET;
|
|
35
|
+
|
|
36
|
+
// Priority 1: Use existing context if it matches the requested type
|
|
37
|
+
if (existingContext && existingContext.numId) {
|
|
38
|
+
if (existingContext.type === requestedType || existingContext.type === 'unknown') {
|
|
39
|
+
return existingContext.numId;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Priority 2: Use cached numId for this format
|
|
44
|
+
if (this.contextMap.has(requestedType)) {
|
|
45
|
+
return this.contextMap.get(requestedType);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Priority 3: Special Handling for Outline (recursive 1.1.1)
|
|
49
|
+
if (requestedType === NumberFormat.OUTLINE) {
|
|
50
|
+
// Use our new predefined Outline scheme (numId 3)
|
|
51
|
+
return '3';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Priority 4: Fallback based on type
|
|
55
|
+
if (requestedType === NumberFormat.DECIMAL) return '2';
|
|
56
|
+
if (requestedType === NumberFormat.BULLET) return '1';
|
|
57
|
+
|
|
58
|
+
// Priority 5: Handle specialized Alpha/Roman at Level 0 (or if context doesn't match)
|
|
59
|
+
// If we are at Level 0 and want something other than Decimal, we need a custom config
|
|
60
|
+
const ilvl = existingContext ? parseInt(existingContext.ilvl || '0') : 0;
|
|
61
|
+
if (ilvl === 0 && requestedType !== NumberFormat.DECIMAL && requestedType !== NumberFormat.BULLET) {
|
|
62
|
+
// Find or create a custom config for this specific format
|
|
63
|
+
const signature = `custom_${requestedType}`;
|
|
64
|
+
if (this.contextMap.has(signature)) {
|
|
65
|
+
return this.contextMap.get(signature);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const newNumId = String(this.nextNumId++);
|
|
69
|
+
this.customConfigs.push({
|
|
70
|
+
numId: newNumId,
|
|
71
|
+
levels: [
|
|
72
|
+
{ format: requestedType, suffix: formatConfig.suffix || NumberSuffix.PERIOD }
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
this.contextMap.set(signature, newNumId);
|
|
76
|
+
return newNumId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Default fallback to NumId 2 (Legal/Nested)
|
|
80
|
+
return '2';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detects numbering format from a string marker (e.g. "1.", "(a)", "i.", "1.1.")
|
|
85
|
+
*
|
|
86
|
+
* @param {string} marker - The marker text
|
|
87
|
+
* @returns {Object} { format, suffix, depth }
|
|
88
|
+
*/
|
|
89
|
+
detectNumberingFormat(marker) {
|
|
90
|
+
const m = (marker || '').trim();
|
|
91
|
+
if (!m) return { format: NumberFormat.BULLET, suffix: NumberSuffix.NONE, depth: 0 };
|
|
92
|
+
|
|
93
|
+
// Bullet
|
|
94
|
+
if (/^[-*•]$/.test(m)) {
|
|
95
|
+
return { format: NumberFormat.BULLET, suffix: NumberSuffix.NONE, depth: 0 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Hierarchical outline: 1.1.2 or 4.1.2.3
|
|
99
|
+
const outlineMatch = m.match(/^(\d+(?:\.\d+)+)\.?$/);
|
|
100
|
+
if (outlineMatch) {
|
|
101
|
+
const depth = outlineMatch[1].split('.').length - 1;
|
|
102
|
+
return { format: NumberFormat.OUTLINE, suffix: NumberSuffix.PERIOD, depth };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parenthesized formats: (a), (i), (1)
|
|
106
|
+
if (/^\([a-z]\)$/.test(m)) {
|
|
107
|
+
return { format: NumberFormat.LOWER_ALPHA, suffix: NumberSuffix.PAREN_BOTH, depth: 0 };
|
|
108
|
+
}
|
|
109
|
+
if (/^\([ivxlc]+\)$/i.test(m)) {
|
|
110
|
+
const isLower = m === m.toLowerCase();
|
|
111
|
+
return { format: isLower ? NumberFormat.LOWER_ROMAN : NumberFormat.UPPER_ROMAN, suffix: NumberSuffix.PAREN_BOTH, depth: 0 };
|
|
112
|
+
}
|
|
113
|
+
if (/^\(\d+\)$/.test(m)) {
|
|
114
|
+
return { format: NumberFormat.DECIMAL, suffix: NumberSuffix.PAREN_BOTH, depth: 0 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Standard formats with period: 1., a., A., i., I.
|
|
118
|
+
if (/^\d+\.$/.test(m)) {
|
|
119
|
+
return { format: NumberFormat.DECIMAL, suffix: NumberSuffix.PERIOD, depth: 0 };
|
|
120
|
+
}
|
|
121
|
+
if (/^[a-z]\.$/.test(m)) {
|
|
122
|
+
return { format: NumberFormat.LOWER_ALPHA, suffix: NumberSuffix.PERIOD, depth: 0 };
|
|
123
|
+
}
|
|
124
|
+
if (/^[A-Z]\.$/.test(m)) {
|
|
125
|
+
return { format: NumberFormat.UPPER_ALPHA, suffix: NumberSuffix.PERIOD, depth: 0 };
|
|
126
|
+
}
|
|
127
|
+
if (/^[ivxlc]+\.$/i.test(m)) {
|
|
128
|
+
const isLower = m === m.toLowerCase();
|
|
129
|
+
return { format: isLower ? NumberFormat.LOWER_ROMAN : NumberFormat.UPPER_ROMAN, suffix: NumberSuffix.PERIOD, depth: 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Default to decimal if it looks like a number
|
|
133
|
+
if (/^\d+/.test(m)) return { format: NumberFormat.DECIMAL, suffix: NumberSuffix.PERIOD, depth: 0 };
|
|
134
|
+
|
|
135
|
+
return { format: NumberFormat.BULLET, suffix: NumberSuffix.NONE, depth: 0 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Maps internal format to OOXML numFmt string
|
|
140
|
+
*/
|
|
141
|
+
formatToOoxmlNumFmt(format) {
|
|
142
|
+
const map = {
|
|
143
|
+
[NumberFormat.DECIMAL]: 'decimal',
|
|
144
|
+
[NumberFormat.LOWER_ALPHA]: 'lowerLetter',
|
|
145
|
+
[NumberFormat.UPPER_ALPHA]: 'upperLetter',
|
|
146
|
+
[NumberFormat.LOWER_ROMAN]: 'lowerRoman',
|
|
147
|
+
[NumberFormat.UPPER_ROMAN]: 'upperRoman',
|
|
148
|
+
[NumberFormat.BULLET]: 'bullet',
|
|
149
|
+
[NumberFormat.OUTLINE]: 'decimal'
|
|
150
|
+
};
|
|
151
|
+
return map[format] || 'decimal';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Maps levels and suffix to OOXML lvlText
|
|
156
|
+
*/
|
|
157
|
+
suffixToOoxmlLevelText(format, suffix, ilvl = 0) {
|
|
158
|
+
if (format === NumberFormat.BULLET) return '•';
|
|
159
|
+
|
|
160
|
+
// Placeholder for current level
|
|
161
|
+
const num = `%${ilvl + 1}`;
|
|
162
|
+
|
|
163
|
+
if (format === NumberFormat.OUTLINE) {
|
|
164
|
+
// Outline %1.%2.%3.
|
|
165
|
+
return Array(ilvl + 1).fill(0).map((_, i) => `%${i + 1}`).join('.') + '.';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (suffix) {
|
|
169
|
+
case NumberSuffix.PERIOD: return `${num}.`;
|
|
170
|
+
case NumberSuffix.PAREN_RIGHT: return `${num})`;
|
|
171
|
+
case NumberSuffix.PAREN_BOTH: return `(${num})`;
|
|
172
|
+
default: return num;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Builds paragraph properties for a list item.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} numId - The numId
|
|
180
|
+
* @param {number} ilvl - Indentation level
|
|
181
|
+
* @returns {string} Serialized w:pPr XML
|
|
182
|
+
*/
|
|
183
|
+
buildListPPr(numId, ilvl, options = {}) {
|
|
184
|
+
const includeListParagraphStyle = options.includeListParagraphStyle === true;
|
|
185
|
+
const styleXml = includeListParagraphStyle ? '\n <w:pStyle w:val="ListParagraph"/>' : '';
|
|
186
|
+
return `
|
|
187
|
+
<w:pPr>${styleXml}
|
|
188
|
+
<w:numPr>
|
|
189
|
+
<w:ilvl w:val="${ilvl}"/>
|
|
190
|
+
<w:numId w:val="${numId}"/>
|
|
191
|
+
</w:numPr>
|
|
192
|
+
</w:pPr>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generates a full w:numbering XML block including custom legal schemes.
|
|
198
|
+
*
|
|
199
|
+
* @param {Array} externalConfigs - Optional array of { numId, levels: [{ format, suffix }] }
|
|
200
|
+
* @returns {string} w:numbering XML
|
|
201
|
+
*/
|
|
202
|
+
generateNumberingXml(externalConfigs = []) {
|
|
203
|
+
// Merge internal customConfigs with external ones
|
|
204
|
+
const allCustomConfigs = [...this.customConfigs, ...externalConfigs];
|
|
205
|
+
|
|
206
|
+
// Default Bullet (numId 1)
|
|
207
|
+
let abstractNum0 = `
|
|
208
|
+
<w:abstractNum w:abstractNumId="0">
|
|
209
|
+
<w:multiLevelType w:val="hybridMultilevel"/>
|
|
210
|
+
${[0, 1, 2, 3, 4, 5, 6, 7, 8].map(lvl => `
|
|
211
|
+
<w:lvl w:ilvl="${lvl}">
|
|
212
|
+
<w:start w:val="1"/>
|
|
213
|
+
<w:numFmt w:val="${lvl % 3 === 0 ? 'bullet' : lvl % 3 === 1 ? 'circle' : 'square'}"/>
|
|
214
|
+
<w:lvlText w:val="${lvl % 3 === 0 ? '•' : lvl % 3 === 1 ? '○' : '■'}"/>
|
|
215
|
+
<w:lvlJc w:val="left"/>
|
|
216
|
+
<w:pPr><w:ind w:left="${720 * (lvl + 1)}" w:hanging="360"/></w:pPr>
|
|
217
|
+
</w:lvl>`).join('')}
|
|
218
|
+
</w:abstractNum>`;
|
|
219
|
+
|
|
220
|
+
// Default Numbered (numId 2) - US/Legal Style
|
|
221
|
+
// Level 0: 1.
|
|
222
|
+
// Level 1: (a)
|
|
223
|
+
// Level 2: (i)
|
|
224
|
+
// Level 3: (1)
|
|
225
|
+
// Level 4: (a) - repeating with different indent...
|
|
226
|
+
let abstractNum1 = `
|
|
227
|
+
<w:abstractNum w:abstractNumId="1">
|
|
228
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
229
|
+
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
|
|
230
|
+
<w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="lowerLetter"/><w:lvlText w:val="(%2)"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="1440" w:hanging="360"/></w:pPr></w:lvl>
|
|
231
|
+
<w:lvl w:ilvl="2"><w:start w:val="1"/><w:numFmt w:val="lowerRoman"/><w:lvlText w:val="(%3)"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="2160" w:hanging="360"/></w:pPr></w:lvl>
|
|
232
|
+
<w:lvl w:ilvl="3"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="(%4)"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="2880" w:hanging="360"/></w:pPr></w:lvl>
|
|
233
|
+
<w:lvl w:ilvl="4"><w:start w:val="1"/><w:numFmt w:val="lowerLetter"/><w:lvlText w:val="%5."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="3600" w:hanging="360"/></w:pPr></w:lvl>
|
|
234
|
+
</w:abstractNum>`;
|
|
235
|
+
|
|
236
|
+
// Outline Numbered (numId 3) - 1 / 1.1 / 1.1.1
|
|
237
|
+
let abstractNum2 = `
|
|
238
|
+
<w:abstractNum w:abstractNumId="2">
|
|
239
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
240
|
+
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
|
|
241
|
+
<w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1.%2"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="1440" w:hanging="360"/></w:pPr></w:lvl>
|
|
242
|
+
<w:lvl w:ilvl="2"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1.%2.%3"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="2160" w:hanging="360"/></w:pPr></w:lvl>
|
|
243
|
+
<w:lvl w:ilvl="3"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1.%2.%3.%4"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="2880" w:hanging="360"/></w:pPr></w:lvl>
|
|
244
|
+
<w:lvl w:ilvl="4"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1.%2.%3.%4.%5"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="3600" w:hanging="360"/></w:pPr></w:lvl>
|
|
245
|
+
</w:abstractNum>`;
|
|
246
|
+
|
|
247
|
+
// Handle custom configurations (e.g. outline 1.1.1)
|
|
248
|
+
let customAbstractNums = '';
|
|
249
|
+
let customNums = '';
|
|
250
|
+
|
|
251
|
+
allCustomConfigs.forEach((config, idx) => {
|
|
252
|
+
const absId = 10 + idx;
|
|
253
|
+
customAbstractNums += `
|
|
254
|
+
<w:abstractNum w:abstractNumId="${absId}">
|
|
255
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
256
|
+
${config.levels.map((l, ilvl) => `
|
|
257
|
+
<w:lvl w:ilvl="${ilvl}">
|
|
258
|
+
<w:start w:val="1"/>
|
|
259
|
+
<w:numFmt w:val="${this.formatToOoxmlNumFmt(l.format)}"/>
|
|
260
|
+
<w:lvlText w:val="${this.suffixToOoxmlLevelText(l.format, l.suffix, ilvl)}"/>
|
|
261
|
+
<w:lvlJc w:val="left"/>
|
|
262
|
+
<w:pPr><w:ind w:left="${720 * (ilvl + 1)}" w:hanging="360"/></w:pPr>
|
|
263
|
+
</w:lvl>`).join('')}
|
|
264
|
+
</w:abstractNum>`;
|
|
265
|
+
|
|
266
|
+
customNums += `
|
|
267
|
+
<w:num w:numId="${config.numId}">
|
|
268
|
+
<w:abstractNumId w:val="${absId}"/>
|
|
269
|
+
</w:num>`;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return `
|
|
273
|
+
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
274
|
+
${abstractNum0}
|
|
275
|
+
${abstractNum1}
|
|
276
|
+
${abstractNum2}
|
|
277
|
+
${customAbstractNums}
|
|
278
|
+
<w:num w:numId="1">
|
|
279
|
+
<w:abstractNumId w:val="0"/>
|
|
280
|
+
</w:num>
|
|
281
|
+
<w:num w:numId="2">
|
|
282
|
+
<w:abstractNumId w:val="1"/>
|
|
283
|
+
</w:num>
|
|
284
|
+
<w:num w:numId="3">
|
|
285
|
+
<w:abstractNumId w:val="2"/>
|
|
286
|
+
</w:num>
|
|
287
|
+
${customNums}
|
|
288
|
+
</w:numbering>`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OOXML package builders.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes `pkg:package` construction used by pipeline/engine/services.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
8
|
+
const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
9
|
+
const NS_PKG = 'http://schemas.microsoft.com/office/2006/xmlPackage';
|
|
10
|
+
const NS_REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
11
|
+
|
|
12
|
+
const REL_OFFICE_DOCUMENT = '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>';
|
|
13
|
+
const REL_NUMBERING = '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>';
|
|
14
|
+
const REL_COMMENTS = '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="comments.xml"/>';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_NUMBERING_XML = `
|
|
17
|
+
<w:numbering xmlns:w="${NS_W}">
|
|
18
|
+
<w:abstractNum w:abstractNumId="0">
|
|
19
|
+
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="•"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
|
|
20
|
+
</w:abstractNum>
|
|
21
|
+
<w:abstractNum w:abstractNumId="1">
|
|
22
|
+
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
|
|
23
|
+
</w:abstractNum>
|
|
24
|
+
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
|
|
25
|
+
<w:num w:numId="2"><w:abstractNumId w:val="1"/></w:num>
|
|
26
|
+
</w:numbering>`.trim();
|
|
27
|
+
|
|
28
|
+
function stripXmlDeclaration(xml) {
|
|
29
|
+
if (!xml) return '';
|
|
30
|
+
return xml.replace(/<\?xml[^>]*\?>/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildWordDocument(bodyXml, includeRelationshipsNamespace = true) {
|
|
34
|
+
const rNs = includeRelationshipsNamespace ? ` xmlns:r="${NS_R}"` : '';
|
|
35
|
+
return `<w:document xmlns:w="${NS_W}"${rNs}><w:body>${bodyXml}</w:body></w:document>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildCommentsPart(commentsXml) {
|
|
39
|
+
return `
|
|
40
|
+
<pkg:part pkg:name="/word/comments.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml">
|
|
41
|
+
<pkg:xmlData>
|
|
42
|
+
${commentsXml}
|
|
43
|
+
</pkg:xmlData>
|
|
44
|
+
</pkg:part>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildNumberingPart(numberingXml) {
|
|
48
|
+
return `
|
|
49
|
+
<pkg:part pkg:name="/word/numbering.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml">
|
|
50
|
+
<pkg:xmlData>
|
|
51
|
+
${stripXmlDeclaration(numberingXml)}
|
|
52
|
+
</pkg:xmlData>
|
|
53
|
+
</pkg:part>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildPackage(documentXml, documentRelationshipsXml = '', extraPartsXml = '') {
|
|
57
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
58
|
+
<pkg:package xmlns:pkg="${NS_PKG}">
|
|
59
|
+
<pkg:part pkg:name="/_rels/.rels" pkg:contentType="application/vnd.openxmlformats-package.relationships+xml">
|
|
60
|
+
<pkg:xmlData>
|
|
61
|
+
<Relationships xmlns="${NS_REL}">
|
|
62
|
+
${REL_OFFICE_DOCUMENT}
|
|
63
|
+
</Relationships>
|
|
64
|
+
</pkg:xmlData>
|
|
65
|
+
</pkg:part>
|
|
66
|
+
<pkg:part pkg:name="/word/_rels/document.xml.rels" pkg:contentType="application/vnd.openxmlformats-package.relationships+xml">
|
|
67
|
+
<pkg:xmlData>
|
|
68
|
+
<Relationships xmlns="${NS_REL}">
|
|
69
|
+
${documentRelationshipsXml}
|
|
70
|
+
</Relationships>
|
|
71
|
+
</pkg:xmlData>
|
|
72
|
+
</pkg:part>${extraPartsXml}
|
|
73
|
+
<pkg:part pkg:name="/word/document.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml">
|
|
74
|
+
<pkg:xmlData>
|
|
75
|
+
${documentXml}
|
|
76
|
+
</pkg:xmlData>
|
|
77
|
+
</pkg:part>
|
|
78
|
+
</pkg:package>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds a package for paragraph/document-fragment insertion.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} paragraphXml - Paragraph OOXML content
|
|
85
|
+
* @param {import('../core/types.js').DocumentFragmentOptions} [options={}] - Packaging options
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function buildDocumentFragmentPackage(paragraphXml, options = {}) {
|
|
89
|
+
const {
|
|
90
|
+
includeNumbering = false,
|
|
91
|
+
numberingXml = null,
|
|
92
|
+
appendTrailingParagraph = true
|
|
93
|
+
} = options;
|
|
94
|
+
|
|
95
|
+
const bodyXml = appendTrailingParagraph
|
|
96
|
+
? `${paragraphXml}<w:p><w:pPr></w:pPr></w:p>`
|
|
97
|
+
: paragraphXml;
|
|
98
|
+
|
|
99
|
+
const documentXml = buildWordDocument(bodyXml, true);
|
|
100
|
+
|
|
101
|
+
let relationshipsXml = '';
|
|
102
|
+
let extraPartsXml = '';
|
|
103
|
+
|
|
104
|
+
if (numberingXml) {
|
|
105
|
+
relationshipsXml = REL_NUMBERING;
|
|
106
|
+
extraPartsXml += buildNumberingPart(numberingXml);
|
|
107
|
+
} else if (includeNumbering) {
|
|
108
|
+
relationshipsXml = REL_NUMBERING;
|
|
109
|
+
extraPartsXml += buildNumberingPart(DEFAULT_NUMBERING_XML);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return buildPackage(documentXml, relationshipsXml, extraPartsXml);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Builds a minimal package containing only paragraph XML in `word/document.xml`.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} paragraphXml - Paragraph OOXML content
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
export function buildParagraphOnlyPackage(paragraphXml) {
|
|
122
|
+
const documentXml = buildWordDocument(paragraphXml, true);
|
|
123
|
+
return buildPackage(documentXml);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds a minimal paragraph package with comments relationship + part.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} paragraphXml - Paragraph OOXML content
|
|
130
|
+
* @param {string} commentsXml - Comments part content
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function buildParagraphCommentsPackage(paragraphXml, commentsXml) {
|
|
134
|
+
const documentXml = buildWordDocument(paragraphXml, false);
|
|
135
|
+
return buildPackage(documentXml, REL_COMMENTS, buildCommentsPart(commentsXml));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Builds a package around caller-provided document XML, adding comments part/relationship.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} documentXml - Document XML payload for `word/document.xml`
|
|
142
|
+
* @param {string} commentsXml - Comments part content
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
export function buildDocumentCommentsPackage(documentXml, commentsXml) {
|
|
146
|
+
return buildPackage(documentXml, REL_COMMENTS, buildCommentsPart(commentsXml));
|
|
147
|
+
}
|