gchemol_readwrite/template/
jinja.rs

1// [[file:../../gchemol-readwrite.note::9ecb73b4][9ecb73b4]]
2use gut::prelude::*;
3use std::path::{Path, PathBuf};
4
5use minijinja::Environment;
6// 9ecb73b4 ends here
7
8// [[file:../../gchemol-readwrite.note::b4df5d88][b4df5d88]]
9use lazy_regex::*;
10
11static GLOBAL_REX: Lazy<Regex> = lazy_regex!("^ab+$"i);
12
13static FORMAT_SPEC: Lazy<Regex> = lazy_regex!(
14    r"(?x)
15(?P<align>[<>^])?
16(?P<sign>[\+\-])?
17(?P<width>\d+)?
18((?:\.)(?P<precision>\d+))?
19"
20);
21
22static FLOAT_NUM: Lazy<Regex> = lazy_regex!(r"^[\-\+]?[0-9]*\.[0-9]+$");
23static NUMBER: Lazy<Regex> = lazy_regex!(r"^[\-\+]?[0-9]+$");
24// b4df5d88 ends here
25
26// [[file:../../gchemol-readwrite.note::4d5a1513][4d5a1513]]
27/// Format number with run time formatting parameters
28#[derive(Debug, Clone, Default)]
29struct Format {
30    align: Option<char>,
31    sign: Option<char>,
32    width: Option<usize>,
33    precision: Option<usize>,
34}
35
36impl Format {
37    fn parse_from(param: &str) -> Self {
38        let mut f = Format::default();
39
40        if let Some(captures) = FORMAT_SPEC.captures(param) {
41            f.align = captures.name("align").and_then(|x| x.as_str().chars().next());
42            f.sign = captures.name("sign").and_then(|x| x.as_str().chars().next());
43            f.width = captures.name("width").and_then(|x| x.as_str().parse().ok());
44            f.precision = captures.name("precision").and_then(|x| x.as_str().parse().ok());
45        }
46
47        f
48    }
49}
50
51#[test]
52fn test_parse_regex() {
53    let captures = FORMAT_SPEC.captures("-12.5").unwrap();
54    assert!(captures.name("align").is_none());
55    assert!(captures.name("sign").is_some());
56    assert!(captures.name("width").is_some());
57    assert!(captures.name("precision").is_some());
58
59    let captures = FORMAT_SPEC.captures("^12").unwrap();
60    assert!(captures.name("align").is_some());
61    assert!(captures.name("width").is_some());
62
63    let captures = FORMAT_SPEC.captures("-12").unwrap();
64    assert!(captures.name("align").is_none());
65    assert!(captures.name("sign").is_some());
66    assert!(captures.name("width").is_some());
67
68    let format = Format::parse_from("-12.5");
69    assert_eq!(format.sign, Some('-'));
70    assert_eq!(format.width, Some(12));
71    assert_eq!(format.precision, Some(5));
72
73    let format = Format::parse_from("-12");
74    assert_eq!(format.align, None);
75    assert_eq!(format.sign, Some('-'));
76    assert_eq!(format.width, Some(12));
77    assert_eq!(format.precision, None);
78
79    let format = Format::parse_from("^12");
80    assert_eq!(format.sign, None);
81    assert_eq!(format.align, Some('^'));
82    assert_eq!(format.width, Some(12));
83    assert_eq!(format.precision, None);
84
85    assert!(FLOAT_NUM.is_match("12.55"));
86    assert!(FLOAT_NUM.is_match("+.55"));
87    assert!(FLOAT_NUM.is_match("-1.55"));
88    assert!(!FLOAT_NUM.is_match("-1.55a"));
89    assert!(NUMBER.is_match("-1"));
90}
91// 4d5a1513 ends here
92
93// [[file:../../gchemol-readwrite.note::*format][format:1]]
94/// format with run time parameters from user
95pub fn format_user_value(value: String, code: String) -> String {
96    let f = Format::parse_from(&code);
97
98    let width = f.width.unwrap_or(0);
99    let precision = f.precision.unwrap_or(width);
100
101    macro_rules! format_value {
102        ($v:ident) => {{
103            match f.align {
104                Some('<') => format!("{value:<width$.precision$}", value = $v, width = width, precision = precision),
105                Some('>') => format!("{value:>width$.precision$}", value = $v, width = width, precision = precision),
106                Some('^') => format!("{value:^width$.precision$}", value = $v, width = width, precision = precision),
107                None => format!("{value:width$.precision$}", value = $v, width = width, precision = precision),
108                _ => todo!(),
109            }
110        }};
111    }
112    if FLOAT_NUM.is_match(&value) {
113        let value: f64 = value.parse().unwrap();
114        format_value!(value)
115    } else if NUMBER.is_match(&value) {
116        let value: usize = value.parse().unwrap();
117        format_value!(value)
118    } else {
119        format_value!(value)
120    }
121}
122// format:1 ends here
123
124// [[file:../../gchemol-readwrite.note::e1b058cf][e1b058cf]]
125pub fn render(json_source: &str, template: &str) -> Result<String> {
126    let mut env = Environment::new();
127    // env.add_filter("format", format_user_value);
128    let ctx: serde_json::Value = serde_json::from_str(json_source)?;
129    let mut s = env.render_str(template, &ctx)?;
130    s.push_str("\n");
131    Ok(s)
132}
133
134/// Template rendering using minijinja
135pub struct Template<'a> {
136    env: Environment<'a>,
137    src: String,
138}
139
140impl<'a> Template<'a> {
141    /// Render template as string using vars from `json`.
142    pub fn render_json(&self, json: &str) -> Result<String> {
143        let ctx: serde_json::Value = serde_json::from_str(json)?;
144        let s = self.render(ctx)?;
145        Ok(s)
146    }
147
148    /// Renders the template into a string using vars from `ctx`.
149    pub fn render<S: Serialize>(&self, ctx: S) -> Result<String> {
150        let mut s = self
151            .env
152            .render_str(&self.src, &ctx)
153            .map_err(|e| anyhow!("Render molecule failure in tera: {:?}", e))?;
154        s.push_str("\n");
155        Ok(s)
156    }
157
158    /// Load template from file in `path`.
159    pub fn try_from_path(path: &Path) -> Result<Self> {
160        let src = gut::fs::read_file(path)?;
161        let t = Self::from_str(&src);
162        Ok(t)
163    }
164
165    /// Construct from `src`
166    pub fn from_str(src: &str) -> Self {
167        let mut env = Environment::new();
168        env.add_filter("format", format_user_value);
169        Self { env, src: src.to_owned() }
170    }
171}
172// e1b058cf ends here
173
174// [[file:../../gchemol-readwrite.note::7d600fb2][7d600fb2]]
175use super::Molecule;
176
177/// render molecule in user defined template
178pub(super) fn render_molecule_with(mol: &Molecule, template: &str) -> Result<String> {
179    let s = Template::from_str(template).render(super::renderable(mol))?;
180    Ok(s)
181}
182// 7d600fb2 ends here