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
package/lib/orcid.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORCID integration for author metadata
|
|
3
|
+
*
|
|
4
|
+
* Fetches author information from ORCID public API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate ORCID format (0000-0000-0000-0000)
|
|
9
|
+
* @param {string} orcid
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isValidOrcid(orcid) {
|
|
13
|
+
return /^(\d{4}-){3}\d{3}[\dX]$/i.test(orcid);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clean ORCID input (removes URLs, whitespace)
|
|
18
|
+
* @param {string} input
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function cleanOrcid(input) {
|
|
22
|
+
if (!input) return '';
|
|
23
|
+
|
|
24
|
+
// Remove URL prefix if present
|
|
25
|
+
let clean = input.trim()
|
|
26
|
+
.replace(/^https?:\/\/(www\.)?orcid\.org\//i, '')
|
|
27
|
+
.replace(/^orcid\.org\//i, '')
|
|
28
|
+
.trim();
|
|
29
|
+
|
|
30
|
+
return clean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch author info from ORCID public API
|
|
35
|
+
* @param {string} orcid
|
|
36
|
+
* @returns {Promise<{name: string, affiliation: string, email: string, works: number}>}
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchOrcidProfile(orcid) {
|
|
39
|
+
const cleanId = cleanOrcid(orcid);
|
|
40
|
+
|
|
41
|
+
if (!isValidOrcid(cleanId)) {
|
|
42
|
+
throw new Error(`Invalid ORCID format: ${orcid}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const url = `https://pub.orcid.org/v3.0/${cleanId}/person`;
|
|
46
|
+
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
headers: {
|
|
49
|
+
'Accept': 'application/json',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
if (response.status === 404) {
|
|
55
|
+
throw new Error(`ORCID not found: ${cleanId}`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`ORCID API error: ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
|
|
62
|
+
// Extract name
|
|
63
|
+
const nameData = data.name;
|
|
64
|
+
let name = '';
|
|
65
|
+
if (nameData) {
|
|
66
|
+
const given = nameData['given-names']?.value || '';
|
|
67
|
+
const family = nameData['family-name']?.value || '';
|
|
68
|
+
name = `${given} ${family}`.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract primary affiliation
|
|
72
|
+
let affiliation = '';
|
|
73
|
+
const affiliations = data.employments?.['affiliation-group'] || [];
|
|
74
|
+
if (affiliations.length > 0) {
|
|
75
|
+
const primary = affiliations[0]?.summaries?.[0]?.['employment-summary'];
|
|
76
|
+
affiliation = primary?.organization?.name || '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract email (if public)
|
|
80
|
+
let email = '';
|
|
81
|
+
const emails = data.emails?.email || [];
|
|
82
|
+
const primaryEmail = emails.find(e => e.primary) || emails[0];
|
|
83
|
+
if (primaryEmail?.email) {
|
|
84
|
+
email = primaryEmail.email;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
orcid: cleanId,
|
|
89
|
+
name,
|
|
90
|
+
affiliation,
|
|
91
|
+
email,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch work count from ORCID
|
|
97
|
+
* @param {string} orcid
|
|
98
|
+
* @returns {Promise<number>}
|
|
99
|
+
*/
|
|
100
|
+
export async function fetchOrcidWorkCount(orcid) {
|
|
101
|
+
const cleanId = cleanOrcid(orcid);
|
|
102
|
+
|
|
103
|
+
if (!isValidOrcid(cleanId)) {
|
|
104
|
+
throw new Error(`Invalid ORCID format: ${orcid}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const url = `https://pub.orcid.org/v3.0/${cleanId}/works`;
|
|
108
|
+
|
|
109
|
+
const response = await fetch(url, {
|
|
110
|
+
headers: {
|
|
111
|
+
'Accept': 'application/json',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
return data.group?.length || 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format author for YAML
|
|
125
|
+
* @param {object} profile
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function formatAuthorYaml(profile) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
lines.push(` - name: ${profile.name}`);
|
|
131
|
+
if (profile.affiliation) {
|
|
132
|
+
lines.push(` affiliation: ${profile.affiliation}`);
|
|
133
|
+
}
|
|
134
|
+
if (profile.email) {
|
|
135
|
+
lines.push(` email: ${profile.email}`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(` orcid: ${profile.orcid}`);
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate ORCID badge markdown
|
|
143
|
+
* @param {string} orcid
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
export function getOrcidBadge(orcid) {
|
|
147
|
+
const cleanId = cleanOrcid(orcid);
|
|
148
|
+
return `[](https://orcid.org/${cleanId})`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF comment rendering for dual export
|
|
3
|
+
*
|
|
4
|
+
* Converts CriticMarkup comments to LaTeX margin notes for PDF output
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* LaTeX preamble for margin comments
|
|
9
|
+
* Uses todonotes package with custom styling
|
|
10
|
+
*/
|
|
11
|
+
export const MARGIN_NOTES_PREAMBLE = `
|
|
12
|
+
% Margin notes for comments
|
|
13
|
+
\\usepackage[colorinlistoftodos,textsize=scriptsize]{todonotes}
|
|
14
|
+
\\usepackage{xcolor}
|
|
15
|
+
|
|
16
|
+
% Define comment colors by author
|
|
17
|
+
\\definecolor{commentblue}{RGB}{59, 130, 246}
|
|
18
|
+
\\definecolor{commentgreen}{RGB}{34, 197, 94}
|
|
19
|
+
\\definecolor{commentorange}{RGB}{249, 115, 22}
|
|
20
|
+
\\definecolor{commentpurple}{RGB}{168, 85, 247}
|
|
21
|
+
\\definecolor{commentgray}{RGB}{107, 114, 128}
|
|
22
|
+
|
|
23
|
+
% Custom margin note command
|
|
24
|
+
\\newcommand{\\margincomment}[2][]{%
|
|
25
|
+
\\todo[linecolor=commentblue,backgroundcolor=commentblue!10,bordercolor=commentblue,size=\\scriptsize,#1]{#2}%
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
% Author-specific commands
|
|
29
|
+
\\newcommand{\\reviewercomment}[2]{%
|
|
30
|
+
\\todo[linecolor=commentgreen,backgroundcolor=commentgreen!10,bordercolor=commentgreen,size=\\scriptsize]{\\textbf{#1:} #2}%
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
% Increase margin for notes (if needed)
|
|
34
|
+
% \\setlength{\\marginparwidth}{2.5cm}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simpler preamble using marginpar (no extra packages needed)
|
|
39
|
+
*/
|
|
40
|
+
export const SIMPLE_MARGIN_PREAMBLE = `
|
|
41
|
+
% Simple margin notes for comments
|
|
42
|
+
\\usepackage{xcolor}
|
|
43
|
+
\\definecolor{commentcolor}{RGB}{59, 130, 246}
|
|
44
|
+
|
|
45
|
+
\\newcommand{\\margincomment}[1]{%
|
|
46
|
+
\\marginpar{\\raggedright\\scriptsize\\textcolor{commentcolor}{#1}}%
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert CriticMarkup comments to LaTeX margin notes
|
|
52
|
+
* {>>Author: comment text<<} -> \margincomment{Author: comment text}
|
|
53
|
+
*
|
|
54
|
+
* @param {string} markdown - Markdown with CriticMarkup comments
|
|
55
|
+
* @param {object} options - { useTodonotes: boolean, stripResolved: boolean }
|
|
56
|
+
* @returns {{markdown: string, commentCount: number, preamble: string}}
|
|
57
|
+
*/
|
|
58
|
+
export function convertCommentsToMarginNotes(markdown, options = {}) {
|
|
59
|
+
const { useTodonotes = true, stripResolved = true } = options;
|
|
60
|
+
|
|
61
|
+
let commentCount = 0;
|
|
62
|
+
|
|
63
|
+
// Pattern for CriticMarkup comments: {>>author: text<<} or {>>text<<}
|
|
64
|
+
// Also handle resolved comments: {>>✓ author: text<<}
|
|
65
|
+
const commentPattern = /\{>>(✓\s*)?([^<]+)<<\}/g;
|
|
66
|
+
|
|
67
|
+
const converted = markdown.replace(commentPattern, (match, resolved, content) => {
|
|
68
|
+
// Skip resolved comments if requested
|
|
69
|
+
if (resolved && stripResolved) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
commentCount++;
|
|
74
|
+
|
|
75
|
+
// Escape LaTeX special characters
|
|
76
|
+
const escaped = escapeLatex(content.trim());
|
|
77
|
+
|
|
78
|
+
if (useTodonotes) {
|
|
79
|
+
// Check if content has author prefix (Author: text)
|
|
80
|
+
const authorMatch = escaped.match(/^([^:]+):\s*(.+)$/s);
|
|
81
|
+
if (authorMatch) {
|
|
82
|
+
const [, author, text] = authorMatch;
|
|
83
|
+
return `\\reviewercomment{${author}}{${text}}`;
|
|
84
|
+
}
|
|
85
|
+
return `\\margincomment{${escaped}}`;
|
|
86
|
+
} else {
|
|
87
|
+
return `\\margincomment{${escaped}}`;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const preamble = useTodonotes ? MARGIN_NOTES_PREAMBLE : SIMPLE_MARGIN_PREAMBLE;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
markdown: converted,
|
|
95
|
+
commentCount,
|
|
96
|
+
preamble,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Escape LaTeX special characters
|
|
102
|
+
* @param {string} text
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
function escapeLatex(text) {
|
|
106
|
+
return text
|
|
107
|
+
.replace(/\\/g, '\\textbackslash{}')
|
|
108
|
+
.replace(/([#$%&_{}])/g, '\\$1')
|
|
109
|
+
.replace(/\^/g, '\\textasciicircum{}')
|
|
110
|
+
.replace(/~/g, '\\textasciitilde{}')
|
|
111
|
+
.replace(/\n/g, ' '); // Replace newlines with spaces
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convert track changes to visible LaTeX formatting
|
|
116
|
+
* {++inserted++} -> \textcolor{green}{inserted}
|
|
117
|
+
* {--deleted--} -> \textcolor{red}{\sout{deleted}}
|
|
118
|
+
* {~~old~>new~~} -> \textcolor{red}{\sout{old}}\textcolor{green}{new}
|
|
119
|
+
*
|
|
120
|
+
* @param {string} markdown
|
|
121
|
+
* @returns {{markdown: string, preamble: string}}
|
|
122
|
+
*/
|
|
123
|
+
export function convertTrackChangesToLatex(markdown) {
|
|
124
|
+
let result = markdown;
|
|
125
|
+
|
|
126
|
+
// Insertions: {++text++} -> green text
|
|
127
|
+
result = result.replace(/\{\+\+([^+]+)\+\+\}/g, (match, text) => {
|
|
128
|
+
return `\\textcolor{green}{${escapeLatex(text)}}`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Deletions: {--text--} -> red strikethrough
|
|
132
|
+
result = result.replace(/\{--([^-]+)--\}/g, (match, text) => {
|
|
133
|
+
return `\\textcolor{red}{\\sout{${escapeLatex(text)}}}`;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Substitutions: {~~old~>new~~} -> red strikethrough + green new
|
|
137
|
+
result = result.replace(/\{~~([^~]+)~>([^~]+)~~\}/g, (match, oldText, newText) => {
|
|
138
|
+
return `\\textcolor{red}{\\sout{${escapeLatex(oldText)}}}\\textcolor{green}{${escapeLatex(newText)}}`;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const preamble = `
|
|
142
|
+
% Track changes visualization
|
|
143
|
+
\\usepackage{xcolor}
|
|
144
|
+
\\usepackage[normalem]{ulem}
|
|
145
|
+
\\definecolor{green}{RGB}{34, 197, 94}
|
|
146
|
+
\\definecolor{red}{RGB}{239, 68, 68}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
return { markdown: result, preamble };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get combined preamble for comments and track changes
|
|
154
|
+
* @param {object} options - { comments: boolean, trackChanges: boolean, useTodonotes: boolean }
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
export function getCombinedPreamble(options = {}) {
|
|
158
|
+
const { comments = true, trackChanges = false, useTodonotes = true } = options;
|
|
159
|
+
|
|
160
|
+
let preamble = '';
|
|
161
|
+
|
|
162
|
+
if (comments) {
|
|
163
|
+
preamble += useTodonotes ? MARGIN_NOTES_PREAMBLE : SIMPLE_MARGIN_PREAMBLE;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (trackChanges) {
|
|
167
|
+
preamble += `
|
|
168
|
+
% Track changes visualization
|
|
169
|
+
\\usepackage[normalem]{ulem}
|
|
170
|
+
`;
|
|
171
|
+
if (!comments) {
|
|
172
|
+
preamble += `\\usepackage{xcolor}\n`;
|
|
173
|
+
}
|
|
174
|
+
preamble += `
|
|
175
|
+
\\definecolor{insertgreen}{RGB}{34, 197, 94}
|
|
176
|
+
\\definecolor{deletered}{RGB}{239, 68, 68}
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return preamble;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Prepare markdown for PDF with visible comments
|
|
185
|
+
* Converts comments to margin notes and optionally shows track changes
|
|
186
|
+
*
|
|
187
|
+
* @param {string} markdown
|
|
188
|
+
* @param {object} options - { showTrackChanges: boolean, useTodonotes: boolean }
|
|
189
|
+
* @returns {{markdown: string, preamble: string, commentCount: number}}
|
|
190
|
+
*/
|
|
191
|
+
export function prepareMarkdownForAnnotatedPdf(markdown, options = {}) {
|
|
192
|
+
const { showTrackChanges = false, useTodonotes = true, stripResolved = true } = options;
|
|
193
|
+
|
|
194
|
+
let result = markdown;
|
|
195
|
+
let preamble = '';
|
|
196
|
+
let commentCount = 0;
|
|
197
|
+
|
|
198
|
+
// Convert comments to margin notes
|
|
199
|
+
const commentResult = convertCommentsToMarginNotes(result, { useTodonotes, stripResolved });
|
|
200
|
+
result = commentResult.markdown;
|
|
201
|
+
commentCount = commentResult.commentCount;
|
|
202
|
+
preamble += commentResult.preamble;
|
|
203
|
+
|
|
204
|
+
// Optionally show track changes
|
|
205
|
+
if (showTrackChanges) {
|
|
206
|
+
const trackResult = convertTrackChangesToLatex(result);
|
|
207
|
+
result = trackResult.markdown;
|
|
208
|
+
// Add ulem package if not already in todonotes preamble
|
|
209
|
+
if (!useTodonotes) {
|
|
210
|
+
preamble += trackResult.preamble;
|
|
211
|
+
} else {
|
|
212
|
+
preamble += `\\usepackage[normalem]{ulem}\n`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { markdown: result, preamble, commentCount };
|
|
217
|
+
}
|