fix: vite
This commit is contained in:
823
node_modules/svelte/src/compiler/phases/1-parse/state/element.js
generated
vendored
Normal file
823
node_modules/svelte/src/compiler/phases/1-parse/state/element.js
generated
vendored
Normal file
@@ -0,0 +1,823 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { is_void } from '../../../../utils.js';
|
||||
import read_expression from '../read/expression.js';
|
||||
import { read_script } from '../read/script.js';
|
||||
import read_style from '../read/style.js';
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { create_fragment } from '../utils/create.js';
|
||||
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
|
||||
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
|
||||
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
|
||||
import { list } from '../../../utils/string.js';
|
||||
import { regex_whitespace } from '../../patterns.js';
|
||||
|
||||
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
|
||||
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
|
||||
const regex_closing_comment = /-->/;
|
||||
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
|
||||
const regex_token_ending_character = /[\s=/>"']/;
|
||||
const regex_starts_with_quote_characters = /^["']/;
|
||||
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
|
||||
const regex_valid_element_name =
|
||||
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
|
||||
export const regex_valid_component_name =
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
|
||||
// (must start with uppercase letter if no dots, can contain dots)
|
||||
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
|
||||
|
||||
/** @type {Map<string, AST.ElementLike['type']>} */
|
||||
const root_only_meta_tags = new Map([
|
||||
['svelte:head', 'SvelteHead'],
|
||||
['svelte:options', 'SvelteOptions'],
|
||||
['svelte:window', 'SvelteWindow'],
|
||||
['svelte:document', 'SvelteDocument'],
|
||||
['svelte:body', 'SvelteBody']
|
||||
]);
|
||||
|
||||
/** @type {Map<string, AST.ElementLike['type']>} */
|
||||
const meta_tags = new Map([
|
||||
...root_only_meta_tags,
|
||||
['svelte:element', 'SvelteElement'],
|
||||
['svelte:component', 'SvelteComponent'],
|
||||
['svelte:self', 'SvelteSelf'],
|
||||
['svelte:fragment', 'SvelteFragment'],
|
||||
['svelte:boundary', 'SvelteBoundary']
|
||||
]);
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function element(parser) {
|
||||
const start = parser.index++;
|
||||
|
||||
let parent = parser.current();
|
||||
|
||||
if (parser.eat('!--')) {
|
||||
const data = parser.read_until(regex_closing_comment);
|
||||
parser.eat('-->', true);
|
||||
|
||||
parser.append({
|
||||
type: 'Comment',
|
||||
start,
|
||||
end: parser.index,
|
||||
data
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const is_closing_tag = parser.eat('/');
|
||||
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
|
||||
|
||||
if (is_closing_tag) {
|
||||
parser.allow_whitespace();
|
||||
parser.eat('>', true);
|
||||
|
||||
if (is_void(name)) {
|
||||
e.void_element_invalid_content(start);
|
||||
}
|
||||
|
||||
// close any elements that don't have their own closing tags, e.g. <div><p></div>
|
||||
while (/** @type {AST.RegularElement} */ (parent).name !== name) {
|
||||
if (parser.loose) {
|
||||
// If the previous element did interpret the next opening tag as an attribute, backtrack
|
||||
if (is_element_node(parent)) {
|
||||
const last = parent.attributes.at(-1);
|
||||
if (last?.type === 'Attribute' && last.name === `<${name}`) {
|
||||
parser.index = last.start;
|
||||
parent.attributes.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.type !== 'RegularElement' && !parser.loose) {
|
||||
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
|
||||
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
|
||||
} else {
|
||||
e.element_invalid_closing_tag(start, name);
|
||||
}
|
||||
}
|
||||
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
|
||||
parent = parser.current();
|
||||
}
|
||||
|
||||
parent.end = parser.index;
|
||||
parser.pop();
|
||||
|
||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||
parser.last_auto_closed_tag = undefined;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
|
||||
const bounds = { start: start + 1, end: start + 1 + name.length };
|
||||
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
|
||||
}
|
||||
|
||||
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
|
||||
// <div. -> in the middle of typing -> allow in loose mode
|
||||
if (!parser.loose || !name.endsWith('.')) {
|
||||
const bounds = { start: start + 1, end: start + 1 + name.length };
|
||||
e.tag_invalid_name(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (root_only_meta_tags.has(name)) {
|
||||
if (name in parser.meta_tags) {
|
||||
e.svelte_meta_duplicate(start, name);
|
||||
}
|
||||
|
||||
if (parent.type !== 'Root') {
|
||||
e.svelte_meta_invalid_placement(start, name);
|
||||
}
|
||||
|
||||
parser.meta_tags[name] = true;
|
||||
}
|
||||
|
||||
const type = meta_tags.has(name)
|
||||
? meta_tags.get(name)
|
||||
: regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.'))
|
||||
? 'Component'
|
||||
: name === 'title' && parent_is_head(parser.stack)
|
||||
? 'TitleElement'
|
||||
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
|
||||
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
|
||||
? 'SlotElement'
|
||||
: 'RegularElement';
|
||||
|
||||
/** @type {AST.ElementLike} */
|
||||
const element =
|
||||
type === 'RegularElement'
|
||||
? {
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
svg: false,
|
||||
mathml: false,
|
||||
scoped: false,
|
||||
has_spread: false,
|
||||
path: []
|
||||
}
|
||||
}
|
||||
: /** @type {AST.ElementLike} */ ({
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
// unpopulated at first, differs between types
|
||||
}
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: name,
|
||||
depth: parser.stack.length
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const unique_names = [];
|
||||
|
||||
const current = parser.current();
|
||||
const is_top_level_script_or_style =
|
||||
(name === 'script' || name === 'style') && current.type === 'Root';
|
||||
|
||||
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
|
||||
|
||||
let attribute;
|
||||
while ((attribute = read(parser))) {
|
||||
// animate and transition can only be specified once per element so no need
|
||||
// to check here, use can be used multiple times, same for the on directive
|
||||
// finally let already has error handling in case of duplicate variable names
|
||||
if (
|
||||
attribute.type === 'Attribute' ||
|
||||
attribute.type === 'BindDirective' ||
|
||||
attribute.type === 'StyleDirective' ||
|
||||
attribute.type === 'ClassDirective'
|
||||
) {
|
||||
// `bind:attribute` and `attribute` are just the same but `class:attribute`,
|
||||
// `style:attribute` and `attribute` are different and should be allowed together
|
||||
// so we concatenate the type while normalizing the type for BindDirective
|
||||
const type = attribute.type === 'BindDirective' ? 'Attribute' : attribute.type;
|
||||
if (unique_names.includes(type + attribute.name)) {
|
||||
e.attribute_duplicate(attribute);
|
||||
// <svelte:element bind:this this=..> is allowed
|
||||
} else if (attribute.name !== 'this') {
|
||||
unique_names.push(type + attribute.name);
|
||||
}
|
||||
}
|
||||
|
||||
element.attributes.push(attribute);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (element.type === 'SvelteComponent') {
|
||||
const index = element.attributes.findIndex(
|
||||
/** @param {any} attr */
|
||||
(attr) => attr.type === 'Attribute' && attr.name === 'this'
|
||||
);
|
||||
if (index === -1) {
|
||||
e.svelte_component_missing_this(start);
|
||||
}
|
||||
|
||||
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
|
||||
if (!is_expression_attribute(definition)) {
|
||||
e.svelte_component_invalid_this(definition.start);
|
||||
}
|
||||
|
||||
element.expression = get_attribute_expression(definition);
|
||||
}
|
||||
|
||||
if (element.type === 'SvelteElement') {
|
||||
const index = element.attributes.findIndex(
|
||||
/** @param {any} attr */
|
||||
(attr) => attr.type === 'Attribute' && attr.name === 'this'
|
||||
);
|
||||
if (index === -1) {
|
||||
e.svelte_element_missing_this(start);
|
||||
}
|
||||
|
||||
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
|
||||
|
||||
if (definition.value === true) {
|
||||
e.svelte_element_missing_this(definition);
|
||||
}
|
||||
|
||||
if (!is_expression_attribute(definition)) {
|
||||
w.svelte_element_invalid_this(definition);
|
||||
|
||||
// note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in `<h>`.
|
||||
// it would be much better to just error here, but we are preserving the existing buggy
|
||||
// Svelte 4 behaviour out of an overabundance of caution regarding breaking changes.
|
||||
// TODO in 6.0, error
|
||||
const chunk = /** @type {Array<AST.ExpressionTag | AST.Text>} */ (definition.value)[0];
|
||||
element.tag =
|
||||
chunk.type === 'Text'
|
||||
? {
|
||||
type: 'Literal',
|
||||
value: chunk.data,
|
||||
raw: `'${chunk.raw}'`,
|
||||
start: chunk.start,
|
||||
end: chunk.end
|
||||
}
|
||||
: chunk.expression;
|
||||
} else {
|
||||
element.tag = get_attribute_expression(definition);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_top_level_script_or_style) {
|
||||
parser.eat('>', true);
|
||||
|
||||
/** @type {AST.Comment | null} */
|
||||
let prev_comment = null;
|
||||
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
|
||||
const node = current.fragment.nodes[i];
|
||||
|
||||
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.type === 'Comment') {
|
||||
prev_comment = node;
|
||||
break;
|
||||
} else if (node.type !== 'Text' || node.data.trim()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'script') {
|
||||
const content = read_script(parser, start, element.attributes);
|
||||
if (prev_comment) {
|
||||
// We take advantage of the fact that the root will never have leadingComments set,
|
||||
// and set the previous comment to it so that the warning mechanism can later
|
||||
// inspect the root and see if there was a html comment before it silencing specific warnings.
|
||||
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
|
||||
}
|
||||
|
||||
if (content.context === 'module') {
|
||||
if (current.module) e.script_duplicate(start);
|
||||
current.module = content;
|
||||
} else {
|
||||
if (current.instance) e.script_duplicate(start);
|
||||
current.instance = content;
|
||||
}
|
||||
} else {
|
||||
const content = read_style(parser, start, element.attributes);
|
||||
content.content.comment = prev_comment;
|
||||
|
||||
if (current.css) e.style_duplicate(start);
|
||||
current.css = content;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parser.append(element);
|
||||
|
||||
const self_closing = parser.eat('/') || is_void(name);
|
||||
const closed = parser.eat('>', true, false);
|
||||
|
||||
// Loose parsing mode
|
||||
if (!closed) {
|
||||
// We may have eaten an opening `<` of the next element and treated it as an attribute...
|
||||
const last = element.attributes.at(-1);
|
||||
if (last?.type === 'Attribute' && last.name === '<') {
|
||||
parser.index = last.start;
|
||||
element.attributes.pop();
|
||||
} else {
|
||||
// ... or we may have eaten part of a following block ...
|
||||
const prev_1 = parser.template[parser.index - 1];
|
||||
const prev_2 = parser.template[parser.index - 2];
|
||||
const current = parser.template[parser.index];
|
||||
if (prev_2 === '{' && prev_1 === '/') {
|
||||
parser.index -= 2;
|
||||
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
|
||||
parser.index -= 1;
|
||||
} else {
|
||||
// ... or we're followed by whitespace, for example near the end of the template,
|
||||
// which we want to take in so that language tools has more room to work with
|
||||
parser.allow_whitespace();
|
||||
if (parser.index === parser.template.length) {
|
||||
while (
|
||||
parser.index < parser.template_untrimmed.length &&
|
||||
regex_whitespace.test(parser.template_untrimmed[parser.index])
|
||||
) {
|
||||
parser.index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self_closing || !closed) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (name === 'textarea') {
|
||||
// special case
|
||||
element.fragment.nodes = read_sequence(
|
||||
parser,
|
||||
() => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
|
||||
'inside <textarea>'
|
||||
);
|
||||
parser.read(regex_closing_textarea_tag);
|
||||
element.end = parser.index;
|
||||
} else if (name === 'script' || name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const data = parser.read_until(new RegExp(`</${name}>`));
|
||||
const end = parser.index;
|
||||
|
||||
/** @type {AST.Text} */
|
||||
const node = {
|
||||
start,
|
||||
end,
|
||||
type: 'Text',
|
||||
data,
|
||||
raw: data
|
||||
};
|
||||
|
||||
element.fragment.nodes.push(node);
|
||||
parser.eat(`</${name}>`, true);
|
||||
element.end = parser.index;
|
||||
} else {
|
||||
parser.stack.push(element);
|
||||
parser.fragments.push(element.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AST.TemplateNode[]} stack */
|
||||
function parent_is_head(stack) {
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
const { type } = stack[i];
|
||||
if (type === 'SvelteHead') return true;
|
||||
if (type === 'RegularElement' || type === 'Component') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {AST.TemplateNode[]} stack */
|
||||
function parent_is_shadowroot_template(stack) {
|
||||
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
if (
|
||||
stack[i].type === 'RegularElement' &&
|
||||
/** @type {AST.RegularElement} */ (stack[i]).attributes.some(
|
||||
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | null}
|
||||
*/
|
||||
function read_static_attribute(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
const name = parser.read_until(regex_token_ending_character);
|
||||
if (!name) return null;
|
||||
|
||||
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
|
||||
let value = true;
|
||||
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
let raw = parser.match_regex(regex_attribute_value);
|
||||
if (!raw) {
|
||||
e.expected_attribute_value(parser.index);
|
||||
}
|
||||
|
||||
parser.index += raw.length;
|
||||
|
||||
const quoted = raw[0] === '"' || raw[0] === "'";
|
||||
if (quoted) {
|
||||
raw = raw.slice(1, -1);
|
||||
}
|
||||
|
||||
value = [
|
||||
{
|
||||
start: parser.index - raw.length - (quoted ? 1 : 0),
|
||||
end: quoted ? parser.index - 1 : parser.index,
|
||||
type: 'Text',
|
||||
raw: raw,
|
||||
data: decode_character_references(raw, true)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (parser.match_regex(regex_starts_with_quote_characters)) {
|
||||
e.expected_token(parser.index, '=');
|
||||
}
|
||||
|
||||
return create_attribute(name, start, parser.index, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
|
||||
*/
|
||||
function read_attribute(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('...')) {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.SpreadAttribute} */
|
||||
const spread = {
|
||||
type: 'SpreadAttribute',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
return spread;
|
||||
} else {
|
||||
const value_start = parser.index;
|
||||
let name = parser.read_identifier();
|
||||
|
||||
if (name === null) {
|
||||
if (
|
||||
parser.loose &&
|
||||
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
|
||||
) {
|
||||
// We're likely in an unclosed opening tag and did read part of a block.
|
||||
// Return null to not crash the parser so it can continue with closing the tag.
|
||||
return null;
|
||||
} else if (parser.loose && parser.match('}')) {
|
||||
// Likely in the middle of typing, just created the shorthand
|
||||
name = '';
|
||||
} else {
|
||||
e.attribute_empty_shorthand(start);
|
||||
}
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const expression = {
|
||||
type: 'ExpressionTag',
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
expression: {
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'Identifier',
|
||||
name
|
||||
},
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
return create_attribute(name, start, parser.index, expression);
|
||||
}
|
||||
}
|
||||
|
||||
const name = parser.read_until(regex_token_ending_character);
|
||||
if (!name) return null;
|
||||
|
||||
let end = parser.index;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const colon_index = name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
|
||||
|
||||
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
|
||||
let value = true;
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
|
||||
const char_start = parser.index;
|
||||
parser.index++; // consume '/'
|
||||
value = [
|
||||
{
|
||||
start: char_start,
|
||||
end: char_start + 1,
|
||||
type: 'Text',
|
||||
raw: '/',
|
||||
data: '/'
|
||||
}
|
||||
];
|
||||
end = parser.index;
|
||||
} else {
|
||||
value = read_attribute_value(parser);
|
||||
end = parser.index;
|
||||
}
|
||||
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
|
||||
e.expected_token(parser.index, '=');
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
|
||||
|
||||
if (directive_name === '') {
|
||||
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
|
||||
}
|
||||
|
||||
if (type === 'StyleDirective') {
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
modifiers: /** @type {Array<'important'>} */ (modifiers),
|
||||
value,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
|
||||
|
||||
/** @type {Expression | null} */
|
||||
let expression = null;
|
||||
|
||||
if (first_value) {
|
||||
const attribute_contains_text =
|
||||
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
|
||||
if (attribute_contains_text) {
|
||||
e.directive_invalid_value(/** @type {number} */ (first_value.start));
|
||||
} else {
|
||||
// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
|
||||
// which means stringified value, which isn't allowed for some directives?
|
||||
expression = first_value.expression;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {AST.Directive} */
|
||||
const directive = {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
|
||||
directive.modifiers = modifiers;
|
||||
|
||||
if (directive.type === 'TransitionDirective') {
|
||||
const direction = name.slice(0, colon_index);
|
||||
directive.intro = direction === 'in' || direction === 'transition';
|
||||
directive.outro = direction === 'out' || direction === 'transition';
|
||||
}
|
||||
|
||||
// Directive name is expression, e.g. <p class:isRed />
|
||||
if (
|
||||
(directive.type === 'BindDirective' || directive.type === 'ClassDirective') &&
|
||||
!directive.expression
|
||||
) {
|
||||
directive.expression = /** @type {any} */ ({
|
||||
start: start + colon_index + 1,
|
||||
end,
|
||||
type: 'Identifier',
|
||||
name: directive.name
|
||||
});
|
||||
}
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
return create_attribute(name, start, end, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {any}
|
||||
*/
|
||||
function get_directive_type(name) {
|
||||
if (name === 'use') return 'UseDirective';
|
||||
if (name === 'animate') return 'AnimateDirective';
|
||||
if (name === 'bind') return 'BindDirective';
|
||||
if (name === 'class') return 'ClassDirective';
|
||||
if (name === 'style') return 'StyleDirective';
|
||||
if (name === 'on') return 'OnDirective';
|
||||
if (name === 'let') return 'LetDirective';
|
||||
if (name === 'in' || name === 'out' || name === 'transition') return 'TransitionDirective';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
|
||||
*/
|
||||
function read_attribute_value(parser) {
|
||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||
if (quote_mark && parser.eat(quote_mark)) {
|
||||
return [
|
||||
{
|
||||
start: parser.index - 1,
|
||||
end: parser.index - 1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/** @type {Array<AST.ExpressionTag | AST.Text>} */
|
||||
let value;
|
||||
try {
|
||||
value = read_sequence(
|
||||
parser,
|
||||
() => {
|
||||
// handle common case of quote marks existing outside of regex for performance reasons
|
||||
if (quote_mark) return parser.match(quote_mark);
|
||||
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
|
||||
},
|
||||
'in attribute value'
|
||||
);
|
||||
} catch (/** @type {any} */ error) {
|
||||
if (error.code === 'js_parse_error') {
|
||||
// if the attribute value didn't close + self-closing tag
|
||||
// eg: `<Component test={{a:1} />`
|
||||
// acorn may throw a `Unterminated regular expression` because of `/>`
|
||||
const pos = error.position?.[0];
|
||||
if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
|
||||
parser.index = pos;
|
||||
e.expected_token(pos, quote_mark || '}');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (value.length === 0 && !quote_mark) {
|
||||
e.expected_attribute_value(parser.index);
|
||||
}
|
||||
|
||||
if (quote_mark) parser.index += 1;
|
||||
|
||||
if (quote_mark || value.length > 1 || value[0].type === 'Text') {
|
||||
return value;
|
||||
} else {
|
||||
return value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {() => boolean} done
|
||||
* @param {string} location
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function read_sequence(parser, done, location) {
|
||||
/** @type {AST.Text} */
|
||||
let current_chunk = {
|
||||
start: parser.index,
|
||||
end: -1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
};
|
||||
|
||||
/** @type {Array<AST.Text | AST.ExpressionTag>} */
|
||||
const chunks = [];
|
||||
|
||||
/** @param {number} end */
|
||||
function flush(end) {
|
||||
if (current_chunk.raw) {
|
||||
current_chunk.data = decode_character_references(current_chunk.raw, true);
|
||||
current_chunk.end = end;
|
||||
chunks.push(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const index = parser.index;
|
||||
|
||||
if (done()) {
|
||||
flush(parser.index);
|
||||
return chunks;
|
||||
} else if (parser.eat('{')) {
|
||||
if (parser.match('#')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('#');
|
||||
const name = parser.read_until(/[^a-z]/);
|
||||
e.block_invalid_placement(index, name, location);
|
||||
} else if (parser.match('@')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('@');
|
||||
const name = parser.read_until(/[^a-z]/);
|
||||
e.tag_invalid_placement(index, name, location);
|
||||
}
|
||||
|
||||
flush(parser.index - 1);
|
||||
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const chunk = {
|
||||
type: 'ExpressionTag',
|
||||
start: index,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: create_expression_metadata()
|
||||
}
|
||||
};
|
||||
|
||||
chunks.push(chunk);
|
||||
|
||||
current_chunk = {
|
||||
start: parser.index,
|
||||
end: -1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
};
|
||||
} else {
|
||||
current_chunk.raw += parser.template[parser.index++];
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.loose) {
|
||||
return chunks;
|
||||
} else {
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user