use super::*; fn at(text: &str, needle: &str) -> Result { let byte = text.find(needle).unwrap(); let prefix = &text[..byte]; let row = prefix.bytes().filter(|byte| *byte == b'\n').count(); let column = prefix.rsplit('\n').next().unwrap().graphemes(true).count(); link_at(text, TextPosition { row, column }) } fn target(path: Option<&str>, fragment: Option<&str>) -> LinkTarget { LinkTarget { path: path.map(PathBuf::from), fragment: fragment.map(str::to_owned), } } #[test] fn follows_inline_labels_destinations_and_grapheme_columns() { let text = "👩‍💻 [go **there**](<../My%20Notes/%E6%97%A5%E6%9C%AC\\(1\\).md#caf%C3%A9>)"; let expected = target(Some("../My Notes/日本(1).md"), Some("café")); assert_eq!(at(text, "there").unwrap(), expected); assert_eq!(at(text, "My%20").unwrap(), expected); assert_eq!( at("[absolute](/Tmp/Note.md)", "absolute").unwrap(), target(Some("/Tmp/Note.md"), None) ); assert_eq!( at("[here](#title)", "here").unwrap(), target(None, Some("title")) ); assert_eq!(at("[top]()", "top").unwrap(), target(None, None)); assert_eq!( at("[here](#title)", "[here").unwrap(), target(None, Some("title")) ); assert_eq!( at("[file](literal%23name.md)", "file").unwrap(), target(Some("literal#name.md"), None) ); assert_eq!( at("[file](My\\[Notes\\].md)", "file").unwrap(), target(Some("My[Notes].md"), None) ); } #[test] fn resolves_all_reference_forms_and_multiline_block_definitions() { let text = "[full][ A B ] [a b][] [A B]\n\n> [a b]:\n> \n> \"A title\"\n\n[a b]: ignored.md\n"; let expected = target(Some("My Notes.md"), Some("title")); for needle in ["full", " A B ", "a b][]", "A B]"] { assert_eq!(at(text, needle).unwrap(), expected); } assert_eq!( at("[Straße]\n\n[STRASSE]: file.md", "Straße").unwrap(), target(Some("file.md"), None) ); let text = "> [multi\n> line][target]\n\n[target]: other.md\n"; assert_eq!(at(text, "line]").unwrap(), target(Some("other.md"), None)); } #[test] fn rejects_images_code_plaintext_and_external_urls() { for (text, needle) in [ ("![image](file.md)", "image"), ("`[code](file.md)`", "code"), ("```md\n[code](file.md)\n```", "file"), (" [code](file.md)", "code"), ("plain file.md text", "file"), ("[web](https://example.com)", "web"), ("[web](//example.com/a)", "web"), ("[web](mailto:a@example.com)", "web"), ("", "example"), ] { assert!(at(text, needle).is_err(), "{text}"); } assert!( at("[missing]", "missing") .unwrap_err() .contains("not defined") ); assert!( at("[bad](file%ZZ.md)", "bad") .unwrap_err() .contains("percent") ); assert!( at("[bad](file%FF.md)", "bad") .unwrap_err() .contains("UTF-8") ); assert!(at("[bad](#)", "bad").unwrap_err().contains("empty")); } #[test] fn locates_visible_unicode_headings_setext_and_unique_slugs() { let text = "# **Café** [日本](destination.md) `code` ###\n\n> ## Café 日本 code\n\nCafé 日本 code\n====\n\n# Café 日本 code-1\n\n# Cafe\u{301} _name_ ![alt](hidden.md)\n\n```md\n# Hidden\n```\n"; assert_eq!(heading_row(text, "café-日本-code").unwrap(), 0); assert_eq!(heading_row(text, "café-日本-code-1").unwrap(), 2); assert_eq!(heading_row(text, "café-日本-code-2").unwrap(), 4); assert_eq!(heading_row(text, "café-日本-code-1-1").unwrap(), 7); assert_eq!(heading_row(text, "cafe\u{301}-name-alt").unwrap(), 9); assert!(heading_row(text, "hidden").is_err()); assert!(heading_row(text, "destinationmd").is_err()); assert!(heading_row(text, "").is_err()); assert_eq!( heading_row("# [Heading][ref]\n\n[ref]: ignored.md", "heading").unwrap(), 0 ); assert_eq!( heading_row("# ![Alt][ref]\n\n[ref]: ignored.md", "alt").unwrap(), 0 ); assert_eq!(heading_row("# ![Alt][missing]", "altmissing").unwrap(), 0); } #[test] fn renders_entities_or_reports_the_explicit_entity_ceiling() { assert_eq!( at("[file](a&b c.md)", "file").unwrap(), target(Some("a&b c.md"), None) ); assert_eq!( at("[file](<name>)", "file").unwrap(), target(Some(""), None) ); assert!( at("[file](a©b.md)", "file") .unwrap_err() .contains("Unsupported Markdown HTML entity") ); assert_eq!(heading_row("# A & B é\n", "a--b-é").unwrap(), 0); assert!( heading_row("# ©\n", "copy") .unwrap_err() .contains("Unsupported Markdown HTML entity") ); assert_eq!(heading_row("# ©\n# Valid\n", "valid").unwrap(), 1); assert_eq!( heading_row("> First\n> Second\n> ------\n", "first-second").unwrap(), 0 ); }