docrev 0.6.7 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/schema.js ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * JSON Schema validation for rev.yaml configuration
3
+ */
4
+
5
+ /**
6
+ * JSON Schema for rev.yaml
7
+ */
8
+ export const revYamlSchema = {
9
+ $schema: 'http://json-schema.org/draft-07/schema#',
10
+ title: 'rev.yaml configuration',
11
+ description: 'Configuration file for docrev document workflow',
12
+ type: 'object',
13
+ properties: {
14
+ title: {
15
+ type: 'string',
16
+ description: 'Document title',
17
+ },
18
+ version: {
19
+ type: 'string',
20
+ description: 'Document version',
21
+ },
22
+ authors: {
23
+ type: 'array',
24
+ description: 'List of authors',
25
+ items: {
26
+ oneOf: [
27
+ { type: 'string' },
28
+ {
29
+ type: 'object',
30
+ properties: {
31
+ name: { type: 'string' },
32
+ affiliation: { type: 'string' },
33
+ email: { type: 'string', format: 'email' },
34
+ orcid: { type: 'string', pattern: '^\\d{4}-\\d{4}-\\d{4}-\\d{3}[0-9X]$' },
35
+ },
36
+ required: ['name'],
37
+ },
38
+ ],
39
+ },
40
+ },
41
+ sections: {
42
+ type: 'array',
43
+ description: 'Ordered list of section files to include',
44
+ items: { type: 'string', pattern: '.*\\.md$' },
45
+ },
46
+ bibliography: {
47
+ type: 'string',
48
+ description: 'Path to bibliography file (.bib)',
49
+ pattern: '.*\\.bib$',
50
+ },
51
+ csl: {
52
+ type: 'string',
53
+ description: 'Path to CSL citation style file',
54
+ },
55
+ crossref: {
56
+ type: 'object',
57
+ description: 'pandoc-crossref settings',
58
+ properties: {
59
+ figureTitle: { type: 'string', default: 'Figure' },
60
+ tableTitle: { type: 'string', default: 'Table' },
61
+ figPrefix: {
62
+ oneOf: [
63
+ { type: 'string' },
64
+ { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
65
+ ],
66
+ },
67
+ tblPrefix: {
68
+ oneOf: [
69
+ { type: 'string' },
70
+ { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
71
+ ],
72
+ },
73
+ eqnPrefix: {
74
+ oneOf: [
75
+ { type: 'string' },
76
+ { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
77
+ ],
78
+ },
79
+ secPrefix: {
80
+ oneOf: [
81
+ { type: 'string' },
82
+ { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
83
+ ],
84
+ },
85
+ linkReferences: { type: 'boolean', default: true },
86
+ },
87
+ additionalProperties: true,
88
+ },
89
+ pdf: {
90
+ type: 'object',
91
+ description: 'PDF output settings',
92
+ properties: {
93
+ template: { type: 'string' },
94
+ documentclass: {
95
+ type: 'string',
96
+ enum: ['article', 'report', 'book', 'memoir', 'scrartcl', 'scrreprt', 'scrbook'],
97
+ default: 'article',
98
+ },
99
+ fontsize: {
100
+ type: 'string',
101
+ pattern: '^\\d{1,2}pt$',
102
+ default: '12pt',
103
+ },
104
+ geometry: { type: 'string', default: 'margin=1in' },
105
+ linestretch: { type: 'number', minimum: 1, maximum: 3, default: 1.5 },
106
+ numbersections: { type: 'boolean', default: false },
107
+ toc: { type: 'boolean', default: false },
108
+ header: { type: 'string' },
109
+ footer: { type: 'string' },
110
+ },
111
+ additionalProperties: true,
112
+ },
113
+ docx: {
114
+ type: 'object',
115
+ description: 'Word output settings',
116
+ properties: {
117
+ reference: { type: 'string', description: 'Reference document for styling' },
118
+ keepComments: { type: 'boolean', default: true },
119
+ toc: { type: 'boolean', default: false },
120
+ },
121
+ additionalProperties: true,
122
+ },
123
+ tex: {
124
+ type: 'object',
125
+ description: 'LaTeX output settings',
126
+ properties: {
127
+ standalone: { type: 'boolean', default: true },
128
+ },
129
+ additionalProperties: true,
130
+ },
131
+ },
132
+ additionalProperties: true,
133
+ };
134
+
135
+ /**
136
+ * Validate a value against a simple schema
137
+ * @param {*} value - Value to validate
138
+ * @param {object} schema - JSON Schema
139
+ * @param {string} path - Current path for error messages
140
+ * @returns {object[]} Array of validation errors
141
+ */
142
+ function validateValue(value, schema, path = '') {
143
+ const errors = [];
144
+
145
+ // Handle oneOf
146
+ if (schema.oneOf) {
147
+ const validForAny = schema.oneOf.some((subSchema) => {
148
+ const subErrors = validateValue(value, subSchema, path);
149
+ return subErrors.length === 0;
150
+ });
151
+ if (!validForAny) {
152
+ errors.push({
153
+ path,
154
+ message: `Value does not match any allowed type`,
155
+ value,
156
+ });
157
+ }
158
+ return errors;
159
+ }
160
+
161
+ // Type check
162
+ if (schema.type) {
163
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
164
+ if (actualType !== schema.type) {
165
+ errors.push({
166
+ path,
167
+ message: `Expected ${schema.type}, got ${actualType}`,
168
+ value,
169
+ });
170
+ return errors; // Stop further validation if type is wrong
171
+ }
172
+ }
173
+
174
+ // String validation
175
+ if (schema.type === 'string' && typeof value === 'string') {
176
+ if (schema.pattern) {
177
+ const regex = new RegExp(schema.pattern);
178
+ if (!regex.test(value)) {
179
+ errors.push({
180
+ path,
181
+ message: `Value "${value}" does not match pattern ${schema.pattern}`,
182
+ value,
183
+ });
184
+ }
185
+ }
186
+ if (schema.enum && !schema.enum.includes(value)) {
187
+ errors.push({
188
+ path,
189
+ message: `Value "${value}" must be one of: ${schema.enum.join(', ')}`,
190
+ value,
191
+ });
192
+ }
193
+ }
194
+
195
+ // Number validation
196
+ if (schema.type === 'number' && typeof value === 'number') {
197
+ if (schema.minimum !== undefined && value < schema.minimum) {
198
+ errors.push({
199
+ path,
200
+ message: `Value ${value} is less than minimum ${schema.minimum}`,
201
+ value,
202
+ });
203
+ }
204
+ if (schema.maximum !== undefined && value > schema.maximum) {
205
+ errors.push({
206
+ path,
207
+ message: `Value ${value} is greater than maximum ${schema.maximum}`,
208
+ value,
209
+ });
210
+ }
211
+ }
212
+
213
+ // Array validation
214
+ if (schema.type === 'array' && Array.isArray(value)) {
215
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
216
+ errors.push({
217
+ path,
218
+ message: `Array must have at least ${schema.minItems} items`,
219
+ value,
220
+ });
221
+ }
222
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
223
+ errors.push({
224
+ path,
225
+ message: `Array must have at most ${schema.maxItems} items`,
226
+ value,
227
+ });
228
+ }
229
+ if (schema.items) {
230
+ value.forEach((item, index) => {
231
+ errors.push(...validateValue(item, schema.items, `${path}[${index}]`));
232
+ });
233
+ }
234
+ }
235
+
236
+ // Object validation
237
+ if (schema.type === 'object' && typeof value === 'object' && value !== null) {
238
+ if (schema.properties) {
239
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
240
+ if (value[key] !== undefined) {
241
+ errors.push(...validateValue(value[key], propSchema, path ? `${path}.${key}` : key));
242
+ }
243
+ }
244
+ }
245
+ if (schema.required) {
246
+ for (const key of schema.required) {
247
+ if (value[key] === undefined) {
248
+ errors.push({
249
+ path: path ? `${path}.${key}` : key,
250
+ message: `Required property "${key}" is missing`,
251
+ value: undefined,
252
+ });
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ return errors;
259
+ }
260
+
261
+ /**
262
+ * Validate rev.yaml configuration
263
+ * @param {object} config - Parsed configuration object
264
+ * @returns {{ valid: boolean, errors: object[], warnings: object[] }}
265
+ */
266
+ export function validateConfig(config) {
267
+ const errors = validateValue(config, revYamlSchema);
268
+ const warnings = [];
269
+
270
+ // Additional semantic validations
271
+ if (config.sections && config.sections.length === 0) {
272
+ warnings.push({
273
+ path: 'sections',
274
+ message: 'No sections specified - build will auto-detect .md files',
275
+ });
276
+ }
277
+
278
+ if (config.bibliography && !config.bibliography.endsWith('.bib')) {
279
+ warnings.push({
280
+ path: 'bibliography',
281
+ message: 'Bibliography file should have .bib extension',
282
+ });
283
+ }
284
+
285
+ if (config.pdf?.linestretch && (config.pdf.linestretch < 1 || config.pdf.linestretch > 3)) {
286
+ warnings.push({
287
+ path: 'pdf.linestretch',
288
+ message: 'Line stretch values outside 1-3 range may produce unexpected results',
289
+ });
290
+ }
291
+
292
+ // Check for common typos
293
+ const knownKeys = Object.keys(revYamlSchema.properties);
294
+ for (const key of Object.keys(config)) {
295
+ if (key.startsWith('_')) continue; // Internal keys
296
+ if (!knownKeys.includes(key)) {
297
+ // Check for similar keys (possible typos)
298
+ const similar = knownKeys.find(
299
+ (k) => levenshtein(key.toLowerCase(), k.toLowerCase()) <= 2
300
+ );
301
+ if (similar) {
302
+ warnings.push({
303
+ path: key,
304
+ message: `Unknown property "${key}" - did you mean "${similar}"?`,
305
+ });
306
+ }
307
+ }
308
+ }
309
+
310
+ return {
311
+ valid: errors.length === 0,
312
+ errors,
313
+ warnings,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Format validation results for display
319
+ * @param {{ valid: boolean, errors: object[], warnings: object[] }} result
320
+ * @param {object} chalk - Chalk instance for coloring
321
+ * @returns {string}
322
+ */
323
+ export function formatValidationResult(result, chalk) {
324
+ const lines = [];
325
+
326
+ if (result.errors.length > 0) {
327
+ lines.push(chalk.red('Configuration errors:'));
328
+ for (const error of result.errors) {
329
+ lines.push(chalk.red(` ✗ ${error.path}: ${error.message}`));
330
+ }
331
+ }
332
+
333
+ if (result.warnings.length > 0) {
334
+ if (lines.length > 0) lines.push('');
335
+ lines.push(chalk.yellow('Warnings:'));
336
+ for (const warning of result.warnings) {
337
+ lines.push(chalk.yellow(` ! ${warning.path}: ${warning.message}`));
338
+ }
339
+ }
340
+
341
+ if (result.valid && result.warnings.length === 0) {
342
+ lines.push(chalk.green('✓ Configuration is valid'));
343
+ }
344
+
345
+ return lines.join('\n');
346
+ }
347
+
348
+ /**
349
+ * Levenshtein distance for typo detection
350
+ */
351
+ function levenshtein(a, b) {
352
+ const matrix = Array(b.length + 1)
353
+ .fill(null)
354
+ .map(() => Array(a.length + 1).fill(null));
355
+ for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
356
+ for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
357
+ for (let j = 1; j <= b.length; j++) {
358
+ for (let i = 1; i <= a.length; i++) {
359
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
360
+ matrix[j][i] = Math.min(
361
+ matrix[j][i - 1] + 1,
362
+ matrix[j - 1][i] + 1,
363
+ matrix[j - 1][i - 1] + cost
364
+ );
365
+ }
366
+ }
367
+ return matrix[b.length][a.length];
368
+ }
package/lib/sections.js CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import * as yaml from 'js-yaml';
7
+ import YAML from 'yaml';
8
8
 
9
9
  /**
10
10
  * @typedef {Object} SectionConfig
@@ -124,7 +124,7 @@ function titleCase(str) {
124
124
  */
125
125
  export function loadConfig(configPath) {
126
126
  const content = fs.readFileSync(configPath, 'utf-8');
127
- const config = yaml.load(content);
127
+ const config = YAML.parse(content);
128
128
 
129
129
  // Normalize: convert string values to full config objects
130
130
  const normalized = { ...config };
@@ -154,12 +154,7 @@ export function loadConfig(configPath) {
154
154
  * @param {object} config
155
155
  */
156
156
  export function saveConfig(configPath, config) {
157
- const yamlStr = yaml.dump(config, {
158
- indent: 2,
159
- lineWidth: 100,
160
- quotingType: '"',
161
- forceQuotes: false,
162
- });
157
+ const yamlStr = YAML.stringify(config, { indent: 2, lineWidth: 100 });
163
158
  fs.writeFileSync(configPath, yamlStr, 'utf-8');
164
159
  }
165
160
 
package/lib/templates.js CHANGED
@@ -212,6 +212,135 @@ paper.md
212
212
  directories: ['figures', 'tables'],
213
213
  },
214
214
 
215
+ /**
216
+ * LaTeX-focused project with direct .tex output
217
+ */
218
+ latex: {
219
+ name: 'LaTeX Project',
220
+ description: 'LaTeX-native with journal template support',
221
+ files: {
222
+ 'rev.yaml': `# LaTeX Paper Configuration
223
+ title: "Paper Title"
224
+ authors:
225
+ - name: First Author
226
+ affiliation: University
227
+ email: author@example.edu
228
+ orcid: 0000-0000-0000-0000
229
+
230
+ sections:
231
+ - introduction.md
232
+ - methods.md
233
+ - results.md
234
+ - discussion.md
235
+
236
+ bibliography: references.bib
237
+ csl: null
238
+
239
+ # LaTeX-specific settings
240
+ pdf:
241
+ documentclass: article
242
+ classoption: [11pt, a4paper]
243
+ fontsize: 11pt
244
+ geometry: "margin=2.5cm"
245
+ linestretch: 1.5
246
+ numbersections: true
247
+ header-includes: |
248
+ \\usepackage{amsmath}
249
+ \\usepackage{graphicx}
250
+ \\usepackage{booktabs}
251
+ \\usepackage{hyperref}
252
+ \\usepackage{natbib}
253
+
254
+ # TEX output settings
255
+ tex:
256
+ standalone: true
257
+ keep-tex: true
258
+ `,
259
+ 'introduction.md': `# Introduction
260
+
261
+ Background and motivation.
262
+
263
+ ## Objectives
264
+
265
+ State your research questions.
266
+
267
+ `,
268
+ 'methods.md': `# Materials and Methods
269
+
270
+ ## Study Area
271
+
272
+ Describe the study area or data sources.
273
+
274
+ ## Statistical Analysis
275
+
276
+ All analyses were performed in R [@R2024].
277
+
278
+ `,
279
+ 'results.md': `# Results
280
+
281
+ Main findings presented here.
282
+
283
+ ![Caption for figure](figures/fig1.pdf){#fig:main width=100%}
284
+
285
+ See @fig:main for the main results.
286
+
287
+ | Variable | Value | SE |
288
+ |----------|-------|------|
289
+ | A | 1.23 | 0.05 |
290
+ | B | 4.56 | 0.12 |
291
+
292
+ : Summary statistics {#tbl:summary}
293
+
294
+ `,
295
+ 'discussion.md': `# Discussion
296
+
297
+ Interpretation of findings.
298
+
299
+ ## Limitations
300
+
301
+ Study limitations.
302
+
303
+ ## Conclusions
304
+
305
+ Key takeaways.
306
+
307
+ `,
308
+ 'references.bib': `@Manual{R2024,
309
+ title = {R: A Language and Environment for Statistical Computing},
310
+ author = {{R Core Team}},
311
+ organization = {R Foundation for Statistical Computing},
312
+ address = {Vienna, Austria},
313
+ year = {2024},
314
+ url = {https://www.R-project.org/}
315
+ }
316
+ `,
317
+ '.gitignore': `# Build outputs
318
+ *.pdf
319
+ *.docx
320
+ paper.md
321
+ .paper-*.md
322
+
323
+ # Keep .tex for version control
324
+ # *.tex
325
+
326
+ # LaTeX auxiliary files
327
+ *.aux
328
+ *.bbl
329
+ *.blg
330
+ *.log
331
+ *.out
332
+ *.toc
333
+ *.fdb_latexmk
334
+ *.fls
335
+ *.synctex.gz
336
+
337
+ # System
338
+ .DS_Store
339
+ `,
340
+ },
341
+ directories: ['figures', 'tables'],
342
+ },
343
+
215
344
  /**
216
345
  * Review article structure
217
346
  */
@@ -303,3 +432,92 @@ export function listTemplates() {
303
432
  description: template.description,
304
433
  }));
305
434
  }
435
+
436
+ /**
437
+ * Convert string to title case for headers
438
+ * @param {string} str
439
+ * @returns {string}
440
+ */
441
+ function titleCase(str) {
442
+ return str
443
+ .split(/[-_\s]+/)
444
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
445
+ .join(' ');
446
+ }
447
+
448
+ /**
449
+ * Generate a custom template with specified sections
450
+ * @param {string[]} sections - Array of section names (without .md extension)
451
+ * @param {object} baseTemplate - Base template to extend (default: paper)
452
+ * @returns {object}
453
+ */
454
+ export function generateCustomTemplate(sections, baseTemplate = TEMPLATES.paper) {
455
+ const files = {};
456
+
457
+ // Generate rev.yaml with custom sections
458
+ const sectionsList = sections.map((s) => ` - ${s}.md`).join('\n');
459
+ files['rev.yaml'] = `# Paper configuration
460
+ title: "Your Paper Title"
461
+ authors:
462
+ - name: First Author
463
+ affiliation: Institution
464
+ email: author@example.com
465
+
466
+ # Section files in order
467
+ sections:
468
+ ${sectionsList}
469
+
470
+ # Bibliography (optional)
471
+ bibliography: references.bib
472
+ csl: null # uses default CSL
473
+
474
+ # Cross-reference settings
475
+ crossref:
476
+ figureTitle: Figure
477
+ tableTitle: Table
478
+ figPrefix: [Fig., Figs.]
479
+ tblPrefix: [Table, Tables]
480
+ linkReferences: true
481
+
482
+ # PDF output settings
483
+ pdf:
484
+ documentclass: article
485
+ fontsize: 12pt
486
+ geometry: margin=1in
487
+ linestretch: 1.5
488
+ numbersections: false
489
+
490
+ # Word output settings
491
+ docx:
492
+ reference: null # path to reference.docx template
493
+ keepComments: true
494
+ `;
495
+
496
+ // Generate section files
497
+ for (const section of sections) {
498
+ const header = titleCase(section);
499
+ files[`${section}.md`] = `# ${header}
500
+
501
+ `;
502
+ }
503
+
504
+ // Add common files
505
+ files['references.bib'] = baseTemplate.files['references.bib'] || '';
506
+ files['.gitignore'] = baseTemplate.files['.gitignore'] || `# Build outputs
507
+ *.pdf
508
+ *.docx
509
+ *.tex
510
+ paper.md
511
+ .paper-*.md
512
+
513
+ # System
514
+ .DS_Store
515
+ `;
516
+
517
+ return {
518
+ name: 'Custom',
519
+ description: 'Custom sections',
520
+ files,
521
+ directories: baseTemplate.directories || ['figures'],
522
+ };
523
+ }