ripple 0.2.147 → 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.147",
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.147"
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
  }
@@ -45,8 +45,6 @@ function mark_control_flow_has_template(path) {
45
45
 
46
46
  function visit_function(node, context) {
47
47
  node.metadata = {
48
- hoisted: false,
49
- hoisted_params: [],
50
48
  scope: context.state.scope,
51
49
  tracked: false,
52
50
  };
@@ -722,12 +720,7 @@ const visitors = {
722
720
  const handler = visit(attr.value, state);
723
721
  const delegated_event = get_delegated_event(event_name, handler, state);
724
722
 
725
- if (delegated_event !== null) {
726
- if (delegated_event.hoisted) {
727
- delegated_event.function.metadata.hoisted = true;
728
- delegated_event.hoisted = true;
729
- }
730
-
723
+ if (delegated_event) {
731
724
  if (attr.metadata === undefined) {
732
725
  attr.metadata = {};
733
726
  }
@@ -15,9 +15,9 @@ 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
- build_hoisted_params,
21
21
  is_inside_component,
22
22
  build_assignment,
23
23
  visit_assignment_expression,
@@ -75,16 +75,6 @@ function visit_function(node, context) {
75
75
  }
76
76
  }
77
77
 
78
- if (metadata?.hoisted === true) {
79
- const params = build_hoisted_params(node, context);
80
-
81
- return /** @type {FunctionExpression} */ ({
82
- ...node,
83
- params,
84
- body: context.visit(node.body, state),
85
- });
86
- }
87
-
88
78
  let body = context.visit(node.body, {
89
79
  ...state,
90
80
  // we are new context so tracking no longer applies
@@ -132,13 +122,7 @@ function visit_head_element(node, context) {
132
122
  '_$_.head',
133
123
  b.arrow(
134
124
  [b.id('__anchor')],
135
- b.block([
136
- ...init,
137
- ...update.map((u) => {
138
- debugger;
139
- }),
140
- ...final,
141
- ]),
125
+ b.block([...init, ...update.map((u) => u.operation), ...final]),
142
126
  ),
143
127
  ),
144
128
  );
@@ -985,21 +969,7 @@ const visitors = {
985
969
  state.events.add(event_name);
986
970
  }
987
971
 
988
- // Hoist function if we can, otherwise we leave the function as is
989
- if (attr.metadata.delegated.hoisted) {
990
- if (attr.metadata.delegated.function === attr.value) {
991
- const func_name = state.scope.root.unique('on_' + event_name);
992
- state.hoisted.push(b.var(func_name, handler));
993
- handler = func_name;
994
- }
995
-
996
- const hoisted_params = /** @type {Expression[]} */ (
997
- attr.metadata.delegated.function.metadata.hoisted_params
998
- );
999
-
1000
- const args = [handler, b.id('__block'), ...hoisted_params];
1001
- delegated_assignment = b.array(args);
1002
- } else if (
972
+ if (
1003
973
  (handler.type === 'Identifier' &&
1004
974
  is_declared_function_within_component(handler, context)) ||
1005
975
  handler.type === 'ArrowFunctionExpression' ||
@@ -1267,31 +1237,36 @@ const visitors = {
1267
1237
  // We visit, but only to gather metadata
1268
1238
  b.call(visit(node.id, { ...state, metadata }));
1269
1239
 
1240
+ // We're calling a component from within svg/mathml context
1241
+ const is_with_ns = state.namespace !== DEFAULT_NAMESPACE;
1242
+
1270
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
+ );
1271
1252
  state.init.push(
1272
- b.stmt(
1273
- b.call(
1274
- '_$_.composite',
1275
- b.thunk(visit(node.id, state)),
1276
- id,
1277
- is_spreading
1278
- ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1279
- : b.object(props),
1280
- ),
1281
- ),
1253
+ is_with_ns
1254
+ ? b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(shared))
1255
+ : b.stmt(shared),
1282
1256
  );
1283
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
+ );
1284
1266
  state.init.push(
1285
- b.stmt(
1286
- b.call(
1287
- visit(node.id, state),
1288
- id,
1289
- is_spreading
1290
- ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1291
- : b.object(props),
1292
- b.id('_$_.active_block'),
1293
- ),
1294
- ),
1267
+ is_with_ns
1268
+ ? b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(shared))
1269
+ : b.stmt(shared),
1295
1270
  );
1296
1271
  }
1297
1272
  }
@@ -2125,7 +2100,6 @@ function transform_ts_child(node, context) {
2125
2100
  .map((child) => visit(child, state))
2126
2101
  .filter((child) => child.type !== 'JSXText' || child.value.trim() !== '');
2127
2102
 
2128
- debugger;
2129
2103
  state.init.push(b.stmt(b.jsx_fragment(children)));
2130
2104
  } else {
2131
2105
  throw new Error('TODO');
@@ -2146,6 +2120,7 @@ function transform_children(children, context) {
2146
2120
  node.type === 'TryStatement' ||
2147
2121
  node.type === 'ForOfStatement' ||
2148
2122
  node.type === 'SwitchStatement' ||
2123
+ node.type === 'TsxCompat' ||
2149
2124
  node.type === 'Html' ||
2150
2125
  (node.type === 'Element' &&
2151
2126
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
@@ -323,8 +323,5 @@ export interface TransformContext {
323
323
  * Delegated event result
324
324
  */
325
325
  export interface DelegatedEventResult {
326
- /** Whether event was hoisted */
327
- hoisted: boolean;
328
- /** The hoisted function */
329
326
  function?: FunctionExpression | FunctionDeclaration | ArrowFunctionExpression;
330
327
  }
@@ -169,206 +169,19 @@ export function is_dom_property(name) {
169
169
  return DOM_PROPERTIES.includes(name);
170
170
  }
171
171
 
172
- const unhoisted = { hoisted: false };
173
-
174
172
  /**
175
- * Determines if an event handler can be hoisted for delegation
173
+ * Determines if an event handler can be delegated
176
174
  * @param {string} event_name
177
175
  * @param {Expression} handler
178
176
  * @param {CompilerState} state
179
- * @returns {DelegatedEventResult | null}
177
+ * @returns {boolean}
180
178
  */
181
179
  export function get_delegated_event(event_name, handler, state) {
182
180
  // Handle delegated event handlers. Bail out if not a delegated event.
183
181
  if (!handler || !is_delegated(event_name)) {
184
- return null;
185
- }
186
-
187
- /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
188
- let target_function = null;
189
- let binding = null;
190
-
191
- if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
192
- target_function = handler;
193
- } else if (handler.type === 'Identifier') {
194
- binding = state.scope.get(handler.name);
195
-
196
- if (state.analysis.module.scope.references.has(handler.name)) {
197
- // If a binding with the same name is referenced in the module scope (even if not declared there), bail out
198
- return unhoisted;
199
- }
200
-
201
- if (binding != null) {
202
- for (const { path } of binding.references) {
203
- const parent = path.at(-1);
204
- if (parent === undefined) {
205
- return unhoisted;
206
- }
207
-
208
- const grandparent = path.at(-2);
209
-
210
- /** @type {Element | null} */
211
- let element = null;
212
- /** @type {string | null} */
213
- let event_name = null;
214
- if (
215
- parent.type === 'Expression' &&
216
- grandparent?.type === 'Attribute' &&
217
- is_event_attribute(grandparent)
218
- ) {
219
- element = /** @type {Element} */ (path.at(-3));
220
- const attribute = /** @type {Attribute} */ (grandparent);
221
- event_name = get_attribute_event_name(attribute.name.name);
222
- }
223
-
224
- if (element && event_name) {
225
- if (
226
- element.type !== 'Element' ||
227
- element.metadata.has_spread ||
228
- !is_delegated(event_name)
229
- ) {
230
- return unhoisted;
231
- }
232
- } else if (
233
- parent.type !== 'FunctionDeclaration' &&
234
- parent.type !== 'VariableDeclarator' &&
235
- parent.type !== 'Attribute'
236
- ) {
237
- return unhoisted;
238
- }
239
- }
240
- }
241
-
242
- // If the binding is exported, bail out
243
- if (
244
- state.analysis?.exports?.find(
245
- (/** @type {{name: string}} */ node) => node.name === handler.name,
246
- )
247
- ) {
248
- return unhoisted;
249
- }
250
-
251
- if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
252
- const binding_type = binding.initial.type;
253
-
254
- if (
255
- binding_type === 'ArrowFunctionExpression' ||
256
- binding_type === 'FunctionDeclaration' ||
257
- binding_type === 'FunctionExpression'
258
- ) {
259
- target_function = binding.initial;
260
- }
261
- }
262
- }
263
-
264
- // If we can't find a function, or the function has multiple parameters, bail out
265
- if (target_function == null || target_function.params.length > 1) {
266
- return unhoisted;
267
- }
268
-
269
- const visited_references = new Set();
270
- const scope = target_function.metadata.scope;
271
- for (const [reference, ref_nodes] of scope.references) {
272
- // Bail out if the arguments keyword is used or $host is referenced
273
- if (reference === 'arguments') return unhoisted;
274
-
275
- const binding = scope.get(reference);
276
- const local_binding = state.scope.get(reference);
277
-
278
- if (local_binding === null || binding == null) {
279
- return unhoisted;
280
- }
281
-
282
- // If we are referencing a binding that is shadowed in another scope then bail out.
283
- if (local_binding.node !== binding.node) {
284
- return unhoisted;
285
- }
286
- const is_tracked = ref_nodes.some(({ node }) => node.tracked);
287
-
288
- if (
289
- binding !== null &&
290
- // Bail out if the the binding is a rest param
291
- (binding.declaration_kind === 'rest_param' || // or any normal not reactive bindings that are mutated.
292
- // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
293
- (binding.kind === 'normal' && !is_tracked && binding.updated))
294
- ) {
295
- return unhoisted;
296
- }
297
- visited_references.add(reference);
298
- }
299
-
300
- return { hoisted: true, function: target_function };
301
- }
302
-
303
- /**
304
- * @param {Node} node
305
- * @param {TransformContext} context
306
- * @returns {Identifier[]}
307
- */
308
- function get_hoisted_params(node, context) {
309
- const scope = context.state.scope;
310
-
311
- /** @type {Identifier[]} */
312
- const params = [];
313
-
314
- /**
315
- * We only want to push if it's not already present to avoid name clashing
316
- * @param {Identifier} id
317
- */
318
- function push_unique(id) {
319
- if (!params.find((param) => param.name === id.name)) {
320
- params.push(id);
321
- }
322
- }
323
-
324
- for (const [reference] of scope.references) {
325
- let binding = scope.get(reference);
326
-
327
- if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
328
- if (binding.kind === 'prop') {
329
- push_unique(b.id('__props'));
330
- } else if (binding.kind === 'for_pattern') {
331
- push_unique(binding.metadata.pattern);
332
- } else if (binding.kind === 'prop_fallback') {
333
- push_unique(b.id(binding.node.name));
334
- } else if (
335
- // imports don't need to be hoisted
336
- binding.declaration_kind !== 'import'
337
- ) {
338
- // create a copy to remove start/end tags which would mess up source maps
339
- push_unique(b.id(binding.node.name));
340
- }
341
- }
342
- }
343
- return params;
344
- }
345
-
346
- /**
347
- * Builds the parameter list for a hoisted function
348
- * @param {FunctionDeclaration|FunctionExpression|ArrowFunctionExpression} node
349
- * @param {TransformContext} context
350
- * @returns {Pattern[]}
351
- */
352
- export function build_hoisted_params(node, context) {
353
- const hoisted_params = get_hoisted_params(node, context);
354
- node.metadata.hoisted_params = hoisted_params;
355
-
356
- /** @type {Pattern[]} */
357
- const params = [];
358
-
359
- if (node.params.length === 0) {
360
- if (hoisted_params.length > 0) {
361
- // For the event object
362
- params.push(b.id(context.state.scope.generate('_')));
363
- }
364
- } else {
365
- for (const param of node.params) {
366
- params.push(/** @type {Pattern} */ (context.visit(param)));
367
- }
182
+ return false;
368
183
  }
369
-
370
- params.push(...hoisted_params, b.id('__block'));
371
- return params;
184
+ return true;
372
185
  }
373
186
 
374
187
  /**
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 = [