extern crate proc_macro;
use proc_macro::{Delimiter, TokenStream, TokenTree};
#[proc_macro_attribute]
pub fn live_view(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro]
pub fn html(input: TokenStream) -> TokenStream {
match parse_template(input) {
Ok(template) => template.into_token_stream(),
Err(message) => compile_error(&message),
}
}
#[derive(Debug)]
struct ParsedTemplate {
static_segments: Vec<String>,
dynamic_segments: Vec<String>,
}
impl ParsedTemplate {
fn into_token_stream(self) -> TokenStream {
let static_segments = self
.static_segments
.iter()
.map(|segment| format!("{segment:?}"))
.collect::<Vec<_>>()
.join(", ");
let dynamic_segments = self
.dynamic_segments
.iter()
.map(|expr| format!("::shelly::escape_html(&::std::format!(\"{{}}\", ({expr})))"))
.collect::<Vec<_>>()
.join(", ");
format!(
"::shelly::Template::new(::std::vec![{static_segments}], ::std::vec![{dynamic_segments}])"
)
.parse()
.expect("generated html! template should parse")
}
}
fn parse_template(input: TokenStream) -> Result<ParsedTemplate, String> {
let mut static_segments = vec![String::new()];
let mut dynamic_segments = Vec::new();
for token in input {
match token {
TokenTree::Literal(literal) => {
let value = parse_string_literal(&literal.to_string())?;
static_segments
.last_mut()
.expect("template should always have a current static segment")
.push_str(&value);
}
TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
let expr = group.stream().to_string();
if expr.trim().is_empty() {
return Err("html! dynamic expression cannot be empty".to_string());
}
dynamic_segments.push(expr);
static_segments.push(String::new());
}
other => {
return Err(format!(
"html! expected string literal or parenthesized expression, found `{other}`"
));
}
}
}
if static_segments.len() != dynamic_segments.len() + 1 {
return Err("html! produced invalid static/dynamic segment counts".to_string());
}
Ok(ParsedTemplate {
static_segments,
dynamic_segments,
})
}
fn parse_string_literal(source: &str) -> Result<String, String> {
if let Some(value) = parse_raw_string_literal(source) {
return Ok(value);
}
let Some(inner) = source
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
else {
return Err(format!("html! expected a string literal, found `{source}`"));
};
unescape_string_literal(inner)
}
fn parse_raw_string_literal(source: &str) -> Option<String> {
let rest = source.strip_prefix('r')?;
let hashes = rest.chars().take_while(|ch| *ch == '#').count();
let rest = &rest[hashes..];
let inner = rest.strip_prefix('"')?;
let suffix = format!("\"{}", "#".repeat(hashes));
Some(inner.strip_suffix(&suffix)?.to_string())
}
fn unescape_string_literal(inner: &str) -> Result<String, String> {
let mut out = String::new();
let mut chars = inner.chars();
while let Some(ch) = chars.next() {
if ch != '\\' {
out.push(ch);
continue;
}
let Some(escaped) = chars.next() else {
return Err("html! string literal ends with an incomplete escape".to_string());
};
match escaped {
'\\' => out.push('\\'),
'"' => out.push('"'),
'n' => out.push('\n'),
'r' => out.push('\r'),
't' => out.push('\t'),
'0' => out.push('\0'),
other => {
return Err(format!(
"html! string literal escape `\\{other}` is not supported yet"
));
}
}
}
Ok(out)
}
fn compile_error(message: &str) -> TokenStream {
format!("::std::compile_error!({message:?});")
.parse()
.expect("compile_error! should parse")
}