use text_document::{AddressablePiece, FindOptions, InlineContent, TextBlock, TextDocument};
fn doc_of(djot: &str) -> TextDocument {
let doc = TextDocument::new();
doc.set_djot(djot).unwrap().wait().unwrap();
doc
}
fn block_containing(doc: &TextDocument, needle: &str) -> TextBlock {
doc.blocks()
.into_iter()
.find(|b| b.text().contains(needle))
.unwrap_or_else(|| panic!("no block contains {needle:?}"))
}
fn expected_slice(content: &InlineContent) -> String {
match content {
InlineContent::Text(t) => t.clone(),
InlineContent::Image { .. } | InlineContent::FootnoteRef { .. } => "\u{FFFC}".to_string(),
InlineContent::Empty => String::new(),
}
}
fn assert_pieces_tile_the_addressable_text(doc: &TextDocument, block: &TextBlock, label: &str) {
let addressable = doc.to_addressable_text().unwrap();
let chars: Vec<char> = addressable.chars().collect();
let pieces = block.addressable_inline_pieces();
let mut cursor = block.position();
for (i, piece) in pieces.iter().enumerate() {
assert_eq!(piece.start, cursor, "{label}: piece {i} gap or overlap");
assert!(piece.start < piece.end, "{label}: piece {i} is empty");
assert!(
piece.end <= chars.len(),
"{label}: piece {i} claims [{}, {}) past a {}-char addressable text",
piece.start,
piece.end,
chars.len()
);
let slice: String = chars[piece.start..piece.end].iter().collect();
assert_eq!(
slice,
expected_slice(&piece.content),
"{label}: piece {i} ({:?}) does not match the addressable text at [{}, {})",
piece.content,
piece.start,
piece.end
);
cursor = piece.end;
}
assert_eq!(
cursor,
block.position() + block.text().chars().count(),
"{label}: pieces did not cover the whole block"
);
}
#[test]
fn a_comment_boundary_right_after_an_inline_image_agrees_with_addressable_text_and_find_all() {
let doc = doc_of("before  after the picture");
let block = block_containing(&doc, "after the picture");
assert_pieces_tile_the_addressable_text(&doc, &block, "image doc");
let pieces = block.addressable_inline_pieces();
let image_idx = pieces
.iter()
.position(|p| matches!(p.content, InlineContent::Image { .. }))
.expect("the paragraph has an image");
let image: &AddressablePiece = &pieces[image_idx];
assert_eq!(
image.end - image.start,
1,
"an image occupies exactly one character of the addressable text"
);
let after = &pieces[image_idx + 1];
assert_eq!(
after.start, image.end,
"the next piece must resume exactly where the image ends"
);
let InlineContent::Text(text) = &after.content else {
panic!("expected text after the image, got {:?}", after.content);
};
assert!(text.starts_with(" after the picture"));
let matches = doc
.find_all(" after the picture", &FindOptions::default())
.unwrap();
let m = matches
.first()
.expect("the text after the image is findable");
assert_eq!(
m.position, image.end,
"find_all's match position must agree with the piece boundary right after the image"
);
}
#[test]
fn a_comment_boundary_right_after_a_footnote_reference_agrees_with_addressable_text_and_find_all() {
let doc = doc_of("A claim.[^n] settles it.\n\n[^n]: The note body.");
let block = block_containing(&doc, "settles it.");
assert_pieces_tile_the_addressable_text(&doc, &block, "footnote doc");
let pieces = block.addressable_inline_pieces();
let note_idx = pieces
.iter()
.position(|p| matches!(p.content, InlineContent::FootnoteRef { .. }))
.expect("the paragraph has a footnote reference");
let note: &AddressablePiece = &pieces[note_idx];
assert_eq!(
note.end - note.start,
1,
"a footnote reference occupies exactly one character of the addressable text"
);
let after = &pieces[note_idx + 1];
assert_eq!(
after.start, note.end,
"the next piece must resume exactly where the footnote reference ends"
);
let InlineContent::Text(text) = &after.content else {
panic!(
"expected text after the footnote reference, got {:?}",
after.content
);
};
assert!(text.starts_with(" settles it."));
let matches = doc
.find_all(" settles it.", &FindOptions::default())
.unwrap();
let m = matches
.first()
.expect("the text after the footnote reference is findable");
assert_eq!(
m.position, note.end,
"find_all's match position must agree with the piece boundary right after the \
footnote reference"
);
}
#[test]
fn a_block_after_a_table_agrees_with_addressable_text_and_find_all() {
let doc = doc_of("intro\n\n| a | b |\n| - | - |\n| c | d |\n\nthe salt-bleached door");
let block = block_containing(&doc, "salt-bleached");
assert_pieces_tile_the_addressable_text(&doc, &block, "table doc");
let pieces = block.addressable_inline_pieces();
assert_eq!(pieces.len(), 1, "a plain paragraph is a single text piece");
let piece = &pieces[0];
let InlineContent::Text(text) = &piece.content else {
panic!("expected text, got {:?}", piece.content);
};
assert_eq!(text, "the salt-bleached door");
assert_eq!(
piece.start,
block.position(),
"the block's one piece must start exactly at the block's own (table-shifted) position"
);
let matches = doc
.find_all("salt-bleached", &FindOptions::default())
.unwrap();
let m = matches.first().expect("findable");
assert_eq!(
m.position,
piece.start + "the ".chars().count(),
"find_all's match position, offset from the piece's table-shifted start, must land \
exactly on \"salt-bleached\""
);
}
#[test]
fn every_block_in_every_construct_tiles_the_addressable_text() {
const BATTERY: &[&str] = &[
"First paragraph.\n\nSecond paragraph.\n\nThird.",
"before  after",
"A claim.[^n]\n\n[^n]: The note body.\n\nAfter.",
"intro\n\n| a | b |\n| - | - |\n| c | d |\n\nafter",
"a  b  c",
"",
];
for src in BATTERY {
let doc = doc_of(src);
for block in doc.blocks() {
assert_pieces_tile_the_addressable_text(&doc, &block, &format!("{src:?}"));
}
}
}