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::core::format::format_number;
16pub(crate) use crate::settings::table::KeyKind;
17pub(crate) use crate::settings::{PathPart, table};
18pub(crate) use crate::settings::{Settings as SettingsTree, SettingsNode};
19pub(crate) use crate::wir;
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct EmitOptions {
24 pub fallback_locale: Option<Locale>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
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(), false)
55 .map(|out| out.text)
56}
57
58pub fn emit_with_options(
61 program: &crate::Program,
62 catalog: &Catalog,
63 locale: &Locale,
64 options: &EmitOptions,
65) -> Result<EmitOutput> {
66 let storage = program.to_wir()?;
67 emit_with_options_inner(&storage, catalog, locale, options, false)
68}
69
70pub(crate) fn emit_with_options_for_conversion(
71 program: &crate::Program,
72 catalog: &Catalog,
73 locale: &Locale,
74 options: &EmitOptions,
75) -> Result<EmitOutput> {
76 let storage = program.to_wir()?;
77 emit_with_options_inner(&storage, catalog, locale, options, true)
78}
79
80#[cfg(test)]
81pub(crate) fn emit_wir_with_options(
82 program: &wir::Program,
83 catalog: &Catalog,
84 locale: &Locale,
85 options: &EmitOptions,
86) -> Result<EmitOutput> {
87 emit_with_options_inner(program, catalog, locale, options, false)
88}
89
90fn emit_with_options_inner(
91 program: &wir::Program,
92 catalog: &Catalog,
93 locale: &Locale,
94 options: &EmitOptions,
95 force_hero_constructors: bool,
96) -> Result<EmitOutput> {
97 let mut emitter = EmitContext {
98 program,
99 catalog,
100 locale: locale.clone(),
101 fallback: options.fallback_locale.clone(),
102 force_hero_constructors,
103 fallback_ids: Vec::new(),
104 out: String::new(),
105 line_count: 0,
106 };
107 emitter.run()?;
108 Ok(EmitOutput {
109 text: emitter.out,
110 fallback_ids: emitter.fallback_ids,
111 })
112}
113
114pub(crate) struct EmitContext<'a> {
115 pub(crate) program: &'a wir::Program,
116 pub(crate) catalog: &'a Catalog,
117 pub(crate) locale: Locale,
118 pub(crate) fallback: Option<Locale>,
120 pub(crate) fallback_ids: Vec<String>,
122 pub(crate) force_hero_constructors: bool,
123 pub(crate) out: String,
124 pub(crate) line_count: usize,
125}
126
127impl EmitContext<'_> {
128 pub(crate) fn run(&mut self) -> Result<()> {
129 if let Some(settings) = &self.program.settings {
131 self.emit_settings(settings)?;
132 self.out.push('\n');
133 }
134 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
135 let variables = self.structural("variables")?;
136 self.line(0, &format!("{variables} {{"))?;
137 if !self.program.global_variables.is_empty() {
138 let global = self.structural("global")?;
139 self.line(1, &format!("{global}:"))?;
140 for variable in self.program.global_variables.iter() {
141 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
142 }
143 }
144 if !self.program.player_variables.is_empty() {
145 let player = self.structural("player")?;
146 self.line(1, &format!("{player}:"))?;
147 for variable in self.program.player_variables.iter() {
148 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
149 }
150 }
151 self.line(0, "}")?;
152 self.out.push('\n');
153 }
154 if !self.program.subroutines.is_empty() {
155 let subroutines = self.structural("subroutines")?;
156 self.line(0, &format!("{subroutines} {{"))?;
157 for subroutine in self.program.subroutines.iter() {
158 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
159 }
160 self.line(0, "}")?;
161 self.out.push('\n');
162 }
163 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
164 if emitted_rules > 0 {
165 self.out.push('\n');
166 }
167 self.rule(rule)?;
168 }
169 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
173 self.out.push('\n');
174 }
175 Ok(())
176 }
177
178 pub(crate) fn malformed(&self, message: impl Into<String>) -> WorkshopError {
179 WorkshopError::Malformed {
180 message: message.into(),
181 span: None,
182 }
183 }
184
185 pub(crate) fn indent(&mut self, level: usize) {
186 for _ in 0..level {
187 self.out.push_str(" ");
188 }
189 }
190
191 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
192 self.indent(level);
193 self.out.push_str(text);
194 self.out.push('\n');
195 self.line_count += 1;
196 Ok(())
197 }
198}
199
200pub(crate) fn is_comparison_operator(name: &str) -> bool {
205 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
206}
207
208pub(crate) fn escape_string(value: &str) -> String {
209 value.replace('"', "\\\"")
210}
211
212pub(crate) fn escape_value_string(value: &str) -> String {
217 let mut out = String::with_capacity(value.len());
218 for ch in value.chars() {
219 match ch {
220 '\\' => out.push_str("\\\\"),
221 '"' => out.push_str("\\\""),
222 '\n' => out.push_str("\\n"),
223 '\r' => out.push_str("\\r"),
224 other => out.push(other),
225 }
226 }
227 out
228}
229
230pub(crate) fn split_string(value: &str) -> Vec<String> {
239 if value.chars().count() <= 128 {
240 return vec![escape_value_string(value)];
241 }
242 let mut segments = Vec::new();
243 let mut rest = value;
244 while rest.chars().count() > 125 {
245 let chunk: String = rest.chars().take(125).collect();
246 let mut text = escape_value_string(&chunk);
247 text.push_str("{0}");
248 segments.push(text);
249 rest = &rest[chunk.len()..];
250 }
251 if !rest.is_empty() {
252 segments.push(escape_value_string(rest));
253 }
254 segments
255}
256
257pub(crate) fn escape_settings_string(value: &str) -> String {
263 let mut out = String::with_capacity(value.len());
264 for ch in value.chars() {
265 match ch {
266 '\\' => out.push_str("\\\\"),
267 '"' => out.push_str("\\\""),
268 '\n' => out.push_str("\\n"),
269 '\t' => out.push_str("\\t"),
270 '\r' => out.push_str("\\r"),
271 other => out.push(other),
272 }
273 }
274 out
275}
276
277pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
283 let Some((first, rest)) = segments.split_first() else {
284 return;
285 };
286 out.push_str(spelling);
287 out.push('(');
288 write!(out, "\"{first}\"").unwrap();
289 for segment in rest {
290 out.push_str(", ");
291 out.push_str(spelling);
292 out.push('(');
293 write!(out, "\"{segment}\"").unwrap();
294 }
295 for _ in 0..=rest.len() {
296 out.push(')');
297 }
298}
299
300pub(crate) fn fold_number(value: f64) -> String {
304 if value.fract() == 0.0 && value.abs() < 1e15 {
305 format!("{}", value as i64)
306 } else {
307 let scaled = (value * 100.0).round();
308 let sign = if scaled < 0.0 { "-" } else { "" };
309 let scaled = scaled.abs() as i64;
310 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
311 }
312}