Which functions handle template expression parsing?
I'll help you find the functions that handle template expression parsing in the Svelte codebase. Let me search for relevant code.
Tool Call: Search files for regex template.*expression|expression.*template|parse.*expression
Status: Completed
Showing matches 1-20 (there were more matches found; use offset: 20 to see next page):
> [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `<h1>hello {name}!</h1>` updates when `name` changes.
Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
## Matches in svelte/packages/svelte/CHANGELOG-pre-5.md
### L911-913
- Support
import.metain template expressions (#5422)
### L957-958
- Fix handling of
thisin inline function expressions in the template (#5033) - Fix collapsing HTML with static content (#5040)
### L1598-1600
- Support dynamic
import()in template expressions
## Matches in svelte/packages/svelte/CHANGELOG.md
### L43-45
- fix: use compiler-driven reactivity in legacy mode template expressions (#16100)
### L599-601
- fix: never deduplicate expressions in templates (#15451)
### L790-792
-
fix: optimise || expressions in template (#15092)
-
fix: correctly handle
novalidateattribute casing (#15083)
### L810-812
-
fix: omit unnecessary nullish coallescing in template expressions (#15056)
-
fix: more efficient template effect grouping (#15050)
### L1266-1268
-
fix: ignore text and expressions outside the template when validating HTML (#14468)
-
fix: better account for render tags when pruning CSS (#14456)
### L1316-1318
- fix: remove template expression inlining (#14374)
### L4312-4314
- fix: improve template literal expression output generation (#10147)
## Matches in svelte/packages/svelte/messages/client-errors/errors.md
### L85-86
Updating state inside
$derived(...),$inspect(...)or a template expression is forbidden. If the value should not be reactive, declare it without$state
## Matches in svelte/packages/svelte/src/compiler/phases/1-parse/acorn.js
### function parse_expression_at( ) › L60-70
export function parse_expression_at(source, typescript, index) { const parser = typescript ? ParserWithTS : acorn.Parser; const { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
4 lines remaining in ancestor node. Read the file to see all.
## Matches in svelte/packages/svelte/src/compiler/phases/1-parse/read/context.js
### L1-11
/** @import { Location } from 'locate-character' / /* @import { Pattern } from 'estree' / /* @import { Parser } from '../index.js' */ import { match_bracket } from '../utils/bracket.js'; import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; import { locator } from '../../../state.js';
/**
- @param {Parser} parser
110 lines remaining in ancestor node. Read the file to see all.
### function read_pattern( ) › L61-63
const expression = /** @type {any} */ (
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
).left;
### function read_type_annotation( ) › L97-101
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it
### function read_type_annotation( ) › L102-106
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.ts, a);
}
### function read_type_annotation( ) › L111-115
}
parser.index = /** @type {number} */ (expression.end);
return {
type: 'TSTypeAnnotation',
## Matches in svelte/packages/svelte/src/compiler/phases/1-parse/read/expression.js
### L1-11
/** @import { Expression } from 'estree' / /* @import { Parser } from '../index.js' */ import { parse_expression_at } from '../acorn.js'; import { regex_whitespace } from '../../patterns.js'; import * as e from '../../../errors.js'; import { find_matching_bracket } from '../utils/bracket.js';
/**
- @param {Parser} parser
- @param {string} [opening_token]
- @returns {Expression | undefined}
71 lines remaining in ancestor node. Read the file to see all.
## Assistant
Let me search more specifically for template-related parsing functions:
**Tool Call: Search files for regex `function.*parse.*template|parse.*template.*function`**
Status: Completed
Found 1 matches:
## Matches in svelte/packages/svelte/src/compiler/phases/1-parse/index.js
### function parse( ) › L301-304
export function parse(template, loose = false) { const parser = new Parser(template, loose); return parser.root; }
## Assistant
Let me look at expression parsing more specifically and template-related files:
**Tool Call: No matches**
Status: Completed
```json
{"offset":0,"current_matches_page":[],"all_matches_len":0}
Tool Call: 17 matches Status: Completed
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/acorn.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/index.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/read/context.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/read/expression.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/read/options.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/read/script.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/read/style.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/state/element.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/state/fragment.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/state/tag.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/state/text.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/utils/create.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/utils/entities.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
file:///Users/faraaz/oss/svelte/packages/svelte/src/compiler/phases/1-parse/utils/html.js
Now let me examine the key files that handle template expression parsing:
Tool Call: expression.js Status: Completed
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
/**
* @param {Parser} parser
* @param {string} [opening_token]
* @returns {Expression | undefined}
*/
export function get_loose_identifier(parser, opening_token) {
// Find the next } and treat it as the end of the expression
const end = find_matching_bracket(parser.template, parser.index, opening_token ?? '{');
if (end) {
const start = parser.index;
parser.index = end;
// We don't know what the expression is and signal this by returning an empty identifier
return {
type: 'Identifier',
start,
end,
name: ''
};
}
}
/**
* @param {Parser} parser
* @param {string} [opening_token]
* @param {boolean} [disallow_loose]
* @returns {Expression}
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
const node = parse_expression_at(parser.template, parser.ts, parser.index);
let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
let index = /** @type {number} */ (node.end);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).end;
}
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
if (parser.loose && !disallow_loose) {
const expression = get_loose_identifier(parser, opening_token);
if (expression) {
return expression;
}
}
parser.acorn_error(err);
}
}Tool Call: fragment.js Status: Completed
/** @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;
}Tool Call: tag.js Status: Completed
This file was too big to read all at once.
Here is an outline of its symbols:
const regex_whitespace_with_closing_curly_brace [L13]
const pointy_bois [L15]
function tag( ) [L18-48]
function open( ) [L51-434]
TSAsExpression( ) [L137-145]
function next( ) [L437-548]
function close( ) [L551-619]
function special( ) [L622-754]
Showing symbols 1-8 (total symbols: 8)
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
to search the file for specific content.Tool Call: tag.js Status: Completed
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { create_expression_metadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
const pointy_bois = { '<': '>' };
/** @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 expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
});
}
/** @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: create_expression_metadata()
}
});
parser.allow_whitespace();
parser.eat('}', true);
parser.stack.push(block);
parser.fragments.push(block.consequent);
return;
}
if (parser.eat('each')) {
parser.require_whitespace();
const template = parser.template;
let end = parser.template.length;
/** @type {Expression | undefined} */
let expression;
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
// the `as { y = z }` is treated as an Expression but it's actually a Pattern.
// the 'fix' is to backtrack and hide everything from the `as` onwards, until
// we get a valid expression
while (!expression) {
try {
expression = read_expression(parser, undefined, true);
} catch (err) {
end = /** @type {any} */ (err).position[0] - 2;
while (end > start && parser.template.slice(end, end + 2) !== 'as') {Tool Call: tag.js Status: Completed
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 expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
});
}Tool Call: index.js Status: Completed
/** @import { AST } from '#compiler' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser {
/**
* @readonly
* @type {string}
*/
template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/**
* Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible
* @type {boolean}
*/
loose;
/** */
index = 0;
/** Whether we're parsing in TypeScript mode */
ts = false;
/** @type {AST.TemplateNode[]} */
stack = [];
/** @type {AST.Fragment[]} */
fragments = [];
/** @type {AST.Root} */
root;Let me also check the main parsing entry point and look for more template expression handling:
Tool Call: Search files for regex ExpressionTag|MustacheTag (case-sensitive)
Status: Completed
Showing matches 1-20 (there were more matches found; use offset: 20 to see next page):
};
},
ExpressionTag(node, { path }) {
const parent = path.at(-1);
if (parent?.type === 'Attribute') {
return {
type: 'MustacheTag',
start: node.start,
end: node.end,
expression: node.expression
};
HtmlTag(node) {
return { ...node, type: 'RawMustacheTag' };
},
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'
? {
8 lines remaining in ancestor node. Read the file to see all.
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();
29 lines remaining in ancestor node. Read the file to see all.
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: {
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('=')) {
} 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;
}
/**
* @param {Parser} parser
* @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
*/
}
/** @type {Array<AST.ExpressionTag | AST.Text>} */
let value;
try {
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>} */
68 lines remaining in ancestor node. Read the file to see all.
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const chunk = {
type: 'ExpressionTag',
start: index,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
};
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
});
/**
* @param {AST.Text | AST.ExpressionTag} chunk
* @param {boolean} is_class
* @returns {string[] | null}
*/
import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
ExportSpecifier,
ExpressionStatement,
ExpressionTag,
FunctionDeclaration,
FunctionExpression,
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
continue;
}
}
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
/**
* @param {AST.ExpressionTag} node
* @param {Context} context
*/
export function ExpressionTag(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
if (in_template && context.state.parent_element) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
// TODO ideally we wouldn't do this here, we'd just do it on encountering
5 lines remaining in ancestor node. Read the file to see all.
Now let me look at the acorn.js file which seems to be the core expression parsing:
Tool Call: acorn.js Status: Completed
/** @import { Comment, Program } from 'estree' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @param {string} source
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
// component instead, so we need to ensure that Acorn doesn't throw
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
return v;
};
}
let ast;
try {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
* @param {string} source
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return ast;
}
/**
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
* to add them after the fact. They 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.
* @param {string} source
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
*/
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
walk(ast, null, {
_(node, { next, path }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = /** @type {CommentWithLocation} */ (comments.shift());
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const parent = /** @type {any} */ (path.at(-1));
if (parent === undefined || node.end !== parent.end) {
const slice = source.slice(node.end, comments[0].start);
const is_last_in_body =
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
parent.body.indexOf(node) === parent.body.length - 1) ||
(parent?.type === 'ArrayExpression' &&
parent.elements.indexOf(node) === parent.elements.length - 1) ||
(parent?.type === 'ObjectExpression' &&
parent.properties.indexOf(node) === parent.properties.length - 1);
if (is_last_in_body) {
// Special case: There can be multiple trailing comments after the last node in a block,
// and they can be separated by newlines
let end = node.end;
while (comments.length) {
const comment = comments[0];
if (parent && comment.start >= parent.end) break;
(node.trailingComments ||= []).push(comment);
comments.shift();
end = comment.end;
}
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
}
}
}
}
});
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
// Adding them ensures that we can later detect the end of the expression tag correctly.
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
(ast.trailingComments ||= []).push(...comments.splice(0));
}
}
};
}Now let me look at the ExpressionTag visitor:
Tool Call: ExpressionTag.js Status: Completed
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.ExpressionTag} node
* @param {Context} context
*/
export function ExpressionTag(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
if (in_template && context.state.parent_element) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
// TODO ideally we wouldn't do this here, we'd just do it on encountering
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}Let me also check how attributes with expressions are parsed:
Tool Call: element.js Status: Completed
/**
* @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) {Based on my exploration of the Svelte compiler codebase, here are the main functions that handle template expression parsing:
This is the main entry point for parsing template expressions. When the parser encounters a { character, it calls this function which:
- Handles different types of tags (
#if,:else,@debug, etc.) - For simple expressions like
{variable}, it callsread_expression()and creates anExpressionTagnode
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 expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
});
}
2. read_expression() function - svelte/packages/svelte/src/compiler/phases/1-parse/read/expression.js
This function parses individual JavaScript expressions within template tags:
- Uses Acorn parser via
parse_expression_at() - Handles parentheses balancing
- Provides fallback for loose parsing mode
- Manages error handling and recovery
export default function read_expression(parser, opening_token, disallow_loose) {
try {
const node = parse_expression_at(parser.template, parser.ts, parser.index);
let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
let index = /** @type {number} */ (node.end);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).end;
}
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
if (parser.loose && !disallow_loose) {
const expression = get_loose_identifier(parser, opening_token);
if (expression) {
return expression;
}
}
parser.acorn_error(err);
}
}
This is the low-level expression parser that uses the Acorn JavaScript parser:
- Handles TypeScript parsing when needed
- Adds comment handling for
svelte-ignoredirectives - Returns proper ESTree AST nodes
export function parse_expression_at(source, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return ast;
}
This function handles parsing sequences that contain both text and expressions (like attribute values):
- Parses mixed content like
"hello {name}!" - Creates both
TextandExpressionTagnodes - Used primarily for attribute values that can contain expressions
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 = [];
// ... (parsing logic that alternates between text and expressions)
// When encountering { in the sequence:
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()
}
};
}
fragment()detects{and callstag()tag()determines the tag type and for expressions callsread_expression()read_expression()usesparse_expression_at()to parse the JavaScriptparse_expression_at()uses Acorn parser to create proper AST nodes- The result is an
ExpressionTagnode in the Svelte AST
The system handles both simple template expressions like {variable} and complex expressions in attributes like class="hello {name}!" through this layered parsing approach.