docrev 0.9.13 → 0.9.15
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/.claude/settings.local.json +9 -9
- package/.gitattributes +1 -1
- package/CHANGELOG.md +149 -149
- package/PLAN-tables-and-postprocess.md +850 -850
- package/README.md +411 -391
- package/bin/rev.js +11 -11
- package/bin/rev.ts +145 -145
- package/completions/rev.bash +127 -127
- package/completions/rev.ps1 +210 -210
- package/completions/rev.zsh +207 -207
- package/dev_notes/stress2/build_adversarial.ts +186 -186
- package/dev_notes/stress2/drift_matcher.ts +62 -62
- package/dev_notes/stress2/probe_anchors.ts +35 -35
- package/dev_notes/stress2/project/discussion.before.md +3 -3
- package/dev_notes/stress2/project/discussion.md +3 -3
- package/dev_notes/stress2/project/methods.before.md +20 -20
- package/dev_notes/stress2/project/methods.md +20 -20
- package/dev_notes/stress2/project/rev.yaml +5 -5
- package/dev_notes/stress2/project/sections.yaml +4 -4
- package/dev_notes/stress2/sections.yaml +5 -5
- package/dev_notes/stress2/trace_placement.ts +50 -50
- package/dev_notes/stresstest_boundaries.ts +27 -27
- package/dev_notes/stresstest_drift_apply.ts +43 -43
- package/dev_notes/stresstest_drift_compare.ts +43 -43
- package/dev_notes/stresstest_drift_v2.ts +54 -54
- package/dev_notes/stresstest_inspect.ts +54 -54
- package/dev_notes/stresstest_pstyle.ts +55 -55
- package/dev_notes/stresstest_section_debug.ts +23 -23
- package/dev_notes/stresstest_split.ts +70 -70
- package/dev_notes/stresstest_trace.ts +19 -19
- package/dev_notes/stresstest_verify_no_overwrite.ts +40 -40
- package/dist/lib/build.d.ts +38 -1
- package/dist/lib/build.d.ts.map +1 -1
- package/dist/lib/build.js +68 -30
- package/dist/lib/build.js.map +1 -1
- package/dist/lib/commands/build.d.ts.map +1 -1
- package/dist/lib/commands/build.js +38 -5
- package/dist/lib/commands/build.js.map +1 -1
- package/dist/lib/commands/utilities.js +164 -164
- package/dist/lib/commands/word-tools.js +8 -8
- package/dist/lib/grammar.js +3 -3
- package/dist/lib/pdf-comments.js +44 -44
- package/dist/lib/plugins.js +57 -57
- package/dist/lib/pptx-themes.js +115 -115
- package/dist/lib/spelling.js +2 -2
- package/dist/lib/templates.js +387 -387
- package/dist/lib/themes.js +51 -51
- package/eslint.config.js +27 -27
- package/lib/anchor-match.ts +276 -276
- package/lib/annotations.ts +644 -644
- package/lib/build.ts +1300 -1251
- package/lib/citations.ts +160 -160
- package/lib/commands/build.ts +833 -801
- package/lib/commands/citations.ts +515 -515
- package/lib/commands/comments.ts +1050 -1050
- package/lib/commands/context.ts +174 -174
- package/lib/commands/core.ts +309 -309
- package/lib/commands/doi.ts +435 -435
- package/lib/commands/file-ops.ts +372 -372
- package/lib/commands/history.ts +320 -320
- package/lib/commands/index.ts +87 -87
- package/lib/commands/init.ts +259 -259
- package/lib/commands/merge-resolve.ts +378 -378
- package/lib/commands/preview.ts +178 -178
- package/lib/commands/project-info.ts +244 -244
- package/lib/commands/quality.ts +517 -517
- package/lib/commands/response.ts +454 -454
- package/lib/commands/section-boundaries.ts +82 -82
- package/lib/commands/sections.ts +451 -451
- package/lib/commands/sync.ts +706 -706
- package/lib/commands/text-ops.ts +449 -449
- package/lib/commands/utilities.ts +448 -448
- package/lib/commands/verify-anchors.ts +272 -272
- package/lib/commands/word-tools.ts +340 -340
- package/lib/comment-realign.ts +517 -517
- package/lib/config.ts +84 -84
- package/lib/crossref.ts +781 -781
- package/lib/csl.ts +191 -191
- package/lib/dependencies.ts +98 -98
- package/lib/diff-engine.ts +465 -465
- package/lib/doi-cache.ts +115 -115
- package/lib/doi.ts +897 -897
- package/lib/equations.ts +506 -506
- package/lib/errors.ts +346 -346
- package/lib/format.ts +541 -541
- package/lib/git.ts +326 -326
- package/lib/grammar.ts +303 -303
- package/lib/image-registry.ts +180 -180
- package/lib/import.ts +911 -911
- package/lib/journals.ts +543 -543
- package/lib/merge.ts +633 -633
- package/lib/orcid.ts +144 -144
- package/lib/pdf-comments.ts +263 -263
- package/lib/pdf-import.ts +524 -524
- package/lib/plugins.ts +362 -362
- package/lib/postprocess.ts +188 -188
- package/lib/pptx-color-filter.lua +37 -37
- package/lib/pptx-template.ts +469 -469
- package/lib/pptx-themes.ts +483 -483
- package/lib/protect-restore.ts +520 -520
- package/lib/rate-limiter.ts +94 -94
- package/lib/response.ts +197 -197
- package/lib/restore-references.ts +240 -240
- package/lib/review.ts +327 -327
- package/lib/schema.ts +417 -417
- package/lib/scientific-words.ts +73 -73
- package/lib/sections.ts +335 -335
- package/lib/slides.ts +756 -756
- package/lib/spelling.ts +334 -334
- package/lib/templates.ts +526 -526
- package/lib/themes.ts +742 -742
- package/lib/trackchanges.ts +247 -247
- package/lib/tui.ts +450 -450
- package/lib/types.ts +550 -550
- package/lib/undo.ts +250 -250
- package/lib/utils.ts +69 -69
- package/lib/variables.ts +179 -179
- package/lib/word-extraction.ts +806 -806
- package/lib/word.ts +643 -643
- package/lib/wordcomments.ts +817 -817
- package/package.json +137 -137
- package/scripts/postbuild.js +28 -28
- package/skill/REFERENCE.md +473 -431
- package/skill/SKILL.md +274 -258
- package/tsconfig.json +26 -26
- package/types/index.d.ts +525 -525
package/lib/trackchanges.ts
CHANGED
|
@@ -1,247 +1,247 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Track changes module - Apply markdown annotations as Word track changes
|
|
3
|
-
*
|
|
4
|
-
* Converts CriticMarkup annotations to Word OOXML track changes format.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as fs from 'fs';
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
|
-
import AdmZip from 'adm-zip';
|
|
11
|
-
import type { TrackChangeMarker } from './types.js';
|
|
12
|
-
import { escapeXml } from './utils.js';
|
|
13
|
-
|
|
14
|
-
interface PrepareOptions {
|
|
15
|
-
author?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface PrepareResult {
|
|
19
|
-
text: string;
|
|
20
|
-
markers: TrackChangeMarker[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ApplyResult {
|
|
24
|
-
success: boolean;
|
|
25
|
-
message: string;
|
|
26
|
-
stats?: {
|
|
27
|
-
insertions: number;
|
|
28
|
-
deletions: number;
|
|
29
|
-
substitutions: number;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Prepare text with CriticMarkup annotations for track changes
|
|
35
|
-
* Replaces annotations with markers that can be processed in DOCX
|
|
36
|
-
*
|
|
37
|
-
* @param text - Text with CriticMarkup annotations
|
|
38
|
-
* @param options - Options
|
|
39
|
-
* @returns Processed text and marker info
|
|
40
|
-
*/
|
|
41
|
-
export function prepareForTrackChanges(text: string, options: PrepareOptions = {}): PrepareResult {
|
|
42
|
-
const { author = 'Reviewer' } = options;
|
|
43
|
-
const markers: TrackChangeMarker[] = [];
|
|
44
|
-
let markerId = 0;
|
|
45
|
-
|
|
46
|
-
let result = text;
|
|
47
|
-
|
|
48
|
-
// Process insertions: {++text++}
|
|
49
|
-
result = result.replace(/\{\+\+(.+?)\+\+\}/gs, (match, content) => {
|
|
50
|
-
const id = markerId++;
|
|
51
|
-
markers.push({
|
|
52
|
-
id,
|
|
53
|
-
type: 'insert',
|
|
54
|
-
content,
|
|
55
|
-
author,
|
|
56
|
-
});
|
|
57
|
-
return `{{TC_${id}}}`;
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Process deletions: {--text--}
|
|
61
|
-
result = result.replace(/\{--(.+?)--\}/gs, (match, content) => {
|
|
62
|
-
const id = markerId++;
|
|
63
|
-
markers.push({
|
|
64
|
-
id,
|
|
65
|
-
type: 'delete',
|
|
66
|
-
content,
|
|
67
|
-
author,
|
|
68
|
-
});
|
|
69
|
-
return `{{TC_${id}}}`;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Process substitutions: {~~old~>new~~}
|
|
73
|
-
result = result.replace(/\{~~(.+?)~>(.+?)~~\}/gs, (match, old, replacement) => {
|
|
74
|
-
const id = markerId++;
|
|
75
|
-
markers.push({
|
|
76
|
-
id,
|
|
77
|
-
type: 'substitute',
|
|
78
|
-
content: old,
|
|
79
|
-
replacement,
|
|
80
|
-
author,
|
|
81
|
-
});
|
|
82
|
-
return `{{TC_${id}}}`;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Process comments: {>>Author: comment<<}
|
|
86
|
-
result = result.replace(/\{>>(.+?)<<\}/gs, (match, content) => {
|
|
87
|
-
const id = markerId++;
|
|
88
|
-
// Extract author if present (format: "Author: comment")
|
|
89
|
-
const colonIdx = content.indexOf(':');
|
|
90
|
-
let commentAuthor = author;
|
|
91
|
-
let commentText = content;
|
|
92
|
-
if (colonIdx > 0 && colonIdx < 30) {
|
|
93
|
-
commentAuthor = content.slice(0, colonIdx).trim();
|
|
94
|
-
commentText = content.slice(colonIdx + 1).trim();
|
|
95
|
-
}
|
|
96
|
-
markers.push({
|
|
97
|
-
id,
|
|
98
|
-
type: 'comment',
|
|
99
|
-
content: commentText,
|
|
100
|
-
author: commentAuthor,
|
|
101
|
-
});
|
|
102
|
-
return `{{TC_${id}}}`;
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return { text: result, markers };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Apply track changes markers to a Word document
|
|
110
|
-
*
|
|
111
|
-
* @param docxPath - Path to input DOCX file
|
|
112
|
-
* @param markers - Markers from prepareForTrackChanges
|
|
113
|
-
* @param outputPath - Path for output DOCX file
|
|
114
|
-
* @returns Result with success status and message
|
|
115
|
-
*/
|
|
116
|
-
export async function applyTrackChangesToDocx(
|
|
117
|
-
docxPath: string,
|
|
118
|
-
markers: TrackChangeMarker[],
|
|
119
|
-
outputPath: string
|
|
120
|
-
): Promise<ApplyResult> {
|
|
121
|
-
if (!fs.existsSync(docxPath)) {
|
|
122
|
-
return { success: false, message: `File not found: ${docxPath}` };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let zip: AdmZip;
|
|
126
|
-
try {
|
|
127
|
-
zip = new AdmZip(docxPath);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
const error = err as Error;
|
|
130
|
-
return { success: false, message: `Invalid DOCX file: ${error.message}` };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Read document.xml
|
|
134
|
-
const docEntry = zip.getEntry('word/document.xml');
|
|
135
|
-
if (!docEntry) {
|
|
136
|
-
return { success: false, message: 'Invalid DOCX: no document.xml' };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let documentXml = zip.readAsText(docEntry);
|
|
140
|
-
|
|
141
|
-
// Generate ISO date for track changes
|
|
142
|
-
const now = new Date().toISOString();
|
|
143
|
-
|
|
144
|
-
// Replace markers with track change XML
|
|
145
|
-
for (const marker of markers) {
|
|
146
|
-
const placeholder = `{{TC_${marker.id}}}`;
|
|
147
|
-
let replacement = '';
|
|
148
|
-
|
|
149
|
-
const escapedContent = escapeXml(marker.content);
|
|
150
|
-
const escapedAuthor = escapeXml(marker.author);
|
|
151
|
-
|
|
152
|
-
if (marker.type === 'insert') {
|
|
153
|
-
replacement = `<w:ins w:id="${marker.id}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:t>${escapedContent}</w:t></w:r></w:ins>`;
|
|
154
|
-
} else if (marker.type === 'delete') {
|
|
155
|
-
replacement = `<w:del w:id="${marker.id}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:delText>${escapedContent}</w:delText></w:r></w:del>`;
|
|
156
|
-
} else if (marker.type === 'substitute') {
|
|
157
|
-
const escapedReplacement = escapeXml(marker.replacement || '');
|
|
158
|
-
replacement = `<w:del w:id="${marker.id}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:delText>${escapedContent}</w:delText></w:r></w:del><w:ins w:id="${marker.id + 1000}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:t>${escapedReplacement}</w:t></w:r></w:ins>`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
documentXml = documentXml.replace(placeholder, replacement);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Update document.xml
|
|
165
|
-
zip.updateFile('word/document.xml', Buffer.from(documentXml));
|
|
166
|
-
|
|
167
|
-
// Enable track revisions in settings.xml
|
|
168
|
-
const settingsEntry = zip.getEntry('word/settings.xml');
|
|
169
|
-
if (settingsEntry) {
|
|
170
|
-
let settingsXml = zip.readAsText(settingsEntry);
|
|
171
|
-
if (!settingsXml.includes('w:trackRevisions')) {
|
|
172
|
-
settingsXml = settingsXml.replace(
|
|
173
|
-
'</w:settings>',
|
|
174
|
-
'<w:trackRevisions/></w:settings>'
|
|
175
|
-
);
|
|
176
|
-
zip.updateFile('word/settings.xml', Buffer.from(settingsXml));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Write output
|
|
181
|
-
zip.writeZip(outputPath);
|
|
182
|
-
|
|
183
|
-
return { success: true, message: `Created ${outputPath} with track changes` };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Build a Word document with track changes from annotated markdown
|
|
188
|
-
*
|
|
189
|
-
* @param mdPath - Path to markdown file with CriticMarkup
|
|
190
|
-
* @param docxPath - Output path for Word document
|
|
191
|
-
* @param options - Options
|
|
192
|
-
* @returns Result with success status and message
|
|
193
|
-
*/
|
|
194
|
-
export async function buildWithTrackChanges(
|
|
195
|
-
mdPath: string,
|
|
196
|
-
docxPath: string,
|
|
197
|
-
options: PrepareOptions = {}
|
|
198
|
-
): Promise<ApplyResult> {
|
|
199
|
-
const { author = 'Author' } = options;
|
|
200
|
-
|
|
201
|
-
if (!fs.existsSync(mdPath)) {
|
|
202
|
-
return { success: false, message: `File not found: ${mdPath}` };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
206
|
-
|
|
207
|
-
// Prepare for track changes
|
|
208
|
-
const { text: prepared, markers } = prepareForTrackChanges(content, { author });
|
|
209
|
-
|
|
210
|
-
// If no annotations, just build normally
|
|
211
|
-
if (markers.length === 0) {
|
|
212
|
-
try {
|
|
213
|
-
execSync(`pandoc "${mdPath}" -o "${docxPath}"`, { encoding: 'utf-8' });
|
|
214
|
-
return { success: true, message: `Created ${docxPath}` };
|
|
215
|
-
} catch (err) {
|
|
216
|
-
const error = err as Error;
|
|
217
|
-
return { success: false, message: error.message };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Write prepared content to temp file
|
|
222
|
-
const tempDir = path.dirname(mdPath);
|
|
223
|
-
const tempMd = path.join(tempDir, `.temp-${Date.now()}.md`);
|
|
224
|
-
const tempDocx = path.join(tempDir, `.temp-${Date.now()}.docx`);
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
fs.writeFileSync(tempMd, prepared, 'utf-8');
|
|
228
|
-
|
|
229
|
-
// Build with pandoc
|
|
230
|
-
execSync(`pandoc "${tempMd}" -o "${tempDocx}"`, { encoding: 'utf-8' });
|
|
231
|
-
|
|
232
|
-
// Apply track changes
|
|
233
|
-
const result = await applyTrackChangesToDocx(tempDocx, markers, docxPath);
|
|
234
|
-
|
|
235
|
-
// Clean up temp files
|
|
236
|
-
fs.unlinkSync(tempMd);
|
|
237
|
-
fs.unlinkSync(tempDocx);
|
|
238
|
-
|
|
239
|
-
return result;
|
|
240
|
-
} catch (err) {
|
|
241
|
-
// Clean up on error
|
|
242
|
-
if (fs.existsSync(tempMd)) fs.unlinkSync(tempMd);
|
|
243
|
-
if (fs.existsSync(tempDocx)) fs.unlinkSync(tempDocx);
|
|
244
|
-
const error = err as Error;
|
|
245
|
-
return { success: false, message: error.message };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Track changes module - Apply markdown annotations as Word track changes
|
|
3
|
+
*
|
|
4
|
+
* Converts CriticMarkup annotations to Word OOXML track changes format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import AdmZip from 'adm-zip';
|
|
11
|
+
import type { TrackChangeMarker } from './types.js';
|
|
12
|
+
import { escapeXml } from './utils.js';
|
|
13
|
+
|
|
14
|
+
interface PrepareOptions {
|
|
15
|
+
author?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PrepareResult {
|
|
19
|
+
text: string;
|
|
20
|
+
markers: TrackChangeMarker[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ApplyResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
message: string;
|
|
26
|
+
stats?: {
|
|
27
|
+
insertions: number;
|
|
28
|
+
deletions: number;
|
|
29
|
+
substitutions: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prepare text with CriticMarkup annotations for track changes
|
|
35
|
+
* Replaces annotations with markers that can be processed in DOCX
|
|
36
|
+
*
|
|
37
|
+
* @param text - Text with CriticMarkup annotations
|
|
38
|
+
* @param options - Options
|
|
39
|
+
* @returns Processed text and marker info
|
|
40
|
+
*/
|
|
41
|
+
export function prepareForTrackChanges(text: string, options: PrepareOptions = {}): PrepareResult {
|
|
42
|
+
const { author = 'Reviewer' } = options;
|
|
43
|
+
const markers: TrackChangeMarker[] = [];
|
|
44
|
+
let markerId = 0;
|
|
45
|
+
|
|
46
|
+
let result = text;
|
|
47
|
+
|
|
48
|
+
// Process insertions: {++text++}
|
|
49
|
+
result = result.replace(/\{\+\+(.+?)\+\+\}/gs, (match, content) => {
|
|
50
|
+
const id = markerId++;
|
|
51
|
+
markers.push({
|
|
52
|
+
id,
|
|
53
|
+
type: 'insert',
|
|
54
|
+
content,
|
|
55
|
+
author,
|
|
56
|
+
});
|
|
57
|
+
return `{{TC_${id}}}`;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Process deletions: {--text--}
|
|
61
|
+
result = result.replace(/\{--(.+?)--\}/gs, (match, content) => {
|
|
62
|
+
const id = markerId++;
|
|
63
|
+
markers.push({
|
|
64
|
+
id,
|
|
65
|
+
type: 'delete',
|
|
66
|
+
content,
|
|
67
|
+
author,
|
|
68
|
+
});
|
|
69
|
+
return `{{TC_${id}}}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Process substitutions: {~~old~>new~~}
|
|
73
|
+
result = result.replace(/\{~~(.+?)~>(.+?)~~\}/gs, (match, old, replacement) => {
|
|
74
|
+
const id = markerId++;
|
|
75
|
+
markers.push({
|
|
76
|
+
id,
|
|
77
|
+
type: 'substitute',
|
|
78
|
+
content: old,
|
|
79
|
+
replacement,
|
|
80
|
+
author,
|
|
81
|
+
});
|
|
82
|
+
return `{{TC_${id}}}`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Process comments: {>>Author: comment<<}
|
|
86
|
+
result = result.replace(/\{>>(.+?)<<\}/gs, (match, content) => {
|
|
87
|
+
const id = markerId++;
|
|
88
|
+
// Extract author if present (format: "Author: comment")
|
|
89
|
+
const colonIdx = content.indexOf(':');
|
|
90
|
+
let commentAuthor = author;
|
|
91
|
+
let commentText = content;
|
|
92
|
+
if (colonIdx > 0 && colonIdx < 30) {
|
|
93
|
+
commentAuthor = content.slice(0, colonIdx).trim();
|
|
94
|
+
commentText = content.slice(colonIdx + 1).trim();
|
|
95
|
+
}
|
|
96
|
+
markers.push({
|
|
97
|
+
id,
|
|
98
|
+
type: 'comment',
|
|
99
|
+
content: commentText,
|
|
100
|
+
author: commentAuthor,
|
|
101
|
+
});
|
|
102
|
+
return `{{TC_${id}}}`;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { text: result, markers };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Apply track changes markers to a Word document
|
|
110
|
+
*
|
|
111
|
+
* @param docxPath - Path to input DOCX file
|
|
112
|
+
* @param markers - Markers from prepareForTrackChanges
|
|
113
|
+
* @param outputPath - Path for output DOCX file
|
|
114
|
+
* @returns Result with success status and message
|
|
115
|
+
*/
|
|
116
|
+
export async function applyTrackChangesToDocx(
|
|
117
|
+
docxPath: string,
|
|
118
|
+
markers: TrackChangeMarker[],
|
|
119
|
+
outputPath: string
|
|
120
|
+
): Promise<ApplyResult> {
|
|
121
|
+
if (!fs.existsSync(docxPath)) {
|
|
122
|
+
return { success: false, message: `File not found: ${docxPath}` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let zip: AdmZip;
|
|
126
|
+
try {
|
|
127
|
+
zip = new AdmZip(docxPath);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const error = err as Error;
|
|
130
|
+
return { success: false, message: `Invalid DOCX file: ${error.message}` };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Read document.xml
|
|
134
|
+
const docEntry = zip.getEntry('word/document.xml');
|
|
135
|
+
if (!docEntry) {
|
|
136
|
+
return { success: false, message: 'Invalid DOCX: no document.xml' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let documentXml = zip.readAsText(docEntry);
|
|
140
|
+
|
|
141
|
+
// Generate ISO date for track changes
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
|
|
144
|
+
// Replace markers with track change XML
|
|
145
|
+
for (const marker of markers) {
|
|
146
|
+
const placeholder = `{{TC_${marker.id}}}`;
|
|
147
|
+
let replacement = '';
|
|
148
|
+
|
|
149
|
+
const escapedContent = escapeXml(marker.content);
|
|
150
|
+
const escapedAuthor = escapeXml(marker.author);
|
|
151
|
+
|
|
152
|
+
if (marker.type === 'insert') {
|
|
153
|
+
replacement = `<w:ins w:id="${marker.id}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:t>${escapedContent}</w:t></w:r></w:ins>`;
|
|
154
|
+
} else if (marker.type === 'delete') {
|
|
155
|
+
replacement = `<w:del w:id="${marker.id}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:delText>${escapedContent}</w:delText></w:r></w:del>`;
|
|
156
|
+
} else if (marker.type === 'substitute') {
|
|
157
|
+
const escapedReplacement = escapeXml(marker.replacement || '');
|
|
158
|
+
replacement = `<w:del w:id="${marker.id}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:delText>${escapedContent}</w:delText></w:r></w:del><w:ins w:id="${marker.id + 1000}" w:author="${escapedAuthor}" w:date="${now}"><w:r><w:t>${escapedReplacement}</w:t></w:r></w:ins>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
documentXml = documentXml.replace(placeholder, replacement);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update document.xml
|
|
165
|
+
zip.updateFile('word/document.xml', Buffer.from(documentXml));
|
|
166
|
+
|
|
167
|
+
// Enable track revisions in settings.xml
|
|
168
|
+
const settingsEntry = zip.getEntry('word/settings.xml');
|
|
169
|
+
if (settingsEntry) {
|
|
170
|
+
let settingsXml = zip.readAsText(settingsEntry);
|
|
171
|
+
if (!settingsXml.includes('w:trackRevisions')) {
|
|
172
|
+
settingsXml = settingsXml.replace(
|
|
173
|
+
'</w:settings>',
|
|
174
|
+
'<w:trackRevisions/></w:settings>'
|
|
175
|
+
);
|
|
176
|
+
zip.updateFile('word/settings.xml', Buffer.from(settingsXml));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Write output
|
|
181
|
+
zip.writeZip(outputPath);
|
|
182
|
+
|
|
183
|
+
return { success: true, message: `Created ${outputPath} with track changes` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build a Word document with track changes from annotated markdown
|
|
188
|
+
*
|
|
189
|
+
* @param mdPath - Path to markdown file with CriticMarkup
|
|
190
|
+
* @param docxPath - Output path for Word document
|
|
191
|
+
* @param options - Options
|
|
192
|
+
* @returns Result with success status and message
|
|
193
|
+
*/
|
|
194
|
+
export async function buildWithTrackChanges(
|
|
195
|
+
mdPath: string,
|
|
196
|
+
docxPath: string,
|
|
197
|
+
options: PrepareOptions = {}
|
|
198
|
+
): Promise<ApplyResult> {
|
|
199
|
+
const { author = 'Author' } = options;
|
|
200
|
+
|
|
201
|
+
if (!fs.existsSync(mdPath)) {
|
|
202
|
+
return { success: false, message: `File not found: ${mdPath}` };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
206
|
+
|
|
207
|
+
// Prepare for track changes
|
|
208
|
+
const { text: prepared, markers } = prepareForTrackChanges(content, { author });
|
|
209
|
+
|
|
210
|
+
// If no annotations, just build normally
|
|
211
|
+
if (markers.length === 0) {
|
|
212
|
+
try {
|
|
213
|
+
execSync(`pandoc "${mdPath}" -o "${docxPath}"`, { encoding: 'utf-8' });
|
|
214
|
+
return { success: true, message: `Created ${docxPath}` };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const error = err as Error;
|
|
217
|
+
return { success: false, message: error.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Write prepared content to temp file
|
|
222
|
+
const tempDir = path.dirname(mdPath);
|
|
223
|
+
const tempMd = path.join(tempDir, `.temp-${Date.now()}.md`);
|
|
224
|
+
const tempDocx = path.join(tempDir, `.temp-${Date.now()}.docx`);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
fs.writeFileSync(tempMd, prepared, 'utf-8');
|
|
228
|
+
|
|
229
|
+
// Build with pandoc
|
|
230
|
+
execSync(`pandoc "${tempMd}" -o "${tempDocx}"`, { encoding: 'utf-8' });
|
|
231
|
+
|
|
232
|
+
// Apply track changes
|
|
233
|
+
const result = await applyTrackChangesToDocx(tempDocx, markers, docxPath);
|
|
234
|
+
|
|
235
|
+
// Clean up temp files
|
|
236
|
+
fs.unlinkSync(tempMd);
|
|
237
|
+
fs.unlinkSync(tempDocx);
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// Clean up on error
|
|
242
|
+
if (fs.existsSync(tempMd)) fs.unlinkSync(tempMd);
|
|
243
|
+
if (fs.existsSync(tempDocx)) fs.unlinkSync(tempDocx);
|
|
244
|
+
const error = err as Error;
|
|
245
|
+
return { success: false, message: error.message };
|
|
246
|
+
}
|
|
247
|
+
}
|