Skip to content

Commit d95e3a5

Browse files
committed
chore: several formatter improvements
1 parent 7486963 commit d95e3a5

File tree

4 files changed

+296
-50
lines changed

4 files changed

+296
-50
lines changed

.vscode/dictionary.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
alacritty
12
antfu
23
biomejs
34
booleanish

src/formatters/pretty.ts

+88-39
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ClarityConfig, Formatter, LogEntry } from '../types'
2+
import process from 'node:process'
23
import { format } from '../format'
34

45
const COLORS = {
@@ -28,24 +29,44 @@ const COLORS = {
2829
bgWhite: '\x1B[47m',
2930
bold: '\x1B[1m',
3031
underline: '\x1B[4m',
32+
gray: '\x1B[90m',
33+
} as const
34+
35+
// Check if Unicode is supported
36+
function isUnicodeSupported(): boolean {
37+
try {
38+
return process.platform !== 'win32'
39+
|| Boolean(process.env.CI)
40+
|| Boolean(process.env.WT_SESSION) // Windows Terminal
41+
|| process.env.TERM_PROGRAM === 'vscode'
42+
|| process.env.TERM === 'xterm-256color'
43+
|| process.env.TERM === 'alacritty'
44+
}
45+
catch {
46+
return false
47+
}
3148
}
3249

50+
const unicode = isUnicodeSupported()
51+
const s = (c: string, fallback: string) => (unicode ? c : fallback)
52+
3353
const ICONS = {
34-
debug: '🔍',
35-
info: 'ℹ️',
36-
success: '✅',
37-
warning: '⚠️',
38-
error: '❌',
39-
}
54+
debug: s('🔍', 'D'),
55+
info: s('ℹ️', 'i'),
56+
success: s('✅', '√'),
57+
warning: s('⚠️', '‼'),
58+
error: s('❌', '×'),
59+
} as const
4060

4161
interface LevelStyle {
4262
color: string
4363
label: string
4464
box?: boolean
65+
bgColor?: string
4566
}
4667

4768
const LEVEL_STYLES: Record<string, LevelStyle> = {
48-
debug: { color: COLORS.dim + COLORS.white, label: 'DEBUG' },
69+
debug: { color: COLORS.gray, label: 'DEBUG' },
4970
info: { color: COLORS.brightBlue, label: 'INFO' },
5071
success: { color: COLORS.brightGreen, label: 'SUCCESS' },
5172
warning: { color: COLORS.brightYellow, label: 'WARN', box: true },
@@ -79,6 +100,11 @@ function stripAnsi(str: string): string {
79100
return result
80101
}
81102

103+
// Calculate string width accounting for ANSI codes
104+
function stringWidth(str: string): number {
105+
return stripAnsi(str).length
106+
}
107+
82108
// Helper to create a box around text
83109
function createBox(text: string, color: string, forFile: boolean = false): string {
84110
const lines = text.split('\n')
@@ -122,6 +148,48 @@ function createBox(text: string, color: string, forFile: boolean = false): strin
122148
}
123149
}
124150

151+
// Format text with character highlighting (backticks and underscores)
152+
function characterFormat(str: string): string {
153+
return str
154+
// highlight backticks
155+
.replace(/`([^`]+)`/g, (_, m) => `${COLORS.cyan}${m}${COLORS.reset}`)
156+
// underline underscores
157+
.replace(/\s+_([^_]+)_\s+/g, (_, m) => ` ${COLORS.underline}${m}${COLORS.reset} `)
158+
}
159+
160+
// Format stack traces
161+
function formatStack(stack: string, forFile: boolean = false): string {
162+
if (!stack)
163+
return ''
164+
165+
const lines = stack.split('\n')
166+
const formattedLines = lines.map((line, i) => {
167+
if (i === 0)
168+
return line // Keep the first line as is
169+
170+
if (line.trim().startsWith('at ')) {
171+
// Use a safer pattern to avoid backtracking issues
172+
const atParts = line.trim().split(/^at\s+/)
173+
if (atParts.length > 1) {
174+
const funcLocationParts = atParts[1].split(' (')
175+
if (funcLocationParts.length > 1) {
176+
const fnName = funcLocationParts[0]
177+
const location = funcLocationParts[1].replace(')', '')
178+
return forFile
179+
? ` at ${fnName} (${location})`
180+
: ` ${COLORS.dim}at ${COLORS.cyan}${fnName}${COLORS.dim} (${location})${COLORS.reset}`
181+
}
182+
}
183+
return forFile
184+
? ` ${line.trim()}`
185+
: ` ${COLORS.dim}${line.trim()}${COLORS.reset}`
186+
}
187+
return ` ${line}`
188+
})
189+
190+
return formattedLines.join('\n')
191+
}
192+
125193
export class PrettyFormatter implements Formatter {
126194
private config: ClarityConfig
127195
private terminalWidth: number
@@ -139,7 +207,7 @@ export class PrettyFormatter implements Formatter {
139207
const style = LEVEL_STYLES[level]
140208
const icon = ICONS[level]
141209

142-
// Format timestamp first for files
210+
// Format timestamp
143211
const timestampStr = timestamp.toISOString()
144212
const formattedTimestamp = forFile
145213
? timestampStr
@@ -162,32 +230,13 @@ export class PrettyFormatter implements Formatter {
162230
if (level === 'error' && formattedMessage.includes('\n')
163231
&& (formattedMessage.includes('at ') || formattedMessage.includes('stack:'))) {
164232
// Format the stack trace with proper indentation and coloring
165-
const lines = formattedMessage.split('\n')
166-
const formattedLines = lines.map((line, i) => {
167-
if (i === 0)
168-
return line // Keep the first line as is
169-
170-
if (line.trim().startsWith('at ')) {
171-
// Use a safer pattern to avoid backtracking issues
172-
const atParts = line.trim().split(/^at\s+/)
173-
if (atParts.length > 1) {
174-
const funcLocationParts = atParts[1].split(' (')
175-
if (funcLocationParts.length > 1) {
176-
const fnName = funcLocationParts[0]
177-
const location = funcLocationParts[1].replace(')', '')
178-
return forFile
179-
? ` at ${fnName} (${location})`
180-
: ` ${COLORS.dim}at ${COLORS.cyan}${fnName}${COLORS.dim} (${location})${COLORS.reset}`
181-
}
182-
}
183-
return forFile
184-
? ` ${line.trim()}`
185-
: ` ${COLORS.dim}${line.trim()}${COLORS.reset}`
186-
}
187-
return ` ${line}`
188-
})
189-
190-
formattedContent = formattedLines.join('\n')
233+
formattedContent = formatStack(formattedMessage, forFile)
234+
}
235+
else {
236+
// Apply character formatting for normal messages
237+
formattedContent = forFile
238+
? formattedContent
239+
: characterFormat(formattedContent)
191240
}
192241

193242
// For file output, construct message with timestamp at start
@@ -230,8 +279,8 @@ export class PrettyFormatter implements Formatter {
230279
const lines = logContent.split('\n')
231280

232281
// Calculate right padding for timestamp on first line
233-
const firstLineVisibleLength = stripAnsi(lines[0]).length
234-
const timestampVisibleLength = stripAnsi(formattedTimestamp).length
282+
const firstLineVisibleLength = stringWidth(lines[0])
283+
const timestampVisibleLength = stringWidth(formattedTimestamp)
235284

236285
// Calculate padding to push timestamp to the right
237286
const padding = Math.max(0, this.terminalWidth - firstLineVisibleLength - timestampVisibleLength - 1)
@@ -243,8 +292,8 @@ export class PrettyFormatter implements Formatter {
243292
}
244293
else {
245294
// Calculate right padding for timestamp
246-
const logContentVisibleLength = stripAnsi(logContent).length
247-
const timestampVisibleLength = stripAnsi(formattedTimestamp).length
295+
const logContentVisibleLength = stringWidth(logContent)
296+
const timestampVisibleLength = stringWidth(formattedTimestamp)
248297

249298
// Calculate padding to push timestamp to the right
250299
const padding = Math.max(0, this.terminalWidth - logContentVisibleLength - timestampVisibleLength - 1)
@@ -254,7 +303,7 @@ export class PrettyFormatter implements Formatter {
254303
}
255304
}
256305

257-
// New method to format for file output (without ANSI colors)
306+
// Format for file output (without ANSI colors)
258307
async formatForFile(entry: LogEntry): Promise<string> {
259308
return this.format(entry, true)
260309
}

src/formatters/text.ts

+87-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,46 @@
11
import type { ClarityConfig, Formatter, LogEntry, LogLevel } from '../types'
2+
import process from 'node:process'
23
import * as colors from '../colors'
34
import { format } from '../format'
45

6+
// Check if Unicode is supported
7+
function isUnicodeSupported(): boolean {
8+
try {
9+
return process.platform !== 'win32'
10+
|| Boolean(process.env.CI)
11+
|| Boolean(process.env.WT_SESSION) // Windows Terminal
12+
|| process.env.TERM_PROGRAM === 'vscode'
13+
|| process.env.TERM === 'xterm-256color'
14+
|| process.env.TERM === 'alacritty'
15+
}
16+
catch {
17+
return false
18+
}
19+
}
20+
21+
const unicode = isUnicodeSupported()
22+
const s = (c: string, fallback: string) => (unicode ? c : fallback)
23+
24+
// ANSI escape codes for colors not in the colors module
25+
const ANSI = {
26+
cyan: '\x1B[36m',
27+
reset: '\x1B[0m',
28+
underline: '\x1B[4m',
29+
}
30+
531
export class TextFormatter implements Formatter {
632
constructor(private config: ClarityConfig) { }
733

8-
async format(entry: LogEntry): Promise<string> {
34+
async format(entry: LogEntry, forFile: boolean = false): Promise<string> {
935
const timestamp = this.config.timestamp ? `${colors.gray(entry.timestamp.toISOString())} ` : ''
1036
const name = colors.gray(`[${entry.name}]`)
1137

1238
const levelSymbols: Record<LogLevel, string> = {
13-
debug: '🔍',
14-
info: 'ℹ️',
15-
success: '✅',
16-
warning: '⚠️',
17-
error: '❌',
39+
debug: s('🔍', 'D'),
40+
info: s('ℹ️', 'i'),
41+
success: s('✅', '√'),
42+
warning: s('⚠️', '‼'),
43+
error: s('❌', '×'),
1844
}
1945

2046
const levelColors: Record<LogLevel, (text: string) => string> = {
@@ -30,11 +56,66 @@ export class TextFormatter implements Formatter {
3056
if (Array.isArray(entry.args))
3157
message = format(entry.message, ...entry.args)
3258

59+
// Format message with character highlighting
60+
message = this.characterFormat(message)
61+
62+
// Format stack traces if present
63+
if (entry.level === 'error' && message.includes('\n')
64+
&& (message.includes('at ') || message.includes('stack:'))) {
65+
message = this.formatStack(message)
66+
}
67+
3368
const symbol = this.config.colors ? levelSymbols[entry.level] : ''
3469
message = this.config.colors
3570
? levelColors[entry.level](message)
3671
: message
3772

73+
// For file output, put timestamp at beginning
74+
if (forFile) {
75+
return `${entry.timestamp.toISOString()} ${name} ${symbol} ${message}`
76+
}
77+
3878
return `${timestamp}${name} ${symbol} ${message}`
3979
}
80+
81+
// Format text with character highlighting (backticks and underscores)
82+
private characterFormat(str: string): string {
83+
if (!this.config.colors)
84+
return str
85+
86+
return str
87+
// highlight backticks
88+
.replace(/`([^`]+)`/g, (_, m) => `${ANSI.cyan}${m}${ANSI.reset}`)
89+
// underline underscores
90+
.replace(/\s+_([^_]+)_\s+/g, (_, m) => ` ${ANSI.underline}${m}${ANSI.reset} `)
91+
}
92+
93+
// Format stack traces
94+
private formatStack(stack: string): string {
95+
if (!stack)
96+
return ''
97+
98+
const lines = stack.split('\n')
99+
const formattedLines = lines.map((line, i) => {
100+
if (i === 0)
101+
return line // Keep the first line as is
102+
103+
if (line.trim().startsWith('at ')) {
104+
// Use a safer pattern to avoid backtracking issues
105+
const atParts = line.trim().split(/^at\s+/)
106+
if (atParts.length > 1) {
107+
const funcLocationParts = atParts[1].split(' (')
108+
if (funcLocationParts.length > 1) {
109+
const fnName = funcLocationParts[0]
110+
const location = funcLocationParts[1].replace(')', '')
111+
return ` ${colors.gray(`at ${ANSI.cyan}${fnName}${ANSI.reset} (${location})`)}`
112+
}
113+
}
114+
return ` ${colors.gray(line.trim())}`
115+
}
116+
return ` ${line}`
117+
})
118+
119+
return formattedLines.join('\n')
120+
}
40121
}

0 commit comments

Comments
 (0)