@orkestrel/console 0.0.11 → 0.0.13
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 +23 -21
- package/dist/src/browser/index.d.ts +85 -74
- package/dist/src/browser/index.js +69 -70
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +990 -1143
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +826 -757
- package/dist/src/core/index.d.ts +826 -757
- package/dist/src/core/index.js +987 -1134
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +130 -153
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +222 -199
- package/dist/src/server/index.d.ts +222 -199
- package/dist/src/server/index.js +130 -151
- package/dist/src/server/index.js.map +1 -1
- package/package.json +12 -13
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES, RESET_CODE } from "../core/index.js";
|
|
1
|
+
import { ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES, RESET_CODE, selectWriter } from "../core/index.js";
|
|
2
2
|
//#region src/browser/constants.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* console renders the
|
|
4
|
+
* Maps each named {@link Color} to its hex value — the 16 standard terminal colors a browser DevTools
|
|
5
|
+
* console renders the same {@link Color} names as. The source of truth for the browser color
|
|
6
6
|
* axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to
|
|
7
7
|
* the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.
|
|
8
8
|
*
|
|
@@ -29,14 +29,15 @@ var COLOR_HEX = Object.freeze({
|
|
|
29
29
|
brightWhite: "#ffffff"
|
|
30
30
|
});
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
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,21 +51,21 @@ var ATTRIBUTE_CSS = Object.freeze({
|
|
|
50
51
|
[ATTRIBUTE_CODES.strikethrough]: "text-decoration:line-through"
|
|
51
52
|
});
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
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
|
/**
|
|
59
|
-
* Matches one SGR sequence (`ESC[ <params> m`) and
|
|
60
|
-
* the subset of ANSI {@link import('@src/core').strip} cares about that carries
|
|
60
|
+
* Matches one SGR sequence (`ESC[ <params> m`) and captures its `;`-separated numeric parameters —
|
|
61
|
+
* the subset of ANSI {@link import('@src/core').strip} cares about that carries style (color /
|
|
61
62
|
* attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks
|
|
62
63
|
* every SGR run in a string; built from core's {@link ESC} so no control-character literal appears
|
|
63
64
|
* in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,
|
|
64
65
|
* which the spec treats as a reset).
|
|
65
66
|
*
|
|
66
67
|
* @remarks
|
|
67
|
-
* A global `RegExp` carries a mutable `lastIndex`; a scan builds a
|
|
68
|
+
* A global `RegExp` carries a mutable `lastIndex`; a scan builds a fresh `RegExp` from this one's
|
|
68
69
|
* `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the
|
|
69
70
|
* canonical definition, not a shared scanner.
|
|
70
71
|
*/
|
|
@@ -72,22 +73,25 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
|
|
|
72
73
|
//#endregion
|
|
73
74
|
//#region src/browser/helpers.ts
|
|
74
75
|
/**
|
|
75
|
-
*
|
|
76
|
-
* `%c`-segmented format string and the parallel array of CSS declarations,
|
|
77
|
-
*
|
|
76
|
+
* Translates an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
|
|
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
|
-
* `ESC[…m`); each delimits a run. A run carrying
|
|
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
|
|
83
87
|
* each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare
|
|
84
|
-
* `ESC[m`) clears the accumulated style back to none. A later color of the same channel
|
|
88
|
+
* `ESC[m`) clears the accumulated style back to none. A later color of the same channel replaces
|
|
85
89
|
* the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style
|
|
86
90
|
* and are left in the text verbatim.
|
|
87
|
-
* - **`%`-safe.** Every
|
|
91
|
+
* - **`%`-safe.** Every literal `%` in the text is doubled to `%%` so the console never treats it as
|
|
88
92
|
* a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`
|
|
89
93
|
* count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.
|
|
90
|
-
* - **Plain text short-circuits.** A string with
|
|
94
|
+
* - **Plain text short-circuits.** A string with no SGR sequence yields `{ format: <escaped text>,
|
|
91
95
|
* styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).
|
|
92
96
|
* - **Partial palette.** A supplied palette overrides only its named colors and attributes. Every
|
|
93
97
|
* omitted entry resolves through {@link COLOR_HEX} or {@link ATTRIBUTE_CSS}, so defaults and
|
|
@@ -108,11 +112,7 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
|
|
|
108
112
|
*/
|
|
109
113
|
function ansiToConsole(text, palette) {
|
|
110
114
|
const scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags);
|
|
111
|
-
let active = Object.freeze({
|
|
112
|
-
foreground: "",
|
|
113
|
-
background: "",
|
|
114
|
-
attributes: Object.freeze([])
|
|
115
|
-
});
|
|
115
|
+
let active = Object.freeze({ attributes: Object.freeze([]) });
|
|
116
116
|
const segments = [];
|
|
117
117
|
const styles = [];
|
|
118
118
|
let cursor = 0;
|
|
@@ -128,19 +128,15 @@ function ansiToConsole(text, palette) {
|
|
|
128
128
|
if (pending !== "") {
|
|
129
129
|
segments.push(`%c${pending}`);
|
|
130
130
|
const declarations = [...active.attributes];
|
|
131
|
-
if (active.foreground !==
|
|
132
|
-
if (active.background !==
|
|
131
|
+
if (active.foreground !== void 0) declarations.push(active.foreground);
|
|
132
|
+
if (active.background !== void 0) declarations.push(active.background);
|
|
133
133
|
styles.push(declarations.join(";"));
|
|
134
134
|
pending = "";
|
|
135
135
|
}
|
|
136
136
|
if (match === null) break;
|
|
137
|
-
for (const code of
|
|
137
|
+
for (const code of scanParameters(match[1] ?? "")) {
|
|
138
138
|
if (code === RESET_CODE) {
|
|
139
|
-
active = Object.freeze({
|
|
140
|
-
foreground: "",
|
|
141
|
-
background: "",
|
|
142
|
-
attributes: Object.freeze([])
|
|
143
|
-
});
|
|
139
|
+
active = Object.freeze({ attributes: Object.freeze([]) });
|
|
144
140
|
continue;
|
|
145
141
|
}
|
|
146
142
|
const foreground = COLORS.find((color) => FOREGROUND_CODES[color] === code);
|
|
@@ -177,8 +173,8 @@ function ansiToConsole(text, palette) {
|
|
|
177
173
|
};
|
|
178
174
|
}
|
|
179
175
|
/**
|
|
180
|
-
*
|
|
181
|
-
* reading a stray `%` (
|
|
176
|
+
* Doubles every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from
|
|
177
|
+
* reading a stray `%` (for example in `50%` or `%s`) as a format directive. The single escape the
|
|
182
178
|
* {@link ansiToConsole} translation applies to every text segment before assembling the format
|
|
183
179
|
* string (so only the `%c`s it inserts are real directives).
|
|
184
180
|
*
|
|
@@ -194,59 +190,66 @@ function escapePercent(text) {
|
|
|
194
190
|
return text.replace(/%/g, "%%");
|
|
195
191
|
}
|
|
196
192
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* likewise counts as a `0` reset, matching the spec.
|
|
193
|
+
* Walks an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})
|
|
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.
|
|
201
196
|
*
|
|
202
197
|
* @param parameters - The raw `;`-separated parameter string (the regex capture)
|
|
203
|
-
* @returns The
|
|
198
|
+
* @returns The SGR codes found (a parameterless / empty field becoming `0`)
|
|
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
204
|
*
|
|
205
205
|
* @example
|
|
206
206
|
* ```ts
|
|
207
|
-
*
|
|
208
|
-
*
|
|
207
|
+
* scanParameters('1;31') // [1, 31]
|
|
208
|
+
* scanParameters('') // [0]
|
|
209
209
|
* ```
|
|
210
210
|
*/
|
|
211
|
-
function
|
|
211
|
+
function scanParameters(parameters) {
|
|
212
212
|
if (parameters === "") return [RESET_CODE];
|
|
213
213
|
return parameters.split(";").map((field) => field === "" ? RESET_CODE : Number(field));
|
|
214
214
|
}
|
|
215
215
|
//#endregion
|
|
216
216
|
//#region src/browser/factories.ts
|
|
217
217
|
/**
|
|
218
|
-
*
|
|
218
|
+
* Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`
|
|
219
219
|
* translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* 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.
|
|
223
222
|
*
|
|
224
223
|
* @param options - See {@link BrowserSinkOptions}
|
|
225
224
|
* @returns A browser `%c` {@link SinkInterface}
|
|
226
225
|
*
|
|
227
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
|
+
*
|
|
228
230
|
* - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
|
|
229
231
|
* re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
|
|
230
232
|
* — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
|
|
231
233
|
* `options.palette` supplies partial named color and attribute overrides to that translation.
|
|
232
234
|
* - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an
|
|
233
|
-
* omitted level) → `console.log` — the
|
|
234
|
-
* reaches the matching DevTools stream.
|
|
235
|
+
* omitted level) → `console.log` — the same routing as core's `createConsoleSink`, so a logger's level
|
|
236
|
+
* reaches the matching DevTools stream. Both call the one
|
|
237
|
+
* {@link import('@src/core').selectWriter} leaf, which is what keeps them identical.
|
|
235
238
|
* - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with
|
|
236
|
-
* a carriage return `\r` (a spinner / progress redraw) has the leading `\r`
|
|
237
|
-
* fresh, non-overwriting line — the locked browser degrade. Only a
|
|
239
|
+
* a carriage return `\r` (a spinner / progress redraw) has the leading `\r` stripped and is written as a
|
|
240
|
+
* fresh, non-overwriting line — the locked browser degrade. Only a leading `\r` is stripped; an interior
|
|
238
241
|
* one is left to the console.
|
|
239
|
-
* - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error`
|
|
240
|
-
*
|
|
241
|
-
* feed this sink's output back into itself (the no-capture-loop principle,
|
|
242
|
-
* precedent). Create the sink (or the logger)
|
|
242
|
+
* - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` at
|
|
243
|
+
* creation and writes through those references, so a later `Capture` that patches `console.*` can never
|
|
244
|
+
* feed this sink's output back into itself (the no-capture-loop principle, following the core
|
|
245
|
+
* sink's precedent). Create the sink (or the logger) before installing a capture.
|
|
243
246
|
*
|
|
244
247
|
* @example
|
|
245
248
|
* ```ts
|
|
246
|
-
* import {
|
|
247
|
-
* import { createBrowserSink } from '@
|
|
249
|
+
* import { Logger } from '@orkestrel/console'
|
|
250
|
+
* import { createBrowserSink } from '@orkestrel/console/browser'
|
|
248
251
|
*
|
|
249
|
-
* const logger =
|
|
252
|
+
* const logger = new Logger({ name: 'app', sink: createBrowserSink() })
|
|
250
253
|
* logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools
|
|
251
254
|
* ```
|
|
252
255
|
*/
|
|
@@ -256,18 +259,14 @@ function createBrowserSink(options) {
|
|
|
256
259
|
const error = console.error.bind(console);
|
|
257
260
|
return { write(text, level) {
|
|
258
261
|
const { format, styles } = ansiToConsole(text.startsWith("\r") ? text.slice(1) : text, options?.palette);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
warn(format, ...styles);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
log(format, ...styles);
|
|
262
|
+
selectWriter(level, {
|
|
263
|
+
log,
|
|
264
|
+
warn,
|
|
265
|
+
error
|
|
266
|
+
})(format, ...styles);
|
|
268
267
|
} };
|
|
269
268
|
}
|
|
270
269
|
//#endregion
|
|
271
|
-
export { ATTRIBUTE_CSS, COLOR_HEX, DIRECTIVE, SGR_PATTERN, ansiToConsole, createBrowserSink, escapePercent,
|
|
270
|
+
export { ATTRIBUTE_CSS, COLOR_HEX, DIRECTIVE, SGR_PATTERN, ansiToConsole, createBrowserSink, escapePercent, scanParameters };
|
|
272
271
|
|
|
273
272
|
//# 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, 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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"}
|
|
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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"}
|