@jbpark/live-editor 1.19.0 → 2.0.1

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.
@@ -57,7 +57,13 @@ const dedent = (str) => {
57
57
  }, Infinity);
58
58
  return indent === Infinity ? str.trim() : lines.map((line) => line.slice(indent)).join("\n");
59
59
  };
60
- const evaluateLiteral = (node) => {
60
+ const unwrapExpression = (node) => {
61
+ let current = node;
62
+ while (import_lib$2.isTSAsExpression(current) || import_lib$2.isTSSatisfiesExpression(current) || import_lib$2.isTSNonNullExpression(current) || import_lib$2.isTSTypeAssertion(current) || import_lib$2.isParenthesizedExpression(current)) current = current.expression;
63
+ return current;
64
+ };
65
+ const evaluateLiteral = (rawNode) => {
66
+ const node = unwrapExpression(rawNode);
61
67
  if (import_lib$2.isStringLiteral(node)) return node.value;
62
68
  if (import_lib$2.isNumericLiteral(node)) return node.value;
63
69
  if (import_lib$2.isBooleanLiteral(node)) return node.value;
@@ -204,9 +210,26 @@ const createNodeFromValue = (type, value) => {
204
210
  default: return null;
205
211
  }
206
212
  };
213
+ const valueToExpression = (value) => {
214
+ if (typeof value === "string") return import_lib$2.stringLiteral(value);
215
+ if (typeof value === "number") return value < 0 ? import_lib$2.unaryExpression("-", import_lib$2.numericLiteral(-value)) : import_lib$2.numericLiteral(value);
216
+ if (typeof value === "boolean") return import_lib$2.booleanLiteral(value);
217
+ if (value === null) return import_lib$2.nullLiteral();
218
+ if (Array.isArray(value)) return import_lib$2.arrayExpression(value.map((item) => valueToExpression(item) ?? import_lib$2.nullLiteral()));
219
+ if (typeof value === "object") {
220
+ const properties = [];
221
+ for (const [key, item] of Object.entries(value)) {
222
+ const expr = valueToExpression(item);
223
+ if (expr === null) continue;
224
+ properties.push(import_lib$2.objectProperty(import_lib$2.stringLiteral(key), expr));
225
+ }
226
+ return import_lib$2.objectExpression(properties);
227
+ }
228
+ return null;
229
+ };
207
230
  const parseArrayExpression = (value) => {
208
231
  try {
209
- const ast = (0, import_lib$1.parseExpression)(value, { plugins: ["jsx", "typescript"] });
232
+ const ast = unwrapExpression((0, import_lib$1.parseExpression)(value, { plugins: ["jsx", "typescript"] }));
210
233
  if (!import_lib$2.isArrayExpression(ast)) return null;
211
234
  return ast;
212
235
  } catch (error) {
@@ -294,11 +317,7 @@ const sanitizeRenderMap = (value) => {
294
317
  }
295
318
  return Object.keys(map).length > 0 ? map : void 0;
296
319
  };
297
- const parseBinding = (bindingValue) => {
298
- if (!bindingValue) return [];
299
- const ast = parseArrayExpression(bindingValue);
300
- if (!ast) return [];
301
- const raw = evaluateLiteral(ast);
320
+ const buildBindingItems = (raw) => {
302
321
  if (!Array.isArray(raw)) return [];
303
322
  const items = [];
304
323
  for (const rawItem of raw) {
@@ -339,6 +358,13 @@ const parseBinding = (bindingValue) => {
339
358
  }
340
359
  return items;
341
360
  };
361
+ const parseBinding = (bindingValue) => {
362
+ if (!bindingValue) return [];
363
+ const ast = parseArrayExpression(bindingValue);
364
+ if (!ast) return [];
365
+ return buildBindingItems(evaluateLiteral(ast));
366
+ };
367
+ const parseBindingExpression = (expression) => buildBindingItems(evaluateLiteral(expression));
342
368
  const getCurrentValue = (node, property) => {
343
369
  switch (property) {
344
370
  case BINDING_PROP.INNER_TEXT: return node.textContent || "";
@@ -360,6 +386,28 @@ const getCurrentValue = (node, property) => {
360
386
  default: return node.attributes.find((attr) => attr.name === property)?.value || "";
361
387
  }
362
388
  };
389
+ const STRING_VALUED_TYPES = /* @__PURE__ */ new Set([
390
+ "string",
391
+ "url",
392
+ "date",
393
+ "color",
394
+ "jsx",
395
+ "richtext",
396
+ "icon-picker",
397
+ "asset-picker"
398
+ ]);
399
+ const getStructuredValue = (node, property, type) => {
400
+ switch (property) {
401
+ case BINDING_PROP.INNER_TEXT:
402
+ case BINDING_PROP.INNER_HTML: return getCurrentValue(node, property);
403
+ case BINDING_PROP.CHILDREN: return node.children ?? [];
404
+ default: {
405
+ const raw = getCurrentValue(node, property);
406
+ if (node.attributes.find((a) => a.name === property)?.isStringLiteral || type && STRING_VALUED_TYPES.has(type)) return raw;
407
+ return parseValue(raw);
408
+ }
409
+ }
410
+ };
363
411
  const hasEditableBindings = (node) => {
364
412
  const bindingAttr = node.dataAttributes.find((attr) => attr.name === DATA_ATTR.BINDING);
365
413
  if (!bindingAttr?.value) return false;
@@ -542,12 +590,20 @@ const markProcessedJSX = (node, processedNodes) => {
542
590
  }
543
591
  if (import_lib$2.isJSXExpressionContainer(node) || import_lib$2.isParenthesizedExpression(node)) markProcessedJSX(node.expression, processedNodes);
544
592
  };
593
+ const getBindingExpression = (opening) => {
594
+ for (const attr of opening.attributes) if (import_lib$2.isJSXAttribute(attr) && import_lib$2.isJSXIdentifier(attr.name) && attr.name.name === DATA_ATTR.BINDING && attr.value && import_lib$2.isJSXExpressionContainer(attr.value)) {
595
+ const expression = unwrapExpression(attr.value.expression);
596
+ if (import_lib$2.isArrayExpression(expression)) return expression;
597
+ }
598
+ return null;
599
+ };
545
600
  const readNodeBindingInfo = (node) => {
546
601
  const opening = node.openingElement;
547
602
  const tagName = getTagName(opening);
548
603
  const { allAttrs, dataAttrs } = extractAttributes(opening.attributes);
549
604
  const bindingAttr = dataAttrs.find((attr) => attr.name === DATA_ATTR.BINDING);
550
- const bindings = bindingAttr?.value ? parseBinding(bindingAttr.value) : [];
605
+ const bindingExpr = getBindingExpression(opening);
606
+ const bindings = bindingExpr ? parseBindingExpression(bindingExpr) : bindingAttr?.value ? parseBinding(bindingAttr.value) : [];
551
607
  const childrenBinding = bindings.find((b) => b.property === BINDING_PROP.CHILDREN);
552
608
  const arrayBindings = bindings.filter((b) => b.property === BINDING_PROP.ITEMS || b.type === "array");
553
609
  const innerHtmlBinding = bindings.find((b) => b.property === BINDING_PROP.INNER_HTML);
@@ -627,59 +683,221 @@ function clearExtractCache() {
627
683
  extractCache.clear();
628
684
  }
629
685
  //#endregion
630
- //#region src/utils/ast/update.ts
631
- const updateInnerText = (path, value) => {
632
- const jsxChildren = path.node.children;
633
- for (let i = jsxChildren.length - 1; i >= 0; i--) if (import_lib$2.isJSXText(jsxChildren[i])) jsxChildren.splice(i, 1);
634
- jsxChildren.push(import_lib$2.jsxText(value));
686
+ //#region src/utils/ast/patch.ts
687
+ const hasOnlyLayoutNewlines = (content) => {
688
+ let index = 0;
689
+ while (index < content.length) {
690
+ const char = content[index];
691
+ if (char === "`" || char === "<") return false;
692
+ if (char === "\"" || char === "'") {
693
+ const next = skipStringLiteral(content, index);
694
+ if (next === -1) return false;
695
+ index = next;
696
+ continue;
697
+ }
698
+ if (char === "/") {
699
+ const following = content[index + 1];
700
+ if (following === "/") {
701
+ const lineEnd = content.indexOf("\n", index + 2);
702
+ index = lineEnd === -1 ? content.length : lineEnd;
703
+ continue;
704
+ }
705
+ if (following === "*") {
706
+ const commentEnd = content.indexOf("*/", index + 2);
707
+ if (commentEnd === -1) return false;
708
+ index = commentEnd + 2;
709
+ continue;
710
+ }
711
+ return false;
712
+ }
713
+ index++;
714
+ }
635
715
  return true;
636
716
  };
637
- const injectPlaceholder = (prefix, value, placeholders, { asRawContent = false } = {}) => {
638
- const name = `__${prefix}_${nanoid(6)}__`;
639
- const container = import_lib$2.jsxExpressionContainer(import_lib$2.identifier(name));
640
- placeholders.set(asRawContent ? `{${name}}` : name, value);
641
- return container;
717
+ const skipStringLiteral = (content, start) => {
718
+ const quote = content[start];
719
+ for (let index = start + 1; index < content.length; index++) {
720
+ const char = content[index];
721
+ if (char === "\\") {
722
+ const escaped = content[index + 1];
723
+ if (escaped === "\n" || escaped === "\r") return -1;
724
+ index++;
725
+ continue;
726
+ }
727
+ if (char === quote) return index + 1;
728
+ if (char === "\n") return -1;
729
+ }
730
+ return -1;
642
731
  };
643
- const updateInnerHTML = (path, value, placeholders) => {
644
- path.node.children = [injectPlaceholder("HTML", value, placeholders, { asRawContent: true })];
645
- return true;
732
+ const lineIndentAt = (source, offset) => {
733
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
734
+ return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
646
735
  };
647
- const updateChildren = (path, value) => {
736
+ const lineTerminatorOf = (source) => {
737
+ return source.includes("\r\n") ? "\r\n" : "\n";
738
+ };
739
+ const applyEdits = (source, edits) => {
740
+ if (edits.length === 0) return source;
741
+ const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
742
+ 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}`);
743
+ for (let i = 1; i < ordered.length; i++) {
744
+ const previous = ordered[i - 1];
745
+ const current = ordered[i];
746
+ if (current.start < previous.end) throw new Error(`Overlapping source edits: [${previous.start}, ${previous.end}) and [${current.start}, ${current.end})`);
747
+ }
748
+ let result = "";
749
+ let cursor = 0;
750
+ const terminator = lineTerminatorOf(source);
751
+ for (const edit of ordered) {
752
+ const content = edit.indent && edit.content.includes("\n") && hasOnlyLayoutNewlines(edit.content) ? edit.content.replace(/\n/g, `${terminator}${lineIndentAt(source, edit.start)}`) : edit.content;
753
+ result += source.slice(cursor, edit.start) + content;
754
+ cursor = edit.end;
755
+ }
756
+ return result + source.slice(cursor);
757
+ };
758
+ //#endregion
759
+ //#region src/utils/ast/update.ts
760
+ const childrenRange = (element) => {
761
+ const { openingElement, closingElement } = element;
762
+ if (!closingElement || openingElement.end == null || closingElement.start == null) return null;
763
+ return {
764
+ start: openingElement.end,
765
+ end: closingElement.start
766
+ };
767
+ };
768
+ const findAttribute = (opening, propertyName) => {
769
+ return opening.attributes.find((attr) => import_lib$2.isJSXAttribute(attr) && import_lib$2.isJSXIdentifier(attr.name) && attr.name.name === propertyName);
770
+ };
771
+ const attributeInsertPoint = (opening) => {
772
+ return opening.attributes[opening.attributes.length - 1]?.end ?? opening.name.end ?? null;
773
+ };
774
+ const editInnerText = (source, element, value) => {
775
+ const range = childrenRange(element);
776
+ if (!range) return [];
777
+ const [only] = element.children;
778
+ if (element.children.length === 1 && import_lib$2.isJSXText(only) && only.start != null && only.end != null) {
779
+ const raw = source.slice(only.start, only.end);
780
+ if (raw.trim() !== "") {
781
+ const leading = raw.length - raw.trimStart().length;
782
+ const trailing = raw.length - raw.trimEnd().length;
783
+ return [{
784
+ start: only.start + leading,
785
+ end: only.end - trailing,
786
+ content: value
787
+ }];
788
+ }
789
+ }
790
+ const edits = [];
791
+ for (const child of element.children) if (import_lib$2.isJSXText(child) && child.start != null && child.end != null) edits.push({
792
+ start: child.start,
793
+ end: child.end,
794
+ content: ""
795
+ });
796
+ edits.push({
797
+ start: range.end,
798
+ end: range.end,
799
+ content: value
800
+ });
801
+ return edits;
802
+ };
803
+ const editInnerHTML = (element, value) => {
804
+ const range = childrenRange(element);
805
+ if (!range) return [];
806
+ return [{
807
+ start: range.start,
808
+ end: range.end,
809
+ content: value
810
+ }];
811
+ };
812
+ const editChildren = (element, value) => {
648
813
  try {
649
- const childrenData = JSON.parse(value);
650
- path.node.children.length = 0;
651
- childrenData.forEach((childData) => {
652
- const jsxElement = nodeToJSX(childData);
653
- if (jsxElement) path.node.children.push(jsxElement);
654
- });
655
- return true;
814
+ const childrenData = typeof value === "string" ? JSON.parse(value) : value;
815
+ const range = childrenRange(element);
816
+ if (!range) return [];
817
+ const content = childrenData.map((childData) => nodeToJSX(childData)).filter((node) => node !== null).map((node) => generateCode(node)).join("");
818
+ return [{
819
+ start: range.start,
820
+ end: range.end,
821
+ content,
822
+ indent: true
823
+ }];
656
824
  } catch (error) {
657
825
  console.error("❌ Children update error:", error);
658
- return false;
826
+ return null;
659
827
  }
660
828
  };
661
- const updateRichtext = (path, value) => {
662
- path.node.children = [];
663
- const opening = path.node.openingElement;
664
- const htmlObject = import_lib$2.objectExpression([import_lib$2.objectProperty(import_lib$2.identifier("__html"), import_lib$2.stringLiteral(value))]);
665
- const existingAttr = opening.attributes.find((a) => import_lib$2.isJSXAttribute(a) && import_lib$2.isJSXIdentifier(a.name) && a.name.name === "dangerouslySetInnerHTML");
666
- if (existingAttr && import_lib$2.isJSXAttribute(existingAttr)) existingAttr.value = import_lib$2.jsxExpressionContainer(htmlObject);
667
- else opening.attributes.push(import_lib$2.jsxAttribute(import_lib$2.jsxIdentifier("dangerouslySetInnerHTML"), import_lib$2.jsxExpressionContainer(htmlObject)));
668
- return true;
669
- };
670
- const updateAttribute = (opening, propertyName, value) => {
671
- const customAttr = opening.attributes.find((attr) => import_lib$2.isJSXAttribute(attr) && import_lib$2.isJSXIdentifier(attr.name) && attr.name.name === propertyName);
672
- if (customAttr && import_lib$2.isJSXAttribute(customAttr)) {
673
- const trimmed = value.trim();
674
- const isExpression = trimmed.startsWith("[") || trimmed.startsWith("{") || REGEX.NUMBER.test(trimmed) || REGEX.BOOLEAN_OR_NULL.test(trimmed);
675
- customAttr.value = attrValue({
676
- name: propertyName,
677
- value,
678
- isStringLiteral: !isExpression
829
+ const editRichtext = (element, value) => {
830
+ const edits = [];
831
+ const range = childrenRange(element);
832
+ if (range && range.start !== range.end) edits.push({
833
+ start: range.start,
834
+ end: range.end,
835
+ content: ""
836
+ });
837
+ const opening = element.openingElement;
838
+ const attribute = import_lib$2.jsxAttribute(import_lib$2.jsxIdentifier("dangerouslySetInnerHTML"), import_lib$2.jsxExpressionContainer(import_lib$2.objectExpression([import_lib$2.objectProperty(import_lib$2.identifier("__html"), import_lib$2.stringLiteral(value))])));
839
+ const content = generateCode(attribute);
840
+ const existing = findAttribute(opening, "dangerouslySetInnerHTML");
841
+ if (existing && existing.start != null && existing.end != null) {
842
+ edits.push({
843
+ start: existing.start,
844
+ end: existing.end,
845
+ content,
846
+ indent: true
679
847
  });
680
- return true;
848
+ return edits;
681
849
  }
682
- return false;
850
+ const insertAt = attributeInsertPoint(opening);
851
+ if (insertAt == null) return null;
852
+ edits.push({
853
+ start: insertAt,
854
+ end: insertAt,
855
+ content: ` ${content}`,
856
+ indent: true
857
+ });
858
+ return edits;
859
+ };
860
+ const editJsxAttribute = (opening, propertyName, value) => {
861
+ const attribute = findAttribute(opening, propertyName);
862
+ if (!attribute) return null;
863
+ const content = `{${String(value).trim()}}`;
864
+ if (attribute.value?.start != null && attribute.value.end != null) return [{
865
+ start: attribute.value.start,
866
+ end: attribute.value.end,
867
+ content
868
+ }];
869
+ const insertAt = attribute.name.end;
870
+ if (insertAt == null) return null;
871
+ return [{
872
+ start: insertAt,
873
+ end: insertAt,
874
+ content: `=${content}`
875
+ }];
876
+ };
877
+ const buildAttributeValue = (value, type) => {
878
+ if (type === "array" || type === "object") {
879
+ if (typeof value === "string") try {
880
+ return import_lib$2.jsxExpressionContainer((0, import_lib$1.parseExpression)(value.trim(), { plugins: ["jsx", "typescript"] }));
881
+ } catch {
882
+ return import_lib$2.stringLiteral(value);
883
+ }
884
+ const expr = valueToExpression(value);
885
+ return expr ? import_lib$2.jsxExpressionContainer(expr) : import_lib$2.stringLiteral("");
886
+ }
887
+ if (typeof value === "string") return import_lib$2.stringLiteral(value);
888
+ const expr = valueToExpression(value);
889
+ return expr ? import_lib$2.jsxExpressionContainer(expr) : import_lib$2.stringLiteral(String(value));
890
+ };
891
+ const editAttribute = (opening, propertyName, value, type) => {
892
+ const attribute = findAttribute(opening, propertyName);
893
+ if (!attribute || attribute.start == null || attribute.end == null) return null;
894
+ const content = generateCode(import_lib$2.jsxAttribute(attribute.name, buildAttributeValue(value, type)));
895
+ return [{
896
+ start: attribute.start,
897
+ end: attribute.end,
898
+ content,
899
+ indent: true
900
+ }];
683
901
  };
684
902
  const update = (code, dataId, label, value, property) => {
685
903
  try {
@@ -689,7 +907,12 @@ const update = (code, dataId, label, value, property) => {
689
907
  plugins: ["jsx", "typescript"]
690
908
  });
691
909
  let changed = false;
692
- const jsxPlaceholders = /* @__PURE__ */ new Map();
910
+ const edits = [];
911
+ const collect = (result) => {
912
+ if (!result) return;
913
+ edits.push(...result);
914
+ changed = true;
915
+ };
693
916
  traverse(ast, { JSXElement(path) {
694
917
  const opening = path.node.openingElement;
695
918
  if (!opening.attributes.find((attr) => {
@@ -709,32 +932,23 @@ const update = (code, dataId, label, value, property) => {
709
932
  const propertyBinding = matches[0];
710
933
  switch (propertyBinding.property) {
711
934
  case BINDING_PROP.INNER_TEXT:
712
- changed = updateInnerText(path, value);
935
+ collect(editInnerText(wrapped, path.node, String(value)));
713
936
  break;
714
937
  case BINDING_PROP.INNER_HTML:
715
- if (propertyBinding.type === "richtext") changed = updateRichtext(path, value);
716
- else changed = updateInnerHTML(path, value, jsxPlaceholders);
938
+ collect(propertyBinding.type === "richtext" ? editRichtext(path.node, String(value)) : editInnerHTML(path.node, String(value)));
717
939
  break;
718
940
  case BINDING_PROP.CHILDREN:
719
- changed = updateChildren(path, value);
941
+ collect(editChildren(path.node, value));
720
942
  break;
721
- default: if (propertyBinding.type === "jsx") {
722
- const attr = opening.attributes.find((a) => import_lib$2.isJSXAttribute(a) && import_lib$2.isJSXIdentifier(a.name) && a.name.name === propertyBinding.property);
723
- if (attr && import_lib$2.isJSXAttribute(attr)) {
724
- attr.value = injectPlaceholder("JSX", value.trim(), jsxPlaceholders);
725
- changed = true;
726
- }
727
- } else changed = updateAttribute(opening, propertyBinding.property, value);
943
+ default: collect(propertyBinding.type === "jsx" ? editJsxAttribute(opening, propertyBinding.property, value) : editAttribute(opening, propertyBinding.property, value, propertyBinding.type));
728
944
  }
729
945
  } });
730
946
  if (!changed) return {
731
947
  code,
732
948
  success: false
733
949
  };
734
- let result = unwrap(generateCode(ast));
735
- for (const [placeholder, original] of jsxPlaceholders) result = result.replace(placeholder, () => original);
736
950
  return {
737
- code: result,
951
+ code: unwrap(applyEdits(wrapped, edits)),
738
952
  success: true
739
953
  };
740
954
  } catch (error) {
@@ -787,13 +1001,12 @@ const validateBindingValue = (binding, value) => {
787
1001
  };
788
1002
  return VALID;
789
1003
  }
790
- const numericValue = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)) ? Number(value) : void 0;
791
- if (numericValue !== void 0) {
792
- if (binding.min !== void 0 && numericValue < binding.min) return {
1004
+ if (typeof value === "number") {
1005
+ if (binding.min !== void 0 && value < binding.min) return {
793
1006
  valid: false,
794
1007
  message: `Must be at least ${binding.min}.`
795
1008
  };
796
- if (binding.max !== void 0 && numericValue > binding.max) return {
1009
+ if (binding.max !== void 0 && value > binding.max) return {
797
1010
  valid: false,
798
1011
  message: `Must be at most ${binding.max}.`
799
1012
  };
@@ -827,6 +1040,6 @@ const validateBindingValue = (binding, value) => {
827
1040
  return VALID;
828
1041
  };
829
1042
  //#endregion
830
- export { parseArrayExpression as _, bulkUpdate as a, generateCode as b, extract as c, parseBinding as d, arrayExpressionToCode as f, flattenEditableValue as g, extractObjectProperties as h, replaceIds as i, findEditableChildren as l, extractNodeValue as m, clone as n, update as o, createNodeFromValue as p, fillIds as r, clearExtractCache as s, validateBindingValue as t, getCurrentValue as u, parseValue as v, setEditableValue as y };
1043
+ export { generateCode as S, extractObjectProperties as _, bulkUpdate as a, parseValue as b, extract as c, getStructuredValue as d, parseBinding as f, extractNodeValue as g, createNodeFromValue as h, replaceIds as i, findEditableChildren as l, arrayExpressionToCode as m, clone as n, update as o, parseBindingExpression as p, fillIds as r, clearExtractCache as s, validateBindingValue as t, getCurrentValue as u, flattenEditableValue as v, setEditableValue as x, parseArrayExpression as y };
831
1044
 
832
- //# sourceMappingURL=ast-CBAw3X_T.js.map
1045
+ //# sourceMappingURL=ast-BmGLa00g.js.map