diff options
Diffstat (limited to 'src/generator.rs')
-rw-r--r-- | src/generator.rs | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..0ab4511 --- /dev/null +++ b/src/generator.rs | |||
@@ -0,0 +1,209 @@ | |||
1 | use crate::entities::{ | ||
2 | Described, Description, EntitiesManager, EntitiesManagerComponents, Entity, Usr, | ||
3 | }; | ||
4 | |||
5 | use anyhow::{Context, Result}; | ||
6 | use thiserror::Error; | ||
7 | use threadpool::ThreadPool; | ||
8 | |||
9 | use std::collections::HashMap; | ||
10 | use std::path::Path; | ||
11 | use std::sync::{mpsc::channel, Arc}; | ||
12 | |||
13 | pub(crate) fn generate(base_dir: &Path, manager: EntitiesManager) -> Result<()> { | ||
14 | let EntitiesManagerComponents { | ||
15 | toplevel_entities, | ||
16 | descriptions, | ||
17 | } = manager.decompose(); | ||
18 | |||
19 | let md_output_dir = base_dir.join("markdown"); | ||
20 | let html_output_dir = base_dir.join("html"); | ||
21 | |||
22 | std::fs::create_dir_all(&md_output_dir) | ||
23 | .context("Failed to create the markdown output directory")?; | ||
24 | std::fs::create_dir_all(&html_output_dir) | ||
25 | .context("Failed to create the HTML output directory")?; | ||
26 | |||
27 | let pool = ThreadPool::new(num_cpus::get()); | ||
28 | |||
29 | let descriptions = Arc::new(descriptions); | ||
30 | |||
31 | let (tx, rx) = channel::<()>(); | ||
32 | |||
33 | for (usr, entity) in toplevel_entities { | ||
34 | generate_recursively( | ||
35 | usr, | ||
36 | entity, | ||
37 | pool.clone(), | ||
38 | tx.clone(), | ||
39 | descriptions.clone(), | ||
40 | &md_output_dir, | ||
41 | &html_output_dir, | ||
42 | ); | ||
43 | } | ||
44 | |||
45 | drop(tx); | ||
46 | |||
47 | // This is not really idiomatic, but iter returns None when every Sender is destroyed, so just | ||
48 | // by passing around Senders in generate_recursively, we wait for every job. | ||
49 | rx.iter().for_each(drop); | ||
50 | |||
51 | Ok(()) | ||
52 | } | ||
53 | |||
54 | fn generate_recursively( | ||
55 | usr: Usr, | ||
56 | entity: Entity, | ||
57 | pool: ThreadPool, | ||
58 | tx: std::sync::mpsc::Sender<()>, | ||
59 | descriptions: Arc<HashMap<Usr, Description>>, | ||
60 | md_output_dir: impl AsRef<Path>, | ||
61 | html_output_dir: impl AsRef<Path>, | ||
62 | ) { | ||
63 | let descriptions = descriptions.clone(); | ||
64 | let md_output_dir = md_output_dir.as_ref().to_owned(); | ||
65 | let html_output_dir = html_output_dir.as_ref().to_owned(); | ||
66 | |||
67 | let pool2 = pool.clone(); | ||
68 | pool.execute(move || { | ||
69 | trace!("Trying to generate {}", usr.0); | ||
70 | |||
71 | let entity = Described::<Entity> { | ||
72 | entity, | ||
73 | description: descriptions.get(&usr).unwrap().clone(), | ||
74 | }; | ||
75 | |||
76 | let leftovers = generate_single( | ||
77 | &usr, | ||
78 | entity, | ||
79 | &descriptions, | ||
80 | &md_output_dir, | ||
81 | &html_output_dir, | ||
82 | ) | ||
83 | .unwrap(); | ||
84 | |||
85 | for (usr, entity) in leftovers { | ||
86 | let pool = pool2.clone(); | ||
87 | let tx = tx.clone(); | ||
88 | generate_recursively( | ||
89 | usr, | ||
90 | entity, | ||
91 | pool, | ||
92 | tx, | ||
93 | descriptions.clone(), | ||
94 | md_output_dir.clone(), | ||
95 | html_output_dir.clone(), | ||
96 | ); | ||
97 | } | ||
98 | }); | ||
99 | } | ||
100 | |||
101 | fn generate_single( | ||
102 | usr: &Usr, | ||
103 | entity: Described<Entity>, | ||
104 | descriptions: &HashMap<Usr, Description>, | ||
105 | md_output_dir: impl AsRef<Path>, | ||
106 | html_output_dir: impl AsRef<Path>, | ||
107 | ) -> Result<HashMap<Usr, Entity>> { | ||
108 | use std::io::prelude::*; | ||
109 | use std::process::{Command, Stdio}; | ||
110 | |||
111 | let md_output_file = md_output_dir.as_ref().join(&usr.0); | ||
112 | let html_output_file = html_output_dir.as_ref().join(&usr.0); | ||
113 | |||
114 | let mut command = Command::new("pandoc"); | ||
115 | |||
116 | command | ||
117 | .stdin(Stdio::piped()) | ||
118 | .stdout(Stdio::piped()) | ||
119 | .stderr(Stdio::piped()) | ||
120 | .args(&[ | ||
121 | "--from=json", | ||
122 | "--to=markdown", | ||
123 | "--output", | ||
124 | md_output_file | ||
125 | .to_str() | ||
126 | .context("Entity name is not valid UTF-8")?, | ||
127 | ]); | ||
128 | |||
129 | debug!("Launching command: {:?}", command); | ||
130 | |||
131 | let mut pandoc = command | ||
132 | .spawn() | ||
133 | .context("Failed to execute Pandoc command")?; | ||
134 | |||
135 | let (pandoc_ast, leftovers) = entity.into_pandoc(&descriptions); | ||
136 | |||
137 | if log_enabled!(log::Level::Trace) { | ||
138 | let json = | ||
139 | serde_json::to_string(&pandoc_ast).context("Failed to convert Pandoc AST to JSON")?; | ||
140 | trace!("Sending json: {}", json); | ||
141 | write!( | ||
142 | pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), | ||
143 | "{}", | ||
144 | &json, | ||
145 | ) | ||
146 | .context("Failed to convert Pandoc AST to JSON")?; | ||
147 | } else { | ||
148 | serde_json::to_writer( | ||
149 | pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), | ||
150 | &pandoc_ast, | ||
151 | ) | ||
152 | .context("Failed to convert Pandoc AST to JSON")?; | ||
153 | } | ||
154 | |||
155 | let output = pandoc | ||
156 | .wait_with_output() | ||
157 | .expect("Pandoc command wasn't running"); | ||
158 | |||
159 | if !output.status.success() { | ||
160 | Err(CommandError { | ||
161 | status: output.status, | ||
162 | stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), | ||
163 | }) | ||
164 | .context("Pandoc command failed")?; | ||
165 | } | ||
166 | |||
167 | let mut command = Command::new("pandoc"); | ||
168 | command | ||
169 | .stdin(Stdio::piped()) | ||
170 | .stdout(Stdio::piped()) | ||
171 | .stderr(Stdio::piped()) | ||
172 | .args(&[ | ||
173 | "--from=markdown", | ||
174 | "--to=html", | ||
175 | "--css=res/style.css", | ||
176 | "--standalone", | ||
177 | "--self-contained", | ||
178 | md_output_file | ||
179 | .to_str() | ||
180 | .context("Entity name is not valid UTF-8")?, | ||
181 | "--output", | ||
182 | html_output_file | ||
183 | .to_str() | ||
184 | .context("Entity name is not valid UTF-8")?, | ||
185 | ]); | ||
186 | |||
187 | debug!("Launching command: {:?}", command); | ||
188 | |||
189 | let output = command | ||
190 | .output() | ||
191 | .context("Failed to execute Pandoc command")?; | ||
192 | |||
193 | if !output.status.success() { | ||
194 | Err(CommandError { | ||
195 | status: output.status, | ||
196 | stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), | ||
197 | }) | ||
198 | .context("Pandoc command failed")?; | ||
199 | } | ||
200 | |||
201 | Ok(leftovers) | ||
202 | } | ||
203 | |||
204 | #[derive(Debug, Clone, Error)] | ||
205 | #[error("Command returned status {:?} and stderr {:?}", status, stderr)] | ||
206 | struct CommandError { | ||
207 | status: std::process::ExitStatus, | ||
208 | stderr: String, | ||
209 | } | ||