links_notation/
lib.rs

1pub mod parser;
2pub mod format_config;
3
4use std::fmt;
5use std::error::Error as StdError;
6
7/// Error type for Lino parsing
8#[derive(Debug)]
9pub enum ParseError {
10    /// Input string is empty or contains only whitespace
11    EmptyInput,
12    /// Syntax error during parsing
13    SyntaxError(String),
14    /// Internal parser error
15    InternalError(String),
16}
17
18impl fmt::Display for ParseError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            ParseError::EmptyInput => write!(f, "Empty input"),
22            ParseError::SyntaxError(msg) => write!(f, "Syntax error: {}", msg),
23            ParseError::InternalError(msg) => write!(f, "Internal error: {}", msg),
24        }
25    }
26}
27
28impl StdError for ParseError {}
29
30#[derive(Debug, Clone, PartialEq)]
31pub enum LiNo<T> {
32    Link { id: Option<T>, values: Vec<Self> },
33    Ref(T),
34}
35
36impl<T> LiNo<T> {
37    pub fn is_ref(&self) -> bool {
38        matches!(self, LiNo::Ref(_))
39    }
40
41    pub fn is_link(&self) -> bool {
42        matches!(self, LiNo::Link { .. })
43    }
44}
45
46impl<T: ToString> fmt::Display for LiNo<T> {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            LiNo::Ref(value) => write!(f, "{}", value.to_string()),
50            LiNo::Link { id, values } => {
51                let id_str = id
52                    .as_ref()
53                    .map(|id| format!("{}: ", id.to_string()))
54                    .unwrap_or_default();
55
56                if f.alternate() {
57                    // Format top-level as lines
58                    let lines = values
59                        .iter()
60                        .map(|value| {
61                            // For alternate formatting, ensure standalone references are wrapped in parentheses
62                            // so that flattened structures like indented blocks render as "(ref)" lines
63                            match value {
64                                LiNo::Ref(_) => format!("{}({})", id_str, value),
65                                _ => format!("{}{}", id_str, value),
66                            }
67                        })
68                        .collect::<Vec<_>>()
69                        .join("\n");
70                    write!(f, "{}", lines)
71                } else {
72                    let values_str = values
73                        .iter()
74                        .map(|value| value.to_string())
75                        .collect::<Vec<_>>()
76                        .join(" ");
77                    write!(f, "({}{})", id_str, values_str)
78                }
79            }
80        }
81    }
82}
83
84// Convert from parser::Link to LiNo (without flattening)
85impl From<parser::Link> for LiNo<String> {
86    fn from(link: parser::Link) -> Self {
87        if link.values.is_empty() && link.children.is_empty() {
88            if let Some(id) = link.id {
89                LiNo::Ref(id)
90            } else {
91                LiNo::Link { id: None, values: vec![] }
92            }
93        } else {
94            let values: Vec<LiNo<String>> = link.values.into_iter().map(|v| v.into()).collect();
95            LiNo::Link { id: link.id, values }
96        }
97    }
98}
99
100// Helper function to flatten indented structures according to Lino spec
101fn flatten_links(links: Vec<parser::Link>) -> Vec<LiNo<String>> {
102    let mut result = vec![];
103    
104    for link in links {
105        flatten_link_recursive(&link, None, &mut result);
106    }
107    
108    result
109}
110
111fn flatten_link_recursive(link: &parser::Link, parent: Option<&LiNo<String>>, result: &mut Vec<LiNo<String>>) {
112    // Special case: If this is an indented ID (with colon) with children,
113    // the children should become the values of the link (indented ID syntax)
114    if link.is_indented_id && link.id.is_some() && link.values.is_empty() && !link.children.is_empty() {
115        let child_values: Vec<LiNo<String>> = link.children.iter().map(|child| {
116            // For indented children, if they have single values, extract them
117            if child.values.len() == 1 && child.values[0].values.is_empty() && child.values[0].children.is_empty() {
118                // Use if let to safely extract the ID instead of unwrap()
119                if let Some(ref id) = child.values[0].id {
120                    LiNo::Ref(id.clone())
121                } else {
122                    // If no ID, create an empty link
123                    parser::Link {
124                        id: child.id.clone(),
125                        values: child.values.clone(),
126                        children: vec![],
127                        is_indented_id: false,
128                    }.into()
129                }
130            } else {
131                parser::Link {
132                    id: child.id.clone(),
133                    values: child.values.clone(),
134                    children: vec![],
135                    is_indented_id: false,
136                }.into()
137            }
138        }).collect();
139        
140        let current = LiNo::Link {
141            id: link.id.clone(),
142            values: child_values
143        };
144
145        let combined = if let Some(parent) = parent {
146            // Wrap parent in parentheses if it's a reference
147            let wrapped_parent = match parent {
148                LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id.clone())] },
149                link => link.clone()
150            };
151
152            LiNo::Link {
153                id: None,
154                values: vec![wrapped_parent, current]
155            }
156        } else {
157            current
158        };
159
160        result.push(combined);
161        return; // Don't process children again
162    }
163    
164    // Create the current link without children
165    let current = if link.values.is_empty() {
166        if let Some(id) = &link.id {
167            LiNo::Ref(id.clone())
168        } else {
169            LiNo::Link { id: None, values: vec![] }
170        }
171    } else {
172        let values: Vec<LiNo<String>> = link.values.iter().map(|v| {
173            parser::Link {
174                id: v.id.clone(),
175                values: v.values.clone(),
176                children: vec![],
177                is_indented_id: false,
178            }.into()
179        }).collect();
180        LiNo::Link { id: link.id.clone(), values }
181    };
182    
183    // Create the combined link (parent + current) with proper wrapping
184    let combined = if let Some(parent) = parent {
185        // Wrap parent in parentheses if it's a reference
186        let wrapped_parent = match parent {
187            LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id.clone())] },
188            link => link.clone()
189        };
190
191        // Wrap current in parentheses if it's a reference
192        let wrapped_current = match &current {
193            LiNo::Ref(ref_id) => LiNo::Link { id: None, values: vec![LiNo::Ref(ref_id.clone())] },
194            link => link.clone()
195        };
196
197        LiNo::Link {
198            id: None,
199            values: vec![wrapped_parent, wrapped_current]
200        }
201    } else {
202        current.clone()
203    };
204
205    result.push(combined.clone());
206
207    // Process children
208    for child in &link.children {
209        flatten_link_recursive(child, Some(&combined), result);
210    }
211}
212
213pub fn parse_lino(document: &str) -> Result<LiNo<String>, ParseError> {
214    // Handle empty or whitespace-only input by returning empty result
215    if document.trim().is_empty() {
216        return Ok(LiNo::Link { id: None, values: vec![] });
217    }
218
219    match parser::parse_document(document) {
220        Ok((_, links)) => {
221            if links.is_empty() {
222                Ok(LiNo::Link { id: None, values: vec![] })
223            } else {
224                // Flatten the indented structure according to Lino spec
225                let flattened = flatten_links(links);
226                Ok(LiNo::Link { id: None, values: flattened })
227            }
228        }
229        Err(e) => Err(ParseError::SyntaxError(format!("{:?}", e)))
230    }
231}
232
233// New function that matches C# and JS API - returns collection of links
234pub fn parse_lino_to_links(document: &str) -> Result<Vec<LiNo<String>>, ParseError> {
235    // Handle empty or whitespace-only input by returning empty collection
236    if document.trim().is_empty() {
237        return Ok(vec![]);
238    }
239
240    match parser::parse_document(document) {
241        Ok((_, links)) => {
242            if links.is_empty() {
243                Ok(vec![])
244            } else {
245                // Flatten the indented structure according to Lino spec
246                let flattened = flatten_links(links);
247                Ok(flattened)
248            }
249        }
250        Err(e) => Err(ParseError::SyntaxError(format!("{:?}", e)))
251    }
252}
253
254/// Formats a collection of LiNo links as a multi-line string.
255/// Each link is formatted on a separate line.
256pub fn format_links(links: &[LiNo<String>]) -> String {
257    links.iter()
258        .map(|link| format!("{}", link))
259        .collect::<Vec<_>>()
260        .join("\n")
261}
262