diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2bcb088480b..cf73a1f6cb02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: jobs: Tests: + permissions: {} runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: @@ -41,6 +42,7 @@ jobs: env: CI: true Lint: + permissions: {} runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -61,6 +63,7 @@ jobs: if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } Benchmarks: + permissions: {} runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index ce7bf04136ac..71df3242e8f1 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -9,6 +9,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - uses: actions/github-script@v6 with: script: | diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml index 1698a456d3df..b1fba0b04b30 100644 --- a/.github/workflows/pkg.pr.new-comment.yml +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -11,6 +11,7 @@ jobs: name: 'Update comment' runs-on: ubuntu-latest steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Download artifact uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 99f8153517f9..90d219faae6a 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -3,6 +3,8 @@ on: [push, pull_request] jobs: build: + permissions: {} + runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1daef0b89cc3..6debe5662a88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: name: Release runs-on: ubuntu-latest steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Checkout Repo uses: actions/checkout@v4 with: diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0ff6b62fe007..44f1f7cb7972 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.22.5 + +### Patch Changes + +- fix: memoize `clsx` calls ([#15456](https://github.com/sveltejs/svelte/pull/15456)) + +- fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes ([#15443](https://github.com/sveltejs/svelte/pull/15443)) + +- fix: always use `setAttribute` when setting `style` ([#15323](https://github.com/sveltejs/svelte/pull/15323)) + +- fix: make `style:` directive and CSS handling more robust ([#15418](https://github.com/sveltejs/svelte/pull/15418)) + ## 5.22.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1f95811cd5b8..fb20167a4e73 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.22.4", + "version": "5.22.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 322293bf6b91..1f636c32df6d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -769,17 +769,24 @@ export function analyze_component(root, source, options) { } let has_class = false; + let has_style = false; let has_spread = false; let has_class_directive = false; + let has_style_directive = false; for (const attribute of node.attributes) { // The spread method appends the hash to the end of the class attribute on its own if (attribute.type === 'SpreadAttribute') { has_spread = true; break; + } else if (attribute.type === 'Attribute') { + has_class ||= attribute.name.toLowerCase() === 'class'; + has_style ||= attribute.name.toLowerCase() === 'style'; + } else if (attribute.type === 'ClassDirective') { + has_class_directive = true; + } else if (attribute.type === 'StyleDirective') { + has_style_directive = true; } - has_class_directive ||= attribute.type === 'ClassDirective'; - has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class'; } // We need an empty class to generate the set_class() or class="" correctly @@ -796,6 +803,21 @@ export function analyze_component(root, source, options) { ]) ); } + + // We need an empty style to generate the set_style() correctly + if (!has_spread && !has_style && has_style_directive) { + node.attributes.push( + create_attribute('style', -1, -1, [ + { + type: 'Text', + data: '', + raw: '', + start: -1, + end: -1 + } + ]) + ); + } } // TODO diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 434b49caa1e6..9b3ecc922d89 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */ +/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { SourceLocation } from '#shared' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ @@ -20,9 +20,9 @@ import { build_getter } from '../utils.js'; import { get_attribute_name, build_attribute_value, - build_style_directives, build_set_attributes, - build_set_class + build_set_class, + build_set_style } from './shared/element.js'; import { process_children } from './shared/fragment.js'; import { @@ -215,21 +215,17 @@ export function RegularElement(node, context) { const node_id = context.state.node; - // Then do attributes - let is_attributes_reactive = has_spread; - if (has_spread) { const attributes_id = b.id(context.state.scope.generate('attributes')); build_set_attributes( attributes, class_directives, + style_directives, context, node, node_id, - attributes_id, - (node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true, - is_custom_element_node(node) && b.true + attributes_id ); // If value binding exists, that one takes care of calling $.init_select @@ -271,11 +267,13 @@ export function RegularElement(node, context) { } const name = get_attribute_name(node, attribute); + if ( !is_custom_element && !cannot_be_set_statically(attribute.name) && (attribute.value === true || is_text_attribute(attribute)) && - (name !== 'class' || class_directives.length === 0) + (name !== 'class' || class_directives.length === 0) && + (name !== 'style' || style_directives.length === 0) ) { let value = is_text_attribute(attribute) ? attribute.value[0].data : true; @@ -296,27 +294,30 @@ export function RegularElement(node, context) { }` ); } - continue; - } + } else if (name === 'autofocus') { + let { value } = build_attribute_value(attribute.value, context); + context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); + } else if (name === 'class') { + const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg'; + build_set_class(node, node_id, attribute, class_directives, context, is_html); + } else if (name === 'style') { + build_set_style(node_id, attribute, style_directives, context); + } else if (is_custom_element) { + build_custom_element_attribute_update_assignment(node_id, attribute, context); + } else { + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value) + ); + + const update = build_element_attribute_update(node, node_id, name, value, attributes); - const is = - is_custom_element && name !== 'class' - ? build_custom_element_attribute_update_assignment(node_id, attribute, context) - : build_element_attribute_update_assignment( - node, - node_id, - attribute, - attributes, - class_directives, - context - ); - if (is) is_attributes_reactive = true; + (has_state ? context.state.update : context.state.init).push(b.stmt(update)); + } } } - // style directives must be applied last since they could override class/style attributes - build_style_directives(style_directives, node_id, context, is_attributes_reactive); - if ( is_load_error_element(node.name) && (has_spread || has_use || lookup.has('onload') || lookup.has('onerror')) @@ -510,22 +511,51 @@ function setup_select_synchronization(value_binding, context) { /** * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context - * @return {ObjectExpression} + * @return {ObjectExpression | Identifier} */ export function build_class_directives_object(class_directives, context) { let properties = []; + let has_call_or_state = false; for (const d of class_directives) { - let expression = /** @type Expression */ (context.visit(d.expression)); + const expression = /** @type Expression */ (context.visit(d.expression)); + properties.push(b.init(d.name, expression)); + has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; + } - if (d.metadata.expression.has_call) { - expression = get_expression_id(context.state, expression); - } + const directives = b.object(properties); - properties.push(b.init(d.name, expression)); + return has_call_or_state ? get_expression_id(context.state, directives) : directives; +} + +/** + * @param {AST.StyleDirective[]} style_directives + * @param {ComponentContext} context + * @return {ObjectExpression | ArrayExpression}} + */ +export function build_style_directives_object(style_directives, context) { + let normal_properties = []; + let important_properties = []; + + for (const directive of style_directives) { + const expression = + directive.value === true + ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) + : build_attribute_value(directive.value, context, (value, metadata) => + metadata.has_call ? get_expression_id(context.state, value) : value + ).value; + const property = b.init(directive.name, expression); + + if (directive.modifiers.includes('important')) { + important_properties.push(property); + } else { + normal_properties.push(property); + } } - return b.object(properties); + return important_properties.length + ? b.array([b.object(normal_properties), b.object(important_properties)]) + : b.object(normal_properties); } /** @@ -552,73 +582,29 @@ export function build_class_directives_object(class_directives, context) { * Returns true if attribute is deemed reactive, false otherwise. * @param {AST.RegularElement} element * @param {Identifier} node_id - * @param {AST.Attribute} attribute + * @param {string} name + * @param {Expression} value * @param {Array} attributes - * @param {AST.ClassDirective[]} class_directives - * @param {ComponentContext} context - * @returns {boolean} */ -function build_element_attribute_update_assignment( - element, - node_id, - attribute, - attributes, - class_directives, - context -) { - const state = context.state; - const name = get_attribute_name(element, attribute); - const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; - const is_mathml = context.state.metadata.namespace === 'mathml'; - - const is_autofocus = name === 'autofocus'; - - let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call - ? // if it's autofocus we will not add this to a template effect so we don't want to get the expression id - // but separately memoize the expression - is_autofocus - ? memoize_expression(state, value) - : get_expression_id(state, value) - : value - ); +function build_element_attribute_update(element, node_id, name, value, attributes) { + if (name === 'muted') { + // Special case for Firefox who needs it set as a property in order to work + return b.assignment('=', b.member(node_id, b.id('muted')), value); + } - if (is_autofocus) { - state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); - return false; + if (name === 'value') { + return b.call('$.set_value', node_id, value); } - // Special case for Firefox who needs it set as a property in order to work - if (name === 'muted') { - if (!has_state) { - state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); - return false; - } - state.update.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); - return false; + if (name === 'checked') { + return b.call('$.set_checked', node_id, value); } - /** @type {Statement} */ - let update; + if (name === 'selected') { + return b.call('$.set_selected', node_id, value); + } - if (name === 'class') { - return build_set_class( - element, - node_id, - attribute, - value, - has_state, - class_directives, - context, - !is_svg && !is_mathml - ); - } else if (name === 'value') { - update = b.stmt(b.call('$.set_value', node_id, value)); - } else if (name === 'checked') { - update = b.stmt(b.call('$.set_checked', node_id, value)); - } else if (name === 'selected') { - update = b.stmt(b.call('$.set_selected', node_id, value)); - } else if ( + if ( // If we would just set the defaultValue property, it would override the value property, // because it is set in the template which implicitly means it's also setting the default value, // and if one updates the default value while the input is pristine it will also update the @@ -629,62 +615,49 @@ function build_element_attribute_update_assignment( ) || (element.name === 'textarea' && element.fragment.nodes.length > 0)) ) { - update = b.stmt(b.call('$.set_default_value', node_id, value)); - } else if ( + return b.call('$.set_default_value', node_id, value); + } + + if ( // See defaultValue comment name === 'defaultChecked' && attributes.some( (attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true ) ) { - update = b.stmt(b.call('$.set_default_checked', node_id, value)); - } else if (is_dom_property(name)) { - update = b.stmt(b.assignment('=', b.member(node_id, name), value)); - } else { - const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute'; - update = b.stmt( - b.call( - callee, - node_id, - b.literal(name), - value, - is_ignored(element, 'hydration_attribute_changed') && b.true - ) - ); + return b.call('$.set_default_checked', node_id, value); } - if (has_state) { - state.update.push(update); - return true; - } else { - state.init.push(update); - return false; + if (is_dom_property(name)) { + return b.assignment('=', b.member(node_id, name), value); } + + return b.call( + name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute', + node_id, + b.literal(name), + value, + is_ignored(element, 'hydration_attribute_changed') && b.true + ); } /** - * Like `build_element_attribute_update_assignment` but without any special attribute treatment. + * Like `build_element_attribute_update` but without any special attribute treatment. * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {ComponentContext} context - * @returns {boolean} */ function build_custom_element_attribute_update_assignment(node_id, attribute, context) { - const state = context.state; - const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive - let { value, has_state } = build_attribute_value(attribute.value, context); + const { value, has_state } = build_attribute_value(attribute.value, context); - const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value)); + // don't lowercase name, as we set the element's property, which might be case sensitive + const call = b.call('$.set_custom_element_data', node_id, b.literal(attribute.name), value); - if (has_state) { - // this is different from other updates — it doesn't get grouped, - // because set_custom_element_data may not be idempotent - state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression)))); - return true; - } else { - state.init.push(update); - return false; - } + // this is different from other updates — it doesn't get grouped, + // because set_custom_element_data may not be idempotent + const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call; + + context.state.init.push(b.stmt(update)); } /** @@ -695,7 +668,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {ComponentContext} context - * @returns {boolean} */ function build_element_special_value_attribute(element, node_id, attribute, context) { const state = context.state; @@ -708,7 +680,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont metadata.has_call ? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately is_select_with_value - ? memoize_expression(context.state, value) + ? memoize_expression(state, value) : get_expression_id(state, value) : value ); @@ -752,9 +724,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont value, update ); - return true; } else { state.init.push(update); - return false; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index ac284c818d3d..115eb6ccc11e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -5,12 +5,7 @@ import { dev, locator } from '../../../../state.js'; import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import { determine_namespace_for_children } from '../../utils.js'; -import { - build_attribute_value, - build_set_attributes, - build_set_class, - build_style_directives -} from './shared/element.js'; +import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js'; import { build_render_statement, get_expression_id } from './shared/utils.js'; /** @@ -77,52 +72,29 @@ export function SvelteElement(node, context) { // Let bindings first, they can be used on attributes context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot - // Then do attributes - let is_attributes_reactive = false; - if ( attributes.length === 1 && attributes[0].type === 'Attribute' && attributes[0].name.toLowerCase() === 'class' && is_text_attribute(attributes[0]) ) { - // special case when there only a class attribute, without call expression - let { value, has_state } = build_attribute_value( - attributes[0].value, - context, - (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value) - ); - - is_attributes_reactive = build_set_class( - node, - element_id, - attributes[0], - value, - has_state, - class_directives, - inner_context, - false - ); + build_set_class(node, element_id, attributes[0], class_directives, inner_context, false); } else if (attributes.length) { const attributes_id = b.id(context.state.scope.generate('attributes')); // Always use spread because we don't know whether the element is a custom element or not, // therefore we need to do the "how to set an attribute" logic at runtime. - is_attributes_reactive = build_set_attributes( + build_set_attributes( attributes, class_directives, + style_directives, inner_context, node, element_id, - attributes_id, - b.binary('===', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')), - b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')) + attributes_id ); } - // style directives must be applied last since they could override class/style attributes - build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive); - const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag))); if (dev) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 81a4b45288eb..084c1e7c675e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,4 +1,4 @@ -/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ +/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ import { escape_html } from '../../../../../../escaping.js'; @@ -6,29 +6,26 @@ import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { build_getter } from '../../utils.js'; -import { build_class_directives_object } from '../RegularElement.js'; +import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; import { build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes * @param {AST.ClassDirective[]} class_directives + * @param {AST.StyleDirective[]} style_directives * @param {ComponentContext} context * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} element_id * @param {Identifier} attributes_id - * @param {false | Expression} preserve_attribute_case - * @param {false | Expression} is_custom_element */ export function build_set_attributes( attributes, class_directives, + style_directives, context, element, element_id, - attributes_id, - preserve_attribute_case, - is_custom_element + attributes_id ) { let is_dynamic = false; @@ -83,6 +80,18 @@ export function build_set_attributes( class_directives.find((directive) => directive.metadata.expression.has_state) !== null; } + if (style_directives.length) { + values.push( + b.prop( + 'init', + b.array([b.id('$.STYLE')]), + build_style_directives_object(style_directives, context) + ) + ); + + is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state); + } + const call = b.call( '$.set_attributes', element_id, @@ -91,8 +100,6 @@ export function build_set_attributes( element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), - preserve_attribute_case, - is_custom_element, is_ignored(element, 'hydration_attribute_changed') && b.true ); @@ -100,54 +107,8 @@ export function build_set_attributes( context.state.init.push(b.let(attributes_id)); const update = b.stmt(b.assignment('=', attributes_id, call)); context.state.update.push(update); - return true; - } - - context.state.init.push(b.stmt(call)); - return false; -} - -/** - * Serializes each style directive into something like `$.set_style(element, style_property, value)` - * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic. - * @param {AST.StyleDirective[]} style_directives - * @param {Identifier} element_id - * @param {ComponentContext} context - * @param {boolean} is_attributes_reactive - */ -export function build_style_directives( - style_directives, - element_id, - context, - is_attributes_reactive -) { - const state = context.state; - - for (const directive of style_directives) { - const { has_state } = directive.metadata.expression; - - let value = - directive.value === true - ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(context.state, value) : value - ).value; - - const update = b.stmt( - b.call( - '$.set_style', - element_id, - b.literal(directive.name), - value, - /** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined) - ) - ); - - if (has_state || is_attributes_reactive) { - state.update.push(update); - } else { - state.init.push(update); - } + } else { + context.state.init.push(b.stmt(call)); } } @@ -195,27 +156,19 @@ export function get_attribute_name(element, attribute) { /** * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} node_id - * @param {AST.Attribute | null} attribute - * @param {Expression} value - * @param {boolean} has_state + * @param {AST.Attribute} attribute * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context * @param {boolean} is_html - * @returns {boolean} */ -export function build_set_class( - element, - node_id, - attribute, - value, - has_state, - class_directives, - context, - is_html -) { - if (attribute && attribute.metadata.needs_clsx) { - value = b.call('$.clsx', value); - } +export function build_set_class(element, node_id, attribute, class_directives, context, is_html) { + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => { + if (attribute.metadata.needs_clsx) { + value = b.call('$.clsx', value); + } + + return metadata.has_call ? get_expression_id(context.state, value) : value; + }); /** @type {Identifier | undefined} */ let previous_id; @@ -223,7 +176,7 @@ export function build_set_class( /** @type {ObjectExpression | Identifier | undefined} */ let prev; - /** @type {ObjectExpression | undefined} */ + /** @type {ObjectExpression | Identifier | undefined} */ let next; if (class_directives.length) { @@ -271,13 +224,48 @@ export function build_set_class( set_class = b.assignment('=', previous_id, set_class); } - const update = b.stmt(set_class); + (has_state ? context.state.update : context.state.init).push(b.stmt(set_class)); +} - if (has_state) { - context.state.update.push(update); - return true; +/** + * @param {Identifier} node_id + * @param {AST.Attribute} attribute + * @param {AST.StyleDirective[]} style_directives + * @param {ComponentContext} context + */ +export function build_set_style(node_id, attribute, style_directives, context) { + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => + metadata.has_call ? get_expression_id(context.state, value) : value + ); + + /** @type {Identifier | undefined} */ + let previous_id; + + /** @type {ObjectExpression | Identifier | undefined} */ + let prev; + + /** @type {ArrayExpression | ObjectExpression | undefined} */ + let next; + + if (style_directives.length) { + next = build_style_directives_object(style_directives, context); + has_state ||= style_directives.some((d) => d.metadata.expression.has_state); + + if (has_state) { + previous_id = b.id(context.state.scope.generate('styles')); + context.state.init.push(b.declaration('let', [b.declarator(previous_id)])); + prev = previous_id; + } else { + prev = b.object([]); + } + } + + /** @type {Expression} */ + let set_style = b.call('$.set_style', node_id, value, prev, next); + + if (previous_id) { + set_style = b.assignment('=', previous_id, set_style); } - context.state.init.push(update); - return false; + (has_state ? context.state.update : context.state.init).push(b.stmt(set_style)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 281d8f061768..4a5becfb2fc6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -1,4 +1,4 @@ -/** @import { Expression, Literal, ObjectExpression } from 'estree' */ +/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ /** @import { AST, Namespace } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { @@ -48,9 +48,6 @@ export function build_element_attributes(node, context) { let content = null; let has_spread = false; - // Use the index to keep the attributes order which is important for spreading - let class_index = -1; - let style_index = -1; let events_to_capture = new Set(); for (const attribute of node.attributes) { @@ -86,7 +83,6 @@ export function build_element_attributes(node, context) { // the defaultValue/defaultChecked properties don't exist as attributes } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { if (attribute.name === 'class') { - class_index = attributes.length; if (attribute.metadata.needs_clsx) { attributes.push({ ...attribute, @@ -102,10 +98,6 @@ export function build_element_attributes(node, context) { attributes.push(attribute); } } else { - if (attribute.name === 'style') { - style_index = attributes.length; - } - attributes.push(attribute); } } @@ -212,41 +204,30 @@ export function build_element_attributes(node, context) { } } - if ((node.metadata.scoped || class_directives.length) && !has_spread) { - const class_attribute = build_to_class( - node.metadata.scoped ? context.state.analysis.css.hash : null, - class_directives, - /** @type {AST.Attribute | null} */ (attributes[class_index] ?? null) - ); - if (class_index === -1) { - attributes.push(class_attribute); - } - } - - if (style_directives.length > 0 && !has_spread) { - build_style_directives( - style_directives, - /** @type {AST.Attribute | null} */ (attributes[style_index] ?? null), - context - ); - if (style_index > -1) { - attributes.splice(style_index, 1); - } - } - if (has_spread) { build_element_spread_attributes(node, attributes, style_directives, class_directives, context); } else { + const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; + for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { - if (attribute.value === true || is_text_attribute(attribute)) { - const name = get_attribute_name(node, attribute); - const literal_value = /** @type {Literal} */ ( + const name = get_attribute_name(node, attribute); + const can_use_literal = + (name !== 'class' || class_directives.length === 0) && + (name !== 'style' || style_directives.length === 0); + + if (can_use_literal && (attribute.value === true || is_text_attribute(attribute))) { + let literal_value = /** @type {Literal} */ ( build_attribute_value( attribute.value, context, WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ) ).value; + + if (name === 'class' && css_hash) { + literal_value = (String(literal_value) + ' ' + css_hash).trim(); + } + if (name !== 'class' || literal_value) { context.state.template.push( b.literal( @@ -258,10 +239,10 @@ export function build_element_attributes(node, context) { ) ); } + continue; } - const name = get_attribute_name(node, attribute); const value = build_attribute_value( attribute.value, context, @@ -269,8 +250,15 @@ export function build_element_attributes(node, context) { ); // pre-escape and inline literal attributes : - if (value.type === 'Literal' && typeof value.value === 'string') { + if (can_use_literal && value.type === 'Literal' && typeof value.value === 'string') { + if (name === 'class' && css_hash) { + value.value = (value.value + ' ' + css_hash).trim(); + } context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`)); + } else if (name === 'class') { + context.state.template.push(build_attr_class(class_directives, value, context, css_hash)); + } else if (name === 'style') { + context.state.template.push(build_attr_style(style_directives, value, context)); } else { context.state.template.push( b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) @@ -379,100 +367,79 @@ function build_element_spread_attributes( /** * - * @param {string | null} hash * @param {AST.ClassDirective[]} class_directives - * @param {AST.Attribute | null} class_attribute - * @returns + * @param {Expression} expression + * @param {ComponentContext} context + * @param {string | null} hash */ -function build_to_class(hash, class_directives, class_attribute) { - if (class_attribute === null) { - class_attribute = create_attribute('class', -1, -1, []); - } - +function build_attr_class(class_directives, expression, context, hash) { /** @type {ObjectExpression | undefined} */ - let classes; + let directives; if (class_directives.length) { - classes = b.object( + directives = b.object( class_directives.map((directive) => - b.prop('init', b.literal(directive.name), directive.expression) + b.prop( + 'init', + b.literal(directive.name), + /** @type {Expression} */ (context.visit(directive.expression, context.state)) + ) ) ); } - /** @type {Expression} */ - let class_name; - - if (class_attribute.value === true) { - class_name = b.literal(''); - } else if (Array.isArray(class_attribute.value)) { - if (class_attribute.value.length === 0) { - class_name = b.null; - } else { - class_name = class_attribute.value - .map((val) => (val.type === 'Text' ? b.literal(val.data) : val.expression)) - .reduce((left, right) => b.binary('+', left, right)); - } - } else { - class_name = class_attribute.value.expression; - } + let css_hash; - /** @type {Expression} */ - let expression; - - if ( - hash && - !classes && - class_name.type === 'Literal' && - (class_name.value === null || class_name.value === '' || typeof class_name.value === 'string') - ) { - if (class_name.value === null || class_name.value === '') { - expression = b.literal(hash); + if (hash) { + if (expression.type === 'Literal' && typeof expression.value === 'string') { + expression.value = (expression.value + ' ' + hash).trim(); } else { - expression = b.literal(escape_html(class_name.value, true) + ' ' + hash); + css_hash = b.literal(hash); } - } else { - expression = b.call('$.to_class', class_name, b.literal(hash), classes); } - class_attribute.value = { - type: 'ExpressionTag', - start: -1, - end: -1, - expression: expression, - metadata: { - expression: create_expression_metadata() - } - }; - - return class_attribute; + return b.call('$.attr_class', expression, css_hash, directives); } /** + * * @param {AST.StyleDirective[]} style_directives - * @param {AST.Attribute | null} style_attribute + * @param {Expression} expression * @param {ComponentContext} context */ -function build_style_directives(style_directives, style_attribute, context) { - const styles = style_directives.map((directive) => { - let value = - directive.value === true - ? b.id(directive.name) - : build_attribute_value(directive.value, context, true); - if (directive.modifiers.includes('important')) { - value = b.binary('+', value, b.literal(' !important')); +function build_attr_style(style_directives, expression, context) { + /** @type {ArrayExpression | ObjectExpression | undefined} */ + let directives; + + if (style_directives.length) { + let normal_properties = []; + let important_properties = []; + + for (const directive of style_directives) { + const expression = + directive.value === true + ? b.id(directive.name) + : build_attribute_value(directive.value, context, true); + + let name = directive.name; + if (name[0] !== '-' || name[1] !== '-') { + name = name.toLowerCase(); + } + + const property = b.init(directive.name, expression); + if (directive.modifiers.includes('important')) { + important_properties.push(property); + } else { + normal_properties.push(property); + } } - return b.init(directive.name, value); - }); - - const arg = - style_attribute === null - ? b.object(styles) - : b.call( - '$.merge_styles', - build_attribute_value(style_attribute.value, context, true), - b.object(styles) - ); - context.state.template.push(b.call('$.add_styles', arg)); + if (important_properties.length) { + directives = b.array([b.object(normal_properties), b.object(important_properties)]); + } else { + directives = b.object(normal_properties); + } + } + + return b.call('$.attr_style', expression, directives); } diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 03fddc5ebd28..8861e440fc30 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -33,6 +33,7 @@ export const UNINITIALIZED = Symbol(); export const FILENAME = Symbol('filename'); export const HMR = Symbol('hmr'); +export const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml'; export const NAMESPACE_SVG = 'http://www.w3.org/2000/svg'; export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML'; diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index dd408dcf8715..5a5d5d7c9b20 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -15,10 +15,15 @@ import { } from '../../runtime.js'; import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; +import { set_style } from './style.js'; +import { NAMESPACE_HTML } from '../../../../constants.js'; export const CLASS = Symbol('class'); export const STYLE = Symbol('style'); +const IS_CUSTOM_ELEMENT = Symbol('is custom element'); +const IS_HTML = Symbol('is html'); + /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need * to remove it upon hydration to avoid a bug when someone resets the form value. @@ -63,8 +68,7 @@ export function remove_input_defaults(input) { * @param {any} value */ export function set_value(element, value) { - // @ts-expect-error - var attributes = (element.__attributes ??= {}); + var attributes = get_attributes(element); if ( attributes.value === @@ -87,8 +91,7 @@ export function set_value(element, value) { * @param {boolean} checked */ export function set_checked(element, checked) { - // @ts-expect-error - var attributes = (element.__attributes ??= {}); + var attributes = get_attributes(element); if ( attributes.checked === @@ -151,8 +154,7 @@ export function set_default_value(element, value) { * @param {boolean} [skip_warning] */ export function set_attribute(element, attribute, value, skip_warning) { - // @ts-expect-error - var attributes = (element.__attributes ??= {}); + var attributes = get_attributes(element); if (hydrating) { attributes[attribute] = element.getAttribute(attribute); @@ -176,11 +178,6 @@ export function set_attribute(element, attribute, value, skip_warning) { if (attributes[attribute] === (attributes[attribute] = value)) return; - if (attribute === 'style' && '__styles' in element) { - // reset styles to force style: directive to update - element.__styles = {}; - } - if (attribute === 'loading') { // @ts-expect-error element[LOADING_ATTR_SYMBOL] = value; @@ -217,6 +214,7 @@ export function set_custom_element_data(node, prop, value) { // or effect var previous_reaction = active_reaction; var previous_effect = active_effect; + // If we're hydrating but the custom element is from Svelte, and it already scaffolded, // then it might run block logic in hydration mode, which we have to prevent. let was_hydrating = hydrating; @@ -226,17 +224,20 @@ export function set_custom_element_data(node, prop, value) { set_active_reaction(null); set_active_effect(null); + try { if ( + // `style` should use `set_attribute` rather than the setter + prop !== 'style' && // Don't compute setters for custom elements while they aren't registered yet, // because during their upgrade/instantiation they might add more setters. // Instead, fall back to a simple "an object, then set as property" heuristic. - setters_cache.has(node.nodeName) || + (setters_cache.has(node.nodeName) || // customElements may not be available in browser extension contexts !customElements || customElements.get(node.tagName.toLowerCase()) ? get_setters(node).includes(prop) - : value && typeof value === 'object' + : value && typeof value === 'object') ) { // @ts-expect-error node[prop] = value; @@ -261,20 +262,15 @@ export function set_custom_element_data(node, prop, value) { * @param {Record | undefined} prev * @param {Record} next New attributes - this function mutates this object * @param {string} [css_hash] - * @param {boolean} [preserve_attribute_case] - * @param {boolean} [is_custom_element] * @param {boolean} [skip_warning] * @returns {Record} */ -export function set_attributes( - element, - prev, - next, - css_hash, - preserve_attribute_case = false, - is_custom_element = false, - skip_warning = false -) { +export function set_attributes(element, prev, next, css_hash, skip_warning = false) { + var attributes = get_attributes(element); + + var is_custom_element = attributes[IS_CUSTOM_ELEMENT]; + var preserve_attribute_case = !attributes[IS_HTML]; + // If we're hydrating but the custom element is from Svelte, and it already scaffolded, // then it might run block logic in hydration mode, which we have to prevent. let is_hydrating_custom_element = hydrating && is_custom_element; @@ -297,10 +293,11 @@ export function set_attributes( next.class = null; /* force call to set_class() */ } - var setters = get_setters(element); + if (next[STYLE]) { + next.style ??= null; /* force call to set_style() */ + } - // @ts-expect-error - var attributes = /** @type {Record} **/ (element.__attributes ??= {}); + var setters = get_setters(element); // since key is captured we use const for (const key in next) { @@ -334,6 +331,13 @@ export function set_attributes( continue; } + if (key === 'style') { + set_style(element, value, prev?.[STYLE], next[STYLE]); + current[key] = value; + current[STYLE] = next[STYLE]; + continue; + } + var prev_value = current[key]; if (value === prev_value) continue; @@ -385,8 +389,9 @@ export function set_attributes( // @ts-ignore element[`__${event_name}`] = undefined; } - } else if (key === 'style' && value != null) { - element.style.cssText = value + ''; + } else if (key === 'style') { + // avoid using the setter + set_attribute(element, key, value); } else if (key === 'autofocus') { autofocus(/** @type {HTMLElement} */ (element), Boolean(value)); } else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) { @@ -432,13 +437,9 @@ export function set_attributes( // @ts-ignore element[name] = value; } else if (typeof value !== 'function') { - set_attribute(element, name, value); + set_attribute(element, name, value, skip_warning); } } - if (key === 'style' && '__styles' in element) { - // reset styles to force style: directive to update - element.__styles = {}; - } } if (is_hydrating_custom_element) { @@ -448,6 +449,20 @@ export function set_attributes( return current; } +/** + * + * @param {Element} element + */ +function get_attributes(element) { + return /** @type {Record} **/ ( + // @ts-expect-error + element.__attributes ??= { + [IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'), + [IS_HTML]: element.namespaceURI === NAMESPACE_HTML + } + ); +} + /** @type {Map} */ var setters_cache = new Map(); diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 7027c84f6260..ecbfcbc010fd 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -33,7 +33,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) // @ts-expect-error need to add __className to patched prototype dom.__className = value; - } else if (next_classes) { + } else if (next_classes && prev_classes !== next_classes) { for (var key in next_classes) { var is_present = !!next_classes[key]; diff --git a/packages/svelte/src/internal/client/dom/elements/style.js b/packages/svelte/src/internal/client/dom/elements/style.js index 34531029c9c6..3e05eec30efa 100644 --- a/packages/svelte/src/internal/client/dom/elements/style.js +++ b/packages/svelte/src/internal/client/dom/elements/style.js @@ -1,22 +1,57 @@ +import { to_style } from '../../../shared/attributes.js'; +import { hydrating } from '../hydration.js'; + /** - * @param {HTMLElement} dom - * @param {string} key - * @param {string} value - * @param {boolean} [important] + * @param {Element & ElementCSSInlineStyle} dom + * @param {Record} prev + * @param {Record} next + * @param {string} [priority] */ -export function set_style(dom, key, value, important) { - // @ts-expect-error - var styles = (dom.__styles ??= {}); +function update_styles(dom, prev = {}, next, priority) { + for (var key in next) { + var value = next[key]; - if (styles[key] === value) { - return; + if (prev[key] !== value) { + if (next[key] == null) { + dom.style.removeProperty(key); + } else { + dom.style.setProperty(key, value, priority); + } + } } +} - styles[key] = value; +/** + * @param {Element & ElementCSSInlineStyle} dom + * @param {string | null} value + * @param {Record | [Record, Record]} [prev_styles] + * @param {Record | [Record, Record]} [next_styles] + */ +export function set_style(dom, value, prev_styles, next_styles) { + // @ts-expect-error + var prev = dom.__style; + + if (hydrating || prev !== value) { + var next_style_attr = to_style(value, next_styles); - if (value == null) { - dom.style.removeProperty(key); - } else { - dom.style.setProperty(key, value, important ? 'important' : ''); + if (!hydrating || next_style_attr !== dom.getAttribute('style')) { + if (next_style_attr == null) { + dom.removeAttribute('style'); + } else { + dom.style.cssText = next_style_attr; + } + } + + // @ts-expect-error + dom.__style = value; + } else if (next_styles) { + if (Array.isArray(next_styles)) { + update_styles(dom, prev_styles?.[0], next_styles[0]); + update_styles(dom, prev_styles?.[1], next_styles[1], 'important'); + } else { + update_styles(dom, prev_styles, next_styles); + } } + + return next_styles; } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index f6ac92456e78..0ad9045b2062 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -48,7 +48,7 @@ export function init_operations() { // @ts-expect-error element_prototype.__attributes = null; // @ts-expect-error - element_prototype.__styles = null; + element_prototype.__style = undefined; // @ts-expect-error element_prototype.__e = undefined; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2591dbe4eaab..6098b496c5ac 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -2,7 +2,7 @@ /** @import { Component, Payload, RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; -import { attr, clsx, to_class } from '../shared/attributes.js'; +import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; import { is_promise, noop } from '../shared/utils.js'; import { subscribe_to_store } from '../../store/utils.js'; import { @@ -210,9 +210,7 @@ export function css_props(payload, is_html, props, component, dynamic = false) { */ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { if (styles) { - attrs.style = attrs.style - ? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles)) - : style_object_to_string(styles); + attrs.style = to_style(attrs.style, styles); } if (attrs.class) { @@ -286,35 +284,23 @@ function style_object_to_string(style_object) { .join(' '); } -/** @param {Record} style_object */ -export function add_styles(style_object) { - const styles = style_object_to_string(style_object); - return styles ? ` style="${styles}"` : ''; +/** + * @param {any} value + * @param {string | undefined} [hash] + * @param {Record} [directives] + */ +export function attr_class(value, hash, directives) { + var result = to_class(value, hash, directives); + return result ? ` class="${escape_html(result, true)}"` : ''; } /** - * @param {string} attribute - * @param {Record} styles + * @param {any} value + * @param {Record|[Record,Record]} [directives] */ -export function merge_styles(attribute, styles) { - /** @type {Record} */ - var merged = {}; - - if (attribute) { - for (var declaration of attribute.split(';')) { - var i = declaration.indexOf(':'); - var name = declaration.slice(0, i).trim(); - var value = declaration.slice(i + 1).trim(); - - if (name !== '') merged[name] = value; - } - } - - for (name in styles) { - merged[name] = styles[name]; - } - - return merged; +export function attr_style(value, directives) { + var result = to_style(value, directives); + return result ? ` style="${escape_html(result, true)}"` : ''; } /** @@ -549,7 +535,7 @@ export function props_id(payload) { return uid; } -export { attr, clsx, to_class }; +export { attr, clsx }; export { html } from './blocks/html.js'; diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index 89cc17e51b9d..c8758c1d4d4d 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -22,7 +22,7 @@ const replacements = { * @returns {string} */ export function attr(name, value, is_boolean = false) { - if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return ''; + if (value == null || (!value && is_boolean)) return ''; const normalized = (name in replacements && replacements[name].get(value)) || value; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; @@ -82,3 +82,138 @@ export function to_class(value, hash, directives) { return classname === '' ? null : classname; } + +/** + * + * @param {Record} styles + * @param {boolean} important + */ +function append_styles(styles, important = false) { + var separator = important ? ' !important;' : ';'; + var css = ''; + + for (var key in styles) { + var value = styles[key]; + if (value != null && value !== '') { + css += ' ' + key + ': ' + value + separator; + } + } + + return css; +} + +/** + * @param {string} name + * @returns {string} + */ +function to_css_name(name) { + if (name[0] !== '-' || name[1] !== '-') { + return name.toLowerCase(); + } + return name; +} + +/** + * @param {any} value + * @param {Record | [Record, Record]} [styles] + * @returns {string | null} + */ +export function to_style(value, styles) { + if (styles) { + var new_style = ''; + + /** @type {Record | undefined} */ + var normal_styles; + + /** @type {Record | undefined} */ + var important_styles; + + if (Array.isArray(styles)) { + normal_styles = styles[0]; + important_styles = styles[1]; + } else { + normal_styles = styles; + } + + if (value) { + value = String(value) + .replaceAll(/\s*\/\*.*?\*\/\s*/g, '') + .trim(); + + /** @type {boolean | '"' | "'"} */ + var in_str = false; + var in_apo = 0; + var in_comment = false; + + var reserved_names = []; + + if (normal_styles) { + reserved_names.push(...Object.keys(normal_styles).map(to_css_name)); + } + if (important_styles) { + reserved_names.push(...Object.keys(important_styles).map(to_css_name)); + } + + var start_index = 0; + var name_index = -1; + + const len = value.length; + for (var i = 0; i < len; i++) { + var c = value[i]; + + if (in_comment) { + if (c === '/' && value[i - 1] === '*') { + in_comment = false; + } + } else if (in_str) { + if (in_str === c) { + in_str = false; + } + } else if (c === '/' && value[i + 1] === '*') { + in_comment = true; + } else if (c === '"' || c === "'") { + in_str = c; + } else if (c === '(') { + in_apo++; + } else if (c === ')') { + in_apo--; + } + + if (!in_comment && in_str === false && in_apo === 0) { + if (c === ':' && name_index === -1) { + name_index = i; + } else if (c === ';' || i === len - 1) { + if (name_index !== -1) { + var name = to_css_name(value.substring(start_index, name_index).trim()); + + if (!reserved_names.includes(name)) { + if (c !== ';') { + i++; + } + + var property = value.substring(start_index, i).trim(); + new_style += ' ' + property + ';'; + } + } + + start_index = i + 1; + name_index = -1; + } + } + } + } + + if (normal_styles) { + new_style += append_styles(normal_styles); + } + + if (important_styles) { + new_style += append_styles(important_styles, true); + } + + new_style = new_style.trim(); + return new_style === '' ? null : new_style; + } + + return value == null ? null : String(value); +} diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 845375314f63..e20a9683ddc6 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.4'; +export const VERSION = '5.22.5'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js index 9ff0007c3713..04c9868ac378 100644 --- a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js @@ -2,6 +2,6 @@ import { test } from '../../test'; export default test({ html: ` -

+

` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js index adcdc4706d88..e9965b2b1e26 100644 --- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js @@ -2,7 +2,7 @@ import { ok, test } from '../../test'; export default test({ html: ` -

color: red

+

color: red;

`, test({ assert, component, target, window }) { diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte index 35b768547e25..e07adaa1c9d8 100644 --- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte @@ -1,5 +1,5 @@

{styles}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js index f6829721795c..20092ddadf34 100644 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js @@ -2,7 +2,7 @@ import { test } from '../../test'; import { flushSync } from 'svelte'; export default test({ - html: `
Hello world
diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js new file mode 100644 index 000000000000..bd76e4e6b929 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js @@ -0,0 +1,95 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_style() should not mutate style on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate'], + + server_props: { + browser: false + }, + + props: { + browser: true + }, + + ssrHtml: ` +
+
+
+
+
+
+
+
+
+
+
+ `, + + html: ` +
+
+
+
+
+
+
+
+
+
+
+ `, + + async test({ target, assert, component, instance }) { + flushSync(); + tick(); + assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']); + + let divs = target.querySelectorAll('div'); + + // Note : we cannot compare HTML because set_style() use dom.style.cssText + // which can alter the format of the attribute... + + divs.forEach((d) => assert.equal(d.style.margin, '')); + divs.forEach((d) => assert.equal(d.style.color, 'red')); + divs.forEach((d) => assert.equal(d.style.fontSize, '18px')); + + component.margin = '1px'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'margin' + ); + divs.forEach((d) => assert.equal(d.style.margin, '1px')); + + component.color = 'yellow'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'color' + ); + divs.forEach((d) => assert.equal(d.style.color, 'yellow')); + + component.fontSize = '10px'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'fontSize' + ); + divs.forEach((d) => assert.equal(d.style.fontSize, '10px')); + + component.fontSize = null; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'fontSize' + ); + divs.forEach((d) => assert.equal(d.style.fontSize, '')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte new file mode 100644 index 000000000000..ae4da8ae37c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte @@ -0,0 +1,54 @@ + + +
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/_config.js b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js new file mode 100644 index 000000000000..52690a431a4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js @@ -0,0 +1,65 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +const style_1 = 'invalid-key:0; margin:4px;;color: green ;color:blue '; +const style_2 = ' other-key : 0 ; padding:2px; COLOR:green; color: blue'; +const style_2_normalized = 'padding: 2px; color: blue;'; + +// https://github.com/sveltejs/svelte/issues/15309 +export default test({ + props: { + style: style_1 + }, + + ssrHtml: ` +
+
+ + + + `, + + async test({ assert, target, component }) { + component.style = style_2; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + ` + ); + + component.style = ''; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + ` + ); + + component.style = null; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte new file mode 100644 index 000000000000..d29590d67035 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte @@ -0,0 +1,9 @@ + + +
+
+ + +