use std::time::Instant;
use text_document::{
DjotImportOptions, FindOptions, TABLE_ANCHOR, TextDocument, djot_to_plain_text,
};
fn plain(djot: &str) -> String {
djot_to_plain_text(djot, &DjotImportOptions::default())
}
#[test]
fn the_prose_is_extracted_and_the_markup_is_not() {
let djot = "She read [the note](https://example.test/note) and *whispered* his name.";
let text = plain(djot);
assert_eq!(
text, "She read the note and whispered his name.",
"the link's URL and the emphasis markers are markup, not prose"
);
assert!(
djot.contains("https"),
"the fixture must actually contain a URL"
);
assert!(
!text.contains("https"),
"a search for `https` must not hit a link's destination"
);
assert!(
!text.contains('*'),
"a search for `*` must not hit an emphasis marker"
);
}
#[test]
fn block_markers_do_not_leak_into_the_prose() {
let text = plain("# A Heading\n\n- first item\n\n- second item\n\n> a quotation");
assert_eq!(text, "A Heading\nfirst item\nsecond item\na quotation");
assert!(!text.contains('#') && !text.contains('-') && !text.contains('>'));
}
#[test]
fn an_offset_found_in_the_extract_is_an_offset_the_document_agrees_with() {
let djot =
"She called *Aurélien* into the trees, and [Aurélien](https://x.test/a) did not answer.";
let doc = TextDocument::new();
doc.set_djot(djot).unwrap().wait().unwrap();
let text = plain(djot);
let from_document: Vec<(usize, usize)> = doc
.find_all("Aurélien", &FindOptions::default())
.unwrap()
.into_iter()
.map(|m| (m.position, m.length))
.collect();
let from_extract: Vec<(usize, usize)> = text
.match_indices("Aurélien")
.map(|(byte, m)| (text[..byte].chars().count(), m.chars().count()))
.collect();
assert_eq!(
from_document.len(),
2,
"both occurrences, incl. the link label"
);
assert_eq!(
from_document, from_extract,
"the document and the extract must agree on WHERE the matches are, or a count \
taken from one will not survive a replace performed through the other"
);
}
#[test]
fn extraction_is_substantially_cheaper_than_a_full_import() {
let scene: String = (0..40)
.map(|i| {
format!(
"## Section {i}\n\nShe called *Aurélien* into the trees, and [the note]\
(https://example.test/{i}) said nothing at all. He waited.\n\n\
- a thought\n\n- another\n\n> and a quotation\n\n"
)
})
.collect();
const ROUNDS: u32 = 5;
let t0 = Instant::now();
for _ in 0..ROUNDS {
let text = plain(&scene);
std::hint::black_box(text);
}
let extract = t0.elapsed();
let t1 = Instant::now();
for _ in 0..ROUNDS {
let doc = TextDocument::new();
doc.set_djot(&scene).unwrap().wait().unwrap();
std::hint::black_box(doc.to_plain_text().unwrap());
}
let import = t1.elapsed();
println!("extract: {extract:?} full import: {import:?}");
assert!(
extract * 2 < import,
"extraction ({extract:?}) is not meaningfully cheaper than a full import \
({import:?}) — it exists precisely so a project-wide search does not have to \
import every scene on every keystroke"
);
}
#[test]
fn a_table_carries_its_anchor_so_offsets_after_it_stay_true() {
let djot = "intro\n\n| a | b |\n| - | - |\n| c | d |\n\nafter";
let text = plain(djot);
assert_eq!(
text,
format!("intro\n{TABLE_ANCHOR}\na\nb\nc\nd\nafter"),
"the table announces itself with its anchor, then its cells row by row"
);
let doc = TextDocument::new();
doc.set_djot(djot).unwrap().wait().unwrap();
let hits = doc.find_all("after", &FindOptions::default()).unwrap();
let from_extract = text.chars().count() - "after".chars().count();
assert_eq!(
hits[0].position, from_extract,
"a word AFTER a table must sit at the same offset in the extract as it does in the \
document — without the anchor the extract is two characters short and every offset \
past the table is wrong"
);
}
#[test]
fn an_empty_block_still_occupies_a_line() {
let djot = "```\n```\n\nA";
let text = plain(djot);
assert_eq!(text, "\nA", "the empty code fence keeps its (empty) line");
let doc = TextDocument::new();
doc.set_djot(djot).unwrap().wait().unwrap();
let hits = doc.find_all("A", &FindOptions::default()).unwrap();
assert_eq!(
hits[0].position, 1,
"the document puts `A` at offset 1, after the empty fence's line — the extract must \
agree, or it is a character short"
);
}
#[test]
fn degenerate_input_yields_no_prose_and_does_not_panic() {
for source in ["", "\n\n", "*", "#", "> ", "```\n```", "{}"] {
let text = plain(source);
assert!(
text.trim().is_empty(),
"{source:?} carries no prose, but the extractor produced {text:?}"
);
}
}