@zjy4fun/json-open 0.1.9 → 0.2.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.
Files changed (2) hide show
  1. package/bin/json.js +203 -0
  2. package/package.json +1 -1
package/bin/json.js CHANGED
@@ -224,12 +224,71 @@ function toHtml(jsonObj, deepParsedObj) {
224
224
  .null { color: #cbd5e1; }
225
225
  .symbol { color: #c4b5fd; }
226
226
  .meta { color: #64748b; }
227
+ /* 搜索框 */
228
+ .search-wrap {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 6px;
232
+ }
233
+ .search-wrap input {
234
+ border: 1px solid #475569;
235
+ background: #1e293b;
236
+ color: #e2e8f0;
237
+ border-radius: 8px;
238
+ padding: 7px 12px;
239
+ font-size: 13px;
240
+ font-family: inherit;
241
+ width: 200px;
242
+ outline: none;
243
+ transition: border-color 0.2s;
244
+ }
245
+ .search-wrap input:focus {
246
+ border-color: #58a6ff;
247
+ }
248
+ .search-wrap input::placeholder {
249
+ color: #64748b;
250
+ }
251
+ .search-count {
252
+ font-size: 12px;
253
+ color: #64748b;
254
+ min-width: 60px;
255
+ }
256
+ .search-nav button {
257
+ padding: 4px 8px;
258
+ font-size: 12px;
259
+ border-radius: 6px;
260
+ }
261
+ /* 搜索高亮 */
262
+ mark.highlight {
263
+ background: #f0883e;
264
+ color: #0d1117;
265
+ border-radius: 2px;
266
+ padding: 0 1px;
267
+ }
268
+ mark.highlight.current {
269
+ background: #58a6ff;
270
+ color: #fff;
271
+ box-shadow: 0 0 0 2px rgba(88,166,255,0.4);
272
+ }
273
+ /* 搜索时隐藏不匹配的行 */
274
+ .search-active .line.hidden-by-search,
275
+ .search-active li.hidden-by-search {
276
+ display: none;
277
+ }
227
278
  </style>
228
279
  </head>
229
280
  <body>
230
281
  <div class=\"toolbar\">
231
282
  <button id=\"expand-all\">Expand all</button>
232
283
  <button id=\"collapse-all\">Collapse all</button>
284
+ <div class=\"search-wrap\">
285
+ <input type=\"text\" id=\"search-input\" placeholder=\"Search...\" autocomplete=\"off\" />
286
+ <span class=\"search-count\" id=\"search-count\"></span>
287
+ <span class=\"search-nav\">
288
+ <button id=\"search-prev\" title=\"Previous (Shift+Enter)\">▲</button>
289
+ <button id=\"search-next\" title=\"Next (Enter)\">▼</button>
290
+ </span>
291
+ </div>
233
292
  <div class=\"toggle-wrap${hasDiff ? '' : ' hidden'}\" title=\"Parse embedded JSON strings inside values\">
234
293
  <span>Parse JSON strings</span>
235
294
  <label class=\"toggle\">
@@ -257,8 +316,152 @@ function toHtml(jsonObj, deepParsedObj) {
257
316
  rawView.style.display = ''
258
317
  parsedView.style.display = 'none'
259
318
  }
319
+ // 切换视图后重新搜索
320
+ if (searchInput.value.trim()) doSearch()
321
+ })
322
+ }
323
+
324
+ // ===== 搜索功能 =====
325
+ const searchInput = document.getElementById('search-input')
326
+ const searchCount = document.getElementById('search-count')
327
+ const searchPrev = document.getElementById('search-prev')
328
+ const searchNext = document.getElementById('search-next')
329
+ let highlights = []
330
+ let currentIdx = -1
331
+
332
+ function getActiveView() {
333
+ return parsedView.style.display === 'none' ? rawView : parsedView
334
+ }
335
+
336
+ function clearSearch() {
337
+ // 移除所有高亮
338
+ document.querySelectorAll('mark.highlight').forEach(mark => {
339
+ const parent = mark.parentNode
340
+ parent.replaceChild(document.createTextNode(mark.textContent), mark)
341
+ parent.normalize()
260
342
  })
343
+ highlights = []
344
+ currentIdx = -1
345
+ searchCount.textContent = ''
346
+ document.body.classList.remove('search-active')
261
347
  }
348
+
349
+ function doSearch() {
350
+ clearSearch()
351
+ const query = searchInput.value.trim()
352
+ if (!query) return
353
+
354
+ const view = getActiveView()
355
+ const regex = new RegExp(query.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')
356
+
357
+ // 遍历所有文本节点进行高亮
358
+ const walker = document.createTreeWalker(view, NodeFilter.SHOW_TEXT, null)
359
+ const textNodes = []
360
+ while (walker.nextNode()) textNodes.push(walker.currentNode)
361
+
362
+ textNodes.forEach(node => {
363
+ const text = node.textContent
364
+ if (!regex.test(text)) return
365
+ regex.lastIndex = 0
366
+
367
+ const frag = document.createDocumentFragment()
368
+ let lastIdx = 0
369
+ let match
370
+ while ((match = regex.exec(text)) !== null) {
371
+ if (match.index > lastIdx) {
372
+ frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)))
373
+ }
374
+ const mark = document.createElement('mark')
375
+ mark.className = 'highlight'
376
+ mark.textContent = match[0]
377
+ frag.appendChild(mark)
378
+ lastIdx = regex.lastIndex
379
+ }
380
+ if (lastIdx < text.length) {
381
+ frag.appendChild(document.createTextNode(text.slice(lastIdx)))
382
+ }
383
+ node.parentNode.replaceChild(frag, node)
384
+ })
385
+
386
+ highlights = Array.from(view.querySelectorAll('mark.highlight'))
387
+ if (highlights.length > 0) {
388
+ // 展开所有包含匹配的 <details>
389
+ highlights.forEach(h => {
390
+ let el = h.parentElement
391
+ while (el && el !== view) {
392
+ if (el.tagName === 'DETAILS') el.open = true
393
+ el = el.parentElement
394
+ }
395
+ })
396
+ currentIdx = 0
397
+ scrollToCurrent()
398
+ }
399
+ updateCount()
400
+ }
401
+
402
+ function updateCount() {
403
+ if (highlights.length === 0 && searchInput.value.trim()) {
404
+ searchCount.textContent = 'No match'
405
+ } else if (highlights.length > 0) {
406
+ searchCount.textContent = (currentIdx + 1) + ' / ' + highlights.length
407
+ } else {
408
+ searchCount.textContent = ''
409
+ }
410
+ }
411
+
412
+ function scrollToCurrent() {
413
+ highlights.forEach((h, i) => {
414
+ h.classList.toggle('current', i === currentIdx)
415
+ })
416
+ if (highlights[currentIdx]) {
417
+ highlights[currentIdx].scrollIntoView({ behavior: 'smooth', block: 'center' })
418
+ }
419
+ updateCount()
420
+ }
421
+
422
+ function goNext() {
423
+ if (highlights.length === 0) return
424
+ currentIdx = (currentIdx + 1) % highlights.length
425
+ scrollToCurrent()
426
+ }
427
+
428
+ function goPrev() {
429
+ if (highlights.length === 0) return
430
+ currentIdx = (currentIdx - 1 + highlights.length) % highlights.length
431
+ scrollToCurrent()
432
+ }
433
+
434
+ // 输入时实时搜索(防抖 200ms)
435
+ let debounceTimer
436
+ searchInput.addEventListener('input', () => {
437
+ clearTimeout(debounceTimer)
438
+ debounceTimer = setTimeout(doSearch, 200)
439
+ })
440
+
441
+ // Enter = 下一个,Shift+Enter = 上一个
442
+ searchInput.addEventListener('keydown', (e) => {
443
+ if (e.key === 'Enter') {
444
+ e.preventDefault()
445
+ e.shiftKey ? goPrev() : goNext()
446
+ }
447
+ if (e.key === 'Escape') {
448
+ searchInput.value = ''
449
+ clearSearch()
450
+ searchInput.blur()
451
+ }
452
+ })
453
+
454
+ searchNext.addEventListener('click', goNext)
455
+ searchPrev.addEventListener('click', goPrev)
456
+
457
+ // Ctrl+F / Cmd+F 聚焦搜索框
458
+ document.addEventListener('keydown', (e) => {
459
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
460
+ e.preventDefault()
461
+ searchInput.focus()
462
+ searchInput.select()
463
+ }
464
+ })
262
465
  </script>
263
466
  </body>
264
467
  </html>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjy4fun/json-open",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Open JSON (stdin or inline text) in a browser with collapsible tree view",
5
5
  "type": "module",
6
6
  "bin": {