use text_document::{
DjotImportOptions, FindOptions, ReplaceOptions, TABLE_ANCHOR, TextDocument, djot_to_plain_text,
};
const BATTERY: &[&str] = &[
"First paragraph.\n\nSecond paragraph.\n\nThird.",
"intro\n\n| a | b |\n| - | - |\n| c | d |\n\nafter",
"> a0\n\na",
"p1\n\n> q1\n\np2\n\n> q2",
"> quoted\n\n# head\n\n- item\n\n| x |\n| - |\n| y |",
"before  after",
"a\n\n| t |\n| - |\n| u |\n\nb\n\n| v |\n| - |\n| w |\n\nc",
"",
];
fn doc_of(djot: &str) -> TextDocument {
let doc = TextDocument::new();
doc.set_djot(djot).unwrap().wait().unwrap();
doc
}
fn assert_blocks_index_the_addressable_text(doc: &TextDocument, label: &str) {
let addressable = doc.to_addressable_text().unwrap();
let chars: Vec<char> = addressable.chars().collect();
for (i, block) in doc.blocks().into_iter().enumerate() {
let start = block.position();
let text = block.text();
let len = text.chars().count();
assert!(
start + len <= chars.len(),
"{label}: block {i} claims [{start}, {}) in a {} char text\n\
addressable = {addressable:?}",
start + len,
chars.len()
);
let slice: String = chars[start..start + len].iter().collect();
assert_eq!(
slice, text,
"{label}: block {i}'s reported start must land on the block's own text\n\
addressable = {addressable:?}"
);
if start > 0 {
assert_eq!(
chars[start - 1],
'\n',
"{label}: block {i} starts at {start}, but the char before it is not the \
block separator\naddressable = {addressable:?}"
);
}
}
}
#[test]
fn block_starts_index_the_addressable_text() {
for src in BATTERY {
assert_blocks_index_the_addressable_text(&doc_of(src), &format!("{src:?}"));
}
}
#[test]
fn prose_after_a_table_resolves_where_its_block_says_it_does() {
let doc = doc_of("intro\n\n| a | b |\n| - | - |\n| c | d |\n\nthe salt-bleached door");
let addressable = doc.to_addressable_text().unwrap();
let chars: Vec<char> = addressable.chars().collect();
let last = doc.blocks().last().unwrap().position();
let tail: String = chars[last..].iter().collect();
assert_eq!(
tail, "the salt-bleached door",
"the last block's reported start must land exactly on the last block's text\n\
addressable = {addressable:?}"
);
}
#[test]
fn a_selection_offset_after_a_table_slices_the_addressable_text() {
let doc = doc_of("intro\n\n| a | b |\n| - | - |\n| c | d |\n\nthe salt-bleached door");
let position = doc
.find_all("salt-bleached", &FindOptions::default())
.unwrap()
.first()
.unwrap()
.position;
let addressable = doc.to_addressable_text().unwrap();
let chars: Vec<char> = addressable.chars().collect();
let captured: String = chars[position..position + "salt-bleached".chars().count()]
.iter()
.collect();
assert_eq!(
captured, "salt-bleached",
"the quote captured at the document's own offset must be the selected words\n\
addressable = {addressable:?}\nposition = {position}"
);
}
#[test]
fn find_all_offsets_slice_the_addressable_text() {
for src in BATTERY {
let doc = doc_of(src);
let addressable = doc.to_addressable_text().unwrap();
let chars: Vec<char> = addressable.chars().collect();
for m in doc.find_all("a", &FindOptions::default()).unwrap() {
let slice: String = chars[m.position..m.position + m.length].iter().collect();
assert_eq!(
slice, m.matched_text,
"a find_all match must occupy its reported range in the addressable text, \
for {src:?}\naddressable = {addressable:?}"
);
}
}
}
#[test]
fn the_live_view_and_the_djot_view_are_the_same_string() {
for src in BATTERY {
assert_eq!(
doc_of(src).to_addressable_text().unwrap(),
djot_to_plain_text(src, &DjotImportOptions::default()),
"to_addressable_text() and djot_to_plain_text() disagree for {src:?}"
);
}
}
#[test]
fn the_export_view_is_the_addressable_view_minus_its_anchors() {
for src in BATTERY {
let doc = doc_of(src);
let addressable = doc.to_addressable_text().unwrap();
let without_anchors: Vec<&str> = addressable
.split('\n')
.filter(|line| *line != TABLE_ANCHOR)
.collect();
assert_eq!(
doc.to_plain_text().unwrap(),
without_anchors.join("\n"),
"to_plain_text() must be to_addressable_text() minus anchor lines, for {src:?}"
);
}
}
#[test]
fn the_addressable_text_stays_true_after_an_edit() {
let doc = doc_of("intro\n\n| a | b |\n| - | - |\n| c | d |\n\nthe salt-bleached door");
let replaced = doc
.replace_text(
"intro",
"a much longer opening line",
true,
&ReplaceOptions::default(),
)
.unwrap();
assert_eq!(replaced, 1, "the edit must actually happen");
assert_blocks_index_the_addressable_text(&doc, "after replace_text");
let position = doc
.find_all("salt-bleached", &FindOptions::default())
.unwrap()
.first()
.unwrap()
.position;
let chars: Vec<char> = doc.to_addressable_text().unwrap().chars().collect();
let captured: String = chars[position..position + "salt-bleached".chars().count()]
.iter()
.collect();
assert_eq!(captured, "salt-bleached");
}
#[test]
fn footnote_bodies_are_searched_in_the_live_document() {
let doc = doc_of("A claim.[^n]\n\n[^n]: The note body.\n\nAfter.");
assert_blocks_index_the_addressable_text(&doc, "footnote document");
let matches = doc
.find_all("The note body", &FindOptions::default())
.unwrap();
let m = matches
.first()
.expect("today, an in-document search finds text inside a footnote's body");
let chars: Vec<char> = doc.to_addressable_text().unwrap().chars().collect();
let slice: String = chars[m.position..m.position + m.length].iter().collect();
assert_eq!(
slice, "The note body",
"to_addressable_text() must include what search searches — the note body"
);
}
#[test]
fn an_empty_document_has_an_empty_addressable_text() {
let doc = doc_of("");
assert_eq!(doc.to_addressable_text().unwrap(), "");
let blocks = doc.blocks();
assert_eq!(blocks.len(), 1, "an empty document holds one empty block");
assert_eq!(blocks[0].position(), 0);
}