forked from
me.webbeef.org/browser.html
Rewild Your Web
1<html>
2 <head>
3<style>
4 body {
5 font-family: monospace;
6 margin: 0;
7 padding: 0;
8 background: #fff;
9 color: #333;
10 }
11
12 #json-raw {
13 display: none;
14 }
15
16 #viewer {
17 padding: 0.5em 1em;
18 line-height: 1.5;
19 }
20
21 #toolbar {
22 display: flex;
23 gap: 1em;
24 padding: 0.5em;
25 align-items: center;
26 background: #f5f5f5;
27 border-bottom: 1px solid #ddd;
28 }
29
30 #toolbar button {
31 min-width: 5em;
32 }
33
34 #toolbar button.active {
35 background: #ddd;
36 font-weight: bold;
37 }
38
39 #raw-view {
40 display: none;
41 padding: 0.5em 1em;
42 white-space: pre-wrap;
43 word-break: break-all;
44 }
45
46 #smart-view {
47 display: none;
48 padding: 0.5em 1em;
49 }
50
51 .json-error {
52 padding: 0.5em 1em;
53 color: #c00;
54 font-weight: bold;
55 }
56
57 /* Syntax highlighting for Json data types */
58 .json-key {
59 color: #881391;
60 }
61
62 .json-string {
63 color: #1a1aa6;
64 }
65
66 .json-number {
67 color: #1c00cf;
68 }
69
70 .json-boolean {
71 color: #0d22aa;
72 }
73
74 .json-null {
75 color: #808080;
76 }
77
78 /* Collapsible tree */
79 .toggle {
80 cursor: pointer;
81 user-select: none;
82 }
83
84 .toggle::before {
85 content: "\25BC";
86 display: inline-block;
87 width: 1em;
88 transition: transform 0.1s;
89 }
90
91 .toggle.collapsed::before {
92 transform: rotate(-90deg);
93 }
94
95 .collapsible {
96 margin-left: 1.5em;
97 }
98
99 .collapsible.hidden {
100 display: none;
101 }
102
103 .bracket {
104 color: #333;
105 }
106
107 .comma {
108 color: #333;
109 }
110
111 .line {
112 padding-left: 0;
113 }
114</style>
115<script>
116 // Shortcut to create an element with an optional class and text content.
117 function createElement(name, classes = null, textContent = null) {
118 let node = document.createElement(name);
119 if (classes) {
120 node.className = classes;
121 }
122 if (textContent) {
123 node.textContent = textContent;
124 }
125 return node;
126 }
127
128 document.addEventListener("DOMContentLoaded", function () {
129 let rawEl = document.getElementById("json-raw");
130 if (!rawEl) {
131 return;
132 }
133 let rawText = rawEl.textContent;
134
135 let data;
136 let parseError = null;
137 try {
138 data = JSON.parse(rawText);
139 } catch (e) {
140 parseError = e;
141 }
142
143 // Build the page structure
144 document.body.innerHTML = "";
145
146 // Toolbar
147 let toolbar = createElement("div");
148 toolbar.id = "toolbar";
149 let smartBtn = null;
150 let prettyBtn = createElement("button", "active", "Pretty");
151 let rawBtn = createElement("button", null, "Raw");
152
153 // Smart view container (may or may not be used)
154 let smartView = createElement("div");
155 smartView.id = "smart-view";
156
157 // Pretty view
158 let viewer = createElement("div");
159 viewer.id = "viewer";
160
161 // Raw view
162 let rawView = createElement("pre");
163 rawView.id = "raw-view";
164
165 // Check for AT Protocol smart view
166 let smartViewerPromise = null;
167 if (!parseError && location.protocol === "at:" && data && data.value) {
168 let segments = location.pathname.split("/").filter(Boolean);
169 console.log("[json-viewer] AT protocol detected, segments:", segments);
170 if (segments.length >= 1) {
171 let collection = segments[0];
172 let tagName = collection.replaceAll(".", "-");
173 let viewerFile = collection.replaceAll(".", "_");
174 let viewerUrl = "beaver://atproto/viewers/" + viewerFile + ".js";
175 console.log("[json-viewer] Trying smart viewer:", viewerUrl, "tag:", tagName);
176
177 smartViewerPromise = import(viewerUrl)
178 .then(() => {
179 console.log("[json-viewer] Smart viewer loaded successfully");
180 // The module registers its custom element via customElements.define().
181 let el = document.createElement(tagName);
182 el.data = data;
183 smartView.append(el);
184 return true;
185 })
186 .catch((err) => {
187 console.log("[json-viewer] Smart viewer failed to load:", err);
188 return false;
189 });
190 }
191 } else {
192 console.log("[json-viewer] No AT protocol detected. protocol:", location.protocol, "has value:", !!(data && data.value));
193 }
194
195 function buildToolbar(hasSmartView) {
196 if (hasSmartView) {
197 smartBtn = createElement("button", "active", "Smart");
198 toolbar.append(smartBtn);
199 // Demote pretty button to inactive
200 prettyBtn.className = "";
201 }
202 toolbar.append(prettyBtn);
203 toolbar.append(rawBtn);
204 document.body.append(toolbar);
205
206 document.body.append(smartView);
207 document.body.append(viewer);
208 document.body.append(rawView);
209
210 if (hasSmartView) {
211 smartView.style.display = "block";
212 viewer.style.display = "none";
213 }
214
215 // Wire up toggle buttons
216 function activateView(activeBtn, showEl) {
217 if (smartBtn) smartBtn.className = "";
218 prettyBtn.className = "";
219 rawBtn.className = "";
220 activeBtn.className = "active";
221
222 smartView.style.display = "none";
223 viewer.style.display = "none";
224 rawView.style.display = "none";
225 showEl.style.display = "block";
226 }
227
228 if (smartBtn) {
229 smartBtn.onclick = () => activateView(smartBtn, smartView);
230 }
231 prettyBtn.onclick = () => activateView(prettyBtn, viewer);
232 rawBtn.onclick = () => activateView(rawBtn, rawView);
233 }
234
235 // Populate pretty and raw views
236 if (parseError) {
237 let errDiv = createElement(
238 "div",
239 "json-error",
240 "Invalid JSON: " + parseError.message,
241 );
242 viewer.append(errDiv);
243 let pre = createElement("pre", null, rawText);
244 viewer.append(pre);
245 rawView.textContent = rawText;
246 buildToolbar(false);
247 } else {
248 renderNode(data, viewer);
249 rawView.textContent = JSON.stringify(data, null, 2);
250
251 if (smartViewerPromise) {
252 smartViewerPromise.then((ok) => buildToolbar(ok));
253 } else {
254 buildToolbar(false);
255 }
256 }
257
258 function renderNode(value, container) {
259 if (value === null) {
260 let s = createElement("span", "json-null", "null");
261 container.append(s);
262 } else if (typeof value === "boolean") {
263 let s = createElement("span", "json-boolean", String(value));
264 container.append(s);
265 } else if (typeof value === "number") {
266 let s = createElement("span", "json-number", String(value));
267 container.append(s);
268 } else if (typeof value === "string") {
269 let s = createElement("span", "json-string", JSON.stringify(value));
270 container.append(s);
271 } else if (Array.isArray(value)) {
272 renderArray(value, container);
273 } else if (typeof value === "object") {
274 renderObject(value, container);
275 }
276 }
277
278 function renderObject(obj, container) {
279 let keys = Object.keys(obj);
280 if (keys.length === 0) {
281 container.append(createElement("span", "bracket", "{}"));
282 return;
283 }
284
285 let toggle = createElement("span", "toggle");
286 container.append(toggle);
287
288 container.append(createElement("span", "bracket", "{"));
289
290 let inner = createElement("div", "collapsible");
291 container.append(inner);
292
293 keys.forEach((key, i) => {
294 let line = createElement("div", "line");
295 line.append(createElement("span", "json-key", JSON.stringify(key)));
296 line.append(document.createTextNode(": "));
297 renderNode(obj[key], line);
298 if (i < keys.length - 1) {
299 line.append(createElement("span", "comma", ","));
300 }
301 inner.append(line);
302 });
303
304 container.append(createElement("span", "bracket", "}"));
305
306 toggle.onclick = function () {
307 toggle.classList.toggle("collapsed");
308 inner.classList.toggle("hidden");
309 };
310 }
311
312 function renderArray(arr, container) {
313 if (arr.length === 0) {
314 container.append(createElement("span", "bracket", "[]"));
315 return;
316 }
317
318 let toggle = createElement("span", "toggle");
319 container.append(toggle);
320
321 container.append(createElement("span", "bracket", "["));
322
323 let inner = createElement("div", "collapsible");
324 container.append(inner);
325
326 arr.forEach((item, i) => {
327 let line = createElement("div", "line");
328 renderNode(item, line);
329 if (i < arr.length - 1) {
330 line.append(createElement("span", "comma", ","));
331 }
332 inner.append(line);
333 });
334
335 container.append(createElement("span", "bracket", "]"));
336
337 toggle.onclick = function () {
338 toggle.classList.toggle("collapsed");
339 inner.classList.toggle("hidden");
340 };
341 }
342 });
343</script>
344</head>
345 <body>
346 <pre id="json-raw">