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;