@steve02081504/virtual-console 0.1.2 → 0.1.4
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 +6 -0
- package/browser.mjs +11 -8
- package/main.mjs +7 -7
- package/node.mjs +154 -84
- package/package.json +1 -1
- package/util.mjs +155 -27
package/README.md
CHANGED
|
@@ -31,6 +31,12 @@ A powerful and flexible virtual console for **Node.js and the Browser** that all
|
|
|
31
31
|
npm install @steve02081504/virtual-console
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
### Browser Import
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import { VirtualConsole } from 'https://esm.sh/@steve02081504/virtual-console';
|
|
38
|
+
```
|
|
39
|
+
|
|
34
40
|
## Usage
|
|
35
41
|
|
|
36
42
|
### 1. Basic Testing (Capture Output)
|
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 } 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
|
|
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 = /%[
|
|
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 +=
|
|
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 +=
|
|
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 +=
|
|
81
|
+
catch { output += safeString(arg) }
|
|
80
82
|
|
|
81
|
-
else output +=
|
|
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
|
-
|
|
115
|
+
options = { ...options }
|
|
116
|
+
this.#base_console = options.base_console || consoleReflect()
|
|
114
117
|
delete options.base_console
|
|
115
118
|
|
|
116
119
|
this.options = {
|
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
|
|
6
|
+
export const { consoleAsyncStorage } = module
|
|
7
7
|
/**
|
|
8
8
|
* @type {typeof VirtualConsole}
|
|
9
9
|
*/
|
|
10
|
-
export const VirtualConsole = module
|
|
10
|
+
export const { VirtualConsole } = module
|
|
11
11
|
/**
|
|
12
12
|
* @type {VirtualConsole}
|
|
13
13
|
*/
|
|
14
|
-
export const defaultConsole = module
|
|
14
|
+
export const { defaultConsole } = module
|
|
15
15
|
/**
|
|
16
16
|
* @type {object}
|
|
17
17
|
*/
|
|
18
|
-
export const globalConsoleAdditionalProperties = module
|
|
18
|
+
export const { globalConsoleAdditionalProperties } = module
|
|
19
19
|
/**
|
|
20
20
|
* @type {function}
|
|
21
21
|
*/
|
|
22
|
-
export const setGlobalConsoleReflect = module
|
|
22
|
+
export const { setGlobalConsoleReflect } = module
|
|
23
23
|
/**
|
|
24
24
|
* @type {function}
|
|
25
25
|
*/
|
|
26
|
-
export const getGlobalConsoleReflect = module
|
|
26
|
+
export const { getGlobalConsoleReflect } = module
|
|
27
27
|
/**
|
|
28
28
|
* @type {VirtualConsole}
|
|
29
29
|
*/
|
|
30
|
-
export const console = module
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 =
|
|
200
|
-
this._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
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
|
+
function escapeHtml(str) {
|
|
11
|
+
return str.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>')
|
|
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(
|
|
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 = /%[
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
}
|