use vize_atelier_core::ast::{
DirectiveNode, ElementNode, ExpressionNode, PropNode, RootNode, SimpleExpressionNode,
TemplateChildNode,
};
use vize_carton::{
camelize, capitalize, is_builtin_directive, is_native_tag, is_simple_identifier, FxHashSet,
String, ToCompactString,
};
use vize_croquis::builtins::is_builtin_component;
#[derive(Debug, Clone, Default)]
pub struct TemplateUsedIdentifiers {
pub used_ids: FxHashSet<String>,
pub v_model_ids: FxHashSet<String>,
}
pub fn is_used_in_template(identifier: &str, root: &RootNode) -> bool {
resolve_template_used_identifiers(root)
.used_ids
.contains(identifier)
}
pub fn resolve_template_v_model_identifiers(root: &RootNode) -> FxHashSet<String> {
resolve_template_analysis_result(root, false).v_model_ids
}
pub fn resolve_template_used_identifiers(root: &RootNode) -> TemplateUsedIdentifiers {
resolve_template_analysis_result(root, true)
}
fn resolve_template_analysis_result(
root: &RootNode,
collect_used_ids: bool,
) -> TemplateUsedIdentifiers {
let mut result = TemplateUsedIdentifiers::default();
for child in root.children.iter() {
walk_node(child, &mut result, collect_used_ids);
}
result
}
fn walk_node(
node: &TemplateChildNode,
result: &mut TemplateUsedIdentifiers,
collect_used_ids: bool,
) {
match node {
TemplateChildNode::Element(element) => {
walk_element(element, result, collect_used_ids);
}
TemplateChildNode::Interpolation(interpolation) => {
if collect_used_ids {
extract_identifiers_from_expression(&interpolation.content, &mut result.used_ids);
}
}
TemplateChildNode::If(if_node) => {
for branch in if_node.branches.iter() {
if collect_used_ids {
if let Some(ref condition) = branch.condition {
extract_identifiers_from_expression(condition, &mut result.used_ids);
}
}
for child in branch.children.iter() {
walk_node(child, result, collect_used_ids);
}
}
}
TemplateChildNode::For(for_node) => {
if collect_used_ids {
extract_identifiers_from_expression(&for_node.source, &mut result.used_ids);
}
for child in for_node.children.iter() {
walk_node(child, result, collect_used_ids);
}
}
TemplateChildNode::TextCall(text_call) => {
if collect_used_ids {
match &text_call.content {
vize_atelier_core::ast::TextCallContent::Interpolation(interp) => {
extract_identifiers_from_expression(&interp.content, &mut result.used_ids);
}
vize_atelier_core::ast::TextCallContent::Compound(compound) => {
extract_identifiers_from_compound(compound, &mut result.used_ids);
}
_ => {}
}
}
}
TemplateChildNode::CompoundExpression(compound) => {
if collect_used_ids {
extract_identifiers_from_compound(compound, &mut result.used_ids);
}
}
_ => {}
}
}
fn walk_element(
element: &ElementNode,
result: &mut TemplateUsedIdentifiers,
collect_used_ids: bool,
) {
let mut tag = element.tag.as_str();
if let Some(dot_pos) = tag.find('.') {
tag = &tag[..dot_pos];
}
if !is_native_tag(tag) && !is_builtin_component(tag) && collect_used_ids {
let camelized = camelize(tag);
let capitalized = capitalize(&camelized);
result.used_ids.insert(camelized.to_compact_string());
result.used_ids.insert(capitalized.to_compact_string());
}
for prop in element.props.iter() {
match prop {
PropNode::Directive(directive) => {
process_directive(directive, result, collect_used_ids);
}
PropNode::Attribute(attr) => {
if collect_used_ids && attr.name.as_str() == "ref" {
if let Some(ref value) = attr.value {
if !value.content.is_empty() {
result.used_ids.insert(value.content.to_compact_string());
}
}
}
}
}
}
for child in element.children.iter() {
walk_node(child, result, collect_used_ids);
}
}
fn process_directive(
directive: &DirectiveNode,
result: &mut TemplateUsedIdentifiers,
collect_used_ids: bool,
) {
let name = directive.name.as_str();
if collect_used_ids && !is_builtin_directive(name) {
let camel = camelize(name);
let cap = capitalize(&camel);
let mut directive_name = String::with_capacity(1 + cap.len());
directive_name.push('v');
directive_name.push_str(&cap);
result.used_ids.insert(directive_name);
}
if name == "model" {
if let Some(ref exp) = directive.exp {
if let ExpressionNode::Simple(simple_exp) = exp {
let exp_string = simple_exp.content.trim();
if is_simple_identifier(exp_string) && exp_string != "undefined" {
result.v_model_ids.insert(exp_string.to_compact_string());
}
}
}
}
if collect_used_ids {
if let Some(ref arg) = directive.arg {
if let ExpressionNode::Simple(simple_arg) = arg {
if !simple_arg.is_static {
extract_identifiers_from_expression(arg, &mut result.used_ids);
}
}
}
}
if collect_used_ids {
if name == "for" {
if let Some(ref for_result) = directive.for_parse_result {
extract_identifiers_from_expression(&for_result.source, &mut result.used_ids);
} else if let Some(ref exp) = directive.exp {
extract_v_for_source_identifiers(exp, &mut result.used_ids);
}
} else if let Some(ref exp) = directive.exp {
extract_identifiers_from_expression(exp, &mut result.used_ids);
} else if name == "bind" {
if let Some(ref arg) = directive.arg {
if let ExpressionNode::Simple(simple_arg) = arg {
if simple_arg.is_static {
let identifier = camelize(simple_arg.content.as_str());
result.used_ids.insert(identifier.to_compact_string());
}
}
}
}
}
}
fn extract_v_for_source_identifiers(exp: &ExpressionNode, ids: &mut FxHashSet<String>) {
if let ExpressionNode::Simple(simple) = exp {
let content = simple.content.as_str();
let source_part = if let Some(pos) = content.find(" in ") {
&content[pos + 4..]
} else if let Some(pos) = content.find(" of ") {
&content[pos + 4..]
} else {
content
};
let source_trimmed = source_part.trim();
if !source_trimmed.is_empty() && is_simple_identifier(source_trimmed) {
ids.insert(source_trimmed.to_compact_string());
}
}
}
fn extract_identifiers_from_expression(node: &ExpressionNode, ids: &mut FxHashSet<String>) {
match node {
ExpressionNode::Simple(simple) => {
extract_identifiers_from_simple_expression(simple, ids);
}
ExpressionNode::Compound(compound) => {
extract_identifiers_from_compound(compound, ids);
}
}
}
fn extract_identifiers_from_simple_expression(
node: &SimpleExpressionNode,
ids: &mut FxHashSet<String>,
) {
if let Some(ref identifiers) = node.identifiers {
for ident in identifiers.iter() {
ids.insert(ident.to_compact_string());
}
return;
}
if node.is_static {
return;
}
let content = node.content.trim();
if !content.is_empty() && is_simple_identifier(content) {
ids.insert(content.to_compact_string());
}
}
fn extract_identifiers_from_compound(
node: &vize_atelier_core::ast::CompoundExpressionNode,
ids: &mut FxHashSet<String>,
) {
if let Some(ref identifiers) = node.identifiers {
for ident in identifiers.iter() {
ids.insert(ident.to_compact_string());
}
return;
}
for child in node.children.iter() {
match child {
vize_atelier_core::ast::CompoundExpressionChild::Simple(simple) => {
extract_identifiers_from_simple_expression(simple, ids);
}
vize_atelier_core::ast::CompoundExpressionChild::Compound(compound) => {
extract_identifiers_from_compound(compound, ids);
}
vize_atelier_core::ast::CompoundExpressionChild::Interpolation(interp) => {
extract_identifiers_from_expression(&interp.content, ids);
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::{is_used_in_template, resolve_template_used_identifiers, TemplateUsedIdentifiers};
use vize_atelier_core::parser::parse;
use vize_carton::Bump;
fn analyze_template(source: &str) -> TemplateUsedIdentifiers {
let allocator = Bump::new();
let (root, _) = parse(&allocator, source);
resolve_template_used_identifiers(&root)
}
#[test]
fn test_component_usage() {
let result = analyze_template("<MyComponent />");
assert!(result.used_ids.contains("MyComponent"));
}
#[test]
fn test_component_usage_kebab() {
let result = analyze_template("<my-component />");
assert!(result.used_ids.contains("myComponent"));
assert!(result.used_ids.contains("MyComponent"));
}
#[test]
fn test_component_with_dot() {
let result = analyze_template("<Foo.Bar />");
assert!(result.used_ids.contains("Foo"));
}
#[test]
fn test_interpolation() {
let result = analyze_template("<div>{{ msg }}</div>");
assert!(result.used_ids.contains("msg"));
}
#[test]
fn test_v_bind() {
let result = analyze_template("<div :class=\"classes\"></div>");
assert!(result.used_ids.contains("classes"));
}
#[test]
fn test_v_on() {
let result = analyze_template("<div @click=\"handleClick\"></div>");
assert!(result.used_ids.contains("handleClick"));
}
#[test]
fn test_v_model() {
let result = analyze_template("<input v-model=\"value\" />");
assert!(result.v_model_ids.contains("value"));
assert!(result.used_ids.contains("value"));
}
#[test]
fn test_v_model_complex() {
let result = analyze_template("<input v-model=\"obj.value\" />");
assert!(!result.v_model_ids.contains("obj.value"));
}
#[test]
fn test_v_for() {
let result = analyze_template("<div v-for=\"item in items\">{{ item }}</div>");
assert!(result.used_ids.contains("items"));
}
#[test]
fn test_v_if() {
let result = analyze_template("<div v-if=\"show\">content</div>");
assert!(result.used_ids.contains("show"));
}
#[test]
fn test_custom_directive() {
let result = analyze_template("<div v-focus></div>");
assert!(result.used_ids.contains("vFocus"));
}
#[test]
fn test_ref_attribute() {
let result = analyze_template("<div ref=\"myRef\"></div>");
assert!(result.used_ids.contains("myRef"));
}
#[test]
fn test_native_tag_not_added() {
let result = analyze_template("<div></div>");
assert!(!result.used_ids.contains("div"));
assert!(!result.used_ids.contains("Div"));
}
#[test]
fn test_builtin_directive_not_added() {
let result = analyze_template("<div v-if=\"show\" v-show=\"visible\"></div>");
assert!(!result.used_ids.contains("vIf"));
assert!(!result.used_ids.contains("vShow"));
}
#[test]
fn test_is_used_in_template() {
let allocator = Bump::new();
let (root, _) = parse(&allocator, "<div>{{ msg }}</div>");
assert!(is_used_in_template("msg", &root));
assert!(!is_used_in_template("other", &root));
}
}