mod context;
pub mod element;
pub mod structural;
pub mod traverse;
use vize_carton::{Box, Bump, SmallVec, String, Vec, profile};
use vize_croquis::{Croquis, ScopeChain};
use crate::errors::CompilerError;
use crate::options::TransformOptions;
use crate::runtime_helpers::RuntimeHelpers;
use crate::*;
use traverse::traverse_children;
pub type NodeTransform<'a> =
fn(&mut TransformContext<'a>, &mut TemplateChildNode<'a>) -> Option<ExitFns<'a>>;
pub type ExitFn<'a> = std::boxed::Box<dyn FnOnce(&mut TransformContext<'a>) + 'a>;
pub type ExitFns<'a> = SmallVec<[ExitFn<'a>; 2]>;
pub type DirectiveTransform<'a> = fn(
&mut TransformContext<'a>,
&mut ElementNode<'a>,
&DirectiveNode<'a>,
) -> Option<DirectiveTransformResult<'a>>;
pub struct DirectiveTransformResult<'a> {
pub props: Vec<'a, PropNode<'a>>,
pub remove_directive: bool,
pub ssr_tag_type: Option<u8>,
}
pub type StructuralDirectiveTransform<'a> =
fn(&mut TransformContext<'a>, &mut ElementNode<'a>, &DirectiveNode<'a>) -> Option<ExitFn<'a>>;
pub struct TransformContext<'a> {
pub allocator: &'a Bump,
pub options: TransformOptions,
pub source: String,
pub root: Option<*mut RootNode<'a>>,
pub parent: Option<ParentNode<'a>>,
pub grandparent: Option<ParentNode<'a>>,
pub current_node: Option<*mut TemplateChildNode<'a>>,
pub child_index: usize,
pub helpers: RuntimeHelpers,
pub components: std::vec::Vec<String>,
pub directives: std::vec::Vec<String>,
#[cfg(feature = "legacy")]
pub(crate) filters: std::vec::Vec<String>,
pub hoists: Vec<'a, Option<JsChildNode<'a>>>,
pub cached: Vec<'a, Option<Box<'a, CacheExpression<'a>>>>,
pub temps: u32,
pub scope_chain: ScopeChain,
pub scoped_slots: u32,
pub in_v_once: bool,
pub in_ssr: bool,
pub errors: std::vec::Vec<CompilerError>,
pub(crate) template_syntax_quirks: bool,
pub(crate) node_removed: bool,
pub(crate) analysis: Option<&'a Croquis>,
pub(crate) hoisted_scope_id: Option<String>,
}
#[derive(Clone, Copy)]
pub enum ParentNode<'a> {
Root(*mut RootNode<'a>),
Element(*mut ElementNode<'a>),
If(*mut IfNode<'a>),
IfBranch(*mut IfBranchNode<'a>),
For(*mut ForNode<'a>),
}
impl<'a> ParentNode<'a> {
#[allow(clippy::mut_from_ref)]
pub fn children_mut(&self) -> &mut Vec<'a, TemplateChildNode<'a>> {
unsafe {
match self {
ParentNode::Root(r) => &mut (*(*r)).children,
ParentNode::Element(e) => &mut (*(*e)).children,
ParentNode::If(_) => {
panic!("IfNode doesn't have direct children")
}
ParentNode::IfBranch(b) => &mut (*(*b)).children,
ParentNode::For(f) => &mut (*(*f)).children,
}
}
}
}
pub fn transform<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
) -> std::vec::Vec<CompilerError> {
transform_inner(allocator, root, options, analysis, false, None)
}
pub fn transform_with_template_syntax_quirks<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
) -> std::vec::Vec<CompilerError> {
transform_inner(allocator, root, options, analysis, true, None)
}
#[deprecated(note = "use transform_with_template_syntax_quirks instead")]
pub fn transform_with_vue_parser_quirks<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
) -> std::vec::Vec<CompilerError> {
transform_with_template_syntax_quirks(allocator, root, options, analysis)
}
#[doc(hidden)]
pub fn transform_with_hoisted_scope_id<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
transform_inner(allocator, root, options, analysis, false, hoisted_scope_id)
}
#[doc(hidden)]
pub fn transform_with_template_syntax_quirks_and_hoisted_scope_id<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
transform_inner(allocator, root, options, analysis, true, hoisted_scope_id)
}
#[doc(hidden)]
#[deprecated(note = "use transform_with_template_syntax_quirks_and_hoisted_scope_id instead")]
pub fn transform_with_vue_parser_quirks_and_hoisted_scope_id<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
transform_with_template_syntax_quirks_and_hoisted_scope_id(
allocator,
root,
options,
analysis,
hoisted_scope_id,
)
}
fn transform_inner<'a>(
allocator: &'a Bump,
root: &mut RootNode<'a>,
options: TransformOptions,
analysis: Option<&'a Croquis>,
template_syntax_quirks: bool,
hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
let source = root.source.clone();
let mut ctx = if let Some(analysis) = analysis {
TransformContext::with_analysis_and_template_syntax_quirks(
allocator,
source,
options,
analysis,
template_syntax_quirks,
)
} else {
TransformContext::new_with_template_syntax_quirks(
allocator,
source,
options,
template_syntax_quirks,
)
};
ctx.hoisted_scope_id = hoisted_scope_id;
ctx.root = Some(root as *mut _);
#[cfg(feature = "legacy")]
{
use vize_armature::legacy::LegacyDialectCapabilities;
let caps = LegacyDialectCapabilities::for_dialect(ctx.options.dialect);
crate::transforms::legacy::desugar_legacy_template(allocator, root, caps);
}
profile!(
"atelier.transform.traverse_children",
traverse_children(&mut ctx, ParentNode::Root(root as *mut _))
);
use crate::transforms::hoist_static::hoist_static;
profile!(
"atelier.transform.hoist_static",
hoist_static(&mut ctx, &mut root.children)
);
profile!(
"atelier.transform.create_root_codegen",
create_root_codegen(&mut ctx, root)
);
for helper in ctx.helpers.iter() {
root.helpers.push(helper);
}
for component in ctx.components.into_iter() {
root.components.push(component);
}
for directive in ctx.directives.into_iter() {
root.directives.push(directive);
}
#[cfg(feature = "legacy")]
for filter in ctx.filters.into_iter() {
root.filters.push(filter);
}
for hoist in ctx.hoists.into_iter() {
root.hoists.push(hoist);
}
root.temps = ctx.temps;
root.transformed = true;
ctx.errors
}
fn create_root_codegen<'a>(ctx: &mut TransformContext<'a>, root: &mut RootNode<'a>) {
if root.children.is_empty() {
return;
}
if root.children.len() > 1 {
ctx.helper(RuntimeHelper::OpenBlock);
ctx.helper(RuntimeHelper::CreateElementBlock);
ctx.helper(RuntimeHelper::Fragment);
}
root.codegen_node = None;
}
#[cfg(test)]
#[allow(clippy::disallowed_macros)]
mod tests {
use super::{transform, transform_with_template_syntax_quirks};
use crate::codegen::generate;
use crate::options::{CodegenOptions, TransformOptions};
use crate::parser::parse;
use bumpalo::Bump;
#[test]
fn test_transform_simple_element() {
assert_transform!("<div>hello</div>" => helpers: [CreateElementVNode]);
}
#[test]
fn test_transform_interpolation() {
assert_transform!("{{ msg }}" => helpers: [ToDisplayString]);
}
#[test]
fn test_transform_component() {
assert_transform!("<MyComponent></MyComponent>" => components: ["MyComponent"]);
assert_transform!("<MyComponent></MyComponent>" => helpers: [ResolveComponent]);
}
#[test]
fn test_transform_pascal_case_dynamic_component() {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, r#"<Component :is="current" />"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
transform(&allocator, &mut root, TransformOptions::default(), None);
assert!(
!root
.components
.iter()
.any(|component| component.as_str() == "Component"),
"Dynamic component special tag should not be tracked as a resolved component"
);
assert!(
!root
.helpers
.iter()
.any(|helper| matches!(helper, crate::RuntimeHelper::ResolveComponent)),
"Dynamic component special tag should not request resolveComponent"
);
}
#[test]
fn test_transform_v_if() {
assert_transform!("<div v-if=\"show\">hello</div>" => helpers: [OpenBlock, CreateBlock, Fragment, CreateComment]);
}
#[test]
fn test_transform_v_for() {
assert_transform!("<div v-for=\"item in items\">{{ item }}</div>" => helpers: [RenderList, OpenBlock, CreateBlock, Fragment]);
}
#[test]
fn test_transform_v_for_rejects_unmatched_edge_parens_by_default() {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, r#"<div v-for="item) in items"></div>"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
transform(&allocator, &mut root, TransformOptions::default(), None);
assert!(
!matches!(&root.children[0], crate::TemplateChildNode::For(_)),
"strict parser mode should not accept unmatched v-for alias parens"
);
}
#[test]
fn test_transform_v_for_template_syntax_quirks_accepts_unmatched_edge_parens() {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, r#"<div v-for="item) in items"></div>"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
transform_with_template_syntax_quirks(
&allocator,
&mut root,
TransformOptions::default(),
None,
);
match &root.children[0] {
crate::TemplateChildNode::For(for_node) => match &for_node.value_alias {
Some(crate::ExpressionNode::Simple(value)) => {
assert_eq!(value.content.as_str(), "item");
}
_ => panic!("expected value alias"),
},
other => panic!("expected ForNode, got {:?}", std::mem::discriminant(other)),
}
}
#[test]
fn test_v_if_creates_if_node() {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, r#"<div v-if="show">visible</div>"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
transform(&allocator, &mut root, TransformOptions::default(), None);
assert_eq!(
root.children.len(),
1,
"Should have 1 child after transform"
);
match &root.children[0] {
crate::TemplateChildNode::If(if_node) => {
assert_eq!(if_node.branches.len(), 1, "Should have 1 branch");
let branch = &if_node.branches[0];
assert!(branch.condition.is_some(), "Branch should have condition");
}
other => panic!("Expected IfNode, got {:?}", std::mem::discriminant(other)),
}
}
#[test]
fn test_v_if_else_creates_branches() {
let allocator = Bump::new();
let (mut root, errors) = parse(
&allocator,
r#"<div v-if="show">yes</div><div v-else>no</div>"#,
);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
transform(&allocator, &mut root, TransformOptions::default(), None);
assert_eq!(
root.children.len(),
1,
"Should have 1 child (IfNode) after transform, got {}",
root.children.len()
);
match &root.children[0] {
crate::TemplateChildNode::If(if_node) => {
assert_eq!(
if_node.branches.len(),
2,
"Should have 2 branches (if + else)"
);
assert!(
if_node.branches[0].condition.is_some(),
"First branch should have condition"
);
assert!(
if_node.branches[1].condition.is_none(),
"Second branch (else) should not have condition"
);
}
other => panic!("Expected IfNode, got {:?}", std::mem::discriminant(other)),
}
}
#[test]
fn test_v_for_creates_for_node() {
let allocator = Bump::new();
let (mut root, errors) =
parse(&allocator, r#"<div v-for="item in items">{{ item }}</div>"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
transform(&allocator, &mut root, TransformOptions::default(), None);
assert_eq!(
root.children.len(),
1,
"Should have 1 child after transform"
);
match &root.children[0] {
crate::TemplateChildNode::For(for_node) => {
match &for_node.source {
crate::ExpressionNode::Simple(exp) => {
assert_eq!(exp.content.as_str(), "items", "Source should be 'items'");
}
_ => panic!("Expected Simple expression for source"),
}
assert!(for_node.value_alias.is_some(), "Should have value alias");
match for_node.value_alias.as_ref().unwrap() {
crate::ExpressionNode::Simple(exp) => {
assert_eq!(exp.content.as_str(), "item", "Value alias should be 'item'");
}
_ => panic!("Expected Simple expression for value alias"),
}
}
other => panic!("Expected ForNode, got {:?}", std::mem::discriminant(other)),
}
}
#[test]
fn test_codegen_v_if() {
let allocator = Bump::new();
let (mut root, _) = parse(&allocator, r#"<div v-if="show">visible</div>"#);
transform(&allocator, &mut root, TransformOptions::default(), None);
let result = generate(&root, CodegenOptions::default());
insta::assert_snapshot!(result.code.as_str());
}
}