docrev 0.9.4 → 0.9.5
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/dist/lib/commands/comments.d.ts.map +1 -1
- package/dist/lib/commands/comments.js +19 -27
- package/dist/lib/commands/comments.js.map +1 -1
- package/dist/lib/commands/context.d.ts +1 -0
- package/dist/lib/commands/context.d.ts.map +1 -1
- package/dist/lib/commands/context.js +1 -2
- package/dist/lib/commands/context.js.map +1 -1
- package/dist/lib/commands/sections.d.ts.map +1 -1
- package/dist/lib/commands/sections.js +13 -0
- package/dist/lib/commands/sections.js.map +1 -1
- package/dist/lib/commands/utilities.d.ts.map +1 -1
- package/dist/lib/commands/utilities.js +20 -53
- package/dist/lib/commands/utilities.js.map +1 -1
- package/dist/lib/comment-realign.d.ts.map +1 -1
- package/dist/lib/comment-realign.js +0 -7
- package/dist/lib/comment-realign.js.map +1 -1
- package/dist/lib/dependencies.d.ts.map +1 -1
- package/dist/lib/dependencies.js +11 -23
- package/dist/lib/dependencies.js.map +1 -1
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +18 -28
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/import.js +1 -10
- package/dist/lib/import.js.map +1 -1
- package/dist/lib/merge.d.ts.map +1 -1
- package/dist/lib/merge.js +29 -117
- package/dist/lib/merge.js.map +1 -1
- package/dist/lib/pdf-comments.d.ts.map +1 -1
- package/dist/lib/pdf-comments.js +1 -13
- package/dist/lib/pdf-comments.js.map +1 -1
- package/dist/lib/pptx-themes.d.ts.map +1 -1
- package/dist/lib/pptx-themes.js +0 -403
- package/dist/lib/pptx-themes.js.map +1 -1
- package/dist/lib/protect-restore.d.ts.map +1 -1
- package/dist/lib/protect-restore.js +34 -36
- package/dist/lib/protect-restore.js.map +1 -1
- package/dist/lib/slides.d.ts.map +1 -1
- package/dist/lib/slides.js +0 -35
- package/dist/lib/slides.js.map +1 -1
- package/dist/lib/trackchanges.d.ts.map +1 -1
- package/dist/lib/trackchanges.js +1 -11
- package/dist/lib/trackchanges.js.map +1 -1
- package/dist/lib/tui.d.ts +36 -45
- package/dist/lib/tui.d.ts.map +1 -1
- package/dist/lib/tui.js +92 -108
- package/dist/lib/tui.js.map +1 -1
- package/dist/lib/undo.d.ts +3 -4
- package/dist/lib/undo.d.ts.map +1 -1
- package/dist/lib/undo.js +0 -7
- package/dist/lib/undo.js.map +1 -1
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +26 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/wordcomments.d.ts.map +1 -1
- package/dist/lib/wordcomments.js +1 -8
- package/dist/lib/wordcomments.js.map +1 -1
- package/dist/package.json +137 -0
- package/lib/commands/comments.ts +20 -25
- package/lib/commands/context.ts +1 -2
- package/lib/commands/sections.ts +13 -0
- package/lib/commands/utilities.ts +30 -53
- package/lib/comment-realign.ts +0 -8
- package/lib/dependencies.ts +12 -20
- package/lib/git.ts +24 -31
- package/lib/import.ts +1 -11
- package/lib/merge.ts +42 -132
- package/lib/pdf-comments.ts +2 -14
- package/lib/pptx-themes.ts +0 -413
- package/lib/protect-restore.ts +48 -44
- package/lib/slides.ts +0 -37
- package/lib/trackchanges.ts +1 -12
- package/lib/{tui.js → tui.ts} +139 -126
- package/lib/undo.ts +3 -12
- package/lib/utils.ts +28 -0
- package/lib/wordcomments.ts +1 -9
- package/package.json +1 -1
|
@@ -26,6 +26,34 @@ import {
|
|
|
26
26
|
// Use the actual BuildConfig from build.ts which allows string|Author[]
|
|
27
27
|
type BuildConfig = ReturnType<typeof loadBuildConfig>;
|
|
28
28
|
|
|
29
|
+
interface ZipLike {
|
|
30
|
+
addLocalFile(localPath: string, zipPath?: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recursively add directory contents to a zip archive, filtering by predicate
|
|
35
|
+
*/
|
|
36
|
+
function addDirToZip(
|
|
37
|
+
zip: ZipLike,
|
|
38
|
+
dir: string,
|
|
39
|
+
shouldInclude: (name: string) => boolean,
|
|
40
|
+
zipPath = '',
|
|
41
|
+
): void {
|
|
42
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const fullPath = path.join(dir, entry.name);
|
|
45
|
+
const entryZipPath = path.join(zipPath, entry.name);
|
|
46
|
+
|
|
47
|
+
if (!shouldInclude(entry.name)) continue;
|
|
48
|
+
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
addDirToZip(zip, fullPath, shouldInclude, entryZipPath);
|
|
51
|
+
} else {
|
|
52
|
+
zip.addLocalFile(fullPath, zipPath || undefined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
// Type definitions for package.json
|
|
30
58
|
interface PackageJson {
|
|
31
59
|
version?: string;
|
|
@@ -523,33 +551,7 @@ export function register(program: Command, pkg?: PackageJson): void {
|
|
|
523
551
|
return true;
|
|
524
552
|
};
|
|
525
553
|
|
|
526
|
-
|
|
527
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
528
|
-
for (const entry of entries) {
|
|
529
|
-
const fullPath = path.join(dir, entry.name);
|
|
530
|
-
const entryZipPath = path.join(zipPath, entry.name);
|
|
531
|
-
|
|
532
|
-
if (!shouldInclude(entry.name)) continue;
|
|
533
|
-
|
|
534
|
-
if (entry.isDirectory()) {
|
|
535
|
-
addDir(fullPath, entryZipPath);
|
|
536
|
-
} else {
|
|
537
|
-
zip.addLocalFile(fullPath, zipPath || undefined);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
// Add current directory
|
|
543
|
-
const entries = fs.readdirSync('.', { withFileTypes: true });
|
|
544
|
-
for (const entry of entries) {
|
|
545
|
-
if (!shouldInclude(entry.name)) continue;
|
|
546
|
-
|
|
547
|
-
if (entry.isDirectory()) {
|
|
548
|
-
addDir(entry.name, entry.name);
|
|
549
|
-
} else if (entry.isFile()) {
|
|
550
|
-
zip.addLocalFile(entry.name);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
554
|
+
addDirToZip(zip, '.', shouldInclude);
|
|
553
555
|
|
|
554
556
|
zip.writeZip(outputPath);
|
|
555
557
|
console.log(fmt.status('success', `Backup created: ${outputPath}`));
|
|
@@ -741,32 +743,7 @@ export function register(program: Command, pkg?: PackageJson): void {
|
|
|
741
743
|
return true;
|
|
742
744
|
};
|
|
743
745
|
|
|
744
|
-
|
|
745
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
746
|
-
for (const entry of entries) {
|
|
747
|
-
const fullPath = path.join(dir, entry.name);
|
|
748
|
-
const entryZipPath = path.join(zipPath, entry.name);
|
|
749
|
-
|
|
750
|
-
if (!shouldInclude(entry.name)) continue;
|
|
751
|
-
|
|
752
|
-
if (entry.isDirectory()) {
|
|
753
|
-
addDir(fullPath, entryZipPath);
|
|
754
|
-
} else {
|
|
755
|
-
zip.addLocalFile(fullPath, zipPath || undefined);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
const entries = fs.readdirSync('.', { withFileTypes: true });
|
|
761
|
-
for (const entry of entries) {
|
|
762
|
-
if (!shouldInclude(entry.name)) continue;
|
|
763
|
-
|
|
764
|
-
if (entry.isDirectory()) {
|
|
765
|
-
addDir(entry.name, entry.name);
|
|
766
|
-
} else if (entry.isFile()) {
|
|
767
|
-
zip.addLocalFile(entry.name);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
746
|
+
addDirToZip(zip, '.', shouldInclude);
|
|
770
747
|
|
|
771
748
|
zip.writeZip(outputPath);
|
|
772
749
|
console.log(fmt.status('success', `Exported: ${outputPath}`));
|
package/lib/comment-realign.ts
CHANGED
|
@@ -213,14 +213,6 @@ function parseMdParagraphs(markdown: string): MdParagraph[] {
|
|
|
213
213
|
return paragraphs;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/**
|
|
217
|
-
* Strip existing comments from a specific author
|
|
218
|
-
*/
|
|
219
|
-
function stripAuthorComments(text: string, author: string): string {
|
|
220
|
-
const pattern = new RegExp(`\\s*\\{>>${author}:[^<]*<<\\}`, 'g');
|
|
221
|
-
return text.replace(pattern, '');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
216
|
/**
|
|
225
217
|
* Normalize text for matching (remove citations, extra whitespace)
|
|
226
218
|
*/
|
package/lib/dependencies.ts
CHANGED
|
@@ -5,44 +5,36 @@
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Check if
|
|
8
|
+
* Check if a command is available by running it silently
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
function commandExists(cmd: string): boolean {
|
|
11
11
|
try {
|
|
12
|
-
execSync(
|
|
12
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
13
13
|
return true;
|
|
14
14
|
} catch {
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Check if pandoc-crossref is available
|
|
21
|
+
*/
|
|
22
|
+
export function hasPandocCrossref(): boolean {
|
|
23
|
+
return commandExists('pandoc-crossref --version');
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
/**
|
|
20
27
|
* Check if pandoc is available
|
|
21
28
|
*/
|
|
22
29
|
export function hasPandoc(): boolean {
|
|
23
|
-
|
|
24
|
-
execSync('pandoc --version', { stdio: 'ignore' });
|
|
25
|
-
return true;
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
30
|
+
return commandExists('pandoc --version');
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Check if LaTeX is available (for PDF generation)
|
|
33
35
|
*/
|
|
34
36
|
export function hasLatex(): boolean {
|
|
35
|
-
|
|
36
|
-
execSync('pdflatex --version', { stdio: 'ignore' });
|
|
37
|
-
return true;
|
|
38
|
-
} catch {
|
|
39
|
-
try {
|
|
40
|
-
execSync('xelatex --version', { stdio: 'ignore' });
|
|
41
|
-
return true;
|
|
42
|
-
} catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
37
|
+
return commandExists('pdflatex --version') || commandExists('xelatex --version');
|
|
46
38
|
}
|
|
47
39
|
|
|
48
40
|
/**
|
package/lib/git.ts
CHANGED
|
@@ -103,14 +103,18 @@ export function getChangedFiles(fromRef: string, toRef: string = 'HEAD'): Change
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
107
|
-
* @param filePath - Path to file
|
|
108
|
-
* @param limit - Maximum number of commits to return
|
|
106
|
+
* Run git log with a given format and optional file path, parse pipe-delimited output
|
|
109
107
|
*/
|
|
110
|
-
|
|
108
|
+
function runGitLog(
|
|
109
|
+
format: string,
|
|
110
|
+
limit: number,
|
|
111
|
+
fields: (keyof CommitInfo)[],
|
|
112
|
+
filePath?: string,
|
|
113
|
+
): CommitInfo[] {
|
|
111
114
|
try {
|
|
115
|
+
const fileArg = filePath ? ` -- "${filePath}"` : '';
|
|
112
116
|
const output = execSync(
|
|
113
|
-
`git log --format="
|
|
117
|
+
`git log --format="${format}" -n ${limit}${fileArg}`,
|
|
114
118
|
{ stdio: 'pipe' }
|
|
115
119
|
).toString().trim();
|
|
116
120
|
|
|
@@ -118,18 +122,26 @@ export function getFileHistory(filePath: string, limit: number = 10): CommitInfo
|
|
|
118
122
|
|
|
119
123
|
return output.split('\n').map(line => {
|
|
120
124
|
const parts = line.split('|');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
125
|
+
const entry: CommitInfo = { hash: '', date: '', author: '', message: '' };
|
|
126
|
+
for (let i = 0; i < fields.length; i++) {
|
|
127
|
+
entry[fields[i]] = parts[i] ?? '';
|
|
128
|
+
}
|
|
129
|
+
return entry;
|
|
127
130
|
});
|
|
128
131
|
} catch {
|
|
129
132
|
return [];
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Get commit history for a file
|
|
138
|
+
* @param filePath - Path to file
|
|
139
|
+
* @param limit - Maximum number of commits to return
|
|
140
|
+
*/
|
|
141
|
+
export function getFileHistory(filePath: string, limit: number = 10): CommitInfo[] {
|
|
142
|
+
return runGitLog('%h|%ci|%s', limit, ['hash', 'date', 'message'], filePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
/**
|
|
134
146
|
* Compare file content between two refs
|
|
135
147
|
* @param filePath - Path to file
|
|
@@ -194,26 +206,7 @@ export function getWordCountDiff(
|
|
|
194
206
|
* @param limit - Maximum number of commits to return
|
|
195
207
|
*/
|
|
196
208
|
export function getRecentCommits(limit: number = 10): CommitInfo[] {
|
|
197
|
-
|
|
198
|
-
const output = execSync(
|
|
199
|
-
`git log --format="%h|%ci|%an|%s" -n ${limit}`,
|
|
200
|
-
{ stdio: 'pipe' }
|
|
201
|
-
).toString().trim();
|
|
202
|
-
|
|
203
|
-
if (!output) return [];
|
|
204
|
-
|
|
205
|
-
return output.split('\n').map(line => {
|
|
206
|
-
const parts = line.split('|');
|
|
207
|
-
return {
|
|
208
|
-
hash: parts[0] ?? '',
|
|
209
|
-
date: parts[1] ?? '',
|
|
210
|
-
author: parts[2] ?? '',
|
|
211
|
-
message: parts[3] ?? ''
|
|
212
|
-
};
|
|
213
|
-
});
|
|
214
|
-
} catch {
|
|
215
|
-
return [];
|
|
216
|
-
}
|
|
209
|
+
return runGitLog('%h|%ci|%an|%s', limit, ['hash', 'date', 'author', 'message']);
|
|
217
210
|
}
|
|
218
211
|
|
|
219
212
|
/**
|
package/lib/import.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
protectTables,
|
|
29
29
|
restoreTables,
|
|
30
30
|
} from './protect-restore.js';
|
|
31
|
+
import { normalizeWhitespace } from './utils.js';
|
|
31
32
|
|
|
32
33
|
const execAsync = promisify(exec);
|
|
33
34
|
|
|
@@ -1138,17 +1139,6 @@ export function insertCommentsIntoMarkdown(
|
|
|
1138
1139
|
return result;
|
|
1139
1140
|
}
|
|
1140
1141
|
|
|
1141
|
-
/**
|
|
1142
|
-
* Normalize text for comparison (handle whitespace differences)
|
|
1143
|
-
*/
|
|
1144
|
-
function normalizeWhitespace(text: string): string {
|
|
1145
|
-
return text
|
|
1146
|
-
.replace(/\r\n/g, '\n') // Normalize line endings
|
|
1147
|
-
.replace(/\t/g, ' ') // Tabs to spaces
|
|
1148
|
-
.replace(/ +/g, ' ') // Collapse multiple spaces
|
|
1149
|
-
.trim();
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1142
|
/**
|
|
1153
1143
|
* Fix citation and math annotations by preserving original markdown syntax
|
|
1154
1144
|
*/
|
package/lib/merge.ts
CHANGED
|
@@ -178,18 +178,15 @@ export async function checkBaseMatch(basePath: string, reviewerPath: string): Pr
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
|
-
* Extract changes from
|
|
182
|
-
*
|
|
183
|
-
* @param originalText - Original text (from base document)
|
|
184
|
-
* @param wordText - Text extracted from reviewer's Word doc
|
|
181
|
+
* Extract changes from diffs between original and modified text
|
|
182
|
+
* @param diffs - Array of diff changes
|
|
185
183
|
* @param reviewer - Reviewer identifier
|
|
186
184
|
*/
|
|
187
|
-
|
|
185
|
+
function extractChangesFromDiffs(
|
|
186
|
+
diffs: Array<{ added?: boolean; removed?: boolean; value: string }>,
|
|
187
|
+
reviewer: string,
|
|
188
|
+
): ReviewerChange[] {
|
|
188
189
|
const changes: ReviewerChange[] = [];
|
|
189
|
-
|
|
190
|
-
// Use sentence-level diff for better granularity
|
|
191
|
-
const diffs = diffSentences(originalText, wordText);
|
|
192
|
-
|
|
193
190
|
let originalPos = 0;
|
|
194
191
|
let i = 0;
|
|
195
192
|
|
|
@@ -198,11 +195,9 @@ export function extractChanges(originalText: string, wordText: string, reviewer:
|
|
|
198
195
|
if (!part) break;
|
|
199
196
|
|
|
200
197
|
if (!part.added && !part.removed) {
|
|
201
|
-
// Unchanged
|
|
202
198
|
originalPos += part.value.length;
|
|
203
199
|
i++;
|
|
204
200
|
} else if (part.removed && diffs[i + 1]?.added) {
|
|
205
|
-
// Replacement: removed followed by added
|
|
206
201
|
const nextPart = diffs[i + 1];
|
|
207
202
|
if (!nextPart) break;
|
|
208
203
|
changes.push({
|
|
@@ -216,7 +211,6 @@ export function extractChanges(originalText: string, wordText: string, reviewer:
|
|
|
216
211
|
originalPos += part.value.length;
|
|
217
212
|
i += 2;
|
|
218
213
|
} else if (part.removed) {
|
|
219
|
-
// Pure deletion
|
|
220
214
|
changes.push({
|
|
221
215
|
reviewer,
|
|
222
216
|
type: 'delete',
|
|
@@ -228,7 +222,6 @@ export function extractChanges(originalText: string, wordText: string, reviewer:
|
|
|
228
222
|
originalPos += part.value.length;
|
|
229
223
|
i++;
|
|
230
224
|
} else if (part.added) {
|
|
231
|
-
// Pure insertion
|
|
232
225
|
changes.push({
|
|
233
226
|
reviewer,
|
|
234
227
|
type: 'insert',
|
|
@@ -244,61 +237,22 @@ export function extractChanges(originalText: string, wordText: string, reviewer:
|
|
|
244
237
|
return changes;
|
|
245
238
|
}
|
|
246
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Extract changes from a Word document compared to original
|
|
242
|
+
* Uses sentence-level diffing for better conflict detection
|
|
243
|
+
* @param originalText - Original text (from base document)
|
|
244
|
+
* @param wordText - Text extracted from reviewer's Word doc
|
|
245
|
+
* @param reviewer - Reviewer identifier
|
|
246
|
+
*/
|
|
247
|
+
export function extractChanges(originalText: string, wordText: string, reviewer: string): ReviewerChange[] {
|
|
248
|
+
return extractChangesFromDiffs(diffSentences(originalText, wordText), reviewer);
|
|
249
|
+
}
|
|
250
|
+
|
|
247
251
|
/**
|
|
248
252
|
* Extract changes using word-level diff (more fine-grained)
|
|
249
253
|
*/
|
|
250
254
|
export function extractChangesWordLevel(originalText: string, wordText: string, reviewer: string): ReviewerChange[] {
|
|
251
|
-
|
|
252
|
-
const diffs = diffWords(originalText, wordText);
|
|
253
|
-
|
|
254
|
-
let originalPos = 0;
|
|
255
|
-
let i = 0;
|
|
256
|
-
|
|
257
|
-
while (i < diffs.length) {
|
|
258
|
-
const part = diffs[i];
|
|
259
|
-
if (!part) break;
|
|
260
|
-
|
|
261
|
-
if (!part.added && !part.removed) {
|
|
262
|
-
originalPos += part.value.length;
|
|
263
|
-
i++;
|
|
264
|
-
} else if (part.removed && diffs[i + 1]?.added) {
|
|
265
|
-
const nextPart = diffs[i + 1];
|
|
266
|
-
if (!nextPart) break;
|
|
267
|
-
changes.push({
|
|
268
|
-
reviewer,
|
|
269
|
-
type: 'replace',
|
|
270
|
-
start: originalPos,
|
|
271
|
-
end: originalPos + part.value.length,
|
|
272
|
-
oldText: part.value,
|
|
273
|
-
newText: nextPart.value,
|
|
274
|
-
});
|
|
275
|
-
originalPos += part.value.length;
|
|
276
|
-
i += 2;
|
|
277
|
-
} else if (part.removed) {
|
|
278
|
-
changes.push({
|
|
279
|
-
reviewer,
|
|
280
|
-
type: 'delete',
|
|
281
|
-
start: originalPos,
|
|
282
|
-
end: originalPos + part.value.length,
|
|
283
|
-
oldText: part.value,
|
|
284
|
-
newText: '',
|
|
285
|
-
});
|
|
286
|
-
originalPos += part.value.length;
|
|
287
|
-
i++;
|
|
288
|
-
} else if (part.added) {
|
|
289
|
-
changes.push({
|
|
290
|
-
reviewer,
|
|
291
|
-
type: 'insert',
|
|
292
|
-
start: originalPos,
|
|
293
|
-
end: originalPos,
|
|
294
|
-
oldText: '',
|
|
295
|
-
newText: part.value,
|
|
296
|
-
});
|
|
297
|
-
i++;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return changes;
|
|
255
|
+
return extractChangesFromDiffs(diffWords(originalText, wordText), reviewer);
|
|
302
256
|
}
|
|
303
257
|
|
|
304
258
|
/**
|
|
@@ -563,23 +517,15 @@ export function clearConflicts(projectDir: string): void {
|
|
|
563
517
|
}
|
|
564
518
|
|
|
565
519
|
/**
|
|
566
|
-
*
|
|
520
|
+
* Core merge logic: extract changes from reviewer docs, detect conflicts, apply annotations
|
|
567
521
|
*/
|
|
568
|
-
|
|
569
|
-
|
|
522
|
+
async function mergeReviewerDocsCore(
|
|
523
|
+
baseText: string,
|
|
570
524
|
reviewerDocs: ReviewerDoc[],
|
|
571
|
-
options: MergeOptions = {}
|
|
572
|
-
): Promise<MergeResult
|
|
525
|
+
options: MergeOptions = {},
|
|
526
|
+
): Promise<MergeResult> {
|
|
573
527
|
const { diffLevel = 'sentence' } = options;
|
|
574
528
|
|
|
575
|
-
if (!fs.existsSync(basePath)) {
|
|
576
|
-
throw new Error(`Base document not found: ${basePath}`);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Extract text from base document
|
|
580
|
-
const { text: baseText } = await extractFromWord(basePath);
|
|
581
|
-
|
|
582
|
-
// Extract changes from each reviewer relative to base
|
|
583
529
|
const allChanges: ReviewerChange[][] = [];
|
|
584
530
|
const allComments: ReviewerComment[] = [];
|
|
585
531
|
|
|
@@ -590,14 +536,12 @@ export async function mergeThreeWay(
|
|
|
590
536
|
|
|
591
537
|
const { text: wordText } = await extractFromWord(doc.path);
|
|
592
538
|
|
|
593
|
-
// Choose diff level
|
|
594
539
|
const changes = diffLevel === 'word'
|
|
595
540
|
? extractChangesWordLevel(baseText, wordText, doc.name)
|
|
596
541
|
: extractChanges(baseText, wordText, doc.name);
|
|
597
542
|
|
|
598
543
|
allChanges.push(changes);
|
|
599
544
|
|
|
600
|
-
// Also extract comments
|
|
601
545
|
try {
|
|
602
546
|
const comments = await extractWordComments(doc.path);
|
|
603
547
|
allComments.push(...comments.map(c => ({ ...c, reviewer: doc.name })));
|
|
@@ -609,13 +553,10 @@ export async function mergeThreeWay(
|
|
|
609
553
|
}
|
|
610
554
|
}
|
|
611
555
|
|
|
612
|
-
// Detect conflicts
|
|
613
556
|
const { conflicts, nonConflicting } = detectConflicts(allChanges);
|
|
614
557
|
|
|
615
|
-
// Apply non-conflicting changes as annotations
|
|
616
558
|
let merged = applyChangesAsAnnotations(baseText, nonConflicting);
|
|
617
559
|
|
|
618
|
-
// Add comments with reviewer attribution
|
|
619
560
|
for (const comment of allComments) {
|
|
620
561
|
merged += `\n{>>${comment.reviewer}: ${comment.text}<<}`;
|
|
621
562
|
}
|
|
@@ -628,7 +569,24 @@ export async function mergeThreeWay(
|
|
|
628
569
|
comments: allComments.length,
|
|
629
570
|
};
|
|
630
571
|
|
|
631
|
-
return { merged, conflicts, stats,
|
|
572
|
+
return { merged, conflicts, stats, originalText: baseText };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Merge multiple Word documents using three-way merge
|
|
577
|
+
*/
|
|
578
|
+
export async function mergeThreeWay(
|
|
579
|
+
basePath: string,
|
|
580
|
+
reviewerDocs: ReviewerDoc[],
|
|
581
|
+
options: MergeOptions = {}
|
|
582
|
+
): Promise<MergeResult & { baseText: string }> {
|
|
583
|
+
if (!fs.existsSync(basePath)) {
|
|
584
|
+
throw new Error(`Base document not found: ${basePath}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const { text: baseText } = await extractFromWord(basePath);
|
|
588
|
+
const result = await mergeReviewerDocsCore(baseText, reviewerDocs, options);
|
|
589
|
+
return { ...result, baseText };
|
|
632
590
|
}
|
|
633
591
|
|
|
634
592
|
/**
|
|
@@ -640,60 +598,12 @@ export async function mergeReviewerDocs(
|
|
|
640
598
|
reviewerDocs: ReviewerDoc[],
|
|
641
599
|
options: MergeOptions = {}
|
|
642
600
|
): Promise<MergeResult> {
|
|
643
|
-
const { autoResolve = false } = options;
|
|
644
|
-
|
|
645
601
|
if (!fs.existsSync(originalPath)) {
|
|
646
602
|
throw new Error(`Original file not found: ${originalPath}`);
|
|
647
603
|
}
|
|
648
604
|
|
|
649
605
|
const originalText = fs.readFileSync(originalPath, 'utf-8');
|
|
650
|
-
|
|
651
|
-
// Extract changes from each reviewer
|
|
652
|
-
const allChanges: ReviewerChange[][] = [];
|
|
653
|
-
const allComments: ReviewerComment[] = [];
|
|
654
|
-
|
|
655
|
-
for (const doc of reviewerDocs) {
|
|
656
|
-
if (!fs.existsSync(doc.path)) {
|
|
657
|
-
throw new Error(`Reviewer file not found: ${doc.path}`);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const { text: wordText } = await extractFromWord(doc.path);
|
|
661
|
-
const changes = extractChanges(originalText, wordText, doc.name);
|
|
662
|
-
allChanges.push(changes);
|
|
663
|
-
|
|
664
|
-
// Also extract comments
|
|
665
|
-
try {
|
|
666
|
-
const comments = await extractWordComments(doc.path);
|
|
667
|
-
allComments.push(...comments.map(c => ({ ...c, reviewer: doc.name })));
|
|
668
|
-
} catch (e) {
|
|
669
|
-
if (process.env.DEBUG) {
|
|
670
|
-
const error = e as Error;
|
|
671
|
-
console.warn(`merge: Failed to extract comments:`, error.message);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Detect conflicts
|
|
677
|
-
const { conflicts, nonConflicting } = detectConflicts(allChanges);
|
|
678
|
-
|
|
679
|
-
// Apply non-conflicting changes as annotations
|
|
680
|
-
let merged = applyChangesAsAnnotations(originalText, nonConflicting);
|
|
681
|
-
|
|
682
|
-
// Add comments
|
|
683
|
-
for (const comment of allComments) {
|
|
684
|
-
// Append comments at the end for now (position tracking is complex)
|
|
685
|
-
merged += `\n{>>${comment.reviewer}: ${comment.text}<<}`;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const stats = {
|
|
689
|
-
reviewers: reviewerDocs.length,
|
|
690
|
-
totalChanges: allChanges.flat().length,
|
|
691
|
-
nonConflicting: nonConflicting.length,
|
|
692
|
-
conflicts: conflicts.length,
|
|
693
|
-
comments: allComments.length,
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
return { merged, conflicts, stats, originalText };
|
|
606
|
+
return mergeReviewerDocsCore(originalText, reviewerDocs, options);
|
|
697
607
|
}
|
|
698
608
|
|
|
699
609
|
/**
|
package/lib/pdf-comments.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Converts CriticMarkup comments to LaTeX margin notes for PDF output
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { escapeLatex } from './utils.js';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* LaTeX preamble for margin comments
|
|
9
11
|
* Uses todonotes package with custom styling
|
|
@@ -117,20 +119,6 @@ export function convertCommentsToMarginNotes(
|
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
/**
|
|
121
|
-
* Escape LaTeX special characters
|
|
122
|
-
* @param text - Text to escape
|
|
123
|
-
* @returns Escaped text
|
|
124
|
-
*/
|
|
125
|
-
function escapeLatex(text: string): string {
|
|
126
|
-
return text
|
|
127
|
-
.replace(/\\/g, '\\textbackslash{}')
|
|
128
|
-
.replace(/([#$%&_{}])/g, '\\$1')
|
|
129
|
-
.replace(/\^/g, '\\textasciicircum{}')
|
|
130
|
-
.replace(/~/g, '\\textasciitilde{}')
|
|
131
|
-
.replace(/\n/g, ' '); // Replace newlines with spaces
|
|
132
|
-
}
|
|
133
|
-
|
|
134
122
|
/**
|
|
135
123
|
* Result of track changes conversion
|
|
136
124
|
*/
|