Files
servo/components/default-resources/resources/json-viewer.html
webbeef 0b5688fdfa parser: add a pretty printer for top-level json documents (#43702)
This adds a new resource implementing a simple pretty printer for json
documents.

Testing: build this branch and launch with `./mach run
https://httpbin.org/json`

<img width="1044" height="1064" alt="image"
src="https://github.com/user-attachments/assets/42680c4b-2971-482a-af2b-9017f0f81752"
/>

---------

Signed-off-by: webbeef <me@webbeef.org>
Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
Co-authored-by: Tim van der Lippe <TimvdLippe@users.noreply.github.com>
2026-04-05 06:50:52 +00:00

270 lines
6.7 KiB
HTML

<html>
<head>
<style>
body {
font-family: monospace;
margin: 0;
padding: 0;
background: #fff;
color: #333;
}
#json-raw {
display: none;
}
#viewer {
display: none;
padding: 0.5em 1em;
line-height: 1.5;
&.active {
display: block;
}
}
#toolbar {
display: flex;
gap: 1em;
padding: 0.5em;
align-items: center;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
& button {
min-width: 5em;
&.active {
background: #ddd;
font-weight: bold;
}
}
}
#raw-view {
display: none;
padding: 0.5em 1em;
white-space: pre-wrap;
word-break: break-all;
&.active {
display: block;
}
}
.json-error {
padding: 0.5em 1em;
color: #c00;
font-weight: bold;
}
/* Syntax highlighting for Json data types */
.json-key {
color: #881391;
}
.json-string {
color: #1a1aa6;
}
.json-number {
color: #1c00cf;
}
.json-boolean {
color: #0d22aa;
}
.json-null {
color: #808080;
}
/* Collapsible tree */
.toggle {
cursor: pointer;
user-select: none;
&::before {
content: "\25BC";
display: inline-block;
width: 1em;
transition: transform 0.1s;
}
&.collapsed::before {
transform: rotate(-90deg);
}
}
.collapsible {
margin-left: 1.5em;
&.hidden {
display: none;
}
}
.bracket {
color: #333;
}
.comma {
color: #333;
}
.line {
padding-left: 0;
}
</style>
<script>
// Shortcut to create an element with an optional class and text content.
function createElement(name, classes = null, textContent = null) {
let node = document.createElement(name);
if (classes) {
node.className = classes;
}
if (textContent) {
node.textContent = textContent;
}
return node;
}
function renderNode(value, container) {
if (value === null) {
let s = createElement("span", "json-null", "null");
container.append(s);
} else if (typeof value === "boolean") {
let s = createElement("span", "json-boolean", String(value));
container.append(s);
} else if (typeof value === "number") {
let s = createElement("span", "json-number", String(value));
container.append(s);
} else if (typeof value === "string") {
let s = createElement("span", "json-string", JSON.stringify(value));
container.append(s);
} else if (Array.isArray(value)) {
renderArray(value, container);
} else if (typeof value === "object") {
renderObject(value, container);
}
}
function renderObject(obj, container) {
let keys = Object.keys(obj);
if (keys.length === 0) {
container.append(createElement("span", "bracket", "{}"));
return;
}
let toggle = createElement("span", "toggle");
container.append(toggle);
container.append(createElement("span", "bracket", "{"));
let inner = createElement("div", "collapsible");
container.append(inner);
keys.forEach((key, i) => {
let line = createElement("div", "line");
line.append(createElement("span", "json-key", JSON.stringify(key)));
line.append(document.createTextNode(": "));
renderNode(obj[key], line);
if (i < keys.length - 1) {
line.append(createElement("span", "comma", ","));
}
inner.append(line);
});
container.append(createElement("span", "bracket", "}"));
toggle.onclick = function () {
toggle.classList.toggle("collapsed");
inner.classList.toggle("hidden");
};
}
function renderArray(arr, container) {
if (arr.length === 0) {
container.append(createElement("span", "bracket", "[]"));
return;
}
let toggle = createElement("span", "toggle");
container.append(toggle);
container.append(createElement("span", "bracket", "["));
let inner = createElement("div", "collapsible");
container.append(inner);
arr.forEach((item, i) => {
let line = createElement("div", "line");
renderNode(item, line);
if (i < arr.length - 1) {
line.append(createElement("span", "comma", ","));
}
inner.append(line);
});
container.append(createElement("span", "bracket", "]"));
toggle.onclick = function () {
toggle.classList.toggle("collapsed");
inner.classList.toggle("hidden");
};
}
document.addEventListener("DOMContentLoaded", function () {
const viewer = document.getElementById("viewer");
const prettyButton = document.getElementById("pretty-toggle");
const rawButton = document.getElementById("raw-toggle");
const rawView = document.getElementById("raw-view");
const rawText = rawView.innerText;
let data;
let parseError = null;
try {
data = JSON.parse(rawText);
} catch (e) {
parseError = e;
}
if (parseError) {
let errDiv = createElement(
"div",
"json-error",
"Invalid JSON: " + parseError.message,
);
viewer.append(errDiv);
viewer.append(createElement("pre", null, rawText));
} else {
renderNode(data, viewer);
rawView.textContent = JSON.stringify(data, null, 2);
}
// Toggle buttons
prettyButton.onclick = function () {
viewer.classList.add("active");
rawView.classList.remove("active");
prettyButton.classList.add("active");
rawButton.classList.remove("active");
};
rawButton.onclick = function () {
rawView.classList.add("active");
viewer.classList.remove("active");
rawButton.classList.add("active");
prettyButton.classList.remove("active");
};
});
</script>
</head>
<body>
<div id="toolbar">
<button id="pretty-toggle" class="active">Pretty</button>
<button id="raw-toggle">Raw</button>
</div>
<div id="viewer" class="active"></div>
<pre id="raw-view">