@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hirokisakabe/pom-cli
2
2
 
3
- CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview and build presentations from pom XML / Markdown.
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
- Open http://localhost:3000 in your browser. The page updates automatically when the file is saved.
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 SVG rendering. These fonts are used when converting slides to SVG in the preview server. System fonts are not scanned.
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
 
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAUA,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,CAwEf;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,CA2Df"}
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 { parseMd } from "@hirokisakabe/pom-md";
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 content = fs.readFileSync(absInput, "utf-8");
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
- let debounceTimer = null;
114
- fs.watch(absInput, () => {
115
- if (debounceTimer)
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 build presentations")
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, { verbose: options.verbose });
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
- if (err instanceof DiagnosticsError) {
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,3 @@
1
+ export declare const EXTRA_FONT_MAPPING: Record<string, string>;
2
+ export declare function resolveBundledFontsDir(): string;
3
+ //# sourceMappingURL=glimpse.d.ts.map
@@ -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"}
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ export interface LoadedInput {
2
+ xml: string;
3
+ slideWidth: number;
4
+ slideHeight: number;
5
+ masterPptxData?: Uint8Array;
6
+ }
7
+ export declare function loadInput(absInput: string, log?: (msg: string) => void): LoadedInput;
8
+ //# sourceMappingURL=input.d.ts.map
@@ -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
@@ -1,4 +1,5 @@
1
1
  export declare function runPreview(inputFile: string, port?: number, options?: {
2
2
  verbose?: boolean;
3
+ open?: boolean;
3
4
  }): void;
4
5
  //# sourceMappingURL=preview.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AA8XA,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,MAAqB,EAC3B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,IAAI,CAkHN"}
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
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
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
- const EXTRA_FONT_MAPPING = {
16
- "游ゴシック Light": "Noto Sans CJK JP",
17
- "Yu Gothic Light": "Noto Sans CJK JP",
18
- };
19
- async function generateSvgs(inputFile, verbose = false) {
20
- const log = makeLog(verbose);
21
- const content = fs.readFileSync(inputFile, "utf-8");
22
- const ext = path.extname(inputFile);
23
- let xml;
24
- let slideWidth = 1280;
25
- let slideHeight = 720;
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
- xml = content;
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 fontsDir = path.resolve(__dirname, "../fonts");
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
- let debounceTimer = null;
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
- console.log(`Preview server: http://localhost:${port}`);
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
  }
@@ -0,0 +1,7 @@
1
+ export type RenderFormat = "png" | "svg";
2
+ export declare function runRender(inputFile: string, outputDir: string, options?: {
3
+ format?: RenderFormat;
4
+ slides?: number[];
5
+ verbose?: boolean;
6
+ }): Promise<void>;
7
+ //# sourceMappingURL=render.d.ts.map
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import fs from "fs";
2
+ export declare function watchInputFile(absPath: string, onChange: () => void): fs.FSWatcher;
3
+ //# sourceMappingURL=watch.d.ts.map
@@ -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.3.2",
4
- "description": "CLI tool for pom — preview and build presentations",
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.2.1",
33
+ "@hirokisakabe/pom": "^8.3.0",
34
34
  "@hirokisakabe/pom-md": "^3.0.0",
35
35
  "commander": "^15.0.0",
36
- "pptx-glimpse": "^0.11.2"
36
+ "pptx-glimpse": "^1.0.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.9.1",
40
- "typescript-eslint": "^8.59.4"
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')\"",