@orkestrel/console 0.0.16 → 0.0.17

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.
@@ -113,6 +113,14 @@ export declare interface ConsoleOutput {
113
113
  readonly styles: readonly string[];
114
114
  }
115
115
 
116
+ /** Records the style and visible text at one ANSI scan boundary. */
117
+ export declare interface ConsoleSegment {
118
+ readonly active: StyleAccumulator;
119
+ readonly cursor: number;
120
+ readonly text: string;
121
+ readonly style: string;
122
+ }
123
+
116
124
  /**
117
125
  * Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`
118
126
  * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
@@ -1,5 +1,5 @@
1
1
  import { ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES, RESET_CODE, selectWriter } from "../core/index.js";
2
- //#region src/browser/constants.ts
2
+ // src/browser/constants.ts
3
3
  /**
4
4
  * Maps each named {@link Color} to its hex value — the 16 standard terminal colors a browser DevTools
5
5
  * console renders the same {@link Color} names as. The source of truth for the browser color
@@ -70,8 +70,7 @@ var DIRECTIVE = "%c";
70
70
  * canonical definition, not a shared scanner.
71
71
  */
72
72
  var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
73
- //#endregion
74
- //#region src/browser/helpers.ts
73
+ // src/browser/helpers.ts
75
74
  /**
76
75
  * Translates an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
77
76
  * `%c`-segmented format string and the parallel array of CSS declarations, an optional partial
@@ -111,65 +110,60 @@ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
111
110
  * ```
112
111
  */
113
112
  function ansiToConsole(text, palette) {
114
- const scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags);
115
- let active = Object.freeze({ attributes: Object.freeze([]) });
116
- const segments = [];
117
- const styles = [];
118
- let cursor = 0;
119
- let pending = "";
120
- let match = scanner.exec(text);
121
- if (match === null) return {
113
+ const matches = [...text.matchAll(new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags))];
114
+ if (matches.length === 0) return {
122
115
  format: escapePercent(text),
123
116
  styles: []
124
117
  };
125
- while (true) {
126
- const boundary = match === null ? text.length : match.index;
127
- pending += escapePercent(text.slice(cursor, boundary));
128
- if (pending !== "") {
129
- segments.push(`%c${pending}`);
130
- const declarations = [...active.attributes];
131
- if (active.foreground !== void 0) declarations.push(active.foreground);
132
- if (active.background !== void 0) declarations.push(active.background);
133
- styles.push(declarations.join(";"));
134
- pending = "";
135
- }
136
- if (match === null) break;
137
- for (const code of scanParameters(match[1] ?? "")) {
138
- if (code === RESET_CODE) {
139
- active = Object.freeze({ attributes: Object.freeze([]) });
140
- continue;
141
- }
118
+ const initial = Object.freeze({ attributes: Object.freeze([]) });
119
+ const output = [];
120
+ for (const match of [...matches, void 0]) {
121
+ const previous = output.at(-1) ?? {
122
+ active: initial,
123
+ cursor: 0
124
+ };
125
+ const segment = escapePercent(text.slice(previous.cursor, match?.index ?? text.length));
126
+ const declarations = [
127
+ ...previous.active.attributes,
128
+ ...previous.active.foreground === void 0 ? [] : [previous.active.foreground],
129
+ ...previous.active.background === void 0 ? [] : [previous.active.background]
130
+ ];
131
+ const active = scanParameters(match?.[1] ?? "").reduce((style, code) => {
132
+ if (code === RESET_CODE) return initial;
142
133
  const foreground = COLORS.find((color) => FOREGROUND_CODES[color] === code);
143
134
  if (foreground !== void 0) {
144
135
  const color = palette?.color?.[foreground] ?? COLOR_HEX[foreground];
145
- active = Object.freeze({
146
- ...active,
136
+ return Object.freeze({
137
+ ...style,
147
138
  foreground: `color:${color}`
148
139
  });
149
- continue;
150
140
  }
151
141
  const background = COLORS.find((color) => BACKGROUND_CODES[color] === code);
152
142
  if (background !== void 0) {
153
143
  const color = palette?.color?.[background] ?? COLOR_HEX[background];
154
- active = Object.freeze({
155
- ...active,
144
+ return Object.freeze({
145
+ ...style,
156
146
  background: `background:${color}`
157
147
  });
158
- continue;
159
148
  }
160
149
  const name = ATTRIBUTES.find((attribute) => ATTRIBUTE_CODES[attribute] === code);
161
150
  const attribute = name === void 0 ? void 0 : palette?.attribute?.[name] ?? ATTRIBUTE_CSS[code];
162
- if (attribute !== void 0 && !active.attributes.includes(attribute)) active = Object.freeze({
163
- ...active,
164
- attributes: Object.freeze([...active.attributes, attribute])
151
+ return attribute === void 0 || style.attributes.includes(attribute) ? style : Object.freeze({
152
+ ...style,
153
+ attributes: Object.freeze([...style.attributes, attribute])
165
154
  });
166
- }
167
- cursor = match.index + match[0].length;
168
- match = scanner.exec(text);
155
+ }, previous.active);
156
+ output.push({
157
+ active,
158
+ cursor: match === void 0 ? text.length : match.index + match[0].length,
159
+ text: segment,
160
+ style: declarations.join(";")
161
+ });
169
162
  }
163
+ const visible = output.filter((segment) => segment.text !== "");
170
164
  return {
171
- format: segments.join(""),
172
- styles
165
+ format: visible.map((segment) => `%c${segment.text}`).join(""),
166
+ styles: visible.map((segment) => segment.style)
173
167
  };
174
168
  }
175
169
  /**
@@ -212,8 +206,7 @@ function scanParameters(parameters) {
212
206
  if (parameters === "") return [RESET_CODE];
213
207
  return parameters.split(";").map((field) => field === "" ? RESET_CODE : Number(field));
214
208
  }
215
- //#endregion
216
- //#region src/browser/factories.ts
209
+ // src/browser/factories.ts
217
210
  /**
218
211
  * Creates the browser `%c` {@link SinkInterface} — the browser output backend. `write(text, level?)`
219
212
  * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
@@ -266,7 +259,6 @@ function createBrowserSink(options) {
266
259
  })(format, ...styles);
267
260
  } };
268
261
  }
269
- //#endregion
270
262
  export { ATTRIBUTE_CSS, COLOR_HEX, DIRECTIVE, SGR_PATTERN, ansiToConsole, createBrowserSink, escapePercent, scanParameters };
271
263
 
272
264
  //# 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 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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"}
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, ConsoleSegment, 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 matches = [...text.matchAll(new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags))]\n\tif (matches.length === 0) return { format: escapePercent(text), styles: [] }\n\tconst initial: StyleAccumulator = Object.freeze({ attributes: Object.freeze([]) })\n\tconst output: ConsoleSegment[] = []\n\tfor (const match of [...matches, undefined]) {\n\t\tconst previous = output.at(-1) ?? { active: initial, cursor: 0 }\n\t\tconst segment = escapePercent(text.slice(previous.cursor, match?.index ?? text.length))\n\t\tconst declarations = [\n\t\t\t...previous.active.attributes,\n\t\t\t...(previous.active.foreground === undefined ? [] : [previous.active.foreground]),\n\t\t\t...(previous.active.background === undefined ? [] : [previous.active.background]),\n\t\t]\n\t\tconst active = scanParameters(match?.[1] ?? '').reduce<StyleAccumulator>((style, code) => {\n\t\t\tif (code === RESET_CODE) return initial\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\treturn Object.freeze({ ...style, foreground: `color:${color}` })\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\treturn Object.freeze({ ...style, background: `background:${color}` })\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\treturn attribute === undefined || style.attributes.includes(attribute)\n\t\t\t\t? style\n\t\t\t\t: Object.freeze({ ...style, attributes: Object.freeze([...style.attributes, attribute]) })\n\t\t}, previous.active)\n\t\toutput.push({\n\t\t\tactive,\n\t\t\tcursor: match === undefined ? text.length : match.index + match[0].length,\n\t\t\ttext: segment,\n\t\t\tstyle: declarations.join(';'),\n\t\t})\n\t}\n\tconst visible = output.filter((segment) => segment.text !== '')\n\treturn {\n\t\tformat: visible.map((segment) => `${DIRECTIVE}${segment.text}`).join(''),\n\t\tstyles: visible.map((segment) => segment.style),\n\t}\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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"}
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_emitter = require("@orkestrel/emitter");
4
- //#region src/core/constants.ts
4
+ // src/core/constants.ts
5
5
  /**
6
6
  * Maps each {@link Color} to its SGR foreground parameter — the 8 base colors at 30–37 and their
7
7
  * bright variants at 90–97. `default` is intentionally absent (it emits no code).
@@ -112,7 +112,7 @@ var BEL = String.fromCharCode(7);
112
112
  /** Holds the Control Sequence Introducer (`ESC[`) that opens every SGR sequence. */
113
113
  var CSI = `[`;
114
114
  /** Holds the full SGR reset sequence (`ESC[0m`) appended after a styled run. */
115
- var RESET = `${CSI}0m`;
115
+ var RESET = ``;
116
116
  /**
117
117
  * Matches any ANSI/VT escape sequence — CSI (SGR color/style plus cursor/erase/scroll,
118
118
  * including colon-parameterized SGR), OSC / DCS / PM / APC / SOS string sequences
@@ -485,8 +485,7 @@ var DEFAULT_THEME = Object.freeze({
485
485
  }),
486
486
  chrome: Object.freeze({ attributes: Object.freeze(["dim"]) })
487
487
  });
488
- //#endregion
489
- //#region src/core/errors.ts
488
+ // src/core/errors.ts
490
489
  /**
491
490
  * Carries a {@link ConsoleErrorCode} and an optional `context` bag — the error the console layer
492
491
  * throws for an internal invariant violated at a defensive guard.
@@ -500,6 +499,7 @@ var ConsoleError = class extends Error {
500
499
  context;
501
500
  constructor(code, message, context) {
502
501
  super(message);
502
+ Object.defineProperty(this, Symbol.for("@orkestrel/console.ConsoleError"), { value: this });
503
503
  this.name = "ConsoleError";
504
504
  this.code = code;
505
505
  if (context !== void 0) this.context = context;
@@ -521,10 +521,14 @@ var ConsoleError = class extends Error {
521
521
  * ```
522
522
  */
523
523
  function isConsoleError(value) {
524
- return (0, _orkestrel_contract.isInstance)(value, ConsoleError);
524
+ return (0, _orkestrel_contract.holds)(() => {
525
+ if (!(0, _orkestrel_contract.isInstance)(value, Error) || _orkestrel_contract.INTRINSICS.prototype(value) === Error.prototype) return false;
526
+ if (_orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/console.ConsoleError"))?.value !== value) return false;
527
+ const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
528
+ return _orkestrel_contract.INTRINSICS.reflect.read(value, "code") === "INVARIANT" && (0, _orkestrel_contract.isString)(_orkestrel_contract.INTRINSICS.reflect.read(value, "message")) && (context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context));
529
+ });
525
530
  }
526
- //#endregion
527
- //#region src/core/helpers.ts
531
+ // src/core/helpers.ts
528
532
  /**
529
533
  * Removes every ANSI escape sequence from `text`, returning the plain visible string.
530
534
  *
@@ -888,23 +892,32 @@ function renderBox(options) {
888
892
  const inner = lines.reduce((max, line) => Math.max(max, width(line)), Math.max(0, titleRoom, budget));
889
893
  const gutter = " ".repeat(padding);
890
894
  const bar = paint(styler, chars.vertical, options.style);
891
- const span = inner + padding * 2;
892
- let top;
893
- if (options.title === void 0) top = paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`, options.style);
895
+ const top = renderBoxTop(options, inner + padding * 2);
896
+ const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`, options.style);
897
+ return [
898
+ top,
899
+ ...lines.map((line) => `${bar}${gutter}${align(line, inner)}${gutter}${bar}`),
900
+ bottom
901
+ ].join("\n");
902
+ }
903
+ /**
904
+ * Renders a box's top border across the supplied inner span.
905
+ * @example
906
+ * ```ts
907
+ * renderBoxTop({ content: '' }, 3) // '┌───┐'
908
+ * ```
909
+ */
910
+ function renderBoxTop(options, span) {
911
+ const chars = BORDER_CHARS[options.border ?? "single"];
912
+ const styler = options.styler;
913
+ if (options.title === void 0) return paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`, options.style);
894
914
  else {
895
915
  const caption = ` ${options.title} `;
896
916
  const room = span - width(caption);
897
917
  const lead = paint(styler, repeatTo(chars.horizontal, 1), options.style);
898
918
  const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1), options.style);
899
- top = `${paint(styler, chars.topLeft, options.style)}${lead}${paint(styler, caption, options.style)}${rest}${paint(styler, chars.topRight, options.style)}`;
919
+ return `${paint(styler, chars.topLeft, options.style)}${lead}${paint(styler, caption, options.style)}${rest}${paint(styler, chars.topRight, options.style)}`;
900
920
  }
901
- const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`, options.style);
902
- const body = lines.map((line) => `${bar}${gutter}${align(line, inner)}${gutter}${bar}`);
903
- return [
904
- top,
905
- ...body,
906
- bottom
907
- ].join("\n");
908
921
  }
909
922
  /**
910
923
  * Renders a bordered grid of `columns` + `rows` with per-column alignment and width-aware
@@ -1128,8 +1141,7 @@ function renderBar(options) {
1128
1141
  const filledCells = Math.round(fraction * track);
1129
1142
  return `${`${paint(options.styler, repeatTo(fill, filledCells), options.style)}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
1130
1143
  }
1131
- //#endregion
1132
- //#region src/core/renderers/ANSIRenderer.ts
1144
+ // src/core/renderers/ANSIRenderer.ts
1133
1145
  /**
1134
1146
  * Implements the cross-environment default {@link RendererInterface} — renders style data as ANSI
1135
1147
  * SGR escape codes, stateless and event-free.
@@ -1161,7 +1173,7 @@ var ANSIRenderer = class {
1161
1173
  if (text === "") return text;
1162
1174
  const codes = this.#codes(style);
1163
1175
  if (codes.length === 0) return text;
1164
- return `${CSI}${codes.join(";")}m${text}${RESET}`;
1176
+ return `[${codes.join(";")}m${text}${RESET}`;
1165
1177
  }
1166
1178
  #codes(style) {
1167
1179
  const codes = [];
@@ -1171,8 +1183,7 @@ var ANSIRenderer = class {
1171
1183
  return codes;
1172
1184
  }
1173
1185
  };
1174
- //#endregion
1175
- //#region src/core/Retention.ts
1186
+ // src/core/Retention.ts
1176
1187
  /**
1177
1188
  * Implements the bounded, level-keyed retention engine the console and process captures buffer
1178
1189
  * through — one capped total buffer
@@ -1228,8 +1239,7 @@ var Retention = class {
1228
1239
  if (buffer.length > this.#limit) buffer.shift();
1229
1240
  }
1230
1241
  };
1231
- //#endregion
1232
- //#region src/core/Capture.ts
1242
+ // src/core/Capture.ts
1233
1243
  /**
1234
1244
  * Implements an observable console interceptor — it takes control of the global `console.*` on
1235
1245
  * the read side. While `active`, every configured `console.x` call is captured as a frozen
@@ -1336,8 +1346,7 @@ var Capture = class {
1336
1346
  });
1337
1347
  }
1338
1348
  };
1339
- //#endregion
1340
- //#region src/core/Styler.ts
1349
+ // src/core/Styler.ts
1341
1350
  /**
1342
1351
  * Builds the fluent, composable styling surface — the consumer-facing API over the style
1343
1352
  * engine. It
@@ -1479,8 +1488,7 @@ var Styler = class Styler {
1479
1488
  }));
1480
1489
  }
1481
1490
  };
1482
- //#endregion
1483
- //#region src/core/factories.ts
1491
+ // src/core/factories.ts
1484
1492
  /**
1485
1493
  * Creates the fluent, composable {@link StylerInterface} — ANSI by default, retargeted by a
1486
1494
  * `renderer` and stripped of color by `enabled: false`.
@@ -1627,8 +1635,7 @@ function createCaptureResult(fn, options) {
1627
1635
  throw error;
1628
1636
  }
1629
1637
  }
1630
- //#endregion
1631
- //#region src/core/loggers/Logger.ts
1638
+ // src/core/loggers/Logger.ts
1632
1639
  /**
1633
1640
  * Implements an observable, leveled logger — the entry point into the structured-logging
1634
1641
  * pipeline. Each `debug` / `info` / `warn` / `error` call builds a frozen {@link LogRecord},
@@ -1738,8 +1745,7 @@ var Logger = class {
1738
1745
  if (this.#entries.length > this.#limit) this.#entries.shift();
1739
1746
  }
1740
1747
  };
1741
- //#endregion
1742
- //#region src/core/loggers/LoggerManager.ts
1748
+ // src/core/loggers/LoggerManager.ts
1743
1749
  /**
1744
1750
  * Implements an event-free registry of named {@link Logger}s plus a convenience fan-out — the
1745
1751
  * manager over the logging layer (a registry, never observable itself; each {@link Logger}
@@ -1833,16 +1839,41 @@ var LoggerManager = class {
1833
1839
  this.#loggers.clear();
1834
1840
  return;
1835
1841
  }
1836
- if ((0, _orkestrel_contract.isArray)(names)) {
1837
- let removed = true;
1838
- for (const name of names) if (!this.#loggers.delete(name)) removed = false;
1839
- return removed;
1840
- }
1842
+ if ((0, _orkestrel_contract.isArray)(names)) return Iterator.from(names).reduce((removed, name) => this.#loggers.delete(name) && removed, true);
1841
1843
  return this.#loggers.delete(names);
1842
1844
  }
1843
1845
  };
1844
- //#endregion
1845
- //#region src/core/Reporter.ts
1846
+ // src/core/LineIterator.ts
1847
+ /**
1848
+ * Yields a position for each requested blank line without allocating the whole sequence.
1849
+ * @example
1850
+ * ```ts
1851
+ * [...new LineIterator(2)] // [0, 1]
1852
+ * ```
1853
+ */
1854
+ var LineIterator = class {
1855
+ #count;
1856
+ #index = 0;
1857
+ constructor(count) {
1858
+ this.#count = count;
1859
+ }
1860
+ next() {
1861
+ if (!(this.#index < this.#count)) return {
1862
+ done: true,
1863
+ value: void 0
1864
+ };
1865
+ const value = this.#index;
1866
+ this.#index += 1;
1867
+ return {
1868
+ done: false,
1869
+ value
1870
+ };
1871
+ }
1872
+ [Symbol.iterator]() {
1873
+ return this;
1874
+ }
1875
+ };
1876
+ // src/core/Reporter.ts
1846
1877
  /**
1847
1878
  * Implements a lean, event-free narrative reporter — the composable verb set for human /
1848
1879
  * build-run output. Each verb formats its line through the shared {@link StylerInterface} and
@@ -1920,7 +1951,7 @@ var Reporter = class {
1920
1951
  this.#sink.write(text);
1921
1952
  }
1922
1953
  blank(count = 1) {
1923
- for (let index = 0; index < count; index += 1) this.#sink.write("");
1954
+ Iterator.from(new LineIterator(count)).forEach(() => this.#sink.write(""));
1924
1955
  }
1925
1956
  #resolveStyle(options) {
1926
1957
  return {
@@ -1930,8 +1961,7 @@ var Reporter = class {
1930
1961
  };
1931
1962
  }
1932
1963
  };
1933
- //#endregion
1934
- //#region src/core/Spinner.ts
1964
+ // src/core/Spinner.ts
1935
1965
  /**
1936
1966
  * Implements a self-driving, observable activity spinner — a glyph cycle that advances on a
1937
1967
  * periodic timer, writing each `\r` + frame line to its {@link SinkInterface} and emitting it on
@@ -2049,8 +2079,7 @@ var Spinner = class {
2049
2079
  this.#sink.write(`\r${line}`);
2050
2080
  }
2051
2081
  };
2052
- //#endregion
2053
- //#region src/core/Progress.ts
2082
+ // src/core/Progress.ts
2054
2083
  /**
2055
2084
  * Implements an update-driven, observable progress bar — {@link update} recomputes the bar through
2056
2085
  * {@link renderBar}, writes `\r` + bar to its {@link SinkInterface}, and emits the `{ current,
@@ -2169,7 +2198,6 @@ var Progress = class {
2169
2198
  this.#sink.write(`\r${line}${final ? "\n" : ""}`, level);
2170
2199
  }
2171
2200
  };
2172
- //#endregion
2173
2201
  exports.ANSIRenderer = ANSIRenderer;
2174
2202
  exports.ANSI_PATTERN = ANSI_PATTERN;
2175
2203
  exports.ATTRIBUTES = ATTRIBUTES;
@@ -2202,6 +2230,7 @@ exports.FOREGROUND_CODES = FOREGROUND_CODES;
2202
2230
  exports.LEVEL_COLORS = LEVEL_COLORS;
2203
2231
  exports.LEVEL_SEVERITY = LEVEL_SEVERITY;
2204
2232
  exports.LOG_LEVELS = LOG_LEVELS;
2233
+ exports.LineIterator = LineIterator;
2205
2234
  exports.Logger = Logger;
2206
2235
  exports.LoggerManager = LoggerManager;
2207
2236
  exports.Progress = Progress;
@@ -2233,6 +2262,7 @@ exports.meetsLevel = meetsLevel;
2233
2262
  exports.paint = paint;
2234
2263
  exports.renderBar = renderBar;
2235
2264
  exports.renderBox = renderBox;
2265
+ exports.renderBoxTop = renderBoxTop;
2236
2266
  exports.renderSeparator = renderSeparator;
2237
2267
  exports.renderTable = renderTable;
2238
2268
  exports.renderTree = renderTree;