Merge remote-tracking branch 'upstream/version-4' into tanlh/feat/action-void-parameter-type

pull/7442/head
Ben McCann 3 years ago
commit abcc7c9d32

@ -6,47 +6,12 @@ on:
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
Setup:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- run: npm install
env:
SKIP_PREPARE: true
- run: npm run build
env:
PUBLISH: true
- name: Upload build assets
id: upload-artifact
uses: actions/upload-artifact@v3
with:
name: build-assets
path: |
index.*
compiler.*
ssr.*
action/
animate/
easing/
internal/
motion/
store/
transition/
types/
Tests: Tests:
needs: Setup
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 15 timeout-minutes: 15
strategy: strategy:
matrix: matrix:
node-version: [8, 10, 12, 14, 16, 18] node-version: [14, 16, 18]
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -54,24 +19,7 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- name: Download build assets
uses: actions/download-artifact@v3
id: download-artifact
with:
name: build-assets
- name: Get Node version ${{ runner.os }}
run: echo "NODE_VERSION=`node --version`" >> $GITHUB_ENV
if: runner.os != 'Windows'
- name: Get Node version ${{ runner.os }}
run: |
chcp 65001
echo ("NODE_VERSION=$(node --version)") >> $env:GITHUB_ENV
if: runner.os == 'Windows'
- run: npm install --save-dev puppeteer@13
if: ${{ runner.os == 'Linux' && (!startsWith(env.NODE_VERSION, 'v8.') && !startsWith(env.NODE_VERSION, 'v10.')) }}
- run: npm install - run: npm install
env:
SKIP_PREPARE: true
- run: npm run test:integration - run: npm run test:integration
env: env:
CI: true CI: true

@ -1,32 +1,80 @@
# Svelte changelog # Svelte changelog
## Unreleased ## Unreleased (4.0)
* Minimum supported Node version is now Node 14
## Unreleased (3.0)
* Handle `width`/`height` attributes when spreading ([#6752](https://github.com/sveltejs/svelte/issues/6752))
* Add support for resize observer bindings (`<div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>`) ([#8022](https://github.com/sveltejs/svelte/pull/8022))
## 3.58.0
* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))
* 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))
* 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
* Add `<svelte:document>` ([#3310](https://github.com/sveltejs/svelte/issues/3310))
* Add a11y `no-noninteractive-element-to-interactive-role` ([#8167](https://github.com/sveltejs/svelte/pull/8167))
* Stop intro transition from triggering incorrectly ([#6152](https://github.com/sveltejs/svelte/issues/6152), [#6812](https://github.com/sveltejs/svelte/issues/6812))
* Support computed and literal properties when destructuring objects in the template ([#6609](https://github.com/sveltejs/svelte/issues/6609))
* Give `style:` directive precedence over `style=` attribute ([#7475](https://github.com/sveltejs/svelte/issues/7475))
* Select `<option>` with `selected` attribute when initial state is `undefined` ([#8361](https://github.com/sveltejs/svelte/issues/8361))
* Prevent derived store callbacks after store is unsubscribed from ([#8364](https://github.com/sveltejs/svelte/issues/8364))
* Account for `bind:group` members being spread across multiple control flow blocks ([#8372](https://github.com/sveltejs/svelte/issues/8372))
* Revert buggy reactive statement optimization ([#8374](https://github.com/sveltejs/svelte/issues/8374))
* Support CSS units in the `fly` and `blur` transitions ([#7623](https://github.com/sveltejs/svelte/pull/7623))
## 3.56.0
* Add `|stopImmediatePropagation` event modifier ([#5085](https://github.com/sveltejs/svelte/issues/5085))
* Add `axis` parameter to `slide` transition ([#6182](https://github.com/sveltejs/svelte/issues/6182))
* Add `readonly` utility to convert `writable` store to readonly ([#6518](https://github.com/sveltejs/svelte/pull/6518))
* Add `readyState` binding for media elements ([#6666](https://github.com/sveltejs/svelte/issues/6666))
* Generate valid automatic component names when the filename contains only special characters ([#7143](https://github.com/sveltejs/svelte/issues/7143))
* Add `naturalWidth` and `naturalHeight` bindings ([#7771](https://github.com/sveltejs/svelte/issues/7771))
* Support `<!-- svelte-ignore ... -->` on components ([#8082](https://github.com/sveltejs/svelte/issues/8082))
* Add a11y warnings: * Add a11y warnings:
* `aria-activedescendant-has-tabindex`: elements with `aria-activedescendant` need to have a `tabindex` ([#8172](https://github.com/sveltejs/svelte/pull/8172)) * `aria-activedescendant-has-tabindex`: checks that elements with `aria-activedescendant` have a `tabindex` ([#8172](https://github.com/sveltejs/svelte/pull/8172))
* `role-supports-aria-props`: checks that the (implicit) element role supports the given aria attributes ([#8195](https://github.com/sveltejs/svelte/pull/8195)) * `role-supports-aria-props`: checks that the (implicit) element role supports the given aria attributes ([#8195](https://github.com/sveltejs/svelte/pull/8195))
* Omit a11y warning on `<video>` tags with `aria-hidden="true"` ([#7874](https://github.com/sveltejs/svelte/issues/7874))
* Omit a11y "no child content" warning on elements with `aria-label` ([#8299](https://github.com/sveltejs/svelte/pull/8299))
* Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289))
* `trusted-types` CSP compatibility for Web Components ([#8134](https://github.com/sveltejs/svelte/issues/8134))
* Add `data-sveltekit-replacestate` and `data-sveltekit-keepfocus` attribute typings ([#8281](https://github.com/sveltejs/svelte/issues/8281)) * Add `data-sveltekit-replacestate` and `data-sveltekit-keepfocus` attribute typings ([#8281](https://github.com/sveltejs/svelte/issues/8281))
* Don't throw when calling `unsubscribe` twice ([#8186](https://github.com/sveltejs/svelte/pull/8186)) * Compute node dimensions immediately before crossfading ([#4111](https://github.com/sveltejs/svelte/issues/4111))
* Detect unused empty attribute CSS selectors ([#8042](https://github.com/sveltejs/svelte/issues/8042)) * Fix potential infinite invalidate loop with `<svelte:component>` ([#4129](https://github.com/sveltejs/svelte/issues/4129))
* Simpler output for reactive statements if dependencies are all static ([#7942](https://github.com/sveltejs/svelte/pull/7942))
* Flush remaining `afterUpdate` calls before `onDestroy` ([#7476](https://github.com/sveltejs/svelte/issues/7476))
* Check value equality for `input` types `url` and `search` ([#7027](https://github.com/sveltejs/svelte/issues/7027))
* Compute node dimensions directly before crossfading ([#4111](https://github.com/sveltejs/svelte/issues/4111))
* Add `readonly` method to convert `writable` store to readonly ([#6518](https://github.com/sveltejs/svelte/pull/6518))
* Ensure `bind:offsetHeight` updates initially ([#4233](https://github.com/sveltejs/svelte/issues/4233)) * Ensure `bind:offsetHeight` updates initially ([#4233](https://github.com/sveltejs/svelte/issues/4233))
* Better handling of `inert` attribute ([#7500](https://github.com/sveltejs/svelte/issues/7500))
* Clear inputs when `bind:group` to `undefined` ([#8214](https://github.com/sveltejs/svelte/issues/8214))
* Ensure nested arrays can change at the same time ([#8282](https://github.com/sveltejs/svelte/issues/8282))
* Reduce use of template literals in SSR output for better performance ([#7539](https://github.com/sveltejs/svelte/pull/7539))
* Allow assigning to property of const while destructuring ([#7964](https://github.com/sveltejs/svelte/issues/7964))
* Don't set selected options if value is unbound or not passed ([#5644](https://github.com/sveltejs/svelte/issues/5644)) * Don't set selected options if value is unbound or not passed ([#5644](https://github.com/sveltejs/svelte/issues/5644))
* Ensure `<input>` value persists when swapping elements with spread attributes in an `#each` block ([#7578](https://github.com/sveltejs/svelte/issues/7578)) * Validate component `:global()` selectors ([#6272](https://github.com/sveltejs/svelte/issues/6272))
* Select first enabled option by default when initial value is undefined ([#7041](https://github.com/sveltejs/svelte/issues/7041)) * Improve warnings:
* Fix race condition on `svelte:element` with transitions ([#7948](https://github.com/sveltejs/svelte/issues/7948)) * Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289))
* Omit a11y warnings on `<video aria-hidden="true">` ([#7874](https://github.com/sveltejs/svelte/issues/7874))
* Omit a11y warnings on `<svelte:element>` ([#7939](https://github.com/sveltejs/svelte/issues/7939))
* Detect unused empty attribute CSS selectors ([#8042](https://github.com/sveltejs/svelte/issues/8042))
* Omit "no child content" warning on elements with `aria-label` ([#8296](https://github.com/sveltejs/svelte/issues/8296))
* Check value equality for `<input type="search">` and `<input type="url">` ([#7027](https://github.com/sveltejs/svelte/issues/7027))
* Do not select a disabled `<option>` by default when the initial bound value is undefined ([#7041](https://github.com/sveltejs/svelte/issues/7041))
* Handle `{@html}` tags inside `<template>` tags ([#7364](https://github.com/sveltejs/svelte/pull/7364))
* Ensure `afterUpdate` is not called after `onDestroy` ([#7476](https://github.com/sveltejs/svelte/issues/7476))
* Improve handling of `inert` attribute ([#7500](https://github.com/sveltejs/svelte/issues/7500))
* Reduce use of template literals in SSR output for better performance ([#7539](https://github.com/sveltejs/svelte/pull/7539))
* Ensure `<input>` value persists when swapping elements with spread attributes in an `{#each}` block ([#7578](https://github.com/sveltejs/svelte/issues/7578))
* Simplify generated code for reactive statements if dependencies are all static ([#7942](https://github.com/sveltejs/svelte/pull/7942))
* Fix race condition on `<svelte:element>` with transitions ([#7948](https://github.com/sveltejs/svelte/issues/7948))
* Allow assigning to a property of a `const` when destructuring ([#7964](https://github.com/sveltejs/svelte/issues/7964))
* Match browser behavior for decoding malformed HTML entities ([#8026](https://github.com/sveltejs/svelte/issues/8026))
* Ensure `trusted-types` CSP compatibility for Web Components ([#8134](https://github.com/sveltejs/svelte/issues/8134))
* Optimise `<svelte:element>` output code for static tag and static attribute ([#8161](https://github.com/sveltejs/svelte/pull/8161))
* Don't throw when calling unsubscribing from a store twice ([#8186](https://github.com/sveltejs/svelte/pull/8186))
* Clear inputs when `bind:group` value is set to `undefined` ([#8214](https://github.com/sveltejs/svelte/issues/8214))
* Fix handling of nested arrays with keyed `{#each}` containing a non-keyed `{#each}` ([#8282](https://github.com/sveltejs/svelte/issues/8282))
## 3.55.1 ## 3.55.1

@ -196,6 +196,9 @@ export interface DOMAttributes<T extends EventTarget> {
// Message Events // Message Events
'on:message'?: MessageEventHandler<T> | undefined | null; 'on:message'?: MessageEventHandler<T> | undefined | null;
'on:messageerror'?: MessageEventHandler<T> | undefined | null; 'on:messageerror'?: MessageEventHandler<T> | undefined | null;
// Document Events
'on:visibilitychange'?: EventHandler<Event, T> | undefined | null;
// Global Events // Global Events
'on:cancel'?: EventHandler<Event, T> | undefined | null; 'on:cancel'?: EventHandler<Event, T> | undefined | null;
@ -531,13 +534,22 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
is?: string | undefined | null; is?: string | undefined | null;
/** /**
* Elements with the contenteditable attribute support innerHTML and textContent bindings. * Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/ */
'bind:innerHTML'?: string | undefined | null; 'bind:innerHTML'?: string | undefined | null;
/** /**
* Elements with the contenteditable attribute support innerHTML and textContent bindings. * Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/ */
'bind:textContent'?: string | undefined | null; 'bind:textContent'?: string | undefined | null;
/**
* Elements with the contenteditable attribute support `innerHTML`, `textContent` and `innerText` bindings.
*/
'bind:innerText'?: string | undefined | null;
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4
readonly 'bind:borderBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4
readonly 'bind:devicePixelContentBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4
// SvelteKit // SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
@ -707,6 +719,9 @@ export interface HTMLImgAttributes extends HTMLAttributes<HTMLImageElement> {
srcset?: string | undefined | null; srcset?: string | undefined | null;
usemap?: string | undefined | null; usemap?: string | undefined | null;
width?: number | string | undefined | null; width?: number | string | undefined | null;
readonly 'bind:naturalWidth'?: number | undefined | null;
readonly 'bind:naturalHeight'?: number | undefined | null;
} }
export interface HTMLInsAttributes extends HTMLAttributes<HTMLModElement> { export interface HTMLInsAttributes extends HTMLAttributes<HTMLModElement> {
@ -842,6 +857,7 @@ export interface HTMLMediaAttributes<T extends HTMLMediaElement> extends HTMLAtt
*/ */
volume?: number | undefined | null; volume?: number | undefined | null;
readonly 'bind:readyState'?: 0 | 1 | 2 | 3 | 4 | undefined | null;
readonly 'bind:duration'?: number | undefined | null; readonly 'bind:duration'?: number | undefined | null;
readonly 'bind:buffered'?: SvelteMediaTimeRange[] | undefined | null; readonly 'bind:buffered'?: SvelteMediaTimeRange[] | undefined | null;
readonly 'bind:played'?: SvelteMediaTimeRange[] | undefined | null; readonly 'bind:played'?: SvelteMediaTimeRange[] | undefined | null;
@ -862,9 +878,9 @@ export interface HTMLMediaAttributes<T extends HTMLMediaElement> extends HTMLAtt
} }
export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> { export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> {
charSet?: string | undefined | null; charset?: string | undefined | null;
content?: string | undefined | null; content?: string | undefined | null;
httpequiv?: string | undefined | null; 'http-equiv'?: string | undefined | null;
name?: string | undefined | null; name?: string | undefined | null;
media?: string | undefined | null; media?: string | undefined | null;
} }
@ -1575,6 +1591,7 @@ export interface SvelteHTMLElements {
// Svelte specific // Svelte specific
'svelte:window': SvelteWindowAttributes; 'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:body': HTMLAttributes<HTMLElement>; 'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string }; 'svelte:fragment': { slot?: string };
'svelte:options': { [name: string]: any }; 'svelte:options': { [name: string]: any };

@ -1,28 +0,0 @@
// This script generates the TypeScript definitions
const { execSync } = require('child_process');
const { readFileSync, writeFileSync } = require('fs');
try {
execSync('tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly');
} catch (err) {
console.error(err.stderr.toString());
throw err;
}
// We need to add these types to the .d.ts files here because if we add them before building, the build will fail,
// because the TS->JS transformation doesn't know these exports are types and produces code that fails at runtime.
// We can't use `export type` syntax either because the TS version we're on doesn't have this feature yet.
function modify(path, modifyFn) {
const content = readFileSync(path, 'utf8');
writeFileSync(path, modifyFn(content));
}
modify(
'types/runtime/index.d.ts',
content => content.replace('SvelteComponentTyped', 'SvelteComponentTyped, ComponentType, ComponentConstructorOptions, ComponentProps, ComponentEvents')
);
modify(
'types/compiler/index.d.ts',
content => content + '\nexport { CompileOptions, ModuleFormat, EnableSourcemap, CssHashGetter } from "./interfaces"'
);

6615
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.55.1", "version": "3.58.0",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -86,7 +86,7 @@
} }
}, },
"engines": { "engines": {
"node": ">= 8" "node": ">= 14"
}, },
"types": "types/runtime/index.d.ts", "types": "types/runtime/index.d.ts",
"scripts": { "scripts": {
@ -95,11 +95,11 @@
"test:unit": "mocha --config .mocharc.unit.js --exit", "test:unit": "mocha --config .mocharc.unit.js --exit",
"quicktest": "mocha --exit", "quicktest": "mocha --exit",
"build": "rollup -c && npm run tsd", "build": "rollup -c && npm run tsd",
"prepare": "node scripts/skip_in_ci.js npm run build", "prepare": "npm run build",
"dev": "rollup -cw", "dev": "rollup -cw",
"posttest": "agadoo internal/index.mjs", "posttest": "agadoo internal/index.mjs",
"prepublishOnly": "node check_publish_env.js && npm run lint && npm run build && npm test", "prepublishOnly": "node check_publish_env.js && npm run lint && npm run build && npm test",
"tsd": "node ./generate-type-definitions.js", "tsd": "tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly",
"lint": "eslint \"{src,test}/**/*.{ts,js}\" --cache" "lint": "eslint \"{src,test}/**/*.{ts,js}\" --cache"
}, },
"repository": { "repository": {
@ -119,45 +119,45 @@
}, },
"homepage": "https://svelte.dev", "homepage": "https://svelte.dev",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.15",
"@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-json": "^4.0.1", "@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^2.3.0", "@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-sucrase": "^3.1.0", "@rollup/plugin-sucrase": "^5.0.1",
"@rollup/plugin-typescript": "^2.0.1", "@rollup/plugin-typescript": "^11.1.0",
"@rollup/plugin-virtual": "^2.0.0", "@rollup/plugin-virtual": "^3.0.1",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0",
"@types/aria-query": "^5.0.0", "@types/aria-query": "^5.0.1",
"@types/mocha": "^7.0.0", "@types/mocha": "^10.0.1",
"@types/node": "^8.10.53", "@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.58.0",
"acorn": "^8.8.1", "acorn": "^8.8.2",
"agadoo": "^2.0.0", "agadoo": "^3.0.0",
"aria-query": "^5.1.1", "aria-query": "^5.1.3",
"axobject-query": "^3.1.1", "axobject-query": "^3.1.1",
"code-red": "^0.2.5", "code-red": "^1.0.0",
"css-tree": "^2.2.1", "css-tree": "^2.3.1",
"eslint": "^8.26.0", "eslint": "^8.35.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"estree-walker": "^3.0.1", "estree-walker": "^3.0.3",
"is-reference": "^3.0.0", "is-reference": "^3.0.1",
"jsdom": "^15.2.1", "jsdom": "^21.1.1",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"locate-character": "^2.0.5", "locate-character": "^2.0.5",
"magic-string": "^0.25.3", "magic-string": "^0.30.0",
"mocha": "^7.0.0", "mocha": "^10.2.0",
"periscopic": "^3.0.4", "periscopic": "^3.1.0",
"puppeteer": "^2.0.0", "puppeteer": "^19.8.5",
"rollup": "^1.27.14", "rollup": "^3.20.2",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"tslib": "^2.4.1", "tslib": "^2.5.0",
"typescript": "^3.7.5", "typescript": "^5.0.4",
"util": "^0.12.5" "util": "^0.12.5"
} }
} }

@ -1,24 +1,58 @@
import fs from 'fs'; import fs from 'node:fs';
import { createRequire } from 'node:module';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import resolve from '@rollup/plugin-node-resolve'; import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
import sucrase from '@rollup/plugin-sucrase'; import sucrase from '@rollup/plugin-sucrase';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import pkg from './package.json';
const require = createRequire(import.meta.url);
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const is_publish = !!process.env.PUBLISH; const is_publish = !!process.env.PUBLISH;
const ts_plugin = is_publish const ts_plugin = is_publish
? typescript({ ? typescript({
include: 'src/**',
typescript: require('typescript') typescript: require('typescript')
}) })
: sucrase({ : sucrase({
transforms: ['typescript'] transforms: ['typescript']
}); });
const external = id => id.startsWith('svelte/'); // The following external and path logic is necessary so that the bundled runtime pieces and the index file
// reference each other correctly instead of bundling their references to each other
/**
* Ensures that relative imports inside `src/runtime` like `./internal` and `../store` are externalized correctly
*/
const external = (id, parent_id) => {
const parent_segments = parent_id.replace(/\\/g, '/').split('/');
// TODO needs to be adjusted when we move to JS modules
if (parent_segments[parent_segments.length - 3] === 'runtime') {
return /\.\.\/\w+$/.test(id);
} else {
return id === './internal' && parent_segments[parent_segments.length - 2] === 'runtime';
}
}
/**
* Transforms externalized import paths like `../store` into correct relative imports with correct index file extension import
*/
const replace_relative_svelte_imports = (id, ending) => {
id = id.replace(/\\/g, '/');
// TODO needs to be adjusted when we move to JS modules
return /src\/runtime\/\w+$/.test(id) && `../${id.split('/').pop()}/${ending}`;
}
/**
* Transforms externalized `./internal` import path into correct relative import with correct index file extension import
*/
const replace_relative_internal_import = (id, ending) => {
id = id.replace(/\\/g, '/');
// TODO needs to be adjusted when we move to JS modules
return id.endsWith('src/runtime/internal') && `./internal/${ending}`;
}
fs.writeFileSync(`./compiler.d.ts`, `export { compile, parse, preprocess, walk, VERSION } from './types/compiler/index';`); fs.writeFileSync(`./compiler.d.ts`, `export { compile, parse, preprocess, walk, VERSION } from './types/compiler/index';`);
@ -30,12 +64,12 @@ export default [
{ {
file: `index.mjs`, file: `index.mjs`,
format: 'esm', format: 'esm',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}/index.mjs` paths: id => replace_relative_internal_import(id, 'index.mjs')
}, },
{ {
file: `index.js`, file: `index.js`,
format: 'cjs', format: 'cjs',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}/index.js` paths: id => replace_relative_internal_import(id, 'index.js')
} }
], ],
external, external,
@ -48,12 +82,12 @@ export default [
{ {
file: `ssr.mjs`, file: `ssr.mjs`,
format: 'esm', format: 'esm',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}/index.mjs` paths: id => replace_relative_internal_import(id, 'index.mjs')
}, },
{ {
file: `ssr.js`, file: `ssr.js`,
format: 'cjs', format: 'cjs',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}/index.js` paths: id => replace_relative_internal_import(id, 'index.js')
} }
], ],
external, external,
@ -68,12 +102,12 @@ export default [
{ {
file: `${dir}/index.mjs`, file: `${dir}/index.mjs`,
format: 'esm', format: 'esm',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '..')}/index.mjs` paths: id => replace_relative_svelte_imports(id, 'index.mjs')
}, },
{ {
file: `${dir}/index.js`, file: `${dir}/index.js`,
format: 'cjs', format: 'cjs',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '..')}/index.js` paths: id => replace_relative_svelte_imports(id, 'index.js')
} }
], ],
external, external,
@ -83,7 +117,7 @@ export default [
}), }),
ts_plugin, ts_plugin,
{ {
writeBundle(bundle) { writeBundle(_options, bundle) {
if (dir === 'internal') { if (dir === 'internal') {
const mod = bundle['index.mjs']; const mod = bundle['index.mjs'];
if (mod) { if (mod) {

@ -1,7 +0,0 @@
if (process.env.SKIP_PREPARE) {
console.log('Skipped "prepare" script');
} else {
const { execSync } = require("child_process");
const command = process.argv.slice(2).join(" ");
execSync(command, { stdio: "inherit" });
}

@ -286,7 +286,7 @@ You cannot `export default`, since the default export is the component itself.
<script context="module"> <script context="module">
let totalComponents = 0; let totalComponents = 0;
// this allows an importer to do e.g. // the export keyword allows this function to imported with e.g.
// `import Example, { alertTotal } from './Example.svelte'` // `import Example, { alertTotal } from './Example.svelte'`
export function alertTotal() { export function alertTotal() {
alert(totalComponents); alert(totalComponents);

@ -539,6 +539,7 @@ The following modifiers are available:
* `preventDefault` — calls `event.preventDefault()` before running the handler * `preventDefault` — calls `event.preventDefault()` before running the handler
* `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element * `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element
* `stopImmediatePropagation` - calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired.
* `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so) * `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
* `nonpassive` — explicitly set `passive: false` * `nonpassive` — explicitly set `passive: false`
* `capture` — fires the handler during the *capture* phase instead of the *bubbling* phase * `capture` — fires the handler during the *capture* phase instead of the *bubbling* phase
@ -690,7 +691,12 @@ When the value of an `<option>` matches its text content, the attribute can be o
--- ---
Elements with the `contenteditable` attribute support `innerHTML` and `textContent` bindings. Elements with the `contenteditable` attribute support the following bindings:
- [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)
- [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText)
- [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
There are slight differences between each of these, read more about them [here](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText).
```sv ```sv
<div contenteditable="true" bind:innerHTML={html}></div> <div contenteditable="true" bind:innerHTML={html}></div>
@ -713,7 +719,7 @@ Elements with the `contenteditable` attribute support `innerHTML` and `textConte
--- ---
Media elements (`<audio>` and `<video>`) have their own set of bindings — six *readonly* ones... Media elements (`<audio>` and `<video>`) have their own set of bindings — seven *readonly* ones...
* `duration` (readonly) — the total duration of the video, in seconds * `duration` (readonly) — the total duration of the video, in seconds
* `buffered` (readonly) — an array of `{start, end}` objects * `buffered` (readonly) — an array of `{start, end}` objects
@ -721,6 +727,7 @@ Media elements (`<audio>` and `<video>`) have their own set of bindings — six
* `seekable` (readonly) — ditto * `seekable` (readonly) — ditto
* `seeking` (readonly) — boolean * `seeking` (readonly) — boolean
* `ended` (readonly) — boolean * `ended` (readonly) — boolean
* `readyState` (readonly) — number between (and including) 0 and 4
...and five *two-way* bindings: ...and five *two-way* bindings:
@ -741,6 +748,7 @@ Videos additionally have readonly `videoWidth` and `videoHeight` bindings.
bind:seekable bind:seekable
bind:seeking bind:seeking
bind:ended bind:ended
bind:readyState
bind:currentTime bind:currentTime
bind:playbackRate bind:playbackRate
bind:paused bind:paused
@ -1743,6 +1751,25 @@ All except `scrollX` and `scrollY` are readonly.
> Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of `scrollX` and `scrollY` will cause scrolling. However, if the scrolling behaviour is desired, call `scrollTo()` in `onMount()`. > Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of `scrollX` and `scrollY` will cause scrolling. However, if the scrolling behaviour is desired, call `scrollTo()` in `onMount()`.
### `<svelte:document>`
```sv
<svelte:document on:event={handler}/>
```
---
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on `document`.
As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element.
```sv
<svelte:document
on:visibilitychange={handleVisibilityChange}
use:someAction
/>
```
### `<svelte:body>` ### `<svelte:body>`
```sv ```sv
@ -1753,7 +1780,7 @@ All except `scrollX` and `scrollY` are readonly.
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on the `<body>` element. Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on the `<body>` element.
As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element. As with `<svelte:window>` and `<svelte:document>`, this element may only appear the top level of your component and must never be inside a block or element.
```sv ```sv
<svelte:body <svelte:body
@ -1774,7 +1801,7 @@ As with `<svelte:window>`, this element may only appear the top level of your co
This element makes it possible to insert elements into `document.head`. During server-side rendering, `head` content is exposed separately to the main `html` content. This element makes it possible to insert elements into `document.head`. During server-side rendering, `head` content is exposed separately to the main `html` content.
As with `<svelte:window>` and `<svelte:body>`, this element may only appear at the top level of your component and must never be inside a block or element. As with `<svelte:window>`, `<svelte:document>` and `<svelte:body>`, this element may only appear at the top level of your component and must never be inside a block or element.
```sv ```sv
<svelte:head> <svelte:head>

@ -693,7 +693,7 @@ Animates a `blur` filter alongside an element's opacity.
* `duration` (`number`, default 400) — milliseconds the transition lasts * `duration` (`number`, default 400) — milliseconds the transition lasts
* `easing` (`function`, default `cubicInOut`) — an [easing function](/docs#run-time-svelte-easing) * `easing` (`function`, default `cubicInOut`) — an [easing function](/docs#run-time-svelte-easing)
* `opacity` (`number`, default 0) - the opacity value to animate out to and in from * `opacity` (`number`, default 0) - the opacity value to animate out to and in from
* `amount` (`number`, default 5) - the size of the blur in pixels * `amount` (`number | string`, default 5) - the size of the blur. Supports css units (for example: `"4rem"`). The default unit is `px`
```sv ```sv
<script> <script>
@ -728,10 +728,11 @@ Animates the x and y positions and the opacity of an element. `in` transitions a
* `delay` (`number`, default 0) — milliseconds before starting * `delay` (`number`, default 0) — milliseconds before starting
* `duration` (`number`, default 400) — milliseconds the transition lasts * `duration` (`number`, default 400) — milliseconds the transition lasts
* `easing` (`function`, default `cubicOut`) — an [easing function](/docs#run-time-svelte-easing) * `easing` (`function`, default `cubicOut`) — an [easing function](/docs#run-time-svelte-easing)
* `x` (`number`, default 0) - the x offset to animate out to and in from * `x` (`number | string`, default 0) - the x offset to animate out to and in from
* `y` (`number`, default 0) - the y offset to animate out to and in from * `y` (`number | string`, default 0) - the y offset to animate out to and in from
* `opacity` (`number`, default 0) - the opacity value to animate out to and in from * `opacity` (`number`, default 0) - the opacity value to animate out to and in from
x and y use `px` by default but support css units, for example `x: '100vw'` or `y: '50%'`.
You can see the `fly` transition in action in the [transition tutorial](/tutorial/adding-parameters-to-transitions). You can see the `fly` transition in action in the [transition tutorial](/tutorial/adding-parameters-to-transitions).
```sv ```sv

@ -55,7 +55,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
### `a11y-click-events-have-key-events` ### `a11y-click-events-have-key-events`
Enforce `on:click` is accompanied by at least one of the following: `onKeyUp`, `onKeyDown`, `onKeyPress`. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users. Enforce `on:click` is accompanied by at least one of the following: `on:keyup`, `on:keydown`, `on:keypress`. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users.
This does not apply for interactive or hidden elements. This does not apply for interactive or hidden elements.
@ -64,6 +64,8 @@ This does not apply for interactive or hidden elements.
<div on:click={() => {}} /> <div on:click={() => {}} />
``` ```
Note that the `keypress` event is now deprecated, so it is officially recommended to use either the `keyup` or `keydown` event instead, accordingly.
--- ---
### `a11y-distracting-elements` ### `a11y-distracting-elements`
@ -135,6 +137,17 @@ Enforce that attributes important for accessibility have a valid value. For exam
--- ---
### `a11y-interactive-supports-focus`
Enforce that elements with an interactive role and interactive handlers (mouse or key press) must be focusable or tabbable.
```sv
<!-- A11y: Elements with the 'button' interactive role must have a tabindex value. -->
<div role="button" on:keypress={() => {}} />
```
---
### `a11y-label-has-associated-control` ### `a11y-label-has-associated-control`
Enforce that a label tag has a text label and an associated control. Enforce that a label tag has a text label and an associated control.
@ -275,6 +288,17 @@ Some HTML elements have default ARIA roles. Giving these elements an ARIA role t
--- ---
### `a11y-no-noninteractive-element-to-interactive-role`
[WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/#usage_intro) roles should not be used to convert a non-interactive element to an interactive element. Interactive ARIA roles include `button`, `link`, `checkbox`, `menuitem`, `menuitemcheckbox`, `menuitemradio`, `option`, `radio`, `searchbox`, `switch` and `textbox`.
```sv
<!-- A11y: Non-interactive element <h3> cannot have interactive role 'searchbox' -->
<h3 role="searchbox">Button</h3>
```
---
### `a11y-no-noninteractive-tabindex` ### `a11y-no-noninteractive-tabindex`
Tab key navigation should be limited to elements on the page that can be interacted with. Tab key navigation should be limited to elements on the page that can be interacted with.

@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>
<svelte:document on:selectionchange={handleSelectionChange} />
<p>Select this text to fire events</p>
<p>Selection: {selection}</p>

@ -2,11 +2,16 @@
title: Contenteditable bindings title: Contenteditable bindings
--- ---
Elements with a `contenteditable="true"` attribute support `textContent` and `innerHTML` bindings: Elements with the `contenteditable` attribute support the following bindings:
- [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)
- [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText)
- [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
There are slight differences between each of these, read more about them [here](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText).
```html ```html
<div <div
contenteditable="true" contenteditable="true"
bind:innerHTML={html} bind:innerHTML={html}
></div> ></div>
``` ```

@ -1,14 +0,0 @@
---
title: <svelte:body>
---
Similar to `<svelte:window>`, the `<svelte:body>` element allows you to listen for events that fire on `document.body`. This is useful with the `mouseenter` and `mouseleave` events, which don't fire on `window`.
Add the `mouseenter` and `mouseleave` handlers to the `<svelte:body>` tag:
```html
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
```

@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>
<svelte:document />
<p>Select this text to fire events</p>
<p>Selection: {selection}</p>

@ -0,0 +1,10 @@
<script>
let selection = '';
const handleSelectionChange = (e) => selection = document.getSelection();
</script>
<svelte:document on:selectionchange={handleSelectionChange} />
<p>Select this text to fire events</p>
<p>Selection: {selection}</p>

@ -0,0 +1,13 @@
---
title: <svelte:document>
---
Similar to `<svelte:window>`, the `<svelte:document>` element allows you to listen for events that fire on `document`. This is useful with events like `selectionchange`, which doesn't fire on `window`.
Add the `selectionchange` handler to the `<svelte:document>` tag:
```html
<svelte:document on:selectionchange={handleSelectionChange} />
```
> Avoid `mouseenter` and `mouseleave` handlers on this element, these events are not fired on `document` in all browsers. Use `<svelte:body>` for this instead.

@ -0,0 +1,14 @@
---
title: <svelte:body>
---
Similar to `<svelte:window>` and `<svelte:document>`, the `<svelte:body>` element allows you to listen for events that fire on `document.body`. This is useful with the `mouseenter` and `mouseleave` events, which don't fire on `window`.
Add the `mouseenter` and `mouseleave` handlers to the `<svelte:body>` tag:
```html
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
```

@ -1,3 +1,4 @@
import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import Stats from '../Stats'; import Stats from '../Stats';
@ -32,13 +33,11 @@ import { print, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords'; import { is_reserved_keyword } from './utils/reserved_keywords';
import { apply_preprocessor_sourcemap } from '../utils/mapped_code'; import { apply_preprocessor_sourcemap } from '../utils/mapped_code';
import Element from './nodes/Element'; import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
import { clone } from '../utils/clone'; import { clone } from '../utils/clone';
import compiler_warnings from './compiler_warnings'; import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors'; import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap'; import check_enable_sourcemap from './utils/check_enable_sourcemap';
import is_dynamic from './render_dom/wrappers/shared/is_dynamic';
interface ComponentOptions { interface ComponentOptions {
namespace?: string; namespace?: string;
@ -951,7 +950,7 @@ export default class Component {
}); });
} }
warn_on_undefined_store_value_references(node: Node, parent: Node, prop: string, scope: Scope) { warn_on_undefined_store_value_references(node: Node, parent: Node, prop: string | number | symbol, scope: Scope) {
if ( if (
node.type === 'LabeledStatement' && node.type === 'LabeledStatement' &&
node.label.name === '$' && node.label.name === '$' &&
@ -1392,11 +1391,12 @@ export default class Component {
module_dependencies.add(name); module_dependencies.add(name);
} }
} }
const is_writable_or_mutated =
variable && (variable.writable || variable.mutated);
if ( if (
should_add_as_dependency && should_add_as_dependency &&
(!owner || owner === component.instance_scope) && (!owner || owner === component.instance_scope) &&
(name[0] === '$' || variable) (name[0] === '$' || is_writable_or_mutated)
) { ) {
dependencies.add(name); dependencies.add(name);
} }
@ -1420,19 +1420,6 @@ export default class Component {
const { expression } = node.body as ExpressionStatement; const { expression } = node.body as ExpressionStatement;
const declaration = expression && (expression as AssignmentExpression).left; const declaration = expression && (expression as AssignmentExpression).left;
const is_dependency_static = Array.from(dependencies).every(
dependency => dependency !== '$$props' && dependency !== '$$restProps' && !is_dynamic(this.var_lookup.get(dependency))
);
if (is_dependency_static) {
assignees.forEach(assignee => {
const variable = component.var_lookup.get(assignee);
if (variable) {
variable.is_reactive_static = true;
}
});
}
unsorted_reactive_declarations.push({ unsorted_reactive_declarations.push({
assignees, assignees,
dependencies, dependencies,

@ -66,7 +66,7 @@ export default {
}, },
missing_contenteditable_attribute: { missing_contenteditable_attribute: {
code: 'missing-contenteditable-attribute', code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings' message: '\'contenteditable\' attribute is required for textContent, innerHTML and innerText two-way bindings'
}, },
dynamic_contenteditable_attribute: { dynamic_contenteditable_attribute: {
code: 'dynamic-contenteditable-attribute', code: 'dynamic-contenteditable-attribute',
@ -234,6 +234,10 @@ export default {
code: 'css-invalid-global-selector', code: 'css-invalid-global-selector',
message: ':global(...) must contain a single selector' message: ':global(...) must contain a single selector'
}, },
css_invalid_global_selector_position: {
code: 'css-invalid-global-selector-position',
message: ':global(...) not at the start of a selector sequence should not contain type or universal selectors'
},
css_invalid_selector: (selector: string) => ({ css_invalid_selector: (selector: string) => ({
code: 'css-invalid-selector', code: 'css-invalid-selector',
message: `Invalid selector "${selector}"` message: `Invalid selector "${selector}"`

@ -119,6 +119,10 @@ export default {
code: 'a11y-no-interactive-element-to-noninteractive-role', code: 'a11y-no-interactive-element-to-noninteractive-role',
message: `A11y: <${element}> cannot have role '${role}'` message: `A11y: <${element}> cannot have role '${role}'`
}), }),
a11y_no_noninteractive_element_to_interactive_role: (role: string | boolean, element: string) => ({
code: 'a11y-no-noninteractive-element-to-interactive-role',
message: `A11y: Non-interactive element <${element}> cannot have interactive role '${role}'`
}),
a11y_role_has_required_aria_props: (role: string, props: string[]) => ({ a11y_role_has_required_aria_props: (role: string, props: string[]) => ({
code: 'a11y-role-has-required-aria-props', code: 'a11y-role-has-required-aria-props',
message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}` message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}`
@ -162,6 +166,10 @@ export default {
code: 'a11y-img-redundant-alt', code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.' message: 'A11y: Screenreaders already announce <img> elements as an image.'
}, },
a11y_interactive_supports_focus: (role: string) => ({
code: 'a11y-interactive-supports-focus',
message: `A11y: Elements with the '${role}' interactive role must have a tabindex value.`
}),
a11y_label_has_associated_control: { a11y_label_has_associated_control: {
code: 'a11y-label-has-associated-control', code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.' message: 'A11y: A form label must be associated with a control.'
@ -186,10 +194,10 @@ export default {
code: 'a11y-mouse-events-have-key-events', code: 'a11y-mouse-events-have-key-events',
message: `A11y: on:${event} must be accompanied by on:${accompanied_by}` message: `A11y: on:${event} must be accompanied by on:${accompanied_by}`
}), }),
a11y_click_events_have_key_events: () => ({ a11y_click_events_have_key_events: {
code: 'a11y-click-events-have-key-events', code: 'a11y-click-events-have-key-events',
message: 'A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.' message: 'A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.'
}), },
a11y_missing_content: (name: string) => ({ a11y_missing_content: (name: string) => ({
code: 'a11y-missing-content', code: 'a11y-missing-content',
message: `A11y: <${name}> element should have child content` message: `A11y: <${name}> element should have child content`
@ -213,5 +221,9 @@ export default {
invalid_rest_eachblock_binding: (rest_element_name: string) => ({ invalid_rest_eachblock_binding: (rest_element_name: string) => ({
code: 'invalid-rest-eachblock-binding', code: 'invalid-rest-eachblock-binding',
message: `...${rest_element_name} operator will create a new object and binding propagation with original object will not work` message: `...${rest_element_name} operator will create a new object and binding propagation with original object will not work`
}) }),
avoid_mouse_events_on_document: {
code: 'avoid-mouse-events-on-document',
message: 'Mouse enter/leave events on the document are not supported in all browsers and should be avoided'
}
}; };

@ -76,7 +76,7 @@ export default class Selector {
this.blocks.forEach((block, i) => { this.blocks.forEach((block, i) => {
if (i > 0) { if (i > 0) {
if (block.start - c > 1) { if (block.start - c > 1) {
code.overwrite(c, block.start, block.combinator.name || ' '); code.update(c, block.start, block.combinator.name || ' ');
} }
} }
@ -112,7 +112,7 @@ export default class Selector {
} }
if (selector.type === 'TypeSelector' && selector.name === '*') { if (selector.type === 'TypeSelector' && selector.name === '*') {
code.overwrite(selector.start, selector.end, attr); code.update(selector.start, selector.end, attr);
} else { } else {
code.appendLeft(selector.end, attr); code.appendLeft(selector.end, attr);
} }
@ -148,6 +148,7 @@ export default class Selector {
} }
this.validate_global_with_multiple_selectors(component); this.validate_global_with_multiple_selectors(component);
this.validate_global_compound_selector(component);
this.validate_invalid_combinator_without_selector(component); this.validate_invalid_combinator_without_selector(component);
} }
@ -179,6 +180,23 @@ export default class Selector {
} }
} }
validate_global_compound_selector(component: Component) {
for (const block of this.blocks) {
for (let index = 0; index < block.selectors.length; index++) {
const selector = block.selectors[index];
if (selector.type === 'PseudoClassSelector' &&
selector.name === 'global' &&
index !== 0 &&
selector.children &&
selector.children.length > 0 &&
!/[.:#\s]/.test(selector.children[0].value[0])
) {
component.error(selector, compiler_errors.css_invalid_global_selector_position);
}
}
}
}
get_amount_class_specificity_increased() { get_amount_class_specificity_increased() {
let count = 0; let count = 0;
for (const block of this.blocks) { for (const block of this.blocks) {

@ -35,7 +35,7 @@ function minify_declarations(
declarations.forEach((declaration, i) => { declarations.forEach((declaration, i) => {
const separator = i > 0 ? ';' : ''; const separator = i > 0 ? ';' : '';
if ((declaration.node.start - c) > separator.length) { if ((declaration.node.start - c) > separator.length) {
code.overwrite(c, declaration.node.start, separator); code.update(c, declaration.node.start, separator);
} }
declaration.minify(code); declaration.minify(code);
c = declaration.node.end; c = declaration.node.end;
@ -75,7 +75,7 @@ class Rule {
if (selector.used) { if (selector.used) {
const separator = started ? ',' : ''; const separator = started ? ',' : '';
if ((selector.node.start - c) > separator.length) { if ((selector.node.start - c) > separator.length) {
code.overwrite(c, selector.node.start, separator); code.update(c, selector.node.start, separator);
} }
selector.minify(code); selector.minify(code);
@ -133,7 +133,7 @@ class Declaration {
if (block.type === 'Identifier') { if (block.type === 'Identifier') {
const name = block.name; const name = block.name;
if (keyframes.has(name)) { if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name)); code.update(block.start, block.end, keyframes.get(name));
} }
} }
}); });
@ -156,7 +156,7 @@ class Declaration {
while (regex_whitespace.test(code.original[start])) start += 1; while (regex_whitespace.test(code.original[start])) start += 1;
if (start - c > 1) { if (start - c > 1) {
code.overwrite(c, start, ':'); code.update(c, start, ':');
} }
} }
} }
@ -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);
}); });
@ -204,7 +204,7 @@ class Atrule {
code.remove(c, this.node.block.start); code.remove(c, this.node.block.start);
} else if (this.node.name === 'supports') { } else if (this.node.name === 'supports') {
let c = this.node.start + 9; let c = this.node.start + 9;
if (this.node.prelude.start - c > 1) code.overwrite(c, this.node.prelude.start, ' '); if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
this.node.prelude.children.forEach((query: CssNode) => { this.node.prelude.children.forEach((query: CssNode) => {
// TODO minify queries // TODO minify queries
c = query.end; c = query.end;
@ -213,7 +213,7 @@ class Atrule {
} else { } else {
let c = this.node.start + this.node.name.length + 1; let c = this.node.start + this.node.name.length + 1;
if (this.node.prelude) { if (this.node.prelude) {
if (this.node.prelude.start - c > 1) code.overwrite(c, this.node.prelude.start, ' '); if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
c = this.node.prelude.end; c = this.node.prelude.end;
} }
if (this.node.block && this.node.block.start - c > 0) { if (this.node.block && this.node.block.start - c > 0) {
@ -255,7 +255,7 @@ class Atrule {
}); });
}); });
} else { } else {
code.overwrite(start, end, keyframes.get(name)); code.update(start, end, keyframes.get(name));
} }
} }
}); });

@ -3,7 +3,7 @@ import get_object from '../utils/get_object';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import Component from '../Component'; import Component from '../Component';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { regex_dimensions } from '../../utils/patterns'; import { regex_dimensions, regex_box_size } from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree'; import { Node as ESTreeNode } from 'estree';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import Element from './Element'; import Element from './Element';
@ -24,7 +24,8 @@ const read_only_media_attributes = new Set([
'videoHeight', 'videoHeight',
'videoWidth', 'videoWidth',
'naturalWidth', 'naturalWidth',
'naturalHeight' 'naturalHeight',
'readyState'
]); ]);
export default class Binding extends Node { export default class Binding extends Node {
@ -91,6 +92,7 @@ export default class Binding extends Node {
this.is_readonly = this.is_readonly =
regex_dimensions.test(this.name) || regex_dimensions.test(this.name) ||
regex_box_size.test(this.name) ||
(isElement(parent) && (isElement(parent) &&
((parent.is_media_node() && read_only_media_attributes.has(this.name)) || ((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file')) /* TODO others? */); (parent.name === 'input' && type === 'file')) /* TODO others? */);

@ -17,6 +17,7 @@ export default class CatchBlock extends AbstractBlock {
this.scope = scope.child(); this.scope = scope.child();
if (parent.catch_node) { if (parent.catch_node) {
parent.catch_contexts.forEach(context => { parent.catch_contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this); this.scope.add(context.key.name, parent.expression.dependencies, this);
}); });
} }

@ -65,6 +65,7 @@ export default class ConstTag extends Node {
}); });
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right); this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
this.contexts.forEach(context => { this.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
const owner = this.scope.get_owner(context.key.name); const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) { if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name)); this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name));

@ -0,0 +1,41 @@
import Node from './shared/Node';
import EventHandler from './EventHandler';
import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Element } from '../../interfaces';
import compiler_warnings from '../compiler_warnings';
export default class Document extends Node {
type: 'Document';
handlers: EventHandler[] = [];
actions: Action[] = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) {
super(component, parent, scope, info);
info.attributes.forEach((node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
});
this.validate();
}
private validate() {
const handlers_map = new Set();
this.handlers.forEach(handler => (
handlers_map.add(handler.name)
));
if (handlers_map.has('mouseenter') || handlers_map.has('mouseleave')) {
this.component.warn(this, compiler_warnings.avoid_mouse_events_on_document);
}
}
}

@ -45,6 +45,7 @@ export default class EachBlock extends AbstractBlock {
unpack_destructuring({ contexts: this.contexts, node: info.context, scope, component, context_rest_properties: this.context_rest_properties }); unpack_destructuring({ contexts: this.contexts, node: info.context, scope, component, context_rest_properties: this.context_rest_properties });
this.contexts.forEach(context => { this.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, this.expression.dependencies, this); this.scope.add(context.key.name, this.expression.dependencies, this);
}); });

@ -11,7 +11,8 @@ import StyleDirective from './StyleDirective';
import Text from './Text'; import Text from './Text';
import { namespaces } from '../../utils/namespaces'; import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children'; import map_children from './shared/map_children';
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns'; import { is_name_contenteditable, get_contenteditable_attr } from '../utils/contenteditable';
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list'; import list from '../../utils/list';
import Let from './Let'; import Let from './Let';
@ -23,15 +24,14 @@ import { string_literal } from '../utils/stringify';
import { Literal } from 'estree'; import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y'; import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role, is_static_element, has_disabled_attribute } from '../utils/a11y';
const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes); const aria_attribute_set = new Set(aria_attributes);
const aria_roles = roles.keys(); const aria_roles = roles.keys();
const aria_role_set = new Set(aria_roles); const aria_role_set = new Set(aria_roles);
const aria_role_abstract_set = new Set(roles.keys().filter(role => roles.get(role).abstract));
const a11y_required_attributes = { const a11y_required_attributes = {
a: ['href'], a: ['href'],
@ -75,6 +75,33 @@ const a11y_labelable = new Set([
'textarea' 'textarea'
]); ]);
const a11y_interactive_handlers = new Set([
// Keyboard events
'keypress',
'keydown',
'keyup',
// Click events
'click',
'contextmenu',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragexit',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup'
]);
const a11y_nested_implicit_semantics = new Map([ const a11y_nested_implicit_semantics = new Map([
['header', 'banner'], ['header', 'banner'],
['footer', 'contentinfo'] ['footer', 'contentinfo']
@ -145,6 +172,35 @@ const input_type_to_implicit_role = new Map([
['url', 'textbox'] ['url', 'textbox']
]); ]);
/**
* Exceptions to the rule which follows common A11y conventions
* TODO make this configurable by the user
*/
const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: [
'listbox',
'menu',
'menubar',
'radiogroup',
'tablist',
'tree',
'treegrid'
],
ol: [
'listbox',
'menu',
'menubar',
'radiogroup',
'tablist',
'tree',
'treegrid'
],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
fieldset: ['radiogroup', 'presentation']
};
const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']); const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']);
function input_implicit_role(attribute_map: Map<string, Attribute>) { function input_implicit_role(attribute_map: Map<string, Attribute>) {
@ -183,6 +239,7 @@ const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([ const valid_modifiers = new Set([
'preventDefault', 'preventDefault',
'stopPropagation', 'stopPropagation',
'stopImmediatePropagation',
'capture', 'capture',
'once', 'once',
'passive', 'passive',
@ -550,7 +607,7 @@ export default class Element extends Node {
} }
// aria-activedescendant-has-tabindex // aria-activedescendant-has-tabindex
if (name === 'aria-activedescendant' && !is_interactive_element(this.name, attribute_map) && !attribute_map.has('tabindex')) { if (name === 'aria-activedescendant' && !this.is_dynamic_element && !is_interactive_element(this.name, attribute_map) && !attribute_map.has('tabindex')) {
component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex); component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex);
} }
} }
@ -565,8 +622,8 @@ export default class Element extends Node {
const value = attribute.get_static_value(); const value = attribute.get_static_value();
if (typeof value === 'string') { if (typeof value === 'string') {
value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefintionKey) => { value.split(regex_any_repeated_whitespaces).forEach((current_role: ARIARoleDefinitionKey) => {
if (current_role && aria_role_abstract_set.has(current_role)) { if (current_role && is_abstract_role(current_role)) {
component.warn(attribute, compiler_warnings.a11y_no_abstract_role(current_role)); component.warn(attribute, compiler_warnings.a11y_no_abstract_role(current_role));
} else if (current_role && !aria_role_set.has(current_role)) { } else if (current_role && !aria_role_set.has(current_role)) {
const match = fuzzymatch(current_role, aria_roles); const match = fuzzymatch(current_role, aria_roles);
@ -590,7 +647,7 @@ export default class Element extends Node {
} }
// role-has-required-aria-props // role-has-required-aria-props
if (!is_semantic_role_element(current_role, this.name, attribute_map)) { if (!this.is_dynamic_element && !is_semantic_role_element(current_role, this.name, attribute_map)) {
const role = roles.get(current_role); const role = roles.get(current_role);
if (role) { if (role) {
const required_role_props = Object.keys(role.requiredProps); const required_role_props = Object.keys(role.requiredProps);
@ -602,12 +659,31 @@ export default class Element extends Node {
} }
} }
// interactive-supports-focus
if (
!has_disabled_attribute(attribute_map) &&
!is_hidden_from_screen_reader(this.name, attribute_map) &&
!is_presentation_role(current_role) &&
is_interactive_roles(current_role) &&
is_static_element(this.name, attribute_map) &&
!attribute_map.get('tabindex')
) {
const has_interactive_handlers = handlers.some((handler) => a11y_interactive_handlers.has(handler.name));
if (has_interactive_handlers) {
component.warn(this, compiler_warnings.a11y_interactive_supports_focus(current_role));
}
}
// no-interactive-element-to-noninteractive-role // no-interactive-element-to-noninteractive-role
if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) { if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) {
component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name)); component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name));
} }
});
// no-noninteractive-element-to-interactive-role
if (is_non_interactive_element(this.name, attribute_map) && is_interactive_roles(current_role) && !a11y_non_interactive_element_to_interactive_role_exceptions[this.name]?.includes(current_role)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_element_to_interactive_role(current_role, this.name));
}
});
} }
} }
@ -622,7 +698,7 @@ export default class Element extends Node {
} }
// scope // scope
if (name === 'scope' && this.name !== 'th') { if (name === 'scope' && !this.is_dynamic_element && this.name !== 'th') {
component.warn(attribute, compiler_warnings.a11y_misplaced_scope); component.warn(attribute, compiler_warnings.a11y_misplaced_scope);
} }
@ -639,9 +715,10 @@ export default class Element extends Node {
// click-events-have-key-events // click-events-have-key-events
if (handlers_map.has('click')) { if (handlers_map.has('click')) {
const role = attribute_map.get('role'); const role = attribute_map.get('role');
const is_non_presentation_role = role?.is_static && !is_presentation_role(role.get_static_value() as ARIARoleDefintionKey); const is_non_presentation_role = role?.is_static && !is_presentation_role(role.get_static_value() as ARIARoleDefinitionKey);
if ( if (
!this.is_dynamic_element &&
!is_hidden_from_screen_reader(this.name, attribute_map) && !is_hidden_from_screen_reader(this.name, attribute_map) &&
(!role || is_non_presentation_role) && (!role || is_non_presentation_role) &&
!is_interactive_element(this.name, attribute_map) && !is_interactive_element(this.name, attribute_map) &&
@ -655,14 +732,14 @@ export default class Element extends Node {
if (!has_key_event) { if (!has_key_event) {
component.warn( component.warn(
this, this,
compiler_warnings.a11y_click_events_have_key_events() compiler_warnings.a11y_click_events_have_key_events
); );
} }
} }
} }
// no-noninteractive-tabindex // no-noninteractive-tabindex
if (!is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefintionKey)) { if (!this.is_dynamic_element && !is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefinitionKey)) {
const tab_index = attribute_map.get('tabindex'); const tab_index = attribute_map.get('tabindex');
if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) { if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex); component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);
@ -671,7 +748,7 @@ export default class Element extends Node {
// role-supports-aria-props // role-supports-aria-props
const role = attribute_map.get('role'); const role = attribute_map.get('role');
const role_value = (role ? role.get_static_value() : get_implicit_role(this.name, attribute_map)) as ARIARoleDefintionKey; const role_value = (role ? role.get_static_value() : get_implicit_role(this.name, attribute_map)) as ARIARoleDefinitionKey;
if (typeof role_value === 'string' && roles.has(role_value)) { if (typeof role_value === 'string' && roles.has(role_value)) {
const { props } = roles.get(role_value); const { props } = roles.get(role_value);
const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props))); const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props)));
@ -978,7 +1055,8 @@ export default class Element extends Node {
name === 'muted' || name === 'muted' ||
name === 'playbackRate' || name === 'playbackRate' ||
name === 'seeking' || name === 'seeking' ||
name === 'ended' name === 'ended' ||
name === 'readyState'
) { ) {
if (this.name !== 'audio' && this.name !== 'video') { if (this.name !== 'audio' && this.name !== 'video') {
return component.error(binding, compiler_errors.invalid_binding_element_with('audio> or <video>', name)); return component.error(binding, compiler_errors.invalid_binding_element_with('audio> or <video>', name));
@ -1005,20 +1083,17 @@ export default class Element extends Node {
if (this.name !== 'img') { if (this.name !== 'img') {
return component.error(binding, compiler_errors.invalid_binding_element_with('<img>', name)); return component.error(binding, compiler_errors.invalid_binding_element_with('<img>', name));
} }
} else if ( } else if (is_name_contenteditable(name)) {
name === 'textContent' || const contenteditable = get_contenteditable_attr(this);
name === 'innerHTML'
) {
const contenteditable = this.attributes.find(
(attribute: Attribute) => attribute.name === 'contenteditable'
);
if (!contenteditable) { if (!contenteditable) {
return component.error(binding, compiler_errors.missing_contenteditable_attribute); return component.error(binding, compiler_errors.missing_contenteditable_attribute);
} else if (contenteditable && !contenteditable.is_static) { } else if (contenteditable && !contenteditable.is_static) {
return component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute); return component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute);
} }
} else if (name !== 'this') { } else if (
name !== 'this' &&
!regex_box_size.test(name)
) {
return component.error(binding, compiler_errors.invalid_binding(binding.name)); return component.error(binding, compiler_errors.invalid_binding(binding.name));
} }
}); });

@ -17,6 +17,7 @@ export default class ThenBlock extends AbstractBlock {
this.scope = scope.child(); this.scope = scope.child();
if (parent.then_node) { if (parent.then_node) {
parent.then_contexts.forEach(context => { parent.then_contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this); this.scope.add(context.key.name, parent.expression.dependencies, this);
}); });
} }

@ -12,6 +12,7 @@ import StyleDirective from './StyleDirective';
import Comment from './Comment'; import Comment from './Comment';
import ConstTag from './ConstTag'; import ConstTag from './ConstTag';
import DebugTag from './DebugTag'; import DebugTag from './DebugTag';
import Document from './Document';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element'; import Element from './Element';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
@ -47,6 +48,7 @@ export type INode = Action
| Comment | Comment
| ConstTag | ConstTag
| DebugTag | DebugTag
| Document
| EachBlock | EachBlock
| Element | Element
| ElseBlock | ElseBlock

@ -1,5 +1,5 @@
import { x } from 'code-red'; import { x } from 'code-red';
import { Node, Identifier, Expression } from 'estree'; import { Node, Identifier, Expression, PrivateIdentifier } from 'estree';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference'; import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone'; import { clone } from '../../../utils/clone';
@ -7,7 +7,16 @@ import Component from '../../Component';
import flatten_reference from '../../utils/flatten_reference'; import flatten_reference from '../../utils/flatten_reference';
import TemplateScope from './TemplateScope'; import TemplateScope from './TemplateScope';
export interface Context { export type Context = DestructuredVariable | ComputedProperty;
interface ComputedProperty {
type: 'ComputedProperty';
property_name: Identifier;
key: Expression | PrivateIdentifier;
}
interface DestructuredVariable {
type: 'DestructuredVariable'
key: Identifier; key: Identifier;
name?: string; name?: string;
modifier: (node: Node) => Node; modifier: (node: Node) => Node;
@ -25,8 +34,8 @@ export function unpack_destructuring({
}: { }: {
contexts: Context[]; contexts: Context[];
node: Node; node: Node;
modifier?: Context['modifier']; modifier?: DestructuredVariable['modifier'];
default_modifier?: Context['default_modifier']; default_modifier?: DestructuredVariable['default_modifier'];
scope: TemplateScope; scope: TemplateScope;
component: Component; component: Component;
context_rest_properties: Map<string, Node>; context_rest_properties: Map<string, Node>;
@ -35,12 +44,14 @@ export function unpack_destructuring({
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
contexts.push({ contexts.push({
type: 'DestructuredVariable',
key: node as Identifier, key: node as Identifier,
modifier, modifier,
default_modifier default_modifier
}); });
} else if (node.type === 'RestElement') { } else if (node.type === 'RestElement') {
contexts.push({ contexts.push({
type: 'DestructuredVariable',
key: node.argument as Identifier, key: node.argument as Identifier,
modifier, modifier,
default_modifier default_modifier
@ -108,12 +119,38 @@ export function unpack_destructuring({
context_rest_properties context_rest_properties
}); });
context_rest_properties.set((property.argument as Identifier).name, property); context_rest_properties.set((property.argument as Identifier).name, property);
} else { } else if (property.type === 'Property') {
const key = property.key as Identifier; const key = property.key;
const value = property.value; const value = property.value;
used_properties.push(x`"${key.name}"`); let new_modifier: (node: Node) => Node;
if (property.computed) {
// e.g { [computedProperty]: ... }
const property_name = component.get_unique_name('computed_property');
contexts.push({
type: 'ComputedProperty',
property_name,
key
});
new_modifier = (node) => x`${modifier(node)}[${property_name}]`;
used_properties.push(x`${property_name}`);
} else if (key.type === 'Identifier') {
// e.g. { someProperty: ... }
const property_name = key.name;
new_modifier = (node) => x`${modifier(node)}.${property_name}`;
used_properties.push(x`"${property_name}"`);
} else if (key.type === 'Literal') {
// e.g. { "property-in-quotes": ... } or { 14: ... }
const property_name = key.value;
new_modifier = (node) => x`${modifier(node)}["${property_name}"]`;
used_properties.push(x`"${property_name}"`);
}
if (value.type === 'AssignmentPattern') { if (value.type === 'AssignmentPattern') {
// e.g. { property = default } or { property: newName = default }
const n = contexts.length; const n = contexts.length;
mark_referenced(value.right, scope, component); mark_referenced(value.right, scope, component);
@ -121,7 +158,7 @@ export function unpack_destructuring({
unpack_destructuring({ unpack_destructuring({
contexts, contexts,
node: value.left, node: value.left,
modifier: (node) => x`${modifier(node)}.${key.name}`, modifier: new_modifier,
default_modifier: (node, to_ctx) => default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference( x`${node} !== undefined ? ${node} : ${update_reference(
contexts, contexts,
@ -134,10 +171,11 @@ export function unpack_destructuring({
context_rest_properties context_rest_properties
}); });
} else { } else {
// e.g. { property } or { property: newName }
unpack_destructuring({ unpack_destructuring({
contexts, contexts,
node: value, node: value,
modifier: (node) => x`${modifier(node)}.${key.name}` as Node, modifier: new_modifier,
default_modifier, default_modifier,
scope, scope,
component, component,
@ -157,7 +195,9 @@ function update_reference(
): Node { ): Node {
const find_from_context = (node: Identifier) => { const find_from_context = (node: Identifier) => {
for (let i = n; i < contexts.length; i++) { for (let i = n; i < contexts.length; i++) {
const { key } = contexts[i]; const cur_context = contexts[i];
if (cur_context.type !== 'DestructuredVariable') continue;
const { key } = cur_context;
if (node.name === key.name) { if (node.name === key.name) {
throw new Error(`Cannot access '${node.name}' before initialization`); throw new Error(`Cannot access '${node.name}' before initialization`);
} }

@ -24,7 +24,7 @@ type Owner = INode;
const regex_contains_term_function_expression = /FunctionExpression/; const regex_contains_term_function_expression = /FunctionExpression/;
export default class Expression { export default class Expression {
type: 'Expression' = 'Expression'; type: 'Expression' = 'Expression' as const;
component: Component; component: Component;
owner: Owner; owner: Owner;
node: Node; node: Node;
@ -373,6 +373,7 @@ export default class Expression {
// add to get_xxx_context // add to get_xxx_context
// child_ctx[x] = function () { ... } // child_ctx[x] = function () { ... }
(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({ (template_scope.get_owner(deps[0]) as EachBlock).contexts.push({
type: 'DestructuredVariable',
key: func_id, key: func_id,
modifier: () => func_expression, modifier: () => func_expression,
default_modifier: node => node default_modifier: node => node

@ -3,6 +3,7 @@ import Body from '../Body';
import ConstTag from '../ConstTag'; import ConstTag from '../ConstTag';
import Comment from '../Comment'; import Comment from '../Comment';
import EachBlock from '../EachBlock'; import EachBlock from '../EachBlock';
import Document from '../Document';
import Element from '../Element'; import Element from '../Element';
import Head from '../Head'; import Head from '../Head';
import IfBlock from '../IfBlock'; import IfBlock from '../IfBlock';
@ -28,6 +29,7 @@ function get_constructor(type) {
case 'Body': return Body; case 'Body': return Body;
case 'Comment': return Comment; case 'Comment': return Comment;
case 'ConstTag': return ConstTag; case 'ConstTag': return ConstTag;
case 'Document': return Document;
case 'EachBlock': return EachBlock; case 'EachBlock': return EachBlock;
case 'Element': return Element; case 'Element': return Element;
case 'Head': return Head; case 'Head': return Head;

@ -1,4 +1,4 @@
import Renderer from './Renderer'; import Renderer, { BindingGroup } from './Renderer';
import Wrapper from './wrappers/shared/Wrapper'; import Wrapper from './wrappers/shared/Wrapper';
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import { Node, Identifier, ArrayPattern } from 'estree'; import { Node, Identifier, ArrayPattern } from 'estree';
@ -40,6 +40,7 @@ export default class Block {
bindings: Map<string, Bindings>; bindings: Map<string, Bindings>;
binding_group_initialised: Set<string> = new Set(); binding_group_initialised: Set<string> = new Set();
binding_groups: Set<BindingGroup> = new Set();
chunks: { chunks: {
declarations: Array<Node | Node[]>; declarations: Array<Node | Node[]>;
@ -249,6 +250,7 @@ export default class Block {
} }
} }
this.render_binding_groups();
this.render_listeners(); this.render_listeners();
const properties: Record<string, any> = {}; const properties: Record<string, any> = {};
@ -500,4 +502,10 @@ export default class Block {
} }
} }
} }
render_binding_groups() {
for (const binding_group of this.binding_groups) {
binding_group.render(this);
}
}
} }

@ -22,6 +22,15 @@ type BitMasks = Array<{
names: string[]; names: string[];
}>; }>;
export interface BindingGroup {
binding_group: (to_reference?: boolean) => Node;
contexts: string[];
list_dependencies: Set<string>;
keypath: string;
add_element: (block: Block, element: Identifier) => void;
render: (block: Block) => void;
}
export default class Renderer { export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component? component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions; options: CompileOptions;
@ -33,7 +42,7 @@ export default class Renderer {
blocks: Array<Block | Node | Node[]> = []; blocks: Array<Block | Node | Node[]> = [];
readonly: Set<string> = new Set(); readonly: Set<string> = new Set();
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map(); binding_groups: Map<string, BindingGroup> = new Map();
block: Block; block: Block;
fragment: FragmentWrapper; fragment: FragmentWrapper;
@ -64,10 +73,6 @@ export default class Renderer {
this.add_to_context('#slots'); this.add_to_context('#slots');
} }
if (this.binding_groups.size > 0) {
this.add_to_context('$$binding_groups');
}
// main block // main block
this.block = new Block({ this.block = new Block({
renderer: this, renderer: this,

@ -1,3 +1,4 @@
import type { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping';
import { b, x, p } from 'code-red'; import { b, x, p } from 'code-red';
import Component from '../Component'; import Component from '../Component';
import Renderer from './Renderer'; import Renderer from './Renderer';
@ -8,7 +9,6 @@ import { invalidate } from './invalidate';
import Block from './Block'; import Block from './Block';
import { ImportDeclaration, ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree'; import { ImportDeclaration, ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/mapped_code'; import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { flatten } from '../../utils/flatten'; import { flatten } from '../../utils/flatten';
import check_enable_sourcemap from '../utils/check_enable_sourcemap'; import check_enable_sourcemap from '../utils/check_enable_sourcemap';
import { push_array } from '../../utils/push_array'; import { push_array } from '../../utils/push_array';
@ -390,13 +390,13 @@ export default function dom(
const resubscribable_reactive_store_unsubscribers = reactive_stores const resubscribable_reactive_store_unsubscribers = reactive_stores
.filter(store => { .filter(store => {
const variable = component.var_lookup.get(store.name.slice(1)); const variable = component.var_lookup.get(store.name.slice(1));
return variable && (variable.reassigned || variable.export_name) && !variable.is_reactive_static; return variable && (variable.reassigned || variable.export_name);
}) })
.map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`); .map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`);
if (has_definition) { if (has_definition) {
const reactive_declarations: Node[] = []; const reactive_declarations: (Node | Node[]) = [];
const fixed_reactive_declarations: Array<Node | Node[]> = []; // not really 'reactive' but whatever const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever
component.reactive_declarations.forEach(d => { component.reactive_declarations.forEach(d => {
const dependencies = Array.from(d.dependencies); const dependencies = Array.from(d.dependencies);
@ -417,15 +417,6 @@ export default function dom(
reactive_declarations.push(statement); reactive_declarations.push(statement);
} else { } else {
fixed_reactive_declarations.push(statement); fixed_reactive_declarations.push(statement);
for (const assignee of d.assignees) {
const variable = component.var_lookup.get(assignee);
if (variable && variable.subscribable) {
fixed_reactive_declarations.push(b`
${component.compile_options.dev && b`@validate_store(${assignee}, '${assignee}');`}
@component_subscribe($$self, ${assignee}, $$value => $$invalidate(${renderer.context_lookup.get('$' + assignee).index}, ${'$' + assignee} = $$value));
`);
}
}
} }
}); });
@ -439,7 +430,7 @@ export default function dom(
const name = $name.slice(1); const name = $name.slice(1);
const store = component.var_lookup.get(name); const store = component.var_lookup.get(name);
if (store && (store.reassigned || store.export_name) && !store.is_reactive_static) { if (store && (store.reassigned || store.export_name)) {
const unsubscribe = `$$unsubscribe_${name}`; const unsubscribe = `$$unsubscribe_${name}`;
const subscribe = `$$subscribe_${name}`; const subscribe = `$$subscribe_${name}`;
const i = renderer.context_lookup.get($name).index; const i = renderer.context_lookup.get($name).index;

@ -19,7 +19,6 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
!variable.hoistable && !variable.hoistable &&
!variable.global && !variable.global &&
!variable.module && !variable.module &&
!variable.is_reactive_static &&
( (
variable.referenced || variable.referenced ||
variable.subscribable || variable.subscribable ||

@ -11,6 +11,7 @@ import CatchBlock from '../../nodes/CatchBlock';
import { Context } from '../../nodes/shared/Context'; import { Context } from '../../nodes/shared/Context';
import { Identifier, Literal, Node } from 'estree'; import { Identifier, Literal, Node } from 'estree';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags'; import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
type Status = 'pending' | 'then' | 'catch'; type Status = 'pending' | 'then' | 'catch';
@ -69,6 +70,7 @@ class AwaitBlockBranch extends Wrapper {
this.renderer.add_to_context(this.value, true); this.renderer.add_to_context(this.value, true);
} else { } else {
contexts.forEach(context => { contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
this.renderer.add_to_context(context.key.name, true); this.renderer.add_to_context(context.key.name, true);
}); });
this.value = this.block.parent.get_unique_name('value').name; this.value = this.block.parent.get_unique_name('value').name;
@ -96,7 +98,15 @@ class AwaitBlockBranch extends Wrapper {
} }
render_get_context() { render_get_context() {
const props = this.is_destructured ? this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), name => this.renderer.reference(name))};`) : null; const props = this.is_destructured ? this.value_contexts.map(prop => {
if (prop.type === 'ComputedProperty') {
const expression = new Expression(this.renderer.component, this.node, this.has_consts(this.node) ? this.node.scope : null, prop.key);
return b`const ${prop.property_name} = ${expression.manipulate(this.block, '#ctx')};`;
} else {
const to_ctx = name => this.renderer.reference(name);
return b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), to_ctx)};`;
}
}) : null;
const const_tags_props = this.has_consts(this.node) ? add_const_tags(this.block, this.node.const_tags, '#ctx') : null; const const_tags_props = this.has_consts(this.node) ? add_const_tags(this.block, this.node.const_tags, '#ctx') : null;

@ -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
);
}
}

@ -0,0 +1,25 @@
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import Document from '../../nodes/Document';
import { Identifier } from 'estree';
import EventHandler from './Element/EventHandler';
import add_event_handlers from './shared/add_event_handlers';
import { TemplateNode } from '../../../interfaces';
import Renderer from '../Renderer';
import add_actions from './shared/add_actions';
export default class DocumentWrapper extends Wrapper {
node: Document;
handlers: EventHandler[];
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
super(renderer, block, parent, node);
this.handlers = this.node.handlers.map(handler => new EventHandler(handler, this));
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
add_event_handlers(block, x`@_document`, this.handlers);
add_actions(block, x`@_document`, this.node.actions);
}
}

@ -9,6 +9,7 @@ import ElseBlock from '../../nodes/ElseBlock';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import get_object from '../../utils/get_object'; import get_object from '../../utils/get_object';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags'; import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
export class ElseBlockWrapper extends Wrapper { export class ElseBlockWrapper extends Wrapper {
node: ElseBlock; node: ElseBlock;
@ -86,6 +87,7 @@ export default class EachBlockWrapper extends Wrapper {
block.add_dependencies(dependencies); block.add_dependencies(dependencies);
this.node.contexts.forEach(context => { this.node.contexts.forEach(context => {
if (context.type !== 'DestructuredVariable') return;
renderer.add_to_context(context.key.name, true); renderer.add_to_context(context.key.name, true);
}); });
add_const_tags_context(renderer, this.node.const_tags); add_const_tags_context(renderer, this.node.const_tags);
@ -147,6 +149,7 @@ export default class EachBlockWrapper extends Wrapper {
const store = object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null; const store = object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null;
node.contexts.forEach(prop => { node.contexts.forEach(prop => {
if (prop.type !== 'DestructuredVariable') return;
this.block.bindings.set(prop.key.name, { this.block.bindings.set(prop.key.name, {
object: this.vars.each_block_value, object: this.vars.each_block_value,
property: this.index_name, property: this.index_name,
@ -361,7 +364,15 @@ export default class EachBlockWrapper extends Wrapper {
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier); this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
} }
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`list[i]`), name => renderer.context_lookup.has(name) ? x`child_ctx[${renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name })};`); this.context_props = this.node.contexts.map(prop => {
if (prop.type === 'DestructuredVariable') {
const to_ctx = (name: string) => renderer.context_lookup.has(name) ? x`child_ctx[${renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name } as Node;
return b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`list[i]`), to_ctx)};`;
} else {
const expression = new Expression(this.renderer.component, this.node, this.node.scope, prop.key);
return b`const ${prop.property_name} = ${expression.manipulate(block, 'child_ctx')};`;
}
});
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`); if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`); if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);

@ -85,6 +85,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
if (node.name === 'value') { if (node.name === 'value') {
handle_select_value_binding(this, node.dependencies); handle_select_value_binding(this, node.dependencies);
this.parent.has_dynamic_value = true;
} }
} }
@ -180,6 +181,17 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
`; `;
} }
if (this.node.name === 'value' && dependencies.length > 0) {
if (this.parent.bindings.some(binding => binding.node.name === 'group')) {
this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed');
block.add_variable(this.parent.dynamic_value_condition, x`false`);
updater = b`
${updater}
${this.parent.dynamic_value_condition} = true;
`;
}
}
if (dependencies.length > 0) { if (dependencies.length > 0) {
const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies)); const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));
@ -352,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'] },

@ -5,12 +5,13 @@ import InlineComponentWrapper from '../InlineComponent';
import get_object from '../../../utils/get_object'; import get_object from '../../../utils/get_object';
import replace_object from '../../../utils/replace_object'; import replace_object from '../../../utils/replace_object';
import Block from '../../Block'; import Block from '../../Block';
import Renderer from '../../Renderer'; import Renderer, { BindingGroup } from '../../Renderer';
import flatten_reference from '../../../utils/flatten_reference'; import flatten_reference from '../../../utils/flatten_reference';
import { Node, Identifier } from 'estree'; import { Node, Identifier } from 'estree';
import add_to_set from '../../../utils/add_to_set'; import add_to_set from '../../../utils/add_to_set';
import mark_each_block_bindings from '../shared/mark_each_block_bindings'; import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import handle_select_value_binding from './handle_select_value_binding'; import handle_select_value_binding from './handle_select_value_binding';
import { regex_box_size } from '../../../../utils/patterns';
export default class BindingWrapper { export default class BindingWrapper {
node: Binding; node: Binding;
@ -26,6 +27,7 @@ export default class BindingWrapper {
snippet: Node; snippet: Node;
is_readonly: boolean; is_readonly: boolean;
needs_lock: boolean; needs_lock: boolean;
binding_group: BindingGroup;
constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) { constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) {
this.node = node; this.node = node;
@ -45,6 +47,10 @@ export default class BindingWrapper {
this.object = get_object(this.node.expression.node).name; this.object = get_object(this.node.expression.node).name;
if (this.node.name === 'group') {
this.binding_group = get_binding_group(parent.renderer, this, block);
}
// view to model // view to model
this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression); this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression);
@ -67,6 +73,10 @@ export default class BindingWrapper {
} }
}); });
if (this.binding_group) {
this.binding_group.list_dependencies.forEach(dep => dependencies.add(dep));
}
return dependencies; return dependencies;
} }
@ -105,6 +115,7 @@ export default class BindingWrapper {
const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : []; const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : [];
const mount_conditions: any[] = []; const mount_conditions: any[] = [];
let update_or_condition: any = null;
const dependency_array = Array.from(this.get_dependencies()); const dependency_array = Array.from(this.get_dependencies());
@ -135,40 +146,19 @@ export default class BindingWrapper {
} }
// model to view // model to view
let update_dom = get_dom_updater(parent, this); let update_dom = get_dom_updater(parent, this, false);
let mount_dom = update_dom; let mount_dom = get_dom_updater(parent, this, true);
// special cases // special cases
switch (this.node.name) { switch (this.node.name) {
case 'group': case 'group':
{ {
const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block);
block.renderer.add_to_context('$$binding_groups'); block.renderer.add_to_context('$$binding_groups');
this.binding_group.add_element(block, this.parent.var);
if (is_context && !block.binding_group_initialised.has(keypath)) { if ((this.parent as ElementWrapper).has_dynamic_value) {
if (contexts.length > 1) { update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition;
let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`;
for (const name of contexts.slice(0, -1)) {
binding_group = x`${binding_group}[${block.renderer.reference(name)}]`;
block.chunks.init.push(
b`${binding_group} = ${binding_group} || [];`
);
}
}
block.chunks.init.push(
b`${binding_group(true)} = [];`
);
block.binding_group_initialised.add(keypath);
} }
block.chunks.hydrate.push(
b`${binding_group(true)}.push(${parent.var});`
);
block.chunks.destroy.push(
b`${binding_group(true)}.splice(${binding_group(true)}.indexOf(${parent.var}), 1);`
);
break; break;
} }
@ -176,6 +166,11 @@ export default class BindingWrapper {
update_conditions.push(x`${this.snippet} !== ${parent.var}.textContent`); update_conditions.push(x`${this.snippet} !== ${parent.var}.textContent`);
mount_conditions.push(x`${this.snippet} !== void 0`); mount_conditions.push(x`${this.snippet} !== void 0`);
break; break;
case 'innerText':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerText`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'innerHTML': case 'innerHTML':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`); update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`);
@ -214,7 +209,8 @@ export default class BindingWrapper {
if (update_dom) { if (update_dom) {
if (update_conditions.length > 0) { if (update_conditions.length > 0) {
const condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`); let condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
if (update_or_condition) condition = x`${update_or_condition} || (${condition})`;
block.chunks.update.push(b` block.chunks.update.push(b`
if (${condition}) { if (${condition}) {
@ -244,7 +240,8 @@ export default class BindingWrapper {
function get_dom_updater( function get_dom_updater(
element: ElementWrapper | InlineComponentWrapper, element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper binding: BindingWrapper,
mounting: boolean
) { ) {
const { node } = element; const { node } = element;
@ -259,6 +256,7 @@ function get_dom_updater(
if (node.name === 'select') { if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ? return node.get_static_attribute_value('multiple') === true ?
b`@select_options(${element.var}, ${binding.snippet})` : b`@select_options(${element.var}, ${binding.snippet})` :
mounting ? b`@select_option(${element.var}, ${binding.snippet}, true)` :
b`@select_option(${element.var}, ${binding.snippet})`; b`@select_option(${element.var}, ${binding.snippet})`;
} }
@ -279,7 +277,8 @@ function get_dom_updater(
return b`${element.var}.${binding.node.name} = ${binding.snippet};`; return b`${element.var}.${binding.node.name} = ${binding.snippet};`;
} }
function get_binding_group(renderer: Renderer, value: Binding, block: Block) { function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: Block) {
const value = binding.node;
const { parts } = flatten_reference(value.raw_expression); const { parts } = flatten_reference(value.raw_expression);
let keypath = parts.join('.'); let keypath = parts.join('.');
@ -314,41 +313,84 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
contexts.push(name); contexts.push(name);
} }
// create a global binding_group across blocks
if (!renderer.binding_groups.has(keypath)) { if (!renderer.binding_groups.has(keypath)) {
const index = renderer.binding_groups.size; const index = renderer.binding_groups.size;
// the bind:group depends on the list in the {#each} block as well
// as reordering (removing and adding back to the DOM) may affect the value
const list_dependencies = new Set<string>();
let parent = value.parent;
while (parent) {
if (parent.type === 'EachBlock') {
for (const dep of parent.expression.dynamic_dependencies()) {
list_dependencies.add(dep);
}
}
parent = parent.parent;
}
/**
* When using bind:group with logic blocks, the inputs with bind:group may be scattered across different blocks.
* This therefore keeps track of all the <input> elements that have the same bind:group within the same block.
*/
const elements = new Map<Block, any>();
contexts.forEach(context => { contexts.forEach(context => {
renderer.add_to_context(context, true); renderer.add_to_context(context, true);
}); });
renderer.binding_groups.set(keypath, { renderer.binding_groups.set(keypath, {
binding_group: (to_reference: boolean = false) => { binding_group: () => {
let binding_group = '$$binding_groups'; let obj = x`$$binding_groups[${index}]`;
let _secondary_indexes = contexts;
if (to_reference) { if (contexts.length > 0) {
binding_group = block.renderer.reference(binding_group); contexts.forEach(secondary_index => {
_secondary_indexes = _secondary_indexes.map(name => block.renderer.reference(name));
}
if (_secondary_indexes.length > 0) {
let obj = x`${binding_group}[${index}]`;
_secondary_indexes.forEach(secondary_index => {
obj = x`${obj}[${secondary_index}]`; obj = x`${obj}[${secondary_index}]`;
}); });
return obj;
} else {
return x`${binding_group}[${index}]`;
} }
return obj;
}, },
is_context: contexts.length > 0,
contexts, contexts,
index, list_dependencies,
keypath keypath,
add_element(block, element) {
if (!elements.has(block)) {
elements.set(block, []);
}
elements.get(block).push(element);
},
render(block) {
const local_name = block.get_unique_name('binding_group');
const binding_group = block.renderer.reference('$$binding_groups');
block.add_variable(local_name);
if (contexts.length > 0) {
const indexes = { type: 'ArrayExpression', elements: contexts.map(name => block.renderer.reference(name)) };
block.chunks.init.push(
b`${local_name} = @init_binding_group_dynamic(${binding_group}[${index}], ${indexes})`
);
block.chunks.update.push(
b`if (${block.renderer.dirty(Array.from(list_dependencies))}) ${local_name}.u(${indexes})`
);
} else {
block.chunks.init.push(
b`${local_name} = @init_binding_group(${binding_group}[${index}])`
);
}
block.chunks.hydrate.push(
b`${local_name}.p(${elements.get(block)})`
);
block.chunks.destroy.push(
b`${local_name}.r()`
);
}
}); });
} }
return renderer.binding_groups.get(keypath); // register the binding_group for the block
const binding_group = renderer.binding_groups.get(keypath);
block.binding_groups.add(binding_group);
return binding_group;
} }
function get_event_handler( function get_event_handler(
@ -386,7 +428,7 @@ function get_event_handler(
} }
} }
const value = get_value_from_dom(renderer, binding.parent, binding, block, contextual_dependencies); const value = get_value_from_dom(renderer, binding.parent, binding, contextual_dependencies);
const mutation = b` const mutation = b`
${lhs} = ${value}; ${lhs} = ${value};
@ -402,10 +444,9 @@ function get_event_handler(
} }
function get_value_from_dom( function get_value_from_dom(
renderer: Renderer, _renderer: Renderer,
element: ElementWrapper | InlineComponentWrapper, element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper, binding: BindingWrapper,
block: Block,
contextual_dependencies: Set<string> contextual_dependencies: Set<string>
) { ) {
const { node } = element; const { node } = element;
@ -415,6 +456,11 @@ function get_value_from_dom(
return x`$$value`; return x`$$value`;
} }
// <div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>
if (regex_box_size.test(name)) {
return x`@ResizeObserverSingleton.entries.get(this)?.${name}`;
}
// <select bind:value='selected> // <select bind:value='selected>
if (node.name === 'select') { if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ? return node.get_static_attribute_value('multiple') === true ?
@ -427,7 +473,7 @@ function get_value_from_dom(
// <input type='checkbox' bind:group='foo'> // <input type='checkbox' bind:group='foo'>
if (name === 'group') { if (name === 'group') {
if (type === 'checkbox') { if (type === 'checkbox') {
const { binding_group, contexts } = get_binding_group(renderer, binding.node, block); const { binding_group, contexts } = binding.binding_group;
add_to_set(contextual_dependencies, contexts); add_to_set(contextual_dependencies, contexts);
return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`; return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`;
} }

@ -41,6 +41,7 @@ export default class EventHandlerWrapper {
if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`; if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`;
if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`; if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`;
if (this.node.modifiers.has('stopImmediatePropagation')) snippet = x`@stop_immediate_propagation(${snippet})`;
if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`; if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`;
if (this.node.modifiers.has('trusted')) snippet = x`@trusted(${snippet})`; if (this.node.modifiers.has('trusted')) snippet = x`@trusted(${snippet})`;
@ -64,6 +65,7 @@ export default class EventHandlerWrapper {
if (block.renderer.options.dev) { if (block.renderer.options.dev) {
args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE); args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE);
args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE); args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE);
args.push(this.node.modifiers.has('stopImmediatePropagation') ? TRUE : FALSE);
} }
block.event_listeners.push( block.event_listeners.push(

@ -12,7 +12,7 @@ import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute'; import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute'; import StyleAttributeWrapper from './StyleAttribute';
import SpreadAttributeWrapper from './SpreadAttribute'; import SpreadAttributeWrapper from './SpreadAttribute';
import { regex_dimensions, regex_starts_with_newline, regex_backslashes } from '../../../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_backslashes, regex_border_box_size, regex_content_box_size, regex_device_pixel_content_box_size, regex_content_rect } from '../../../../utils/patterns';
import Binding from './Binding'; import Binding from './Binding';
import add_to_set from '../../../utils/add_to_set'; import add_to_set from '../../../utils/add_to_set';
import { add_event_handler } from '../shared/add_event_handlers'; import { add_event_handler } from '../shared/add_event_handlers';
@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag'; import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag';
import is_dynamic from '../shared/is_dynamic'; import is_dynamic from '../shared/is_dynamic';
import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable';
import create_debugging_comment from '../shared/create_debugging_comment'; import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array'; import { push_array } from '../../../../utils/push_array';
@ -48,8 +49,8 @@ const events = [
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) =>
(name === 'textContent' || name === 'innerHTML') && is_name_contenteditable(name) &&
node.attributes.some(attribute => attribute.name === 'contenteditable') has_contenteditable_attr(node)
}, },
{ {
event_names: ['change'], event_names: ['change'],
@ -63,13 +64,29 @@ const events = [
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'input' && node.get_static_attribute_value('type') === 'range' node.name === 'input' && node.get_static_attribute_value('type') === 'range'
}, },
// resize events
{ {
event_names: ['elementresize'], event_names: ['elementresize'],
filter: (_node: Element, name: string) => filter: (_node: Element, name: string) =>
regex_dimensions.test(name) regex_dimensions.test(name)
}, },
{
event_names: ['elementresizecontentbox'],
filter: (_node: Element, name: string) =>
regex_content_rect.test(name) ?? regex_content_box_size.test(name)
},
{
event_names: ['elementresizeborderbox'],
filter: (_node: Element, name: string) =>
regex_border_box_size.test(name)
},
{
event_names: ['elementresizedevicepixelcontentbox'],
filter: (_node: Element, name: string) =>
regex_device_pixel_content_box_size.test(name)
},
// media events // media events
{ {
event_names: ['timeupdate'], event_names: ['timeupdate'],
@ -131,7 +148,14 @@ const events = [
node.is_media_node() && node.is_media_node() &&
(name === 'videoHeight' || name === 'videoWidth') (name === 'videoHeight' || name === 'videoWidth')
}, },
{
// from https://html.spec.whatwg.org/multipage/media.html#ready-states
// and https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource
event_names: ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'readyState'
},
// details event // details event
{ {
event_names: ['toggle'], event_names: ['toggle'],
@ -155,8 +179,12 @@ export default class ElementWrapper extends Wrapper {
bindings: Binding[]; bindings: Binding[];
event_handlers: EventHandler[]; event_handlers: EventHandler[];
class_dependencies: string[]; class_dependencies: string[];
dynamic_style_dependencies: Set<string>;
has_dynamic_attribute: boolean;
select_binding_dependencies?: Set<string>; select_binding_dependencies?: Set<string>;
has_dynamic_value: boolean;
dynamic_value_condition: any;
var: any; var: any;
void: boolean; void: boolean;
@ -164,6 +192,8 @@ export default class ElementWrapper extends Wrapper {
child_dynamic_element_block?: Block = null; child_dynamic_element_block?: Block = null;
child_dynamic_element?: ElementWrapper = null; child_dynamic_element?: ElementWrapper = null;
element_data_name = null;
constructor( constructor(
renderer: Renderer, renderer: Renderer,
block: Block, block: Block,
@ -206,6 +236,8 @@ export default class ElementWrapper extends Wrapper {
return; return;
} }
this.dynamic_style_dependencies = new Set();
if (this.node.children.length) { if (this.node.children.length) {
this.node.lets.forEach(l => { this.node.lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => { extract_names(l.value || l.name).forEach(name => {
@ -223,6 +255,7 @@ export default class ElementWrapper extends Wrapper {
} }
return new AttributeWrapper(this, block, attribute); return new AttributeWrapper(this, block, attribute);
}); });
this.has_dynamic_attribute = !!this.attributes.find(attr => attr.node.get_dependencies().length > 0);
// ordinarily, there'll only be one... but we need to handle // ordinarily, there'll only be one... but we need to handle
// the rare case where an element can have multiple bindings, // the rare case where an element can have multiple bindings,
@ -274,6 +307,8 @@ export default class ElementWrapper extends Wrapper {
} }
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
this.element_data_name = block.get_unique_name(`${this.var.name}_data`);
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
@ -291,9 +326,8 @@ export default class ElementWrapper extends Wrapper {
(x`#nodes` as unknown) as Identifier (x`#nodes` as unknown) as Identifier
); );
const previous_tag = block.get_unique_name('previous_tag'); const is_tag_dynamic = this.node.tag_expr.dynamic_dependencies().length > 0;
const tag = this.node.tag_expr.manipulate(block); const tag = this.node.tag_expr.manipulate(block);
block.add_variable(previous_tag, tag);
block.chunks.init.push(b` block.chunks.init.push(b`
${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`}
@ -315,62 +349,72 @@ export default class ElementWrapper extends Wrapper {
if (${this.var}) ${this.var}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'}); if (${this.var}) ${this.var}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'});
`); `);
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); if (is_tag_dynamic) {
const has_transitions = !!(this.node.intro || this.node.outro); const previous_tag = block.get_unique_name('previous_tag');
const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; block.add_variable(previous_tag, tag);
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const has_transitions = !!(this.node.intro || this.node.outro);
const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
const tag_will_be_removed = block.get_unique_name('tag_will_be_removed'); const tag_will_be_removed = block.get_unique_name('tag_will_be_removed');
if (has_transitions) { if (has_transitions) {
block.add_variable(tag_will_be_removed, x`false`); block.add_variable(tag_will_be_removed, x`false`);
} }
block.chunks.update.push(b` block.chunks.update.push(b`
if (${tag}) { if (${tag}) {
if (!${previous_tag}) { if (!${previous_tag}) {
${this.var} = ${this.child_dynamic_element_block.name}(#ctx); ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);
${previous_tag} = ${tag}; ${previous_tag} = ${tag};
${this.var}.c(); ${this.var}.c();
${has_transitions && b`@transition_in(${this.var})`} ${has_transitions && b`@transition_in(${this.var})`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
} else if (${not_equal}(${previous_tag}, ${tag})) { } else if (${not_equal}(${previous_tag}, ${tag})) {
${this.var}.d(1); ${this.var}.d(1);
${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`}
${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`}
${this.var} = ${this.child_dynamic_element_block.name}(#ctx); ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);
${previous_tag} = ${tag}; ${previous_tag} = ${tag};
${this.var}.c(); ${this.var}.c();
${has_transitions && b`if (${tag_will_be_removed}) { ${has_transitions && b`if (${tag_will_be_removed}) {
${tag_will_be_removed} = false; ${tag_will_be_removed} = false;
@transition_in(${this.var}) @transition_in(${this.var})
}`} }`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
} else { } else {
${has_transitions && b`if (${tag_will_be_removed}) { ${has_transitions && b`if (${tag_will_be_removed}) {
${tag_will_be_removed} = false; ${tag_will_be_removed} = false;
@transition_in(${this.var}) @transition_in(${this.var})
}`} }`}
${this.var}.p(#ctx, #dirty); ${this.var}.p(#ctx, #dirty);
} }
} else if (${previous_tag}) { } else if (${previous_tag}) {
${has_transitions ${has_transitions
? b` ? b`
${tag_will_be_removed} = true; ${tag_will_be_removed} = true;
@group_outros(); @group_outros();
@transition_out(${this.var}, 1, 1, () => { @transition_out(${this.var}, 1, 1, () => {
${this.var} = null;
${previous_tag} = ${tag};
${tag_will_be_removed} = false;
});
@check_outros();
`
: b`
${this.var}.d(1);
${this.var} = null; ${this.var} = null;
${previous_tag} = ${tag}; ${previous_tag} = ${tag};
${tag_will_be_removed} = false; `
}); }
@check_outros(); }
` `);
: b` } else {
${this.var}.d(1); block.chunks.update.push(b`
${this.var} = null; if (${tag}) {
${previous_tag} = ${tag}; ${this.var}.p(#ctx, #dirty);
` }
} `);
} }
`);
if (this.child_dynamic_element_block.has_intros) { if (this.child_dynamic_element_block.has_intros) {
block.chunks.intro.push(b`@transition_in(${this.var});`); block.chunks.intro.push(b`@transition_in(${this.var});`);
@ -494,7 +538,8 @@ export default class ElementWrapper extends Wrapper {
child.render( child.render(
block, block,
is_template ? x`${node}.content` : node, is_template ? x`${node}.content` : node,
nodes nodes,
{ element_data_name: this.element_data_name }
); );
}); });
} }
@ -509,7 +554,11 @@ export default class ElementWrapper extends Wrapper {
block.maintain_context = true; block.maintain_context = true;
} }
this.add_attributes(block); if (this.node.is_dynamic_element) {
this.add_dynamic_element_attributes(block);
} else {
this.add_attributes(block);
}
this.add_directives_in_order(block); this.add_directives_in_order(block);
this.add_transitions(block); this.add_transitions(block);
this.add_animation(block); this.add_animation(block);
@ -716,14 +765,33 @@ export default class ElementWrapper extends Wrapper {
`); `);
binding_group.events.forEach(name => { binding_group.events.forEach(name => {
if (name === 'elementresize') { if (['elementresize', 'elementresizecontentbox', 'elementresizeborderbox', 'elementresizedevicepixelcontentbox'].indexOf(name) !== -1) {
// special case
const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`); const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`);
block.add_variable(resize_listener); block.add_variable(resize_listener);
block.chunks.mount.push( // Can't dynamically do `@fn[name]`, code-red doesn't know how to resolve it
b`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));` switch (name) {
); case 'elementresize':
block.chunks.mount.push(
b`${resize_listener} = @add_iframe_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
);
break;
case 'elementresizecontentbox':
block.chunks.mount.push(
b`${resize_listener} = @resize_observer_content_box.observe(${this.var}, ${callee}.bind(${this.var}));`
);
break;
case 'elementresizeborderbox':
block.chunks.mount.push(
b`${resize_listener} = @resize_observer_border_box.observe(${this.var}, ${callee}.bind(${this.var}));`
);
break;
case 'elementresizedevicepixelcontentbox':
block.chunks.mount.push(
b`${resize_listener} = @resize_observer_device_pixel_content_box.observe(${this.var}, ${callee}.bind(${this.var}));`
);
break;
}
block.chunks.destroy.push( block.chunks.destroy.push(
b`${resize_listener}();` b`${resize_listener}();`
@ -741,14 +809,11 @@ export default class ElementWrapper extends Wrapper {
const should_initialise = ( const should_initialise = (
this.node.name === 'select' || this.node.name === 'select' ||
binding_group.bindings.find(binding => { binding_group.bindings.find(binding => (
return ( binding.node.name === 'indeterminate' ||
binding.node.name === 'indeterminate' || is_name_contenteditable(binding.node.name) ||
binding.node.name === 'textContent' || binding.is_readonly_media_attribute()
binding.node.name === 'innerHTML' || ))
binding.is_readonly_media_attribute()
);
})
); );
if (should_initialise) { if (should_initialise) {
@ -779,15 +844,17 @@ export default class ElementWrapper extends Wrapper {
} }
add_attributes(block: Block) { add_attributes(block: Block) {
// Get all the class dependencies first // Get all the class and style dependencies first
this.attributes.forEach((attribute) => { this.attributes.forEach((attribute) => {
if (attribute.node.name === 'class') { if (attribute.node.name === 'class') {
const dependencies = attribute.node.get_dependencies(); const dependencies = attribute.node.get_dependencies();
push_array(this.class_dependencies, dependencies); push_array(this.class_dependencies, dependencies);
} else if (attribute.node.name === 'style') {
add_to_set(this.dynamic_style_dependencies, attribute.node.get_dependencies());
} }
}); });
if (this.node.attributes.some(attr => attr.is_spread) || this.node.is_dynamic_element) { if (this.node.attributes.some(attr => attr.is_spread)) {
this.add_spread_attributes(block); this.add_spread_attributes(block);
return; return;
} }
@ -799,7 +866,6 @@ export default class ElementWrapper extends Wrapper {
add_spread_attributes(block: Block) { add_spread_attributes(block: Block) {
const levels = block.get_unique_name(`${this.var.name}_levels`); const levels = block.get_unique_name(`${this.var.name}_levels`);
const data = block.get_unique_name(`${this.var.name}_data`);
const initial_props = []; const initial_props = [];
const updates = []; const updates = [];
@ -830,37 +896,26 @@ export default class ElementWrapper extends Wrapper {
block.chunks.init.push(b` block.chunks.init.push(b`
let ${levels} = [${initial_props}]; let ${levels} = [${initial_props}];
let ${data} = {}; let ${this.element_data_name} = {};
for (let #i = 0; #i < ${levels}.length; #i += 1) { for (let #i = 0; #i < ${levels}.length; #i += 1) {
${data} = @assign(${data}, ${levels}[#i]); ${this.element_data_name} = @assign(${this.element_data_name}, ${levels}[#i]);
} }
`); `);
const fn = this.node.namespace === namespaces.svg ? x`@set_svg_attributes` : x`@set_attributes`; const fn =
this.node.namespace === namespaces.svg
? x`@set_svg_attributes`
: this.node.is_dynamic_element
? x`@set_dynamic_element_data(${this.node.tag_expr.manipulate(block)})`
: x`@set_attributes`;
if (this.node.is_dynamic_element) { block.chunks.hydrate.push(
// call attribute bindings for custom element if tag is custom element b`${fn}(${this.var}, ${this.element_data_name});`
const tag = this.node.tag_expr.manipulate(block); );
const attr_update = this.node.namespace === namespaces.svg
? b`${fn}(${this.var}, ${data});`
: b`
if (/-/.test(${tag})) {
@set_custom_element_data_map(${this.var}, ${data});
} else {
${fn}(${this.var}, ${data});
}`;
block.chunks.hydrate.push(attr_update);
block.chunks.update.push(b`
${data} = @get_spread_update(${levels}, [${updates}]);
${attr_update}`
);
} else {
block.chunks.hydrate.push(
b`${fn}(${this.var}, ${data});`
);
if (this.has_dynamic_attribute) {
block.chunks.update.push(b` block.chunks.update.push(b`
${fn}(${this.var}, ${data} = @get_spread_update(${levels}, [ ${fn}(${this.var}, ${this.element_data_name} = @get_spread_update(${levels}, [
${updates} ${updates}
])); ]));
`); `);
@ -876,23 +931,23 @@ export default class ElementWrapper extends Wrapper {
} }
block.chunks.mount.push(b` block.chunks.mount.push(b`
'value' in ${data} && (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); 'value' in ${this.element_data_name} && (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value);
`); `);
block.chunks.update.push(b` block.chunks.update.push(b`
if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${data}) (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${this.element_data_name}) (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value);
`); `);
} else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) { } else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) {
const type = this.node.get_static_attribute_value('type'); const type = this.node.get_static_attribute_value('type');
if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') { if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') {
block.chunks.mount.push(b` block.chunks.mount.push(b`
if ('value' in ${data}) { if ('value' in ${this.element_data_name}) {
${this.var}.value = ${data}.value; ${this.var}.value = ${this.element_data_name}.value;
} }
`); `);
block.chunks.update.push(b` block.chunks.update.push(b`
if ('value' in ${data}) { if ('value' in ${this.element_data_name}) {
${this.var}.value = ${data}.value; ${this.var}.value = ${this.element_data_name}.value;
} }
`); `);
} }
@ -905,6 +960,35 @@ export default class ElementWrapper extends Wrapper {
} }
} }
add_dynamic_element_attributes(block: Block) {
if (this.attributes.length === 0) return;
if (this.has_dynamic_attribute) {
this.add_spread_attributes(block);
return;
}
const static_attributes = [];
this.attributes.forEach((attr) => {
if (attr instanceof SpreadAttributeWrapper) {
static_attributes.push({ type: 'SpreadElement', argument: attr.node.expression.node });
} else {
const name = attr.property_name || attr.name;
static_attributes.push(p`${name}: ${attr.get_value(block)}`);
}
});
const fn =
this.node.namespace === namespaces.svg
? x`@set_svg_attributes`
: this.node.is_dynamic_element
? x`@set_dynamic_element_data(${this.node.tag_expr.manipulate(block)})`
: x`@set_attributes`;
block.chunks.hydrate.push(
b`${fn}(${this.var}, {${static_attributes}});`
);
}
add_transitions(block: Block) { add_transitions(block: Block) {
const { intro, outro } = this.node; const { intro, outro } = this.node;
if (!intro && !outro) return; if (!intro && !outro) return;
@ -922,6 +1006,7 @@ export default class ElementWrapper extends Wrapper {
const intro_block = b` const intro_block = b`
@add_render_callback(() => { @add_render_callback(() => {
if (!#current) return;
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true); if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
${name}.run(1); ${name}.run(1);
}); });
@ -967,6 +1052,7 @@ export default class ElementWrapper extends Wrapper {
if (outro) { if (outro) {
intro_block = b` intro_block = b`
@add_render_callback(() => { @add_render_callback(() => {
if (!#current) return;
if (${outro_name}) ${outro_name}.end(1); if (${outro_name}) ${outro_name}.end(1);
${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet}); ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
${intro_name}.start(); ${intro_name}.start();
@ -1102,7 +1188,7 @@ export default class ElementWrapper extends Wrapper {
block.chunks.hydrate.push(updater); block.chunks.hydrate.push(updater);
if (has_spread || this.node.is_dynamic_element) { if ((this.node.is_dynamic_element || has_spread) && this.has_dynamic_attribute) {
block.chunks.update.push(updater); block.chunks.update.push(updater);
} else if ((dependencies && dependencies.size > 0) || this.class_dependencies.length) { } else if ((dependencies && dependencies.size > 0) || this.class_dependencies.length) {
const all_dependencies = this.class_dependencies.concat(...dependencies); const all_dependencies = this.class_dependencies.concat(...dependencies);
@ -1130,8 +1216,18 @@ export default class ElementWrapper extends Wrapper {
add_styles(block: Block) { add_styles(block: Block) {
const has_spread = this.node.attributes.some(attr => attr.is_spread); const has_spread = this.node.attributes.some(attr => attr.is_spread);
let style_changed_var: Identifier | undefined;
const maybe_create_style_changed_var = () => {
if (!style_changed_var && this.dynamic_style_dependencies.size) {
style_changed_var = block.get_unique_name('style_changed');
const style_attr_dirty = block.renderer.dirty([...this.dynamic_style_dependencies]);
block.chunks.update.push(b`const ${style_changed_var} = ${style_attr_dirty};`);
}
};
this.node.styles.forEach((style_directive) => { this.node.styles.forEach((style_directive) => {
const { name, expression, should_cache, important } = style_directive; const { name, expression, important, should_cache } = style_directive;
const snippet = expression.manipulate(block); const snippet = expression.manipulate(block);
let cached_snippet: Identifier | undefined; let cached_snippet: Identifier | undefined;
@ -1144,23 +1240,38 @@ export default class ElementWrapper extends Wrapper {
block.chunks.hydrate.push(updater); block.chunks.hydrate.push(updater);
const dependencies = expression.dynamic_dependencies(); // Assume that style has changed through the spread attribute
if (has_spread) { if (has_spread) {
block.chunks.update.push(updater); block.chunks.update.push(updater);
} else if (dependencies.length > 0) { } else {
const self_deps = expression.dynamic_dependencies();
const all_deps = new Set([
...self_deps,
...this.dynamic_style_dependencies
]);
if (all_deps.size === 0) return;
let condition = block.renderer.dirty([...all_deps]);
if (should_cache) { if (should_cache) {
block.chunks.update.push(b` condition = x`${condition} && ${cached_snippet} !== (${cached_snippet} = ${snippet})`;
if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) { }
${updater}
} if (this.dynamic_style_dependencies.size > 0) {
`); maybe_create_style_changed_var();
} else { // If all dependencies are same as the style attribute dependencies, then we can skip the dirty check
block.chunks.update.push(b` condition =
if (${block.renderer.dirty(dependencies)}) { all_deps.size === this.dynamic_style_dependencies.size
${updater} ? style_changed_var
} : x`${style_changed_var} || ${condition}`;
`);
} }
block.chunks.update.push(b`
if (${condition}) {
${updater}
}
`);
} }
}); });
} }

@ -2,8 +2,9 @@ import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Body from './Body'; import Body from './Body';
import DebugTag from './DebugTag'; import DebugTag from './DebugTag';
import Document from './Document';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element/index'; import Element from './Element';
import Head from './Head'; import Head from './Head';
import IfBlock from './IfBlock'; import IfBlock from './IfBlock';
import KeyBlock from './KeyBlock'; import KeyBlock from './KeyBlock';
@ -13,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';
@ -26,8 +28,9 @@ import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = { const wrappers = {
AwaitBlock, AwaitBlock,
Body, Body,
Comment: null, Comment,
DebugTag, DebugTag,
Document,
EachBlock, EachBlock,
Element, Element,
Head, Head,
@ -116,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);

@ -21,6 +21,7 @@ import SlotTemplate from '../../../nodes/SlotTemplate';
import { is_head } from '../shared/is_head'; import { is_head } from '../shared/is_head';
import compiler_warnings from '../../../compiler_warnings'; import compiler_warnings from '../../../compiler_warnings';
import { namespaces } from '../../../../utils/namespaces'; import { namespaces } from '../../../../utils/namespaces';
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
@ -111,9 +112,12 @@ export default class InlineComponentWrapper extends Wrapper {
return; return;
} }
const ignores = extract_ignores_above_node(this.node);
this.renderer.component.push_ignores(ignores);
if (variable.reassigned || variable.export_name || variable.is_reactive_dependency) { if (variable.reassigned || variable.export_name || variable.is_reactive_dependency) {
this.renderer.component.warn(this.node, compiler_warnings.reactive_component(name)); this.renderer.component.warn(this.node, compiler_warnings.reactive_component(name));
} }
this.renderer.component.pop_ignores();
} }
render( render(
@ -414,6 +418,7 @@ export default class InlineComponentWrapper extends Wrapper {
const switch_props = block.get_unique_name('switch_props'); const switch_props = block.get_unique_name('switch_props');
const snippet = this.node.expression.manipulate(block); const snippet = this.node.expression.manipulate(block);
const dependencies = this.node.expression.dynamic_dependencies();
if (has_css_custom_properties) { if (has_css_custom_properties) {
this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace); this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
@ -464,8 +469,13 @@ export default class InlineComponentWrapper extends Wrapper {
? b`@insert(${tmp_anchor}.parentNode, ${css_custom_properties_wrapper}, ${tmp_anchor});` ? b`@insert(${tmp_anchor}.parentNode, ${css_custom_properties_wrapper}, ${tmp_anchor});`
: b`@insert(${parent_node}, ${css_custom_properties_wrapper}, ${tmp_anchor});`); : b`@insert(${parent_node}, ${css_custom_properties_wrapper}, ${tmp_anchor});`);
let update_condition = x`${switch_value} !== (${switch_value} = ${snippet})`;
if (dependencies.length > 0) {
update_condition = x`${block.renderer.dirty(dependencies)} && ${update_condition}`;
}
block.chunks.update.push(b` block.chunks.update.push(b`
if (${switch_value} !== (${switch_value} = ${snippet})) { if (${update_condition}) {
if (${name}) { if (${name}) {
@group_outros(); @group_outros();
const old_component = ${name}; const old_component = ${name};

@ -5,7 +5,9 @@ import Wrapper from './shared/Wrapper';
import MustacheTag from '../../nodes/MustacheTag'; import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag';
import { x } from 'code-red'; import { x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier, Expression } from 'estree';
import ElementWrapper from './Element';
import AttributeWrapper from './Element/Attribute';
export default class MustacheTagWrapper extends Tag { export default class MustacheTagWrapper extends Tag {
var: Identifier = { type: 'Identifier', name: 't' }; var: Identifier = { type: 'Identifier', name: 't' };
@ -14,10 +16,40 @@ export default class MustacheTagWrapper extends Tag {
super(renderer, block, parent, node); super(renderer, block, parent, node);
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier, data: Record<string, unknown> | undefined) {
const contenteditable_attributes =
this.parent instanceof ElementWrapper &&
this.parent.attributes.filter((a) => a.node.name === 'contenteditable');
const spread_attributes =
this.parent instanceof ElementWrapper &&
this.parent.attributes.filter((a) => a.node.is_spread);
let contenteditable_attr_value: Expression | true | undefined = undefined;
if (contenteditable_attributes.length > 0) {
const attribute = contenteditable_attributes[0] as AttributeWrapper;
if ([true, 'true', ''].includes(attribute.node.get_static_value())) {
contenteditable_attr_value = true;
} else {
contenteditable_attr_value = x`${attribute.get_value(block)}`;
}
} else if (spread_attributes.length > 0 && data.element_data_name) {
contenteditable_attr_value = x`${data.element_data_name}['contenteditable']`;
}
const { init } = this.rename_this_method( const { init } = this.rename_this_method(
block, block,
value => x`@set_data(${this.var}, ${value})` value => {
if (contenteditable_attr_value) {
if (contenteditable_attr_value === true) {
return x`@set_data_contenteditable(${this.var}, ${value})`;
} else {
return x`@set_data_maybe_contenteditable(${this.var}, ${value}, ${contenteditable_attr_value})`;
}
} else {
return x`@set_data(${this.var}, ${value})`;
}
}
); );
block.add_element( block.add_element(

@ -85,7 +85,7 @@ export default class Wrapper {
); );
} }
render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier, _data: Record<string, any> = undefined) {
throw Error('Wrapper class is not renderable'); throw Error('Wrapper class is not renderable');
} }
} }

@ -1,24 +1,33 @@
import ConstTag from '../../../nodes/ConstTag'; import ConstTag from '../../../nodes/ConstTag';
import Block from '../../Block'; import Block from '../../Block';
import { b, x } from 'code-red'; import { b, Node, x } from 'code-red';
import Renderer from '../../Renderer'; import Renderer from '../../Renderer';
import Expression from '../../../nodes/shared/Expression';
export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) { export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) {
const const_tags_props = []; const const_tags_props = [];
const_tags.forEach((const_tag, i) => { const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`; const name = `#constants_${i}`;
const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`); const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`);
const_tag.contexts.forEach(context => { const to_ctx = (name: string) => block.renderer.context_lookup.has(name) ? x`${ctx}[${block.renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name } as Node;
const_tags_props.push(b`${ctx}[${block.renderer.context_lookup.get(context.key.name).index}] = ${context.default_modifier(context.modifier({ type: 'Identifier', name }), name => block.renderer.context_lookup.has(name) ? x`${ctx}[${block.renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name })};`);
}); const_tag.contexts.forEach(context => {
}); if (context.type === 'DestructuredVariable') {
return const_tags_props; const_tags_props.push(b`${ctx}[${block.renderer.context_lookup.get(context.key.name).index}] = ${context.default_modifier(context.modifier({ type: 'Identifier', name }), to_ctx)}`);
} else {
const expression = new Expression(block.renderer.component, const_tag, const_tag.scope, context.key);
const_tags_props.push(b`const ${context.property_name} = ${expression.manipulate(block, ctx)}`);
}
});
});
return const_tags_props;
} }
export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) { export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) {
const_tags.forEach(const_tag => { const_tags.forEach(const_tag => {
const_tag.contexts.forEach(context => { const_tag.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true); if (context.type !== 'DestructuredVariable') return;
}); renderer.add_to_context(context.key.name, true);
}); });
});
} }

@ -28,6 +28,7 @@ const handlers: Record<string, Handler> = {
Body: noop, Body: noop,
Comment, Comment,
DebugTag, DebugTag,
Document: noop,
EachBlock, EachBlock,
Element, Element,
Head, Head,

@ -1,7 +1,9 @@
import { is_void } from '../../../../shared/utils/names'; import { is_void } from '../../../../shared/utils/names';
import { get_attribute_expression, get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value'; import { get_attribute_expression, get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
import { boolean_attributes } from '../../../../shared/boolean_attributes'; import { boolean_attributes } from '../../../../shared/boolean_attributes';
import { is_name_contenteditable, is_contenteditable } from '../../utils/contenteditable';
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
import Binding from '../../nodes/Binding';
import Element from '../../nodes/Element'; import Element from '../../nodes/Element';
import { p, x } from 'code-red'; import { p, x } from 'code-red';
import Expression from '../../nodes/shared/Expression'; import Expression from '../../nodes/shared/Expression';
@ -18,11 +20,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
// awkward special case // awkward special case
let node_contents; let node_contents;
const contenteditable = ( const contenteditable = is_contenteditable(node);
node.name !== 'textarea' &&
node.name !== 'input' &&
node.attributes.some((attribute) => attribute.name === 'contenteditable')
);
if (node.is_dynamic_element) { if (node.is_dynamic_element) {
renderer.push(); renderer.push();
@ -128,7 +126,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
} }
} }
node.bindings.forEach(binding => { node.bindings.forEach((binding: Binding) => {
const { name, expression } = binding; const { name, expression } = binding;
if (binding.is_readonly) { if (binding.is_readonly) {
@ -144,7 +142,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
const condition = type === 'checkbox' ? x`~${bound}.indexOf(${value})` : x`${value} === ${bound}`; const condition = type === 'checkbox' ? x`~${bound}.indexOf(${value})` : x`${value} === ${bound}`;
renderer.add_expression(x`${condition} ? @add_attribute("checked", true, 1) : ""`); renderer.add_expression(x`${condition} ? @add_attribute("checked", true, 1) : ""`);
} }
} else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) { } else if (contenteditable && is_name_contenteditable(name)) {
node_contents = expression.node; node_contents = expression.node;
// TODO where was this used? // TODO where was this used?

@ -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}`);
} }

@ -1,5 +1,14 @@
import * as assert from 'assert'; import * as assert from 'assert';
import get_name_from_filename from './get_name_from_filename'; import get_name_from_filename from './get_name_from_filename';
import {
is_contenteditable,
has_contenteditable_attr,
is_name_contenteditable,
get_contenteditable_attr,
CONTENTEDITABLE_BINDINGS
} from './contenteditable';
import Element from '../nodes/Element';
import Attribute from '../nodes/Attribute';
describe('get_name_from_filename', () => { describe('get_name_from_filename', () => {
it('uses the basename', () => { it('uses the basename', () => {
@ -13,4 +22,68 @@ describe('get_name_from_filename', () => {
it('handles Windows filenames', () => { it('handles Windows filenames', () => {
assert.equal(get_name_from_filename('path\\to\\Widget.svelte'), 'Widget'); assert.equal(get_name_from_filename('path\\to\\Widget.svelte'), 'Widget');
}); });
it('handles special characters in filenames', () => {
assert.equal(get_name_from_filename('@.svelte'), '_');
assert.equal(get_name_from_filename('&.svelte'), '_');
assert.equal(get_name_from_filename('~.svelte'), '_');
});
});
describe('contenteditable', () => {
describe('is_contenteditable', () => {
it('returns false if node is input', () => {
const node = { name: 'input' } as Element;
assert.equal(is_contenteditable(node), false);
});
it('returns false if node is textarea', () => {
const node = { name: 'textarea' } as Element;
assert.equal(is_contenteditable(node), false);
});
it('returns false if node is not input or textarea AND it is not contenteditable', () => {
const attr = { name: 'href' } as Attribute;
const node = { name: 'a', attributes: [attr] } as Element;
assert.equal(is_contenteditable(node), false);
});
it('returns true if node is not input or textarea AND it is contenteditable', () => {
const attr = { name: 'contenteditable' } as Attribute;
const node = { name: 'a', attributes: [attr] } as Element;
assert.equal(is_contenteditable(node), true);
});
});
describe('has_contenteditable_attr', () => {
it('returns true if attribute is contenteditable', () => {
const attr = { name: 'contenteditable' } as Attribute;
const node = { attributes: [attr] } as Element;
assert.equal(has_contenteditable_attr(node), true);
});
it('returns false if attribute is not contenteditable', () => {
const attr = { name: 'href' } as Attribute;
const node = { attributes: [attr] } as Element;
assert.equal(has_contenteditable_attr(node), false);
});
});
describe('is_name_contenteditable', () => {
it('returns true if name is a contenteditable type', () => {
assert.equal(is_name_contenteditable(CONTENTEDITABLE_BINDINGS[0]), true);
});
it('returns false if name is not contenteditable type', () => {
assert.equal(is_name_contenteditable('value'), false);
});
});
describe('get_contenteditable_attr', () => {
it('returns the contenteditable Attribute if it exists', () => {
const attr = { name: 'contenteditable' } as Attribute;
const node = { name: 'div', attributes: [attr] } as Element;
assert.equal(get_contenteditable_attr(node), attr);
});
it('returns undefined if contenteditable attribute cannot be found', () => {
const node = { name: 'div', attributes: [] } as Element;
assert.equal(get_contenteditable_attr(node), undefined);
});
});
}); });

@ -1,5 +1,5 @@
import { import {
ARIARoleDefintionKey, ARIARoleDefinitionKey,
roles as roles_map, roles as roles_map,
elementRoles, elementRoles,
ARIARoleRelationConcept ARIARoleRelationConcept
@ -7,7 +7,9 @@ import {
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
import Attribute from '../nodes/Attribute'; import Attribute from '../nodes/Attribute';
const non_abstract_roles = [...roles_map.keys()].filter((name) => !roles_map.get(name).abstract); const aria_roles = roles_map.keys();
const abstract_roles = new Set(aria_roles.filter(role => roles_map.get(role).abstract));
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.has(name));
const non_interactive_roles = new Set( const non_interactive_roles = new Set(
non_abstract_roles non_abstract_roles
@ -32,17 +34,21 @@ const interactive_roles = new Set(
non_abstract_roles.filter((name) => !non_interactive_roles.has(name)) non_abstract_roles.filter((name) => !non_interactive_roles.has(name))
); );
export function is_non_interactive_roles(role: ARIARoleDefintionKey) { export function is_non_interactive_roles(role: ARIARoleDefinitionKey) {
return non_interactive_roles.has(role); return non_interactive_roles.has(role);
} }
export function is_interactive_roles(role: ARIARoleDefintionKey) { export function is_interactive_roles(role: ARIARoleDefinitionKey) {
return interactive_roles.has(role); return interactive_roles.has(role);
} }
export function is_abstract_role(role: ARIARoleDefinitionKey) {
return abstract_roles.has(role);
}
const presentation_roles = new Set(['presentation', 'none']); const presentation_roles = new Set(['presentation', 'none']);
export function is_presentation_role(role: ARIARoleDefintionKey) { export function is_presentation_role(role: ARIARoleDefinitionKey) {
return presentation_roles.has(role); return presentation_roles.has(role);
} }
@ -62,10 +68,28 @@ export function is_hidden_from_screen_reader(tag_name: string, attribute_map: Ma
return aria_hidden_value === true || aria_hidden_value === 'true'; return aria_hidden_value === true || aria_hidden_value === 'true';
} }
export function has_disabled_attribute(attribute_map: Map<string, Attribute>) {
const disabled_attr = attribute_map.get('disabled');
const disabled_attr_value = disabled_attr && disabled_attr.get_static_value();
if (disabled_attr_value) {
return true;
}
const aria_disabled_attr = attribute_map.get('aria-disabled');
if (aria_disabled_attr) {
const aria_disabled_attr_value = aria_disabled_attr.get_static_value();
if (aria_disabled_attr_value === true) {
return true;
}
}
return false;
}
const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = []; const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = [];
elementRoles.entries().forEach(([schema, roles]) => { elementRoles.entries().forEach(([schema, roles]) => {
if ([...roles].every((role) => non_interactive_roles.has(role))) { if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.has(role))) {
non_interactive_element_role_schemas.push(schema); non_interactive_element_role_schemas.push(schema);
} }
}); });
@ -82,6 +106,10 @@ const interactive_ax_objects = new Set(
[...AXObjects.keys()].filter((name) => AXObjects.get(name).type === 'widget') [...AXObjects.keys()].filter((name) => AXObjects.get(name).type === 'widget')
); );
const non_interactive_ax_objects = new Set(
[...AXObjects.keys()].filter((name) => ['windows', 'structure'].includes(AXObjects.get(name).type))
);
const interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = []; const interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = [];
elementAXObjects.entries().forEach(([schema, ax_object]) => { elementAXObjects.entries().forEach(([schema, ax_object]) => {
@ -90,6 +118,14 @@ elementAXObjects.entries().forEach(([schema, ax_object]) => {
} }
}); });
const non_interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = [];
elementAXObjects.entries().forEach(([schema, ax_object]) => {
if ([...ax_object].every((role) => non_interactive_ax_objects.has(role))) {
non_interactive_element_ax_object_schemas.push(schema);
}
});
function match_schema( function match_schema(
schema: ARIARoleRelationConcept, schema: ARIARoleRelationConcept,
tag_name: string, tag_name: string,
@ -110,24 +146,31 @@ function match_schema(
}); });
} }
export function is_interactive_element( export enum ElementInteractivity {
Interactive = 'interactive',
NonInteractive = 'non-interactive',
Static = 'static',
}
export function element_interactivity(
tag_name: string, tag_name: string,
attribute_map: Map<string, Attribute> attribute_map: Map<string, Attribute>
): boolean { ): ElementInteractivity {
if ( if (
interactive_element_role_schemas.some((schema) => interactive_element_role_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map) match_schema(schema, tag_name, attribute_map)
) )
) { ) {
return true; return ElementInteractivity.Interactive;
} }
if ( if (
tag_name !== 'header' &&
non_interactive_element_role_schemas.some((schema) => non_interactive_element_role_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map) match_schema(schema, tag_name, attribute_map)
) )
) { ) {
return false; return ElementInteractivity.NonInteractive;
} }
if ( if (
@ -135,13 +178,33 @@ export function is_interactive_element(
match_schema(schema, tag_name, attribute_map) match_schema(schema, tag_name, attribute_map)
) )
) { ) {
return true; return ElementInteractivity.Interactive;
} }
return false; if (
non_interactive_element_ax_object_schemas.some((schema) =>
match_schema(schema, tag_name, attribute_map)
)
) {
return ElementInteractivity.NonInteractive;
}
return ElementInteractivity.Static;
}
export function is_interactive_element(tag_name: string, attribute_map: Map<string, Attribute>): boolean {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive;
}
export function is_non_interactive_element(tag_name: string, attribute_map: Map<string, Attribute>): boolean {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive;
}
export function is_static_element(tag_name: string, attribute_map: Map<string, Attribute>): boolean {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static;
} }
export function is_semantic_role_element(role: ARIARoleDefintionKey, tag_name: string, attribute_map: Map<string, Attribute>) { export function is_semantic_role_element(role: ARIARoleDefinitionKey, tag_name: string, attribute_map: Map<string, Attribute>) {
for (const [schema, ax_object] of elementAXObjects.entries()) { for (const [schema, ax_object] of elementAXObjects.entries()) {
if (schema.name === tag_name && (!schema.attributes || schema.attributes.every( if (schema.name === tag_name && (!schema.attributes || schema.attributes.every(
(attr) => attribute_map.has(attr.name) && attribute_map.get(attr.name).get_static_value() === attr.value (attr) => attribute_map.has(attr.name) && attribute_map.get(attr.name).get_static_value() === attr.value

@ -0,0 +1,57 @@
// Utilities for managing contenteditable nodes
import Attribute from '../nodes/Attribute';
import Element from '../nodes/Element';
export const CONTENTEDITABLE_BINDINGS = [
'textContent',
'innerHTML',
'innerText'
];
/**
* Returns true if node is an 'input' or 'textarea'.
* @param {Element} node The element to be checked
*/
function is_input_or_textarea(node: Element): boolean {
return node.name === 'textarea' || node.name === 'input';
}
/**
* Check if a given attribute is 'contenteditable'.
* @param {Attribute} attribute A node.attribute
*/
function is_attr_contenteditable(attribute: Attribute): boolean {
return attribute.name === 'contenteditable';
}
/**
* Check if any of a node's attributes are 'contentenditable'.
* @param {Element} node The element to be checked
*/
export function has_contenteditable_attr(node: Element): boolean {
return node.attributes.some(is_attr_contenteditable);
}
/**
* Returns true if node is not textarea or input, but has 'contenteditable' attribute.
* @param {Element} node The element to be tested
*/
export function is_contenteditable(node: Element): boolean {
return !is_input_or_textarea(node) && has_contenteditable_attr(node);
}
/**
* Returns true if a given binding/node is contenteditable.
* @param {string} name A binding or node name to be checked
*/
export function is_name_contenteditable(name: string): boolean {
return CONTENTEDITABLE_BINDINGS.includes(name);
}
/**
* Returns the contenteditable attribute from the node (if it exists).
* @param {Element} node The element to get the attribute from
*/
export function get_contenteditable_attr(node: Element): Attribute | undefined {
return node.attributes.find(is_attr_contenteditable);
}

@ -1,9 +1,8 @@
import { regex_starts_with_underscore, regex_ends_with_underscore } from '../../utils/patterns';
const regex_percentage_characters = /%/g; const regex_percentage_characters = /%/g;
const regex_file_ending = /\.[^.]+$/; const regex_file_ending = /\.[^.]+$/;
const regex_repeated_invalid_variable_identifier_characters = /[^a-zA-Z_$0-9]+/g; const regex_repeated_invalid_variable_identifier_characters = /[^a-zA-Z_$0-9]+/g;
const regex_starts_with_digit = /^(\d)/; const regex_starts_with_digit = /^(\d)/;
const regex_may_starts_or_ends_with_underscore = /^_?(.+?)_?$/;
export default function get_name_from_filename(filename: string) { export default function get_name_from_filename(filename: string) {
if (!filename) return null; if (!filename) return null;
@ -18,12 +17,12 @@ export default function get_name_from_filename(filename: string) {
} }
} }
const base = parts.pop() const base = parts
.pop()
.replace(regex_percentage_characters, 'u') .replace(regex_percentage_characters, 'u')
.replace(regex_file_ending, '') .replace(regex_file_ending, '')
.replace(regex_repeated_invalid_variable_identifier_characters, '_') .replace(regex_repeated_invalid_variable_identifier_characters, '_')
.replace(regex_starts_with_underscore, '') .replace(regex_may_starts_or_ends_with_underscore, '$1')
.replace(regex_ends_with_underscore, '')
.replace(regex_starts_with_digit, '_$1'); .replace(regex_starts_with_digit, '_$1');
if (!base) { if (!base) {

@ -2,6 +2,6 @@ export { default as compile } from './compile/index';
export { default as parse } from './parse/index'; export { default as parse } from './parse/index';
export { default as preprocess } from './preprocess/index'; export { default as preprocess } from './preprocess/index';
export { walk } from 'estree-walker'; export { walk } from 'estree-walker';
export type { CompileOptions, ModuleFormat, EnableSourcemap, CssHashGetter } from './interfaces';
export const VERSION = '__VERSION__'; export const VERSION = '__VERSION__';
// additional exports added through generate-type-definitions.js

@ -63,7 +63,7 @@ interface BaseExpressionDirective extends BaseDirective {
} }
export interface Element extends BaseNode { export interface Element extends BaseNode {
type: 'InlineComponent' | 'SlotTemplate' | 'Title' | 'Slot' | 'Element' | 'Head' | 'Options' | 'Window' | 'Body'; type: 'InlineComponent' | 'SlotTemplate' | 'Title' | 'Slot' | 'Element' | 'Head' | 'Options' | 'Window' | 'Document' | 'Body';
attributes: Array<BaseDirective | Attribute | SpreadAttribute>; attributes: Array<BaseDirective | Attribute | SpreadAttribute>;
name: string; name: string;
} }
@ -223,7 +223,6 @@ export interface Var {
subscribable?: boolean; subscribable?: boolean;
is_reactive_dependency?: boolean; is_reactive_dependency?: boolean;
imported?: boolean; imported?: boolean;
is_reactive_static?: boolean;
} }
export interface CssResult { export interface CssResult {

@ -3,12 +3,12 @@ import * as code_red from 'code-red';
export const parse = (source: string): Node => code_red.parse(source, { export const parse = (source: string): Node => code_red.parse(source, {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 12, ecmaVersion: 13,
locations: true locations: true
}); });
export const parse_expression_at = (source: string, index: number): Node => code_red.parseExpressionAt(source, index, { export const parse_expression_at = (source: string, index: number): Node => code_red.parseExpressionAt(source, index, {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 12, ecmaVersion: 13,
locations: true locations: true
}); });

@ -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';
@ -78,7 +79,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
}); });
parser.read(regex_starts_with_closing_style_tag); parser.read(regex_starts_with_closing_style_tag);
const end = parser.index; const end = parser.index;
return { return {

@ -19,6 +19,7 @@ const meta_tags = new Map([
['svelte:head', 'Head'], ['svelte:head', 'Head'],
['svelte:options', 'Options'], ['svelte:options', 'Options'],
['svelte:window', 'Window'], ['svelte:window', 'Window'],
['svelte:document', 'Document'],
['svelte:body', 'Body'] ['svelte:body', 'Body']
]); ]);
@ -519,7 +520,7 @@ function read_sequence(parser: Parser, done: () => boolean, location: string): T
function flush(end: number) { function flush(end: number) {
if (current_chunk.raw) { if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw); current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end; current_chunk.end = end;
chunks.push(current_chunk); chunks.push(current_chunk);
} }

@ -19,7 +19,7 @@ export default function text(parser: Parser) {
end: parser.index, end: parser.index,
type: 'Text', type: 'Text',
raw: data, raw: data,
data: decode_character_references(data) data: decode_character_references(data, false)
}; };
parser.current().children.push(node); parser.current().children.push(node);

File diff suppressed because it is too large Load Diff

@ -35,12 +35,32 @@ const windows_1252 = [
376 376
]; ];
const entity_pattern = new RegExp( function reg_exp_entity(entity_name: string, is_attribute_value: boolean) {
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`, // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
'g' // doesn't decode the html entity which not ends with ; and next character is =, number or alphabet in attribute value.
); if (is_attribute_value && !entity_name.endsWith(';')) {
return `${entity_name}\\b(?!=)`;
}
return entity_name;
}
function get_entity_pattern(is_attribute_value: boolean) {
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map(entity_name => reg_exp_entity(entity_name, is_attribute_value));
const entity_pattern = new RegExp(
`&(${reg_exp_num}|${reg_exp_entities.join('|')})`,
'g'
);
return entity_pattern;
}
const entity_pattern_content = get_entity_pattern(false);
const entity_pattern_attr_value = get_entity_pattern(true);
export function decode_character_references(html: string) { export function decode_character_references(html: string, is_attribute_value: boolean) {
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(entity_pattern, (match, entity) => { return html.replace(entity_pattern, (match, entity) => {
let code; let code;

@ -1,4 +1,4 @@
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import type { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping';
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code'; import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code';
import { decode_map } from './decode_sourcemap'; import { decode_map } from './decode_sourcemap';
@ -57,7 +57,7 @@ class PreprocessResult implements Source {
to_processed(): Processed { to_processed(): Processed {
// Combine all the source maps for each preprocessor function into one // Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps(this.file_basename, this.sourcemap_list); const map = combine_sourcemaps(this.file_basename, this.sourcemap_list);
return { return {
// TODO return separated output, in future version where svelte.compile supports it: // TODO return separated output, in future version where svelte.compile supports it:

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save