Merge branch 'sites' into feat/redo-blog-logic

pull/8447/head
Puru Vijay 3 years ago
commit fe5781f756

@ -1,11 +1,17 @@
# Svelte changelog # Svelte changelog
## Unreleased ## 3.58.0
- Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311)) - Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))
- Relax `a11y-no-noninteractive-element-to-interactive-role` warning ([#8402](https://github.com/sveltejs/svelte/pull/8402)) - Add support for CSS `@container` queries ([#6969](https://github.com/sveltejs/svelte/issues/6969))
- Respect `preserveComments` in DOM output ([#7182](https://github.com/sveltejs/svelte/pull/7182))
- Allow use of `document` for `target` in typings ([#7554](https://github.com/sveltejs/svelte/pull/7554))
- Add `a11y-interactive-supports-focus` warning ([#8392](https://github.com/sveltejs/svelte/pull/8392)) - Add `a11y-interactive-supports-focus` warning ([#8392](https://github.com/sveltejs/svelte/pull/8392))
- Fix equality check when updating dynamic text ([#5931](https://github.com/sveltejs/svelte/issues/5931)) - Fix equality check when updating dynamic text ([#5931](https://github.com/sveltejs/svelte/issues/5931))
- Relax `a11y-no-noninteractive-element-to-interactive-role` warning ([#8402](https://github.com/sveltejs/svelte/pull/8402))
- Properly handle microdata attributes ([#8413](https://github.com/sveltejs/svelte/issues/8413))
- Prevent name collision when using computed destructuring variables ([#8417](https://github.com/sveltejs/svelte/issues/8417))
- Fix escaping `<textarea value={...}>` values in SSR ([#8429](https://github.com/sveltejs/svelte/issues/8429))
## 3.57.0 ## 3.57.0

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.57.0", "version": "3.58.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "svelte", "name": "svelte",
"version": "3.57.0", "version": "3.58.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^0.3.0",

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

@ -0,0 +1,109 @@
---
title: "What's new in Svelte: April 2023"
description: "Loads of new Svelte compiler features, plus Svelte Summit and SvelteHack"
author: Dani Sandoval
authorURL: https://dreamindani.com
---
Happy April, everyone! This month, we're covering all the new features in the Svelte compiler, some quality-of-life improvements in SvelteKit and a huge showcase (like always).
In core team news, Dominic Gannaway has joined Vercel to work on Svelte full-time! Dominic is a world-class expert on wringing performance out of javascript engines, on the DOM, on reactivity, on accessibility, and more! You might know him as the creator of the [Inferno](https://www.infernojs.org/) UI framework or [Lexical](https://lexical.dev/), Meta's WYSIWYG editor. It'll be great to see his talents at work across the Svelte ecosystem 🌱
Don't forget! Svelte Summit Spring, Svelte's 6th virtual conference, will be happening on May 6th. Also, there's just two weeks left until the end of [SvelteHack](https://hack.sveltesociety.dev/)... It's a great opportunity to share your creations with the community and maybe even earn a prize!
Now let's jump into this month's changes...
## What's new in Svelte
- A bunch of new features are now available as of **3.56.0**!
- Add `|stopImmediatePropagation` event modifier for `on:eventname` ([#5085](https://github.com/sveltejs/svelte/issues/5085), [Docs](https://svelte.dev/docs#template-syntax-element-directives-on-eventname))
- Add `axis` parameter to `slide` transition ([#6182](https://github.com/sveltejs/svelte/issues/6182), [Docs](https://svelte.dev/docs#run-time-svelte-transition-slide))
- Add `readonly` utility to convert `writable` store to readonly ([#6518](https://github.com/sveltejs/svelte/pull/6518), [Docs](https://svelte.dev/docs#run-time-svelte-store-writable))
- Add `readyState` binding for media elements ([#6666](https://github.com/sveltejs/svelte/issues/6666), [Docs](https://svelte.dev/docs#template-syntax-element-directives-bind-property-media-element-bindings))
- Add `naturalWidth` and `naturalHeight` bindings to images ([#7771](https://github.com/sveltejs/svelte/issues/7771), [Docs](https://svelte.dev/docs#template-syntax-element-directives-bind-property-image-element-bindings))
- Support `<!-- svelte-ignore ... -->` on components ([#8082](https://github.com/sveltejs/svelte/issues/8082))
- Inputs in a `bind:group` will clear when their value is set to `undefined` (**3.56.0**, [#8214](https://github.com/sveltejs/svelte/issues/8214))
- `<input>` values will now persist when swapping elements with spread attributes in an `{#each}` block (**3.56.0**, [#7578](https://github.com/sveltejs/svelte/issues/7578))
- Better warnings across the board - from `noreferrer` to `aria` rules (**3.56.0**)
- Add <svelte:document> (**3.57.0**, [#3310](https://github.com/sveltejs/svelte/issues/3310))
- The `style:` directive will now take precedence over a `style=` attribute (**3.57.0**, [#7475](https://github.com/sveltejs/svelte/issues/7475))
- CSS units are now supported in the `fly` and `blur` transitions (**3.57.0**, [#7623](https://github.com/sveltejs/svelte/pull/7623), [Docs](https://svelte.dev/docs#run-time-svelte-transition))
For all the changes to the Svelte compiler, including unreleased changes, check out the [CHANGELOG](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md).
## What's new in SvelteKit
- You can now get all cookies for a request with `cookies.getAll` (**1.10.0**, [#9287](https://github.com/sveltejs/kit/pull/9287), [Docs](https://kit.svelte.dev/docs/types#public-types-cookies))
- Easily manage the submission status of (multiple) forms with the new exposed `submitter` parameter in `use:enhance` (**1.12.0**, [#9425](https://github.com/sveltejs/kit/pull/9425), [Docs](https://kit.svelte.dev/docs/types#public-types-submitfunction))
- The default error page now has dark mode styles (**1.13.0**, [#9460](https://github.com/sveltejs/kit/pull/9460))
- You can now omit types on all methods and variables with special meaning to SvelteKit and still benefit from full type safety! Read more about it in the [announcement blog post](https://svelte.dev/blog/zero-config-type-safety)
---
## Community Showcase
**Apps & Sites built with Svelte**
- [Peerbeer](https://peer.beer/) lets you share files peer-to-peer (p2p) without any third parties or data limits
- [unplaneted](https://unplaneted.com/) is an interface for exploring very large space images
- [PokeBook](https://github.com/pokegh0st/pokebook) is a digital notebook for writing poetry that provides a beautiful distraction-free environment and autosave
- [papi](https://papi.run/) lets you create prompts for AI models and share them with others with a unique link
- [Mathesar](https://github.com/centerofci/mathesar) is a straightforward open source tool that provides a spreadsheet-like interface to a PostgreSQL database
- [SQLite Playground](https://neil.macmunn.com/sqlite#) lets you learn how SQLite runs and stores data in the browser
- [svgl](https://github.com/pheralb/svgl) is a beautiful library with SVG logos
- [Swehl](https://swehl.com/) is an eCommerce store, community and tutorial site for breastfeeding mothers
- [Codeverter](https://github.com/TGlide/codeverter) is a GPT-powered code converter, allowing you to convert between different languages and frameworks
- [Game On Or Not](https://gameonornot.com/) is a free web app that helps you organize sports with your friends
- [Sveltia CMS](https://github.com/sveltia/sveltia-cms) is a Git-based lightweight headless CMS
**Learning Resources**
_Featuring Svelte Contributors and Ambassadors_
- [Streaming, snapshots, and other new features since SvelteKit 1.0](https://svelte.dev/blog/streaming-snapshots-sveltekit) by Geoff Rich on the svelte.dev Blog
- [Dev Vlog: Rich Harris shows us what's new in Svelte and Kit, March 2023](https://www.youtube.com/watch?v=vgXgex5E-8g) from Svelte Society
- If you missed this one live, check out [the next one](https://www.youtube.com/watch?v=MJHO6FSioPI) - scheduled for April 5th
- [Svelte Society - London February 2023](https://www.youtube.com/watch?v=RkQ_f7XxdMI)
- Svelte Radio episodes from this month:
- [We all live in a Svelte Submarine](https://www.svelteradio.com/episodes/we-all-live-in-a-svelte-submarine)
- [Building furniture using Svelte with Bert Bengtson](https://www.svelteradio.com/episodes/building-furniture-using-svelte-with-bert-bengtson)
- [Svelte Hackathon Announcement](https://www.svelteradio.com/episodes/svelte-hackathon-announcement)
- [LevelUpTuts 6 months later with Scott Tolinski](https://www.svelteradio.com/episodes/leveluptuts-6-months-later-with-scott-tolinski)
- [I got a cold and had fever dreams about React 😱](https://www.svelteradio.com/episodes/i-got-a-cold-and-had-fever-dreams-about-react)
- This Week In Svelte videos:
- [2023 March 10 - New prompts! Underline your links!](https://www.youtube.com/watch?v=WiCjQVoE-3k)
- [2023 March 17 - More a11y warnings! How to: Dynamic Form Actions!](https://www.youtube.com/watch?v=sRhZQ-2VxVU)
- [2023 March 23 - SvelteKit 1.13.0, Vitest and Playwright overview](https://www.youtube.com/watch?v=vpbhsbg2otg)
_To Watch or Hear_
- [Full Stack SvelteKit App Deployment Using Vercel And Supabase For $0](https://www.youtube.com/watch?v=uAF4Yd-gddo) by Joy of Code
- [Why Is Svelte.js so Popular?](https://www.youtube.com/watch?v=73Y8Yyg54zc) by Prismic
- [Interactive Tables in SvelteKit with TanStack Table](https://www.youtube.com/watch?v=-Zuo3UWjjI8) by hartenfellerdev
- [SvelteKit + GraphQL with Houdini](https://www.youtube.com/watch?v=ADnaRwQZfqw&list=PLm0ILX0LGQk_220vvpsbyXH2VesRlCm-E) by Aftab Alam
_To Read_
- [Thoughts on Svelte](https://tyhopp.com/notes/thoughts-on-svelte) by Ty Hopp
- [Storybook](https://storybook.js.org/blog/storybook-for-sveltekit/) on why (and how) it supports SvelteKit
- [Svelte Authentication Tutorial with Authorizer](https://thethinks.vercel.app/blog/svelte-authorizer) by The Thinks
- [Use Zod to Validate Forms on the Server with SvelteKit](https://blog.robino.dev/posts/svelte-zod-error) by Ross Robino
- [Do I need a sitemap for my SvelteKit app, and how do I create it?](https://maier.tech/posts/do-i-need-a-sitemap-for-my-sveltekit-app-and-how-do-i-create-it) and [Complement zero-effort type safety in SvelteKit with Zod for even more type safety](https://maier.tech/posts/complement-zero-effort-type-safety-in-sveltekit-with-zod-for-even-more-type-safety) and [Configuring Turborepo for a SvelteKit monorepo](https://maier.tech/posts/configuring-turborepo-for-a-sveltekit-monorepo) by Thilo Maier
- [Adding page transitions in SvelteKit](https://joshcollinsworth.com/blog/sveltekit-page-transitions) by Josh Collinsworth
- [E2E testing with SvelteKit and Playwright](https://www.okupter.com/blog/e2e-testing-with-sveltekit-and-playwright) and [Why you should use TypeScript in your next SvelteKit projects](https://www.okupter.com/blog/sveltekit-with-typescript) by Justin Ahinon
- [Understanding the structure of a SvelteKit project](https://www.inow.dev/understanding-the-structure-of-a-svelte-kit-project/) by Igor Nowosad
- [Secure Authentication in Svelte using Hooks](https://dev.to/brewhousedigital/secure-authentication-in-svelte-using-hooks-k5j) by Brewhouse Digital
**Libraries, Tools & Components**
- [@vavite/node-loader](https://github.com/cyco130/vavite/tree/main/packages/node-loader) is a Node ESM loader that uses Vite to transpile modules to enable sourcemap and breakpoints support in SvelteKit (or any Vite) project
- [Inlang](https://github.com/inlang/inlang) is building i18n for SvelteKit and is [looking for feedback](https://www.reddit.com/r/sveltejs/comments/11ydtui/sveltekit_and_i18n_lets_finally_solve_this_never/)
- [Skeleton](https://www.skeleton.dev/) - the UI toolkit for Svelte and Tailwind - is now 1.0 🎉
- [SvelteKit-integrated-WebSocket](https://github.com/suhaildawood/SvelteKit-integrated-WebSocket) provides first-class support for WebSockets within SvelteKit by attaching a WebSocket server to the global state
- [Svelte Legos](https://github.com/ankurrsinghal/svelte-legos) is a collection of essential Svelte Composition Utilities
- [svelte-stored-writable](https://github.com/efstajas/svelte-stored-writable) is a drop-in extension of Svelte's writable that additionally stores and restores its contents using localStorage.
- [svelte-virtual](https://github.com/ghostebony/svelte-virtual) provides Svelte components for efficiently rendering large lists.
- ChatGPT Clones and Starters
- [chatwithme.chat](https://github.com/kierangilliam/chatwithme.chat) is an open source ChatGPT UI
- [SlickGPT](https://github.com/ShipBit/slickgpt) is a light-weight "use-your-own-API-key" web client for the OpenAI API written in Svelte
- [AI Chat Bestie](https://github.com/KTruong008/aichatbestie) is an unofficial ChatGPT app
- [chatgpt-svelte](https://github.com/ichbtrv/chatgpt-svelte) is a simple UI for the ChatGPT Open AI API
Thanks for reading! And don't forget to try your hand at the [Svelte Hackathon](https://hack.sveltesociety.dev/) 🧑‍💻
As always, feel free to let us know if we missed anything on [Reddit](https://www.reddit.com/r/sveltejs/) or [Discord](https://discord.gg/svelte).
See ya next time!

@ -13,7 +13,5 @@ GITHUB_CLIENT_SECRET=
SUPABASE_URL=https://kpaaohfbmxvespqoqdzp.supabase.co SUPABASE_URL=https://kpaaohfbmxvespqoqdzp.supabase.co
SUPABASE_KEY= SUPABASE_KEY=
PUBLIC_API_BASE="https://api.svelte.dev"
# client-side # client-side
VITE_MAPBOX_ACCESS_TOKEN= VITE_MAPBOX_ACCESS_TOKEN=

@ -10,3 +10,4 @@
/src/routes/_components/Supporters/donors.jpg /src/routes/_components/Supporters/donors.jpg
/src/routes/_components/Supporters/donors.js /src/routes/_components/Supporters/donors.js
.vercel .vercel
examples-data.js

File diff suppressed because it is too large Load Diff

@ -4,8 +4,8 @@
"description": "Docs and examples for Svelte", "description": "Docs and examples for Svelte",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/update.js && vite dev", "dev": "node scripts/generate_examples.js && node scripts/update.js && vite dev",
"build": "node scripts/update.js && vite build", "build": "node scripts/generate_examples.js && node scripts/update.js && vite build",
"update": "node scripts/update.js --force=true", "update": "node scripts/update.js --force=true",
"preview": "vite preview", "preview": "vite preview",
"start": "node build", "start": "node build",
@ -16,19 +16,18 @@
"test": "uvu -r ts-node/register src/lib/server/markdown" "test": "uvu -r ts-node/register src/lib/server/markdown"
}, },
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.13.0", "@supabase/supabase-js": "^2.13.1",
"@sveltejs/repl": "^0.2.0", "@sveltejs/repl": "^0.2.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"devalue": "^4.3.0", "devalue": "^4.3.0",
"do-not-zip": "^1.0.0", "do-not-zip": "^1.0.0",
"flexsearch": "^0.7.31", "flexsearch": "^0.7.31",
"flru": "^1.0.2", "flru": "^1.0.2",
"sourcemap-codec": "^1.4.8", "sourcemap-codec": "^1.4.8"
"svelte-local-storage-store": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-vercel": "^2.4.1",
"@sveltejs/kit": "^1.15.0", "@sveltejs/kit": "^1.15.0",
"@sveltejs/site-kit": "^3.3.6", "@sveltejs/site-kit": "^3.3.6",
"@sveltejs/vite-plugin-svelte": "^2.0.4", "@sveltejs/vite-plugin-svelte": "^2.0.4",

@ -0,0 +1,13 @@
import { get_examples_data } from '../src/lib/server/examples/get-examples.js';
import fs from 'node:fs';
const examples_data = get_examples_data(
new URL('../../../site/content/examples', import.meta.url).pathname
);
fs.mkdirSync(new URL('../src/lib/generated/', import.meta.url), { recursive: true });
fs.writeFileSync(
new URL('../src/lib/generated/examples-data.js', import.meta.url),
`export default ${JSON.stringify(examples_data)}`
);

@ -1,21 +1,21 @@
<script> <script>
import Repl from '@sveltejs/repl';
import { onMount } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { process_example } from '$lib/utils/examples'; import { process_example } from '$lib/utils/examples';
import { PUBLIC_API_BASE } from '$env/static/public'; import Repl from '@sveltejs/repl';
import { onMount } from 'svelte';
export let version = '3'; export let version = '3';
export let gist = null; export let gist = null;
export let example = null; export let example = null;
export let embedded = false; export let embedded = false;
/** @type {import('@sveltejs/repl').default} */
let repl; let repl;
let name = 'loading...'; let name = 'loading...';
let mounted = false; let mounted = false;
function load(gist, example) { async function load(gist, example) {
if (version !== 'local') { if (version !== 'local') {
fetch(`https://unpkg.com/svelte@${version}/package.json`) fetch(`https://unpkg.com/svelte@${version}/package.json`)
.then((r) => r.json()) .then((r) => r.json())
@ -58,15 +58,12 @@
repl.set({ components }); repl.set({ components });
}); });
} else if (example) { } else if (example) {
fetch(`${PUBLIC_API_BASE}/docs/svelte/examples/${example}`).then(async (response) => { const components = process_example(
if (response.ok) { (await fetch(`/examples/api/${example}.json`).then((r) => r.json())).files
const data = await response.json(); );
const components = process_example(data.files);
repl.set({
repl.set({ components,
components,
});
}
}); });
} }
} }

@ -1,18 +1,15 @@
// @ts-check // @ts-check
import fs from 'node:fs'; import fs from 'node:fs';
const base = '../../site/content/examples/'; const BASE = '../../site/content/examples/';
/** /**
* @returns {import('./types').ExamplesData} * @returns {import('./types').ExamplesData}
*/ */
export function get_examples_data() { export function get_examples_data(base = BASE) {
const examples = []; const examples = [];
for (const subdir of fs.readdirSync(base)) { for (const subdir of fs.readdirSync(base)) {
// Exclude embeds
if (subdir.endsWith('99-embeds')) continue;
const section = { const section = {
title: '', // Initialise with empty title: '', // Initialise with empty
slug: subdir.split('-').slice(1).join('-'), slug: subdir.split('-').slice(1).join('-'),
@ -42,7 +39,7 @@ export function get_examples_data() {
.readdirSync(example_base_dir) .readdirSync(example_base_dir)
.filter((file) => !file.endsWith('meta.json'))) { .filter((file) => !file.endsWith('meta.json'))) {
files.push({ files.push({
filename: file, name: file,
type: file.split('.').at(-1), type: file.split('.').at(-1),
content: fs.readFileSync(`${example_base_dir}/${file}`, 'utf-8') content: fs.readFileSync(`${example_base_dir}/${file}`, 'utf-8')
}); });

@ -6,8 +6,8 @@ export type ExamplesData = {
slug: string; slug: string;
files: { files: {
content: string; content: string;
type: 'svelte' | 'js'; type: string;
filename: string; name: string;
}[]; }[];
}[]; }[];
}[]; }[];

@ -0,0 +1,24 @@
// @ts-check
import fs from 'node:fs';
import { extract_frontmatter } from '../markdown';
const base = '../../site/content/faq';
/**
* @returns {import('./types').FAQData}
*/
export function get_faq_data() {
const faqs = [];
for (const file of fs.readdirSync(base)) {
const { metadata, body } = extract_frontmatter(fs.readFileSync(`${base}/${file}`, 'utf-8'));
faqs.push({
title: metadata.question, // Initialise with empty
slug: file.split('-').slice(1).join('-').replace('.md', ''),
content: body,
});
}
return faqs;
}

@ -0,0 +1,87 @@
// @ts-check
import { createShikiHighlighter } from 'shiki-twoslash';
import { transform } from '../markdown';
const languages = {
bash: 'bash',
env: 'bash',
html: 'svelte',
svelte: 'svelte',
sv: 'svelte',
js: 'javascript',
css: 'css',
diff: 'diff',
ts: 'typescript',
'': '',
};
/**
* @param {import('./types').FAQData} faq_data
*/
export async function get_parsed_faq(faq_data) {
const highlighter = await createShikiHighlighter({ theme: 'css-variables' });
return Promise.all(
faq_data.map(async ({ content, slug, title }) => {
return {
title,
slug,
content: transform(content, {
/**
* @param {string} html
*/
heading(html) {
const title = html
.replace(/<\/?code>/g, '')
.replace(/&quot;/g, '"')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
return title;
},
code: (source, language) => {
let html = '';
source = source
.replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => {
if (prefix && language !== 'diff') return match;
// for no good reason at all, marked replaces tabs with spaces
let tabs = '';
for (let i = 0; i < spaces.length; i += 4) {
tabs += ' ';
}
return prefix + tabs;
})
.replace(/\*\\\//g, '*/');
html = highlighter.codeToHtml(source, { lang: languages[language] });
html = html
.replace(
/^(\s+)<span class="token comment">([\s\S]+?)<\/span>\n/gm,
(match, intro_whitespace, content) => {
// we use some CSS trickery to make comments break onto multiple lines while preserving indentation
const lines = (intro_whitespace + content + '').split('\n');
return lines
.map((line) => {
const match = /^(\s*)(.*)/.exec(line);
const indent = (match?.[1] ?? '').replace(/\t/g, ' ').length;
return `<span class="token comment wrapped" style="--indent: ${indent}ch">${
line ?? ''
}</span>`;
})
.join('');
}
)
.replace(/\/\*…\*\//g, '…');
return html;
},
codespan: (text) => '<code>' + text + '</code>',
}),
};
})
);
}

@ -0,0 +1,5 @@
export type FAQData = {
title: string;
slug: string;
content: string;
}[];

@ -2,12 +2,12 @@
import fs from 'node:fs'; import fs from 'node:fs';
import { extract_frontmatter } from '../markdown/index.js'; import { extract_frontmatter } from '../markdown/index.js';
const base = '../../site/content/tutorial/'; const BASE = '../../site/content/tutorial/';
/** /**
* @returns {import('./types').TutorialData} * @returns {import('./types').TutorialData}
*/ */
export function get_tutorial_data() { export function get_tutorial_data(base = BASE) {
const tutorials = []; const tutorials = [];
for (const subdir of fs.readdirSync(base)) { for (const subdir of fs.readdirSync(base)) {

@ -2,6 +2,6 @@ import * as session from '$lib/db/session';
export async function load({ request }) { export async function load({ request }) {
return { return {
user: await session.from_cookie(request.headers.get('cookie')) user: session.from_cookie(request.headers.get('cookie')),
}; };
} }

@ -1,15 +1,19 @@
import { error, json } from '@sveltejs/kit';
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import * as session from '$lib/db/session';
import { client } from '$lib/db/client'; import { client } from '$lib/db/client';
import * as gist from '$lib/db/gist'; import * as gist from '$lib/db/gist';
import { PUBLIC_API_BASE } from '$env/static/public'; import { get_example } from '$lib/server/examples';
import { get_examples_list } from '$lib/server/examples/get-examples';
import { error, json } from '@sveltejs/kit';
import examples_data from '$lib/generated/examples-data.js';
export const prerender = 'auto';
const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/; const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/;
/** @type {Set<string>} */ /** @type {Set<string>} */
let examples; let examples;
/** @param {import('$lib/server/examples/types').ExamplesData[number]['examples'][number]['files'][number][]} files */
function munge(files) { function munge(files) {
return files return files
.map((file) => { .map((file) => {
@ -18,6 +22,7 @@ function munge(files) {
let type = file.name.slice(dot + 1); let type = file.name.slice(dot + 1);
if (type === 'html') type = 'svelte'; if (type === 'html') type = 'svelte';
// @ts-expect-error what is file.source? by @PuruVJ
return { name, type, source: file.source ?? file.content ?? '' }; return { name, type, source: file.source ?? file.content ?? '' };
}) })
.sort((a, b) => { .sort((a, b) => {
@ -31,33 +36,25 @@ function munge(files) {
} }
export async function GET({ params }) { export async function GET({ params }) {
if (!examples) { // Currently, these pages(that are in examples/) are prerendered. To avoid making any FS requests,
const res = await fetch(`${PUBLIC_API_BASE}/docs/svelte/examples`); // We prerender examples pages during build time. That means, when something like `/repl/hello-world.json`
examples = new Set( // is accessed, this function won't be run at all, as it will be served from the filesystem
(await res.json())
.map((category) => category.examples)
.flat()
.map((example) => example.slug)
);
}
if (examples.has(params.id)) {
const res = await fetch(`${PUBLIC_API_BASE}/docs/svelte/examples/${params.id}`);
if (!res.ok) { examples = new Set(
return new Response(await res.json(), { get_examples_list(examples_data)
status: res.status, .map((category) => category.examples)
headers: { 'Content-Type': 'application/json' } .flat()
}); .map((example) => example.slug)
} );
const example = await res.json(); if (examples.has(params.id)) {
const example = get_example(examples_data, params.id);
return json({ return json({
id: params.id, id: params.id,
name: example.name, name: example.title,
owner: null, owner: null,
relaxed: example.relaxed, // TODO is this right? relaxed: false, // TODO is this right? EDIT: It was example.relaxed before, which no example return to my knowledge. By @PuruVJ
components: munge(example.files) components: munge(example.files)
}); });
} }
@ -65,7 +62,16 @@ export async function GET({ params }) {
if (dev && !client) { if (dev && !client) {
// in dev with no local Supabase configured, proxy to production // in dev with no local Supabase configured, proxy to production
// this lets us at least load saved REPLs // this lets us at least load saved REPLs
return await fetch(`https://svelte.dev/repl/${params.id}.json`); const res = await fetch(`https://svelte.dev/repl/${params.id}.json`);
// returning the response directly results in a bizarre
// content encoding error, so we create a new one
return new Response(await res.text(), {
status: res.status,
headers: {
'content-type': 'application/json'
}
});
} }
if (!UUID_REGEX.test(params.id)) { if (!UUID_REGEX.test(params.id)) {
@ -83,17 +89,7 @@ export async function GET({ params }) {
name: app.name, name: app.name,
owner: app.userid, owner: app.userid,
relaxed: false, relaxed: false,
// @ts-expect-error app.files has a `source` property
components: munge(app.files) components: munge(app.files)
}); });
} }
// TODO reimplement as an action
export async function PUT({ params, request }) {
const user = await session.from_cookie(request.headers.get('cookie'));
if (!user) throw error(401, 'Unauthorized');
const body = await request.json();
await gist.update(user, params.id, body);
return new Response(undefined, { status: 204 });
}

@ -11,6 +11,6 @@ export async function load({ fetch, params, url }) {
return { return {
gist, gist,
version: url.searchParams.get('version') || '3' version: url.searchParams.get('version') || '3',
}; };
} }

@ -106,7 +106,7 @@
// ~> Any missing files are considered deleted! // ~> Any missing files are considered deleted!
const { components } = repl.toJSON(); const { components } = repl.toJSON();
const r = await fetch(`/repl/${gist.id}.json`, { const r = await fetch(`/repl/save/${gist.id}.json`, {
method: 'PUT', method: 'PUT',
credentials: 'include', credentials: 'include',
headers: { headers: {

@ -2,7 +2,6 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import ReplWidget from '$lib/components/ReplWidget.svelte'; import ReplWidget from '$lib/components/ReplWidget.svelte';
/** @type {import('./$types').PageData} */
export let data; export let data;
</script> </script>

@ -0,0 +1,14 @@
import * as gist from '$lib/db/gist';
import * as session from '$lib/db/session';
import { error } from '@sveltejs/kit';
// TODO reimplement as an action
export async function PUT({ params, request }) {
const user = await session.from_cookie(request.headers.get('cookie'));
if (!user) throw error(401, 'Unauthorized');
const body = await request.json();
await gist.update(user, params.id, body);
return new Response(undefined, { status: 204 });
}

@ -15,36 +15,38 @@
</svelte:head> </svelte:head>
<Shell nav_visible={$page.url.pathname !== '/repl/embed'}> <Shell nav_visible={$page.url.pathname !== '/repl/embed'}>
<Nav logo="/svelte-logo.svg"> {#if $page.url.pathname !== '/repl/embed'}
<svelte:fragment slot="nav-center"> <Nav logo="/svelte-logo.svg">
{#if $page.url.pathname !== '/search'} <svelte:fragment slot="nav-center">
<li><Search /></li> {#if $page.url.pathname !== '/search'}
{/if} <li><Search /></li>
</svelte:fragment> {/if}
</svelte:fragment>
<svelte:fragment slot="nav-right"> <svelte:fragment slot="nav-right">
<NavItem href="/tutorial">Tutorial</NavItem> <NavItem href="/tutorial">Tutorial</NavItem>
<NavItem href="/docs/introduction">Docs</NavItem> <NavItem href="/docs/introduction">Docs</NavItem>
<NavItem href="/examples">Examples</NavItem> <NavItem href="/examples">Examples</NavItem>
<NavItem href="/repl">REPL</NavItem> <NavItem href="/repl">REPL</NavItem>
<NavItem href="/blog">Blog</NavItem> <NavItem href="/blog">Blog</NavItem>
<NavItem href="/faq">FAQ</NavItem> <NavItem href="/faq">FAQ</NavItem>
<Separator /> <Separator />
<NavItem external="https://kit.svelte.dev">SvelteKit</NavItem> <NavItem external="https://kit.svelte.dev">SvelteKit</NavItem>
<NavItem external="/chat" title="Discord Chat"> <NavItem external="/chat" title="Discord Chat">
<span slot="small">Discord</span> <span slot="small">Discord</span>
<Icon name="message-square" /> <Icon name="message-square" />
</NavItem> </NavItem>
<NavItem external="https://github.com/sveltejs/svelte" title="GitHub Repo"> <NavItem external="https://github.com/sveltejs/svelte" title="GitHub Repo">
<span slot="small">GitHub</span> <span slot="small">GitHub</span>
<Icon name="github" /> <Icon name="github" />
</NavItem> </NavItem>
</svelte:fragment> </svelte:fragment>
</Nav> </Nav>
{/if}
<slot /> <slot />
</Shell> </Shell>

@ -1,11 +1,11 @@
<script> <script>
import { Blurb } from '@sveltejs/site-kit/components'; import { Blurb } from '@sveltejs/site-kit/components';
import Supporters from './_components/Supporters/index.svelte'; import Balls from './svelte-balls.png?w=640;1280;2560;3840&format=avif;webp;png&picture';
import Demo from './_components/Demo.svelte';
import Hero from './_components/Hero.svelte'; import Hero from './_components/Hero.svelte';
import Image from './_components/Image.svelte'; import Image from './_components/Image.svelte';
import Demo from './_components/Demo.svelte'; import Supporters from './_components/Supporters/index.svelte';
import WhosUsingSvelte from './_components/WhosUsingSvelte/index.svelte'; import WhosUsingSvelte from './_components/WhosUsingSvelte/index.svelte';
import Balls from './svelte-balls.png?w=640;1280;2560;3840&format=avif;webp;png&picture';
</script> </script>
<svelte:head> <svelte:head>

@ -48,10 +48,12 @@
<a href="/examples">more <span class="large-show">&nbsp;examples</span> &rarr;</a> <a href="/examples">more <span class="large-show">&nbsp;examples</span> &rarr;</a>
</div> </div>
<Example id={selected.id} /> {#if selected}
<Example id={selected?.id} />
{/if}
</div> </div>
<p class="description">{@html selected.description}</p> <p class="description">{@html selected?.description}</p>
</Section> </Section>
<style> <style>

@ -16,7 +16,7 @@
let repl; let repl;
const clone = (file) => ({ const clone = (file) => ({
name: file.filename.replace(/.\w+$/, ''), name: file.name.replace(/.\w+$/, ''),
type: file.type, type: file.type,
source: file.content, source: file.content,
}); });

@ -14,31 +14,33 @@
<ul class="examples-toc"> <ul class="examples-toc">
{#each sections as section} {#each sections as section}
<li> {#if section.title !== undefined}
<span class="section-title">{section.title}</span> <li>
<span class="section-title">{section.title}</span>
{#each section.examples as example}
<div class="row" class:active={example.slug === active_section} class:loading={isLoading}> {#each section.examples as example}
<a <div class="row" class:active={example.slug === active_section} class:loading={isLoading}>
href="/examples/{example.slug}" <a
class="row" href="/examples/{example.slug}"
class:active={example.slug === active_section} class="row"
class:loading={isLoading} class:active={example.slug === active_section}
> class:loading={isLoading}
<img >
class="thumbnail" <img
alt="{example.title} thumbnail" class="thumbnail"
src="/examples/thumbnails/{example.slug}.jpg" alt="{example.title} thumbnail"
/> src="/examples/thumbnails/{example.slug}.jpg"
/>
<span>{example.title}</span>
</a> <span>{example.title}</span>
{#if example.slug === active_section} </a>
<a bind:this={active_el} href="/repl/{example.slug}" class="repl-link">REPL</a> {#if example.slug === active_section}
{/if} <a bind:this={active_el} href="/repl/{example.slug}" class="repl-link">REPL</a>
</div> {/if}
{/each} </div>
</li> {/each}
</li>
{/if}
{/each} {/each}
</ul> </ul>

@ -0,0 +1,8 @@
// @ts-check
import examples_data from '$lib/generated/examples-data.js';
import { get_examples_list } from '$lib/server/examples/get-examples';
import { json } from '@sveltejs/kit';
export const GET = () => {
return json(get_examples_list(examples_data));
};

@ -0,0 +1,17 @@
import examples_data from '$lib/generated/examples-data.js';
import { get_example } from '$lib/server/examples';
import { get_examples_list } from '$lib/server/examples/get-examples';
import { error, json } from '@sveltejs/kit';
export const GET = ({ params }) => {
const examples = new Set(
get_examples_list(examples_data)
.map((category) => category.examples)
.flat()
.map((example) => example.slug)
);
if (!examples.has(params.slug)) throw error(404, 'Example not found');
return json(get_example(examples_data, params.slug));
};

@ -1,12 +0,0 @@
import { PUBLIC_API_BASE } from '$env/static/public';
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
const faqs = await fetch(`${PUBLIC_API_BASE}/docs/svelte/faq?content`).then((r) => r.json());
setHeaders({
'cache-control': 'public, max-age=60'
});
return { faqs };
}

@ -0,0 +1,8 @@
import { get_parsed_faq } from '$lib/server/faq';
import { get_faq_data } from '$lib/server/faq/get-faq';
export const prerender = true;
export async function load() {
return { faqs: get_parsed_faq(get_faq_data()) };
}

@ -1,7 +1,6 @@
<script> <script>
import '@sveltejs/site-kit/styles/code.css'; import '@sveltejs/site-kit/styles/code.css';
/** @type {import('./$types').PageData} */
export let data; export let data;
</script> </script>
@ -85,11 +84,7 @@
@media (min-width: 768px) { @media (min-width: 768px) {
.faqs :global(.anchor:focus), .faqs :global(.anchor:focus),
.faqs :global(h2):hover :global(.anchor), .faqs :global(:where(h2, h3, h4, h5, h6)):hover :global(.anchor) {
.faqs :global(h3):hover :global(.anchor),
.faqs :global(h4):hover :global(.anchor),
.faqs :global(h5):hover :global(.anchor),
.faqs :global(h6):hover :global(.anchor) {
opacity: 1; opacity: 1;
} }

@ -1,5 +1,5 @@
import { get_parsed_tutorial } from '$lib/server/tutorial'; import { get_parsed_tutorial } from '$lib/server/tutorial';
import { get_tutorial_data, get_tutorial_list } from '$lib/server/tutorial/get-tutorial-data'; import { get_tutorial_data, get_tutorial_list } from '$lib/server/tutorial/get-tutorial';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
export const prerender = true; export const prerender = true;

@ -153,16 +153,6 @@
{/if} {/if}
</div> </div>
<!-- HACK to prerender -->
<p style="display: none;">
{#each data.tutorials_list as { tutorials }}
{#each tutorials as { slug }}
<!-- svelte-ignore a11y-missing-content -->
<a href="/tutorial/{slug}" />
{/each}
{/each}
</p>
<style> <style>
.tutorial-outer { .tutorial-outer {
position: relative; position: relative;

@ -1,9 +1,31 @@
// @ts-check // @ts-check
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-vercel';
import { get_examples_data, get_examples_list } from './src/lib/server/examples/get-examples.js';
import { get_tutorial_data, get_tutorial_list } from './src/lib/server/tutorial/get-tutorial.js';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
export default { export default {
kit: { kit: {
adapter: adapter() adapter: adapter(),
prerender: {
// TODO use route entries instead, once https://github.com/sveltejs/kit/pull/9571 is merged
entries: ['*', ...repl_json_entries(), ...tutorial_entries()]
}
} }
}; };
function repl_json_entries() {
return get_examples_list(
get_examples_data(new URL('../../site/content/examples', import.meta.url).pathname)
).flatMap(({ examples }) =>
examples.map(({ slug }) => /** @type {(`/${string}`)} */ (`/repl/${slug}.json`))
);
}
function tutorial_entries() {
return get_tutorial_list(
get_tutorial_data(new URL('../../site/content/tutorial', import.meta.url).pathname)
).flatMap(({ tutorials }) =>
tutorials.map(({ slug }) => /** @type {(`/${string}`)} */ (`/tutorial/${slug}`))
);
}

@ -50,6 +50,22 @@ const config = {
fs: { fs: {
strict: false strict: false
} }
},
// !HACK: Remove once vite 4.3 is out
worker: {
plugins: [
{
name: 'remove-manifest',
configResolved(c) {
const manifestPlugin = c.worker.plugins.findIndex((p) => p.name === 'vite:manifest');
c.worker.plugins.splice(manifestPlugin, 1);
const ssrManifestPlugin = c.worker.plugins.findIndex(
(p) => p.name === 'vite:ssr-manifest'
);
c.plugins.splice(ssrManifestPlugin, 1);
}
}
]
} }
}; };

@ -173,7 +173,7 @@ class Atrule {
} }
apply(node: Element) { apply(node: Element) {
if (this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') { if (this.node.name === 'container' || this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
this.children.forEach(child => { this.children.forEach(child => {
child.apply(node); child.apply(node);
}); });

@ -11,7 +11,7 @@ export type Context = DestructuredVariable | ComputedProperty;
interface ComputedProperty { interface ComputedProperty {
type: 'ComputedProperty'; type: 'ComputedProperty';
property_name: string; property_name: Identifier;
key: Expression | PrivateIdentifier; key: Expression | PrivateIdentifier;
} }
@ -30,8 +30,7 @@ export function unpack_destructuring({
default_modifier = (node) => node, default_modifier = (node) => node,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props = { n: 0 }
}: { }: {
contexts: Context[]; contexts: Context[];
node: Node; node: Node;
@ -40,10 +39,6 @@ export function unpack_destructuring({
scope: TemplateScope; scope: TemplateScope;
component: Component; component: Component;
context_rest_properties: Map<string, Node>; context_rest_properties: Map<string, Node>;
// we want to pass this by reference, as a sort of global variable, because
// if we pass this by value, we could get computed_property_# variable collisions
// when we deal with nested object destructuring
number_of_computed_props?: { n: number };
}) { }) {
if (!node) return; if (!node) return;
@ -72,8 +67,7 @@ export function unpack_destructuring({
default_modifier, default_modifier,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props
}); });
context_rest_properties.set((element.argument as Identifier).name, element); context_rest_properties.set((element.argument as Identifier).name, element);
} else if (element && element.type === 'AssignmentPattern') { } else if (element && element.type === 'AssignmentPattern') {
@ -93,8 +87,7 @@ export function unpack_destructuring({
)}` as Node, )}` as Node,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props
}); });
} else { } else {
unpack_destructuring({ unpack_destructuring({
@ -104,8 +97,7 @@ export function unpack_destructuring({
default_modifier, default_modifier,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props
}); });
} }
}); });
@ -124,8 +116,7 @@ export function unpack_destructuring({
default_modifier, default_modifier,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props
}); });
context_rest_properties.set((property.argument as Identifier).name, property); context_rest_properties.set((property.argument as Identifier).name, property);
} else if (property.type === 'Property') { } else if (property.type === 'Property') {
@ -136,8 +127,7 @@ export function unpack_destructuring({
if (property.computed) { if (property.computed) {
// e.g { [computedProperty]: ... } // e.g { [computedProperty]: ... }
const property_name = `computed_property_${number_of_computed_props.n}`; const property_name = component.get_unique_name('computed_property');
number_of_computed_props.n += 1;
contexts.push({ contexts.push({
type: 'ComputedProperty', type: 'ComputedProperty',
@ -178,8 +168,7 @@ export function unpack_destructuring({
)}` as Node, )}` as Node,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props
}); });
} else { } else {
// e.g. { property } or { property: newName } // e.g. { property } or { property: newName }
@ -190,8 +179,7 @@ export function unpack_destructuring({
default_modifier, default_modifier,
scope, scope,
component, component,
context_rest_properties, context_rest_properties
number_of_computed_props
}); });
} }
} }

@ -0,0 +1,41 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Comment from '../../nodes/Comment';
import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import { Identifier } from 'estree';
export default class CommentWrapper extends Wrapper {
node: Comment;
var: Identifier;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Comment
) {
super(renderer, block, parent, node);
this.var = x`c` as Identifier;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (!this.renderer.options.preserveComments) return;
const string_literal = {
type: 'Literal',
value: this.node.data,
loc: {
start: this.renderer.locate(this.node.start),
end: this.renderer.locate(this.node.end)
}
};
block.add_element(
this.var,
x`@comment(${string_literal})`,
parent_nodes && x`@claim_comment(${parent_nodes}, ${string_literal})`,
parent_node
);
}
}

@ -364,7 +364,6 @@ const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & { [k
indeterminate: { applies_to: ['input'] }, indeterminate: { applies_to: ['input'] },
inert: {}, inert: {},
ismap: { property_name: 'isMap', applies_to: ['img'] }, ismap: { property_name: 'isMap', applies_to: ['img'] },
itemscope: {},
loop: { applies_to: ['audio', 'bgsound', 'video'] }, loop: { applies_to: ['audio', 'bgsound', 'video'] },
multiple: { applies_to: ['input', 'select'] }, multiple: { applies_to: ['input', 'select'] },
muted: { applies_to: ['audio', 'video'] }, muted: { applies_to: ['audio', 'video'] },

@ -14,6 +14,7 @@ import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot'; import Slot from './Slot';
import SlotTemplate from './SlotTemplate'; import SlotTemplate from './SlotTemplate';
import Text from './Text'; import Text from './Text';
import Comment from './Comment';
import Title from './Title'; import Title from './Title';
import Window from './Window'; import Window from './Window';
import { INode } from '../../nodes/interfaces'; import { INode } from '../../nodes/interfaces';
@ -27,7 +28,7 @@ import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = { const wrappers = {
AwaitBlock, AwaitBlock,
Body, Body,
Comment: null, Comment,
DebugTag, DebugTag,
Document, Document,
EachBlock, EachBlock,
@ -118,7 +119,7 @@ export default class FragmentWrapper {
link(last_child, last_child = wrapper); link(last_child, last_child = wrapper);
} else { } else {
const Wrapper = wrappers[child.type]; const Wrapper = wrappers[child.type];
if (!Wrapper) continue; if (!Wrapper || (child.type === 'Comment' && !renderer.options.preserveComments)) continue;
const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling); const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling);
this.nodes.unshift(wrapper); this.nodes.unshift(wrapper);

@ -19,11 +19,17 @@ export function get_class_attribute_value(attribute: Attribute): ESTreeExpressio
export function get_attribute_value(attribute: Attribute): ESTreeExpression { export function get_attribute_value(attribute: Attribute): ESTreeExpression {
if (attribute.chunks.length === 0) return x`""`; if (attribute.chunks.length === 0) return x`""`;
/**
* For value attribute of textarea, it will render as child node of `<textarea>` element.
* Therefore, we need to escape as content (not attribute).
*/
const is_textarea_value = attribute.parent.name.toLowerCase() === 'textarea' && attribute.name.toLowerCase() === 'value';
return attribute.chunks return attribute.chunks
.map((chunk) => { .map((chunk) => {
return chunk.type === 'Text' return chunk.type === 'Text'
? string_literal(chunk.data.replace(regex_double_quotes, '&quot;')) as ESTreeExpression ? string_literal(chunk.data.replace(regex_double_quotes, '&quot;')) as ESTreeExpression
: x`@escape(${chunk.node}, true)`; : x`@escape(${chunk.node}, ${is_textarea_value ? 'false' : 'true'})`;
}) })
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`); .reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
} }

@ -0,0 +1,31 @@
// @ts-nocheck
// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from
// `css-tree` Node module directly. This allows the production build of Svelte to work correctly.
import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js';
import * as node from './node';
/**
* Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`.
*
* The new nodes are located in `./node`.
*/
const cqSyntax = fork({
atrule: { // extend or override at-rule dictionary
container: {
parse: {
prelude() {
return this.createSingleNodeList(
this.ContainerQuery()
);
},
block(isStyleBlock = false) {
return this.Block(isStyleBlock);
}
}
}
},
node
});
export const parse = cqSyntax.parse;

@ -0,0 +1,48 @@
// @ts-nocheck
import { Delim } from 'css-tree/tokenizer';
export const name = 'Comparison';
export const structure = {
value: String
};
export function parse() {
const start = this.tokenStart;
const char1 = this.consume(Delim);
// The first character in the comparison operator must match '<', '=', or '>'.
if (char1 !== '<' && char1 !== '>' && char1 !== '=') {
this.error('Malformed comparison operator');
}
let char2;
if (this.tokenType === Delim) {
char2 = this.consume(Delim);
// The second character in the comparison operator must match '='.
if (char2 !== '=') {
this.error('Malformed comparison operator');
}
}
// If the next token is also 'Delim' then it is malformed.
if (this.tokenType === Delim) {
this.error('Malformed comparison operator');
}
const value = char2 ? `${char1}${char2}` : char1;
return {
type: 'Comparison',
loc: this.getLocation(start, this.tokenStart),
value
};
}
export function generate(node) {
for (let index = 0; index < node.value.length; index++) {
this.token(Delim, node.value.charAt(index));
}
}

@ -0,0 +1,85 @@
// @ts-nocheck
import {
Function,
Ident,
Number,
Dimension,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';
export const name = 'ContainerFeatureStyle';
export const structure = {
name: String,
value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
export function parse() {
const start = this.tokenStart;
let value = null;
const function_name = this.consumeFunctionName();
if (function_name !== 'style') {
this.error('Unknown container style query identifier; "style" is expected');
}
this.skipSC();
const name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case Dimension:
value = this.Dimension();
break;
case Function:
value = this.QueryCSSFunction();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio, function or identifier is expected');
break;
}
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'ContainerFeatureStyle',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
export function generate(node) {
this.token(Function, 'style(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
}

@ -0,0 +1,92 @@
// @ts-nocheck
import {
WhiteSpace,
Comment,
Function,
Ident,
LeftParenthesis
} from 'css-tree/tokenizer';
import { lookahead_is_range } from './lookahead_is_range';
const CONTAINER_QUERY_KEYWORDS = new Set(['none', 'and', 'not', 'or']);
export const name = 'ContainerQuery';
export const structure = {
name: 'Identifier',
children: [[
'Identifier',
'QueryFeature',
'QueryFeatureRange',
'ContainerFeatureStyle',
'WhiteSpace'
]]
};
export function parse() {
const start = this.tokenStart;
const children = this.createList();
let child = null;
let name = null;
// Parse potential container name.
if (this.tokenType === Ident) {
const container_name = this.substring(this.tokenStart, this.tokenEnd);
// Container name doesn't match a query keyword, so assign it as container name.
if (!CONTAINER_QUERY_KEYWORDS.has(container_name.toLowerCase())) {
name = container_name;
this.eatIdent(container_name);
}
}
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Ident:
child = this.Identifier();
break;
case Function:
child = this.ContainerFeatureStyle();
break;
case LeftParenthesis:
// Lookahead to determine if range feature.
child = lookahead_is_range.call(this) ? this.QueryFeatureRange() : this.QueryFeature();
break;
default:
break scan;
}
children.push(child);
}
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
return {
type: 'ContainerQuery',
loc: this.getLocation(start, this.tokenStart - 1),
name,
children
};
}
export function generate(node) {
if (typeof node.name === 'string') {
this.token(Ident, node.name);
}
this.children(node);
}

@ -0,0 +1,7 @@
export * as Comparison from './comparison';
export * as ContainerFeatureStyle from './container_feature_style';
export * as ContainerQuery from './container_query';
export * as MediaQuery from './media_query';
export * as QueryFeature from './query_feature';
export * as QueryFeatureRange from './query_feature_range';
export * as QueryCSSFunction from './query_css_function';

@ -0,0 +1,44 @@
// @ts-nocheck
import {
EOF,
WhiteSpace,
Delim,
RightParenthesis,
LeftCurlyBracket,
Colon
} from 'css-tree/tokenizer';
/**
* Looks ahead to determine if query feature is a range query. This involves locating at least one delimiter and no
* colon tokens.
*
* @returns {boolean} Is potential range query.
*/
export function lookahead_is_range() {
let type;
let offset = 0;
let count = 0;
let delim_found = false;
let no_colon = true;
// A range query has maximum 5 tokens when formatted as 'mf-range' /
// '<mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>'. So only look ahead maximum of 6 non-whitespace tokens.
do {
type = this.lookupNonWSType(offset++);
if (type !== WhiteSpace) {
count++;
}
if (type === Delim) {
delim_found = true;
}
if (type === Colon) {
no_colon = false;
}
if (type === LeftCurlyBracket || type === RightParenthesis) {
break;
}
} while (type !== EOF && count <= 6);
return delim_found && no_colon;
}

@ -0,0 +1,64 @@
// @ts-nocheck
import {
WhiteSpace,
Comment,
Ident,
LeftParenthesis
} from 'css-tree/tokenizer';
import { lookahead_is_range } from './lookahead_is_range';
export const name = 'MediaQuery';
export const structure = {
children: [[
'Identifier',
'QueryFeature',
'QueryFeatureRange',
'WhiteSpace'
]]
};
export function parse() {
const children = this.createList();
let child = null;
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Ident:
child = this.Identifier();
break;
case LeftParenthesis:
// Lookahead to determine if range feature.
child = lookahead_is_range.call(this) ? this.QueryFeatureRange() : this.QueryFeature();
break;
default:
break scan;
}
children.push(child);
}
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
return {
type: 'MediaQuery',
loc: this.getLocationFromList(children),
children
};
}
export function generate(node) {
this.children(node);
}

@ -0,0 +1,41 @@
// @ts-nocheck
import {
RightParenthesis
} from 'css-tree/tokenizer';
const QUERY_CSS_FUNCTIONS = new Set(['calc', 'clamp', 'min', 'max']);
export const name = 'QueryCSSFunction';
export const structure = {
name: String,
expression: String
};
export function parse() {
const start = this.tokenStart;
const name = this.consumeFunctionName();
if (!QUERY_CSS_FUNCTIONS.has(name)) {
this.error('Unknown query single value function; expected: "calc", "clamp", "max", min"');
}
const body = this.Raw(this.tokenIndex, null, false);
this.eat(RightParenthesis);
return {
type: 'QueryCSSFunction',
loc: this.getLocation(start, this.tokenStart),
name,
expression: body.value
};
}
export function generate(node) {
this.token(Function, `${node.name}(`);
this.node(node.expression);
this.token(RightParenthesis, ')');
}

@ -0,0 +1,82 @@
// @ts-nocheck
import {
Ident,
Number,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';
export const name = 'QueryFeature';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
export function parse() {
const start = this.tokenStart;
let value = null;
this.eat(LeftParenthesis);
this.skipSC();
const name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case Dimension:
value = this.Dimension();
break;
case Function:
value = this.QueryCSSFunction();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio, function, or identifier is expected');
break;
}
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'QueryFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
export function generate(node) {
this.token(LeftParenthesis, '(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
}

@ -0,0 +1,87 @@
// @ts-nocheck
import {
Ident,
Number,
Delim,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
WhiteSpace
} from 'css-tree/tokenizer';
export const name = 'QueryFeatureRange';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
function lookup_non_WS_type_and_value(offset, type, referenceStr) {
let current_type;
do {
current_type = this.lookupType(offset++);
if (current_type !== WhiteSpace) {
break;
}
} while (current_type !== 0); // NULL -> 0
return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false;
}
export function parse() {
const start = this.tokenStart;
const children = this.createList();
let child = null;
this.eat(LeftParenthesis);
this.skipSC();
while (!this.eof && this.tokenType !== RightParenthesis) {
switch (this.tokenType) {
case Number:
if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) {
child = this.Ratio();
} else {
child = this.Number();
}
break;
case Delim:
child = this.Comparison();
break;
case Dimension:
child = this.Dimension();
break;
case Function:
child = this.QueryCSSFunction();
break;
case Ident:
child = this.Identifier();
break;
default:
this.error('Number, dimension, comparison, ratio, function, or identifier is expected');
break;
}
children.push(child);
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'QueryFeatureRange',
loc: this.getLocation(start, this.tokenStart),
children
};
}
export function generate(node) {
this.children(node);
}

@ -1,5 +1,6 @@
// @ts-ignore // @ts-ignore
import parse from 'css-tree/parser'; // import parse from 'css-tree/parser'; // When css-tree supports container queries uncomment.
import { parse } from './css-tree-cq/css_tree_parse'; // Use extended css-tree for container query support.
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { Parser } from '../index'; import { Parser } from '../index';
import { Node } from 'estree'; import { Node } from 'estree';

@ -164,8 +164,9 @@ export interface SvelteComponentDev {
$destroy(): void; $destroy(): void;
[accessor: string]: any; [accessor: string]: any;
} }
export interface ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>> { export interface ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>> {
target: Element | ShadowRoot; target: Element | Document | ShadowRoot;
anchor?: Element; anchor?: Element;
props?: Props; props?: Props;
context?: Map<any, any>; context?: Map<any, any>;

@ -254,6 +254,10 @@ export function empty() {
return text(''); return text('');
} }
export function comment(content: string) {
return document.createComment(content);
}
export function listen(node: EventTarget, 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);
@ -550,6 +554,19 @@ export function claim_space(nodes) {
return claim_text(nodes, ' '); return claim_text(nodes, ' ');
} }
export function claim_comment(nodes:ChildNodeArray, data) {
return claim_node<Comment>(
nodes,
(node: ChildNode): node is Comment => node.nodeType === 8,
(node: Comment) => {
node.data = '' + data;
return undefined;
},
() => comment(data),
true
);
}
function find_comment(nodes, text, start) { function find_comment(nodes, text, start) {
for (let i = start; i < nodes.length; i += 1) { for (let i = start; i < nodes.length; i += 1) {
const node = nodes[i]; const node = nodes[i];

@ -13,7 +13,6 @@ const _boolean_attributes = [
'hidden', 'hidden',
'inert', 'inert',
'ismap', 'ismap',
'itemscope',
'loop', 'loop',
'multiple', 'multiple',
'muted', 'muted',

@ -0,0 +1 @@
div.svelte-xyz{container:test-container / inline-size}@container (min-width: 400px){div.svelte-xyz{color:red}}@container test-container (min-width: 410px){div.svelte-xyz{color:green}}@container test-container (width < 400px){div.svelte-xyz{color:blue}}@container test-container (0 <= width < 300px){div.svelte-xyz{color:purple}}@container not (width < 400px){div.svelte-xyz{color:pink}}@container (width > 400px) and (height > 400px){div.svelte-xyz{color:lightgreen}}@container (width > 400px) or (height > 400px){div.svelte-xyz{color:lightblue}}@container (width > 400px) and (width > 800px) or (orientation: portrait){div.svelte-xyz{color:salmon}}@container style(color: blue){div.svelte-xyz{color:tan}}@container test-container (min-width: calc(400px + 1px)){div.svelte-xyz{color:green}}@container test-container (width < clamp(200px, 40%, 400px)){div.svelte-xyz{color:blue}}@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)){div.svelte-xyz{color:purple}}@container style(--var: calc(400px + 1px)){div.svelte-xyz{color:sandybrown}}

@ -0,0 +1,87 @@
<div>container query</div>
<style>
div {
container: test-container / inline-size;
}
/* Most common container query statements. */
@container (min-width: 400px) {
div {
color: red;
}
}
@container test-container (min-width: 410px) {
div {
color: green;
}
}
@container test-container (width < 400px) {
div {
color: blue;
}
}
@container test-container (0 <= width < 300px) {
div {
color: purple;
}
}
@container not (width < 400px) {
div {
color: pink;
}
}
@container (width > 400px) and (height > 400px) {
div {
color: lightgreen;
}
}
@container (width > 400px) or (height > 400px) {
div {
color: lightblue;
}
}
@container (width > 400px) and (width > 800px) or (orientation: portrait) {
div {
color: salmon;
}
}
@container style(color: blue) {
div {
color: tan;
}
}
@container test-container (min-width: calc(400px + 1px)) {
div {
color: green;
}
}
@container test-container (width < clamp(200px, 40%, 400px)) {
div {
color: blue;
}
}
@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)) {
div {
color: purple;
}
}
@container style(--var: calc(400px + 1px)) {
div {
color: sandybrown;
}
}
</style>

@ -1 +1 @@
@media(min-width: 400px){.large-screen.svelte-xyz{display:block}} @media(min-width: 400px){.large-screen.svelte-xyz{display:block}}@media(min-width: calc(400px + 1px)){.large-screen.svelte-xyz{display:block}}@media(width >= 600px){.large-screen.svelte-xyz{display:block}}@media(400px <= width <= 1000px){.large-screen.svelte-xyz{display:block}}@media(width < clamp(200px, 40%, 400px)){.large-screen.svelte-xyz{display:block}}@media(calc(400px + 1px) <= width <= calc(1000px + 1px)){.large-screen.svelte-xyz{display:block}}

@ -6,4 +6,34 @@
display: block; display: block;
} }
} }
@media (min-width: calc(400px + 1px)) {
.large-screen {
display: block;
}
}
@media (width >= 600px) {
.large-screen {
display: block;
}
}
@media (400px <= width <= 1000px) {
.large-screen {
display: block;
}
}
@media (width < clamp(200px, 40%, 400px)) {
.large-screen {
display: block;
}
}
@media (calc(400px + 1px) <= width <= calc(1000px + 1px)) {
.large-screen {
display: block;
}
}
</style> </style>

@ -0,0 +1 @@
<div><!-- test1 --><!-- test2 --></div>

@ -0,0 +1,20 @@
export default {
compileOptions: {
preserveComments:true
},
snapshot(target) {
const div = target.querySelector('div');
return {
div,
comment: div.childNodes[0]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.childNodes[0], snapshot.comment);
assert.equal(div.childNodes[1].nodeType, 8);
}
};

@ -0,0 +1 @@
<div><!-- test1 --><!-- test2 --></div>

@ -0,0 +1,5 @@
export default {
options: {
preserveComments: true
}
};

@ -0,0 +1,58 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
comment,
detach,
element,
init,
insert,
noop,
safe_not_equal,
space
} from "svelte/internal";
function create_fragment(ctx) {
let div0;
let t1;
let c;
let t2;
let div1;
return {
c() {
div0 = element("div");
div0.textContent = "content";
t1 = space();
c = comment(" comment ");
t2 = space();
div1 = element("div");
div1.textContent = "more content";
},
m(target, anchor) {
insert(target, div0, anchor);
insert(target, t1, anchor);
insert(target, c, anchor);
insert(target, t2, anchor);
insert(target, div1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div0);
if (detaching) detach(t1);
if (detaching) detach(c);
if (detaching) detach(t2);
if (detaching) detach(div1);
}
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,3 @@
<div>content</div>
<!-- comment -->
<div>more content</div>

@ -0,0 +1,10 @@
export default {
props: {
hidden: true
},
html: '<div hidden />',
test({ assert, component, target }) {
component.hidden = false;
assert.htmlEqual(target.innerHTML, '<div />');
}
};

@ -0,0 +1,5 @@
<script>
export let hidden = false;
</script>
<div {hidden} />

@ -1,11 +0,0 @@
export default {
props: {
itemscope: true
},
test({ assert, target, component }) {
const div = target.querySelector('div');
assert.ok(div.itemscope);
component.itemscope = false;
assert.ok(!div.itemscope);
}
};

@ -1,5 +0,0 @@
<script>
export let itemscope;
</script>
<div {itemscope} />

@ -0,0 +1,4 @@
export default {
html: '<textarea></textarea>',
ssrHtml: '<textarea>test\'"&gt;&lt;/textarea&gt;&lt;script&gt;alert(\'BIM\');&lt;/script&gt;</textarea>'
};

@ -0,0 +1 @@
<textarea value={`test'"></textarea><script>alert('BIM');</script>`} />

@ -0,0 +1,25 @@
// There is no relationship between the attribute and the dom node with regards to microdata attributes https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata
export default {
html: `<div itemscope itemtype="https://schema.org/SoftwareApplication">
<span itemprop="name">Game</span> - REQUIRES
<span itemprop="operatingSystem">OS</span><br/>
<link itemprop="applicationCategory" href="https://schema.org/GameApplication"/>
<div itemprop="aggregateRating" itemscope="" itemtype="https://schema.org/AggregateRating">RATING:
<span itemprop="ratingValue">4.6</span> (
<span itemprop="ratingCount">8864</span> ratings )</div>
<div itemref="offers"></div>
</div>
<div
itemprop="offers"
itemid="offers"
id="offers"
itemscope
itemtype="https://schema.org/Offer"
>
Price: $<span itemprop="price">1.00</span>
<meta itemprop="priceCurrency" content="USD"/>
</div>
`
};

@ -0,0 +1,31 @@
<!-- Example from https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata -->
<div itemscope itemtype="https://schema.org/SoftwareApplication">
<span itemprop="name">Game</span> - REQUIRES
<span itemprop="operatingSystem">OS</span><br />
<link
itemprop="applicationCategory"
href="https://schema.org/GameApplication"
/>
<div
itemprop="aggregateRating"
itemscope
itemtype="https://schema.org/AggregateRating"
>
RATING:
<span itemprop="ratingValue">4.6</span> (
<span itemprop="ratingCount">8864</span> ratings )
</div>
<div itemref="offers" />
</div>
<div
itemprop="offers"
itemid="offers"
id="offers"
itemscope
itemtype="https://schema.org/Offer"
>
Price: $<span itemprop="price">1.00</span>
<meta itemprop="priceCurrency" content="USD" />
</div>

@ -0,0 +1,20 @@
export default {
html: `
<p>4, 12, 60</p>
`,
async test({ component, target, assert }) {
component.permutation = [2, 3, 1];
await (component.promise1 = Promise.resolve({length: 1, width: 2, height: 3}));
try {
await (component.promise2 = Promise.reject({length: 97, width: 98, height: 99}));
} catch (e) {
// nothing
}
assert.htmlEqual(target.innerHTML, `
<p>2, 11, 2</p>
<p>9506, 28811, 98</p>
`);
}
};

@ -0,0 +1,27 @@
<script>
export let promise1 = {length: 5, width: 3, height: 4};
export let promise2 = {length: 12, width: 5, height: 13};
export let permutation = [1, 2, 3];
function calculate(length, width, height) {
return {
'1-Dimensions': [length, width, height],
'2-Dimensions': [length * width, width * height, length * height],
'3-Dimensions': [length * width * height, length + width + height, length * width + width * height + length * height]
};
}
const th = 'th';
</script>
{#await promise1 then { length, width, height }}
{@const { [0]: a, [1]: b, [2]: c } = permutation}
{@const { [`${a}-Dimensions`]: { [c - 1]: first }, [`${b}-Dimensions`]: { [b - 1]: second }, [`${c}-Dimensions`]: { [a - 1]: third } } = calculate(length, width, height) }
<p>{first}, {second}, {third}</p>
{/await}
{#await promise2 catch { [`leng${th}`]: l, [`wid${th}`]: w, height: h }}
{@const [a, b, c] = permutation}
{@const { [`${a}-Dimensions`]: { [c - 1]: first }, [`${b}-Dimensions`]: { [b - 1]: second }, [`${c}-Dimensions`]: { [a - 1]: third } } = calculate(l, w, h) }
<p>{first}, {second}, {third}</p>
{/await}

@ -0,0 +1,15 @@
export default {
html: `
<button>6, 12, 8, 24</button>
<button>45, 35, 63, 315</button>
<button>60, 48, 80, 480</button>
`,
async test({ component, target, assert }) {
component.boxes = [{ length: 10, width: 20, height: 30 }];
assert.htmlEqual(target.innerHTML,
'<button>200, 600, 300, 6000</button>'
);
}
};

@ -0,0 +1,44 @@
<script>
export let boxes = [
{length: 2, width: 3, height: 4},
{length: 9, width: 5, height: 7},
{length: 10, width: 6, height: 8}
];
function calculate(length, width, height) {
return {
twoDimensions: {
bottomArea: length * width,
sideArea1: width * height,
sideArea2: length * height
},
threeDimensions: {
volume: length * width * height
}
};
}
export let dimension = 'Dimensions';
function changeDimension() {
dimension = 'DIMENSIONS';
}
let area = 'Area';
let th = 'th';
</script>
{#each boxes as { [`leng${th}`]: length, [`wid${th}`]: width, height }}
{@const {
[`two${dimension}`]: areas,
[`three${dimension}`]: {
volume
}
} = calculate(length, width, height)}
{@const {
i = 1,
[`bottom${area}`]: bottom,
[`side${area}${i++}`]: sideone,
[`side${area}${i++}`]: sidetwo
} = areas}
<button on:click={changeDimension}>{bottom}, {sideone}, {sidetwo}, {volume}</button>
{/each}
Loading…
Cancel
Save