pub(crate) use std::fmt::Write;
pub(crate) use crate::catalog::{Catalog, Kind, Locale};
pub(crate) use crate::core::error::{Result, WorkshopError};
pub(crate) use crate::core::format::format_number;
pub(crate) use crate::settings::table::{self, KeyKind, PathPart};
pub(crate) use crate::settings::{Settings as SettingsTree, SettingsNode};
pub(crate) use crate::wir;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EmitOptions {
pub fallback_locale: Option<Locale>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmitOutput {
pub text: String,
pub fallback_ids: Vec<String>,
}
pub fn emit(program: &crate::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
}
#[doc(hidden)]
pub fn emit_wir(program: &wir::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
emit_with_options_inner(program, catalog, locale, &EmitOptions::default(), false)
.map(|out| out.text)
}
pub fn emit_with_options(
program: &crate::Program,
catalog: &Catalog,
locale: &Locale,
options: &EmitOptions,
) -> Result<EmitOutput> {
let storage = program.to_wir()?;
emit_with_options_inner(&storage, catalog, locale, options, false)
}
pub(crate) fn emit_with_options_for_conversion(
program: &crate::Program,
catalog: &Catalog,
locale: &Locale,
options: &EmitOptions,
) -> Result<EmitOutput> {
let storage = program.to_wir()?;
emit_with_options_inner(&storage, catalog, locale, options, true)
}
#[doc(hidden)]
pub fn emit_wir_with_options(
program: &wir::Program,
catalog: &Catalog,
locale: &Locale,
options: &EmitOptions,
) -> Result<EmitOutput> {
emit_with_options_inner(program, catalog, locale, options, false)
}
fn emit_with_options_inner(
program: &wir::Program,
catalog: &Catalog,
locale: &Locale,
options: &EmitOptions,
force_hero_constructors: bool,
) -> Result<EmitOutput> {
let mut emitter = EmitContext {
program,
catalog,
locale: locale.clone(),
fallback: options.fallback_locale.clone(),
force_hero_constructors,
fallback_ids: Vec::new(),
out: String::new(),
line_count: 0,
};
emitter.run()?;
Ok(EmitOutput {
text: emitter.out,
fallback_ids: emitter.fallback_ids,
})
}
pub(crate) struct EmitContext<'a> {
pub(crate) program: &'a wir::Program,
pub(crate) catalog: &'a Catalog,
pub(crate) locale: Locale,
pub(crate) fallback: Option<Locale>,
pub(crate) fallback_ids: Vec<String>,
pub(crate) force_hero_constructors: bool,
pub(crate) out: String,
pub(crate) line_count: usize,
}
impl EmitContext<'_> {
pub(crate) fn run(&mut self) -> Result<()> {
if let Some(settings) = &self.program.settings {
self.emit_settings(settings)?;
self.out.push('\n');
}
if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
let variables = self.structural("variables")?;
self.line(0, &format!("{variables} {{"))?;
if !self.program.global_variables.is_empty() {
let global = self.structural("global")?;
self.line(1, &format!("{global}:"))?;
for variable in self.program.global_variables.iter() {
self.line(2, &format!("{}: {}", variable.index, variable.name))?;
}
}
if !self.program.player_variables.is_empty() {
let player = self.structural("player")?;
self.line(1, &format!("{player}:"))?;
for variable in self.program.player_variables.iter() {
self.line(2, &format!("{}: {}", variable.index, variable.name))?;
}
}
self.line(0, "}")?;
self.out.push('\n');
}
if !self.program.subroutines.is_empty() {
let subroutines = self.structural("subroutines")?;
self.line(0, &format!("{subroutines} {{"))?;
for subroutine in self.program.subroutines.iter() {
self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
}
self.line(0, "}")?;
self.out.push('\n');
}
for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
if emitted_rules > 0 {
self.out.push('\n');
}
self.rule(rule)?;
}
if !self.out.is_empty() && !self.out.ends_with("\n\n") {
self.out.push('\n');
}
Ok(())
}
pub(crate) fn malformed(&self, message: impl Into<String>) -> WorkshopError {
WorkshopError::Malformed {
message: message.into(),
span: None,
}
}
pub(crate) fn line(&mut self, level: usize, text: &str) -> Result<()> {
for _ in 0..level {
self.out.push_str(" ");
}
self.out.push_str(text);
self.out.push('\n');
self.line_count += 1;
Ok(())
}
}
pub(crate) fn is_comparison_operator(name: &str) -> bool {
matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
}
pub(crate) fn escape_string(value: &str) -> String {
value.replace('"', "\\\"")
}
pub(crate) fn escape_value_string(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
other => out.push(other),
}
}
out
}
pub(crate) fn split_string(value: &str) -> Vec<String> {
if value.chars().count() <= 128 {
return vec![escape_value_string(value)];
}
let mut segments = Vec::new();
let mut rest = value;
while rest.chars().count() > 125 {
let chunk: String = rest.chars().take(125).collect();
let mut text = escape_value_string(&chunk);
text.push_str("{0}");
segments.push(text);
rest = &rest[chunk.len()..];
}
if !rest.is_empty() {
segments.push(escape_value_string(rest));
}
segments
}
pub(crate) fn escape_settings_string(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
other => out.push(other),
}
}
out
}
pub(crate) fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
let Some((first, rest)) = segments.split_first() else {
return;
};
out.push_str(spelling);
out.push('(');
write!(out, "\"{first}\"").unwrap();
for segment in rest {
out.push_str(", ");
out.push_str(spelling);
out.push('(');
write!(out, "\"{segment}\"").unwrap();
}
for _ in 0..=rest.len() {
out.push(')');
}
}
pub(crate) fn fold_number(value: f64) -> String {
if value.fract() == 0.0 && value.abs() < 1e15 {
format!("{}", value as i64)
} else {
let scaled = (value * 100.0).round();
let sign = if scaled < 0.0 { "-" } else { "" };
let scaled = scaled.abs() as i64;
format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
}
}