modern-json-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1670 @@
1
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/JsonEditor.tsx
5
+ var MODE_OPTIONS = [
6
+ { value: "code", label: "Code" },
7
+ { value: "tree", label: "Tree" },
8
+ { value: "split", label: "Split" }
9
+ ];
10
+ var Toolbar = ({
11
+ mode,
12
+ onModeChange,
13
+ canUndo,
14
+ canRedo,
15
+ onUndo,
16
+ onRedo,
17
+ onFormat,
18
+ searchable,
19
+ isSearchOpen,
20
+ onToggleSearch,
21
+ readOnly,
22
+ className = ""
23
+ }) => {
24
+ return /* @__PURE__ */ jsxs("div", { className: `mjr-toolbar ${className}`, role: "toolbar", "aria-label": "Editor controls", children: [
25
+ /* @__PURE__ */ jsx("div", { className: "mjr-toolbar__modes", role: "tablist", "aria-label": "Editor mode", children: MODE_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsx(
26
+ "button",
27
+ {
28
+ role: "tab",
29
+ "aria-selected": mode === value,
30
+ className: `mjr-toolbar__mode-btn ${mode === value ? "mjr-toolbar__mode-btn--active" : ""}`,
31
+ onClick: () => onModeChange(value),
32
+ children: label
33
+ },
34
+ value
35
+ )) }),
36
+ /* @__PURE__ */ jsx("div", { className: "mjr-toolbar__separator", "aria-hidden": "true" }),
37
+ searchable && /* @__PURE__ */ jsx(
38
+ "button",
39
+ {
40
+ className: `mjr-toolbar__btn ${isSearchOpen ? "mjr-toolbar__btn--active" : ""}`,
41
+ onClick: onToggleSearch,
42
+ "aria-label": "Toggle search",
43
+ "aria-pressed": isSearchOpen,
44
+ title: "Search (Ctrl+F)",
45
+ children: "\u{1F50D}"
46
+ }
47
+ ),
48
+ /* @__PURE__ */ jsx("div", { className: "mjr-toolbar__separator", "aria-hidden": "true" }),
49
+ !readOnly && /* @__PURE__ */ jsxs(Fragment, { children: [
50
+ /* @__PURE__ */ jsx(
51
+ "button",
52
+ {
53
+ className: "mjr-toolbar__btn",
54
+ onClick: onUndo,
55
+ disabled: !canUndo,
56
+ "aria-label": "Undo",
57
+ title: "Undo (Ctrl+Z)",
58
+ children: "\u21B6"
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsx(
62
+ "button",
63
+ {
64
+ className: "mjr-toolbar__btn",
65
+ onClick: onRedo,
66
+ disabled: !canRedo,
67
+ "aria-label": "Redo",
68
+ title: "Redo (Ctrl+Shift+Z)",
69
+ children: "\u21B7"
70
+ }
71
+ )
72
+ ] }),
73
+ !readOnly && /* @__PURE__ */ jsx(
74
+ "button",
75
+ {
76
+ className: "mjr-toolbar__btn",
77
+ onClick: onFormat,
78
+ "aria-label": "Format document",
79
+ title: "Format (Ctrl+Shift+P)",
80
+ children: "{ }"
81
+ }
82
+ )
83
+ ] });
84
+ };
85
+ var CodeEditor = ({
86
+ value,
87
+ onChange,
88
+ parseError,
89
+ readOnly,
90
+ lineNumbers,
91
+ searchMatches,
92
+ currentMatchIndex,
93
+ onCursorChange,
94
+ className = ""
95
+ }) => {
96
+ const textareaRef = useRef(null);
97
+ const lines = useMemo(() => value.split("\n"), [value]);
98
+ const handleChange = useCallback(
99
+ (e) => {
100
+ if (readOnly) return;
101
+ onChange(e.target.value);
102
+ },
103
+ [onChange, readOnly]
104
+ );
105
+ const handleKeyDown = useCallback(
106
+ (e) => {
107
+ if (readOnly) return;
108
+ if (e.key === "Tab") {
109
+ e.preventDefault();
110
+ const textarea = textareaRef.current;
111
+ if (!textarea) return;
112
+ const start = textarea.selectionStart;
113
+ const end = textarea.selectionEnd;
114
+ const indent = " ";
115
+ const newValue = value.substring(0, start) + indent + value.substring(end);
116
+ onChange(newValue);
117
+ requestAnimationFrame(() => {
118
+ textarea.selectionStart = textarea.selectionEnd = start + indent.length;
119
+ });
120
+ }
121
+ if (e.key === "{" || e.key === "[" || e.key === '"') {
122
+ const textarea = textareaRef.current;
123
+ if (!textarea) return;
124
+ const start = textarea.selectionStart;
125
+ const end = textarea.selectionEnd;
126
+ const closeChar = e.key === "{" ? "}" : e.key === "[" ? "]" : '"';
127
+ if (start === end) {
128
+ e.preventDefault();
129
+ const newValue = value.substring(0, start) + e.key + closeChar + value.substring(end);
130
+ onChange(newValue);
131
+ requestAnimationFrame(() => {
132
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
133
+ });
134
+ }
135
+ }
136
+ },
137
+ [value, onChange, readOnly]
138
+ );
139
+ const handleCursorMove = useCallback(() => {
140
+ const textarea = textareaRef.current;
141
+ if (!textarea) return;
142
+ const pos = textarea.selectionStart;
143
+ const textBefore = value.substring(0, pos);
144
+ const linesBefore = textBefore.split("\n");
145
+ const line = linesBefore.length;
146
+ const column = linesBefore[linesBefore.length - 1].length + 1;
147
+ onCursorChange({ line, column, offset: pos });
148
+ }, [value, onCursorChange]);
149
+ const highlightedLines = useMemo(() => {
150
+ return lines.map((line, idx) => {
151
+ const lineNum = idx + 1;
152
+ const isErrorLine = parseError?.line === lineNum;
153
+ return /* @__PURE__ */ jsxs(
154
+ "div",
155
+ {
156
+ className: `mjr-code__line ${isErrorLine ? "mjr-code__line--error" : ""}`,
157
+ children: [
158
+ lineNumbers && /* @__PURE__ */ jsx("span", { className: "mjr-code__line-number", "aria-hidden": "true", children: lineNum }),
159
+ /* @__PURE__ */ jsx("span", { className: "mjr-code__line-content", children: highlightJsonLine(line) })
160
+ ]
161
+ },
162
+ idx
163
+ );
164
+ });
165
+ }, [lines, lineNumbers, parseError]);
166
+ return /* @__PURE__ */ jsxs("div", { className: `mjr-code-editor ${className}`, children: [
167
+ /* @__PURE__ */ jsx("div", { className: "mjr-code__display", "aria-hidden": "true", children: highlightedLines }),
168
+ /* @__PURE__ */ jsx(
169
+ "textarea",
170
+ {
171
+ ref: textareaRef,
172
+ className: "mjr-code__textarea",
173
+ value,
174
+ onChange: handleChange,
175
+ onKeyDown: handleKeyDown,
176
+ onKeyUp: handleCursorMove,
177
+ onClick: handleCursorMove,
178
+ readOnly,
179
+ spellCheck: false,
180
+ autoCapitalize: "off",
181
+ autoComplete: "off",
182
+ autoCorrect: "off",
183
+ "aria-label": "JSON code editor",
184
+ "aria-multiline": "true",
185
+ "aria-readonly": readOnly,
186
+ "data-testid": "code-editor-textarea"
187
+ }
188
+ ),
189
+ parseError && /* @__PURE__ */ jsxs(
190
+ "div",
191
+ {
192
+ className: "mjr-code__error-tooltip",
193
+ role: "alert",
194
+ style: { top: `${(parseError.line - 1) * 1.5}em` },
195
+ children: [
196
+ "Line ",
197
+ parseError.line,
198
+ ", Col ",
199
+ parseError.column,
200
+ ": ",
201
+ parseError.message
202
+ ]
203
+ }
204
+ )
205
+ ] });
206
+ };
207
+ function highlightJsonLine(line) {
208
+ const tokens = [];
209
+ let i = 0;
210
+ let key = 0;
211
+ while (i < line.length) {
212
+ const ch = line[i];
213
+ if (ch === " " || ch === " ") {
214
+ let ws = "";
215
+ while (i < line.length && (line[i] === " " || line[i] === " ")) {
216
+ ws += line[i];
217
+ i++;
218
+ }
219
+ tokens.push(/* @__PURE__ */ jsx("span", { children: ws }, key++));
220
+ continue;
221
+ }
222
+ if (ch === '"') {
223
+ let str = '"';
224
+ i++;
225
+ while (i < line.length) {
226
+ if (line[i] === "\\") {
227
+ str += line[i] + (line[i + 1] || "");
228
+ i += 2;
229
+ continue;
230
+ }
231
+ str += line[i];
232
+ if (line[i] === '"') {
233
+ i++;
234
+ break;
235
+ }
236
+ i++;
237
+ }
238
+ const rest = line.substring(i).trimStart();
239
+ const isKey = rest.startsWith(":");
240
+ tokens.push(
241
+ /* @__PURE__ */ jsx("span", { className: isKey ? "mjr-syn-key" : "mjr-syn-string", children: str }, key++)
242
+ );
243
+ continue;
244
+ }
245
+ if (ch === "-" || ch >= "0" && ch <= "9") {
246
+ let num = "";
247
+ while (i < line.length && /[\d.eE+\-]/.test(line[i])) {
248
+ num += line[i];
249
+ i++;
250
+ }
251
+ tokens.push(
252
+ /* @__PURE__ */ jsx("span", { className: "mjr-syn-number", children: num }, key++)
253
+ );
254
+ continue;
255
+ }
256
+ if (line.substring(i, i + 4) === "true") {
257
+ tokens.push(/* @__PURE__ */ jsx("span", { className: "mjr-syn-boolean", children: "true" }, key++));
258
+ i += 4;
259
+ continue;
260
+ }
261
+ if (line.substring(i, i + 5) === "false") {
262
+ tokens.push(/* @__PURE__ */ jsx("span", { className: "mjr-syn-boolean", children: "false" }, key++));
263
+ i += 5;
264
+ continue;
265
+ }
266
+ if (line.substring(i, i + 4) === "null") {
267
+ tokens.push(/* @__PURE__ */ jsx("span", { className: "mjr-syn-null", children: "null" }, key++));
268
+ i += 4;
269
+ continue;
270
+ }
271
+ if (ch === "{" || ch === "}" || ch === "[" || ch === "]") {
272
+ tokens.push(/* @__PURE__ */ jsx("span", { className: "mjr-syn-bracket", children: ch }, key++));
273
+ i++;
274
+ continue;
275
+ }
276
+ tokens.push(/* @__PURE__ */ jsx("span", { className: "mjr-syn-punctuation", children: ch }, key++));
277
+ i++;
278
+ }
279
+ return tokens;
280
+ }
281
+ var TYPE_LABELS = {
282
+ string: "str",
283
+ number: "num",
284
+ boolean: "bool",
285
+ null: "null",
286
+ object: "obj",
287
+ array: "arr"
288
+ };
289
+ var TreeNodeComponent = ({
290
+ node,
291
+ onToggle,
292
+ onValueChange,
293
+ onKeyChange,
294
+ onDelete,
295
+ onTypeChange,
296
+ readOnly
297
+ }) => {
298
+ const [isEditing, setIsEditing] = useState(false);
299
+ const [editValue, setEditValue] = useState("");
300
+ const [isEditingKey, setIsEditingKey] = useState(false);
301
+ const [editKey, setEditKey] = useState("");
302
+ const isExpandable = node.type === "object" || node.type === "array";
303
+ const handleToggle = useCallback(() => {
304
+ if (isExpandable) {
305
+ onToggle(node.id);
306
+ }
307
+ }, [isExpandable, node.id, onToggle]);
308
+ const handleStartEdit = useCallback(() => {
309
+ if (readOnly || isExpandable) return;
310
+ setEditValue(node.type === "string" ? String(node.value) : JSON.stringify(node.value));
311
+ setIsEditing(true);
312
+ }, [readOnly, isExpandable, node.value, node.type]);
313
+ const handleFinishEdit = useCallback(() => {
314
+ setIsEditing(false);
315
+ let newValue;
316
+ if (node.type === "string") {
317
+ newValue = editValue;
318
+ } else if (node.type === "number") {
319
+ const num = Number(editValue);
320
+ newValue = isNaN(num) ? node.value : num;
321
+ } else if (node.type === "boolean") {
322
+ newValue = editValue === "true";
323
+ } else if (node.type === "null") {
324
+ newValue = null;
325
+ } else {
326
+ try {
327
+ newValue = JSON.parse(editValue);
328
+ } catch {
329
+ newValue = node.value;
330
+ }
331
+ }
332
+ onValueChange(node.path, newValue);
333
+ }, [editValue, node.type, node.value, node.path, onValueChange]);
334
+ const handleKeyEdit = useCallback(() => {
335
+ if (readOnly || typeof node.key === "number") return;
336
+ setEditKey(String(node.key));
337
+ setIsEditingKey(true);
338
+ }, [readOnly, node.key]);
339
+ const handleFinishKeyEdit = useCallback(() => {
340
+ setIsEditingKey(false);
341
+ if (editKey !== String(node.key)) {
342
+ onKeyChange(node.path, String(node.key), editKey);
343
+ }
344
+ }, [editKey, node.key, node.path, onKeyChange]);
345
+ const handleTypeChange = useCallback(
346
+ (e) => {
347
+ const newType = e.target.value;
348
+ onTypeChange(node.path, newType);
349
+ },
350
+ [node.path, onTypeChange]
351
+ );
352
+ const renderValue = () => {
353
+ if (isEditing) {
354
+ return /* @__PURE__ */ jsx(
355
+ "input",
356
+ {
357
+ className: "mjr-tree-node__edit-input",
358
+ value: editValue,
359
+ onChange: (e) => setEditValue(e.target.value),
360
+ onBlur: handleFinishEdit,
361
+ onKeyDown: (e) => {
362
+ if (e.key === "Enter") handleFinishEdit();
363
+ if (e.key === "Escape") setIsEditing(false);
364
+ },
365
+ autoFocus: true,
366
+ "aria-label": `Edit value for ${node.key}`,
367
+ "data-testid": `edit-value-${node.path}`
368
+ }
369
+ );
370
+ }
371
+ if (isExpandable) {
372
+ const symbol = node.type === "object" ? "{}" : "[]";
373
+ return /* @__PURE__ */ jsxs("span", { className: "mjr-tree-node__preview", children: [
374
+ node.expanded ? "" : `${symbol}`,
375
+ !node.expanded && /* @__PURE__ */ jsxs("span", { className: "mjr-tree-node__count", children: [
376
+ "(",
377
+ node.childCount,
378
+ " ",
379
+ node.childCount === 1 ? "item" : "items",
380
+ ")"
381
+ ] })
382
+ ] });
383
+ }
384
+ return /* @__PURE__ */ jsx(
385
+ "span",
386
+ {
387
+ className: `mjr-tree-node__value mjr-tree-node__value--${node.type}`,
388
+ onDoubleClick: handleStartEdit,
389
+ role: "button",
390
+ tabIndex: 0,
391
+ "aria-label": `Value: ${formatDisplayValue(node.value, node.type)}. Double-click to edit.`,
392
+ "data-testid": `value-${node.path}`,
393
+ children: formatDisplayValue(node.value, node.type)
394
+ }
395
+ );
396
+ };
397
+ return /* @__PURE__ */ jsxs(
398
+ "div",
399
+ {
400
+ className: "mjr-tree-node",
401
+ role: "treeitem",
402
+ "aria-expanded": isExpandable ? node.expanded : void 0,
403
+ "aria-level": node.depth + 1,
404
+ "aria-label": `${node.key}: ${node.type}`,
405
+ style: { paddingLeft: `${node.depth * 20}px` },
406
+ "data-testid": `tree-node-${node.path}`,
407
+ children: [
408
+ /* @__PURE__ */ jsx(
409
+ "span",
410
+ {
411
+ className: `mjr-tree-node__arrow ${isExpandable ? "mjr-tree-node__arrow--expandable" : ""} ${node.expanded ? "mjr-tree-node__arrow--expanded" : ""}`,
412
+ onClick: handleToggle,
413
+ "aria-hidden": "true",
414
+ children: isExpandable ? node.expanded ? "\u25BC" : "\u25B6" : "\xA0"
415
+ }
416
+ ),
417
+ isEditingKey ? /* @__PURE__ */ jsx(
418
+ "input",
419
+ {
420
+ className: "mjr-tree-node__key-input",
421
+ value: editKey,
422
+ onChange: (e) => setEditKey(e.target.value),
423
+ onBlur: handleFinishKeyEdit,
424
+ onKeyDown: (e) => {
425
+ if (e.key === "Enter") handleFinishKeyEdit();
426
+ if (e.key === "Escape") setIsEditingKey(false);
427
+ },
428
+ autoFocus: true,
429
+ "aria-label": `Edit key name`
430
+ }
431
+ ) : /* @__PURE__ */ jsx(
432
+ "span",
433
+ {
434
+ className: "mjr-tree-node__key",
435
+ onDoubleClick: handleKeyEdit,
436
+ "data-testid": `key-${node.path}`,
437
+ children: typeof node.key === "number" ? node.key : `"${node.key}"`
438
+ }
439
+ ),
440
+ /* @__PURE__ */ jsx("span", { className: "mjr-tree-node__colon", "aria-hidden": "true", children: ":" }),
441
+ renderValue(),
442
+ !readOnly ? /* @__PURE__ */ jsx(
443
+ "select",
444
+ {
445
+ className: "mjr-tree-node__type-badge",
446
+ value: node.type,
447
+ onChange: handleTypeChange,
448
+ "aria-label": `Type for ${node.key}`,
449
+ "data-testid": `type-${node.path}`,
450
+ children: Object.entries(TYPE_LABELS).map(([type, label]) => /* @__PURE__ */ jsx("option", { value: type, children: label }, type))
451
+ }
452
+ ) : /* @__PURE__ */ jsx("span", { className: "mjr-tree-node__type-badge-ro", children: TYPE_LABELS[node.type] }),
453
+ !readOnly && /* @__PURE__ */ jsx(
454
+ "button",
455
+ {
456
+ className: "mjr-tree-node__delete",
457
+ onClick: () => onDelete(node.path),
458
+ "aria-label": `Delete ${node.key}`,
459
+ title: "Delete",
460
+ "data-testid": `delete-${node.path}`,
461
+ children: "\\u2715"
462
+ }
463
+ ),
464
+ isExpandable && node.expanded && node.children.length > 0 && /* @__PURE__ */ jsx("div", { role: "group", className: "mjr-tree-node__children", children: node.children.map((child) => /* @__PURE__ */ jsx(
465
+ TreeNodeComponent,
466
+ {
467
+ node: child,
468
+ onToggle,
469
+ onValueChange,
470
+ onKeyChange,
471
+ onDelete,
472
+ onTypeChange,
473
+ readOnly
474
+ },
475
+ child.id
476
+ )) })
477
+ ]
478
+ }
479
+ );
480
+ };
481
+ function formatDisplayValue(value, type) {
482
+ if (type === "null") return "null";
483
+ if (type === "string") return `"${String(value)}"`;
484
+ if (type === "boolean") return String(value);
485
+ if (type === "number") return String(value);
486
+ return "";
487
+ }
488
+
489
+ // src/core/path.ts
490
+ function getByPath(obj, path) {
491
+ if (path === "$" || path === "") return obj;
492
+ const segments = parsePath(path);
493
+ let current = obj;
494
+ for (const segment of segments) {
495
+ if (current === null || current === void 0) return void 0;
496
+ if (Array.isArray(current)) {
497
+ const index = parseInt(segment, 10);
498
+ if (isNaN(index)) return void 0;
499
+ current = current[index];
500
+ } else if (typeof current === "object") {
501
+ current = current[segment];
502
+ } else {
503
+ return void 0;
504
+ }
505
+ }
506
+ return current;
507
+ }
508
+ function setByPath(obj, path, value) {
509
+ if (path === "$" || path === "") return value;
510
+ const segments = parsePath(path);
511
+ return setBySegments(obj, segments, 0, value);
512
+ }
513
+ function setBySegments(current, segments, index, value) {
514
+ if (index === segments.length) return value;
515
+ const segment = segments[index];
516
+ if (Array.isArray(current)) {
517
+ const arrIndex = parseInt(segment, 10);
518
+ const newArr = [...current];
519
+ newArr[arrIndex] = setBySegments(current[arrIndex], segments, index + 1, value);
520
+ return newArr;
521
+ }
522
+ if (typeof current === "object" && current !== null) {
523
+ return {
524
+ ...current,
525
+ [segment]: setBySegments(
526
+ current[segment],
527
+ segments,
528
+ index + 1,
529
+ value
530
+ )
531
+ };
532
+ }
533
+ const nextSegment = segments[index + 1];
534
+ const isArrayIndex = nextSegment !== void 0 && /^\d+$/.test(nextSegment);
535
+ const container = isArrayIndex ? [] : {};
536
+ return {
537
+ [segment]: setBySegments(container, segments, index + 1, value)
538
+ };
539
+ }
540
+ function deleteByPath(obj, path) {
541
+ if (path === "$" || path === "") return void 0;
542
+ const segments = parsePath(path);
543
+ return deleteBySegments(obj, segments, 0);
544
+ }
545
+ function deleteBySegments(current, segments, index) {
546
+ if (index === segments.length - 1) {
547
+ const segment2 = segments[index];
548
+ if (Array.isArray(current)) {
549
+ const arrIndex = parseInt(segment2, 10);
550
+ return current.filter((_, i) => i !== arrIndex);
551
+ }
552
+ if (typeof current === "object" && current !== null) {
553
+ const { [segment2]: _, ...rest } = current;
554
+ return rest;
555
+ }
556
+ return current;
557
+ }
558
+ const segment = segments[index];
559
+ if (Array.isArray(current)) {
560
+ const arrIndex = parseInt(segment, 10);
561
+ const newArr = [...current];
562
+ newArr[arrIndex] = deleteBySegments(current[arrIndex], segments, index + 1);
563
+ return newArr;
564
+ }
565
+ if (typeof current === "object" && current !== null) {
566
+ return {
567
+ ...current,
568
+ [segment]: deleteBySegments(
569
+ current[segment],
570
+ segments,
571
+ index + 1
572
+ )
573
+ };
574
+ }
575
+ return current;
576
+ }
577
+ function parsePath(path) {
578
+ let normalized = path.replace(/^\$\.?/, "");
579
+ normalized = normalized.replace(/\[(\d+)\]/g, ".$1");
580
+ normalized = normalized.replace(/\["([^"]+)"\]/g, ".$1");
581
+ normalized = normalized.replace(/\['([^']+)'\]/g, ".$1");
582
+ return normalized.split(".").filter((s) => s !== "");
583
+ }
584
+ function buildPath(segments) {
585
+ if (segments.length === 0) return "$";
586
+ return "$." + segments.map((s) => typeof s === "number" ? `[${s}]` : s).join(".");
587
+ }
588
+ var TreeEditor = ({
589
+ value,
590
+ onChange,
591
+ readOnly,
592
+ className = ""
593
+ }) => {
594
+ const [expandedPaths, setExpandedPaths] = useState(/* @__PURE__ */ new Set(["$"]));
595
+ const tree = useMemo(() => {
596
+ return buildTree(value, "$", "root", 0, expandedPaths);
597
+ }, [value, expandedPaths]);
598
+ const handleToggle = useCallback((id) => {
599
+ setExpandedPaths((prev) => {
600
+ const next = new Set(prev);
601
+ const path = id;
602
+ if (next.has(path)) {
603
+ next.delete(path);
604
+ } else {
605
+ next.add(path);
606
+ }
607
+ return next;
608
+ });
609
+ }, []);
610
+ const handleValueChange = useCallback(
611
+ (path, newValue) => {
612
+ const updated = setByPath(value, path, newValue);
613
+ onChange(updated);
614
+ },
615
+ [value, onChange]
616
+ );
617
+ const handleKeyChange = useCallback(
618
+ (path, oldKey, newKey) => {
619
+ const segments = path.split(".");
620
+ const parentPath = segments.slice(0, -1).join(".") || "$";
621
+ const currentValue = getNestedValue(value, path);
622
+ let updated = deleteByPath(value, path);
623
+ const newPath = parentPath === "$" ? `$.${newKey}` : `${parentPath}.${newKey}`;
624
+ updated = setByPath(updated, newPath, currentValue);
625
+ onChange(updated);
626
+ },
627
+ [value, onChange]
628
+ );
629
+ const handleDelete = useCallback(
630
+ (path) => {
631
+ const updated = deleteByPath(value, path);
632
+ onChange(updated);
633
+ },
634
+ [value, onChange]
635
+ );
636
+ const handleTypeChange = useCallback(
637
+ (path, newType) => {
638
+ const defaults = {
639
+ string: "",
640
+ number: 0,
641
+ boolean: false,
642
+ null: null,
643
+ object: {},
644
+ array: []
645
+ };
646
+ const updated = setByPath(value, path, defaults[newType]);
647
+ onChange(updated);
648
+ },
649
+ [value, onChange]
650
+ );
651
+ const handleAddProperty = useCallback(() => {
652
+ if (value === void 0 || value === null) {
653
+ onChange({});
654
+ return;
655
+ }
656
+ if (typeof value === "object" && !Array.isArray(value)) {
657
+ const obj = value;
658
+ let newKey = "newKey";
659
+ let counter = 1;
660
+ while (newKey in obj) {
661
+ newKey = `newKey${counter++}`;
662
+ }
663
+ onChange({ ...obj, [newKey]: "" });
664
+ } else if (Array.isArray(value)) {
665
+ onChange([...value, ""]);
666
+ }
667
+ }, [value, onChange]);
668
+ if (tree === null) {
669
+ return /* @__PURE__ */ jsxs("div", { className: `mjr-tree-editor mjr-tree-editor--empty ${className}`, children: [
670
+ /* @__PURE__ */ jsx("p", { className: "mjr-tree-editor__empty-msg", children: "No valid JSON to display" }),
671
+ !readOnly && /* @__PURE__ */ jsx("button", { className: "mjr-tree-editor__add-btn", onClick: () => onChange({}), children: "+ Create empty object" })
672
+ ] });
673
+ }
674
+ return /* @__PURE__ */ jsxs(
675
+ "div",
676
+ {
677
+ className: `mjr-tree-editor ${className}`,
678
+ role: "tree",
679
+ "aria-label": "JSON tree editor",
680
+ "data-testid": "tree-editor",
681
+ children: [
682
+ /* @__PURE__ */ jsx(
683
+ TreeNodeComponent,
684
+ {
685
+ node: tree,
686
+ onToggle: handleToggle,
687
+ onValueChange: handleValueChange,
688
+ onKeyChange: handleKeyChange,
689
+ onDelete: handleDelete,
690
+ onTypeChange: handleTypeChange,
691
+ readOnly
692
+ }
693
+ ),
694
+ !readOnly && /* @__PURE__ */ jsx(
695
+ "button",
696
+ {
697
+ className: "mjr-tree-editor__add-root",
698
+ onClick: handleAddProperty,
699
+ "data-testid": "add-property",
700
+ children: "+ Add property"
701
+ }
702
+ )
703
+ ]
704
+ }
705
+ );
706
+ };
707
+ function buildTree(value, path, key, depth, expandedPaths) {
708
+ if (value === void 0) return null;
709
+ const type = getType(value);
710
+ const expanded = expandedPaths.has(path);
711
+ const id = path;
712
+ const node = {
713
+ id,
714
+ key,
715
+ value,
716
+ type,
717
+ path,
718
+ depth,
719
+ expanded,
720
+ children: [],
721
+ childCount: 0
722
+ };
723
+ if (type === "object" && value !== null) {
724
+ const obj = value;
725
+ const keys = Object.keys(obj);
726
+ node.childCount = keys.length;
727
+ if (expanded) {
728
+ node.children = keys.map(
729
+ (k) => buildTree(obj[k], `${path}.${k}`, k, depth + 1, expandedPaths)
730
+ ).filter(Boolean);
731
+ }
732
+ } else if (type === "array") {
733
+ const arr = value;
734
+ node.childCount = arr.length;
735
+ if (expanded) {
736
+ node.children = arr.map(
737
+ (item, i) => buildTree(item, `${path}[${i}]`, i, depth + 1, expandedPaths)
738
+ ).filter(Boolean);
739
+ }
740
+ }
741
+ return node;
742
+ }
743
+ function getType(value) {
744
+ if (value === null) return "null";
745
+ if (Array.isArray(value)) return "array";
746
+ const t = typeof value;
747
+ if (t === "string") return "string";
748
+ if (t === "number") return "number";
749
+ if (t === "boolean") return "boolean";
750
+ if (t === "object") return "object";
751
+ return "string";
752
+ }
753
+ function getNestedValue(obj, path) {
754
+ const segments = path.replace(/^\$\.?/, "").replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
755
+ let current = obj;
756
+ for (const seg of segments) {
757
+ if (current === null || current === void 0) return void 0;
758
+ if (Array.isArray(current)) {
759
+ current = current[parseInt(seg, 10)];
760
+ } else if (typeof current === "object") {
761
+ current = current[seg];
762
+ }
763
+ }
764
+ return current;
765
+ }
766
+ var StatusBar = ({
767
+ parseError,
768
+ validationErrors,
769
+ cursor,
770
+ stats,
771
+ className = ""
772
+ }) => {
773
+ const hasErrors = parseError !== null;
774
+ const hasWarnings = validationErrors.length > 0;
775
+ let statusIcon;
776
+ let statusText;
777
+ let statusClass;
778
+ if (hasErrors) {
779
+ statusIcon = "\u2715";
780
+ statusText = `Invalid JSON (line ${parseError.line})`;
781
+ statusClass = "mjr-status--error";
782
+ } else if (hasWarnings) {
783
+ statusIcon = "\u26A0";
784
+ statusText = `${validationErrors.length} validation ${validationErrors.length === 1 ? "issue" : "issues"}`;
785
+ statusClass = "mjr-status--warning";
786
+ } else {
787
+ statusIcon = "\u2713";
788
+ statusText = "Valid JSON";
789
+ statusClass = "mjr-status--valid";
790
+ }
791
+ return /* @__PURE__ */ jsxs(
792
+ "div",
793
+ {
794
+ className: `mjr-status-bar ${statusClass} ${className}`,
795
+ role: "status",
796
+ "aria-live": "polite",
797
+ "aria-atomic": "true",
798
+ children: [
799
+ /* @__PURE__ */ jsxs("span", { className: "mjr-status-bar__indicator", children: [
800
+ /* @__PURE__ */ jsx("span", { className: "mjr-status-bar__icon", "aria-hidden": "true", children: statusIcon }),
801
+ /* @__PURE__ */ jsx("span", { className: "mjr-status-bar__text", children: statusText })
802
+ ] }),
803
+ /* @__PURE__ */ jsx("span", { className: "mjr-status-bar__separator", "aria-hidden": "true", children: "|" }),
804
+ /* @__PURE__ */ jsxs("span", { className: "mjr-status-bar__cursor", children: [
805
+ "Ln ",
806
+ cursor.line,
807
+ ", Col ",
808
+ cursor.column
809
+ ] }),
810
+ stats && /* @__PURE__ */ jsxs(Fragment, { children: [
811
+ /* @__PURE__ */ jsx("span", { className: "mjr-status-bar__separator", "aria-hidden": "true", children: "|" }),
812
+ /* @__PURE__ */ jsxs("span", { className: "mjr-status-bar__stats", children: [
813
+ stats.properties,
814
+ " ",
815
+ stats.properties === 1 ? "property" : "properties",
816
+ stats.arrays > 0 && `, ${stats.arrays} ${stats.arrays === 1 ? "array" : "arrays"}`
817
+ ] })
818
+ ] }),
819
+ stats && stats.byteSize > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
820
+ /* @__PURE__ */ jsx("span", { className: "mjr-status-bar__separator", "aria-hidden": "true", children: "|" }),
821
+ /* @__PURE__ */ jsx("span", { className: "mjr-status-bar__size", children: formatBytes(stats.byteSize) })
822
+ ] })
823
+ ]
824
+ }
825
+ );
826
+ };
827
+ function formatBytes(bytes) {
828
+ if (bytes < 1024) return `${bytes} B`;
829
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
830
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
831
+ }
832
+
833
+ // src/core/parser.ts
834
+ function parseJson(text) {
835
+ if (text.trim() === "") {
836
+ return { value: void 0, error: null };
837
+ }
838
+ try {
839
+ const value = JSON.parse(text);
840
+ return { value, error: null };
841
+ } catch (e) {
842
+ const error = extractParseError(e, text);
843
+ return { value: void 0, error };
844
+ }
845
+ }
846
+ function extractParseError(err, text) {
847
+ const message = err.message;
848
+ let offset = -1;
849
+ let line = 1;
850
+ let column = 1;
851
+ const posMatch = message.match(/at position (\d+)/);
852
+ const lineColMatch = message.match(/at line (\d+) column (\d+)/);
853
+ if (lineColMatch) {
854
+ line = parseInt(lineColMatch[1], 10);
855
+ column = parseInt(lineColMatch[2], 10);
856
+ offset = getOffsetFromLineCol(text, line, column);
857
+ } else if (posMatch) {
858
+ offset = parseInt(posMatch[1], 10);
859
+ const loc = getLineColFromOffset(text, offset);
860
+ line = loc.line;
861
+ column = loc.column;
862
+ }
863
+ const cleanMessage = message.replace(/^JSON\.parse: /, "").replace(/ at position \d+.*$/, "").replace(/ at line \d+ column \d+.*$/, "");
864
+ return {
865
+ message: cleanMessage || "Invalid JSON",
866
+ line,
867
+ column,
868
+ offset: Math.max(0, offset)
869
+ };
870
+ }
871
+ function getLineColFromOffset(text, offset) {
872
+ let line = 1;
873
+ let lastNewline = -1;
874
+ for (let i = 0; i < offset && i < text.length; i++) {
875
+ if (text[i] === "\n") {
876
+ line++;
877
+ lastNewline = i;
878
+ }
879
+ }
880
+ return { line, column: offset - lastNewline };
881
+ }
882
+ function getOffsetFromLineCol(text, line, column) {
883
+ let currentLine = 1;
884
+ for (let i = 0; i < text.length; i++) {
885
+ if (currentLine === line) {
886
+ return i + column - 1;
887
+ }
888
+ if (text[i] === "\n") {
889
+ currentLine++;
890
+ }
891
+ }
892
+ return text.length;
893
+ }
894
+ function stringifyJson(value, indent = 2) {
895
+ if (value === void 0) return "";
896
+ try {
897
+ return JSON.stringify(value, null, indent);
898
+ } catch {
899
+ return "";
900
+ }
901
+ }
902
+ function isValidJson(text) {
903
+ try {
904
+ JSON.parse(text);
905
+ return true;
906
+ } catch {
907
+ return false;
908
+ }
909
+ }
910
+
911
+ // src/core/validator.ts
912
+ function validateSchema(value, schema, path = "$") {
913
+ const errors = [];
914
+ if (value === void 0 || value === null) {
915
+ if (schema.type && schema.type !== "null") {
916
+ errors.push(makeError(`Expected type "${schema.type}", got null`, path, "type", schema.type));
917
+ }
918
+ return { valid: errors.length === 0, errors };
919
+ }
920
+ if (schema.type) {
921
+ const actualType = getJsonType(value);
922
+ const allowedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
923
+ const typeMatches = allowedTypes.includes(actualType) || actualType === "integer" && allowedTypes.includes("number");
924
+ if (!typeMatches) {
925
+ errors.push(
926
+ makeError(
927
+ `Expected type "${allowedTypes.join(" | ")}", got "${actualType}"`,
928
+ path,
929
+ "type",
930
+ schema.type,
931
+ value
932
+ )
933
+ );
934
+ }
935
+ }
936
+ if (schema.enum && Array.isArray(schema.enum)) {
937
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
938
+ errors.push(
939
+ makeError(
940
+ `Value must be one of: ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}`,
941
+ path,
942
+ "enum",
943
+ schema.enum,
944
+ value
945
+ )
946
+ );
947
+ }
948
+ }
949
+ if (typeof value === "string") {
950
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
951
+ errors.push(
952
+ makeError(
953
+ `String must be at least ${schema.minLength} characters`,
954
+ path,
955
+ "minLength",
956
+ schema.minLength,
957
+ value
958
+ )
959
+ );
960
+ }
961
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
962
+ errors.push(
963
+ makeError(
964
+ `String must be at most ${schema.maxLength} characters`,
965
+ path,
966
+ "maxLength",
967
+ schema.maxLength,
968
+ value
969
+ )
970
+ );
971
+ }
972
+ if (schema.pattern) {
973
+ const re = new RegExp(schema.pattern);
974
+ if (!re.test(value)) {
975
+ errors.push(
976
+ makeError(`String must match pattern "${schema.pattern}"`, path, "pattern", schema.pattern, value)
977
+ );
978
+ }
979
+ }
980
+ }
981
+ if (typeof value === "number") {
982
+ if (schema.minimum !== void 0 && value < schema.minimum) {
983
+ errors.push(
984
+ makeError(`Value must be >= ${schema.minimum}`, path, "minimum", schema.minimum, value)
985
+ );
986
+ }
987
+ if (schema.maximum !== void 0 && value > schema.maximum) {
988
+ errors.push(
989
+ makeError(`Value must be <= ${schema.maximum}`, path, "maximum", schema.maximum, value)
990
+ );
991
+ }
992
+ if (schema.exclusiveMinimum !== void 0 && value <= schema.exclusiveMinimum) {
993
+ errors.push(
994
+ makeError(
995
+ `Value must be > ${schema.exclusiveMinimum}`,
996
+ path,
997
+ "exclusiveMinimum",
998
+ schema.exclusiveMinimum,
999
+ value
1000
+ )
1001
+ );
1002
+ }
1003
+ if (schema.exclusiveMaximum !== void 0 && value >= schema.exclusiveMaximum) {
1004
+ errors.push(
1005
+ makeError(
1006
+ `Value must be < ${schema.exclusiveMaximum}`,
1007
+ path,
1008
+ "exclusiveMaximum",
1009
+ schema.exclusiveMaximum,
1010
+ value
1011
+ )
1012
+ );
1013
+ }
1014
+ if (schema.multipleOf !== void 0 && value % schema.multipleOf !== 0) {
1015
+ errors.push(
1016
+ makeError(`Value must be a multiple of ${schema.multipleOf}`, path, "multipleOf", schema.multipleOf, value)
1017
+ );
1018
+ }
1019
+ }
1020
+ if (Array.isArray(value)) {
1021
+ if (schema.minItems !== void 0 && value.length < schema.minItems) {
1022
+ errors.push(
1023
+ makeError(`Array must have at least ${schema.minItems} items`, path, "minItems", schema.minItems, value)
1024
+ );
1025
+ }
1026
+ if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
1027
+ errors.push(
1028
+ makeError(`Array must have at most ${schema.maxItems} items`, path, "maxItems", schema.maxItems, value)
1029
+ );
1030
+ }
1031
+ if (schema.uniqueItems && new Set(value.map((v) => JSON.stringify(v))).size !== value.length) {
1032
+ errors.push(makeError("Array items must be unique", path, "uniqueItems", true, value));
1033
+ }
1034
+ if (schema.items && !Array.isArray(schema.items)) {
1035
+ value.forEach((item, index) => {
1036
+ const result = validateSchema(item, schema.items, `${path}[${index}]`);
1037
+ errors.push(...result.errors);
1038
+ });
1039
+ }
1040
+ }
1041
+ if (isPlainObject(value)) {
1042
+ const obj = value;
1043
+ const keys = Object.keys(obj);
1044
+ if (schema.required && Array.isArray(schema.required)) {
1045
+ for (const req of schema.required) {
1046
+ if (!(req in obj)) {
1047
+ errors.push(
1048
+ makeError(`Missing required property "${req}"`, path, "required", schema.required)
1049
+ );
1050
+ }
1051
+ }
1052
+ }
1053
+ if (schema.minProperties !== void 0 && keys.length < schema.minProperties) {
1054
+ errors.push(
1055
+ makeError(
1056
+ `Object must have at least ${schema.minProperties} properties`,
1057
+ path,
1058
+ "minProperties",
1059
+ schema.minProperties,
1060
+ value
1061
+ )
1062
+ );
1063
+ }
1064
+ if (schema.maxProperties !== void 0 && keys.length > schema.maxProperties) {
1065
+ errors.push(
1066
+ makeError(
1067
+ `Object must have at most ${schema.maxProperties} properties`,
1068
+ path,
1069
+ "maxProperties",
1070
+ schema.maxProperties,
1071
+ value
1072
+ )
1073
+ );
1074
+ }
1075
+ if (schema.properties) {
1076
+ const props = schema.properties;
1077
+ for (const key of keys) {
1078
+ if (props[key]) {
1079
+ const result = validateSchema(obj[key], props[key], `${path}.${key}`);
1080
+ errors.push(...result.errors);
1081
+ } else if (schema.additionalProperties === false) {
1082
+ errors.push(
1083
+ makeError(
1084
+ `Unexpected property "${key}"`,
1085
+ `${path}.${key}`,
1086
+ "additionalProperties",
1087
+ false,
1088
+ obj[key]
1089
+ )
1090
+ );
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ return { valid: errors.length === 0, errors };
1096
+ }
1097
+ async function runCustomValidators(value, validators, path = "$") {
1098
+ const results = await Promise.all(validators.map((v) => v(value, path)));
1099
+ return results.flat();
1100
+ }
1101
+ function getJsonType(value) {
1102
+ if (value === null) return "null";
1103
+ if (Array.isArray(value)) return "array";
1104
+ if (typeof value === "number") {
1105
+ return Number.isInteger(value) ? "integer" : "number";
1106
+ }
1107
+ return typeof value;
1108
+ }
1109
+ function makeError(message, path, schemaKeyword, schemaRule, actualValue, severity = "error") {
1110
+ return { message, path, severity, schemaKeyword, schemaRule, actualValue };
1111
+ }
1112
+ function isPlainObject(val) {
1113
+ return typeof val === "object" && val !== null && !Array.isArray(val);
1114
+ }
1115
+ function deepEqual(a, b) {
1116
+ if (a === b) return true;
1117
+ if (typeof a !== typeof b) return false;
1118
+ if (a === null || b === null) return false;
1119
+ if (typeof a !== "object") return false;
1120
+ return JSON.stringify(a) === JSON.stringify(b);
1121
+ }
1122
+
1123
+ // src/hooks/useJsonParser.ts
1124
+ function useJsonParser(initialValue, options = {}) {
1125
+ const { schema, validators, debounce = 300 } = options;
1126
+ const [text, setText] = useState(
1127
+ () => initialValue !== void 0 ? stringifyJson(initialValue) : ""
1128
+ );
1129
+ const [parsedValue, setParsedValue] = useState(initialValue);
1130
+ const [parseError, setParseError] = useState(null);
1131
+ const [validationErrors, setValidationErrors] = useState([]);
1132
+ const debounceTimer = useRef();
1133
+ useEffect(() => {
1134
+ const result = parseJson(text);
1135
+ setParsedValue(result.value);
1136
+ setParseError(result.error);
1137
+ if (!result.error && result.value !== void 0) {
1138
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
1139
+ debounceTimer.current = setTimeout(async () => {
1140
+ let errors = [];
1141
+ if (schema) {
1142
+ const schemaResult = validateSchema(result.value, schema);
1143
+ errors = [...schemaResult.errors];
1144
+ }
1145
+ if (validators && validators.length > 0) {
1146
+ const customErrors = await runCustomValidators(result.value, validators);
1147
+ errors = [...errors, ...customErrors];
1148
+ }
1149
+ setValidationErrors(errors);
1150
+ }, debounce);
1151
+ } else {
1152
+ setValidationErrors([]);
1153
+ }
1154
+ return () => {
1155
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
1156
+ };
1157
+ }, [text, schema, validators, debounce]);
1158
+ const handleSetText = useCallback((newText) => {
1159
+ setText(newText);
1160
+ }, []);
1161
+ const handleSetValue = useCallback((value) => {
1162
+ const newText = stringifyJson(value);
1163
+ setText(newText);
1164
+ }, []);
1165
+ const format = useCallback(
1166
+ (indent = 2) => {
1167
+ if (parsedValue !== void 0) {
1168
+ setText(stringifyJson(parsedValue, indent));
1169
+ }
1170
+ },
1171
+ [parsedValue]
1172
+ );
1173
+ return {
1174
+ text,
1175
+ parsedValue,
1176
+ parseError,
1177
+ validationErrors,
1178
+ isValid: parseError === null && validationErrors.length === 0,
1179
+ setText: handleSetText,
1180
+ setValue: handleSetValue,
1181
+ format
1182
+ };
1183
+ }
1184
+ function useUndoRedo(initialValue, options = {}) {
1185
+ const { maxHistory = 100, groupingInterval = 300 } = options;
1186
+ const historyRef = useRef([initialValue]);
1187
+ const positionRef = useRef(0);
1188
+ const lastUpdateTime = useRef(0);
1189
+ const [, forceUpdate] = useState(0);
1190
+ const rerender = useCallback(() => forceUpdate((n) => n + 1), []);
1191
+ const current = historyRef.current[positionRef.current];
1192
+ const set = useCallback(
1193
+ (value) => {
1194
+ const now = Date.now();
1195
+ const shouldGroup = now - lastUpdateTime.current < groupingInterval;
1196
+ lastUpdateTime.current = now;
1197
+ const prev = historyRef.current;
1198
+ const pos = positionRef.current;
1199
+ let newHistory;
1200
+ let newPos;
1201
+ if (shouldGroup && pos > 0) {
1202
+ newHistory = [...prev.slice(0, pos), value];
1203
+ newPos = pos;
1204
+ } else {
1205
+ newHistory = [...prev.slice(0, pos + 1), value];
1206
+ newPos = pos + 1;
1207
+ }
1208
+ if (newHistory.length > maxHistory) {
1209
+ const trimCount = newHistory.length - maxHistory;
1210
+ newHistory = newHistory.slice(trimCount);
1211
+ newPos = newPos - trimCount;
1212
+ }
1213
+ historyRef.current = newHistory;
1214
+ positionRef.current = Math.max(0, newPos);
1215
+ rerender();
1216
+ },
1217
+ [maxHistory, groupingInterval, rerender]
1218
+ );
1219
+ const undo = useCallback(() => {
1220
+ if (positionRef.current > 0) {
1221
+ positionRef.current -= 1;
1222
+ rerender();
1223
+ }
1224
+ }, [rerender]);
1225
+ const redo = useCallback(() => {
1226
+ if (positionRef.current < historyRef.current.length - 1) {
1227
+ positionRef.current += 1;
1228
+ rerender();
1229
+ }
1230
+ }, [rerender]);
1231
+ const reset = useCallback(
1232
+ (value) => {
1233
+ historyRef.current = [value];
1234
+ positionRef.current = 0;
1235
+ lastUpdateTime.current = 0;
1236
+ rerender();
1237
+ },
1238
+ [rerender]
1239
+ );
1240
+ return {
1241
+ current,
1242
+ canUndo: positionRef.current > 0,
1243
+ canRedo: positionRef.current < historyRef.current.length - 1,
1244
+ historyLength: historyRef.current.length,
1245
+ set,
1246
+ undo,
1247
+ redo,
1248
+ reset
1249
+ };
1250
+ }
1251
+ function useSearch(text) {
1252
+ const [query, setQuery] = useState("");
1253
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
1254
+ const [isActive, setIsActive] = useState(false);
1255
+ const [options, setOptionsState] = useState({
1256
+ caseSensitive: false,
1257
+ useRegex: false,
1258
+ searchKeys: true,
1259
+ searchValues: true
1260
+ });
1261
+ const matches = useMemo(() => {
1262
+ if (!query || !text) return [];
1263
+ try {
1264
+ const flags = options.caseSensitive ? "g" : "gi";
1265
+ const pattern = options.useRegex ? query : escapeRegex(query);
1266
+ const regex = new RegExp(pattern, flags);
1267
+ const results = [];
1268
+ const lines = text.split("\n");
1269
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1270
+ const line = lines[lineIdx];
1271
+ let match;
1272
+ regex.lastIndex = 0;
1273
+ while ((match = regex.exec(line)) !== null) {
1274
+ results.push({
1275
+ line: lineIdx + 1,
1276
+ columnStart: match.index,
1277
+ columnEnd: match.index + match[0].length,
1278
+ matchText: match[0]
1279
+ });
1280
+ if (match.index === regex.lastIndex) regex.lastIndex++;
1281
+ }
1282
+ }
1283
+ return results;
1284
+ } catch {
1285
+ return [];
1286
+ }
1287
+ }, [query, text, options.caseSensitive, options.useRegex]);
1288
+ const goToNext = useCallback(() => {
1289
+ if (matches.length === 0) return;
1290
+ setCurrentMatchIndex((i) => (i + 1) % matches.length);
1291
+ }, [matches.length]);
1292
+ const goToPrevious = useCallback(() => {
1293
+ if (matches.length === 0) return;
1294
+ setCurrentMatchIndex((i) => (i - 1 + matches.length) % matches.length);
1295
+ }, [matches.length]);
1296
+ const setOptions = useCallback((opts) => {
1297
+ setOptionsState((prev) => ({ ...prev, ...opts }));
1298
+ setCurrentMatchIndex(0);
1299
+ }, []);
1300
+ const open = useCallback(() => setIsActive(true), []);
1301
+ const close = useCallback(() => {
1302
+ setIsActive(false);
1303
+ setQuery("");
1304
+ setCurrentMatchIndex(0);
1305
+ }, []);
1306
+ return {
1307
+ query,
1308
+ setQuery: (q) => {
1309
+ setQuery(q);
1310
+ setCurrentMatchIndex(0);
1311
+ },
1312
+ matches,
1313
+ currentMatchIndex,
1314
+ totalMatches: matches.length,
1315
+ goToNext,
1316
+ goToPrevious,
1317
+ options,
1318
+ setOptions,
1319
+ isActive,
1320
+ open,
1321
+ close
1322
+ };
1323
+ }
1324
+ function escapeRegex(str) {
1325
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1326
+ }
1327
+
1328
+ // src/core/formatter.ts
1329
+ function formatJson(text, indent = 2) {
1330
+ try {
1331
+ const parsed = JSON.parse(text);
1332
+ const space = indent === "tab" ? " " : indent;
1333
+ return JSON.stringify(parsed, null, space);
1334
+ } catch {
1335
+ return text;
1336
+ }
1337
+ }
1338
+ function minifyJson(text) {
1339
+ try {
1340
+ return JSON.stringify(JSON.parse(text));
1341
+ } catch {
1342
+ return text;
1343
+ }
1344
+ }
1345
+ function sortJsonKeys(text, order = "asc", indent = 2) {
1346
+ try {
1347
+ const parsed = JSON.parse(text);
1348
+ const sorted = deepSortKeys(parsed, order);
1349
+ const space = indent === "tab" ? " " : indent;
1350
+ return JSON.stringify(sorted, null, space);
1351
+ } catch {
1352
+ return text;
1353
+ }
1354
+ }
1355
+ function deepSortKeys(value, order) {
1356
+ if (Array.isArray(value)) {
1357
+ return value.map((item) => deepSortKeys(item, order));
1358
+ }
1359
+ if (value !== null && typeof value === "object") {
1360
+ const obj = value;
1361
+ const comparator = typeof order === "function" ? order : order === "desc" ? (a, b) => b.localeCompare(a) : (a, b) => a.localeCompare(b);
1362
+ const sorted = {};
1363
+ const keys = Object.keys(obj).sort(comparator);
1364
+ for (const key of keys) {
1365
+ sorted[key] = deepSortKeys(obj[key], order);
1366
+ }
1367
+ return sorted;
1368
+ }
1369
+ return value;
1370
+ }
1371
+ function computeStats(value, text) {
1372
+ const stats = {
1373
+ properties: 0,
1374
+ arrays: 0,
1375
+ totalNodes: 0,
1376
+ maxDepth: 0,
1377
+ byteSize: text ? new TextEncoder().encode(text).length : 0
1378
+ };
1379
+ function walk(val, depth) {
1380
+ stats.totalNodes++;
1381
+ stats.maxDepth = Math.max(stats.maxDepth, depth);
1382
+ if (Array.isArray(val)) {
1383
+ stats.arrays++;
1384
+ val.forEach((item) => walk(item, depth + 1));
1385
+ } else if (val !== null && typeof val === "object") {
1386
+ const keys = Object.keys(val);
1387
+ stats.properties += keys.length;
1388
+ keys.forEach((key) => walk(val[key], depth + 1));
1389
+ }
1390
+ }
1391
+ if (value !== void 0) {
1392
+ walk(value, 0);
1393
+ }
1394
+ return stats;
1395
+ }
1396
+
1397
+ // src/themes/light.ts
1398
+ var lightTheme = {
1399
+ name: "light",
1400
+ bg: "#ffffff",
1401
+ fg: "#1e1e1e",
1402
+ border: "#e0e0e0",
1403
+ gutterBg: "#f5f5f5",
1404
+ gutterFg: "#999999",
1405
+ selection: "#add6ff",
1406
+ cursor: "#000000",
1407
+ keyColor: "#0451a5",
1408
+ stringColor: "#0a7e07",
1409
+ numberColor: "#098658",
1410
+ booleanColor: "#7a3e9d",
1411
+ nullColor: "#808080",
1412
+ bracketColor: "#333333",
1413
+ bracketMatchBg: "#bad0f847",
1414
+ errorColor: "#e51400",
1415
+ warningColor: "#bf8803",
1416
+ successColor: "#16825d",
1417
+ treeLine: "#d0d0d0",
1418
+ treeHover: "#f0f4ff",
1419
+ treeSelected: "#e0ecff",
1420
+ typeBadgeBg: "#eef2f7"
1421
+ };
1422
+
1423
+ // src/themes/dark.ts
1424
+ var darkTheme = {
1425
+ name: "dark",
1426
+ bg: "#1e1e1e",
1427
+ fg: "#d4d4d4",
1428
+ border: "#3c3c3c",
1429
+ gutterBg: "#252526",
1430
+ gutterFg: "#858585",
1431
+ selection: "#264f78",
1432
+ cursor: "#ffffff",
1433
+ keyColor: "#9cdcfe",
1434
+ stringColor: "#ce9178",
1435
+ numberColor: "#b5cea8",
1436
+ booleanColor: "#569cd6",
1437
+ nullColor: "#808080",
1438
+ bracketColor: "#cccccc",
1439
+ bracketMatchBg: "#0064001a",
1440
+ errorColor: "#f14c4c",
1441
+ warningColor: "#cca700",
1442
+ successColor: "#89d185",
1443
+ treeLine: "#4a4a4a",
1444
+ treeHover: "#2a2d2e",
1445
+ treeSelected: "#094771",
1446
+ typeBadgeBg: "#2d2d30"
1447
+ };
1448
+ var JsonEditor = ({
1449
+ value: externalValue,
1450
+ onChange,
1451
+ mode: controlledMode,
1452
+ onModeChange,
1453
+ schema,
1454
+ validators,
1455
+ validationMode = "onChange",
1456
+ onValidate,
1457
+ theme = "light",
1458
+ height = 400,
1459
+ readOnly = false,
1460
+ searchable = true,
1461
+ sortable = true,
1462
+ indentation = 2,
1463
+ lineNumbers = true,
1464
+ bracketMatching = true,
1465
+ maxSize,
1466
+ virtualize = "auto",
1467
+ onError,
1468
+ onFocus,
1469
+ onBlur,
1470
+ className = "",
1471
+ style,
1472
+ "aria-label": ariaLabel = "JSON Editor"
1473
+ }) => {
1474
+ const [internalMode, setInternalMode] = useState("code");
1475
+ const mode = controlledMode ?? internalMode;
1476
+ const handleModeChange = useCallback(
1477
+ (newMode) => {
1478
+ if (onModeChange) onModeChange(newMode);
1479
+ else setInternalMode(newMode);
1480
+ },
1481
+ [onModeChange]
1482
+ );
1483
+ const parser = useJsonParser(externalValue, {
1484
+ schema,
1485
+ validators,
1486
+ debounce: validationMode === "onChange" ? 300 : void 0
1487
+ });
1488
+ const history = useUndoRedo(parser.text, { maxHistory: 100 });
1489
+ useEffect(() => {
1490
+ if (externalValue !== void 0) {
1491
+ const text = typeof externalValue === "string" ? externalValue : JSON.stringify(externalValue, null, indentation === "tab" ? " " : indentation);
1492
+ if (text !== parser.text) {
1493
+ parser.setText(text);
1494
+ history.reset(text);
1495
+ }
1496
+ }
1497
+ }, [externalValue]);
1498
+ const handleTextChange = useCallback(
1499
+ (text) => {
1500
+ parser.setText(text);
1501
+ history.set(text);
1502
+ if (onChange) {
1503
+ try {
1504
+ const parsed = JSON.parse(text);
1505
+ onChange(parsed, text);
1506
+ } catch {
1507
+ onChange(void 0, text);
1508
+ }
1509
+ }
1510
+ },
1511
+ [parser, history, onChange]
1512
+ );
1513
+ const handleTreeChange = useCallback(
1514
+ (newValue) => {
1515
+ const indent = indentation === "tab" ? " " : indentation;
1516
+ const text = JSON.stringify(newValue, null, indent);
1517
+ handleTextChange(text);
1518
+ },
1519
+ [handleTextChange, indentation]
1520
+ );
1521
+ useEffect(() => {
1522
+ if (onValidate) {
1523
+ onValidate(parser.validationErrors);
1524
+ }
1525
+ }, [parser.validationErrors, onValidate]);
1526
+ const search = useSearch(parser.text);
1527
+ const [cursor, setCursor] = useState({ line: 1, column: 1, offset: 0 });
1528
+ const stats = useMemo(
1529
+ () => parser.parsedValue !== void 0 ? computeStats(parser.parsedValue, parser.text) : null,
1530
+ [parser.parsedValue, parser.text]
1531
+ );
1532
+ const resolvedTheme = useMemo(() => {
1533
+ if (typeof theme === "object") return theme;
1534
+ if (theme === "dark") return darkTheme;
1535
+ if (theme === "auto") {
1536
+ if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
1537
+ return darkTheme;
1538
+ }
1539
+ return lightTheme;
1540
+ }
1541
+ return lightTheme;
1542
+ }, [theme]);
1543
+ const themeVars = useMemo(() => {
1544
+ const vars = {};
1545
+ vars["--mjr-bg"] = resolvedTheme.bg;
1546
+ vars["--mjr-fg"] = resolvedTheme.fg;
1547
+ vars["--mjr-border"] = resolvedTheme.border;
1548
+ vars["--mjr-gutter-bg"] = resolvedTheme.gutterBg;
1549
+ vars["--mjr-gutter-fg"] = resolvedTheme.gutterFg;
1550
+ vars["--mjr-selection"] = resolvedTheme.selection;
1551
+ vars["--mjr-cursor"] = resolvedTheme.cursor;
1552
+ vars["--mjr-key"] = resolvedTheme.keyColor;
1553
+ vars["--mjr-string"] = resolvedTheme.stringColor;
1554
+ vars["--mjr-number"] = resolvedTheme.numberColor;
1555
+ vars["--mjr-boolean"] = resolvedTheme.booleanColor;
1556
+ vars["--mjr-null"] = resolvedTheme.nullColor;
1557
+ vars["--mjr-bracket"] = resolvedTheme.bracketColor;
1558
+ vars["--mjr-bracket-match"] = resolvedTheme.bracketMatchBg;
1559
+ vars["--mjr-error"] = resolvedTheme.errorColor;
1560
+ vars["--mjr-warning"] = resolvedTheme.warningColor;
1561
+ vars["--mjr-success"] = resolvedTheme.successColor;
1562
+ vars["--mjr-tree-line"] = resolvedTheme.treeLine;
1563
+ vars["--mjr-tree-hover"] = resolvedTheme.treeHover;
1564
+ vars["--mjr-tree-selected"] = resolvedTheme.treeSelected;
1565
+ vars["--mjr-type-badge-bg"] = resolvedTheme.typeBadgeBg;
1566
+ return vars;
1567
+ }, [resolvedTheme]);
1568
+ useEffect(() => {
1569
+ const handleKeyDown = (e) => {
1570
+ const mod = e.metaKey || e.ctrlKey;
1571
+ if (mod && e.key === "z" && !e.shiftKey) {
1572
+ e.preventDefault();
1573
+ history.undo();
1574
+ const undoneText = history.current;
1575
+ if (undoneText !== void 0) {
1576
+ parser.setText(undoneText);
1577
+ }
1578
+ }
1579
+ if (mod && e.key === "z" && e.shiftKey) {
1580
+ e.preventDefault();
1581
+ history.redo();
1582
+ const redoneText = history.current;
1583
+ if (redoneText !== void 0) {
1584
+ parser.setText(redoneText);
1585
+ }
1586
+ }
1587
+ if (mod && e.key === "f") {
1588
+ e.preventDefault();
1589
+ if (searchable) search.open();
1590
+ }
1591
+ if (e.key === "Escape") {
1592
+ search.close();
1593
+ }
1594
+ };
1595
+ document.addEventListener("keydown", handleKeyDown);
1596
+ return () => document.removeEventListener("keydown", handleKeyDown);
1597
+ }, [history, parser, search, searchable]);
1598
+ const heightStyle = typeof height === "number" ? `${height}px` : height;
1599
+ return /* @__PURE__ */ jsxs(
1600
+ "div",
1601
+ {
1602
+ className: `mjr-editor ${className}`,
1603
+ style: { ...themeVars, height: heightStyle, ...style },
1604
+ role: "application",
1605
+ "aria-label": ariaLabel,
1606
+ onFocus,
1607
+ onBlur,
1608
+ "data-testid": "json-editor",
1609
+ children: [
1610
+ /* @__PURE__ */ jsx(
1611
+ Toolbar,
1612
+ {
1613
+ mode,
1614
+ onModeChange: handleModeChange,
1615
+ canUndo: history.canUndo,
1616
+ canRedo: history.canRedo,
1617
+ onUndo: () => {
1618
+ history.undo();
1619
+ },
1620
+ onRedo: () => {
1621
+ history.redo();
1622
+ },
1623
+ onFormat: () => parser.format(indentation === "tab" ? " " : indentation),
1624
+ searchable,
1625
+ isSearchOpen: search.isActive,
1626
+ onToggleSearch: () => search.isActive ? search.close() : search.open(),
1627
+ readOnly
1628
+ }
1629
+ ),
1630
+ /* @__PURE__ */ jsxs("div", { className: "mjr-editor__content", children: [
1631
+ (mode === "code" || mode === "split") && /* @__PURE__ */ jsx("div", { className: `mjr-editor__panel ${mode === "split" ? "mjr-editor__panel--half" : ""}`, children: /* @__PURE__ */ jsx(
1632
+ CodeEditor,
1633
+ {
1634
+ value: parser.text,
1635
+ onChange: handleTextChange,
1636
+ parseError: parser.parseError,
1637
+ readOnly,
1638
+ lineNumbers,
1639
+ bracketMatching,
1640
+ searchMatches: search.matches,
1641
+ currentMatchIndex: search.currentMatchIndex,
1642
+ onCursorChange: setCursor
1643
+ }
1644
+ ) }),
1645
+ (mode === "tree" || mode === "split") && /* @__PURE__ */ jsx("div", { className: `mjr-editor__panel ${mode === "split" ? "mjr-editor__panel--half" : ""}`, children: /* @__PURE__ */ jsx(
1646
+ TreeEditor,
1647
+ {
1648
+ value: parser.parsedValue,
1649
+ onChange: handleTreeChange,
1650
+ readOnly
1651
+ }
1652
+ ) })
1653
+ ] }),
1654
+ /* @__PURE__ */ jsx(
1655
+ StatusBar,
1656
+ {
1657
+ parseError: parser.parseError,
1658
+ validationErrors: parser.validationErrors,
1659
+ cursor,
1660
+ stats
1661
+ }
1662
+ )
1663
+ ]
1664
+ }
1665
+ );
1666
+ };
1667
+
1668
+ export { JsonEditor, buildPath, computeStats, darkTheme, deleteByPath, formatJson, getByPath, isValidJson, lightTheme, minifyJson, parseJson, parsePath, runCustomValidators, setByPath, sortJsonKeys, stringifyJson, useJsonParser, useSearch, useUndoRedo, validateSchema };
1669
+ //# sourceMappingURL=index.mjs.map
1670
+ //# sourceMappingURL=index.mjs.map