@loomcli/core 0.2.0 → 0.4.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 (51) hide show
  1. package/dist/application.d.ts +59 -34
  2. package/dist/application.js +162 -59
  3. package/dist/chain.d.ts +25 -6
  4. package/dist/chain.js +99 -15
  5. package/dist/command.d.ts +146 -42
  6. package/dist/command.js +642 -78
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -59
  10. package/dist/errors.js +49 -106
  11. package/dist/extension.d.ts +80 -20
  12. package/dist/extension.js +115 -30
  13. package/dist/globals.d.ts +14 -25
  14. package/dist/globals.js +10 -61
  15. package/dist/glyphs.generated.d.ts +464 -0
  16. package/dist/glyphs.generated.js +491 -0
  17. package/dist/host.js +2 -1
  18. package/dist/index.d.ts +15 -6
  19. package/dist/index.js +5 -2
  20. package/dist/inspect.d.ts +38 -4
  21. package/dist/inspect.js +69 -6
  22. package/dist/lanes.d.ts +26 -0
  23. package/dist/lanes.js +45 -0
  24. package/dist/output.d.ts +93 -15
  25. package/dist/output.js +307 -34
  26. package/dist/plugin.d.ts +32 -16
  27. package/dist/plugin.js +46 -18
  28. package/dist/rendering.d.ts +21 -0
  29. package/dist/rendering.js +72 -0
  30. package/dist/sequence.d.ts +41 -0
  31. package/dist/sequence.js +225 -0
  32. package/dist/style-ansi.d.ts +13 -0
  33. package/dist/style-ansi.js +306 -0
  34. package/dist/style-layout.d.ts +29 -0
  35. package/dist/style-layout.js +228 -0
  36. package/dist/style-resolve.d.ts +6 -0
  37. package/dist/style-resolve.js +26 -0
  38. package/dist/style-state.d.ts +14 -0
  39. package/dist/style-state.js +179 -0
  40. package/dist/style-wire.d.ts +31 -0
  41. package/dist/style-wire.js +201 -0
  42. package/dist/style.d.ts +86 -0
  43. package/dist/style.js +201 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +22 -0
  46. package/dist/types.d.ts +174 -21
  47. package/dist/validation.d.ts +8 -1
  48. package/dist/validation.js +17 -2
  49. package/dist/view.d.ts +180 -0
  50. package/dist/view.js +307 -0
  51. package/package.json +2 -1
@@ -0,0 +1,21 @@
1
+ import type { Host } from './types.js';
2
+ type SwitchPolicy = 'auto' | 'always' | 'never';
3
+ interface RenderingPolicy {
4
+ color?: SwitchPolicy | undefined;
5
+ modifiers?: SwitchPolicy | undefined;
6
+ hyperlinks?: SwitchPolicy | undefined;
7
+ terminalControls?: 'strip' | 'preserve' | undefined;
8
+ }
9
+ interface Capabilities {
10
+ color: boolean;
11
+ modifiers: boolean;
12
+ hyperlinks: boolean;
13
+ terminalControls: boolean;
14
+ depth: 16 | 256 | 'truecolor';
15
+ mainGlyphs: boolean;
16
+ }
17
+ declare function renderingPolicy(value: unknown): RenderingPolicy;
18
+ /** A run owns captured facts; stdout and stderr each resolve this one policy independently. */
19
+ declare function capabilities(host: Host, destination: 'stdout' | 'stderr', policy: RenderingPolicy): Capabilities;
20
+ export type { Capabilities, RenderingPolicy };
21
+ export { capabilities, renderingPolicy };
@@ -0,0 +1,72 @@
1
+ import { DeclarationError } from './errors.js';
2
+ import { isPlainObject } from './facts.js';
3
+ function renderingPolicy(value) {
4
+ if (value === undefined) {
5
+ return {};
6
+ }
7
+ if (!isPlainObject(value)) {
8
+ throw new DeclarationError('The rendering policy must be an object.');
9
+ }
10
+ const result = {};
11
+ for (const field of ['color', 'modifiers', 'hyperlinks']) {
12
+ const policy = value[field];
13
+ if (policy !== undefined && policy !== 'auto' && policy !== 'always' && policy !== 'never') {
14
+ throw new DeclarationError(`Rendering ${field} must be auto, always, or never.`);
15
+ }
16
+ if (policy !== undefined) {
17
+ result[field] = policy;
18
+ }
19
+ }
20
+ const controls = value.terminalControls;
21
+ if (controls !== undefined && controls !== 'strip' && controls !== 'preserve') {
22
+ throw new DeclarationError('Rendering terminalControls must be strip or preserve.');
23
+ }
24
+ if (controls !== undefined) {
25
+ result.terminalControls = controls;
26
+ }
27
+ return result;
28
+ }
29
+ function colorDepth(env) {
30
+ if (env.COLORTERM === 'truecolor' ||
31
+ env.COLORTERM === '24bit' ||
32
+ ['xterm-kitty', 'xterm-ghostty', 'wezterm'].includes(env.TERM ?? '')) {
33
+ return 'truecolor';
34
+ }
35
+ if (env.TERM_PROGRAM === 'iTerm.app') {
36
+ const major = /^\d+/u.exec(env.TERM_PROGRAM_VERSION ?? '')?.[0];
37
+ return major !== undefined && Number(major) >= 3 ? 'truecolor' : 256;
38
+ }
39
+ return env.TERM_PROGRAM === 'Apple_Terminal' || /-256(?:color)?$/iu.test(env.TERM ?? '')
40
+ ? 256
41
+ : 16;
42
+ }
43
+ function mainGlyphs({ env, platform }) {
44
+ if (platform !== 'win32') {
45
+ return env.TERM !== 'linux';
46
+ }
47
+ return (Boolean(env.CI || env.WT_SESSION || env.TERMINUS_SUBLIME) ||
48
+ env.ConEmuTask === '{cmd::Cmder}' ||
49
+ env.TERM_PROGRAM === 'Terminus-Sublime' ||
50
+ env.TERM_PROGRAM === 'vscode' ||
51
+ env.TERM === 'xterm-256color' ||
52
+ env.TERM === 'alacritty' ||
53
+ env.TERMINAL_EMULATOR === 'JetBrains-JediTerm');
54
+ }
55
+ function enabled(policy, automatic) {
56
+ return policy === 'always' || (policy !== 'never' && automatic);
57
+ }
58
+ /** A run owns captured facts; stdout and stderr each resolve this one policy independently. */
59
+ function capabilities(host, destination, policy) {
60
+ const tty = host.terminal[destination].isTTY;
61
+ const capable = tty && host.env.TERM !== 'dumb';
62
+ const forced = Boolean(host.env.FORCE_COLOR);
63
+ return {
64
+ color: enabled(policy.color, forced || (!host.env.NO_COLOR && capable)),
65
+ depth: colorDepth(host.env),
66
+ hyperlinks: enabled(policy.hyperlinks, tty),
67
+ mainGlyphs: mainGlyphs(host),
68
+ modifiers: enabled(policy.modifiers, forced || capable),
69
+ terminalControls: policy.terminalControls === 'preserve',
70
+ };
71
+ }
72
+ export { capabilities, renderingPolicy };
@@ -0,0 +1,41 @@
1
+ import type { IncompleteResult } from './lanes.js';
2
+ import type { ViewContext } from './types.js';
3
+ import type { ResolvedRowView } from './view.js';
4
+ /**
5
+ * The view one sequence writes through. A row view writes each row as the source yields it; a
6
+ * whole view collects every row and renders once at the end of the source.
7
+ */
8
+ type SequenceView<Row> = {
9
+ kind: 'rows';
10
+ view: ResolvedRowView<Row>;
11
+ } | {
12
+ kind: 'whole';
13
+ render: (rows: readonly Row[], context: ViewContext) => unknown;
14
+ };
15
+ /** What one sequence writes through, supplied by the output that reserved its place. */
16
+ interface SequenceWriter<Row> {
17
+ /** The context of the destination the pieces reach. */
18
+ context: ViewContext;
19
+ /** Ends the reservation, so later output to that destination writes after this sequence. */
20
+ close: () => void;
21
+ /** Writes the incomplete-result line on stderr, before the fault's own report. */
22
+ incomplete: (facts: IncompleteResult) => void;
23
+ path: readonly string[];
24
+ /** Resolves and queues one piece on the reserved place, rejecting on a view or write fault. */
25
+ piece: (produce: () => unknown) => Promise<void>;
26
+ signal: AbortSignal;
27
+ source: Iterable<Row> | AsyncIterable<Row>;
28
+ /** Records a source failure, which this invocation reports after its primary outcome. */
29
+ stopped: (cause: unknown) => void;
30
+ view: SequenceView<Row>;
31
+ }
32
+ /** One sequence in flight: the promise its call answers with, and the switch that ends it early. */
33
+ interface LiveSequence {
34
+ pending: Promise<void>;
35
+ /** Ends the sequence where it next would go on. A finished sequence answers it with nothing. */
36
+ stop: () => void;
37
+ }
38
+ /** One sequence, started here and stoppable from the invocation that issued it. */
39
+ declare function writeSequence<Row>(writer: SequenceWriter<Row>): LiveSequence;
40
+ export type { LiveSequence, SequenceView, SequenceWriter };
41
+ export { writeSequence };
@@ -0,0 +1,225 @@
1
+ import { InternalError, routedSubject } from './errors.js';
2
+ /**
3
+ * A failure the source raised, so the writer tells it apart from the view and write faults its own
4
+ * pieces raise, which the output path has accounted for already.
5
+ */
6
+ class SourceFault {
7
+ cause;
8
+ constructor(cause) {
9
+ this.cause = cause;
10
+ }
11
+ }
12
+ /**
13
+ * The value a stopped writer unwinds with, compared by identity where the sequence ends. The
14
+ * invocation that stopped it has a primary failure of its own, so this value reports nothing.
15
+ */
16
+ const stopRequested = Symbol('stopped');
17
+ /**
18
+ * The steps one source answers with, under whichever protocol it carries. The asynchronous one
19
+ * answers first, as the language's own `for await` does.
20
+ * A value that iterates neither way answers with nothing, and its caller reports the fault.
21
+ */
22
+ function iterate(source) {
23
+ const asynchronous = source?.[Symbol.asyncIterator];
24
+ if (typeof asynchronous === 'function') {
25
+ return asynchronous.call(source);
26
+ }
27
+ const synchronous = source?.[Symbol.iterator];
28
+ if (typeof synchronous === 'function') {
29
+ return synchronous.call(source);
30
+ }
31
+ return undefined;
32
+ }
33
+ /**
34
+ * The steps one source answers with, with whatever the protocol raised marked as the source's own
35
+ * failure. A value that iterates neither way is the same fault, named for the Command that emitted
36
+ * it, because the types reject it and a JavaScript author alone reaches it.
37
+ */
38
+ function stepsOf(writer) {
39
+ let steps = undefined;
40
+ try {
41
+ steps = iterate(writer.source);
42
+ }
43
+ catch (error) {
44
+ throw new SourceFault(error);
45
+ }
46
+ if (!steps) {
47
+ throw new SourceFault(new InternalError(`The result of ${routedSubject(writer.path)} is not iterable.`, undefined));
48
+ }
49
+ return steps;
50
+ }
51
+ /** One request of the source, with whatever it raised marked as the source's own failure. */
52
+ async function pull(steps) {
53
+ try {
54
+ const result = await steps.next();
55
+ return result.done === true ? { done: true } : { done: false, value: result.value };
56
+ }
57
+ catch (error) {
58
+ throw new SourceFault(error);
59
+ }
60
+ }
61
+ /**
62
+ * The stop, read before the writer requests the next row and before it writes the piece it holds.
63
+ * A cancelled run stops here too, so core requests no further rows and writes no further pieces
64
+ * from the moment the signal aborted.
65
+ */
66
+ function halt(reading) {
67
+ if (reading.live.stopped || reading.signal.aborted) {
68
+ throw stopRequested;
69
+ }
70
+ }
71
+ /** One step, counted where the source produced a row. A stopped writer requests none. */
72
+ async function step(reading) {
73
+ halt(reading);
74
+ const result = await pull(reading.steps);
75
+ if (!result.done) {
76
+ reading.counts.yielded += 1;
77
+ }
78
+ // A row an in-flight request delivered after the stop is counted, and nothing is written for it.
79
+ halt(reading);
80
+ return result;
81
+ }
82
+ /**
83
+ * The source is told the writer wants no more rows, so a generator runs its own cleanup.
84
+ * The settlement is never awaited: a cleanup that never settles would pin the destination's tail,
85
+ * and the failure that stopped the sequence would never be reported.
86
+ * Whatever that cleanup raises is the source's business, and it is observed here alone, because the
87
+ * stop is reported through the fault that raised it and a second failure would replace it.
88
+ */
89
+ function endSteps(steps) {
90
+ try {
91
+ const ended = steps.return?.();
92
+ void Promise.resolve(ended).catch(() => undefined);
93
+ }
94
+ catch {
95
+ // A cleanup that throws outright is the same business, and it ends here too.
96
+ }
97
+ }
98
+ /** The opening piece, skipped where the view supplies no `head` function. */
99
+ async function writeHead(writer, produce) {
100
+ if (produce) {
101
+ await writer.piece(() => produce(writer.context));
102
+ }
103
+ }
104
+ /** The closing piece, skipped where the view supplies no `tail` function. */
105
+ async function writeTail(writer, produce, count) {
106
+ if (produce) {
107
+ await writer.piece(() => produce(count, writer.context));
108
+ }
109
+ }
110
+ /**
111
+ * Every piece but `tail`: `head`, then each row as the source yields it. Each piece is awaited
112
+ * before the next row is requested, so a slow destination applies back-pressure to the source.
113
+ */
114
+ async function writeEachRow(writer, view, reading) {
115
+ halt(reading);
116
+ await writeHead(writer, view.head);
117
+ for (;;) {
118
+ const next = await step(reading);
119
+ if (next.done) {
120
+ return;
121
+ }
122
+ const index = reading.counts.yielded - 1;
123
+ await writer.piece(() => view.row(next.value, index, writer.context));
124
+ reading.counts.written += 1;
125
+ }
126
+ }
127
+ /** Every row the source produced, which a whole view renders once at the end of the source. */
128
+ async function collectRows(reading) {
129
+ const collected = [];
130
+ for (;;) {
131
+ const next = await step(reading);
132
+ if (next.done) {
133
+ return collected;
134
+ }
135
+ collected.push(next.value);
136
+ }
137
+ }
138
+ /**
139
+ * Every piece the source decides, and the closing piece a complete sequence ends with: `head` and
140
+ * each row under a row view, whose closing piece is `tail`, and the collected rows under a whole
141
+ * view, whose closing piece is its one render. A whole view queues nothing before the source ends,
142
+ * so `written` counts no row under it.
143
+ */
144
+ async function writePieces(writer, reading) {
145
+ if (writer.view.kind === 'whole') {
146
+ const { render } = writer.view;
147
+ const collected = await collectRows(reading);
148
+ return () => writer.piece(() => render(collected, writer.context));
149
+ }
150
+ const { view } = writer.view;
151
+ await writeEachRow(writer, view, reading);
152
+ return () => writeTail(writer, view.tail, reading.counts.written);
153
+ }
154
+ /** The same pieces, with the source told to stop where one of them raised. */
155
+ async function closingPiece(writer, reading) {
156
+ try {
157
+ return await writePieces(writer, reading);
158
+ }
159
+ catch (error) {
160
+ endSteps(reading.steps);
161
+ throw error;
162
+ }
163
+ }
164
+ /**
165
+ * The sequence itself: every piece the source decides, then the closing piece. Every piece is
166
+ * awaited before the next row is requested, so a slow destination applies back-pressure to the
167
+ * source. The stop is read once more between the source's end and the closing piece, so a run
168
+ * cancelled there, and an action that failed there, never write it and a truncated result never
169
+ * reads as a complete one.
170
+ */
171
+ async function writeRows(writer, live, counts) {
172
+ const reading = { counts, live, signal: writer.signal, steps: stepsOf(writer) };
173
+ const close = await closingPiece(writer, reading);
174
+ if (live.stopped || writer.signal.aborted) {
175
+ return false;
176
+ }
177
+ await close();
178
+ return true;
179
+ }
180
+ /**
181
+ * One sequence written on the place its call reserved. A stop before the end retracts nothing, adds
182
+ * no `tail`, and writes one incomplete-result line on stderr before the fault's own report; a
183
+ * source failure is recorded there, because a call the action never awaited observes none.
184
+ */
185
+ async function runSequence(writer, live) {
186
+ const counts = { written: 0, yielded: 0 };
187
+ try {
188
+ if (!(await writeRows(writer, live, counts))) {
189
+ writer.incomplete({ path: writer.path, ...counts });
190
+ }
191
+ }
192
+ catch (error) {
193
+ writer.incomplete({ path: writer.path, ...counts });
194
+ raise(writer, error);
195
+ }
196
+ finally {
197
+ writer.close();
198
+ }
199
+ }
200
+ /**
201
+ * What one stop leaves behind once its line is written: nothing, where the invocation asked for
202
+ * the stop and reports its own failure, the source's failure recorded and raised, or the fault of
203
+ * a piece, which the output path has accounted for already.
204
+ */
205
+ function raise(writer, error) {
206
+ if (error === stopRequested) {
207
+ return;
208
+ }
209
+ if (error instanceof SourceFault) {
210
+ writer.stopped(error.cause);
211
+ throw error.cause;
212
+ }
213
+ throw error;
214
+ }
215
+ /** One sequence, started here and stoppable from the invocation that issued it. */
216
+ function writeSequence(writer) {
217
+ const live = { stopped: false };
218
+ return {
219
+ pending: runSequence(writer, live),
220
+ stop: () => {
221
+ live.stopped = true;
222
+ },
223
+ };
224
+ }
225
+ export { writeSequence };
@@ -0,0 +1,13 @@
1
+ import type { Capabilities } from './rendering.js';
2
+ import type { Attributes } from './style-state.js';
3
+ import type { Boundary, ParsedText } from './style-wire.js';
4
+ interface TextUnit {
5
+ kind: 'text' | 'control' | 'hyperlink' | 'sgr';
6
+ text: string;
7
+ attributes: Attributes;
8
+ }
9
+ type Unit = TextUnit | Boundary;
10
+ /** Scans the full decoded stream before emitting padding or generated terminal bytes. */
11
+ declare function scanAnsi(parsed: ParsedText, caps: Capabilities): Unit[];
12
+ export type { TextUnit, Unit };
13
+ export { scanAnsi };
@@ -0,0 +1,306 @@
1
+ import { applyOperations, emptyAttributes } from './style-state.js';
2
+ import { colors, isColorName } from './style.js';
3
+ /** Control strings are opaque through their terminator; payload bytes are never rescanned. */
4
+ function controlString(text, start, osc) {
5
+ for (let cursor = start; cursor < text.length; cursor += 1) {
6
+ const character = text.charCodeAt(cursor);
7
+ if ((osc && character === 7) || character === 156) {
8
+ return { body: text.slice(start, cursor), end: cursor + 1, kind: osc ? 'osc' : 'control' };
9
+ }
10
+ if (character === 27 && text[cursor + 1] === '\\') {
11
+ return { body: text.slice(start, cursor), end: cursor + 2, kind: osc ? 'osc' : 'control' };
12
+ }
13
+ }
14
+ return { body: '', end: text.length, kind: 'incomplete' };
15
+ }
16
+ function csi(text, start) {
17
+ for (let cursor = start; cursor < text.length; cursor += 1) {
18
+ const character = text.charCodeAt(cursor);
19
+ if (character >= 64 && character <= 126) {
20
+ return {
21
+ body: text.slice(start, cursor),
22
+ end: cursor + 1,
23
+ kind: character === 109 ? 'sgr' : 'control',
24
+ };
25
+ }
26
+ if (character < 32 || character > 63) {
27
+ return { body: '', end: cursor, kind: 'incomplete' };
28
+ }
29
+ }
30
+ return { body: '', end: text.length, kind: 'incomplete' };
31
+ }
32
+ function command(text, cursor) {
33
+ const code = text.charCodeAt(cursor);
34
+ if (code === 27) {
35
+ const next = text[cursor + 1];
36
+ if (next === '[') {
37
+ return csi(text, cursor + 2);
38
+ }
39
+ if (next === ']') {
40
+ return controlString(text, cursor + 2, true);
41
+ }
42
+ if (next === 'P' || next === 'X' || next === '^' || next === '_') {
43
+ return controlString(text, cursor + 2, false);
44
+ }
45
+ for (let end = cursor + 1; end < text.length; end += 1) {
46
+ const value = text.charCodeAt(end);
47
+ if (value >= 48 && value <= 126) {
48
+ return { body: '', end: end + 1, kind: 'control' };
49
+ }
50
+ if (value < 32 || value > 47) {
51
+ return { body: '', end, kind: 'incomplete' };
52
+ }
53
+ }
54
+ return { body: '', end: text.length, kind: 'incomplete' };
55
+ }
56
+ if (code === 155) {
57
+ return csi(text, cursor + 1);
58
+ }
59
+ if (code === 157) {
60
+ return controlString(text, cursor + 1, true);
61
+ }
62
+ if (code === 144 || code === 152 || code === 158 || code === 159) {
63
+ return controlString(text, cursor + 1, false);
64
+ }
65
+ if ((code < 32 && code !== 9 && code !== 10 && !(code === 13 && text[cursor + 1] === '\n')) ||
66
+ (code >= 127 && code <= 159)) {
67
+ return { body: '', end: cursor + 1, kind: 'control' };
68
+ }
69
+ return undefined;
70
+ }
71
+ const enableModifiers = new Map([
72
+ [1, 'bold'],
73
+ [2, 'faint'],
74
+ [3, 'italic'],
75
+ [4, 'underline'],
76
+ [7, 'inverse'],
77
+ [8, 'hidden'],
78
+ [9, 'strikethrough'],
79
+ [53, 'overline'],
80
+ ]);
81
+ const disableModifiers = new Map([
82
+ [22, ['bold', 'faint']],
83
+ [23, ['italic']],
84
+ [24, ['underline']],
85
+ [27, ['inverse']],
86
+ [28, ['hidden']],
87
+ [29, ['strikethrough']],
88
+ [55, ['overline']],
89
+ ]);
90
+ /** Raw ANSI overrides participate in Loom's scoped attributes, and resets restore their base. */
91
+ class AnsiState {
92
+ base = emptyAttributes;
93
+ foreground;
94
+ background;
95
+ modifiers = new Map();
96
+ scope;
97
+ computed;
98
+ saved = new WeakMap();
99
+ palette;
100
+ constructor(root, palette) {
101
+ this.palette = palette;
102
+ this.scope = root;
103
+ this.saved.set(root, emptyAttributes);
104
+ }
105
+ sync(scope) {
106
+ if (scope === this.scope) {
107
+ return;
108
+ }
109
+ this.saved.set(this.scope, this.attributes());
110
+ const entering = [];
111
+ let ancestor = scope;
112
+ while (ancestor && !this.saved.has(ancestor)) {
113
+ entering.push(ancestor);
114
+ ancestor = ancestor.parent;
115
+ }
116
+ let effective = ancestor ? (this.saved.get(ancestor) ?? emptyAttributes) : emptyAttributes;
117
+ for (let index = entering.length - 1; index >= 0; index -= 1) {
118
+ const next = entering[index];
119
+ if (next) {
120
+ effective = applyOperations(effective, next.operations, this.palette);
121
+ this.saved.set(next, effective);
122
+ }
123
+ }
124
+ this.scope = scope;
125
+ this.base = scope.attributes;
126
+ this.foreground = effective.foreground;
127
+ this.background = effective.background;
128
+ this.modifiers.clear();
129
+ for (const key of this.base.modifiers) {
130
+ this.modifiers.set(key, false);
131
+ }
132
+ for (const key of effective.modifiers) {
133
+ this.modifiers.set(key, true);
134
+ }
135
+ this.computed = undefined;
136
+ }
137
+ attributes() {
138
+ if (this.computed !== undefined) {
139
+ return this.computed;
140
+ }
141
+ if (this.foreground === undefined &&
142
+ this.background === undefined &&
143
+ this.modifiers.size === 0) {
144
+ return this.base;
145
+ }
146
+ const modifiers = new Set(this.base.modifiers);
147
+ for (const [name, on] of this.modifiers) {
148
+ if (on) {
149
+ modifiers.add(name);
150
+ }
151
+ else {
152
+ modifiers.delete(name);
153
+ }
154
+ }
155
+ this.computed = {
156
+ background: this.background ?? this.base.background,
157
+ foreground: this.foreground ?? this.base.foreground,
158
+ modifiers,
159
+ };
160
+ return this.computed;
161
+ }
162
+ sgr(body) {
163
+ this.computed = undefined;
164
+ if (!/^[\d;:]*$/u.test(body)) {
165
+ return '';
166
+ }
167
+ const parameters = body.split(';');
168
+ const unknown = [];
169
+ for (let index = 0; index < parameters.length; index += 1) {
170
+ const raw = parameters[index] ?? '';
171
+ const colon = raw.split(':');
172
+ const code = Number(colon[0]);
173
+ if (code === 0) {
174
+ this.foreground = undefined;
175
+ this.background = undefined;
176
+ this.modifiers.clear();
177
+ continue;
178
+ }
179
+ if (code === 39 || code === 49) {
180
+ this[code === 39 ? 'foreground' : 'background'] = undefined;
181
+ continue;
182
+ }
183
+ const enabled = enableModifiers.get(code);
184
+ if (enabled) {
185
+ this.modifiers.set(enabled, true);
186
+ continue;
187
+ }
188
+ const disabled = disableModifiers.get(code);
189
+ if (disabled) {
190
+ for (const name of disabled) {
191
+ this.modifiers.delete(name);
192
+ }
193
+ continue;
194
+ }
195
+ const background = (code >= 40 && code <= 47) || (code >= 100 && code <= 107) || code === 48;
196
+ const paletteIndex = code >= 30 && code <= 47
197
+ ? code - (background ? 40 : 30)
198
+ : code - (background ? 100 : 90) + 8;
199
+ if ((code >= 30 && code <= 37) ||
200
+ (code >= 40 && code <= 47) ||
201
+ (code >= 90 && code <= 97) ||
202
+ (code >= 100 && code <= 107)) {
203
+ const name = Object.keys(colors).find((key) => isColorName(key) && colors[key] === paletteIndex);
204
+ if (isColorName(name)) {
205
+ this[background ? 'background' : 'foreground'] = name;
206
+ }
207
+ continue;
208
+ }
209
+ if (code === 38 || code === 48) {
210
+ const mode = Number(colon.length > 1 ? colon[1] : parameters[index + 1]);
211
+ let count = 0;
212
+ if (mode === 2) {
213
+ count = 3;
214
+ }
215
+ else if (mode === 5) {
216
+ count = 1;
217
+ }
218
+ const values = colon.length > 1
219
+ ? colon.slice(colon.length - count).map(Number)
220
+ : parameters.slice(index + 2, index + 2 + count).map(Number);
221
+ if (colon.length === 1) {
222
+ index += 1 + count;
223
+ }
224
+ if (values.length !== count ||
225
+ !values.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) {
226
+ continue;
227
+ }
228
+ const [red, green, blue] = values;
229
+ let color = undefined;
230
+ if (mode === 5 && red !== undefined) {
231
+ color = ['ansi256', red];
232
+ }
233
+ else if (mode === 2 && red !== undefined && green !== undefined && blue !== undefined) {
234
+ color = ['rgb', red, green, blue];
235
+ }
236
+ if (color !== undefined) {
237
+ this[background ? 'background' : 'foreground'] = color;
238
+ }
239
+ continue;
240
+ }
241
+ unknown.push(raw);
242
+ }
243
+ return unknown.length ? `\u001b[${unknown.join(';')}m` : '';
244
+ }
245
+ }
246
+ /** Scans the full decoded stream before emitting padding or generated terminal bytes. */
247
+ function scanAnsi(parsed, caps) {
248
+ const result = [];
249
+ const state = new AnsiState(parsed.root, parsed.palette);
250
+ let hyperlinkOpen = false;
251
+ function boundaries(start, end) {
252
+ for (let cursor = start; cursor < end; cursor += 1) {
253
+ for (const boundary of parsed.boundaries.get(cursor) ?? []) {
254
+ if (boundary.kind === 'open') {
255
+ state.sync(boundary.scope);
256
+ result.push({ ...boundary, attributes: state.attributes() });
257
+ }
258
+ else {
259
+ result.push(boundary);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ for (let cursor = 0; cursor < parsed.text.length;) {
265
+ state.sync(parsed.scopes[cursor] ?? parsed.root);
266
+ const found = command(parsed.text, cursor);
267
+ if (found) {
268
+ boundaries(cursor, found.end);
269
+ state.sync(parsed.scopes[found.end - 1] ?? parsed.root);
270
+ const raw = parsed.text.slice(cursor, found.end);
271
+ if (found.kind === 'sgr') {
272
+ const unknown = state.sgr(found.body);
273
+ if (caps.terminalControls && unknown) {
274
+ result.push({ attributes: state.attributes(), kind: 'sgr', text: unknown });
275
+ }
276
+ }
277
+ else if (found.kind === 'osc' && found.body.startsWith('8;')) {
278
+ const separator = found.body.indexOf(';', 2);
279
+ if (separator !== -1 && caps.hyperlinks) {
280
+ hyperlinkOpen = found.body.slice(separator + 1) !== '';
281
+ result.push({ attributes: state.attributes(), kind: 'hyperlink', text: raw });
282
+ }
283
+ }
284
+ else if (found.kind !== 'incomplete' && caps.terminalControls) {
285
+ result.push({ attributes: state.attributes(), kind: 'control', text: raw });
286
+ }
287
+ cursor = found.end;
288
+ continue;
289
+ }
290
+ boundaries(cursor, cursor + 1);
291
+ state.sync(parsed.scopes[cursor] ?? parsed.root);
292
+ const character = parsed.text[cursor] ?? '';
293
+ // The LF owns the complete line-ending atom, so deferred spaces cannot split CRLF.
294
+ if (!(character === '\r' && parsed.text[cursor + 1] === '\n')) {
295
+ const text = character === '\n' && parsed.text[cursor - 1] === '\r' ? '\r\n' : character;
296
+ result.push({ attributes: state.attributes(), kind: 'text', text });
297
+ }
298
+ cursor += 1;
299
+ }
300
+ boundaries(parsed.text.length, parsed.text.length + 1);
301
+ if (hyperlinkOpen) {
302
+ result.push({ attributes: emptyAttributes, kind: 'hyperlink', text: '\u001b]8;;\u001b\\' });
303
+ }
304
+ return result;
305
+ }
306
+ export { scanAnsi };