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::{self, KeyKind, PathPart};
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#[doc(hidden)]
48pub fn emit_wir(program: &wir::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
49 emit_with_options_inner(program, catalog, locale, &EmitOptions::default(), false)
50 .map(|out| out.text)
51}
52
53pub fn emit_with_options(
56 program: &crate::Program,
57 catalog: &Catalog,
58 locale: &Locale,
59 options: &EmitOptions,
60) -> Result<EmitOutput> {
61 let storage = program.to_wir()?;
62 emit_with_options_inner(&storage, catalog, locale, options, false)
63}
64
65pub(crate) fn emit_with_options_for_conversion(
66 program: &crate::Program,
67 catalog: &Catalog,
68 locale: &Locale,
69 options: &EmitOptions,
70) -> Result<EmitOutput> {
71 let storage = program.to_wir()?;
72 emit_with_options_inner(&storage, catalog, locale, options, true)
73}
74
75#[doc(hidden)]
76pub fn emit_wir_with_options(
77 program: &wir::Program,
78 catalog: &Catalog,
79 locale: &Locale,
80 options: &EmitOptions,
81) -> Result<EmitOutput> {
82 emit_with_options_inner(program, catalog, locale, options, false)
83}
84
85fn emit_with_options_inner(
86 program: &wir::Program,
87 catalog: &Catalog,
88 locale: &Locale,
89 options: &EmitOptions,
90 force_hero_constructors: bool,
91) -> Result<EmitOutput> {
92 let mut emitter = EmitContext {
93 program,
94 catalog,
95 locale: locale.clone(),
96 fallback: options.fallback_locale.clone(),
97 force_hero_constructors,
98 fallback_ids: Vec::new(),
99 out: String::new(),
100 line_count: 0,
101 };
102 emitter.run()?;
103 Ok(EmitOutput {
104 text: emitter.out,
105 fallback_ids: emitter.fallback_ids,
106 })
107}
108
109pub(crate) struct EmitContext<'a> {
110 pub(crate) program: &'a wir::Program,
111 pub(crate) catalog: &'a Catalog,
112 pub(crate) locale: Locale,
113 pub(crate) fallback: Option<Locale>,
115 pub(crate) fallback_ids: Vec<String>,
117 pub(crate) force_hero_constructors: bool,
118 pub(crate) out: String,
119 pub(crate) line_count: usize,
120}
121
122impl EmitContext<'_> {
123 pub(crate) fn run(&mut self) -> Result<()> {
124 if let Some(settings) = &self.program.settings {
126 self.emit_settings(settings)?;
127 self.out.push('\n');
128 }
129 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
130 let variables = self.structural("variables")?;
131 self.line(0, &format!("{variables} {{"))?;
132 if !self.program.global_variables.is_empty() {
133 let global = self.structural("global")?;
134 self.line(1, &format!("{global}:"))?;
135 for variable in self.program.global_variables.iter() {
136 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
137 }
138 }
139 if !self.program.player_variables.is_empty() {
140 let player = self.structural("player")?;
141 self.line(1, &format!("{player}:"))?;
142 for variable in self.program.player_variables.iter() {
143 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
144 }
145 }
146 self.line(0, "}")?;
147 self.out.push('\n');
148 }
149 if !self.program.subroutines.is_empty() {
150 let subroutines = self.structural("subroutines")?;
151 self.line(0, &format!("{subroutines} {{"))?;
152 for subroutine in self.program.subroutines.iter() {
153 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
154 }
155 self.line(0, "}")?;
156 self.out.push('\n');
157 }
158 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
159 if emitted_rules > 0 {
160 self.out.push('\n');
161 }
162 self.rule(rule)?;
163 }
164 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
168 self.out.push('\n');
169 }
170 Ok(())
171 }
172
173 pub(crate) fn malformed(&self, message: impl Into<String>) -> WorkshopError {
174 WorkshopError::Malformed {
175 message: message.into(),
176 span: None,
177 }
178 }
179
180 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
181 for _ in 0..level {
182 self.out.push_str(" ");
183 }
184 self.out.push_str(text);
185 self.out.push('\n');
186 self.line_count += 1;
187 Ok(())
188 }
189}
190
191pub(crate) fn is_comparison_operator(name: &str) -> bool {
196 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
197}
198
199pub(crate) fn escape_string(value: &str) -> String {
200 value.replace('"', "\\\"")
201}
202
203pub(crate) fn escape_value_string(value: &str) -> String {
208 let mut out = String::with_capacity(value.len());
209 for ch in value.chars() {
210 match ch {
211 '\\' => out.push_str("\\\\"),
212 '"' => out.push_str("\\\""),
213 '\n' => out.push_str("\\n"),
214 '\r' => out.push_str("\\r"),
215 other => out.push(other),
216 }
217 }
218 out
219}
220
221pub(crate) fn split_string(value: &str) -> Vec<String> {
230 if value.chars().count() <= 128 {
231 return vec![escape_value_string(value)];
232 }
233 let mut segments = Vec::new();
234 let mut rest = value;
235 while rest.chars().count() > 125 {
236 let chunk: String = rest.chars().take(125).collect();
237 let mut text = escape_value_string(&chunk);
238 text.push_str("{0}");
239 segments.push(text);
240 rest = &rest[chunk.len()..];
241 }
242 if !rest.is_empty() {
243 segments.push(escape_value_string(rest));
244 }
245 segments
246}
247
248pub(crate) fn escape_settings_string(value: &str) -> String {
254 let mut out = String::with_capacity(value.len());
255 for ch in value.chars() {
256 match ch {
257 '\\' => out.push_str("\\\\"),
258 '"' => out.push_str("\\\""),
259 '\n' => out.push_str("\\n"),
260 '\t' => out.push_str("\\t"),
261 '\r' => out.push_str("\\r"),
262 other => out.push(other),
263 }
264 }
265 out
266}
267
268pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
274 let Some((first, rest)) = segments.split_first() else {
275 return;
276 };
277 out.push_str(spelling);
278 out.push('(');
279 write!(out, "\"{first}\"").unwrap();
280 for segment in rest {
281 out.push_str(", ");
282 out.push_str(spelling);
283 out.push('(');
284 write!(out, "\"{segment}\"").unwrap();
285 }
286 for _ in 0..=rest.len() {
287 out.push(')');
288 }
289}
290
291pub(crate) fn fold_number(value: f64) -> String {
295 if value.fract() == 0.0 && value.abs() < 1e15 {
296 format!("{}", value as i64)
297 } else {
298 let scaled = (value * 100.0).round();
299 let sign = if scaled < 0.0 { "-" } else { "" };
300 let scaled = scaled.abs() as i64;
301 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
302 }
303}