`, denotes a regular HTML element. A capitalised tag
By default, attributes work exactly like their HTML counterparts.
-```html
+```sv
can't touch this
@@ -36,7 +36,7 @@ By default, attributes work exactly like their HTML counterparts.
As in HTML, values may be unquoted.
-```html
+```sv
```
@@ -44,7 +44,7 @@ As in HTML, values may be unquoted.
Attribute values can contain JavaScript expressions.
-```html
+```sv
page {p}
```
@@ -52,7 +52,7 @@ Attribute values can contain JavaScript expressions.
Or they can *be* JavaScript expressions.
-```html
+```sv
...
```
@@ -71,7 +71,7 @@ All other attributes are included unless their value is [nullish](https://develo
An expression might include characters that would cause syntax highlighting to fail in regular HTML, so quoting the value is permitted. The quotes do not affect how the value is parsed:
-```html
+```sv
...
```
@@ -79,7 +79,7 @@ An expression might include characters that would cause syntax highlighting to f
When the attribute name and value match (`name={name}`), they can be replaced with `{name}`.
-```html
+```sv
...
...
@@ -91,7 +91,7 @@ By convention, values passed to components are referred to as *properties* or *p
As with elements, `name={name}` can be replaced with the `{name}` shorthand.
-```html
+```sv
```
@@ -101,7 +101,7 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
An element or component can have multiple spread attributes, interspersed with regular ones.
-```html
+```sv
```
@@ -109,7 +109,7 @@ An element or component can have multiple spread attributes, interspersed with r
*`$$props`* references all props that are passed to a component – including ones that are not declared with `export`. It is useful in rare cases, but not generally recommended, as it is difficult for Svelte to optimise.
-```html
+```sv
```
@@ -133,7 +133,7 @@ An element or component can have multiple spread attributes, interspersed with r
Text can also contain JavaScript expressions:
-```html
+```sv
Hello {name}!
{a} + {b} = {a + b}.
```
@@ -145,7 +145,7 @@ Text can also contain JavaScript expressions:
You can use HTML comments inside components.
-```html
+```sv
Hello world
```
@@ -154,7 +154,7 @@ You can use HTML comments inside components.
Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually these are accessibility warnings; make sure that you're disabling them for a good reason.
-```html
+```sv
```
@@ -176,7 +176,7 @@ Comments beginning with `svelte-ignore` disable warnings for the next block of m
Content that is conditionally rendered can be wrapped in an if block.
-```html
+```sv
{#if answer === 42}
what was the question?
{/if}
@@ -186,7 +186,7 @@ Content that is conditionally rendered can be wrapped in an if block.
Additional conditions can be added with `{:else if expression}`, optionally ending in an `{:else}` clause.
-```html
+```sv
{#if porridge.temperature > 100}
too hot!
{:else if 80 > porridge.temperature}
@@ -206,6 +206,9 @@ Additional conditions can be added with `{:else if expression}`, optionally endi
{#each expression as name, index}...{/each}
```
```sv
+{#each expression as name (key)}...{/each}
+```
+```sv
{#each expression as name, index (key)}...{/each}
```
```sv
@@ -216,7 +219,7 @@ Additional conditions can be added with `{:else if expression}`, optionally endi
Iterating over lists of values can be done with an each block.
-```html
+```sv
Shopping list
{#each items as item}
@@ -231,7 +234,7 @@ You can use each blocks to iterate over any array or array-like value — that i
An each block can also specify an *index*, equivalent to the second argument in an `array.map(...)` callback:
-```html
+```sv
{#each items as item, i}
{i + 1}: {item.name} x {item.qty}
{/each}
@@ -241,7 +244,12 @@ An each block can also specify an *index*, equivalent to the second argument in
If a *key* expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
-```html
+```sv
+{#each items as item (item.id)}
+ {item.name} x {item.qty}
+{/each}
+
+
{#each items as item, i (item.id)}
{i + 1}: {item.name} x {item.qty}
{/each}
@@ -251,7 +259,7 @@ If a *key* expression is provided — which must uniquely identify each list ite
You can freely use destructuring and rest patterns in each blocks.
-```html
+```sv
{#each items as { id, name, qty }, i (id)}
{i + 1}: {name} x {qty}
{/each}
@@ -269,7 +277,7 @@ You can freely use destructuring and rest patterns in each blocks.
An each block can also have an `{:else}` clause, which is rendered if the list is empty.
-```html
+```sv
{#each todos as todo}
{todo.text}
{:else}
@@ -294,7 +302,7 @@ An each block can also have an `{:else}` clause, which is rendered if the list i
Await blocks allow you to branch on the three possible states of a Promise — pending, fulfilled or rejected.
-```html
+```sv
{#await promise}
waiting for the promise to resolve...
@@ -311,7 +319,7 @@ Await blocks allow you to branch on the three possible states of a Promise — p
The `catch` block can be omitted if you don't need to render anything when the promise rejects (or no error is possible).
-```html
+```sv
{#await promise}
waiting for the promise to resolve...
@@ -325,7 +333,7 @@ The `catch` block can be omitted if you don't need to render anything when the p
If you don't care about the pending state, you can also omit the initial block.
-```html
+```sv
{#await promise then value}
The value is {value}
{/await}
@@ -346,7 +354,7 @@ The expression should be valid standalone HTML — `{@html ""}content{@html
> Svelte does not sanitize expressions before injecting HTML. If the data comes from an untrusted source, you must sanitize it, or you are exposing your users to an XSS vulnerability.
-```html
+```sv
{post.title}
{@html post.content}
@@ -369,7 +377,7 @@ The `{@debug ...}` tag offers an alternative to `console.log(...)`. It logs the
It accepts a comma-separated list of variable names (not arbitrary expressions).
-```html
+```sv
@@ -1341,7 +1351,7 @@ The `
` element renders a component dynamically, using the comp
If `this` is falsy, no component is rendered.
-```html
+```sv
```
@@ -1359,7 +1369,7 @@ If `this` is falsy, no component is rendered.
The `` element allows you to add event listeners to the `window` object without worrying about removing them when the component is destroyed, or checking for the existence of `window` when server-side rendering.
-```html
+```sv
@@ -582,7 +582,7 @@ Animates a `blur` filter alongside an element's opacity.
* `opacity` (`number`, default 0) - the opacity value to animate out to and in from
* `amount` (`number`, default 5) - the size of the blur in pixels
-```html
+```sv
@@ -621,7 +621,7 @@ Animates the x and y positions and the opacity of an element. `in` transitions a
You can see the `fly` transition in action in the [transition tutorial](tutorial/adding-parameters-to-transitions).
-```html
+```sv
+
+
+
+
+ Frequently Asked Questions • Svelte
+
+
+
+
+
+
+
+
Frequently Asked Questions
+ {#each faqs as faq}
+
+
+
+
+
+ {faq.metadata.question}
+
+ {@html faq.answer}
+
+ {/each}
+
+
+
diff --git a/site/src/routes/repl/[id]/_components/AppControls/index.svelte b/site/src/routes/repl/[id]/_components/AppControls/index.svelte
index 007dba3461..10639e91c0 100644
--- a/site/src/routes/repl/[id]/_components/AppControls/index.svelte
+++ b/site/src/routes/repl/[id]/_components/AppControls/index.svelte
@@ -30,7 +30,7 @@
$: canSave = $session.user && gist && gist.owner === $session.user.uid;
function handleKeydown(event) {
- if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
+ if (event.key === 's' && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
save();
}
diff --git a/site/src/template.html b/site/src/template.html
index 344684d766..28ea1910a5 100644
--- a/site/src/template.html
+++ b/site/src/template.html
@@ -7,6 +7,7 @@
%sapper.base%
+
diff --git a/site/src/utils/highlight.js b/site/src/utils/highlight.js
index d8be46b3a6..d9e4fe5ec8 100644
--- a/site/src/utils/highlight.js
+++ b/site/src/utils/highlight.js
@@ -1,6 +1,7 @@
import { langs } from '@sveltejs/site-kit/utils/markdown.js';
import PrismJS from 'prismjs';
import 'prismjs/components/prism-bash';
+import 'prism-svelte';
export function highlight(source, lang) {
const plang = langs[lang] || '';
diff --git a/site/static/global.css b/site/static/global.css
index 14e4e0a0c0..7aa0715a98 100644
--- a/site/static/global.css
+++ b/site/static/global.css
@@ -1,500 +1,31 @@
-/*
------------------------------------------------
- vars – css custom-properties
+/* headers anchors */
- NOTE
- - some vars change inside media-queries!
- - under normal conditions, there's no need to touch these
------------------------------------------------
-*/
-:root {
- --nav-h: 6rem;
- --top-offset: 6rem;
- --sidebar-w: 30rem;
- --sidebar-mid-w: 36rem;
- --sidebar-large-w: 48rem;
- --main-width: 80rem;
- --code-w: 72em;
- --side-nav: 3.2rem;
- --side-page: var(--side-nav);
-
- /* easings */
- --in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
- --out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
- --inout-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
-
- --in-back: cubic-bezier(0.6, -0.28, 0.735, 0.045);
- --out-back: cubic-bezier(0.175, 0.885, 0.32, 1.275);
- --inout-back: cubic-bezier(0.68, -0.55, 0.265, 1.55);
-}
-
-@media screen and (min-width: 768px) {
- :root {
- --side-page: 14vw;
- --top-offset: 10rem;
- --side-nav: 4.8rem;
- }
-}
-
-/* theme vars */
-.theme-default {
- --back: #ffffff;
- --back-light: #f6fafd;
- --back-api: #eff8ff;
- --prime: #ff3e00;
- --second: #676778;
- --flash: #40b3ff;
- --heading: var(--second);
- --text: #444;
- --sidebar-text: rgba(255, 255, 255, .75);
- --border-w: .3rem; /* border-width */
- --border-r: .4rem; /* border-radius */
-}
-
-/* typo vars */
-.typo-default {
- --unit: .8rem;
- --code-fs: 1.3rem;
- --h6: 1.4rem;
- --h5: 1.6rem;
- --h4: 1.8rem; /* default font-size */
- --h3: 2.6rem;
- --h2: 3rem;
- --h1: 3.2rem;
- --linemax: 42em; /* max line-length */
- --lh: 1.5; /* base line-height */
-}
-
-body {
- --font: 'Overpass', sans-serif;
- --font-mono: 'Fira Mono', monospace;
- --font-ui: var(--font-mono);
- --font-system: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
-}
-
-
-/* fonts ---------------------------------- */
-/* overpass-300normal - latin */
-@font-face {
- font-family: 'Overpass';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src:
- local('Overpass Light '),
- local('Overpass-Light'),
- url('fonts/overpass/overpass-latin-300.woff2') format('woff2');
-}
-
-/* overpass-600normal - latin */
-@font-face {
- font-family: 'Overpass';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src:
- local('Overpass Bold '),
- local('Overpass-Bold'),
- url('fonts/overpass/overpass-latin-600.woff2') format('woff2');
-}
-
-/* fira-mono-400normal - latin */
-@font-face {
- font-family: 'Fira Mono';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src:
- local('Fira Mono Regular '),
- local('Fira Mono-Regular'),
- url('fonts/fira-mono/fira-mono-latin-400.woff2') format('woff2');
-}
-
-/* base reset ----------------------------- */
-html {
- font-size: 62.5%;
- -ms-text-size-adjust: 62.5%;
- -webkit-text-size-adjust: 62.5%;
- -ms-overflow-style: -ms-autohiding-scrollbar;
- box-sizing: border-box;
- border-collapse: collapse;
-}
-
-html,
-body,
-#sapper {
- width: 100%;
- height: 100%;
-}
-
-* {
- box-sizing: inherit;
- margin: 0;
- padding: 0;
-}
-
-/* link reset ----------------------------- */
-a {
- text-decoration: none;
- cursor: pointer;
- color: inherit;
-}
-
-a:hover, a:active { color: var(--flash) }
-a:focus { outline: none }
-
-/*
------------------------------------------------
- global styles
------------------------------------------------
-*/
-
-/* typography ----------------------------- */
-body {
- font: 300 var(--h4)/var(--lh) var(--font);
- background-color: var(--back);
- color: var(--text);
-
- /* default spacing of Overpass is a bit too airy */
- /* letter-spacing: -.013em; */
-}
-
-h1, h2, h3, h4, h5, h6, blockquote {
- position: relative;
- margin: 0;
- color: var(--heading);
-}
-
-/* h1, h2, h3, h4, h5, h6 { font-weight: 600 } */
-h6 { font-size: var(--h6) }
-h5 { font-size: var(--h5) }
-h4 { font-size: var(--h4) }
-h3 { font-size: var(--h3) }
-h2 { font-size: var(--h2) }
-h1 { font-size: var(--h1) }
-
-h1, h2 {
- font-family: var(--font);
- line-height: 1.25;
-}
-
-h3 { font-weight: 300 }
-
-p, ol, ul {
- margin: 0 0 1em 0;
-}
-
-.b, b, strong { font-weight: 600 }
-
-tt, code, kbd, samp {
- font: 400 var(--code-fs)/1.7 var(--font-mono);
-}
-
-code {
- position: relative;
- border-radius: .3em;
- white-space: nowrap;
- color: #444;
- -webkit-font-smoothing: initial;
-}
-
-pre code {
- top: 0;
- white-space: inherit;
- background-color: none;
-}
-
-/* sync CodeMirror with prism */
-.CodeMirror {
- font-size: var(--code-fs) !important;
-}
-
-::selection {
- background: var(--flash);
- color: white;
-}
-
-/* opinionated styles --------------------- */
-
-li:not(.white) > h2 {
- color: var(--second)
-}
-
-blockquote {
- position: relative;
- margin: 1.6rem 0 2.4rem;
- padding: 2rem 2.4rem 1.8rem 2.4rem;
- border-radius: var(--border-r);
- font-family: var(--font);
- max-width: var(--linemax);
-}
-
-blockquote p {
- font-size: var(--h5);
-}
-
-blockquote :last-child {
- margin: 0;
-}
-
-/* buttons -------------------------------- */
-button {
- font-family: inherit;
- font-size: inherit;
- background-color: transparent;
- border: none;
- color: currentColor;
- cursor: pointer;
-}
-
-button:focus,
-.btn:focus { outline: 0 }
-
-button[disabled],
-.btn[disabled],
-.btn:hover[disabled] {
- opacity: .55;
- pointer-events: none;
-}
-
-button > svg,
-.btn > svg {
- position: relative;
- top: -.1rem;
- width: 2rem !important;
- height: 2rem !important;
- stroke: currentColor !important;
-}
-
-/* reset ------- */
-.btn {
- --btn-h: 4rem;
- --btn-outline: .2rem;
- --btn-font: var(--font);
- --btn-calc-h: calc(var(--btn-h) - var(--btn-outline) * 2);
- --btn-hover: linear-gradient(to top, rgba(0,0,0,.07), rgba(0,0,0,.07));
-
- position: relative;
- margin: 0 .8rem .8rem 0;
- vertical-align: middle;
- white-space: nowrap;
- display: inline-block;
- zoom: 1;
- border: none transparent;
- font: var(--h4) var(--btn-font);
- border-radius: var(--border-r);
- color: currentColor;
- cursor: pointer;
-}
-
-/* default */
-.btn {
- line-height: var(--btn-h);
- height: var(--btn-h);
- padding: 0 1.6rem;
- transition: all .1s;
-}
-
-.btn:hover {
- transform: scale(.98);
- mix-blend-mode: multiply;
- background-image: var(--btn-hover);
-}
-
-/* optional */
-.btn[outline] {
- line-height: var(--btn-calc-h);
- height: var(--btn-calc-h);
- border: var(--btn-outline) solid currentColor;
- background-color: white;
- color: currentColor;
-}
-
-/* links ------------------------------------- */
-a {
- position: relative;
- padding: 0 0 1px 0;
- border-bottom: 1px solid currentColor;
- user-select: none;
- color: var(--prime);
- transition: color .2s, border .2s, padding .2s;
-}
-
-a:hover {
- color: var(--flash);
-}
-
-a:hover {
- padding: 0;
- border-bottom: 2px solid currentColor;
-}
-
-a.no-underline {
- border-bottom: none;
- padding: 0;
-}
-
-/* a:hover:not(.disabled) > .icon { stroke: var(--flash) } */
-
-/* lists ---------------------------------- */
-.listify ol,
-.listify ul {
- --list-padding: 2.9rem;
-
- list-style: none;
- color: currentColor;
- margin-left: var(--list-padding);
-}
-
-.listify ol > li,
-.listify ul > li {
- max-width: calc(var(--linemax) - var(--list-padding));
- line-height: 1.5;
- margin: 0 0 0.4rem 0;
-}
-
-.listify ul > li:before {
- content: '';
- position: absolute;
- margin-top: 1.1rem;
- margin-left: -1.8rem;
- background-color: var(--second);
- width: .6rem;
- height: .6rem;
- border-radius: 2px;
- opacity: 0.7;
-}
-
-.listify ol { list-style: decimal }
-
-/* tables --------------------------------- */
-table {
- width: 100%;
- font-size: var(--h5);
-}
-
-td, th {
- text-align: left;
- border-bottom: 1px solid #eee;
- padding: 0.4rem 0.8rem 0.4rem 0;
-}
-
-table code, table span {
- white-space: pre;
-}
-
-/* grid ----------------------------------- */
-.grid, .grid.half {
- display: grid;
- grid-gap: 2.4rem;
- grid-template-columns: 1fr;
- align-items: center;
-}
-
-.grid.stretch { align-items: stretch }
-
-.grid > .cols-2,
-.grid > .cols-3 { grid-column: span 1 }
-
-@media screen and (min-width: 840px) {
- .grid.half,
- .grid { grid-template-columns: repeat(2, 1fr) }
- .grid > .cols-2,
- .grid > .cols-3 { grid-column: span 2 }
-}
-
-@media screen and (min-width: 1100px) {
- .grid { grid-template-columns: repeat(3, 1fr) }
- .grid > .cols-2 { grid-column: span 2 }
- .grid > .cols-3 { grid-column: span 3 }
-}
-
-/* helper styles -------------------------- */
-.flex-auto { flex: 1 0 auto }
-
-.py0 {
- padding-top: 0 !important;
- padding-bottom: 0 !important;
-}
-
-.legend, figcaption, .post aside {
- max-width: none;
- margin: 0 auto;
- padding: 1.6rem 0 0 .8rem;
- font: 1.2rem/1.6 var(--font-ui);
-}
-
-.filename {
- display: inline-block;
- padding: 1.6rem 0 0 1rem;
- font: var(--h6) var(--font-ui);
-}
-
-.box {
- padding: 2.4rem 3.2rem;
- border-radius: var(--border-r);
-}
-
-/* theme colors --------------------------- */
-.prime { color: var(--prime) !important }
-.second { color: var(--second) !important }
-.flash { color: var(--flash) !important }
-.black { color: black !important }
-.white { color: white !important }
-
-.back { background-color: var(--back) !important }
-.back-light { background-color: var(--back-light) !important }
-.bg-prime { background-color: var(--prime) !important }
-.bg-second { background-color: var(--second) !important }
-.bg-flash { background-color: var(--flash) !important }
-
-/* inputs --------------------------------- */
-input[type="checkbox"] {
- /* display: block; */
+.offset-anchor {
position: relative;
- height: 1em;
- width: calc(100% - 0.6em);
- max-width: 2em;
- top: -2px;
- border-radius: 0.5em;
- -webkit-appearance: none;
- outline: none;
- margin: 0 0.6em 0 0;
-}
-
-input[type="checkbox"]::before {
- content: "";
- position: absolute;
display: block;
- height: 100%;
- width: 100%;
- padding: 2px;
- border-radius: 1em;
- top: 0;
- left: 0;
- background: var(--second);
- /* box-sizing: border-box; */
- box-sizing: content-box;
+ top: calc(-1 * (var(--nav-h) + var(--top-offset)) + 11rem);
+ width: 0;
+ height: 0;
}
-input[type="checkbox"]:checked::before {
- background: var(--prime);
-}
-
-input[type="checkbox"]::after {
- content: "";
+.anchor {
position: absolute;
display: block;
+ background: url(/icons/link.svg) 0 50% no-repeat;
+ background-size: 1em 1em;
+ width: 1.4em;
height: 1em;
- width: 1em;
- top: 2px;
- left: 2px;
- border-radius: 1em;
- background: white;
- box-shadow: 0 0px 1px rgba(0,0,0,.4), 0 4px 2px rgba(0,0,0,.1);
- -webkit-transition: background .2s ease-out, left .2s ease-out;
-}
-
-input[type="checkbox"]:checked::after {
- left: calc(100% - 9px);
+ top: calc(((var(--h3) - 24px) / 2) + 0.6em);
+ left: -1.4em;
+ opacity: 0;
+ transition: opacity 0.2s;
+ border: none !important; /* TODO get rid of linkify */
+}
+
+h2:hover .anchor,
+h3:hover .anchor,
+h4:hover .anchor,
+h5:hover .anchor,
+h6:hover .anchor {
+ opacity: 1;
}
diff --git a/src/compiler/compile/nodes/AwaitBlock.ts b/src/compiler/compile/nodes/AwaitBlock.ts
index a7b8fb815e..66bc15364c 100644
--- a/src/compiler/compile/nodes/AwaitBlock.ts
+++ b/src/compiler/compile/nodes/AwaitBlock.ts
@@ -3,17 +3,21 @@ import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import Expression from './shared/Expression';
-import { Pattern } from 'estree';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
-import traverse_destructure_pattern from '../utils/traverse_destructure_pattern';
+import { Context, unpack_destructuring } from './shared/Context';
+import { Node as ESTreeNode } from 'estree';
export default class AwaitBlock extends Node {
type: 'AwaitBlock';
expression: Expression;
- value: DestructurePattern;
- error: DestructurePattern;
+
+ then_contexts: Context[];
+ catch_contexts: Context[];
+
+ then_node: ESTreeNode | null;
+ catch_node: ESTreeNode | null;
pending: PendingBlock;
then: ThenBlock;
@@ -24,24 +28,21 @@ export default class AwaitBlock extends Node {
this.expression = new Expression(component, this, scope, info.expression);
- this.value = info.value && new DestructurePattern(info.value);
- this.error = info.error && new DestructurePattern(info.error);
+ this.then_node = info.value;
+ this.catch_node = info.error;
+
+ if (this.then_node) {
+ this.then_contexts = [];
+ unpack_destructuring(this.then_contexts, info.value, node => node);
+ }
+
+ if (this.catch_node) {
+ this.catch_contexts = [];
+ unpack_destructuring(this.catch_contexts, info.error, node => node);
+ }
this.pending = new PendingBlock(component, this, scope, info.pending);
this.then = new ThenBlock(component, this, scope, info.then);
this.catch = new CatchBlock(component, this, scope, info.catch);
}
}
-
-export class DestructurePattern {
- pattern: Pattern;
- expressions: string[];
- identifier_name: string | undefined;
-
- constructor(pattern: Pattern) {
- this.pattern = pattern;
- this.expressions = [];
- traverse_destructure_pattern(pattern, (node) => this.expressions.push(node.name));
- this.identifier_name = this.pattern.type === 'Identifier' ? this.pattern.name : undefined;
- }
-}
diff --git a/src/compiler/compile/nodes/CatchBlock.ts b/src/compiler/compile/nodes/CatchBlock.ts
index 8b3736a2b9..1a92f617bb 100644
--- a/src/compiler/compile/nodes/CatchBlock.ts
+++ b/src/compiler/compile/nodes/CatchBlock.ts
@@ -13,9 +13,9 @@ export default class CatchBlock extends AbstractBlock {
super(component, parent, scope, info);
this.scope = scope.child();
- if (parent.error) {
- parent.error.expressions.forEach(expression => {
- this.scope.add(expression, parent.expression.dependencies, this);
+ if (parent.catch_node) {
+ parent.catch_contexts.forEach(context => {
+ this.scope.add(context.key.name, parent.expression.dependencies, this);
});
}
this.children = map_children(component, parent, this.scope, info.children);
diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts
index 31850f8745..6458ea0020 100644
--- a/src/compiler/compile/nodes/EachBlock.ts
+++ b/src/compiler/compile/nodes/EachBlock.ts
@@ -4,56 +4,8 @@ import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import Element from './Element';
-import { x } from 'code-red';
-import { Node, Identifier, RestElement } from 'estree';
-
-interface Context {
- key: Identifier;
- name?: string;
- modifier: (node: Node) => Node;
-}
-
-function unpack_destructuring(contexts: Context[], node: Node, modifier: (node: Node) => Node) {
- if (!node) return;
-
- if (node.type === 'Identifier' || (node as any).type === 'RestIdentifier') { // TODO is this right? not RestElement?
- contexts.push({
- key: node as Identifier,
- modifier
- });
- } else if (node.type === 'ArrayPattern') {
- node.elements.forEach((element, i) => {
- if (element && (element as any).type === 'RestIdentifier') {
- unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node);
- } else {
- unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node);
- }
- });
- } else if (node.type === 'ObjectPattern') {
- const used_properties = [];
-
- node.properties.forEach((property, i) => {
- if ((property as any).kind === 'rest') { // TODO is this right?
- const replacement: RestElement = {
- type: 'RestElement',
- argument: property.key as Identifier
- };
-
- node.properties[i] = replacement as any;
-
- unpack_destructuring(
- contexts,
- property.value,
- node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node
- );
- } else {
- used_properties.push(x`"${(property.key as Identifier).name}"`);
-
- unpack_destructuring(contexts, property.value, node => x`${modifier(node)}.${(property.key as Identifier).name}` as Node);
- }
- });
- }
-}
+import { Context, unpack_destructuring } from './shared/Context';
+import { Node } from 'estree';
export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
index f786255518..2d35fd211c 100644
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -19,14 +19,10 @@ import { INode } from './interfaces';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
-const aria_attributes = 'activedescendant atomic autocomplete busy checked colindex controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowindex selected setsize sort valuemax valuemin valuenow valuetext'.split(
- ' '
-);
+const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes);
-const aria_roles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term textbox timer toolbar tooltip tree treegrid treeitem widget window'.split(
- ' '
-);
+const aria_roles = 'alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic grid gridcell group heading img link list listbox listitem log main marquee math meter menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem'.split(' ');
const aria_role_set = new Set(aria_roles);
const a11y_required_attributes = {
@@ -57,6 +53,11 @@ const a11y_required_content = new Set([
'h6',
]);
+const a11y_no_onchange = new Set([
+ 'select',
+ 'option'
+]);
+
const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set(['preventDefault', 'stopPropagation', 'capture', 'once', 'passive', 'self']);
@@ -257,6 +258,7 @@ export default class Element extends Node {
}
this.validate_attributes();
+ this.validate_special_cases();
this.validate_bindings();
this.validate_content();
this.validate_event_handlers();
@@ -404,25 +406,45 @@ export default class Element extends Node {
attribute_map.set(attribute.name, attribute);
});
+ }
+
+ validate_special_cases() {
+ const { component, attributes, handlers } = this;
+ const attribute_map = new Map();
+ const handlers_map = new Map();
+
+ attributes.forEach(attribute => (
+ attribute_map.set(attribute.name, attribute)
+ ));
+
+ handlers.forEach(handler => (
+ handlers_map.set(handler.name, handler)
+ ));
- // handle special cases
if (this.name === 'a') {
- const attribute = attribute_map.get('href') || attribute_map.get('xlink:href');
+ const href_attribute = attribute_map.get('href') || attribute_map.get('xlink:href');
+ const id_attribute = attribute_map.get('id');
+ const name_attribute = attribute_map.get('name');
- if (attribute) {
- const value = attribute.get_static_value();
+ if (href_attribute) {
+ const href_value = href_attribute.get_static_value();
- if (value === '' || value === '#') {
- component.warn(attribute, {
+ if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) {
+ component.warn(href_attribute, {
code: `a11y-invalid-attribute`,
- message: `A11y: '${value}' is not a valid ${attribute.name} attribute`,
+ message: `A11y: '${href_value}' is not a valid ${href_attribute.name} attribute`
});
}
} else {
- component.warn(this, {
- code: `a11y-missing-attribute`,
- message: `A11y: element should have an href attribute`,
- });
+ const id_attribute_valid = id_attribute && id_attribute.get_static_value() !== '';
+ const name_attribute_valid = name_attribute && name_attribute.get_static_value() !== '';
+
+ if (!id_attribute_valid && !name_attribute_valid) {
+ component.warn(this, {
+ code: `a11y-missing-attribute`,
+ message: `A11y: element should have an href attribute`
+ });
+ }
}
} else {
const required_attributes = a11y_required_attributes[this.name];
@@ -433,6 +455,7 @@ export default class Element extends Node {
should_have_attribute(this, required_attributes);
}
}
+ }
if (this.name === 'input') {
const type = attribute_map.get('type');
@@ -440,12 +463,38 @@ export default class Element extends Node {
const required_attributes = ['alt', 'aria-label', 'aria-labelledby'];
const has_attribute = required_attributes.some((name) => attribute_map.has(name));
- if (!has_attribute) {
- should_have_attribute(this, required_attributes, 'input type="image"');
- }
+ if (!has_attribute) {
+ should_have_attribute(this, required_attributes, 'input type="image"');
}
}
}
+
+ if (this.name === 'img') {
+ const alt_attribute = attribute_map.get('alt');
+ const aria_hidden_attribute = attribute_map.get('aria-hidden');
+
+ const aria_hidden_exist = aria_hidden_attribute && aria_hidden_attribute.get_static_value();
+
+ if (alt_attribute && !aria_hidden_exist) {
+ const alt_value = alt_attribute.get_static_value();
+
+ if (/\b(image|picture|photo)\b/i.test(alt_value)) {
+ component.warn(this, {
+ code: `a11y-img-redundant-alt`,
+ message: `A11y: Screenreaders already announce elements as an image.`
+ });
+ }
+ }
+ }
+
+ if (a11y_no_onchange.has(this.name)) {
+ if (handlers_map.has('change') && !handlers_map.has('blur')) {
+ component.warn(this, {
+ code: `a11y-no-onchange`,
+ message: `A11y: on:blur must be used instead of on:change, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users.`
+ });
+ }
+ }
}
validate_bindings() {
@@ -560,6 +609,7 @@ export default class Element extends Node {
name === 'seekable' ||
name === 'played' ||
name === 'volume' ||
+ name === 'muted' ||
name === 'playbackRate' ||
name === 'seeking' ||
name === 'ended'
diff --git a/src/compiler/compile/nodes/ThenBlock.ts b/src/compiler/compile/nodes/ThenBlock.ts
index 7eefe2e6fb..720f88ad78 100644
--- a/src/compiler/compile/nodes/ThenBlock.ts
+++ b/src/compiler/compile/nodes/ThenBlock.ts
@@ -13,9 +13,9 @@ export default class ThenBlock extends AbstractBlock {
super(component, parent, scope, info);
this.scope = scope.child();
- if (parent.value) {
- parent.value.expressions.forEach(expression => {
- this.scope.add(expression, parent.expression.dependencies, this);
+ if (parent.then_node) {
+ parent.then_contexts.forEach(context => {
+ this.scope.add(context.key.name, parent.expression.dependencies, this);
});
}
this.children = map_children(component, parent, this.scope, info.children);
diff --git a/src/compiler/compile/nodes/shared/Context.ts b/src/compiler/compile/nodes/shared/Context.ts
new file mode 100644
index 0000000000..797405b0fe
--- /dev/null
+++ b/src/compiler/compile/nodes/shared/Context.ts
@@ -0,0 +1,58 @@
+import { x } from 'code-red';
+import { Node, Identifier, RestElement, Property } from 'estree';
+
+export interface Context {
+ key: Identifier;
+ name?: string;
+ modifier: (node: Node) => Node;
+}
+
+export function unpack_destructuring(contexts: Context[], node: Node, modifier: (node: Node) => Node) {
+ if (!node) return;
+
+ if (node.type === 'Identifier') {
+ contexts.push({
+ key: node as Identifier,
+ modifier
+ });
+ } else if (node.type === 'RestElement') {
+ contexts.push({
+ key: node.argument as Identifier,
+ modifier
+ });
+ } else if (node.type === 'ArrayPattern') {
+ node.elements.forEach((element, i) => {
+ if (element && element.type === 'RestElement') {
+ unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node);
+ } else if (element && element.type === 'AssignmentPattern') {
+ unpack_destructuring(contexts, element.left, node => x`${modifier(node)}[${i}] !== undefined ? ${modifier(node)}[${i}] : ${element.right}` as Node);
+ } else {
+ unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node);
+ }
+ });
+ } else if (node.type === 'ObjectPattern') {
+ const used_properties = [];
+
+ node.properties.forEach((property) => {
+ const props: (RestElement | Property) = (property as any);
+
+ if (props.type === 'RestElement') {
+ unpack_destructuring(
+ contexts,
+ props.argument,
+ node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node
+ );
+ } else {
+ const key = property.key as Identifier;
+ const value = property.value;
+
+ used_properties.push(x`"${(key as Identifier).name}"`);
+ if (value.type === 'AssignmentPattern') {
+ unpack_destructuring(contexts, value.left, node => x`${modifier(node)}.${key.name} !== undefined ? ${modifier(node)}.${key.name} : ${value.right}` as Node);
+ } else {
+ unpack_destructuring(contexts, value, node => x`${modifier(node)}.${key.name}` as Node);
+ }
+ }
+ });
+ }
+}
diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts
index 9183972196..4ea00094b1 100644
--- a/src/compiler/compile/render_dom/Block.ts
+++ b/src/compiler/compile/render_dom/Block.ts
@@ -185,7 +185,7 @@ export default class Block {
this.chunks.mount.push(b`@append(${parent_node}, ${id});`);
if (is_head(parent_node) && !no_detach) this.chunks.destroy.push(b`@detach(${id});`);
} else {
- this.chunks.mount.push(b`@insert(#target, ${id}, anchor);`);
+ this.chunks.mount.push(b`@insert(#target, ${id}, #anchor);`);
if (!no_detach) this.chunks.destroy.push(b`if (detaching) @detach(${id});`);
}
}
@@ -295,11 +295,11 @@ export default class Block {
if (this.chunks.mount.length === 0) {
properties.mount = noop;
} else if (this.event_listeners.length === 0) {
- properties.mount = x`function #mount(#target, anchor) {
+ properties.mount = x`function #mount(#target, #anchor) {
${this.chunks.mount}
}`;
} else {
- properties.mount = x`function #mount(#target, anchor, #remount) {
+ properties.mount = x`function #mount(#target, #anchor, #remount) {
${this.chunks.mount}
}`;
}
diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts
index c3cd1ed8aa..a94065650a 100644
--- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts
@@ -8,27 +8,37 @@ import FragmentWrapper from './Fragment';
import PendingBlock from '../../nodes/PendingBlock';
import ThenBlock from '../../nodes/ThenBlock';
import CatchBlock from '../../nodes/CatchBlock';
-import { Identifier } from 'estree';
-import traverse_destructure_pattern from '../../utils/traverse_destructure_pattern';
+import { Context } from '../../nodes/shared/Context';
+import { Identifier, Literal, Node } from 'estree';
+
+type Status = 'pending' | 'then' | 'catch';
class AwaitBlockBranch extends Wrapper {
+ parent: AwaitBlockWrapper;
node: PendingBlock | ThenBlock | CatchBlock;
block: Block;
fragment: FragmentWrapper;
is_dynamic: boolean;
var = null;
+ status: Status;
+
+ value: string;
+ value_index: Literal;
+ value_contexts: Context[];
+ is_destructured: boolean;
constructor(
- status: string,
+ status: Status,
renderer: Renderer,
block: Block,
- parent: Wrapper,
- node: AwaitBlock,
+ parent: AwaitBlockWrapper,
+ node: PendingBlock | ThenBlock | CatchBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
+ this.status = status;
this.block = block.child({
comment: create_debugging_comment(node, this.renderer.component),
@@ -36,6 +46,8 @@ class AwaitBlockBranch extends Wrapper {
type: status,
});
+ this.add_context(parent.node[status + '_node'], parent.node[status + '_contexts']);
+
this.fragment = new FragmentWrapper(
renderer,
this.block,
@@ -48,20 +60,43 @@ class AwaitBlockBranch extends Wrapper {
this.is_dynamic = this.block.dependencies.size > 0;
}
+ add_context(node: Node | null, contexts: Context[]) {
+ if (!node) return;
+
+ if (node.type === 'Identifier') {
+ this.value = node.name;
+ this.renderer.add_to_context(this.value, true);
+ } else {
+ contexts.forEach(context => {
+ this.renderer.add_to_context(context.key.name, true);
+ });
+ this.value = this.block.parent.get_unique_name('value').name;
+ this.value_contexts = contexts;
+ this.renderer.add_to_context(this.value, true);
+ this.is_destructured = true;
+ }
+ this.value_index = this.renderer.context_lookup.get(this.value).index;
+ }
+
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.fragment.render(block, parent_node, parent_nodes);
- }
- render_destructure(block: Block, value, node, index) {
- if (value && node.pattern.type !== 'Identifier') {
- traverse_destructure_pattern(node.pattern, (node, parent, index) => {
- parent[index] = x`#ctx[${block.renderer.context_lookup.get(node.name).index}]`;
- });
+ if (this.is_destructured) {
+ this.render_destructure();
+ }
+ }
- this.block.chunks.declarations.push(b`(${node.pattern} = #ctx[${index}])`);
- if (this.block.has_update_method) {
- this.block.chunks.update.push(b`(${node.pattern} = #ctx[${index}])`);
+ render_destructure() {
+ const props = this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`#ctx[${this.value_index}]`)};`);
+ const get_context = this.block.renderer.component.get_unique_name(`get_${this.status}_context`);
+ this.block.renderer.blocks.push(b`
+ function ${get_context}(#ctx) {
+ ${props}
}
+ `);
+ this.block.chunks.declarations.push(b`${get_context}(#ctx)`);
+ if (this.block.has_update_method) {
+ this.block.chunks.update.push(b`${get_context}(#ctx)`);
}
}
}
@@ -73,9 +108,6 @@ export default class AwaitBlockWrapper extends Wrapper {
then: AwaitBlockBranch;
catch: AwaitBlockBranch;
- value: string;
- error: string;
-
var: Identifier = { type: 'Identifier', name: 'await_block' };
constructor(
@@ -92,26 +124,12 @@ export default class AwaitBlockWrapper extends Wrapper {
this.not_static_content();
block.add_dependencies(this.node.expression.dependencies);
- if (this.node.value) {
- for (const ctx of this.node.value.expressions) {
- block.renderer.add_to_context(ctx, true);
- }
- this.value = this.node.value.identifier_name || block.get_unique_name('value').name;
- block.renderer.add_to_context(this.value, true);
- }
- if (this.node.error) {
- for (const ctx of this.node.error.expressions) {
- block.renderer.add_to_context(ctx, true);
- }
- this.error = this.node.error.identifier_name || block.get_unique_name('error').name;
- block.renderer.add_to_context(this.error, true);
- }
let is_dynamic = false;
let has_intros = false;
let has_outros = false;
- ['pending', 'then', 'catch'].forEach((status) => {
+ ['pending', 'then', 'catch'].forEach((status: Status) => {
const child = this.node[status];
const branch = new AwaitBlockBranch(status, renderer, block, this, child, strip_whitespace, next_sibling);
@@ -154,9 +172,6 @@ export default class AwaitBlockWrapper extends Wrapper {
block.maintain_context = true;
- const value_index = this.value && block.renderer.context_lookup.get(this.value).index;
- const error_index = this.error && block.renderer.context_lookup.get(this.error).index;
-
const info_props: any = x`{
ctx: #ctx,
current: null,
@@ -164,8 +179,8 @@ export default class AwaitBlockWrapper extends Wrapper {
pending: ${this.pending.block.name},
then: ${this.then.block.name},
catch: ${this.catch.block.name},
- value: ${value_index},
- error: ${error_index},
+ value: ${this.then.value_index},
+ error: ${this.catch.value_index},
blocks: ${this.pending.block.has_outro_method && x`[,,,]`}
}`;
@@ -188,7 +203,7 @@ export default class AwaitBlockWrapper extends Wrapper {
}
const initial_mount_node = parent_node || '#target';
- const anchor_node = parent_node ? 'null' : 'anchor';
+ const anchor_node = parent_node ? 'null' : '#anchor';
const has_transitions = this.pending.block.has_intro_method || this.pending.block.has_outro_method;
@@ -218,7 +233,7 @@ export default class AwaitBlockWrapper extends Wrapper {
} else {
const #child_ctx = #ctx.slice();
- ${this.value && b`#child_ctx[${value_index}] = ${info}.resolved;`}
+ ${this.then.value && b`#child_ctx[${this.then.value_index}] = ${info}.resolved;`}
${info}.block.p(#child_ctx, #dirty);
}
`);
@@ -232,7 +247,7 @@ export default class AwaitBlockWrapper extends Wrapper {
block.chunks.update.push(b`
{
const #child_ctx = #ctx.slice();
- ${this.value && b`#child_ctx[${value_index}] = ${info}.resolved;`}
+ ${this.then.value && b`#child_ctx[${this.then.value_index}] = ${info}.resolved;`}
${info}.block.p(#child_ctx, #dirty);
}
`);
@@ -256,7 +271,5 @@ export default class AwaitBlockWrapper extends Wrapper {
[this.pending, this.then, this.catch].forEach((branch) => {
branch.render(branch.block, null, x`#nodes` as Identifier);
});
- this.then.render_destructure(block, this.value, this.node.value, value_index);
- this.catch.render_destructure(block, this.error, this.node.error, error_index);
}
}
diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
index 49b63aeae8..6ccb114ed0 100644
--- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
@@ -183,7 +183,7 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
- const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : 'anchor' };
+ const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : '#anchor' };
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts
index 8a246c7a58..8a8f05844c 100644
--- a/src/compiler/compile/render_dom/wrappers/Element/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts
@@ -82,7 +82,9 @@ const events = [
},
{
event_names: ['volumechange'],
- filter: (node: Element, name: string) => node.is_media_node() && name === 'volume',
+ filter: (node: Element, name: string) =>
+ node.is_media_node() &&
+ (name === 'volume' || name === 'muted')
},
{
event_names: ['ratechange'],
@@ -292,7 +294,7 @@ export default class ElementWrapper extends Wrapper {
block.chunks.destroy.push(b`@detach(${node});`);
}
} else {
- block.chunks.mount.push(b`@insert(#target, ${node}, anchor);`);
+ block.chunks.mount.push(b`@insert(#target, ${node}, #anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
index 7f23290279..00a82ddd12 100644
--- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
@@ -265,7 +265,7 @@ export default class IfBlockWrapper extends Wrapper {
`);
const initial_mount_node = parent_node || '#target';
- const anchor_node = parent_node ? 'null' : 'anchor';
+ const anchor_node = parent_node ? 'null' : '#anchor';
if (if_exists_condition) {
block.chunks.mount.push(b`if (${if_exists_condition}) ${name}.m(${initial_mount_node}, ${anchor_node});`);
@@ -363,8 +363,8 @@ export default class IfBlockWrapper extends Wrapper {
snippet &&
(dependencies.length > 0
? b`if (${block.renderer.dirty(dependencies)}) ${condition} = !!${snippet}`
- : b`if (${condition} == -1) ${condition} = !!${snippet}`)
- }
+ : b`if (${condition} == null) ${condition} = !!${snippet}`
+ )}
if (${condition}) return ${i};`
: b`return ${i};`
)}
@@ -396,7 +396,7 @@ export default class IfBlockWrapper extends Wrapper {
}
const initial_mount_node = parent_node || '#target';
- const anchor_node = parent_node ? 'null' : 'anchor';
+ const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(
if_current_block_type_index(
@@ -488,7 +488,7 @@ export default class IfBlockWrapper extends Wrapper {
`);
const initial_mount_node = parent_node || '#target';
- const anchor_node = parent_node ? 'null' : 'anchor';
+ const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(b`if (${name}) ${name}.m(${initial_mount_node}, ${anchor_node});`);
diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
index ae4abcb036..b5b55d9a37 100644
--- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
@@ -400,7 +400,7 @@ export default class InlineComponentWrapper extends Wrapper {
block.chunks.mount.push(b`
if (${name}) {
- @mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});
+ @mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'});
}
`);
@@ -470,7 +470,7 @@ export default class InlineComponentWrapper extends Wrapper {
}
block.chunks.mount.push(
- b`@mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});`
+ b`@mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'});`
);
block.chunks.intro.push(b`
diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
index 3b13e6c68a..63889dd829 100644
--- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
+++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
@@ -54,7 +54,7 @@ export default class RawMustacheTagWrapper extends Tag {
const update_anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${init}, ${update_anchor});`);
- block.chunks.mount.push(b`${html_tag}.m(${parent_node || '#target'}, ${parent_node ? null : 'anchor'});`);
+ block.chunks.mount.push(b`${html_tag}.m(${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`);
if (needs_anchor) {
block.add_element(html_anchor, x`@empty()`, x`@empty()`, parent_node);
diff --git a/src/compiler/compile/render_dom/wrappers/Slot.ts b/src/compiler/compile/render_dom/wrappers/Slot.ts
index 16e2873f15..a5596c004e 100644
--- a/src/compiler/compile/render_dom/wrappers/Slot.ts
+++ b/src/compiler/compile/render_dom/wrappers/Slot.ts
@@ -130,7 +130,7 @@ export default class SlotWrapper extends Wrapper {
block.chunks.mount.push(b`
if (${slot_or_fallback}) {
- ${slot_or_fallback}.m(${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});
+ ${slot_or_fallback}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'});
}
`);
@@ -153,10 +153,7 @@ export default class SlotWrapper extends Wrapper {
const slot_update = b`
if (${slot}.p && ${renderer.dirty(dynamic_dependencies)}) {
- ${slot}.p(
- @get_slot_context(${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${get_slot_context_fn}),
- @get_slot_changes(${slot_definition}, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn})
- );
+ @update_slot(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_context_fn});
}
`;
const fallback_update =
diff --git a/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts b/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts
index 97199a71c2..8a9830b01b 100644
--- a/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts
+++ b/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts
@@ -55,6 +55,9 @@ export default function bind_this(component: Component, block: Block, binding: B
const args = [];
for (const id of contextual_dependencies) {
args.push(id);
+ if (block.variables.has(id.name)) {
+ if (block.renderer.context_lookup.get(id.name).is_contextual) continue;
+ }
block.add_variable(id, block.renderer.reference(id.name));
}
diff --git a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts
index d6bb86f1a4..6a62f872bb 100644
--- a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts
+++ b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts
@@ -14,7 +14,7 @@ export default function(node: AwaitBlock, renderer: Renderer, options: RenderOpt
renderer.add_expression(x`
function(__value) {
if (@is_promise(__value)) return ${pending};
- return (function(${node.value ? node.value.pattern : ''}) { return ${then}; }(__value));
+ return (function(${node.then_node ? node.then_node : ''}) { return ${then}; }(__value));
}(${node.expression.node})
`);
}
diff --git a/src/compiler/compile/utils/traverse_destructure_pattern.ts b/src/compiler/compile/utils/traverse_destructure_pattern.ts
deleted file mode 100644
index 6c918c28c0..0000000000
--- a/src/compiler/compile/utils/traverse_destructure_pattern.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Pattern, Identifier, RestElement } from "estree";
-import { Node } from "acorn";
-
-export default function traverse_destructure_pattern(
- node: Pattern,
- callback: (node: Identifier, parent: Node, key: string | number) => void
-) {
- function traverse(node: Pattern, parent, key) {
- switch (node.type) {
- case "Identifier":
- return callback(node, parent, key);
- case "ArrayPattern":
- for (let i = 0; i < node.elements.length; i++) {
- const element = node.elements[i];
- traverse(element, node.elements, i);
- }
- break;
- case "ObjectPattern":
- for (let i = 0; i < node.properties.length; i++) {
- const property = node.properties[i];
- if (property.type === "Property") {
- traverse(property.value, property, "value");
- } else {
- traverse((property as any) as RestElement, node.properties, i);
- }
- }
- break;
- case "RestElement":
- return traverse(node.argument, node, 'argument');
- case "AssignmentPattern":
- return traverse(node.left, node, 'left');
- }
- }
- traverse(node, null, null);
-}
diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts
index c21e6d6f79..a809eeebeb 100644
--- a/src/compiler/parse/index.ts
+++ b/src/compiler/parse/index.ts
@@ -5,9 +5,6 @@ import { reserved } from '../utils/names';
import full_char_code_at from '../utils/full_char_code_at';
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces';
import error from '../utils/error';
-import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from './utils/bracket';
-import { parse_expression_at } from './acorn';
-import { Pattern } from 'estree';
type ParserState = (parser: Parser) => (ParserState | void);
@@ -173,51 +170,6 @@ export class Parser {
return identifier;
}
- read_destructure_pattern(): Pattern {
- const start = this.index;
- let i = this.index;
-
- const code = full_char_code_at(this.template, i);
- if (isIdentifierStart(code, true)) {
- return { type: 'Identifier', name: this.read_identifier() };
- }
-
- if (!is_bracket_open(code)) {
- this.error({
- code: 'unexpected-token',
- message: 'Expected identifier or destructure pattern',
- });
- }
-
- const bracket_stack = [code];
- i += code <= 0xffff ? 1 : 2;
-
- while (i < this.template.length) {
- const code = full_char_code_at(this.template, i);
- if (is_bracket_open(code)) {
- bracket_stack.push(code);
- } else if (is_bracket_close(code)) {
- if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
- this.error({
- code: 'unexpected-token',
- message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}`
- });
- }
- bracket_stack.pop();
- if (bracket_stack.length === 0) {
- i += code <= 0xffff ? 1 : 2;
- break;
- }
- }
- i += code <= 0xffff ? 1 : 2;
- }
-
- this.index = i;
-
- const pattern_string = this.template.slice(start, i);
- return (parse_expression_at(`(${pattern_string} = 1)`, 0) as any).left as Pattern;
- }
-
read_until(pattern: RegExp) {
if (this.index >= this.template.length)
this.error({
diff --git a/src/compiler/parse/read/context.ts b/src/compiler/parse/read/context.ts
index fe666467f8..8d28bd2024 100644
--- a/src/compiler/parse/read/context.ts
+++ b/src/compiler/parse/read/context.ts
@@ -1,202 +1,82 @@
-import { Parser } from '../index';
-import { reserved } from '../../utils/names';
-
-interface Identifier {
- start: number;
- end: number;
- type: 'Identifier';
- name: string;
-}
-
-interface Property {
- start: number;
- end: number;
- type: 'Property';
- kind: 'init' | 'rest';
- shorthand: boolean;
- key: Identifier;
- value: Context;
-}
-
-interface Context {
- start: number;
- end: number;
- type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern' | 'RestIdentifier';
- name?: string;
- elements?: Context[];
- properties?: Property[];
-}
+import { Parser } from "../index";
+import { isIdentifierStart } from "acorn";
+import full_char_code_at from "../../utils/full_char_code_at";
+import {
+ is_bracket_open,
+ is_bracket_close,
+ is_bracket_pair,
+ get_bracket_close
+} from "../utils/bracket";
+import { parse_expression_at } from "../acorn";
+import { Pattern } from "estree";
+
+export default function read_context(
+ parser: Parser
+): Pattern & { start: number; end: number } {
+ const start = parser.index;
+ let i = parser.index;
+
+ const code = full_char_code_at(parser.template, i);
+ if (isIdentifierStart(code, true)) {
+ return {
+ type: "Identifier",
+ name: parser.read_identifier(),
+ start,
+ end: parser.index
+ };
+ }
-function error_on_assignment_pattern(parser: Parser) {
- if (parser.eat('=')) {
+ if (!is_bracket_open(code)) {
parser.error({
- code: 'invalid-assignment-pattern',
- message: 'Assignment patterns are not supported'
- }, parser.index - 1);
+ code: "unexpected-token",
+ message: "Expected identifier or destructure pattern"
+ });
}
-}
-
-function error_on_rest_pattern_not_last(parser: Parser) {
- parser.error({
- code: 'rest-pattern-not-last',
- message: 'Rest destructuring expected to be last'
- }, parser.index);
-}
-
-export default function read_context(parser: Parser) {
- const context: Context = {
- start: parser.index,
- end: null,
- type: null
- };
- if (parser.eat('[')) {
- context.type = 'ArrayPattern';
- context.elements = [];
-
- do {
- parser.allow_whitespace();
-
- const lastContext = context.elements[context.elements.length - 1];
- if (lastContext && lastContext.type === 'RestIdentifier') {
- error_on_rest_pattern_not_last(parser);
+ const bracket_stack = [code];
+ i += code <= 0xffff ? 1 : 2;
+
+ while (i < parser.template.length) {
+ const code = full_char_code_at(parser.template, i);
+ if (is_bracket_open(code)) {
+ bracket_stack.push(code);
+ } else if (is_bracket_close(code)) {
+ if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
+ parser.error({
+ code: "unexpected-token",
+ message: `Expected ${String.fromCharCode(
+ get_bracket_close(bracket_stack[bracket_stack.length - 1])
+ )}`
+ });
}
-
- if (parser.template[parser.index] === ',') {
- context.elements.push(null);
- } else {
- context.elements.push(read_context(parser));
- parser.allow_whitespace();
- }
- } while (parser.eat(','));
-
- error_on_assignment_pattern(parser);
- parser.eat(']', true);
- context.end = parser.index;
- }
-
- else if (parser.eat('{')) {
- context.type = 'ObjectPattern';
- context.properties = [];
-
- do {
- parser.allow_whitespace();
-
- if (parser.eat('...')) {
- parser.allow_whitespace();
-
- const start = parser.index;
- const name = parser.read_identifier();
- const key: Identifier = {
- start,
- end: parser.index,
- type: 'Identifier',
- name
- };
- const property: Property = {
- start,
- end: parser.index,
- type: 'Property',
- kind: 'rest',
- shorthand: true,
- key,
- value: key
- };
-
- context.properties.push(property);
-
- parser.allow_whitespace();
-
- if (parser.eat(',')) {
- parser.error({
- code: `comma-after-rest`,
- message: `Comma is not permitted after the rest element`
- }, parser.index - 1);
- }
-
+ bracket_stack.pop();
+ if (bracket_stack.length === 0) {
+ i += code <= 0xffff ? 1 : 2;
break;
}
-
- // TODO: DRY this out somehow
- // We don't know whether we want to allow reserved words until we see whether there's a ':' after it
- // Probably ideally we'd use Acorn to do all of this
- const start = parser.index;
- const name = parser.read_identifier(true);
- const key: Identifier = {
- start,
- end: parser.index,
- type: 'Identifier',
- name
- };
- parser.allow_whitespace();
-
- let value: Context;
- if (parser.eat(':')) {
- parser.allow_whitespace();
- value = read_context(parser);
- } else {
- if (reserved.has(name)) {
- parser.error({
- code: `unexpected-reserved-word`,
- message: `'${name}' is a reserved word in JavaScript and cannot be used here`
- }, start);
- }
- value = key;
- }
-
- const property: Property = {
- start,
- end: value.end,
- type: 'Property',
- kind: 'init',
- shorthand: value.type === 'Identifier' && value.name === name,
- key,
- value
- };
-
- context.properties.push(property);
-
- parser.allow_whitespace();
- } while (parser.eat(','));
-
- error_on_assignment_pattern(parser);
- parser.eat('}', true);
- context.end = parser.index;
- }
-
- else if (parser.eat('...')) {
- const name = parser.read_identifier();
- if (name) {
- context.type = 'RestIdentifier';
- context.end = parser.index;
- context.name = name;
- }
-
- else {
- parser.error({
- code: 'invalid-context',
- message: 'Expected a rest pattern'
- });
}
+ i += code <= 0xffff ? 1 : 2;
}
- else {
- const name = parser.read_identifier();
- if (name) {
- context.type = 'Identifier';
- context.end = parser.index;
- context.name = name;
- }
-
- else {
- parser.error({
- code: 'invalid-context',
- message: 'Expected a name, array pattern or object pattern'
- });
- }
-
- error_on_assignment_pattern(parser);
+ parser.index = i;
+
+ const pattern_string = parser.template.slice(start, i);
+ try {
+ // the length of the `space_with_newline` has to be start - 1
+ // because we added a `(` in front of the pattern_string,
+ // which shifted the entire string to right by 1
+ // so we offset it by removing 1 character in the `space_with_newline`
+ // to achieve that, we remove the 1st space encountered,
+ // so it will not affect the `column` of the node
+ let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
+ const first_space = space_with_newline.indexOf(' ');
+ space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
+
+ return (parse_expression_at(
+ `${space_with_newline}(${pattern_string} = 1)`,
+ start - 1
+ ) as any).left;
+ } catch (error) {
+ parser.acorn_error(error);
}
-
- return context;
}
diff --git a/src/compiler/parse/read/script.ts b/src/compiler/parse/read/script.ts
index 130c346ba1..8260894215 100644
--- a/src/compiler/parse/read/script.ts
+++ b/src/compiler/parse/read/script.ts
@@ -37,7 +37,8 @@ export default function read_script(parser: Parser, start: number, attributes: N
message: `
-
+
diff --git a/test/motion/index.ts b/test/motion/index.ts
index 3ac87be482..d4299629a4 100644
--- a/test/motion/index.ts
+++ b/test/motion/index.ts
@@ -19,5 +19,12 @@ describe('motion', () => {
size.set(100);
assert.equal(get(size), 100);
});
+
+ it('sets immediately when duration is 0', () => {
+ const size = tweened(0);
+
+ size.set(100, { duration : 0 });
+ assert.equal(get(size), 100);
+ });
});
});
diff --git a/test/parser/samples/await-catch/output.json b/test/parser/samples/await-catch/output.json
index 7232deaf13..744dbb6a05 100644
--- a/test/parser/samples/await-catch/output.json
+++ b/test/parser/samples/await-catch/output.json
@@ -26,6 +26,8 @@
},
"value": null,
"error": {
+ "start": 47,
+ "end": 55,
"type": "Identifier",
"name": "theError"
},
diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json
index 120faec7e0..49e07d6e80 100644
--- a/test/parser/samples/await-then-catch/output.json
+++ b/test/parser/samples/await-then-catch/output.json
@@ -25,10 +25,14 @@
"name": "thePromise"
},
"value": {
+ "start": 46,
+ "end": 54,
"type": "Identifier",
"name": "theValue"
},
"error": {
+ "start": 96,
+ "end": 104,
"type": "Identifier",
"name": "theError"
},
diff --git a/test/parser/samples/each-block-destructured/input.svelte b/test/parser/samples/each-block-destructured/input.svelte
index 09e9885be0..5313050fa0 100644
--- a/test/parser/samples/each-block-destructured/input.svelte
+++ b/test/parser/samples/each-block-destructured/input.svelte
@@ -1,3 +1,7 @@
+
+
{#each animals as [key, value, ...rest]}
{key}: {value}
{/each}
diff --git a/test/parser/samples/no-error-if-before-closing/output.json b/test/parser/samples/no-error-if-before-closing/output.json
index 15541c3d71..16cbc39250 100644
--- a/test/parser/samples/no-error-if-before-closing/output.json
+++ b/test/parser/samples/no-error-if-before-closing/output.json
@@ -116,6 +116,8 @@
"raw": "true"
},
"value": {
+ "start": 97,
+ "end": 98,
"type": "Identifier",
"name": "f"
},
@@ -202,6 +204,8 @@
"raw": "true"
},
"value": {
+ "start": 137,
+ "end": 138,
"type": "Identifier",
"name": "f"
},
diff --git a/test/runtime/samples/action-custom-event-handler-this/_config.js b/test/runtime/samples/action-custom-event-handler-this/_config.js
index 194fa08c31..88a30232ab 100644
--- a/test/runtime/samples/action-custom-event-handler-this/_config.js
+++ b/test/runtime/samples/action-custom-event-handler-this/_config.js
@@ -4,7 +4,7 @@ export default {
test({ assert, component, target, window }) {
const input = target.querySelector('input');
const event = new window.KeyboardEvent('keydown', {
- which: 13
+ key: 'Enter'
});
let blurred = false;
diff --git a/test/runtime/samples/action-custom-event-handler-this/main.svelte b/test/runtime/samples/action-custom-event-handler-this/main.svelte
index a7347cdc99..c3ace108a5 100644
--- a/test/runtime/samples/action-custom-event-handler-this/main.svelte
+++ b/test/runtime/samples/action-custom-event-handler-this/main.svelte
@@ -1,7 +1,7 @@
-
\ No newline at end of file
+
diff --git a/test/runtime/samples/binding-audio-currenttime-duration-volume/_config.js b/test/runtime/samples/binding-audio-currenttime-duration-volume/_config.js
index d4c2b7cd0f..158536b5fb 100644
--- a/test/runtime/samples/binding-audio-currenttime-duration-volume/_config.js
+++ b/test/runtime/samples/binding-audio-currenttime-duration-volume/_config.js
@@ -8,6 +8,7 @@ export default {
assert.equal(component.t, 0);
assert.equal(component.d, 0);
assert.equal(component.v, 0.5);
+ assert.equal(component.m, true);
assert.equal(component.r, 1);
assert.equal(component.paused, true);
@@ -20,6 +21,7 @@ export default {
audio.currentTime = 10;
audio.duration = 20;
audio.volume = 0.75;
+ audio.muted = false;
audio.playbackRate = 2;
audio.dispatchEvent(timeupdate);
audio.dispatchEvent(durationchange);
@@ -30,6 +32,7 @@ export default {
assert.equal(component.t, 10);
assert.equal(component.d, 0); // not 20, because read-only. Not sure how to test this!
assert.equal(component.v, 0.75);
+ assert.equal(component.m, false);
assert.equal(component.r, 2);
assert.equal(component.paused, true); // ditto...
}
diff --git a/test/runtime/samples/binding-audio-currenttime-duration-volume/main.svelte b/test/runtime/samples/binding-audio-currenttime-duration-volume/main.svelte
index 69335182e8..e232c920bf 100644
--- a/test/runtime/samples/binding-audio-currenttime-duration-volume/main.svelte
+++ b/test/runtime/samples/binding-audio-currenttime-duration-volume/main.svelte
@@ -4,7 +4,8 @@
export let paused;
export let v;
export let r;
+ export let m;
-
\ No newline at end of file
diff --git a/test/runtime/samples/binding-input-text-undefined/_config.js b/test/runtime/samples/binding-input-text-undefined/_config.js
new file mode 100644
index 0000000000..009e1b37ba
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-undefined/_config.js
@@ -0,0 +1,30 @@
+export default {
+ html: `
+
+ `,
+
+ ssrHtml: `
+
+ `,
+
+ async test({ assert, component, target, window }) {
+ const input = target.querySelector('input');
+ assert.equal(input.value, '');
+
+ component.x = null;
+ assert.equal(input.value, '');
+
+ component.x = undefined;
+ assert.equal(input.value, '');
+
+ component.x = 'string';
+ component.x = undefined;
+ assert.equal(input.value, '');
+
+ component.x = 0;
+ assert.equal(input.value, '0');
+
+ component.x = undefined;
+ assert.equal(input.value, '');
+ },
+};
diff --git a/test/runtime/samples/binding-input-text-undefined/main.svelte b/test/runtime/samples/binding-input-text-undefined/main.svelte
new file mode 100644
index 0000000000..b4c2a84fd2
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-undefined/main.svelte
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/test/runtime/samples/binding-this-each-object-props/_config.js b/test/runtime/samples/binding-this-each-object-props/_config.js
new file mode 100644
index 0000000000..5372667b82
--- /dev/null
+++ b/test/runtime/samples/binding-this-each-object-props/_config.js
@@ -0,0 +1,14 @@
+export default {
+ html: ``,
+
+ async test({ assert, component, target }) {
+ component.visible = true;
+ assert.htmlEqual(target.innerHTML, `
+ b
b
c
c
+ `);
+ assert.equal(component.items1[1], target.querySelector('div'));
+ assert.equal(component.items2[1], target.querySelector('div:nth-child(2)'));
+ assert.equal(component.items1[2], target.querySelector('div:nth-child(3)'));
+ assert.equal(component.items2[2], target.querySelector('div:last-child'));
+ }
+};
diff --git a/test/runtime/samples/binding-this-each-object-props/main.svelte b/test/runtime/samples/binding-this-each-object-props/main.svelte
new file mode 100644
index 0000000000..9654a58418
--- /dev/null
+++ b/test/runtime/samples/binding-this-each-object-props/main.svelte
@@ -0,0 +1,13 @@
+
+
+{#each data as item (item.id)}
+ {item.text}
+ {item.text}
+{/each}
diff --git a/test/runtime/samples/binding-this-each-object-spread/_config.js b/test/runtime/samples/binding-this-each-object-spread/_config.js
new file mode 100644
index 0000000000..cecec08db9
--- /dev/null
+++ b/test/runtime/samples/binding-this-each-object-spread/_config.js
@@ -0,0 +1,14 @@
+export default {
+ html: ``,
+
+ async test({ assert, component, target }) {
+ component.visible = true;
+ assert.htmlEqual(target.innerHTML, `
+ a
a
b
b
+ `);
+ assert.equal(component.items1[1], target.querySelector('div'));
+ assert.equal(component.items2[1], target.querySelector('div:nth-child(2)'));
+ assert.equal(component.items1[2], target.querySelector('div:nth-child(3)'));
+ assert.equal(component.items2[2], target.querySelector('div:last-child'));
+ }
+};
diff --git a/test/runtime/samples/binding-this-each-object-spread/main.svelte b/test/runtime/samples/binding-this-each-object-spread/main.svelte
new file mode 100644
index 0000000000..256ed0ede6
--- /dev/null
+++ b/test/runtime/samples/binding-this-each-object-spread/main.svelte
@@ -0,0 +1,13 @@
+
+
+{#each data as {id, text} (id)}
+ {text}
+ {text}
+{/each}
diff --git a/test/runtime/samples/bitmask-overflow-slot-6/Slotted.svelte b/test/runtime/samples/bitmask-overflow-slot-6/Slotted.svelte
new file mode 100644
index 0000000000..322a31359e
--- /dev/null
+++ b/test/runtime/samples/bitmask-overflow-slot-6/Slotted.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ {#if open}
+
+ {/if}
+
diff --git a/test/runtime/samples/bitmask-overflow-slot-6/_config.js b/test/runtime/samples/bitmask-overflow-slot-6/_config.js
new file mode 100644
index 0000000000..8cd8c07a65
--- /dev/null
+++ b/test/runtime/samples/bitmask-overflow-slot-6/_config.js
@@ -0,0 +1,32 @@
+// overflow bitmask + slot missing `let:`
+export default {
+ html: `
+
+ Toggle inside 1
+
+ Toggle outside
+ `,
+
+ async test({ assert, component, target, window }) {
+ const button = target.querySelectorAll('button')[1];
+ const div = target.querySelector('div');
+ await div.dispatchEvent(new window.MouseEvent('click'));
+
+ assert.htmlEqual(target.innerHTML, `
+
+
Toggle inside 1
+
Open
+
+ Toggle outside
+ `);
+
+ await button.dispatchEvent(new window.MouseEvent('click'));
+ assert.htmlEqual(target.innerHTML, `
+
+
Toggle inside 2
+
Open
+
+ Toggle outside
+ `);
+ }
+};
\ No newline at end of file
diff --git a/test/runtime/samples/bitmask-overflow-slot-6/main.svelte b/test/runtime/samples/bitmask-overflow-slot-6/main.svelte
new file mode 100644
index 0000000000..8845cb2ef5
--- /dev/null
+++ b/test/runtime/samples/bitmask-overflow-slot-6/main.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+ Toggle inside {last}
+
+
+
+ Open
+
+
+
+Toggle outside
\ No newline at end of file
diff --git a/test/runtime/samples/deconflict-anchor/Anchor.svelte b/test/runtime/samples/deconflict-anchor/Anchor.svelte
new file mode 100644
index 0000000000..e758d60056
--- /dev/null
+++ b/test/runtime/samples/deconflict-anchor/Anchor.svelte
@@ -0,0 +1 @@
+Anchor
diff --git a/test/runtime/samples/deconflict-anchor/_config.js b/test/runtime/samples/deconflict-anchor/_config.js
new file mode 100644
index 0000000000..817dd42fcb
--- /dev/null
+++ b/test/runtime/samples/deconflict-anchor/_config.js
@@ -0,0 +1,4 @@
+export default {
+ preserveIdentifiers: true,
+ html: `Anchor
`
+};
diff --git a/test/runtime/samples/deconflict-anchor/main.svelte b/test/runtime/samples/deconflict-anchor/main.svelte
new file mode 100644
index 0000000000..cce6281e49
--- /dev/null
+++ b/test/runtime/samples/deconflict-anchor/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/runtime/samples/each-block-destructured-default/_config.js b/test/runtime/samples/each-block-destructured-default/_config.js
new file mode 100644
index 0000000000..133fd68532
--- /dev/null
+++ b/test/runtime/samples/each-block-destructured-default/_config.js
@@ -0,0 +1,22 @@
+export default {
+ props: {
+ animalEntries: [
+ { animal: 'raccoon', class: 'mammal', species: 'P. lotor', kilogram: 25 },
+ { animal: 'eagle', class: 'bird', kilogram: 5.4 }
+ ]
+ },
+
+ html: `
+ raccoon - P. lotor - 25kg
+ eagle - unknown - 5.4kg
+ `,
+
+
+
+ test({ assert, component, target }) {
+ component.animalEntries = [{ animal: 'cow', class: 'mammal', species: 'B. taurus' }];
+ assert.htmlEqual(target.innerHTML, `
+ cow - B. taurus - 50kg
+ `);
+ },
+};
diff --git a/test/runtime/samples/each-block-destructured-default/main.svelte b/test/runtime/samples/each-block-destructured-default/main.svelte
new file mode 100644
index 0000000000..a91b45299e
--- /dev/null
+++ b/test/runtime/samples/each-block-destructured-default/main.svelte
@@ -0,0 +1,7 @@
+
+
+{#each animalEntries as { animal, species = 'unknown', kilogram: weight = 50 , ...props } }
+ {animal} - {species} - {weight}kg
+{/each}
diff --git a/test/runtime/samples/if-block-static-with-elseif-else-and-outros/RRR.svelte b/test/runtime/samples/if-block-static-with-elseif-else-and-outros/RRR.svelte
new file mode 100644
index 0000000000..7242373249
--- /dev/null
+++ b/test/runtime/samples/if-block-static-with-elseif-else-and-outros/RRR.svelte
@@ -0,0 +1 @@
+rrr
\ No newline at end of file
diff --git a/test/runtime/samples/if-block-static-with-elseif-else-and-outros/_config.js b/test/runtime/samples/if-block-static-with-elseif-else-and-outros/_config.js
new file mode 100644
index 0000000000..8b2c6d2d66
--- /dev/null
+++ b/test/runtime/samples/if-block-static-with-elseif-else-and-outros/_config.js
@@ -0,0 +1,3 @@
+export default {
+ html: 'eee'
+};
diff --git a/test/runtime/samples/if-block-static-with-elseif-else-and-outros/main.svelte b/test/runtime/samples/if-block-static-with-elseif-else-and-outros/main.svelte
new file mode 100644
index 0000000000..b60885722e
--- /dev/null
+++ b/test/runtime/samples/if-block-static-with-elseif-else-and-outros/main.svelte
@@ -0,0 +1,13 @@
+
+
+{#if "Eva".startsWith('E')}
+ eee
+{:else if x}
+ def
+{:else}
+
+{/if}
diff --git a/test/runtime/samples/loop-protect-generator-opt-out/_config.js b/test/runtime/samples/loop-protect-generator-opt-out/_config.js
new file mode 100644
index 0000000000..9b6a24b513
--- /dev/null
+++ b/test/runtime/samples/loop-protect-generator-opt-out/_config.js
@@ -0,0 +1,6 @@
+export default {
+ compileOptions: {
+ dev: true,
+ loopGuardTimeout: 1,
+ },
+};
diff --git a/test/runtime/samples/loop-protect-generator-opt-out/main.svelte b/test/runtime/samples/loop-protect-generator-opt-out/main.svelte
new file mode 100644
index 0000000000..a7aa790681
--- /dev/null
+++ b/test/runtime/samples/loop-protect-generator-opt-out/main.svelte
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/test/runtime/samples/spread-element-input-bind-group-with-value-attr/_config.js b/test/runtime/samples/spread-element-input-bind-group-with-value-attr/_config.js
new file mode 100644
index 0000000000..6e1b890351
--- /dev/null
+++ b/test/runtime/samples/spread-element-input-bind-group-with-value-attr/_config.js
@@ -0,0 +1,15 @@
+export default {
+ props: {
+ props: {
+ 'data-foo': 'bar'
+ }
+ },
+
+ html: ` `,
+
+ async test({ assert, component, target, window }) {
+ const input = target.querySelector('input');
+ assert.equal(input.value, 'abc');
+ assert.equal(input.__value, 'abc');
+ }
+};
diff --git a/test/runtime/samples/spread-element-input-bind-group-with-value-attr/main.svelte b/test/runtime/samples/spread-element-input-bind-group-with-value-attr/main.svelte
new file mode 100644
index 0000000000..ce6b76f093
--- /dev/null
+++ b/test/runtime/samples/spread-element-input-bind-group-with-value-attr/main.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/test/runtime/samples/transition-js-promise-outro/_config.js b/test/runtime/samples/transition-js-promise-outro/_config.js
new file mode 100644
index 0000000000..eba6971476
--- /dev/null
+++ b/test/runtime/samples/transition-js-promise-outro/_config.js
@@ -0,0 +1,14 @@
+export default {
+ props: {
+ visible: true,
+ },
+
+ test({ assert, component, target }) {
+ component.visible = false;
+ assert.notEqual(target.querySelector('span'), undefined);
+ component.resolve();
+ setTimeout(() => {
+ assert.equal(target.querySelector('span'), undefined);
+ });
+ },
+};
diff --git a/test/runtime/samples/transition-js-promise-outro/main.svelte b/test/runtime/samples/transition-js-promise-outro/main.svelte
new file mode 100644
index 0000000000..80b6cc6f2d
--- /dev/null
+++ b/test/runtime/samples/transition-js-promise-outro/main.svelte
@@ -0,0 +1,14 @@
+
+
+{#if visible}
+ hello
+{/if}
diff --git a/test/runtime/samples/window-event-custom/_config.js b/test/runtime/samples/window-event-custom/_config.js
index 207fad1b0b..1c1075c0de 100644
--- a/test/runtime/samples/window-event-custom/_config.js
+++ b/test/runtime/samples/window-event-custom/_config.js
@@ -3,7 +3,7 @@ export default {
async test({ assert, component, target, window }) {
const event = new window.KeyboardEvent('keydown', {
- which: 27
+ key: 'Escape'
});
await window.dispatchEvent(event);
diff --git a/test/runtime/samples/window-event-custom/main.svelte b/test/runtime/samples/window-event-custom/main.svelte
index ce453d9334..ff286f4309 100644
--- a/test/runtime/samples/window-event-custom/main.svelte
+++ b/test/runtime/samples/window-event-custom/main.svelte
@@ -3,7 +3,7 @@
function esc(node, callback) {
function onKeyDown(event) {
- if (event.which === 27) callback(event);
+ if (event.key === 'Escape') callback(event);
}
node.addEventListener('keydown', onKeyDown);
return {
@@ -16,4 +16,4 @@
-escaped: {escaped}
\ No newline at end of file
+escaped: {escaped}
diff --git a/test/sourcemaps/samples/script-after-comment/input.svelte b/test/sourcemaps/samples/script-after-comment/input.svelte
new file mode 100644
index 0000000000..1f06455678
--- /dev/null
+++ b/test/sourcemaps/samples/script-after-comment/input.svelte
@@ -0,0 +1,9 @@
+
+
+
+{foo.bar.baz}
diff --git a/test/sourcemaps/samples/script-after-comment/test.js b/test/sourcemaps/samples/script-after-comment/test.js
new file mode 100644
index 0000000000..b473970112
--- /dev/null
+++ b/test/sourcemaps/samples/script-after-comment/test.js
@@ -0,0 +1,16 @@
+export function test({ assert, smc, locateInSource, locateInGenerated }) {
+ const expected = locateInSource( 'assertThisLine' );
+ const start = locateInGenerated( 'assertThisLine' );
+
+ const actual = smc.originalPositionFor({
+ line: start.line + 1,
+ column: start.column
+ });
+
+ assert.deepEqual( actual, {
+ source: 'input.svelte',
+ name: null,
+ line: expected.line + 1,
+ column: expected.column
+ });
+}
diff --git a/test/sourcemaps/samples/two-scripts/input.svelte b/test/sourcemaps/samples/two-scripts/input.svelte
new file mode 100644
index 0000000000..4c869827c0
--- /dev/null
+++ b/test/sourcemaps/samples/two-scripts/input.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+{foo.bar.baz}
diff --git a/test/sourcemaps/samples/two-scripts/test.js b/test/sourcemaps/samples/two-scripts/test.js
new file mode 100644
index 0000000000..b473970112
--- /dev/null
+++ b/test/sourcemaps/samples/two-scripts/test.js
@@ -0,0 +1,16 @@
+export function test({ assert, smc, locateInSource, locateInGenerated }) {
+ const expected = locateInSource( 'assertThisLine' );
+ const start = locateInGenerated( 'assertThisLine' );
+
+ const actual = smc.originalPositionFor({
+ line: start.line + 1,
+ column: start.column
+ });
+
+ assert.deepEqual( actual, {
+ source: 'input.svelte',
+ name: null,
+ line: expected.line + 1,
+ column: expected.column
+ });
+}
diff --git a/test/validator/samples/a11y-anchor-is-valid/input.svelte b/test/validator/samples/a11y-anchor-is-valid/input.svelte
index 7b14a80c9f..8933bd7560 100644
--- a/test/validator/samples/a11y-anchor-is-valid/input.svelte
+++ b/test/validator/samples/a11y-anchor-is-valid/input.svelte
@@ -1,3 +1,8 @@
not actually a link
invalid
-invalid
\ No newline at end of file
+invalid
+invalid
+invalid
+invalid
+valid
+valid
\ No newline at end of file
diff --git a/test/validator/samples/a11y-anchor-is-valid/warnings.json b/test/validator/samples/a11y-anchor-is-valid/warnings.json
index 532c44ad24..f04c6f1593 100644
--- a/test/validator/samples/a11y-anchor-is-valid/warnings.json
+++ b/test/validator/samples/a11y-anchor-is-valid/warnings.json
@@ -43,5 +43,50 @@
"character": 61
},
"pos": 53
+ },
+ {
+ "code": "a11y-invalid-attribute",
+ "message": "A11y: 'javascript:void(0)' is not a valid href attribute",
+ "start": {
+ "line": 4,
+ "column": 3,
+ "character": 77
+ },
+ "end": {
+ "line": 4,
+ "column": 28,
+ "character": 102
+ },
+ "pos": 77
+ },
+ {
+ "code": "a11y-missing-attribute",
+ "message": "A11y: element should have an href attribute",
+ "start": {
+ "line": 5,
+ "column": 0,
+ "character": 115
+ },
+ "end": {
+ "line": 5,
+ "column": 22,
+ "character": 137
+ },
+ "pos": 115
+ },
+ {
+ "code": "a11y-missing-attribute",
+ "message": "A11y: element should have an href attribute",
+ "start": {
+ "line": 6,
+ "column": 0,
+ "character": 138
+ },
+ "end": {
+ "line": 6,
+ "column": 20,
+ "character": 158
+ },
+ "pos": 138
}
]
diff --git a/test/validator/samples/a11y-figcaption-in-non-element-block/input.svelte b/test/validator/samples/a11y-figcaption-in-non-element-block/input.svelte
index 9d4b6ded4d..33e7a891f0 100644
--- a/test/validator/samples/a11y-figcaption-in-non-element-block/input.svelte
+++ b/test/validator/samples/a11y-figcaption-in-non-element-block/input.svelte
@@ -3,7 +3,7 @@
-
+
{#if caption}
{caption}
{/if}
diff --git a/test/validator/samples/a11y-figcaption-right-place/input.svelte b/test/validator/samples/a11y-figcaption-right-place/input.svelte
index 808dbee941..783219c8f8 100644
--- a/test/validator/samples/a11y-figcaption-right-place/input.svelte
+++ b/test/validator/samples/a11y-figcaption-right-place/input.svelte
@@ -1,5 +1,5 @@
-
+
a foo in its natural habitat
diff --git a/test/validator/samples/a11y-figcaption-wrong-place/input.svelte b/test/validator/samples/a11y-figcaption-wrong-place/input.svelte
index ffa7dde65d..e99d1cc86c 100644
--- a/test/validator/samples/a11y-figcaption-wrong-place/input.svelte
+++ b/test/validator/samples/a11y-figcaption-wrong-place/input.svelte
@@ -1,5 +1,5 @@
-
+
a foo in its natural habitat
@@ -9,7 +9,7 @@
-
+
diff --git a/test/validator/samples/a11y-figcaption-wrong-place/warnings.json b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json
index ee5a89d3ff..eba5b6f31e 100644
--- a/test/validator/samples/a11y-figcaption-wrong-place/warnings.json
+++ b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json
@@ -5,14 +5,14 @@
"start": {
"line": 4,
"column": 1,
- "character": 57
+ "character": 44
},
"end": {
"line": 6,
"column": 14,
- "character": 115
+ "character": 102
},
- "pos": 57
+ "pos": 44
},
{
"code": "a11y-structure",
@@ -20,13 +20,13 @@
"start": {
"line": 15,
"column": 2,
- "character": 252
+ "character": 226
},
"end": {
"line": 17,
"column": 15,
- "character": 328
+ "character": 302
},
- "pos": 252
+ "pos": 226
}
]
diff --git a/test/validator/samples/a11y-img-redundant-alt/input.svelte b/test/validator/samples/a11y-img-redundant-alt/input.svelte
new file mode 100644
index 0000000000..a3528a5c41
--- /dev/null
+++ b/test/validator/samples/a11y-img-redundant-alt/input.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/validator/samples/a11y-img-redundant-alt/warnings.json b/test/validator/samples/a11y-img-redundant-alt/warnings.json
new file mode 100644
index 0000000000..44106c23d0
--- /dev/null
+++ b/test/validator/samples/a11y-img-redundant-alt/warnings.json
@@ -0,0 +1,47 @@
+[
+ {
+ "code": "a11y-img-redundant-alt",
+ "message": "A11y: Screenreaders already announce elements as an image.",
+ "end": {
+ "character": 173,
+ "column": 49,
+ "line": 3
+ },
+ "start": {
+ "character": 124,
+ "column": 0,
+ "line": 3
+ },
+ "pos": 124
+ },
+ {
+ "code": "a11y-img-redundant-alt",
+ "message": "A11y: Screenreaders already announce elements as an image.",
+ "end": {
+ "character": 219,
+ "column": 45,
+ "line": 4
+ },
+ "start": {
+ "character": 174,
+ "column": 0,
+ "line": 4
+ },
+ "pos": 174
+ },
+ {
+ "code": "a11y-img-redundant-alt",
+ "message": "A11y: Screenreaders already announce elements as an image.",
+ "end": {
+ "character": 272,
+ "column": 52,
+ "line": 5
+ },
+ "start": {
+ "character": 220,
+ "column": 0,
+ "line": 5
+ },
+ "pos": 220
+ }
+]
\ No newline at end of file
diff --git a/test/validator/samples/a11y-no-onchange/input.svelte b/test/validator/samples/a11y-no-onchange/input.svelte
new file mode 100644
index 0000000000..af2041f406
--- /dev/null
+++ b/test/validator/samples/a11y-no-onchange/input.svelte
@@ -0,0 +1,16 @@
+ {}}>
+ foo
+ bar
+
+ {}} on:blur={e => {}}>
+ foo
+ bar
+
+
+ {}}>foo
+ bar
+
+
+ {}} on:blur={e => {}}>foo
+ bar
+
\ No newline at end of file
diff --git a/test/validator/samples/a11y-no-onchange/warnings.json b/test/validator/samples/a11y-no-onchange/warnings.json
new file mode 100644
index 0000000000..461f546c0b
--- /dev/null
+++ b/test/validator/samples/a11y-no-onchange/warnings.json
@@ -0,0 +1,32 @@
+[
+ {
+ "code": "a11y-no-onchange",
+ "end": {
+ "character": 88,
+ "column": 9,
+ "line": 4
+ },
+ "message": "A11y: on:blur must be used instead of on:change, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users.",
+ "pos": 0,
+ "start": {
+ "character": 0,
+ "column": 0,
+ "line": 1
+ }
+ },
+ {
+ "code": "a11y-no-onchange",
+ "end": {
+ "character": 249,
+ "column": 44,
+ "line": 10
+ },
+ "message": "A11y: on:blur must be used instead of on:change, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users.",
+ "pos": 209,
+ "start": {
+ "character": 209,
+ "column": 4,
+ "line": 10
+ }
+ }
+]
diff --git a/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json b/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json
index 549b6960eb..df899b7702 100644
--- a/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json
+++ b/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json
@@ -1,5 +1,5 @@
[{
- "code": "comma-after-rest",
+ "code": "parse-error",
"message": "Comma is not permitted after the rest element",
"pos": 100,
"start": {
diff --git a/test/validator/samples/each-block-invalid-context-destructured-object/errors.json b/test/validator/samples/each-block-invalid-context-destructured-object/errors.json
index 085021ff5a..c96e3d2c8c 100644
--- a/test/validator/samples/each-block-invalid-context-destructured-object/errors.json
+++ b/test/validator/samples/each-block-invalid-context-destructured-object/errors.json
@@ -1,6 +1,6 @@
[{
- "code": "unexpected-reserved-word",
- "message": "'case' is a reserved word in JavaScript and cannot be used here",
+ "code": "parse-error",
+ "message": "Unexpected keyword 'case'",
"start": {
"line": 1,
"column": 18,
diff --git a/test/validator/samples/each-block-invalid-context-destructured/errors.json b/test/validator/samples/each-block-invalid-context-destructured/errors.json
index afe99ee219..62d6f62e87 100644
--- a/test/validator/samples/each-block-invalid-context-destructured/errors.json
+++ b/test/validator/samples/each-block-invalid-context-destructured/errors.json
@@ -1,6 +1,6 @@
[{
- "code": "unexpected-reserved-word",
- "message": "'case' is a reserved word in JavaScript and cannot be used here",
+ "code": "parse-error",
+ "message": "Unexpected token",
"start": {
"line": 1,
"column": 17,