pub mod error;
pub mod model;
pub mod options;
#[cfg(feature = "parse")]
mod parser;
#[cfg(feature = "printer")]
pub mod printer;
#[cfg(feature = "serde-deserialize")]
pub mod de;
#[cfg(feature = "serde-serialize")]
pub mod ser;
pub use error::{Error, Result};
pub use model::{BoolOptions, CclObject, Entry, ListOptions};
pub use options::{CrlfBehavior, DelimiterStrategy, ParserOptions, SpacingBehavior, TabBehavior};
#[cfg(feature = "printer")]
pub use printer::{print, round_trip, CclPrinter, PrinterConfig};
#[cfg(feature = "parse")]
pub fn parse(input: &str) -> Result<Vec<Entry>> {
parse_with_options_internal(input, &ParserOptions::default())
}
#[cfg(all(feature = "parse", feature = "unstable"))]
pub fn parse_with_options(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
parse_with_options_internal(input, options)
}
#[cfg(feature = "parse")]
fn parse_with_options_internal(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
parser::parse_to_entries(input, options)
}
#[cfg(feature = "hierarchy")]
pub fn build_hierarchy(entries: &[Entry]) -> Result<CclObject> {
let mut map: indexmap::IndexMap<String, Vec<String>> = indexmap::IndexMap::new();
for entry in entries {
map.entry(entry.key.clone())
.or_default()
.push(entry.value.clone());
}
build_model(map)
}
#[cfg(feature = "hierarchy")]
fn is_valid_ccl_key(key: &str) -> bool {
if key.is_empty() {
return true; }
if key.starts_with('-') {
return false;
}
!key.contains(' ')
}
#[cfg(feature = "hierarchy")]
fn build_model(map: indexmap::IndexMap<String, Vec<String>>) -> Result<CclObject> {
let mut result = indexmap::IndexMap::new();
for (key, values) in map {
#[cfg(feature = "reference_compliant")]
let values = {
let mut v = values;
if v.len() > 1 && !key.is_empty() {
v.sort();
}
v
};
let mut nested_values = Vec::new();
for value in values {
if value.contains('\n') && value.contains('=') {
match load(&value) {
Ok(parsed) => {
if !parsed.is_empty() {
let has_valid_keys = parsed.keys().all(|k| is_valid_ccl_key(k));
if has_valid_keys {
nested_values.push(parsed);
} else {
nested_values.push(CclObject::from_string(value));
}
} else {
nested_values.push(CclObject::from_string(value));
}
}
Err(_) => {
nested_values.push(CclObject::from_string(value));
}
}
} else {
nested_values.push(CclObject::from_string(value));
}
}
if !key.is_empty() && nested_values.len() > 1 {
let all_simple_strings = nested_values.iter().all(|obj| {
obj.len() == 1 && obj.iter().next().is_some_and(|(_, v)| v.is_empty())
});
if all_simple_strings {
result.insert(key, nested_values);
} else {
let composed = nested_values
.iter()
.fold(CclObject::new(), |acc, obj| acc.compose(obj));
result.insert(key, vec![composed]);
}
} else {
result.insert(key, nested_values);
}
}
Ok(CclObject::from_map(result))
}
#[cfg(feature = "parse")]
pub fn parse_indented(input: &str) -> Result<Vec<Entry>> {
parse_indented_with_options_internal(input, &ParserOptions::default())
}
#[cfg(all(feature = "parse", feature = "unstable"))]
pub fn parse_indented_with_options(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
parse_indented_with_options_internal(input, options)
}
#[cfg(feature = "parse")]
fn parse_indented_with_options_internal(
input: &str,
options: &ParserOptions,
) -> Result<Vec<Entry>> {
let min_indent = input
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
let dedented = input
.lines()
.map(|line| {
let for_dedent = line.replace('\t', " ");
if for_dedent.trim().is_empty() {
options.process_tabs(line).into_owned()
} else if for_dedent.len() > min_indent {
if options.preserve_tabs() {
if line.len() > min_indent {
line[min_indent..].to_string()
} else {
line.trim_start().to_string()
}
} else {
for_dedent[min_indent..].to_string()
}
} else if options.preserve_tabs() {
line.trim_start().to_string()
} else {
for_dedent.trim_start().to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
let entries_at_min_indent = dedented
.lines()
.filter(|line| {
let indent = line.len() - line.trim_start().len();
indent == 0 && line.trim().contains('=')
})
.count();
if entries_at_min_indent > 1 {
parse_flat_entries(&dedented, options)
} else {
parse_single_entry_with_raw_value(&dedented, options)
}
}
#[cfg(feature = "parse")]
fn parse_flat_entries(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
let mut entries = Vec::new();
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(eq_pos) = trimmed.find('=') {
let key = trimmed[..eq_pos].trim().to_string();
let value_raw = &trimmed[eq_pos + 1..];
let value = if options.is_strict_spacing() {
value_raw
.trim_start_matches(' ')
.trim_end_matches(' ')
.to_string()
} else {
value_raw.trim().to_string()
};
entries.push(Entry::new(key, value));
} else {
entries.push(Entry::new(trimmed.to_string(), String::new()));
}
}
Ok(entries)
}
#[cfg(feature = "parse")]
fn parse_single_entry_with_raw_value(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
let mut lines = input.lines();
let first_line = lines.next().unwrap_or("");
if let Some(eq_pos) = first_line.find('=') {
let key = first_line[..eq_pos].trim().to_string();
let first_value = first_line[eq_pos + 1..].trim_start().to_string();
let remaining_lines: Vec<&str> = lines.collect();
let value = if !remaining_lines.is_empty() {
let joined = if first_value.trim().is_empty() {
"\n".to_string() + &remaining_lines.join("\n")
} else {
first_value + "\n" + &remaining_lines.join("\n")
};
options.process_tabs(&joined).into_owned()
} else {
first_value
};
Ok(vec![Entry::new(key, value)])
} else {
Ok(vec![Entry::new(
first_line.trim().to_string(),
String::new(),
)])
}
}
#[cfg(feature = "hierarchy")]
pub fn load(input: &str) -> Result<CclObject> {
load_with_options_internal(input, &ParserOptions::default())
}
#[cfg(all(feature = "hierarchy", feature = "unstable"))]
pub fn load_with_options(input: &str, options: &ParserOptions) -> Result<CclObject> {
load_with_options_internal(input, options)
}
#[cfg(feature = "hierarchy")]
fn load_with_options_internal(input: &str, options: &ParserOptions) -> Result<CclObject> {
let entries = parse_with_options_internal(input, options)?;
build_hierarchy(&entries)
}
#[cfg(feature = "serde-deserialize")]
pub use de::{from_str, from_str_with_options};
#[cfg(feature = "serde-serialize")]
pub use ser::{to_string, to_string_with_config};