├── 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() { %]| [% col %] | [% } %]
4 | [% } %]
5 |
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#"
"#
6 | for s in self.students.iter() {
7 | "- " s "
"
8 | }
9 |
10 | 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 |
8 | [% for (i,team) in self.teams.iter().enumerate() { %]
9 | -
10 | [% team.name %]: [% team.score %]
11 |
12 | [% } %]
13 |
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 | [](https://docs.rs/display-as)
4 | [](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 |
--------------------------------------------------------------------------------