diff --git a/src/console-script.js b/src/console-script.js index 58ce4b2..66774ef 100644 --- a/src/console-script.js +++ b/src/console-script.js @@ -20,44 +20,112 @@ export const generateConsoleScript = ({ html, css }) => { pushToConsole({line:fixedLine, column, message}, "error") } + const encodeFunction = (fn) => ({ + type: 'function', + name: fn.name || '(anonymous)', + async: fn.constructor?.name === 'AsyncFunction', + generator: fn.constructor?.name === 'GeneratorFunction', + content: fn.toString() + }) + + const encode = (value, seen) => { + if (typeof value === 'function') return encodeFunction(value) + + // primitives + if (value === null || typeof value !== 'object') return value + + // circular detection + if (seen.has(value)) return { type: 'circular' } + seen.add(value) + + if (Array.isArray(value)) { + return value.map(item => encode(item, seen)) + } + + if (value instanceof RegExp) { + return { type: 'regexp', value: value.toString() } + } + + if (value instanceof Date) { + return { type: 'date', value: value.toISOString() } + } + + if (value instanceof Set) { + const values = [] + for (const v of value.values()) { + values.push(encode(v, seen)) + } + return { type: 'set', size: value.size, values } + } + + if (value instanceof Map) { + const entries = [] + for (const [k, v] of value.entries()) { + entries.push([encode(k, seen), encode(v, seen)]) + } + return { type: 'map', size: value.size, entries } + } + + if (value.constructor === Object) { + const out = {} + const keys = Reflect.ownKeys(value) // includes symbols + + for (const key of keys) { + const outKey = typeof key === 'symbol' ? key.toString() : key + out[outKey] = encode(value[key], seen) + } + + return out + } + + // Fallback: try to serialize toString (safe) + try { + return { type: 'unknown', value: value.toString() } + } catch (e) { + return { type: 'unknown', value: Object.prototype.toString.call(value) } + } + } + + const serialize = (args) => args.map(arg => encode(arg, new WeakSet())) + const counts = {} const timers = {} const console = { log: function(...args){ - pushToConsole(args, "log:log") + pushToConsole(serialize(args), "log:log") }, error: function(...args){ - pushToConsole(args, "log:error") + pushToConsole(serialize(args), "log:error") }, warn: function(...args){ - pushToConsole(args, "log:warn") + pushToConsole(serialize(args), "log:warn") }, info: function(...args){ - pushToConsole(args, "log:info") + pushToConsole(serialize(args), "log:info") }, debug: function(...args){ - pushToConsole(args, "log:debug") + pushToConsole(serialize(args), "log:debug") }, table: function(data){ - pushToConsole([data], "log:table") + pushToConsole(serialize([data]), "log:table") }, count: function(label = 'default'){ counts[label] = (counts[label] || 0) + 1 - pushToConsole([label + ": " + counts[label]], "log:count") + pushToConsole(serialize([label + ": " + counts[label]]), "log:count") }, countReset: function(label = 'default'){ counts[label] = 0 }, trace: function(...args){ const stack = new Error().stack - pushToConsole([...args, stack], "log:trace") + pushToConsole(serialize([...args, stack]), "log:trace") }, dir: function(obj){ - pushToConsole([obj], "log:dir") + pushToConsole(serialize([obj]), "log:dir") }, dirxml: function(obj){ - pushToConsole([obj], "log:dirxml") + pushToConsole(serialize([obj]), "log:dirxml") }, time: function(label = 'default'){ timers[label] = performance.now() @@ -65,19 +133,19 @@ export const generateConsoleScript = ({ html, css }) => { timeEnd: function(label = 'default'){ if (timers[label]) { const duration = performance.now() - timers[label] - pushToConsole([label + ": " + duration.toFixed(2) + "ms"], "log:time") + pushToConsole(serialize([label + ": " + duration.toFixed(2) + "ms"]), "log:time") delete timers[label] } }, timeLog: function(label = 'default', ...args){ if (timers[label]) { const duration = performance.now() - timers[label] - pushToConsole([label + ": " + duration.toFixed(2) + "ms"].concat(args), "log:time") + pushToConsole(serialize([label + ": " + duration.toFixed(2) + "ms"].concat(args)), "log:time") } }, assert: function(condition, ...args){ if (!condition) { - pushToConsole(["Assertion failed:", ...args], "log:assert") + pushToConsole(serialize(["Assertion failed:", ...args]), "log:assert") } }, clear: function(){ diff --git a/src/console.js b/src/console.js index b79f502..2f253f6 100644 --- a/src/console.js +++ b/src/console.js @@ -134,15 +134,121 @@ const formatValue = (value, indentLevel = 0) => { } if (typeof value === 'object') { + if (value && value.type === 'function') { + const fnContent = escapeHtml(value.content) + const startOfBody = fnContent.indexOf('{') + + const isAsync = value.async + const isGenerator = value.generator + + let className = 'console-fn' + if (isAsync) { + className += ' console-async-fn' + } + if (isGenerator) { + className += ' console-generator-fn' + } + + // Function signature logic + let signature + if (startOfBody === -1) { + signature = fnContent.trim() + } else { + signature = fnContent.substring(0, startOfBody).trim() + } + + signature = signature + .replace(/^async\s+/, '') + .replace(/^function\s*\*\s*/, '') + .replace(/^function\s*/, '') + .trim() + + // Function body logic + let functionBody + if (startOfBody === -1) { + functionBody = '' + } else { + const bodyContent = fnContent.substring(startOfBody + 1, fnContent.lastIndexOf('}')) + const compressedBody = bodyContent.trim().length > 0 ? '...' : '' + functionBody = ` {${compressedBody}}` + } + + return `${signature}${functionBody}` + } + + if (value && value.type === 'circular') { + return '[Circular]' + } + + if (value && value.type === 'regexp') { + return `${escapeHtml(value.value)}` + } + + if (value && value.type === 'unknown') { + return `${escapeHtml(String(value.value))}` + } + + if (value && value.type === 'date') { + const isoString = value.value + const cleanedString = isoString + .replace('T', ' ') + .replace(/\.\d{3}Z$/, '') + + return `${escapeHtml(cleanedString)}` + } + + if (value && value.type === 'set') { + const short = `Set(${value.size})` + + if (value.size === 0) return `${short} {}` + + let result = `${short} {\n` + + ;(value.values || []).forEach((v, index) => { + result += `${indent} ${formatValue(v, indentLevel + 1)}` + if (index < value.values.length - 1) result += ',' + result += '\n' + }) + + result += `${indent}}` + return result + } + + const isSymbolKey = (k) => typeof k === 'string' && k.startsWith('Symbol(') && k.endsWith(')') + + const formatKey = (key) => { + return (isValidIdentifier(key) || isSymbolKey(key)) + ? `${escapeHtml(key)}` + : `"${escapeHtml(key)}"` + } + + if (value && value.type === 'map') { + const short = `Map(${value.size})` + if (value.size === 0) return `${short} {}` + + let result = `${short} {\n` + + ;(value.entries || []).forEach(([k, v], index) => { + const keyFormatted = formatKey(k) + const valueFormatted = formatValue(v, indentLevel + 1) + + result += `${indent} ${keyFormatted} => ${valueFormatted}` + + if (index < value.entries.length - 1) result += ',' + result += '\n' + }) + + result += `${indent}}` + return result + } + const keys = Object.keys(value) if (keys.length === 0) return '{}' let result = '{\n' keys.forEach((key, index) => { - const renderedKey = isValidIdentifier(key) - ? `${escapeHtml(key)}` - : `"${escapeHtml(key)}"` + const renderedKey = formatKey(key) result += `${indent} ${renderedKey}: ${formatValue(value[key], indentLevel + 1)}` diff --git a/src/css/console.css b/src/css/console.css index c3f9871..8c9c642 100644 --- a/src/css/console.css +++ b/src/css/console.css @@ -128,6 +128,28 @@ color: #9cdcfe; } + .console-regexp { + color: #b46695; + } + + .console-date { + color: #9cdcfe; + } + + .console-fn::before { + content: "ƒ "; + font-style: italic; + color: #ce9178; + } + + .console-async-fn::before { + content: "async ƒ "; + } + + .console-generator-fn::before { + content: "ƒ* "; + } + .console-badge { display: inline-block; padding: 2px 6px;