use crate::ast::*;
use crate::lexer::TokenKind;
use crate::parser::element::{ElementParent, ParsedElement};
use crate::whitespace::is_svelte_ws;
use bumpalo::Bump;
use tsv_lang::source_scan::{TriviaProfile, skip_trivia_run};
use tsv_lang::{Error, Span};
use tsv_ts::__private::TopLevelAs;
use tsv_ts::ast::{Expression, ExpressionKind};
use super::expression_tag::{scan_head_to_matching_brace, scan_to_matching_paren};
use super::parser_impl::{EmbeddedParseMark, SvelteParser};
use super::{match_bracket, subslice_offset};
fn strip_head_keyword<'s>(rest: &'s str, keyword: &str) -> Option<&'s str> {
let after_kw = rest
.trim_start_matches(is_svelte_ws)
.strip_prefix(keyword)?;
let value = after_kw.trim_start_matches(is_svelte_ws);
(value.len() < after_kw.len()).then_some(value)
}
fn continuation_words(keyword: &str) -> impl Iterator<Item = &str> {
keyword.split(is_svelte_ws).filter(|w| !w.is_empty())
}
fn strip_head_keyword_or_final<'s>(rest: &'s str, keyword: &str) -> Option<&'s str> {
strip_head_keyword(rest, keyword).or_else(|| {
let after_kw = rest
.trim_start_matches(is_svelte_ws)
.strip_prefix(keyword)?;
after_kw
.trim_start_matches(is_svelte_ws)
.is_empty()
.then_some("")
})
}
#[derive(Clone, Copy)]
enum AwaitClause {
Then,
Catch,
}
impl AwaitClause {
fn keyword(self) -> &'static str {
match self {
Self::Then => "then",
Self::Catch => "catch",
}
}
}
const AWAIT_BODY_STOPS: &[&str] = &["then", "catch", "await"];
type AwaitSlot<'arena> = (Option<Fragment<'arena>>, Option<&'arena Expression<'arena>>);
const ASSERTION_KEYWORDS: [(&str, bool); 2] = [("as", true), ("satisfies", false)];
fn skip_ws_and_comments(s: &str) -> &str {
&s[skip_trivia_run(s, 0, TriviaProfile::COMMENTS, is_svelte_ws)..]
}
#[derive(Debug, PartialEq, Eq)]
enum EachHeadSplit {
FirstAs,
LaterAs { separator: usize, binding: usize },
NoBinding { run_end: usize },
}
fn each_binding_separator(
s: &str,
s_offset: usize,
host_nesting: u32,
arena: &Bump,
) -> EachHeadSplit {
let offset_of = |part: &str| s_offset + subslice_offset(s, part);
let mut separator = None;
let mut run_end = None;
let mut linked = false;
let mut item = s;
loop {
let item_at = offset_of(item);
let Ok(type_end) = tsv_ts::__private::parse_type_extent(item, item_at, host_nesting, arena)
else {
break;
};
let after_type = skip_ws_and_comments(&item[type_end - item_at..]);
let Some((is_separator, rest)) =
ASSERTION_KEYWORDS.iter().find_map(|&(kw, is_separator)| {
strip_head_keyword(after_type, kw).map(|rest| (is_separator, rest))
})
else {
run_end = Some(type_end);
break;
};
separator = is_separator.then(|| (offset_of(after_type), offset_of(rest)));
linked = true;
item = rest;
}
match (separator, run_end) {
(Some((separator, binding)), _) => EachHeadSplit::LaterAs { separator, binding },
(None, Some(run_end)) if linked => EachHeadSplit::NoBinding { run_end },
_ => EachHeadSplit::FirstAs,
}
}
fn await_head_clause(after_expr: &str) -> Option<(AwaitClause, &str)> {
if let Some(value_str) = strip_head_keyword_or_final(after_expr, "then") {
return Some((AwaitClause::Then, value_str));
}
strip_head_keyword_or_final(after_expr, "catch")
.map(|error_str| (AwaitClause::Catch, error_str))
}
fn snippet_signature<'arena>(
program: &tsv_ts::ast::Program<'arena>,
) -> Option<&'arena tsv_ts::ast::FunctionDeclaration<'arena>> {
match program.body.first().map(|stmt| &stmt.kind) {
Some(tsv_ts::ast::StatementKind::FunctionDeclaration(func)) => Some(*func),
_ => None,
}
}
type EachBindingResult<'arena> = (
&'arena Expression<'arena>,
Option<&'arena str>,
Option<EachKey<'arena>>,
usize,
);
type IndexAndKeyResult<'arena> = (Option<&'arena str>, Option<EachKey<'arena>>, usize);
struct EachIndexAndKey<'s> {
index: Option<&'s str>,
key: Option<EachKeyRegion<'s>>,
consumed_end: usize,
}
struct EachKeyRegion<'s> {
expr_str: &'s str,
expr_offset: usize,
span: Span,
}
impl<'a, 'arena> SvelteParser<'a, 'arena> {
pub(crate) fn parse_block(&mut self) -> Result<FragmentNode<'arena>, Error> {
self.nested(2, Self::parse_block_body)
}
fn parse_block_body(&mut self) -> Result<FragmentNode<'arena>, Error> {
let start = self.current_start();
if !self.check(TokenKind::BlockOpen) {
return Err(self.error_expected_found("'{#'"));
}
let keyword = self.keyword_at(self.current_end());
match keyword {
"if" => self.parse_if_block(start),
"each" => self.parse_each_block(start),
"await" => self.parse_await_block(start),
"key" => self.parse_key_block(start),
"snippet" => self.parse_snippet_block(start),
_ => Err(self.error_unknown_at("block type", &format!("{{#{keyword}}}"), start)),
}
}
fn parse_if_block(&mut self, start: usize) -> Result<FragmentNode<'arena>, Error> {
self.parse_if_block_inner(start, false)
}
fn parse_if_block_inner(
&mut self,
start: usize,
is_elseif: bool,
) -> Result<FragmentNode<'arena>, Error> {
let tag_content_start = self.current_end();
let (expr_content, content_start) = self.scan_block_tag_content(tag_content_start)?;
let expr_str = if is_elseif {
let after_else = self
.strip_dispatched_keyword(expr_content, "else", tag_content_start)?
.trim_start_matches(is_svelte_ws);
self.strip_block_keyword(after_else, "if", tag_content_start)?
.trim_start_matches(is_svelte_ws)
} else {
self.strip_block_keyword(expr_content, "if", tag_content_start)?
.trim_start_matches(is_svelte_ws)
};
let expr_offset = tag_content_start + subslice_offset(expr_content, expr_str);
let test = self.parse_ts_expression(expr_str, expr_offset, "'}'")?;
let opening_tag_span = Span {
start: start as u32,
end: content_start as u32,
};
let consequent = self.parse_block_children(&["else", "if"], content_start)?;
let alternate = if self.check(TokenKind::BlockContinue) {
let keyword = self.continuation_keyword_at(self.current_end());
let mut words = continuation_words(keyword);
let first = words.next();
let second = words.next();
let is_else_if = first == Some("else") && second == Some("if");
let is_else = first == Some("else") && second.is_none();
if is_else_if {
let elseif_start = self.current_start();
let elseif_block =
self.nested(2, |p| p.parse_if_block_inner(elseif_start, true))?;
let mut nodes = self.bvec();
nodes.push(elseif_block);
Some(Fragment {
nodes: nodes.into_bump_slice(),
})
} else if is_else {
let else_tag_start = self.current_end();
let (else_tag_content, else_content_start) =
self.scan_block_tag_content(else_tag_start)?; let after_else =
self.strip_dispatched_keyword(else_tag_content, "else", else_tag_start)?;
self.reject_trailing_tag_content(after_else, else_tag_start + "else".len())?;
let else_content = self.parse_block_children(&["if"], else_content_start)?;
self.reject_duplicate_else()?;
Some(else_content)
} else {
None
}
} else {
None
};
let elseif_end = alternate.as_ref().and_then(|alt| {
if let Some(FragmentNode::IfBlock(inner)) = alt.nodes.first()
&& inner.elseif
{
return Some(inner.span.end as usize);
}
None
});
let end = if let Some(end_pos) = elseif_end {
end_pos
} else {
self.expect_block_close_keyword("if", start)?
};
Ok(FragmentNode::IfBlock(IfBlock {
elseif: is_elseif,
test,
consequent,
alternate,
span: Span {
start: start as u32,
end: end as u32,
},
opening_tag_span,
}))
}
fn reparse_each_iterable(
&mut self,
iterable: &str,
expr_offset: usize,
mark: EmbeddedParseMark,
) -> Result<&'arena Expression<'arena>, Error> {
self.rewind_embedded_parses(mark);
let iterable = iterable.trim_start_matches(is_svelte_ws);
self.parse_ts_expression_checked(iterable, expr_offset, "'}'", |this, expression| {
match super::tag::bare_sequence_comma(expression, iterable) {
Some(comma) => Err(this.error_expected_at("'as'", expr_offset + comma)),
None => Ok(()),
}
})
}
fn check_each_head_tail(
&self,
content: &str,
content_offset: usize,
expression: &Expression<'arena>,
expr_end_pos: usize,
) -> Result<(), Error> {
if strip_head_keyword(&content[expr_end_pos - content_offset..], "as").is_some() {
return Ok(());
}
let semantic_end = expression.span().end as usize;
let tail =
self.split_index_and_key(&content[semantic_end - content_offset..], semantic_end)?;
self.reject_trailing_tag_content(
&content[tail.consumed_end - content_offset..],
tail.consumed_end,
)
}
fn parse_each_block(&mut self, start: usize) -> Result<FragmentNode<'arena>, Error> {
let tag_content_start = self.current_end();
let (tag_content, content_start) = self.scan_block_tag_content(tag_content_start)?;
let content = self.strip_block_keyword(tag_content, "each", tag_content_start)?;
let content_offset = tag_content_start + subslice_offset(tag_content, content);
let mark_before_expr = self.embedded_parse_mark();
let expr_str = content.trim_start_matches(is_svelte_ws);
let expr_offset = content_offset + subslice_offset(content, expr_str);
let (expression, expr_end_pos) = self.parse_ts_expression_partial(
expr_str,
expr_offset,
TopLevelAs::HostSeparator,
|this, expression, end_pos| {
this.check_each_head_tail(content, content_offset, expression, end_pos)
},
)?;
let opening_tag_span = Span {
start: start as u32,
end: content_start as u32,
};
let after_expr = &content[expr_end_pos - content_offset..];
let mut expression = expression;
let binding = match strip_head_keyword(after_expr, "as") {
None => None,
Some(binding_str) => {
let binding_offset = content_offset + subslice_offset(content, binding_str);
match each_binding_separator(
binding_str,
binding_offset,
self.island_nesting(),
self.arena,
) {
EachHeadSplit::FirstAs => Some((binding_str, binding_offset)),
EachHeadSplit::LaterAs { separator, binding } => {
let iterable = &content[..separator - content_offset];
expression =
self.reparse_each_iterable(iterable, expr_offset, mark_before_expr)?;
Some((&content[binding - content_offset..], binding))
}
EachHeadSplit::NoBinding { run_end } => {
let iterable = &content[..run_end - content_offset];
expression =
self.reparse_each_iterable(iterable, expr_offset, mark_before_expr)?;
None
}
}
}
};
let (context, index, key, binding_end) = match binding {
Some((binding_str, binding_offset)) => {
let (ctx, idx, k, b_end) = self.parse_each_binding(binding_str, binding_offset)?;
(Some(ctx), idx, k, b_end)
}
None => {
let semantic_end = expression.span().end as usize;
let after_semantic = &content[semantic_end - content_offset..];
let (idx, k, b_end) =
self.parse_index_and_key_after_context(after_semantic, semantic_end)?;
(None, idx, k, b_end)
}
};
let brace_pos = content_start - 1;
self.reject_trailing_tag_content(&self.source[binding_end..brace_pos], binding_end)?;
let body = self.parse_block_children(&["else", "each"], content_start)?;
let fallback = if self.check(TokenKind::BlockContinue)
&& self.continuation_keyword_at(self.current_end()) == "else"
{
let else_tag_start = self.current_end();
let (else_tag_content, else_content_start) =
self.scan_block_tag_content(else_tag_start)?; let after_else =
self.strip_dispatched_keyword(else_tag_content, "else", else_tag_start)?;
self.reject_trailing_tag_content(after_else, else_tag_start + "else".len())?;
let fallback_content = self.parse_block_children(&["each"], else_content_start)?;
self.reject_duplicate_else()?;
Some(fallback_content)
} else {
None
};
let end = self.expect_block_close_keyword("each", start)?;
Ok(FragmentNode::EachBlock(EachBlock {
expression,
context,
index,
key,
body,
fallback,
span: Span {
start: start as u32,
end: end as u32,
},
opening_tag_span,
}))
}
fn parse_each_binding(
&mut self,
binding: &str,
binding_offset: usize,
) -> Result<EachBindingResult<'arena>, Error> {
let leading_ws = binding.len() - binding.trim_start_matches(is_svelte_ws).len();
let trimmed = binding.trim_matches(is_svelte_ws);
let adjusted_offset = binding_offset + leading_ws;
let (context, pattern_end) = self.parse_block_pattern(trimmed, adjusted_offset)?;
let consumed = pattern_end - adjusted_offset;
let remaining = &trimmed[consumed..];
let (index, key, consumed_end) =
self.parse_index_and_key_after_context(remaining, pattern_end)?;
Ok((context, index, key, consumed_end))
}
pub(super) fn parse_block_pattern(
&mut self,
input: &str,
offset: usize,
) -> Result<(&'arena Expression<'arena>, usize), Error> {
let trimmed = input.trim_start_matches(is_svelte_ws);
let ws_len = input.len() - trimmed.len();
let adjusted = offset + ws_len;
let end = match trimmed.as_bytes().first() {
Some(&open @ (b'{' | b'[')) => self.matching_bracket_end(trimmed, open, adjusted)?,
_ => {
let Some(name) = self.read_identifier(trimmed, adjusted)? else {
return Err(
self.error_expected_at("identifier or destructure pattern", adjusted)
);
};
name.len()
}
};
let expr = self.parse_ts_pattern(&trimmed[..end], adjusted)?;
let after_pattern = trimmed[end..].trim_start_matches(is_svelte_ws);
if !after_pattern.starts_with(':') {
return Ok((expr, adjusted + end));
}
let ws_before_colon = trimmed.len() - end - after_pattern.len();
let colon_offset = adjusted + end + ws_before_colon;
let ta = self.parse_ts_type_annotation(after_pattern, colon_offset)?;
let annotation_end = ta.span.end as usize;
tsv_ts::__private::attach_pattern_type_annotation(expr, ta, self.arena)?;
Ok((expr, annotation_end))
}
fn matching_bracket_end(&self, input: &str, open: u8, offset: usize) -> Result<usize, Error> {
let close = if open == b'{' { b'}' } else { b']' };
let bytes = input.as_bytes();
match_bracket(bytes, 0, bytes.len(), open, close, TriviaProfile::JS)
.map(|close_pos| close_pos + 1) .ok_or_else(|| self.error_msg_at("Unmatched bracket", offset))
}
fn parse_index_and_key_after_context(
&mut self,
remaining: &str,
remaining_offset: usize,
) -> Result<IndexAndKeyResult<'arena>, Error> {
let tail = self.split_index_and_key(remaining, remaining_offset)?;
let index = tail.index.map(|name| self.alloc_str_in(name));
let key = match tail.key {
Some(region) => Some(EachKey {
expression: self.parse_ts_expression(region.expr_str, region.expr_offset, "')'")?,
span: region.span,
}),
None => None,
};
Ok((index, key, tail.consumed_end))
}
fn split_index_and_key<'s>(
&self,
remaining: &'s str,
remaining_offset: usize,
) -> Result<EachIndexAndKey<'s>, Error>
where
'a: 's,
{
let trimmed = remaining.trim_start_matches(is_svelte_ws);
let ws_len = remaining.len() - trimmed.len();
let offset = remaining_offset + ws_len;
let mut rest = trimmed;
let mut rest_offset = offset;
let mut index = None;
let mut consumed_end = remaining_offset;
if let Some(after_comma) = rest.strip_prefix(',') {
let after_comma_trimmed = after_comma.trim_start_matches(is_svelte_ws);
let comma_ws = after_comma.len() - after_comma_trimmed.len();
let idx_offset = offset + 1 + comma_ws;
if let Some(name) = self.read_identifier(after_comma_trimmed, idx_offset)? {
index = Some(name);
rest = &after_comma_trimmed[name.len()..];
rest_offset = idx_offset + name.len();
consumed_end = rest_offset;
}
}
let rest_trimmed = rest.trim_start_matches(is_svelte_ws);
let key = if rest_trimmed.starts_with('(') {
let key_ws = rest.len() - rest_trimmed.len();
let paren_start = rest_offset + key_ws; let close = scan_to_matching_paren(rest_trimmed.as_bytes(), 1)
.ok_or_else(|| self.error_expected_at("')'", paren_start + rest_trimmed.len()))?;
let key_str = &rest_trimmed[1..close];
let key_offset = paren_start + 1; let key_expr_str = key_str.trim_start_matches(is_svelte_ws);
let span = Span::new(paren_start as u32, (paren_start + close + 1) as u32);
consumed_end = span.end as usize;
Some(EachKeyRegion {
expr_str: key_expr_str,
expr_offset: key_offset + (key_str.len() - key_expr_str.len()),
span,
})
} else {
None
};
Ok(EachIndexAndKey {
index,
key,
consumed_end,
})
}
fn parse_await_block(&mut self, start: usize) -> Result<FragmentNode<'arena>, Error> {
let tag_content_start = self.current_end();
let (tag_content, content_start) = self.scan_block_tag_content(tag_content_start)?;
let content = self.strip_block_keyword(tag_content, "await", tag_content_start)?;
let content_offset = tag_content_start + subslice_offset(tag_content, content);
let expr_str = content.trim_start_matches(is_svelte_ws);
let (expression, expr_end_pos) = self.parse_ts_expression_partial(
expr_str,
content_offset + subslice_offset(content, expr_str),
TopLevelAs::Assertion,
|this, _, end_pos| {
let after_expr = &content[end_pos - content_offset..];
match await_head_clause(after_expr) {
Some(_) => Ok(()),
None => this.reject_trailing_tag_content(after_expr, end_pos),
}
},
)?;
let opening_tag_span = Span {
start: start as u32,
end: content_start as u32,
};
let expr_consumed = expr_end_pos - content_offset;
let after_expr = &content[expr_consumed..];
let head_clause = await_head_clause(after_expr);
let pending_block = head_clause.is_none();
let head_binding = match head_clause {
Some((_, s)) if !s.is_empty() => {
let keyword_end = expr_end_pos + subslice_offset(after_expr, s);
Some(self.parse_await_value_pattern(s, keyword_end)?)
}
_ => None,
};
let mut then_slot: AwaitSlot<'arena> = (None, None);
let mut catch_slot: AwaitSlot<'arena> = (None, None);
let mut pending = None;
let first_body = self.parse_block_children(AWAIT_BODY_STOPS, content_start)?;
match head_clause {
Some((AwaitClause::Then, _)) => then_slot = (Some(first_body), head_binding),
Some((AwaitClause::Catch, _)) => catch_slot = (Some(first_body), head_binding),
None => pending = (!first_body.nodes.is_empty()).then_some(first_body),
}
while let Some(clause) = [AwaitClause::Then, AwaitClause::Catch]
.into_iter()
.find(|c| self.check_await_continuation(c.keyword()))
{
let slot = match clause {
AwaitClause::Then => &mut then_slot,
AwaitClause::Catch => &mut catch_slot,
};
if slot.0.is_some() {
return Err(self.error_duplicate_clause(clause.keyword()));
}
*slot = self.parse_await_continuation(clause.keyword(), AWAIT_BODY_STOPS)?;
}
let (then_fragment, value) = then_slot;
let (catch_fragment, error) = catch_slot;
let end = self.expect_block_close_keyword("await", start)?;
Ok(FragmentNode::AwaitBlock(AwaitBlock {
expression,
value,
error,
pending,
pending_block,
then: then_fragment,
catch: catch_fragment,
span: Span {
start: start as u32,
end: end as u32,
},
opening_tag_span,
}))
}
fn reject_duplicate_else(&self) -> Result<(), Error> {
if !self.check(TokenKind::BlockContinue) {
return Ok(());
}
let mut words = continuation_words(self.continuation_keyword_at(self.current_end()));
if words.next() != Some("else") {
return Ok(());
}
Err(if words.next() == Some("if") {
self.error_clause_after("else if", "else")
} else {
self.error_duplicate_clause("else")
})
}
fn check_await_continuation(&self, keyword: &str) -> bool {
self.check(TokenKind::BlockContinue)
&& self
.continuation_keyword_at(self.current_end())
.starts_with(keyword)
}
fn parse_await_continuation(
&mut self,
keyword: &str,
stop_keywords: &[&str],
) -> Result<(Option<Fragment<'arena>>, Option<&'arena Expression<'arena>>), Error> {
let tag_start = self.current_end();
let (tag_content, content_start) = self.scan_block_tag_content(tag_start)?;
let rest = self.strip_keyword_value(tag_content, keyword, tag_start)?;
let binding = if rest.is_empty() {
None
} else {
let offset = tag_start + subslice_offset(tag_content, rest);
Some(self.parse_await_value_pattern(rest, offset)?)
};
let fragment = self.parse_block_children(stop_keywords, content_start)?;
Ok((Some(fragment), binding))
}
pub(super) fn keyword_at(&self, pos: usize) -> &'a str {
let remaining = &self.source[pos..];
let end = remaining
.find(|c: char| !c.is_alphabetic())
.unwrap_or(remaining.len());
&remaining[..end]
}
fn continuation_keyword_at(&self, pos: usize) -> &'a str {
let remaining = &self.source[pos..];
if remaining.starts_with(is_svelte_ws) {
return "";
}
let end = remaining
.find(|c: char| !c.is_alphabetic() && !is_svelte_ws(c))
.unwrap_or(remaining.len());
remaining[..end].trim_end_matches(is_svelte_ws)
}
fn strip_keyword_value(
&self,
content: &'a str,
keyword: &str,
keyword_start: usize,
) -> Result<&'a str, Error> {
let rest = self.strip_dispatched_keyword(content, keyword, keyword_start)?;
if rest.is_empty() || rest.starts_with(is_svelte_ws) {
Ok(rest)
} else {
Err(self.error_expected_at(&format!("whitespace after `{keyword}`"), keyword_start))
}
}
fn strip_dispatched_keyword(
&self,
content: &'a str,
keyword: &str,
keyword_start: usize,
) -> Result<&'a str, Error> {
content
.strip_prefix(keyword)
.ok_or_else(|| self.error_expected_at(&format!("`{keyword}`"), keyword_start))
}
pub(super) fn strip_block_keyword(
&self,
content: &'a str,
keyword: &str,
keyword_start: usize,
) -> Result<&'a str, Error> {
let rest = self.strip_keyword_value(content, keyword, keyword_start)?;
if rest.is_empty() {
return Err(
self.error_expected_at(&format!("whitespace after `{keyword}`"), keyword_start)
);
}
Ok(rest)
}
fn reject_trailing_tag_content(&self, region: &str, region_start: usize) -> Result<(), Error> {
let trailing = region.trim_start_matches(is_svelte_ws);
if !trailing.is_empty() {
let trailing_start = region_start + subslice_offset(region, trailing);
return Err(self.error_expected_at("'}'", trailing_start));
}
Ok(())
}
fn parse_await_value_pattern(
&mut self,
region: &str,
region_offset: usize,
) -> Result<&'arena Expression<'arena>, Error> {
let (pattern, binding_end) = self.parse_block_pattern(region, region_offset)?;
self.reject_trailing_tag_content(®ion[binding_end - region_offset..], binding_end)?;
Ok(pattern)
}
fn expect_block_close_keyword(
&mut self,
expected: &str,
block_start: usize,
) -> Result<usize, Error> {
if self.check(TokenKind::BlockContinue) {
return Err(self.error_misplaced_continuation(expected));
}
if !self.check(TokenKind::BlockClose) {
return Err(self.error_unclosed_at(&format!("{{#{expected}}} block"), block_start));
}
if self.keyword_at(self.current_end()) != expected {
return Err(self.error_expected_at(&format!("{{/{expected}}}"), self.current_start()));
}
let close_tag_start = self.current_end();
let (close_content, after_close) = self.scan_block_tag_content(close_tag_start)?;
self.reject_trailing_tag_content(
&close_content[expected.len()..],
close_tag_start + expected.len(),
)?;
Ok(after_close)
}
fn error_misplaced_continuation(&self, expected: &str) -> Error {
let clause = |keyword: &str| format!("{{:{keyword}}}");
let message = match expected {
"if" if self.source[self.current_end()..].starts_with("elseif") => {
"'elseif' should be 'else if'".to_owned()
}
"if" => format!("Expected token {} or {}", clause("else"), clause("else if")),
"each" => format!("Expected token {}", clause("else")),
"await" => format!(
"Expected token {} or {}",
clause("then ..."),
clause("catch ...")
),
_ => format!(
"{} block is invalid at this position (did you forget to close the preceding \
element or block?)",
clause("...")
),
};
self.error_msg_at(&message, self.current_start())
}
fn parse_key_block(&mut self, start: usize) -> Result<FragmentNode<'arena>, Error> {
let tag_content_start = self.current_end();
let (tag_content, content_start) = self.scan_block_tag_content(tag_content_start)?;
let expr_str = self
.strip_block_keyword(tag_content, "key", tag_content_start)?
.trim_start_matches(is_svelte_ws);
let expr_offset = tag_content_start + subslice_offset(tag_content, expr_str);
let expression = self.parse_ts_expression(expr_str, expr_offset, "'}'")?;
let opening_tag_span = Span {
start: start as u32,
end: content_start as u32,
};
let fragment = self.parse_block_children(&["key"], content_start)?;
let end = self.expect_block_close_keyword("key", start)?;
Ok(FragmentNode::KeyBlock(KeyBlock {
expression,
fragment,
span: Span {
start: start as u32,
end: end as u32,
},
opening_tag_span,
}))
}
fn parse_snippet_block(&mut self, start: usize) -> Result<FragmentNode<'arena>, Error> {
let tag_content_start = self.current_end();
let (tag_content, content_start) = self.scan_block_tag_content(tag_content_start)?;
let content = self
.strip_block_keyword(tag_content, "snippet", tag_content_start)?
.trim_matches(is_svelte_ws);
let content_bytes = content.as_bytes();
let content_offset = tag_content_start + subslice_offset(tag_content, content);
let Some(name_str) = self.read_identifier(content, content_offset)? else {
return Err(self.error_expected_at("snippet name", content_offset));
};
let name_len = name_str.len();
let expression =
self.parse_ts_expression(name_str, content_offset, "end of snippet name")?;
if !matches!(expression.kind, ExpressionKind::Identifier(_)) {
return Err(self.error_expected_at("snippet name", content_offset));
}
let after_name = content[name_len..].trim_start_matches(is_svelte_ws);
let head_start = content.len() - after_name.len();
let (after_generic, type_params_raw): (usize, Option<&'arena str>) =
if after_name.starts_with('<') {
let close_pos =
self.find_matching_angle_bracket(content, content_offset, head_start)?;
(
close_pos + 1,
Some(self.alloc_str_in(&content[head_start + 1..close_pos])),
)
} else {
(head_start, None)
};
let after_generic_str = &content[after_generic..];
let paren_pos = after_generic
+ (after_generic_str.len() - after_generic_str.trim_start_matches(is_svelte_ws).len());
if !content[paren_pos..].starts_with('(') {
return Err(self.error_expected_at("'('", content_offset + paren_pos));
}
let opening_tag_span = Span {
start: start as u32,
end: content_start as u32,
};
let close_paren = snippet_params_close(content_bytes, paren_pos)
.ok_or_else(|| self.error_expected_at("')'", content_offset + content.len()))?;
self.reject_trailing_tag_content(
&content[close_paren + 1..],
content_offset + close_paren + 1,
)?;
let params_paren = Some(Span {
start: (content_offset + paren_pos) as u32,
end: (content_offset + close_paren) as u32,
});
let params_str = &content[paren_pos + 1..close_paren];
let mut type_parameters: Option<tsv_ts::ast::TSTypeParameterDeclaration<'arena>> = None;
let mut parameters: &'arena [Expression<'arena>] = &[];
if type_params_raw.is_some() || !params_str.trim_matches(is_svelte_ws).is_empty() {
let head_slice = &content[head_start..=close_paren];
const WRAPPER_PREFIX: &str = "function f";
let wrapper = format!("{WRAPPER_PREFIX}{head_slice} {{}}");
let base = (content_offset + head_start).saturating_sub(WRAPPER_PREFIX.len());
let (preserved, preserved_a_paren) = tsv_ts::__private::parse_embedded_preserve_parens(
&wrapper,
base,
self.island_nesting(),
self.arena,
)?;
let signature_error =
|| self.error_expected_at("snippet signature", content_offset + head_start);
let (program, func) = if preserved_a_paren {
let plain = tsv_ts::__private::parse_embedded(
&wrapper,
base,
self.island_nesting(),
self.arena,
)?;
debug_assert!(
plain.comments.len() == preserved.comments.len()
&& plain
.comments
.iter()
.zip(preserved.comments)
.all(|(a, b)| a.span == b.span),
"a snippet head's paren-free and preserved parses collected different comments"
);
let wire = snippet_signature(&preserved)
.ok_or_else(signature_error)?
.params;
let func = snippet_signature(&plain).ok_or_else(signature_error)?;
self.snippet_wire_parameters.push(SnippetWireParameters {
snippet_start: start as u32,
parameters: wire,
});
(plain, func)
} else {
let func = snippet_signature(&preserved).ok_or_else(signature_error)?;
(preserved, func)
};
self.expression_comments.extend_from_slice(program.comments);
type_parameters.clone_from(&func.type_parameters);
parameters = func.params;
}
let body = self.parse_block_children(&["snippet"], content_start)?;
let end = self.expect_block_close_keyword("snippet", start)?;
Ok(FragmentNode::SnippetBlock(SnippetBlock {
expression,
type_parameters,
type_params_raw,
parameters,
params_paren,
body,
span: Span {
start: start as u32,
end: end as u32,
},
opening_tag_span,
}))
}
fn find_matching_angle_bracket(
&self,
content: &str,
content_offset: usize,
open_pos: usize,
) -> Result<usize, Error> {
match_bracket(
content.as_bytes(),
open_pos,
content.len(),
b'<',
b'>',
TriviaProfile::JS,
)
.ok_or_else(|| self.error_expected_at("'>'", content_offset + content.len()))
}
pub(super) fn scan_block_tag_content(
&mut self,
start: usize,
) -> Result<(&'a str, usize), Error> {
let Some(end) = scan_head_to_matching_brace(self.source.as_bytes(), start) else {
return Err(self.error_unclosed_at("block tag", start));
};
let content = &self.source[start..end];
let after_close = end + 1; self.advance_to_position(after_close)?;
Ok((content, after_close))
}
fn parse_block_children(
&mut self,
stop_keywords: &[&str],
content_start: usize,
) -> Result<Fragment<'arena>, Error> {
let mut nodes = self.bvec();
let mut last_end = content_start;
loop {
self.capture_text_if_gap(last_end, &mut nodes)?;
if self.check(TokenKind::Eof) {
break;
}
if self.check(TokenKind::BlockClose)
&& stop_keywords.contains(&self.keyword_at(self.current_end()))
{
break;
}
if self.check(TokenKind::BlockContinue) {
let keyword = self.continuation_keyword_at(self.current_end());
let should_stop = stop_keywords.iter().any(|sk| keyword.starts_with(sk));
if should_stop {
break;
}
}
if self.check(TokenKind::Comment) {
let comment = self.parse_comment()?;
last_end = comment.span.end_usize();
nodes.push(FragmentNode::Comment(comment));
} else if self.check(TokenKind::LeftAngle) {
if self.is_next_token(TokenKind::Slash)? {
break;
}
match self.parse_element_or_special(ElementParent::Nested)? {
ParsedElement::Element(elem) => {
last_end = elem.span.end_usize();
nodes.push(FragmentNode::Element(elem));
}
ParsedElement::SpecialElement(elem) => {
last_end = elem.span.end_usize();
nodes.push(FragmentNode::SpecialElement(elem));
}
}
} else if self.check(TokenKind::LeftBrace) {
let tag = self.parse_brace_tag()?;
last_end = tag.span().end_usize();
nodes.push(tag);
} else if self.check(TokenKind::BlockOpen) {
let block = self.parse_block()?;
last_end = block.span().end_usize();
nodes.push(block);
} else if self.check(TokenKind::TagOpen) {
let tag = self.parse_template_tag()?;
last_end = tag.span().end_usize();
nodes.push(tag);
} else {
break;
}
}
Ok(Fragment {
nodes: nodes.into_bump_slice(),
})
}
}
#[inline(never)]
fn snippet_params_close(bytes: &[u8], open: usize) -> Option<usize> {
let mut depth = 0_u32;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::{EachHeadSplit, each_binding_separator, is_svelte_ws, strip_head_keyword};
use bumpalo::Bump;
#[test]
fn head_separator_takes_a_whitespace_run_of_any_width() {
for kw in ["as", "then", "catch"] {
for ws in [
" ", " ", "\t", "\n", "\r\n", "\u{a0}", "\u{2028}", "\u{feff}",
] {
let rest = format!("{ws}{kw}{ws}v");
assert_eq!(strip_head_keyword(&rest, kw), Some("v"), "{rest:?}");
assert_eq!(
strip_head_keyword(&rest[ws.len()..], kw),
Some("v"),
"{rest:?}"
);
}
for rest in [
String::new(),
format!(" {kw}"),
kw.to_string(),
format!(" {kw}v"),
format!(" {kw}/* c */v"),
format!(" {kw}}}"),
] {
assert_eq!(strip_head_keyword(&rest, kw), None, "{rest:?}");
}
assert_eq!(strip_head_keyword(&format!(" {kw} "), kw), Some(""));
}
assert_eq!(strip_head_keyword(" a s item", "as"), None);
}
#[test]
fn each_binding_separator_reports_the_binding_offset() {
let arena = Bump::new();
for (input, binding) in [
("A[] as item", "item"),
("A[]\tas\nitem", "item"),
("A[] as item", "item"),
("A as B[] as item", "item"),
("A[] as { a, b }", "{ a, b }"),
("A[] as item, i (key)", "item, i (key)"),
("A<() => string> as item", "item"),
("(() => string)[] as item", "item"),
("A<{ a: () => string }> as item", "item"),
("unknown as A<() => string> as item", "item"),
("A satisfies B as item", "item"),
("A[] satisfies B[] as item, i", "item, i"),
("A satisfies B satisfies C as item", "item"),
("{ [K in T as U]: V } as item", "item"),
("{ [K in keyof A as string]: () => string } as item", "item"),
("A[] /* c */ as item", "item"),
("A[]as item", "item"),
("A[] as { a = 1 }", "{ a = 1 }"),
("A[] as [a = 1]", "[a = 1]"),
] {
let EachHeadSplit::LaterAs {
separator,
binding: start,
} = each_binding_separator(input, 0, 0, &arena)
else {
panic!("expected a later `as` in {input:?}");
};
assert_eq!(&input[start..], binding, "{input:?}");
assert_eq!(
input[separator..start].trim_matches(is_svelte_ws),
"as",
"{input:?}"
);
}
for input in [
"item",
"{ a } as", "Array<A as B>",
"item /* as x */",
"item // as x",
"item['as x']",
"Aas x",
"{ a = 1 }",
"{ a = (x) => x as T }",
"[a = 1]",
"A satisfies { a = 1 }",
] {
assert_eq!(
each_binding_separator(input, 0, 0, &arena),
EachHeadSplit::FirstAs,
"{input:?}"
);
}
for (input, run) in [
("A satisfies B", "A satisfies B"),
("A satisfies item", "A satisfies item"),
("A[] as item satisfies B", "A[] as item satisfies B"),
("A as B satisfies C", "A as B satisfies C"),
("A[] satisfies B[], i", "A[] satisfies B[]"),
("A satisfies B, i (i)", "A satisfies B"),
] {
assert_eq!(
each_binding_separator(input, 0, 0, &arena),
EachHeadSplit::NoBinding { run_end: run.len() },
"{input:?}"
);
assert_eq!(&input[..run.len()], run, "{input:?}");
}
}
}