@jbpark/live-editor 4.3.1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +2 -2
  2. package/dist/{boundary-B17cepvm.js → boundary-DnjGgsK6.js} +8 -5
  3. package/dist/boundary-DnjGgsK6.js.map +1 -0
  4. package/dist/{compile-0Dvp8zp5.js → compile-DDfk5P25.js} +3 -3
  5. package/dist/{compile-0Dvp8zp5.js.map → compile-DDfk5P25.js.map} +1 -1
  6. package/dist/{constants-CKvdqZtO.js → constants-h1JAgOeW.js} +4 -2
  7. package/dist/constants-h1JAgOeW.js.map +1 -0
  8. package/dist/{context-93X3gyfJ.js → context-DsEXJl4h.js} +11 -5
  9. package/dist/context-DsEXJl4h.js.map +1 -0
  10. package/dist/{core-B3ruccKb.js → core-CRsAGpei.js} +2 -2
  11. package/dist/{core-B3ruccKb.js.map → core-CRsAGpei.js.map} +1 -1
  12. package/dist/{detect-typescript-5MV4mCM7.js → detect-typescript-CCj9ALXc.js} +2 -2
  13. package/dist/{detect-typescript-5MV4mCM7.js.map → detect-typescript-CCj9ALXc.js.map} +1 -1
  14. package/dist/dnd/index.d.ts +2 -2
  15. package/dist/dnd/index.js +1 -1
  16. package/dist/{dnd-x8Mvj--6.js → dnd-DGNp20Nk.js} +496 -307
  17. package/dist/dnd-DGNp20Nk.js.map +1 -0
  18. package/dist/{document-C-hGKMkk.js → document-DGHTfrda.js} +7739 -7736
  19. package/dist/document-DGHTfrda.js.map +1 -0
  20. package/dist/editor/index.js +2 -2
  21. package/dist/{editor-DvMruljw.js → editor-DQ8Q--qR.js} +2 -2
  22. package/dist/{editor-DvMruljw.js.map → editor-DQ8Q--qR.js.map} +1 -1
  23. package/dist/error/index.d.ts +1 -1
  24. package/dist/error/index.js +2 -2
  25. package/dist/{error-DWhxQEom.js → error-DwqwjJ2C.js} +13 -6
  26. package/dist/error-DwqwjJ2C.js.map +1 -0
  27. package/dist/index-CHknA6k1.d.ts +221 -0
  28. package/dist/{index-CtlUYnWq.d.ts → index-P9Aifaui.d.ts} +14 -4
  29. package/dist/index-WQgFy510.d.ts +11 -0
  30. package/dist/{index-C4SGQ3Ci.d.ts → index-vtyggWy0.d.ts} +4 -1
  31. package/dist/index.d.ts +18 -17
  32. package/dist/index.js +12 -8
  33. package/dist/index.js.map +1 -1
  34. package/dist/{ast-CehbCXwh.js → items-DdrwGxJw.js} +467 -426
  35. package/dist/items-DdrwGxJw.js.map +1 -0
  36. package/dist/messages-Cp3g1mn1.d.ts +129 -0
  37. package/dist/messages-CzUPVnzH.js +132 -0
  38. package/dist/messages-CzUPVnzH.js.map +1 -0
  39. package/dist/preview/index.js +2 -2
  40. package/dist/{preview-BYck9mVK.js → preview-BKFgq_5u.js} +6 -4
  41. package/dist/preview-BKFgq_5u.js.map +1 -0
  42. package/dist/provider/index.d.ts +3 -8
  43. package/dist/provider/index.js +3 -2
  44. package/dist/{sections-Ce63wbtG.js → sections-BRH3KHYc.js} +9 -4
  45. package/dist/sections-BRH3KHYc.js.map +1 -0
  46. package/dist/style.css +505 -21
  47. package/dist/{use-dynamic-tailwind-QKcGvUJs.js → use-dynamic-tailwind-T9r9ExbR.js} +21 -4
  48. package/dist/use-dynamic-tailwind-T9r9ExbR.js.map +1 -0
  49. package/dist/utils/ast/index.d.ts +3 -2
  50. package/dist/utils/ast/index.js +66 -3
  51. package/dist/utils/ast/index.js.map +1 -0
  52. package/dist/utils/index.d.ts +30 -13
  53. package/dist/utils/index.js +26 -6
  54. package/dist/utils/index.js.map +1 -0
  55. package/dist/validate-DB59Z_Nx.d.ts +100 -0
  56. package/dist/validate-iLo8IONY.js +64 -0
  57. package/dist/validate-iLo8IONY.js.map +1 -0
  58. package/package.json +3 -3
  59. package/dist/ast-CehbCXwh.js.map +0 -1
  60. package/dist/boundary-B17cepvm.js.map +0 -1
  61. package/dist/constants-CKvdqZtO.js.map +0 -1
  62. package/dist/context-93X3gyfJ.js.map +0 -1
  63. package/dist/dnd-x8Mvj--6.js.map +0 -1
  64. package/dist/document-C-hGKMkk.js.map +0 -1
  65. package/dist/error-DWhxQEom.js.map +0 -1
  66. package/dist/index-w3kLcCXl.d.ts +0 -255
  67. package/dist/preview-BYck9mVK.js.map +0 -1
  68. package/dist/sections-Ce63wbtG.js.map +0 -1
  69. package/dist/use-dynamic-tailwind-QKcGvUJs.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { C as require_lib, S as require_lib$1, _ as nanoid, b as unwrap, d as traverse, g as getJSXTagName, v as attrValue, w as __toESM, x as wrap, y as generateCode } from "./document-C-hGKMkk.js";
2
- import { c as RESERVED_BINDING_PROPERTIES, n as CONFIG, r as DATA_ATTR, s as REGEX, t as BINDING_PROP } from "./constants-CKvdqZtO.js";
1
+ import { C as require_lib, S as require_lib$1, _ as generateCode, b as getJSXTagName, d as traverse, g as attrValue, v as unwrap, w as __toESM, x as nanoid, y as wrap } from "./document-DGHTfrda.js";
2
+ import { c as RESERVED_BINDING_PROPERTIES, n as CONFIG, r as DATA_ATTR, s as REGEX, t as BINDING_PROP } from "./constants-h1JAgOeW.js";
3
3
  import { n as registerEditorCache } from "./editor-caches-ClMvdcQF.js";
4
4
  import { t as createBoundedCache } from "./cache-B0-XcIxJ.js";
5
5
  import { z } from "zod";
@@ -372,6 +372,63 @@ const buildBindingItems = (raw) => {
372
372
  }];
373
373
  });
374
374
  };
375
+ const registryEntries = /* @__PURE__ */ new WeakMap();
376
+ const isComponentTagName = (tagName) => tagName !== "" && (tagName.includes(".") || !/^[a-z]/.test(tagName));
377
+ const getRegistryBindings = (registry, tagName) => {
378
+ if (!registry) return;
379
+ let entries = registryEntries.get(registry);
380
+ if (!entries) {
381
+ entries = /* @__PURE__ */ new Map();
382
+ registryEntries.set(registry, entries);
383
+ const ignored = Object.keys(registry).filter((key) => !isComponentTagName(key));
384
+ if (ignored.length > 0) console.warn(`Live.Dnd \`bindings\` only applies to components. Ignored: ${ignored.join(", ")}. Bind an HTML element with its own data-binding.`);
385
+ }
386
+ if (!isComponentTagName(tagName) || !Object.hasOwn(registry, tagName)) return;
387
+ let bindings = entries.get(tagName);
388
+ if (!bindings) {
389
+ bindings = buildBindingItems(registry[tagName]);
390
+ entries.set(tagName, bindings);
391
+ }
392
+ return bindings;
393
+ };
394
+ const keyEntries = /* @__PURE__ */ new WeakMap();
395
+ const warnedKeys = /* @__PURE__ */ new Set();
396
+ const warnOnce = (id, message) => {
397
+ if (!warnedKeys.has(id)) {
398
+ warnedKeys.add(id);
399
+ console.warn(message);
400
+ }
401
+ };
402
+ const getKeyBindings = (bindingKeys, key) => {
403
+ if (!bindingKeys || !Object.hasOwn(bindingKeys, key)) return;
404
+ let entries = keyEntries.get(bindingKeys);
405
+ if (!entries) {
406
+ entries = /* @__PURE__ */ new Map();
407
+ keyEntries.set(bindingKeys, entries);
408
+ }
409
+ let bindings = entries.get(key);
410
+ if (!bindings) {
411
+ bindings = buildBindingItems(bindingKeys[key]);
412
+ entries.set(key, bindings);
413
+ }
414
+ return bindings;
415
+ };
416
+ const resolveBindings = (own, key, tagName, options = {}) => {
417
+ if (own) {
418
+ if (key !== void 0) warnOnce(`both:${key}`, `Live.Dnd: an element has both data-binding and data-binding-key="${key ?? ""}". Its data-binding is used; remove one of them.`);
419
+ return own;
420
+ }
421
+ if (key === null) {
422
+ warnOnce("not-a-string", "Live.Dnd: data-binding-key must be a plain string, such as data-binding-key=\"hero-title\". The element has no fields.");
423
+ return [];
424
+ }
425
+ if (key !== void 0) {
426
+ const bindings = getKeyBindings(options.bindingKeys, key);
427
+ if (!bindings) warnOnce(`missing:${key}`, `Live.Dnd: data-binding-key="${key}" isn't in bindingKeys. The element has no fields.`);
428
+ return bindings ?? [];
429
+ }
430
+ return getRegistryBindings(options.bindings, tagName) ?? [];
431
+ };
375
432
  const parseBinding = (bindingValue) => {
376
433
  if (!bindingValue) return [];
377
434
  const ast = parseArrayExpression(bindingValue);
@@ -420,11 +477,12 @@ const getStructuredValue = (node, property, type) => {
420
477
  }
421
478
  }
422
479
  };
423
- const hasEditableBindings = (node) => {
480
+ const readNodeBindings = (node) => {
481
+ if (node.bindings) return node.bindings;
424
482
  const bindingAttr = node.dataAttributes.find((attr) => attr.name === DATA_ATTR.BINDING);
425
- if (!bindingAttr?.value) return false;
426
- return (node.bindings || parseBinding(bindingAttr.value)).length > 0;
483
+ return bindingAttr?.value ? parseBinding(bindingAttr.value) : [];
427
484
  };
485
+ const hasEditableBindings = (node) => readNodeBindings(node).length > 0;
428
486
  const findEditableChildren = (node) => {
429
487
  const editableChildren = [];
430
488
  const traverse = (children) => {
@@ -438,337 +496,6 @@ const findEditableChildren = (node) => {
438
496
  return editableChildren;
439
497
  };
440
498
  //#endregion
441
- //#region src/utils/selection.ts
442
- const removeIndices = (items, indices) => {
443
- return items.filter((_, index) => !indices.has(index));
444
- };
445
- /**
446
- * Shifts every selected index up/down by one step as a block, preserving
447
- * relative order — scattered selections stop moving individually once they
448
- * hit an unselected neighbor, so the whole group slides together instead of
449
- * items passing through each other.
450
- */
451
- const moveSelectedIndices = (items, indices, direction) => {
452
- const next = [...items];
453
- const nextIndices = new Set(indices);
454
- const ordered = [...indices].sort((a, b) => direction === "up" ? a - b : b - a);
455
- for (const index of ordered) {
456
- const target = direction === "up" ? index - 1 : index + 1;
457
- if (target < 0 || target >= next.length || nextIndices.has(target)) continue;
458
- [next[index], next[target]] = [next[target], next[index]];
459
- nextIndices.delete(index);
460
- nextIndices.add(target);
461
- }
462
- return {
463
- items: next,
464
- indices: nextIndices
465
- };
466
- };
467
- //#endregion
468
- //#region src/utils/ast/patch.ts
469
- const hasOnlyLayoutNewlines = (content) => {
470
- let index = 0;
471
- while (index < content.length) {
472
- const char = content[index];
473
- if (char === "`" || char === "<") return false;
474
- if (char === "\"" || char === "'") {
475
- const next = skipStringLiteral(content, index);
476
- if (next === -1) return false;
477
- index = next;
478
- continue;
479
- }
480
- if (char === "/") {
481
- const following = content[index + 1];
482
- if (following === "/") {
483
- const lineEnd = content.indexOf("\n", index + 2);
484
- index = lineEnd === -1 ? content.length : lineEnd;
485
- continue;
486
- }
487
- if (following === "*") {
488
- const commentEnd = content.indexOf("*/", index + 2);
489
- if (commentEnd === -1) return false;
490
- index = commentEnd + 2;
491
- continue;
492
- }
493
- return false;
494
- }
495
- index++;
496
- }
497
- return true;
498
- };
499
- const skipStringLiteral = (content, start) => {
500
- const quote = content[start];
501
- for (let index = start + 1; index < content.length; index++) {
502
- const char = content[index];
503
- if (char === "\\") {
504
- const escaped = content[index + 1];
505
- if (escaped === "\n" || escaped === "\r") return -1;
506
- index++;
507
- continue;
508
- }
509
- if (char === quote) return index + 1;
510
- if (char === "\n") return -1;
511
- }
512
- return -1;
513
- };
514
- const lineIndentAt = (source, offset) => {
515
- const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
516
- return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
517
- };
518
- const lineTerminatorOf = (source) => {
519
- return source.includes("\r\n") ? "\r\n" : "\n";
520
- };
521
- const applyEdits = (source, edits) => {
522
- if (edits.length === 0) return source;
523
- const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
524
- for (const edit of ordered) if (!Number.isInteger(edit.start) || !Number.isInteger(edit.end) || edit.start < 0 || edit.end > source.length || edit.start > edit.end) throw new Error(`Invalid source edit [${edit.start}, ${edit.end}) for a source of length ${source.length}`);
525
- for (let i = 1; i < ordered.length; i++) {
526
- const previous = ordered[i - 1];
527
- const current = ordered[i];
528
- if (current.start < previous.end) throw new Error(`Overlapping source edits: [${previous.start}, ${previous.end}) and [${current.start}, ${current.end})`);
529
- }
530
- let result = "";
531
- let cursor = 0;
532
- const terminator = lineTerminatorOf(source);
533
- for (const edit of ordered) {
534
- const content = edit.indent && edit.content.includes("\n") && hasOnlyLayoutNewlines(edit.content) ? edit.content.replace(/\n/g, `${terminator}${lineIndentAt(source, edit.start)}`) : edit.content;
535
- result += source.slice(cursor, edit.start) + content;
536
- cursor = edit.end;
537
- }
538
- return result + source.slice(cursor);
539
- };
540
- //#endregion
541
- //#region src/utils/ast/array-source.ts
542
- const commaIn = (source, start, end) => {
543
- let comma = null;
544
- for (let at = start; at < end; at++) {
545
- if (/\s/.test(source[at])) continue;
546
- if (source.startsWith("/*", at)) {
547
- const close = source.indexOf("*/", at + 2);
548
- if (close < 0 || close + 2 > end) return null;
549
- at = close + 1;
550
- } else if (source.startsWith("//", at)) {
551
- const newline = source.indexOf("\n", at + 2);
552
- at = newline < 0 ? end : newline;
553
- } else if (source[at] === "," && comma === null) comma = at;
554
- else return null;
555
- }
556
- return comma;
557
- };
558
- const denseArraySource = (source) => {
559
- const array = parseArrayExpression(source);
560
- if (!array || array.elements.some((node) => !import_lib$1.isExpression(node) || node.extra?.parenthesized)) return null;
561
- const elements = array.elements;
562
- const commas = elements.slice(0, -1).map((node, index) => commaIn(source, node.end, elements[index + 1].start));
563
- if (commas.some((comma) => comma === null)) return null;
564
- const last = elements.at(-1);
565
- return {
566
- array,
567
- elements,
568
- commas,
569
- trailing: last ? commaIn(source, last.end, array.end - 1) : null
570
- };
571
- };
572
- const canStructurallyEditArray = (source) => denseArraySource(source) !== null;
573
- const validArrayIndices = (indices, length) => [...indices].every((index) => Number.isInteger(index) && index >= 0 && index < length);
574
- const reorderArraySource = (source, elements, next) => applyEdits(source, elements.map((node, index) => ({
575
- start: node.start,
576
- end: node.end,
577
- content: source.slice(next[index].start, next[index].end)
578
- })));
579
- const removeArraySource = (source, data, indices) => {
580
- const edits = [];
581
- const removedCommas = /* @__PURE__ */ new Set();
582
- const { elements, commas, trailing } = data;
583
- for (let index = 0; index < elements.length; index++) {
584
- if (!indices.has(index)) continue;
585
- const start = index;
586
- while (index + 1 < elements.length && indices.has(index + 1)) index++;
587
- for (let current = start; current <= index; current++) {
588
- const node = elements[current];
589
- edits.push({
590
- start: node.start,
591
- end: node.end,
592
- content: ""
593
- });
594
- if (current < elements.length - 1) removedCommas.add(commas[current]);
595
- }
596
- if (index === elements.length - 1) {
597
- if (trailing !== null) removedCommas.add(trailing);
598
- else if (start > 0) removedCommas.add(commas[start - 1]);
599
- }
600
- }
601
- for (const comma of removedCommas) edits.push({
602
- start: comma,
603
- end: comma + 1,
604
- content: ""
605
- });
606
- return applyEdits(source, edits);
607
- };
608
- const appendArraySource = (source, data, copies) => {
609
- const last = data.elements.at(-1);
610
- if (!last || copies.length === 0) return source;
611
- return applyEdits(source, [{
612
- start: last.end,
613
- end: last.end,
614
- content: `, ${copies.join(", ")}`
615
- }]);
616
- };
617
- //#endregion
618
- //#region src/utils/ast/items.ts
619
- const elementsOf = (code) => parseArrayExpression(code)?.elements ?? null;
620
- const kindOf = (element) => import_lib$1.isObjectExpression(element) ? "object" : "primitive";
621
- const parseItems = (code) => {
622
- return elementsOf(code)?.flatMap((node, index) => import_lib$1.isExpression(node) ? [{
623
- index,
624
- kind: kindOf(node),
625
- node
626
- }] : []) ?? null;
627
- };
628
- const resolveRenderLeaf = (render, key) => {
629
- const leaf = render?.[key];
630
- return leaf && "type" in leaf ? leaf : null;
631
- };
632
- const toTemplateRaw = (value) => {
633
- return value.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${").replace(/\r/g, "\\r");
634
- };
635
- const buildPropertyValue = (value, declaredType, renderLeaf, current) => {
636
- if (renderLeaf?.property === BINDING_PROP.INNER_HTML) {
637
- const raw = String(value);
638
- return import_lib$1.templateLiteral([import_lib$1.templateElement({
639
- raw: toTemplateRaw(raw),
640
- cooked: raw
641
- }, true)], []);
642
- }
643
- if (renderLeaf?.type === "jsx" || import_lib$1.isJSXElement(current) || import_lib$1.isJSXFragment(current)) {
644
- const trimmed = String(value).trim();
645
- if (!trimmed.startsWith("<")) return import_lib$1.stringLiteral(trimmed);
646
- try {
647
- return (0, import_lib.parseExpression)(trimmed, { plugins: ["jsx", "typescript"] });
648
- } catch {
649
- return;
650
- }
651
- }
652
- if (declaredType === "array" || declaredType === "object") {
653
- if (typeof value === "string") try {
654
- return (0, import_lib.parseExpression)(value, { plugins: ["jsx", "typescript"] });
655
- } catch {
656
- return;
657
- }
658
- if (!isLosslesslyEvaluable(current)) return;
659
- return valueToExpression(value) ?? void 0;
660
- }
661
- return createNodeFromValue(declaredType, parseValue(value)) ?? void 0;
662
- };
663
- const updateArrayItemProperty = (code, index, key, value, render) => {
664
- const elements = elementsOf(code);
665
- const element = elements?.[index];
666
- if (!elements || !import_lib$1.isObjectExpression(element)) return null;
667
- const target = element.properties.find((property) => import_lib$1.isObjectProperty(property) && import_lib$1.isIdentifier(property.key) && property.key.name === key);
668
- if (!target) return null;
669
- const declaredType = extractObjectProperties(element)[key]?.type ?? "string";
670
- const nextValue = buildPropertyValue(value, declaredType, resolveRenderLeaf(render, key), target.value);
671
- if (!nextValue) return null;
672
- let serialized = typeof value === "string" && (declaredType === "array" || declaredType === "object" || import_lib$1.isJSXElement(nextValue) || import_lib$1.isJSXFragment(nextValue)) ? value.trim() : generateCode(nextValue);
673
- if (nextValue.trailingComments?.some((comment) => comment.type === "CommentLine")) serialized += "\n";
674
- const content = target.shorthand ? `${key}: ${serialized}` : serialized;
675
- return applyEdits(code, [{
676
- start: target.value.start,
677
- end: target.value.end,
678
- content
679
- }]);
680
- };
681
- const updateArrayItemValue = (code, index, value) => {
682
- const elements = elementsOf(code);
683
- const element = elements?.[index];
684
- if (!elements || !import_lib$1.isExpression(element)) return null;
685
- const nextValue = createNodeFromValue(extractNodeValue(element).type, parseValue(value));
686
- if (!nextValue) return null;
687
- return applyEdits(code, [{
688
- start: element.start,
689
- end: element.end,
690
- content: generateCode(nextValue)
691
- }]);
692
- };
693
- const moveArrayItem = (code, from, to) => {
694
- const data = denseArraySource(code);
695
- if (!data || !validArrayIndices([from], data.elements.length) || !Number.isInteger(to)) return null;
696
- const next = [...data.elements];
697
- const [moved] = next.splice(from, 1);
698
- next.splice(to, 0, moved);
699
- return reorderArraySource(code, data.elements, next);
700
- };
701
- const moveArrayItems = (code, indices, direction) => {
702
- const data = denseArraySource(code);
703
- if (!data || !validArrayIndices(indices, data.elements.length) || !["up", "down"].includes(direction)) return null;
704
- const next = moveSelectedIndices(data.elements, indices, direction);
705
- return {
706
- code: reorderArraySource(code, data.elements, next.items),
707
- indices: next.indices
708
- };
709
- };
710
- const removeArrayItems = (code, indices, kind) => {
711
- const data = denseArraySource(code);
712
- if (!data || !validArrayIndices(indices, data.elements.length)) return null;
713
- const remaining = removeIndices(data.elements, indices);
714
- if (!(kind === void 0 ? remaining : remaining.filter((node) => kindOf(node) === kind)).length) return null;
715
- return removeArraySource(code, data, indices);
716
- };
717
- const copyItem = (code, element, generateId, usedIds) => {
718
- const edits = [];
719
- let unsupported = false;
720
- const reserveId = (id) => {
721
- if (usedIds.has(id)) unsupported = true;
722
- usedIds.add(id);
723
- return JSON.stringify(id);
724
- };
725
- import_lib$1.traverseFast(element, (node) => {
726
- if (!import_lib$1.isJSXOpeningElement(node)) return;
727
- const ids = node.attributes.filter((attr) => import_lib$1.isJSXAttribute(attr) && import_lib$1.isJSXIdentifier(attr.name, { name: "data-id" }));
728
- const attr = ids[0];
729
- if (ids.length > 1 || attr && (!import_lib$1.isStringLiteral(attr.value) || node.attributes.slice(node.attributes.indexOf(attr) + 1).some((a) => import_lib$1.isJSXSpreadAttribute(a)))) {
730
- unsupported = true;
731
- return;
732
- }
733
- if (attr && import_lib$1.isStringLiteral(attr.value)) edits.push({
734
- start: attr.value.start,
735
- end: attr.value.end,
736
- content: reserveId(generateId())
737
- });
738
- });
739
- if (import_lib$1.isObjectExpression(element)) {
740
- const keys = element.properties.filter((prop) => import_lib$1.isObjectProperty(prop) && !prop.computed && (import_lib$1.isIdentifier(prop.key, { name: "key" }) || import_lib$1.isStringLiteral(prop.key, { value: "key" })));
741
- const key = keys[0];
742
- if (keys.length > 1 || key && (!import_lib$1.isStringLiteral(key.value) || element.properties.slice(element.properties.indexOf(key) + 1).some((prop) => import_lib$1.isSpreadElement(prop) || "computed" in prop && prop.computed))) return null;
743
- if (key && import_lib$1.isStringLiteral(key.value)) edits.push({
744
- start: key.value.start,
745
- end: key.value.end,
746
- content: reserveId(`${key.value.value}-${generateId()}`)
747
- });
748
- }
749
- if (unsupported) return null;
750
- return applyEdits(code.slice(element.start, element.end), edits.map((edit) => ({
751
- ...edit,
752
- start: edit.start - element.start,
753
- end: edit.end - element.start
754
- })));
755
- };
756
- const duplicateArrayItems = (code, indices, generateId = () => nanoid(6)) => {
757
- const data = denseArraySource(code);
758
- if (!data || !indices.size || !validArrayIndices(indices, data.elements.length)) return null;
759
- const usedIds = /* @__PURE__ */ new Set();
760
- import_lib$1.traverseFast(data.array, (node) => {
761
- if (import_lib$1.isJSXAttribute(node) && import_lib$1.isJSXIdentifier(node.name, { name: "data-id" }) && import_lib$1.isStringLiteral(node.value)) usedIds.add(node.value.value);
762
- else if (import_lib$1.isObjectProperty(node) && !node.computed && (import_lib$1.isIdentifier(node.key, { name: "key" }) || import_lib$1.isStringLiteral(node.key, { value: "key" })) && import_lib$1.isStringLiteral(node.value)) usedIds.add(node.value.value);
763
- });
764
- const copies = [...indices].sort((a, b) => a - b).map((index) => copyItem(code, data.elements[index], generateId, usedIds));
765
- return copies.every((copy) => copy !== null) ? appendArraySource(code, data, copies) : null;
766
- };
767
- const appendArrayItem = (code, kind, generateId = () => nanoid(6)) => {
768
- const index = denseArraySource(code)?.elements.findIndex((node) => kindOf(node) === kind) ?? -1;
769
- return index < 0 ? null : duplicateArrayItems(code, /* @__PURE__ */ new Set([index]), generateId);
770
- };
771
- //#endregion
772
499
  //#region src/utils/ast/extract.ts
773
500
  const collectText = (children) => {
774
501
  return children.filter((c) => import_lib$1.isJSXText(c)).map((c) => c.value.trim()).filter((v) => v.length).join(" ");
@@ -781,6 +508,22 @@ const parseJSXName = (tagName) => {
781
508
  return expr;
782
509
  };
783
510
  const extractCache = createBoundedCache(CONFIG.CACHE_LIMIT);
511
+ const NO_MAP = {};
512
+ let optionExtractCaches = /* @__PURE__ */ new WeakMap();
513
+ const cacheFor = ({ bindings, bindingKeys }) => {
514
+ if (!bindings && !bindingKeys) return extractCache;
515
+ let byKeys = optionExtractCaches.get(bindings ?? NO_MAP);
516
+ if (!byKeys) {
517
+ byKeys = /* @__PURE__ */ new WeakMap();
518
+ optionExtractCaches.set(bindings ?? NO_MAP, byKeys);
519
+ }
520
+ let cache = byKeys.get(bindingKeys ?? NO_MAP);
521
+ if (!cache) {
522
+ cache = createBoundedCache(CONFIG.CACHE_LIMIT);
523
+ byKeys.set(bindingKeys ?? NO_MAP, cache);
524
+ }
525
+ return cache;
526
+ };
784
527
  const extractAttributes = (attributes, source) => {
785
528
  const allAttrs = [];
786
529
  const dataAttrs = [];
@@ -872,13 +615,13 @@ const createFragmentNode = (textContent, children) => ({
872
615
  children,
873
616
  isFragment: true
874
617
  });
875
- const processChildrenBinding = (jsxElement, processedNodes, shouldWrap = true, source) => {
618
+ const processChildrenBinding = (jsxElement, processedNodes, shouldWrap = true, source, options) => {
876
619
  const jsxChildren = jsxElement.children.filter((child) => import_lib$1.isJSXElement(child) || import_lib$1.isJSXFragment(child));
877
620
  if (!jsxChildren.length) return;
878
621
  const childrenNodes = [];
879
622
  jsxChildren.forEach((child) => {
880
623
  if (import_lib$1.isJSXElement(child)) {
881
- const childResults = extractFromNode(child, processedNodes, source);
624
+ const childResults = extractFromNode(child, processedNodes, source, options);
882
625
  if (shouldWrap) {
883
626
  const wrapperNode = createWrapperNode(collectText(child.children), childResults);
884
627
  wrapperNode.source = source?.slice(child.start, child.end);
@@ -891,7 +634,7 @@ const processChildrenBinding = (jsxElement, processedNodes, shouldWrap = true, s
891
634
  const fragmentChildren = [];
892
635
  child.children.forEach((fragmentChild) => {
893
636
  if (import_lib$1.isJSXElement(fragmentChild)) {
894
- const childResults = extractFromNode(fragmentChild, processedNodes, source);
637
+ const childResults = extractFromNode(fragmentChild, processedNodes, source, options);
895
638
  fragmentChildren.push(...childResults);
896
639
  }
897
640
  });
@@ -928,13 +671,14 @@ const getBindingExpression = (opening) => {
928
671
  }
929
672
  return null;
930
673
  };
931
- const readNodeBindingInfo = (node, source) => {
674
+ const readNodeBindingInfo = (node, source, options) => {
932
675
  const opening = node.openingElement;
933
676
  const tagName = getJSXTagName(opening);
934
677
  const { allAttrs, dataAttrs } = extractAttributes(opening.attributes, source);
935
678
  const bindingAttr = dataAttrs.find((attr) => attr.name === DATA_ATTR.BINDING);
936
679
  const bindingExpr = getBindingExpression(opening);
937
- const bindings = bindingExpr ? parseBindingExpression(bindingExpr) : bindingAttr?.value ? parseBinding(bindingAttr.value) : [];
680
+ const keyAttr = dataAttrs.find((attr) => attr.name === DATA_ATTR.BINDING_KEY);
681
+ const bindings = resolveBindings(bindingExpr ? parseBindingExpression(bindingExpr) : bindingAttr ? parseBinding(bindingAttr.value) : void 0, keyAttr && (keyAttr.isStringLiteral ? keyAttr.value : null), tagName, options);
938
682
  const childrenBinding = bindings.find((b) => b.property === BINDING_PROP.CHILDREN);
939
683
  const arrayBindings = bindings.filter((b) => b.property === BINDING_PROP.ITEMS || b.type === "array");
940
684
  const innerHtmlBinding = bindings.find((b) => b.property === BINDING_PROP.INNER_HTML);
@@ -950,7 +694,7 @@ const readNodeBindingInfo = (node, source) => {
950
694
  rawChildren
951
695
  };
952
696
  };
953
- const parseToNodes = (raw) => {
697
+ const parseToNodes = (raw, options) => {
954
698
  const wrapped = wrap(raw);
955
699
  const ast = (0, import_lib.parse)(wrapped, {
956
700
  sourceType: "module",
@@ -961,10 +705,10 @@ const parseToNodes = (raw) => {
961
705
  const processedNodes = /* @__PURE__ */ new WeakSet();
962
706
  traverse(ast, { JSXElement(path) {
963
707
  if (processedNodes.has(path.node)) return;
964
- const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, arrayBindings, rawChildren } = readNodeBindingInfo(path.node, wrapped);
708
+ const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, arrayBindings, rawChildren } = readNodeBindingInfo(path.node, wrapped, options);
965
709
  if (!tagName || !dataAttrs.length) return;
966
710
  let childrenNodes;
967
- if (childrenBinding) childrenNodes = processChildrenBinding(path.node, processedNodes, true, wrapped);
711
+ if (childrenBinding) childrenNodes = processChildrenBinding(path.node, processedNodes, true, wrapped, options);
968
712
  for (const arrayBinding of arrayBindings) skipItemsChildren(path.node, processedNodes, arrayBinding.property);
969
713
  results.push({
970
714
  tagName,
@@ -988,32 +732,134 @@ const parseToNodes = (raw) => {
988
732
  } });
989
733
  return results;
990
734
  };
991
- function extract(raw) {
992
- if (extractCache.has(raw)) return extractCache.get(raw);
993
- const results = parseToNodes(raw);
994
- extractCache.set(raw, results);
995
- return results;
996
- }
997
- function extractFromNode(node, processedNodes, source) {
998
- processedNodes?.add(node);
999
- const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, rawChildren } = readNodeBindingInfo(node, source);
1000
- let childrenNodes;
1001
- if (childrenBinding) childrenNodes = processChildrenBinding(node, processedNodes, true, source);
1002
- if (!childrenNodes) childrenNodes = processChildrenBinding(node, processedNodes, false, source);
1003
- return [{
1004
- tagName,
1005
- attributes: allAttrs,
1006
- dataAttributes: dataAttrs,
1007
- textContent: collectText(node.children),
1008
- rawChildren,
1009
- children: childrenNodes,
1010
- bindings
1011
- }];
1012
- }
1013
- function clearExtractCache() {
1014
- extractCache.clear();
1015
- }
1016
- registerEditorCache(clearExtractCache);
735
+ function extract(raw, options = {}) {
736
+ const cache = cacheFor(options);
737
+ if (cache.has(raw)) return cache.get(raw);
738
+ const results = parseToNodes(raw, options);
739
+ cache.set(raw, results);
740
+ return results;
741
+ }
742
+ function extractFromNode(node, processedNodes, source, options) {
743
+ processedNodes?.add(node);
744
+ const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, rawChildren } = readNodeBindingInfo(node, source, options);
745
+ let childrenNodes;
746
+ if (childrenBinding) childrenNodes = processChildrenBinding(node, processedNodes, true, source, options);
747
+ if (!childrenNodes) childrenNodes = processChildrenBinding(node, processedNodes, false, source, options);
748
+ return [{
749
+ tagName,
750
+ attributes: allAttrs,
751
+ dataAttributes: dataAttrs,
752
+ textContent: collectText(node.children),
753
+ rawChildren,
754
+ children: childrenNodes,
755
+ bindings
756
+ }];
757
+ }
758
+ function clearExtractCache() {
759
+ extractCache.clear();
760
+ optionExtractCaches = /* @__PURE__ */ new WeakMap();
761
+ }
762
+ registerEditorCache(clearExtractCache);
763
+ //#endregion
764
+ //#region src/utils/selection.ts
765
+ const removeIndices = (items, indices) => {
766
+ return items.filter((_, index) => !indices.has(index));
767
+ };
768
+ /**
769
+ * Shifts every selected index up/down by one step as a block, preserving
770
+ * relative order — scattered selections stop moving individually once they
771
+ * hit an unselected neighbor, so the whole group slides together instead of
772
+ * items passing through each other.
773
+ */
774
+ const moveSelectedIndices = (items, indices, direction) => {
775
+ const next = [...items];
776
+ const nextIndices = new Set(indices);
777
+ const ordered = [...indices].sort((a, b) => direction === "up" ? a - b : b - a);
778
+ for (const index of ordered) {
779
+ const target = direction === "up" ? index - 1 : index + 1;
780
+ if (target < 0 || target >= next.length || nextIndices.has(target)) continue;
781
+ [next[index], next[target]] = [next[target], next[index]];
782
+ nextIndices.delete(index);
783
+ nextIndices.add(target);
784
+ }
785
+ return {
786
+ items: next,
787
+ indices: nextIndices
788
+ };
789
+ };
790
+ //#endregion
791
+ //#region src/utils/ast/patch.ts
792
+ const hasOnlyLayoutNewlines = (content) => {
793
+ let index = 0;
794
+ while (index < content.length) {
795
+ const char = content[index];
796
+ if (char === "`" || char === "<") return false;
797
+ if (char === "\"" || char === "'") {
798
+ const next = skipStringLiteral(content, index);
799
+ if (next === -1) return false;
800
+ index = next;
801
+ continue;
802
+ }
803
+ if (char === "/") {
804
+ const following = content[index + 1];
805
+ if (following === "/") {
806
+ const lineEnd = content.indexOf("\n", index + 2);
807
+ index = lineEnd === -1 ? content.length : lineEnd;
808
+ continue;
809
+ }
810
+ if (following === "*") {
811
+ const commentEnd = content.indexOf("*/", index + 2);
812
+ if (commentEnd === -1) return false;
813
+ index = commentEnd + 2;
814
+ continue;
815
+ }
816
+ return false;
817
+ }
818
+ index++;
819
+ }
820
+ return true;
821
+ };
822
+ const skipStringLiteral = (content, start) => {
823
+ const quote = content[start];
824
+ for (let index = start + 1; index < content.length; index++) {
825
+ const char = content[index];
826
+ if (char === "\\") {
827
+ const escaped = content[index + 1];
828
+ if (escaped === "\n" || escaped === "\r") return -1;
829
+ index++;
830
+ continue;
831
+ }
832
+ if (char === quote) return index + 1;
833
+ if (char === "\n") return -1;
834
+ }
835
+ return -1;
836
+ };
837
+ const lineIndentAt = (source, offset) => {
838
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
839
+ return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
840
+ };
841
+ const lineTerminatorOf = (source) => {
842
+ return source.includes("\r\n") ? "\r\n" : "\n";
843
+ };
844
+ const applyEdits = (source, edits) => {
845
+ if (edits.length === 0) return source;
846
+ const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
847
+ for (const edit of ordered) if (!Number.isInteger(edit.start) || !Number.isInteger(edit.end) || edit.start < 0 || edit.end > source.length || edit.start > edit.end) throw new Error(`Invalid source edit [${edit.start}, ${edit.end}) for a source of length ${source.length}`);
848
+ for (let i = 1; i < ordered.length; i++) {
849
+ const previous = ordered[i - 1];
850
+ const current = ordered[i];
851
+ if (current.start < previous.end) throw new Error(`Overlapping source edits: [${previous.start}, ${previous.end}) and [${current.start}, ${current.end})`);
852
+ }
853
+ let result = "";
854
+ let cursor = 0;
855
+ const terminator = lineTerminatorOf(source);
856
+ for (const edit of ordered) {
857
+ const content = edit.indent && edit.content.includes("\n") && hasOnlyLayoutNewlines(edit.content) ? edit.content.replace(/\n/g, `${terminator}${lineIndentAt(source, edit.start)}`) : edit.content;
858
+ result += source.slice(cursor, edit.start) + content;
859
+ cursor = edit.end;
860
+ }
861
+ return result + source.slice(cursor);
862
+ };
1017
863
  //#endregion
1018
864
  //#region src/utils/ast/children.ts
1019
865
  const modeledSignatures = (nodes) => nodes.map((node) => {
@@ -1061,13 +907,13 @@ const copySource = (source, usedIds) => {
1061
907
  });
1062
908
  return applyEdits(source, edits);
1063
909
  };
1064
- const editChildrenSource = (source, parent, input) => {
910
+ const editChildrenSource = (source, parent, input, options) => {
1065
911
  try {
1066
912
  if (!parent.closingElement || parent.start == null || parent.end == null) return null;
1067
913
  const value = typeof input === "string" ? JSON.parse(input) : input;
1068
914
  const children = parent.children.filter((child) => import_lib$1.isJSXElement(child) || import_lib$1.isJSXFragment(child));
1069
915
  const raw = children.map((child) => source.slice(child.start, child.end));
1070
- const models = extract(source.slice(parent.start, parent.end))[0]?.children ?? [];
916
+ const models = extract(source.slice(parent.start, parent.end), options)[0]?.children ?? [];
1071
917
  if (models.length !== children.length) return null;
1072
918
  const signatures = modeledSignatures(models);
1073
919
  const replaceSlots = (next) => {
@@ -1340,7 +1186,7 @@ const canEditAttributeValue = (attribute, value, type) => {
1340
1186
  if (!import_lib$1.isJSXExpressionContainer(attribute.value) || import_lib$1.isJSXEmptyExpression(attribute.value.expression) || isLosslesslyEvaluable(attribute.value.expression)) return true;
1341
1187
  return typeof value === "string" && holdsStructuralExpression(attribute.value) && !(type && STRING_VALUED_TYPES.has(type)) && structuralSource(value) !== null;
1342
1188
  };
1343
- const update = (code, dataId, label, value, property) => {
1189
+ const update = (code, dataId, label, value, property, options = {}) => {
1344
1190
  try {
1345
1191
  const wrapped = wrap(code);
1346
1192
  const ast = (0, import_lib.parse)(wrapped, {
@@ -1364,25 +1210,38 @@ const update = (code, dataId, label, value, property) => {
1364
1210
  return import_lib$1.isJSXAttribute(attr) && import_lib$1.isJSXIdentifier(attr.name) && attr.name.name === DATA_ATTR.ID && attr.value && import_lib$1.isStringLiteral(attr.value) && attr.value.value === dataId;
1365
1211
  })) return;
1366
1212
  const bindingAttr = opening.attributes.find((attr) => import_lib$1.isJSXAttribute(attr) && import_lib$1.isJSXIdentifier(attr.name) && attr.name.name === DATA_ATTR.BINDING);
1367
- if (!bindingAttr?.value) {
1213
+ if (bindingAttr && !bindingAttr.value) {
1368
1214
  failure = {
1369
1215
  reason: "no-binding",
1370
1216
  dataId
1371
1217
  };
1372
1218
  return;
1373
1219
  }
1374
- let bindingValue = "";
1375
- if (import_lib$1.isStringLiteral(bindingAttr.value)) bindingValue = bindingAttr.value.value;
1376
- else if (import_lib$1.isJSXExpressionContainer(bindingAttr.value)) try {
1377
- bindingValue = generateCode(bindingAttr.value.expression);
1378
- } catch (error) {
1220
+ let own;
1221
+ if (bindingAttr?.value) {
1222
+ let bindingValue = "";
1223
+ if (import_lib$1.isStringLiteral(bindingAttr.value)) bindingValue = bindingAttr.value.value;
1224
+ else if (import_lib$1.isJSXExpressionContainer(bindingAttr.value)) try {
1225
+ bindingValue = generateCode(bindingAttr.value.expression);
1226
+ } catch (error) {
1227
+ failure = {
1228
+ reason: "parse-error",
1229
+ error
1230
+ };
1231
+ return;
1232
+ }
1233
+ own = parseBinding(bindingValue);
1234
+ }
1235
+ const keyAttr = opening.attributes.find((attr) => import_lib$1.isJSXAttribute(attr) && import_lib$1.isJSXIdentifier(attr.name) && attr.name.name === DATA_ATTR.BINDING_KEY);
1236
+ const bindings = resolveBindings(own, keyAttr && (keyAttr.value && import_lib$1.isStringLiteral(keyAttr.value) ? keyAttr.value.value : null), getJSXTagName(opening), options);
1237
+ if (!own && bindings.length === 0) {
1379
1238
  failure = {
1380
- reason: "parse-error",
1381
- error
1239
+ reason: "no-binding",
1240
+ dataId
1382
1241
  };
1383
1242
  return;
1384
1243
  }
1385
- const matches = parseBinding(bindingValue).filter((binding) => property !== void 0 ? binding.property === property : binding.label === label);
1244
+ const matches = bindings.filter((binding) => property !== void 0 ? binding.property === property : binding.label === label);
1386
1245
  if (matches.length === 0) {
1387
1246
  failure = {
1388
1247
  reason: "binding-not-declared",
@@ -1443,7 +1302,7 @@ const update = (code, dataId, label, value, property) => {
1443
1302
  };
1444
1303
  break;
1445
1304
  }
1446
- collect(editChildrenSource(wrapped, path.node, value), () => ({
1305
+ collect(editChildrenSource(wrapped, path.node, value, options), () => ({
1447
1306
  reason: "parse-error",
1448
1307
  error: /* @__PURE__ */ new Error("Unsupported or stale children edit; source was preserved")
1449
1308
  }));
@@ -1506,10 +1365,10 @@ const update = (code, dataId, label, value, property) => {
1506
1365
  };
1507
1366
  }
1508
1367
  };
1509
- const updateAll = (raw, entries) => {
1368
+ const updateAll = (raw, entries, options = {}) => {
1510
1369
  let current = raw;
1511
1370
  for (const [index, entry] of entries.entries()) {
1512
- const result = update(current, entry.dataId, entry.label, entry.value, entry.property);
1371
+ const result = update(current, entry.dataId, entry.label, entry.value, entry.property, options);
1513
1372
  if (!result.success) return {
1514
1373
  success: false,
1515
1374
  code: raw,
@@ -1549,55 +1408,237 @@ const bulkUpdate = (raw, entries) => {
1549
1408
  };
1550
1409
  };
1551
1410
  //#endregion
1552
- //#region src/utils/ast/validate.ts
1553
- const VALID = { valid: true };
1554
- const validateBindingValue = (binding, value) => {
1555
- if (value === "" || value === null || value === void 0) {
1556
- if (binding.required) return {
1557
- valid: false,
1558
- message: "This field is required."
1559
- };
1560
- return VALID;
1411
+ //#region src/utils/ast/array-source.ts
1412
+ const commaIn = (source, start, end) => {
1413
+ let comma = null;
1414
+ for (let at = start; at < end; at++) {
1415
+ if (/\s/.test(source[at])) continue;
1416
+ if (source.startsWith("/*", at)) {
1417
+ const close = source.indexOf("*/", at + 2);
1418
+ if (close < 0 || close + 2 > end) return null;
1419
+ at = close + 1;
1420
+ } else if (source.startsWith("//", at)) {
1421
+ const newline = source.indexOf("\n", at + 2);
1422
+ at = newline < 0 ? end : newline;
1423
+ } else if (source[at] === "," && comma === null) comma = at;
1424
+ else return null;
1561
1425
  }
1562
- if (typeof value === "number") {
1563
- if (binding.min !== void 0 && value < binding.min) return {
1564
- valid: false,
1565
- message: `Must be at least ${binding.min}.`
1566
- };
1567
- if (binding.max !== void 0 && value > binding.max) return {
1568
- valid: false,
1569
- message: `Must be at most ${binding.max}.`
1570
- };
1426
+ return comma;
1427
+ };
1428
+ const denseArraySource = (source) => {
1429
+ const array = parseArrayExpression(source);
1430
+ if (!array || array.elements.some((node) => !import_lib$1.isExpression(node) || node.extra?.parenthesized)) return null;
1431
+ const elements = array.elements;
1432
+ const commas = elements.slice(0, -1).map((node, index) => commaIn(source, node.end, elements[index + 1].start));
1433
+ if (commas.some((comma) => comma === null)) return null;
1434
+ const last = elements.at(-1);
1435
+ return {
1436
+ array,
1437
+ elements,
1438
+ commas,
1439
+ trailing: last ? commaIn(source, last.end, array.end - 1) : null
1440
+ };
1441
+ };
1442
+ const canStructurallyEditArray = (source) => denseArraySource(source) !== null;
1443
+ const validArrayIndices = (indices, length) => [...indices].every((index) => Number.isInteger(index) && index >= 0 && index < length);
1444
+ const reorderArraySource = (source, elements, next) => applyEdits(source, elements.map((node, index) => ({
1445
+ start: node.start,
1446
+ end: node.end,
1447
+ content: source.slice(next[index].start, next[index].end)
1448
+ })));
1449
+ const removeArraySource = (source, data, indices) => {
1450
+ const edits = [];
1451
+ const removedCommas = /* @__PURE__ */ new Set();
1452
+ const { elements, commas, trailing } = data;
1453
+ for (let index = 0; index < elements.length; index++) {
1454
+ if (!indices.has(index)) continue;
1455
+ const start = index;
1456
+ while (index + 1 < elements.length && indices.has(index + 1)) index++;
1457
+ for (let current = start; current <= index; current++) {
1458
+ const node = elements[current];
1459
+ edits.push({
1460
+ start: node.start,
1461
+ end: node.end,
1462
+ content: ""
1463
+ });
1464
+ if (current < elements.length - 1) removedCommas.add(commas[current]);
1465
+ }
1466
+ if (index === elements.length - 1) {
1467
+ if (trailing !== null) removedCommas.add(trailing);
1468
+ else if (start > 0) removedCommas.add(commas[start - 1]);
1469
+ }
1470
+ }
1471
+ for (const comma of removedCommas) edits.push({
1472
+ start: comma,
1473
+ end: comma + 1,
1474
+ content: ""
1475
+ });
1476
+ return applyEdits(source, edits);
1477
+ };
1478
+ const appendArraySource = (source, data, copies) => {
1479
+ const last = data.elements.at(-1);
1480
+ if (!last || copies.length === 0) return source;
1481
+ return applyEdits(source, [{
1482
+ start: last.end,
1483
+ end: last.end,
1484
+ content: `, ${copies.join(", ")}`
1485
+ }]);
1486
+ };
1487
+ //#endregion
1488
+ //#region src/utils/ast/items.ts
1489
+ const elementsOf = (code) => parseArrayExpression(code)?.elements ?? null;
1490
+ const kindOf = (element) => import_lib$1.isObjectExpression(element) ? "object" : "primitive";
1491
+ const parseItems = (code) => {
1492
+ return elementsOf(code)?.flatMap((node, index) => import_lib$1.isExpression(node) ? [{
1493
+ index,
1494
+ kind: kindOf(node),
1495
+ node
1496
+ }] : []) ?? null;
1497
+ };
1498
+ const resolveRenderLeaf = (render, key) => {
1499
+ const leaf = render?.[key];
1500
+ return leaf && "type" in leaf ? leaf : null;
1501
+ };
1502
+ const toTemplateRaw = (value) => {
1503
+ return value.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${").replace(/\r/g, "\\r");
1504
+ };
1505
+ const buildPropertyValue = (value, declaredType, renderLeaf, current) => {
1506
+ if (renderLeaf?.property === BINDING_PROP.INNER_HTML) {
1507
+ const raw = String(value);
1508
+ return import_lib$1.templateLiteral([import_lib$1.templateElement({
1509
+ raw: toTemplateRaw(raw),
1510
+ cooked: raw
1511
+ }, true)], []);
1571
1512
  }
1572
- if (typeof value === "string" && binding.pattern) {
1573
- let regex;
1513
+ if (renderLeaf?.type === "jsx" || import_lib$1.isJSXElement(current) || import_lib$1.isJSXFragment(current)) {
1514
+ const trimmed = String(value).trim();
1515
+ if (!trimmed.startsWith("<")) return import_lib$1.stringLiteral(trimmed);
1574
1516
  try {
1575
- regex = new RegExp(binding.pattern);
1517
+ return (0, import_lib.parseExpression)(trimmed, { plugins: ["jsx", "typescript"] });
1576
1518
  } catch {
1577
- return VALID;
1519
+ return;
1578
1520
  }
1579
- if (!regex.test(value)) return {
1580
- valid: false,
1581
- message: "Value does not match the required format."
1582
- };
1583
1521
  }
1584
- if (typeof value === "string" && binding.type === "url") try {
1585
- new URL(value);
1586
- } catch {
1587
- return {
1588
- valid: false,
1589
- message: "Must be a valid URL."
1590
- };
1522
+ if (declaredType === "array" || declaredType === "object") {
1523
+ if (typeof value === "string") try {
1524
+ return (0, import_lib.parseExpression)(value, { plugins: ["jsx", "typescript"] });
1525
+ } catch {
1526
+ return;
1527
+ }
1528
+ if (!isLosslesslyEvaluable(current)) return;
1529
+ return valueToExpression(value) ?? void 0;
1591
1530
  }
1592
- if (typeof value === "string" && binding.type === "date") {
1593
- if (Number.isNaN(Date.parse(value))) return {
1594
- valid: false,
1595
- message: "Must be a valid date."
1596
- };
1531
+ return createNodeFromValue(declaredType, parseValue(value)) ?? void 0;
1532
+ };
1533
+ const updateArrayItemProperty = (code, index, key, value, render) => {
1534
+ const elements = elementsOf(code);
1535
+ const element = elements?.[index];
1536
+ if (!elements || !import_lib$1.isObjectExpression(element)) return null;
1537
+ const target = element.properties.find((property) => import_lib$1.isObjectProperty(property) && import_lib$1.isIdentifier(property.key) && property.key.name === key);
1538
+ if (!target) return null;
1539
+ const declaredType = extractObjectProperties(element)[key]?.type ?? "string";
1540
+ const nextValue = buildPropertyValue(value, declaredType, resolveRenderLeaf(render, key), target.value);
1541
+ if (!nextValue) return null;
1542
+ let serialized = typeof value === "string" && (declaredType === "array" || declaredType === "object" || import_lib$1.isJSXElement(nextValue) || import_lib$1.isJSXFragment(nextValue)) ? value.trim() : generateCode(nextValue);
1543
+ if (nextValue.trailingComments?.some((comment) => comment.type === "CommentLine")) serialized += "\n";
1544
+ const content = target.shorthand ? `${key}: ${serialized}` : serialized;
1545
+ return applyEdits(code, [{
1546
+ start: target.value.start,
1547
+ end: target.value.end,
1548
+ content
1549
+ }]);
1550
+ };
1551
+ const updateArrayItemValue = (code, index, value) => {
1552
+ const elements = elementsOf(code);
1553
+ const element = elements?.[index];
1554
+ if (!elements || !import_lib$1.isExpression(element)) return null;
1555
+ const nextValue = createNodeFromValue(extractNodeValue(element).type, parseValue(value));
1556
+ if (!nextValue) return null;
1557
+ return applyEdits(code, [{
1558
+ start: element.start,
1559
+ end: element.end,
1560
+ content: generateCode(nextValue)
1561
+ }]);
1562
+ };
1563
+ const moveArrayItem = (code, from, to) => {
1564
+ const data = denseArraySource(code);
1565
+ if (!data || !validArrayIndices([from], data.elements.length) || !Number.isInteger(to)) return null;
1566
+ const next = [...data.elements];
1567
+ const [moved] = next.splice(from, 1);
1568
+ next.splice(to, 0, moved);
1569
+ return reorderArraySource(code, data.elements, next);
1570
+ };
1571
+ const moveArrayItems = (code, indices, direction) => {
1572
+ const data = denseArraySource(code);
1573
+ if (!data || !validArrayIndices(indices, data.elements.length) || !["up", "down"].includes(direction)) return null;
1574
+ const next = moveSelectedIndices(data.elements, indices, direction);
1575
+ return {
1576
+ code: reorderArraySource(code, data.elements, next.items),
1577
+ indices: next.indices
1578
+ };
1579
+ };
1580
+ const removeArrayItems = (code, indices, kind) => {
1581
+ const data = denseArraySource(code);
1582
+ if (!data || !validArrayIndices(indices, data.elements.length)) return null;
1583
+ const remaining = removeIndices(data.elements, indices);
1584
+ if (!(kind === void 0 ? remaining : remaining.filter((node) => kindOf(node) === kind)).length) return null;
1585
+ return removeArraySource(code, data, indices);
1586
+ };
1587
+ const copyItem = (code, element, generateId, usedIds) => {
1588
+ const edits = [];
1589
+ let unsupported = false;
1590
+ const reserveId = (id) => {
1591
+ if (usedIds.has(id)) unsupported = true;
1592
+ usedIds.add(id);
1593
+ return JSON.stringify(id);
1594
+ };
1595
+ import_lib$1.traverseFast(element, (node) => {
1596
+ if (!import_lib$1.isJSXOpeningElement(node)) return;
1597
+ const ids = node.attributes.filter((attr) => import_lib$1.isJSXAttribute(attr) && import_lib$1.isJSXIdentifier(attr.name, { name: "data-id" }));
1598
+ const attr = ids[0];
1599
+ if (ids.length > 1 || attr && (!import_lib$1.isStringLiteral(attr.value) || node.attributes.slice(node.attributes.indexOf(attr) + 1).some((a) => import_lib$1.isJSXSpreadAttribute(a)))) {
1600
+ unsupported = true;
1601
+ return;
1602
+ }
1603
+ if (attr && import_lib$1.isStringLiteral(attr.value)) edits.push({
1604
+ start: attr.value.start,
1605
+ end: attr.value.end,
1606
+ content: reserveId(generateId())
1607
+ });
1608
+ });
1609
+ if (import_lib$1.isObjectExpression(element)) {
1610
+ const keys = element.properties.filter((prop) => import_lib$1.isObjectProperty(prop) && !prop.computed && (import_lib$1.isIdentifier(prop.key, { name: "key" }) || import_lib$1.isStringLiteral(prop.key, { value: "key" })));
1611
+ const key = keys[0];
1612
+ if (keys.length > 1 || key && (!import_lib$1.isStringLiteral(key.value) || element.properties.slice(element.properties.indexOf(key) + 1).some((prop) => import_lib$1.isSpreadElement(prop) || "computed" in prop && prop.computed))) return null;
1613
+ if (key && import_lib$1.isStringLiteral(key.value)) edits.push({
1614
+ start: key.value.start,
1615
+ end: key.value.end,
1616
+ content: reserveId(`${key.value.value}-${generateId()}`)
1617
+ });
1597
1618
  }
1598
- return VALID;
1619
+ if (unsupported) return null;
1620
+ return applyEdits(code.slice(element.start, element.end), edits.map((edit) => ({
1621
+ ...edit,
1622
+ start: edit.start - element.start,
1623
+ end: edit.end - element.start
1624
+ })));
1625
+ };
1626
+ const duplicateArrayItems = (code, indices, generateId = () => nanoid(6)) => {
1627
+ const data = denseArraySource(code);
1628
+ if (!data || !indices.size || !validArrayIndices(indices, data.elements.length)) return null;
1629
+ const usedIds = /* @__PURE__ */ new Set();
1630
+ import_lib$1.traverseFast(data.array, (node) => {
1631
+ if (import_lib$1.isJSXAttribute(node) && import_lib$1.isJSXIdentifier(node.name, { name: "data-id" }) && import_lib$1.isStringLiteral(node.value)) usedIds.add(node.value.value);
1632
+ else if (import_lib$1.isObjectProperty(node) && !node.computed && (import_lib$1.isIdentifier(node.key, { name: "key" }) || import_lib$1.isStringLiteral(node.key, { value: "key" })) && import_lib$1.isStringLiteral(node.value)) usedIds.add(node.value.value);
1633
+ });
1634
+ const copies = [...indices].sort((a, b) => a - b).map((index) => copyItem(code, data.elements[index], generateId, usedIds));
1635
+ return copies.every((copy) => copy !== null) ? appendArraySource(code, data, copies) : null;
1636
+ };
1637
+ const appendArrayItem = (code, kind, generateId = () => nanoid(6)) => {
1638
+ const index = denseArraySource(code)?.elements.findIndex((node) => kindOf(node) === kind) ?? -1;
1639
+ return index < 0 ? null : duplicateArrayItems(code, /* @__PURE__ */ new Set([index]), generateId);
1599
1640
  };
1600
1641
  //#endregion
1601
- export { parseValue as A, arrayExpressionToCode as C, extractObjectProperties as D, extractNodeValue as E, flattenEditableValue as O, parseBindingExpression as S, createNodeFromValue as T, moveSelectedIndices as _, getChildrenSignatures as a, getStructuredValue as b, appendArrayItem as c, moveArrayItems as d, parseItems as f, canStructurallyEditArray as g, updateArrayItemValue as h, updateAll as i, setEditableValue as j, parseArrayExpression as k, duplicateArrayItems as l, updateArrayItemProperty as m, bulkUpdate as n, clearExtractCache as o, removeArrayItems as p, update as r, extract as s, validateBindingValue as t, moveArrayItem as u, findEditableChildren as v, canLosslesslyEvaluateSource as w, parseBinding as x, getCurrentValue as y };
1642
+ export { extractNodeValue as A, parseBinding as C, arrayExpressionToCode as D, resolveBindings as E, setEditableValue as F, flattenEditableValue as M, parseArrayExpression as N, canLosslesslyEvaluateSource as O, parseValue as P, isComponentTagName as S, readNodeBindings as T, findEditableChildren as _, parseItems as a, getRegistryBindings as b, updateArrayItemValue as c, update as d, updateAll as f, extract as g, clearExtractCache as h, moveArrayItems as i, extractObjectProperties as j, createNodeFromValue as k, canStructurallyEditArray as l, moveSelectedIndices as m, duplicateArrayItems as n, removeArrayItems as o, getChildrenSignatures as p, moveArrayItem as r, updateArrayItemProperty as s, appendArrayItem as t, bulkUpdate as u, getCurrentValue as v, parseBindingExpression as w, getStructuredValue as x, getKeyBindings as y };
1602
1643
 
1603
- //# sourceMappingURL=ast-CehbCXwh.js.map
1644
+ //# sourceMappingURL=items-DdrwGxJw.js.map