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;