This commit is contained in:
2026-10-08 06:49:13 +04:00
parent 41d434ef97
commit 35611e2a91
84 changed files with 3430 additions and 375 deletions

120
docx/src/config/validate.js Normal file
View File

@@ -0,0 +1,120 @@
'use strict';
const fs = require('fs');
const SECTION_TYPES = ['toc', 'structural', 'numbered', 'bibliography', 'appendix'];
const NUMBERING_SCOPES = ['global', 'section'];
function isPositiveInt(value) {
return Number.isInteger(value) && value > 0;
}
function validateConfig(config) {
const errors = [];
const { document: doc, typography, page, toc, appendix, numbering } = config;
if (!Array.isArray(config.sections) || config.sections.length === 0) {
errors.push('sections: должен быть непустой список частей документа');
} else {
const letters = new Set();
config.sections.forEach((entry, index) => {
const where = `sections[${index}]`;
if (!entry || typeof entry !== 'object') {
errors.push(`${where}: ожидался объект с полем type`);
return;
}
if (!SECTION_TYPES.includes(entry.type)) {
errors.push(
`${where}: неизвестный type "${entry.type}" (допустимо: ${SECTION_TYPES.join(', ')})`
);
return;
}
if (entry.type === 'toc') {
if (entry.file) errors.push(`${where}: часть type=toc не должна иметь file`);
return;
}
if (!entry.file) {
errors.push(`${where}: отсутствует file`);
} else if (!fs.existsSync(entry.file)) {
errors.push(`${where}: файл не найден: ${entry.file}`);
}
if (entry.type === 'structural' && !entry.title) {
errors.push(`${where}: для structural требуется title`);
}
if (entry.type === 'appendix') {
if (!entry.letter || String(entry.letter).length !== 1) {
errors.push(`${where}: для appendix требуется одиночная буква в letter`);
} else {
const letter = String(entry.letter).toUpperCase();
if (appendix.forbiddenLetters.map((l) => l.toUpperCase()).includes(letter)) {
errors.push(`${where}: буква приложения "${letter}" запрещена СТО`);
}
if (letters.has(letter)) errors.push(`${where}: повтор буквы приложения "${letter}"`);
letters.add(letter);
}
}
});
}
if (doc.titlePage && !fs.existsSync(doc.titlePage)) {
errors.push(`document.titlePage: файл не найден: ${doc.titlePage}`);
}
for (const key of ['sizeBody', 'sizeTable', 'sizeSmall']) {
if (!isPositiveInt(typography[key])) {
errors.push(`typography.${key}: ожидалось положительное целое (half-points)`);
}
}
if (!(typeof typography.lineSpacing === 'number' && typography.lineSpacing > 0)) {
errors.push('typography.lineSpacing: ожидалось положительное число');
}
for (const [key, value] of Object.entries(page.margins)) {
if (!Number.isInteger(value) || value < 0) {
errors.push(`page.margins.${key}: ожидалось неотрицательное целое (twips)`);
}
}
if (!isPositiveInt(toc.depth) || toc.depth > 3) {
errors.push('toc.depth: ожидалось 1..3');
}
if (typeof toc.enabled !== 'boolean') {
errors.push('toc.enabled: ожидалось true/false');
}
for (const key of ['figures', 'tables', 'formulas']) {
if (!NUMBERING_SCOPES.includes(numbering[key])) {
errors.push(`numbering.${key}: допустимо ${NUMBERING_SCOPES.join(' | ')}`);
}
}
if (config.plantuml) {
const p = config.plantuml;
if (typeof p.enabled !== 'boolean') {
errors.push('plantuml.enabled: ожидалось true/false');
}
if (p.format !== 'png') {
errors.push(`plantuml.format: поддерживается только "png", получено "${p.format}"`);
}
if (typeof p.jar !== 'string' || !p.jar) {
errors.push('plantuml.jar: ожидался путь к plantuml.jar');
}
if (typeof p.java !== 'string' || !p.java) {
errors.push('plantuml.java: ожидалось имя или путь к java (или "auto")');
}
if (p.cacheDir != null && typeof p.cacheDir !== 'string') {
errors.push('plantuml.cacheDir: ожидалась строка');
}
if (!isPositiveInt(p.timeoutMs)) {
errors.push('plantuml.timeoutMs: ожидалось положительное целое (мс)');
}
if (typeof p.cache !== 'boolean') {
errors.push('plantuml.cache: ожидалось true/false');
}
}
return errors;
}
module.exports = { validateConfig, SECTION_TYPES };