use vize_l0::{Allocator, Span};
use vize_l1::embed::{
DecodeSegmentKind, Embed, Grammar, Lang, Shape, prepare_attribute_value,
syntax::{NativeSyntax, parse_once},
};
use vize_l2::expr::{
JsExpr,
js::{JsCoordinates, JsSegment},
};
use vize_l2::resolution::{BindingId, BindingLookup, resolve_expression};
use vize_l4::expr::vue::{Access, AccessStyle, Binding, VueAccess};
use vize_l4::expr::{EmitErrorKind, write_expression};
use vize_l4::runtime::Vocabulary;
use vize_l4::write::{NoLinks, Recorded, Writer};
struct Context;
impl BindingLookup for Context {
fn lookup(&self, name: &str) -> Option<BindingId> {
match name {
"é" | "fj" | "value" => Some(BindingId::new(0)),
"作者" => Some(BindingId::new(1)),
_ => None,
}
}
}
const VOCABULARY: Vocabulary = Vocabulary { modules: &[] };
#[cfg(test)]
fn transfer<'a>(
arena: &'a Allocator,
file: &'a str,
syntax: &'a NativeSyntax<'a>,
) -> &'a JsExpr<'a> {
let prepared = syntax.source();
let mut segments = vize_l0::Vec::new_in(&arena);
if let Some(map) = prepared.decode_map() {
for segment in map.segments() {
segments.push(JsSegment {
decoded: segment.decoded(),
authored: segment.authored(),
entity: segment.kind() == DecodeSegmentKind::Entity,
});
}
}
let segments = arena.alloc(segments);
let coordinates = JsCoordinates::checked(
file,
prepared.text(),
prepared.span(),
2,
segments.as_slice(),
)
.unwrap();
JsExpr::from_retained_in(
arena,
syntax.expression().unwrap(),
prepared.text(),
prepared.span(),
coordinates,
)
.unwrap()
}
#[cfg(test)]
fn access() -> VueAccess<'static> {
VueAccess::checked(
&[
Binding {
id: BindingId::new(0),
access: Access::Context,
},
Binding {
id: BindingId::new(1),
access: Access::Context,
},
],
AccessStyle::Function,
&VOCABULARY,
)
.unwrap()
}
#[test]
fn actual_attribute_decode_and_once_parsed_ast_keep_entity_unicode_named_links() {
let arena = Allocator::default();
let file = "<x a=\"é + é + 作者\">";
let prepared = prepare_attribute_value(&arena, file, Span::new(6, 28)).unwrap();
assert_eq!(prepared.text(), "é + é + 作者");
let syntax = parse_once(
&arena,
Embed {
source: prepared,
grammar: Grammar {
lang: Lang::Js,
shape: Shape::Expr,
},
},
);
assert!(syntax.hole().is_none());
let expression = transfer(&arena, file, &syntax);
let table = resolve_expression(expression, &Context).unwrap();
assert!(core::ptr::eq(expression.ast, syntax.expression().unwrap()));
assert!(core::ptr::eq(
table.expression().ast,
syntax.expression().unwrap()
));
assert_eq!(
table
.occurrences()
.iter()
.map(|entry| entry.span)
.collect::<Vec<_>>(),
vec![Span::new(0, 2), Span::new(5, 7), Span::new(10, 16)]
);
let mut writer = Writer::<Recorded>::default();
writer.push("out=");
write_expression(&mut writer, file, &table, &access()).unwrap();
let output = writer.finish();
assert_eq!(output.text.as_str(), "out=_ctx.é + _ctx.é + _ctx.作者");
let links: Vec<_> = output
.links
.links()
.iter()
.filter(|link| link.name.is_some())
.map(|link| {
(
link.generated,
link.authored,
link.name.as_ref().unwrap().as_str(),
)
})
.collect();
assert_eq!(
links,
vec![
(Span::new(4, 11), Span::new(6, 8), "é"),
(Span::new(14, 21), Span::new(11, 19), "é"),
(Span::new(24, 35), Span::new(22, 28), "作者"),
]
);
let map = output.into_document().source_map("input.vue", file);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&map).unwrap(),
serde_json::json!({
"file": "input.vue",
"mappings": "IAAMA,MAAC,GAAGA,MAAQ,GAAGC",
"names": ["é", "作者"],
"sources": ["input.vue"],
"sourcesContent": [file],
"version": 3,
})
);
let mut plain = Writer::<NoLinks>::default();
plain.push("out=");
write_expression(&mut plain, file, &table, &access()).unwrap();
assert_eq!(plain.as_str(), "out=_ctx.é + _ctx.é + _ctx.作者");
}
#[test]
fn equal_byte_length_multi_scalar_entity_is_copied_whole_and_never_split() {
let arena = Allocator::default();
let file = "<x a=\"'∾̳' + value\">";
let span = Span::new(6, file.len() as u32 - 2);
let prepared = prepare_attribute_value(&arena, file, span).unwrap();
assert_eq!(prepared.text(), "'∾\u{333}' + value");
assert_eq!(prepared.text().len(), (span.end - span.start) as usize);
assert!(prepared.authored_span(Span::new(1, 4)).is_err());
let syntax = parse_once(
&arena,
Embed {
source: prepared,
grammar: Grammar {
lang: Lang::Js,
shape: Shape::Expr,
},
},
);
let expression = transfer(&arena, file, &syntax);
assert!(expression.authored_span(Span::new(1, 4)).is_none());
let table = resolve_expression(expression, &Context).unwrap();
let mut writer = Writer::<Recorded>::default();
write_expression(&mut writer, file, &table, &access()).unwrap();
let output = writer.finish();
assert_eq!(output.text.as_str(), "'∾\u{333}' + _ctx.value");
assert_eq!(output.links.links()[0].authored, Span::new(6, 16));
assert_eq!(output.links.links()[1].authored, Span::new(16, 21));
}
#[test]
fn multi_character_identifier_entity_maps_one_complete_rewrite_and_rejects_stale_source() {
let arena = Allocator::default();
let file = "<x a=\"fj\">";
let prepared = prepare_attribute_value(&arena, file, Span::new(6, 13)).unwrap();
assert_eq!(prepared.text(), "fj");
let syntax = parse_once(
&arena,
Embed {
source: prepared,
grammar: Grammar {
lang: Lang::Js,
shape: Shape::Expr,
},
},
);
let expression = transfer(&arena, file, &syntax);
assert!(expression.authored_span(Span::new(0, 1)).is_none());
let table = resolve_expression(expression, &Context).unwrap();
let mut writer = Writer::<Recorded>::default();
write_expression(&mut writer, file, &table, &access()).unwrap();
let output = writer.finish();
assert_eq!(output.text.as_str(), "_ctx.fj");
assert_eq!(output.links.links()[0].authored, Span::new(6, 13));
let mut writer = Writer::<Recorded>::default();
writer.push("unchanged");
assert_eq!(
write_expression(&mut writer, "<x a=\"&xxxxx;\">", &table, &access())
.unwrap_err()
.kind,
EmitErrorKind::SourceMismatch
);
assert_eq!(writer.as_str(), "unchanged");
}