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