ptxiagram 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hiio420official
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # ptxiagram
2
+
3
+ **Generate architecture and workflow diagrams as native, editable PowerPoint shapes — not an embedded image.**
4
+
5
+ ptxiagram is an agent skill for Claude (and a standalone CLI/library) that turns a plain-English description of a system or process into a `.pptx` slide built from real PowerPoint shapes: rectangles, cylinders, clouds, connectors, text boxes. Anyone can open the result in PowerPoint — or Hancom 한쇼 — and click a box to change its color, text, or position, because it *is* a box, not a picture of one.
6
+
7
+ - **Native shapes, not a picture** — every box and arrow is a real PPTX shape, editable after generation
8
+ - **Two diagram types today** — `workflow` (swimlanes, happy-path flows) and `architecture` (free-placed components, trust boundaries, hub-and-spoke)
9
+ - **A validator that catches real layout bugs before rendering** — node overlap, labels wider than their shape, and connectors that cut through an unrelated node all fail loudly with a specific fix, instead of shipping a broken-looking slide
10
+ - **Ships a Hancom Office compatibility fix** — every generated file is post-processed to strip a documented PptxGenJS defect that PowerPoint silently repairs but Hancom's 한쇼 flags as a corrupted document (see below)
11
+ - **Route presets instead of hand-computed coordinates** — `straight`, `drop`, `elbow-right`, `arc-over-top` cover swimlane transitions, hub-and-spoke fan-out, and routing around a boundary box
12
+
13
+ ## Install
14
+
15
+ As a Claude Skill:
16
+
17
+ ```bash
18
+ npx skills add hiio420official/ptxiagram -g
19
+ ```
20
+
21
+ As a CLI / library:
22
+
23
+ ```bash
24
+ npm install -g ptxiagram
25
+ ptxiagram doctor
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```bash
31
+ ptxiagram render workflow examples/onboarding.workflow.json out.pptx
32
+ ptxiagram check out.pptx
33
+ ```
34
+
35
+ `render` validates the input (schema + layout) before writing anything, and always repairs the output for Hancom Office compatibility. `check` confirms the repair took, dumps every embedded text run for a spelling/content spot-check, and — if LibreOffice is installed — writes a PNG preview for a quick visual check.
36
+
37
+ See [`ptxiagram/SKILL.md`](ptxiagram/SKILL.md) for the full JSON shape of both diagram types, the node-type → shape/color mapping, and the route presets.
38
+
39
+ ## Why every file gets repaired
40
+
41
+ PptxGenJS ships a notes-slide scaffold (6 placeholder shapes) on every file it generates, whether or not notes are used. PowerPoint silently repairs this on open. Hancom Office's 한쇼 does not — it shows a "damaged document" dialog instead, on every file, every time. `lib/repair-pptx.mjs` strips the notes infrastructure and the always-empty `charts`/`embeddings` scaffolding before the file is written. Source: [gitbrent/PptxGenJS#1449](https://github.com/gitbrent/PptxGenJS/issues/1449).
42
+
43
+ ## License
44
+
45
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: ptxiagram
3
+ description: Create architecture and workflow diagrams as native, editable PowerPoint shapes (real rectangles, connectors, and text boxes a user can click and restyle in PowerPoint or Hancom 한쇼) instead of an embedded image. Accepts a plain-language description, turns it into a small JSON file, and renders it with pptxgenjs. Use when the user asks for a diagram, architecture drawing, or process flow that needs to end up inside a .pptx deck or a Korean-office-suite-compatible presentation - not when they just want a standalone image or an HTML/web diagram (use an SVG-based diagram tool for that instead).
4
+ license: MIT
5
+ metadata:
6
+ version: "0.1.0"
7
+ status: prototype
8
+ based_on: "gitbrent/PptxGenJS (MIT)"
9
+ ---
10
+
11
+ # ptxiagram
12
+
13
+ Turns a plain-language description of a system or process into a single
14
+ `.pptx` slide built from **native PowerPoint shapes** — real rectangles,
15
+ cylinders, connectors, and text boxes — not a picture pasted into a slide.
16
+ Anyone can then click a box in PowerPoint (or Hancom 한쇼) and change its
17
+ color, text, or position directly.
18
+
19
+ This is the thing to reach for when the deliverable is a slide deck. If the
20
+ deliverable is a standalone image, an HTML page, or a README diagram, an
21
+ SVG/HTML-based diagram tool is the better fit — this skill's whole reason to
22
+ exist is that the output has to remain editable inside PowerPoint.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npx skills add hiio420official/ptxiagram -g
28
+ ```
29
+
30
+ or as an npm package (CLI + library):
31
+
32
+ ```bash
33
+ npm install -g ptxiagram
34
+ ptxiagram doctor
35
+ ```
36
+
37
+ ## Setup
38
+
39
+ ```bash
40
+ node bin/cli.mjs doctor
41
+ ```
42
+
43
+ Confirms pptxgenjs, jszip, and ajv are present, and reports whether
44
+ LibreOffice is installed (optional — enables the `check` command's PNG
45
+ preview for visual QA; without it you still get full schema/layout
46
+ validation and the embedded-text dump, just no rendered preview image).
47
+
48
+ ## Choosing a diagram type
49
+
50
+ | Type | Use for | Schema |
51
+ |------|---------|--------|
52
+ | `workflow` | Cross-team processes, approval chains, anything with swimlanes and a happy path | `schemas/workflow.schema.json` |
53
+ | `architecture` | System components, services, datastores, trust boundaries, hub-and-spoke integrations | `schemas/architecture.schema.json` |
54
+
55
+ ## The loop
56
+
57
+ 1. **Read the matching example** before writing JSON by hand — copy its
58
+ shape, don't guess the field names:
59
+ - `examples/onboarding.workflow.json`
60
+ - `examples/order-processing.architecture.json`
61
+ 2. Write `<name>.<type>.json`.
62
+ 3. Render:
63
+ ```bash
64
+ node bin/cli.mjs render workflow <input>.json <output>.pptx
65
+ ```
66
+ This validates the JSON (schema + layout) and **always** repairs the
67
+ output for Hancom Office compatibility before writing the final file —
68
+ see "Why every file gets repaired" below. If layout validation fails, the
69
+ error names exactly what's wrong (which nodes overlap, which label is
70
+ wider than its shape, which edge cuts through an unrelated node) — fix
71
+ the JSON and re-run; don't hand-edit the generated pptx.
72
+ 4. Spot-check before handing it over:
73
+ ```bash
74
+ node bin/cli.mjs check <output>.pptx
75
+ ```
76
+ Confirms the Hancom repair actually took (no leftover notes/chart
77
+ scaffolding), dumps every embedded text run so you can read the real
78
+ strings back (LibreOffice's PNG rendering has shown font-substitution
79
+ artifacts that don't reflect the actual file — trust this text dump over
80
+ the preview image for spelling/content, and trust the PNG for layout).
81
+ If LibreOffice is installed, this also writes a PNG preview next to the
82
+ file so you can visually confirm layout before delivering it.
83
+
84
+ ## Workflow JSON shape
85
+
86
+ ```json
87
+ {
88
+ "schema_version": 1,
89
+ "diagram_type": "workflow",
90
+ "meta": { "title": "...", "subtitle": "...", "output": "out.pptx" },
91
+ "lanes": [{ "id": "hr", "label": "HR팀" }],
92
+ "nodes": [
93
+ { "id": "apply", "lane": "hr", "col": 0, "type": "neutral", "label": "...", "sublabel": "..." }
94
+ ],
95
+ "edges": [
96
+ { "from": "apply", "to": "request", "variant": "default", "route": "straight" }
97
+ ],
98
+ "cards": []
99
+ }
100
+ ```
101
+
102
+ **Layout budget**: lanes stack vertically and are auto-positioned (default
103
+ height 1.75in). Each lane has 6 column slots (`col` 0–5, each 2.0in wide);
104
+ a node claims exactly one slot, so default-width nodes can never overlap —
105
+ omit `col` and the renderer assigns the next free slot in that lane in JSON
106
+ order. Default node size is 1.8×0.95in.
107
+
108
+ ## Architecture JSON shape
109
+
110
+ ```json
111
+ {
112
+ "schema_version": 1,
113
+ "diagram_type": "architecture",
114
+ "meta": { "title": "...", "output": "out.pptx" },
115
+ "components": [
116
+ { "id": "api", "type": "backend", "label": "API 서버", "pos": [5.9, 3.1], "size": [1.85, 1.1] }
117
+ ],
118
+ "boundaries": [{ "label": "사내 인프라 (VPC)", "wraps": ["api", "db"] }],
119
+ "connections": [
120
+ { "from": "api", "to": "db", "variant": "default", "route": "elbow-right", "label": "주문 저장" }
121
+ ],
122
+ "cards": []
123
+ }
124
+ ```
125
+
126
+ **Layout budget**: free placement in inches on a 13.33×7.5in (16:9) slide.
127
+ `boundaries[].wraps` takes component ids, not coordinates — the renderer
128
+ computes a padded frame around them, so never hand-compute a boundary box.
129
+
130
+ ## Node types
131
+
132
+ `client` `frontend` `neutral` `backend` `database` `cache` `queue`
133
+ `external` `security` `decision` — each maps to a shape preset (ellipse,
134
+ rect, roundRect, can/cylinder, hexagon, cube, cloud, diamond) and a color
135
+ pair. Pick by role, not by desired color.
136
+
137
+ ## Route presets
138
+
139
+ Connectors never need hand-computed coordinates — pick the preset that
140
+ matches the story:
141
+
142
+ - `straight` — adjacent nodes, same row. One segment.
143
+ - `drop` — top-to-bottom flow (cross-lane, or stacked components). Straight
144
+ if the two nodes share an x-center, otherwise a vertical→horizontal→vertical
145
+ jog through the midpoint (`bias` picks where, default 0.5).
146
+ - `elbow-right` — hub-and-spoke fan-out where targets sit at different row
147
+ heights. Straight if the two nodes share a y-center, otherwise a
148
+ horizontal→vertical→horizontal jog.
149
+ - `arc-over-top` — routes above a shared clearance line (`clearY`, required)
150
+ instead of cutting through whatever sits between two nodes. Use this for
151
+ a connection that has to reach past a boundary box or a row of components
152
+ a straight/elbow line would otherwise cross.
153
+
154
+ `node overlaps`, `label wider than its shape`, and `edge crosses an
155
+ unrelated node` are exactly the failure modes `render`/`validate` catch
156
+ before a file is written — if you hit one, change the route or `bias`
157
+ rather than fighting raw coordinates.
158
+
159
+ ## Why every file gets repaired
160
+
161
+ pptxgenjs ships a notes-slide scaffold (6 placeholder shapes) on every
162
+ file it generates, regardless of whether notes are used. PowerPoint
163
+ silently repairs this on open; Hancom Office's 한쇼 does not — it shows a
164
+ "damaged document" dialog instead. `lib/repair-pptx.mjs` strips the notes
165
+ infrastructure and the always-empty `charts`/`embeddings` scaffolding
166
+ after generation and before the file is written. Every `render` call does
167
+ this automatically; `check` verifies it actually happened. Source:
168
+ [gitbrent/PptxGenJS#1449](https://github.com/gitbrent/PptxGenJS/issues/1449).
169
+
170
+ ## What's not built yet
171
+
172
+ - `sequence`, `dataflow`, `lifecycle` diagram types (only `workflow` and
173
+ `architecture` exist so far)
174
+ - No CJK exact-metrics font measurement — the label-width check is a
175
+ conservative estimate, not real glyph widths
package/bin/cli.mjs ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import JSZip from "jszip";
8
+ import { renderWorkflow } from "../renderers/workflow/render-workflow.mjs";
9
+ import { renderArchitecture } from "../renderers/architecture/render-architecture.mjs";
10
+ import { validateAll } from "../lib/validate.mjs";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ const RENDERERS = { workflow: renderWorkflow, architecture: renderArchitecture };
15
+
16
+ function usage() {
17
+ console.log(`Usage:
18
+ cli render <type> <input.json> [output.pptx]
19
+ cli validate <type> <input.json> [--json]
20
+ cli check <output.pptx>
21
+ cli doctor
22
+
23
+ Types: workflow, architecture`);
24
+ }
25
+
26
+ async function cmdRender(type, inputPath, outputPath) {
27
+ const renderFn = RENDERERS[type];
28
+ if (!renderFn) throw new Error(`Unknown type "${type}". Expected one of: ${Object.keys(RENDERERS).join(", ")}`);
29
+ const outFile = await renderFn(inputPath, outputPath);
30
+ console.log(outFile);
31
+ }
32
+
33
+ async function cmdValidate(type, inputPath, asJson) {
34
+ const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
35
+ try {
36
+ const problems = await validateAll(type, raw);
37
+ const result = { ok: problems.length === 0, type, input: inputPath, problems };
38
+ if (asJson) {
39
+ console.log(JSON.stringify(result, null, 2));
40
+ } else if (problems.length === 0) {
41
+ console.log("OK: schema and layout are clean.");
42
+ } else {
43
+ console.log(`Layout problems (${problems.length}):`);
44
+ for (const p of problems) console.log(`- ${p}`);
45
+ }
46
+ if (problems.length) process.exitCode = 1;
47
+ } catch (err) {
48
+ if (asJson) {
49
+ console.log(JSON.stringify({ ok: false, type, input: inputPath, error: err.message }, null, 2));
50
+ } else {
51
+ console.error(err.message);
52
+ }
53
+ process.exitCode = 1;
54
+ }
55
+ }
56
+
57
+ function findSoffice() {
58
+ const candidates = [
59
+ "C:/Program Files/LibreOffice/program/soffice.exe",
60
+ "/usr/bin/soffice",
61
+ "/opt/libreoffice/program/soffice",
62
+ ];
63
+ return candidates.find((p) => existsSync(p));
64
+ }
65
+
66
+ async function cmdCheck(pptxPath) {
67
+ const buf = await fs.readFile(pptxPath);
68
+ const zip = await JSZip.loadAsync(buf);
69
+ const problems = [];
70
+
71
+ const hasNotes = Object.keys(zip.files).some(
72
+ (n) => n.startsWith("ppt/notesMasters/") || n.startsWith("ppt/notesSlides/")
73
+ );
74
+ if (hasNotes) problems.push("notesMaster/notesSlide parts present — repairPptx() was not applied (Hancom Office will flag this as corrupted).");
75
+
76
+ const hasEmptyScaffold = Object.keys(zip.files).some(
77
+ (n) => n.startsWith("ppt/charts/") || n.startsWith("ppt/embeddings/")
78
+ );
79
+ if (hasEmptyScaffold) problems.push("empty ppt/charts/ or ppt/embeddings/ scaffolding present.");
80
+
81
+ const slideFiles = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
82
+ const texts = [];
83
+ for (const name of slideFiles) {
84
+ const content = await zip.file(name).async("string");
85
+ const matches = [...content.matchAll(/<a:t>([^<]*)<\/a:t>/g)].map((m) => m[1]);
86
+ texts.push(...matches);
87
+ }
88
+
89
+ console.log(JSON.stringify({ ok: problems.length === 0, file: pptxPath, problems, embeddedText: texts }, null, 2));
90
+
91
+ const soffice = findSoffice();
92
+ if (soffice) {
93
+ const outDir = path.join(path.dirname(pptxPath), "render");
94
+ await fs.mkdir(outDir, { recursive: true });
95
+ await execFileAsync(soffice, ["--headless", "--convert-to", "png", "--outdir", outDir, pptxPath]);
96
+ const pngName = path.basename(pptxPath).replace(/\.pptx$/, ".png");
97
+ console.log(`PNG preview: ${path.join(outDir, pngName)}`);
98
+ } else {
99
+ console.log("LibreOffice not found — skipping PNG preview. Install it for visual QA (soffice --headless --convert-to png).");
100
+ }
101
+
102
+ if (problems.length) process.exitCode = 1;
103
+ }
104
+
105
+ async function cmdDoctor() {
106
+ const checks = [];
107
+ try {
108
+ await import("pptxgenjs");
109
+ checks.push(["pptxgenjs", true]);
110
+ } catch {
111
+ checks.push(["pptxgenjs", false]);
112
+ }
113
+ try {
114
+ await import("jszip");
115
+ checks.push(["jszip", true]);
116
+ } catch {
117
+ checks.push(["jszip", false]);
118
+ }
119
+ try {
120
+ await import("ajv/dist/2020.js");
121
+ checks.push(["ajv", true]);
122
+ } catch {
123
+ checks.push(["ajv", false]);
124
+ }
125
+ const soffice = findSoffice();
126
+ checks.push(["LibreOffice (optional, enables `check` PNG preview + visual QA)", Boolean(soffice)]);
127
+
128
+ for (const [name, ok] of checks) {
129
+ console.log(`[${ok ? "ok" : "MISSING"}] ${name}`);
130
+ }
131
+ if (checks.some(([, ok]) => !ok)) process.exitCode = 1;
132
+ }
133
+
134
+ const [, , cmd, ...rest] = process.argv;
135
+
136
+ try {
137
+ if (cmd === "render") {
138
+ const [type, input, output] = rest;
139
+ if (!type || !input) { usage(); process.exit(1); }
140
+ await cmdRender(type, input, output);
141
+ } else if (cmd === "validate") {
142
+ const asJson = rest.includes("--json");
143
+ const [type, input] = rest.filter((a) => a !== "--json");
144
+ if (!type || !input) { usage(); process.exit(1); }
145
+ await cmdValidate(type, input, asJson);
146
+ } else if (cmd === "check") {
147
+ const [pptxPath] = rest;
148
+ if (!pptxPath) { usage(); process.exit(1); }
149
+ await cmdCheck(pptxPath);
150
+ } else if (cmd === "doctor") {
151
+ await cmdDoctor();
152
+ } else {
153
+ usage();
154
+ process.exit(cmd ? 1 : 0);
155
+ }
156
+ } catch (err) {
157
+ console.error(err.message);
158
+ process.exit(1);
159
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "schema_version": 1,
3
+ "diagram_type": "workflow",
4
+ "meta": {
5
+ "title": "신규 직원 온보딩 프로세스",
6
+ "subtitle": "HR팀 → IT팀 → 팀장",
7
+ "output": "test/output/onboarding.schema-driven.pptx"
8
+ },
9
+ "lanes": [
10
+ { "id": "hr", "label": "HR팀" },
11
+ { "id": "it", "label": "IT팀" },
12
+ { "id": "lead", "label": "팀장" }
13
+ ],
14
+ "nodes": [
15
+ { "id": "apply", "lane": "hr", "col": 0, "type": "neutral", "label": "입사 서류 접수", "sublabel": "인사팀 접수 확인" },
16
+ { "id": "request", "lane": "hr", "col": 1, "type": "neutral", "label": "계정 발급 요청", "sublabel": "IT팀에 요청 전달" },
17
+ { "id": "account", "lane": "it", "col": 1, "type": "backend", "label": "계정 생성", "sublabel": "사내 계정 · 이메일" },
18
+ { "id": "equipment", "lane": "it", "col": 2, "type": "backend", "label": "장비 지급", "sublabel": "노트북 · 출입카드" },
19
+ { "id": "kickoff", "lane": "lead", "col": 3, "type": "database", "label": "킥오프 미팅", "sublabel": "팀 소개 · 업무 안내" },
20
+ { "id": "done", "lane": "lead", "col": 4, "type": "database", "label": "온보딩 완료", "sublabel": "1주차 체크인 예약" }
21
+ ],
22
+ "edges": [
23
+ { "from": "apply", "to": "request", "variant": "default", "route": "straight" },
24
+ { "from": "request", "to": "account", "variant": "emphasis", "route": "drop", "label": "요청 전달" },
25
+ { "from": "account", "to": "equipment", "variant": "emphasis", "route": "straight" },
26
+ { "from": "equipment", "to": "kickoff", "variant": "default", "route": "drop", "label": "장비 수령 후" },
27
+ { "from": "kickoff", "to": "done", "variant": "default", "route": "straight" }
28
+ ]
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "schema_version": 1,
3
+ "diagram_type": "architecture",
4
+ "meta": {
5
+ "title": "온라인 쇼핑몰 주문처리 아키텍처",
6
+ "subtitle": "클라이언트 → API 서버 → 내부 인프라 / 외부 결제 PG",
7
+ "output": "test/output/order-processing.schema-driven.pptx"
8
+ },
9
+ "components": [
10
+ { "id": "client", "type": "client", "label": "사용자", "sublabel": "브라우저", "pos": [0.4, 3.1], "size": [1.5, 1.1] },
11
+ { "id": "web", "type": "frontend", "label": "웹 / 앱", "sublabel": "React", "pos": [2.35, 3.1], "size": [1.7, 1.1] },
12
+ { "id": "api", "type": "backend", "label": "API 서버", "sublabel": "주문 처리", "pos": [5.9, 3.1], "size": [1.85, 1.1] },
13
+ { "id": "db", "type": "database", "label": "주문 DB", "pos": [8.4, 1.55], "size": [1.0, 1.2] },
14
+ { "id": "cache", "type": "cache", "label": "캐시", "sublabel": "Redis", "pos": [8.3, 3.1], "size": [1.15, 1.1] },
15
+ { "id": "queue", "type": "queue", "label": "알림 큐", "pos": [8.3, 4.55], "size": [1.15, 1.05] },
16
+ { "id": "pg", "type": "external", "label": "결제 PG사", "sublabel": "외부 서비스", "pos": [10.4, 3.0], "size": [2.1, 1.3] }
17
+ ],
18
+ "boundaries": [
19
+ { "label": "사내 인프라 (VPC)", "wraps": ["api", "db", "cache", "queue"], "color": "F59E0B" }
20
+ ],
21
+ "connections": [
22
+ { "from": "client", "to": "web", "variant": "default", "route": "straight" },
23
+ { "from": "web", "to": "api", "variant": "emphasis", "route": "straight" },
24
+ { "from": "api", "to": "db", "variant": "default", "route": "elbow-right", "label": "주문 저장" },
25
+ { "from": "api", "to": "cache", "variant": "default", "route": "elbow-right" },
26
+ { "from": "api", "to": "queue", "variant": "default", "route": "elbow-right", "label": "발송 알림" },
27
+ { "from": "api", "to": "pg", "variant": "dashed", "route": "arc-over-top", "clearY": 1.0, "label": "결제 요청 (async)" }
28
+ ]
29
+ }
package/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ export { renderWorkflow } from "./renderers/workflow/render-workflow.mjs";
2
+ export { renderArchitecture } from "./renderers/architecture/render-architecture.mjs";
3
+ export { validateAll, validateSchema, validateWorkflowLayout, validateArchitectureLayout } from "./lib/validate.mjs";
4
+ export { repairPptx } from "./lib/repair-pptx.mjs";
package/lib/layout.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // Pure layout math shared by the renderers (which draw the boxes this
2
+ // computes) and the validator (which checks them before anything is drawn).
3
+ // Keeping this in one place means the renderer and the validator can never
4
+ // silently drift apart.
5
+
6
+ export const SLIDE_W = 13.33, SLIDE_H = 7.5;
7
+
8
+ export const WORKFLOW_LAYOUT = {
9
+ LANE_X: 0.4, LANE_W: 12.5,
10
+ LANE_START_Y: 1.15, LANE_GAP: 0.15, DEFAULT_LANE_H: 1.75,
11
+ COL_X0: 0.6, COL_WIDTH: 2.0,
12
+ NODE_W: 1.8, NODE_H: 0.95, NODE_Y_OFFSET: 0.35,
13
+ };
14
+
15
+ export function computeWorkflowLayout(raw) {
16
+ const L = WORKFLOW_LAYOUT;
17
+ let y = L.LANE_START_Y;
18
+ const laneBoxes = {};
19
+ for (const lane of raw.lanes) {
20
+ const h = lane.height || L.DEFAULT_LANE_H;
21
+ laneBoxes[lane.id] = { x: L.LANE_X, y, w: L.LANE_W, h };
22
+ y += h + L.LANE_GAP;
23
+ }
24
+ const cardsY = y + 0.1;
25
+
26
+ const usedCols = {};
27
+ for (const n of raw.nodes) {
28
+ if (n.col !== undefined) (usedCols[n.lane] ||= new Set()).add(n.col);
29
+ }
30
+ const nodeBoxes = {};
31
+ const nodeMeta = {};
32
+ for (const n of raw.nodes) {
33
+ const laneBox = laneBoxes[n.lane];
34
+ if (!laneBox) throw new Error(`Node "${n.id}" references unknown lane "${n.lane}"`);
35
+ let col = n.col;
36
+ if (col === undefined) {
37
+ const used = (usedCols[n.lane] ||= new Set());
38
+ col = 0;
39
+ while (used.has(col)) col++;
40
+ used.add(col);
41
+ }
42
+ const w = n.width || L.NODE_W;
43
+ const h = n.height || L.NODE_H;
44
+ nodeBoxes[n.id] = { x: L.COL_X0 + col * L.COL_WIDTH, y: laneBox.y + L.NODE_Y_OFFSET, w, h };
45
+ nodeMeta[n.id] = { lane: n.lane, col };
46
+ }
47
+ return { laneBoxes, nodeBoxes, nodeMeta, cardsY };
48
+ }
49
+
50
+ const BOUNDARY_PAD = 0.3;
51
+
52
+ export function computeArchitectureLayout(raw) {
53
+ const boxes = {};
54
+ for (const c of raw.components) {
55
+ const [x, y] = c.pos;
56
+ const [w, h] = c.size || [1.8, 0.95];
57
+ boxes[c.id] = { x, y, w, h };
58
+ }
59
+
60
+ const boundaryBoxes = (raw.boundaries || []).map((b) => {
61
+ const wrapped = b.wraps.map((id) => {
62
+ if (!boxes[id]) throw new Error(`Boundary "${b.label}" wraps unknown component "${id}"`);
63
+ return boxes[id];
64
+ });
65
+ const minX = Math.min(...wrapped.map((n) => n.x)) - BOUNDARY_PAD;
66
+ const minY = Math.min(...wrapped.map((n) => n.y)) - BOUNDARY_PAD;
67
+ const maxX = Math.max(...wrapped.map((n) => n.x + n.w)) + BOUNDARY_PAD;
68
+ const maxY = Math.max(...wrapped.map((n) => n.y + n.h)) + BOUNDARY_PAD;
69
+ return { label: b.label, color: b.color, box: { x: minX, y: minY, w: maxX - minX, h: maxY - minY } };
70
+ });
71
+
72
+ return { boxes, boundaryBoxes };
73
+ }
@@ -0,0 +1,73 @@
1
+ import fs from "node:fs/promises";
2
+ import JSZip from "jszip";
3
+
4
+ /**
5
+ * Fixes known PptxGenJS OOXML defects that PowerPoint and Hancom Office's
6
+ * 한쇼 flag as a corrupted/damaged document (LibreOffice is lenient and
7
+ * renders the unfixed output fine, which is why the defects go unnoticed
8
+ * until a stricter parser opens the file).
9
+ *
10
+ * Source: https://github.com/gitbrent/PptxGenJS/issues/1449
11
+ * - Defect 5: notesMaster1.xml always ships 6 placeholder shapes
12
+ * (hdr/dt/sldImg/body/ftr/sldNum) that PowerPoint-family parsers
13
+ * consider malformed. PptxGenJS emits this notes scaffold on every
14
+ * file regardless of whether notes are used, so every generated
15
+ * pptx carries it.
16
+ * - Defect 6: empty ppt/charts/ and ppt/embeddings/ directories are
17
+ * always created even when no chart/embedding exists.
18
+ *
19
+ * Strategy: since our diagrams never use PPT speaker notes or embedded
20
+ * charts, the simplest fix is to strip the notes infrastructure and the
21
+ * empty scaffold directories entirely rather than trying to make them
22
+ * spec-conformant.
23
+ */
24
+ export async function repairPptx(filePath) {
25
+ const buf = await fs.readFile(filePath);
26
+ const zip = await JSZip.loadAsync(buf);
27
+
28
+ // Defect 5: drop notes parts
29
+ for (const name of Object.keys(zip.files)) {
30
+ if (name.startsWith("ppt/notesMasters/") || name.startsWith("ppt/notesSlides/")) {
31
+ zip.remove(name);
32
+ }
33
+ }
34
+
35
+ // Defect 6: drop empty chart/embedding scaffolding
36
+ for (const name of Object.keys(zip.files)) {
37
+ if (name.startsWith("ppt/charts/") || name.startsWith("ppt/embeddings/")) {
38
+ zip.remove(name);
39
+ }
40
+ }
41
+
42
+ // [Content_Types].xml: remove notesMaster/notesSlide Overrides
43
+ const ctPath = "[Content_Types].xml";
44
+ let ct = await zip.file(ctPath).async("string");
45
+ ct = ct.replace(/<Override[^>]*PartName="\/ppt\/notesMasters\/[^"]*"[^>]*\/>/g, "");
46
+ ct = ct.replace(/<Override[^>]*PartName="\/ppt\/notesSlides\/[^"]*"[^>]*\/>/g, "");
47
+ zip.file(ctPath, ct);
48
+
49
+ // presentation.xml: remove <p:notesMasterIdLst>
50
+ const presPath = "ppt/presentation.xml";
51
+ let pres = await zip.file(presPath).async("string");
52
+ pres = pres.replace(/<p:notesMasterIdLst>[\s\S]*?<\/p:notesMasterIdLst>/g, "");
53
+ zip.file(presPath, pres);
54
+
55
+ // presentation.xml.rels: remove notesMaster relationship
56
+ const presRelsPath = "ppt/_rels/presentation.xml.rels";
57
+ let presRels = await zip.file(presRelsPath).async("string");
58
+ presRels = presRels.replace(/<Relationship[^>]*Type="[^"]*\/notesMaster"[^>]*\/>/g, "");
59
+ zip.file(presRelsPath, presRels);
60
+
61
+ // each slide's .rels: remove notesSlide relationship
62
+ const slideRelsPattern = /^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/;
63
+ for (const name of Object.keys(zip.files)) {
64
+ if (slideRelsPattern.test(name)) {
65
+ let content = await zip.file(name).async("string");
66
+ const cleaned = content.replace(/<Relationship[^>]*Type="[^"]*\/notesSlide"[^>]*\/>/g, "");
67
+ if (cleaned !== content) zip.file(name, cleaned);
68
+ }
69
+ }
70
+
71
+ const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
72
+ await fs.writeFile(filePath, out);
73
+ }