use std::borrow::Cow;
use crate::ast;
use crate::whitespace::is_svelte_ws;
use tsv_css::__private::{write_css_children, write_css_comments};
use tsv_lang::{
Comment, JsonWriter, LocationMapper, Span, Wire, WirePositions, WireTables,
estimated_json_capacity, write_array, write_or_null,
};
use tsv_ts::__private::{
CommentAttach, CommentMode, EmbedWriter, write_expression_embedded,
write_identifier_expression_with_character, write_pattern_embedded, write_program_embedded,
write_variable_declaration_embedded,
};
use super::comment_attachment::{
AttachInputs, attach_binding_pattern, attach_const_tag_init, attach_expression,
attach_expression_list, attach_script, attach_statement, grouping_parens_around,
is_template_comment, pattern_comment_window,
};
use super::special::{bool_option, component_is_typescript, find_option_values, text_value};
pub(crate) fn write_root_bytes(root: &ast::Root<'_>, source: &str, wire: Wire) -> Vec<u8> {
let tables = WireTables::new(source, crate::WIRE_COORDINATES, wire);
let script_spans = crate::script_content_spans(root);
let template_comments: Vec<&Comment> = root
.comments
.iter()
.filter(|c| is_template_comment(c, &script_spans))
.collect();
let ctx = Ctx {
source,
positions: tables.positions(),
snippet_wire_parameters: root.snippet_wire_parameters,
comments: &template_comments,
component_is_ts: component_is_typescript(root, source),
};
let mut w = JsonWriter::with_capacity(estimated_json_capacity(source.len(), wire));
write_root(&mut w, root, &ctx);
w.into_bytes()
}
#[derive(Clone, Copy)]
struct Ctx<'a> {
source: &'a str,
positions: WirePositions<'a>,
snippet_wire_parameters: &'a [ast::SnippetWireParameters<'a>],
comments: &'a [&'a Comment],
component_is_ts: bool,
}
impl<'a> Ctx<'a> {
#[inline]
fn pos(&self, byte: u32) -> u32 {
self.positions.pos(byte)
}
fn snippet_wire_parameters(
&self,
snippet_start: u32,
) -> Option<&'a [tsv_ts::ast::Expression<'a>]> {
let wire = self.snippet_wire_parameters;
wire.binary_search_by_key(&snippet_start, |entry| entry.snippet_start)
.ok()
.map(|i| wire[i].parameters)
}
#[inline]
fn embed(&self, mode: CommentMode<'a>) -> EmbedWriter<'a> {
EmbedWriter {
source: self.source,
positions: self.positions,
comments: mode,
vanilla_acorn: !self.component_is_ts,
annotation_comments: CommentMode::Off,
}
}
#[inline]
fn embed_pattern(
&self,
mode: CommentMode<'a>,
annotation_mode: CommentMode<'a>,
) -> EmbedWriter<'a> {
EmbedWriter {
annotation_comments: annotation_mode,
..self.embed(mode)
}
}
#[inline]
fn attach_inputs(&self) -> AttachInputs<'a> {
AttachInputs {
template_comments: self.comments,
source: self.source,
}
}
#[inline]
fn without_comments(&self) -> Ctx<'a> {
Ctx {
comments: &[],
..*self
}
}
#[inline]
fn any_comment_in(&self, start: u32, end: u32) -> bool {
let idx = self.comments.partition_point(|c| c.span.start < start);
self.comments.get(idx).is_some_and(|c| c.span.start < end)
}
}
#[inline]
fn fragment_first_start(fragment: &ast::Fragment<'_>) -> Option<u32> {
fragment.nodes.first().map(|n| n.span().start)
}
#[inline]
fn preceded_by_quote(source: &str, pos: u32) -> bool {
matches!(
(pos as usize)
.checked_sub(1)
.and_then(|i| source.as_bytes().get(i)),
Some(b'"' | b'\'')
)
}
fn preceding_comment<'a>(
root: &'a ast::Root<'_>,
tag_start: u32,
source: &str,
) -> Option<&'a ast::HtmlComment> {
let mut expected_end = tag_start;
for node in root.fragment.nodes.iter().rev() {
let span = node.span();
if span.start >= tag_start {
continue;
}
if span.end != expected_end {
break;
}
match node {
ast::FragmentNode::Comment(comment) => return Some(comment),
ast::FragmentNode::Text(text)
if text.data(source).trim_matches(is_svelte_ws).is_empty() =>
{
expected_end = span.start;
}
_ => break,
}
}
None
}
fn write_root(w: &mut JsonWriter, root: &ast::Root<'_>, ctx: &Ctx<'_>) {
let source = ctx.source;
let find_preceding_comment = |tag_start: u32| preceding_comment(root, tag_start, source);
w.raw("{\"css\":");
write_or_null(w, root.css.as_ref(), |w, style| {
let style_comment = find_preceding_comment(style.span.start);
write_style_sheet(w, style, style_comment, ctx);
});
w.raw(",\"js\":[],\"start\":");
w.span_start_end(ctx.positions, 0, source.len() as u32);
w.raw(",\"type\":\"Root\",\"fragment\":");
write_fragment(w, &root.fragment, ctx);
w.raw(",\"options\":");
write_or_null(w, root.options.as_ref(), |w, opts| {
write_svelte_options(w, opts, ctx);
});
w.raw(",\"comments\":");
write_array(w, root.comments.iter(), |w, c| {
write_root_comment(w, c, ctx);
});
if let Some(script) = root.module {
let comment = find_preceding_comment(script.span.start);
w.raw(",\"module\":");
write_script(w, script, comment, ctx);
}
if let Some(script) = root.instance {
let comment = find_preceding_comment(script.span.start);
w.raw(",\"instance\":");
write_script(w, script, comment, ctx);
}
w.raw("}");
}
fn write_root_comment(w: &mut JsonWriter, comment: &Comment, ctx: &Ctx<'_>) {
let span = comment.span;
w.raw("{\"type\":\"");
w.raw(if comment.is_block { "Block" } else { "Line" });
if comment.from_template_reader {
w.raw("\",\"start\":");
w.start_end(ctx.pos(span.start), ctx.pos(span.end));
if let Some(lines) = ctx.positions.lines() {
let (start, end) = lines.span_positions(span.start, span.end);
w.loc_field::<true>(start, end);
}
w.raw(",\"value\":");
w.string(&comment.wire_value(ctx.source));
} else {
w.raw("\",\"value\":");
w.string(&comment.wire_value(ctx.source));
w.span_start_end_field(ctx.positions, span.start, span.end);
}
w.raw("}");
}
fn write_fragment(w: &mut JsonWriter, fragment: &ast::Fragment<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"Fragment\",\"nodes\":");
write_array(w, fragment.nodes, |w, n| write_fragment_node(w, n, ctx));
w.raw("}");
}
fn write_fragment_node(w: &mut JsonWriter, node: &ast::FragmentNode<'_>, ctx: &Ctx<'_>) {
match node {
ast::FragmentNode::Element(elem) => write_element(w, elem, ctx),
ast::FragmentNode::SpecialElement(elem) => write_special_element(w, elem, ctx),
ast::FragmentNode::ExpressionTag(tag) => write_expression_tag(w, tag, ctx),
ast::FragmentNode::Text(text) => write_text(w, text, ctx),
ast::FragmentNode::Comment(comment) => write_html_comment(w, comment, ctx),
ast::FragmentNode::IfBlock(block) => write_if_block(w, block, ctx),
ast::FragmentNode::EachBlock(block) => write_each_block(w, block, ctx),
ast::FragmentNode::AwaitBlock(block) => write_await_block(w, block, ctx),
ast::FragmentNode::KeyBlock(block) => write_key_block(w, block, ctx),
ast::FragmentNode::SnippetBlock(block) => write_snippet_block(w, block, ctx),
ast::FragmentNode::HtmlTag(tag) => write_html_tag(w, tag, ctx),
ast::FragmentNode::ConstTag(tag) => write_const_tag(w, tag, ctx),
ast::FragmentNode::DeclarationTag(tag) => write_declaration_tag(w, tag, ctx),
ast::FragmentNode::DebugTag(tag) => write_debug_tag(w, tag, ctx),
ast::FragmentNode::RenderTag(tag) => write_render_tag(w, tag, ctx),
}
}
fn write_generic_island(
w: &mut JsonWriter,
expr: &tsv_ts::ast::Expression<'_>,
container_start: u32,
range_end: u32,
ctx: &Ctx<'_>,
) {
if ctx.any_comment_in(container_start, range_end) {
let attach =
attach_expression(ctx.attach_inputs(), container_start, expr.span(), range_end);
write_expression_embedded(w, expr, ctx.embed(attach.mode()));
} else {
write_expression_embedded(w, expr, ctx.embed(CommentMode::Off));
}
}
fn write_braced_island(
w: &mut JsonWriter,
expr: &tsv_ts::ast::Expression<'_>,
window: Span,
ctx: &Ctx<'_>,
) {
write_generic_island(w, expr, window.start, window.end, ctx);
}
fn write_name_loc_field(w: &mut JsonWriter, span: Span, lines: LocationMapper<'_>) {
let (start, end) = lines.one_line_span_positions(span.start, span.end);
debug_assert_eq!(start.1.line, end.1.line, "a name span lies on one line");
let mut run = w.stage_run();
run.loc_object::<true>(",\"name_loc\":", start, end);
run.flush();
}
const NAME_KEY: &[u8; 8] = b",\"name\":";
#[inline(never)]
fn write_element_name_field(w: &mut JsonWriter, elem: &ast::Element<'_>, ctx: &Ctx<'_>) {
let name = name_bytes(elem.name_span, ctx);
match elem.kind {
ast::ElementKind::Html => w.string_escape_free_led(NAME_KEY, name),
ast::ElementKind::Component => write_scanned_name_field(w, name),
}
}
#[inline(never)]
fn write_scanned_name_field(w: &mut JsonWriter, name: &[u8]) {
w.string_led(NAME_KEY, name);
}
#[inline]
fn name_bytes<'s>(span: Span, ctx: &Ctx<'s>) -> &'s [u8] {
debug_assert!(
ctx.source.is_char_boundary(span.start as usize)
&& ctx.source.is_char_boundary(span.end as usize),
"a name span covers whole characters"
);
&ctx.source.as_bytes()[span.range()]
}
fn write_element(w: &mut JsonWriter, elem: &ast::Element<'_>, ctx: &Ctx<'_>) {
let mut run = w.stage_run();
match elem.kind {
ast::ElementKind::Component => run.raw("{\"type\":\"Component\",\"start\":"),
ast::ElementKind::Html => run.raw("{\"type\":\"RegularElement\",\"start\":"),
}
run.u32(ctx.pos(elem.span.start));
run.raw(",\"end\":");
run.u32(ctx.pos(elem.span.end));
run.flush();
w.span_loc(ctx.positions, elem.span.start, elem.span.end);
write_element_name_field(w, elem, ctx);
if let Some(lines) = ctx.positions.lines() {
write_name_loc_field(w, elem.name_span, lines);
}
if elem.attributes.is_empty() {
w.raw(",\"attributes\":[],\"fragment\":{\"type\":\"Fragment\",\"nodes\":[");
} else {
w.raw(",\"attributes\":[");
write_items(w, elem.attributes, |w, a| write_attribute_node(w, a, ctx));
w.raw("],\"fragment\":{\"type\":\"Fragment\",\"nodes\":[");
}
let nodes = elem.fragment.nodes;
if !nodes.is_empty() && elem.name(ctx.source) == "textarea" {
write_items(w, nodes, |w, n| match n {
ast::FragmentNode::Text(text) => write_sequence_text(w, text, ctx),
_ => write_fragment_node(w, n, ctx),
});
} else {
write_items(w, nodes, |w, n| write_fragment_node(w, n, ctx));
}
w.raw("]}}");
}
#[inline]
fn write_items<T>(w: &mut JsonWriter, items: &[T], mut f: impl FnMut(&mut JsonWriter, &T)) {
let mut first = true;
for item in items {
if !first {
w.raw(",");
}
first = false;
f(w, item);
}
}
fn write_special_element(w: &mut JsonWriter, elem: &ast::SpecialElement<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"");
w.raw(elem.kind.node_type());
w.raw("\",\"start\":");
w.span_start_end(ctx.positions, elem.span.start, elem.span.end);
w.raw(",\"name\":");
w.token(elem.kind.tag_name());
if let Some(lines) = ctx.positions.lines() {
write_name_loc_field(w, elem.name_span, lines);
}
w.raw(",\"attributes\":");
write_array(w, elem.attributes, |w, a| write_attribute_node(w, a, ctx));
w.raw(",\"fragment\":");
write_fragment(w, &elem.fragment, ctx);
if let Some(tag) = elem.kind.tag() {
w.raw(",\"tag\":");
write_special_tag(w, tag, ctx);
}
if let Some(tag) = elem.kind.expression() {
w.raw(",\"expression\":");
write_braced_island(w, tag.expression, tag.span, ctx);
}
w.raw("}");
}
fn write_special_tag(w: &mut JsonWriter, this: &ast::SpecialThis<'_>, ctx: &Ctx<'_>) {
match this {
ast::SpecialThis::Plain { content, span } => {
w.raw("{\"type\":\"Literal\",\"value\":");
w.string(content);
w.raw(",\"raw\":");
w.string(&format!("'{content}'"));
w.span_start_end_field(ctx.positions, span.start, span.end);
w.raw("}");
}
ast::SpecialThis::Braced(tag) => {
write_braced_island(w, tag.expression, tag.span, ctx);
}
}
}
fn write_expression_tag(w: &mut JsonWriter, tag: &ast::ExpressionTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"ExpressionTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"expression\":");
write_braced_island(w, tag.expression, tag.span, ctx);
w.raw("}");
}
fn write_shorthand_expression_tag(w: &mut JsonWriter, tag: &ast::ExpressionTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"ExpressionTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"expression\":");
write_identifier_expression_with_character(w, tag.expression, ctx.embed(CommentMode::Off));
w.raw("}");
}
fn write_text(w: &mut JsonWriter, text: &ast::Text, ctx: &Ctx<'_>) {
if matches!(text.decoding, ast::TextDecoding::Raw) {
w.span_start_end_object(ctx.positions, text.span.start, text.span.end);
debug_assert_eq!(text.data(ctx.source), text.raw(ctx.source));
w.raw(",\"type\":\"Text\",\"data\":");
w.string_pair(text.raw(ctx.source), ",\"raw\":");
w.raw("}");
return;
}
let mut run = w.stage_run();
run.raw("{\"type\":\"Text\",\"start\":");
run.u32(ctx.pos(text.span.start));
run.raw(",\"end\":");
run.u32(ctx.pos(text.span.end));
if ctx.positions.has_locations() {
run.flush();
w.span_loc(ctx.positions, text.span.start, text.span.end);
w.raw(",\"raw\":");
} else {
run.raw(",\"raw\":");
run.flush();
}
write_raw_then_data(w, text, ctx);
w.raw("}");
}
fn write_raw_then_data(w: &mut JsonWriter, text: &ast::Text, ctx: &Ctx<'_>) {
if text.is_collapsible_ws_only {
write_whitespace_raw_then_data(w, text, ctx);
} else {
write_content_raw_then_data(w, text, ctx);
}
}
#[inline(never)]
fn write_whitespace_raw_then_data(w: &mut JsonWriter, text: &ast::Text, ctx: &Ctx<'_>) {
w.string_pair_whitespace(&ctx.source.as_bytes()[text.raw_span.range()], b",\"data\":");
}
#[inline(never)]
fn write_content_raw_then_data(w: &mut JsonWriter, text: &ast::Text, ctx: &Ctx<'_>) {
let raw = text.raw(ctx.source);
if text.data_is_raw(raw) {
w.string_pair(raw, ",\"data\":");
} else {
w.string(raw);
w.raw(",\"data\":");
w.string(&text.data(ctx.source));
}
}
fn write_sequence_text(w: &mut JsonWriter, text: &ast::Text, ctx: &Ctx<'_>) {
w.span_start_end_object(ctx.positions, text.span.start, text.span.end);
w.raw(",\"type\":\"Text\",\"raw\":");
write_raw_then_data(w, text, ctx);
w.raw("}");
}
fn write_html_comment(w: &mut JsonWriter, comment: &ast::HtmlComment, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"Comment\",\"start\":");
w.span_start_end(ctx.positions, comment.span.start, comment.span.end);
w.raw(",\"data\":");
w.string(comment.content(ctx.source));
w.raw("}");
}
fn write_if_block(w: &mut JsonWriter, block: &ast::IfBlock<'_>, ctx: &Ctx<'_>) {
if block.elseif {
w.span_start_end_object(ctx.positions, block.span.start, block.span.end);
w.raw(",\"type\":\"IfBlock\",\"elseif\":true");
} else {
w.raw("{\"type\":\"IfBlock\",\"elseif\":false,\"start\":");
w.span_start_end(ctx.positions, block.span.start, block.span.end);
}
let range_end = fragment_first_start(&block.consequent).unwrap_or(block.span.end);
w.raw(",\"test\":");
write_generic_island(w, block.test, block.span.start, range_end, ctx);
w.raw(",\"consequent\":");
write_fragment(w, &block.consequent, ctx);
w.raw(",\"alternate\":");
write_optional_fragment(w, block.alternate.as_ref(), ctx);
w.raw("}");
}
fn write_each_block(w: &mut JsonWriter, block: &ast::EachBlock<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"EachBlock\",\"start\":");
w.span_start_end(ctx.positions, block.span.start, block.span.end);
let range_end = fragment_first_start(&block.body).unwrap_or(block.span.end);
w.raw(",\"expression\":");
write_generic_island(w, block.expression, block.span.start, range_end, ctx);
w.raw(",\"body\":");
write_fragment(w, &block.body, ctx);
w.raw(",\"context\":");
write_or_null(w, block.context.as_ref(), |w, c| {
write_pattern_island(w, c, ctx);
});
if let Some(index) = block.index {
w.raw(",\"index\":");
w.string(index);
}
if let Some(key) = &block.key {
w.raw(",\"key\":");
write_generic_island(w, key.expression, key.span.start + 1, key.span.end - 1, ctx);
}
if let Some(fallback) = &block.fallback {
w.raw(",\"fallback\":");
write_fragment(w, fallback, ctx);
}
w.raw("}");
}
fn write_await_block(w: &mut JsonWriter, block: &ast::AwaitBlock<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"AwaitBlock\",\"start\":");
w.span_start_end(ctx.positions, block.span.start, block.span.end);
let range_end = [
block.pending.as_ref(),
block.then.as_ref(),
block.catch.as_ref(),
]
.into_iter()
.flatten()
.filter_map(fragment_first_start)
.min()
.unwrap_or(block.span.end);
w.raw(",\"expression\":");
write_generic_island(w, block.expression, block.span.start, range_end, ctx);
w.raw(",\"value\":");
write_or_null(w, block.value.as_ref(), |w, v| {
write_pattern_island(w, v, ctx);
});
w.raw(",\"error\":");
write_or_null(w, block.error.as_ref(), |w, e| {
write_pattern_island(w, e, ctx);
});
w.raw(",\"pending\":");
match block.pending.as_ref() {
Some(fragment) => write_fragment(w, fragment, ctx),
None if block.pending_block => write_fragment(w, &ast::Fragment { nodes: &[] }, ctx),
None => w.raw("null"),
}
w.raw(",\"then\":");
write_optional_fragment(w, block.then.as_ref(), ctx);
w.raw(",\"catch\":");
write_optional_fragment(w, block.catch.as_ref(), ctx);
w.raw("}");
}
fn write_key_block(w: &mut JsonWriter, block: &ast::KeyBlock<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"KeyBlock\",\"start\":");
w.span_start_end(ctx.positions, block.span.start, block.span.end);
let range_end = fragment_first_start(&block.fragment).unwrap_or(block.span.end);
w.raw(",\"expression\":");
write_generic_island(w, block.expression, block.span.start, range_end, ctx);
w.raw(",\"fragment\":");
write_fragment(w, &block.fragment, ctx);
w.raw("}");
}
fn write_snippet_block(w: &mut JsonWriter, block: &ast::SnippetBlock<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"SnippetBlock\",\"start\":");
w.span_start_end(ctx.positions, block.span.start, block.span.end);
let range_end = fragment_first_start(&block.body).unwrap_or(block.span.end);
w.raw(",\"expression\":");
write_snippet_name(w, block.expression, block.span.start, range_end, ctx);
if let Some(type_params) = block.type_params_raw {
w.raw(",\"typeParams\":");
w.string(type_params);
}
w.raw(",\"parameters\":");
write_snippet_parameters(
w,
ctx.snippet_wire_parameters(block.span.start)
.unwrap_or(block.parameters),
block.span.start,
range_end,
ctx,
);
w.raw(",\"body\":");
write_fragment(w, &block.body, ctx);
w.raw("}");
}
fn write_snippet_name(
w: &mut JsonWriter,
expr: &tsv_ts::ast::Expression<'_>,
container_start: u32,
range_end: u32,
ctx: &Ctx<'_>,
) {
if ctx.any_comment_in(container_start, range_end) {
let attach =
attach_expression(ctx.attach_inputs(), container_start, expr.span(), range_end);
write_identifier_expression_with_character(w, expr, ctx.embed(attach.mode()));
} else {
write_identifier_expression_with_character(w, expr, ctx.embed(CommentMode::Off));
}
}
fn write_snippet_parameters(
w: &mut JsonWriter,
parameters: &[tsv_ts::ast::Expression<'_>],
container_start: u32,
range_end: u32,
ctx: &Ctx<'_>,
) {
if !parameters.is_empty() && ctx.any_comment_in(container_start, range_end) {
let attach =
attach_expression_list(ctx.attach_inputs(), container_start, range_end, None, false);
write_array(w, parameters, |w, p| {
write_expression_embedded(w, p, ctx.embed(attach.mode()));
});
} else {
write_array(w, parameters, |w, p| {
write_expression_embedded(w, p, ctx.embed(CommentMode::Off));
});
}
}
fn write_html_tag(w: &mut JsonWriter, tag: &ast::HtmlTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"HtmlTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"expression\":");
write_braced_island(w, tag.expression, tag.span, ctx);
w.raw("}");
}
fn write_render_tag(w: &mut JsonWriter, tag: &ast::RenderTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"RenderTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"expression\":");
write_braced_island(w, tag.expression, tag.span, ctx);
w.raw("}");
}
fn write_debug_tag(w: &mut JsonWriter, tag: &ast::DebugTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"DebugTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"identifiers\":");
let spliced = if tag.identifiers.iter().any(|entry| {
matches!(
entry.unwrap_comment_bound_pairs().kind,
tsv_ts::ast::ExpressionKind::SequenceExpression(_)
)
}) {
let mut flat = Vec::with_capacity(tag.identifiers.len() + 1);
for entry in tag.identifiers {
match &entry.unwrap_comment_bound_pairs().kind {
tsv_ts::ast::ExpressionKind::SequenceExpression(seq) => {
flat.extend(seq.expressions.iter().map(|e| (*e).clone()));
}
_ => flat.push(entry.clone()),
}
}
Some(flat)
} else {
None
};
let identifiers = spliced.as_deref().unwrap_or(tag.identifiers);
if let [_, _, ..] = identifiers
&& ctx.any_comment_in(tag.span.start, tag.span.end)
{
let attach_inputs = ctx.attach_inputs();
let element_parens: Vec<Vec<Span>> = identifiers
.iter()
.map(|id| {
grouping_parens_around(attach_inputs, tag.span.start, id.span(), tag.span.end)
})
.collect();
let outer = |i: usize| {
element_parens[i]
.first()
.copied()
.unwrap_or_else(|| identifiers[i].span())
};
let wrapper = Span::new(outer(0).start, outer(identifiers.len() - 1).end);
let attach = attach_expression_list(
attach_inputs,
tag.span.start,
tag.span.end,
Some(wrapper),
true,
);
let mode = attach.mode();
write_array(
w,
identifiers.iter().zip(&element_parens),
|w, (id, parens)| {
mode.with_silent_parens(parens, || {
write_expression_embedded(w, id, ctx.embed(mode));
});
},
);
} else {
write_array(w, identifiers, |w, id| {
write_braced_island(w, id, tag.span, ctx);
});
}
w.raw("}");
}
fn write_const_tag(w: &mut JsonWriter, tag: &ast::ConstTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"ConstTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"declaration\":");
let decl_end = tag.span.end - 1;
let declarator_end = const_declarator_end(tag, ctx);
if !ctx.any_comment_in(tag.span.start, tag.span.end) {
write_const_declaration(
w,
tag,
decl_end,
declarator_end,
(CommentMode::Off, CommentMode::Off),
CommentMode::Off,
ctx,
);
} else {
let id_attach = attach_binding_pattern(tag.id, ctx.attach_inputs());
let init_attach = attach_const_tag_init(tag, ctx.attach_inputs());
write_const_declaration(
w,
tag,
decl_end,
declarator_end,
id_attach.modes(),
init_attach.mode(),
ctx,
);
}
w.raw("}");
}
fn const_declarator_end(tag: &ast::ConstTag<'_>, ctx: &Ctx<'_>) -> u32 {
let close = (tag.span.end - 1) as usize; let mut end = tag.init.span().end;
let mut i = end as usize;
loop {
while let Some(ch) = ctx.source[i..close].chars().next() {
if !is_svelte_ws(ch) {
break;
}
i += ch.len_utf8();
}
if i >= close {
break;
}
if ctx.source.as_bytes()[i] == b')' {
i += 1;
end = i as u32;
continue;
}
let idx = ctx
.comments
.partition_point(|c| (c.span.start as usize) < i);
match ctx.comments.get(idx) {
Some(c) if c.span.start as usize == i => {
i = c.span.end as usize;
end = c.span.end;
}
_ => break,
}
}
end
}
fn write_const_declaration(
w: &mut JsonWriter,
tag: &ast::ConstTag<'_>,
decl_end: u32,
declarator_end: u32,
(id_mode, id_annotation_mode): (CommentMode<'_>, CommentMode<'_>),
init_mode: CommentMode<'_>,
ctx: &Ctx<'_>,
) {
w.raw(
"{\"type\":\"VariableDeclaration\",\"kind\":\"const\",\"declarations\":[{\"type\":\"VariableDeclarator\",\"id\":",
);
write_pattern_embedded(w, tag.id, ctx.embed_pattern(id_mode, id_annotation_mode));
w.raw(",\"init\":");
write_expression_embedded(w, tag.init, ctx.embed(init_mode));
w.span_start_end_field(ctx.positions, tag.id.span().start, declarator_end);
w.raw("}],\"start\":");
w.span_start_end(ctx.positions, tag.span.start + 2, decl_end);
w.raw("}");
}
fn write_declaration_tag(w: &mut JsonWriter, tag: &ast::DeclarationTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"DeclarationTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"declaration\":");
if !ctx.any_comment_in(tag.span.start, tag.span.end) {
write_variable_declaration_embedded(w, &tag.declaration, ctx.embed(CommentMode::Off));
} else {
let attach = attach_statement(
ctx.attach_inputs(),
tag.span.start,
tag.declaration.span.end,
tag.span.end,
);
write_variable_declaration_embedded(w, &tag.declaration, ctx.embed(attach.mode()));
}
w.raw("}");
}
fn write_attribute_node(w: &mut JsonWriter, node: &ast::AttributeNode<'_>, ctx: &Ctx<'_>) {
match node {
ast::AttributeNode::Attribute(a) => write_attribute(w, a, ctx),
ast::AttributeNode::SpreadAttribute(s) => write_spread_attribute(w, s, ctx),
ast::AttributeNode::AttachTag(t) => write_attach_tag(w, t, ctx),
ast::AttributeNode::OnDirective(d) => write_on_directive(w, d, ctx),
ast::AttributeNode::BindDirective(d) => write_bind_directive(w, d, ctx),
ast::AttributeNode::ClassDirective(d) => write_class_directive(w, d, ctx),
ast::AttributeNode::StyleDirective(d) => write_style_directive(w, d, ctx),
ast::AttributeNode::UseDirective(d) => write_use_directive(w, d, ctx),
ast::AttributeNode::TransitionDirective(d) => write_transition_directive(w, d, ctx),
ast::AttributeNode::AnimateDirective(d) => write_animate_directive(w, d, ctx),
ast::AttributeNode::LetDirective(d) => write_let_directive(w, d, ctx),
}
}
fn write_attribute(w: &mut JsonWriter, attr: &ast::Attribute<'_>, ctx: &Ctx<'_>) {
let mut run = w.stage_run();
run.raw("{\"type\":\"Attribute\",\"start\":");
run.u32(ctx.pos(attr.span.start));
run.raw(",\"end\":");
run.u32(ctx.pos(attr.span.end));
run.flush();
w.span_loc(ctx.positions, attr.span.start, attr.span.end);
write_scanned_name_field(w, name_bytes(attr.name_span, ctx));
if let Some(lines) = ctx.positions.lines() {
write_name_loc_field(w, attr.name_span, lines);
}
let Some(values) = attr.value else {
w.raw(",\"value\":true}");
return;
};
w.raw(",\"value\":");
write_attribute_value_field(w, values, ctx);
w.raw("}");
}
fn write_attribute_value_field(
w: &mut JsonWriter,
values: &[ast::AttributeValue<'_>],
ctx: &Ctx<'_>,
) {
let has_text = values
.iter()
.any(|v| matches!(v, ast::AttributeValue::Text(_)));
let quoted = values.len() == 1
&& matches!(&values[0], ast::AttributeValue::ExpressionTag(tag)
if preceded_by_quote(ctx.source, tag.span.start));
if has_text || quoted {
write_array(w, values, |w, v| write_attribute_value(w, v, ctx));
} else if values.len() == 1 {
match &values[0] {
ast::AttributeValue::ExpressionTag(tag)
if matches!(&tag.expression.kind, tsv_ts::ast::ExpressionKind::Identifier(id)
if tag.span == id.span) =>
{
write_shorthand_expression_tag(w, tag, ctx);
}
v => write_attribute_value(w, v, ctx),
}
} else {
write_array(w, values, |w, v| write_attribute_value(w, v, ctx));
}
}
fn write_attribute_value(w: &mut JsonWriter, value: &ast::AttributeValue<'_>, ctx: &Ctx<'_>) {
match value {
ast::AttributeValue::Text(text) => write_sequence_text(w, text, ctx),
ast::AttributeValue::ExpressionTag(tag) => write_expression_tag(w, tag, ctx),
}
}
fn write_spread_attribute(w: &mut JsonWriter, spread: &ast::SpreadAttribute<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"SpreadAttribute\",\"start\":");
w.span_start_end(ctx.positions, spread.span.start, spread.span.end);
w.raw(",\"expression\":");
write_braced_island(w, spread.expression, spread.span, ctx);
w.raw("}");
}
fn write_attach_tag(w: &mut JsonWriter, tag: &ast::AttachTag<'_>, ctx: &Ctx<'_>) {
w.raw("{\"type\":\"AttachTag\",\"start\":");
w.span_start_end(ctx.positions, tag.span.start, tag.span.end);
w.raw(",\"expression\":");
write_braced_island(w, tag.expression, tag.span, ctx);
w.raw("}");
}
fn write_directive_head(
w: &mut JsonWriter,
node_type: &str,
span: Span,
name_span: Span,
head_span: Span,
ctx: &Ctx<'_>,
) {
w.span_start_end_object(ctx.positions, span.start, span.end);
w.raw(",\"type\":\"");
w.raw(node_type);
w.raw("\",\"name\":");
w.string(name_span.extract(ctx.source));
if let Some(lines) = ctx.positions.lines() {
write_name_loc_field(w, head_span, lines);
}
}
fn write_modifiers(w: &mut JsonWriter, modifiers: &[&str]) {
write_array(w, modifiers, |w, m| w.string(m));
}
fn write_optional_directive_expression(
w: &mut JsonWriter,
expression: Option<&tsv_ts::ast::Expression<'_>>,
span: Span,
ctx: &Ctx<'_>,
) {
write_or_null(w, expression, |w, e| {
write_braced_island(w, e, span, ctx);
});
}
macro_rules! expression_directive_writer {
($fn_name:ident, $ty:ident) => {
fn $fn_name(w: &mut JsonWriter, d: &ast::$ty<'_>, ctx: &Ctx<'_>) {
write_directive_head(w, stringify!($ty), d.span, d.name_span, d.head_span, ctx);
w.raw(",\"expression\":");
write_optional_directive_expression(w, d.expression, d.span, ctx);
w.raw(",\"modifiers\":");
write_modifiers(w, d.modifiers);
w.raw("}");
}
};
}
expression_directive_writer!(write_on_directive, OnDirective);
expression_directive_writer!(write_use_directive, UseDirective);
expression_directive_writer!(write_animate_directive, AnimateDirective);
expression_directive_writer!(write_let_directive, LetDirective);
fn write_transition_directive(w: &mut JsonWriter, d: &ast::TransitionDirective<'_>, ctx: &Ctx<'_>) {
write_directive_head(
w,
"TransitionDirective",
d.span,
d.name_span,
d.head_span,
ctx,
);
w.raw(",\"expression\":");
write_optional_directive_expression(w, d.expression, d.span, ctx);
w.raw(",\"modifiers\":");
write_modifiers(w, d.modifiers);
w.raw(",\"intro\":");
w.bool(d.direction.has_intro());
w.raw(",\"outro\":");
w.bool(d.direction.has_outro());
w.raw("}");
}
fn write_directive_value_expression(
w: &mut JsonWriter,
expr: &tsv_ts::ast::Expression<'_>,
has_expression_tag: bool,
span: Span,
ctx: &Ctx<'_>,
) {
if has_expression_tag {
write_braced_island(w, expr, span, ctx);
} else {
#[expect(clippy::unreachable)]
let tsv_ts::ast::ExpressionKind::Identifier(id) = &expr.kind else {
unreachable!("shorthand directive expression is always an Identifier");
};
w.span_start_end_object(ctx.positions, id.span.start, id.span.end);
w.raw(",\"type\":\"Identifier\",\"name\":");
w.string(id.name(ctx.source));
w.raw("}");
}
}
fn write_bind_directive(w: &mut JsonWriter, d: &ast::BindDirective<'_>, ctx: &Ctx<'_>) {
write_directive_head(w, "BindDirective", d.span, d.name_span, d.head_span, ctx);
w.raw(",\"expression\":");
write_directive_value_expression(
w,
d.expression,
d.expression_tag_span.is_some(),
d.span,
ctx,
);
w.raw(",\"modifiers\":");
write_modifiers(w, d.modifiers);
w.raw("}");
}
fn write_class_directive(w: &mut JsonWriter, d: &ast::ClassDirective<'_>, ctx: &Ctx<'_>) {
write_directive_head(w, "ClassDirective", d.span, d.name_span, d.head_span, ctx);
w.raw(",\"expression\":");
write_directive_value_expression(
w,
d.expression,
d.expression_tag_span.is_some(),
d.span,
ctx,
);
w.raw(",\"modifiers\":");
write_modifiers(w, d.modifiers);
w.raw("}");
}
fn write_style_directive(w: &mut JsonWriter, d: &ast::StyleDirective<'_>, ctx: &Ctx<'_>) {
write_directive_head(w, "StyleDirective", d.span, d.name_span, d.head_span, ctx);
w.raw(",\"modifiers\":");
write_modifiers(w, d.modifiers);
w.raw(",\"value\":");
match &d.value {
ast::StyleDirectiveValue::True => w.raw("true"),
ast::StyleDirectiveValue::ExpressionTag(tag) => {
if preceded_by_quote(ctx.source, tag.span.start) {
w.raw("[");
write_expression_tag(w, tag, ctx);
w.raw("]");
} else {
write_expression_tag(w, tag, ctx);
}
}
ast::StyleDirectiveValue::Parts(parts) => {
write_array(w, *parts, |w, p| write_attribute_value(w, p, ctx));
}
}
w.raw("}");
}
fn write_style_sheet(
w: &mut JsonWriter,
style: &ast::Style<'_>,
preceding_comment: Option<&ast::HtmlComment>,
ctx: &Ctx<'_>,
) {
w.raw("{\"type\":\"StyleSheet\",\"start\":");
w.span_start_end(ctx.positions, style.span.start, style.span.end);
w.raw(",\"attributes\":");
write_value_attributes(w, style.attributes, ctx);
w.raw(",\"children\":");
let css_comments = write_css_children(w, &style.css_stylesheet, ctx.source, ctx.positions);
w.raw(",\"comments\":");
write_css_comments(w, &css_comments, ctx.source, ctx.positions);
w.raw(",\"content\":{\"start\":");
w.span_start_end(
ctx.positions,
style.content_span.start,
style.content_span.end,
);
w.raw(",\"styles\":");
w.string(style.content_span.extract(ctx.source));
w.raw(",\"comment\":");
write_or_null(w, preceding_comment, |w, c| write_html_comment(w, c, ctx));
w.raw("}}");
}
fn write_script(
w: &mut JsonWriter,
script: &ast::Script<'_>,
html_leading_comment: Option<&ast::HtmlComment>,
ctx: &Ctx<'_>,
) {
w.raw("{\"type\":\"Script\",\"start\":");
w.span_start_end(ctx.positions, script.span.start, script.span.end);
w.raw(",\"context\":");
w.token(script.context.as_str());
w.raw(",\"content\":");
let attach = if script.content.comments.is_empty() && html_leading_comment.is_none() {
None
} else {
Some(attach_script(
script,
ctx.attach_inputs(),
html_leading_comment,
))
};
let mode = attach
.as_ref()
.map_or(CommentMode::Off, CommentAttach::mode);
write_program_embedded(w, &script.content, ctx.embed(mode));
w.raw(",\"attributes\":");
write_value_attributes(w, script.attributes, ctx);
w.raw("}");
}
fn write_svelte_options(w: &mut JsonWriter, options: &ast::SvelteOptions<'_>, ctx: &Ctx<'_>) {
let attrs = options.attributes;
w.span_start_end_object(ctx.positions, options.span.start, options.span.end);
w.raw(",\"attributes\":");
write_value_attributes(w, attrs, ctx);
if let Some(runes) = bool_option(attrs, "runes", ctx.source) {
w.raw(",\"runes\":");
w.bool(runes);
}
if let Some(immutable) = bool_option(attrs, "immutable", ctx.source) {
w.raw(",\"immutable\":");
w.bool(immutable);
}
if let Some(css) =
find_option_values(attrs, "css", ctx.source).and_then(|v| text_value(v, ctx.source))
{
w.raw(",\"css\":");
w.string(&css);
}
if let Some(accessors) = bool_option(attrs, "accessors", ctx.source) {
w.raw(",\"accessors\":");
w.bool(accessors);
}
if let Some(preserve_whitespace) = bool_option(attrs, "preserveWhitespace", ctx.source) {
w.raw(",\"preserveWhitespace\":");
w.bool(preserve_whitespace);
}
if let Some(namespace) =
find_option_values(attrs, "namespace", ctx.source).and_then(|v| text_value(v, ctx.source))
{
w.raw(",\"namespace\":");
w.string(&namespace);
}
write_custom_element_field(w, attrs, ctx);
w.raw("}");
}
fn json_comma(w: &mut JsonWriter, first: &mut bool) {
if *first {
*first = false;
} else {
w.raw(",");
}
}
fn write_custom_element_props(
w: &mut JsonWriter,
props_obj: &tsv_ts::ast::ObjectExpression<'_>,
ctx: &Ctx<'_>,
) {
use tsv_ts::ast::{Expression, ExpressionKind, LiteralValue, ObjectProperty};
w.raw("{");
let mut first_prop = true;
for prop in props_obj.properties {
let ObjectProperty::Property(p) = prop else {
continue;
};
let (
Expression {
kind: ExpressionKind::Identifier(key),
..
},
Expression {
kind: ExpressionKind::ObjectExpression(inner),
..
},
) = (&p.key, &p.value)
else {
continue;
};
json_comma(w, &mut first_prop);
w.string(key.name(ctx.source));
w.raw(":{");
let mut first_attr = true;
for inner_prop in inner.properties {
let ObjectProperty::Property(ip) = inner_prop else {
continue;
};
let (
Expression {
kind: ExpressionKind::Identifier(ikey),
..
},
Expression {
kind: ExpressionKind::Literal(lit),
..
},
) = (&ip.key, &ip.value)
else {
continue;
};
let key_name = ikey.name(ctx.source);
match &lit.value {
LiteralValue::String(cooked) => {
json_comma(w, &mut first_attr);
w.string(key_name);
w.raw(":");
w.string(cooked.resolve(lit.span, ctx.source));
}
LiteralValue::Boolean(b) => {
json_comma(w, &mut first_attr);
w.string(key_name);
w.raw(":");
w.bool(*b);
}
_ => {}
}
}
w.raw("}");
}
w.raw("}");
}
fn write_custom_element_field(w: &mut JsonWriter, attrs: &[ast::AttributeNode<'_>], ctx: &Ctx<'_>) {
use tsv_ts::ast::{Expression, ExpressionKind, LiteralValue, ObjectProperty};
let Some(values) = find_option_values(attrs, "customElement", ctx.source) else {
return;
};
for v in values {
if let ast::AttributeValue::ExpressionTag(expr) = v
&& let ExpressionKind::ObjectExpression(obj) = &expr.expression.kind
{
let mut tag: Option<&Expression<'_>> = None;
let mut props: Option<&Expression<'_>> = None;
let mut shadow: Option<&Expression<'_>> = None;
let mut extend: Option<&Expression<'_>> = None;
for prop in obj.properties {
if let ObjectProperty::Property(p) = prop
&& let ExpressionKind::Identifier(key) = &p.key.kind
{
let slot = match key.name(ctx.source) {
"tag" => &mut tag,
"props" => &mut props,
"shadow" => &mut shadow,
"extend" => &mut extend,
_ => continue,
};
if slot.is_none() {
*slot = Some(p.value);
}
}
}
w.raw(",\"customElement\":{");
let mut first = true;
if let Some(Expression {
kind: ExpressionKind::Literal(lit),
..
}) = tag
&& let LiteralValue::String(cooked) = &lit.value
{
json_comma(w, &mut first);
w.raw("\"tag\":");
w.string(cooked.resolve(lit.span, ctx.source));
}
if let Some(Expression {
kind: ExpressionKind::ObjectExpression(props_obj),
..
}) = props
{
json_comma(w, &mut first);
w.raw("\"props\":");
write_custom_element_props(w, props_obj, ctx);
}
match shadow {
Some(Expression {
kind: ExpressionKind::Literal(lit),
..
}) => {
if let LiteralValue::String(cooked) = &lit.value {
json_comma(w, &mut first);
w.raw("\"shadow\":");
w.string(cooked.resolve(lit.span, ctx.source));
}
}
Some(
shadow_expr @ Expression {
kind: ExpressionKind::ObjectExpression(_),
..
},
) => {
json_comma(w, &mut first);
w.raw("\"shadow\":");
write_expression_embedded(w, shadow_expr, ctx.embed(CommentMode::Off));
}
_ => {}
}
if let Some(extend_expr) = extend {
json_comma(w, &mut first);
w.raw("\"extend\":");
write_expression_embedded(w, extend_expr, ctx.embed(CommentMode::Off));
}
w.raw("}");
return;
}
let tag_str = match v {
ast::AttributeValue::Text(text) => Some(text.data(ctx.source)),
ast::AttributeValue::ExpressionTag(expr) => {
if let ExpressionKind::Literal(lit) = &expr.expression.kind
&& let LiteralValue::String(cooked) = &lit.value
{
Some(Cow::Borrowed(cooked.resolve(lit.span, ctx.source)))
} else {
None
}
}
};
if let Some(tag) = tag_str {
w.raw(",\"customElement\":{\"tag\":");
w.string(&tag);
w.raw("}");
return;
}
}
}
fn write_value_attributes(
w: &mut JsonWriter,
attributes: &[ast::AttributeNode<'_>],
ctx: &Ctx<'_>,
) {
let ctx = ctx.without_comments();
write_array(w, attributes, |w, a| write_attribute_node(w, a, &ctx));
}
fn write_pattern_island(w: &mut JsonWriter, expr: &tsv_ts::ast::Expression<'_>, ctx: &Ctx<'_>) {
let window = pattern_comment_window(expr);
if !ctx.any_comment_in(window.start, window.end) {
write_pattern_embedded(
w,
expr,
ctx.embed_pattern(CommentMode::Off, CommentMode::Off),
);
return;
}
let attach = attach_binding_pattern(expr, ctx.attach_inputs());
let (mode, annotation_mode) = attach.modes();
write_pattern_embedded(w, expr, ctx.embed_pattern(mode, annotation_mode));
}
fn write_optional_fragment(
w: &mut JsonWriter,
fragment: Option<&ast::Fragment<'_>>,
ctx: &Ctx<'_>,
) {
write_or_null(w, fragment, |w, f| write_fragment(w, f, ctx));
}
#[cfg(test)]
mod tests {
use serde_json::Value;
fn convert_svelte(source: &str) -> Value {
let arena = bumpalo::Bump::new();
let root = crate::__private::parse(source, &arena).expect("parse");
serde_json::from_slice(&crate::__private::convert_ast_json_bytes(&root, source))
.expect("wire")
}
#[cfg(feature = "locations")]
fn convert_svelte_with_locations(source: &str) -> Value {
let arena = bumpalo::Bump::new();
let root = crate::__private::parse(source, &arena).expect("parse");
serde_json::from_slice(&crate::__private::convert_ast_json_bytes_with_locations(
&root, source,
))
.expect("wire")
}
#[test]
fn const_tag_declaration_end_is_byte_before_closing_brace() {
let ast = convert_svelte("{#snippet s()}{@const x = 1 }{/snippet}");
let decl = &ast["fragment"]["nodes"][0]["body"]["nodes"][0]["declaration"];
assert_eq!(decl["end"], 28);
let ast = convert_svelte("{#snippet s()}{@const x = 1 }{/snippet}\n{/* c */ y}");
let decl = &ast["fragment"]["nodes"][0]["body"]["nodes"][0]["declaration"];
assert_eq!(decl["end"], 28);
}
#[cfg(feature = "locations")]
fn name_locs(node: &Value, out: &mut Vec<(Value, Value)>) {
match node {
Value::Object(fields) => {
if let Some(loc) = fields.get("name_loc") {
out.push((loc["start"].clone(), loc["end"].clone()));
}
fields.values().for_each(|v| name_locs(v, out));
}
Value::Array(items) => items.iter().for_each(|v| name_locs(v, out)),
_ => {}
}
}
#[test]
#[cfg(feature = "locations")]
fn name_loc_end_shares_its_start_line() {
for term in ["\n", "\r", "\r\n", "\u{2028}", "\u{2029}", "\t", " "] {
for lead in ["", "é\n", "😀", "\r\n\n"] {
let sources = [
format!("{lead}<div{term}class=\"a\"{term}id={term}b></div>"),
format!("{lead}<Foo.Bar{term}x{term}/>"),
format!("{lead}<svelte:head{term}></svelte:head>"),
format!("{lead}<a{term}on:click|once|preventDefault={{f}}{term}>é</a>"),
format!("{lead}<p{term}bind:value={{v}}{term}class:é={{c}}></p>"),
format!("{lead}<input{term}data-é{term}/>"),
format!("{lead}<div{term}{{x}}{term}{{é}}></div>"),
];
for source in &sources {
let units: Vec<u16> = source.encode_utf16().collect();
let mut locs = Vec::new();
name_locs(&convert_svelte_with_locations(source), &mut locs);
assert!(!locs.is_empty(), "{source:?} carries a name_loc");
for (start, end) in locs {
assert_eq!(start["line"], end["line"], "{source:?}");
for point in [&start, &end] {
let character =
point["character"].as_u64().expect("character") as usize;
let before = &units[..character];
let line_start = before
.iter()
.rposition(|&u| u == u16::from(b'\n'))
.map_or(0, |i| i + 1);
let line =
1 + before.iter().filter(|&&u| u == u16::from(b'\n')).count();
assert_eq!(point["line"], line, "{source:?} {point}");
assert_eq!(
point["column"],
character - line_start,
"{source:?} {point}"
);
}
}
}
}
}
}
const SHAPES: &str = concat!(
r#"<div a b="c" d="{e}" f={g} {h} i="j{k}">t{x}<!--c-->"#,
r#"<textarea>y{z}</textarea><textarea></textarea><Foo /></div>"#,
);
#[test]
fn element_and_attribute_shapes_write_these_bytes() {
let arena = bumpalo::Bump::new();
let root = crate::__private::parse(SHAPES, &arena).expect("parse");
let wire = crate::__private::convert_ast_json_bytes(&root, SHAPES);
let expected = concat!(
r#"{"css":null,"js":[],"start":0,"end":111,"type":"Root","fragment":{"type":"Fragment","#,
r#""nodes":[{"type":"RegularElement","start":0,"end":111,"name":"div","#,
r#""attributes":[{"type":"Attribute","start":5,"end":6,"name":"a","value":true},"#,
r#"{"type":"Attribute","start":7,"end":12,"name":"b","value":[{"start":10,"end":11,"#,
r#""type":"Text","raw":"c","data":"c"}]},"#,
r#"{"type":"Attribute","start":13,"end":20,"name":"d","value":[{"type":"ExpressionTag","#,
r#""start":16,"end":19,"expression":{"type":"Identifier","start":17,"end":18,"#,
r#""name":"e"}}]},"#,
r#"{"type":"Attribute","start":21,"end":26,"name":"f","value":{"type":"ExpressionTag","#,
r#""start":23,"end":26,"expression":{"type":"Identifier","start":24,"end":25,"name":"g"}}},"#,
r#"{"type":"Attribute","start":27,"end":30,"name":"h","value":{"type":"ExpressionTag","#,
r#""start":28,"end":29,"expression":{"type":"Identifier","name":"h","start":28,"end":29}}},"#,
r#"{"type":"Attribute","start":31,"end":39,"name":"i","value":[{"start":34,"end":35,"#,
r#""type":"Text","raw":"j","data":"j"},"#,
r#"{"type":"ExpressionTag","start":35,"end":38,"expression":{"type":"Identifier","#,
r#""start":36,"end":37,"name":"k"}}]}],"fragment":{"type":"Fragment","#,
r#""nodes":[{"type":"Text","start":40,"end":41,"raw":"t","data":"t"},"#,
r#"{"type":"ExpressionTag","start":41,"end":44,"expression":{"type":"Identifier","#,
r#""start":42,"end":43,"name":"x"}},"#,
r#"{"type":"Comment","start":44,"end":52,"data":"c"},"#,
r#"{"type":"RegularElement","start":52,"end":77,"name":"textarea","attributes":[],"#,
r#""fragment":{"type":"Fragment","nodes":[{"start":62,"end":63,"type":"Text","raw":"y","#,
r#""data":"y"},"#,
r#"{"type":"ExpressionTag","start":63,"end":66,"expression":{"type":"Identifier","#,
r#""start":64,"end":65,"name":"z"}}]}},"#,
r#"{"type":"RegularElement","start":77,"end":98,"name":"textarea","attributes":[],"#,
r#""fragment":{"type":"Fragment","nodes":[]}},"#,
r#"{"type":"Component","start":98,"end":105,"name":"Foo","attributes":[],"#,
r#""fragment":{"type":"Fragment","nodes":[]}}]}}]},"options":null,"comments":[]}"#
);
assert_eq!(String::from_utf8(wire).expect("UTF-8 wire"), expected);
}
fn shifted(node: &Value, by: u64) -> Value {
match node {
Value::Object(fields) => Value::Object(
fields
.iter()
.map(|(key, value)| {
let moved = match (key.as_str(), value.as_u64()) {
("start" | "end" | "character" | "column", Some(n)) => {
Value::from(n + by)
}
_ => shifted(value, by),
};
(key.clone(), moved)
})
.collect(),
),
Value::Array(items) => Value::Array(items.iter().map(|v| shifted(v, by)).collect()),
other => other.clone(),
}
}
#[test]
fn positions_survive_every_digit_width() {
let wires = |source: &str| {
let arena = bumpalo::Bump::new();
let root = crate::__private::parse(source, &arena).expect("parse");
[
#[cfg(feature = "locations")]
crate::__private::convert_ast_json_bytes_with_locations(&root, source),
crate::__private::convert_ast_json_bytes(&root, source),
]
.map(|wire| serde_json::from_slice::<Value>(&wire).expect("wire"))
};
let bare = wires(SHAPES);
let lead_shell = "<i></i>".len();
for power in [10usize, 100, 1_000, 10_000, 100_000] {
let widest = power + 1;
let narrowest = power.saturating_sub(SHAPES.len() + 1).max(lead_shell);
for lead in narrowest..=widest {
let source = format!("<i>{}</i>{SHAPES}", "p".repeat(lead - lead_shell));
for (padded, bare) in wires(&source).iter().zip(&bare) {
assert_eq!(
padded["fragment"]["nodes"][1],
shifted(&bare["fragment"]["nodes"][0], lead as u64),
"lead {lead}"
);
}
}
}
}
#[test]
#[cfg(feature = "locations")]
fn name_loc_line_repeats_at_every_digit_width() {
for line in [
1usize, 9, 10, 99, 100, 999, 1_000, 9_999, 10_000, 99_999, 100_000,
] {
let source = format!(
"{}<div class=\"a\" {{b}} on:click|once={{f}}><Foo.Bar /></div><svelte:head></svelte:head>",
"\n".repeat(line - 1)
);
let mut locs = Vec::new();
name_locs(&convert_svelte_with_locations(&source), &mut locs);
assert_eq!(locs.len(), 6, "line {line}");
for (start, end) in locs {
assert_eq!(start["line"], line, "line {line}");
assert_eq!(end["line"], line, "line {line}");
}
}
}
#[test]
fn whitespace_window_covers_the_collapsible_class() {
for byte in (0..=u8::MAX).filter(|&b| crate::ast::is_collapsible_ws(b)) {
for len in [1, 16, 17] {
let text = String::from_utf8(vec![byte; len]).expect("an ASCII run");
let quoted = serde_json::to_string(&text).expect("serde_json serializes a str");
let expected = format!("{quoted},\"data\":{quoted}");
for cap in [0, 256] {
let mut w = super::JsonWriter::with_capacity(cap);
w.string_pair_whitespace(text.as_bytes(), b",\"data\":");
assert_eq!(
String::from_utf8(w.into_bytes()).expect("UTF-8 wire"),
expected,
"{byte:#04x} x {len} (capacity {cap})"
);
}
}
}
}
#[test]
fn declaration_tag_end_is_acorns_declaration_end() {
let ast = convert_svelte("{let x = 1 }");
assert_eq!(ast["fragment"]["nodes"][0]["declaration"]["end"], 10);
let ast = convert_svelte("{let x = 1 }\n{/* c */ y}");
assert_eq!(ast["fragment"]["nodes"][0]["declaration"]["end"], 10);
}
}