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, panicking with the position of the defect if the
69///    parser refuses the text after all
70#[proc_macro]
71pub fn lino(input: TokenStream) -> TokenStream {
72    let input2: proc_macro2::TokenStream = input.into();
73
74    // Try to parse as a string literal first
75    let lino_str = match syn::parse2::<LitStr>(input2.clone()) {
76        Ok(lit_str) => lit_str.value(),
77        Err(_) => {
78            // Not a string literal, parse as direct tokens
79            match syn::parse2::<DirectLinoInput>(input2.clone()) {
80                Ok(direct) => direct.content,
81                Err(e) => {
82                    return syn::Error::new(
83                        proc_macro2::Span::call_site(),
84                        format!("Failed to parse Links Notation input: {}", e),
85                    )
86                    .to_compile_error()
87                    .into();
88                }
89            }
90        }
91    };
92
93    // Validate syntax at compile time using a simple parser
94    // We can't use the full runtime parser here due to cyclic dependencies,
95    // so we do basic validation
96    if let Err(e) = validate_lino_syntax(&lino_str) {
97        return syn::Error::new(
98            proc_macro2::Span::call_site(),
99            format!("Invalid Links Notation: {}", e),
100        )
101        .to_compile_error()
102        .into();
103    }
104
105    // Generate code that parses at runtime
106    // The const assertion ensures the string is valid at compile time
107    let expanded = quote! {
108        {
109            // Compile-time validation marker
110            const _: () = {
111                // This validates the string literal is well-formed
112                let _ = #lino_str;
113            };
114
115            // Runtime parsing. Compile-time validation only checks that
116            // parentheses and quotes balance, so the parser can still refuse
117            // the text; when it does, the panic says where it stopped rather
118            // than only that it did.
119            match links_notation::parse_lino(#lino_str) {
120                Ok(parsed) => parsed,
121                Err(error) => panic!("lino!: {error}"),
122            }
123        }
124    };
125
126    TokenStream::from(expanded)
127}
128
129/// Custom parser for direct Links Notation syntax without string literals.
130///
131/// This parser converts tokens directly to a Links Notation string.
132struct DirectLinoInput {
133    content: String,
134}
135
136impl Parse for DirectLinoInput {
137    fn parse(input: ParseStream) -> syn::Result<Self> {
138        let mut content = String::new();
139        let tokens: proc_macro2::TokenStream = input.parse()?;
140
141        tokens_to_lino_string(tokens, &mut content);
142
143        Ok(DirectLinoInput { content })
144    }
145}
146
147/// Convert a token stream to a Links Notation string representation.
148///
149/// This function handles the conversion of Rust tokens to the equivalent
150/// Links Notation text, preserving the structure and meaning.
151fn tokens_to_lino_string(tokens: proc_macro2::TokenStream, output: &mut String) {
152    let mut prev_needs_space = false;
153    let mut tokens_iter = tokens.into_iter().peekable();
154
155    while let Some(token) = tokens_iter.next() {
156        match token {
157            TokenTree::Ident(ident) => {
158                if prev_needs_space {
159                    output.push(' ');
160                }
161                output.push_str(&ident.to_string());
162                prev_needs_space = true;
163            }
164            TokenTree::Punct(punct) => {
165                let ch = punct.as_char();
166                match ch {
167                    ':' => {
168                        // Colon is used for ID separator in Links Notation
169                        // Don't add space before colon, but add space after
170                        output.push(':');
171                        prev_needs_space = true;
172                    }
173                    '-' => {
174                        // Check if this is part of a negative number or hyphenated word
175                        // Look at next token
176                        if let Some(TokenTree::Literal(_) | TokenTree::Ident(_)) =
177                            tokens_iter.peek()
178                        {
179                            // Part of a compound like -123 or hyphenated word
180                            if prev_needs_space {
181                                output.push(' ');
182                            }
183                            output.push('-');
184                            prev_needs_space = false;
185                        } else {
186                            if prev_needs_space {
187                                output.push(' ');
188                            }
189                            output.push('-');
190                            prev_needs_space = true;
191                        }
192                    }
193                    '_' => {
194                        // Underscore might be part of an identifier
195                        output.push('_');
196                        prev_needs_space = false;
197                    }
198                    '.' => {
199                        // Period - could be decimal or sentence end
200                        output.push('.');
201                        prev_needs_space = false;
202                    }
203                    '\'' => {
204                        // Single quote
205                        output.push('\'');
206                        prev_needs_space = false;
207                    }
208                    '"' => {
209                        // Double quote (escaped)
210                        output.push('"');
211                        prev_needs_space = false;
212                    }
213                    _ => {
214                        // Other punctuation
215                        if prev_needs_space && !matches!(ch, ',' | ';' | '!' | '?') {
216                            output.push(' ');
217                        }
218                        output.push(ch);
219                        prev_needs_space = !matches!(ch, '(' | '[' | '{' | '<');
220                    }
221                }
222            }
223            TokenTree::Literal(lit) => {
224                if prev_needs_space {
225                    output.push(' ');
226                }
227                // Handle different literal types
228                let lit_str = lit.to_string();
229
230                // Check if it's a string literal (starts and ends with quotes)
231                if (lit_str.starts_with('"') && lit_str.ends_with('"'))
232                    || (lit_str.starts_with('\'') && lit_str.ends_with('\''))
233                {
234                    // It's a quoted string literal in Rust, use it as-is in Links Notation
235                    output.push_str(&lit_str);
236                } else {
237                    // Numeric or other literal
238                    output.push_str(&lit_str);
239                }
240                prev_needs_space = true;
241            }
242            TokenTree::Group(group) => {
243                let delimiter = group.delimiter();
244                match delimiter {
245                    proc_macro2::Delimiter::Parenthesis => {
246                        // In Links Notation, parentheses define links
247                        if prev_needs_space {
248                            output.push(' ');
249                        }
250                        output.push('(');
251                        tokens_to_lino_string(group.stream(), output);
252                        output.push(')');
253                        prev_needs_space = true;
254                    }
255                    proc_macro2::Delimiter::Bracket => {
256                        // Square brackets - pass through
257                        if prev_needs_space {
258                            output.push(' ');
259                        }
260                        output.push('[');
261                        tokens_to_lino_string(group.stream(), output);
262                        output.push(']');
263                        prev_needs_space = true;
264                    }
265                    proc_macro2::Delimiter::Brace => {
266                        // Curly braces - pass through
267                        if prev_needs_space {
268                            output.push(' ');
269                        }
270                        output.push('{');
271                        tokens_to_lino_string(group.stream(), output);
272                        output.push('}');
273                        prev_needs_space = true;
274                    }
275                    proc_macro2::Delimiter::None => {
276                        // No delimiter group
277                        tokens_to_lino_string(group.stream(), output);
278                    }
279                }
280            }
281        }
282    }
283}
284
285/// Basic syntax validation for Links Notation.
286/// This is a simplified validator that catches common errors without needing the full parser.
287fn validate_lino_syntax(input: &str) -> Result<(), String> {
288    // Check for balanced parentheses
289    let mut depth = 0;
290    let mut in_single_quote = false;
291    let mut in_double_quote = false;
292    let mut escape_next = false;
293
294    for c in input.chars() {
295        if escape_next {
296            escape_next = false;
297            continue;
298        }
299
300        match c {
301            '\\' => escape_next = true,
302            '\'' if !in_double_quote => in_single_quote = !in_single_quote,
303            '"' if !in_single_quote => in_double_quote = !in_double_quote,
304            '(' if !in_single_quote && !in_double_quote => depth += 1,
305            ')' if !in_single_quote && !in_double_quote => {
306                depth -= 1;
307                if depth < 0 {
308                    return Err("Unmatched closing parenthesis".to_string());
309                }
310            }
311            _ => {}
312        }
313    }
314
315    if depth != 0 {
316        return Err(format!(
317            "Unbalanced parentheses: {} unclosed opening parenthes{}",
318            depth,
319            if depth == 1 { "is" } else { "es" }
320        ));
321    }
322
323    if in_single_quote {
324        return Err("Unclosed single quote".to_string());
325    }
326
327    if in_double_quote {
328        return Err("Unclosed double quote".to_string());
329    }
330
331    Ok(())
332}
333
334// Unit tests are in a separate file: tests.rs
335#[cfg(test)]
336mod tests;