@zjy4fun/json-open 0.2.1 → 0.3.0

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/index.html ADDED
@@ -0,0 +1,1007 @@
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-sample">Sample</button>
482
+ <button id="btn-clear">Clear</button>
483
+ </div>
484
+ <textarea class="input-area" id="json-input" placeholder="Paste or type JSON here..." spellcheck="false"></textarea>
485
+ </div>
486
+
487
+ <!-- Resizer -->
488
+ <div class="resizer" id="resizer"></div>
489
+
490
+ <!-- Right Panel: Viewer -->
491
+ <div class="panel panel-right" id="panel-right">
492
+ <div class="toolbar">
493
+ <button id="expand-all">Expand all</button>
494
+ <button id="collapse-all">Collapse all</button>
495
+ <div class="toggle-wrap hidden" id="toggle-wrap" title="Parse embedded JSON strings inside values">
496
+ <span>Parse JSON strings</span>
497
+ <label class="toggle">
498
+ <input type="checkbox" id="deep-parse-toggle" />
499
+ <span class="slider"></span>
500
+ </label>
501
+ </div>
502
+ <div class="search-wrap">
503
+ <input type="text" id="search-input" placeholder="Search..." autocomplete="off" />
504
+ <span class="search-count" id="search-count"></span>
505
+ <span class="search-nav">
506
+ <button id="search-prev" title="Previous (Shift+Enter)">&#9650;</button>
507
+ <button id="search-next" title="Next (Enter)">&#9660;</button>
508
+ </span>
509
+ </div>
510
+ </div>
511
+ <div class="viewer" id="viewer">
512
+ <div class="empty-state">
513
+ <div class="icon">{ }</div>
514
+ <div>Paste JSON on the left and click <b>Format</b></div>
515
+ <div style="font-size:12px">or press Ctrl+Enter</div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+
521
+ <div class="status-bar">
522
+ <span id="status-info">Ready</span>
523
+ <span id="status-size" style="margin-left:auto"></span>
524
+ </div>
525
+
526
+ <script>
527
+ // ===== Utility =====
528
+ function escapeHtml(str) {
529
+ return str
530
+ .replace(/&/g, '&amp;')
531
+ .replace(/</g, '&lt;')
532
+ .replace(/>/g, '&gt;')
533
+ .replace(/"/g, '&quot;')
534
+ .replace(/'/g, '&#39;')
535
+ }
536
+
537
+ function valueToHtml(value, key) {
538
+ const keyHtml = key === undefined
539
+ ? ''
540
+ : '<span class="key">' + escapeHtml(String(key)) + '</span><span class="colon">: </span>'
541
+
542
+ if (value === null) {
543
+ return '<div class="line">' + keyHtml + '<span class="null">null</span></div>'
544
+ }
545
+
546
+ const type = typeof value
547
+
548
+ if (type === 'string') {
549
+ return '<div class="line">' + keyHtml + '<span class="string">"' + escapeHtml(value) + '"</span></div>'
550
+ }
551
+ if (type === 'number') {
552
+ return '<div class="line">' + keyHtml + '<span class="number">' + value + '</span></div>'
553
+ }
554
+ if (type === 'boolean') {
555
+ return '<div class="line">' + keyHtml + '<span class="boolean">' + value + '</span></div>'
556
+ }
557
+
558
+ if (Array.isArray(value)) {
559
+ var isParsedArr = !!value[PARSED_FROM_STRING]
560
+ const children = value
561
+ .map(function (item, idx) { return '<li>' + valueToHtml(item, idx) + '</li>' })
562
+ .join('')
563
+ return '<details open class="' + (isParsedArr ? 'parsed-json' : '') + '">' +
564
+ '<summary>' + keyHtml + '<span class="symbol">[ ]</span> <span class="meta">(' + value.length + ' items)</span></summary>' +
565
+ '<ul>' + children + '</ul>' +
566
+ '</details>'
567
+ }
568
+
569
+ if (type === 'object') {
570
+ var isParsedObj = !!value[PARSED_FROM_STRING]
571
+ const entries = Object.entries(value)
572
+ const children = entries
573
+ .map(function (e) { return '<li>' + valueToHtml(e[1], e[0]) + '</li>' })
574
+ .join('')
575
+ return '<details open class="' + (isParsedObj ? 'parsed-json' : '') + '">' +
576
+ '<summary>' + keyHtml + '<span class="symbol">{ }</span> <span class="meta">(' + entries.length + ' keys)</span></summary>' +
577
+ '<ul>' + children + '</ul>' +
578
+ '</details>'
579
+ }
580
+
581
+ return '<div class="line">' + keyHtml + '<span>' + escapeHtml(String(value)) + '</span></div>'
582
+ }
583
+
584
+ // Sentinel to mark values that were parsed from JSON strings
585
+ var PARSED_FROM_STRING = Symbol('parsed-from-string')
586
+
587
+ function deepParseJsonStrings(obj) {
588
+ if (obj === null || obj === undefined) return obj
589
+ if (Array.isArray(obj)) return obj.map(deepParseJsonStrings)
590
+ if (typeof obj === 'object') {
591
+ // Don't re-process already-marked objects
592
+ var result = {}
593
+ for (var k in obj) {
594
+ if (obj.hasOwnProperty(k)) result[k] = deepParseJsonStrings(obj[k])
595
+ }
596
+ if (obj[PARSED_FROM_STRING]) result[PARSED_FROM_STRING] = true
597
+ return result
598
+ }
599
+ if (typeof obj === 'string') {
600
+ var trimmed = obj.trim()
601
+ if ((trimmed[0] === '{' && trimmed[trimmed.length - 1] === '}') ||
602
+ (trimmed[0] === '[' && trimmed[trimmed.length - 1] === ']')) {
603
+ try {
604
+ var parsed = deepParseJsonStrings(JSON.parse(trimmed))
605
+ // Mark the parsed result so we can highlight it
606
+ if (typeof parsed === 'object' && parsed !== null) {
607
+ parsed[PARSED_FROM_STRING] = true
608
+ }
609
+ return parsed
610
+ } catch (e) {
611
+ return obj
612
+ }
613
+ }
614
+ }
615
+ return obj
616
+ }
617
+
618
+ function smartParse(input) {
619
+ // Try direct parse
620
+ try {
621
+ return JSON.parse(input)
622
+ } catch (e) { /* continue */ }
623
+
624
+ // Try handling serialized JSON strings (double-escaped)
625
+ try {
626
+ var cleaned = input.trim()
627
+ if ((cleaned[0] === '"' && cleaned[cleaned.length - 1] === '"') ||
628
+ (cleaned[0] === "'" && cleaned[cleaned.length - 1] === "'")) {
629
+ cleaned = cleaned.slice(1, -1)
630
+ }
631
+ cleaned = cleaned
632
+ .replace(/\\"/g, '"')
633
+ .replace(/\\\\/g, '\\')
634
+ .replace(/\\n/g, '\n')
635
+ .replace(/\\t/g, '\t')
636
+ .replace(/\\r/g, '\r')
637
+ return JSON.parse(cleaned)
638
+ } catch (e) { /* continue */ }
639
+
640
+ // Try recursive parse (multi-level serialization)
641
+ try {
642
+ var result = input.trim()
643
+ var depth = 0
644
+ while (typeof result === 'string' && depth < 5) {
645
+ result = JSON.parse(result)
646
+ depth++
647
+ }
648
+ if (typeof result === 'object' && result !== null) return result
649
+ } catch (e) { /* continue */ }
650
+
651
+ throw new Error('Invalid JSON')
652
+ }
653
+
654
+ function formatBytes(bytes) {
655
+ if (bytes < 1024) return bytes + ' B'
656
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
657
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
658
+ }
659
+
660
+ // ===== Elements =====
661
+ var jsonInput = document.getElementById('json-input')
662
+ var viewer = document.getElementById('viewer')
663
+ var btnFormat = document.getElementById('btn-format')
664
+ var btnSample = document.getElementById('btn-sample')
665
+ var btnClear = document.getElementById('btn-clear')
666
+ var btnExpandAll = document.getElementById('expand-all')
667
+ var btnCollapseAll = document.getElementById('collapse-all')
668
+ var searchInput = document.getElementById('search-input')
669
+ var searchCount = document.getElementById('search-count')
670
+ var searchPrev = document.getElementById('search-prev')
671
+ var searchNext = document.getElementById('search-next')
672
+ var toggleWrap = document.getElementById('toggle-wrap')
673
+ var deepParseToggle = document.getElementById('deep-parse-toggle')
674
+ var statusInfo = document.getElementById('status-info')
675
+ var statusSize = document.getElementById('status-size')
676
+ var panelLeft = document.getElementById('panel-left')
677
+ var resizer = document.getElementById('resizer')
678
+
679
+ var currentParsed = null
680
+ var currentDeepParsed = null
681
+
682
+ // ===== Core: Format =====
683
+ function doFormat() {
684
+ var input = jsonInput.value.trim()
685
+ if (!input) {
686
+ 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>'
687
+ statusInfo.textContent = 'Ready'
688
+ statusSize.textContent = ''
689
+ toggleWrap.classList.add('hidden')
690
+ clearSearch()
691
+ return
692
+ }
693
+
694
+ try {
695
+ currentParsed = smartParse(input)
696
+ currentDeepParsed = deepParseJsonStrings(currentParsed)
697
+ var hasDiff = JSON.stringify(currentParsed) !== JSON.stringify(currentDeepParsed)
698
+
699
+ if (hasDiff) {
700
+ toggleWrap.classList.remove('hidden')
701
+ } else {
702
+ toggleWrap.classList.add('hidden')
703
+ }
704
+
705
+ renderView()
706
+
707
+ // Also auto-format the input
708
+ jsonInput.value = JSON.stringify(currentParsed, null, 2)
709
+
710
+ var keys = 0
711
+ try { keys = JSON.stringify(currentParsed).length } catch (e) {}
712
+ statusInfo.textContent = 'Formatted successfully'
713
+ statusSize.textContent = formatBytes(new Blob([input]).size)
714
+ } catch (e) {
715
+ viewer.innerHTML = '<div class="error-msg"><b>Parse Error</b><br>' + escapeHtml(e.message) + '</div>'
716
+ statusInfo.textContent = 'Parse error'
717
+ statusSize.textContent = ''
718
+ toggleWrap.classList.add('hidden')
719
+ }
720
+ }
721
+
722
+ function renderView() {
723
+ var showDeep = deepParseToggle.checked
724
+ var obj = showDeep ? currentDeepParsed : currentParsed
725
+ if (!obj) return
726
+ viewer.innerHTML = valueToHtml(obj)
727
+ clearSearch()
728
+ if (searchInput.value.trim()) doSearch()
729
+ }
730
+
731
+ // ===== Buttons =====
732
+ btnFormat.addEventListener('click', doFormat)
733
+
734
+ btnSample.addEventListener('click', function () {
735
+ jsonInput.value = JSON.stringify({
736
+ name: 'json-open',
737
+ version: '0.2.1',
738
+ description: 'Open JSON in browser with interactive tree view',
739
+ features: ['collapsible tree', 'search & highlight', 'dark theme', 'auto-parse nested JSON'],
740
+ author: { name: 'zjy4fun', github: 'https://github.com/zjy4fun' },
741
+ stats: { stars: 42, forks: 7, issues: 3 },
742
+ tags: ['json', 'viewer', 'cli', 'browser'],
743
+ nested_json: '{"deeply": {"embedded": "json string"}}',
744
+ active: true,
745
+ license: 'MIT'
746
+ }, null, 2)
747
+ doFormat()
748
+ })
749
+
750
+ btnClear.addEventListener('click', function () {
751
+ jsonInput.value = ''
752
+ 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>'
753
+ statusInfo.textContent = 'Ready'
754
+ statusSize.textContent = ''
755
+ toggleWrap.classList.add('hidden')
756
+ currentParsed = null
757
+ currentDeepParsed = null
758
+ clearSearch()
759
+ })
760
+
761
+ // Ctrl+Enter to format
762
+ jsonInput.addEventListener('keydown', function (e) {
763
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
764
+ e.preventDefault()
765
+ doFormat()
766
+ }
767
+ // Tab support
768
+ if (e.key === 'Tab') {
769
+ e.preventDefault()
770
+ var start = this.selectionStart
771
+ var end = this.selectionEnd
772
+ this.value = this.value.substring(0, start) + ' ' + this.value.substring(end)
773
+ this.selectionStart = this.selectionEnd = start + 2
774
+ }
775
+ })
776
+
777
+ // ===== Expand / Collapse =====
778
+ function getDetails() {
779
+ return Array.from(viewer.querySelectorAll('details'))
780
+ }
781
+ btnExpandAll.addEventListener('click', function () {
782
+ getDetails().forEach(function (d) { d.open = true })
783
+ })
784
+ btnCollapseAll.addEventListener('click', function () {
785
+ getDetails().forEach(function (d) { d.open = false })
786
+ })
787
+
788
+ // ===== Deep Parse Toggle =====
789
+ deepParseToggle.addEventListener('change', function () {
790
+ renderView()
791
+ })
792
+
793
+ // ===== Search =====
794
+ var highlights = []
795
+ var currentIdx = -1
796
+
797
+ function clearSearch() {
798
+ viewer.querySelectorAll('mark.highlight').forEach(function (mark) {
799
+ var parent = mark.parentNode
800
+ parent.replaceChild(document.createTextNode(mark.textContent), mark)
801
+ parent.normalize()
802
+ })
803
+ highlights = []
804
+ currentIdx = -1
805
+ searchCount.textContent = ''
806
+ }
807
+
808
+ function doSearch() {
809
+ clearSearch()
810
+ var query = searchInput.value.trim()
811
+ if (!query) return
812
+
813
+ var regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
814
+
815
+ var walker = document.createTreeWalker(viewer, NodeFilter.SHOW_TEXT, null)
816
+ var textNodes = []
817
+ while (walker.nextNode()) textNodes.push(walker.currentNode)
818
+
819
+ textNodes.forEach(function (node) {
820
+ var text = node.textContent
821
+ if (!regex.test(text)) return
822
+ regex.lastIndex = 0
823
+
824
+ var frag = document.createDocumentFragment()
825
+ var lastIdx = 0
826
+ var match
827
+ while ((match = regex.exec(text)) !== null) {
828
+ if (match.index > lastIdx) {
829
+ frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)))
830
+ }
831
+ var mark = document.createElement('mark')
832
+ mark.className = 'highlight'
833
+ mark.textContent = match[0]
834
+ frag.appendChild(mark)
835
+ lastIdx = regex.lastIndex
836
+ }
837
+ if (lastIdx < text.length) {
838
+ frag.appendChild(document.createTextNode(text.slice(lastIdx)))
839
+ }
840
+ node.parentNode.replaceChild(frag, node)
841
+ })
842
+
843
+ highlights = Array.from(viewer.querySelectorAll('mark.highlight'))
844
+ if (highlights.length > 0) {
845
+ highlights.forEach(function (h) {
846
+ var el = h.closest('details')
847
+ while (el) {
848
+ el.open = true
849
+ el = el.parentElement ? el.parentElement.closest('details') : null
850
+ }
851
+ })
852
+ currentIdx = 0
853
+ requestAnimationFrame(scrollToCurrent)
854
+ }
855
+ updateCount()
856
+ }
857
+
858
+ function updateCount() {
859
+ if (highlights.length === 0 && searchInput.value.trim()) {
860
+ searchCount.textContent = 'No match'
861
+ } else if (highlights.length > 0) {
862
+ searchCount.textContent = (currentIdx + 1) + ' / ' + highlights.length
863
+ } else {
864
+ searchCount.textContent = ''
865
+ }
866
+ }
867
+
868
+ function scrollToCurrent() {
869
+ highlights.forEach(function (h, i) {
870
+ h.classList.toggle('current', i === currentIdx)
871
+ })
872
+ if (highlights[currentIdx]) {
873
+ var el = highlights[currentIdx].closest('details')
874
+ while (el) {
875
+ el.open = true
876
+ el = el.parentElement ? el.parentElement.closest('details') : null
877
+ }
878
+ highlights[currentIdx].scrollIntoView({ behavior: 'smooth', block: 'center' })
879
+ }
880
+ updateCount()
881
+ }
882
+
883
+ function goNext() {
884
+ if (!highlights.length) return
885
+ currentIdx = (currentIdx + 1) % highlights.length
886
+ scrollToCurrent()
887
+ }
888
+ function goPrev() {
889
+ if (!highlights.length) return
890
+ currentIdx = (currentIdx - 1 + highlights.length) % highlights.length
891
+ scrollToCurrent()
892
+ }
893
+
894
+ var debounceTimer
895
+ searchInput.addEventListener('input', function () {
896
+ clearTimeout(debounceTimer)
897
+ debounceTimer = setTimeout(doSearch, 200)
898
+ })
899
+
900
+ searchInput.addEventListener('keydown', function (e) {
901
+ if (e.key === 'Enter') {
902
+ e.preventDefault()
903
+ e.shiftKey ? goPrev() : goNext()
904
+ }
905
+ if (e.key === 'Escape') {
906
+ searchInput.value = ''
907
+ clearSearch()
908
+ searchInput.blur()
909
+ }
910
+ })
911
+
912
+ searchNext.addEventListener('click', goNext)
913
+ searchPrev.addEventListener('click', goPrev)
914
+
915
+ // Ctrl+F / Cmd+F -> focus search
916
+ document.addEventListener('keydown', function (e) {
917
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
918
+ e.preventDefault()
919
+ searchInput.focus()
920
+ searchInput.select()
921
+ }
922
+ })
923
+
924
+ // ===== Resizer =====
925
+ ;(function () {
926
+ var startX, startWidth
927
+ resizer.addEventListener('mousedown', function (e) {
928
+ startX = e.clientX
929
+ startWidth = panelLeft.getBoundingClientRect().width
930
+ resizer.classList.add('active')
931
+ document.addEventListener('mousemove', onMouseMove)
932
+ document.addEventListener('mouseup', onMouseUp)
933
+ e.preventDefault()
934
+ })
935
+ function onMouseMove(e) {
936
+ var newWidth = startWidth + (e.clientX - startX)
937
+ var minW = 200
938
+ var maxW = window.innerWidth - 300
939
+ newWidth = Math.max(minW, Math.min(maxW, newWidth))
940
+ panelLeft.style.flex = 'none'
941
+ panelLeft.style.width = newWidth + 'px'
942
+ }
943
+ function onMouseUp() {
944
+ resizer.classList.remove('active')
945
+ document.removeEventListener('mousemove', onMouseMove)
946
+ document.removeEventListener('mouseup', onMouseUp)
947
+ }
948
+ })()
949
+
950
+ // ===== Drag & Drop =====
951
+ jsonInput.addEventListener('dragover', function (e) {
952
+ e.preventDefault()
953
+ e.dataTransfer.dropEffect = 'copy'
954
+ })
955
+ jsonInput.addEventListener('drop', function (e) {
956
+ e.preventDefault()
957
+ var file = e.dataTransfer.files[0]
958
+ if (file) {
959
+ var reader = new FileReader()
960
+ reader.onload = function (ev) {
961
+ jsonInput.value = ev.target.result
962
+ doFormat()
963
+ }
964
+ reader.readAsText(file)
965
+ }
966
+ })
967
+
968
+ // ===== Paste auto-format =====
969
+ jsonInput.addEventListener('paste', function () {
970
+ setTimeout(function () {
971
+ if (jsonInput.value.trim()) doFormat()
972
+ }, 0)
973
+ })
974
+
975
+ // ===== Theme Toggle =====
976
+ ;(function () {
977
+ var themeBtn = document.getElementById('theme-toggle')
978
+ var iconMoon = document.getElementById('icon-moon')
979
+ var iconSun = document.getElementById('icon-sun')
980
+ var root = document.documentElement
981
+
982
+ function applyTheme(theme) {
983
+ if (theme === 'light') {
984
+ root.classList.add('light')
985
+ iconMoon.style.display = 'none'
986
+ iconSun.style.display = ''
987
+ } else {
988
+ root.classList.remove('light')
989
+ iconMoon.style.display = ''
990
+ iconSun.style.display = 'none'
991
+ }
992
+ }
993
+
994
+ // Load saved preference, default to dark
995
+ var saved = localStorage.getItem('json-open-theme') || 'dark'
996
+ applyTheme(saved)
997
+
998
+ themeBtn.addEventListener('click', function () {
999
+ var isLight = root.classList.contains('light')
1000
+ var newTheme = isLight ? 'dark' : 'light'
1001
+ localStorage.setItem('json-open-theme', newTheme)
1002
+ applyTheme(newTheme)
1003
+ })
1004
+ })()
1005
+ </script>
1006
+ </body>
1007
+ </html>