links_notation/
lib.rs

1pub mod format_config;
2pub mod parser;
3
4use format_config::FormatConfig;
5use std::error::Error as StdError;
6use std::fmt;
7
8/// Error type for Lino parsing
9#[derive(Debug)]
10pub enum ParseError {
11    /// Input string is empty or contains only whitespace
12    EmptyInput,
13    /// Syntax error during parsing
14    SyntaxError(String),
15    /// Internal parser error
16    InternalError(String),
17}
18
19impl fmt::Display for ParseError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            ParseError::EmptyInput => write!(f, "Empty input"),
23            ParseError::SyntaxError(msg) => write!(f, "Syntax error: {}", msg),
24            ParseError::InternalError(msg) => write!(f, "Internal error: {}", msg),
25        }
26    }
27}
28
29impl StdError for ParseError {}
30
31#[derive(Debug, Clone, PartialEq)]
32pub enum LiNo<T> {
33    Link { id: Option<T>, values: Vec<Self> },
34    Ref(T),
35}
36
37impl<T> LiNo<T> {
38    pub fn is_ref(&self) -> bool {
39        matches!(self, LiNo::Ref(_))
40    }
41
42    pub fn is_link(&self) -> bool {
43        matches!(self, LiNo::Link { .. })
44    }
45}
46
47impl<T: ToString + Clone> LiNo<T> {
48    /// Format the link using FormatConfig configuration.
49    ///
50    /// # Arguments
51    /// * `config` - The FormatConfig to use for formatting
52    ///
53    /// # Returns
54    /// Formatted string representation
55    pub fn format_with_config(&self, config: &FormatConfig) -> String {
56        match self {
57            LiNo::Ref(value) => {
58                let escaped = escape_reference(&value.to_string());
59                if config.less_parentheses {
60                    escaped
61                } else {
62                    format!("({})", escaped)
63                }
64            }
65            LiNo::Link { id, values } => {
66                // Empty link
67                if id.is_none() && values.is_empty() {
68                    return if config.less_parentheses {
69                        String::new()
70                    } else {
71                        "()".to_string()
72                    };
73                }
74
75                // Link with only ID, no values
76                if values.is_empty() {
77                    if let Some(ref id_val) = id {
78                        let escaped_id = escape_reference(&id_val.to_string());
79                        return if config.less_parentheses && !needs_parentheses(&id_val.to_string())
80                        {
81                            escaped_id
82                        } else {
83                            format!("({})", escaped_id)
84                        };
85                    }
86                    return if config.less_parentheses {
87                        String::new()
88                    } else {
89                        "()".to_string()
90                    };
91                }
92
93                // Check if we should use indented format
94                let mut should_indent = false;
95                if config.should_indent_by_ref_count(values.len()) {
96                    should_indent = true;
97                } else {
98                    // Try inline format first to check line length
99                    let values_str = values
100                        .iter()
101                        .map(|v| format_value(v))
102                        .collect::<Vec<_>>()
103                        .join(" ");
104
105                    let test_line = if let Some(ref id_val) = id {
106                        let id_str = escape_reference(&id_val.to_string());
107                        if config.less_parentheses {
108                            format!("{}: {}", id_str, values_str)
109                        } else {
110                            format!("({}: {})", id_str, values_str)
111                        }
112                    } else if config.less_parentheses {
113                        values_str.clone()
114                    } else {
115                        format!("({})", values_str)
116                    };
117
118                    if config.should_indent_by_length(&test_line) {
119                        should_indent = true;
120                    }
121                }
122
123                // Format with indentation if needed
124                if should_indent && !config.prefer_inline {
125                    return self.format_indented(config);
126                }
127
128                // Standard inline formatting
129                let values_str = values
130                    .iter()
131                    .map(|v| format_value(v))
132                    .collect::<Vec<_>>()
133                    .join(" ");
134
135                // Link with values only (null id)
136                if id.is_none() {
137                    if config.less_parentheses {
138                        // Check if all values are simple (no nested values)
139                        let all_simple = values.iter().all(|v| matches!(v, LiNo::Ref(_)));
140                        if all_simple {
141                            return values
142                                .iter()
143                                .map(|v| match v {
144                                    LiNo::Ref(r) => escape_reference(&r.to_string()),
145                                    _ => format_value(v),
146                                })
147                                .collect::<Vec<_>>()
148                                .join(" ");
149                        }
150                        return values_str;
151                    }
152                    return format!("({})", values_str);
153                }
154
155                // Link with ID and values
156                let id_str = escape_reference(&id.as_ref().unwrap().to_string());
157                let with_colon = format!("{}: {}", id_str, values_str);
158                if config.less_parentheses && !needs_parentheses(&id.as_ref().unwrap().to_string())
159                {
160                    with_colon
161                } else {
162                    format!("({})", with_colon)
163                }
164            }
165        }
166    }
167
168    /// Format the link with indentation.
169    fn format_indented(&self, config: &FormatConfig) -> String {
170        match self {
171            LiNo::Ref(value) => {
172                let escaped = escape_reference(&value.to_string());
173                format!("({})", escaped)
174            }
175            LiNo::Link { id, values } => {
176                if id.is_none() {
177                    // Values only - format each on separate line
178                    values
179                        .iter()
180                        .map(|v| format!("{}{}", config.indent_string, format_value(v)))
181                        .collect::<Vec<_>>()
182                        .join("\n")
183                } else {
184                    // Link with ID - format as id:\n  value1\n  value2
185                    let id_str = escape_reference(&id.as_ref().unwrap().to_string());
186                    let mut lines = vec![format!("{}:", id_str)];
187                    for v in values {
188                        lines.push(format!("{}{}", config.indent_string, format_value(v)));
189                    }
190                    lines.join("\n")
191                }
192            }
193        }
194    }
195}
196
197impl<T: ToString> fmt::Display for LiNo<T> {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            LiNo::Ref(value) => write!(f, "{}", value.to_string()),
201            LiNo::Link { id, values } => {
202                let id_str = id
203                    .as_ref()
204                    .map(|id| format!("{}: ", id.to_string()))
205                    .unwrap_or_default();
206
207                if f.alternate() {
208                    // Format top-level as lines
209                    let lines = values
210                        .iter()
211                        .map(|value| {
212                            // For alternate formatting, ensure standalone references are wrapped in parentheses
213                            // so that flattened structures like indented blocks render as "(ref)" lines
214                            match value {
215                                LiNo::Ref(_) => format!("{}({})", id_str, value),
216                                _ => format!("{}{}", id_str, value),
217                            }
218                        })
219                        .collect::<Vec<_>>()
220                        .join("\n");
221                    write!(f, "{}", lines)
222                } else {
223                    let values_str = values
224                        .iter()
225                        .map(|value| value.to_string())
226                        .collect::<Vec<_>>()
227                        .join(" ");
228                    write!(f, "({}{})", id_str, values_str)
229                }
230            }
231        }
232    }
233}
234
235// Convert from parser::Link to LiNo (without flattening)
236impl From<parser::Link> for LiNo<String> {
237    fn from(link: parser::Link) -> Self {
238        if link.values.is_empty() && link.children.is_empty() {
239            if let Some(id) = link.id {
240                LiNo::Ref(id)
241            } else {
242                LiNo::Link {
243                    id: None,
244                    values: vec![],
245                }
246            }
247        } else {
248            let values: Vec<LiNo<String>> = link.values.into_iter().map(|v| v.into()).collect();
249            LiNo::Link {
250                id: link.id,
251                values,
252            }
253        }
254    }
255}
256
257// Helper function to flatten indented structures according to Lino spec
258fn flatten_links(links: Vec<parser::Link>) -> Vec<LiNo<String>> {
259    let mut result = vec![];
260
261    for link in links {
262        flatten_link_recursive(&link, None, &mut result);
263    }
264
265    result
266}
267
268fn flatten_link_recursive(
269    link: &parser::Link,
270    parent: Option<&LiNo<String>>,
271    result: &mut Vec<LiNo<String>>,
272) {
273    // Special case: If this is an indented ID (with colon) with children,
274    // the children should become the values of the link (indented ID syntax)
275    if link.is_indented_id
276        && link.id.is_some()
277        && link.values.is_empty()
278        && !link.children.is_empty()
279    {
280        let child_values: Vec<LiNo<String>> = link
281            .children
282            .iter()
283            .map(|child| {
284                // For indented children, if they have single values, extract them
285                if child.values.len() == 1
286                    && child.values[0].values.is_empty()
287                    && child.values[0].children.is_empty()
288                {
289                    // Use if let to safely extract the ID instead of unwrap()
290                    if let Some(ref id) = child.values[0].id {
291                        LiNo::Ref(id.clone())
292                    } else {
293                        // If no ID, create an empty link
294                        parser::Link {
295                            id: child.id.clone(),
296                            values: child.values.clone(),
297                            children: vec![],
298                            is_indented_id: false,
299                        }
300                        .into()
301                    }
302                } else {
303                    parser::Link {
304                        id: child.id.clone(),
305                        values: child.values.clone(),
306                        children: vec![],
307                        is_indented_id: false,
308                    }
309                    .into()
310                }
311            })
312            .collect();
313
314        let current = LiNo::Link {
315            id: link.id.clone(),
316            values: child_values,
317        };
318
319        let combined = if let Some(parent) = parent {
320            // Wrap parent in parentheses if it's a reference
321            let wrapped_parent = match parent {
322                LiNo::Ref(ref_id) => LiNo::Link {
323                    id: None,
324                    values: vec![LiNo::Ref(ref_id.clone())],
325                },
326                link => link.clone(),
327            };
328
329            LiNo::Link {
330                id: None,
331                values: vec![wrapped_parent, current],
332            }
333        } else {
334            current
335        };
336
337        result.push(combined);
338        return; // Don't process children again
339    }
340
341    // Create the current link without children
342    let current = if link.values.is_empty() {
343        if let Some(id) = &link.id {
344            LiNo::Ref(id.clone())
345        } else {
346            LiNo::Link {
347                id: None,
348                values: vec![],
349            }
350        }
351    } else {
352        let values: Vec<LiNo<String>> = link
353            .values
354            .iter()
355            .map(|v| {
356                parser::Link {
357                    id: v.id.clone(),
358                    values: v.values.clone(),
359                    children: vec![],
360                    is_indented_id: false,
361                }
362                .into()
363            })
364            .collect();
365        LiNo::Link {
366            id: link.id.clone(),
367            values,
368        }
369    };
370
371    // Create the combined link (parent + current) with proper wrapping
372    let combined = if let Some(parent) = parent {
373        // Wrap parent in parentheses if it's a reference
374        let wrapped_parent = match parent {
375            LiNo::Ref(ref_id) => LiNo::Link {
376                id: None,
377                values: vec![LiNo::Ref(ref_id.clone())],
378            },
379            link => link.clone(),
380        };
381
382        // Wrap current in parentheses if it's a reference
383        let wrapped_current = match &current {
384            LiNo::Ref(ref_id) => LiNo::Link {
385                id: None,
386                values: vec![LiNo::Ref(ref_id.clone())],
387            },
388            link => link.clone(),
389        };
390
391        LiNo::Link {
392            id: None,
393            values: vec![wrapped_parent, wrapped_current],
394        }
395    } else {
396        current.clone()
397    };
398
399    result.push(combined.clone());
400
401    // Process children
402    for child in &link.children {
403        flatten_link_recursive(child, Some(&combined), result);
404    }
405}
406
407pub fn parse_lino(document: &str) -> Result<LiNo<String>, ParseError> {
408    // Handle empty or whitespace-only input by returning empty result
409    if document.trim().is_empty() {
410        return Ok(LiNo::Link {
411            id: None,
412            values: vec![],
413        });
414    }
415
416    match parser::parse_document(document) {
417        Ok((_, links)) => {
418            if links.is_empty() {
419                Ok(LiNo::Link {
420                    id: None,
421                    values: vec![],
422                })
423            } else {
424                // Flatten the indented structure according to Lino spec
425                let flattened = flatten_links(links);
426                Ok(LiNo::Link {
427                    id: None,
428                    values: flattened,
429                })
430            }
431        }
432        Err(e) => Err(ParseError::SyntaxError(format!("{:?}", e))),
433    }
434}
435
436// New function that matches C# and JS API - returns collection of links
437pub fn parse_lino_to_links(document: &str) -> Result<Vec<LiNo<String>>, ParseError> {
438    // Handle empty or whitespace-only input by returning empty collection
439    if document.trim().is_empty() {
440        return Ok(vec![]);
441    }
442
443    match parser::parse_document(document) {
444        Ok((_, links)) => {
445            if links.is_empty() {
446                Ok(vec![])
447            } else {
448                // Flatten the indented structure according to Lino spec
449                let flattened = flatten_links(links);
450                Ok(flattened)
451            }
452        }
453        Err(e) => Err(ParseError::SyntaxError(format!("{:?}", e))),
454    }
455}
456
457/// Formats a collection of LiNo links as a multi-line string.
458/// Each link is formatted on a separate line.
459pub fn format_links(links: &[LiNo<String>]) -> String {
460    links
461        .iter()
462        .map(|link| format!("{}", link))
463        .collect::<Vec<_>>()
464        .join("\n")
465}
466
467/// Formats a collection of LiNo links as a multi-line string using FormatConfig.
468/// Supports all formatting options including consecutive link grouping.
469///
470/// # Arguments
471/// * `links` - The collection of links to format
472/// * `config` - The FormatConfig to use for formatting
473///
474/// # Returns
475/// Formatted string in Lino notation
476pub fn format_links_with_config(links: &[LiNo<String>], config: &FormatConfig) -> String {
477    if links.is_empty() {
478        return String::new();
479    }
480
481    // Apply consecutive link grouping if enabled
482    let links_to_format = if config.group_consecutive {
483        group_consecutive_links(links)
484    } else {
485        links.to_vec()
486    };
487
488    links_to_format
489        .iter()
490        .map(|link| link.format_with_config(config))
491        .collect::<Vec<_>>()
492        .join("\n")
493}
494
495/// Groups consecutive links with the same ID.
496///
497/// For example:
498/// ```text
499/// SetA a
500/// SetA b
501/// SetA c
502/// ```
503/// Becomes:
504/// ```text
505/// SetA
506///   a
507///   b
508///   c
509/// ```
510fn group_consecutive_links(links: &[LiNo<String>]) -> Vec<LiNo<String>> {
511    if links.is_empty() {
512        return vec![];
513    }
514
515    let mut grouped = vec![];
516    let mut i = 0;
517
518    while i < links.len() {
519        let current = &links[i];
520
521        // Look ahead for consecutive links with same ID
522        if let LiNo::Link {
523            id: Some(ref current_id),
524            values: ref current_values,
525        } = current
526        {
527            if !current_values.is_empty() {
528                // Collect all values with same ID
529                let mut same_id_values = current_values.clone();
530                let mut j = i + 1;
531
532                while j < links.len() {
533                    if let LiNo::Link {
534                        id: Some(ref next_id),
535                        values: ref next_values,
536                    } = &links[j]
537                    {
538                        if next_id == current_id && !next_values.is_empty() {
539                            same_id_values.extend(next_values.clone());
540                            j += 1;
541                        } else {
542                            break;
543                        }
544                    } else {
545                        break;
546                    }
547                }
548
549                // If we found consecutive links, create grouped link
550                if j > i + 1 {
551                    grouped.push(LiNo::Link {
552                        id: Some(current_id.clone()),
553                        values: same_id_values,
554                    });
555                    i = j;
556                    continue;
557                }
558            }
559        }
560
561        grouped.push(current.clone());
562        i += 1;
563    }
564
565    grouped
566}
567
568/// Escape a reference string by adding quotes if necessary.
569fn escape_reference(reference: &str) -> String {
570    if reference.is_empty() || reference.trim().is_empty() {
571        return String::new();
572    }
573
574    let has_single_quote = reference.contains('\'');
575    let has_double_quote = reference.contains('"');
576
577    let needs_quoting = reference.contains(':')
578        || reference.contains('(')
579        || reference.contains(')')
580        || reference.contains(' ')
581        || reference.contains('\t')
582        || reference.contains('\n')
583        || reference.contains('\r')
584        || has_double_quote
585        || has_single_quote;
586
587    // Handle edge case: reference contains both single and double quotes
588    if has_single_quote && has_double_quote {
589        // Escape single quotes and wrap in single quotes
590        return format!("'{}'", reference.replace('\'', "\\'"));
591    }
592
593    // Prefer single quotes if double quotes are present
594    if has_double_quote {
595        return format!("'{}'", reference);
596    }
597
598    // Use double quotes if single quotes are present
599    if has_single_quote {
600        return format!("\"{}\"", reference);
601    }
602
603    // Use single quotes for special characters
604    if needs_quoting {
605        return format!("'{}'", reference);
606    }
607
608    // No quoting needed
609    reference.to_string()
610}
611
612/// Check if a string needs to be wrapped in parentheses.
613fn needs_parentheses(s: &str) -> bool {
614    s.contains(' ') || s.contains(':') || s.contains('(') || s.contains(')')
615}
616
617/// Format a value within a link.
618fn format_value<T: ToString>(value: &LiNo<T>) -> String {
619    match value {
620        LiNo::Ref(r) => escape_reference(&r.to_string()),
621        LiNo::Link { id, values } => {
622            // Simple link with just an ID - don't wrap in extra parentheses
623            if values.is_empty() {
624                if let Some(ref id_val) = id {
625                    return escape_reference(&id_val.to_string());
626                }
627                return String::new();
628            }
629            // Complex value - format with parentheses
630            format!("{}", value)
631        }
632    }
633}