1pub(crate) use std::fmt::Write;
2
3pub(crate) use crate::catalog::{Catalog, Kind, Locale};
4pub(crate) use crate::core::error::{Result, WorkshopError};
5pub(crate) use crate::settings::{PathPart, table};
6pub(crate) use crate::settings::{Settings as SettingsTree, SettingsNode};
7pub(crate) use crate::wir;
8
9#[derive(Debug, Clone, Default, PartialEq, Eq)]
11#[non_exhaustive]
12pub struct EmitOptions {
13 pub fallback_locale: Option<Locale>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct EmitOutput {
24 pub text: String,
26 pub fallback_ids: Vec<String>,
30}
31
32pub fn emit(program: &crate::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
35 emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
36}
37
38#[cfg(test)]
39pub(crate) fn emit_wir(
40 program: &wir::Program,
41 catalog: &Catalog,
42 locale: &Locale,
43) -> Result<String> {
44 emit_with_options_inner(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
45}
46
47pub fn emit_with_options(
50 program: &crate::Program,
51 catalog: &Catalog,
52 locale: &Locale,
53 options: &EmitOptions,
54) -> Result<EmitOutput> {
55 let storage = program.to_wir()?;
56 emit_with_options_inner(&storage, catalog, locale, options)
57}
58
59#[cfg(test)]
60pub(crate) fn emit_wir_with_options(
61 program: &wir::Program,
62 catalog: &Catalog,
63 locale: &Locale,
64 options: &EmitOptions,
65) -> Result<EmitOutput> {
66 emit_with_options_inner(program, catalog, locale, options)
67}
68
69fn emit_with_options_inner(
70 program: &wir::Program,
71 catalog: &Catalog,
72 locale: &Locale,
73 options: &EmitOptions,
74) -> Result<EmitOutput> {
75 let mut emitter = EmitContext {
76 program,
77 catalog,
78 locale: locale.clone(),
79 fallback: options.fallback_locale.clone(),
80 fallback_ids: Vec::new(),
81 out: String::new(),
82 line_count: 0,
83 };
84 emitter.run()?;
85 Ok(EmitOutput {
86 text: emitter.out,
87 fallback_ids: emitter.fallback_ids,
88 })
89}
90
91pub(crate) struct EmitContext<'a> {
92 pub(crate) program: &'a wir::Program,
93 pub(crate) catalog: &'a Catalog,
94 pub(crate) locale: Locale,
95 pub(crate) fallback: Option<Locale>,
96 pub(crate) fallback_ids: Vec<String>,
97 pub(crate) out: String,
98 pub(crate) line_count: usize,
99}
100
101impl EmitContext<'_> {
102 pub(crate) fn run(&mut self) -> Result<()> {
103 if let Some(settings) = &self.program.settings {
104 self.emit_settings(settings)?;
105 self.out.push('\n');
106 }
107 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
108 let variables = self.structural("variables")?;
109 self.line(0, &format!("{variables} {{"))?;
110 if !self.program.global_variables.is_empty() {
111 let global = self.structural("global")?;
112 self.line(1, &format!("{global}:"))?;
113 for variable in self.program.global_variables.iter() {
114 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
115 }
116 }
117 if !self.program.player_variables.is_empty() {
118 let player = self.structural("player")?;
119 self.line(1, &format!("{player}:"))?;
120 for variable in self.program.player_variables.iter() {
121 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
122 }
123 }
124 self.line(0, "}")?;
125 self.out.push('\n');
126 }
127 if !self.program.subroutines.is_empty() {
128 let subroutines = self.structural("subroutines")?;
129 self.line(0, &format!("{subroutines} {{"))?;
130 for subroutine in self.program.subroutines.iter() {
131 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
132 }
133 self.line(0, "}")?;
134 self.out.push('\n');
135 }
136 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
137 if emitted_rules > 0 {
138 self.out.push('\n');
139 }
140 self.rule(rule)?;
141 }
142 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
146 self.out.push('\n');
147 }
148 Ok(())
149 }
150
151 pub(crate) fn indent(&mut self, level: usize) {
152 for _ in 0..level {
153 self.out.push_str(" ");
154 }
155 }
156
157 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
158 self.indent(level);
159 self.out.push_str(text);
160 self.out.push('\n');
161 self.line_count += 1;
162 Ok(())
163 }
164}
165
166pub(crate) fn escape_string(value: &str) -> String {
167 let mut out = String::with_capacity(value.len());
168 for ch in value.chars() {
169 match ch {
170 '\\' => out.push_str("\\\\"),
171 '"' => out.push_str("\\\""),
172 '\n' => out.push_str("\\n"),
173 '\r' => out.push_str("\\r"),
174 '\t' => out.push_str("\\t"),
175 other => out.push(other),
176 }
177 }
178 out
179}
180
181pub(crate) fn escape_value_string(value: &str) -> String {
186 let mut out = String::with_capacity(value.len());
187 for ch in value.chars() {
188 match ch {
189 '\\' => out.push_str("\\\\"),
190 '"' => out.push_str("\\\""),
191 '\n' => out.push_str("\\n"),
192 '\r' => out.push_str("\\r"),
193 other => out.push(other),
194 }
195 }
196 out
197}
198
199pub(crate) fn split_string(value: &str) -> Vec<String> {
208 if value.chars().count() <= 128 {
209 return vec![escape_value_string(value)];
210 }
211 let mut segments = Vec::new();
212 let mut rest = value;
213 while rest.chars().count() > 125 {
214 let chunk: String = rest.chars().take(125).collect();
215 let mut text = escape_value_string(&chunk);
216 text.push_str("{0}");
217 segments.push(text);
218 rest = &rest[chunk.len()..];
219 }
220 if !rest.is_empty() {
221 segments.push(escape_value_string(rest));
222 }
223 segments
224}
225
226pub(crate) fn escape_settings_string(value: &str) -> String {
232 let mut out = String::with_capacity(value.len());
233 for ch in value.chars() {
234 match ch {
235 '\\' => out.push_str("\\\\"),
236 '"' => out.push_str("\\\""),
237 '\n' => out.push_str("\\n"),
238 '\t' => out.push_str("\\t"),
239 '\r' => out.push_str("\\r"),
240 other => out.push(other),
241 }
242 }
243 out
244}
245
246pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
252 let Some((first, rest)) = segments.split_first() else {
253 return;
254 };
255 out.push_str(spelling);
256 out.push('(');
257 write!(out, "\"{first}\"").unwrap();
258 for segment in rest {
259 out.push_str(", ");
260 out.push_str(spelling);
261 out.push('(');
262 write!(out, "\"{segment}\"").unwrap();
263 }
264 for _ in 0..=rest.len() {
265 out.push(')');
266 }
267}
268
269pub(crate) fn fold_number(value: f64) -> String {
273 if value.fract() == 0.0 && value.abs() < 1e15 {
274 format!("{}", value as i64)
275 } else {
276 let scaled = (value * 100.0).round();
277 let sign = if scaled < 0.0 { "-" } else { "" };
278 let scaled = scaled.abs() as i64;
279 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
280 }
281}