1pub(crate) use std::fmt::Write;
12
13pub(crate) use crate::catalog::{Catalog, Kind, Locale};
14pub(crate) use crate::core::error::{Result, WorkshopError};
15pub(crate) use crate::settings::{PathPart, table};
16pub(crate) use crate::settings::{Settings as SettingsTree, SettingsNode};
17pub(crate) use crate::wir;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct EmitOptions {
23 pub fallback_locale: Option<Locale>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32#[non_exhaustive]
33pub struct EmitOutput {
34 pub text: String,
36 pub fallback_ids: Vec<String>,
40}
41
42pub fn emit(program: &crate::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
45 emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
46}
47
48#[cfg(test)]
49pub(crate) fn emit_wir(
50 program: &wir::Program,
51 catalog: &Catalog,
52 locale: &Locale,
53) -> Result<String> {
54 emit_with_options_inner(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
55}
56
57pub fn emit_with_options(
60 program: &crate::Program,
61 catalog: &Catalog,
62 locale: &Locale,
63 options: &EmitOptions,
64) -> Result<EmitOutput> {
65 let storage = program.to_wir()?;
66 emit_with_options_inner(&storage, catalog, locale, options)
67}
68
69#[cfg(test)]
70pub(crate) fn emit_wir_with_options(
71 program: &wir::Program,
72 catalog: &Catalog,
73 locale: &Locale,
74 options: &EmitOptions,
75) -> Result<EmitOutput> {
76 emit_with_options_inner(program, catalog, locale, options)
77}
78
79fn emit_with_options_inner(
80 program: &wir::Program,
81 catalog: &Catalog,
82 locale: &Locale,
83 options: &EmitOptions,
84) -> Result<EmitOutput> {
85 let mut emitter = EmitContext {
86 program,
87 catalog,
88 locale: locale.clone(),
89 fallback: options.fallback_locale.clone(),
90 fallback_ids: Vec::new(),
91 out: String::new(),
92 line_count: 0,
93 };
94 emitter.run()?;
95 Ok(EmitOutput {
96 text: emitter.out,
97 fallback_ids: emitter.fallback_ids,
98 })
99}
100
101pub(crate) struct EmitContext<'a> {
102 pub(crate) program: &'a wir::Program,
103 pub(crate) catalog: &'a Catalog,
104 pub(crate) locale: Locale,
105 pub(crate) fallback: Option<Locale>,
107 pub(crate) fallback_ids: Vec<String>,
109 pub(crate) out: String,
110 pub(crate) line_count: usize,
111}
112
113impl EmitContext<'_> {
114 pub(crate) fn run(&mut self) -> Result<()> {
115 if let Some(settings) = &self.program.settings {
117 self.emit_settings(settings)?;
118 self.out.push('\n');
119 }
120 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
121 let variables = self.structural("variables")?;
122 self.line(0, &format!("{variables} {{"))?;
123 if !self.program.global_variables.is_empty() {
124 let global = self.structural("global")?;
125 self.line(1, &format!("{global}:"))?;
126 for variable in self.program.global_variables.iter() {
127 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
128 }
129 }
130 if !self.program.player_variables.is_empty() {
131 let player = self.structural("player")?;
132 self.line(1, &format!("{player}:"))?;
133 for variable in self.program.player_variables.iter() {
134 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
135 }
136 }
137 self.line(0, "}")?;
138 self.out.push('\n');
139 }
140 if !self.program.subroutines.is_empty() {
141 let subroutines = self.structural("subroutines")?;
142 self.line(0, &format!("{subroutines} {{"))?;
143 for subroutine in self.program.subroutines.iter() {
144 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
145 }
146 self.line(0, "}")?;
147 self.out.push('\n');
148 }
149 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
150 if emitted_rules > 0 {
151 self.out.push('\n');
152 }
153 self.rule(rule)?;
154 }
155 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
159 self.out.push('\n');
160 }
161 Ok(())
162 }
163
164 pub(crate) fn indent(&mut self, level: usize) {
165 for _ in 0..level {
166 self.out.push_str(" ");
167 }
168 }
169
170 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
171 self.indent(level);
172 self.out.push_str(text);
173 self.out.push('\n');
174 self.line_count += 1;
175 Ok(())
176 }
177}
178
179pub(crate) fn escape_string(value: &str) -> String {
184 value.replace('"', "\\\"")
185}
186
187pub(crate) fn escape_value_string(value: &str) -> String {
192 let mut out = String::with_capacity(value.len());
193 for ch in value.chars() {
194 match ch {
195 '\\' => out.push_str("\\\\"),
196 '"' => out.push_str("\\\""),
197 '\n' => out.push_str("\\n"),
198 '\r' => out.push_str("\\r"),
199 other => out.push(other),
200 }
201 }
202 out
203}
204
205pub(crate) fn split_string(value: &str) -> Vec<String> {
214 if value.chars().count() <= 128 {
215 return vec![escape_value_string(value)];
216 }
217 let mut segments = Vec::new();
218 let mut rest = value;
219 while rest.chars().count() > 125 {
220 let chunk: String = rest.chars().take(125).collect();
221 let mut text = escape_value_string(&chunk);
222 text.push_str("{0}");
223 segments.push(text);
224 rest = &rest[chunk.len()..];
225 }
226 if !rest.is_empty() {
227 segments.push(escape_value_string(rest));
228 }
229 segments
230}
231
232pub(crate) fn escape_settings_string(value: &str) -> String {
238 let mut out = String::with_capacity(value.len());
239 for ch in value.chars() {
240 match ch {
241 '\\' => out.push_str("\\\\"),
242 '"' => out.push_str("\\\""),
243 '\n' => out.push_str("\\n"),
244 '\t' => out.push_str("\\t"),
245 '\r' => out.push_str("\\r"),
246 other => out.push(other),
247 }
248 }
249 out
250}
251
252pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
258 let Some((first, rest)) = segments.split_first() else {
259 return;
260 };
261 out.push_str(spelling);
262 out.push('(');
263 write!(out, "\"{first}\"").unwrap();
264 for segment in rest {
265 out.push_str(", ");
266 out.push_str(spelling);
267 out.push('(');
268 write!(out, "\"{segment}\"").unwrap();
269 }
270 for _ in 0..=rest.len() {
271 out.push(')');
272 }
273}
274
275pub(crate) fn fold_number(value: f64) -> String {
279 if value.fract() == 0.0 && value.abs() < 1e15 {
280 format!("{}", value as i64)
281 } else {
282 let scaled = (value * 100.0).round();
283 let sign = if scaled < 0.0 { "-" } else { "" };
284 let scaled = scaled.abs() as i64;
285 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
286 }
287}