diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..02fbcd4 --- /dev/null +++ b/src/main.rs | |||
@@ -0,0 +1,165 @@ | |||
1 | use clap::Parser; | ||
2 | use pandoc::{ | ||
3 | InputFormat,InputKind,OutputFormat,OutputKind,Pandoc | ||
4 | }; | ||
5 | use pandoc_ast::Block; | ||
6 | use std::borrow::Cow; | ||
7 | use std::collections::HashMap; | ||
8 | use lazy_static::lazy_static; | ||
9 | use regex::{Captures,Regex}; | ||
10 | use std::fs; | ||
11 | use std::io::Result; | ||
12 | use std::path::PathBuf; | ||
13 | |||
14 | const BASE: &str = "./"; | ||
15 | |||
16 | /// A tangler for Literate Programming in Pandoc | ||
17 | #[derive(Parser, Debug)] | ||
18 | #[clap(author, version, about, long_about = None)] | ||
19 | struct Config { | ||
20 | /// Maximum substitution depth | ||
21 | #[clap(short, long, default_value_t = 10)] | ||
22 | depth: u32, | ||
23 | /// Base output directory [default: './'] | ||
24 | #[clap(short, long)] | ||
25 | output: Option<PathBuf>, | ||
26 | /// Input files | ||
27 | input: Vec<PathBuf>, | ||
28 | } | ||
29 | |||
30 | type Blocks<'a> = HashMap<String,Cow<'a,str>>; | ||
31 | |||
32 | fn build( | ||
33 | base: &Option<PathBuf>, | ||
34 | blocks: &Blocks, | ||
35 | max_depth: u32 | ||
36 | ) { | ||
37 | lazy_static! { | ||
38 | static ref PATH: Regex = | ||
39 | Regex::new( | ||
40 | r"^(?:[[:word:]\.-]+/)*[[:word:]\.-]+\.[[:alpha:]]+$" | ||
41 | ).unwrap(); | ||
42 | static ref MACRO: Regex = | ||
43 | Regex::new( | ||
44 | r"(?m)^([[:blank:]]*)<<([^>\s]+)>>" | ||
45 | ).unwrap(); | ||
46 | } | ||
47 | blocks | ||
48 | .iter() | ||
49 | .for_each(|(path,code)| if PATH.is_match(path) { | ||
50 | let mut current_depth = 0; | ||
51 | let mut code = code.clone(); | ||
52 | while let Cow::Owned(new_code) = MACRO.replace_all( | ||
53 | &code, | ||
54 | |caps: &Captures| { | ||
55 | if current_depth < max_depth { | ||
56 | let block = blocks | ||
57 | .get(&caps[2]) | ||
58 | .expect("Block not present") | ||
59 | .clone(); | ||
60 | indent(block, caps[1].len()) | ||
61 | } else { | ||
62 | eprintln!("Reached maximum depth, \ | ||
63 | output might be truncated.\n\ | ||
64 | Increase `--depth` accordingly."); | ||
65 | Cow::Owned(String::from("")) | ||
66 | } | ||
67 | } | ||
68 | ) { | ||
69 | code = Cow::from(new_code); | ||
70 | current_depth += 1; | ||
71 | } | ||
72 | let file = base | ||
73 | .clone() | ||
74 | .unwrap_or(PathBuf::from(BASE)) | ||
75 | .join(path); | ||
76 | write_to_file(file, &code) | ||
77 | .expect("Unable to write to file"); | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | fn indent<'a>( | ||
82 | input: Cow<'a,str>, | ||
83 | indent: usize | ||
84 | ) -> Cow<'a,str> { | ||
85 | if indent > 0 { | ||
86 | let prefix = format!("{:indent$}", ""); | ||
87 | let size = input.len() + indent*input.lines().count(); | ||
88 | let mut output = String::with_capacity(size); | ||
89 | input.lines().enumerate().for_each(|(i,line)| { | ||
90 | if i > 0 { | ||
91 | output.push('\n'); | ||
92 | } | ||
93 | if !line.is_empty() { | ||
94 | output.push_str(&prefix); | ||
95 | output.push_str(line); | ||
96 | } | ||
97 | }); | ||
98 | Cow::Owned(output) | ||
99 | } else { | ||
100 | input | ||
101 | } | ||
102 | } | ||
103 | |||
104 | fn write_to_file( | ||
105 | path: PathBuf, content: &str | ||
106 | ) -> std::io::Result<()> { | ||
107 | if path.is_relative() { | ||
108 | fs::create_dir_all(path.parent().unwrap())?; | ||
109 | fs::write(path, content)?; | ||
110 | } else { | ||
111 | eprintln!( | ||
112 | "Absolute paths not supported: {}", | ||
113 | path.to_string_lossy() | ||
114 | ) | ||
115 | } | ||
116 | Ok(()) | ||
117 | } | ||
118 | |||
119 | |||
120 | fn main() -> Result<()> { | ||
121 | let config = Config::parse(); | ||
122 | let mut pandoc = Pandoc::new(); | ||
123 | pandoc.set_input(InputKind::Files(config.input)); | ||
124 | pandoc.set_input_format(InputFormat::Markdown, vec![]); | ||
125 | pandoc.set_output(OutputKind::Pipe); | ||
126 | pandoc.set_output_format(OutputFormat::Json, vec![]); | ||
127 | pandoc.add_filter( | ||
128 | move |json| pandoc_ast::filter(json, | ||
129 | |pandoc| { | ||
130 | let mut blocks: Blocks = HashMap::new(); | ||
131 | pandoc.blocks.iter().for_each(|block| | ||
132 | if let Block::CodeBlock((id,clss,attrs), code) = block { | ||
133 | if !id.is_empty() { | ||
134 | let key = { | ||
135 | let path = attrs.iter().find(|(k,_)| k == "path"); | ||
136 | if let Some(path) = path { | ||
137 | format!("{}{}", path.1, id) | ||
138 | } else { | ||
139 | id.to_string() | ||
140 | } | ||
141 | }; | ||
142 | if clss.iter().any(|c| c == "override") { | ||
143 | blocks.insert(key, Cow::from(code)); | ||
144 | } else { | ||
145 | blocks.entry(key) | ||
146 | .and_modify(|s| { | ||
147 | *s += "\n"; | ||
148 | *s += Cow::from(code) | ||
149 | }) | ||
150 | .or_insert(Cow::from(code)); | ||
151 | } | ||
152 | } else { | ||
153 | eprintln!("Ignoring code block without ID:"); | ||
154 | eprintln!("{}", indent(Cow::from(code),4)); | ||
155 | } | ||
156 | } | ||
157 | ); | ||
158 | build(&config.output, &blocks, config.depth); | ||
159 | pandoc | ||
160 | } | ||
161 | ) | ||
162 | ); | ||
163 | pandoc.execute().unwrap(); | ||
164 | Ok(()) | ||
165 | } \ No newline at end of file | ||