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

174
pptx/src/plantuml.js Normal file
View File

@@ -0,0 +1,174 @@
"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(), "lab-gen-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 };