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.
Files changed (78) hide show
  1. package/dist/lib/commands/comments.d.ts.map +1 -1
  2. package/dist/lib/commands/comments.js +19 -27
  3. package/dist/lib/commands/comments.js.map +1 -1
  4. package/dist/lib/commands/context.d.ts +1 -0
  5. package/dist/lib/commands/context.d.ts.map +1 -1
  6. package/dist/lib/commands/context.js +1 -2
  7. package/dist/lib/commands/context.js.map +1 -1
  8. package/dist/lib/commands/sections.d.ts.map +1 -1
  9. package/dist/lib/commands/sections.js +13 -0
  10. package/dist/lib/commands/sections.js.map +1 -1
  11. package/dist/lib/commands/utilities.d.ts.map +1 -1
  12. package/dist/lib/commands/utilities.js +20 -53
  13. package/dist/lib/commands/utilities.js.map +1 -1
  14. package/dist/lib/comment-realign.d.ts.map +1 -1
  15. package/dist/lib/comment-realign.js +0 -7
  16. package/dist/lib/comment-realign.js.map +1 -1
  17. package/dist/lib/dependencies.d.ts.map +1 -1
  18. package/dist/lib/dependencies.js +11 -23
  19. package/dist/lib/dependencies.js.map +1 -1
  20. package/dist/lib/git.d.ts.map +1 -1
  21. package/dist/lib/git.js +18 -28
  22. package/dist/lib/git.js.map +1 -1
  23. package/dist/lib/import.d.ts.map +1 -1
  24. package/dist/lib/import.js +1 -10
  25. package/dist/lib/import.js.map +1 -1
  26. package/dist/lib/merge.d.ts.map +1 -1
  27. package/dist/lib/merge.js +29 -117
  28. package/dist/lib/merge.js.map +1 -1
  29. package/dist/lib/pdf-comments.d.ts.map +1 -1
  30. package/dist/lib/pdf-comments.js +1 -13
  31. package/dist/lib/pdf-comments.js.map +1 -1
  32. package/dist/lib/pptx-themes.d.ts.map +1 -1
  33. package/dist/lib/pptx-themes.js +0 -403
  34. package/dist/lib/pptx-themes.js.map +1 -1
  35. package/dist/lib/protect-restore.d.ts.map +1 -1
  36. package/dist/lib/protect-restore.js +34 -36
  37. package/dist/lib/protect-restore.js.map +1 -1
  38. package/dist/lib/slides.d.ts.map +1 -1
  39. package/dist/lib/slides.js +0 -35
  40. package/dist/lib/slides.js.map +1 -1
  41. package/dist/lib/trackchanges.d.ts.map +1 -1
  42. package/dist/lib/trackchanges.js +1 -11
  43. package/dist/lib/trackchanges.js.map +1 -1
  44. package/dist/lib/tui.d.ts +36 -45
  45. package/dist/lib/tui.d.ts.map +1 -1
  46. package/dist/lib/tui.js +92 -108
  47. package/dist/lib/tui.js.map +1 -1
  48. package/dist/lib/undo.d.ts +3 -4
  49. package/dist/lib/undo.d.ts.map +1 -1
  50. package/dist/lib/undo.js +0 -7
  51. package/dist/lib/undo.js.map +1 -1
  52. package/dist/lib/utils.d.ts +12 -0
  53. package/dist/lib/utils.d.ts.map +1 -1
  54. package/dist/lib/utils.js +26 -0
  55. package/dist/lib/utils.js.map +1 -1
  56. package/dist/lib/wordcomments.d.ts.map +1 -1
  57. package/dist/lib/wordcomments.js +1 -8
  58. package/dist/lib/wordcomments.js.map +1 -1
  59. package/dist/package.json +137 -0
  60. package/lib/commands/comments.ts +20 -25
  61. package/lib/commands/context.ts +1 -2
  62. package/lib/commands/sections.ts +13 -0
  63. package/lib/commands/utilities.ts +30 -53
  64. package/lib/comment-realign.ts +0 -8
  65. package/lib/dependencies.ts +12 -20
  66. package/lib/git.ts +24 -31
  67. package/lib/import.ts +1 -11
  68. package/lib/merge.ts +42 -132
  69. package/lib/pdf-comments.ts +2 -14
  70. package/lib/pptx-themes.ts +0 -413
  71. package/lib/protect-restore.ts +48 -44
  72. package/lib/slides.ts +0 -37
  73. package/lib/trackchanges.ts +1 -12
  74. package/lib/{tui.js → tui.ts} +139 -126
  75. package/lib/undo.ts +3 -12
  76. package/lib/utils.ts +28 -0
  77. package/lib/wordcomments.ts +1 -9
  78. 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
- const addDir = (dir: string, zipPath = ''): void => {
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
- const addDir = (dir: string, zipPath = ''): void => {
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}`));
@@ -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
  */
@@ -5,44 +5,36 @@
5
5
  import { execSync } from 'child_process';
6
6
 
7
7
  /**
8
- * Check if pandoc-crossref is available
8
+ * Check if a command is available by running it silently
9
9
  */
10
- export function hasPandocCrossref(): boolean {
10
+ function commandExists(cmd: string): boolean {
11
11
  try {
12
- execSync('pandoc-crossref --version', { stdio: 'ignore' });
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
- try {
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
- try {
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
- * Get commit history for a file
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
- export function getFileHistory(filePath: string, limit: number = 10): CommitInfo[] {
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="%h|%ci|%s" -n ${limit} -- "${filePath}"`,
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
- return {
122
- hash: parts[0] ?? '',
123
- date: parts[1] ?? '',
124
- message: parts[2] ?? '',
125
- author: ''
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
- try {
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 a Word document compared to original
182
- * Uses sentence-level diffing for better conflict detection
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
- export function extractChanges(originalText: string, wordText: string, reviewer: string): ReviewerChange[] {
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
- const changes: ReviewerChange[] = [];
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
- * Merge multiple Word documents using three-way merge
520
+ * Core merge logic: extract changes from reviewer docs, detect conflicts, apply annotations
567
521
  */
568
- export async function mergeThreeWay(
569
- basePath: string,
522
+ async function mergeReviewerDocsCore(
523
+ baseText: string,
570
524
  reviewerDocs: ReviewerDoc[],
571
- options: MergeOptions = {}
572
- ): Promise<MergeResult & { baseText: string }> {
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, baseText, originalText: baseText };
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
  /**
@@ -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
  */