From 17b10fab16bc5df3a969826150e92f50e88a99b9 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Sun, 28 Nov 2021 00:19:12 +0100 Subject: add css and html template, refactor build --- src/build.rs | 411 +++++++++++++++++++++++------------------------------------ 1 file changed, 161 insertions(+), 250 deletions(-) (limited to 'src/build.rs') 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 @@ use std::path::{Path, PathBuf}; -use eyre::{eyre, ContextCompat, Result, WrapErr}; +use eyre::{eyre, Result, WrapErr}; use log::{debug, error, log_enabled, trace, warn}; use pandoc_ast::MutVisitor; use crate::{ - filters, - utils::{AutoIdentifier, PandocMeta, PandocOutputExt}, + filters::{self, relativize_summary}, + utils::{AutoIdentifier, PandocMeta, PandocOutputExt, PathExt}, }; +const CSS: &str = include_str!("../res/style.css"); +const HTML_TEMPLATE: &str = include_str!("../res/template.html"); + pub fn do_build(config: &crate::config::Config) -> Result<()> { - let summary = Summary::try_from_file(&config.book.summary)?; + let tmpdir = tempfile::tempdir().wrap_err("Could not create temporary directory")?; + debug!("Created temporary directory at: '{}'", tmpdir.path().display()); + let template_path = tmpdir.path().join("template.html"); + trace!("Writing HTML template to: '{}'", template_path.display()); + std::fs::write(&template_path, HTML_TEMPLATE) + .wrap_err("Could not save HTML template in temporary directory")?; + let source_root = Path::new(&config.book.summary) .parent() .expect("Summary has no parent"); - let files = summary.collect_source_files(source_root)?; + let (summary, files) = process_summary(&config.book.summary, source_root)?; let build_dir = Path::new(&config.build.build_dir); trace!("Creating build directory: '{}'", build_dir.display()); @@ -25,6 +34,10 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> { ) })?; + let style_path = build_dir.join("style.css"); + debug!("Generating file: '{}'", style_path.display()); + std::fs::write(&style_path, CSS).wrap_err("Could not create style.css")?; + // Pre-create files so that we know which links to relativize for SourceFile { path, .. } in &files { let output_file = build_dir.join(path.with_extension("html")); @@ -47,7 +60,7 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> { })?; } - for SourceFile { path, source } in &files { + for SourceFile { path, source, .. } in &files { let mut pandoc_command = pandoc::new(); let output_file = build_dir.join(path.with_extension("html")); @@ -61,25 +74,36 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> { .expect("Source file has no parent") .to_path_buf(); let build_dir_clone = build_dir.to_path_buf(); - let summary_clone = summary.source.clone(); + + let level = source_dir + .components() + .skip_while(|c| matches!(c, std::path::Component::CurDir)) + .count(); + + let summary = relativize_summary(&summary.source, level); + let mut source = source.clone(); + source.meta.insert( + "summary".to_string(), + pandoc_ast::MetaValue::MetaBlocks(summary.blocks), + ); + + let style_path = std::iter::repeat(std::path::Component::ParentDir) + .take(level) + .collect::() + .join("style.css"); pandoc_command .set_input(pandoc::InputKind::Pipe(source.to_json())) .set_input_format(pandoc::InputFormat::Json, vec![]) .set_output(pandoc::OutputKind::File(output_file)) .set_output_format(pandoc::OutputFormat::Html5, vec![]) - .add_options(&[pandoc::PandocOption::SelfContained]) + .add_options(&[ + pandoc::PandocOption::Css(style_path.to_string()), + pandoc::PandocOption::SectionDivs, + pandoc::PandocOption::Standalone, + pandoc::PandocOption::Template(template_path.clone()), + ]) .add_filter(move |source| { - let level = source_dir - .components() - .skip_while(|c| matches!(c, std::path::Component::CurDir)) - .count(); - - let mut insert_summary_filter = filters::InsertSummary { - level, - summary: &summary_clone, - }; - let mut relativize_urls_filter = filters::RelativizeUrls { config: &config_clone, // TODO: other output formats @@ -89,7 +113,6 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> { }; let mut source = pandoc_ast::Pandoc::from_json(&source); - insert_summary_filter.walk_pandoc(&mut source); relativize_urls_filter.walk_pandoc(&mut source); source.to_json() }); @@ -109,7 +132,7 @@ pub fn do_build(config: &crate::config::Config) -> Result<()> { // TODO: move that into generated.rs fn generate_source( title: Vec, - children: Vec<(PandocMeta, PathBuf)>, + children: &[(&PandocMeta, &Path)], level: usize, ) -> Result { // TODO: make that text configurable @@ -117,8 +140,8 @@ fn generate_source( "Here are the articles in this section:".to_string(), )])]; - for (mut child, file) in children { - let title = match child.remove("title") { + for (child, file) in children { + let title = match child.get("title").cloned() { None => { warn!("Missing title for file: '{}'", file.display()); vec![pandoc_ast::Inline::Str("Untitled page".to_string())] @@ -174,17 +197,13 @@ fn list_content(block: &mut pandoc_ast::Block) -> Result<&mut Vec>) -> Result> { - vec.iter_mut().map(Node::try_from_vec_block).collect() -} - // TODO: support separators like these: // --------- #[derive(Debug)] pub struct Summary { source: pandoc_ast::Pandoc, - nodes: Vec, + //nodes: Vec, } #[derive(Debug)] @@ -194,258 +213,150 @@ struct SourceFile { } // TODO: move that into summary.rs -impl Summary { - fn try_from_file(file: &str) -> Result { - debug!("Parsing summary"); - let mut pandoc_command = pandoc::new(); - pandoc_command - .add_input(file) - .set_output_format(pandoc::OutputFormat::Json, vec![]) - .set_output(pandoc::OutputKind::Pipe); +fn process_summary(file: &str, source_root: &Path) -> Result<(Summary, Vec)> { + debug!("Parsing summary"); + let mut pandoc_command = pandoc::new(); + pandoc_command + .add_input(file) + .set_output_format(pandoc::OutputFormat::Json, vec![]) + .set_output(pandoc::OutputKind::Pipe); + + if log_enabled!(log::Level::Trace) { + pandoc_command.set_show_cmdline(true); + } - trace!("Launching pandoc command"); + let output = pandoc_command + .execute() + .wrap_err("Could not execute pandoc")? + .buffer(); - if log_enabled!(log::Level::Trace) { - pandoc_command.set_show_cmdline(true); - } - - let output = pandoc_command - .execute() - .wrap_err("Could not execute pandoc")? - .buffer(); - - let document = pandoc_ast::Pandoc::from_json(&output); - - let summary: Self = document.try_into()?; - if summary.has_files_missing( - Path::new(file) - .parent() - .expect("Summary file has no parent"), - ) { - return Err(eyre!("Files from the summary are missing, aborting")); - } + let mut document = pandoc_ast::Pandoc::from_json(&output); - Ok(summary) + if document.blocks.len() != 1 { + return Err(eyre!("Summary does not contain a single list")); } - fn has_files_missing(&self, root: &Path) -> bool { - // Do not use `.any()` to prevent short-circuiting, we want to report all missing files - self.nodes.iter().fold(false, |acc, node| { - let missing = node.has_files_missing(root); - acc || missing - }) - } + let root = &mut document.blocks[0]; - /// Get a list of source files. - /// - /// If a file is a generated file, generate it and store it in memory. - fn collect_source_files(&self, root: &Path) -> Result> { - let mut result = Vec::new(); + let list = list_content(root)?; - for node in &self.nodes { - node.collect_source_files(&mut result, root, Path::new("."), 0)?; - } + let mut source_files = Vec::new(); - Ok(result) + for element in list.iter_mut() { + process_summary_element(element, Path::new("./"), source_root, &mut source_files)?; } -} - -impl TryFrom for Summary { - type Error = eyre::Error; - fn try_from(mut document: pandoc_ast::Pandoc) -> Result { - if document.blocks.len() != 1 { - return Err(eyre!("Summary does not contain a single list")); - } + Ok((Summary { source: document }, source_files)) +} - let root = &mut document.blocks[0]; +fn process_summary_element( + element: &mut Vec, + parent: &Path, + source_root: &Path, + source_files: &mut Vec, +) -> Result<()> { + if element.len() != 1 && element.len() != 2 { + // TODO: better error message? + return Err(eyre!("Summary element does not contain a single list")); + } - let list = list_content(root)?; + trace!("Parsing summary element"); + let mut value = element.iter_mut(); - let nodes = list - .iter_mut() - .map(Node::try_from_vec_block) - .collect::>()?; + let item = match value.next().unwrap() { + pandoc_ast::Block::Plain(inlines) => inlines, + pandoc_ast::Block::Para(inlines) => inlines, + _ => return Err(eyre!("List item is not a link or plain text")), + }; - Ok(Summary { - source: document, - nodes, - }) + if item.is_empty() { + return Err(eyre!("Summary list items cannot be empty")); } -} -#[derive(Debug)] -pub enum Node { - Provided { - file: String, - children: Vec, - }, - Generated { - file: String, - title: Vec, - children: Vec, - }, -} - -impl Node { - fn children(&self) -> &[Node] { - match self { - Node::Provided { children, .. } => children, - Node::Generated { children, .. } => children, + let child_parent = match &item[0] { + pandoc_ast::Inline::Link(_, _, target) => { + let file = &target.0; + Path::new(&file).with_extension("") } - } - - fn has_files_missing(&self, root: &Path) -> bool { - if let Node::Provided { file, .. } = self { - if !root.join(file).exists() { - error!("File '{}' specified in summary does not exists", file); - return true; - } + _ => { + let title = item.clone(); + let id = AutoIdentifier::from(title.as_slice()); + parent.join(&id.0) } + }; + trace!("Summary element is {:?}", child_parent); - // Do not use `.any()` to prevent short-circuiting, we want to report all missing files - self.children().iter().fold(false, |acc, node| { - let missing = node.has_files_missing(root); - acc || missing - }) + let previous_source_len = source_files.len(); + if let Some(children) = value.next() { + for child in list_content(children)? { + process_summary_element(child, &child_parent, source_root, source_files)?; + } } - fn collect_source_files( - &self, - result: &mut Vec, - root: &Path, - parent: &Path, - level: usize, - ) -> Result<()> { - let new_parent; - let children_; - let path; - let source: Box _>; - - match self { - Node::Provided { file, children } => { - trace!("Parsing file: '{}'", file); - - // TODO: some filters here? not all filters, since we may want to filter generated - // files too - let mut pandoc_command = pandoc::new(); - pandoc_command - .add_input(&root.join(file)) - .set_output(pandoc::OutputKind::Pipe) - .set_output_format(pandoc::OutputFormat::Json, vec![]); - - if log_enabled!(log::Level::Trace) { - pandoc_command.set_show_cmdline(true); - } - - let raw_source = pandoc_command - .execute() - .wrap_err_with(|| format!("Failed to parse '{}'", file))? - .buffer(); - source = Box::new(move |_| Ok(pandoc_ast::Pandoc::from_json(&raw_source))); - - let file = Path::new(&file); - let stem = file.file_stem().expect("No file name"); - let id = - AutoIdentifier::from(stem.to_str().wrap_err("Invalid unicode in file name")?); - - path = file.into(); - new_parent = file.parent().expect("Source file has no parent").join(&*id); - children_ = children; + match &item[0] { + pandoc_ast::Inline::Link(_, _, target) => { + if item.len() != 1 { + return Err(eyre!("Summary list item not a single link or plain text")); } - Self::Generated { - file, - title, - children, - } => { - trace!("Found file to generate: '{}'", file); - - path = file.into(); - - source = Box::new(move |direct_children| { - generate_source(title.clone(), direct_children, level) - }); - new_parent = Path::new(file).with_extension(""); - children_ = children; - } - }; - - let mut direct_children = Vec::with_capacity(children_.len()); - - for child in children_ { - child.collect_source_files(result, root, &new_parent, level + 1)?; - let direct_child = result.last().unwrap(); - direct_children.push((direct_child.source.meta.clone(), direct_child.path.clone())); + let file = target.0.clone(); + source_files.push(parse_file(&file, source_root)?); } - - result.push(SourceFile { - path, - source: source(direct_children)?, - }); - - Ok(()) - } - - // Wil also modify the block to linkify generated pages - fn try_from_vec_block(value: &mut Vec) -> Result { - if value.len() != 1 && value.len() != 2 { - // TODO: better error message? - return Err(eyre!("Summary does not contain a single list")); - } - - let mut value = value.iter_mut(); - - let item = match value.next().unwrap() { - pandoc_ast::Block::Plain(inlines) => inlines, - pandoc_ast::Block::Para(inlines) => inlines, - _ => return Err(eyre!("List item is not a link or plain text")), - }; - - if item.is_empty() { - return Err(eyre!("Summary list items cannot be empty")); + _ => { + let title = item.clone(); + + let id = AutoIdentifier::from(title.as_slice()); + + *item = vec![pandoc_ast::Inline::Link( + (String::new(), vec!["generated".to_string()], vec![]), + item.clone(), + ( + parent.join(&id.0).with_extension("html").to_string(), + String::new(), + ), + )]; + + // TODO: this shows children recursively (and has a bug when in a subdirectory) + let children_metadata = source_files[previous_source_len..source_files.len()] + .iter() + .map(|source| (&source.source.meta, source.path.as_ref())) + .collect::>(); + + let source = generate_source(title, &children_metadata, 0)?; + + source_files.push(SourceFile { + path: child_parent.with_extension("html"), + source, + }); } + } - let children = if let Some(children) = value.next() { - try_into_node_vec(list_content(children)?)? - } else { - vec![] - }; - - match &item[0] { - pandoc_ast::Inline::Link(_, _, target) => { - if item.len() != 1 { - return Err(eyre!("Summary list item not a single link or plain text")); - } - - let file = target.0.clone(); - - Ok(Node::Provided { file, children }) - } - _ => { - let title = item.clone(); + Ok(()) +} - let id = AutoIdentifier::from(title.as_slice()); +fn parse_file(file: &str, source_root: &Path) -> Result { + trace!("Parsing file: '{}'", file); - // TODO: missing parent + // TODO: some filters here? not all filters, since we may want to filter generated + // files too + let mut pandoc_command = pandoc::new(); + pandoc_command + .add_input(&source_root.join(file)) + .set_output(pandoc::OutputKind::Pipe) + .set_output_format(pandoc::OutputFormat::Json, vec![]); - // Move generate page into this pass - //let mut file = parent.join(&*id); - //file.set_extension("md"); + if log_enabled!(log::Level::Trace) { + pandoc_command.set_show_cmdline(true); + } - // TODO: Attribute to style them differently - *item = vec![pandoc_ast::Inline::Link( - (String::new(), vec!["generated".to_string()], vec![]), - item.clone(), - (id.0.clone(), String::new()), - )]; + let raw_source = pandoc_command + .execute() + .wrap_err_with(|| format!("Failed to parse '{}'", file))? + .buffer(); + let source = pandoc_ast::Pandoc::from_json(&raw_source); - Ok(Node::Generated { - file: id.0, - title, - children, - }) - } - } - } + Ok(SourceFile { + path: file.into(), + source, + }) } -- cgit v1.2.3