LW2H4ZRVK6RX7UXF7D6UYPU4RFLPFIVCVETTYIRDXXFZULFP7YZAC
w.write_all(b"<html lang=\"en\">")?;w.write_all(b"<head>")?;w.write_all(b"<meta charset=\"utf8\">")?;w.write_all(b"</meta>")?;w.write_all(b" ")?;w.write_all(b"<title>")?;({% block title %}).render_into(w)?;w.write_all(b"{{ common.title }}")?;({% endblock %}).render_into(w)?;w.write_all(b"</title>")?;w.write_all(b" ")?;w.write_all(b"<link rel=\"stylesheet\" href=\"/static/font-awesome-4.7.0/css/font-awesome.min.css\">")?;w.write_all(b"</link>")?;w.write_all(b" ")?;w.write_all(b"<link rel=\"stylesheet\" href=\"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/atom-one-light.min.css\">")?;w.write_all(b"</link>")?;w.write_all(b" ")?;w.write_all(b"<script src=\"/static/highlight.pack.js\">")?;w.write_all(b"</script>")?;w.write_all(b" ")?;w.write_all(b"<script>")?;w.write_all(b" hljs.configure(")?;({languages: [] }).render_into(w)?;w.write_all(b"); hljs.initHighlightingOnLoad(); ")?;w.write_all(b"</script>")?;w.write_all(b" ")?;w.write_all(b"<link rel=\"stylesheet\" href=\"/static/nest.css\">")?;w.write_all(b"</link>")?;w.write_all(b" ")?;w.write_all(b"</head>")?;w.write_all(b"<body>")?;({% block head %}).render_into(w)?;w.write_all(b"{% endblock %} ")?;w.write_all(b"<div class=\"bg\">")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"header\">")?;w.write_all(b" ")?;({% include "header.html" %}).render_into(w)?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"wrapper-fixed\">")?;w.write_all(b" ")?;w.write_all(b"<div class=\"home\">")?;w.write_all(b" ")?;w.write_all(b"<div class=\"home-intro\">")?;w.write_all(b" ")?;w.write_all(b"<h1>")?;w.write_all(b"The Pijul Nest")?;w.write_all(b"</h1>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b"Pijul brings ")?;w.write_all(b"<strong>")?;w.write_all(b"patches")?;w.write_all(b"</strong>")?;w.write_all(b" back to collaborative work, for faster, easier and really distributed workflows.")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b"With ")?;w.write_all(b"<strong>")?;w.write_all(b"nest.pijul.com")?;w.write_all(b"</strong>")?;w.write_all(b", you can share open source work, meet other developers.")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b"By the way, ")?;w.write_all(b"<span class=\"blinking\">")?;w.write_all(b"Pijul and this website are still beta quality software")?;w.write_all(b"</span>")?;w.write_all(b".")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;({% if !common.user.is_in() %}).render_into(w)?;w.write_all(b" ")?;w.write_all(b"<div class=\"home-signup\">")?;w.write_all(b" ")?;w.write_all(b"<h3>")?;w.write_all(b"Get a free account now")?;w.write_all(b"</h3>")?;w.write_all(b" ")?;w.write_all(b"<form class=\"reset\" method=\"POST\" action=\"/signup\">")?;w.write_all(b" ")?;w.write_all(b"<table>")?;w.write_all(b" ")?;w.write_all(b"<tbody>")?;w.write_all(b"<tr>")?;w.write_all(b" ")?;w.write_all(b"<td class=\"inputname\">")?;w.write_all(b"Login")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"<td>")?;w.write_all(b"<input type=\"text\" name=\"reset_login\" placeholder=\"Login\">")?;w.write_all(b"</input>")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"</tr>")?;w.write_all(b" ")?;w.write_all(b"<tr>")?;w.write_all(b" ")?;w.write_all(b"<td class=\"inputname\">")?;w.write_all(b"Password")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"<td>")?;w.write_all(b"<input type=\"password\" name=\"reset_password\" placeholder=\"Password\">")?;w.write_all(b"</input>")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"</tr>")?;w.write_all(b" ")?;w.write_all(b"<tr>")?;w.write_all(b" ")?;w.write_all(b"<td class=\"inputname\">")?;w.write_all(b"Confirm password")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"<td>")?;w.write_all(b"<input type=\"password\" name=\"reset_confirm_password\" placeholder=\"Confirm password\">")?;w.write_all(b"</input>")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"</tr>")?;w.write_all(b" ")?;w.write_all(b"<tr>")?;w.write_all(b" ")?;w.write_all(b"<td class=\"inputname\">")?;w.write_all(b"Email address")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"<td>")?;w.write_all(b"<input type=\"text\" name=\"reset_email\" placeholder=\"Email address\">")?;w.write_all(b"</input>")?;w.write_all(b"</td>")?;w.write_all(b" ")?;w.write_all(b"</tr>")?;w.write_all(b" ")?;w.write_all(b"</tbody>")?;w.write_all(b"</table>")?;w.write_all(b" ")?;w.write_all(b"<div id=\"signup\">")?;w.write_all(b" ")?;w.write_all(b"<button name=\"action\" value=\"signup\">")?;w.write_all(b" Sign up ")?;w.write_all(b"</button>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div id=\"oauth\">")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"oauth-button\">")?;w.write_all(b" Sign in with\u{2026} ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<ul>")?;w.write_all(b" ")?;w.write_all(b"<li>")?;w.write_all(b" ")?;w.write_all(b"<a class=\"button\" href=\"/oauth/twitter\">")?;w.write_all(b" ")?;w.write_all(b"<img style=\"vertical-align:middle\" src=\"/static/twitter_signin_normal.svg\">")?;w.write_all(b"</img>")?;w.write_all(b" Twitter ")?;w.write_all(b"</a>")?;w.write_all(b" ")?;w.write_all(b"</li>")?;w.write_all(b" ")?;w.write_all(b"<li>")?;w.write_all(b" ")?;w.write_all(b"<a class=\"button\" href=\"/oauth/google\">")?;w.write_all(b" ")?;w.write_all(b"<img style=\"vertical-align:middle\" src=\"/static/google_signin_normal.svg\">")?;w.write_all(b"</img>")?;w.write_all(b" Google ")?;w.write_all(b"</a>")?;w.write_all(b" ")?;w.write_all(b"</li>")?;w.write_all(b" ")?;w.write_all(b"<li>")?;w.write_all(b" ")?;w.write_all(b"<a class=\"button\" href=\"/oauth/github\">")?;w.write_all(b" ")?;w.write_all(b"<img style=\"vertical-align:middle\" src=\"/static/github_signin_normal.svg\">")?;w.write_all(b"</img>")?;w.write_all(b" Github ")?;w.write_all(b"</a>")?;w.write_all(b" ")?;w.write_all(b"</li>")?;w.write_all(b" ")?;w.write_all(b"</ul>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</form>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;({% endif %}).render_into(w)?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"home-text\">")?;w.write_all(b" ")?;w.write_all(b"<div class=\"wrapper\">")?;w.write_all(b" ")?;w.write_all(b"<div class=\"home-icons\">")?;w.write_all(b" ")?;w.write_all(b"<h1>")?;w.write_all(b"Move faster")?;w.write_all(b"</h1>")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b"Pijul is a version control system based on a sound mathematical theory of patches.")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b"This makes it the easiest and fastest tool to use, whether you\'re ")?;w.write_all(b"<strong>")?;w.write_all(b"starting a new project")?;w.write_all(b"</strong>")?;w.write_all(b" and need to move fast, taking on ")?;w.write_all(b"<strong>")?;w.write_all(b"new team members")?;w.write_all(b"</strong>")?;w.write_all(b" and need to teach them your tools, or even ")?;w.write_all(b"<strong>")?;w.write_all(b"managing large projects")?;w.write_all(b"</strong>")?;w.write_all(b" with many independent components. ")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"home-icons\">")?;w.write_all(b" ")?;w.write_all(b"<h1>")?;w.write_all(b"Collaborate more efficiently")?;w.write_all(b"</h1>")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b" Pijul works exclusively with ")?;w.write_all(b"<em>")?;w.write_all(b"patches")?;w.write_all(b"</em>")?;w.write_all(b", which are atomic units of team work. Its unique theory of patches allows for more efficient team work, where members can share only what is ready to be shared. Conflicts are also a true part of the theory, making them easier to solve than ever.")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"home-icons\">")?;w.write_all(b" ")?;w.write_all(b"<h1>")?;w.write_all(b"We all make mistakes")?;w.write_all(b"</h1>")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b" ")?;w.write_all(b"<p>")?;w.write_all(b"In Pijul, all actions are reversible. And since patches are the simplest component of team work, you know what you are doing when fixing mistakes.")?;w.write_all(b"</p>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"wrapper\">")?;w.write_all(b" ")?;({% block content %}).render_into(w)?;w.write_all(b"{% endblock %} ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div class=\"footer-container\">")?;w.write_all(b" ")?;w.write_all(b"<div class=\"footer\">")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b"\u{a9} {{ common.copyright_years }} the Pijul team")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b"<a href=\"/site/terms.html\">")?;w.write_all(b"Terms")?;w.write_all(b"</a>")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b"<a href=\"/site/privacy.html\">")?;w.write_all(b"Privacy")?;w.write_all(b"</a>")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"<div>")?;w.write_all(b"<a href=\"https://pijul.org/manual/the_nest.html\">")?;w.write_all(b"Help")?;w.write_all(b"</a>")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b" ")?;w.write_all(b"</div>")?;w.write_all(b"</body>")?;w.write_all(b"</html>")?;
#![recursion_limit = "128"]
extern crate proc_macro;
extern crate proc_macro2;
extern crate syn;
#[macro_use]
extern crate quote;
extern crate regex;
#[macro_use]
extern crate log;
extern crate xml;
use proc_macro2::*;
use xml::reader::{EventReader, XmlEvent, ParserConfig};
use std::iter::once;
use std::io::{BufRead, Read};
use regex::Regex;
#[proc_macro_attribute]
pub fn template(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
env_logger::try_init().unwrap_or(());
let attr = proc_macro2::TokenStream::from(attr);
let item = proc_macro2::TokenStream::from(item);
let mut file_ = String::new();
let mut attr = attr.into_iter();
let mut re = regex::Regex::new(r"(\{\{)|(\}\})|(\{([^\}\n]+)\})").expect("regex");
while let Some(a) = attr.next() {
if format!("{}", a) == "path" {
if let (Some(a), Some(b)) = (attr.next(), attr.next()) {
if format!("{}", a) == "=" {
file_ = format!("{}", b)
}
}
} else if format!("{}", a) == "regex" {
if let (Some(a), Some(b)) = (attr.next(), attr.next()) {
if format!("{}", a) == "=" {
re = regex::Regex::new(&format!("{}", b)).expect("regex");
}
}
} else {
println!("unknown attribute {:?}", a);
}
}
let cargo_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mut file = std::path::Path::new(&cargo_dir).join("templates");
file.push(file_.trim_matches('"'));
info!("parsing {:?}", file);
let template = {
let mut f = std::fs::File::open(&file).unwrap();
let mut template = String::new();
f.read_to_string(&mut template).unwrap();
template
};
// Replacing all & (except in "&blabla;") with &
let re_amp = regex::Regex::new(r"(&[a-zA-Z]+;)|&").unwrap();
let template = re_amp.replace_all(&template, |cap: ®ex::Captures| {
if let Some(c) = cap.get(1) {
c.as_str().to_string()
} else {
"&".to_string()
}
});
let mut name = None;
let mut item = item.into_iter();
let mut item_ = Vec::new();
let mut spec = proc_macro2::TokenStream::new();
let mut spec2 = proc_macro2::TokenStream::new();
let mut is_name = true;
let mut last_was_name = false;
loop {
match item.next() {
Some(TokenTree::Ident(id)) => {
if id.to_string() == "struct" || id.to_string() == "enum" {
let it = item.next().unwrap();
name = Some(syn::Ident::new(&format!("{}", it), it.span()));
item_.push(TokenTree::Ident(id));
item_.push(it);
last_was_name = true;
} else {
item_.push(TokenTree::Ident(id));
}
}
None => break,
Some(TokenTree::Punct(p)) => {
// println!("punct {:?} {:?}", p, last_was_name);
if last_was_name {
if p.to_string() == "<" {
let mut level = 1;
spec.extend(once(TokenTree::Punct(p.clone())));
spec2.extend(once(TokenTree::Punct(p.clone())));
item_.push(TokenTree::Punct(p));
loop {
match item.next() {
Some(TokenTree::Punct(p)) => {
let pp = p.to_string();
spec.extend(once(TokenTree::Punct(p.clone())));
item_.push(TokenTree::Punct(p.clone()));
if pp == ">" {
level -= 1;
if level <= 0 {
spec2.extend(once(TokenTree::Punct(p.clone())));
break
}
} else if pp == "<" {
level += 1;
} else if pp == ":" {
is_name = false;
} else if pp == "," && level == 1 {
spec2.extend(once(TokenTree::Punct(p.clone())));
is_name = true;
} else if is_name {
spec2.extend(once(TokenTree::Punct(p.clone())));
}
}
Some(it) => {
spec.extend(once(it.clone()));
if is_name {
spec2.extend(once(it.clone()));
}
item_.push(it)
}
None => break,
}
}
} else {
item_.push(TokenTree::Punct(p));
}
} else {
item_.push(TokenTree::Punct(p))
}
}
Some(it) => {
item_.push(it)
}
}
}
let name = name.unwrap();
use std::iter::FromIterator;
let item = proc_macro2::TokenStream::from_iter(item_);
let tokens = walk(&re, std::io::Cursor::new(template.as_bytes()));
let tok: TokenStream = tokens.parse().unwrap();
let file = file.to_str().unwrap();
let tokens = quote! {
impl #spec cuach::Render for #name #spec2 {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
let _ = include_bytes!(#file);
use std::fmt::Write;
use cuach::Render;
#tok
Ok(())
}
}
#item
};
debug!("{}", tokens);
proc_macro::TokenStream::from(tokens)
}
use std::fmt::Write;
fn walk<B: BufRead>(re: &Regex, file: B) -> String {
let mut tokens = String::new();
let t0 = std::io::Cursor::new("<template>");
let t1 = std::io::Cursor::new("</template>");
let config = ParserConfig::new()
.ignore_comments(false);
let parser = EventReader::new_with_config(t0.chain(file).chain(t1), config);
let mut level = 0;
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, attributes, .. }) => {
if level == 0 {
// the "<template>" tag.
level += 1;
continue
}
level += 1;
let mut s = format!("<{}", name.local_name);
for attr in attributes.iter() {
write!(&mut s, " {}=\"{}\"", attr.name.local_name, attr.value).unwrap();
}
if name.local_name == "br" {
write!(&mut s, "/>").unwrap();
} else {
write!(&mut s, ">").unwrap();
}
// Replace {bla} with a formatting string.
let arguments = arguments(re, &s);
for args in arguments {
match args {
Arg::Text(t) => writeln!(tokens, "w.write_str({:?})?;", t).unwrap(),
Arg::Arg(a) => writeln!(tokens, "(\n{}\n).render_into(w)?;", a).unwrap(),
}
}
}
Ok(XmlEvent::EndElement { name }) => {
level -= 1;
if level == 0 {
// the "</template>" tag.
continue
}
if name.local_name != "br" {
writeln!(tokens, "w.write_str(\"</{}>\")?;", name.local_name).unwrap()
}
}
Ok(XmlEvent::Comment(contents)) => {
let contents = contents.replace("&", "&");
tokens.push_str(&contents);
tokens.push_str("\n");
}
Ok(XmlEvent::Characters(contents)) => {
let arguments = arguments(re, &contents);
for args in arguments {
match args {
Arg::Text(t) => {
let re = regex::Regex::new(r"\s+").expect("regex");
let t = re.replace_all(t, " ");
writeln!(tokens, "w.write_str({:?})?;", t).unwrap()
},
Arg::Arg(a) => writeln!(tokens, "({}).render_into(w)?;", a).unwrap(),
}
}
}
Ok(XmlEvent::Whitespace(w)) => {
if !w.chars().any(|x| x == '\n') {
writeln!(tokens, r#"w.write_str(" ")?;"#).unwrap()
}
}
Ok(XmlEvent::StartDocument { .. }) => {}
Ok(XmlEvent::EndDocument) => {}
Ok(XmlEvent::ProcessingInstruction { .. }) => {}
Ok(XmlEvent::CData(_)) => {}
Err(e) => {
panic!("{:?}", e);
}
}
}
tokens
}
enum Arg<'a> {
Text(&'a str),
Arg(&'a str),
}
fn arguments<'a>(re: &Regex, s: &'a str) -> Vec<Arg<'a>> {
let mut arguments = Vec::new();
let mut start = 0;
for cap in re.captures_iter(s) {
debug!("cap = {:?}", cap);
if let Some(cap2) = cap.get(1) {
arguments.push(Arg::Text("{"));
start = cap2.end()
} else if let Some(cap2) = cap.get(2) {
arguments.push(Arg::Text("}"));
start = cap2.end()
} else if let Some(cap2) = cap.get(3) {
let (a, _) = s.split_at(cap2.end());
let (a, _) = a.split_at(cap2.start());
if cap2.start() > start {
arguments.push(Arg::Text(a.split_at(start).1))
}
arguments.push(Arg::Arg(cap.get(4).unwrap().as_str()));
start = cap2.end();
}
}
if start < s.len() {
arguments.push(Arg::Text(s.split_at(start).1))
}
arguments
}
[package]
name = "cuach-derive"
description = "A HTML template system for Rust"
repository = "https://nest.pijul.org/pmeunier/cuach"
documentation = "https://docs.rs/cuach-derive"
license = "MIT/Apache-2.0"
version = "0.2.4"
authors = ["Pierre-Étienne Meunier <pe@pijul.org>"]
edition = "2018"
include = [ "/Cargo.toml", "src/lib.rs" ]
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
regex = "1.4"
xml-rs = "0.8"
log = "0.4"
env_logger = "0.8"
extern crate cuach_derive;
extern crate uuid;
extern crate v_htmlescape;
use v_htmlescape::HTMLEscape;
pub use cuach_derive::template;
pub trait Render {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error>;
fn render(&self) -> Result<String, anyhow::Error> {
let mut v = String::new();
self.render_into(&mut v)?;
Ok(v)
}
}
impl Render for () {
fn render_into<W: std::fmt::Write>(&self, _: &mut W) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl<'a, R: Render> Render for &'a R {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok((*self).render_into(w)?)
}
}
impl Render for usize {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
impl Render for isize {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
impl Render for i32 {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
impl Render for i64 {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
impl Render for u32 {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
impl Render for u64 {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
impl<'a> Render for &'a str {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", HTMLEscape::new(self.as_bytes()))?)
}
}
impl Render for String {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", HTMLEscape::new(self.as_bytes()))?)
}
}
#[derive(Debug, Clone)]
pub struct PreEscaped<S: AsRef<str>>(pub S);
impl<S: AsRef<str>> Render for PreEscaped<S> {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self.0.as_ref())?)
}
}
impl Render for uuid::Uuid {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
Ok(write!(w, "{}", self)?)
}
}
use std::borrow::Borrow;
impl<'a> Render for std::borrow::Cow<'a, str> {
fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
let r: &str = self.borrow();
r.render_into(w)
}
}
[package]
name = "cuach"
description = "A HTML template system for Rust"
repository = "https://nest.pijul.org/pmeunier/cuach"
documentation = "https://docs.rs/cuach"
license = "MIT/Apache-2.0"
version = "0.4.0"
authors = ["Pierre-Étienne Meunier <pe@pijul.org>"]
edition = "2018"
include = [ "/README.md", "/Cargo.toml", "/src/lib.rs" ]
[dependencies]
cuach-derive = "0.2.4"
uuid = "0.8"
v_htmlescape = "0.12"
anyhow = "1.0"
# Cuach, a HTML template system for Rust
Cuach defines a single trait `Render`, usable to produce HTML pages from templates and Rust `struct`s. Cuach compiles on stable Rust and uses procedural macros to produce efficient static code.
Cuach requires a folder called `templates` at the root of a repository (alongside `Cargo.toml`). Then, if that folder contains a file called `index.html`, we can use it like so:
```
#[macro_use]
extern crate cuach;
use cuach::*;
#[template(path="index.html")]
struct A<'a> {
field: &'a str,
other_field: usize
}
fn main() {
println!("{}, (A { field: "blah", other_field: 0 }).render().unwrap())
}
```
The template can be as simple as an empty file, or contain more elaborate things, such as:
```
<html>
<body>
field is equal to <strong>{ self.field }</strong>, and other_field to { self.other_field }.
</body>
</html>
```
## Rules
All the variables and functions of the module where `A` is defined are in scope in the template. There is also an extra variable in scope, `w`, of type `&mut std::fmt::Write`.
For any Rust expression `e`, `{ e }` in the template gets translated to `(e).render_into(w)`.
Moreover, more complex Rust expressions can be used using HTML comments. For instance, the following produces ten HTML paragraphs, with 0, 1, …, 9 as their contents. Anything inside HTML comments is parsed as Rust.
```
<!-- for i in 0..10 { -->
<p>{ i }</p>
<!-- } -->
```
This means in particular that any expression of the form `{ e }` in the template is strictly equivalent to
```
(e).render_into(w)?;
```
## Whitespace and escaping
Any amount of whitespace between tags or comments is ignored if it contains at least one newline character, and is output as is else. One way to force whitespace on an otherwise blank line is to add whitespace between two HTMLcomments, like so:
```
<!-- --> <!-- -->
```
Comments can be used in the templates, but they have to be Rust comments inside HTML comments:
```
<!-- // this is a comment -->
<!-- /* this is
another,
multiline,
comment */ -->
```
The parsing of templates is done using an XML parsing library, [xml-rs](https://crates.io/crates/xml-rs). Therefore, standard XML escaping works and can be used, but it sometimes conflicts with Rust borrows. Cuach handles that situation by replacing, before parsing, all `&` that are not recognised by regular expression `&[a-z]+;` with `&`. After parsing, all instances of `&` are converted back to `&`.
## Including other files
There is nothing special about including other files in Cuach, as Rust provides that ability already, by doing something like `{ include_str!("../templates/included.html") }`. Unfortunately, the parsing libraries used by Cuach (such as `proc-macro2`) do not yet allow Cuach to use filepaths relative to the current file.
## Conflict with JS
The curly braces might sometimes conflict with JS embedded in HTML. The way to deal with this is that Cuach only considers curly braces on the same line: in order to be understood as a Rust expression, the code between `{` and `}` must not contain the character `\n`.
Here is a valid example of JS templated by Cuach, embedded in HTML:
```js
<script>my_function("{self.argument}")</script>
```
And the following is a valid example of passing an object to a function in JS, not transformed by Cuach:
```js
<script>
my_function({
"arg": 0
})
</script>
```
[workspace]
members = [ "cuach", "cuach-derive" ]
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee67c11feeac938fae061b232e38e0b6d94f97a9df10e6271319325ac4c56a86"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "buf-min"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa17aa1cf56bdd6bb30518767d00e58019d326f3f05d8c3e0730b549d332ea83"
dependencies = [
"bytes",
]
[[package]]
name = "bytes"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cuach"
version = "0.4.0"
dependencies = [
"anyhow",
"cuach-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid",
"v_htmlescape",
]
[[package]]
name = "cuach-derive"
version = "0.2.4"
dependencies = [
"env_logger",
"log",
"proc-macro2",
"quote",
"regex",
"syn",
"xml-rs",
]
[[package]]
name = "cuach-derive"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e5cfdb8728a242ffa16bb54df7b65259341c91d23139ac0e2962e722bc0bf3"
dependencies = [
"env_logger",
"log",
"proc-macro2",
"quote",
"regex",
"syn",
"xml-rs",
]
[[package]]
name = "env_logger"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "hermit-abi"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
dependencies = [
"libc",
]
[[package]]
name = "humantime"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
[[package]]
name = "log"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "memchr"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "nom"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
[[package]]
name = "syn"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
dependencies = [
"lazy_static",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "uuid"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
[[package]]
name = "v_escape"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e0ab5fab1db278a9413d2ea794cb66f471f898c5b020c3c394f6447625d9d4"
dependencies = [
"buf-min",
"v_escape_derive",
]
[[package]]
name = "v_escape_derive"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c860ad1273f4eee7006cee05db20c9e60e5d24cba024a32e1094aa8e574f3668"
dependencies = [
"nom",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "v_htmlescape"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f9a8af610ad6f7fc9989c9d2590d9764bc61f294884e9ee93baa58795174572"
dependencies = [
"cfg-if 1.0.0",
"v_escape",
]
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xml-rs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"