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/schema.ts
CHANGED
|
@@ -1,417 +1,417 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSON Schema validation for rev.yaml configuration
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Validation error
|
|
7
|
-
*/
|
|
8
|
-
interface ValidationError {
|
|
9
|
-
path: string;
|
|
10
|
-
message: string;
|
|
11
|
-
value?: unknown;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Validation warning
|
|
16
|
-
*/
|
|
17
|
-
interface ValidationWarning {
|
|
18
|
-
path: string;
|
|
19
|
-
message: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Validation result
|
|
24
|
-
*/
|
|
25
|
-
interface ValidationResult {
|
|
26
|
-
valid: boolean;
|
|
27
|
-
errors: ValidationError[];
|
|
28
|
-
warnings: ValidationWarning[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* JSON Schema type
|
|
33
|
-
*/
|
|
34
|
-
interface Schema {
|
|
35
|
-
$schema?: string;
|
|
36
|
-
title?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
type?: string;
|
|
39
|
-
properties?: Record<string, Schema>;
|
|
40
|
-
required?: string[];
|
|
41
|
-
items?: Schema;
|
|
42
|
-
oneOf?: Schema[];
|
|
43
|
-
enum?: string[];
|
|
44
|
-
pattern?: string;
|
|
45
|
-
format?: string;
|
|
46
|
-
minimum?: number;
|
|
47
|
-
maximum?: number;
|
|
48
|
-
minItems?: number;
|
|
49
|
-
maxItems?: number;
|
|
50
|
-
additionalProperties?: boolean;
|
|
51
|
-
default?: unknown;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* JSON Schema for rev.yaml
|
|
56
|
-
*/
|
|
57
|
-
export const revYamlSchema: Schema = {
|
|
58
|
-
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
59
|
-
title: 'rev.yaml configuration',
|
|
60
|
-
description: 'Configuration file for docrev document workflow',
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
title: {
|
|
64
|
-
type: 'string',
|
|
65
|
-
description: 'Document title',
|
|
66
|
-
},
|
|
67
|
-
version: {
|
|
68
|
-
type: 'string',
|
|
69
|
-
description: 'Document version',
|
|
70
|
-
},
|
|
71
|
-
authors: {
|
|
72
|
-
type: 'array',
|
|
73
|
-
description: 'List of authors',
|
|
74
|
-
items: {
|
|
75
|
-
oneOf: [
|
|
76
|
-
{ type: 'string' },
|
|
77
|
-
{
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {
|
|
80
|
-
name: { type: 'string' },
|
|
81
|
-
affiliation: { type: 'string' },
|
|
82
|
-
email: { type: 'string', format: 'email' },
|
|
83
|
-
orcid: { type: 'string', pattern: '^\\d{4}-\\d{4}-\\d{4}-\\d{3}[0-9X]$' },
|
|
84
|
-
},
|
|
85
|
-
required: ['name'],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
journal: {
|
|
91
|
-
type: 'string',
|
|
92
|
-
description: 'Journal profile name for formatting defaults and validation',
|
|
93
|
-
},
|
|
94
|
-
sections: {
|
|
95
|
-
type: 'array',
|
|
96
|
-
description: 'Ordered list of section files to include',
|
|
97
|
-
items: { type: 'string', pattern: '.*\\.md$' },
|
|
98
|
-
},
|
|
99
|
-
bibliography: {
|
|
100
|
-
type: 'string',
|
|
101
|
-
description: 'Path to bibliography file (.bib)',
|
|
102
|
-
pattern: '.*\\.bib$',
|
|
103
|
-
},
|
|
104
|
-
csl: {
|
|
105
|
-
type: 'string',
|
|
106
|
-
description: 'Path to CSL citation style file',
|
|
107
|
-
},
|
|
108
|
-
crossref: {
|
|
109
|
-
type: 'object',
|
|
110
|
-
description: 'pandoc-crossref settings',
|
|
111
|
-
properties: {
|
|
112
|
-
figureTitle: { type: 'string', default: 'Figure' },
|
|
113
|
-
tableTitle: { type: 'string', default: 'Table' },
|
|
114
|
-
figPrefix: {
|
|
115
|
-
oneOf: [
|
|
116
|
-
{ type: 'string' },
|
|
117
|
-
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
tblPrefix: {
|
|
121
|
-
oneOf: [
|
|
122
|
-
{ type: 'string' },
|
|
123
|
-
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
124
|
-
],
|
|
125
|
-
},
|
|
126
|
-
eqnPrefix: {
|
|
127
|
-
oneOf: [
|
|
128
|
-
{ type: 'string' },
|
|
129
|
-
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
secPrefix: {
|
|
133
|
-
oneOf: [
|
|
134
|
-
{ type: 'string' },
|
|
135
|
-
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
136
|
-
],
|
|
137
|
-
},
|
|
138
|
-
linkReferences: { type: 'boolean', default: true },
|
|
139
|
-
},
|
|
140
|
-
additionalProperties: true,
|
|
141
|
-
},
|
|
142
|
-
pdf: {
|
|
143
|
-
type: 'object',
|
|
144
|
-
description: 'PDF output settings',
|
|
145
|
-
properties: {
|
|
146
|
-
template: { type: 'string' },
|
|
147
|
-
documentclass: {
|
|
148
|
-
type: 'string',
|
|
149
|
-
enum: ['article', 'report', 'book', 'memoir', 'scrartcl', 'scrreprt', 'scrbook'],
|
|
150
|
-
default: 'article',
|
|
151
|
-
},
|
|
152
|
-
fontsize: {
|
|
153
|
-
type: 'string',
|
|
154
|
-
pattern: '^\\d{1,2}pt$',
|
|
155
|
-
default: '12pt',
|
|
156
|
-
},
|
|
157
|
-
geometry: { type: 'string', default: 'margin=1in' },
|
|
158
|
-
linestretch: { type: 'number', minimum: 1, maximum: 3, default: 1.5 },
|
|
159
|
-
numbersections: { type: 'boolean', default: false },
|
|
160
|
-
toc: { type: 'boolean', default: false },
|
|
161
|
-
header: { type: 'string' },
|
|
162
|
-
footer: { type: 'string' },
|
|
163
|
-
},
|
|
164
|
-
additionalProperties: true,
|
|
165
|
-
},
|
|
166
|
-
docx: {
|
|
167
|
-
type: 'object',
|
|
168
|
-
description: 'Word output settings',
|
|
169
|
-
properties: {
|
|
170
|
-
reference: { type: 'string', description: 'Reference document for styling' },
|
|
171
|
-
keepComments: { type: 'boolean', default: true },
|
|
172
|
-
toc: { type: 'boolean', default: false },
|
|
173
|
-
},
|
|
174
|
-
additionalProperties: true,
|
|
175
|
-
},
|
|
176
|
-
tex: {
|
|
177
|
-
type: 'object',
|
|
178
|
-
description: 'LaTeX output settings',
|
|
179
|
-
properties: {
|
|
180
|
-
standalone: { type: 'boolean', default: true },
|
|
181
|
-
},
|
|
182
|
-
additionalProperties: true,
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
additionalProperties: true,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Validate a value against a simple schema
|
|
190
|
-
*/
|
|
191
|
-
function validateValue(value: unknown, schema: Schema, path = ''): ValidationError[] {
|
|
192
|
-
const errors: ValidationError[] = [];
|
|
193
|
-
|
|
194
|
-
// Handle oneOf
|
|
195
|
-
if (schema.oneOf) {
|
|
196
|
-
const validForAny = schema.oneOf.some((subSchema) => {
|
|
197
|
-
const subErrors = validateValue(value, subSchema, path);
|
|
198
|
-
return subErrors.length === 0;
|
|
199
|
-
});
|
|
200
|
-
if (!validForAny) {
|
|
201
|
-
errors.push({
|
|
202
|
-
path,
|
|
203
|
-
message: `Value does not match any allowed type`,
|
|
204
|
-
value,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
return errors;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Type check
|
|
211
|
-
if (schema.type) {
|
|
212
|
-
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
213
|
-
if (actualType !== schema.type) {
|
|
214
|
-
errors.push({
|
|
215
|
-
path,
|
|
216
|
-
message: `Expected ${schema.type}, got ${actualType}`,
|
|
217
|
-
value,
|
|
218
|
-
});
|
|
219
|
-
return errors; // Stop further validation if type is wrong
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// String validation
|
|
224
|
-
if (schema.type === 'string' && typeof value === 'string') {
|
|
225
|
-
if (schema.pattern) {
|
|
226
|
-
const regex = new RegExp(schema.pattern);
|
|
227
|
-
if (!regex.test(value)) {
|
|
228
|
-
errors.push({
|
|
229
|
-
path,
|
|
230
|
-
message: `Value "${value}" does not match pattern ${schema.pattern}`,
|
|
231
|
-
value,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (schema.enum && !schema.enum.includes(value)) {
|
|
236
|
-
errors.push({
|
|
237
|
-
path,
|
|
238
|
-
message: `Value "${value}" must be one of: ${schema.enum.join(', ')}`,
|
|
239
|
-
value,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Number validation
|
|
245
|
-
if (schema.type === 'number' && typeof value === 'number') {
|
|
246
|
-
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
247
|
-
errors.push({
|
|
248
|
-
path,
|
|
249
|
-
message: `Value ${value} is less than minimum ${schema.minimum}`,
|
|
250
|
-
value,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
254
|
-
errors.push({
|
|
255
|
-
path,
|
|
256
|
-
message: `Value ${value} is greater than maximum ${schema.maximum}`,
|
|
257
|
-
value,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Array validation
|
|
263
|
-
if (schema.type === 'array' && Array.isArray(value)) {
|
|
264
|
-
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
265
|
-
errors.push({
|
|
266
|
-
path,
|
|
267
|
-
message: `Array must have at least ${schema.minItems} items`,
|
|
268
|
-
value,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
272
|
-
errors.push({
|
|
273
|
-
path,
|
|
274
|
-
message: `Array must have at most ${schema.maxItems} items`,
|
|
275
|
-
value,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
if (schema.items) {
|
|
279
|
-
value.forEach((item, index) => {
|
|
280
|
-
errors.push(...validateValue(item, schema.items!, `${path}[${index}]`));
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Object validation
|
|
286
|
-
if (schema.type === 'object' && typeof value === 'object' && value !== null) {
|
|
287
|
-
const obj = value as Record<string, unknown>;
|
|
288
|
-
if (schema.properties) {
|
|
289
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
290
|
-
if (obj[key] !== undefined) {
|
|
291
|
-
errors.push(...validateValue(obj[key], propSchema, path ? `${path}.${key}` : key));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (schema.required) {
|
|
296
|
-
for (const key of schema.required) {
|
|
297
|
-
if (obj[key] === undefined) {
|
|
298
|
-
errors.push({
|
|
299
|
-
path: path ? `${path}.${key}` : key,
|
|
300
|
-
message: `Required property "${key}" is missing`,
|
|
301
|
-
value: undefined,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return errors;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Validate rev.yaml configuration
|
|
313
|
-
*/
|
|
314
|
-
export function validateConfig(config: Record<string, unknown>): ValidationResult {
|
|
315
|
-
const errors = validateValue(config, revYamlSchema);
|
|
316
|
-
const warnings: ValidationWarning[] = [];
|
|
317
|
-
|
|
318
|
-
// Additional semantic validations
|
|
319
|
-
if (config.sections && Array.isArray(config.sections) && config.sections.length === 0) {
|
|
320
|
-
warnings.push({
|
|
321
|
-
path: 'sections',
|
|
322
|
-
message: 'No sections specified - build will auto-detect .md files',
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (config.bibliography && typeof config.bibliography === 'string' && !config.bibliography.endsWith('.bib')) {
|
|
327
|
-
warnings.push({
|
|
328
|
-
path: 'bibliography',
|
|
329
|
-
message: 'Bibliography file should have .bib extension',
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const pdf = config.pdf as { linestretch?: number } | undefined;
|
|
334
|
-
if (pdf?.linestretch && (pdf.linestretch < 1 || pdf.linestretch > 3)) {
|
|
335
|
-
warnings.push({
|
|
336
|
-
path: 'pdf.linestretch',
|
|
337
|
-
message: 'Line stretch values outside 1-3 range may produce unexpected results',
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Check for common typos
|
|
342
|
-
const knownKeys = Object.keys(revYamlSchema.properties || {});
|
|
343
|
-
for (const key of Object.keys(config)) {
|
|
344
|
-
if (key.startsWith('_')) continue; // Internal keys
|
|
345
|
-
if (!knownKeys.includes(key)) {
|
|
346
|
-
// Check for similar keys (possible typos)
|
|
347
|
-
const similar = knownKeys.find(
|
|
348
|
-
(k) => levenshtein(key.toLowerCase(), k.toLowerCase()) <= 2
|
|
349
|
-
);
|
|
350
|
-
if (similar) {
|
|
351
|
-
warnings.push({
|
|
352
|
-
path: key,
|
|
353
|
-
message: `Unknown property "${key}" - did you mean "${similar}"?`,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
valid: errors.length === 0,
|
|
361
|
-
errors,
|
|
362
|
-
warnings,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Format validation results for display
|
|
368
|
-
*/
|
|
369
|
-
export function formatValidationResult(
|
|
370
|
-
result: ValidationResult,
|
|
371
|
-
chalk: { red: (s: string) => string; yellow: (s: string) => string; green: (s: string) => string }
|
|
372
|
-
): string {
|
|
373
|
-
const lines: string[] = [];
|
|
374
|
-
|
|
375
|
-
if (result.errors.length > 0) {
|
|
376
|
-
lines.push(chalk.red('Configuration errors:'));
|
|
377
|
-
for (const error of result.errors) {
|
|
378
|
-
lines.push(chalk.red(` ✗ ${error.path}: ${error.message}`));
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (result.warnings.length > 0) {
|
|
383
|
-
if (lines.length > 0) lines.push('');
|
|
384
|
-
lines.push(chalk.yellow('Warnings:'));
|
|
385
|
-
for (const warning of result.warnings) {
|
|
386
|
-
lines.push(chalk.yellow(` ! ${warning.path}: ${warning.message}`));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (result.valid && result.warnings.length === 0) {
|
|
391
|
-
lines.push(chalk.green('✓ Configuration is valid'));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return lines.join('\n');
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Levenshtein distance for typo detection
|
|
399
|
-
*/
|
|
400
|
-
function levenshtein(a: string, b: string): number {
|
|
401
|
-
const matrix = Array(b.length + 1)
|
|
402
|
-
.fill(null)
|
|
403
|
-
.map(() => Array(a.length + 1).fill(null));
|
|
404
|
-
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
|
|
405
|
-
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
|
|
406
|
-
for (let j = 1; j <= b.length; j++) {
|
|
407
|
-
for (let i = 1; i <= a.length; i++) {
|
|
408
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
409
|
-
matrix[j][i] = Math.min(
|
|
410
|
-
matrix[j][i - 1] + 1,
|
|
411
|
-
matrix[j - 1][i] + 1,
|
|
412
|
-
matrix[j - 1][i - 1] + cost
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return matrix[b.length][a.length];
|
|
417
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema validation for rev.yaml configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validation error
|
|
7
|
+
*/
|
|
8
|
+
interface ValidationError {
|
|
9
|
+
path: string;
|
|
10
|
+
message: string;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validation warning
|
|
16
|
+
*/
|
|
17
|
+
interface ValidationWarning {
|
|
18
|
+
path: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validation result
|
|
24
|
+
*/
|
|
25
|
+
interface ValidationResult {
|
|
26
|
+
valid: boolean;
|
|
27
|
+
errors: ValidationError[];
|
|
28
|
+
warnings: ValidationWarning[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* JSON Schema type
|
|
33
|
+
*/
|
|
34
|
+
interface Schema {
|
|
35
|
+
$schema?: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
type?: string;
|
|
39
|
+
properties?: Record<string, Schema>;
|
|
40
|
+
required?: string[];
|
|
41
|
+
items?: Schema;
|
|
42
|
+
oneOf?: Schema[];
|
|
43
|
+
enum?: string[];
|
|
44
|
+
pattern?: string;
|
|
45
|
+
format?: string;
|
|
46
|
+
minimum?: number;
|
|
47
|
+
maximum?: number;
|
|
48
|
+
minItems?: number;
|
|
49
|
+
maxItems?: number;
|
|
50
|
+
additionalProperties?: boolean;
|
|
51
|
+
default?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* JSON Schema for rev.yaml
|
|
56
|
+
*/
|
|
57
|
+
export const revYamlSchema: Schema = {
|
|
58
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
59
|
+
title: 'rev.yaml configuration',
|
|
60
|
+
description: 'Configuration file for docrev document workflow',
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
title: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Document title',
|
|
66
|
+
},
|
|
67
|
+
version: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Document version',
|
|
70
|
+
},
|
|
71
|
+
authors: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
description: 'List of authors',
|
|
74
|
+
items: {
|
|
75
|
+
oneOf: [
|
|
76
|
+
{ type: 'string' },
|
|
77
|
+
{
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
name: { type: 'string' },
|
|
81
|
+
affiliation: { type: 'string' },
|
|
82
|
+
email: { type: 'string', format: 'email' },
|
|
83
|
+
orcid: { type: 'string', pattern: '^\\d{4}-\\d{4}-\\d{4}-\\d{3}[0-9X]$' },
|
|
84
|
+
},
|
|
85
|
+
required: ['name'],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
journal: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Journal profile name for formatting defaults and validation',
|
|
93
|
+
},
|
|
94
|
+
sections: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
description: 'Ordered list of section files to include',
|
|
97
|
+
items: { type: 'string', pattern: '.*\\.md$' },
|
|
98
|
+
},
|
|
99
|
+
bibliography: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Path to bibliography file (.bib)',
|
|
102
|
+
pattern: '.*\\.bib$',
|
|
103
|
+
},
|
|
104
|
+
csl: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'Path to CSL citation style file',
|
|
107
|
+
},
|
|
108
|
+
crossref: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
description: 'pandoc-crossref settings',
|
|
111
|
+
properties: {
|
|
112
|
+
figureTitle: { type: 'string', default: 'Figure' },
|
|
113
|
+
tableTitle: { type: 'string', default: 'Table' },
|
|
114
|
+
figPrefix: {
|
|
115
|
+
oneOf: [
|
|
116
|
+
{ type: 'string' },
|
|
117
|
+
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
tblPrefix: {
|
|
121
|
+
oneOf: [
|
|
122
|
+
{ type: 'string' },
|
|
123
|
+
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
eqnPrefix: {
|
|
127
|
+
oneOf: [
|
|
128
|
+
{ type: 'string' },
|
|
129
|
+
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
secPrefix: {
|
|
133
|
+
oneOf: [
|
|
134
|
+
{ type: 'string' },
|
|
135
|
+
{ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
linkReferences: { type: 'boolean', default: true },
|
|
139
|
+
},
|
|
140
|
+
additionalProperties: true,
|
|
141
|
+
},
|
|
142
|
+
pdf: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
description: 'PDF output settings',
|
|
145
|
+
properties: {
|
|
146
|
+
template: { type: 'string' },
|
|
147
|
+
documentclass: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
enum: ['article', 'report', 'book', 'memoir', 'scrartcl', 'scrreprt', 'scrbook'],
|
|
150
|
+
default: 'article',
|
|
151
|
+
},
|
|
152
|
+
fontsize: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
pattern: '^\\d{1,2}pt$',
|
|
155
|
+
default: '12pt',
|
|
156
|
+
},
|
|
157
|
+
geometry: { type: 'string', default: 'margin=1in' },
|
|
158
|
+
linestretch: { type: 'number', minimum: 1, maximum: 3, default: 1.5 },
|
|
159
|
+
numbersections: { type: 'boolean', default: false },
|
|
160
|
+
toc: { type: 'boolean', default: false },
|
|
161
|
+
header: { type: 'string' },
|
|
162
|
+
footer: { type: 'string' },
|
|
163
|
+
},
|
|
164
|
+
additionalProperties: true,
|
|
165
|
+
},
|
|
166
|
+
docx: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
description: 'Word output settings',
|
|
169
|
+
properties: {
|
|
170
|
+
reference: { type: 'string', description: 'Reference document for styling' },
|
|
171
|
+
keepComments: { type: 'boolean', default: true },
|
|
172
|
+
toc: { type: 'boolean', default: false },
|
|
173
|
+
},
|
|
174
|
+
additionalProperties: true,
|
|
175
|
+
},
|
|
176
|
+
tex: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
description: 'LaTeX output settings',
|
|
179
|
+
properties: {
|
|
180
|
+
standalone: { type: 'boolean', default: true },
|
|
181
|
+
},
|
|
182
|
+
additionalProperties: true,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
additionalProperties: true,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validate a value against a simple schema
|
|
190
|
+
*/
|
|
191
|
+
function validateValue(value: unknown, schema: Schema, path = ''): ValidationError[] {
|
|
192
|
+
const errors: ValidationError[] = [];
|
|
193
|
+
|
|
194
|
+
// Handle oneOf
|
|
195
|
+
if (schema.oneOf) {
|
|
196
|
+
const validForAny = schema.oneOf.some((subSchema) => {
|
|
197
|
+
const subErrors = validateValue(value, subSchema, path);
|
|
198
|
+
return subErrors.length === 0;
|
|
199
|
+
});
|
|
200
|
+
if (!validForAny) {
|
|
201
|
+
errors.push({
|
|
202
|
+
path,
|
|
203
|
+
message: `Value does not match any allowed type`,
|
|
204
|
+
value,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return errors;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Type check
|
|
211
|
+
if (schema.type) {
|
|
212
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
213
|
+
if (actualType !== schema.type) {
|
|
214
|
+
errors.push({
|
|
215
|
+
path,
|
|
216
|
+
message: `Expected ${schema.type}, got ${actualType}`,
|
|
217
|
+
value,
|
|
218
|
+
});
|
|
219
|
+
return errors; // Stop further validation if type is wrong
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// String validation
|
|
224
|
+
if (schema.type === 'string' && typeof value === 'string') {
|
|
225
|
+
if (schema.pattern) {
|
|
226
|
+
const regex = new RegExp(schema.pattern);
|
|
227
|
+
if (!regex.test(value)) {
|
|
228
|
+
errors.push({
|
|
229
|
+
path,
|
|
230
|
+
message: `Value "${value}" does not match pattern ${schema.pattern}`,
|
|
231
|
+
value,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
236
|
+
errors.push({
|
|
237
|
+
path,
|
|
238
|
+
message: `Value "${value}" must be one of: ${schema.enum.join(', ')}`,
|
|
239
|
+
value,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Number validation
|
|
245
|
+
if (schema.type === 'number' && typeof value === 'number') {
|
|
246
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
247
|
+
errors.push({
|
|
248
|
+
path,
|
|
249
|
+
message: `Value ${value} is less than minimum ${schema.minimum}`,
|
|
250
|
+
value,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
254
|
+
errors.push({
|
|
255
|
+
path,
|
|
256
|
+
message: `Value ${value} is greater than maximum ${schema.maximum}`,
|
|
257
|
+
value,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Array validation
|
|
263
|
+
if (schema.type === 'array' && Array.isArray(value)) {
|
|
264
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
265
|
+
errors.push({
|
|
266
|
+
path,
|
|
267
|
+
message: `Array must have at least ${schema.minItems} items`,
|
|
268
|
+
value,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
272
|
+
errors.push({
|
|
273
|
+
path,
|
|
274
|
+
message: `Array must have at most ${schema.maxItems} items`,
|
|
275
|
+
value,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (schema.items) {
|
|
279
|
+
value.forEach((item, index) => {
|
|
280
|
+
errors.push(...validateValue(item, schema.items!, `${path}[${index}]`));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Object validation
|
|
286
|
+
if (schema.type === 'object' && typeof value === 'object' && value !== null) {
|
|
287
|
+
const obj = value as Record<string, unknown>;
|
|
288
|
+
if (schema.properties) {
|
|
289
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
290
|
+
if (obj[key] !== undefined) {
|
|
291
|
+
errors.push(...validateValue(obj[key], propSchema, path ? `${path}.${key}` : key));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (schema.required) {
|
|
296
|
+
for (const key of schema.required) {
|
|
297
|
+
if (obj[key] === undefined) {
|
|
298
|
+
errors.push({
|
|
299
|
+
path: path ? `${path}.${key}` : key,
|
|
300
|
+
message: `Required property "${key}" is missing`,
|
|
301
|
+
value: undefined,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return errors;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Validate rev.yaml configuration
|
|
313
|
+
*/
|
|
314
|
+
export function validateConfig(config: Record<string, unknown>): ValidationResult {
|
|
315
|
+
const errors = validateValue(config, revYamlSchema);
|
|
316
|
+
const warnings: ValidationWarning[] = [];
|
|
317
|
+
|
|
318
|
+
// Additional semantic validations
|
|
319
|
+
if (config.sections && Array.isArray(config.sections) && config.sections.length === 0) {
|
|
320
|
+
warnings.push({
|
|
321
|
+
path: 'sections',
|
|
322
|
+
message: 'No sections specified - build will auto-detect .md files',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (config.bibliography && typeof config.bibliography === 'string' && !config.bibliography.endsWith('.bib')) {
|
|
327
|
+
warnings.push({
|
|
328
|
+
path: 'bibliography',
|
|
329
|
+
message: 'Bibliography file should have .bib extension',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const pdf = config.pdf as { linestretch?: number } | undefined;
|
|
334
|
+
if (pdf?.linestretch && (pdf.linestretch < 1 || pdf.linestretch > 3)) {
|
|
335
|
+
warnings.push({
|
|
336
|
+
path: 'pdf.linestretch',
|
|
337
|
+
message: 'Line stretch values outside 1-3 range may produce unexpected results',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check for common typos
|
|
342
|
+
const knownKeys = Object.keys(revYamlSchema.properties || {});
|
|
343
|
+
for (const key of Object.keys(config)) {
|
|
344
|
+
if (key.startsWith('_')) continue; // Internal keys
|
|
345
|
+
if (!knownKeys.includes(key)) {
|
|
346
|
+
// Check for similar keys (possible typos)
|
|
347
|
+
const similar = knownKeys.find(
|
|
348
|
+
(k) => levenshtein(key.toLowerCase(), k.toLowerCase()) <= 2
|
|
349
|
+
);
|
|
350
|
+
if (similar) {
|
|
351
|
+
warnings.push({
|
|
352
|
+
path: key,
|
|
353
|
+
message: `Unknown property "${key}" - did you mean "${similar}"?`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
valid: errors.length === 0,
|
|
361
|
+
errors,
|
|
362
|
+
warnings,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Format validation results for display
|
|
368
|
+
*/
|
|
369
|
+
export function formatValidationResult(
|
|
370
|
+
result: ValidationResult,
|
|
371
|
+
chalk: { red: (s: string) => string; yellow: (s: string) => string; green: (s: string) => string }
|
|
372
|
+
): string {
|
|
373
|
+
const lines: string[] = [];
|
|
374
|
+
|
|
375
|
+
if (result.errors.length > 0) {
|
|
376
|
+
lines.push(chalk.red('Configuration errors:'));
|
|
377
|
+
for (const error of result.errors) {
|
|
378
|
+
lines.push(chalk.red(` ✗ ${error.path}: ${error.message}`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (result.warnings.length > 0) {
|
|
383
|
+
if (lines.length > 0) lines.push('');
|
|
384
|
+
lines.push(chalk.yellow('Warnings:'));
|
|
385
|
+
for (const warning of result.warnings) {
|
|
386
|
+
lines.push(chalk.yellow(` ! ${warning.path}: ${warning.message}`));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (result.valid && result.warnings.length === 0) {
|
|
391
|
+
lines.push(chalk.green('✓ Configuration is valid'));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return lines.join('\n');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Levenshtein distance for typo detection
|
|
399
|
+
*/
|
|
400
|
+
function levenshtein(a: string, b: string): number {
|
|
401
|
+
const matrix = Array(b.length + 1)
|
|
402
|
+
.fill(null)
|
|
403
|
+
.map(() => Array(a.length + 1).fill(null));
|
|
404
|
+
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
|
|
405
|
+
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
|
|
406
|
+
for (let j = 1; j <= b.length; j++) {
|
|
407
|
+
for (let i = 1; i <= a.length; i++) {
|
|
408
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
409
|
+
matrix[j][i] = Math.min(
|
|
410
|
+
matrix[j][i - 1] + 1,
|
|
411
|
+
matrix[j - 1][i] + 1,
|
|
412
|
+
matrix[j - 1][i - 1] + cost
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return matrix[b.length][a.length];
|
|
417
|
+
}
|