we don't need to traverse these nodes
or
these
ones
these
trailing
nodes
can
be
completely
ignored
diff --git a/.changeset/forty-llamas-unite.md b/.changeset/forty-llamas-unite.md
new file mode 100644
index 000000000000..933cece1c6ed
--- /dev/null
+++ b/.changeset/forty-llamas-unite.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: XHTML compliance
diff --git a/.changeset/smart-boats-accept.md b/.changeset/smart-boats-accept.md
new file mode 100644
index 000000000000..4cdfeb30d037
--- /dev/null
+++ b/.changeset/smart-boats-accept.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: add `fragments: 'html' | 'tree'` option for wider CSP compliance
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index f2eda3a7d210..e2e006c14bec 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -154,10 +154,6 @@ export function client_component(analysis, options) {
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
metadata: {
- context: {
- template_needs_import_node: false,
- template_contains_script_tag: false
- },
namespace: options.namespace,
bound_contenteditable: false
},
@@ -174,8 +170,7 @@ export function client_component(analysis, options) {
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
- template: /** @type {any} */ (null),
- locations: /** @type {any} */ (null)
+ template: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js
new file mode 100644
index 000000000000..ce56c43d7c50
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js
@@ -0,0 +1,18 @@
+const svg_attributes =
+ 'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split(
+ ' '
+ );
+
+const svg_attribute_lookup = new Map();
+
+svg_attributes.forEach((name) => {
+ svg_attribute_lookup.set(name.toLowerCase(), name);
+});
+
+/**
+ * @param {string} name
+ */
+export default function fix_attribute_casing(name) {
+ name = name.toLowerCase();
+ return svg_attribute_lookup.get(name) || name;
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
new file mode 100644
index 000000000000..d0327e702ad5
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
@@ -0,0 +1,68 @@
+/** @import { Location } from 'locate-character' */
+/** @import { Namespace } from '#compiler' */
+/** @import { ComponentClientTransformState } from '../types.js' */
+/** @import { Node } from './types.js' */
+import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../../../constants.js';
+import { dev, locator } from '../../../../state.js';
+import * as b from '../../../../utils/builders.js';
+
+/**
+ * @param {Node[]} nodes
+ */
+function build_locations(nodes) {
+ const array = b.array([]);
+
+ for (const node of nodes) {
+ if (node.type !== 'element') continue;
+
+ const { line, column } = /** @type {Location} */ (locator(node.start));
+
+ const expression = b.array([b.literal(line), b.literal(column)]);
+ const children = build_locations(node.children);
+
+ if (children.elements.length > 0) {
+ expression.elements.push(children);
+ }
+
+ array.elements.push(expression);
+ }
+
+ return array;
+}
+
+/**
+ * @param {ComponentClientTransformState} state
+ * @param {Namespace} namespace
+ * @param {number} [flags]
+ */
+export function transform_template(state, namespace, flags = 0) {
+ const tree = state.options.fragments === 'tree';
+
+ const expression = tree ? state.template.as_tree() : state.template.as_html();
+
+ if (tree) {
+ if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
+ if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
+ }
+
+ let call = b.call(
+ tree ? `$.from_tree` : `$.from_${namespace}`,
+ expression,
+ flags ? b.literal(flags) : undefined
+ );
+
+ if (state.template.contains_script_tag) {
+ call = b.call(`$.with_script`, call);
+ }
+
+ if (dev) {
+ call = b.call(
+ '$.add_locations',
+ call,
+ b.member(b.id(state.analysis.name), '$.FILENAME', true),
+ build_locations(state.template.nodes)
+ );
+ }
+
+ return call;
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js
new file mode 100644
index 000000000000..8f7f8a1f4357
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js
@@ -0,0 +1,162 @@
+/** @import { AST } from '#compiler' */
+/** @import { Node, Element } from './types'; */
+import { escape_html } from '../../../../../escaping.js';
+import { is_void } from '../../../../../utils.js';
+import * as b from '#compiler/builders';
+import fix_attribute_casing from './fix-attribute-casing.js';
+import { regex_starts_with_newline } from '../../../patterns.js';
+
+export class Template {
+ /**
+ * `true` if HTML template contains a `
hello
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte b/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte new file mode 100644 index 000000000000..302a01f33502 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte @@ -0,0 +1 @@ +hello
diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index 3e5a12ed9dec..9bb45ebf78e6 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -5,7 +5,7 @@ function increment(_, counter) { counter.count += 1; } -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function Await_block_scope($$anchor) { let counter = $.proxy({ count: 0 }); diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 390e86a3510a..ba3f4b155a31 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -10,7 +10,7 @@ const snippet = ($$anchor) => { $.append($$anchor, text); }; -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function Bind_component_snippet($$anchor) { let value = $.state(''); diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 219db6ffd529..3a13fa7e15d9 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`child element
another child element
child element
+another child element
+we don't need to traverse these nodes
or
these
ones
these
trailing
nodes
can
be
completely
ignored
we don't need to traverse these nodes
or
these
ones
these
trailing
nodes
can
be
completely
ignored
we don't need to traverse these nodes
or
these
ones
${$.html(content)}these
trailing
nodes
can
be
completely
ignored
we don't need to traverse these nodes
or
these
ones
${$.html(content)}these
trailing
nodes
can
be
completely
ignored
`); +var root = $.from_html(`
`); export default function Text_nodes_deriveds($$anchor) { let count1 = 0; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index fcc1ec315cdd..201dbed9aa83 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -985,6 +985,15 @@ declare module 'svelte/compiler' { * @default false */ preserveWhitespace?: boolean; + /** + * Which strategy to use when cloning DOM fragments: + * + * - `html` populates a `` with `innerHTML` and clones it. This is faster, but cannot be used if your app's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) includes [`require-trusted-types-for 'script'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) + * - `tree` creates the fragment one element at a time and _then_ clones it. This is slower, but works everywhere + * + * @default 'html' + */ + fragments?: 'html' | 'tree'; /** * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage. * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage. @@ -2872,6 +2881,15 @@ declare module 'svelte/types/compiler/interfaces' { * @default false */ preserveWhitespace?: boolean; + /** + * Which strategy to use when cloning DOM fragments: + * + * - `html` populates a `` with `innerHTML` and clones it. This is faster, but cannot be used if your app's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) includes [`require-trusted-types-for 'script'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) + * - `tree` creates the fragment one element at a time and _then_ clones it. This is slower, but works everywhere + * + * @default 'html' + */ + fragments?: 'html' | 'tree'; /** * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage. * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage. diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 3a62286b2476..93d01a4b1079 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -85,9 +85,25 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { } write(output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map)); - write(output_map, compiled.js.map.toString()); + // generate with fragments: 'tree' + if (generate === 'client') { + const compiled = compile(source, { + dev: true, + filename: input, + generate, + runes: argv.values.runes, + fragments: 'tree' + }); + + const output_js = `${cwd}/output/${generate}/${file}.tree.js`; + const output_map = `${cwd}/output/${generate}/${file}.tree.js.map`; + + write(output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map)); + write(output_map, compiled.js.map.toString()); + } + if (compiled.css) { write(output_css, compiled.css.code); }