{@html string}
``` -> Svelte doesn't perform any sanitization of the expression inside `{@html ...}` before it gets inserted into the DOM. In other words, if you use this feature it's critical that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks. +> **Warning!** Svelte doesn't perform any sanitization of the expression inside `{@html ...}` before it gets inserted into the DOM. In other words, if you use this feature it's **critical** that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks. diff --git a/site/content/tutorial/18-special-tags/meta.json b/site/content/tutorial/18-special-tags/meta.json new file mode 100644 index 0000000000..06e1732030 --- /dev/null +++ b/site/content/tutorial/18-special-tags/meta.json @@ -0,0 +1,3 @@ +{ + "title": "Special tags" +} \ No newline at end of file diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index c0d703892b..ffac6e1f07 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings'; import compiler_errors from './compiler_errors'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import check_enable_sourcemap from './utils/check_enable_sourcemap'; +import is_dynamic from './render_dom/wrappers/shared/is_dynamic'; interface ComponentOptions { namespace?: string; @@ -1380,12 +1381,11 @@ export default class Component { module_dependencies.add(name); } } - const is_writable_or_mutated = - variable && (variable.writable || variable.mutated); + if ( should_add_as_dependency && (!owner || owner === component.instance_scope) && - (name[0] === '$' || is_writable_or_mutated) + (name[0] === '$' || variable) ) { dependencies.add(name); } @@ -1409,6 +1409,19 @@ export default class Component { const { expression } = node.body as ExpressionStatement; const declaration = expression && (expression as AssignmentExpression).left; + const is_dependency_static = Array.from(dependencies).every( + dependency => dependency !== '$$props' && dependency !== '$$restProps' && !is_dynamic(this.var_lookup.get(dependency)) + ); + + if (is_dependency_static) { + assignees.forEach(assignee => { + const variable = component.var_lookup.get(assignee); + if (variable) { + variable.is_reactive_static = true; + } + }); + } + unsorted_reactive_declarations.push({ assignees, dependencies, diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index e4d4ffddad..b0c836681c 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -44,7 +44,7 @@ export default { code: 'invalid-binding', message: 'Cannot bind to a variable declared with {@const ...}' }, - invalid_binding_writibale: { + invalid_binding_writable: { code: 'invalid-binding', message: 'Cannot bind to a variable which is not writable' }, diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 3f552eb8b8..a10fe6155c 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -187,6 +187,10 @@ export default { code: 'a11y-no-noninteractive-tabindex', message: 'A11y: noninteractive element cannot have nonnegative tabIndex value' }, + a11y_aria_activedescendant_has_tabindex: { + code: 'a11y-aria-activedescendant-has-tabindex', + message: 'A11y: Elements with attribute aria-activedescendant should have tabindex value' + }, redundant_event_modifier_for_touch: { code: 'redundant-event-modifier', message: 'Touch event handlers that don\'t use the \'event\' object are passive by default' diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 07e2dc439e..5feb59ec0c 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -350,7 +350,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string, const attr = node.attributes.find((attr: CssNode) => attr.name === name); if (!attr) return false; if (attr.is_true) return operator === null; - if (!expected_value) return true; + if (expected_value == null) return true; if (attr.chunks.length === 1) { const value = attr.chunks[0]; diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index f826df4828..9a62b68850 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -22,7 +22,9 @@ const read_only_media_attributes = new Set([ 'seeking', 'ended', 'videoHeight', - 'videoWidth' + 'videoWidth', + 'naturalWidth', + 'naturalHeight' ]); export default class Binding extends Node { @@ -80,7 +82,7 @@ export default class Binding extends Node { variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; if (info.expression.type === 'Identifier' && !variable.writable) { - component.error(this.expression.node as any, compiler_errors.invalid_binding_writibale); + component.error(this.expression.node as any, compiler_errors.invalid_binding_writable); return; } } diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 0d3e8a01bd..1ac75faffd 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -225,6 +225,7 @@ export default class Element extends Node { namespace: string; needs_manual_style_scoping: boolean; tag_expr: Expression; + contains_a11y_label: boolean; get is_dynamic_element() { return this.name === 'svelte:element'; @@ -484,6 +485,11 @@ export default class Element extends Node { component.warn(attribute, compiler_warnings.a11y_incorrect_attribute_type(schema, name)); } } + + // aria-activedescendant-has-tabindex + if (name === 'aria-activedescendant' && !is_interactive_element(this.name, attribute_map) && !attribute_map.has('tabindex')) { + component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex); + } } // aria-role @@ -620,6 +626,7 @@ export default class Element extends Node { const id_attribute = attribute_map.get('id'); const name_attribute = attribute_map.get('name'); const target_attribute = attribute_map.get('target'); + const aria_label_attribute = attribute_map.get('aria-label'); // links with target="_blank" should have noopener or noreferrer: https://developer.chrome.com/docs/lighthouse/best-practices/external-anchors-use-rel-noopener/ // modern browsers add noopener by default, so we only need to check legacy browsers @@ -642,6 +649,13 @@ export default class Element extends Node { } } + if (aria_label_attribute) { + const aria_value = aria_label_attribute.get_static_value(); + if (aria_value != '') { + this.contains_a11y_label = true; + } + } + if (href_attribute) { const href_value = href_attribute.get_static_value(); @@ -718,7 +732,10 @@ export default class Element extends Node { } if (this.name === 'video') { - if (attribute_map.has('muted')) { + const aria_hidden_attribute = attribute_map.get('aria-hidden'); + const aria_hidden_exist = aria_hidden_attribute && aria_hidden_attribute.get_static_value(); + + if (attribute_map.has('muted') || aria_hidden_exist === 'true') { return; } @@ -901,6 +918,13 @@ export default class Element extends Node { } else if (is_void(this.name)) { return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `void elements like <${this.name}>. Use a wrapper element instead`)); } + } else if ( + name === 'naturalWidth' || + name === 'naturalHeight' + ) { + if (this.name !== 'img') { + return component.error(binding, compiler_errors.invalid_binding_element_with('
\ No newline at end of file
diff --git a/test/css/samples/unused-selector-empty-attribute/input.svelte b/test/css/samples/unused-selector-empty-attribute/input.svelte
new file mode 100644
index 0000000000..0200e537be
--- /dev/null
+++ b/test/css/samples/unused-selector-empty-attribute/input.svelte
@@ -0,0 +1,11 @@
+
+
+
diff --git a/test/js/samples/capture-inject-state/expected.js b/test/js/samples/capture-inject-state/expected.js
index 882b3e9cf6..41f9b966b0 100644
--- a/test/js/samples/capture-inject-state/expected.js
+++ b/test/js/samples/capture-inject-state/expected.js
@@ -48,7 +48,7 @@ function create_fragment(ctx) {
t8 = text(/*$prop*/ ctx[2]);
t9 = space();
t10 = text(/*shadowedByModule*/ ctx[4]);
- add_location(p, file, 22, 0, 430);
+ add_location(p, file, 22, 0, 431);
},
l: function claim(nodes) {
throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");
@@ -91,7 +91,7 @@ function create_fragment(ctx) {
}
let moduleLiveBinding;
-const moduleContantProps = 4;
+const moduleConstantProps = 4;
let moduleLet;
const moduleConst = 2;
let shadowedByModule;
@@ -137,7 +137,7 @@ function instance($$self, $$props, $$invalidate) {
$$self.$capture_state = () => ({
moduleLiveBinding,
- moduleContantProps,
+ moduleConstantProps,
moduleLet,
moduleConst,
shadowedByModule,
@@ -197,4 +197,4 @@ class Component extends SvelteComponentDev {
}
export default Component;
-export { moduleLiveBinding, moduleContantProps };
\ No newline at end of file
+export { moduleLiveBinding, moduleConstantProps };
diff --git a/test/js/samples/capture-inject-state/input.svelte b/test/js/samples/capture-inject-state/input.svelte
index a1051bc147..a80fecf00f 100644
--- a/test/js/samples/capture-inject-state/input.svelte
+++ b/test/js/samples/capture-inject-state/input.svelte
@@ -1,6 +1,6 @@
+
+
+
+{naturalWidth} x {naturalHeight}
diff --git a/test/js/samples/reactive-class-optimized/expected.js b/test/js/samples/reactive-class-optimized/expected.js
index 1d0606ad60..f75a4015b0 100644
--- a/test/js/samples/reactive-class-optimized/expected.js
+++ b/test/js/samples/reactive-class-optimized/expected.js
@@ -9,7 +9,6 @@ import {
noop,
safe_not_equal,
space,
- subscribe,
toggle_class
} from "svelte/internal";
@@ -133,13 +132,8 @@ let reactiveModuleVar = Math.random();
function instance($$self, $$props, $$invalidate) {
let reactiveDeclaration;
let $reactiveStoreVal;
-
- let $reactiveDeclaration,
- $$unsubscribe_reactiveDeclaration = noop,
- $$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration);
-
+ let $reactiveDeclaration;
component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value));
- $$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration());
nonReactiveGlobal = Math.random();
const reactiveConst = { x: Math.random() };
reactiveModuleVar += 1;
@@ -148,7 +142,8 @@ function instance($$self, $$props, $$invalidate) {
reactiveConst.x += 1;
}
- $: $$subscribe_reactiveDeclaration($$invalidate(1, reactiveDeclaration = reactiveModuleVar * 2));
+ $: reactiveDeclaration = reactiveModuleVar * 2;
+ component_subscribe($$self, reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value));
return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration];
}
diff --git a/test/js/samples/reactive-values/expected.js b/test/js/samples/reactive-values/expected.js
new file mode 100644
index 0000000000..7ed435d6ad
--- /dev/null
+++ b/test/js/samples/reactive-values/expected.js
@@ -0,0 +1,60 @@
+/* generated by Svelte vX.Y.Z */
+import {
+ SvelteComponent,
+ detach,
+ element,
+ init,
+ insert,
+ noop,
+ safe_not_equal,
+ set_data,
+ space,
+ text
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+ let h1;
+ let t3;
+ let t4;
+
+ return {
+ c() {
+ h1 = element("h1");
+ h1.textContent = `Hello ${name}!`;
+ t3 = space();
+ t4 = text(/*foo*/ ctx[0]);
+ },
+ m(target, anchor) {
+ insert(target, h1, anchor);
+ insert(target, t3, anchor);
+ insert(target, t4, anchor);
+ },
+ p(ctx, [dirty]) {
+ if (dirty & /*foo*/ 1) set_data(t4, /*foo*/ ctx[0]);
+ },
+ i: noop,
+ o: noop,
+ d(detaching) {
+ if (detaching) detach(h1);
+ if (detaching) detach(t3);
+ if (detaching) detach(t4);
+ }
+ };
+}
+
+let name = 'world';
+
+function instance($$self) {
+ let foo;
+ $: foo = name + name;
+ return [foo];
+}
+
+class Component extends SvelteComponent {
+ constructor(options) {
+ super();
+ init(this, options, instance, create_fragment, safe_not_equal, {});
+ }
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/reactive-values/input.svelte b/test/js/samples/reactive-values/input.svelte
new file mode 100644
index 0000000000..d713217771
--- /dev/null
+++ b/test/js/samples/reactive-values/input.svelte
@@ -0,0 +1,7 @@
+
+
+