summaryrefslogtreecommitdiff
path: root/crates/vcs_docs/build.rs
blob: d1e878f3f62e1a1b191f4cf5156e3ea307f4eca1 (plain)
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
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::Path;

// Template markers for code generation
const TEMPLATE_DOCUMENT_BEGIN: &str = "--- TEMPLATE DOCUMENT BEGIN ---";
const TEMPLATE_DOCUMENT_END: &str = "--- TEMPLATE DOCUMENT END ---";
const TEMPLATE_FUNC_BEGIN: &str = "--- TEMPLATE FUNC BEGIN ---";
const TEMPLATE_FUNC_END: &str = "--- TEMPLATE FUNC END ---";
const TEMPLATE_LIST_BEGIN: &str = "--- TEMPLATE LIST BEGIN ---";
const TEMPLATE_LIST_END: &str = "--- TEMPLATE LIST END ---";

// Template parameter patterns for substitution
const PARAM_DOCUMENT_PATH: &str = "{{DOCUMENT_PATH}}";
const PARAM_DOCUMENT_CONSTANT_NAME: &str = "{{DOCUMENT_CONSTANT_NAME}}";
const PARAM_DOCUMENT_CONTENT: &str = "{{DOCUMENT_CONTENT}}";
const PARAM_DOCUMENT_PATH_SNAKE_CASE: &str = "{{DOCUMENT_PATH_SNAKE_CASE}}";

fn main() -> io::Result<()> {
    println!("cargo:rerun-if-changed=src/docs.rs.template");
    println!("cargo:rerun-if-changed=../../docs/Documents");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("docs.rs");

    // Read all markdown files from docs directory recursively
    let docs_dir = Path::new("../../docs/Documents");
    let mut documents = Vec::new();

    if docs_dir.exists() {
        collect_text_files(docs_dir, &mut documents)?;
    }

    // Read template file
    let template_path = Path::new("src/docs.rs.template");
    let template_content = fs::read_to_string(template_path)?;

    // Extract template sections preserving original indentation
    let document_template = template_content
        .split(TEMPLATE_DOCUMENT_BEGIN)
        .nth(1)
        .and_then(|s| s.split(TEMPLATE_DOCUMENT_END).next())
        .unwrap_or("")
        .trim_start_matches('\n')
        .trim_end_matches('\n');

    let match_arm_template = template_content
        .split(TEMPLATE_FUNC_BEGIN)
        .nth(1)
        .and_then(|s| s.split(TEMPLATE_FUNC_END).next())
        .unwrap_or("")
        .trim_start_matches('\n')
        .trim_end_matches('\n');

    // Generate document blocks and match arms
    let mut document_blocks = String::new();
    let mut match_arms = String::new();
    let mut list_items = String::new();

    for (relative_path, content) in &documents {
        // Calculate parameters for template substitution
        let document_path = format!("./docs/Documents/{}", relative_path);

        // Generate constant name from relative path
        let document_constant_name = relative_path
            .replace(['/', '\\', '-'], "_")
            .replace(".md", "")
            .replace(".txt", "")
            .replace(".toml", "")
            .replace(".yaml", "")
            .replace(".yml", "")
            .replace(".json", "")
            .replace(".rs", "")
            .to_uppercase();

        // Generate snake_case name for function matching
        let document_path_snake_case = relative_path
            .replace(['/', '\\', '-'], "_")
            .replace(".md", "")
            .replace(".txt", "")
            .replace(".toml", "")
            .replace(".yaml", "")
            .replace(".yml", "")
            .replace(".json", "")
            .replace(".rs", "")
            .to_lowercase();

        // Escape double quotes in content
        let escaped_content = content.trim().replace('\"', "\\\"");

        // Replace template parameters in document block preserving indentation
        let document_block = document_template
            .replace(PARAM_DOCUMENT_PATH, &document_path)
            .replace(PARAM_DOCUMENT_CONSTANT_NAME, &document_constant_name)
            .replace(PARAM_DOCUMENT_CONTENT, &escaped_content)
            .replace("r#\"\"#", &format!("r#\"{}\"#", escaped_content));

        document_blocks.push_str(&document_block);
        document_blocks.push_str("\n\n");

        // Replace template parameters in match arm preserving indentation
        let match_arm = match_arm_template
            .replace(PARAM_DOCUMENT_PATH_SNAKE_CASE, &document_path_snake_case)
            .replace(PARAM_DOCUMENT_CONSTANT_NAME, &document_constant_name);

        match_arms.push_str(&match_arm);
        match_arms.push('\n');

        // Generate list item for documents() function
        let list_item = format!("        \"{}\".to_string(),", document_path_snake_case);
        list_items.push_str(&list_item);
        list_items.push('\n');
    }

    // Remove trailing newline from the last list item
    if !list_items.is_empty() {
        list_items.pop();
    }

    // Build final output by replacing template sections
    let mut output = String::new();

    // Add header before document blocks
    if let Some(header) = template_content.split(TEMPLATE_DOCUMENT_BEGIN).next() {
        output.push_str(header.trim());
        output.push_str("\n\n");
    }

    // Add document blocks
    output.push_str(&document_blocks);

    // Add function section
    if let Some(func_section) = template_content.split(TEMPLATE_FUNC_BEGIN).next()
        && let Some(rest) = func_section.split(TEMPLATE_DOCUMENT_END).nth(1)
    {
        output.push_str(rest.trim());
        output.push('\n');
    }

    // Add match arms
    output.push_str(&match_arms);

    // Add list items for documents() function
    if let Some(list_section) = template_content.split(TEMPLATE_LIST_BEGIN).next()
        && let Some(rest) = list_section.split(TEMPLATE_FUNC_END).nth(1)
    {
        output.push_str(rest.trim());
        output.push('\n');
    }
    output.push_str(&list_items);

    // Add footer
    if let Some(footer) = template_content.split(TEMPLATE_LIST_END).nth(1) {
        // Preserve original indentation in footer
        output.push_str(footer);
    }

    // Write generated file
    let mut file = fs::File::create(&dest_path)?;
    file.write_all(output.as_bytes())?;

    // Copy to src directory for development
    let src_dest_path = Path::new("src/docs.rs");
    fs::write(src_dest_path, output)?;

    Ok(())
}

fn collect_text_files(dir: &Path, documents: &mut Vec<(String, String)>) -> io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.is_dir() {
            collect_text_files(&path, documents)?;
        } else if path.extension().is_some_and(|ext| {
            ext == "md"
                || ext == "txt"
                || ext == "toml"
                || ext == "yaml"
                || ext == "yml"
                || ext == "json"
                || ext == "rs"
        }) && let Ok(relative_path) = path.strip_prefix("../../docs/Documents")
            && let Some(relative_path_str) = relative_path.to_str()
        {
            let content = fs::read_to_string(&path)?;
            documents.push((
                relative_path_str.trim_start_matches('/').to_string(),
                content,
            ));
        }
    }
    Ok(())
}