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(()) + } }