use crate::Compiler::Core::Config::OperationalSettings;
use crate::Compiler::Core::Tokenizer::{Token, TokenType, Tokenizer};
#[inline]
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[inline]
fn is_grouped_entry_separator(c: char) -> bool {
matches!(c, '(' | '[' | '{' | ')' | ']' | '}' | '=' | ':' | '.')
}
#[inline]
fn renders_empty(token: &Token) -> bool {
matches!(
token.token_type,
TokenType::Comment(_) | TokenType::EndOfFile
)
}
#[inline]
fn skip_empty_from(tokens: &[Token], from: usize) -> Option<usize> {
let mut i = from;
while i < tokens.len() && renders_empty(&tokens[i]) {
i += 1;
}
if i < tokens.len() {
Some(i)
} else {
None
}
}
fn is_next_grouped_entry(tokens: &[Token], from: usize) -> bool {
let Some(mut i) = skip_empty_from(tokens, from) else {
return false;
};
if !matches!(tokens[i].token_type, TokenType::Identifier(_)) {
return false;
}
i += 1;
loop {
let Some(j) = skip_empty_from(tokens, i) else {
return false;
};
i = j;
match &tokens[i].token_type {
TokenType::DoubleColon => return true,
TokenType::Symbol(':') => return true,
TokenType::Symbol('.') => {
i += 1;
let Some(k) = skip_empty_from(tokens, i) else {
return false;
};
i = k;
if !matches!(tokens[i].token_type, TokenType::Identifier(_)) {
return false;
}
i += 1;
}
_ => return false,
}
}
}
fn render_token(token: &Token) -> String {
match &token.token_type {
TokenType::Double(d) => {
if d.is_finite() && d.fract() == 0.0 {
format!("{:.1}", d) } else {
format!("{}", d)
}
}
TokenType::Float(f) => {
format!("{}f", f)
}
TokenType::String(s) => format!("\"{}\"", escape_for_reserialization(s, '"')),
TokenType::StringSingle(s) => format!("'{}'", escape_for_reserialization(s, '\'')),
TokenType::InterpolatedString(s) => format!("$\"{}\"", escape_for_reserialization(s, '"')),
TokenType::SectionConfig => "@CONFIG".to_string(),
TokenType::SectionImports => "@IMPORTS".to_string(),
TokenType::SectionDLM => "@DLM".to_string(),
TokenType::SectionEnums => "@ENUMS".to_string(),
TokenType::SectionQuickFuncs => "@QUICKFUNCS".to_string(),
TokenType::SectionData => "@DATA".to_string(),
TokenType::SectionSecurity => "@SECURITY".to_string(),
TokenType::Comment(_) | TokenType::EndOfFile => {
String::new()
}
_ => token.get_token_value(),
}
}
fn escape_for_reserialization(s: &str, quote: char) -> String {
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\0' => out.push_str("\\0"),
c if c == quote => { out.push('\\'); out.push(c); }
c => out.push(c),
}
}
out
}
pub struct DixCompactor;
impl DixCompactor {
pub fn minify(content: &str) -> String {
if content.trim().is_empty() {
return String::new();
}
let settings = OperationalSettings::default();
let tokenizer = Tokenizer::new(content, &settings);
let tok_result = tokenizer.tokenize();
let tokens = &tok_result.tokens;
let mut result = String::with_capacity(content.len());
let mut prev_rendered: Option<String> = None;
let mut force_space = false;
let mut i = 0;
while i < tokens.len() {
let token = &tokens[i];
if matches!(token.token_type, TokenType::Symbol(','))
&& is_next_grouped_entry(tokens, i + 1) {
force_space = true;
i += 1;
continue;
}
if !force_space && prev_rendered.is_some() && is_next_grouped_entry(tokens, i) {
let prev = prev_rendered.as_deref().unwrap_or("");
if let Some(last) = prev.chars().last() {
if !is_grouped_entry_separator(last) {
force_space = true;
}
}
}
let rendered = render_token(token);
if rendered.is_empty() {
i += 1;
continue;
}
if prev_rendered.is_some() {
let prev = prev_rendered.as_deref().unwrap_or("");
let prev_ends_word = prev.chars().last().map(is_word_char).unwrap_or(false);
let curr_starts_word = rendered.chars().next().map(is_word_char).unwrap_or(false);
if force_space || (prev_ends_word && curr_starts_word) {
result.push(' ');
}
}
force_space = false;
result.push_str(&rendered);
prev_rendered = Some(rendered);
i += 1;
}
result
}
pub fn compact(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut result = String::with_capacity(content.len());
let mut consecutive_blank = 0usize;
for line in &lines {
let trimmed = line.trim_end();
if trimmed.is_empty() {
consecutive_blank += 1;
if consecutive_blank <= 1 {
result.push('\n');
}
} else {
consecutive_blank = 0;
result.push_str(trimmed);
result.push('\n');
}
}
result
}
pub fn remove_comments(content: &str) -> String {
let mut result = String::with_capacity(content.len());
let chars: Vec<char> = content.chars().collect();
let mut i = 0;
let mut in_string = false;
let mut string_char = '\0';
while i < chars.len() {
let c = chars[i];
let next = if i + 1 < chars.len() { chars[i + 1] } else { '\0' };
let prev = if i > 0 { chars[i - 1] } else { '\0' };
if (c == '"' || c == '\'') && prev != '\\' {
if !in_string {
in_string = true;
string_char = c;
} else if c == string_char {
in_string = false;
}
result.push(c);
i += 1;
continue;
}
if in_string {
result.push(c);
i += 1;
continue;
}
if c == '/' && next == '/' {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
continue;
}
if c == '/' && next == '*' {
i += 2;
while i + 1 < chars.len() {
if chars[i] == '*' && chars[i + 1] == '/' {
i += 2;
break;
}
i += 1;
}
continue;
}
result.push(c);
i += 1;
}
result
}
pub fn get_compression_ratio(original: &str, compressed: &str) -> f64 {
if original.is_empty() {
return 0.0;
}
1.0 - (compressed.len() as f64 / original.len() as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minify_basic_config() {
let input = "@CONFIG(\n version -> \"1.0.0\"\n)";
let output = DixCompactor::minify(input);
assert_eq!(output, "@CONFIG(version->\"1.0.0\")");
}
#[test]
fn test_minify_preserves_strings_with_spaces() {
let input = "name = \"Hello World\"";
let output = DixCompactor::minify(input);
assert_eq!(output, "name=\"Hello World\"");
}
#[test]
fn test_minify_keeps_space_between_let_and_identifier() {
let input = "let x = 5";
let output = DixCompactor::minify(input);
assert_eq!(output, "let x=5");
}
#[test]
fn test_minify_no_fusion_integer_table_path() {
let input = "@DATA(\n count = 789\n table: host = \"x\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains("789table"),
"integer and table-path fused — got: {output}"
);
assert!(
output.contains("789 table:"),
"expected '789 table:' in output, got: {output}"
);
}
#[test]
fn test_minify_no_fusion_bool_identifier() {
let input = "@DATA(\n flag = true\n other = 5\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains("trueother"),
"bool and identifier fused — got: {output}"
);
assert!(
output.contains("true") && output.contains("other"),
"tokens gone — got: {output}"
);
}
#[test]
fn test_minify_deep_indentation_no_fusion() {
let input = "@DATA(\n deeply = 1\n nested = 2\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains("1nested"),
"deep-indent tokens fused — got: {output}"
);
}
#[test]
fn test_minify_keyword_identifier_space() {
let input = "let result = 42";
let output = DixCompactor::minify(input);
assert!(output.contains("let result"), "got: {output}");
}
#[test]
fn test_minify_strips_single_line_comments() {
let input = "x = 5 // comment\ny = 10";
let output = DixCompactor::minify(input);
assert!(!output.contains("comment"), "got: {output}");
assert!(output.contains("x=5"), "got: {output}");
assert!(output.contains("y=10"), "got: {output}");
}
#[test]
fn test_minify_strips_multi_line_comments() {
let input = "x = 5 /* a multi\nline comment */ y = 10";
let output = DixCompactor::minify(input);
assert!(!output.contains("multi"), "got: {output}");
assert!(output.contains("x=5"), "got: {output}");
assert!(output.contains("y=10"), "got: {output}");
}
#[test]
fn test_minify_preserves_url_in_string() {
let input = "url = \"https://example.com/path\"";
let output = DixCompactor::minify(input);
assert!(
output.contains("https://example.com/path"),
"URL inside string was incorrectly stripped — got: {output}"
);
}
#[test]
fn test_minify_arrow_operator_no_spaces() {
let input = "@CONFIG(\n version -> \"2.0\"\n)";
let output = DixCompactor::minify(input);
assert!(output.contains("version->\"2.0\""), "got: {output}");
}
#[test]
fn test_minify_double_colon_array() {
let input = "@DATA(\n tags:: \"a\", \"b\"\n)";
let output = DixCompactor::minify(input);
assert!(output.contains("tags::"), "got: {output}");
}
#[test]
fn test_minify_empty_and_whitespace_only() {
assert_eq!(DixCompactor::minify(""), "");
assert_eq!(DixCompactor::minify(" \n \n"), "");
}
#[test]
fn test_minify_only_comments_returns_empty() {
let input = "// single line\n/* multi\nline */";
let output = DixCompactor::minify(input);
assert!(output.trim().is_empty(), "expected empty, got: {output:?}");
}
#[test]
fn test_minify_preserves_single_quoted_string() {
let input = "@DATA(\n name = 'Hello World'\n)";
let output = DixCompactor::minify(input);
assert!(
output.contains("'Hello World'"),
"single-quoted string lost its delimiters — got: {output}"
);
}
#[test]
fn test_minify_data_section_keyword() {
let input = "@DATA(\n x = 1\n)";
let output = DixCompactor::minify(input);
assert!(
output.starts_with("@DATA("),
"expected output to start with '@DATA(', got: {output}"
);
assert!(
!output.contains("SectionData"),
"Display fallback leaked into minified output — got: {output}"
);
}
#[test]
fn test_minify_idempotent_on_already_minified_config() {
let once = DixCompactor::minify("@CONFIG(\n version -> \"1.0.0\"\n)");
let twice = DixCompactor::minify(&once);
assert_eq!(once, twice, "minify should be idempotent: {once} vs {twice}");
}
#[test]
fn test_minify_replaces_comma_before_table_property_with_space() {
let input = "@DATA(\n count = 1,\n host: key = \"v\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",host"),
"comma leaked before table-property — got: {output}"
);
assert!(
output.contains(" host:") || output.contains("1 host:"),
"no space before table-property — got: {output}"
);
assert!(
output.contains("host:"),
"table-property missing — got: {output}"
);
}
#[test]
fn test_minify_replaces_comma_before_dotted_table_property() {
let input = "@DATA(\n count = 1,\n db.host: port = 5432\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",db"),
"comma leaked before dotted table-property — got: {output}"
);
assert!(
output.contains("db.host:") || output.contains("db.host :"),
"dotted table-property missing — got: {output}"
);
}
#[test]
fn test_minify_replaces_comma_before_group_array_with_space() {
let input = "@DATA(\n x = 1,\n tags:: \"a\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",tags"),
"comma leaked before group-array — got: {output}"
);
assert!(
output.contains(" tags::") || output.contains("1 tags::"),
"no space before group-array — got: {output}"
);
assert!(
output.contains("tags::"),
"group-array missing — got: {output}"
);
}
#[test]
fn test_minify_replaces_comma_before_dotted_group_array() {
let input = "@DATA(\n x = 1,\n db.tags:: \"a\", \"b\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",db"),
"comma leaked before dotted group-array — got: {output}"
);
assert!(
output.contains("db.tags::"),
"dotted group-array missing — got: {output}"
);
}
#[test]
fn test_minify_replaces_comma_between_table_properties() {
let input = "@DATA(\n db: host = \"a\",\n cache: host = \"b\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",cache"),
"comma leaked between table-properties — got: {output}"
);
assert!(
output.contains("db:") && output.contains("cache:"),
"a table-property is missing — got: {output}"
);
assert!(
output.contains(" cache:"),
"no space between table-property blocks — got: {output}"
);
}
#[test]
fn test_minify_replaces_comma_between_group_arrays() {
let input = "@DATA(\n tags:: \"a\",\n flags:: true\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",flags"),
"comma leaked between group-arrays — got: {output}"
);
assert!(
output.contains("tags::") && output.contains("flags::"),
"a group-array is missing — got: {output}"
);
assert!(
output.contains(" flags::"),
"no space between group-array declarations — got: {output}"
);
}
#[test]
fn test_minify_keeps_comma_within_group_array_items() {
let input = "@DATA(\n tags:: \"a\", \"b\", \"c\"\n)";
let output = DixCompactor::minify(input);
assert!(output.contains("\"a\""), "got: {output}");
assert!(output.contains("\"b\""), "got: {output}");
assert!(output.contains("\"c\""), "got: {output}");
assert!(
output.contains("\"a\",\"b\"") || output.contains("\"a\", \"b\""),
"comma between group-array items was incorrectly dropped — got: {output}"
);
assert!(
output.contains("\"b\",\"c\"") || output.contains("\"b\", \"c\""),
"second comma between group-array items was incorrectly dropped — got: {output}"
);
}
#[test]
fn test_minify_keeps_comma_within_table_property_assignments() {
let input = "@DATA(\n db: host = \"a\", port = 5432\n)";
let output = DixCompactor::minify(input);
assert!(
output.contains("host=") && output.contains("port="),
"an assignment was dropped — got: {output}"
);
assert!(
output.contains(",port") || output.contains(", port"),
"comma between table-property assignments was incorrectly dropped — got: {output}"
);
}
#[test]
fn test_minify_space_after_string_value_before_table_property() {
let input = "@DATA(\n name = \"Alice\",\n db: host = \"x\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains(",db"),
"comma leaked — got: {output}"
);
assert!(
!output.contains("\"Alice\"db"),
"string-value and table-property fused — got: {output}"
);
assert!(
output.contains("db:"),
"table-property missing — got: {output}"
);
}
#[test]
fn test_minify_space_after_string_item_before_group_array() {
let input = "@DATA(\n tags:: \"x\", \"y\",\n flags:: true\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains("\"y\"flags"),
"string item and group-array fused — got: {output}"
);
assert!(
!output.contains(",flags"),
"comma leaked before second group-array — got: {output}"
);
assert!(
output.contains("flags::"),
"second group-array missing — got: {output}"
);
assert!(
output.contains("\"x\",\"y\"") || output.contains("\"x\", \"y\""),
"inner comma between group-array items dropped — got: {output}"
);
}
#[test]
fn test_minify_proactive_space_string_value_no_comma_before_table() {
let input = "@DATA(\n name = \"Alice\"\n db: host = \"x\"\n)";
let output = DixCompactor::minify(input);
assert!(
!output.contains("\"Alice\"db"),
"string and table-property fused without comma — got: {output}"
);
assert!(output.contains("db:"), "table-property missing — got: {output}");
}
#[test]
fn test_minify_full_data_section_mixed() {
let input = concat!(
"@DATA(\n",
" count = 42,\n",
" label = \"hello\",\n",
" db: host = \"localhost\", port = 5432,\n",
" tags:: \"x\", \"y\"\n",
")"
);
let output = DixCompactor::minify(input);
assert!(!output.contains(",db"), "comma before 'db:' — got: {output}");
assert!(!output.contains(",tags"), "comma before 'tags::' — got: {output}");
assert!(output.contains("count="), "got: {output}");
assert!(output.contains("label="), "got: {output}");
assert!(output.contains("db:"), "got: {output}");
assert!(output.contains("host="), "got: {output}");
assert!(output.contains("port="), "got: {output}");
assert!(output.contains("tags::"), "got: {output}");
assert!(output.contains("\"x\""), "got: {output}");
assert!(output.contains("\"y\""), "got: {output}");
assert!(output.contains(" db:"), "no space before 'db:' — got: {output}");
assert!(output.contains(" tags::"), "no space before 'tags::' — got: {output}");
}
#[test]
fn test_compact_removes_trailing_whitespace() {
let input = "line1 \nline2\t\t";
let output = DixCompactor::compact(input);
assert_eq!(output, "line1\nline2\n");
}
#[test]
fn test_compact_collapses_many_blank_lines() {
let input = "line1\n\n\n\nline2";
let output = DixCompactor::compact(input);
assert_eq!(output, "line1\n\nline2\n");
}
#[test]
fn test_compact_single_blank_line_preserved() {
let output = DixCompactor::compact("a\n\nb");
assert_eq!(output, "a\n\nb\n");
}
#[test]
fn test_compact_preserves_indentation() {
let input = "@DATA( \n x = 1 \n)";
let output = DixCompactor::compact(input);
assert!(output.contains(" x = 1"), "indentation lost: {output}");
}
#[test]
fn test_remove_comments_single_line() {
let output = DixCompactor::remove_comments("x = 5 // a comment\ny = 10");
assert_eq!(output, "x = 5 \ny = 10");
}
#[test]
fn test_remove_comments_multi_line() {
let output = DixCompactor::remove_comments("x = 5 /* comment */ y = 10");
assert_eq!(output, "x = 5 y = 10");
}
#[test]
fn test_remove_comments_preserves_url_in_string() {
let output = DixCompactor::remove_comments("url = \"http://example.com\" // comment");
assert_eq!(output, "url = \"http://example.com\" ");
}
#[test]
fn test_remove_comments_preserves_comment_text_in_string() {
let input = "s = \"/* not a comment */\" // real comment";
let output = DixCompactor::remove_comments(input);
assert!(output.contains("/* not a comment */"), "got: {output}");
assert!(!output.contains("real comment"), "got: {output}");
}
#[test]
fn test_compression_ratio() {
let original = "hello world";
let compressed = "hello";
let ratio = DixCompactor::get_compression_ratio(original, compressed);
assert!((ratio - (1.0 - 5.0 / 11.0)).abs() < 0.001);
}
#[test]
fn test_compression_ratio_empty_original() {
assert_eq!(DixCompactor::get_compression_ratio("", ""), 0.0);
}
#[test]
fn test_compression_ratio_no_change() {
let s = "abc";
assert_eq!(DixCompactor::get_compression_ratio(s, s), 0.0);
}
}