use super::sink::InlineSink;
use crate::syntax::SyntaxKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SvelteKind {
BlockLogic,
Tag,
Expression,
}
impl SvelteKind {
fn parent(self) -> SyntaxKind {
match self {
SvelteKind::BlockLogic => SyntaxKind::SVELTE_BLOCK_LOGIC,
SvelteKind::Tag => SyntaxKind::SVELTE_TAG,
SvelteKind::Expression => SyntaxKind::SVELTE_EXPRESSION,
}
}
}
pub(crate) fn try_parse_svelte_template(text: &str) -> Option<(usize, SvelteKind, String)> {
let bytes = text.as_bytes();
if bytes.first() != Some(&b'{') {
return None;
}
if bytes.len() >= 3 && bytes[1] == b'{' && bytes[2] == b'<' {
return None;
}
let mut depth: i32 = 0;
let mut pos = 0;
let mut close = None;
while pos < bytes.len() {
match bytes[pos] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
close = Some(pos);
break;
}
}
_ => {}
}
pos += 1;
}
let close = close?;
let content = &text[1..close];
let kind = classify(content);
let total_len = close + 1;
Some((total_len, kind, content.to_string()))
}
fn classify(content: &str) -> SvelteKind {
match content.trim_start().as_bytes().first() {
Some(b'#') | Some(b':') | Some(b'/') => SvelteKind::BlockLogic,
Some(b'@') => SvelteKind::Tag,
_ => SvelteKind::Expression,
}
}
pub(crate) fn emit_svelte_template(builder: &mut impl InlineSink, kind: SvelteKind, content: &str) {
builder.start_node(kind.parent().into());
builder.token(SyntaxKind::SVELTE_MARKER_OPEN.into(), "{");
builder.start_node(SyntaxKind::SVELTE_CONTENT.into());
if !content.is_empty() {
builder.token(SyntaxKind::TEXT.into(), content);
}
builder.finish_node();
builder.token(SyntaxKind::SVELTE_MARKER_CLOSE.into(), "}");
builder.finish_node(); }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_simple_expression() {
let (len, kind, content) = try_parse_svelte_template("{count}").unwrap();
assert_eq!(len, 7);
assert_eq!(kind, SvelteKind::Expression);
assert_eq!(content, "count");
}
#[test]
fn classifies_block_logic() {
let (_, kind, content) = try_parse_svelte_template("{#if active}rest").unwrap();
assert_eq!(kind, SvelteKind::BlockLogic);
assert_eq!(content, "#if active");
}
#[test]
fn classifies_else_and_close() {
assert_eq!(
try_parse_svelte_template("{:else}").unwrap().1,
SvelteKind::BlockLogic
);
assert_eq!(
try_parse_svelte_template("{/each}").unwrap().1,
SvelteKind::BlockLogic
);
}
#[test]
fn classifies_tag() {
let (_, kind, content) = try_parse_svelte_template("{@html body}").unwrap();
assert_eq!(kind, SvelteKind::Tag);
assert_eq!(content, "@html body");
}
#[test]
fn handles_nested_braces_in_expression() {
let (len, kind, content) = try_parse_svelte_template("{ {a: 1} }rest").unwrap();
assert_eq!(kind, SvelteKind::Expression);
assert_eq!(content, " {a: 1} ");
assert_eq!(len, "{ {a: 1} }".len());
}
#[test]
fn preserves_internal_whitespace_verbatim() {
let (_, _, content) = try_parse_svelte_template("{#each items as item}").unwrap();
assert_eq!(content, "#each items as item");
}
#[test]
fn rejects_unbalanced_brace() {
assert!(try_parse_svelte_template("{#if active").is_none());
}
#[test]
fn leaves_shortcode_opener_to_shortcode_probe() {
assert!(try_parse_svelte_template("{{< meta x >}}").is_none());
}
#[test]
fn rejects_non_brace_start() {
assert!(try_parse_svelte_template("count}").is_none());
}
}