use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
struct TopicInfo {
topic: String,
title: String,
when_to_use: String,
keywords: Vec<String>,
file: String,
content: String, }
fn parse_front_matter(content: &str) -> Option<TopicInfo> {
let parts: Vec<&str> = content.splitn(3, "---").collect();
if parts.len() < 3 {
return None;
}
let yaml_content = parts[1].trim();
if yaml_content.is_empty() {
return None;
}
let mut topic = String::new();
let mut title = String::new();
let mut when_to_use = String::new();
let mut keywords = Vec::new();
let mut collecting_list = false;
for line in yaml_content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(stripped) = trimmed.strip_prefix("- ") {
if collecting_list {
keywords.push(stripped.trim().to_string());
}
continue;
}
if let Some(colon_pos) = trimmed.find(':') {
let key = trimmed[..colon_pos].trim();
let value = trimmed[colon_pos + 1..].trim();
collecting_list = false;
if !value.is_empty() {
match key {
"topic" => topic = value.to_string(),
"title" => title = value.to_string(),
"when_to_use" => when_to_use = value.to_string(),
_ => {}
}
} else if key == "keywords" {
collecting_list = true;
keywords.clear();
}
}
}
if topic.is_empty() {
return None;
}
Some(TopicInfo {
topic,
title,
when_to_use,
keywords,
file: String::new(), content: String::new(), })
}
fn scan_markdown_files_impl(current_path: &Path, base_path: &Path) -> Vec<TopicInfo> {
let mut topics = Vec::new();
if let Ok(entries) = fs::read_dir(current_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
topics.extend(scan_markdown_files_impl(&path, base_path));
} else if path.is_file() {
if let Some(ext) = path.extension()
&& ext == "md"
{
if let Some(file_name) = path.file_name() {
let name = file_name.to_string_lossy().to_lowercase();
if name == "readme.md" {
continue;
}
}
if let Ok(content) = fs::read_to_string(&path)
&& let Some(mut topic_info) = parse_front_matter(&content)
{
if let Ok(relative) = path.strip_prefix(base_path) {
topic_info.file = relative.display().to_string();
topic_info.content = content; topics.push(topic_info);
}
}
}
}
}
}
topics
}
fn scan_markdown_files(docs_path: &Path) -> Vec<TopicInfo> {
scan_markdown_files_impl(docs_path, docs_path)
}
fn escape_rust_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn main() {
println!("cargo:rerun-if-changed=src/python312_best_practices");
let out_dir = env::var("OUT_DIR").unwrap();
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let docs_path = PathBuf::from("src/python312_best_practices");
if !docs_path.exists() {
panic!("Documentation directory not found: {:?}", docs_path);
}
let topics = scan_markdown_files(&docs_path);
println!("Found {} topics", topics.len());
let mut code = String::from("// Auto-generated by build.rs - DO NOT EDIT\n");
code.push_str("// All documentation content is embedded at compile time\n\n");
code.push_str("pub const TOPICS: &[TopicInfo] = &[\n");
for topic in &topics {
code.push_str(" TopicInfo {\n");
code.push_str(&format!(
" topic: \"{}\",\n",
escape_rust_string(&topic.topic)
));
code.push_str(&format!(
" title: \"{}\",\n",
escape_rust_string(&topic.title)
));
code.push_str(&format!(
" when_to_use: \"{}\",\n",
escape_rust_string(&topic.when_to_use)
));
code.push_str(" keywords: &[\n");
for keyword in &topic.keywords {
code.push_str(&format!(
" \"{}\",\n",
escape_rust_string(keyword)
));
}
code.push_str(" ],\n");
code.push_str(&format!(
" file: \"{}\",\n",
escape_rust_string(&topic.file)
));
let file_path = format!(
"{}/src/python312_best_practices/{}",
manifest_dir, topic.file
);
code.push_str(&format!(
" content: include_str!(r\"{}\"),\n",
file_path
));
code.push_str(" },\n");
}
code.push_str("];\n");
let dest_path = PathBuf::from(out_dir).join("topics.rs");
fs::write(dest_path, code).expect("Failed to write generated code");
}