ripple 0.2.146 → 0.2.148
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 +2 -2
- package/src/compiler/phases/1-parse/index.js +21 -3
- package/src/compiler/phases/2-analyze/index.js +1 -8
- package/src/compiler/phases/3-transform/client/index.js +2 -34
- package/src/compiler/types/index.d.ts +0 -3
- package/src/compiler/utils.js +4 -191
- package/tests/client/svg.test.ripple +72 -25
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.
|
|
6
|
+
"version": "0.2.148",
|
|
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.
|
|
84
|
+
"ripple": "0.2.148"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -976,6 +976,15 @@ function RipplePlugin(config) {
|
|
|
976
976
|
return this.finishNode(node, 'JSXExpressionContainer');
|
|
977
977
|
}
|
|
978
978
|
|
|
979
|
+
jsx_parseEmptyExpression() {
|
|
980
|
+
// Override to properly handle the range for JSXEmptyExpression
|
|
981
|
+
// The range should be from after { to before }
|
|
982
|
+
const node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
|
|
983
|
+
node.end = this.start;
|
|
984
|
+
node.loc.end = this.startLoc;
|
|
985
|
+
return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
|
|
986
|
+
}
|
|
987
|
+
|
|
979
988
|
jsx_parseTupleContainer() {
|
|
980
989
|
var t = this.startNode();
|
|
981
990
|
return (
|
|
@@ -1588,9 +1597,7 @@ function RipplePlugin(config) {
|
|
|
1588
1597
|
const node = this.jsx_parseExpressionContainer();
|
|
1589
1598
|
node.type = node.html ? 'Html' : 'Text';
|
|
1590
1599
|
delete node.html;
|
|
1591
|
-
|
|
1592
|
-
body.push(node);
|
|
1593
|
-
}
|
|
1600
|
+
body.push(node);
|
|
1594
1601
|
} else if (this.type.label === '}') {
|
|
1595
1602
|
return;
|
|
1596
1603
|
} else if (this.type.label === 'jsxTagStart') {
|
|
@@ -1915,6 +1922,17 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1915
1922
|
return;
|
|
1916
1923
|
}
|
|
1917
1924
|
}
|
|
1925
|
+
// Handle JSXEmptyExpression - these represent {/* comment */} in JSX
|
|
1926
|
+
if (node.type === 'JSXEmptyExpression') {
|
|
1927
|
+
// Collect all comments that fall within this JSXEmptyExpression
|
|
1928
|
+
while (comments[0] && comments[0].start >= node.start && comments[0].end <= node.end) {
|
|
1929
|
+
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
|
1930
|
+
(node.innerComments ||= []).push(comment);
|
|
1931
|
+
}
|
|
1932
|
+
if (node.innerComments && node.innerComments.length > 0) {
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1918
1936
|
// Handle empty Element nodes the same way as empty BlockStatements
|
|
1919
1937
|
if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
|
|
1920
1938
|
if (comments[0].start < node.end && comments[0].end < node.end) {
|
|
@@ -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
|
|
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
|
}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from '../../../../constants.js';
|
|
18
18
|
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
|
|
19
19
|
import {
|
|
20
|
-
build_hoisted_params,
|
|
21
20
|
is_inside_component,
|
|
22
21
|
build_assignment,
|
|
23
22
|
visit_assignment_expression,
|
|
@@ -75,16 +74,6 @@ function visit_function(node, context) {
|
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
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
77
|
let body = context.visit(node.body, {
|
|
89
78
|
...state,
|
|
90
79
|
// we are new context so tracking no longer applies
|
|
@@ -132,13 +121,7 @@ function visit_head_element(node, context) {
|
|
|
132
121
|
'_$_.head',
|
|
133
122
|
b.arrow(
|
|
134
123
|
[b.id('__anchor')],
|
|
135
|
-
b.block([
|
|
136
|
-
...init,
|
|
137
|
-
...update.map((u) => {
|
|
138
|
-
debugger;
|
|
139
|
-
}),
|
|
140
|
-
...final,
|
|
141
|
-
]),
|
|
124
|
+
b.block([...init, ...update.map((u) => u.operation), ...final]),
|
|
142
125
|
),
|
|
143
126
|
),
|
|
144
127
|
);
|
|
@@ -985,21 +968,7 @@ const visitors = {
|
|
|
985
968
|
state.events.add(event_name);
|
|
986
969
|
}
|
|
987
970
|
|
|
988
|
-
|
|
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 (
|
|
971
|
+
if (
|
|
1003
972
|
(handler.type === 'Identifier' &&
|
|
1004
973
|
is_declared_function_within_component(handler, context)) ||
|
|
1005
974
|
handler.type === 'ArrowFunctionExpression' ||
|
|
@@ -2125,7 +2094,6 @@ function transform_ts_child(node, context) {
|
|
|
2125
2094
|
.map((child) => visit(child, state))
|
|
2126
2095
|
.filter((child) => child.type !== 'JSXText' || child.value.trim() !== '');
|
|
2127
2096
|
|
|
2128
|
-
debugger;
|
|
2129
2097
|
state.init.push(b.stmt(b.jsx_fragment(children)));
|
|
2130
2098
|
} else {
|
|
2131
2099
|
throw new Error('TODO');
|
|
@@ -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
|
}
|
package/src/compiler/utils.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
/**
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
describe('SVG namespace handling', () => {
|
|
2
2
|
it('should render static SVG elements with correct namespace', () => {
|
|
3
3
|
component App() {
|
|
4
|
-
<svg
|
|
4
|
+
<svg
|
|
5
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
6
|
+
width="24"
|
|
7
|
+
height="24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
>
|
|
5
11
|
<path d="m14 12 4 4 4-4" />
|
|
6
12
|
<circle cx="12" cy="12" r="4" />
|
|
7
13
|
<rect x="4" y="4" width="16" height="16" />
|
|
@@ -32,9 +38,20 @@ describe('SVG namespace handling', () => {
|
|
|
32
38
|
|
|
33
39
|
it('should render dynamic SVG paths with for loop (original issue)', () => {
|
|
34
40
|
component App() {
|
|
35
|
-
const d = [
|
|
41
|
+
const d = [
|
|
42
|
+
'm14 12 4 4 4-4',
|
|
43
|
+
'M18 16V7',
|
|
44
|
+
'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
|
|
45
|
+
'M3.304 13h6.392',
|
|
46
|
+
];
|
|
36
47
|
|
|
37
|
-
<svg
|
|
48
|
+
<svg
|
|
49
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
50
|
+
width="24"
|
|
51
|
+
height="24"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
>
|
|
38
55
|
for (const pathData of d) {
|
|
39
56
|
<path d={pathData} />
|
|
40
57
|
}
|
|
@@ -52,7 +69,12 @@ describe('SVG namespace handling', () => {
|
|
|
52
69
|
|
|
53
70
|
// Critical test: dynamic paths should have correct SVG namespace
|
|
54
71
|
expect(paths.length).toBe(4);
|
|
55
|
-
const expectedPaths = [
|
|
72
|
+
const expectedPaths = [
|
|
73
|
+
'm14 12 4 4 4-4',
|
|
74
|
+
'M18 16V7',
|
|
75
|
+
'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
|
|
76
|
+
'M3.304 13h6.392',
|
|
77
|
+
];
|
|
56
78
|
paths.forEach((path, i) => {
|
|
57
79
|
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
58
80
|
expect(path.getAttribute('d')).toBe(expectedPaths[i]);
|
|
@@ -60,7 +82,7 @@ describe('SVG namespace handling', () => {
|
|
|
60
82
|
|
|
61
83
|
// Verify paths are actually SVG elements (should have getBBox method)
|
|
62
84
|
// Note: getBBox might not work in test environment, so just check namespace
|
|
63
|
-
paths.forEach(path => {
|
|
85
|
+
paths.forEach((path) => {
|
|
64
86
|
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
65
87
|
expect(path.tagName.toLowerCase()).toBe('path');
|
|
66
88
|
});
|
|
@@ -68,9 +90,15 @@ describe('SVG namespace handling', () => {
|
|
|
68
90
|
|
|
69
91
|
it('should handle mixed static and dynamic SVG elements', () => {
|
|
70
92
|
component App() {
|
|
71
|
-
const dynamicPaths = [
|
|
72
|
-
|
|
73
|
-
<svg
|
|
93
|
+
const dynamicPaths = ['M12 2L2 7v10c0 5.55 3.84 10 9 11 5.16-1 9-5.45 9-11V7l-10-5z'];
|
|
94
|
+
|
|
95
|
+
<svg
|
|
96
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
97
|
+
width="24"
|
|
98
|
+
height="24"
|
|
99
|
+
fill="none"
|
|
100
|
+
stroke="currentColor"
|
|
101
|
+
>
|
|
74
102
|
<circle cx="12" cy="12" r="10" />
|
|
75
103
|
for (const pathData of dynamicPaths) {
|
|
76
104
|
<path d={pathData} />
|
|
@@ -93,14 +121,16 @@ describe('SVG namespace handling', () => {
|
|
|
93
121
|
expect(rect.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
94
122
|
|
|
95
123
|
// Verify content
|
|
96
|
-
expect(path.getAttribute('d')).toBe(
|
|
124
|
+
expect(path.getAttribute('d')).toBe(
|
|
125
|
+
'M12 2L2 7v10c0 5.55 3.84 10 9 11 5.16-1 9-5.45 9-11V7l-10-5z',
|
|
126
|
+
);
|
|
97
127
|
});
|
|
98
128
|
|
|
99
129
|
it('should handle nested SVG groups with for loops', () => {
|
|
100
130
|
component App() {
|
|
101
131
|
const items = [
|
|
102
|
-
{ x:
|
|
103
|
-
{ x:
|
|
132
|
+
{ x: '10', y: '10', width: '20', height: '20' },
|
|
133
|
+
{ x: '40', y: '40', width: '20', height: '20' },
|
|
104
134
|
];
|
|
105
135
|
|
|
106
136
|
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
|
|
@@ -123,7 +153,7 @@ describe('SVG namespace handling', () => {
|
|
|
123
153
|
expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
124
154
|
expect(rects.length).toBe(2);
|
|
125
155
|
|
|
126
|
-
rects.forEach(rect => {
|
|
156
|
+
rects.forEach((rect) => {
|
|
127
157
|
expect(rect.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
128
158
|
});
|
|
129
159
|
|
|
@@ -200,22 +230,39 @@ describe('SVG namespace handling', () => {
|
|
|
200
230
|
|
|
201
231
|
it('should compare static vs dynamic SVG rendering (original problem case)', () => {
|
|
202
232
|
component App() {
|
|
203
|
-
const d = [
|
|
233
|
+
const d = [
|
|
234
|
+
'm14 12 4 4 4-4',
|
|
235
|
+
'M18 16V7',
|
|
236
|
+
'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
|
|
237
|
+
'M3.304 13h6.392',
|
|
238
|
+
];
|
|
204
239
|
|
|
205
240
|
<div class="container">
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
<svg
|
|
242
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
243
|
+
width="24"
|
|
244
|
+
height="24"
|
|
245
|
+
fill="none"
|
|
246
|
+
stroke="currentColor"
|
|
247
|
+
class="dynamic-svg"
|
|
248
|
+
>
|
|
208
249
|
for (const path of d) {
|
|
209
250
|
<path d={path} />
|
|
210
251
|
}
|
|
211
252
|
</svg>
|
|
212
253
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
254
|
+
<svg
|
|
255
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
256
|
+
width="24"
|
|
257
|
+
height="24"
|
|
258
|
+
fill="none"
|
|
259
|
+
stroke="currentColor"
|
|
260
|
+
class="static-svg"
|
|
261
|
+
>
|
|
262
|
+
<path d="m14 12 4 4 4-4" />
|
|
263
|
+
<path d="M18 16V7" />
|
|
264
|
+
<path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" />
|
|
265
|
+
<path d="M3.304 13h6.392" />
|
|
219
266
|
</svg>
|
|
220
267
|
</div>
|
|
221
268
|
}
|
|
@@ -236,10 +283,10 @@ describe('SVG namespace handling', () => {
|
|
|
236
283
|
expect(staticPaths.length).toBe(4);
|
|
237
284
|
|
|
238
285
|
// All paths should have SVG namespace
|
|
239
|
-
dynamicPaths.forEach(path => {
|
|
286
|
+
dynamicPaths.forEach((path) => {
|
|
240
287
|
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
241
288
|
});
|
|
242
|
-
staticPaths.forEach(path => {
|
|
289
|
+
staticPaths.forEach((path) => {
|
|
243
290
|
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
244
291
|
});
|
|
245
292
|
|
|
@@ -249,11 +296,11 @@ describe('SVG namespace handling', () => {
|
|
|
249
296
|
});
|
|
250
297
|
|
|
251
298
|
// Critical test: all paths should be proper SVG elements
|
|
252
|
-
dynamicPaths.forEach(path => {
|
|
299
|
+
dynamicPaths.forEach((path) => {
|
|
253
300
|
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
254
301
|
expect(path.tagName.toLowerCase()).toBe('path');
|
|
255
302
|
});
|
|
256
|
-
staticPaths.forEach(path => {
|
|
303
|
+
staticPaths.forEach((path) => {
|
|
257
304
|
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
258
305
|
expect(path.tagName.toLowerCase()).toBe('path');
|
|
259
306
|
});
|