Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 81 additions & 13 deletions src/console-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,64 +20,132 @@ 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()
},
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(){
Expand Down
112 changes: 109 additions & 3 deletions src/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<span class="${className}">${signature}${functionBody}</span>`
}

if (value && value.type === 'circular') {
return '<span>[Circular]</span>'
}

if (value && value.type === 'regexp') {
return `<span class="console-regexp">${escapeHtml(value.value)}</span>`
}

if (value && value.type === 'unknown') {
return `<span>${escapeHtml(String(value.value))}</span>`
}

if (value && value.type === 'date') {
const isoString = value.value
const cleanedString = isoString
.replace('T', ' ')
.replace(/\.\d{3}Z$/, '')

return `<span class="console-date">${escapeHtml(cleanedString)}</span>`
}

if (value && value.type === 'set') {
const short = `Set(${value.size})`

if (value.size === 0) return `<span>${short} {}</span>`

let result = `<span>${short} {\n`

;(value.values || []).forEach((v, index) => {
result += `${indent} ${formatValue(v, indentLevel + 1)}`
if (index < value.values.length - 1) result += ','
result += '\n'
})

result += `${indent}}</span>`
return result
}

const isSymbolKey = (k) => typeof k === 'string' && k.startsWith('Symbol(') && k.endsWith(')')

const formatKey = (key) => {
return (isValidIdentifier(key) || isSymbolKey(key))
? `<span class="console-key">${escapeHtml(key)}</span>`
: `<span class="console-string">"${escapeHtml(key)}"</span>`
}

if (value && value.type === 'map') {
const short = `Map(${value.size})`
if (value.size === 0) return `<span>${short} {}</span>`

let result = `<span>${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}}</span>`
return result
}

const keys = Object.keys(value)
if (keys.length === 0) return '{}'

let result = '{\n'

keys.forEach((key, index) => {
const renderedKey = isValidIdentifier(key)
? `<span class="console-key">${escapeHtml(key)}</span>`
: `<span class="console-string">"${escapeHtml(key)}"</span>`
const renderedKey = formatKey(key)

result += `${indent} ${renderedKey}: ${formatValue(value[key], indentLevel + 1)}`

Expand Down
22 changes: 22 additions & 0 deletions src/css/console.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down