@f5xc-salesdemos/pi-utils 19.41.1 → 19.42.1
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/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/mermaid-color.ts +231 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/pi-utils",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.42.1",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "^1.3",
|
|
42
|
-
"@f5xc-salesdemos/pi-natives": "19.
|
|
42
|
+
"@f5xc-salesdemos/pi-natives": "19.42.1"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"bun": ">=1.3.7"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid color helpers — terminal color-mode selection, diagram-type detection,
|
|
3
|
+
* and a best-effort per-node tint pass for ASCII/Unicode mermaid output.
|
|
4
|
+
*
|
|
5
|
+
* `beautiful-mermaid` colorizes strictly by global character role (text/border/
|
|
6
|
+
* line/arrow/…), so per-node hues are not reachable through its API. `tintMermaidNodes`
|
|
7
|
+
* adds them as a post-pass: it detects node-box rectangles in the rendered output and
|
|
8
|
+
* overrides the foreground color of each box's border + label cells with a cycled accent.
|
|
9
|
+
* It is intentionally fail-safe — any ambiguity returns the input unchanged so the
|
|
10
|
+
* library's per-role coloring still shows through.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Color mode subset we drive `beautiful-mermaid` with. */
|
|
14
|
+
export type MermaidColorMode = "none" | "ansi256" | "truecolor";
|
|
15
|
+
|
|
16
|
+
export interface ColorModeInput {
|
|
17
|
+
/** NO_COLOR set, output piped, or color explicitly disabled. */
|
|
18
|
+
noColor: boolean;
|
|
19
|
+
/** Terminal advertises 24-bit color. */
|
|
20
|
+
trueColor: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Choose the richest color mode the environment robustly supports. */
|
|
24
|
+
export function pickColorMode({ noColor, trueColor }: ColorModeInput): MermaidColorMode {
|
|
25
|
+
if (noColor) return "none";
|
|
26
|
+
return trueColor ? "truecolor" : "ansi256";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Complexity ceiling for mermaid sources. beautiful-mermaid's ASCII edge router
|
|
31
|
+
* (A* pathfinder) can allocate unbounded memory and hang for tens of seconds —
|
|
32
|
+
* then throw `RangeError: Out of memory` — on large/dense graphs. Callers should
|
|
33
|
+
* reject sources past this limit BEFORE rendering rather than attempt the layout.
|
|
34
|
+
*/
|
|
35
|
+
export const MERMAID_MAX_CHARS = 20000;
|
|
36
|
+
export const MERMAID_MAX_LINES = 400;
|
|
37
|
+
|
|
38
|
+
/** True when a mermaid source is too large to render safely (see limits above). */
|
|
39
|
+
export function mermaidSourceExceedsLimit(source: string): boolean {
|
|
40
|
+
if (source.length > MERMAID_MAX_CHARS) return true;
|
|
41
|
+
let lines = 1;
|
|
42
|
+
for (let i = 0; i < source.length; i++) if (source.charCodeAt(i) === 10 && ++lines > MERMAID_MAX_LINES) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type MermaidDiagramType = "flowchart" | "sequence" | "class" | "er" | "state" | "xychart" | "unknown";
|
|
47
|
+
|
|
48
|
+
/** Detect the diagram type from the first meaningful header line of mermaid source. */
|
|
49
|
+
export function detectDiagramType(source: string): MermaidDiagramType {
|
|
50
|
+
for (const raw of source.split("\n")) {
|
|
51
|
+
const line = raw.trim();
|
|
52
|
+
// Skip blanks, YAML frontmatter fences, %%{init}%% directives, and %% comments.
|
|
53
|
+
if (line === "" || line === "---" || line.startsWith("%%")) continue;
|
|
54
|
+
// Skip frontmatter key/value lines (anything before we hit a known header).
|
|
55
|
+
if (/^(graph|flowchart)\b/.test(line)) return "flowchart";
|
|
56
|
+
if (/^sequenceDiagram\b/.test(line)) return "sequence";
|
|
57
|
+
if (/^classDiagram(-v2)?\b/.test(line)) return "class";
|
|
58
|
+
if (/^erDiagram\b/.test(line)) return "er";
|
|
59
|
+
if (/^stateDiagram(-v2)?\b/.test(line)) return "state";
|
|
60
|
+
if (/^xychart(-beta)?\b/.test(line)) return "xychart";
|
|
61
|
+
// A header-shaped token that isn't recognized → give up (avoid matching frontmatter values).
|
|
62
|
+
if (/^[A-Za-z]/.test(line) && !line.includes(":")) return "unknown";
|
|
63
|
+
}
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── per-node tint pass ──────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
interface Cell {
|
|
70
|
+
char: string;
|
|
71
|
+
/** Active foreground SGR sequence ("" = default). */
|
|
72
|
+
fg: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Rect {
|
|
76
|
+
top: number;
|
|
77
|
+
left: number;
|
|
78
|
+
bottom: number;
|
|
79
|
+
right: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const TL = new Set(["┌", "╭", "+"]);
|
|
83
|
+
const isH = (ch: string): boolean => ch === "─" || ch === "-";
|
|
84
|
+
const isV = (ch: string): boolean => ch === "│" || ch === "|";
|
|
85
|
+
// Borders may carry an edge-attachment junction (a tee/cross) where an edge meets
|
|
86
|
+
// the box; accept those so attached nodes are still detected and tinted.
|
|
87
|
+
const isHBorder = (ch: string): boolean => isH(ch) || ch === "┬" || ch === "┴" || ch === "┼";
|
|
88
|
+
const isVBorder = (ch: string): boolean => isV(ch) || ch === "├" || ch === "┤" || ch === "┼";
|
|
89
|
+
|
|
90
|
+
/** Parse a colored line into per-visible-cell {char, fg}, tracking the active fg color. */
|
|
91
|
+
function parseCells(line: string): Cell[] {
|
|
92
|
+
const cells: Cell[] = [];
|
|
93
|
+
let fg = "";
|
|
94
|
+
const re = /\x1b\[[0-9;]*m/g;
|
|
95
|
+
let last = 0;
|
|
96
|
+
for (let m = re.exec(line); m !== null; m = re.exec(line)) {
|
|
97
|
+
for (const ch of line.slice(last, m.index)) cells.push({ char: ch, fg });
|
|
98
|
+
fg = nextFg(fg, m[0]);
|
|
99
|
+
last = re.lastIndex;
|
|
100
|
+
}
|
|
101
|
+
for (const ch of line.slice(last)) cells.push({ char: ch, fg });
|
|
102
|
+
return cells;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Compute the new active fg from an SGR sequence (only fg-affecting codes matter). */
|
|
106
|
+
function nextFg(prev: string, seq: string): string {
|
|
107
|
+
const params = seq.slice(2, -1); // strip ESC[ and trailing m
|
|
108
|
+
if (params === "" || params === "0") return "";
|
|
109
|
+
if (params.startsWith("38;2;") || params.startsWith("38;5;")) return seq;
|
|
110
|
+
if (params === "39") return "";
|
|
111
|
+
if (/^(3[0-7]|9[0-7])$/.test(params)) return seq;
|
|
112
|
+
return prev; // bold/underline/bg/etc. — leave fg unchanged
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Locate a single rectangle whose top-left corner is at (r,c), or null. */
|
|
116
|
+
function readRect(grid: string[][], r: number, c: number): Rect | null {
|
|
117
|
+
const tl = grid[r]?.[c];
|
|
118
|
+
if (tl === undefined || !TL.has(tl)) return null;
|
|
119
|
+
const ascii = tl === "+";
|
|
120
|
+
const trCh = ascii ? "+" : tl === "╭" ? "╮" : "┐";
|
|
121
|
+
const blCh = ascii ? "+" : tl === "╭" ? "╰" : "└";
|
|
122
|
+
const brCh = ascii ? "+" : tl === "╭" ? "╯" : "┘";
|
|
123
|
+
|
|
124
|
+
const row = grid[r]!;
|
|
125
|
+
let right = -1;
|
|
126
|
+
for (let x = c + 1; x < row.length; x++) {
|
|
127
|
+
const ch = row[x]!;
|
|
128
|
+
if (ch === trCh) {
|
|
129
|
+
right = x;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (!isHBorder(ch)) break;
|
|
133
|
+
}
|
|
134
|
+
if (right < c + 2) return null; // need interior width
|
|
135
|
+
|
|
136
|
+
let bottom = -1;
|
|
137
|
+
for (let y = r + 1; y < grid.length; y++) {
|
|
138
|
+
const ch = grid[y]?.[c];
|
|
139
|
+
if (ch === undefined) break;
|
|
140
|
+
if (ch === blCh) {
|
|
141
|
+
bottom = y;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (!isVBorder(ch)) break;
|
|
145
|
+
}
|
|
146
|
+
if (bottom < r + 2) return null; // need interior height
|
|
147
|
+
|
|
148
|
+
if (grid[bottom]?.[right] !== brCh) return null;
|
|
149
|
+
// Verify bottom edge and right edge (junctions where edges attach are allowed).
|
|
150
|
+
for (let x = c + 1; x < right; x++) if (!isHBorder(grid[bottom]?.[x] ?? "")) return null;
|
|
151
|
+
for (let y = r + 1; y < bottom; y++) if (!isVBorder(grid[y]?.[right] ?? "")) return null;
|
|
152
|
+
|
|
153
|
+
return { top: r, left: c, bottom, right };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function findRects(grid: string[][]): Rect[] {
|
|
157
|
+
const rects: Rect[] = [];
|
|
158
|
+
for (let r = 0; r < grid.length; r++) {
|
|
159
|
+
const row = grid[r]!;
|
|
160
|
+
for (let c = 0; c < row.length; c++) {
|
|
161
|
+
if (TL.has(row[c]!)) {
|
|
162
|
+
const rect = readRect(grid, r, c);
|
|
163
|
+
if (rect) rects.push(rect);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return rects;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function contains(a: Rect, b: Rect): boolean {
|
|
171
|
+
if (a === b) return false;
|
|
172
|
+
return a.top <= b.top && a.left <= b.left && a.bottom >= b.bottom && a.right >= b.right;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Tint each node box a cycled accent color. `coloredLines` is the role-colored
|
|
177
|
+
* mermaid render; `accentsAnsi` is a list of fg SGR escapes to cycle across nodes.
|
|
178
|
+
* Returns the input unchanged if no boxes are found or no accents are given.
|
|
179
|
+
*/
|
|
180
|
+
export function tintMermaidNodes(coloredLines: string[], accentsAnsi: string[]): string[] {
|
|
181
|
+
if (accentsAnsi.length === 0) return coloredLines;
|
|
182
|
+
try {
|
|
183
|
+
const rows = coloredLines.map(parseCells);
|
|
184
|
+
const grid = rows.map(cells => cells.map(cell => cell.char));
|
|
185
|
+
const rects = findRects(grid);
|
|
186
|
+
if (rects.length === 0) return coloredLines;
|
|
187
|
+
|
|
188
|
+
// Nodes are the innermost rectangles (a subgraph frame contains others → skip it).
|
|
189
|
+
const nodes = rects
|
|
190
|
+
.filter(rect => !rects.some(other => contains(rect, other)))
|
|
191
|
+
.sort((p, q) => p.top - q.top || p.left - q.left);
|
|
192
|
+
if (nodes.length === 0) return coloredLines;
|
|
193
|
+
|
|
194
|
+
// overrides: "r,c" → accent fg sequence.
|
|
195
|
+
const overrides = new Map<string, string>();
|
|
196
|
+
nodes.forEach((rect, i) => {
|
|
197
|
+
const accent = accentsAnsi[i % accentsAnsi.length]!;
|
|
198
|
+
for (let x = rect.left; x <= rect.right; x++) {
|
|
199
|
+
overrides.set(`${rect.top},${x}`, accent);
|
|
200
|
+
overrides.set(`${rect.bottom},${x}`, accent);
|
|
201
|
+
}
|
|
202
|
+
for (let y = rect.top; y <= rect.bottom; y++) {
|
|
203
|
+
overrides.set(`${y},${rect.left}`, accent);
|
|
204
|
+
overrides.set(`${y},${rect.right}`, accent);
|
|
205
|
+
for (let x = rect.left + 1; x < rect.right; x++) {
|
|
206
|
+
if ((grid[y]?.[x] ?? " ") !== " ") overrides.set(`${y},${x}`, accent);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return rows.map((cells, r) => {
|
|
212
|
+
const hasOverride = cells.some((_, c) => overrides.has(`${r},${c}`));
|
|
213
|
+
if (!hasOverride) return coloredLines[r]!;
|
|
214
|
+
let out = "";
|
|
215
|
+
let lastFg = "";
|
|
216
|
+
for (let c = 0; c < cells.length; c++) {
|
|
217
|
+
const cell = cells[c]!;
|
|
218
|
+
const fg = overrides.get(`${r},${c}`) ?? cell.fg;
|
|
219
|
+
if (fg !== lastFg) {
|
|
220
|
+
out += fg === "" ? "\x1b[39m" : fg;
|
|
221
|
+
lastFg = fg;
|
|
222
|
+
}
|
|
223
|
+
out += cell.char;
|
|
224
|
+
}
|
|
225
|
+
if (lastFg !== "") out += "\x1b[0m";
|
|
226
|
+
return out;
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
return coloredLines;
|
|
230
|
+
}
|
|
231
|
+
}
|