|
1 | 1 | use std::{borrow::Cow, path::Path}; |
2 | 2 |
|
| 3 | +use askama::Template; |
| 4 | + |
3 | 5 |
|
4 | 6 | struct Line<S> { |
5 | 7 | pub hut: S, |
@@ -30,51 +32,70 @@ impl<T> From<Vec<T>> for Line<T> { |
30 | 32 | } |
31 | 33 | } |
32 | 34 |
|
33 | | -const CONVERT_GENERAL: &str = r##" |
34 | | -pub fn {from}_to_{to}(value: {From}) -> Option<{To}> { |
| 35 | +mod filters { |
| 36 | + pub fn pad_right<T: std::fmt::Display>(s: T, _: &dyn askama::Values, len: usize) -> askama::Result<String> { |
| 37 | + let mut s = s.to_string(); |
| 38 | + while s.len() < len { |
| 39 | + s.push(' '); |
| 40 | + } |
| 41 | + Ok(s) |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +#[derive(Debug, Clone, askama::Template)] |
| 46 | +#[template(ext = "txt", source = r#"// This file is auto-generated. Do not edit manually. |
| 47 | +
|
| 48 | +
|
| 49 | +pub fn {{from | lower}}_to_{{to | lower}}(value: {{from}}) -> Option<{{to}}> { |
35 | 50 | let result = match value { |
36 | | - #( $key => {prefix}$value{suffix}, )# |
| 51 | + {% for (k, v) in map -%} |
| 52 | + {{ k | pad_right(*k_len) }} => {{prefix}}{{ v | pad_right(*v_len) }}{{suffix}}, |
| 53 | + {% endfor -%} |
37 | 54 | _ => return None, |
38 | 55 | }; |
39 | 56 | Some(result) |
40 | 57 | } |
41 | 58 |
|
42 | | -impl crate::convert::Convert<{From}, {To}> for crate::convert::Converter { |
43 | | - fn convert(value: {From}) -> Option<{To}> { |
44 | | - {from}_to_{to}(value) |
| 59 | +impl crate::convert::Convert<{{from}}, {{to}}> for crate::convert::Converter { |
| 60 | + fn convert(value: {{from}}) -> Option<{{to}}> { |
| 61 | + {{from|lower}}_to_{{to|lower}}(value) |
45 | 62 | } |
46 | 63 | } |
47 | | -"##; |
48 | | - |
49 | | -fn gen_template(template: &str, from: &str, to: &str, prefix: Option<&str>, suffix: Option<&str>) -> String { |
50 | | - template.replace("{from}", &from.to_lowercase()).replace("{to}", &to.to_lowercase()) |
51 | | - .replace("{From}", from).replace("{To}", to) |
52 | | - .replace("{prefix}", prefix.unwrap_or_default()).replace("{suffix}", suffix.unwrap_or_default()) |
| 64 | +"#)] |
| 65 | +pub struct GeneralTemplate<'a> { |
| 66 | + pub from: &'a str, |
| 67 | + pub to: &'a str, |
| 68 | + pub prefix: &'a str, |
| 69 | + pub suffix: &'a str, |
| 70 | + pub map: Vec<(Cow<'a, str>, Cow<'a, str>)>, |
| 71 | + pub k_len: usize, |
| 72 | + pub v_len: usize, |
53 | 73 | } |
54 | 74 |
|
55 | | -fn gen_convert<S1: AsRef<str>, S2: AsRef<str>, I: IntoIterator<Item = (S1, S2)>>(template: &str, map: I) -> String { |
56 | | - fn normalize(s: &str) -> Cow<str> { |
57 | | - let s = s.trim_start(); |
58 | | - if s.contains('*') { |
59 | | - s.rsplit('*').next().unwrap(); |
60 | | - return format!("{}{}", s.trim_end().trim_end_matches('*'), s.rsplit('*').next().unwrap()).into() |
| 75 | +impl<'a> GeneralTemplate<'a> { |
| 76 | + pub fn create(from: &'a str, to: &'a str, prefix: Option<&'a str>, suffix: Option<&'a str>) -> Self { |
| 77 | + let prefix = prefix.unwrap_or(""); |
| 78 | + let suffix = suffix.unwrap_or(""); |
| 79 | + Self { |
| 80 | + from, |
| 81 | + to, |
| 82 | + prefix, |
| 83 | + suffix, |
| 84 | + map: vec![], |
| 85 | + k_len: 0, |
| 86 | + v_len: 0, |
61 | 87 | } |
62 | | - s.into() |
63 | | - } |
64 | | - let map = map.into_iter().collect::<Vec<_>>(); |
65 | | - // https://rustexp.lpil.uk/ |
66 | | - let generated_code = regex::Regex::new(r"\n([ \t]*)#\(([\s\S]*)\)#(\n?)").unwrap().replace_all(template, |caps: ®ex::Captures| { |
67 | | - let indent = &caps[1]; |
68 | | - let inner = caps[2].trim(); |
69 | | - let newline = &caps[3]; |
70 | | - let content = map.iter().map(|(key, value)| { |
71 | | - inner.replace("$key", &normalize(key.as_ref())).replace("$value", &normalize(value.as_ref())) |
72 | | - }).collect::<Vec<String>>().join(&format!("{newline}{indent}")); |
73 | | - format!("{newline}{indent}{content}{newline}") |
74 | | - }); |
75 | | - format!("// This file is auto-generated. Do not edit manually.\n\n{}", generated_code) |
76 | | -} |
| 88 | + } |
77 | 89 |
|
| 90 | + pub fn build(self, map: Vec<(Cow<'a, str>, Cow<'a, str>)>) -> String { |
| 91 | + Self { |
| 92 | + k_len: map.iter().map(|(k, _)| k.len()).max().unwrap_or(0), |
| 93 | + v_len: map.iter().map(|(_, v)| v.len()).max().unwrap_or(0), |
| 94 | + map, |
| 95 | + ..self |
| 96 | + }.render().unwrap() |
| 97 | + } |
| 98 | +} |
78 | 99 |
|
79 | 100 | #[expect(unused)] |
80 | 101 | #[derive(Debug, Clone, Copy)] |
@@ -123,26 +144,39 @@ impl KeyType { |
123 | 144 | _ => None |
124 | 145 | } |
125 | 146 | } |
126 | | -} |
127 | 147 |
|
128 | | -fn is_valid<S: AsRef<str>>(s: S) -> bool { |
129 | | - let s = s.as_ref().trim(); |
130 | | - !s.is_empty() && !s.starts_with("n!") && !s.starts_with("na!") && !s.starts_with("todo!") && !s.starts_with("none!") |
131 | | -} |
132 | | -fn kv_is_valid<K: AsRef<str>, V: AsRef<str>>((k, v): &(K, V)) -> bool { |
133 | | - is_valid(k) && is_valid(v) && !k.as_ref().trim().ends_with('*') |
| 148 | + pub fn is_valid<S: AsRef<str>>(self, s: S) -> bool { |
| 149 | + let s = s.as_ref().trim(); |
| 150 | + !s.is_empty() && !s.starts_with("n!") && !s.starts_with("na!") && !s.starts_with("todo!") && !s.starts_with("none!") |
| 151 | + } |
| 152 | + |
| 153 | + pub fn get_content_unchecked<'a>(self, s: &'a str) -> Cow<'a, str> { |
| 154 | + let s = s.trim_start(); |
| 155 | + Cow::Borrowed(s.trim().trim_end_matches("*")) |
| 156 | + } |
134 | 157 | } |
135 | 158 |
|
| 159 | + |
| 160 | +#[derive(Debug, Clone, Copy)] |
136 | 161 | struct Gen(KeyType, KeyType); |
137 | 162 |
|
138 | 163 | impl Gen { |
139 | | - pub fn build(self, csv: &[Line<&str>]) -> String { |
| 164 | + pub fn build_kv<'a>(self, csv: &'a [Line<&'a str>]) -> Vec<(Cow<'a, str>, Cow<'a, str>)> { |
| 165 | + let from = self.0; |
| 166 | + let to = self.1; |
| 167 | + csv.iter().map(|i| (from.get_line(i), to.get_line(i))).filter(|t| self.kv_is_valid(t)).map(|(k, v)| (from.get_content_unchecked(k), to.get_content_unchecked(v))).collect() |
| 168 | + } |
| 169 | + |
| 170 | + pub fn kv_is_valid<K: AsRef<str>, V: AsRef<str>>(self, (k, v): &(K, V)) -> bool { |
| 171 | + self.0.is_valid(k) && self.1.is_valid(v) && !k.as_ref().trim().ends_with('*') |
| 172 | + } |
| 173 | + |
| 174 | + pub fn build_general<'a>(self, csv: &'a [Line<&'a str>]) -> String { |
140 | 175 | let from = self.0; |
141 | 176 | let to = self.1; |
142 | | - gen_convert( |
143 | | - &gen_template(CONVERT_GENERAL, from.name(), to.name(), to.as_value_prefix(), to.as_value_suffix()), |
144 | | - csv.iter().map(|i| (from.get_line(i), to.get_line(i))).filter(kv_is_valid) |
145 | | - ) |
| 177 | + let map = self.build_kv(csv); |
| 178 | + GeneralTemplate::create(from.name(), to.name(), to.as_value_prefix(), to.as_value_suffix()) |
| 179 | + .build(map) |
146 | 180 | } |
147 | 181 | } |
148 | 182 |
|
@@ -187,7 +221,7 @@ pub fn main() { |
187 | 221 | ] { |
188 | 222 | let (from, to) = tuple; |
189 | 223 | let filename = format!("generated.{from:?}_to_{to:?}.rs"); |
190 | | - let content = Gen(from, to).build(&csv); |
| 224 | + let content = Gen(from, to).build_general(&csv); |
191 | 225 | save_file(format!("{output_path}/{filename}"), content) |
192 | 226 | .expect("Failed to write generated.rs"); |
193 | 227 | } |
|
0 commit comments