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