aboutsummaryrefslogtreecommitdiff
path: root/just_template/src/expand.rs
blob: 954d903309188019d82d210742eaa4ca6eb76b58 (plain) (blame)
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
use std::collections::HashMap;

use just_fmt::snake_case;

use crate::template::Template;

const DISPLAY_BLOCK_BEGIN: &str = "??? >>> ";
const DISPLAY_BLOCK_END: &str = "??? <<<";

const IMPL_AREA_BEGIN: &str = "@@@ >>> ";
const IMPL_AREA_END: &str = "@@@ <<<";

const IMPL_BEGIN: &str = ">>>>>>>>>>";

const PARAM_BEGIN: &str = "<<<";
const PARAM_BEND: &str = ">>>";

impl Template {
    pub fn expand(mut self) -> Option<String> {
        // Extract template text
        let expanded = std::mem::take(&mut self.template_str);

        let (expanded, impl_areas) = read_impl_areas(expanded)?;
        let expanded = apply_impls(&self, expanded, impl_areas)?;
        let expanded = apply_display_blocks(&self.params, expanded);
        let expanded = apply_param(&self, expanded)?;
        Some(expanded.trim().to_string())
    }
}

/// Read all ImplAreas (HashMap<Name, Codes>)
fn read_impl_areas(content: String) -> Option<(String, HashMap<String, String>)> {
    let mut striped_content = String::new();
    let mut impl_areas: HashMap<String, String> = HashMap::new();

    let mut current_area_name = String::default();
    let mut current_area_codes: Vec<String> = Vec::new();

    for line in content.split("\n") {
        let trimmed_line = line.trim();

        // Implementation block end
        if trimmed_line.starts_with(IMPL_AREA_END) {
            // If the current ImplArea name length is less than 1, it means no block is being matched,
            // so matching fails, exit early
            if current_area_name.is_empty() {
                return None;
            }

            // Submit Impl Area
            let name = std::mem::take(&mut current_area_name);
            impl_areas.insert(name, current_area_codes.join("\n"));
            current_area_codes.clear();
            continue;
        }

        // Implementation block start
        if trimmed_line.starts_with(IMPL_AREA_BEGIN) {
            // If the current ImplArea name is not empty, a block is already active.
            // Nesting is not allowed, so matching fails and exits early.
            if !current_area_name.is_empty() {
                return None;
            }

            // Get a snake_case name
            let snake_name = snake_case!(line.trim_start_matches(IMPL_AREA_BEGIN).trim());
            current_area_name = snake_name;

            // Continue to next line
            continue;
        }

        // During implementation block
        if !current_area_name.is_empty() {
            // Add to current block code
            current_area_codes.push(line.to_string());
            continue;
        } else {
            // Add to remaining content
            striped_content += "\n";
            striped_content += line;
        }
    }

    Some((striped_content, impl_areas))
}

/// Apply Template parameters to implementation block areas
fn apply_impls(
    template: &Template,
    content: String,
    impl_areas: HashMap<String, String>,
) -> Option<String> {
    let mut applied_content = String::new();

    let mut impled_areas: HashMap<String, Vec<String>> = HashMap::new();
    for (impl_area_name, impl_area_template) in impl_areas {
        // Get user-provided parameters
        let impl_items = template.impl_params.get(&impl_area_name);

        // No parameters, return early
        let Some(impl_items) = impl_items else {
            impled_areas.insert(impl_area_name, Vec::new());
            continue;
        };

        let mut impled_area_code_applied = Vec::new();

        // Split items
        for item in impl_items {
            // Get base template
            let mut applied = impl_area_template.clone();

            // Merge global params with arm-specific params for display block check
            let mut display_params = template.params.clone();
            for (k, v) in item {
                display_params.insert(k.clone(), v.clone());
            }
            applied = apply_display_blocks(&display_params, applied);

            // Extract parameters
            for (param_name, param_value) in item {
                // Apply parameter
                applied = applied.replace(
                    &format!("{}{}{}", PARAM_BEGIN, param_name, PARAM_BEND),
                    param_value,
                );
            }

            // Add applied template
            impled_area_code_applied.push(applied);
        }

        impled_areas.insert(impl_area_name, impled_area_code_applied);
    }

    for line in content.split("\n") {
        let trimmed_line = line.trim();

        // Recognize implementation line
        if trimmed_line.starts_with(IMPL_BEGIN) {
            let impl_name = snake_case!(trimmed_line.trim_start_matches(IMPL_BEGIN).trim());

            // Try to get implementation code block
            let Some(impled_code) = impled_areas.get(&impl_name) else {
                continue;
            };

            if !impled_code.is_empty() {
                applied_content += "\n";
                applied_content += impled_code.join("\n").as_str();
            }
        } else {
            // Other content directly appended
            applied_content += "\n";
            applied_content += line;
        }
    }

    Some(applied_content)
}

/// Process display blocks (`??? >>> name` / `??? <<<`).
///
/// If `params` contains a key matching the block name, the block content is
/// included (with markers removed). Otherwise the entire block is omitted.
fn apply_display_blocks(params: &HashMap<String, String>, content: String) -> String {
    let mut result = String::new();
    let lines: Vec<&str> = content.split("\n").collect();
    let mut i = 0;
    let mut first = true;

    while i < lines.len() {
        let line = lines[i];
        let trimmed = line.trim();

        if trimmed.starts_with(DISPLAY_BLOCK_BEGIN) {
            let block_name = trimmed.trim_start_matches(DISPLAY_BLOCK_BEGIN).trim();
            let show = params.contains_key(block_name);
            i += 1;

            while i < lines.len() && !lines[i].trim().starts_with(DISPLAY_BLOCK_END) {
                if show {
                    if !first {
                        result += "\n";
                    }
                    result += lines[i];
                    first = false;
                }
                i += 1;
            }
        } else if !trimmed.starts_with(DISPLAY_BLOCK_END) {
            if !first {
                result += "\n";
            }
            result += line;
            first = false;
        }

        i += 1;
    }

    result
}

fn apply_param(template: &Template, content: String) -> Option<String> {
    let mut content = content;
    for (k, v) in template.params.iter() {
        content = content.replace(&format!("{}{}{}", PARAM_BEGIN, k, PARAM_BEND), v);
    }
    Some(content)
}