//! Cluster 8b — multi-line list-item continuation. //! //! Lines indented strictly past the item's marker attach to the item //! rather than becoming a sibling paragraph. Mirrors upstream vimwiki //! + Markdown. use nuwiki_core::ast::{BlockNode, InlineNode}; use nuwiki_core::render::{HtmlRenderer, Renderer}; use nuwiki_core::syntax::vimwiki::VimwikiSyntax; use nuwiki_core::syntax::SyntaxPlugin; fn parse(src: &str) -> nuwiki_core::ast::DocumentNode { VimwikiSyntax::new().parse(src) } fn single_list(doc: &nuwiki_core::ast::DocumentNode) -> &nuwiki_core::ast::ListNode { match &doc.children[0] { BlockNode::List(l) => l, other => panic!("expected list, got {other:?}"), } } fn text_of(inlines: &[InlineNode]) -> String { let mut out = String::new(); for n in inlines { match n { InlineNode::Text(t) => out.push_str(&t.content), InlineNode::Bold(b) => out.push_str(&text_of(&b.children)), InlineNode::Italic(i) => out.push_str(&text_of(&i.children)), _ => {} } } out } #[test] fn indented_line_after_marker_continues_the_item() { let doc = parse("- first item\n continuing here\n- second\n"); assert_eq!(doc.children.len(), 1, "should be a single list block"); let list = single_list(&doc); assert_eq!(list.items.len(), 2); let first = &list.items[0]; let joined = text_of(&first.children); assert_eq!(joined, "first item continuing here"); let second = &list.items[1]; assert_eq!(text_of(&second.children), "second"); } #[test] fn two_continuation_lines_both_attach() { let doc = parse("- top\n middle\n bottom\n"); let list = single_list(&doc); assert_eq!(list.items.len(), 1); let joined = text_of(&list.items[0].children); assert_eq!(joined, "top middle bottom"); } #[test] fn unindented_line_breaks_the_list() { let doc = parse("- item\nflush against margin\n"); // Two blocks: a list (just `- item`) and a paragraph. assert!( doc.children.len() >= 2, "want list + paragraph, got {}", doc.children.len() ); match &doc.children[0] { BlockNode::List(l) => { assert_eq!(text_of(&l.items[0].children), "item"); } other => panic!("expected list first, got {other:?}"), } } #[test] fn blank_line_breaks_the_list_item() { // A blank line ends the current list (paragraph semantics). let doc = parse("- item\n\n orphaned\n"); let list = single_list(&doc); assert_eq!(list.items.len(), 1); assert_eq!(text_of(&list.items[0].children), "item"); } #[test] fn continuation_works_for_nested_lists_too() { let doc = parse("- parent\n - child\n continued child\n- sibling\n"); let list = single_list(&doc); assert_eq!(list.items.len(), 2); let parent = &list.items[0]; let sublist = parent.sublist.as_ref().expect("nested list"); assert_eq!(sublist.items.len(), 1); let child = &sublist.items[0]; assert_eq!(text_of(&child.children), "child continued child"); } #[test] fn html_renderer_emits_a_single_li_per_item() { let doc = parse("- first item\n continuing here\n- second\n"); let out = HtmlRenderer::new().render_to_string(&doc).unwrap(); // Single
. assert_eq!(out.matches("
"), "no orphan paragraph; got: {out}"); assert!(out.contains("first item continuing here"), "got: {out}"); }