Skip to content

Instantly share code, notes, and snippets.

@faraazahmad
Created September 8, 2025 07:20
Show Gist options
  • Select an option

  • Save faraazahmad/9df86ed5bd6f70ef0d6602101d47d22d to your computer and use it in GitHub Desktop.

Select an option

Save faraazahmad/9df86ed5bd6f70ef0d6602101d47d22d to your computer and use it in GitHub Desktop.

User

Which functions handle template expression parsing?

Assistant

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):

Matches in svelte/documentation/docs/02-runes/04-$effect.md

L41-42

> [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `<h1>hello {name}!</h1>` updates when `name` changes.

Matches in svelte/documentation/docs/98-reference/.generated/client-errors.md

L128-129

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.meta in template expressions (#5422)

3.25.1


### L957-958
  • Fix handling of this in inline function expressions in the template (#5033)
  • Fix collapsing HTML with static content (#5040)

### L1598-1600
  • Support dynamic import() in template expressions

2.13.3


## Matches in svelte/packages/svelte/CHANGELOG.md

### L43-45
  • fix: use compiler-driven reactivity in legacy mode template expressions (#16100)

5.34.3


### L599-601
  • fix: never deduplicate expressions in templates (#15451)

5.22.3


### L790-792
  • fix: optimise || expressions in template (#15092)

  • fix: correctly handle novalidate attribute 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)

5.2.5


### L4312-4314
  • fix: improve template literal expression output generation (#10147)

5.0.0-next.32


## 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

Assistant

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;

Assistant

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):

Matches in svelte/packages/svelte/src/compiler/legacy.js

function convert( ) › ExpressionTag( ) › L301-305

				};
			},
			ExpressionTag(node, { path }) {
				const parent = path.at(-1);
				if (parent?.type === 'Attribute') {

function convert( ) › ExpressionTag( ) › L316-321

				return {
					type: 'MustacheTag',
					start: node.start,
					end: node.end,
					expression: node.expression
				};

function convert( ) › HtmlTag( ) › L323-325

			HtmlTag(node) {
				return { ...node, type: 'RawMustacheTag' };
			},

Matches in svelte/packages/svelte/src/compiler/phases/1-parse/state/element.js

function element( ) › L277-287

		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( ) › L451-461

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.

function read_attribute( ) › L562-575

			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: {

function read_attribute( ) › L592-596

	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('=')) {

function read_attribute( ) › L651-655

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

L714-717

/**
 * @param {Parser} parser
 * @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
 */

function read_attribute_value( ) › L730-734

	}

	/** @type {Array<AST.ExpressionTag | AST.Text>} */
	let value;
	try {

function read_sequence( ) › L777-787

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.

function read_sequence( ) › L823-834

			parser.eat('}', true);

			/** @type {AST.ExpressionTag} */
			const chunk = {
				type: 'ExpressionTag',
				start: index,
				end: parser.index,
				expression,
				metadata: {
					expression: create_expression_metadata()
				}
			};

Matches in svelte/packages/svelte/src/compiler/phases/1-parse/state/tag.js

function tag( ) › L39-47

	parser.append({
		type: 'ExpressionTag',
		start,
		end: parser.index,
		expression,
		metadata: {
			expression: create_expression_metadata()
		}
	});

Matches in svelte/packages/svelte/src/compiler/phases/2-analyze/css/utils.js

L80-84

/**
 * @param {AST.Text | AST.ExpressionTag} chunk
 * @param {boolean} is_class
 * @returns {string[] | null}
 */

Matches in svelte/packages/svelte/src/compiler/phases/2-analyze/index.js

L35-39

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';

const visitors › L152-156

	ExportSpecifier,
	ExpressionStatement,
	ExpressionTag,
	FunctionDeclaration,
	FunctionExpression,

Matches in svelte/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js

function Attribute( ) › L50-59

		for (const chunk of get_attribute_chunks(node.value)) {
			if (chunk.type !== 'ExpressionTag') continue;

			if (
				chunk.expression.type === 'FunctionExpression' ||
				chunk.expression.type === 'ArrowFunctionExpression'
			) {
				continue;
			}
		}

function get_delegated_event( ) › L136-140

				} else if (
					parent.type === 'ExpressionTag' &&
					grandparent?.type === 'Attribute' &&
					is_event_attribute(grandparent)
				) {

Matches in svelte/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js

L7-10

/**
 * @param {AST.ExpressionTag} node
 * @param {Context} context
 */

function ExpressionTag( ) › L11-21

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.

Assistant

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

Assistant

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

Assistant

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) {

Assistant

Based on my exploration of the Svelte compiler codebase, here are the main functions that handle template expression parsing:

Primary Template Expression Parsing Functions

1. tag() function - svelte/packages/svelte/src/compiler/phases/1-parse/state/tag.js

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 calls read_expression() and creates an ExpressionTag node
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);
	}
}

3. parse_expression_at() function - svelte/packages/svelte/src/compiler/phases/1-parse/acorn.js

This is the low-level expression parser that uses the Acorn JavaScript parser:

  • Handles TypeScript parsing when needed
  • Adds comment handling for svelte-ignore directives
  • 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;
}

4. read_sequence() function - svelte/packages/svelte/src/compiler/phases/1-parse/state/element.js

This function handles parsing sequences that contain both text and expressions (like attribute values):

  • Parses mixed content like "hello {name}!"
  • Creates both Text and ExpressionTag nodes
  • 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()
		}
	};
}

Flow Summary

  1. fragment() detects { and calls tag()
  2. tag() determines the tag type and for expressions calls read_expression()
  3. read_expression() uses parse_expression_at() to parse the JavaScript
  4. parse_expression_at() uses Acorn parser to create proper AST nodes
  5. The result is an ExpressionTag node 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment