docrev 0.8.1 → 0.8.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/.claude/settings.local.json +9 -0
- package/PLAN-tables-and-postprocess.md +850 -0
- package/README.md +33 -0
- package/bin/rev.js +12 -131
- package/bin/rev.ts +145 -0
- package/dist/bin/rev.d.ts +9 -0
- package/dist/bin/rev.d.ts.map +1 -0
- package/dist/bin/rev.js +118 -0
- package/dist/bin/rev.js.map +1 -0
- package/dist/lib/annotations.d.ts +91 -0
- package/dist/lib/annotations.d.ts.map +1 -0
- package/dist/lib/annotations.js +554 -0
- package/dist/lib/annotations.js.map +1 -0
- package/dist/lib/build.d.ts +171 -0
- package/dist/lib/build.d.ts.map +1 -0
- package/dist/lib/build.js +755 -0
- package/dist/lib/build.js.map +1 -0
- package/dist/lib/citations.d.ts +34 -0
- package/dist/lib/citations.d.ts.map +1 -0
- package/dist/lib/citations.js +140 -0
- package/dist/lib/citations.js.map +1 -0
- package/dist/lib/commands/build.d.ts +13 -0
- package/dist/lib/commands/build.d.ts.map +1 -0
- package/dist/lib/commands/build.js +678 -0
- package/dist/lib/commands/build.js.map +1 -0
- package/dist/lib/commands/citations.d.ts +11 -0
- package/dist/lib/commands/citations.d.ts.map +1 -0
- package/dist/lib/commands/citations.js +428 -0
- package/dist/lib/commands/citations.js.map +1 -0
- package/dist/lib/commands/comments.d.ts +11 -0
- package/dist/lib/commands/comments.d.ts.map +1 -0
- package/dist/lib/commands/comments.js +883 -0
- package/dist/lib/commands/comments.js.map +1 -0
- package/dist/lib/commands/context.d.ts +35 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/context.js +59 -0
- package/dist/lib/commands/context.js.map +1 -0
- package/dist/lib/commands/core.d.ts +11 -0
- package/dist/lib/commands/core.d.ts.map +1 -0
- package/dist/lib/commands/core.js +246 -0
- package/dist/lib/commands/core.js.map +1 -0
- package/dist/lib/commands/doi.d.ts +11 -0
- package/dist/lib/commands/doi.d.ts.map +1 -0
- package/dist/lib/commands/doi.js +373 -0
- package/dist/lib/commands/doi.js.map +1 -0
- package/dist/lib/commands/history.d.ts +11 -0
- package/dist/lib/commands/history.d.ts.map +1 -0
- package/dist/lib/commands/history.js +245 -0
- package/dist/lib/commands/history.js.map +1 -0
- package/dist/lib/commands/index.d.ts +28 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/index.js +35 -0
- package/dist/lib/commands/index.js.map +1 -0
- package/dist/lib/commands/init.d.ts +11 -0
- package/dist/lib/commands/init.d.ts.map +1 -0
- package/dist/lib/commands/init.js +209 -0
- package/dist/lib/commands/init.js.map +1 -0
- package/dist/lib/commands/response.d.ts +11 -0
- package/dist/lib/commands/response.d.ts.map +1 -0
- package/dist/lib/commands/response.js +317 -0
- package/dist/lib/commands/response.js.map +1 -0
- package/dist/lib/commands/sections.d.ts +11 -0
- package/dist/lib/commands/sections.d.ts.map +1 -0
- package/dist/lib/commands/sections.js +1071 -0
- package/dist/lib/commands/sections.js.map +1 -0
- package/dist/lib/commands/utilities.d.ts +19 -0
- package/dist/lib/commands/utilities.d.ts.map +1 -0
- package/dist/lib/commands/utilities.js +2009 -0
- package/dist/lib/commands/utilities.js.map +1 -0
- package/dist/lib/comment-realign.d.ts +50 -0
- package/dist/lib/comment-realign.d.ts.map +1 -0
- package/dist/lib/comment-realign.js +372 -0
- package/dist/lib/comment-realign.js.map +1 -0
- package/dist/lib/config.d.ts +41 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +76 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/crossref.d.ts +108 -0
- package/dist/lib/crossref.d.ts.map +1 -0
- package/dist/lib/crossref.js +597 -0
- package/dist/lib/crossref.js.map +1 -0
- package/dist/lib/dependencies.d.ts +30 -0
- package/dist/lib/dependencies.d.ts.map +1 -0
- package/dist/lib/dependencies.js +95 -0
- package/dist/lib/dependencies.js.map +1 -0
- package/dist/lib/doi-cache.d.ts +29 -0
- package/dist/lib/doi-cache.d.ts.map +1 -0
- package/dist/lib/doi-cache.js +104 -0
- package/dist/lib/doi-cache.js.map +1 -0
- package/dist/lib/doi.d.ts +65 -0
- package/dist/lib/doi.d.ts.map +1 -0
- package/dist/lib/doi.js +710 -0
- package/dist/lib/doi.js.map +1 -0
- package/dist/lib/equations.d.ts +61 -0
- package/dist/lib/equations.d.ts.map +1 -0
- package/dist/lib/equations.js +445 -0
- package/dist/lib/equations.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +303 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.d.ts +104 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +416 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/git.d.ts +88 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +304 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/grammar.d.ts +62 -0
- package/dist/lib/grammar.d.ts.map +1 -0
- package/dist/lib/grammar.js +244 -0
- package/dist/lib/grammar.js.map +1 -0
- package/dist/lib/image-registry.d.ts +68 -0
- package/dist/lib/image-registry.d.ts.map +1 -0
- package/dist/lib/image-registry.js +112 -0
- package/dist/lib/image-registry.js.map +1 -0
- package/dist/lib/import.d.ts +184 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/import.js +1581 -0
- package/dist/lib/import.js.map +1 -0
- package/dist/lib/journals.d.ts +55 -0
- package/dist/lib/journals.d.ts.map +1 -0
- package/dist/lib/journals.js +417 -0
- package/dist/lib/journals.js.map +1 -0
- package/dist/lib/merge.d.ts +138 -0
- package/dist/lib/merge.d.ts.map +1 -0
- package/dist/lib/merge.js +603 -0
- package/dist/lib/merge.js.map +1 -0
- package/dist/lib/orcid.d.ts +36 -0
- package/dist/lib/orcid.d.ts.map +1 -0
- package/dist/lib/orcid.js +117 -0
- package/dist/lib/orcid.js.map +1 -0
- package/dist/lib/pdf-comments.d.ts +95 -0
- package/dist/lib/pdf-comments.d.ts.map +1 -0
- package/dist/lib/pdf-comments.js +192 -0
- package/dist/lib/pdf-comments.js.map +1 -0
- package/dist/lib/pdf-import.d.ts +118 -0
- package/dist/lib/pdf-import.d.ts.map +1 -0
- package/dist/lib/pdf-import.js +397 -0
- package/dist/lib/pdf-import.js.map +1 -0
- package/dist/lib/plugins.d.ts +76 -0
- package/dist/lib/plugins.d.ts.map +1 -0
- package/dist/lib/plugins.js +235 -0
- package/dist/lib/plugins.js.map +1 -0
- package/dist/lib/postprocess.d.ts +42 -0
- package/dist/lib/postprocess.d.ts.map +1 -0
- package/dist/lib/postprocess.js +138 -0
- package/dist/lib/postprocess.js.map +1 -0
- package/dist/lib/pptx-template.d.ts +59 -0
- package/dist/lib/pptx-template.d.ts.map +1 -0
- package/dist/lib/pptx-template.js +613 -0
- package/dist/lib/pptx-template.js.map +1 -0
- package/dist/lib/pptx-themes.d.ts +80 -0
- package/dist/lib/pptx-themes.d.ts.map +1 -0
- package/dist/lib/pptx-themes.js +818 -0
- package/dist/lib/pptx-themes.js.map +1 -0
- package/dist/lib/protect-restore.d.ts +137 -0
- package/dist/lib/protect-restore.d.ts.map +1 -0
- package/dist/lib/protect-restore.js +394 -0
- package/dist/lib/protect-restore.js.map +1 -0
- package/dist/lib/rate-limiter.d.ts +27 -0
- package/dist/lib/rate-limiter.d.ts.map +1 -0
- package/dist/lib/rate-limiter.js +79 -0
- package/dist/lib/rate-limiter.js.map +1 -0
- package/dist/lib/response.d.ts +41 -0
- package/dist/lib/response.d.ts.map +1 -0
- package/dist/lib/response.js +150 -0
- package/dist/lib/response.js.map +1 -0
- package/dist/lib/review.d.ts +35 -0
- package/dist/lib/review.d.ts.map +1 -0
- package/dist/lib/review.js +263 -0
- package/dist/lib/review.js.map +1 -0
- package/dist/lib/schema.d.ts +66 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +339 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/scientific-words.d.ts +6 -0
- package/dist/lib/scientific-words.d.ts.map +1 -0
- package/dist/lib/scientific-words.js +66 -0
- package/dist/lib/scientific-words.js.map +1 -0
- package/dist/lib/sections.d.ts +40 -0
- package/dist/lib/sections.d.ts.map +1 -0
- package/dist/lib/sections.js +288 -0
- package/dist/lib/sections.js.map +1 -0
- package/dist/lib/slides.d.ts +86 -0
- package/dist/lib/slides.d.ts.map +1 -0
- package/dist/lib/slides.js +676 -0
- package/dist/lib/slides.js.map +1 -0
- package/dist/lib/spelling.d.ts +76 -0
- package/dist/lib/spelling.d.ts.map +1 -0
- package/dist/lib/spelling.js +272 -0
- package/dist/lib/spelling.js.map +1 -0
- package/dist/lib/templates.d.ts +30 -0
- package/dist/lib/templates.d.ts.map +1 -0
- package/dist/lib/templates.js +504 -0
- package/dist/lib/templates.js.map +1 -0
- package/dist/lib/themes.d.ts +85 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +652 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/lib/trackchanges.d.ts +51 -0
- package/dist/lib/trackchanges.d.ts.map +1 -0
- package/dist/lib/trackchanges.js +202 -0
- package/dist/lib/trackchanges.js.map +1 -0
- package/dist/lib/tui.d.ts +76 -0
- package/dist/lib/tui.d.ts.map +1 -0
- package/dist/lib/tui.js +377 -0
- package/dist/lib/tui.js.map +1 -0
- package/dist/lib/types.d.ts +447 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +6 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/undo.d.ts +57 -0
- package/dist/lib/undo.d.ts.map +1 -0
- package/dist/lib/undo.js +185 -0
- package/dist/lib/undo.js.map +1 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +40 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/variables.d.ts +42 -0
- package/dist/lib/variables.d.ts.map +1 -0
- package/dist/lib/variables.js +141 -0
- package/dist/lib/variables.js.map +1 -0
- package/dist/lib/word.d.ts +80 -0
- package/dist/lib/word.d.ts.map +1 -0
- package/dist/lib/word.js +360 -0
- package/dist/lib/word.js.map +1 -0
- package/dist/lib/wordcomments.d.ts +51 -0
- package/dist/lib/wordcomments.d.ts.map +1 -0
- package/dist/lib/wordcomments.js +587 -0
- package/dist/lib/wordcomments.js.map +1 -0
- package/eslint.config.js +27 -0
- package/lib/annotations.ts +622 -0
- package/lib/apply-buildup-colors.py +88 -0
- package/lib/build.ts +1013 -0
- package/lib/{citations.js → citations.ts} +38 -27
- package/lib/commands/{build.js → build.ts} +80 -27
- package/lib/commands/{citations.js → citations.ts} +36 -18
- package/lib/commands/{comments.js → comments.ts} +187 -54
- package/lib/commands/{context.js → context.ts} +18 -8
- package/lib/commands/{core.js → core.ts} +34 -20
- package/lib/commands/{doi.js → doi.ts} +32 -16
- package/lib/commands/{history.js → history.ts} +25 -12
- package/lib/commands/{index.js → index.ts} +9 -5
- package/lib/commands/{init.js → init.ts} +20 -8
- package/lib/commands/{response.js → response.ts} +47 -20
- package/lib/commands/{sections.js → sections.ts} +273 -68
- package/lib/commands/{utilities.js → utilities.ts} +338 -158
- package/lib/{comment-realign.js → comment-realign.ts} +117 -45
- package/lib/config.ts +84 -0
- package/lib/{crossref.js → crossref.ts} +213 -138
- package/lib/dependencies.ts +106 -0
- package/lib/doi-cache.ts +115 -0
- package/lib/{doi.js → doi.ts} +115 -281
- package/lib/{equations.js → equations.ts} +60 -64
- package/lib/{errors.js → errors.ts} +56 -48
- package/lib/{format.js → format.ts} +137 -63
- package/lib/{git.js → git.ts} +66 -63
- package/lib/{grammar.js → grammar.ts} +45 -32
- package/lib/image-registry.ts +180 -0
- package/lib/import.ts +2060 -0
- package/lib/journals.ts +505 -0
- package/lib/{merge.js → merge.ts} +185 -135
- package/lib/{orcid.js → orcid.ts} +17 -22
- package/lib/{pdf-comments.js → pdf-comments.ts} +76 -18
- package/lib/{pdf-import.js → pdf-import.ts} +148 -70
- package/lib/{plugins.js → plugins.ts} +82 -39
- package/lib/postprocess.ts +188 -0
- package/lib/pptx-color-filter.lua +37 -0
- package/lib/pptx-template.ts +625 -0
- package/lib/pptx-themes/academic.pptx +0 -0
- package/lib/pptx-themes/corporate.pptx +0 -0
- package/lib/pptx-themes/dark.pptx +0 -0
- package/lib/pptx-themes/default.pptx +0 -0
- package/lib/pptx-themes/minimal.pptx +0 -0
- package/lib/pptx-themes/plant.pptx +0 -0
- package/lib/pptx-themes.ts +896 -0
- package/lib/protect-restore.ts +516 -0
- package/lib/rate-limiter.ts +94 -0
- package/lib/{response.js → response.ts} +36 -21
- package/lib/{review.js → review.ts} +53 -43
- package/lib/{schema.js → schema.ts} +70 -25
- package/lib/{sections.js → sections.ts} +71 -76
- package/lib/slides.ts +793 -0
- package/lib/{spelling.js → spelling.ts} +43 -59
- package/lib/{templates.js → templates.ts} +20 -17
- package/lib/themes.ts +742 -0
- package/lib/{trackchanges.js → trackchanges.ts} +52 -23
- package/lib/types.ts +509 -0
- package/lib/{undo.js → undo.ts} +75 -52
- package/lib/utils.ts +41 -0
- package/lib/{variables.js → variables.ts} +60 -54
- package/lib/word.ts +428 -0
- package/lib/{wordcomments.js → wordcomments.ts} +94 -40
- package/package.json +15 -5
- package/skill/REFERENCE.md +67 -0
- package/tsconfig.json +26 -0
- package/lib/annotations.js +0 -414
- package/lib/build.js +0 -639
- package/lib/config.js +0 -79
- package/lib/import.js +0 -1145
- package/lib/journals.js +0 -629
- package/lib/word.js +0 -225
- /package/lib/{scientific-words.js → scientific-words.ts} +0 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section commands: import, extract, split, sync, merge
|
|
3
|
+
*
|
|
4
|
+
* Commands for importing Word documents, splitting/syncing section files.
|
|
5
|
+
*/
|
|
6
|
+
import { chalk, fs, path, fmt, findFiles, countAnnotations, loadConfig, extractSectionsFromText, splitAnnotatedPaper, buildRegistry, convertHardcodedRefs, inlineDiffPreview, } from './context.js';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
/**
|
|
9
|
+
* Detect sections from Word document text
|
|
10
|
+
* Looks for common academic paper section headers
|
|
11
|
+
*/
|
|
12
|
+
function detectSectionsFromWord(text) {
|
|
13
|
+
const lines = text.split('\n');
|
|
14
|
+
const sections = [];
|
|
15
|
+
const headerPatterns = [
|
|
16
|
+
/^(Abstract|Summary)$/i,
|
|
17
|
+
/^(Introduction|Background)$/i,
|
|
18
|
+
/^(Methods?|Materials?\s*(and|&)\s*Methods?|Methodology|Experimental\s*Methods?)$/i,
|
|
19
|
+
/^(Results?)$/i,
|
|
20
|
+
/^(Results?\s*(and|&)\s*Discussion)$/i,
|
|
21
|
+
/^(Discussion)$/i,
|
|
22
|
+
/^(Conclusions?|Summary\s*(and|&)?\s*Conclusions?)$/i,
|
|
23
|
+
/^(Acknowledgements?|Acknowledgments?)$/i,
|
|
24
|
+
/^(References|Bibliography|Literature\s*Cited|Works\s*Cited)$/i,
|
|
25
|
+
/^(Appendix|Appendices|Supplementary\s*(Materials?|Information)?|Supporting\s*Information)$/i,
|
|
26
|
+
/^(Literature\s*Review|Related\s*Work|Previous\s*Work)$/i,
|
|
27
|
+
/^(Study\s*Area|Study\s*Site|Site\s*Description)$/i,
|
|
28
|
+
/^(Data\s*Analysis|Statistical\s*Analysis|Data\s*Collection)$/i,
|
|
29
|
+
/^(Theoretical\s*Framework|Conceptual\s*Framework)$/i,
|
|
30
|
+
/^(Case\s*Study|Case\s*Studies)$/i,
|
|
31
|
+
/^(Limitations?)$/i,
|
|
32
|
+
/^(Future\s*Work|Future\s*Directions?)$/i,
|
|
33
|
+
/^(Funding|Author\s*Contributions?|Conflict\s*of\s*Interest|Data\s*Availability)$/i,
|
|
34
|
+
];
|
|
35
|
+
const numberedHeaderPattern = /^(\d+\.?\s+)(Abstract|Introduction|Background|Methods?|Materials|Results?|Discussion|Conclusions?|References|Acknowledgements?|Appendix)/i;
|
|
36
|
+
let currentSection = null;
|
|
37
|
+
let currentContent = [];
|
|
38
|
+
let preambleContent = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (!trimmed) {
|
|
42
|
+
if (currentSection) {
|
|
43
|
+
currentContent.push(line);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
preambleContent.push(line);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let isHeader = false;
|
|
51
|
+
let headerText = trimmed;
|
|
52
|
+
for (const pattern of headerPatterns) {
|
|
53
|
+
if (pattern.test(trimmed)) {
|
|
54
|
+
isHeader = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!isHeader) {
|
|
59
|
+
const match = trimmed.match(numberedHeaderPattern);
|
|
60
|
+
if (match) {
|
|
61
|
+
isHeader = true;
|
|
62
|
+
headerText = trimmed.replace(/^\d+\.?\s+/, '');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (isHeader) {
|
|
66
|
+
if (currentSection) {
|
|
67
|
+
sections.push({
|
|
68
|
+
header: currentSection,
|
|
69
|
+
content: currentContent.join('\n'),
|
|
70
|
+
file: headerToFilename(currentSection),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (preambleContent.some(l => l.trim())) {
|
|
74
|
+
sections.push({
|
|
75
|
+
header: 'Preamble',
|
|
76
|
+
content: preambleContent.join('\n'),
|
|
77
|
+
file: 'preamble.md',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
currentSection = headerText;
|
|
81
|
+
currentContent = [];
|
|
82
|
+
}
|
|
83
|
+
else if (currentSection) {
|
|
84
|
+
currentContent.push(line);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
preambleContent.push(line);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (currentSection) {
|
|
91
|
+
sections.push({
|
|
92
|
+
header: currentSection,
|
|
93
|
+
content: currentContent.join('\n'),
|
|
94
|
+
file: headerToFilename(currentSection),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (sections.length === 0) {
|
|
98
|
+
const allContent = [...preambleContent, ...currentContent].join('\n');
|
|
99
|
+
if (allContent.trim()) {
|
|
100
|
+
sections.push({
|
|
101
|
+
header: 'Content',
|
|
102
|
+
content: allContent,
|
|
103
|
+
file: 'content.md',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return sections;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Convert a section header to a filename
|
|
111
|
+
*/
|
|
112
|
+
function headerToFilename(header) {
|
|
113
|
+
return header
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
116
|
+
.replace(/^-|-$/g, '')
|
|
117
|
+
.slice(0, 30) + '.md';
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Bootstrap a new project from a Word document
|
|
121
|
+
*/
|
|
122
|
+
async function bootstrapFromWord(docx, options) {
|
|
123
|
+
const outputDir = path.resolve(options.output);
|
|
124
|
+
console.log(chalk.cyan(`Bootstrapping project from ${path.basename(docx)}...\n`));
|
|
125
|
+
try {
|
|
126
|
+
const mammoth = await import('mammoth');
|
|
127
|
+
const { default: YAML } = await import('yaml');
|
|
128
|
+
const result = await mammoth.extractRawText({ path: docx });
|
|
129
|
+
const text = result.value;
|
|
130
|
+
const sections = detectSectionsFromWord(text);
|
|
131
|
+
if (sections.length === 0) {
|
|
132
|
+
console.error(chalk.yellow('No sections detected. Creating single content.md file.'));
|
|
133
|
+
sections.push({ header: 'Content', content: text, file: 'content.md' });
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.green(`Detected ${sections.length} section(s):\n`));
|
|
136
|
+
if (!fs.existsSync(outputDir)) {
|
|
137
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
const sectionFiles = [];
|
|
140
|
+
for (const section of sections) {
|
|
141
|
+
const filePath = path.join(outputDir, section.file);
|
|
142
|
+
const content = `# ${section.header}\n\n${section.content.trim()}\n`;
|
|
143
|
+
console.log(` ${chalk.bold(section.file)} - "${section.header}" (${section.content.split('\n').length} lines)`);
|
|
144
|
+
if (!options.dryRun) {
|
|
145
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
sectionFiles.push(section.file);
|
|
148
|
+
}
|
|
149
|
+
const docxName = path.basename(docx, '.docx');
|
|
150
|
+
const title = docxName.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
151
|
+
const config = {
|
|
152
|
+
title: title,
|
|
153
|
+
authors: [],
|
|
154
|
+
sections: sectionFiles,
|
|
155
|
+
bibliography: null,
|
|
156
|
+
crossref: {
|
|
157
|
+
figureTitle: 'Figure',
|
|
158
|
+
tableTitle: 'Table',
|
|
159
|
+
figPrefix: ['Fig.', 'Figs.'],
|
|
160
|
+
tblPrefix: ['Table', 'Tables'],
|
|
161
|
+
},
|
|
162
|
+
pdf: {
|
|
163
|
+
documentclass: 'article',
|
|
164
|
+
fontsize: '12pt',
|
|
165
|
+
geometry: 'margin=1in',
|
|
166
|
+
linestretch: 1.5,
|
|
167
|
+
},
|
|
168
|
+
docx: {
|
|
169
|
+
keepComments: true,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const configPath = path.join(outputDir, 'rev.yaml');
|
|
173
|
+
console.log(`\n ${chalk.bold('rev.yaml')} - project configuration`);
|
|
174
|
+
if (!options.dryRun) {
|
|
175
|
+
fs.writeFileSync(configPath, YAML.stringify(config), 'utf-8');
|
|
176
|
+
}
|
|
177
|
+
const figuresDir = path.join(outputDir, 'figures');
|
|
178
|
+
if (!fs.existsSync(figuresDir) && !options.dryRun) {
|
|
179
|
+
fs.mkdirSync(figuresDir, { recursive: true });
|
|
180
|
+
console.log(` ${chalk.dim('figures/')} - image directory`);
|
|
181
|
+
}
|
|
182
|
+
if (options.dryRun) {
|
|
183
|
+
console.log(chalk.yellow('\n(Dry run - no files written)'));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(chalk.green('\nProject created!'));
|
|
187
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
188
|
+
if (outputDir !== process.cwd()) {
|
|
189
|
+
console.log(chalk.dim(` cd ${path.relative(process.cwd(), outputDir) || '.'}`));
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.dim(' # Edit rev.yaml to add authors and adjust settings'));
|
|
192
|
+
console.log(chalk.dim(' # Review and clean up section files'));
|
|
193
|
+
console.log(chalk.dim(' rev build # Build PDF and DOCX'));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
const error = err;
|
|
198
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
199
|
+
if (process.env.DEBUG)
|
|
200
|
+
console.error(error.stack);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Register section commands with the program
|
|
206
|
+
*/
|
|
207
|
+
export function register(program) {
|
|
208
|
+
// ==========================================================================
|
|
209
|
+
// IMPORT command - Import from Word (bootstrap or diff mode)
|
|
210
|
+
// ==========================================================================
|
|
211
|
+
program
|
|
212
|
+
.command('import')
|
|
213
|
+
.description('Import from Word: creates sections from scratch, or diffs against existing MD')
|
|
214
|
+
.argument('<docx>', 'Word document')
|
|
215
|
+
.argument('[original]', 'Optional: original Markdown file to compare against')
|
|
216
|
+
.option('-o, --output <dir>', 'Output directory for bootstrap mode', '.')
|
|
217
|
+
.option('-a, --author <name>', 'Author name for changes (diff mode)', 'Reviewer')
|
|
218
|
+
.option('--dry-run', 'Preview without saving')
|
|
219
|
+
.action(async (docx, original, options) => {
|
|
220
|
+
if (!fs.existsSync(docx)) {
|
|
221
|
+
console.error(chalk.red(`Error: Word file not found: ${docx}`));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
if (!original) {
|
|
225
|
+
await bootstrapFromWord(docx, options);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!fs.existsSync(original)) {
|
|
229
|
+
console.error(chalk.red(`Error: Original MD not found: ${original}`));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
console.log(chalk.cyan(`Comparing ${path.basename(docx)} against ${path.basename(original)}...`));
|
|
233
|
+
try {
|
|
234
|
+
const { importFromWord } = await import('../import.js');
|
|
235
|
+
const { annotated, stats } = await importFromWord(docx, original, {
|
|
236
|
+
author: options.author,
|
|
237
|
+
});
|
|
238
|
+
console.log(chalk.cyan('\nChanges detected:'));
|
|
239
|
+
if (stats.insertions > 0)
|
|
240
|
+
console.log(chalk.green(` + Insertions: ${stats.insertions}`));
|
|
241
|
+
if (stats.deletions > 0)
|
|
242
|
+
console.log(chalk.red(` - Deletions: ${stats.deletions}`));
|
|
243
|
+
if (stats.substitutions > 0)
|
|
244
|
+
console.log(chalk.yellow(` ~ Substitutions: ${stats.substitutions}`));
|
|
245
|
+
if (stats.comments > 0)
|
|
246
|
+
console.log(chalk.blue(` # Comments: ${stats.comments}`));
|
|
247
|
+
if (stats.total === 0) {
|
|
248
|
+
console.log(chalk.green('\nNo changes detected.'));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.log(chalk.dim(`\n Total: ${stats.total}`));
|
|
252
|
+
if (options.dryRun) {
|
|
253
|
+
console.log(chalk.cyan('\n--- Preview (first 1000 chars) ---\n'));
|
|
254
|
+
console.log(annotated.slice(0, 1000));
|
|
255
|
+
if (annotated.length > 1000)
|
|
256
|
+
console.log(chalk.dim('\n... (truncated)'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const outputPath = options.output || original;
|
|
260
|
+
fs.writeFileSync(outputPath, annotated, 'utf-8');
|
|
261
|
+
console.log(chalk.green(`\nSaved annotated version to ${outputPath}`));
|
|
262
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
263
|
+
console.log(` 1. ${chalk.bold('rev review ' + outputPath)} - Accept/reject track changes`);
|
|
264
|
+
console.log(` 2. Work with Claude to address comments`);
|
|
265
|
+
console.log(` 3. ${chalk.bold('rev build docx')} - Rebuild Word doc`);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
const error = err;
|
|
269
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
270
|
+
if (process.env.DEBUG)
|
|
271
|
+
console.error(error.stack);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// ==========================================================================
|
|
276
|
+
// EXTRACT command - Just extract text from Word
|
|
277
|
+
// ==========================================================================
|
|
278
|
+
program
|
|
279
|
+
.command('extract')
|
|
280
|
+
.description('Extract plain text from Word document (no diff)')
|
|
281
|
+
.argument('<docx>', 'Word document')
|
|
282
|
+
.option('-o, --output <file>', 'Output file (default: stdout)')
|
|
283
|
+
.action(async (docx, options) => {
|
|
284
|
+
if (!fs.existsSync(docx)) {
|
|
285
|
+
console.error(chalk.red(`Error: File not found: ${docx}`));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const mammoth = await import('mammoth');
|
|
290
|
+
const result = await mammoth.extractRawText({ path: docx });
|
|
291
|
+
if (options.output) {
|
|
292
|
+
fs.writeFileSync(options.output, result.value, 'utf-8');
|
|
293
|
+
console.error(chalk.green(`Extracted to ${options.output}`));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
process.stdout.write(result.value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
const error = err;
|
|
301
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// ==========================================================================
|
|
306
|
+
// SPLIT command - Split annotated paper.md back to section files
|
|
307
|
+
// ==========================================================================
|
|
308
|
+
program
|
|
309
|
+
.command('split')
|
|
310
|
+
.description('Split annotated paper.md back to section files')
|
|
311
|
+
.argument('<file>', 'Annotated paper.md file')
|
|
312
|
+
.option('-c, --config <file>', 'Sections config file', 'sections.yaml')
|
|
313
|
+
.option('-d, --dir <directory>', 'Output directory for section files', '.')
|
|
314
|
+
.option('--dry-run', 'Preview without writing files')
|
|
315
|
+
.action((file, options) => {
|
|
316
|
+
if (!fs.existsSync(file)) {
|
|
317
|
+
console.error(chalk.red(`File not found: ${file}`));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
const configPath = path.resolve(options.dir, options.config);
|
|
321
|
+
if (!fs.existsSync(configPath)) {
|
|
322
|
+
console.error(chalk.red(`Config not found: ${configPath}`));
|
|
323
|
+
console.error(chalk.dim('Run "rev init" first to generate sections.yaml'));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
console.log(chalk.cyan(`Splitting ${file} using ${options.config}...`));
|
|
327
|
+
const config = loadConfig(configPath);
|
|
328
|
+
const paperContent = fs.readFileSync(file, 'utf-8');
|
|
329
|
+
const sections = splitAnnotatedPaper(paperContent, config.sections);
|
|
330
|
+
if (sections.size === 0) {
|
|
331
|
+
console.error(chalk.yellow('No sections detected.'));
|
|
332
|
+
console.error(chalk.dim('Check that headers match sections.yaml'));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
console.log(chalk.green(`\nFound ${sections.size} sections:\n`));
|
|
336
|
+
for (const [sectionFile, content] of sections) {
|
|
337
|
+
const outputPath = path.join(options.dir, sectionFile);
|
|
338
|
+
const lines = content.split('\n').length;
|
|
339
|
+
const annotations = countAnnotations(content);
|
|
340
|
+
console.log(` ${chalk.bold(sectionFile)} (${lines} lines)`);
|
|
341
|
+
if (annotations.total > 0) {
|
|
342
|
+
const parts = [];
|
|
343
|
+
if (annotations.inserts > 0)
|
|
344
|
+
parts.push(chalk.green(`+${annotations.inserts}`));
|
|
345
|
+
if (annotations.deletes > 0)
|
|
346
|
+
parts.push(chalk.red(`-${annotations.deletes}`));
|
|
347
|
+
if (annotations.substitutes > 0)
|
|
348
|
+
parts.push(chalk.yellow(`~${annotations.substitutes}`));
|
|
349
|
+
if (annotations.comments > 0)
|
|
350
|
+
parts.push(chalk.blue(`#${annotations.comments}`));
|
|
351
|
+
console.log(chalk.dim(` Annotations: ${parts.join(' ')}`));
|
|
352
|
+
}
|
|
353
|
+
if (!options.dryRun) {
|
|
354
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (options.dryRun) {
|
|
358
|
+
console.log(chalk.yellow('\n(Dry run - no files written)'));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log(chalk.green('\nSection files updated.'));
|
|
362
|
+
console.log(chalk.cyan('\nNext: rev review <section.md> for each section'));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// ==========================================================================
|
|
366
|
+
// SYNC command - Import with section awareness
|
|
367
|
+
// ==========================================================================
|
|
368
|
+
program
|
|
369
|
+
.command('sync')
|
|
370
|
+
.alias('sections')
|
|
371
|
+
.description('Sync feedback from Word/PDF back to section files')
|
|
372
|
+
.argument('[file]', 'Word (.docx) or PDF file from reviewer (default: most recent)')
|
|
373
|
+
.argument('[sections...]', 'Specific sections to sync (default: all)')
|
|
374
|
+
.option('-c, --config <file>', 'Sections config file', 'sections.yaml')
|
|
375
|
+
.option('-d, --dir <directory>', 'Directory with section files', '.')
|
|
376
|
+
.option('--no-crossref', 'Skip converting hardcoded figure/table refs')
|
|
377
|
+
.option('--no-diff', 'Skip showing diff preview')
|
|
378
|
+
.option('--force', 'Overwrite files without conflict warning')
|
|
379
|
+
.option('--dry-run', 'Preview without writing files')
|
|
380
|
+
.action(async (docx, sections, options) => {
|
|
381
|
+
// Auto-detect most recent docx or pdf if not provided
|
|
382
|
+
if (!docx) {
|
|
383
|
+
const docxFiles = findFiles('.docx');
|
|
384
|
+
const pdfFiles = findFiles('.pdf');
|
|
385
|
+
const allFiles = [...docxFiles, ...pdfFiles];
|
|
386
|
+
if (allFiles.length === 0) {
|
|
387
|
+
console.error(fmt.status('error', 'No .docx or .pdf files found in current directory.'));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
const sorted = allFiles
|
|
391
|
+
.map(f => ({ name: f, mtime: fs.statSync(f).mtime }))
|
|
392
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
393
|
+
docx = sorted[0].name;
|
|
394
|
+
console.log(fmt.status('info', `Using most recent: ${docx}`));
|
|
395
|
+
console.log();
|
|
396
|
+
}
|
|
397
|
+
if (!fs.existsSync(docx)) {
|
|
398
|
+
console.error(fmt.status('error', `File not found: ${docx}`));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
// Handle PDF files
|
|
402
|
+
if (docx.toLowerCase().endsWith('.pdf')) {
|
|
403
|
+
const { extractPdfComments, formatPdfComments, getPdfCommentStats } = await import('../pdf-import.js');
|
|
404
|
+
const spin = fmt.spinner(`Extracting comments from ${path.basename(docx)}...`).start();
|
|
405
|
+
try {
|
|
406
|
+
const comments = await extractPdfComments(docx);
|
|
407
|
+
spin.stop();
|
|
408
|
+
if (comments.length === 0) {
|
|
409
|
+
console.log(fmt.status('info', 'No comments found in PDF.'));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const stats = getPdfCommentStats(comments);
|
|
413
|
+
console.log(fmt.header(`PDF Comments from ${path.basename(docx)}`));
|
|
414
|
+
console.log();
|
|
415
|
+
console.log(formatPdfComments(comments));
|
|
416
|
+
console.log();
|
|
417
|
+
const authorList = Object.entries(stats.byAuthor)
|
|
418
|
+
.map(([author, count]) => `${author} (${count})`)
|
|
419
|
+
.join(', ');
|
|
420
|
+
console.log(chalk.dim(`Total: ${stats.total} comments from ${authorList}`));
|
|
421
|
+
console.log();
|
|
422
|
+
const configPath = path.resolve(options.dir, options.config);
|
|
423
|
+
if (fs.existsSync(configPath) && !options.dryRun) {
|
|
424
|
+
const config = loadConfig(configPath);
|
|
425
|
+
const mainSection = config.sections?.[0];
|
|
426
|
+
if (mainSection && typeof mainSection === 'string') {
|
|
427
|
+
const mainPath = path.join(options.dir, mainSection);
|
|
428
|
+
if (fs.existsSync(mainPath)) {
|
|
429
|
+
console.log(chalk.dim(`Use 'rev pdf-comments ${docx} --append ${mainSection}' to add comments to markdown.`));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
spin.stop();
|
|
436
|
+
const error = err;
|
|
437
|
+
console.error(fmt.status('error', `Failed to extract PDF comments: ${error.message}`));
|
|
438
|
+
if (process.env.DEBUG)
|
|
439
|
+
console.error(error.stack);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const configPath = path.resolve(options.dir, options.config);
|
|
445
|
+
if (!fs.existsSync(configPath)) {
|
|
446
|
+
console.error(fmt.status('error', `Config not found: ${configPath}`));
|
|
447
|
+
console.error(chalk.dim(' Run "rev init" first to generate sections.yaml'));
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
const spin = fmt.spinner(`Importing ${path.basename(docx)}...`).start();
|
|
451
|
+
try {
|
|
452
|
+
const config = loadConfig(configPath);
|
|
453
|
+
const { importFromWord, extractWordComments, extractCommentAnchors, insertCommentsIntoMarkdown, extractFromWord } = await import('../import.js');
|
|
454
|
+
let registry = null;
|
|
455
|
+
let totalRefConversions = 0;
|
|
456
|
+
if (options.crossref !== false) {
|
|
457
|
+
registry = buildRegistry(options.dir);
|
|
458
|
+
}
|
|
459
|
+
const comments = await extractWordComments(docx);
|
|
460
|
+
const { anchors, fullDocText: xmlDocText } = await extractCommentAnchors(docx);
|
|
461
|
+
// Use pandoc for extraction to preserve markdown formatting (bold, tables, etc.)
|
|
462
|
+
// Mammoth only extracts plain text which loses all formatting
|
|
463
|
+
const wordExtraction = await extractFromWord(docx, { mediaDir: options.dir });
|
|
464
|
+
let wordText = wordExtraction.text;
|
|
465
|
+
const wordTables = wordExtraction.tables || [];
|
|
466
|
+
// Restore crossref on FULL text BEFORE splitting into sections
|
|
467
|
+
// This ensures duplicate labels from track changes are handled correctly
|
|
468
|
+
// (the same figure may appear multiple times in old/new versions)
|
|
469
|
+
const { restoreCrossrefFromWord, restoreImagesFromRegistry } = await import('../import.js');
|
|
470
|
+
const crossrefResult = restoreCrossrefFromWord(wordText, options.dir);
|
|
471
|
+
wordText = crossrefResult.text;
|
|
472
|
+
if (crossrefResult.restored > 0) {
|
|
473
|
+
console.log(`Restored ${crossrefResult.restored} crossref reference(s)`);
|
|
474
|
+
}
|
|
475
|
+
// Also restore images from registry using shared restoredLabels
|
|
476
|
+
const imageRestoreResult = restoreImagesFromRegistry(wordText, options.dir, crossrefResult.restoredLabels);
|
|
477
|
+
wordText = imageRestoreResult.text;
|
|
478
|
+
if (imageRestoreResult.restored > 0) {
|
|
479
|
+
console.log(`Restored ${imageRestoreResult.restored} image(s) from registry`);
|
|
480
|
+
}
|
|
481
|
+
let wordSections = extractSectionsFromText(wordText, config.sections);
|
|
482
|
+
if (wordSections.length === 0) {
|
|
483
|
+
spin.stop();
|
|
484
|
+
console.error(fmt.status('warning', 'No sections detected in Word document.'));
|
|
485
|
+
console.error(chalk.dim(' Check that headings match sections.yaml'));
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
if (sections && sections.length > 0) {
|
|
489
|
+
const onlyList = sections.map(s => s.trim().toLowerCase());
|
|
490
|
+
wordSections = wordSections.filter(section => {
|
|
491
|
+
const fileName = section.file.replace(/\.md$/i, '').toLowerCase();
|
|
492
|
+
const header = section.header.toLowerCase();
|
|
493
|
+
return onlyList.some(name => fileName === name || fileName.includes(name) || header.includes(name));
|
|
494
|
+
});
|
|
495
|
+
if (wordSections.length === 0) {
|
|
496
|
+
spin.stop();
|
|
497
|
+
console.error(fmt.status('error', `No sections matched: ${sections.join(', ')}`));
|
|
498
|
+
console.error(chalk.dim(` Available: ${extractSectionsFromText(wordText, config.sections).map(s => s.file.replace(/\.md$/i, '')).join(', ')}`));
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
spin.stop();
|
|
503
|
+
console.log(fmt.header(`Import from ${path.basename(docx)}`));
|
|
504
|
+
console.log();
|
|
505
|
+
// Conflict detection
|
|
506
|
+
if (!options.force && !options.dryRun) {
|
|
507
|
+
const conflicts = [];
|
|
508
|
+
for (const section of wordSections) {
|
|
509
|
+
const sectionPath = path.join(options.dir, section.file);
|
|
510
|
+
if (fs.existsSync(sectionPath)) {
|
|
511
|
+
const existing = fs.readFileSync(sectionPath, 'utf-8');
|
|
512
|
+
const existingCounts = countAnnotations(existing);
|
|
513
|
+
if (existingCounts.total > 0) {
|
|
514
|
+
conflicts.push({
|
|
515
|
+
file: section.file,
|
|
516
|
+
annotations: existingCounts.total,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (conflicts.length > 0) {
|
|
522
|
+
console.log(fmt.status('warning', 'Files with existing annotations will be overwritten:'));
|
|
523
|
+
for (const c of conflicts) {
|
|
524
|
+
console.log(chalk.yellow(` - ${c.file} (${c.annotations} annotations)`));
|
|
525
|
+
}
|
|
526
|
+
console.log();
|
|
527
|
+
const rl = readline.createInterface({
|
|
528
|
+
input: process.stdin,
|
|
529
|
+
output: process.stdout,
|
|
530
|
+
});
|
|
531
|
+
const answer = await new Promise((resolve) => rl.question(chalk.cyan('Continue and overwrite? [y/N] '), resolve));
|
|
532
|
+
rl.close();
|
|
533
|
+
if (answer.toLowerCase() !== 'y') {
|
|
534
|
+
console.log(chalk.dim('Aborted. Use --force to skip this check.'));
|
|
535
|
+
process.exit(0);
|
|
536
|
+
}
|
|
537
|
+
console.log();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const sectionResults = [];
|
|
541
|
+
let totalChanges = 0;
|
|
542
|
+
// Calculate section boundaries in the XML document text for comment filtering
|
|
543
|
+
// Comment positions (docPosition) are relative to xmlDocText, NOT wordText
|
|
544
|
+
// So we must find section headers in xmlDocText to get matching boundaries
|
|
545
|
+
const sectionBoundaries = [];
|
|
546
|
+
const xmlLower = xmlDocText.toLowerCase();
|
|
547
|
+
// Standard section header keywords to search for in XML
|
|
548
|
+
// Map from file name pattern to search terms
|
|
549
|
+
const sectionKeywords = {
|
|
550
|
+
'abstract': ['abstract', 'summary'],
|
|
551
|
+
'introduction': ['introduction', 'background'],
|
|
552
|
+
'methods': ['methods', 'materials and methods', 'methodology'],
|
|
553
|
+
'results': ['results'],
|
|
554
|
+
'discussion': ['discussion'],
|
|
555
|
+
'conclusion': ['conclusion', 'conclusions'],
|
|
556
|
+
};
|
|
557
|
+
// Helper: find section header (skip labels like "Methods:" in structured abstracts)
|
|
558
|
+
// Real section headers are NOT followed by ":" immediately
|
|
559
|
+
function findSectionHeader(text, keyword, startFrom = 0) {
|
|
560
|
+
const lower = text.toLowerCase();
|
|
561
|
+
let idx = startFrom;
|
|
562
|
+
while ((idx = lower.indexOf(keyword, idx)) !== -1) {
|
|
563
|
+
// Check what follows the keyword
|
|
564
|
+
const afterKeyword = text.slice(idx + keyword.length, idx + keyword.length + 5);
|
|
565
|
+
// Skip if followed by ":" (this is a label, not a section header)
|
|
566
|
+
// Real headers are followed by text content, a newline, or a subheading
|
|
567
|
+
if (!afterKeyword.startsWith(':') && !afterKeyword.startsWith(' :')) {
|
|
568
|
+
return idx;
|
|
569
|
+
}
|
|
570
|
+
idx++;
|
|
571
|
+
}
|
|
572
|
+
return -1;
|
|
573
|
+
}
|
|
574
|
+
for (const section of wordSections) {
|
|
575
|
+
const fileBase = section.file.replace(/\.md$/i, '').toLowerCase();
|
|
576
|
+
// Get keywords for this section
|
|
577
|
+
const keywords = sectionKeywords[fileBase] || [fileBase];
|
|
578
|
+
// Find the first valid keyword that exists in XML (not a label)
|
|
579
|
+
let headerIdx = -1;
|
|
580
|
+
for (const kw of keywords) {
|
|
581
|
+
const idx = findSectionHeader(xmlDocText, kw, 0);
|
|
582
|
+
if (idx >= 0 && (headerIdx < 0 || idx < headerIdx)) {
|
|
583
|
+
headerIdx = idx;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (headerIdx >= 0) {
|
|
587
|
+
// Find the next section's start to determine end boundary
|
|
588
|
+
let nextHeaderIdx = xmlDocText.length;
|
|
589
|
+
const sectionIdx = wordSections.indexOf(section);
|
|
590
|
+
if (sectionIdx < wordSections.length - 1) {
|
|
591
|
+
const nextFileBase = wordSections[sectionIdx + 1].file.replace(/\.md$/i, '').toLowerCase();
|
|
592
|
+
const nextKeywords = sectionKeywords[nextFileBase] || [nextFileBase];
|
|
593
|
+
for (const nkw of nextKeywords) {
|
|
594
|
+
const foundNext = findSectionHeader(xmlDocText, nkw, headerIdx + 10);
|
|
595
|
+
if (foundNext >= 0 && foundNext < nextHeaderIdx) {
|
|
596
|
+
nextHeaderIdx = foundNext;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
sectionBoundaries.push({
|
|
601
|
+
file: section.file,
|
|
602
|
+
start: headerIdx,
|
|
603
|
+
end: nextHeaderIdx
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Document length is the XML text length (same coordinate system as docPosition)
|
|
608
|
+
const docLength = xmlDocText.length;
|
|
609
|
+
for (const section of wordSections) {
|
|
610
|
+
const sectionPath = path.join(options.dir, section.file);
|
|
611
|
+
if (!fs.existsSync(sectionPath)) {
|
|
612
|
+
sectionResults.push({
|
|
613
|
+
file: section.file,
|
|
614
|
+
header: section.header,
|
|
615
|
+
status: 'skipped',
|
|
616
|
+
stats: undefined,
|
|
617
|
+
});
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const result = await importFromWord(docx, sectionPath, {
|
|
621
|
+
sectionContent: section.content,
|
|
622
|
+
author: 'Reviewer',
|
|
623
|
+
wordTables: wordTables,
|
|
624
|
+
});
|
|
625
|
+
let { annotated, stats } = result;
|
|
626
|
+
let refConversions = [];
|
|
627
|
+
if (registry && options.crossref !== false) {
|
|
628
|
+
const crossrefResult = convertHardcodedRefs(annotated, registry);
|
|
629
|
+
annotated = crossrefResult.converted;
|
|
630
|
+
refConversions = crossrefResult.conversions;
|
|
631
|
+
totalRefConversions += refConversions.length;
|
|
632
|
+
}
|
|
633
|
+
let commentsInserted = 0;
|
|
634
|
+
if (comments.length > 0 && anchors.size > 0) {
|
|
635
|
+
// Filter comments to only those that belong to this section
|
|
636
|
+
// Use exact position matching: docPosition is in xmlDocText coordinates,
|
|
637
|
+
// and sectionBoundaries are also in xmlDocText coordinates (same source!)
|
|
638
|
+
const boundary = sectionBoundaries.find(b => b.file === section.file);
|
|
639
|
+
const isFirstSection = wordSections.indexOf(section) === 0;
|
|
640
|
+
const firstBoundaryStart = sectionBoundaries.length > 0 ? Math.min(...sectionBoundaries.map(b => b.start)) : 0;
|
|
641
|
+
const sectionComments = comments.filter((c) => {
|
|
642
|
+
const anchorData = anchors.get(c.id);
|
|
643
|
+
if (!anchorData)
|
|
644
|
+
return false;
|
|
645
|
+
// Use exact position - no scaling needed since both are in xmlDocText coordinates
|
|
646
|
+
if (anchorData.docPosition !== undefined && boundary) {
|
|
647
|
+
// Include comments within section boundaries
|
|
648
|
+
if (anchorData.docPosition >= boundary.start && anchorData.docPosition < boundary.end) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
// Also include "outside" comments (before first section) in the first section file
|
|
652
|
+
if (isFirstSection && anchorData.docPosition < firstBoundaryStart) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return false;
|
|
657
|
+
});
|
|
658
|
+
if (process.env.DEBUG) {
|
|
659
|
+
console.log(`[DEBUG] ${section.file}: ${sectionComments.length} comments to place (boundary: ${boundary?.start}-${boundary?.end})`);
|
|
660
|
+
}
|
|
661
|
+
if (sectionComments.length > 0) {
|
|
662
|
+
// Use a more robust pattern that handles < in comment text
|
|
663
|
+
const commentPattern = /\{>>.*?<<\}/gs;
|
|
664
|
+
const beforeCount = (annotated.match(commentPattern) || []).length;
|
|
665
|
+
annotated = insertCommentsIntoMarkdown(annotated, sectionComments, anchors, {
|
|
666
|
+
quiet: !process.env.DEBUG,
|
|
667
|
+
sectionBoundary: boundary // Pass section boundary for position-based insertion
|
|
668
|
+
});
|
|
669
|
+
const afterCount = (annotated.match(commentPattern) || []).length;
|
|
670
|
+
commentsInserted = afterCount - beforeCount;
|
|
671
|
+
if (process.env.DEBUG) {
|
|
672
|
+
console.log(`[DEBUG] ${section.file}: inserted ${commentsInserted} of ${sectionComments.length} comments`);
|
|
673
|
+
}
|
|
674
|
+
if (commentsInserted > 0) {
|
|
675
|
+
stats.comments = (stats.comments || 0) + commentsInserted;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
totalChanges += stats.total;
|
|
680
|
+
sectionResults.push({
|
|
681
|
+
file: section.file,
|
|
682
|
+
header: section.header,
|
|
683
|
+
status: 'ok',
|
|
684
|
+
stats,
|
|
685
|
+
refs: refConversions.length,
|
|
686
|
+
});
|
|
687
|
+
if (!options.dryRun) {
|
|
688
|
+
fs.writeFileSync(sectionPath, annotated, 'utf-8');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const tableRows = sectionResults.map((r) => {
|
|
692
|
+
if (r.status === 'skipped') {
|
|
693
|
+
return [
|
|
694
|
+
chalk.dim(r.file),
|
|
695
|
+
chalk.dim(r.header.slice(0, 25)),
|
|
696
|
+
chalk.yellow('skipped'),
|
|
697
|
+
'',
|
|
698
|
+
'',
|
|
699
|
+
'',
|
|
700
|
+
'',
|
|
701
|
+
];
|
|
702
|
+
}
|
|
703
|
+
const s = r.stats;
|
|
704
|
+
return [
|
|
705
|
+
chalk.bold(r.file),
|
|
706
|
+
r.header.length > 25 ? r.header.slice(0, 22) + '...' : r.header,
|
|
707
|
+
s.insertions > 0 ? chalk.green(`+${s.insertions}`) : chalk.dim('-'),
|
|
708
|
+
s.deletions > 0 ? chalk.red(`-${s.deletions}`) : chalk.dim('-'),
|
|
709
|
+
s.substitutions > 0 ? chalk.yellow(`~${s.substitutions}`) : chalk.dim('-'),
|
|
710
|
+
s.comments > 0 ? chalk.blue(`#${s.comments}`) : chalk.dim('-'),
|
|
711
|
+
r.refs > 0 ? chalk.magenta(`@${r.refs}`) : chalk.dim('-'),
|
|
712
|
+
];
|
|
713
|
+
});
|
|
714
|
+
console.log(fmt.table(['File', 'Section', 'Ins', 'Del', 'Sub', 'Cmt', 'Ref'], tableRows, { align: ['left', 'left', 'right', 'right', 'right', 'right', 'right'] }));
|
|
715
|
+
console.log();
|
|
716
|
+
if (options.diff !== false && totalChanges > 0) {
|
|
717
|
+
console.log(fmt.header('Changes Preview'));
|
|
718
|
+
console.log();
|
|
719
|
+
for (const result of sectionResults) {
|
|
720
|
+
if (result.status === 'ok' && result.stats && result.stats.total > 0) {
|
|
721
|
+
const sectionPath = path.join(options.dir, result.file);
|
|
722
|
+
if (fs.existsSync(sectionPath)) {
|
|
723
|
+
const content = fs.readFileSync(sectionPath, 'utf-8');
|
|
724
|
+
const preview = inlineDiffPreview(content, { maxLines: 3 });
|
|
725
|
+
if (preview) {
|
|
726
|
+
console.log(chalk.bold(result.file) + ':');
|
|
727
|
+
console.log(preview);
|
|
728
|
+
console.log();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (options.dryRun) {
|
|
735
|
+
console.log(fmt.box(chalk.yellow('Dry run - no files written'), { padding: 0 }));
|
|
736
|
+
}
|
|
737
|
+
else if (totalChanges > 0 || totalRefConversions > 0 || comments.length > 0) {
|
|
738
|
+
const summaryLines = [];
|
|
739
|
+
summaryLines.push(`${chalk.bold(wordSections.length)} sections processed`);
|
|
740
|
+
if (totalChanges > 0)
|
|
741
|
+
summaryLines.push(`${chalk.bold(totalChanges)} annotations imported`);
|
|
742
|
+
if (comments.length > 0)
|
|
743
|
+
summaryLines.push(`${chalk.bold(comments.length)} comments placed`);
|
|
744
|
+
if (totalRefConversions > 0)
|
|
745
|
+
summaryLines.push(`${chalk.bold(totalRefConversions)} refs converted to @-syntax`);
|
|
746
|
+
console.log(fmt.box(summaryLines.join('\n'), { title: 'Summary', padding: 0 }));
|
|
747
|
+
console.log();
|
|
748
|
+
console.log(chalk.dim('Next steps:'));
|
|
749
|
+
console.log(chalk.dim(' 1. rev review <section.md> - Accept/reject changes'));
|
|
750
|
+
console.log(chalk.dim(' 2. rev comments <section.md> - View/address comments'));
|
|
751
|
+
console.log(chalk.dim(' 3. rev build docx - Rebuild Word doc'));
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
console.log(fmt.status('success', 'No changes detected.'));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
spin.stop();
|
|
759
|
+
const error = err;
|
|
760
|
+
console.error(fmt.status('error', error.message));
|
|
761
|
+
if (process.env.DEBUG)
|
|
762
|
+
console.error(error.stack);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
// ==========================================================================
|
|
767
|
+
// MERGE command - Combine feedback from multiple reviewers (three-way merge)
|
|
768
|
+
// ==========================================================================
|
|
769
|
+
program
|
|
770
|
+
.command('merge')
|
|
771
|
+
.description('Merge feedback from multiple Word documents using three-way merge')
|
|
772
|
+
.argument('<docx...>', 'Word documents from reviewers')
|
|
773
|
+
.option('-b, --base <file>', 'Base document (original sent to reviewers). Auto-detected if not specified.')
|
|
774
|
+
.option('-o, --output <file>', 'Output file (default: writes to section files)')
|
|
775
|
+
.option('--names <names>', 'Reviewer names (comma-separated, in order of docx files)')
|
|
776
|
+
.option('--strategy <strategy>', 'Conflict resolution: first, latest, or interactive (default)', 'interactive')
|
|
777
|
+
.option('--diff-level <level>', 'Diff granularity: sentence or word (default: sentence)', 'sentence')
|
|
778
|
+
.option('--dry-run', 'Show conflicts without writing')
|
|
779
|
+
.option('--sections', 'Split merged output back to section files')
|
|
780
|
+
.action(async (docxFiles, options) => {
|
|
781
|
+
const { mergeThreeWay, formatConflict, resolveConflict, getBaseDocument, checkBaseMatch, saveConflicts, } = await import('../merge.js');
|
|
782
|
+
// Validate reviewer files exist
|
|
783
|
+
for (const docx of docxFiles) {
|
|
784
|
+
if (!fs.existsSync(docx)) {
|
|
785
|
+
console.error(fmt.status('error', `Reviewer file not found: ${docx}`));
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Determine base document
|
|
790
|
+
let basePath = options.base;
|
|
791
|
+
let baseSource = 'specified';
|
|
792
|
+
if (!basePath) {
|
|
793
|
+
// Try to use .rev/base.docx
|
|
794
|
+
const projectDir = process.cwd();
|
|
795
|
+
basePath = getBaseDocument(projectDir) ?? undefined;
|
|
796
|
+
if (basePath) {
|
|
797
|
+
baseSource = 'auto (.rev/base.docx)';
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
console.log(chalk.yellow('\n No base document found in .rev/base.docx'));
|
|
801
|
+
console.log(chalk.dim(' Tip: Run "rev build docx" to automatically save the base document.\n'));
|
|
802
|
+
console.error(fmt.status('error', 'Base document required. Use --base <file.docx>'));
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (!fs.existsSync(basePath)) {
|
|
807
|
+
console.error(fmt.status('error', `Base document not found: ${basePath}`));
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
// Check similarity between base and reviewer docs
|
|
811
|
+
const { matches, similarity } = await checkBaseMatch(basePath, docxFiles[0]);
|
|
812
|
+
if (!matches) {
|
|
813
|
+
console.log(chalk.yellow(`\n Warning: Base document may not match reviewer file (${Math.round(similarity * 100)}% similar)`));
|
|
814
|
+
console.log(chalk.dim(' If this is wrong, use --base to specify the correct original document.\n'));
|
|
815
|
+
}
|
|
816
|
+
// Parse reviewer names
|
|
817
|
+
const names = options.names
|
|
818
|
+
? options.names.split(',').map(n => n.trim())
|
|
819
|
+
: docxFiles.map((f, i) => {
|
|
820
|
+
// Try to extract name from filename (e.g., paper_reviewer_A.docx)
|
|
821
|
+
const basename = path.basename(f, '.docx');
|
|
822
|
+
const match = basename.match(/_([A-Za-z]+)$/);
|
|
823
|
+
return match ? match[1] : `Reviewer ${i + 1}`;
|
|
824
|
+
});
|
|
825
|
+
// Pad names if needed
|
|
826
|
+
while (names.length < docxFiles.length) {
|
|
827
|
+
names.push(`Reviewer ${names.length + 1}`);
|
|
828
|
+
}
|
|
829
|
+
const reviewerDocs = docxFiles.map((p, i) => ({
|
|
830
|
+
path: p,
|
|
831
|
+
name: names[i],
|
|
832
|
+
}));
|
|
833
|
+
console.log(fmt.header('Three-Way Merge'));
|
|
834
|
+
console.log();
|
|
835
|
+
console.log(chalk.dim(` Base: ${path.basename(basePath)} (${baseSource})`));
|
|
836
|
+
console.log(chalk.dim(` Reviewers: ${names.join(', ')}`));
|
|
837
|
+
console.log(chalk.dim(` Diff level: ${options.diffLevel}`));
|
|
838
|
+
console.log();
|
|
839
|
+
const spin = fmt.spinner('Analyzing changes...').start();
|
|
840
|
+
try {
|
|
841
|
+
const { merged, conflicts, stats, baseText } = await mergeThreeWay(basePath, reviewerDocs, {
|
|
842
|
+
diffLevel: options.diffLevel,
|
|
843
|
+
});
|
|
844
|
+
spin.stop();
|
|
845
|
+
// Display stats
|
|
846
|
+
console.log(fmt.table(['Metric', 'Count'], [
|
|
847
|
+
['Total changes', stats.totalChanges.toString()],
|
|
848
|
+
['Non-conflicting', stats.nonConflicting.toString()],
|
|
849
|
+
['Conflicts', stats.conflicts.toString()],
|
|
850
|
+
['Comments', stats.comments.toString()],
|
|
851
|
+
]));
|
|
852
|
+
console.log();
|
|
853
|
+
let finalMerged = merged;
|
|
854
|
+
// Handle conflicts
|
|
855
|
+
if (conflicts.length > 0) {
|
|
856
|
+
console.log(chalk.yellow(`Found ${conflicts.length} conflict(s):\n`));
|
|
857
|
+
if (options.strategy === 'first') {
|
|
858
|
+
// Auto-resolve: take first reviewer's change
|
|
859
|
+
for (const conflict of conflicts) {
|
|
860
|
+
console.log(chalk.dim(` Conflict ${conflict.id}: using ${conflict.changes[0].reviewer}'s change`));
|
|
861
|
+
resolveConflict(conflict, 0);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else if (options.strategy === 'latest') {
|
|
865
|
+
// Auto-resolve: take last reviewer's change
|
|
866
|
+
for (const conflict of conflicts) {
|
|
867
|
+
const lastIdx = conflict.changes.length - 1;
|
|
868
|
+
console.log(chalk.dim(` Conflict ${conflict.id}: using ${conflict.changes[lastIdx].reviewer}'s change`));
|
|
869
|
+
resolveConflict(conflict, lastIdx);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else if (!options.dryRun) {
|
|
873
|
+
// Interactive resolution
|
|
874
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
875
|
+
const conflict = conflicts[i];
|
|
876
|
+
console.log(chalk.bold(`\nConflict ${i + 1}/${conflicts.length} (${conflict.id}):`));
|
|
877
|
+
console.log(formatConflict(conflict, baseText));
|
|
878
|
+
console.log();
|
|
879
|
+
const rl = readline.createInterface({
|
|
880
|
+
input: process.stdin,
|
|
881
|
+
output: process.stdout,
|
|
882
|
+
});
|
|
883
|
+
const answer = await new Promise((resolve) => rl.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip): `), resolve));
|
|
884
|
+
rl.close();
|
|
885
|
+
if (answer.toLowerCase() !== 's' && !isNaN(parseInt(answer))) {
|
|
886
|
+
const choice = parseInt(answer) - 1;
|
|
887
|
+
if (choice >= 0 && choice < conflict.changes.length) {
|
|
888
|
+
resolveConflict(conflict, choice);
|
|
889
|
+
console.log(chalk.green(` ✓ Applied: ${conflict.changes[choice].reviewer}'s change`));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
console.log(chalk.dim(' Skipped (will need manual resolution)'));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Save unresolved conflicts for later
|
|
898
|
+
const unresolved = conflicts.filter(c => c.resolved === null);
|
|
899
|
+
if (unresolved.length > 0) {
|
|
900
|
+
saveConflicts(process.cwd(), conflicts, basePath);
|
|
901
|
+
console.log(chalk.yellow(`\n ${unresolved.length} unresolved conflict(s) saved to .rev/conflicts.json`));
|
|
902
|
+
console.log(chalk.dim(' Run "rev conflicts" to view, "rev merge-resolve" to resolve'));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Write output
|
|
906
|
+
if (!options.dryRun) {
|
|
907
|
+
if (options.output) {
|
|
908
|
+
// Write to single file
|
|
909
|
+
fs.writeFileSync(options.output, finalMerged, 'utf-8');
|
|
910
|
+
console.log(fmt.status('success', `Merged output written to ${options.output}`));
|
|
911
|
+
}
|
|
912
|
+
else if (options.sections) {
|
|
913
|
+
// Split to section files (TODO: implement section splitting)
|
|
914
|
+
console.log(chalk.yellow(' Section splitting not yet implemented'));
|
|
915
|
+
console.log(chalk.dim(' Use -o to specify output file'));
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// Default: write to merged.md
|
|
919
|
+
const outPath = 'merged.md';
|
|
920
|
+
fs.writeFileSync(outPath, finalMerged, 'utf-8');
|
|
921
|
+
console.log(fmt.status('success', `Merged output written to ${outPath}`));
|
|
922
|
+
}
|
|
923
|
+
console.log();
|
|
924
|
+
console.log(chalk.dim('Next steps:'));
|
|
925
|
+
console.log(chalk.dim(' 1. rev review merged.md - Accept/reject changes'));
|
|
926
|
+
console.log(chalk.dim(' 2. rev comments merged.md - Address comments'));
|
|
927
|
+
if (conflicts.some(c => c.resolved === null)) {
|
|
928
|
+
console.log(chalk.dim(' 3. rev merge-resolve - Resolve remaining conflicts'));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
console.log(fmt.status('info', 'Dry run - no output written'));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
spin.stop();
|
|
937
|
+
const error = err;
|
|
938
|
+
console.error(fmt.status('error', error.message));
|
|
939
|
+
if (process.env.DEBUG)
|
|
940
|
+
console.error(error.stack);
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
// ==========================================================================
|
|
945
|
+
// CONFLICTS command - List unresolved conflicts
|
|
946
|
+
// ==========================================================================
|
|
947
|
+
program
|
|
948
|
+
.command('conflicts')
|
|
949
|
+
.description('List unresolved merge conflicts')
|
|
950
|
+
.action(async () => {
|
|
951
|
+
const { loadConflicts, formatConflict } = await import('../merge.js');
|
|
952
|
+
const projectDir = process.cwd();
|
|
953
|
+
const data = loadConflicts(projectDir);
|
|
954
|
+
if (!data) {
|
|
955
|
+
console.log(fmt.status('info', 'No conflicts file found'));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const unresolved = data.conflicts.filter((c) => c.resolved === null);
|
|
959
|
+
if (unresolved.length === 0) {
|
|
960
|
+
console.log(fmt.status('success', 'All conflicts resolved!'));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
console.log(fmt.header(`Unresolved Conflicts (${unresolved.length})`));
|
|
964
|
+
console.log();
|
|
965
|
+
console.log(chalk.dim(` Base: ${data.base}`));
|
|
966
|
+
console.log(chalk.dim(` Merged: ${data.merged}`));
|
|
967
|
+
console.log();
|
|
968
|
+
for (const conflict of unresolved) {
|
|
969
|
+
console.log(chalk.bold(`Conflict ${conflict.id}:`));
|
|
970
|
+
// Show abbreviated info
|
|
971
|
+
console.log(chalk.dim(` Original: "${conflict.original.slice(0, 50)}${conflict.original.length > 50 ? '...' : ''}"`));
|
|
972
|
+
console.log(chalk.dim(` Options: ${conflict.changes.map((c) => c.reviewer).join(', ')}`));
|
|
973
|
+
console.log();
|
|
974
|
+
}
|
|
975
|
+
console.log(chalk.dim('Run "rev merge-resolve" to resolve conflicts interactively'));
|
|
976
|
+
});
|
|
977
|
+
// ==========================================================================
|
|
978
|
+
// MERGE-RESOLVE command - Interactively resolve merge conflicts
|
|
979
|
+
// ==========================================================================
|
|
980
|
+
program
|
|
981
|
+
.command('merge-resolve')
|
|
982
|
+
.alias('mresolve')
|
|
983
|
+
.description('Resolve merge conflicts interactively')
|
|
984
|
+
.option('--theirs', 'Accept all changes from last reviewer')
|
|
985
|
+
.option('--ours', 'Accept all changes from first reviewer')
|
|
986
|
+
.action(async (options) => {
|
|
987
|
+
const { loadConflicts, saveConflicts, clearConflicts, resolveConflict, formatConflict } = await import('../merge.js');
|
|
988
|
+
const projectDir = process.cwd();
|
|
989
|
+
const data = loadConflicts(projectDir);
|
|
990
|
+
if (!data) {
|
|
991
|
+
console.log(fmt.status('info', 'No conflicts to resolve'));
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const unresolved = data.conflicts.filter((c) => c.resolved === null);
|
|
995
|
+
if (unresolved.length === 0) {
|
|
996
|
+
console.log(fmt.status('success', 'All conflicts already resolved!'));
|
|
997
|
+
clearConflicts(projectDir);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
console.log(fmt.header(`Resolving ${unresolved.length} Conflict(s)`));
|
|
1001
|
+
console.log();
|
|
1002
|
+
if (options.theirs) {
|
|
1003
|
+
// Accept all from last reviewer
|
|
1004
|
+
for (const conflict of unresolved) {
|
|
1005
|
+
const lastIdx = conflict.changes.length - 1;
|
|
1006
|
+
resolveConflict(conflict, lastIdx);
|
|
1007
|
+
console.log(chalk.dim(` ${conflict.id}: accepted ${conflict.changes[lastIdx].reviewer}'s change`));
|
|
1008
|
+
}
|
|
1009
|
+
saveConflicts(projectDir, data.conflicts, data.base);
|
|
1010
|
+
console.log(fmt.status('success', `Resolved ${unresolved.length} conflicts (--theirs)`));
|
|
1011
|
+
}
|
|
1012
|
+
else if (options.ours) {
|
|
1013
|
+
// Accept all from first reviewer
|
|
1014
|
+
for (const conflict of unresolved) {
|
|
1015
|
+
resolveConflict(conflict, 0);
|
|
1016
|
+
console.log(chalk.dim(` ${conflict.id}: accepted ${conflict.changes[0].reviewer}'s change`));
|
|
1017
|
+
}
|
|
1018
|
+
saveConflicts(projectDir, data.conflicts, data.base);
|
|
1019
|
+
console.log(fmt.status('success', `Resolved ${unresolved.length} conflicts (--ours)`));
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
// Interactive resolution
|
|
1023
|
+
// Read base text for context display
|
|
1024
|
+
let baseText = '';
|
|
1025
|
+
try {
|
|
1026
|
+
const { extractFromWord } = await import('../import.js');
|
|
1027
|
+
const { text } = await extractFromWord(data.base);
|
|
1028
|
+
baseText = text;
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
// Can't read base, show without context
|
|
1032
|
+
}
|
|
1033
|
+
for (let i = 0; i < unresolved.length; i++) {
|
|
1034
|
+
const conflict = unresolved[i];
|
|
1035
|
+
console.log(chalk.bold(`\nConflict ${i + 1}/${unresolved.length} (${conflict.id}):`));
|
|
1036
|
+
console.log(formatConflict(conflict, baseText));
|
|
1037
|
+
console.log();
|
|
1038
|
+
const rl = readline.createInterface({
|
|
1039
|
+
input: process.stdin,
|
|
1040
|
+
output: process.stdout,
|
|
1041
|
+
});
|
|
1042
|
+
const answer = await new Promise((resolve) => rl.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip, q=quit): `), resolve));
|
|
1043
|
+
rl.close();
|
|
1044
|
+
if (answer.toLowerCase() === 'q') {
|
|
1045
|
+
console.log(chalk.dim('\n Saving progress...'));
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
if (answer.toLowerCase() !== 's' && !isNaN(parseInt(answer))) {
|
|
1049
|
+
const choice = parseInt(answer) - 1;
|
|
1050
|
+
if (choice >= 0 && choice < conflict.changes.length) {
|
|
1051
|
+
resolveConflict(conflict, choice);
|
|
1052
|
+
console.log(chalk.green(` ✓ Applied: ${conflict.changes[choice].reviewer}'s change`));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
console.log(chalk.dim(' Skipped'));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
saveConflicts(projectDir, data.conflicts, data.base);
|
|
1060
|
+
const remaining = data.conflicts.filter((c) => c.resolved === null).length;
|
|
1061
|
+
if (remaining === 0) {
|
|
1062
|
+
console.log(fmt.status('success', '\nAll conflicts resolved!'));
|
|
1063
|
+
clearConflicts(projectDir);
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
console.log(chalk.yellow(`\n ${remaining} conflict(s) remaining`));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
//# sourceMappingURL=sections.js.map
|