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::table::KeyKind;
16pub(crate) use crate::settings::{PathPart, table};
17pub(crate) use crate::settings::{Settings as SettingsTree, SettingsNode};
18pub(crate) use crate::wir;
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct EmitOptions {
23 pub fallback_locale: Option<Locale>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct EmitOutput {
33 pub text: String,
35 pub fallback_ids: Vec<String>,
39}
40
41pub fn emit(program: &crate::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
44 emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
45}
46
47#[cfg(test)]
48pub(crate) fn emit_wir(
49 program: &wir::Program,
50 catalog: &Catalog,
51 locale: &Locale,
52) -> Result<String> {
53 emit_with_options_inner(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
54}
55
56pub fn emit_with_options(
59 program: &crate::Program,
60 catalog: &Catalog,
61 locale: &Locale,
62 options: &EmitOptions,
63) -> Result<EmitOutput> {
64 let storage = program.to_wir()?;
65 emit_with_options_inner(&storage, catalog, locale, options)
66}
67
68#[cfg(test)]
69pub(crate) fn emit_wir_with_options(
70 program: &wir::Program,
71 catalog: &Catalog,
72 locale: &Locale,
73 options: &EmitOptions,
74) -> Result<EmitOutput> {
75 emit_with_options_inner(program, catalog, locale, options)
76}
77
78fn emit_with_options_inner(
79 program: &wir::Program,
80 catalog: &Catalog,
81 locale: &Locale,
82 options: &EmitOptions,
83) -> Result<EmitOutput> {
84 let mut emitter = EmitContext {
85 program,
86 catalog,
87 locale: locale.clone(),
88 fallback: options.fallback_locale.clone(),
89 fallback_ids: Vec::new(),
90 out: String::new(),
91 line_count: 0,
92 };
93 emitter.run()?;
94 Ok(EmitOutput {
95 text: emitter.out,
96 fallback_ids: emitter.fallback_ids,
97 })
98}
99
100pub(crate) struct EmitContext<'a> {
101 pub(crate) program: &'a wir::Program,
102 pub(crate) catalog: &'a Catalog,
103 pub(crate) locale: Locale,
104 pub(crate) fallback: Option<Locale>,
106 pub(crate) fallback_ids: Vec<String>,
108 pub(crate) out: String,
109 pub(crate) line_count: usize,
110}
111
112impl EmitContext<'_> {
113 pub(crate) fn run(&mut self) -> Result<()> {
114 if let Some(settings) = &self.program.settings {
116 self.emit_settings(settings)?;
117 self.out.push('\n');
118 }
119 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
120 let variables = self.structural("variables")?;
121 self.line(0, &format!("{variables} {{"))?;
122 if !self.program.global_variables.is_empty() {
123 let global = self.structural("global")?;
124 self.line(1, &format!("{global}:"))?;
125 for variable in self.program.global_variables.iter() {
126 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
127 }
128 }
129 if !self.program.player_variables.is_empty() {
130 let player = self.structural("player")?;
131 self.line(1, &format!("{player}:"))?;
132 for variable in self.program.player_variables.iter() {
133 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
134 }
135 }
136 self.line(0, "}")?;
137 self.out.push('\n');
138 }
139 if !self.program.subroutines.is_empty() {
140 let subroutines = self.structural("subroutines")?;
141 self.line(0, &format!("{subroutines} {{"))?;
142 for subroutine in self.program.subroutines.iter() {
143 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
144 }
145 self.line(0, "}")?;
146 self.out.push('\n');
147 }
148 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
149 if emitted_rules > 0 {
150 self.out.push('\n');
151 }
152 self.rule(rule)?;
153 }
154 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
158 self.out.push('\n');
159 }
160 Ok(())
161 }
162
163 pub(crate) fn malformed(&self, message: impl Into<String>) -> WorkshopError {
164 WorkshopError::Malformed {
165 message: message.into(),
166 span: None,
167 }
168 }
169
170 pub(crate) fn indent(&mut self, level: usize) {
171 for _ in 0..level {
172 self.out.push_str(" ");
173 }
174 }
175
176 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
177 self.indent(level);
178 self.out.push_str(text);
179 self.out.push('\n');
180 self.line_count += 1;
181 Ok(())
182 }
183}
184
185pub(crate) fn is_comparison_operator(name: &str) -> bool {
190 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
191}
192
193pub(crate) fn escape_string(value: &str) -> String {
194 value.replace('"', "\\\"")
195}
196
197pub(crate) fn escape_value_string(value: &str) -> String {
202 let mut out = String::with_capacity(value.len());
203 for ch in value.chars() {
204 match ch {
205 '\\' => out.push_str("\\\\"),
206 '"' => out.push_str("\\\""),
207 '\n' => out.push_str("\\n"),
208 '\r' => out.push_str("\\r"),
209 other => out.push(other),
210 }
211 }
212 out
213}
214
215pub(crate) fn split_string(value: &str) -> Vec<String> {
224 if value.chars().count() <= 128 {
225 return vec![escape_value_string(value)];
226 }
227 let mut segments = Vec::new();
228 let mut rest = value;
229 while rest.chars().count() > 125 {
230 let chunk: String = rest.chars().take(125).collect();
231 let mut text = escape_value_string(&chunk);
232 text.push_str("{0}");
233 segments.push(text);
234 rest = &rest[chunk.len()..];
235 }
236 if !rest.is_empty() {
237 segments.push(escape_value_string(rest));
238 }
239 segments
240}
241
242pub(crate) fn escape_settings_string(value: &str) -> String {
248 let mut out = String::with_capacity(value.len());
249 for ch in value.chars() {
250 match ch {
251 '\\' => out.push_str("\\\\"),
252 '"' => out.push_str("\\\""),
253 '\n' => out.push_str("\\n"),
254 '\t' => out.push_str("\\t"),
255 '\r' => out.push_str("\\r"),
256 other => out.push(other),
257 }
258 }
259 out
260}
261
262pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
268 let Some((first, rest)) = segments.split_first() else {
269 return;
270 };
271 out.push_str(spelling);
272 out.push('(');
273 write!(out, "\"{first}\"").unwrap();
274 for segment in rest {
275 out.push_str(", ");
276 out.push_str(spelling);
277 out.push('(');
278 write!(out, "\"{segment}\"").unwrap();
279 }
280 for _ in 0..=rest.len() {
281 out.push(')');
282 }
283}
284
285pub(crate) fn fold_number(value: f64) -> String {
289 if value.fract() == 0.0 && value.abs() < 1e15 {
290 format!("{}", value as i64)
291 } else {
292 let scaled = (value * 100.0).round();
293 let sign = if scaled < 0.0 { "-" } else { "" };
294 let scaled = scaled.abs() as i64;
295 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
296 }
297}