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:
contents: read # to fetch code (actions/checkout)
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:
needs: Setup
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
@ -11,21 +46,38 @@ jobs:
node-version: [8, 10, 12, 14, 16]
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
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 test
env:
SKIP_PREPARE: true
- run: npm run test:integration
env:
CI: true
Lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: npm
- run: 'npm i && npm run lint'
@ -36,8 +88,11 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
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 = {
file: [
'test/test.ts'
],
file: is_unit_test ? [] : ['test/test.ts'],
require: [
'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
## 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
* Add a11y warnings:

@ -86,15 +86,15 @@
},
"types": "types/runtime/index.d.ts",
"scripts": {
"test": "mocha --exit",
"test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts --exit",
"quicktest": "mocha",
"test": "npm run test:unit && npm run test:integration",
"test:integration": "mocha --exit",
"test:unit": "mocha --config .mocharc.unit.js --exit",
"quicktest": "mocha --exit",
"build": "rollup -c && npm run tsd",
"prepare": "npm run build",
"prepare": "node scripts/skip_in_ci.js npm run build",
"dev": "rollup -cw",
"pretest": "npm run build",
"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",
"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
<!-- 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
<!-- 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`
* `clientHeight`
@ -874,6 +874,9 @@ The `style:` directive provides a shorthand for setting multiple styles on an el
<!-- Multiple styles can be included -->
<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.
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.
@ -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.
@ -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
<!-- 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 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.

@ -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.
```sv
<!-- A11y: not interactive element cannot have positive tabIndex value -->
<!-- A11y: noninteractive element cannot have positive tabIndex value -->
<div tabindex='0' />
```

@ -47,6 +47,10 @@ interface ComponentOptions {
preserveWhitespace?: boolean;
}
const regex_leading_directory_separator = /^[/\\]/;
const regex_starts_with_term_export = /^Export/;
const regex_contains_term_function = /Function/;
export default class Component {
stats: Stats;
warnings: Warning[];
@ -136,7 +140,7 @@ export default class Component {
(typeof process !== 'undefined'
? compile_options.filename
.replace(process.cwd(), '')
.replace(/^[/\\]/, '')
.replace(regex_leading_directory_separator, '')
: compile_options.filename);
this.locate = getLocator(this.source, { offsetLine: 1 });
@ -638,7 +642,7 @@ export default class Component {
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);
if (replacement) {
body[i] = replacement;
@ -788,6 +792,42 @@ export default class Component {
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') {
component.extract_imports(node);
// TODO: to use actual remove
@ -795,7 +835,7 @@ export default class Component {
return this.skip();
}
if (/^Export/.test(node.type)) {
if (regex_starts_with_term_export.test(node.type)) {
const replacement = component.extract_exports(node);
if (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 (!((/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);
}
}
@ -965,7 +1005,7 @@ export default class Component {
walk(this.ast.instance.content, {
enter(node: Node) {
if (/Function/.test(node.type)) {
if (regex_contains_term_function.test(node.type)) {
return this.skip();
}
@ -1089,7 +1129,7 @@ export default class Component {
this.replace(b`
${node.declarations.length ? node : null}
${ props.length > 0 && b`let { ${ props } } = $$props;`}
${ props.length > 0 && b`let { ${props} } = $$props;`}
${inserts}
` as any);
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) {
const component_options: ComponentOptions = {
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');
function get_value(attribute, {code, message}) {
function get_value(attribute, { code, message }) {
const { value } = attribute;
const chunk = value[0];
@ -1505,7 +1547,7 @@ function process_component_options(component: Component, nodes) {
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);
}

@ -282,6 +282,10 @@ export default {
code: 'invalid-component-style-directive',
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) => ({
code: 'invalid-component-svelte-directive',
message: `svelte:${name} directives cannot be used on components`
@ -301,5 +305,5 @@ export default {
directive_conflict: (directive1, directive2) => ({
code: 'directive-conflict',
message: `Cannot use ${directive1} and ${directive2} on the same element`
})
})
};

@ -185,7 +185,7 @@ export default {
}),
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: {
code: 'redundant-event-modifier',

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

@ -9,9 +9,12 @@ import hash from '../utils/hash';
import compiler_warnings from '../compiler_warnings';
import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore';
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 {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
return name.replace(regex_css_browser_prefix, '');
}
const is_keyframes_node = (node: CssNode) =>
@ -147,10 +150,10 @@ class Declaration {
// Don't minify whitespace in custom properties, since some browsers (Chromium < 99)
// 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;
while (/\s/.test(code.original[start])) start += 1;
while (regex_whitespace.test(code.original[start])) start += 1;
if (start - c > 1) {
code.overwrite(c, start, ':');

@ -35,6 +35,9 @@ const valid_options = [
'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[]) {
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}')`);
}
if (name && /^[a-z]/.test(name)) {
if (name && regex_starts_with_lowercase_character.test(name)) {
const message = 'options.name should be capitalised';
warnings.push({
code: 'options-lowercase-name',

@ -3,7 +3,7 @@ import get_object from '../utils/get_object';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import {dimensions} from '../../utils/patterns';
import { regex_dimensions } from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
@ -88,7 +88,7 @@ export default class Binding extends Node {
const type = parent.get_static_attribute_value('type');
this.is_readonly =
dimensions.test(this.name) ||
regex_dimensions.test(this.name) ||
(isElement(parent) &&
((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file')) /* TODO others? */);

@ -11,7 +11,7 @@ import StyleDirective from './StyleDirective';
import Text from './Text';
import { namespaces } from '../../utils/namespaces';
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 list from '../../utils/list';
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 {
type: 'Element';
name: string;
@ -255,7 +259,7 @@ export default class Element extends Node {
// 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/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
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
if (regex_illegal_attribute_character.test(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));
}
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));
}
@ -627,6 +631,26 @@ export default class Element extends Node {
const href_attribute = attribute_map.get('href') || attribute_map.get('xlink:href');
const id_attribute = attribute_map.get('id');
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) {
const href_value = href_attribute.get_static_value();
@ -747,7 +771,7 @@ export default class Element extends Node {
if (this.name === 'figure') {
const children = this.children.filter(node => {
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;
});
@ -879,7 +903,7 @@ export default class Element extends Node {
if (this.name !== 'video') {
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')) {
return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `<svg>. Use '${name.replace('offset', 'client')}' instead`));
} else if (is_svg(this.name)) {
@ -1016,7 +1040,7 @@ export default class Element extends Node {
if (attribute && !attribute.is_true) {
attribute.chunks.forEach((chunk, index) => {
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) {
data = data.trimLeft();
} 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(
node,
attributes: string[],
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 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
@ -1043,10 +1069,12 @@ function should_have_attribute(
node.component.warn(node, compiler_warnings.a11y_missing_attribute(name, article, sequence));
}
const regex_minus_sign = /-/;
function within_custom_element(parent: INode) {
while (parent) {
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;
}
return false;

@ -6,6 +6,8 @@ import { Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
const regex_contains_term_function_expression = /FunctionExpression/;
export default class EventHandler extends Node {
type: 'EventHandler';
name: string;
@ -25,7 +27,7 @@ export default class EventHandler extends Node {
this.expression = new Expression(component, this, template_scope, info.expression);
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
// `event` is passed to another function, we can make it passive
this.can_make_passive = true;
@ -55,7 +57,7 @@ export default class EventHandler extends Node {
}
const node = this.expression.node;
if (/FunctionExpression/.test(node.type)) {
if (regex_contains_term_function_expression.test(node.type)) {
return false;
}

@ -5,6 +5,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { regex_non_whitespace_character } from '../../utils/patterns';
export default class Head extends Node {
type: 'Head';
@ -20,7 +21,7 @@ export default class Head extends Node {
}
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) {

@ -10,6 +10,7 @@ import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { regex_only_whitespaces } from '../../utils/patterns';
export default class InlineComponent extends Node {
type: 'InlineComponent';
@ -73,7 +74,7 @@ export default class InlineComponent extends Node {
case 'Transition':
return component.error(node, compiler_errors.invalid_transition);
case 'StyleDirective':
return component.error(node, compiler_errors.invalid_component_style_directive);
@ -168,7 +169,7 @@ export default class InlineComponent extends 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) {

@ -1,20 +1,42 @@
import { TemplateNode } from '../../interfaces';
import list from '../../utils/list';
import compiler_errors from '../compiler_errors';
import Component from '../Component';
import { nodes_to_template_literal } from '../utils/nodes_to_template_literal';
import Expression from './shared/Expression';
import Node from './shared/Node';
import TemplateScope from './shared/TemplateScope';
const valid_modifiers = new Set(['important']);
export default class StyleDirective extends Node {
type: 'StyleDirective';
name: string;
modifiers: Set<string>;
expression: Expression;
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);
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
// the StyleDirective going forward.
@ -34,6 +56,9 @@ export default class StyleDirective extends Node {
this.expression = new Expression(component, this, scope, raw_expression);
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 { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import { regex_non_whitespace_character } from '../../utils/patterns';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
@ -16,6 +17,8 @@ const elements_without_text = new Set([
'video'
]);
const regex_ends_with_svg = /svg$/;
export default class Text extends Node {
type: 'Text';
data: string;
@ -28,7 +31,7 @@ export default class Text extends Node {
}
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)/);
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];
// 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;
}

@ -4,6 +4,8 @@ import Node from './Node';
import { INode } from '../interfaces';
import compiler_warnings from '../../compiler_warnings';
const regex_non_whitespace_characters = /[^ \r\n\f\v\t]/;
export default class AbstractBlock extends Node {
block: Block;
children: INode[];
@ -17,7 +19,7 @@ export default class AbstractBlock extends Node {
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);
}
}

@ -21,6 +21,8 @@ import compiler_errors from '../../compiler_errors';
type Owner = INode;
const regex_contains_term_function_expression = /FunctionExpression/;
export default class Expression {
type: 'Expression' = 'Expression';
component: Component;
@ -72,7 +74,7 @@ export default class Expression {
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;
}
@ -126,6 +128,7 @@ export default class Expression {
deep = node.left.type === 'MemberExpression';
names = extract_names(deep ? get_object(node.left) : node.left);
} else if (node.type === 'UpdateExpression') {
deep = node.argument.type === 'MemberExpression';
names = extract_names(get_object(node.argument));
}
}
@ -147,7 +150,26 @@ export default class Expression {
component.add_reference(node, 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 { Node, Identifier, ArrayPattern } from 'estree';
import { is_head } from './wrappers/shared/is_head';
import { regex_double_quotes } from '../../utils/patterns';
export interface Bindings {
object: Identifier;
@ -415,7 +416,7 @@ export default class Block {
block: ${block},
id: ${this.name || 'create_fragment'}.name,
type: "${this.type}",
source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}",
source: "${this.comment ? this.comment.replace(regex_double_quotes, '\\"') : ''}",
ctx: #ctx
});
return ${block};`

@ -168,7 +168,7 @@ export default class Renderer {
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);
}

@ -12,6 +12,7 @@ import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types
import { flatten } from '../../utils/flatten';
import check_enable_sourcemap from '../utils/check_enable_sourcemap';
import { push_array } from '../../utils/push_array';
import { regex_backslashes } from '../../utils/patterns';
export default function dom(
component: Component,
@ -82,7 +83,7 @@ export default function dom(
}
const uses_slots = component.var_lookup.has('$$slots');
let compute_slots;
let compute_slots: Node[] | undefined;
if (uses_slots) {
compute_slots = b`
const $$slots = @compute_slots(#slots);
@ -395,7 +396,7 @@ export default function dom(
if (has_definition) {
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 => {
const dependencies = Array.from(d.dependencies);
@ -440,7 +441,7 @@ export default function dom(
return b`let ${$name};`;
});
let unknown_props_check;
let unknown_props_check: Node[] | undefined;
if (component.compile_options.dev && !(uses_props || uses_rest)) {
unknown_props_check = b`
const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}];
@ -530,7 +531,7 @@ export default function dom(
constructor(options) {
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});

@ -80,7 +80,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
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);
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';
let remove_old_blocks;
let remove_old_blocks: Node[];
if (this.block.has_outros) {
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 { namespaces } from '../../../../utils/namespaces';
import { boolean_attributes } from '../../../../../shared/boolean_attributes';
import { regex_double_quotes } from '../../../../utils/patterns';
const non_textlike_input_types = new Set([
'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 {
node: Attribute;
parent: ElementWrapper;
@ -115,7 +119,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = /-/.test(element.node.name)
const method = regex_minus_sign.test(element.node.name)
? '@set_custom_element_data'
: name.slice(0, 6) === 'xlink:'
? '@xlink_attr'
@ -126,7 +130,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
const dependencies = this.get_dependencies();
const value = this.get_value(block);
let updater;
let updater: Node[];
const init = this.get_init(block, value);
if (is_legacy_input_type) {
@ -196,7 +200,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
get_init(block: Block, value) {
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);
@ -254,7 +258,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return metadata;
}
get_value(block) {
get_value(block: Block) {
if (this.node.is_true) {
if (this.metadata && boolean_attributes.has(this.metadata.property_name.toLowerCase())) {
return x`true`;
@ -283,7 +287,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
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 rendered = this.render_chunks(block);
@ -313,7 +317,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return `="${value.map(chunk => {
return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"')
? chunk.data.replace(regex_double_quotes, '\\"')
: `\${${chunk.manipulate()}}`;
}).join('')}"`;
}
@ -381,13 +385,14 @@ function should_cache(attribute: AttributeWrapper) {
return attribute.is_src || attribute.node.should_cache();
}
const regex_contains_checked_or_group = /checked|group/;
function is_indirectly_bound_value(attribute: AttributeWrapper) {
const element = attribute.parent;
return attribute.name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.some(
(binding) =>
/checked|group/.test(binding.name)
(binding) => regex_contains_checked_or_group.test(binding.name)
)));
}

@ -299,8 +299,8 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
for (const dep of contextual_dependencies) {
const context = block.bindings.get(dep);
let key;
let name;
let key: string;
let name: string;
if (context) {
key = context.object.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 context = block.bindings.get(name);
let set_store;
let set_store: Node[] | undefined;
if (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);
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>) {
const props: StyleProp[] = [];
let chunks = value.slice();
@ -78,7 +80,7 @@ function optimize_style(value: Array<Text | Expression>) {
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;
const key = key_match[1];
@ -106,6 +108,9 @@ function optimize_style(value: Array<Text | Expression>) {
return props;
}
const regex_important_flag = /\s*!important\s*$/;
const regex_semicolon_or_whitespace = /[;\s]/;
function get_style_value(chunks: Array<Text | Expression>) {
const value: Array<Text | Expression> = [];
@ -151,7 +156,7 @@ function get_style_value(chunks: Array<Text | Expression>) {
} 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);
if (remaining_data) {
@ -172,9 +177,9 @@ function get_style_value(chunks: Array<Text | Expression>) {
let important = false;
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;
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();
}

@ -6,7 +6,7 @@ svg_attributes.forEach(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();
return svg_attribute_lookup.get(name) || name;
}

@ -12,14 +12,14 @@ import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
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 add_to_set from '../../../utils/add_to_set';
import { add_event_handler } from '../shared/add_event_handlers';
import { add_action } from '../shared/add_actions';
import bind_this from '../shared/bind_this';
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 { extract_names } from 'periscopic';
import Action from '../../../nodes/Action';
@ -34,12 +34,16 @@ interface BindingGroup {
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 = [
{
event_names: ['input'],
filter: (node: Element, _name: string) =>
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'],
@ -51,7 +55,8 @@ const events = [
event_names: ['change'],
filter: (node: Element, _name: string) =>
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'],
@ -62,7 +67,7 @@ const events = [
{
event_names: ['elementresize'],
filter: (_node: Element, name: string) =>
dimensions.test(name)
regex_dimensions.test(name)
},
// media events
@ -136,6 +141,8 @@ const events = [
];
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 {
node: Element;
@ -182,7 +189,7 @@ export default class ElementWrapper extends Wrapper {
this.var = {
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);
@ -539,7 +546,7 @@ export default class ElementWrapper extends Wrapper {
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.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.namespace) {
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;
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');
function getOrder (item: OrderedAttribute) {
function getOrder(item: OrderedAttribute) {
if (item instanceof EventHandler) {
return item.node.start;
} else if (item instanceof Binding) {
@ -639,7 +646,7 @@ export default class ElementWrapper extends Wrapper {
// media bindings — awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our
// own hands
let animation_frame;
let animation_frame: Identifier | undefined;
if (binding_group.events[0] === 'timeupdate') {
animation_frame = block.get_unique_name(`${this.var.name}_animationframe`);
block.add_variable(animation_frame);
@ -879,9 +886,7 @@ export default class ElementWrapper extends Wrapper {
}
}
add_transitions(
block: Block
) {
add_transitions(block: Block) {
const { intro, outro } = this.node;
if (!intro && !outro) return;
@ -938,7 +943,7 @@ export default class ElementWrapper extends Wrapper {
const fn = this.renderer.reference(intro.name);
let intro_block;
let intro_block: Node[];
if (outro) {
intro_block = b`
@ -1037,7 +1042,7 @@ export default class ElementWrapper extends Wrapper {
${outro && b`@add_transform(${this.var}, ${rect});`}
`);
let params;
let params: Node | ReturnType<typeof x>;
if (this.node.animation.expression) {
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);
this.node.classes.forEach(class_directive => {
const { expression, name } = class_directive;
let snippet;
let dependencies;
let snippet: Node | string;
let dependencies: Set<string>;
if (expression) {
snippet = expression.manipulate(block);
dependencies = expression.dependencies;
@ -1107,16 +1112,16 @@ export default class ElementWrapper extends Wrapper {
add_styles(block: Block) {
const has_spread = this.node.attributes.some(attr => attr.is_spread);
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);
let cached_snippet;
let cached_snippet: Identifier | undefined;
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);
}
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);
@ -1178,9 +1183,7 @@ export default class ElementWrapper extends Wrapper {
}
}
add_manual_style_scoping(block) {
add_manual_style_scoping(block: Block) {
if (this.node.needs_manual_style_scoping) {
const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`;
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) {
wrappers.forEach(wrapper => {
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
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))
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
.replace(regex_backslashes, '\\\\')
.replace(regex_backticks, '\\`')
.replace(regex_dollar_signs, '\\$');
} else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) {
literal.quasis.push(state.quasi);
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>`.
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
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';
}
}
@ -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>`.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
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';
}
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 { link } from '../../../utils/link';
import { Identifier } from 'estree';
import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = {
AwaitBlock,
@ -64,7 +65,7 @@ export default class FragmentWrapper {
this.nodes = [];
let last_child: Wrapper;
let window_wrapper;
let window_wrapper: Window | undefined;
let i = nodes.length;
while (i--) {
@ -92,7 +93,7 @@ export default class FragmentWrapper {
// *unless* there is no whitespace between this node and its next sibling
if (this.nodes.length === 0) {
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()) {

@ -33,7 +33,7 @@ export default class HeadWrapper extends Wrapper {
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
let nodes;
let nodes: Identifier;
if (this.renderer.options.hydratable && this.fragment.nodes.length) {
nodes = block.get_unique_name('head_nodes');
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 { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
type DetachingOrNull = 'detaching' | null;
function is_else_if(node: ElseBlock) {
return (
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 detaching = parent_node && !is_head(parent_node) ? null : 'detaching';
const detaching: DetachingOrNull = parent_node && !is_head(parent_node) ? null : 'detaching';
if (this.node.else) {
this.branches.forEach(branch => {
@ -275,9 +277,9 @@ export default class IfBlockWrapper extends Wrapper {
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
dynamic: boolean,
{ 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 current_block_type = block.get_unique_name('current_block_type');
@ -414,9 +416,9 @@ export default class IfBlockWrapper extends Wrapper {
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
dynamic: boolean,
{ 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 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_current_block_type_index = has_else
? nodes => nodes
: nodes => b`if (~${current_block_type_index}) { ${nodes} }`;
? (nodes: Node[]) => nodes
: (nodes: Node[]) => b`if (~${current_block_type_index}) { ${nodes} }`;
block.add_variable(current_block_type_index);
block.add_variable(name);
@ -593,9 +595,9 @@ export default class IfBlockWrapper extends Wrapper {
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
dynamic: boolean,
{ name, anchor, if_exists_condition, has_transitions },
detaching
detaching: DetachingOrNull
) {
const branch = this.branches[0];
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 };
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
export default class InlineComponentWrapper extends Wrapper {
var: Identifier;
slots: Map<string, SlotDefinition> = new Map();
@ -137,7 +139,7 @@ export default class InlineComponentWrapper extends Wrapper {
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 uses_spread = !!this.node.attributes.find(a => a.is_spread);
@ -232,7 +234,7 @@ export default class InlineComponentWrapper extends Wrapper {
: null;
const unchanged = dependencies.size === 0;
let change_object;
let change_object: Node | ReturnType<typeof x>;
if (attr.is_spread) {
const value = attr.expression.manipulate(block);
initial_props.push(value);
@ -596,7 +598,7 @@ export default class InlineComponentWrapper extends Wrapper {
this.node.css_custom_properties.forEach((attr) => {
const dependencies = attr.get_dependencies();
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);
const value = attr.get_value(block);
const init = should_cache ? x`${last} = ${value}` : value;

@ -8,7 +8,7 @@ import Element from '../../nodes/Element';
import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import { is_head } from './shared/is_head';
import { Identifier } from 'estree';
import { Identifier, Node } from 'estree';
export default class RawMustacheTagWrapper extends Tag {
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;
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(
block,

@ -9,7 +9,7 @@ import add_to_set from '../../utils/add_to_set';
import get_slot_data from '../../utils/get_slot_data';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
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';
export default class SlotWrapper extends Wrapper {
@ -75,9 +75,9 @@ export default class SlotWrapper extends Wrapper {
block = this.slot_block;
}
let get_slot_changes_fn;
let get_slot_spread_changes_fn;
let get_slot_context_fn;
let get_slot_changes_fn: Identifier | 'null';
let get_slot_spread_changes_fn: Identifier | undefined;
let get_slot_context_fn: Identifier | 'null';
if (this.node.values.size > 0) {
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);
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) {
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 { Identifier } from 'estree';
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class TextWrapper extends Wrapper {
node: Text;
data: string;
@ -27,7 +29,7 @@ export default class TextWrapper extends Wrapper {
use_space() {
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();
}

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

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

@ -1,5 +1,7 @@
import Component from '../../../Component';
import { INode } from '../../../nodes/interfaces';
import { regex_whitespace_characters } from '../../../../utils/patterns';
export default function create_debugging_comment(
node: INode,
@ -13,7 +15,7 @@ export default function create_debugging_comment(
while (source[c - 1] === '{') c -= 1;
}
let d;
let d: number;
if (node.type === 'InlineComponent' || node.type === 'Element' || node.type === 'SlotTemplate') {
if (node.children.length) {
@ -36,5 +38,5 @@ export default function create_debugging_comment(
const start = locate(c);
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 Block from '../../Block';
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[]) {
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();
lets.forEach(l => {
let value;
let value: Identifier;
if (l.names.length > 1) {
// more than one, probably destructuring
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]
? grouped[g]
.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`;
}

@ -1,3 +1,5 @@
export function is_head(node) {
return node && node.type === 'MemberExpression' && node.object.name === '@_document' && node.property.name === 'head';
import { Node } from 'estree';
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 fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
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';
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}`);
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}`;
});
@ -178,7 +181,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
const value_attribute = node.attributes.find(({ name }) => name === 'value');
if (value_attribute) {
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');
}
}
@ -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/syntax.html#element-restrictions
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');
}
}

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer';
import Head from '../../nodes/Head';
import { x } from 'code-red';
import { Node } from 'estree';
export default function(node: Head, renderer: Renderer, options: RenderOptions) {
const head_options = {
@ -11,6 +12,12 @@ export default function(node: Head, renderer: Renderer, options: RenderOptions)
renderer.push();
renderer.render(node.children, head_options);
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 Expression from '../../../nodes/shared/Expression';
import { Expression as ESTreeExpression } from 'estree';
import { regex_double_quotes } from '../../../../utils/patterns';
export function get_class_attribute_value(attribute: Attribute): ESTreeExpression {
// handle special case — `class={possiblyUndefined}` with scoped CSS
@ -21,7 +22,7 @@ export function get_attribute_value(attribute: Attribute): ESTreeExpression {
return attribute.chunks
.map((chunk) => {
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)`;
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);

@ -1,6 +1,7 @@
import { INode } from '../../../nodes/interfaces';
import { trim_end, trim_start } from '../../../../utils/trim';
import { link } from '../../../../utils/link';
import { regex_starts_with_whitespace } from '../../../../utils/patterns';
// similar logic from `compile/render_dom/wrappers/Fragment`
// 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) {
const should_trim = next
? next.type === 'Text' &&
/^\s/.test(next.data) &&
regex_starts_with_whitespace.test(next.data) &&
trimmable_at(child, next)
: !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) {
if (!filename) return null;
@ -12,12 +19,12 @@ export default function get_name_from_filename(filename: string) {
}
const base = parts.pop()
.replace(/%/g, 'u')
.replace(/\.[^.]+$/, '')
.replace(/[^a-zA-Z_$0-9]+/g, '_')
.replace(/^_/, '')
.replace(/_$/, '')
.replace(/^(\d)/, '_$1');
.replace(regex_percentage_characters, 'u')
.replace(regex_file_ending, '')
.replace(regex_repeated_invalid_variable_identifier_characters, '_')
.replace(regex_starts_with_underscore, '')
.replace(regex_ends_with_underscore, '')
.replace(regex_starts_with_digit, '_$1');
if (!base) {
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
const regex_return_characters = /\r/g;
export default function hash(str: string): string {
str = str.replace(/\r/g, '');
str = str.replace(regex_return_characters, '');
let hash = 5381;
let i = str.length;

@ -19,10 +19,14 @@ const escaped = {
'>': '&gt;'
};
const regex_html_characters_to_escape = /["'&<>]/g;
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) {
return str.replace(/(\${|`|\\)/g, '\\$1');
return str.replace(regex_template_characters_to_escape, '\\$1');
}

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

@ -10,6 +10,7 @@ import {
import { parse_expression_at } from '../acorn';
import { Pattern } from 'estree';
import parser_errors from '../errors';
import { regex_not_newline_characters } from '../../utils/patterns';
export default function read_context(
parser: Parser
@ -65,7 +66,7 @@ export default function read_context(
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// 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(' ');
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 { Parser } from '../index';
import { Node } from 'estree';
import { whitespace } from '../../utils/patterns';
import { regex_whitespace } from '../../utils/patterns';
import parser_errors from '../errors';
export default function read_expression(parser: Parser): Node {
@ -20,7 +20,7 @@ export default function read_expression(parser: Parser): Node {
if (char === ')') {
num_parens -= 1;
} else if (!whitespace.test(char)) {
} else if (!regex_whitespace.test(char)) {
parser.error(parser_errors.unexpected_token(')'), index);
}

@ -3,6 +3,9 @@ import { Parser } from '../index';
import { Script } from '../../interfaces';
import { Node, Program } from 'estree';
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 {
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 {
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) {
parser.error(parser_errors.unclosed_script);
}
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + data;
parser.read(/<\/script\s*>/);
const source = parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_closing_script_tag);
let ast: Program;

@ -5,10 +5,12 @@ import { Node } from 'estree';
import { Style } from '../../interfaces';
import parser_errors from '../errors';
const regex_closing_style_tag = /<\/style\s*>/;
export default function read_style(parser: Parser, start: number, attributes: Node[]): Style {
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) {
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;
return {

@ -1,7 +1,7 @@
import read_context from '../read/context';
import read_expression from '../read/expression';
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 { to_string } from '../utils/node';
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) {
const start = parser.index;
parser.index += 1;
@ -87,8 +89,8 @@ export default function mustache(parser: Parser) {
// strip leading/trailing whitespace as necessary
const char_before = parser.template[block.start - 1];
const char_after = parser.template[parser.index];
const trim_before = !char_before || whitespace.test(char_before);
const trim_after = !char_after || whitespace.test(char_after);
const trim_before = !char_before || regex_whitespace.test(char_before);
const trim_after = !char_after || regex_whitespace.test(char_after);
trim_whitespace(block, trim_before, trim_after);
@ -106,8 +108,8 @@ export default function mustache(parser: Parser) {
const block = parser.current();
if (block.type !== 'IfBlock') {
parser.error(
parser.stack.some(block => block.type === 'IfBlock')
? parser_errors.invalid_elseif_placement_unclosed_block(to_string(block))
parser.stack.some(block => block.type === 'IfBlock')
? parser_errors.invalid_elseif_placement_unclosed_block(to_string(block))
: parser_errors.invalid_elseif_placement_outside_if
);
}
@ -141,8 +143,8 @@ export default function mustache(parser: Parser) {
const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error(
parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
? parser_errors.invalid_else_placement_unclosed_block(to_string(block))
parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
? parser_errors.invalid_else_placement_unclosed_block(to_string(block))
: parser_errors.invalid_else_placement_outside_if
);
}
@ -167,15 +169,15 @@ export default function mustache(parser: Parser) {
if (block.type !== 'PendingBlock') {
parser.error(
parser.stack.some(block => block.type === 'PendingBlock')
? parser_errors.invalid_then_placement_unclosed_block(to_string(block))
: parser_errors.invalid_then_placement_without_await
? parser_errors.invalid_then_placement_unclosed_block(to_string(block))
: parser_errors.invalid_then_placement_without_await
);
}
} else {
if (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_without_await
? parser_errors.invalid_catch_placement_unclosed_block(to_string(block))
: 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');
if (await_block_shorthand) {
if (parser.match_regex(/\s*}/)) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
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');
if (await_block_catch_shorthand) {
if (parser.match_regex(/\s*}/)) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
@ -350,7 +352,7 @@ export default function mustache(parser: Parser) {
let identifiers;
// Implies {@debug} which indicates "debug all"
if (parser.read(/\s*}/)) {
if (parser.read(regex_whitespace_with_closing_curly_brace)) {
identifiers = [];
} else {
const expression = read_expression(parser);

@ -53,13 +53,17 @@ function parent_is_head(stack) {
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) {
const start = parser.index++;
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(/-->/);
const data = parser.read_until(regex_closing_comment);
parser.eat('-->', true, parser_errors.unclosed_comment);
parser.current().children.push({
@ -104,7 +108,7 @@ export default function tag(parser: Parser) {
const type = meta_tags.has(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 === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
@ -218,11 +222,10 @@ export default function tag(parser: Parser) {
// special case
element.children = read_sequence(
parser,
() =>
/^<\/textarea(\s[^>]*)?>/i.test(parser.template.slice(parser.index)),
() => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
'inside <textarea>'
);
parser.read(/^<\/textarea(\s[^>]*)?>/i);
parser.read(regex_closing_textarea_tag);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
// 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) {
const start = parser.index;
@ -266,7 +271,7 @@ function read_tag_name(parser: Parser) {
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;
@ -295,6 +300,10 @@ function use_name_as_expression(type:string, name:string):boolean {
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>) {
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(/[\s=\/>"']/);
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
let end = parser.index;
@ -369,7 +377,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
parser.allow_whitespace();
value = read_attribute_value(parser);
end = parser.index;
} else if (parser.match_regex(/["']/)) {
} else if (parser.match_regex(regex_quote_characters)) {
parser.error(parser_errors.unexpected_token('='), parser.index);
}
@ -396,6 +404,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
end,
type,
name: directive_name,
modifiers,
value
};
}

@ -4,6 +4,7 @@ import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_off
import { decode_map } from './decode_sourcemap';
import { replace_in_code, slice_source } from './replace_in_code';
import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
import { regex_whitespaces } from '../utils/patterns';
export * from './types';
@ -13,8 +14,10 @@ interface SourceUpdate {
dependencies?: string[];
}
const regex_filepath_separator = /[/\\]/;
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);
}
const regex_quoted_value = /^['"](.*)['"]$/;
function parse_tag_attributes(str: string) {
// note: won't work with attribute values containing spaces.
return str
.split(/\s+/)
.split(regex_whitespaces)
.filter(Boolean)
.reduce((attrs, attr) => {
const i = attr.indexOf('=');
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 };
}, {});
}
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.
*/
@ -143,10 +152,7 @@ async function process_tag(
source: Source
): Promise<SourceUpdate> {
const { filename, source: markup } = source;
const tag_regex =
tag_name === 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const tag_regex = tag_name === 'style' ? regex_style_tags : regex_script_tags;
const dependencies: string[] = [];
@ -190,7 +196,7 @@ async function process_markup(process: MarkupPreprocessor, source: Source) {
string: processed.code,
map: processed.map
? // TODO: can we use decode_sourcemap?
typeof processed.map === 'string'
typeof processed.map === 'string'
? JSON.parse(processed.map)
: processed.map
: undefined,

@ -1,11 +1,12 @@
import { TemplateNode } from '../interfaces';
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[] {
const match = pattern.exec(text);
return match ? match[1].split(/[^\S]/).map(x => x.trim()).filter(Boolean) : [];
const match = regex_svelte_ignore.exec(text);
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[] {

@ -1,5 +1,7 @@
const regex_tabs = /^\t+/;
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(

@ -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];
}
const regex_line_token = /([^\d\w\s]|\s+)/g;
export class MappedCode {
string: string;
map: DecodedSourceMap;
@ -195,7 +197,7 @@ export class MappedCode {
const line_list = source.split('\n');
for (let line = 0; line < line_list.length; line++) {
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++) {
if (token_list[token] == '') continue;
map.mappings[line].push([column, 0, offset.line + line, column]);
@ -245,7 +247,7 @@ export function combine_sourcemaps(
if (!map.file) delete map.file; // skip optional field `file`
// When source maps are combined and the leading map is empty, sources is not set.
// Add the filename to the empty array in this case.
// Add the filename to the empty array in this case.
// Further improvements to remapping may help address this as well https://github.com/ampproject/remapping/issues/116
if (!map.sources.length) map.sources = [filename];
@ -289,6 +291,8 @@ export function apply_preprocessor_sourcemap(filename: string, svelte_map: Sourc
return result_map as SourceMap;
}
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code
export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void {
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) => {
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) {
// sourceMappingURL is data URL
if (processed.map) {

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

@ -1,6 +1,24 @@
export const whitespace = /[ \t\r\n]/;
export const start_whitespace = /^[ \t\r\n]*/;
export const end_whitespace = /[ \t\r\n]*$/;
export const start_newline = /^\r?\n/;
export const regex_whitespace = /\s/;
export const regex_whitespaces = /\s+/;
export const regex_starts_with_whitespace = /^\s/;
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) {
return str.replace(start_whitespace, '');
return str.replace(regex_starts_with_whitespaces, '');
}
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';
function instance($$self, $$props, $$invalidate) {
const world3 = 'world';
let world3 = 'world';
function foo() {
$$invalidate(0, world3 = 'svelte');

@ -1,7 +1,7 @@
<script>
let world1 = 'world';
let world2 = 'world';
const world3 = 'world';
let world3 = 'world';
function foo() {
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,
"type": "StyleDirective",
"name": "color",
"modifiers": [],
"value": true
}
],
@ -22,4 +23,4 @@
}
]
}
}
}

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

@ -15,6 +15,7 @@
"end": 26,
"type": "StyleDirective",
"name": "color",
"modifiers": [],
"value": [
{
"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);
try {
assert.htmlEqual(
(compileOptions.hydratable
? assert.htmlEqualWithComments
: assert.htmlEqual)(
head,
fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8')
);

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

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