use clap::Parser; use pandoc::{ InputFormat,InputKind,OutputFormat,OutputKind,Pandoc }; use pandoc_ast::Block; use std::borrow::Cow; use std::collections::HashMap; use lazy_static::lazy_static; use regex::{Captures,Regex}; use std::fs; use std::io::Result; use std::path::PathBuf; const BASE: &str = "./"; /// A tangler for Literate Programming in Pandoc #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Config { /// Simply list entry points and exit #[clap(short, long)] list: bool, /// Maximum substitution depth #[clap(short, long, default_value_t=10, value_name="N")] depth: u32, /// Base output directory [default: './'] #[clap(short, long, value_name="PATH")] output: Option, /// Limit entry points to those matching the given prefix #[clap(short, long, value_name="PREFIX")] target: Option, /// Input files input: Vec, } #[derive(Eq, Hash, PartialEq)] enum Key { Macro(String), Entry(PathBuf) } impl Key { fn get_path(&self) -> Option<&PathBuf> { match self { Self::Entry(s) => Some(&s), Self::Macro(_) => None } } } type Blocks<'a> = HashMap>; fn build( base: &Option, blocks: &Blocks, max_depth: u32 ) { lazy_static! { static ref MACRO: Regex = Regex::new( r"(?m)^([[:blank:]]*)<<([^>\s]+)>>" ).unwrap(); } blocks .iter() .filter_map(|(key,code)| { key.get_path().map(|k| (k,code)) }) .for_each(|(path,code)| { let mut current_depth = 0; let mut code = code.clone(); while let Cow::Owned(new_code) = MACRO.replace_all( &code, |caps: &Captures| { if current_depth < max_depth { let block = blocks .get(&Key::Macro(caps[2].to_string())) .unwrap_or_else(|| panic!( "Block \"{}\" not present", caps[2].to_string())) .clone(); indent(block, caps[1].len()) } else { eprintln!("Reached maximum depth, \ output might be truncated.\n\ Increase `--depth` accordingly."); Cow::Owned(String::from("")) } } ) { code = Cow::from(new_code); current_depth += 1; } let file = base .clone() .unwrap_or(PathBuf::from(BASE)) .join(path); write_to_file(file, &code).unwrap(); }) } fn indent<'a>( input: Cow<'a,str>, indent: usize ) -> Cow<'a,str> { if indent > 0 { let prefix = format!("{:indent$}", ""); let size = input.len() + indent*input.lines().count(); let mut output = String::with_capacity(size); input.lines().enumerate().for_each(|(i,line)| { if i > 0 { output.push('\n'); } if !line.is_empty() { output.push_str(&prefix); output.push_str(line); } }); Cow::Owned(output) } else { input } } fn write_to_file( path: PathBuf, content: &str ) -> std::io::Result<()> { if path.is_relative() { fs::create_dir_all(path.parent().unwrap())?; fs::write(path, content)?; } else { eprintln!( "Absolute paths not supported: {}", path.display() ) } Ok(()) } fn main() -> Result<()> { let config = Config::parse(); let target = config.target.unwrap_or_default(); let mut pandoc = Pandoc::new(); pandoc.set_input(InputKind::Files(config.input)); pandoc.set_input_format(InputFormat::Markdown, vec![]); pandoc.set_output(OutputKind::Pipe); pandoc.set_output_format(OutputFormat::Json, vec![]); pandoc.add_filter( move |json| pandoc_ast::filter(json, |pandoc| { let mut blocks: Blocks = HashMap::new(); pandoc.blocks.iter().for_each(|block| if let Block::CodeBlock((id,clss,attrs), code) = block { if !id.is_empty() { let key = { lazy_static! { static ref PATH: Regex = Regex::new( r"^(?:[[:word:]\.-]+/)*[[:word:]\.-]+\.[[:alpha:]]+$" ).unwrap(); } let entry = clss.contains(&String::from("entry")); let path = attrs .into_iter() .find_map(|(k,p)| if k == "path" { Some(p.clone()) } else { None }); if entry || path.is_some() || PATH.is_match(id) { let path = PathBuf::from(path.unwrap_or_default()).join(id); if path.starts_with(&target) { Some(Key::Entry(path)) } else { None } } else { Some(Key::Macro(id.to_string())) } }; if let Some(key) = key { if clss.iter().any(|c| c == "override") { blocks.insert(key, Cow::from(code)); } else { blocks.entry(key) .and_modify(|s| { *s += "\n"; *s += Cow::from(code) }) .or_insert(Cow::from(code)); } } } else { eprintln!("Ignoring code block without ID:"); eprintln!("{}", indent(Cow::from(code),4)); } } ); if config.list { blocks.keys().for_each(|k| match k { Key::Entry(s) => println!("{}", s.display()), Key::Macro(_) => {} }); } else { build(&config.output, &blocks, config.depth); } pandoc } ) ); pandoc.execute().unwrap(); Ok(()) }