docrev 0.9.7 → 0.9.11

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 (65) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dev_notes/stress2/adversarial.docx +0 -0
  3. package/dev_notes/stress2/build_adversarial.ts +186 -0
  4. package/dev_notes/stress2/drift_matcher.ts +62 -0
  5. package/dev_notes/stress2/probe_anchors.ts +35 -0
  6. package/dev_notes/stress2/project/adversarial.docx +0 -0
  7. package/dev_notes/stress2/project/discussion.before.md +3 -0
  8. package/dev_notes/stress2/project/discussion.md +3 -0
  9. package/dev_notes/stress2/project/methods.before.md +20 -0
  10. package/dev_notes/stress2/project/methods.md +20 -0
  11. package/dev_notes/stress2/project/rev.yaml +5 -0
  12. package/dev_notes/stress2/project/sections.yaml +4 -0
  13. package/dev_notes/stress2/sections.yaml +5 -0
  14. package/dev_notes/stress2/trace_placement.ts +50 -0
  15. package/dev_notes/stresstest_boundaries.ts +27 -0
  16. package/dev_notes/stresstest_drift_apply.ts +43 -0
  17. package/dev_notes/stresstest_drift_compare.ts +43 -0
  18. package/dev_notes/stresstest_drift_v2.ts +54 -0
  19. package/dev_notes/stresstest_inspect.ts +54 -0
  20. package/dev_notes/stresstest_pstyle.ts +55 -0
  21. package/dev_notes/stresstest_section_debug.ts +23 -0
  22. package/dev_notes/stresstest_split.ts +70 -0
  23. package/dev_notes/stresstest_trace.ts +19 -0
  24. package/dev_notes/stresstest_verify_no_overwrite.ts +40 -0
  25. package/dist/lib/anchor-match.d.ts +10 -0
  26. package/dist/lib/anchor-match.d.ts.map +1 -1
  27. package/dist/lib/anchor-match.js +35 -0
  28. package/dist/lib/anchor-match.js.map +1 -1
  29. package/dist/lib/annotations.d.ts.map +1 -1
  30. package/dist/lib/annotations.js +16 -6
  31. package/dist/lib/annotations.js.map +1 -1
  32. package/dist/lib/commands/quality.js +1 -1
  33. package/dist/lib/commands/quality.js.map +1 -1
  34. package/dist/lib/commands/section-boundaries.d.ts +1 -1
  35. package/dist/lib/commands/section-boundaries.d.ts.map +1 -1
  36. package/dist/lib/commands/section-boundaries.js +12 -2
  37. package/dist/lib/commands/section-boundaries.js.map +1 -1
  38. package/dist/lib/commands/sync.js +19 -13
  39. package/dist/lib/commands/sync.js.map +1 -1
  40. package/dist/lib/commands/verify-anchors.d.ts.map +1 -1
  41. package/dist/lib/commands/verify-anchors.js +15 -4
  42. package/dist/lib/commands/verify-anchors.js.map +1 -1
  43. package/dist/lib/comment-realign.js +2 -2
  44. package/dist/lib/comment-realign.js.map +1 -1
  45. package/dist/lib/import.d.ts +12 -0
  46. package/dist/lib/import.d.ts.map +1 -1
  47. package/dist/lib/import.js +152 -45
  48. package/dist/lib/import.js.map +1 -1
  49. package/dist/lib/response.js +1 -1
  50. package/dist/lib/response.js.map +1 -1
  51. package/dist/lib/wordcomments.d.ts.map +1 -1
  52. package/dist/lib/wordcomments.js +165 -73
  53. package/dist/lib/wordcomments.js.map +1 -1
  54. package/lib/anchor-match.ts +38 -0
  55. package/lib/annotations.ts +16 -6
  56. package/lib/commands/quality.ts +1 -1
  57. package/lib/commands/section-boundaries.ts +11 -1
  58. package/lib/commands/sync.ts +21 -16
  59. package/lib/commands/verify-anchors.ts +15 -4
  60. package/lib/comment-realign.ts +2 -2
  61. package/lib/import.ts +170 -46
  62. package/lib/response.ts +1 -1
  63. package/lib/wordcomments.ts +180 -82
  64. package/package.json +1 -1
  65. package/dist/package.json +0 -137
@@ -72,8 +72,35 @@ function generateParaId(commentIdx: number, paraNum: number): string {
72
72
  * - comments: array with author, text, isReply, parentIdx
73
73
  */
74
74
  export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
75
- // Match all comments with optional anchor
76
- const commentPattern = /\{>>(.+?)<<\}(?:\s*\[([^\]]+)\]\{\.mark\})?/g;
75
+ // Match the comment block first; extend manually to capture an optional
76
+ // trailing `[anchor]{.mark}` span. A regex `[^\]]+` for the anchor would
77
+ // bail on the inner `]` of nested syntax (e.g. `[[0..9]]{.mark}` or
78
+ // `[*phrase*]{.mark}` after pandoc-rewriting), so we walk the brackets
79
+ // ourselves and verify a `{.mark}` suffix.
80
+ const commentPattern = /\{>>([\s\S]+?)<<\}/g;
81
+
82
+ function tryParseTrailingAnchor(
83
+ text: string,
84
+ fromIdx: number,
85
+ ): { anchor: string; endIdx: number } | null {
86
+ let i = fromIdx;
87
+ while (i < text.length && /\s/.test(text[i] ?? '')) i++;
88
+ if (text[i] !== '[') return null;
89
+ let depth = 1;
90
+ let j = i + 1;
91
+ while (j < text.length) {
92
+ const ch = text[j];
93
+ if (ch === '[') depth++;
94
+ else if (ch === ']') {
95
+ depth--;
96
+ if (depth === 0) break;
97
+ }
98
+ j++;
99
+ }
100
+ if (depth !== 0) return null;
101
+ if (text.slice(j + 1, j + 8) !== '{.mark}') return null;
102
+ return { anchor: text.slice(i + 1, j), endIdx: j + 8 };
103
+ }
77
104
 
78
105
  const rawMatches: ParsedComment[] = [];
79
106
  let match: RegExpExecArray | null;
@@ -87,14 +114,25 @@ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
87
114
  text = content.slice(colonIdx + 1).trim();
88
115
  }
89
116
 
117
+ const commentEnd = match.index + match[0].length;
118
+ const trailing = tryParseTrailingAnchor(markdown, commentEnd);
119
+
90
120
  rawMatches.push({
91
121
  author,
92
122
  text,
93
- anchor: match[2] || null,
123
+ anchor: trailing ? trailing.anchor : null,
94
124
  start: match.index,
95
- end: match.index + match[0].length,
96
- fullMatch: match[0]
125
+ end: trailing ? trailing.endIdx : commentEnd,
126
+ fullMatch: markdown.slice(match.index, trailing ? trailing.endIdx : commentEnd),
97
127
  });
128
+
129
+ // Advance regex lastIndex past the consumed anchor so the next iteration
130
+ // doesn't re-scan inside it (e.g. `[*emphasis*]{.mark}` would otherwise
131
+ // tempt the matcher to look for another `{>>...<<}` in the body of the
132
+ // anchor span).
133
+ if (trailing) {
134
+ commentPattern.lastIndex = trailing.endIdx;
135
+ }
98
136
  }
99
137
 
100
138
  if (rawMatches.length === 0) {
@@ -179,10 +217,11 @@ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
179
217
 
180
218
  if (c.isReply) {
181
219
  // Reply: remove from document entirely (will be in comments.xml only)
182
- // Also consume leading whitespace to avoid double spaces
220
+ // Also consume one preceding whitespace char to avoid double spaces.
221
+ // We deliberately consume at most one — walking arbitrarily backwards
222
+ // would shift positions that lower-index comments still depend on.
183
223
  let removeStart = c.start;
184
- const charBefore = markedMarkdown[removeStart - 1];
185
- while (removeStart > 0 && charBefore && /\s/.test(charBefore)) {
224
+ if (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1] ?? '')) {
186
225
  removeStart--;
187
226
  }
188
227
 
@@ -205,10 +244,10 @@ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
205
244
  } else {
206
245
  // Parent comment
207
246
  if (c.anchorFromReply) {
208
- // Anchor markers are placed by the reply, just remove this comment
247
+ // Anchor markers are placed by the reply, just remove this comment.
248
+ // Consume one preceding whitespace char only (see reply branch above).
209
249
  let removeStart = c.start;
210
- const charBefore = markedMarkdown[removeStart - 1];
211
- while (removeStart > 0 && charBefore && /\s/.test(charBefore)) {
250
+ if (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1] ?? '')) {
212
251
  removeStart--;
213
252
  }
214
253
  markedMarkdown = markedMarkdown.slice(0, removeStart) + markedMarkdown.slice(c.end);
@@ -421,93 +460,152 @@ export async function injectCommentsAtMarkers(
421
460
  const endMarker = `${MARKER_END_PREFIX}${idx}${MARKER_SUFFIX}`;
422
461
 
423
462
  const startPos = documentXml.indexOf(startMarker);
424
- const endPos = documentXml.indexOf(endMarker);
463
+ const endPos = documentXml.indexOf(endMarker, startPos + startMarker.length);
425
464
 
426
465
  if (startPos === -1 || endPos === -1) continue;
427
466
 
428
- // Find the <w:r> containing the markers
429
- const rStartBefore = documentXml.lastIndexOf('<w:r>', startPos);
430
- const rStartOpen = documentXml.lastIndexOf('<w:r ', startPos);
431
- const rStart = Math.max(rStartBefore, rStartOpen);
432
- const rEndPos = documentXml.indexOf('</w:r>', endPos);
433
-
434
- if (rStart === -1 || rEndPos === -1) continue;
435
-
436
- const rEnd = rEndPos + '</w:r>'.length;
437
- const runContent = documentXml.slice(rStart, rEnd);
438
-
439
- // Extract styling
440
- const rPrMatch = runContent.match(/<w:rPr>[\s\S]*?<\/w:rPr>/);
441
- const rPr = rPrMatch ? rPrMatch[0] : '';
442
-
443
- // Extract text
444
- const textMatch = runContent.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/);
445
- if (!textMatch) continue;
446
-
447
- const fullText = textMatch[1] ?? '';
448
- const tElementMatch = textMatch[0].match(/<w:t[^>]*>/);
449
- if (!tElementMatch) continue;
450
- const tElement = tElementMatch[0];
451
-
452
- const startInText = fullText.indexOf(startMarker);
453
- const endInText = fullText.indexOf(endMarker);
454
- if (startInText === -1 || endInText === -1) continue;
467
+ // Find the runs containing each marker. Pandoc may split a single
468
+ // markdown anchor across multiple <w:r> blocks when it applies styling
469
+ // mid-anchor (smart-quote substitution, *italic*, `code`, **bold**).
470
+ // The same-run path (current happy path) collapses into the multi-run
471
+ // path when start and end runs coincide.
472
+ const startRunOpen = Math.max(
473
+ documentXml.lastIndexOf('<w:r>', startPos),
474
+ documentXml.lastIndexOf('<w:r ', startPos),
475
+ );
476
+ const startRunCloseIdx = documentXml.indexOf('</w:r>', startPos);
477
+ const endRunOpen = Math.max(
478
+ documentXml.lastIndexOf('<w:r>', endPos),
479
+ documentXml.lastIndexOf('<w:r ', endPos),
480
+ );
481
+ const endRunCloseIdx = documentXml.indexOf('</w:r>', endPos);
482
+
483
+ if (
484
+ startRunOpen === -1 || startRunCloseIdx === -1 ||
485
+ endRunOpen === -1 || endRunCloseIdx === -1
486
+ ) continue;
487
+
488
+ const startRunClose = startRunCloseIdx + '</w:r>'.length;
489
+ const endRunClose = endRunCloseIdx + '</w:r>'.length;
490
+
491
+ const startRunFull = documentXml.slice(startRunOpen, startRunClose);
492
+ const endRunFull = documentXml.slice(endRunOpen, endRunClose);
493
+
494
+ // Extract <w:rPr> and <w:t> element shape from each run. Both pieces
495
+ // are needed verbatim so a textBefore split keeps its original styling
496
+ // and so the post-anchor textAfter render keeps the end run's styling.
497
+ function dissectRun(runXml: string, marker: string): {
498
+ rPr: string;
499
+ tElement: string;
500
+ textBefore: string;
501
+ textAfter: string;
502
+ } | null {
503
+ const rPrMatch = runXml.match(/<w:rPr>[\s\S]*?<\/w:rPr>/);
504
+ const tMatch = runXml.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/);
505
+ if (!tMatch) return null;
506
+ const tOpenMatch = tMatch[0].match(/<w:t[^>]*>/);
507
+ if (!tOpenMatch) return null;
508
+ const tContent = tMatch[1] ?? '';
509
+ const markerInT = tContent.indexOf(marker);
510
+ if (markerInT === -1) return null;
511
+ return {
512
+ rPr: rPrMatch ? rPrMatch[0] : '',
513
+ tElement: tOpenMatch[0],
514
+ textBefore: tContent.slice(0, markerInT),
515
+ textAfter: tContent.slice(markerInT + marker.length),
516
+ };
517
+ }
455
518
 
456
- let textBefore = fullText.slice(0, startInText);
457
- let anchorText = fullText.slice(startInText + startMarker.length, endInText);
458
- let textAfter = fullText.slice(endInText + endMarker.length);
519
+ let replacement = '';
520
+ const replies = commentsWithIds.filter(c => c.isReply && c.parentIdx === comment?.commentIdx);
459
521
 
460
- // When anchor is empty, use the first word from textAfter as fallback
461
- if (!anchorText && textAfter) {
462
- const wordMatch = textAfter.match(/^\s*(\S+)/);
463
- if (wordMatch) {
464
- anchorText = wordMatch[1] ?? '';
465
- textAfter = textAfter.slice(wordMatch[0].length);
522
+ const emitRangeStarts = () => {
523
+ replacement += `<w:commentRangeStart w:id="${comment.id}"/>`;
524
+ for (const reply of replies) {
525
+ replacement += `<w:commentRangeStart w:id="${reply.id}"/>`;
466
526
  }
467
- }
527
+ };
528
+
529
+ const emitRangeEnds = () => {
530
+ replacement += `<w:commentRangeEnd w:id="${comment.id}"/>`;
531
+ replacement += `<w:r><w:commentReference w:id="${comment.id}"/></w:r>`;
532
+ for (const reply of replies) {
533
+ replacement += `<w:commentRangeEnd w:id="${reply.id}"/>`;
534
+ replacement += `<w:r><w:commentReference w:id="${reply.id}"/></w:r>`;
535
+ injectedIds.add(reply.id);
536
+ }
537
+ };
538
+
539
+ if (startRunOpen === endRunOpen) {
540
+ // Same-run path: both markers live inside one <w:t>. Original logic.
541
+ const startInfo = dissectRun(startRunFull, startMarker);
542
+ if (!startInfo) continue;
543
+ const fullText = startInfo.textBefore + startMarker + startInfo.textAfter;
544
+ const endInTextRel = startInfo.textAfter.indexOf(endMarker);
545
+ if (endInTextRel === -1) continue;
546
+ const anchorTextSame = startInfo.textAfter.slice(0, endInTextRel);
547
+ let textAfter = startInfo.textAfter.slice(endInTextRel + endMarker.length);
548
+ let anchorText = anchorTextSame;
549
+ let textBefore = startInfo.textBefore;
550
+
551
+ // Empty anchor: borrow the next word so the comment has something
552
+ // to anchor on. Then normalize the trailing double space.
553
+ if (!anchorText && textAfter) {
554
+ const wordMatch = textAfter.match(/^\s*(\S+)/);
555
+ if (wordMatch) {
556
+ anchorText = wordMatch[1] ?? '';
557
+ textAfter = textAfter.slice(wordMatch[0].length);
558
+ }
559
+ }
560
+ if (!anchorText && textBefore.endsWith(' ') && textAfter.startsWith(' ')) {
561
+ textAfter = textAfter.slice(1);
562
+ }
563
+ // Suppress unused warning for pre-empty-anchor fullText var
564
+ void fullText;
468
565
 
469
- // When anchor is still empty, normalize double spaces to single space
470
- if (!anchorText && textBefore.endsWith(' ') && textAfter.startsWith(' ')) {
471
- textAfter = textAfter.slice(1); // Remove leading space from textAfter
566
+ if (textBefore) {
567
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${textBefore}</w:t></w:r>`;
568
+ }
569
+ emitRangeStarts();
570
+ if (anchorText) {
571
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${anchorText}</w:t></w:r>`;
572
+ }
573
+ emitRangeEnds();
574
+ if (textAfter) {
575
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${textAfter}</w:t></w:r>`;
576
+ }
577
+ documentXml = documentXml.slice(0, startRunOpen) + replacement + documentXml.slice(startRunClose);
578
+ injectedIds.add(comment.id);
579
+ continue;
472
580
  }
473
581
 
474
- // Build replacement
475
- let replacement = '';
582
+ // Multi-run path: markers sit in different <w:r> blocks because pandoc
583
+ // applied mid-anchor styling. Split the start run at the start marker,
584
+ // keep all middle runs verbatim (they carry the styled anchor portions),
585
+ // split the end run at the end marker.
586
+ const startInfo = dissectRun(startRunFull, startMarker);
587
+ const endInfo = dissectRun(endRunFull, endMarker);
588
+ if (!startInfo || !endInfo) continue;
476
589
 
477
- if (textBefore) {
478
- replacement += `<w:r>${rPr}${tElement}${textBefore}</w:t></w:r>`;
479
- }
480
-
481
- // Find replies to this comment
482
- const replies = commentsWithIds.filter(c => c.isReply && c.parentIdx === comment?.commentIdx);
590
+ const middle = documentXml.slice(startRunClose, endRunOpen);
483
591
 
484
- // Start ranges for parent AND all replies (nested)
485
- replacement += `<w:commentRangeStart w:id="${comment.id}"/>`;
486
- for (const reply of replies) {
487
- replacement += `<w:commentRangeStart w:id="${reply.id}"/>`;
592
+ if (startInfo.textBefore) {
593
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${startInfo.textBefore}</w:t></w:r>`;
488
594
  }
489
-
490
- // Anchor text
491
- if (anchorText) {
492
- replacement += `<w:r>${rPr}${tElement}${anchorText}</w:t></w:r>`;
595
+ emitRangeStarts();
596
+ if (startInfo.textAfter) {
597
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${startInfo.textAfter}</w:t></w:r>`;
493
598
  }
494
-
495
- // End parent range and reference (NO rStyle wrapper - required for threading)
496
- replacement += `<w:commentRangeEnd w:id="${comment.id}"/>`;
497
- replacement += `<w:r><w:commentReference w:id="${comment.id}"/></w:r>`;
498
-
499
- // End reply ranges and references (same position as parent, NO rStyle wrapper)
500
- for (const reply of replies) {
501
- replacement += `<w:commentRangeEnd w:id="${reply.id}"/>`;
502
- replacement += `<w:r><w:commentReference w:id="${reply.id}"/></w:r>`;
503
- injectedIds.add(reply.id);
599
+ replacement += middle;
600
+ if (endInfo.textBefore) {
601
+ replacement += `<w:r>${endInfo.rPr}${endInfo.tElement}${endInfo.textBefore}</w:t></w:r>`;
504
602
  }
505
-
506
- if (textAfter) {
507
- replacement += `<w:r>${rPr}${tElement}${textAfter}</w:t></w:r>`;
603
+ emitRangeEnds();
604
+ if (endInfo.textAfter) {
605
+ replacement += `<w:r>${endInfo.rPr}${endInfo.tElement}${endInfo.textAfter}</w:t></w:r>`;
508
606
  }
509
607
 
510
- documentXml = documentXml.slice(0, rStart) + replacement + documentXml.slice(rEnd);
608
+ documentXml = documentXml.slice(0, startRunOpen) + replacement + documentXml.slice(endRunClose);
511
609
  injectedIds.add(comment.id);
512
610
  }
513
611
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docrev",
3
- "version": "0.9.7",
3
+ "version": "0.9.11",
4
4
  "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
5
  "type": "module",
6
6
  "types": "dist/lib/types.d.ts",
package/dist/package.json DELETED
@@ -1,137 +0,0 @@
1
- {
2
- "name": "docrev",
3
- "version": "0.9.4",
4
- "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
- "type": "module",
6
- "types": "dist/lib/types.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/lib/annotations.d.ts",
10
- "import": "./dist/lib/annotations.js"
11
- },
12
- "./annotations": {
13
- "types": "./dist/lib/annotations.d.ts",
14
- "import": "./dist/lib/annotations.js"
15
- },
16
- "./build": {
17
- "types": "./dist/lib/build.d.ts",
18
- "import": "./dist/lib/build.js"
19
- },
20
- "./citations": {
21
- "types": "./dist/lib/citations.d.ts",
22
- "import": "./dist/lib/citations.js"
23
- },
24
- "./crossref": {
25
- "types": "./dist/lib/crossref.d.ts",
26
- "import": "./dist/lib/crossref.js"
27
- },
28
- "./doi": {
29
- "types": "./dist/lib/doi.d.ts",
30
- "import": "./dist/lib/doi.js"
31
- },
32
- "./equations": {
33
- "types": "./dist/lib/equations.d.ts",
34
- "import": "./dist/lib/equations.js"
35
- },
36
- "./git": {
37
- "types": "./dist/lib/git.d.ts",
38
- "import": "./dist/lib/git.js"
39
- },
40
- "./journals": {
41
- "types": "./dist/lib/journals.d.ts",
42
- "import": "./dist/lib/journals.js"
43
- },
44
- "./merge": {
45
- "types": "./dist/lib/merge.d.ts",
46
- "import": "./dist/lib/merge.js"
47
- },
48
- "./sections": {
49
- "types": "./dist/lib/sections.d.ts",
50
- "import": "./dist/lib/sections.js"
51
- },
52
- "./word": {
53
- "types": "./dist/lib/word.d.ts",
54
- "import": "./dist/lib/word.js"
55
- },
56
- "./variables": {
57
- "types": "./dist/lib/variables.d.ts",
58
- "import": "./dist/lib/variables.js"
59
- },
60
- "./grammar": {
61
- "types": "./dist/lib/grammar.d.ts",
62
- "import": "./dist/lib/grammar.js"
63
- },
64
- "./trackchanges": {
65
- "types": "./dist/lib/trackchanges.d.ts",
66
- "import": "./dist/lib/trackchanges.js"
67
- },
68
- "./spelling": {
69
- "types": "./dist/lib/spelling.d.ts",
70
- "import": "./dist/lib/spelling.js"
71
- },
72
- "./wordcomments": {
73
- "types": "./dist/lib/wordcomments.d.ts",
74
- "import": "./dist/lib/wordcomments.js"
75
- }
76
- },
77
- "engines": {
78
- "node": ">=18.0.0"
79
- },
80
- "bin": {
81
- "rev": "bin/rev.js"
82
- },
83
- "scripts": {
84
- "build": "tsc && node scripts/postbuild.js",
85
- "build:watch": "tsc --watch",
86
- "dev": "tsx bin/rev.ts",
87
- "test": "tsx --test test/*.test.js",
88
- "test:ts": "tsx --test test/*.test.ts",
89
- "test:watch": "node --test --watch test/*.test.js",
90
- "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
91
- "typecheck": "tsc --noEmit",
92
- "prepublishOnly": "npm run build"
93
- },
94
- "repository": {
95
- "type": "git",
96
- "url": "git+https://github.com/gcol33/docrev.git"
97
- },
98
- "bugs": {
99
- "url": "https://github.com/gcol33/docrev/issues"
100
- },
101
- "homepage": "https://github.com/gcol33/docrev#readme",
102
- "keywords": [
103
- "markdown",
104
- "word",
105
- "docx",
106
- "track-changes",
107
- "comments",
108
- "academic",
109
- "writing",
110
- "pandoc",
111
- "criticmarkup"
112
- ],
113
- "author": "Gilles Colling",
114
- "license": "MIT",
115
- "dependencies": {
116
- "adm-zip": "^0.5.16",
117
- "chalk": "^5.3.0",
118
- "commander": "^12.0.0",
119
- "dictionary-en": "^4.0.0",
120
- "dictionary-en-gb": "^3.0.0",
121
- "diff": "^8.0.2",
122
- "mathml-to-latex": "^1.5.0",
123
- "nspell": "^2.1.5",
124
- "pdf-lib": "^1.17.1",
125
- "pdfjs-dist": "^5.4.530",
126
- "tsx": "^4.21.0",
127
- "xml2js": "^0.6.2",
128
- "yaml": "^2.8.2"
129
- },
130
- "devDependencies": {
131
- "@types/adm-zip": "^0.5.7",
132
- "@types/node": "^25.2.0",
133
- "@types/xml2js": "^0.4.14",
134
- "c8": "^10.1.2",
135
- "typescript": "^5.9.3"
136
- }
137
- }