@orkestrel/console 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/src/browser/index.d.ts +47 -38
- package/dist/src/browser/index.js +27 -18
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +121 -82
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +263 -174
- package/dist/src/core/index.d.ts +263 -174
- package/dist/src/core/index.js +121 -82
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +64 -45
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +115 -80
- package/dist/src/server/index.d.ts +115 -80
- package/dist/src/server/index.js +64 -45
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -16
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @orkestrel/console
|
|
2
2
|
|
|
3
|
-
A unified output-control system for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(observable lifecycle), reusing it as it takes shape.
|
|
3
|
+
> A unified output-control system for a terminal, a browser, and a server: a style engine over
|
|
4
|
+
> frozen `Style` data, structured logging whose record and `entry` event are the transport seam,
|
|
5
|
+
> narrative reporting, console and stream capture, and live animations — one engine, environment
|
|
6
|
+
> sinks, with the platform backend swapped at the `Sink` seam.
|
|
7
|
+
|
|
8
|
+
Install the package, build a `Logger` or a `Reporter`, and swap its `sink` to move the same code
|
|
9
|
+
between a terminal, a browser, and a server. Part of the `@orkestrel` line, built to sit beside
|
|
10
|
+
`@orkestrel/emitter` (observable lifecycle), reusing it as it takes shape.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { Attribute } from '@orkestrel/console';
|
|
2
|
-
import { Color } from '@orkestrel/console';
|
|
3
|
-
import { SinkInterface } from '@orkestrel/console';
|
|
1
|
+
import type { Attribute } from '@orkestrel/console';
|
|
2
|
+
import type { Color } from '@orkestrel/console';
|
|
3
|
+
import type { SinkInterface } from '@orkestrel/console';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Translates an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
|
|
7
|
-
* `%c`-segmented format string and the parallel array of CSS declarations,
|
|
8
|
-
*
|
|
7
|
+
* `%c`-segmented format string and the parallel array of CSS declarations, an optional partial
|
|
8
|
+
* {@link BrowserPalette} overriding the CSS per named lookup.
|
|
9
9
|
*
|
|
10
10
|
* @remarks
|
|
11
|
+
* A DevTools console then renders the same styling a terminal would; the browser sink calls
|
|
12
|
+
* `console[method](format, ...styles)`.
|
|
13
|
+
*
|
|
11
14
|
* - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —
|
|
12
15
|
* `ESC[…m`); each delimits a run. A run carrying visible text emits one `%c` directive plus that
|
|
13
16
|
* text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at
|
|
@@ -40,14 +43,15 @@ import { SinkInterface } from '@orkestrel/console';
|
|
|
40
43
|
export declare function ansiToConsole(text: string, palette?: BrowserPalette): ConsoleOutput;
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
|
-
* Maps each text-{@link Attribute}'s SGR "on" number to its equivalent CSS declaration — the
|
|
44
|
-
* counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2
|
|
45
|
-
* `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,
|
|
46
|
-
* `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`).
|
|
47
|
-
* number (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly
|
|
48
|
-
* while scanning a run.
|
|
46
|
+
* Maps each text-{@link Attribute}'s SGR "on" number to its equivalent CSS declaration — the
|
|
47
|
+
* browser counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2
|
|
48
|
+
* → `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,
|
|
49
|
+
* `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`).
|
|
49
50
|
*
|
|
50
51
|
* @remarks
|
|
52
|
+
* Keyed by the SGR number (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a
|
|
53
|
+
* parameter up directly while scanning a run.
|
|
54
|
+
*
|
|
51
55
|
* `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,
|
|
52
56
|
* which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as
|
|
53
57
|
* approximate, never silently dropped. Deeply frozen.
|
|
@@ -55,8 +59,8 @@ export declare function ansiToConsole(text: string, palette?: BrowserPalette): C
|
|
|
55
59
|
export declare const ATTRIBUTE_CSS: Readonly<Record<number, string>>;
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
* Holds partial browser CSS overrides for the core color and attribute axes
|
|
59
|
-
*
|
|
62
|
+
* Holds partial browser CSS overrides for the core color and attribute axes — a named `color` or
|
|
63
|
+
* `attribute` entry replaces only that entry, and every omission keeps its default.
|
|
60
64
|
*
|
|
61
65
|
* @remarks
|
|
62
66
|
* - `color` maps a named non-default {@link Color} to the CSS color value used for both foreground
|
|
@@ -69,10 +73,8 @@ export declare interface BrowserPalette {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* Configures {@link import('./factories.js').createBrowserSink}
|
|
73
|
-
*
|
|
74
|
-
* @remarks
|
|
75
|
-
* `palette` partially overrides the browser's default color and attribute CSS mappings.
|
|
76
|
+
* Configures {@link import('./factories.js').createBrowserSink} — the optional `palette` partially
|
|
77
|
+
* overriding the browser's named color and attribute CSS mappings.
|
|
76
78
|
*/
|
|
77
79
|
export declare interface BrowserSinkOptions {
|
|
78
80
|
readonly palette?: BrowserPalette;
|
|
@@ -91,10 +93,10 @@ export declare interface BrowserSinkOptions {
|
|
|
91
93
|
export declare const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>>;
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
|
-
* Represents the `console.log`-ready output {@link import('./helpers.js').ansiToConsole} produces
|
|
95
|
-
* an ANSI-styled string — a format string of `%c`-prefixed segments and the parallel array
|
|
96
|
-
*
|
|
97
|
-
*
|
|
96
|
+
* Represents the `console.log`-ready output {@link import('./helpers.js').ansiToConsole} produces
|
|
97
|
+
* from an ANSI-styled string — a format string of `%c`-prefixed segments and the parallel array of
|
|
98
|
+
* CSS declarations, ready to spread into a browser `console` call as `console.log(format,
|
|
99
|
+
* ...styles)`.
|
|
98
100
|
*
|
|
99
101
|
* @remarks
|
|
100
102
|
* - `format` — the text with each styled run prefixed by one `%c` directive (the directive
|
|
@@ -114,14 +116,16 @@ export declare interface ConsoleOutput {
|
|
|
114
116
|
/**
|
|
115
117
|
* Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`
|
|
116
118
|
* translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
|
|
117
|
-
* through {@link ansiToConsole},
|
|
118
|
-
*
|
|
119
|
-
* core output to the browser console with no change to the core.
|
|
119
|
+
* through {@link ansiToConsole}, an optional partial {@link BrowserPalette} overriding the named
|
|
120
|
+
* color and attribute CSS.
|
|
120
121
|
*
|
|
121
122
|
* @param options - See {@link BrowserSinkOptions}
|
|
122
123
|
* @returns A browser `%c` {@link SinkInterface}
|
|
123
124
|
*
|
|
124
125
|
* @remarks
|
|
126
|
+
* Drop it in as a logger / reporter / spinner sink (`new Logger({ sink: createBrowserSink() })`)
|
|
127
|
+
* to retarget the core output to the browser console with no change to the core.
|
|
128
|
+
*
|
|
125
129
|
* - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
|
|
126
130
|
* re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
|
|
127
131
|
* — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
|
|
@@ -129,7 +133,7 @@ export declare interface ConsoleOutput {
|
|
|
129
133
|
* - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an
|
|
130
134
|
* omitted level) → `console.log` — the same routing as core's `createConsoleSink`, so a logger's level
|
|
131
135
|
* reaches the matching DevTools stream. Both call the one
|
|
132
|
-
* {@link import('@
|
|
136
|
+
* {@link import('@orkestrel/console').selectWriter} leaf, which is what keeps them identical.
|
|
133
137
|
* - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with
|
|
134
138
|
* a carriage return `\r` (a spinner / progress redraw) has the leading `\r` stripped and is written as a
|
|
135
139
|
* fresh, non-overwriting line — the locked browser degrade. Only a leading `\r` is stripped; an interior
|
|
@@ -151,9 +155,9 @@ export declare interface ConsoleOutput {
|
|
|
151
155
|
export declare function createBrowserSink(options?: BrowserSinkOptions): SinkInterface;
|
|
152
156
|
|
|
153
157
|
/**
|
|
154
|
-
* Names the browser console directive that switches the active style — one `%c` prefixes every
|
|
155
|
-
* in the {@link import('./types.js').ConsoleOutput} format string, consuming the next
|
|
156
|
-
* parallel CSS array. The single source of truth for the directive token.
|
|
158
|
+
* Names the browser console directive that switches the active style — one `%c` prefixes every
|
|
159
|
+
* styled run in the {@link import('./types.js').ConsoleOutput} format string, consuming the next
|
|
160
|
+
* entry of the parallel CSS array. The single source of truth for the directive token.
|
|
157
161
|
*/
|
|
158
162
|
export declare const DIRECTIVE = "%c";
|
|
159
163
|
|
|
@@ -175,14 +179,17 @@ export declare function escapePercent(text: string): string;
|
|
|
175
179
|
|
|
176
180
|
/**
|
|
177
181
|
* Walks an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})
|
|
178
|
-
* and returns its numeric codes — `'1;31'` → `[1, 31]
|
|
179
|
-
*
|
|
180
|
-
* sequence as a reset; an empty field within a list (`'1;;4'`) likewise counts as a `0` reset,
|
|
181
|
-
* matching the spec, and a non-numeric field yields `NaN`, which the caller then ignores.
|
|
182
|
+
* and returns its numeric codes — `'1;31'` → `[1, 31]`, a bare or empty field becoming a `0`
|
|
183
|
+
* reset. It is total: every input yields a code list.
|
|
182
184
|
*
|
|
183
185
|
* @param parameters - The raw `;`-separated parameter string (the regex capture)
|
|
184
186
|
* @returns The SGR codes found (a parameterless / empty field becoming `0`)
|
|
185
187
|
*
|
|
188
|
+
* @remarks
|
|
189
|
+
* An empty list (a bare `ESC[m`) yields `[0]`, because the SGR spec treats a parameterless
|
|
190
|
+
* sequence as a reset; an empty field within a list (`'1;;4'`) likewise counts as a `0` reset,
|
|
191
|
+
* matching the spec, and a non-numeric field yields `NaN`, which the caller then ignores.
|
|
192
|
+
*
|
|
186
193
|
* @example
|
|
187
194
|
* ```ts
|
|
188
195
|
* scanParameters('1;31') // [1, 31]
|
|
@@ -193,7 +200,7 @@ export declare function scanParameters(parameters: string): readonly number[];
|
|
|
193
200
|
|
|
194
201
|
/**
|
|
195
202
|
* Matches one SGR sequence (`ESC[ <params> m`) and captures its `;`-separated numeric parameters —
|
|
196
|
-
* the subset of ANSI {@link import('@
|
|
203
|
+
* the subset of ANSI {@link import('@orkestrel/console').strip} cares about that carries style (color /
|
|
197
204
|
* attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks
|
|
198
205
|
* every SGR run in a string; built from core's {@link ESC} so no control-character literal appears
|
|
199
206
|
* in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,
|
|
@@ -207,13 +214,15 @@ export declare function scanParameters(parameters: string): readonly number[];
|
|
|
207
214
|
export declare const SGR_PATTERN: RegExp;
|
|
208
215
|
|
|
209
216
|
/**
|
|
210
|
-
* Represents the immutable
|
|
211
|
-
* translating SGR codes to CSS —
|
|
212
|
-
*
|
|
213
|
-
* declarations. An SGR reset drops both channels and empties the list;
|
|
214
|
-
* {@link import('./helpers.js').ansiToConsole} folds it into the `;`-joined CSS string a run emits.
|
|
217
|
+
* Represents the immutable scan state {@link import('./helpers.js').ansiToConsole} replaces while
|
|
218
|
+
* translating SGR codes to CSS — an optional `foreground` and `background` declaration plus a
|
|
219
|
+
* readonly list of attribute declarations.
|
|
215
220
|
*
|
|
216
221
|
* @remarks
|
|
222
|
+
* A later color of the same channel replaces that channel; an SGR reset drops both channels and
|
|
223
|
+
* empties the list; {@link import('./helpers.js').ansiToConsole} folds the state into the
|
|
224
|
+
* `;`-joined CSS string a run emits.
|
|
225
|
+
*
|
|
217
226
|
* Each SGR sequence produces a new frozen value; earlier run snapshots never drift when a later
|
|
218
227
|
* sequence changes a channel. A channel holds the full CSS declaration (`'color:#cd0000'`, not a
|
|
219
228
|
* bare hex), and is absent when unset.
|
|
@@ -29,14 +29,15 @@ var COLOR_HEX = Object.freeze({
|
|
|
29
29
|
brightWhite: "#ffffff"
|
|
30
30
|
});
|
|
31
31
|
/**
|
|
32
|
-
* Maps each text-{@link Attribute}'s SGR "on" number to its equivalent CSS declaration — the
|
|
33
|
-
* counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2
|
|
34
|
-
* `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,
|
|
35
|
-
* `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`).
|
|
36
|
-
* number (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly
|
|
37
|
-
* while scanning a run.
|
|
32
|
+
* Maps each text-{@link Attribute}'s SGR "on" number to its equivalent CSS declaration — the
|
|
33
|
+
* browser counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2
|
|
34
|
+
* → `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,
|
|
35
|
+
* `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`).
|
|
38
36
|
*
|
|
39
37
|
* @remarks
|
|
38
|
+
* Keyed by the SGR number (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a
|
|
39
|
+
* parameter up directly while scanning a run.
|
|
40
|
+
*
|
|
40
41
|
* `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,
|
|
41
42
|
* which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as
|
|
42
43
|
* approximate, never silently dropped. Deeply frozen.
|
|
@@ -50,9 +51,9 @@ var ATTRIBUTE_CSS = Object.freeze({
|
|
|
50
51
|
[ATTRIBUTE_CODES.strikethrough]: "text-decoration:line-through"
|
|
51
52
|
});
|
|
52
53
|
/**
|
|
53
|
-
* Names the browser console directive that switches the active style — one `%c` prefixes every
|
|
54
|
-
* in the {@link import('./types.js').ConsoleOutput} format string, consuming the next
|
|
55
|
-
* parallel CSS array. The single source of truth for the directive token.
|
|
54
|
+
* Names the browser console directive that switches the active style — one `%c` prefixes every
|
|
55
|
+
* styled run in the {@link import('./types.js').ConsoleOutput} format string, consuming the next
|
|
56
|
+
* entry of the parallel CSS array. The single source of truth for the directive token.
|
|
56
57
|
*/
|
|
57
58
|
var DIRECTIVE = "%c";
|
|
58
59
|
/**
|
|
@@ -73,10 +74,13 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
|
|
|
73
74
|
//#region src/browser/helpers.ts
|
|
74
75
|
/**
|
|
75
76
|
* Translates an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
|
|
76
|
-
* `%c`-segmented format string and the parallel array of CSS declarations,
|
|
77
|
-
*
|
|
77
|
+
* `%c`-segmented format string and the parallel array of CSS declarations, an optional partial
|
|
78
|
+
* {@link BrowserPalette} overriding the CSS per named lookup.
|
|
78
79
|
*
|
|
79
80
|
* @remarks
|
|
81
|
+
* A DevTools console then renders the same styling a terminal would; the browser sink calls
|
|
82
|
+
* `console[method](format, ...styles)`.
|
|
83
|
+
*
|
|
80
84
|
* - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —
|
|
81
85
|
* `ESC[…m`); each delimits a run. A run carrying visible text emits one `%c` directive plus that
|
|
82
86
|
* text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at
|
|
@@ -187,14 +191,17 @@ function escapePercent(text) {
|
|
|
187
191
|
}
|
|
188
192
|
/**
|
|
189
193
|
* Walks an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})
|
|
190
|
-
* and returns its numeric codes — `'1;31'` → `[1, 31]
|
|
191
|
-
*
|
|
192
|
-
* sequence as a reset; an empty field within a list (`'1;;4'`) likewise counts as a `0` reset,
|
|
193
|
-
* matching the spec, and a non-numeric field yields `NaN`, which the caller then ignores.
|
|
194
|
+
* and returns its numeric codes — `'1;31'` → `[1, 31]`, a bare or empty field becoming a `0`
|
|
195
|
+
* reset. It is total: every input yields a code list.
|
|
194
196
|
*
|
|
195
197
|
* @param parameters - The raw `;`-separated parameter string (the regex capture)
|
|
196
198
|
* @returns The SGR codes found (a parameterless / empty field becoming `0`)
|
|
197
199
|
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* An empty list (a bare `ESC[m`) yields `[0]`, because the SGR spec treats a parameterless
|
|
202
|
+
* sequence as a reset; an empty field within a list (`'1;;4'`) likewise counts as a `0` reset,
|
|
203
|
+
* matching the spec, and a non-numeric field yields `NaN`, which the caller then ignores.
|
|
204
|
+
*
|
|
198
205
|
* @example
|
|
199
206
|
* ```ts
|
|
200
207
|
* scanParameters('1;31') // [1, 31]
|
|
@@ -210,14 +217,16 @@ function scanParameters(parameters) {
|
|
|
210
217
|
/**
|
|
211
218
|
* Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`
|
|
212
219
|
* translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
|
|
213
|
-
* through {@link ansiToConsole},
|
|
214
|
-
*
|
|
215
|
-
* core output to the browser console with no change to the core.
|
|
220
|
+
* through {@link ansiToConsole}, an optional partial {@link BrowserPalette} overriding the named
|
|
221
|
+
* color and attribute CSS.
|
|
216
222
|
*
|
|
217
223
|
* @param options - See {@link BrowserSinkOptions}
|
|
218
224
|
* @returns A browser `%c` {@link SinkInterface}
|
|
219
225
|
*
|
|
220
226
|
* @remarks
|
|
227
|
+
* Drop it in as a logger / reporter / spinner sink (`new Logger({ sink: createBrowserSink() })`)
|
|
228
|
+
* to retarget the core output to the browser console with no change to the core.
|
|
229
|
+
*
|
|
221
230
|
* - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
|
|
222
231
|
* re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
|
|
223
232
|
* — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { Color } from '@src/core'\nimport { ATTRIBUTE_CODES, ESC } from '@src/core'\n\n// The SGR → CSS translation data the browser sink maps ANSI runs through (the browser branch).\n// The core `src/core/console` is the source of truth for the SGR numbers (which code is which\n// color / attribute); this module owns only the browser-side mapping — a named-color → hex\n// palette and the attribute CSS declarations. The live translator resolves color SGR numbers\n// through core's code maps and then reads this named palette, so no second number→CSS table drifts.\n// The SGR-scan pattern is built from core's `ESC` so no control-character literal appears in\n// source. UPPER_SNAKE, deeply `Object.freeze`d, every member exported.\n\n/**\n * Maps each named {@link Color} to its hex value — the 16 standard terminal colors a browser DevTools\n * console renders the same {@link Color} names as. The source of truth for the browser color\n * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to\n * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.\n *\n * @remarks\n * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`\n * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.\n */\nexport const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>> = Object.freeze({\n\tblack: '#000000',\n\tred: '#cd0000',\n\tgreen: '#00cd00',\n\tyellow: '#cdcd00',\n\tblue: '#0000ee',\n\tmagenta: '#cd00cd',\n\tcyan: '#00cdcd',\n\twhite: '#e5e5e5',\n\tbrightBlack: '#7f7f7f',\n\tbrightRed: '#ff0000',\n\tbrightGreen: '#00ff00',\n\tbrightYellow: '#ffff00',\n\tbrightBlue: '#5c5cff',\n\tbrightMagenta: '#ff00ff',\n\tbrightCyan: '#00ffff',\n\tbrightWhite: '#ffffff',\n})\n\n/**\n * Maps each text-{@link Attribute}'s SGR \"on\" number to its equivalent CSS declaration — the browser\n * counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2 →\n * `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,\n * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`). Keyed by the SGR\n * number (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly\n * while scanning a run.\n *\n * @remarks\n * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,\n * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as\n * approximate, never silently dropped. Deeply frozen.\n */\nexport const ATTRIBUTE_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[ATTRIBUTE_CODES.bold]: 'font-weight:bold',\n\t[ATTRIBUTE_CODES.dim]: 'opacity:0.6',\n\t[ATTRIBUTE_CODES.italic]: 'font-style:italic',\n\t[ATTRIBUTE_CODES.underline]: 'text-decoration:underline',\n\t[ATTRIBUTE_CODES.inverse]: 'filter:invert(100%)',\n\t[ATTRIBUTE_CODES.strikethrough]: 'text-decoration:line-through',\n})\n\n/**\n * Names the browser console directive that switches the active style — one `%c` prefixes every styled run\n * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the\n * parallel CSS array. The single source of truth for the directive token.\n */\nexport const DIRECTIVE = '%c'\n\n/**\n * Matches one SGR sequence (`ESC[ <params> m`) and captures its `;`-separated numeric parameters —\n * the subset of ANSI {@link import('@src/core').strip} cares about that carries style (color /\n * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks\n * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears\n * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,\n * which the spec treats as a reset).\n *\n * @remarks\n * A global `RegExp` carries a mutable `lastIndex`; a scan builds a fresh `RegExp` from this one's\n * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the\n * canonical definition, not a shared scanner.\n */\nexport const SGR_PATTERN = new RegExp(`${ESC}\\\\[([0-9;]*)m`, 'g')\n","import type { BrowserPalette, ConsoleOutput, StyleAccumulator } from './types.js'\nimport {\n\tATTRIBUTE_CODES,\n\tATTRIBUTES,\n\tBACKGROUND_CODES,\n\tCOLORS,\n\tFOREGROUND_CODES,\n\tRESET_CODE,\n} from '@src/core'\nimport { ATTRIBUTE_CSS, COLOR_HEX, DIRECTIVE, SGR_PATTERN } from './constants.js'\n\n// The pure, browser-only translation behind the `%c` console sink (the browser branch). The core\n// styler / Logger / Reporter emit ANSI-styled strings; a DevTools console can't render ANSI but\n// can style through `console.log('%ctext', 'css')`, so `ansiToConsole` parses the SGR runs in the\n// incoming text and re-emits them as a `%c`-ready format string + parallel CSS array — the\n// translation happens at the output boundary, leaving the core unchanged. Pure + total + `%`-safe.\n// `ansiToConsole` carries immutable style snapshots while its local arrays assemble the final\n// `%c` output; only the standalone, reusable `escapePercent` / `scanParameters` utilities are\n// exported alongside it.\n\n/**\n * Translates an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a\n * `%c`-segmented format string and the parallel array of CSS declarations, so a DevTools console\n * renders the same styling a terminal would (the browser sink calls `console[method](format, ...styles)`).\n *\n * @remarks\n * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —\n * `ESC[…m`); each delimits a run. A run carrying visible text emits one `%c` directive plus that\n * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at\n * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare\n * `ESC[m`) clears the accumulated style back to none. A later color of the same channel replaces\n * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style\n * and are left in the text verbatim.\n * - **`%`-safe.** Every literal `%` in the text is doubled to `%%` so the console never treats it as\n * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`\n * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.\n * - **Plain text short-circuits.** A string with no SGR sequence yields `{ format: <escaped text>,\n * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).\n * - **Partial palette.** A supplied palette overrides only its named colors and attributes. Every\n * omitted entry resolves through {@link COLOR_HEX} or {@link ATTRIBUTE_CSS}, so defaults and\n * unrelated entries stay byte-identical.\n * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,\n * lone `ESC`, unterminated sequences all fall through as literal text).\n *\n * @param text - Any string, ANSI-styled or plain\n * @param palette - Optional partial browser CSS overrides\n * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})\n *\n * @example\n * ```ts\n * ansiToConsole('\\x1b[31mred\\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }\n * ansiToConsole('plain') // { format: 'plain', styles: [] }\n * ansiToConsole('50%') // { format: '50%%', styles: [] }\n * ```\n */\nexport function ansiToConsole(text: string, palette?: BrowserPalette): ConsoleOutput {\n\tconst scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags)\n\t// The accumulated active style across a run — a separate foreground / background declaration\n\t// (each channel replaceable) plus an ordered, de-duplicated list of attribute declarations. An\n\t// SGR reset drops both channels and empties the list. Serialized to a `;`-joined CSS string per\n\t// emitted run.\n\tlet active: StyleAccumulator = Object.freeze({ attributes: Object.freeze([]) })\n\tconst segments: string[] = []\n\tconst styles: string[] = []\n\tlet cursor = 0\n\tlet pending = ''\n\tlet match: RegExpExecArray | null = scanner.exec(text)\n\tif (match === null) return { format: escapePercent(text), styles: [] }\n\n\t// A null match is the final text boundary, so every visible run passes through one flush path.\n\twhile (true) {\n\t\tconst boundary = match === null ? text.length : match.index\n\t\tpending += escapePercent(text.slice(cursor, boundary))\n\t\tif (pending !== '') {\n\t\t\tsegments.push(`${DIRECTIVE}${pending}`)\n\t\t\tconst declarations = [...active.attributes]\n\t\t\tif (active.foreground !== undefined) declarations.push(active.foreground)\n\t\t\tif (active.background !== undefined) declarations.push(active.background)\n\t\t\tstyles.push(declarations.join(';'))\n\t\t\tpending = ''\n\t\t}\n\t\tif (match === null) break\n\n\t\t// Apply one SGR sequence by replacing the readonly accumulator. A reset clears every channel;\n\t\t// colors replace their channel; attributes accumulate once; unknown extensions are ignored.\n\t\tfor (const code of scanParameters(match[1] ?? '')) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive = Object.freeze({ attributes: Object.freeze([]) })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst foreground = COLORS.find((color) => FOREGROUND_CODES[color] === code)\n\t\t\tif (foreground !== undefined) {\n\t\t\t\tconst color = palette?.color?.[foreground] ?? COLOR_HEX[foreground]\n\t\t\t\tactive = Object.freeze({ ...active, foreground: `color:${color}` })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst background = COLORS.find((color) => BACKGROUND_CODES[color] === code)\n\t\t\tif (background !== undefined) {\n\t\t\t\tconst color = palette?.color?.[background] ?? COLOR_HEX[background]\n\t\t\t\tactive = Object.freeze({ ...active, background: `background:${color}` })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst name = ATTRIBUTES.find((attribute) => ATTRIBUTE_CODES[attribute] === code)\n\t\t\tconst attribute =\n\t\t\t\tname === undefined ? undefined : (palette?.attribute?.[name] ?? ATTRIBUTE_CSS[code])\n\t\t\tif (attribute !== undefined && !active.attributes.includes(attribute)) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\t...active,\n\t\t\t\t\tattributes: Object.freeze([...active.attributes, attribute]),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tcursor = match.index + match[0].length\n\t\tmatch = scanner.exec(text)\n\t}\n\treturn { format: segments.join(''), styles }\n}\n\n/**\n * Doubles every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (for example in `50%` or `%s`) as a format directive. The single escape the\n * {@link ansiToConsole} translation applies to every text segment before assembling the format\n * string (so only the `%c`s it inserts are real directives).\n *\n * @param text - A literal text segment (no inserted directives)\n * @returns `text` with each `%` doubled\n *\n * @example\n * ```ts\n * escapePercent('100% done') // '100%% done'\n * ```\n */\nexport function escapePercent(text: string): string {\n\treturn text.replace(/%/g, '%%')\n}\n\n/**\n * Walks an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * and returns its numeric codes — `'1;31'` → `[1, 31]`. It is total: every input yields a code\n * list. An empty list (a bare `ESC[m`) yields `[0]`, because the SGR spec treats a parameterless\n * sequence as a reset; an empty field within a list (`'1;;4'`) likewise counts as a `0` reset,\n * matching the spec, and a non-numeric field yields `NaN`, which the caller then ignores.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The SGR codes found (a parameterless / empty field becoming `0`)\n *\n * @example\n * ```ts\n * scanParameters('1;31') // [1, 31]\n * scanParameters('') // [0]\n * ```\n */\nexport function scanParameters(parameters: string): readonly number[] {\n\tif (parameters === '') return [RESET_CODE]\n\treturn parameters.split(';').map((field) => (field === '' ? RESET_CODE : Number(field)))\n}\n","import type { LogLevel, SinkInterface } from '@src/core'\nimport type { BrowserSinkOptions } from './types.js'\nimport { selectWriter } from '@src/core'\nimport { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the browser branch) — the platform-bound backend that satisfies core's\n// `SinkInterface` in a browser DevTools console. The core styler / Logger / Reporter emit ANSI-styled\n// strings; a DevTools console can't render ANSI but can style through `console.log('%ctext', 'css')`, so\n// this sink translates the incoming ANSI runs into a `%c` call at the output boundary (the env-split\n// rule: core owns the contract + universal logic, the browser provides the platform backend). A thin\n// stateless adapter, so a frozen-object factory — like core's `createConsoleSink` — not a class.\n// `SinkInterface` / `LogLevel` are imported from `@src/core`, never redeclared.\n\n/**\n * Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * through {@link ansiToConsole}, so a DevTools console renders the same styling a terminal does. Drop it in\n * as a logger / reporter / spinner sink (`new Logger({ sink: createBrowserSink() })`) to retarget the\n * core output to the browser console with no change to the core.\n *\n * @param options - See {@link BrowserSinkOptions}\n * @returns A browser `%c` {@link SinkInterface}\n *\n * @remarks\n * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and\n * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}\n * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.\n * `options.palette` supplies partial named color and attribute overrides to that translation.\n * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an\n * omitted level) → `console.log` — the same routing as core's `createConsoleSink`, so a logger's level\n * reaches the matching DevTools stream. Both call the one\n * {@link import('@src/core').selectWriter} leaf, which is what keeps them identical.\n * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with\n * a carriage return `\\r` (a spinner / progress redraw) has the leading `\\r` stripped and is written as a\n * fresh, non-overwriting line — the locked browser degrade. Only a leading `\\r` is stripped; an interior\n * one is left to the console.\n * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` at\n * creation and writes through those references, so a later `Capture` that patches `console.*` can never\n * feed this sink's output back into itself (the no-capture-loop principle, following the core\n * sink's precedent). Create the sink (or the logger) before installing a capture.\n *\n * @example\n * ```ts\n * import { Logger } from '@orkestrel/console'\n * import { createBrowserSink } from '@orkestrel/console/browser'\n *\n * const logger = new Logger({ name: 'app', sink: createBrowserSink() })\n * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools\n * ```\n */\nexport function createBrowserSink(options?: BrowserSinkOptions): SinkInterface {\n\t// Snapshot the console writers at construction — bound to their `console` receiver — so a later patch of\n\t// `console.*` (by Capture) can never reach this sink's output (no capture loop), exactly as core's\n\t// `createConsoleSink` does.\n\tconst log = console.log.bind(console)\n\tconst warn = console.warn.bind(console)\n\tconst error = console.error.bind(console)\n\treturn {\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\t// Degrade the animation redraw first: a leading `\\r` can't overwrite a line in a browser\n\t\t\t// console, so drop it and write a fresh, non-overwriting line (the locked decision).\n\t\t\tconst line = text.startsWith('\\r') ? text.slice(1) : text\n\t\t\tconst { format, styles } = ansiToConsole(line, options?.palette)\n\t\t\tselectWriter(level, { log, warn, error })(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { Color } from '@src/core'\nimport { ATTRIBUTE_CODES, ESC } from '@src/core'\n\n// The SGR → CSS translation data the browser sink maps ANSI runs through (the browser branch).\n// The core `src/core/console` is the source of truth for the SGR numbers (which code is which\n// color / attribute); this module owns only the browser-side mapping — a named-color → hex\n// palette and the attribute CSS declarations. The live translator resolves color SGR numbers\n// through core's code maps and then reads this named palette, so no second number→CSS table drifts.\n// The SGR-scan pattern is built from core's `ESC` so no control-character literal appears in\n// source. UPPER_SNAKE, deeply `Object.freeze`d, every member exported.\n\n/**\n * Maps each named {@link Color} to its hex value — the 16 standard terminal colors a browser DevTools\n * console renders the same {@link Color} names as. The source of truth for the browser color\n * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to\n * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.\n *\n * @remarks\n * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`\n * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.\n */\nexport const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>> = Object.freeze({\n\tblack: '#000000',\n\tred: '#cd0000',\n\tgreen: '#00cd00',\n\tyellow: '#cdcd00',\n\tblue: '#0000ee',\n\tmagenta: '#cd00cd',\n\tcyan: '#00cdcd',\n\twhite: '#e5e5e5',\n\tbrightBlack: '#7f7f7f',\n\tbrightRed: '#ff0000',\n\tbrightGreen: '#00ff00',\n\tbrightYellow: '#ffff00',\n\tbrightBlue: '#5c5cff',\n\tbrightMagenta: '#ff00ff',\n\tbrightCyan: '#00ffff',\n\tbrightWhite: '#ffffff',\n})\n\n/**\n * Maps each text-{@link Attribute}'s SGR \"on\" number to its equivalent CSS declaration — the\n * browser counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2\n * → `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,\n * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`).\n *\n * @remarks\n * Keyed by the SGR number (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a\n * parameter up directly while scanning a run.\n *\n * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,\n * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as\n * approximate, never silently dropped. Deeply frozen.\n */\nexport const ATTRIBUTE_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[ATTRIBUTE_CODES.bold]: 'font-weight:bold',\n\t[ATTRIBUTE_CODES.dim]: 'opacity:0.6',\n\t[ATTRIBUTE_CODES.italic]: 'font-style:italic',\n\t[ATTRIBUTE_CODES.underline]: 'text-decoration:underline',\n\t[ATTRIBUTE_CODES.inverse]: 'filter:invert(100%)',\n\t[ATTRIBUTE_CODES.strikethrough]: 'text-decoration:line-through',\n})\n\n/**\n * Names the browser console directive that switches the active style — one `%c` prefixes every\n * styled run in the {@link import('./types.js').ConsoleOutput} format string, consuming the next\n * entry of the parallel CSS array. The single source of truth for the directive token.\n */\nexport const DIRECTIVE = '%c'\n\n/**\n * Matches one SGR sequence (`ESC[ <params> m`) and captures its `;`-separated numeric parameters —\n * the subset of ANSI {@link import('@src/core').strip} cares about that carries style (color /\n * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks\n * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears\n * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,\n * which the spec treats as a reset).\n *\n * @remarks\n * A global `RegExp` carries a mutable `lastIndex`; a scan builds a fresh `RegExp` from this one's\n * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the\n * canonical definition, not a shared scanner.\n */\nexport const SGR_PATTERN = new RegExp(`${ESC}\\\\[([0-9;]*)m`, 'g')\n","import type { BrowserPalette, ConsoleOutput, StyleAccumulator } from './types.js'\nimport {\n\tATTRIBUTE_CODES,\n\tATTRIBUTES,\n\tBACKGROUND_CODES,\n\tCOLORS,\n\tFOREGROUND_CODES,\n\tRESET_CODE,\n} from '@src/core'\nimport { ATTRIBUTE_CSS, COLOR_HEX, DIRECTIVE, SGR_PATTERN } from './constants.js'\n\n// The pure, browser-only translation behind the `%c` console sink (the browser branch). The core\n// styler / Logger / Reporter emit ANSI-styled strings; a DevTools console can't render ANSI but\n// can style through `console.log('%ctext', 'css')`, so `ansiToConsole` parses the SGR runs in the\n// incoming text and re-emits them as a `%c`-ready format string + parallel CSS array — the\n// translation happens at the output boundary, leaving the core unchanged. Pure + total + `%`-safe.\n// `ansiToConsole` carries immutable style snapshots while its local arrays assemble the final\n// `%c` output; only the standalone, reusable `escapePercent` / `scanParameters` utilities are\n// exported alongside it.\n\n/**\n * Translates an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a\n * `%c`-segmented format string and the parallel array of CSS declarations, an optional partial\n * {@link BrowserPalette} overriding the CSS per named lookup.\n *\n * @remarks\n * A DevTools console then renders the same styling a terminal would; the browser sink calls\n * `console[method](format, ...styles)`.\n *\n * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —\n * `ESC[…m`); each delimits a run. A run carrying visible text emits one `%c` directive plus that\n * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at\n * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare\n * `ESC[m`) clears the accumulated style back to none. A later color of the same channel replaces\n * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style\n * and are left in the text verbatim.\n * - **`%`-safe.** Every literal `%` in the text is doubled to `%%` so the console never treats it as\n * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`\n * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.\n * - **Plain text short-circuits.** A string with no SGR sequence yields `{ format: <escaped text>,\n * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).\n * - **Partial palette.** A supplied palette overrides only its named colors and attributes. Every\n * omitted entry resolves through {@link COLOR_HEX} or {@link ATTRIBUTE_CSS}, so defaults and\n * unrelated entries stay byte-identical.\n * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,\n * lone `ESC`, unterminated sequences all fall through as literal text).\n *\n * @param text - Any string, ANSI-styled or plain\n * @param palette - Optional partial browser CSS overrides\n * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})\n *\n * @example\n * ```ts\n * ansiToConsole('\\x1b[31mred\\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }\n * ansiToConsole('plain') // { format: 'plain', styles: [] }\n * ansiToConsole('50%') // { format: '50%%', styles: [] }\n * ```\n */\nexport function ansiToConsole(text: string, palette?: BrowserPalette): ConsoleOutput {\n\tconst scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags)\n\t// The accumulated active style across a run — a separate foreground / background declaration\n\t// (each channel replaceable) plus an ordered, de-duplicated list of attribute declarations. An\n\t// SGR reset drops both channels and empties the list. Serialized to a `;`-joined CSS string per\n\t// emitted run.\n\tlet active: StyleAccumulator = Object.freeze({ attributes: Object.freeze([]) })\n\tconst segments: string[] = []\n\tconst styles: string[] = []\n\tlet cursor = 0\n\tlet pending = ''\n\tlet match: RegExpExecArray | null = scanner.exec(text)\n\tif (match === null) return { format: escapePercent(text), styles: [] }\n\n\t// A null match is the final text boundary, so every visible run passes through one flush path.\n\twhile (true) {\n\t\tconst boundary = match === null ? text.length : match.index\n\t\tpending += escapePercent(text.slice(cursor, boundary))\n\t\tif (pending !== '') {\n\t\t\tsegments.push(`${DIRECTIVE}${pending}`)\n\t\t\tconst declarations = [...active.attributes]\n\t\t\tif (active.foreground !== undefined) declarations.push(active.foreground)\n\t\t\tif (active.background !== undefined) declarations.push(active.background)\n\t\t\tstyles.push(declarations.join(';'))\n\t\t\tpending = ''\n\t\t}\n\t\tif (match === null) break\n\n\t\t// Apply one SGR sequence by replacing the readonly accumulator. A reset clears every channel;\n\t\t// colors replace their channel; attributes accumulate once; unknown extensions are ignored.\n\t\tfor (const code of scanParameters(match[1] ?? '')) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive = Object.freeze({ attributes: Object.freeze([]) })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst foreground = COLORS.find((color) => FOREGROUND_CODES[color] === code)\n\t\t\tif (foreground !== undefined) {\n\t\t\t\tconst color = palette?.color?.[foreground] ?? COLOR_HEX[foreground]\n\t\t\t\tactive = Object.freeze({ ...active, foreground: `color:${color}` })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst background = COLORS.find((color) => BACKGROUND_CODES[color] === code)\n\t\t\tif (background !== undefined) {\n\t\t\t\tconst color = palette?.color?.[background] ?? COLOR_HEX[background]\n\t\t\t\tactive = Object.freeze({ ...active, background: `background:${color}` })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst name = ATTRIBUTES.find((attribute) => ATTRIBUTE_CODES[attribute] === code)\n\t\t\tconst attribute =\n\t\t\t\tname === undefined ? undefined : (palette?.attribute?.[name] ?? ATTRIBUTE_CSS[code])\n\t\t\tif (attribute !== undefined && !active.attributes.includes(attribute)) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\t...active,\n\t\t\t\t\tattributes: Object.freeze([...active.attributes, attribute]),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tcursor = match.index + match[0].length\n\t\tmatch = scanner.exec(text)\n\t}\n\treturn { format: segments.join(''), styles }\n}\n\n/**\n * Doubles every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (for example in `50%` or `%s`) as a format directive. The single escape the\n * {@link ansiToConsole} translation applies to every text segment before assembling the format\n * string (so only the `%c`s it inserts are real directives).\n *\n * @param text - A literal text segment (no inserted directives)\n * @returns `text` with each `%` doubled\n *\n * @example\n * ```ts\n * escapePercent('100% done') // '100%% done'\n * ```\n */\nexport function escapePercent(text: string): string {\n\treturn text.replace(/%/g, '%%')\n}\n\n/**\n * Walks an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * and returns its numeric codes — `'1;31'` → `[1, 31]`, a bare or empty field becoming a `0`\n * reset. It is total: every input yields a code list.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The SGR codes found (a parameterless / empty field becoming `0`)\n *\n * @remarks\n * An empty list (a bare `ESC[m`) yields `[0]`, because the SGR spec treats a parameterless\n * sequence as a reset; an empty field within a list (`'1;;4'`) likewise counts as a `0` reset,\n * matching the spec, and a non-numeric field yields `NaN`, which the caller then ignores.\n *\n * @example\n * ```ts\n * scanParameters('1;31') // [1, 31]\n * scanParameters('') // [0]\n * ```\n */\nexport function scanParameters(parameters: string): readonly number[] {\n\tif (parameters === '') return [RESET_CODE]\n\treturn parameters.split(';').map((field) => (field === '' ? RESET_CODE : Number(field)))\n}\n","import type { LogLevel, SinkInterface } from '@src/core'\nimport type { BrowserSinkOptions } from './types.js'\nimport { selectWriter } from '@src/core'\nimport { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the browser branch) — the platform-bound backend that satisfies core's\n// `SinkInterface` in a browser DevTools console. The core styler / Logger / Reporter emit ANSI-styled\n// strings; a DevTools console can't render ANSI but can style through `console.log('%ctext', 'css')`, so\n// this sink translates the incoming ANSI runs into a `%c` call at the output boundary (the env-split\n// rule: core owns the contract + universal logic, the browser provides the platform backend). A thin\n// stateless adapter, so a frozen-object factory — like core's `createConsoleSink` — not a class.\n// `SinkInterface` / `LogLevel` are imported from `@src/core`, never redeclared.\n\n/**\n * Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * through {@link ansiToConsole}, an optional partial {@link BrowserPalette} overriding the named\n * color and attribute CSS.\n *\n * @param options - See {@link BrowserSinkOptions}\n * @returns A browser `%c` {@link SinkInterface}\n *\n * @remarks\n * Drop it in as a logger / reporter / spinner sink (`new Logger({ sink: createBrowserSink() })`)\n * to retarget the core output to the browser console with no change to the core.\n *\n * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and\n * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}\n * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.\n * `options.palette` supplies partial named color and attribute overrides to that translation.\n * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an\n * omitted level) → `console.log` — the same routing as core's `createConsoleSink`, so a logger's level\n * reaches the matching DevTools stream. Both call the one\n * {@link import('@src/core').selectWriter} leaf, which is what keeps them identical.\n * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with\n * a carriage return `\\r` (a spinner / progress redraw) has the leading `\\r` stripped and is written as a\n * fresh, non-overwriting line — the locked browser degrade. Only a leading `\\r` is stripped; an interior\n * one is left to the console.\n * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` at\n * creation and writes through those references, so a later `Capture` that patches `console.*` can never\n * feed this sink's output back into itself (the no-capture-loop principle, following the core\n * sink's precedent). Create the sink (or the logger) before installing a capture.\n *\n * @example\n * ```ts\n * import { Logger } from '@orkestrel/console'\n * import { createBrowserSink } from '@orkestrel/console/browser'\n *\n * const logger = new Logger({ name: 'app', sink: createBrowserSink() })\n * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools\n * ```\n */\nexport function createBrowserSink(options?: BrowserSinkOptions): SinkInterface {\n\t// Snapshot the console writers at construction — bound to their `console` receiver — so a later patch of\n\t// `console.*` (by Capture) can never reach this sink's output (no capture loop), exactly as core's\n\t// `createConsoleSink` does.\n\tconst log = console.log.bind(console)\n\tconst warn = console.warn.bind(console)\n\tconst error = console.error.bind(console)\n\treturn {\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\t// Degrade the animation redraw first: a leading `\\r` can't overwrite a line in a browser\n\t\t\t// console, so drop it and write a fresh, non-overwriting line (the locked decision).\n\t\t\tconst line = text.startsWith('\\r') ? text.slice(1) : text\n\t\t\tconst { format, styles } = ansiToConsole(line, options?.palette)\n\t\t\tselectWriter(level, { log, warn, error })(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|