JSON html viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @return {string} | |
*/ | |
export function jsonHtml(json) { | |
let indentSize = 0 | |
function indent() { | |
return `<span style="font-size: 0px; position: absolute;">${' '.repeat( | |
indentSize, | |
)}</span>` | |
} | |
const html = tokenizeJson(json) | |
.map(({ type, value, ...extra }) => { | |
if (type === 'key') { | |
return `<div style="padding-top: 0.25rem" data-for="${ | |
extra.for | |
}">${indent()}<span data-key style="color: #bfc7d5">${value}</span>` | |
} | |
if (type === 'object-open') indentSize += 2 | |
if (type === 'object-close') indentSize -= 2 | |
const config = { | |
'key-end': { suffix: '</div>' }, | |
comma: { style: 'color: #c792ea' }, | |
'object-open': { | |
suffix: '<div data-object style="padding-left: 2ch">', | |
style: 'color: #c792ea', | |
}, | |
'object-close': { | |
prefix: '</div>' + indent(), | |
style: 'color: #c792ea', | |
}, | |
string: { style: 'color: #c3e88d' }, | |
number: { style: 'color: #f78c6c' }, | |
null: { style: 'color: #bfc7d5; font-style: italic' }, | |
separator: { style: 'color: #89ddff', suffix: ' ' }, | |
} | |
if (type in config) { | |
const cfg = config[type] | |
return ( | |
(cfg.prefix || '') + | |
(cfg.style ? `<span style='${cfg.style}'>${value}</span>` : value) + | |
(cfg.suffix || '') | |
) | |
} | |
return value | |
}) | |
.join('') | |
return `<div><div data-wrap style='background-color: rgb(41, 45, 62); color: white; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; padding: 1rem; border-radius: 0.4rem;white-space:pre'>${html}</div> | |
<script> | |
var wrap = document.currentScript.parentElement.querySelector('[data-wrap]') | |
for(const o of wrap.querySelectorAll('[data-for="object"]')) { | |
const plus = document.createElement('button') | |
plus.style.all = 'unset' | |
plus.style.position = 'absolute' | |
plus.style.transform = 'translate(-1.1rem, -0.2rem)' | |
plus.style.cursor = 'pointer' | |
plus.style.padding = '0.125rem' | |
plus.style.userSelect = 'none' | |
plus.innerText = '-' | |
o.prepend(plus) | |
} | |
for(const o of wrap.querySelectorAll('[data-for="object"] > [data-key]')) { | |
o.style.cursor = 'pointer' | |
} | |
wrap.addEventListener('click', (event) => { | |
if(event.target.tagName !== 'BUTTON' && (!event.target.dataset || event.target.dataset.key === undefined)) return | |
event.preventDefault() | |
const obj = event.target.parentElement.querySelector('button ~ [data-object]') | |
const button = event.target.parentElement.querySelector('button') | |
if (button.innerText === '+') { | |
button.innerText = '-' | |
const dots = event.target.parentElement.querySelector('[data-dots]') | |
event.target.parentElement.removeChild(dots) | |
obj.style.fontSize = '' | |
obj.style.position = '' | |
} else { | |
button.innerText = '+' | |
const dots = document.createElement('span') | |
dots.dataset.dots = '' | |
dots.innerText = '…' | |
dots.style.userSelect = 'none' | |
event.target.parentElement.insertBefore(dots, obj) | |
obj.style.fontSize = '0px' | |
obj.style.position = 'absolute' | |
} | |
}) | |
console.log(wrap) | |
</script> | |
</div>` | |
} | |
function tokenizeJson(value) { | |
if (typeof value === 'string') { | |
return [{ type: 'string', value: JSON.stringify(value) }] | |
} else if (typeof value === 'number') { | |
return [{ type: 'number', value: value + '' }] | |
} else if (value === null) { | |
return [{ type: 'null', value: 'null' }] | |
} else if (value === undefined) { | |
return [{ type: undefined, value: 'undefined' }] | |
} else if (Array.isArray(value)) { | |
const parts = value | |
.map((v, i, list) => { | |
return [ | |
{ | |
type: 'key', | |
value: '', | |
for: typeof v !== 'object' ? typeof v : v ? 'object' : 'null', | |
}, | |
...tokenizeJson(v), | |
...(i === list.length - 1 ? [] : [{ type: 'comma', value: ',' }]), | |
{ type: 'key-end', value: '' }, | |
] | |
}) | |
.reduce((a, b) => a.concat(b), []) | |
return [ | |
{ type: 'object-open', value: '[' }, | |
...parts, | |
{ type: 'object-close', value: ']' }, | |
] | |
} else if (typeof value === 'object') { | |
const parts = Object.entries(value) | |
.filter(([k, v]) => v !== undefined) | |
.map(([k, v], i, list) => { | |
return [ | |
{ | |
type: 'key', | |
value: JSON.stringify(k + ''), | |
for: typeof v !== 'object' ? typeof v : v ? 'object' : 'null', | |
}, | |
{ type: 'separator', value: ':' }, | |
...tokenizeJson(v), | |
...(i === list.length - 1 ? [] : [{ type: 'comma', value: ',' }]), | |
{ type: 'key-end', value: '' }, | |
] | |
}) | |
.reduce((a, b) => a.concat(b), []) | |
return [ | |
{ type: 'object-open', value: '{' }, | |
...parts, | |
{ type: 'object-close', value: '}' }, | |
] | |
} | |
return [{ type: 'null', value: 'null' }] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment