docrev 0.6.13 → 0.7.6
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/CHANGELOG.md +32 -0
- package/README.md +191 -133
- package/bin/rev.js +113 -5059
- package/completions/rev.ps1 +210 -0
- package/lib/annotations.js +41 -11
- package/lib/build.js +95 -8
- package/lib/commands/build.js +708 -0
- package/lib/commands/citations.js +497 -0
- package/lib/commands/comments.js +922 -0
- package/lib/commands/context.js +165 -0
- package/lib/commands/core.js +295 -0
- package/lib/commands/doi.js +419 -0
- package/lib/commands/history.js +307 -0
- package/lib/commands/index.js +56 -0
- package/lib/commands/init.js +247 -0
- package/lib/commands/response.js +374 -0
- package/lib/commands/sections.js +862 -0
- package/lib/commands/utilities.js +2272 -0
- package/lib/config.js +19 -0
- package/lib/crossref.js +17 -2
- package/lib/doi.js +279 -43
- package/lib/errors.js +338 -0
- package/lib/format.js +53 -6
- package/lib/git.js +92 -0
- package/lib/import.js +24 -3
- package/lib/journals.js +28 -4
- package/lib/orcid.js +149 -0
- package/lib/pdf-comments.js +217 -0
- package/lib/pdf-import.js +446 -0
- package/lib/plugins.js +285 -0
- package/lib/review.js +109 -0
- package/lib/schema.js +368 -0
- package/lib/sections.js +3 -8
- package/lib/templates.js +218 -0
- package/lib/tui.js +437 -0
- package/lib/undo.js +236 -0
- package/lib/wordcomments.js +15 -20
- package/package.json +5 -3
- package/skill/REFERENCE.md +76 -18
- package/skill/SKILL.md +122 -27
- package/.rev-dictionary +0 -4
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context for command modules
|
|
3
|
+
*
|
|
4
|
+
* This module provides shared utilities and state that command modules need.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fmt from '../format.js';
|
|
11
|
+
|
|
12
|
+
// Global flags (set by main CLI)
|
|
13
|
+
export let quietMode = false;
|
|
14
|
+
export let jsonMode = false;
|
|
15
|
+
|
|
16
|
+
export function setQuietMode(value) {
|
|
17
|
+
quietMode = value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setJsonMode(value) {
|
|
21
|
+
jsonMode = value;
|
|
22
|
+
if (value) {
|
|
23
|
+
chalk.level = 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// JSON output helper
|
|
28
|
+
export function jsonOutput(data) {
|
|
29
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Find files by extension
|
|
33
|
+
export function findFiles(ext, cwd = process.cwd()) {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readdirSync(cwd)
|
|
36
|
+
.filter(f => f.endsWith(ext) && !f.startsWith('.'));
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Re-export common dependencies
|
|
43
|
+
export { chalk, fs, path, fmt };
|
|
44
|
+
|
|
45
|
+
// Re-export from lib modules
|
|
46
|
+
export {
|
|
47
|
+
parseAnnotations,
|
|
48
|
+
stripAnnotations,
|
|
49
|
+
countAnnotations,
|
|
50
|
+
getComments,
|
|
51
|
+
setCommentStatus,
|
|
52
|
+
hasAnnotations,
|
|
53
|
+
getTrackChanges,
|
|
54
|
+
applyDecision,
|
|
55
|
+
} from '../annotations.js';
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
interactiveReview,
|
|
59
|
+
listComments,
|
|
60
|
+
interactiveCommentReview,
|
|
61
|
+
} from '../review.js';
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
generateConfig,
|
|
65
|
+
loadConfig,
|
|
66
|
+
saveConfig,
|
|
67
|
+
matchHeading,
|
|
68
|
+
extractSectionsFromText,
|
|
69
|
+
splitAnnotatedPaper,
|
|
70
|
+
getOrderedSections,
|
|
71
|
+
} from '../sections.js';
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
buildRegistry,
|
|
75
|
+
detectHardcodedRefs,
|
|
76
|
+
convertHardcodedRefs,
|
|
77
|
+
getRefStatus,
|
|
78
|
+
formatRegistry,
|
|
79
|
+
} from '../crossref.js';
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
build,
|
|
83
|
+
loadConfig as loadBuildConfig,
|
|
84
|
+
hasPandoc,
|
|
85
|
+
hasPandocCrossref,
|
|
86
|
+
formatBuildResults,
|
|
87
|
+
hasLatex,
|
|
88
|
+
checkDependencies,
|
|
89
|
+
getInstallInstructions,
|
|
90
|
+
} from '../build.js';
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
getTemplate,
|
|
94
|
+
listTemplates,
|
|
95
|
+
generateCustomTemplate,
|
|
96
|
+
} from '../templates.js';
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
getUserName,
|
|
100
|
+
setUserName,
|
|
101
|
+
getConfigPath,
|
|
102
|
+
getDefaultSections,
|
|
103
|
+
setDefaultSections,
|
|
104
|
+
loadUserConfig,
|
|
105
|
+
saveUserConfig,
|
|
106
|
+
} from '../config.js';
|
|
107
|
+
|
|
108
|
+
export { inlineDiffPreview } from '../format.js';
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
parseCommentsWithReplies,
|
|
112
|
+
collectComments,
|
|
113
|
+
generateResponseLetter,
|
|
114
|
+
groupByReviewer,
|
|
115
|
+
} from '../response.js';
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
validateCitations,
|
|
119
|
+
getCitationStats,
|
|
120
|
+
} from '../citations.js';
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
extractEquations,
|
|
124
|
+
getEquationStats,
|
|
125
|
+
createEquationsDoc,
|
|
126
|
+
extractEquationsFromWord,
|
|
127
|
+
getWordEquationStats,
|
|
128
|
+
} from '../equations.js';
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
parseBibEntries,
|
|
132
|
+
checkBibDois,
|
|
133
|
+
fetchBibtex,
|
|
134
|
+
addToBib,
|
|
135
|
+
isValidDoiFormat,
|
|
136
|
+
lookupDoi,
|
|
137
|
+
lookupMissingDois,
|
|
138
|
+
clearDoiCache,
|
|
139
|
+
getDoiCacheStats,
|
|
140
|
+
} from '../doi.js';
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
formatError,
|
|
144
|
+
getFileNotFoundSuggestions,
|
|
145
|
+
getDependencySuggestions,
|
|
146
|
+
getAnnotationSuggestions,
|
|
147
|
+
getBuildSuggestions,
|
|
148
|
+
exitWithError,
|
|
149
|
+
requireFile,
|
|
150
|
+
} from '../errors.js';
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
listJournals,
|
|
154
|
+
getJournalProfile,
|
|
155
|
+
validateManuscript,
|
|
156
|
+
validateProject,
|
|
157
|
+
} from '../journals.js';
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
listCustomProfiles,
|
|
161
|
+
saveProfileTemplate,
|
|
162
|
+
getPluginDirs,
|
|
163
|
+
} from '../plugins.js';
|
|
164
|
+
|
|
165
|
+
export { tuiCommentReview } from '../tui.js';
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core commands: review, strip, status
|
|
3
|
+
*
|
|
4
|
+
* Basic annotation operations for track changes workflow.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
chalk,
|
|
9
|
+
fs,
|
|
10
|
+
path,
|
|
11
|
+
fmt,
|
|
12
|
+
quietMode,
|
|
13
|
+
jsonMode,
|
|
14
|
+
jsonOutput,
|
|
15
|
+
findFiles,
|
|
16
|
+
parseAnnotations,
|
|
17
|
+
stripAnnotations,
|
|
18
|
+
countAnnotations,
|
|
19
|
+
getComments,
|
|
20
|
+
interactiveReview,
|
|
21
|
+
exitWithError,
|
|
22
|
+
getFileNotFoundSuggestions,
|
|
23
|
+
requireFile,
|
|
24
|
+
} from './context.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register core commands with the program
|
|
28
|
+
* @param {import('commander').Command} program
|
|
29
|
+
*/
|
|
30
|
+
export function register(program) {
|
|
31
|
+
// ==========================================================================
|
|
32
|
+
// REVIEW command - Interactive track change review
|
|
33
|
+
// ==========================================================================
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('review')
|
|
37
|
+
.description('Interactively review and accept/reject track changes')
|
|
38
|
+
.argument('<file>', 'Markdown file to review')
|
|
39
|
+
.action(async (file) => {
|
|
40
|
+
requireFile(file, 'Markdown file');
|
|
41
|
+
|
|
42
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
43
|
+
const result = await interactiveReview(text);
|
|
44
|
+
|
|
45
|
+
if (result.accepted > 0 || result.rejected > 0) {
|
|
46
|
+
// Confirm save
|
|
47
|
+
const rl = await import('readline');
|
|
48
|
+
const readline = rl.createInterface({
|
|
49
|
+
input: process.stdin,
|
|
50
|
+
output: process.stdout,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
readline.question(chalk.cyan(`\nSave changes to ${file}? [y/N] `), (answer) => {
|
|
54
|
+
readline.close();
|
|
55
|
+
if (answer.toLowerCase() === 'y') {
|
|
56
|
+
fs.writeFileSync(file, result.text, 'utf-8');
|
|
57
|
+
console.log(chalk.green(`Saved ${file}`));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(chalk.yellow('Changes not saved.'));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ==========================================================================
|
|
66
|
+
// STRIP command - Remove annotations
|
|
67
|
+
// ==========================================================================
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('strip')
|
|
71
|
+
.description('Strip annotations, outputting clean Markdown')
|
|
72
|
+
.argument('<file>', 'Markdown file to strip')
|
|
73
|
+
.option('-o, --output <file>', 'Output file (default: stdout)')
|
|
74
|
+
.option('-c, --keep-comments', 'Keep comment annotations')
|
|
75
|
+
.action((file, options) => {
|
|
76
|
+
requireFile(file, 'Markdown file');
|
|
77
|
+
|
|
78
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
79
|
+
const clean = stripAnnotations(text, { keepComments: options.keepComments });
|
|
80
|
+
|
|
81
|
+
if (options.output) {
|
|
82
|
+
fs.writeFileSync(options.output, clean, 'utf-8');
|
|
83
|
+
console.error(chalk.green(`Written to ${options.output}`));
|
|
84
|
+
} else {
|
|
85
|
+
process.stdout.write(clean);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ==========================================================================
|
|
90
|
+
// STATUS command - Show annotation statistics
|
|
91
|
+
// ==========================================================================
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('status')
|
|
95
|
+
.alias('s')
|
|
96
|
+
.description('Show project overview or file annotation statistics')
|
|
97
|
+
.argument('[file]', 'Markdown file to analyze (default: project overview)')
|
|
98
|
+
.action(async (file) => {
|
|
99
|
+
// If a specific file is given, show its annotations
|
|
100
|
+
if (file) {
|
|
101
|
+
if (!fs.existsSync(file)) {
|
|
102
|
+
if (jsonMode) {
|
|
103
|
+
jsonOutput({ error: `File not found: ${file}` });
|
|
104
|
+
} else {
|
|
105
|
+
exitWithError(`File not found: ${file}`, getFileNotFoundSuggestions(file));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
110
|
+
const counts = countAnnotations(text);
|
|
111
|
+
const comments = getComments(text);
|
|
112
|
+
|
|
113
|
+
if (jsonMode) {
|
|
114
|
+
jsonOutput({
|
|
115
|
+
file: path.basename(file),
|
|
116
|
+
annotations: counts,
|
|
117
|
+
comments: comments.map(c => ({
|
|
118
|
+
author: c.author || null,
|
|
119
|
+
content: c.content,
|
|
120
|
+
line: c.line,
|
|
121
|
+
resolved: c.resolved || false,
|
|
122
|
+
})),
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (counts.total === 0) {
|
|
128
|
+
console.log(fmt.status('success', 'No annotations found.'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(fmt.header(`Annotations in ${path.basename(file)}`));
|
|
133
|
+
console.log();
|
|
134
|
+
|
|
135
|
+
// Build stats table
|
|
136
|
+
const rows = [];
|
|
137
|
+
if (counts.inserts > 0) rows.push([chalk.green('+'), 'Insertions', chalk.green(counts.inserts)]);
|
|
138
|
+
if (counts.deletes > 0) rows.push([chalk.red('-'), 'Deletions', chalk.red(counts.deletes)]);
|
|
139
|
+
if (counts.substitutes > 0) rows.push([chalk.yellow('~'), 'Substitutions', chalk.yellow(counts.substitutes)]);
|
|
140
|
+
if (counts.comments > 0) rows.push([chalk.blue('#'), 'Comments', chalk.blue(counts.comments)]);
|
|
141
|
+
rows.push([chalk.dim('Σ'), chalk.dim('Total'), chalk.dim(counts.total)]);
|
|
142
|
+
|
|
143
|
+
console.log(fmt.table(['', 'Type', 'Count'], rows, { align: ['center', 'left', 'right'] }));
|
|
144
|
+
|
|
145
|
+
// List comments with authors in a table
|
|
146
|
+
if (comments.length > 0) {
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(fmt.header('Comments'));
|
|
149
|
+
console.log();
|
|
150
|
+
|
|
151
|
+
const commentRows = comments.map((c, i) => [
|
|
152
|
+
chalk.dim(i + 1),
|
|
153
|
+
c.author ? chalk.blue(c.author) : chalk.dim('Anonymous'),
|
|
154
|
+
c.content.length > 45 ? c.content.slice(0, 45) + '...' : c.content,
|
|
155
|
+
chalk.dim(`L${c.line}`),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
console.log(fmt.table(['#', 'Author', 'Comment', 'Line'], commentRows, {
|
|
159
|
+
align: ['right', 'left', 'left', 'right'],
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Project overview mode
|
|
166
|
+
// Find all markdown files
|
|
167
|
+
const mdFiles = findFiles('.md');
|
|
168
|
+
if (mdFiles.length === 0) {
|
|
169
|
+
if (jsonMode) {
|
|
170
|
+
jsonOutput({ error: 'No markdown files found', files: [] });
|
|
171
|
+
} else {
|
|
172
|
+
console.log(fmt.status('warning', 'No markdown files found in current directory.'));
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Gather stats across all files
|
|
178
|
+
let totalWords = 0;
|
|
179
|
+
let totalComments = 0;
|
|
180
|
+
let pendingComments = 0;
|
|
181
|
+
let totalInserts = 0;
|
|
182
|
+
let totalDeletes = 0;
|
|
183
|
+
let totalSubstitutes = 0;
|
|
184
|
+
const fileStats = [];
|
|
185
|
+
|
|
186
|
+
for (const f of mdFiles) {
|
|
187
|
+
const text = fs.readFileSync(f, 'utf-8');
|
|
188
|
+
const counts = countAnnotations(text);
|
|
189
|
+
const comments = getComments(text);
|
|
190
|
+
const pending = comments.filter(c => !c.resolved).length;
|
|
191
|
+
|
|
192
|
+
// Simple word count (excluding annotations)
|
|
193
|
+
const stripped = stripAnnotations(text);
|
|
194
|
+
const words = stripped.split(/\s+/).filter(w => w.length > 0).length;
|
|
195
|
+
|
|
196
|
+
totalWords += words;
|
|
197
|
+
totalComments += comments.length;
|
|
198
|
+
pendingComments += pending;
|
|
199
|
+
totalInserts += counts.inserts;
|
|
200
|
+
totalDeletes += counts.deletes;
|
|
201
|
+
totalSubstitutes += counts.substitutes;
|
|
202
|
+
|
|
203
|
+
if (counts.total > 0 || words > 0) {
|
|
204
|
+
fileStats.push({
|
|
205
|
+
file: f,
|
|
206
|
+
words,
|
|
207
|
+
inserts: counts.inserts,
|
|
208
|
+
deletes: counts.deletes,
|
|
209
|
+
substitutions: counts.substitutes,
|
|
210
|
+
comments: comments.length,
|
|
211
|
+
pending,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// JSON output
|
|
217
|
+
if (jsonMode) {
|
|
218
|
+
const docxFiles = findFiles('.docx');
|
|
219
|
+
const latestDocx = docxFiles.length > 0
|
|
220
|
+
? docxFiles
|
|
221
|
+
.map(f => ({ name: f, mtime: fs.statSync(f).mtime }))
|
|
222
|
+
.sort((a, b) => b.mtime - a.mtime)[0]
|
|
223
|
+
: null;
|
|
224
|
+
|
|
225
|
+
jsonOutput({
|
|
226
|
+
summary: {
|
|
227
|
+
words: totalWords,
|
|
228
|
+
files: mdFiles.length,
|
|
229
|
+
comments: totalComments,
|
|
230
|
+
pendingComments,
|
|
231
|
+
insertions: totalInserts,
|
|
232
|
+
deletions: totalDeletes,
|
|
233
|
+
substitutions: totalSubstitutes,
|
|
234
|
+
},
|
|
235
|
+
files: fileStats,
|
|
236
|
+
latestDocx: latestDocx ? { name: latestDocx.name, mtime: latestDocx.mtime.toISOString() } : null,
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Normal output
|
|
242
|
+
console.log(fmt.header('Project Status'));
|
|
243
|
+
console.log();
|
|
244
|
+
|
|
245
|
+
// Summary
|
|
246
|
+
console.log(` ${chalk.bold(totalWords.toLocaleString())} words across ${mdFiles.length} files`);
|
|
247
|
+
|
|
248
|
+
if (totalComments > 0) {
|
|
249
|
+
console.log(` ${chalk.blue(totalComments)} comments (${chalk.yellow(pendingComments)} pending)`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const totalChanges = totalInserts + totalDeletes + totalSubstitutes;
|
|
253
|
+
if (totalChanges > 0) {
|
|
254
|
+
console.log(` ${chalk.green(`+${totalInserts}`)} insertions, ${chalk.red(`-${totalDeletes}`)} deletions, ${chalk.yellow(`~${totalSubstitutes}`)} substitutions`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Per-file breakdown if there are annotations
|
|
258
|
+
if (totalChanges > 0 || totalComments > 0) {
|
|
259
|
+
console.log();
|
|
260
|
+
const rows = fileStats
|
|
261
|
+
.filter(f => f.inserts + f.deletes + f.substitutions + f.comments > 0)
|
|
262
|
+
.map(f => [
|
|
263
|
+
f.file,
|
|
264
|
+
f.words.toLocaleString(),
|
|
265
|
+
f.inserts > 0 ? chalk.green(`+${f.inserts}`) : chalk.dim('-'),
|
|
266
|
+
f.deletes > 0 ? chalk.red(`-${f.deletes}`) : chalk.dim('-'),
|
|
267
|
+
f.substitutions > 0 ? chalk.yellow(`~${f.substitutions}`) : chalk.dim('-'),
|
|
268
|
+
f.pending > 0 ? chalk.yellow(f.pending) : (f.comments > 0 ? chalk.dim(f.comments) : chalk.dim('-')),
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
if (rows.length > 0) {
|
|
272
|
+
console.log(fmt.table(
|
|
273
|
+
['File', 'Words', 'Ins', 'Del', 'Sub', 'Cmt'],
|
|
274
|
+
rows,
|
|
275
|
+
{ align: ['left', 'right', 'right', 'right', 'right', 'right'] }
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for recent docx files
|
|
281
|
+
const docxFiles = findFiles('.docx');
|
|
282
|
+
if (docxFiles.length > 0) {
|
|
283
|
+
const sorted = docxFiles
|
|
284
|
+
.map(f => ({ name: f, mtime: fs.statSync(f).mtime }))
|
|
285
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
286
|
+
const latest = sorted[0];
|
|
287
|
+
const age = Date.now() - latest.mtime.getTime();
|
|
288
|
+
const ageStr = age < 3600000 ? `${Math.round(age / 60000)}m ago` :
|
|
289
|
+
age < 86400000 ? `${Math.round(age / 3600000)}h ago` :
|
|
290
|
+
`${Math.round(age / 86400000)}d ago`;
|
|
291
|
+
console.log();
|
|
292
|
+
console.log(chalk.dim(` Latest DOCX: ${latest.name} (${ageStr})`));
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|