@promptctl/rich-js 0.26.0 → 0.28.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 (132) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +52 -15
  4. package/dist/core/animation.js +82 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  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 +93 -5
  17. package/dist/core/color.d.ts +26 -18
  18. package/dist/core/color.js +37 -34
  19. package/dist/core/console.d.ts +26 -1
  20. package/dist/core/console.js +26 -6
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/easing.js +6 -1
  24. package/dist/core/env.d.ts +6 -0
  25. package/dist/core/env.js +9 -1
  26. package/dist/core/export-html.js +4 -3
  27. package/dist/core/export-lines.d.ts +2 -0
  28. package/dist/core/export-lines.js +7 -3
  29. package/dist/core/export-svg.js +6 -5
  30. package/dist/core/json.js +9 -2
  31. package/dist/core/markup.js +4 -3
  32. package/dist/core/oklch.d.ts +2 -0
  33. package/dist/core/oklch.js +3 -3
  34. package/dist/core/orient.d.ts +68 -0
  35. package/dist/core/orient.js +224 -0
  36. package/dist/core/osc8.d.ts +12 -5
  37. package/dist/core/osc8.js +14 -7
  38. package/dist/core/pretty.js +22 -18
  39. package/dist/core/protocol.d.ts +53 -22
  40. package/dist/core/protocol.js +14 -6
  41. package/dist/core/render.d.ts +8 -3
  42. package/dist/core/render.js +19 -5
  43. package/dist/core/seal.d.ts +26 -0
  44. package/dist/core/seal.js +75 -0
  45. package/dist/core/segment.d.ts +9 -1
  46. package/dist/core/segment.js +11 -1
  47. package/dist/core/style.d.ts +29 -1
  48. package/dist/core/style.js +41 -5
  49. package/dist/core/text-order.d.ts +51 -0
  50. package/dist/core/text-order.js +50 -0
  51. package/dist/core/text.d.ts +10 -0
  52. package/dist/core/text.js +133 -87
  53. package/dist/core/timeline.js +6 -6
  54. package/dist/core/view.d.ts +23 -9
  55. package/dist/core/view.js +37 -12
  56. package/dist/core/wrap.d.ts +24 -6
  57. package/dist/core/wrap.js +82 -16
  58. package/dist/host/app.d.ts +48 -7
  59. package/dist/host/app.js +116 -11
  60. package/dist/host/index.d.ts +3 -2
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +35 -2
  63. package/dist/host/input.js +88 -4
  64. package/dist/host/terminal-colors.d.ts +18 -0
  65. package/dist/host/terminal-colors.js +46 -0
  66. package/dist/host/terminal-host.d.ts +2 -1
  67. package/dist/index.d.ts +10 -8
  68. package/dist/index.js +2 -2
  69. package/dist/renderables/change.d.ts +1 -3
  70. package/dist/renderables/change.js +4 -6
  71. package/dist/renderables/effect.d.ts +61 -60
  72. package/dist/renderables/effect.js +91 -81
  73. package/dist/renderables/effects.d.ts +37 -8
  74. package/dist/renderables/effects.js +193 -55
  75. package/dist/renderables/frames.d.ts +1 -10
  76. package/dist/renderables/frames.js +1 -16
  77. package/dist/renderables/glyph-effect.d.ts +12 -13
  78. package/dist/renderables/glyph-effect.js +34 -32
  79. package/dist/renderables/glyph-effects.d.ts +27 -8
  80. package/dist/renderables/glyph-effects.js +75 -7
  81. package/dist/renderables/lifecycle.d.ts +40 -32
  82. package/dist/renderables/lifecycle.js +87 -48
  83. package/dist/renderables/live.d.ts +3 -3
  84. package/dist/renderables/live.js +4 -4
  85. package/dist/renderables/markdown.js +4 -2
  86. package/dist/renderables/panel.js +6 -4
  87. package/dist/renderables/progress.d.ts +11 -4
  88. package/dist/renderables/progress.js +29 -17
  89. package/dist/renderables/progressBar.d.ts +3 -3
  90. package/dist/renderables/progressBar.js +16 -13
  91. package/dist/renderables/syntax.js +7 -2
  92. package/dist/renderables/table.js +2 -1
  93. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  94. package/dist/template-bindings/animation-funcs.js +115 -32
  95. package/dist/template-bindings/index.d.ts +7 -5
  96. package/dist/template-bindings/index.js +8 -5
  97. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  98. package/dist/template-bindings/palette-funcs.js +1 -1
  99. package/dist/themes/buildPalette.d.ts +15 -39
  100. package/dist/themes/buildPalette.js +196 -67
  101. package/dist/themes/colorMath.d.ts +10 -4
  102. package/dist/themes/colorMath.js +33 -1
  103. package/dist/themes/data/index.js +1 -4
  104. package/dist/themes/data/textual-ansi.js +0 -143
  105. package/dist/themes/data/types.d.ts +3 -1
  106. package/dist/themes/palette.d.ts +22 -6
  107. package/dist/themes/palette.js +13 -21
  108. package/dist/themes/ramp.d.ts +1 -1
  109. package/dist/themes/ramp.js +1 -1
  110. package/dist/themes/registry.d.ts +7 -10
  111. package/dist/themes/registry.js +9 -30
  112. package/dist/themes/terminalThemes.d.ts +4 -10
  113. package/dist/themes/terminalThemes.js +31 -46
  114. package/dist/themes/transpose.d.ts +5 -10
  115. package/dist/themes/transpose.js +13 -24
  116. package/dist/widgets/button.d.ts +1 -1
  117. package/dist/widgets/button.js +6 -6
  118. package/dist/widgets/checkbox.js +4 -4
  119. package/dist/widgets/dropdown.d.ts +1 -1
  120. package/dist/widgets/dropdown.js +7 -5
  121. package/dist/widgets/event-router.js +2 -2
  122. package/dist/widgets/ink.d.ts +5 -4
  123. package/dist/widgets/ink.js +6 -9
  124. package/dist/widgets/text-input.d.ts +1 -1
  125. package/dist/widgets/text-input.js +41 -32
  126. package/dist/widgets/themed-widget.js +3 -3
  127. package/dist/widgets/toggle.d.ts +1 -1
  128. package/dist/widgets/toggle.js +5 -5
  129. package/dist/widgets/types.d.ts +2 -2
  130. package/dist/widgets/widget-app.d.ts +3 -3
  131. package/dist/widgets/widget-app.js +5 -5
  132. package/package.json +5 -4
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import type { Segment } from "./segment.js";
6
6
  import { Style, type Hold, type StyleSyntaxError, type Theme } from "./style.js";
7
- import { ColorDepth } from "./color.js";
7
+ import { ColorDepth, type TerminalColors } from "./color.js";
8
+ import type { TextOrder } from "./text-order.js";
8
9
  import type { Highlighter } from "./highlighter.js";
9
10
  /**
10
11
  * What becomes of a line still wider than its width once wrapping is done.
@@ -18,13 +19,27 @@ import type { Highlighter } from "./highlighter.js";
18
19
  */
19
20
  export type OverflowMethod = "fold" | "crop" | "ellipsis" | "ignore";
20
21
  /**
21
- * What draws frames — a `Live`, an `App` — as what draws in them sees it:
22
- * the time a frame drawn now is drawn for, the host's clock read through its
23
- * timeline for a view on it (`frameTime`, `./view.ts`). Not a `Clock`: under
24
- * a paused timeline it holds, and under a reversed one it runs backwards.
22
+ * The frame being drawn, as a value: `time`, the moment it is drawn for, in
23
+ * seconds, on the clock its cells' `nextChange` stamps are read on, and
24
+ * `host`, the identity of what drew it — nothing to read, so what draws in
25
+ * the frame has one time, this one, and cannot ask the host for another.
26
+ * [LAW:types-are-the-program] A `Progress` keeps the identity to refuse a
27
+ * frame of a host other than the one its tasks are timed on.
28
+ */
29
+ export interface HostFrame {
30
+ readonly time: number;
31
+ readonly host: object;
32
+ }
33
+ /**
34
+ * What draws frames — a `Live`, an `App` — as what runs between them sees
35
+ * it: the frame it would draw now, its time the host's clock read through
36
+ * its timeline (`frameNow`, `./view.ts`). Not a `Clock`: under a paused
37
+ * timeline it holds, and under a reversed one it runs backwards. Asked
38
+ * inside a render it throws (`RenderClockError`): a frame has the time it was
39
+ * drawn for, and a renderable reads that one.
25
40
  */
26
41
  export interface FrameHost {
27
- readonly frameTime: number;
42
+ frameNow(): HostFrame;
28
43
  }
29
44
  export interface RenderOptions {
30
45
  /**
@@ -83,6 +98,17 @@ export interface RenderOptions {
83
98
  * handed. Absent means truecolor.
84
99
  */
85
100
  colorSystem?: ColorDepth | null;
101
+ /** What the output's destination does with direction. Absent is `"plain"`. */
102
+ textOrder?: TextOrder;
103
+ /**
104
+ * The terminal the output is drawn on: the colours a cell that sets none is
105
+ * drawn in, and the sixteen an ANSI colour names. The terminal's own palette
106
+ * is never queried, so the program says what it is, once, where the output
107
+ * is decided; a renderable reads it here (`Effected`, `ProgressBar`) and
108
+ * nothing hands it down to one. Absent is the terminal the depth assumes
109
+ * (`resolveTerminal`).
110
+ */
111
+ terminal?: TerminalColors;
86
112
  /**
87
113
  * The names a style string may use. A `Console` passes its own; absent, the
88
114
  * built-in defaults apply. Read it through `getStyle` rather than directly.
@@ -97,26 +123,22 @@ export interface RenderOptions {
97
123
  onStyleError?: StyleErrorHandler;
98
124
  /**
99
125
  * The reader asked for reduced motion — CSS's `prefers-reduced-motion`,
100
- * which a terminal has no way to report, so a host is told. A renderable
101
- * built for the time the host draws at draws every animation reduced, its
102
- * motion taken out and its content kept on time (`shownAt`,
103
- * `./view.ts`; `Animation.reduced`, `./animation.ts`). Absent,
104
- * everything moves. Nothing here reads it from the environment.
126
+ * which a terminal has no way to report, so a host is told. Every placed
127
+ * animation is drawn reduced, its motion taken out and its content kept on
128
+ * time (`shownAt`, `Animation.reduced`, `./animation.ts`), and a `Destination`
129
+ * told the same writes no blink (`shownOn`, `./render.ts`). Absent, everything
130
+ * moves. Nothing here reads it from the environment.
105
131
  */
106
132
  reducedMotion?: boolean;
107
133
  /**
108
- * The frame being drawn: `time`, the moment it is drawn for, in seconds, on
109
- * the clock its cells' `nextChange` stamps are read on, and `host`, the
110
- * host drawing it, whose `frameTime` that moment was. It is how a
111
- * renderable that lives across frames — a widget, a `Progress` — draws for
112
- * the time without being rebuilt each frame with one, and without reading
113
- * a clock. Absent, the frame is drawn for no moment — a `print` — and
114
- * nothing drawn from it moves.
134
+ * The frame being drawn (`HostFrame`): how a renderable that lives across
135
+ * frames — a widget, a `Progress` — draws for the time without being
136
+ * rebuilt each frame with one, and without reading a clock. A host writes
137
+ * it once a frame, and a print writes the one its caller named
138
+ * (`PrintOptions.frame`). Absent, the frame is drawn for no moment, 0
139
+ * (`frameTimeOf`, `./animation.ts`), and nothing drawn from it moves.
115
140
  */
116
- frame?: {
117
- readonly time: number;
118
- readonly host: FrameHost;
119
- };
141
+ frame?: HostFrame;
120
142
  /**
121
143
  * Told of every renderable `render` is asked for, as it is asked, so the
122
144
  * calls arrive in document order: a container before what it holds, what
@@ -138,6 +160,15 @@ export interface RenderOptions {
138
160
  * decides the frame's layout, so when it changes, the frame may.
139
161
  */
140
162
  onHold?: (hold: Hold) => void;
163
+ /**
164
+ * Told of the hold the element `key` names, drawn for a time on its own
165
+ * clock, is in: an effect's frame, which alone knows, since the answer
166
+ * depends on what it drew. `t` is that clock's, not the frame's, so this cannot be `onHold`:
167
+ * the animation placed around the value carries the answer back through the
168
+ * combinators that remapped the time (`Moves`, `./animation.ts`). It reaches
169
+ * whatever `frame` reaches.
170
+ */
171
+ onMoved?: (t: number, key: string, hold: Hold) => void;
141
172
  }
142
173
  /** The one field of the render options that says whether the reader wants motion. */
143
174
  export type Motion = Pick<RenderOptions, "reducedMotion">;
@@ -3,7 +3,7 @@
3
3
  * [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
4
4
  */
5
5
  import { cellCount } from "./cells.js";
6
- import { DEFAULT_THEME, Style } from "./style.js";
6
+ import { DEFAULT_THEME, NEVER_CHANGES, Style } from "./style.js";
7
7
  /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
8
8
  export const isAscii = (text) => /^[\x00-\x7F]*$/.test(text);
9
9
  export function drawable(options, chosen, ascii, glyphs = String) {
@@ -262,7 +262,9 @@ export function render(renderable, options) {
262
262
  * enters and takes some. A motion held at no width therefore wakes its host
263
263
  * for frames that look the same: when it next takes room is not a moment its
264
264
  * hold can name, and an early answer costs a frame where a late one hides the
265
- * element. It commits nothing, as a measurement commits nothing —
265
+ * element. An effect is not such a motion: it cannot give itself room, so
266
+ * however it moves, a held one reports held for good, and whatever gives it
267
+ * room wakes the host. It commits nothing, as a measurement commits nothing —
266
268
  * its effects (`afterPass`) are learned at a width nobody sees, so a
267
269
  * `Viewport` it shows content through keeps the scroll the last frame shown
268
270
  * left it.
@@ -273,10 +275,15 @@ export function render(renderable, options) {
273
275
  * can repeat.
274
276
  */
275
277
  function holding(options) {
276
- const { onPresent } = options;
278
+ const { onPresent, onMoved } = options;
277
279
  return {
278
280
  phase: { pass: phaseHere().pass, effects: [] },
279
- drawing: { ...options, maxWidth: 0, onPresent: onPresent && ((content) => onPresent(content, "held")) },
281
+ drawing: {
282
+ ...options,
283
+ maxWidth: 0,
284
+ onPresent: onPresent && ((content) => onPresent(content, "held")),
285
+ onMoved: onMoved && ((t, key) => onMoved(t, key, NEVER_CHANGES)),
286
+ },
280
287
  };
281
288
  }
282
289
  /**
@@ -295,13 +302,14 @@ export function measure(measurable, options) {
295
302
  * `options` as a measurement is asked with. A `measure` that renders to learn
296
303
  * its shape draws nothing on the frame, so it reports nothing to `onPresent` —
297
304
  * one that did would put a widget in the view at the moment a container
298
- * measured it, and keep one the frame then cropped away. It still tells its
305
+ * measured it, and keep one the frame then cropped away — and nothing to
306
+ * `onMoved`, which speaks only for what the frame shows. It still tells its
299
307
  * hold (`onHold`): a layout decided by a measurement changes when it does.
300
308
  *
301
309
  * [LAW:single-enforcer] Every measurement starts here: `measure` asks with it.
302
310
  */
303
311
  function measuring(options) {
304
- return { ...options, onPresent: undefined };
312
+ return { ...options, onPresent: undefined, onMoved: undefined };
305
313
  }
306
314
  /**
307
315
  * The options a renderable should *lay out against*: `withCellWidth`, with an
@@ -49,7 +49,7 @@ import type { ColorSystemName, Destination } from "./color.js";
49
49
  import type { Env } from "./env.js";
50
50
  import { Segment } from "./segment.js";
51
51
  import type { Renderable, RenderOptions } from "./protocol.js";
52
- export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "theme" | "onStyleError" | "highlight" | "highlighter"> {
52
+ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "textOrder" | "terminal" | "theme" | "onStyleError" | "highlight" | "highlighter" | "frame"> {
53
53
  /** Cell width to render into. Default 80. */
54
54
  width?: number;
55
55
  /**
@@ -92,9 +92,14 @@ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" |
92
92
  export declare function shownOn(segment: Segment, destination: Destination): Segment;
93
93
  /**
94
94
  * Encodes one segment as ANSI bytes for `destination`: the reference's
95
- * `Style.render` for that segment, and nothing for a control segment.
95
+ * `Style.render` for that segment, and nothing for a control segment. It seals
96
+ * the segment alone, and a `logical` row's isolate spans its segments, so a row
97
+ * goes through `segmentsToString` whole.
96
98
  */
97
99
  export declare function segmentToString(segment: Segment, destination: Destination): string;
98
- /** Encodes a sequence of segments as ANSI bytes for `destination`, one segment at a time. */
100
+ /**
101
+ * Encodes a sequence of segments as ANSI bytes for `destination`, one segment
102
+ * at a time, over what `sealed` lets reach the wire.
103
+ */
99
104
  export declare function segmentsToString(segments: Iterable<Segment>, destination: Destination): string;
100
105
  export declare function renderToString(renderable: Renderable, options?: RenderToStringOptions): string;
@@ -48,6 +48,7 @@ import { ColorDepth, resolveDestination } from "./color.js";
48
48
  import { Segment } from "./segment.js";
49
49
  import { render } from "./protocol.js";
50
50
  import { OSC8_CLOSE, osc8Open } from "./osc8.js";
51
+ import { sealed } from "./seal.js";
51
52
  const DEFAULT_WIDTH = 80;
52
53
  /**
53
54
  * `segment` as `destination` shows it: under reduced motion its style
@@ -64,9 +65,14 @@ export function shownOn(segment, destination) {
64
65
  }
65
66
  /**
66
67
  * Encodes one segment as ANSI bytes for `destination`: the reference's
67
- * `Style.render` for that segment, and nothing for a control segment.
68
+ * `Style.render` for that segment, and nothing for a control segment. It seals
69
+ * the segment alone, and a `logical` row's isolate spans its segments, so a row
70
+ * goes through `segmentsToString` whole.
68
71
  */
69
72
  export function segmentToString(segment, destination) {
73
+ return segmentsToString([segment], destination);
74
+ }
75
+ function encoded(segment, destination) {
70
76
  if (segment.isControl)
71
77
  return "";
72
78
  const { text, style } = shownOn(segment, destination);
@@ -80,11 +86,14 @@ export function segmentToString(segment, destination) {
80
86
  const link = destination.hyperlinks ? style.link : undefined;
81
87
  return link === undefined ? styled : `${osc8Open(link)}${styled}${OSC8_CLOSE}`;
82
88
  }
83
- /** Encodes a sequence of segments as ANSI bytes for `destination`, one segment at a time. */
89
+ /**
90
+ * Encodes a sequence of segments as ANSI bytes for `destination`, one segment
91
+ * at a time, over what `sealed` lets reach the wire.
92
+ */
84
93
  export function segmentsToString(segments, destination) {
85
94
  const parts = [];
86
- for (const segment of segments)
87
- parts.push(segmentToString(segment, destination));
95
+ for (const segment of sealed(segments, destination.textOrder))
96
+ parts.push(encoded(segment, destination));
88
97
  return parts.join("");
89
98
  }
90
99
  export function renderToString(renderable, options) {
@@ -92,7 +101,9 @@ export function renderToString(renderable, options) {
92
101
  // [LAW:dataflow-not-control-flow] An absent spec is truecolor; an explicit
93
102
  // `null` stays null (`??` would collapse it).
94
103
  // [LAW:single-enforcer] Specs resolve through `resolveDestination`.
95
- const destinationOptions = { reducedMotion: options?.reducedMotion };
104
+ // Stated, never detected: this call's bytes have no known destination, and
105
+ // its output is deterministic.
106
+ const destinationOptions = { reducedMotion: options?.reducedMotion, textOrder: options?.textOrder ?? "plain" };
96
107
  if (options?.env !== undefined)
97
108
  destinationOptions.env = options.env;
98
109
  if (options?.isTTY !== undefined)
@@ -114,13 +125,16 @@ export function renderToString(renderable, options) {
114
125
  asciiOnly: options?.asciiOnly ?? false,
115
126
  // [LAW:one-source-of-truth] What is drawn and how it is encoded take one preference.
116
127
  reducedMotion: destination.reducedMotion,
128
+ terminal: options?.terminal,
117
129
  theme: options?.theme,
118
130
  onStyleError: options?.onStyleError,
119
131
  highlight: options?.highlight,
120
132
  highlighter: options?.highlighter,
133
+ frame: options?.frame,
121
134
  // [LAW:one-source-of-truth] The depth the segments below are encoded at,
122
135
  // so a renderable measures what this very call will draw.
123
136
  colorSystem: destination.colorSystem,
137
+ textOrder: destination.textOrder,
124
138
  };
125
139
  return segmentsToString(render(renderable, renderOptions), destination);
126
140
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `sealed` — the one total enforcer of what the wire holds.
3
+ *
4
+ * [LAW:single-enforcer] An explicit bidi control reaches the bytes only inside
5
+ * an isolate that accounts for it. The rule has no list of who wrote what: in a
6
+ * segment with no logical stamp every explicit control is deleted, in every
7
+ * order, so a raw `new Segment(text)` is contained and merely shows no effect.
8
+ * What the census withholds goes here too (`wireText`), a segment at a time, on
9
+ * text nothing cuts again, which is why a rule that reads neighbours can live here
10
+ * and not in the sanitizer. A span edge is therefore where that reading stops. A
11
+ * flag cut by one loses its tags and stays a flag; a selector after one is kept;
12
+ * a ZWJ before one is not taken with a deletion past it, so a join can form that
13
+ * `cellLen`, which reads the whole string, did not measure. Segments have always
14
+ * been measured apart and written together, so the same edge in the same text with
15
+ * nothing deleted draws the same row.
16
+ *
17
+ * This runs on the way out, below every cut, because a row is cut after it is
18
+ * drawn: `scroll` divides it, `mergeHorizontal` and a viewport crop it, an
19
+ * overlay splices into it. Controls written at drawing time would be cut with
20
+ * it. `Style.bidi` is a fact on the cells instead, so what survives still names
21
+ * its stamp, and the controls are written around the survivors.
22
+ */
23
+ import { Segment } from "./segment.js";
24
+ import type { TextOrder } from "./text-order.js";
25
+ /** `segments` as the wire may hold them for `order`. */
26
+ export declare function sealed(segments: Iterable<Segment>, order: TextOrder): Iterable<Segment>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `sealed` — the one total enforcer of what the wire holds.
3
+ *
4
+ * [LAW:single-enforcer] An explicit bidi control reaches the bytes only inside
5
+ * an isolate that accounts for it. The rule has no list of who wrote what: in a
6
+ * segment with no logical stamp every explicit control is deleted, in every
7
+ * order, so a raw `new Segment(text)` is contained and merely shows no effect.
8
+ * What the census withholds goes here too (`wireText`), a segment at a time, on
9
+ * text nothing cuts again, which is why a rule that reads neighbours can live here
10
+ * and not in the sanitizer. A span edge is therefore where that reading stops. A
11
+ * flag cut by one loses its tags and stays a flag; a selector after one is kept;
12
+ * a ZWJ before one is not taken with a deletion past it, so a join can form that
13
+ * `cellLen`, which reads the whole string, did not measure. Segments have always
14
+ * been measured apart and written together, so the same edge in the same text with
15
+ * nothing deleted draws the same row.
16
+ *
17
+ * This runs on the way out, below every cut, because a row is cut after it is
18
+ * drawn: `scroll` divides it, `mergeHorizontal` and a viewport crop it, an
19
+ * overlay splices into it. Controls written at drawing time would be cut with
20
+ * it. `Style.bidi` is a fact on the cells instead, so what survives still names
21
+ * its stamp, and the controls are written around the survivors.
22
+ */
23
+ import { advance, closersOf, opened, withoutExplicit } from "./bidi.js";
24
+ import { wireText } from "./cells.js";
25
+ import { Segment } from "./segment.js";
26
+ const LRI = "\u2066";
27
+ const RLI = "\u2067";
28
+ const PDI = "\u2069";
29
+ /** `text` with closers that match nothing in `open` dropped, so content cannot end the isolate around it. */
30
+ function balanced(text, open) {
31
+ return Array.from(text, (character) => (advance(open, character) ? character : "")).join("");
32
+ }
33
+ /**
34
+ * One row's run of one stamp in its isolate. The initiator is stated from the
35
+ * whole line's direction rather than FSI, so a wrapped row does not re-detect
36
+ * it. Embeddings close by PDF ahead of the PDI because VTE discards a zero-width
37
+ * character at column 0, initiator included, and a PDI matching nothing there
38
+ * closes nothing.
39
+ */
40
+ function* isolated(run, { level, opens }) {
41
+ const open = opened(opens);
42
+ yield new Segment((level === 1 ? RLI : LRI) + opens);
43
+ for (const segment of run) {
44
+ const text = balanced(segment.text, open);
45
+ yield text === segment.text ? segment : new Segment(text, segment.style, segment.control);
46
+ }
47
+ yield new Segment(closersOf(open.stack.join("")) + PDI);
48
+ }
49
+ /** `segments` as the wire may hold them for `order`. */
50
+ export function* sealed(segments, order) {
51
+ let run = [];
52
+ let stamp;
53
+ const flush = function* () {
54
+ if (stamp !== undefined)
55
+ yield* isolated(run, stamp);
56
+ run = [];
57
+ stamp = undefined;
58
+ };
59
+ for (const raw of segments) {
60
+ const drawn = wireText(raw.text);
61
+ const segment = drawn === raw.text ? raw : new Segment(drawn, raw.style, raw.control);
62
+ const own = order === "logical" && !segment.isControl ? segment.style?.bidi : undefined;
63
+ if (own?.order === "logical") {
64
+ if (own !== stamp)
65
+ yield* flush();
66
+ stamp = own;
67
+ run.push(segment);
68
+ continue;
69
+ }
70
+ yield* flush();
71
+ const text = withoutExplicit(segment.text);
72
+ yield text === segment.text ? segment : new Segment(text, segment.style, segment.control);
73
+ }
74
+ yield* flush();
75
+ }
@@ -130,6 +130,14 @@ export declare class Segment {
130
130
  * short rows padded with spaces, long ones cut.
131
131
  */
132
132
  static splitAndCropLines(segments: Iterable<Segment>, width: number): Segment[][];
133
+ /**
134
+ * The plain text of one line of segments, in the coordinate system
135
+ * `divideLine` and `Segment.divide` share: cell offsets into the styled line
136
+ * are cell offsets into this string. [LAW:one-source-of-truth] for that
137
+ * correspondence — the offsets are found in this text and applied to the
138
+ * segments it came from.
139
+ */
140
+ static plainOf(line: readonly Segment[]): string;
133
141
  /**
134
142
  * Returns total cell width of a line. Ignores control segments.
135
143
  */
@@ -177,7 +185,7 @@ export declare class Segment {
177
185
  * rest every time, so a long line wrapped into many rows took time quadratic
178
186
  * in its length.
179
187
  */
180
- static divide(segments: Segment[], cuts: number[]): Segment[][];
188
+ static divide(segments: readonly Segment[], cuts: number[]): Segment[][];
181
189
  /**
182
190
  * Pads below to fill height.
183
191
  */
@@ -307,6 +307,16 @@ export class Segment {
307
307
  static splitAndCropLines(segments, width) {
308
308
  return Segment.splitLines(segments).map((line) => Segment.adjustLineLength(line, width));
309
309
  }
310
+ /**
311
+ * The plain text of one line of segments, in the coordinate system
312
+ * `divideLine` and `Segment.divide` share: cell offsets into the styled line
313
+ * are cell offsets into this string. [LAW:one-source-of-truth] for that
314
+ * correspondence — the offsets are found in this text and applied to the
315
+ * segments it came from.
316
+ */
317
+ static plainOf(line) {
318
+ return line.map((segment) => segment.text).join("");
319
+ }
310
320
  /**
311
321
  * Returns total cell width of a line. Ignores control segments.
312
322
  */
@@ -426,7 +436,7 @@ export class Segment {
426
436
  */
427
437
  static divide(segments, cuts) {
428
438
  if (cuts.length === 0)
429
- return [segments];
439
+ return [[...segments]];
430
440
  const sections = [[]];
431
441
  let offset = 0;
432
442
  let next = 0;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { ColorSpec, ColorDepth } from "./color.js";
5
5
  import { type Anchor } from "./anchor.js";
6
+ import type { BidiStamp } from "./text-order.js";
6
7
  /**
7
8
  * Canonical text-attribute inventory. Single source of truth consumed by
8
9
  * `Style.parse` / `Style.toString` and the template bindings — adding an
@@ -45,6 +46,8 @@ export interface StyleOptions {
45
46
  link?: string;
46
47
  meta?: Record<string, unknown>;
47
48
  anchor?: Anchor;
49
+ boxFrame?: object;
50
+ bidi?: BidiStamp;
48
51
  /**
49
52
  * The hold the style's cells are in, both ends or neither: a cell saying
50
53
  * only when it next changes would claim, by default, that it always was,
@@ -58,6 +61,10 @@ export interface Hold {
58
61
  readonly lastChange: number;
59
62
  readonly nextChange: number;
60
63
  }
64
+ /** The hold of a cell that never changes: always was, and always will be. */
65
+ export declare const NEVER_CHANGES: Hold;
66
+ /** The moments both `a` and `b` hold through: the later `lastChange` and the sooner `nextChange`. */
67
+ export declare function intersection(a: Hold, b: Hold): Hold;
61
68
  export declare class StyleSyntaxError extends Error {
62
69
  constructor(message: string, options?: ErrorOptions);
63
70
  }
@@ -85,6 +92,23 @@ export declare class Style {
85
92
  * unequal, because they came from different places.
86
93
  */
87
94
  readonly anchor: Anchor | undefined;
95
+ /**
96
+ * The drawing of a box these cells are the frame of (`Box.frame`), compared
97
+ * by identity. A frame is told by what drew it, never by its glyphs, as a
98
+ * strip's seam is (`JOIN`, `./strip.ts`): a box may draw its frame in blanks,
99
+ * and what it draws in ink an element's own content may draw too. It is a
100
+ * drawing and not a box, so two panels of one `ROUNDED` are two frames, and
101
+ * it is no `Anchor`: a frame's cells say whose they are and not where, since
102
+ * a table draws its top rule before it knows its height. It draws nothing.
103
+ */
104
+ readonly boxFrame: object | undefined;
105
+ /**
106
+ * What drawing said of these cells' direction, compared by identity, as a
107
+ * link is a fact the encoder turns into bytes: `Segment` never holds a control
108
+ * the library wrote, so no cut of a row can unbalance one, and what survives a
109
+ * cut still names its stamp (`./seal.ts`). It draws nothing.
110
+ */
111
+ readonly bidi: BidiStamp | undefined;
88
112
  /**
89
113
  * The hold this style's cells are in, on the timeline of whoever drew the
90
114
  * frame: since when what they show has been what it is (`lastChange`), and
@@ -117,6 +141,10 @@ export declare class Style {
117
141
  * The same look, drawn from the place `anchor` names, or from nowhere.
118
142
  */
119
143
  withAnchor(anchor: Anchor | undefined): Style;
144
+ /** The same look, as the frame of the drawing `boxFrame` names, or of none. */
145
+ withBoxFrame(boxFrame: object | undefined): Style;
146
+ /** The same look, its cells' direction as `bidi` says. */
147
+ withBidi(bidi: BidiStamp | undefined): Style;
120
148
  /** The style's text attributes, each as it sets it. */
121
149
  get attributes(): Attributes;
122
150
  /**
@@ -163,7 +191,7 @@ export declare class Style {
163
191
  /**
164
192
  * The look as a canonical string that round-trips through `Style.parse()`:
165
193
  * colours, attributes and the link, which is all a style string can spell.
166
- * What a style carries beyond its look — `meta`, `anchor`, `hold` —
194
+ * What a style carries beyond its look — `meta`, `anchor`, `boxFrame`, `bidi`, `hold` —
167
195
  * has no spelling and is not here, so two styles that differ only there
168
196
  * read alike; `equals` tells them apart.
169
197
  */
@@ -63,7 +63,11 @@ export const ATTRIBUTE_SHORT_ALIASES = {
63
63
  uu: "underline2",
64
64
  };
65
65
  /** The hold of a cell that never changes: always was, and always will be. */
66
- const NEVER_CHANGES = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
66
+ export const NEVER_CHANGES = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
67
+ /** The moments both `a` and `b` hold through: the later `lastChange` and the sooner `nextChange`. */
68
+ export function intersection(a, b) {
69
+ return { lastChange: Math.max(a.lastChange, b.lastChange), nextChange: Math.min(a.nextChange, b.nextChange) };
70
+ }
67
71
  export class StyleSyntaxError extends Error {
68
72
  constructor(message, options) {
69
73
  super(message, options);
@@ -97,6 +101,23 @@ export class Style {
97
101
  * unequal, because they came from different places.
98
102
  */
99
103
  anchor;
104
+ /**
105
+ * The drawing of a box these cells are the frame of (`Box.frame`), compared
106
+ * by identity. A frame is told by what drew it, never by its glyphs, as a
107
+ * strip's seam is (`JOIN`, `./strip.ts`): a box may draw its frame in blanks,
108
+ * and what it draws in ink an element's own content may draw too. It is a
109
+ * drawing and not a box, so two panels of one `ROUNDED` are two frames, and
110
+ * it is no `Anchor`: a frame's cells say whose they are and not where, since
111
+ * a table draws its top rule before it knows its height. It draws nothing.
112
+ */
113
+ boxFrame;
114
+ /**
115
+ * What drawing said of these cells' direction, compared by identity, as a
116
+ * link is a fact the encoder turns into bytes: `Segment` never holds a control
117
+ * the library wrote, so no cut of a row can unbalance one, and what survives a
118
+ * cut still names its stamp (`./seal.ts`). It draws nothing.
119
+ */
120
+ bidi;
100
121
  /**
101
122
  * The hold this style's cells are in, on the timeline of whoever drew the
102
123
  * frame: since when what they show has been what it is (`lastChange`), and
@@ -142,6 +163,8 @@ export class Style {
142
163
  this.link = options.link || undefined;
143
164
  this.meta = options.meta;
144
165
  this.anchor = options.anchor;
166
+ this.boxFrame = options.boxFrame;
167
+ this.bidi = options.bidi;
145
168
  }
146
169
  get isNull() {
147
170
  return (this.color === undefined &&
@@ -162,6 +185,8 @@ export class Style {
162
185
  this.link === undefined &&
163
186
  this.meta === undefined &&
164
187
  this.anchor === undefined &&
188
+ this.boxFrame === undefined &&
189
+ this.bidi === undefined &&
165
190
  this.hold.nextChange === Number.POSITIVE_INFINITY &&
166
191
  this.hold.lastChange === Number.NEGATIVE_INFINITY);
167
192
  }
@@ -199,6 +224,14 @@ export class Style {
199
224
  withAnchor(anchor) {
200
225
  return this.with({ anchor });
201
226
  }
227
+ /** The same look, as the frame of the drawing `boxFrame` names, or of none. */
228
+ withBoxFrame(boxFrame) {
229
+ return this.with({ boxFrame });
230
+ }
231
+ /** The same look, its cells' direction as `bidi` says. */
232
+ withBidi(bidi) {
233
+ return this.with({ bidi });
234
+ }
202
235
  /** The style's text attributes, each as it sets it. */
203
236
  get attributes() {
204
237
  // [LAW:one-source-of-truth] Keyed by `ATTRIBUTE_NAMES`, whose every name is a key here: the cast is that list's own claim.
@@ -257,9 +290,10 @@ export class Style {
257
290
  ? { ...this.meta, ...other.meta }
258
291
  : other.meta ?? this.meta,
259
292
  anchor: other.anchor ?? this.anchor,
260
- // Cells under both change when either does: the sooner, and have been
261
- // what they are since the later — the intersection of the two holds.
262
- hold: { lastChange: Math.max(this.hold.lastChange, other.hold.lastChange), nextChange: Math.min(this.hold.nextChange, other.hold.nextChange) },
293
+ boxFrame: other.boxFrame ?? this.boxFrame,
294
+ bidi: other.bidi ?? this.bidi,
295
+ // Cells under both change when either does.
296
+ hold: intersection(this.hold, other.hold),
263
297
  });
264
298
  }
265
299
  equals(other) {
@@ -282,6 +316,8 @@ export class Style {
282
316
  this.overline === other.overline &&
283
317
  this.link === other.link &&
284
318
  anchorsEqual(this.anchor, other.anchor) &&
319
+ this.boxFrame === other.boxFrame &&
320
+ this.bidi === other.bidi &&
285
321
  this.hold.nextChange === other.hold.nextChange &&
286
322
  this.hold.lastChange === other.hold.lastChange);
287
323
  }
@@ -333,7 +369,7 @@ export class Style {
333
369
  /**
334
370
  * The look as a canonical string that round-trips through `Style.parse()`:
335
371
  * colours, attributes and the link, which is all a style string can spell.
336
- * What a style carries beyond its look — `meta`, `anchor`, `hold` —
372
+ * What a style carries beyond its look — `meta`, `anchor`, `boxFrame`, `bidi`, `hold` —
337
373
  * has no spelling and is not here, so two styles that differ only there
338
374
  * read alike; `equals` tells them apart.
339
375
  */
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What a destination does with text direction.
3
+ *
4
+ * Every order above `plain` is a claim about the terminal, and a wrong claim
5
+ * costs more than none. Visual order on a terminal that also reorders reverses
6
+ * the text twice, and no query answers for every such terminal. An isolate on a
7
+ * terminal whose width table predates it advances the cursor (xterm.js 6 does),
8
+ * which moves a table's border and the rows a painter goes back over. So what
9
+ * nothing is known about gets bare text, and `detectTextOrder` grants more only
10
+ * on evidence.
11
+ */
12
+ import { type Env } from "./env.js";
13
+ /**
14
+ * - `plain`: nothing is known of the destination, or it is not a display: a
15
+ * pipe, a file, an unknown terminal. Characters only, in logical order, with
16
+ * no explicit bidi control.
17
+ * - `logical`: a display known to reorder and to hold the controls at zero
18
+ * width. Rows are sealed in isolates.
19
+ * - `visual`: a display known not to reorder. Rows are written reordered and
20
+ * shaped, with no controls.
21
+ */
22
+ export type TextOrder = "plain" | "logical" | "visual";
23
+ /** A row's paragraph direction: 0 left to right, 1 right to left. */
24
+ export type ParagraphLevel = 0 | 1;
25
+ /** Cells written for a terminal that reorders them: their row's direction, and what is still open where they begin. */
26
+ export interface LogicalStamp {
27
+ readonly order: "logical";
28
+ readonly level: ParagraphLevel;
29
+ readonly opens: string;
30
+ }
31
+ /** Cells already in visual order, for an export not to reverse again. */
32
+ export interface VisualStamp {
33
+ readonly order: "visual";
34
+ }
35
+ export type BidiStamp = LogicalStamp | VisualStamp;
36
+ /**
37
+ * The order `env` gives evidence for.
38
+ *
39
+ * An environment is inherited: `code .` from Windows Terminal carries
40
+ * `WT_SESSION` into VS Code's xterm.js terminal, and Alacritty clears nothing
41
+ * from GNOME Terminal. `TERM` is the one variable every terminal writes for its
42
+ * own child, so it is read first, and a variable a parent could have left counts
43
+ * only beside the `TERM` its own terminal sets. The rows are in the order that
44
+ * makes that true; a terminal naming itself off this list is `plain`.
45
+ *
46
+ * Not listed, each for a reason: xterm.js lays a row out through the browser and
47
+ * advances for an isolate, and VS Code is xterm.js. WezTerm's bidi is a setting
48
+ * the environment cannot show. iTerm2, Konsole, Terminal.app, mintty and mlterm
49
+ * are unmeasured. Any program may state its order.
50
+ */
51
+ export declare function detectTextOrder(env: Env, isTTY: boolean): TextOrder;