@promptctl/rich-js 0.27.0 → 0.29.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 (185) hide show
  1. package/README.md +7 -6
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +65 -46
  4. package/dist/core/animation.js +91 -67
  5. package/dist/core/ansi.d.ts +19 -12
  6. package/dist/core/ansi.js +114 -36
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +94 -6
  17. package/dist/core/clock.d.ts +1 -1
  18. package/dist/core/color.d.ts +56 -57
  19. package/dist/core/color.js +79 -81
  20. package/dist/core/console.d.ts +24 -7
  21. package/dist/core/console.js +27 -8
  22. package/dist/core/control-codes.d.ts +61 -0
  23. package/dist/core/control-codes.js +157 -0
  24. package/dist/core/easing.d.ts +40 -40
  25. package/dist/core/easing.js +40 -40
  26. package/dist/core/env.d.ts +6 -0
  27. package/dist/core/env.js +9 -1
  28. package/dist/core/export-html.js +5 -4
  29. package/dist/core/export-lines.d.ts +13 -11
  30. package/dist/core/export-lines.js +19 -15
  31. package/dist/core/export-svg.d.ts +3 -3
  32. package/dist/core/export-svg.js +10 -9
  33. package/dist/core/json.js +9 -2
  34. package/dist/core/markup.js +5 -4
  35. package/dist/core/memo.d.ts +1 -1
  36. package/dist/core/memo.js +1 -1
  37. package/dist/core/oklch.d.ts +14 -14
  38. package/dist/core/oklch.js +10 -10
  39. package/dist/core/orient.d.ts +68 -0
  40. package/dist/core/orient.js +224 -0
  41. package/dist/core/osc8.d.ts +12 -5
  42. package/dist/core/osc8.js +14 -7
  43. package/dist/core/paint.d.ts +9 -9
  44. package/dist/core/paint.js +3 -3
  45. package/dist/core/pretty.d.ts +1 -1
  46. package/dist/core/pretty.js +23 -19
  47. package/dist/core/protocol.d.ts +47 -26
  48. package/dist/core/protocol.js +31 -23
  49. package/dist/core/render.d.ts +19 -14
  50. package/dist/core/render.js +30 -17
  51. package/dist/core/seal.d.ts +26 -0
  52. package/dist/core/seal.js +75 -0
  53. package/dist/core/segment.d.ts +12 -4
  54. package/dist/core/segment.js +16 -6
  55. package/dist/core/strip.d.ts +83 -83
  56. package/dist/core/strip.js +70 -70
  57. package/dist/core/style.d.ts +46 -18
  58. package/dist/core/style.js +92 -56
  59. package/dist/core/text-order.d.ts +51 -0
  60. package/dist/core/text-order.js +50 -0
  61. package/dist/core/text.d.ts +12 -2
  62. package/dist/core/text.js +136 -90
  63. package/dist/core/timeline.d.ts +5 -5
  64. package/dist/core/timeline.js +11 -11
  65. package/dist/core/tween.d.ts +6 -6
  66. package/dist/core/tween.js +10 -10
  67. package/dist/core/view.d.ts +5 -5
  68. package/dist/core/view.js +15 -14
  69. package/dist/core/wrap.d.ts +24 -6
  70. package/dist/core/wrap.js +82 -16
  71. package/dist/host/app.d.ts +50 -9
  72. package/dist/host/app.js +120 -15
  73. package/dist/host/host-environment.d.ts +2 -2
  74. package/dist/host/host-environment.js +2 -2
  75. package/dist/host/index.d.ts +3 -2
  76. package/dist/host/index.js +1 -0
  77. package/dist/host/input.d.ts +42 -9
  78. package/dist/host/input.js +91 -7
  79. package/dist/host/terminal-colors.d.ts +18 -0
  80. package/dist/host/terminal-colors.js +46 -0
  81. package/dist/host/terminal-host.d.ts +4 -3
  82. package/dist/host/terminal-host.js +1 -1
  83. package/dist/index.d.ts +17 -16
  84. package/dist/index.js +11 -11
  85. package/dist/node/prompt.d.ts +1 -1
  86. package/dist/node/prompt.js +1 -1
  87. package/dist/node/terminal-host.d.ts +1 -1
  88. package/dist/node/traceback.js +2 -2
  89. package/dist/renderables/change.d.ts +1 -3
  90. package/dist/renderables/change.js +6 -8
  91. package/dist/renderables/effect.d.ts +88 -87
  92. package/dist/renderables/effect.js +124 -114
  93. package/dist/renderables/effects.d.ts +105 -76
  94. package/dist/renderables/effects.js +309 -176
  95. package/dist/renderables/embed.d.ts +1 -1
  96. package/dist/renderables/embed.js +2 -2
  97. package/dist/renderables/flexStrip.d.ts +7 -7
  98. package/dist/renderables/flexStrip.js +15 -15
  99. package/dist/renderables/frames.d.ts +7 -16
  100. package/dist/renderables/frames.js +12 -27
  101. package/dist/renderables/glyph-effect.d.ts +22 -23
  102. package/dist/renderables/glyph-effect.js +43 -41
  103. package/dist/renderables/glyph-effects.d.ts +33 -14
  104. package/dist/renderables/glyph-effects.js +90 -22
  105. package/dist/renderables/html-entities.d.ts +1 -1
  106. package/dist/renderables/html-entities.js +1 -1
  107. package/dist/renderables/lifecycle.d.ts +8 -10
  108. package/dist/renderables/lifecycle.js +17 -18
  109. package/dist/renderables/live.d.ts +2 -2
  110. package/dist/renderables/live.js +7 -7
  111. package/dist/renderables/markdown-parse.js +3 -3
  112. package/dist/renderables/markdown.js +4 -2
  113. package/dist/renderables/panel.js +7 -5
  114. package/dist/renderables/progress.d.ts +1 -1
  115. package/dist/renderables/progress.js +1 -1
  116. package/dist/renderables/progressBar.d.ts +3 -3
  117. package/dist/renderables/progressBar.js +21 -18
  118. package/dist/renderables/prompt.d.ts +1 -1
  119. package/dist/renderables/scroll.d.ts +1 -1
  120. package/dist/renderables/scroll.js +3 -3
  121. package/dist/renderables/spinner.d.ts +4 -4
  122. package/dist/renderables/spinner.js +10 -10
  123. package/dist/renderables/syntax.d.ts +2 -2
  124. package/dist/renderables/syntax.js +9 -4
  125. package/dist/renderables/table.js +5 -4
  126. package/dist/renderables/traceback.d.ts +1 -1
  127. package/dist/renderables/traceback.js +1 -1
  128. package/dist/renderables/tree.js +3 -3
  129. package/dist/template-bindings/animation-funcs.d.ts +60 -29
  130. package/dist/template-bindings/animation-funcs.js +126 -43
  131. package/dist/template-bindings/color-funcs.d.ts +14 -14
  132. package/dist/template-bindings/color-funcs.js +25 -25
  133. package/dist/template-bindings/index.d.ts +16 -14
  134. package/dist/template-bindings/index.js +17 -14
  135. package/dist/template-bindings/palette-funcs.d.ts +2 -2
  136. package/dist/template-bindings/palette-funcs.js +4 -4
  137. package/dist/template-bindings/style-funcs.d.ts +5 -5
  138. package/dist/template-bindings/style-funcs.js +16 -16
  139. package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
  140. package/dist/themes/{transpose.js → adjust.js} +28 -28
  141. package/dist/themes/buildPalette.d.ts +6 -5
  142. package/dist/themes/buildPalette.js +22 -22
  143. package/dist/themes/colorMath.d.ts +16 -16
  144. package/dist/themes/colorMath.js +32 -32
  145. package/dist/themes/data/ansi-tables.d.ts +2 -2
  146. package/dist/themes/data/ansi-tables.js +2 -2
  147. package/dist/themes/data/index.js +1 -4
  148. package/dist/themes/data/textual-ansi.js +0 -143
  149. package/dist/themes/data/types.d.ts +4 -4
  150. package/dist/themes/data/types.js +2 -2
  151. package/dist/themes/palette.d.ts +12 -12
  152. package/dist/themes/palette.js +16 -16
  153. package/dist/themes/ramp.d.ts +17 -17
  154. package/dist/themes/ramp.js +12 -12
  155. package/dist/themes/registry.d.ts +6 -8
  156. package/dist/themes/registry.js +6 -8
  157. package/dist/themes/terminalThemes.d.ts +4 -11
  158. package/dist/themes/terminalThemes.js +31 -47
  159. package/dist/widgets/button.d.ts +3 -3
  160. package/dist/widgets/button.js +8 -8
  161. package/dist/widgets/checkbox.d.ts +2 -2
  162. package/dist/widgets/checkbox.js +4 -4
  163. package/dist/widgets/dropdown.d.ts +3 -3
  164. package/dist/widgets/dropdown.js +7 -5
  165. package/dist/widgets/event-router.d.ts +6 -6
  166. package/dist/widgets/event-router.js +2 -2
  167. package/dist/widgets/focus-manager.d.ts +3 -3
  168. package/dist/widgets/focus-manager.js +3 -3
  169. package/dist/widgets/index.d.ts +2 -2
  170. package/dist/widgets/index.js +1 -1
  171. package/dist/widgets/ink.d.ts +3 -3
  172. package/dist/widgets/ink.js +4 -4
  173. package/dist/widgets/slider.d.ts +2 -2
  174. package/dist/widgets/static-item.d.ts +2 -2
  175. package/dist/widgets/text-input.d.ts +2 -2
  176. package/dist/widgets/text-input.js +5 -5
  177. package/dist/widgets/themed-widget.js +4 -4
  178. package/dist/widgets/toggle.d.ts +4 -4
  179. package/dist/widgets/toggle.js +6 -6
  180. package/dist/widgets/types.d.ts +6 -6
  181. package/dist/widgets/types.js +2 -2
  182. package/dist/widgets/widget-app.d.ts +10 -10
  183. package/dist/widgets/widget-app.js +8 -8
  184. package/dist/widgets/widget-base.d.ts +2 -2
  185. package/package.json +5 -4
@@ -37,7 +37,7 @@ const OUTLINE = {
37
37
  encircle: [FRAME, "border-radius:0.5em"],
38
38
  };
39
39
  /**
40
- * The glyph colour, and the background when one is painted.
40
+ * The glyph color, and the background when one is painted.
41
41
  *
42
42
  * Dim arrives already blended into `foreground`. `opacity` is never written:
43
43
  * it fades a painted background along with the glyph, which is the bug the
@@ -121,13 +121,14 @@ function onGrid(text, draw) {
121
121
  * blinks with the glyph. A wide glyph's box sits between the two: it inherits
122
122
  * the double underline, and the inner span inside it draws the other lines.
123
123
  */
124
- function runHtml({ text, look }) {
124
+ function runHtml({ text, look, inVisualOrder }) {
125
125
  const drawn = look.underline === "double"
126
126
  ? span([...paintCss(look), "text-decoration-line:underline", "text-decoration-style:double", ...BLINK[look.blink]], onGrid(text, (glyph) => span([`color:${look.foreground.hex}`, ...glyphCss(look)], glyph)))
127
127
  : span([...paintCss(look), ...glyphCss(look)], onGrid(text, (glyph) => glyph));
128
+ const ordered = inVisualOrder ? span(["unicode-bidi:bidi-override"], drawn) : drawn;
128
129
  return look.href === null
129
- ? drawn
130
- : `<a href="${escapeAttribute(look.href)}" style="${ANCHOR_CSS}">${drawn}</a>`;
130
+ ? ordered
131
+ : `<a href="${escapeAttribute(look.href)}" style="${ANCHOR_CSS}">${ordered}</a>`;
131
132
  }
132
133
  /**
133
134
  * The CSS a page includes once, wherever fragments appear: what an inline
@@ -3,15 +3,15 @@
3
3
  * lines every exporter draws.
4
4
  *
5
5
  * The HTML and SVG exporters are two encodings of one picture, and this module
6
- * is the picture. What a styled cell *looks like* — which colour its glyph is,
6
+ * is the picture. What a styled cell *looks like* — which color its glyph is,
7
7
  * whether its background is painted, which decorations it carries, where it
8
8
  * links — is decided here, once, and an exporter decides only how to write
9
9
  * that down. Before it existed the decision was made twice, by a CSS method
10
10
  * on `Style` and by a private converter on `Console`, and the two had already
11
- * disagreed: one honoured `underline2`, the other dropped it.
11
+ * disagreed: one honoured `doubleUnderline`, the other dropped it.
12
12
  *
13
13
  * [LAW:types-are-the-program] `ExportLook` has no `reverse`, `dim`, `conceal`
14
- * or default colour, because all four are consumed by `resolveLook` and turned
14
+ * or default color, because all four are consumed by `resolveLook` and turned
15
15
  * into plain RGB. An exporter holding a look cannot forget to swap a reversed
16
16
  * run or to fade a dim one: there is nothing left in its hands to forget. That
17
17
  * is the whole reason the representation is resolved rather than a `Style`
@@ -44,7 +44,7 @@ export type Href = string & {
44
44
  };
45
45
  /**
46
46
  * Where a run's background comes from. `"canvas"` is the theme background
47
- * showing through — nothing painted — and is distinct from a painted colour
47
+ * showing through — nothing painted — and is distinct from a painted color
48
48
  * that happens to equal it, because `on default` and `on #000000` are
49
49
  * different requests even over a black theme.
50
50
  */
@@ -65,6 +65,8 @@ export interface ExportLook {
65
65
  export interface ExportRun {
66
66
  readonly text: string;
67
67
  readonly look: ExportLook;
68
+ /** Its cells are in display order already, so a browser must not reorder them. */
69
+ readonly inVisualOrder: boolean;
68
70
  }
69
71
  /** One terminal row, left to right. An empty array is a blank row. */
70
72
  export type ExportLine = readonly ExportRun[];
@@ -94,25 +96,25 @@ export interface ExportCanvas {
94
96
  * The ground every export is drawn on under `theme`.
95
97
  *
96
98
  * An exporter paints its page or window with this, and a run with no
97
- * background of its own resolves to `"canvas"` rather than a colour, so a run
98
- * showing the canvas and the page around it cannot come out different colours.
99
+ * background of its own resolves to `"canvas"` rather than a color, so a run
100
+ * showing the canvas and the page around it cannot come out different colors.
99
101
  */
100
102
  export declare function exportCanvas(theme?: TerminalTheme): ExportCanvas;
101
103
  /**
102
104
  * A `Style` as it appears on screen under `theme`.
103
105
  *
104
- * The order is the terminal's: the colours are `Style.drawnColors`, the ones
106
+ * The order is the terminal's: the colors are `Style.drawnColors`, the ones
105
107
  * `toSgrCodes` writes, resolved through the theme — `reverse` swaps them,
106
108
  * `dim` fades the glyph toward whatever background it ended up on, and
107
109
  * `conceal` finally paints the glyph in that background. Every step works on
108
- * opaque colour, so no exporter ever draws a translucent one. Text under
110
+ * opaque color, so no exporter ever draws a translucent one. Text under
109
111
  * `conceal` is still present, and still selectable, in both formats.
110
112
  *
111
- * A translucent colour is flattened where the writer flattens it, over
113
+ * A translucent color is flattened where the writer flattens it, over
112
114
  * `SURFACE_BLACK`, not over this export's canvas. The writer's truecolor
113
115
  * bytes are absolute — no terminal theme retints them — so a terminal under
114
- * `theme` shows that colour, and an export that flattened over its own canvas
115
- * would picture a colour no terminal draws.
116
+ * `theme` shows that color, and an export that flattened over its own canvas
117
+ * would picture a color no terminal draws.
116
118
  *
117
119
  * `theme` omitted is `ColorSpec.getTruecolor`'s own fallback — black canvas,
118
120
  * white ink, the standard ANSI table. Choosing it there rather than naming a
@@ -3,15 +3,15 @@
3
3
  * lines every exporter draws.
4
4
  *
5
5
  * The HTML and SVG exporters are two encodings of one picture, and this module
6
- * is the picture. What a styled cell *looks like* — which colour its glyph is,
6
+ * is the picture. What a styled cell *looks like* — which color its glyph is,
7
7
  * whether its background is painted, which decorations it carries, where it
8
8
  * links — is decided here, once, and an exporter decides only how to write
9
9
  * that down. Before it existed the decision was made twice, by a CSS method
10
10
  * on `Style` and by a private converter on `Console`, and the two had already
11
- * disagreed: one honoured `underline2`, the other dropped it.
11
+ * disagreed: one honoured `doubleUnderline`, the other dropped it.
12
12
  *
13
13
  * [LAW:types-are-the-program] `ExportLook` has no `reverse`, `dim`, `conceal`
14
- * or default colour, because all four are consumed by `resolveLook` and turned
14
+ * or default color, because all four are consumed by `resolveLook` and turned
15
15
  * into plain RGB. An exporter holding a look cannot forget to swap a reversed
16
16
  * run or to fade a dim one: there is nothing left in its hands to forget. That
17
17
  * is the whole reason the representation is resolved rather than a `Style`
@@ -31,6 +31,7 @@
31
31
  */
32
32
  import { blendRgb, ColorSpec } from "./color.js";
33
33
  import { NULL_STYLE } from "./style.js";
34
+ import { sealed } from "./seal.js";
34
35
  /**
35
36
  * The URL schemes an export will link.
36
37
  *
@@ -81,8 +82,8 @@ const DEFAULT_COLOR = ColorSpec.default();
81
82
  * The ground every export is drawn on under `theme`.
82
83
  *
83
84
  * An exporter paints its page or window with this, and a run with no
84
- * background of its own resolves to `"canvas"` rather than a colour, so a run
85
- * showing the canvas and the page around it cannot come out different colours.
85
+ * background of its own resolves to `"canvas"` rather than a color, so a run
86
+ * showing the canvas and the page around it cannot come out different colors.
86
87
  */
87
88
  export function exportCanvas(theme) {
88
89
  return {
@@ -93,18 +94,18 @@ export function exportCanvas(theme) {
93
94
  /**
94
95
  * A `Style` as it appears on screen under `theme`.
95
96
  *
96
- * The order is the terminal's: the colours are `Style.drawnColors`, the ones
97
+ * The order is the terminal's: the colors are `Style.drawnColors`, the ones
97
98
  * `toSgrCodes` writes, resolved through the theme — `reverse` swaps them,
98
99
  * `dim` fades the glyph toward whatever background it ended up on, and
99
100
  * `conceal` finally paints the glyph in that background. Every step works on
100
- * opaque colour, so no exporter ever draws a translucent one. Text under
101
+ * opaque color, so no exporter ever draws a translucent one. Text under
101
102
  * `conceal` is still present, and still selectable, in both formats.
102
103
  *
103
- * A translucent colour is flattened where the writer flattens it, over
104
+ * A translucent color is flattened where the writer flattens it, over
104
105
  * `SURFACE_BLACK`, not over this export's canvas. The writer's truecolor
105
106
  * bytes are absolute — no terminal theme retints them — so a terminal under
106
- * `theme` shows that colour, and an export that flattened over its own canvas
107
- * would picture a colour no terminal draws.
107
+ * `theme` shows that color, and an export that flattened over its own canvas
108
+ * would picture a color no terminal draws.
108
109
  *
109
110
  * `theme` omitted is `ColorSpec.getTruecolor`'s own fallback — black canvas,
110
111
  * white ink, the standard ANSI table. Choosing it there rather than naming a
@@ -129,10 +130,10 @@ export function resolveLook(style, theme) {
129
130
  background,
130
131
  bold: style.bold === true,
131
132
  italic: style.italic === true,
132
- underline: style.underline2 ? "double" : style.underline ? "single" : "none",
133
+ underline: style.doubleUnderline ? "double" : style.underline ? "single" : "none",
133
134
  strike: style.strike === true,
134
135
  overline: style.overline === true,
135
- blink: style.blink2 ? "fast" : style.blink ? "slow" : "none",
136
+ blink: style.rapidBlink ? "fast" : style.blink ? "slow" : "none",
136
137
  outline: style.encircle ? "encircle" : style.frame ? "frame" : "none",
137
138
  href: style.link === undefined ? null : parseHref(style.link),
138
139
  };
@@ -146,13 +147,16 @@ export function resolveLook(style, theme) {
146
147
  */
147
148
  export function exportLines(segments, theme) {
148
149
  const rows = [[]];
149
- for (const segment of segments) {
150
- const look = resolveLook(segment.style ?? NULL_STYLE, theme);
150
+ // A browser is a display that reorders and holds the controls at zero width.
151
+ for (const segment of sealed(segments, "logical")) {
152
+ const style = segment.style ?? NULL_STYLE;
153
+ const look = resolveLook(style, theme);
154
+ const inVisualOrder = style.bidi?.order === "visual";
151
155
  segment.text.split("\n").forEach((piece, index) => {
152
156
  if (index > 0)
153
157
  rows.push([]);
154
158
  if (piece.length > 0)
155
- rows[rows.length - 1].push({ text: piece, look });
159
+ rows[rows.length - 1].push({ text: piece, look, inVisualOrder });
156
160
  });
157
161
  }
158
162
  return rows[rows.length - 1].length === 0 ? rows.slice(0, -1) : rows;
@@ -9,11 +9,11 @@
9
9
  *
10
10
  * The geometry is Rich's — a 20px character, a cell 0.61 of that wide, a row
11
11
  * 1.22 of it tall, the same margin and padding — so a screenshot of the same
12
- * program matches the library this ports. The frame's colour is not Rich's:
12
+ * program matches the library this ports. The frame's color is not Rich's:
13
13
  * Rich strokes it white at 35% whatever the theme, which vanishes around a
14
14
  * light canvas. Here it is the theme's ink blended 35% into the canvas — over
15
- * a black canvas the colour Rich draws — and opaque, so the half of the stroke
16
- * lying outside the canvas does not take on the colour of the page around it.
15
+ * a black canvas the color Rich draws — and opaque, so the half of the stroke
16
+ * lying outside the canvas does not take on the color of the page around it.
17
17
  *
18
18
  * Nor is how a glyph lands on its cell. The exporter has no font engine and
19
19
  * does not know which font the viewer will draw with, so no advance width can
@@ -9,11 +9,11 @@
9
9
  *
10
10
  * The geometry is Rich's — a 20px character, a cell 0.61 of that wide, a row
11
11
  * 1.22 of it tall, the same margin and padding — so a screenshot of the same
12
- * program matches the library this ports. The frame's colour is not Rich's:
12
+ * program matches the library this ports. The frame's color is not Rich's:
13
13
  * Rich strokes it white at 35% whatever the theme, which vanishes around a
14
14
  * light canvas. Here it is the theme's ink blended 35% into the canvas — over
15
- * a black canvas the colour Rich draws — and opaque, so the half of the stroke
16
- * lying outside the canvas does not take on the colour of the page around it.
15
+ * a black canvas the color Rich draws — and opaque, so the half of the stroke
16
+ * lying outside the canvas does not take on the color of the page around it.
17
17
  *
18
18
  * Nor is how a glyph lands on its cell. The exporter has no font engine and
19
19
  * does not know which font the viewer will draw with, so no advance width can
@@ -87,10 +87,10 @@ function chunk(text, column) {
87
87
  function place(rows) {
88
88
  return rows.flatMap((runs, row) => {
89
89
  let column = 0;
90
- return runs.map(({ text, look }) => {
90
+ return runs.map(({ text, look, inVisualOrder }) => {
91
91
  const chunks = chunk(text, column);
92
92
  const cells = chunks.reduce((sum, c) => sum + c.cells, 0);
93
- const placed = { row, column, cells, look, chunks };
93
+ const placed = { row, column, cells, look, inVisualOrder, chunks };
94
94
  column += cells;
95
95
  return placed;
96
96
  });
@@ -132,15 +132,16 @@ function backgroundSvg(run) {
132
132
  ? []
133
133
  : [`<rect fill="${run.look.background.hex}" ${cellBox(run)} shape-rendering="crispEdges"/>`];
134
134
  }
135
- function textSvg({ text, column, cells }, baseline) {
135
+ function textSvg({ text, column, cells }, baseline, inVisualOrder) {
136
136
  // A chunk of no cells has no length to stretch to; it takes the font's.
137
137
  const length = cells === 0 ? "" : ` textLength="${n(cells * CELL_WIDTH)}" lengthAdjust="spacingAndGlyphs"`;
138
- return `<text x="${n(column * CELL_WIDTH)}" y="${n(baseline)}"${length} xml:space="preserve">${escapeText(text)}</text>`;
138
+ const order = inVisualOrder ? ` unicode-bidi="bidi-override"` : "";
139
+ return `<text x="${n(column * CELL_WIDTH)}" y="${n(baseline)}"${length}${order} xml:space="preserve">${escapeText(text)}</text>`;
139
140
  }
140
141
  /**
141
142
  * One run: its glyphs, then its lines and outline over them, in a group whose
142
143
  * class carries the look — so the lines take the glyph's `fill` and blink
143
- * with it. The outline is a stroke, so it names its colour itself.
144
+ * with it. The outline is a stroke, so it names its color itself.
144
145
  */
145
146
  function runSvg(run, className) {
146
147
  const { look } = run;
@@ -155,7 +156,7 @@ function runSvg(run, className) {
155
156
  ];
156
157
  const radius = OUTLINE_RADIUS[look.outline];
157
158
  const drawn = [
158
- ...run.chunks.map((c) => textSvg(c, baseline)),
159
+ ...run.chunks.map((c) => textSvg(c, baseline, run.inVisualOrder)),
159
160
  ...lineYs.map((y) => `<rect x="${x}" y="${n(y)}" width="${width}" height="${LINE_THICKNESS}"/>`),
160
161
  ...(radius === null
161
162
  ? []
package/dist/core/json.js CHANGED
@@ -6,10 +6,13 @@
6
6
  * imports core primitives only. `Console.printJson` delegates here, so this is
7
7
  * where the console can reach it without an upward edge. [LAW:one-way-deps]
8
8
  */
9
+ import { escaped } from "./bidi.js";
9
10
  import { RichText } from "./text.js";
10
11
  import { JSONHighlighter } from "./highlighter.js";
11
12
  import { measure, render } from "./protocol.js";
12
13
  const highlighter = new JSONHighlighter();
14
+ /** `character` as JSON escapes, a surrogate pair for one past the BMP. */
15
+ const jsonEscape = (character) => Array.from({ length: character.length }, (_, at) => `\\u${character.charCodeAt(at).toString(16).padStart(4, "0")}`).join("");
13
16
  export class JSONRenderable {
14
17
  text;
15
18
  constructor(text) {
@@ -38,8 +41,12 @@ export class JSONRenderable {
38
41
  return value;
39
42
  }
40
43
  : undefined;
41
- const jsonStr = JSON.stringify(data, replacer, indent);
42
- const text = new RichText(jsonStr);
44
+ // `undefined` for what JSON has no form of, a function or a symbol, whatever its type says.
45
+ const jsonStr = JSON.stringify(data, replacer, indent) ?? "";
46
+ // Data on show: one string holds every key and value, so a control left in
47
+ // one would act on the rest of its line, and what content deletes or the wire
48
+ // withholds would leave a document that no longer parses back to this data.
49
+ const text = new RichText(escaped(jsonStr, jsonEscape));
43
50
  if (doHighlight) {
44
51
  highlighter.highlight(text);
45
52
  }
@@ -17,9 +17,10 @@
17
17
  * and is rejected with a `MarkupSyntaxError`. For the same reason a style tag
18
18
  * may not open on one side of a plugin tag and close on the other.
19
19
  */
20
+ import { closed } from "./bidi.js";
20
21
  import { cellLen } from "./cells.js";
21
22
  import { Style, StyleSyntaxError } from "./style.js";
22
- import { RichText, stripControlChars } from "./text.js";
23
+ import { RichText, sanitized } from "./text.js";
23
24
  import { emojiReplace } from "./emoji.js";
24
25
  import { pattern } from "./pattern.js";
25
26
  import { State } from "./state.js";
@@ -545,7 +546,7 @@ function walk(tokens, source, registry, emoji) {
545
546
  // counted along the way — a span, a splice point — indexes the text it
546
547
  // lands on. Stripped only by the constructor, a control character
547
548
  // shifted every style after it one cell right.
548
- write(out, stripControlChars(emoji ? emojiReplace(text) : text));
549
+ write(out, sanitized(emoji ? emojiReplace(text) : text));
549
550
  continue;
550
551
  }
551
552
  const plugin = pluginOf(token, registry);
@@ -580,7 +581,7 @@ function walk(tokens, source, registry, emoji) {
580
581
  const children = compose(takeFrom(out, target.mark));
581
582
  const text = handler({ attrs, children, raw: source.slice(target.tag.end, token.start) });
582
583
  out.spliced.push({ at: out.length, text });
583
- write(out, text.plain);
584
+ write(out, closed(text.plain));
584
585
  }
585
586
  // Close what is still open, innermost first. The reference pops its stack
586
587
  // here (`while style_stack: start, tag = style_stack.pop()`), and popping is
@@ -615,7 +616,7 @@ function compose(frame, baseStyle) {
615
616
  // The reversal is the whole of the tie-break, not decoration.
616
617
  // `[red][blue]x[/blue][/red]` yields two spans that both start at 0, and a
617
618
  // stable sort leaves those in the order they arrived — closing order,
618
- // innermost first — which is the outer colour winning. Reversed first, the
619
+ // innermost first — which is the outer color winning. Reversed first, the
619
620
  // outer arrives first and the inner repaints it (rich-markup-krk).
620
621
  const spans = [...frame.spans].reverse().sort((a, b) => a.start - b.start);
621
622
  const result = new RichText(frame.plain);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * memo — a string-keyed memo with a size bound: the one cache shape for a
3
3
  * pure computation whose keys a long-running host derives without end. A
4
- * statusline daemon redrawing a time-varying colour parses a new hex string,
4
+ * statusline daemon redrawing a time-varying color parses a new hex string,
5
5
  * matches a new RGB triple, measures a new cell, on every frame; a memo that
6
6
  * never evicts grows by one entry per frame for the life of the process.
7
7
  */
package/dist/core/memo.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * memo — a string-keyed memo with a size bound: the one cache shape for a
3
3
  * pure computation whose keys a long-running host derives without end. A
4
- * statusline daemon redrawing a time-varying colour parses a new hex string,
4
+ * statusline daemon redrawing a time-varying color parses a new hex string,
5
5
  * matches a new RGB triple, measures a new cell, on every frame; a memo that
6
6
  * never evicts grows by one entry per frame for the life of the process.
7
7
  */
@@ -14,7 +14,7 @@
14
14
  * sRGB is the wire format terminals consume (what ANSI escape codes
15
15
  * carry); OKLCH is the perceptual manipulation space (where equal
16
16
  * numerical deltas mean equal perceptual deltas, which is the property
17
- * "transposition" requires). Two types, mutual conversion, no hybrid.
17
+ * "adjustment" requires). Two types, mutual conversion, no hybrid.
18
18
  *
19
19
  * [LAW:one-way-deps] This module imports `ColorRgba` from core/color and
20
20
  * nothing else from inside the project. Nothing in core/ depends back on
@@ -25,13 +25,13 @@ import { ColorRgba } from "./color.js";
25
25
  * A perceptually-uniform color transform.
26
26
  *
27
27
  * [LAW:dataflow-not-control-flow] Identity is *data* (`IDENTITY`), not a
28
- * special case. Every transposition runs the same code path — the values
28
+ * special case. Every adjustment runs the same code path — the values
29
29
  * decide whether the operation is a no-op or a 180° rotation.
30
30
  *
31
31
  * Lightness is parameterized as `L' = clamp01(L * scale + shift)` to make
32
32
  * "invert" expressible as `{scale: -1, shift: 1}` without a boolean knob.
33
33
  */
34
- export interface ThemeKey {
34
+ export interface ColorAdjustment {
35
35
  /** Hue offset in degrees; positive rotates toward warmer→cooler→back. */
36
36
  readonly hueShift: number;
37
37
  /** Multiplier on chroma. 0 = grayscale, 1 = identity, >1 = more saturated. */
@@ -48,22 +48,22 @@ export interface OklchWeights {
48
48
  readonly h: number;
49
49
  readonly alpha: number;
50
50
  }
51
- export declare const IDENTITY: ThemeKey;
51
+ export declare const IDENTITY: ColorAdjustment;
52
52
  /**
53
53
  * Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
54
- * untouched. Combined with the anchor logic in `transposePalette`, this
54
+ * untouched. Combined with the anchor logic in `adjustPalette`, this
55
55
  * converts a dark theme to its light "octave": semantic anchors
56
56
  * (error/success/warning) still lightness-invert — only their *hue* is
57
57
  * preserved — so errors stay red and dark-on-light becomes light-on-dark.
58
58
  */
59
- export declare const INVERT_LIGHTNESS: ThemeKey;
59
+ export declare const INVERT_LIGHTNESS: ColorAdjustment;
60
60
  /**
61
61
  * Whether a key is the no-op transform. Exported so that callers
62
- * (`transposePalette`) can fast-path byte-exact identity without going
62
+ * (`adjustPalette`) can fast-path byte-exact identity without going
63
63
  * through the lossy sRGB↔OKLCH round-trip. [LAW:one-source-of-truth] —
64
64
  * the predicate lives once, here.
65
65
  */
66
- export declare function isIdentityKey(k: ThemeKey): boolean;
66
+ export declare function isIdentityKey(k: ColorAdjustment): boolean;
67
67
  export declare function srgbToLinear(v: number): number;
68
68
  export declare function linearToSrgb(v: number): number;
69
69
  /**
@@ -96,13 +96,13 @@ export declare class Oklch {
96
96
  */
97
97
  toRgba(): ColorRgba;
98
98
  /**
99
- * Apply a `ThemeKey`. Returns a new `Oklch`; pure.
99
+ * Apply a `ColorAdjustment`. Returns a new `Oklch`; pure.
100
100
  *
101
101
  * [LAW:dataflow-not-control-flow] Identity short-circuits to `this` —
102
102
  * same shape as `ColorRgba.compositeOver(bg)` returning `this` when
103
103
  * `alpha === 1`. Callers invoke unconditionally; the data decides.
104
104
  */
105
- applyKey(k: ThemeKey): Oklch;
105
+ applyKey(k: ColorAdjustment): Oklch;
106
106
  /**
107
107
  * The color `t` of the way from this one toward `toward`, in OKLCH.
108
108
  * `t = 0` is this color, `t = 1` is `toward`; pure.
@@ -115,7 +115,7 @@ export declare class Oklch {
115
115
  * that gains chroma, instead of rotating through the wheel from 0°.
116
116
  *
117
117
  * `t` must lie in [0, 1]: this is interpolation, not extrapolation, and a
118
- * `t` outside it (NaN included) is a RangeError, never a colour off the
118
+ * `t` outside it (NaN included) is a RangeError, never a color off the
119
119
  * far end of the segment. [LAW:no-silent-failure]
120
120
  *
121
121
  * [LAW:one-source-of-truth] Achromatic means the same `ACHROMATIC_EPS`
@@ -133,19 +133,19 @@ export declare class Oklch {
133
133
  * whichever is called.
134
134
  *
135
135
  * It exists because perceptual axes are independent: a tint can take most of
136
- * a hue's colourfulness while keeping close to the lightness it started at,
136
+ * a hue's colorfulness while keeping close to the lightness it started at,
137
137
  * which a single `t` cannot say — raising `t` for chroma drags lightness with
138
138
  * it. Every weight is required: an axis left out would need a default, and
139
139
  * "unchanged" (0) and "same as the others" are both plausible readings.
140
140
  *
141
- * A weak `h` beside a strong `c` shows the starting colour's hue at high
141
+ * A weak `h` beside a strong `c` shows the starting color's hue at high
142
142
  * chroma. The powerless rule covers only a truly achromatic start, so a
143
143
  * near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
144
144
  * noise in proportion. To land on the target's hue, pass `h: 1`.
145
145
  */
146
146
  mixAxes(toward: Oklch, weights: OklchWeights): Oklch;
147
147
  /**
148
- * ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
148
+ * ΔE_OK — the Euclidean distance between this color and `other` in OKLab
149
149
  * (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
150
150
  * resolves. Alpha is not a coordinate of the space and does not count.
151
151
  * Symmetric and pure.
@@ -14,7 +14,7 @@
14
14
  * sRGB is the wire format terminals consume (what ANSI escape codes
15
15
  * carry); OKLCH is the perceptual manipulation space (where equal
16
16
  * numerical deltas mean equal perceptual deltas, which is the property
17
- * "transposition" requires). Two types, mutual conversion, no hybrid.
17
+ * "adjustment" requires). Two types, mutual conversion, no hybrid.
18
18
  *
19
19
  * [LAW:one-way-deps] This module imports `ColorRgba` from core/color and
20
20
  * nothing else from inside the project. Nothing in core/ depends back on
@@ -33,7 +33,7 @@ export const IDENTITY = Object.freeze({
33
33
  });
34
34
  /**
35
35
  * Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
36
- * untouched. Combined with the anchor logic in `transposePalette`, this
36
+ * untouched. Combined with the anchor logic in `adjustPalette`, this
37
37
  * converts a dark theme to its light "octave": semantic anchors
38
38
  * (error/success/warning) still lightness-invert — only their *hue* is
39
39
  * preserved — so errors stay red and dark-on-light becomes light-on-dark.
@@ -46,7 +46,7 @@ export const INVERT_LIGHTNESS = Object.freeze({
46
46
  });
47
47
  /**
48
48
  * Whether a key is the no-op transform. Exported so that callers
49
- * (`transposePalette`) can fast-path byte-exact identity without going
49
+ * (`adjustPalette`) can fast-path byte-exact identity without going
50
50
  * through the lossy sRGB↔OKLCH round-trip. [LAW:one-source-of-truth] —
51
51
  * the predicate lives once, here.
52
52
  */
@@ -179,7 +179,7 @@ export class Oklch {
179
179
  return new ColorRgba(sR, sG, sB, this.alpha);
180
180
  }
181
181
  /**
182
- * Apply a `ThemeKey`. Returns a new `Oklch`; pure.
182
+ * Apply a `ColorAdjustment`. Returns a new `Oklch`; pure.
183
183
  *
184
184
  * [LAW:dataflow-not-control-flow] Identity short-circuits to `this` —
185
185
  * same shape as `ColorRgba.compositeOver(bg)` returning `this` when
@@ -205,7 +205,7 @@ export class Oklch {
205
205
  * that gains chroma, instead of rotating through the wheel from 0°.
206
206
  *
207
207
  * `t` must lie in [0, 1]: this is interpolation, not extrapolation, and a
208
- * `t` outside it (NaN included) is a RangeError, never a colour off the
208
+ * `t` outside it (NaN included) is a RangeError, never a color off the
209
209
  * far end of the segment. [LAW:no-silent-failure]
210
210
  *
211
211
  * [LAW:one-source-of-truth] Achromatic means the same `ACHROMATIC_EPS`
@@ -228,12 +228,12 @@ export class Oklch {
228
228
  * whichever is called.
229
229
  *
230
230
  * It exists because perceptual axes are independent: a tint can take most of
231
- * a hue's colourfulness while keeping close to the lightness it started at,
231
+ * a hue's colorfulness while keeping close to the lightness it started at,
232
232
  * which a single `t` cannot say — raising `t` for chroma drags lightness with
233
233
  * it. Every weight is required: an axis left out would need a default, and
234
234
  * "unchanged" (0) and "same as the others" are both plausible readings.
235
235
  *
236
- * A weak `h` beside a strong `c` shows the starting colour's hue at high
236
+ * A weak `h` beside a strong `c` shows the starting color's hue at high
237
237
  * chroma. The powerless rule covers only a truly achromatic start, so a
238
238
  * near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
239
239
  * noise in proportion. To land on the target's hue, pass `h: 1`.
@@ -265,7 +265,7 @@ export class Oklch {
265
265
  return new _a(lerp(this.l, toward.l, wl), c, hueOf(c, wrapHue(lerp(fromU, toU, wh))), lerp(this.alpha, toward.alpha, walpha));
266
266
  }
267
267
  /**
268
- * ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
268
+ * ΔE_OK — the Euclidean distance between this color and `other` in OKLab
269
269
  * (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
270
270
  * resolves. Alpha is not a coordinate of the space and does not count.
271
271
  * Symmetric and pure.
@@ -276,9 +276,9 @@ export class Oklch {
276
276
  return Math.hypot(this.l - other.l, a - otherA, b - otherB);
277
277
  }
278
278
  /**
279
- * This colour's OKLab a and b, kept with it: a colour matched against a
279
+ * This color's OKLab a and b, kept with it: a color matched against a
280
280
  * palette is measured against every entry, and each entry against every
281
- * colour it is matched with, so the trigonometry is paid once a colour
281
+ * color it is matched with, so the trigonometry is paid once a color
282
282
  * rather than once a comparison.
283
283
  */
284
284
  #lab;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Orienting a drawn line for its destination's `TextOrder`.
3
+ *
4
+ * Levels are resolved over the whole logical line and a row is ordered alone,
5
+ * because UAX #9 runs L1 and L2 after line breaking. So `directed` runs ahead of
6
+ * wrapping and each row's `orient` after it, and cuts fall on logical offsets in
7
+ * every order: what survives a cut is a logical prefix of the sentence, which is
8
+ * what a reader of either direction expects.
9
+ *
10
+ * [LAW:dataflow-not-control-flow] The order chooses values and never steps: a
11
+ * `plain` line passes through both.
12
+ */
13
+ import { Segment } from "./segment.js";
14
+ import { type Style } from "./style.js";
15
+ import type { TextOrder } from "./text-order.js";
16
+ /**
17
+ * The cells that end `text` and are no content: its whitespace, once what draws no
18
+ * cell among it is gone. A mark drawn on a space is not in it, because that space
19
+ * is a cell with something on it.
20
+ */
21
+ export declare function drawnTail(text: string): string;
22
+ /**
23
+ * The cells of trailing whitespace on a wrapped line.
24
+ *
25
+ * A wrap cuts before a word, so the line it closes ends with the whitespace
26
+ * that followed *its* last word — padding the break created, not content the
27
+ * author wrote. Measuring it is what lets the overflow method tell "this line
28
+ * was cut" from "this line ends in spaces" and stamp an ellipsis only on the
29
+ * first, and what keeps a centred line from drifting half a space off true.
30
+ *
31
+ * Asked of the line's text rather than walked back through its segments,
32
+ * because where a line ends is a fact about the characters and not about how
33
+ * they were split. Walking the segments made it a fact about both: it read a
34
+ * segment with no trailing whitespace as content and stopped there, which an
35
+ * empty segment also looks like — and a crop leaves one behind. A styled line
36
+ * whose last cells were cropped therefore reported no hanging whitespace at
37
+ * all and was centred as though it were content to the edge, so a span landing
38
+ * anywhere in a wrap's whitespace un-centred the line it closed. What draws no
39
+ * cell after the whitespace is no content either: `closed` writes a bidi control
40
+ * exactly there, and a cut leaves a zero-width code point that opened the next
41
+ * word on the row before it.
42
+ */
43
+ export declare function hangingWhitespace(line: Segment[]): number;
44
+ /** What a row of a directed line is, given the text it started as and its offset in the line. */
45
+ export interface ReorderedLine {
46
+ /** The line to cut: X9's removals gone where the terminal will not need them. */
47
+ readonly line: readonly Segment[];
48
+ /** Whether a row's cells may not be merged with the padding beside them. */
49
+ readonly oriented: boolean;
50
+ /** A row of `source` at `start`: the segments it became, ready for padding. */
51
+ orient(shown: readonly Segment[], start: number, source: string): Segment[];
52
+ }
53
+ /** What `order` does with `line`, one logical line of drawn segments. */
54
+ export declare function reorder(line: readonly Segment[], order: TextOrder): ReorderedLine;
55
+ /** `text` as `order` measures it: what `directed` removes ahead of wrapping is no word. */
56
+ export declare function measured(text: string, order: TextOrder): string;
57
+ /** The offsets in `text`, one row, of the characters `order` shows at its left and right edges. */
58
+ export declare function edges(text: string, order: TextOrder): {
59
+ readonly left: number;
60
+ readonly right: number;
61
+ };
62
+ /**
63
+ * A label set between `before` and `after`, which are frame and stay where they
64
+ * are, for a row drawn with no `RichText`: only the label is oriented, or a
65
+ * button's brackets would be mirrored around it. A label `directed` leaves alone
66
+ * is one segment with its frame, as it always was.
67
+ */
68
+ export declare function labeled(before: string, label: string, after: string, style: Style | undefined, order: TextOrder | undefined): Segment[];