ptxiagram 0.1.0 → 0.3.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 +39 -10
- package/SKILL.md +88 -7
- package/bin/cli.mjs +11 -2
- package/examples/agent-run.lifecycle.json +26 -0
- package/examples/cache-miss.sequence.json +23 -0
- package/examples/product-analytics.dataflow.json +30 -0
- package/index.mjs +11 -1
- package/lib/layout.mjs +50 -0
- package/lib/routes.mjs +20 -1
- package/lib/shapes.mjs +14 -0
- package/lib/validate.mjs +60 -3
- package/package.json +6 -3
- package/renderers/architecture/render-architecture.mjs +2 -0
- package/renderers/dataflow/render-dataflow.mjs +73 -0
- package/renderers/lifecycle/render-lifecycle.mjs +10 -0
- package/renderers/sequence/render-sequence.mjs +67 -0
- package/renderers/workflow/render-workflow.mjs +9 -2
- package/schemas/common.schema.json +1 -1
- package/schemas/dataflow.schema.json +80 -0
- package/schemas/lifecycle.schema.json +86 -0
- package/schemas/sequence.schema.json +68 -0
package/README.md
CHANGED
|
@@ -1,40 +1,69 @@
|
|
|
1
1
|
# ptxiagram
|
|
2
2
|
|
|
3
|
-
**Generate architecture and
|
|
3
|
+
**Generate architecture, workflow, and sequence diagrams as native, editable PowerPoint shapes — not an embedded image.**
|
|
4
|
+
|
|
5
|
+
> Built with [Claude Code](https://claude.com/claude-code) (Claude Sonnet 5) — from the initial prototype through the pptxgenjs/Hancom Office compatibility investigation, the JSON schema design, the layout validator, and this package's release automation. See the [commit history](https://github.com/hiio420official/ptxiagram/commits/main) for the build process.
|
|
4
6
|
|
|
5
7
|
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
8
|
|
|
7
9
|
- **Native shapes, not a picture** — every box and arrow is a real PPTX shape, editable after generation
|
|
8
|
-
- **
|
|
10
|
+
- **Five diagram types today** — `workflow` (swimlanes, happy-path flows), `architecture` (free-placed components, trust boundaries, hub-and-spoke), `sequence` (participants + lifelines, API call chains), `dataflow` (stage-to-stage pipelines with PII/lineage annotations), and `lifecycle` (state machines with terminal outcomes)
|
|
9
11
|
- **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
12
|
- **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
13
|
- **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
14
|
|
|
13
|
-
##
|
|
15
|
+
## Use it in an agent (Claude Code, Codex, Cursor, ...)
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
Install with the [`skills` CLI](https://github.com/vercel-labs/skills) — it detects every supported agent on your machine and wires the skill into each one:
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
npx skills add hiio420official/ptxiagram -g
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Then just ask in chat:
|
|
22
24
|
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
```text
|
|
26
|
+
Use ptxiagram to draw an architecture diagram for this repo's runtime,
|
|
27
|
+
as a PPTX I can drop into a slide deck.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Use ptxiagram to make a workflow diagram of our incident response process,
|
|
32
|
+
with a lane per team, so I can paste it into the postmortem deck.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
Use ptxiagram to draw a sequence diagram of this API's cache-fallback path,
|
|
37
|
+
from request to response, for the engineering review deck.
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
The agent reads `SKILL.md`, writes a small JSON file describing your nodes and
|
|
41
|
+
edges, and runs the CLI to produce a `.pptx` with real, editable shapes.
|
|
42
|
+
|
|
43
|
+
**Per-agent notes:**
|
|
44
|
+
- **Claude Code**: works out of the box after `npx skills add`.
|
|
45
|
+
- **Codex CLI / opencode**: install with an explicit `--agent` flag if you don't want it installed everywhere, e.g. `npx skills use hiio420official/ptxiagram@ptxiagram --agent codex`.
|
|
46
|
+
- **Any other `skills`-supported agent** (Cursor, Windsurf, Continue, Antigravity, Gemini CLI, ...): the same `npx skills add` command installs to whichever of these are detected on your machine.
|
|
47
|
+
|
|
48
|
+
## Use it standalone (CLI / library)
|
|
29
49
|
|
|
30
50
|
```bash
|
|
51
|
+
npm install -g ptxiagram
|
|
52
|
+
ptxiagram doctor
|
|
31
53
|
ptxiagram render workflow examples/onboarding.workflow.json out.pptx
|
|
32
54
|
ptxiagram check out.pptx
|
|
33
55
|
```
|
|
34
56
|
|
|
57
|
+
Or as a library:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { renderWorkflow } from "ptxiagram";
|
|
61
|
+
await renderWorkflow("my-diagram.workflow.json", "out.pptx");
|
|
62
|
+
```
|
|
63
|
+
|
|
35
64
|
`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
65
|
|
|
37
|
-
See [`
|
|
66
|
+
See [`SKILL.md`](SKILL.md) for the full JSON shape of all five diagram types, the node-type → shape/color mapping, and the route presets.
|
|
38
67
|
|
|
39
68
|
## Why every file gets repaired
|
|
40
69
|
|
package/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ptxiagram
|
|
3
|
-
description: Create architecture and
|
|
3
|
+
description: Create architecture, workflow, sequence, dataflow, and lifecycle 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, process flow, API call sequence, data pipeline, or state machine 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
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.3.0"
|
|
7
7
|
status: prototype
|
|
8
8
|
based_on: "gitbrent/PptxGenJS (MIT)"
|
|
9
9
|
---
|
|
@@ -51,6 +51,9 @@ validation and the embedded-text dump, just no rendered preview image).
|
|
|
51
51
|
|------|---------|--------|
|
|
52
52
|
| `workflow` | Cross-team processes, approval chains, anything with swimlanes and a happy path | `schemas/workflow.schema.json` |
|
|
53
53
|
| `architecture` | System components, services, datastores, trust boundaries, hub-and-spoke integrations | `schemas/architecture.schema.json` |
|
|
54
|
+
| `sequence` | API call chains, request/response traces, cache-fallback flows — up to 6 participants, up to 9 messages | `schemas/sequence.schema.json` |
|
|
55
|
+
| `dataflow` | Data pipelines, ETL/ELT, PII/lineage annotations, stage-to-stage flows | `schemas/dataflow.schema.json` |
|
|
56
|
+
| `lifecycle` | State machines, status transitions, terminal outcomes (same JSON shape as `workflow`) | `schemas/lifecycle.schema.json` |
|
|
54
57
|
|
|
55
58
|
## The loop
|
|
56
59
|
|
|
@@ -58,6 +61,9 @@ validation and the embedded-text dump, just no rendered preview image).
|
|
|
58
61
|
shape, don't guess the field names:
|
|
59
62
|
- `examples/onboarding.workflow.json`
|
|
60
63
|
- `examples/order-processing.architecture.json`
|
|
64
|
+
- `examples/cache-miss.sequence.json`
|
|
65
|
+
- `examples/product-analytics.dataflow.json`
|
|
66
|
+
- `examples/agent-run.lifecycle.json`
|
|
61
67
|
2. Write `<name>.<type>.json`.
|
|
62
68
|
3. Render:
|
|
63
69
|
```bash
|
|
@@ -127,12 +133,83 @@ order. Default node size is 1.8×0.95in.
|
|
|
127
133
|
`boundaries[].wraps` takes component ids, not coordinates — the renderer
|
|
128
134
|
computes a padded frame around them, so never hand-compute a boundary box.
|
|
129
135
|
|
|
136
|
+
## Sequence JSON shape
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"schema_version": 1,
|
|
141
|
+
"diagram_type": "sequence",
|
|
142
|
+
"meta": { "title": "...", "output": "out.pptx" },
|
|
143
|
+
"participants": [
|
|
144
|
+
{ "id": "api", "type": "backend", "label": "API", "sublabel": "handler" }
|
|
145
|
+
],
|
|
146
|
+
"messages": [
|
|
147
|
+
{ "from": "client", "to": "api", "label": "GET /orders/123", "variant": "emphasis" }
|
|
148
|
+
],
|
|
149
|
+
"cards": []
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Layout budget**: up to 6 participants in a row (auto-spaced, 2.2in apart),
|
|
154
|
+
each with a vertical dashed lifeline. Messages are drawn in JSON array order
|
|
155
|
+
at auto-incrementing y positions (0.55in apart) — order matters, there's no
|
|
156
|
+
explicit `y` field. Up to 9 messages without `cards`, ~6 with (cards need
|
|
157
|
+
room at the bottom, so the lifeline gets shorter). Self-messages (same
|
|
158
|
+
`from`/`to`) aren't supported yet.
|
|
159
|
+
|
|
160
|
+
## Dataflow JSON shape
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"schema_version": 1,
|
|
165
|
+
"diagram_type": "dataflow",
|
|
166
|
+
"meta": { "title": "...", "output": "out.pptx" },
|
|
167
|
+
"stages": [{ "label": "Sources" }, { "label": "Ingest" }],
|
|
168
|
+
"nodes": [
|
|
169
|
+
{ "id": "web", "type": "frontend", "label": "웹 앱", "stage": 0, "row": 0 }
|
|
170
|
+
],
|
|
171
|
+
"flows": [
|
|
172
|
+
{ "from": "web", "to": "queue", "label": "이벤트 전송", "classification": "PII touch", "variant": "security" }
|
|
173
|
+
],
|
|
174
|
+
"cards": []
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Layout budget**: up to 5 stages (columns, 2.6in apart) x 5 rows (fixed y
|
|
179
|
+
positions). `flow.label` is required; `classification` is optional
|
|
180
|
+
free-text rendered in parentheses after the label (e.g. "PII touch",
|
|
181
|
+
"non-PII", "approved only") for sensitivity/lineage annotations. Route is
|
|
182
|
+
picked automatically — `straight` when both ends share a row, `elbow-right`
|
|
183
|
+
otherwise — so flows don't take a `route` field.
|
|
184
|
+
|
|
185
|
+
## Lifecycle JSON shape
|
|
186
|
+
|
|
187
|
+
Structurally identical to the workflow schema (same `lanes`/`nodes`/`edges`
|
|
188
|
+
shape, same layout budget) — only `diagram_type` differs. Convention: a
|
|
189
|
+
`main` lane for the happy-path states and a `terminal` lane for end states.
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"schema_version": 1,
|
|
194
|
+
"diagram_type": "lifecycle",
|
|
195
|
+
"meta": { "title": "...", "output": "out.pptx" },
|
|
196
|
+
"lanes": [{ "id": "main", "label": "실행 단계" }, { "id": "terminal", "label": "종료 결과" }],
|
|
197
|
+
"nodes": [
|
|
198
|
+
{ "id": "queued", "lane": "main", "col": 0, "type": "neutral", "label": "대기 중" },
|
|
199
|
+
{ "id": "completed", "lane": "terminal", "col": 3, "type": "success", "label": "완료" }
|
|
200
|
+
],
|
|
201
|
+
"edges": [{ "from": "queued", "to": "completed", "variant": "emphasis", "route": "drop" }]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
130
205
|
## Node types
|
|
131
206
|
|
|
132
207
|
`client` `frontend` `neutral` `backend` `database` `cache` `queue`
|
|
133
|
-
`external` `security` `decision`
|
|
134
|
-
rect, roundRect, can/cylinder,
|
|
135
|
-
|
|
208
|
+
`external` `security` `decision` `active` `waiting` `success` `failure` —
|
|
209
|
+
each maps to a shape preset (ellipse, rect, roundRect, can/cylinder,
|
|
210
|
+
hexagon, cube, cloud, diamond) and a color pair. The last four
|
|
211
|
+
(`active`/`waiting`/`success`/`failure`) exist for `lifecycle` states;
|
|
212
|
+
pick by role, not by desired color.
|
|
136
213
|
|
|
137
214
|
## Route presets
|
|
138
215
|
|
|
@@ -169,7 +246,11 @@ this automatically; `check` verifies it actually happened. Source:
|
|
|
169
246
|
|
|
170
247
|
## What's not built yet
|
|
171
248
|
|
|
172
|
-
- `sequence`, `dataflow`, `lifecycle` diagram types (only `workflow` and
|
|
173
|
-
`architecture` exist so far)
|
|
174
249
|
- No CJK exact-metrics font measurement — the label-width check is a
|
|
175
250
|
conservative estimate, not real glyph widths
|
|
251
|
+
- Sequence self-messages (a participant messaging itself)
|
|
252
|
+
- No label-vs-label collision detection — two edges converging on the same
|
|
253
|
+
node from different rows can still end up with close (though no longer
|
|
254
|
+
overlapping, after the `elbow-right` label-anchor fix) labels in a busy
|
|
255
|
+
diagram; the validator catches label-vs-*shape* and edge-vs-*node*
|
|
256
|
+
collisions, not label-vs-label
|
package/bin/cli.mjs
CHANGED
|
@@ -7,11 +7,20 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import JSZip from "jszip";
|
|
8
8
|
import { renderWorkflow } from "../renderers/workflow/render-workflow.mjs";
|
|
9
9
|
import { renderArchitecture } from "../renderers/architecture/render-architecture.mjs";
|
|
10
|
+
import { renderSequence } from "../renderers/sequence/render-sequence.mjs";
|
|
11
|
+
import { renderDataflow } from "../renderers/dataflow/render-dataflow.mjs";
|
|
12
|
+
import { renderLifecycle } from "../renderers/lifecycle/render-lifecycle.mjs";
|
|
10
13
|
import { validateAll } from "../lib/validate.mjs";
|
|
11
14
|
|
|
12
15
|
const execFileAsync = promisify(execFile);
|
|
13
16
|
|
|
14
|
-
const RENDERERS = {
|
|
17
|
+
const RENDERERS = {
|
|
18
|
+
workflow: renderWorkflow,
|
|
19
|
+
architecture: renderArchitecture,
|
|
20
|
+
sequence: renderSequence,
|
|
21
|
+
dataflow: renderDataflow,
|
|
22
|
+
lifecycle: renderLifecycle,
|
|
23
|
+
};
|
|
15
24
|
|
|
16
25
|
function usage() {
|
|
17
26
|
console.log(`Usage:
|
|
@@ -20,7 +29,7 @@ function usage() {
|
|
|
20
29
|
cli check <output.pptx>
|
|
21
30
|
cli doctor
|
|
22
31
|
|
|
23
|
-
Types: workflow, architecture`);
|
|
32
|
+
Types: workflow, architecture, sequence, dataflow, lifecycle`);
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
async function cmdRender(type, inputPath, outputPath) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"diagram_type": "lifecycle",
|
|
4
|
+
"meta": {
|
|
5
|
+
"title": "에이전트 실행 라이프사이클",
|
|
6
|
+
"subtitle": "상태와 종료 결과",
|
|
7
|
+
"output": "test/output/agent-run.schema-driven.pptx"
|
|
8
|
+
},
|
|
9
|
+
"lanes": [
|
|
10
|
+
{ "id": "main", "label": "실행 단계" },
|
|
11
|
+
{ "id": "terminal", "label": "종료 결과" }
|
|
12
|
+
],
|
|
13
|
+
"nodes": [
|
|
14
|
+
{ "id": "queued", "lane": "main", "col": 0, "type": "neutral", "label": "대기 중", "sublabel": "큐에 등록됨" },
|
|
15
|
+
{ "id": "running", "lane": "main", "col": 1, "type": "active", "label": "실행 중", "sublabel": "작업 처리" },
|
|
16
|
+
{ "id": "reviewing", "lane": "main", "col": 3, "type": "waiting", "label": "승인 대기", "sublabel": "휴먼 리뷰" },
|
|
17
|
+
{ "id": "completed", "lane": "terminal", "col": 3, "type": "success", "label": "완료" },
|
|
18
|
+
{ "id": "failed", "lane": "terminal", "col": 5, "type": "failure", "label": "실패", "sublabel": "재시도 필요" }
|
|
19
|
+
],
|
|
20
|
+
"edges": [
|
|
21
|
+
{ "from": "queued", "to": "running", "variant": "default", "route": "straight" },
|
|
22
|
+
{ "from": "running", "to": "reviewing", "variant": "default", "route": "straight" },
|
|
23
|
+
{ "from": "reviewing", "to": "completed", "variant": "emphasis", "route": "drop", "label": "승인" },
|
|
24
|
+
{ "from": "reviewing", "to": "failed", "variant": "security", "route": "drop", "label": "반려" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"diagram_type": "sequence",
|
|
4
|
+
"meta": {
|
|
5
|
+
"title": "캐시 미스 요청 흐름",
|
|
6
|
+
"subtitle": "Client → API → Cache → DB",
|
|
7
|
+
"output": "test/output/cache-miss.schema-driven.pptx"
|
|
8
|
+
},
|
|
9
|
+
"participants": [
|
|
10
|
+
{ "id": "client", "type": "client", "label": "Client", "sublabel": "브라우저" },
|
|
11
|
+
{ "id": "api", "type": "backend", "label": "API", "sublabel": "handler" },
|
|
12
|
+
{ "id": "cache", "type": "cache", "label": "Cache", "sublabel": "Redis" },
|
|
13
|
+
{ "id": "db", "type": "database", "label": "DB", "sublabel": "PostgreSQL" }
|
|
14
|
+
],
|
|
15
|
+
"messages": [
|
|
16
|
+
{ "from": "client", "to": "api", "label": "GET /orders/123", "variant": "emphasis" },
|
|
17
|
+
{ "from": "api", "to": "cache", "label": "조회", "variant": "default" },
|
|
18
|
+
{ "from": "cache", "to": "api", "label": "캐시 미스", "variant": "dashed" },
|
|
19
|
+
{ "from": "api", "to": "db", "label": "쿼리 실행", "variant": "emphasis" },
|
|
20
|
+
{ "from": "db", "to": "api", "label": "결과 반환", "variant": "dashed" },
|
|
21
|
+
{ "from": "api", "to": "client", "label": "200 OK", "variant": "emphasis" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"diagram_type": "dataflow",
|
|
4
|
+
"meta": {
|
|
5
|
+
"title": "제품 분석 데이터 파이프라인",
|
|
6
|
+
"subtitle": "이벤트 수집 → 적재 → 소비",
|
|
7
|
+
"output": "test/output/product-analytics.schema-driven.pptx"
|
|
8
|
+
},
|
|
9
|
+
"stages": [
|
|
10
|
+
{ "label": "Sources" },
|
|
11
|
+
{ "label": "Ingest" },
|
|
12
|
+
{ "label": "Store" },
|
|
13
|
+
{ "label": "Consumers" }
|
|
14
|
+
],
|
|
15
|
+
"nodes": [
|
|
16
|
+
{ "id": "web", "type": "frontend", "label": "웹 앱", "sublabel": "clickstream", "stage": 0, "row": 0 },
|
|
17
|
+
{ "id": "mobile", "type": "frontend", "label": "모바일 앱", "sublabel": "clickstream", "stage": 0, "row": 1 },
|
|
18
|
+
{ "id": "queue", "type": "queue", "label": "이벤트 큐", "sublabel": "Kafka", "stage": 1, "row": 0 },
|
|
19
|
+
{ "id": "warehouse", "type": "database", "label": "데이터 웨어하우스", "sublabel": "BigQuery", "stage": 2, "row": 0 },
|
|
20
|
+
{ "id": "dashboard", "type": "external", "label": "대시보드", "sublabel": "내부 분석팀", "stage": 3, "row": 0 },
|
|
21
|
+
{ "id": "ml", "type": "external", "label": "추천 모델", "sublabel": "배치 학습", "stage": 3, "row": 1 }
|
|
22
|
+
],
|
|
23
|
+
"flows": [
|
|
24
|
+
{ "from": "web", "to": "queue", "label": "이벤트 전송", "classification": "PII touch", "variant": "security" },
|
|
25
|
+
{ "from": "mobile", "to": "queue", "label": "이벤트 전송", "classification": "PII touch", "variant": "security" },
|
|
26
|
+
{ "from": "queue", "to": "warehouse", "label": "적재", "classification": "approved only", "variant": "default" },
|
|
27
|
+
{ "from": "warehouse", "to": "dashboard", "label": "집계 쿼리", "classification": "non-PII", "variant": "default" },
|
|
28
|
+
{ "from": "warehouse", "to": "ml", "label": "피처 추출", "classification": "non-PII", "variant": "dashed" }
|
|
29
|
+
]
|
|
30
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export { renderWorkflow } from "./renderers/workflow/render-workflow.mjs";
|
|
2
2
|
export { renderArchitecture } from "./renderers/architecture/render-architecture.mjs";
|
|
3
|
-
export {
|
|
3
|
+
export { renderSequence } from "./renderers/sequence/render-sequence.mjs";
|
|
4
|
+
export { renderDataflow } from "./renderers/dataflow/render-dataflow.mjs";
|
|
5
|
+
export { renderLifecycle } from "./renderers/lifecycle/render-lifecycle.mjs";
|
|
6
|
+
export {
|
|
7
|
+
validateAll,
|
|
8
|
+
validateSchema,
|
|
9
|
+
validateWorkflowLayout,
|
|
10
|
+
validateArchitectureLayout,
|
|
11
|
+
validateSequenceLayout,
|
|
12
|
+
validateDataflowLayout,
|
|
13
|
+
} from "./lib/validate.mjs";
|
|
4
14
|
export { repairPptx } from "./lib/repair-pptx.mjs";
|
package/lib/layout.mjs
CHANGED
|
@@ -47,6 +47,56 @@ export function computeWorkflowLayout(raw) {
|
|
|
47
47
|
return { laneBoxes, nodeBoxes, nodeMeta, cardsY };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export const SEQUENCE_LAYOUT = {
|
|
51
|
+
X0: 0.7, GAP: 2.2, BOX_W: 1.5, BOX_H: 0.7,
|
|
52
|
+
TOP_Y: 1.1, LIFELINE_BOTTOM: 7.0,
|
|
53
|
+
MSG_START_Y: 2.3, MSG_STEP: 0.55,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function computeSequenceLayout(raw) {
|
|
57
|
+
const L = SEQUENCE_LAYOUT;
|
|
58
|
+
const participantBoxes = {};
|
|
59
|
+
const centerX = {};
|
|
60
|
+
raw.participants.forEach((p, i) => {
|
|
61
|
+
const x = L.X0 + i * L.GAP;
|
|
62
|
+
participantBoxes[p.id] = { x, y: L.TOP_Y, w: L.BOX_W, h: L.BOX_H };
|
|
63
|
+
centerX[p.id] = x + L.BOX_W / 2;
|
|
64
|
+
});
|
|
65
|
+
const messageYs = raw.messages.map((_, i) => L.MSG_START_Y + i * L.MSG_STEP);
|
|
66
|
+
// cards need ~1.75in of vertical room, so diagrams that use them get a
|
|
67
|
+
// shorter lifeline (fewer messages fit) instead of overflowing the slide
|
|
68
|
+
const lifelineBottom = raw.cards && raw.cards.length ? 5.3 : L.LIFELINE_BOTTOM;
|
|
69
|
+
const cardsY = lifelineBottom + 0.15;
|
|
70
|
+
return { participantBoxes, centerX, messageYs, lifelineBottom, cardsY };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const DATAFLOW_LAYOUT = {
|
|
74
|
+
STAGE_X0: 1.7, STAGE_GAP: 2.6,
|
|
75
|
+
ROW_Y: [1.3, 2.5, 3.7, 4.9, 6.1],
|
|
76
|
+
NODE_W: 1.6, NODE_H: 0.85,
|
|
77
|
+
STAGE_LABEL_Y: 0.95,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Shared by the dataflow renderer and validator so route selection can
|
|
81
|
+
// never drift between what's drawn and what's checked.
|
|
82
|
+
export function sameRow(a, b) {
|
|
83
|
+
return Math.abs(a.y - b.y) < 0.05;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function computeDataflowLayout(raw) {
|
|
87
|
+
const L = DATAFLOW_LAYOUT;
|
|
88
|
+
const stageCenterX = raw.stages.map((_, i) => L.STAGE_X0 + i * L.STAGE_GAP);
|
|
89
|
+
const nodeBoxes = {};
|
|
90
|
+
for (const n of raw.nodes) {
|
|
91
|
+
const cx = stageCenterX[n.stage];
|
|
92
|
+
if (cx === undefined) throw new Error(`Node "${n.id}" references unknown stage index ${n.stage}`);
|
|
93
|
+
const y = L.ROW_Y[n.row];
|
|
94
|
+
if (y === undefined) throw new Error(`Node "${n.id}" references unknown row index ${n.row}`);
|
|
95
|
+
nodeBoxes[n.id] = { x: cx - L.NODE_W / 2, y, w: L.NODE_W, h: L.NODE_H };
|
|
96
|
+
}
|
|
97
|
+
return { nodeBoxes, stageCenterX };
|
|
98
|
+
}
|
|
99
|
+
|
|
50
100
|
const BOUNDARY_PAD = 0.3;
|
|
51
101
|
|
|
52
102
|
export function computeArchitectureLayout(raw) {
|
package/lib/routes.mjs
CHANGED
|
@@ -65,7 +65,10 @@ export function computeSegments(from, to, opts = {}) {
|
|
|
65
65
|
{ ...boxFromPoints({ x: midX, y: Math.min(a.y, b.y) }, { x: midX, y: Math.max(a.y, b.y) }), arrow: false },
|
|
66
66
|
{ ...boxFromPoints({ x: midX, y: b.y }, b), arrow: true },
|
|
67
67
|
],
|
|
68
|
-
|
|
68
|
+
// anchored at the jog's own midpoint (like "drop" does), not at
|
|
69
|
+
// either endpoint's row -- otherwise this collides with a sibling
|
|
70
|
+
// "straight" edge whose label sits right at the target's row
|
|
71
|
+
labelAnchor: label ? { x: midX - 0.6, y: (a.y + b.y) / 2, w: 1.2 } : null,
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -110,3 +113,19 @@ export function connector(slide, from, to, opts = {}) {
|
|
|
110
113
|
});
|
|
111
114
|
}
|
|
112
115
|
}
|
|
116
|
+
|
|
117
|
+
/** Draws a single point-to-point arrow at an explicit y (sequence
|
|
118
|
+
* messages, self-loops) instead of anchoring to node box sides. Direction
|
|
119
|
+
* matters: the arrowhead lands at (x2,y). opts: { color, dashed, label } */
|
|
120
|
+
export function drawArrow(slide, { x1, x2, y, color = "94A3B8", dashed = false, label }) {
|
|
121
|
+
slide.addShape("line", {
|
|
122
|
+
x: Math.min(x1, x2), y, w: Math.abs(x2 - x1), h: 0,
|
|
123
|
+
line: { color, width: 2, dashType: dashed ? "dash" : "solid", endArrowType: "triangle" },
|
|
124
|
+
});
|
|
125
|
+
if (label) {
|
|
126
|
+
slide.addText(label, {
|
|
127
|
+
x: Math.min(x1, x2), y: y - 0.26, w: Math.abs(x2 - x1), h: 0.24,
|
|
128
|
+
align: "center", fontFace: FONT, fontSize: 8.5, color: "475569",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
package/lib/shapes.mjs
CHANGED
|
@@ -12,6 +12,11 @@ export const NODE_TYPES = {
|
|
|
12
12
|
external: { shape: "cloud", fill: "FEF3C7", stroke: "F59E0B" },
|
|
13
13
|
security: { shape: "roundRect", fill: "FFE4E6", stroke: "F43F5E" },
|
|
14
14
|
decision: { shape: "diamond", fill: "FFE4E6", stroke: "F43F5E" },
|
|
15
|
+
// lifecycle-diagram states
|
|
16
|
+
active: { shape: "roundRect", fill: "DBEAFE", stroke: "3B82F6" },
|
|
17
|
+
waiting: { shape: "roundRect", fill: "FEF3C7", stroke: "F59E0B" },
|
|
18
|
+
success: { shape: "roundRect", fill: "D1FAE5", stroke: "10B981" },
|
|
19
|
+
failure: { shape: "roundRect", fill: "FFE4E6", stroke: "F43F5E" },
|
|
15
20
|
};
|
|
16
21
|
|
|
17
22
|
export const FONT = "맑은 고딕";
|
|
@@ -92,6 +97,15 @@ export function drawCards(slide, cards, y) {
|
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
// Draws a participant's vertical lifeline (a sequence-diagram convention:
|
|
101
|
+
// a thin dashed reference line a participant's messages attach to).
|
|
102
|
+
export function drawLifeline(slide, { x, fromY, toY }) {
|
|
103
|
+
slide.addShape("line", {
|
|
104
|
+
x, y: fromY, w: 0, h: toY - fromY,
|
|
105
|
+
line: { color: "CBD5E1", width: 1, dashType: "dash" },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
// Draws a swimlane frame with a title strip. Returns the lane's box.
|
|
96
110
|
export function drawLane(slide, { x, y, w, h, label }) {
|
|
97
111
|
slide.addShape("rect", {
|
package/lib/validate.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
5
|
-
import { computeWorkflowLayout, computeArchitectureLayout, SLIDE_W, SLIDE_H } from "./layout.mjs";
|
|
5
|
+
import { computeWorkflowLayout, computeArchitectureLayout, computeSequenceLayout, computeDataflowLayout, sameRow, SLIDE_W, SLIDE_H } from "./layout.mjs";
|
|
6
6
|
import { computeSegments } from "./routes.mjs";
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -64,7 +64,7 @@ function offSlide(box) {
|
|
|
64
64
|
return box.x < 0 || box.y < 0 || box.x + box.w > SLIDE_W || box.y + box.h > SLIDE_H;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function checkCommon({ problems, ids, boxes, labelOf, fontSizePt, edgesOrConns, resolveEndpoints, edgeLabel }) {
|
|
67
|
+
function checkCommon({ problems, ids, boxes, labelOf, fontSizePt, edgesOrConns, resolveEndpoints, edgeLabel, resolveRoute = (e) => e.route || "straight" }) {
|
|
68
68
|
for (const id of ids) {
|
|
69
69
|
const box = boxes[id];
|
|
70
70
|
const label = labelOf(id);
|
|
@@ -89,7 +89,7 @@ function checkCommon({ problems, ids, boxes, labelOf, fontSizePt, edgesOrConns,
|
|
|
89
89
|
const { from, to } = resolveEndpoints(e);
|
|
90
90
|
if (!from || !to) continue; // unknown-id errors surface from the renderer/schema instead
|
|
91
91
|
const { segments } = computeSegments(from, to, {
|
|
92
|
-
route: e
|
|
92
|
+
route: resolveRoute(e, from, to), bias: e.bias, clearY: e.clearY, label: e.label,
|
|
93
93
|
});
|
|
94
94
|
for (const id of ids) {
|
|
95
95
|
if (id === e.from || id === e.to) continue;
|
|
@@ -133,11 +133,68 @@ export function validateArchitectureLayout(raw) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
export function validateDataflowLayout(raw) {
|
|
137
|
+
const { nodeBoxes } = computeDataflowLayout(raw);
|
|
138
|
+
const labelOf = (id) => raw.nodes.find((n) => n.id === id).label;
|
|
139
|
+
return checkCommon({
|
|
140
|
+
problems: [],
|
|
141
|
+
ids: Object.keys(nodeBoxes),
|
|
142
|
+
boxes: nodeBoxes,
|
|
143
|
+
labelOf,
|
|
144
|
+
fontSizePt: 12.5,
|
|
145
|
+
edgesOrConns: raw.flows,
|
|
146
|
+
resolveEndpoints: (f) => ({ from: nodeBoxes[f.from], to: nodeBoxes[f.to] }),
|
|
147
|
+
edgeLabel: (f) => `Flow "${f.from}"->"${f.to}"`,
|
|
148
|
+
resolveRoute: (f, from, to) => (from && to && sameRow(from, to) ? "straight" : "elbow-right"),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateSequenceLayout(raw) {
|
|
153
|
+
const problems = [];
|
|
154
|
+
const { participantBoxes, messageYs, lifelineBottom } = computeSequenceLayout(raw);
|
|
155
|
+
const ids = Object.keys(participantBoxes);
|
|
156
|
+
|
|
157
|
+
for (const p of raw.participants) {
|
|
158
|
+
const box = participantBoxes[p.id];
|
|
159
|
+
const w = estimateTextWidth(p.label, 12.5);
|
|
160
|
+
if (w > box.w - 0.1) {
|
|
161
|
+
problems.push(`Participant "${p.id}" label "${p.label}" (~${w.toFixed(2)}in) is wider than its shape (${box.w.toFixed(2)}in) — shorten the label or move detail to sublabel.`);
|
|
162
|
+
}
|
|
163
|
+
if (offSlide(box)) {
|
|
164
|
+
problems.push(`Participant "${p.id}" is off-slide (slide is ${SLIDE_W}x${SLIDE_H}in).`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < ids.length; i++) {
|
|
169
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
170
|
+
if (rectsOverlap(participantBoxes[ids[i]], participantBoxes[ids[j]])) {
|
|
171
|
+
problems.push(`Participants "${ids[i]}" and "${ids[j]}" overlap.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
raw.messages.forEach((m, i) => {
|
|
177
|
+
if (m.from === m.to) {
|
|
178
|
+
problems.push(`Message ${i} ("${m.label}") has the same participant "${m.from}" as from and to — self-messages aren't supported yet.`);
|
|
179
|
+
}
|
|
180
|
+
if (!participantBoxes[m.from]) problems.push(`Message ${i} references unknown participant "${m.from}".`);
|
|
181
|
+
if (!participantBoxes[m.to]) problems.push(`Message ${i} references unknown participant "${m.to}".`);
|
|
182
|
+
if (messageYs[i] > lifelineBottom - 0.3) {
|
|
183
|
+
problems.push(`Message ${i} ("${m.label}") falls below the lifeline (too many messages for the available space) — split into fewer messages or drop the cards to free up room.`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return problems;
|
|
188
|
+
}
|
|
189
|
+
|
|
136
190
|
/** Runs schema validation, then layout collision checks. Returns the list
|
|
137
191
|
* of layout problems (empty = clean); throws if schema validation fails. */
|
|
138
192
|
export async function validateAll(type, raw) {
|
|
139
193
|
await validateSchema(type, raw);
|
|
140
194
|
if (type === "workflow") return validateWorkflowLayout(raw);
|
|
195
|
+
if (type === "lifecycle") return validateWorkflowLayout(raw); // same JSON shape as workflow
|
|
141
196
|
if (type === "architecture") return validateArchitectureLayout(raw);
|
|
197
|
+
if (type === "sequence") return validateSequenceLayout(raw);
|
|
198
|
+
if (type === "dataflow") return validateDataflowLayout(raw);
|
|
142
199
|
throw new Error(`Unknown diagram type: ${type}`);
|
|
143
200
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ptxiagram",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Generate native, editable PowerPoint (PPTX) architecture and
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Generate native, editable PowerPoint (PPTX) architecture, workflow, sequence, dataflow, and lifecycle diagrams from a small JSON file — real shapes and connectors, not an embedded image. Ships with a Claude Skill (SKILL.md) and a CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
7
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"doctor": "node bin/cli.mjs doctor",
|
|
21
|
-
"test": "node test/render-workflow.test.mjs && node test/render-architecture.test.mjs && node test/validate.test.mjs && node test/workflow-lib-smoke.test.mjs"
|
|
21
|
+
"test": "node test/render-workflow.test.mjs && node test/render-architecture.test.mjs && node test/render-sequence.test.mjs && node test/render-dataflow.test.mjs && node test/render-lifecycle.test.mjs && node test/validate.test.mjs && node test/workflow-lib-smoke.test.mjs"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"pptx",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"claude-skill",
|
|
28
28
|
"architecture-diagram",
|
|
29
29
|
"workflow-diagram",
|
|
30
|
+
"sequence-diagram",
|
|
31
|
+
"dataflow-diagram",
|
|
32
|
+
"lifecycle-diagram",
|
|
30
33
|
"hancom",
|
|
31
34
|
"hanshow",
|
|
32
35
|
"office-automation"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pptxgen from "pptxgenjs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { drawNode, drawBoundary, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
|
|
4
5
|
import { connector } from "../../lib/routes.mjs";
|
|
5
6
|
import { repairPptx } from "../../lib/repair-pptx.mjs";
|
|
@@ -62,6 +63,7 @@ export async function renderArchitecture(inputPath, outputPath) {
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
const outFile = outputPath || raw.meta.output || "output.pptx";
|
|
66
|
+
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
65
67
|
await pptx.writeFile({ fileName: outFile });
|
|
66
68
|
await repairPptx(outFile);
|
|
67
69
|
return outFile;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pptxgen from "pptxgenjs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { drawNode, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
|
|
5
|
+
import { connector } from "../../lib/routes.mjs";
|
|
6
|
+
import { repairPptx } from "../../lib/repair-pptx.mjs";
|
|
7
|
+
import { validateSchema, validateDataflowLayout } from "../../lib/validate.mjs";
|
|
8
|
+
import { computeDataflowLayout, DATAFLOW_LAYOUT, sameRow } from "../../lib/layout.mjs";
|
|
9
|
+
|
|
10
|
+
export async function renderDataflow(inputPath, outputPath) {
|
|
11
|
+
const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
|
|
12
|
+
await validateSchema("dataflow", raw);
|
|
13
|
+
|
|
14
|
+
const { nodeBoxes, stageCenterX } = computeDataflowLayout(raw);
|
|
15
|
+
const problems = validateDataflowLayout(raw);
|
|
16
|
+
if (problems.length) {
|
|
17
|
+
throw new Error(`Dataflow layout validation failed:\n- ${problems.join("\n- ")}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pptx = new pptxgen();
|
|
21
|
+
pptx.defineLayout({ name: "WIDE", width: 13.33, height: 7.5 });
|
|
22
|
+
pptx.layout = "WIDE";
|
|
23
|
+
const slide = pptx.addSlide();
|
|
24
|
+
|
|
25
|
+
slide.addText(raw.meta.title, {
|
|
26
|
+
x: 0.4, y: 0.2, w: 12.5, h: 0.5,
|
|
27
|
+
fontFace: FONT, fontSize: 20, bold: true, color: "0F172A",
|
|
28
|
+
});
|
|
29
|
+
if (raw.meta.subtitle) {
|
|
30
|
+
slide.addText(raw.meta.subtitle, {
|
|
31
|
+
x: 0.4, y: 0.65, w: 12.5, h: 0.3,
|
|
32
|
+
fontFace: FONT, fontSize: 11, color: "64748B",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
raw.stages.forEach((s, i) => {
|
|
37
|
+
slide.addText(s.label, {
|
|
38
|
+
x: stageCenterX[i] - 1.0, y: DATAFLOW_LAYOUT.STAGE_LABEL_Y, w: 2.0, h: 0.3,
|
|
39
|
+
align: "center", fontFace: FONT, fontSize: 11, bold: true, color: "64748B",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
for (const n of raw.nodes) {
|
|
44
|
+
drawNode(slide, { ...nodeBoxes[n.id], label: n.label, sublabel: n.sublabel, type: n.type, tag: n.tag });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const f of raw.flows) {
|
|
48
|
+
const variant = EDGE_VARIANTS[f.variant || "default"];
|
|
49
|
+
const from = nodeBoxes[f.from], to = nodeBoxes[f.to];
|
|
50
|
+
if (!from) throw new Error(`Flow references unknown node "${f.from}"`);
|
|
51
|
+
if (!to) throw new Error(`Flow references unknown node "${f.to}"`);
|
|
52
|
+
const label = f.classification ? `${f.label} (${f.classification})` : f.label;
|
|
53
|
+
connector(slide, from, to, {
|
|
54
|
+
color: variant.color, dashed: variant.dashed, label,
|
|
55
|
+
route: sameRow(from, to) ? "straight" : "elbow-right",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (raw.cards && raw.cards.length) {
|
|
60
|
+
drawCards(slide, raw.cards, 6.5);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
slide.addText("Made with a Claude Skill · native PPTX shapes, editable in PowerPoint", {
|
|
64
|
+
x: 0.4, y: 7.15, w: 12.5, h: 0.3,
|
|
65
|
+
fontFace: FONT, fontSize: 8, color: "94A3B8", italic: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const outFile = outputPath || raw.meta.output || "output.pptx";
|
|
69
|
+
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
70
|
+
await pptx.writeFile({ fileName: outFile });
|
|
71
|
+
await repairPptx(outFile);
|
|
72
|
+
return outFile;
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { renderWorkflow } from "../workflow/render-workflow.mjs";
|
|
2
|
+
|
|
3
|
+
// lifecycle.schema.json is structurally identical to workflow.schema.json
|
|
4
|
+
// (lanes/nodes/edges, same layout budget) -- it only differs in its
|
|
5
|
+
// diagram_type const and typical usage (states + terminal outcomes instead
|
|
6
|
+
// of process steps), so it reuses the workflow renderer wholesale rather
|
|
7
|
+
// than duplicating the same ~70 lines under a different file name.
|
|
8
|
+
export async function renderLifecycle(inputPath, outputPath) {
|
|
9
|
+
return renderWorkflow(inputPath, outputPath, "lifecycle");
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pptxgen from "pptxgenjs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { drawNode, drawLifeline, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
|
|
5
|
+
import { drawArrow } from "../../lib/routes.mjs";
|
|
6
|
+
import { repairPptx } from "../../lib/repair-pptx.mjs";
|
|
7
|
+
import { validateSchema, validateSequenceLayout } from "../../lib/validate.mjs";
|
|
8
|
+
import { computeSequenceLayout } from "../../lib/layout.mjs";
|
|
9
|
+
|
|
10
|
+
export async function renderSequence(inputPath, outputPath) {
|
|
11
|
+
const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
|
|
12
|
+
await validateSchema("sequence", raw);
|
|
13
|
+
|
|
14
|
+
const { participantBoxes, centerX, messageYs, lifelineBottom, cardsY } = computeSequenceLayout(raw);
|
|
15
|
+
const problems = validateSequenceLayout(raw);
|
|
16
|
+
if (problems.length) {
|
|
17
|
+
throw new Error(`Sequence layout validation failed:\n- ${problems.join("\n- ")}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pptx = new pptxgen();
|
|
21
|
+
pptx.defineLayout({ name: "WIDE", width: 13.33, height: 7.5 });
|
|
22
|
+
pptx.layout = "WIDE";
|
|
23
|
+
const slide = pptx.addSlide();
|
|
24
|
+
|
|
25
|
+
slide.addText(raw.meta.title, {
|
|
26
|
+
x: 0.4, y: 0.2, w: 12.5, h: 0.5,
|
|
27
|
+
fontFace: FONT, fontSize: 20, bold: true, color: "0F172A",
|
|
28
|
+
});
|
|
29
|
+
if (raw.meta.subtitle) {
|
|
30
|
+
slide.addText(raw.meta.subtitle, {
|
|
31
|
+
x: 0.4, y: 0.65, w: 12.5, h: 0.3,
|
|
32
|
+
fontFace: FONT, fontSize: 11, color: "64748B",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const p of raw.participants) {
|
|
37
|
+
drawNode(slide, { ...participantBoxes[p.id], label: p.label, sublabel: p.sublabel, type: p.type });
|
|
38
|
+
drawLifeline(slide, {
|
|
39
|
+
x: centerX[p.id],
|
|
40
|
+
fromY: participantBoxes[p.id].y + participantBoxes[p.id].h,
|
|
41
|
+
toY: lifelineBottom,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
raw.messages.forEach((m, i) => {
|
|
46
|
+
const variant = EDGE_VARIANTS[m.variant || "default"];
|
|
47
|
+
drawArrow(slide, {
|
|
48
|
+
x1: centerX[m.from], x2: centerX[m.to], y: messageYs[i],
|
|
49
|
+
color: variant.color, dashed: variant.dashed, label: m.label,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (raw.cards && raw.cards.length) {
|
|
54
|
+
drawCards(slide, raw.cards, cardsY);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
slide.addText("Made with a Claude Skill · native PPTX shapes, editable in PowerPoint", {
|
|
58
|
+
x: 0.4, y: 7.15, w: 12.5, h: 0.3,
|
|
59
|
+
fontFace: FONT, fontSize: 8, color: "94A3B8", italic: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const outFile = outputPath || raw.meta.output || "output.pptx";
|
|
63
|
+
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
64
|
+
await pptx.writeFile({ fileName: outFile });
|
|
65
|
+
await repairPptx(outFile);
|
|
66
|
+
return outFile;
|
|
67
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import pptxgen from "pptxgenjs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { drawNode, drawLane, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
|
|
4
5
|
import { connector } from "../../lib/routes.mjs";
|
|
5
6
|
import { repairPptx } from "../../lib/repair-pptx.mjs";
|
|
6
7
|
import { validateSchema, validateWorkflowLayout } from "../../lib/validate.mjs";
|
|
7
8
|
import { computeWorkflowLayout } from "../../lib/layout.mjs";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// `schemaType` lets renderLifecycle (schemas/lifecycle.schema.json) reuse
|
|
11
|
+
// this renderer as-is: lifecycle JSON is structurally identical to
|
|
12
|
+
// workflow's (lanes/nodes/edges), it just validates against its own
|
|
13
|
+
// diagram_type const and typically uses the start/active/waiting/
|
|
14
|
+
// success/failure node types instead of workflow's process-step ones.
|
|
15
|
+
export async function renderWorkflow(inputPath, outputPath, schemaType = "workflow") {
|
|
10
16
|
const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
|
|
11
|
-
await validateSchema(
|
|
17
|
+
await validateSchema(schemaType, raw);
|
|
12
18
|
|
|
13
19
|
const { laneBoxes, nodeBoxes, cardsY } = computeWorkflowLayout(raw);
|
|
14
20
|
const problems = validateWorkflowLayout(raw);
|
|
@@ -61,6 +67,7 @@ export async function renderWorkflow(inputPath, outputPath) {
|
|
|
61
67
|
});
|
|
62
68
|
|
|
63
69
|
const outFile = outputPath || raw.meta.output || "output.pptx";
|
|
70
|
+
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
64
71
|
await pptx.writeFile({ fileName: outFile });
|
|
65
72
|
await repairPptx(outFile);
|
|
66
73
|
return outFile;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"maxItems": 2
|
|
16
16
|
},
|
|
17
17
|
"nodeType": {
|
|
18
|
-
"enum": ["client", "frontend", "neutral", "backend", "database", "cache", "queue", "external", "security", "decision"]
|
|
18
|
+
"enum": ["client", "frontend", "neutral", "backend", "database", "cache", "queue", "external", "security", "decision", "active", "waiting", "success", "failure"]
|
|
19
19
|
},
|
|
20
20
|
"route": {
|
|
21
21
|
"enum": ["straight", "drop", "elbow-right", "arc-over-top"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://claude-skills/pptx-diagram/schemas/dataflow.schema.json",
|
|
4
|
+
"title": "Native PPTX Dataflow Diagram",
|
|
5
|
+
"description": "Nodes sit in a stage (column, left-to-right) x row (0-4) grid. Flows carry a mandatory label plus an optional classification (e.g. 'PII touch', 'non-PII') for sensitivity/lineage annotations.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "diagram_type", "meta", "stages", "nodes", "flows"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": { "const": 1 },
|
|
11
|
+
"diagram_type": { "const": "dataflow" },
|
|
12
|
+
"meta": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["title"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"title": { "type": "string", "minLength": 1 },
|
|
18
|
+
"subtitle": { "type": "string" },
|
|
19
|
+
"output": { "type": "string" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"stages": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 2,
|
|
25
|
+
"maxItems": 5,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["label"],
|
|
30
|
+
"properties": { "label": { "type": "string", "minLength": 1 } }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"nodes": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"minItems": 1,
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["id", "type", "label", "stage", "row"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"id": { "$ref": "common.schema.json#/$defs/id" },
|
|
42
|
+
"type": { "$ref": "common.schema.json#/$defs/nodeType" },
|
|
43
|
+
"label": { "type": "string", "minLength": 1 },
|
|
44
|
+
"sublabel": { "type": "string" },
|
|
45
|
+
"tag": { "type": "string" },
|
|
46
|
+
"stage": { "type": "integer", "minimum": 0, "maximum": 4 },
|
|
47
|
+
"row": { "type": "integer", "minimum": 0, "maximum": 4 }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"flows": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"required": ["from", "to", "label"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"from": { "$ref": "common.schema.json#/$defs/id" },
|
|
59
|
+
"to": { "$ref": "common.schema.json#/$defs/id" },
|
|
60
|
+
"label": { "type": "string", "minLength": 1 },
|
|
61
|
+
"classification": { "type": "string" },
|
|
62
|
+
"variant": { "enum": ["default", "emphasis", "security", "dashed"] }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"cards": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"items": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["title", "items"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"title": { "type": "string", "minLength": 1 },
|
|
74
|
+
"color": { "$ref": "common.schema.json#/$defs/hexColor" },
|
|
75
|
+
"items": { "type": "array", "items": { "type": "string" } }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://claude-skills/pptx-diagram/schemas/lifecycle.schema.json",
|
|
4
|
+
"title": "Native PPTX Lifecycle Diagram",
|
|
5
|
+
"description": "Structurally identical to the workflow schema (lanes/nodes/edges, same layout budget) -- use it for state machines instead of process flows. Convention: a 'main' lane for the happy-path states and a 'terminal' lane for end states, with node types start/active/waiting/success/failure.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "diagram_type", "meta", "lanes", "nodes", "edges"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": { "const": 1 },
|
|
11
|
+
"diagram_type": { "const": "lifecycle" },
|
|
12
|
+
"meta": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["title"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"title": { "type": "string", "minLength": 1 },
|
|
18
|
+
"subtitle": { "type": "string" },
|
|
19
|
+
"output": { "type": "string" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"lanes": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["id", "label"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"id": { "$ref": "common.schema.json#/$defs/id" },
|
|
31
|
+
"label": { "type": "string", "minLength": 1 },
|
|
32
|
+
"height": { "type": "number", "minimum": 1.2, "maximum": 3, "description": "inches; default 1.75" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"nodes": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"minItems": 1,
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["id", "lane", "type", "label"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"id": { "$ref": "common.schema.json#/$defs/id" },
|
|
45
|
+
"lane": { "$ref": "common.schema.json#/$defs/id" },
|
|
46
|
+
"col": { "type": "integer", "minimum": 0, "maximum": 5, "description": "slot index within the lane; auto-assigned to the next free slot if omitted" },
|
|
47
|
+
"type": { "$ref": "common.schema.json#/$defs/nodeType" },
|
|
48
|
+
"label": { "type": "string", "minLength": 1 },
|
|
49
|
+
"sublabel": { "type": "string" },
|
|
50
|
+
"tag": { "type": "string" },
|
|
51
|
+
"width": { "type": "number", "minimum": 0.6, "maximum": 1.85, "description": "inches; default 1.8, capped so it never exceeds one slot" },
|
|
52
|
+
"height": { "type": "number", "minimum": 0.5, "maximum": 1.5, "description": "inches; default 0.95" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"edges": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"items": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"required": ["from", "to"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"from": { "$ref": "common.schema.json#/$defs/id" },
|
|
64
|
+
"to": { "$ref": "common.schema.json#/$defs/id" },
|
|
65
|
+
"label": { "type": "string" },
|
|
66
|
+
"variant": { "enum": ["default", "emphasis", "security", "dashed"] },
|
|
67
|
+
"route": { "$ref": "common.schema.json#/$defs/route" },
|
|
68
|
+
"bias": { "type": "number", "minimum": 0.1, "maximum": 0.9 }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"cards": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": ["title", "items"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"title": { "type": "string", "minLength": 1 },
|
|
80
|
+
"color": { "$ref": "common.schema.json#/$defs/hexColor" },
|
|
81
|
+
"items": { "type": "array", "items": { "type": "string" } }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://claude-skills/pptx-diagram/schemas/sequence.schema.json",
|
|
4
|
+
"title": "Native PPTX Sequence Diagram",
|
|
5
|
+
"description": "Participants sit in a row (max 6, auto-spaced) with a vertical lifeline; messages are drawn in JSON order at auto-incrementing y positions. See renderers/sequence for the layout budget.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "diagram_type", "meta", "participants", "messages"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": { "const": 1 },
|
|
11
|
+
"diagram_type": { "const": "sequence" },
|
|
12
|
+
"meta": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["title"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"title": { "type": "string", "minLength": 1 },
|
|
18
|
+
"subtitle": { "type": "string" },
|
|
19
|
+
"output": { "type": "string" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"participants": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 2,
|
|
25
|
+
"maxItems": 6,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["id", "type", "label"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"id": { "$ref": "common.schema.json#/$defs/id" },
|
|
32
|
+
"type": { "$ref": "common.schema.json#/$defs/nodeType" },
|
|
33
|
+
"label": { "type": "string", "minLength": 1 },
|
|
34
|
+
"sublabel": { "type": "string" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"messages": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"maxItems": 9,
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": ["from", "to", "label"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"from": { "$ref": "common.schema.json#/$defs/id" },
|
|
48
|
+
"to": { "$ref": "common.schema.json#/$defs/id" },
|
|
49
|
+
"label": { "type": "string", "minLength": 1 },
|
|
50
|
+
"variant": { "enum": ["default", "emphasis", "security", "dashed"] }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"cards": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["title", "items"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"title": { "type": "string", "minLength": 1 },
|
|
62
|
+
"color": { "$ref": "common.schema.json#/$defs/hexColor" },
|
|
63
|
+
"items": { "type": "array", "items": { "type": "string" } }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|