@jbpark/live-editor 3.0.1 → 3.1.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 (43) hide show
  1. package/README.ko.md +3 -2
  2. package/README.md +3 -2
  3. package/dist/{ast-D6R-dsk0.js → ast-CIe-mhh_.js} +416 -177
  4. package/dist/ast-CIe-mhh_.js.map +1 -0
  5. package/dist/{boundary-BF7iZp6e.js → boundary-BTP1nfG6.js} +2 -2
  6. package/dist/{boundary-BF7iZp6e.js.map → boundary-BTP1nfG6.js.map} +1 -1
  7. package/dist/{context-DH70wj9d.js → context-649REFsj.js} +2 -2
  8. package/dist/{context-DH70wj9d.js.map → context-649REFsj.js.map} +1 -1
  9. package/dist/core-D3q5rpuz.js +77 -0
  10. package/dist/core-D3q5rpuz.js.map +1 -0
  11. package/dist/dnd/index.d.ts +2 -2
  12. package/dist/dnd/index.js +2 -2
  13. package/dist/{dnd-CZJjzuTC.js → dnd-BnXoytb5.js} +385 -398
  14. package/dist/dnd-BnXoytb5.js.map +1 -0
  15. package/dist/editor/index.d.ts +1 -1
  16. package/dist/editor/index.js +2 -2
  17. package/dist/{editor-Ct9wuhLm.js → editor-DQSoTCe8.js} +2 -2
  18. package/dist/{editor-Ct9wuhLm.js.map → editor-DQSoTCe8.js.map} +1 -1
  19. package/dist/error/index.js +2 -2
  20. package/dist/{error-lsJey-it.js → error-CO0EbMlA.js} +2 -2
  21. package/dist/{error-lsJey-it.js.map → error-CO0EbMlA.js.map} +1 -1
  22. package/dist/{index-C4TBkJBG.d.ts → index-Ck3akHag.d.ts} +2 -2
  23. package/dist/{index-Bqc5kUAG.d.ts → index-DswWDEXZ.d.ts} +27 -3
  24. package/dist/{index-GuEiafMN.d.ts → index-DxbG789f.d.ts} +25 -2
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +5 -5
  27. package/dist/preview/index.js +1 -1
  28. package/dist/{preview-CNVXyDgY.js → preview-DFH-z_JL.js} +4 -4
  29. package/dist/{preview-CNVXyDgY.js.map → preview-DFH-z_JL.js.map} +1 -1
  30. package/dist/provider/index.js +1 -1
  31. package/dist/style.css +105 -123
  32. package/dist/{use-dynamic-tailwind-C0Onss1A.js → use-dynamic-tailwind-DqqQu2de.js} +2 -2
  33. package/dist/{use-dynamic-tailwind-C0Onss1A.js.map → use-dynamic-tailwind-DqqQu2de.js.map} +1 -1
  34. package/dist/utils/ast/index.d.ts +2 -2
  35. package/dist/utils/ast/index.js +2 -2
  36. package/dist/utils/index.js +1 -1
  37. package/dist/{utils-BwEgY5pe.js → utils-WyxPYZGh.js} +12 -10
  38. package/dist/{utils-BwEgY5pe.js.map → utils-WyxPYZGh.js.map} +1 -1
  39. package/package.json +2 -2
  40. package/dist/ast-D6R-dsk0.js.map +0 -1
  41. package/dist/core-CbuS_p_d.js +0 -123
  42. package/dist/core-CbuS_p_d.js.map +0 -1
  43. package/dist/dnd-CZJjzuTC.js.map +0 -1
@@ -453,42 +453,164 @@ const moveSelectedIndices = (items, indices, direction) => {
453
453
  };
454
454
  };
455
455
  //#endregion
456
- //#region src/utils/ast/tree.ts
457
- const replaceIds = (code, generateId = () => nanoid(6)) => {
458
- return code.replace(new RegExp(`${DATA_ATTR.ID}="[^"]*"`, "g"), () => {
459
- return `${DATA_ATTR.ID}="${generateId()}"`;
460
- });
456
+ //#region src/utils/ast/patch.ts
457
+ const hasOnlyLayoutNewlines = (content) => {
458
+ let index = 0;
459
+ while (index < content.length) {
460
+ const char = content[index];
461
+ if (char === "`" || char === "<") return false;
462
+ if (char === "\"" || char === "'") {
463
+ const next = skipStringLiteral(content, index);
464
+ if (next === -1) return false;
465
+ index = next;
466
+ continue;
467
+ }
468
+ if (char === "/") {
469
+ const following = content[index + 1];
470
+ if (following === "/") {
471
+ const lineEnd = content.indexOf("\n", index + 2);
472
+ index = lineEnd === -1 ? content.length : lineEnd;
473
+ continue;
474
+ }
475
+ if (following === "*") {
476
+ const commentEnd = content.indexOf("*/", index + 2);
477
+ if (commentEnd === -1) return false;
478
+ index = commentEnd + 2;
479
+ continue;
480
+ }
481
+ return false;
482
+ }
483
+ index++;
484
+ }
485
+ return true;
461
486
  };
462
- const fillIds = (code, generateId = () => nanoid(6)) => {
463
- return code.replace(new RegExp(`${DATA_ATTR.ID}=""`, "g"), () => {
464
- return `${DATA_ATTR.ID}="${generateId()}"`;
487
+ const skipStringLiteral = (content, start) => {
488
+ const quote = content[start];
489
+ for (let index = start + 1; index < content.length; index++) {
490
+ const char = content[index];
491
+ if (char === "\\") {
492
+ const escaped = content[index + 1];
493
+ if (escaped === "\n" || escaped === "\r") return -1;
494
+ index++;
495
+ continue;
496
+ }
497
+ if (char === quote) return index + 1;
498
+ if (char === "\n") return -1;
499
+ }
500
+ return -1;
501
+ };
502
+ const lineIndentAt = (source, offset) => {
503
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
504
+ return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
505
+ };
506
+ const lineTerminatorOf = (source) => {
507
+ return source.includes("\r\n") ? "\r\n" : "\n";
508
+ };
509
+ const applyEdits = (source, edits) => {
510
+ if (edits.length === 0) return source;
511
+ const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
512
+ 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}`);
513
+ for (let i = 1; i < ordered.length; i++) {
514
+ const previous = ordered[i - 1];
515
+ const current = ordered[i];
516
+ if (current.start < previous.end) throw new Error(`Overlapping source edits: [${previous.start}, ${previous.end}) and [${current.start}, ${current.end})`);
517
+ }
518
+ let result = "";
519
+ let cursor = 0;
520
+ const terminator = lineTerminatorOf(source);
521
+ for (const edit of ordered) {
522
+ const content = edit.indent && edit.content.includes("\n") && hasOnlyLayoutNewlines(edit.content) ? edit.content.replace(/\n/g, `${terminator}${lineIndentAt(source, edit.start)}`) : edit.content;
523
+ result += source.slice(cursor, edit.start) + content;
524
+ cursor = edit.end;
525
+ }
526
+ return result + source.slice(cursor);
527
+ };
528
+ //#endregion
529
+ //#region src/utils/ast/array-source.ts
530
+ const commaIn = (source, start, end) => {
531
+ let comma = null;
532
+ for (let at = start; at < end; at++) {
533
+ if (/\s/.test(source[at])) continue;
534
+ if (source.startsWith("/*", at)) {
535
+ const close = source.indexOf("*/", at + 2);
536
+ if (close < 0 || close + 2 > end) return null;
537
+ at = close + 1;
538
+ } else if (source.startsWith("//", at)) {
539
+ const newline = source.indexOf("\n", at + 2);
540
+ at = newline < 0 ? end : newline;
541
+ } else if (source[at] === "," && comma === null) comma = at;
542
+ else return null;
543
+ }
544
+ return comma;
545
+ };
546
+ const denseArraySource = (source) => {
547
+ const array = parseArrayExpression(source);
548
+ if (!array || array.elements.some((node) => !import_lib$2.isExpression(node) || node.extra?.parenthesized)) return null;
549
+ const elements = array.elements;
550
+ const commas = elements.slice(0, -1).map((node, index) => commaIn(source, node.end, elements[index + 1].start));
551
+ if (commas.some((comma) => comma === null)) return null;
552
+ const last = elements.at(-1);
553
+ return {
554
+ array,
555
+ elements,
556
+ commas,
557
+ trailing: last ? commaIn(source, last.end, array.end - 1) : null
558
+ };
559
+ };
560
+ const validArrayIndices = (indices, length) => [...indices].every((index) => Number.isInteger(index) && index >= 0 && index < length);
561
+ const reorderArraySource = (source, elements, next) => applyEdits(source, elements.map((node, index) => ({
562
+ start: node.start,
563
+ end: node.end,
564
+ content: source.slice(next[index].start, next[index].end)
565
+ })));
566
+ const removeArraySource = (source, data, indices) => {
567
+ const edits = [];
568
+ const removedCommas = /* @__PURE__ */ new Set();
569
+ const { elements, commas, trailing } = data;
570
+ for (let index = 0; index < elements.length; index++) {
571
+ if (!indices.has(index)) continue;
572
+ const start = index;
573
+ while (index + 1 < elements.length && indices.has(index + 1)) index++;
574
+ for (let current = start; current <= index; current++) {
575
+ const node = elements[current];
576
+ edits.push({
577
+ start: node.start,
578
+ end: node.end,
579
+ content: ""
580
+ });
581
+ if (current < elements.length - 1) removedCommas.add(commas[current]);
582
+ }
583
+ if (index === elements.length - 1) {
584
+ if (trailing !== null) removedCommas.add(trailing);
585
+ else if (start > 0) removedCommas.add(commas[start - 1]);
586
+ }
587
+ }
588
+ for (const comma of removedCommas) edits.push({
589
+ start: comma,
590
+ end: comma + 1,
591
+ content: ""
465
592
  });
593
+ return applyEdits(source, edits);
466
594
  };
467
- const clone = (element, generateId = () => nanoid(6)) => {
468
- const cloned = import_lib$2.cloneNode(element, true);
469
- const generatedCode = generateCode(cloned);
470
- const code = replaceIds(generatedCode, generateId);
471
- return (0, import_lib$1.parseExpression)(code, { plugins: ["jsx", "typescript"] });
595
+ const appendArraySource = (source, data, copies) => {
596
+ const last = data.elements.at(-1);
597
+ if (!last || copies.length === 0) return source;
598
+ return applyEdits(source, [{
599
+ start: last.end,
600
+ end: last.end,
601
+ content: `, ${copies.join(", ")}`
602
+ }]);
472
603
  };
473
604
  //#endregion
474
605
  //#region src/utils/ast/items.ts
475
- const elementsOf = (code) => {
476
- const ast = parseArrayExpression(code);
477
- if (!ast) return null;
478
- return ast.elements.filter((element) => Boolean(element));
479
- };
606
+ const elementsOf = (code) => parseArrayExpression(code)?.elements ?? null;
480
607
  const kindOf = (element) => import_lib$2.isObjectExpression(element) ? "object" : "primitive";
481
- const toCode = (elements) => {
482
- return generateCode(import_lib$2.arrayExpression(elements));
483
- };
484
608
  const parseItems = (code) => {
485
- const elements = elementsOf(code);
486
- if (!elements) return null;
487
- return elements.map((node, index) => ({
609
+ return elementsOf(code)?.flatMap((node, index) => import_lib$2.isExpression(node) ? [{
488
610
  index,
489
611
  kind: kindOf(node),
490
612
  node
491
- }));
613
+ }] : []) ?? null;
492
614
  };
493
615
  const resolveRenderLeaf = (render, key) => {
494
616
  const leaf = render?.[key];
@@ -542,79 +664,104 @@ const updateArrayItemProperty = (code, index, key, value, render) => {
542
664
  const declaredType = extractObjectProperties(element)[key]?.type ?? "string";
543
665
  const nextValue = buildPropertyValue(value, declaredType, resolveRenderLeaf(render, key), target.value);
544
666
  if (!nextValue) return null;
545
- target.value = nextValue;
546
- return toCode(elements);
667
+ let serialized = typeof value === "string" && (declaredType === "array" || declaredType === "object" || import_lib$2.isJSXElement(nextValue) || import_lib$2.isJSXFragment(nextValue)) ? value.trim() : generateCode(nextValue);
668
+ if (nextValue.trailingComments?.some((comment) => comment.type === "CommentLine")) serialized += "\n";
669
+ const content = target.shorthand ? `${key}: ${serialized}` : serialized;
670
+ return applyEdits(code, [{
671
+ start: target.value.start,
672
+ end: target.value.end,
673
+ content
674
+ }]);
547
675
  };
548
676
  const updateArrayItemValue = (code, index, value) => {
549
677
  const elements = elementsOf(code);
550
678
  const element = elements?.[index];
551
- if (!elements || !element) return null;
679
+ if (!elements || !import_lib$2.isExpression(element)) return null;
552
680
  const nextValue = createNodeFromValue(extractNodeValue(element).type, parseValue(value));
553
681
  if (!nextValue) return null;
554
- elements[index] = nextValue;
555
- return toCode(elements);
682
+ return applyEdits(code, [{
683
+ start: element.start,
684
+ end: element.end,
685
+ content: generateCode(nextValue)
686
+ }]);
556
687
  };
557
688
  const moveArrayItem = (code, from, to) => {
558
- const elements = elementsOf(code);
559
- if (!elements?.[from]) return null;
560
- const next = [...elements];
689
+ const data = denseArraySource(code);
690
+ if (!data || !validArrayIndices([from], data.elements.length) || !Number.isInteger(to)) return null;
691
+ const next = [...data.elements];
561
692
  const [moved] = next.splice(from, 1);
562
693
  next.splice(to, 0, moved);
563
- return toCode(next);
694
+ return reorderArraySource(code, data.elements, next);
564
695
  };
565
696
  const moveArrayItems = (code, indices, direction) => {
566
- const elements = elementsOf(code);
567
- if (!elements) return null;
568
- const { items, indices: nextIndices } = moveSelectedIndices(elements, indices, direction);
697
+ const data = denseArraySource(code);
698
+ if (!data || !validArrayIndices(indices, data.elements.length) || !["up", "down"].includes(direction)) return null;
699
+ const next = moveSelectedIndices(data.elements, indices, direction);
569
700
  return {
570
- code: toCode(items),
571
- indices: nextIndices
701
+ code: reorderArraySource(code, data.elements, next.items),
702
+ indices: next.indices
572
703
  };
573
704
  };
574
705
  const removeArrayItems = (code, indices, kind) => {
575
- const elements = elementsOf(code);
576
- if (!elements) return null;
577
- const remaining = removeIndices(elements, indices);
578
- if ((kind === void 0 ? remaining : remaining.filter((element) => kindOf(element) === kind)).length < 1) return null;
579
- return toCode(remaining);
580
- };
581
- const cloneItem = (element, generateId) => {
582
- const cloned = clone(element);
583
- if (!import_lib$2.isObjectExpression(cloned)) return cloned;
584
- const editable = extractObjectProperties(cloned);
585
- cloned.properties.forEach((property) => {
586
- if (!import_lib$2.isObjectProperty(property) || !import_lib$2.isIdentifier(property.key)) return;
587
- const key = property.key.name;
588
- if (key === "key" && import_lib$2.isStringLiteral(property.value)) {
589
- property.value = import_lib$2.stringLiteral(`${property.value.value}-${generateId()}`);
706
+ const data = denseArraySource(code);
707
+ if (!data || !validArrayIndices(indices, data.elements.length)) return null;
708
+ const remaining = removeIndices(data.elements, indices);
709
+ if (!(kind === void 0 ? remaining : remaining.filter((node) => kindOf(node) === kind)).length) return null;
710
+ return removeArraySource(code, data, indices);
711
+ };
712
+ const copyItem = (code, element, generateId, usedIds) => {
713
+ const edits = [];
714
+ let unsupported = false;
715
+ const reserveId = (id) => {
716
+ if (usedIds.has(id)) unsupported = true;
717
+ usedIds.add(id);
718
+ return JSON.stringify(id);
719
+ };
720
+ import_lib$2.traverseFast(element, (node) => {
721
+ if (!import_lib$2.isJSXOpeningElement(node)) return;
722
+ const ids = node.attributes.filter((attr) => import_lib$2.isJSXAttribute(attr) && import_lib$2.isJSXIdentifier(attr.name, { name: "data-id" }));
723
+ const attr = ids[0];
724
+ if (ids.length > 1 || attr && (!import_lib$2.isStringLiteral(attr.value) || node.attributes.slice(node.attributes.indexOf(attr) + 1).some((a) => import_lib$2.isJSXSpreadAttribute(a)))) {
725
+ unsupported = true;
590
726
  return;
591
727
  }
592
- const source = editable[key];
593
- if (source) {
594
- const next = createNodeFromValue(source.type, source.value);
595
- if (next) property.value = next;
596
- }
728
+ if (attr && import_lib$2.isStringLiteral(attr.value)) edits.push({
729
+ start: attr.value.start,
730
+ end: attr.value.end,
731
+ content: reserveId(generateId())
732
+ });
597
733
  });
598
- return cloned;
734
+ if (import_lib$2.isObjectExpression(element)) {
735
+ const keys = element.properties.filter((prop) => import_lib$2.isObjectProperty(prop) && !prop.computed && (import_lib$2.isIdentifier(prop.key, { name: "key" }) || import_lib$2.isStringLiteral(prop.key, { value: "key" })));
736
+ const key = keys[0];
737
+ if (keys.length > 1 || key && (!import_lib$2.isStringLiteral(key.value) || element.properties.slice(element.properties.indexOf(key) + 1).some((prop) => import_lib$2.isSpreadElement(prop) || "computed" in prop && prop.computed))) return null;
738
+ if (key && import_lib$2.isStringLiteral(key.value)) edits.push({
739
+ start: key.value.start,
740
+ end: key.value.end,
741
+ content: reserveId(`${key.value.value}-${generateId()}`)
742
+ });
743
+ }
744
+ if (unsupported) return null;
745
+ return applyEdits(code.slice(element.start, element.end), edits.map((edit) => ({
746
+ ...edit,
747
+ start: edit.start - element.start,
748
+ end: edit.end - element.start
749
+ })));
599
750
  };
600
751
  const duplicateArrayItems = (code, indices, generateId = () => nanoid(6)) => {
601
- const elements = elementsOf(code);
602
- if (!elements) return null;
603
- const clones = [...indices].sort((a, b) => a - b).map((index) => elements[index]).filter((element) => Boolean(element)).map((element) => cloneItem(element, generateId));
604
- if (clones.length === 0) return null;
605
- return toCode([...elements, ...clones]);
752
+ const data = denseArraySource(code);
753
+ if (!data || !indices.size || !validArrayIndices(indices, data.elements.length)) return null;
754
+ const usedIds = /* @__PURE__ */ new Set();
755
+ import_lib$2.traverseFast(data.array, (node) => {
756
+ if (import_lib$2.isJSXAttribute(node) && import_lib$2.isJSXIdentifier(node.name, { name: "data-id" }) && import_lib$2.isStringLiteral(node.value)) usedIds.add(node.value.value);
757
+ else if (import_lib$2.isObjectProperty(node) && !node.computed && (import_lib$2.isIdentifier(node.key, { name: "key" }) || import_lib$2.isStringLiteral(node.key, { value: "key" })) && import_lib$2.isStringLiteral(node.value)) usedIds.add(node.value.value);
758
+ });
759
+ const copies = [...indices].sort((a, b) => a - b).map((index) => copyItem(code, data.elements[index], generateId, usedIds));
760
+ return copies.every((copy) => copy !== null) ? appendArraySource(code, data, copies) : null;
606
761
  };
607
762
  const appendArrayItem = (code, kind, generateId = () => nanoid(6)) => {
608
- const elements = elementsOf(code);
609
- if (!elements) return null;
610
- const template = elements.find((element) => kindOf(element) === kind);
611
- if (!template) return null;
612
- if (kind === "primitive") {
613
- const { type, value } = extractNodeValue(template);
614
- const next = createNodeFromValue(type, value);
615
- return next ? toCode([...elements, next]) : null;
616
- }
617
- return toCode([...elements, cloneItem(template, generateId)]);
763
+ const index = denseArraySource(code)?.elements.findIndex((node) => kindOf(node) === kind) ?? -1;
764
+ return index < 0 ? null : duplicateArrayItems(code, /* @__PURE__ */ new Set([index]), generateId);
618
765
  };
619
766
  //#endregion
620
767
  //#region src/utils/ast/extract.ts
@@ -647,7 +794,7 @@ const parseJSXName = (tagName) => {
647
794
  return expr;
648
795
  };
649
796
  const extractCache = createBoundedCache(CONFIG.CACHE_LIMIT);
650
- const extractAttributes = (attributes) => {
797
+ const extractAttributes = (attributes, source) => {
651
798
  const allAttrs = [];
652
799
  const dataAttrs = [];
653
800
  for (const attr of attributes) {
@@ -660,7 +807,9 @@ const extractAttributes = (attributes) => {
660
807
  value = attr.value.value;
661
808
  isStringLiteral = true;
662
809
  } else if (import_lib$2.isJSXExpressionContainer(attr.value)) try {
663
- value = generateCode(attr.value.expression);
810
+ const expression = attr.value.expression;
811
+ const structural = import_lib$2.isArrayExpression(unwrapExpression(expression)) || import_lib$2.isObjectExpression(unwrapExpression(expression));
812
+ value = source && structural && expression.start != null && expression.end != null ? source.slice(expression.start, expression.end) : generateCode(expression);
664
813
  isStringLiteral = false;
665
814
  } catch {
666
815
  value = null;
@@ -736,15 +885,18 @@ const createFragmentNode = (textContent, children) => ({
736
885
  children,
737
886
  isFragment: true
738
887
  });
739
- const processChildrenBinding = (jsxElement, processedNodes, shouldWrap = true) => {
888
+ const processChildrenBinding = (jsxElement, processedNodes, shouldWrap = true, source) => {
740
889
  const jsxChildren = jsxElement.children.filter((child) => import_lib$2.isJSXElement(child) || import_lib$2.isJSXFragment(child));
741
890
  if (!jsxChildren.length) return;
742
891
  const childrenNodes = [];
743
892
  jsxChildren.forEach((child) => {
744
893
  if (import_lib$2.isJSXElement(child)) {
745
- const childResults = extractFromNode(child, processedNodes);
894
+ const childResults = extractFromNode(child, processedNodes, source);
746
895
  if (shouldWrap) {
747
896
  const wrapperNode = createWrapperNode(collectText(child.children), childResults);
897
+ wrapperNode.source = source?.slice(child.start, child.end);
898
+ const dataId = childResults[0]?.dataAttributes.find((attr) => attr.name === DATA_ATTR.ID)?.value;
899
+ if (dataId) wrapperNode.id = `child:${dataId}`;
748
900
  childrenNodes.push(wrapperNode);
749
901
  } else childrenNodes.push(...childResults);
750
902
  } else if (import_lib$2.isJSXFragment(child)) {
@@ -752,12 +904,13 @@ const processChildrenBinding = (jsxElement, processedNodes, shouldWrap = true) =
752
904
  const fragmentChildren = [];
753
905
  child.children.forEach((fragmentChild) => {
754
906
  if (import_lib$2.isJSXElement(fragmentChild)) {
755
- const childResults = extractFromNode(fragmentChild, processedNodes);
907
+ const childResults = extractFromNode(fragmentChild, processedNodes, source);
756
908
  fragmentChildren.push(...childResults);
757
909
  }
758
910
  });
759
911
  if (fragmentChildren.length) {
760
912
  const fragmentNode = createFragmentNode(collectText(child.children), fragmentChildren);
913
+ fragmentNode.source = source?.slice(child.start, child.end);
761
914
  childrenNodes.push(fragmentNode);
762
915
  }
763
916
  }
@@ -788,10 +941,10 @@ const getBindingExpression = (opening) => {
788
941
  }
789
942
  return null;
790
943
  };
791
- const readNodeBindingInfo = (node) => {
944
+ const readNodeBindingInfo = (node, source) => {
792
945
  const opening = node.openingElement;
793
946
  const tagName = getTagName(opening);
794
- const { allAttrs, dataAttrs } = extractAttributes(opening.attributes);
947
+ const { allAttrs, dataAttrs } = extractAttributes(opening.attributes, source);
795
948
  const bindingAttr = dataAttrs.find((attr) => attr.name === DATA_ATTR.BINDING);
796
949
  const bindingExpr = getBindingExpression(opening);
797
950
  const bindings = bindingExpr ? parseBindingExpression(bindingExpr) : bindingAttr?.value ? parseBinding(bindingAttr.value) : [];
@@ -821,10 +974,10 @@ const parseToNodes = (raw) => {
821
974
  const processedNodes = /* @__PURE__ */ new WeakSet();
822
975
  traverse(ast, { JSXElement(path) {
823
976
  if (processedNodes.has(path.node)) return;
824
- const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, arrayBindings, rawChildren } = readNodeBindingInfo(path.node);
977
+ const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, arrayBindings, rawChildren } = readNodeBindingInfo(path.node, wrapped);
825
978
  if (!tagName || !dataAttrs.length) return;
826
979
  let childrenNodes;
827
- if (childrenBinding) childrenNodes = processChildrenBinding(path.node, processedNodes);
980
+ if (childrenBinding) childrenNodes = processChildrenBinding(path.node, processedNodes, true, wrapped);
828
981
  for (const arrayBinding of arrayBindings) skipItemsChildren(path.node, processedNodes, arrayBinding.property);
829
982
  results.push({
830
983
  tagName,
@@ -854,12 +1007,12 @@ function extract(raw) {
854
1007
  extractCache.set(raw, results);
855
1008
  return results;
856
1009
  }
857
- function extractFromNode(node, processedNodes) {
1010
+ function extractFromNode(node, processedNodes, source) {
858
1011
  processedNodes?.add(node);
859
- const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, rawChildren } = readNodeBindingInfo(node);
1012
+ const { tagName, allAttrs, dataAttrs, bindings, childrenBinding, rawChildren } = readNodeBindingInfo(node, source);
860
1013
  let childrenNodes;
861
- if (childrenBinding) childrenNodes = processChildrenBinding(node, processedNodes);
862
- if (!childrenNodes) childrenNodes = processChildrenBinding(node, processedNodes, false);
1014
+ if (childrenBinding) childrenNodes = processChildrenBinding(node, processedNodes, true, source);
1015
+ if (!childrenNodes) childrenNodes = processChildrenBinding(node, processedNodes, false, source);
863
1016
  return [{
864
1017
  tagName,
865
1018
  attributes: allAttrs,
@@ -874,77 +1027,150 @@ function clearExtractCache() {
874
1027
  extractCache.clear();
875
1028
  }
876
1029
  //#endregion
877
- //#region src/utils/ast/patch.ts
878
- const hasOnlyLayoutNewlines = (content) => {
879
- let index = 0;
880
- while (index < content.length) {
881
- const char = content[index];
882
- if (char === "`" || char === "<") return false;
883
- if (char === "\"" || char === "'") {
884
- const next = skipStringLiteral(content, index);
885
- if (next === -1) return false;
886
- index = next;
887
- continue;
1030
+ //#region src/utils/ast/children.ts
1031
+ const modeledSignatures = (nodes) => nodes.map((node) => {
1032
+ const jsx = nodeToJSX(node);
1033
+ if (!jsx) throw new Error("Invalid child node");
1034
+ return generateCode(jsx);
1035
+ });
1036
+ const getChildrenSignatures = (nodes) => {
1037
+ const modeled = modeledSignatures(nodes);
1038
+ return nodes.map((node, index) => JSON.stringify([node.source ?? null, modeled[index]]));
1039
+ };
1040
+ const indicesAreValid = (indices, count) => Array.isArray(indices) && new Set(indices).size === indices.length && indices.every((index) => Number.isInteger(index) && index >= 0 && index < count);
1041
+ const copySource = (source, usedIds) => {
1042
+ const node = (0, import_lib$1.parseExpression)(source, { plugins: ["jsx", "typescript"] });
1043
+ const edits = [];
1044
+ const newId = () => {
1045
+ let id = nanoid(12);
1046
+ while (usedIds.has(id)) id = nanoid(12);
1047
+ usedIds.add(id);
1048
+ return id;
1049
+ };
1050
+ import_lib$2.traverseFast(node, (child) => {
1051
+ if (!import_lib$2.isJSXOpeningElement(child)) return;
1052
+ const ids = child.attributes.filter((attr) => import_lib$2.isJSXAttribute(attr) && import_lib$2.isJSXIdentifier(attr.name, { name: "data-id" }));
1053
+ if (ids.length > 1) throw new Error("Duplicate data-id attributes");
1054
+ const attr = ids[0];
1055
+ if (attr && child.attributes.slice(child.attributes.indexOf(attr) + 1).some((attribute) => import_lib$2.isJSXSpreadAttribute(attribute))) throw new Error("A spread may override the copied data-id");
1056
+ if (attr) {
1057
+ const value = attr.value;
1058
+ if (!import_lib$2.isStringLiteral(value) || value.start == null || value.end == null) throw new Error("Cannot copy a dynamic data-id");
1059
+ edits.push({
1060
+ start: value.start,
1061
+ end: value.end,
1062
+ content: JSON.stringify(newId())
1063
+ });
1064
+ } else {
1065
+ const at = child.attributes.at(-1)?.end ?? child.name.end;
1066
+ if (at == null) throw new Error("Missing opening element span");
1067
+ edits.push({
1068
+ start: at,
1069
+ end: at,
1070
+ content: ` data-id="${newId()}"`
1071
+ });
888
1072
  }
889
- if (char === "/") {
890
- const following = content[index + 1];
891
- if (following === "/") {
892
- const lineEnd = content.indexOf("\n", index + 2);
893
- index = lineEnd === -1 ? content.length : lineEnd;
894
- continue;
895
- }
896
- if (following === "*") {
897
- const commentEnd = content.indexOf("*/", index + 2);
898
- if (commentEnd === -1) return false;
899
- index = commentEnd + 2;
900
- continue;
1073
+ });
1074
+ return applyEdits(source, edits);
1075
+ };
1076
+ const editChildrenSource = (source, parent, input) => {
1077
+ try {
1078
+ if (!parent.closingElement || parent.start == null || parent.end == null) return null;
1079
+ const value = typeof input === "string" ? JSON.parse(input) : input;
1080
+ const children = parent.children.filter((child) => import_lib$2.isJSXElement(child) || import_lib$2.isJSXFragment(child));
1081
+ const raw = children.map((child) => source.slice(child.start, child.end));
1082
+ const models = extract(source.slice(parent.start, parent.end))[0]?.children ?? [];
1083
+ if (models.length !== children.length) return null;
1084
+ const signatures = modeledSignatures(models);
1085
+ const replaceSlots = (next) => {
1086
+ const edits = children.map((child, index) => ({
1087
+ start: child.start,
1088
+ end: child.end,
1089
+ content: next[index] ?? ""
1090
+ }));
1091
+ if (next.length > children.length) {
1092
+ const at = parent.closingElement.start;
1093
+ edits.push({
1094
+ start: at,
1095
+ end: at,
1096
+ content: next.slice(children.length).join("")
1097
+ });
901
1098
  }
902
- return false;
1099
+ return edits;
1100
+ };
1101
+ if (Array.isArray(value)) {
1102
+ const desired = modeledSignatures(value);
1103
+ const lossless = raw.every((text, index) => generateCode((0, import_lib$1.parseExpression)(text, { plugins: ["jsx", "typescript"] })) === signatures[index]);
1104
+ const consumed = /* @__PURE__ */ new Set();
1105
+ const next = desired.map((signature, position) => {
1106
+ const index = signatures.findIndex((candidate, i) => candidate === signature && !consumed.has(i) && (value[position].source === void 0 || value[position].source === raw[i]));
1107
+ if (index >= 0) {
1108
+ consumed.add(index);
1109
+ return raw[index];
1110
+ }
1111
+ if (!lossless || value[position].source !== void 0 && !raw.includes(value[position].source)) throw new Error("Use children commands to copy or edit unsupported source");
1112
+ return signature;
1113
+ });
1114
+ const countIds = (text) => {
1115
+ const counts = /* @__PURE__ */ new Map();
1116
+ import_lib$2.traverseFast((0, import_lib$1.parseExpression)(text, { plugins: ["jsx", "typescript"] }), (node) => {
1117
+ if (import_lib$2.isJSXAttribute(node) && import_lib$2.isJSXIdentifier(node.name, { name: "data-id" }) && import_lib$2.isStringLiteral(node.value)) counts.set(node.value.value, (counts.get(node.value.value) ?? 0) + 1);
1118
+ });
1119
+ return counts;
1120
+ };
1121
+ const previousIds = countIds(source);
1122
+ if ([...countIds(applyEdits(source, replaceSlots(next)))].some(([id, count]) => count > 1 && count > (previousIds.get(id) ?? 0))) return null;
1123
+ return replaceSlots(next);
903
1124
  }
904
- index++;
905
- }
906
- return true;
907
- };
908
- const skipStringLiteral = (content, start) => {
909
- const quote = content[start];
910
- for (let index = start + 1; index < content.length; index++) {
911
- const char = content[index];
912
- if (char === "\\") {
913
- const escaped = content[index + 1];
914
- if (escaped === "\n" || escaped === "\r") return -1;
915
- index++;
916
- continue;
1125
+ const request = value;
1126
+ if (request?.kind !== "children-edit" || !Array.isArray(request.expected) || request.expected.length !== signatures.length || !getChildrenSignatures(models).every((signature, index) => signature === request.expected[index]) || !request.action) return null;
1127
+ const { action } = request;
1128
+ const usedIds = /* @__PURE__ */ new Set();
1129
+ import_lib$2.traverseFast(parent, (node) => {
1130
+ if (import_lib$2.isJSXAttribute(node) && import_lib$2.isJSXIdentifier(node.name, { name: "data-id" }) && import_lib$2.isStringLiteral(node.value)) usedIds.add(node.value.value);
1131
+ });
1132
+ switch (action.type) {
1133
+ case "move": {
1134
+ if (!indicesAreValid([action.from], raw.length) || !indicesAreValid([action.to], raw.length)) return null;
1135
+ const next = [...raw];
1136
+ const [moved] = next.splice(action.from, 1);
1137
+ next.splice(action.to, 0, moved);
1138
+ return replaceSlots(next);
1139
+ }
1140
+ case "move-selected":
1141
+ if (!indicesAreValid(action.indices, raw.length) || !["up", "down"].includes(action.direction)) return null;
1142
+ return replaceSlots(moveSelectedIndices(raw, new Set(action.indices), action.direction).items);
1143
+ case "remove":
1144
+ if (!indicesAreValid(action.indices, raw.length)) return null;
1145
+ return action.indices.map((index) => ({
1146
+ start: children[index].start,
1147
+ end: children[index].end,
1148
+ content: ""
1149
+ }));
1150
+ case "duplicate": {
1151
+ if (!indicesAreValid(action.indices, raw.length)) return null;
1152
+ const content = [...action.indices].sort((a, b) => a - b).map((index) => copySource(raw[index], usedIds)).join("");
1153
+ const at = parent.closingElement.start;
1154
+ return [{
1155
+ start: at,
1156
+ end: at,
1157
+ content
1158
+ }];
1159
+ }
1160
+ case "append": {
1161
+ const content = copySource(raw[0] ?? "<div></div>", usedIds);
1162
+ const at = parent.closingElement.start;
1163
+ return [{
1164
+ start: at,
1165
+ end: at,
1166
+ content
1167
+ }];
1168
+ }
1169
+ default: return null;
917
1170
  }
918
- if (char === quote) return index + 1;
919
- if (char === "\n") return -1;
920
- }
921
- return -1;
922
- };
923
- const lineIndentAt = (source, offset) => {
924
- const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
925
- return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
926
- };
927
- const lineTerminatorOf = (source) => {
928
- return source.includes("\r\n") ? "\r\n" : "\n";
929
- };
930
- const applyEdits = (source, edits) => {
931
- if (edits.length === 0) return source;
932
- const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
933
- 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}`);
934
- for (let i = 1; i < ordered.length; i++) {
935
- const previous = ordered[i - 1];
936
- const current = ordered[i];
937
- if (current.start < previous.end) throw new Error(`Overlapping source edits: [${previous.start}, ${previous.end}) and [${current.start}, ${current.end})`);
938
- }
939
- let result = "";
940
- let cursor = 0;
941
- const terminator = lineTerminatorOf(source);
942
- for (const edit of ordered) {
943
- const content = edit.indent && edit.content.includes("\n") && hasOnlyLayoutNewlines(edit.content) ? edit.content.replace(/\n/g, `${terminator}${lineIndentAt(source, edit.start)}`) : edit.content;
944
- result += source.slice(cursor, edit.start) + content;
945
- cursor = edit.end;
1171
+ } catch {
1172
+ return null;
946
1173
  }
947
- return result + source.slice(cursor);
948
1174
  };
949
1175
  //#endregion
950
1176
  //#region src/utils/ast/update.ts
@@ -1000,23 +1226,6 @@ const editInnerHTML = (element, value) => {
1000
1226
  content: value
1001
1227
  }];
1002
1228
  };
1003
- const editChildren = (element, value) => {
1004
- try {
1005
- const childrenData = typeof value === "string" ? JSON.parse(value) : value;
1006
- const range = childrenRange(element);
1007
- if (!range) return [];
1008
- const content = childrenData.map((childData) => nodeToJSX(childData)).filter((node) => node !== null).map((node) => generateCode(node)).join("");
1009
- return [{
1010
- start: range.start,
1011
- end: range.end,
1012
- content,
1013
- indent: true
1014
- }];
1015
- } catch (error) {
1016
- console.error("❌ Children update error:", error);
1017
- return null;
1018
- }
1019
- };
1020
1229
  const editRichtext = (element, value) => {
1021
1230
  const edits = [];
1022
1231
  const range = childrenRange(element);
@@ -1065,11 +1274,11 @@ const editJsxAttribute = (opening, propertyName, value) => {
1065
1274
  content: `=${content}`
1066
1275
  }];
1067
1276
  };
1068
- const holdsStructuralExpression = (value) => import_lib$2.isJSXExpressionContainer(value) && (import_lib$2.isArrayExpression(value.expression) || import_lib$2.isObjectExpression(value.expression));
1277
+ const holdsStructuralExpression = (value) => import_lib$2.isJSXExpressionContainer(value) && (import_lib$2.isArrayExpression(unwrapExpression(value.expression)) || import_lib$2.isObjectExpression(unwrapExpression(value.expression)));
1069
1278
  const structuralSource = (value) => {
1070
1279
  try {
1071
1280
  const expression = (0, import_lib$1.parseExpression)(value.trim(), { plugins: ["jsx", "typescript"] });
1072
- return import_lib$2.isArrayExpression(expression) || import_lib$2.isObjectExpression(expression) ? expression : null;
1281
+ return import_lib$2.isArrayExpression(unwrapExpression(expression)) || import_lib$2.isObjectExpression(unwrapExpression(expression)) ? expression : null;
1073
1282
  } catch {
1074
1283
  return null;
1075
1284
  }
@@ -1097,6 +1306,18 @@ const buildAttributeValue = (value, type, current) => {
1097
1306
  const editAttribute = (opening, propertyName, value, type) => {
1098
1307
  const attribute = findAttribute(opening, propertyName);
1099
1308
  if (!attribute || attribute.start == null || attribute.end == null) return null;
1309
+ if (typeof value === "string" && (type === "array" || type === "object" || holdsStructuralExpression(attribute.value) && !(type && STRING_VALUED_TYPES.has(type)))) {
1310
+ const expression = structuralSource(value);
1311
+ if (expression && import_lib$2.isJSXExpressionContainer(attribute.value)) {
1312
+ const text = value.trim();
1313
+ const lineComment = expression.trailingComments?.some((comment) => comment.type === "CommentLine");
1314
+ return [{
1315
+ start: attribute.value.expression.start,
1316
+ end: attribute.value.expression.end,
1317
+ content: `${text}${lineComment ? "\n" : ""}`
1318
+ }];
1319
+ }
1320
+ }
1100
1321
  const content = generateCode(import_lib$2.jsxAttribute(attribute.name, buildAttributeValue(value, type, attribute.value)));
1101
1322
  return [{
1102
1323
  start: attribute.start,
@@ -1183,9 +1404,9 @@ const update = (code, dataId, label, value, property) => {
1183
1404
  }));
1184
1405
  break;
1185
1406
  case BINDING_PROP.CHILDREN:
1186
- collect(editChildren(path.node, value), () => ({
1407
+ collect(editChildrenSource(wrapped, path.node, value), () => ({
1187
1408
  reason: "parse-error",
1188
- error: /* @__PURE__ */ new Error(`could not update "${prop}"`)
1409
+ error: /* @__PURE__ */ new Error("Unsupported or stale children edit; source was preserved")
1189
1410
  }));
1190
1411
  break;
1191
1412
  default: collect(propertyBinding.type === "jsx" ? editJsxAttribute(opening, prop, value) : editAttribute(opening, prop, value, propertyBinding.type), () => ({
@@ -1237,6 +1458,24 @@ const bulkUpdate = (raw, entries) => {
1237
1458
  };
1238
1459
  };
1239
1460
  //#endregion
1461
+ //#region src/utils/ast/tree.ts
1462
+ const replaceIds = (code, generateId = () => nanoid(6)) => {
1463
+ return code.replace(new RegExp(`${DATA_ATTR.ID}="[^"]*"`, "g"), () => {
1464
+ return `${DATA_ATTR.ID}="${generateId()}"`;
1465
+ });
1466
+ };
1467
+ const fillIds = (code, generateId = () => nanoid(6)) => {
1468
+ return code.replace(new RegExp(`${DATA_ATTR.ID}=""`, "g"), () => {
1469
+ return `${DATA_ATTR.ID}="${generateId()}"`;
1470
+ });
1471
+ };
1472
+ const clone = (element, generateId = () => nanoid(6)) => {
1473
+ const cloned = import_lib$2.cloneNode(element, true);
1474
+ const generatedCode = generateCode(cloned);
1475
+ const code = replaceIds(generatedCode, generateId);
1476
+ return (0, import_lib$1.parseExpression)(code, { plugins: ["jsx", "typescript"] });
1477
+ };
1478
+ //#endregion
1240
1479
  //#region src/utils/ast/validate.ts
1241
1480
  const VALID = { valid: true };
1242
1481
  const validateBindingValue = (binding, value) => {
@@ -1286,6 +1525,6 @@ const validateBindingValue = (binding, value) => {
1286
1525
  return VALID;
1287
1526
  };
1288
1527
  //#endregion
1289
- export { parseValue as A, parseBindingExpression as C, extractObjectProperties as D, extractNodeValue as E, generateCode as M, flattenEditableValue as O, parseBinding as S, createNodeFromValue as T, moveSelectedIndices as _, extract as a, getCurrentValue as b, moveArrayItem as c, removeArrayItems as d, updateArrayItemProperty as f, replaceIds as g, fillIds as h, clearExtractCache as i, setEditableValue as j, parseArrayExpression as k, moveArrayItems as l, clone as m, bulkUpdate as n, appendArrayItem as o, updateArrayItemValue as p, update as r, duplicateArrayItems as s, validateBindingValue as t, parseItems as u, removeIndices as v, arrayExpressionToCode as w, getStructuredValue as x, findEditableChildren as y };
1528
+ export { setEditableValue as A, arrayExpressionToCode as C, flattenEditableValue as D, extractObjectProperties as E, parseArrayExpression as O, parseBindingExpression as S, extractNodeValue as T, updateArrayItemValue as _, bulkUpdate as a, getStructuredValue as b, clearExtractCache as c, duplicateArrayItems as d, moveArrayItem as f, updateArrayItemProperty as g, removeArrayItems as h, replaceIds as i, generateCode as j, parseValue as k, extract as l, parseItems as m, clone as n, update as o, moveArrayItems as p, fillIds as r, getChildrenSignatures as s, validateBindingValue as t, appendArrayItem as u, findEditableChildren as v, createNodeFromValue as w, parseBinding as x, getCurrentValue as y };
1290
1529
 
1291
- //# sourceMappingURL=ast-D6R-dsk0.js.map
1530
+ //# sourceMappingURL=ast-CIe-mhh_.js.map