//! Block parsing, block operations and component inserts. use notescraft_blocks::{ BlockKind as K, Component, Direction, Target, block_at, delete_block, duplicate_block, insert_component, move_block, parse_blocks, turn_into, }; fn kinds(text: &str) -> Vec { parse_blocks(text).into_iter().map(|b| b.kind).collect() } fn slice(text: &str, i: usize) -> &str { &text[parse_blocks(text)[i].range.clone()] } #[test] fn every_kind_is_parsed() { let text = "---\ntitle: x\n---\n\n# Head\n\npara\nline two\n\n> quote\n\n> [!note] T\n> body\n\n- [ ] todo\n- [x] done\n- item\n1. one\n\n| a | b |\n| - | - |\n\n```rust\nfn x() {}\n```\n\n---\n\n![[note]]\n"; assert_eq!( kinds(text), vec![ K::Frontmatter, K::Heading(1), K::Paragraph, K::Quote, K::Callout, K::TaskItem { checked: false }, K::TaskItem { checked: true }, K::ListItem { ordered: false }, K::ListItem { ordered: true }, K::Table, K::CodeFence, K::Divider, K::Embed ] ); assert_eq!(slice(text, 2), "para\nline two"); } #[test] fn fence_with_slash_hash_and_blank_lines_is_one_block() { let text = "intro\n\n```\n# not heading\n/ not slash\n\n- not list\n```\n\nafter"; assert_eq!(kinds(text), vec![K::Paragraph, K::CodeFence, K::Paragraph]); let b = &parse_blocks(text)[1]; assert_eq!(&text[b.text_range.clone()], "# not heading\n/ not slash\n\n- not list\n"); } #[test] fn unclosed_fence_runs_to_end() { let text = "```\nabc\n\ndef"; assert_eq!(kinds(text), vec![K::CodeFence]); assert_eq!(parse_blocks(text)[0].range, 0..text.len()); } #[test] fn list_item_keeps_children() { let text = "- a\n - b\n\n more\n- c"; assert_eq!(parse_blocks(text).len(), 2); assert_eq!(slice(text, 0), "- a\n - b\n\n more"); assert_eq!(slice(text, 1), "- c"); } #[test] fn text_ranges_skip_markers() { let text = "## Hi\n\n- [x] done\n\n> [!tip] t"; let bs = parse_blocks(text); assert_eq!(&text[bs[0].text_range.clone()], "Hi"); assert_eq!(&text[bs[1].text_range.clone()], "done"); assert_eq!(&text[bs[2].text_range.clone()], "[!tip] t"); } #[test] fn block_at_finds_block() { let text = "one\n\ntwo"; assert_eq!(block_at(text, 0), Some(0)); assert_eq!(block_at(text, 3), Some(0)); assert_eq!(block_at(text, 4), None); assert_eq!(block_at(text, 5), Some(1)); } #[test] fn move_round_trip_is_identity_and_single_replace() { let text = "# T\n\npara\n\n- a\n- b\n child\n\n```\ncode\n```\n\n> q"; let n = parse_blocks(text).len(); for i in 0..n - 1 { let down = move_block(text, i, Direction::Down).unwrap(); let (range, new) = &down.replace; let mut applied = text.to_owned(); applied.replace_range(range.clone(), new); assert_eq!(applied, down.text, "replace reproduces text"); assert_ne!(down.text, text); let back = move_block(&down.text, i + 1, Direction::Up).unwrap(); assert_eq!(back.text, text, "round trip at {i}"); } } #[test] fn move_keeps_tight_list_and_blank_separator() { let text = "a\n\nb\nc\n\nd"; let up = move_block(text, 1, Direction::Up).unwrap(); assert_eq!(up.text, "b\nc\n\na\n\nd"); assert_eq!(up.caret, 0); let down = move_block(text, 0, Direction::Down).unwrap(); assert_eq!(&down.text[down.caret..=down.caret], "a"); } #[test] fn move_edges_and_frontmatter_error() { assert!(move_block("a\n\nb", 0, Direction::Up).is_err()); assert!(move_block("a\n\nb", 1, Direction::Down).is_err()); assert!(move_block("a", 5, Direction::Down).is_err()); assert!(move_block("---\na: 1\n---\n\nbody", 1, Direction::Up).is_err()); } #[test] fn duplicate_and_delete() { let d = duplicate_block("a\n\nb", 0).unwrap(); assert_eq!(d.text, "a\n\na\n\nb"); assert_eq!(d.caret, 3); let d = duplicate_block("- a\n- b", 0).unwrap(); assert_eq!(d.text, "- a\n- a\n- b"); assert_eq!(delete_block("a\n\nb\n\nc", 1).unwrap().text, "a\n\nc"); assert_eq!(delete_block("a\n\nb", 1).unwrap().text, "a"); assert_eq!(delete_block("a", 0).unwrap().text, ""); assert!(delete_block("a", 1).is_err()); } #[test] fn turn_into_keeps_text() { let p = turn_into("hello world", 0, &Target::Heading(2)).unwrap(); assert_eq!(p.text, "## hello world"); let q = turn_into(&p.text, 0, &Target::Quote).unwrap(); assert_eq!(q.text, "> hello world"); let t = turn_into(&q.text, 0, &Target::Task).unwrap(); assert_eq!(t.text, "- [ ] hello world"); let c = turn_into(&t.text, 0, &Target::Callout("warning".into())).unwrap(); assert_eq!(c.text, "> [!warning]\n> hello world"); let back = turn_into(&c.text, 0, &Target::Paragraph).unwrap(); assert_eq!(back.text, "hello world"); assert_eq!(back.caret, back.text.len()); } #[test] fn turn_into_list_keeps_indent_and_children() { let t = turn_into(" - item\n child", 0, &Target::Task).unwrap(); assert_eq!(t.text, " - [ ] item\n child"); let p = turn_into("- [x] item\n child", 0, &Target::Paragraph).unwrap(); assert_eq!(p.text, "item\n child"); } #[test] fn turn_into_refuses_unsupported() { assert!(turn_into("```\nx\n```", 0, &Target::Paragraph).is_err()); assert!(turn_into("a", 0, &Target::Heading(7)).is_err()); assert!(turn_into("a", 3, &Target::Quote).is_err()); } #[test] fn components() { let t = insert_component("ab", 2, &Component::Table).unwrap(); assert!(t.text.starts_with("ab\n| | |\n| --- | --- |")); assert_eq!(&t.text[..t.caret], "ab\n| "); assert_eq!(kinds(&t.text), vec![K::Paragraph, K::Table]); let c = insert_component("", 0, &Component::Callout).unwrap(); assert_eq!(c.text, "> [!note]\n> "); assert_eq!(c.caret, c.text.len()); for comp in [Component::Calendar, Component::Diagram, Component::Whiteboard, Component::Template("Daily".into())] { let e = insert_component("x\n\ny", 3, &comp).unwrap(); assert!(e.text.is_char_boundary(e.caret)); assert!(e.text.ends_with("\ny")); } let d = insert_component("", 0, &Component::Diagram).unwrap(); assert_eq!(kinds(&d.text), vec![K::CodeFence]); assert!(d.text[..d.caret].ends_with("A --> B")); } #[test] fn hostile_inputs_never_panic() { assert!(insert_component("é", 1, &Component::Table).is_err()); assert!(insert_component("a", 9, &Component::Table).is_err()); for text in ["", "\n", "é\n\n日本語\n- é\n 日", "```", "~~~\n", "---", "---\n", "> ", "|", "- [", "- [x", "![[", "![[]]", "#", "\r\n\r\n- a\r\n b\r\n"] { let n = parse_blocks(text).len(); for i in 0..=n { let _ = move_block(text, i, Direction::Up); let _ = move_block(text, i, Direction::Down); let _ = duplicate_block(text, i); let _ = delete_block(text, i); let _ = turn_into(text, i, &Target::Heading(3)); } for o in 0..=text.len() + 1 { let _ = block_at(text, o); let _ = insert_component(text, o, &Component::Callout); } } } #[test] fn hundred_thousand_lines() { let text: String = (0..100_000).map(|i| if i % 3 == 0 { format!("- item {i}\n child\n") } else { format!("line {i}\n\n") }).collect(); let bs = parse_blocks(&text); assert!(bs.len() > 50_000); let blanks = "\n".repeat(100_000); let t = format!("- a\n{blanks}x"); assert_eq!(parse_blocks(&t).len(), 2); }