@promptctl/rich-js 0.17.0 → 0.18.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.
Files changed (206) hide show
  1. package/README.md +13 -26
  2. package/package.json +7 -3
  3. package/dist/core/box.d.ts +0 -144
  4. package/dist/core/box.js +0 -391
  5. package/dist/core/cells.d.ts +0 -148
  6. package/dist/core/cells.js +0 -266
  7. package/dist/core/color.d.ts +0 -250
  8. package/dist/core/color.js +0 -1029
  9. package/dist/core/console.d.ts +0 -156
  10. package/dist/core/console.js +0 -472
  11. package/dist/core/emoji.d.ts +0 -24
  12. package/dist/core/emoji.js +0 -391
  13. package/dist/core/export-html.d.ts +0 -24
  14. package/dist/core/export-html.js +0 -106
  15. package/dist/core/export-lines.d.ts +0 -121
  16. package/dist/core/export-lines.js +0 -141
  17. package/dist/core/highlighter.d.ts +0 -33
  18. package/dist/core/highlighter.js +0 -124
  19. package/dist/core/json.d.ts +0 -27
  20. package/dist/core/json.js +0 -53
  21. package/dist/core/markup.d.ts +0 -97
  22. package/dist/core/markup.js +0 -619
  23. package/dist/core/measure.d.ts +0 -27
  24. package/dist/core/measure.js +0 -61
  25. package/dist/core/oklch.d.ts +0 -161
  26. package/dist/core/oklch.js +0 -312
  27. package/dist/core/osc8.d.ts +0 -68
  28. package/dist/core/osc8.js +0 -90
  29. package/dist/core/pretty.d.ts +0 -141
  30. package/dist/core/pretty.js +0 -472
  31. package/dist/core/protocol.d.ts +0 -137
  32. package/dist/core/protocol.js +0 -101
  33. package/dist/core/render.d.ts +0 -84
  34. package/dist/core/render.js +0 -145
  35. package/dist/core/segment.d.ts +0 -142
  36. package/dist/core/segment.js +0 -433
  37. package/dist/core/spinnerData.d.ts +0 -10
  38. package/dist/core/spinnerData.js +0 -115
  39. package/dist/core/strip.d.ts +0 -145
  40. package/dist/core/strip.js +0 -308
  41. package/dist/core/style.d.ts +0 -144
  42. package/dist/core/style.js +0 -723
  43. package/dist/core/subscription.d.ts +0 -12
  44. package/dist/core/subscription.js +0 -12
  45. package/dist/core/text.d.ts +0 -255
  46. package/dist/core/text.js +0 -990
  47. package/dist/core/wrap.d.ts +0 -41
  48. package/dist/core/wrap.js +0 -127
  49. package/dist/host/host-stream.d.ts +0 -30
  50. package/dist/host/host-stream.js +0 -35
  51. package/dist/host/index.d.ts +0 -3
  52. package/dist/host/index.js +0 -2
  53. package/dist/host/terminal-host.d.ts +0 -143
  54. package/dist/host/terminal-host.js +0 -132
  55. package/dist/index.d.ts +0 -87
  56. package/dist/index.js +0 -104
  57. package/dist/node/prompt.d.ts +0 -21
  58. package/dist/node/prompt.js +0 -30
  59. package/dist/node/save.d.ts +0 -17
  60. package/dist/node/save.js +0 -21
  61. package/dist/node/terminal-host.d.ts +0 -84
  62. package/dist/node/terminal-host.js +0 -160
  63. package/dist/node/traceback.d.ts +0 -27
  64. package/dist/node/traceback.js +0 -112
  65. package/dist/renderables/align.d.ts +0 -16
  66. package/dist/renderables/align.js +0 -46
  67. package/dist/renderables/columns.d.ts +0 -61
  68. package/dist/renderables/columns.js +0 -168
  69. package/dist/renderables/constrain.d.ts +0 -15
  70. package/dist/renderables/constrain.js +0 -37
  71. package/dist/renderables/flexStrip.d.ts +0 -41
  72. package/dist/renderables/flexStrip.js +0 -200
  73. package/dist/renderables/group.d.ts +0 -11
  74. package/dist/renderables/group.js +0 -15
  75. package/dist/renderables/layout.d.ts +0 -92
  76. package/dist/renderables/layout.js +0 -315
  77. package/dist/renderables/live.d.ts +0 -47
  78. package/dist/renderables/live.js +0 -131
  79. package/dist/renderables/markdown.d.ts +0 -24
  80. package/dist/renderables/markdown.js +0 -182
  81. package/dist/renderables/padding.d.ts +0 -38
  82. package/dist/renderables/padding.js +0 -140
  83. package/dist/renderables/panel.d.ts +0 -116
  84. package/dist/renderables/panel.js +0 -322
  85. package/dist/renderables/progress.d.ts +0 -90
  86. package/dist/renderables/progress.js +0 -241
  87. package/dist/renderables/progressBar.d.ts +0 -31
  88. package/dist/renderables/progressBar.js +0 -52
  89. package/dist/renderables/prompt.d.ts +0 -49
  90. package/dist/renderables/prompt.js +0 -125
  91. package/dist/renderables/rule.d.ts +0 -24
  92. package/dist/renderables/rule.js +0 -90
  93. package/dist/renderables/spinner.d.ts +0 -29
  94. package/dist/renderables/spinner.js +0 -64
  95. package/dist/renderables/status.d.ts +0 -23
  96. package/dist/renderables/status.js +0 -60
  97. package/dist/renderables/syntax.d.ts +0 -35
  98. package/dist/renderables/syntax.js +0 -114
  99. package/dist/renderables/table.d.ts +0 -182
  100. package/dist/renderables/table.js +0 -697
  101. package/dist/renderables/traceback.d.ts +0 -31
  102. package/dist/renderables/traceback.js +0 -109
  103. package/dist/renderables/tree.d.ts +0 -54
  104. package/dist/renderables/tree.js +0 -159
  105. package/dist/template-bindings/color-funcs.d.ts +0 -87
  106. package/dist/template-bindings/color-funcs.js +0 -219
  107. package/dist/template-bindings/helpers.d.ts +0 -24
  108. package/dist/template-bindings/helpers.js +0 -48
  109. package/dist/template-bindings/index.d.ts +0 -107
  110. package/dist/template-bindings/index.js +0 -144
  111. package/dist/template-bindings/palette-funcs.d.ts +0 -94
  112. package/dist/template-bindings/palette-funcs.js +0 -151
  113. package/dist/template-bindings/style-funcs.d.ts +0 -28
  114. package/dist/template-bindings/style-funcs.js +0 -151
  115. package/dist/themes/buildPalette.d.ts +0 -36
  116. package/dist/themes/buildPalette.js +0 -56
  117. package/dist/themes/colorMath.d.ts +0 -82
  118. package/dist/themes/colorMath.js +0 -254
  119. package/dist/themes/colorRef.d.ts +0 -51
  120. package/dist/themes/colorRef.js +0 -96
  121. package/dist/themes/data/atom-one-dark.d.ts +0 -3
  122. package/dist/themes/data/atom-one-dark.js +0 -158
  123. package/dist/themes/data/atom-one-light.d.ts +0 -3
  124. package/dist/themes/data/atom-one-light.js +0 -158
  125. package/dist/themes/data/catppuccin-frappe.d.ts +0 -3
  126. package/dist/themes/data/catppuccin-frappe.js +0 -158
  127. package/dist/themes/data/catppuccin-latte.d.ts +0 -3
  128. package/dist/themes/data/catppuccin-latte.js +0 -158
  129. package/dist/themes/data/catppuccin-macchiato.d.ts +0 -3
  130. package/dist/themes/data/catppuccin-macchiato.js +0 -158
  131. package/dist/themes/data/catppuccin-mocha.d.ts +0 -3
  132. package/dist/themes/data/catppuccin-mocha.js +0 -158
  133. package/dist/themes/data/cyberpunk.d.ts +0 -17
  134. package/dist/themes/data/cyberpunk.js +0 -191
  135. package/dist/themes/data/default.d.ts +0 -3
  136. package/dist/themes/data/default.js +0 -158
  137. package/dist/themes/data/dracula.d.ts +0 -3
  138. package/dist/themes/data/dracula.js +0 -158
  139. package/dist/themes/data/flexoki.d.ts +0 -3
  140. package/dist/themes/data/flexoki.js +0 -158
  141. package/dist/themes/data/gruvbox.d.ts +0 -3
  142. package/dist/themes/data/gruvbox.js +0 -158
  143. package/dist/themes/data/index.d.ts +0 -35
  144. package/dist/themes/data/index.js +0 -64
  145. package/dist/themes/data/monokai.d.ts +0 -3
  146. package/dist/themes/data/monokai.js +0 -158
  147. package/dist/themes/data/nord.d.ts +0 -3
  148. package/dist/themes/data/nord.js +0 -158
  149. package/dist/themes/data/rose-pine-dawn.d.ts +0 -3
  150. package/dist/themes/data/rose-pine-dawn.js +0 -158
  151. package/dist/themes/data/rose-pine-moon.d.ts +0 -3
  152. package/dist/themes/data/rose-pine-moon.js +0 -158
  153. package/dist/themes/data/rose-pine.d.ts +0 -3
  154. package/dist/themes/data/rose-pine.js +0 -158
  155. package/dist/themes/data/solarized-dark.d.ts +0 -3
  156. package/dist/themes/data/solarized-dark.js +0 -158
  157. package/dist/themes/data/solarized-light.d.ts +0 -3
  158. package/dist/themes/data/solarized-light.js +0 -158
  159. package/dist/themes/data/svg-export.d.ts +0 -3
  160. package/dist/themes/data/svg-export.js +0 -158
  161. package/dist/themes/data/textual-ansi.d.ts +0 -3
  162. package/dist/themes/data/textual-ansi.js +0 -158
  163. package/dist/themes/data/textual-dark.d.ts +0 -3
  164. package/dist/themes/data/textual-dark.js +0 -158
  165. package/dist/themes/data/textual-light.d.ts +0 -3
  166. package/dist/themes/data/textual-light.js +0 -158
  167. package/dist/themes/data/tokyo-night.d.ts +0 -3
  168. package/dist/themes/data/tokyo-night.js +0 -158
  169. package/dist/themes/data/types.d.ts +0 -15
  170. package/dist/themes/data/types.js +0 -1
  171. package/dist/themes/palette.d.ts +0 -21
  172. package/dist/themes/palette.js +0 -28
  173. package/dist/themes/ramp.d.ts +0 -83
  174. package/dist/themes/ramp.js +0 -122
  175. package/dist/themes/registry.d.ts +0 -43
  176. package/dist/themes/registry.js +0 -82
  177. package/dist/themes/terminalThemes.d.ts +0 -54
  178. package/dist/themes/terminalThemes.js +0 -68
  179. package/dist/themes/transpose.d.ts +0 -87
  180. package/dist/themes/transpose.js +0 -138
  181. package/dist/widgets/button.d.ts +0 -49
  182. package/dist/widgets/button.js +0 -206
  183. package/dist/widgets/checkbox.d.ts +0 -41
  184. package/dist/widgets/checkbox.js +0 -155
  185. package/dist/widgets/dropdown.d.ts +0 -67
  186. package/dist/widgets/dropdown.js +0 -453
  187. package/dist/widgets/event-router.d.ts +0 -78
  188. package/dist/widgets/event-router.js +0 -622
  189. package/dist/widgets/focus-manager.d.ts +0 -26
  190. package/dist/widgets/focus-manager.js +0 -193
  191. package/dist/widgets/index.d.ts +0 -22
  192. package/dist/widgets/index.js +0 -15
  193. package/dist/widgets/screen.d.ts +0 -98
  194. package/dist/widgets/screen.js +0 -530
  195. package/dist/widgets/slider.d.ts +0 -57
  196. package/dist/widgets/slider.js +0 -270
  197. package/dist/widgets/static-item.d.ts +0 -40
  198. package/dist/widgets/static-item.js +0 -60
  199. package/dist/widgets/text-input.d.ts +0 -274
  200. package/dist/widgets/text-input.js +0 -1162
  201. package/dist/widgets/toggle.d.ts +0 -44
  202. package/dist/widgets/toggle.js +0 -179
  203. package/dist/widgets/types.d.ts +0 -108
  204. package/dist/widgets/types.js +0 -31
  205. package/dist/widgets/widget-base.d.ts +0 -42
  206. package/dist/widgets/widget-base.js +0 -173
@@ -1,619 +0,0 @@
1
- /**
2
- * Markup — BBCode-inspired markup parser for inline styling.
3
- * Parses `[bold red]text[/bold red]` into RichText with styled spans.
4
- *
5
- * Plugin tags ([LAW:locality-or-seam]): a `MarkupRegistry` lets consumers
6
- * register tag handlers without forking the parser. When the parser sees
7
- * `[name attrs...]inner[/name]` and `name` is registered, it parses attrs,
8
- * recursively renders the inner markup as a child Renderable, and calls the
9
- * handler — splicing the handler's returned Renderable into the output. The
10
- * built-in style dialect and the plugin dialect are routed by the registry,
11
- * which is the single trust boundary between them.
12
- *
13
- * Plugin pairs must nest. The built-in dialect admits non-strict nesting
14
- * (`[bold]a[italic]b[/bold]c[/italic]`) because a style is an annotation and
15
- * annotations may overlap freely; a plugin tag is a replacement whose handler
16
- * takes one contiguous `inner`, so an overlapping pair has no slice to hand it
17
- * and is rejected with a `MarkupSyntaxError`.
18
- */
19
- import { cellLen } from "./cells.js";
20
- import { Style, StyleSyntaxError } from "./style.js";
21
- import { RichText, Span } from "./text.js";
22
- import { emojiReplace } from "./emoji.js";
23
- // --- Tag ---
24
- export class Tag {
25
- name;
26
- parameters;
27
- constructor(name, parameters) {
28
- this.name = name;
29
- this.parameters = parameters;
30
- }
31
- toString() {
32
- return this.parameters !== undefined
33
- ? `${this.name} ${this.parameters}`
34
- : this.name;
35
- }
36
- get markup() {
37
- return this.parameters !== undefined
38
- ? `[${this.name}=${this.parameters}]`
39
- : `[${this.name}]`;
40
- }
41
- }
42
- // --- MarkupError ---
43
- export class MarkupError extends Error {
44
- constructor(message) {
45
- super(message);
46
- this.name = "MarkupError";
47
- }
48
- }
49
- // --- MarkupSyntaxError ---
50
- /**
51
- * A markup string that cannot be parsed, with where it went wrong.
52
- *
53
- * [LAW:types-are-the-program] A subclass rather than optional fields on
54
- * `MarkupError`, because the parent is also what `MarkupRegistry.register`
55
- * throws, and a registration has no source text to point into. Every error this
56
- * class describes has a location, so none of its fields is optional and no
57
- * caller has to ask whether one is present — `instanceof` is the only question.
58
- *
59
- * [LAW:one-source-of-truth] The constructor takes the facts the parser holds —
60
- * the string, the offset of the offending tag, what was open — and derives
61
- * `line`, `column` and the message from them, so the numbers a caller reads
62
- * and the caret the message draws cannot disagree.
63
- */
64
- export class MarkupSyntaxError extends MarkupError {
65
- /** The problem alone, with no location, for callers building their own message. */
66
- reason;
67
- /** The whole markup string that failed, as the caller passed it. */
68
- markup;
69
- /** Index into `markup` of the tag the parser rejected. */
70
- offset;
71
- /** 1-based line of `offset`; lines are separated by `\n`. */
72
- line;
73
- /** 1-based column of `offset` within its line, in UTF-16 code units. */
74
- column;
75
- /**
76
- * The opening tags still open at `offset`, outermost first, as written. For
77
- * overlapping plugin tags, only plugin tags are named.
78
- */
79
- openTags;
80
- constructor(reason, markup, offset, openTags) {
81
- const lineStart = markup.lastIndexOf("\n", offset - 1) + 1;
82
- const lineEnd = markup.indexOf("\n", offset);
83
- const text = markup.slice(lineStart, lineEnd === -1 ? markup.length : lineEnd);
84
- const line = countNewlines(markup.slice(0, lineStart)) + 1;
85
- const column = offset - lineStart + 1;
86
- const open = openTags.length > 0 ? openTags.join(" ") : "none";
87
- super(`${printable(reason)} (line ${line}, column ${column})\n` +
88
- `${excerpt(text, offset - lineStart)}\n` +
89
- `Open tags: ${printable(open)}`);
90
- this.name = "MarkupSyntaxError";
91
- this.reason = reason;
92
- this.markup = markup;
93
- this.offset = offset;
94
- this.line = line;
95
- this.column = column;
96
- this.openTags = openTags;
97
- }
98
- }
99
- function countNewlines(text) {
100
- return text.split("\n").length - 1;
101
- }
102
- // Code points shown either side of the error before the excerpt is cut and
103
- // marked with an ellipsis. Enough to recognise the surrounding tags on a line
104
- // that is long, templated, or machine-assembled, without printing all of it.
105
- const EXCERPT_CONTEXT = 30;
106
- /**
107
- * `text` with each C0 control and DEL shown as one space.
108
- *
109
- * [LAW:single-enforcer] Every part of the message that quotes the caller's
110
- * markup passes through here: the reason and the open tags quote tag text, and
111
- * the tag grammar admits control characters, so an `ESC ]` inside a tag would
112
- * otherwise open an OSC sequence in the terminal printing the error. In the
113
- * excerpt it also keeps the caret aligned, since a control occupies no cell.
114
- * The fields keep the text as written; only the message is made printable.
115
- */
116
- function printable(text) {
117
- return text.replace(/[\x00-\x1f\x7f]/g, " ");
118
- }
119
- /**
120
- * Two lines: the source line around `index`, and a caret under it.
121
- *
122
- * The window is cut in code points, so a surrogate pair is never split, and the
123
- * caret is placed by cell width, so a wide character before the error still
124
- * leaves it under the right cell.
125
- */
126
- function excerpt(text, index) {
127
- const chars = Array.from(printable(text));
128
- const at = Array.from(text.slice(0, index)).length;
129
- const from = Math.max(0, at - EXCERPT_CONTEXT);
130
- const to = Math.min(chars.length, at + EXCERPT_CONTEXT);
131
- const head = from > 0 ? "…" : "";
132
- const tail = to < chars.length ? "…" : "";
133
- const before = head + chars.slice(from, at).join("");
134
- const shown = before + chars.slice(at, to).join("") + tail;
135
- return ` ${shown}\n ${" ".repeat(cellLen(before))}^`;
136
- }
137
- // --- escape ---
138
- /**
139
- * Escapes markup characters so they render as literal text.
140
- */
141
- export function escape(text) {
142
- return text.replace(/\[/g, "\\[");
143
- }
144
- // --- render ---
145
- // Fast path: if no `[` in text, skip parsing entirely
146
- const HAS_TAG_RE = /\[/;
147
- // [LAW:one-source-of-truth] The authority for this grammar is `RE_TAGS` in
148
- // `rich/markup.py`, and this is deliberately its expression rather than a
149
- // re-derivation, so the two can be read side by side; `[^[]*?` is lazy against
150
- // the following `]`, which is what stops the body at the first one.
151
- //
152
- // The enumeration it replaced — `[a-zA-Z#][a-zA-Z0-9_.# -]*` — was a second
153
- // drawing of the same territory and had drifted both ways at once: it invented
154
- // uppercase, so `[INFO]` was taken as an opening tag and its text vanished from
155
- // every `Console.print` with no error, and it omitted `@` and the punctuation
156
- // the reference admits, so `[a+b]` stayed literal (rich-markup-sec).
157
- // `markup-grammar.golden.txt` pins both directions.
158
- // The leading alternative handles an escaped bracket: \[
159
- const TAG_RE = /(?:\\\[)|(\[[a-z#/@][^[]*?\])/g;
160
- function parseTags(markup) {
161
- const tags = [];
162
- const re = new RegExp(TAG_RE.source, TAG_RE.flags);
163
- let match;
164
- while ((match = re.exec(markup)) !== null) {
165
- // Escaped bracket
166
- if (match[0] === "\\[")
167
- continue;
168
- const captured = match[1];
169
- if (!captured)
170
- continue;
171
- const inner = captured.slice(1, -1); // Remove [ ]
172
- const isClose = inner.startsWith("/");
173
- const stylePart = isClose ? inner.slice(1) : inner;
174
- // Check for parameters (name=value)
175
- const eqIdx = stylePart.indexOf("=");
176
- const styleName = eqIdx >= 0 ? stylePart.slice(0, eqIdx).trim() : stylePart.trim();
177
- const parameters = eqIdx >= 0 ? stylePart.slice(eqIdx + 1) : undefined;
178
- // [LAW:types-are-the-program] What separates `[/]` from `[/red]` is whether
179
- // a name survives the slash, which is how the reference asks it too
180
- // (`style_name = tag.name[1:].strip()`). Testing the raw tag text against
181
- // the literal `"/"` answered a narrower question, and the widened grammar
182
- // is what exposed the gap: `[/ ]` reaches here now, and under the old test
183
- // it was an explicit close of the empty style rather than an implicit one.
184
- const isImplicitClose = isClose && styleName === "";
185
- const isClosing = isClose && !isImplicitClose;
186
- tags.push({
187
- fullMatch: captured,
188
- isClosing,
189
- isImplicitClose,
190
- styleName,
191
- parameters,
192
- start: match.index,
193
- end: match.index + match[0].length,
194
- });
195
- }
196
- return tags;
197
- }
198
- function wholeString(markup) {
199
- return { source: markup, base: 0, enclosing: [] };
200
- }
201
- /**
202
- * Parses the built-in style dialect — `[bold red]text[/bold red]` — into a
203
- * `RichText` with styled spans. Module-private on purpose: `renderMarkup` is
204
- * this module's one crossing, and it delegates straight here the moment a
205
- * string carries no paired plugin tag.
206
- *
207
- * [LAW:single-enforcer] Exporting this is what let a caller bind to the inner
208
- * layer, and two of them did — `console.ts` and `prompt.ts` imported it as
209
- * `render as renderMarkup`, so the import line read identically to the
210
- * plugin-aware sites and the difference was invisible at every point of use. A
211
- * tag registered on `globalMarkupRegistry` resolved in a table cell and was
212
- * silently eaten by `console.print`, the path almost every consumer takes
213
- * (rich-markup-pcp). Unexported, that drift is a compile error rather than a
214
- * rule someone has to keep remembering.
215
- *
216
- * [LAW:dataflow-not-control-flow] Rendering *without* plugins stays reachable
217
- * as a value rather than a second name: `renderMarkup(s, { registry: new
218
- * MarkupRegistry() })`. Two exported functions put that variability in the
219
- * function names; an empty registry puts it in the data, where the one
220
- * boundary admits both.
221
- */
222
- function render(markup, origin, baseStyle, options) {
223
- // Fast path: no brackets at all
224
- if (!HAS_TAG_RE.test(markup)) {
225
- let text = markup;
226
- if (options?.emoji !== false) {
227
- text = emojiReplace(text);
228
- }
229
- const result = new RichText(text);
230
- if (baseStyle)
231
- result.stylize(baseStyle);
232
- return result;
233
- }
234
- const tags = parseTags(markup);
235
- const doEmoji = options?.emoji !== false;
236
- // Build plain text and track spans
237
- let plainText = "";
238
- const spans = [];
239
- const openStack = [];
240
- const unparsable = (reason, tag) => new MarkupSyntaxError(reason, origin.source, origin.base + tag.start, [
241
- ...origin.enclosing,
242
- ...openStack.map((opened) => opened.tag),
243
- ]);
244
- let lastEnd = 0;
245
- for (const tag of tags) {
246
- // Add text before this tag
247
- const textBefore = markup.slice(lastEnd, tag.start);
248
- const processed = unescapeBrackets(doEmoji ? emojiReplace(textBefore) : textBefore);
249
- plainText += processed;
250
- if (tag.isImplicitClose) {
251
- // [/] — close the most recent open tag
252
- if (openStack.length === 0) {
253
- // [/] never closes a plugin tag, so an enclosing plugin tag can be open
254
- // here while no style tag is.
255
- throw unparsable(`Closing tag ${tag.fullMatch} has no open style tag to close`, tag);
256
- }
257
- const opened = openStack.pop();
258
- spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
259
- }
260
- else if (tag.isClosing) {
261
- // [/style] — find and close matching open tag
262
- const idx = findLastOpen(openStack, tag.styleName);
263
- if (idx === -1) {
264
- throw unparsable(`Closing tag ${tag.fullMatch} doesn't match any open tag`, tag);
265
- }
266
- const opened = openStack[idx];
267
- spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
268
- openStack.splice(idx, 1);
269
- }
270
- else {
271
- // Opening tag
272
- openStack.push({
273
- tag: tag.fullMatch,
274
- styleName: tag.styleName,
275
- parameters: tag.parameters,
276
- textStart: plainText.length,
277
- });
278
- }
279
- lastEnd = tag.end;
280
- }
281
- // Add remaining text after last tag
282
- const trailing = markup.slice(lastEnd);
283
- const processedTrailing = unescapeBrackets(doEmoji ? emojiReplace(trailing) : trailing);
284
- plainText += processedTrailing;
285
- // Auto-close what is still open, innermost first. The reference pops its
286
- // stack here (`while style_stack: start, tag = style_stack.pop()`), and
287
- // popping is what keeps every span in this list in the order its tag closed
288
- // — the one order the sort below is defined against. Walking the stack
289
- // forwards instead put the outermost unclosed tag in first, and that lone
290
- // disagreement was invisible while nothing sorted: it cancelled the missing
291
- // sort, so `[red][blue]x` was the one nesting this port already got right.
292
- while (openStack.length > 0) {
293
- const opened = openStack.pop();
294
- spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
295
- }
296
- // [LAW:one-source-of-truth] The paint order is the reference's, expressed
297
- // rather than re-derived: `sorted(spans[::-1], key=attrgetter("start"))` in
298
- // `rich/markup.py`. `RichText` applies spans in list order and each one adds
299
- // over the last, so this list's order *is* which style wins where two
300
- // overlap; sorting by start lays the outer span down first and lets the
301
- // inner one repaint the run it sits in.
302
- //
303
- // The reversal is the whole of the tie-break, not decoration.
304
- // `[red][blue]x[/blue][/red]` yields two spans that both start at 0, and a
305
- // stable sort leaves those in the order they arrived — closing order,
306
- // innermost first — which is the outer colour winning. Reversed first, the
307
- // outer arrives first and the inner repaints it (rich-markup-krk).
308
- spans.reverse();
309
- spans.sort((a, b) => a.start - b.start);
310
- const result = new RichText(plainText);
311
- if (baseStyle)
312
- result.stylize(baseStyle);
313
- // Apply link parameters as link styles
314
- for (const span of spans) {
315
- const style = typeof span.style === "string" ? span.style : span.style.toString();
316
- // Check if this is a link tag (name=url pattern was parsed)
317
- const linkMatch = /^link\s+(.+)$/.exec(style);
318
- if (linkMatch) {
319
- result.stylize(new Style({ link: linkMatch[1] }), span.start, span.end);
320
- }
321
- else {
322
- result.stylize(style, span.start, span.end);
323
- }
324
- }
325
- return result;
326
- }
327
- /**
328
- * The style string an open tag closes into — `name`, or `name parameters` when
329
- * the tag carried an `=`.
330
- *
331
- * [LAW:one-source-of-truth] One answer for the four sites that ask it: the three
332
- * ways a tag can close (`[/name]`, `[/]`, and end of input) and the search that
333
- * matches `[/red on blue]` back to `[red on blue]`. It was written out at each,
334
- * which is how the three closing paths could — and did — drift apart on
335
- * something none of the four copies was about.
336
- */
337
- function openTagStyle(opened) {
338
- return opened.parameters !== undefined
339
- ? `${opened.styleName} ${opened.parameters}`
340
- : opened.styleName;
341
- }
342
- function findLastOpen(stack, name) {
343
- // Handle "on" in style names for closing: [/red on blue] should match [red on blue]
344
- const normalized = name.trim();
345
- for (let i = stack.length - 1; i >= 0; i--) {
346
- const entry = stack[i];
347
- if (openTagStyle(entry) === normalized || entry.styleName === normalized)
348
- return i;
349
- }
350
- return -1;
351
- }
352
- function unescapeBrackets(text) {
353
- return text.replace(/\\\[/g, "[");
354
- }
355
- // [LAW:one-source-of-truth] The one answer to "what is a legal plugin tag
356
- // name?". Both sites that need it are derived from this fragment rather than
357
- // each carrying its own scanner: `register` anchors it to reject a name markup
358
- // could never address, and the match site anchors it at the head with a
359
- // boundary lookahead. When those two disagreed, registering `table` silently
360
- // destroyed every `table.*` built-in style and a name containing a dot could
361
- // never fire.
362
- //
363
- // It opens on `a-z` because a plugin name has to be a name `TAG_RE` can reach,
364
- // and that is strictly narrower than "a letter": nothing lowercases the tag
365
- // text on the way in, so `[Foo]` is literal and a handler registered as `Foo`
366
- // could never fire. This tracked `[A-Za-z]` while `TAG_RE` wrongly admitted
367
- // uppercase, which is the same drift, one subset down (rich-markup-sec).
368
- const PLUGIN_TAG_NAME_SRC = "[a-z][A-Za-z0-9_-]*";
369
- const LEGAL_PLUGIN_TAG_NAME = new RegExp(`^${PLUGIN_TAG_NAME_SRC}$`);
370
- export class MarkupRegistry {
371
- _handlers = new Map();
372
- register(name, handler) {
373
- // [LAW:no-silent-failure] Registration used to accept any string, including
374
- // names markup can never address (`a.b` stops the tag scan at the dot), and
375
- // install a handler that could never fire.
376
- if (!LEGAL_PLUGIN_TAG_NAME.test(name)) {
377
- throw new MarkupError(`Cannot register markup tag "${name}": a tag name must be a lowercase letter followed by letters, digits, "_" or "-", so markup could never address this name.`);
378
- }
379
- if (isReservedTagName(name)) {
380
- throw new MarkupError(`Cannot register markup tag "${name}": name is reserved by a built-in style.`);
381
- }
382
- this._handlers.set(name, handler);
383
- }
384
- unregister(name) {
385
- this._handlers.delete(name);
386
- }
387
- has(name) {
388
- return this._handlers.has(name);
389
- }
390
- get(name) {
391
- return this._handlers.get(name);
392
- }
393
- }
394
- // [LAW:one-source-of-truth] Reserved-name detection delegates to Style.parse —
395
- // if a name is a valid built-in style, it cannot be hijacked by a plugin.
396
- // There is no second list of "reserved names" to drift out of sync.
397
- function isReservedTagName(name) {
398
- try {
399
- Style.parse(name);
400
- return true;
401
- }
402
- catch (err) {
403
- if (err instanceof StyleSyntaxError)
404
- return false;
405
- throw err;
406
- }
407
- }
408
- // [LAW:no-shared-mutable-globals] The process-wide default tag set, which
409
- // `renderMarkup` falls back to when a caller passes no registry. It has one
410
- // owner and one explicit API — `MarkupRegistry`'s own methods. There is
411
- // deliberately no free-function façade over it: `registerMarkupTag(name, fn)`
412
- // forwarding to `.register(name, fn)` would be a second way to say one thing,
413
- // and the two would document the same invariants in two places.
414
- export const globalMarkupRegistry = new MarkupRegistry();
415
- // --- Plugin-aware tag parsing ---
416
- function isAlpha(c) {
417
- const cc = c.charCodeAt(0);
418
- return (cc >= 65 && cc <= 90) || (cc >= 97 && cc <= 122);
419
- }
420
- function isIdentChar(c) {
421
- if (isAlpha(c))
422
- return true;
423
- const cc = c.charCodeAt(0);
424
- return (cc >= 48 && cc <= 57) || c === "_" || c === "-";
425
- }
426
- function isSpace(c) {
427
- return c === " " || c === "\t";
428
- }
429
- // A registered name addresses a plugin only when the tag text ends there or
430
- // continues with the whitespace that introduces attributes. Anything else —
431
- // `[table.header]`, `[table=x]` — is the built-in dialect's, and falls through
432
- // exactly as an unregistered name would.
433
- const PLUGIN_TAG_HEAD = new RegExp(`^(${PLUGIN_TAG_NAME_SRC})(?=$|[ \\t])`);
434
- function parsePluginAttrs(after) {
435
- // Hand-written tokenizer. Walks `key=value` pairs separated by whitespace;
436
- // values may be bare, single-quoted, or double-quoted. Matches the syntax
437
- // the regex it replaces accepted.
438
- const attrs = {};
439
- let i = 0;
440
- while (i < after.length) {
441
- while (i < after.length && isSpace(after[i]))
442
- i++;
443
- if (i >= after.length)
444
- break;
445
- if (!isAlpha(after[i])) {
446
- // Skip a stray non-name character to avoid infinite loops on malformed
447
- // input — the legacy regex would simply fail to match here.
448
- i++;
449
- continue;
450
- }
451
- const nameStart = i;
452
- while (i < after.length && isIdentChar(after[i]))
453
- i++;
454
- const name = after.slice(nameStart, i);
455
- while (i < after.length && isSpace(after[i]))
456
- i++;
457
- if (after[i] !== "=")
458
- continue;
459
- i++;
460
- while (i < after.length && isSpace(after[i]))
461
- i++;
462
- let value = "";
463
- const quote = after[i];
464
- if (quote === '"' || quote === "'") {
465
- i++;
466
- const valStart = i;
467
- while (i < after.length && after[i] !== quote)
468
- i++;
469
- value = after.slice(valStart, i);
470
- if (i < after.length)
471
- i++;
472
- }
473
- else {
474
- const valStart = i;
475
- while (i < after.length && !isSpace(after[i]) && after[i] !== "]")
476
- i++;
477
- value = after.slice(valStart, i);
478
- }
479
- attrs[name] = value;
480
- }
481
- return attrs;
482
- }
483
- /**
484
- * Plugin-aware markup render. Always returns a `RichText`. Built-in style
485
- * tags become spans; registered plugin tags are resolved by their handler,
486
- * whose returned `RichText` is appended (text + spans) into the output. The
487
- * recursion is over markup *slices* — between top-level plugin tag pairs and
488
- * inside each pair — and every recursion node returns a `RichText`, so the
489
- * append path is uniform.
490
- */
491
- export function renderMarkup(markup, options) {
492
- return renderSlice(markup, wholeString(markup), options);
493
- }
494
- function renderSlice(markup, origin, options) {
495
- const registry = options?.registry ?? globalMarkupRegistry;
496
- const baseStyle = options?.baseStyle;
497
- const doEmoji = options?.emoji !== false;
498
- // Fast path: no `[` at all → no possible tags.
499
- if (!HAS_TAG_RE.test(markup)) {
500
- return render(markup, origin, baseStyle, { emoji: doEmoji });
501
- }
502
- const tags = parseTags(markup);
503
- // Pair each opening plugin tag with its matching closer up-front, so the
504
- // splice walk can just iterate top-level pairs in source order with no
505
- // nested-state book-keeping.
506
- const { annotated, topLevel: tagPairs } = pairPluginTags(tags, registry, origin);
507
- if (tagPairs.size === 0) {
508
- return render(markup, origin, baseStyle, { emoji: doEmoji });
509
- }
510
- // [LAW:one-type-per-behavior] Always assemble a single RichText. Fragments
511
- // between plugin pairs are parsed by the built-in `render`, plugin pairs
512
- // hand their inner slice (recursively) to a handler; both produce RichText,
513
- // both get appended into one accumulator.
514
- const out = new RichText("");
515
- let cursor = 0;
516
- for (const [openIdx, closeIdx] of tagPairs) {
517
- const open = annotated[openIdx];
518
- const close = annotated[closeIdx];
519
- if (open.start > cursor) {
520
- out.append(render(markup.slice(cursor, open.start), offsetBy(origin, cursor), baseStyle, { emoji: doEmoji }));
521
- }
522
- const innerRaw = markup.slice(open.end, close.start);
523
- const innerRichText = renderSlice(innerRaw, { ...offsetBy(origin, open.end), enclosing: [...origin.enclosing, open.fullMatch] }, options);
524
- const handler = registry.get(open.pluginName);
525
- out.append(handler({ attrs: open.attrs, children: innerRichText, raw: innerRaw }));
526
- cursor = close.end;
527
- }
528
- if (cursor < markup.length) {
529
- out.append(render(markup.slice(cursor), offsetBy(origin, cursor), baseStyle, { emoji: doEmoji }));
530
- }
531
- if (baseStyle)
532
- out.stylize(baseStyle);
533
- return out;
534
- }
535
- /** The origin of a sub-slice starting `offset` into the slice `origin` describes. */
536
- function offsetBy(origin, offset) {
537
- return { ...origin, base: origin.base + offset };
538
- }
539
- function pairPluginTags(tags, registry, origin) {
540
- // Annotate tags with plugin info, then pair openers with closers. Only
541
- // top-level pairs are returned; inner pairs will be re-discovered by the
542
- // recursive `renderMarkup` call on the inner slice.
543
- const annotated = tags.map((t) => annotatePluginTag(t, registry));
544
- const stack = [];
545
- const pairs = new Map();
546
- for (let i = 0; i < annotated.length; i++) {
547
- const t = annotated[i];
548
- if (!t.pluginName)
549
- continue;
550
- if (t.isImplicitClose)
551
- continue;
552
- if (t.isClosing) {
553
- // Find matching open in stack.
554
- for (let j = stack.length - 1; j >= 0; j--) {
555
- const openIdx = stack[j];
556
- if (annotated[openIdx].pluginName === t.pluginName) {
557
- pairs.set(openIdx, i);
558
- stack.splice(j, 1);
559
- break;
560
- }
561
- }
562
- }
563
- else {
564
- stack.push(i);
565
- }
566
- }
567
- // Filter pairs to top-level only. A pair that opens inside the current one is
568
- // either contained — re-discovered when the recursion renders the outer
569
- // pair's inner slice — or overlapping, and only its *closing* position tells
570
- // the two apart. Testing the open position alone conflated them and dropped
571
- // the overlapping pair, which orphaned its closing tag into the trailing
572
- // slice, where the built-in parser blamed the wrong tag for the wrong reason.
573
- const topLevel = new Map();
574
- let outerEnd = -1;
575
- let outerOpenIdx = -1;
576
- const sortedOpens = [...pairs.keys()].sort((a, b) => a - b);
577
- for (const openIdx of sortedOpens) {
578
- const closeIdx = pairs.get(openIdx);
579
- if (annotated[openIdx].start < outerEnd) {
580
- // [LAW:no-silent-failure] Overlap is unrepresentable here rather than
581
- // unimplemented: a handler receives `children` as one contiguous slice,
582
- // so a region straddling another pair's closing boundary has nothing to
583
- // hand it. A style span may overlap because it annotates; a plugin pair
584
- // may not because it replaces.
585
- if (annotated[closeIdx].end > outerEnd) {
586
- const inner = annotated[openIdx];
587
- const outer = annotated[outerOpenIdx];
588
- // The caret goes under the outer pair's closing tag, the point where
589
- // nesting breaks. This pass sees plugin tags only; of those, a pair is
590
- // open there when it opens before that tag and closes at or after it.
591
- const caret = annotated[pairs.get(outerOpenIdx)].start;
592
- const openAtCaret = sortedOpens
593
- .filter((o) => annotated[o].start < caret && annotated[pairs.get(o)].start >= caret)
594
- .map((o) => annotated[o].fullMatch);
595
- throw new MarkupSyntaxError(`Plugin tag [${inner.pluginName}] overlaps [${outer.pluginName}]: plugin tags must nest, ` +
596
- `because a handler receives one contiguous slice. ` +
597
- `Close [/${inner.pluginName}] before [/${outer.pluginName}].`, origin.source, origin.base + caret, [...origin.enclosing, ...openAtCaret]);
598
- }
599
- continue;
600
- }
601
- topLevel.set(openIdx, closeIdx);
602
- outerOpenIdx = openIdx;
603
- outerEnd = annotated[closeIdx].end;
604
- }
605
- return { annotated, topLevel };
606
- }
607
- function annotatePluginTag(tag, registry) {
608
- // The legacy parser splits at the first `=`, so tag.styleName for
609
- // `[click verb=foo]` is "click verb". Re-read the tag text for a name that
610
- // actually addresses a plugin.
611
- const inner = tag.fullMatch.slice(1, -1).replace(/^\//, "");
612
- const head = PLUGIN_TAG_HEAD.exec(inner);
613
- const name = head?.[1];
614
- if (name === undefined || !registry.has(name))
615
- return tag;
616
- const after = inner.slice(name.length);
617
- const attrs = tag.isClosing ? {} : parsePluginAttrs(after);
618
- return { ...tag, pluginName: name, attrs };
619
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Measurement — min/max cell width calculation for renderables.
3
- */
4
- import type { Measurable, RenderOptions } from "./protocol.js";
5
- export declare class Measurement {
6
- readonly minimum: number;
7
- readonly maximum: number;
8
- constructor(minimum: number, maximum: number);
9
- get span(): number;
10
- /**
11
- * A range with no cell meaning becomes one with the nearest meaning there is:
12
- * negative floors to zero, an inverted pair collapses to its ceiling, and NaN
13
- * reads as zero cells the same way `cellCount` reads it.
14
- *
15
- * NaN and Infinity are not treated alike here, and the difference is the whole
16
- * point. `Infinity` is a maximum a renderable means — `Table` reports it when
17
- * a column asks for every cell there is, and `withBoundedWidth` throws on it
18
- * so a caller learns their offer was unanswerable. Flooring it to zero would
19
- * turn that loud failure into a table measured at no width at all.
20
- */
21
- normalize(): Measurement;
22
- withMaximum(width: number): Measurement;
23
- withMinimum(width: number): Measurement;
24
- clamp(minWidth: number, maxWidth: number): Measurement;
25
- static get(options: RenderOptions, measurable: Measurable): Measurement;
26
- }
27
- export declare function measureRenderables(options: RenderOptions, measurables: Measurable[]): Measurement;