Skip to main content

links_notation_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenTree;
3use quote::quote;
4use syn::{parse::Parse, parse::ParseStream, LitStr};
5
6/// Procedural macro that provides compile-time validation of Links Notation syntax.
7///
8/// This macro takes Links Notation and validates it at compile time.
9/// At runtime, it calls the parser to construct the `LiNo` structure, but any syntax errors
10/// are caught during compilation.
11///
12/// # Syntax Options
13///
14/// The macro supports two syntax options:
15///
16/// ## 1. Direct Syntax (Recommended)
17///
18/// Write Links Notation directly without quotes:
19///
20/// ```rust,ignore
21/// use links_notation::lino;
22///
23/// let result = lino!(papa (lovesMama: loves mama));
24/// let triplet = lino!(papa has car);
25/// let nested = lino!((outer: (inner: value)));
26/// ```
27///
28/// ## 2. String Literal Syntax
29///
30/// Use string literals for complex cases with special characters:
31///
32/// ```rust,ignore
33/// use links_notation::lino;
34///
35/// let result = lino!("papa (lovesMama: loves mama)");
36/// let with_newlines = lino!("line1\nline2");
37/// let with_quotes = lino!(r#"("quoted id": "quoted value")"#);
38/// ```
39///
40/// # Examples
41///
42/// ```rust,ignore
43/// use links_notation::lino;
44///
45/// // Direct syntax - cleaner and more native
46/// let result = lino!(papa (lovesMama: loves mama));
47///
48/// // String literal for special characters
49/// let result = lino!("contains special: chars");
50///
51/// // Syntax errors caught at compile time!
52/// // let invalid = lino!((unclosed);  // ← Compile error
53/// ```
54///
55/// # Benefits
56///
57/// - **Compile-time validation**: Syntax errors are caught at compile time
58/// - **Zero overhead**: Simple wrapper around the runtime parser
59/// - **Type-safe**: Returns fully typed `LiNo<String>` structures
60/// - **Convenient**: No need to manually handle parse errors in most cases
61/// - **Native syntax**: Direct syntax option for cleaner, quote-free code
62///
63/// # Implementation
64///
65/// The macro expands to code that:
66/// 1. Contains a compile-time validation check
67/// 2. Calls `parse_lino()` at runtime
68/// 3. Unwraps the result (safe because validation passed at compile time)
69#[proc_macro]
70pub fn lino(input: TokenStream) -> TokenStream {
71    let input2: proc_macro2::TokenStream = input.into();
72
73    // Try to parse as a string literal first
74    let lino_str = match syn::parse2::<LitStr>(input2.clone()) {
75        Ok(lit_str) => lit_str.value(),
76        Err(_) => {
77            // Not a string literal, parse as direct tokens
78            match syn::parse2::<DirectLinoInput>(input2.clone()) {
79                Ok(direct) => direct.content,
80                Err(e) => {
81                    return syn::Error::new(
82                        proc_macro2::Span::call_site(),
83                        format!("Failed to parse Links Notation input: {}", e),
84                    )
85                    .to_compile_error()
86                    .into();
87                }
88            }
89        }
90    };
91
92    // Validate syntax at compile time using a simple parser
93    // We can't use the full runtime parser here due to cyclic dependencies,
94    // so we do basic validation
95    if let Err(e) = validate_lino_syntax(&lino_str) {
96        return syn::Error::new(
97            proc_macro2::Span::call_site(),
98            format!("Invalid Links Notation: {}", e),
99        )
100        .to_compile_error()
101        .into();
102    }
103
104    // Generate code that parses at runtime
105    // The const assertion ensures the string is valid at compile time
106    let expanded = quote! {
107        {
108            // Compile-time validation marker
109            const _: () = {
110                // This validates the string literal is well-formed
111                let _ = #lino_str;
112            };
113
114            // Runtime parsing
115            links_notation::parse_lino(#lino_str).expect("lino! macro: validated at compile time but runtime parse failed")
116        }
117    };
118
119    TokenStream::from(expanded)
120}
121
122/// Custom parser for direct Links Notation syntax without string literals.
123///
124/// This parser converts tokens directly to a Links Notation string.
125struct DirectLinoInput {
126    content: String,
127}
128
129impl Parse for DirectLinoInput {
130    fn parse(input: ParseStream) -> syn::Result<Self> {
131        let mut content = String::new();
132        let tokens: proc_macro2::TokenStream = input.parse()?;
133
134        tokens_to_lino_string(tokens, &mut content);
135
136        Ok(DirectLinoInput { content })
137    }
138}
139
140/// Convert a token stream to a Links Notation string representation.
141///
142/// This function handles the conversion of Rust tokens to the equivalent
143/// Links Notation text, preserving the structure and meaning.
144fn tokens_to_lino_string(tokens: proc_macro2::TokenStream, output: &mut String) {
145    let mut prev_needs_space = false;
146    let mut tokens_iter = tokens.into_iter().peekable();
147
148    while let Some(token) = tokens_iter.next() {
149        match token {
150            TokenTree::Ident(ident) => {
151                if prev_needs_space {
152                    output.push(' ');
153                }
154                output.push_str(&ident.to_string());
155                prev_needs_space = true;
156            }
157            TokenTree::Punct(punct) => {
158                let ch = punct.as_char();
159                match ch {
160                    ':' => {
161                        // Colon is used for ID separator in Links Notation
162                        // Don't add space before colon, but add space after
163                        output.push(':');
164                        prev_needs_space = true;
165                    }
166                    '-' => {
167                        // Check if this is part of a negative number or hyphenated word
168                        // Look at next token
169                        if let Some(TokenTree::Literal(_) | TokenTree::Ident(_)) =
170                            tokens_iter.peek()
171                        {
172                            // Part of a compound like -123 or hyphenated word
173                            if prev_needs_space {
174                                output.push(' ');
175                            }
176                            output.push('-');
177                            prev_needs_space = false;
178                        } else {
179                            if prev_needs_space {
180                                output.push(' ');
181                            }
182                            output.push('-');
183                            prev_needs_space = true;
184                        }
185                    }
186                    '_' => {
187                        // Underscore might be part of an identifier
188                        output.push('_');
189                        prev_needs_space = false;
190                    }
191                    '.' => {
192                        // Period - could be decimal or sentence end
193                        output.push('.');
194                        prev_needs_space = false;
195                    }
196                    '\'' => {
197                        // Single quote
198                        output.push('\'');
199                        prev_needs_space = false;
200                    }
201                    '"' => {
202                        // Double quote (escaped)
203                        output.push('"');
204                        prev_needs_space = false;
205                    }
206                    _ => {
207                        // Other punctuation
208                        if prev_needs_space && !matches!(ch, ',' | ';' | '!' | '?') {
209                            output.push(' ');
210                        }
211                        output.push(ch);
212                        prev_needs_space = !matches!(ch, '(' | '[' | '{' | '<');
213                    }
214                }
215            }
216            TokenTree::Literal(lit) => {
217                if prev_needs_space {
218                    output.push(' ');
219                }
220                // Handle different literal types
221                let lit_str = lit.to_string();
222
223                // Check if it's a string literal (starts and ends with quotes)
224                if (lit_str.starts_with('"') && lit_str.ends_with('"'))
225                    || (lit_str.starts_with('\'') && lit_str.ends_with('\''))
226                {
227                    // It's a quoted string literal in Rust, use it as-is in Links Notation
228                    output.push_str(&lit_str);
229                } else {
230                    // Numeric or other literal
231                    output.push_str(&lit_str);
232                }
233                prev_needs_space = true;
234            }
235            TokenTree::Group(group) => {
236                let delimiter = group.delimiter();
237                match delimiter {
238                    proc_macro2::Delimiter::Parenthesis => {
239                        // In Links Notation, parentheses define links
240                        if prev_needs_space {
241                            output.push(' ');
242                        }
243                        output.push('(');
244                        tokens_to_lino_string(group.stream(), output);
245                        output.push(')');
246                        prev_needs_space = true;
247                    }
248                    proc_macro2::Delimiter::Bracket => {
249                        // Square brackets - pass through
250                        if prev_needs_space {
251                            output.push(' ');
252                        }
253                        output.push('[');
254                        tokens_to_lino_string(group.stream(), output);
255                        output.push(']');
256                        prev_needs_space = true;
257                    }
258                    proc_macro2::Delimiter::Brace => {
259                        // Curly braces - pass through
260                        if prev_needs_space {
261                            output.push(' ');
262                        }
263                        output.push('{');
264                        tokens_to_lino_string(group.stream(), output);
265                        output.push('}');
266                        prev_needs_space = true;
267                    }
268                    proc_macro2::Delimiter::None => {
269                        // No delimiter group
270                        tokens_to_lino_string(group.stream(), output);
271                    }
272                }
273            }
274        }
275    }
276}
277
278/// Basic syntax validation for Links Notation.
279/// This is a simplified validator that catches common errors without needing the full parser.
280fn validate_lino_syntax(input: &str) -> Result<(), String> {
281    // Check for balanced parentheses
282    let mut depth = 0;
283    let mut in_single_quote = false;
284    let mut in_double_quote = false;
285    let mut escape_next = false;
286
287    for c in input.chars() {
288        if escape_next {
289            escape_next = false;
290            continue;
291        }
292
293        match c {
294            '\\' => escape_next = true,
295            '\'' if !in_double_quote => in_single_quote = !in_single_quote,
296            '"' if !in_single_quote => in_double_quote = !in_double_quote,
297            '(' if !in_single_quote && !in_double_quote => depth += 1,
298            ')' if !in_single_quote && !in_double_quote => {
299                depth -= 1;
300                if depth < 0 {
301                    return Err("Unmatched closing parenthesis".to_string());
302                }
303            }
304            _ => {}
305        }
306    }
307
308    if depth != 0 {
309        return Err(format!(
310            "Unbalanced parentheses: {} unclosed opening parenthes{}",
311            depth,
312            if depth == 1 { "is" } else { "es" }
313        ));
314    }
315
316    if in_single_quote {
317        return Err("Unclosed single quote".to_string());
318    }
319
320    if in_double_quote {
321        return Err("Unclosed double quote".to_string());
322    }
323
324    Ok(())
325}
326
327// Unit tests are in a separate file: tests.rs
328#[cfg(test)]
329mod tests;