@herbertgao/pi-extensions 2026.9.7 → 2026.9.9

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.
Files changed (158) hide show
  1. package/README.md +8 -4
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  7. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  8. package/node_modules/@narumitw/pi-btw/README.md +6 -1
  9. package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  11. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  12. package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
  13. package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
  14. package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
  15. package/node_modules/grok-mermaid/CHANGELOG.md +46 -0
  16. package/node_modules/grok-mermaid/LICENSE +205 -0
  17. package/node_modules/grok-mermaid/README.md +191 -0
  18. package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
  19. package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
  20. package/node_modules/grok-mermaid/dist/ansi.js +23 -0
  21. package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
  22. package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
  23. package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
  24. package/node_modules/grok-mermaid/dist/canvas.js +366 -0
  25. package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
  26. package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
  27. package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
  28. package/node_modules/grok-mermaid/dist/graph.js +91 -0
  29. package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
  30. package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
  31. package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
  32. package/node_modules/grok-mermaid/dist/index.js +100 -0
  33. package/node_modules/grok-mermaid/dist/index.js.map +1 -0
  34. package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
  35. package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
  36. package/node_modules/grok-mermaid/dist/labels.js +324 -0
  37. package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
  38. package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
  39. package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
  40. package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
  41. package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
  42. package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
  43. package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
  44. package/node_modules/grok-mermaid/dist/layout.js +881 -0
  45. package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
  46. package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
  47. package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
  48. package/node_modules/grok-mermaid/dist/parse.js +1151 -0
  49. package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
  50. package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
  51. package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
  52. package/node_modules/grok-mermaid/dist/source-box.js +78 -0
  53. package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
  54. package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
  55. package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
  56. package/node_modules/grok-mermaid/dist/types.js +1 -0
  57. package/node_modules/grok-mermaid/dist/types.js.map +1 -0
  58. package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
  59. package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
  60. package/node_modules/grok-mermaid/dist/width-data.js +994 -0
  61. package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
  62. package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
  63. package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
  64. package/node_modules/grok-mermaid/dist/width.js +76 -0
  65. package/node_modules/grok-mermaid/dist/width.js.map +1 -0
  66. package/node_modules/grok-mermaid/package.json +49 -0
  67. package/node_modules/grok-mermaid/src/ansi.ts +34 -0
  68. package/node_modules/grok-mermaid/src/canvas.ts +373 -0
  69. package/node_modules/grok-mermaid/src/graph.ts +142 -0
  70. package/node_modules/grok-mermaid/src/index.ts +104 -0
  71. package/node_modules/grok-mermaid/src/labels.ts +326 -0
  72. package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
  73. package/node_modules/grok-mermaid/src/layout.ts +1015 -0
  74. package/node_modules/grok-mermaid/src/parse.ts +1189 -0
  75. package/node_modules/grok-mermaid/src/source-box.ts +89 -0
  76. package/node_modules/grok-mermaid/src/types.ts +43 -0
  77. package/node_modules/grok-mermaid/src/width-data.ts +993 -0
  78. package/node_modules/grok-mermaid/src/width.ts +74 -0
  79. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
  80. package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
  81. package/node_modules/pi-jev-auto-mode/README.md +285 -0
  82. package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
  83. package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
  84. package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
  85. package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
  86. package/node_modules/pi-jev-auto-mode/index.ts +1 -0
  87. package/node_modules/pi-jev-auto-mode/package.json +72 -0
  88. package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
  89. package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
  90. package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
  91. package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
  92. package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
  93. package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
  94. package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
  95. package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
  96. package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
  97. package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
  98. package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
  99. package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
  100. package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
  101. package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
  102. package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
  103. package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
  104. package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
  105. package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
  106. package/node_modules/pi-lens/CHANGELOG.md +45 -0
  107. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
  108. package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
  109. package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
  110. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
  111. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
  112. package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
  113. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
  114. package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
  115. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
  116. package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
  117. package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
  118. package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
  119. package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
  120. package/node_modules/pi-lens/dist/index.js +729 -344
  121. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
  122. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
  123. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
  124. package/node_modules/pi-lens/package.json +1 -1
  125. package/node_modules/pi-typesafe/LICENSE +21 -0
  126. package/node_modules/pi-typesafe/README.md +153 -0
  127. package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
  128. package/node_modules/pi-typesafe/dist/ask.js +25 -0
  129. package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
  130. package/node_modules/pi-typesafe/dist/auth.js +114 -0
  131. package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
  132. package/node_modules/pi-typesafe/dist/batch.js +116 -0
  133. package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
  134. package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
  135. package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
  136. package/node_modules/pi-typesafe/dist/client.js +188 -0
  137. package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
  138. package/node_modules/pi-typesafe/dist/credentials.js +112 -0
  139. package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
  140. package/node_modules/pi-typesafe/dist/errors.js +31 -0
  141. package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
  142. package/node_modules/pi-typesafe/dist/extension.js +222 -0
  143. package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
  144. package/node_modules/pi-typesafe/dist/index.js +9 -0
  145. package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
  146. package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
  147. package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
  148. package/node_modules/pi-typesafe/dist/login.js +38 -0
  149. package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
  150. package/node_modules/pi-typesafe/dist/schema.js +152 -0
  151. package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
  152. package/node_modules/pi-typesafe/dist/ui.js +3 -0
  153. package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
  154. package/node_modules/pi-typesafe/dist/usage.js +182 -0
  155. package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
  156. package/node_modules/pi-typesafe/extensions/index.js +2 -0
  157. package/node_modules/pi-typesafe/package.json +89 -0
  158. package/package.json +16 -8
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The shared diagram model. Flowchart, state, class and ER sources all parse
3
+ * into a `Graph`; only sequence diagrams have their own model.
4
+ */
5
+
6
+ import { asciiUpper } from './labels.ts'
7
+
8
+ /** Caps that keep layout bounded; exceeding one drops the diagram to fallback. */
9
+ export const MAX_NODES = 128
10
+ export const MAX_EDGES = 512
11
+ export const MAX_GROUPS = 24
12
+ export const MAX_GROUP_DEPTH = 6
13
+ /** Class members / ER attributes listed per box before eliding with `…`. */
14
+ export const MAX_MEMBERS = 8
15
+
16
+ export type Shape = 'rect' | 'round' | 'diamond'
17
+
18
+ /** Decoration at one end of an edge. */
19
+ export type Head =
20
+ | 'none'
21
+ | 'arrow'
22
+ | 'circle'
23
+ | 'cross'
24
+ | 'triangle'
25
+ | 'diamondFill'
26
+ | 'diamondOpen'
27
+
28
+ export type LineKind = 'solid' | 'dotted' | 'thick'
29
+
30
+ export type Dir = 'down' | 'up' | 'right' | 'left'
31
+
32
+ export interface Node {
33
+ label: string
34
+ shape: Shape
35
+ }
36
+
37
+ export interface Edge {
38
+ from: number
39
+ to: number
40
+ label: string | null
41
+ headTo: Head
42
+ headFrom: Head
43
+ line: LineKind
44
+ }
45
+
46
+ export interface Group {
47
+ id: string
48
+ label: string
49
+ parent: number | null
50
+ }
51
+
52
+ /** Extra compartment content for class and ER boxes. */
53
+ export interface ClassInfo {
54
+ annotation: string | null
55
+ attrs: string[]
56
+ methods: string[]
57
+ }
58
+
59
+ export const emptyClassInfo = (): ClassInfo => ({ annotation: null, attrs: [], methods: [] })
60
+
61
+ /** `LR`/`RL`/`BT` as written in a header or `direction` statement; else `down`. */
62
+ export function parseDir(token: string): Dir {
63
+ switch (asciiUpper(token)) {
64
+ case 'LR':
65
+ return 'right'
66
+ case 'RL':
67
+ return 'left'
68
+ case 'BT':
69
+ return 'up'
70
+ default:
71
+ return 'down'
72
+ }
73
+ }
74
+
75
+ export class Graph {
76
+ nodes: Node[] = []
77
+ edges: Edge[] = []
78
+ index = new Map<string, number>()
79
+ groups: Group[] = []
80
+ /** Innermost subgraph each node was declared in, parallel to `nodes`. */
81
+ nodeGroup: (number | null)[] = []
82
+ curGroup: number | null = null
83
+ /** Set when a cap was hit; the caller abandons the parse. */
84
+ overCap = false
85
+ /**
86
+ * Text the flowchart grammar could not read and silently discarded.
87
+ *
88
+ * Flowchart parsing is deliberately lenient — a malformed statement
89
+ * contributes whatever prefix parsed and the rest is dropped — so without
90
+ * these the reader gets a clean diagram that is not what they wrote.
91
+ */
92
+ warnings: string[] = []
93
+ dir: Dir = 'down'
94
+
95
+ constructor(dir: Dir = 'down') {
96
+ this.dir = dir
97
+ }
98
+
99
+ /**
100
+ * Index of `id`, creating the node if new. A later declaration carrying a
101
+ * label overwrites the placeholder one an edge created. Returns `null` once
102
+ * `MAX_NODES` is reached, which aborts the parse.
103
+ */
104
+ nodeIndex(id: string, label: string | null, shape: Shape): number | null {
105
+ const existing = this.index.get(id)
106
+ if (existing !== undefined) {
107
+ if (label !== null) {
108
+ this.nodes[existing].label = label
109
+ this.nodes[existing].shape = shape
110
+ }
111
+ return existing
112
+ }
113
+ if (this.nodes.length >= MAX_NODES) {
114
+ this.overCap = true
115
+ return null
116
+ }
117
+ this.index.set(id, this.nodes.length)
118
+ this.nodes.push({ label: label ?? id, shape })
119
+ this.nodeGroup.push(this.curGroup)
120
+ return this.nodes.length - 1
121
+ }
122
+
123
+ /** Set a node's label without disturbing its shape, creating it if new. */
124
+ nodeLabel(id: string, label: string): number | null {
125
+ const existing = this.index.get(id)
126
+ if (existing !== undefined) {
127
+ this.nodes[existing].label = label
128
+ return existing
129
+ }
130
+ return this.nodeIndex(id, label, 'round')
131
+ }
132
+
133
+ /** Append an edge, or flag `overCap` when `MAX_EDGES` is reached. */
134
+ pushEdge(edge: Edge): boolean {
135
+ if (this.edges.length >= MAX_EDGES) {
136
+ this.overCap = true
137
+ return false
138
+ }
139
+ this.edges.push(edge)
140
+ return true
141
+ }
142
+ }
@@ -0,0 +1,104 @@
1
+ import { stripControls } from './labels.ts'
2
+ import { type CanvasResult, layoutClass, layoutFlowchart, layoutGrouped } from './layout.ts'
3
+ import { layoutSequence } from './layout-seq.ts'
4
+ import { diagramKind, parseClass, parseEr, parseGraph, parseSequence, parseState } from './parse.ts'
5
+ import type { MermaidArt } from './types.ts'
6
+
7
+ export { type AnsiTheme, DEFAULT_THEME, toAnsi } from './ansi.ts'
8
+ export { type DiagramKind, diagramKind } from './parse.ts'
9
+ export { sourceBox } from './source-box.ts'
10
+ export type { Cls, MermaidArt, Span } from './types.ts'
11
+
12
+ /**
13
+ * Render a Mermaid source block as Unicode box-drawing art.
14
+ *
15
+ * Supported: `graph`/`flowchart` (including `subgraph`), `stateDiagram`,
16
+ * `classDiagram`, `erDiagram` and `sequenceDiagram`.
17
+ *
18
+ * The diagram is laid out at whatever size it needs; `art.width` reports the
19
+ * columns that turned out to be. Deciding what to do when that exceeds the
20
+ * space at hand is the caller's — `sourceBox` is the usual answer:
21
+ *
22
+ * ```ts
23
+ * const art = render(src)
24
+ * show(art && art.width <= cols ? art : sourceBox(src, cols))
25
+ * ```
26
+ *
27
+ * `null` means there is no art to show: blank input, a syntax error, a diagram
28
+ * type this renderer does not draw, or one large enough that laying it out is
29
+ * refused. `diagramKind` separates the middle two.
30
+ *
31
+ * Rendering is best-effort. A flowchart keeps whatever parsed; the stricter
32
+ * grammars additionally get one retry without their final line, which is what
33
+ * keeps a streaming diagram on screen while its last statement is half-typed.
34
+ * Everything given up on is listed in `art.warnings` — advisory only, never a
35
+ * reason to withhold the art.
36
+ */
37
+ export function render(src: string): MermaidArt | null {
38
+ src = stripControls(src)
39
+ if (src.trim() === '') return null
40
+ const drawn = attempt(src)
41
+ if (drawn === null) return null
42
+ return { ...drawn.canvas.toLines(), warnings: drawn.warnings }
43
+ }
44
+
45
+ type Drawn = { canvas: NonNullable<CanvasResult>; warnings: string[] }
46
+
47
+ /**
48
+ * Draw `src`, retrying once without its last line if the grammar rejects it.
49
+ *
50
+ * State, class, ER and sequence fail a whole diagram on one unreadable
51
+ * statement, and while a source is streaming its last line is usually still
52
+ * being typed — so without this a diagram alternates with the source box all
53
+ * the way in. Only the final line is dropped, and doing so is always reported,
54
+ * so a finished document with a bad last line still says what it lost rather
55
+ * than quietly rendering short.
56
+ */
57
+ function attempt(src: string): Drawn | null {
58
+ const drawn = draw(src)
59
+ if (drawn !== null) return drawn
60
+
61
+ const body = src.replace(/\s+$/, '')
62
+ const cut = body.lastIndexOf('\n')
63
+ if (cut === -1) return null
64
+ const salvaged = draw(body.slice(0, cut))
65
+ if (salvaged === null) return null
66
+
67
+ const dropped = body.slice(cut + 1).trim()
68
+ return {
69
+ canvas: salvaged.canvas,
70
+ warnings: [...salvaged.warnings, `dropped, unreadable final line: "${dropped}"`],
71
+ }
72
+ }
73
+
74
+ /** Dispatch on the declared diagram type; `null` means nothing was drawn. */
75
+ function draw(src: string): Drawn | null {
76
+ const plain = (canvas: CanvasResult) => (canvas === null ? null : { canvas, warnings: [] })
77
+
78
+ switch (diagramKind(src)) {
79
+ case 'flowchart': {
80
+ const graph = parseGraph(src)
81
+ if (graph === null) return null
82
+ const canvas = graph.groups.length === 0 ? layoutFlowchart(graph) : layoutGrouped(graph)
83
+ return canvas === null ? null : { canvas, warnings: graph.warnings }
84
+ }
85
+ case 'state': {
86
+ const state = parseState(src)
87
+ return state === null ? null : plain(layoutFlowchart(state))
88
+ }
89
+ case 'class': {
90
+ const cls = parseClass(src)
91
+ return cls === null ? null : plain(layoutClass(cls.graph, cls.infos))
92
+ }
93
+ case 'er': {
94
+ const er = parseEr(src)
95
+ return er === null ? null : plain(layoutClass(er.graph, er.infos))
96
+ }
97
+ case 'sequence': {
98
+ const seq = parseSequence(src)
99
+ return seq === null ? null : plain(layoutSequence(seq))
100
+ }
101
+ default:
102
+ return null
103
+ }
104
+ }
@@ -0,0 +1,326 @@
1
+ import { measured, stringWidth } from './width.ts'
2
+
3
+ /** Node labels wrap to at most this many display columns per line ... */
4
+ export const WRAP_WIDTH = 24
5
+ /** ... and at most this many lines; overflow is truncated with an ellipsis. */
6
+ export const MAX_LINES = 4
7
+ /** Edge labels are truncated to this many columns. */
8
+ export const MAX_LABEL = 28
9
+
10
+ /**
11
+ * Identifier-boundary characters preferred as break points when a single word
12
+ * is too wide to fit, so it is not sliced mid-segment.
13
+ *
14
+ * Mirrors `TOKEN_BREAK_CHARS` in grok-build's
15
+ * `third_party/mermaid-to-svg/src/text_wrap.rs`; the two renderers are
16
+ * deliberately independent, so keep these in sync.
17
+ */
18
+ export const LABEL_BREAK_CHARS = ['_', '-', '.', '/']
19
+
20
+ /**
21
+ * ASCII-only case folding, matching Rust's `to_ascii_lowercase`.
22
+ *
23
+ * `String.prototype.toLowerCase` can change a string's length (`İ` becomes two
24
+ * code points), which would desync the byte offsets some parsers slice with.
25
+ */
26
+ export const asciiLower = (s: string): string => s.replace(/[A-Z]/g, (c) => c.toLowerCase())
27
+ export const asciiUpper = (s: string): string => s.replace(/[a-z]/g, (c) => c.toUpperCase())
28
+
29
+ /**
30
+ * C0 and C1 controls, less the `\t\n\r` the parsers and `srcLines` read.
31
+ *
32
+ * They measure one column and paint none, so a box sized around one is drawn a
33
+ * column short of its own border; NUL also collides with the `CONT` sentinel
34
+ * and is dropped after layout has already paid for its cell; ESC would inject
35
+ * ANSI into the caller's scrollback. `decodeEntityBody` refuses to decode an
36
+ * entity into one — this closes the same hole for literals.
37
+ */
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: the point is to match them
39
+ const CONTROLS = /[\0-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g
40
+
41
+ /** Applied by every public entry point that takes untrusted source. */
42
+ export const stripControls = (src: string): string => src.replace(CONTROLS, '')
43
+
44
+ /**
45
+ * Split source into lines the way Rust's `str::lines()` does: on `\n`, with a
46
+ * trailing `\r` stripped, and *without* a final empty line when the input ends
47
+ * in a newline. `String.split` yields that extra element, which would show up
48
+ * as a spurious blank row inside a source box.
49
+ */
50
+ export function srcLines(src: string): string[] {
51
+ const out = src.split('\n').map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l))
52
+ if (out.length > 0 && out[out.length - 1] === '') out.pop()
53
+ return out
54
+ }
55
+
56
+ const ALNUM = /[\p{Alphabetic}\p{N}]/u
57
+
58
+ /** Matches Rust's `char::is_alphanumeric`. */
59
+ export const isAlphanumeric = (c: string): boolean => ALNUM.test(c)
60
+
61
+ /** Characters allowed in a bare node/state/class identifier. */
62
+ export const isIdChar = (c: string): boolean => isAlphanumeric(c) || c === '_'
63
+
64
+ const ENTITY_LOOKAHEAD = 10
65
+
66
+ const NAMED_ENTITIES: Record<string, string> = {
67
+ lt: '<',
68
+ gt: '>',
69
+ amp: '&',
70
+ quot: '"',
71
+ apos: "'",
72
+ }
73
+
74
+ function decodeEntityBody(body: string): string | null {
75
+ const named = NAMED_ENTITIES[body]
76
+ if (named !== undefined) return named
77
+ if (!body.startsWith('#')) return null
78
+ const num = body.slice(1)
79
+ const hex = /^[xX]/.test(num)
80
+ const digits = hex ? num.slice(1) : num
81
+ if (!(hex ? /^[0-9a-fA-F]+$/ : /^[0-9]+$/).test(digits)) return null
82
+ const code = Number.parseInt(digits, hex ? 16 : 10)
83
+ // Surrogates and out-of-range values are not characters at all.
84
+ if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return null
85
+ // Reject control chars: NUL collides with the CONT sentinel and ESC would
86
+ // inject ANSI into scrollback.
87
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f)) return null
88
+ return String.fromCodePoint(code)
89
+ }
90
+
91
+ /**
92
+ * Decode HTML entities in label text. Called once per label: via `cleanLabel`
93
+ * for bracketed labels, or explicitly at each direct-push sink.
94
+ */
95
+ export function decodeHtmlEntities(s: string): string {
96
+ if (!s.includes('&')) return s
97
+ const chars = [...s]
98
+ let out = ''
99
+ let i = 0
100
+ while (i < chars.length) {
101
+ if (chars[i] !== '&') {
102
+ out += chars[i]
103
+ i++
104
+ continue
105
+ }
106
+ // Scan a bounded window including the terminating `;`, so a stray `&` or an
107
+ // over-long run stays literal.
108
+ const hi = Math.min(i + 1 + ENTITY_LOOKAHEAD, chars.length)
109
+ let semi = -1
110
+ for (let j = i + 1; j < hi; j++) {
111
+ if (chars[j] === ';') {
112
+ semi = j
113
+ break
114
+ }
115
+ }
116
+ const decoded = semi === -1 ? null : decodeEntityBody(chars.slice(i + 1, semi).join(''))
117
+ if (decoded === null) {
118
+ out += '&'
119
+ i++
120
+ } else {
121
+ // Resume past the `;`. The single pass never re-scans emitted text, so
122
+ // `&amp;lt;` decodes to the literal `&lt;` rather than to `<`.
123
+ out += decoded
124
+ i = semi + 1
125
+ }
126
+ }
127
+ return out
128
+ }
129
+
130
+ /** Strip markdown emphasis from a `` `backtick` `` label string. */
131
+ export function stripMarkdown(s: string): string {
132
+ const noCode = [...s].filter((c) => c !== '`').join('')
133
+ const noStrong = noCode.replaceAll('**', '').replaceAll('__', '')
134
+ const chars = [...noStrong]
135
+ let out = ''
136
+ for (let i = 0; i < chars.length; i++) {
137
+ const c = chars[i]
138
+ // Keep `*`/`_` only when they sit inside a word, so snake_case survives.
139
+ const inWord =
140
+ i > 0 &&
141
+ isAlphanumeric(chars[i - 1]) &&
142
+ chars[i + 1] !== undefined &&
143
+ isAlphanumeric(chars[i + 1])
144
+ if ((c === '*' || c === '_') && !inWord) continue
145
+ out += c
146
+ }
147
+ return out.trim()
148
+ }
149
+
150
+ /**
151
+ * Inline formatting tags that carry no meaning in a terminal. Anything else
152
+ * that looks like a tag — `Vec<String>`, `<id>` — is left alone.
153
+ */
154
+ const HTML_FORMAT_TAGS = new Set([
155
+ 'b',
156
+ 'strong',
157
+ 'i',
158
+ 'em',
159
+ 'u',
160
+ 's',
161
+ 'strike',
162
+ 'del',
163
+ 'ins',
164
+ 'mark',
165
+ 'small',
166
+ 'big',
167
+ 'sub',
168
+ 'sup',
169
+ 'code',
170
+ 'kbd',
171
+ 'samp',
172
+ 'var',
173
+ 'tt',
174
+ 'span',
175
+ 'font',
176
+ 'q',
177
+ 'abbr',
178
+ 'cite',
179
+ 'pre',
180
+ ])
181
+
182
+ /** Read a tag starting at `start`, returning its name and the index after `>`. */
183
+ function htmlTagAt(chars: string[], start: number): { name: string; end: number } | null {
184
+ let i = start + 1
185
+ if (chars[i] === '/') i++
186
+ const nameStart = i
187
+ while (i < chars.length && /^[0-9A-Za-z]$/.test(chars[i])) i++
188
+ if (i === nameStart) return null
189
+ const name = chars.slice(nameStart, i).join('')
190
+ while (i < chars.length && chars[i] !== '>') {
191
+ if (chars[i] === '<') return null
192
+ i++
193
+ }
194
+ return chars[i] === '>' ? { name, end: i + 1 } : null
195
+ }
196
+
197
+ export function stripHtmlTags(s: string): string {
198
+ const chars = [...s]
199
+ let out = ''
200
+ let i = 0
201
+ while (i < chars.length) {
202
+ if (chars[i] === '<') {
203
+ const tag = htmlTagAt(chars, i)
204
+ if (tag) {
205
+ const lower = tag.name.toLowerCase()
206
+ if (lower === 'br') {
207
+ out += ' '
208
+ i = tag.end
209
+ continue
210
+ }
211
+ if (HTML_FORMAT_TAGS.has(lower)) {
212
+ i = tag.end
213
+ continue
214
+ }
215
+ }
216
+ }
217
+ out += chars[i]
218
+ i++
219
+ }
220
+ return out
221
+ }
222
+
223
+ /** Strip one matching pair of wrapping delimiters, if present. */
224
+ function unwrap(s: string, open: string, close: string): string | null {
225
+ return s.length >= open.length + close.length && s.startsWith(open) && s.endsWith(close)
226
+ ? s.slice(open.length, s.length - close.length)
227
+ : null
228
+ }
229
+
230
+ /**
231
+ * Normalise raw label text: strip markup, unquote, and decode entities.
232
+ *
233
+ * Decoding happens after tag-stripping so `<b>` is removed as markup while
234
+ * `&lt;b&gt;` survives as the literal text `<b>`.
235
+ */
236
+ export function cleanLabel(raw: string): string {
237
+ const trimmed = stripHtmlTags(raw.trim()).trim()
238
+ const unquoted = (unwrap(trimmed, '"', '"') ?? unwrap(trimmed, "'", "'") ?? trimmed).trim()
239
+ const md = unwrap(unquoted, '`', '`')
240
+ return decodeHtmlEntities(md === null ? unquoted : stripMarkdown(md.trim()))
241
+ }
242
+
243
+ /** Index of the last identifier-boundary character, or -1. */
244
+ function lastBreak(s: string): number {
245
+ let best = -1
246
+ for (const c of LABEL_BREAK_CHARS) best = Math.max(best, s.lastIndexOf(c))
247
+ return best
248
+ }
249
+
250
+ /**
251
+ * Wrap a label to `width` columns over at most `maxLines` lines, truncating the
252
+ * last line with an ellipsis if it overflows.
253
+ *
254
+ * A word too wide to fit is broken after the last identifier boundary
255
+ * (`_-./`) that fits, falling back to a per-character break when it has none.
256
+ */
257
+ export function wrapLabel(label: string, width: number, maxLines: number): string[] {
258
+ width = Math.max(1, width)
259
+ const lines: string[] = []
260
+ let cur = ''
261
+ let curW = 0
262
+
263
+ for (const word of label.split(/\s+/).filter((w) => w !== '')) {
264
+ const ww = stringWidth(word)
265
+ if (ww > width) {
266
+ if (cur !== '') {
267
+ lines.push(cur)
268
+ cur = ''
269
+ }
270
+ let chunk = ''
271
+ let chunkW = 0
272
+ for (const [ch, cw] of measured(word)) {
273
+ if (chunkW + cw > width && chunk !== '') {
274
+ const p = lastBreak(chunk)
275
+ const carry = p === -1 ? '' : chunk.slice(p + 1)
276
+ lines.push(p === -1 ? chunk : chunk.slice(0, p + 1))
277
+ chunk = carry
278
+ chunkW = stringWidth(carry)
279
+ }
280
+ chunk += ch
281
+ chunkW += cw
282
+ }
283
+ cur = chunk
284
+ curW = chunkW
285
+ } else if (cur === '') {
286
+ cur = word
287
+ curW = ww
288
+ } else if (curW + 1 + ww <= width) {
289
+ cur += ` ${word}`
290
+ curW += 1 + ww
291
+ } else {
292
+ lines.push(cur)
293
+ cur = word
294
+ curW = ww
295
+ }
296
+ }
297
+ if (cur !== '') lines.push(cur)
298
+ if (lines.length === 0) lines.push('')
299
+
300
+ if (lines.length > maxLines) {
301
+ lines.length = maxLines
302
+ const target = Math.max(1, width - 1)
303
+ let s = ''
304
+ let sw = 0
305
+ for (const [ch, cw] of measured(lines[lines.length - 1])) {
306
+ if (sw + cw > target) break
307
+ s += ch
308
+ sw += cw
309
+ }
310
+ lines[lines.length - 1] = `${s}…`
311
+ }
312
+ return lines
313
+ }
314
+
315
+ /** Truncate to `inner` columns, leaving room for the ellipsis. */
316
+ export function fitLabel(label: string, inner: number): string {
317
+ if (stringWidth(label) <= inner) return label
318
+ let out = ''
319
+ let used = 0
320
+ for (const [c, cw] of measured(label)) {
321
+ if (used + cw + 1 > inner) break
322
+ out += c
323
+ used += cw
324
+ }
325
+ return `${out}…`
326
+ }