@zjy4fun/json-open 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pages.yml +36 -0
- package/README.md +55 -103
- package/README.zh-CN.md +55 -100
- package/favicon.svg +4 -0
- package/index.html +1068 -0
- package/package.json +5 -4
package/index.html
ADDED
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>JSON Formatter</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
8
|
+
<style>
|
|
9
|
+
/* ===== Theme Variables ===== */
|
|
10
|
+
:root {
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
--bg-body: #0f172a;
|
|
13
|
+
--bg-header: #1e293b;
|
|
14
|
+
--bg-input: #0f172a;
|
|
15
|
+
--bg-button: #1e293b;
|
|
16
|
+
--bg-button-hover: #334155;
|
|
17
|
+
--bg-primary: #1d4ed8;
|
|
18
|
+
--bg-primary-hover: #2563eb;
|
|
19
|
+
--border: #334155;
|
|
20
|
+
--border-button: #475569;
|
|
21
|
+
--border-search-focus: #58a6ff;
|
|
22
|
+
--text: #e2e8f0;
|
|
23
|
+
--text-secondary: #94a3b8;
|
|
24
|
+
--text-muted: #64748b;
|
|
25
|
+
--text-label: #cbd5e1;
|
|
26
|
+
--text-placeholder: #475569;
|
|
27
|
+
--resizer: #334155;
|
|
28
|
+
--resizer-active: #58a6ff;
|
|
29
|
+
--tree-line: #334155;
|
|
30
|
+
--arrow: #94a3b8;
|
|
31
|
+
--key: #93c5fd;
|
|
32
|
+
--colon: #94a3b8;
|
|
33
|
+
--string: #86efac;
|
|
34
|
+
--number: #fcd34d;
|
|
35
|
+
--boolean: #f9a8d4;
|
|
36
|
+
--null: #cbd5e1;
|
|
37
|
+
--symbol: #c4b5fd;
|
|
38
|
+
--meta: #64748b;
|
|
39
|
+
--highlight-bg: #f0883e;
|
|
40
|
+
--highlight-text: #0d1117;
|
|
41
|
+
--highlight-current-bg: #58a6ff;
|
|
42
|
+
--highlight-current-text: #fff;
|
|
43
|
+
--error-text: #f87171;
|
|
44
|
+
--error-bg: rgba(248,113,113,0.1);
|
|
45
|
+
--error-border: rgba(248,113,113,0.2);
|
|
46
|
+
--toggle-bg: #334155;
|
|
47
|
+
--toggle-active: #58a6ff;
|
|
48
|
+
--toggle-knob: #e2e8f0;
|
|
49
|
+
--github-link: #94a3b8;
|
|
50
|
+
--github-link-hover: #e2e8f0;
|
|
51
|
+
--theme-btn-color: #94a3b8;
|
|
52
|
+
--theme-btn-hover: #e2e8f0;
|
|
53
|
+
--parsed-bg: rgba(251,191,36,0.06);
|
|
54
|
+
--parsed-border: #f59e0b;
|
|
55
|
+
--parsed-badge: #f59e0b;
|
|
56
|
+
--parsed-badge-bg: rgba(245,158,11,0.15);
|
|
57
|
+
}
|
|
58
|
+
:root.light {
|
|
59
|
+
color-scheme: light;
|
|
60
|
+
--bg-body: #f8fafc;
|
|
61
|
+
--bg-header: #e2e8f0;
|
|
62
|
+
--bg-input: #ffffff;
|
|
63
|
+
--bg-button: #e2e8f0;
|
|
64
|
+
--bg-button-hover: #cbd5e1;
|
|
65
|
+
--bg-primary: #2563eb;
|
|
66
|
+
--bg-primary-hover: #1d4ed8;
|
|
67
|
+
--border: #cbd5e1;
|
|
68
|
+
--border-button: #94a3b8;
|
|
69
|
+
--border-search-focus: #2563eb;
|
|
70
|
+
--text: #1e293b;
|
|
71
|
+
--text-secondary: #475569;
|
|
72
|
+
--text-muted: #64748b;
|
|
73
|
+
--text-label: #334155;
|
|
74
|
+
--text-placeholder: #94a3b8;
|
|
75
|
+
--resizer: #cbd5e1;
|
|
76
|
+
--resizer-active: #2563eb;
|
|
77
|
+
--tree-line: #cbd5e1;
|
|
78
|
+
--arrow: #64748b;
|
|
79
|
+
--key: #1d4ed8;
|
|
80
|
+
--colon: #64748b;
|
|
81
|
+
--string: #16a34a;
|
|
82
|
+
--number: #b45309;
|
|
83
|
+
--boolean: #be185d;
|
|
84
|
+
--null: #64748b;
|
|
85
|
+
--symbol: #7c3aed;
|
|
86
|
+
--meta: #94a3b8;
|
|
87
|
+
--highlight-bg: #fbbf24;
|
|
88
|
+
--highlight-text: #1e293b;
|
|
89
|
+
--highlight-current-bg: #2563eb;
|
|
90
|
+
--highlight-current-text: #fff;
|
|
91
|
+
--error-text: #dc2626;
|
|
92
|
+
--error-bg: rgba(220,38,38,0.08);
|
|
93
|
+
--error-border: rgba(220,38,38,0.2);
|
|
94
|
+
--toggle-bg: #cbd5e1;
|
|
95
|
+
--toggle-active: #2563eb;
|
|
96
|
+
--toggle-knob: #ffffff;
|
|
97
|
+
--github-link: #64748b;
|
|
98
|
+
--github-link-hover: #1e293b;
|
|
99
|
+
--theme-btn-color: #64748b;
|
|
100
|
+
--theme-btn-hover: #1e293b;
|
|
101
|
+
--parsed-bg: rgba(245,158,11,0.07);
|
|
102
|
+
--parsed-border: #d97706;
|
|
103
|
+
--parsed-badge: #92400e;
|
|
104
|
+
--parsed-badge-bg: rgba(217,119,6,0.12);
|
|
105
|
+
}
|
|
106
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
107
|
+
body {
|
|
108
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
109
|
+
background: var(--bg-body);
|
|
110
|
+
color: var(--text);
|
|
111
|
+
height: 100vh;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ===== Header ===== */
|
|
116
|
+
.header {
|
|
117
|
+
height: 48px;
|
|
118
|
+
background: var(--bg-header);
|
|
119
|
+
border-bottom: 1px solid var(--border);
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
padding: 0 20px;
|
|
123
|
+
gap: 16px;
|
|
124
|
+
flex-shrink: 0;
|
|
125
|
+
}
|
|
126
|
+
.header h1 {
|
|
127
|
+
font-size: 15px;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
color: var(--text);
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
}
|
|
132
|
+
.header .actions {
|
|
133
|
+
display: flex;
|
|
134
|
+
gap: 12px;
|
|
135
|
+
margin-left: auto;
|
|
136
|
+
align-items: center;
|
|
137
|
+
}
|
|
138
|
+
.theme-toggle {
|
|
139
|
+
background: none;
|
|
140
|
+
border: none;
|
|
141
|
+
color: var(--theme-btn-color);
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
padding: 4px;
|
|
146
|
+
border-radius: 6px;
|
|
147
|
+
transition: color 0.2s;
|
|
148
|
+
}
|
|
149
|
+
.theme-toggle:hover {
|
|
150
|
+
color: var(--theme-btn-hover);
|
|
151
|
+
}
|
|
152
|
+
.github-link {
|
|
153
|
+
color: var(--github-link);
|
|
154
|
+
transition: color 0.2s;
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
}
|
|
158
|
+
.github-link:hover {
|
|
159
|
+
color: var(--github-link-hover);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ===== Layout ===== */
|
|
163
|
+
.container {
|
|
164
|
+
display: flex;
|
|
165
|
+
height: calc(100vh - 48px);
|
|
166
|
+
}
|
|
167
|
+
.panel {
|
|
168
|
+
flex: 1;
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
min-width: 0;
|
|
172
|
+
}
|
|
173
|
+
.panel-left {
|
|
174
|
+
border-right: 1px solid var(--border);
|
|
175
|
+
}
|
|
176
|
+
.panel-header {
|
|
177
|
+
height: 40px;
|
|
178
|
+
background: var(--bg-header);
|
|
179
|
+
border-bottom: 1px solid var(--border);
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
padding: 0 16px;
|
|
183
|
+
gap: 10px;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
font-size: 13px;
|
|
186
|
+
color: var(--text-secondary);
|
|
187
|
+
}
|
|
188
|
+
.panel-header .label {
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
color: var(--text-label);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ===== Resizer ===== */
|
|
194
|
+
.resizer {
|
|
195
|
+
width: 5px;
|
|
196
|
+
background: var(--resizer);
|
|
197
|
+
cursor: col-resize;
|
|
198
|
+
flex-shrink: 0;
|
|
199
|
+
transition: background 0.15s;
|
|
200
|
+
}
|
|
201
|
+
.resizer:hover, .resizer.active {
|
|
202
|
+
background: var(--resizer-active);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ===== Left Panel: Input ===== */
|
|
206
|
+
.input-area {
|
|
207
|
+
flex: 1;
|
|
208
|
+
resize: none;
|
|
209
|
+
border: none;
|
|
210
|
+
background: var(--bg-input);
|
|
211
|
+
color: var(--text);
|
|
212
|
+
font-family: inherit;
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
line-height: 1.6;
|
|
215
|
+
padding: 16px;
|
|
216
|
+
outline: none;
|
|
217
|
+
tab-size: 2;
|
|
218
|
+
}
|
|
219
|
+
.input-area::placeholder {
|
|
220
|
+
color: var(--text-placeholder);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ===== Right Panel: Viewer ===== */
|
|
224
|
+
.toolbar {
|
|
225
|
+
background: var(--bg-header);
|
|
226
|
+
border-bottom: 1px solid var(--border);
|
|
227
|
+
padding: 8px 16px;
|
|
228
|
+
display: flex;
|
|
229
|
+
gap: 8px;
|
|
230
|
+
align-items: center;
|
|
231
|
+
flex-wrap: wrap;
|
|
232
|
+
flex-shrink: 0;
|
|
233
|
+
}
|
|
234
|
+
button {
|
|
235
|
+
border: 1px solid var(--border-button);
|
|
236
|
+
background: var(--bg-button);
|
|
237
|
+
color: var(--text);
|
|
238
|
+
border-radius: 6px;
|
|
239
|
+
padding: 5px 10px;
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
font-size: 12px;
|
|
242
|
+
font-family: inherit;
|
|
243
|
+
white-space: nowrap;
|
|
244
|
+
}
|
|
245
|
+
button:hover {
|
|
246
|
+
background: var(--bg-button-hover);
|
|
247
|
+
}
|
|
248
|
+
button.primary {
|
|
249
|
+
background: var(--bg-primary);
|
|
250
|
+
border-color: var(--bg-primary-hover);
|
|
251
|
+
color: #fff;
|
|
252
|
+
}
|
|
253
|
+
button.primary:hover {
|
|
254
|
+
background: var(--bg-primary-hover);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ===== Search ===== */
|
|
258
|
+
.search-wrap {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 6px;
|
|
262
|
+
margin-left: auto;
|
|
263
|
+
}
|
|
264
|
+
.search-wrap input {
|
|
265
|
+
border: 1px solid var(--border-button);
|
|
266
|
+
background: var(--bg-input);
|
|
267
|
+
color: var(--text);
|
|
268
|
+
border-radius: 6px;
|
|
269
|
+
padding: 5px 10px;
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
font-family: inherit;
|
|
272
|
+
width: 180px;
|
|
273
|
+
outline: none;
|
|
274
|
+
transition: border-color 0.2s;
|
|
275
|
+
}
|
|
276
|
+
.search-wrap input:focus {
|
|
277
|
+
border-color: var(--border-search-focus);
|
|
278
|
+
}
|
|
279
|
+
.search-wrap input::placeholder {
|
|
280
|
+
color: var(--text-muted);
|
|
281
|
+
}
|
|
282
|
+
.search-count {
|
|
283
|
+
font-size: 11px;
|
|
284
|
+
color: var(--text-muted);
|
|
285
|
+
min-width: 50px;
|
|
286
|
+
}
|
|
287
|
+
.search-nav button {
|
|
288
|
+
padding: 3px 7px;
|
|
289
|
+
font-size: 11px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ===== Toggle ===== */
|
|
293
|
+
.toggle-wrap {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 6px;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
color: var(--text-secondary);
|
|
299
|
+
}
|
|
300
|
+
.toggle-wrap.hidden { display: none; }
|
|
301
|
+
.toggle {
|
|
302
|
+
position: relative;
|
|
303
|
+
width: 36px;
|
|
304
|
+
height: 20px;
|
|
305
|
+
cursor: pointer;
|
|
306
|
+
}
|
|
307
|
+
.toggle input {
|
|
308
|
+
opacity: 0;
|
|
309
|
+
width: 0;
|
|
310
|
+
height: 0;
|
|
311
|
+
}
|
|
312
|
+
.toggle .slider {
|
|
313
|
+
position: absolute;
|
|
314
|
+
inset: 0;
|
|
315
|
+
background: var(--toggle-bg);
|
|
316
|
+
border-radius: 20px;
|
|
317
|
+
transition: background 0.2s;
|
|
318
|
+
}
|
|
319
|
+
.toggle .slider::before {
|
|
320
|
+
content: '';
|
|
321
|
+
position: absolute;
|
|
322
|
+
width: 14px;
|
|
323
|
+
height: 14px;
|
|
324
|
+
left: 3px;
|
|
325
|
+
bottom: 3px;
|
|
326
|
+
background: var(--toggle-knob);
|
|
327
|
+
border-radius: 50%;
|
|
328
|
+
transition: transform 0.2s;
|
|
329
|
+
}
|
|
330
|
+
.toggle input:checked + .slider {
|
|
331
|
+
background: var(--toggle-active);
|
|
332
|
+
}
|
|
333
|
+
.toggle input:checked + .slider::before {
|
|
334
|
+
transform: translateX(16px);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ===== JSON Tree ===== */
|
|
338
|
+
.viewer {
|
|
339
|
+
flex: 1;
|
|
340
|
+
overflow: auto;
|
|
341
|
+
padding: 16px;
|
|
342
|
+
line-height: 1.5;
|
|
343
|
+
font-size: 13px;
|
|
344
|
+
}
|
|
345
|
+
.viewer .empty-state {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-direction: column;
|
|
348
|
+
align-items: center;
|
|
349
|
+
justify-content: center;
|
|
350
|
+
height: 100%;
|
|
351
|
+
color: var(--text-placeholder);
|
|
352
|
+
font-size: 14px;
|
|
353
|
+
gap: 8px;
|
|
354
|
+
}
|
|
355
|
+
.viewer .empty-state .icon {
|
|
356
|
+
font-size: 36px;
|
|
357
|
+
opacity: 0.5;
|
|
358
|
+
}
|
|
359
|
+
.viewer .error-msg {
|
|
360
|
+
color: var(--error-text);
|
|
361
|
+
background: var(--error-bg);
|
|
362
|
+
border: 1px solid var(--error-border);
|
|
363
|
+
border-radius: 8px;
|
|
364
|
+
padding: 12px 16px;
|
|
365
|
+
font-size: 13px;
|
|
366
|
+
}
|
|
367
|
+
details {
|
|
368
|
+
margin-left: 16px;
|
|
369
|
+
}
|
|
370
|
+
summary {
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
list-style: none;
|
|
373
|
+
}
|
|
374
|
+
summary::-webkit-details-marker {
|
|
375
|
+
display: none;
|
|
376
|
+
}
|
|
377
|
+
summary::before {
|
|
378
|
+
content: '\25B8';
|
|
379
|
+
margin-right: 6px;
|
|
380
|
+
color: var(--arrow);
|
|
381
|
+
}
|
|
382
|
+
details[open] > summary::before {
|
|
383
|
+
content: '\25BE';
|
|
384
|
+
}
|
|
385
|
+
ul {
|
|
386
|
+
list-style: none;
|
|
387
|
+
margin: 4px 0 0 12px;
|
|
388
|
+
padding-left: 12px;
|
|
389
|
+
border-left: 1px dashed var(--tree-line);
|
|
390
|
+
}
|
|
391
|
+
.key { color: var(--key); }
|
|
392
|
+
.colon { color: var(--colon); }
|
|
393
|
+
.string { color: var(--string); }
|
|
394
|
+
.number { color: var(--number); }
|
|
395
|
+
.boolean { color: var(--boolean); }
|
|
396
|
+
.null { color: var(--null); }
|
|
397
|
+
.symbol { color: var(--symbol); }
|
|
398
|
+
.meta { color: var(--meta); }
|
|
399
|
+
|
|
400
|
+
/* ===== Parsed JSON string highlight ===== */
|
|
401
|
+
.parsed-json {
|
|
402
|
+
background: var(--parsed-bg);
|
|
403
|
+
border-left: 2px solid var(--parsed-border);
|
|
404
|
+
border-radius: 4px;
|
|
405
|
+
padding: 2px 0 2px 8px;
|
|
406
|
+
margin: 2px 0;
|
|
407
|
+
}
|
|
408
|
+
.parsed-json > summary::after {
|
|
409
|
+
content: 'parsed';
|
|
410
|
+
font-size: 10px;
|
|
411
|
+
color: var(--parsed-badge);
|
|
412
|
+
background: var(--parsed-badge-bg);
|
|
413
|
+
border-radius: 3px;
|
|
414
|
+
padding: 1px 5px;
|
|
415
|
+
margin-left: 6px;
|
|
416
|
+
vertical-align: middle;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* ===== Search highlight ===== */
|
|
420
|
+
mark.highlight {
|
|
421
|
+
background: var(--highlight-bg);
|
|
422
|
+
color: var(--highlight-text);
|
|
423
|
+
border-radius: 2px;
|
|
424
|
+
padding: 0 1px;
|
|
425
|
+
}
|
|
426
|
+
mark.highlight.current {
|
|
427
|
+
background: var(--highlight-current-bg);
|
|
428
|
+
color: var(--highlight-current-text);
|
|
429
|
+
box-shadow: 0 0 0 2px rgba(88,166,255,0.4);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* ===== Status Bar ===== */
|
|
433
|
+
.status-bar {
|
|
434
|
+
height: 24px;
|
|
435
|
+
background: var(--bg-header);
|
|
436
|
+
border-top: 1px solid var(--border);
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
padding: 0 16px;
|
|
440
|
+
font-size: 11px;
|
|
441
|
+
color: var(--text-muted);
|
|
442
|
+
gap: 16px;
|
|
443
|
+
flex-shrink: 0;
|
|
444
|
+
}
|
|
445
|
+
</style>
|
|
446
|
+
</head>
|
|
447
|
+
<body>
|
|
448
|
+
<div class="header">
|
|
449
|
+
<h1>{ } JSON Formatter</h1>
|
|
450
|
+
<div class="actions">
|
|
451
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">
|
|
452
|
+
<svg id="icon-moon" height="20" width="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
453
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
454
|
+
</svg>
|
|
455
|
+
<svg id="icon-sun" height="20" width="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
|
|
456
|
+
<circle cx="12" cy="12" r="5"/>
|
|
457
|
+
<line x1="12" y1="1" x2="12" y2="3"/>
|
|
458
|
+
<line x1="12" y1="21" x2="12" y2="23"/>
|
|
459
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
|
460
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
461
|
+
<line x1="1" y1="12" x2="3" y2="12"/>
|
|
462
|
+
<line x1="21" y1="12" x2="23" y2="12"/>
|
|
463
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
|
464
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
465
|
+
</svg>
|
|
466
|
+
</button>
|
|
467
|
+
<a href="https://github.com/zjy4fun/json-open" target="_blank" rel="noopener noreferrer" class="github-link" title="View on GitHub">
|
|
468
|
+
<svg height="24" width="24" viewBox="0 0 16 16" fill="currentColor">
|
|
469
|
+
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
|
470
|
+
</svg>
|
|
471
|
+
</a>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<div class="container">
|
|
476
|
+
<!-- Left Panel: Input -->
|
|
477
|
+
<div class="panel panel-left" id="panel-left">
|
|
478
|
+
<div class="panel-header">
|
|
479
|
+
<span class="label">Input</span>
|
|
480
|
+
<button id="btn-format" class="primary">Format</button>
|
|
481
|
+
<button id="btn-copy">Copy</button>
|
|
482
|
+
<button id="btn-sample">Sample</button>
|
|
483
|
+
<button id="btn-clear">Clear</button>
|
|
484
|
+
<button id="btn-share">Share</button>
|
|
485
|
+
</div>
|
|
486
|
+
<textarea class="input-area" id="json-input" placeholder="Paste or type JSON here..." spellcheck="false"></textarea>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Resizer -->
|
|
490
|
+
<div class="resizer" id="resizer"></div>
|
|
491
|
+
|
|
492
|
+
<!-- Right Panel: Viewer -->
|
|
493
|
+
<div class="panel panel-right" id="panel-right">
|
|
494
|
+
<div class="toolbar">
|
|
495
|
+
<button id="expand-all">Expand all</button>
|
|
496
|
+
<button id="collapse-all">Collapse all</button>
|
|
497
|
+
<div class="toggle-wrap hidden" id="toggle-wrap" title="Parse embedded JSON strings inside values">
|
|
498
|
+
<span>Parse JSON strings</span>
|
|
499
|
+
<label class="toggle">
|
|
500
|
+
<input type="checkbox" id="deep-parse-toggle" />
|
|
501
|
+
<span class="slider"></span>
|
|
502
|
+
</label>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="search-wrap">
|
|
505
|
+
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" />
|
|
506
|
+
<span class="search-count" id="search-count"></span>
|
|
507
|
+
<span class="search-nav">
|
|
508
|
+
<button id="search-prev" title="Previous (Shift+Enter)">▲</button>
|
|
509
|
+
<button id="search-next" title="Next (Enter)">▼</button>
|
|
510
|
+
</span>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="viewer" id="viewer">
|
|
514
|
+
<div class="empty-state">
|
|
515
|
+
<div class="icon">{ }</div>
|
|
516
|
+
<div>Paste JSON on the left and click <b>Format</b></div>
|
|
517
|
+
<div style="font-size:12px">or press Ctrl+Enter</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div class="status-bar">
|
|
524
|
+
<span id="status-info">Ready</span>
|
|
525
|
+
<span id="status-size" style="margin-left:auto"></span>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<script>
|
|
529
|
+
// ===== Utility =====
|
|
530
|
+
function escapeHtml(str) {
|
|
531
|
+
return str
|
|
532
|
+
.replace(/&/g, '&')
|
|
533
|
+
.replace(/</g, '<')
|
|
534
|
+
.replace(/>/g, '>')
|
|
535
|
+
.replace(/"/g, '"')
|
|
536
|
+
.replace(/'/g, ''')
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function valueToHtml(value, key) {
|
|
540
|
+
const keyHtml = key === undefined
|
|
541
|
+
? ''
|
|
542
|
+
: '<span class="key">' + escapeHtml(String(key)) + '</span><span class="colon">: </span>'
|
|
543
|
+
|
|
544
|
+
if (value === null) {
|
|
545
|
+
return '<div class="line">' + keyHtml + '<span class="null">null</span></div>'
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const type = typeof value
|
|
549
|
+
|
|
550
|
+
if (type === 'string') {
|
|
551
|
+
return '<div class="line">' + keyHtml + '<span class="string">"' + escapeHtml(value) + '"</span></div>'
|
|
552
|
+
}
|
|
553
|
+
if (type === 'number') {
|
|
554
|
+
return '<div class="line">' + keyHtml + '<span class="number">' + value + '</span></div>'
|
|
555
|
+
}
|
|
556
|
+
if (type === 'boolean') {
|
|
557
|
+
return '<div class="line">' + keyHtml + '<span class="boolean">' + value + '</span></div>'
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (Array.isArray(value)) {
|
|
561
|
+
var isParsedArr = !!value[PARSED_FROM_STRING]
|
|
562
|
+
const children = value
|
|
563
|
+
.map(function (item, idx) { return '<li>' + valueToHtml(item, idx) + '</li>' })
|
|
564
|
+
.join('')
|
|
565
|
+
return '<details open class="' + (isParsedArr ? 'parsed-json' : '') + '">' +
|
|
566
|
+
'<summary>' + keyHtml + '<span class="symbol">[ ]</span> <span class="meta">(' + value.length + ' items)</span></summary>' +
|
|
567
|
+
'<ul>' + children + '</ul>' +
|
|
568
|
+
'</details>'
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (type === 'object') {
|
|
572
|
+
var isParsedObj = !!value[PARSED_FROM_STRING]
|
|
573
|
+
const entries = Object.entries(value)
|
|
574
|
+
const children = entries
|
|
575
|
+
.map(function (e) { return '<li>' + valueToHtml(e[1], e[0]) + '</li>' })
|
|
576
|
+
.join('')
|
|
577
|
+
return '<details open class="' + (isParsedObj ? 'parsed-json' : '') + '">' +
|
|
578
|
+
'<summary>' + keyHtml + '<span class="symbol">{ }</span> <span class="meta">(' + entries.length + ' keys)</span></summary>' +
|
|
579
|
+
'<ul>' + children + '</ul>' +
|
|
580
|
+
'</details>'
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return '<div class="line">' + keyHtml + '<span>' + escapeHtml(String(value)) + '</span></div>'
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Sentinel to mark values that were parsed from JSON strings
|
|
587
|
+
var PARSED_FROM_STRING = Symbol('parsed-from-string')
|
|
588
|
+
|
|
589
|
+
function deepParseJsonStrings(obj) {
|
|
590
|
+
if (obj === null || obj === undefined) return obj
|
|
591
|
+
if (Array.isArray(obj)) return obj.map(deepParseJsonStrings)
|
|
592
|
+
if (typeof obj === 'object') {
|
|
593
|
+
// Don't re-process already-marked objects
|
|
594
|
+
var result = {}
|
|
595
|
+
for (var k in obj) {
|
|
596
|
+
if (obj.hasOwnProperty(k)) result[k] = deepParseJsonStrings(obj[k])
|
|
597
|
+
}
|
|
598
|
+
if (obj[PARSED_FROM_STRING]) result[PARSED_FROM_STRING] = true
|
|
599
|
+
return result
|
|
600
|
+
}
|
|
601
|
+
if (typeof obj === 'string') {
|
|
602
|
+
var trimmed = obj.trim()
|
|
603
|
+
if ((trimmed[0] === '{' && trimmed[trimmed.length - 1] === '}') ||
|
|
604
|
+
(trimmed[0] === '[' && trimmed[trimmed.length - 1] === ']')) {
|
|
605
|
+
try {
|
|
606
|
+
var parsed = deepParseJsonStrings(JSON.parse(trimmed))
|
|
607
|
+
// Mark the parsed result so we can highlight it
|
|
608
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
609
|
+
parsed[PARSED_FROM_STRING] = true
|
|
610
|
+
}
|
|
611
|
+
return parsed
|
|
612
|
+
} catch (e) {
|
|
613
|
+
return obj
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return obj
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function smartParse(input) {
|
|
621
|
+
// Try direct parse
|
|
622
|
+
try {
|
|
623
|
+
return JSON.parse(input)
|
|
624
|
+
} catch (e) { /* continue */ }
|
|
625
|
+
|
|
626
|
+
// Try handling serialized JSON strings (double-escaped)
|
|
627
|
+
try {
|
|
628
|
+
var cleaned = input.trim()
|
|
629
|
+
if ((cleaned[0] === '"' && cleaned[cleaned.length - 1] === '"') ||
|
|
630
|
+
(cleaned[0] === "'" && cleaned[cleaned.length - 1] === "'")) {
|
|
631
|
+
cleaned = cleaned.slice(1, -1)
|
|
632
|
+
}
|
|
633
|
+
cleaned = cleaned
|
|
634
|
+
.replace(/\\"/g, '"')
|
|
635
|
+
.replace(/\\\\/g, '\\')
|
|
636
|
+
.replace(/\\n/g, '\n')
|
|
637
|
+
.replace(/\\t/g, '\t')
|
|
638
|
+
.replace(/\\r/g, '\r')
|
|
639
|
+
return JSON.parse(cleaned)
|
|
640
|
+
} catch (e) { /* continue */ }
|
|
641
|
+
|
|
642
|
+
// Try recursive parse (multi-level serialization)
|
|
643
|
+
try {
|
|
644
|
+
var result = input.trim()
|
|
645
|
+
var depth = 0
|
|
646
|
+
while (typeof result === 'string' && depth < 5) {
|
|
647
|
+
result = JSON.parse(result)
|
|
648
|
+
depth++
|
|
649
|
+
}
|
|
650
|
+
if (typeof result === 'object' && result !== null) return result
|
|
651
|
+
} catch (e) { /* continue */ }
|
|
652
|
+
|
|
653
|
+
throw new Error('Invalid JSON')
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatBytes(bytes) {
|
|
657
|
+
if (bytes < 1024) return bytes + ' B'
|
|
658
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
659
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ===== Elements =====
|
|
663
|
+
var jsonInput = document.getElementById('json-input')
|
|
664
|
+
var viewer = document.getElementById('viewer')
|
|
665
|
+
var btnFormat = document.getElementById('btn-format')
|
|
666
|
+
var btnCopy = document.getElementById('btn-copy')
|
|
667
|
+
var btnSample = document.getElementById('btn-sample')
|
|
668
|
+
var btnClear = document.getElementById('btn-clear')
|
|
669
|
+
var btnShare = document.getElementById('btn-share')
|
|
670
|
+
var btnExpandAll = document.getElementById('expand-all')
|
|
671
|
+
var btnCollapseAll = document.getElementById('collapse-all')
|
|
672
|
+
var searchInput = document.getElementById('search-input')
|
|
673
|
+
var searchCount = document.getElementById('search-count')
|
|
674
|
+
var searchPrev = document.getElementById('search-prev')
|
|
675
|
+
var searchNext = document.getElementById('search-next')
|
|
676
|
+
var toggleWrap = document.getElementById('toggle-wrap')
|
|
677
|
+
var deepParseToggle = document.getElementById('deep-parse-toggle')
|
|
678
|
+
var statusInfo = document.getElementById('status-info')
|
|
679
|
+
var statusSize = document.getElementById('status-size')
|
|
680
|
+
var panelLeft = document.getElementById('panel-left')
|
|
681
|
+
var resizer = document.getElementById('resizer')
|
|
682
|
+
|
|
683
|
+
var currentParsed = null
|
|
684
|
+
var currentDeepParsed = null
|
|
685
|
+
|
|
686
|
+
// ===== Core: Format =====
|
|
687
|
+
function doFormat() {
|
|
688
|
+
var input = jsonInput.value.trim()
|
|
689
|
+
if (!input) {
|
|
690
|
+
viewer.innerHTML = '<div class="empty-state"><div class="icon">{ }</div><div>Paste JSON on the left and click <b>Format</b></div><div style="font-size:12px">or press Ctrl+Enter</div></div>'
|
|
691
|
+
statusInfo.textContent = 'Ready'
|
|
692
|
+
statusSize.textContent = ''
|
|
693
|
+
toggleWrap.classList.add('hidden')
|
|
694
|
+
clearSearch()
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
currentParsed = smartParse(input)
|
|
700
|
+
currentDeepParsed = deepParseJsonStrings(currentParsed)
|
|
701
|
+
var hasDiff = JSON.stringify(currentParsed) !== JSON.stringify(currentDeepParsed)
|
|
702
|
+
|
|
703
|
+
if (hasDiff) {
|
|
704
|
+
toggleWrap.classList.remove('hidden')
|
|
705
|
+
} else {
|
|
706
|
+
toggleWrap.classList.add('hidden')
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
renderView()
|
|
710
|
+
|
|
711
|
+
// Also auto-format the input
|
|
712
|
+
jsonInput.value = JSON.stringify(currentParsed, null, 2)
|
|
713
|
+
|
|
714
|
+
var keys = 0
|
|
715
|
+
try { keys = JSON.stringify(currentParsed).length } catch (e) {}
|
|
716
|
+
statusInfo.textContent = 'Formatted successfully'
|
|
717
|
+
statusSize.textContent = formatBytes(new Blob([input]).size)
|
|
718
|
+
} catch (e) {
|
|
719
|
+
viewer.innerHTML = '<div class="error-msg"><b>Parse Error</b><br>' + escapeHtml(e.message) + '</div>'
|
|
720
|
+
statusInfo.textContent = 'Parse error'
|
|
721
|
+
statusSize.textContent = ''
|
|
722
|
+
toggleWrap.classList.add('hidden')
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function renderView() {
|
|
727
|
+
var showDeep = deepParseToggle.checked
|
|
728
|
+
var obj = showDeep ? currentDeepParsed : currentParsed
|
|
729
|
+
if (!obj) return
|
|
730
|
+
viewer.innerHTML = valueToHtml(obj)
|
|
731
|
+
clearSearch()
|
|
732
|
+
if (searchInput.value.trim()) doSearch()
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ===== Buttons =====
|
|
736
|
+
btnFormat.addEventListener('click', doFormat)
|
|
737
|
+
|
|
738
|
+
btnCopy.addEventListener('click', function () {
|
|
739
|
+
var text = jsonInput.value.trim()
|
|
740
|
+
if (!text) return
|
|
741
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
742
|
+
var orig = btnCopy.textContent
|
|
743
|
+
btnCopy.textContent = 'Copied!'
|
|
744
|
+
setTimeout(function () { btnCopy.textContent = orig }, 1500)
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
btnSample.addEventListener('click', function () {
|
|
749
|
+
jsonInput.value = JSON.stringify({
|
|
750
|
+
name: 'json-open',
|
|
751
|
+
version: '0.2.1',
|
|
752
|
+
description: 'Open JSON in browser with interactive tree view',
|
|
753
|
+
features: ['collapsible tree', 'search & highlight', 'dark theme', 'auto-parse nested JSON'],
|
|
754
|
+
author: { name: 'zjy4fun', github: 'https://github.com/zjy4fun' },
|
|
755
|
+
stats: { stars: 42, forks: 7, issues: 3 },
|
|
756
|
+
tags: ['json', 'viewer', 'cli', 'browser'],
|
|
757
|
+
nested_json: '{"deeply": {"embedded": "json string"}}',
|
|
758
|
+
active: true,
|
|
759
|
+
license: 'MIT'
|
|
760
|
+
}, null, 2)
|
|
761
|
+
doFormat()
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
btnClear.addEventListener('click', function () {
|
|
765
|
+
jsonInput.value = ''
|
|
766
|
+
viewer.innerHTML = '<div class="empty-state"><div class="icon">{ }</div><div>Paste JSON on the left and click <b>Format</b></div><div style="font-size:12px">or press Ctrl+Enter</div></div>'
|
|
767
|
+
statusInfo.textContent = 'Ready'
|
|
768
|
+
statusSize.textContent = ''
|
|
769
|
+
toggleWrap.classList.add('hidden')
|
|
770
|
+
currentParsed = null
|
|
771
|
+
currentDeepParsed = null
|
|
772
|
+
clearSearch()
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
// ===== Share =====
|
|
776
|
+
btnShare.addEventListener('click', function () {
|
|
777
|
+
var input = jsonInput.value.trim()
|
|
778
|
+
if (!input) return
|
|
779
|
+
try {
|
|
780
|
+
// Validate JSON first
|
|
781
|
+
JSON.parse(input)
|
|
782
|
+
} catch (e) {
|
|
783
|
+
// Try smart parse
|
|
784
|
+
try {
|
|
785
|
+
var parsed = smartParse(input)
|
|
786
|
+
input = JSON.stringify(parsed)
|
|
787
|
+
} catch (e2) {
|
|
788
|
+
statusInfo.textContent = 'Cannot share: invalid JSON'
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
var encoded = encodeURIComponent(input)
|
|
793
|
+
var url = location.origin + location.pathname + '#json=' + encoded
|
|
794
|
+
navigator.clipboard.writeText(url).then(function () {
|
|
795
|
+
var orig = btnShare.textContent
|
|
796
|
+
btnShare.textContent = 'Link copied!'
|
|
797
|
+
statusInfo.textContent = 'Share link copied (' + formatBytes(url.length) + ')'
|
|
798
|
+
setTimeout(function () { btnShare.textContent = orig }, 2000)
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
// Ctrl+Enter to format
|
|
803
|
+
jsonInput.addEventListener('keydown', function (e) {
|
|
804
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
805
|
+
e.preventDefault()
|
|
806
|
+
doFormat()
|
|
807
|
+
}
|
|
808
|
+
// Tab support
|
|
809
|
+
if (e.key === 'Tab') {
|
|
810
|
+
e.preventDefault()
|
|
811
|
+
var start = this.selectionStart
|
|
812
|
+
var end = this.selectionEnd
|
|
813
|
+
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end)
|
|
814
|
+
this.selectionStart = this.selectionEnd = start + 2
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
// ===== Expand / Collapse =====
|
|
819
|
+
function getDetails() {
|
|
820
|
+
return Array.from(viewer.querySelectorAll('details'))
|
|
821
|
+
}
|
|
822
|
+
btnExpandAll.addEventListener('click', function () {
|
|
823
|
+
getDetails().forEach(function (d) { d.open = true })
|
|
824
|
+
})
|
|
825
|
+
btnCollapseAll.addEventListener('click', function () {
|
|
826
|
+
getDetails().forEach(function (d) { d.open = false })
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
// ===== Deep Parse Toggle =====
|
|
830
|
+
deepParseToggle.addEventListener('change', function () {
|
|
831
|
+
renderView()
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
// ===== Search =====
|
|
835
|
+
var highlights = []
|
|
836
|
+
var currentIdx = -1
|
|
837
|
+
|
|
838
|
+
function clearSearch() {
|
|
839
|
+
viewer.querySelectorAll('mark.highlight').forEach(function (mark) {
|
|
840
|
+
var parent = mark.parentNode
|
|
841
|
+
parent.replaceChild(document.createTextNode(mark.textContent), mark)
|
|
842
|
+
parent.normalize()
|
|
843
|
+
})
|
|
844
|
+
highlights = []
|
|
845
|
+
currentIdx = -1
|
|
846
|
+
searchCount.textContent = ''
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function doSearch() {
|
|
850
|
+
clearSearch()
|
|
851
|
+
var query = searchInput.value.trim()
|
|
852
|
+
if (!query) return
|
|
853
|
+
|
|
854
|
+
var regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
855
|
+
|
|
856
|
+
var walker = document.createTreeWalker(viewer, NodeFilter.SHOW_TEXT, null)
|
|
857
|
+
var textNodes = []
|
|
858
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode)
|
|
859
|
+
|
|
860
|
+
textNodes.forEach(function (node) {
|
|
861
|
+
var text = node.textContent
|
|
862
|
+
if (!regex.test(text)) return
|
|
863
|
+
regex.lastIndex = 0
|
|
864
|
+
|
|
865
|
+
var frag = document.createDocumentFragment()
|
|
866
|
+
var lastIdx = 0
|
|
867
|
+
var match
|
|
868
|
+
while ((match = regex.exec(text)) !== null) {
|
|
869
|
+
if (match.index > lastIdx) {
|
|
870
|
+
frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)))
|
|
871
|
+
}
|
|
872
|
+
var mark = document.createElement('mark')
|
|
873
|
+
mark.className = 'highlight'
|
|
874
|
+
mark.textContent = match[0]
|
|
875
|
+
frag.appendChild(mark)
|
|
876
|
+
lastIdx = regex.lastIndex
|
|
877
|
+
}
|
|
878
|
+
if (lastIdx < text.length) {
|
|
879
|
+
frag.appendChild(document.createTextNode(text.slice(lastIdx)))
|
|
880
|
+
}
|
|
881
|
+
node.parentNode.replaceChild(frag, node)
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
highlights = Array.from(viewer.querySelectorAll('mark.highlight'))
|
|
885
|
+
if (highlights.length > 0) {
|
|
886
|
+
highlights.forEach(function (h) {
|
|
887
|
+
var el = h.closest('details')
|
|
888
|
+
while (el) {
|
|
889
|
+
el.open = true
|
|
890
|
+
el = el.parentElement ? el.parentElement.closest('details') : null
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
currentIdx = 0
|
|
894
|
+
requestAnimationFrame(scrollToCurrent)
|
|
895
|
+
}
|
|
896
|
+
updateCount()
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function updateCount() {
|
|
900
|
+
if (highlights.length === 0 && searchInput.value.trim()) {
|
|
901
|
+
searchCount.textContent = 'No match'
|
|
902
|
+
} else if (highlights.length > 0) {
|
|
903
|
+
searchCount.textContent = (currentIdx + 1) + ' / ' + highlights.length
|
|
904
|
+
} else {
|
|
905
|
+
searchCount.textContent = ''
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function scrollToCurrent() {
|
|
910
|
+
highlights.forEach(function (h, i) {
|
|
911
|
+
h.classList.toggle('current', i === currentIdx)
|
|
912
|
+
})
|
|
913
|
+
if (highlights[currentIdx]) {
|
|
914
|
+
var el = highlights[currentIdx].closest('details')
|
|
915
|
+
while (el) {
|
|
916
|
+
el.open = true
|
|
917
|
+
el = el.parentElement ? el.parentElement.closest('details') : null
|
|
918
|
+
}
|
|
919
|
+
highlights[currentIdx].scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
920
|
+
}
|
|
921
|
+
updateCount()
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function goNext() {
|
|
925
|
+
if (!highlights.length) return
|
|
926
|
+
currentIdx = (currentIdx + 1) % highlights.length
|
|
927
|
+
scrollToCurrent()
|
|
928
|
+
}
|
|
929
|
+
function goPrev() {
|
|
930
|
+
if (!highlights.length) return
|
|
931
|
+
currentIdx = (currentIdx - 1 + highlights.length) % highlights.length
|
|
932
|
+
scrollToCurrent()
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
var debounceTimer
|
|
936
|
+
searchInput.addEventListener('input', function () {
|
|
937
|
+
clearTimeout(debounceTimer)
|
|
938
|
+
debounceTimer = setTimeout(doSearch, 200)
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
searchInput.addEventListener('keydown', function (e) {
|
|
942
|
+
if (e.key === 'Enter') {
|
|
943
|
+
e.preventDefault()
|
|
944
|
+
e.shiftKey ? goPrev() : goNext()
|
|
945
|
+
}
|
|
946
|
+
if (e.key === 'Escape') {
|
|
947
|
+
searchInput.value = ''
|
|
948
|
+
clearSearch()
|
|
949
|
+
searchInput.blur()
|
|
950
|
+
}
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
searchNext.addEventListener('click', goNext)
|
|
954
|
+
searchPrev.addEventListener('click', goPrev)
|
|
955
|
+
|
|
956
|
+
// Ctrl+F / Cmd+F -> focus search
|
|
957
|
+
document.addEventListener('keydown', function (e) {
|
|
958
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
|
959
|
+
e.preventDefault()
|
|
960
|
+
searchInput.focus()
|
|
961
|
+
searchInput.select()
|
|
962
|
+
}
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
// ===== Resizer =====
|
|
966
|
+
;(function () {
|
|
967
|
+
var startX, startWidth
|
|
968
|
+
resizer.addEventListener('mousedown', function (e) {
|
|
969
|
+
startX = e.clientX
|
|
970
|
+
startWidth = panelLeft.getBoundingClientRect().width
|
|
971
|
+
resizer.classList.add('active')
|
|
972
|
+
document.addEventListener('mousemove', onMouseMove)
|
|
973
|
+
document.addEventListener('mouseup', onMouseUp)
|
|
974
|
+
e.preventDefault()
|
|
975
|
+
})
|
|
976
|
+
function onMouseMove(e) {
|
|
977
|
+
var newWidth = startWidth + (e.clientX - startX)
|
|
978
|
+
var minW = 200
|
|
979
|
+
var maxW = window.innerWidth - 300
|
|
980
|
+
newWidth = Math.max(minW, Math.min(maxW, newWidth))
|
|
981
|
+
panelLeft.style.flex = 'none'
|
|
982
|
+
panelLeft.style.width = newWidth + 'px'
|
|
983
|
+
}
|
|
984
|
+
function onMouseUp() {
|
|
985
|
+
resizer.classList.remove('active')
|
|
986
|
+
document.removeEventListener('mousemove', onMouseMove)
|
|
987
|
+
document.removeEventListener('mouseup', onMouseUp)
|
|
988
|
+
}
|
|
989
|
+
})()
|
|
990
|
+
|
|
991
|
+
// ===== Drag & Drop =====
|
|
992
|
+
jsonInput.addEventListener('dragover', function (e) {
|
|
993
|
+
e.preventDefault()
|
|
994
|
+
e.dataTransfer.dropEffect = 'copy'
|
|
995
|
+
})
|
|
996
|
+
jsonInput.addEventListener('drop', function (e) {
|
|
997
|
+
e.preventDefault()
|
|
998
|
+
var file = e.dataTransfer.files[0]
|
|
999
|
+
if (file) {
|
|
1000
|
+
var reader = new FileReader()
|
|
1001
|
+
reader.onload = function (ev) {
|
|
1002
|
+
jsonInput.value = ev.target.result
|
|
1003
|
+
doFormat()
|
|
1004
|
+
}
|
|
1005
|
+
reader.readAsText(file)
|
|
1006
|
+
}
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
// ===== Paste auto-format =====
|
|
1010
|
+
jsonInput.addEventListener('paste', function () {
|
|
1011
|
+
setTimeout(function () {
|
|
1012
|
+
if (jsonInput.value.trim()) doFormat()
|
|
1013
|
+
}, 0)
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
// ===== Load from URL hash =====
|
|
1017
|
+
;(function () {
|
|
1018
|
+
function loadFromHash() {
|
|
1019
|
+
var hash = location.hash
|
|
1020
|
+
if (!hash || !hash.startsWith('#json=')) return
|
|
1021
|
+
var encoded = hash.slice(6) // remove '#json='
|
|
1022
|
+
try {
|
|
1023
|
+
var json = decodeURIComponent(encoded)
|
|
1024
|
+
if (json) {
|
|
1025
|
+
jsonInput.value = json
|
|
1026
|
+
doFormat()
|
|
1027
|
+
}
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
statusInfo.textContent = 'Failed to load shared JSON'
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
loadFromHash()
|
|
1033
|
+
window.addEventListener('hashchange', loadFromHash)
|
|
1034
|
+
})()
|
|
1035
|
+
|
|
1036
|
+
// ===== Theme Toggle =====
|
|
1037
|
+
;(function () {
|
|
1038
|
+
var themeBtn = document.getElementById('theme-toggle')
|
|
1039
|
+
var iconMoon = document.getElementById('icon-moon')
|
|
1040
|
+
var iconSun = document.getElementById('icon-sun')
|
|
1041
|
+
var root = document.documentElement
|
|
1042
|
+
|
|
1043
|
+
function applyTheme(theme) {
|
|
1044
|
+
if (theme === 'light') {
|
|
1045
|
+
root.classList.add('light')
|
|
1046
|
+
iconMoon.style.display = 'none'
|
|
1047
|
+
iconSun.style.display = ''
|
|
1048
|
+
} else {
|
|
1049
|
+
root.classList.remove('light')
|
|
1050
|
+
iconMoon.style.display = ''
|
|
1051
|
+
iconSun.style.display = 'none'
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Load saved preference, default to dark
|
|
1056
|
+
var saved = localStorage.getItem('json-open-theme') || 'dark'
|
|
1057
|
+
applyTheme(saved)
|
|
1058
|
+
|
|
1059
|
+
themeBtn.addEventListener('click', function () {
|
|
1060
|
+
var isLight = root.classList.contains('light')
|
|
1061
|
+
var newTheme = isLight ? 'dark' : 'light'
|
|
1062
|
+
localStorage.setItem('json-open-theme', newTheme)
|
|
1063
|
+
applyTheme(newTheme)
|
|
1064
|
+
})
|
|
1065
|
+
})()
|
|
1066
|
+
</script>
|
|
1067
|
+
</body>
|
|
1068
|
+
</html>
|