@steve02081504/virtual-console 0.1.3 → 0.1.5

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.
Files changed (5) hide show
  1. package/browser.mjs +18 -10
  2. package/main.mjs +7 -7
  3. package/node.mjs +154 -84
  4. package/package.json +1 -1
  5. package/util.mjs +155 -27
package/browser.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FullProxy } from 'full-proxy'
2
2
 
3
- import { argsToHtml } from './util.mjs'
3
+ import { argsToHtml, safeString, circularToString, escapeHtml } from './util.mjs'
4
4
 
5
5
  /**
6
6
  * 存储原始的浏览器 console 对象。
@@ -23,14 +23,14 @@ function formatArgs(args) {
23
23
  return JSON.stringify(arg, null, '\t')
24
24
  }
25
25
  catch {
26
- return String(arg)
26
+ return safeString(arg)
27
27
  }
28
28
  }).join(' ')
29
29
 
30
30
  let output = ''
31
31
  let argIndex = 1
32
32
  let lastIndex = 0
33
- const regex = /%[sdifoOc%]/g
33
+ const regex = /%[%Ocdfijos]/g
34
34
  let match
35
35
 
36
36
  while ((match = regex.exec(format)) !== null) {
@@ -52,7 +52,7 @@ function formatArgs(args) {
52
52
  case '%c':
53
53
  break
54
54
  case '%s':
55
- output += String(arg)
55
+ output += safeString(arg)
56
56
  break
57
57
  case '%d':
58
58
  case '%i':
@@ -63,8 +63,10 @@ function formatArgs(args) {
63
63
  break
64
64
  case '%o':
65
65
  case '%O':
66
+ return circularToString(arg)
67
+ case '%j':
66
68
  try { output += JSON.stringify(arg, null, '\t') }
67
- catch { output += String(arg) }
69
+ catch { output += safeString(arg) }
68
70
  break
69
71
  }
70
72
  }
@@ -76,9 +78,9 @@ function formatArgs(args) {
76
78
  if (arg instanceof Error && arg.stack) output += arg.stack
77
79
  else if ((arg === null || arg instanceof Object) && !(arg instanceof Function))
78
80
  try { output += JSON.stringify(arg, null, '\t') }
79
- catch { output += String(arg) }
81
+ catch { output += safeString(arg) }
80
82
 
81
- else output += String(arg)
83
+ else output += safeString(arg)
82
84
  }
83
85
 
84
86
  return output
@@ -110,7 +112,8 @@ export class VirtualConsole {
110
112
  * @param {Console} [options.base_console=window.console] - 用于 realConsoleOutput 的底层控制台实例。
111
113
  */
112
114
  constructor(options = {}) {
113
- this.#base_console = options.base_console || originalConsole
115
+ options = { ...options }
116
+ this.#base_console = options.base_console || consoleReflect()
114
117
  delete options.base_console
115
118
 
116
119
  this.options = {
@@ -120,7 +123,7 @@ export class VirtualConsole {
120
123
  ...options,
121
124
  }
122
125
 
123
- const methods = ['log', 'info', 'warn', 'debug', 'error', 'table', 'dir', 'assert', 'count', 'countReset', 'time', 'timeLog', 'timeEnd', 'group', 'groupCollapsed', 'groupEnd']
126
+ const methods = ['log', 'info', 'warn', 'debug', 'error', 'table', 'dir', 'assert', 'count', 'countReset', 'time', 'timeLog', 'timeEnd', 'group', 'groupCollapsed', 'groupEnd', 'trace']
124
127
  for (const method of methods)
125
128
  if (this.#base_console[method] instanceof Function)
126
129
  /**
@@ -137,6 +140,12 @@ export class VirtualConsole {
137
140
  this.outputsHtml += argsToHtml(args) + '<br/>\n'
138
141
  }
139
142
 
143
+ if (method == 'trace') {
144
+ const stack = new Error().stack
145
+ this.outputs += stack.slice(stack.indexOf('\n') + 1) + '\n'
146
+ this.outputsHtml += escapeHtml(stack.slice(stack.indexOf('\n') + 1)) + '<br/>\n'
147
+ }
148
+
140
149
  // 实际输出
141
150
  if (this.options.realConsoleOutput)
142
151
  this.#base_console[method](...args)
@@ -199,7 +208,6 @@ export class VirtualConsole {
199
208
  this.outputsHtml = ''
200
209
  if (this.options.realConsoleOutput)
201
210
  this.#base_console.clear()
202
-
203
211
  }
204
212
  }
205
213
 
package/main.mjs CHANGED
@@ -3,28 +3,28 @@ const module = await import(globalThis.document ? './browser.mjs' : './node.mjs'
3
3
  /**
4
4
  * @type {AsyncLocalStorage}
5
5
  */
6
- export const consoleAsyncStorage = module.consoleAsyncStorage
6
+ export const { consoleAsyncStorage } = module
7
7
  /**
8
8
  * @type {typeof VirtualConsole}
9
9
  */
10
- export const VirtualConsole = module.VirtualConsole
10
+ export const { VirtualConsole } = module
11
11
  /**
12
12
  * @type {VirtualConsole}
13
13
  */
14
- export const defaultConsole = module.defaultConsole
14
+ export const { defaultConsole } = module
15
15
  /**
16
16
  * @type {object}
17
17
  */
18
- export const globalConsoleAdditionalProperties = module.globalConsoleAdditionalProperties
18
+ export const { globalConsoleAdditionalProperties } = module
19
19
  /**
20
20
  * @type {function}
21
21
  */
22
- export const setGlobalConsoleReflect = module.setGlobalConsoleReflect
22
+ export const { setGlobalConsoleReflect } = module
23
23
  /**
24
24
  * @type {function}
25
25
  */
26
- export const getGlobalConsoleReflect = module.getGlobalConsoleReflect
26
+ export const { getGlobalConsoleReflect } = module
27
27
  /**
28
28
  * @type {VirtualConsole}
29
29
  */
30
- export const console = module.console
30
+ export const { console } = module
package/node.mjs CHANGED
@@ -13,11 +13,147 @@ import { argsToHtml } from './util.mjs'
13
13
  * 全局异步存储,用于管理控制台上下文。
14
14
  */
15
15
  export const consoleAsyncStorage = new AsyncLocalStorage()
16
- const cleanupRegistry = new FinalizationRegistry(cleanupToken => {
17
- const { stream, listener } = cleanupToken
18
- stream.off?.('resize', listener)
16
+
17
+ /**
18
+ * WeakMap 用于存储每个流对应的 resize 监听器信息。
19
+ * @type {WeakMap<Writable, { listener: () => void, virtualStreams: Set<WeakRef<Writable>> }>}
20
+ */
21
+ const streamResizeListeners = new WeakMap()
22
+
23
+ /**
24
+ * FinalizationRegistry 用于清理虚拟流引用。
25
+ */
26
+ const virtualStreamCleanupRegistry = new FinalizationRegistry(({ stream, virtualStreamRef }) => {
27
+ const listenerInfo = streamResizeListeners.get(stream)
28
+ if (!listenerInfo) return
29
+ listenerInfo.virtualStreams.delete(virtualStreamRef)
30
+ if (listenerInfo.virtualStreams.size) return
31
+ stream.off?.('resize', listenerInfo.listener)
32
+ streamResizeListeners.delete(stream)
19
33
  })
20
34
 
35
+ /**
36
+ * 获取或创建一个流对应的监听器信息。
37
+ * @param {Writable} stream - 目标流。
38
+ * @returns {{ listener: () => void, virtualStreams: Set<WeakRef<Writable>> }} 监听器信息。
39
+ */
40
+ function getListenerInfo(stream) {
41
+ const existing = streamResizeListeners.get(stream)
42
+ if (existing) return existing
43
+ const listenerInfo = {
44
+ /**
45
+ * 统一的 resize 监听器,会通知所有使用该流的虚拟流。
46
+ * @returns {void}
47
+ */
48
+ listener: () => {
49
+ for (const ref of listenerInfo.virtualStreams) {
50
+ const virtualStream = ref.deref()
51
+ if (virtualStream) try { virtualStream.emit?.('resize') } catch (error) { console.error(error) }
52
+ else listenerInfo.virtualStreams.delete(ref)
53
+ }
54
+ if (listenerInfo.virtualStreams.size) return
55
+ stream.off?.('resize', listenerInfo.listener)
56
+ streamResizeListeners.delete(stream)
57
+ },
58
+ virtualStreams: new Set()
59
+ }
60
+ stream.on?.('resize', listenerInfo.listener)
61
+
62
+ streamResizeListeners.set(stream, listenerInfo)
63
+ return listenerInfo
64
+ }
65
+
66
+ /**
67
+ * 虚拟流类,用于创建虚拟控制台流。
68
+ * @augments {Writable}
69
+ */
70
+ class VirtualStream extends Writable {
71
+ /**
72
+ * @param {NodeJS.WritableStream} targetStream - 目标流。
73
+ * @param {object} context - 虚拟控制台上下文。
74
+ * @param {() => void} context.onWrite - 写入时的回调函数,用于重置 loggedFreshLineId。
75
+ * @param {object} context.options - 虚拟控制台的配置选项。
76
+ * @param {boolean} context.options.recordOutput - 是否记录输出。
77
+ * @param {boolean} context.options.realConsoleOutput - 是否输出到真实控制台。
78
+ * @param {{ outputs: string }} context.state - 虚拟控制台的状态对象,包含 outputs 属性。
79
+ */
80
+ constructor(targetStream, context) {
81
+ super({
82
+ /**
83
+ * 写入数据到虚拟流。
84
+ * @param {Buffer | string} chunk - 要写入的数据块。
85
+ * @param {string} encoding - 编码格式。
86
+ * @param {() => void} callback - 写入完成的回调函数。
87
+ */
88
+ write: (chunk, encoding, callback) => {
89
+ context.onWrite()
90
+
91
+ if (context.options.recordOutput)
92
+ context.state.outputs += chunk.toString()
93
+ if (context.options.realConsoleOutput)
94
+ targetStream.write(chunk, encoding, callback)
95
+ else
96
+ callback()
97
+ },
98
+ })
99
+
100
+ this.#targetStream = targetStream
101
+
102
+ if (targetStream.isTTY) {
103
+ const virtualStreamRef = new WeakRef(this)
104
+ const listenerInfo = getListenerInfo(targetStream)
105
+ listenerInfo.virtualStreams.add(virtualStreamRef)
106
+ virtualStreamCleanupRegistry.register(this, {
107
+ stream: targetStream,
108
+ virtualStreamRef
109
+ })
110
+ }
111
+ }
112
+
113
+ /** @private @type {NodeJS.WritableStream} - 目标流 */
114
+ #targetStream
115
+
116
+ /**
117
+ * 判断目标流是否为 TTY
118
+ * @returns {boolean} 是否为 TTY
119
+ */
120
+ get isTTY() {
121
+ return this.#targetStream?.isTTY ?? false
122
+ }
123
+
124
+ /**
125
+ * 获取目标流的列数
126
+ * @returns {number} 列数
127
+ */
128
+ get columns() {
129
+ return this.#targetStream.columns
130
+ }
131
+
132
+ /**
133
+ * 获取目标流的行数
134
+ * @returns {number} 行数
135
+ */
136
+ get rows() {
137
+ return this.#targetStream.rows
138
+ }
139
+
140
+ /**
141
+ * 获取目标流的颜色深度
142
+ * @returns {number} 颜色深度
143
+ */
144
+ getColorDepth() {
145
+ return this.#targetStream.getColorDepth()
146
+ }
147
+
148
+ /**
149
+ * 判断目标流是否支持颜色
150
+ * @returns {boolean} 是否支持颜色
151
+ */
152
+ hasColors() {
153
+ return this.#targetStream.hasColors()
154
+ }
155
+ }
156
+
21
157
  /**
22
158
  * 创建一个虚拟控制台,用于捕获输出,同时可以选择性地将输出传递给真实的控制台。
23
159
  * @augments {Console}
@@ -72,15 +208,16 @@ export class VirtualConsole extends Console {
72
208
  constructor(options = {}) {
73
209
  super(new Writable({ /** 啥也不干 */ write: () => { } }), new Writable({ /** 啥也不干 */ write: () => { } }))
74
210
 
75
- this.base_console = options.base_console || consoleReflect()
211
+ const base_console = options.base_console || consoleReflect()
76
212
  delete options.base_console
77
213
  this.options = {
78
214
  realConsoleOutput: false,
79
215
  recordOutput: true,
80
- supportsAnsi: this.#base_console.options?.supportsAnsi || supportsAnsi,
216
+ supportsAnsi: base_console.options?.supportsAnsi || supportsAnsi,
81
217
  error_handler: null,
82
218
  ...options,
83
219
  }
220
+ this.base_console = base_console
84
221
  this.freshLine = this.freshLine.bind(this)
85
222
  this.clear = this.clear.bind(this)
86
223
  for (const method of ['log', 'info', 'warn', 'debug', 'error']) {
@@ -117,87 +254,20 @@ export class VirtualConsole extends Console {
117
254
  set base_console(value) {
118
255
  this.#base_console = value
119
256
 
120
- /**
121
- * 创建一个虚拟的控制台流,用于捕获输出。
122
- * @param {NodeJS.WritableStream} targetStream - 目标流。
123
- * @returns {NodeJS.WritableStream} 虚拟流。
124
- */
125
- const createVirtualStream = (targetStream) => {
126
- const virtualStream = new Writable({
127
- /**
128
- * 写入数据到虚拟流。
129
- * @param {Buffer | string} chunk - 要写入的数据块。
130
- * @param {string} encoding - 编码格式。
131
- * @param {() => void} callback - 写入完成的回调函数。
132
- */
133
- write: (chunk, encoding, callback) => {
134
- this.#loggedFreshLineId = null
135
-
136
- if (this.options.recordOutput)
137
- this.outputs += chunk.toString()
138
- if (this.options.realConsoleOutput)
139
- targetStream.write(chunk, encoding, callback)
140
- else
141
- callback()
142
- },
143
- })
144
-
145
- if (targetStream.isTTY) {
146
- Object.defineProperties(virtualStream, {
147
- isTTY: { value: true, configurable: true, writable: false, enumerable: true },
148
- columns: {
149
- /**
150
- * 获取目标流的列数
151
- * @returns {number} 列数
152
- */
153
- get: () => targetStream.columns, configurable: true, enumerable: true
154
- },
155
- rows: {
156
- /**
157
- * 获取目标流的行数
158
- * @returns {number} 行数
159
- */
160
- get: () => targetStream.rows, configurable: true, enumerable: true
161
- },
162
- getColorDepth: {
163
- /**
164
- * 获取目标流的颜色深度
165
- * @returns {number} 颜色深度
166
- */
167
- get: () => targetStream.getColorDepth.bind(targetStream), configurable: true, enumerable: true
168
- },
169
- hasColors: {
170
- /**
171
- * 判断目标流是否支持颜色
172
- * @returns {boolean} 是否支持颜色
173
- */
174
- get: () => targetStream.hasColors.bind(targetStream), configurable: true, enumerable: true
175
- },
176
- })
177
-
178
- const virtualStreamRef = new WeakRef(virtualStream)
179
-
180
- /**
181
- * 监听目标流的 resize 事件,并在虚拟流上触发相应的事件。
182
- * @returns {void}
183
- */
184
- const resizeListener = () => {
185
- virtualStreamRef.deref()?.emit('resize')
186
- }
187
-
188
- targetStream.on?.('resize', resizeListener)
189
-
190
- cleanupRegistry.register(this, {
191
- stream: targetStream,
192
- listener: resizeListener,
193
- }, this)
194
- }
195
-
196
- return virtualStream
257
+ const context = {
258
+ /**
259
+ * 写入完成时的回调函数,用于重置 loggedFreshLineId。
260
+ * @returns {void}
261
+ */
262
+ onWrite: () => {
263
+ this.#loggedFreshLineId = null
264
+ },
265
+ options: this.options,
266
+ state: this
197
267
  }
198
268
 
199
- this._stdout = createVirtualStream(this.#base_console?._stdout || process.stdout)
200
- this._stderr = createVirtualStream(this.#base_console?._stderr || process.stderr)
269
+ this._stdout = new VirtualStream(this.#base_console?._stdout || process.stdout, context)
270
+ this._stderr = new VirtualStream(this.#base_console?._stderr || process.stderr, context)
201
271
  }
202
272
 
203
273
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/virtual-console",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A virtual console for capturing and manipulating terminal output.",
5
5
  "main": "main.mjs",
6
6
  "type": "module",
package/util.mjs CHANGED
@@ -2,6 +2,150 @@ import { AnsiUp } from 'ansi_up'
2
2
 
3
3
  const ansi_up = new AnsiUp()
4
4
 
5
+ /**
6
+ * 转义 HTML 字符
7
+ * @param {string} str - 要转义的字符串。
8
+ * @returns {string} 转义后的字符串。
9
+ */
10
+ export function escapeHtml(str) {
11
+ return str.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
12
+ }
13
+
14
+ /**
15
+ * 安全地将值转换为字符串,处理无原型对象(如 Object.create(null))。
16
+ * @param {any} arg - 要转换的值。
17
+ * @returns {string} 转换后的字符串。
18
+ */
19
+ export function safeString(arg) {
20
+ try {
21
+ return String(arg)
22
+ } catch {
23
+ try {
24
+ return JSON.stringify(arg)
25
+ }
26
+ catch {
27
+ return Object.prototype.toString.call(arg)
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * 将对象转换为字符串,处理循环引用。
34
+ * @param {any} target - 要转换的目标对象。
35
+ * @param {object} [options] - 转换选项。
36
+ * @returns {string} 转换后的字符串。
37
+ */
38
+ export function circularToString(target, options = {}) {
39
+ const { depth = Infinity } = options
40
+
41
+ const colors = {
42
+ reset: '\x1b[0m',
43
+ green: '\x1b[32m',
44
+ yellow: '\x1b[33m',
45
+ cyan: '\x1b[36m',
46
+ grey: '\x1b[90m',
47
+ magenta: '\x1b[35m'
48
+ }
49
+
50
+ const circularIds = new Map()
51
+ const walkStack = new Set()
52
+ let idCounter = 1
53
+
54
+ /**
55
+ * 扫描对象以检测循环引用。
56
+ * @param {any} value - 要扫描的值。
57
+ */
58
+ function scan(value) {
59
+ if (typeof value !== 'object' || value === null) return
60
+
61
+ if (walkStack.has(value)) {
62
+ if (!circularIds.has(value)) circularIds.set(value, idCounter++)
63
+ return
64
+ }
65
+
66
+ walkStack.add(value)
67
+ const keys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)]
68
+ for (const key of keys) scan(value[key])
69
+ walkStack.delete(value)
70
+ }
71
+
72
+ scan(target)
73
+ const seen = new Set()
74
+
75
+ /**
76
+ * 格式化值为字符串。
77
+ * @param {any} value - 要格式化的值。
78
+ * @param {number} currentDepth - 当前递归深度。
79
+ * @returns {string} 格式化后的字符串。
80
+ */
81
+ function format(value, currentDepth) {
82
+ // 1. 处理原始类型
83
+ if (typeof value === 'string') return `${colors.green}'${value}'${colors.reset}`
84
+ if (typeof value === 'number') return `${colors.yellow}${value}${colors.reset}`
85
+ if (typeof value === 'boolean') return `${colors.yellow}${value}${colors.reset}`
86
+ if (value === undefined) return `${colors.grey}undefined${colors.reset}`
87
+ if (value === null) return `${colors.reset}null${colors.reset}`
88
+ if (typeof value === 'symbol') return `${colors.green}${value.toString()}${colors.reset}`
89
+ if (typeof value === 'function') return `${colors.cyan}[Function: ${value.name || '(anonymous)'}]${colors.reset}`
90
+
91
+ // 2. 处理特殊对象
92
+ if (value instanceof Date) return `${colors.magenta}${value.toISOString()}${colors.reset}`
93
+ if (value instanceof RegExp) return `${colors.magenta}${value.toString()}${colors.reset}`
94
+
95
+ // 3. 处理循环引用检查
96
+ const refId = circularIds.get(value)
97
+ if (seen.has(value) && refId) return `${colors.cyan}[Circular *${refId}]${colors.reset}`
98
+
99
+ // 4. 处理深度限制
100
+ if (currentDepth > depth) return `${colors.cyan}[Object]${colors.reset}`
101
+
102
+ // 标记为已处理
103
+ seen.add(value)
104
+
105
+ // 5. 构建对象/数组字符串
106
+ const isArray = Array.isArray(value)
107
+ const prefix = refId ? `${colors.cyan}<ref *${refId}>${colors.reset} ` : ''
108
+ const open = isArray ? '[' : '{'
109
+ const close = isArray ? ']' : '}'
110
+
111
+ const keys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)]
112
+
113
+ if (!keys.length) return `${prefix}${open}${close}`
114
+
115
+ const spaces = '\t'.repeat(currentDepth)
116
+ const nextSpaces = '\t'.repeat(currentDepth + 1)
117
+
118
+ const content = keys.map(key => {
119
+ let keyStr = ''
120
+ if (!isArray) {
121
+ keyStr = typeof key === 'symbol' ? `[${key.toString()}]` : key
122
+ if (!/^[$A-Z_a-z][\w$]*$/.test(keyStr)) keyStr = `'${keyStr.replaceAll('\'', '\\\'').replaceAll('\n', '\\n')}'`
123
+ keyStr += ': '
124
+ }
125
+ const valStr = format(value[key], currentDepth + 1)
126
+ return `${nextSpaces}${keyStr}${valStr}`
127
+ }).join(',\n')
128
+ seen.delete(value)
129
+ return `${prefix}${open}\n${content}\n${spaces}${close}`
130
+ }
131
+
132
+ return format(target, 0)
133
+ }
134
+
135
+ /**
136
+ * 将参数格式化为 HTML 字符串。
137
+ * @param {any} arg - 参数。
138
+ * @returns {string} 格式化后的 HTML 字符串。
139
+ */
140
+ function argToHtml(arg) {
141
+ if (arg instanceof Error && arg.stack) return ansi_up.ansi_to_html(arg.stack)
142
+ if ((arg === null || arg instanceof Object) && !(arg instanceof Function))
143
+ try { return ansi_up.ansi_to_html(JSON.stringify(arg, null, '\t')) }
144
+ catch { /* fall through */ }
145
+
146
+ return ansi_up.ansi_to_html(circularToString(arg))
147
+ }
148
+
5
149
  /**
6
150
  * 将 console 参数格式化为 HTML 字符串。
7
151
  * @param {any[]} args - console 方法接收的参数数组。
@@ -11,21 +155,13 @@ export function argsToHtml(args) {
11
155
  if (args.length === 0) return ''
12
156
  const format = args[0]
13
157
  if (format?.constructor !== String)
14
- return args.map(arg => {
15
- if (arg instanceof Error && arg.stack) return ansi_up.ansi_to_html(arg.stack)
16
- if ((arg === null || arg instanceof Object) && !(arg instanceof Function))
17
- try { return ansi_up.ansi_to_html(JSON.stringify(arg, null, '\t')) }
18
- catch { return String(arg) }
19
-
20
- return ansi_up.ansi_to_html(String(arg))
21
- }).join(' ')
22
-
158
+ return args.map(argToHtml).join(' ')
23
159
 
24
160
  let html = ansi_up.ansi_to_html(format)
25
161
  let argIndex = 1
26
162
  let hasStyle = false
27
163
 
28
- const regex = /%[sdifoOc%]/g
164
+ const regex = /%[%Ocdfijos]/g
29
165
  html = html.replace(regex, (match) => {
30
166
  if (match === '%%') return '%'
31
167
  if (argIndex >= args.length) return match
@@ -34,20 +170,21 @@ export function argsToHtml(args) {
34
170
  switch (match) {
35
171
  case '%c': {
36
172
  hasStyle = true
37
- const style = String(arg).replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
38
- return `</span><span style="${style}">`
173
+ return `</span><span style="${escapeHtml(safeString(arg))}">`
39
174
  }
40
175
  case '%s':
41
- return ansi_up.ansi_to_html(String(arg))
176
+ return ansi_up.ansi_to_html(safeString(arg))
42
177
  case '%d':
43
178
  case '%i':
44
- return String(parseInt(arg))
179
+ return String(parseInt(safeString(arg)))
45
180
  case '%f':
46
- return String(parseFloat(arg))
181
+ return String(parseFloat(safeString(arg)))
47
182
  case '%o':
48
183
  case '%O':
184
+ return ansi_up.ansi_to_html(circularToString(arg))
185
+ case '%j':
49
186
  try { return ansi_up.ansi_to_html(JSON.stringify(arg)) }
50
- catch { return String(arg) }
187
+ catch { return ansi_up.ansi_to_html(safeString(arg)) }
51
188
  }
52
189
  return match
53
190
  })
@@ -63,16 +200,7 @@ export function argsToHtml(args) {
63
200
  html = html.replaceAll(key, value)
64
201
  })
65
202
 
66
- while (argIndex < args.length) {
67
- const arg = args[argIndex++]
68
- html += ' '
69
- if (arg instanceof Error && arg.stack) html += ansi_up.ansi_to_html(arg.stack)
70
- else if ((arg === null || arg instanceof Object) && !(arg instanceof Function))
71
- try { html += ansi_up.ansi_to_html(JSON.stringify(arg, null, '\t')) }
72
- catch { html += String(arg) }
73
-
74
- else html += ansi_up.ansi_to_html(String(arg))
75
- }
203
+ html += ' ' + args.slice(argIndex).map(argToHtml).join(' ')
76
204
 
77
- return html
205
+ return html.trim().replaceAll('\n', '<br/>\n')
78
206
  }