@orkestrel/console 0.0.6 → 0.0.7

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.
@@ -1,3 +1,4 @@
1
+ import { Attribute } from '@orkestrel/console';
1
2
  import { Color } from '@orkestrel/console';
2
3
  import { SinkInterface } from '@orkestrel/console';
3
4
 
@@ -19,10 +20,14 @@ import { SinkInterface } from '@orkestrel/console';
19
20
  * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.
20
21
  * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,
21
22
  * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).
23
+ * - **Partial palette.** A supplied palette overrides only its named colors and attributes. Every
24
+ * omitted entry resolves through {@link COLOR_HEX} or {@link ATTRIBUTE_CSS}, so defaults and
25
+ * unrelated entries stay byte-identical.
22
26
  * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,
23
27
  * lone `ESC`, unterminated sequences all fall through as literal text).
24
28
  *
25
29
  * @param text - Any string, ANSI-styled or plain
30
+ * @param palette - Optional partial browser CSS overrides
26
31
  * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})
27
32
  *
28
33
  * @example
@@ -32,7 +37,7 @@ import { SinkInterface } from '@orkestrel/console';
32
37
  * ansiToConsole('50%') // { format: '50%%', styles: [] }
33
38
  * ```
34
39
  */
35
- export declare function ansiToConsole(text: string): ConsoleOutput;
40
+ export declare function ansiToConsole(text: string, palette?: BrowserPalette): ConsoleOutput;
36
41
 
37
42
  /**
38
43
  * Each text-{@link Attribute}'s SGR "on" number → its equivalent CSS declaration — the browser
@@ -50,15 +55,28 @@ export declare function ansiToConsole(text: string): ConsoleOutput;
50
55
  export declare const ATTRIBUTE_CSS: Readonly<Record<number, string>>;
51
56
 
52
57
  /**
53
- * Each SGR BACKGROUND parameter (40–47 / 100–107) its `background:<hex>` CSS. The sink reads this
54
- * while scanning a run to translate a background code to CSS.
58
+ * Partial browser CSS overrides for the core color and attribute axes. Omitted entries retain the
59
+ * built-in browser mappings, so one override changes only its named value.
55
60
  *
56
61
  * @remarks
57
- * Built the same way as {@link FOREGROUND_CSS} keys from core's {@link BACKGROUND_CODES}, values
58
- * from {@link COLOR_HEX} — and covered by the same exhaustive walk over core's {@link COLORS} in
59
- * `tests/src/browser/helpers.test.ts`. Deeply frozen.
62
+ * - `color` maps a named non-default {@link Color} to the CSS color value used for both foreground
63
+ * and background SGR codes.
64
+ * - `attribute` maps an {@link Attribute} to the CSS declaration used for its SGR code.
60
65
  */
61
- export declare const BACKGROUND_CSS: Readonly<Record<number, string>>;
66
+ export declare interface BrowserPalette {
67
+ readonly color?: Readonly<Partial<Record<Exclude<Color, 'default'>, string>>>;
68
+ readonly attribute?: Readonly<Partial<Record<Attribute, string>>>;
69
+ }
70
+
71
+ /**
72
+ * Options for {@link import('./factories.js').createBrowserSink}.
73
+ *
74
+ * @remarks
75
+ * `palette` partially overrides the browser's default color and attribute CSS mappings.
76
+ */
77
+ export declare interface BrowserSinkOptions {
78
+ readonly palette?: BrowserPalette;
79
+ }
62
80
 
63
81
  /**
64
82
  * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools
@@ -100,12 +118,14 @@ export declare interface ConsoleOutput {
100
118
  * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the
101
119
  * core output to the browser console with no change to the core.
102
120
  *
121
+ * @param options - See {@link BrowserSinkOptions}
103
122
  * @returns A browser `%c` {@link SinkInterface}
104
123
  *
105
124
  * @remarks
106
125
  * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
107
126
  * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
108
127
  * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
128
+ * `options.palette` supplies partial named color and attribute overrides to that translation.
109
129
  * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an
110
130
  * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level
111
131
  * reaches the matching DevTools stream.
@@ -127,7 +147,7 @@ export declare interface ConsoleOutput {
127
147
  * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools
128
148
  * ```
129
149
  */
130
- export declare function createBrowserSink(): SinkInterface;
150
+ export declare function createBrowserSink(options?: BrowserSinkOptions): SinkInterface;
131
151
 
132
152
  /**
133
153
  * The browser console directive that switches the active style — one `%c` prefixes every styled run
@@ -152,19 +172,6 @@ export declare const DIRECTIVE = "%c";
152
172
  */
153
173
  export declare function escapePercent(text: string): string;
154
174
 
155
- /**
156
- * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS. The sink reads this while
157
- * scanning a run to translate a foreground code to CSS.
158
- *
159
- * @remarks
160
- * Every key is core's {@link FOREGROUND_CODES} entry and every value reads {@link COLOR_HEX}, so
161
- * neither the number↔name mapping nor the palette is duplicated here — this is a table of
162
- * references, not of literals. `tests/src/browser/helpers.test.ts` walks core's {@link COLORS} and
163
- * asserts this record covers every one of them, so a color added to core fails there rather than
164
- * going silently untranslated. Deeply frozen.
165
- */
166
- export declare const FOREGROUND_CSS: Readonly<Record<number, string>>;
167
-
168
175
  /**
169
176
  * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})
170
177
  * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since
@@ -1,4 +1,4 @@
1
- import { ATTRIBUTE_CODES, BACKGROUND_CODES, ESC, FOREGROUND_CODES, RESET_CODE } from "../core/index.js";
1
+ import { ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES, RESET_CODE } from "../core/index.js";
2
2
  //#region src/browser/constants.ts
3
3
  /**
4
4
  * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools
@@ -50,62 +50,6 @@ var ATTRIBUTE_CSS = Object.freeze({
50
50
  [ATTRIBUTE_CODES.strikethrough]: "text-decoration:line-through"
51
51
  });
52
52
  /**
53
- * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS. The sink reads this while
54
- * scanning a run to translate a foreground code to CSS.
55
- *
56
- * @remarks
57
- * Every key is core's {@link FOREGROUND_CODES} entry and every value reads {@link COLOR_HEX}, so
58
- * neither the number↔name mapping nor the palette is duplicated here — this is a table of
59
- * references, not of literals. `tests/src/browser/helpers.test.ts` walks core's {@link COLORS} and
60
- * asserts this record covers every one of them, so a color added to core fails there rather than
61
- * going silently untranslated. Deeply frozen.
62
- */
63
- var FOREGROUND_CSS = Object.freeze({
64
- [FOREGROUND_CODES.black]: `color:${COLOR_HEX.black}`,
65
- [FOREGROUND_CODES.red]: `color:${COLOR_HEX.red}`,
66
- [FOREGROUND_CODES.green]: `color:${COLOR_HEX.green}`,
67
- [FOREGROUND_CODES.yellow]: `color:${COLOR_HEX.yellow}`,
68
- [FOREGROUND_CODES.blue]: `color:${COLOR_HEX.blue}`,
69
- [FOREGROUND_CODES.magenta]: `color:${COLOR_HEX.magenta}`,
70
- [FOREGROUND_CODES.cyan]: `color:${COLOR_HEX.cyan}`,
71
- [FOREGROUND_CODES.white]: `color:${COLOR_HEX.white}`,
72
- [FOREGROUND_CODES.brightBlack]: `color:${COLOR_HEX.brightBlack}`,
73
- [FOREGROUND_CODES.brightRed]: `color:${COLOR_HEX.brightRed}`,
74
- [FOREGROUND_CODES.brightGreen]: `color:${COLOR_HEX.brightGreen}`,
75
- [FOREGROUND_CODES.brightYellow]: `color:${COLOR_HEX.brightYellow}`,
76
- [FOREGROUND_CODES.brightBlue]: `color:${COLOR_HEX.brightBlue}`,
77
- [FOREGROUND_CODES.brightMagenta]: `color:${COLOR_HEX.brightMagenta}`,
78
- [FOREGROUND_CODES.brightCyan]: `color:${COLOR_HEX.brightCyan}`,
79
- [FOREGROUND_CODES.brightWhite]: `color:${COLOR_HEX.brightWhite}`
80
- });
81
- /**
82
- * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS. The sink reads this
83
- * while scanning a run to translate a background code to CSS.
84
- *
85
- * @remarks
86
- * Built the same way as {@link FOREGROUND_CSS} — keys from core's {@link BACKGROUND_CODES}, values
87
- * from {@link COLOR_HEX} — and covered by the same exhaustive walk over core's {@link COLORS} in
88
- * `tests/src/browser/helpers.test.ts`. Deeply frozen.
89
- */
90
- var BACKGROUND_CSS = Object.freeze({
91
- [BACKGROUND_CODES.black]: `background:${COLOR_HEX.black}`,
92
- [BACKGROUND_CODES.red]: `background:${COLOR_HEX.red}`,
93
- [BACKGROUND_CODES.green]: `background:${COLOR_HEX.green}`,
94
- [BACKGROUND_CODES.yellow]: `background:${COLOR_HEX.yellow}`,
95
- [BACKGROUND_CODES.blue]: `background:${COLOR_HEX.blue}`,
96
- [BACKGROUND_CODES.magenta]: `background:${COLOR_HEX.magenta}`,
97
- [BACKGROUND_CODES.cyan]: `background:${COLOR_HEX.cyan}`,
98
- [BACKGROUND_CODES.white]: `background:${COLOR_HEX.white}`,
99
- [BACKGROUND_CODES.brightBlack]: `background:${COLOR_HEX.brightBlack}`,
100
- [BACKGROUND_CODES.brightRed]: `background:${COLOR_HEX.brightRed}`,
101
- [BACKGROUND_CODES.brightGreen]: `background:${COLOR_HEX.brightGreen}`,
102
- [BACKGROUND_CODES.brightYellow]: `background:${COLOR_HEX.brightYellow}`,
103
- [BACKGROUND_CODES.brightBlue]: `background:${COLOR_HEX.brightBlue}`,
104
- [BACKGROUND_CODES.brightMagenta]: `background:${COLOR_HEX.brightMagenta}`,
105
- [BACKGROUND_CODES.brightCyan]: `background:${COLOR_HEX.brightCyan}`,
106
- [BACKGROUND_CODES.brightWhite]: `background:${COLOR_HEX.brightWhite}`
107
- });
108
- /**
109
53
  * The browser console directive that switches the active style — one `%c` prefixes every styled run
110
54
  * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the
111
55
  * parallel CSS array. The single source of truth for the directive token.
@@ -145,10 +89,14 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
145
89
  * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.
146
90
  * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,
147
91
  * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).
92
+ * - **Partial palette.** A supplied palette overrides only its named colors and attributes. Every
93
+ * omitted entry resolves through {@link COLOR_HEX} or {@link ATTRIBUTE_CSS}, so defaults and
94
+ * unrelated entries stay byte-identical.
148
95
  * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,
149
96
  * lone `ESC`, unterminated sequences all fall through as literal text).
150
97
  *
151
98
  * @param text - Any string, ANSI-styled or plain
99
+ * @param palette - Optional partial browser CSS overrides
152
100
  * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})
153
101
  *
154
102
  * @example
@@ -158,7 +106,7 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
158
106
  * ansiToConsole('50%') // { format: '50%%', styles: [] }
159
107
  * ```
160
108
  */
161
- function ansiToConsole(text) {
109
+ function ansiToConsole(text, palette) {
162
110
  const scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags);
163
111
  let active = Object.freeze({
164
112
  foreground: "",
@@ -195,23 +143,26 @@ function ansiToConsole(text) {
195
143
  });
196
144
  continue;
197
145
  }
198
- const foreground = FOREGROUND_CSS[code];
146
+ const foreground = COLORS.find((color) => FOREGROUND_CODES[color] === code);
199
147
  if (foreground !== void 0) {
148
+ const color = palette?.color?.[foreground] ?? COLOR_HEX[foreground];
200
149
  active = Object.freeze({
201
150
  ...active,
202
- foreground
151
+ foreground: `color:${color}`
203
152
  });
204
153
  continue;
205
154
  }
206
- const background = BACKGROUND_CSS[code];
155
+ const background = COLORS.find((color) => BACKGROUND_CODES[color] === code);
207
156
  if (background !== void 0) {
157
+ const color = palette?.color?.[background] ?? COLOR_HEX[background];
208
158
  active = Object.freeze({
209
159
  ...active,
210
- background
160
+ background: `background:${color}`
211
161
  });
212
162
  continue;
213
163
  }
214
- const attribute = ATTRIBUTE_CSS[code];
164
+ const name = ATTRIBUTES.find((attribute) => ATTRIBUTE_CODES[attribute] === code);
165
+ const attribute = name === void 0 ? void 0 : palette?.attribute?.[name] ?? ATTRIBUTE_CSS[code];
215
166
  if (attribute !== void 0 && !active.attributes.includes(attribute)) active = Object.freeze({
216
167
  ...active,
217
168
  attributes: Object.freeze([...active.attributes, attribute])
@@ -270,12 +221,14 @@ function parseParameters(parameters) {
270
221
  * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the
271
222
  * core output to the browser console with no change to the core.
272
223
  *
224
+ * @param options - See {@link BrowserSinkOptions}
273
225
  * @returns A browser `%c` {@link SinkInterface}
274
226
  *
275
227
  * @remarks
276
228
  * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
277
229
  * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
278
230
  * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
231
+ * `options.palette` supplies partial named color and attribute overrides to that translation.
279
232
  * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an
280
233
  * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level
281
234
  * reaches the matching DevTools stream.
@@ -297,12 +250,12 @@ function parseParameters(parameters) {
297
250
  * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools
298
251
  * ```
299
252
  */
300
- function createBrowserSink() {
253
+ function createBrowserSink(options) {
301
254
  const log = console.log.bind(console);
302
255
  const warn = console.warn.bind(console);
303
256
  const error = console.error.bind(console);
304
257
  return { write(text, level) {
305
- const { format, styles } = ansiToConsole(text.startsWith("\r") ? text.slice(1) : text);
258
+ const { format, styles } = ansiToConsole(text.startsWith("\r") ? text.slice(1) : text, options?.palette);
306
259
  if (level === "error") {
307
260
  error(format, ...styles);
308
261
  return;
@@ -315,6 +268,6 @@ function createBrowserSink() {
315
268
  } };
316
269
  }
317
270
  //#endregion
318
- export { ATTRIBUTE_CSS, BACKGROUND_CSS, COLOR_HEX, DIRECTIVE, FOREGROUND_CSS, SGR_PATTERN, ansiToConsole, createBrowserSink, escapePercent, parseParameters };
271
+ export { ATTRIBUTE_CSS, COLOR_HEX, DIRECTIVE, SGR_PATTERN, ansiToConsole, createBrowserSink, escapePercent, parseParameters };
319
272
 
320
273
  //# sourceMappingURL=index.js.map
@@ -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, BACKGROUND_CODES, ESC, FOREGROUND_CODES } from '@src/core'\n\n// The SGR → CSS translation DATA the browser sink maps ANSI runs through (the C-f 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 each SGR number → its CSS declaration. Each number→CSS row is written as a\n// COMPUTED key over core's code maps with its value read from the palette, so neither the\n// number↔name mapping nor a hex value is re-hardcoded here; `tests/src/browser/helpers.test.ts`\n// walks core's `COLORS` and fails if a row is missing. This file holds DATA only — a derivation\n// written as a callback would be module function syntax in a data-kind file (AGENTS §5).\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 (AGENTS §5).\n\n/**\n * Each named {@link Color}'s 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 * Each text-{@link Attribute}'s SGR \"on\" number → 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 * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS. The sink reads this while\n * scanning a run to translate a foreground code to CSS.\n *\n * @remarks\n * Every key is core's {@link FOREGROUND_CODES} entry and every value reads {@link COLOR_HEX}, so\n * neither the number↔name mapping nor the palette is duplicated here — this is a table of\n * references, not of literals. `tests/src/browser/helpers.test.ts` walks core's {@link COLORS} and\n * asserts this record covers every one of them, so a color added to core fails there rather than\n * going silently untranslated. Deeply frozen.\n */\nexport const FOREGROUND_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[FOREGROUND_CODES.black]: `color:${COLOR_HEX.black}`,\n\t[FOREGROUND_CODES.red]: `color:${COLOR_HEX.red}`,\n\t[FOREGROUND_CODES.green]: `color:${COLOR_HEX.green}`,\n\t[FOREGROUND_CODES.yellow]: `color:${COLOR_HEX.yellow}`,\n\t[FOREGROUND_CODES.blue]: `color:${COLOR_HEX.blue}`,\n\t[FOREGROUND_CODES.magenta]: `color:${COLOR_HEX.magenta}`,\n\t[FOREGROUND_CODES.cyan]: `color:${COLOR_HEX.cyan}`,\n\t[FOREGROUND_CODES.white]: `color:${COLOR_HEX.white}`,\n\t[FOREGROUND_CODES.brightBlack]: `color:${COLOR_HEX.brightBlack}`,\n\t[FOREGROUND_CODES.brightRed]: `color:${COLOR_HEX.brightRed}`,\n\t[FOREGROUND_CODES.brightGreen]: `color:${COLOR_HEX.brightGreen}`,\n\t[FOREGROUND_CODES.brightYellow]: `color:${COLOR_HEX.brightYellow}`,\n\t[FOREGROUND_CODES.brightBlue]: `color:${COLOR_HEX.brightBlue}`,\n\t[FOREGROUND_CODES.brightMagenta]: `color:${COLOR_HEX.brightMagenta}`,\n\t[FOREGROUND_CODES.brightCyan]: `color:${COLOR_HEX.brightCyan}`,\n\t[FOREGROUND_CODES.brightWhite]: `color:${COLOR_HEX.brightWhite}`,\n})\n\n/**\n * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS. The sink reads this\n * while scanning a run to translate a background code to CSS.\n *\n * @remarks\n * Built the same way as {@link FOREGROUND_CSS} — keys from core's {@link BACKGROUND_CODES}, values\n * from {@link COLOR_HEX} — and covered by the same exhaustive walk over core's {@link COLORS} in\n * `tests/src/browser/helpers.test.ts`. Deeply frozen.\n */\nexport const BACKGROUND_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[BACKGROUND_CODES.black]: `background:${COLOR_HEX.black}`,\n\t[BACKGROUND_CODES.red]: `background:${COLOR_HEX.red}`,\n\t[BACKGROUND_CODES.green]: `background:${COLOR_HEX.green}`,\n\t[BACKGROUND_CODES.yellow]: `background:${COLOR_HEX.yellow}`,\n\t[BACKGROUND_CODES.blue]: `background:${COLOR_HEX.blue}`,\n\t[BACKGROUND_CODES.magenta]: `background:${COLOR_HEX.magenta}`,\n\t[BACKGROUND_CODES.cyan]: `background:${COLOR_HEX.cyan}`,\n\t[BACKGROUND_CODES.white]: `background:${COLOR_HEX.white}`,\n\t[BACKGROUND_CODES.brightBlack]: `background:${COLOR_HEX.brightBlack}`,\n\t[BACKGROUND_CODES.brightRed]: `background:${COLOR_HEX.brightRed}`,\n\t[BACKGROUND_CODES.brightGreen]: `background:${COLOR_HEX.brightGreen}`,\n\t[BACKGROUND_CODES.brightYellow]: `background:${COLOR_HEX.brightYellow}`,\n\t[BACKGROUND_CODES.brightBlue]: `background:${COLOR_HEX.brightBlue}`,\n\t[BACKGROUND_CODES.brightMagenta]: `background:${COLOR_HEX.brightMagenta}`,\n\t[BACKGROUND_CODES.brightCyan]: `background:${COLOR_HEX.brightCyan}`,\n\t[BACKGROUND_CODES.brightWhite]: `background:${COLOR_HEX.brightWhite}`,\n})\n\n/**\n * 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 { ConsoleOutput, StyleAccumulator } from './types.js'\nimport { RESET_CODE } from '@src/core'\nimport {\n\tATTRIBUTE_CSS,\n\tBACKGROUND_CSS,\n\tDIRECTIVE,\n\tFOREGROUND_CSS,\n\tSGR_PATTERN,\n} from './constants.js'\n\n// The pure, browser-only translation behind the `%c` console sink (the C-f branch). The core\n// styler / Logger / Reporter emit ANSI-styled STRINGS; a DevTools console can't render ANSI but\n// can style via `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` / `parseParameters` utilities are\n// exported alongside it.\n\n/**\n * Translate 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 C-f 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 * - **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 * @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): 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 empties all three. Serialized to a `;`-joined CSS string per emitted run.\n\tlet active: StyleAccumulator = Object.freeze({\n\t\tforeground: '',\n\t\tbackground: '',\n\t\tattributes: Object.freeze([]),\n\t})\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 !== '') declarations.push(active.foreground)\n\t\t\tif (active.background !== '') 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 parseParameters(match[1] ?? '')) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\tforeground: '',\n\t\t\t\t\tbackground: '',\n\t\t\t\t\tattributes: Object.freeze([]),\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst foreground = FOREGROUND_CSS[code]\n\t\t\tif (foreground !== undefined) {\n\t\t\t\tactive = Object.freeze({ ...active, foreground })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst background = BACKGROUND_CSS[code]\n\t\t\tif (background !== undefined) {\n\t\t\t\tactive = Object.freeze({ ...active, background })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst attribute = 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 * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (e.g. 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 * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since\n * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)\n * likewise counts as a `0` reset, matching the spec.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)\n *\n * @example\n * ```ts\n * parseParameters('1;31') // [1, 31]\n * parseParameters('') // [0]\n * ```\n */\nexport function parseParameters(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 { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the C-f 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 via `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// (AGENTS §5). `SinkInterface` / `LogLevel` are IMPORTED from `@src/core`, never redeclared.\n\n/**\n * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in\n * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the\n * core output to the browser console with no change to the core.\n *\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 * - **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.\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, AGENTS / the core sink's\n * precedent). Create the sink (or the logger) BEFORE installing a capture.\n *\n * @example\n * ```ts\n * import { createLogger } from '@src/core'\n * import { createBrowserSink } from '@src/browser'\n *\n * const logger = createLogger({ name: 'app', sink: createBrowserSink() })\n * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools\n * ```\n */\nexport function createBrowserSink(): SinkInterface {\n\t// Snapshot the three console writers NOW — 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)\n\t\t\tif (level === 'error') {\n\t\t\t\terror(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\twarn(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,IAAa,YAAiE,OAAO,OAAO;CAC3F,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,MAAM;CACN,SAAS;CACT,MAAM;CACN,OAAO;CACP,aAAa;CACb,WAAW;CACX,aAAa;CACb,cAAc;CACd,YAAY;CACZ,eAAe;CACf,YAAY;CACZ,aAAa;AACd,CAAC;;;;;;;;;;;;;;AAeD,IAAa,gBAAkD,OAAO,OAAO;EAC3E,gBAAgB,OAAO;EACvB,gBAAgB,MAAM;EACtB,gBAAgB,SAAS;EACzB,gBAAgB,YAAY;EAC5B,gBAAgB,UAAU;EAC1B,gBAAgB,gBAAgB;AAClC,CAAC;;;;;;;;;;;;AAaD,IAAa,iBAAmD,OAAO,OAAO;EAC5E,iBAAiB,QAAQ,SAAS,UAAU;EAC5C,iBAAiB,MAAM,SAAS,UAAU;EAC1C,iBAAiB,QAAQ,SAAS,UAAU;EAC5C,iBAAiB,SAAS,SAAS,UAAU;EAC7C,iBAAiB,OAAO,SAAS,UAAU;EAC3C,iBAAiB,UAAU,SAAS,UAAU;EAC9C,iBAAiB,OAAO,SAAS,UAAU;EAC3C,iBAAiB,QAAQ,SAAS,UAAU;EAC5C,iBAAiB,cAAc,SAAS,UAAU;EAClD,iBAAiB,YAAY,SAAS,UAAU;EAChD,iBAAiB,cAAc,SAAS,UAAU;EAClD,iBAAiB,eAAe,SAAS,UAAU;EACnD,iBAAiB,aAAa,SAAS,UAAU;EACjD,iBAAiB,gBAAgB,SAAS,UAAU;EACpD,iBAAiB,aAAa,SAAS,UAAU;EACjD,iBAAiB,cAAc,SAAS,UAAU;AACpD,CAAC;;;;;;;;;;AAWD,IAAa,iBAAmD,OAAO,OAAO;EAC5E,iBAAiB,QAAQ,cAAc,UAAU;EACjD,iBAAiB,MAAM,cAAc,UAAU;EAC/C,iBAAiB,QAAQ,cAAc,UAAU;EACjD,iBAAiB,SAAS,cAAc,UAAU;EAClD,iBAAiB,OAAO,cAAc,UAAU;EAChD,iBAAiB,UAAU,cAAc,UAAU;EACnD,iBAAiB,OAAO,cAAc,UAAU;EAChD,iBAAiB,QAAQ,cAAc,UAAU;EACjD,iBAAiB,cAAc,cAAc,UAAU;EACvD,iBAAiB,YAAY,cAAc,UAAU;EACrD,iBAAiB,cAAc,cAAc,UAAU;EACvD,iBAAiB,eAAe,cAAc,UAAU;EACxD,iBAAiB,aAAa,cAAc,UAAU;EACtD,iBAAiB,gBAAgB,cAAc,UAAU;EACzD,iBAAiB,aAAa,cAAc,UAAU;EACtD,iBAAiB,cAAc,cAAc,UAAU;AACzD,CAAC;;;;;;AAOD,IAAa,YAAY;;;;;;;;;;;;;;AAezB,IAAa,cAAc,IAAI,OAAO,GAAG,IAAI,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7FhE,SAAgB,cAAc,MAA6B;CAC1D,MAAM,UAAU,IAAI,OAAO,YAAY,QAAQ,YAAY,KAAK;CAIhE,IAAI,SAA2B,OAAO,OAAO;EAC5C,YAAY;EACZ,YAAY;EACZ,YAAY,OAAO,OAAO,CAAC,CAAC;CAC7B,CAAC;CACD,MAAM,WAAqB,CAAC;CAC5B,MAAM,SAAmB,CAAC;CAC1B,IAAI,SAAS;CACb,IAAI,UAAU;CACd,IAAI,QAAgC,QAAQ,KAAK,IAAI;CACrD,IAAI,UAAU,MAAM,OAAO;EAAE,QAAQ,cAAc,IAAI;EAAG,QAAQ,CAAC;CAAE;CAGrE,OAAO,MAAM;EACZ,MAAM,WAAW,UAAU,OAAO,KAAK,SAAS,MAAM;EACtD,WAAW,cAAc,KAAK,MAAM,QAAQ,QAAQ,CAAC;EACrD,IAAI,YAAY,IAAI;GACnB,SAAS,KAAK,KAAe,SAAS;GACtC,MAAM,eAAe,CAAC,GAAG,OAAO,UAAU;GAC1C,IAAI,OAAO,eAAe,IAAI,aAAa,KAAK,OAAO,UAAU;GACjE,IAAI,OAAO,eAAe,IAAI,aAAa,KAAK,OAAO,UAAU;GACjE,OAAO,KAAK,aAAa,KAAK,GAAG,CAAC;GAClC,UAAU;EACX;EACA,IAAI,UAAU,MAAM;EAIpB,KAAK,MAAM,QAAQ,gBAAgB,MAAM,MAAM,EAAE,GAAG;GACnD,IAAI,SAAS,YAAY;IACxB,SAAS,OAAO,OAAO;KACtB,YAAY;KACZ,YAAY;KACZ,YAAY,OAAO,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD;GACD;GACA,MAAM,aAAa,eAAe;GAClC,IAAI,eAAe,KAAA,GAAW;IAC7B,SAAS,OAAO,OAAO;KAAE,GAAG;KAAQ;IAAW,CAAC;IAChD;GACD;GACA,MAAM,aAAa,eAAe;GAClC,IAAI,eAAe,KAAA,GAAW;IAC7B,SAAS,OAAO,OAAO;KAAE,GAAG;KAAQ;IAAW,CAAC;IAChD;GACD;GACA,MAAM,YAAY,cAAc;GAChC,IAAI,cAAc,KAAA,KAAa,CAAC,OAAO,WAAW,SAAS,SAAS,GACnE,SAAS,OAAO,OAAO;IACtB,GAAG;IACH,YAAY,OAAO,OAAO,CAAC,GAAG,OAAO,YAAY,SAAS,CAAC;GAC5D,CAAC;EAEH;EACA,SAAS,MAAM,QAAQ,MAAM,EAAE,CAAC;EAChC,QAAQ,QAAQ,KAAK,IAAI;CAC1B;CACA,OAAO;EAAE,QAAQ,SAAS,KAAK,EAAE;EAAG;CAAO;AAC5C;;;;;;;;;;;;;;;AAgBA,SAAgB,cAAc,MAAsB;CACnD,OAAO,KAAK,QAAQ,MAAM,IAAI;AAC/B;;;;;;;;;;;;;;;;AAiBA,SAAgB,gBAAgB,YAAuC;CACtE,IAAI,eAAe,IAAI,OAAO,CAAC,UAAU;CACzC,OAAO,WAAW,MAAM,GAAG,CAAC,CAAC,KAAK,UAAW,UAAU,KAAK,aAAa,OAAO,KAAK,CAAE;AACxF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3GA,SAAgB,oBAAmC;CAIlD,MAAM,MAAM,QAAQ,IAAI,KAAK,OAAO;CACpC,MAAM,OAAO,QAAQ,KAAK,KAAK,OAAO;CACtC,MAAM,QAAQ,QAAQ,MAAM,KAAK,OAAO;CACxC,OAAO,EACN,MAAM,MAAc,OAAwB;EAI3C,MAAM,EAAE,QAAQ,WAAW,cADd,KAAK,WAAW,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,IACR;EAC7C,IAAI,UAAU,SAAS;GACtB,MAAM,QAAQ,GAAG,MAAM;GACvB;EACD;EACA,IAAI,UAAU,QAAQ;GACrB,KAAK,QAAQ,GAAG,MAAM;GACtB;EACD;EACA,IAAI,QAAQ,GAAG,MAAM;CACtB,EACD;AACD"}
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 C-f 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 (AGENTS §5).\n\n/**\n * Each named {@link Color}'s 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 * Each text-{@link Attribute}'s SGR \"on\" number → 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 * 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 C-f branch). The core\n// styler / Logger / Reporter emit ANSI-styled STRINGS; a DevTools console can't render ANSI but\n// can style via `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` / `parseParameters` utilities are\n// exported alongside it.\n\n/**\n * Translate 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 C-f 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 empties all three. Serialized to a `;`-joined CSS string per emitted run.\n\tlet active: StyleAccumulator = Object.freeze({\n\t\tforeground: '',\n\t\tbackground: '',\n\t\tattributes: Object.freeze([]),\n\t})\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 !== '') declarations.push(active.foreground)\n\t\t\tif (active.background !== '') 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 parseParameters(match[1] ?? '')) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\tforeground: '',\n\t\t\t\t\tbackground: '',\n\t\t\t\t\tattributes: Object.freeze([]),\n\t\t\t\t})\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 * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (e.g. 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 * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since\n * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)\n * likewise counts as a `0` reset, matching the spec.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)\n *\n * @example\n * ```ts\n * parseParameters('1;31') // [1, 31]\n * parseParameters('') // [0]\n * ```\n */\nexport function parseParameters(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 { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the C-f 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 via `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// (AGENTS §5). `SinkInterface` / `LogLevel` are IMPORTED from `@src/core`, never redeclared.\n\n/**\n * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in\n * as a logger / reporter / spinner sink (`createLogger({ 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.\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, AGENTS / the core sink's\n * precedent). Create the sink (or the logger) BEFORE installing a capture.\n *\n * @example\n * ```ts\n * import { createLogger } from '@src/core'\n * import { createBrowserSink } from '@src/browser'\n *\n * const logger = createLogger({ 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 three console writers NOW — 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\tif (level === 'error') {\n\t\t\t\terror(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\twarn(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":"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"}