1// JSON Format Tool with additional features 2$(document).ready(function() { 3 const i18n = window.FORMAT_I18N || {}; 4 const t = function (key, replacements) { 5 let value = i18n[key] || key; 6 Object.keys(replacements || {}).forEach(function (name) { 7 value = value.replace(':' + name, replacements[name]); 8 }); 9 return value; 10 }; 11 12 function parseJson(content) { 13 try { 14 return JSON.parse(content); 15 } catch (nativeError) { 16 // The bundled parser gives users a useful line hint when the 17 // browser's native SyntaxError does not expose a position. 18 if (window.jsonlint && typeof window.jsonlint.parse === 'function') { 19 try { 20 return window.jsonlint.parse(content); 21 } catch (lintError) { 22 throw lintError; 23 } 24 } 25 throw nativeError; 26 } 27 } 28 29 function formatJsonError(error, source) { 30 const message = error && error.message ? error.message : String(error); 31 const lineMatch = message.match(/(?:on line|line)\s+(\d+)/i); 32 const colMatch = message.match(/(?:column|col)\s*[: ]\s*(\d+)/i); 33 const lineNumber = lineMatch ? parseInt(lineMatch[1], 10) : 0; 34 const columnNumber = colMatch ? parseInt(colMatch[1], 10) : 0; 35 if (!lineNumber) return t('invalidJson', { message: message }); 36 37 const lines = String(source || '').split('\n'); 38 const lineText = lines[lineNumber - 1] || ''; 39 const caret = columnNumber > 0 ? '\n' + new Array(columnNumber).join(' ') + '^' : ''; 40 return t('invalidJson', { message: message }) + '\nLine ' + lineNumber + (columnNumber ? ', column ' + columnNumber : '') + ':\n' + lineText + caret; 41 } 42 43 // JSON.parse hands every number through an IEEE-754 double, so an integer 44 // longer than a double can hold comes back with its tail rewritten, silently. 45 // Those literals are parked behind a token and put back once the text is built. 46 function shieldBigIntegers(text) { 47 const token = '__bignum' + Math.random().toString(36).slice(2, 8) + '_'; 48 if (text.indexOf(token) !== -1) { 49 return shieldBigIntegers(text); 50 } 51 52 const literals = []; 53 const shielded = text.replace( 54 /("(?:[^"\\]|\\[\s\S])*")|(-?(?:0|[1-9]\d*))(?=[\s,}\]:])/g, 55 function (whole, str, num) { 56 if (str) return str; 57 if (String(Number(num)) === num) return num; 58 literals.push(num); 59 return '"' + token + literals.length + '"'; 60 } 61 ); 62 63 return { text: shielded, token: token, literals: literals }; 64 } 65 66 function restoreBigIntegers(out, shield) { 67 if (!shield.literals.length) return out; 68 const literal = function (whole, index) { 69 return shield.literals[Number(index) - 1] || whole; 70 }; 71 // The quoted pass covers plain serialised output; the highlighted view can 72 // render a string value without its quotes, so catch the bare token too. 73 return String(out) 74 .replace(new RegExp('"' + shield.token + '([0-9]+)"', 'g'), literal) 75 .replace(new RegExp(shield.token + '([0-9]+)', 'g'), literal); 76 } 77 78 function syncJsonInputs(content) { 79 compressInput.val(content); 80 jsonToXmlInput.val(content); 81 treeJsonInput.val(content); 82 } 83 // DOM elements 84 const jsonInput = $('#jsonInput'); 85 const jsonOutput = $('#jsonOutput'); 86 const formatJsonBtn = $('#formatJsonBtn'); 87 const validateJsonBtn = $('#validateJsonBtn'); 88 const jsonCopyBtn = $('#jsonCopyBtn'); 89 const formattedJsonCopyBtn = $('#formattedJsonCopyBtn'); 90 const downloadJsonBtn = $('#downloadJsonBtn'); 91 // The highlighted view drops key quotes, so copy/download read the real document from here. 92 const rawOutput = {}; 93 const jsonFileInput = $('#jsonFileInput'); 94 const jsonClearBtn = $('#jsonClearBtn'); 95 const loadJsonSampleBtn = $('#loadJsonSampleBtn'); 96 const errorMessage = $('#errorMessage'); 97 98 // Compress tab elements 99 const compressInput = $('#compressInput'); 100 const compressedOutput = $('#compressedOutput'); 101 const compressJsonBtn = $('#compressJsonBtn'); 102 const compressJsonCopyBtn = $('#compressJsonCopyBtn'); 103 const compressOutputCopyBtn = $('#compressOutputCopyBtn'); 104 const downloadCompressedJsonBtn = $('#downloadCompressedJsonBtn'); 105 106 // XML tab elements 107 const jsonToXmlInput = $('#jsonToXmlInput'); 108 const xmlOutput = $('#xmlOutput'); 109 const jsonToXmlBtn = $('#jsonToXmlBtn'); 110 const jsonToXmlCopyBtn = $('#jsonToXmlCopyBtn'); 111 const xmlOutputCopyBtn = $('#xmlOutputCopyBtn'); 112 const downloadXmlBtn = $('#downloadXmlBtn'); 113 114 // Path search elements 115 const pathSearchInput = $('#pathSearchInput'); 116 const pathSearchBtn = $('#pathSearchBtn'); 117 const clearPathResultsBtn = $('#clearPathResultsBtn'); 118 const pathResults = $('#pathResults'); 119 120 // Diff and repair elements 121 const diffLeftInput = $('#diffLeftInput'); 122 const diffRightInput = $('#diffRightInput'); 123 const compareJsonBtn = $('#compareJsonBtn'); 124 const diffOutput = $('#diffOutput'); 125 const repairInput = $('#repairInput'); 126 const repairOutput = $('#repairOutput'); 127 const repairJsonBtn = $('#repairJsonBtn'); 128 const applyRepairBtn = $('#applyRepairBtn'); 129 const copyRepairBtn = $('#copyRepairBtn'); 130 let lastRepairedJson = ''; 131 132 // Tabs 133 const tabButtons = $('.tab-btn'); 134 const tabContents = $('.tab-content'); 135 136 // Options
137 const indentSize = $('#indentSize'); 138 const showLineNumbers = $('#showLineNumbers'); 139 const quoteKeys = $('#quoteKeys'); 140 const liveFormat = $('#liveFormat'); 141 142 // Tab switching functionality 143 tabButtons.on('click', function() { 144 const tabId = $(this).data('tab'); 145 146 // Update active tab button 147 tabButtons.removeClass('active'); 148 $(this).addClass('active'); 149 150 // Show selected tab content 151 tabContents.hide(); 152 $('#' + tabId + '-tab').show(); 153 }); 154 155 // Format JSON function 156 function formatJson() { 157 const content = jsonInput.val().trim(); 158 159 if (!content) { 160 showError(t('emptyJson')); 161 return; 162 } 163 164 try { 165 // Parse the JSON to validate it first, with any literal a double 166 // cannot hold parked so the round trip does not rewrite its tail. 167 const shield = shieldBigIntegers(content); 168 const parsedJson = parseJson(shield.text); 169 170 // Get options 171 const indentSpaces = parseInt(indentSize.val()); 172 const lineNumbers = showLineNumbers.is(':checked'); 173 const quoteKeysOpt = quoteKeys.is(':checked'); 174 175 // Format the JSON with specified indentation 176 const formatted = restoreBigIntegers(JSON.stringify(parsedJson, null, indentSpaces), shield); 177 rawOutput.jsonOutput = formatted; 178 179 // Use pretty-print-json library to add syntax highlighting 180 const options = { 181 indent: indentSpaces, 182 lineNumbers: lineNumbers, 183 quoteKeys: quoteKeysOpt 184 }; 185 186 const html = restoreBigIntegers(prettyPrintJson.toHtml(parsedJson, options), shield); 187 jsonOutput.html(window.FORMAT_SANITIZE_HTML ? window.FORMAT_SANITIZE_HTML(html) : $('<div/>').text(html).html()); 188 189 hideError(); 190 } catch (e) { 191 showError(formatJsonError(e, content)); 192 } 193 } 194 195 // Compress JSON function 196 function compressJson() { 197 const content = compressInput.val().trim(); 198 199 if (!content) { 200 showError(t('emptyJson')); 201 return; 202 } 203 204 try { 205 // Parse the JSON to validate it first 206 const shield = shieldBigIntegers(content); 207 const parsedJson = parseJson(shield.text); 208 209 // Compress by converting to string without formatting 210 const compressed = restoreBigIntegers(JSON.stringify(parsedJson), shield); 211 212 compressedOutput.text(compressed); 213 214 hideError(); 215 } catch (e) { 216 showError(formatJsonError(e, content)); 217 } 218 } 219 220 // Convert JSON to XML function 221 function jsonToXml() { 222 const content = jsonToXmlInput.val().trim(); 223 224 if (!content) { 225 showError(t('emptyJson')); 226 return; 227 } 228 229 try { 230 // Parse the JSON to validate it first 231 const shield = shieldBigIntegers(content); 232 const parsedJson = parseJson(shield.text); 233 234 // Convert JSON to XML 235 const xmlString = jsonToXmlConverter(parsedJson, 0); 236 237 xmlOutput.html($('<div/>').text(restoreBigIntegers(xmlString, shield)).html()); 238 239 hideError(); 240 } catch (e) { 241 showError(formatJsonError(e, content)); 242 } 243 } 244 245 // Helper function to convert JSON to XML 246 function jsonToXmlConverter(obj, level) { 247 let xml = ''; 248 const indent = ' '.repeat(level); 249 250 if (obj === null || obj === undefined) { 251 return `${indent}<null/>`; 252 } 253 254 if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') { 255 return `${indent}${escapeXml(obj.toString())}`; 256 } 257 258 if (Array.isArray(obj)) { 259 obj.forEach(item => { 260 xml += `${indent}<item>${jsonToXmlConverter(item, level + 1)}</item>\n`; 261 }); 262 return xml; 263 } 264 265 if (typeof obj === 'object') { 266 for (const key in obj) { 267 if (obj.hasOwnProperty(key)) { 268 const value = obj[key]; 269 xml += `${indent}<${escapeXml(key)}>`; 270 271 if (value !== null && typeof value === 'object') { 272 xml += `\n${jsonToXmlConverter(value, level + 1)}${indent}</${escapeXml(key)}>\n`; 273 } else { 274 xml += `${jsonToXmlConverter(value, 0)}</${escapeXml(key)}>\n`; 275 } 276 } 277 } 278 return xml; 279 } 280 281 return xml; 282 } 283 284 // Helper function to escape XML special characters 285 function escapeXml(unsafe) { 286 if (typeof unsafe !== 'string') { 287 unsafe = String(unsafe); 288 } 289 return unsafe.replace(/[<>&'"]/g, function (c) { 290 switch (c) { 291 case '<': return '<'; 292 case '>': return '>'; 293 case '&': return '&'; 294 case '\'': return '''; 295 case '"': return '"'; 296 default: return c; 297 } 298 }); 299 } 300 301 // JSON Path Search function 302 function searchJsonPath() { 303 const pathExpression = pathSearchInput.val().trim(); 304 const content = jsonInput.val().trim(); // Search in the main input 305 306 pathResults.hide().empty(); 307 308 if (!content) { 309 showError(t('pathNoInput')); 310 return; 311 } 312 313 if (!pathExpression) { 314 showError(t('pathNoQuery')); 315 return; 316 } 317 318 try { 319 const shield = shieldBigIntegers(content); 320 const jsonData = parseJson(shield.text); 321 const nodes = evaluateJsonPath(jsonData, pathExpression); 322 323 pathResults.show(); 324 if (!nodes.length) { 325 appendPathResult(t('noMatches')); 326 } else {
327 nodes.forEach(function (node) { 328 appendPathResult(restoreBigIntegers(node.path + ' = ' + stringifyResult(node.value), shield)); 329 }); 330 } 331 332 hideError(); 333 } catch (e) { 334 showError(formatJsonError(e, content)); 335 } 336 } 337 338 // Each step is a key, an index or `*`, and a wildcard fans the match list out 339 // over every child, so `users[*].name` returns one node per user. Anything a 340 // step cannot read simply yields no matches. 341 function evaluateJsonPath(obj, path) { 342 let rest = path; 343 if (rest.startsWith('$')) { 344 rest = rest.substring(1); 345 } 346 347 const parts = parsePathSteps(rest); 348 let nodes = [{ path: '$', value: obj }]; 349 350 parts.forEach(function (part) { 351 const matched = []; 352 nodes.forEach(function (node) { 353 const value = node.value; 354 if (value === null || typeof value !== 'object') return; 355 const keys = part === '*' ? Object.keys(value) : (Object.prototype.hasOwnProperty.call(value, part) ? [part] : []); 356 keys.forEach(function (key) { 357 matched.push({ path: node.path + childSuffix(value, key), value: value[key] }); 358 }); 359 }); 360 nodes = matched; 361 }); 362 363 return nodes; 364 } 365 366 // `.key`, a leading bare `key`, `[0]`, `[*]` and `['quoted key']` are the 367 // steps accepted. Quoting exists because the result list prints an odd key in 368 // that form, and a quoted key may contain a dot without being split in two. 369 function parsePathSteps(rest) { 370 const pattern = /\[(\d+|\*)\]|\[(['"])((?:[^\\]|\\.)*?)\2\]|\.([^.\[\]]+)|^([^.\[\]]+)/g; 371 const steps = []; 372 let match; 373 while ((match = pattern.exec(rest))) { 374 if (match[1] !== undefined) steps.push(match[1]); 375 else if (match[3] !== undefined) steps.push(match[3].replace(/\\(.)/g, '$1')); 376 else if (match[4] !== undefined) steps.push(match[4]); 377 else if (match[5] !== undefined) steps.push(match[5]); 378 } 379 return steps; 380 } 381 382 function childSuffix(value, key) { 383 if (Array.isArray(value)) return '[' + key + ']'; 384 if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return '.' + key; 385 return "['" + key.replace(/'/g, "\\'") + "']"; 386 } 387 388 function stringifyResult(result) { 389 return typeof result === 'object' ? JSON.stringify(result) : String(result); 390 } 391 392 function appendPathResult(text) { 393 $('<div/>', { class: 'path-result-item', text: text }).appendTo(pathResults); 394 } 395 396 // Validate JSON function 397 function validateJson() { 398 const content = jsonInput.val().trim(); 399 400 if (!content) { 401 showError(t('emptyJson')); 402 return; 403 } 404 405 try { 406 parseJson(content); 407 layer.msg(t('validJson'), { icon: 1 }); 408 hideError(); 409 } catch (e) { 410 showError(formatJsonError(e, content)); 411 } 412 } 413 414 // Copy and download must not read the highlighted view: it drops key quotes and can carry line numbers. 415 function outputText(element) { 416 const id = element.attr('id'); 417 return rawOutput[id] !== undefined ? rawOutput[id] : element.text(); 418 } 419 420 // Copy to clipboard function 421 function copyToClipboard(element, name) { 422 let content; 423 if (element.attr('id') === 'jsonInput' || element.attr('id') === 'compressInput' || element.attr('id') === 'jsonToXmlInput') { 424 content = element.val(); 425 } else { 426 content = outputText(element); 427 } 428 429 if (!content) { 430 layer.msg(t('nothingToCopy', { name: name })); 431 return; 432 } 433 434 navigator.clipboard.writeText(content).then(() => { 435 layer.msg(t('copied', { name: name })); 436 }).catch(err => { 437 layer.msg('Failed to copy: ' + err); 438 }); 439 } 440 441 // Download JSON function 442 function downloadJson(content, filename) { 443 if (!content) { 444 layer.msg(t('nothingToCopy', { name: 'content' })); 445 return; 446 } 447 448 const blob = new Blob([content], { type: 'application/json;charset=utf-8;' }); 449 const url = URL.createObjectURL(blob); 450 const link = document.createElement('a'); 451 link.href = url; 452 link.download = filename; 453 link.style.display = 'none'; 454 document.body.appendChild(link); 455 link.click(); 456 document.body.removeChild(link); 457 URL.revokeObjectURL(url); 458 layer.msg(t('downloaded')); 459 } 460 461 // Download XML function 462 function downloadXml(content) { 463 if (!content) { 464 layer.msg(t('nothingToCopy', { name: 'XML' })); 465 return; 466 } 467 468 const blob = new Blob([content], { type: 'application/xml;charset=utf-8;' }); 469 const url = URL.createObjectURL(blob); 470 const link = document.createElement('a'); 471 link.href = url; 472 link.download = 'converted-xml.xml'; 473 link.style.display = 'none'; 474 document.body.appendChild(link); 475 link.click(); 476 document.body.removeChild(link); 477 URL.revokeObjectURL(url); 478 layer.msg(t('downloaded')); 479 } 480 481 // File handling function for JSON 482 function handleJsonFile(file) { 483 const maxSize = window.TOOL_MAX_SIZE || (1024 * 1024); 484 485 if (file.size > maxSize) { 486 showError((window.FormatTool && FormatTool.maxSizeError()) || 'File size exceeds limit'); 487 return; 488 } 489 490 if (!file.name.endsWith('.json') && !file.type.includes('json')) { 491 showError('Please select a valid JSON file'); 492 return; 493 } 494 495 const reader = new FileReader(); 496 497 reader.onload = function(e) { 498 try { 499 const content = e.target.result; 500 jsonInput.val(content); 501 syncJsonInputs(content); 502 503 // Auto-format after loading 504 formatJson(); 505 hideError(); 506 } catch (err) { 507 showError('Error processing file: ' + err.message); 508 } 509 }; 510 511 reader.onerror = function() { 512 showError('Error reading file'); 513 }; 514 515 reader.readAsText(file); 516 } 517 518 // Clear editor function 519 function clearEditor() { 520 jsonInput.val(''); 521 jsonOutput.text(t('formatEmpty')); 522 rawOutput.jsonOutput = ''; 523 syncJsonInputs(''); 524 compressedOutput.text(t('compressEmpty')); 525 xmlOutput.text(t('xmlEmpty')); 526 diffLeftInput.val(''); 527 diffRightInput.val(''); 528 diffOutput.text(t('jsonDiffEmpty')); 529 repairInput.val(''); 530 repairOutput.text(t('jsonRepairEmpty')); 531 lastRepairedJson = ''; 532 hideError(); 533 } 534 535 function loadSample() { 536 const sample = i18n.sample || '{"name":"Jane","active":true}'; 537 jsonInput.val(JSON.stringify(JSON.parse(sample), null, 2)); 538 syncJsonInputs(jsonInput.val()); 539 formatJson(); 540 } 541 542 // One pass over the document with the string state carried along, so nothing 543 // inside a quoted value is ever rewritten. Only repairs that cannot change 544 // the data are applied: a BOM, a trailing comma, a comment, a single-quoted 545 // string and an unquoted key all describe the same document as valid JSON. 546 function safeRepairText(source) { 547 const text = String(source || '').replace(/^\uFEFF/, ''); 548 let repaired = ''; 549 let inString = false;
550 let quote = '"'; 551 let escaped = false; 552 553 for (let i = 0; i < text.length; i++) { 554 const character = text[i]; 555 556 if (inString) { 557 repaired += character; 558 if (escaped) { 559 escaped = false; 560 } else if (character === '\\') { 561 escaped = true; 562 } else if (character === quote) { 563 inString = false; 564 } 565 continue; 566 } 567 568 if (character === '"') { 569 inString = true; 570 quote = '"'; 571 repaired += character; 572 continue; 573 } 574 575 if (character === "'") { 576 const converted = convertQuoted(text, i, "'"); 577 if (converted) { 578 repaired += converted.text; 579 i = converted.end; 580 continue; 581 } 582 inString = true; 583 quote = "'"; 584 repaired += character; 585 continue; 586 } 587 588 if (character === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) { 589 const end = skipComment(text, i); 590 if (end === null) { repaired += character; continue; } 591 i = end - 1; 592 continue; 593 } 594 595 if (/[A-Za-z_$]/.test(character)) { 596 const converted = convertBareWord(text, i); 597 if (converted) { 598 repaired += converted.text; 599 i = converted.end - 1; 600 continue; 601 } 602 } 603 604 if (character === ',') { 605 let next = i + 1; 606 while (next < text.length && /\s/.test(text[next])) next++; 607 if (text[next] === '}' || text[next] === ']') continue; 608 } 609 610 repaired += character; 611 } 612 return repaired; 613 } 614 615 // Read a 'single quoted' run and re-emit it as a JSON string. An unterminated 616 // run returns null so the caller leaves the input untouched and the parser, 617 // not this function, decides what the error message says. 618 function convertQuoted(text, start, delimiter) { 619 let body = ''; 620 for (let i = start + 1; i < text.length; i++) { 621 const character = text[i]; 622 if (character === '\\') { 623 const next = text[i + 1]; 624 if (next === undefined) return null; 625 if (next === delimiter) body += delimiter; 626 else body += character + next; 627 i++; 628 continue; 629 } 630 if (character === delimiter) return { text: '"' + body.replace(/"/g, '\\"') + '"', end: i }; 631 body += character; 632 } 633 return null; 634 } 635 636 // A bare word is a key only when a colon follows it; `true`, `false` and 637 // `null` reach the parser unchanged. 638 function convertBareWord(text, start) { 639 let end = start; 640 while (end < text.length && /[A-Za-z0-9_$]/.test(text[end])) end++; 641 let probe = end; 642 while (probe < text.length && /\s/.test(text[probe])) probe++; 643 if (text[probe] !== ':') return null; 644 return { text: '"' + text.slice(start, end) + '"', end: end }; 645 } 646 647 function skipComment(text, start) { 648 if (text[start + 1] === '/') { 649 let i = start + 2; 650 while (i < text.length && text[i] !== '\n') i++; 651 return i; 652 } 653 const close = text.indexOf('*/', start + 2); 654 return close === -1 ? null : close + 2; 655 } 656 657 function repairJson() { 658 const source = repairInput.val().trim(); 659 if (!source) { 660 showError(t('emptyJson')); 661 return; 662 } 663 const repaired = safeRepairText(source); 664 try { 665 const parsed = parseJson(repaired); 666 lastRepairedJson = JSON.stringify(parsed, null, parseInt(indentSize.val(), 10)); 667 repairOutput.text(lastRepairedJson); 668 hideError(); 669 if (repaired === source) layer.msg(t('jsonRepairNone'), { icon: 1 }); 670 } catch (e) { 671 lastRepairedJson = ''; 672 repairOutput.text(t('jsonRepairEmpty')); 673 showError(formatJsonError(e, repaired)); 674 } 675 } 676 677 function displayDiffValue(value) { 678 if (typeof value === 'undefined') return '(missing)'; 679 return typeof value === 'string' ? '"' + value + '"' : JSON.stringify(value); 680 } 681 682 function collectDiff(left, right, path, changes) { 683 if (changes.length >= 500) return; 684 const leftObject = left !== null && typeof left === 'object'; 685 const rightObject = right !== null && typeof right === 'object'; 686 if (!leftObject || !rightObject || Array.isArray(left) !== Array.isArray(right)) { 687 if (JSON.stringify(left) !== JSON.stringify(right)) { 688 changes.push({ path: path || '$', left: left, right: right }); 689 } 690 return; 691 }
692 const keys = Array.from(new Set(Object.keys(left).concat(Object.keys(right)))).sort(); 693 keys.forEach(function(key) { 694 const childPath = Array.isArray(left) ? (path || '$') + '[' + key + ']' : (path || '$') + '.' + key; 695 if (!Object.prototype.hasOwnProperty.call(left, key) || !Object.prototype.hasOwnProperty.call(right, key)) { 696 changes.push({ path: childPath, left: left[key], right: right[key] }); 697 return; 698 } 699 collectDiff(left[key], right[key], childPath, changes); 700 }); 701 } 702 703 function compareJson() { 704 const leftSource = diffLeftInput.val().trim(); 705 const rightSource = diffRightInput.val().trim(); 706 if (!leftSource || !rightSource) { 707 showError(t('jsonDiffEmpty')); 708 return; 709 } 710 try { 711 const left = parseJson(leftSource); 712 const right = parseJson(rightSource); 713 const changes = []; 714 collectDiff(left, right, '$', changes); 715 diffOutput.empty(); 716 if (!changes.length) { 717 diffOutput.text(t('jsonDiffSame')); 718 } else { 719 diffOutput.append($('<p/>', { class: 'json-diff-summary', text: t('jsonDiffChanged', { count: changes.length }) }));
720 changes.forEach(function(change) { 721 const row = $('<div/>', { class: 'json-diff-row' }); 722 $('<code/>', { text: change.path }).appendTo(row); 723 $('<span/>', { text: displayDiffValue(change.left) }).appendTo(row); 724 $('<span/>', { text: 'â' }).appendTo(row); 725 $('<span/>', { text: displayDiffValue(change.right) }).appendTo(row); 726 row.appendTo(diffOutput); 727 }); 728 } 729 hideError(); 730 } catch (e) { 731 showError(formatJsonError(e, leftSource + '\n' + rightSource)); 732 } 733 } 734 735 // Show error message 736 function showError(message) { 737 errorMessage.text(message).show(); 738 } 739 740 // Hide error message 741 function hideError() { 742 errorMessage.hide(); 743 } 744 745 // Event listeners 746 formatJsonBtn.on('click', formatJson); 747 loadJsonSampleBtn.on('click', loadSample); 748 validateJsonBtn.on('click', validateJson); 749 jsonCopyBtn.on('click', () => copyToClipboard(jsonInput, 'Input JSON')); 750 formattedJsonCopyBtn.on('click', () => copyToClipboard(jsonOutput, 'Formatted JSON')); 751 downloadJsonBtn.on('click', function() { 752 const content = outputText(jsonOutput).trim(); 753 downloadJson(content, 'formatted-json.json'); 754 }); 755 jsonClearBtn.on('click', clearEditor); 756 compareJsonBtn.on('click', compareJson); 757 repairJsonBtn.on('click', repairJson); 758 applyRepairBtn.on('click', function() { 759 if (!lastRepairedJson) { 760 repairJson(); 761 } 762 if (!lastRepairedJson) return; 763 jsonInput.val(lastRepairedJson); 764 syncJsonInputs(lastRepairedJson); 765 $('.tab-btn[data-tab="format"]').trigger('click'); 766 formatJson(); 767 layer.msg(t('jsonRepairApplied'), { icon: 1 }); 768 }); 769 copyRepairBtn.on('click', function() { 770 copyToClipboard(repairOutput, 'Repaired JSON'); 771 }); 772 773 // Compress event listeners 774 compressJsonBtn.on('click', compressJson); 775 compressJsonCopyBtn.on('click', () => copyToClipboard(compressInput, 'Input JSON')); 776 compressOutputCopyBtn.on('click', () => copyToClipboard(compressedOutput, 'Compressed JSON')); 777 downloadCompressedJsonBtn.on('click', function() { 778 const content = compressedOutput.text().trim(); 779 downloadJson(content, 'compressed-json.json'); 780 }); 781 782 // XML conversion event listeners 783 jsonToXmlBtn.on('click', jsonToXml); 784 jsonToXmlCopyBtn.on('click', () => copyToClipboard(jsonToXmlInput, 'Input JSON')); 785 xmlOutputCopyBtn.on('click', () => copyToClipboard(xmlOutput, 'Converted XML')); 786 downloadXmlBtn.on('click', function() { 787 const content = xmlOutput.text().trim(); 788 downloadXml(content); 789 }); 790 791 // Path search event listeners 792 pathSearchBtn.on('click', searchJsonPath); 793 clearPathResultsBtn.on('click', function() { 794 pathResults.hide(); 795 pathSearchInput.val(''); 796 }); 797 798 // File input handler 799 jsonFileInput.on('change', function() { 800 if (this.files.length) { 801 handleJsonFile(this.files[0]); 802 } 803 }); 804 805 jsonInput.closest('.editor-wrapper').on('dragover', function(e) { 806 e.preventDefault(); 807 $(this).addClass('format-drop-active'); 808 }).on('dragleave', function() { 809 $(this).removeClass('format-drop-active'); 810 }).on('drop', function(e) { 811 e.preventDefault(); 812 $(this).removeClass('format-drop-active'); 813 const files = e.originalEvent.dataTransfer.files; 814 if (files && files.length) handleJsonFile(files[0]); 815 }); 816 817 // Option change handlers 818 indentSize.on('change', formatJson); 819 showLineNumbers.on('change', formatJson); 820 quoteKeys.on('change', formatJson); 821 822 // Auto-format when typing stops 823 jsonInput.on('input', function() { 824 syncJsonInputs(jsonInput.val()); 825 if (!liveFormat.is(':checked')) return; 826 clearTimeout(window.jsonFormatTimer); 827 window.jsonFormatTimer = setTimeout(formatJson, 500); 828 }); 829 830 // Auto-compress when typing stops 831 compressInput.on('input', function() { 832 clearTimeout(window.compressJsonTimer); 833 window.compressJsonTimer = setTimeout(compressJson, 500); 834 }); 835 836 // Auto-convert to XML when typing stops 837 jsonToXmlInput.on('input', function() { 838 clearTimeout(window.jsonToXmlTimer); 839 window.jsonToXmlTimer = setTimeout(jsonToXml, 500); 840 }); 841 842 // Search path when Enter is pressed 843 pathSearchInput.on('keypress', function(e) { 844 if (e.which === 13) { 845 searchJsonPath(); 846 } 847 }); 848 849 // JSON Tree viewer 850 const treeJsonInput = $('#treeJsonInput'); 851 const jsonTreeOutput = $('#jsonTreeOutput'); 852 const buildTreeBtn = $('#buildTreeBtn'); 853 854 function renderTreeNode(key, value, depth) { 855 var row = document.createElement('div'); 856 row.className = 'json-tree-node'; 857 row.style.marginLeft = (depth * 16) + 'px'; 858 var keyNode = document.createElement('span'); 859 keyNode.className = 'json-tree-key'; 860 861 if (value !== null && typeof value === 'object') { 862 keyNode.textContent = Array.isArray(value) ? key + ' [Array(' + value.length + ')]' : key + ' {Object}'; 863 row.appendChild(keyNode); 864 var fragment = document.createDocumentFragment(); 865 fragment.appendChild(row); 866 Object.keys(value).forEach(function (childKey) { 867 fragment.appendChild(renderTreeNode(childKey, value[childKey], depth + 1)); 868 }); 869 return fragment; 870 } 871 872 keyNode.textContent = key + ':'; 873 var valueNode = document.createElement('span'); 874 valueNode.className = 'json-tree-leaf'; 875 valueNode.textContent = ' ' + (typeof value === 'string' ? '"' + value + '"' : String(value)); 876 row.appendChild(keyNode); 877 row.appendChild(valueNode); 878 return row; 879 } 880 881 function buildJsonTree() { 882 try { 883 var treeContent = treeJsonInput.val().trim(); 884 var parsed = parseJson(treeContent); 885 jsonTreeOutput.empty().append(renderTreeNode('root', parsed, 0)); 886 errorMessage.hide(); 887 } catch (e) { 888 showError(formatJsonError(e, treeContent)); 889 jsonTreeOutput.text(formatJsonError(e, treeContent)); 890 } 891 } 892 893 buildTreeBtn.on('click', buildJsonTree); 894 895 // Initial validation and format 896 const initialContent = jsonInput.val().trim(); 897 try { 898 if (initialContent) { 899 parseJson(initialContent); 900 formatJson(); 901 } 902 } catch (e) {
903 // Initial content is invalid, show error but don't format 904 showError(formatJsonError(e, initialContent)); 905 } 906});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.