use super::*; use crate::sexpr; fn parse(source: &str) -> Result, Diagnostic> { let doc = sexpr::read(source).expect("reads"); let form = &doc.values[0]; parse_rich_form(form.as_list().expect("list"), form.span()).expect("a rich form") } #[test] fn nesting_is_the_structure() { let runs = parse(r##"(rich "Hello " (bold "World") (color "#ff00ff" "!"))"##).unwrap(); assert_eq!(runs.len(), 3); assert_eq!(runs[0].text, "Hello "); assert!(!runs[0].bold); assert_eq!(runs[1].text, "World"); assert!(runs[1].bold); assert_eq!(runs[2].text, "!"); assert_eq!(runs[2].color, Some((0xff, 0x00, 0xff, 0xff))); } #[test] fn styles_compose_by_nesting() { // The reason nesting is worth having: neither form knows about the other. let runs = parse(r##"(rich (bold (italic (color "#ff0000" "loud"))))"##).unwrap(); assert_eq!(runs.len(), 1); assert!(runs[0].bold && runs[0].italic); assert_eq!(runs[0].color, Some((0xff, 0, 0, 0xff))); } #[test] fn a_style_covers_everything_inside_it() { let runs = parse(r##"(rich (bold "one" (italic "two") "three"))"##).unwrap(); assert_eq!(runs.len(), 3); assert!(runs.iter().all(|r| r.bold), "all of it is inside the bold"); assert_eq!( runs.iter().filter(|r| r.italic).count(), 1, "only the nested part is italic" ); } #[test] fn size_and_the_rest() { let runs = parse(r##"(rich (size 20 (underline (strike "x"))))"##).unwrap(); assert_eq!(runs[0].size, Some(20.0)); assert!(runs[0].underline && runs[0].strikethrough); } #[test] fn a_link_is_a_style_form_like_any_other() { let runs = parse(r##"(rich "See the " (link "home" "home page") " for more.")"##).unwrap(); assert_eq!(runs.len(), 3); assert_eq!(runs[0].link, None, "prose before the link"); assert_eq!(runs[1].text, "home page"); assert_eq!(runs[1].link.as_deref(), Some("home")); assert_eq!(runs[2].link, None, "prose after it"); } #[test] fn a_link_composes_with_the_styles_by_nesting() { // The point of `link` being a style form: neither it nor `bold` knows the // other exists, in either order. for source in [ r##"(rich (link "home" (bold "Home")))"##, r##"(rich (bold (link "home" "Home")))"##, ] { let runs = parse(source).unwrap(); assert_eq!(runs.len(), 1, "{source}"); assert!(runs[0].bold, "{source}"); assert_eq!(runs[0].link.as_deref(), Some("home"), "{source}"); } } #[test] fn a_link_covers_everything_inside_it() { let runs = parse(r##"(rich (link "faq" "the " (italic "FAQ")))"##).unwrap(); assert_eq!(runs.len(), 2); assert!( runs.iter().all(|r| r.link.as_deref() == Some("faq")), "all of it is inside the link", ); } #[test] fn malformed_rich_text_is_pointed() { for (source, want) in [ (r##"(rich)"##, "needs some text"), (r##"(rich (bold))"##, "no text in it"), (r##"(rich (blink "x"))"##, "not a rich-text style"), // A link with no target at all… (r##"(rich (link))"##, "takes a target first"), // …and one whose target is not a string. Both point at a span: the // form when nothing was written where the target belongs, what *was* // written when something was. (r##"(rich (link 3 "x"))"##, "takes a target first"), // A target but no text is the same case as `(bold)`. (r##"(rich (link "home"))"##, "no text in it"), (r##"(rich (color "nope" "x"))"##, "is not a color"), // A string first *is* read as the colour, so this is the no-colour case. (r##"(rich (color 3 "x"))"##, "takes a color first"), (r##"(rich (color "#fff"))"##, "no text in it"), (r##"(rich (size "big" "x"))"##, "takes a number first"), (r##"(rich 3)"##, "a string or a style form"), ] { let error = parse(source).expect_err(source); assert!( error.message.contains(want), "`{source}`: wanted {want:?}, got: {}", error.message ); } } #[test] fn a_bad_link_target_points_at_the_target() { // A diagnostic that points at the form when the mistake is *in* the form // sends the reader looking. `(link 3 "x")` underlines the `3`. let source = r##"(rich (link 3 "x"))"##; let error = parse(source).expect_err(source); assert_eq!( &source[error.span.start..error.span.end], "3", "the offending target is what is underlined", ); // With nothing written where the target belongs, the form itself is the // only honest thing to point at. let source = r##"(rich (link))"##; let error = parse(source).expect_err(source); assert_eq!(&source[error.span.start..error.span.end], "(link)"); }