docrev 0.2.1 → 0.5.0
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/.rev-dictionary +4 -0
- package/CHANGELOG.md +76 -0
- package/README.md +89 -0
- package/bin/rev.js +2126 -0
- package/completions/rev.bash +127 -0
- package/completions/rev.zsh +207 -0
- package/lib/build.js +12 -1
- package/lib/doi.js +6 -2
- package/lib/equations.js +29 -12
- package/lib/git.js +238 -0
- package/lib/grammar.js +290 -0
- package/lib/journals.js +605 -0
- package/lib/merge.js +365 -0
- package/lib/scientific-words.js +73 -0
- package/lib/spelling.js +350 -0
- package/lib/trackchanges.js +229 -0
- package/lib/variables.js +173 -0
- package/lib/word.js +225 -0
- package/package.json +80 -2
- package/skill/REFERENCE.md +279 -0
- package/skill/SKILL.md +137 -0
- package/types/index.d.ts +525 -0
- package/CLAUDE.md +0 -75
package/lib/journals.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal validation profiles
|
|
3
|
+
* Check manuscripts against journal-specific requirements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Journal requirement profiles
|
|
11
|
+
* Based on publicly available author guidelines
|
|
12
|
+
*/
|
|
13
|
+
export const JOURNAL_PROFILES = {
|
|
14
|
+
nature: {
|
|
15
|
+
name: 'Nature',
|
|
16
|
+
url: 'https://www.nature.com/nature/for-authors',
|
|
17
|
+
requirements: {
|
|
18
|
+
wordLimit: { main: 3000, abstract: 150, title: 90 },
|
|
19
|
+
references: { max: 50, doiRequired: true },
|
|
20
|
+
figures: { max: 6, combinedWithTables: true },
|
|
21
|
+
sections: {
|
|
22
|
+
required: ['Abstract', 'Introduction', 'Results', 'Discussion', 'Methods'],
|
|
23
|
+
methodsPosition: 'end',
|
|
24
|
+
},
|
|
25
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
science: {
|
|
30
|
+
name: 'Science',
|
|
31
|
+
url: 'https://www.science.org/content/page/instructions-preparing-initial-manuscript',
|
|
32
|
+
requirements: {
|
|
33
|
+
wordLimit: { main: 2500, abstract: 125, title: 120 },
|
|
34
|
+
references: { max: 40, doiRequired: true },
|
|
35
|
+
figures: { max: 4, combinedWithTables: true },
|
|
36
|
+
sections: {
|
|
37
|
+
required: ['Abstract', 'Introduction', 'Results', 'Discussion'],
|
|
38
|
+
supplementary: true,
|
|
39
|
+
},
|
|
40
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
'plos-one': {
|
|
45
|
+
name: 'PLOS ONE',
|
|
46
|
+
url: 'https://journals.plos.org/plosone/s/submission-guidelines',
|
|
47
|
+
requirements: {
|
|
48
|
+
wordLimit: { main: null, abstract: 300, title: 250 },
|
|
49
|
+
references: { max: null, doiRequired: false },
|
|
50
|
+
figures: { max: null, combinedWithTables: false },
|
|
51
|
+
sections: {
|
|
52
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
53
|
+
methodsPosition: 'before-results',
|
|
54
|
+
},
|
|
55
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
56
|
+
dataAvailability: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
'pnas': {
|
|
61
|
+
name: 'PNAS',
|
|
62
|
+
url: 'https://www.pnas.org/author-center/submitting-your-manuscript',
|
|
63
|
+
requirements: {
|
|
64
|
+
wordLimit: { main: 4500, abstract: 250, title: null },
|
|
65
|
+
references: { max: 50, doiRequired: true },
|
|
66
|
+
figures: { max: 6, combinedWithTables: true },
|
|
67
|
+
sections: {
|
|
68
|
+
required: ['Abstract', 'Introduction', 'Results', 'Discussion'],
|
|
69
|
+
significanceStatement: true,
|
|
70
|
+
},
|
|
71
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
'ecology-letters': {
|
|
76
|
+
name: 'Ecology Letters',
|
|
77
|
+
url: 'https://onlinelibrary.wiley.com/page/journal/14610248/homepage/forauthors.html',
|
|
78
|
+
requirements: {
|
|
79
|
+
wordLimit: { main: 5000, abstract: 150, title: null },
|
|
80
|
+
references: { max: 50, doiRequired: true },
|
|
81
|
+
figures: { max: 6, combinedWithTables: true },
|
|
82
|
+
sections: {
|
|
83
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
84
|
+
},
|
|
85
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
86
|
+
keywords: { min: 3, max: 10 },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
'ecological-applications': {
|
|
91
|
+
name: 'Ecological Applications',
|
|
92
|
+
url: 'https://esajournals.onlinelibrary.wiley.com/hub/journal/19395582/author-guidelines',
|
|
93
|
+
requirements: {
|
|
94
|
+
wordLimit: { main: 7000, abstract: 350, title: null },
|
|
95
|
+
references: { max: null, doiRequired: true },
|
|
96
|
+
figures: { max: null, combinedWithTables: false },
|
|
97
|
+
sections: {
|
|
98
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
99
|
+
},
|
|
100
|
+
dataAvailability: true,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
'molecular-ecology': {
|
|
105
|
+
name: 'Molecular Ecology',
|
|
106
|
+
url: 'https://onlinelibrary.wiley.com/page/journal/1365294x/homepage/forauthors.html',
|
|
107
|
+
requirements: {
|
|
108
|
+
wordLimit: { main: 8000, abstract: 250, title: null },
|
|
109
|
+
references: { max: null, doiRequired: true },
|
|
110
|
+
figures: { max: 8, combinedWithTables: false },
|
|
111
|
+
sections: {
|
|
112
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
113
|
+
},
|
|
114
|
+
dataAvailability: true,
|
|
115
|
+
keywords: { min: 4, max: 8 },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
'elife': {
|
|
120
|
+
name: 'eLife',
|
|
121
|
+
url: 'https://reviewer.elifesciences.org/author-guide/full',
|
|
122
|
+
requirements: {
|
|
123
|
+
wordLimit: { main: null, abstract: 150, title: null },
|
|
124
|
+
references: { max: null, doiRequired: true },
|
|
125
|
+
figures: { max: null, combinedWithTables: false },
|
|
126
|
+
sections: {
|
|
127
|
+
required: ['Abstract', 'Introduction', 'Results', 'Discussion', 'Methods'],
|
|
128
|
+
methodsPosition: 'end',
|
|
129
|
+
},
|
|
130
|
+
impactStatement: true,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
'cell': {
|
|
135
|
+
name: 'Cell',
|
|
136
|
+
url: 'https://www.cell.com/cell/authors',
|
|
137
|
+
requirements: {
|
|
138
|
+
wordLimit: { main: 7000, abstract: 150, title: null },
|
|
139
|
+
references: { max: 100, doiRequired: true },
|
|
140
|
+
figures: { max: 7, combinedWithTables: true },
|
|
141
|
+
sections: {
|
|
142
|
+
required: ['Abstract', 'Introduction', 'Results', 'Discussion'],
|
|
143
|
+
graphicalAbstract: true,
|
|
144
|
+
highlights: true,
|
|
145
|
+
},
|
|
146
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
'current-biology': {
|
|
151
|
+
name: 'Current Biology',
|
|
152
|
+
url: 'https://www.cell.com/current-biology/authors',
|
|
153
|
+
requirements: {
|
|
154
|
+
wordLimit: { main: 5000, abstract: 150, title: 150 },
|
|
155
|
+
references: { max: 60, doiRequired: true },
|
|
156
|
+
figures: { max: 4, combinedWithTables: true },
|
|
157
|
+
sections: {
|
|
158
|
+
required: ['Summary', 'Results', 'Discussion'],
|
|
159
|
+
},
|
|
160
|
+
authors: { maxInitial: null, correspondingRequired: true },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
'conservation-biology': {
|
|
165
|
+
name: 'Conservation Biology',
|
|
166
|
+
url: 'https://conbio.onlinelibrary.wiley.com/hub/journal/15231739/homepage/forauthors.html',
|
|
167
|
+
requirements: {
|
|
168
|
+
wordLimit: { main: 7000, abstract: 300, title: null },
|
|
169
|
+
references: { max: null, doiRequired: true },
|
|
170
|
+
figures: { max: 6, combinedWithTables: true },
|
|
171
|
+
sections: {
|
|
172
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
173
|
+
},
|
|
174
|
+
keywords: { min: 5, max: 10 },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
'biological-conservation': {
|
|
179
|
+
name: 'Biological Conservation',
|
|
180
|
+
url: 'https://www.elsevier.com/journals/biological-conservation/0006-3207/guide-for-authors',
|
|
181
|
+
requirements: {
|
|
182
|
+
wordLimit: { main: 8000, abstract: 400, title: null },
|
|
183
|
+
references: { max: null, doiRequired: true },
|
|
184
|
+
figures: { max: null, combinedWithTables: false },
|
|
185
|
+
sections: {
|
|
186
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
187
|
+
},
|
|
188
|
+
highlights: true,
|
|
189
|
+
keywords: { min: 4, max: 6 },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
'journal-of-ecology': {
|
|
194
|
+
name: 'Journal of Ecology',
|
|
195
|
+
url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/13652745/author-guidelines',
|
|
196
|
+
requirements: {
|
|
197
|
+
wordLimit: { main: 7000, abstract: 350, title: null },
|
|
198
|
+
references: { max: null, doiRequired: true },
|
|
199
|
+
figures: { max: null, combinedWithTables: false },
|
|
200
|
+
sections: {
|
|
201
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
202
|
+
},
|
|
203
|
+
keywords: { min: 4, max: 8 },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
'functional-ecology': {
|
|
208
|
+
name: 'Functional Ecology',
|
|
209
|
+
url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/13652435/author-guidelines',
|
|
210
|
+
requirements: {
|
|
211
|
+
wordLimit: { main: 7000, abstract: 350, title: null },
|
|
212
|
+
references: { max: null, doiRequired: true },
|
|
213
|
+
figures: { max: null, combinedWithTables: false },
|
|
214
|
+
sections: {
|
|
215
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
216
|
+
},
|
|
217
|
+
keywords: { min: 4, max: 8 },
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
'global-change-biology': {
|
|
222
|
+
name: 'Global Change Biology',
|
|
223
|
+
url: 'https://onlinelibrary.wiley.com/page/journal/13652486/homepage/forauthors.html',
|
|
224
|
+
requirements: {
|
|
225
|
+
wordLimit: { main: 7000, abstract: 300, title: null },
|
|
226
|
+
references: { max: null, doiRequired: true },
|
|
227
|
+
figures: { max: 8, combinedWithTables: false },
|
|
228
|
+
sections: {
|
|
229
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
230
|
+
},
|
|
231
|
+
keywords: { min: 4, max: 8 },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
'oikos': {
|
|
236
|
+
name: 'Oikos',
|
|
237
|
+
url: 'https://nsojournals.onlinelibrary.wiley.com/hub/journal/16000706/author-guidelines',
|
|
238
|
+
requirements: {
|
|
239
|
+
wordLimit: { main: 8000, abstract: 350, title: null },
|
|
240
|
+
references: { max: null, doiRequired: true },
|
|
241
|
+
figures: { max: null, combinedWithTables: false },
|
|
242
|
+
sections: {
|
|
243
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
244
|
+
},
|
|
245
|
+
keywords: { min: 4, max: 10 },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
'oecologia': {
|
|
250
|
+
name: 'Oecologia',
|
|
251
|
+
url: 'https://www.springer.com/journal/442/submission-guidelines',
|
|
252
|
+
requirements: {
|
|
253
|
+
wordLimit: { main: 8000, abstract: 250, title: null },
|
|
254
|
+
references: { max: null, doiRequired: true },
|
|
255
|
+
figures: { max: null, combinedWithTables: false },
|
|
256
|
+
sections: {
|
|
257
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
258
|
+
},
|
|
259
|
+
keywords: { min: 4, max: 6 },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
'biological-invasions': {
|
|
264
|
+
name: 'Biological Invasions',
|
|
265
|
+
url: 'https://www.springer.com/journal/10530/submission-guidelines',
|
|
266
|
+
requirements: {
|
|
267
|
+
wordLimit: { main: null, abstract: 250, title: null },
|
|
268
|
+
references: { max: null, doiRequired: true },
|
|
269
|
+
figures: { max: null, combinedWithTables: false },
|
|
270
|
+
sections: {
|
|
271
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
272
|
+
},
|
|
273
|
+
keywords: { min: 4, max: 6 },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
'diversity-distributions': {
|
|
278
|
+
name: 'Diversity and Distributions',
|
|
279
|
+
url: 'https://onlinelibrary.wiley.com/page/journal/14724642/homepage/forauthors.html',
|
|
280
|
+
requirements: {
|
|
281
|
+
wordLimit: { main: 6000, abstract: 300, title: null },
|
|
282
|
+
references: { max: null, doiRequired: true },
|
|
283
|
+
figures: { max: 6, combinedWithTables: true },
|
|
284
|
+
sections: {
|
|
285
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
286
|
+
},
|
|
287
|
+
keywords: { min: 4, max: 8 },
|
|
288
|
+
biosketch: true,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
'neobiota': {
|
|
293
|
+
name: 'NeoBiota',
|
|
294
|
+
url: 'https://neobiota.pensoft.net/about#Author_Guidelines',
|
|
295
|
+
requirements: {
|
|
296
|
+
wordLimit: { main: null, abstract: 350, title: null },
|
|
297
|
+
references: { max: null, doiRequired: true },
|
|
298
|
+
figures: { max: null, combinedWithTables: false },
|
|
299
|
+
sections: {
|
|
300
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
301
|
+
},
|
|
302
|
+
keywords: { min: 6, max: 12 },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
'peerj': {
|
|
307
|
+
name: 'PeerJ',
|
|
308
|
+
url: 'https://peerj.com/about/author-instructions/',
|
|
309
|
+
requirements: {
|
|
310
|
+
wordLimit: { main: null, abstract: 500, title: null },
|
|
311
|
+
references: { max: null, doiRequired: false },
|
|
312
|
+
figures: { max: null, combinedWithTables: false },
|
|
313
|
+
sections: {
|
|
314
|
+
required: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* List all available journal profiles
|
|
322
|
+
* @returns {Array<{id: string, name: string, url: string}>}
|
|
323
|
+
*/
|
|
324
|
+
export function listJournals() {
|
|
325
|
+
return Object.entries(JOURNAL_PROFILES).map(([id, profile]) => ({
|
|
326
|
+
id,
|
|
327
|
+
name: profile.name,
|
|
328
|
+
url: profile.url,
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get a specific journal profile
|
|
334
|
+
* @param {string} journalId
|
|
335
|
+
* @returns {Object|null}
|
|
336
|
+
*/
|
|
337
|
+
export function getJournalProfile(journalId) {
|
|
338
|
+
const normalized = journalId.toLowerCase().replace(/\s+/g, '-');
|
|
339
|
+
return JOURNAL_PROFILES[normalized] || null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Count words in text (excluding markdown syntax)
|
|
344
|
+
* @param {string} text
|
|
345
|
+
* @returns {number}
|
|
346
|
+
*/
|
|
347
|
+
function countWords(text) {
|
|
348
|
+
// Remove markdown syntax
|
|
349
|
+
let clean = text
|
|
350
|
+
.replace(/^---[\s\S]*?---/m, '') // YAML frontmatter
|
|
351
|
+
.replace(/!\[.*?\]\(.*?\)/g, '') // Images
|
|
352
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Links
|
|
353
|
+
.replace(/#+\s*/g, '') // Headers
|
|
354
|
+
.replace(/\*\*|__/g, '') // Bold
|
|
355
|
+
.replace(/\*|_/g, '') // Italic
|
|
356
|
+
.replace(/`[^`]+`/g, '') // Inline code
|
|
357
|
+
.replace(/```[\s\S]*?```/g, '') // Code blocks
|
|
358
|
+
.replace(/\{[^}]+\}/g, '') // CriticMarkup and attributes
|
|
359
|
+
.replace(/@\w+:\w+/g, '') // Cross-references
|
|
360
|
+
.replace(/@\w+/g, '') // Citations
|
|
361
|
+
.replace(/\|[^|]+\|/g, ' ') // Table cells
|
|
362
|
+
.replace(/[-=]{3,}/g, '') // Horizontal rules
|
|
363
|
+
.replace(/\n+/g, ' ') // Newlines
|
|
364
|
+
.replace(/\s+/g, ' ') // Multiple spaces
|
|
365
|
+
.trim();
|
|
366
|
+
|
|
367
|
+
return clean.split(/\s+/).filter(w => w.length > 0).length;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Extract abstract from markdown
|
|
372
|
+
* @param {string} text
|
|
373
|
+
* @returns {string|null}
|
|
374
|
+
*/
|
|
375
|
+
function extractAbstract(text) {
|
|
376
|
+
// Try to find abstract section
|
|
377
|
+
const patterns = [
|
|
378
|
+
/^#+\s*Abstract\s*\n([\s\S]*?)(?=^#+|\Z)/mi,
|
|
379
|
+
/^Abstract[:\s]*\n([\s\S]*?)(?=^#+|\n\n)/mi,
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
for (const pattern of patterns) {
|
|
383
|
+
const match = text.match(pattern);
|
|
384
|
+
if (match) {
|
|
385
|
+
return match[1].trim();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Extract title from markdown
|
|
394
|
+
* @param {string} text
|
|
395
|
+
* @returns {string|null}
|
|
396
|
+
*/
|
|
397
|
+
function extractTitle(text) {
|
|
398
|
+
// Try YAML frontmatter
|
|
399
|
+
const yamlMatch = text.match(/^---\n[\s\S]*?title:\s*["']?([^"'\n]+)["']?[\s\S]*?\n---/m);
|
|
400
|
+
if (yamlMatch) {
|
|
401
|
+
return yamlMatch[1].trim();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Try first H1
|
|
405
|
+
const h1Match = text.match(/^#\s+(.+)$/m);
|
|
406
|
+
if (h1Match) {
|
|
407
|
+
return h1Match[1].trim();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Extract sections from markdown
|
|
415
|
+
* @param {string} text
|
|
416
|
+
* @returns {string[]}
|
|
417
|
+
*/
|
|
418
|
+
function extractSections(text) {
|
|
419
|
+
const sections = [];
|
|
420
|
+
const headerPattern = /^#+\s+(.+)$/gm;
|
|
421
|
+
let match;
|
|
422
|
+
|
|
423
|
+
while ((match = headerPattern.exec(text)) !== null) {
|
|
424
|
+
sections.push(match[1].trim());
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return sections;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Count figures in markdown
|
|
432
|
+
* @param {string} text
|
|
433
|
+
* @returns {number}
|
|
434
|
+
*/
|
|
435
|
+
function countFigures(text) {
|
|
436
|
+
// Count images with figure captions
|
|
437
|
+
const figurePattern = /!\[.*?\]\(.*?\)(\{#fig:[^}]+\})?/g;
|
|
438
|
+
const matches = text.match(figurePattern) || [];
|
|
439
|
+
return matches.length;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Count tables in markdown
|
|
444
|
+
* @param {string} text
|
|
445
|
+
* @returns {number}
|
|
446
|
+
*/
|
|
447
|
+
function countTables(text) {
|
|
448
|
+
// Count tables (lines starting with |)
|
|
449
|
+
const tablePattern = /^\|[^|]+\|/gm;
|
|
450
|
+
const matches = text.match(tablePattern) || [];
|
|
451
|
+
// Divide by approximate rows per table
|
|
452
|
+
return Math.ceil(matches.length / 5);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Count references/citations in markdown
|
|
457
|
+
* @param {string} text
|
|
458
|
+
* @returns {number}
|
|
459
|
+
*/
|
|
460
|
+
function countReferences(text) {
|
|
461
|
+
// Count unique citation keys
|
|
462
|
+
const citationPattern = /@(\w+)/g;
|
|
463
|
+
const citations = new Set();
|
|
464
|
+
let match;
|
|
465
|
+
|
|
466
|
+
while ((match = citationPattern.exec(text)) !== null) {
|
|
467
|
+
// Exclude cross-refs like @fig:label
|
|
468
|
+
if (!match[0].includes(':')) {
|
|
469
|
+
citations.add(match[1]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return citations.size;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Validate manuscript against journal requirements
|
|
478
|
+
* @param {string} text - Markdown content
|
|
479
|
+
* @param {string} journalId - Journal profile ID
|
|
480
|
+
* @returns {{valid: boolean, errors: string[], warnings: string[], stats: Object}}
|
|
481
|
+
*/
|
|
482
|
+
export function validateManuscript(text, journalId) {
|
|
483
|
+
const profile = getJournalProfile(journalId);
|
|
484
|
+
|
|
485
|
+
if (!profile) {
|
|
486
|
+
return {
|
|
487
|
+
valid: false,
|
|
488
|
+
errors: [`Unknown journal: ${journalId}`],
|
|
489
|
+
warnings: [],
|
|
490
|
+
stats: null,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const req = profile.requirements;
|
|
495
|
+
const errors = [];
|
|
496
|
+
const warnings = [];
|
|
497
|
+
|
|
498
|
+
// Extract content
|
|
499
|
+
const abstract = extractAbstract(text);
|
|
500
|
+
const title = extractTitle(text);
|
|
501
|
+
const sections = extractSections(text);
|
|
502
|
+
const mainWordCount = countWords(text);
|
|
503
|
+
const figureCount = countFigures(text);
|
|
504
|
+
const tableCount = countTables(text);
|
|
505
|
+
const refCount = countReferences(text);
|
|
506
|
+
|
|
507
|
+
const stats = {
|
|
508
|
+
wordCount: mainWordCount,
|
|
509
|
+
abstractWords: abstract ? countWords(abstract) : 0,
|
|
510
|
+
titleChars: title ? title.length : 0,
|
|
511
|
+
figures: figureCount,
|
|
512
|
+
tables: tableCount,
|
|
513
|
+
references: refCount,
|
|
514
|
+
sections: sections.length,
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Word limits
|
|
518
|
+
if (req.wordLimit) {
|
|
519
|
+
if (req.wordLimit.main && mainWordCount > req.wordLimit.main) {
|
|
520
|
+
errors.push(`Main text exceeds ${req.wordLimit.main} words (current: ${mainWordCount})`);
|
|
521
|
+
}
|
|
522
|
+
if (req.wordLimit.abstract && abstract) {
|
|
523
|
+
const absWords = countWords(abstract);
|
|
524
|
+
if (absWords > req.wordLimit.abstract) {
|
|
525
|
+
errors.push(`Abstract exceeds ${req.wordLimit.abstract} words (current: ${absWords})`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (req.wordLimit.title && title) {
|
|
529
|
+
if (title.length > req.wordLimit.title) {
|
|
530
|
+
warnings.push(`Title exceeds ${req.wordLimit.title} characters (current: ${title.length})`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// References
|
|
536
|
+
if (req.references) {
|
|
537
|
+
if (req.references.max && refCount > req.references.max) {
|
|
538
|
+
errors.push(`References exceed ${req.references.max} (current: ${refCount})`);
|
|
539
|
+
}
|
|
540
|
+
if (req.references.doiRequired) {
|
|
541
|
+
warnings.push('DOI required for all references - run "rev doi check" to verify');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Figures/tables
|
|
546
|
+
if (req.figures) {
|
|
547
|
+
const totalVisual = req.figures.combinedWithTables
|
|
548
|
+
? figureCount + tableCount
|
|
549
|
+
: figureCount;
|
|
550
|
+
const label = req.figures.combinedWithTables ? 'figures + tables' : 'figures';
|
|
551
|
+
|
|
552
|
+
if (req.figures.max && totalVisual > req.figures.max) {
|
|
553
|
+
errors.push(`${label} exceed ${req.figures.max} (current: ${totalVisual})`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Required sections
|
|
558
|
+
if (req.sections?.required) {
|
|
559
|
+
for (const reqSection of req.sections.required) {
|
|
560
|
+
const found = sections.some(s =>
|
|
561
|
+
s.toLowerCase().includes(reqSection.toLowerCase())
|
|
562
|
+
);
|
|
563
|
+
if (!found) {
|
|
564
|
+
warnings.push(`Missing required section: ${reqSection}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Data availability
|
|
570
|
+
if (req.dataAvailability) {
|
|
571
|
+
const hasDataStatement = sections.some(s =>
|
|
572
|
+
s.toLowerCase().includes('data') ||
|
|
573
|
+
text.toLowerCase().includes('data availability') ||
|
|
574
|
+
text.toLowerCase().includes('data statement')
|
|
575
|
+
);
|
|
576
|
+
if (!hasDataStatement) {
|
|
577
|
+
warnings.push('Data availability statement may be required');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
valid: errors.length === 0,
|
|
583
|
+
errors,
|
|
584
|
+
warnings,
|
|
585
|
+
stats,
|
|
586
|
+
journal: profile.name,
|
|
587
|
+
url: profile.url,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Validate multiple files against journal requirements
|
|
593
|
+
* @param {string[]} files - Markdown file paths
|
|
594
|
+
* @param {string} journalId - Journal profile ID
|
|
595
|
+
* @returns {Object}
|
|
596
|
+
*/
|
|
597
|
+
export function validateProject(files, journalId) {
|
|
598
|
+
// Combine all file contents
|
|
599
|
+
const combined = files
|
|
600
|
+
.filter(f => fs.existsSync(f))
|
|
601
|
+
.map(f => fs.readFileSync(f, 'utf-8'))
|
|
602
|
+
.join('\n\n');
|
|
603
|
+
|
|
604
|
+
return validateManuscript(combined, journalId);
|
|
605
|
+
}
|