@bpmnkit/ascii 0.0.8

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 ADDED
@@ -0,0 +1,93 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/bpmn-sdk/monorepo/main/doc/logos/logo-2-gateway.svg" width="72" height="72" alt="BPMN Kit logo">
3
+ <h1>@bpmnkit/ascii</h1>
4
+ <p>Render BPMN diagrams as Unicode box-drawing ASCII art — perfect for terminals and docs</p>
5
+
6
+ [![npm](https://img.shields.io/npm/v/@bpmnkit/ascii?style=flat-square&color=6244d7)](https://www.npmjs.com/package/@bpmnkit/ascii)
7
+ [![license](https://img.shields.io/npm/l/@bpmnkit/ascii?style=flat-square)](https://github.com/bpmnkit/monorepo/blob/main/LICENSE)
8
+ [![typescript](https://img.shields.io/badge/TypeScript-strict-6244d7?style=flat-square&logo=typescript&logoColor=white)](https://github.com/bpmnkit/monorepo)
9
+
10
+ [Documentation](https://bpmn-sdk-docs.pages.dev) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/ascii/CHANGELOG.md)
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## Overview
16
+
17
+ `@bpmnkit/ascii` converts BPMN 2.0 diagrams into Unicode box-drawing text art. It uses the same Sugiyama layout engine as the visual renderer, so the spatial flow of the diagram is preserved.
18
+
19
+ Useful for: CLI output, plain-text documentation, terminal UIs, test snapshots, and LLM prompts where visual BPMN isn't available.
20
+
21
+ ## Features
22
+
23
+ - **Unicode box-drawing** — `┌─┐`, `│`, `└─┘`, `→` for clean terminal output
24
+ - **Automatic layout** — uses `@bpmnkit/core`'s layout engine
25
+ - **All element types** — tasks, events, gateways, sub-processes
26
+ - **Configurable output** — optional title, element type labels
27
+ - **Zero additional dependencies** — only requires `@bpmnkit/core` (already bundled)
28
+
29
+ ## Installation
30
+
31
+ ```sh
32
+ npm install @bpmnkit/ascii
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```typescript
38
+ import { renderBpmnAscii } from "@bpmnkit/ascii"
39
+ import { readFileSync } from "node:fs"
40
+
41
+ const xml = readFileSync("my-process.bpmn", "utf8")
42
+ const art = renderBpmnAscii(xml, { title: true, showTypes: true })
43
+ console.log(art)
44
+ ```
45
+
46
+ ### Example output
47
+
48
+ ```
49
+ Order Flow
50
+ ══════════════════════════════════════════════
51
+ ╭──────────╮ ╭───────────────╮ ╭╮
52
+ │ Order │────▶│ Validate Order│────▶││
53
+ │ Received │ │ (ServiceTask) │ │◇│
54
+ ╰──────────╯ ╰───────────────╯ ╰╯
55
+
56
+ ╭───────────────╯│╰───────────────╮
57
+ ▼ ▼
58
+ ╭───────────╮ ╭──────────────╮
59
+ │ Fulfill │ │ Reject │
60
+ │ Order │ │ Order │
61
+ ╰───────────╯ ╰──────────────╯
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ```typescript
67
+ function renderBpmnAscii(xml: string, options?: RenderOptions): string
68
+
69
+ interface RenderOptions {
70
+ title?: boolean // Show process name as header. Default: false
71
+ showTypes?: boolean // Include element type in boxes. Default: false
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Related Packages
78
+
79
+ | Package | Description |
80
+ |---------|-------------|
81
+ | [`@bpmnkit/core`](https://www.npmjs.com/package/@bpmnkit/core) | BPMN/DMN/Form parser, builder, layout engine |
82
+ | [`@bpmnkit/canvas`](https://www.npmjs.com/package/@bpmnkit/canvas) | Zero-dependency SVG BPMN viewer |
83
+ | [`@bpmnkit/editor`](https://www.npmjs.com/package/@bpmnkit/editor) | Full-featured interactive BPMN editor |
84
+ | [`@bpmnkit/engine`](https://www.npmjs.com/package/@bpmnkit/engine) | Lightweight BPMN process execution engine |
85
+ | [`@bpmnkit/feel`](https://www.npmjs.com/package/@bpmnkit/feel) | FEEL expression language parser & evaluator |
86
+ | [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
87
+ | [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
88
+ | [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
89
+ | [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
90
+
91
+ ## License
92
+
93
+ [MIT](https://github.com/bpmnkit/monorepo/blob/main/LICENSE) © bpmn-sdk
package/dist/dmn.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { RenderOptions } from "./types.js";
2
+ /**
3
+ * Render a DMN XML string as ASCII art.
4
+ *
5
+ * Each decision with a decision table is rendered as a labeled table with
6
+ * box-drawing characters. Multiple decisions are separated by a blank line.
7
+ */
8
+ export declare function renderDmnAscii(xml: string, options?: RenderOptions): string;
9
+ //# sourceMappingURL=dmn.d.ts.map
package/dist/dmn.js ADDED
@@ -0,0 +1,130 @@
1
+ import { Dmn } from "@bpmnkit/core";
2
+ // ── Box-drawing characters ───────────────────────────────────────────────────
3
+ const H = "═";
4
+ const V = "║";
5
+ const TL = "╔";
6
+ const TR = "╗";
7
+ const BL = "╚";
8
+ const BR = "╝";
9
+ const TM = "╦";
10
+ const BM = "╩";
11
+ const LM = "╠";
12
+ const RM = "╣";
13
+ const MM = "╬";
14
+ // ── Helpers ──────────────────────────────────────────────────────────────────
15
+ function pad(s, w) {
16
+ return s.length >= w ? s.slice(0, w) : s + " ".repeat(w - s.length);
17
+ }
18
+ function repeat(ch, n) {
19
+ return n <= 0 ? "" : ch.repeat(n);
20
+ }
21
+ // ── Decision table renderer ──────────────────────────────────────────────────
22
+ function renderTable(name, table) {
23
+ const { hitPolicy, aggregation, inputs, outputs, rules } = table;
24
+ // Column widths: pad each column to at least the header label width
25
+ const numW = Math.max(1, String(rules.length).length);
26
+ const inputWidths = inputs.map((inp, i) => {
27
+ const headerW = (inp.label ?? inp.inputExpression.text ?? `In${i}`).length;
28
+ const entryW = rules.reduce((m, r) => {
29
+ const t = r.inputEntries[i]?.text ?? "";
30
+ return Math.max(m, t.length);
31
+ }, 0);
32
+ return Math.max(headerW, entryW, 4);
33
+ });
34
+ const outputWidths = outputs.map((out, i) => {
35
+ const headerW = (out.label ?? out.name ?? `Out${i}`).length;
36
+ const entryW = rules.reduce((m, r) => {
37
+ const t = r.outputEntries[i]?.text ?? "";
38
+ return Math.max(m, t.length);
39
+ }, 0);
40
+ return Math.max(headerW, entryW, 4);
41
+ });
42
+ // Hit policy label for the # column header cell
43
+ const policy = hitPolicy ?? "UNIQUE";
44
+ const policyLabel = aggregation ? `${policy}(${aggregation[0]})` : policy;
45
+ const numHeader = pad(policyLabel, numW);
46
+ // Build column separators
47
+ const numBar = repeat(H, numW + 2);
48
+ const inputBars = inputWidths.map((w) => repeat(H, w + 2));
49
+ const outputBars = outputWidths.map((w) => repeat(H, w + 2));
50
+ const allBars = [numBar, ...inputBars, ...outputBars];
51
+ function topRow() {
52
+ return TL + allBars.join(TM) + TR;
53
+ }
54
+ function midRow() {
55
+ return LM + allBars.join(MM) + RM;
56
+ }
57
+ function botRow() {
58
+ return BL + allBars.join(BM) + BR;
59
+ }
60
+ function cell(s, w) {
61
+ return ` ${pad(s, w)} `;
62
+ }
63
+ const lines = [];
64
+ // Header above the table
65
+ const tableTitle = name ? `${name}` : "Decision";
66
+ const hitLine = ` [${policy}${aggregation ? `/${aggregation}` : ""}]`;
67
+ lines.push(tableTitle + hitLine);
68
+ lines.push("─".repeat(tableTitle.length + hitLine.length));
69
+ lines.push("");
70
+ // Top border
71
+ lines.push(topRow());
72
+ // Column headers
73
+ const headerCells = [
74
+ cell(numHeader, numW),
75
+ ...inputs.map((inp, i) => cell(inp.label ?? inp.inputExpression.text ?? `In${i}`, inputWidths[i] ?? 0)),
76
+ ...outputs.map((out, i) => cell(out.label ?? out.name ?? `Out${i}`, outputWidths[i] ?? 0)),
77
+ ];
78
+ lines.push(V + headerCells.join(V) + V);
79
+ // Separator
80
+ lines.push(midRow());
81
+ // Rules
82
+ if (rules.length === 0) {
83
+ const emptyW = allBars.reduce((s, b) => s + b.length, 0) + allBars.length - 1;
84
+ lines.push(`${V} ${pad("(no rules)", emptyW)} ${V}`);
85
+ }
86
+ else {
87
+ for (let r = 0; r < rules.length; r++) {
88
+ const rule = rules[r];
89
+ if (!rule)
90
+ continue;
91
+ const ruleCells = [
92
+ cell(String(r + 1), numW),
93
+ ...inputs.map((_, i) => cell(rule.inputEntries[i]?.text ?? "", inputWidths[i] ?? 0)),
94
+ ...outputs.map((_, i) => cell(rule.outputEntries[i]?.text ?? "", outputWidths[i] ?? 0)),
95
+ ];
96
+ lines.push(V + ruleCells.join(V) + V);
97
+ }
98
+ }
99
+ // Bottom border
100
+ lines.push(botRow());
101
+ return lines.join("\n");
102
+ }
103
+ // ── Public API ───────────────────────────────────────────────────────────────
104
+ /**
105
+ * Render a DMN XML string as ASCII art.
106
+ *
107
+ * Each decision with a decision table is rendered as a labeled table with
108
+ * box-drawing characters. Multiple decisions are separated by a blank line.
109
+ */
110
+ export function renderDmnAscii(xml, options) {
111
+ const defs = Dmn.parse(xml);
112
+ const decisions = defs.decisions.filter((d) => d.decisionTable);
113
+ if (decisions.length === 0)
114
+ return "(no decision tables)";
115
+ const blocks = decisions.map((d) => renderTable(d.name, d.decisionTable));
116
+ const body = blocks.join("\n\n");
117
+ const title = resolveTitle(options, defs.name);
118
+ if (!title)
119
+ return body;
120
+ const line = "─".repeat(title.length);
121
+ return `${title}\n${line}\n\n${body}`;
122
+ }
123
+ function resolveTitle(options, definitionsName) {
124
+ if (options?.title === false)
125
+ return undefined;
126
+ if (typeof options?.title === "string")
127
+ return options.title;
128
+ return definitionsName ?? undefined;
129
+ }
130
+ //# sourceMappingURL=dmn.js.map
@@ -0,0 +1,16 @@
1
+ import type { AsciiGrid } from "./grid.js";
2
+ /**
3
+ * Draw an orthogonal sequence-flow edge from a source exit point to a target
4
+ * entry point, with an optional short label floated above the edge mid-point.
5
+ *
6
+ * `srcCol` — first column after the source element's right border.
7
+ * `dstCol` — column of the target element's left border (arrow lands at dstCol-1).
8
+ *
9
+ * Routing strategy:
10
+ * • Same row → direct horizontal ──────►
11
+ * • Going down → right + turn-down + right ──┐ / └──►
12
+ * • Going up → right + turn-up + right ──┘ / ┌──►
13
+ * • Backward → route above the diagram via row 0
14
+ */
15
+ export declare function drawEdge(grid: AsciiGrid, srcCol: number, srcRow: number, dstCol: number, dstRow: number, label?: string): void;
16
+ //# sourceMappingURL=edges.d.ts.map
package/dist/edges.js ADDED
@@ -0,0 +1,91 @@
1
+ import { truncate } from "./util.js";
2
+ /**
3
+ * Draw an orthogonal sequence-flow edge from a source exit point to a target
4
+ * entry point, with an optional short label floated above the edge mid-point.
5
+ *
6
+ * `srcCol` — first column after the source element's right border.
7
+ * `dstCol` — column of the target element's left border (arrow lands at dstCol-1).
8
+ *
9
+ * Routing strategy:
10
+ * • Same row → direct horizontal ──────►
11
+ * • Going down → right + turn-down + right ──┐ / └──►
12
+ * • Going up → right + turn-up + right ──┘ / ┌──►
13
+ * • Backward → route above the diagram via row 0
14
+ */
15
+ export function drawEdge(grid, srcCol, srcRow, dstCol, dstRow, label) {
16
+ if (dstCol <= srcCol) {
17
+ drawBackwardEdge(grid, srcCol, srcRow, dstCol, dstRow);
18
+ return;
19
+ }
20
+ if (srcRow === dstRow) {
21
+ drawHorizontal(grid, srcCol, dstCol, srcRow, label);
22
+ return;
23
+ }
24
+ // L-shaped route: bend immediately after the source exit so that multiple
25
+ // outgoing edges from the same element diverge right at the exit point,
26
+ // producing clean ├ junctions rather than a shared horizontal segment.
27
+ const midCol = srcCol + 1;
28
+ const goingDown = dstRow > srcRow;
29
+ // Horizontal leg: srcCol → midCol-1
30
+ drawHorizontalSegment(grid, srcCol, midCol - 1, srcRow);
31
+ // Turn at (midCol, srcRow)
32
+ grid.setLine(midCol, srcRow, goingDown ? "┐" : "┘");
33
+ // Vertical leg
34
+ const rowLo = Math.min(srcRow, dstRow);
35
+ const rowHi = Math.max(srcRow, dstRow);
36
+ for (let r = rowLo + 1; r < rowHi; r++) {
37
+ grid.setLine(midCol, r, "│");
38
+ }
39
+ // Turn at (midCol, dstRow)
40
+ grid.setLine(midCol, dstRow, goingDown ? "└" : "┌");
41
+ // Horizontal leg: midCol+1 → dstCol-2, then arrow
42
+ drawHorizontalSegment(grid, midCol + 1, dstCol - 2, dstRow);
43
+ grid.set(dstCol - 1, dstRow, "►");
44
+ // Label: float above the first horizontal segment's midpoint
45
+ if (label) {
46
+ const labelCol = Math.floor((srcCol + midCol) / 2) - Math.floor(label.length / 2);
47
+ grid.write(labelCol, srcRow - 1, truncate(label, midCol - srcCol));
48
+ }
49
+ }
50
+ /** Draw a straight horizontal edge from srcCol to dstCol on the given row. */
51
+ function drawHorizontal(grid, srcCol, dstCol, row, label) {
52
+ drawHorizontalSegment(grid, srcCol, dstCol - 2, row);
53
+ grid.set(dstCol - 1, row, "►");
54
+ if (label) {
55
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
56
+ grid.write(mid, row - 1, truncate(label, dstCol - srcCol));
57
+ }
58
+ }
59
+ /** Fill `─` characters from col `a` to col `b` inclusive on the given row.
60
+ * Uses setLine so characters merge correctly at junctions (e.g. ─+┐→┬, ─+│→┼). */
61
+ function drawHorizontalSegment(grid, a, b, row) {
62
+ for (let c = a; c <= b; c++) {
63
+ grid.setLine(c, row, "─");
64
+ }
65
+ }
66
+ /**
67
+ * Backward (loop) edge: routes above the diagram (row 0) so it doesn't
68
+ * collide with forward-direction elements.
69
+ *
70
+ * Route: go up from src to row 0, go left to dstCol, go down to dst.
71
+ */
72
+ function drawBackwardEdge(grid, srcCol, srcRow, dstCol, dstRow) {
73
+ const routeRow = 0;
74
+ // Vertical up from srcRow to routeRow
75
+ for (let r = routeRow + 1; r < srcRow; r++) {
76
+ grid.setLine(srcCol, r, "│");
77
+ }
78
+ grid.setLine(srcCol, srcRow, "┘");
79
+ grid.setLine(srcCol, routeRow, "┐");
80
+ // Horizontal along routeRow from dstCol+1 to srcCol-1
81
+ for (let c = dstCol + 1; c < srcCol; c++) {
82
+ grid.setLine(c, routeRow, "─");
83
+ }
84
+ grid.setLine(dstCol, routeRow, "┌");
85
+ // Vertical down from routeRow to dstRow
86
+ for (let r = routeRow + 1; r < dstRow; r++) {
87
+ grid.setLine(dstCol, r, "│");
88
+ }
89
+ grid.set(dstCol - 1, dstRow, "►");
90
+ }
91
+ //# sourceMappingURL=edges.js.map
package/dist/form.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { RenderOptions } from "./types.js";
2
+ /**
3
+ * Render a Camunda Form JSON string as ASCII art.
4
+ *
5
+ * Each form component is drawn in document order as a text mock-up,
6
+ * showing labels, input placeholders, options, and buttons.
7
+ */
8
+ export declare function renderFormAscii(json: string, options?: RenderOptions): string;
9
+ //# sourceMappingURL=form.d.ts.map
package/dist/form.js ADDED
@@ -0,0 +1,209 @@
1
+ import { Form } from "@bpmnkit/core";
2
+ // ── Width ────────────────────────────────────────────────────────────────────
3
+ const WIDTH = 48; // inner content width (chars)
4
+ function hline(ch = "─") {
5
+ return ch.repeat(WIDTH);
6
+ }
7
+ function required(req) {
8
+ return req ? " *" : "";
9
+ }
10
+ // ── Component renderers ───────────────────────────────────────────────────────
11
+ function renderComponent(c, depth = 0) {
12
+ const indent = " ".repeat(depth);
13
+ const lines = [];
14
+ const k = c;
15
+ switch (k.type) {
16
+ case "text": {
17
+ // Render each line of the text content, prefixed with indent
18
+ for (const line of k.text.split("\n")) {
19
+ lines.push(indent + line);
20
+ }
21
+ break;
22
+ }
23
+ case "textfield":
24
+ case "textarea": {
25
+ const lbl = k.label + required(k.validate?.required);
26
+ const inputBox = k.type === "textarea"
27
+ ? "┌──────────────────────┐\n│ │\n│ │\n└──────────────────────┘"
28
+ : "[ ______________________ ]";
29
+ lines.push(indent + lbl);
30
+ for (const l of inputBox.split("\n"))
31
+ lines.push(indent + l);
32
+ break;
33
+ }
34
+ case "number": {
35
+ const lbl = k.label + required(k.validate?.required);
36
+ lines.push(indent + lbl);
37
+ lines.push(`${indent}[ 0 ▲▼ ]`);
38
+ break;
39
+ }
40
+ case "datetime": {
41
+ const lbl = k.dateLabel ?? k.timeLabel ?? "Date/Time";
42
+ lines.push(indent + lbl + required(k.validate?.required));
43
+ lines.push(`${indent}[ YYYY-MM-DD ] [ HH:MM ]`);
44
+ break;
45
+ }
46
+ case "select": {
47
+ const lbl = k.label + required(k.validate?.required);
48
+ lines.push(indent + lbl);
49
+ lines.push(`${indent}[ ▼ Select... ]`);
50
+ break;
51
+ }
52
+ case "taglist": {
53
+ const lbl = k.label + required(k.validate?.required);
54
+ lines.push(indent + lbl);
55
+ lines.push(`${indent}[ × Tag1 × Tag2 +Add ]`);
56
+ break;
57
+ }
58
+ case "radio": {
59
+ const lbl = k.label + required(k.validate?.required);
60
+ lines.push(indent + lbl);
61
+ if (k.values) {
62
+ for (const opt of k.values) {
63
+ lines.push(`${indent} ○ ${opt.label}`);
64
+ }
65
+ }
66
+ else {
67
+ lines.push(`${indent} ○ (dynamic options)`);
68
+ }
69
+ break;
70
+ }
71
+ case "checkbox": {
72
+ const req = k.validate?.required;
73
+ lines.push(`${indent}☐ ${k.label}${required(req)}`);
74
+ break;
75
+ }
76
+ case "checklist": {
77
+ const lbl = k.label + required(k.validate?.required);
78
+ lines.push(indent + lbl);
79
+ if (k.values) {
80
+ for (const opt of k.values) {
81
+ lines.push(`${indent} ☐ ${opt.label}`);
82
+ }
83
+ }
84
+ else {
85
+ lines.push(`${indent} ☐ (dynamic options)`);
86
+ }
87
+ break;
88
+ }
89
+ case "button": {
90
+ const label = k.label;
91
+ const inner = ` ${label} `;
92
+ lines.push(`${indent}[${inner}]`);
93
+ break;
94
+ }
95
+ case "separator": {
96
+ lines.push(indent + hline());
97
+ break;
98
+ }
99
+ case "spacer": {
100
+ lines.push("");
101
+ break;
102
+ }
103
+ case "group": {
104
+ const top = `┌─ ${k.label} ${"─".repeat(Math.max(0, WIDTH - k.label.length - 4))}┐`;
105
+ lines.push(indent + top);
106
+ for (const child of k.components) {
107
+ for (const l of renderComponent(child, depth + 1)) {
108
+ lines.push(l);
109
+ }
110
+ }
111
+ lines.push(`${indent}└${"─".repeat(WIDTH - 1)}┘`);
112
+ break;
113
+ }
114
+ case "dynamiclist": {
115
+ const label = k.label ?? "List";
116
+ const top = `┌─ ${label} ${"─".repeat(Math.max(0, WIDTH - label.length - 4))}┐`;
117
+ lines.push(indent + top);
118
+ for (const child of k.components) {
119
+ for (const l of renderComponent(child, depth + 1)) {
120
+ lines.push(l);
121
+ }
122
+ }
123
+ lines.push(`${indent}└${"─".repeat(WIDTH - 1)}┘`);
124
+ lines.push(`${indent} [+ Add item]`);
125
+ break;
126
+ }
127
+ case "table": {
128
+ const cols = k.columns ?? [];
129
+ if (cols.length === 0) {
130
+ lines.push(`${indent}[ Table: ${k.label ?? "(no label)"} ]`);
131
+ }
132
+ else {
133
+ const colW = Math.max(8, Math.floor((WIDTH - cols.length - 1) / cols.length));
134
+ const bar = cols.map(() => "─".repeat(colW));
135
+ lines.push(indent + (k.label ? k.label : "Table"));
136
+ lines.push(`${indent}┌${bar.join("┬")}┐`);
137
+ lines.push(`${indent}│${cols.map((col) => col.label.slice(0, colW).padEnd(colW)).join("│")}│`);
138
+ lines.push(`${indent}├${bar.join("┼")}┤`);
139
+ lines.push(`${indent}│${bar.map(() => " ".repeat(colW)).join("│")}│`);
140
+ lines.push(`${indent}└${bar.join("┴")}┘`);
141
+ }
142
+ break;
143
+ }
144
+ case "image": {
145
+ lines.push(`${indent}[ 🖼 ${k.alt ?? "Image"} ]`);
146
+ break;
147
+ }
148
+ case "iframe": {
149
+ lines.push(`${indent}[ iframe: ${k.url ?? "(url)"} ]`);
150
+ break;
151
+ }
152
+ case "html": {
153
+ lines.push(`${indent}[ HTML content ]`);
154
+ break;
155
+ }
156
+ case "expression": {
157
+ lines.push(`${indent}[ expr: ${k.expression ?? ""} ]`);
158
+ break;
159
+ }
160
+ case "filepicker": {
161
+ const lbl = k.label ?? "File";
162
+ lines.push(indent + lbl + required(undefined));
163
+ lines.push(`${indent}[ 📎 Choose file... ]`);
164
+ break;
165
+ }
166
+ case "documentPreview": {
167
+ lines.push(`${indent}[ 📄 ${k.label ?? "Document"} ]`);
168
+ break;
169
+ }
170
+ default: {
171
+ lines.push(`${indent}[ ${c.type} ]`);
172
+ break;
173
+ }
174
+ }
175
+ return lines;
176
+ }
177
+ // ── Public API ────────────────────────────────────────────────────────────────
178
+ /**
179
+ * Render a Camunda Form JSON string as ASCII art.
180
+ *
181
+ * Each form component is drawn in document order as a text mock-up,
182
+ * showing labels, input placeholders, options, and buttons.
183
+ */
184
+ export function renderFormAscii(json, options) {
185
+ const form = Form.parse(json);
186
+ const lines = [];
187
+ for (const comp of form.components) {
188
+ const compLines = renderComponent(comp);
189
+ lines.push(...compLines);
190
+ lines.push(""); // blank line between top-level components
191
+ }
192
+ // Remove trailing blank line
193
+ while (lines.length > 0 && lines[lines.length - 1] === "")
194
+ lines.pop();
195
+ const body = lines.join("\n");
196
+ const title = resolveTitle(options, form.id);
197
+ if (!title)
198
+ return body;
199
+ const line = "─".repeat(title.length);
200
+ return `${title}\n${line}\n\n${body}`;
201
+ }
202
+ function resolveTitle(options, formId) {
203
+ if (options?.title === false)
204
+ return undefined;
205
+ if (typeof options?.title === "string")
206
+ return options.title;
207
+ return formId ?? undefined;
208
+ }
209
+ //# sourceMappingURL=form.js.map
package/dist/grid.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Mutable 2-D character canvas that serialises to a multiline string.
3
+ *
4
+ * Coordinates are (col, row) = (x, y), zero-based from the top-left.
5
+ * Out-of-bounds writes are silently ignored.
6
+ */
7
+ export declare class AsciiGrid {
8
+ private readonly cells;
9
+ readonly cols: number;
10
+ readonly rows: number;
11
+ constructor(cols: number, rows: number);
12
+ /** Overwrite a single cell. Out-of-bounds writes are silently ignored. */
13
+ set(col: number, row: number, ch: string): void;
14
+ get(col: number, row: number): string;
15
+ /**
16
+ * Write text left-to-right starting at (col, row).
17
+ * Characters that fall outside the grid are ignored.
18
+ */
19
+ write(col: number, row: number, text: string): void;
20
+ /**
21
+ * Set a cell, combining with any existing box-drawing char at that position.
22
+ * Used for edge junctions (e.g. two edges crossing at the same column).
23
+ */
24
+ setLine(col: number, row: number, ch: string): void;
25
+ /** Serialise to a newline-separated string. Trailing spaces on each row are stripped. */
26
+ toString(): string;
27
+ }
28
+ /**
29
+ * Merge two box-drawing characters at a junction point by taking the union of
30
+ * their connection directions. Returns `next` when no merge applies.
31
+ */
32
+ export declare function mergeBoxChars(existing: string, next: string): string;
33
+ //# sourceMappingURL=grid.d.ts.map
package/dist/grid.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Mutable 2-D character canvas that serialises to a multiline string.
3
+ *
4
+ * Coordinates are (col, row) = (x, y), zero-based from the top-left.
5
+ * Out-of-bounds writes are silently ignored.
6
+ */
7
+ export class AsciiGrid {
8
+ cells;
9
+ cols;
10
+ rows;
11
+ constructor(cols, rows) {
12
+ this.cols = cols;
13
+ this.rows = rows;
14
+ this.cells = Array.from({ length: rows }, () => Array(cols).fill(" "));
15
+ }
16
+ /** Overwrite a single cell. Out-of-bounds writes are silently ignored. */
17
+ set(col, row, ch) {
18
+ if (col < 0 || col >= this.cols || row < 0 || row >= this.rows)
19
+ return;
20
+ const r = this.cells[row];
21
+ if (r !== undefined)
22
+ r[col] = ch[0] ?? " ";
23
+ }
24
+ get(col, row) {
25
+ if (col < 0 || col >= this.cols || row < 0 || row >= this.rows)
26
+ return " ";
27
+ return this.cells[row]?.[col] ?? " ";
28
+ }
29
+ /**
30
+ * Write text left-to-right starting at (col, row).
31
+ * Characters that fall outside the grid are ignored.
32
+ */
33
+ write(col, row, text) {
34
+ for (let i = 0; i < text.length; i++) {
35
+ this.set(col + i, row, text[i] ?? " ");
36
+ }
37
+ }
38
+ /**
39
+ * Set a cell, combining with any existing box-drawing char at that position.
40
+ * Used for edge junctions (e.g. two edges crossing at the same column).
41
+ */
42
+ setLine(col, row, ch) {
43
+ const existing = this.get(col, row);
44
+ this.set(col, row, mergeBoxChars(existing, ch));
45
+ }
46
+ /** Serialise to a newline-separated string. Trailing spaces on each row are stripped. */
47
+ toString() {
48
+ return this.cells.map((row) => row.join("").trimEnd()).join("\n");
49
+ }
50
+ }
51
+ // Directions each box-drawing char has connections in (L=left R=right U=up D=down)
52
+ const CHAR_DIRS = {
53
+ "─": "LR",
54
+ "│": "UD",
55
+ "┌": "RD",
56
+ "┐": "LD",
57
+ "└": "RU",
58
+ "┘": "LU",
59
+ "├": "RUD",
60
+ "┤": "LUD",
61
+ "┬": "LRD",
62
+ "┴": "LRU",
63
+ "┼": "LRUD",
64
+ };
65
+ // Reverse map: sorted direction string → box-drawing char
66
+ const DIRS_CHAR = {};
67
+ for (const [ch, dirs] of Object.entries(CHAR_DIRS)) {
68
+ DIRS_CHAR[[...dirs].sort().join("")] = ch;
69
+ }
70
+ /**
71
+ * Merge two box-drawing characters at a junction point by taking the union of
72
+ * their connection directions. Returns `next` when no merge applies.
73
+ */
74
+ export function mergeBoxChars(existing, next) {
75
+ if (existing === " " || existing === next)
76
+ return next;
77
+ // Arrows always win
78
+ if (next === "►" || next === "▼")
79
+ return next;
80
+ if (existing === "►" || existing === "▼")
81
+ return existing;
82
+ const eDirs = CHAR_DIRS[existing];
83
+ const nDirs = CHAR_DIRS[next];
84
+ if (!eDirs || !nDirs)
85
+ return next;
86
+ const union = [...new Set([...eDirs, ...nDirs])].sort().join("");
87
+ return DIRS_CHAR[union] ?? next;
88
+ }
89
+ //# sourceMappingURL=grid.js.map
@@ -0,0 +1,5 @@
1
+ export { renderBpmnAscii } from "./render.js";
2
+ export { renderDmnAscii } from "./dmn.js";
3
+ export { renderFormAscii } from "./form.js";
4
+ export type { RenderOptions } from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { renderBpmnAscii } from "./render.js";
2
+ export { renderDmnAscii } from "./dmn.js";
3
+ export { renderFormAscii } from "./form.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ import type { RenderOptions } from "./types.js";
2
+ /**
3
+ * Render a BPMN XML string as a Unicode box-drawing ASCII diagram.
4
+ *
5
+ * Uses the same Sugiyama layout engine as the canvas renderer to position
6
+ * elements, then maps each element to a fixed-size ASCII box and routes
7
+ * sequence flows as orthogonal lines.
8
+ */
9
+ export declare function renderBpmnAscii(xml: string, options?: RenderOptions): string;
10
+ //# sourceMappingURL=render.d.ts.map
package/dist/render.js ADDED
@@ -0,0 +1,62 @@
1
+ import { Bpmn, layoutFlowNodes } from "@bpmnkit/core";
2
+ import { drawEdge } from "./edges.js";
3
+ import { AsciiGrid } from "./grid.js";
4
+ import { CELL_H, CELL_W, drawElement, entryCol, exitCol, midRow } from "./shapes.js";
5
+ /**
6
+ * Render a BPMN XML string as a Unicode box-drawing ASCII diagram.
7
+ *
8
+ * Uses the same Sugiyama layout engine as the canvas renderer to position
9
+ * elements, then maps each element to a fixed-size ASCII box and routes
10
+ * sequence flows as orthogonal lines.
11
+ */
12
+ export function renderBpmnAscii(xml, options) {
13
+ const defs = Bpmn.parse(xml);
14
+ const process = defs.processes[0];
15
+ if (!process)
16
+ return "(empty)";
17
+ // Use layoutFlowNodes directly to skip the overlap assertion — the ASCII renderer
18
+ // uses layer/position indices (not pixel coords) so pixel-level overlaps are harmless.
19
+ const layout = layoutFlowNodes(process.flowElements, process.sequenceFlows);
20
+ const { nodes, edges } = layout;
21
+ if (nodes.length === 0)
22
+ return "(empty)";
23
+ // Compute grid dimensions from the layout's layer/position extents
24
+ const maxLayer = Math.max(...nodes.map((n) => n.layer));
25
+ const maxPos = Math.max(...nodes.map((n) => n.position));
26
+ // +2 padding on each axis, +1 because layers/positions are 0-indexed
27
+ const gridCols = (maxLayer + 1) * CELL_W + 4;
28
+ const gridRows = (maxPos + 1) * CELL_H + 4;
29
+ const grid = new AsciiGrid(gridCols, gridRows);
30
+ // Build an id → node map for O(1) edge-endpoint look-up
31
+ const nodeById = new Map();
32
+ for (const node of nodes)
33
+ nodeById.set(node.id, node);
34
+ // Draw edges first so that shapes render on top of any edge overlap
35
+ for (const edge of edges) {
36
+ const src = nodeById.get(edge.sourceRef);
37
+ const dst = nodeById.get(edge.targetRef);
38
+ if (!src || !dst)
39
+ continue;
40
+ drawEdge(grid, exitCol(src.type, src.layer), midRow(src.position), entryCol(dst.type, dst.layer), midRow(dst.position));
41
+ }
42
+ // Draw element boxes on top
43
+ for (const node of nodes) {
44
+ drawElement(grid, node.type, node.layer, node.position, node.label);
45
+ }
46
+ const diagram = grid.toString();
47
+ // Optional title header
48
+ const title = resolveTitle(options, process.name);
49
+ if (!title)
50
+ return diagram;
51
+ const line = "─".repeat(title.length);
52
+ return `${title}\n${line}\n\n${diagram}`;
53
+ }
54
+ /** Pick the title string to show above the diagram (or undefined to suppress). */
55
+ function resolveTitle(options, processName) {
56
+ if (options?.title === false)
57
+ return undefined;
58
+ if (typeof options?.title === "string")
59
+ return options.title;
60
+ return processName ?? undefined;
61
+ }
62
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,37 @@
1
+ import type { AsciiGrid } from "./grid.js";
2
+ /** Total character width of a task box (including the two │ borders). */
3
+ export declare const TASK_W = 24;
4
+ /** Total character width of an event compact box (including borders). */
5
+ export declare const ELEM_W = 11;
6
+ /**
7
+ * Total character width of a gateway box (including the two border chars).
8
+ * Narrower than event boxes; corners use / and \ to suggest a rotated square.
9
+ *
10
+ * /───────\
11
+ * │ × Lbl │
12
+ * \───────/
13
+ */
14
+ export declare const GATEWAY_W = 9;
15
+ /** Character width of one logical grid cell (one Sugiyama layer). */
16
+ export declare const CELL_W = 28;
17
+ /** Character height of one logical grid cell (one Sugiyama position). */
18
+ export declare const CELL_H = 8;
19
+ /** Top-left column of an element within its cell. */
20
+ export declare function elemCol(type: string, layer: number): number;
21
+ /** Top-left row of a task or event element within its cell (3 rows tall). */
22
+ export declare function elemRow(position: number): number;
23
+ /**
24
+ * Column of the right exit connection point (first column AFTER the element's
25
+ * right border — where an outgoing edge begins).
26
+ */
27
+ export declare function exitCol(type: string, layer: number): number;
28
+ /**
29
+ * Column of the left entry connection point (the column OF the element's left
30
+ * border — where an incoming edge arrow lands just before it).
31
+ */
32
+ export declare function entryCol(type: string, layer: number): number;
33
+ /** Row of the horizontal mid-point (used as the connection row for edges). */
34
+ export declare function midRow(position: number): number;
35
+ /** Draw any BPMN element onto the grid at its logical (layer, position). */
36
+ export declare function drawElement(grid: AsciiGrid, type: string, layer: number, position: number, label: string | undefined): void;
37
+ //# sourceMappingURL=shapes.d.ts.map
package/dist/shapes.js ADDED
@@ -0,0 +1,222 @@
1
+ import { truncate } from "./util.js";
2
+ // ── Layout constants ────────────────────────────────────────────────────────
3
+ /** Total character width of a task box (including the two │ borders). */
4
+ export const TASK_W = 24;
5
+ /** Total character width of an event compact box (including borders). */
6
+ export const ELEM_W = 11;
7
+ /**
8
+ * Total character width of a gateway box (including the two border chars).
9
+ * Narrower than event boxes; corners use / and \ to suggest a rotated square.
10
+ *
11
+ * /───────\
12
+ * │ × Lbl │
13
+ * \───────/
14
+ */
15
+ export const GATEWAY_W = 9;
16
+ /** Character width of one logical grid cell (one Sugiyama layer). */
17
+ export const CELL_W = 28;
18
+ /** Character height of one logical grid cell (one Sugiyama position). */
19
+ export const CELL_H = 8;
20
+ // Derived inner widths (space available for content inside the borders)
21
+ const TASK_INNER = TASK_W - 2; // 22
22
+ const ELEM_INNER = ELEM_W - 2; // 9
23
+ // ── Element classification ──────────────────────────────────────────────────
24
+ function isTaskLike(type) {
25
+ return (type === "task" ||
26
+ type === "serviceTask" ||
27
+ type === "userTask" ||
28
+ type === "scriptTask" ||
29
+ type === "sendTask" ||
30
+ type === "receiveTask" ||
31
+ type === "businessRuleTask" ||
32
+ type === "manualTask" ||
33
+ type === "callActivity" ||
34
+ type === "subProcess" ||
35
+ type === "adHocSubProcess" ||
36
+ type === "eventSubProcess" ||
37
+ type === "transaction");
38
+ }
39
+ function isGateway(type) {
40
+ return (type === "exclusiveGateway" ||
41
+ type === "parallelGateway" ||
42
+ type === "inclusiveGateway" ||
43
+ type === "eventBasedGateway" ||
44
+ type === "complexGateway");
45
+ }
46
+ // ── Label / marker helpers ──────────────────────────────────────────────────
47
+ /** Short type tag prepended to task content, e.g. "[svc] ". */
48
+ function taskTag(type) {
49
+ switch (type) {
50
+ case "serviceTask":
51
+ return "[svc] ";
52
+ case "userTask":
53
+ return "[usr] ";
54
+ case "scriptTask":
55
+ return "[scr] ";
56
+ case "sendTask":
57
+ return "[snd] ";
58
+ case "receiveTask":
59
+ return "[rcv] ";
60
+ case "businessRuleTask":
61
+ return "[dmn] ";
62
+ case "manualTask":
63
+ return "[man] ";
64
+ case "callActivity":
65
+ return "[cal] ";
66
+ case "subProcess":
67
+ case "adHocSubProcess":
68
+ case "eventSubProcess":
69
+ return "[sub] ";
70
+ case "transaction":
71
+ return "[txn] ";
72
+ default:
73
+ return "";
74
+ }
75
+ }
76
+ /** Single Unicode marker shown inside an event or gateway shape. */
77
+ function elementMarker(type) {
78
+ switch (type) {
79
+ case "startEvent":
80
+ return "○";
81
+ case "endEvent":
82
+ return "●";
83
+ case "intermediateCatchEvent":
84
+ return "◎";
85
+ case "intermediateThrowEvent":
86
+ return "◉";
87
+ case "boundaryEvent":
88
+ return "◈";
89
+ case "exclusiveGateway":
90
+ return "×";
91
+ case "parallelGateway":
92
+ return "+";
93
+ case "inclusiveGateway":
94
+ return "◇";
95
+ case "eventBasedGateway":
96
+ return "?";
97
+ case "complexGateway":
98
+ return "✱";
99
+ default:
100
+ return "·";
101
+ }
102
+ }
103
+ // ── Position helpers ────────────────────────────────────────────────────────
104
+ /** Width of the element drawn for this type. */
105
+ function elemW(type) {
106
+ if (isTaskLike(type))
107
+ return TASK_W;
108
+ if (isGateway(type))
109
+ return GATEWAY_W;
110
+ return ELEM_W;
111
+ }
112
+ /** Top-left column of an element within its cell. */
113
+ export function elemCol(type, layer) {
114
+ return layer * CELL_W + Math.floor((CELL_W - elemW(type)) / 2);
115
+ }
116
+ /** Top-left row of a task or event element within its cell (3 rows tall). */
117
+ export function elemRow(position) {
118
+ // Centre the 3-row element vertically within the cell
119
+ return position * CELL_H + Math.floor((CELL_H - 3) / 2);
120
+ }
121
+ /**
122
+ * Column of the right exit connection point (first column AFTER the element's
123
+ * right border — where an outgoing edge begins).
124
+ */
125
+ export function exitCol(type, layer) {
126
+ return elemCol(type, layer) + elemW(type);
127
+ }
128
+ /**
129
+ * Column of the left entry connection point (the column OF the element's left
130
+ * border — where an incoming edge arrow lands just before it).
131
+ */
132
+ export function entryCol(type, layer) {
133
+ return elemCol(type, layer);
134
+ }
135
+ /** Row of the horizontal mid-point (used as the connection row for edges). */
136
+ export function midRow(position) {
137
+ return elemRow(position) + 1; // middle of the 3-row element
138
+ }
139
+ // ── Drawing ─────────────────────────────────────────────────────────────────
140
+ /** Draw any BPMN element onto the grid at its logical (layer, position). */
141
+ export function drawElement(grid, type, layer, position, label) {
142
+ const col = elemCol(type, layer);
143
+ const name = label ?? "";
144
+ if (isTaskLike(type)) {
145
+ drawTaskBox(grid, col, elemRow(position), name, type);
146
+ }
147
+ else if (isGateway(type)) {
148
+ drawGatewayBox(grid, col, elemRow(position), name, elementMarker(type));
149
+ }
150
+ else {
151
+ drawCompactBox(grid, col, elemRow(position), name, elementMarker(type));
152
+ }
153
+ }
154
+ /**
155
+ * Rectangular task box — 3 rows tall, TASK_W wide.
156
+ *
157
+ * ┌──────────────────────┐
158
+ * │ [tag] Label… │
159
+ * └──────────────────────┘
160
+ */
161
+ function drawTaskBox(grid, col, row, label, type) {
162
+ const inner = TASK_INNER;
163
+ // Top border
164
+ grid.set(col, row, "┌");
165
+ grid.write(col + 1, row, "─".repeat(inner));
166
+ grid.set(col + inner + 1, row, "┐");
167
+ // Middle row
168
+ grid.set(col, row + 1, "│");
169
+ const tag = taskTag(type);
170
+ const content = truncate(tag + label, inner - 1); // -1 for leading space
171
+ grid.write(col + 1, row + 1, ` ${content}`);
172
+ grid.set(col + inner + 1, row + 1, "│");
173
+ // Bottom border
174
+ grid.set(col, row + 2, "└");
175
+ grid.write(col + 1, row + 2, "─".repeat(inner));
176
+ grid.set(col + inner + 1, row + 2, "┘");
177
+ }
178
+ /**
179
+ * Compact rounded box for events — 3 rows tall, ELEM_W wide.
180
+ *
181
+ * ╭─────────╮
182
+ * │ ○ Label │
183
+ * ╰─────────╯
184
+ */
185
+ function drawCompactBox(grid, col, row, label, marker) {
186
+ const inner = ELEM_INNER;
187
+ // Top border (rounded)
188
+ grid.set(col, row, "╭");
189
+ grid.write(col + 1, row, "─".repeat(inner));
190
+ grid.set(col + inner + 1, row, "╮");
191
+ // Middle row: marker + label
192
+ grid.set(col, row + 1, "│");
193
+ const content = truncate(`${marker} ${label}`, inner - 1); // -1 for leading space
194
+ grid.write(col + 1, row + 1, ` ${content}`);
195
+ grid.set(col + inner + 1, row + 1, "│");
196
+ // Bottom border (rounded)
197
+ grid.set(col, row + 2, "╰");
198
+ grid.write(col + 1, row + 2, "─".repeat(inner));
199
+ grid.set(col + inner + 1, row + 2, "╯");
200
+ }
201
+ /**
202
+ * Gateway box — 3 rows tall, GATEWAY_W wide. Diagonal / \ corners suggest a
203
+ * rotated square. Marker + label shown inside.
204
+ *
205
+ * /─────────\
206
+ * │ × Label │
207
+ * \─────────/
208
+ */
209
+ function drawGatewayBox(grid, col, row, label, marker) {
210
+ const inner = GATEWAY_W - 2;
211
+ grid.set(col, row, "/");
212
+ grid.write(col + 1, row, "─".repeat(inner));
213
+ grid.set(col + inner + 1, row, "\\");
214
+ grid.set(col, row + 1, "│");
215
+ const content = truncate(`${marker} ${label}`, inner - 1); // -1 for leading space
216
+ grid.write(col + 1, row + 1, ` ${content}`);
217
+ grid.set(col + inner + 1, row + 1, "│");
218
+ grid.set(col, row + 2, "\\");
219
+ grid.write(col + 1, row + 2, "─".repeat(inner));
220
+ grid.set(col + inner + 1, row + 2, "/");
221
+ }
222
+ //# sourceMappingURL=shapes.js.map
@@ -0,0 +1,10 @@
1
+ /** Options for renderBpmnAscii(). */
2
+ export interface RenderOptions {
3
+ /**
4
+ * Process name shown as a header above the diagram.
5
+ * Pass `false` to suppress the header entirely.
6
+ * Defaults to the process name from the BPMN XML.
7
+ */
8
+ title?: string | false;
9
+ }
10
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/dist/util.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Truncate `text` to at most `maxLen` characters, appending "…" when shortened. */
2
+ export declare function truncate(text: string, maxLen: number): string;
3
+ /** Pad `text` with trailing spaces so the result is exactly `width` characters wide. */
4
+ export declare function padEnd(text: string, width: number): string;
5
+ //# sourceMappingURL=util.d.ts.map
package/dist/util.js ADDED
@@ -0,0 +1,17 @@
1
+ /** Truncate `text` to at most `maxLen` characters, appending "…" when shortened. */
2
+ export function truncate(text, maxLen) {
3
+ if (maxLen <= 0)
4
+ return "";
5
+ if (text.length <= maxLen)
6
+ return text;
7
+ if (maxLen === 1)
8
+ return "…";
9
+ return `${text.slice(0, maxLen - 1)}…`;
10
+ }
11
+ /** Pad `text` with trailing spaces so the result is exactly `width` characters wide. */
12
+ export function padEnd(text, width) {
13
+ if (text.length >= width)
14
+ return text.slice(0, width);
15
+ return text + " ".repeat(width - text.length);
16
+ }
17
+ //# sourceMappingURL=util.js.map
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@bpmnkit/ascii",
3
+ "version": "0.0.8",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": ["dist/**/*.js", "dist/**/*.d.ts"],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "typecheck": "tsc --noEmit",
17
+ "check": "biome check .",
18
+ "test": "vitest run"
19
+ },
20
+ "dependencies": {
21
+ "@bpmnkit/core": "workspace:*"
22
+ },
23
+ "description": "Render BPMN diagrams as Unicode box-drawing ASCII art — perfect for terminals and docs",
24
+ "keywords": ["bpmn", "ascii", "terminal", "diagram", "typescript"],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/bpmnkit/monorepo"
29
+ }
30
+ }