Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/tsv_svelte/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ What separates this crate from `tsv_ts` / `tsv_css`:
- **Embeds two other languages.** The printer delegates `<script>` / `<style>` and every template `{expr}` to `tsv_ts::build_program_doc` / `tsv_css::build_stylesheet_doc`, passing an `EmbedContext` with the current indent state and `LayoutMode::Embedded` so binary expressions use continuation indent. See `printer/script_style.rs` and `printer/tags.rs`.
- **Element classification adapter.** `printer/classification/element.rs` resolves interned tag-name symbols and dispatches to the pure `tsv_html` functions. The Svelte-specific overlay (Components are inline; non-empty `<script>`/`<style>` are block) lives here, not in `tsv_html`.
- **No `escapes/` module.** String/template-literal escapes are handled inside the embedded TS/CSS — Svelte itself has no escape rules at the template level.
- **Lazy entity decoding on `Text`.** The internal `Text` node stores `raw` plus a parse-time `TextDecoding` context (`Fragment` / `AttributeValue` / `Raw`); the decoded form comes from `Text::data() -> Cow<str>`, which borrows `raw` when no `&` is present (decode is identity) or in `Raw` context. Contexts mirror the canonical parser: fragment text decodes with text-content rules, quoted attribute values with attribute rules, raw-content element text and unquoted attribute values not at all (the unquoted case diverges from Svelte — see the TODO in `parser/attribute.rs`). The printer reads `raw`; `data()` serves boundary and cold paths (public-AST conversion, `lang`/`context` attribute checks, root-span whitespace trimming).
- **Lazy entity decoding on `Text`.** The internal `Text` node stores `raw` plus a parse-time `TextDecoding` context (`Fragment` / `AttributeValue` / `Raw`); the decoded form comes from `Text::data() -> Cow<str>`, which borrows `raw` when no `&` is present (decode is identity) or in `Raw` context. Contexts mirror the canonical parser: fragment text decodes with text-content rules, quoted and unquoted attribute values with attribute rules, raw-content element text not at all. The printer reads `raw`; `data()` serves boundary and cold paths (public-AST conversion, `lang`/`context` attribute checks, root-span whitespace trimming).
- **Dual schema for `<script>` conversion.** `ast/convert/special.rs` picks `tsv_ts::ast::convert::Schema::Acorn` for `lang="ts"` and `Schema::SvelteScript` otherwise. The latter omits `importKind` / `exportKind = "value"` and always emits `attributes` on import/export declarations to match Svelte's parser output.

## See Also
Expand Down
296 changes: 114 additions & 182 deletions crates/tsv_svelte/src/parser/attribute.rs

Large diffs are not rendered by default.

64 changes: 11 additions & 53 deletions crates/tsv_svelte/src/parser/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::lexer::TokenKind;
use crate::parser::element::ParsedElement;
use tsv_lang::{ParseError, Span};

use super::expression_tag::scan_to_matching_brace;
use super::parser_impl::SvelteParser;

/// Find the position of the LAST top-level ` as ` keyword in a string.
Expand Down Expand Up @@ -1312,63 +1313,20 @@ impl<'a> SvelteParser<'a> {
/// Scan source from a position until we find the closing } of a block tag
/// Returns (content between start and }, position after })
fn scan_block_tag_content(&mut self, start: usize) -> Result<(&'a str, usize), ParseError> {
let source_bytes = self.source.as_bytes();
let mut end = start;
let mut brace_depth = 0;
let mut in_string = false;
let mut string_char = '\0';
let mut escape_next = false;

for (i, &byte) in source_bytes.iter().enumerate().skip(start) {
let ch = byte as char;

if in_string && escape_next {
escape_next = false;
continue;
}

if in_string && ch == '\\' {
escape_next = true;
continue;
}

if in_string {
if ch == string_char {
in_string = false;
}
} else if ch == '"' || ch == '\'' || ch == '`' {
in_string = true;
string_char = ch;
} else if ch == '{' {
brace_depth += 1;
} else if ch == '}' {
if brace_depth == 0 {
end = i;
break;
}
brace_depth -= 1;
}
}
// Find the block tag's closing `}` (skips strings/comments/regex). `start`
// is just after the `{#…`/`{@…` keyword, so the opening `{` is the depth-1
// brace that `scan_to_matching_brace` matches.
let Some(end) = scan_to_matching_brace(self.source.as_bytes(), start) else {
return Err(self.error_unclosed_at("block tag", start));
};

let content = &self.source[start..end];

// Recreate lexer at the position after }
// Reposition the lexer past `}`. Block tags only occur in template content,
// so `inside_tag` is already `false` (template mode) and stays that way for
// the block body, which `advance_to_position` preserves.
let after_close = end + 1; // Skip past the }
let remaining_source = &self.source[after_close..];
let mut new_lexer = crate::lexer::Lexer::new(remaining_source);
new_lexer.inside_tag = false; // Back to template mode

let (token_kind, token_start, token_end) = {
let token = new_lexer.next_token()?;
(token.kind, token.start, token.end)
};

self.lexer = new_lexer;
self.base_offset = after_close;
self.current_kind = token_kind;
self.current_start = after_close + token_start;
self.current_end = after_close + token_end;
self.peek_cache = None;
self.advance_to_position(after_close)?;

Ok((content, after_close))
}
Expand Down
19 changes: 4 additions & 15 deletions crates/tsv_svelte/src/parser/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,21 +438,10 @@ impl<'a> SvelteParser<'a> {
);
}

// Reposition lexer to the closing tag
let remaining_source = &self.source[content_end..];
let mut new_lexer = crate::lexer::Lexer::new(remaining_source);

let (token_kind, token_start, token_end) = {
let token = new_lexer.next_token()?;
(token.kind, token.start, token.end)
};

self.lexer = new_lexer;
self.base_offset = content_end;
self.current_kind = token_kind;
self.current_start = content_end + token_start;
self.current_end = content_end + token_end;
self.peek_cache = None;
// Reposition the lexer to the closing tag. We resume at `<`, which lexes to
// `LeftAngle` in either mode; `inside_tag` is `false` here (after the opening
// tag's `>`), which `advance_to_position` preserves.
self.advance_to_position(content_end)?;

// Create a Text node (Svelte always emits one, even if empty)
let raw_content = &self.source[content_start..content_end];
Expand Down
Loading