diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c3969cd54..45e754b35d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,8 @@ jobs: - name: type check run: pnpm check - name: lint + if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail (avoids multiple runs uncovering different issues at different steps) run: pnpm lint - name: build and check generated types + 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 and commit the changes after you have reviewed them"; git diff; exit 1); } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/options.js b/packages/svelte/src/compiler/phases/1-parse/read/options.js index 31e81c7c71..69ca025f58 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/options.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/options.js @@ -1,3 +1,4 @@ +import { namespace_svg } from '../../../../constants.js'; import { error } from '../../../errors.js'; const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/; @@ -156,7 +157,7 @@ export default function read_options(node) { error(attribute, 'invalid-svelte-option-namespace'); } - if (value === 'http://www.w3.org/2000/svg') { + if (value === namespace_svg) { component_options.namespace = 'svg'; } else if (value === 'html' || value === 'svg' || value === 'foreign') { component_options.namespace = value; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 5f2eda1f17..da177f596a 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -139,7 +139,10 @@ export default function tag(parser) { name, attributes: [], fragment: create_fragment(true), - parent: null + parent: null, + metadata: { + svg: false + } }; parser.allow_whitespace(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2f7cd0da04..e3ab2b8539 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -20,7 +20,7 @@ import { warn } from '../../warnings.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import { regex_starts_with_newline } from '../patterns.js'; import { create_attribute, is_element_node } from '../nodes.js'; -import { DelegatedEvents } from '../../../constants.js'; +import { DelegatedEvents, namespace_svg } from '../../../constants.js'; import { should_proxy_or_freeze } from '../3-transform/client/utils.js'; /** @@ -1104,8 +1104,47 @@ const common_visitors = { context.state.analysis.elements.push(node); }, - SvelteElement(node, { state }) { - state.analysis.elements.push(node); + SvelteElement(node, context) { + context.state.analysis.elements.push(node); + + if ( + context.state.options.namespace !== 'foreign' && + node.tag.type === 'Literal' && + typeof node.tag.value === 'string' && + SVGElements.includes(node.tag.value) + ) { + node.metadata.svg = true; + return; + } + + for (const attribute of node.attributes) { + if (attribute.type === 'Attribute') { + if (attribute.name === 'xmlns' && is_text_attribute(attribute)) { + node.metadata.svg = attribute.value[0].data === namespace_svg; + return; + } + } + } + + for (let i = context.path.length - 1; i >= 0; i--) { + const ancestor = context.path[i]; + if ( + ancestor.type === 'Component' || + ancestor.type === 'SvelteComponent' || + ancestor.type === 'SvelteFragment' || + ancestor.type === 'SnippetBlock' + ) { + // Inside a slot or a snippet -> this resets the namespace, so we can't determine it + return; + } + if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') { + node.metadata.svg = + ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject' + ? false + : ancestor.metadata.svg; + return; + } + } } }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 19b9d3f244..61d426a798 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -9,7 +9,7 @@ import { import { binding_properties } from '../../../bindings.js'; import { clean_nodes, - determine_element_namespace, + determine_namespace_for_children, escape_html, infer_namespace } from '../../utils.js'; @@ -43,11 +43,7 @@ import { sanitize_template_string } from '../../../../utils/sanitize_template_st */ function get_attribute_name(element, attribute, context) { let name = attribute.name; - if ( - element.type === 'RegularElement' && - !element.metadata.svg && - context.state.metadata.namespace !== 'foreign' - ) { + if (!element.metadata.svg && context.state.metadata.namespace !== 'foreign') { name = name.toLowerCase(); if (name in AttributeAliases) { name = AttributeAliases[name]; @@ -1854,7 +1850,7 @@ export const template_visitors = { const metadata = context.state.metadata; const child_metadata = { ...context.state.metadata, - namespace: determine_element_namespace(node, context.state.metadata.namespace, context.path) + namespace: determine_namespace_for_children(node, context.state.metadata.namespace) }; context.state.template.push(`<${node.name}`); @@ -2079,9 +2075,6 @@ export const template_visitors = { /** @type {import('estree').ExpressionStatement[]} */ const lets = []; - /** @type {string | null} */ - let namespace = null; - // Create a temporary context which picks up the init/update statements. // They'll then be added to the function parameter of $.element const element_id = b.id(context.state.scope.generate('$$element')); @@ -2102,9 +2095,6 @@ export const template_visitors = { for (const attribute of node.attributes) { if (attribute.type === 'Attribute') { attributes.push(attribute); - if (attribute.name === 'xmlns' && is_text_attribute(attribute)) { - namespace = attribute.value[0].data; - } } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); } else if (attribute.type === 'ClassDirective') { @@ -2153,19 +2143,32 @@ export const template_visitors = { } } inner.push(...inner_context.state.after_update); - inner.push(...create_block(node, 'dynamic_element', node.fragment.nodes, context)); + inner.push( + ...create_block(node, 'dynamic_element', node.fragment.nodes, { + ...context, + state: { + ...context.state, + metadata: { + ...context.state.metadata, + namespace: determine_namespace_for_children(node, context.state.metadata.namespace) + } + } + }) + ); context.state.after_update.push( b.stmt( b.call( '$.element', context.state.node, get_tag, + node.metadata.svg === true + ? b.true + : node.metadata.svg === false + ? b.false + : b.literal(null), inner.length === 0 ? /** @type {any} */ (undefined) - : b.arrow([element_id, b.id('$$anchor')], b.block(inner)), - namespace === 'http://www.w3.org/2000/svg' - ? b.literal(true) - : /** @type {any} */ (undefined) + : b.arrow([element_id, b.id('$$anchor')], b.block(inner)) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 14b8cd2105..5c92d4e906 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -15,7 +15,7 @@ import { } from '../../constants.js'; import { clean_nodes, - determine_element_namespace, + determine_namespace_for_children, escape_html, infer_namespace, transform_inspect_rune @@ -482,11 +482,7 @@ function serialize_set_binding(node, context, fallback) { */ function get_attribute_name(element, attribute, context) { let name = attribute.name; - if ( - element.type === 'RegularElement' && - !element.metadata.svg && - context.state.metadata.namespace !== 'foreign' - ) { + if (!element.metadata.svg && context.state.metadata.namespace !== 'foreign') { name = name.toLowerCase(); // don't lookup boolean aliases here, the server runtime function does only // check for the lowercase variants of boolean attributes @@ -761,10 +757,10 @@ function serialize_element_spread_attributes( } const lowercase_attributes = - element.type !== 'RegularElement' || element.metadata.svg || is_custom_element_node(element) + element.metadata.svg || (element.type === 'RegularElement' && is_custom_element_node(element)) ? b.false : b.true; - const is_svg = element.type === 'RegularElement' && element.metadata.svg ? b.true : b.false; + const is_svg = element.metadata.svg ? b.true : b.false; /** @type {import('estree').Expression[]} */ const args = [ b.array(values), @@ -1165,7 +1161,7 @@ const template_visitors = { RegularElement(node, context) { const metadata = { ...context.state.metadata, - namespace: determine_element_namespace(node, context.state.metadata.namespace, context.path) + namespace: determine_namespace_for_children(node, context.state.metadata.namespace) }; context.state.template.push(t_string(`<${node.name}`)); @@ -1255,11 +1251,16 @@ const template_visitors = { context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag)))); } + const metadata = { + ...context.state.metadata, + namespace: determine_namespace_for_children(node, context.state.metadata.namespace) + }; /** @type {import('./types').ComponentContext} */ const inner_context = { ...context, state: { ...context.state, + metadata, template: [], init: [] } @@ -1276,7 +1277,10 @@ const template_visitors = { inner_context.state.template.push(t_string('>')); const before = serialize_template(inner_context.state.template); - const main = create_block(node, node.fragment.nodes, context); + const main = create_block(node, node.fragment.nodes, { + ...context, + state: { ...context.state, metadata } + }); const after = serialize_template([ t_expression(inner_id), t_string(' string} tag_fn + * @param {boolean | null} is_svg `null` == not statically known * @param {undefined | ((element: Element, anchor: Node) => void)} render_fn - * @param {any} is_svg * @returns {void} */ -export function element(anchor_node, tag_fn, render_fn, is_svg = false) { +export function element(anchor_node, tag_fn, is_svg, render_fn) { const block = create_dynamic_element_block(); hydrate_block_anchor(anchor_node); let has_mounted = false; @@ -1607,7 +1613,7 @@ export function element(anchor_node, tag_fn, render_fn, is_svg = false) { /** @type {string} */ let tag; - /** @type {null | HTMLElement | SVGElement} */ + /** @type {null | Element} */ let element = null; const element_effect = render_effect( () => { @@ -1623,11 +1629,20 @@ export function element(anchor_node, tag_fn, render_fn, is_svg = false) { // Managed effect const render_effect_signal = render_effect( () => { + // We try our best infering the namespace in case it's not possible to determine statically, + // but on the first render on the client (without hydration) the parent will be undefined, + // since the anchor is not attached to its parent / the dom yet. + const ns = + is_svg || tag === 'svg' + ? namespace_svg + : is_svg === false || anchor_node.parentElement?.tagName === 'foreignObject' + ? null + : anchor_node.parentElement?.namespaceURI ?? null; const next_element = tag ? current_hydration_fragment !== null - ? /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]) - : is_svg - ? document.createElementNS('http://www.w3.org/2000/svg', tag) + ? /** @type {Element} */ (current_hydration_fragment[0]) + : ns + ? document.createElementNS(ns, tag) : document.createElement(tag) : null; const prev_element = element; @@ -2098,7 +2113,7 @@ export function cssProps(anchor, is_html, props, component) { tag = document.createElement('div'); tag.style.display = 'contents'; } else { - tag = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + tag = document.createElementNS(namespace_svg, 'g'); } insert(tag, null, anchor); component_anchor = empty(); @@ -2629,7 +2644,7 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { /** @type {Element & ElementCSSInlineStyle} */ (node), prev, attrs, - node.namespaceURI !== 'http://www.w3.org/2000/svg', + node.namespaceURI !== namespace_svg, css_hash ); } diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 10ab1ef432..a3a2a92933 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -40,15 +40,12 @@ export { unwrap, freeze } from './client/runtime.js'; - export * from './client/each.js'; export * from './client/render.js'; export * from './client/validate.js'; export { raf } from './client/timing.js'; export { proxy, readonly, unstate } from './client/proxy.js'; - export { create_custom_element } from './client/custom-element.js'; - export { child, child_frag, diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js new file mode 100644 index 0000000000..12a0960d87 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; + +export default test({ + // This is skipped for now, because it's not clear how to make this work on client-side initial run: + // The anchor isn't connected to its parent at the time we can do a runtime check for the namespace, and we + // need the parent for this check. (this didn't work in Svelte 4 either) + skip: true, + html: '', + + test({ assert, target }) { + const svg = target.querySelector('svg'); + const rect = target.querySelector('path'); + assert.equal(svg?.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(rect?.namespaceURI, 'http://www.w3.org/2000/svg'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte new file mode 100644 index 0000000000..00069f711c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/_config.js new file mode 100644 index 0000000000..999b8fc9b5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [svg1, svg2] = target.querySelectorAll('svg'); + const [path1, path2] = target.querySelectorAll('path'); + const [fO1, fO2] = target.querySelectorAll('foreignObject'); + const [span1, span2] = target.querySelectorAll('span'); + + assert.equal(svg1.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(path1.namespaceURI, 'http://www.w3.org/2000/svg'); + + assert.equal(svg2.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(path2.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(fO1.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(span1.namespaceURI, 'http://www.w3.org/1999/xhtml'); + assert.equal(fO2.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(span2.namespaceURI, 'http://www.w3.org/1999/xhtml'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/main.svelte new file mode 100644 index 0000000000..8d62a862e8 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/main.svelte @@ -0,0 +1,22 @@ + + + + {#each iconNode as [tag, attrs]} + + {/each} + + + + + + ok + + + {#if true} + ok + {/if} + + + diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js index 6fcc18c0c4..b2734b8c46 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js @@ -11,7 +11,7 @@ export default function Svelte_element($$anchor, $$props) { var fragment = $.comment($$anchor); var node = $.child_frag(fragment); - $.element(node, tag); + $.element(node, tag, false); $.close_frag($$anchor, fragment); $.pop(); } \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 494ca3c5d2..72248b8124 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1342,6 +1342,13 @@ declare module 'svelte/compiler' { type: 'SvelteElement'; name: 'svelte:element'; tag: Expression; + metadata: { + /** + * `true`/`false` if this is definitely (not) an svg element. + * `null` means we can't know statically. + */ + svg: boolean | null; + }; } interface SvelteFragment extends BaseElement {