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