use crate::game::eu4::script::parser::{AstStatement, AstValue, ScalarValue};
use crate::merge::MergeError;
use crate::project::DEFAULT_EMIT_INDENT;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct EmitOptions {
indent: String,
drop_unread_decimals: bool,
}
impl EmitOptions {
pub(crate) fn with_indent(indent: impl Into<String>) -> Self {
Self {
indent: indent.into(),
drop_unread_decimals: true,
}
}
pub(crate) fn indent(&self) -> &str {
&self.indent
}
pub(crate) fn drops_unread_decimals(&self) -> bool {
self.drop_unread_decimals
}
pub(crate) fn with_unread_decimals_dropped(mut self, drop: bool) -> Self {
self.drop_unread_decimals = drop;
self
}
}
impl Default for EmitOptions {
fn default() -> Self {
Self::with_indent(DEFAULT_EMIT_INDENT)
}
}
pub(crate) fn emit_clausewitz_statements(
statements: &[AstStatement],
) -> Result<String, MergeError> {
emit_clausewitz_statements_with_options(statements, &EmitOptions::default())
}
pub fn emit_native_statements(statements: &[AstStatement]) -> Result<String, String> {
emit_clausewitz_statements(statements).map_err(|error| error.to_string())
}
pub(crate) fn emit_clausewitz_statements_with_options(
statements: &[AstStatement],
options: &EmitOptions,
) -> Result<String, MergeError> {
let mut out = String::new();
emit_statements(statements, 0, &mut out, options)?;
Ok(out)
}
fn emit_statements(
statements: &[AstStatement],
indent: usize,
out: &mut String,
options: &EmitOptions,
) -> Result<(), MergeError> {
for statement in statements {
emit_statement(statement, indent, out, options)?;
}
Ok(())
}
fn emit_statement(
statement: &AstStatement,
indent: usize,
out: &mut String,
options: &EmitOptions,
) -> Result<(), MergeError> {
match statement {
AstStatement::Assignment { key, value, .. } => {
indent_into(out, indent, options.indent());
out.push_str(key);
out.push_str(" = ");
emit_value(value, indent, out, options)?;
out.push('\n');
Ok(())
}
AstStatement::Item { value, .. } => {
indent_into(out, indent, options.indent());
emit_value(value, indent, out, options)?;
out.push('\n');
Ok(())
}
AstStatement::Comment { text, .. } => {
indent_into(out, indent, options.indent());
out.push_str("# ");
out.push_str(text);
out.push('\n');
Ok(())
}
}
}
fn emit_value(
value: &AstValue,
indent: usize,
out: &mut String,
options: &EmitOptions,
) -> Result<(), MergeError> {
match value {
AstValue::Scalar { value, .. } => {
out.push_str(&render_scalar(value));
Ok(())
}
AstValue::Block { items, .. } => {
out.push_str("{\n");
emit_statements(items, indent + 1, out, options)?;
indent_into(out, indent, options.indent());
out.push('}');
Ok(())
}
}
}
fn render_scalar(value: &ScalarValue) -> String {
match value {
ScalarValue::Identifier(value) => value.clone(),
ScalarValue::String(value) => format!("\"{value}\""),
ScalarValue::Number(value) => value.clone(),
ScalarValue::Bool(value) => {
if *value {
"yes".to_string()
} else {
"no".to_string()
}
}
}
}
fn indent_into(out: &mut String, indent: usize, indent_text: &str) {
for _ in 0..indent {
out.push_str(indent_text);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::eu4::script::parser::{Span, SpanRange};
#[test]
fn default_emit_options_keep_tab_indentation() {
let statements = nested_statements();
let emitted = emit_clausewitz_statements(&statements).expect("emit statements");
let explicit_default = emit_clausewitz_statements_with_options(
&statements,
&EmitOptions::with_indent(DEFAULT_EMIT_INDENT),
)
.expect("emit statements with default options");
let expected = "root = {\n\tchild = {\n\t\tleaf = yes\n\t}\n}\n";
assert_eq!(emitted, expected);
assert_eq!(explicit_default, expected);
}
#[test]
fn custom_emit_options_indent_each_level_verbatim() {
let emitted = emit_clausewitz_statements_with_options(
&nested_statements(),
&EmitOptions::with_indent(" "),
)
.expect("emit statements with custom options");
assert_eq!(emitted, "root = {\n child = {\n leaf = yes\n }\n}\n");
}
#[test]
fn string_emission_preserves_clausewitz_escape_text() {
let statements = vec![
assignment(
"textureFile",
scalar(ScalarValue::String(
r"gfx\\interface\\small_tiles_dialog.dds".to_string(),
)),
),
assignment(
"tooltip",
scalar(ScalarValue::String(r#"say \"hello\""#.to_string())),
),
];
let emitted = emit_clausewitz_statements(&statements).expect("emit string values");
assert_eq!(
emitted,
r#"textureFile = "gfx\\interface\\small_tiles_dialog.dds"
tooltip = "say \"hello\""
"#
);
}
fn nested_statements() -> Vec<AstStatement> {
vec![assignment(
"root",
block(vec![assignment(
"child",
block(vec![assignment("leaf", scalar(ScalarValue::Bool(true)))]),
)]),
)]
}
fn assignment(key: &str, value: AstValue) -> AstStatement {
AstStatement::Assignment {
key: key.to_string(),
key_span: span(),
value,
span: span(),
}
}
fn block(items: Vec<AstStatement>) -> AstValue {
AstValue::Block {
items,
span: span(),
}
}
fn scalar(value: ScalarValue) -> AstValue {
AstValue::Scalar {
value,
span: span(),
}
}
fn span() -> SpanRange {
SpanRange {
start: Span {
line: 0,
column: 0,
offset: 0,
},
end: Span {
line: 0,
column: 0,
offset: 0,
},
}
}
}