summaryrefslogtreecommitdiffstats
path: root/src/build.rs
diff options
context:
space:
mode:
authorMinijackson <minijackson@riseup.net>2021-11-28 00:19:12 +0100
committerMinijackson <minijackson@riseup.net>2021-11-28 00:21:46 +0100
commit17b10fab16bc5df3a969826150e92f50e88a99b9 (patch)
tree12a0c721e89ef17f07a9ede593d82a4c95bc7937 /src/build.rs
parent517cabe8ec54d0bf5f5f9cc9089d76a1fad7bb6a (diff)
downloadpandoc-docbook-17b10fab16bc5df3a969826150e92f50e88a99b9.tar.gz
pandoc-docbook-17b10fab16bc5df3a969826150e92f50e88a99b9.zip
add css and html template, refactor build
Diffstat (limited to 'src/build.rs')
-rw-r--r--src/build.rs411
1 files changed, 161 insertions, 250 deletions
diff --git a/src/build.rs b/src/build.rs
index 0b0c646..1c12476 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1,20 +1,29 @@
1use std::path::{Path, PathBuf}; 1use std::path::{Path, PathBuf};
2 2
3use eyre::{eyre, ContextCompat, Result, WrapErr}; 3use eyre::{eyre, Result, WrapErr};
4use log::{debug, error, log_enabled, trace, warn}; 4use log::{debug, error, log_enabled, trace, warn};
5use pandoc_ast::MutVisitor; 5use pandoc_ast::MutVisitor;
6 6
7use crate::{ 7use crate::{
8 filters, 8 filters::{self, relativize_summary},
9 utils::{AutoIdentifier, PandocMeta, PandocOutputExt}, 9 utils::{AutoIdentifier, PandocMeta, PandocOutputExt, PathExt},
10}; 10};
11 11
12const CSS: &str = include_str!("../res/style.css");
13const HTML_TEMPLATE: &str = include_str!("../res/template.html");
14
12pub fn do_build(config: &crate::config::Config) -> Result<()> { 15pub fn do_build(config: &crate::config::Config) -> Result<()> {
13 let summary = Summary::try_from_file(&config.book.summary)?; 16 let tmpdir = tempfile::tempdir().wrap_err("Could not create temporary directory")?;
17 debug!("Created temporary directory at: '{}'", tmpdir.path().display());
18 let template_path = tmpdir.path().join("template.html");
19 trace!("Writing HTML template to: '{}'", template_path.display());
20 std::fs::write(&template_path, HTML_TEMPLATE)
21 .wrap_err("Could not save HTML template in temporary directory")?;
22
14 let source_root = Path::new(&config.book.summary) 23 let source_root = Path::new(&config.book.summary)
15 .parent() 24 .parent()
16 .expect("Summary has no parent"); 25 .expect("Summary has no parent");
17 let files = summary.collect_source_files(source_root)?; 26 let (summary, files) = process_summary(&config.book.summary, source_root)?;
18 27
19 let build_dir = Path::new(&config.build.build_dir); 28 let build_dir = Path::new(&config.build.build_dir);
20 trace!("Creating build directory: '{}'", build_dir.display()); 29 trace!("Creating build directory: '{}'", build_dir.display());
@@ -25,6 +34,10 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
25 ) 34 )
26 })?; 35 })?;
27 36
37 let style_path = build_dir.join("style.css");
38 debug!("Generating file: '{}'", style_path.display());
39 std::fs::write(&style_path, CSS).wrap_err("Could not create style.css")?;
40
28 // Pre-create files so that we know which links to relativize 41 // Pre-create files so that we know which links to relativize
29 for SourceFile { path, .. } in &files { 42 for SourceFile { path, .. } in &files {
30 let output_file = build_dir.join(path.with_extension("html")); 43 let output_file = build_dir.join(path.with_extension("html"));
@@ -47,7 +60,7 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
47 })?; 60 })?;
48 } 61 }
49 62
50 for SourceFile { path, source } in &files { 63 for SourceFile { path, source, .. } in &files {
51 let mut pandoc_command = pandoc::new(); 64 let mut pandoc_command = pandoc::new();
52 65
53 let output_file = build_dir.join(path.with_extension("html")); 66 let output_file = build_dir.join(path.with_extension("html"));
@@ -61,25 +74,36 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
61 .expect("Source file has no parent") 74 .expect("Source file has no parent")
62 .to_path_buf(); 75 .to_path_buf();
63 let build_dir_clone = build_dir.to_path_buf(); 76 let build_dir_clone = build_dir.to_path_buf();
64 let summary_clone = summary.source.clone(); 77
78 let level = source_dir
79 .components()
80 .skip_while(|c| matches!(c, std::path::Component::CurDir))
81 .count();
82
83 let summary = relativize_summary(&summary.source, level);
84 let mut source = source.clone();
85 source.meta.insert(
86 "summary".to_string(),
87 pandoc_ast::MetaValue::MetaBlocks(summary.blocks),
88 );
89
90 let style_path = std::iter::repeat(std::path::Component::ParentDir)
91 .take(level)
92 .collect::<PathBuf>()
93 .join("style.css");
65 94
66 pandoc_command 95 pandoc_command
67 .set_input(pandoc::InputKind::Pipe(source.to_json())) 96 .set_input(pandoc::InputKind::Pipe(source.to_json()))
68 .set_input_format(pandoc::InputFormat::Json, vec![]) 97 .set_input_format(pandoc::InputFormat::Json, vec![])
69 .set_output(pandoc::OutputKind::File(output_file)) 98 .set_output(pandoc::OutputKind::File(output_file))
70 .set_output_format(pandoc::OutputFormat::Html5, vec![]) 99 .set_output_format(pandoc::OutputFormat::Html5, vec![])
71 .add_options(&[pandoc::PandocOption::SelfContained]) 100 .add_options(&[
101 pandoc::PandocOption::Css(style_path.to_string()),
102 pandoc::PandocOption::SectionDivs,
103 pandoc::PandocOption::Standalone,
104 pandoc::PandocOption::Template(template_path.clone()),
105 ])
72 .add_filter(move |source| { 106 .add_filter(move |source| {
73 let level = source_dir
74 .components()
75 .skip_while(|c| matches!(c, std::path::Component::CurDir))
76 .count();
77
78 let mut insert_summary_filter = filters::InsertSummary {
79 level,
80 summary: &summary_clone,
81 };
82
83 let mut relativize_urls_filter = filters::RelativizeUrls { 107 let mut relativize_urls_filter = filters::RelativizeUrls {
84 config: &config_clone, 108 config: &config_clone,
85 // TODO: other output formats 109 // TODO: other output formats
@@ -89,7 +113,6 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
89 }; 113 };
90 114
91 let mut source = pandoc_ast::Pandoc::from_json(&source); 115 let mut source = pandoc_ast::Pandoc::from_json(&source);
92 insert_summary_filter.walk_pandoc(&mut source);
93 relativize_urls_filter.walk_pandoc(&mut source); 116 relativize_urls_filter.walk_pandoc(&mut source);
94 source.to_json() 117 source.to_json()
95 }); 118 });
@@ -109,7 +132,7 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> {
109// TODO: move that into generated.rs 132// TODO: move that into generated.rs
110fn generate_source( 133fn generate_source(
111 title: Vec<pandoc_ast::Inline>, 134 title: Vec<pandoc_ast::Inline>,
112 children: Vec<(PandocMeta, PathBuf)>, 135 children: &[(&PandocMeta, &Path)],
113 level: usize, 136 level: usize,
114) -> Result<pandoc_ast::Pandoc> { 137) -> Result<pandoc_ast::Pandoc> {
115 // TODO: make that text configurable 138 // TODO: make that text configurable
@@ -117,8 +140,8 @@ fn generate_source(
117 "Here are the articles in this section:".to_string(), 140 "Here are the articles in this section:".to_string(),
118 )])]; 141 )])];
119 142
120 for (mut child, file) in children { 143 for (child, file) in children {
121 let title = match child.remove("title") { 144 let title = match child.get("title").cloned() {
122 None => { 145 None => {
123 warn!("Missing title for file: '{}'", file.display()); 146 warn!("Missing title for file: '{}'", file.display());
124 vec![pandoc_ast::Inline::Str("Untitled page".to_string())] 147 vec![pandoc_ast::Inline::Str("Untitled page".to_string())]
@@ -174,17 +197,13 @@ fn list_content(block: &mut pandoc_ast::Block) -> Result<&mut Vec<Vec<pandoc_ast
174 } 197 }
175} 198}
176 199
177fn try_into_node_vec(vec: &mut Vec<Vec<pandoc_ast::Block>>) -> Result<Vec<Node>> {
178 vec.iter_mut().map(Node::try_from_vec_block).collect()
179}
180
181// TODO: support separators like these: 200// TODO: support separators like these:
182// --------- 201// ---------
183 202
184#[derive(Debug)] 203#[derive(Debug)]
185pub struct Summary { 204pub struct Summary {
186 source: pandoc_ast::Pandoc, 205 source: pandoc_ast::Pandoc,
187 nodes: Vec<Node>, 206 //nodes: Vec<Node>,
188} 207}
189 208
190#[derive(Debug)] 209#[derive(Debug)]
@@ -194,258 +213,150 @@ struct SourceFile {
194} 213}
195 214
196// TODO: move that into summary.rs 215// TODO: move that into summary.rs
197impl Summary { 216fn process_summary(file: &str, source_root: &Path) -> Result<(Summary, Vec<SourceFile>)> {
198 fn try_from_file(file: &str) -> Result<Self> { 217 debug!("Parsing summary");
199 debug!("Parsing summary"); 218 let mut pandoc_command = pandoc::new();
200 let mut pandoc_command = pandoc::new(); 219 pandoc_command
201 pandoc_command 220 .add_input(file)
202 .add_input(file) 221 .set_output_format(pandoc::OutputFormat::Json, vec![])
203 .set_output_format(pandoc::OutputFormat::Json, vec![]) 222 .set_output(pandoc::OutputKind::Pipe);
204 .set_output(pandoc::OutputKind::Pipe); 223
224 if log_enabled!(log::Level::Trace) {
225 pandoc_command.set_show_cmdline(true);
226 }
205 227
206 trace!("Launching pandoc command"); 228 let output = pandoc_command
229 .execute()
230 .wrap_err("Could not execute pandoc")?
231 .buffer();
207 232
208 if log_enabled!(log::Level::Trace) { 233 let mut document = pandoc_ast::Pandoc::from_json(&output);
209 pandoc_command.set_show_cmdline(true);
210 }
211
212 let output = pandoc_command
213 .execute()
214 .wrap_err("Could not execute pandoc")?
215 .buffer();
216
217 let document = pandoc_ast::Pandoc::from_json(&output);
218
219 let summary: Self = document.try_into()?;
220 if summary.has_files_missing(
221 Path::new(file)
222 .parent()
223 .expect("Summary file has no parent"),
224 ) {
225 return Err(eyre!("Files from the summary are missing, aborting"));
226 }
227 234
228 Ok(summary) 235 if document.blocks.len() != 1 {
236 return Err(eyre!("Summary does not contain a single list"));
229 } 237 }
230 238
231 fn has_files_missing(&self, root: &Path) -> bool { 239 let root = &mut document.blocks[0];
232 // Do not use `.any()` to prevent short-circuiting, we want to report all missing files
233 self.nodes.iter().fold(false, |acc, node| {
234 let missing = node.has_files_missing(root);
235 acc || missing
236 })
237 }
238 240
239 /// Get a list of source files. 241 let list = list_content(root)?;
240 ///
241 /// If a file is a generated file, generate it and store it in memory.
242 fn collect_source_files(&self, root: &Path) -> Result<Vec<SourceFile>> {
243 let mut result = Vec::new();
244 242
245 for node in &self.nodes { 243 let mut source_files = Vec::new();
246 node.collect_source_files(&mut result, root, Path::new("."), 0)?;
247 }
248 244
249 Ok(result) 245 for element in list.iter_mut() {
246 process_summary_element(element, Path::new("./"), source_root, &mut source_files)?;
250 } 247 }
251}
252
253impl TryFrom<pandoc_ast::Pandoc> for Summary {
254 type Error = eyre::Error;
255 248
256 fn try_from(mut document: pandoc_ast::Pandoc) -> Result<Self, Self::Error> { 249 Ok((Summary { source: document }, source_files))
257 if document.blocks.len() != 1 { 250}
258 return Err(eyre!("Summary does not contain a single list"));
259 }
260 251
261 let root = &mut document.blocks[0]; 252fn process_summary_element(
253 element: &mut Vec<pandoc_ast::Block>,
254 parent: &Path,
255 source_root: &Path,
256 source_files: &mut Vec<SourceFile>,
257) -> Result<()> {
258 if element.len() != 1 && element.len() != 2 {
259 // TODO: better error message?
260 return Err(eyre!("Summary element does not contain a single list"));
261 }
262 262
263 let list = list_content(root)?; 263 trace!("Parsing summary element");
264 let mut value = element.iter_mut();
264 265
265 let nodes = list 266 let item = match value.next().unwrap() {
266 .iter_mut() 267 pandoc_ast::Block::Plain(inlines) => inlines,
267 .map(Node::try_from_vec_block) 268 pandoc_ast::Block::Para(inlines) => inlines,
268 .collect::<Result<_>>()?; 269 _ => return Err(eyre!("List item is not a link or plain text")),
270 };
269 271
270 Ok(Summary { 272 if item.is_empty() {
271 source: document, 273 return Err(eyre!("Summary list items cannot be empty"));
272 nodes,
273 })
274 } 274 }
275}
276 275
277#[derive(Debug)] 276 let child_parent = match &item[0] {
278pub enum Node { 277 pandoc_ast::Inline::Link(_, _, target) => {
279 Provided { 278 let file = &target.0;
280 file: String, 279 Path::new(&file).with_extension("")
281 children: Vec<Node>,
282 },
283 Generated {
284 file: String,
285 title: Vec<pandoc_ast::Inline>,
286 children: Vec<Node>,
287 },
288}
289
290impl Node {
291 fn children(&self) -> &[Node] {
292 match self {
293 Node::Provided { children, .. } => children,
294 Node::Generated { children, .. } => children,
295 } 280 }
296 } 281 _ => {
297 282 let title = item.clone();
298 fn has_files_missing(&self, root: &Path) -> bool { 283 let id = AutoIdentifier::from(title.as_slice());
299 if let Node::Provided { file, .. } = self { 284 parent.join(&id.0)
300 if !root.join(file).exists() {
301 error!("File '{}' specified in summary does not exists", file);
302 return true;
303 }
304 } 285 }
286 };
287 trace!("Summary element is {:?}", child_parent);
305 288
306 // Do not use `.any()` to prevent short-circuiting, we want to report all missing files 289 let previous_source_len = source_files.len();
307 self.children().iter().fold(false, |acc, node| { 290 if let Some(children) = value.next() {
308 let missing = node.has_files_missing(root); 291 for child in list_content(children)? {
309 acc || missing 292 process_summary_element(child, &child_parent, source_root, source_files)?;
310 }) 293 }
311 } 294 }
312 295
313 fn collect_source_files( 296 match &item[0] {
314 &self, 297 pandoc_ast::Inline::Link(_, _, target) => {
315 result: &mut Vec<SourceFile>, 298 if item.len() != 1 {
316 root: &Path, 299 return Err(eyre!("Summary list item not a single link or plain text"));
317 parent: &Path,
318 level: usize,
319 ) -> Result<()> {
320 let new_parent;
321 let children_;
322 let path;
323 let source: Box<dyn FnOnce(_) -> _>;
324
325 match self {
326 Node::Provided { file, children } => {
327 trace!("Parsing file: '{}'", file);
328
329 // TODO: some filters here? not all filters, since we may want to filter generated
330 // files too
331 let mut pandoc_command = pandoc::new();
332 pandoc_command
333 .add_input(&root.join(file))
334 .set_output(pandoc::OutputKind::Pipe)
335 .set_output_format(pandoc::OutputFormat::Json, vec![]);
336
337 if log_enabled!(log::Level::Trace) {
338 pandoc_command.set_show_cmdline(true);
339 }
340
341 let raw_source = pandoc_command
342 .execute()
343 .wrap_err_with(|| format!("Failed to parse '{}'", file))?
344 .buffer();
345 source = Box::new(move |_| Ok(pandoc_ast::Pandoc::from_json(&raw_source)));
346
347 let file = Path::new(&file);
348 let stem = file.file_stem().expect("No file name");
349 let id =
350 AutoIdentifier::from(stem.to_str().wrap_err("Invalid unicode in file name")?);
351
352 path = file.into();
353 new_parent = file.parent().expect("Source file has no parent").join(&*id);
354 children_ = children;
355 } 300 }
356 301
357 Self::Generated { 302 let file = target.0.clone();
358 file, 303 source_files.push(parse_file(&file, source_root)?);
359 title,
360 children,
361 } => {
362 trace!("Found file to generate: '{}'", file);
363
364 path = file.into();
365
366 source = Box::new(move |direct_children| {
367 generate_source(title.clone(), direct_children, level)
368 });
369 new_parent = Path::new(file).with_extension("");
370 children_ = children;
371 }
372 };
373
374 let mut direct_children = Vec::with_capacity(children_.len());
375
376 for child in children_ {
377 child.collect_source_files(result, root, &new_parent, level + 1)?;
378 let direct_child = result.last().unwrap();
379 direct_children.push((direct_child.source.meta.clone(), direct_child.path.clone()));
380 } 304 }
381 305 _ => {
382 result.push(SourceFile { 306 let title = item.clone();
383 path, 307
384 source: source(direct_children)?, 308 let id = AutoIdentifier::from(title.as_slice());
385 }); 309
386 310 *item = vec![pandoc_ast::Inline::Link(
387 Ok(()) 311 (String::new(), vec!["generated".to_string()], vec![]),
388 } 312 item.clone(),
389 313 (
390 // Wil also modify the block to linkify generated pages 314 parent.join(&id.0).with_extension("html").to_string(),
391 fn try_from_vec_block(value: &mut Vec<pandoc_ast::Block>) -> Result<Self> { 315 String::new(),
392 if value.len() != 1 && value.len() != 2 { 316 ),
393 // TODO: better error message? 317 )];
394 return Err(eyre!("Summary does not contain a single list")); 318
395 } 319 // TODO: this shows children recursively (and has a bug when in a subdirectory)
396 320 let children_metadata = source_files[previous_source_len..source_files.len()]
397 let mut value = value.iter_mut(); 321 .iter()
398 322 .map(|source| (&source.source.meta, source.path.as_ref()))
399 let item = match value.next().unwrap() { 323 .collect::<Vec<_>>();
400 pandoc_ast::Block::Plain(inlines) => inlines, 324
401 pandoc_ast::Block::Para(inlines) => inlines, 325 let source = generate_source(title, &children_metadata, 0)?;
402 _ => return Err(eyre!("List item is not a link or plain text")), 326
403 }; 327 source_files.push(SourceFile {
404 328 path: child_parent.with_extension("html"),
405 if item.is_empty() { 329 source,
406 return Err(eyre!("Summary list items cannot be empty")); 330 });
407 } 331 }
332 }
408 333
409 let children = if let Some(children) = value.next() { 334 Ok(())
410 try_into_node_vec(list_content(children)?)? 335}
411 } else {
412 vec![]
413 };
414
415 match &item[0] {
416 pandoc_ast::Inline::Link(_, _, target) => {
417 if item.len() != 1 {
418 return Err(eyre!("Summary list item not a single link or plain text"));
419 }
420
421 let file = target.0.clone();
422
423 Ok(Node::Provided { file, children })
424 }
425 _ => {
426 let title = item.clone();
427 336
428 let id = AutoIdentifier::from(title.as_slice()); 337fn parse_file(file: &str, source_root: &Path) -> Result<SourceFile> {
338 trace!("Parsing file: '{}'", file);
429 339
430 // TODO: missing parent 340 // TODO: some filters here? not all filters, since we may want to filter generated
341 // files too
342 let mut pandoc_command = pandoc::new();
343 pandoc_command
344 .add_input(&source_root.join(file))
345 .set_output(pandoc::OutputKind::Pipe)
346 .set_output_format(pandoc::OutputFormat::Json, vec![]);
431 347
432 // Move generate page into this pass 348 if log_enabled!(log::Level::Trace) {
433 //let mut file = parent.join(&*id); 349 pandoc_command.set_show_cmdline(true);
434 //file.set_extension("md"); 350 }
435 351
436 // TODO: Attribute to style them differently 352 let raw_source = pandoc_command
437 *item = vec![pandoc_ast::Inline::Link( 353 .execute()
438 (String::new(), vec!["generated".to_string()], vec![]), 354 .wrap_err_with(|| format!("Failed to parse '{}'", file))?
439 item.clone(), 355 .buffer();
440 (id.0.clone(), String::new()), 356 let source = pandoc_ast::Pandoc::from_json(&raw_source);
441 )];
442 357
443 Ok(Node::Generated { 358 Ok(SourceFile {
444 file: id.0, 359 path: file.into(),
445 title, 360 source,
446 children, 361 })
447 })
448 }
449 }
450 }
451} 362}