@ix-xs/node-comfort 1.0.57 → 1.1.1

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/src/Logger.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Text styles for console ANSI formatting
3
+ * @private
4
+ */
5
+ const _style = {
6
+ normal: 0,
7
+ bold: 1,
8
+ italic: 3,
9
+ underline: 4,
10
+ overline: 9,
11
+ };
12
+
13
+ /**
14
+ * Text colors for console ANSI formatting
15
+ * @private
16
+ */
17
+ const _color = {
18
+ fg: {
19
+ black: 30,
20
+ red: 31,
21
+ green: 32,
22
+ yellow: 33,
23
+ blue: 34,
24
+ magenta: 35,
25
+ cyan: 36,
26
+ white: 37,
27
+ gray: 90,
28
+ redBright: 91,
29
+ greenBright: 92,
30
+ yellowBright: 93,
31
+ blueBright: 94,
32
+ magentaBright: 95,
33
+ cyanBright: 96,
34
+ whiteBright: 97,
35
+ rgb: 38,
36
+ },
37
+ bg: {
38
+ black: 40,
39
+ red: 41,
40
+ green: 42,
41
+ yellow: 43,
42
+ blue: 44,
43
+ magenta: 45,
44
+ cyan: 46,
45
+ white: 47,
46
+ gray: 100,
47
+ redBright: 101,
48
+ greenBright: 102,
49
+ yellowBright: 103,
50
+ blueBright: 104,
51
+ magentaBright: 105,
52
+ cyanBright: 106,
53
+ whiteBright: 107,
54
+ rgb: 48,
55
+ },
56
+ };
57
+
58
+ /**
59
+ * Default logger options
60
+ * @private
61
+ */
62
+ const _options = {
63
+ delimiter: {
64
+ open: "<%",
65
+ close: "%>",
66
+ },
67
+ timestamp: true,
68
+ };
69
+
70
+ /**
71
+ * Array to manage log groups (indentation)
72
+ * @private
73
+ */
74
+ const _groups = [];
75
+
76
+ /**
77
+ * Returns the ANSI code for a given style, color, or bg
78
+ * @private
79
+ * @param {string} value - The style/color name (e.g., "bold", "red", "rgb(255, 0, 0)")
80
+ * @returns {number|string|null} The ANSI code, rgb marker, or null if unknown
81
+ */
82
+ const _getCode = (value) => {
83
+ if (_style[value] !== undefined) return _style[value];
84
+ if (_color.fg[value] !== undefined) return _color.fg[value];
85
+ if (value.startsWith("bg")) {
86
+ const bg = value.charAt(2).toLowerCase() + value.slice(3);
87
+ if (_color.bg[bg] !== undefined) return _color.bg[bg];
88
+ }
89
+
90
+ const rgbMatch = value.match(/^rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)$/);
91
+ if (rgbMatch) {
92
+ const r = parseInt(rgbMatch[1], 10);
93
+ const g = parseInt(rgbMatch[2], 10);
94
+ const b = parseInt(rgbMatch[3], 10);
95
+ if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
96
+ return `rgb:${r},${g},${b}`;
97
+ }
98
+ }
99
+
100
+ return null;
101
+ };
102
+
103
+ /**
104
+ * Parses content between delimiters to extract styles and text
105
+ * @private
106
+ * @param {string} input - Content between delimiters (e.g., "red bold hello" or "rgb(255, 0, 0) hello")
107
+ * @returns {{ codes: number[], text: string }} ANSI codes and text to display
108
+ */
109
+ const _parse = (input) => {
110
+ const rgbMatch = input.match(/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)/);
111
+ const rgbValue = rgbMatch ? rgbMatch[0] : null;
112
+ const cleanedInput = input.replace(/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)/, "").trim();
113
+ const tokens = cleanedInput.trim().split(/\s+/);
114
+ const styles = [];
115
+ const fgs = [];
116
+ const bgs = [];
117
+ const txts = [];
118
+
119
+ for (const token of tokens) {
120
+ if (!token) continue;
121
+ const code = _getCode(token);
122
+ if (code === null) {
123
+ txts.push(token);
124
+ } else {
125
+ if (_style[token] !== undefined) {
126
+ styles.push(code);
127
+ } else if (token.startsWith("bg")) {
128
+ bgs.push(code);
129
+ } else {
130
+ fgs.push(code);
131
+ }
132
+ }
133
+ }
134
+
135
+ const codes = [...styles, ...bgs, ...fgs];
136
+
137
+ if (rgbValue) {
138
+ const rgbParts = rgbValue.match(/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)/);
139
+ if (rgbParts) {
140
+ const r = parseInt(rgbParts[1], 10);
141
+ const g = parseInt(rgbParts[2], 10);
142
+ const b = parseInt(rgbParts[3], 10);
143
+ codes.push(38, 2, r, g, b);
144
+ }
145
+ }
146
+
147
+ const text = cleanedInput
148
+ .replace(
149
+ /^\s*((redBright|greenBright|yellowBright|blueBright|magentaBright|cyanBright|whiteBright|red|green|yellow|blue|magenta|cyan|white|gray|black|bold|italic|underline|overline|bg\w+|rgb\(\d+\s*,\s*\d+\s*,\s*\d+\)|\s+)*)/gi,
150
+ "",
151
+ )
152
+ .trimStart();
153
+ return { codes, text };
154
+ };
155
+
156
+ /**
157
+ * Builds the ANSI sequence from codes
158
+ * @private
159
+ * @param {number[]} codes - Array of ANSI codes
160
+ * @returns {string} The ANSI sequence (e.g., "\x1B[1;31m")
161
+ */
162
+ const _build = (codes) => {
163
+ if (codes.length === 0) return "";
164
+ return `\x1B[${codes.join(";")}m`;
165
+ };
166
+
167
+ /**
168
+ * Applies ANSI formatting to a string, replacing content between delimiters
169
+ * @private
170
+ * @param {string} str - String to format (e.g., "<%red hello%>")
171
+ * @returns {string} String with ANSI sequences
172
+ */
173
+ const _apply = (str) => {
174
+ const esc_start = _options.delimiter.open.replace(
175
+ /[.*+?^${}()|[\]\\]/g,
176
+ "\\$&",
177
+ );
178
+ const esc_end = _options.delimiter.close.replace(
179
+ /[.*+?^${}()|[\]\\]/g,
180
+ "\\$&",
181
+ );
182
+
183
+ return str.replace(
184
+ new RegExp(`${esc_start}([\\s\\S]*?)${esc_end}`, "g"),
185
+ (match, content) => {
186
+ const { codes, text } = _parse(content);
187
+ if (codes.length === 0 || text === "") return text;
188
+
189
+ if (text.includes("\n")) {
190
+ return text
191
+ .split(/\r?\n/)
192
+ .filter((line) => line.trim())
193
+ .map((line) => `${_build(codes)}${line}\x1B[0m`)
194
+ .join("\n");
195
+ }
196
+
197
+ return `${_build(codes)}${text}\x1B[0m`;
198
+ },
199
+ );
200
+ };
201
+
202
+ /**
203
+ * Generates the formatted timestamp using the current delimiter
204
+ * @private
205
+ * @returns {string} Formatted timestamp (e.g., "<%gray [08:21:54]%>") or "" if disabled
206
+ */
207
+ const _timestamp = () => {
208
+ if (!_options.timestamp) return "";
209
+ return _apply(
210
+ `${_options.delimiter.open}gray [${new Date().toLocaleTimeString(undefined, { hour12: false })}]${_options.delimiter.close}`,
211
+ );
212
+ };
213
+
214
+ /**
215
+ * Returns the current indentation based on the number of active groups
216
+ * @private
217
+ * @returns {string} Indentation spaces (2 spaces per group)
218
+ */
219
+ const _indent = () => {
220
+ return " ".repeat(_groups.length);
221
+ };
222
+
223
+ module.exports = {
224
+ /**
225
+ * Enables or disables timestamp in logs
226
+ * @param {boolean} value - True to enable, false to disable
227
+ * @returns {Logger} The same Logger instance (for chaining)
228
+ */
229
+ setTimestamp(value) {
230
+ _options.timestamp = value;
231
+ return /** @type {Logger} */ (this);
232
+ },
233
+
234
+ /**
235
+ * Changes the delimiters used for text formatting
236
+ * @param {object} options - Delimiter options
237
+ * @param {string} [options.open] - New opening delimiter
238
+ * @param {string} [options.close] - New closing delimiter
239
+ * @returns {Logger} The same Logger instance (for chaining)
240
+ */
241
+ setDelimiter(options = {}) {
242
+ _options.delimiter = {
243
+ ..._options.delimiter,
244
+ ...options,
245
+ };
246
+ return /** @type {Logger} */ (this);
247
+ },
248
+
249
+ /**
250
+ * Prints a message to the console with ANSI formatting
251
+ * @param {*} content - Content to print (string, object, or other)
252
+ * @returns {Logger} The same Logger instance (for chaining)
253
+ */
254
+ log(content) {
255
+ let str;
256
+ if (typeof content === "string") {
257
+ str = content;
258
+ } else if (typeof content === "object") {
259
+ str = JSON.stringify(content, null, 2);
260
+ } else {
261
+ str = String(content);
262
+ }
263
+
264
+ const prefix = `${_timestamp()}${_indent()}`;
265
+ const fullStr = prefix ? `${prefix} ${str}` : str;
266
+
267
+ const formatted = _apply(fullStr);
268
+
269
+ if (formatted.includes("\n")) {
270
+ const lines = formatted.split("\n");
271
+
272
+ if (lines[0].trim()) console.log(lines[0]);
273
+
274
+ const indentOnly = _indent();
275
+ for (let i = 1; i < lines.length; i++) {
276
+ if (lines[i].trim()) {
277
+ console.log(`${indentOnly}${lines[i]}`);
278
+ }
279
+ }
280
+ } else {
281
+ console.log(formatted);
282
+ }
283
+
284
+ return /** @type {Logger} */ (this);
285
+ },
286
+
287
+ /**
288
+ * Start a log group (displays a label and adds indentation)
289
+ * @param {string} label - The group name to display
290
+ * @returns {Logger} The same Logger instance (for chaining)
291
+ */
292
+ group(label) {
293
+ _groups.push(label);
294
+ const prefix = `${_timestamp()}${_indent()}`;
295
+ console.log(prefix ? `${prefix}▼ ${label}` : `▼ ${label}`);
296
+ return /** @type {Logger} */ (this);
297
+ },
298
+
299
+ /**
300
+ * End the current log group
301
+ * @returns {Logger} The same Logger instance (for chaining)
302
+ */
303
+ groupEnd() {
304
+ if (_groups.length > 0) _groups.pop();
305
+ return /** @type {Logger} */ (this);
306
+ },
307
+ };