cleanpy 0.1.3

Python 3.12+ 最佳实践知识库 - MCP 服务器
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

/// Represents a topic extracted from markdown Front Matter
#[derive(Debug)]
struct TopicInfo {
    topic: String,
    title: String,
    when_to_use: String,
    keywords: Vec<String>,
    file: String,
    content: String, // Store the full file content
}

/// Parse Front Matter from markdown content
fn parse_front_matter(content: &str) -> Option<TopicInfo> {
    // Match YAML Front Matter: ---\n...\n---
    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;
    }

    // Parse simple YAML manually
    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;
        }

        // Handle list items
        if let Some(stripped) = trimmed.strip_prefix("- ") {
            if collecting_list {
                keywords.push(stripped.trim().to_string());
            }
            continue;
        }

        // Handle key-value pairs
        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() {
                // Single-line value
                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" {
                // Start of list
                collecting_list = true;
                keywords.clear();
            }
        }
    }

    if topic.is_empty() {
        return None;
    }

    Some(TopicInfo {
        topic,
        title,
        when_to_use,
        keywords,
        file: String::new(),    // Will be set by caller
        content: String::new(), // Will be set by caller
    })
}

/// Recursively scan directory for markdown files
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() {
                // Recursively scan subdirectories
                topics.extend(scan_markdown_files_impl(&path, base_path));
            } else if path.is_file() {
                // Check if it's a markdown file
                if let Some(ext) = path.extension()
                    && ext == "md"
                {
                    // Skip README files
                    if let Some(file_name) = path.file_name() {
                        let name = file_name.to_string_lossy().to_lowercase();
                        if name == "readme.md" {
                            continue;
                        }
                    }

                    // Read and parse the file
                    if let Ok(content) = fs::read_to_string(&path)
                        && let Some(mut topic_info) = parse_front_matter(&content)
                    {
                        // Calculate relative path from base_path
                        if let Ok(relative) = path.strip_prefix(base_path) {
                            topic_info.file = relative.display().to_string();
                            topic_info.content = content; // Store the full file content
                            topics.push(topic_info);
                        }
                    }
                }
            }
        }
    }

    topics
}

/// Scan directory for markdown files
fn scan_markdown_files(docs_path: &Path) -> Vec<TopicInfo> {
    scan_markdown_files_impl(docs_path, docs_path)
}

/// Escape string for Rust code generation
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);
    }

    // Scan and parse all markdown files
    let topics = scan_markdown_files(&docs_path);

    println!("Found {} topics", topics.len());

    // Generate Rust code with embedded content using include_str!
    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)
        ));
        // Embed the content using include_str! for compile-time embedding
        // Use absolute path from CARGO_MANIFEST_DIR to ensure correct resolution
        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");

    // Write generated code to OUT_DIR
    let dest_path = PathBuf::from(out_dir).join("topics.rs");
    fs::write(dest_path, code).expect("Failed to write generated code");
}