1use crate::clone_ext::CloneExt;
3use crate::error::InputStreamError;
4use crate::filename::{NotePath, NotePathStr};
5use crate::{config::LocalLinkKind, error::NoteError};
6use html_escape;
7use parking_lot::RwLock;
8use parse_hyperlinks::parser::Link;
9use parse_hyperlinks_extras::iterator_html::HtmlLinkInlineImage;
10use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
11use std::path::MAIN_SEPARATOR_STR;
12use std::{
13 borrow::Cow,
14 collections::HashSet,
15 path::{Component, Path, PathBuf},
16 sync::Arc,
17};
18
19pub(crate) const HTML_EXT: &str = ".html";
20
21const FORMAT_SEPARATOR: char = '?';
24
25const FORMAT_ONLY_SORT_TAG: char = '#';
28
29const FORMAT_COMPLETE_FILENAME: &str = "?";
32
33const FORMAT_FROM_TO_SEPARATOR: char = ':';
37
38static PATH_SEGMENT: &AsciiSet = &CONTROLS.add(b'#').add(b'?').add(b'%').add(b' ');
48
49fn split_path_and_fragment(dest: &str) -> (&str, &str) {
58 match (dest.rfind('#'), dest.rfind(['/', '\\'])) {
59 (Some(n), sep) if sep.is_some_and(|sep| n > sep) || sep.is_none() => {
60 (&dest[..n], &dest[n..])
61 }
62 _ => (dest, ""),
63 }
64}
65
66fn percent_encode_path(path: &str) -> String {
73 path.split('/')
74 .map(|segment| utf8_percent_encode(segment, PATH_SEGMENT).to_string())
75 .collect::<Vec<_>>()
76 .join("/")
77}
78
79fn assemble_link(
89 root_path: &Path,
90 docdir: &Path,
91 dest: &Path,
92 rewrite_rel_paths: bool,
93 rewrite_abs_paths: bool,
94) -> Option<PathBuf> {
95 fn append(path: &mut PathBuf, append: &Path) {
100 for dir in append.components() {
102 match dir {
103 Component::ParentDir => {
104 if !path.pop() {
105 let path_is_relative = {
106 let mut c = path.components();
107 !(c.next() == Some(Component::RootDir)
108 || c.next() == Some(Component::RootDir))
109 };
110 if path_is_relative {
111 path.push(Component::ParentDir.as_os_str());
112 } else {
113 path.clear();
114 break;
115 }
116 }
117 }
118 Component::Normal(c) => path.push(c),
119 _ => {}
120 }
121 }
122 }
123
124 let dest_is_relative = {
126 let mut c = dest.components();
127 !(c.next() == Some(Component::RootDir) || c.next() == Some(Component::RootDir))
128 };
129
130 debug_assert!(docdir.starts_with(root_path));
133
134 let mut link = match (rewrite_rel_paths, rewrite_abs_paths, dest_is_relative) {
136 (true, false, true) => {
139 let link = PathBuf::from(Component::RootDir.as_os_str());
140 link.join(docdir.strip_prefix(root_path).ok()?)
141 }
142 (true, true, true) => docdir.to_path_buf(),
144 (false, _, true) => PathBuf::new(),
146 (_, false, false) => PathBuf::from(Component::RootDir.as_os_str()),
149 (_, true, false) => root_path.to_path_buf(),
151 };
152 append(&mut link, dest);
153
154 if link.as_os_str().is_empty() {
155 None
156 } else {
157 Some(link)
158 }
159}
160
161trait Hyperlink {
162 fn decode_ampersand_and_percent(&mut self);
166
167 #[allow(clippy::ptr_arg)]
169 fn is_local_fn(value: &Cow<str>) -> bool;
170
171 fn strip_local_scheme(&mut self);
177
178 fn strip_scheme_fn(input: &mut Cow<str>);
180
181 fn is_autolink(&self) -> bool;
189
190 fn rebase_local_link(
221 &mut self,
222 root_path: &Path,
223 docdir: &Path,
224 rewrite_rel_paths: bool,
225 rewrite_abs_paths: bool,
226 ) -> Result<(), NoteError>;
227
228 fn expand_shorthand_link(&mut self, prepend_path: Option<&Path>) -> Result<(), NoteError>;
234
235 fn rewrite_autolink(&mut self);
239
240 fn apply_format_attribute(&mut self);
249
250 fn get_local_link_dest_path(&self) -> Option<&Path>;
254
255 fn get_local_link_src_path(&self) -> Option<&Path>;
259
260 fn append_html_ext(&mut self);
264
265 fn to_html(&self) -> String;
270}
271
272impl Hyperlink for Link<'_> {
273 #[inline]
274 fn decode_ampersand_and_percent(&mut self) {
275 fn dec_amp(val: &mut Cow<str>) {
277 let decoded_text = html_escape::decode_html_entities(val);
278 if matches!(&decoded_text, Cow::Owned(..)) {
279 let decoded_text = Cow::Owned(decoded_text.into_owned());
281 let _ = std::mem::replace(val, decoded_text);
283 }
284 }
285
286 fn dec_amp_percent(val: &mut Cow<str>) {
288 dec_amp(val);
289 let decoded_dest = percent_decode_str(val.as_ref()).decode_utf8().unwrap();
290 if matches!(&decoded_dest, Cow::Owned(..)) {
291 let decoded_dest = Cow::Owned(decoded_dest.into_owned());
293 let _ = std::mem::replace(val, decoded_dest);
295 }
296 }
297
298 match self {
299 Link::Text2Dest(text1, dest, title) => {
300 dec_amp(text1);
301 dec_amp_percent(dest);
302 dec_amp(title);
303 }
304 Link::Image(alt, src) => {
305 dec_amp(alt);
306 dec_amp_percent(src);
307 }
308 Link::Image2Dest(text1, alt, src, text2, dest, title) => {
309 dec_amp(text1);
310 dec_amp(alt);
311 dec_amp_percent(src);
312 dec_amp(text2);
313 dec_amp_percent(dest);
314 dec_amp(title);
315 }
316 _ => unimplemented!(),
317 };
318 }
319
320 fn is_local_fn(dest: &Cow<str>) -> bool {
322 !((dest.contains("://") && !dest.contains(":///"))
323 || dest.starts_with("mailto:")
324 || dest.starts_with("tel:"))
325 }
326
327 fn strip_local_scheme(&mut self) {
329 fn strip(dest: &mut Cow<str>) {
330 if <Link<'_> as Hyperlink>::is_local_fn(dest) {
331 <Link<'_> as Hyperlink>::strip_scheme_fn(dest);
332 }
333 }
334
335 match self {
336 Link::Text2Dest(_, dest, _title) => strip(dest),
337 Link::Image2Dest(_, _, src, _, dest, _) => {
338 strip(src);
339 strip(dest);
340 }
341 Link::Image(_, src) => strip(src),
342 _ => {}
343 };
344 }
345
346 fn strip_scheme_fn(inout: &mut Cow<str>) {
348 let output = inout
349 .trim_start_matches("https://")
350 .trim_start_matches("https:")
351 .trim_start_matches("http://")
352 .trim_start_matches("http:")
353 .trim_start_matches("tpnote:")
354 .trim_start_matches("mailto:")
355 .trim_start_matches("tel:");
356 if output != inout.as_ref() {
357 let _ = std::mem::replace(inout, Cow::Owned(output.to_string()));
358 }
359 }
360
361 fn is_autolink(&self) -> bool {
363 let (text, dest) = match self {
364 Link::Text2Dest(text, dest, _title) => (text, dest),
365 Link::Image(alt, source) => (alt, source),
366 _ => return false,
368 };
369 text == dest
370 }
371
372 fn rebase_local_link(
374 &mut self,
375 root_path: &Path,
376 docdir: &Path,
377 rewrite_rel_paths: bool,
378 rewrite_abs_paths: bool,
379 ) -> Result<(), NoteError> {
380 let do_rebase = |path: &mut Cow<str>| -> Result<(), NoteError> {
381 if <Link as Hyperlink>::is_local_fn(path) {
382 let dest_out = assemble_link(
383 root_path,
384 docdir,
385 Path::new(path.as_ref()),
386 rewrite_rel_paths,
387 rewrite_abs_paths,
388 )
389 .ok_or(NoteError::InvalidLocalPath {
390 path: path.as_ref().to_string(),
391 })?;
392
393 let new_dest = Cow::Owned(dest_out.to_str().unwrap_or_default().to_string());
395 let _ = std::mem::replace(path, new_dest);
396 }
397 Ok(())
398 };
399
400 match self {
401 Link::Text2Dest(_, dest, _) => do_rebase(dest),
402 Link::Image2Dest(_, _, src, _, dest, _) => do_rebase(src).and_then(|_| do_rebase(dest)),
403 Link::Image(_, src) => do_rebase(src),
404 _ => unimplemented!(),
405 }
406 }
407
408 fn expand_shorthand_link(&mut self, prepend_path: Option<&Path>) -> Result<(), NoteError> {
410 let shorthand_link = match self {
411 Link::Text2Dest(_, dest, _) => dest,
412 Link::Image2Dest(_, _, _, _, dest, _) => dest,
413 _ => return Ok(()),
414 };
415
416 if !<Link as Hyperlink>::is_local_fn(shorthand_link) {
417 return Ok(());
418 }
419
420 let (shorthand_str, shorthand_format) = match shorthand_link.split_once(FORMAT_SEPARATOR) {
421 Some((path, fmt)) => (path, Some(fmt)),
422 None => (shorthand_link.as_ref(), None),
423 };
424
425 let shorthand_path = Path::new(shorthand_str);
426
427 if let Some(sort_tag) = shorthand_str.is_valid_sort_tag() {
428 let full_shorthand_path = if let Some(root_path) = prepend_path {
429 let shorthand_path = shorthand_path
431 .strip_prefix(MAIN_SEPARATOR_STR)
432 .unwrap_or(shorthand_path);
433 Cow::Owned(root_path.join(shorthand_path))
434 } else {
435 Cow::Borrowed(shorthand_path)
436 };
437
438 let found = full_shorthand_path
440 .parent()
441 .and_then(|dir| dir.find_file_with_sort_tag(sort_tag));
442
443 if let Some(path) = found {
444 let found_link = path
447 .strip_prefix(prepend_path.unwrap_or(Path::new("")))
448 .unwrap();
449 let mut found_link = Path::new(MAIN_SEPARATOR_STR)
451 .join(found_link)
452 .to_str()
453 .unwrap_or_default()
454 .to_string();
455
456 if let Some(fmt) = shorthand_format {
457 found_link.push(FORMAT_SEPARATOR);
458 found_link.push_str(fmt);
459 }
460
461 let _ = std::mem::replace(shorthand_link, Cow::Owned(found_link));
463 } else {
464 return Err(NoteError::CanNotExpandShorthandLink {
465 path: full_shorthand_path.to_string_lossy().into_owned(),
466 });
467 }
468 }
469 Ok(())
470 }
471
472 fn rewrite_autolink(&mut self) {
474 let text = match self {
475 Link::Text2Dest(text, _, _) => text,
476 Link::Image(alt, _) => alt,
477 _ => return,
478 };
479
480 <Link as Hyperlink>::strip_scheme_fn(text);
481 }
482
483 fn apply_format_attribute(&mut self) {
485 let (text, dest) = match self {
488 Link::Text2Dest(text, dest, _) => (text, dest),
489 Link::Image(alt, source) => (alt, source),
490 _ => return,
491 };
492
493 if !<Link as Hyperlink>::is_local_fn(dest) {
494 return;
495 }
496
497 let (path, format) = match dest.split_once(FORMAT_SEPARATOR) {
502 Some(s) => s,
503 None => return,
504 };
505
506 let mut short_text = Path::new(path)
507 .file_name()
508 .unwrap_or_default()
509 .to_str()
510 .unwrap_or_default();
511
512 let format = if format.starts_with(FORMAT_COMPLETE_FILENAME) {
514 format
516 .strip_prefix(FORMAT_COMPLETE_FILENAME)
517 .unwrap_or(format)
518 } else if format.starts_with(FORMAT_ONLY_SORT_TAG) {
519 short_text = Path::new(path).disassemble().0;
521 format.strip_prefix(FORMAT_ONLY_SORT_TAG).unwrap_or(format)
522 } else {
523 short_text = Path::new(path).disassemble().2;
525 format
526 };
527
528 match format.split_once(FORMAT_FROM_TO_SEPARATOR) {
529 None => {
531 if !format.is_empty()
532 && let Some(idx) = short_text.find(format) {
533 short_text = &short_text[..idx];
534 };
535 }
536 Some((from, to)) => {
538 if !from.is_empty()
539 && let Some(idx) = short_text.find(from) {
540 short_text = &short_text[(idx + from.len())..];
541 };
542 if !to.is_empty()
543 && let Some(idx) = short_text.find(to) {
544 short_text = &short_text[..idx];
545 };
546 }
547 }
548 let _ = std::mem::replace(text, Cow::Owned(short_text.to_string()));
550 let _ = std::mem::replace(dest, Cow::Owned(path.to_string()));
551 }
552
553 fn get_local_link_dest_path(&self) -> Option<&Path> {
555 let dest = match self {
556 Link::Text2Dest(_, dest, _) => dest,
557 Link::Image2Dest(_, _, _, _, dest, _) => dest,
558 _ => return None,
559 };
560 if <Link as Hyperlink>::is_local_fn(dest) {
561 Some(Path::new(split_path_and_fragment(dest.as_ref()).0))
562 } else {
563 None
564 }
565 }
566
567 fn get_local_link_src_path(&self) -> Option<&Path> {
569 let src = match self {
570 Link::Image2Dest(_, _, src, _, _, _) => src,
571 Link::Image(_, src) => src,
572 _ => return None,
573 };
574 if <Link as Hyperlink>::is_local_fn(src) {
575 Some(Path::new(src.as_ref()))
576 } else {
577 None
578 }
579 }
580
581 fn append_html_ext(&mut self) {
583 let dest = match self {
584 Link::Text2Dest(_, dest, _) => dest,
585 Link::Image2Dest(_, _, _, _, dest, _) => dest,
586 _ => return,
587 };
588 if <Link as Hyperlink>::is_local_fn(dest) {
589 let path = dest.as_ref();
590 if path.has_tpnote_ext() {
591 let mut newpath = path.to_string();
592 newpath.push_str(HTML_EXT);
593
594 let _ = std::mem::replace(dest, Cow::Owned(newpath));
595 }
596 }
597 }
598
599 fn to_html(&self) -> String {
601 fn enc_amp(val: Cow<str>) -> Cow<str> {
603 let s = html_escape::encode_double_quoted_attribute(val.as_ref());
604 if s == val {
605 val
606 } else {
607 Cow::Owned(s.into_owned())
609 }
610 }
611 fn repl_backspace_enc_amp(val: Cow<str>) -> Cow<str> {
614 let val = if val.as_ref().contains('\\') {
618 Cow::Owned(val.to_string().replace('\\', "/"))
619 } else {
620 val
621 };
622 let (path, fragment) = split_path_and_fragment(val.as_ref());
623 let encoded = format!("{}{}", percent_encode_path(path), fragment);
624 let s = html_escape::encode_double_quoted_attribute(&encoded);
625 Cow::Owned(s.into_owned())
626 }
627
628 match self {
629 Link::Text2Dest(text, dest, title) => {
630 let title_html = if !title.is_empty() {
632 format!(" title=\"{}\"", enc_amp(title.shallow_clone()))
633 } else {
634 "".to_string()
635 };
636
637 format!(
638 "<a href=\"{}\"{}>{}</a>",
639 repl_backspace_enc_amp(dest.shallow_clone()),
640 title_html,
641 text
642 )
643 }
644 Link::Image2Dest(text1, alt, src, text2, dest, title) => {
645 let title_html = if !title.is_empty() {
647 format!(" title=\"{}\"", enc_amp(title.shallow_clone()))
648 } else {
649 "".to_string()
650 };
651
652 format!(
653 "<a href=\"{}\"{}>{}<img src=\"{}\" alt=\"{}\">{}</a>",
654 repl_backspace_enc_amp(dest.shallow_clone()),
655 title_html,
656 text1,
657 repl_backspace_enc_amp(src.shallow_clone()),
658 enc_amp(alt.shallow_clone()),
659 text2
660 )
661 }
662 Link::Image(alt, src) => {
663 format!(
664 "<img src=\"{}\" alt=\"{}\">",
665 repl_backspace_enc_amp(src.shallow_clone()),
666 enc_amp(alt.shallow_clone())
667 )
668 }
669 _ => unimplemented!(),
670 }
671 }
672}
673
674#[inline]
675pub fn rewrite_links(
715 html_input: String,
716 root_path: &Path,
717 docdir: &Path,
718 local_link_kind: LocalLinkKind,
719 rewrite_ext: bool,
720 allowed_local_links: Arc<RwLock<HashSet<PathBuf>>>,
721) -> String {
722 let (rewrite_rel_paths, rewrite_abs_paths) = match local_link_kind {
723 LocalLinkKind::Off => (false, false),
724 LocalLinkKind::Short => (true, false),
725 LocalLinkKind::Long => (true, true),
726 };
727
728 let mut rest = &*html_input;
731 let mut html_out = String::new();
732 for ((skipped, _consumed, remaining), mut link) in HtmlLinkInlineImage::new(&html_input) {
733 html_out.push_str(skipped);
734 rest = remaining;
735
736 let mut link_is_autolink = link.is_autolink();
738
739 link.decode_ampersand_and_percent();
741
742 link_is_autolink = link_is_autolink || link.is_autolink();
744
745 link.strip_local_scheme();
746
747 match link
749 .rebase_local_link(root_path, docdir, rewrite_rel_paths, rewrite_abs_paths)
750 .and_then(|_| {
751 link.expand_shorthand_link(
752 (matches!(local_link_kind, LocalLinkKind::Short)).then_some(root_path),
753 )
754 }) {
755 Ok(()) => {}
756 Err(e) => {
757 let e = e.to_string();
758 let e = html_escape::encode_text(&e);
759 html_out.push_str(&format!("<i>{}</i>", e));
760 continue;
761 }
762 };
763
764 if link_is_autolink {
765 link.rewrite_autolink();
766 }
767
768 link.apply_format_attribute();
769
770 if let Some(dest_path) = link.get_local_link_dest_path() {
771 allowed_local_links.write().insert(dest_path.to_path_buf());
772 };
773 if let Some(src_path) = link.get_local_link_src_path() {
774 allowed_local_links.write().insert(src_path.to_path_buf());
775 };
776
777 if rewrite_ext {
778 link.append_html_ext();
779 }
780 html_out.push_str(&link.to_html());
781 }
782 html_out.push_str(rest);
784
785 log::trace!(
786 "Viewer: referenced allowed local files: {}",
787 allowed_local_links
788 .read_recursive()
789 .iter()
790 .map(|p| {
791 let mut s = "\n '".to_string();
792 s.push_str(&p.display().to_string());
793 s
794 })
795 .collect::<String>()
796 );
797
798 html_out
799 }
801
802pub trait HtmlStr {
804 const TAG_DOCTYPE_PAT: &'static str = "<!doctype";
806 const TAG_DOCTYPE_HTML_PAT: &'static str = "<!doctype html";
808 const TAG_DOCTYPE_HTML: &'static str = "<!DOCTYPE html>";
811 const START_TAG_HTML_PAT: &'static str = "<html";
813 const END_TAG_HTML: &'static str = "</html>";
815
816 fn is_empty_html(&self) -> bool;
819
820 fn is_empty_html2(html: &str) -> bool {
825 html.is_empty_html()
826 }
827
828 fn has_html_start_tag(&self) -> bool;
830
831 fn has_html_start_tag2(html: &str) -> bool {
835 html.has_html_start_tag()
836 }
837
838 fn is_html_unchecked(&self) -> bool;
847}
848
849impl HtmlStr for str {
850 fn is_empty_html(&self) -> bool {
851 if self.is_empty() {
852 return true;
853 }
854
855 let html = self
856 .trim_start()
857 .lines()
858 .next()
859 .map(|l| l.to_ascii_lowercase())
860 .unwrap_or_default();
861
862 html.as_str().starts_with(Self::TAG_DOCTYPE_HTML_PAT)
863 && html.find('>').unwrap_or_default() == html.len()-1
865 }
866
867 fn has_html_start_tag(&self) -> bool {
868 let html = self
869 .trim_start()
870 .lines()
871 .next()
872 .map(|l| l.to_ascii_lowercase());
873 html.as_ref()
874 .is_some_and(|l| l.starts_with(Self::TAG_DOCTYPE_HTML_PAT))
875 }
876
877 fn is_html_unchecked(&self) -> bool {
878 let html = self
879 .trim_start()
880 .lines()
881 .next()
882 .map(|l| l.to_ascii_lowercase());
883 html.as_ref().is_some_and(|l| {
884 (l.starts_with(Self::TAG_DOCTYPE_HTML_PAT)
885 && l[Self::TAG_DOCTYPE_HTML_PAT.len()..].contains('>'))
886 || (l.starts_with(Self::START_TAG_HTML_PAT)
887 && l[Self::START_TAG_HTML_PAT.len()..].contains('>'))
888 })
889 }
890}
891
892pub trait HtmlString: Sized {
894 fn prepend_html_start_tag(self) -> Result<Self, InputStreamError>;
899}
900
901impl HtmlString for String {
902 fn prepend_html_start_tag(self) -> Result<Self, InputStreamError> {
903 use crate::html::HtmlStr;
905
906 let html2 = self
907 .trim_start()
908 .lines()
909 .next()
910 .map(|l| l.to_ascii_lowercase())
911 .unwrap_or_default();
912
913 if html2.starts_with(<str as HtmlStr>::TAG_DOCTYPE_HTML_PAT) {
914 Ok(self)
916 } else if !html2.starts_with(<str as HtmlStr>::TAG_DOCTYPE_PAT) {
917 let mut html = self;
919 html.insert_str(0, <str as HtmlStr>::TAG_DOCTYPE_HTML);
920 Ok(html)
921 } else {
922 Err(InputStreamError::NonHtmlDoctype {
924 html: self.chars().take(25).collect::<String>(),
925 })
926 }
927 }
928}
929
930#[cfg(test)]
931mod tests {
932
933 use crate::error::InputStreamError;
934 use crate::error::NoteError;
935 use crate::html::Hyperlink;
936 use crate::html::assemble_link;
937 use crate::html::rewrite_links;
938 use parking_lot::RwLock;
939 use parse_hyperlinks::parser::Link;
940 use parse_hyperlinks_extras::parser::parse_html::take_link;
941 use std::borrow::Cow;
942 use std::{
943 collections::HashSet,
944 path::{Path, PathBuf},
945 sync::Arc,
946 };
947
948 #[test]
949 fn test_assemble_link() {
950 let output = assemble_link(
952 Path::new("/my"),
953 Path::new("/my/doc/path"),
954 Path::new("../local/link to/note.md"),
955 true,
956 false,
957 )
958 .unwrap();
959 assert_eq!(output, Path::new("/doc/local/link to/note.md"));
960
961 let output = assemble_link(
963 Path::new("/my"),
964 Path::new("/my/doc/path"),
965 Path::new("../local/link to/note.md"),
966 false,
967 false,
968 )
969 .unwrap();
970 assert_eq!(output, Path::new("../local/link to/note.md"));
971
972 let output = assemble_link(
974 Path::new("/my"),
975 Path::new("/my/doc/path"),
976 Path::new("/test/../abs/local/link to/note.md"),
977 false,
978 false,
979 )
980 .unwrap();
981 assert_eq!(output, Path::new("/abs/local/link to/note.md"));
982
983 let output = assemble_link(
985 Path::new("/my"),
986 Path::new("/my/doc/path"),
987 Path::new("/../local/link to/note.md"),
988 false,
989 false,
990 );
991 assert_eq!(output, None);
992
993 let output = assemble_link(
995 Path::new("/my"),
996 Path::new("/my/doc/path"),
997 Path::new("/abs/local/link to/note.md"),
998 false,
999 true,
1000 )
1001 .unwrap();
1002 assert_eq!(output, Path::new("/my/abs/local/link to/note.md"));
1003
1004 let output = assemble_link(
1006 Path::new("/my"),
1007 Path::new("/my/doc/path"),
1008 Path::new("/test/../abs/local/link to/note.md"),
1009 false,
1010 false,
1011 )
1012 .unwrap();
1013 assert_eq!(output, Path::new("/abs/local/link to/note.md"));
1014
1015 let output = assemble_link(
1017 Path::new("/my"),
1018 Path::new("/my/doc/path"),
1019 Path::new("abs/local/link to/note.md"),
1020 true,
1021 true,
1022 )
1023 .unwrap();
1024 assert_eq!(output, Path::new("/my/doc/path/abs/local/link to/note.md"));
1025 }
1026
1027 #[test]
1028 fn test_decode_html_escape_and_percent() {
1029 let mut input = Link::Text2Dest(Cow::from("text"), Cow::from("dest"), Cow::from("title"));
1031 let expected = Link::Text2Dest(Cow::from("text"), Cow::from("dest"), Cow::from("title"));
1032 input.decode_ampersand_and_percent();
1033 let output = input;
1034 assert_eq!(output, expected);
1035
1036 let mut input = Link::Text2Dest(
1038 Cow::from("te%20xt"),
1039 Cow::from("de%20st"),
1040 Cow::from("title"),
1041 );
1042 let expected =
1043 Link::Text2Dest(Cow::from("te%20xt"), Cow::from("de st"), Cow::from("title"));
1044 input.decode_ampersand_and_percent();
1045 let output = input;
1046 assert_eq!(output, expected);
1047
1048 let mut input =
1050 Link::Text2Dest(Cow::from("text"), Cow::from("d:e%20st"), Cow::from("title"));
1051 let expected = Link::Text2Dest(Cow::from("text"), Cow::from("d:e st"), Cow::from("title"));
1052 input.decode_ampersand_and_percent();
1053 let output = input;
1054 assert_eq!(output, expected);
1055
1056 let mut input = Link::Text2Dest(
1057 Cow::from("a&"lt"),
1058 Cow::from("a&"lt"),
1059 Cow::from("a&"lt"),
1060 );
1061 let expected = Link::Text2Dest(
1062 Cow::from("a&\"lt"),
1063 Cow::from("a&\"lt"),
1064 Cow::from("a&\"lt"),
1065 );
1066 input.decode_ampersand_and_percent();
1067 let output = input;
1068 assert_eq!(output, expected);
1069
1070 let mut input = Link::Image(Cow::from("al%20t"), Cow::from("de%20st"));
1072 let expected = Link::Image(Cow::from("al%20t"), Cow::from("de st"));
1073 input.decode_ampersand_and_percent();
1074 let output = input;
1075 assert_eq!(output, expected);
1076
1077 let mut input = Link::Image(Cow::from("a\\lt"), Cow::from("d\\est"));
1079 let expected = Link::Image(Cow::from("a\\lt"), Cow::from("d\\est"));
1080 input.decode_ampersand_and_percent();
1081 let output = input;
1082 assert_eq!(output, expected);
1083
1084 let mut input = Link::Image(Cow::from("a&"lt"), Cow::from("a&"lt"));
1086 let expected = Link::Image(Cow::from("a&\"lt"), Cow::from("a&\"lt"));
1087 input.decode_ampersand_and_percent();
1088 let output = input;
1089 assert_eq!(output, expected);
1090 }
1091
1092 #[test]
1093 fn test_is_local() {
1094 let input = Cow::from("/path/My doc.md");
1095 assert!(<Link as Hyperlink>::is_local_fn(&input));
1096
1097 let input = Cow::from("tpnote:path/My doc.md");
1098 assert!(<Link as Hyperlink>::is_local_fn(&input));
1099
1100 let input = Cow::from("tpnote:/path/My doc.md");
1101 assert!(<Link as Hyperlink>::is_local_fn(&input));
1102
1103 let input = Cow::from("https://getreu.net");
1104 assert!(!<Link as Hyperlink>::is_local_fn(&input));
1105 }
1106
1107 #[test]
1108 fn strip_local_scheme() {
1109 let mut input = Link::Text2Dest(
1110 Cow::from("xyz"),
1111 Cow::from("https://getreu.net"),
1112 Cow::from("xyz"),
1113 );
1114 let expected = input.clone();
1115 input.strip_local_scheme();
1116 assert_eq!(input, expected);
1117
1118 let mut input = Link::Text2Dest(
1120 Cow::from("xyz"),
1121 Cow::from("tpnote:/dir/My doc.md"),
1122 Cow::from("xyz"),
1123 );
1124 let expected = Link::Text2Dest(
1125 Cow::from("xyz"),
1126 Cow::from("/dir/My doc.md"),
1127 Cow::from("xyz"),
1128 );
1129 input.strip_local_scheme();
1130 assert_eq!(input, expected);
1131 }
1132
1133 #[test]
1134 fn test_is_autolink() {
1135 let input = Link::Image(Cow::from("abc"), Cow::from("abc"));
1136 assert!(input.is_autolink());
1137
1138 let input = Link::Text2Dest(Cow::from("abc"), Cow::from("abc"), Cow::from("xyz"));
1140 assert!(input.is_autolink());
1141
1142 let input = Link::Image(Cow::from("abc"), Cow::from("abcd"));
1144 assert!(!input.is_autolink());
1145
1146 let input = Link::Text2Dest(Cow::from("abc"), Cow::from("abcd"), Cow::from("xyz"));
1148 assert!(!input.is_autolink());
1149 }
1150
1151 #[test]
1152 fn test_rewrite_local_link() {
1153 let root_path = Path::new("/my/");
1154 let docdir = Path::new("/my/abs/note path/");
1155
1156 let mut input = take_link("<a href=\"ftp://getreu.net\">Blog</a>")
1158 .unwrap()
1159 .1
1160 .1;
1161 input
1162 .rebase_local_link(root_path, docdir, true, false)
1163 .unwrap();
1164 assert!(input.get_local_link_dest_path().is_none());
1165
1166 let root_path = Path::new("/my/");
1168 let docdir = Path::new("/my/abs/note path/");
1169
1170 let mut input = take_link("<img src=\"down/./down/../../t m p.jpg\" alt=\"Image\" />")
1172 .unwrap()
1173 .1
1174 .1;
1175 let expected = "<img src=\"/abs/note%20path/t%20m%20p.jpg\" \
1176 alt=\"Image\">";
1177 input
1178 .rebase_local_link(root_path, docdir, true, false)
1179 .unwrap();
1180 let outpath = input.get_local_link_src_path().unwrap();
1181 let output = input.to_html();
1182 assert_eq!(output, expected);
1183 assert_eq!(outpath, PathBuf::from("/abs/note path/t m p.jpg"));
1184
1185 let mut input = take_link("<img src=\"down/./../../t m p.jpg\" alt=\"Image\" />")
1187 .unwrap()
1188 .1
1189 .1;
1190 let expected = "<img src=\"/abs/t%20m%20p.jpg\" alt=\"Image\">";
1191 input
1192 .rebase_local_link(root_path, docdir, true, false)
1193 .unwrap();
1194 let outpath = input.get_local_link_src_path().unwrap();
1195 let output = input.to_html();
1196 assert_eq!(output, expected);
1197 assert_eq!(outpath, PathBuf::from("/abs/t m p.jpg"));
1198
1199 let mut input = take_link("<a href=\"./down/./../my note 1.md\">my note 1</a>")
1201 .unwrap()
1202 .1
1203 .1;
1204 let expected = "<a href=\"/abs/note%20path/my%20note%201.md\">my note 1</a>";
1205 input
1206 .rebase_local_link(root_path, docdir, true, false)
1207 .unwrap();
1208 let outpath = input.get_local_link_dest_path().unwrap();
1209 let output = input.to_html();
1210 assert_eq!(output, expected);
1211 assert_eq!(outpath, PathBuf::from("/abs/note path/my note 1.md"));
1212
1213 let mut input = take_link("<a href=\"/dir/./down/../my note 1.md\">my note 1</a>")
1215 .unwrap()
1216 .1
1217 .1;
1218 let expected = "<a href=\"/dir/my%20note%201.md\">my note 1</a>";
1219 input
1220 .rebase_local_link(root_path, docdir, true, false)
1221 .unwrap();
1222 let outpath = input.get_local_link_dest_path().unwrap();
1223 let output = input.to_html();
1224 assert_eq!(output, expected);
1225 assert_eq!(outpath, PathBuf::from("/dir/my note 1.md"));
1226
1227 let mut input = take_link("<a href=\"./down/./../dir/my note 1.md\">my note 1</a>")
1229 .unwrap()
1230 .1
1231 .1;
1232 let expected = "<a href=\"dir/my%20note%201.md\">my note 1</a>";
1233 input
1234 .rebase_local_link(root_path, docdir, false, false)
1235 .unwrap();
1236 let outpath = input.get_local_link_dest_path().unwrap();
1237 let output = input.to_html();
1238 assert_eq!(output, expected);
1239 assert_eq!(outpath, PathBuf::from("dir/my note 1.md"));
1240
1241 let mut input = take_link("<a href=\"./down/./../dir/my note 1.md\">my note 1</a>")
1243 .unwrap()
1244 .1
1245 .1;
1246 let expected = "<a href=\"/path/dir/my%20note%201.md\">my note 1</a>";
1247 input
1248 .rebase_local_link(
1249 Path::new("/my/note/"),
1250 Path::new("/my/note/path/"),
1251 true,
1252 false,
1253 )
1254 .unwrap();
1255 let outpath = input.get_local_link_dest_path().unwrap();
1256 let output = input.to_html();
1257 assert_eq!(output, expected);
1258 assert_eq!(outpath, PathBuf::from("/path/dir/my note 1.md"));
1259
1260 let mut input = take_link("<a href=\"/down/./../dir/my note 1.md\">my note 1</a>")
1262 .unwrap()
1263 .1
1264 .1;
1265 let expected = "<a href=\"/dir/my%20note%201.md\">my note 1</a>";
1266 input
1267 .rebase_local_link(root_path, Path::new("/my/ignored/"), true, false)
1268 .unwrap();
1269 let outpath = input.get_local_link_dest_path().unwrap();
1270 let output = input.to_html();
1271 assert_eq!(output, expected);
1272 assert_eq!(outpath, PathBuf::from("/dir/my note 1.md"));
1273
1274 let mut input = take_link("<a href=\"/down/../../dir/my note 1.md\">my note 1</a>")
1276 .unwrap()
1277 .1
1278 .1;
1279 let output = input
1280 .rebase_local_link(root_path, Path::new("/my/notepath/"), true, false)
1281 .unwrap_err();
1282 assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
1283
1284 let mut input = take_link("<a href=\"../../dir/my note 1.md\">my note 1</a>")
1286 .unwrap()
1287 .1
1288 .1;
1289 let output = input
1290 .rebase_local_link(root_path, Path::new("/my/notepath/"), true, false)
1291 .unwrap_err();
1292 assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
1293
1294 let root_path = Path::new("/");
1296 let mut input = take_link("<a href=\"../../dir/my note 1.md\">my note 1</a>")
1297 .unwrap()
1298 .1
1299 .1;
1300 let output = input
1301 .rebase_local_link(root_path, Path::new("/my/"), true, false)
1302 .unwrap_err();
1303 assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
1304
1305 let root_path = Path::new("/my");
1307 let mut input = take_link("<a href=\"../../dir/my note 1.md\">my note 1</a>")
1308 .unwrap()
1309 .1
1310 .1;
1311 let output = input
1312 .rebase_local_link(root_path, Path::new("/my/notepath"), true, false)
1313 .unwrap_err();
1314 assert!(matches!(output, NoteError::InvalidLocalPath { .. }));
1315
1316 let root_path = Path::new("/my");
1318 let mut input =
1319 take_link("<a href=\"tpnote:dir/3.0-my note.md\">tpnote:dir/3.0-my note.md</a>")
1320 .unwrap()
1321 .1
1322 .1;
1323 input.strip_local_scheme();
1324 input
1325 .rebase_local_link(root_path, Path::new("/my/path"), true, false)
1326 .unwrap();
1327 input.rewrite_autolink();
1328 input.apply_format_attribute();
1329 let outpath = input.get_local_link_dest_path().unwrap();
1330 let output = input.to_html();
1331 let expected = "<a href=\"/path/dir/3.0-my%20note.md\">dir/3.0-my note.md</a>";
1332 assert_eq!(output, expected);
1333 assert_eq!(outpath, PathBuf::from("/path/dir/3.0-my note.md"));
1334
1335 let root_path = Path::new("/my");
1337 let mut input = take_link("<a href=\"tpnote:dir/3.0\">tpnote:dir/3.0</a>")
1338 .unwrap()
1339 .1
1340 .1;
1341 input.strip_local_scheme();
1342 input
1343 .rebase_local_link(root_path, Path::new("/my/path"), true, false)
1344 .unwrap();
1345 input.rewrite_autolink();
1346 input.apply_format_attribute();
1347 let outpath = input.get_local_link_dest_path().unwrap();
1348 let output = input.to_html();
1349 let expected = "<a href=\"/path/dir/3.0\">dir/3.0</a>";
1350 assert_eq!(output, expected);
1351 assert_eq!(outpath, PathBuf::from("/path/dir/3.0"));
1352
1353 let root_path = Path::new("/my");
1355 let mut input = take_link(
1356 "<a href=\
1357 \"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em>\
1358 </a>",
1359 )
1360 .unwrap()
1361 .1
1362 .1;
1363 input.strip_local_scheme();
1364 input
1365 .rebase_local_link(root_path, Path::new("/my/path"), true, false)
1366 .unwrap();
1367 let outpath = input.get_local_link_dest_path().unwrap();
1368 let expected = "<a href=\"/uri\">link <em>foo <strong>bar\
1369 </strong> <code>#</code></em></a>";
1370
1371 let output = input.to_html();
1372 assert_eq!(output, expected);
1373 assert_eq!(outpath, PathBuf::from("/uri"));
1374 }
1375
1376 #[test]
1377 fn test_rewrite_autolink() {
1378 let mut input = Link::Text2Dest(
1380 Cow::from("http://getreu.net"),
1381 Cow::from("http://getreu.net"),
1382 Cow::from("title"),
1383 );
1384 let expected = Link::Text2Dest(
1385 Cow::from("getreu.net"),
1386 Cow::from("http://getreu.net"),
1387 Cow::from("title"),
1388 );
1389 input.rewrite_autolink();
1390 let output = input;
1391 assert_eq!(output, expected);
1392
1393 let mut input = Link::Text2Dest(
1395 Cow::from("/dir/3.0"),
1396 Cow::from("/dir/3.0-My note.md"),
1397 Cow::from("title"),
1398 );
1399 let expected = Link::Text2Dest(
1400 Cow::from("/dir/3.0"),
1401 Cow::from("/dir/3.0-My note.md"),
1402 Cow::from("title"),
1403 );
1404 input.rewrite_autolink();
1405 let output = input;
1406 assert_eq!(output, expected);
1407
1408 let mut input = Link::Text2Dest(
1410 Cow::from("tpnote:/dir/3.0"),
1411 Cow::from("/dir/3.0-My note.md"),
1412 Cow::from("title"),
1413 );
1414 let expected = Link::Text2Dest(
1415 Cow::from("/dir/3.0"),
1416 Cow::from("/dir/3.0-My note.md"),
1417 Cow::from("title"),
1418 );
1419 input.rewrite_autolink();
1420 let output = input;
1421 assert_eq!(output, expected);
1422
1423 let mut input = Link::Text2Dest(
1425 Cow::from("tpnote:/dir/3.0"),
1426 Cow::from("/dir/3.0-My note.md?"),
1427 Cow::from("title"),
1428 );
1429 let expected = Link::Text2Dest(
1430 Cow::from("/dir/3.0"),
1431 Cow::from("/dir/3.0-My note.md?"),
1432 Cow::from("title"),
1433 );
1434 input.rewrite_autolink();
1435 let output = input;
1436 assert_eq!(output, expected);
1437
1438 let mut input = Link::Text2Dest(
1440 Cow::from("/dir/3.0-My note.md"),
1441 Cow::from("/dir/3.0-My note.md"),
1442 Cow::from("title"),
1443 );
1444 let expected = Link::Text2Dest(
1445 Cow::from("/dir/3.0-My note.md"),
1446 Cow::from("/dir/3.0-My note.md"),
1447 Cow::from("title"),
1448 );
1449 input.rewrite_autolink();
1450 let output = input;
1451 assert_eq!(output, expected);
1452 }
1453
1454 #[test]
1455 fn test_apply_format_attribute() {
1456 let mut input = Link::Text2Dest(
1458 Cow::from("tpnote:/dir/3.0"),
1459 Cow::from("/dir/3.0-My note.md"),
1460 Cow::from("title"),
1461 );
1462 let expected = Link::Text2Dest(
1463 Cow::from("tpnote:/dir/3.0"),
1464 Cow::from("/dir/3.0-My note.md"),
1465 Cow::from("title"),
1466 );
1467 input.apply_format_attribute();
1468 let output = input;
1469 assert_eq!(output, expected);
1470
1471 let mut input = Link::Text2Dest(
1473 Cow::from("does not matter"),
1474 Cow::from("/dir/3.0-My note.md?"),
1475 Cow::from("title"),
1476 );
1477 let expected = Link::Text2Dest(
1478 Cow::from("My note"),
1479 Cow::from("/dir/3.0-My note.md"),
1480 Cow::from("title"),
1481 );
1482 input.apply_format_attribute();
1483 let output = input;
1484 assert_eq!(output, expected);
1485
1486 let mut input = Link::Text2Dest(
1487 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1488 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1489 Cow::from("title"),
1490 );
1491 let expected = Link::Text2Dest(
1492 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1493 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1494 Cow::from("title"),
1495 );
1496 input.apply_format_attribute();
1497 let output = input;
1498 assert_eq!(output, expected);
1499
1500 let mut input = Link::Text2Dest(
1502 Cow::from("does not matter"),
1503 Cow::from("/dir/3.0-My note--red_blue_green.jpg?"),
1504 Cow::from("title"),
1505 );
1506 let expected = Link::Text2Dest(
1507 Cow::from("My note--red_blue_green"),
1508 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1509 Cow::from("title"),
1510 );
1511 input.apply_format_attribute();
1512 let output = input;
1513 assert_eq!(output, expected);
1514
1515 let mut input = Link::Text2Dest(
1517 Cow::from("does not matter"),
1518 Cow::from("/dir/3.0-My note--red_blue_green.jpg?--"),
1519 Cow::from("title"),
1520 );
1521 let expected = Link::Text2Dest(
1522 Cow::from("My note"),
1523 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1524 Cow::from("title"),
1525 );
1526 input.apply_format_attribute();
1527 let output = input;
1528 assert_eq!(output, expected);
1529
1530 let mut input = Link::Text2Dest(
1532 Cow::from("does not matter"),
1533 Cow::from("/dir/3.0-My note--red_blue_green.jpg?_"),
1534 Cow::from("title"),
1535 );
1536 let expected = Link::Text2Dest(
1537 Cow::from("My note--red"),
1538 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1539 Cow::from("title"),
1540 );
1541 input.apply_format_attribute();
1542 let output = input;
1543 assert_eq!(output, expected);
1544
1545 let mut input = Link::Text2Dest(
1547 Cow::from("does not matter"),
1548 Cow::from("/dir/3.0-My note--red_blue_green.jpg??"),
1549 Cow::from("title"),
1550 );
1551 let expected = Link::Text2Dest(
1552 Cow::from("3.0-My note--red_blue_green.jpg"),
1553 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1554 Cow::from("title"),
1555 );
1556 input.apply_format_attribute();
1557 let output = input;
1558 assert_eq!(output, expected);
1559
1560 let mut input = Link::Text2Dest(
1562 Cow::from("does not matter"),
1563 Cow::from("/dir/3.0-My note--red_blue_green.jpg?#."),
1564 Cow::from("title"),
1565 );
1566 let expected = Link::Text2Dest(
1567 Cow::from("3"),
1568 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1569 Cow::from("title"),
1570 );
1571 input.apply_format_attribute();
1572 let output = input;
1573 assert_eq!(output, expected);
1574
1575 let mut input = Link::Text2Dest(
1577 Cow::from("does not matter"),
1578 Cow::from("/dir/3.0-My note--red_blue_green.jpg??.:_"),
1579 Cow::from("title"),
1580 );
1581 let expected = Link::Text2Dest(
1582 Cow::from("0-My note--red"),
1583 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1584 Cow::from("title"),
1585 );
1586 input.apply_format_attribute();
1587 let output = input;
1588 assert_eq!(output, expected);
1589
1590 let mut input = Link::Text2Dest(
1592 Cow::from("does not matter"),
1593 Cow::from("/dir/3.0-My note--red_blue_green.jpg?_:_"),
1594 Cow::from("title"),
1595 );
1596 let expected = Link::Text2Dest(
1597 Cow::from("blue"),
1598 Cow::from("/dir/3.0-My note--red_blue_green.jpg"),
1599 Cow::from("title"),
1600 );
1601 input.apply_format_attribute();
1602 let output = input;
1603 assert_eq!(output, expected);
1604 }
1605
1606 #[test]
1607 fn get_local_link_dest_path() {
1608 let input = Link::Text2Dest(Cow::from("xyz"), Cow::from("/dir/3.0"), Cow::from("title"));
1610 assert_eq!(
1611 input.get_local_link_dest_path(),
1612 Some(Path::new("/dir/3.0"))
1613 );
1614
1615 let input = Link::Text2Dest(
1617 Cow::from("xyz"),
1618 Cow::from("http://getreu.net"),
1619 Cow::from("title"),
1620 );
1621 assert_eq!(input.get_local_link_dest_path(), None);
1622
1623 let input = Link::Text2Dest(Cow::from("xyz"), Cow::from("dir/doc.md"), Cow::from("xyz"));
1625 let expected = Path::new("dir/doc.md");
1626 let res = input.get_local_link_dest_path().unwrap();
1627 assert_eq!(res, expected);
1628
1629 let input = Link::Text2Dest(Cow::from("xyz"), Cow::from("d#ir/doc.md"), Cow::from("xyz"));
1631 let expected = Path::new("d#ir/doc.md");
1632 let res = input.get_local_link_dest_path().unwrap();
1633 assert_eq!(res, expected);
1634
1635 let input = Link::Text2Dest(
1637 Cow::from("xyz"),
1638 Cow::from("dir/doc.md#1"),
1639 Cow::from("xyz"),
1640 );
1641 let expected = Path::new("dir/doc.md");
1642 let res = input.get_local_link_dest_path().unwrap();
1643 assert_eq!(res, expected);
1644 }
1645
1646 #[test]
1647 fn test_split_path_and_fragment() {
1648 use crate::html::split_path_and_fragment;
1649
1650 assert_eq!(
1653 split_path_and_fragment("Task #7/note.md"),
1654 ("Task #7/note.md", "")
1655 );
1656
1657 assert_eq!(
1660 split_path_and_fragment("note.md#anchor"),
1661 ("note.md", "#anchor")
1662 );
1663
1664 assert_eq!(
1667 split_path_and_fragment("Task #7/note.md#anchor"),
1668 ("Task #7/note.md", "#anchor")
1669 );
1670
1671 assert_eq!(split_path_and_fragment("dir/note.md"), ("dir/note.md", ""));
1673
1674 assert_eq!(split_path_and_fragment("#1"), ("", "#1"));
1676 }
1677
1678 #[test]
1679 fn test_percent_encode_path() {
1680 use crate::html::percent_encode_path;
1681 use percent_encoding::percent_decode_str;
1682
1683 for segment in [
1685 "Meeting #12-x",
1686 "a?b",
1687 "100%",
1688 "report %23.md",
1689 "with space",
1690 "a+b",
1691 "a&b",
1692 "em—dash",
1693 "a↔b",
1694 "already%20encoded",
1695 ] {
1696 let path = format!("/dir/{segment}/note.md");
1697 let encoded = percent_encode_path(&path);
1698 let decoded = percent_decode_str(&encoded).decode_utf8().unwrap();
1699 assert_eq!(decoded, path, "round-trip failed for segment {segment:?}");
1700 }
1701
1702 assert_eq!(
1705 percent_encode_path("/Meeting #12/note.md"),
1706 "/Meeting%20%2312/note.md"
1707 );
1708 assert_eq!(percent_encode_path("/a?b"), "/a%3Fb");
1709
1710 assert_eq!(percent_encode_path("/report %23.md"), "/report%20%2523.md");
1713 let encoded = percent_encode_path("/report %23.md");
1714 let decoded = percent_decode_str(&encoded).decode_utf8().unwrap();
1715 assert_eq!(decoded, "/report %23.md");
1716
1717 assert!(percent_encode_path("/a/b/c").starts_with('/'));
1719 assert_eq!(percent_encode_path("/a/b/c"), "/a/b/c");
1720 }
1721
1722 #[test]
1723 fn test_append_html_ext() {
1724 let mut input = Link::Text2Dest(
1726 Cow::from("abc"),
1727 Cow::from("/dir/3.0-My note.md"),
1728 Cow::from("title"),
1729 );
1730 let expected = Link::Text2Dest(
1731 Cow::from("abc"),
1732 Cow::from("/dir/3.0-My note.md.html"),
1733 Cow::from("title"),
1734 );
1735 input.append_html_ext();
1736 let output = input;
1737 assert_eq!(output, expected);
1738 }
1739
1740 #[test]
1741 fn test_to_html() {
1742 let input = Link::Text2Dest(
1744 Cow::from("te\\x/t"),
1745 Cow::from("de\\s/t"),
1746 Cow::from("ti\\t/le"),
1747 );
1748 let expected = "<a href=\"de/s/t\" title=\"ti\\t/le\">te\\x/t</a>";
1749 let output = input.to_html();
1750 assert_eq!(output, expected);
1751
1752 let input = Link::Text2Dest(
1754 Cow::from("te&> xt"),
1755 Cow::from("de&> st"),
1756 Cow::from("ti&> tle"),
1757 );
1758 let expected = "<a href=\"de&>%20st\" title=\"ti&> tle\">te&> xt</a>";
1759 let output = input.to_html();
1760 assert_eq!(output, expected);
1761
1762 let input = Link::Image(Cow::from("al&t"), Cow::from("sr&c"));
1764 let expected = "<img src=\"sr&c\" alt=\"al&t\">";
1765 let output = input.to_html();
1766 assert_eq!(output, expected);
1767
1768 let input = Link::Text2Dest(Cow::from("te&> xt"), Cow::from("de&> st"), Cow::from(""));
1770 let expected = "<a href=\"de&>%20st\">te&> xt</a>";
1771 let output = input.to_html();
1772 assert_eq!(output, expected);
1773 }
1774
1775 #[test]
1776 fn test_rewrite_links() {
1777 use crate::config::LocalLinkKind;
1778
1779 let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
1780 let input = "abc<a href=\"ftp://getreu.net\">Blog</a>\
1781 def<a href=\"https://getreu.net\">https://getreu.net</a>\
1782 ghi<img src=\"t m p.jpg\" alt=\"test 1\" />\
1783 jkl<a href=\"down/../down/my note 1.md\">my note 1</a>\
1784 mno<a href=\"http:./down/../dir/my note.md\">http:./down/../dir/my note.md</a>\
1785 pqr<a href=\"http:/down/../dir/my note.md\">\
1786 http:/down/../dir/my note.md</a>\
1787 stu<a href=\"http:/../dir/underflow/my note.md\">\
1788 not allowed dir</a>\
1789 vwx<a href=\"http:../../../not allowed dir/my note.md\">\
1790 not allowed</a>"
1791 .to_string();
1792 let expected = "abc<a href=\"ftp://getreu.net\">Blog</a>\
1793 def<a href=\"https://getreu.net\">getreu.net</a>\
1794 ghi<img src=\"/abs/note%20path/t%20m%20p.jpg\" alt=\"test 1\">\
1795 jkl<a href=\"/abs/note%20path/down/my%20note%201.md\">my note 1</a>\
1796 mno<a href=\"/abs/note%20path/dir/my%20note.md\">./down/../dir/my note.md</a>\
1797 pqr<a href=\"/dir/my%20note.md\">/down/../dir/my note.md</a>\
1798 stu<i><INVALID: /../dir/underflow/my note.md></i>\
1799 vwx<i><INVALID: ../../../not allowed dir/my note.md></i>"
1800 .to_string();
1801
1802 let root_path = Path::new("/my/");
1803 let docdir = Path::new("/my/abs/note path/");
1804 let output = rewrite_links(
1805 input,
1806 root_path,
1807 docdir,
1808 LocalLinkKind::Short,
1809 false,
1810 allowed_urls.clone(),
1811 );
1812 let url = allowed_urls.read_recursive();
1813
1814 assert!(url.contains(&PathBuf::from("/abs/note path/t m p.jpg")));
1815 assert!(url.contains(&PathBuf::from("/abs/note path/dir/my note.md")));
1816 assert!(url.contains(&PathBuf::from("/abs/note path/down/my note 1.md")));
1817 assert_eq!(output, expected);
1818 }
1819
1820 #[test]
1821 fn test_rewrite_links2() {
1822 use crate::config::LocalLinkKind;
1823
1824 let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
1825 let input = "abd<a href=\"tpnote:dir/my note.md\">\
1826 <img src=\"/imagedir/favicon-32x32.png\" alt=\"logo\"></a>abd"
1827 .to_string();
1828 let expected = "abd<a href=\"/abs/note%20path/dir/my%20note.md\">\
1829 <img src=\"/imagedir/favicon-32x32.png\" alt=\"logo\"></a>abd";
1830 let root_path = Path::new("/my/");
1831 let docdir = Path::new("/my/abs/note path/");
1832 let output = rewrite_links(
1833 input,
1834 root_path,
1835 docdir,
1836 LocalLinkKind::Short,
1837 false,
1838 allowed_urls.clone(),
1839 );
1840 let url = allowed_urls.read_recursive();
1841 println!("{:?}", allowed_urls.read_recursive());
1842 assert!(url.contains(&PathBuf::from("/abs/note path/dir/my note.md")));
1843 assert_eq!(output, expected);
1844 }
1845
1846 #[test]
1847 fn test_rewrite_links3() {
1848 use crate::config::LocalLinkKind;
1849
1850 let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
1851 let input = "abd<a href=\"#1\"></a>abd".to_string();
1852 let expected = "abd<a href=\"/abs/note%20path/#1\"></a>abd";
1853 let root_path = Path::new("/my/");
1854 let docdir = Path::new("/my/abs/note path/");
1855 let output = rewrite_links(
1856 input,
1857 root_path,
1858 docdir,
1859 LocalLinkKind::Short,
1860 false,
1861 allowed_urls.clone(),
1862 );
1863 let url = allowed_urls.read_recursive();
1864 println!("{:?}", allowed_urls.read_recursive());
1865 assert!(url.contains(&PathBuf::from("/abs/note path/")));
1866 assert_eq!(output, expected);
1867 }
1868
1869 #[test]
1875 fn test_rewrite_links_hash_in_dir_name() {
1876 use crate::config::LocalLinkKind;
1877
1878 let allowed_urls = Arc::new(RwLock::new(HashSet::new()));
1879 let input = "<a href=\"01-Agenda.md\">link</a>".to_string();
1880 let root_path = Path::new("/notes/");
1881 let docdir = Path::new("/notes/Meeting #12-Project kickoff/");
1882 let output = rewrite_links(
1883 input,
1884 root_path,
1885 docdir,
1886 LocalLinkKind::Short,
1887 false,
1888 allowed_urls.clone(),
1889 );
1890
1891 assert!(
1894 output.contains("href=\"/Meeting%20%2312-Project%20kickoff/01-Agenda.md\""),
1895 "unexpected output: {output}"
1896 );
1897 assert!(!output.contains("Meeting #12"));
1899
1900 let url = allowed_urls.read_recursive();
1905 assert!(url.contains(&PathBuf::from(
1906 "/Meeting #12-Project kickoff/01-Agenda.md"
1907 )));
1908 }
1909
1910 #[test]
1911 fn test_is_empty_html() {
1912 use crate::html::HtmlStr;
1914
1915 assert!(String::from("<!DOCTYPE html>").is_empty_html());
1918
1919 assert!(!String::from("<!DOCTYPE html>>").is_empty_html());
1921
1922 assert!(
1925 String::from(
1926 " <!DOCTYPE HTML PUBLIC \
1927 \"-//W3C//DTD HTML 4.01 Transitional//EN\" \
1928 \"http://www.w3.org/TR/html4/loose.dtd\">"
1929 )
1930 .is_empty_html()
1931 );
1932
1933 assert!(
1936 String::from(
1937 " <!DOCTYPE html PUBLIC \
1938 \"-//W3C//DTD XHTML 1.1//EN\" \
1939 \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">"
1940 )
1941 .is_empty_html()
1942 );
1943
1944 assert!(!String::from("<!DOCTYPE html>Some content").is_empty_html());
1946
1947 assert!(String::from("").is_empty_html());
1949
1950 assert!(!String::from("<html></html>").is_empty_html());
1953
1954 assert!(!String::from("<!DOCTYPE html><html></html>").is_empty_html());
1957 }
1958
1959 #[test]
1960 fn test_has_html_start_tag() {
1961 use crate::html::HtmlStr;
1963
1964 assert!(String::from("<!DOCTYPE html>Some content").has_html_start_tag());
1966
1967 assert!(!String::from("<html>Some content</html>").has_html_start_tag());
1970
1971 assert!(!String::from("<HTML>").has_html_start_tag());
1974
1975 assert!(String::from(" <!doctype html>Some content").has_html_start_tag());
1977
1978 assert!(!String::from("<!DOCTYPE other>").has_html_start_tag());
1980
1981 assert!(!String::from("").has_html_start_tag());
1983 }
1984
1985 #[test]
1986 fn test_is_html_unchecked() {
1987 use crate::html::HtmlStr;
1989
1990 let html = "<!doctype html>";
1992 assert!(html.is_html_unchecked());
1993
1994 let html = "<!doctype html abc>def";
1996 assert!(html.is_html_unchecked());
1997
1998 let html = "<!doctype html";
2000 assert!(!html.is_html_unchecked());
2001
2002 let html = "<html><body></body></html>";
2004 assert!(html.is_html_unchecked());
2005
2006 let html = "<html abc>def";
2008 assert!(html.is_html_unchecked());
2009
2010 let html = "<html abc def";
2012 assert!(!html.is_html_unchecked());
2013
2014 let html = " <!doctype html><html><body></body></html>";
2016 assert!(html.is_html_unchecked());
2017
2018 let html = "<!DOCTYPE xml><root></root>";
2020 assert!(!html.is_html_unchecked());
2021
2022 let html = "<!doctype>";
2024 assert!(!html.is_html_unchecked());
2025 }
2026
2027 #[test]
2028 fn test_prepend_html_start_tag() {
2029 use crate::html::HtmlString;
2031
2032 assert_eq!(
2034 String::from("<!DOCTYPE html>Some content").prepend_html_start_tag(),
2035 Ok(String::from("<!DOCTYPE html>Some content"))
2036 );
2037
2038 assert_eq!(
2040 String::from("<!DOCTYPE html>").prepend_html_start_tag(),
2041 Ok(String::from("<!DOCTYPE html>"))
2042 );
2043
2044 assert_eq!(
2046 String::from("<html>Some content").prepend_html_start_tag(),
2047 Ok(String::from("<!DOCTYPE html><html>Some content"))
2048 );
2049
2050 assert_eq!(
2052 String::from("<!DOCTYPE other>").prepend_html_start_tag(),
2053 Err(InputStreamError::NonHtmlDoctype {
2054 html: "<!DOCTYPE other>".to_string()
2055 })
2056 );
2057
2058 assert_eq!(
2060 String::from("Some content").prepend_html_start_tag(),
2061 Ok(String::from("<!DOCTYPE html>Some content"))
2062 );
2063
2064 assert_eq!(
2066 String::from("").prepend_html_start_tag(),
2067 Ok(String::from("<!DOCTYPE html>"))
2068 );
2069 }
2070}