@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/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)">&#9650;</button>
509
+ <button id="search-next" title="Next (Enter)">&#9660;</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, '&amp;')
533
+ .replace(/</g, '&lt;')
534
+ .replace(/>/g, '&gt;')
535
+ .replace(/"/g, '&quot;')
536
+ .replace(/'/g, '&#39;')
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>