PageSourceSearch

https://tidycode.org/tools/format/js/tool/format/json.js?v=1790583980

js tidycode.org collected 2026-10-02 01:07:52 UTC 32,457 bytes, 906 lines download raw bytes

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 '&lt;';
292                case '>': return '&gt;';
293                case '&': return '&amp;';
294                case '\'': return '&apos;';
295                case '"': return '&quot;';
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.