Files
SSAUGenerator/pptx/src/plantuml.js
StepanovPlaton bf7816ab7c Rename project to SSAU Generator and restructure content
- Move projects into content/<project>/{docx,pptx} with shared assets/
- Flatten config paths; output = project name in project folder
- Add pptx --init scaffold; remove docx exe build
- Add feature-complete content/example; ignore content/* except example
- Rewrite README and AGENTS.md
2026-10-11 23:25:22 +04:00

175 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<string[]>} список проблем (ошибок/предупреждений)
*/
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 };