From 063101100147c8bacceb495bef2d60a1aebcd339 Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 12:00:24 +0100 Subject: [PATCH 01/11] feat: enable `animate` directive for snippets --- documentation/docs/98-reference/.generated/compile-errors.md | 2 +- packages/svelte/messages/compile-errors/template.md | 2 +- packages/svelte/src/compiler/errors.js | 4 ++-- .../src/compiler/phases/2-analyze/visitors/shared/element.js | 4 ++-- packages/svelte/src/internal/client/dom/blocks/each.js | 3 ++- .../tests/validator/samples/animation-not-in-each/errors.json | 2 +- .../tests/validator/samples/animation-siblings/errors.json | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 298363f78d38..f940281009cf 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -9,7 +9,7 @@ An element can only have one 'animate' directive ### animation_invalid_placement ``` -An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block +An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet ``` ### animation_missing_key diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 02961b61fccc..872a2fca4145 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -4,7 +4,7 @@ ## animation_invalid_placement -> An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block +> An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet ## animation_missing_key diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 9ea13e811e5f..c1e56ac36817 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -625,12 +625,12 @@ export function animation_duplicate(node) { } /** - * An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block + * An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet * @param {null | number | NodeLike} node * @returns {never} */ export function animation_invalid_placement(node) { - e(node, "animation_invalid_placement", `An element that uses the \`animate:\` directive must be the only child of a keyed \`{#each ...}\` block\nhttps://svelte.dev/e/animation_invalid_placement`); + e(node, "animation_invalid_placement", `An element that uses the \`animate:\` directive must be the only child of a keyed \`{#each ...}\` block, or an only child of a snippet\nhttps://svelte.dev/e/animation_invalid_placement`); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js index 725a4aded89d..306bbad985e1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js @@ -91,9 +91,9 @@ export function validate_element(node, context) { validate_attribute_name(attribute); } else if (attribute.type === 'AnimateDirective') { const parent = context.path.at(-2); - if (parent?.type !== 'EachBlock') { + if (parent?.type !== 'EachBlock' && parent?.type !== 'SnippetBlock') { e.animation_invalid_placement(attribute); - } else if (!parent.key) { + } else if (parent.type === 'EachBlock' && !parent.key) { e.animation_missing_key(attribute); } else if ( parent.body.nodes.filter( diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 9e6405594059..d3846f54e889 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -276,7 +276,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f * @returns {void} */ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, get_collection) { - var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var length = array.length; @@ -284,6 +283,8 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge var first = state.first; var current = first; + var is_animated = items.get(get_key(array[0], 0))?.a !== null; + /** @type {undefined | Set} */ var seen; diff --git a/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json b/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json index 2b5f73585dc3..c30abe9189f2 100644 --- a/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json +++ b/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json @@ -1,7 +1,7 @@ [ { "code": "animation_invalid_placement", - "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block", + "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet", "start": { "line": 5, "column": 5 diff --git a/packages/svelte/tests/validator/samples/animation-siblings/errors.json b/packages/svelte/tests/validator/samples/animation-siblings/errors.json index b6412ebca464..b05ee902be58 100644 --- a/packages/svelte/tests/validator/samples/animation-siblings/errors.json +++ b/packages/svelte/tests/validator/samples/animation-siblings/errors.json @@ -1,7 +1,7 @@ [ { "code": "animation_invalid_placement", - "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block", + "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet", "start": { "line": 6, "column": 6 From 485d06d48f08fa4cdfe86020ad2bc37c4d377dd6 Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 12:40:18 +0100 Subject: [PATCH 02/11] use EACH_IS_ANIMATED flag --- .../3-transform/client/visitors/EachBlock.js | 22 ++++++++++--------- .../src/internal/client/dom/blocks/each.js | 5 ++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 9f70981205a1..523dc51ac66f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -80,16 +80,7 @@ export function EachBlock(node, context) { flags |= EACH_ITEM_IMMUTABLE; } - // Since `animate:` can only appear on elements that are the sole child of a keyed each block, - // we can determine at compile time whether the each block is animated or not (in which - // case it should measure animated elements before and after reconciliation). - if ( - node.key && - node.body.nodes.some((child) => { - if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; - return child.attributes.some((attr) => attr.type === 'AnimateDirective'); - }) - ) { + if (node.key && node.body.nodes.some(is_animate_directive)) { flags |= EACH_IS_ANIMATED; } @@ -348,3 +339,14 @@ function collect_transitive_dependencies(binding, seen = new Set()) { return [...seen]; } + +/** @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child */ +function is_animate_directive(child) { + if (child.type === 'RenderTag') { + for (const snippetBlock of child.metadata.snippets) { + return snippetBlock.body.nodes.some(is_animate_directive); + } + } + if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; + return child.attributes.some((attr) => attr.type === 'AnimateDirective'); +} \ No newline at end of file diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index d3846f54e889..f6a71d6a8515 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -276,15 +276,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f * @returns {void} */ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, get_collection) { + var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var length = array.length; var items = state.items; var first = state.first; var current = first; - - var is_animated = items.get(get_key(array[0], 0))?.a !== null; - + /** @type {undefined | Set} */ var seen; From 3263c625a0149da1f2338be354c1c3de05299d2a Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 12:43:58 +0100 Subject: [PATCH 03/11] changeset --- .changeset/sixty-grapes-dream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-grapes-dream.md diff --git a/.changeset/sixty-grapes-dream.md b/.changeset/sixty-grapes-dream.md new file mode 100644 index 000000000000..8d30d6b8e99e --- /dev/null +++ b/.changeset/sixty-grapes-dream.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: enable `animate:` directive for snippets From 3cf1f188a42f600326a3e374f8c21286da17cfb9 Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 12:58:21 +0100 Subject: [PATCH 04/11] Reinclude removed comment --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 3 +++ packages/svelte/src/internal/client/dom/blocks/each.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 523dc51ac66f..280a1700a637 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -80,6 +80,9 @@ export function EachBlock(node, context) { flags |= EACH_ITEM_IMMUTABLE; } + // Since `animate:` can only appear on elements that are the sole child of a keyed each block, + // we can determine at compile time whether the each block is animated or not (in which + // case it should measure animated elements before and after reconciliation). if (node.key && node.body.nodes.some(is_animate_directive)) { flags |= EACH_IS_ANIMATED; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index f6a71d6a8515..9e6405594059 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -283,7 +283,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge var items = state.items; var first = state.first; var current = first; - + /** @type {undefined | Set} */ var seen; From da63e25bce762b8e1b217efd30e3c42ed8f785f3 Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 13:21:41 +0100 Subject: [PATCH 05/11] snake case --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 280a1700a637..9a091ff0aaf1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -346,8 +346,8 @@ function collect_transitive_dependencies(binding, seen = new Set()) { /** @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child */ function is_animate_directive(child) { if (child.type === 'RenderTag') { - for (const snippetBlock of child.metadata.snippets) { - return snippetBlock.body.nodes.some(is_animate_directive); + for (const snippet_block of child.metadata.snippets) { + return snippet_block.body.nodes.some(is_animate_directive); } } if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; From 809f987a3ce44a6b30f10f1adc5d31b70287953a Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 13:37:39 +0100 Subject: [PATCH 06/11] pnpm format --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 9a091ff0aaf1..6aca3baaf9a4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -352,4 +352,4 @@ function is_animate_directive(child) { } if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; return child.attributes.some((attr) => attr.type === 'AnimateDirective'); -} \ No newline at end of file +} From f8e47af7ff1bd69f3501dc71104e2088c708665f Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 14:10:08 +0100 Subject: [PATCH 07/11] Errors for snippets that uses the `animate:` directive --- .../phases/2-analyze/visitors/RenderTag.js | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 045224276a2e..9ceda38587e3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -27,9 +27,13 @@ export function RenderTag(node, context) { */ let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding); + /** @type {AST.SnippetBlock | undefined} */ + let snippet + if (binding?.initial?.type === 'SnippetBlock') { // if this render tag unambiguously references a local snippet, our job is easy - node.metadata.snippets.add(binding.initial); + snippet = binding.initial + node.metadata.snippets.add(snippet); } context.state.analysis.snippet_renderers.set(node, resolved); @@ -49,8 +53,31 @@ export function RenderTag(node, context) { ) { e.render_tag_invalid_call_expression(node); } + + const parent = context.path.at(-2); + const is_animated = snippet?.body.nodes.some(is_animate_directive); + + if(is_animated) { + if (parent?.type !== 'EachBlock') { + e.animation_invalid_placement(node); + } + else if (!parent.key) { + e.animation_missing_key(parent); + } + } mark_subtree_dynamic(context.path); context.next({ ...context.state, render_tag: node }); } + +/** @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child */ +function is_animate_directive(child) { + if (child.type === 'RenderTag') { + for (const snippet_block of child.metadata.snippets) { + return snippet_block.body.nodes.some(is_animate_directive); + } + } + if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; + return child.attributes.some((attr) => attr.type === 'AnimateDirective'); +} From f716298b58b6d691051aaf8bd0d6c916fb5b98dc Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 14:21:40 +0100 Subject: [PATCH 08/11] Error if multiple children with an animated snippet --- .../src/compiler/phases/2-analyze/visitors/RenderTag.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 9ceda38587e3..934a8652bcc1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -63,6 +63,12 @@ export function RenderTag(node, context) { } else if (!parent.key) { e.animation_missing_key(parent); + } else if (parent.body.nodes.filter((n) => + n.type !== 'Comment' && + n.type !== 'ConstTag' && + (n.type !== 'Text' || n.data.trim() !== '')).length > 1 + ) { + e.animation_invalid_placement(node); } } From 0e2036551ec773368b61b2bfb1d2fc6e1b222ae0 Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 16:02:39 +0100 Subject: [PATCH 09/11] Fix recursion --- .../phases/2-analyze/visitors/RenderTag.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 934a8652bcc1..bc45f7c14396 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -55,7 +55,7 @@ export function RenderTag(node, context) { } const parent = context.path.at(-2); - const is_animated = snippet?.body.nodes.some(is_animate_directive); + const is_animated = snippet?.body.nodes.some(n => is_animate_directive(n)); if(is_animated) { if (parent?.type !== 'EachBlock') { @@ -77,11 +77,17 @@ export function RenderTag(node, context) { context.next({ ...context.state, render_tag: node }); } -/** @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child */ -function is_animate_directive(child) { +/** + * @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child + * @param {boolean} renderSnippet + * @returns {boolean} +*/ +function is_animate_directive(child, renderSnippet = false) { if (child.type === 'RenderTag') { + if(renderSnippet) return false // Prevent infinite recursion for (const snippet_block of child.metadata.snippets) { - return snippet_block.body.nodes.some(is_animate_directive); + if(snippet_block.body.nodes.includes(child)) break + return snippet_block.body.nodes.some(n => is_animate_directive(n, true)); } } if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; From e0ef1a5f18e336e14646a907b6d7f7112c37e86c Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 16:02:59 +0100 Subject: [PATCH 10/11] snake case --- .../src/compiler/phases/2-analyze/visitors/RenderTag.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index bc45f7c14396..f927c5eb281a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -79,12 +79,12 @@ export function RenderTag(node, context) { /** * @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child - * @param {boolean} renderSnippet + * @param {boolean} render_snippet * @returns {boolean} */ -function is_animate_directive(child, renderSnippet = false) { +function is_animate_directive(child, render_snippet = false) { if (child.type === 'RenderTag') { - if(renderSnippet) return false // Prevent infinite recursion + if(render_snippet) return false // Prevent infinite recursion for (const snippet_block of child.metadata.snippets) { if(snippet_block.body.nodes.includes(child)) break return snippet_block.body.nodes.some(n => is_animate_directive(n, true)); From 10c01bc801bff6be9b125acb66b128e56d21ba41 Mon Sep 17 00:00:00 2001 From: "Arthur (@Refzlund)" Date: Sat, 21 Dec 2024 16:08:07 +0100 Subject: [PATCH 11/11] lint --- .../phases/2-analyze/visitors/RenderTag.js | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index f927c5eb281a..0f99428ab17e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -28,11 +28,11 @@ export function RenderTag(node, context) { let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding); /** @type {AST.SnippetBlock | undefined} */ - let snippet + let snippet; if (binding?.initial?.type === 'SnippetBlock') { // if this render tag unambiguously references a local snippet, our job is easy - snippet = binding.initial + snippet = binding.initial; node.metadata.snippets.add(snippet); } @@ -53,20 +53,22 @@ export function RenderTag(node, context) { ) { e.render_tag_invalid_call_expression(node); } - + const parent = context.path.at(-2); - const is_animated = snippet?.body.nodes.some(n => is_animate_directive(n)); + const is_animated = snippet?.body.nodes.some((n) => is_animate_directive(n)); - if(is_animated) { + if (is_animated) { if (parent?.type !== 'EachBlock') { e.animation_invalid_placement(node); - } - else if (!parent.key) { + } else if (!parent.key) { e.animation_missing_key(parent); - } else if (parent.body.nodes.filter((n) => - n.type !== 'Comment' && - n.type !== 'ConstTag' && - (n.type !== 'Text' || n.data.trim() !== '')).length > 1 + } else if ( + parent.body.nodes.filter( + (n) => + n.type !== 'Comment' && + n.type !== 'ConstTag' && + (n.type !== 'Text' || n.data.trim() !== '') + ).length > 1 ) { e.animation_invalid_placement(node); } @@ -77,17 +79,17 @@ export function RenderTag(node, context) { context.next({ ...context.state, render_tag: node }); } -/** +/** * @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child * @param {boolean} render_snippet * @returns {boolean} -*/ + */ function is_animate_directive(child, render_snippet = false) { if (child.type === 'RenderTag') { - if(render_snippet) return false // Prevent infinite recursion + if (render_snippet) return false; // Prevent infinite recursion for (const snippet_block of child.metadata.snippets) { - if(snippet_block.body.nodes.includes(child)) break - return snippet_block.body.nodes.some(n => is_animate_directive(n, true)); + if (snippet_block.body.nodes.includes(child)) break; + return snippet_block.body.nodes.some((n) => is_animate_directive(n, true)); } } if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false;