├── display-as ├── README.md ├── src │ ├── grade.html │ ├── student.html │ ├── base.html │ ├── class.html │ ├── rust.rs │ ├── utf8.rs │ ├── url.rs │ ├── latex.rs │ ├── mathlatex.rs │ ├── html.rs │ ├── float.rs │ └── lib.rs ├── tests │ ├── from-file-include.html │ ├── from-file-base.html │ ├── from-file-base-base.html │ ├── from-file.html │ ├── delimiters.html │ ├── match.rs │ ├── mixed-types.rs │ ├── if_let_with_pattern.rs │ ├── loop_in_let.rs │ ├── write_as.rs │ ├── format_as.rs │ └── with_template.rs ├── benches │ ├── team-displayas.html │ ├── big-table-displayas.html │ ├── teams-displayas.html │ └── templates-benchmark-rs.rs └── Cargo.toml ├── display-as-proc-macro ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── Cargo.toml ├── .gitignore ├── .github └── workflows │ └── test.yml └── README.md /display-as/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /display-as-proc-macro/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /display-as/src/grade.html: -------------------------------------------------------------------------------- 1 |
"# self.value r#" 2 | 3 | -------------------------------------------------------------------------------- /display-as/tests/from-file-include.html: -------------------------------------------------------------------------------- 1 | "# include!("from-file.html"); r#" 2 | -------------------------------------------------------------------------------- /display-as/benches/team-displayas.html: -------------------------------------------------------------------------------- 1 | [% team.name %]: [% team.score %] 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = ["display-as", "display-as-proc-macro"] 4 | -------------------------------------------------------------------------------- /display-as/src/student.html: -------------------------------------------------------------------------------- 1 | Name: "# self.name r#" 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | *~ 5 | #* 6 | /display-as/target 7 | -------------------------------------------------------------------------------- /display-as/src/base.html: -------------------------------------------------------------------------------- 1 | "# title r#" 2 | 3 | "# body r#" 4 | 5 | 6 | -------------------------------------------------------------------------------- /display-as/tests/from-file-base.html: -------------------------------------------------------------------------------- 1 | "# 2 | let name = { self as URL }; 3 | let age = { self.age " years old" }; 4 | include!("from-file-base-base.html"); 5 | r#" 6 | -------------------------------------------------------------------------------- /display-as/benches/big-table-displayas.html: -------------------------------------------------------------------------------- 1 | 2 | [% for row in self.table.iter() { %] 3 | [% for col in row.iter() { %][% } %] 4 | [% } %] 5 |
[% col %]
6 | -------------------------------------------------------------------------------- /display-as/tests/from-file-base-base.html: -------------------------------------------------------------------------------- 1 | FromFile: " 2 | if self.age < 18 { 3 | r"minor " name 4 | } else { 5 | r"grown-up " name r" who is " age 6 | } 7 | r" (THE END) 8 | -------------------------------------------------------------------------------- /display-as/src/class.html: -------------------------------------------------------------------------------- 1 | "# 2 | 3 | let title = { "PH" self.coursenumber ": " self.coursename }; 4 | let body = { 5 | r#""# 11 | }; 12 | include!("base.html"); 13 | 14 | r#" 15 | -------------------------------------------------------------------------------- /display-as/tests/from-file.html: -------------------------------------------------------------------------------- 1 | FromFile: " 2 | if self.age < 18 { 3 | r"minor " &self.name 4 | } else { 5 | r"grown-up " &self.name r" who is " self.age r" years old" 6 | } 7 | r" (THE END) 8 | -------------------------------------------------------------------------------- /display-as/tests/delimiters.html: -------------------------------------------------------------------------------- 1 | FromFile: }} 2 | if self.age < 18 { 3 | {{minor }} &self.name 4 | } else { 5 | {{grown-up }} &self.name {{ who is }} self.age {{ years old}} 6 | } 7 | {{ (THE END) 8 | -------------------------------------------------------------------------------- /display-as/benches/teams-displayas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [% self.year %] 4 | 5 | 6 |

CSL [% self.year %]

7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /display-as-proc-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "display-as-proc-macro" 3 | version = "0.6.3" 4 | authors = ["David Roundy "] 5 | description = "A helper crate for display-as-template." 6 | repository = "https://github.com/droundy/display-as" 7 | license = "Apache-2.0 OR MIT" 8 | edition = "2018" 9 | 10 | readme = "README.md" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | quote = "1.0.15" 17 | proc-macro2 = "1.0.36" 18 | glob = "0.3.0" 19 | rand = "0.8.3" -------------------------------------------------------------------------------- /display-as/tests/match.rs: -------------------------------------------------------------------------------- 1 | extern crate display_as; 2 | 3 | use display_as::{format_as, HTML}; 4 | 5 | #[test] 6 | fn test_match() { 7 | let mut someone = Some(1); 8 | assert_eq!( 9 | format_as!(HTML, match someone { 10 | Some(x) => { x } 11 | None => { "None!" } 12 | }).into_string(), 13 | r"1" 14 | ); 15 | 16 | someone = None; 17 | assert_eq!( 18 | format_as!(HTML, match someone { 19 | Some(x) => { x } 20 | None => { "None!" } 21 | }).into_string(), 22 | r"None!" 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /display-as/tests/mixed-types.rs: -------------------------------------------------------------------------------- 1 | use display_as::{format_as, URL, HTML}; 2 | 3 | #[test] 4 | fn mixed_types() { 5 | assert_eq!(&format_as!(HTML, "hello world " 5 as URL " urls are " 5.0).into_string(), 6 | "hello world 5 urls are 5"); 7 | } 8 | 9 | #[test] 10 | fn mixed_formats_in_let() { 11 | assert_eq!(&format_as!(HTML, 12 | let foo = { 13 | "hello world " 5 as URL " urls are " 5.0 14 | }; 15 | foo).into_string(), 16 | "hello world 5 urls are 5"); 17 | } 18 | -------------------------------------------------------------------------------- /display-as/tests/if_let_with_pattern.rs: -------------------------------------------------------------------------------- 1 | extern crate display_as; 2 | 3 | use display_as::{format_as, HTML}; 4 | 5 | #[test] 6 | #[allow(irrefutable_let_patterns)] 7 | fn test_if_let() { 8 | struct Foo { x: usize } 9 | let foo = Foo { x: 37 }; 10 | assert_eq!( 11 | format_as!(HTML, if let Foo { x } = foo { 12 | x 13 | }).into_string(), 14 | r"37" 15 | ); 16 | } 17 | 18 | #[test] 19 | fn test_let_with_braces_match() { 20 | struct Foo { x: usize } 21 | let foo = Foo { x: 37 }; 22 | assert_eq!( 23 | format_as!(HTML, { 24 | let Foo { x } = foo; 25 | x 26 | }).into_string(), 27 | r"37" 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /display-as/src/rust.rs: -------------------------------------------------------------------------------- 1 | //! Format as rust code 2 | 3 | use super::*; 4 | 5 | /// Format as Rust. 6 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 7 | pub struct Rust; 8 | impl Format for Rust { 9 | fn escape(f: &mut Formatter, s: &str) -> Result<(), Error> { 10 | (&s as &dyn std::fmt::Debug).fmt(f) 11 | } 12 | fn mime() -> mime::Mime { 13 | return "text/x-rust".parse().unwrap(); 14 | } 15 | fn this_format() -> Self { 16 | Rust 17 | } 18 | } 19 | 20 | display_integers_as!(Rust); 21 | display_floats_as!(Rust, "e", "", 1, None); 22 | 23 | #[test] 24 | fn escaping() { 25 | assert_eq!(&format_as!(Rust, ("&")).into_string(), r#""&""#); 26 | } 27 | #[test] 28 | fn floats() { 29 | assert_eq!(&format_as!(Rust, 3.0).into_string(), "3"); 30 | assert_eq!(&format_as!(Rust, 3e5).into_string(), "3e5"); 31 | assert_eq!(&format_as!(Rust, 3e4).into_string(), "3e4"); 32 | assert_eq!(&format_as!(Rust, 3e3).into_string(), "3e3"); 33 | assert_eq!(&format_as!(Rust, 3e2).into_string(), "300"); 34 | } 35 | -------------------------------------------------------------------------------- /display-as/src/utf8.rs: -------------------------------------------------------------------------------- 1 | //! Format as rust code 2 | 3 | use super::*; 4 | 5 | /// Format as raw UTF8. 6 | /// 7 | /// This is one way to output a raw string. 8 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 9 | pub struct UTF8; 10 | impl Format for UTF8 { 11 | fn escape(f: &mut Formatter, s: &str) -> Result<(), Error> { 12 | f.write_str(s) 13 | } 14 | fn mime() -> mime::Mime { 15 | return mime::TEXT_PLAIN_UTF_8; 16 | } 17 | fn this_format() -> Self { 18 | UTF8 19 | } 20 | } 21 | 22 | display_integers_as!(UTF8); 23 | display_floats_as!(UTF8, "e", "", 1, None); 24 | 25 | #[test] 26 | fn escaping() { 27 | assert_eq!(&format_as!(UTF8, ("&")).into_string(), "&"); 28 | } 29 | #[test] 30 | fn floats() { 31 | assert_eq!(&format_as!(UTF8, 3.0).into_string(), "3"); 32 | assert_eq!(&format_as!(UTF8, 3e5).into_string(), "3e5"); 33 | assert_eq!(&format_as!(UTF8, 3e4).into_string(), "3e4"); 34 | assert_eq!(&format_as!(UTF8, 3e3).into_string(), "3e3"); 35 | assert_eq!(&format_as!(UTF8, 3e2).into_string(), "300"); 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | rust: 12 | - stable 13 | - 1.54.0 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: ${{ matrix.rust }} 20 | override: true 21 | - uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | args: --all --all-features 25 | 26 | test: 27 | name: Test Suite 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | rust: 32 | - stable 33 | # - 1.54.0 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions-rs/toolchain@v1 37 | with: 38 | profile: minimal 39 | toolchain: ${{ matrix.rust }} 40 | override: true 41 | - uses: actions-rs/cargo@v1 42 | with: 43 | command: test 44 | args: --all 45 | -------------------------------------------------------------------------------- /display-as/tests/loop_in_let.rs: -------------------------------------------------------------------------------- 1 | extern crate display_as; 2 | 3 | use display_as::{format_as, HTML}; 4 | 5 | #[test] 6 | fn test_let() { 7 | let foos = vec!["hello", "world"]; 8 | assert_eq!( 9 | format_as!(HTML, let foo = { 10 | for i in foos.iter() { 11 | "counting " i " " 12 | } 13 | }; 14 | foo).into_string(), 15 | r"counting hello counting world " 16 | ); 17 | } 18 | 19 | #[test] 20 | fn test_loop_no_let() { 21 | assert_eq!( 22 | format_as!( 23 | HTML, 24 | for i in [1u8, 2].iter() { 25 | "counting " * i 26 | } 27 | ).into_string(), 28 | r"counting 1counting 2" 29 | ); 30 | } 31 | 32 | #[test] 33 | fn test_loop_no_let_b() { 34 | assert_eq!( 35 | format_as!(HTML, 36 | for i in [1u8,2].iter() { 37 | let j: u8 = *i; 38 | "counting " j 39 | }).into_string(), 40 | r"counting 1counting 2" 41 | ); 42 | } 43 | 44 | #[test] 45 | fn test_no_loop_no_let_c() { 46 | assert_eq!(&format_as!(HTML, let i = 1u8; i).into_string(), r"1"); 47 | } 48 | -------------------------------------------------------------------------------- /display-as/tests/write_as.rs: -------------------------------------------------------------------------------- 1 | use display_as::{write_as, HTML}; 2 | 3 | #[test] 4 | fn write_to_string() { 5 | let mut s = String::new(); 6 | write_as!(HTML, s, r"Hello world").unwrap(); 7 | assert_eq!(&s, r"Hello world"); 8 | } 9 | 10 | #[test] 11 | fn write_to_mut_ref_string() { 12 | let mut s = String::new(); 13 | write_as!(HTML, &mut s, r"Hello world").unwrap(); 14 | assert_eq!(&s, r"Hello world"); 15 | } 16 | 17 | #[test] 18 | fn write_integer() { 19 | let mut s = String::new(); 20 | write_as!(HTML, s, 137).unwrap(); 21 | assert_eq!(&s, r"137"); 22 | } 23 | 24 | #[test] 25 | fn write_nice_loop() { 26 | let data = ["hello", "world"]; 27 | let mut s = String::new(); 28 | write_as!(HTML, s, for d in data.iter() { 29 | " " d 30 | }) 31 | .unwrap(); 32 | assert_eq!(&s, r" hello world"); 33 | } 34 | 35 | #[test] 36 | fn write_nice_loop_strings() { 37 | let data = ["hello".to_string(), "world".to_string()]; 38 | let mut s = String::new(); 39 | write_as!(HTML, s, for d in data.iter() { 40 | " " d 41 | }) 42 | .unwrap(); 43 | assert_eq!(&s, r" hello world"); 44 | } 45 | -------------------------------------------------------------------------------- /display-as/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "display-as" 3 | version = "0.7.0" 4 | authors = ["David Roundy "] 5 | description = "Compile-time templates for displaying data in different markup formats." 6 | repository = "https://github.com/droundy/display-as" 7 | keywords = ["template", "latex", "html", "display"] 8 | license = "Apache-2.0 OR MIT" 9 | edition = "2018" 10 | readme = "README.md" 11 | 12 | [features] 13 | 14 | gotham-web = ["gotham"] 15 | serde1 = ["serde"] 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | 20 | [dependencies] 21 | 22 | display-as-proc-macro = { version = "0.6.2", path = "../display-as-proc-macro" } 23 | 24 | mime = "0.3.12" 25 | percent-encoding = "1.0.1" 26 | 27 | rouille = { version = "2.2.0", optional = true } 28 | actix-web = { version = "0.7.14", optional = true } 29 | gotham = { version = "0.5.0", optional = true } 30 | warp = { version = "0.3.3", optional = true } 31 | 32 | serde = { version = "1.0.125", features = ["derive"], optional = true } 33 | 34 | [dev-dependencies] 35 | criterion = "0.2" 36 | 37 | [[bench]] 38 | name = "templates-benchmark-rs" 39 | harness = false 40 | -------------------------------------------------------------------------------- /display-as/src/url.rs: -------------------------------------------------------------------------------- 1 | //! [Format] as URL, with escaping using percent encoding. 2 | 3 | use super::*; 4 | use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; 5 | 6 | /// [Format] as URL. 7 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 8 | pub struct URL; 9 | impl Format for URL { 10 | fn escape(f: &mut Formatter, s: &str) -> Result<(), Error> { 11 | f.write_str(&utf8_percent_encode(s, DEFAULT_ENCODE_SET).to_string()) 12 | } 13 | /// The MIME type for URL is [mime::TEXT_URL_UTF_8]. 14 | fn mime() -> mime::Mime { 15 | return "text/x-url".parse().unwrap(); 16 | } 17 | fn this_format() -> Self { 18 | URL 19 | } 20 | } 21 | 22 | display_integers_as!(URL); 23 | display_floats_as!(URL, "e", "", 1, None); 24 | 25 | #[test] 26 | fn escaping() { 27 | assert_eq!(&format_as!(URL, ("&")).into_string(), "&"); 28 | assert_eq!( 29 | &format_as!(URL, ("hello &>this is cool")).into_string(), 30 | "hello%20&%3Ethis%20is%20cool" 31 | ); 32 | assert_eq!( 33 | &format_as!(URL, ("hello &>this is 'cool")).into_string(), 34 | "hello%20&%3Ethis%20is%20\'cool" 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DisplayAs 2 | 3 | [![Documentation](https://docs.rs/display-as/badge.svg)](https://docs.rs/display-as) 4 | [![Build Status](https://github.com/droundy/display-as/actions/workflows/test.yml/badge.svg)](https://github.com/droundy/display-as/actions) 5 | 6 | These crates creates rusty templates that are evaluated at 7 | compile-time (much like [askama](https://docs.rs/askama)). 8 | `DisplayAs` is explicitly designed to support multiple output formats 9 | (thus the "as" in its name). 10 | 11 | ## Comparison with other template engines in rust 12 | 13 | Given there are numerous existing template engines, you might ask what 14 | distinguishes `display-as` from these other engines? 15 | 16 | 1. The most notable distinction is that `display-as` 17 | compiles the templates at compile time, like 18 | [askama](https://docs.rs/askama) and 19 | [ructe](https://crates.io/crates/ructe) but unlike most other 20 | engines. 21 | 22 | 2. `diplay-as-template` supports (almost) arbitrary rust code in the 23 | template, unlike [askama](https://github.com/djc/askama/issues/95) 24 | or ructe. In the case of askama, there is a conscious decision not 25 | to support this. I believe that it is nicer and easier not to 26 | learn a new language for the expressiosn within templates. 27 | 28 | 3. `DisplayAs` and `display-as` support embedding one format 29 | into another, so that you can mix languages. This is most common 30 | in HTML, which supports numerous formats such as javascript or CSS, 31 | but also math mode within either LaTeX or even in HTML using 32 | MathJax. This has been discussed as a 33 | [possible feature in ructe](https://github.com/kaj/ructe/issues/1). 34 | 35 | 4. Using `display-as` is typesafe on the output side as well 36 | as the input side. You can't accidentally include javascript 37 | formatted text into HTML, or 38 | [double-escape HTML strings](https://github.com/djc/askama/issues/108). 39 | 40 | -------------------------------------------------------------------------------- /display-as/src/latex.rs: -------------------------------------------------------------------------------- 1 | //! Format as LaTeX 2 | 3 | use super::*; 4 | 5 | /// Format as LaTeX. 6 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 7 | pub struct LaTeX; 8 | impl Format for LaTeX { 9 | fn mime() -> mime::Mime { 10 | return "text/x-latex".parse().unwrap(); 11 | } 12 | fn this_format() -> Self { 13 | LaTeX 14 | } 15 | fn escape(f: &mut Formatter, mut s: &str) -> Result<(), Error> { 16 | let badstuff = "&{}#%\\~$_^"; 17 | while let Some(idx) = s.find(|c| badstuff.contains(c)) { 18 | let (first, rest) = s.split_at(idx); 19 | let (badchar, tail) = rest.split_at(1); 20 | f.write_str(first)?; 21 | f.write_str(match badchar { 22 | "&" => r"\&", 23 | "{" => r"\{", 24 | "}" => r"\}", 25 | "#" => r"\#", 26 | "%" => r"\%", 27 | "\\" => r"\textbackslash{}", 28 | "~" => r"\textasciitilde{}", 29 | "$" => r"\$", 30 | "_" => r"\_", 31 | "^" => r"\^", 32 | _ => unreachable!(), 33 | })?; 34 | s = tail; 35 | } 36 | f.write_str(s) 37 | } 38 | } 39 | 40 | display_integers_as!(LaTeX); 41 | display_floats_as!(LaTeX, r"$\times10^{", "}$", 3, Some("$10^{")); 42 | 43 | #[test] 44 | fn escaping() { 45 | assert_eq!(&format_as!(LaTeX, ("&")).into_string(), r"\&"); 46 | assert_eq!( 47 | &format_as!(LaTeX, ("hello &>this is cool")).into_string(), 48 | r"hello \&>this is cool" 49 | ); 50 | assert_eq!( 51 | &format_as!(LaTeX, ("hello &>this is 'cool")).into_string(), 52 | r"hello \&>this is 'cool" 53 | ); 54 | } 55 | #[test] 56 | fn floats() { 57 | assert_eq!(&format_as!(LaTeX, 3.0).into_string(), "3"); 58 | assert_eq!(&format_as!(LaTeX, 3e5).into_string(), r"3$\times10^{5}$"); 59 | assert_eq!(&format_as!(LaTeX, 3e4).into_string(), "30000"); 60 | } 61 | -------------------------------------------------------------------------------- /display-as/src/mathlatex.rs: -------------------------------------------------------------------------------- 1 | //! Format as LaTeX math mode 2 | 3 | use super::*; 4 | 5 | /// Format as LaTeX math mode. 6 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 7 | pub struct Math; 8 | impl Format for Math { 9 | fn mime() -> mime::Mime { 10 | return "text/x-latex".parse().unwrap(); 11 | } 12 | fn this_format() -> Self { 13 | Math 14 | } 15 | fn escape(f: &mut Formatter, mut s: &str) -> Result<(), Error> { 16 | let badstuff = "&{}#%\\~$_^"; 17 | while let Some(idx) = s.find(|c| badstuff.contains(c)) { 18 | let (first, rest) = s.split_at(idx); 19 | let (badchar, tail) = rest.split_at(1); 20 | f.write_str(first)?; 21 | f.write_str(match badchar { 22 | "&" => r"\&", 23 | "{" => r"\{", 24 | "}" => r"\}", 25 | "#" => r"\#", 26 | "%" => r"\%", 27 | "\\" => r"\textbackslash{}", 28 | "~" => r"\textasciitilde{}", 29 | "$" => r"\$", 30 | "_" => r"\_", 31 | "^" => r"\^", 32 | _ => unreachable!(), 33 | })?; 34 | s = tail; 35 | } 36 | f.write_str(s) 37 | } 38 | } 39 | 40 | display_integers_as!(Math); 41 | display_floats_as!(Math, r"\times10^{", "}", 3, Some("10^{")); 42 | 43 | #[test] 44 | fn escaping() { 45 | assert_eq!(&format_as!(Math, ("&")).into_string(), r"\&"); 46 | assert_eq!( 47 | &format_as!(Math, ("hello &>this is cool")).into_string(), 48 | r"hello \&>this is cool" 49 | ); 50 | assert_eq!( 51 | &format_as!(Math, ("hello &>this is 'cool")).into_string(), 52 | r"hello \&>this is 'cool" 53 | ); 54 | } 55 | #[test] 56 | fn floats() { 57 | assert_eq!(&format_as!(Math, 3.0).into_string(), "3"); 58 | assert_eq!(&format_as!(Math, 3e5).into_string(), r"3\times10^{5}"); 59 | assert_eq!(&format_as!(Math, 1e5).into_string(), r"10^{5}"); 60 | assert_eq!(&format_as!(Math, 3e4).into_string(), "30000"); 61 | } 62 | -------------------------------------------------------------------------------- /display-as/benches/templates-benchmark-rs.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use display_as::{with_template, DisplayAs, HTML, format_as}; 5 | 6 | pub fn displayas_big_table(b: &mut criterion::Bencher, size: &usize) { 7 | let mut table = Vec::with_capacity(*size); 8 | for _ in 0..*size { 9 | let mut inner = Vec::with_capacity(*size); 10 | for i in 0..*size { 11 | inner.push(i); 12 | } 13 | table.push(inner); 14 | } 15 | let ctx = BigTable { table }; 16 | b.iter(|| format_as!(HTML, ctx)); 17 | } 18 | 19 | struct BigTable { 20 | table: Vec>, 21 | } 22 | 23 | #[with_template("[%" "%]" "big-table-displayas.html")] 24 | impl DisplayAs for BigTable {} 25 | 26 | pub fn displayas_teams(b: &mut criterion::Bencher, _: &usize) { 27 | let teams = Teams { 28 | year: 2015, 29 | teams: vec![ 30 | Team { 31 | name: "Jiangsu".into(), 32 | score: 43, 33 | }, 34 | Team { 35 | name: "Beijing".into(), 36 | score: 27, 37 | }, 38 | Team { 39 | name: "Guangzhou".into(), 40 | score: 22, 41 | }, 42 | Team { 43 | name: "Shandong".into(), 44 | score: 12, 45 | }, 46 | ], 47 | }; 48 | b.iter(|| format_as!(HTML, teams)); 49 | } 50 | 51 | struct Teams { 52 | year: u16, 53 | teams: Vec, 54 | } 55 | 56 | #[with_template("[%" "%]" "teams-displayas.html")] 57 | impl DisplayAs for Teams {} 58 | 59 | struct Team { 60 | name: String, 61 | score: u8, 62 | } 63 | 64 | use criterion::{Criterion, Fun}; 65 | 66 | fn big_table(c: &mut Criterion) { 67 | c.bench_functions( 68 | "Big table", 69 | vec![ 70 | Fun::new("DisplayAs", displayas_big_table), 71 | ], 72 | 100, 73 | ); 74 | } 75 | 76 | fn teams(c: &mut Criterion) { 77 | c.bench_functions( 78 | "Teams", 79 | vec![ 80 | Fun::new("DisplayAs", displayas_teams), 81 | ], 82 | 0, 83 | ); 84 | } 85 | 86 | criterion_group!(benches, big_table, teams); 87 | criterion_main!(benches); 88 | -------------------------------------------------------------------------------- /display-as/tests/format_as.rs: -------------------------------------------------------------------------------- 1 | extern crate display_as; 2 | 3 | use display_as::{format_as, with_template, DisplayAs, Rust, HTML}; 4 | 5 | #[test] 6 | fn just_string() { 7 | assert_eq!( 8 | format_as!(HTML, r"Hello world").into_string(), 9 | r"Hello world" 10 | ); 11 | } 12 | #[test] 13 | fn string_and_integer() { 14 | assert_eq!( 15 | format_as!(HTML, r"Number " 3 r" is odd").into_string(), 16 | r"Number 3 is odd" 17 | ); 18 | assert_eq!( 19 | format_as!(HTML, r"Number " 2+1 r" is odd").into_string(), 20 | r"Number 3 is odd" 21 | ); 22 | assert_eq!( 23 | format_as!(HTML, r"Number " 4-1 r" is odd").into_string(), 24 | r"Number 3 is odd" 25 | ); 26 | assert_eq!( 27 | format_as!(HTML, r"Number " 8/2-1 r" is odd").into_string(), 28 | r"Number 3 is odd" 29 | ); 30 | assert_eq!( 31 | format_as!(HTML, r"Number " 1*3 r" is odd").into_string(), 32 | r"Number 3 is odd" 33 | ); 34 | } 35 | #[test] 36 | fn integer_reference() { 37 | assert_eq!( 38 | format_as!(HTML, r"Number " &3u64 r" is odd").into_string(), 39 | r"Number 3 is odd" 40 | ); 41 | assert_eq!( 42 | format_as!(HTML, r"Number " 3u64 r" is odd").into_string(), 43 | r"Number 3 is odd" 44 | ); 45 | assert_eq!( 46 | format_as!(HTML, r"Number " &&3u64 r" is odd").into_string(), 47 | r"Number 3 is odd" 48 | ); 49 | } 50 | #[test] 51 | fn string_and_float() { 52 | assert_eq!( 53 | format_as!(HTML, r"Number " 3.0 r" is odd").into_string(), 54 | r"Number 3 is odd" 55 | ); 56 | assert_eq!( 57 | format_as!(Rust, r"Number " 1e10 r" is even").into_string(), 58 | r"Number 1e10 is even" 59 | ); 60 | assert_eq!( 61 | format_as!(Rust, r"Number " 1e2 r" is even").into_string(), 62 | r"Number 100 is even" 63 | ); 64 | assert_eq!( 65 | format_as!(Rust, r"Number " 1.2345e2 r" is even").into_string(), 66 | r"Number 123.45 is even" 67 | ); 68 | } 69 | #[test] 70 | fn test_conditionals() { 71 | let foo = 3.0; 72 | let bar = 2.0; 73 | assert_eq!( 74 | format_as!(HTML, r"Game: " if foo > bar { r"foo wins" }).into_string(), 75 | r"Game: foo wins" 76 | ); 77 | assert_eq!( 78 | format_as!(HTML, r"Counting: " for i in 0..5 { i " " }).into_string(), 79 | r"Counting: 0 1 2 3 4 " 80 | ); 81 | } 82 | 83 | #[test] 84 | fn test_mixed_formats() { 85 | let foo = 3e6; 86 | assert_eq!( 87 | format_as!(HTML, r"Number: " foo r" and " foo as Rust r"!").into_string(), 88 | r"Number: 3×106 and 3e6!" 89 | ); 90 | } 91 | 92 | #[test] 93 | fn test_let() { 94 | assert_eq!( 95 | format_as!(HTML, let foo = { 96 | for i in 0..3 { 97 | "counting " i " " 98 | } 99 | }; 100 | foo).into_string(), 101 | r"counting 0 counting 1 counting 2 " 102 | ); 103 | } 104 | #[test] 105 | fn test_let_again() { 106 | struct Foo(isize); 107 | #[with_template("Foo " self.0)] 108 | impl DisplayAs for Foo {} 109 | let foos = vec![Foo(1), Foo(2)]; 110 | assert_eq!( 111 | format_as!(HTML, 112 | let foo = { 113 | "I am " 114 | for i in foos.iter() { 115 | "counting " i " " 116 | } 117 | }; 118 | foo "and I am done!").into_string(), 119 | r"I am counting Foo 1 counting Foo 2 and I am done!" 120 | ); 121 | } 122 | 123 | #[test] 124 | fn nested_format_as() { 125 | struct Foo(isize); 126 | #[with_template("Foo " self.0)] 127 | impl DisplayAs for Foo {} 128 | format_as!(HTML, "testing" format_as!(HTML, "hello" Foo(2)) " and " Foo(1)); 129 | } -------------------------------------------------------------------------------- /display-as/tests/with_template.rs: -------------------------------------------------------------------------------- 1 | extern crate display_as; 2 | 3 | use display_as::{with_template, DisplayAs, HTML, URL, format_as}; 4 | 5 | struct Foo { 6 | name: String, 7 | age: usize, 8 | } 9 | 10 | #[with_template("Foo: " &self.name " with age " self.age)] 11 | impl DisplayAs for Foo {} 12 | 13 | #[test] 14 | fn foo() { 15 | assert_eq!( 16 | &format!( 17 | "{}", 18 | Foo { 19 | name: "David".to_string(), 20 | age: 45 21 | } 22 | .display() 23 | ), 24 | "Foo: David with age 45" 25 | ); 26 | } 27 | 28 | struct TestingIf { 29 | name: String, 30 | age: usize, 31 | } 32 | 33 | #[with_template(r"TestingIf: " 34 | if self.age < 18 { 35 | r"minor " &self.name 36 | } else { 37 | r"grown-up " &self.name r" who is " self.age r" years old" 38 | } 39 | r" (THE END)" 40 | )] 41 | impl DisplayAs for TestingIf {} 42 | 43 | #[test] 44 | fn testing_if() { 45 | assert_eq!( 46 | &format!( 47 | "{}", 48 | TestingIf { 49 | name: "David".to_string(), 50 | age: 45 51 | } 52 | .display() 53 | ), 54 | "TestingIf: grown-up David who is 45 years old (THE END)" 55 | ); 56 | assert_eq!( 57 | &format!( 58 | "{}", 59 | TestingIf { 60 | name: "Miri".to_string(), 61 | age: 2 62 | } 63 | .display() 64 | ), 65 | "TestingIf: minor Miri (THE END)" 66 | ); 67 | } 68 | 69 | struct FromFile { 70 | name: String, 71 | age: usize, 72 | } 73 | 74 | #[with_template("from-file.html")] 75 | impl DisplayAs for FromFile {} 76 | 77 | #[test] 78 | fn from_file() { 79 | assert_eq!( 80 | &format!( 81 | "{}", 82 | FromFile { 83 | name: "David".to_string(), 84 | age: 45 85 | } 86 | .display() 87 | ), 88 | "FromFile: grown-up David who is 45 years old (THE END)\n" 89 | ); 90 | assert_eq!( 91 | &format!( 92 | "{}", 93 | FromFile { 94 | name: "Miri".to_string(), 95 | age: 2 96 | } 97 | .display() 98 | ), 99 | "FromFile: minor Miri (THE END)\n" 100 | ); 101 | } 102 | 103 | struct FromFileInclude { 104 | name: String, 105 | age: usize, 106 | } 107 | 108 | #[with_template("from-file-include.html")] 109 | impl DisplayAs for FromFileInclude {} 110 | 111 | #[test] 112 | fn from_file_include() { 113 | assert_eq!( 114 | &format!( 115 | "{}", 116 | FromFileInclude { 117 | name: "David".to_string(), 118 | age: 45 119 | } 120 | .display() 121 | ), 122 | "FromFile: grown-up David who is 45 years old (THE END)\n\n" 123 | ); 124 | assert_eq!( 125 | &format!( 126 | "{}", 127 | FromFileInclude { 128 | name: "Miri".to_string(), 129 | age: 2 130 | } 131 | .display() 132 | ), 133 | "FromFile: minor Miri (THE END)\n\n" 134 | ); 135 | } 136 | 137 | struct FromFileBase { 138 | name: String, 139 | age: usize, 140 | } 141 | 142 | #[with_template("from-file-base.html")] 143 | impl DisplayAs for FromFileBase {} 144 | #[with_template("url/" self.name)] 145 | impl DisplayAs for FromFileBase {} 146 | 147 | #[test] 148 | fn from_file_base() { 149 | assert_eq!( 150 | &format_as!(HTML, (FromFileBase { 151 | name: "David".to_string(), 152 | age: 45 153 | })).into_string(), 154 | "FromFile: grown-up url/David who is 45 years old (THE END)\n\n" 155 | ); 156 | assert_eq!( 157 | &format_as!(HTML, (FromFileBase { 158 | name: "Miri".to_string(), 159 | age: 2 160 | })).into_string(), 161 | "FromFile: minor url/Miri (THE END)\n\n" 162 | ); 163 | } 164 | 165 | 166 | struct Delimiters { 167 | name: String, 168 | age: usize, 169 | } 170 | #[with_template("}}" "{{" "delimiters.html")] 171 | impl DisplayAs for Delimiters {} 172 | 173 | #[test] 174 | fn from_delimiters() { 175 | assert_eq!( 176 | &format!( 177 | "{}", 178 | Delimiters { 179 | name: "David".to_string(), 180 | age: 45 181 | } 182 | .display() 183 | ), 184 | "FromFile: grown-up David who is 45 years old (THE END)\n" 185 | ); 186 | assert_eq!( 187 | &format!( 188 | "{}", 189 | Delimiters { 190 | name: "Miri".to_string(), 191 | age: 2 192 | } 193 | .display() 194 | ), 195 | "FromFile: minor Miri (THE END)\n" 196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /display-as/src/html.rs: -------------------------------------------------------------------------------- 1 | //! [Format] as HTML 2 | 3 | use super::*; 4 | 5 | /// [Format] as HTML. 6 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 7 | pub struct HTML; 8 | impl Format for HTML { 9 | fn escape(f: &mut Formatter, mut s: &str) -> Result<(), Error> { 10 | let badstuff = "<>&\"'/"; 11 | while let Some(idx) = s.find(|c| badstuff.contains(c)) { 12 | let (first, rest) = s.split_at(idx); 13 | let (badchar, tail) = rest.split_at(1); 14 | f.write_str(first)?; 15 | f.write_str(match badchar { 16 | "<" => "<", 17 | ">" => ">", 18 | "&" => "&", 19 | "\"" => """, 20 | "'" => "'", 21 | "/" => "/", 22 | _ => unreachable!(), 23 | })?; 24 | s = tail; 25 | } 26 | f.write_str(s) 27 | } 28 | /// The MIME type for HTML is [mime::TEXT_HTML_UTF_8]. 29 | fn mime() -> mime::Mime { 30 | return mime::TEXT_HTML_UTF_8; 31 | } 32 | fn this_format() -> Self { 33 | HTML 34 | } 35 | } 36 | 37 | macro_rules! display_as_from_display { 38 | ($format:ty, $type:ty) => { 39 | impl DisplayAs<$format> for $type { 40 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 41 | (&self as &dyn Display).fmt(f) 42 | } 43 | } 44 | }; 45 | } 46 | 47 | /// Conveniently implement [DisplayAs] for integers for a new [Format]. 48 | #[macro_export] 49 | macro_rules! display_integers_as { 50 | ($format:ty) => { 51 | display_as_from_display!($format, i8); 52 | display_as_from_display!($format, u8); 53 | display_as_from_display!($format, i16); 54 | display_as_from_display!($format, u16); 55 | display_as_from_display!($format, i32); 56 | display_as_from_display!($format, u32); 57 | display_as_from_display!($format, i64); 58 | display_as_from_display!($format, u64); 59 | display_as_from_display!($format, i128); 60 | display_as_from_display!($format, u128); 61 | display_as_from_display!($format, isize); 62 | display_as_from_display!($format, usize); 63 | }; 64 | } 65 | 66 | display_integers_as!(HTML); 67 | 68 | /// Inconveniently implement [DisplayAs] for floats for a new [Format]. 69 | /// 70 | /// This is inconvenient because we want to enable pretty formatting 71 | /// of both large and small numbers in whatever markup language we are 72 | /// using. The first argument of the macro is the format that wants 73 | /// implementation of [DisplayAs] for floats. 74 | /// 75 | /// For partial documentation of the other files, see 76 | /// [Floating::fmt_with](float/enum.Floating.html#method.fmt_with). 77 | /// However, I think some examples for HTML will most easily define 78 | /// the other arguments. 79 | /// ``` 80 | /// #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 81 | /// struct HTML; 82 | /// use display_as::{Format, format_as}; 83 | /// impl Format for HTML { 84 | /// fn escape(f: &mut ::std::fmt::Formatter, mut s: &str) -> Result<(), ::std::fmt::Error> { 85 | /// f.write_str(s) // for example I skip escaping... 86 | /// } 87 | /// fn mime() -> mime::Mime { return mime::TEXT_HTML_UTF_8; } 88 | /// fn this_format() -> Self { HTML } 89 | /// } 90 | /// display_as::display_floats_as!(HTML, "×10", "", 3, Some("10")); 91 | /// fn main() { 92 | /// assert_eq!(&format_as!(HTML, 1e3).into_string(), "1000"); 93 | /// assert_eq!(&format_as!(HTML, 3e4).into_string(), "30000"); 94 | /// assert_eq!(&format_as!(HTML, 1e5).into_string(), "105"); 95 | /// assert_eq!(&format_as!(HTML, 2e5).into_string(), "2×105"); 96 | /// assert_eq!(&format_as!(HTML, 1e6).into_string(), "106"); 97 | /// } 98 | /// ``` 99 | #[macro_export] 100 | macro_rules! display_floats_as { 101 | ($format:ty, $e:expr, $after_e:expr, $e_cost:expr, $power_ten:expr) => { 102 | impl $crate::DisplayAs<$format> for f64 { 103 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 104 | $crate::float::Floating::from(*self).fmt_with(f, $e, $after_e, $e_cost, $power_ten) 105 | } 106 | } 107 | impl $crate::DisplayAs<$format> for f32 { 108 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 109 | $crate::float::Floating::from(*self).fmt_with(f, $e, $after_e, $e_cost, $power_ten) 110 | } 111 | } 112 | }; 113 | } 114 | display_floats_as!(HTML, "×10", "", 3, Some("10")); 115 | 116 | #[test] 117 | fn escaping() { 118 | assert_eq!(&format_as!(HTML, ("&")).into_string(), "&"); 119 | assert_eq!( 120 | &format_as!(HTML, ("hello &>this is cool")).into_string(), 121 | "hello &>this is cool" 122 | ); 123 | assert_eq!( 124 | &format_as!(HTML, ("hello &>this is 'cool")).into_string(), 125 | "hello &>this is 'cool" 126 | ); 127 | } 128 | #[test] 129 | fn floats() { 130 | assert_eq!(&format_as!(HTML, 3.0).into_string(), "3"); 131 | assert_eq!(&format_as!(HTML, 3e5).into_string(), "3×105"); 132 | assert_eq!(&format_as!(HTML, 1e-6).into_string(), "10-6"); 133 | assert_eq!(&format_as!(HTML, 3e4).into_string(), "30000"); 134 | } 135 | -------------------------------------------------------------------------------- /display-as/src/float.rs: -------------------------------------------------------------------------------- 1 | //! Internal helper code required for [display_floats_as]. 2 | //! 3 | //! The standard library does nice exact conversions to decimal, but 4 | //! lacks a nice output format, so this module helps to do that. **Do 5 | //! not use this code direcly, but instead call [display_floats_as]!** 6 | 7 | use std::fmt::{Display, Error, Formatter}; 8 | use std::str::FromStr; 9 | 10 | /// This represents an f32 or f64 that has been converted to a string, 11 | /// but which we have not yet decided for certain how to represent 12 | /// (e.g. how many digits to show, or whether to use `e` or `E` 13 | /// notation). 14 | #[derive(Eq, PartialEq, Debug)] 15 | pub enum Floating { 16 | /// A normal floating point number 17 | Normal { 18 | /// The exponent 19 | exponent: i16, 20 | /// The mantissa, without any decimal point 21 | mantissa: String, 22 | /// Is it negative? 23 | is_negative: bool, 24 | }, 25 | /// This is a NaN or an infinity 26 | Abnormal(String), 27 | } 28 | 29 | impl From for Floating { 30 | fn from(x: f64) -> Self { 31 | if !x.is_normal() { 32 | return Floating::Abnormal(format!("{}", x)); 33 | } 34 | let is_negative = x < 0.; 35 | let x = if is_negative { -x } else { x }; 36 | let x = format!("{:e}", x); 37 | let mut parts = x.splitn(2, "e"); 38 | if let Some(mantissa) = parts.next() { 39 | let mut mantissa = mantissa.to_string(); 40 | if mantissa.len() > 1 { 41 | mantissa.remove(1); 42 | } 43 | let exponent = i16::from_str(parts.next().expect("float repr should have exponent")) 44 | .expect("exponent should be integer"); 45 | Floating::Normal { 46 | exponent, 47 | mantissa, 48 | is_negative, 49 | } 50 | } else { 51 | panic!("I think thi sis impossible..."); 52 | } 53 | } 54 | } 55 | impl From for Floating { 56 | fn from(x: f32) -> Self { 57 | if !x.is_normal() { 58 | return Floating::Abnormal(format!("{}", x)); 59 | } 60 | let is_negative = x < 0.; 61 | let x = if is_negative { -x } else { x }; 62 | let x = format!("{:e}", x); 63 | let mut parts = x.splitn(2, "e"); 64 | if let Some(mantissa) = parts.next() { 65 | let mut mantissa = mantissa.to_string(); 66 | if mantissa.len() > 1 { 67 | mantissa.remove(1); 68 | } 69 | let exponent = i16::from_str(parts.next().expect("float repr should have exponent")) 70 | .expect("exponent should be integer"); 71 | Floating::Normal { 72 | exponent, 73 | mantissa, 74 | is_negative, 75 | } 76 | } else { 77 | panic!("I think thi sis impossible..."); 78 | } 79 | } 80 | } 81 | 82 | #[test] 83 | fn to_floating() { 84 | assert_eq!( 85 | Floating::from(1.0), 86 | Floating::Normal { 87 | exponent: 0, 88 | mantissa: "1".to_string(), 89 | is_negative: false 90 | } 91 | ); 92 | assert_eq!( 93 | Floating::from(1.2e10), 94 | Floating::Normal { 95 | exponent: 10, 96 | mantissa: "12".to_string(), 97 | is_negative: false 98 | } 99 | ); 100 | } 101 | 102 | impl Floating { 103 | /// Format this floating point number nicely, using `e` and 104 | /// `after_e` to delimit the exponent in case we decide to format 105 | /// it using scientific notation. `e_waste` is the number 106 | /// of characters we consider wasted when using scientific 107 | /// notation. 108 | pub fn fmt_with( 109 | &self, 110 | f: &mut Formatter, 111 | e: &str, 112 | after_e: &str, 113 | e_waste: usize, 114 | power_ten: Option<&str>, 115 | ) -> Result<(), Error> { 116 | match self { 117 | Floating::Abnormal(s) => f.write_str(&s), 118 | Floating::Normal { 119 | exponent, 120 | mantissa, 121 | is_negative, 122 | } => { 123 | let e_waste = e_waste as i16; 124 | if *is_negative { 125 | f.write_str("-")?; 126 | } 127 | if *exponent > 1 + e_waste || *exponent < -2 - e_waste { 128 | if mantissa.len() > 1 { 129 | let (a, r) = mantissa.split_at(1); 130 | f.write_str(a)?; 131 | f.write_str(".")?; 132 | f.write_str(r)?; 133 | f.write_str(e)?; 134 | exponent.fmt(f)?; 135 | f.write_str(after_e) 136 | } else if mantissa == "1" && power_ten.is_some() { 137 | // We can omit the mantissa, keeping things 138 | // pretty and compact. 139 | f.write_str(power_ten.unwrap())?; 140 | exponent.fmt(f)?; 141 | f.write_str(after_e) 142 | } else { 143 | f.write_str(mantissa)?; 144 | f.write_str(e)?; 145 | exponent.fmt(f)?; 146 | f.write_str(after_e) 147 | } 148 | } else { 149 | if *exponent + 1 > mantissa.len() as i16 { 150 | f.write_str(mantissa)?; 151 | for _ in 0..*exponent as usize + 1 - mantissa.len() { 152 | f.write_str("0")?; 153 | } 154 | Ok(()) 155 | } else if *exponent < 0 { 156 | f.write_str("0.")?; 157 | for _ in 0..-exponent - 1 { 158 | f.write_str("0")?; 159 | } 160 | f.write_str(&mantissa) 161 | } else if *exponent + 1 == mantissa.len() as i16 { 162 | f.write_str(mantissa) 163 | } else { 164 | let (a, b) = mantissa.split_at(*exponent as usize + 1); 165 | f.write_str(a)?; 166 | f.write_str(".")?; 167 | f.write_str(b) 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | impl Display for Floating { 176 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 177 | self.fmt_with(f, "e", "", 1, None) 178 | } 179 | } 180 | 181 | #[test] 182 | fn display() { 183 | assert_eq!(&format!("{}", Floating::from(1.0)), "1"); 184 | assert_eq!(&format!("{}", Floating::from(0.1)), "0.1"); 185 | assert_eq!(&format!("{}", Floating::from(1e-10)), "1e-10"); 186 | assert_eq!(&format!("{}", Floating::from(1.2e-10)), "1.2e-10"); 187 | assert_eq!(&format!("{}", Floating::from(120.)), "120"); 188 | assert_eq!(&format!("{}", Floating::from(123.)), "123"); 189 | assert_eq!(&format!("{}", Floating::from(123.4)), "123.4"); 190 | assert_eq!(&format!("{}", Floating::from(1.2e6)), "1.2e6"); 191 | assert_eq!(&format!("{}", Floating::from(0.001)), "0.001"); 192 | assert_eq!(&format!("{}", Floating::from(0.0001)), "1e-4"); 193 | assert_eq!(&format!("{}", Floating::from(0.001234)), "0.001234"); 194 | } 195 | -------------------------------------------------------------------------------- /display-as/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! This template crate uses and defines a [`DisplayAs`] trait, which 3 | //! allows a type to be displayed in a particular format. 4 | //! 5 | //! # Overview 6 | //! 7 | //! This crate defines three things that you need be aware of in order 8 | //! to use it: the [`Format`] trait, which defines a markup language or 9 | //! other format, the [`DisplayAs`] trait which is implemented for any 10 | //! type that can be converted into some [`Format`], and finally the 11 | //! template language and macros which allow you to conveniently 12 | //! implement [`DisplayAs`] for your own types. I will describe each of 13 | //! these concepts in order. (**FIXME** I should also have a 14 | //! quick-start...) 15 | //! 16 | //! ## [`Format`] 17 | //! 18 | //! There are a number of predefined Formats (and I can easily add 19 | //! more if there are user requests), so the focus here will be on 20 | //! using these Formats, rather than on defining your own (which also 21 | //! isn't too hard). A format is a zero-size type that has a rule for 22 | //! escaping strings and an associated MIME type. The builtin formats 23 | //! include [`HTML`], [`LaTeX`], and [`Math`] (which is math-mode LaTeX). 24 | //! 25 | //! ## [`DisplayAs`]`` 26 | //! 27 | //! The `[`DisplayAs`]` trait is entirely analogous to the [Display](std::fmt::Display) trait 28 | //! in the standard library, except that it is parametrized by a 29 | //! [`Format`] so you can have different representations for the same 30 | //! type in different formats. This also makes it harder to 31 | //! accidentally include the wrong representation in your output. 32 | //! 33 | //! Most of the primitive types already have [`DisplayAs`] implemented 34 | //! for the included Formats. If you encounter a type that you wish 35 | //! had [`DisplayAs`] implemented for a given format, just let me know. 36 | //! You can manually implement [`DisplayAs`] for any of your own types 37 | //! (it's not worse than implementing [Display](std::fmt::Display)) but that isn't how 38 | //! you are intended to do things (except perhaps in very simple 39 | //! cases, like a wrapper around an integer). Instead you will want 40 | //! to use a template to implement [`DisplayAs`] for your own types. 41 | //! 42 | //! ## Templates! 43 | //! 44 | //! There are two template macros that you can use. If you just want 45 | //! to get a string out of one or more [`DisplayAs`] objects, you will 46 | //! use something like `format_as!(HTML, "hello world" value).into_string()`. If 47 | //! you want to implement [`DisplayAs`], you will use the attribute 48 | //! [`with_template!`]. In these examples I will use 49 | //! [`format_as!`] because that makes it easy to write testable 50 | //! documentation. But in practice you will most likely primarily use 51 | //! the [with_template] attribute. 52 | //! 53 | //! ### String literals 54 | //! 55 | //! The first thing you can include in a template is a string literal, 56 | //! which is treated literally. 57 | //! 58 | //! ``` 59 | //! use display_as::{HTML, format_as}; 60 | //! assert_eq!(&format_as!(HTML, "Treat this literally <" ).into_string(), 61 | //! "Treat this literally <"); 62 | //! ``` 63 | //! 64 | //! ### Expressions 65 | //! 66 | //! String literals are essential to representing some other [`Format`]. 67 | //! To include your data in the output, you can include any expression 68 | //! that yields a type with [`DisplayAs`]`` where `F` is your [`Format`]. 69 | //! Each expression is delimited by string literals (or the other 70 | //! options below). Note that since an expression is 71 | //! 72 | //! ``` 73 | //! use display_as::{HTML, format_as}; 74 | //! let s = "This is not a literal: <"; 75 | //! assert_eq!(&format_as!(HTML, s ).into_string(), 76 | //! "This is not a literal: <"); 77 | //! ``` 78 | //! 79 | //! ### Blocks and conditionals 80 | //! 81 | //! You can use braces to enclose any template expression. Any rust 82 | //! code before the braces is treated as literal rust. This enables 83 | //! you to write conditionals, match expressions, and loops. 84 | //! 85 | //! ``` 86 | //! use display_as::{HTML, format_as}; 87 | //! assert_eq!(&format_as!(HTML, 88 | //! for i in 1..4 { 89 | //! "Counting " i "...\n" 90 | //! } 91 | //! "Blast off!").into_string(), 92 | //! "Counting 1...\nCounting 2...\nCounting 3...\nBlast off!"); 93 | //! ``` 94 | //! 95 | //! ### Semicolons 96 | //! 97 | //! You may also play any rust statements you wish, if you end them 98 | //! with a semicolon. This enables you to define local variables. 99 | //! 100 | //! ``` 101 | //! use display_as::{HTML, format_as}; 102 | //! assert_eq!(&format_as!(HTML, "I am counting " let count = 5; 103 | //! count " and again " count ).into_string(), 104 | //! "I am counting 5 and again 5"); 105 | //! ``` 106 | //! 107 | //! ### Embedding a different format 108 | //! 109 | //! You can also embed in one format a representation from another 110 | //! type. This can be helpful, for instance, if you want to use 111 | //! MathJax to handle LaTeX math embedded in an HTML file. 112 | //! 113 | //! ``` 114 | //! use display_as::{HTML, Math, format_as}; 115 | //! assert_eq!(&format_as!(HTML, "The number $" 1.2e12 as Math "$").into_string(), 116 | //! r"The number $1.2\times10^{12}$"); 117 | //! ``` 118 | //! 119 | //! ### Saving a portion of a template for reuse 120 | //! 121 | //! You can also save a template expression using a let statement, 122 | //! provided the template expression is enclosed in braces. This 123 | //! allows you to achieve goals similar to the base templates in 124 | //! Jinja2. (Once we have an include feature... Example to come in 125 | //! the future.) 126 | //! 127 | //! ``` 128 | //! use display_as::{HTML, format_as}; 129 | //! assert_eq!(&format_as!(HTML, 130 | //! let x = 1; 131 | //! let announce = { "number " x }; 132 | //! "The " announce " is silly " announce).into_string(), 133 | //! "The number 1 is silly number 1"); 134 | //! ``` 135 | //! 136 | //! ## Differences when putting a template in a file 137 | //! 138 | //! You will most likely always put largish templates in a separate 139 | //! file. This makes editing your template simpler and keeps things 140 | //! in general easier. The template language for templates held in a 141 | //! distinct file has one difference from those shown above: the file 142 | //! always begins and ends with string literals, but their initial and 143 | //! final quotes respectively are omitted. Furthermore, the first and 144 | //! last string literals must be "raw" literals with a number of # 145 | //! signs equal to the maximum used in the template. I suggest using 146 | //! an equal number of # signs for all string literals in a given 147 | //! template. Thus a template might look like: 148 | //! 149 | //! ```ignore 150 | //! 151 | //! 152 | //! "## self.title r##" 153 | //! 154 | //! 155 | //! ``` 156 | 157 | //! You can see that the quotes appear "inside out." This is 158 | //! intentional, so that for most formats the quotes will appear to 159 | //! enclose the rust code rather than everything else, and as a result 160 | //! editors will hopefully be able to do the "right thing" for the 161 | //! template format (e.g. HTML in this case). 162 | 163 | //! ## Using `include!("...")` within a template 164 | //! 165 | //! Now I will demonstrate how you can include template files within 166 | //! other template files by using the `include!` macro within a 167 | //! template. To demonstrate this, we will need a few template files. 168 | //! 169 | //! We will begin with a "base" template that describes how a page is 170 | //! laid out. 171 | //! #### `base.html`: 172 | //! ```ignore 173 | #![doc = include_str!("base.html")] 174 | //! ``` 175 | //! We can have a template for how we will display students... 176 | //! #### `student.html`: 177 | //! ```ignore 178 | #![doc = include_str!("student.html")] 179 | //!``` 180 | //! Finally, an actual web page describing a class! 181 | //! #### `class.html`: 182 | //! ```ignore 183 | #![doc = include_str!("class.html")] 184 | //! ``` 185 | //! Now to put all this together, we'll need some rust code. 186 | //! 187 | //! ``` 188 | //! use display_as::{DisplayAs, HTML, format_as, with_template}; 189 | //! struct Student { name: &'static str }; 190 | //! #[with_template("student.html")] 191 | //! impl DisplayAs for Student {} 192 | //! 193 | //! struct Class { coursename: &'static str, coursenumber: usize, students: Vec }; 194 | //! #[with_template("class.html")] 195 | //! impl DisplayAs for Class {} 196 | //! 197 | //! let myclass = Class { 198 | //! coursename: "Templates", 199 | //! coursenumber: 365, 200 | //! students: vec![Student {name: "David"}, Student {name: "Joel"}], 201 | //! }; 202 | //! assert_eq!(&format_as!(HTML, myclass).into_string(), r#"PH365: Templates 203 | //! 204 | //!
  • Name: David 205 | //! 206 | //!
  • Name: Joel 207 | //! 208 | //!
209 | //! 210 | //! 211 | //! 212 | //! "#); 213 | //! ``` 214 | 215 | extern crate display_as_proc_macro; 216 | extern crate mime; 217 | extern crate self as display_as; 218 | 219 | /// Use the given template to create a [`FormattedString`]. 220 | /// 221 | /// You can think of this as being kind of like [`format!`] on strange drugs. 222 | /// We return a [`FormattedString`] instaed of a [String] so that 223 | /// You can store the output and use it later in another template without 224 | /// having the contents escaped. 225 | /// 226 | /// To obtain a [`String`], use the [`FormattedString::into_string`] method. 227 | pub use display_as_proc_macro::format_as; 228 | 229 | /// Write the given template to a file. 230 | /// 231 | /// You can think of this as being kind of like [`write!`] on strange drugs. 232 | pub use display_as_proc_macro::write_as; 233 | 234 | /// Can I write doc here? 235 | pub use display_as_proc_macro::with_template; 236 | 237 | use std::fmt::{Display, Error, Formatter}; 238 | 239 | #[macro_use] 240 | mod html; 241 | mod latex; 242 | mod mathlatex; 243 | mod rust; 244 | mod url; 245 | mod utf8; 246 | 247 | pub mod float; 248 | 249 | pub use crate::html::HTML; 250 | pub use crate::latex::LaTeX; 251 | pub use crate::mathlatex::Math; 252 | pub use crate::rust::Rust; 253 | pub use crate::url::URL; 254 | pub use crate::utf8::UTF8; 255 | 256 | /// Format is a format that we can use for displaying data. 257 | pub trait Format: Sync + Send + Copy + Eq + Ord + std::hash::Hash { 258 | /// "Escape" the given string so it can be safely displayed in 259 | /// this format. The precise meaning of this may vary from format 260 | /// to format, but the general sense is that this string does not 261 | /// have any internal formatting, and must be displayed 262 | /// appropriately. 263 | fn escape(f: &mut Formatter, s: &str) -> Result<(), Error>; 264 | /// The mime type of this format. 265 | fn mime() -> mime::Mime; 266 | /// Return an actual [`Format`] for use in [`As`] below. 267 | fn this_format() -> Self; 268 | } 269 | 270 | /// This trait is analogous to [Display](std::fmt::Display), but will display the data in 271 | /// `F` [`Format`]. 272 | pub trait DisplayAs { 273 | /// Formats the value using the given formatter. 274 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error>; 275 | 276 | /// Estimate the size of this when displayed 277 | fn estimate_size(&self) -> usize { 278 | 4 279 | } 280 | 281 | /// Creates a display object 282 | fn display<'a>(&'a self) -> As<'a, F, Self> { 283 | As::from(self) 284 | } 285 | } 286 | 287 | /// Create a Display object, which can be used with various web frameworks. 288 | pub fn display<'a, F: Format, T: DisplayAs>(_f: F, x: &'a T) -> As<'a, F, T> { 289 | x.display() 290 | } 291 | 292 | struct Closure Result<(), Error>> { 293 | f: C, 294 | _format: F, 295 | } 296 | /// Display the given closure as this format. 297 | /// 298 | /// This is used internally in template handling. 299 | pub fn display_closure_as( 300 | f: F, 301 | c: impl Fn(&mut Formatter) -> Result<(), Error>, 302 | ) -> impl DisplayAs { 303 | Closure { f: c, _format: f } 304 | } 305 | impl Result<(), Error>> DisplayAs for Closure { 306 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 307 | (self.f)(f) 308 | } 309 | } 310 | impl PartialEq> for str { 311 | fn eq(&self, other: &FormattedString) -> bool { 312 | self == &other.inner 313 | } 314 | } 315 | impl PartialEq for FormattedString { 316 | fn eq(&self, other: &str) -> bool { 317 | &self.inner == other 318 | } 319 | } 320 | #[test] 321 | fn test_closure() { 322 | let x = |__f: &mut Formatter| -> Result<(), Error> { 323 | __f.write_str("hello world")?; 324 | Ok(()) 325 | }; 326 | assert_eq!( 327 | "hello world", 328 | &format_as!(HTML, display_closure_as(HTML, x)) 329 | ); 330 | assert_eq!( 331 | &format_as!(HTML, display_closure_as(HTML, x)), 332 | "hello world" 333 | ); 334 | } 335 | 336 | /// Choose to [Display](std::fmt::Display) this type using a particular [`Format`] `F`. 337 | pub struct As<'a, F: Format, T: DisplayAs + ?Sized> { 338 | inner: &'a T, 339 | _format: F, 340 | } 341 | impl<'a, F: Format, T: DisplayAs + ?Sized> From<&'a T> for As<'a, F, T> { 342 | fn from(value: &'a T) -> Self { 343 | As { 344 | _format: F::this_format(), 345 | inner: value, 346 | } 347 | } 348 | } 349 | impl<'a, F: Format, T: DisplayAs + ?Sized> Display for As<'a, F, T> { 350 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 351 | self.inner.fmt(f) 352 | } 353 | } 354 | 355 | /// The `rouille` feature flag enables conversion of any `As` 356 | /// type into a [rouille::Response]. Note that it is necessary to be 357 | /// explicit about the format because a given type `T` may be 358 | /// displayed in multiple different formats. 359 | #[cfg(feature = "rouille")] 360 | pub mod rouille { 361 | extern crate rouille; 362 | use super::{As, DisplayAs, Format}; 363 | impl<'a, F: Format, T: DisplayAs> Into for As<'a, F, T> { 364 | fn into(self) -> rouille::Response { 365 | let s = format!("{}", &self); 366 | rouille::Response::from_data(F::mime().as_ref().to_string(), s) 367 | } 368 | } 369 | } 370 | 371 | /// The `actix-web` feature flag makes any [`As`] type a 372 | /// [actix_web::Responder]. 373 | #[cfg(feature = "actix-web")] 374 | pub mod actix { 375 | extern crate actix_web; 376 | use self::actix_web::{HttpRequest, HttpResponse, Responder}; 377 | use super::{As, DisplayAs, Format}; 378 | impl<'a, F: Format, T: 'a + DisplayAs> Responder for As<'a, F, T> { 379 | type Item = HttpResponse; 380 | type Error = ::std::io::Error; 381 | fn respond_to( 382 | self, 383 | _req: &HttpRequest, 384 | ) -> Result { 385 | Ok(HttpResponse::Ok() 386 | .content_type(F::mime().as_ref().to_string()) 387 | .body(format!("{}", &self))) 388 | } 389 | } 390 | } 391 | 392 | /// The `gotham-web` feature flag makes any [`As`] type a 393 | /// [::gotham::handler::IntoResponse]. 394 | #[cfg(feature = "gotham")] 395 | pub mod gotham { 396 | use crate::{As, DisplayAs, Format}; 397 | use gotham::{ 398 | handler::IntoResponse, 399 | hyper::{Body, Response, StatusCode}, 400 | state::State, 401 | }; 402 | 403 | impl<'a, F: Format, T: 'a + DisplayAs> IntoResponse for As<'a, F, T> { 404 | fn into_response(self, state: &State) -> Response { 405 | let s = format!("{}", self); 406 | (StatusCode::OK, F::mime(), s).into_response(state) 407 | } 408 | } 409 | } 410 | 411 | /// The `warp` feature flag makes any [`DisplayAs`] type a [warp::Reply]. 412 | #[cfg(feature = "warp")] 413 | pub mod warp { 414 | use crate::{As, DisplayAs, Format}; 415 | impl<'a, F: Format, T: DisplayAs + Sync> warp::Reply for As<'a, F, T> { 416 | /// Convert into a [warp::Reply]. 417 | fn into_response(self) -> warp::reply::Response { 418 | let s = format!("{}", self); 419 | let m = F::mime().as_ref().to_string(); 420 | warp::http::Response::builder() 421 | .header("Content-type", m.as_bytes()) 422 | .status(warp::http::StatusCode::OK) 423 | .body(s) 424 | .unwrap() 425 | .map(warp::hyper::Body::from) 426 | } 427 | } 428 | 429 | #[test] 430 | fn test_warp() { 431 | use crate::{display, HTML}; 432 | use warp::Reply; 433 | // This sloppy test just verify that the code runs. 434 | display(HTML, &"hello world".to_string()).into_response(); 435 | } 436 | } 437 | 438 | impl DisplayAs for String { 439 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 440 | F::escape(f, self) 441 | } 442 | } 443 | impl<'a, F: Format> DisplayAs for &'a String { 444 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 445 | F::escape(f, self) 446 | } 447 | } 448 | impl DisplayAs for str { 449 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 450 | F::escape(f, self) 451 | } 452 | } 453 | impl<'a, F: Format> DisplayAs for &'a str { 454 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 455 | F::escape(f, self) 456 | } 457 | } 458 | 459 | #[cfg(test)] 460 | mod tests { 461 | use super::{format_as, HTML}; 462 | #[test] 463 | fn html_escaping() { 464 | assert_eq!(&format_as!(HTML, ("&")).into_string(), "&"); 465 | assert_eq!( 466 | &format_as!(HTML, ("hello &>this is cool")).into_string(), 467 | "hello &>this is cool" 468 | ); 469 | assert_eq!( 470 | &format_as!(HTML, ("hello &>this is 'cool")).into_string(), 471 | "hello &>this is 'cool" 472 | ); 473 | } 474 | } 475 | 476 | #[cfg(feature = "serde1")] 477 | use serde::{Deserialize, Serialize}; 478 | 479 | /// A `String` that is formatted in `F` 480 | /// 481 | /// The `serde1`` feature flag enables a [`FormattedString`] to be 482 | /// serialized and deserialized by [serde]. 483 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] 484 | #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] 485 | #[cfg_attr(feature = "serde1", serde(transparent))] 486 | #[cfg_attr(feature = "serde1", serde(bound(deserialize = "F: Format")))] 487 | pub struct FormattedString { 488 | inner: String, 489 | #[cfg_attr(feature = "serde1", serde(skip, default = "F::this_format"))] 490 | _format: F, 491 | } 492 | 493 | impl FormattedString { 494 | /// Create a new `FormattedString` from an already-formatted `String`. 495 | pub fn from_formatted>(s: S) -> Self { 496 | FormattedString { 497 | inner: s.into(), 498 | _format: F::this_format(), 499 | } 500 | } 501 | /// Convert back into a string 502 | pub fn into_string(self) -> String { 503 | self.inner 504 | } 505 | /// Reference a `&str` from this 506 | pub fn as_str(&self) -> &str { 507 | &self.inner 508 | } 509 | } 510 | 511 | impl DisplayAs for FormattedString { 512 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 513 | f.write_str(&self.inner) 514 | } 515 | } 516 | impl std::fmt::Debug for FormattedString { 517 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 518 | write!(f, "{:?}", self.inner) 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /display-as-proc-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This is the implementation crate for `display-as-template`. 2 | 3 | extern crate proc_macro; 4 | // extern crate syn; 5 | #[macro_use] 6 | extern crate quote; 7 | extern crate glob; 8 | extern crate proc_macro2; 9 | 10 | use proc_macro::{Delimiter, Group, TokenStream, TokenTree}; 11 | use std::fmt::Write; 12 | use std::fs::File; 13 | use std::io::Read; 14 | use std::path::{Path, PathBuf}; 15 | 16 | fn find_template_file(path: &str) -> PathBuf { 17 | let sourcedirs: std::collections::HashSet<_> = glob::glob("**/*.rs").unwrap() 18 | .flat_map(|x| x.ok()) 19 | .filter(|x| !x.starts_with("target/")) 20 | .map(|x| PathBuf::from(x.clone().parent().unwrap())) 21 | .collect(); 22 | let paths: Vec<_> = sourcedirs.into_iter() 23 | .filter(|d| d.join(path).exists()) 24 | .collect(); 25 | if paths.len() == 0 { 26 | panic!("No template file named {:?} exists.", path); 27 | } else if paths.len() > 1 { 28 | panic!(r"Multiple files named {:?} exist. Eventually display-as will 29 | support this, but for now each template file must have a unique name.", path); 30 | } 31 | paths.into_iter().next().unwrap() 32 | } 33 | 34 | fn proc_to_two(i: TokenStream) -> proc_macro2::TokenStream { 35 | i.into() 36 | } 37 | fn two_to_proc(i: proc_macro2::TokenStream) -> TokenStream { 38 | i.into() 39 | } 40 | 41 | fn is_str(x: &TokenTree) -> bool { 42 | match x { 43 | TokenTree::Literal(_) => { 44 | let s = x.to_string(); 45 | s.len() > 0 && s.contains("\"") && s.chars().next() != Some('b') 46 | } 47 | _ => false, 48 | } 49 | } 50 | 51 | fn to_tokens(s: &str) -> impl Iterator { 52 | let ts: TokenStream = s.parse().unwrap(); 53 | ts.into_iter() 54 | } 55 | 56 | fn count_pounds(x: &str) -> &'static str { 57 | for pounds in &["#######", "######", "#####", "####", "###", "##", "#", ""] { 58 | if x.contains(pounds) { 59 | return pounds; 60 | } 61 | } 62 | "" 63 | } 64 | 65 | /// Use the given template to create a string. 66 | /// 67 | /// You can think of this as being kind of like `format!` on strange drugs. 68 | #[proc_macro] 69 | pub fn format_as(input: TokenStream) -> TokenStream { 70 | let mut tokens = input.into_iter(); 71 | let format = if let Some(format) = tokens.next() { 72 | proc_to_two(format.into()) 73 | } else { 74 | panic!("format_as! needs a Format as its first argument") 75 | }; 76 | if let Some(comma) = tokens.next() { 77 | if &comma.to_string() != "," { 78 | panic!( 79 | "format_as! needs a Format followed by a comma, not {}", 80 | comma.to_string() 81 | ); 82 | } 83 | } else { 84 | panic!("format_as! needs a Format followed by a comma"); 85 | } 86 | 87 | let statements = proc_to_two(template_to_statements( 88 | "templates".as_ref(), 89 | &format, 90 | tokens.collect(), "", "" 91 | )); 92 | 93 | quote!( 94 | { 95 | use std::fmt::Write; 96 | use display_as::DisplayAs; 97 | let doit = || -> Result { 98 | let mut __f = String::with_capacity(32); 99 | #statements 100 | Ok(__f) 101 | }; 102 | display_as::FormattedString::<#format>::from_formatted(doit().expect("trouble writing to String??!")) 103 | } 104 | ) 105 | .into() 106 | } 107 | 108 | /// Write the given template to a file. 109 | /// 110 | /// You can think of this as being kind of like `write!` on strange drugs. 111 | #[proc_macro] 112 | pub fn write_as(input: TokenStream) -> TokenStream { 113 | let mut tokens = input.into_iter(); 114 | let format = if let Some(format) = tokens.next() { 115 | proc_to_two(format.into()) 116 | } else { 117 | panic!("write_as! needs a Format as its first argument") 118 | }; 119 | if let Some(comma) = tokens.next() { 120 | if &comma.to_string() != "," { 121 | panic!( 122 | "write_as! needs a Format followed by a comma, not {}", 123 | comma.to_string() 124 | ); 125 | } 126 | } else { 127 | panic!("write_as! needs a Format followed by a comma"); 128 | } 129 | 130 | let mut writer: Vec = Vec::new(); 131 | while let Some(tok) = tokens.next() { 132 | if &tok.to_string() == "," { 133 | break; 134 | } else { 135 | writer.push(tok); 136 | } 137 | } 138 | if writer.len() == 0 { 139 | panic!("write_as! needs a Writer as its second argument followed by comma.") 140 | } 141 | let writer = proc_to_two(writer.into_iter().collect()); 142 | 143 | let statements = proc_to_two(template_to_statements( 144 | "templates".as_ref(), 145 | &format, 146 | tokens.collect(), "", "" 147 | )); 148 | 149 | quote!( 150 | { 151 | use std::fmt::Write; 152 | use display_as::DisplayAs; 153 | let __f = &mut #writer; 154 | let mut doit = || -> Result<(), std::fmt::Error> { 155 | #statements 156 | Ok(()) 157 | }; 158 | doit() 159 | } 160 | ) 161 | .into() 162 | } 163 | 164 | fn expr_toks_to_stmt( 165 | format: &proc_macro2::TokenStream, 166 | expr: &mut Vec, 167 | ) -> impl Iterator { 168 | let len = expr.len(); 169 | 170 | let to_display_as = { 171 | // We generate a unique method name to avoid a bug that happens if 172 | // there are nested calls to format_as!. The ToDisplayAs type below 173 | // is my hokey approach to use deref coersion (which happens on method 174 | // calls) ensure that either references to DisplayAs types or the types 175 | // themselves can be used. 176 | use rand::distributions::Alphanumeric; 177 | use rand::{thread_rng, Rng}; 178 | use std::iter; 179 | 180 | let mut rng = thread_rng(); 181 | let rand_chars: String = iter::repeat(()) 182 | .map(|()| rng.sample(Alphanumeric)) 183 | .map(char::from) 184 | .take(13) 185 | .collect(); 186 | proc_macro2::Ident::new( 187 | &format!("ToDisplayAs{}xxx{}", format, rand_chars), 188 | proc_macro2::Span::call_site(), 189 | ) 190 | }; 191 | if len > 2 && expr[len - 2].to_string() == "as" { 192 | let format = proc_to_two(expr.pop().unwrap().into()); 193 | expr.pop(); 194 | let expr = proc_to_two(expr.drain(..).collect()); 195 | two_to_proc(quote! { 196 | { 197 | trait ToDisplayAs { 198 | fn #to_display_as(&self) -> &Self; 199 | } 200 | impl> ToDisplayAs for T { 201 | fn #to_display_as(&self) -> &Self { self } 202 | } 203 | __f.write_fmt(format_args!("{}", <_ as DisplayAs<#format>>::display((#expr).#to_display_as())))?; 204 | } 205 | }) 206 | .into_iter() 207 | } else if expr.len() > 0 { 208 | let expr = proc_to_two(expr.drain(..).collect()); 209 | let format = format.clone(); 210 | two_to_proc(quote! { 211 | { 212 | trait ToDisplayAs { 213 | fn #to_display_as(&self) -> &Self; 214 | } 215 | impl> ToDisplayAs for T { 216 | fn #to_display_as(&self) -> &Self { self } 217 | } 218 | __f.write_fmt(format_args!("{}", <_ as DisplayAs<#format>>::display((#expr).#to_display_as())))?; 219 | } 220 | }) 221 | .into_iter() 222 | } else { 223 | two_to_proc(quote! {}).into_iter() 224 | } 225 | } 226 | fn expr_toks_to_conditional(expr: &mut Vec) -> TokenStream { 227 | expr.drain(..).collect() 228 | } 229 | 230 | fn read_template_file(dirname: &Path, pathname: &str, 231 | left_delim: &str, right_delim: &str) -> TokenStream { 232 | let path = dirname.join(&pathname); 233 | if let Ok(mut f) = File::open(&path) { 234 | let mut contents = String::new(); 235 | f.read_to_string(&mut contents) 236 | .expect("something went wrong reading the file"); 237 | let raw_template_len = contents.len(); 238 | let pounds: String = if left_delim == "" { 239 | count_pounds(&contents).to_string() 240 | } else { 241 | let mut pounds = count_pounds(&contents).to_string(); 242 | pounds.write_str("#").unwrap(); 243 | contents = contents.replace(left_delim, &format!(r#""{}"#, pounds)); 244 | contents = contents.replace(right_delim, &format!(r#"r{}""#, pounds)); 245 | pounds 246 | }; 247 | contents.write_str("\"").unwrap(); 248 | contents.write_str(£s).unwrap(); 249 | let mut template = "r".to_string(); 250 | template.write_str(£s).unwrap(); 251 | template.write_str("\"").unwrap(); 252 | template.write_str(&contents).unwrap(); 253 | template 254 | .write_str(" ({ assert_eq!(include_str!(\"") 255 | .unwrap(); 256 | template.write_str(&pathname).unwrap(); 257 | write!(template, "\").len(), {}); \"\"}}); ", raw_template_len).unwrap(); 258 | template.parse().expect("trouble parsing file") 259 | } else { 260 | panic!("No such file: {}", path.display()) 261 | } 262 | } 263 | 264 | fn template_to_statements( 265 | dir: &Path, 266 | format: &proc_macro2::TokenStream, 267 | template: TokenStream, 268 | left_delim: &str, 269 | right_delim: &str) -> TokenStream 270 | { 271 | let mut toks: Vec = Vec::new(); 272 | let mut next_expr: Vec = Vec::new(); 273 | for t in template.into_iter() { 274 | if let TokenTree::Group(g) = t.clone() { 275 | let next_expr_len = next_expr.len(); 276 | if g.delimiter() == Delimiter::Brace { 277 | if next_expr_len > 2 278 | && !next_expr.iter().any(|x| x.to_string() == "=") 279 | && &next_expr[0].to_string() == "if" 280 | && &next_expr[1].to_string() == "let" 281 | { 282 | // We presumably are looking at a destructuring 283 | // pattern. 284 | next_expr.push(t); 285 | } else if next_expr_len > 1 286 | && &next_expr[next_expr_len - 1].to_string() != "=" 287 | && &next_expr[0].to_string() == "let" 288 | { 289 | // We presumably are looking at a destructuring 290 | // pattern. 291 | next_expr.push(t); 292 | } else if next_expr_len > 2 293 | && &next_expr[next_expr_len - 1].to_string() == "=" 294 | && &next_expr[0].to_string() == "let" 295 | { 296 | // We are doing an assignment to a template 297 | // thingy, so let's create a DisplayAs thingy 298 | // rather than adding the stuff right now. 299 | toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter()); 300 | let actions = proc_to_two(template_to_statements(dir, format, g.stream(), 301 | left_delim, right_delim)); 302 | toks.extend( 303 | two_to_proc(quote! { 304 | display_as::display_closure_as(#format, |__f: &mut ::std::fmt::Formatter| 305 | -> Result<(), ::std::fmt::Error> { 306 | { #actions }; 307 | Ok(()) 308 | }) 309 | // |_format: #format, __f: &mut ::std::fmt::Formatter| 310 | // -> Result<(), ::std::fmt::Error> { 311 | // { #actions }; 312 | // Ok(()) 313 | // } 314 | }) 315 | .into_iter(), 316 | ); 317 | } else if next_expr_len > 0 && &next_expr[0].to_string() == "match" { 318 | toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter()); 319 | let mut interior_toks: Vec = Vec::new(); 320 | for x in g.stream() { 321 | if let TokenTree::Group(g) = x.clone() { 322 | if g.delimiter() == Delimiter::Brace { 323 | interior_toks.push(TokenTree::Group(Group::new( 324 | Delimiter::Brace, 325 | template_to_statements(dir, format, g.stream(), 326 | left_delim, right_delim), 327 | ))); 328 | } else { 329 | interior_toks.push(x); 330 | } 331 | } else { 332 | interior_toks.push(x); 333 | } 334 | } 335 | toks.push(TokenTree::Group(Group::new(Delimiter::Brace, 336 | interior_toks.into_iter().collect()))); 337 | } else { 338 | toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter()); 339 | toks.push(TokenTree::Group(Group::new( 340 | Delimiter::Brace, 341 | template_to_statements(dir, format, g.stream(), 342 | left_delim, right_delim), 343 | ))); 344 | } 345 | } else if g.delimiter() == Delimiter::Parenthesis 346 | && next_expr.len() >= 2 347 | && &next_expr[next_expr_len - 1].to_string() == "!" 348 | && &next_expr[next_expr_len - 2].to_string() == "include" 349 | { 350 | next_expr.pop(); 351 | next_expr.pop(); // remove the include! 352 | let filenames: Vec<_> = g.stream().into_iter().collect(); 353 | if filenames.len() != 1 { 354 | panic!( 355 | "include! macro within a template must have one argument, a string literal" 356 | ); 357 | } 358 | let filename = filenames[0].to_string().replace("\"", ""); 359 | let templ = read_template_file(dir, &filename, left_delim, right_delim); 360 | let statements = template_to_statements(dir, format, templ, 361 | left_delim, right_delim); 362 | next_expr.extend(statements.into_iter()); 363 | next_expr.extend(to_tokens(";").into_iter()); 364 | toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter()); 365 | toks.push(t); 366 | } else { 367 | next_expr.push(t); 368 | } 369 | } else if t.to_string() == ";" { 370 | toks.extend(expr_toks_to_conditional(&mut next_expr).into_iter()); 371 | toks.push(t); 372 | } else if is_str(&t) { 373 | // First print the previous expression... 374 | toks.extend(expr_toks_to_stmt(&format, &mut next_expr)); 375 | // Now we print this str... 376 | toks.extend(to_tokens("__f.write_str")); 377 | toks.push(TokenTree::Group(Group::new( 378 | Delimiter::Parenthesis, 379 | TokenStream::from(t), 380 | ))); 381 | toks.extend(to_tokens("?;")); 382 | } else { 383 | next_expr.push(t); 384 | } 385 | } 386 | // Now print the final expression... 387 | toks.extend(expr_toks_to_stmt(&format, &mut next_expr)); 388 | TokenTree::Group(Group::new(Delimiter::Brace, toks.into_iter().collect())).into() 389 | } 390 | 391 | /// Implement `DisplayAs` for a given type. 392 | /// 393 | /// Why not use `derive`? Because we need to be able to specify which 394 | /// format we want to implement, and we might want to also use 395 | /// additional generic bounds. 396 | /// 397 | /// You may use `with_template` in two different ways: inline or with 398 | /// a separate template file. To use an inline template, you provide 399 | /// your template as an argument, as in `#[with_template("Vec(" self.x 400 | /// "," self.y "," self.z ",")]`. The template consists of 401 | /// alternating strings and expressions, although you can also use if 402 | /// statements, for loops, or match expressions, although match 403 | /// expressions must use curly braces on each branch. 404 | /// 405 | /// A template file is specified by giving the path relative to the 406 | /// current source file as a string argument: 407 | /// `#[with_template("filename.html")]`. There are a few hokey 408 | /// restrictions on your filenames. 409 | /// 410 | /// 1. Your filename cannot have an embedded `"` character. 411 | /// 2. Your string specifying the filename cannot be a "raw" string. 412 | /// 3. You cannot use any characters (including a backslash) that need escaping in rust strings. 413 | /// 414 | /// These constraints are very hokey, and may be lifted in the future. 415 | /// File a bug report if you have a good use for lifting these 416 | /// constraints. 417 | /// 418 | /// The file itself will have a template like those above, but without 419 | /// the beginning or ending quotation marks. Furthermore, it is 420 | /// assumed that you are using raw strings, and that you use an equal 421 | /// number of `#` signs throughout. 422 | /// 423 | /// You may also give **three** strings to `with_template`, in which 424 | /// case the first two strings are the left and right delimiters for 425 | /// rust content. This can make your template files a little easier 426 | /// to read. 427 | #[proc_macro_attribute] 428 | pub fn with_template(input: TokenStream, my_impl: TokenStream) -> TokenStream { 429 | let mut sourcedir = PathBuf::from("."); 430 | 431 | let mut impl_toks: Vec<_> = my_impl.into_iter().collect(); 432 | if &impl_toks[0].to_string() != "impl" || impl_toks.len() < 3 { 433 | panic!("with_template can only be applied to an impl of DisplayAs"); 434 | } 435 | let mut my_format: proc_macro2::TokenStream = quote!(); 436 | for i in 0..impl_toks.len() - 2 { 437 | if impl_toks[i].to_string() == "DisplayAs" && impl_toks[i + 1].to_string() == "<" { 438 | my_format = proc_to_two(impl_toks[i + 2].clone().into()); 439 | break; 440 | } 441 | } 442 | let last = impl_toks.pop().unwrap(); 443 | match last.to_string().as_ref() { 444 | "{ }" | "{ }" | "{}" => (), // this is what we expect. 445 | s => panic!( 446 | "with_template must be applied to an impl that ends in '{{}}', not {}", 447 | s 448 | ), 449 | }; 450 | let my_format = my_format; // no longer mut 451 | 452 | let input_vec: Vec<_> = input.clone().into_iter().collect(); 453 | let mut left_delim = "".to_string(); 454 | let mut right_delim = "".to_string(); 455 | let input = if input_vec.len() == 1 { 456 | let pathname = input_vec[0].to_string().replace("\"", ""); 457 | sourcedir = find_template_file(&pathname); 458 | read_template_file(&sourcedir, &pathname, "", "") 459 | } else if input_vec.len() == 3 460 | && input_vec[0].to_string().contains("\"") 461 | && input_vec[1].to_string().contains("\"") 462 | && input_vec[2].to_string().contains("\"") 463 | { 464 | // If we have three string literals, the first two are the 465 | // delimiters we want to use. 466 | let pathname = input_vec[2].to_string().replace("\"", ""); 467 | sourcedir = find_template_file(&pathname); 468 | left_delim = input_vec[0].to_string().replace("\"", ""); 469 | right_delim = input_vec[1].to_string().replace("\"", ""); 470 | read_template_file(&sourcedir, &pathname, &left_delim, &right_delim) 471 | } else { 472 | input 473 | }; 474 | let statements = proc_to_two(template_to_statements(&sourcedir, &my_format, input, 475 | &left_delim, &right_delim)); 476 | 477 | let out = quote! { 478 | { 479 | #statements 480 | Ok(()) 481 | } 482 | }; 483 | let mut new_impl: Vec = Vec::new(); 484 | new_impl.extend(impl_toks.into_iter()); 485 | new_impl.extend( 486 | two_to_proc(quote! { 487 | { 488 | fn fmt(&self, __f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 489 | #out 490 | } 491 | } 492 | }) 493 | .into_iter(), 494 | ); 495 | let new_impl = new_impl.into_iter().collect(); 496 | 497 | // println!("new_impl is {}", &new_impl); 498 | new_impl 499 | } 500 | 501 | /// Like [macro@with_template], but also generate any web responder 502 | /// implementations that are handled via feature flags. 503 | #[proc_macro_attribute] 504 | pub fn with_response_template(input: TokenStream, my_impl: TokenStream) -> TokenStream { 505 | let displayas_impl = with_template(input, my_impl.clone()); 506 | displayas_impl 507 | } 508 | --------------------------------------------------------------------------------