mirror of
https://github.com/StepanovPlaton/SSAUGenerator.git
synced 2026-10-31 10:18:29 +04:00
- 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
175 lines
5.4 KiB
JavaScript
175 lines
5.4 KiB
JavaScript
"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 };
|