@leftium/gg 0.0.27 → 0.0.28

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 CHANGED
@@ -20,7 +20,67 @@ npm add @leftium/gg
20
20
 
21
21
  ## Usage
22
22
 
23
- _Coming soon..._
23
+ ### Basic Logging
24
+
25
+ ```javascript
26
+ import { gg } from '@leftium/gg';
27
+
28
+ // Simple logging
29
+ gg('Hello world');
30
+
31
+ // Log expressions (returns first argument)
32
+ const result = gg(someFunction());
33
+
34
+ // Multiple arguments
35
+ gg('User:', user, 'Status:', status);
36
+ ```
37
+
38
+ ### Color Support (ANSI)
39
+
40
+ Color your logs for better visual distinction using `fg()` (foreground/text) and `bg()` (background):
41
+
42
+ ```javascript
43
+ import { gg, fg, bg } from '@leftium/gg';
44
+
45
+ // Simple foreground/background colors
46
+ gg(fg('red')`Error occurred`);
47
+ gg(bg('yellow')`Warning message`);
48
+
49
+ // Method chaining (order doesn't matter!)
50
+ gg(fg('white').bg('red')`Critical error!`);
51
+ gg(bg('green').fg('white')`Success message`);
52
+
53
+ // Define reusable color schemes
54
+ const input = fg('blue').bg('yellow');
55
+ const transcript = bg('green').fg('white');
56
+ const error = fg('white').bg('red');
57
+
58
+ gg(input`User input message`);
59
+ gg(transcript`AI transcript response`);
60
+ gg(error`Something went wrong`);
61
+
62
+ // Mix colored and normal text
63
+ gg(fg('red')`Error: ` + bg('yellow')`warning` + ' normal text');
64
+
65
+ // Custom hex colors with chaining
66
+ gg(fg('#ff6347').bg('#98fb98')`Custom colors`);
67
+
68
+ // RGB colors
69
+ gg(fg('rgb(255,99,71)')`Tomato text`);
70
+ ```
71
+
72
+ **Supported color formats:**
73
+
74
+ - Named colors: `'red'`, `'green'`, `'blue'`, `'cyan'`, `'magenta'`, `'yellow'`, `'white'`, `'black'`, `'gray'`, `'orange'`, `'purple'`, `'pink'`
75
+ - Hex codes: `'#ff0000'`, `'#f00'`
76
+ - RGB: `'rgb(255,0,0)'`, `'rgba(255,0,0,0.5)'`
77
+
78
+ **Where colors work:**
79
+
80
+ - ✅ Native browser console (Chrome DevTools, Firefox, etc.)
81
+ - ✅ Eruda GG panel (mobile debugging)
82
+ - ✅ Node.js terminal
83
+ - ✅ All environments that support ANSI escape codes
24
84
 
25
85
  ## Technical Details
26
86
 
@@ -570,7 +570,8 @@ export function createGgPlugin(options, gg) {
570
570
  if (typeof arg === 'object' && arg !== null) {
571
571
  return JSON.stringify(arg);
572
572
  }
573
- return String(arg);
573
+ // Strip ANSI escape codes from string args
574
+ return stripAnsi(String(arg));
574
575
  })
575
576
  .join(' ');
576
577
  return `${time} ${ns} ${argsStr}`;
@@ -715,13 +716,13 @@ export function createGgPlugin(options, gg) {
715
716
  return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
716
717
  }
717
718
  else {
718
- // Convert URLs to clickable links
719
+ // Parse ANSI codes first, then convert URLs to clickable links
719
720
  const argStr = String(arg);
720
- const urlRegex = /(https?:\/\/[^\s]+)/g;
721
- const linkedText = argStr.replace(urlRegex, (url) => {
722
- return `<a href="${escapeHtml(url)}" target="_blank" style="color: #0066cc; text-decoration: underline;">${escapeHtml(url)}</a>`;
723
- });
724
- return `<span>${linkedText}</span>`;
721
+ const parsedAnsi = parseAnsiToHtml(argStr);
722
+ // Note: URL linking happens after ANSI parsing, so links work inside colored text
723
+ // This is a simple approach - URLs inside ANSI codes won't be linkified
724
+ // For more complex parsing, we'd need to track ANSI state while matching URLs
725
+ return `<span>${parsedAnsi}</span>`;
725
726
  }
726
727
  })
727
728
  .join(' ');
@@ -773,5 +774,72 @@ export function createGgPlugin(options, gg) {
773
774
  div.textContent = text;
774
775
  return div.innerHTML;
775
776
  }
777
+ /**
778
+ * Strip ANSI escape codes from text
779
+ * Removes all ANSI escape sequences like \x1b[...m
780
+ */
781
+ function stripAnsi(text) {
782
+ // Remove all ANSI escape codes
783
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
784
+ }
785
+ /**
786
+ * Parse ANSI escape codes and convert to HTML with inline styles
787
+ * Supports:
788
+ * - 24-bit RGB: \x1b[38;2;r;g;bm (foreground), \x1b[48;2;r;g;bm (background)
789
+ * - Reset: \x1b[0m
790
+ */
791
+ function parseAnsiToHtml(text) {
792
+ // ANSI escape sequence regex
793
+ // Matches: \x1b[38;2;r;g;bm, \x1b[48;2;r;g;bm, \x1b[0m
794
+ const ansiRegex = /\x1b\[([0-9;]+)m/g;
795
+ let html = '';
796
+ let lastIndex = 0;
797
+ let currentFg = null;
798
+ let currentBg = null;
799
+ let match;
800
+ while ((match = ansiRegex.exec(text)) !== null) {
801
+ // Add text before this code (with current styling)
802
+ const textBefore = text.slice(lastIndex, match.index);
803
+ if (textBefore) {
804
+ html += wrapWithStyle(escapeHtml(textBefore), currentFg, currentBg);
805
+ }
806
+ // Parse the ANSI code
807
+ const code = match[1];
808
+ const parts = code.split(';').map(Number);
809
+ if (parts[0] === 0) {
810
+ // Reset
811
+ currentFg = null;
812
+ currentBg = null;
813
+ }
814
+ else if (parts[0] === 38 && parts[1] === 2 && parts.length >= 5) {
815
+ // Foreground RGB: 38;2;r;g;b
816
+ currentFg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
817
+ }
818
+ else if (parts[0] === 48 && parts[1] === 2 && parts.length >= 5) {
819
+ // Background RGB: 48;2;r;g;b
820
+ currentBg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
821
+ }
822
+ lastIndex = ansiRegex.lastIndex;
823
+ }
824
+ // Add remaining text
825
+ const remaining = text.slice(lastIndex);
826
+ if (remaining) {
827
+ html += wrapWithStyle(escapeHtml(remaining), currentFg, currentBg);
828
+ }
829
+ return html || escapeHtml(text);
830
+ }
831
+ /**
832
+ * Wrap text with inline color styles
833
+ */
834
+ function wrapWithStyle(text, fg, bg) {
835
+ if (!fg && !bg)
836
+ return text;
837
+ const styles = [];
838
+ if (fg)
839
+ styles.push(`color: ${fg}`);
840
+ if (bg)
841
+ styles.push(`background-color: ${bg}`);
842
+ return `<span style="${styles.join('; ')}">${text}</span>`;
843
+ }
776
844
  return plugin;
777
845
  }
@@ -0,0 +1,31 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface GgTagPluginOptions {
3
+ /**
4
+ * Pattern to strip from file paths to produce short callpoints.
5
+ * Should match up to and including the source root folder.
6
+ *
7
+ * Default: /.*?(\/(?:src|chunks)\/)/ which strips everything up to "src/" or "chunks/",
8
+ * matching the dev-mode behavior of gg().
9
+ *
10
+ * Example: "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
11
+ */
12
+ srcRootPattern?: string;
13
+ }
14
+ /**
15
+ * Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
16
+ * at build time. This gives each call site a unique namespace with zero runtime
17
+ * cost — no stack trace parsing needed.
18
+ *
19
+ * Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
20
+ * with the callpoint baked in as a string literal. Without the plugin, gg()
21
+ * falls back to runtime stack parsing in dev and bare `gg:` in prod.
22
+ *
23
+ * @example
24
+ * // vite.config.ts
25
+ * import { ggTagPlugin } from '@leftium/gg';
26
+ *
27
+ * export default defineConfig({
28
+ * plugins: [ggTagPlugin()]
29
+ * });
30
+ */
31
+ export default function ggTagPlugin(options?: GgTagPluginOptions): Plugin;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
3
+ * at build time. This gives each call site a unique namespace with zero runtime
4
+ * cost — no stack trace parsing needed.
5
+ *
6
+ * Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
7
+ * with the callpoint baked in as a string literal. Without the plugin, gg()
8
+ * falls back to runtime stack parsing in dev and bare `gg:` in prod.
9
+ *
10
+ * @example
11
+ * // vite.config.ts
12
+ * import { ggTagPlugin } from '@leftium/gg';
13
+ *
14
+ * export default defineConfig({
15
+ * plugins: [ggTagPlugin()]
16
+ * });
17
+ */
18
+ export default function ggTagPlugin(options = {}) {
19
+ const srcRootPattern = options.srcRootPattern ?? '.*?(/(?:src|chunks)/)';
20
+ const srcRootRegex = new RegExp(srcRootPattern, 'i');
21
+ return {
22
+ name: 'gg-tag',
23
+ transform(code, id) {
24
+ // Only process JS/TS/Svelte files
25
+ if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
26
+ return null;
27
+ // Quick bail: no gg calls in this file
28
+ if (!code.includes('gg('))
29
+ return null;
30
+ // Don't transform gg's own source files
31
+ if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
32
+ return null;
33
+ // Build the short callpoint from the file path
34
+ // e.g. "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
35
+ const shortPath = id.replace(srcRootRegex, '');
36
+ return transformGgCalls(code, shortPath);
37
+ }
38
+ };
39
+ }
40
+ /**
41
+ * Find the enclosing function name for a given position in source code.
42
+ * Scans backwards from the position looking for function/method declarations.
43
+ */
44
+ function findEnclosingFunction(code, position) {
45
+ // Look backwards from the gg( call for the nearest function declaration
46
+ const before = code.slice(0, position);
47
+ // Try several patterns, take the closest (last) match
48
+ // Named function: function handleClick(
49
+ // Arrow in variable: const handleClick = (...) =>
50
+ // Arrow in variable: let handleClick = (...) =>
51
+ // Method shorthand: handleClick() {
52
+ // Method: handleClick: function(
53
+ // Class method: async handleClick(
54
+ const patterns = [
55
+ // function declarations: function foo(
56
+ /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
57
+ // const/let/var assignment to arrow or function: const foo =
58
+ /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
59
+ // object method shorthand: foo() { or async foo() {
60
+ /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
61
+ // object property function: foo: function
62
+ /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:async\s+)?function/g
63
+ ];
64
+ let closestName = '';
65
+ let closestPos = -1;
66
+ for (const pattern of patterns) {
67
+ let match;
68
+ while ((match = pattern.exec(before)) !== null) {
69
+ const name = match[1];
70
+ // Skip common false positives
71
+ if ([
72
+ 'if',
73
+ 'for',
74
+ 'while',
75
+ 'switch',
76
+ 'catch',
77
+ 'return',
78
+ 'import',
79
+ 'export',
80
+ 'from',
81
+ 'new',
82
+ 'typeof',
83
+ 'instanceof',
84
+ 'void',
85
+ 'delete',
86
+ 'throw',
87
+ 'case',
88
+ 'else',
89
+ 'in',
90
+ 'of',
91
+ 'do',
92
+ 'try',
93
+ 'class',
94
+ 'super',
95
+ 'this',
96
+ 'with',
97
+ 'yield',
98
+ 'await',
99
+ 'debugger',
100
+ 'default'
101
+ ].includes(name)) {
102
+ continue;
103
+ }
104
+ if (match.index > closestPos) {
105
+ closestPos = match.index;
106
+ closestName = name;
107
+ }
108
+ }
109
+ }
110
+ return closestName;
111
+ }
112
+ /**
113
+ * Transform gg() calls in source code to gg.ns('callpoint', ...) calls.
114
+ *
115
+ * Handles:
116
+ * - bare gg(...) → gg.ns('callpoint', ...)
117
+ * - gg.ns(...) → left untouched (user-specified namespace)
118
+ * - gg.enable, gg.disable, gg.clearPersist, gg._onLog → left untouched
119
+ * - gg inside strings and comments → left untouched
120
+ */
121
+ function transformGgCalls(code, shortPath) {
122
+ // Match gg( that is:
123
+ // - not preceded by a dot (would be obj.gg() — not our function)
124
+ // - not preceded by a word char (would be dogg() or something)
125
+ // - not followed by a dot before the paren (gg.ns, gg.enable, etc.)
126
+ //
127
+ // We use a manual scan approach to correctly handle strings and comments.
128
+ const result = [];
129
+ let lastIndex = 0;
130
+ let modified = false;
131
+ // States for string/comment tracking
132
+ let i = 0;
133
+ while (i < code.length) {
134
+ // Skip single-line comments
135
+ if (code[i] === '/' && code[i + 1] === '/') {
136
+ const end = code.indexOf('\n', i);
137
+ i = end === -1 ? code.length : end + 1;
138
+ continue;
139
+ }
140
+ // Skip multi-line comments
141
+ if (code[i] === '/' && code[i + 1] === '*') {
142
+ const end = code.indexOf('*/', i + 2);
143
+ i = end === -1 ? code.length : end + 2;
144
+ continue;
145
+ }
146
+ // Skip template literals (backticks)
147
+ if (code[i] === '`') {
148
+ i++;
149
+ let depth = 0;
150
+ while (i < code.length) {
151
+ if (code[i] === '\\') {
152
+ i += 2;
153
+ continue;
154
+ }
155
+ if (code[i] === '$' && code[i + 1] === '{') {
156
+ depth++;
157
+ i += 2;
158
+ continue;
159
+ }
160
+ if (code[i] === '}' && depth > 0) {
161
+ depth--;
162
+ i++;
163
+ continue;
164
+ }
165
+ if (code[i] === '`' && depth === 0) {
166
+ i++;
167
+ break;
168
+ }
169
+ i++;
170
+ }
171
+ continue;
172
+ }
173
+ // Skip strings (single and double quotes)
174
+ if (code[i] === '"' || code[i] === "'") {
175
+ const quote = code[i];
176
+ i++;
177
+ while (i < code.length) {
178
+ if (code[i] === '\\') {
179
+ i += 2;
180
+ continue;
181
+ }
182
+ if (code[i] === quote) {
183
+ i++;
184
+ break;
185
+ }
186
+ i++;
187
+ }
188
+ continue;
189
+ }
190
+ // Look for 'gg(' pattern
191
+ if (code[i] === 'g' && code[i + 1] === 'g' && code[i + 2] === '(') {
192
+ // Check preceding character: must not be a word char or dot
193
+ const prevChar = i > 0 ? code[i - 1] : '';
194
+ if (prevChar && /[a-zA-Z0-9_$.]/.test(prevChar)) {
195
+ i++;
196
+ continue;
197
+ }
198
+ // Check it's not gg.something (gg.ns, gg.enable, etc.)
199
+ // At this point we know code[i..i+2] is "gg(" — it's a bare call
200
+ // Find the enclosing function
201
+ const fnName = findEnclosingFunction(code, i);
202
+ const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
203
+ const escaped = callpoint.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
204
+ // Emit everything before this match
205
+ result.push(code.slice(lastIndex, i));
206
+ // Replace gg( with gg.ns('callpoint',
207
+ // Need to handle gg() with no args → gg.ns('callpoint')
208
+ // and gg(x) → gg.ns('callpoint', x)
209
+ // Peek ahead to check if it's gg() with no args
210
+ const afterParen = code.indexOf(')', i + 3);
211
+ const betweenParens = code.slice(i + 3, afterParen);
212
+ const isNoArgs = betweenParens.trim() === '';
213
+ if (isNoArgs && afterParen !== -1 && !betweenParens.includes('(')) {
214
+ // gg() → gg.ns('callpoint')
215
+ result.push(`gg.ns('${escaped}')`);
216
+ lastIndex = afterParen + 1;
217
+ i = afterParen + 1;
218
+ }
219
+ else {
220
+ // gg(args...) → gg.ns('callpoint', args...)
221
+ result.push(`gg.ns('${escaped}', `);
222
+ lastIndex = i + 3; // skip past "gg("
223
+ i = i + 3;
224
+ }
225
+ modified = true;
226
+ continue;
227
+ }
228
+ i++;
229
+ }
230
+ if (!modified)
231
+ return null;
232
+ result.push(code.slice(lastIndex));
233
+ return { code: result.join(''), map: null };
234
+ }
package/dist/gg.d.ts CHANGED
@@ -23,8 +23,68 @@ export declare namespace gg {
23
23
  var enable: (namespaces: string) => void;
24
24
  var clearPersist: () => void;
25
25
  }
26
+ /**
27
+ * ANSI Color Helpers for gg()
28
+ *
29
+ * Create reusable color schemes with foreground (fg) and background (bg) colors.
30
+ * Works in both native console and Eruda plugin.
31
+ *
32
+ * @example
33
+ * // Method chaining (order doesn't matter)
34
+ * gg(fg('white').bg('red')`Critical error!`);
35
+ * gg(bg('green').fg('white')`Success!`);
36
+ *
37
+ * @example
38
+ * // Define color schemes once, reuse everywhere
39
+ * const input = fg('blue').bg('yellow');
40
+ * const transcript = bg('green').fg('white');
41
+ * const error = fg('white').bg('red');
42
+ *
43
+ * gg(input`User said: hello`);
44
+ * gg(transcript`AI responded: hi`);
45
+ * gg(error`Something broke!`);
46
+ *
47
+ * @example
48
+ * // Mix colored and normal text inline
49
+ * gg(fg('red')`Error: ` + bg('yellow')`warning` + ' normal text');
50
+ *
51
+ * @example
52
+ * // Custom colors (hex, rgb, or named)
53
+ * gg(fg('#ff6347').bg('#98fb98')`Custom colors`);
54
+ *
55
+ * @example
56
+ * // Just foreground or background
57
+ * gg(fg('cyan')`Cyan text`);
58
+ * gg(bg('magenta')`Magenta background`);
59
+ */
60
+ type ColorTagFunction = (strings: TemplateStringsArray, ...values: unknown[]) => string;
61
+ interface ChainableColorFn extends ColorTagFunction {
62
+ fg: (color: string) => ChainableColorFn;
63
+ bg: (color: string) => ChainableColorFn;
64
+ }
65
+ /**
66
+ * Foreground (text) color helper
67
+ * Can be used directly or chained with .bg()
68
+ *
69
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
70
+ * @example
71
+ * gg(fg('red')`Error`);
72
+ * gg(fg('white').bg('red')`Critical!`);
73
+ */
74
+ export declare function fg(color: string): ChainableColorFn;
75
+ /**
76
+ * Background color helper
77
+ * Can be used directly or chained with .fg()
78
+ *
79
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
80
+ * @example
81
+ * gg(bg('yellow')`Warning`);
82
+ * gg(bg('green').fg('white')`Success!`);
83
+ */
84
+ export declare function bg(color: string): ChainableColorFn;
26
85
  export declare namespace gg {
27
86
  let _onLog: OnLogCallback | null;
87
+ let ns: (nsLabel: string, ...args: unknown[]) => unknown;
28
88
  }
29
89
  /**
30
90
  * Run gg diagnostics and log configuration status
package/dist/gg.js CHANGED
@@ -255,6 +255,71 @@ export function gg(...args) {
255
255
  }
256
256
  return returnValue;
257
257
  }
258
+ /**
259
+ * gg.ns() - Log with an explicit namespace (callpoint label).
260
+ *
261
+ * In production builds, the ggTagPlugin Vite plugin rewrites bare gg() calls
262
+ * to gg.ns('callpoint', ...) so each call site gets a unique namespace even
263
+ * after minification. Users can also call gg.ns() directly to set a meaningful
264
+ * label that survives across builds.
265
+ *
266
+ * @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
267
+ * @param args - Same arguments as gg()
268
+ * @returns Same as gg() - the first arg, or call-site info if no args
269
+ *
270
+ * @example
271
+ * gg.ns("auth", "login failed") // logs under namespace "gg:auth"
272
+ * gg.ns("cart", item, quantity) // logs under namespace "gg:cart"
273
+ */
274
+ gg.ns = function (nsLabel, ...args) {
275
+ if (!ggConfig.enabled || isCloudflareWorker()) {
276
+ return args.length ? args[0] : { url: '', stack: [] };
277
+ }
278
+ const namespace = `gg:${nsLabel}`;
279
+ if (nsLabel.length < 80 && nsLabel.length > maxCallpointLength) {
280
+ maxCallpointLength = nsLabel.length;
281
+ }
282
+ const ggLogFunction = namespaceToLogFunction.get(namespace) ||
283
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
284
+ // Prepare args for logging
285
+ let logArgs;
286
+ let returnValue;
287
+ if (!args.length) {
288
+ logArgs = [` 📝 ${nsLabel}`];
289
+ returnValue = { fileName: '', functionName: '', url: '', stack: [] };
290
+ }
291
+ else if (args.length === 1) {
292
+ logArgs = [args[0]];
293
+ returnValue = args[0];
294
+ }
295
+ else {
296
+ logArgs = [args[0], ...args.slice(1)];
297
+ returnValue = args[0];
298
+ }
299
+ // Log to console via debug
300
+ if (logArgs.length === 1) {
301
+ ggLogFunction(logArgs[0]);
302
+ }
303
+ else {
304
+ ggLogFunction(logArgs[0], ...logArgs.slice(1));
305
+ }
306
+ // Call capture hook if registered (for Eruda plugin)
307
+ const entry = {
308
+ namespace,
309
+ color: ggLogFunction.color,
310
+ diff: ggLogFunction.diff || 0,
311
+ message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
312
+ args: logArgs,
313
+ timestamp: Date.now()
314
+ };
315
+ if (_onLogCallback) {
316
+ _onLogCallback(entry);
317
+ }
318
+ else {
319
+ earlyLogBuffer.push(entry);
320
+ }
321
+ return returnValue;
322
+ };
258
323
  gg.disable = isCloudflareWorker() ? () => { } : debugFactory.disable;
259
324
  gg.enable = isCloudflareWorker() ? () => { } : debugFactory.enable;
260
325
  /**
@@ -272,6 +337,120 @@ gg.clearPersist = () => {
272
337
  }
273
338
  }
274
339
  };
340
+ /**
341
+ * Parse color string to RGB values
342
+ * Accepts: named colors, hex (#rgb, #rrggbb), rgb(r,g,b), rgba(r,g,b,a)
343
+ */
344
+ function parseColor(color) {
345
+ // Named colors map (basic ANSI colors + common web colors)
346
+ const namedColors = {
347
+ black: '#000000',
348
+ red: '#ff0000',
349
+ green: '#00ff00',
350
+ yellow: '#ffff00',
351
+ blue: '#0000ff',
352
+ magenta: '#ff00ff',
353
+ cyan: '#00ffff',
354
+ white: '#ffffff',
355
+ // Bright variants
356
+ brightBlack: '#808080',
357
+ brightRed: '#ff6666',
358
+ brightGreen: '#66ff66',
359
+ brightYellow: '#ffff66',
360
+ brightBlue: '#6666ff',
361
+ brightMagenta: '#ff66ff',
362
+ brightCyan: '#66ffff',
363
+ brightWhite: '#ffffff',
364
+ // Common aliases
365
+ gray: '#808080',
366
+ grey: '#808080',
367
+ orange: '#ffa500',
368
+ purple: '#800080',
369
+ pink: '#ffc0cb'
370
+ };
371
+ // Check named colors first
372
+ const normalized = color.toLowerCase().trim();
373
+ if (namedColors[normalized]) {
374
+ color = namedColors[normalized];
375
+ }
376
+ // Parse hex color
377
+ const hexMatch = color.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
378
+ if (hexMatch) {
379
+ return {
380
+ r: parseInt(hexMatch[1], 16),
381
+ g: parseInt(hexMatch[2], 16),
382
+ b: parseInt(hexMatch[3], 16)
383
+ };
384
+ }
385
+ // Parse short hex (#rgb)
386
+ const shortHexMatch = color.match(/^#?([a-f\d])([a-f\d])([a-f\d])$/i);
387
+ if (shortHexMatch) {
388
+ return {
389
+ r: parseInt(shortHexMatch[1] + shortHexMatch[1], 16),
390
+ g: parseInt(shortHexMatch[2] + shortHexMatch[2], 16),
391
+ b: parseInt(shortHexMatch[3] + shortHexMatch[3], 16)
392
+ };
393
+ }
394
+ // Parse rgb(r,g,b) or rgba(r,g,b,a)
395
+ const rgbMatch = color.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
396
+ if (rgbMatch) {
397
+ return {
398
+ r: parseInt(rgbMatch[1]),
399
+ g: parseInt(rgbMatch[2]),
400
+ b: parseInt(rgbMatch[3])
401
+ };
402
+ }
403
+ return null;
404
+ }
405
+ /**
406
+ * Internal helper to create chainable color function with method chaining
407
+ */
408
+ function createColorFunction(fgCode = '', bgCode = '') {
409
+ const tagFn = function (strings, ...values) {
410
+ const text = strings.reduce((acc, str, i) => acc + str + (values[i] !== undefined ? String(values[i]) : ''), '');
411
+ return fgCode + bgCode + text + '\x1b[0m';
412
+ };
413
+ // Add method chaining
414
+ tagFn.fg = (color) => {
415
+ const rgb = parseColor(color);
416
+ const newFgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
417
+ return createColorFunction(newFgCode, bgCode);
418
+ };
419
+ tagFn.bg = (color) => {
420
+ const rgb = parseColor(color);
421
+ const newBgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
422
+ return createColorFunction(fgCode, newBgCode);
423
+ };
424
+ return tagFn;
425
+ }
426
+ /**
427
+ * Foreground (text) color helper
428
+ * Can be used directly or chained with .bg()
429
+ *
430
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
431
+ * @example
432
+ * gg(fg('red')`Error`);
433
+ * gg(fg('white').bg('red')`Critical!`);
434
+ */
435
+ export function fg(color) {
436
+ const rgb = parseColor(color);
437
+ const fgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
438
+ return createColorFunction(fgCode, '');
439
+ }
440
+ /**
441
+ * Background color helper
442
+ * Can be used directly or chained with .fg()
443
+ *
444
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
445
+ * @example
446
+ * gg(bg('yellow')`Warning`);
447
+ * gg(bg('green').fg('white')`Success!`);
448
+ */
449
+ export function bg(color) {
450
+ const rgb = parseColor(color);
451
+ const bgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
452
+ return createColorFunction('', bgCode);
453
+ }
275
454
  /**
276
455
  * Hook for capturing gg() output (used by Eruda plugin)
277
456
  * Set this to a callback function to receive log entries
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- import { gg } from './gg.js';
1
+ import { gg, fg, bg } from './gg.js';
2
2
  import openInEditorPlugin from './open-in-editor.js';
3
- export { gg, openInEditorPlugin };
3
+ import ggTagPlugin from './gg-tag-plugin.js';
4
+ export { gg, fg, bg, openInEditorPlugin, ggTagPlugin };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Reexport your entry components here
2
- import { gg } from './gg.js';
2
+ import { gg, fg, bg } from './gg.js';
3
3
  import openInEditorPlugin from './open-in-editor.js';
4
- export { gg, openInEditorPlugin };
4
+ import ggTagPlugin from './gg-tag-plugin.js';
5
+ export { gg, fg, bg, openInEditorPlugin, ggTagPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"