1pub(crate) use std::fmt::Write;
17
18pub(crate) use crate::catalog::{Catalog, Kind, Locale};
19pub(crate) use crate::core::error::{Result, WorkshopError};
20pub(crate) use crate::core::format::format_number;
21pub(crate) use crate::settings::table::{self, KeyKind, PathPart};
22pub(crate) use crate::settings::{Settings as SettingsTree, SettingsNode};
23pub(crate) use crate::wir;
24
25#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct EmitOptions {
28 pub fallback_locale: Option<Locale>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct EmitOutput {
38 pub text: String,
40 pub fallback_ids: Vec<String>,
44}
45
46pub fn emit(program: &wir::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
49 emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
50}
51
52pub fn emit_with_options(
55 program: &wir::Program,
56 catalog: &Catalog,
57 locale: &Locale,
58 options: &EmitOptions,
59) -> Result<EmitOutput> {
60 emit_with_options_inner(program, catalog, locale, options, false)
61}
62
63pub(crate) fn emit_with_options_for_conversion(
64 program: &wir::Program,
65 catalog: &Catalog,
66 locale: &Locale,
67 options: &EmitOptions,
68) -> Result<EmitOutput> {
69 emit_with_options_inner(program, catalog, locale, options, true)
70}
71
72fn emit_with_options_inner(
73 program: &wir::Program,
74 catalog: &Catalog,
75 locale: &Locale,
76 options: &EmitOptions,
77 force_hero_constructors: bool,
78) -> Result<EmitOutput> {
79 let mut emitter = EmitContext {
80 program,
81 catalog,
82 locale: locale.clone(),
83 fallback: options.fallback_locale.clone(),
84 force_hero_constructors,
85 fallback_ids: Vec::new(),
86 out: String::new(),
87 line_count: 0,
88 };
89 emitter.run()?;
90 Ok(EmitOutput {
91 text: emitter.out,
92 fallback_ids: emitter.fallback_ids,
93 })
94}
95
96pub(crate) struct EmitContext<'a> {
97 pub(crate) program: &'a wir::Program,
98 pub(crate) catalog: &'a Catalog,
99 pub(crate) locale: Locale,
100 pub(crate) fallback: Option<Locale>,
102 pub(crate) fallback_ids: Vec<String>,
104 pub(crate) force_hero_constructors: bool,
105 pub(crate) out: String,
106 pub(crate) line_count: usize,
107}
108
109impl EmitContext<'_> {
110 pub(crate) fn run(&mut self) -> Result<()> {
111 if let Some(settings) = &self.program.settings {
113 self.emit_settings(settings)?;
114 self.out.push('\n');
115 }
116 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
117 let variables = self.structural("variables")?;
118 self.line(0, &format!("{variables} {{"))?;
119 if !self.program.global_variables.is_empty() {
120 let global = self.structural("global")?;
121 self.line(1, &format!("{global}:"))?;
122 for variable in self.program.global_variables.iter() {
123 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
124 }
125 }
126 if !self.program.player_variables.is_empty() {
127 let player = self.structural("player")?;
128 self.line(1, &format!("{player}:"))?;
129 for variable in self.program.player_variables.iter() {
130 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
131 }
132 }
133 self.line(0, "}")?;
134 self.out.push('\n');
135 }
136 if !self.program.subroutines.is_empty() {
137 let subroutines = self.structural("subroutines")?;
138 self.line(0, &format!("{subroutines} {{"))?;
139 for subroutine in self.program.subroutines.iter() {
140 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
141 }
142 self.line(0, "}")?;
143 self.out.push('\n');
144 }
145 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
146 if emitted_rules > 0 {
147 self.out.push('\n');
148 }
149 self.rule(rule)?;
150 }
151 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
155 self.out.push('\n');
156 }
157 Ok(())
158 }
159
160 pub(crate) fn malformed(&self, message: impl Into<String>) -> WorkshopError {
161 WorkshopError::Malformed {
162 message: message.into(),
163 span: None,
164 }
165 }
166
167 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
168 for _ in 0..level {
169 self.out.push_str(" ");
170 }
171 self.out.push_str(text);
172 self.out.push('\n');
173 self.line_count += 1;
174 Ok(())
175 }
176}
177
178pub(crate) fn is_comparison_operator(name: &str) -> bool {
183 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
184}
185
186pub(crate) fn escape_string(value: &str) -> String {
187 value.replace('"', "\\\"")
188}
189
190pub(crate) fn escape_value_string(value: &str) -> String {
195 let mut out = String::with_capacity(value.len());
196 for ch in value.chars() {
197 match ch {
198 '\\' => out.push_str("\\\\"),
199 '"' => out.push_str("\\\""),
200 '\n' => out.push_str("\\n"),
201 '\r' => out.push_str("\\r"),
202 other => out.push(other),
203 }
204 }
205 out
206}
207
208pub(crate) fn split_string(value: &str) -> Vec<String> {
217 if value.chars().count() <= 128 {
218 return vec![escape_value_string(value)];
219 }
220 let mut segments = Vec::new();
221 let mut rest = value;
222 while rest.chars().count() > 125 {
223 let chunk: String = rest.chars().take(125).collect();
224 let mut text = escape_value_string(&chunk);
225 text.push_str("{0}");
226 segments.push(text);
227 rest = &rest[chunk.len()..];
228 }
229 if !rest.is_empty() {
230 segments.push(escape_value_string(rest));
231 }
232 segments
233}
234
235pub(crate) fn escape_settings_string(value: &str) -> String {
241 let mut out = String::with_capacity(value.len());
242 for ch in value.chars() {
243 match ch {
244 '\\' => out.push_str("\\\\"),
245 '"' => out.push_str("\\\""),
246 '\n' => out.push_str("\\n"),
247 '\t' => out.push_str("\\t"),
248 '\r' => out.push_str("\\r"),
249 other => out.push(other),
250 }
251 }
252 out
253}
254
255pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
261 let Some((first, rest)) = segments.split_first() else {
262 return;
263 };
264 out.push_str(spelling);
265 out.push('(');
266 write!(out, "\"{first}\"").unwrap();
267 for segment in rest {
268 out.push_str(", ");
269 out.push_str(spelling);
270 out.push('(');
271 write!(out, "\"{segment}\"").unwrap();
272 }
273 for _ in 0..=rest.len() {
274 out.push(')');
275 }
276}
277
278pub(crate) fn fold_number(value: f64) -> String {
282 if value.fract() == 0.0 && value.abs() < 1e15 {
283 format!("{}", value as i64)
284 } else {
285 let scaled = (value * 100.0).round();
286 let sign = if scaled < 0.0 { "-" } else { "" };
287 let scaled = scaled.abs() as i64;
288 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
289 }
290}