UNPKG

@steve02081504/virtual-console

Version:

A virtual console for capturing and manipulating terminal output.

186 lines (164 loc) 5.83 kB
/** * 解析 `%` 格式串、将参数转为 `LogSegment[]`、单行格式化(与 Node `util.format` / `console.log` 对齐)。 */ import { DEFAULT_SNAPSHOT_DEPTH, serializeArgSnapshot, } from '../core/snapshot.mjs' import { coerceString, } from './ansi.mjs' /** @typedef {{ kind: 'literal', text: string }} PrintfLiteralPart */ /** @typedef {{ kind: 'arg', spec: string, value: any }} PrintfArgPart */ /** @typedef {{ kind: 'missingSpec', spec: string }} PrintfMissingPart */ /** * @typedef {object} PrintfSegmentBuildContext * @property {import('../shared.d.mts').LogSegment[]} segments * @property {number} maxDepth * @property {object | null} expansionScope * @property {(text: string) => void} pushText */ /** * 扫描 printf 风格首参 `format` 与 `args[1..]` 的对齐关系,产出统一 token 流(供 `buildArgsSegments` / `renderPlain` 消费)。 * @param {string} format - 首参模板串。 * @param {any[]} args - 完整实参数组(含 `args[0] === format`)。 * @param {number} [startArgIndex=1] - 从第几个下标开始消费(通常为 `1`)。 * @returns {{ parts: Array<PrintfLiteralPart | PrintfArgPart | PrintfMissingPart>; nextArgIndex: number }} * `parts` 为从左到右的模板片段与占位解析结果;`nextArgIndex` 为已消费的最后一个实参的下一索引(尾部额外实参从此继续)。 */ export function collectPrintfFormatParts(format, args, startArgIndex = 1) { const parts = /** @type {Array<PrintfLiteralPart | PrintfArgPart | PrintfMissingPart>} */[] let lastIndex = 0 let argIndex = startArgIndex const regex = /%[%Ocdfijos]/g let match while ((match = regex.exec(format)) !== null) { const before = format.slice(lastIndex, match.index) if (before) parts.push({ kind: 'literal', text: before }) lastIndex = regex.lastIndex const spec = match[0] if (spec === '%%') { parts.push({ kind: 'literal', text: '%' }) continue } if (argIndex >= args.length) { parts.push({ kind: 'missingSpec', spec }) continue } const value = args[argIndex++] parts.push({ kind: 'arg', spec, value }) } const tail = format.slice(lastIndex) if (tail) parts.push({ kind: 'literal', text: tail }) return { parts, nextArgIndex: argIndex } } /** * 按单个 printf 说明符将实参折叠进 `buildContext`(推入 `text` / `value` / `css` 等片段)。 * @param {string} spec - 如 `%s`(已排除 `%%`)。 * @param {any} value - 绑定实参。 * @param {PrintfSegmentBuildContext} buildContext - `buildArgsSegments` 侧可变状态。 * @returns {void} */ function applyPrintfSpecifier(spec, value, buildContext) { const { segments, maxDepth, expansionScope, pushText } = buildContext switch (spec) { case '%c': segments.push({ kind: 'css', css: coerceString(value) }) break case '%s': pushText(coerceString(value)) break case '%d': case '%i': { let n try { n = String(parseInt(value)) } catch { n = 'NaN' } pushText(n) break } case '%f': { let n try { n = String(parseFloat(value)) } catch { n = 'NaN' } pushText(n) break } case '%o': case '%O': { const snap = serializeArgSnapshot(value, { maxDepth, expansionScope }) segments.push({ kind: 'value', snapshot: snap }) break } case '%j': try { const jsonText = JSON.stringify(value, null, '\t') pushText(jsonText) } catch { pushText(coerceString(value)) } break default: break } } /** * 将 printf 实参转为结构化片段(含快照树);`expansionScope` / `snapshotDepth` 仅影响快照序列化。 * @param {any[]} args - `console.*` 收到的原始参数数组。 * @param {object | null} [expansionScope=null] - 惰性展开上下文(如 {@link createExpansionScope} 返回值);无宿主条目时为 `null`。 * @param {number} [snapshotDepth] - 快照递归深度上限;默认 {@link DEFAULT_SNAPSHOT_DEPTH}。 * @returns {import('../shared.d.mts').LogSegment[]} 有序片段。 */ export function buildArgsSegments(args, expansionScope = null, snapshotDepth = DEFAULT_SNAPSHOT_DEPTH) { if (!args.length) return [] const maxDepth = snapshotDepth const format = args[0] if (format?.constructor !== String) { const segments = /** @type {import('../shared.d.mts').LogSegment[]} */[] for (let i = 0; i < args.length; i++) { if (i) segments.push({ kind: 'text', text: ' ' }) segments.push({ kind: 'value', snapshot: serializeArgSnapshot(args[i], { maxDepth, expansionScope }), }) } return segments } // 与 Node `console.log` / `util.format` 一致:`util.format` 只有单个字符串参数时原样返回,不解析 `%`。 if (args.length === 1) return [{ kind: 'text', text: String(format) }] const segments = /** @type {import('../shared.d.mts').LogSegment[]} */[] const { parts, nextArgIndex } = collectPrintfFormatParts(format, args, 1) /** * 追加字面文本段。 * @param {string} text - 要写入的文本。 */ function pushText(text) { if (!text) return segments.push({ kind: 'text', text }) } const segmentBuildContext = { segments, maxDepth, expansionScope, pushText } for (const part of parts) { if (part.kind === 'literal') { pushText(part.text) continue } if (part.kind === 'missingSpec') { pushText(part.spec) continue } applyPrintfSpecifier(part.spec, part.value, segmentBuildContext) } for (let argIndex = nextArgIndex; argIndex < args.length; argIndex++) { const arg = args[argIndex] if (segments.length) segments.push({ kind: 'text', text: ' ' }) const t = typeof arg if ((t === 'object' && arg !== null) || t === 'function') segments.push({ kind: 'value', snapshot: serializeArgSnapshot(arg, { maxDepth, expansionScope }), }) else pushText(coerceString(arg)) } return segments }