docrev 0.9.13 → 0.9.15
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 -9
- package/.gitattributes +1 -1
- package/CHANGELOG.md +149 -149
- package/PLAN-tables-and-postprocess.md +850 -850
- package/README.md +411 -391
- package/bin/rev.js +11 -11
- package/bin/rev.ts +145 -145
- package/completions/rev.bash +127 -127
- package/completions/rev.ps1 +210 -210
- package/completions/rev.zsh +207 -207
- package/dev_notes/stress2/build_adversarial.ts +186 -186
- package/dev_notes/stress2/drift_matcher.ts +62 -62
- package/dev_notes/stress2/probe_anchors.ts +35 -35
- package/dev_notes/stress2/project/discussion.before.md +3 -3
- package/dev_notes/stress2/project/discussion.md +3 -3
- package/dev_notes/stress2/project/methods.before.md +20 -20
- package/dev_notes/stress2/project/methods.md +20 -20
- package/dev_notes/stress2/project/rev.yaml +5 -5
- package/dev_notes/stress2/project/sections.yaml +4 -4
- package/dev_notes/stress2/sections.yaml +5 -5
- package/dev_notes/stress2/trace_placement.ts +50 -50
- package/dev_notes/stresstest_boundaries.ts +27 -27
- package/dev_notes/stresstest_drift_apply.ts +43 -43
- package/dev_notes/stresstest_drift_compare.ts +43 -43
- package/dev_notes/stresstest_drift_v2.ts +54 -54
- package/dev_notes/stresstest_inspect.ts +54 -54
- package/dev_notes/stresstest_pstyle.ts +55 -55
- package/dev_notes/stresstest_section_debug.ts +23 -23
- package/dev_notes/stresstest_split.ts +70 -70
- package/dev_notes/stresstest_trace.ts +19 -19
- package/dev_notes/stresstest_verify_no_overwrite.ts +40 -40
- package/dist/lib/build.d.ts +38 -1
- package/dist/lib/build.d.ts.map +1 -1
- package/dist/lib/build.js +68 -30
- package/dist/lib/build.js.map +1 -1
- package/dist/lib/commands/build.d.ts.map +1 -1
- package/dist/lib/commands/build.js +38 -5
- package/dist/lib/commands/build.js.map +1 -1
- package/dist/lib/commands/utilities.js +164 -164
- package/dist/lib/commands/word-tools.js +8 -8
- package/dist/lib/grammar.js +3 -3
- package/dist/lib/pdf-comments.js +44 -44
- package/dist/lib/plugins.js +57 -57
- package/dist/lib/pptx-themes.js +115 -115
- package/dist/lib/spelling.js +2 -2
- package/dist/lib/templates.js +387 -387
- package/dist/lib/themes.js +51 -51
- package/eslint.config.js +27 -27
- package/lib/anchor-match.ts +276 -276
- package/lib/annotations.ts +644 -644
- package/lib/build.ts +1300 -1251
- package/lib/citations.ts +160 -160
- package/lib/commands/build.ts +833 -801
- package/lib/commands/citations.ts +515 -515
- package/lib/commands/comments.ts +1050 -1050
- package/lib/commands/context.ts +174 -174
- package/lib/commands/core.ts +309 -309
- package/lib/commands/doi.ts +435 -435
- package/lib/commands/file-ops.ts +372 -372
- package/lib/commands/history.ts +320 -320
- package/lib/commands/index.ts +87 -87
- package/lib/commands/init.ts +259 -259
- package/lib/commands/merge-resolve.ts +378 -378
- package/lib/commands/preview.ts +178 -178
- package/lib/commands/project-info.ts +244 -244
- package/lib/commands/quality.ts +517 -517
- package/lib/commands/response.ts +454 -454
- package/lib/commands/section-boundaries.ts +82 -82
- package/lib/commands/sections.ts +451 -451
- package/lib/commands/sync.ts +706 -706
- package/lib/commands/text-ops.ts +449 -449
- package/lib/commands/utilities.ts +448 -448
- package/lib/commands/verify-anchors.ts +272 -272
- package/lib/commands/word-tools.ts +340 -340
- package/lib/comment-realign.ts +517 -517
- package/lib/config.ts +84 -84
- package/lib/crossref.ts +781 -781
- package/lib/csl.ts +191 -191
- package/lib/dependencies.ts +98 -98
- package/lib/diff-engine.ts +465 -465
- package/lib/doi-cache.ts +115 -115
- package/lib/doi.ts +897 -897
- package/lib/equations.ts +506 -506
- package/lib/errors.ts +346 -346
- package/lib/format.ts +541 -541
- package/lib/git.ts +326 -326
- package/lib/grammar.ts +303 -303
- package/lib/image-registry.ts +180 -180
- package/lib/import.ts +911 -911
- package/lib/journals.ts +543 -543
- package/lib/merge.ts +633 -633
- package/lib/orcid.ts +144 -144
- package/lib/pdf-comments.ts +263 -263
- package/lib/pdf-import.ts +524 -524
- package/lib/plugins.ts +362 -362
- package/lib/postprocess.ts +188 -188
- package/lib/pptx-color-filter.lua +37 -37
- package/lib/pptx-template.ts +469 -469
- package/lib/pptx-themes.ts +483 -483
- package/lib/protect-restore.ts +520 -520
- package/lib/rate-limiter.ts +94 -94
- package/lib/response.ts +197 -197
- package/lib/restore-references.ts +240 -240
- package/lib/review.ts +327 -327
- package/lib/schema.ts +417 -417
- package/lib/scientific-words.ts +73 -73
- package/lib/sections.ts +335 -335
- package/lib/slides.ts +756 -756
- package/lib/spelling.ts +334 -334
- package/lib/templates.ts +526 -526
- package/lib/themes.ts +742 -742
- package/lib/trackchanges.ts +247 -247
- package/lib/tui.ts +450 -450
- package/lib/types.ts +550 -550
- package/lib/undo.ts +250 -250
- package/lib/utils.ts +69 -69
- package/lib/variables.ts +179 -179
- package/lib/word-extraction.ts +806 -806
- package/lib/word.ts +643 -643
- package/lib/wordcomments.ts +817 -817
- package/package.json +137 -137
- package/scripts/postbuild.js +28 -28
- package/skill/REFERENCE.md +473 -431
- package/skill/SKILL.md +274 -258
- package/tsconfig.json +26 -26
- package/types/index.d.ts +525 -525
package/lib/plugins.ts
CHANGED
|
@@ -1,362 +1,362 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin system for custom journal profiles and export formats
|
|
3
|
-
*
|
|
4
|
-
* Users can add custom profiles in:
|
|
5
|
-
* - Project: .rev/profiles/*.yaml
|
|
6
|
-
* - User: ~/.rev/profiles/*.yaml
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as fs from 'fs';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as os from 'os';
|
|
12
|
-
import * as yaml from 'yaml';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Journal profile requirements
|
|
16
|
-
*/
|
|
17
|
-
interface ProfileRequirements {
|
|
18
|
-
wordLimit?: Record<string, number | null>;
|
|
19
|
-
references?: Record<string, unknown>;
|
|
20
|
-
figures?: Record<string, unknown>;
|
|
21
|
-
sections?: Record<string, unknown>;
|
|
22
|
-
authors?: Record<string, unknown>;
|
|
23
|
-
keywords?: { min?: number; max?: number } | null;
|
|
24
|
-
dataAvailability?: boolean;
|
|
25
|
-
[key: string]: unknown;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Journal profile
|
|
30
|
-
*/
|
|
31
|
-
interface Profile {
|
|
32
|
-
id?: string;
|
|
33
|
-
name: string;
|
|
34
|
-
url?: string | null;
|
|
35
|
-
custom?: boolean;
|
|
36
|
-
requirements?: ProfileRequirements;
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Journal formatting defaults
|
|
42
|
-
*/
|
|
43
|
-
interface ProfileFormatting {
|
|
44
|
-
csl?: string;
|
|
45
|
-
pdf?: Record<string, unknown>;
|
|
46
|
-
docx?: Record<string, unknown>;
|
|
47
|
-
crossref?: Record<string, unknown>;
|
|
48
|
-
[key: string]: unknown;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Normalized profile
|
|
53
|
-
*/
|
|
54
|
-
interface NormalizedProfile {
|
|
55
|
-
name: string;
|
|
56
|
-
url: string | null;
|
|
57
|
-
custom: boolean;
|
|
58
|
-
requirements: ProfileRequirements;
|
|
59
|
-
formatting?: ProfileFormatting;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Profile list entry
|
|
64
|
-
*/
|
|
65
|
-
interface ProfileListEntry {
|
|
66
|
-
id: string;
|
|
67
|
-
name: string;
|
|
68
|
-
source: 'user' | 'project';
|
|
69
|
-
path: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Plugin directories info
|
|
74
|
-
*/
|
|
75
|
-
interface PluginDirsInfo {
|
|
76
|
-
user: string;
|
|
77
|
-
project: string;
|
|
78
|
-
userExists: boolean;
|
|
79
|
-
projectExists: boolean;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Plugin directories
|
|
83
|
-
const USER_PLUGINS_DIR = path.join(os.homedir(), '.rev', 'profiles');
|
|
84
|
-
const PROJECT_PLUGINS_DIR = path.join(process.cwd(), '.rev', 'profiles');
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Load all custom journal profiles
|
|
88
|
-
*/
|
|
89
|
-
export function loadCustomProfiles(): Record<string, NormalizedProfile> {
|
|
90
|
-
const profiles: Record<string, NormalizedProfile> = {};
|
|
91
|
-
|
|
92
|
-
// Load user profiles first (lower priority)
|
|
93
|
-
const userProfiles = loadProfilesFromDir(USER_PLUGINS_DIR);
|
|
94
|
-
Object.assign(profiles, userProfiles);
|
|
95
|
-
|
|
96
|
-
// Load project profiles (higher priority, can override)
|
|
97
|
-
const projectProfiles = loadProfilesFromDir(PROJECT_PLUGINS_DIR);
|
|
98
|
-
Object.assign(profiles, projectProfiles);
|
|
99
|
-
|
|
100
|
-
return profiles;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Load profiles from a directory
|
|
105
|
-
*/
|
|
106
|
-
function loadProfilesFromDir(dir: string): Record<string, NormalizedProfile> {
|
|
107
|
-
const profiles: Record<string, NormalizedProfile> = {};
|
|
108
|
-
|
|
109
|
-
if (!fs.existsSync(dir)) {
|
|
110
|
-
return profiles;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const files = fs.readdirSync(dir).filter(f =>
|
|
115
|
-
f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json')
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
for (const file of files) {
|
|
119
|
-
try {
|
|
120
|
-
const filePath = path.join(dir, file);
|
|
121
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
122
|
-
const profile = file.endsWith('.json')
|
|
123
|
-
? JSON.parse(content)
|
|
124
|
-
: yaml.parse(content);
|
|
125
|
-
|
|
126
|
-
if (validateProfile(profile)) {
|
|
127
|
-
const id = profile.id || path.basename(file, path.extname(file));
|
|
128
|
-
profiles[id] = normalizeProfile(profile);
|
|
129
|
-
}
|
|
130
|
-
} catch (err: unknown) {
|
|
131
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
-
console.error(`Warning: Failed to load profile ${file}: ${message}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} catch {
|
|
136
|
-
// Directory not readable
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return profiles;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Validate a profile structure
|
|
144
|
-
*/
|
|
145
|
-
function validateProfile(profile: unknown): profile is Profile {
|
|
146
|
-
if (!profile || typeof profile !== 'object') {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const p = profile as Profile;
|
|
151
|
-
|
|
152
|
-
// Must have a name
|
|
153
|
-
if (!p.name || typeof p.name !== 'string') {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Requirements must be an object if present
|
|
158
|
-
if (p.requirements && typeof p.requirements !== 'object') {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Normalize profile to standard structure
|
|
167
|
-
*/
|
|
168
|
-
function normalizeProfile(profile: Profile): NormalizedProfile {
|
|
169
|
-
const normalized: NormalizedProfile = {
|
|
170
|
-
name: profile.name,
|
|
171
|
-
url: profile.url || null,
|
|
172
|
-
custom: true,
|
|
173
|
-
requirements: {
|
|
174
|
-
wordLimit: profile.requirements?.wordLimit || (profile as { wordLimit?: Record<string, number> }).wordLimit || {},
|
|
175
|
-
references: profile.requirements?.references || (profile as { references?: Record<string, unknown> }).references || {},
|
|
176
|
-
figures: profile.requirements?.figures || (profile as { figures?: Record<string, unknown> }).figures || {},
|
|
177
|
-
sections: profile.requirements?.sections || (profile as { sections?: Record<string, unknown> }).sections || {},
|
|
178
|
-
authors: profile.requirements?.authors || (profile as { authors?: Record<string, unknown> }).authors || {},
|
|
179
|
-
keywords: profile.requirements?.keywords || (profile as { keywords?: { min?: number; max?: number } }).keywords || null,
|
|
180
|
-
dataAvailability: profile.requirements?.dataAvailability || (profile as { dataAvailability?: boolean }).dataAvailability || false,
|
|
181
|
-
...profile.requirements,
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// Pass through formatting if present
|
|
186
|
-
const formatting = (profile as { formatting?: ProfileFormatting }).formatting;
|
|
187
|
-
if (formatting && typeof formatting === 'object') {
|
|
188
|
-
normalized.formatting = formatting;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return normalized;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Initialize plugin directories
|
|
196
|
-
*/
|
|
197
|
-
export function initPluginDir(project = false): string {
|
|
198
|
-
const dir = project ? PROJECT_PLUGINS_DIR : USER_PLUGINS_DIR;
|
|
199
|
-
|
|
200
|
-
if (!fs.existsSync(dir)) {
|
|
201
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return dir;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Get plugin directories info
|
|
209
|
-
*/
|
|
210
|
-
export function getPluginDirs(): PluginDirsInfo {
|
|
211
|
-
return {
|
|
212
|
-
user: USER_PLUGINS_DIR,
|
|
213
|
-
project: PROJECT_PLUGINS_DIR,
|
|
214
|
-
userExists: fs.existsSync(USER_PLUGINS_DIR),
|
|
215
|
-
projectExists: fs.existsSync(PROJECT_PLUGINS_DIR),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Create a sample profile template
|
|
221
|
-
*/
|
|
222
|
-
export function createProfileTemplate(journalName: string): string {
|
|
223
|
-
const id = journalName.toLowerCase().replace(/\s+/g, '-');
|
|
224
|
-
|
|
225
|
-
return `# Custom journal profile for ${journalName}
|
|
226
|
-
# Save as: ~/.rev/profiles/${id}.yaml (user-wide)
|
|
227
|
-
# Or: .rev/profiles/${id}.yaml (project-specific)
|
|
228
|
-
|
|
229
|
-
id: ${id}
|
|
230
|
-
name: "${journalName}"
|
|
231
|
-
url: "https://journal-website.com/author-guidelines"
|
|
232
|
-
|
|
233
|
-
# Word count limits
|
|
234
|
-
wordLimit:
|
|
235
|
-
main: 8000 # null for no limit
|
|
236
|
-
abstract: 300
|
|
237
|
-
title: null # characters
|
|
238
|
-
|
|
239
|
-
# Reference requirements
|
|
240
|
-
references:
|
|
241
|
-
max: null # null for no limit
|
|
242
|
-
doiRequired: true
|
|
243
|
-
|
|
244
|
-
# Figure/table limits
|
|
245
|
-
figures:
|
|
246
|
-
max: 8
|
|
247
|
-
combinedWithTables: false
|
|
248
|
-
|
|
249
|
-
# Required sections
|
|
250
|
-
sections:
|
|
251
|
-
required:
|
|
252
|
-
- Abstract
|
|
253
|
-
- Introduction
|
|
254
|
-
- Methods
|
|
255
|
-
- Results
|
|
256
|
-
- Discussion
|
|
257
|
-
methodsPosition: null # 'end' or 'before-results'
|
|
258
|
-
|
|
259
|
-
# Keywords
|
|
260
|
-
keywords:
|
|
261
|
-
min: 4
|
|
262
|
-
max: 8
|
|
263
|
-
|
|
264
|
-
# Other requirements
|
|
265
|
-
dataAvailability: true
|
|
266
|
-
highlights: false
|
|
267
|
-
graphicalAbstract: false
|
|
268
|
-
|
|
269
|
-
# Build formatting defaults (applied via rev build -j ${id})
|
|
270
|
-
# formatting:
|
|
271
|
-
# csl: "${id}" # CSL style name or path
|
|
272
|
-
# pdf:
|
|
273
|
-
# fontsize: 12pt
|
|
274
|
-
# geometry: margin=1in
|
|
275
|
-
# linestretch: 1.5
|
|
276
|
-
# numbersections: false
|
|
277
|
-
# docx:
|
|
278
|
-
# reference: null # Path to reference .docx template
|
|
279
|
-
# crossref:
|
|
280
|
-
# figPrefix: [Fig., Figs.]
|
|
281
|
-
# tblPrefix: [Table, Tables]
|
|
282
|
-
`;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Save a profile template
|
|
287
|
-
*/
|
|
288
|
-
export function saveProfileTemplate(journalName: string, project = false): string {
|
|
289
|
-
const dir = initPluginDir(project);
|
|
290
|
-
const id = journalName.toLowerCase().replace(/\s+/g, '-');
|
|
291
|
-
const filePath = path.join(dir, `${id}.yaml`);
|
|
292
|
-
|
|
293
|
-
if (fs.existsSync(filePath)) {
|
|
294
|
-
throw new Error(`Profile already exists: ${filePath}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const content = createProfileTemplate(journalName);
|
|
298
|
-
fs.writeFileSync(filePath, content, 'utf-8');
|
|
299
|
-
|
|
300
|
-
return filePath;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* List all custom profiles
|
|
305
|
-
*/
|
|
306
|
-
export function listCustomProfiles(): ProfileListEntry[] {
|
|
307
|
-
const result: ProfileListEntry[] = [];
|
|
308
|
-
|
|
309
|
-
// User profiles
|
|
310
|
-
if (fs.existsSync(USER_PLUGINS_DIR)) {
|
|
311
|
-
const files = fs.readdirSync(USER_PLUGINS_DIR).filter(f =>
|
|
312
|
-
f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json')
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
for (const file of files) {
|
|
316
|
-
try {
|
|
317
|
-
const filePath = path.join(USER_PLUGINS_DIR, file);
|
|
318
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
319
|
-
const profile = file.endsWith('.json') ? JSON.parse(content) : yaml.parse(content);
|
|
320
|
-
|
|
321
|
-
if (validateProfile(profile)) {
|
|
322
|
-
result.push({
|
|
323
|
-
id: profile.id || path.basename(file, path.extname(file)),
|
|
324
|
-
name: profile.name,
|
|
325
|
-
source: 'user',
|
|
326
|
-
path: filePath,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
} catch {
|
|
330
|
-
// Skip invalid profiles
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Project profiles
|
|
336
|
-
if (fs.existsSync(PROJECT_PLUGINS_DIR)) {
|
|
337
|
-
const files = fs.readdirSync(PROJECT_PLUGINS_DIR).filter(f =>
|
|
338
|
-
f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json')
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
for (const file of files) {
|
|
342
|
-
try {
|
|
343
|
-
const filePath = path.join(PROJECT_PLUGINS_DIR, file);
|
|
344
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
345
|
-
const profile = file.endsWith('.json') ? JSON.parse(content) : yaml.parse(content);
|
|
346
|
-
|
|
347
|
-
if (validateProfile(profile)) {
|
|
348
|
-
result.push({
|
|
349
|
-
id: profile.id || path.basename(file, path.extname(file)),
|
|
350
|
-
name: profile.name,
|
|
351
|
-
source: 'project',
|
|
352
|
-
path: filePath,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
} catch {
|
|
356
|
-
// Skip invalid profiles
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return result;
|
|
362
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Plugin system for custom journal profiles and export formats
|
|
3
|
+
*
|
|
4
|
+
* Users can add custom profiles in:
|
|
5
|
+
* - Project: .rev/profiles/*.yaml
|
|
6
|
+
* - User: ~/.rev/profiles/*.yaml
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as yaml from 'yaml';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Journal profile requirements
|
|
16
|
+
*/
|
|
17
|
+
interface ProfileRequirements {
|
|
18
|
+
wordLimit?: Record<string, number | null>;
|
|
19
|
+
references?: Record<string, unknown>;
|
|
20
|
+
figures?: Record<string, unknown>;
|
|
21
|
+
sections?: Record<string, unknown>;
|
|
22
|
+
authors?: Record<string, unknown>;
|
|
23
|
+
keywords?: { min?: number; max?: number } | null;
|
|
24
|
+
dataAvailability?: boolean;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Journal profile
|
|
30
|
+
*/
|
|
31
|
+
interface Profile {
|
|
32
|
+
id?: string;
|
|
33
|
+
name: string;
|
|
34
|
+
url?: string | null;
|
|
35
|
+
custom?: boolean;
|
|
36
|
+
requirements?: ProfileRequirements;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Journal formatting defaults
|
|
42
|
+
*/
|
|
43
|
+
interface ProfileFormatting {
|
|
44
|
+
csl?: string;
|
|
45
|
+
pdf?: Record<string, unknown>;
|
|
46
|
+
docx?: Record<string, unknown>;
|
|
47
|
+
crossref?: Record<string, unknown>;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalized profile
|
|
53
|
+
*/
|
|
54
|
+
interface NormalizedProfile {
|
|
55
|
+
name: string;
|
|
56
|
+
url: string | null;
|
|
57
|
+
custom: boolean;
|
|
58
|
+
requirements: ProfileRequirements;
|
|
59
|
+
formatting?: ProfileFormatting;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Profile list entry
|
|
64
|
+
*/
|
|
65
|
+
interface ProfileListEntry {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
source: 'user' | 'project';
|
|
69
|
+
path: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Plugin directories info
|
|
74
|
+
*/
|
|
75
|
+
interface PluginDirsInfo {
|
|
76
|
+
user: string;
|
|
77
|
+
project: string;
|
|
78
|
+
userExists: boolean;
|
|
79
|
+
projectExists: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Plugin directories
|
|
83
|
+
const USER_PLUGINS_DIR = path.join(os.homedir(), '.rev', 'profiles');
|
|
84
|
+
const PROJECT_PLUGINS_DIR = path.join(process.cwd(), '.rev', 'profiles');
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Load all custom journal profiles
|
|
88
|
+
*/
|
|
89
|
+
export function loadCustomProfiles(): Record<string, NormalizedProfile> {
|
|
90
|
+
const profiles: Record<string, NormalizedProfile> = {};
|
|
91
|
+
|
|
92
|
+
// Load user profiles first (lower priority)
|
|
93
|
+
const userProfiles = loadProfilesFromDir(USER_PLUGINS_DIR);
|
|
94
|
+
Object.assign(profiles, userProfiles);
|
|
95
|
+
|
|
96
|
+
// Load project profiles (higher priority, can override)
|
|
97
|
+
const projectProfiles = loadProfilesFromDir(PROJECT_PLUGINS_DIR);
|
|
98
|
+
Object.assign(profiles, projectProfiles);
|
|
99
|
+
|
|
100
|
+
return profiles;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load profiles from a directory
|
|
105
|
+
*/
|
|
106
|
+
function loadProfilesFromDir(dir: string): Record<string, NormalizedProfile> {
|
|
107
|
+
const profiles: Record<string, NormalizedProfile> = {};
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(dir)) {
|
|
110
|
+
return profiles;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const files = fs.readdirSync(dir).filter(f =>
|
|
115
|
+
f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json')
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
try {
|
|
120
|
+
const filePath = path.join(dir, file);
|
|
121
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
122
|
+
const profile = file.endsWith('.json')
|
|
123
|
+
? JSON.parse(content)
|
|
124
|
+
: yaml.parse(content);
|
|
125
|
+
|
|
126
|
+
if (validateProfile(profile)) {
|
|
127
|
+
const id = profile.id || path.basename(file, path.extname(file));
|
|
128
|
+
profiles[id] = normalizeProfile(profile);
|
|
129
|
+
}
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
console.error(`Warning: Failed to load profile ${file}: ${message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Directory not readable
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return profiles;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate a profile structure
|
|
144
|
+
*/
|
|
145
|
+
function validateProfile(profile: unknown): profile is Profile {
|
|
146
|
+
if (!profile || typeof profile !== 'object') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const p = profile as Profile;
|
|
151
|
+
|
|
152
|
+
// Must have a name
|
|
153
|
+
if (!p.name || typeof p.name !== 'string') {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Requirements must be an object if present
|
|
158
|
+
if (p.requirements && typeof p.requirements !== 'object') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Normalize profile to standard structure
|
|
167
|
+
*/
|
|
168
|
+
function normalizeProfile(profile: Profile): NormalizedProfile {
|
|
169
|
+
const normalized: NormalizedProfile = {
|
|
170
|
+
name: profile.name,
|
|
171
|
+
url: profile.url || null,
|
|
172
|
+
custom: true,
|
|
173
|
+
requirements: {
|
|
174
|
+
wordLimit: profile.requirements?.wordLimit || (profile as { wordLimit?: Record<string, number> }).wordLimit || {},
|
|
175
|
+
references: profile.requirements?.references || (profile as { references?: Record<string, unknown> }).references || {},
|
|
176
|
+
figures: profile.requirements?.figures || (profile as { figures?: Record<string, unknown> }).figures || {},
|
|
177
|
+
sections: profile.requirements?.sections || (profile as { sections?: Record<string, unknown> }).sections || {},
|
|
178
|
+
authors: profile.requirements?.authors || (profile as { authors?: Record<string, unknown> }).authors || {},
|
|
179
|
+
keywords: profile.requirements?.keywords || (profile as { keywords?: { min?: number; max?: number } }).keywords || null,
|
|
180
|
+
dataAvailability: profile.requirements?.dataAvailability || (profile as { dataAvailability?: boolean }).dataAvailability || false,
|
|
181
|
+
...profile.requirements,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Pass through formatting if present
|
|
186
|
+
const formatting = (profile as { formatting?: ProfileFormatting }).formatting;
|
|
187
|
+
if (formatting && typeof formatting === 'object') {
|
|
188
|
+
normalized.formatting = formatting;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return normalized;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Initialize plugin directories
|
|
196
|
+
*/
|
|
197
|
+
export function initPluginDir(project = false): string {
|
|
198
|
+
const dir = project ? PROJECT_PLUGINS_DIR : USER_PLUGINS_DIR;
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(dir)) {
|
|
201
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return dir;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get plugin directories info
|
|
209
|
+
*/
|
|
210
|
+
export function getPluginDirs(): PluginDirsInfo {
|
|
211
|
+
return {
|
|
212
|
+
user: USER_PLUGINS_DIR,
|
|
213
|
+
project: PROJECT_PLUGINS_DIR,
|
|
214
|
+
userExists: fs.existsSync(USER_PLUGINS_DIR),
|
|
215
|
+
projectExists: fs.existsSync(PROJECT_PLUGINS_DIR),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a sample profile template
|
|
221
|
+
*/
|
|
222
|
+
export function createProfileTemplate(journalName: string): string {
|
|
223
|
+
const id = journalName.toLowerCase().replace(/\s+/g, '-');
|
|
224
|
+
|
|
225
|
+
return `# Custom journal profile for ${journalName}
|
|
226
|
+
# Save as: ~/.rev/profiles/${id}.yaml (user-wide)
|
|
227
|
+
# Or: .rev/profiles/${id}.yaml (project-specific)
|
|
228
|
+
|
|
229
|
+
id: ${id}
|
|
230
|
+
name: "${journalName}"
|
|
231
|
+
url: "https://journal-website.com/author-guidelines"
|
|
232
|
+
|
|
233
|
+
# Word count limits
|
|
234
|
+
wordLimit:
|
|
235
|
+
main: 8000 # null for no limit
|
|
236
|
+
abstract: 300
|
|
237
|
+
title: null # characters
|
|
238
|
+
|
|
239
|
+
# Reference requirements
|
|
240
|
+
references:
|
|
241
|
+
max: null # null for no limit
|
|
242
|
+
doiRequired: true
|
|
243
|
+
|
|
244
|
+
# Figure/table limits
|
|
245
|
+
figures:
|
|
246
|
+
max: 8
|
|
247
|
+
combinedWithTables: false
|
|
248
|
+
|
|
249
|
+
# Required sections
|
|
250
|
+
sections:
|
|
251
|
+
required:
|
|
252
|
+
- Abstract
|
|
253
|
+
- Introduction
|
|
254
|
+
- Methods
|
|
255
|
+
- Results
|
|
256
|
+
- Discussion
|
|
257
|
+
methodsPosition: null # 'end' or 'before-results'
|
|
258
|
+
|
|
259
|
+
# Keywords
|
|
260
|
+
keywords:
|
|
261
|
+
min: 4
|
|
262
|
+
max: 8
|
|
263
|
+
|
|
264
|
+
# Other requirements
|
|
265
|
+
dataAvailability: true
|
|
266
|
+
highlights: false
|
|
267
|
+
graphicalAbstract: false
|
|
268
|
+
|
|
269
|
+
# Build formatting defaults (applied via rev build -j ${id})
|
|
270
|
+
# formatting:
|
|
271
|
+
# csl: "${id}" # CSL style name or path
|
|
272
|
+
# pdf:
|
|
273
|
+
# fontsize: 12pt
|
|
274
|
+
# geometry: margin=1in
|
|
275
|
+
# linestretch: 1.5
|
|
276
|
+
# numbersections: false
|
|
277
|
+
# docx:
|
|
278
|
+
# reference: null # Path to reference .docx template
|
|
279
|
+
# crossref:
|
|
280
|
+
# figPrefix: [Fig., Figs.]
|
|
281
|
+
# tblPrefix: [Table, Tables]
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Save a profile template
|
|
287
|
+
*/
|
|
288
|
+
export function saveProfileTemplate(journalName: string, project = false): string {
|
|
289
|
+
const dir = initPluginDir(project);
|
|
290
|
+
const id = journalName.toLowerCase().replace(/\s+/g, '-');
|
|
291
|
+
const filePath = path.join(dir, `${id}.yaml`);
|
|
292
|
+
|
|
293
|
+
if (fs.existsSync(filePath)) {
|
|
294
|
+
throw new Error(`Profile already exists: ${filePath}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const content = createProfileTemplate(journalName);
|
|
298
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
299
|
+
|
|
300
|
+
return filePath;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* List all custom profiles
|
|
305
|
+
*/
|
|
306
|
+
export function listCustomProfiles(): ProfileListEntry[] {
|
|
307
|
+
const result: ProfileListEntry[] = [];
|
|
308
|
+
|
|
309
|
+
// User profiles
|
|
310
|
+
if (fs.existsSync(USER_PLUGINS_DIR)) {
|
|
311
|
+
const files = fs.readdirSync(USER_PLUGINS_DIR).filter(f =>
|
|
312
|
+
f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json')
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
try {
|
|
317
|
+
const filePath = path.join(USER_PLUGINS_DIR, file);
|
|
318
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
319
|
+
const profile = file.endsWith('.json') ? JSON.parse(content) : yaml.parse(content);
|
|
320
|
+
|
|
321
|
+
if (validateProfile(profile)) {
|
|
322
|
+
result.push({
|
|
323
|
+
id: profile.id || path.basename(file, path.extname(file)),
|
|
324
|
+
name: profile.name,
|
|
325
|
+
source: 'user',
|
|
326
|
+
path: filePath,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
// Skip invalid profiles
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Project profiles
|
|
336
|
+
if (fs.existsSync(PROJECT_PLUGINS_DIR)) {
|
|
337
|
+
const files = fs.readdirSync(PROJECT_PLUGINS_DIR).filter(f =>
|
|
338
|
+
f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json')
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
for (const file of files) {
|
|
342
|
+
try {
|
|
343
|
+
const filePath = path.join(PROJECT_PLUGINS_DIR, file);
|
|
344
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
345
|
+
const profile = file.endsWith('.json') ? JSON.parse(content) : yaml.parse(content);
|
|
346
|
+
|
|
347
|
+
if (validateProfile(profile)) {
|
|
348
|
+
result.push({
|
|
349
|
+
id: profile.id || path.basename(file, path.extname(file)),
|
|
350
|
+
name: profile.name,
|
|
351
|
+
source: 'project',
|
|
352
|
+
path: filePath,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
// Skip invalid profiles
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return result;
|
|
362
|
+
}
|