1use std::{borrow::Cow, fmt::Write as _, fs, path::Path};
2
3use fluent_syntax::ast::{Attribute, CallArguments, Entry, Expression, Identifier, InlineExpression, Pattern, PatternElement, VariantKey};
4
5use crate::util;
6
7pub fn generate(dir: &str, to_name: &str, file_header: &str, transform: &impl Fn(&str) -> Cow<str>, check: bool, verbose: bool) {
8 let dir_path = Path::new(dir);
9 let pattern = dir_path.join("**/*.ftl");
10 let to_dir = dir_path.with_file_name(to_name);
11 for entry in glob::glob(&pattern.display().to_string()).unwrap_or_else(|e| fatal!("cannot read `{dir}`, {e}")) {
12 let entry = entry.unwrap_or_else(|e| fatal!("cannot read `{dir}` entry, {e}"));
13 let relative_entry = entry.strip_prefix(dir_path).unwrap();
14 let to_file = to_dir.join(relative_entry);
15 let _ = util::check_or_create_dir_all(check, to_file.parent().unwrap());
16 let display_to = to_file.strip_prefix(to_dir.parent().unwrap()).unwrap();
17 generate_file(&entry, &to_file, display_to, file_header, transform, check, verbose);
18 }
19}
20
21fn generate_file(
22 from: &Path,
23 to: &Path,
24 display_to: &Path,
25 file_header: &str,
26 transform: &impl Fn(&str) -> Cow<str>,
27 check: bool,
28 verbose: bool,
29) {
30 let source = match fs::read_to_string(from) {
31 Ok(s) => s,
32 Err(e) => {
33 error!("cannot read `{}`, {e}", from.display());
34 return;
35 }
36 };
37
38 let source = match fluent_syntax::parser::parse(source) {
39 Ok(s) => s,
40 Err((s, e)) => {
41 error!(
42 "cannot parse `{}`\n{}",
43 from.display(),
44 e.into_iter().map(|e| format!(" {e}")).collect::<Vec<_>>().join("\n")
45 );
46 s
47 }
48 };
49
50 let mut output = file_header.to_owned();
51
52 for entry in source.body {
53 match entry {
54 Entry::Message(m) => write_entry(&mut output, false, &m.id, m.value.as_ref(), &m.attributes, transform),
55 Entry::Term(t) => write_entry(&mut output, true, &t.id, Some(&t.value), &t.attributes, transform),
56 Entry::Comment(_) | Entry::GroupComment(_) | Entry::ResourceComment(_) | Entry::Junk { .. } => {}
57 }
58 }
59
60 if let Err(e) = util::check_or_write(check, to, output.as_bytes(), verbose) {
61 error!("cannot write `{}`, {e}", to.display());
62 } else {
63 println!(" generated {}", display_to.display());
64 }
65}
66
67fn write_entry(
68 output: &mut String,
69 is_term: bool,
70 id: &Identifier<String>,
71 value: Option<&Pattern<String>>,
72 attributes: &[Attribute<String>],
73 transform: &impl Fn(&str) -> Cow<str>,
74) {
75 write!(output, "\n\n{}{} = ", if is_term { "-" } else { "" }, id.name).unwrap();
76 if let Some(value) = value {
77 write_pattern(output, value, transform, 1);
78 }
79 for attr in attributes {
80 write!(output, "\n .{} = ", attr.id.name).unwrap();
81 write_pattern(output, &attr.value, transform, 2);
82 }
83}
84
85fn write_pattern(output: &mut String, pattern: &Pattern<String>, transform: &impl Fn(&str) -> Cow<str>, depth: usize) {
86 for el in &pattern.elements {
87 match el {
88 PatternElement::TextElement { value } => {
89 let mut prefix = String::new();
90 for line in value.split('\n') {
91 write!(output, "{prefix}{}", transform(line)).unwrap();
93 prefix = format!("\n{}", " ".repeat(depth * 4));
94 }
95 }
96 PatternElement::Placeable { expression } => write_expression(output, expression, transform, depth),
97 }
98 }
99}
100
101fn write_expression(output: &mut String, expr: &Expression<String>, transform: &impl Fn(&str) -> Cow<str>, depth: usize) {
102 match expr {
103 Expression::Select { selector, variants } => {
104 write!(output, "{{").unwrap();
105 write_inline_expression_inner(output, selector, transform, depth);
106 writeln!(output, " ->").unwrap();
107
108 for v in variants {
109 write!(output, "{}", " ".repeat((depth + 1) * 4)).unwrap();
110 if v.default {
111 write!(output, "*").unwrap();
112 }
113 let key = match &v.key {
114 VariantKey::Identifier { name } => name,
115 VariantKey::NumberLiteral { value } => value,
116 };
117 write!(output, "[{key}] ").unwrap();
118
119 write_pattern(output, &v.value, transform, depth + 2);
120 writeln!(output).unwrap();
121 }
122
123 writeln!(output, "}}").unwrap();
124 }
125 Expression::Inline(e) => write_inline_expression(output, e, transform, depth),
126 }
127}
128fn write_inline_expression(output: &mut String, expr: &InlineExpression<String>, transform: &impl Fn(&str) -> Cow<str>, depth: usize) {
129 write!(output, "{{ ").unwrap();
130 write_inline_expression_inner(output, expr, transform, depth);
131 write!(output, " }} ").unwrap();
132}
133fn write_inline_expression_inner(
134 output: &mut String,
135 expr: &InlineExpression<String>,
136 transform: &impl Fn(&str) -> Cow<str>,
137 depth: usize,
138) {
139 match expr {
140 InlineExpression::StringLiteral { value } => {
141 let value = transform(value);
142 let value = value.replace('\\', "\\\\").replace('"', "\\\"");
143 write!(output, "\"{value}\"").unwrap()
144 }
145 InlineExpression::NumberLiteral { value } => write!(output, "{value}").unwrap(),
146 InlineExpression::FunctionReference { id, arguments } => {
147 write!(output, "{}", id.name).unwrap();
148 write_arguments(output, arguments, transform, depth);
149 }
150 InlineExpression::MessageReference { id, attribute } => {
151 write!(output, "{}", id.name).unwrap();
152 if let Some(a) = attribute {
153 write!(output, ".{}", a.name).unwrap();
154 }
155 }
156 InlineExpression::TermReference { id, attribute, arguments } => {
157 write!(output, "-{}", id.name).unwrap();
158 if let Some(a) = attribute {
159 write!(output, ".{}", a.name).unwrap();
160 }
161 if let Some(args) = arguments {
162 write_arguments(output, args, transform, depth);
163 }
164 }
165 InlineExpression::VariableReference { id } => write!(output, "${}", id.name).unwrap(),
166 InlineExpression::Placeable { expression } => {
167 write_expression(output, expression, transform, depth);
168 }
169 }
170}
171
172fn write_arguments(output: &mut String, arguments: &CallArguments<String>, transform: &impl Fn(&str) -> Cow<str>, depth: usize) {
173 write!(output, "(").unwrap();
174 let mut sep = "";
175 for a in &arguments.positional {
176 write!(output, "{sep}").unwrap();
177 write_inline_expression_inner(output, a, transform, depth);
178 sep = ", ";
179 }
180 for a in &arguments.named {
181 write!(output, "{sep}{}:", a.name.name).unwrap();
182 write_inline_expression_inner(output, &a.value, transform, depth);
183 sep = ", ";
184 }
185 write!(output, ")").unwrap();
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn basic_write_entry() {
194 let source = r#"
195-lang = en-US
196
197button = Button
198
199window =
200 .title = Localize Example ({-lang})
201
202click-count = {$n ->
203 [one] Clicked {$n} time
204 *[other] Clicked {$n} times
205}
206key-count = {NUMBER($n) ->
207 [one] Clicked {$n} time
208 *[other] Clicked {$n} times
209}
210 "#;
211 let source = fluent_syntax::parser::parse(source.to_owned()).unwrap();
212
213 let mut output = String::new();
214 for entry in &source.body {
215 match entry {
216 Entry::Message(m) => write_entry(&mut output, false, &m.id, m.value.as_ref(), &m.attributes, &|a| Cow::Borrowed(a)),
217 Entry::Term(t) => write_entry(&mut output, true, &t.id, Some(&t.value), &t.attributes, &|a| Cow::Borrowed(a)),
218 _ => {}
219 }
220 }
221
222 let _ =
223 fluent_syntax::parser::parse(output.clone()).unwrap_or_else(|e| panic!("write_entry output invalid\n{}\n{output}", &e.1[0]));
224 }
225}