├── rustfmt.toml ├── playground ├── src │ ├── templates │ │ ├── mod.rs │ │ ├── base.rs │ │ └── home.rs │ └── main.rs └── Cargo.toml ├── .gitignore ├── Cargo.toml ├── README.tpl ├── Makefile ├── README.md └── src ├── html.rs └── lib.rs /rustfmt.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/templates/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod home; 2 | pub mod base; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | rusty-tags.vi 5 | /playground/target/ 6 | -------------------------------------------------------------------------------- /playground/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stpl-playground" 3 | version = "0.1.0" 4 | authors = ["Dawid Ciężarkiewicz "] 5 | 6 | [dependencies] 7 | stpl = { version = "*", path = "../" } 8 | serde = "1" 9 | serde_derive = "1" 10 | -------------------------------------------------------------------------------- /playground/src/templates/base.rs: -------------------------------------------------------------------------------- 1 | use stpl::Render; 2 | 3 | #[derive(Serialize, Deserialize, Clone)] 4 | pub struct Data { 5 | pub title: String, 6 | } 7 | 8 | pub fn base(data: &Data, content : C) -> impl Render { 9 | use stpl::html::*; 10 | #[cfg_attr(rustfmt, rustfmt_skip)] 11 | html(( 12 | head( 13 | title(data.title.clone()) 14 | ), 15 | body( 16 | content 17 | ) 18 | )) 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stpl" 3 | version = "0.5.0" 4 | authors = ["Dawid Ciężarkiewicz "] 5 | description = "Super templates (html, etc.) with Plain-Rust, no textfiles" 6 | keywords = ["template", "html", "markup", "dynamic"] 7 | categories = ["template-engine"] 8 | license = "MPL-2.0/MIT/Apache-2.0" 9 | documentation = "https://docs.rs/stpl" 10 | homepage = "https://github.com/dpc/stpl" 11 | repository = "https://github.com/dpc/stpl" 12 | readme = "README.md" 13 | 14 | [workspace] 15 | members = [ "playground" ] 16 | 17 | [dependencies] 18 | -------------------------------------------------------------------------------- /playground/src/templates/home.rs: -------------------------------------------------------------------------------- 1 | use stpl::Render; 2 | 3 | use super::base; 4 | 5 | #[derive(Serialize, Deserialize, Clone)] 6 | pub struct Data { 7 | pub page: base::Data, 8 | pub name: String, 9 | } 10 | 11 | pub fn page(data: &Data) -> impl Render { 12 | use stpl::html::*; 13 | #[cfg_attr(rustfmt, rustfmt_skip)] 14 | let content = ( 15 | h1.class("main")("Welcome page"), 16 | p( 17 | format!("Hi, {}!", data.name) 18 | ), 19 | ul( 20 | (0..2).map(|n| li(n)).collect::>() 21 | ) 22 | ); 23 | 24 | base::base(&data.page, content) 25 | } 26 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | Travis CI Build Status 6 | 7 | 8 | crates.io 9 | 10 | 11 | Gitter Chat 12 | 13 |
14 |

15 | 16 | # {{crate}} 17 | 18 | {{readme}} 19 | 20 | # License 21 | 22 | {{crate}} is licensed under: {{license}} 23 | -------------------------------------------------------------------------------- /playground/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(universal_impl_trait)] 2 | #![feature(conservative_impl_trait)] 3 | 4 | extern crate stpl; 5 | extern crate serde; 6 | #[macro_use] 7 | extern crate serde_derive; 8 | 9 | use std::io::Write; 10 | use stpl::html; 11 | 12 | pub mod templates; 13 | use templates::*; 14 | 15 | 16 | pub fn print_template(tpl: impl stpl::Render) { 17 | let mut v = vec![]; 18 | tpl 19 | .render(&mut stpl::html::Renderer::new(&mut v)) 20 | .unwrap(); 21 | std::io::stdout().write_all(&v).unwrap(); 22 | } 23 | 24 | pub fn home_tpl() -> impl stpl::Template { 25 | html::Template::new("home", ::templates::home::page) 26 | } 27 | 28 | fn main() { 29 | stpl::handle_dynamic() 30 | .template(&home_tpl()); 31 | 32 | let data = templates::home::Data { 33 | page: base::Data { 34 | title: "Hello!".into(), 35 | }, 36 | name: "William".into() 37 | }; 38 | 39 | println!("Change `src/templates/home.rs` and rerun `cargo build` to pick a new template version"); 40 | println!(); 41 | loop { 42 | println!("Static:"); 43 | print_template(templates::home::page(&data)); 44 | println!(""); 45 | println!("dynamic:"); 46 | std::io::stdout().write_all(&stpl::render_dynamic_self(&home_tpl(), &data).unwrap()).unwrap(); 47 | println!(""); 48 | std::thread::sleep(std::time::Duration::from_secs(5)); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG_NAME=$(shell grep name Cargo.toml | head -n 1 | awk -F \" '{print $$2}') 2 | DOCS_DEFAULT_MODULE=$(PKG_NAME) 3 | ifeq (, $(shell which cargo-check 2> /dev/null)) 4 | DEFAULT_TARGET=build 5 | else 6 | DEFAULT_TARGET=build 7 | endif 8 | 9 | default: $(DEFAULT_TARGET) 10 | 11 | ALL_TARGETS += build $(EXAMPLES) test doc 12 | ifneq ($(RELEASE),) 13 | $(info RELEASE BUILD: $(PKG_NAME)) 14 | CARGO_FLAGS += --release 15 | ALL_TARGETS += bench 16 | else 17 | $(info DEBUG BUILD: $(PKG_NAME); use `RELEASE=true make [args]` for release build) 18 | endif 19 | 20 | EXAMPLES = $(shell cd examples 2>/dev/null && ls *.rs 2>/dev/null | sed -e 's/.rs$$//g' ) 21 | 22 | all: $(ALL_TARGETS) 23 | 24 | .PHONY: run test build doc clean clippy 25 | run test build clean: 26 | cargo $@ $(CARGO_FLAGS) 27 | 28 | check: 29 | $(info Running check; use `make build` to actually build) 30 | cargo $@ $(CARGO_FLAGS) 31 | 32 | clippy: 33 | cargo build --features clippy 34 | 35 | .PHONY: bench 36 | bench: 37 | cargo $@ $(filter-out --release,$(CARGO_FLAGS)) 38 | 39 | .PHONY: travistest 40 | travistest: 41 | for i in `seq 1`; do make test || exit 1 ; done 42 | 43 | .PHONY: readme 44 | readme: README.md 45 | 46 | README.md: README.tpl FORCE 47 | cargo readme > README.md 48 | 49 | .PHONY: longtest 50 | longtest: 51 | @echo "Running longtest. Press Ctrl+C to stop at any time" 52 | @sleep 2 53 | @i=0; while i=$$((i + 1)) && echo "Iteration $$i" && make test ; do :; done 54 | 55 | .PHONY: $(EXAMPLES) 56 | $(EXAMPLES): 57 | cargo build --example $@ $(CARGO_FLAGS) 58 | 59 | .PHONY: doc 60 | doc: FORCE 61 | rm -rf target/doc 62 | cargo doc 63 | 64 | .PHONY: publishdoc 65 | publishdoc: doc 66 | echo '' > target/doc/index.html 67 | ghp-import -n target/doc 68 | git push -f origin gh-pages 69 | 70 | .PHONY: docview 71 | docview: doc 72 | xdg-open target/doc/$(PKG_NAME)/index.html 73 | 74 | .PHONY: FORCE 75 | FORCE: 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | Travis CI Build Status 6 | 7 | 8 | crates.io 9 | 10 | 11 | Gitter Chat 12 | 13 |
14 |

15 | 16 | # stpl 17 | 18 | ### stpl - Super template library for Rust 19 | 20 | `stpl` is a plain-Rust-only template library with some neat properties and 21 | features. 22 | 23 | ### Main idea 24 | 25 | In `stpl` there are no magic macros or DSLs, and no clunky 26 | text-files with weird syntax. Everything is just normal, easy 27 | to understand Rust code. 28 | 29 | Let's take a look at a real-life example from the pilot project: an HTML 30 | based-skeleton template for a Bootstrap-based UI. 31 | 32 | ```rust 33 | pub fn base(data: &Data, content: C) -> impl Render { 34 | ( 35 | doctype("html"), 36 | html(( 37 | head(( 38 | meta.charset("utf-8"), 39 | meta.name("viewport").content("width=device-width, initial-scale=1, shrink-to-fit=no"), 40 | meta.name("description").content(""), 41 | meta.name("author").content("Dawid Ciężarkiewicz"), 42 | title(data.title.clone()), 43 | ( 44 | link.rel("icon").href("/static/favicon.ico"), 45 | link.rel("stylesheet").href("/static/theme/flatly/bootstrap.min.css"), 46 | link.rel("stylesheet").href("/static/theme/starter-template.css"), 47 | ) 48 | )), 49 | body(( 50 | navbar(data), 51 | main 52 | .id("main") 53 | .role("main") 54 | .class("container mb-5")( 55 | content, 56 | ), 57 | ( 58 | script.src("https://code.jquery.com/jquery-3.2.1.min.js").crossorigin("anonymous"), 59 | script.src("https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js") 60 | .integrity("sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh") 61 | .crossorigin("anonymous"), 62 | script.src("https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js") 63 | .integrity("sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ") 64 | .crossorigin("anonymous"), 65 | script.type_("text/javascript")( 66 | raw(include_str!("white-icon.js")) 67 | ), 68 | ) 69 | )) 70 | )) 71 | ) 72 | } 73 | ``` 74 | 75 | It is just a function. There is no magic, no macros, no text files involved. 76 | The whole template was formatted with `rustfmt` just like a normal Rust code. 77 | 78 | The function accepts arguments: 79 | 80 | * `data: Data` containing information how to "fill the blanks", and 81 | * `content: Render` - sub-template value that will be used as main page content. 82 | 83 | The function returns `Render` value that can be rendered as a string or bytes, or 84 | composed with other templates. The value is basically a one big tuple 85 | nesting many other `Render` values. `Render` is implemented for many standard types, 86 | can be implemented for new types or can be generated using functions/closures. 87 | 88 | Users are free to use any Rust language primitives to generate their 89 | templates and structure relationship between them in any way that 90 | suits them. 91 | 92 | ### Pros 93 | 94 | * robust: template generation can reuse any existing code and data 95 | structures 96 | * convenient: Rust tooling can work with plain-Rust-templates just 97 | like any other code; `rustfmt` takes care of formatting, typos result 98 | in normal error messages etc. 99 | * fast: the compiler optimizes the template code to essential logic 100 | necessary to write-out the rendered template data to the IO; there 101 | is no parsing involved 102 | 103 | ### Cons 104 | 105 | * `nightly`-only: This library relies on some unstable features: 106 | * `#![feature(unboxed_closures)]` 107 | # `![feature(fn_traits)]` 108 | * immature and incomplete: This library is still work in progress, and will 109 | mature with time. 110 | 111 | ## Where to start 112 | 113 | You are most probably interested in reading `html` module documentation 114 | 115 | # License 116 | 117 | stpl is licensed under: MPL-2.0/MIT/Apache-2.0 118 | -------------------------------------------------------------------------------- /src/html.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt; 3 | use std::io; 4 | 5 | use super::Fn; 6 | use Render; 7 | 8 | pub trait RenderExt: Render { 9 | fn render_to_vec(&self) -> Vec { 10 | let mut v: Vec = vec![]; 11 | self.render(&mut v).unwrap(); 12 | v 13 | } 14 | 15 | fn render_to_string(&self) -> String { 16 | String::from_utf8_lossy(&self.render_to_vec()).into() 17 | } 18 | } 19 | 20 | impl RenderExt for T {} 21 | 22 | impl super::Renderer for T { 23 | fn write_raw(&mut self, data: &[u8]) -> io::Result<()> { 24 | self.write_all(data) 25 | } 26 | 27 | fn write_raw_fmt(&mut self, fmt: &fmt::Arguments) -> io::Result<()> { 28 | self.write_fmt(*fmt) 29 | } 30 | 31 | fn write(&mut self, data: &[u8]) -> io::Result<()> { 32 | for c in data { 33 | match *c as char { 34 | '&' => self.write_all(b"&")?, 35 | '<' => self.write_all(b"<")?, 36 | '>' => self.write_all(b">")?, 37 | '"' => self.write_all(b""")?, 38 | '\'' => self.write_all(b"'")?, 39 | '/' => self.write_all(b"/")?, 40 | // Additional one for old IE (unpatched IE8 and below) 41 | // See https://github.com/OWASP/owasp-java-encoder/wiki/Grave-Accent-Issue 42 | '`' => self.write_all(b"`")?, 43 | _ => self.write_all(&[*c])?, 44 | } 45 | } 46 | 47 | Ok(()) 48 | } 49 | } 50 | 51 | type CowStr = Cow<'static, str>; 52 | 53 | pub struct BareTag { 54 | tag: &'static str, 55 | } 56 | 57 | pub struct Tag { 58 | tag: CowStr, 59 | attrs: Vec<(CowStr, Option)>, 60 | } 61 | 62 | pub struct FinalTag { 63 | tag: CowStr, 64 | attrs: Vec<(CowStr, Option)>, 65 | inn: I, 66 | } 67 | 68 | impl Render for Tag { 69 | fn render(&self, r: &mut super::Renderer) -> io::Result<()> { 70 | r.write_raw_str("<")?; 71 | r.write_raw_str(&*self.tag)?; 72 | for &(ref k, ref v) in self.attrs.iter() { 73 | r.write_raw_str(" ")?; 74 | r.write_raw_str(&*k)?; 75 | if let Some(ref v) = *v { 76 | r.write_raw_str("=\"")?; 77 | r.write_raw_str(&*v)?; 78 | r.write_raw_str("\"")?; 79 | } 80 | } 81 | 82 | r.write_raw_str(">")?; 83 | r.write_raw_str("") 86 | } 87 | } 88 | 89 | impl Render for BareTag { 90 | fn render(&self, r: &mut super::Renderer) -> io::Result<()> { 91 | r.write_raw_str("<")?; 92 | r.write_raw_str(&*self.tag)?; 93 | r.write_raw_str(">")?; 94 | r.write_raw_str("") 97 | } 98 | } 99 | 100 | impl Render for FinalTag { 101 | fn render(&self, r: &mut super::Renderer) -> io::Result<()> { 102 | r.write_raw_str("<")?; 103 | r.write_raw_str(&*self.tag)?; 104 | for &(ref k, ref v) in self.attrs.iter() { 105 | r.write_raw_str(" ")?; 106 | r.write_raw_str(&*k)?; 107 | if let Some(ref v) = *v { 108 | r.write_raw_str("=\"")?; 109 | r.write_raw_str(&*v)?; 110 | r.write_raw_str("\"")?; 111 | } 112 | } 113 | 114 | r.write_raw_str(">")?; 115 | self.inn.render(r)?; 116 | r.write_raw_str("") 119 | } 120 | } 121 | 122 | macro_rules! impl_attr { 123 | ($t:ident) => { 124 | pub fn $t>(self, val: V) -> Tag { 125 | self.attr(stringify!($t), val) 126 | } 127 | } 128 | } 129 | 130 | macro_rules! impl_attr1 { 131 | ($t:ident) => { 132 | pub fn $t(self) -> Tag { 133 | self.attr1(stringify!($t)) 134 | } 135 | } 136 | } 137 | 138 | macro_rules! impl_attr2 { 139 | ($t1:ident, $t2:expr) => { 140 | pub fn $t1>(self, val: V) -> Tag { 141 | self.attr($t2, val) 142 | } 143 | } 144 | } 145 | macro_rules! impl_attr_all { 146 | () => { 147 | impl_attr!(class); 148 | impl_attr!(id); 149 | impl_attr!(charset); 150 | impl_attr!(content); 151 | impl_attr!(name); 152 | impl_attr!(href); 153 | impl_attr!(rel); 154 | impl_attr!(src); 155 | impl_attr!(integrity); 156 | impl_attr!(crossorigin); 157 | impl_attr!(role); 158 | impl_attr!(method); 159 | impl_attr!(action); 160 | impl_attr!(placeholder); 161 | impl_attr!(value); 162 | impl_attr!(rows); 163 | impl_attr!(alt); 164 | impl_attr!(style); 165 | impl_attr!(onclick); 166 | impl_attr!(placement); 167 | impl_attr!(toggle); 168 | impl_attr!(scope); 169 | impl_attr!(title); 170 | impl_attr1!(checked); 171 | impl_attr1!(enabled); 172 | impl_attr1!(disabled); 173 | impl_attr2!(type_, "type"); 174 | impl_attr2!(data_toggle, "data-toggle"); 175 | impl_attr2!(data_target, "data-target"); 176 | impl_attr2!(data_placement, "data-placement"); 177 | impl_attr2!(aria_controls, "aria-controls"); 178 | impl_attr2!(aria_expanded, "aria-expanded"); 179 | impl_attr2!(aria_label, "aria-label"); 180 | impl_attr2!(aria_haspopup, "aria-haspopup"); 181 | impl_attr2!(aria_labelledby, "aria-labelledby"); 182 | impl_attr2!(aria_current, "aria-current"); 183 | impl_attr2!(for_, "for"); 184 | }; 185 | } 186 | 187 | impl Tag { 188 | pub fn attr, V: Into>(self, key: K, val: V) -> Tag { 189 | let Tag { tag, mut attrs } = self; 190 | attrs.push((key.into(), Some(val.into()))); 191 | Tag { 192 | tag: tag, 193 | attrs: attrs, 194 | } 195 | } 196 | pub fn attr1>(self, key: K) -> Tag { 197 | let Tag { tag, mut attrs } = self; 198 | attrs.push((key.into(), None)); 199 | Tag { 200 | tag: tag, 201 | attrs: attrs, 202 | } 203 | } 204 | impl_attr_all!(); 205 | } 206 | 207 | impl BareTag { 208 | pub fn attr, V: Into>(self, key: K, val: V) -> Tag { 209 | Tag { 210 | tag: self.tag.into(), 211 | attrs: vec![(key.into(), Some(val.into()))], 212 | } 213 | } 214 | pub fn attr1>(self, key: K) -> Tag { 215 | Tag { 216 | tag: self.tag.into(), 217 | attrs: vec![(key.into(), None)], 218 | } 219 | } 220 | impl_attr_all!(); 221 | } 222 | 223 | impl FnOnce<(A,)> for Tag { 224 | type Output = FinalTag; 225 | extern "rust-call" fn call_once(self, args: (A,)) -> Self::Output { 226 | FinalTag { 227 | tag: self.tag, 228 | attrs: self.attrs, 229 | inn: args.0, 230 | } 231 | } 232 | } 233 | 234 | impl FnOnce<(A,)> for BareTag { 235 | type Output = FinalTag; 236 | extern "rust-call" fn call_once(self, args: (A,)) -> Self::Output { 237 | FinalTag { 238 | tag: self.tag.into(), 239 | attrs: vec![], 240 | inn: args.0, 241 | } 242 | } 243 | } 244 | 245 | /// Implement a Render function wrapping in a simple tag 246 | macro_rules! impl_tag { 247 | ($t:ident) => { 248 | #[allow(non_upper_case_globals)] 249 | pub const $t: BareTag = BareTag { 250 | tag: stringify!($t), 251 | }; 252 | }; 253 | } 254 | 255 | pub fn doctype(t: &'static str) -> impl Render { 256 | Fn(move |r: &mut super::Renderer| { 257 | r.write_raw(b"") 260 | }) 261 | } 262 | macro_rules! impl_esc { 263 | ($i:ident, $t:ident, $s:expr) => { 264 | #[derive(Copy, Clone)] 265 | /// Implement `$i` 266 | pub struct $t; 267 | 268 | #[allow(non_upper_case_globals)] 269 | pub const $i: $t = $t; 270 | 271 | impl Render for $t { 272 | fn render(&self, r: &mut super::Renderer) -> io::Result<()> { 273 | r.write_raw_str($s) 274 | } 275 | } 276 | }; 277 | } 278 | 279 | impl_esc!(nbsp, Nbsp, " "); 280 | impl_esc!(lt, Lt, "<"); 281 | impl_esc!(gt, Gt, ">"); 282 | 283 | pub fn raw(x: T) -> impl Render { 284 | Fn(move |r: &mut super::Renderer| x.render(&mut super::RawRenderer(r))) 285 | } 286 | 287 | impl_tag!(html); 288 | impl_tag!(head); 289 | impl_tag!(meta); 290 | impl_tag!(title); 291 | impl_tag!(body); 292 | impl_tag!(div); 293 | impl_tag!(section); 294 | impl_tag!(h1); 295 | impl_tag!(h2); 296 | impl_tag!(h3); 297 | impl_tag!(h4); 298 | impl_tag!(h5); 299 | impl_tag!(li); 300 | impl_tag!(ul); 301 | impl_tag!(ol); 302 | impl_tag!(p); 303 | impl_tag!(span); 304 | impl_tag!(b); 305 | impl_tag!(i); 306 | impl_tag!(u); 307 | impl_tag!(tt); 308 | impl_tag!(string); 309 | impl_tag!(pre); 310 | impl_tag!(link); 311 | impl_tag!(script); 312 | impl_tag!(main); 313 | impl_tag!(nav); 314 | impl_tag!(a); 315 | impl_tag!(form); 316 | impl_tag!(button); 317 | impl_tag!(input); 318 | impl_tag!(img); 319 | impl_tag!(blockquote); 320 | impl_tag!(footer); 321 | impl_tag!(wrapper); 322 | impl_tag!(label); 323 | impl_tag!(table); 324 | impl_tag!(thead); 325 | impl_tag!(th); 326 | impl_tag!(tr); 327 | impl_tag!(td); 328 | impl_tag!(tbody); 329 | impl_tag!(textarea); 330 | 331 | // vim: foldmethod=marker foldmarker={{{,}}} 332 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ## stpl - Super template library for Rust 2 | //! 3 | //! `stpl` is a plain-Rust-only template library with some neat properties and 4 | //! features. 5 | //! 6 | //! ## Main idea 7 | //! 8 | //! In `stpl` there are no magic macros or DSLs, and no clunky 9 | //! text-files with weird syntax. Everything is just normal, easy 10 | //! to understand Rust code. 11 | //! 12 | //! Let's take a look at a real-life example from the pilot project: an HTML 13 | //! based-skeleton template for a Bootstrap-based UI. 14 | //! 15 | //! ```ignore 16 | //! pub fn base(data: &Data, content: C) -> impl Render { 17 | //! ( 18 | //! doctype("html"), 19 | //! html(( 20 | //! head(( 21 | //! meta.charset("utf-8"), 22 | //! meta.name("viewport").content("width=device-width, initial-scale=1, shrink-to-fit=no"), 23 | //! meta.name("description").content(""), 24 | //! meta.name("author").content("Dawid Ciężarkiewicz"), 25 | //! title(data.title.clone()), 26 | //! ( 27 | //! link.rel("icon").href("/static/favicon.ico"), 28 | //! link.rel("stylesheet").href("/static/theme/flatly/bootstrap.min.css"), 29 | //! link.rel("stylesheet").href("/static/theme/starter-template.css"), 30 | //! ) 31 | //! )), 32 | //! body(( 33 | //! navbar(data), 34 | //! main 35 | //! .id("main") 36 | //! .role("main") 37 | //! .class("container mb-5")( 38 | //! content, 39 | //! ), 40 | //! ( 41 | //! script.src("https://code.jquery.com/jquery-3.2.1.min.js").crossorigin("anonymous"), 42 | //! script.src("https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js") 43 | //! .integrity("sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh") 44 | //! .crossorigin("anonymous"), 45 | //! script.src("https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js") 46 | //! .integrity("sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ") 47 | //! .crossorigin("anonymous"), 48 | //! script.type_("text/javascript")( 49 | //! raw(include_str!("white-icon.js")) 50 | //! ), 51 | //! ) 52 | //! )) 53 | //! )) 54 | //! ) 55 | //! } 56 | //! ``` 57 | //! 58 | //! It is just a function. There is no magic, no macros, no text files involved. 59 | //! The whole template was formatted with `rustfmt` just like a normal Rust code. 60 | //! 61 | //! The function accepts arguments: 62 | //! 63 | //! * `data: Data` containing information how to "fill the blanks", and 64 | //! * `content: Render` - sub-template value that will be used as main page content. 65 | //! 66 | //! The function returns `Render` value that can be rendered as a string or bytes, or 67 | //! composed with other templates. The value is basically a one big tuple 68 | //! nesting many other `Render` values. `Render` is implemented for many standard types, 69 | //! can be implemented for new types or can be generated using functions/closures. 70 | //! 71 | //! Users are free to use any Rust language primitives to generate their 72 | //! templates and structure relationship between them in any way that 73 | //! suits them. 74 | //! 75 | //! ## Pros 76 | //! 77 | //! * robust: template generation can reuse any existing code and data 78 | //! structures 79 | //! * convenient: Rust tooling can work with plain-Rust-templates just 80 | //! like any other code; `rustfmt` takes care of formatting, typos result 81 | //! in normal error messages etc. 82 | //! * fast: the compiler optimizes the template code to essential logic 83 | //! necessary to write-out the rendered template data to the IO; there 84 | //! is no parsing involved 85 | //! 86 | //! ## Cons 87 | //! 88 | //! * `nightly`-only: This library relies on some unstable features: 89 | //! * `#![feature(unboxed_closures)]` 90 | //! # `![feature(fn_traits)]` 91 | //! * immature and incomplete: This library is still work in progress, and will 92 | //! mature with time. 93 | //! 94 | //! # Where to start 95 | //! 96 | //! You are most probably interested in reading `html` module documentation 97 | #![feature(unboxed_closures)] 98 | #![feature(fn_traits)] 99 | use std::fmt::Arguments; 100 | use std::{fmt, io}; 101 | 102 | /// HTML rendering 103 | pub mod html; 104 | 105 | /// Rendering logic responsible for string escaping and such. 106 | /// 107 | /// See `html::Renderer` for implementation. 108 | pub trait Renderer { 109 | /// Normal write: perform escaping etc. if necessary 110 | fn write(&mut self, data: &[u8]) -> io::Result<()> { 111 | self.write_raw(data) 112 | } 113 | /// Normal write but with `format_args!` 114 | fn write_fmt(&mut self, fmt: &Arguments) -> io::Result<()> { 115 | self.write(format!("{}", fmt).as_bytes()) 116 | } 117 | /// Normal write for `&str` 118 | fn write_str(&mut self, s: &str) -> io::Result<()> { 119 | self.write(s.as_bytes()) 120 | } 121 | 122 | /// Raw write: no escaping should be performed 123 | fn write_raw(&mut self, data: &[u8]) -> io::Result<()>; 124 | 125 | /// Raw write but with `format_args!` 126 | fn write_raw_fmt(&mut self, fmt: &Arguments) -> io::Result<()> { 127 | self.write_raw(format!("{}", fmt).as_bytes()) 128 | } 129 | 130 | /// Raw write for `&str` 131 | fn write_raw_str(&mut self, s: &str) -> io::Result<()> { 132 | self.write_raw(s.as_bytes()) 133 | } 134 | } 135 | 136 | /// A `Renderer` that does not escape anything it renders 137 | /// 138 | /// A `Renderer` that uses underlying Renderer to call 139 | /// only `raw` methods, and thus avoid escaping values. 140 | pub struct RawRenderer<'a, T: 'a + ?Sized>(&'a mut T); 141 | 142 | impl<'a, T: 'a + Renderer + ?Sized> Renderer for RawRenderer<'a, T> { 143 | fn write(&mut self, data: &[u8]) -> io::Result<()> { 144 | self.0.write_raw(data) 145 | } 146 | fn write_fmt(&mut self, fmt: &Arguments) -> io::Result<()> { 147 | self.0.write_raw_fmt(fmt) 148 | } 149 | fn write_str(&mut self, s: &str) -> io::Result<()> { 150 | self.0.write_raw_str(s) 151 | } 152 | fn write_raw(&mut self, data: &[u8]) -> io::Result<()> { 153 | self.0.write_raw(data) 154 | } 155 | fn write_raw_fmt(&mut self, fmt: &Arguments) -> io::Result<()> { 156 | self.0.write_raw_fmt(fmt) 157 | } 158 | fn write_raw_str(&mut self, s: &str) -> io::Result<()> { 159 | self.0.write_raw_str(s) 160 | } 161 | } 162 | 163 | /// A value that can be rendered - part or a whole template 164 | /// 165 | /// This can be generally thought as a part or a whole `template`, 166 | /// with "the blanks" already filled with a data, but not yet 167 | /// rendered to `Renderer`. 168 | /// 169 | /// It is defined for bunch of `std` types. Please send PR if 170 | /// something is missing. 171 | /// 172 | /// You can impl it for your own types too. You usually compose it 173 | /// from many other `impl Render` data. 174 | pub trait Render { 175 | fn render(&self, &mut Renderer) -> io::Result<()>; 176 | } 177 | 178 | // {{{ impl Render 179 | impl Render for Vec { 180 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 181 | for t in self.iter() { 182 | t.render(r)?; 183 | } 184 | Ok(()) 185 | } 186 | } 187 | 188 | impl Render for [T] { 189 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 190 | for t in self.iter() { 191 | t.render(r)?; 192 | } 193 | Ok(()) 194 | } 195 | } 196 | 197 | macro_rules! impl_narr { 198 | ($n:expr) => { 199 | impl Render for [T; $n] { 200 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 201 | for t in self.iter() { 202 | t.render(r)?; 203 | } 204 | Ok(()) 205 | } 206 | } 207 | }; 208 | } 209 | 210 | impl_narr!(0); 211 | impl_narr!(1); 212 | impl_narr!(2); 213 | impl_narr!(3); 214 | impl_narr!(4); 215 | impl_narr!(5); 216 | impl_narr!(6); 217 | impl_narr!(7); 218 | impl_narr!(8); 219 | impl_narr!(9); 220 | impl_narr!(10); 221 | impl_narr!(11); 222 | impl_narr!(12); 223 | impl_narr!(13); 224 | impl_narr!(14); 225 | impl_narr!(15); 226 | impl_narr!(16); 227 | impl_narr!(17); 228 | impl_narr!(18); 229 | impl_narr!(19); 230 | impl_narr!(20); 231 | impl_narr!(21); 232 | impl_narr!(22); 233 | impl_narr!(23); 234 | impl_narr!(24); 235 | impl_narr!(25); 236 | impl_narr!(26); 237 | impl_narr!(27); 238 | impl_narr!(28); 239 | impl_narr!(29); 240 | impl_narr!(30); 241 | impl_narr!(31); 242 | impl_narr!(32); 243 | 244 | impl<'a, T: Render + ?Sized> Render for &'a mut T { 245 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 246 | (**self).render(r)?; 247 | Ok(()) 248 | } 249 | } 250 | 251 | impl Render for Box { 252 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 253 | (**self).render(r)?; 254 | Ok(()) 255 | } 256 | } 257 | 258 | impl Render for () { 259 | fn render(&self, _: &mut Renderer) -> io::Result<()> { 260 | Ok(()) 261 | } 262 | } 263 | 264 | impl Render for Option { 265 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 266 | if let &Some(ref s) = self { 267 | s.render(r)? 268 | } 269 | Ok(()) 270 | } 271 | } 272 | impl Render for String { 273 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 274 | r.write_raw(self.as_bytes()) 275 | } 276 | } 277 | 278 | macro_rules! impl_render_raw { 279 | ($t:ty) => { 280 | impl Render for $t { 281 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 282 | r.write_raw_fmt(&format_args!("{}", self)) 283 | } 284 | } 285 | }; 286 | } 287 | 288 | impl_render_raw!(f64); 289 | impl_render_raw!(f32); 290 | impl_render_raw!(i64); 291 | impl_render_raw!(u64); 292 | impl_render_raw!(i32); 293 | impl_render_raw!(u32); 294 | impl_render_raw!(usize); 295 | impl_render_raw!(isize); 296 | 297 | impl<'a> Render for &'a str { 298 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 299 | r.write_str(self) 300 | } 301 | } 302 | 303 | impl<'a> Render for fmt::Arguments<'a> { 304 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 305 | r.write_fmt(self) 306 | } 307 | } 308 | 309 | impl<'a> Render for &'a fmt::Arguments<'a> { 310 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 311 | r.write_fmt(self) 312 | } 313 | } 314 | 315 | impl Render for (A,) 316 | where 317 | A: Render, 318 | { 319 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 320 | self.0.render(r) 321 | } 322 | } 323 | 324 | impl Render for (A, B) 325 | where 326 | A: Render, 327 | B: Render, 328 | { 329 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 330 | self.0.render(r)?; 331 | self.1.render(r) 332 | } 333 | } 334 | 335 | impl Render for (A, B, C) 336 | where 337 | A: Render, 338 | B: Render, 339 | C: Render, 340 | { 341 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 342 | self.0.render(r)?; 343 | self.1.render(r)?; 344 | self.2.render(r) 345 | } 346 | } 347 | 348 | impl Render for (A, B, C, D) 349 | where 350 | A: Render, 351 | B: Render, 352 | C: Render, 353 | D: Render, 354 | { 355 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 356 | self.0.render(r)?; 357 | self.1.render(r)?; 358 | self.2.render(r)?; 359 | self.3.render(r) 360 | } 361 | } 362 | impl Render for (A, B, C, D, E) 363 | where 364 | A: Render, 365 | B: Render, 366 | C: Render, 367 | D: Render, 368 | E: Render, 369 | { 370 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 371 | self.0.render(r)?; 372 | self.1.render(r)?; 373 | self.2.render(r)?; 374 | self.3.render(r)?; 375 | self.4.render(r) 376 | } 377 | } 378 | 379 | impl Render for (A, B, C, D, E, F) 380 | where 381 | A: Render, 382 | B: Render, 383 | C: Render, 384 | D: Render, 385 | E: Render, 386 | F: Render, 387 | { 388 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 389 | self.0.render(r)?; 390 | self.1.render(r)?; 391 | self.2.render(r)?; 392 | self.3.render(r)?; 393 | self.4.render(r)?; 394 | self.5.render(r) 395 | } 396 | } 397 | 398 | impl Render for (A, B, C, D, E, F, G) 399 | where 400 | A: Render, 401 | B: Render, 402 | C: Render, 403 | D: Render, 404 | E: Render, 405 | F: Render, 406 | G: Render, 407 | { 408 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 409 | self.0.render(r)?; 410 | self.1.render(r)?; 411 | self.2.render(r)?; 412 | self.3.render(r)?; 413 | self.4.render(r)?; 414 | self.5.render(r)?; 415 | self.6.render(r) 416 | } 417 | } 418 | 419 | impl Render for (A, B, C, D, E, F, G, H) 420 | where 421 | A: Render, 422 | B: Render, 423 | C: Render, 424 | D: Render, 425 | E: Render, 426 | F: Render, 427 | G: Render, 428 | H: Render, 429 | { 430 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 431 | self.0.render(r)?; 432 | self.1.render(r)?; 433 | self.2.render(r)?; 434 | self.3.render(r)?; 435 | self.4.render(r)?; 436 | self.5.render(r)?; 437 | self.6.render(r)?; 438 | self.7.render(r)?; 439 | Ok(()) 440 | } 441 | } 442 | 443 | /// Use to wrap closures with 444 | pub struct Fn(pub F); 445 | 446 | impl Render for Fn 447 | where 448 | F: std::ops::Fn(&mut Renderer) -> io::Result<()>, 449 | { 450 | fn render(&self, r: &mut Renderer) -> io::Result<()> { 451 | self.0(r) 452 | } 453 | } 454 | // }}} 455 | // 456 | // vim: foldmethod=marker foldmarker={{{,}}} 457 | --------------------------------------------------------------------------------