1extern crate proc_macro;
2
3use anyhow::Result;
4use camino::Utf8PathBuf;
5use diff::Patches;
6use node::LNode;
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::HashMap,
11 fs::File,
12 io::Read,
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16use syn::{
17 spanned::Spanned,
18 visit::{self, Visit},
19 Macro,
20};
21use walkdir::WalkDir;
22
23pub mod diff;
24pub mod node;
25pub mod parsing;
26
27pub const HOT_RELOAD_JS: &str = include_str!("patch.js");
28
29#[derive(Debug, Clone, Default)]
30pub struct ViewMacros {
31 views: Arc<RwLock<HashMap<Utf8PathBuf, Vec<MacroInvocation>>>>,
33}
34
35impl ViewMacros {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn update_from_paths<T: AsRef<Path>>(&self, paths: &[T]) -> Result<()> {
41 let mut views = HashMap::new();
42
43 for path in paths {
44 for entry in WalkDir::new(path).into_iter().flatten() {
45 if entry.file_type().is_file() {
46 let path: PathBuf = entry.path().into();
47 let path = Utf8PathBuf::try_from(path)?;
48 if path.extension() == Some("rs") || path.ends_with(".rs") {
49 let macros = Self::parse_file(&path)?;
50 let entry = views.entry(path.clone()).or_default();
51 *entry = macros;
52 }
53 }
54 }
55 }
56
57 *self.views.write() = views;
58
59 Ok(())
60 }
61
62 pub fn parse_file(path: &Utf8PathBuf) -> Result<Vec<MacroInvocation>> {
63 let mut file = File::open(path)?;
64 let mut content = String::new();
65 file.read_to_string(&mut content)?;
66 let ast = syn::parse_file(&content)?;
67
68 let mut visitor = ViewMacroVisitor::default();
69 visitor.visit_file(&ast);
70 let mut views = Vec::new();
71 for view in visitor.views {
72 let span = view.span();
73 let id = span_to_stable_id(path, span.start().line);
74 let tokens = view.tokens.clone().into_iter();
75 let rsx = rstml::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
77 let template = LNode::parse_view(rsx)?;
78 views.push(MacroInvocation { id, template })
79 }
80 Ok(views)
81 }
82
83 pub fn patch(&self, path: &Utf8PathBuf) -> Result<Option<Patches>> {
84 let new_views = Self::parse_file(path)?;
85 let mut lock = self.views.write();
86 let diffs = match lock.get(path) {
87 None => return Ok(None),
88 Some(current_views) => {
89 if current_views.len() == new_views.len() {
90 let mut diffs = Vec::new();
91 for (current_view, new_view) in current_views.iter().zip(&new_views) {
92 if current_view.id == new_view.id && current_view.template != new_view.template {
93 diffs.push((current_view.id.clone(), current_view.template.diff(&new_view.template)));
94 }
95 }
96 diffs
97 } else {
98 return Ok(None);
99 }
100 }
101 };
102
103 lock.insert(path.clone(), new_views);
105
106 Ok(Some(Patches(diffs)))
107 }
108}
109
110#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub struct MacroInvocation {
112 id: String,
113 template: LNode,
114}
115
116impl std::fmt::Debug for MacroInvocation {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("MacroInvocation").field("id", &self.id).finish()
119 }
120}
121
122#[derive(Default, Debug)]
123pub struct ViewMacroVisitor<'a> {
124 views: Vec<&'a Macro>,
125}
126
127impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
128 fn visit_macro(&mut self, node: &'ast Macro) {
129 let ident = node.path.get_ident().map(|n| n.to_string());
130 if ident == Some("view".to_string()) {
131 self.views.push(node);
132 }
133
134 visit::visit_macro(self, node);
136 }
137}
138
139pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
140 let file = path.as_ref().to_str().unwrap_or_default().replace(['/', '\\'], "-");
141 format!("{file}-{line}")
142}