use text_document::{MoveMode, TextDocument};
fn new_doc_with_markdown(md: &str) -> TextDocument {
let doc = TextDocument::new();
doc.set_markdown(md).unwrap().wait().unwrap();
doc
}
#[test]
fn wrap_current_block_in_blockquote_then_export_has_quote_prefix() {
let doc = new_doc_with_markdown("A plain paragraph.\n");
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
assert!(!cursor.is_in_blockquote(), "precondition: not in a quote");
cursor.wrap_selection_in_blockquote().unwrap();
assert!(
cursor.is_in_blockquote(),
"after wrap, the cursor's block must be inside a blockquote"
);
let md = doc.to_markdown().unwrap();
assert!(
md.contains("> A plain paragraph"),
"exported markdown must carry the `>` prefix; got: {md:?}"
);
assert!(
!md.replace("> ", "")
.contains("A plain paragraph\\.\n\nA plain paragraph"),
"block must not appear twice; got: {md:?}"
);
}
#[test]
fn toggle_blockquote_round_trips() {
let doc = new_doc_with_markdown("Hello.\n");
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
assert!(!cursor.is_in_blockquote());
cursor.toggle_blockquote().unwrap();
assert!(cursor.is_in_blockquote(), "toggle on a plain block wraps");
cursor.toggle_blockquote().unwrap();
assert!(!cursor.is_in_blockquote(), "toggle inside a quote unwraps");
let md = doc.to_markdown().unwrap();
assert!(
!md.contains('>'),
"after toggle off, no `>` prefix should remain; got: {md:?}"
);
}
#[test]
fn increase_then_decrease_blockquote_depth_round_trips() {
let doc = new_doc_with_markdown("Hello.\n");
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
assert_eq!(cursor.blockquote_depth_at_cursor(), 0);
cursor.increase_blockquote_depth().unwrap();
assert_eq!(
cursor.blockquote_depth_at_cursor(),
1,
"first increase yields depth 1"
);
cursor.increase_blockquote_depth().unwrap();
assert_eq!(
cursor.blockquote_depth_at_cursor(),
2,
"second increase yields nested depth 2"
);
cursor.decrease_blockquote_depth().unwrap();
assert_eq!(
cursor.blockquote_depth_at_cursor(),
1,
"first decrease drops to depth 1"
);
cursor.decrease_blockquote_depth().unwrap();
assert_eq!(
cursor.blockquote_depth_at_cursor(),
0,
"second decrease drops to plain"
);
}
#[test]
fn unwrap_current_frame_lifts_block_to_parent() {
let doc = new_doc_with_markdown("> Quoted line.\n");
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
assert!(cursor.is_in_blockquote());
cursor.unwrap_current_frame().unwrap();
assert!(!cursor.is_in_blockquote(), "unwrap removes the quote frame");
let md = doc.to_markdown().unwrap();
assert!(
!md.contains('>'),
"markdown after unwrap must not contain a `>`; got: {md:?}"
);
assert!(md.contains("Quoted line"));
}
#[test]
fn wrap_then_unwrap_round_trips_to_identical_markdown() {
let original = "First.\n\nSecond.\n";
let doc = new_doc_with_markdown(original);
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
cursor.wrap_selection_in_blockquote().unwrap();
cursor.unwrap_current_frame().unwrap();
let md = doc.to_markdown().unwrap();
assert!(
md.contains("First") && md.contains("Second") && !md.contains('>'),
"round-trip wrap+unwrap must restore plain content; got: {md:?}"
);
}
#[test]
fn wrap_inside_existing_blockquote_creates_nested_depth() {
let doc = new_doc_with_markdown("> Outer line.\n");
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
assert_eq!(cursor.blockquote_depth_at_cursor(), 1);
cursor.wrap_selection_in_blockquote().unwrap();
assert_eq!(
cursor.blockquote_depth_at_cursor(),
2,
"wrapping inside an existing quote produces a nested quote"
);
let md = doc.to_markdown().unwrap();
assert!(
md.contains("> >") || md.contains(">>"),
"nested quote must export with two `>` levels; got: {md:?}"
);
}
#[test]
fn insert_block_inside_quote_produces_clean_markdown() {
let doc = TextDocument::new();
doc.set_markdown("> A\n").unwrap().wait().unwrap();
let cursor = doc.cursor_at(0);
cursor.set_position(1, MoveMode::MoveAnchor);
cursor.insert_block().unwrap();
let md = doc.to_markdown().unwrap();
assert!(
md.contains("> A"),
"exported markdown must keep `> A`; got {md:?}"
);
assert!(
!md.contains("\n\n\n"),
"no more than two consecutive newlines between blocks; got {md:?}"
);
}
#[test]
fn enter_enter_at_end_of_depth3_quote_keeps_c_above_the_new_empty() {
let doc = TextDocument::new();
doc.set_markdown("> block A\n> > block B\n> > > block C\n")
.unwrap()
.wait()
.unwrap();
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
for _ in 0..3 {
cursor.move_position(
text_document::MoveOperation::EndOfBlock,
MoveMode::MoveAnchor,
1,
);
cursor.move_position(
text_document::MoveOperation::NextBlock,
MoveMode::MoveAnchor,
1,
);
}
cursor.move_position(
text_document::MoveOperation::EndOfBlock,
MoveMode::MoveAnchor,
1,
);
assert_eq!(
cursor.blockquote_depth_at_cursor(),
3,
"precondition: cursor must be at depth 3 (end of `block C`)"
);
cursor.insert_block().unwrap();
assert_eq!(cursor.blockquote_depth_at_cursor(), 3);
assert!(cursor.current_block_is_empty());
cursor.unwrap_current_block_from_blockquote().unwrap();
assert_eq!(cursor.blockquote_depth_at_cursor(), 2);
use text_document::FlowElementSnapshot;
fn flatten(els: &[FlowElementSnapshot], depth: usize, out: &mut Vec<(usize, String)>) {
for el in els {
match el {
FlowElementSnapshot::Block(b) => out.push((depth, b.text.clone())),
FlowElementSnapshot::Frame(f) => flatten(&f.elements, depth + 1, out),
FlowElementSnapshot::Table(_) => {}
}
}
}
let snap = doc.snapshot_flow();
let mut tuples = Vec::new();
flatten(&snap.elements, 0, &mut tuples);
let c_pos = tuples
.iter()
.position(|(_, t)| t == "block C")
.expect("`block C` must remain in the flow");
let empty_pos = tuples
.iter()
.position(|(_, t)| t.is_empty())
.expect("the new empty block must remain in the flow");
assert!(
c_pos < empty_pos,
"`block C` (flow idx {c_pos}) must come before the new empty block (flow idx {empty_pos}); flow: {tuples:?}"
);
}
#[test]
fn enter_then_enter_at_end_of_quote_exits_after_not_before() {
let doc = TextDocument::new();
doc.set_markdown("> A\n").unwrap().wait().unwrap();
let a_pos = flow_position_helper(&doc, "A");
let end_of_a = a_pos + 1; let cursor = doc.cursor_at(end_of_a);
cursor.set_position(end_of_a, MoveMode::MoveAnchor);
cursor.insert_block().unwrap();
let cursor = doc.cursor_at(cursor.position());
assert!(
cursor.is_in_blockquote(),
"after the 1st Enter, the new empty block must still be inside the quote"
);
cursor.unwrap_current_block_from_blockquote().unwrap();
let md = doc.to_markdown().unwrap();
let a_idx = md
.find("> A")
.unwrap_or_else(|| panic!("expected quoted 'A' to remain; got: {md:?}"));
let before_a = &md[..a_idx];
assert!(
!before_a.contains('>'),
"no quoted content must appear BEFORE '> A' after exiting the quote; got: {md:?}"
);
let cursor = doc.cursor_at(cursor.position());
assert!(
!cursor.is_in_blockquote(),
"cursor must be outside the quote after exit; got md: {md:?}"
);
}
fn flow_position_helper(doc: &TextDocument, needle: &str) -> usize {
let snap = doc.snapshot_flow();
fn walk(
els: &[text_document::FlowElementSnapshot],
out: &mut Vec<text_document::BlockSnapshot>,
) {
for el in els {
match el {
text_document::FlowElementSnapshot::Block(b) => out.push(b.clone()),
text_document::FlowElementSnapshot::Frame(f) => walk(&f.elements, out),
text_document::FlowElementSnapshot::Table(t) => {
for cell in &t.cells {
out.extend(cell.blocks.iter().cloned());
}
}
}
}
}
let mut blocks = Vec::new();
walk(&snap.elements, &mut blocks);
blocks
.into_iter()
.find(|b| b.text.contains(needle))
.map(|b| b.position)
.expect("block with needle present in flow")
}
#[test]
fn undo_restores_pre_wrap_state() {
let doc = new_doc_with_markdown("Hello.\n");
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
cursor.wrap_selection_in_blockquote().unwrap();
assert!(cursor.is_in_blockquote());
doc.undo().unwrap();
let cursor = doc.cursor_at(0);
cursor.set_position(0, MoveMode::MoveAnchor);
assert!(
!cursor.is_in_blockquote(),
"undo must restore the pre-wrap state"
);
let md = doc.to_markdown().unwrap();
assert!(
!md.contains('>'),
"markdown after undo must not contain `>`; got: {md:?}"
);
}