'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: ожидалось положительное число'); } if (typography.language != null && typeof typography.language !== 'string') { errors.push('typography.language: ожидалась строка (например, ru-RU)'); } 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 };