1extern crate proc_macro;
2
3use anyhow::Result;
4use camino::Utf8PathBuf;
5use diff::Patches;
6use node::LNode;
7use or_poisoned::OrPoisoned;
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::HashMap,
11 fs::File,
12 io::Read,
13 path::{Path, PathBuf},
14 sync::{Arc, RwLock},
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 #[must_use]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub fn update_from_paths<T: AsRef<Path>>(&self, paths: &[T]) -> Result<()> {
45 let mut views = HashMap::new();
46
47 for path in paths {
48 for entry in WalkDir::new(path).into_iter().flatten() {
49 if entry.file_type().is_file() {
50 let path: PathBuf = entry.path().into();
51 let path = Utf8PathBuf::try_from(path)?;
52 if path.extension() == Some("rs") || path.ends_with(".rs") {
53 let macros = Self::parse_file(&path)?;
54 let entry = views.entry(path.clone()).or_default();
55 *entry = macros;
56 }
57 }
58 }
59 }
60
61 *self.views.write().or_poisoned() = views;
62
63 Ok(())
64 }
65
66 pub fn parse_file(path: &Utf8PathBuf) -> Result<Vec<MacroInvocation>> {
70 let mut file = File::open(path)?;
71 let mut content = String::new();
72 file.read_to_string(&mut content)?;
73 let ast = syn::parse_file(&content)?;
74
75 let mut visitor = ViewMacroVisitor::default();
76 visitor.visit_file(&ast);
77 let mut views = Vec::new();
78 for view in visitor.views {
79 let span = view.span();
80 let id = span_to_stable_id(path, span.start().line);
81 if view.tokens.is_empty() {
82 views.push(MacroInvocation {
83 id,
84 template: LNode::Fragment(Vec::new()),
85 });
86 } else {
87 let tokens = view.tokens.clone().into_iter();
88 let rsx = rstml::parse2(
90 tokens.collect::<proc_macro2::TokenStream>(),
91 )?;
92 let template = LNode::parse_view(rsx)?;
93 views.push(MacroInvocation { id, template });
94 }
95 }
96 Ok(views)
97 }
98
99 pub fn patch(&self, path: &Utf8PathBuf) -> Result<Option<Patches>> {
103 let new_views = Self::parse_file(path)?;
104 let mut lock = self.views.write().or_poisoned();
105 let diffs = match lock.get(path) {
106 None => return Ok(None),
107 Some(current_views) => {
108 if current_views.len() == new_views.len() {
109 let mut diffs = Vec::new();
110 for (current_view, new_view) in
111 current_views.iter().zip(&new_views)
112 {
113 if current_view.id == new_view.id
114 && current_view.template != new_view.template
115 {
116 diffs.push((
117 current_view.id.clone(),
118 current_view.template.diff(&new_view.template),
119 ));
120 }
121 }
122 diffs
123 } else {
124 lock.insert(path.clone(), new_views);
128 return Ok(None);
129 }
130 }
131 };
132
133 lock.insert(path.clone(), new_views);
135
136 Ok(Some(Patches(diffs)))
137 }
138}
139
140#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
141pub struct MacroInvocation {
142 id: String,
143 template: LNode,
144}
145
146impl core::fmt::Debug for MacroInvocation {
147 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148 f.debug_struct("MacroInvocation")
149 .field("id", &self.id)
150 .finish_non_exhaustive()
151 }
152}
153
154#[derive(Default, Debug)]
155pub struct ViewMacroVisitor<'a> {
156 views: Vec<&'a Macro>,
157}
158
159impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
160 fn visit_macro(&mut self, node: &'ast Macro) {
161 let ident = node.path.get_ident().map(ToString::to_string);
162 if ident == Some("view".to_string()) {
163 self.views.push(node);
164 }
165
166 visit::visit_macro(self, node);
168 }
169}
170
171pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
172 let file = path
173 .as_ref()
174 .to_str()
175 .unwrap_or_default()
176 .replace(['/', '\\'], "-");
177 format!("{file}-{line}")
178}