,
+}
+
+impl DependencyManager {
+ pub(crate) fn new() -> Self {
+ DependencyManager {
+ file_stack: Vec::new(),
+ dependencies: 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(())
+ }
+
+ 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")?)
+ }
+
+ 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(())
+ }
+
+ /// 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
+ }
+}
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;
diff --git a/src/context/mod.rs b/src/context/mod.rs
index cea8689..41be3c4 100644
--- a/src/context/mod.rs
+++ b/src/context/mod.rs
@@ -11,6 +11,8 @@ mod clock;
mod code;
mod comment;
mod comment_block;
+mod dependency;
+mod dependency_manager;
mod diary_sexp;
mod document_element;
mod drawer;
@@ -72,6 +74,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/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/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/context/render_context.rs b/src/context/render_context.rs
index 18ad7d0..122b415 100644
--- a/src/context/render_context.rs
+++ b/src/context/render_context.rs
@@ -3,11 +3,12 @@ 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> {
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,
@@ -17,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> {
@@ -25,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,
})
}
}
diff --git a/src/intermediate/blog_post.rs b/src/intermediate/blog_post.rs
index adb8ac2..246959a 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() {
@@ -73,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/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;
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,
}
}
diff --git a/src/intermediate/regular_link.rs b/src/intermediate/regular_link.rs
index fe0d19b..2eafdb2 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,
@@ -42,7 +49,7 @@ intermediate!(
}
);
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub(crate) enum LinkTarget {
Raw(String),
Post {
@@ -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,6 +143,170 @@ impl LinkTarget {
.unwrap_or_default(),
target_id
))),
+ 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)?;
+ let web_path = get_web_path(
+ render_context.config,
+ render_context.output_root_directory,
+ render_context.output_file,
+ relative_path_to_file,
+ )?;
+ let path_to_file = render_context
+ .dependency_manager
+ .lock()
+ .unwrap()
+ .mark_file_for_copying(path_to_file)?;
+ Ok(Some(web_path))
+ }
+ }
+ }
+
+ /// 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;
+
+ 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, typ) in [(
+ "https://test.example/foo",
+ LinkType::Protocol(Cow::from("https")),
+ )] {
+ assert_eq!(
+ 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(())
+ }
+}
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;