eleva 1.0.0-rc.5 → 1.0.0-rc.6

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.
@@ -1,4 +1,4 @@
1
- /*! Eleva Plugins v1.0.0-rc.5 | MIT License | https://elevajs.com */
1
+ /*! Eleva Plugins v1.0.0-rc.6 | MIT License | https://elevajs.com */
2
2
  /**
3
3
  * A regular expression to match hyphenated lowercase letters.
4
4
  * @private
@@ -1323,6 +1323,68 @@ const RouterPlugin = {
1323
1323
  }
1324
1324
  };
1325
1325
 
1326
+ /**
1327
+ * @class 🔒 TemplateEngine
1328
+ * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
1329
+ * Provides a safe way to evaluate expressions in templates while preventing XSS attacks.
1330
+ * All methods are static and can be called directly on the class.
1331
+ *
1332
+ * @example
1333
+ * const template = "Hello, {{name}}!";
1334
+ * const data = { name: "World" };
1335
+ * const result = TemplateEngine.parse(template, data); // Returns: "Hello, World!"
1336
+ */
1337
+ class TemplateEngine {
1338
+ /**
1339
+ * @private {RegExp} Regular expression for matching template expressions in the format {{ expression }}
1340
+ * @type {RegExp}
1341
+ */
1342
+ static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
1343
+
1344
+ /**
1345
+ * Parses a template string, replacing expressions with their evaluated values.
1346
+ * Expressions are evaluated in the provided data context.
1347
+ *
1348
+ * @public
1349
+ * @static
1350
+ * @param {string} template - The template string to parse.
1351
+ * @param {Record<string, unknown>} data - The data context for evaluating expressions.
1352
+ * @returns {string} The parsed template with expressions replaced by their values.
1353
+ * @example
1354
+ * const result = TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
1355
+ * user: { name: "John", age: 30 }
1356
+ * }); // Returns: "John is 30 years old"
1357
+ */
1358
+ static parse(template, data) {
1359
+ if (typeof template !== "string") return template;
1360
+ return template.replace(this.expressionPattern, (_, expression) => this.evaluate(expression, data));
1361
+ }
1362
+
1363
+ /**
1364
+ * Evaluates an expression in the context of the provided data object.
1365
+ * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
1366
+ * The use of the `with` statement is necessary for expression evaluation but has security implications.
1367
+ * Expressions should be carefully validated before evaluation.
1368
+ *
1369
+ * @public
1370
+ * @static
1371
+ * @param {string} expression - The expression to evaluate.
1372
+ * @param {Record<string, unknown>} data - The data context for evaluation.
1373
+ * @returns {unknown} The result of the evaluation, or an empty string if evaluation fails.
1374
+ * @example
1375
+ * const result = TemplateEngine.evaluate("user.name", { user: { name: "John" } }); // Returns: "John"
1376
+ * const age = TemplateEngine.evaluate("user.age", { user: { age: 30 } }); // Returns: 30
1377
+ */
1378
+ static evaluate(expression, data) {
1379
+ if (typeof expression !== "string") return expression;
1380
+ try {
1381
+ return new Function("data", `with(data) { return ${expression}; }`)(data);
1382
+ } catch {
1383
+ return "";
1384
+ }
1385
+ }
1386
+ }
1387
+
1326
1388
  /**
1327
1389
  * @class 🎯 PropsPlugin
1328
1390
  * @classdesc A plugin that extends Eleva's props data handling to support any type of data structure
@@ -1381,7 +1443,7 @@ const PropsPlugin = {
1381
1443
  * Plugin version
1382
1444
  * @type {string}
1383
1445
  */
1384
- version: "1.0.0-rc.1",
1446
+ version: "1.0.0-rc.2",
1385
1447
  /**
1386
1448
  * Plugin description
1387
1449
  * @type {string}
@@ -1611,6 +1673,167 @@ const PropsPlugin = {
1611
1673
  return await originalMount.call(eleva, container, compName, enhancedProps);
1612
1674
  };
1613
1675
 
1676
+ // Override Eleva's _mountComponents method to enable signal reference passing
1677
+ const originalMountComponents = eleva._mountComponents;
1678
+
1679
+ // Cache to store parent contexts by container element
1680
+ const parentContextCache = new WeakMap();
1681
+ // Store child instances that need signal linking
1682
+ const pendingSignalLinks = new Set();
1683
+ eleva._mountComponents = async (container, children, childInstances) => {
1684
+ for (const [selector, component] of Object.entries(children)) {
1685
+ if (!selector) continue;
1686
+ for (const el of container.querySelectorAll(selector)) {
1687
+ if (!(el instanceof HTMLElement)) continue;
1688
+
1689
+ // Extract props from DOM attributes
1690
+ const extractedProps = eleva._extractProps(el);
1691
+
1692
+ // Get parent context to check for signal references
1693
+ let enhancedProps = extractedProps;
1694
+
1695
+ // Try to find parent context by looking up the DOM tree
1696
+ let parentContext = parentContextCache.get(container);
1697
+ if (!parentContext) {
1698
+ let currentElement = container;
1699
+ while (currentElement && !parentContext) {
1700
+ if (currentElement._eleva_instance && currentElement._eleva_instance.data) {
1701
+ parentContext = currentElement._eleva_instance.data;
1702
+ // Cache the parent context for future use
1703
+ parentContextCache.set(container, parentContext);
1704
+ break;
1705
+ }
1706
+ currentElement = currentElement.parentElement;
1707
+ }
1708
+ }
1709
+ if (enableReactivity && parentContext) {
1710
+ const signalProps = {};
1711
+
1712
+ // Check each extracted prop to see if there's a matching signal in parent context
1713
+ Object.keys(extractedProps).forEach(propName => {
1714
+ if (parentContext[propName] && parentContext[propName] instanceof eleva.signal) {
1715
+ // Found a signal in parent context with the same name as the prop
1716
+ // Pass the signal reference instead of creating a new one
1717
+ signalProps[propName] = parentContext[propName];
1718
+ }
1719
+ });
1720
+
1721
+ // Merge signal props with regular props (signal props take precedence)
1722
+ enhancedProps = {
1723
+ ...extractedProps,
1724
+ ...signalProps
1725
+ };
1726
+ }
1727
+
1728
+ // Create reactive props for non-signal props only
1729
+ let finalProps = enhancedProps;
1730
+ if (enableReactivity) {
1731
+ // Only create reactive props for values that aren't already signals
1732
+ const nonSignalProps = {};
1733
+ Object.entries(enhancedProps).forEach(([key, value]) => {
1734
+ if (!(value && typeof value === "object" && "value" in value && "watch" in value)) {
1735
+ // This is not a signal, create a reactive prop for it
1736
+ nonSignalProps[key] = value;
1737
+ }
1738
+ });
1739
+
1740
+ // Create reactive props only for non-signal values
1741
+ const reactiveNonSignalProps = createReactiveProps(nonSignalProps);
1742
+
1743
+ // Merge signal props with reactive non-signal props
1744
+ finalProps = {
1745
+ ...reactiveNonSignalProps,
1746
+ ...enhancedProps // Signal props take precedence
1747
+ };
1748
+ }
1749
+
1750
+ /** @type {MountResult} */
1751
+ const instance = await eleva.mount(el, component, finalProps);
1752
+ if (instance && !childInstances.includes(instance)) {
1753
+ childInstances.push(instance);
1754
+
1755
+ // If we have extracted props but no parent context yet, mark for later signal linking
1756
+ if (enableReactivity && Object.keys(extractedProps).length > 0 && !parentContext) {
1757
+ pendingSignalLinks.add({
1758
+ instance,
1759
+ extractedProps,
1760
+ container,
1761
+ component
1762
+ });
1763
+ }
1764
+ }
1765
+ }
1766
+ }
1767
+
1768
+ // After mounting all children, try to link signals for pending instances
1769
+ if (enableReactivity && pendingSignalLinks.size > 0) {
1770
+ for (const pending of pendingSignalLinks) {
1771
+ const {
1772
+ instance,
1773
+ extractedProps,
1774
+ container,
1775
+ component
1776
+ } = pending;
1777
+
1778
+ // Try to find parent context again
1779
+ let parentContext = parentContextCache.get(container);
1780
+ if (!parentContext) {
1781
+ let currentElement = container;
1782
+ while (currentElement && !parentContext) {
1783
+ if (currentElement._eleva_instance && currentElement._eleva_instance.data) {
1784
+ parentContext = currentElement._eleva_instance.data;
1785
+ parentContextCache.set(container, parentContext);
1786
+ break;
1787
+ }
1788
+ currentElement = currentElement.parentElement;
1789
+ }
1790
+ }
1791
+ if (parentContext) {
1792
+ const signalProps = {};
1793
+
1794
+ // Check each extracted prop to see if there's a matching signal in parent context
1795
+ Object.keys(extractedProps).forEach(propName => {
1796
+ if (parentContext[propName] && parentContext[propName] instanceof eleva.signal) {
1797
+ signalProps[propName] = parentContext[propName];
1798
+ }
1799
+ });
1800
+
1801
+ // Update the child instance's data with signal references
1802
+ if (Object.keys(signalProps).length > 0) {
1803
+ Object.assign(instance.data, signalProps);
1804
+
1805
+ // Set up signal watchers for the newly linked signals
1806
+ Object.keys(signalProps).forEach(propName => {
1807
+ const signal = signalProps[propName];
1808
+ if (signal && typeof signal.watch === "function") {
1809
+ signal.watch(newValue => {
1810
+ // Trigger a re-render of the child component when the signal changes
1811
+ const childComponent = eleva._components.get(component) || component;
1812
+ if (childComponent && childComponent.template) {
1813
+ const templateResult = typeof childComponent.template === "function" ? childComponent.template(instance.data) : childComponent.template;
1814
+ const newHtml = TemplateEngine.parse(templateResult, instance.data);
1815
+ eleva.renderer.patchDOM(instance.container, newHtml);
1816
+ }
1817
+ });
1818
+ }
1819
+ });
1820
+
1821
+ // Initial re-render to show the correct signal values
1822
+ const childComponent = eleva._components.get(component) || component;
1823
+ if (childComponent && childComponent.template) {
1824
+ const templateResult = typeof childComponent.template === "function" ? childComponent.template(instance.data) : childComponent.template;
1825
+ const newHtml = TemplateEngine.parse(templateResult, instance.data);
1826
+ eleva.renderer.patchDOM(instance.container, newHtml);
1827
+ }
1828
+ }
1829
+
1830
+ // Remove from pending list
1831
+ pendingSignalLinks.delete(pending);
1832
+ }
1833
+ }
1834
+ }
1835
+ };
1836
+
1614
1837
  /**
1615
1838
  * Expose utility methods on the Eleva instance
1616
1839
  * @namespace eleva.props
@@ -1650,6 +1873,7 @@ const PropsPlugin = {
1650
1873
  // Store original methods for uninstall
1651
1874
  eleva._originalExtractProps = eleva._extractProps;
1652
1875
  eleva._originalMount = originalMount;
1876
+ eleva._originalMountComponents = originalMountComponents;
1653
1877
  },
1654
1878
  /**
1655
1879
  * Uninstalls the plugin from the Eleva instance
@@ -1678,6 +1902,12 @@ const PropsPlugin = {
1678
1902
  delete eleva._originalMount;
1679
1903
  }
1680
1904
 
1905
+ // Restore original _mountComponents method
1906
+ if (eleva._originalMountComponents) {
1907
+ eleva._mountComponents = eleva._originalMountComponents;
1908
+ delete eleva._originalMountComponents;
1909
+ }
1910
+
1681
1911
  // Remove plugin utility methods
1682
1912
  if (eleva.props) {
1683
1913
  delete eleva.props;