svelte 5.57.1 → 5.57.2
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/compiler/index.js +1 -1
- package/package.json +1 -1
- package/src/compiler/errors.js +10 -0
- package/src/compiler/migrate/index.js +15 -4
- package/src/compiler/phases/1-parse/state/tag.js +2 -0
- package/src/compiler/phases/2-analyze/visitors/LetDirective.js +22 -0
- package/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +2 -2
- package/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +2 -1
- package/src/compiler/phases/3-transform/client/visitors/shared/events.js +3 -2
- package/src/compiler/phases/scope.js +8 -0
- package/src/compiler/print/index.js +3 -1
- package/src/index-client.js +1 -1
- package/src/internal/client/dev/tracing.js +3 -1
- package/src/internal/client/dom/blocks/boundary.js +39 -8
- package/src/internal/client/dom/blocks/each.js +8 -6
- package/src/internal/client/dom/blocks/svelte-element.js +3 -1
- package/src/internal/client/dom/css.js +1 -1
- package/src/internal/client/dom/elements/attributes.js +168 -148
- package/src/internal/client/dom/elements/bindings/document.js +3 -2
- package/src/internal/client/dom/elements/transitions.js +3 -3
- package/src/internal/client/error-handling.js +5 -1
- package/src/internal/client/reactivity/async.js +5 -0
- package/src/internal/client/reactivity/batch.js +12 -4
- package/src/internal/client/reactivity/sources.js +3 -3
- package/src/internal/client/runtime.js +13 -16
- package/src/internal/server/index.js +1 -1
- package/src/reactivity/date.js +2 -2
- package/src/reactivity/url-search-params.js +1 -1
- package/src/version.js +1 -1
- package/types/index.d.ts.map +1 -1
package/package.json
CHANGED
package/src/compiler/errors.js
CHANGED
|
@@ -1213,6 +1213,16 @@ export function let_directive_invalid_placement(node) {
|
|
|
1213
1213
|
e(node, 'let_directive_invalid_placement', `\`let:\` directive at invalid position\nhttps://svelte.dev/e/let_directive_invalid_placement`);
|
|
1214
1214
|
}
|
|
1215
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
|
|
1218
|
+
* @param {null | number | NodeLike} node
|
|
1219
|
+
* @param {string} name
|
|
1220
|
+
* @returns {never}
|
|
1221
|
+
*/
|
|
1222
|
+
export function let_directive_snippet_conflict(node, name) {
|
|
1223
|
+
e(node, 'let_directive_snippet_conflict', `Cannot use \`let:\` directives on a component that has a \`children\` snippet. Use snippet parameters instead (e.g. \`{#snippet children({ ${name} })}\`)\nhttps://svelte.dev/e/let_directive_snippet_conflict`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1216
1226
|
/**
|
|
1217
1227
|
* Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
|
|
1218
1228
|
* @param {null | number | NodeLike} node
|
|
@@ -910,8 +910,10 @@ const instance_script = {
|
|
|
910
910
|
start = /** @type {number} */ (parent.start);
|
|
911
911
|
end = /** @type {number} */ (parent.end);
|
|
912
912
|
}
|
|
913
|
-
|
|
914
|
-
|
|
913
|
+
// remove the declaration's whole line, or what there is of it inside the script
|
|
914
|
+
const script = /** @type {{ start: number; end: number }} */ (state.analysis.instance.ast);
|
|
915
|
+
while (start > script.start && state.str.original[start] !== '\n') start--;
|
|
916
|
+
while (end < script.end && state.str.original[end] !== '\n') end++;
|
|
915
917
|
state.str.update(start, end, '');
|
|
916
918
|
}
|
|
917
919
|
},
|
|
@@ -987,8 +989,11 @@ const instance_script = {
|
|
|
987
989
|
return;
|
|
988
990
|
}
|
|
989
991
|
|
|
992
|
+
let declared = 0;
|
|
990
993
|
for (const binding of bindings) {
|
|
991
|
-
|
|
994
|
+
// a binding this statement declares needs its `let` whether or not it is reassigned
|
|
995
|
+
if (ids.includes(binding.node) || (binding.reassigned && expression_ids.length === 0)) {
|
|
996
|
+
declared++;
|
|
992
997
|
check_rune_binding('state');
|
|
993
998
|
const init =
|
|
994
999
|
binding.kind === 'state'
|
|
@@ -1004,7 +1009,13 @@ const instance_script = {
|
|
|
1004
1009
|
}
|
|
1005
1010
|
}
|
|
1006
1011
|
|
|
1007
|
-
|
|
1012
|
+
// the statement goes only once every target has a declaration standing in for it
|
|
1013
|
+
if (
|
|
1014
|
+
expression_ids.length === 0 &&
|
|
1015
|
+
bindings.length > 0 &&
|
|
1016
|
+
declared === bindings.length &&
|
|
1017
|
+
bindings.every((b) => b.kind !== 'store_sub')
|
|
1018
|
+
) {
|
|
1008
1019
|
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
|
|
1009
1020
|
return;
|
|
1010
1021
|
}
|
|
@@ -532,6 +532,7 @@ function next(parser) {
|
|
|
532
532
|
if (block.type === 'IfBlock') {
|
|
533
533
|
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
|
|
534
534
|
if (parser.eat('if')) e.block_invalid_elseif(start);
|
|
535
|
+
if (block.alternate) e.block_duplicate_clause(start, '{:else}');
|
|
535
536
|
|
|
536
537
|
parser.allow_whitespace();
|
|
537
538
|
|
|
@@ -580,6 +581,7 @@ function next(parser) {
|
|
|
580
581
|
|
|
581
582
|
if (block.type === 'EachBlock') {
|
|
582
583
|
if (!parser.eat('else')) e.expected_token(start, '{:else}');
|
|
584
|
+
if (block.fallback) e.block_duplicate_clause(start, '{:else}');
|
|
583
585
|
|
|
584
586
|
parser.allow_whitespace();
|
|
585
587
|
parser.eat('}', true);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @import { AST } from '#compiler' */
|
|
2
2
|
/** @import { Context } from '../types' */
|
|
3
3
|
import * as e from '../../../errors.js';
|
|
4
|
+
import { determine_slot } from '../../../utils/slot.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {AST.LetDirective} node
|
|
@@ -21,4 +22,25 @@ export function LetDirective(node, context) {
|
|
|
21
22
|
) {
|
|
22
23
|
e.let_directive_invalid_placement(node);
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
// an explicit `children` snippet replaces the default slot content, so the `let:`
|
|
27
|
+
// directive would never be provided. if the component has a `slot` attribute, the
|
|
28
|
+
// directive applies to the component itself instead, which is fine
|
|
29
|
+
if (
|
|
30
|
+
(parent.type === 'Component' ||
|
|
31
|
+
parent.type === 'SvelteComponent' ||
|
|
32
|
+
parent.type === 'SvelteSelf') &&
|
|
33
|
+
!determine_slot(parent) &&
|
|
34
|
+
parent.fragment.nodes.some(
|
|
35
|
+
(child) => child.type === 'SnippetBlock' && child.expression.name === 'children'
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
const { expression } = node;
|
|
39
|
+
const pattern =
|
|
40
|
+
expression === null || (expression.type === 'Identifier' && expression.name === node.name)
|
|
41
|
+
? node.name
|
|
42
|
+
: `${node.name}: ${context.state.analysis.source.slice(expression.start, expression.end)}`;
|
|
43
|
+
|
|
44
|
+
e.let_directive_snippet_conflict(node, pattern);
|
|
45
|
+
}
|
|
24
46
|
}
|
|
@@ -173,8 +173,8 @@ export function check_element(node, context) {
|
|
|
173
173
|
current_role === get_implicit_role(node.name, attribute_map) &&
|
|
174
174
|
// <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
|
|
175
175
|
!['ul', 'ol', 'li', 'menu'].includes(node.name) &&
|
|
176
|
-
// <a role="link" />
|
|
177
|
-
!(node.name === 'a' && !attribute_map.has('href'))
|
|
176
|
+
// <a role="link" /> and <area role="link" /> are ok because without href they don't have a role of link
|
|
177
|
+
!((node.name === 'a' || node.name === 'area') && !attribute_map.has('href'))
|
|
178
178
|
) {
|
|
179
179
|
w.a11y_no_redundant_roles(attribute, current_role);
|
|
180
180
|
}
|
|
@@ -18,7 +18,8 @@ export function AwaitExpression(node, context) {
|
|
|
18
18
|
// in dev, note which values are read inside a reactive expression,
|
|
19
19
|
// but don't track them
|
|
20
20
|
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
|
|
21
|
-
|
|
21
|
+
// don't use b.await here, this way we preserve location information
|
|
22
|
+
return b.call({ ...node, argument: b.call('$.track_reactivity_loss', argument) });
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
return argument === node.argument ? node : { ...node, argument };
|
|
@@ -119,8 +119,9 @@ export function build_event_handler(node, metadata, context) {
|
|
|
119
119
|
|
|
120
120
|
// local variable can be assigned directly
|
|
121
121
|
// except in dev mode where when need $.apply()
|
|
122
|
-
// in order to handle warnings
|
|
123
|
-
|
|
122
|
+
// in order to handle warnings, or when it is
|
|
123
|
+
// assigned after a top-level `await`
|
|
124
|
+
if (!dev && binding?.declaration_kind !== 'import' && !binding?.blocker) {
|
|
124
125
|
return handler;
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -1343,6 +1343,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
|
|
|
1343
1343
|
}
|
|
1344
1344
|
|
|
1345
1345
|
context.next({ scope: child_scope });
|
|
1346
|
+
|
|
1347
|
+
// the parameters and what the body declares end up in the same function
|
|
1348
|
+
const body_scope = /** @type {Scope} */ (scopes.get(node.body));
|
|
1349
|
+
for (const [name, binding] of body_scope.declarations) {
|
|
1350
|
+
if (child_scope.declarations.has(name)) {
|
|
1351
|
+
e.declaration_duplicate(binding.node, name);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1346
1354
|
},
|
|
1347
1355
|
|
|
1348
1356
|
Fragment: (node, context) => {
|
|
@@ -754,7 +754,9 @@ const svelte_visitors = (comments, state) => ({
|
|
|
754
754
|
if (node.pending) {
|
|
755
755
|
context.write('}');
|
|
756
756
|
block(context, node.pending, state.preserve_whitespace > 0);
|
|
757
|
-
|
|
757
|
+
if (node.then || node.catch) {
|
|
758
|
+
context.write('{:');
|
|
759
|
+
}
|
|
758
760
|
} else {
|
|
759
761
|
context.write(' ');
|
|
760
762
|
}
|
package/src/index-client.js
CHANGED
|
@@ -17,7 +17,7 @@ if (DEV) {
|
|
|
17
17
|
if (!(rune in globalThis)) {
|
|
18
18
|
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
|
|
19
19
|
/** @type {any} */
|
|
20
|
-
let value; // let's hope
|
|
20
|
+
let value; // let's hope no one modifies this global, but belts and braces
|
|
21
21
|
Object.defineProperty(globalThis, rune, {
|
|
22
22
|
configurable: true,
|
|
23
23
|
// eslint-disable-next-line getter-return
|
|
@@ -131,8 +131,10 @@ export function trace(label, fn) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* @
|
|
134
|
+
* @template {Value} T
|
|
135
|
+
* @param {T} source
|
|
135
136
|
* @param {string} label
|
|
137
|
+
* @returns {T}
|
|
136
138
|
*/
|
|
137
139
|
export function tag(source, label) {
|
|
138
140
|
source.label = label;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/** @import { Effect, Source, TemplateNode, } from '#client' */
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
BOUNDARY_EFFECT,
|
|
4
|
+
DESTROYED,
|
|
5
|
+
DESTROYING,
|
|
6
|
+
EFFECT_PRESERVED,
|
|
7
|
+
EFFECT_TRANSPARENT
|
|
8
|
+
} from '#client/constants';
|
|
9
|
+
import {
|
|
10
|
+
HYDRATION_ERROR,
|
|
11
|
+
HYDRATION_START_ELSE,
|
|
12
|
+
HYDRATION_START_FAILED
|
|
13
|
+
} from '../../../../constants.js';
|
|
4
14
|
import { component_context, set_component_context } from '../../context.js';
|
|
5
15
|
import { invoke_error_boundary } from '../../error-handling.js';
|
|
6
16
|
import {
|
|
@@ -22,6 +32,7 @@ import {
|
|
|
22
32
|
hydrate_node,
|
|
23
33
|
hydrating,
|
|
24
34
|
next,
|
|
35
|
+
read_hydration_instruction,
|
|
25
36
|
skip_nodes,
|
|
26
37
|
set_hydrate_node
|
|
27
38
|
} from '../hydration.js';
|
|
@@ -154,16 +165,18 @@ export class Boundary {
|
|
|
154
165
|
|
|
155
166
|
this.#effect = block(() => {
|
|
156
167
|
if (hydrating) {
|
|
157
|
-
const
|
|
168
|
+
const comment_data = read_hydration_instruction(
|
|
169
|
+
/** @type {TemplateNode} */ (this.#hydrate_open)
|
|
170
|
+
);
|
|
158
171
|
hydrate_next();
|
|
159
172
|
|
|
160
|
-
const server_rendered_pending =
|
|
161
|
-
const server_rendered_failed =
|
|
173
|
+
const server_rendered_pending = comment_data === HYDRATION_START_ELSE;
|
|
174
|
+
const server_rendered_failed = comment_data.startsWith(HYDRATION_START_FAILED);
|
|
162
175
|
|
|
163
176
|
if (server_rendered_failed) {
|
|
164
177
|
// Server rendered the failed snippet - hydrate it.
|
|
165
178
|
// The serialized error is embedded in the comment: <!--[?<json>-->
|
|
166
|
-
const serialized_error = JSON.parse(
|
|
179
|
+
const serialized_error = JSON.parse(comment_data.slice(HYDRATION_START_FAILED.length));
|
|
167
180
|
this.#hydrate_failed_content(serialized_error);
|
|
168
181
|
} else if (server_rendered_pending) {
|
|
169
182
|
this.#hydrate_pending_content();
|
|
@@ -220,6 +233,8 @@ export class Boundary {
|
|
|
220
233
|
var calling_on_error = false;
|
|
221
234
|
|
|
222
235
|
const reset = () => {
|
|
236
|
+
if (this.#is_destroyed()) return;
|
|
237
|
+
|
|
223
238
|
if (did_reset) {
|
|
224
239
|
w.svelte_boundary_reset_noop();
|
|
225
240
|
return;
|
|
@@ -243,6 +258,8 @@ export class Boundary {
|
|
|
243
258
|
};
|
|
244
259
|
|
|
245
260
|
const invoke_onerror = () => {
|
|
261
|
+
if (this.#is_destroyed()) return;
|
|
262
|
+
|
|
246
263
|
try {
|
|
247
264
|
calling_on_error = true;
|
|
248
265
|
this.#props.onerror?.(error, reset);
|
|
@@ -255,6 +272,10 @@ export class Boundary {
|
|
|
255
272
|
return { reset, invoke_onerror };
|
|
256
273
|
}
|
|
257
274
|
|
|
275
|
+
#is_destroyed() {
|
|
276
|
+
return (this.#effect.f & (DESTROYED | DESTROYING)) !== 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
258
279
|
#hydrate_pending_content() {
|
|
259
280
|
const pending = this.#props.pending;
|
|
260
281
|
if (!pending) return;
|
|
@@ -263,6 +284,8 @@ export class Boundary {
|
|
|
263
284
|
this.#pending_effect = branch(() => pending(this.#anchor));
|
|
264
285
|
|
|
265
286
|
queue_micro_task(() => {
|
|
287
|
+
if (this.#is_destroyed()) return;
|
|
288
|
+
|
|
266
289
|
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
|
|
267
290
|
var anchor = create_text();
|
|
268
291
|
var handled = false;
|
|
@@ -445,6 +468,10 @@ export class Boundary {
|
|
|
445
468
|
|
|
446
469
|
/** @param {unknown} error */
|
|
447
470
|
error(error) {
|
|
471
|
+
if (error === HYDRATION_ERROR) {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
|
|
448
475
|
// If we have nothing to capture the error, or if we hit an error while
|
|
449
476
|
// rendering the fallback, re-throw for another boundary to handle
|
|
450
477
|
if (!this.#props.onerror && !this.#props.failed) {
|
|
@@ -457,7 +484,7 @@ export class Boundary {
|
|
|
457
484
|
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
|
|
458
485
|
|
|
459
486
|
current_batch.oncommit(() => {
|
|
460
|
-
this.#handle_error(error);
|
|
487
|
+
if (!this.#is_destroyed()) this.#handle_error(error);
|
|
461
488
|
});
|
|
462
489
|
} else {
|
|
463
490
|
this.#handle_error(error);
|
|
@@ -493,11 +520,13 @@ export class Boundary {
|
|
|
493
520
|
|
|
494
521
|
/** @param {unknown} transformed_error */
|
|
495
522
|
const handle_error_result = (transformed_error) => {
|
|
523
|
+
if (this.#is_destroyed()) return;
|
|
524
|
+
|
|
496
525
|
const { reset, invoke_onerror } = this.#create_reset(transformed_error);
|
|
497
526
|
|
|
498
527
|
invoke_onerror();
|
|
499
528
|
|
|
500
|
-
if (failed) {
|
|
529
|
+
if (failed && !this.#is_destroyed()) {
|
|
501
530
|
this.#failed_effect = this.#run(() => {
|
|
502
531
|
try {
|
|
503
532
|
return branch(() => {
|
|
@@ -523,6 +552,8 @@ export class Boundary {
|
|
|
523
552
|
};
|
|
524
553
|
|
|
525
554
|
queue_micro_task(() => {
|
|
555
|
+
if (this.#is_destroyed()) return;
|
|
556
|
+
|
|
526
557
|
// Run the error through the API-level transformError transform (e.g. SvelteKit's handleError)
|
|
527
558
|
/** @type {unknown} */
|
|
528
559
|
var result;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource,
|
|
1
|
+
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, TemplateNode, TransitionManager } from '#client' */
|
|
2
2
|
/** @import { Batch } from '../../reactivity/batch.js'; */
|
|
3
3
|
import {
|
|
4
4
|
EACH_INDEX_REACTIVE,
|
|
@@ -216,9 +216,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
|
|
|
216
216
|
tag(each_array, '{#each ...}');
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
/** @type {V[]} */
|
|
220
|
-
var array;
|
|
221
|
-
|
|
222
219
|
/** @type {Map<Batch, Set<any>>} */
|
|
223
220
|
var pending = new Map();
|
|
224
221
|
|
|
@@ -234,6 +231,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
|
|
|
234
231
|
|
|
235
232
|
state.pending.delete(batch);
|
|
236
233
|
|
|
234
|
+
// The effect doesn't necessarily re-run in a batch right before that batch commits
|
|
235
|
+
// (its view of the collection may not have changed), so we read the collection
|
|
236
|
+
// as the committing batch sees it rather than using the most recent block run's result
|
|
237
|
+
var array = get(each_array);
|
|
238
|
+
|
|
237
239
|
state.fallback = fallback;
|
|
238
240
|
reconcile(state, array, anchor, flags, get_key);
|
|
239
241
|
|
|
@@ -264,7 +266,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
|
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
var effect = block(() => {
|
|
267
|
-
array = /** @type {V[]} */ (get(each_array));
|
|
269
|
+
var array = /** @type {V[]} */ (get(each_array));
|
|
268
270
|
var length = array.length;
|
|
269
271
|
|
|
270
272
|
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
|
|
@@ -664,7 +666,7 @@ function reconcile(state, array, anchor, flags, get_key) {
|
|
|
664
666
|
* @param {V} value
|
|
665
667
|
* @param {unknown} key
|
|
666
668
|
* @param {number} index
|
|
667
|
-
* @param {(anchor: Node, item:
|
|
669
|
+
* @param {(anchor: Node, item: MaybeSource<V>, index: MaybeSource<number>, collection: () => V[]) => void} render_fn
|
|
668
670
|
* @param {number} flags
|
|
669
671
|
* @param {() => V[]} get_collection
|
|
670
672
|
* @returns {EachItem}
|
|
@@ -71,6 +71,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
|
|
|
71
71
|
|
|
72
72
|
branches.ensure(next_tag, (anchor) => {
|
|
73
73
|
if (next_tag) {
|
|
74
|
+
var is_hydrating = hydrating;
|
|
74
75
|
element = hydrating ? /** @type {Element} */ (element) : create_element(next_tag, ns);
|
|
75
76
|
|
|
76
77
|
if (DEV && location) {
|
|
@@ -123,7 +124,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
|
|
|
123
124
|
// we do this after calling `render_fn` so that child effects don't override `nodes.end`
|
|
124
125
|
/** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element;
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
// we only move the node if we are not hydrating since a claimed element is already in place
|
|
128
|
+
if (!is_hydrating) anchor.before(element);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
if (hydrating) {
|
|
@@ -12,7 +12,7 @@ export function append_styles(anchor, css) {
|
|
|
12
12
|
// Use an effect to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
|
|
13
13
|
effect(() => {
|
|
14
14
|
// Bit of a hack: branches.js/each.js use offscreen fragments with temporary text nodes that will
|
|
15
|
-
// never be connected to the real dom.
|
|
15
|
+
// never be connected to the real dom. Therefore walk up to the branch that has created the component
|
|
16
16
|
// whose styles we want to append, and check its node instead. It will be connected by the time we get here.
|
|
17
17
|
anchor = active_effect?.parent?.nodes?.start ?? anchor;
|
|
18
18
|
var root = anchor.getRootNode();
|