#![deny(missing_docs)]
#[derive(Debug)]
pub struct LayerEntry {
pub name: Box<str>,
pub path: Box<str>,
}
#[derive(Debug)]
pub struct LayerGroup {
pub internal_name: Box<str>,
pub display_name: Option<Box<str>>,
pub entries: Vec<LayerEntry>,
}
#[derive(Debug)]
pub struct CharacterDefinition {
pub layers: Vec<LayerGroup>,
}
impl CharacterDefinition {
pub fn parse(input: &str) -> Self {
let mut layers = Vec::new();
let mut current_group: Option<LayerGroup> = None;
for line in input.lines() {
let line = line
.split_once('#')
.map_or(line, |(entry, _comment)| entry)
.trim();
if line.is_empty() {
continue;
}
if let Some(entry) = line.strip_prefix('-') {
if let Some(group) = &mut current_group {
let entry = entry.trim();
group
.entries
.push(if let Some((name, path)) = entry.split_once(':') {
LayerEntry {
name: name.trim_end().into(),
path: path.trim_start().into(),
}
} else {
LayerEntry {
name: entry.into(),
path: "".into(),
}
});
}
continue;
}
if let Some(group) = current_group.take() {
layers.push(group);
}
let (internal_name, display_name) = line.split_once(':').map_or_else(
|| (line.trim().into(), None),
|(internal, display)| (internal.trim().into(), Some(display.trim().into())),
);
current_group = Some(LayerGroup {
internal_name,
display_name,
entries: Vec::new(),
});
}
if let Some(group) = current_group.take() {
layers.push(group);
}
Self { layers }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_basic_definition() {
let input = r"
base
- Default: base.png
expression: Mood
- Happy: happy.png
- Sad: sad.png
";
let def = CharacterDefinition::parse(input);
assert_eq!(def.layers.len(), 2);
assert_eq!(def.layers[0].internal_name.as_ref(), "base");
assert_eq!(def.layers[1].display_name.as_deref(), Some("Mood"));
assert_eq!(def.layers[1].entries[1].name.as_ref(), "Sad");
}
#[test]
fn handles_empty_paths() {
let input = r"
outfit
- Shirt: shirt.png
- None
";
let def = CharacterDefinition::parse(input);
assert!(def.layers[0].entries[1].path.is_empty());
}
#[test]
fn ignores_comments() {
let input = r"
# This is a comment
base # Base layer comment
- Default: base.png # Default variant
# Expression group
expression: Mood
- Happy: happy.png
- Sad: sad.png # Disabled below
- None
";
let def = CharacterDefinition::parse(input);
assert_eq!(def.layers.len(), 2);
assert_eq!(def.layers[0].internal_name.as_ref(), "base");
assert!(def.layers[1].entries[2].path.is_empty());
}
#[test]
fn handles_inline_comments() {
let input = r"
base # Important base layer
- Default: base.png # Main variant
- Alternate: alternate.png
";
let def = CharacterDefinition::parse(input);
assert_eq!(def.layers[0].entries[0].name.as_ref(), "Default");
assert_eq!(def.layers[0].entries[1].path.as_ref(), "alternate.png");
}
}