tylax 0.3.5

Tylax - High-performance bidirectional LaTeX ↔ Typst converter
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
//! Preprocessing for Typst to LaTeX conversion
//!
//! This module implements an AST-based macro interpreter that:
//! 1. Extracts #let definitions from the source
//! 2. Expands macro calls and variable references by traversing the AST
//!
//! This approach is more robust than regex-based substitution because it
//! respects the syntactic structure of the document.

use std::collections::HashMap;
use typst_syntax::{parse, SyntaxKind, SyntaxNode};

/// Database of Typst variable/function definitions
#[derive(Debug, Default, Clone)]
pub struct TypstDefDb {
    /// Simple variable definitions: name -> value (as source text)
    variables: HashMap<String, String>,
    /// Function definitions: name -> (params, body_source_text)
    /// Body is stored as source text so it can be re-parsed for expansion
    functions: HashMap<String, (Vec<String>, String)>,
}

impl TypstDefDb {
    pub fn new() -> Self {
        Self::default()
    }

    /// Define a simple variable
    pub fn define_variable(&mut self, name: &str, value: &str) {
        self.variables.insert(name.to_string(), value.to_string());
    }

    /// Define a function with arguments
    pub fn define_function(&mut self, name: &str, args: Vec<String>, body: &str) {
        self.functions
            .insert(name.to_string(), (args, body.to_string()));
    }

    /// Get a variable value
    pub fn get_variable(&self, name: &str) -> Option<&str> {
        self.variables.get(name).map(|s| s.as_str())
    }

    /// Get all variables
    pub fn variables(&self) -> &HashMap<String, String> {
        &self.variables
    }

    /// Get a function definition
    pub fn get_function(&self, name: &str) -> Option<&(Vec<String>, String)> {
        self.functions.get(name)
    }

    /// Check if a name is defined
    pub fn is_defined(&self, name: &str) -> bool {
        self.variables.contains_key(name) || self.functions.contains_key(name)
    }

    /// Get count of definitions
    pub fn len(&self) -> usize {
        self.variables.len() + self.functions.len()
    }

    /// Check if empty
    pub fn is_empty(&self) -> bool {
        self.variables.is_empty() && self.functions.is_empty()
    }
}

/// AST-based Macro Expander
///
/// This struct implements a tree rewriter that traverses the Typst AST
/// and expands macro calls/variable references on the fly.
pub struct MacroExpander<'a> {
    /// The definition database
    db: &'a TypstDefDb,
    /// Current scope bindings (for macro parameter substitution)
    /// This is a stack of scopes, where each scope is a map of name -> value
    scope_stack: Vec<HashMap<String, String>>,
    /// Recursion depth limit to prevent infinite loops
    max_depth: usize,
    /// Current recursion depth
    current_depth: usize,
}

impl<'a> MacroExpander<'a> {
    pub fn new(db: &'a TypstDefDb) -> Self {
        Self {
            db,
            scope_stack: Vec::new(),
            max_depth: 50, // Reasonable limit for nested macro expansion
            current_depth: 0,
        }
    }

    /// Look up a name in current scopes (innermost first) and db
    fn lookup(&self, name: &str) -> Option<String> {
        // Check scope stack first (innermost scope has priority)
        for scope in self.scope_stack.iter().rev() {
            if let Some(value) = scope.get(name) {
                return Some(value.clone());
            }
        }
        // Then check global variables
        self.db.get_variable(name).map(|s| s.to_string())
    }

    /// Main entry point: expand all macros in the given source
    pub fn expand(&mut self, source: &str) -> String {
        let root = parse(source);
        self.expand_node(&root)
    }

    /// Expand a single AST node, returning the expanded text
    pub fn expand_node(&mut self, node: &SyntaxNode) -> String {
        // Check recursion depth
        if self.current_depth > self.max_depth {
            return get_node_full_text(node);
        }

        match node.kind() {
            // Skip let bindings and set/show rules in output
            SyntaxKind::LetBinding | SyntaxKind::SetRule | SyntaxKind::ShowRule => String::new(),

            // Function call - check if it's a defined macro
            SyntaxKind::FuncCall => self.expand_func_call(node),

            // Identifier - check if it's a defined variable/parameter
            SyntaxKind::Ident => self.expand_ident(node),

            // Hash (code expression) - process its children
            SyntaxKind::Hash => self.expand_hash(node),

            // For all other nodes, recursively expand children
            _ => self.expand_children(node),
        }
    }

    /// Expand a Hash node (#expression)
    fn expand_hash(&self, node: &SyntaxNode) -> String {
        let mut result = String::new();
        let children: Vec<_> = node.children().collect();

        if children.is_empty() {
            // Hash with no children is just the # symbol - skip it
            // (it's Typst syntax, not content)
            return String::new();
        }

        for child in &children {
            match child.kind() {
                // If child is an identifier that we can expand, expand it without #
                SyntaxKind::Ident => {
                    let name = child.text().to_string();
                    if let Some(value) = self.lookup(&name) {
                        result.push_str(&value);
                    } else {
                        // Keep the # prefix for unknown identifiers
                        result.push('#');
                        result.push_str(&name);
                    }
                }
                // For function calls, check if it's a macro
                SyntaxKind::FuncCall => {
                    let func_name = self.get_func_name(child);
                    if self.db.is_defined(&func_name) {
                        // It's a macro - expand without # (the expansion handles output)
                        let expanded = self.expand_func_call_immut(child);
                        result.push_str(&expanded);
                    } else {
                        // Not a macro - keep the # prefix for Typst built-in functions
                        result.push('#');
                        result.push_str(&get_node_full_text(child));
                    }
                }
                _ => {
                    result.push_str(child.text().as_ref());
                }
            }
        }

        result
    }

    /// Expand an identifier node
    fn expand_ident(&self, node: &SyntaxNode) -> String {
        let name = node.text().to_string();

        // Check if this identifier is in our scope or db
        if let Some(value) = self.lookup(&name) {
            value
        } else {
            // Return unchanged
            name
        }
    }

    /// Expand a function call node (mutable version for internal use)
    fn expand_func_call(&mut self, node: &SyntaxNode) -> String {
        let func_name = self.get_func_name(node);

        // Check if this is a defined macro
        if let Some((params, body)) = self.db.get_function(&func_name).cloned() {
            // Parse actual arguments
            let actual_args = self.parse_args(node);

            // Perform text-based substitution since the body may not parse as code
            let expanded = self.substitute_params(&body, &params, &actual_args);

            // Recursively expand in case there are nested macro calls
            self.current_depth += 1;
            let result = self.expand(&expanded);
            self.current_depth -= 1;

            result
        } else {
            // Not a macro - expand children normally
            self.expand_children(node)
        }
    }

    /// Substitute parameters in body text with actual argument values
    fn substitute_params(&self, body: &str, params: &[String], args: &[String]) -> String {
        let mut result = body.to_string();

        for (i, param) in params.iter().enumerate() {
            if let Some(arg) = args.get(i) {
                // Replace #param references (common in Typst function bodies)
                let hash_pattern = format!("#{}", param);
                result = result.replace(&hash_pattern, arg);

                // Replace parameter references using regex word boundaries to avoid partial matches.
                // Example: param "a" should replace "a" but not "apple".
                result = replace_word(&result, param, arg);
            }
        }

        result
    }

    /// Expand a function call node (immutable version for use in closures)
    fn expand_func_call_immut(&self, node: &SyntaxNode) -> String {
        let func_name = self.get_func_name(node);

        // Check if this is a defined macro
        if let Some((params, body)) = self.db.get_function(&func_name).cloned() {
            // Parse actual arguments
            let actual_args = self.parse_args_immut(node);

            // Perform text-based substitution
            let expanded = self.substitute_params(&body, &params, &actual_args);

            // Create a new expander for recursive expansion
            let mut expander = MacroExpander::new(self.db);
            expander.scope_stack = self.scope_stack.clone();
            expander.current_depth = self.current_depth.saturating_add(1);

            if expander.current_depth > expander.max_depth {
                return get_node_full_text(node);
            }

            expander.expand(&expanded)
        } else {
            // Not a macro - return original text
            get_node_full_text(node)
        }
    }

    /// Get the function name from a FuncCall node
    fn get_func_name(&self, node: &SyntaxNode) -> String {
        for child in node.children() {
            match child.kind() {
                SyntaxKind::Ident => return child.text().to_string(),
                SyntaxKind::FieldAccess => {
                    // For math.vec, we want "vec" as the local name
                    // but also track full path for future use
                    return self.get_field_access_name(child);
                }
                _ => {}
            }
        }
        String::new()
    }

    /// Get name from FieldAccess (e.g., "vec" from "math.vec")
    fn get_field_access_name(&self, node: &SyntaxNode) -> String {
        let mut parts = Vec::new();
        Self::collect_field_access_parts(node, &mut parts);
        // Return the last part (the actual function name)
        parts.last().cloned().unwrap_or_default()
    }

    fn collect_field_access_parts(node: &SyntaxNode, parts: &mut Vec<String>) {
        for child in node.children() {
            match child.kind() {
                SyntaxKind::Ident => parts.push(child.text().to_string()),
                SyntaxKind::FieldAccess => Self::collect_field_access_parts(child, parts),
                _ => {}
            }
        }
    }

    /// Parse function arguments from FuncCall node
    fn parse_args(&self, node: &SyntaxNode) -> Vec<String> {
        let mut args = Vec::new();

        for child in node.children() {
            if child.kind() == SyntaxKind::Args {
                for arg_child in child.children() {
                    match arg_child.kind() {
                        // Skip commas, parens, and other syntax
                        SyntaxKind::LeftParen
                        | SyntaxKind::RightParen
                        | SyntaxKind::Comma
                        | SyntaxKind::Space => {}
                        // For actual arguments, get the expanded text
                        _ => {
                            let arg_text = arg_child.text().to_string().trim().to_string();
                            if !arg_text.is_empty() {
                                args.push(arg_text);
                            }
                        }
                    }
                }
            }
            // Also check for content blocks [...] as trailing arguments
            if child.kind() == SyntaxKind::ContentBlock {
                let content = child.text().to_string();
                // Strip the [ and ]
                let inner = content
                    .trim_start_matches('[')
                    .trim_end_matches(']')
                    .to_string();
                args.push(inner);
            }
        }

        args
    }

    /// Parse function arguments (immutable version)
    fn parse_args_immut(&self, node: &SyntaxNode) -> Vec<String> {
        self.parse_args(node)
    }

    /// Expand all children and concatenate
    fn expand_children(&mut self, node: &SyntaxNode) -> String {
        let mut result = String::new();
        let children: Vec<_> = node.children().collect();
        let mut i = 0;

        while i < children.len() {
            let child = &children[i];

            // Check for Hash followed by SetRule/ShowRule/LetBinding - skip both
            if child.kind() == SyntaxKind::Hash && i + 1 < children.len() {
                let next = &children[i + 1];
                if matches!(
                    next.kind(),
                    SyntaxKind::SetRule | SyntaxKind::ShowRule | SyntaxKind::LetBinding
                ) {
                    // Skip both Hash and the rule
                    i += 2;
                    continue;
                }

                // Check for Hash followed by FuncCall - handle specially
                if next.kind() == SyntaxKind::FuncCall {
                    let func_name = self.get_func_name(next);
                    if self.db.is_defined(&func_name) {
                        // It's a macro - expand without # prefix
                        result.push_str(&self.expand_func_call(next));
                    } else {
                        // Not a macro - keep # prefix and output function call
                        result.push('#');
                        result.push_str(&get_node_full_text(next));
                    }
                    i += 2; // Skip both Hash and FuncCall
                    continue;
                }
            }

            result.push_str(&self.expand_node(child));
            i += 1;
        }

        // If no children, return the node's full text
        if children.is_empty() {
            return get_node_full_text(node);
        }

        result
    }
}

/// Replace a word in text (respecting word boundaries)
fn replace_word(text: &str, word: &str, replacement: &str) -> String {
    let mut result = String::new();
    let chars: Vec<char> = text.chars().collect();
    let word_chars: Vec<char> = word.chars().collect();
    let mut i = 0;

    while i < chars.len() {
        // Check if we have a potential match
        if i + word_chars.len() <= chars.len() {
            let slice: String = chars[i..i + word_chars.len()].iter().collect();
            if slice == word {
                // Check word boundaries
                let before_ok = i == 0 || !chars[i - 1].is_alphanumeric();
                let after_ok = i + word_chars.len() >= chars.len()
                    || !chars[i + word_chars.len()].is_alphanumeric();

                if before_ok && after_ok {
                    result.push_str(replacement);
                    i += word_chars.len();
                    continue;
                }
            }
        }
        result.push(chars[i]);
        i += 1;
    }

    result
}

/// Extract #let definitions from Typst source using AST and return cleaned source
pub fn extract_let_definitions(input: &str) -> (TypstDefDb, String) {
    extract_let_from_ast(input)
}

/// Get the full text of a syntax node (handles inner nodes)
fn get_node_full_text(node: &SyntaxNode) -> String {
    // For leaf nodes, text() returns the content
    // For inner nodes, text() returns empty, so we need to clone and use into_text
    let text = node.text().to_string();
    if !text.is_empty() {
        return text;
    }
    // Clone and use into_text for inner nodes
    node.clone().into_text().to_string()
}

/// Extract #let definitions using typst-syntax AST
pub fn extract_let_from_ast(input: &str) -> (TypstDefDb, String) {
    let mut db = TypstDefDb::new();
    let root = parse(input);

    // Collect patterns to remove (exact text matches)
    let mut patterns_to_remove: Vec<String> = Vec::new();

    extract_lets_recursive(&root, &mut db, &mut patterns_to_remove);

    // Remove let bindings from source
    let mut result = input.to_string();
    for pattern in &patterns_to_remove {
        result = result.replace(pattern, "");
    }

    // Clean up multiple blank lines
    while result.contains("\n\n\n") {
        result = result.replace("\n\n\n", "\n\n");
    }

    (db, result.trim().to_string())
}

/// Recursively extract let bindings from AST
fn extract_lets_recursive(node: &SyntaxNode, db: &mut TypstDefDb, patterns: &mut Vec<String>) {
    // In the Typst AST, LetBinding is a sibling of Hash, not a child
    // We need to look at consecutive siblings: Hash followed by LetBinding
    let children: Vec<_> = node.children().collect();
    let mut i = 0;

    while i < children.len() {
        let child = &children[i];

        match child.kind() {
            SyntaxKind::Hash => {
                // Check if next sibling is a LetBinding
                if i + 1 < children.len() && children[i + 1].kind() == SyntaxKind::LetBinding {
                    let let_binding = &children[i + 1];

                    // Check if this is a function definition (has Closure node)
                    let has_closure = let_binding
                        .children()
                        .any(|c| c.kind() == SyntaxKind::Closure);
                    let has_params = let_binding
                        .children()
                        .any(|c| c.kind() == SyntaxKind::Params);

                    if has_closure || has_params {
                        extract_function_definition(let_binding, db);
                    } else {
                        extract_variable_definition(let_binding, db);
                    }

                    // Build the pattern to remove: # + let binding text
                    let hash_text = get_node_full_text(child);
                    let let_text = get_node_full_text(let_binding);
                    let pattern = format!("{}{}", hash_text, let_text);
                    patterns.push(pattern);

                    // Skip the LetBinding we just processed
                    i += 2;
                    continue;
                } else {
                    // Just a Hash without LetBinding, recurse into it
                    extract_lets_recursive(child, db, patterns);
                }
            }
            SyntaxKind::LetBinding => {
                // Standalone LetBinding (shouldn't happen in valid Typst, but handle it)
                let full_text = get_node_full_text(child);

                let has_closure = child.children().any(|c| c.kind() == SyntaxKind::Closure);
                let has_params = child.children().any(|c| c.kind() == SyntaxKind::Params);

                if has_closure || has_params {
                    extract_function_definition(child, db);
                } else {
                    extract_variable_definition(child, db);
                }

                patterns.push(full_text);
            }
            _ => {
                // Recurse into other nodes
                extract_lets_recursive(child, db, patterns);
            }
        }

        i += 1;
    }
}

/// Extract a simple variable definition from AST node
fn extract_variable_definition(node: &SyntaxNode, db: &mut TypstDefDb) {
    let mut name: Option<String> = None;
    let mut value: Option<String> = None;

    for child in node.children() {
        match child.kind() {
            SyntaxKind::Ident => {
                if name.is_none() {
                    name = Some(child.text().to_string());
                }
            }
            // Value could be various types
            SyntaxKind::Int
            | SyntaxKind::Float
            | SyntaxKind::Str
            | SyntaxKind::Bool
            | SyntaxKind::ContentBlock
            | SyntaxKind::FuncCall
            | SyntaxKind::Math
            | SyntaxKind::Equation => {
                if name.is_some() && value.is_none() {
                    value = Some(child.text().to_string());
                }
            }
            _ => {
                // For complex expressions, use the text representation
                if name.is_some() && value.is_none() {
                    let text = child.text().to_string();
                    if !text.trim().is_empty() && text != "=" && text != "#" && text != "let" {
                        value = Some(text);
                    }
                }
            }
        }
    }

    if let (Some(n), Some(v)) = (name, value) {
        db.define_variable(&n, &v);
    }
}

/// Extract a function definition from AST node
fn extract_function_definition(node: &SyntaxNode, db: &mut TypstDefDb) {
    let mut name: Option<String> = None;
    let mut params: Vec<String> = Vec::new();
    let mut body: Option<String> = None;

    for child in node.children() {
        match child.kind() {
            SyntaxKind::Ident => {
                // Direct identifier at LetBinding level (old style)
                if name.is_none() {
                    name = Some(child.text().to_string());
                }
            }
            SyntaxKind::Params => {
                // Direct params at LetBinding level (old style)
                extract_params_from_node(child, &mut params);
            }
            SyntaxKind::Closure => {
                // New style: Closure contains the function name, params, and body
                // Structure: Closure -> [Ident (name), Params, Eq, Body]
                for closure_child in child.children() {
                    match closure_child.kind() {
                        SyntaxKind::Ident => {
                            // Function name is inside Closure
                            if name.is_none() {
                                name = Some(closure_child.text().to_string());
                            }
                        }
                        SyntaxKind::Params => {
                            extract_params_from_node(closure_child, &mut params);
                        }
                        // Body types - store the FULL source text using helper
                        SyntaxKind::ContentBlock
                        | SyntaxKind::Math
                        | SyntaxKind::Equation
                        | SyntaxKind::Code
                        | SyntaxKind::CodeBlock
                        | SyntaxKind::FuncCall => {
                            body = Some(get_node_full_text(closure_child));
                        }
                        _ => {
                            // For other content, if we already have name and params
                            // but no body yet, capture it
                            if body.is_none() && name.is_some() && !params.is_empty() {
                                let text = get_node_full_text(closure_child);
                                if !text.trim().is_empty()
                                    && !matches!(text.trim(), "(" | ")" | "," | "=>" | "=" | " ")
                                {
                                    body = Some(text);
                                }
                            }
                        }
                    }
                }
            }
            // Direct body at LetBinding level
            SyntaxKind::ContentBlock
            | SyntaxKind::Math
            | SyntaxKind::Equation
            | SyntaxKind::FuncCall => {
                if name.is_some() && body.is_none() {
                    body = Some(child.text().to_string());
                }
            }
            _ => {}
        }
    }

    if let Some(n) = name {
        // Store the body as-is for re-parsing
        let body_str = body.unwrap_or_default();
        db.define_function(&n, params, &body_str);
    }
}

/// Extract parameter names from a Params node
fn extract_params_from_node(params_node: &SyntaxNode, params: &mut Vec<String>) {
    for child in params_node.children() {
        match child.kind() {
            SyntaxKind::Ident => {
                params.push(child.text().to_string());
            }
            _ => {
                // For complex parameter patterns, try to find the identifier
                for sub_child in child.children() {
                    if sub_child.kind() == SyntaxKind::Ident {
                        params.push(sub_child.text().to_string());
                        break;
                    }
                }
            }
        }
    }
}

/// Preprocess Typst source: extract definitions and expand using AST
pub fn preprocess_typst(input: &str) -> String {
    // Step 1: Extract definitions
    let (db, cleaned) = extract_let_from_ast(input);

    // If no definitions, return cleaned source as-is
    if db.is_empty() {
        return cleaned;
    }

    // Step 2: Create macro expander and expand
    let mut expander = MacroExpander::new(&db);
    expander.expand(&cleaned)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extract_simple_let() {
        let input = r#"#let x = 5
#let name = "hello"
Some text with #x"#;

        let (db, cleaned) = extract_let_definitions(input);

        assert_eq!(db.get_variable("x"), Some("5"));
        assert_eq!(db.get_variable("name"), Some("\"hello\""));
        assert!(!cleaned.contains("#let x"));
        assert!(cleaned.contains("Some text"));
    }

    #[test]
    fn test_preprocess_full() {
        let input = r#"#let greeting = "Hello"
#greeting World"#;

        let result = preprocess_typst(input);
        assert!(result.contains("Hello") || result.contains("World"));
    }

    #[test]
    fn test_function_expansion() {
        let input = r#"#let double(x) = $2 #x$
The result is #double(5)"#;

        let result = preprocess_typst(input);
        // After expansion, #double(5) should become $2 5$
        assert!(result.contains("2") && result.contains("5"));
    }

    #[test]
    fn test_nested_function_expansion() {
        let input = r#"#let f(x) = $#x^2$
#let g(y) = #f(#y)
The result is #g(3)"#;

        let result = preprocess_typst(input);
        // After expansion, should contain 3^2
        assert!(result.contains("3") || result.contains("^2"));
    }

    #[test]
    fn test_macro_expander_depth_limit() {
        let mut db = TypstDefDb::new();
        // Create a recursive macro (should hit depth limit)
        db.define_function("recurse", vec!["x".to_string()], "#recurse(#x)");

        let mut expander = MacroExpander::new(&db);
        expander.max_depth = 5;

        // This should not hang - depth limit kicks in
        let result = expander.expand("#recurse(1)");
        assert!(!result.is_empty());
    }

    #[test]
    fn test_typst_def_db() {
        let mut db = TypstDefDb::new();
        assert!(db.is_empty());

        db.define_variable("x", "10");
        assert!(!db.is_empty());
        assert_eq!(db.len(), 1);
        assert!(db.is_defined("x"));
        assert!(!db.is_defined("y"));
    }

    #[test]
    fn test_math_let() {
        let input = r#"#let alpha = $\alpha$
The Greek letter is #alpha"#;

        let (db, cleaned) = extract_let_definitions(input);
        assert!(db.is_defined("alpha"));
        assert!(!cleaned.contains("#let"));
    }

    #[test]
    fn test_myvec_expansion() {
        let input = r#"#let myvec(x, y) = $vec(#x, #y)$
The vector is #myvec(a, b)"#;

        let result = preprocess_typst(input);
        // Should expand to contain vec(a, b) in some form
        assert!(result.contains("vec") || result.contains("a") && result.contains("b"));
    }
}