160 lines
4.7 KiB
Rust
160 lines
4.7 KiB
Rust
//! Pasting a URL over selected text makes a markdown link; every other paste
|
|
//! stays a plain paste.
|
|
|
|
use notescraft_editor::keys::{self, Edit};
|
|
|
|
fn applied(text: &str, edit: &Edit) -> String {
|
|
let mut out = String::from(text);
|
|
out.replace_range(edit.range.clone(), &edit.insert);
|
|
out
|
|
}
|
|
|
|
fn link(text: &str, range: std::ops::Range<usize>, pasted: &str) -> Option<String> {
|
|
keys::on_paste_url(text, range, pasted).map(|edit| applied(text, &edit))
|
|
}
|
|
|
|
const URL: &str = "https://example.com";
|
|
|
|
#[test]
|
|
fn a_plain_word_becomes_a_link() {
|
|
let text = "hello world";
|
|
assert_eq!(link(text, 6..11, URL), Some(String::from("hello [world](https://example.com)")));
|
|
}
|
|
|
|
#[test]
|
|
fn a_phrase_becomes_a_link() {
|
|
let text = "the quick brown fox";
|
|
assert_eq!(link(text, 4..15, URL), Some(String::from("the [quick brown](https://example.com) fox")));
|
|
}
|
|
|
|
#[test]
|
|
fn the_cursor_ends_after_the_closing_paren() {
|
|
let text = "hello world";
|
|
let edit = keys::on_paste_url(text, 6..11, URL).unwrap();
|
|
assert_eq!(edit.sel, 34..34);
|
|
assert_eq!(edit.range, 6..11);
|
|
}
|
|
|
|
#[test]
|
|
fn brackets_in_the_selection_are_escaped() {
|
|
let text = "a [b] c";
|
|
assert_eq!(link(text, 0..7, URL), Some(String::from("[a \\[b\\] c](https://example.com)")));
|
|
}
|
|
|
|
#[test]
|
|
fn a_wrapped_selection_keeps_its_label() {
|
|
let text = "[label](old)";
|
|
assert_eq!(link(text, 0..12, URL), Some(String::from("[label](https://example.com)")));
|
|
let text = "[label]";
|
|
assert_eq!(link(text, 0..7, URL), Some(String::from("[label](https://example.com)")));
|
|
}
|
|
|
|
#[test]
|
|
fn an_angle_bracketed_url_is_unwrapped() {
|
|
let text = "hello world";
|
|
assert_eq!(link(text, 6..11, "<https://example.com>"), Some(String::from("hello [world](https://example.com)")));
|
|
}
|
|
|
|
#[test]
|
|
fn other_schemes_are_refused() {
|
|
let text = "hello";
|
|
assert!(link(text, 0..5, "javascript:alert(1)").is_none());
|
|
assert!(link(text, 0..5, "file:///etc/passwd").is_none());
|
|
assert!(link(text, 0..5, "mailto:me@example.com").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_url_with_spaces_is_refused() {
|
|
let text = "hello";
|
|
assert!(link(text, 0..5, "https://example.com/a b").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_host_without_a_dot_is_refused() {
|
|
let text = "hello";
|
|
assert!(link(text, 0..5, "http://example").is_none());
|
|
assert!(link(text, 0..5, "http://localhost:8080/x").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn the_scheme_is_case_insensitive() {
|
|
let text = "hello";
|
|
assert!(link(text, 0..5, "HTTPS://EXAMPLE.COM").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn a_url_over_2048_bytes_is_refused() {
|
|
let text = "hello";
|
|
let fits = format!("https://example.com/{}", "x".repeat(2028));
|
|
assert_eq!(fits.len(), 2048);
|
|
assert!(link(text, 0..5, &fits).is_some());
|
|
let too_long = format!("https://example.com/{}", "x".repeat(2029));
|
|
assert!(link(text, 0..5, &too_long).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_selection_is_refused() {
|
|
let text = "hello world";
|
|
assert!(link(text, 6..6, URL).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_inside_a_fenced_block_is_refused() {
|
|
let text = "```rust\nfn main() {}\n```";
|
|
let start = text.find("fn main").unwrap();
|
|
assert!(link(text, start..start + 9, URL).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_inside_inline_code_is_refused() {
|
|
let text = "a `code` b";
|
|
let start = text.find("code").unwrap();
|
|
assert!(link(text, start..start + 4, URL).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_inside_a_link_is_refused() {
|
|
let text = "[label](https://example.com)";
|
|
let start = text.find("label").unwrap();
|
|
assert!(link(text, start..start + 5, URL).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_multi_line_selection_is_refused() {
|
|
let text = "one\ntwo";
|
|
assert!(link(text, 0..7, URL).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_over_500_bytes_is_refused() {
|
|
let text = "x".repeat(501);
|
|
assert!(link(&text, 0..501, URL).is_none());
|
|
let text = "x".repeat(500);
|
|
assert!(link(&text, 0..500, URL).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_that_is_a_url_pastes_normally() {
|
|
let text = "https://example.com";
|
|
assert!(link(text, 0..19, URL).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_offsets_are_kept() {
|
|
let text = "caf\u{e9} na\u{ef}ve";
|
|
let start = text.find("na\u{ef}ve").unwrap();
|
|
let (head, tail) = text.split_at_checked(start).unwrap();
|
|
assert_eq!(head, "caf\u{e9} ");
|
|
assert_eq!(tail, "na\u{ef}ve");
|
|
assert_eq!(link(text, start..start + tail.len(), URL), Some(String::from("caf\u{e9} [na\u{ef}ve](https://example.com)")));
|
|
}
|
|
|
|
#[test]
|
|
fn a_second_paste_changes_nothing() {
|
|
let text = "hello world";
|
|
let edit = keys::on_paste_url(text, 6..11, URL).unwrap();
|
|
let linked = applied(text, &edit);
|
|
assert_eq!(keys::on_paste_url(text, 6..11, URL), Some(edit.clone()));
|
|
assert!(keys::on_paste_url(&linked, edit.sel.clone(), URL).is_none());
|
|
}
|