lino/
lib.rs

1pub mod parser;
2
3use std::fmt;
4
5#[derive(Debug, Clone)]
6pub enum LiNo<T> {
7    Link { id: Option<T>, values: Vec<Self> },
8    Ref(T),
9}
10
11impl<T> LiNo<T> {
12    pub fn is_ref(&self) -> bool {
13        matches!(self, LiNo::Ref(_))
14    }
15
16    pub fn is_link(&self) -> bool {
17        matches!(self, LiNo::Link { .. })
18    }
19}
20
21impl<T: ToString> fmt::Display for LiNo<T> {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            LiNo::Ref(value) => write!(f, "{}", value.to_string()),
25            LiNo::Link { id, values } => {
26                let id_str = id
27                    .as_ref()
28                    .map(|id| format!("{}: ", id.to_string()))
29                    .unwrap_or_default();
30
31                if f.alternate() {
32                    // Format top-level as lines
33                    let lines = values
34                        .iter()
35                        .map(|value| {
36                            // For alternate formatting, ensure standalone references are wrapped in parentheses
37                            // so that flattened structures like indented blocks render as "(ref)" lines
38                            match value {
39                                LiNo::Ref(_) => format!("{}({})", id_str, value),
40                                _ => format!("{}{}", id_str, value),
41                            }
42                        })
43                        .collect::<Vec<_>>()
44                        .join("\n");
45                    write!(f, "{}", lines)
46                } else {
47                    let values_str = values
48                        .iter()
49                        .map(|value| value.to_string())
50                        .collect::<Vec<_>>()
51                        .join(" ");
52                    write!(f, "({}{})", id_str, values_str)
53                }
54            }
55        }
56    }
57}
58
59// Convert from parser::Link to LiNo (without flattening)
60impl From<parser::Link> for LiNo<String> {
61    fn from(link: parser::Link) -> Self {
62        if link.values.is_empty() && link.children.is_empty() {
63            if let Some(id) = link.id {
64                LiNo::Ref(id)
65            } else {
66                LiNo::Link { id: None, values: vec![] }
67            }
68        } else {
69            let values: Vec<LiNo<String>> = link.values.into_iter().map(|v| v.into()).collect();
70            LiNo::Link { id: link.id, values }
71        }
72    }
73}
74
75// Helper function to flatten indented structures according to Lino spec
76fn flatten_links(links: Vec<parser::Link>) -> Vec<LiNo<String>> {
77    let mut result = vec![];
78    
79    for link in links {
80        flatten_link_recursive(&link, None, &mut result);
81    }
82    
83    result
84}
85
86fn flatten_link_recursive(link: &parser::Link, parent: Option<LiNo<String>>, result: &mut Vec<LiNo<String>>) {
87    // Create the current link without children
88    let current = if link.values.is_empty() {
89        if let Some(id) = &link.id {
90            LiNo::Ref(id.clone())
91        } else {
92            LiNo::Link { id: None, values: vec![] }
93        }
94    } else {
95        let values: Vec<LiNo<String>> = link.values.iter().map(|v| {
96            parser::Link {
97                id: v.id.clone(),
98                values: v.values.clone(),
99                children: vec![]
100            }.into()
101        }).collect();
102        LiNo::Link { id: link.id.clone(), values }
103    };
104    
105    // Create the combined link (parent + current)
106    let combined = if let Some(parent) = parent {
107        LiNo::Link { 
108            id: None, 
109            values: vec![parent.clone(), current.clone()]
110        }
111    } else {
112        current.clone()
113    };
114    
115    result.push(combined.clone());
116    
117    // Process children
118    for child in &link.children {
119        flatten_link_recursive(child, Some(combined.clone()), result);
120    }
121}
122
123pub fn parse_lino(document: &str) -> Result<LiNo<String>, String> {
124    // Handle empty or whitespace-only input by returning empty result
125    if document.trim().is_empty() {
126        return Ok(LiNo::Link { id: None, values: vec![] });
127    }
128    
129    match parser::parse_document(document) {
130        Ok((_, links)) => {
131            if links.is_empty() {
132                Ok(LiNo::Link { id: None, values: vec![] })
133            } else {
134                // Flatten the indented structure according to Lino spec
135                let flattened = flatten_links(links);
136                Ok(LiNo::Link { id: None, values: flattened })
137            }
138        }
139        Err(e) => Err(format!("Parse error: {:?}", e))
140    }
141}
142