Merge branch 'render_images'
This commit is contained in:
		
						commit
						79c36476bd
					
				| @ -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} | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -0,0 +1 @@ | ||||
| <img src="{.src}" alt="{.alt}" /> | ||||
							
								
								
									
										22
									
								
								org_test_documents/regular_link/image_links.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								org_test_documents/regular_link/image_links.org
									
									
									
									
									
										Normal file
									
								
							| @ -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]] | ||||
							
								
								
									
										4
									
								
								rust-toolchain.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								rust-toolchain.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| [toolchain] | ||||
| channel = "nightly" | ||||
| profile = "default" | ||||
| components = ["clippy", "rustfmt"] | ||||
| @ -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,19 +86,27 @@ 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.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(()) | ||||
| @ -113,20 +122,28 @@ 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.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?; | ||||
| 
 | ||||
|                 let dependencies = dependency_manager.lock().unwrap().take_dependencies(); | ||||
|                 for dependency in dependencies { | ||||
|                     dependency.perform(render_context.clone()).await?; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -194,14 +211,17 @@ 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.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)?; | ||||
| @ -209,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(()) | ||||
|     } | ||||
|  | ||||
| @ -163,7 +163,7 @@ async fn load_pages(config: &Config) -> Result<Vec<IPage>, 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?, | ||||
|             ); | ||||
|  | ||||
| @ -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,76 +50,79 @@ render!( | ||||
|     original, | ||||
|     render_context, | ||||
|     { | ||||
|         let css_files = vec![ | ||||
|             get_web_path( | ||||
|         push_file!(render_context, &original.page.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.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) | ||||
|         }) | ||||
|     } | ||||
| ); | ||||
|  | ||||
| @ -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::<Result<Vec<_>, _>>()?; | ||||
|             let children = index_page | ||||
|                 .children | ||||
|                 .iter() | ||||
|                 .map(|child| RenderDocumentElement::new(render_context.clone(), child)) | ||||
|                 .collect::<Result<Vec<_>, _>>()?; | ||||
| 
 | ||||
|         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, | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| ); | ||||
|  | ||||
							
								
								
									
										45
									
								
								src/context/dependency.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/context/dependency.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| 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?; | ||||
| 
 | ||||
|                 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(()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								src/context/dependency_manager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/context/dependency_manager.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| use std::path::Path; | ||||
| use std::path::PathBuf; | ||||
| 
 | ||||
| use crate::error::CustomError; | ||||
| 
 | ||||
| use super::dependency::Dependency; | ||||
| 
 | ||||
| pub(crate) type RefDependencyManager = std::sync::Arc<std::sync::Mutex<DependencyManager>>; | ||||
| 
 | ||||
| #[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<PathBuf>, | ||||
| 
 | ||||
|     dependencies: Vec<Dependency>, | ||||
| } | ||||
| 
 | ||||
| impl DependencyManager { | ||||
|     pub(crate) fn new() -> Self { | ||||
|         DependencyManager { | ||||
|             file_stack: Vec::new(), | ||||
|             dependencies: Vec::new(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) fn push_file<P>(&mut self, path: P) -> Result<(), CustomError> | ||||
|     where | ||||
|         P: Into<PathBuf>, | ||||
|     { | ||||
|         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<P>(&mut self, path: P) -> Result<(), CustomError> | ||||
|     where | ||||
|         P: Into<PathBuf>, | ||||
|     { | ||||
|         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<Dependency> { | ||||
|         let mut dependencies = Vec::new(); | ||||
|         std::mem::swap(&mut self.dependencies, &mut dependencies); | ||||
|         dependencies | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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) | ||||
|     }) | ||||
| }); | ||||
|  | ||||
| @ -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<RenderObject>, | ||||
|     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) | ||||
| }); | ||||
|  | ||||
| @ -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<RenderContext<'intermediate>, CustomError> { | ||||
|         Ok(RenderContext { | ||||
|             config, | ||||
|             output_root_directory: output_directory, | ||||
|             output_file, | ||||
|             id_addition, | ||||
|             dependency_manager, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -35,6 +35,7 @@ impl BlogPost { | ||||
|         ) -> Result<BlogPost, CustomError> { | ||||
|             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?, | ||||
|                     ); | ||||
|  | ||||
| @ -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<P: Into<PathBuf>>( | ||||
|     pub(crate) fn new<P: Into<PathBuf>, S: Into<PathBuf>>( | ||||
|         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<String>, | ||||
| 
 | ||||
|     pub(crate) date: Option<String>, | ||||
| @ -79,6 +88,7 @@ intermediate!( | ||||
| 
 | ||||
|         Ok(BlogPostPage { | ||||
|             path: original.path, | ||||
|             src: original.src, | ||||
|             title: get_title(original.document), | ||||
|             date: get_date(original.document), | ||||
|             children, | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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<String>, | ||||
| 
 | ||||
|     #[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<P: Into<PathBuf>>( | ||||
|     pub(crate) fn new<P: Into<PathBuf>, S: Into<PathBuf>>( | ||||
|         path: P, | ||||
|         src: S, | ||||
|         document: &'b organic::types::Document<'parse>, | ||||
|     ) -> PageInput<'b, 'parse> { | ||||
|         PageInput { | ||||
|             path: path.into(), | ||||
|             src: src.into(), | ||||
|             document, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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<LinkTarget, CustomError> { | ||||
|         // 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<String, CustomError> { | ||||
|         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<String, CustomError> { | ||||
|         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(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| #![feature(let_chains)] | ||||
| #![feature(async_closure)] | ||||
| use std::process::ExitCode; | ||||
| 
 | ||||
| use clap::Parser; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Tom Alexander
						Tom Alexander