Skip to main content

pax_language/
helpers.rs

1use std::fs;
2use std::path::Path;
3use syn::spanned::Spanned;
4use syn::visit::Visit;
5use syn::{parse_file, ItemStruct};
6
7#[derive(Debug)]
8/// Source span and contents for a Pax template embedded in an `#[inlined(...)]` attribute.
9pub struct InlinedTemplate {
10    pub struct_name: String,
11    pub start: (usize, usize),
12    pub end: (usize, usize),
13    pub template: String,
14}
15
16#[derive(Debug)]
17/// AST visitor that extracts `#[inlined(...)]` templates from `#[pax]` structs.
18pub struct InlinedTemplateFinder {
19    pub file_contents: String,
20    pub templates: Vec<InlinedTemplate>,
21}
22
23impl InlinedTemplateFinder {
24    /// Prepare a finder for one Rust source file.
25    pub fn new(file_contents: String) -> Self {
26        InlinedTemplateFinder {
27            file_contents,
28            templates: Vec::new(),
29        }
30    }
31}
32
33impl<'ast> Visit<'ast> for InlinedTemplateFinder {
34    fn visit_item_struct(&mut self, i: &'ast ItemStruct) {
35        let mut has_pax = false;
36        let struct_name = i.ident.to_string();
37        for attr in &i.attrs {
38            // Check for #[pax]
39            if attr.path.is_ident("pax") {
40                has_pax = true;
41            }
42
43            if attr.path.is_ident("inlined") {
44                let start = attr.tokens.span().start();
45                let start_tuple = (start.line, start.column + 1);
46                let end = attr.tokens.span().end();
47                let end_tuple = (end.line, end.column + 1);
48                let content =
49                    get_substring_by_line_column(&self.file_contents, start_tuple, end_tuple)
50                        .unwrap()
51                        .trim_start_matches("(")
52                        .trim_end_matches(")")
53                        .to_string();
54                if has_pax {
55                    let found_template = InlinedTemplate {
56                        struct_name: struct_name.clone(),
57                        start: start_tuple,
58                        end: end_tuple,
59                        template: content,
60                    };
61                    self.templates.insert(0, found_template);
62                }
63            }
64        }
65    }
66}
67
68fn find_start_end_bytes(
69    input: &str,
70    start: (usize, usize),
71    end: (usize, usize),
72) -> (Option<usize>, Option<usize>) {
73    let mut current_line = 1;
74    let mut current_column = 1;
75    let mut start_byte = None;
76    let mut end_byte = None;
77
78    for (i, c) in input.char_indices() {
79        if current_line == start.0 && current_column == start.1 {
80            start_byte = Some(i);
81        }
82        if current_line == end.0 && current_column == end.1 {
83            end_byte = Some(i);
84            break;
85        }
86
87        if c == '\n' {
88            current_line += 1;
89            current_column = 1;
90        } else {
91            current_column += 1;
92        }
93    }
94
95    (start_byte, end_byte)
96}
97
98/// Replace a source span addressed by one-indexed line/column coordinates.
99pub fn replace_by_line_column(
100    input: &str,
101    start: (usize, usize),
102    end: (usize, usize),
103    replacement: String,
104) -> Option<String> {
105    let (start_byte, end_byte) = find_start_end_bytes(input, start, end);
106
107    match (start_byte, end_byte) {
108        (Some(start_byte), Some(end_byte)) => {
109            let mut result = String::new();
110            result.push_str(&input[..start_byte]);
111            result.push_str(&replacement);
112            result.push_str(&input[end_byte..]);
113            Some(result)
114        }
115        _ => None,
116    }
117}
118
119/// Extract a source substring addressed by one-indexed line/column coordinates.
120pub fn get_substring_by_line_column(
121    input: &str,
122    start: (usize, usize),
123    end: (usize, usize),
124) -> Option<String> {
125    let (start_byte, end_byte) = find_start_end_bytes(input, start, end);
126
127    match (start_byte, end_byte) {
128        (Some(start_byte), Some(end_byte)) => Some(input[start_byte..end_byte].to_string()),
129        _ => None,
130    }
131}
132
133/// Replace a matching `#[inlined(...)]` template with an empty template body.
134pub fn clear_inlined_template(file_path: &str, pascal_identifier: &str) {
135    let path = Path::new(file_path);
136    let content = fs::read_to_string(path).expect("Failed to read file");
137    let ast = parse_file(&content).expect("Failed to parse file");
138
139    let mut finder = InlinedTemplateFinder::new(content.clone());
140    finder.visit_file(&ast);
141
142    let mut modified_content = content;
143    for template in finder.templates {
144        if template.struct_name == pascal_identifier {
145            let blank_template = format!("()");
146            modified_content = replace_by_line_column(
147                &modified_content,
148                template.start,
149                template.end,
150                blank_template,
151            )
152            .unwrap();
153        }
154    }
155    fs::write(path, modified_content).expect("Failed to write to file");
156}