the template is read by the parser: one call per component, the compiler's shape put on the tree after it, the scope tables cut per expression for the analysis, the parser's errors mapped to ours; phase 1's own reader goes

goodbye-acorn
Nic 4 hours ago
parent 57c7de58a5
commit a7f4c5c5bb

@ -5,8 +5,7 @@
/** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { parse, regex_valid_component_name } from '../phases/1-parse/index.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, UNKNOWN_FILENAME } from '../state.js';

@ -0,0 +1,80 @@
/** The template language, as the parser reads it: the delimiters, the elements and their fields, the directives and the blocks. */
export const grammar = `
host svelte
document Root css=style js=list options=null comments=comments module?=script:module { instance?=script { fragment=fragment } }
delimiters { }
attributes expressions shorthand
sigils open=# branch=: close=/ tag=@
autoclose
trim
void area base br col command embed hr img input keygen link meta param source track wbr
fragment Fragment nodes scope
elements name=name attributes=attributes children=fragment
text Text data=data raw=raw
comment Comment data=data
element svelte:element SvelteElement this=tag:text
element svelte:component SvelteComponent this=expression
element svelte:self SvelteSelf
element svelte:window SvelteWindow root once
element svelte:document SvelteDocument root once
element svelte:body SvelteBody root once
element svelte:head SvelteHead root once
element svelte:options SvelteOptions root once
element svelte:fragment SvelteFragment
element svelte:boundary SvelteBoundary
element title TitleElement inside svelte:head
element slot SlotElement outside shadowrootmode
element textarea RegularElement rcdata
element script RegularElement raw
element style RegularElement raw
element component-name Component
element * RegularElement
script script module=context:module module=module typescript=lang:ts
style style
directives arg=: modifier=| field:arg=name field:modifiers=modifiers
directive bind BindDirective expression?name unique:attribute
directive on OnDirective expression?
directive use UseDirective expression?
directive class ClassDirective expression?name unique
directive style StyleDirective value unique
directive transition TransitionDirective expression? intro outro
directive in TransitionDirective expression? intro !outro
directive out TransitionDirective expression? !intro outro
directive animate AnimateDirective expression?
directive let LetDirective pattern?name declares
spread SpreadAttribute expression
block if IfBlock chain=elseif
open test=expression -> consequent
branch else if test=expression -> alternate chain consequent
branch else -> alternate
block each EachBlock
open expression=expression [ as context=pattern ] [ , index?=identifier ] [ ( key?=expression ) ] -> body declares context index
branch else -> fallback?
block await AwaitBlock
open expression=expression [ then [ value=pattern ] -> then declares value | catch [ error=pattern ] -> catch declares error ] -> pending
branch then [ value=pattern ] -> then declares value
branch catch [ error=pattern ] -> catch declares error
block key KeyBlock
open expression=expression -> fragment
block snippet SnippetBlock
open expression=identifier [ typeParams?=typeParameters ] parameters=params -> body declares expression:outside parameters
tag html HtmlTag expression=expression
tag debug DebugTag identifiers=identifiers
tag const ConstTag declaration=const
tag render RenderTag expression=expression
tag attach AttachTag expression=expression attribute
declaration DeclarationTag declaration=statement
expression ExpressionTag expression=expression
`;

File diff suppressed because it is too large Load Diff

@ -1,6 +1,5 @@
/** @import { Expression, Pattern, Program, Statement } from 'estree' */
/** @import { Program } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as teasel from '@teasel/parser';
import * as e from '../../errors.js';
import { keep_tables } from '../../utils/ast.js';
@ -26,143 +25,12 @@ export function parse(source, comments, typescript) {
return handle_parse_error(err);
}
add_comments(source, comments, /** @type {teasel.Comment[]} */ (answer.comments));
keep_tables(answer.node, answer);
return answer.node;
}
/**
* The program inside a `<script>`, with the positions of the whole template.
* @param {Parser} parser
* @param {number} start
* @param {number} end
* @returns {Program}
*/
export function parse_script(parser, start, end) {
let answer;
try {
answer = parser.js.parse('program', start, { end });
} catch (err) {
return handle_parse_error(err);
}
add_comments(
parser.template,
parser.root.comments,
/** @type {teasel.Comment[]} */ (answer.comments)
);
keep_tables(answer.node, answer);
unsupported(answer.typescript);
return answer.node;
}
/**
* Reads JavaScript at the cursor with the template's parser and moves the cursor past it.
* @template {{ end: number, comments?: teasel.Comment[], typescript?: teasel.Kept[] }} T
* @param {Parser} parser
* @param {(js: teasel.Source) => T} run
* @returns {T}
*/
function read(parser, run) {
let answer;
try {
answer = run(parser.js);
} catch (err) {
// the parser's syntax errors; a compile error thrown while reading is the host's own
if (!(err instanceof SyntaxError)) throw err;
return handle_parse_error(err);
}
add_comments(
parser.template,
parser.root.comments,
/** @type {teasel.Comment[]} */ (answer.comments)
);
unsupported(answer.typescript);
parser.index = answer.end;
return answer;
}
/**
* @param {Parser} parser
* @param {string[]} [stop_at] the template's own tokens after the expression, which end it
* @returns {Expression}
*/
export function read_expression(parser, stop_at) {
const start = parser.index;
const answer = read(parser, (js) => js.parse('expression', start, { stopAt: stop_at }));
keep_tables(answer.node, answer);
const node = answer.node;
// the language tools copy an expression's text by its node's range, so the placeholder standing for one that could not be read spans what was read
if (node.type === 'Identifier' && node.name === '' && node.start === node.end) {
node.start = start;
node.end = answer.end;
}
return node;
}
/**
* @param {Parser} parser
* @returns {Pattern}
*/
export function read_pattern(parser) {
const start = parser.index;
const id = parser.read_identifier();
if (id.name !== '') {
const after = parser.index;
parser.allow_whitespace();
// a type annotation makes it a job for the parser
if (!parser.match(':')) {
parser.index = after;
return id;
}
} else {
const char = parser.template[start];
if (char !== '{' && char !== '[') {
e.expected_pattern(start);
}
for (const comment of /** @type {teasel.Comment[]} */ (answer.comments)) {
dedent(comment, source);
comments.push(/** @type {AST.JSComment} */ (comment));
}
const answer = read(parser, (js) => js.parse('pattern', start));
keep_tables(answer.node, answer);
return answer.node;
}
/**
* @param {Parser} parser at the opening paren
* @returns {Pattern[]}
*/
export function read_params(parser) {
const start = parser.index;
const answer = read(parser, (js) => js.parse('params', start));
keep_tables(answer.node, answer);
return answer.node;
}
/**
* @param {Parser} parser at the opening `<`
*/
export function read_type_parameters(parser) {
const start = parser.index;
read(parser, (js) => js.parse('typeParameters', start));
}
/**
* @param {Parser} parser
* @returns {Statement}
*/
export function read_statement(parser) {
const start = parser.index;
const answer = read(parser, (js) => js.parse('statement', start));
keep_tables(answer.node, answer);
return answer.node;
}
@ -178,7 +46,7 @@ const UNSUPPORTED = {
};
/** @param {teasel.Kept[] | undefined} kept */
function unsupported(kept) {
export function unsupported(kept) {
const node = kept?.[0];
if (node) {
e.typescript_invalid_feature(
@ -201,12 +69,10 @@ function handle_parse_error(err) {
* Comments are needed in order to support `svelte-ignore` comments in JS code and so that
* `prettier-plugin-svelte` doesn't remove all comments when formatting. A block comment loses
* the indentation of the line it starts on.
* @param {teasel.Comment} comment
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {teasel.Comment[]} parsed
*/
function add_comments(source, comments, parsed) {
for (const comment of parsed) {
export function dedent(comment, source) {
if (comment.type === 'Block' && comment.value.includes('\n')) {
let a = comment.start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
@ -217,7 +83,4 @@ function add_comments(source, comments, parsed) {
const indentation = source.slice(a, b);
comment.value = comment.value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push(/** @type {AST.JSComment} */ (comment));
}
}

@ -1,83 +0,0 @@
/** @import { Program } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import { parse_script } from './js.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { is_text_attribute } from '../../utils/ast.js';
import { locator } from '../../state.js';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /<\/script\s*>/y;
const RESERVED_ATTRIBUTES = ['server', 'client', 'worker', 'test', 'default'];
const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
parser.read_until_regex(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script');
}
const ast = parse_script(parser, script_start, parser.index);
parser.read(regex_starts_with_closing_script_tag);
if (ast.loc) {
// the legacy AST places the program at the tag, not at its contents
({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
}
/** @type {'default' | 'module'} */
let context = 'default';
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (RESERVED_ATTRIBUTES.includes(attribute.name)) {
e.script_reserved_attribute(attribute, attribute.name);
}
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
w.script_unknown_attribute(attribute);
}
if (attribute.name === 'module') {
if (attribute.value !== true) {
// Deliberately a generic code to future-proof for potential other attributes
e.script_invalid_attribute_value(attribute, attribute.name);
}
context = 'module';
}
if (attribute.name === 'context') {
if (attribute.value === true || !is_text_attribute(attribute)) {
e.script_invalid_context(attribute);
}
const value = attribute.value[0].data;
if (value !== 'module') {
e.script_invalid_context(attribute);
}
context = 'module';
}
}
return {
type: 'Script',
start,
end: parser.index,
context,
content: ast,
// @ts-ignore
attributes
};
}

@ -1,988 +0,0 @@
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js';
import { read_expression } from '../js.js';
import { read_script } from '../script.js';
import read_style from '../css.js';
import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import {
create_attribute,
create_fragment,
ExpressionMetadata,
is_element_node
} from '../../nodes.js';
import {
get_attribute_expression,
is_expression_attribute,
value_names
} from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import { is_whitespace } from '../utils/whitespace.js';
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
const regex_starts_with_quote_characters = /["']/y;
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
const regex_doctype_name = /^![a-zA-Z]+$/;
const regex_namespaced_name = /^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/;
/** @param {string} name */
function is_valid_element_name(name) {
// DOCTYPE (e.g. !DOCTYPE)
if (regex_doctype_name.test(name)) return true;
// svelte:* meta tags (e.g. svelte:element, svelte:head)
if (regex_namespaced_name.test(name)) return true;
// standard HTML/SVG/MathML elements and custom elements
return REGEX_VALID_TAG_NAME.test(name);
}
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('-->');
parser.eat('-->', true);
parser.append({
type: 'Comment',
start,
end: parser.index,
data
});
return;
}
if (parser.eat('/')) {
const name = read_tag_name(parser);
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') {
if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed(
{ start: parent.start, end },
`</${name}>`,
`</${parent.name}>`
);
}
} else if (!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;
}
const tag = read_tag(parser);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !tag.name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.tag_invalid_name(bounds);
}
}
if (root_only_meta_tags.has(tag.name)) {
if (tag.name in parser.meta_tags) {
e.svelte_meta_duplicate(start, tag.name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, tag.name);
}
parser.meta_tags[tag.name] = true;
}
const type = meta_tags.has(tag.name)
? meta_tags.get(tag.name)
: regex_valid_component_name.test(tag.name) || (parser.loose && tag.name.endsWith('.'))
? 'Component'
: tag.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
tag.name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement'
: 'RegularElement';
/** @type {AST.ElementLike} */
const element =
type === 'RegularElement'
? {
type,
start,
end: -1,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
svg: false,
mathml: false,
scoped: false,
has_spread: false,
path: [],
synthetic_value_node: null
}
}
: /** @type {AST.ElementLike} */ ({
type,
start,
end: -1,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
// unpopulated at first, differs between types
}
});
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, tag.name)) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed({ start: parent.start, end }, `<${tag.name}>`, `</${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: tag.name,
depth: parser.stack.length
};
}
/** @type {string[]} */
const unique_names = [];
const current = parser.current();
const is_top_level_script_or_style =
(tag.name === 'script' || tag.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 === 'Component') {
element.metadata.expression = new ExpressionMetadata();
}
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);
element.metadata.expression = new ExpressionMetadata();
}
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);
}
element.metadata.expression = new ExpressionMetadata();
}
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 (tag.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(tag.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 (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (tag.name === 'textarea') {
// special case
element.fragment.nodes = read_sequence(
parser,
() => {
regex_closing_textarea_tag.lastIndex = parser.index;
return regex_closing_textarea_tag.test(parser.template);
},
'inside <textarea>'
);
parser.read(regex_closing_textarea_tag);
element.end = parser.index;
} else if (tag.name === 'script' || tag.name === 'style') {
// special case
const start = parser.index;
const close_tag = `</${tag.name}>`;
const close_index = parser.template.indexOf(close_tag, parser.index);
const data = parser.template.slice(
parser.index,
close_index === -1 ? parser.template.length : close_index
);
parser.index = close_index === -1 ? parser.template.length : close_index;
const end = parser.index;
/** @type {AST.Text} */
const node = {
start,
end,
type: 'Text',
data,
raw: data
};
element.fragment.nodes.push(node);
parser.eat(`</${tag.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 tag = read_tag(parser, true);
if (!tag.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(tag.name, tag.loc, start, parser.index, value);
}
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
/** @type {AST.JSComment | null} */
// eslint-disable-next-line no-useless-assignment -- it is, in fact, eslint that is useless
let comment = null;
while ((comment = read_comment(parser))) {
parser.root.comments.push(comment);
parser.allow_whitespace();
}
const start = parser.index;
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('@attach')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.AttachTag} */
const attachment = {
type: 'AttachTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
};
return attachment;
}
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: new ExpressionMetadata()
}
};
return spread;
} else {
const id = parser.read_identifier();
if (id.name === '') {
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
} else {
e.attribute_empty_shorthand(start);
}
}
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: id.start,
end: id.end,
expression: id,
metadata: {
expression: new ExpressionMetadata()
}
};
return create_attribute(id.name, id.loc, start, parser.index, expression);
}
}
const tag = read_tag(parser, true);
if (!tag.name) return null;
let end = parser.index;
parser.allow_whitespace();
const colon_index = tag.name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(tag.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] = tag.name.slice(colon_index + 1).split('|');
if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, tag.name);
}
if (type === 'StyleDirective') {
return {
start,
end,
type,
name: directive_name,
name_loc: tag.loc,
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: new ExpressionMetadata()
}
};
}
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;
}
}
const directive = /** @type {AST.Directive} */ ({
start,
end,
type,
name: directive_name,
name_loc: tag.loc,
expression,
metadata: {
expression: new ExpressionMetadata()
}
});
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers;
if (directive.type === 'TransitionDirective') {
const direction = tag.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
});
value_names.add(/** @type {Identifier} */ (directive.expression));
}
return directive;
}
return create_attribute(tag.name, tag.loc, start, end, value);
}
/**
* @param {Parser} parser
* @returns {AST.JSComment | null}
*/
function read_comment(parser) {
const start = parser.index;
if (parser.eat('//')) {
const value = parser.read_until('\n');
const end = parser.index;
return {
type: 'Line',
start,
end,
value,
loc: {
start: locator(start),
end: locator(end)
}
};
}
if (parser.eat('/*')) {
const value = parser.read_until('*/');
parser.eat('*/');
const end = parser.index;
return {
type: 'Block',
start,
end,
value,
loc: {
start: locator(start),
end: locator(end)
}
};
}
return null;
}
/**
* @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',
// `<Component test={ />` is an unclosed value, not a regex
['/>']
);
} catch (/** @type {any} */ error) {
const pos = error.position?.[0];
if (
error.code === 'js_parse_error' &&
pos !== undefined &&
parser.template.startsWith('/>', pos)
) {
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
* @param {string[]} [stop_at] the template's tokens that end an expression, see `read_expression`
* @returns {any[]}
*/
function read_sequence(parser, done, location, stop_at) {
/** @type {Array<AST.Text | AST.ExpressionTag>} */
const chunks = [];
let chunk_start = parser.index;
/** @param {number} end */
function flush(end) {
if (end > chunk_start) {
const raw = parser.template.slice(chunk_start, end);
chunks.push({
start: chunk_start,
end,
type: 'Text',
raw,
data: decode_character_references(raw, true)
});
}
}
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_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.tag_invalid_placement(index, name, location);
}
flush(parser.index - 1);
parser.allow_whitespace();
const expression = read_expression(parser, stop_at);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const chunk = {
type: 'ExpressionTag',
start: index,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
};
chunks.push(chunk);
chunk_start = parser.index;
} else {
parser.index++;
}
}
if (parser.loose) {
return chunks;
} else {
e.unexpected_eof(parser.template.length);
}
}
/**
* @param {Parser} parser
* @param {boolean} [attribute]
*/
function read_tag_name(parser, attribute = false) {
const start = parser.index;
if (start >= parser.template.length && !parser.loose) e.unexpected_eof(parser.template.length);
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
if (
is_whitespace(cc) ||
cc === 47 || // /
cc === 62 || // >
(attribute && (cc === 34 || cc === 39 || cc === 61)) // " ' =
) {
break;
}
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}
/**
* @param {Parser} parser
* @param {boolean} [attribute]
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, attribute = false) {
const start = parser.index;
const name = read_tag_name(parser, attribute);
const end = parser.index;
/** @type {Identifier & { start: number, end: number, loc: SourceLocation }} */
const identifier = {
type: 'Identifier',
name,
start,
end,
loc: {
start: locator(start),
end: locator(end)
}
};
value_names.add(identifier);
return identifier;
}
/** @param {Parser} parser */
function read_lowercase_name(parser) {
const start = parser.index;
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
// a-z
if (cc < 97 || cc > 122) break;
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}

@ -1,17 +0,0 @@
/** @import { Parser } from '../index.js' */
import element from './element.js';
import tag from './tag.js';
import text from './text.js';
/** @param {Parser} parser */
export default function fragment(parser) {
if (parser.match('<')) {
return element;
}
if (parser.match('{')) {
return tag;
}
return text;
}

@ -1,664 +0,0 @@
/** @import { Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
import { create_fragment, ExpressionMetadata } from '../../nodes.js';
import { read_expression, read_params, read_pattern, read_statement, read_type_parameters } from '../js.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
const regex_supported_declaration = /(?:let|const)\b/y;
const regex_unsupported_declaration = /(?:var|interface|enum)\b/y;
// `type` is a contextual keyword; this is just a shape hint, confirmed by parsing.
const regex_maybe_type_declaration = /type\b/y;
/** @param {Parser} parser */
export default function tag(parser) {
const start = parser.index;
parser.index += 1;
parser.allow_whitespace();
if (parser.eat('#')) return open(parser);
if (parser.eat(':')) return next(parser);
if (parser.eat('@')) return special(parser);
if (parser.match('/')) {
if (!parser.match('/*') && !parser.match('//')) {
parser.eat('/');
return close(parser);
}
}
const declaration = read_declaration(parser);
if (declaration) {
parser.append({
type: 'DeclarationTag',
start,
end: parser.index,
declaration: /** @type {VariableDeclaration} */ (declaration),
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
});
}
/**
* @param {Parser} parser
* @returns {null | import('estree').VariableDeclaration}
*/
function read_declaration(parser) {
const start = parser.index;
const unsupported = parser.match_regex(regex_unsupported_declaration);
if (unsupported) {
e.declaration_tag_invalid_type({ start, end: start + unsupported.length });
}
if (
!parser.match_regex(regex_supported_declaration) &&
// `type` is special, since it is not a reserved keyword and can be used
// as part of a valid expression. We gotta parse first and then see what it is.
!parser.match_regex(regex_maybe_type_declaration)
) {
return null;
}
const initial_comment_count = parser.root.comments.length;
const declaration = read_statement(parser);
if (declaration.type !== 'VariableDeclaration') {
if (declaration.type === 'ExpressionStatement') {
parser.index = start;
parser.root.comments.length = initial_comment_count; // Else they show up duplicated
return null;
} else {
// This is a TSTypeAliasDeclaration
e.declaration_tag_invalid_type({
start: declaration.start ?? start,
end: declaration.end ?? parser.index
});
}
}
// TODO support using
if (declaration.kind !== 'let' && declaration.kind !== 'const') {
e.declaration_tag_invalid_type(declaration);
}
parser.allow_whitespace();
parser.eat('}', true);
return declaration;
}
/** @param {Parser} parser */
function open(parser) {
let start = parser.index - 2;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('if')) {
parser.require_whitespace();
/** @type {AST.IfBlock} */
const block = parser.append({
type: 'IfBlock',
elseif: false,
start,
end: -1,
test: read_expression(parser),
consequent: create_fragment(),
alternate: null,
metadata: {
expression: new ExpressionMetadata()
}
});
parser.allow_whitespace();
parser.eat('}', true);
parser.stack.push(block);
parser.fragments.push(block.consequent);
return;
}
if (parser.eat('each')) {
parser.require_whitespace();
// the list ends at the `as` naming the item or the `,` before the index, so an assertion in it needs parens
const expression = read_expression(parser, ['as', ',']);
parser.allow_whitespace();
/** @type {Pattern | null} */
let context = null;
let index;
let key;
if (parser.eat('as')) {
parser.require_whitespace();
context = read_pattern(parser);
}
parser.allow_whitespace();
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier().name;
if (!index) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
}
if (parser.eat('(')) {
parser.allow_whitespace();
key = read_expression(parser);
parser.allow_whitespace();
parser.eat(')', true);
parser.allow_whitespace();
}
parser.eat('}', true, false);
/** @type {AST.EachBlock} */
const block = parser.append({
type: 'EachBlock',
start,
end: -1,
expression,
body: create_fragment(),
context,
index,
key,
metadata: /** @type {any} */ (null) // filled in later
});
parser.stack.push(block);
parser.fragments.push(block.body);
return;
}
if (parser.eat('await')) {
parser.require_whitespace();
const expression = read_expression(parser, ['then', 'catch']);
parser.allow_whitespace();
/** @type {AST.AwaitBlock} */
const block = parser.append({
type: 'AwaitBlock',
start,
end: -1,
expression,
value: null,
error: null,
pending: null,
then: null,
catch: null,
metadata: {
expression: new ExpressionMetadata()
}
});
if (parser.eat('then')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.value = read_pattern(parser);
parser.allow_whitespace();
}
block.then = create_fragment();
parser.fragments.push(block.then);
} else if (parser.eat('catch')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.error = read_pattern(parser);
parser.allow_whitespace();
}
block.catch = create_fragment();
parser.fragments.push(block.catch);
} else {
block.pending = create_fragment();
parser.fragments.push(block.pending);
}
parser.eat('}', true, false);
parser.stack.push(block);
return;
}
if (parser.eat('key')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.KeyBlock} */
const block = parser.append({
type: 'KeyBlock',
start,
end: -1,
expression,
fragment: create_fragment(),
metadata: {
expression: new ExpressionMetadata()
}
});
parser.stack.push(block);
parser.fragments.push(block.fragment);
return;
}
if (parser.eat('snippet')) {
parser.require_whitespace();
const id = parser.read_identifier();
if (id.name === '' && !parser.loose) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
/** @type {string | undefined} */
let type_params;
if (parser.ts && parser.match('<')) {
const start = parser.index;
read_type_parameters(parser);
type_params = parser.template.slice(start + 1, parser.index - 1);
}
parser.allow_whitespace();
/** @type {import('estree').Pattern[]} */
let parameters = [];
if (parser.match('(')) {
parameters = read_params(parser);
} else if (!parser.loose) {
e.expected_token(parser.index, '(');
}
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.SnippetBlock} */
const block = parser.append({
type: 'SnippetBlock',
start,
end: -1,
expression: id,
typeParams: type_params,
parameters,
body: create_fragment(),
metadata: {
can_hoist: false,
sites: new Set()
}
});
parser.stack.push(block);
parser.fragments.push(block.body);
return;
}
e.expected_block_type(parser.index);
}
/** @param {Parser} parser */
function next(parser) {
const start = parser.index - 1;
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
if (block.type === 'IfBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
if (parser.eat('if')) e.block_invalid_elseif(start);
parser.allow_whitespace();
parser.fragments.pop();
block.alternate = create_fragment();
parser.fragments.push(block.alternate);
// :else if
if (parser.eat('if')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
let elseif_start = start - 1;
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
/** @type {AST.IfBlock} */
const child = parser.append({
start: elseif_start,
end: -1,
type: 'IfBlock',
elseif: true,
test: expression,
consequent: create_fragment(),
alternate: null,
metadata: {
expression: new ExpressionMetadata()
}
});
parser.stack.push(child);
parser.fragments.pop();
parser.fragments.push(child.consequent);
} else {
// :else
parser.allow_whitespace();
parser.eat('}', true);
}
return;
}
if (block.type === 'EachBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else}');
parser.allow_whitespace();
parser.eat('}', true);
block.fallback = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.fallback);
return;
}
if (block.type === 'AwaitBlock') {
if (parser.eat('then')) {
if (block.then) {
e.block_duplicate_clause(start, '{:then}');
}
if (!parser.eat('}')) {
parser.require_whitespace();
block.value = read_pattern(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
block.then = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.then);
return;
}
if (parser.eat('catch')) {
if (block.catch) {
e.block_duplicate_clause(start, '{:catch}');
}
if (!parser.eat('}')) {
parser.require_whitespace();
block.error = read_pattern(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
block.catch = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.catch);
return;
}
e.expected_token(start, '{:then ...} or {:catch ...}');
}
e.block_invalid_continuation_placement(start);
}
/** @param {Parser} parser */
function close(parser) {
const start = parser.index - 1;
let block = parser.current();
/** Only relevant/reached for loose parsing mode */
let matched;
switch (block.type) {
case 'IfBlock':
matched = parser.eat('if', true, false);
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = /** @type {AST.IfBlock} */ (parser.current());
}
block.end = parser.index;
parser.pop();
return;
case 'EachBlock':
matched = parser.eat('each', true, false);
break;
case 'KeyBlock':
matched = parser.eat('key', true, false);
break;
case 'AwaitBlock':
matched = parser.eat('await', true, false);
break;
case 'SnippetBlock':
matched = parser.eat('snippet', true, false);
break;
case 'RegularElement':
if (parser.loose) {
matched = false;
} else {
// TODO handle implicitly closed elements
e.block_unexpected_close(start);
}
break;
default:
e.block_unexpected_close(start);
}
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
block.end = parser.index;
parser.pop();
}
/** @param {Parser} parser */
function special(parser) {
let start = parser.index;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('html')) {
// {@html content} tag
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'HtmlTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
if (parser.eat('debug')) {
/** @type {Identifier[]} */
let identifiers;
// Implies {@debug} which indicates "debug all"
if (parser.read(regex_whitespace_with_closing_curly_brace)) {
identifiers = [];
} else {
const expression = read_expression(parser);
identifiers =
expression.type === 'SequenceExpression'
? /** @type {Identifier[]} */ (expression.expressions)
: [/** @type {Identifier} */ (expression)];
identifiers.forEach(
/** @param {any} node */ (node) => {
if (node.type !== 'Identifier') {
e.debug_tag_invalid_arguments(/** @type {number} */ (node.start));
}
}
);
parser.allow_whitespace();
parser.eat('}', true);
}
parser.append({
type: 'DebugTag',
start,
end: parser.index,
identifiers
});
return;
}
if (parser.eat('const')) {
parser.require_whitespace();
const id = read_pattern(parser);
parser.allow_whitespace();
parser.eat('=', true);
parser.allow_whitespace();
const init = read_expression(parser);
// parser is past wrapping parens, but `init.end` is not — use the parser position
const declarator_end = parser.index;
if (init.type === 'SequenceExpression' && !init.parenthesized) {
// const a = (b, c) is allowed but a = b, c = d is not;
e.const_tag_invalid_expression(init);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ConstTag',
start,
end: parser.index,
declaration: {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{ type: 'VariableDeclarator', id, init, start: id.start, end: declarator_end }
],
start: start + 2, // start at const, not at @const
end: parser.index - 1
},
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
if (parser.eat('render')) {
// {@render foo(...)}
parser.require_whitespace();
const expression = read_expression(parser);
if (
expression.type !== 'CallExpression' &&
(expression.type !== 'ChainExpression' || expression.expression.type !== 'CallExpression')
) {
e.render_tag_invalid_expression(expression);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'RenderTag',
start,
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: new ExpressionMetadata(),
dynamic: false,
arguments: [],
path: [],
snippets: new Set()
}
});
return;
}
e.expected_tag(parser.index);
}

@ -1,23 +0,0 @@
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { decode_character_references } from '../utils/html.js';
/** @param {Parser} parser */
export default function text(parser) {
const start = parser.index;
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
parser.index++;
}
const data = parser.template.slice(start, parser.index);
/** @type {AST.Text} */
parser.append({
type: 'Text',
start,
end: parser.index,
raw: data,
data: decode_character_references(data, false)
});
}

@ -1,134 +0,0 @@
import entities from './entities.js';
const windows_1252 = [
8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216,
8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376
];
/**
* @param {string} entity_name
* @param {boolean} is_attribute_value
*/
function reg_exp_entity(entity_name, is_attribute_value) {
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
// doesn't decode the html entity which not ends with ; and next character is =, number or alphabet in attribute value.
if (is_attribute_value && !entity_name.endsWith(';')) {
return `${entity_name}\\b(?!=)`;
}
return entity_name;
}
/** @param {boolean} is_attribute_value */
function get_entity_pattern(is_attribute_value) {
const reg_exp_num = '#(?:[xX][a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map(
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
);
const entity_pattern = new RegExp(`&(${reg_exp_num}|${reg_exp_entities.join('|')})`, 'g');
return entity_pattern;
}
const entity_pattern_content = get_entity_pattern(false);
const entity_pattern_attr_value = get_entity_pattern(true);
/**
* @param {string} html
* @param {boolean} is_attribute_value
*/
export function decode_character_references(html, is_attribute_value) {
if (html.indexOf('&') === -1) return html; // fast path
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(
entity_pattern,
/**
* @param {any} match
* @param {keyof typeof entities} entity
*/ (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x' || entity[1] === 'X') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code, is_attribute_value));
}
);
}
const NUL = 0;
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal
// code points with alternatives in some cases - since we're bypassing that mechanism, we need
// to replace them ourselves
//
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/**
* @param {number} code
* @param {boolean} is_attribute_value
*/
function validate_code(code, is_attribute_value) {
// line feed becomes generic whitespace, since it is collapsed along with the
// surrounding whitespace anyway. In an attribute value it is significant, so it
// is left alone there
if (code === 10 && !is_attribute_value) {
return 32;
}
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if (code < 128) {
return code;
}
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing € signs and so on
if (code <= 159) {
return windows_1252[code - 128];
}
// basic multilingual plane
if (code < 55296) {
return code;
}
// UTF-16 surrogate halves
if (code <= 57343) {
return NUL;
}
// rest of the basic multilingual plane
if (code <= 65535) {
return code;
}
// supplementary multilingual plane 0x10000 - 0x1ffff
if (code >= 65536 && code <= 131071) {
return code;
}
// supplementary ideographic plane 0x20000 - 0x2ffff
if (code >= 131072 && code <= 196607) {
return code;
}
// supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
return code;
}
return NUL;
}

@ -15,6 +15,7 @@ import {
unwrap_pattern
} from '../utils/ast.js';
import { is_reserved, is_rune } from '../../utils.js';
import { is_template_node } from './1-parse/index.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
@ -128,7 +129,11 @@ export class Reference {
get path() {
if (this.#path === undefined) {
const inside = [];
for (let parent = parentOf(this.node); parent !== undefined; parent = parentOf(parent)) {
for (
let parent = parentOf(this.node);
parent !== undefined && !is_template_node(parent);
parent = parentOf(parent)
) {
inside.push(/** @type {AST.SvelteNode} */ (parent));
}
this.#path = this.#template.concat(inside.reverse());
@ -1202,8 +1207,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
parsed !== outermost &&
ours.has(parsed)
) {
// a fragment has no span of its own: anything inside it is closer
const current = /** @type {any} */ (found.node);
if (!current || node.start >= current.start) found = parsed;
if (!current || current.start === undefined || node.start >= current.start)
found = parsed;
}
}
return found;

@ -55,13 +55,11 @@
"loc": {
"start": {
"line": 1,
"column": 17,
"character": 17
"column": 17
},
"end": {
"line": 1,
"column": 22,
"character": 22
"column": 22
}
}
},

@ -40,13 +40,11 @@
"loc": {
"start": {
"line": 1,
"column": 6,
"character": 6
"column": 6
},
"end": {
"line": 1,
"column": 8,
"character": 8
"column": 8
}
}
}

@ -33,13 +33,11 @@
"loc": {
"start": {
"line": 3,
"column": 8,
"character": 47
"column": 8
},
"end": {
"line": 3,
"column": 16,
"character": 55
"column": 16
}
}
},

@ -32,13 +32,11 @@
"loc": {
"start": {
"line": 3,
"column": 7,
"character": 46
"column": 7
},
"end": {
"line": 3,
"column": 15,
"character": 54
"column": 15
}
}
},
@ -50,13 +48,11 @@
"loc": {
"start": {
"line": 5,
"column": 8,
"character": 96
"column": 8
},
"end": {
"line": 5,
"column": 16,
"character": 104
"column": 16
}
}
},

@ -38,7 +38,17 @@
"start": 43,
"end": 46,
"type": "Identifier",
"name": "foo"
"name": "foo",
"loc": {
"start": {
"line": 5,
"column": 13
},
"end": {
"line": 5,
"column": 16
}
}
},
"modifiers": []
}

@ -48,13 +48,11 @@
"loc": {
"start": {
"line": 1,
"column": 18,
"character": 18
"column": 18
},
"end": {
"line": 1,
"column": 24,
"character": 24
"column": 24
}
}
},

@ -76,13 +76,11 @@
"loc": {
"start": {
"line": 1,
"column": 18,
"character": 18
"column": 18
},
"end": {
"line": 1,
"column": 24,
"character": 24
"column": 24
}
}
},

@ -48,13 +48,11 @@
"loc": {
"start": {
"line": 1,
"column": 16,
"character": 16
"column": 16
},
"end": {
"line": 1,
"column": 20,
"character": 20
"column": 20
}
}
},

@ -48,13 +48,11 @@
"loc": {
"start": {
"line": 1,
"column": 18,
"character": 18
"column": 18
},
"end": {
"line": 1,
"column": 24,
"character": 24
"column": 24
}
}
},

@ -23,13 +23,11 @@
"loc": {
"start": {
"line": 4,
"column": 10,
"character": 40
"column": 10
},
"end": {
"line": 4,
"column": 17,
"character": 47
"column": 17
}
}
},
@ -141,13 +139,11 @@
"loc": {
"start": {
"line": 8,
"column": 10,
"character": 104
"column": 10
},
"end": {
"line": 8,
"column": 25,
"character": 119
"column": 25
}
}
},

@ -111,13 +111,11 @@
"loc": {
"start": {
"line": 9,
"column": 10,
"character": 63
"column": 10
},
"end": {
"line": 9,
"column": 10,
"character": 63
"column": 10
}
}
},
@ -143,13 +141,11 @@
"loc": {
"start": {
"line": 12,
"column": 10,
"character": 87
"column": 10
},
"end": {
"line": 12,
"column": 13,
"character": 90
"column": 13
}
}
},

@ -40,13 +40,11 @@
"loc": {
"start": {
"line": 1,
"column": 6,
"character": 6
"column": 6
},
"end": {
"line": 1,
"column": 6,
"character": 6
"column": 6
}
}
}
@ -725,13 +723,11 @@
"loc": {
"start": {
"line": 15,
"column": 16,
"character": 197
"column": 16
},
"end": {
"line": 15,
"column": 20,
"character": 201
"column": 20
}
}
},
@ -812,29 +808,20 @@
"type": "EachBlock",
"start": 219,
"end": 246,
"children": [],
"context": {
"type": "Identifier",
"name": "item",
"children": [
{
"type": "Text",
"start": 234,
"end": 238,
"loc": {
"start": {
"line": 17,
"column": 15,
"character": 234
},
"end": {
"line": 17,
"column": 19,
"character": 238
}
"end": 239,
"raw": "item}",
"data": "item}"
}
},
],
"context": null,
"expression": {
"type": "MemberExpression",
"start": 226,
"end": 231,
"end": 233,
"loc": {
"start": {
"line": 17,
@ -842,7 +829,7 @@
},
"end": {
"line": 17,
"column": 12
"column": 14
}
},
"object": {
@ -864,7 +851,7 @@
"property": {
"type": "Identifier",
"start": 231,
"end": 231,
"end": 233,
"loc": {
"start": {
"line": 17,
@ -872,10 +859,10 @@
},
"end": {
"line": 17,
"column": 12
"column": 14
}
},
"name": ""
"name": "as"
},
"computed": false,
"optional": false
@ -979,7 +966,7 @@
"expression": {
"type": "MemberExpression",
"start": 277,
"end": 280,
"end": 284,
"loc": {
"start": {
"line": 21,
@ -987,7 +974,7 @@
},
"end": {
"line": 21,
"column": 11
"column": 15
}
},
"object": {
@ -1009,7 +996,7 @@
"property": {
"type": "Identifier",
"start": 280,
"end": 280,
"end": 284,
"loc": {
"start": {
"line": 21,
@ -1017,46 +1004,37 @@
},
"end": {
"line": 21,
"column": 11
"column": 15
}
},
"name": ""
"name": "then"
},
"computed": false,
"optional": false
},
"value": {
"type": "Identifier",
"name": "y",
"start": 285,
"end": 286,
"loc": {
"start": {
"line": 21,
"column": 16,
"character": 285
},
"end": {
"line": 21,
"column": 17,
"character": 286
}
}
},
"value": null,
"error": null,
"pending": {
"type": "PendingBlock",
"start": null,
"end": null,
"children": [],
"skip": true
"start": 285,
"end": 287,
"children": [
{
"type": "Text",
"start": 285,
"end": 287,
"raw": "y}",
"data": "y}"
}
],
"skip": false
},
"then": {
"type": "ThenBlock",
"start": 287,
"end": 267,
"start": null,
"end": null,
"children": [],
"skip": false
"skip": true
},
"catch": {
"type": "CatchBlock",
@ -1080,7 +1058,7 @@
"expression": {
"type": "MemberExpression",
"start": 305,
"end": 308,
"end": 313,
"loc": {
"start": {
"line": 23,
@ -1088,7 +1066,7 @@
},
"end": {
"line": 23,
"column": 11
"column": 16
}
},
"object": {
@ -1110,7 +1088,7 @@
"property": {
"type": "Identifier",
"start": 308,
"end": 308,
"end": 313,
"loc": {
"start": {
"line": 23,
@ -1118,39 +1096,30 @@
},
"end": {
"line": 23,
"column": 11
"column": 16
}
},
"name": ""
"name": "catch"
},
"computed": false,
"optional": false
},
"value": null,
"error": {
"type": "Identifier",
"name": "y",
"start": 314,
"end": 315,
"loc": {
"start": {
"line": 23,
"column": 17,
"character": 314
},
"end": {
"line": 23,
"column": 18,
"character": 315
}
}
},
"error": null,
"pending": {
"type": "PendingBlock",
"start": null,
"end": null,
"children": [],
"skip": true
"start": 314,
"end": 316,
"children": [
{
"type": "Text",
"start": 314,
"end": 316,
"raw": "y}",
"data": "y}"
}
],
"skip": false
},
"then": {
"type": "ThenBlock",
@ -1161,10 +1130,10 @@
},
"catch": {
"type": "CatchBlock",
"start": 316,
"end": 295,
"start": null,
"end": null,
"children": [],
"skip": false
"skip": true
}
}
]

@ -244,13 +244,11 @@
"loc": {
"start": {
"line": 19,
"column": 12,
"character": 138
"column": 12
},
"end": {
"line": 19,
"column": 13,
"character": 139
"column": 13
}
}
},

@ -123,13 +123,11 @@
"loc": {
"start": {
"line": 13,
"column": 7,
"character": 97
"column": 7
},
"end": {
"line": 13,
"column": 8,
"character": 98
"column": 8
}
}
},
@ -223,13 +221,11 @@
"loc": {
"start": {
"line": 18,
"column": 7,
"character": 137
"column": 7
},
"end": {
"line": 18,
"column": 8,
"character": 138
"column": 8
}
}
},

@ -48,13 +48,11 @@
"loc": {
"start": {
"line": 1,
"column": 17,
"character": 17
"column": 17
},
"end": {
"line": 1,
"column": 19,
"character": 19
"column": 19
}
}
},

@ -77,6 +77,24 @@
"raw": "2",
"data": "2"
}
],
"leadingComments": [
{
"type": "Line",
"value": " this is a line comment",
"start": 20,
"end": 45,
"loc": {
"start": {
"line": 3,
"column": 1
},
"end": {
"line": 3,
"column": 26
}
}
}
]
},
{
@ -104,6 +122,56 @@
"raw": "3",
"data": "3"
}
],
"leadingComments": [
{
"type": "Block",
"value": " this is a\n\t\tm\n\t\tu\n\t\tl\n\t\tt\n\t\ti\n\t\tl\n\t\ti\n\t\tn\n\t\te\n\t\tcomment\n\t",
"start": 61,
"end": 123,
"loc": {
"start": {
"line": 5,
"column": 1
},
"end": {
"line": 16,
"column": 3
}
}
},
{
"type": "Line",
"value": " oh look another line comment",
"start": 125,
"end": 156,
"loc": {
"start": {
"line": 17,
"column": 1
},
"end": {
"line": 17,
"column": 32
}
}
},
{
"type": "Line",
"value": " (two, in fact)",
"start": 158,
"end": 175,
"loc": {
"start": {
"line": 18,
"column": 1
},
"end": {
"line": 18,
"column": 18
}
}
}
]
}
],
@ -162,6 +230,40 @@
"raw": "1",
"data": "1"
}
],
"leadingComments": [
{
"type": "Block",
"value": " inline ",
"start": 207,
"end": 219,
"loc": {
"start": {
"line": 22,
"column": 6
},
"end": {
"line": 22,
"column": 18
}
}
},
{
"type": "Block",
"value": " another inline ",
"start": 220,
"end": 240,
"loc": {
"start": {
"line": 22,
"column": 19
},
"end": {
"line": 22,
"column": 39
}
}
}
]
}
],
@ -182,13 +284,11 @@
"loc": {
"start": {
"line": 3,
"column": 1,
"character": 20
"column": 1
},
"end": {
"line": 3,
"column": 26,
"character": 45
"column": 26
}
}
},
@ -200,13 +300,11 @@
"loc": {
"start": {
"line": 5,
"column": 1,
"character": 61
"column": 1
},
"end": {
"line": 16,
"column": 3,
"character": 123
"column": 3
}
}
},
@ -218,13 +316,11 @@
"loc": {
"start": {
"line": 17,
"column": 1,
"character": 125
"column": 1
},
"end": {
"line": 17,
"column": 32,
"character": 156
"column": 32
}
}
},
@ -236,13 +332,11 @@
"loc": {
"start": {
"line": 18,
"column": 1,
"character": 158
"column": 1
},
"end": {
"line": 18,
"column": 18,
"character": 175
"column": 18
}
}
},
@ -254,13 +348,11 @@
"loc": {
"start": {
"line": 22,
"column": 6,
"character": 207
"column": 6
},
"end": {
"line": 22,
"column": 18,
"character": 219
"column": 18
}
}
},
@ -272,13 +364,11 @@
"loc": {
"start": {
"line": 22,
"column": 19,
"character": 220
"column": 19
},
"end": {
"line": 22,
"column": 39,
"character": 240
"column": 39
}
}
}

@ -55,13 +55,11 @@
"loc": {
"start": {
"line": 2,
"column": 9,
"character": 31
"column": 9
},
"end": {
"line": 2,
"column": 10,
"character": 32
"column": 10
}
}
},
@ -116,11 +114,31 @@
"optional": false
},
"start": 31,
"end": 47
"end": 47,
"loc": {
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 25
}
}
}
],
"start": 25,
"end": 47
"end": 47,
"loc": {
"start": {
"line": 2,
"column": 3
},
"end": {
"line": 2,
"column": 25
}
}
}
},
{
@ -148,13 +166,11 @@
"loc": {
"start": {
"line": 3,
"column": 9,
"character": 58
"column": 9
},
"end": {
"line": 3,
"column": 10,
"character": 59
"column": 10
}
}
},
@ -241,11 +257,31 @@
}
},
"start": 58,
"end": 74
"end": 74,
"loc": {
"start": {
"line": 3,
"column": 9
},
"end": {
"line": 3,
"column": 25
}
}
}
],
"start": 52,
"end": 74
"end": 74,
"loc": {
"start": {
"line": 3,
"column": 3
},
"end": {
"line": 3,
"column": 25
}
}
}
},
{
@ -273,13 +309,11 @@
"loc": {
"start": {
"line": 4,
"column": 9,
"character": 85
"column": 9
},
"end": {
"line": 4,
"column": 10,
"character": 86
"column": 10
}
}
},
@ -386,11 +420,31 @@
]
},
"start": 85,
"end": 104
"end": 104,
"loc": {
"start": {
"line": 4,
"column": 9
},
"end": {
"line": 4,
"column": 28
}
}
}
],
"start": 79,
"end": 104
"end": 104,
"loc": {
"start": {
"line": 4,
"column": 3
},
"end": {
"line": 4,
"column": 28
}
}
}
},
{
@ -513,13 +567,11 @@
"loc": {
"start": {
"line": 1,
"column": 16,
"character": 16
"column": 16
},
"end": {
"line": 1,
"column": 20,
"character": 20
"column": 20
}
}
}

@ -26,13 +26,11 @@
"loc": {
"start": {
"line": 4,
"column": 10,
"character": 40
"column": 10
},
"end": {
"line": 4,
"column": 13,
"character": 43
"column": 13
}
}
},

@ -26,13 +26,11 @@
"loc": {
"start": {
"line": 4,
"column": 10,
"character": 40
"column": 10
},
"end": {
"line": 4,
"column": 17,
"character": 47
"column": 17
}
}
},
@ -161,13 +159,11 @@
"loc": {
"start": {
"line": 8,
"column": 10,
"character": 104
"column": 10
},
"end": {
"line": 8,
"column": 25,
"character": 119
"column": 25
}
}
},

@ -152,13 +152,11 @@
"loc": {
"start": {
"line": 9,
"column": 10,
"character": 63
"column": 10
},
"end": {
"line": 9,
"column": 10,
"character": 63
"column": 10
}
}
},
@ -195,13 +193,11 @@
"loc": {
"start": {
"line": 12,
"column": 10,
"character": 87
"column": 10
},
"end": {
"line": 12,
"column": 13,
"character": 90
"column": 13
}
}
},

@ -54,13 +54,11 @@
"loc": {
"start": {
"line": 1,
"column": 6,
"character": 6
"column": 6
},
"end": {
"line": 1,
"column": 6,
"character": 6
"column": 6
}
}
}
@ -832,13 +830,11 @@
"loc": {
"start": {
"line": 15,
"column": 16,
"character": 197
"column": 16
},
"end": {
"line": 15,
"column": 20,
"character": 201
"column": 20
}
}
},
@ -906,7 +902,7 @@
"expression": {
"type": "MemberExpression",
"start": 226,
"end": 231,
"end": 233,
"loc": {
"start": {
"line": 17,
@ -914,7 +910,7 @@
},
"end": {
"line": 17,
"column": 12
"column": 14
}
},
"object": {
@ -936,7 +932,7 @@
"property": {
"type": "Identifier",
"start": 231,
"end": 231,
"end": 233,
"loc": {
"start": {
"line": 17,
@ -944,36 +940,27 @@
},
"end": {
"line": 17,
"column": 12
"column": 14
}
},
"name": ""
"name": "as"
},
"computed": false,
"optional": false
},
"body": {
"type": "Fragment",
"nodes": []
},
"context": {
"type": "Identifier",
"name": "item",
"nodes": [
{
"type": "Text",
"start": 234,
"end": 238,
"loc": {
"start": {
"line": 17,
"column": 15,
"character": 234
},
"end": {
"line": 17,
"column": 19,
"character": 238
}
}
"end": 239,
"raw": "item}",
"data": "item}"
}
]
},
"context": null
},
{
"type": "Text",
@ -1058,7 +1045,7 @@
"expression": {
"type": "MemberExpression",
"start": 277,
"end": 280,
"end": 284,
"loc": {
"start": {
"line": 21,
@ -1066,7 +1053,7 @@
},
"end": {
"line": 21,
"column": 11
"column": 15
}
},
"object": {
@ -1088,7 +1075,7 @@
"property": {
"type": "Identifier",
"start": 280,
"end": 280,
"end": 284,
"loc": {
"start": {
"line": 21,
@ -1096,38 +1083,29 @@
},
"end": {
"line": 21,
"column": 11
"column": 15
}
},
"name": ""
"name": "then"
},
"computed": false,
"optional": false
},
"value": {
"type": "Identifier",
"name": "y",
"start": 285,
"end": 286,
"loc": {
"start": {
"line": 21,
"column": 16,
"character": 285
},
"end": {
"line": 21,
"column": 17,
"character": 286
}
}
},
"value": null,
"error": null,
"pending": null,
"then": {
"pending": {
"type": "Fragment",
"nodes": []
"nodes": [
{
"type": "Text",
"start": 285,
"end": 287,
"raw": "y}",
"data": "y}"
}
]
},
"then": null,
"catch": null
},
{
@ -1144,7 +1122,7 @@
"expression": {
"type": "MemberExpression",
"start": 305,
"end": 308,
"end": 313,
"loc": {
"start": {
"line": 23,
@ -1152,7 +1130,7 @@
},
"end": {
"line": 23,
"column": 11
"column": 16
}
},
"object": {
@ -1174,7 +1152,7 @@
"property": {
"type": "Identifier",
"start": 308,
"end": 308,
"end": 313,
"loc": {
"start": {
"line": 23,
@ -1182,39 +1160,30 @@
},
"end": {
"line": 23,
"column": 11
"column": 16
}
},
"name": ""
"name": "catch"
},
"computed": false,
"optional": false
},
"value": null,
"error": {
"type": "Identifier",
"name": "y",
"error": null,
"pending": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 314,
"end": 315,
"loc": {
"start": {
"line": 23,
"column": 17,
"character": 314
},
"end": {
"line": 23,
"column": 18,
"character": 315
}
"end": 316,
"raw": "y}",
"data": "y}"
}
]
},
"pending": null,
"then": null,
"catch": {
"type": "Fragment",
"nodes": []
}
"catch": null
}
]
},

@ -26,13 +26,11 @@
"loc": {
"start": {
"line": 3,
"column": 10,
"character": 39
"column": 10
},
"end": {
"line": 3,
"column": 13,
"character": 42
"column": 13
}
}
},

Loading…
Cancel
Save