ripple 0.2.91 → 0.2.92

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,6 @@
1
- import { build_assignment_value } from '../utils/ast.js';
1
+ /** @import { Identifier, Pattern, Super, FunctionExpression, FunctionDeclaration, ArrowFunctionExpression, MemberExpression, AssignmentExpression, Expression, Node, AssignmentOperator } from 'estree' */
2
+ /** @import { Component, Element, Attribute, SpreadAttribute, ScopeInterface, Binding, RippleNode, CompilerState, TransformContext, DelegatedEventResult, TextNode } from '#compiler' */
3
+ import { build_assignment_value, extract_paths } from '../utils/ast.js';
2
4
  import * as b from '../utils/builders.js';
3
5
  import { get_attribute_event_name, is_delegated, is_event_attribute } from '../utils/events.js';
4
6
 
@@ -27,6 +29,11 @@ const VOID_ELEMENT_NAMES = [
27
29
  * Returns `true` if `name` is of a void element
28
30
  * @param {string} name
29
31
  */
32
+ /**
33
+ * Returns true if name is a void element
34
+ * @param {string} name
35
+ * @returns {boolean}
36
+ */
30
37
  export function is_void_element(name) {
31
38
  return VOID_ELEMENT_NAMES.includes(name) || name.toLowerCase() === '!doctype';
32
39
  }
@@ -82,6 +89,11 @@ const RESERVED_WORDS = [
82
89
  'yield',
83
90
  ];
84
91
 
92
+ /**
93
+ * Returns true if word is a reserved JS keyword
94
+ * @param {string} word
95
+ * @returns {boolean}
96
+ */
85
97
  export function is_reserved(word) {
86
98
  return RESERVED_WORDS.includes(word);
87
99
  }
@@ -121,6 +133,11 @@ const DOM_BOOLEAN_ATTRIBUTES = [
121
133
  'disableremoteplayback',
122
134
  ];
123
135
 
136
+ /**
137
+ * Returns true if name is a boolean DOM attribute
138
+ * @param {string} name
139
+ * @returns {boolean}
140
+ */
124
141
  export function is_boolean_attribute(name) {
125
142
  return DOM_BOOLEAN_ATTRIBUTES.includes(name);
126
143
  }
@@ -143,12 +160,24 @@ const DOM_PROPERTIES = [
143
160
  'disableRemotePlayback',
144
161
  ];
145
162
 
163
+ /**
164
+ * Returns true if name is a DOM property
165
+ * @param {string} name
166
+ * @returns {boolean}
167
+ */
146
168
  export function is_dom_property(name) {
147
169
  return DOM_PROPERTIES.includes(name);
148
170
  }
149
171
 
150
172
  const unhoisted = { hoisted: false };
151
173
 
174
+ /**
175
+ * Determines if an event handler can be hoisted for delegation
176
+ * @param {string} event_name
177
+ * @param {Expression} handler
178
+ * @param {CompilerState} state
179
+ * @returns {DelegatedEventResult | null}
180
+ */
152
181
  export function get_delegated_event(event_name, handler, state) {
153
182
  // Handle delegated event handlers. Bail out if not a delegated event.
154
183
  if (!handler || !is_delegated(event_name)) {
@@ -176,18 +205,18 @@ export function get_delegated_event(event_name, handler, state) {
176
205
 
177
206
  const grandparent = path.at(-2);
178
207
 
179
- /** @type {AST.RegularElement | null} */
208
+ /** @type {Element | null} */
180
209
  let element = null;
181
210
  /** @type {string | null} */
182
211
  let event_name = null;
183
212
  if (
184
- parent.type === 'ExpressionTag' &&
213
+ parent.type === 'Expression' &&
185
214
  grandparent?.type === 'Attribute' &&
186
215
  is_event_attribute(grandparent)
187
216
  ) {
188
- element = /** @type {AST.RegularElement} */ (path.at(-3));
189
- const attribute = /** @type {AST.Attribute} */ (grandparent);
190
- event_name = get_attribute_event_name(attribute.name);
217
+ element = /** @type {Element} */ (path.at(-3));
218
+ const attribute = /** @type {Attribute} */ (grandparent);
219
+ event_name = get_attribute_event_name(attribute.name.name);
191
220
  }
192
221
 
193
222
  if (element && event_name) {
@@ -205,7 +234,11 @@ export function get_delegated_event(event_name, handler, state) {
205
234
  }
206
235
 
207
236
  // If the binding is exported, bail out
208
- if (state.analysis.exports.find((node) => node.name === handler.name)) {
237
+ if (
238
+ state.analysis?.exports?.find(
239
+ (/** @type {{name: string}} */ node) => node.name === handler.name,
240
+ )
241
+ ) {
209
242
  return unhoisted;
210
243
  }
211
244
 
@@ -256,6 +289,11 @@ export function get_delegated_event(event_name, handler, state) {
256
289
  return { hoisted: true, function: target_function };
257
290
  }
258
291
 
292
+ /**
293
+ * @param {Node} node
294
+ * @param {TransformContext} context
295
+ * @returns {Identifier[]}
296
+ */
259
297
  function get_hoisted_params(node, context) {
260
298
  const scope = context.state.scope;
261
299
 
@@ -292,6 +330,12 @@ function get_hoisted_params(node, context) {
292
330
  return params;
293
331
  }
294
332
 
333
+ /**
334
+ * Builds the parameter list for a hoisted function
335
+ * @param {FunctionDeclaration|FunctionExpression|ArrowFunctionExpression} node
336
+ * @param {TransformContext} context
337
+ * @returns {Pattern[]}
338
+ */
295
339
  export function build_hoisted_params(node, context) {
296
340
  const hoisted_params = get_hoisted_params(node, context);
297
341
  node.metadata.hoisted_params = hoisted_params;
@@ -314,6 +358,11 @@ export function build_hoisted_params(node, context) {
314
358
  return params;
315
359
  }
316
360
 
361
+ /**
362
+ * Returns true if context is inside a top-level await
363
+ * @param {TransformContext} context
364
+ * @returns {boolean}
365
+ */
317
366
  export function is_top_level_await(context) {
318
367
  if (!is_inside_component(context)) {
319
368
  return false;
@@ -323,7 +372,7 @@ export function is_top_level_await(context) {
323
372
  const context_node = context.path[i];
324
373
  const type = context_node.type;
325
374
 
326
- if (type === 'Component') {
375
+ if (/** @type {Component} */ (context_node).type === 'Component') {
327
376
  return true;
328
377
  }
329
378
 
@@ -338,6 +387,12 @@ export function is_top_level_await(context) {
338
387
  return true;
339
388
  }
340
389
 
390
+ /**
391
+ * Returns true if context is inside a Component node
392
+ * @param {TransformContext} context
393
+ * @param {boolean} [includes_functions=false]
394
+ * @returns {boolean}
395
+ */
341
396
  export function is_inside_component(context, includes_functions = false) {
342
397
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
343
398
  const context_node = context.path[i];
@@ -351,13 +406,18 @@ export function is_inside_component(context, includes_functions = false) {
351
406
  ) {
352
407
  return false;
353
408
  }
354
- if (type === 'Component') {
409
+ if (/** @type {Component} */ (context_node).type === 'Component') {
355
410
  return true;
356
411
  }
357
412
  }
358
413
  return false;
359
414
  }
360
415
 
416
+ /**
417
+ * Returns true if context is inside a component-level function
418
+ * @param {TransformContext} context
419
+ * @returns {boolean}
420
+ */
361
421
  export function is_component_level_function(context) {
362
422
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
363
423
  const context_node = context.path[i];
@@ -378,18 +438,32 @@ export function is_component_level_function(context) {
378
438
  return true;
379
439
  }
380
440
 
441
+ /**
442
+ * Returns true if callee is a Ripple track call
443
+ * @param {Expression | Super} callee
444
+ * @param {TransformContext} context
445
+ * @returns {boolean}
446
+ */
381
447
  export function is_ripple_track_call(callee, context) {
382
- return (
383
- (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'trackSplit')) ||
384
- (callee.type === 'MemberExpression' &&
385
- callee.object.type === 'Identifier' &&
386
- callee.property.type === 'Identifier' &&
387
- (callee.property.name === 'track' || callee.property.name === 'trackSplit') &&
388
- !callee.computed &&
389
- is_ripple_import(callee, context))
390
- );
448
+ // Super expressions cannot be Ripple track calls
449
+ if (callee.type === 'Super') return false;
450
+
451
+ return (
452
+ (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'trackSplit')) ||
453
+ (callee.type === 'MemberExpression' &&
454
+ callee.object.type === 'Identifier' &&
455
+ callee.property.type === 'Identifier' &&
456
+ (callee.property.name === 'track' || callee.property.name === 'trackSplit') &&
457
+ !callee.computed &&
458
+ is_ripple_import(callee, context))
459
+ );
391
460
  }
392
461
 
462
+ /**
463
+ * Returns true if context is inside a call expression
464
+ * @param {TransformContext} context
465
+ * @returns {boolean}
466
+ */
393
467
  export function is_inside_call_expression(context) {
394
468
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
395
469
  const context_node = context.path[i];
@@ -413,6 +487,11 @@ export function is_inside_call_expression(context) {
413
487
  return false;
414
488
  }
415
489
 
490
+ /**
491
+ * Returns true if node is a static value (Literal, ArrayExpression, etc)
492
+ * @param {Node} node
493
+ * @returns {boolean}
494
+ */
416
495
  export function is_value_static(node) {
417
496
  if (node.type === 'Literal') {
418
497
  return true;
@@ -430,12 +509,20 @@ export function is_value_static(node) {
430
509
  return false;
431
510
  }
432
511
 
512
+ /**
513
+ * Returns true if callee is a Ripple import
514
+ * @param {Expression} callee
515
+ * @param {TransformContext} context
516
+ * @returns {boolean}
517
+ */
433
518
  export function is_ripple_import(callee, context) {
434
519
  if (callee.type === 'Identifier') {
435
520
  const binding = context.state.scope.get(callee.name);
436
521
 
437
522
  return (
438
523
  binding?.declaration_kind === 'import' &&
524
+ binding.initial !== null &&
525
+ binding.initial.type === 'ImportDeclaration' &&
439
526
  binding.initial.source.type === 'Literal' &&
440
527
  binding.initial.source.value === 'ripple'
441
528
  );
@@ -448,6 +535,8 @@ export function is_ripple_import(callee, context) {
448
535
 
449
536
  return (
450
537
  binding?.declaration_kind === 'import' &&
538
+ binding.initial !== null &&
539
+ binding.initial.type === 'ImportDeclaration' &&
451
540
  binding.initial.source.type === 'Literal' &&
452
541
  binding.initial.source.value === 'ripple'
453
542
  );
@@ -456,8 +545,14 @@ export function is_ripple_import(callee, context) {
456
545
  return false;
457
546
  }
458
547
 
548
+ /**
549
+ * Returns true if node is a function declared within a component
550
+ * @param {import('estree').Identifier} node
551
+ * @param {TransformContext} context
552
+ * @returns {boolean}
553
+ */
459
554
  export function is_declared_function_within_component(node, context) {
460
- const component = context.path.find((n) => n.type === 'Component');
555
+ const component = context.path?.find(/** @param {RippleNode} n */ (n) => n.type === 'Component');
461
556
 
462
557
  if (node.type === 'Identifier' && component) {
463
558
  const binding = context.state.scope.get(node.name);
@@ -485,11 +580,13 @@ export function is_declared_function_within_component(node, context) {
485
580
 
486
581
  return false;
487
582
  }
488
-
489
- function is_non_coercive_operator(operator) {
490
- return ['=', '||=', '&&=', '??='].includes(operator);
491
- }
492
-
583
+ /**
584
+ * Visits and transforms an assignment expression
585
+ * @param {AssignmentExpression} node
586
+ * @param {TransformContext} context
587
+ * @param {Function} build_assignment
588
+ * @returns {Expression | AssignmentExpression | null}
589
+ */
493
590
  export function visit_assignment_expression(node, context, build_assignment) {
494
591
  if (
495
592
  node.left.type === 'ArrayPattern' ||
@@ -523,7 +620,7 @@ export function visit_assignment_expression(node, context, build_assignment) {
523
620
  return null;
524
621
  }
525
622
 
526
- const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
623
+ const is_standalone = context.path.at(-1).type.endsWith('Statement');
527
624
  const sequence = b.sequence(assignments);
528
625
 
529
626
  if (!is_standalone) {
@@ -535,11 +632,7 @@ export function visit_assignment_expression(node, context, build_assignment) {
535
632
  // the right hand side is a complex expression, wrap in an IIFE to cache it
536
633
  const iife = b.arrow([rhs], sequence);
537
634
 
538
- const iife_is_async =
539
- is_expression_async(value) ||
540
- assignments.some((assignment) => is_expression_async(assignment));
541
-
542
- return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
635
+ return b.call(iife, value);
543
636
  }
544
637
 
545
638
  return sequence;
@@ -558,6 +651,14 @@ export function visit_assignment_expression(node, context, build_assignment) {
558
651
  return transformed;
559
652
  }
560
653
 
654
+ /**
655
+ * Builds an assignment node, possibly transforming for reactivity
656
+ * @param {AssignmentOperator} operator
657
+ * @param {Pattern | MemberExpression | Identifier} left
658
+ * @param {Expression} right
659
+ * @param {TransformContext} context
660
+ * @returns {Expression|null}
661
+ */
561
662
  export function build_assignment(operator, left, right, context) {
562
663
  let object = left;
563
664
 
@@ -611,6 +712,12 @@ export function build_assignment(operator, left, right, context) {
611
712
  const ATTR_REGEX = /[&"<]/g;
612
713
  const CONTENT_REGEX = /[&<]/g;
613
714
 
715
+ /**
716
+ * Escapes HTML special characters in a string
717
+ * @param {string} value
718
+ * @param {boolean} [is_attr=false]
719
+ * @returns {string}
720
+ */
614
721
  export function escape_html(value, is_attr = false) {
615
722
  const str = String(value ?? '');
616
723
 
@@ -630,6 +737,11 @@ export function escape_html(value, is_attr = false) {
630
737
  return escaped + str.substring(last);
631
738
  }
632
739
 
740
+ /**
741
+ * Hashes a string to a base36 value
742
+ * @param {string} str
743
+ * @returns {string}
744
+ */
633
745
  export function hash(str) {
634
746
  str = str.replace(regex_return_characters, '');
635
747
  let hash = 5381;
@@ -639,6 +751,11 @@ export function hash(str) {
639
751
  return (hash >>> 0).toString(36);
640
752
  }
641
753
 
754
+ /**
755
+ * Returns true if node is a DOM element (not a component)
756
+ * @param {Element} node
757
+ * @returns {boolean}
758
+ */
642
759
  export function is_element_dom_element(node) {
643
760
  return (
644
761
  node.id.type === 'Identifier' &&
@@ -648,7 +765,14 @@ export function is_element_dom_element(node) {
648
765
  );
649
766
  }
650
767
 
768
+ /**
769
+ * Normalizes children nodes (merges adjacent text, removes empty)
770
+ * @param {RippleNode[]} children
771
+ * @param {TransformContext} context
772
+ * @returns {RippleNode[]}
773
+ */
651
774
  export function normalize_children(children, context) {
775
+ /** @type {RippleNode[]} */
652
776
  const normalized = [];
653
777
 
654
778
  for (const node of children) {
@@ -678,6 +802,11 @@ export function normalize_children(children, context) {
678
802
  return normalized;
679
803
  }
680
804
 
805
+ /**
806
+ * @param {RippleNode} node
807
+ * @param {RippleNode[]} normalized
808
+ * @param {TransformContext} context
809
+ */
681
810
  function normalize_child(node, normalized, context) {
682
811
  if (node.type === 'EmptyStatement') {
683
812
  return;
@@ -694,9 +823,17 @@ function normalize_child(node, normalized, context) {
694
823
  }
695
824
  }
696
825
 
826
+ /**
827
+ * Builds a getter for a tracked identifier
828
+ * @param {Identifier} node
829
+ * @param {TransformContext} context
830
+ * @returns {Expression | Identifier}
831
+ */
697
832
  export function build_getter(node, context) {
698
833
  const state = context.state;
699
834
 
835
+ if (!context.path) return node;
836
+
700
837
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
701
838
  const binding = state.scope.get(node.name);
702
839
  const transform = binding?.transform;
@@ -714,6 +851,12 @@ export function build_getter(node, context) {
714
851
  return node;
715
852
  }
716
853
 
854
+ /**
855
+ * Determines the namespace for child elements
856
+ * @param {string} element_name
857
+ * @param {string} current_namespace
858
+ * @returns {string}
859
+ */
717
860
  export function determine_namespace_for_children(element_name, current_namespace) {
718
861
  if (element_name === 'foreignObject') {
719
862
  return 'html';
@@ -47,6 +47,7 @@ export {
47
47
  track,
48
48
  track_split as trackSplit,
49
49
  untrack,
50
+ tick,
50
51
  } from './internal/client/runtime.js';
51
52
 
52
53
  export { TrackedArray } from './array.js';
@@ -8,14 +8,11 @@ import { assign_nodes, create_fragment_from_html } from './template.js';
8
8
  /**
9
9
  * Renders dynamic HTML content into the DOM by inserting it before the anchor node.
10
10
  * Manages the lifecycle of HTML blocks, removing old content and inserting new content.
11
- *
12
- * TODO handle SVG/MathML
13
- *
14
11
  * @param {ChildNode} node
15
12
  * @param {() => string} get_html
16
13
  * @returns {void}
17
14
  */
18
- export function html(node, get_html) {
15
+ export function html(node, get_html, svg = false, mathml = false) {
19
16
  /** @type {ChildNode} */
20
17
  var anchor = node;
21
18
  /** @type {string} */
@@ -25,17 +22,30 @@ export function html(node, get_html) {
25
22
  var block = /** @type {Block} */ (active_block);
26
23
  html = get_html() + '';
27
24
 
25
+ if (svg) html = `<svg>${html}</svg>`;
26
+ else if (mathml) html = `<math>${html}</math>`;
27
+
28
28
  if (block.s !== null && block.s.start !== null) {
29
- remove_block_dom(block.s.start, /** @type {Node} */ (block.s.end));
29
+ remove_block_dom(block.s.start, /** @type {Node} */(block.s.end));
30
30
  block.s.start = block.s.end = null;
31
31
  }
32
32
 
33
33
  if (html === '') return;
34
- /** @type {DocumentFragment} */
34
+ /** @type {DocumentFragment | Element} */
35
35
  var node = create_fragment_from_html(html);
36
36
 
37
- assign_nodes(/** @type {Node } */ (first_child(node)), /** @type {Node} */ (node.lastChild));
37
+ if (svg || mathml) {
38
+ node = /** @type {Element} */(first_child(node));
39
+ }
40
+
41
+ assign_nodes(/** @type {Element} */(first_child(node)), /** @type {Element} */(node.lastChild));
38
42
 
39
- anchor.before(node);
43
+ if (svg || mathml) {
44
+ while (first_child(node)) {
45
+ anchor.before(/** @type {Element} */(first_child(node)));
46
+ }
47
+ } else {
48
+ anchor.before(node);
49
+ }
40
50
  });
41
51
  }
@@ -46,6 +46,7 @@ export {
46
46
  exclude_from_object,
47
47
  derived,
48
48
  maybe_tracked,
49
+ tick,
49
50
  } from './runtime.js';
50
51
 
51
52
  export { composite } from './composite.js';
@@ -1,43 +1,66 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
- import { branch, destroy_block, render } from './blocks.js';
3
+ import { branch, destroy_block, remove_block_dom, render } from './blocks.js';
4
4
  import { UNINITIALIZED } from './constants.js';
5
5
  import { handle_root_events } from './events.js';
6
6
  import { create_text } from './operations.js';
7
+ import { active_block } from './runtime.js';
7
8
 
8
9
  /**
9
- * @param {any} _
10
- * @param {{ target: Element, children: (anchor: Node) => void }} props
10
+ * @param {any} _
11
+ * @param {{ target: Element, children: (anchor: Node, props: {}, block: Block) => void }} props
11
12
  * @returns {void}
12
13
  */
13
14
  export function Portal(_, props) {
14
- /** @type {Element | symbol} */
15
- let target = UNINITIALIZED;
16
- /** @type {((anchor: Node) => void) | symbol} */
17
- let children = UNINITIALIZED;
18
- /** @type {Block | null} */
19
- var b = null;
20
- /** @type {Text | null} */
21
- var anchor = null;
22
-
23
- render(() => {
24
- if (target === (target = props.target)) return;
25
- if (children === (children = props.children)) return;
26
-
27
- if (b !== null) {
28
- destroy_block(b);
29
- }
30
-
31
- anchor = create_text();
32
- /** @type {Element} */ (target).append(anchor);
33
-
34
- const cleanup_events = handle_root_events(/** @type {Element} */ (target));
35
-
36
- b = branch(() => /** @type {(anchor: Node) => void} */ (children)(/** @type {Text} */ (anchor)));
37
-
38
- return () => {
39
- cleanup_events();
40
- /** @type {Text} */ (anchor).remove();
41
- };
42
- });
15
+ /** @type {Element | symbol} */
16
+ let target = UNINITIALIZED;
17
+ /** @type {((anchor: Node, props: {}, block: Block) => void) | symbol} */
18
+ let children = UNINITIALIZED;
19
+ /** @type {Block | null} */
20
+ var b = null;
21
+ /** @type {Text | null} */
22
+ var anchor = null;
23
+ /** @type {Node | null} */
24
+ var dom_start = null;
25
+ /** @type {Node | null} */
26
+ var dom_end = null;
27
+
28
+ render(() => {
29
+ if (target === (target = props.target)) return;
30
+ if (children === (children = props.children)) return;
31
+
32
+ if (b !== null) {
33
+ destroy_block(b);
34
+ }
35
+
36
+ if (anchor !== null) {
37
+ anchor.remove();
38
+ }
39
+
40
+ dom_start = dom_end = null;
41
+
42
+ anchor = create_text();
43
+ /** @type {Element} */ (target).append(anchor);
44
+
45
+ const cleanup_events = handle_root_events(/** @type {Element} */ (target));
46
+
47
+ var block = /** @type {Block} */ (active_block);
48
+
49
+ b = branch(() => {
50
+ if (typeof children === 'function') {
51
+ children(/** @type {Text} */ (anchor), {}, block);
52
+ }
53
+ });
54
+
55
+ dom_start = b?.s?.start;
56
+ dom_end = b?.s?.end;
57
+
58
+ return () => {
59
+ cleanup_events();
60
+ /** @type {Text} */ (anchor).remove();
61
+ if (dom_start && dom_end) {
62
+ remove_block_dom(dom_start, dom_end);
63
+ }
64
+ };
65
+ });
43
66
  }
@@ -16,6 +16,7 @@ import {
16
16
  } from '../../../utils/events.js';
17
17
  import { get } from './runtime.js';
18
18
  import { clsx } from 'clsx';
19
+ import { normalize_css_property_name } from '../../../utils/normalize_css_property_name.js';
19
20
 
20
21
  /**
21
22
  * @param {Text} text
@@ -85,13 +86,42 @@ export function set_attribute(element, attribute, value) {
85
86
 
86
87
  if (value == null) {
87
88
  element.removeAttribute(attribute);
88
- } else if (typeof value !== 'string' && get_setters(element).includes(attribute)) {
89
+ } else if (attribute === 'style' && typeof value !== 'string') {
90
+ apply_styles(/** @type {HTMLElement} */ (element), value);
91
+ } else if (typeof value !== 'string' && get_setters(element).includes(attribute)) {
89
92
  /** @type {any} */ (element)[attribute] = value;
90
93
  } else {
91
94
  element.setAttribute(attribute, value);
92
95
  }
93
96
  }
94
97
 
98
+ /**
99
+ * @param {HTMLElement} element
100
+ * @param {HTMLElement['style']} newStyles
101
+ */
102
+ export function apply_styles(element, newStyles) {
103
+ const style = element.style;
104
+ const new_properties = new Set();
105
+
106
+ for(const [property, value] of Object.entries(newStyles)) {
107
+ const normalized_property = normalize_css_property_name(property);
108
+ const normalized_value = String(value);
109
+
110
+ if (style.getPropertyValue(normalized_property) !== normalized_value) {
111
+ style.setProperty(normalized_property, normalized_value);
112
+ }
113
+
114
+ new_properties.add(normalized_property);
115
+ }
116
+
117
+ for (let i = style.length - 1; i >= 0; i--) {
118
+ const property = style[i];
119
+ if (!new_properties.has(property)) {
120
+ style.removeProperty(property);
121
+ }
122
+ }
123
+ }
124
+
95
125
  /**
96
126
  * @param {Element} element
97
127
  * @param {Record<string, any>} attributes