lino/
lib.rs

1pub mod parser;
2
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq)]
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) with proper wrapping
106    let combined = if let Some(parent) = parent {
107        // Wrap parent in parentheses if it's a reference
108        let wrapped_parent = match parent {
109            LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id)] },
110            link => link
111        };
112        
113        // Wrap current in parentheses if it's a reference
114        let wrapped_current = match current.clone() {
115            LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id)] },
116            link => link
117        };
118        
119        LiNo::Link { 
120            id: None, 
121            values: vec![wrapped_parent, wrapped_current]
122        }
123    } else {
124        current.clone()
125    };
126    
127    result.push(combined.clone());
128    
129    // Process children
130    for child in &link.children {
131        flatten_link_recursive(child, Some(combined.clone()), result);
132    }
133}
134
135pub fn parse_lino(document: &str) -> Result<LiNo<String>, String> {
136    // Handle empty or whitespace-only input by returning empty result
137    if document.trim().is_empty() {
138        return Ok(LiNo::Link { id: None, values: vec![] });
139    }
140    
141    match parser::parse_document(document) {
142        Ok((_, links)) => {
143            if links.is_empty() {
144                Ok(LiNo::Link { id: None, values: vec![] })
145            } else {
146                // Flatten the indented structure according to Lino spec
147                let flattened = flatten_links(links);
148                Ok(LiNo::Link { id: None, values: flattened })
149            }
150        }
151        Err(e) => Err(format!("Parse error: {:?}", e))
152    }
153}
154
155// New function that matches C# and JS API - returns collection of links
156pub fn parse_lino_to_links(document: &str) -> Result<Vec<LiNo<String>>, String> {
157    // Handle empty or whitespace-only input by returning empty collection
158    if document.trim().is_empty() {
159        return Ok(vec![]);
160    }
161    
162    match parser::parse_document(document) {
163        Ok((_, links)) => {
164            if links.is_empty() {
165                Ok(vec![])
166            } else {
167                // Flatten the indented structure according to Lino spec
168                let flattened = flatten_links(links);
169                Ok(flattened)
170            }
171        }
172        Err(e) => Err(format!("Parse error: {:?}", e))
173    }
174}
175