Spaces:
Runtime error
Runtime error
<script lang="ts"> | |
import { onMount, createEventDispatcher, tick, afterUpdate } from "svelte"; | |
import { Marked } from "marked"; | |
import { markedHighlight } from "marked-highlight"; | |
import hljs from 'highlight.js'; | |
import "highlight.js/styles/github.css"; // You can choose a different style | |
export let value: any; | |
export let depth = 0; | |
export let is_root = false; | |
export let is_last_item = true; | |
export let key: string | number | null = null; | |
export let open = false; | |
export let theme_mode: "system" | "light" | "dark" = "system"; | |
export let show_indices = false; | |
const dispatch = createEventDispatcher(); | |
let root_element: HTMLElement; | |
let collapsed = open ? false : depth >= 3; | |
let child_nodes: any[] = []; | |
function is_collapsible(val: any): boolean { | |
return val !== null && (typeof val === "object" || Array.isArray(val)); | |
} | |
async function toggle_collapse(): Promise<void> { | |
collapsed = !collapsed; | |
await tick(); | |
dispatch("toggle", { collapsed, depth }); | |
} | |
function get_collapsed_preview(val: any): string { | |
if (Array.isArray(val)) return `Array(${val.length})`; | |
if (typeof val === "object" && val !== null) | |
return `Object(${Object.keys(val).length})`; | |
return String(val); | |
} | |
const marked = new Marked( | |
markedHighlight({ | |
langPrefix: 'hljs language-', | |
highlight(code, lang, info) { | |
const language = hljs.getLanguage(lang) ? lang : 'plaintext'; | |
return hljs.highlight(code, { language }).value; | |
} | |
}) | |
); | |
function escapeXmlTagsOutsideCodeBlocks(value: string): string { | |
const codeBlockRegex = /(```[\s\S]*?```)/g; | |
const parts = value.split(codeBlockRegex); | |
return parts.map(part => { | |
if (part.startsWith('```') && part.endsWith('```')) { | |
return part; // Do not escape inside code blocks | |
} | |
return part.replace(/</g, '<').replace(/>/g, '>'); // Escape outside code blocks | |
}).join(''); | |
} | |
function toMarkdown(value: string): string { | |
// console.log("Render:" + value); | |
const escapedValue = escapeXmlTagsOutsideCodeBlocks(value); | |
//console.log("Escaped:" + escapedValue); | |
const parsed = marked.parse(escapedValue); | |
const parser = new DOMParser(); | |
const doc = parser.parseFromString(parsed, 'text/html'); | |
doc.querySelectorAll('code').forEach((codeElement) => { | |
codeElement.setAttribute('style', 'border: 1px solid #d3d3d3; background-color: rgba(150,150,150,0.05);'); | |
}); | |
return doc.body.innerHTML; | |
} | |
$: if (is_collapsible(value)) { | |
child_nodes = Object.entries(value); | |
} else { | |
child_nodes = []; | |
} | |
$: if (is_root && root_element) { | |
updateLineNumbers(); | |
} | |
function updateLineNumbers(): void { | |
const lines = root_element.querySelectorAll(".line"); | |
lines.forEach((line, index) => { | |
const line_number = line.querySelector(".line-number"); | |
if (line_number) { | |
line_number.setAttribute("data-pseudo-content", (index + 1).toString()); | |
line_number?.setAttribute( | |
"aria-roledescription", | |
`Line number ${index + 1}` | |
); | |
line_number?.setAttribute("title", `Line number ${index + 1}`); | |
} | |
}); | |
} | |
onMount(() => { | |
if (is_root) { | |
updateLineNumbers(); | |
} | |
}); | |
afterUpdate(() => { | |
if (is_root) { | |
updateLineNumbers(); | |
} | |
}); | |
</script> | |
<div | |
class="json-node" | |
class:root={is_root} | |
class:dark-mode={theme_mode === "dark"} | |
bind:this={root_element} | |
on:toggle | |
style="--depth: {depth};" | |
> | |
<div class="line" class:collapsed> | |
<span class="line-number"></span> | |
<span class="content"> | |
{#if is_collapsible(value)} | |
<button | |
data-pseudo-content={collapsed ? "▶" : "▼"} | |
aria-label={collapsed ? "Expand" : "Collapse"} | |
class="toggle" | |
on:click={toggle_collapse} | |
/> | |
{/if} | |
{#if key !== null} | |
<span class="key">"{key}"</span><span class="punctuation colon" | |
>: | |
</span> | |
{/if} | |
{#if is_collapsible(value)} | |
<span | |
class="punctuation bracket" | |
class:square-bracket={Array.isArray(value)} | |
>{Array.isArray(value) ? "[" : "{"}</span | |
> | |
{#if collapsed} | |
<button on:click={toggle_collapse} class="preview"> | |
{get_collapsed_preview(value)} | |
</button> | |
<span | |
class="punctuation bracket" | |
class:square-bracket={Array.isArray(value)} | |
>{Array.isArray(value) ? "]" : "}"}</span | |
> | |
{/if} | |
{:else if typeof value === "string"} | |
<span class="value">{@html toMarkdown(value)}</span> | |
{:else if typeof value === "number"} | |
<span class="value number">{value}</span> | |
{:else if typeof value === "boolean"} | |
<span class="value bool">{value.toString()}</span> | |
{:else if value === null} | |
<span class="value null">null</span> | |
{:else} | |
<span>{value}</span> | |
{/if} | |
{#if !is_last_item && (!is_collapsible(value) || collapsed)} | |
<span class="punctuation">,</span> | |
{/if} | |
</span> | |
</div> | |
{#if is_collapsible(value)} | |
<div class="children" class:hidden={collapsed}> | |
{#each child_nodes as [subKey, subVal], i} | |
<svelte:self | |
value={subVal} | |
depth={depth + 1} | |
is_last_item={i === child_nodes.length - 1} | |
key={subKey} | |
{open} | |
{theme_mode} | |
{show_indices} | |
on:toggle | |
/> | |
{/each} | |
<div class="line"> | |
<span class="line-number"></span> | |
<span class="content"> | |
<span | |
class="punctuation bracket" | |
class:square-bracket={Array.isArray(value)} | |
>{Array.isArray(value) ? "]" : "}"}</span | |
> | |
{#if !is_last_item}<span class="punctuation">,</span>{/if} | |
</span> | |
</div> | |
</div> | |
{/if} | |
</div> | |
<style> | |
.json-node { | |
font-family: var(--font-mono); | |
--text-color: #d18770; | |
--key-color: var(--text-color); | |
--string-color: #ce9178; | |
--number-color: #719fad; | |
--bracket-color: #5d8585; | |
--square-bracket-color: #be6069; | |
--punctuation-color: #8fbcbb; | |
--line-number-color: #6a737d; | |
--separator-color: var(--line-number-color); | |
} | |
.json-node.dark-mode { | |
--bracket-color: #7eb4b3; | |
--number-color: #638d9a; | |
} | |
.json-node.root { | |
position: relative; | |
padding-left: var(--size-14); | |
} | |
.json-node.root::before { | |
content: ""; | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
left: var(--size-11); | |
width: 1px; | |
background-color: var(--separator-color); | |
} | |
.line { | |
display: flex; | |
align-items: flex-start; | |
padding: 0; | |
margin: 0; | |
line-height: var(--line-md); | |
} | |
.line-number { | |
position: absolute; | |
left: 0; | |
width: calc(var(--size-7)); | |
text-align: right; | |
color: var(--line-number-color); | |
user-select: none; | |
text-overflow: ellipsis; | |
text-overflow: ellipsis; | |
direction: rtl; | |
overflow: hidden; | |
} | |
.content { | |
flex: 1; | |
display: flex; | |
align-items: center; | |
padding-left: calc(var(--depth) * var(--size-2)); | |
flex-wrap: wrap; | |
} | |
.children { | |
padding-left: var(--size-4); | |
} | |
.children.hidden { | |
display: none; | |
} | |
.key { | |
color: var(--key-color); | |
} | |
.string { | |
color: var(--string-color); | |
} | |
.number { | |
color: var(--number-color); | |
} | |
.bool { | |
color: var(--text-color); | |
} | |
.null { | |
color: var(--text-color); | |
} | |
.value { | |
margin-left: var(--spacing-md); | |
} | |
.punctuation { | |
color: var(--punctuation-color); | |
} | |
.bracket { | |
margin-left: var(--spacing-sm); | |
color: var(--bracket-color); | |
} | |
.square-bracket { | |
margin-left: var(--spacing-sm); | |
color: var(--square-bracket-color); | |
} | |
.toggle, | |
.preview { | |
background: none; | |
border: none; | |
color: inherit; | |
cursor: pointer; | |
padding: 0; | |
margin: 0; | |
} | |
.toggle { | |
user-select: none; | |
margin-right: var(--spacing-md); | |
} | |
.preview { | |
margin: 0 var(--spacing-sm) 0 var(--spacing-lg); | |
} | |
.preview:hover { | |
text-decoration: underline; | |
} | |
:global([data-pseudo-content])::before { | |
content: attr(data-pseudo-content); | |
} | |
</style> | |