Set up literate programming structure
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
use pulldown_cmark::{Event, Parser, Tag, TagEnd, CodeBlockKind};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn extract_file_attr(info_string: &str) -> Option<&str> {
|
||||
// Parse info strings like "rust {file=coracle-lib/src/event.rs}"
|
||||
let brace_start = info_string.find('{')?;
|
||||
let brace_end = info_string.find('}')?;
|
||||
let attrs = &info_string[brace_start + 1..brace_end];
|
||||
for attr in attrs.split(',') {
|
||||
let attr = attr.trim();
|
||||
if let Some(path) = attr.strip_prefix("file=") {
|
||||
return Some(path.trim());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn tangle(book_dir: &Path, output_base: &Path) {
|
||||
let mut files: BTreeMap<PathBuf, String> = BTreeMap::new();
|
||||
|
||||
// Collect all .md files sorted by name
|
||||
let mut md_files: Vec<PathBuf> = fs::read_dir(book_dir)
|
||||
.expect("Failed to read book directory")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|ext| ext == "md"))
|
||||
.collect();
|
||||
md_files.sort();
|
||||
|
||||
for md_path in &md_files {
|
||||
let content = fs::read_to_string(md_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to read {}", md_path.display()));
|
||||
|
||||
let parser = Parser::new(&content);
|
||||
let mut current_file: Option<PathBuf> = None;
|
||||
let mut in_code_block = false;
|
||||
|
||||
for event in parser {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
|
||||
let info_str = info.as_ref();
|
||||
if let Some(file_path) = extract_file_attr(info_str) {
|
||||
current_file = Some(output_base.join(file_path));
|
||||
in_code_block = true;
|
||||
}
|
||||
}
|
||||
Event::Text(text) if in_code_block => {
|
||||
if let Some(ref path) = current_file {
|
||||
files.entry(path.clone())
|
||||
.or_default()
|
||||
.push_str(text.as_ref());
|
||||
}
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
if in_code_block {
|
||||
// Add a newline between concatenated blocks
|
||||
if let Some(ref path) = current_file {
|
||||
files.entry(path.clone())
|
||||
.or_default()
|
||||
.push('\n');
|
||||
}
|
||||
current_file = None;
|
||||
in_code_block = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if files.is_empty() {
|
||||
println!("No tangled files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (path, content) in &files {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.unwrap_or_else(|_| panic!("Failed to create directory {}", parent.display()));
|
||||
}
|
||||
fs::write(path, content)
|
||||
.unwrap_or_else(|_| panic!("Failed to write {}", path.display()));
|
||||
println!(" tangled: {}", path.display());
|
||||
}
|
||||
|
||||
println!("\n{} files tangled.", files.len());
|
||||
}
|
||||
|
||||
fn weave(book_dir: &Path, output_dir: &Path) {
|
||||
// Preprocess markdown files: transform {file=...} annotations into
|
||||
// readable captions for mdBook, writing to a temp directory.
|
||||
let staging = output_dir.join("book-staging");
|
||||
if staging.exists() {
|
||||
fs::remove_dir_all(&staging).expect("Failed to clean staging directory");
|
||||
}
|
||||
let staging_src = staging.join("src");
|
||||
fs::create_dir_all(&staging_src).expect("Failed to create staging src directory");
|
||||
|
||||
for entry in fs::read_dir(book_dir)
|
||||
.expect("Failed to read book directory")
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
let filename = path.file_name().unwrap();
|
||||
|
||||
if path.file_name().is_some_and(|n| n == "book.toml") {
|
||||
// book.toml goes at the staging root
|
||||
let content = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|_| panic!("Failed to read {}", path.display()));
|
||||
let dest = staging.join(filename);
|
||||
fs::write(&dest, content)
|
||||
.unwrap_or_else(|_| panic!("Failed to write {}", dest.display()));
|
||||
} else if path.extension().is_some_and(|ext| ext == "md") {
|
||||
// Markdown files go into staging/src/
|
||||
let content = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|_| panic!("Failed to read {}", path.display()));
|
||||
let transformed = transform_for_weave(&content);
|
||||
let dest = staging_src.join(filename);
|
||||
fs::write(&dest, transformed)
|
||||
.unwrap_or_else(|_| panic!("Failed to write {}", dest.display()));
|
||||
}
|
||||
}
|
||||
|
||||
println!("Preprocessed markdown written to {}", staging.display());
|
||||
println!("Run: mdbook build {}", staging.display());
|
||||
}
|
||||
|
||||
fn transform_for_weave(content: &str) -> String {
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if line.starts_with("```") && line.contains("{file=") {
|
||||
// Extract language and file path
|
||||
let lang = line.trim_start_matches('`')
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("rust");
|
||||
if let Some(file_path) = extract_file_attr(line) {
|
||||
result.push_str(&format!("```{lang}\n// -> {file_path}\n"));
|
||||
} else {
|
||||
result.push_str(line);
|
||||
result.push('\n');
|
||||
}
|
||||
} else {
|
||||
result.push_str(line);
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let (command, book_dir) = match args.len() {
|
||||
1 => ("tangle", "book"),
|
||||
2 => (args[1].as_str(), "book"),
|
||||
3 => (args[1].as_str(), args[2].as_str()),
|
||||
_ => {
|
||||
eprintln!("Usage: coracle-tangle [tangle|weave] [book-dir]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let book_path = Path::new(book_dir);
|
||||
let output_base = Path::new(".");
|
||||
|
||||
match command {
|
||||
"tangle" => tangle(book_path, output_base),
|
||||
"weave" => weave(book_path, Path::new("target")),
|
||||
_ => {
|
||||
eprintln!("Unknown command: {command}. Use 'tangle' or 'weave'.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user