[chore]: store regexp as variable instead of defining it inline (#7716)

* store regexp as variable instead of defining it inline

* fix naming of `regex_quoted_value`

* some more variables

* optimize `.replace() calls

* restore formatting changes

* optimize `parser.*` calls

* small refactor

* optimize `.test() calls

* rename some variables

* fix tests

* rename pattern variables

* extract common regexes into `patters.ts`

* rename variables to use snake_case

* fix trim
pull/7942/head
Hofer Ivan 2 years ago committed by GitHub
parent 26a428972b
commit 0eba57113b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;
@ -795,7 +799,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 +922,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 +969,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 +1093,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 +1464,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 +1479,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 +1511,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);
}

@ -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';
@ -203,6 +203,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;
@ -253,7 +257,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, '');
}
}
@ -398,7 +402,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));
}
@ -464,7 +468,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));
}
@ -729,7 +733,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;
});
@ -861,7 +865,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)) {
@ -988,7 +992,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) {
@ -1002,12 +1006,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];
@ -1015,10 +1021,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,10 +74,10 @@ 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);
default:
throw new Error(`Not implemented: ${node.type}`);
}
@ -165,7 +166,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) {

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

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

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

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

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

@ -12,7 +12,7 @@ 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';
@ -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);
@ -547,7 +554,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
@ -561,7 +568,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) {
@ -1100,7 +1107,7 @@ export default class ElementWrapper extends Wrapper {
const snippet = expression.manipulate(block);
let cached_snippet;
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);
}
@ -1138,6 +1145,9 @@ 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) {
@ -1155,9 +1165,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));
@ -1192,7 +1202,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';
}
}
@ -1204,7 +1214,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,
@ -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()) {

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

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

@ -12,6 +12,8 @@ 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;
@ -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,
@ -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, ' ');
}

@ -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) {
@ -173,7 +173,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');
}
}
@ -188,7 +188,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');
}
}

@ -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;
@ -286,6 +291,10 @@ function read_tag_name(parser: Parser) {
return name;
}
// 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;
@ -344,8 +353,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;
@ -360,7 +368,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);
}

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

Loading…
Cancel
Save