From 7c92b602bc6c36acd055eb612173dc4a7ef545a6 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 1 Feb 2025 17:20:27 -0500 Subject: [PATCH 01/13] Add automated test for testing the link target code. --- src/intermediate/regular_link.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/intermediate/regular_link.rs b/src/intermediate/regular_link.rs index fe0d19b..6050c7e 100644 --- a/src/intermediate/regular_link.rs +++ b/src/intermediate/regular_link.rs @@ -42,7 +42,7 @@ intermediate!( } ); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum LinkTarget { Raw(String), Post { @@ -124,3 +124,27 @@ impl LinkTarget { } } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::Mutex; + + use crate::intermediate::Registry; + + use super::*; + + #[test] + fn link_target_raw() -> Result<(), CustomError> { + let registry = Registry::new(); + let registry = Arc::new(Mutex::new(registry)); + let intermediate_context = IntermediateContext::new(registry)?; + for inp in ["https://test.example/foo"] { + assert_eq!( + LinkTarget::from_string(intermediate_context.clone(), inp.to_owned())?, + LinkTarget::Raw(inp.to_owned()) + ); + } + Ok(()) + } +} From eb1818513185e20f0cb1b61be7dcf396211aab46 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 1 Feb 2025 20:50:13 -0500 Subject: [PATCH 02/13] Add support for Image targets in the intermediate step. --- .../regular_link/image_links.org | 22 +++ src/intermediate/regular_link.rs | 148 +++++++++++++++++- 2 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 org_test_documents/regular_link/image_links.org diff --git a/org_test_documents/regular_link/image_links.org b/org_test_documents/regular_link/image_links.org new file mode 100644 index 0000000..d2ddf71 --- /dev/null +++ b/org_test_documents/regular_link/image_links.org @@ -0,0 +1,22 @@ +[[file:image.svg]] + +[[file:/image.svg]] + +[[file:./image.svg]] + +[[/image.svg]] + +[[./image.svg]] + +# Check capitalization of extension +[[./image.SVG]] + +# Check spaces in path +[[./image and stuff.SVG]] + +[[/ssh:admin@test.example:important/file.svg]] + +[[file:/ssh:admin@test.example:important/file.svg]] + +# Check multiple parts in the path +[[file:/foo/bar/baz/image.svg]] diff --git a/src/intermediate/regular_link.rs b/src/intermediate/regular_link.rs index 6050c7e..b9fce81 100644 --- a/src/intermediate/regular_link.rs +++ b/src/intermediate/regular_link.rs @@ -1,3 +1,7 @@ +use std::borrow::Cow; +use std::path::Path; + +use organic::types::LinkType; use organic::types::StandardProperties; use url::Url; @@ -31,8 +35,11 @@ intermediate!( ret }; let raw_link = original.get_raw_link(); - let target = - LinkTarget::from_string(intermediate_context.clone(), raw_link.clone().into_owned())?; + let target = LinkTarget::from_string( + intermediate_context.clone(), + raw_link.clone().into_owned(), + &original.link_type, + )?; Ok(IRegularLink { raw_link: raw_link.into_owned(), children, @@ -52,13 +59,28 @@ pub(crate) enum LinkTarget { Target { target_id: String, }, + Image { + src: String, + alt: String, + }, } impl LinkTarget { pub(crate) fn from_string( intermediate_context: IntermediateContext<'_, '_>, input: String, + link_type: &LinkType<'_>, ) -> Result { + // If link type is file and the path ends in .svg then make it an image target + if let LinkType::File = link_type + && input.to_ascii_lowercase().ends_with(".svg") + { + let src = Self::get_image_src(&input)?; + let alt = Self::get_image_alt(&input)?; + + return Ok(LinkTarget::Image { src, alt }); + }; + let parsed = Url::parse(&input); if let Err(url::ParseError::RelativeUrlWithoutBase) = parsed { let target_id = { @@ -121,12 +143,64 @@ impl LinkTarget { .unwrap_or_default(), target_id ))), + LinkTarget::Image { src, .. } => Ok(Some(src.clone())), + } + } + + /// Get the value for the src attribute of the image. + fn get_image_src(input: &str) -> Result { + let input = if input.to_ascii_lowercase().starts_with("file:") { + Cow::Borrowed(&input[5..]) + } else { + Cow::Borrowed(input) + }; + let path = Path::new(input.as_ref()); + + if input.to_ascii_lowercase().starts_with("/ssh:") { + return Ok(format!("file:/{}", input)); + } + + if path.is_absolute() { + return Ok(format!("file://{}", input)); + } + return Ok(input.into_owned()); + } + + /// Get file name from the last segment of an image path. + fn get_image_alt(input: &str) -> Result { + let input = if input.to_ascii_lowercase().starts_with("file:") { + Cow::Borrowed(&input[5..]) + } else { + Cow::Borrowed(input) + }; + let path = Path::new(input.as_ref()); + match path + .components() + .last() + .ok_or("Images should have at least one component in their path.")? + { + std::path::Component::Prefix(_) => { + // Prefix components only occur on windows + panic!("Prefix components are not supporterd.") + } + std::path::Component::RootDir + | std::path::Component::CurDir + | std::path::Component::ParentDir => { + return Err( + "Final component of an image path should be a normal component.".into(), + ); + } + std::path::Component::Normal(file_name) => Ok(file_name + .to_str() + .ok_or("Image link was not valid utf-8.")? + .to_owned()), } } } #[cfg(test)] mod tests { + use std::borrow::Cow; use std::sync::Arc; use std::sync::Mutex; @@ -139,12 +213,78 @@ mod tests { let registry = Registry::new(); let registry = Arc::new(Mutex::new(registry)); let intermediate_context = IntermediateContext::new(registry)?; - for inp in ["https://test.example/foo"] { + for (inp, typ) in [( + "https://test.example/foo", + LinkType::Protocol(Cow::from("https")), + )] { assert_eq!( - LinkTarget::from_string(intermediate_context.clone(), inp.to_owned())?, + LinkTarget::from_string(intermediate_context.clone(), inp.to_owned(), &typ)?, LinkTarget::Raw(inp.to_owned()) ); } Ok(()) } + + #[test] + fn link_target_image() -> Result<(), CustomError> { + let registry = Registry::new(); + let registry = Arc::new(Mutex::new(registry)); + let intermediate_context = IntermediateContext::new(registry)?; + for (inp, typ, expected_src, expected_alt) in [ + ("file:image.svg", LinkType::File, "image.svg", "image.svg"), + ( + "file:/image.svg", + LinkType::File, + "file:///image.svg", + "image.svg", + ), + ( + "file:./image.svg", + LinkType::File, + "./image.svg", + "image.svg", + ), + ( + "/image.svg", + LinkType::File, + "file:///image.svg", + "image.svg", + ), + ("./image.svg", LinkType::File, "./image.svg", "image.svg"), + ("./image.SVG", LinkType::File, "./image.SVG", "image.SVG"), + ( + "./image and stuff.SVG", + LinkType::File, + "./image and stuff.SVG", + "image and stuff.SVG", + ), + ( + "/ssh:admin@test.example:important/file.svg", + LinkType::File, + "file://ssh:admin@test.example:important/file.svg", + "file.svg", + ), + ( + "file:/ssh:admin@test.example:important/file.svg", + LinkType::File, + "file://ssh:admin@test.example:important/file.svg", + "file.svg", + ), + ( + "file:/foo/bar/baz/image.svg", + LinkType::File, + "file:///foo/bar/baz/image.svg", + "image.svg", + ), + ] { + assert_eq!( + LinkTarget::from_string(intermediate_context.clone(), inp.to_owned(), &typ)?, + LinkTarget::Image { + src: expected_src.to_owned(), + alt: expected_alt.to_owned() + } + ); + } + Ok(()) + } } From 4fb67c18aee16528b744de29db54c116749c82f6 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 1 Feb 2025 22:36:55 -0500 Subject: [PATCH 03/13] Add support for rendering the images. --- .../templates/html/ast_node.dust | 3 +- .../templates/html/object.dust | 3 +- ...lar_link.dust => regular_link_anchor.dust} | 0 .../templates/html/regular_link_image.dust | 1 + src/context/regular_link.rs | 43 ++++++++++++++++--- src/intermediate/mod.rs | 1 + 6 files changed, 42 insertions(+), 9 deletions(-) rename default_environment/templates/html/{regular_link.dust => regular_link_anchor.dust} (100%) create mode 100644 default_environment/templates/html/regular_link_image.dust diff --git a/default_environment/templates/html/ast_node.dust b/default_environment/templates/html/ast_node.dust index cb3b7a9..3fef338 100644 --- a/default_environment/templates/html/ast_node.dust +++ b/default_environment/templates/html/ast_node.dust @@ -32,7 +32,8 @@ {@eq value="code"}{>code/}{/eq} {@eq value="verbatim"}{>verbatim/}{/eq} {@eq value="plain_text"}{>plain_text/}{/eq} - {@eq value="regular_link"}{>regular_link/}{/eq} + {@eq value="regular_link_anchor"}{>regular_link_anchor/}{/eq} + {@eq value="regular_link_image"}{>regular_link_image/}{/eq} {@eq value="radio_link"}{>radio_link/}{/eq} {@eq value="radio_target"}{>radio_target/}{/eq} {@eq value="plain_link"}{>plain_link/}{/eq} diff --git a/default_environment/templates/html/object.dust b/default_environment/templates/html/object.dust index 41e026a..8b15582 100644 --- a/default_environment/templates/html/object.dust +++ b/default_environment/templates/html/object.dust @@ -6,7 +6,8 @@ {@eq value="code"}{>code/}{/eq} {@eq value="verbatim"}{>verbatim/}{/eq} {@eq value="plain_text"}{>plain_text/}{/eq} - {@eq value="regular_link"}{>regular_link/}{/eq} + {@eq value="regular_link_anchor"}{>regular_link_anchor/}{/eq} + {@eq value="regular_link_image"}{>regular_link_image/}{/eq} {@eq value="radio_link"}{>radio_link/}{/eq} {@eq value="radio_target"}{>radio_target/}{/eq} {@eq value="plain_link"}{>plain_link/}{/eq} diff --git a/default_environment/templates/html/regular_link.dust b/default_environment/templates/html/regular_link_anchor.dust similarity index 100% rename from default_environment/templates/html/regular_link.dust rename to default_environment/templates/html/regular_link_anchor.dust diff --git a/default_environment/templates/html/regular_link_image.dust b/default_environment/templates/html/regular_link_image.dust new file mode 100644 index 0000000..f8b3bfd --- /dev/null +++ b/default_environment/templates/html/regular_link_image.dust @@ -0,0 +1 @@ +{.alt} diff --git a/src/context/regular_link.rs b/src/context/regular_link.rs index fc77598..fcf9602 100644 --- a/src/context/regular_link.rs +++ b/src/context/regular_link.rs @@ -3,6 +3,7 @@ use serde::Serialize; use super::render_context::RenderContext; use crate::error::CustomError; use crate::intermediate::IRegularLink; +use crate::intermediate::LinkTarget; use super::macros::render; use super::RenderObject; @@ -10,13 +11,29 @@ use super::RenderObject; #[derive(Debug, Serialize)] #[serde(tag = "type")] #[serde(rename = "regular_link")] -pub(crate) struct RenderRegularLink { +pub(crate) enum RenderRegularLink { + #[serde(rename = "regular_link_anchor")] + Anchor(RenderRegularLinkAnchor), + #[serde(rename = "regular_link_image")] + Image(RenderRegularLinkImage), +} + +#[derive(Debug, Serialize)] +pub(crate) struct RenderRegularLinkAnchor { target: String, raw_link: String, children: Vec, post_blank: organic::types::PostBlank, } +#[derive(Debug, Serialize)] +pub(crate) struct RenderRegularLinkImage { + src: String, + alt: String, + raw_link: String, + post_blank: organic::types::PostBlank, +} + render!(RenderRegularLink, IRegularLink, original, render_context, { let children = { let mut ret = Vec::new(); @@ -31,10 +48,22 @@ render!(RenderRegularLink, IRegularLink, original, render_context, { .generate_final_target(render_context.clone())? .unwrap_or_else(|| "".to_owned()); - Ok(RenderRegularLink { - target, - raw_link: original.raw_link.clone(), - children, - post_blank: original.post_blank, - }) + let render_link = match &original.target { + LinkTarget::Raw(_) | LinkTarget::Post { .. } | LinkTarget::Target { .. } => { + RenderRegularLink::Anchor(RenderRegularLinkAnchor { + target, + raw_link: original.raw_link.clone(), + children, + post_blank: original.post_blank, + }) + } + LinkTarget::Image { alt, .. } => RenderRegularLink::Image(RenderRegularLinkImage { + src: target, + alt: alt.clone(), + raw_link: original.raw_link.clone(), + post_blank: original.post_blank, + }), + }; + + Ok(render_link) }); diff --git a/src/intermediate/mod.rs b/src/intermediate/mod.rs index 042e971..44c257f 100644 --- a/src/intermediate/mod.rs +++ b/src/intermediate/mod.rs @@ -123,6 +123,7 @@ pub(crate) use radio_link::IRadioLink; pub(crate) use radio_target::IRadioTarget; pub(crate) use registry::Registry; pub(crate) use regular_link::IRegularLink; +pub(crate) use regular_link::LinkTarget; pub(crate) use section::ISection; pub(crate) use special_block::ISpecialBlock; pub(crate) use src_block::ISrcBlock; From 8fd37cbf2287024838fb7c1daa641c1202834570 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 1 Feb 2025 23:28:09 -0500 Subject: [PATCH 04/13] Remove a previously completed TODO. --- src/context/render_context.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/context/render_context.rs b/src/context/render_context.rs index 18ad7d0..7cb9b0b 100644 --- a/src/context/render_context.rs +++ b/src/context/render_context.rs @@ -7,7 +7,6 @@ use crate::error::CustomError; #[derive(Debug, Clone)] pub(crate) struct RenderContext<'intermediate> { pub(crate) config: &'intermediate Config, - // TODO: Perhaps rename to output_root_directory. pub(crate) output_root_directory: &'intermediate Path, pub(crate) output_file: &'intermediate Path, From 463be34302dda58bf23383c5c8d6f0a1e22fd0f2 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 7 Feb 2025 20:51:31 -0500 Subject: [PATCH 05/13] Async closure is now stable. --- src/intermediate/blog_post.rs | 3 +++ src/main.rs | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/intermediate/blog_post.rs b/src/intermediate/blog_post.rs index adb8ac2..b09227f 100644 --- a/src/intermediate/blog_post.rs +++ b/src/intermediate/blog_post.rs @@ -35,6 +35,7 @@ impl BlogPost { ) -> Result { let post_id = post_dir.strip_prefix(posts_dir)?.as_os_str(); + // Load all the *.org files under the post directory from disk into memory let org_files = { let mut ret = Vec::new(); let org_files_iter = get_org_files(post_dir).await?; @@ -43,6 +44,8 @@ impl BlogPost { } ret }; + + // Parse all the *.org files let parsed_org_files = { let mut ret = Vec::new(); for (path, contents) in org_files.iter() { diff --git a/src/main.rs b/src/main.rs index bbe13c6..5cf19b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ #![feature(let_chains)] -#![feature(async_closure)] use std::process::ExitCode; use clap::Parser; From 5cac44c62527fc406e316de146ef8356c52a1b77 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 7 Feb 2025 21:08:06 -0500 Subject: [PATCH 06/13] Store the path to the original source file in the blog post object. --- src/command/build/runner.rs | 2 +- src/intermediate/blog_post.rs | 6 +++++- src/intermediate/blog_post_page.rs | 12 +++++++++++- src/intermediate/page.rs | 12 +++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/command/build/runner.rs b/src/command/build/runner.rs index a4aa01b..835a4f4 100644 --- a/src/command/build/runner.rs +++ b/src/command/build/runner.rs @@ -163,7 +163,7 @@ async fn load_pages(config: &Config) -> Result, CustomError> { ret.push( IPage::new( intermediate_context, - PageInput::new(relative_to_pages_dir_path, parsed_document), + PageInput::new(relative_to_pages_dir_path, real_path, parsed_document), ) .await?, ); diff --git a/src/intermediate/blog_post.rs b/src/intermediate/blog_post.rs index b09227f..246959a 100644 --- a/src/intermediate/blog_post.rs +++ b/src/intermediate/blog_post.rs @@ -76,7 +76,11 @@ impl BlogPost { ret.push( BlogPostPage::new( intermediate_context, - BlogPostPageInput::new(relative_to_post_dir_path, parsed_document), + BlogPostPageInput::new( + relative_to_post_dir_path, + real_path, + parsed_document, + ), ) .await?, ); diff --git a/src/intermediate/blog_post_page.rs b/src/intermediate/blog_post_page.rs index dec8273..55a81fe 100644 --- a/src/intermediate/blog_post_page.rs +++ b/src/intermediate/blog_post_page.rs @@ -11,17 +11,23 @@ use super::ISection; #[derive(Debug)] pub(crate) struct BlogPostPageInput<'b, 'parse> { + /// Relative path from the root of the blog post. path: PathBuf, + + /// The path to the .org source for the file. + src: PathBuf, document: &'b organic::types::Document<'parse>, } impl<'b, 'parse> BlogPostPageInput<'b, 'parse> { - pub(crate) fn new>( + pub(crate) fn new, S: Into>( path: P, + src: S, document: &'b organic::types::Document<'parse>, ) -> BlogPostPageInput<'b, 'parse> { BlogPostPageInput { path: path.into(), + src: src.into(), document, } } @@ -32,6 +38,9 @@ pub(crate) struct BlogPostPage { /// Relative path from the root of the blog post. pub(crate) path: PathBuf, + /// The path to the .org source for the file. + pub(crate) src: PathBuf, + pub(crate) title: Option, pub(crate) date: Option, @@ -79,6 +88,7 @@ intermediate!( Ok(BlogPostPage { path: original.path, + src: original.src, title: get_title(original.document), date: get_date(original.document), children, diff --git a/src/intermediate/page.rs b/src/intermediate/page.rs index 45e042d..6c673c5 100644 --- a/src/intermediate/page.rs +++ b/src/intermediate/page.rs @@ -13,6 +13,9 @@ pub(crate) struct IPage { /// Relative path from the root of the pages directory. pub(crate) path: PathBuf, + /// The path to the .org source for the file. + pub(crate) src: PathBuf, + pub(crate) title: Option, #[allow(dead_code)] @@ -61,6 +64,7 @@ intermediate!( Ok(IPage { path: original.path, + src: original.src, title: get_title(original.document), date: get_date(original.document), children, @@ -80,17 +84,23 @@ impl IPage { #[derive(Debug)] pub(crate) struct PageInput<'b, 'parse> { + /// Relative path from the root of the page. path: PathBuf, + + /// The path to the .org source for the file. + src: PathBuf, document: &'b organic::types::Document<'parse>, } impl<'b, 'parse> PageInput<'b, 'parse> { - pub(crate) fn new>( + pub(crate) fn new, S: Into>( path: P, + src: S, document: &'b organic::types::Document<'parse>, ) -> PageInput<'b, 'parse> { PageInput { path: path.into(), + src: src.into(), document, } } From 3867f965d22db73940e724f02327cf066fb31e2d Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 17:27:20 -0500 Subject: [PATCH 07/13] Add a dependency manager for render-time actions. This will be used for supporting things like copying static files or rendering code blocks like gnuplot or graphviz. --- rust-toolchain.toml | 4 ++++ src/command/build/render.rs | 13 +++++++++++++ src/context/dependency_manager.rs | 19 +++++++++++++++++++ src/context/mod.rs | 2 ++ src/context/render_context.rs | 11 +++++++++++ 5 files changed, 49 insertions(+) create mode 100644 rust-toolchain.toml create mode 100644 src/context/dependency_manager.rs diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..a5f9492 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly" +profile = "default" +components = ["clippy", "rustfmt"] diff --git a/src/command/build/render.rs b/src/command/build/render.rs index 5108ccd..7ec4410 100644 --- a/src/command/build/render.rs +++ b/src/command/build/render.rs @@ -7,6 +7,7 @@ use tokio::fs::DirEntry; use tokio::task::JoinHandle; use crate::config::Config; +use crate::context::DependencyManager; use crate::context::RenderBlogPostPage; use crate::context::RenderBlogPostPageInput; use crate::context::RenderBlogStream; @@ -85,11 +86,14 @@ impl SiteRenderer { for page in &self.pages { let output_path = self.output_directory.join(page.get_output_path()); + let dependency_manager = + std::sync::Arc::new(std::sync::Mutex::new(DependencyManager::new())); let render_context = RenderContext::new( config, self.output_directory.as_path(), output_path.as_path(), None, + dependency_manager, )?; let render_context = RenderPage::new(render_context, page)?; let rendered_output = renderer_integration.render(render_context)?; @@ -113,12 +117,15 @@ impl SiteRenderer { .join(config.get_relative_path_to_post(&blog_post.id)) .join(blog_post_page.get_output_path()); + let dependency_manager = + std::sync::Arc::new(std::sync::Mutex::new(DependencyManager::new())); let convert_input = RenderBlogPostPageInput::new(blog_post, blog_post_page); let render_context = RenderContext::new( config, self.output_directory.as_path(), output_path.as_path(), None, + dependency_manager, )?; let render_context = RenderBlogPostPage::new(render_context, &convert_input)?; let rendered_output = renderer_integration.render(render_context)?; @@ -127,6 +134,9 @@ impl SiteRenderer { .ok_or("Output file should have a containing directory.")?; tokio::fs::create_dir_all(parent_directory).await?; tokio::fs::write(output_path, rendered_output).await?; + + // TODO: Copy post files to output. + // TODO: Update link src to generate path correct for where the page is rendered. } } @@ -194,12 +204,15 @@ impl SiteRenderer { )?) }; + let dependency_manager = + std::sync::Arc::new(std::sync::Mutex::new(DependencyManager::new())); let convert_input = RenderBlogStreamInput::new(chunk, older_link, newer_link); let render_context = RenderContext::new( config, self.output_directory.as_path(), output_file.as_path(), None, + dependency_manager, )?; let blog_stream = RenderBlogStream::new(render_context, &convert_input)?; diff --git a/src/context/dependency_manager.rs b/src/context/dependency_manager.rs new file mode 100644 index 0000000..0e32026 --- /dev/null +++ b/src/context/dependency_manager.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +pub(crate) type RefDependencyManager = std::sync::Arc>; + +#[derive(Debug)] +pub(crate) struct DependencyManager { + /// A stack of paths for the files being visited. + /// + /// The last entry is the current file being processed. This can be used for handling relative-path links. + file_stack: Vec, +} + +impl DependencyManager { + pub(crate) fn new() -> Self { + DependencyManager { + file_stack: Vec::new(), + } + } +} diff --git a/src/context/mod.rs b/src/context/mod.rs index cea8689..997ce1c 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -11,6 +11,7 @@ mod clock; mod code; mod comment; mod comment_block; +mod dependency_manager; mod diary_sexp; mod document_element; mod drawer; @@ -72,6 +73,7 @@ pub(crate) use blog_post_page::RenderBlogPostPage; pub(crate) use blog_post_page::RenderBlogPostPageInput; pub(crate) use blog_stream::RenderBlogStream; pub(crate) use blog_stream::RenderBlogStreamInput; +pub(crate) use dependency_manager::DependencyManager; pub(crate) use document_element::RenderDocumentElement; pub(crate) use element::RenderElement; pub(crate) use footnote_definition::RenderRealFootnoteDefinition; diff --git a/src/context/render_context.rs b/src/context/render_context.rs index 7cb9b0b..122b415 100644 --- a/src/context/render_context.rs +++ b/src/context/render_context.rs @@ -3,6 +3,8 @@ use std::path::Path; use crate::config::Config; use crate::error::CustomError; +use super::dependency_manager::RefDependencyManager; + /// The supporting information used for converting the intermediate representation into the dust context for rendering. #[derive(Debug, Clone)] pub(crate) struct RenderContext<'intermediate> { @@ -16,6 +18,13 @@ pub(crate) struct RenderContext<'intermediate> { /// IDs, for example, multiple blog posts with footnotes in a blog /// stream. pub(crate) id_addition: Option<&'intermediate str>, + + /// Tracks dependencies from rendering Org document(s). + /// + /// Examples of dependencies would be: + /// - Static files that need to be copied to the output folder + /// - Code blocks that need to be executed (for example, gnuplot graphs) + pub(crate) dependency_manager: RefDependencyManager, } impl<'intermediate> RenderContext<'intermediate> { @@ -24,12 +33,14 @@ impl<'intermediate> RenderContext<'intermediate> { output_directory: &'intermediate Path, output_file: &'intermediate Path, id_addition: Option<&'intermediate str>, + dependency_manager: RefDependencyManager, ) -> Result, CustomError> { Ok(RenderContext { config, output_root_directory: output_directory, output_file, id_addition, + dependency_manager, }) } } From 4e0f66401d2dfae6d8968f885d8a77e94232b4b8 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 18:01:59 -0500 Subject: [PATCH 08/13] Update the dependency manager file stack when rendering blog post pages. --- src/context/blog_post_page.rs | 130 +++++++++++++++++------------- src/context/dependency_manager.rs | 17 ++++ 2 files changed, 89 insertions(+), 58 deletions(-) diff --git a/src/context/blog_post_page.rs b/src/context/blog_post_page.rs index 794b11d..2a764b3 100644 --- a/src/context/blog_post_page.rs +++ b/src/context/blog_post_page.rs @@ -49,76 +49,90 @@ render!( original, render_context, { - let css_files = vec![ - get_web_path( + render_context + .dependency_manager + .lock() + .unwrap() + .push_file(&original.page.src)?; + let ret = (|| { + let css_files = vec![ + get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "stylesheet/reset.css", + )?, + get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "stylesheet/main.css", + )?, + ]; + let js_files = vec![get_web_path( render_context.config, render_context.output_root_directory, render_context.output_file, - "stylesheet/reset.css", - )?, - get_web_path( + "blog_post.js", + )?]; + let global_settings = + GlobalSettings::new(original.page.title.clone(), css_files, js_files); + let page_header = PageHeader::new( + render_context.config.get_site_title().map(str::to_string), + Some(get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "", + )?), + ); + let link_to_blog_post = get_web_path( render_context.config, render_context.output_root_directory, render_context.output_file, - "stylesheet/main.css", - )?, - ]; - let js_files = vec![get_web_path( - render_context.config, - render_context.output_root_directory, - render_context.output_file, - "blog_post.js", - )?]; - let global_settings = GlobalSettings::new(original.page.title.clone(), css_files, js_files); - let page_header = PageHeader::new( - render_context.config.get_site_title().map(str::to_string), - Some(get_web_path( - render_context.config, - render_context.output_root_directory, - render_context.output_file, - "", - )?), - ); - let link_to_blog_post = get_web_path( - render_context.config, - render_context.output_root_directory, - render_context.output_file, - render_context - .output_file - .strip_prefix(render_context.output_root_directory)?, - )?; + render_context + .output_file + .strip_prefix(render_context.output_root_directory)?, + )?; - let children = { - let mut children = Vec::new(); + let children = { + let mut children = Vec::new(); - for child in original.page.children.iter() { - children.push(RenderDocumentElement::new(render_context.clone(), child)?); - } + for child in original.page.children.iter() { + children.push(RenderDocumentElement::new(render_context.clone(), child)?); + } - children - }; + children + }; - let footnotes = { - let mut ret = Vec::new(); + let footnotes = { + let mut ret = Vec::new(); - for footnote in original.page.footnotes.iter() { - ret.push(RenderRealFootnoteDefinition::new( - render_context.clone(), - footnote, - )?); - } + for footnote in original.page.footnotes.iter() { + ret.push(RenderRealFootnoteDefinition::new( + render_context.clone(), + footnote, + )?); + } - ret - }; + ret + }; - let ret = RenderBlogPostPage { - global_settings, - page_header: Some(page_header), - title: original.page.title.clone(), - self_link: Some(link_to_blog_post), - children, - footnotes, - }; - Ok(ret) + let ret = RenderBlogPostPage { + global_settings, + page_header: Some(page_header), + title: original.page.title.clone(), + self_link: Some(link_to_blog_post), + children, + footnotes, + }; + Ok(ret) + })(); + render_context + .dependency_manager + .lock() + .unwrap() + .pop_file()?; + ret } ); diff --git a/src/context/dependency_manager.rs b/src/context/dependency_manager.rs index 0e32026..b685668 100644 --- a/src/context/dependency_manager.rs +++ b/src/context/dependency_manager.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use crate::error::CustomError; + pub(crate) type RefDependencyManager = std::sync::Arc>; #[derive(Debug)] @@ -16,4 +18,19 @@ impl DependencyManager { file_stack: Vec::new(), } } + + pub(crate) fn push_file

(&mut self, path: P) -> Result<(), CustomError> + where + P: Into, + { + self.file_stack.push(path.into()); + Ok(()) + } + + pub(crate) fn pop_file(&mut self) -> Result<(), CustomError> { + self.file_stack + .pop() + .expect("Popped more files off the dependency manager file stack than exist."); + Ok(()) + } } From 3e952ef0f4484cbb8b47446d7229853b681b1e30 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 18:13:55 -0500 Subject: [PATCH 09/13] Implement a macro for pushing the directory. --- src/context/blog_post_page.rs | 16 +++------------- src/context/macros.rs | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/context/blog_post_page.rs b/src/context/blog_post_page.rs index 2a764b3..9dce558 100644 --- a/src/context/blog_post_page.rs +++ b/src/context/blog_post_page.rs @@ -1,6 +1,7 @@ use serde::Serialize; use super::render_context::RenderContext; +use crate::context::macros::push_file; use crate::error::CustomError; use crate::intermediate::get_web_path; use crate::intermediate::BlogPost; @@ -49,12 +50,7 @@ render!( original, render_context, { - render_context - .dependency_manager - .lock() - .unwrap() - .push_file(&original.page.src)?; - let ret = (|| { + push_file!(render_context, &original.page.src, { let css_files = vec![ get_web_path( render_context.config, @@ -127,12 +123,6 @@ render!( footnotes, }; Ok(ret) - })(); - render_context - .dependency_manager - .lock() - .unwrap() - .pop_file()?; - ret + }) } ); diff --git a/src/context/macros.rs b/src/context/macros.rs index 9635f8e..e4a3431 100644 --- a/src/context/macros.rs +++ b/src/context/macros.rs @@ -35,3 +35,23 @@ macro_rules! rnoop { } pub(crate) use rnoop; + +/// Push a file onto the render DependencyManager's file stack while inside the code block. +macro_rules! push_file { + ($render_context:ident, $path:expr, $body:tt) => {{ + $render_context + .dependency_manager + .lock() + .unwrap() + .push_file($path)?; + let ret = (|| $body)(); + $render_context + .dependency_manager + .lock() + .unwrap() + .pop_file()?; + ret + }}; +} + +pub(crate) use push_file; From ff478253c3de90a2692f247baab74ae9a41db45d Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 18:59:45 -0500 Subject: [PATCH 10/13] Generate render link based on path to file. --- src/context/blog_stream.rs | 45 +++++------ src/context/dependency_manager.rs | 10 +++ src/context/page.rs | 119 +++++++++++++++--------------- src/intermediate/regular_link.rs | 24 +++++- 4 files changed, 118 insertions(+), 80 deletions(-) diff --git a/src/context/blog_stream.rs b/src/context/blog_stream.rs index 342fdde..fac3606 100644 --- a/src/context/blog_stream.rs +++ b/src/context/blog_stream.rs @@ -2,6 +2,7 @@ use serde::Serialize; use super::macros::render; use super::render_context::RenderContext; +use crate::context::macros::push_file; use crate::context::RenderDocumentElement; use crate::context::RenderRealFootnoteDefinition; use crate::error::CustomError; @@ -164,32 +165,34 @@ render!( .get_index_page() .ok_or_else(|| format!("Blog post {} needs an index page.", original.original.id))?; - let title = index_page.title.clone(); + push_file!(render_context, &index_page.src, { + let title = index_page.title.clone(); - let children = index_page - .children - .iter() - .map(|child| RenderDocumentElement::new(render_context.clone(), child)) - .collect::, _>>()?; + let children = index_page + .children + .iter() + .map(|child| RenderDocumentElement::new(render_context.clone(), child)) + .collect::, _>>()?; - let footnotes = { - let mut ret = Vec::new(); + let footnotes = { + let mut ret = Vec::new(); - for footnote in index_page.footnotes.iter() { - ret.push(RenderRealFootnoteDefinition::new( - render_context.clone(), - footnote, - )?); - } + for footnote in index_page.footnotes.iter() { + ret.push(RenderRealFootnoteDefinition::new( + render_context.clone(), + footnote, + )?); + } - ret - }; + ret + }; - Ok(RenderBlogStreamEntry { - title, - self_link: Some(link_to_blog_post), - children, - footnotes, + Ok(RenderBlogStreamEntry { + title, + self_link: Some(link_to_blog_post), + children, + footnotes, + }) }) } ); diff --git a/src/context/dependency_manager.rs b/src/context/dependency_manager.rs index b685668..6553129 100644 --- a/src/context/dependency_manager.rs +++ b/src/context/dependency_manager.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::path::PathBuf; use crate::error::CustomError; @@ -33,4 +34,13 @@ impl DependencyManager { .expect("Popped more files off the dependency manager file stack than exist."); Ok(()) } + + pub(crate) fn get_current_folder(&self) -> Result<&Path, CustomError> { + Ok(self + .file_stack + .last() + .ok_or("No current file")? + .parent() + .ok_or("Current file was not in a directory")?) + } } diff --git a/src/context/page.rs b/src/context/page.rs index d87d91e..cd8e5f0 100644 --- a/src/context/page.rs +++ b/src/context/page.rs @@ -4,6 +4,7 @@ use super::render_context::RenderContext; use super::GlobalSettings; use super::PageHeader; use super::RenderDocumentElement; +use crate::context::macros::push_file; use crate::error::CustomError; use crate::intermediate::get_web_path; use crate::intermediate::IPage; @@ -28,75 +29,77 @@ pub(crate) struct RenderPage { } render!(RenderPage, IPage, original, render_context, { - let css_files = vec![ - get_web_path( + push_file!(render_context, &original.src, { + let css_files = vec![ + get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "stylesheet/reset.css", + )?, + get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "stylesheet/main.css", + )?, + ]; + let js_files = vec![get_web_path( render_context.config, render_context.output_root_directory, render_context.output_file, - "stylesheet/reset.css", - )?, - get_web_path( + "blog_post.js", + )?]; + let global_settings = GlobalSettings::new(original.title.clone(), css_files, js_files); + let page_header = PageHeader::new( + render_context.config.get_site_title().map(str::to_string), + Some(get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + "", + )?), + ); + let link_to_blog_post = get_web_path( render_context.config, render_context.output_root_directory, render_context.output_file, - "stylesheet/main.css", - )?, - ]; - let js_files = vec![get_web_path( - render_context.config, - render_context.output_root_directory, - render_context.output_file, - "blog_post.js", - )?]; - let global_settings = GlobalSettings::new(original.title.clone(), css_files, js_files); - let page_header = PageHeader::new( - render_context.config.get_site_title().map(str::to_string), - Some(get_web_path( - render_context.config, - render_context.output_root_directory, - render_context.output_file, - "", - )?), - ); - let link_to_blog_post = get_web_path( - render_context.config, - render_context.output_root_directory, - render_context.output_file, - render_context - .output_file - .strip_prefix(render_context.output_root_directory)?, - )?; + render_context + .output_file + .strip_prefix(render_context.output_root_directory)?, + )?; - let children = { - let mut children = Vec::new(); + let children = { + let mut children = Vec::new(); - for child in original.children.iter() { - children.push(RenderDocumentElement::new(render_context.clone(), child)?); - } + for child in original.children.iter() { + children.push(RenderDocumentElement::new(render_context.clone(), child)?); + } - children - }; + children + }; - let footnotes = { - let mut ret = Vec::new(); + let footnotes = { + let mut ret = Vec::new(); - for footnote in original.footnotes.iter() { - ret.push(RenderRealFootnoteDefinition::new( - render_context.clone(), - footnote, - )?); - } + for footnote in original.footnotes.iter() { + ret.push(RenderRealFootnoteDefinition::new( + render_context.clone(), + footnote, + )?); + } - ret - }; + ret + }; - let ret = RenderPage { - global_settings, - page_header: Some(page_header), - title: original.title.clone(), - self_link: Some(link_to_blog_post), - children, - footnotes, - }; - Ok(ret) + let ret = RenderPage { + global_settings, + page_header: Some(page_header), + title: original.title.clone(), + self_link: Some(link_to_blog_post), + children, + footnotes, + }; + Ok(ret) + }) }); diff --git a/src/intermediate/regular_link.rs b/src/intermediate/regular_link.rs index b9fce81..e36577a 100644 --- a/src/intermediate/regular_link.rs +++ b/src/intermediate/regular_link.rs @@ -143,7 +143,29 @@ impl LinkTarget { .unwrap_or_default(), target_id ))), - LinkTarget::Image { src, .. } => Ok(Some(src.clone())), + LinkTarget::Image { src, .. } => { + let path_to_file = render_context + .dependency_manager + .lock() + .unwrap() + .get_current_folder()? + .join(src) + .canonicalize()?; + let input_root_directory = render_context.config.get_root_directory(); + let relative_path_to_file = path_to_file.strip_prefix(input_root_directory)?; + dbg!(input_root_directory); + dbg!(&path_to_file); + dbg!(relative_path_to_file); + let web_path = get_web_path( + render_context.config, + render_context.output_root_directory, + render_context.output_file, + relative_path_to_file, + )?; + dbg!(&web_path); + // TODO: Record interest in copying the file to output. + Ok(Some(web_path)) + } } } From bf7f37260c7da80efab6e27e62d1d3c4b79a1dee Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 19:23:19 -0500 Subject: [PATCH 11/13] Mark the image files for copying. --- src/context/dependency_manager.rs | 15 +++++++++++++++ src/context/mod.rs | 1 + src/intermediate/regular_link.rs | 10 +++++----- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/context/dependency_manager.rs b/src/context/dependency_manager.rs index 6553129..0147a7b 100644 --- a/src/context/dependency_manager.rs +++ b/src/context/dependency_manager.rs @@ -3,6 +3,8 @@ use std::path::PathBuf; use crate::error::CustomError; +use super::dependency::Dependency; + pub(crate) type RefDependencyManager = std::sync::Arc>; #[derive(Debug)] @@ -11,12 +13,15 @@ pub(crate) struct DependencyManager { /// /// The last entry is the current file being processed. This can be used for handling relative-path links. file_stack: Vec, + + dependencies: Vec, } impl DependencyManager { pub(crate) fn new() -> Self { DependencyManager { file_stack: Vec::new(), + dependencies: Vec::new(), } } @@ -43,4 +48,14 @@ impl DependencyManager { .parent() .ok_or("Current file was not in a directory")?) } + + pub(crate) fn mark_file_for_copying

(&mut self, path: P) -> Result<(), CustomError> + where + P: Into, + { + self.dependencies.push(Dependency::StaticFile { + absolute_path: path.into(), + }); + Ok(()) + } } diff --git a/src/context/mod.rs b/src/context/mod.rs index 997ce1c..41be3c4 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -11,6 +11,7 @@ mod clock; mod code; mod comment; mod comment_block; +mod dependency; mod dependency_manager; mod diary_sexp; mod document_element; diff --git a/src/intermediate/regular_link.rs b/src/intermediate/regular_link.rs index e36577a..2eafdb2 100644 --- a/src/intermediate/regular_link.rs +++ b/src/intermediate/regular_link.rs @@ -153,17 +153,17 @@ impl LinkTarget { .canonicalize()?; let input_root_directory = render_context.config.get_root_directory(); let relative_path_to_file = path_to_file.strip_prefix(input_root_directory)?; - dbg!(input_root_directory); - dbg!(&path_to_file); - dbg!(relative_path_to_file); let web_path = get_web_path( render_context.config, render_context.output_root_directory, render_context.output_file, relative_path_to_file, )?; - dbg!(&web_path); - // TODO: Record interest in copying the file to output. + let path_to_file = render_context + .dependency_manager + .lock() + .unwrap() + .mark_file_for_copying(path_to_file)?; Ok(Some(web_path)) } } From 59ee13345e9549c4afeffde0232335ab89ddf515 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 19:46:46 -0500 Subject: [PATCH 12/13] Copy the images into the output. --- src/command/build/render.rs | 38 ++++++++++++++++++++----------- src/context/dependency.rs | 36 +++++++++++++++++++++++++++++ src/context/dependency_manager.rs | 7 ++++++ 3 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 src/context/dependency.rs diff --git a/src/command/build/render.rs b/src/command/build/render.rs index 7ec4410..a5420cb 100644 --- a/src/command/build/render.rs +++ b/src/command/build/render.rs @@ -93,15 +93,20 @@ impl SiteRenderer { self.output_directory.as_path(), output_path.as_path(), None, - dependency_manager, + dependency_manager.clone(), )?; - let render_context = RenderPage::new(render_context, page)?; - let rendered_output = renderer_integration.render(render_context)?; + let dust_context = RenderPage::new(render_context.clone(), page)?; + let rendered_output = renderer_integration.render(dust_context)?; let parent_directory = output_path .parent() .ok_or("Output file should have a containing directory.")?; tokio::fs::create_dir_all(parent_directory).await?; - tokio::fs::write(output_path, rendered_output).await?; + tokio::fs::write(&output_path, rendered_output).await?; + + let dependencies = dependency_manager.lock().unwrap().take_dependencies(); + for dependency in dependencies { + dependency.perform(render_context.clone()).await?; + } } Ok(()) @@ -125,18 +130,20 @@ impl SiteRenderer { self.output_directory.as_path(), output_path.as_path(), None, - dependency_manager, + dependency_manager.clone(), )?; - let render_context = RenderBlogPostPage::new(render_context, &convert_input)?; - let rendered_output = renderer_integration.render(render_context)?; + let dust_context = RenderBlogPostPage::new(render_context.clone(), &convert_input)?; + let rendered_output = renderer_integration.render(dust_context)?; let parent_directory = output_path .parent() .ok_or("Output file should have a containing directory.")?; tokio::fs::create_dir_all(parent_directory).await?; - tokio::fs::write(output_path, rendered_output).await?; + tokio::fs::write(&output_path, rendered_output).await?; - // TODO: Copy post files to output. - // TODO: Update link src to generate path correct for where the page is rendered. + let dependencies = dependency_manager.lock().unwrap().take_dependencies(); + for dependency in dependencies { + dependency.perform(render_context.clone()).await?; + } } } @@ -212,9 +219,9 @@ impl SiteRenderer { self.output_directory.as_path(), output_file.as_path(), None, - dependency_manager, + dependency_manager.clone(), )?; - let blog_stream = RenderBlogStream::new(render_context, &convert_input)?; + let blog_stream = RenderBlogStream::new(render_context.clone(), &convert_input)?; // Pass each RenderBlogStream to dust as the context to render index.html and any additional stream pages. let rendered_output = renderer_integration.render(blog_stream)?; @@ -222,7 +229,12 @@ impl SiteRenderer { .parent() .ok_or("Output file should have a containing directory.")?; tokio::fs::create_dir_all(parent_directory).await?; - tokio::fs::write(output_file, rendered_output).await?; + tokio::fs::write(&output_file, rendered_output).await?; + + let dependencies = dependency_manager.lock().unwrap().take_dependencies(); + for dependency in dependencies { + dependency.perform(render_context.clone()).await?; + } } Ok(()) } diff --git a/src/context/dependency.rs b/src/context/dependency.rs new file mode 100644 index 0000000..aa4aab7 --- /dev/null +++ b/src/context/dependency.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use crate::error::CustomError; + +use super::RenderContext; + +#[derive(Debug)] +pub(crate) enum Dependency { + StaticFile { absolute_path: PathBuf }, +} + +impl Dependency { + pub(crate) async fn perform( + &self, + render_context: RenderContext<'_>, + ) -> Result<(), CustomError> { + match self { + Dependency::StaticFile { absolute_path } => { + let input_root_directory = render_context.config.get_root_directory(); + let relative_path_to_file = absolute_path.strip_prefix(input_root_directory)?; + let path_to_output = render_context + .output_root_directory + .join(relative_path_to_file); + tokio::fs::create_dir_all( + path_to_output + .parent() + .ok_or("Output file should have a containing directory.")?, + ) + .await?; + // TODO: If file already exists, either error out or compare hash to avoid duplicate write. + tokio::fs::copy(absolute_path, path_to_output).await?; + Ok(()) + } + } + } +} diff --git a/src/context/dependency_manager.rs b/src/context/dependency_manager.rs index 0147a7b..a5355b6 100644 --- a/src/context/dependency_manager.rs +++ b/src/context/dependency_manager.rs @@ -58,4 +58,11 @@ impl DependencyManager { }); Ok(()) } + + /// Return the dependencies and forget about them. + pub(crate) fn take_dependencies(&mut self) -> Vec { + let mut dependencies = Vec::new(); + std::mem::swap(&mut self.dependencies, &mut dependencies); + dependencies + } } From 4a0cbf3ba56d2f530a5805e5f63222d778899949 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 8 Feb 2025 20:06:09 -0500 Subject: [PATCH 13/13] Do not copy a file if it already exists. --- src/context/dependency.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/context/dependency.rs b/src/context/dependency.rs index aa4aab7..e79063f 100644 --- a/src/context/dependency.rs +++ b/src/context/dependency.rs @@ -27,8 +27,17 @@ impl Dependency { .ok_or("Output file should have a containing directory.")?, ) .await?; - // TODO: If file already exists, either error out or compare hash to avoid duplicate write. - tokio::fs::copy(absolute_path, path_to_output).await?; + + if tokio::fs::metadata(&path_to_output).await.is_ok() { + // TODO: compare hash and error out if they do not match. + println!( + "Not copying {} to {} because the output file already exists.", + absolute_path.display(), + path_to_output.display() + ); + } else { + tokio::fs::copy(absolute_path, path_to_output).await?; + } Ok(()) } }