"use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); const crypto = require("crypto"); const { spawn } = require("child_process"); const SOURCE_EXTENSIONS = new Set([".puml", ".plantuml", ".pu", ".iuml"]); function isPlantUmlSource(filePath) { if (!filePath) return false; return SOURCE_EXTENSIONS.has(path.extname(String(filePath)).toLowerCase()); } function resolveJava(javaCfg) { if (javaCfg && javaCfg !== "auto") return javaCfg; const home = process.env.JAVA_HOME; if (home) { const exe = process.platform === "win32" ? "java.exe" : "java"; const candidate = path.join(home, "bin", exe); if (fs.existsSync(candidate)) return candidate; } return "java"; } function runProcess(command, args, input, timeoutMs) { return new Promise((resolve, reject) => { let child; try { child = spawn(command, args, { windowsHide: true }); } catch (err) { reject(new Error(`PlantUML: не удалось запустить "${command}": ${err.message}`)); return; } const stdout = []; const stderr = []; let settled = false; let timer = null; const finish = (fn, arg) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); fn(arg); }; if (timeoutMs > 0) { timer = setTimeout(() => { child.kill(); finish(reject, new Error(`PlantUML: превышено время ожидания (${timeoutMs} мс)`)); }, timeoutMs); } child.on("error", (err) => { finish(reject, new Error(`PlantUML: не удалось запустить "${command}": ${err.message}`)); }); child.stdout.on("data", (chunk) => stdout.push(chunk)); child.stderr.on("data", (chunk) => stderr.push(chunk)); child.on("close", (code) => { finish(resolve, { code, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr).toString("utf8").trim(), }); }); child.stdin.end(input == null ? "" : input); }); } async function renderPlantUml(sourceBuffer, cfg) { if (!cfg.jar) throw new Error("PlantUML: не задан путь к plantuml.jar"); if (!fs.existsSync(cfg.jar)) { throw new Error(`PlantUML: plantuml.jar не найден: ${cfg.jar}`); } const java = resolveJava(cfg.java); const args = [ "-jar", cfg.jar, "-pipe", `-t${cfg.format || "png"}`, "-charset", cfg.charset || "UTF-8", "-failfast2", "-nometadata", ]; const result = await runProcess(java, args, sourceBuffer, cfg.timeoutMs); if (result.code !== 0 || result.stdout.length === 0) { const detail = result.stderr || `код выхода ${result.code}`; throw new Error(`PlantUML: ошибка рендеринга: ${detail}`); } return result.stdout; } function jarFingerprint(cfg) { try { const st = fs.statSync(cfg.jar); return `${st.size}:${Math.round(st.mtimeMs)}`; } catch { return "nojar"; } } function cacheFileName(sourceBuffer, cfg) { const hash = crypto .createHash("sha1") .update(sourceBuffer) .update(`|${cfg.format || "png"}`) .update(`|${jarFingerprint(cfg)}`) .digest("hex"); return `${hash}.png`; } async function renderCached(sourceBuffer, cfg) { const dir = cfg.cacheDir || path.join(os.tmpdir(), "ssau-generator-pptx-plantuml"); const target = path.join(dir, cacheFileName(sourceBuffer, cfg)); if (cfg.cache && fs.existsSync(target)) { return { buffer: fs.readFileSync(target), cachePath: target }; } const buffer = await renderPlantUml(sourceBuffer, cfg); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(target, buffer); return { buffer, cachePath: target }; } /** * Проставляет каждому блоку ::: image путь к готовому изображению. * Для исходников PlantUML рендерит PNG; для растровых — проверяет наличие. * @returns {Promise} список проблем (ошибок/предупреждений) */ async function resolveImages(config, deck) { const issues = []; const cfg = config.plantuml || {}; const contentDir = config.content.dir; for (const entry of deck) { for (const block of entry.parsed.blocks) { if (block.kind !== "image" || !block.file) continue; const source = path.isAbsolute(block.file) ? block.file : path.resolve(contentDir, block.file); if (isPlantUmlSource(source)) { if (cfg.enabled === false) { issues.push(`PlantUML отключён, а файл требует рендеринга: ${block.file}`); continue; } if (!fs.existsSync(source)) { issues.push(`Не найден исходник PlantUML: ${block.file}`); continue; } try { const { cachePath } = await renderCached(fs.readFileSync(source), { ...cfg, jar: cfg.jar, }); block.renderFile = cachePath; } catch (err) { issues.push(`Рисунок "${block.file}": ${err.message}`); } } else { if (!fs.existsSync(source)) { issues.push(`Не найден файл изображения: ${block.file}`); continue; } block.renderFile = source; } } } return issues; } module.exports = { isPlantUmlSource, resolveJava, renderPlantUml, renderCached, resolveImages };