@promptctl/rich-js 0.27.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 (110) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +36 -17
  4. package/dist/core/animation.js +61 -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 +6 -7
  18. package/dist/core/color.js +11 -13
  19. package/dist/core/console.d.ts +18 -1
  20. package/dist/core/console.js +24 -5
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/env.d.ts +6 -0
  24. package/dist/core/env.js +9 -1
  25. package/dist/core/export-html.js +4 -3
  26. package/dist/core/export-lines.d.ts +2 -0
  27. package/dist/core/export-lines.js +7 -3
  28. package/dist/core/export-svg.js +6 -5
  29. package/dist/core/json.js +9 -2
  30. package/dist/core/markup.js +4 -3
  31. package/dist/core/orient.d.ts +68 -0
  32. package/dist/core/orient.js +224 -0
  33. package/dist/core/osc8.d.ts +12 -5
  34. package/dist/core/osc8.js +14 -7
  35. package/dist/core/pretty.js +22 -18
  36. package/dist/core/protocol.d.ts +27 -6
  37. package/dist/core/protocol.js +14 -6
  38. package/dist/core/render.d.ts +8 -3
  39. package/dist/core/render.js +18 -5
  40. package/dist/core/seal.d.ts +26 -0
  41. package/dist/core/seal.js +75 -0
  42. package/dist/core/segment.d.ts +9 -1
  43. package/dist/core/segment.js +11 -1
  44. package/dist/core/style.d.ts +29 -1
  45. package/dist/core/style.js +41 -5
  46. package/dist/core/text-order.d.ts +51 -0
  47. package/dist/core/text-order.js +50 -0
  48. package/dist/core/text.d.ts +10 -0
  49. package/dist/core/text.js +133 -87
  50. package/dist/core/timeline.js +6 -6
  51. package/dist/core/view.d.ts +1 -1
  52. package/dist/core/view.js +5 -4
  53. package/dist/core/wrap.d.ts +24 -6
  54. package/dist/core/wrap.js +82 -16
  55. package/dist/host/app.d.ts +44 -3
  56. package/dist/host/app.js +111 -6
  57. package/dist/host/index.d.ts +3 -2
  58. package/dist/host/index.js +1 -0
  59. package/dist/host/input.d.ts +35 -2
  60. package/dist/host/input.js +88 -4
  61. package/dist/host/terminal-colors.d.ts +18 -0
  62. package/dist/host/terminal-colors.js +46 -0
  63. package/dist/host/terminal-host.d.ts +2 -1
  64. package/dist/index.d.ts +5 -4
  65. package/dist/index.js +2 -2
  66. package/dist/renderables/change.d.ts +1 -3
  67. package/dist/renderables/change.js +4 -6
  68. package/dist/renderables/effect.d.ts +60 -59
  69. package/dist/renderables/effect.js +91 -81
  70. package/dist/renderables/effects.d.ts +37 -8
  71. package/dist/renderables/effects.js +185 -52
  72. package/dist/renderables/frames.d.ts +1 -10
  73. package/dist/renderables/frames.js +1 -16
  74. package/dist/renderables/glyph-effect.d.ts +12 -13
  75. package/dist/renderables/glyph-effect.js +34 -32
  76. package/dist/renderables/glyph-effects.d.ts +27 -8
  77. package/dist/renderables/glyph-effects.js +75 -7
  78. package/dist/renderables/lifecycle.d.ts +5 -7
  79. package/dist/renderables/lifecycle.js +6 -7
  80. package/dist/renderables/markdown.js +4 -2
  81. package/dist/renderables/panel.js +6 -4
  82. package/dist/renderables/progressBar.d.ts +3 -3
  83. package/dist/renderables/progressBar.js +16 -13
  84. package/dist/renderables/syntax.js +7 -2
  85. package/dist/renderables/table.js +2 -1
  86. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  87. package/dist/template-bindings/animation-funcs.js +115 -32
  88. package/dist/template-bindings/index.d.ts +7 -5
  89. package/dist/template-bindings/index.js +8 -5
  90. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  91. package/dist/template-bindings/palette-funcs.js +1 -1
  92. package/dist/themes/buildPalette.d.ts +3 -2
  93. package/dist/themes/buildPalette.js +2 -2
  94. package/dist/themes/data/index.js +1 -4
  95. package/dist/themes/data/textual-ansi.js +0 -143
  96. package/dist/themes/ramp.d.ts +1 -1
  97. package/dist/themes/ramp.js +1 -1
  98. package/dist/themes/registry.d.ts +6 -8
  99. package/dist/themes/registry.js +2 -4
  100. package/dist/themes/terminalThemes.d.ts +4 -11
  101. package/dist/themes/terminalThemes.js +31 -47
  102. package/dist/widgets/button.d.ts +1 -1
  103. package/dist/widgets/button.js +6 -6
  104. package/dist/widgets/checkbox.js +4 -4
  105. package/dist/widgets/dropdown.d.ts +1 -1
  106. package/dist/widgets/dropdown.js +7 -5
  107. package/dist/widgets/themed-widget.js +3 -3
  108. package/dist/widgets/toggle.d.ts +1 -1
  109. package/dist/widgets/toggle.js +5 -5
  110. package/package.json +5 -4
@@ -3,7 +3,8 @@
3
3
  */
4
4
  import { Segment } from "./segment.js";
5
5
  import { NULL_STYLE, DEFAULT_THEME } from "./style.js";
6
- import { isDumbTerminal, resolveDestination } from "./color.js";
6
+ import { resolveDestination } from "./color.js";
7
+ import { isDumbTerminal } from "./env.js";
7
8
  import { encodeHtml } from "./export-html.js";
8
9
  import { encodeSvg } from "./export-svg.js";
9
10
  import { RichText } from "./text.js";
@@ -17,6 +18,7 @@ import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
17
18
  // rather than being restated here; see `rule()`.
18
19
  import { Rule } from "../renderables/rule.js";
19
20
  import { segmentsToString, shownOn } from "./render.js";
21
+ import { sealed } from "./seal.js";
20
22
  import { placeBlock } from "./place.js";
21
23
  import { activePass, isRenderable, measure, render } from "./protocol.js";
22
24
  import { ListState, State, refuseRenderWrite } from "./state.js";
@@ -181,6 +183,7 @@ export class Console {
181
183
  _lastLogTime = new State(null);
182
184
  _style;
183
185
  _isTerminal;
186
+ _attended;
184
187
  _interactive;
185
188
  _file;
186
189
  // [LAW:one-source-of-truth] The resolved output stream, not the `stderr`
@@ -191,6 +194,7 @@ export class Console {
191
194
  _markup;
192
195
  _highlight;
193
196
  _theme;
197
+ _terminal;
194
198
  _onStyleError;
195
199
  _asciiOnly;
196
200
  _highlighter;
@@ -211,7 +215,7 @@ export class Console {
211
215
  // [LAW:dataflow-not-control-flow] `isTTY` and `env` are forwarded
212
216
  // unconditionally, so detection stays inside the console's injected
213
217
  // environment rather than the ambient process's.
214
- const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env, reducedMotion: options?.reducedMotion });
218
+ const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env, reducedMotion: options?.reducedMotion, ...(options?.textOrder === undefined ? {} : { textOrder: options.textOrder }) });
215
219
  // [LAW:single-enforcer] The console's one override, an explicit
216
220
  // `hyperlinks`, is applied here and nowhere else; every write encodes for
217
221
  // this value whole. [LAW:one-source-of-truth] The reader's motion
@@ -223,9 +227,12 @@ export class Console {
223
227
  // [LAW:no-ambient-temporal-coupling] The theme is assigned before the
224
228
  // style, because the console's own style may be one of the theme's names.
225
229
  this._theme = options?.theme ?? DEFAULT_THEME;
230
+ this._terminal = options?.terminal;
226
231
  this._style = this._theme.resolve(options?.style ?? NULL_STYLE);
227
- // Rich's rule: a terminal is interactive unless it is a dumb one.
228
- this._interactive = options?.forceInteractive ?? (this._isTerminal && !isDumbTerminal(environment.env));
232
+ // Rich's rule: a terminal is interactive unless it is a dumb one. Forcing that
233
+ // is animation policy, and says nothing of who is at the terminal.
234
+ this._attended = this._isTerminal && !isDumbTerminal(environment.env);
235
+ this._interactive = options?.forceInteractive ?? this._attended;
229
236
  this._file = options?.file;
230
237
  this._record = options?.record ?? false;
231
238
  this._markup = options?.markup !== false;
@@ -290,6 +297,8 @@ export class Console {
290
297
  theme: this._theme,
291
298
  onStyleError: this._onStyleError,
292
299
  colorSystem: this._destination.colorSystem,
300
+ textOrder: this._destination.textOrder,
301
+ terminal: this._terminal,
293
302
  markup: this._markup,
294
303
  highlight: this._highlight,
295
304
  highlighter: this._highlighter,
@@ -495,6 +504,16 @@ export class Console {
495
504
  // stamp, so the next row's must show.
496
505
  this._lastLogTime.set(stamped);
497
506
  }
507
+ /**
508
+ * Sounds the terminal's bell, Rich's `Console.bell`, for whoever is at it: a
509
+ * pipe, a dumb terminal and a capture get nothing. It goes straight to the sink
510
+ * because a bell moves no cursor, and a live region would repaint its frame.
511
+ */
512
+ bell() {
513
+ refuseRenderWrite(this._madeIn);
514
+ if (this._attended && this._capture.get() === null)
515
+ this.file.write("\x07");
516
+ }
498
517
  // [LAW:one-source-of-truth] `RuleOptions` is `Rule`'s, not a restatement of
499
518
  // it. `console.ts` used to declare its own copy and field-by-field forward
500
519
  // into this constructor, which made a second map of one shape — identical
@@ -547,7 +566,7 @@ export class Console {
547
566
  }
548
567
  // --- Export (when record:true) ---
549
568
  exportText({ clear = true } = {}) {
550
- const text = this._recorded.get().map((s) => s.text).join("");
569
+ const text = [...sealed(this._recorded.get(), "plain")].map((s) => s.text).join("");
551
570
  if (clear)
552
571
  this._recorded.set([]);
553
572
  return text;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The census of code points that do not print: every one in Unicode's general
3
+ * categories Cc, Cf, Zl and Zp, and the variation selectors, with what `RichText`
4
+ * does when content carries it. [LAW:one-source-of-truth] The sanitizer
5
+ * (`sanitized`), the wire (`wireText`) and the cell-width measure
6
+ * (`cellLen`) all read this table, so whether content keeps a code point has one
7
+ * statement. `docs/control-codes.md` is each family's use and harm, and what real
8
+ * terminals were measured doing with it.
9
+ *
10
+ * A row does not carry a width. A kept code point takes the cells Rich's table
11
+ * gives it (`cell-widths.ts`), and a second statement of them here would be one
12
+ * that could drift.
13
+ */
14
+ /**
15
+ * What `RichText` does with a code point. `strip` deletes it from content. `keep`
16
+ * holds it as a character. `line-break` is a line ending, so content holds an LF
17
+ * in its place. `pictured` means nothing inside a span, so content holds the
18
+ * stand-in `pictureOf` gives in its place and the reader sees something was there.
19
+ * `withheld` is held in content and measured as nothing, and `wireText` decides
20
+ * from its neighbours whether it is written.
21
+ *
22
+ * Only `keep`, and a `withheld` code point `wireText` lets by, reaches the wire. A
23
+ * raw `Segment` is not content, so what it holds of the others is deleted there.
24
+ */
25
+ export type Disposition = "strip" | "keep" | "line-break" | "pictured" | "withheld";
26
+ /**
27
+ * The code points `first` to `last`, inclusive, and what is done with each. A
28
+ * `pictured` row carries what content shows in place of each of its code points.
29
+ */
30
+ export type ControlCodeRow = {
31
+ readonly first: number;
32
+ readonly last: number;
33
+ readonly disposition: Exclude<Disposition, "pictured">;
34
+ } | {
35
+ readonly first: number;
36
+ readonly last: number;
37
+ readonly disposition: "pictured";
38
+ readonly picture: (codePoint: number) => string;
39
+ };
40
+ /**
41
+ * Ascending and disjoint, every Cc, Cf, Zl, Zp and Variation_Selector code point
42
+ * in exactly one row (`test/core/control-codes.test.ts`).
43
+ *
44
+ * Rich's `Text` strips only `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR) and writes
45
+ * every other code point to the wire. Here a directive reaches the wire only
46
+ * through the library's own constructs: `Style`, a link, `Console.bell`, the paint
47
+ * layer.
48
+ */
49
+ export declare const CONTROL_CODES: readonly ControlCodeRow[];
50
+ /** What content shows in place of `codePoint`, whose disposition is `pictured`. */
51
+ export declare function pictureOf(codePoint: number): string;
52
+ /** A regular expression class of the code points in rows `chosen` picks, for `u`. */
53
+ export declare function controlClass(chosen: (disposition: Disposition) => boolean): string;
54
+ /**
55
+ * UTS #51's subdivision flag, as a `u` source: the black flag, a CLDR subdivision
56
+ * spelled in tag letters and digits, and CANCEL TAG. The one sequence tag
57
+ * characters have a reader for.
58
+ */
59
+ export declare const SUBDIVISION_FLAG = "\uD83C\uDFF4[\uDB40\uDC30-\uDB40\uDC39\uDB40\uDC61-\uDB40\uDC7A]{3,6}\uDB40\uDC7F";
60
+ /** What happens to `codePoint`. One in no row is kept. */
61
+ export declare function dispositionOf(codePoint: number): Disposition;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The census of code points that do not print: every one in Unicode's general
3
+ * categories Cc, Cf, Zl and Zp, and the variation selectors, with what `RichText`
4
+ * does when content carries it. [LAW:one-source-of-truth] The sanitizer
5
+ * (`sanitized`), the wire (`wireText`) and the cell-width measure
6
+ * (`cellLen`) all read this table, so whether content keeps a code point has one
7
+ * statement. `docs/control-codes.md` is each family's use and harm, and what real
8
+ * terminals were measured doing with it.
9
+ *
10
+ * A row does not carry a width. A kept code point takes the cells Rich's table
11
+ * gives it (`cell-widths.ts`), and a second statement of them here would be one
12
+ * that could drift.
13
+ */
14
+ const strip = (first, last) => ({ first, last, disposition: "strip" });
15
+ const keep = (first, last) => ({ first, last, disposition: "keep" });
16
+ const lineBreak = (first, last) => ({ first, last, disposition: "line-break" });
17
+ const pictured = (first, last, picture) => ({ first, last, disposition: "pictured", picture });
18
+ /**
19
+ * The stand-ins. A C0 control's Control Pictures symbol is U+2400 up, DEL's is
20
+ * U+2421. Unicode gives C1 no pictures, so each shows U+2426 SYMBOL FOR SUBSTITUTE:
21
+ * one cell under every East Asian width setting, which U+FFFD is not, and apart
22
+ * from a real decoding error. Each is one code unit, so offsets hold.
23
+ */
24
+ const controlPicture = (codePoint) => String.fromCharCode(0x2400 + codePoint);
25
+ const deletePicture = () => "\u2421";
26
+ const substitute = () => "\u2426";
27
+ const withheld = (first, last) => ({ first, last, disposition: "withheld" });
28
+ /**
29
+ * Ascending and disjoint, every Cc, Cf, Zl, Zp and Variation_Selector code point
30
+ * in exactly one row (`test/core/control-codes.test.ts`).
31
+ *
32
+ * Rich's `Text` strips only `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR) and writes
33
+ * every other code point to the wire. Here a directive reaches the wire only
34
+ * through the library's own constructs: `Style`, a link, `Console.bell`, the paint
35
+ * layer.
36
+ */
37
+ export const CONTROL_CODES = [
38
+ // NUL to BS. BEL's alert is `Console.bell`, and BS's overstrike, bold and
39
+ // underline in nroff output, is `decodeAnsi`: taken a code point at a time, text
40
+ // can turn neither into what it means.
41
+ pictured(0x00, 0x08, controlPicture),
42
+ keep(0x09, 0x0a),
43
+ // VT and FF, mandatory breaks in UAX #14 as LS and PS are.
44
+ lineBreak(0x0b, 0x0c),
45
+ // CR. A CRLF's LF ends its line however the two were chunked, and a CR's
46
+ // overwrite is `decodeAnsi`'s. What is given up is a classic Mac file's lines.
47
+ strip(0x0d, 0x0d),
48
+ // SO and SI switch the terminal's character set; DLE to SUB are transmission
49
+ // controls, and CAN and SUB abort a sequence; ESC begins one, and content that
50
+ // means styling says so with `decodeAnsi`; FS to US are data separators.
51
+ pictured(0x0e, 0x1f, controlPicture),
52
+ // DEL.
53
+ pictured(0x7f, 0x7f, deletePicture),
54
+ // The C1 controls. VTE, WezTerm and xterm.js read U+009B as CSI and U+009D as
55
+ // OSC; xterm, kitty and others ignore them. None has a meaning in text but NEL,
56
+ // which Unicode treats as CR, LF and CRLF are treated (§5.8). A stray one is
57
+ // often windows-1252 decoded as Latin-1, but which, a code point alone cannot
58
+ // say, so mojibake is the caller's to repair. `TextInput` refuses them as keys.
59
+ pictured(0x80, 0x84, substitute),
60
+ lineBreak(0x85, 0x85),
61
+ pictured(0x86, 0x9f, substitute),
62
+ // SOFT HYPHEN: a place a word may break, drawn as a hyphen only where one does.
63
+ // Terminals disagree on its cell, so it never reaches them.
64
+ withheld(0xad, 0xad),
65
+ // The Arabic and Syriac number signs and other prepended concatenation marks,
66
+ // which draw a glyph.
67
+ keep(0x600, 0x605),
68
+ // ARABIC LETTER MARK, a bidi mark like LRM and RLM below: it holds no state,
69
+ // so right-to-left text can use it.
70
+ keep(0x61c, 0x61c),
71
+ // More prepended concatenation marks.
72
+ keep(0x6dd, 0x6dd),
73
+ keep(0x70f, 0x70f),
74
+ keep(0x890, 0x891),
75
+ keep(0x8e2, 0x8e2),
76
+ // The Mongolian free variation selectors.
77
+ withheld(0x180b, 0x180d),
78
+ // MONGOLIAN VOWEL SEPARATOR, which shapes the vowel after it.
79
+ keep(0x180e, 0x180e),
80
+ withheld(0x180f, 0x180f),
81
+ // ZWSP, a place a line may break (`wrap.ts`); ZWNJ, which Persian and Urdu spell
82
+ // with; ZWJ, for emoji and conjuncts; and the bidi marks LRM and RLM, which hold
83
+ // no state, so right-to-left text can use them. ZWJ's joining is Rich's width
84
+ // rule, which `cells.ts` holds apart from this row.
85
+ keep(0x200b, 0x200f),
86
+ // LINE SEPARATOR and PARAGRAPH SEPARATOR.
87
+ lineBreak(0x2028, 0x2029),
88
+ // The bidi embeddings and overrides with their closer PDF, and the isolates
89
+ // below. Content holds them; what reaches the wire is `sealed`'s to decide.
90
+ keep(0x202a, 0x202e),
91
+ // WORD JOINER, which only asks that nothing break here, and the invisible
92
+ // mathematical operators.
93
+ keep(0x2060, 0x2064),
94
+ // The bidi isolates with their closer PDI.
95
+ keep(0x2066, 0x2069),
96
+ // The deprecated shaping and swapping switches. Each holds state to the end of
97
+ // the text, Unicode deprecates them, and no terminal was found honouring one.
98
+ strip(0x206a, 0x206f),
99
+ // VARIATION SELECTOR-1 to 16: glyph variants, and text or emoji presentation.
100
+ withheld(0xfe00, 0xfe0f),
101
+ // ZERO WIDTH NO-BREAK SPACE, the byte order mark, as a word joiner.
102
+ keep(0xfeff, 0xfeff),
103
+ // Interlinear annotation. No terminal draws ruby, the annotating text stays
104
+ // visible, and the anchors hold no state, so there is nothing to contain.
105
+ keep(0xfff9, 0xfffb),
106
+ // The Kaithi number signs, which draw a glyph.
107
+ keep(0x110bd, 0x110bd),
108
+ keep(0x110cd, 0x110cd),
109
+ // Egyptian hieroglyph, shorthand and musical format controls.
110
+ keep(0x13430, 0x1343f),
111
+ keep(0x1bca0, 0x1bca3),
112
+ keep(0x1d173, 0x1d17a),
113
+ // LANGUAGE TAG, deprecated.
114
+ strip(0xe0001, 0xe0001),
115
+ // The tag characters: an emoji subdivision flag, or text no reader can see.
116
+ withheld(0xe0020, 0xe007f),
117
+ // The ideographic variation selectors: Japanese and Chinese glyph variants.
118
+ withheld(0xe0100, 0xe01ef),
119
+ ];
120
+ /** What content shows in place of `codePoint`, whose disposition is `pictured`. */
121
+ export function pictureOf(codePoint) {
122
+ const row = rowOf(codePoint);
123
+ if (row?.disposition !== "pictured")
124
+ throw new RangeError(`U+${codePoint.toString(16).padStart(4, "0")} is not pictured`);
125
+ return row.picture(codePoint);
126
+ }
127
+ /** A regular expression class of the code points in rows `chosen` picks, for `u`. */
128
+ export function controlClass(chosen) {
129
+ const hex = (codePoint) => `\\u{${codePoint.toString(16)}}`;
130
+ return `[${CONTROL_CODES.filter((row) => chosen(row.disposition)).map((row) => `${hex(row.first)}-${hex(row.last)}`).join("")}]`;
131
+ }
132
+ /**
133
+ * UTS #51's subdivision flag, as a `u` source: the black flag, a CLDR subdivision
134
+ * spelled in tag letters and digits, and CANCEL TAG. The one sequence tag
135
+ * characters have a reader for.
136
+ */
137
+ export const SUBDIVISION_FLAG = "\u{1f3f4}[\u{e0030}-\u{e0039}\u{e0061}-\u{e007a}]{3,6}\u{e007f}";
138
+ /** What happens to `codePoint`. One in no row is kept. */
139
+ export function dispositionOf(codePoint) {
140
+ return rowOf(codePoint)?.disposition ?? "keep";
141
+ }
142
+ /** The row holding `codePoint`, if any. */
143
+ function rowOf(codePoint) {
144
+ let low = 0;
145
+ let high = CONTROL_CODES.length - 1;
146
+ while (low <= high) {
147
+ const mid = (low + high) >> 1;
148
+ const row = CONTROL_CODES[mid];
149
+ if (codePoint < row.first)
150
+ high = mid - 1;
151
+ else if (codePoint > row.last)
152
+ low = mid + 1;
153
+ else
154
+ return row;
155
+ }
156
+ return undefined;
157
+ }
@@ -14,3 +14,9 @@
14
14
  * `NodeJS` namespace. `test/seam/browser-types.test.ts` holds that line.
15
15
  */
16
16
  export type Env = Readonly<Record<string, string | undefined>>;
17
+ /**
18
+ * A terminal that says it cannot be drawn on: `TERM` is `dumb` or `unknown`,
19
+ * as Emacs' shell mode sets it. It takes no colour and nothing that moves the
20
+ * cursor.
21
+ */
22
+ export declare function isDumbTerminal(env: Env): boolean;
package/dist/core/env.js CHANGED
@@ -13,4 +13,12 @@
13
13
  * without `@types/node` — the ordinary browser project — cannot resolve the
14
14
  * `NodeJS` namespace. `test/seam/browser-types.test.ts` holds that line.
15
15
  */
16
- export {};
16
+ /**
17
+ * A terminal that says it cannot be drawn on: `TERM` is `dumb` or `unknown`,
18
+ * as Emacs' shell mode sets it. It takes no colour and nothing that moves the
19
+ * cursor.
20
+ */
21
+ export function isDumbTerminal(env) {
22
+ const term = env["TERM"];
23
+ return term === "dumb" || term === "unknown";
24
+ }
@@ -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
@@ -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[];
@@ -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
  *
@@ -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;
@@ -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,10 +132,11 @@ 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
@@ -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
@@ -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 Directed {
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 directed(line: readonly Segment[], order: TextOrder): Directed;
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 labelled(before: string, label: string, after: string, style: Style | undefined, order: TextOrder | undefined): Segment[];