use sha2::{Digest, Sha256};
use crate::ast::*;
use crate::util::bytes_to_hex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Region {
Static,
Dynamic,
}
#[derive(Debug, Clone)]
pub struct BindingMap {
pub static_indices: Vec<usize>,
pub dynamic_indices: Vec<usize>,
pub expr_count: usize,
}
pub fn analyze_template(nodes: &[Node]) -> BindingMap {
let mut static_indices = Vec::new();
let mut dynamic_indices = Vec::new();
let mut expr_count = 0;
for (i, node) in nodes.iter().enumerate() {
let (region, count) = classify_node_inner(node);
expr_count += count;
if region == Region::Dynamic {
dynamic_indices.push(i);
} else {
static_indices.push(i);
}
}
BindingMap {
static_indices,
dynamic_indices,
expr_count,
}
}
pub fn classify_node(node: &Node) -> Region {
classify_node_inner(node).0
}
fn classify_node_inner(node: &Node) -> (Region, usize) {
match node {
Node::Text(parts) => {
let expr_count = parts
.iter()
.filter(|p| matches!(p, TextPart::Expr(_)))
.count();
if expr_count > 0 {
(Region::Dynamic, expr_count)
} else {
(Region::Static, 0)
}
}
Node::Element(el) => classify_element(el),
Node::If(block) => {
let mut count = 1; for n in &block.then_children {
count += classify_node_inner(n).1;
}
if let Some(els) = &block.else_children {
for n in els {
count += classify_node_inner(n).1;
}
}
(Region::Dynamic, count)
}
Node::For(block) => {
let mut count = 1; for n in &block.body {
count += classify_node_inner(n).1;
}
(Region::Dynamic, count)
}
Node::Match(block) => {
let mut count = 1; for arm in &block.arms {
for n in &arm.body {
count += classify_node_inner(n).1;
}
}
(Region::Dynamic, count)
}
Node::LetDecl(_) => (Region::Dynamic, 1),
Node::Include(inc) => (Region::Dynamic, inc.props.len().max(1)),
Node::Embed(embed) => (Region::Dynamic, embed.props.len().max(1)),
Node::RawText(_) => (Region::Dynamic, 1),
}
}
fn classify_element(el: &Element) -> (Region, usize) {
let mut expr_count = 0;
let mut is_dynamic = false;
if el.tag == "slot-rotate" {
return (Region::Dynamic, 1);
}
expr_count += el.bindings.len();
if !el.bindings.is_empty() {
is_dynamic = true;
}
expr_count += el.conditional_classes.len();
if !el.conditional_classes.is_empty() {
is_dynamic = true;
}
if el.classes.iter().any(|c| c.contains('{')) {
is_dynamic = true;
}
if !el.animations.is_empty() {
is_dynamic = true;
expr_count += el.animations.len();
}
if !el.event_handlers.is_empty() {
is_dynamic = true;
expr_count += el.event_handlers.len();
}
for child in &el.children {
let (child_region, child_count) = classify_node_inner(child);
expr_count += child_count;
if child_region == Region::Dynamic {
is_dynamic = true;
}
}
if is_dynamic {
(Region::Dynamic, expr_count)
} else {
(Region::Static, 0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Fingerprint {
pub content_hash: String,
pub section: Option<String>,
pub stage: String,
}
impl Fingerprint {
pub fn new(source: &str, section: Option<&str>, stage: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(source.as_bytes());
let hash = bytes_to_hex(hasher.finalize().as_ref());
Self {
content_hash: hash,
section: section.map(|s| s.to_string()),
stage: stage.to_string(),
}
}
pub fn cache_key(&self) -> String {
let sec = self.section.as_deref().unwrap_or("_");
format!("{}-{}-{}", &self.content_hash[..16], sec, self.stage)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_deterministic() {
let a = Fingerprint::new("hello", None, "render");
let b = Fingerprint::new("hello", None, "render");
assert_eq!(a.content_hash, b.content_hash);
assert_eq!(a.cache_key(), b.cache_key());
}
#[test]
fn different_source_different_fingerprint() {
let a = Fingerprint::new("hello", None, "render");
let b = Fingerprint::new("world", None, "render");
assert_ne!(a.content_hash, b.content_hash);
}
#[test]
fn section_in_cache_key() {
let fp = Fingerprint::new("src", Some("Card"), "codegen");
assert!(fp.cache_key().contains("Card"));
}
}