@promptctl/rich-js 0.18.0 → 0.18.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/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +143 -0
- package/dist/core/box.js +379 -0
- package/dist/core/cells.d.ts +159 -0
- package/dist/core/cells.js +287 -0
- package/dist/core/color.d.ts +246 -0
- package/dist/core/color.js +1038 -0
- package/dist/core/console.d.ts +165 -0
- package/dist/core/console.js +527 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +200 -0
- package/dist/core/export-lines.d.ts +121 -0
- package/dist/core/export-lines.js +141 -0
- package/dist/core/highlighter.d.ts +33 -0
- package/dist/core/highlighter.js +124 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +119 -0
- package/dist/core/markup.js +731 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +62 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +90 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +144 -0
- package/dist/core/pretty.js +476 -0
- package/dist/core/protocol.d.ts +267 -0
- package/dist/core/protocol.js +163 -0
- package/dist/core/render.d.ts +84 -0
- package/dist/core/render.js +145 -0
- package/dist/core/segment.d.ts +161 -0
- package/dist/core/segment.js +492 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +141 -0
- package/dist/core/strip.js +305 -0
- package/dist/core/style.d.ts +169 -0
- package/dist/core/style.js +695 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +256 -0
- package/dist/core/text.js +976 -0
- package/dist/core/wrap.d.ts +41 -0
- package/dist/core/wrap.js +127 -0
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +179 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +107 -0
- package/dist/node/prompt.d.ts +21 -0
- package/dist/node/prompt.js +30 -0
- package/dist/node/save.d.ts +17 -0
- package/dist/node/save.js +21 -0
- package/dist/node/terminal-host.d.ts +113 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +37 -0
- package/dist/node/traceback.js +134 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +33 -0
- package/dist/renderables/columns.d.ts +61 -0
- package/dist/renderables/columns.js +152 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +37 -0
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +11 -0
- package/dist/renderables/group.js +17 -0
- package/dist/renderables/layout.d.ts +92 -0
- package/dist/renderables/layout.js +312 -0
- package/dist/renderables/live.d.ts +50 -0
- package/dist/renderables/live.js +175 -0
- package/dist/renderables/markdown.d.ts +24 -0
- package/dist/renderables/markdown.js +184 -0
- package/dist/renderables/padding.d.ts +38 -0
- package/dist/renderables/padding.js +141 -0
- package/dist/renderables/panel.d.ts +116 -0
- package/dist/renderables/panel.js +307 -0
- package/dist/renderables/progress.d.ts +90 -0
- package/dist/renderables/progress.js +238 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +53 -0
- package/dist/renderables/prompt.d.ts +49 -0
- package/dist/renderables/prompt.js +125 -0
- package/dist/renderables/rule.d.ts +25 -0
- package/dist/renderables/rule.js +90 -0
- package/dist/renderables/spinner.d.ts +31 -0
- package/dist/renderables/spinner.js +70 -0
- package/dist/renderables/status.d.ts +23 -0
- package/dist/renderables/status.js +60 -0
- package/dist/renderables/syntax.d.ts +35 -0
- package/dist/renderables/syntax.js +115 -0
- package/dist/renderables/table.d.ts +194 -0
- package/dist/renderables/table.js +795 -0
- package/dist/renderables/traceback.d.ts +31 -0
- package/dist/renderables/traceback.js +108 -0
- package/dist/renderables/tree.d.ts +68 -0
- package/dist/renderables/tree.js +150 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.d.ts +87 -0
- package/dist/template-bindings/color-funcs.js +217 -0
- package/dist/template-bindings/helpers.d.ts +24 -0
- package/dist/template-bindings/helpers.js +48 -0
- package/dist/template-bindings/index.d.ts +107 -0
- package/dist/template-bindings/index.js +146 -0
- package/dist/template-bindings/palette-funcs.d.ts +94 -0
- package/dist/template-bindings/palette-funcs.js +141 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +155 -0
- package/dist/themes/buildPalette.d.ts +36 -0
- package/dist/themes/buildPalette.js +56 -0
- package/dist/themes/colorMath.d.ts +82 -0
- package/dist/themes/colorMath.js +254 -0
- package/dist/themes/colorRef.d.ts +51 -0
- package/dist/themes/colorRef.js +96 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +21 -0
- package/dist/themes/palette.js +28 -0
- package/dist/themes/ramp.d.ts +83 -0
- package/dist/themes/ramp.js +122 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +94 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +49 -0
- package/dist/widgets/button.js +208 -0
- package/dist/widgets/checkbox.d.ts +41 -0
- package/dist/widgets/checkbox.js +157 -0
- package/dist/widgets/dropdown.d.ts +67 -0
- package/dist/widgets/dropdown.js +450 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/slider.d.ts +57 -0
- package/dist/widgets/slider.js +270 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +274 -0
- package/dist/widgets/text-input.js +1165 -0
- package/dist/widgets/toggle.d.ts +44 -0
- package/dist/widgets/toggle.js +180 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +40 -0
- package/dist/widgets/widget-base.js +165 -0
- package/package.json +6 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color *values* for the rich-js template binding.
|
|
3
|
+
*
|
|
4
|
+
* ### Why colors are values
|
|
5
|
+
*
|
|
6
|
+
* Every other function in this binding layer takes a child fragment and
|
|
7
|
+
* returns a styled fragment. These do not: they take colors and return a
|
|
8
|
+
* color. That is the whole point. Before this module existed, a color could
|
|
9
|
+
* not survive between two operations — every color function resolved and
|
|
10
|
+
* applied in one breath — so composing two color operations had to be spelled
|
|
11
|
+
* as a *string grammar* (`"primary-darken-3 50%"`), a second language
|
|
12
|
+
* reimplementing function application inside a string literal. Every new color
|
|
13
|
+
* operation meant a new grammar production. [LAW:no-mode-explosion]
|
|
14
|
+
*
|
|
15
|
+
* With colors as values the surface is one shape that grows by nesting:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* {{ fg (color "primary") .branch }}
|
|
19
|
+
* {{ fg (darken (color "primary") 2) .branch }}
|
|
20
|
+
* {{ fg (mix (color "foreground") (color "surface") 65) .label }}
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The simple case is a literal prefix of the composed case — an author adding
|
|
24
|
+
* a `darken` to working code wraps it, rather than rewriting it into an
|
|
25
|
+
* unfamiliar form. [LAW:composability]
|
|
26
|
+
*
|
|
27
|
+
* ### The carrier is a hex string, deliberately
|
|
28
|
+
*
|
|
29
|
+
* A color crosses the template seam as `#RRGGBB`, not as an opaque object,
|
|
30
|
+
* for three reasons:
|
|
31
|
+
*
|
|
32
|
+
* 1. **The type separates.** `"string"` is the strictest arg slot the template
|
|
33
|
+
* engine has — it *refuses* typed `T`. So a color slot cannot silently
|
|
34
|
+
* accept a text fragment. An opaque object would land in `"T"`/`"liftable"`,
|
|
35
|
+
* both of which admit any non-primitive, and `{{ bold (color "primary") }}`
|
|
36
|
+
* would type-check its way into nonsense. [LAW:types-are-the-program]
|
|
37
|
+
* 2. **It flows through the language.** `{{ $muted := mix … }}` holds it,
|
|
38
|
+
* `printf` prints it, `eq` compares it — no engine changes required. Naming
|
|
39
|
+
* a color once and using it ten times is the ergonomic payoff.
|
|
40
|
+
* 3. **Misuse is visible.** A color that lands in text position renders as
|
|
41
|
+
* `#7aa2f7` — wrong in a way an author immediately sees, never a silently
|
|
42
|
+
* dropped style. [LAW:no-silent-failure]
|
|
43
|
+
*
|
|
44
|
+
* No precision is given up: `darken`, `blendRgb`, and `contrastFor` already
|
|
45
|
+
* take and return 8-bit `ColorRgba`. The one caveat is OKLCH: each of the
|
|
46
|
+
* axis functions below round-trips sRGB→OKLCH→sRGB, so chaining three or more
|
|
47
|
+
* of them quantizes visibly. Compose two, or reach for `transposePalette` when
|
|
48
|
+
* a whole palette needs adapting — the same caveat that already applies to
|
|
49
|
+
* chained transpositions.
|
|
50
|
+
*
|
|
51
|
+
* ### Palette-free on purpose
|
|
52
|
+
*
|
|
53
|
+
* Nothing here knows what a palette is. `color` and `ramp` (the two
|
|
54
|
+
* palette-dependent functions, in `palette-funcs.ts`) turn names into hex;
|
|
55
|
+
* from that point on the math is pure. A consumer with no theme system at all still gets
|
|
56
|
+
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
|
+
*/
|
|
58
|
+
import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
|
|
59
|
+
import { ColorDepth } from "../core/color.js";
|
|
60
|
+
/**
|
|
61
|
+
* The `readableOn` binding, measuring at the depth `drawnAt` names when it is
|
|
62
|
+
* evaluated — a consumer that renders at a per-call depth registers its own.
|
|
63
|
+
*/
|
|
64
|
+
export declare const readableOnFunc: (drawnAt: () => ColorDepth) => TemplateFunc;
|
|
65
|
+
/**
|
|
66
|
+
* The palette-free color vocabulary: every function takes colors and returns a
|
|
67
|
+
* color, so they compose by nesting.
|
|
68
|
+
*
|
|
69
|
+
* | function | meaning |
|
|
70
|
+
* |---|---|
|
|
71
|
+
* | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
|
|
72
|
+
* | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
|
|
73
|
+
* | `contrastOn bg` | black or white, whichever is readable on `bg` |
|
|
74
|
+
* | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
|
|
75
|
+
* | `shiftHue c deg` | rotate hue in OKLCH |
|
|
76
|
+
* | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
|
|
77
|
+
* | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
|
|
78
|
+
* | `shiftLightness c d` | add to lightness, after any scale |
|
|
79
|
+
*
|
|
80
|
+
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
81
|
+
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
82
|
+
*
|
|
83
|
+
* @param drawnAt the depth the terminal will draw at, read on every
|
|
84
|
+
* `readableOn` evaluation so a host that learns its depth per render passes
|
|
85
|
+
* one getter; defaults to truecolor.
|
|
86
|
+
*/
|
|
87
|
+
export declare function colorFuncs(drawnAt?: () => ColorDepth): FuncMap;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color *values* for the rich-js template binding.
|
|
3
|
+
*
|
|
4
|
+
* ### Why colors are values
|
|
5
|
+
*
|
|
6
|
+
* Every other function in this binding layer takes a child fragment and
|
|
7
|
+
* returns a styled fragment. These do not: they take colors and return a
|
|
8
|
+
* color. That is the whole point. Before this module existed, a color could
|
|
9
|
+
* not survive between two operations — every color function resolved and
|
|
10
|
+
* applied in one breath — so composing two color operations had to be spelled
|
|
11
|
+
* as a *string grammar* (`"primary-darken-3 50%"`), a second language
|
|
12
|
+
* reimplementing function application inside a string literal. Every new color
|
|
13
|
+
* operation meant a new grammar production. [LAW:no-mode-explosion]
|
|
14
|
+
*
|
|
15
|
+
* With colors as values the surface is one shape that grows by nesting:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* {{ fg (color "primary") .branch }}
|
|
19
|
+
* {{ fg (darken (color "primary") 2) .branch }}
|
|
20
|
+
* {{ fg (mix (color "foreground") (color "surface") 65) .label }}
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The simple case is a literal prefix of the composed case — an author adding
|
|
24
|
+
* a `darken` to working code wraps it, rather than rewriting it into an
|
|
25
|
+
* unfamiliar form. [LAW:composability]
|
|
26
|
+
*
|
|
27
|
+
* ### The carrier is a hex string, deliberately
|
|
28
|
+
*
|
|
29
|
+
* A color crosses the template seam as `#RRGGBB`, not as an opaque object,
|
|
30
|
+
* for three reasons:
|
|
31
|
+
*
|
|
32
|
+
* 1. **The type separates.** `"string"` is the strictest arg slot the template
|
|
33
|
+
* engine has — it *refuses* typed `T`. So a color slot cannot silently
|
|
34
|
+
* accept a text fragment. An opaque object would land in `"T"`/`"liftable"`,
|
|
35
|
+
* both of which admit any non-primitive, and `{{ bold (color "primary") }}`
|
|
36
|
+
* would type-check its way into nonsense. [LAW:types-are-the-program]
|
|
37
|
+
* 2. **It flows through the language.** `{{ $muted := mix … }}` holds it,
|
|
38
|
+
* `printf` prints it, `eq` compares it — no engine changes required. Naming
|
|
39
|
+
* a color once and using it ten times is the ergonomic payoff.
|
|
40
|
+
* 3. **Misuse is visible.** A color that lands in text position renders as
|
|
41
|
+
* `#7aa2f7` — wrong in a way an author immediately sees, never a silently
|
|
42
|
+
* dropped style. [LAW:no-silent-failure]
|
|
43
|
+
*
|
|
44
|
+
* No precision is given up: `darken`, `blendRgb`, and `contrastFor` already
|
|
45
|
+
* take and return 8-bit `ColorRgba`. The one caveat is OKLCH: each of the
|
|
46
|
+
* axis functions below round-trips sRGB→OKLCH→sRGB, so chaining three or more
|
|
47
|
+
* of them quantizes visibly. Compose two, or reach for `transposePalette` when
|
|
48
|
+
* a whole palette needs adapting — the same caveat that already applies to
|
|
49
|
+
* chained transpositions.
|
|
50
|
+
*
|
|
51
|
+
* ### Palette-free on purpose
|
|
52
|
+
*
|
|
53
|
+
* Nothing here knows what a palette is. `color` and `ramp` (the two
|
|
54
|
+
* palette-dependent functions, in `palette-funcs.ts`) turn names into hex;
|
|
55
|
+
* from that point on the math is pure. A consumer with no theme system at all still gets
|
|
56
|
+
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
|
+
*/
|
|
58
|
+
import { blendRgb, ColorDepth } from "../core/color.js";
|
|
59
|
+
import { Oklch, IDENTITY } from "../core/oklch.js";
|
|
60
|
+
import { HEX_COLOR_RE, parseHexColor } from "../themes/colorRef.js";
|
|
61
|
+
import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
|
|
62
|
+
/**
|
|
63
|
+
* The gate every color argument crosses. [LAW:parse-dont-validate] — the
|
|
64
|
+
* function bodies below receive `ColorRgba` and never re-check.
|
|
65
|
+
*
|
|
66
|
+
* Parsing itself is `parseHexColor`'s job; this wrapper exists only to replace
|
|
67
|
+
* its error *wording*, because the mistake it catches is almost always the
|
|
68
|
+
* same one — a palette name passed where a color belongs (`darken "primary" 2`)
|
|
69
|
+
* — and an author who reads "wrap it: darken (color "primary")" is corrected in
|
|
70
|
+
* one step, where "expected #RRGGBB" leaves them guessing.
|
|
71
|
+
* [LAW:no-silent-failure]
|
|
72
|
+
*/
|
|
73
|
+
function asColor(value, func) {
|
|
74
|
+
if (!HEX_COLOR_RE.test(value.trim())) {
|
|
75
|
+
throw new TypeError(`${func} expected a color (#RRGGBB or #RRGGBBAA), got ${JSON.stringify(value)}` +
|
|
76
|
+
` — to use a palette name here, wrap it: ${func} (color ${JSON.stringify(value)}) …`);
|
|
77
|
+
}
|
|
78
|
+
return parseHexColor(value);
|
|
79
|
+
}
|
|
80
|
+
// [LAW:single-enforcer] Every colour function is fixed-arity, so the engine's
|
|
81
|
+
// arity gate refuses a missing or surplus argument before the body runs.
|
|
82
|
+
function colorFunc(argTypes, fn) {
|
|
83
|
+
return { fn, argTypes, arity: { kind: "exact" }, returnType: "string" };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The gate every *numeric* argument crosses, with an optional inclusive range.
|
|
87
|
+
*
|
|
88
|
+
* [LAW:single-enforcer] One check for every `"float"` slot. An `"int"` slot is
|
|
89
|
+
* a safe integer by the engine's gate and needs none, but a `"float"` slot admits
|
|
90
|
+
* NaN and Infinity (legitimate IEEE-754 values), which would otherwise surface
|
|
91
|
+
* as `ColorRgba.red must be an integer in [0, 255]; got NaN` — loud, but
|
|
92
|
+
* pointing at a channel the author never wrote. [LAW:no-silent-failure]
|
|
93
|
+
*/
|
|
94
|
+
function asAmount(value, func, param, range) {
|
|
95
|
+
const inRange = range === undefined || (value >= range.min && value <= range.max);
|
|
96
|
+
if (!Number.isFinite(value) || !inRange) {
|
|
97
|
+
const bound = range === undefined
|
|
98
|
+
? "a finite number"
|
|
99
|
+
: `within ${range.min}..${range.max} (${range.note})`;
|
|
100
|
+
throw new RangeError(`${func}'s ${param} must be ${bound}, got ${value}`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
// --- HSL lightness ---
|
|
105
|
+
//
|
|
106
|
+
// `lighten` is `darken` with the sign flipped — the same operation, not a
|
|
107
|
+
// second one (colorMath's `lighten` is literally `darken(c, -levels)`). Both
|
|
108
|
+
// names ship because "lighten by 2" and "darken by -2" are not equally
|
|
109
|
+
// readable at a call site, and the operation is genuinely bidirectional.
|
|
110
|
+
const darkenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "darken"), levels).hex));
|
|
111
|
+
const lightenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "lighten"), -levels).hex));
|
|
112
|
+
// --- Blending ---
|
|
113
|
+
//
|
|
114
|
+
// [LAW:one-type-per-behavior] There is no separate `alphaBlend` binding.
|
|
115
|
+
// `alphaBlend(fg, bg, a)` in colorMath is *defined* as `blendRgb(bg, fg, a)` —
|
|
116
|
+
// one operation wearing two names. Compositing a translucent color over a
|
|
117
|
+
// background and mixing two colors by a percentage are the same arithmetic,
|
|
118
|
+
// so they get one function. Terminals have no alpha channel, which is also why
|
|
119
|
+
// no function here *produces* a translucent color: it could not be rendered,
|
|
120
|
+
// and a color that silently does nothing in the sink is worse than no
|
|
121
|
+
// function at all.
|
|
122
|
+
const MIX_PCT_MAX = 100;
|
|
123
|
+
const MIX_RANGE = { min: 0, max: MIX_PCT_MAX, note: "0 = from, 100 = toward" };
|
|
124
|
+
const mixFunc = colorFunc(["string", "string", "float"], ((fromHex, towardHex, pct) => blendRgb(asColor(fromHex, "mix"), asColor(towardHex, "mix"), asAmount(pct, "mix", "percentage", MIX_RANGE) / MIX_PCT_MAX).hex));
|
|
125
|
+
// --- Readability ---
|
|
126
|
+
//
|
|
127
|
+
// Two different questions, so two functions rather than one with a flag:
|
|
128
|
+
// `contrastOn` asks "what is maximally readable here" (black or white, WCAG
|
|
129
|
+
// relative-luminance cutoff); `readableOn` asks "keep this color recognizably
|
|
130
|
+
// itself, but guarantee it clears `ratio`" — it slides OKLCH lightness and
|
|
131
|
+
// preserves hue, so a blue on dark blue becomes a lighter blue rather than
|
|
132
|
+
// white.
|
|
133
|
+
//
|
|
134
|
+
// [LAW:no-silent-failure] `ratio` is required, not defaulted to WCAG AA's 4.5.
|
|
135
|
+
// The threshold is the whole decision this function makes, and the right value
|
|
136
|
+
// genuinely differs by intent — 4.5 for body text, 3.0 for large or
|
|
137
|
+
// deliberately recessive text. A hidden default would silently govern an
|
|
138
|
+
// accessibility outcome the author never saw, and would quietly over-correct a
|
|
139
|
+
// caller whose *point* was to be quiet: de-emphasized text floored at 4.5 is no
|
|
140
|
+
// longer de-emphasized.
|
|
141
|
+
// WCAG 2.x contrast ratios live in [1, 21] — 1 is a color on itself.
|
|
142
|
+
const RATIO_RANGE = {
|
|
143
|
+
min: 1,
|
|
144
|
+
max: 21,
|
|
145
|
+
note: "4.5 = AA body text, 3 = AA large text",
|
|
146
|
+
};
|
|
147
|
+
const contrastOnFunc = colorFunc(["string"], ((bgHex) => contrastFor(asColor(bgHex, "contrastOn")).hex));
|
|
148
|
+
/**
|
|
149
|
+
* The `readableOn` binding, measuring at the depth `drawnAt` names when it is
|
|
150
|
+
* evaluated — a consumer that renders at a per-call depth registers its own.
|
|
151
|
+
*/
|
|
152
|
+
export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), drawnAt()).hex));
|
|
153
|
+
// --- OKLCH axes ---
|
|
154
|
+
//
|
|
155
|
+
// [LAW:one-source-of-truth] One function per `ThemeKey` axis, and the table is
|
|
156
|
+
// keyed BY the axis so the compiler proves it total: add a field to `ThemeKey`
|
|
157
|
+
// and this object stops satisfying `Record<keyof ThemeKey, string>` until a
|
|
158
|
+
// binding name is chosen for it. The same derive-registrations-from-the-real-
|
|
159
|
+
// inventory trick `attributeFuncs()` uses over `ATTRIBUTE_NAMES`.
|
|
160
|
+
//
|
|
161
|
+
// Naming follows the axis semantics rather than the field spelling: `shift`
|
|
162
|
+
// for the additive axes, `scale` for the multiplicative ones, so a reader can
|
|
163
|
+
// tell from the call site whether `1` means "no change" or "one unit".
|
|
164
|
+
const OKLCH_AXIS_FUNCS = {
|
|
165
|
+
hueShift: "shiftHue",
|
|
166
|
+
chromaScale: "scaleChroma",
|
|
167
|
+
lightnessScale: "scaleLightness",
|
|
168
|
+
lightnessShift: "shiftLightness",
|
|
169
|
+
};
|
|
170
|
+
function oklchAxisFuncs() {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [axis, funcName] of Object.entries(OKLCH_AXIS_FUNCS)) {
|
|
173
|
+
// [LAW:dataflow-not-control-flow] Every axis takes the identical path —
|
|
174
|
+
// splice one value into the identity key and apply it. The axis is data.
|
|
175
|
+
const key = axis;
|
|
176
|
+
out[funcName] = colorFunc(["string", "float"], ((hex, amount) => {
|
|
177
|
+
const themeKey = {
|
|
178
|
+
...IDENTITY,
|
|
179
|
+
[key]: asAmount(amount, funcName, "amount"),
|
|
180
|
+
};
|
|
181
|
+
return Oklch.fromRgba(asColor(hex, funcName)).applyKey(themeKey).toRgba().hex;
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The palette-free color vocabulary: every function takes colors and returns a
|
|
188
|
+
* color, so they compose by nesting.
|
|
189
|
+
*
|
|
190
|
+
* | function | meaning |
|
|
191
|
+
* |---|---|
|
|
192
|
+
* | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
|
|
193
|
+
* | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
|
|
194
|
+
* | `contrastOn bg` | black or white, whichever is readable on `bg` |
|
|
195
|
+
* | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
|
|
196
|
+
* | `shiftHue c deg` | rotate hue in OKLCH |
|
|
197
|
+
* | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
|
|
198
|
+
* | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
|
|
199
|
+
* | `shiftLightness c d` | add to lightness, after any scale |
|
|
200
|
+
*
|
|
201
|
+
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
202
|
+
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
203
|
+
*
|
|
204
|
+
* @param drawnAt the depth the terminal will draw at, read on every
|
|
205
|
+
* `readableOn` evaluation so a host that learns its depth per render passes
|
|
206
|
+
* one getter; defaults to truecolor.
|
|
207
|
+
*/
|
|
208
|
+
export function colorFuncs(drawnAt = () => ColorDepth.TRUECOLOR) {
|
|
209
|
+
return {
|
|
210
|
+
darken: darkenFunc,
|
|
211
|
+
lighten: lightenFunc,
|
|
212
|
+
mix: mixFunc,
|
|
213
|
+
contrastOn: contrastOnFunc,
|
|
214
|
+
readableOn: readableOnFunc(drawnAt),
|
|
215
|
+
...oklchAxisFuncs(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internal helpers for the rich-js template binding layer.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:one-source-of-truth] `applyStyleToFragment` is the single implementation
|
|
5
|
+
* of "apply a Style to an existing RichText fragment." Both the style-function
|
|
6
|
+
* registrations and the palette-function registrations use this same operation;
|
|
7
|
+
* keeping one copy prevents the two from drifting.
|
|
8
|
+
*/
|
|
9
|
+
import { Style } from "../core/style.js";
|
|
10
|
+
import { RichText } from "../core/text.js";
|
|
11
|
+
/**
|
|
12
|
+
* Apply a style on top of an already-styled RichText fragment.
|
|
13
|
+
*
|
|
14
|
+
* [LAW:single-enforcer] Type validation lives here; callers trust the result.
|
|
15
|
+
* The `"liftable"` arg type lifts string literals to `RichText` via the
|
|
16
|
+
* engine's `fromString` before the body runs, so `child` is always a
|
|
17
|
+
* `RichText` by the time this runs. The instanceof check exists because
|
|
18
|
+
* `"liftable"` admits any non-primitive — the engine cannot prove the object
|
|
19
|
+
* is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
|
|
20
|
+
* rather than producing a malformed fragment.
|
|
21
|
+
*
|
|
22
|
+
* Conflict resolution: `Style.add` — the outer (newly applied) style wins.
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyStyleToFragment(child: unknown, style: Style): RichText;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internal helpers for the rich-js template binding layer.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:one-source-of-truth] `applyStyleToFragment` is the single implementation
|
|
5
|
+
* of "apply a Style to an existing RichText fragment." Both the style-function
|
|
6
|
+
* registrations and the palette-function registrations use this same operation;
|
|
7
|
+
* keeping one copy prevents the two from drifting.
|
|
8
|
+
*/
|
|
9
|
+
import { Style, StyleSyntaxError } from "../core/style.js";
|
|
10
|
+
import { RichText } from "../core/text.js";
|
|
11
|
+
/**
|
|
12
|
+
* Apply a style on top of an already-styled RichText fragment.
|
|
13
|
+
*
|
|
14
|
+
* [LAW:single-enforcer] Type validation lives here; callers trust the result.
|
|
15
|
+
* The `"liftable"` arg type lifts string literals to `RichText` via the
|
|
16
|
+
* engine's `fromString` before the body runs, so `child` is always a
|
|
17
|
+
* `RichText` by the time this runs. The instanceof check exists because
|
|
18
|
+
* `"liftable"` admits any non-primitive — the engine cannot prove the object
|
|
19
|
+
* is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
|
|
20
|
+
* rather than producing a malformed fragment.
|
|
21
|
+
*
|
|
22
|
+
* Conflict resolution: `Style.add` — the outer (newly applied) style wins.
|
|
23
|
+
*/
|
|
24
|
+
export function applyStyleToFragment(child, style) {
|
|
25
|
+
if (!(child instanceof RichText)) {
|
|
26
|
+
throw new TypeError(`template function expected a RichText fragment, got ${typeof child === "object" ? Object.prototype.toString.call(child) : typeof child}`);
|
|
27
|
+
}
|
|
28
|
+
const result = child.copy();
|
|
29
|
+
result.style = baseStyleOf(child.style).add(style);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A fragment's base style as a `Style`. A string must parse as a definition: a
|
|
34
|
+
* name resolves only against the theme of the render that draws it, and a
|
|
35
|
+
* template runs before any render exists.
|
|
36
|
+
*/
|
|
37
|
+
function baseStyleOf(style) {
|
|
38
|
+
if (style instanceof Style)
|
|
39
|
+
return style;
|
|
40
|
+
try {
|
|
41
|
+
return Style.parse(style);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (!(err instanceof StyleSyntaxError))
|
|
45
|
+
throw err;
|
|
46
|
+
throw new TypeError(`template function cannot style a fragment whose base style "${style}" is not a style definition: a style name resolves only against the theme of a render`, { cause: err });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rich-js template bindings — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:one-source-of-truth] This module is the public entry point for
|
|
5
|
+
* rich-js's styling vocabulary as `@promptctl/go-template-js` template
|
|
6
|
+
* functions. The split between the two exported registrations is exactly the
|
|
7
|
+
* split between what needs a theme and what does not:
|
|
8
|
+
*
|
|
9
|
+
* - `richTextFuncs()` — the colour sinks (`fg`/`bg`), the palette-free colour
|
|
10
|
+
* math (`darken`, `mix`, `contrastOn`, the OKLCH axes …), text attributes,
|
|
11
|
+
* and `link`. Needs no configuration; safe to register unconditionally. A
|
|
12
|
+
* consumer with no theme system still gets the complete colour vocabulary by
|
|
13
|
+
* feeding it hex literals.
|
|
14
|
+
* - `paletteFuncs(getPalette)` — `color` and `ramp`, the two functions that
|
|
15
|
+
* resolve a palette name. This is the *only* registration that knows a
|
|
16
|
+
* palette exists, which is why it is the only one that takes an argument.
|
|
17
|
+
* [LAW:one-way-deps]
|
|
18
|
+
*
|
|
19
|
+
* `createRichTextEngine()` wires up `richTextFuncs()` only — it cannot supply a
|
|
20
|
+
* palette. Consumers that name theme colours call `paletteFuncs(getPalette)`
|
|
21
|
+
* and merge it into their own engine config.
|
|
22
|
+
*
|
|
23
|
+
* Colours compose by nesting, like every other function:
|
|
24
|
+
* `{{ fg (darken (color "primary") 2) (bold "x") }}`. There is no second
|
|
25
|
+
* markup grammar and no colour-spec mini-language — the template *is* the
|
|
26
|
+
* composition mechanism.
|
|
27
|
+
*
|
|
28
|
+
* Fragment type: `RichText`. Chosen because it is the library's primary
|
|
29
|
+
* text type (implements `Renderable` + `Measurable`), composes via
|
|
30
|
+
* `append`, and flows directly into the existing render pipeline with no
|
|
31
|
+
* adapter. Top-level template expressions emit as `RichText[]` — natural
|
|
32
|
+
* for cases where a single template produces multiple independently
|
|
33
|
+
* addressable fragments (e.g. multiple top-level `link` calls, each
|
|
34
|
+
* carrying its own URL).
|
|
35
|
+
*/
|
|
36
|
+
import { type Engine, type FuncMap } from "@promptctl/go-template-js";
|
|
37
|
+
import { RichText } from "../core/text.js";
|
|
38
|
+
import { Segment } from "../core/segment.js";
|
|
39
|
+
import { ColorDepth } from "../core/color.js";
|
|
40
|
+
export { paletteFuncs } from "./palette-funcs.js";
|
|
41
|
+
export { colorFuncs, readableOnFunc } from "./color-funcs.js";
|
|
42
|
+
/**
|
|
43
|
+
* Funcs registered by the rich-js binding — the colour sinks, the palette-free
|
|
44
|
+
* colour math, text attributes, and the `link` cell-splitter. Nothing here
|
|
45
|
+
* needs a theme; the two that do (`color`, `ramp`) ship separately via
|
|
46
|
+
* `paletteFuncs(getPalette)`, merged consumer-side. `drawnAt` is forwarded to
|
|
47
|
+
* `colorFuncs` — the depth `readableOn` measures at, truecolor when omitted.
|
|
48
|
+
*
|
|
49
|
+
* `FuncMap` is not parameterised over `T` in `@promptctl/go-template-js` — the engine's
|
|
50
|
+
* `T` lives on the `Engine`/`EngineConfig`, and per-function input/output
|
|
51
|
+
* types are carried as runtime `argTypes` on each `TemplateFunc`. The map
|
|
52
|
+
* therefore *type-checks* against any `EngineConfig<T>.funcs`, but the
|
|
53
|
+
* functions returned here are only *runtime-compatible* with
|
|
54
|
+
* `Engine<RichText>`: each style function returns a `RichText` and
|
|
55
|
+
* requires its lifted child to be a `RichText` (enforced by an
|
|
56
|
+
* `instanceof` check that throws on misuse). Consumers merging this map
|
|
57
|
+
* into a wider engine must keep `T = RichText`; merging into an engine
|
|
58
|
+
* whose `T` is something else will compile but fail at evaluation time.
|
|
59
|
+
*/
|
|
60
|
+
export declare function richTextFuncs(drawnAt?: () => ColorDepth): FuncMap;
|
|
61
|
+
/**
|
|
62
|
+
* Construct an `Engine<RichText>` with the rich-js style-function set
|
|
63
|
+
* registered. Consumers that already manage their own engine should call
|
|
64
|
+
* `richTextFuncs()` and merge the result into their own `createEngine`
|
|
65
|
+
* configuration; consumers that just want rich-js styling can use this
|
|
66
|
+
* factory directly.
|
|
67
|
+
*
|
|
68
|
+
* `fromString` lifts text literals into `RichText`. `toString` flattens a
|
|
69
|
+
* `RichText` to its plain text — the engine's no-silent-flatten guard uses
|
|
70
|
+
* this only for `printf "%s"` / `"%q"` and `print*` slots, where ANSI is
|
|
71
|
+
* not desired anyway.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createRichTextEngine(): Engine<RichText>;
|
|
74
|
+
/**
|
|
75
|
+
* Compile a template source against `engine` and render the result to a
|
|
76
|
+
* flat `Segment[]`. The 90%-case convenience over chaining `engine.compile`,
|
|
77
|
+
* `RichText.fromFragments`, and `.render` by hand:
|
|
78
|
+
*
|
|
79
|
+
* - Runs `engine.compile(source)(scope)` to get the engine's `RichText[]`.
|
|
80
|
+
* - Flattens that fragment list into a single styled `RichText` via
|
|
81
|
+
* `RichText.fromFragments` so every fragment's wrapping style survives.
|
|
82
|
+
* - Renders to a `Segment[]` at the requested `maxWidth`.
|
|
83
|
+
* - Wraps the whole flow in a try/catch — on parse/evaluate failure,
|
|
84
|
+
* emits a single dim styled `[error: <message>]` segment the caller can
|
|
85
|
+
* drop into their layout. No bespoke fallback wiring required at every
|
|
86
|
+
* call site.
|
|
87
|
+
*
|
|
88
|
+
* [LAW:single-enforcer] One place owns "render a template to segments,
|
|
89
|
+
* degrade gracefully on errors" — every consumer that wants this exact
|
|
90
|
+
* shape reads from here rather than re-implementing the same try/catch +
|
|
91
|
+
* error-formatting glue.
|
|
92
|
+
*
|
|
93
|
+
* For the 10% — custom error UX, intermediate access to the `RichText`,
|
|
94
|
+
* pre-compiled templates re-used many times — call the engine directly
|
|
95
|
+
* and use `RichText.fromFragments` to flatten. This helper is sugar for
|
|
96
|
+
* the live-render case (e.g. a preview pane), not a replacement for the
|
|
97
|
+
* compile-once-evaluate-many pattern.
|
|
98
|
+
*
|
|
99
|
+
* @param maxWidth defaults to 400 — large enough that downstream `splitLines`
|
|
100
|
+
* / `adjustLineLength` clipping decides actual width, matching the typical
|
|
101
|
+
* "render wide, fit on output" pipeline.
|
|
102
|
+
* @param errorStyle is a `Style.parse` spec (default `"red dim"`).
|
|
103
|
+
*/
|
|
104
|
+
export declare function renderTemplate(engine: Engine<RichText>, source: string, scope?: unknown, options?: {
|
|
105
|
+
maxWidth?: number;
|
|
106
|
+
errorStyle?: string;
|
|
107
|
+
}): Segment[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rich-js template bindings — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:one-source-of-truth] This module is the public entry point for
|
|
5
|
+
* rich-js's styling vocabulary as `@promptctl/go-template-js` template
|
|
6
|
+
* functions. The split between the two exported registrations is exactly the
|
|
7
|
+
* split between what needs a theme and what does not:
|
|
8
|
+
*
|
|
9
|
+
* - `richTextFuncs()` — the colour sinks (`fg`/`bg`), the palette-free colour
|
|
10
|
+
* math (`darken`, `mix`, `contrastOn`, the OKLCH axes …), text attributes,
|
|
11
|
+
* and `link`. Needs no configuration; safe to register unconditionally. A
|
|
12
|
+
* consumer with no theme system still gets the complete colour vocabulary by
|
|
13
|
+
* feeding it hex literals.
|
|
14
|
+
* - `paletteFuncs(getPalette)` — `color` and `ramp`, the two functions that
|
|
15
|
+
* resolve a palette name. This is the *only* registration that knows a
|
|
16
|
+
* palette exists, which is why it is the only one that takes an argument.
|
|
17
|
+
* [LAW:one-way-deps]
|
|
18
|
+
*
|
|
19
|
+
* `createRichTextEngine()` wires up `richTextFuncs()` only — it cannot supply a
|
|
20
|
+
* palette. Consumers that name theme colours call `paletteFuncs(getPalette)`
|
|
21
|
+
* and merge it into their own engine config.
|
|
22
|
+
*
|
|
23
|
+
* Colours compose by nesting, like every other function:
|
|
24
|
+
* `{{ fg (darken (color "primary") 2) (bold "x") }}`. There is no second
|
|
25
|
+
* markup grammar and no colour-spec mini-language — the template *is* the
|
|
26
|
+
* composition mechanism.
|
|
27
|
+
*
|
|
28
|
+
* Fragment type: `RichText`. Chosen because it is the library's primary
|
|
29
|
+
* text type (implements `Renderable` + `Measurable`), composes via
|
|
30
|
+
* `append`, and flows directly into the existing render pipeline with no
|
|
31
|
+
* adapter. Top-level template expressions emit as `RichText[]` — natural
|
|
32
|
+
* for cases where a single template produces multiple independently
|
|
33
|
+
* addressable fragments (e.g. multiple top-level `link` calls, each
|
|
34
|
+
* carrying its own URL).
|
|
35
|
+
*/
|
|
36
|
+
import { createEngine } from "@promptctl/go-template-js";
|
|
37
|
+
import { RichText } from "../core/text.js";
|
|
38
|
+
import { Style } from "../core/style.js";
|
|
39
|
+
import { Segment } from "../core/segment.js";
|
|
40
|
+
import { richTextStyleFuncs } from "./style-funcs.js";
|
|
41
|
+
import { colorFuncs } from "./color-funcs.js";
|
|
42
|
+
export { paletteFuncs } from "./palette-funcs.js";
|
|
43
|
+
export { colorFuncs, readableOnFunc } from "./color-funcs.js";
|
|
44
|
+
/**
|
|
45
|
+
* Funcs registered by the rich-js binding — the colour sinks, the palette-free
|
|
46
|
+
* colour math, text attributes, and the `link` cell-splitter. Nothing here
|
|
47
|
+
* needs a theme; the two that do (`color`, `ramp`) ship separately via
|
|
48
|
+
* `paletteFuncs(getPalette)`, merged consumer-side. `drawnAt` is forwarded to
|
|
49
|
+
* `colorFuncs` — the depth `readableOn` measures at, truecolor when omitted.
|
|
50
|
+
*
|
|
51
|
+
* `FuncMap` is not parameterised over `T` in `@promptctl/go-template-js` — the engine's
|
|
52
|
+
* `T` lives on the `Engine`/`EngineConfig`, and per-function input/output
|
|
53
|
+
* types are carried as runtime `argTypes` on each `TemplateFunc`. The map
|
|
54
|
+
* therefore *type-checks* against any `EngineConfig<T>.funcs`, but the
|
|
55
|
+
* functions returned here are only *runtime-compatible* with
|
|
56
|
+
* `Engine<RichText>`: each style function returns a `RichText` and
|
|
57
|
+
* requires its lifted child to be a `RichText` (enforced by an
|
|
58
|
+
* `instanceof` check that throws on misuse). Consumers merging this map
|
|
59
|
+
* into a wider engine must keep `T = RichText`; merging into an engine
|
|
60
|
+
* whose `T` is something else will compile but fail at evaluation time.
|
|
61
|
+
*/
|
|
62
|
+
// [LAW:one-source-of-truth] The truecolor default lives on `colorFuncs` alone;
|
|
63
|
+
// an omitted `drawnAt` is forwarded as omitted.
|
|
64
|
+
export function richTextFuncs(drawnAt) {
|
|
65
|
+
return { ...richTextStyleFuncs(), ...colorFuncs(drawnAt) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Construct an `Engine<RichText>` with the rich-js style-function set
|
|
69
|
+
* registered. Consumers that already manage their own engine should call
|
|
70
|
+
* `richTextFuncs()` and merge the result into their own `createEngine`
|
|
71
|
+
* configuration; consumers that just want rich-js styling can use this
|
|
72
|
+
* factory directly.
|
|
73
|
+
*
|
|
74
|
+
* `fromString` lifts text literals into `RichText`. `toString` flattens a
|
|
75
|
+
* `RichText` to its plain text — the engine's no-silent-flatten guard uses
|
|
76
|
+
* this only for `printf "%s"` / `"%q"` and `print*` slots, where ANSI is
|
|
77
|
+
* not desired anyway.
|
|
78
|
+
*/
|
|
79
|
+
export function createRichTextEngine() {
|
|
80
|
+
return createEngine({
|
|
81
|
+
fromString: (s) => new RichText(s),
|
|
82
|
+
toString: (rt) => rt.plain,
|
|
83
|
+
funcs: richTextFuncs(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compile a template source against `engine` and render the result to a
|
|
88
|
+
* flat `Segment[]`. The 90%-case convenience over chaining `engine.compile`,
|
|
89
|
+
* `RichText.fromFragments`, and `.render` by hand:
|
|
90
|
+
*
|
|
91
|
+
* - Runs `engine.compile(source)(scope)` to get the engine's `RichText[]`.
|
|
92
|
+
* - Flattens that fragment list into a single styled `RichText` via
|
|
93
|
+
* `RichText.fromFragments` so every fragment's wrapping style survives.
|
|
94
|
+
* - Renders to a `Segment[]` at the requested `maxWidth`.
|
|
95
|
+
* - Wraps the whole flow in a try/catch — on parse/evaluate failure,
|
|
96
|
+
* emits a single dim styled `[error: <message>]` segment the caller can
|
|
97
|
+
* drop into their layout. No bespoke fallback wiring required at every
|
|
98
|
+
* call site.
|
|
99
|
+
*
|
|
100
|
+
* [LAW:single-enforcer] One place owns "render a template to segments,
|
|
101
|
+
* degrade gracefully on errors" — every consumer that wants this exact
|
|
102
|
+
* shape reads from here rather than re-implementing the same try/catch +
|
|
103
|
+
* error-formatting glue.
|
|
104
|
+
*
|
|
105
|
+
* For the 10% — custom error UX, intermediate access to the `RichText`,
|
|
106
|
+
* pre-compiled templates re-used many times — call the engine directly
|
|
107
|
+
* and use `RichText.fromFragments` to flatten. This helper is sugar for
|
|
108
|
+
* the live-render case (e.g. a preview pane), not a replacement for the
|
|
109
|
+
* compile-once-evaluate-many pattern.
|
|
110
|
+
*
|
|
111
|
+
* @param maxWidth defaults to 400 — large enough that downstream `splitLines`
|
|
112
|
+
* / `adjustLineLength` clipping decides actual width, matching the typical
|
|
113
|
+
* "render wide, fit on output" pipeline.
|
|
114
|
+
* @param errorStyle is a `Style.parse` spec (default `"red dim"`).
|
|
115
|
+
*/
|
|
116
|
+
export function renderTemplate(engine, source, scope = {}, options) {
|
|
117
|
+
try {
|
|
118
|
+
const frags = engine.compile(source)(scope);
|
|
119
|
+
const rt = RichText.fromFragments(frags);
|
|
120
|
+
return Array.from(rt.render({
|
|
121
|
+
maxWidth: options?.maxWidth ?? 400,
|
|
122
|
+
isTerminal: true,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
// An engine error's `message` is its one-line summary; `String(e)` adds a
|
|
127
|
+
// multi-line code frame, which would break this single segment across lines.
|
|
128
|
+
const summary = e instanceof Error ? e.message : String(e);
|
|
129
|
+
return [new Segment(`[error: ${summary.slice(0, 80)}]`, safeErrorStyle(options?.errorStyle))];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// [LAW:single-enforcer] The error-path Style must not itself throw, or
|
|
133
|
+
// the whole "degrade gracefully" promise of renderTemplate is broken. A
|
|
134
|
+
// bogus user-supplied `errorStyle` spec falls back to a hard-coded safe
|
|
135
|
+
// Style — never propagates the parse failure to the caller.
|
|
136
|
+
const FALLBACK_ERROR_STYLE = new Style({ color: "red", dim: true });
|
|
137
|
+
function safeErrorStyle(spec) {
|
|
138
|
+
if (spec === undefined)
|
|
139
|
+
return FALLBACK_ERROR_STYLE;
|
|
140
|
+
try {
|
|
141
|
+
return Style.parse(spec);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return FALLBACK_ERROR_STYLE;
|
|
145
|
+
}
|
|
146
|
+
}
|