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
This commit is contained in:
2026-10-11 23:25:22 +04:00
parent 35611e2a91
commit bf7816ab7c
80 changed files with 1166 additions and 3432 deletions

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { build } = require("./build");
const { ensureScaffold } = require("./scaffold");
function parseArgs(argv) {
const options = { config: "config.yaml" };
@@ -20,6 +22,9 @@ function parseArgs(argv) {
case "--check":
options.check = true;
break;
case "--init":
options.init = true;
break;
case "--verbose":
case "-v":
options.verbose = true;
@@ -43,15 +48,16 @@ function parseArgs(argv) {
}
function printHelp() {
console.log(`lab-gen-pptx — генератор презентаций PPTX из Markdown
console.log(`SSAU Generator PPTX — генератор презентаций PPTX из Markdown
Использование:
lab-gen-pptx [config.yaml] [опции]
node pptx/src/cli.js [config.yaml] [опции]
Опции:
-c, --config <file> путь к config.yaml (по умолчанию config.yaml)
-o, --output <file> переопределить имя выходного файла
--check только проверить конфигурацию и ссылки
--init создать шаблон config.yaml/slides/assets и выйти
-v, --verbose подробный вывод
-h, --help показать справку
--version показать версию
@@ -68,6 +74,26 @@ async function main() {
}
const configPath = path.resolve(options.config);
if (options.init || !fs.existsSync(configPath)) {
const targetDir = path.dirname(configPath);
try {
const created = await ensureScaffold(targetDir);
if (created.length) {
console.log("Создан шаблон генератора:");
created.forEach((file) => console.log(" " + path.relative(targetDir, file)));
} else {
console.log("Файлы уже существуют, ничего не создано.");
}
console.log("");
console.log("Отредактируйте config.yaml и файлы в slides/, затем запустите генератор снова.");
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
return undefined;
}
try {
const result = await build({ config: configPath, check: options.check, output: options.output });
const { config, issues } = result;

View File

@@ -24,9 +24,9 @@ const DEFAULTS = {
line: "BFD3DD",
},
backgrounds: {
title: "content/assets/title-bg.jpeg",
content: "content/assets/content-bg.jpg",
final: "content/assets/final-bg.jpeg",
title: "assets/title-bg.jpeg",
content: "assets/content-bg.jpg",
final: "assets/final-bg.jpeg",
},
slideNumber: {
show: true,
@@ -110,7 +110,7 @@ const DEFAULTS = {
timeoutMs: 30000,
},
content: {
dir: "content",
dir: ".",
},
slides: [],
};

View File

@@ -114,7 +114,7 @@ function cacheFileName(sourceBuffer, cfg) {
}
async function renderCached(sourceBuffer, cfg) {
const dir = cfg.cacheDir || path.join(os.tmpdir(), "lab-gen-pptx-plantuml");
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)) {

68
pptx/src/png.js Normal file
View File

@@ -0,0 +1,68 @@
"use strict";
const zlib = require("zlib");
const CRC_TABLE = (() => {
const table = new Int32Array(256);
for (let n = 0; n < 256; n += 1) {
let c = n;
for (let k = 0; k < 8; k += 1) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[n] = c;
}
return table;
})();
function crc32(buffer) {
let crc = -1;
for (let i = 0; i < buffer.length; i += 1) {
crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ -1) >>> 0;
}
function chunk(type, data) {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length, 0);
const typeBuf = Buffer.from(type, "ascii");
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
return Buffer.concat([length, typeBuf, data, crc]);
}
/** Кодирует PNG, получая цвет пикселя через getPixel(x, y) -> [r,g,b]. */
function encodePng(width, height, getPixel) {
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 2;
const rowLength = width * 3;
const raw = Buffer.alloc((rowLength + 1) * height);
for (let y = 0; y < height; y += 1) {
const offset = y * (rowLength + 1);
for (let x = 0; x < width; x += 1) {
const [r, g, b] = getPixel(x, y);
const px = offset + 1 + x * 3;
raw[px] = r;
raw[px + 1] = g;
raw[px + 2] = b;
}
}
return Buffer.concat([
signature,
chunk("IHDR", ihdr),
chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0)),
]);
}
function solidPng(width, height, color) {
return encodePng(width, height, () => color);
}
module.exports = { solidPng, encodePng };

115
pptx/src/scaffold.js Normal file
View File

@@ -0,0 +1,115 @@
"use strict";
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const { DEFAULTS, deepMerge } = require("./config");
const { solidPng } = require("./png");
const CONFIG_HEADER = `# Конфигурация генератора SSAU Generator PPTX.
# Все относительные пути считаются от каталога этого файла.
# Любой ключ можно опустить — будет использовано значение по умолчанию.
`;
const SAMPLE = {
"slides/00-title.md": `# Название презентации
## Подзаголовок
`,
"slides/01-content.md": `# Первый слайд
- первый пункт списка;
- второй пункт списка:
- вложенный пункт;
Обычный абзац с **полужирным**, *курсивом* и ==выделением==.
`,
"slides/02-final.md": `# Спасибо за внимание
`,
};
// Цвета задников: [R, G, B].
const BRAND = [27, 135, 201];
const BACKGROUNDS = {
"title-bg.png": BRAND,
"content-bg.png": [255, 255, 255],
"final-bg.png": BRAND,
};
function isGeneratorDir(baseDir) {
const base = path.basename(baseDir).toLowerCase();
return base === "docx" || base === "pptx";
}
/**
* Раскладка проекта: имя проекта = имя папки проекта, общие ресурсы в
* content/<проект>/assets/, результат — content/<проект>/<проект>.pptx.
*/
function projectLayout(baseDir) {
const isSub = isGeneratorDir(baseDir);
const projectDir = isSub ? path.dirname(baseDir) : baseDir;
const projectName = path.basename(projectDir) || "project";
return {
projectDir,
projectName,
assetsRel: isSub ? "../assets" : "assets",
assetsDir: path.join(projectDir, "assets"),
outputRel: isSub ? `../${projectName}.pptx` : `${projectName}.pptx`,
};
}
function defaultConfigYaml(layout) {
const config = deepMerge(DEFAULTS, {
presentation: { output: layout.outputRel },
theme: {
backgrounds: {
title: `${layout.assetsRel}/title-bg.png`,
content: `${layout.assetsRel}/content-bg.png`,
final: `${layout.assetsRel}/final-bg.png`,
},
},
content: { dir: "." },
slides: [
{ type: "title", file: "slides/00-title.md" },
{ type: "content", file: "slides/01-content.md" },
{ type: "final", file: "slides/02-final.md" },
],
});
return (
CONFIG_HEADER +
yaml.dump(config, { noRefs: true, lineWidth: -1, sortKeys: false })
);
}
/**
* Создаёт config.yaml, примеры слайдов и общие ресурсы проекта
* (content/<проект>/assets/), если их ещё нет.
*/
async function ensureScaffold(baseDir) {
const layout = projectLayout(baseDir);
const created = [];
const write = (target, content) => {
if (fs.existsSync(target)) return;
fs.mkdirSync(path.dirname(target), { recursive: true });
if (content && typeof content === "object" && content.binary) {
fs.writeFileSync(target, content.binary);
} else {
fs.writeFileSync(target, content, "utf8");
}
created.push(target);
};
write(path.join(baseDir, "config.yaml"), defaultConfigYaml(layout));
for (const [name, content] of Object.entries(SAMPLE)) {
write(path.join(baseDir, name), content);
}
for (const [name, color] of Object.entries(BACKGROUNDS)) {
write(path.join(layout.assetsDir, name), { binary: solidPng(1920, 1080, color) });
}
return created;
}
module.exports = { projectLayout, defaultConfigYaml, ensureScaffold, SAMPLE };