use crate::clone_ext::CloneExt;
use crate::error::InputStreamError;
use crate::filename::{NotePath, NotePathStr};
use crate::{config::LocalLinkKind, error::NoteError};
use html_escape;
use parking_lot::RwLock;
use parse_hyperlinks::parser::Link;
use parse_hyperlinks_extras::iterator_html::HtmlLinkInlineImage;
use percent_encoding::percent_decode_str;
use std::path::MAIN_SEPARATOR_STR;
use std::{
borrow::Cow,
collections::HashSet,
path::{Component, Path, PathBuf},
sync::Arc,
};
pub(crate) const HTML_EXT: &str = ".html";
const FORMAT_SEPARATOR: char = '?';
const FORMAT_ONLY_SORT_TAG: char = '#';
const FORMAT_COMPLETE_FILENAME: &str = "?";
const FORMAT_FROM_TO_SEPARATOR: char = ':';
fn assemble_link(
root_path: &Path,
docdir: &Path,
dest: &Path,
rewrite_rel_paths: bool,
rewrite_abs_paths: bool,
) -> Option<PathBuf> {
fn append(path: &mut PathBuf, append: &Path) {
for dir in append.components() {
match dir {
Component::ParentDir => {
if !path.pop() {
let path_is_relative = {
let mut c = path.components();
!(c.next() == Some(Component::RootDir)
|| c.next() == Some(Component::RootDir))
};
if path_is_relative {
path.push(Component::ParentDir.as_os_str());
} else {
path.clear();
break;
}
}
}
Component::Normal(c) => path.push(c),
_ => {}
}
}
}
let dest_is_relative = {
let mut c = dest.components();
!(c.next() == Some(Component::RootDir) || c.next() == Some(Component::RootDir))
};
debug_assert!(docdir.starts_with(root_path));
let mut link = match (rewrite_rel_paths, rewrite_abs_paths, dest_is_relative) {
(true, false, true) => {
let link = PathBuf::from(Component::RootDir.as_os_str());
link.join(docdir.strip_prefix(root_path).ok()?)
}
(true, true, true) => docdir.to_path_buf(),
(false, _, true) => PathBuf::new(),
(_, false, false) => PathBuf::from(Component::RootDir.as_os_str()),
(_, true, false) => root_path.to_path_buf(),
};
append(&mut link, dest);
if link.as_os_str().is_empty() {
None
} else {
Some(link)
}
}
trait Hyperlink {
fn decode_ampersand_and_percent(&mut self);
#[allow(clippy::ptr_arg)]
fn is_local_fn(value: &Cow<str>) -> bool;
fn strip_local_scheme(&mut self);
fn strip_scheme_fn(input: &mut Cow<str>);
fn is_autolink(&self) -> bool;
fn rebase_local_link(
&mut self,
root_path: &Path,
docdir: &Path,
rewrite_rel_paths: bool,
rewrite_abs_paths: bool,
) -> Result<(), NoteError>;
fn expand_shorthand_link(&mut self, prepend_path: Option<&Path>) -> Result<(), NoteError>;
fn rewrite_autolink(&mut self);
fn apply_format_attribute(&mut self);
fn get_local_link_dest_path(&self) -> Option<&Path>;
fn get_local_link_src_path(&self) -> Option<&Path>;
fn append_html_ext(&mut self);
fn to_html(&self) -> String;
}
impl Hyperlink for Link<'_> {
#[inline]
fn decode_ampersand_and_percent(&mut self) {
fn dec_amp(val: &mut Cow<str>) {
let decoded_text = html_escape::decode_html_entities(val);
if matches!(&decoded_text, Cow::Owned(..)) {
let decoded_text = Cow::Owned(decoded_text.into_owned());
let _ = std::mem::replace(val, decoded_text);
}
}
fn dec_amp_percent(val: &mut Cow<str>) {
dec_amp(val);
let decoded_dest = percent_decode_str(val.as_ref()).decode_utf8().unwrap();
if matches!(&decoded_dest, Cow::Owned(..)) {
let decoded_dest = Cow::Owned(decoded_dest.into_owned());
let _ = std::mem::replace(val, decoded_dest);
}
}
match self {
Link::Text2Dest(text1, dest, title) => {
dec_amp(text1);
dec_amp_percent(dest);
dec_amp(title);
}
Link::Image(alt, src) => {
dec_amp(alt);
dec_amp_percent(src);
}
Link::Image2Dest(text1, alt, src, text2, dest, title) => {
dec_amp(text1);
dec_amp(alt);
dec_amp_percent(src);
dec_amp(text2);
dec_amp_percent(dest);
dec_amp(title);
}
_ => unimplemented!(),
};
}
fn is_local_fn(dest: &Cow<str>) -> bool {
!((dest.contains("://") && !dest.contains(":///"))
|| dest.starts_with("mailto:")
|| dest.starts_with("tel:"))
}
fn strip_local_scheme(&mut self) {
fn strip(dest: &mut Cow<str>) {
if <Link<'_> as Hyperlink>::is_local_fn(dest) {
<Link<'_> as Hyperlink>::strip_scheme_fn(dest);
}
}
match self {
Link::Text2Dest(_, dest, _title) => strip(dest),
Link::Image2Dest(_, _, src, _, dest, _) => {
strip(src);
strip(dest);
}
Link::Image(_, src) => strip(src),
_ => {}
};
}
fn strip_scheme_fn(inout: &mut Cow<str>) {
let output = inout
.trim_start_matches("https://")
.trim_start_matches("https:")
.trim_start_matches("http://")
.trim_start_matches("http:")
.trim_start_matches("tpnote:")
.trim_start_matches("mailto:")
.trim_start_matches("tel:");
if output != inout.as_ref() {
let _ = std::mem::replace(inout, Cow::Owned(output.to_string()));
}
}
fn is_autolink(&self) -> bool {
let (text, dest) = match self {
Link::Text2Dest(text, dest, _title) => (text, dest),
Link::Image(alt, source) => (alt, source),
_ => return false,
};
text == dest
}
fn rebase_local_link(
&mut self,
root_path: &Path,
docdir: &Path,
rewrite_rel_paths: bool,
rewrite_abs_paths: bool,
) -> Result<(), NoteError> {
let do_rebase = |path: &mut Cow<str>| -> Result<(), NoteError> {
if <Link as Hyperlink>::is_local_fn(path) {
let dest_out = assemble_link(
root_path,
docdir,
Path::new(path.as_ref()),
rewrite_rel_paths,
rewrite_abs_paths,
)
.ok_or(NoteError::InvalidLocalPath {
path: path.as_ref().to_string(),
})?;
let new_dest = Cow::Owned(dest_out.to_str().unwrap_or_default().to_string());
let _ = std::mem::replace(path, new_dest);
}
Ok(())
};
match self {
Link::Text2Dest(_, dest, _) => do_rebase(dest),
Link::Image2Dest(_, _, src, _, dest, _) => do_rebase(src).and_then(|_| do_rebase(dest)),
Link::Image(_, src) => do_rebase(src),
_ => unimplemented!(),
}
}
fn expand_shorthand_link(&mut self, prepend_path: Option<&Path>) -> Result<(), NoteError> {
let shorthand_link = match self {
Link::Text2Dest(_, dest, _) => dest,
Link::Image2Dest(_, _, _, _, dest, _) => dest,
_ => return Ok(()),
};
if !<Link as Hyperlink>::is_local_fn(shorthand_link) {
return Ok(());
}
let (shorthand_str, shorthand_format) = match shorthand_link.split_once(FORMAT_SEPARATOR) {
Some((path, fmt)) => (path, Some(fmt)),
None => (shorthand_link.as_ref(), None),
};
let shorthand_path = Path::new(shorthand_str);
if let Some(sort_tag) = shorthand_str.is_valid_sort_tag() {
let full_shorthand_path = if let Some(root_path) = prepend_path {
let shorthand_path = shorthand_path
.strip_prefix(MAIN_SEPARATOR_STR)
.unwrap_or(shorthand_path);
Cow::Owned(root_path.join(shorthand_path))
} else {
Cow::Borrowed(shorthand_path)
};
let found = full_shorthand_path
.parent()
.and_then(|dir| dir.find_file_with_sort_tag(sort_tag));
if let Some(path) = found {
let found_link = path
.strip_prefix(prepend_path.unwrap_or(Path::new("")))
.unwrap();
let mut found_link = Path::new(MAIN_SEPARATOR_STR)
.join(found_link)
.to_str()
.unwrap_or_default()
.to_string();
if let Some(fmt) = shorthand_format {
found_link.push(FORMAT_SEPARATOR);
found_link.push_str(fmt);
}
let _ = std::mem::replace(shorthand_link, Cow::Owned(found_link));
} else {
return Err(NoteError::CanNotExpandShorthandLink {
path: full_shorthand_path.to_string_lossy().into_owned(),
});
}
}
Ok(())
}
fn rewrite_autolink(&mut self) {
let text = match self {
Link::Text2Dest(text, _, _) => text,
Link::Image(alt, _) => alt,
_ => return,
};
<Link as Hyperlink>::strip_scheme_fn(text);
}
fn apply_format_attribute(&mut self) {
let (text, dest) = match self {
Link::Text2Dest(text, dest, _) => (text, dest),
Link::Image(alt, source) => (alt, source),
_ => return,
};
if !<Link as Hyperlink>::is_local_fn(dest) {
return;
}
let (path, format) = match dest.split_once(FORMAT_SEPARATOR) {
Some(s) => s,
None => return,
};
let mut short_text = Path::new(path)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let format = if format.starts_with(FORMAT_COMPLETE_FILENAME) {
format
.strip_prefix(FORMAT_COMPLETE_FILENAME)
.unwrap_or(format)
} else if format.starts_with(FORMAT_ONLY_SORT_TAG) {
short_text = Path::new(path).disassemble().0;
format.strip_prefix(FORMAT_ONLY_SORT_TAG).unwrap_or(format)
} else {
short_text = Path::new(path).disassemble().2;
format
};
match format.split_once(FORMAT_FROM_TO_SEPARATOR) {
None => {
if !format.is_empty() {
if let Some(idx) = short_text.find(format) {
short_text = &short_text[..idx];
};
}
}
Some((from, to)) => {
if !from.is_empty() {
if let Some(idx) = short_text.find(from) {
short_text = &short_text[(idx + from.len())..];
};
}
if !to.is_empty() {
if let Some(idx) = short_text.find(to) {
short_text = &short_text[..idx];
};
}
}
}
let _ = std::mem::replace(text, Cow::Owned(short_text.to_string()));
let _ = std::mem::replace(dest, Cow::Owned(path.to_string()));
}
fn get_local_link_dest_path(&self) -> Option<&Path> {
let dest = match self {
Link::Text2Dest(_, dest, _) => dest,
Link::Image2Dest(_, _, _, _, dest, _) => dest,
_ => return None,
};
if <Link as Hyperlink>::is_local_fn(dest) {
match (dest.rfind('#'), dest.rfind(['/', '\\'])) {
(Some(n), sep) if sep.is_some_and(|sep| n > sep) || sep.is_none() => {
Some(Path::new(&dest.as_ref()[..n]))
}
_ => Some(Path::new(dest.as_ref())),
}
} else {
None
}
}
fn get_local_link_src_path(&self) -> Option<&Path> {
let src = match self {
Link::Image2Dest(_, _, src, _, _, _) => src,
Link::Image(_, src) => src,
_ => return None,
};
if <Link as Hyperlink>::is_local_fn(src) {
Some(Path::new(src.as_ref()))
} else {
None
}
}
fn append_html_ext(&mut self) {
let dest = match self {
Link::Text2Dest(_, dest, _) => dest,
Link::Image2Dest(_, _, _, _, dest, _) => dest,
_ => return,
};
if <Link as Hyperlink>::is_local_fn(dest) {
let path = dest.as_ref();
if path.has_tpnote_ext() {
let mut newpath = path.to_string();
newpath.push_str(HTML_EXT);
let _ = std::mem::replace(dest, Cow::Owned(newpath));
}
}
}
fn to_html(&self) -> String {
fn enc_amp(val: Cow<str>) -> Cow<str> {
let s = html_escape::encode_double_quoted_attribute(val.as_ref());
if s == val {
val
} else {
Cow::Owned(s.into_owned())
}
}
fn repl_backspace_enc_amp(val: Cow<str>) -> Cow<str> {
let val = if val.as_ref().contains('\\') {
Cow::Owned(val.to_string().replace('\\', "/"))
} else {
val
};
let s = html_escape::encode_double_quoted_attribute(val.as_ref());
if s == val {
val
} else {
Cow::Owned(s.into_owned())
}
}
match self {
Link::Text2Dest(text, dest, title) => {
let title_html = if !title.is_empty() {
format!(" title=\"{}\"", enc_amp(title.shallow_clone()))
} else {
"".to_string()
};
format!(
"<a href=\"{}\"{}>{}</a>",
repl_backspace_enc_amp(dest.shallow_clone()),
title_html,
text
)
}
Link::Image2Dest(text1, alt, src, text2, dest, title) => {
let title_html = if !title.is_empty() {
format!(" title=\"{}\"", enc_amp(title.shallow_clone()))
} else {
"".to_string()
};
format!(
"<a href=\"{}\"{}>{}<img src=\"{}\" alt=\"{}\">{}</a>",
repl_backspace_enc_amp(dest.shallow_clone()),
title_html,
text1,
repl_backspace_enc_amp(src.shallow_clone()),
enc_amp(alt.shallow_clone()),
text2
)
}
Link::Image(alt, src) => {
format!(
"<img src=\"{}\" alt=\"{}\">",
repl_backspace_enc_amp(src.shallow_clone()),
enc_amp(alt.shallow_clone())
)
}
_ => unimplemented!(),
}
}
}
#[inline]
pub fn rewrite_links(
html_input: String,
root_path: &Path,
docdir: &Path,
local_link_kind: LocalLinkKind,
rewrite_ext: bool,
allowed_local_links: Arc<RwLock<HashSet<PathBuf>>>,
) -> String {
let (rewrite_rel_paths, rewrite_abs_paths) = match local_link_kind {
LocalLinkKind::Off => (false, false),
LocalLinkKind::Short => (true, false),
LocalLinkKind::Long => (true, true),
};
let mut rest = &*html_input;
let mut html_out = String::new();
for ((skipped, _consumed, remaining), mut link) in HtmlLinkInlineImage::new(&html_input) {
html_out.push_str(skipped);
rest = remaining;
let mut link_is_autolink = link.is_autolink();
link.decode_ampersand_and_percent();
link_is_autolink = link_is_autolink || link.is_autolink();
link.strip_local_scheme();
match link
.rebase_local_link(root_path, docdir, rewrite_rel_paths, rewrite_abs_paths)
.and_then(|_| {
link.expand_shorthand_link(
(matches!(local_link_kind, LocalLinkKind::Short)).then_some(root_path),
)
}) {
Ok(()) => {}
Err(e) => {
let e = e.to_string();
let e = html_escape::encode_text(&e);
html_out.push_str(&format!("<i>{}</i>", e));
continue;
}
};
if link_is_autolink {
link.rewrite_autolink();
}
link.apply_format_attribute();
if let Some(dest_path) = link.get_local_link_dest_path() {
allowed_local_links.write().insert(dest_path.to_path_buf());
};
if let Some(src_path) = link.get_local_link_src_path() {
allowed_local_links.write().insert(src_path.to_path_buf());
};
if rewrite_ext {
link.append_html_ext();
}
html_out.push_str(&link.to_html());
}
html_out.push_str(rest);
log::trace!(
"Viewer: referenced allowed local files: {}",
allowed_local_links
.read_recursive()
.iter()
.map(|p| {
let mut s = "\n '".to_string();
s.push_str(&p.display().to_string());
s
})
.collect::<String>()
);
html_out
}
pub trait HtmlStr {
const TAG_DOCTYPE_PAT: &'static str = "<!doctype";
const TAG_DOCTYPE_HTML_PAT: &'static str = "<!doctype html";
const TAG_DOCTYPE_HTML: &'static str = "<!DOCTYPE html>";
const START_TAG_HTML_PAT: &'static str = "<html";
const END_TAG_HTML: &'static str = "</html>";
fn is_empty_html(&self) -> bool;
fn is_empty_html2(html: &str) -> bool {
html.is_empty_html()
}
fn has_html_start_tag(&self) -> bool;
fn has_html_start_tag2(html: &str) -> bool {
html.has_html_start_tag()
}
fn is_html_unchecked(&self) -> bool;
}
impl HtmlStr for str {
fn is_empty_html(&self) -> bool {
if self.is_empty() {
return true;
}
let html = self
.trim_start()
.lines()
.next()
.map(|l| l.to_ascii_lowercase())
.unwrap_or_default();
html.as_str().starts_with(Self::TAG_DOCTYPE_HTML_PAT)
&& html.find('>').unwrap_or_default() == html.len()-1
}
fn has_html_start_tag(&self) -> bool {
let html = self
.trim_start()
.lines()
.next()
.map(|l| l.to_ascii_lowercase());
html.as_ref()
.is_some_and(|l| l.starts_with(Self::TAG_DOCTYPE_HTML_PAT))
}
fn is_html_unchecked(&self) -> bool {
let html = self
.trim_start()
.lines()
.next()
.map(|l| l.to_ascii_lowercase());
html.as_ref().is_some_and(|l| {
(l.starts_with(Self::TAG_DOCTYPE_HTML_PAT)
&& l[Self::TAG_DOCTYPE_HTML_PAT.len()..].contains('>'))
|| (l.starts_with(Self::START_TAG_HTML_PAT)
&& l[Self::START_TAG_HTML_PAT.len()..].contains('>'))
})
}
}
pub trait HtmlString: Sized {
fn prepend_html_start_tag(self) -> Result<Self, InputStreamError>;
}
impl HtmlString for String {
fn prepend_html_start_tag(self) -> Result<Self, InputStreamError> {
use crate::html::HtmlStr;
let html2 = self
.trim_start()
.lines()
.next()
.map(|l| l.to_ascii_lowercase())
.unwrap_or_default();
if html2.starts_with(<str as HtmlStr>::TAG_DOCTYPE_HTML_PAT) {
Ok(self)
} else if !html2.starts_with(<str as HtmlStr>::TAG_DOCTYPE_PAT) {
let mut html = self;
html.insert_str(0, <str as HtmlStr>::TAG_DOCTYPE_HTML);
Ok(html)
} else {
Err(InputStreamError::NonHtmlDoctype {
html: self.chars().take(25).collect::<String>(),
})
}
}
}
#[cfg(test)]
mod tests {
use crate::error::InputStreamError;
use crate::error::NoteError;
use crate::html::Hyperlink;
use crate::html::assemble_link;
use crate::html::rewrite_links;
use parking_lot::RwLock;
use parse_hyperlinks::parser::Link;
use parse_hyperlinks_extras::parser::parse_html::take_link;
use std::borrow::Cow;
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
};
#[test]
fn test_assemble_link() {
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("../local/link to/note.md"),
true,
false,
)
.unwrap();
assert_eq!(output, Path::new("/doc/local/link to/note.md"));
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("../local/link to/note.md"),
false,
false,
)
.unwrap();
assert_eq!(output, Path::new("../local/link to/note.md"));
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("/test/../abs/local/link to/note.md"),
false,
false,
)
.unwrap();
assert_eq!(output, Path::new("/abs/local/link to/note.md"));
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("/../local/link to/note.md"),
false,
false,
);
assert_eq!(output, None);
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("/abs/local/link to/note.md"),
false,
true,
)
.unwrap();
assert_eq!(output, Path::new("/my/abs/local/link to/note.md"));
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("/test/../abs/local/link to/note.md"),
false,
false,
)
.unwrap();
assert_eq!(output, Path::new("/abs/local/link to/note.md"));
let output = assemble_link(
Path::new("/my"),
Path::new("/my/doc/path"),
Path::new("abs/local/link to/note.md"),
true,
true,
)
.unwrap();
assert_eq!(output, Path::new("/my/doc/path/abs/local/link to/note.md"));
}
#[test]
fn test_decode_html_escape_and_percent() {
let mut input = Link::Text2Dest(Cow::from("text"), Cow::from("dest"), Cow::from("title"));
let expected = Link::Text2Dest(Cow::from("text"), Cow::from("dest"), Cow::from("title"));
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("te%20xt"),
Cow::from("de%20st"),
Cow::from("title"),
);
let expected =
Link::Text2Dest(Cow::from("te%20xt"), Cow::from("de st"), Cow::from("title"));
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
let mut input =
Link::Text2Dest(Cow::from("text"), Cow::from("d:e%20st"), Cow::from("title"));
let expected = Link::Text2Dest(Cow::from("text"), Cow::from("d:e st"), Cow::from("title"));
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("a&"lt"),
Cow::from("a&"lt"),
Cow::from("a&"lt"),
);
let expected = Link::Text2Dest(
Cow::from("a&\"lt"),
Cow::from("a&\"lt"),
Cow::from("a&\"lt"),
);
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Image(Cow::from("al%20t"), Cow::from("de%20st"));
let expected = Link::Image(Cow::from("al%20t"), Cow::from("de st"));
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Image(Cow::from("a\\lt"), Cow::from("d\\est"));
let expected = Link::Image(Cow::from("a\\lt"), Cow::from("d\\est"));
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Image(Cow::from("a&"lt"), Cow::from("a&"lt"));
let expected = Link::Image(Cow::from("a&\"lt"), Cow::from("a&\"lt"));
input.decode_ampersand_and_percent();
let output = input;
assert_eq!(output, expected);
}
#[test]
fn test_is_local() {
let input = Cow::from("/path/My doc.md");
assert!(<Link as Hyperlink>::is_local_fn(&input));
let input = Cow::from("tpnote:path/My doc.md");
assert!(<Link as Hyperlink>::is_local_fn(&input));
let input = Cow::from("tpnote:/path/My doc.md");
assert!(<Link as Hyperlink>::is_local_fn(&input));
let input = Cow::from("https://getreu.net");
assert!(!<Link as Hyperlink>::is_local_fn(&input));
}
#[test]
fn strip_local_scheme() {
let mut input = Link::Text2Dest(
Cow::from("xyz"),
Cow::from("https://getreu.net"),
Cow::from("xyz"),
);
let expected = input.clone();
input.strip_local_scheme();
assert_eq!(input, expected);
let mut input = Link::Text2Dest(
Cow::from("xyz"),
Cow::from("tpnote:/dir/My doc.md"),
Cow::from("xyz"),
);
let expected = Link::Text2Dest(
Cow::from("xyz"),
Cow::from("/dir/My doc.md"),
Cow::from("xyz"),
);
input.strip_local_scheme();
assert_eq!(input, expected);
}
#[test]
fn test_is_autolink() {
let input = Link::Image(Cow::from("abc"), Cow::from("abc"));
assert!(input.is_autolink());
let input = Link::Text2Dest(Cow::from("abc"), Cow::from("abc"), Cow::from("xyz"));
assert!(input.is_autolink());
let input = Link::Image(Cow::from("abc"), Cow::from("abcd"));
assert!(!input.is_autolink());
let input = Link::Text2Dest(Cow::from("abc"), Cow::from("abcd"), Cow::from("xyz"));
assert!(!input.is_autolink());
}
#[test]
fn test_rewrite_local_link() {
let root_path = Path::new("/my/");
let docdir = Path::new("/my/abs/note path/");
let mut input = take_link("<a href=\"ftp://getreu.net\">Blog</a>")
.unwrap()
.1
.1;
input
.rebase_local_link(root_path, docdir, true, false)
.unwrap();
assert!(input.get_local_link_dest_path().is_none());
let root_path = Path::new("/my/");
let docdir = Path::new("/my/abs/note path/");
let mut input = take_link("<img src=\"down/./down/../../t m p.jpg\" alt=\"Image\" />")
.unwrap()
.1
.1;
let expected = "<img src=\"/abs/note path/t m p.jpg\" \
alt=\"Image\">";
input
.rebase_local_link(root_path, docdir, true, false)
.unwrap();
let outpath = input.get_local_link_src_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/abs/note path/t m p.jpg"));
let mut input = take_link("<img src=\"down/./../../t m p.jpg\" alt=\"Image\" />")
.unwrap()
.1
.1;
let expected = "<img src=\"/abs/t m p.jpg\" alt=\"Image\">";
input
.rebase_local_link(root_path, docdir, true, false)
.unwrap();
let outpath = input.get_local_link_src_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/abs/t m p.jpg"));
let mut input = take_link("<a href=\"./down/./../my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let expected = "<a href=\"/abs/note path/my note 1.md\">my note 1</a>";
input
.rebase_local_link(root_path, docdir, true, false)
.unwrap();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/abs/note path/my note 1.md"));
let mut input = take_link("<a href=\"/dir/./down/../my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let expected = "<a href=\"/dir/my note 1.md\">my note 1</a>";
input
.rebase_local_link(root_path, docdir, true, false)
.unwrap();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/dir/my note 1.md"));
let mut input = take_link("<a href=\"./down/./../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let expected = "<a href=\"dir/my note 1.md\">my note 1</a>";
input
.rebase_local_link(root_path, docdir, false, false)
.unwrap();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("dir/my note 1.md"));
let mut input = take_link("<a href=\"./down/./../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let expected = "<a href=\"/path/dir/my note 1.md\">my note 1</a>";
input
.rebase_local_link(
Path::new("/my/note/"),
Path::new("/my/note/path/"),
true,
false,
)
.unwrap();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/path/dir/my note 1.md"));
let mut input = take_link("<a href=\"/down/./../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let expected = "<a href=\"/dir/my note 1.md\">my note 1</a>";
input
.rebase_local_link(root_path, Path::new("/my/ignored/"), true, false)
.unwrap();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/dir/my note 1.md"));
let mut input = take_link("<a href=\"/down/../../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let output = input
.rebase_local_link(root_path, Path::new("/my/notepath/"), true, false)
.unwrap_err();
assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
let mut input = take_link("<a href=\"../../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let output = input
.rebase_local_link(root_path, Path::new("/my/notepath/"), true, false)
.unwrap_err();
assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
let root_path = Path::new("/");
let mut input = take_link("<a href=\"../../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let output = input
.rebase_local_link(root_path, Path::new("/my/"), true, false)
.unwrap_err();
assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
let root_path = Path::new("/my");
let mut input = take_link("<a href=\"../../dir/my note 1.md\">my note 1</a>")
.unwrap()
.1
.1;
let output = input
.rebase_local_link(root_path, Path::new("/my/notepath"), true, false)
.unwrap_err();
assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
let root_path = Path::new("/my");
let mut input =
take_link("<a href=\"tpnote:dir/3.0-my note.md\">tpnote:dir/3.0-my note.md</a>")
.unwrap()
.1
.1;
input.strip_local_scheme();
input
.rebase_local_link(root_path, Path::new("/my/path"), true, false)
.unwrap();
input.rewrite_autolink();
input.apply_format_attribute();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
let expected = "<a href=\"/path/dir/3.0-my note.md\">dir/3.0-my note.md</a>";
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/path/dir/3.0-my note.md"));
let root_path = Path::new("/my");
let mut input = take_link("<a href=\"tpnote:dir/3.0\">tpnote:dir/3.0</a>")
.unwrap()
.1
.1;
input.strip_local_scheme();
input
.rebase_local_link(root_path, Path::new("/my/path"), true, false)
.unwrap();
input.rewrite_autolink();
input.apply_format_attribute();
let outpath = input.get_local_link_dest_path().unwrap();
let output = input.to_html();
let expected = "<a href=\"/path/dir/3.0\">dir/3.0</a>";
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/path/dir/3.0"));
let root_path = Path::new("/my");
let mut input = take_link(
"<a href=\
\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em>\
</a>",
)
.unwrap()
.1
.1;
input.strip_local_scheme();
input
.rebase_local_link(root_path, Path::new("/my/path"), true, false)
.unwrap();
let outpath = input.get_local_link_dest_path().unwrap();
let expected = "<a href=\"/uri\">link <em>foo <strong>bar\
</strong> <code>#</code></em></a>";
let output = input.to_html();
assert_eq!(output, expected);
assert_eq!(outpath, PathBuf::from("/uri"));
}
#[test]
fn test_rewrite_autolink() {
let mut input = Link::Text2Dest(
Cow::from("http://getreu.net"),
Cow::from("http://getreu.net"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("getreu.net"),
Cow::from("http://getreu.net"),
Cow::from("title"),
);
input.rewrite_autolink();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("/dir/3.0"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("/dir/3.0"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
input.rewrite_autolink();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("tpnote:/dir/3.0"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("/dir/3.0"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
input.rewrite_autolink();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("tpnote:/dir/3.0"),
Cow::from("/dir/3.0-My note.md?"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("/dir/3.0"),
Cow::from("/dir/3.0-My note.md?"),
Cow::from("title"),
);
input.rewrite_autolink();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("/dir/3.0-My note.md"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("/dir/3.0-My note.md"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
input.rewrite_autolink();
let output = input;
assert_eq!(output, expected);
}
#[test]
fn test_apply_format_attribute() {
let mut input = Link::Text2Dest(
Cow::from("tpnote:/dir/3.0"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("tpnote:/dir/3.0"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note.md?"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("My note"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg?"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("My note--red_blue_green"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg?--"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("My note"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg?_"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("My note--red"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg??"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("3.0-My note--red_blue_green.jpg"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg?#."),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("3"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg??.:_"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("0-My note--red"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
let mut input = Link::Text2Dest(
Cow::from("does not matter"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg?_:_"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("blue"),
Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
Cow::from("title"),
);
input.apply_format_attribute();
let output = input;
assert_eq!(output, expected);
}
#[test]
fn get_local_link_dest_path() {
let input = Link::Text2Dest(Cow::from("xyz"), Cow::from("/dir/3.0"), Cow::from("title"));
assert_eq!(
input.get_local_link_dest_path(),
Some(Path::new("/dir/3.0"))
);
let input = Link::Text2Dest(
Cow::from("xyz"),
Cow::from("http://getreu.net"),
Cow::from("title"),
);
assert_eq!(input.get_local_link_dest_path(), None);
let input = Link::Text2Dest(Cow::from("xyz"), Cow::from("dir/doc.md"), Cow::from("xyz"));
let expected = Path::new("dir/doc.md");
let res = input.get_local_link_dest_path().unwrap();
assert_eq!(res, expected);
let input = Link::Text2Dest(Cow::from("xyz"), Cow::from("d#ir/doc.md"), Cow::from("xyz"));
let expected = Path::new("d#ir/doc.md");
let res = input.get_local_link_dest_path().unwrap();
assert_eq!(res, expected);
let input = Link::Text2Dest(
Cow::from("xyz"),
Cow::from("dir/doc.md#1"),
Cow::from("xyz"),
);
let expected = Path::new("dir/doc.md");
let res = input.get_local_link_dest_path().unwrap();
assert_eq!(res, expected);
}
#[test]
fn test_append_html_ext() {
let mut input = Link::Text2Dest(
Cow::from("abc"),
Cow::from("/dir/3.0-My note.md"),
Cow::from("title"),
);
let expected = Link::Text2Dest(
Cow::from("abc"),
Cow::from("/dir/3.0-My note.md.html"),
Cow::from("title"),
);
input.append_html_ext();
let output = input;
assert_eq!(output, expected);
}
#[test]
fn test_to_html() {
let input = Link::Text2Dest(
Cow::from("te\\x/t"),
Cow::from("de\\s/t"),
Cow::from("ti\\t/le"),
);
let expected = "<a href=\"de/s/t\" title=\"ti\\t/le\">te\\x/t</a>";
let output = input.to_html();
assert_eq!(output, expected);
let input = Link::Text2Dest(
Cow::from("te&> xt"),
Cow::from("de&> st"),
Cow::from("ti&> tle"),
);
let expected = "<a href=\"de&> st\" title=\"ti&> tle\">te&> xt</a>";
let output = input.to_html();
assert_eq!(output, expected);
let input = Link::Image(Cow::from("al&t"), Cow::from("sr&c"));
let expected = "<img src=\"sr&c\" alt=\"al&t\">";
let output = input.to_html();
assert_eq!(output, expected);
let input = Link::Text2Dest(Cow::from("te&> xt"), Cow::from("de&> st"), Cow::from(""));
let expected = "<a href=\"de&> st\">te&> xt</a>";
let output = input.to_html();
assert_eq!(output, expected);
}
#[test]
fn test_rewrite_links() {
use crate::config::LocalLinkKind;
let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
let input = "abc<a href=\"ftp://getreu.net\">Blog</a>\
def<a href=\"https://getreu.net\">https://getreu.net</a>\
ghi<img src=\"t m p.jpg\" alt=\"test 1\" />\
jkl<a href=\"down/../down/my note 1.md\">my note 1</a>\
mno<a href=\"http:./down/../dir/my note.md\">http:./down/../dir/my note.md</a>\
pqr<a href=\"http:/down/../dir/my note.md\">\
http:/down/../dir/my note.md</a>\
stu<a href=\"http:/../dir/underflow/my note.md\">\
not allowed dir</a>\
vwx<a href=\"http:../../../not allowed dir/my note.md\">\
not allowed</a>"
.to_string();
let expected = "abc<a href=\"ftp://getreu.net\">Blog</a>\
def<a href=\"https://getreu.net\">getreu.net</a>\
ghi<img src=\"/abs/note path/t m p.jpg\" alt=\"test 1\">\
jkl<a href=\"/abs/note path/down/my note 1.md\">my note 1</a>\
mno<a href=\"/abs/note path/dir/my note.md\">./down/../dir/my note.md</a>\
pqr<a href=\"/dir/my note.md\">/down/../dir/my note.md</a>\
stu<i><INVALID: /../dir/underflow/my note.md></i>\
vwx<i><INVALID: ../../../not allowed dir/my note.md></i>"
.to_string();
let root_path = Path::new("/my/");
let docdir = Path::new("/my/abs/note path/");
let output = rewrite_links(
input,
root_path,
docdir,
LocalLinkKind::Short,
false,
allowed_urls.clone(),
);
let url = allowed_urls.read_recursive();
assert!(url.contains(&PathBuf::from("/abs/note path/t m p.jpg")));
assert!(url.contains(&PathBuf::from("/abs/note path/dir/my note.md")));
assert!(url.contains(&PathBuf::from("/abs/note path/down/my note 1.md")));
assert_eq!(output, expected);
}
#[test]
fn test_rewrite_links2() {
use crate::config::LocalLinkKind;
let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
let input = "abd<a href=\"tpnote:dir/my note.md\">\
<img src=\"/imagedir/favicon-32x32.png\" alt=\"logo\"></a>abd"
.to_string();
let expected = "abd<a href=\"/abs/note path/dir/my note.md\">\
<img src=\"/imagedir/favicon-32x32.png\" alt=\"logo\"></a>abd";
let root_path = Path::new("/my/");
let docdir = Path::new("/my/abs/note path/");
let output = rewrite_links(
input,
root_path,
docdir,
LocalLinkKind::Short,
false,
allowed_urls.clone(),
);
let url = allowed_urls.read_recursive();
println!("{:?}", allowed_urls.read_recursive());
assert!(url.contains(&PathBuf::from("/abs/note path/dir/my note.md")));
assert_eq!(output, expected);
}
#[test]
fn test_rewrite_links3() {
use crate::config::LocalLinkKind;
let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
let input = "abd<a href=\"#1\"></a>abd".to_string();
let expected = "abd<a href=\"/abs/note path/#1\"></a>abd";
let root_path = Path::new("/my/");
let docdir = Path::new("/my/abs/note path/");
let output = rewrite_links(
input,
root_path,
docdir,
LocalLinkKind::Short,
false,
allowed_urls.clone(),
);
let url = allowed_urls.read_recursive();
println!("{:?}", allowed_urls.read_recursive());
assert!(url.contains(&PathBuf::from("/abs/note path/")));
assert_eq!(output, expected);
}
#[test]
fn test_is_empty_html() {
use crate::html::HtmlStr;
assert!(String::from("<!DOCTYPE html>").is_empty_html());
assert!(!String::from("<!DOCTYPE html>>").is_empty_html());
assert!(
String::from(
" <!DOCTYPE HTML PUBLIC \
\"-//W3C//DTD HTML 4.01 Transitional//EN\" \
\"http://www.w3.org/TR/html4/loose.dtd\">"
)
.is_empty_html()
);
assert!(
String::from(
" <!DOCTYPE html PUBLIC \
\"-//W3C//DTD XHTML 1.1//EN\" \
\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">"
)
.is_empty_html()
);
assert!(!String::from("<!DOCTYPE html>Some content").is_empty_html());
assert!(String::from("").is_empty_html());
assert!(!String::from("<html></html>").is_empty_html());
assert!(!String::from("<!DOCTYPE html><html></html>").is_empty_html());
}
#[test]
fn test_has_html_start_tag() {
use crate::html::HtmlStr;
assert!(String::from("<!DOCTYPE html>Some content").has_html_start_tag());
assert!(!String::from("<html>Some content</html>").has_html_start_tag());
assert!(!String::from("<HTML>").has_html_start_tag());
assert!(String::from(" <!doctype html>Some content").has_html_start_tag());
assert!(!String::from("<!DOCTYPE other>").has_html_start_tag());
assert!(!String::from("").has_html_start_tag());
}
#[test]
fn test_is_html_unchecked() {
use crate::html::HtmlStr;
let html = "<!doctype html>";
assert!(html.is_html_unchecked());
let html = "<!doctype html abc>def";
assert!(html.is_html_unchecked());
let html = "<!doctype html";
assert!(!html.is_html_unchecked());
let html = "<html><body></body></html>";
assert!(html.is_html_unchecked());
let html = "<html abc>def";
assert!(html.is_html_unchecked());
let html = "<html abc def";
assert!(!html.is_html_unchecked());
let html = " <!doctype html><html><body></body></html>";
assert!(html.is_html_unchecked());
let html = "<!DOCTYPE xml><root></root>";
assert!(!html.is_html_unchecked());
let html = "<!doctype>";
assert!(!html.is_html_unchecked());
}
#[test]
fn test_prepend_html_start_tag() {
use crate::html::HtmlString;
assert_eq!(
String::from("<!DOCTYPE html>Some content").prepend_html_start_tag(),
Ok(String::from("<!DOCTYPE html>Some content"))
);
assert_eq!(
String::from("<!DOCTYPE html>").prepend_html_start_tag(),
Ok(String::from("<!DOCTYPE html>"))
);
assert_eq!(
String::from("<html>Some content").prepend_html_start_tag(),
Ok(String::from("<!DOCTYPE html><html>Some content"))
);
assert_eq!(
String::from("<!DOCTYPE other>").prepend_html_start_tag(),
Err(InputStreamError::NonHtmlDoctype {
html: "<!DOCTYPE other>".to_string()
})
);
assert_eq!(
String::from("Some content").prepend_html_start_tag(),
Ok(String::from("<!DOCTYPE html>Some content"))
);
assert_eq!(
String::from("").prepend_html_start_tag(),
Ok(String::from("<!DOCTYPE html>"))
);
}
}