Merge branch 'master' into gh-2612

pull/2963/head
Rich Harris 6 years ago committed by GitHub
commit b801c67403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,12 @@
# Svelte changelog
## 3.5.4
* Preserve whitespace at the boundaries of `{#each}` blocks ([#713](https://github.com/sveltejs/svelte/issues/713))
* Fix dynamic `bind:this` on components ([#2333](https://github.com/sveltejs/svelte/issues/2333))
* Fix binding to values in a component when it uses `$$props` ([#2725](https://github.com/sveltejs/svelte/issues/2725))
* Fix parsing ambiguous HTML entities ([#3071](https://github.com/sveltejs/svelte/pull/3071))
## 3.5.3
* Don't double-destroy keyed each blocks with outros ([#3055](https://github.com/sveltejs/svelte/issues/3055))

2
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.5.3",
"version": "3.5.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.5.3",
"version": "3.5.4",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",

@ -4,7 +4,10 @@ Set up the project:
```bash
git clone https://github.com/sveltejs/svelte.git
cd svelte/site
cd svelte
npm ci
PUBLISH=1 npm run build
cd site
npm ci
npm run update
```
@ -17,7 +20,7 @@ By default, the REPL will fetch the most recent version of Svelte from https://u
To produce the proper browser-compatible UMD build of the compiler, you will need to run `npm run build` (or `npm run dev`) in the root of this repository with the `PUBLISH` environment variable set to any non-empty string.
Then visit the REPL at [localhost:3000/repl?version=local](http://localhost:3000/repl?version=local).
Then visit the REPL at [localhost:3000/repl?version=local](http://localhost:3000/repl?version=local). Please note that the local REPL only works with `npm run dev` and not when building the site for production usage.
## REPL GitHub integration
@ -32,6 +35,13 @@ In order for the REPL's GitHub integration to work properly when running locally
GITHUB_CLIENT_SECRET=[your app's Client Secret]
BASEURL=http://localhost:3000
```
## Building the site
To build the website, run `npm run sapper`. The output can be found in `__sapper__/build`.
## Testing
Tests can be run using `npm run test`.
## Translating the API docs

@ -188,12 +188,20 @@ If a *key* expression is provided — which must uniquely identify each list ite
---
You can freely use destructuring patterns in each blocks.
You can freely use destructuring and rest patterns in each blocks.
```html
{#each items as { id, name, qty }, i (id)}
<li>{i + 1}: {name} x {qty}</li>
{/each}
{#each objects as { id, ...rest }}
<li><span>{id}</span><MyComponent {...rest}/></li>
{/each}
{#each items as [id, ...rest]}
<li><span>{id}</span><MyComponent values={rest}/></li>
{/each}
```
---
@ -502,6 +510,14 @@ When the value of an `<option>` matches its text content, the attribute can be o
</select>
```
---
Elements with the `contenteditable` attribute support `innerHTML` and `textContent` bindings.
```html
<div contenteditable="true" bind:innerHTML={html}></div>
```
##### Media element bindings
---
@ -1241,7 +1257,7 @@ It cannot appear at the top level of your markup; it must be inside an if or eac
### `<svelte:component>`
```sv
<svelte:component this={expression}>
<svelte:component this={expression}/>
```
---
@ -1318,7 +1334,7 @@ As with `<svelte:window>`, this element allows you to add listeners to events on
### `<svelte:head>`
```sv
<svelte:head>
<svelte:head>...</svelte:head>
```
---
@ -1335,7 +1351,7 @@ This element makes it possible to insert elements into `document.head`. During s
### `<svelte:options>`
```sv
<svelte:options option={value}>
<svelte:options option={value}/>
```
---
@ -1351,4 +1367,4 @@ The `<svelte:options>` element provides a place to specify per-component compile
```html
<svelte:options tag="my-custom-element"/>
```
```

@ -0,0 +1,15 @@
<script>
let html = '<p>Write some text!</p>';
</script>
<div contenteditable="true"></div>
<pre>{html}</pre>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

@ -0,0 +1,18 @@
<script>
let html = '<p>Write some text!</p>';
</script>
<div
contenteditable="true"
bind:innerHTML={html}
></div>
<pre>{html}</pre>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

@ -0,0 +1,12 @@
---
title: Contenteditable bindings
---
Elements with a `contenteditable="true"` attribute support `textContent` and `innerHTML` bindings:
```html
<div
contenteditable="true"
bind:innerHTML={html}
></div>
```

@ -52,6 +52,8 @@
<a target="_blank" rel="noopener" href="https://itslearning.com"><img src="organisations/itslearning.svg" alt="itslearning logo"></a>
<a target="_blank" rel="noopener" href="http://mustlab.ru"><img src="organisations/mustlab.png" alt="Mustlab logo"></a>
<a target="_blank" rel="noopener" href="https://www.nesta.org.uk"><img src="organisations/nesta.svg" alt="Nesta logo"></a>
<a target="_blank" rel="noopener" href="https://www.nonkositelecoms.com"><img src="organisations/nonkosi.svg" alt="Nonkosi Telecoms logo"></a>
<a target="_blank" rel="noopener" href="https://www.nzz.ch"><img src="organisations/nzz.svg" alt="Neue Zürcher Zeitung logo"></a>
<a target="_blank" rel="noopener" href="https://nytimes.com"><img src="organisations/nyt.svg" alt="The New York Times logo"></a>
<a target="_blank" rel="noopener" href="https://openstate.eu"><img src="organisations/open-state-foundation.svg" alt="Open State Foundation logo"></a>
<a target="_blank" rel="noopener" href="https://razorpay.com"><img src="organisations/razorpay.svg" alt="Razorpay logo"></a>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

@ -608,8 +608,8 @@ export default class Element extends Node {
});
}
} else if (
name === 'text' ||
name === 'html'
name === 'textContent' ||
name === 'innerHTML'
) {
const contenteditable = this.attributes.find(
(attribute: Attribute) => attribute.name === 'contenteditable'
@ -618,7 +618,7 @@ export default class Element extends Node {
if (!contenteditable) {
component.error(binding, {
code: `missing-contenteditable-attribute`,
message: `'contenteditable' attribute is required for text and html two-way bindings`
message: `'contenteditable' attribute is required for textContent and innerHTML two-way bindings`
});
} else if (contenteditable && !contenteditable.is_static) {
component.error(contenteditable, {

@ -82,7 +82,7 @@ export default function dom(
${$$props} => {
${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)}
${writable_props.map(prop =>
`if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};`
`if ('${prop.export_name}' in ${$$props}) ${component.invalidate(prop.name, `${prop.name} = ${$$props}.${prop.export_name}`)};`
)}
${component.slots.size > 0 &&
`if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`}

@ -133,6 +133,14 @@ export default class BindingWrapper {
break;
}
case 'textContent':
update_conditions.push(`${this.snippet} !== ${parent.var}.textContent`);
break;
case 'innerHTML':
update_conditions.push(`${this.snippet} !== ${parent.var}.innerHTML`);
break;
case 'currentTime':
case 'playbackRate':
case 'volume':
@ -162,7 +170,9 @@ export default class BindingWrapper {
);
}
if (!/(currentTime|paused)/.test(this.node.name)) {
if (this.node.name === 'innerHTML' || this.node.name === 'textContent') {
block.builders.mount.add_block(`if (${this.snippet} !== void 0) ${update_dom}`);
} else if (!/(currentTime|paused)/.test(this.node.name)) {
block.builders.mount.add_block(update_dom);
}
}
@ -198,14 +208,6 @@ function get_dom_updater(
return `${element.var}.checked = ${condition};`;
}
if (binding.node.name === 'text') {
return `if (${binding.snippet} !== ${element.var}.textContent) ${element.var}.textContent = ${binding.snippet};`;
}
if (binding.node.name === 'html') {
return `if (${binding.snippet} !== ${element.var}.innerHTML) ${element.var}.innerHTML = ${binding.snippet};`;
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`;
}
@ -318,14 +320,6 @@ function get_value_from_dom(
return `@time_ranges_to_array(this.${name})`;
}
if (name === 'text') {
return `this.textContent`;
}
if (name === 'html') {
return `this.innerHTML`;
}
// everything else
return `this.${name}`;
}

@ -30,7 +30,7 @@ const events = [
{
event_names: ['input'],
filter: (node: Element, name: string) =>
(name === 'text' || name === 'html') &&
(name === 'textContent' || name === 'innerHTML') &&
node.attributes.some(attribute => attribute.name === 'contenteditable')
},
{
@ -510,7 +510,19 @@ export default class ElementWrapper extends Wrapper {
.map(binding => `${binding.snippet} === void 0`)
.join(' || ');
if (this.node.name === 'select' || group.bindings.find(binding => binding.node.name === 'indeterminate' || binding.is_readonly_media_attribute())) {
const should_initialise = (
this.node.name === 'select' ||
group.bindings.find(binding => {
return (
binding.node.name === 'indeterminate' ||
binding.node.name === 'textContent' ||
binding.node.name === 'innerHTML' ||
binding.is_readonly_media_attribute()
);
})
);
if (should_initialise) {
const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`;
block.builders.hydrate.add_line(
`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`

@ -276,15 +276,17 @@ export default class InlineComponentWrapper extends Wrapper {
lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim();
}
const contextual_dependencies = [...binding.expression.contextual_dependencies];
component.partly_hoisted.push(deindent`
function ${fn}($$component) {
function ${fn}(${['$$component', ...contextual_dependencies].join(', ')}) {
${lhs} = $$component;
${object && component.invalidate(object)}
}
`);
block.builders.destroy.add_line(`ctx.${fn}(null);`);
return `@add_binding_callback(() => ctx.${fn}(${this.var}));`;
return `@add_binding_callback(() => ctx.${fn}(${[this.var, ...contextual_dependencies.map(name => `ctx.${name}`)].join(', ')}));`;
}
const name = component.get_unique_name(`${this.var}_${binding.name}_binding`);

@ -53,7 +53,11 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
slot_scopes: Map<any, any>;
}) {
let opening_tag = `<${node.name}`;
let node_contents; // awkward special case
// awkward special case
let node_contents;
let value;
const contenteditable = (
node.name !== 'textarea' &&
node.name !== 'input' &&
@ -150,33 +154,34 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (name === 'group') {
// TODO server-render group bindings
} else if (contenteditable && (name === 'text' || name === 'html')) {
const snippet = snip(expression);
if (name == 'text') {
node_contents = '${@escape(' + snippet + ')}';
} else {
// Do not escape HTML content
node_contents = '${' + snippet + '}';
}
} else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) {
node_contents = snip(expression);
value = name === 'textContent' ? '@escape($$value)' : '$$value';
} else if (binding.name === 'value' && node.name === 'textarea') {
const snippet = snip(expression);
node_contents='${(' + snippet + ') || ""}';
node_contents = '${(' + snippet + ') || ""}';
} else {
const snippet = snip(expression);
opening_tag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + @_JSON.stringify(v))) : "")(' + snippet + ')}';
opening_tag += '${@add_attribute("' + name + '", ' + snippet + ')}';
}
});
if (add_class_attribute) {
opening_tag += `\${((v) => v ? ' class="' + v + '"' : '')([${class_expression}].join(' ').trim())}`;
opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`;
}
opening_tag += '>';
renderer.append(opening_tag);
if ((node.name === 'textarea' || contenteditable) && node_contents !== undefined) {
renderer.append(node_contents);
if (node_contents !== undefined) {
if (contenteditable) {
renderer.append('${($$value => $$value === void 0 ? `');
renderer.render(node.children, options);
renderer.append('` : ' + value + ')(' + node_contents + ')}');
} else {
renderer.append(node_contents);
}
} else {
renderer.render(node.children, options);
}

@ -7,10 +7,11 @@ export default function flatten_reference(node: Node) {
const prop_end = node.end;
while (node.type === 'MemberExpression') {
if (node.computed) return null;
nodes.unshift(node.property);
parts.unshift(node.property.name);
if (!node.computed) {
parts.unshift(node.property.name);
}
node = node.object;
}
@ -20,10 +21,11 @@ export default function flatten_reference(node: Node) {
? node.name
: node.type === 'ThisExpression' ? 'this' : null;
if (!name) return null;
parts.unshift(name);
nodes.unshift(node);
if (!node.computed) {
parts.unshift(name);
}
return { name, nodes, parts, keypath: `${name}[✂${prop_start}-${prop_end}✂]` };
}

@ -2,6 +2,7 @@ import { walk } from 'estree-walker';
import is_reference from 'is-reference';
import { Node } from '../../interfaces';
import { Node as ESTreeNode } from 'estree';
import get_object from './get_object';
export function create_scopes(expression: Node) {
const map = new WeakMap();
@ -114,6 +115,10 @@ const extractors = {
nodes.push(param);
},
MemberExpression(nodes: Node[], param: Node) {
nodes.push(get_object(param));
},
ObjectPattern(nodes: Node[], param: Node) {
param.properties.forEach((prop: Node) => {
if (prop.type === 'RestElement') {

@ -36,7 +36,7 @@ const windows_1252 = [
];
const entity_pattern = new RegExp(
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}));?`,
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`,
'g'
);

@ -119,3 +119,12 @@ export function get_store_value<T>(store: Readable<T>): T | undefined {
store.subscribe(_ => value = _)();
return value;
}
export function add_attribute(name, value) {
if (!value) return '';
return ` ${name}${value === true ? '' : `=${JSON.stringify(value)}`}`;
}
export function add_classes(classes) {
return classes ? ` class="${classes}"` : ``;
}

@ -1 +1 @@
<div data-foo='&quot;quoted&quot;'></div>
<div data-foo='semi:&quot;space:&quot letter:&quote number:&quot1 end:&quot'></div>

@ -1,27 +1,27 @@
{
"html": {
"start": 0,
"end": 41,
"end": 83,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 41,
"end": 83,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 34,
"end": 76,
"type": "Attribute",
"name": "data-foo",
"value": [
{
"start": 15,
"end": 33,
"end": 75,
"type": "Text",
"raw": "&quot;quoted&quot;",
"data": "\"quoted\""
"raw": "semi:&quot;space:&quot letter:&quote number:&quot1 end:&quot",
"data": "semi:\"space:\" letter:&quote number:&quot1 end:\""
}
]
}

@ -0,0 +1,40 @@
export default {
html: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello <b>world</b></p>
`,
ssrHtml: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello undefined</p>
`,
async test({ assert, component, target, window }) {
assert.equal(component.name, '<b>world</b>');
const el = target.querySelector('editor');
el.innerHTML = 'every<span>body</span>';
// No updates to data yet
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">every<span>body</span></editor>
<p>hello <b>world</b></p>
`);
// Handle user input
const event = new window.Event('input');
await el.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">every<span>body</span></editor>
<p>hello every<span>body</span></p>
`);
component.name = 'good<span>bye</span>';
assert.equal(el.innerHTML, 'good<span>bye</span>');
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">good<span>bye</span></editor>
<p>hello good<span>bye</span></p>
`);
},
};

@ -0,0 +1,8 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:innerHTML={name}>
<b>world</b>
</editor>
<p>hello {@html name}</p>

@ -8,11 +8,6 @@ export default {
<p>hello <b>world</b></p>
`,
ssrHtml: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello <b>world</b></p>
`,
async test({ assert, component, target, window }) {
const el = target.querySelector('editor');
assert.equal(el.innerHTML, '<b>world</b>');

@ -0,0 +1,6 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:innerHTML={name}></editor>
<p>hello {@html name}</p>

@ -0,0 +1,34 @@
export default {
html: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello world</p>
`,
ssrHtml: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello undefined</p>
`,
async test({ assert, component, target, window }) {
assert.equal(component.name, 'world');
const el = target.querySelector('editor');
const event = new window.Event('input');
el.textContent = 'everybody';
await el.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">everybody</editor>
<p>hello everybody</p>
`);
component.name = 'goodbye';
assert.equal(el.textContent, 'goodbye');
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">goodbye</editor>
<p>hello goodbye</p>
`);
},
};

@ -0,0 +1,8 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:textContent={name}>
<b>world</b>
</editor>
<p>hello {name}</p>

@ -8,11 +8,6 @@ export default {
<p>hello world</p>
`,
ssrHtml: `
<editor contenteditable="true">world</editor>
<p>hello world</p>
`,
async test({ assert, component, target, window }) {
const el = target.querySelector('editor');
assert.equal(el.textContent, 'world');

@ -0,0 +1,6 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:textContent={name}></editor>
<p>hello {name}</p>

@ -0,0 +1,8 @@
export default {
skip_if_ssr: true,
html: `
<div>foo</div>
<div>has foo: true</div>
`
};

@ -0,0 +1,9 @@
<script>
import Foo from './Foo.svelte';
export let foo = {};
</script>
<Foo bind:this={foo['computed']}/>
<div>
has foo: {!!foo.computed}
</div>

@ -0,0 +1,12 @@
export default {
skip_if_ssr: true,
html: `
<div>foo</div>
<div>first has foo: true</div>
<div>foo</div>
<div>second has foo: true</div>
<div>foo</div>
<div>third has foo: true</div>
`
};

@ -0,0 +1,11 @@
<script>
import Foo from './Foo.svelte';
export let foo = {};
</script>
{#each ["first", "second", "third"] as value}
<Foo bind:this={foo[value]}/>
<div>
{value} has foo: {!!foo[value]}
</div>
{/each}

@ -0,0 +1,12 @@
export default {
skip_if_ssr: true,
html: `
<div>foo</div>
<div>0 has foo: true</div>
<div>foo</div>
<div>1 has foo: true</div>
<div>foo</div>
<div>2 has foo: true</div>
`
};

@ -0,0 +1,11 @@
<script>
import Foo from './Foo.svelte';
export let foo = [];
</script>
{#each Array(3) as _, i}
<Foo bind:this={foo[i]}/>
<div>
{i} has foo: {!!foo[i]}
</div>
{/each}

@ -0,0 +1,6 @@
<script>
export let actualValue;
let x = $$props;
</script>
<input bind:value={actualValue}>

@ -0,0 +1,14 @@
export default {
async test({ assert, target, window }) {
const input = target.querySelector('input');
const event = new window.Event('input');
input.value = 'changed';
await input.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<input>
<p>changed</p>
`);
}
};

@ -0,0 +1,7 @@
<script>
import Input from './TextInput.svelte';
export let actualValue = '';
</script>
<Input bind:actualValue />
<p>{actualValue}</p>

@ -1,6 +0,0 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:html={name}></editor>
<p>hello {@html name}</p>

@ -1,6 +0,0 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:text={name}></editor>
<p>hello {name}</p>

@ -0,0 +1,23 @@
export default {
html: `
<ul>
<li>Gruyere</li>
<li>Compté</li>
<li>Beaufort</li>
<li>Abondance</li>
</ul>
`,
async test({ assert, component, target }) {
await component.swap(0, 1);
assert.htmlEqual(target.innerHTML, `
<ul>
<li>Compté</li>
<li>Gruyere</li>
<li>Beaufort</li>
<li>Abondance</li>
</ul>
`);
}
};

@ -0,0 +1,18 @@
<script>
let cheese = [
'Gruyere',
'Compté',
'Beaufort',
'Abondance',
];
export function swap(a, b) {
[cheese[a], cheese[b]] = [cheese[b], cheese[a]];
}
</script>
<ul>
{#each cheese as cheese}
<li>{cheese}</li>
{/each}
</ul>

@ -9,6 +9,6 @@ export default {
<span>A</span>
<span></span>
<span>&notanentity;</span>
<span>&amp;notanentity;</span>
`
};

@ -3,4 +3,4 @@
let toggle = false;
</script>
<editor contenteditable={toggle} bind:html={name}></editor>
<editor contenteditable={toggle} bind:innerHTML={name}></editor>

@ -1,6 +1,6 @@
[{
"code": "missing-contenteditable-attribute",
"message": "'contenteditable' attribute is required for text and html two-way bindings",
"message": "'contenteditable' attribute is required for textContent and innerHTML two-way bindings",
"start": {
"line": 4,
"column": 8,
@ -8,8 +8,8 @@
},
"end": {
"line": 4,
"column": 24,
"character": 64
"column": 31,
"character": 71
},
"pos": 48
}]

@ -1,4 +1,4 @@
<script>
export let name;
</script>
<editor bind:text={name}></editor>
<editor bind:textContent={name}></editor>

Loading…
Cancel
Save