use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
type Quiz = (String, String);
type BlocksResult = Result<(Vec<Block>, Vec<Quiz>), String>;
#[derive(Debug)]
pub struct Block {
pub content: String,
pub is_code: bool,
pub is_admonish: bool,
pub quiz_reference: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_blocks_basic() {
let input = "/- Simple comment -/\ndef foo := 1";
let (blocks, _) = build_blocks(input).unwrap();
println!("Number of blocks: {}", blocks.len());
for (i, block) in blocks.iter().enumerate() {
println!(
"Block {}: is_code={}, content='{}'",
i, block.is_code, block.content
);
}
assert_eq!(blocks.len(), 2);
}
#[test]
fn test_ignore_marker() {
let input = "line 1\nline 2 --#\nline 3";
let (blocks, _) = build_blocks(input).unwrap();
assert!(!blocks[0].content.contains("line 2"));
}
#[test]
fn test_force_include_marker() {
let input = "line 1\nline 2 --#--!\nline 3";
let (blocks, _) = build_blocks(input).unwrap();
assert!(blocks[0].content.contains("line 2 --#"));
}
}
pub fn build_blocks(content: &str) -> BlocksResult {
let mut blocks = Vec::new();
let mut quizzes = Vec::new();
let mut current_content = String::new();
let mut in_comment_block = false;
let mut in_ignore_block = false;
let mut in_code_example = false;
let mut in_quiz = false;
let mut current_quiz_name = String::new();
let mut current_quiz_content = String::new();
for line in content.lines() {
let line = line.trim_end();
if line == "--#--" {
in_ignore_block = !in_ignore_block;
continue;
}
if in_ignore_block {
continue;
}
if in_comment_block {
if line.starts_with("--@quiz:") && !in_quiz {
current_quiz_name = line[8..].trim().to_string();
in_quiz = true;
current_quiz_content.clear();
continue;
}
if line == "--@quiz-end" && in_quiz {
in_quiz = false;
quizzes.push((current_quiz_name.clone(), current_quiz_content.clone()));
if !current_content.trim().is_empty() {
blocks.push(Block {
content: current_content.trim().to_string(),
is_code: false,
is_admonish: false,
quiz_reference: None,
});
current_content = String::new();
}
blocks.push(Block {
content: String::new(),
is_code: false,
is_admonish: false,
quiz_reference: Some(current_quiz_name.clone()),
});
continue;
}
if in_quiz {
current_quiz_content.push_str(line);
current_quiz_content.push('\n');
continue;
}
}
if line.ends_with("--#") {
continue;
}
if let Some(stripped) = line.strip_suffix("--!") {
let content = stripped.trim_end().to_string();
current_content.push_str(&content);
current_content.push('\n');
continue; }
if in_comment_block && line.trim() == "```lean" {
in_code_example = true;
current_content.push_str(line);
current_content.push('\n');
continue;
}
if in_comment_block && line.trim() == "```" && in_code_example {
in_code_example = false;
current_content.push_str(line);
current_content.push('\n');
continue;
}
if in_comment_block && in_code_example {
current_content.push_str(line);
current_content.push('\n');
continue;
}
if in_comment_block {
if line.ends_with("--#") {
continue;
}
if line.trim_end().ends_with("-/") && !line.contains("```") {
let is_admonish = line.ends_with("--+");
if let Some(end_idx) = line.rfind("-/") {
current_content.push_str(&line[0..end_idx]);
current_content.push('\n');
} else {
current_content.push_str(line);
current_content.push('\n');
}
blocks.push(Block {
content: current_content.trim().to_string(),
is_code: false,
is_admonish,
quiz_reference: None,
});
in_comment_block = false;
current_content = String::new();
} else {
current_content.push_str(line);
current_content.push('\n');
}
continue;
}
if line.ends_with("--#") {
continue;
}
if line.starts_with("/--") && line.contains("-/") && line.ends_with("--+") {
if !current_content.trim().is_empty() {
blocks.push(Block {
content: current_content.trim().to_string(),
is_code: true,
is_admonish: false,
quiz_reference: None,
});
}
let start_idx = 3; let end_idx = line.rfind("-/").unwrap();
if start_idx <= end_idx {
let comment_text = &line[start_idx..end_idx];
blocks.push(Block {
content: comment_text.trim().to_string(),
is_code: false,
is_admonish: true, quiz_reference: None,
});
}
current_content = String::new();
continue;
}
if line.starts_with("/--") && line.contains("-/") && !in_comment_block {
current_content.push_str(line);
current_content.push('\n');
continue;
}
if line.starts_with("/-")
&& !line.starts_with("/--")
&& line.contains("-/")
&& !in_comment_block
{
if !current_content.trim().is_empty() {
blocks.push(Block {
content: current_content.trim().to_string(),
is_code: true,
is_admonish: false,
quiz_reference: None,
});
}
let start_idx = line.find("/-").unwrap() + 2;
let end_idx = line.rfind("-/").unwrap();
if start_idx <= end_idx {
let comment_text = &line[start_idx..end_idx];
blocks.push(Block {
content: comment_text.trim().to_string(),
is_code: false,
is_admonish: false,
quiz_reference: None,
});
}
if let Some(rest) = line.split("-/").nth(1) {
if !rest.trim().is_empty() && !rest.trim_end().ends_with("--+") {
current_content = rest.to_string();
current_content.push('\n');
} else {
current_content = String::new();
}
} else {
current_content = String::new();
}
continue;
}
if line.starts_with("/-") && !line.starts_with("/--") && !in_comment_block {
if !current_content.trim().is_empty() {
blocks.push(Block {
content: current_content.trim().to_string(),
is_code: true,
is_admonish: false,
quiz_reference: None,
});
}
in_comment_block = true;
current_content = line[2..].to_string();
current_content.push('\n');
continue;
}
current_content.push_str(line);
current_content.push('\n');
}
if !current_content.trim().is_empty() {
blocks.push(Block {
content: current_content.trim().to_string(),
is_code: !in_comment_block,
is_admonish: false,
quiz_reference: None,
});
}
if in_comment_block {
return Err("Unclosed comment block at the end of file".to_string());
}
Ok((blocks, quizzes))
}
fn merge_blocks(blocks: &[Block]) -> String {
let mut result = String::new();
for block in blocks {
if block.content.is_empty() && block.quiz_reference.is_none() {
continue;
}
if let Some(quiz_ref) = &block.quiz_reference {
result.push_str(&format!("{{{{#quiz ../quizzes/{}.toml}}}}\n\n", quiz_ref));
continue;
}
if block.is_code {
result.push_str("```lean\n");
result.push_str(&block.content);
result.push_str("\n```\n\n");
} else if block.is_admonish {
result.push_str("```admonish abstract collapsible = false, title = \"Docstring\"\n");
result.push_str(&block.content);
result.push_str("\n```\n\n");
} else {
result.push_str(&block.content);
result.push_str("\n\n");
}
}
result.trim_end().to_string() + "\n"
}
pub fn process_directory(src_dir: &Path, tgt_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
fs::create_dir_all(tgt_dir)?;
let parent_dir = tgt_dir.parent().unwrap_or(Path::new("."));
let quizzes_dir = parent_dir.join("quizzes");
fs::create_dir_all(&quizzes_dir)?;
for entry in fs::read_dir(src_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let src_subdir = path.file_name().unwrap();
let tgt_subdir = tgt_dir.join(src_subdir);
process_directory(&path, &tgt_subdir)?;
} else if let Some(ext) = path.extension() {
if ext == "lean" {
let content = fs::read_to_string(&path)?;
let md_path = tgt_dir.join(path.file_stem().unwrap()).with_extension("md");
println!("Converting {} to {}", path.display(), md_path.display());
let (blocks, quizzes) = build_blocks(&content)?;
let markdown = merge_blocks(&blocks);
for (name, content) in quizzes {
let quiz_path = quizzes_dir.join(format!("{}.toml", name));
let mut file = File::create(&quiz_path)?;
file.write_all(content.as_bytes())?;
println!(" Generated quiz: {}", quiz_path.display());
}
fs::write(md_path, markdown)?;
}
}
}
Ok(())
}