links_notation/
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    // Special case: If this is an indented ID (with colon) with children,
88    // the children should become the values of the link (indented ID syntax)
89    if link.is_indented_id && link.id.is_some() && link.values.is_empty() && !link.children.is_empty() {
90        let child_values: Vec<LiNo<String>> = link.children.iter().map(|child| {
91            // For indented children, if they have single values, extract them
92            if child.values.len() == 1 && child.values[0].id.is_some() && child.values[0].values.is_empty() && child.values[0].children.is_empty() {
93                LiNo::Ref(child.values[0].id.clone().unwrap())
94            } else {
95                parser::Link {
96                    id: child.id.clone(),
97                    values: child.values.clone(),
98                    children: vec![],
99                    is_indented_id: false,
100                }.into()
101            }
102        }).collect();
103        
104        let current = LiNo::Link { 
105            id: link.id.clone(), 
106            values: child_values 
107        };
108        
109        let combined = if let Some(parent) = parent {
110            // Wrap parent in parentheses if it's a reference
111            let wrapped_parent = match parent {
112                LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id)] },
113                link => link
114            };
115            
116            LiNo::Link { 
117                id: None, 
118                values: vec![wrapped_parent, current]
119            }
120        } else {
121            current
122        };
123        
124        result.push(combined);
125        return; // Don't process children again
126    }
127    
128    // Create the current link without children
129    let current = if link.values.is_empty() {
130        if let Some(id) = &link.id {
131            LiNo::Ref(id.clone())
132        } else {
133            LiNo::Link { id: None, values: vec![] }
134        }
135    } else {
136        let values: Vec<LiNo<String>> = link.values.iter().map(|v| {
137            parser::Link {
138                id: v.id.clone(),
139                values: v.values.clone(),
140                children: vec![],
141                is_indented_id: false,
142            }.into()
143        }).collect();
144        LiNo::Link { id: link.id.clone(), values }
145    };
146    
147    // Create the combined link (parent + current) with proper wrapping
148    let combined = if let Some(parent) = parent {
149        // Wrap parent in parentheses if it's a reference
150        let wrapped_parent = match parent {
151            LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id)] },
152            link => link
153        };
154        
155        // Wrap current in parentheses if it's a reference
156        let wrapped_current = match current.clone() {
157            LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id)] },
158            link => link
159        };
160        
161        LiNo::Link { 
162            id: None, 
163            values: vec![wrapped_parent, wrapped_current]
164        }
165    } else {
166        current.clone()
167    };
168    
169    result.push(combined.clone());
170    
171    // Process children
172    for child in &link.children {
173        flatten_link_recursive(child, Some(combined.clone()), result);
174    }
175}
176
177pub fn parse_lino(document: &str) -> Result<LiNo<String>, String> {
178    // Handle empty or whitespace-only input by returning empty result
179    if document.trim().is_empty() {
180        return Ok(LiNo::Link { id: None, values: vec![] });
181    }
182    
183    match parser::parse_document(document) {
184        Ok((_, links)) => {
185            if links.is_empty() {
186                Ok(LiNo::Link { id: None, values: vec![] })
187            } else {
188                // Flatten the indented structure according to Lino spec
189                let flattened = flatten_links(links);
190                Ok(LiNo::Link { id: None, values: flattened })
191            }
192        }
193        Err(e) => Err(format!("Parse error: {:?}", e))
194    }
195}
196
197// New function that matches C# and JS API - returns collection of links
198pub fn parse_lino_to_links(document: &str) -> Result<Vec<LiNo<String>>, String> {
199    // Handle empty or whitespace-only input by returning empty collection
200    if document.trim().is_empty() {
201        return Ok(vec![]);
202    }
203
204    match parser::parse_document(document) {
205        Ok((_, links)) => {
206            if links.is_empty() {
207                Ok(vec![])
208            } else {
209                // Flatten the indented structure according to Lino spec
210                let flattened = flatten_links(links);
211                Ok(flattened)
212            }
213        }
214        Err(e) => Err(format!("Parse error: {:?}", e))
215    }
216}
217
218/// Formats a collection of LiNo links as a multi-line string.
219/// Each link is formatted on a separate line.
220pub fn format_links(links: &[LiNo<String>]) -> String {
221    links.iter()
222        .map(|link| format!("{}", link))
223        .collect::<Vec<_>>()
224        .join("\n")
225}
226