ripple 0.2.148 → 0.2.149

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.148",
6
+ "version": "0.2.149",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -81,6 +81,6 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "ripple": "0.2.148"
84
+ "ripple": "0.2.149"
85
85
  }
86
86
  }
@@ -39,6 +39,8 @@ const regex_whitespace_only = /\s/;
39
39
  */
40
40
  function skipWhitespace(parser) {
41
41
  const originalStart = parser.start;
42
+ /** @type {acorn.Position | undefined} */
43
+ let lineInfo;
42
44
  while (
43
45
  parser.start < parser.input.length &&
44
46
  regex_whitespace_only.test(parser.input[parser.start])
@@ -47,11 +49,14 @@ function skipWhitespace(parser) {
47
49
  }
48
50
  // Update line tracking if whitespace was skipped
49
51
  if (parser.start !== originalStart) {
50
- const lineInfo = acorn.getLineInfo(parser.input, parser.start);
52
+ lineInfo = acorn.getLineInfo(parser.input, parser.start);
51
53
  parser.curLine = lineInfo.line;
52
54
  parser.lineStart = parser.start - lineInfo.column;
53
- parser.startLoc = lineInfo;
54
55
  }
56
+
57
+ // After skipping whitespace, update startLoc to reflect our actual position
58
+ // so the next node's start location is correct
59
+ parser.startLoc = lineInfo || acorn.getLineInfo(parser.input, parser.start);
55
60
  }
56
61
 
57
62
  function isWhitespaceTextNode(node) {
@@ -73,6 +78,8 @@ function RipplePlugin(config) {
73
78
  const original = acorn.Parser.prototype;
74
79
  const tt = Parser.tokTypes || acorn.tokTypes;
75
80
  const tc = Parser.tokContexts || acorn.tokContexts;
81
+ const tstt = Parser.acornTypeScript.tokTypes;
82
+ const tstc = Parser.acornTypeScript.tokContexts;
76
83
 
77
84
  class RippleParser extends Parser {
78
85
  /** @type {any[]} */
@@ -214,9 +221,8 @@ function RipplePlugin(config) {
214
221
  if (allWhitespace && this.pos + 1 < this.input.length) {
215
222
  const nextChar = this.input.charCodeAt(this.pos + 1);
216
223
  if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
217
- const tokTypes = this.acornTypeScript.tokTypes;
218
224
  ++this.pos;
219
- return this.finishToken(tokTypes.jsxTagStart);
225
+ return this.finishToken(tstt.jsxTagStart);
220
226
  }
221
227
  }
222
228
  }
@@ -592,7 +598,7 @@ function RipplePlugin(config) {
592
598
  this.expect(tt.braceL);
593
599
  this.enterScope(0);
594
600
  while (this.type !== tt.braceR) {
595
- var stmt = this.parseStatement(null, true);
601
+ const stmt = this.parseStatement(null, true);
596
602
  body.body.push(stmt);
597
603
  }
598
604
  this.next();
@@ -986,7 +992,7 @@ function RipplePlugin(config) {
986
992
  }
987
993
 
988
994
  jsx_parseTupleContainer() {
989
- var t = this.startNode();
995
+ const t = this.startNode();
990
996
  return (
991
997
  this.next(),
992
998
  (t.expression =
@@ -1057,7 +1063,7 @@ function RipplePlugin(config) {
1057
1063
  if (this.type.label === '@') {
1058
1064
  this.next(); // consume @
1059
1065
 
1060
- if (this.type === tt.name || this.type.label === 'jsxName') {
1066
+ if (this.type === tt.name || this.type === tstt.jsxName) {
1061
1067
  node.name = this.value;
1062
1068
  node.tracked = true;
1063
1069
  this.next();
@@ -1066,14 +1072,14 @@ function RipplePlugin(config) {
1066
1072
  this.unexpected();
1067
1073
  }
1068
1074
  } else if (
1069
- (this.type === tt.name || this.type.label === 'jsxName') &&
1075
+ (this.type === tt.name || this.type === tstt.jsxName) &&
1070
1076
  this.value &&
1071
1077
  this.value.startsWith('@')
1072
1078
  ) {
1073
1079
  node.name = this.value.substring(1);
1074
1080
  node.tracked = true;
1075
1081
  this.next();
1076
- } else if (this.type === tt.name || this.type.keyword || this.type.label === 'jsxName') {
1082
+ } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1077
1083
  node.name = this.value;
1078
1084
  node.tracked = false; // Explicitly mark as not tracked
1079
1085
  this.next();
@@ -1085,7 +1091,7 @@ function RipplePlugin(config) {
1085
1091
  }
1086
1092
 
1087
1093
  jsx_parseElementName() {
1088
- if (this.type?.label === 'jsxTagEnd') {
1094
+ if (this.type === tstt.jsxTagEnd) {
1089
1095
  return '';
1090
1096
  }
1091
1097
 
@@ -1148,17 +1154,15 @@ function RipplePlugin(config) {
1148
1154
  }
1149
1155
 
1150
1156
  jsx_parseAttributeValue() {
1151
- const tok = this.acornTypeScript.tokTypes;
1152
-
1153
1157
  switch (this.type) {
1154
1158
  case tt.braceL:
1155
- var t = this.jsx_parseExpressionContainer();
1159
+ const t = this.jsx_parseExpressionContainer();
1156
1160
  return (
1157
1161
  'JSXEmptyExpression' === t.expression.type &&
1158
1162
  this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
1159
1163
  t
1160
1164
  );
1161
- case tok.jsxTagStart:
1165
+ case tstt.jsxTagStart:
1162
1166
  case tt.string:
1163
1167
  return this.parseExprAtom();
1164
1168
  default:
@@ -1179,7 +1183,7 @@ function RipplePlugin(config) {
1179
1183
  }
1180
1184
 
1181
1185
  if (this.type === tt._catch) {
1182
- var clause = this.startNode();
1186
+ const clause = this.startNode();
1183
1187
  this.next();
1184
1188
  if (this.eat(tt.parenL)) {
1185
1189
  clause.param = this.parseCatchClauseParam();
@@ -1209,7 +1213,6 @@ function RipplePlugin(config) {
1209
1213
  }
1210
1214
  let out = '',
1211
1215
  chunkStart = this.pos;
1212
- const tok = this.acornTypeScript.tokTypes;
1213
1216
 
1214
1217
  while (true) {
1215
1218
  if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
@@ -1220,7 +1223,7 @@ function RipplePlugin(config) {
1220
1223
  case 123: // '{'
1221
1224
  if (ch === 60 && this.exprAllowed) {
1222
1225
  ++this.pos;
1223
- return this.finishToken(tok.jsxTagStart);
1226
+ return this.finishToken(tstt.jsxTagStart);
1224
1227
  }
1225
1228
  if (ch === 123 && this.exprAllowed) {
1226
1229
  return this.getTokenFromCode(ch);
@@ -1359,7 +1362,6 @@ function RipplePlugin(config) {
1359
1362
  const inside_head = this.#path.findLast(
1360
1363
  (n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
1361
1364
  );
1362
- const tok = this.acornTypeScript.tokTypes;
1363
1365
  // Adjust the start so we capture the `<` as part of the element
1364
1366
  const prev_pos = this.pos;
1365
1367
  this.pos = this.start - 1;
@@ -1444,7 +1446,7 @@ function RipplePlugin(config) {
1444
1446
  }
1445
1447
  this.pos = start + content.length + 1;
1446
1448
 
1447
- this.type = tok.jsxTagStart;
1449
+ this.type = tstt.jsxTagStart;
1448
1450
  this.next();
1449
1451
  if (this.value === '/') {
1450
1452
  this.next();
@@ -1481,7 +1483,7 @@ function RipplePlugin(config) {
1481
1483
  }
1482
1484
  this.pos = start + content.length + 1;
1483
1485
 
1484
- this.type = tok.jsxTagStart;
1486
+ this.type = tstt.jsxTagStart;
1485
1487
  this.next();
1486
1488
  if (this.value === '/') {
1487
1489
  this.next();
@@ -1495,10 +1497,9 @@ function RipplePlugin(config) {
1495
1497
  element.children = [parsed_css];
1496
1498
 
1497
1499
  // Ensure we escape JSX <tag></tag> context
1498
- const tokContexts = this.acornTypeScript.tokContexts;
1499
1500
  const curContext = this.curContext();
1500
1501
 
1501
- if (curContext === tokContexts.tc_expr) {
1502
+ if (curContext === tstc.tc_expr) {
1502
1503
  this.context.pop();
1503
1504
  }
1504
1505
 
@@ -1535,7 +1536,7 @@ function RipplePlugin(config) {
1535
1536
  raise_error();
1536
1537
  }
1537
1538
  this.next();
1538
- if (this.type.label !== 'jsxTagEnd') {
1539
+ if (this.type !== tstt.jsxTagEnd) {
1539
1540
  raise_error();
1540
1541
  }
1541
1542
  this.next();
@@ -1551,10 +1552,9 @@ function RipplePlugin(config) {
1551
1552
  }
1552
1553
  }
1553
1554
  // Ensure we escape JSX <tag></tag> context
1554
- const tokContexts = this.acornTypeScript.tokContexts;
1555
1555
  const curContext = this.curContext();
1556
1556
 
1557
- if (curContext === tokContexts.tc_expr) {
1557
+ if (curContext === tstc.tc_expr) {
1558
1558
  this.context.pop();
1559
1559
  }
1560
1560
  }
@@ -1584,23 +1584,28 @@ function RipplePlugin(config) {
1584
1584
  this.exprAllowed = true;
1585
1585
 
1586
1586
  while (true) {
1587
- const node = super.parseExpression();
1588
- body.push(node);
1589
-
1590
1587
  if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
1591
1588
  return;
1592
1589
  }
1590
+
1591
+ if (this.type === tt.braceL) {
1592
+ const node = this.jsx_parseExpressionContainer();
1593
+ body.push(node);
1594
+ } else {
1595
+ // Parse regular JSX expression (JSXElement, JSXFragment, etc.)
1596
+ const node = super.parseExpression();
1597
+ body.push(node);
1598
+ }
1593
1599
  }
1594
1600
  }
1595
-
1596
- if (this.type.label === '{') {
1601
+ if (this.type === tt.braceL) {
1597
1602
  const node = this.jsx_parseExpressionContainer();
1598
1603
  node.type = node.html ? 'Html' : 'Text';
1599
1604
  delete node.html;
1600
1605
  body.push(node);
1601
- } else if (this.type.label === '}') {
1606
+ } else if (this.type === tt.braceR) {
1602
1607
  return;
1603
- } else if (this.type.label === 'jsxTagStart') {
1608
+ } else if (this.type === tstt.jsxTagStart) {
1604
1609
  this.next();
1605
1610
  if (this.value === '/') {
1606
1611
  this.next();
@@ -1658,7 +1663,7 @@ function RipplePlugin(config) {
1658
1663
 
1659
1664
  // Ensure we're not in JSX context before recursing
1660
1665
  // This is important when elements are parsed at statement level
1661
- if (this.curContext() === this.acornTypeScript.tokContexts.tc_expr) {
1666
+ if (this.curContext() === tstc.tc_expr) {
1662
1667
  this.context.pop();
1663
1668
  }
1664
1669
  }
@@ -1667,14 +1672,12 @@ function RipplePlugin(config) {
1667
1672
  }
1668
1673
 
1669
1674
  parseStatement(context, topLevel, exports) {
1670
- const tok = this.acornTypeScript.tokContexts;
1671
-
1672
1675
  if (
1673
1676
  context !== 'for' &&
1674
1677
  context !== 'if' &&
1675
1678
  this.context.at(-1) === tc.b_stat &&
1676
1679
  this.type === tt.braceL &&
1677
- this.context.some((c) => c === tok.tc_expr)
1680
+ this.context.some((c) => c === tstt.tc_expr)
1678
1681
  ) {
1679
1682
  this.next();
1680
1683
  const node = this.jsx_parseExpressionContainer();
@@ -1739,7 +1742,7 @@ function RipplePlugin(config) {
1739
1742
  }
1740
1743
  }
1741
1744
 
1742
- if (this.type.label === 'jsxTagStart') {
1745
+ if (this.type === tstt.jsxTagStart) {
1743
1746
  this.next();
1744
1747
  if (this.value === '/') {
1745
1748
  this.unexpected();
@@ -1925,7 +1928,11 @@ function get_comment_handlers(source, comments, index = 0) {
1925
1928
  // Handle JSXEmptyExpression - these represent {/* comment */} in JSX
1926
1929
  if (node.type === 'JSXEmptyExpression') {
1927
1930
  // Collect all comments that fall within this JSXEmptyExpression
1928
- while (comments[0] && comments[0].start >= node.start && comments[0].end <= node.end) {
1931
+ while (
1932
+ comments[0] &&
1933
+ comments[0].start >= node.start &&
1934
+ comments[0].end <= node.end
1935
+ ) {
1929
1936
  comment = /** @type {CommentWithLocation} */ (comments.shift());
1930
1937
  (node.innerComments ||= []).push(comment);
1931
1938
  }
@@ -15,6 +15,7 @@ import {
15
15
  TEMPLATE_SVG_NAMESPACE,
16
16
  TEMPLATE_MATHML_NAMESPACE,
17
17
  } from '../../../../constants.js';
18
+ import { DEFAULT_NAMESPACE } from '../../../../runtime/internal/client/constants.js';
18
19
  import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
19
20
  import {
20
21
  is_inside_component,
@@ -1236,31 +1237,36 @@ const visitors = {
1236
1237
  // We visit, but only to gather metadata
1237
1238
  b.call(visit(node.id, { ...state, metadata }));
1238
1239
 
1240
+ // We're calling a component from within svg/mathml context
1241
+ const is_with_ns = state.namespace !== DEFAULT_NAMESPACE;
1242
+
1239
1243
  if (metadata.tracking) {
1244
+ const shared = b.call(
1245
+ '_$_.composite',
1246
+ b.thunk(visit(node.id, state)),
1247
+ id,
1248
+ is_spreading
1249
+ ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1250
+ : b.object(props),
1251
+ );
1240
1252
  state.init.push(
1241
- b.stmt(
1242
- b.call(
1243
- '_$_.composite',
1244
- b.thunk(visit(node.id, state)),
1245
- id,
1246
- is_spreading
1247
- ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1248
- : b.object(props),
1249
- ),
1250
- ),
1253
+ is_with_ns
1254
+ ? b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(shared))
1255
+ : b.stmt(shared),
1251
1256
  );
1252
1257
  } else {
1258
+ const shared = b.call(
1259
+ visit(node.id, state),
1260
+ id,
1261
+ is_spreading
1262
+ ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1263
+ : b.object(props),
1264
+ b.id('_$_.active_block'),
1265
+ );
1253
1266
  state.init.push(
1254
- b.stmt(
1255
- b.call(
1256
- visit(node.id, state),
1257
- id,
1258
- is_spreading
1259
- ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1260
- : b.object(props),
1261
- b.id('_$_.active_block'),
1262
- ),
1263
- ),
1267
+ is_with_ns
1268
+ ? b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(shared))
1269
+ : b.stmt(shared),
1264
1270
  );
1265
1271
  }
1266
1272
  }
@@ -2114,6 +2120,7 @@ function transform_children(children, context) {
2114
2120
  node.type === 'TryStatement' ||
2115
2121
  node.type === 'ForOfStatement' ||
2116
2122
  node.type === 'SwitchStatement' ||
2123
+ node.type === 'TsxCompat' ||
2117
2124
  node.type === 'Html' ||
2118
2125
  (node.type === 'Element' &&
2119
2126
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
package/src/constants.js CHANGED
@@ -4,4 +4,3 @@ export const IS_CONTROLLED = 1 << 2;
4
4
  export const IS_INDEXED = 1 << 3;
5
5
  export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
6
6
  export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
7
-
@@ -1,8 +1,8 @@
1
1
  /** @import { Block, Component } from '#client' */
2
2
 
3
3
  import { branch, destroy_block, render, render_spread } from './blocks.js';
4
- import { COMPOSITE_BLOCK } from './constants.js';
5
- import { active_block } from './runtime.js';
4
+ import { COMPOSITE_BLOCK, NAMESPACE_URI, DEFAULT_NAMESPACE } from './constants.js';
5
+ import { active_block, active_namespace } from './runtime.js';
6
6
 
7
7
  /**
8
8
  * @typedef {((anchor: Node, props: Record<string, any>, block: Block | null) => void)} ComponentFunction
@@ -36,9 +36,14 @@ export function composite(get_component, node, props) {
36
36
  b = branch(() => {
37
37
  var block = /** @type {Block} */ (active_block);
38
38
 
39
- var element = document.createElement(
40
- /** @type {keyof HTMLElementTagNameMap} */ (component),
41
- );
39
+ var element =
40
+ active_namespace !== DEFAULT_NAMESPACE
41
+ ? document.createElementNS(
42
+ NAMESPACE_URI[active_namespace],
43
+ /** @type {keyof HTMLElementTagNameMap} */ (component),
44
+ )
45
+ : document.createElement(/** @type {keyof HTMLElementTagNameMap} */ (component));
46
+
42
47
  /** @type {ChildNode} */ (anchor).before(element);
43
48
 
44
49
  if (block.s === null) {
@@ -30,3 +30,9 @@ export var REF_PROP = 'ref';
30
30
  /** @type {unique symbol} */
31
31
  export const ARRAY_SET_INDEX_AT = Symbol();
32
32
  export const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
33
+ export const DEFAULT_NAMESPACE = 'html';
34
+ export const NAMESPACE_URI = {
35
+ html: 'http://www.w3.org/1999/xhtml',
36
+ svg: 'http://www.w3.org/2000/svg',
37
+ mathml: 'http://www.w3.org/1998/Math/MathML',
38
+ };
@@ -49,6 +49,7 @@ export {
49
49
  tick,
50
50
  proxy_tracked,
51
51
  with_block,
52
+ with_ns,
52
53
  } from './runtime.js';
53
54
 
54
55
  export { composite } from './composite.js';
@@ -1,4 +1,5 @@
1
1
  /** @import { Block, Component, Dependency, Derived, Tracked } from '#client' */
2
+ /** @import { NAMESPACE_URI } from './constants.js' */
2
3
 
3
4
  import { DEV } from 'esm-env';
4
5
  import {
@@ -26,6 +27,7 @@ import {
26
27
  UNINITIALIZED,
27
28
  REF_PROP,
28
29
  TRACKED_OBJECT,
30
+ DEFAULT_NAMESPACE,
29
31
  } from './constants.js';
30
32
  import { capture, suspend } from './try.js';
31
33
  import {
@@ -48,6 +50,8 @@ export let active_reaction = null;
48
50
  export let active_scope = null;
49
51
  /** @type {null | Component} */
50
52
  export let active_component = null;
53
+ /** @type {keyof NAMESPACE_URI} */
54
+ export let active_namespace = DEFAULT_NAMESPACE;
51
55
  /** @type {boolean} */
52
56
  export let is_mutating_allowed = true;
53
57
 
@@ -1163,6 +1167,22 @@ export function pop_component() {
1163
1167
  active_component = component.p;
1164
1168
  }
1165
1169
 
1170
+ /**
1171
+ * @template T
1172
+ * @param {() => T} fn
1173
+ * @param {keyof NAMESPACE_URI} namespace
1174
+ * @returns {T}
1175
+ */
1176
+ export function with_ns(namespace, fn) {
1177
+ var previous_namespace = active_namespace;
1178
+ active_namespace = namespace;
1179
+ try {
1180
+ return fn();
1181
+ } finally {
1182
+ active_namespace = previous_namespace;
1183
+ }
1184
+ }
1185
+
1166
1186
  /**
1167
1187
  * @returns {symbol}
1168
1188
  */
@@ -7,7 +7,7 @@ import {
7
7
  TEMPLATE_MATHML_NAMESPACE,
8
8
  } from '../../../constants.js';
9
9
  import { first_child, is_firefox } from './operations.js';
10
- import { active_block } from './runtime.js';
10
+ import { active_block, active_namespace } from './runtime.js';
11
11
 
12
12
  /**
13
13
  * Assigns start and end nodes to the active block's state.
@@ -64,15 +64,16 @@ export function template(content, flags) {
64
64
  var use_mathml_namespace = (flags & TEMPLATE_MATHML_NAMESPACE) !== 0;
65
65
  /** @type {Node | DocumentFragment | undefined} */
66
66
  var node;
67
- var has_start = !content.startsWith('<!>');
67
+ var is_comment = content === '<!>';
68
+ var has_start = !is_comment && !content.startsWith('<!>');
68
69
 
69
70
  return () => {
71
+ // If using runtime namespace, check active_namespace
72
+ var svg = !is_comment && (use_svg_namespace || active_namespace === 'svg');
73
+ var mathml = !is_comment && (use_mathml_namespace || active_namespace === 'mathml');
74
+
70
75
  if (node === undefined) {
71
- node = create_fragment_from_html(
72
- has_start ? content : '<!>' + content,
73
- use_svg_namespace,
74
- use_mathml_namespace,
75
- );
76
+ node = create_fragment_from_html(has_start ? content : '<!>' + content, svg, mathml);
76
77
  if (!is_fragment) node = /** @type {Node} */ (first_child(node));
77
78
  }
78
79
 
@@ -119,8 +120,9 @@ function from_namespace(content, ns = 'svg') {
119
120
  var root = /** @type {Element} */ (first_child(fragment));
120
121
  var result = document.createDocumentFragment();
121
122
 
122
- while (first_child(root)) {
123
- result.appendChild(/** @type {Node} */ (first_child(root)));
123
+ var first;
124
+ while ((first = first_child(root))) {
125
+ result.appendChild(/** @type {Node} */ (first));
124
126
  }
125
127
 
126
128
  return result;
@@ -1,3 +1,5 @@
1
+ import { track } from 'ripple';
2
+
1
3
  describe('SVG namespace handling', () => {
2
4
  it('should render static SVG elements with correct namespace', () => {
3
5
  component App() {
@@ -228,6 +230,107 @@ describe('SVG namespace handling', () => {
228
230
  expect(div.textContent).toBe('HTML inside SVG');
229
231
  });
230
232
 
233
+ it('should render SVG with children as svg elements', () => {
234
+ component SVG({ children }) {
235
+ <svg width={20} height={20} fill="blue" viewBox="0 0 30 10" preserveAspectRatio="none">
236
+ <children />
237
+ </svg>
238
+ }
239
+
240
+ component App() {
241
+ let isDiamond = true;
242
+ <SVG>
243
+ if (isDiamond) {
244
+ <polygon points="0,0 30,0 15,10" />
245
+ } else {
246
+ <polygon points="0,0 30,0 15,10" />
247
+ }
248
+ </SVG>
249
+ }
250
+
251
+ render(App);
252
+
253
+ const svg = container.querySelector('svg');
254
+ const polygon = container.querySelector('polygon');
255
+
256
+ expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg');
257
+ expect(polygon.namespaceURI).toBe('http://www.w3.org/2000/svg');
258
+ });
259
+
260
+ it('should render SVG with props as svg elements', () => {
261
+ component SVG({ Polygon }) {
262
+ <svg width={20} height={20} fill="blue" viewBox="0 0 30 10" preserveAspectRatio="none">
263
+ <Polygon />
264
+ </svg>
265
+ }
266
+
267
+ component App() {
268
+ <SVG {Polygon} />
269
+ }
270
+
271
+ component Polygon() {
272
+ <polygon points="0,0 30,0 15,10" />
273
+ }
274
+
275
+ render(App);
276
+
277
+ const svg = container.querySelector('svg');
278
+ const polygon = container.querySelector('polygon');
279
+
280
+ expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg');
281
+ expect(polygon.namespaceURI).toBe('http://www.w3.org/2000/svg');
282
+ });
283
+
284
+ it('should render SVG with children as dynamic elements', () => {
285
+ component SVG({ children }) {
286
+ <svg width={20} height={20} fill="blue" viewBox="0 0 30 10" preserveAspectRatio="none">
287
+ <children />
288
+ </svg>
289
+ }
290
+
291
+ component App() {
292
+ let dynTag = track('polygon');
293
+ <SVG>
294
+ <@dynTag points="0,0 30,0 15,10" />
295
+ </SVG>
296
+ }
297
+
298
+ render(App);
299
+
300
+ const svg = container.querySelector('svg');
301
+ const polygon = container.querySelector('polygon');
302
+
303
+ expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg');
304
+ expect(polygon.namespaceURI).toBe('http://www.w3.org/2000/svg');
305
+ });
306
+
307
+ it('should render SVG with children as dynamic components', () => {
308
+ component SVG({ children }) {
309
+ <svg width={20} height={20} fill="blue" viewBox="0 0 30 10" preserveAspectRatio="none">
310
+ <children />
311
+ </svg>
312
+ }
313
+
314
+ component Polygon({ points }) {
315
+ <polygon {points} />
316
+ }
317
+
318
+ component App() {
319
+ let Component = track(() => Polygon);
320
+ <SVG>
321
+ <@Component points="0,0 30,0 15,10" />
322
+ </SVG>
323
+ }
324
+
325
+ render(App);
326
+
327
+ const svg = container.querySelector('svg');
328
+ const polygon = container.querySelector('polygon');
329
+
330
+ expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg');
331
+ expect(polygon.namespaceURI).toBe('http://www.w3.org/2000/svg');
332
+ });
333
+
231
334
  it('should compare static vs dynamic SVG rendering (original problem case)', () => {
232
335
  component App() {
233
336
  const d = [