adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
285 lines
6.2 KiB
Svelte
285 lines
6.2 KiB
Svelte
<script lang="ts">
|
|
import { tick } from "svelte";
|
|
import Self from "./JSONNode.svelte";
|
|
|
|
let {
|
|
value,
|
|
depth = 0,
|
|
is_root = false,
|
|
is_last_item = true,
|
|
key = null,
|
|
open = false,
|
|
theme_mode = "system" as "system" | "light" | "dark",
|
|
show_indices = false,
|
|
interactive = true
|
|
}: {
|
|
value: any;
|
|
depth?: number;
|
|
is_root?: boolean;
|
|
is_last_item?: boolean;
|
|
key?: string | number | null;
|
|
open?: boolean;
|
|
theme_mode?: "system" | "light" | "dark";
|
|
show_indices?: boolean;
|
|
interactive?: boolean;
|
|
} = $props();
|
|
|
|
let root_element = $state<HTMLElement>();
|
|
let collapsed = $state(open ? false : depth >= 3);
|
|
let child_nodes = $state<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();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
$effect(() => {
|
|
if (is_collapsible(value)) {
|
|
child_nodes = Object.entries(value);
|
|
} else {
|
|
child_nodes = [];
|
|
}
|
|
});
|
|
|
|
function updateLineNumbers(): void {
|
|
if (!root_element) return;
|
|
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}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
$effect(() => {
|
|
value;
|
|
collapsed;
|
|
|
|
if (is_root && root_element) {
|
|
tick().then(() => {
|
|
updateLineNumbers();
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="json-node"
|
|
class:root={is_root}
|
|
class:dark-mode={theme_mode === "dark"}
|
|
bind:this={root_element}
|
|
style="--depth: {depth};"
|
|
>
|
|
<div class="line" class:collapsed>
|
|
<span class="line-number"></span>
|
|
<span class="content">
|
|
{#if is_collapsible(value)}
|
|
<button
|
|
data-pseudo-content={interactive ? (collapsed ? "▶" : "▼") : ""}
|
|
aria-label={collapsed ? "Expand" : "Collapse"}
|
|
class="toggle"
|
|
disabled={!interactive}
|
|
onclick={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 onclick={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 string">"{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}
|
|
<Self
|
|
value={subVal}
|
|
depth={depth + 1}
|
|
is_last_item={i === child_nodes.length - 1}
|
|
key={Array.isArray(value) && !show_indices ? null : subKey}
|
|
{open}
|
|
{theme_mode}
|
|
{show_indices}
|
|
{interactive}
|
|
/>
|
|
{/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>
|