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: &wir::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
44 emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
45}
46
47pub fn emit_with_options(
50 program: &wir::Program,
51 catalog: &Catalog,
52 locale: &Locale,
53 options: &EmitOptions,
54) -> Result<EmitOutput> {
55 emit_with_options_inner(program, catalog, locale, options, false)
56}
57
58pub(crate) fn emit_with_options_for_conversion(
59 program: &wir::Program,
60 catalog: &Catalog,
61 locale: &Locale,
62 options: &EmitOptions,
63) -> Result<EmitOutput> {
64 emit_with_options_inner(program, catalog, locale, options, true)
65}
66
67fn emit_with_options_inner(
68 program: &wir::Program,
69 catalog: &Catalog,
70 locale: &Locale,
71 options: &EmitOptions,
72 force_hero_constructors: bool,
73) -> Result<EmitOutput> {
74 let mut emitter = EmitContext {
75 program,
76 catalog,
77 locale: locale.clone(),
78 fallback: options.fallback_locale.clone(),
79 force_hero_constructors,
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>,
97 pub(crate) fallback_ids: Vec<String>,
99 pub(crate) force_hero_constructors: bool,
100 pub(crate) out: String,
101 pub(crate) line_count: usize,
102}
103
104impl EmitContext<'_> {
105 pub(crate) fn run(&mut self) -> Result<()> {
106 if let Some(settings) = &self.program.settings {
108 self.emit_settings(settings)?;
109 self.out.push('\n');
110 }
111 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
112 let variables = self.structural("variables")?;
113 self.line(0, &format!("{variables} {{"))?;
114 if !self.program.global_variables.is_empty() {
115 let global = self.structural("global")?;
116 self.line(1, &format!("{global}:"))?;
117 for variable in self.program.global_variables.iter() {
118 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
119 }
120 }
121 if !self.program.player_variables.is_empty() {
122 let player = self.structural("player")?;
123 self.line(1, &format!("{player}:"))?;
124 for variable in self.program.player_variables.iter() {
125 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
126 }
127 }
128 self.line(0, "}")?;
129 self.out.push('\n');
130 }
131 if !self.program.subroutines.is_empty() {
132 let subroutines = self.structural("subroutines")?;
133 self.line(0, &format!("{subroutines} {{"))?;
134 for subroutine in self.program.subroutines.iter() {
135 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
136 }
137 self.line(0, "}")?;
138 self.out.push('\n');
139 }
140 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
141 if emitted_rules > 0 {
142 self.out.push('\n');
143 }
144 self.rule(rule)?;
145 }
146 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
150 self.out.push('\n');
151 }
152 Ok(())
153 }
154
155 pub(crate) fn malformed(&self, message: impl Into<String>) -> WorkshopError {
156 WorkshopError::Malformed {
157 message: message.into(),
158 span: None,
159 }
160 }
161
162 pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
163 for _ in 0..level {
164 self.out.push_str(" ");
165 }
166 self.out.push_str(text);
167 self.out.push('\n');
168 self.line_count += 1;
169 Ok(())
170 }
171}
172
173pub(crate) fn is_comparison_operator(name: &str) -> bool {
178 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
179}
180
181pub(crate) fn escape_string(value: &str) -> String {
182 value.replace('"', "\\\"")
183}
184
185pub(crate) fn escape_value_string(value: &str) -> String {
190 let mut out = String::with_capacity(value.len());
191 for ch in value.chars() {
192 match ch {
193 '\\' => out.push_str("\\\\"),
194 '"' => out.push_str("\\\""),
195 '\n' => out.push_str("\\n"),
196 '\r' => out.push_str("\\r"),
197 other => out.push(other),
198 }
199 }
200 out
201}
202
203pub(crate) fn split_string(value: &str) -> Vec<String> {
212 if value.chars().count() <= 128 {
213 return vec![escape_value_string(value)];
214 }
215 let mut segments = Vec::new();
216 let mut rest = value;
217 while rest.chars().count() > 125 {
218 let chunk: String = rest.chars().take(125).collect();
219 let mut text = escape_value_string(&chunk);
220 text.push_str("{0}");
221 segments.push(text);
222 rest = &rest[chunk.len()..];
223 }
224 if !rest.is_empty() {
225 segments.push(escape_value_string(rest));
226 }
227 segments
228}
229
230pub(crate) fn escape_settings_string(value: &str) -> String {
236 let mut out = String::with_capacity(value.len());
237 for ch in value.chars() {
238 match ch {
239 '\\' => out.push_str("\\\\"),
240 '"' => out.push_str("\\\""),
241 '\n' => out.push_str("\\n"),
242 '\t' => out.push_str("\\t"),
243 '\r' => out.push_str("\\r"),
244 other => out.push(other),
245 }
246 }
247 out
248}
249
250pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
256 let Some((first, rest)) = segments.split_first() else {
257 return;
258 };
259 out.push_str(spelling);
260 out.push('(');
261 write!(out, "\"{first}\"").unwrap();
262 for segment in rest {
263 out.push_str(", ");
264 out.push_str(spelling);
265 out.push('(');
266 write!(out, "\"{segment}\"").unwrap();
267 }
268 for _ in 0..=rest.len() {
269 out.push(')');
270 }
271}
272
273pub(crate) fn fold_number(value: f64) -> String {
277 if value.fract() == 0.0 && value.abs() < 1e15 {
278 format!("{}", value as i64)
279 } else {
280 let scaled = (value * 100.0).round();
281 let sign = if scaled < 0.0 { "-" } else { "" };
282 let scaled = scaled.abs() as i64;
283 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
284 }
285}