Merge branch 'master' into master

pull/8443/head
adiGuba 4 years ago committed by GitHub
commit d983d63d72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,7 +3,42 @@ on: [push, pull_request]
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@v1
- uses: actions/setup-node@v2
with:
node-version: 16
cache: npm
- run: npm install
env:
SKIP_PREPARE: true
- run: npm run build
env:
PUBLISH: true
- uses: actions/cache@v3
with:
# cache key based on OS as the full path for each OS may be different
# and windows is not able to reuse the cache from ubuntu
key: output-${{ github.run_id }}-${{ matrix.os }}
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:
@ -11,21 +46,38 @@ jobs:
node-version: [8, 10, 12, 14, 16] node-version: [8, 10, 12, 14, 16]
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- uses: actions/cache@v3
with:
key: output-${{ github.run_id }}-${{ matrix.os }}
path: |
index.*
compiler.*
ssr.*
action/
animate/
easing/
internal/
motion/
store/
transition/
types/
- run: npm install - run: npm install
- run: npm test env:
SKIP_PREPARE: true
- run: npm run test:integration
env: env:
CI: true CI: true
Lint: Lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
cache: npm cache: npm
- run: 'npm i && npm run lint' - run: 'npm i && npm run lint'
@ -36,8 +88,11 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
cache: npm cache: npm
- run: 'npm i && npm run test:unit' - run: npm install
env:
SKIP_PREPARE: true
- run: npm run test:unit

@ -1,7 +1,7 @@
const is_unit_test = process.env.UNIT_TEST;
module.exports = { module.exports = {
file: [ file: is_unit_test ? [] : ['test/test.ts'],
'test/test.ts'
],
require: [ require: [
'sucrase/register' 'sucrase/register'
] ]

@ -0,0 +1,15 @@
module.exports = {
spec: [
'src/**/__test__.ts',
],
require: [
'sucrase/register'
],
recursive: true,
};
// add coverage options when running 'npx c8 mocha'
if (process.env.NODE_V8_COVERAGE) {
module.exports.fullTrace = true;
module.exports.require.push('source-map-support/register');
}

@ -1,5 +1,12 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Support `|important` modifier to style directive ([#7489](https://github.com/sveltejs/svelte/pull/7489))
* Warn when using `<a target="_blank">` without `rel="noreferrer"` ([#6188](https://github.com/sveltejs/svelte/issues/6188))
* Throw helpful compiler error for attempting to update `const` variable ([#4895](https://github.com/sveltejs/svelte/issues/4895))
* Refix hydration with `{@html}` and components in `<svelte:head>` ([#7941](https://github.com/sveltejs/svelte/pull/7941))
## 3.51.0 ## 3.51.0
* Add a11y warnings: * Add a11y warnings:

@ -86,15 +86,15 @@
}, },
"types": "types/runtime/index.d.ts", "types": "types/runtime/index.d.ts",
"scripts": { "scripts": {
"test": "mocha --exit", "test": "npm run test:unit && npm run test:integration",
"test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts --exit", "test:integration": "mocha --exit",
"quicktest": "mocha", "test:unit": "mocha --config .mocharc.unit.js --exit",
"quicktest": "mocha --exit",
"build": "rollup -c && npm run tsd", "build": "rollup -c && npm run tsd",
"prepare": "npm run build", "prepare": "node scripts/skip_in_ci.js npm run build",
"dev": "rollup -cw", "dev": "rollup -cw",
"pretest": "npm run build",
"posttest": "agadoo internal/index.mjs", "posttest": "agadoo internal/index.mjs",
"prepublishOnly": "node check_publish_env.js && npm run lint && npm test", "prepublishOnly": "node check_publish_env.js && npm run lint && npm run build && npm test",
"tsd": "node ./generate-type-definitions.js", "tsd": "node ./generate-type-definitions.js",
"lint": "eslint \"{src,test}/**/*.{ts,js}\"" "lint": "eslint \"{src,test}/**/*.{ts,js}\""
}, },

@ -0,0 +1,7 @@
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" });
}

@ -158,7 +158,7 @@ You can use HTML comments inside components.
--- ---
Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually these are accessibility warnings; make sure that you're disabling them for a good reason. Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually, these are accessibility warnings; make sure that you're disabling them for a good reason.
```sv ```sv
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
@ -593,7 +593,7 @@ The simplest bindings reflect the value of a property, such as `input.value`.
--- ---
If the name matches the value, you can use a shorthand. If the name matches the value, you can use shorthand.
```sv ```sv
<!-- These are equivalent --> <!-- These are equivalent -->
@ -750,7 +750,7 @@ Videos additionally have readonly `videoWidth` and `videoHeight` bindings.
--- ---
Block-level elements have 4 readonly bindings, measured using a technique similar to [this one](http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/): Block-level elements have 4 read-only bindings, measured using a technique similar to [this one](http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/):
* `clientWidth` * `clientWidth`
* `clientHeight` * `clientHeight`
@ -874,6 +874,9 @@ The `style:` directive provides a shorthand for setting multiple styles on an el
<!-- Multiple styles can be included --> <!-- Multiple styles can be included -->
<div style:color style:width="12rem" style:background-color={darkMode ? "black" : "white"}>...</div> <div style:color style:width="12rem" style:background-color={darkMode ? "black" : "white"}>...</div>
<!-- Styles can be marked as important -->
<div style:color|important="red">...</div>
``` ```
--- ---
@ -981,7 +984,7 @@ transition = (node: HTMLElement, params: any) => {
A transition is triggered by an element entering or leaving the DOM as a result of a state change. A transition is triggered by an element entering or leaving the DOM as a result of a state change.
When a block is transitioning out, all elements inside the block, including those that do not have their own transitions, are kept in the DOM until every transition in the block has completed. When a block is transitioning out, all elements inside the block, including those that do not have their own transitions, are kept in the DOM until every transition in the block has been completed.
The `transition:` directive indicates a *bidirectional* transition, which means it can be smoothly reversed while the transition is in progress. The `transition:` directive indicates a *bidirectional* transition, which means it can be smoothly reversed while the transition is in progress.
@ -1241,7 +1244,7 @@ As with actions and transitions, animations can have parameters.
--- ---
Animations can use custom functions that provide the `node`, an `animation` object and any `parameters` as arguments. The `animation` parameter is an object containing `from` and `to` properties each containing a [DOMRect](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect#Properties) describing the geometry of the element in its `start` and `end` positions. The `from` property is the DOMRect of the element in its starting position, the `to` property is the DOMRect of the element in its final position after the list has been reordered and the DOM updated. Animations can use custom functions that provide the `node`, an `animation` object and any `parameters` as arguments. The `animation` parameter is an object containing `from` and `to` properties each containing a [DOMRect](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect#Properties) describing the geometry of the element in its `start` and `end` positions. The `from` property is the DOMRect of the element in its starting position, and the `to` property is the DOMRect of the element in its final position after the list has been reordered and the DOM updated.
If the returned object has a `css` method, Svelte will create a CSS animation that plays on the element. If the returned object has a `css` method, Svelte will create a CSS animation that plays on the element.
@ -1390,7 +1393,7 @@ For SVG namespace, the example above desugars into using `<g>` instead:
--- ---
Svelte's CSS Variables support allows for easily themable components: Svelte's CSS Variables support allows for easily themeable components:
```sv ```sv
<!-- Slider.svelte --> <!-- Slider.svelte -->
@ -1654,7 +1657,7 @@ If `this` is falsy, no component is rendered.
The `<svelte:element>` element lets you render an element of a dynamically specified type. This is useful for example when displaying rich text content from a CMS. Any properties and event listeners present will be applied to the element. The `<svelte:element>` element lets you render an element of a dynamically specified type. This is useful for example when displaying rich text content from a CMS. Any properties and event listeners present will be applied to the element.
The only supported binding is `bind:this`, since the element type specific bindings that Svelte does at build time (e.g. `bind:value` for input elements) does not work with a dynamic tag type. The only supported binding is `bind:this`, since the element type specific bindings that Svelte does at build time (e.g. `bind:value` for input elements) do not work with a dynamic tag type.
If `this` has a nullish value, the element and its children will not be rendered. If `this` has a nullish value, the element and its children will not be rendered.

@ -268,7 +268,7 @@ Some HTML elements have default ARIA roles. Giving these elements an ARIA role t
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.
```sv ```sv
<!-- A11y: not interactive element cannot have positive tabIndex value --> <!-- A11y: noninteractive element cannot have positive tabIndex value -->
<div tabindex='0' /> <div tabindex='0' />
``` ```

@ -47,6 +47,10 @@ interface ComponentOptions {
preserveWhitespace?: boolean; preserveWhitespace?: boolean;
} }
const regex_leading_directory_separator = /^[/\\]/;
const regex_starts_with_term_export = /^Export/;
const regex_contains_term_function = /Function/;
export default class Component { export default class Component {
stats: Stats; stats: Stats;
warnings: Warning[]; warnings: Warning[];
@ -136,7 +140,7 @@ export default class Component {
(typeof process !== 'undefined' (typeof process !== 'undefined'
? compile_options.filename ? compile_options.filename
.replace(process.cwd(), '') .replace(process.cwd(), '')
.replace(/^[/\\]/, '') .replace(regex_leading_directory_separator, '')
: compile_options.filename); : compile_options.filename);
this.locate = getLocator(this.source, { offsetLine: 1 }); this.locate = getLocator(this.source, { offsetLine: 1 });
@ -638,7 +642,7 @@ export default class Component {
body.splice(i, 1); body.splice(i, 1);
} }
if (/^Export/.test(node.type)) { if (regex_starts_with_term_export.test(node.type)) {
const replacement = this.extract_exports(node, true); const replacement = this.extract_exports(node, true);
if (replacement) { if (replacement) {
body[i] = replacement; body[i] = replacement;
@ -788,6 +792,42 @@ export default class Component {
scope = map.get(node); scope = map.get(node);
} }
let deep = false;
let names: string[] | undefined;
if (node.type === 'AssignmentExpression') {
deep = node.left.type === 'MemberExpression';
names = deep
? [get_object(node.left).name]
: extract_names(node.left);
} else if (node.type === 'UpdateExpression') {
deep = node.argument.type === 'MemberExpression';
const { name } = get_object(node.argument);
names = [name];
}
if (names) {
names.forEach(name => {
let current_scope = scope;
let declaration;
while (current_scope) {
if (current_scope.declarations.has(name)) {
declaration = current_scope.declarations.get(name);
break;
}
current_scope = current_scope.parent;
}
if (declaration && declaration.kind === 'const' && !deep) {
component.error(node as any, {
code: 'assignment-to-const',
message: 'You are assigning to a const'
});
}
});
}
if (node.type === 'ImportDeclaration') { if (node.type === 'ImportDeclaration') {
component.extract_imports(node); component.extract_imports(node);
// TODO: to use actual remove // TODO: to use actual remove
@ -795,7 +835,7 @@ export default class Component {
return this.skip(); return this.skip();
} }
if (/^Export/.test(node.type)) { if (regex_starts_with_term_export.test(node.type)) {
const replacement = component.extract_exports(node); const replacement = component.extract_exports(node);
if (replacement) { if (replacement) {
this.replace(replacement); this.replace(replacement);
@ -918,7 +958,7 @@ export default class Component {
} }
if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) { if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) {
if (!((/Function/.test(parent.type) && prop === 'params') || (parent.type === 'VariableDeclarator' && prop === 'id'))) { if (!((regex_contains_term_function.test(parent.type) && prop === 'params') || (parent.type === 'VariableDeclarator' && prop === 'id'))) {
return this.error(node as any, compiler_errors.contextual_store); return this.error(node as any, compiler_errors.contextual_store);
} }
} }
@ -965,7 +1005,7 @@ export default class Component {
walk(this.ast.instance.content, { walk(this.ast.instance.content, {
enter(node: Node) { enter(node: Node) {
if (/Function/.test(node.type)) { if (regex_contains_term_function.test(node.type)) {
return this.skip(); return this.skip();
} }
@ -1089,7 +1129,7 @@ export default class Component {
this.replace(b` this.replace(b`
${node.declarations.length ? node : null} ${node.declarations.length ? node : null}
${ props.length > 0 && b`let { ${ props } } = $$props;`} ${ props.length > 0 && b`let { ${props} } = $$props;`}
${inserts} ${inserts}
` as any); ` as any);
return this.skip(); return this.skip();
@ -1460,6 +1500,8 @@ export default class Component {
} }
} }
const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/;
function process_component_options(component: Component, nodes) { function process_component_options(component: Component, nodes) {
const component_options: ComponentOptions = { const component_options: ComponentOptions = {
immutable: component.compile_options.immutable || false, immutable: component.compile_options.immutable || false,
@ -1473,7 +1515,7 @@ function process_component_options(component: Component, nodes) {
const node = nodes.find(node => node.name === 'svelte:options'); const node = nodes.find(node => node.name === 'svelte:options');
function get_value(attribute, {code, message}) { function get_value(attribute, { code, message }) {
const { value } = attribute; const { value } = attribute;
const chunk = value[0]; const chunk = value[0];
@ -1505,7 +1547,7 @@ function process_component_options(component: Component, nodes) {
return component.error(attribute, compiler_errors.invalid_tag_attribute); return component.error(attribute, compiler_errors.invalid_tag_attribute);
} }
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) { if (tag && !regex_valid_tag_name.test(tag)) {
return component.error(attribute, compiler_errors.invalid_tag_property); return component.error(attribute, compiler_errors.invalid_tag_property);
} }

@ -282,6 +282,10 @@ export default {
code: 'invalid-component-style-directive', code: 'invalid-component-style-directive',
message: 'Style directives cannot be used on components' message: 'Style directives cannot be used on components'
}, },
invalid_style_directive_modifier: (valid: string) => ({
code: 'invalid-style-directive-modifier',
message: `Valid modifiers for style directives are: ${valid}`
}),
invalid_component_svelte_directive: (name) => ({ invalid_component_svelte_directive: (name) => ({
code: 'invalid-component-svelte-directive', code: 'invalid-component-svelte-directive',
message: `svelte:${name} directives cannot be used on components` message: `svelte:${name} directives cannot be used on components`
@ -301,5 +305,5 @@ export default {
directive_conflict: (directive1, directive2) => ({ directive_conflict: (directive1, directive2) => ({
code: 'directive-conflict', code: 'directive-conflict',
message: `Cannot use ${directive1} and ${directive2} on the same element` message: `Cannot use ${directive1} and ${directive2} on the same element`
}) })
}; };

@ -185,7 +185,7 @@ export default {
}), }),
a11y_no_noninteractive_tabindex: { a11y_no_noninteractive_tabindex: {
code: 'a11y-no-noninteractive-tabindex', code: 'a11y-no-noninteractive-tabindex',
message: 'A11y: not interactive element cannot have positive tabIndex value' message: 'A11y: noninteractive element cannot have positive tabIndex value'
}, },
redundant_event_modifier_for_touch: { redundant_event_modifier_for_touch: {
code: 'redundant-event-modifier', code: 'redundant-event-modifier',

@ -9,6 +9,7 @@ import EachBlock from '../nodes/EachBlock';
import IfBlock from '../nodes/IfBlock'; import IfBlock from '../nodes/IfBlock';
import AwaitBlock from '../nodes/AwaitBlock'; import AwaitBlock from '../nodes/AwaitBlock';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../utils/patterns';
enum BlockAppliesToNode { enum BlockAppliesToNode {
NotPossible, NotPossible,
@ -25,6 +26,8 @@ const whitelist_attribute_selector = new Map([
['dialog', new Set(['open'])] ['dialog', new Set(['open'])]
]); ]);
const regex_is_single_css_selector = /[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/;
export default class Selector { export default class Selector {
node: CssNode; node: CssNode;
stylesheet: Stylesheet; stylesheet: Stylesheet;
@ -157,7 +160,7 @@ export default class Selector {
for (const block of this.blocks) { for (const block of this.blocks) {
for (const selector of block.selectors) { for (const selector of block.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
if (/[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/.test(selector.children[0].value)) { if (regex_is_single_css_selector.test(selector.children[0].value)) {
component.error(selector, compiler_errors.css_invalid_global_selector); component.error(selector, compiler_errors.css_invalid_global_selector);
} }
} }
@ -281,12 +284,14 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: Array<{
return true; return true;
} }
const regex_backslash_and_following_character = /\\(.)/g;
function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToNode { function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToNode {
let i = block.selectors.length; let i = block.selectors.length;
while (i--) { while (i--) {
const selector = block.selectors[i]; const selector = block.selectors[i];
const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1'); const name = typeof selector.name === 'string' && selector.name.replace(regex_backslash_and_following_character, '$1');
if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) { if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) {
return BlockAppliesToNode.NotPossible; return BlockAppliesToNode.NotPossible;
@ -371,7 +376,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string,
const start_with_space = []; const start_with_space = [];
const remaining = []; const remaining = [];
current_possible_values.forEach((current_possible_value: string) => { current_possible_values.forEach((current_possible_value: string) => {
if (/^\s/.test(current_possible_value)) { if (regex_starts_with_whitespace.test(current_possible_value)) {
start_with_space.push(current_possible_value); start_with_space.push(current_possible_value);
} else { } else {
remaining.push(current_possible_value); remaining.push(current_possible_value);
@ -392,7 +397,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string,
prev_values = combined; prev_values = combined;
start_with_space.forEach((value: string) => { start_with_space.forEach((value: string) => {
if (/\s$/.test(value)) { if (regex_ends_with_whitespace.test(value)) {
possible_values.add(value); possible_values.add(value);
} else { } else {
prev_values.push(value); prev_values.push(value);
@ -406,7 +411,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string,
} }
current_possible_values.forEach((current_possible_value: string) => { current_possible_values.forEach((current_possible_value: string) => {
if (/\s$/.test(current_possible_value)) { if (regex_ends_with_whitespace.test(current_possible_value)) {
possible_values.add(current_possible_value); possible_values.add(current_possible_value);
} else { } else {
prev_values.push(current_possible_value); prev_values.push(current_possible_value);

@ -9,9 +9,12 @@ import hash from '../utils/hash';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore'; import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore';
import { push_array } from '../../utils/push_array'; import { push_array } from '../../utils/push_array';
import { regex_only_whitespaces, regex_whitespace } from '../../utils/patterns';
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
function remove_css_prefix(name: string): string { function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, ''); return name.replace(regex_css_browser_prefix, '');
} }
const is_keyframes_node = (node: CssNode) => const is_keyframes_node = (node: CssNode) =>
@ -147,10 +150,10 @@ class Declaration {
// Don't minify whitespace in custom properties, since some browsers (Chromium < 99) // Don't minify whitespace in custom properties, since some browsers (Chromium < 99)
// treat --foo: ; and --foo:; differently // treat --foo: ; and --foo:; differently
if (first.type === 'Raw' && /^\s+$/.test(first.value)) return; if (first.type === 'Raw' && regex_only_whitespaces.test(first.value)) return;
let start = first.start; let start = first.start;
while (/\s/.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.overwrite(c, start, ':');

@ -35,6 +35,9 @@ const valid_options = [
'cssHash' 'cssHash'
]; ];
const regex_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
const regex_starts_with_lowercase_character = /^[a-z]/;
function validate_options(options: CompileOptions, warnings: Warning[]) { function validate_options(options: CompileOptions, warnings: Warning[]) {
const { name, filename, loopGuardTimeout, dev, namespace } = options; const { name, filename, loopGuardTimeout, dev, namespace } = options;
@ -48,11 +51,11 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
} }
}); });
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) { if (name && !regex_valid_identifier.test(name)) {
throw new Error(`options.name must be a valid identifier (got '${name}')`); throw new Error(`options.name must be a valid identifier (got '${name}')`);
} }
if (name && /^[a-z]/.test(name)) { if (name && regex_starts_with_lowercase_character.test(name)) {
const message = 'options.name should be capitalised'; const message = 'options.name should be capitalised';
warnings.push({ warnings.push({
code: 'options-lowercase-name', code: 'options-lowercase-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 {dimensions} from '../../utils/patterns'; import { regex_dimensions } 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';
@ -88,7 +88,7 @@ export default class Binding extends Node {
const type = parent.get_static_attribute_value('type'); const type = parent.get_static_attribute_value('type');
this.is_readonly = this.is_readonly =
dimensions.test(this.name) || regex_dimensions.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? */);

@ -11,7 +11,7 @@ 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 { dimensions, start_newline } from '../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } 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';
@ -204,6 +204,10 @@ function is_valid_aria_attribute_value(schema: ARIAPropertyDefinition, value: st
} }
} }
const regex_any_repeated_whitespaces = /[\s]+/g;
const regex_heading_tags = /^h[1-6]$/;
const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/;
export default class Element extends Node { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; name: string;
@ -255,7 +259,7 @@ export default class Element extends Node {
// places if there's another newline afterwards. // places if there's another newline afterwards.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element // see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
first.data = first.data.replace(start_newline, ''); first.data = first.data.replace(regex_starts_with_newline, '');
} }
} }
@ -416,7 +420,7 @@ export default class Element extends Node {
// Errors // Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) { if (regex_illegal_attribute_character.test(name)) {
return component.error(attribute, compiler_errors.illegal_attribute(name)); return component.error(attribute, compiler_errors.illegal_attribute(name));
} }
@ -482,7 +486,7 @@ export default class Element extends Node {
component.warn(attribute, compiler_warnings.a11y_unknown_aria_attribute(type, match)); component.warn(attribute, compiler_warnings.a11y_unknown_aria_attribute(type, match));
} }
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) { if (name === 'aria-hidden' && regex_heading_tags.test(this.name)) {
component.warn(attribute, compiler_warnings.a11y_hidden(this.name)); component.warn(attribute, compiler_warnings.a11y_hidden(this.name));
} }
@ -627,6 +631,26 @@ export default class Element extends Node {
const href_attribute = attribute_map.get('href') || attribute_map.get('xlink:href'); const href_attribute = attribute_map.get('href') || attribute_map.get('xlink:href');
const id_attribute = attribute_map.get('id'); const id_attribute = attribute_map.get('id');
const name_attribute = attribute_map.get('name'); const name_attribute = attribute_map.get('name');
const target_attribute = attribute_map.get('target');
if (target_attribute && target_attribute.get_static_value() === '_blank' && href_attribute) {
const href_static_value = href_attribute.get_static_value() ? href_attribute.get_static_value().toLowerCase() : null;
if (href_static_value === null || href_static_value.match(/^(https?:)?\/\//i)) {
const rel = attribute_map.get('rel');
const rel_values = rel ? rel.get_static_value().split(' ') : [];
const expected_values = ['noreferrer'];
expected_values.forEach(expected_value => {
if (!rel || rel && rel_values.indexOf(expected_value) < 0) {
component.warn(this, {
code: `security-anchor-rel-${expected_value}`,
message: `Security: Anchor with "target=_blank" should have rel attribute containing the value "${expected_value}"`
});
}
});
}
}
if (href_attribute) { if (href_attribute) {
const href_value = href_attribute.get_static_value(); const href_value = href_attribute.get_static_value();
@ -747,7 +771,7 @@ export default class Element extends Node {
if (this.name === 'figure') { if (this.name === 'figure') {
const children = this.children.filter(node => { const children = this.children.filter(node => {
if (node.type === 'Comment') return false; if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data); if (node.type === 'Text') return regex_non_whitespace_character.test(node.data);
return true; return true;
}); });
@ -879,7 +903,7 @@ export default class Element extends Node {
if (this.name !== 'video') { if (this.name !== 'video') {
return component.error(binding, compiler_errors.invalid_binding_element_with('<video>', name)); return component.error(binding, compiler_errors.invalid_binding_element_with('<video>', name));
} }
} else if (dimensions.test(name)) { } else if (regex_dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) { if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `<svg>. Use '${name.replace('offset', 'client')}' instead`)); return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `<svg>. Use '${name.replace('offset', 'client')}' instead`));
} else if (is_svg(this.name)) { } else if (is_svg(this.name)) {
@ -1016,7 +1040,7 @@ export default class Element extends Node {
if (attribute && !attribute.is_true) { if (attribute && !attribute.is_true) {
attribute.chunks.forEach((chunk, index) => { attribute.chunks.forEach((chunk, index) => {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
let data = chunk.data.replace(/[\s\n\t]+/g, ' '); let data = chunk.data.replace(regex_any_repeated_whitespaces, ' ');
if (index === 0) { if (index === 0) {
data = data.trimLeft(); data = data.trimLeft();
} else if (index === attribute.chunks.length - 1) { } else if (index === attribute.chunks.length - 1) {
@ -1030,12 +1054,14 @@ export default class Element extends Node {
} }
} }
const regex_starts_with_vovel = /^[aeiou]/;
function should_have_attribute( function should_have_attribute(
node, node,
attributes: string[], attributes: string[],
name = node.name name = node.name
) { ) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a'; const article = regex_starts_with_vovel.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ? const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0]; attributes[0];
@ -1043,10 +1069,12 @@ function should_have_attribute(
node.component.warn(node, compiler_warnings.a11y_missing_attribute(name, article, sequence)); node.component.warn(node, compiler_warnings.a11y_missing_attribute(name, article, sequence));
} }
const regex_minus_sign = /-/;
function within_custom_element(parent: INode) { function within_custom_element(parent: INode) {
while (parent) { while (parent) {
if (parent.type === 'InlineComponent') return false; if (parent.type === 'InlineComponent') return false;
if (parent.type === 'Element' && /-/.test(parent.name)) return true; if (parent.type === 'Element' && regex_minus_sign.test(parent.name)) return true;
parent = parent.parent; parent = parent.parent;
} }
return false; return false;

@ -6,6 +6,8 @@ import { Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
const regex_contains_term_function_expression = /FunctionExpression/;
export default class EventHandler extends Node { export default class EventHandler extends Node {
type: 'EventHandler'; type: 'EventHandler';
name: string; name: string;
@ -25,7 +27,7 @@ export default class EventHandler extends Node {
this.expression = new Expression(component, this, template_scope, info.expression); this.expression = new Expression(component, this, template_scope, info.expression);
this.uses_context = this.expression.uses_context; this.uses_context = this.expression.uses_context;
if (/FunctionExpression/.test(info.expression.type) && info.expression.params.length === 0) { if (regex_contains_term_function_expression.test(info.expression.type) && info.expression.params.length === 0) {
// TODO make this detection more accurate — if `event.preventDefault` isn't called, and // TODO make this detection more accurate — if `event.preventDefault` isn't called, and
// `event` is passed to another function, we can make it passive // `event` is passed to another function, we can make it passive
this.can_make_passive = true; this.can_make_passive = true;
@ -55,7 +57,7 @@ export default class EventHandler extends Node {
} }
const node = this.expression.node; const node = this.expression.node;
if (/FunctionExpression/.test(node.type)) { if (regex_contains_term_function_expression.test(node.type)) {
return false; return false;
} }

@ -5,6 +5,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { regex_non_whitespace_character } from '../../utils/patterns';
export default class Head extends Node { export default class Head extends Node {
type: 'Head'; type: 'Head';
@ -20,7 +21,7 @@ export default class Head extends Node {
} }
this.children = map_children(component, parent, scope, info.children.filter(child => { this.children = map_children(component, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data)); return (child.type !== 'Text' || regex_non_whitespace_character.test(child.data));
})); }));
if (this.children.length > 0) { if (this.children.length > 0) {

@ -10,6 +10,7 @@ import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces'; import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { regex_only_whitespaces } from '../../utils/patterns';
export default class InlineComponent extends Node { export default class InlineComponent extends Node {
type: 'InlineComponent'; type: 'InlineComponent';
@ -168,7 +169,7 @@ export default class InlineComponent extends Node {
} }
function not_whitespace_text(node) { function not_whitespace_text(node) {
return !(node.type === 'Text' && /^\s+$/.test(node.data)); return !(node.type === 'Text' && regex_only_whitespaces.test(node.data));
} }
function get_namespace(parent: Node, explicit_namespace: string) { function get_namespace(parent: Node, explicit_namespace: string) {

@ -1,20 +1,42 @@
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import list from '../../utils/list';
import compiler_errors from '../compiler_errors';
import Component from '../Component'; import Component from '../Component';
import { nodes_to_template_literal } from '../utils/nodes_to_template_literal'; import { nodes_to_template_literal } from '../utils/nodes_to_template_literal';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import Node from './shared/Node'; import Node from './shared/Node';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
const valid_modifiers = new Set(['important']);
export default class StyleDirective extends Node { export default class StyleDirective extends Node {
type: 'StyleDirective'; type: 'StyleDirective';
name: string; name: string;
modifiers: Set<string>;
expression: Expression; expression: Expression;
should_cache: boolean; should_cache: boolean;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(
component: Component,
parent: Node,
scope: TemplateScope,
info: TemplateNode
) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.name = info.name; this.name = info.name;
this.modifiers = new Set(info.modifiers);
for (const modifier of this.modifiers) {
if (!valid_modifiers.has(modifier)) {
component.error(
this,
compiler_errors.invalid_style_directive_modifier(
list([...valid_modifiers])
)
);
}
}
// Convert the value array to an expression so it's easier to handle // Convert the value array to an expression so it's easier to handle
// the StyleDirective going forward. // the StyleDirective going forward.
@ -34,6 +56,9 @@ export default class StyleDirective extends Node {
this.expression = new Expression(component, this, scope, raw_expression); this.expression = new Expression(component, this, scope, raw_expression);
this.should_cache = raw_expression.expressions.length > 0; this.should_cache = raw_expression.expressions.length > 0;
} }
}
get important() {
return this.modifiers.has('important');
} }
} }

@ -3,6 +3,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces'; import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import { regex_non_whitespace_character } from '../../utils/patterns';
// Whitespace inside one of these elements will not result in // Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This // a whitespace node being created in any circumstances. (This
@ -16,6 +17,8 @@ const elements_without_text = new Set([
'video' 'video'
]); ]);
const regex_ends_with_svg = /svg$/;
export default class Text extends Node { export default class Text extends Node {
type: 'Text'; type: 'Text';
data: string; data: string;
@ -28,7 +31,7 @@ export default class Text extends Node {
} }
should_skip() { should_skip() {
if (/\S/.test(this.data)) return false; if (regex_non_whitespace_character.test(this.data)) return false;
const parent_element = this.find_nearest(/(?:Element|InlineComponent|SlotTemplate|Head)/); const parent_element = this.find_nearest(/(?:Element|InlineComponent|SlotTemplate|Head)/);
if (!parent_element) return false; if (!parent_element) return false;
@ -37,7 +40,7 @@ export default class Text extends Node {
if (parent_element.type === 'InlineComponent') return parent_element.children.length === 1 && this === parent_element.children[0]; if (parent_element.type === 'InlineComponent') return parent_element.children.length === 1 && this === parent_element.children[0];
// svg namespace exclusions // svg namespace exclusions
if (/svg$/.test(parent_element.namespace)) { if (regex_ends_with_svg.test(parent_element.namespace)) {
if (this.prev && this.prev.type === 'Element' && this.prev.name === 'tspan') return false; if (this.prev && this.prev.type === 'Element' && this.prev.name === 'tspan') return false;
} }

@ -4,6 +4,8 @@ import Node from './Node';
import { INode } from '../interfaces'; import { INode } from '../interfaces';
import compiler_warnings from '../../compiler_warnings'; import compiler_warnings from '../../compiler_warnings';
const regex_non_whitespace_characters = /[^ \r\n\f\v\t]/;
export default class AbstractBlock extends Node { export default class AbstractBlock extends Node {
block: Block; block: Block;
children: INode[]; children: INode[];
@ -17,7 +19,7 @@ export default class AbstractBlock extends Node {
const child = this.children[0]; const child = this.children[0];
if (!child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data))) { if (!child || (child.type === 'Text' && !regex_non_whitespace_characters.test(child.data))) {
this.component.warn(this, compiler_warnings.empty_block); this.component.warn(this, compiler_warnings.empty_block);
} }
} }

@ -21,6 +21,8 @@ import compiler_errors from '../../compiler_errors';
type Owner = INode; type Owner = INode;
const regex_contains_term_function_expression = /FunctionExpression/;
export default class Expression { export default class Expression {
type: 'Expression' = 'Expression'; type: 'Expression' = 'Expression';
component: Component; component: Component;
@ -72,7 +74,7 @@ export default class Expression {
scope = map.get(node); scope = map.get(node);
} }
if (!function_expression && /FunctionExpression/.test(node.type)) { if (!function_expression && regex_contains_term_function_expression.test(node.type)) {
function_expression = node; function_expression = node;
} }
@ -126,6 +128,7 @@ export default class Expression {
deep = node.left.type === 'MemberExpression'; deep = node.left.type === 'MemberExpression';
names = extract_names(deep ? get_object(node.left) : node.left); names = extract_names(deep ? get_object(node.left) : node.left);
} else if (node.type === 'UpdateExpression') { } else if (node.type === 'UpdateExpression') {
deep = node.argument.type === 'MemberExpression';
names = extract_names(get_object(node.argument)); names = extract_names(get_object(node.argument));
} }
} }
@ -147,7 +150,26 @@ export default class Expression {
component.add_reference(node, name); component.add_reference(node, name);
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
if (variable) {
variable[deep ? 'mutated' : 'reassigned'] = true;
}
const declaration: any = scope.find_owner(name)?.declarations.get(name);
if (declaration) {
if (declaration.kind === 'const' && !deep) {
component.error(node, {
code: 'assignment-to-const',
message: 'You are assigning to a const'
});
}
} else if (variable && variable.writable === false && !deep) {
component.error(node, {
code: 'assignment-to-const',
message: 'You are assigning to a const'
});
}
} }
}); });
} }

@ -3,6 +3,7 @@ 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';
import { is_head } from './wrappers/shared/is_head'; import { is_head } from './wrappers/shared/is_head';
import { regex_double_quotes } from '../../utils/patterns';
export interface Bindings { export interface Bindings {
object: Identifier; object: Identifier;
@ -415,7 +416,7 @@ export default class Block {
block: ${block}, block: ${block},
id: ${this.name || 'create_fragment'}.name, id: ${this.name || 'create_fragment'}.name,
type: "${this.type}", type: "${this.type}",
source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}", source: "${this.comment ? this.comment.replace(regex_double_quotes, '\\"') : ''}",
ctx: #ctx ctx: #ctx
}); });
return ${block};` return ${block};`

@ -168,7 +168,7 @@ export default class Renderer {
return member; return member;
} }
invalidate(name: string, value?, main_execution_context: boolean = false) { invalidate(name: string, value?: unknown, main_execution_context: boolean = false) {
return renderer_invalidate(this, name, value, main_execution_context); return renderer_invalidate(this, name, value, main_execution_context);
} }

@ -12,6 +12,7 @@ import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/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';
import { regex_backslashes } from '../../utils/patterns';
export default function dom( export default function dom(
component: Component, component: Component,
@ -82,7 +83,7 @@ export default function dom(
} }
const uses_slots = component.var_lookup.has('$$slots'); const uses_slots = component.var_lookup.has('$$slots');
let compute_slots; let compute_slots: Node[] | undefined;
if (uses_slots) { if (uses_slots) {
compute_slots = b` compute_slots = b`
const $$slots = @compute_slots(#slots); const $$slots = @compute_slots(#slots);
@ -395,7 +396,7 @@ export default function dom(
if (has_definition) { if (has_definition) {
const reactive_declarations: (Node | Node[]) = []; const reactive_declarations: (Node | Node[]) = [];
const fixed_reactive_declarations = []; // 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);
@ -440,7 +441,7 @@ export default function dom(
return b`let ${$name};`; return b`let ${$name};`;
}); });
let unknown_props_check; let unknown_props_check: Node[] | undefined;
if (component.compile_options.dev && !(uses_props || uses_rest)) { if (component.compile_options.dev && !(uses_props || uses_rest)) {
unknown_props_check = b` unknown_props_check = b`
const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}]; const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}];
@ -530,7 +531,7 @@ export default function dom(
constructor(options) { constructor(options) {
super(); super();
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${css_sourcemap_enabled && options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`} ${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(regex_backslashes, '\\\\')}${css_sourcemap_enabled && options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
@init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty}); @init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty});

@ -80,7 +80,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
return invalidate; return invalidate;
} }
export function renderer_invalidate(renderer: Renderer, name: string, value?, main_execution_context: boolean = false) { export function renderer_invalidate(renderer: Renderer, name: string, value?: unknown, main_execution_context: boolean = false) {
const variable = renderer.component.var_lookup.get(name); const variable = renderer.component.var_lookup.get(name);
if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) { if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {

@ -582,7 +582,7 @@ export default class EachBlockWrapper extends Wrapper {
const start = this.block.has_update_method ? 0 : '#old_length'; const start = this.block.has_update_method ? 0 : '#old_length';
let remove_old_blocks; let remove_old_blocks: Node[];
if (this.block.has_outros) { if (this.block.has_outros) {
const out = block.get_unique_name('out'); const out = block.get_unique_name('out');

@ -10,6 +10,7 @@ import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces'; import { namespaces } from '../../../../utils/namespaces';
import { boolean_attributes } from '../../../../../shared/boolean_attributes'; import { boolean_attributes } from '../../../../../shared/boolean_attributes';
import { regex_double_quotes } from '../../../../utils/patterns';
const non_textlike_input_types = new Set([ const non_textlike_input_types = new Set([
'button', 'button',
@ -42,9 +43,12 @@ export class BaseAttributeWrapper {
} }
} }
render(_block: Block) {} render(_block: Block) { }
} }
const regex_minus_sign = /-/;
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
export default class AttributeWrapper extends BaseAttributeWrapper { export default class AttributeWrapper extends BaseAttributeWrapper {
node: Attribute; node: Attribute;
parent: ElementWrapper; parent: ElementWrapper;
@ -115,7 +119,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
// xlink is a special case... we could maybe extend this to generic // xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in // namespaced attributes but I'm not sure that's applicable in
// HTML5? // HTML5?
const method = /-/.test(element.node.name) const method = regex_minus_sign.test(element.node.name)
? '@set_custom_element_data' ? '@set_custom_element_data'
: name.slice(0, 6) === 'xlink:' : name.slice(0, 6) === 'xlink:'
? '@xlink_attr' ? '@xlink_attr'
@ -126,7 +130,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
const dependencies = this.get_dependencies(); const dependencies = this.get_dependencies();
const value = this.get_value(block); const value = this.get_value(block);
let updater; let updater: Node[];
const init = this.get_init(block, value); const init = this.get_init(block, value);
if (is_legacy_input_type) { if (is_legacy_input_type) {
@ -196,7 +200,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
get_init(block: Block, value) { get_init(block: Block, value) {
this.last = this.should_cache && block.get_unique_name( this.last = this.should_cache && block.get_unique_name(
`${this.parent.var.name}_${this.name.replace(/[^a-zA-Z_$]/g, '_')}_value` `${this.parent.var.name}_${this.name.replace(regex_invalid_variable_identifier_characters, '_')}_value`
); );
if (this.should_cache) block.add_variable(this.last); if (this.should_cache) block.add_variable(this.last);
@ -254,7 +258,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return metadata; return metadata;
} }
get_value(block) { get_value(block: Block) {
if (this.node.is_true) { if (this.node.is_true) {
if (this.metadata && boolean_attributes.has(this.metadata.property_name.toLowerCase())) { if (this.metadata && boolean_attributes.has(this.metadata.property_name.toLowerCase())) {
return x`true`; return x`true`;
@ -283,7 +287,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return value; return value;
} }
get_class_name_text(block) { get_class_name_text(block: Block) {
const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic); const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic);
const rendered = this.render_chunks(block); const rendered = this.render_chunks(block);
@ -313,7 +317,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return `="${value.map(chunk => { return `="${value.map(chunk => {
return chunk.type === 'Text' return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"') ? chunk.data.replace(regex_double_quotes, '\\"')
: `\${${chunk.manipulate()}}`; : `\${${chunk.manipulate()}}`;
}).join('')}"`; }).join('')}"`;
} }
@ -381,13 +385,14 @@ function should_cache(attribute: AttributeWrapper) {
return attribute.is_src || attribute.node.should_cache(); return attribute.is_src || attribute.node.should_cache();
} }
const regex_contains_checked_or_group = /checked|group/;
function is_indirectly_bound_value(attribute: AttributeWrapper) { function is_indirectly_bound_value(attribute: AttributeWrapper) {
const element = attribute.parent; const element = attribute.parent;
return attribute.name === 'value' && return attribute.name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound (element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' && (element.node.name === 'input' &&
element.node.bindings.some( element.node.bindings.some(
(binding) => (binding) => regex_contains_checked_or_group.test(binding.name)
/checked|group/.test(binding.name)
))); )));
} }

@ -299,8 +299,8 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
for (const dep of contextual_dependencies) { for (const dep of contextual_dependencies) {
const context = block.bindings.get(dep); const context = block.bindings.get(dep);
let key; let key: string;
let name; let name: string;
if (context) { if (context) {
key = context.object.name; key = context.object.name;
name = context.property.name; name = context.property.name;
@ -364,7 +364,7 @@ function get_event_handler(
const contextual_dependencies = new Set<string>(binding.node.expression.contextual_dependencies); const contextual_dependencies = new Set<string>(binding.node.expression.contextual_dependencies);
const context = block.bindings.get(name); const context = block.bindings.get(name);
let set_store; let set_store: Node[] | undefined;
if (context) { if (context) {
const { object, property, store, snippet } = context; const { object, property, store, snippet } = context;

@ -26,7 +26,7 @@ export default class EventHandlerWrapper {
} }
} }
get_snippet(block) { get_snippet(block: Block) {
const snippet = this.node.expression ? this.node.expression.manipulate(block) : block.renderer.reference(this.node.handler_name); const snippet = this.node.expression ? this.node.expression.manipulate(block) : block.renderer.reference(this.node.handler_name);
if (this.node.reassigned) { if (this.node.reassigned) {

@ -69,6 +69,8 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
} }
} }
const regex_style_prop_key = /^\s*([\w-]+):\s*/;
function optimize_style(value: Array<Text | Expression>) { function optimize_style(value: Array<Text | Expression>) {
const props: StyleProp[] = []; const props: StyleProp[] = [];
let chunks = value.slice(); let chunks = value.slice();
@ -78,7 +80,7 @@ function optimize_style(value: Array<Text | Expression>) {
if (chunk.type !== 'Text') return null; if (chunk.type !== 'Text') return null;
const key_match = /^\s*([\w-]+):\s*/.exec(chunk.data); const key_match = regex_style_prop_key.exec(chunk.data);
if (!key_match) return null; if (!key_match) return null;
const key = key_match[1]; const key = key_match[1];
@ -106,6 +108,9 @@ function optimize_style(value: Array<Text | Expression>) {
return props; return props;
} }
const regex_important_flag = /\s*!important\s*$/;
const regex_semicolon_or_whitespace = /[;\s]/;
function get_style_value(chunks: Array<Text | Expression>) { function get_style_value(chunks: Array<Text | Expression>) {
const value: Array<Text | Expression> = []; const value: Array<Text | Expression> = [];
@ -151,7 +156,7 @@ function get_style_value(chunks: Array<Text | Expression>) {
} as Text); } as Text);
} }
while (/[;\s]/.test(chunk.data[c])) c += 1; while (regex_semicolon_or_whitespace.test(chunk.data[c])) c += 1;
const remaining_data = chunk.data.slice(c); const remaining_data = chunk.data.slice(c);
if (remaining_data) { if (remaining_data) {
@ -172,9 +177,9 @@ function get_style_value(chunks: Array<Text | Expression>) {
let important = false; let important = false;
const last_chunk = value[value.length - 1]; const last_chunk = value[value.length - 1];
if (last_chunk && last_chunk.type === 'Text' && /\s*!important\s*$/.test(last_chunk.data)) { if (last_chunk && last_chunk.type === 'Text' && regex_important_flag.test(last_chunk.data)) {
important = true; important = true;
last_chunk.data = last_chunk.data.replace(/\s*!important\s*$/, ''); last_chunk.data = last_chunk.data.replace(regex_important_flag, '');
if (!last_chunk.data) value.pop(); if (!last_chunk.data) value.pop();
} }

@ -6,7 +6,7 @@ svg_attributes.forEach(name => {
svg_attribute_lookup.set(name.toLowerCase(), name); svg_attribute_lookup.set(name.toLowerCase(), name);
}); });
export default function fix_attribute_casing(name) { export default function fix_attribute_casing(name: string) {
name = name.toLowerCase(); name = name.toLowerCase();
return svg_attribute_lookup.get(name) || name; return svg_attribute_lookup.get(name) || name;
} }

@ -12,14 +12,14 @@ 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 { dimensions, start_newline } from '../../../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_backslashes } 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';
import { add_action } from '../shared/add_actions'; import { add_action } from '../shared/add_actions';
import bind_this from '../shared/bind_this'; import bind_this from '../shared/bind_this';
import { is_head } from '../shared/is_head'; import { is_head } from '../shared/is_head';
import { Identifier, ExpressionStatement, CallExpression } from 'estree'; import { Identifier, ExpressionStatement, CallExpression, Node } from 'estree';
import EventHandler from './EventHandler'; import EventHandler from './EventHandler';
import { extract_names } from 'periscopic'; import { extract_names } from 'periscopic';
import Action from '../../../nodes/Action'; import Action from '../../../nodes/Action';
@ -34,12 +34,16 @@ interface BindingGroup {
bindings: Binding[]; bindings: Binding[];
} }
const regex_contains_radio_or_checkbox_or_file = /radio|checkbox|file/;
const regex_contains_radio_or_checkbox_or_range_or_file = /radio|checkbox|range|file/;
const events = [ const events = [
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'textarea' || node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox|range|file/.test(node.get_static_attribute_value('type') as string) node.name === 'input' &&
!regex_contains_radio_or_checkbox_or_range_or_file.test(node.get_static_attribute_value('type') as string)
}, },
{ {
event_names: ['input'], event_names: ['input'],
@ -51,7 +55,8 @@ const events = [
event_names: ['change'], event_names: ['change'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'select' || node.name === 'select' ||
node.name === 'input' && /radio|checkbox|file/.test(node.get_static_attribute_value('type') as string) node.name === 'input' &&
regex_contains_radio_or_checkbox_or_file.test(node.get_static_attribute_value('type') as string)
}, },
{ {
event_names: ['change', 'input'], event_names: ['change', 'input'],
@ -62,7 +67,7 @@ const events = [
{ {
event_names: ['elementresize'], event_names: ['elementresize'],
filter: (_node: Element, name: string) => filter: (_node: Element, name: string) =>
dimensions.test(name) regex_dimensions.test(name)
}, },
// media events // media events
@ -136,6 +141,8 @@ const events = [
]; ];
const CHILD_DYNAMIC_ELEMENT_BLOCK = 'child_dynamic_element'; const CHILD_DYNAMIC_ELEMENT_BLOCK = 'child_dynamic_element';
const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g;
const regex_minus_signs = /-/g;
export default class ElementWrapper extends Wrapper { export default class ElementWrapper extends Wrapper {
node: Element; node: Element;
@ -182,7 +189,7 @@ export default class ElementWrapper extends Wrapper {
this.var = { this.var = {
type: 'Identifier', type: 'Identifier',
name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_') name: node.name.replace(regex_invalid_variable_identifier_characters, '_')
}; };
this.void = is_void(node.name); this.void = is_void(node.name);
@ -539,7 +546,7 @@ export default class ElementWrapper extends Wrapper {
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`); .map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
let reference; let reference: string | ReturnType<typeof x>;
if (this.node.tag_expr.node.type === 'Literal') { if (this.node.tag_expr.node.type === 'Literal') {
if (this.node.namespace) { if (this.node.namespace) {
reference = `"${this.node.tag_expr.node.value}"`; reference = `"${this.node.tag_expr.node.value}"`;
@ -559,7 +566,7 @@ export default class ElementWrapper extends Wrapper {
} }
} }
add_directives_in_order (block: Block) { add_directives_in_order(block: Block) {
type OrderedAttribute = EventHandler | BindingGroup | Binding | Action; type OrderedAttribute = EventHandler | BindingGroup | Binding | Action;
const binding_groups = events const binding_groups = events
@ -573,7 +580,7 @@ export default class ElementWrapper extends Wrapper {
const this_binding = this.bindings.find(b => b.node.name === 'this'); const this_binding = this.bindings.find(b => b.node.name === 'this');
function getOrder (item: OrderedAttribute) { function getOrder(item: OrderedAttribute) {
if (item instanceof EventHandler) { if (item instanceof EventHandler) {
return item.node.start; return item.node.start;
} else if (item instanceof Binding) { } else if (item instanceof Binding) {
@ -639,7 +646,7 @@ export default class ElementWrapper extends Wrapper {
// media bindings — awkward special case. The native timeupdate events // media bindings — awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our // fire too infrequently, so we need to take matters into our
// own hands // own hands
let animation_frame; let animation_frame: Identifier | undefined;
if (binding_group.events[0] === 'timeupdate') { if (binding_group.events[0] === 'timeupdate') {
animation_frame = block.get_unique_name(`${this.var.name}_animationframe`); animation_frame = block.get_unique_name(`${this.var.name}_animationframe`);
block.add_variable(animation_frame); block.add_variable(animation_frame);
@ -879,9 +886,7 @@ export default class ElementWrapper extends Wrapper {
} }
} }
add_transitions( add_transitions(block: Block) {
block: Block
) {
const { intro, outro } = this.node; const { intro, outro } = this.node;
if (!intro && !outro) return; if (!intro && !outro) return;
@ -938,7 +943,7 @@ export default class ElementWrapper extends Wrapper {
const fn = this.renderer.reference(intro.name); const fn = this.renderer.reference(intro.name);
let intro_block; let intro_block: Node[];
if (outro) { if (outro) {
intro_block = b` intro_block = b`
@ -1037,7 +1042,7 @@ export default class ElementWrapper extends Wrapper {
${outro && b`@add_transform(${this.var}, ${rect});`} ${outro && b`@add_transform(${this.var}, ${rect});`}
`); `);
let params; let params: Node | ReturnType<typeof x>;
if (this.node.animation.expression) { if (this.node.animation.expression) {
params = this.node.animation.expression.manipulate(block); params = this.node.animation.expression.manipulate(block);
@ -1065,8 +1070,8 @@ export default class ElementWrapper extends Wrapper {
const has_spread = this.node.attributes.some(attr => attr.is_spread); const has_spread = this.node.attributes.some(attr => attr.is_spread);
this.node.classes.forEach(class_directive => { this.node.classes.forEach(class_directive => {
const { expression, name } = class_directive; const { expression, name } = class_directive;
let snippet; let snippet: Node | string;
let dependencies; let dependencies: Set<string>;
if (expression) { if (expression) {
snippet = expression.manipulate(block); snippet = expression.manipulate(block);
dependencies = expression.dependencies; dependencies = expression.dependencies;
@ -1107,16 +1112,16 @@ 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);
this.node.styles.forEach((style_directive) => { this.node.styles.forEach((style_directive) => {
const { name, expression, should_cache } = style_directive; const { name, expression, should_cache, important } = style_directive;
const snippet = expression.manipulate(block); const snippet = expression.manipulate(block);
let cached_snippet; let cached_snippet: Identifier | undefined;
if (should_cache) { if (should_cache) {
cached_snippet = block.get_unique_name(`style_${name.replace(/-/g, '_')}`); cached_snippet = block.get_unique_name(`style_${name.replace(regex_minus_signs, '_')}`);
block.add_variable(cached_snippet, snippet); block.add_variable(cached_snippet, snippet);
} }
const updater = b`@set_style(${this.var}, "${name}", ${should_cache ? cached_snippet : snippet}, false)`; const updater = b`@set_style(${this.var}, "${name}", ${should_cache ? cached_snippet : snippet}, ${important ? 1 : null})`;
block.chunks.hydrate.push(updater); block.chunks.hydrate.push(updater);
@ -1178,9 +1183,7 @@ export default class ElementWrapper extends Wrapper {
} }
} }
add_manual_style_scoping(block: Block) {
add_manual_style_scoping(block) {
if (this.node.needs_manual_style_scoping) { if (this.node.needs_manual_style_scoping) {
const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`; const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`;
block.chunks.hydrate.push(updater); block.chunks.hydrate.push(updater);
@ -1189,10 +1192,13 @@ export default class ElementWrapper extends Wrapper {
} }
} }
const regex_backticks = /`/g;
const regex_dollar_signs = /\$/g;
function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) { function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) {
wrappers.forEach(wrapper => { wrappers.forEach(wrapper => {
if (wrapper instanceof TextWrapper) { if (wrapper instanceof TextWrapper) {
// Don't add the <pre>/<textare> newline logic here because pre/textarea.innerHTML // Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML
// would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't // would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' '; if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' ';
@ -1206,9 +1212,9 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
); );
state.quasi.value.raw += (raw ? wrapper.data : escape_html(wrapper.data)) state.quasi.value.raw += (raw ? wrapper.data : escape_html(wrapper.data))
.replace(/\\/g, '\\\\') .replace(regex_backslashes, '\\\\')
.replace(/`/g, '\\`') .replace(regex_backticks, '\\`')
.replace(/\$/g, '\\$'); .replace(regex_dollar_signs, '\\$');
} else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) { } else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) {
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi);
literal.expressions.push(wrapper.node.expression.manipulate(block)); literal.expressions.push(wrapper.node.expression.manipulate(block));
@ -1243,7 +1249,7 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
// Two or more leading newlines are required to restore the leading newline immediately after `<pre>`. // Two or more leading newlines are required to restore the leading newline immediately after `<pre>`.
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element // see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = wrapper.fragment.nodes[0]; const first = wrapper.fragment.nodes[0];
if (first && first.node.type === 'Text' && start_newline.test(first.node.data)) { if (first && first.node.type === 'Text' && regex_starts_with_newline.test(first.node.data)) {
state.quasi.value.raw += '\n'; state.quasi.value.raw += '\n';
} }
} }
@ -1255,7 +1261,7 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`. // Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
const first = value_attribute.node.chunks[0]; const first = value_attribute.node.chunks[0];
if (first && first.type === 'Text' && start_newline.test(first.data)) { if (first && first.type === 'Text' && regex_starts_with_newline.test(first.data)) {
state.quasi.value.raw += '\n'; state.quasi.value.raw += '\n';
} }
to_html_for_attr_value(value_attribute, block, literal, state); to_html_for_attr_value(value_attribute, block, literal, state);

@ -21,6 +21,7 @@ import Block from '../Block';
import { trim_start, trim_end } from '../../../utils/trim'; import { trim_start, trim_end } from '../../../utils/trim';
import { link } from '../../../utils/link'; import { link } from '../../../utils/link';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = { const wrappers = {
AwaitBlock, AwaitBlock,
@ -64,7 +65,7 @@ export default class FragmentWrapper {
this.nodes = []; this.nodes = [];
let last_child: Wrapper; let last_child: Wrapper;
let window_wrapper; let window_wrapper: Window | undefined;
let i = nodes.length; let i = nodes.length;
while (i--) { while (i--) {
@ -92,7 +93,7 @@ export default class FragmentWrapper {
// *unless* there is no whitespace between this node and its next sibling // *unless* there is no whitespace between this node and its next sibling
if (this.nodes.length === 0) { if (this.nodes.length === 0) {
const should_trim = ( const should_trim = (
next_sibling ? (next_sibling.node.type === 'Text' && /^\s/.test(next_sibling.node.data) && trimmable_at(child, next_sibling)) : !child.has_ancestor('EachBlock') next_sibling ? (next_sibling.node.type === 'Text' && regex_starts_with_whitespace.test(next_sibling.node.data) && trimmable_at(child, next_sibling)) : !child.has_ancestor('EachBlock')
); );
if (should_trim && !child.keep_space()) { if (should_trim && !child.keep_space()) {

@ -33,7 +33,7 @@ export default class HeadWrapper extends Wrapper {
} }
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
let nodes; let nodes: Identifier;
if (this.renderer.options.hydratable && this.fragment.nodes.length) { if (this.renderer.options.hydratable && this.fragment.nodes.length) {
nodes = block.get_unique_name('head_nodes'); nodes = block.get_unique_name('head_nodes');
block.chunks.claim.push(b`const ${nodes} = @head_selector('${this.node.id}', @_document.head);`); block.chunks.claim.push(b`const ${nodes} = @head_selector('${this.node.id}', @_document.head);`);

@ -13,6 +13,8 @@ import { Identifier, Node } from 'estree';
import { push_array } from '../../../utils/push_array'; import { push_array } from '../../../utils/push_array';
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';
type DetachingOrNull = 'detaching' | null;
function is_else_if(node: ElseBlock) { function is_else_if(node: ElseBlock) {
return ( return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock' node && node.children.length === 1 && node.children[0].type === 'IfBlock'
@ -213,7 +215,7 @@ export default class IfBlockWrapper extends Wrapper {
const vars = { name, anchor, if_exists_condition, has_else, has_transitions }; const vars = { name, anchor, if_exists_condition, has_else, has_transitions };
const detaching = parent_node && !is_head(parent_node) ? null : 'detaching'; const detaching: DetachingOrNull = parent_node && !is_head(parent_node) ? null : 'detaching';
if (this.node.else) { if (this.node.else) {
this.branches.forEach(branch => { this.branches.forEach(branch => {
@ -275,9 +277,9 @@ export default class IfBlockWrapper extends Wrapper {
block: Block, block: Block,
parent_node: Identifier, parent_node: Identifier,
_parent_nodes: Identifier, _parent_nodes: Identifier,
dynamic, dynamic: boolean,
{ name, anchor, has_else, if_exists_condition, has_transitions }, { name, anchor, has_else, if_exists_condition, has_transitions },
detaching detaching: DetachingOrNull
) { ) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type'); const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type = block.get_unique_name('current_block_type'); const current_block_type = block.get_unique_name('current_block_type');
@ -414,9 +416,9 @@ export default class IfBlockWrapper extends Wrapper {
block: Block, block: Block,
parent_node: Identifier, parent_node: Identifier,
_parent_nodes: Identifier, _parent_nodes: Identifier,
dynamic, dynamic: boolean,
{ name, anchor, has_else, has_transitions, if_exists_condition }, { name, anchor, has_else, has_transitions, if_exists_condition },
detaching detaching: DetachingOrNull
) { ) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type'); const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type_index = block.get_unique_name('current_block_type_index'); const current_block_type_index = block.get_unique_name('current_block_type_index');
@ -428,8 +430,8 @@ export default class IfBlockWrapper extends Wrapper {
const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type_index})` : x`#ctx`; const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type_index})` : x`#ctx`;
const if_current_block_type_index = has_else const if_current_block_type_index = has_else
? nodes => nodes ? (nodes: Node[]) => nodes
: nodes => b`if (~${current_block_type_index}) { ${nodes} }`; : (nodes: Node[]) => b`if (~${current_block_type_index}) { ${nodes} }`;
block.add_variable(current_block_type_index); block.add_variable(current_block_type_index);
block.add_variable(name); block.add_variable(name);
@ -593,9 +595,9 @@ export default class IfBlockWrapper extends Wrapper {
block: Block, block: Block,
parent_node: Identifier, parent_node: Identifier,
_parent_nodes: Identifier, _parent_nodes: Identifier,
dynamic, dynamic: boolean,
{ name, anchor, if_exists_condition, has_transitions }, { name, anchor, if_exists_condition, has_transitions },
detaching detaching: DetachingOrNull
) { ) {
const branch = this.branches[0]; const branch = this.branches[0];
const if_ctx = branch.get_ctx_name ? x`${branch.get_ctx_name}(#ctx)` : x`#ctx`; const if_ctx = branch.get_ctx_name ? x`${branch.get_ctx_name}(#ctx)` : x`#ctx`;

@ -24,6 +24,8 @@ import { namespaces } from '../../../../utils/namespaces';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
export default class InlineComponentWrapper extends Wrapper { export default class InlineComponentWrapper extends Wrapper {
var: Identifier; var: Identifier;
slots: Map<string, SlotDefinition> = new Map(); slots: Map<string, SlotDefinition> = new Map();
@ -137,7 +139,7 @@ export default class InlineComponentWrapper extends Wrapper {
child.render(block, null, x`#nodes` as Identifier); child.render(block, null, x`#nodes` as Identifier);
}); });
let props; let props: Identifier | undefined;
const name_changes = block.get_unique_name(`${name.name}_changes`); const name_changes = block.get_unique_name(`${name.name}_changes`);
const uses_spread = !!this.node.attributes.find(a => a.is_spread); const uses_spread = !!this.node.attributes.find(a => a.is_spread);
@ -232,7 +234,7 @@ export default class InlineComponentWrapper extends Wrapper {
: null; : null;
const unchanged = dependencies.size === 0; const unchanged = dependencies.size === 0;
let change_object; let change_object: Node | ReturnType<typeof x>;
if (attr.is_spread) { if (attr.is_spread) {
const value = attr.expression.manipulate(block); const value = attr.expression.manipulate(block);
initial_props.push(value); initial_props.push(value);
@ -596,7 +598,7 @@ export default class InlineComponentWrapper extends Wrapper {
this.node.css_custom_properties.forEach((attr) => { this.node.css_custom_properties.forEach((attr) => {
const dependencies = attr.get_dependencies(); const dependencies = attr.get_dependencies();
const should_cache = attr.should_cache(); const should_cache = attr.should_cache();
const last = should_cache && block.get_unique_name(`${attr.name.replace(/[^a-zA-Z_$]/g, '_')}_last`); const last = should_cache && block.get_unique_name(`${attr.name.replace(regex_invalid_variable_identifier_characters, '_')}_last`);
if (should_cache) block.add_variable(last); if (should_cache) block.add_variable(last);
const value = attr.get_value(block); const value = attr.get_value(block);
const init = should_cache ? x`${last} = ${value}` : value; const init = should_cache ? x`${last} = ${value}` : value;

@ -8,7 +8,7 @@ import Element from '../../nodes/Element';
import MustacheTag from '../../nodes/MustacheTag'; import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag';
import { is_head } from './shared/is_head'; import { is_head } from './shared/is_head';
import { Identifier } from 'estree'; import { Identifier, Node } from 'estree';
export default class RawMustacheTagWrapper extends Tag { export default class RawMustacheTagWrapper extends Tag {
var: Identifier = { type: 'Identifier', name: 'raw' }; var: Identifier = { type: 'Identifier', name: 'raw' };
@ -30,7 +30,7 @@ export default class RawMustacheTagWrapper extends Tag {
const can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next; const can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next;
if (can_use_innerhtml) { if (can_use_innerhtml) {
const insert = content => b`${parent_node}.innerHTML = ${content};`[0]; const insert = (content: Node) => b`${parent_node}.innerHTML = ${content};`[0];
const { init } = this.rename_this_method( const { init } = this.rename_this_method(
block, block,

@ -9,7 +9,7 @@ import add_to_set from '../../utils/add_to_set';
import get_slot_data from '../../utils/get_slot_data'; import get_slot_data from '../../utils/get_slot_data';
import { is_reserved_keyword } from '../../utils/reserved_keywords'; import { is_reserved_keyword } from '../../utils/reserved_keywords';
import is_dynamic from './shared/is_dynamic'; import is_dynamic from './shared/is_dynamic';
import { Identifier, ObjectExpression } from 'estree'; import { Identifier, ObjectExpression, Node } from 'estree';
import create_debugging_comment from './shared/create_debugging_comment'; import create_debugging_comment from './shared/create_debugging_comment';
export default class SlotWrapper extends Wrapper { export default class SlotWrapper extends Wrapper {
@ -75,9 +75,9 @@ export default class SlotWrapper extends Wrapper {
block = this.slot_block; block = this.slot_block;
} }
let get_slot_changes_fn; let get_slot_changes_fn: Identifier | 'null';
let get_slot_spread_changes_fn; let get_slot_spread_changes_fn: Identifier | undefined;
let get_slot_context_fn; let get_slot_context_fn: Identifier | 'null';
if (this.node.values.size > 0) { if (this.node.values.size > 0) {
get_slot_changes_fn = renderer.component.get_unique_name(`get_${sanitize(slot_name)}_slot_changes`); get_slot_changes_fn = renderer.component.get_unique_name(`get_${sanitize(slot_name)}_slot_changes`);
@ -176,7 +176,7 @@ export default class SlotWrapper extends Wrapper {
].filter(Boolean); ].filter(Boolean);
const all_dirty_condition = all_dirty_conditions.length ? all_dirty_conditions.reduce((condition1, condition2) => x`${condition1} || ${condition2}`) : null; const all_dirty_condition = all_dirty_conditions.length ? all_dirty_conditions.reduce((condition1, condition2) => x`${condition1} || ${condition2}`) : null;
let slot_update; let slot_update: Node[];
if (all_dirty_condition) { if (all_dirty_condition) {
const dirty = x`${all_dirty_condition} ? @get_all_dirty_from_scope(${renderer.reference('$$scope')}) : @get_slot_changes(${slot_definition}, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn})`; const dirty = x`${all_dirty_condition} ? @get_all_dirty_from_scope(${renderer.reference('$$scope')}) : @get_slot_changes(${slot_definition}, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn})`;

@ -5,6 +5,8 @@ import Wrapper from './shared/Wrapper';
import { x } from 'code-red'; import { x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class TextWrapper extends Wrapper { export default class TextWrapper extends Wrapper {
node: Text; node: Text;
data: string; data: string;
@ -27,7 +29,7 @@ export default class TextWrapper extends Wrapper {
use_space() { use_space() {
if (this.renderer.component.component_options.preserveWhitespace) return false; if (this.renderer.component.component_options.preserveWhitespace) return false;
if (/[\S\u00A0]/.test(this.data)) return false; if (regex_non_whitespace_characters.test(this.data)) return false;
return !this.node.within_pre(); return !this.node.within_pre();
} }

@ -45,7 +45,7 @@ export default class WindowWrapper extends Wrapper {
const { renderer } = this; const { renderer } = this;
const { component } = renderer; const { component } = renderer;
const events = {}; const events: Record<string, Array<{ name: string; value: string }>> = {};
const bindings: Record<string, string> = {}; const bindings: Record<string, string> = {};
add_actions(block, '@_window', this.node.actions); add_actions(block, '@_window', this.node.actions);

@ -1,7 +1,7 @@
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import Block from '../../Block'; import Block from '../../Block';
import Action from '../../../nodes/Action'; import Action from '../../../nodes/Action';
import { Expression } from 'estree'; import { Expression, Node } from 'estree';
import is_contextual from '../../../nodes/shared/is_contextual'; import is_contextual from '../../../nodes/shared/is_contextual';
export default function add_actions( export default function add_actions(
@ -12,10 +12,12 @@ export default function add_actions(
actions.forEach(action => add_action(block, target, action)); actions.forEach(action => add_action(block, target, action));
} }
const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g;
export function add_action(block: Block, target: string | Expression, action: Action) { export function add_action(block: Block, target: string | Expression, action: Action) {
const { expression, template_scope } = action; const { expression, template_scope } = action;
let snippet; let snippet: Node | undefined;
let dependencies; let dependencies: string[] | undefined;
if (expression) { if (expression) {
snippet = expression.manipulate(block); snippet = expression.manipulate(block);
@ -23,7 +25,7 @@ export function add_action(block: Block, target: string | Expression, action: Ac
} }
const id = block.get_unique_name( const id = block.get_unique_name(
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action` `${action.name.replace(regex_invalid_variable_identifier_characters, '_')}_action`
); );
block.add_variable(id); block.add_variable(id);

@ -1,5 +1,7 @@
import Component from '../../../Component'; import Component from '../../../Component';
import { INode } from '../../../nodes/interfaces'; import { INode } from '../../../nodes/interfaces';
import { regex_whitespace_characters } from '../../../../utils/patterns';
export default function create_debugging_comment( export default function create_debugging_comment(
node: INode, node: INode,
@ -13,7 +15,7 @@ export default function create_debugging_comment(
while (source[c - 1] === '{') c -= 1; while (source[c - 1] === '{') c -= 1;
} }
let d; let d: number;
if (node.type === 'InlineComponent' || node.type === 'Element' || node.type === 'SlotTemplate') { if (node.type === 'InlineComponent' || node.type === 'Element' || node.type === 'SlotTemplate') {
if (node.children.length) { if (node.children.length) {
@ -36,5 +38,5 @@ export default function create_debugging_comment(
const start = locate(c); const start = locate(c);
const loc = `(${start.line}:${start.column})`; const loc = `(${start.line}:${start.column})`;
return `${loc} ${source.slice(c, d)}`.replace(/\s/g, ' '); return `${loc} ${source.slice(c, d)}`.replace(regex_whitespace_characters, ' ');
} }

@ -2,7 +2,7 @@ import Let from '../../../nodes/Let';
import { x, p } from 'code-red'; import { x, p } from 'code-red';
import Block from '../../Block'; import Block from '../../Block';
import TemplateScope from '../../../nodes/shared/TemplateScope'; import TemplateScope from '../../../nodes/shared/TemplateScope';
import { BinaryExpression } from 'estree'; import { BinaryExpression, Identifier } from 'estree';
export function get_slot_definition(block: Block, scope: TemplateScope, lets: Let[]) { export function get_slot_definition(block: Block, scope: TemplateScope, lets: Let[]) {
if (lets.length === 0) return { block, scope }; if (lets.length === 0) return { block, scope };
@ -21,7 +21,7 @@ export function get_slot_definition(block: Block, scope: TemplateScope, lets: Le
const value_map = new Map(); const value_map = new Map();
lets.forEach(l => { lets.forEach(l => {
let value; let value: Identifier;
if (l.names.length > 1) { if (l.names.length > 1) {
// more than one, probably destructuring // more than one, probably destructuring
const unique_name = block.get_unique_name(l.names.join('_')).name; const unique_name = block.get_unique_name(l.names.join('_')).name;
@ -86,7 +86,7 @@ export function get_slot_definition(block: Block, scope: TemplateScope, lets: Le
elements[g] = grouped[g] elements[g] = grouped[g]
? grouped[g] ? grouped[g]
.map(({ name, n }) => x`${name} ? ${1 << n} : 0`) .map(({ name, n }) => x`${name} ? ${1 << n} : 0`)
.reduce((lhs, rhs) => x`${lhs} | ${rhs}`) .reduce((lhs: ReturnType<typeof x>, rhs: ReturnType<typeof x>) => x`${lhs} | ${rhs}`)
: x`0`; : x`0`;
} }

@ -1,3 +1,5 @@
export function is_head(node) { import { Node } from 'estree';
return node && node.type === 'MemberExpression' && node.object.name === '@_document' && node.property.name === 'head';
export function is_head(node: Node) {
return node && node.type === 'MemberExpression' && node.object['name'] === '@_document' && node.property['name'] === 'head';
} }

@ -8,7 +8,7 @@ import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children'; import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
import { namespaces } from '../../../utils/namespaces'; import { namespaces } from '../../../utils/namespaces';
import { start_newline } from '../../../utils/patterns'; import { regex_starts_with_newline } from '../../../utils/patterns';
import { Node, Expression as ESExpression } from 'estree'; import { Node, Expression as ESExpression } from 'estree';
export default function (node: Element, renderer: Renderer, options: RenderOptions) { export default function (node: Element, renderer: Renderer, options: RenderOptions) {
@ -44,7 +44,10 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`); class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`);
const style_expression_list = node.styles.map(style_directive => { const style_expression_list = node.styles.map(style_directive => {
const { name, expression: { node: expression } } = style_directive; let { name, important, expression: { node: expression } } = style_directive;
if (important) {
expression = x`${expression} + ' !important'`;
}
return p`"${name}": ${expression}`; return p`"${name}": ${expression}`;
}); });
@ -178,7 +181,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
const value_attribute = node.attributes.find(({ name }) => name === 'value'); const value_attribute = node.attributes.find(({ name }) => name === 'value');
if (value_attribute) { if (value_attribute) {
const first = value_attribute.chunks[0]; const first = value_attribute.chunks[0];
if (first && first.type === 'Text' && start_newline.test(first.data)) { if (first && first.type === 'Text' && regex_starts_with_newline.test(first.data)) {
renderer.add_string('\n'); renderer.add_string('\n');
} }
} }
@ -193,7 +196,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element // see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
const first = children[0]; const first = children[0];
if (first && first.type === 'Text' && start_newline.test(first.data)) { if (first && first.type === 'Text' && regex_starts_with_newline.test(first.data)) {
renderer.add_string('\n'); renderer.add_string('\n');
} }
} }

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
import Head from '../../nodes/Head'; import Head from '../../nodes/Head';
import { x } from 'code-red'; import { x } from 'code-red';
import { Node } from 'estree';
export default function(node: Head, renderer: Renderer, options: RenderOptions) { export default function(node: Head, renderer: Renderer, options: RenderOptions) {
const head_options = { const head_options = {
@ -11,6 +12,12 @@ export default function(node: Head, renderer: Renderer, options: RenderOptions)
renderer.push(); renderer.push();
renderer.render(node.children, head_options); renderer.render(node.children, head_options);
const result = renderer.pop(); const result = renderer.pop();
let expression: Node = result;
if (options.hydratable) {
const start_comment = `HEAD_${node.id}_START`;
const end_comment = `HEAD_${node.id}_END`;
expression = x`'<!-- ${start_comment} -->' + ${expression} + '<!-- ${end_comment} -->'`;
}
renderer.add_expression(x`$$result.head += '<!-- HEAD_${node.id}_START -->' + ${result} + '<!-- HEAD_${node.id}_END -->', ""`); renderer.add_expression(x`$$result.head += ${expression}, ""`);
} }

@ -4,6 +4,7 @@ import Text from '../../../nodes/Text';
import { x } from 'code-red'; import { x } from 'code-red';
import Expression from '../../../nodes/shared/Expression'; import Expression from '../../../nodes/shared/Expression';
import { Expression as ESTreeExpression } from 'estree'; import { Expression as ESTreeExpression } from 'estree';
import { regex_double_quotes } from '../../../../utils/patterns';
export function get_class_attribute_value(attribute: Attribute): ESTreeExpression { export function get_class_attribute_value(attribute: Attribute): ESTreeExpression {
// handle special case — `class={possiblyUndefined}` with scoped CSS // handle special case — `class={possiblyUndefined}` with scoped CSS
@ -21,7 +22,7 @@ export function get_attribute_value(attribute: Attribute): ESTreeExpression {
return attribute.chunks return attribute.chunks
.map((chunk) => { .map((chunk) => {
return chunk.type === 'Text' return chunk.type === 'Text'
? string_literal(chunk.data.replace(/"/g, '&quot;')) as ESTreeExpression ? string_literal(chunk.data.replace(regex_double_quotes, '&quot;')) as ESTreeExpression
: x`@escape(${chunk.node}, true)`; : x`@escape(${chunk.node}, true)`;
}) })
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`); .reduce((lhs, rhs) => x`${lhs} + ${rhs}`);

@ -1,6 +1,7 @@
import { INode } from '../../../nodes/interfaces'; import { INode } from '../../../nodes/interfaces';
import { trim_end, trim_start } from '../../../../utils/trim'; import { trim_end, trim_start } from '../../../../utils/trim';
import { link } from '../../../../utils/link'; import { link } from '../../../../utils/link';
import { regex_starts_with_whitespace } from '../../../../utils/patterns';
// similar logic from `compile/render_dom/wrappers/Fragment` // similar logic from `compile/render_dom/wrappers/Fragment`
// We want to remove trailing whitespace inside an element/component/block, // We want to remove trailing whitespace inside an element/component/block,
@ -22,7 +23,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
if (nodes.length === 0) { if (nodes.length === 0) {
const should_trim = next const should_trim = next
? next.type === 'Text' && ? next.type === 'Text' &&
/^\s/.test(next.data) && regex_starts_with_whitespace.test(next.data) &&
trimmable_at(child, next) trimmable_at(child, next)
: !child.has_ancestor('EachBlock'); : !child.has_ancestor('EachBlock');

@ -1,3 +1,10 @@
import { regex_starts_with_underscore, regex_ends_with_underscore } from '../../utils/patterns';
const regex_percentage_characters = /%/g;
const regex_file_ending = /\.[^.]+$/;
const regex_repeated_invalid_variable_identifier_characters = /[^a-zA-Z_$0-9]+/g;
const regex_starts_with_digit = /^(\d)/;
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;
@ -12,12 +19,12 @@ export default function get_name_from_filename(filename: string) {
} }
const base = parts.pop() const base = parts.pop()
.replace(/%/g, 'u') .replace(regex_percentage_characters, 'u')
.replace(/\.[^.]+$/, '') .replace(regex_file_ending, '')
.replace(/[^a-zA-Z_$0-9]+/g, '_') .replace(regex_repeated_invalid_variable_identifier_characters, '_')
.replace(/^_/, '') .replace(regex_starts_with_underscore, '')
.replace(/_$/, '') .replace(regex_ends_with_underscore, '')
.replace(/^(\d)/, '_$1'); .replace(regex_starts_with_digit, '_$1');
if (!base) { if (!base) {
throw new Error(`Could not derive component name from file ${filename}`); throw new Error(`Could not derive component name from file ${filename}`);

@ -1,6 +1,9 @@
// https://github.com/darkskyapp/string-hash/blob/master/index.js // https://github.com/darkskyapp/string-hash/blob/master/index.js
const regex_return_characters = /\r/g;
export default function hash(str: string): string { export default function hash(str: string): string {
str = str.replace(/\r/g, ''); str = str.replace(regex_return_characters, '');
let hash = 5381; let hash = 5381;
let i = str.length; let i = str.length;

@ -19,10 +19,14 @@ const escaped = {
'>': '&gt;' '>': '&gt;'
}; };
const regex_html_characters_to_escape = /["'&<>]/g;
export function escape_html(html) { export function escape_html(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]); return String(html).replace(regex_html_characters_to_escape, match => escaped[match]);
} }
const regex_template_characters_to_escape = /(\${|`|\\)/g;
export function escape_template(str) { export function escape_template(str) {
return str.replace(/(\${|`|\\)/g, '\\$1'); return str.replace(regex_template_characters_to_escape, '\\$1');
} }

@ -1,6 +1,6 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment'; import fragment from './state/fragment';
import { whitespace } from '../utils/patterns'; import { regex_whitespace } from '../utils/patterns';
import { reserved } from '../utils/names'; import { reserved } from '../utils/names';
import full_char_code_at from '../utils/full_char_code_at'; import full_char_code_at from '../utils/full_char_code_at';
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces'; import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces';
@ -15,6 +15,8 @@ interface LastAutoClosedTag {
depth: number; depth: number;
} }
const regex_position_indicator = / \(\d+:\d+\)$/;
export class Parser { export class Parser {
readonly template: string; readonly template: string;
readonly filename?: string; readonly filename?: string;
@ -74,10 +76,10 @@ export class Parser {
if (this.html.children.length) { if (this.html.children.length) {
let start = this.html.children[0].start; let start = this.html.children[0].start;
while (whitespace.test(template[start])) start += 1; while (regex_whitespace.test(template[start])) start += 1;
let end = this.html.children[this.html.children.length - 1].end; let end = this.html.children[this.html.children.length - 1].end;
while (whitespace.test(template[end - 1])) end -= 1; while (regex_whitespace.test(template[end - 1])) end -= 1;
this.html.start = start; this.html.start = start;
this.html.end = end; this.html.end = end;
@ -93,7 +95,7 @@ export class Parser {
acorn_error(err: any) { acorn_error(err: any) {
this.error({ this.error({
code: 'parse-error', code: 'parse-error',
message: err.message.replace(/ \(\d+:\d+\)$/, '') message: err.message.replace(regex_position_indicator, '')
}, err.pos); }, err.pos);
} }
@ -138,7 +140,7 @@ export class Parser {
allow_whitespace() { allow_whitespace() {
while ( while (
this.index < this.template.length && this.index < this.template.length &&
whitespace.test(this.template[this.index]) regex_whitespace.test(this.template[this.index])
) { ) {
this.index++; this.index++;
} }
@ -200,7 +202,7 @@ export class Parser {
} }
require_whitespace() { require_whitespace() {
if (!whitespace.test(this.template[this.index])) { if (!regex_whitespace.test(this.template[this.index])) {
this.error({ this.error({
code: 'missing-whitespace', code: 'missing-whitespace',
message: 'Expected whitespace' message: 'Expected whitespace'

@ -10,6 +10,7 @@ import {
import { parse_expression_at } from '../acorn'; import { parse_expression_at } from '../acorn';
import { Pattern } from 'estree'; import { Pattern } from 'estree';
import parser_errors from '../errors'; import parser_errors from '../errors';
import { regex_not_newline_characters } from '../../utils/patterns';
export default function read_context( export default function read_context(
parser: Parser parser: Parser
@ -65,7 +66,7 @@ export default function read_context(
// so we offset it by removing 1 character in the `space_with_newline` // so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered, // to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node // so it will not affect the `column` of the node
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' '); let space_with_newline = parser.template.slice(0, start).replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' '); const first_space = space_with_newline.indexOf(' ');
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);

@ -1,7 +1,7 @@
import { parse_expression_at } from '../acorn'; import { parse_expression_at } from '../acorn';
import { Parser } from '../index'; import { Parser } from '../index';
import { Node } from 'estree'; import { Node } from 'estree';
import { whitespace } from '../../utils/patterns'; import { regex_whitespace } from '../../utils/patterns';
import parser_errors from '../errors'; import parser_errors from '../errors';
export default function read_expression(parser: Parser): Node { export default function read_expression(parser: Parser): Node {
@ -20,7 +20,7 @@ export default function read_expression(parser: Parser): Node {
if (char === ')') { if (char === ')') {
num_parens -= 1; num_parens -= 1;
} else if (!whitespace.test(char)) { } else if (!regex_whitespace.test(char)) {
parser.error(parser_errors.unexpected_token(')'), index); parser.error(parser_errors.unexpected_token(')'), index);
} }

@ -3,6 +3,9 @@ import { Parser } from '../index';
import { Script } from '../../interfaces'; import { Script } from '../../interfaces';
import { Node, Program } from 'estree'; import { Node, Program } from 'estree';
import parser_errors from '../errors'; import parser_errors from '../errors';
import { regex_not_newline_characters } from '../../utils/patterns';
const regex_closing_script_tag = /<\/script\s*>/;
function get_context(parser: Parser, attributes: any[], start: number): string { function get_context(parser: Parser, attributes: any[], start: number): string {
const context = attributes.find(attribute => attribute.name === 'context'); const context = attributes.find(attribute => attribute.name === 'context');
@ -23,13 +26,13 @@ function get_context(parser: Parser, attributes: any[], start: number): string {
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script { export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
const script_start = parser.index; const script_start = parser.index;
const data = parser.read_until(/<\/script\s*>/, parser_errors.unclosed_script); const data = parser.read_until(regex_closing_script_tag, parser_errors.unclosed_script);
if (parser.index >= parser.template.length) { if (parser.index >= parser.template.length) {
parser.error(parser_errors.unclosed_script); parser.error(parser_errors.unclosed_script);
} }
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + data; const source = parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(/<\/script\s*>/); parser.read(regex_closing_script_tag);
let ast: Program; let ast: Program;

@ -5,10 +5,12 @@ import { Node } from 'estree';
import { Style } from '../../interfaces'; import { Style } from '../../interfaces';
import parser_errors from '../errors'; import parser_errors from '../errors';
const regex_closing_style_tag = /<\/style\s*>/;
export default function read_style(parser: Parser, start: number, attributes: Node[]): Style { export default function read_style(parser: Parser, start: number, attributes: Node[]): Style {
const content_start = parser.index; const content_start = parser.index;
const styles = parser.read_until(/<\/style\s*>/, parser_errors.unclosed_style); const styles = parser.read_until(regex_closing_style_tag, parser_errors.unclosed_style);
if (parser.index >= parser.template.length) { if (parser.index >= parser.template.length) {
parser.error(parser_errors.unclosed_style); parser.error(parser_errors.unclosed_style);
@ -67,7 +69,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
} }
}); });
parser.read(/<\/style\s*>/); parser.read(regex_closing_style_tag);
const end = parser.index; const end = parser.index;
return { return {

@ -1,7 +1,7 @@
import read_context from '../read/context'; import read_context from '../read/context';
import read_expression from '../read/expression'; import read_expression from '../read/expression';
import { closing_tag_omitted } from '../utils/html'; import { closing_tag_omitted } from '../utils/html';
import { whitespace } from '../../utils/patterns'; import { regex_whitespace } from '../../utils/patterns';
import { trim_start, trim_end } from '../../utils/trim'; import { trim_start, trim_end } from '../../utils/trim';
import { to_string } from '../utils/node'; import { to_string } from '../utils/node';
import { Parser } from '../index'; import { Parser } from '../index';
@ -33,6 +33,8 @@ function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after:
} }
} }
const regex_whitespace_with_closing_curly_brace = /\s*}/;
export default function mustache(parser: Parser) { export default function mustache(parser: Parser) {
const start = parser.index; const start = parser.index;
parser.index += 1; parser.index += 1;
@ -87,8 +89,8 @@ export default function mustache(parser: Parser) {
// strip leading/trailing whitespace as necessary // strip leading/trailing whitespace as necessary
const char_before = parser.template[block.start - 1]; const char_before = parser.template[block.start - 1];
const char_after = parser.template[parser.index]; const char_after = parser.template[parser.index];
const trim_before = !char_before || whitespace.test(char_before); const trim_before = !char_before || regex_whitespace.test(char_before);
const trim_after = !char_after || whitespace.test(char_after); const trim_after = !char_after || regex_whitespace.test(char_after);
trim_whitespace(block, trim_before, trim_after); trim_whitespace(block, trim_before, trim_after);
@ -174,8 +176,8 @@ export default function mustache(parser: Parser) {
} else { } else {
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') { if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
parser.error(parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock') parser.error(parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock')
? parser_errors.invalid_catch_placement_unclosed_block(to_string(block)) ? parser_errors.invalid_catch_placement_unclosed_block(to_string(block))
: parser_errors.invalid_catch_placement_without_await : parser_errors.invalid_catch_placement_without_await
); );
} }
} }
@ -290,7 +292,7 @@ export default function mustache(parser: Parser) {
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then'); const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
if (await_block_shorthand) { if (await_block_shorthand) {
if (parser.match_regex(/\s*}/)) { if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace(); parser.allow_whitespace();
} else { } else {
parser.require_whitespace(); parser.require_whitespace();
@ -301,7 +303,7 @@ export default function mustache(parser: Parser) {
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch'); const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
if (await_block_catch_shorthand) { if (await_block_catch_shorthand) {
if (parser.match_regex(/\s*}/)) { if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace(); parser.allow_whitespace();
} else { } else {
parser.require_whitespace(); parser.require_whitespace();
@ -350,7 +352,7 @@ export default function mustache(parser: Parser) {
let identifiers; let identifiers;
// Implies {@debug} which indicates "debug all" // Implies {@debug} which indicates "debug all"
if (parser.read(/\s*}/)) { if (parser.read(regex_whitespace_with_closing_curly_brace)) {
identifiers = []; identifiers = [];
} else { } else {
const expression = read_expression(parser); const expression = read_expression(parser);

@ -53,13 +53,17 @@ function parent_is_head(stack) {
return false; return false;
} }
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_capital_letter = /[A-Z]/;
export default function tag(parser: Parser) { export default function tag(parser: Parser) {
const start = parser.index++; const start = parser.index++;
let parent = parser.current(); let parent = parser.current();
if (parser.eat('!--')) { if (parser.eat('!--')) {
const data = parser.read_until(/-->/); const data = parser.read_until(regex_closing_comment);
parser.eat('-->', true, parser_errors.unclosed_comment); parser.eat('-->', true, parser_errors.unclosed_comment);
parser.current().children.push({ parser.current().children.push({
@ -104,7 +108,7 @@ export default function tag(parser: Parser) {
const type = meta_tags.has(name) const type = meta_tags.has(name)
? meta_tags.get(name) ? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent' : (regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'svelte:fragment' ? 'SlotTemplate' : name === 'svelte:fragment' ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack) ? 'Title' : name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element'; : name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
@ -218,11 +222,10 @@ export default function tag(parser: Parser) {
// special case // special case
element.children = read_sequence( element.children = read_sequence(
parser, parser,
() => () => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
/^<\/textarea(\s[^>]*)?>/i.test(parser.template.slice(parser.index)),
'inside <textarea>' 'inside <textarea>'
); );
parser.read(/^<\/textarea(\s[^>]*)?>/i); parser.read(regex_closing_textarea_tag);
element.end = parser.index; element.end = parser.index;
} else if (name === 'script' || name === 'style') { } else if (name === 'script' || name === 'style') {
// special case // special case
@ -237,6 +240,8 @@ export default function tag(parser: Parser) {
} }
} }
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
function read_tag_name(parser: Parser) { function read_tag_name(parser: Parser) {
const start = parser.index; const start = parser.index;
@ -266,7 +271,7 @@ function read_tag_name(parser: Parser) {
if (parser.read(SLOT)) return 'svelte:fragment'; if (parser.read(SLOT)) return 'svelte:fragment';
const name = parser.read_until(/(\s|\/|>)/); const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (meta_tags.has(name)) return name; if (meta_tags.has(name)) return name;
@ -295,6 +300,10 @@ function use_name_as_expression(type:string, name:string):boolean {
return false; return false;
} }
// eslint-disable-next-line no-useless-escape
const regex_token_ending_character = /[\s=\/>"']/;
const regex_quote_characters = /["']/;
function read_attribute(parser: Parser, unique_names: Set<string>) { function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index; const start = parser.index;
@ -353,8 +362,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
} }
} }
// eslint-disable-next-line no-useless-escape const name = parser.read_until(regex_token_ending_character);
const name = parser.read_until(/[\s=\/>"']/);
if (!name) return null; if (!name) return null;
let end = parser.index; let end = parser.index;
@ -369,7 +377,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
parser.allow_whitespace(); parser.allow_whitespace();
value = read_attribute_value(parser); value = read_attribute_value(parser);
end = parser.index; end = parser.index;
} else if (parser.match_regex(/["']/)) { } else if (parser.match_regex(regex_quote_characters)) {
parser.error(parser_errors.unexpected_token('='), parser.index); parser.error(parser_errors.unexpected_token('='), parser.index);
} }
@ -396,6 +404,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
end, end,
type, type,
name: directive_name, name: directive_name,
modifiers,
value value
}; };
} }

@ -4,6 +4,7 @@ import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_off
import { decode_map } from './decode_sourcemap'; import { decode_map } from './decode_sourcemap';
import { replace_in_code, slice_source } from './replace_in_code'; import { replace_in_code, slice_source } from './replace_in_code';
import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types'; import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
import { regex_whitespaces } from '../utils/patterns';
export * from './types'; export * from './types';
@ -13,8 +14,10 @@ interface SourceUpdate {
dependencies?: string[]; dependencies?: string[];
} }
const regex_filepath_separator = /[/\\]/;
function get_file_basename(filename: string) { function get_file_basename(filename: string) {
return filename.split(/[/\\]/).pop(); return filename.split(regex_filepath_separator).pop();
} }
/** /**
@ -120,20 +123,26 @@ function processed_tag_to_code(
return tag_open_code.concat(content_code).concat(tag_close_code); return tag_open_code.concat(content_code).concat(tag_close_code);
} }
const regex_quoted_value = /^['"](.*)['"]$/;
function parse_tag_attributes(str: string) { function parse_tag_attributes(str: string) {
// note: won't work with attribute values containing spaces. // note: won't work with attribute values containing spaces.
return str return str
.split(/\s+/) .split(regex_whitespaces)
.filter(Boolean) .filter(Boolean)
.reduce((attrs, attr) => { .reduce((attrs, attr) => {
const i = attr.indexOf('='); const i = attr.indexOf('=');
const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i + 1)] : [attr]; const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i + 1)] : [attr];
const [, unquoted] = (value && value.match(/^['"](.*)['"]$/)) || []; const [, unquoted] = (value && value.match(regex_quoted_value)) || [];
return { ...attrs, [key]: unquoted ?? value ?? true }; return { ...attrs, [key]: unquoted ?? value ?? true };
}, {}); }, {});
} }
const regex_style_tags = /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi;
const regex_script_tags = /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
/** /**
* Calculate the updates required to process all instances of the specified tag. * Calculate the updates required to process all instances of the specified tag.
*/ */
@ -143,10 +152,7 @@ async function process_tag(
source: Source source: Source
): Promise<SourceUpdate> { ): Promise<SourceUpdate> {
const { filename, source: markup } = source; const { filename, source: markup } = source;
const tag_regex = const tag_regex = tag_name === 'style' ? regex_style_tags : regex_script_tags;
tag_name === 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const dependencies: string[] = []; const dependencies: string[] = [];
@ -190,7 +196,7 @@ async function process_markup(process: MarkupPreprocessor, source: Source) {
string: processed.code, string: processed.code,
map: processed.map map: processed.map
? // TODO: can we use decode_sourcemap? ? // TODO: can we use decode_sourcemap?
typeof processed.map === 'string' typeof processed.map === 'string'
? JSON.parse(processed.map) ? JSON.parse(processed.map)
: processed.map : processed.map
: undefined, : undefined,

@ -1,11 +1,12 @@
import { TemplateNode } from '../interfaces'; import { TemplateNode } from '../interfaces';
import { flatten } from './flatten'; import { flatten } from './flatten';
import { regex_whitespace } from './patterns';
const pattern = /^\s*svelte-ignore\s+([\s\S]+)\s*$/m; const regex_svelte_ignore = /^\s*svelte-ignore\s+([\s\S]+)\s*$/m;
export function extract_svelte_ignore(text: string): string[] { export function extract_svelte_ignore(text: string): string[] {
const match = pattern.exec(text); const match = regex_svelte_ignore.exec(text);
return match ? match[1].split(/[^\S]/).map(x => x.trim()).filter(Boolean) : []; return match ? match[1].split(regex_whitespace).map(x => x.trim()).filter(Boolean) : [];
} }
export function extract_svelte_ignore_from_comments<Node extends { leadingComments?: Array<{value: string}> }>(node: Node): string[] { export function extract_svelte_ignore_from_comments<Node extends { leadingComments?: Array<{value: string}> }>(node: Node): string[] {

@ -1,5 +1,7 @@
const regex_tabs = /^\t+/;
function tabs_to_spaces(str: string) { function tabs_to_spaces(str: string) {
return str.replace(/^\t+/, match => match.split('\t').join(' ')); return str.replace(regex_tabs, match => match.split('\t').join(' '));
} }
export default function get_code_frame( export default function get_code_frame(

@ -61,6 +61,8 @@ function merge_tables<T>(this_table: T[], other_table: T[]): [T[], number[], boo
return [new_table, idx_map, val_changed, idx_changed]; return [new_table, idx_map, val_changed, idx_changed];
} }
const regex_line_token = /([^\d\w\s]|\s+)/g;
export class MappedCode { export class MappedCode {
string: string; string: string;
map: DecodedSourceMap; map: DecodedSourceMap;
@ -195,7 +197,7 @@ export class MappedCode {
const line_list = source.split('\n'); const line_list = source.split('\n');
for (let line = 0; line < line_list.length; line++) { for (let line = 0; line < line_list.length; line++) {
map.mappings.push([]); map.mappings.push([]);
const token_list = line_list[line].split(/([^\d\w\s]|\s+)/g); const token_list = line_list[line].split(regex_line_token);
for (let token = 0, column = 0; token < token_list.length; token++) { for (let token = 0, column = 0; token < token_list.length; token++) {
if (token_list[token] == '') continue; if (token_list[token] == '') continue;
map.mappings[line].push([column, 0, offset.line + line, column]); map.mappings[line].push([column, 0, offset.line + line, column]);
@ -289,6 +291,8 @@ export function apply_preprocessor_sourcemap(filename: string, svelte_map: Sourc
return result_map as SourceMap; return result_map as SourceMap;
} }
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code // parse attached sourcemap in processed.code
export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void { export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void {
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)'; const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
@ -302,7 +306,7 @@ export function parse_attached_sourcemap(processed: Processed, tag_name: 'script
} }
processed.code = processed.code.replace(regex, (_, match1, match2) => { processed.code = processed.code.replace(regex, (_, match1, match2) => {
const map_url = (tag_name == 'script') ? (match1 || match2) : match1; const map_url = (tag_name == 'script') ? (match1 || match2) : match1;
const map_data = (map_url.match(/data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/) || [])[1]; const map_data = (map_url.match(regex_data_uri) || [])[1];
if (map_data) { if (map_data) {
// sourceMappingURL is data URL // sourceMappingURL is data URL
if (processed.map) { if (processed.map) {

@ -1,5 +1,6 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import full_char_code_at from './full_char_code_at'; import full_char_code_at from './full_char_code_at';
import { regex_starts_with_underscore, regex_ends_with_underscore } from './patterns';
export const reserved = new Set([ export const reserved = new Set([
'arguments', 'arguments',
@ -65,10 +66,13 @@ export function is_valid(str: string): boolean {
return true; return true;
} }
const regex_non_standard_characters = /[^a-zA-Z0-9_]+/g;
const regex_starts_with_number = /^[0-9]/;
export function sanitize(name: string) { export function sanitize(name: string) {
return name return name
.replace(/[^a-zA-Z0-9_]+/g, '_') .replace(regex_non_standard_characters, '_')
.replace(/^_/, '') .replace(regex_starts_with_underscore, '')
.replace(/_$/, '') .replace(regex_ends_with_underscore, '')
.replace(/^[0-9]/, '_$&'); .replace(regex_starts_with_number, '_$&');
} }

@ -1,6 +1,24 @@
export const whitespace = /[ \t\r\n]/; export const regex_whitespace = /\s/;
export const start_whitespace = /^[ \t\r\n]*/; export const regex_whitespaces = /\s+/;
export const end_whitespace = /[ \t\r\n]*$/; export const regex_starts_with_whitespace = /^\s/;
export const start_newline = /^\r?\n/; export const regex_starts_with_whitespaces = /^[ \t\r\n]*/;
export const regex_ends_with_whitespace = /\s$/;
export const regex_ends_with_whitespaces = /[ \t\r\n]*$/;
export const regex_only_whitespaces = /^\s+$/;
export const dimensions = /^(?:offset|client)(?:Width|Height)$/; export const regex_whitespace_characters = /\s/g;
export const regex_non_whitespace_character = /\S/;
export const regex_starts_with_newline = /^\r?\n/;
export const regex_not_newline_characters = /[^\n]/g;
export const regex_double_quotes = /"/g;
export const regex_backslashes = /\\/g;
export const regex_starts_with_underscore = /^_/;
export const regex_ends_with_underscore = /_$/;
export const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g;
export const regex_dimensions = /^(?:offset|client)(?:Width|Height)$/;

@ -1,9 +1,9 @@
import { start_whitespace, end_whitespace } from './patterns'; import { regex_starts_with_whitespaces, regex_ends_with_whitespaces } from './patterns';
export function trim_start(str: string) { export function trim_start(str: string) {
return str.replace(start_whitespace, ''); return str.replace(regex_starts_with_whitespaces, '');
} }
export function trim_end(str: string) { export function trim_end(str: string) {
return str.replace(end_whitespace, ''); return str.replace(regex_ends_with_whitespaces, '');
} }

@ -73,7 +73,7 @@ let world1 = 'world';
let world2 = 'world'; let world2 = 'world';
function instance($$self, $$props, $$invalidate) { function instance($$self, $$props, $$invalidate) {
const world3 = 'world'; let world3 = 'world';
function foo() { function foo() {
$$invalidate(0, world3 = 'svelte'); $$invalidate(0, world3 = 'svelte');

@ -1,7 +1,7 @@
<script> <script>
let world1 = 'world'; let world1 = 'world';
let world2 = 'world'; let world2 = 'world';
const world3 = 'world'; let world3 = 'world';
function foo() { function foo() {
world3 = 'svelte'; world3 = 'svelte';
} }

@ -0,0 +1 @@
<div style:color|important={myColor}></div>

@ -0,0 +1,50 @@
{
"html": {
"start": 0,
"end": 43,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 43,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 36,
"type": "StyleDirective",
"name": "color",
"modifiers": [
"important"
],
"value": [
{
"start": 27,
"end": 36,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 28,
"end": 35,
"loc": {
"start": {
"line": 1,
"column": 28
},
"end": {
"line": 1,
"column": 35
}
},
"name": "myColor"
}
}
]
}
],
"children": []
}
]
}
}

@ -15,6 +15,7 @@
"end": 16, "end": 16,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": true "value": true
} }
], ],

@ -15,6 +15,7 @@
"end": 22, "end": 22,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 18, "start": 18,
@ -45,6 +46,7 @@
"start": 35, "start": 35,
"end": 52, "end": 52,
"type": "StyleDirective", "type": "StyleDirective",
"modifiers": [],
"name": "color", "name": "color",
"value": [ "value": [
{ {
@ -77,6 +79,7 @@
"end": 80, "end": 80,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 77, "start": 77,
@ -108,6 +111,7 @@
"end": 120, "end": 120,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 106, "start": 106,
@ -160,6 +164,7 @@
"end": 160, "end": 160,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 146, "start": 146,
@ -212,6 +217,7 @@
"end": 198, "end": 198,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 185, "start": 185,
@ -264,6 +270,7 @@
"end": 245, "end": 245,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 223, "start": 223,

@ -15,6 +15,7 @@
"end": 26, "end": 26,
"type": "StyleDirective", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [],
"value": [ "value": [
{ {
"start": 17, "start": 17,

@ -0,0 +1,22 @@
export default {
html: `
<h1 class="svelte-szzkfu" style="background-color: red;">hello</h1>
<h1 class="svelte-szzkfu" style="background-color: red !important;">hello</h1>
`,
ssrHtml: `
<h1 class="svelte-szzkfu" style="background-color: red;">hello</h1>
<h1 class="svelte-szzkfu" style="background-color: red !important;">hello</h1>
`,
test({ assert, target, window, component }) {
const h1s = target.querySelectorAll('h1');
assert.equal(window.getComputedStyle(h1s[0])['backgroundColor'], 'rgb(0, 0, 255)');
assert.equal(window.getComputedStyle(h1s[1])['backgroundColor'], 'rgb(255, 0, 0)');
component.color = 'yellow';
assert.equal(window.getComputedStyle(h1s[0])['backgroundColor'], 'rgb(0, 0, 255)');
assert.equal(window.getComputedStyle(h1s[1])['backgroundColor'], 'rgb(255, 255, 0)');
}
};

@ -0,0 +1,12 @@
<script>
export let color = 'red'
</script>
<h1 style:background-color={color} >hello</h1>
<h1 style:background-color|important="{color}">hello</h1>
<style>
h1 {
background-color: blue !important;
}
</style>

@ -117,7 +117,9 @@ describe('ssr', () => {
fs.writeFileSync(`${dir}/_actual-head.html`, head); fs.writeFileSync(`${dir}/_actual-head.html`, head);
try { try {
assert.htmlEqual( (compileOptions.hydratable
? assert.htmlEqualWithComments
: assert.htmlEqual)(
head, head,
fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8') fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8')
); );

@ -1,5 +1,6 @@
export default { export default {
compileOptions: { compileOptions: {
hydratable: true hydratable: true
} },
withoutNormalizeHtml: true
}; };

@ -6,7 +6,7 @@
"column": 20, "column": 20,
"line": 11 "line": 11
}, },
"message": "A11y: not interactive element cannot have positive tabIndex value", "message": "A11y: noninteractive element cannot have positive tabIndex value",
"pos": 221, "pos": 221,
"start": { "start": {
"character": 221, "character": 221,
@ -21,7 +21,7 @@
"column": 35, "column": 35,
"line": 12 "line": 12
}, },
"message": "A11y: not interactive element cannot have positive tabIndex value", "message": "A11y: noninteractive element cannot have positive tabIndex value",
"pos": 242, "pos": 242,
"start": { "start": {
"character": 242, "character": 242,
@ -36,7 +36,7 @@
"column": 24, "column": 24,
"line": 13 "line": 13
}, },
"message": "A11y: not interactive element cannot have positive tabIndex value", "message": "A11y: noninteractive element cannot have positive tabIndex value",
"pos": 278, "pos": 278,
"start": { "start": {
"character": 278, "character": 278,
@ -51,7 +51,7 @@
"column": 26, "column": 26,
"line": 14 "line": 14
}, },
"message": "A11y: not interactive element cannot have positive tabIndex value", "message": "A11y: noninteractive element cannot have positive tabIndex value",
"pos": 303, "pos": 303,
"start": { "start": {
"character": 303, "character": 303,

@ -0,0 +1,17 @@
[
{
"code": "assignment-to-const",
"message": "You are assigning to a const",
"start": {
"line": 13,
"column": 24,
"character": 282
},
"end": {
"line": 13,
"column": 35,
"character": 293
},
"pos": 282
}
]

@ -0,0 +1,15 @@
<script>
const immutable = 0;
const obj1 = { prop: true };
const obj2 = { prop: 0 }
</script>
<!-- should not error -->
<button on:click={() => obj1.prop = false}>click</button>
<button on:click={() => obj2.prop++}>click</button>
<!-- should error -->
<button on:click={() => immutable++}>click</button>

@ -0,0 +1,17 @@
[
{
"code": "assignment-to-const",
"message": "You are assigning to a const",
"start": {
"line": 14,
"column": 3,
"character": 172
},
"end": {
"line": 14,
"column": 10,
"character": 179
},
"pos": 172
}
]

@ -0,0 +1,20 @@
<script>
const foo = 'hello';
function shouldNotError() {
let foo = 0;
function inner() {
foo = 1;
}
}
function shouldError() {
function inner() {
foo = 1;
}
}
</script>
<button on:click={shouldNotError}>click</button>
<button on:click={shouldError}>click</button>

@ -0,0 +1,17 @@
[
{
"code": "assignment-to-const",
"message": "You are assigning to a const",
"start": {
"line": 17,
"column": 2,
"character": 189
},
"end": {
"line": 17,
"column": 9,
"character": 196
},
"pos": 189
}
]

@ -0,0 +1,21 @@
<script>
const foo = 'hello';
</script>
<button on:click={() => {
let foo = 0;
function inner() {
foo = 1;
}
}}>
click
</button>
<button on:click={() => {
function inner() {
foo = 1;
}
}}>
click
</button>

@ -0,0 +1,17 @@
[
{
"code": "assignment-to-const",
"message": "You are assigning to a const",
"start": {
"line": 16,
"column": 2,
"character": 225
},
"end": {
"line": 16,
"column": 18,
"character": 241
},
"pos": 225
}
]

@ -0,0 +1,24 @@
<script>
const immutable = false;
const obj1 = { prop: true };
const obj2 = { prop: 0 };
function shouldNotError() {
obj1.prop = false;
}
function shouldNotError2() {
obj2.prop++;
}
function shouldError() {
immutable = true
}
</script>
<button on:click={shouldNotError}>click</button>
<button on:click={shouldNotError2}>click</button>
<button on:click={shouldError}>click</button>

@ -0,0 +1,31 @@
<a href="https://svelte.dev" target="_blank">svelte website (invalid)</a>
<a href="https://svelte.dev" target="_blank" rel="">svelte website (invalid)</a>
<a href="https://svelte.dev" target="_blank" rel="noopener">svelte website (invalid)</a>
<a href={'https://svelte.dev'} target="_blank">svelte website (invalid)</a>
<a href={'https://svelte.dev'} target="_blank" rel="">svelte website (invalid)</a>
<a href={'https://svelte.dev'} target="_blank" rel="noopener">svelte website (invalid)</a>
<a href="//svelte.dev" target="_blank">svelte website (invalid)</a>
<a href="//svelte.dev" target="_blank" rel="">svelte website (invalid)</a>
<a href="//svelte.dev" target="_blank" rel="noopener">svelte website (invalid)</a>
<a href="http://svelte.dev" target="_blank">svelte website (invalid)</a>
<a href="http://svelte.dev" target="_blank" rel="">svelte website (invalid)</a>
<a href="http://svelte.dev" target="_blank" rel="noopener">svelte website (invalid)</a>
<a href="HTTP://svelte.dev" target="_blank">svelte website (invalid)</a>
<a href="HTTP://svelte.dev" target="_blank" rel="">svelte website (invalid)</a>
<a href="HTTP://svelte.dev" target="_blank" rel="noopener">svelte website (invalid)</a>
<a href={'HTTPS://svelte.dev'} target="_blank">svelte website (invalid)</a>
<a href={'HTTPS://svelte.dev'} target="_blank" rel="">svelte website (invalid)</a>
<a href={'HTTPS://svelte.dev'} target="_blank" rel="noopener">svelte website (invalid)</a>
<a href="same-host" target="_blank">Same host (valid)</a>
<a href="same-host" target="_blank" rel="">Same host (valid)</a>
<a href="same-host" target="_blank" rel="noopener">Same host (valid)</a>
<a href="http://svelte.dev" target="_blank" rel="noreferrer">svelte website (valid)</a>
<a href="http://svelte.dev" target="_blank" rel="noreferrer noopener">svelte website (valid)</a>
<a href="HTTP://svelte.dev" target="_blank" rel="noreferrer">svelte website (valid)</a>
<a href="HTTP://svelte.dev" target="_blank" rel="noreferrer noopener">svelte website (valid)</a>
<a href="https://svelte.dev" target="_blank" rel="noreferrer">svelte website (valid)</a>
<a href="https://svelte.dev" target="_blank" rel="noreferrer noopener">svelte website (valid)</a>
<a href="HTTPS://svelte.dev" target="_blank" rel="noreferrer">svelte website (valid)</a>
<a href="HTTPS://svelte.dev" target="_blank" rel="noreferrer noopener">svelte website (valid)</a>
<a href="//svelte.dev" target="_blank" rel="noreferrer">svelte website (valid)</a>
<a href="//svelte.dev" target="_blank" rel="noreferrer noopener">svelte website (valid)</a>

@ -0,0 +1,272 @@
[
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 73,
"column": 73,
"line": 1
},
"pos": 0,
"start": {
"character": 0,
"column": 0,
"line": 1
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 154,
"column": 80,
"line": 2
},
"pos": 74,
"start": {
"character": 74,
"column": 0,
"line": 2
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 243,
"column": 88,
"line": 3
},
"pos": 155,
"start": {
"character": 155,
"column": 0,
"line": 3
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 319,
"column": 75,
"line": 4
},
"pos": 244,
"start": {
"character": 244,
"column": 0,
"line": 4
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 402,
"column": 82,
"line": 5
},
"pos": 320,
"start": {
"character": 320,
"column": 0,
"line": 5
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 493,
"column": 90,
"line": 6
},
"pos": 403,
"start": {
"character": 403,
"column": 0,
"line": 6
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 561,
"column": 67,
"line": 7
},
"pos": 494,
"start": {
"character": 494,
"column": 0,
"line": 7
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 636,
"column": 74,
"line": 8
},
"pos": 562,
"start": {
"character": 562,
"column": 0,
"line": 8
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 719,
"column": 82,
"line": 9
},
"pos": 637,
"start": {
"character": 637,
"column": 0,
"line": 9
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 792,
"column": 72,
"line": 10
},
"pos": 720,
"start": {
"character": 720,
"column": 0,
"line": 10
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 872,
"column": 79,
"line": 11
},
"pos": 793,
"start": {
"character": 793,
"column": 0,
"line": 11
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 960,
"column": 87,
"line": 12
},
"pos": 873,
"start": {
"character": 873,
"column": 0,
"line": 12
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 1033,
"column": 72,
"line": 13
},
"pos": 961,
"start": {
"character": 961,
"column": 0,
"line": 13
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 1113,
"column": 79,
"line": 14
},
"pos": 1034,
"start": {
"character": 1034,
"column": 0,
"line": 14
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 1201,
"column": 87,
"line": 15
},
"pos": 1114,
"start": {
"character": 1114,
"column": 0,
"line": 15
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 1277,
"column": 75,
"line": 16
},
"pos": 1202,
"start": {
"character": 1202,
"column": 0,
"line": 16
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 1360,
"column": 82,
"line": 17
},
"pos": 1278,
"start": {
"character": 1278,
"column": 0,
"line": 17
}
},
{
"code": "security-anchor-rel-noreferrer",
"message": "Security: Anchor with \"target=_blank\" should have rel attribute containing the value \"noreferrer\"",
"end": {
"character": 1451,
"column": 90,
"line": 18
},
"pos": 1361,
"start": {
"character": 1361,
"column": 0,
"line": 18
}
}
]

@ -0,0 +1,15 @@
[{
"message": "Valid modifiers for style directives are: important",
"code": "invalid-style-directive-modifier",
"start": {
"line": 1,
"column": 8,
"character": 8
},
"end": {
"line": 1,
"column": 29,
"character": 29
},
"pos": 8
}]
Loading…
Cancel
Save