@hirokisakabe/pom-cli 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +6 -40
- package/dist/cli.js +52 -12
- package/dist/glimpse.d.ts +3 -0
- package/dist/glimpse.d.ts.map +1 -0
- package/dist/glimpse.js +15 -0
- package/dist/input.d.ts +8 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +37 -0
- package/dist/preview.d.ts +1 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +53 -51
- package/dist/render.d.ts +7 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +71 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +37 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hirokisakabe/pom-cli
|
|
2
2
|
|
|
3
|
-
CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview and
|
|
3
|
+
CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview, build, and render presentations from pom XML / Markdown.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,6 +8,16 @@ CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview and build pr
|
|
|
8
8
|
npm install -g @hirokisakabe/pom-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
One command is all it takes — no global install required:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @hirokisakabe/pom-cli preview slides.pom.xml
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This starts a local preview server, opens your browser automatically, and live-reloads the preview every time the file is saved. This also works well when invoked from agent skills or scripts.
|
|
20
|
+
|
|
11
21
|
## Usage
|
|
12
22
|
|
|
13
23
|
### Preview
|
|
@@ -19,7 +29,13 @@ pom preview slides.pom.xml
|
|
|
19
29
|
pom preview slides.pom.md
|
|
20
30
|
```
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
The browser opens http://localhost:3000 automatically. The page updates whenever the file is saved — including atomic saves performed by editors like Vim.
|
|
33
|
+
|
|
34
|
+
To suppress the automatic browser open (e.g. in CI or headless environments):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pom preview slides.pom.xml --no-open
|
|
38
|
+
```
|
|
23
39
|
|
|
24
40
|
To use a different port (e.g. when 3000 is already in use):
|
|
25
41
|
|
|
@@ -67,9 +83,38 @@ To print per-step timing on stderr:
|
|
|
67
83
|
pom build slides.pom.xml -o output.pptx --verbose
|
|
68
84
|
```
|
|
69
85
|
|
|
86
|
+
### Render
|
|
87
|
+
|
|
88
|
+
Renders each slide to a PNG (default) or SVG image — no LibreOffice or other external tools required.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pom render slides.pom.xml -o ./images
|
|
92
|
+
pom render slides.pom.md -o ./images
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The images are written to the output directory as `slide-01.png`, `slide-02.png`, ... The directory is created if it does not exist. The rendering pipeline is the same as the preview server, so the images match what you see in `pom preview`.
|
|
96
|
+
|
|
97
|
+
To output SVG instead of PNG:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pom render slides.pom.xml -o ./images --format svg
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
To render only specific slides (1-based, comma-separated) — useful when re-checking just the slides you edited:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pom render slides.pom.xml -o ./images --slides 2,5
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
To print per-step timing on stderr:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pom render slides.pom.xml -o ./images --verbose
|
|
113
|
+
```
|
|
114
|
+
|
|
70
115
|
## Fonts
|
|
71
116
|
|
|
72
|
-
This package bundles Carlito and Noto Sans CJK JP fonts for
|
|
117
|
+
This package bundles Carlito and Noto Sans CJK JP fonts for image rendering. These fonts are used when converting slides to SVG in the preview server and to PNG / SVG in `pom render`. System fonts are not scanned.
|
|
73
118
|
|
|
74
119
|
## License
|
|
75
120
|
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAWA,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAuDf"}
|
package/dist/build.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { buildPptx, DiagnosticsError } from "@hirokisakabe/pom";
|
|
4
|
-
import {
|
|
4
|
+
import { loadInput } from "./input.js";
|
|
5
|
+
import { watchInputFile } from "./watch.js";
|
|
5
6
|
function makeLog(verbose) {
|
|
6
7
|
if (!verbose)
|
|
7
8
|
return (_msg) => { };
|
|
@@ -17,37 +18,7 @@ export async function runBuild(inputFile, outputFile, options = {}) {
|
|
|
17
18
|
throw new Error(`Input file not found: ${absInput}`);
|
|
18
19
|
}
|
|
19
20
|
log(`Reading file: ${absInput}`);
|
|
20
|
-
const
|
|
21
|
-
const ext = path.extname(absInput);
|
|
22
|
-
let xml;
|
|
23
|
-
let slideWidth = 1280;
|
|
24
|
-
let slideHeight = 720;
|
|
25
|
-
let masterPptxData;
|
|
26
|
-
if (ext === ".md") {
|
|
27
|
-
const t = Date.now();
|
|
28
|
-
const result = parseMd(content);
|
|
29
|
-
xml = result.xml;
|
|
30
|
-
slideWidth = result.meta.size.w;
|
|
31
|
-
slideHeight = result.meta.size.h;
|
|
32
|
-
log(`Parsing Markdown... done (${Date.now() - t}ms)`);
|
|
33
|
-
if (result.meta.masterPptx) {
|
|
34
|
-
const masterPath = path.resolve(path.dirname(absInput), result.meta.masterPptx);
|
|
35
|
-
try {
|
|
36
|
-
masterPptxData = new Uint8Array(fs.readFileSync(masterPath));
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
|
|
40
|
-
console.warn(`Warning: masterPptx not found: ${masterPath}`);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
throw e;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
xml = content;
|
|
50
|
-
}
|
|
21
|
+
const { xml, slideWidth, slideHeight, masterPptxData } = loadInput(absInput, log);
|
|
51
22
|
const t1 = Date.now();
|
|
52
23
|
const { pptx } = await buildPptx(xml, { w: slideWidth, h: slideHeight }, {
|
|
53
24
|
...(masterPptxData ? { masterPptx: masterPptxData } : {}),
|
|
@@ -110,13 +81,8 @@ export async function runBuildWatch(inputFile, outputFile, options = {}) {
|
|
|
110
81
|
}
|
|
111
82
|
}
|
|
112
83
|
await doBuild();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
clearTimeout(debounceTimer);
|
|
117
|
-
debounceTimer = setTimeout(() => {
|
|
118
|
-
watchLog("File changed, rebuilding...");
|
|
119
|
-
void doBuild();
|
|
120
|
-
}, 100);
|
|
84
|
+
watchInputFile(absInput, () => {
|
|
85
|
+
watchLog("File changed, rebuilding...");
|
|
86
|
+
void doBuild();
|
|
121
87
|
});
|
|
122
88
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,25 @@ import { Command } from "commander";
|
|
|
4
4
|
import { DiagnosticsError } from "@hirokisakabe/pom";
|
|
5
5
|
import { runBuild, runBuildWatch } from "./build.js";
|
|
6
6
|
import { runPreview } from "./preview.js";
|
|
7
|
+
import { runRender } from "./render.js";
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const { version } = require("../package.json");
|
|
9
10
|
const program = new Command();
|
|
11
|
+
function printBuildError(err) {
|
|
12
|
+
if (err instanceof DiagnosticsError) {
|
|
13
|
+
const count = err.diagnostics.length;
|
|
14
|
+
console.error(`✗ Build failed (${count} ${count === 1 ? "error" : "errors"})\n`);
|
|
15
|
+
for (const d of err.diagnostics) {
|
|
16
|
+
console.error(` [${d.code}] ${d.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
10
23
|
program
|
|
11
24
|
.name("pom")
|
|
12
|
-
.description("CLI tool for pom — preview and
|
|
25
|
+
.description("CLI tool for pom — preview, build, and render presentations")
|
|
13
26
|
.version(version);
|
|
14
27
|
program
|
|
15
28
|
.command("preview")
|
|
@@ -17,6 +30,7 @@ program
|
|
|
17
30
|
.argument("<input>", "Input file (.pom.xml or .pom.md)")
|
|
18
31
|
.option("--port <number>", "Port to listen on")
|
|
19
32
|
.option("--verbose", "Show build step timing on stderr")
|
|
33
|
+
.option("--no-open", "Do not open the browser automatically")
|
|
20
34
|
.action((input, options) => {
|
|
21
35
|
let port;
|
|
22
36
|
if (options.port !== undefined) {
|
|
@@ -27,7 +41,10 @@ program
|
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
try {
|
|
30
|
-
runPreview(input, port, {
|
|
44
|
+
runPreview(input, port, {
|
|
45
|
+
verbose: options.verbose,
|
|
46
|
+
open: options.open,
|
|
47
|
+
});
|
|
31
48
|
}
|
|
32
49
|
catch (err) {
|
|
33
50
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -50,18 +67,41 @@ program
|
|
|
50
67
|
}
|
|
51
68
|
else {
|
|
52
69
|
runBuild(input, options.o, { verbose: options.verbose }).catch((err) => {
|
|
53
|
-
|
|
54
|
-
const count = err.diagnostics.length;
|
|
55
|
-
console.error(`✗ Build failed (${count} ${count === 1 ? "error" : "errors"})\n`);
|
|
56
|
-
for (const d of err.diagnostics) {
|
|
57
|
-
console.error(` [${d.code}] ${d.message}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
62
|
-
}
|
|
70
|
+
printBuildError(err);
|
|
63
71
|
process.exit(1);
|
|
64
72
|
});
|
|
65
73
|
}
|
|
66
74
|
});
|
|
75
|
+
program
|
|
76
|
+
.command("render")
|
|
77
|
+
.description("Render each slide to a PNG or SVG image")
|
|
78
|
+
.argument("<input>", "Input file (.pom.xml or .pom.md)")
|
|
79
|
+
.requiredOption("-o <dir>", "Output directory for rendered images")
|
|
80
|
+
.option("--format <format>", "Output format: png or svg", "png")
|
|
81
|
+
.option("--slides <numbers>", "Comma-separated slide numbers to render (e.g. 2,5)")
|
|
82
|
+
.option("--verbose", "Show build step timing on stderr")
|
|
83
|
+
.action((input, options) => {
|
|
84
|
+
if (options.format !== "png" && options.format !== "svg") {
|
|
85
|
+
console.error(`Invalid format: ${options.format} (expected "png" or "svg")`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const format = options.format;
|
|
89
|
+
let slides;
|
|
90
|
+
if (options.slides !== undefined) {
|
|
91
|
+
slides = options.slides.split(",").map((s) => Number(s.trim()));
|
|
92
|
+
if (slides.length === 0 ||
|
|
93
|
+
slides.some((n) => !Number.isInteger(n) || n <= 0)) {
|
|
94
|
+
console.error(`Invalid slides: ${options.slides} (expected comma-separated slide numbers, e.g. 2,5)`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
runRender(input, options.o, {
|
|
99
|
+
format,
|
|
100
|
+
slides,
|
|
101
|
+
verbose: options.verbose,
|
|
102
|
+
}).catch((err) => {
|
|
103
|
+
printBuildError(err);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
67
107
|
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glimpse.d.ts","sourceRoot":"","sources":["../src/glimpse.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGrD,CAAC;AAEF,wBAAgB,sBAAsB,IAAI,MAAM,CAQ/C"}
|
package/dist/glimpse.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
export const EXTRA_FONT_MAPPING = {
|
|
6
|
+
"游ゴシック Light": "Noto Sans CJK JP",
|
|
7
|
+
"Yu Gothic Light": "Noto Sans CJK JP",
|
|
8
|
+
};
|
|
9
|
+
export function resolveBundledFontsDir() {
|
|
10
|
+
const fontsDir = path.resolve(__dirname, "../fonts");
|
|
11
|
+
if (!fs.existsSync(fontsDir)) {
|
|
12
|
+
throw new Error(`Bundled fonts directory not found: ${fontsDir}. The package may be corrupted.`);
|
|
13
|
+
}
|
|
14
|
+
return fontsDir;
|
|
15
|
+
}
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,UAAU,CAAC;CAC7B;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAe,GACpC,WAAW,CAqCb"}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { parseMd } from "@hirokisakabe/pom-md";
|
|
4
|
+
export function loadInput(absInput, log = () => { }) {
|
|
5
|
+
const content = fs.readFileSync(absInput, "utf-8");
|
|
6
|
+
const ext = path.extname(absInput);
|
|
7
|
+
let xml;
|
|
8
|
+
let slideWidth = 1280;
|
|
9
|
+
let slideHeight = 720;
|
|
10
|
+
let masterPptxData;
|
|
11
|
+
if (ext === ".md") {
|
|
12
|
+
const t = Date.now();
|
|
13
|
+
const result = parseMd(content);
|
|
14
|
+
xml = result.xml;
|
|
15
|
+
slideWidth = result.meta.size.w;
|
|
16
|
+
slideHeight = result.meta.size.h;
|
|
17
|
+
log(`Parsing Markdown... done (${Date.now() - t}ms)`);
|
|
18
|
+
if (result.meta.masterPptx) {
|
|
19
|
+
const masterPath = path.resolve(path.dirname(absInput), result.meta.masterPptx);
|
|
20
|
+
try {
|
|
21
|
+
masterPptxData = new Uint8Array(fs.readFileSync(masterPath));
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
|
|
25
|
+
console.warn(`Warning: masterPptx not found: ${masterPath}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
xml = content;
|
|
35
|
+
}
|
|
36
|
+
return { xml, slideWidth, slideHeight, masterPptxData };
|
|
37
|
+
}
|
package/dist/preview.d.ts
CHANGED
package/dist/preview.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAyWA,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,MAAqB,EAC3B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,IAAI,CAmIN"}
|
package/dist/preview.js
CHANGED
|
@@ -1,54 +1,43 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
1
2
|
import fs from "fs";
|
|
2
3
|
import http from "http";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { buildPptx } from "@hirokisakabe/pom";
|
|
6
|
-
import { parseMd } from "@hirokisakabe/pom-md";
|
|
7
6
|
import { convertPptxToSvg } from "pptx-glimpse";
|
|
8
|
-
|
|
7
|
+
import { EXTRA_FONT_MAPPING, resolveBundledFontsDir } from "./glimpse.js";
|
|
8
|
+
import { loadInput } from "./input.js";
|
|
9
|
+
import { watchInputFile } from "./watch.js";
|
|
9
10
|
const DEFAULT_PORT = 3000;
|
|
10
11
|
function makeLog(verbose) {
|
|
11
12
|
if (!verbose)
|
|
12
13
|
return (_msg) => { };
|
|
13
14
|
return (msg) => process.stderr.write(`[pom] ${msg}\n`);
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let masterPptxData;
|
|
27
|
-
if (ext === ".md") {
|
|
28
|
-
const t = Date.now();
|
|
29
|
-
const result = parseMd(content);
|
|
30
|
-
xml = result.xml;
|
|
31
|
-
slideWidth = result.meta.size.w;
|
|
32
|
-
slideHeight = result.meta.size.h;
|
|
33
|
-
log(`Parsing Markdown... done (${Date.now() - t}ms)`);
|
|
34
|
-
if (result.meta.masterPptx) {
|
|
35
|
-
const masterPath = path.resolve(path.dirname(inputFile), result.meta.masterPptx);
|
|
36
|
-
try {
|
|
37
|
-
masterPptxData = new Uint8Array(fs.readFileSync(masterPath));
|
|
38
|
-
}
|
|
39
|
-
catch (e) {
|
|
40
|
-
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
|
|
41
|
-
process.stderr.write(`Warning: masterPptx not found: ${masterPath}\n`);
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
throw e;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
16
|
+
function openBrowser(url) {
|
|
17
|
+
let cmd;
|
|
18
|
+
let args;
|
|
19
|
+
if (process.platform === "darwin") {
|
|
20
|
+
cmd = "open";
|
|
21
|
+
args = [url];
|
|
22
|
+
}
|
|
23
|
+
else if (process.platform === "win32") {
|
|
24
|
+
// 空文字はウィンドウタイトル。省略すると URL がタイトル扱いされる
|
|
25
|
+
cmd = "cmd";
|
|
26
|
+
args = ["/c", "start", "", url];
|
|
48
27
|
}
|
|
49
28
|
else {
|
|
50
|
-
|
|
29
|
+
cmd = "xdg-open";
|
|
30
|
+
args = [url];
|
|
51
31
|
}
|
|
32
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
33
|
+
child.on("error", () => {
|
|
34
|
+
console.log(`Could not open browser automatically. Open ${url} manually.`);
|
|
35
|
+
});
|
|
36
|
+
child.unref();
|
|
37
|
+
}
|
|
38
|
+
async function generateSvgs(inputFile, verbose = false) {
|
|
39
|
+
const log = makeLog(verbose);
|
|
40
|
+
const { xml, slideWidth, slideHeight, masterPptxData } = loadInput(inputFile, log);
|
|
52
41
|
if (!xml.trim()) {
|
|
53
42
|
return { type: "empty" };
|
|
54
43
|
}
|
|
@@ -62,11 +51,7 @@ async function generateSvgs(inputFile, verbose = false) {
|
|
|
62
51
|
if (!(buffer instanceof Uint8Array)) {
|
|
63
52
|
throw new Error("Unexpected output type from pptx.write");
|
|
64
53
|
}
|
|
65
|
-
const
|
|
66
|
-
if (!fs.existsSync(fontsDir)) {
|
|
67
|
-
throw new Error(`Bundled fonts directory not found: ${fontsDir}. The package may be corrupted.`);
|
|
68
|
-
}
|
|
69
|
-
const fontDirs = [fontsDir];
|
|
54
|
+
const fontDirs = [resolveBundledFontsDir()];
|
|
70
55
|
const t2 = Date.now();
|
|
71
56
|
const slides = await convertPptxToSvg(buffer, {
|
|
72
57
|
width: slideWidth,
|
|
@@ -371,7 +356,15 @@ export function runPreview(inputFile, port = DEFAULT_PORT, options = {}) {
|
|
|
371
356
|
client.write(`event: update\ndata: ${data}\n\n`);
|
|
372
357
|
}
|
|
373
358
|
}
|
|
359
|
+
let isBuilding = false;
|
|
360
|
+
let pendingRefresh = false;
|
|
374
361
|
function refresh() {
|
|
362
|
+
if (isBuilding) {
|
|
363
|
+
pendingRefresh = true;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
isBuilding = true;
|
|
367
|
+
pendingRefresh = false;
|
|
375
368
|
const totalStart = Date.now();
|
|
376
369
|
log("File changed, rebuilding...");
|
|
377
370
|
broadcastBuilding();
|
|
@@ -385,29 +378,34 @@ export function runPreview(inputFile, port = DEFAULT_PORT, options = {}) {
|
|
|
385
378
|
type: "error",
|
|
386
379
|
message: err instanceof Error ? err.message : String(err),
|
|
387
380
|
});
|
|
381
|
+
})
|
|
382
|
+
.finally(() => {
|
|
383
|
+
isBuilding = false;
|
|
384
|
+
if (pendingRefresh)
|
|
385
|
+
refresh();
|
|
388
386
|
});
|
|
389
387
|
}
|
|
388
|
+
isBuilding = true;
|
|
390
389
|
const initialStart = Date.now();
|
|
391
390
|
generateSvgs(absInput, verbose)
|
|
392
391
|
.then((result) => {
|
|
393
392
|
currentResult = result;
|
|
394
|
-
initialBuildDone = true;
|
|
395
393
|
log(`Initial build done (total: ${Date.now() - initialStart}ms)`);
|
|
396
394
|
broadcast(result);
|
|
397
395
|
})
|
|
398
396
|
.catch((err) => {
|
|
399
|
-
initialBuildDone = true;
|
|
400
397
|
broadcast({
|
|
401
398
|
type: "error",
|
|
402
399
|
message: err instanceof Error ? err.message : String(err),
|
|
403
400
|
});
|
|
401
|
+
})
|
|
402
|
+
.finally(() => {
|
|
403
|
+
initialBuildDone = true;
|
|
404
|
+
isBuilding = false;
|
|
405
|
+
if (pendingRefresh)
|
|
406
|
+
refresh();
|
|
404
407
|
});
|
|
405
|
-
|
|
406
|
-
fs.watch(absInput, () => {
|
|
407
|
-
if (debounceTimer)
|
|
408
|
-
clearTimeout(debounceTimer);
|
|
409
|
-
debounceTimer = setTimeout(refresh, 100);
|
|
410
|
-
});
|
|
408
|
+
watchInputFile(absInput, refresh);
|
|
411
409
|
const html = buildPreviewHtml(path.basename(absInput));
|
|
412
410
|
const server = http.createServer((req, res) => {
|
|
413
411
|
if (req.url === "/_sse") {
|
|
@@ -446,8 +444,12 @@ export function runPreview(inputFile, port = DEFAULT_PORT, options = {}) {
|
|
|
446
444
|
process.exit(1);
|
|
447
445
|
});
|
|
448
446
|
server.listen(port, () => {
|
|
449
|
-
|
|
447
|
+
const url = `http://localhost:${port}`;
|
|
448
|
+
console.log(`Preview server: ${url}`);
|
|
450
449
|
console.log(`Watching: ${absInput}`);
|
|
451
450
|
console.log("Press Ctrl+C to stop");
|
|
451
|
+
if (options.open ?? true) {
|
|
452
|
+
openBrowser(url);
|
|
453
|
+
}
|
|
452
454
|
});
|
|
453
455
|
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,CAAC;AAOzC,wBAAsB,SAAS,CAC7B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,GACL,OAAO,CAAC,IAAI,CAAC,CAoFf"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { buildPptx } from "@hirokisakabe/pom";
|
|
4
|
+
import { convertPptxToPng, convertPptxToSvg } from "pptx-glimpse";
|
|
5
|
+
import { EXTRA_FONT_MAPPING, resolveBundledFontsDir } from "./glimpse.js";
|
|
6
|
+
import { loadInput } from "./input.js";
|
|
7
|
+
function makeLog(verbose) {
|
|
8
|
+
if (!verbose)
|
|
9
|
+
return (_msg) => { };
|
|
10
|
+
return (msg) => process.stderr.write(`[pom] ${msg}\n`);
|
|
11
|
+
}
|
|
12
|
+
export async function runRender(inputFile, outputDir, options = {}) {
|
|
13
|
+
const verbose = options.verbose ?? false;
|
|
14
|
+
const format = options.format ?? "png";
|
|
15
|
+
const log = makeLog(verbose);
|
|
16
|
+
const totalStart = Date.now();
|
|
17
|
+
const absInput = path.resolve(inputFile);
|
|
18
|
+
const absOutputDir = path.resolve(outputDir);
|
|
19
|
+
if (!fs.existsSync(absInput)) {
|
|
20
|
+
throw new Error(`Input file not found: ${absInput}`);
|
|
21
|
+
}
|
|
22
|
+
log(`Reading file: ${absInput}`);
|
|
23
|
+
const { xml, slideWidth, slideHeight, masterPptxData } = loadInput(absInput, log);
|
|
24
|
+
const t1 = Date.now();
|
|
25
|
+
const { pptx } = await buildPptx(xml, { w: slideWidth, h: slideHeight }, {
|
|
26
|
+
textMeasurement: "fallback",
|
|
27
|
+
...(masterPptxData ? { masterPptx: masterPptxData } : {}),
|
|
28
|
+
strict: true,
|
|
29
|
+
});
|
|
30
|
+
log(`Building PPTX... done (${Date.now() - t1}ms)`);
|
|
31
|
+
const buffer = await pptx.write({ outputType: "uint8array" });
|
|
32
|
+
if (!(buffer instanceof Uint8Array)) {
|
|
33
|
+
throw new Error("Unexpected output type from pptx.write");
|
|
34
|
+
}
|
|
35
|
+
const convertOptions = {
|
|
36
|
+
width: slideWidth,
|
|
37
|
+
fontDirs: [resolveBundledFontsDir()],
|
|
38
|
+
fontMapping: EXTRA_FONT_MAPPING,
|
|
39
|
+
skipSystemFonts: true,
|
|
40
|
+
...(options.slides ? { slides: options.slides } : {}),
|
|
41
|
+
};
|
|
42
|
+
const t2 = Date.now();
|
|
43
|
+
let outputs;
|
|
44
|
+
if (format === "svg") {
|
|
45
|
+
const slides = await convertPptxToSvg(buffer, convertOptions);
|
|
46
|
+
outputs = slides.map((s) => ({ slideNumber: s.slideNumber, data: s.svg }));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const slides = await convertPptxToPng(buffer, convertOptions);
|
|
50
|
+
outputs = slides.map((s) => ({ slideNumber: s.slideNumber, data: s.png }));
|
|
51
|
+
}
|
|
52
|
+
log(`Rendering ${format.toUpperCase()}... done (${Date.now() - t2}ms)`);
|
|
53
|
+
if (options.slides) {
|
|
54
|
+
const rendered = new Set(outputs.map((o) => o.slideNumber));
|
|
55
|
+
const missing = options.slides.filter((n) => !rendered.has(n));
|
|
56
|
+
if (missing.length > 0) {
|
|
57
|
+
console.warn(`Warning: slide ${missing.join(", ")} not found in the presentation`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (outputs.length === 0) {
|
|
61
|
+
throw new Error("No slides were rendered");
|
|
62
|
+
}
|
|
63
|
+
fs.mkdirSync(absOutputDir, { recursive: true });
|
|
64
|
+
const padWidth = Math.max(2, ...outputs.map((o) => String(o.slideNumber).length));
|
|
65
|
+
for (const { slideNumber, data } of outputs) {
|
|
66
|
+
const file = path.join(absOutputDir, `slide-${String(slideNumber).padStart(padWidth, "0")}.${format}`);
|
|
67
|
+
fs.writeFileSync(file, data);
|
|
68
|
+
console.log(`Saved: ${file}`);
|
|
69
|
+
}
|
|
70
|
+
log(`Total: ${Date.now() - totalStart}ms`);
|
|
71
|
+
}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAOpB,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,IAAI,GACnB,EAAE,CAAC,SAAS,CA4Bd"}
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const DEBOUNCE_MS = 100;
|
|
4
|
+
// Watches the parent directory instead of the file itself so the watcher
|
|
5
|
+
// survives atomic saves (rename + replace) performed by editors like Vim.
|
|
6
|
+
export function watchInputFile(absPath, onChange) {
|
|
7
|
+
const dir = path.dirname(absPath);
|
|
8
|
+
const base = path.basename(absPath);
|
|
9
|
+
let debounceTimer = null;
|
|
10
|
+
let lastMtimeMs = -1;
|
|
11
|
+
try {
|
|
12
|
+
lastMtimeMs = fs.statSync(absPath).mtimeMs;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// 監視開始時点でファイルが無くても、作成され次第のイベントで拾う
|
|
16
|
+
}
|
|
17
|
+
return fs.watch(dir, (_eventType, filename) => {
|
|
18
|
+
// filename can be null on some platforms; treat it as a potential match
|
|
19
|
+
if (filename !== null && filename !== base)
|
|
20
|
+
return;
|
|
21
|
+
// 同一ディレクトリの別ファイル変更 (filename が null の環境) や
|
|
22
|
+
// 自前の出力ファイル書き込みで発火しないよう、mtime の実変化を確認する
|
|
23
|
+
let mtimeMs;
|
|
24
|
+
try {
|
|
25
|
+
mtimeMs = fs.statSync(absPath).mtimeMs;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (mtimeMs === lastMtimeMs)
|
|
31
|
+
return;
|
|
32
|
+
lastMtimeMs = mtimeMs;
|
|
33
|
+
if (debounceTimer)
|
|
34
|
+
clearTimeout(debounceTimer);
|
|
35
|
+
debounceTimer = setTimeout(onChange, DEBOUNCE_MS);
|
|
36
|
+
});
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hirokisakabe/pom-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI tool for pom — preview and
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "CLI tool for pom — preview, build, and render presentations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pom": "./dist/cli.js"
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"node": ">=18"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@hirokisakabe/pom": "^8.
|
|
33
|
+
"@hirokisakabe/pom": "^8.3.0",
|
|
34
34
|
"@hirokisakabe/pom-md": "^3.0.0",
|
|
35
35
|
"commander": "^15.0.0",
|
|
36
|
-
"pptx-glimpse": "^0.
|
|
36
|
+
"pptx-glimpse": "^1.0.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^25.9.
|
|
40
|
-
"typescript-eslint": "^8.
|
|
39
|
+
"@types/node": "^25.9.2",
|
|
40
|
+
"typescript-eslint": "^8.60.1"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc && node -e \"require('fs').chmodSync('dist/cli.js', '755')\"",
|