Merge branch 'master' into pr/4490

pull/4490/head
Conduitry 6 years ago
commit 12a2f70b59

1
.gitignore vendored

@ -1,6 +1,7 @@
.idea .idea
.DS_Store .DS_Store
.nyc_output .nyc_output
.vscode
node_modules node_modules
*.map *.map
/src/compiler/compile/internal_exports.ts /src/compiler/compile/internal_exports.ts

@ -1,5 +1,15 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Support dimension bindings in cross-origin environments ([#2147](https://github.com/sveltejs/svelte/issues/2147))
* Try using `globalThis` rather than `globals` for the benefit of non-Node servers and web workers ([#3561](https://github.com/sveltejs/svelte/issues/3561), [#4545](https://github.com/sveltejs/svelte/issues/4545))
* Fix attaching of JS debugging comments to HTML comments ([#4565](https://github.com/sveltejs/svelte/issues/4565))
## 3.20.1
* Fix compiler regression with slots ([#4562](https://github.com/sveltejs/svelte/issues/4562))
## 3.20.0 ## 3.20.0
* Allow destructuring in `{#await}` blocks ([#1851](https://github.com/sveltejs/svelte/issues/1851)) * Allow destructuring in `{#await}` blocks ([#1851](https://github.com/sveltejs/svelte/issues/1851))

2
package-lock.json generated

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

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

@ -893,7 +893,7 @@ A custom transition function can also return a `tick` function, which is called
function typewriter(node, { speed = 50 }) { function typewriter(node, { speed = 50 }) {
const valid = ( const valid = (
node.childNodes.length === 1 && node.childNodes.length === 1 &&
node.childNodes[0].nodeType === 3 node.childNodes[0].nodeType === Node.TEXT_NODE
); );
if (!valid) return {}; if (!valid) return {};

@ -804,7 +804,7 @@ You can see a full example on the [animations tutorial](tutorial/animate)
### `svelte/easing` ### `svelte/easing`
Easing functions specificy the rate of change over time and are useful when working with Svelte's built-in transitions and animations as well as the tweened and spring utilities. `svelte/easing` contains 31 named exports, a `linear` ease and 3 variants of 10 different easing functions: `in`, `out` and `inOut`. Easing functions specify the rate of change over time and are useful when working with Svelte's built-in transitions and animations as well as the tweened and spring utilities. `svelte/easing` contains 31 named exports, a `linear` ease and 3 variants of 10 different easing functions: `in`, `out` and `inOut`.
You can explore the various eases using the [ease visualiser](examples#easing) in the [examples section](examples). You can explore the various eases using the [ease visualiser](examples#easing) in the [examples section](examples).

@ -18,7 +18,7 @@
Remove first thing Remove first thing
</button> </button>
<div style="display: grid; grid-template-columns: 1fr 1fr; grip-gap: 1em"> <div style="display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1em">
<div> <div>
<h2>Keyed</h2> <h2>Keyed</h2>
{#each things as thing (thing.id)} {#each things as thing (thing.id)}

@ -4,7 +4,7 @@
function typewriter(node, { speed = 50 }) { function typewriter(node, { speed = 50 }) {
const valid = ( const valid = (
node.childNodes.length === 1 && node.childNodes.length === 1 &&
node.childNodes[0].nodeType === 3 node.childNodes[0].nodeType === Node.TEXT_NODE
); );
if (!valid) { if (!valid) {

@ -3,6 +3,8 @@
export let item; export let item;
export let returnTo; export let returnTo;
$: url = !item.domain ? `https://news.ycombinator.com/${item.url}` : item.url;
</script> </script>
<style> <style>
@ -24,9 +26,11 @@
<a href={returnTo}>&laquo; back</a> <a href={returnTo}>&laquo; back</a>
<article> <article>
<a href="{item.url}"> <a href="{url}">
<h1>{item.title}</h1> <h1>{item.title}</h1>
{#if item.domain}
<small>{item.domain}</small> <small>{item.domain}</small>
{/if}
</a> </a>
<p class="meta">submitted by {item.user} {item.time_ago} <p class="meta">submitted by {item.user} {item.time_ago}

@ -7,6 +7,8 @@
const c = item.comments_count; const c = item.comments_count;
return `${c} ${c === 1 ? 'comment' : 'comments'}`; return `${c} ${c === 1 ? 'comment' : 'comments'}`;
} }
$: url = item.type === "ask" ? `https://news.ycombinator.com/${item.url}` : item.url;
</script> </script>
<style> <style>
@ -33,6 +35,6 @@
<article> <article>
<span>{i + offset + 1}</span> <span>{i + offset + 1}</span>
<h2><a target="_blank" href={item.url}>{item.title}</a></h2> <h2><a target="_blank" href={url}>{item.title}</a></h2>
<p class="meta"><a href="#/item/{item.id}">{comment_text()}</a> by {item.user} {item.time_ago}</p> <p class="meta"><a href="#/item/{item.id}">{comment_text()}</a> by {item.user} {item.time_ago}</p>
</article> </article>

@ -20,3 +20,5 @@ Add a `<script>` tag that imports `Nested.svelte`...
``` ```
Notice that even though `Nested.svelte` has a `<p>` element, the styles from `App.svelte` don't leak in. Notice that even though `Nested.svelte` has a `<p>` element, the styles from `App.svelte` don't leak in.
Also notice that the component name `Nested` is capitalised. This convention has been adopted to allow us to differentiate between user-defined components and regular HTML tags.

@ -4,7 +4,7 @@
function typewriter(node, { speed = 50 }) { function typewriter(node, { speed = 50 }) {
const valid = ( const valid = (
node.childNodes.length === 1 && node.childNodes.length === 1 &&
node.childNodes[0].nodeType === 3 node.childNodes[0].nodeType === Node.TEXT_NODE
); );
if (!valid) { if (!valid) {

@ -8,7 +8,7 @@ While you should generally use CSS for transitions as much as possible, there ar
function typewriter(node, { speed = 50 }) { function typewriter(node, { speed = 50 }) {
const valid = ( const valid = (
node.childNodes.length === 1 && node.childNodes.length === 1 &&
node.childNodes[0].nodeType === 3 node.childNodes[0].nodeType === Node.TEXT_NODE
); );
if (!valid) { if (!valid) {

@ -1291,13 +1291,14 @@
} }
}, },
"@sveltejs/svelte-repl": { "@sveltejs/svelte-repl": {
"version": "0.1.17", "version": "0.1.18",
"resolved": "https://registry.npmjs.org/@sveltejs/svelte-repl/-/svelte-repl-0.1.17.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/svelte-repl/-/svelte-repl-0.1.18.tgz",
"integrity": "sha512-rM0DC+pZnqwH6PiuxXUmFRwYZ9XNkexxTNt+prR91Qs7ssxGgf0QkH6kGivSNLbrOtOvcgJbt1nUDybWra5HKA==", "integrity": "sha512-b1psPdlJ9qRqn28l+sPSma6OSfilue0dGK4BcS75MOsfdjZjSvsc0AIAoriQ3q1mB/frCoyUri6nxFGgEKv0iQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"codemirror": "^5.49.2", "codemirror": "^5.49.2",
"estree-walker": "^0.9.0", "estree-walker": "^0.9.0",
"marked": "^0.8.2",
"sourcemap-codec": "^1.4.6", "sourcemap-codec": "^1.4.6",
"svelte-json-tree": "0.0.5", "svelte-json-tree": "0.0.5",
"yootils": "0.0.16" "yootils": "0.0.16"
@ -1309,10 +1310,16 @@
"integrity": "sha512-12U47o7XHUX329+x3FzNVjCx3SHEzMF0nkDv7r/HnBzX/xNTKxajBk6gyygaxrAFtLj39219oMfbtxv4KpaOiA==", "integrity": "sha512-12U47o7XHUX329+x3FzNVjCx3SHEzMF0nkDv7r/HnBzX/xNTKxajBk6gyygaxrAFtLj39219oMfbtxv4KpaOiA==",
"dev": true "dev": true
}, },
"marked": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz",
"integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==",
"dev": true
},
"sourcemap-codec": { "sourcemap-codec": {
"version": "1.4.6", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true "dev": true
} }
} }
@ -1594,9 +1601,9 @@
"dev": true "dev": true
}, },
"codemirror": { "codemirror": {
"version": "5.49.2", "version": "5.52.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.2.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.52.2.tgz",
"integrity": "sha512-dwJ2HRPHm8w51WB5YTF9J7m6Z5dtkqbU9ntMZ1dqXyFB9IpjoUFDj80ahRVEoVanfIp6pfASJbOlbWdEf8FOzQ==", "integrity": "sha512-WCGCixNUck2HGvY8/ZNI1jYfxPG5cRHv0VjmWuNzbtCLz8qYA5d+je4QhSSCtCaagyeOwMi/HmmPTjBgiTm2lQ==",
"dev": true "dev": true
}, },
"color-convert": { "color-convert": {
@ -3483,9 +3490,9 @@
} }
}, },
"sapper": { "sapper": {
"version": "0.27.8", "version": "0.27.11",
"resolved": "https://registry.npmjs.org/sapper/-/sapper-0.27.8.tgz", "resolved": "https://registry.npmjs.org/sapper/-/sapper-0.27.11.tgz",
"integrity": "sha512-78K+56yu9nGOEU0B0XjBvNchRuPEv4aHbAKK4D874S4aoapMAkHCT0bHtPK12S3P7JPxvvT8GzHaq/8NetMmbg==", "integrity": "sha512-5EaPZhlc8OnyN3UCI6dRSM1Gz5sxyzLZG/1z5nMvZhg9Ng+rSvEvJx/rW/tSHcnQPa8or7+YcbfvQHKS5oPHiw==",
"dev": true, "dev": true,
"requires": { "requires": {
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",

@ -36,7 +36,7 @@
"@babel/runtime": "^7.6.0", "@babel/runtime": "^7.6.0",
"@sindresorhus/slugify": "^0.9.1", "@sindresorhus/slugify": "^0.9.1",
"@sveltejs/site-kit": "^1.1.4", "@sveltejs/site-kit": "^1.1.4",
"@sveltejs/svelte-repl": "^0.1.17", "@sveltejs/svelte-repl": "^0.1.18",
"degit": "^2.1.4", "degit": "^2.1.4",
"dotenv": "^8.1.0", "dotenv": "^8.1.0",
"esm": "^3.2.25", "esm": "^3.2.25",
@ -53,7 +53,7 @@
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"rollup-plugin-svelte": "^5.1.0", "rollup-plugin-svelte": "^5.1.0",
"rollup-plugin-terser": "^5.1.1", "rollup-plugin-terser": "^5.1.1",
"sapper": "^0.27.8", "sapper": "^0.27.11",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"svelte": "^3.12.0" "svelte": "^3.12.0"
}, },

@ -1216,7 +1216,6 @@ export default class Component {
}); });
const lookup = new Map(); const lookup = new Map();
let seen;
unsorted_reactive_declarations.forEach(declaration => { unsorted_reactive_declarations.forEach(declaration => {
declaration.assignees.forEach(name => { declaration.assignees.forEach(name => {
@ -1251,28 +1250,19 @@ export default class Component {
} }
const add_declaration = declaration => { const add_declaration = declaration => {
if (this.reactive_declarations.indexOf(declaration) !== -1) { if (this.reactive_declarations.includes(declaration)) return;
return;
}
seen.add(declaration);
declaration.dependencies.forEach(name => { declaration.dependencies.forEach(name => {
if (declaration.assignees.has(name)) return; if (declaration.assignees.has(name)) return;
const earlier_declarations = lookup.get(name); const earlier_declarations = lookup.get(name);
if (earlier_declarations) if (earlier_declarations)
earlier_declarations.forEach(declaration => { earlier_declarations.forEach(add_declaration);
add_declaration(declaration);
});
}); });
this.reactive_declarations.push(declaration); this.reactive_declarations.push(declaration);
}; };
unsorted_reactive_declarations.forEach(declaration => { unsorted_reactive_declarations.forEach(add_declaration);
seen = new Set();
add_declaration(declaration);
});
} }
warn_if_undefined(name: string, node, template_scope: TemplateScope) { warn_if_undefined(name: string, node, template_scope: TemplateScope) {

@ -419,8 +419,8 @@ export default class Block {
return body; return body;
} }
has_content() { has_content(): boolean {
return this.first || return !!this.first ||
this.event_listeners.length > 0 || this.event_listeners.length > 0 ||
this.chunks.intro.length > 0 || this.chunks.intro.length > 0 ||
this.chunks.outro.length > 0 || this.chunks.outro.length > 0 ||

@ -284,4 +284,8 @@ export default class Renderer {
return node; return node;
} }
remove_block(block: Block | Node | Node[]) {
this.blocks.splice(this.blocks.indexOf(block), 1);
}
} }

@ -586,7 +586,7 @@ export default class ElementWrapper extends Wrapper {
); );
block.chunks.destroy.push( block.chunks.destroy.push(
b`${resize_listener}.cancel();` b`${resize_listener}();`
); );
} else { } else {
block.event_listeners.push( block.event_listeners.push(

@ -155,6 +155,7 @@ export default class InlineComponentWrapper extends Wrapper {
// removing empty slot // removing empty slot
for (const slot of this.slots.keys()) { for (const slot of this.slots.keys()) {
if (!this.slots.get(slot).block.has_content()) { if (!this.slots.get(slot).block.has_content()) {
this.renderer.remove_block(this.slots.get(slot).block);
this.slots.delete(slot); this.slots.delete(slot);
} }
} }

@ -38,6 +38,7 @@ export default class SlotWrapper extends Wrapper {
name: this.renderer.component.get_unique_name(`fallback_block`), name: this.renderer.component.get_unique_name(`fallback_block`),
type: 'fallback' type: 'fallback'
}); });
renderer.blocks.push(this.fallback);
} }
this.fragment = new FragmentWrapper( this.fragment = new FragmentWrapper(
@ -113,19 +114,23 @@ export default class SlotWrapper extends Wrapper {
get_slot_context_fn = 'null'; get_slot_context_fn = 'null';
} }
let has_fallback = !!this.fallback;
if (this.fallback) { if (this.fallback) {
this.fragment.render(this.fallback, null, x`#nodes` as Identifier); this.fragment.render(this.fallback, null, x`#nodes` as Identifier);
renderer.blocks.push(this.fallback); has_fallback = this.fallback.has_content();
if (!has_fallback) {
renderer.remove_block(this.fallback);
}
} }
const slot = block.get_unique_name(`${sanitize(slot_name)}_slot`); const slot = block.get_unique_name(`${sanitize(slot_name)}_slot`);
const slot_definition = block.get_unique_name(`${sanitize(slot_name)}_slot_template`); const slot_definition = block.get_unique_name(`${sanitize(slot_name)}_slot_template`);
const slot_or_fallback = this.fallback ? block.get_unique_name(`${sanitize(slot_name)}_slot_or_fallback`) : slot; const slot_or_fallback = has_fallback ? block.get_unique_name(`${sanitize(slot_name)}_slot_or_fallback`) : slot;
block.chunks.init.push(b` block.chunks.init.push(b`
const ${slot_definition} = ${renderer.reference('$$slots')}.${slot_name}; const ${slot_definition} = ${renderer.reference('$$slots')}.${slot_name};
const ${slot} = @create_slot(${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${get_slot_context_fn}); const ${slot} = @create_slot(${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${get_slot_context_fn});
${this.fallback ? b`const ${slot_or_fallback} = ${slot} || ${this.fallback.name}(#ctx);` : null} ${has_fallback ? b`const ${slot_or_fallback} = ${slot} || ${this.fallback.name}(#ctx);` : null}
`); `);
block.chunks.create.push( block.chunks.create.push(
@ -161,7 +166,7 @@ export default class SlotWrapper extends Wrapper {
const dynamic_dependencies = Array.from(this.dependencies).filter(is_dependency_dynamic); const dynamic_dependencies = Array.from(this.dependencies).filter(is_dependency_dynamic);
const fallback_dynamic_dependencies = this.fallback const fallback_dynamic_dependencies = has_fallback
? Array.from(this.fallback.dependencies).filter(is_dependency_dynamic) ? Array.from(this.fallback.dependencies).filter(is_dependency_dynamic)
: []; : [];
@ -173,7 +178,7 @@ export default class SlotWrapper extends Wrapper {
); );
} }
`; `;
const fallback_update = this.fallback && fallback_dynamic_dependencies.length > 0 && b` const fallback_update = has_fallback && fallback_dynamic_dependencies.length > 0 && b`
if (${slot_or_fallback} && ${slot_or_fallback}.p && ${renderer.dirty(fallback_dynamic_dependencies)}) { if (${slot_or_fallback} && ${slot_or_fallback}.p && ${renderer.dirty(fallback_dynamic_dependencies)}) {
${slot_or_fallback}.p(#ctx, #dirty); ${slot_or_fallback}.p(#ctx, #dirty);
} }

@ -24,7 +24,7 @@ export default function create_debugging_comment(
while (source[d] !== '>') d += 1; while (source[d] !== '>') d += 1;
d += 1; d += 1;
} }
} else if (node.type === 'Text') { } else if (node.type === 'Text' || node.type === 'Comment') {
d = node.end; d = node.end;
} else { } else {
// @ts-ignore // @ts-ignore

@ -57,7 +57,7 @@ export function empty() {
return text(''); return text('');
} }
export function listen(node: Node, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) { export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
node.addEventListener(event, handler, options); node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options); return () => node.removeEventListener(event, handler, options);
} }
@ -234,37 +234,61 @@ export function select_multiple_value(select) {
return [].map.call(select.querySelectorAll(':checked'), option => option.__value); return [].map.call(select.querySelectorAll(':checked'), option => option.__value);
} }
export function add_resize_listener(element, fn) { // unfortunately this can't be a constant as that wouldn't be tree-shakeable
if (getComputedStyle(element).position === 'static') { // so we cache the result instead
element.style.position = 'relative'; let crossorigin: boolean;
export function is_crossorigin() {
if (crossorigin === undefined) {
crossorigin = false;
try {
if (typeof window !== 'undefined' && window.parent) {
void window.parent.document;
}
} catch (error) {
crossorigin = true;
}
} }
const object = document.createElement('object'); return crossorigin;
object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); }
object.setAttribute('aria-hidden', 'true');
object.type = 'text/html';
object.tabIndex = -1;
let win; export function add_resize_listener(node: HTMLElement, fn: () => void) {
const computed_style = getComputedStyle(node);
const z_index = (parseInt(computed_style.zIndex) || 0) - 1;
object.onload = () => { if (computed_style.position === 'static') {
win = object.contentDocument.defaultView; node.style.position = 'relative';
win.addEventListener('resize', fn); }
};
const iframe = element('iframe');
iframe.setAttribute('style',
`display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; ` +
`overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: ${z_index};`
);
iframe.setAttribute('aria-hidden', 'true');
iframe.tabIndex = -1;
let unsubscribe: () => void;
if (/Trident/.test(navigator.userAgent)) { if (is_crossorigin()) {
element.appendChild(object); iframe.src = `data:text/html,<script>onresize=function(){parent.postMessage(0,'*')}</script>`;
object.data = 'about:blank'; unsubscribe = listen(window, 'message', (event: MessageEvent) => {
if (event.source === iframe.contentWindow) fn();
});
} else { } else {
object.data = 'about:blank'; iframe.src = 'about:blank';
element.appendChild(object); iframe.onload = () => {
unsubscribe = listen(iframe.contentWindow, 'resize', fn);
};
} }
return { append(node, iframe);
cancel: () => {
win && win.removeEventListener && win.removeEventListener('resize', fn); return () => {
element.removeChild(object); detach(iframe);
} if (unsubscribe) unsubscribe();
}; };
} }

@ -1,3 +1,7 @@
declare const global: any; declare const global: any;
export const globals = (typeof window !== 'undefined' ? window : global) as unknown as typeof globalThis; export const globals = (typeof window !== 'undefined'
? window
: typeof globalThis !== 'undefined'
? globalThis
: global) as unknown as typeof globalThis;

@ -30,7 +30,7 @@ function create_fragment(ctx) {
o: noop, o: noop,
d(detaching) { d(detaching) {
if (detaching) detach(div); if (detaching) detach(div);
div_resize_listener.cancel(); div_resize_listener();
} }
}; };
} }

@ -59,7 +59,7 @@ function create_fragment(ctx) {
o: noop, o: noop,
d(detaching) { d(detaching) {
if (detaching) detach(video); if (detaching) detach(video);
video_resize_listener.cancel(); video_resize_listener();
run_all(dispose); run_all(dispose);
} }
}; };

@ -1,8 +1,8 @@
export default { export default {
async test({ assert, target }) { async test({ assert, target }) {
const object = target.querySelector('object'); const iframe = target.querySelector('iframe');
assert.equal(object.getAttribute('aria-hidden'), "true"); assert.equal(iframe.getAttribute('aria-hidden'), "true");
assert.equal(object.getAttribute('tabindex'), "-1"); assert.equal(iframe.getAttribute('tabindex'), "-1");
} }
}; };

@ -0,0 +1,6 @@
<slot>
<div>Hello</div>
<div>world</div>
<div>Bye</div>
<div>World</div>
</slot>

@ -0,0 +1,6 @@
export default {
html: `
<div>Hello World</div>
<div>Hello</div><div>world</div><div>Bye</div><div>World</div>
`,
};

@ -0,0 +1,9 @@
<script>
import Inner from "./Inner.svelte";
</script>
<Inner>
<div>Hello World</div>
</Inner>
<Inner></Inner>

@ -0,0 +1,3 @@
<slot name="a"><!-- placeholder--></slot>
<slot name="b"><!-- placeholder--> </slot>
<slot name="c"><!-- placeholder--> foobar </slot>

@ -0,0 +1,5 @@
export default {
html: `
foobar
`,
};

@ -0,0 +1,5 @@
<script>
import Inner from "./Inner.svelte";
</script>
<Inner></Inner>
Loading…
Cancel
Save