use crate::error::FocrResult;
pub const EOS_MARKER: &str = "<|end▁of▁sentence|>";
pub const PAGE_MARKER: &str = "<PAGE>";
#[derive(Debug, Clone, PartialEq)]
pub struct RefMatch {
pub full: String,
pub offset: usize,
pub label: String,
pub boxes: Vec<[i64; 4]>,
pub level: Option<u8>,
}
impl RefMatch {
#[must_use]
pub fn is_image(&self) -> bool {
self.label.trim() == "image" || self.full.contains("<|ref|>image<|/ref|>")
}
#[must_use]
pub fn rescaled_boxes(&self, width: u32, height: u32) -> Vec<[i64; 4]> {
self.boxes
.iter()
.map(|&[x1, y1, x2, y2]| {
[
rescale(x1, width),
rescale(y1, height),
rescale(x2, width),
rescale(y2, height),
]
})
.collect()
}
}
#[must_use]
fn rescale(coord: i64, extent: u32) -> i64 {
(coord as f64 / 999.0 * f64::from(extent)) as i64
}
#[must_use]
pub fn strip_eos(text: &str) -> String {
let body = text.strip_suffix(EOS_MARKER).unwrap_or(text);
body.trim().to_string()
}
#[must_use]
pub fn re_match(text: &str) -> Vec<RefMatch> {
let mut out = Vec::new();
out.extend(scan_ref_spans(text));
out.extend(scan_det_spans(text));
out
}
fn scan_ref_spans(text: &str) -> Vec<RefMatch> {
const REF_OPEN: &str = "<|ref|>";
const REF_CLOSE: &str = "<|/ref|>";
const DET_OPEN: &str = "<|det|>";
const DET_CLOSE: &str = "<|/det|>";
let mut out = Vec::new();
let bytes = text.as_bytes();
let mut i = 0usize;
while let Some(rel) = find_from(bytes, i, REF_OPEN) {
let body_start = rel + REF_OPEN.len();
let Some(ref_close) = find_from(bytes, body_start, REF_CLOSE) else {
break;
};
let label = &text[body_start..ref_close];
let after_ref = ref_close + REF_CLOSE.len();
if !slice_starts_with(bytes, after_ref, DET_OPEN) {
i = body_start;
continue;
}
let det_body_start = after_ref + DET_OPEN.len();
let Some(det_close) = find_from(bytes, det_body_start, DET_CLOSE) else {
break;
};
let box_text = &text[det_body_start..det_close];
let span_end = det_close + DET_CLOSE.len();
let full = &text[rel..span_end];
out.push(RefMatch {
full: full.to_string(),
offset: rel,
label: label.to_string(),
boxes: parse_boxes(box_text),
level: None,
});
i = span_end;
}
out
}
fn scan_det_spans(text: &str) -> Vec<RefMatch> {
const DET_OPEN: &str = "<|det|>";
const DET_CLOSE: &str = "<|/det|>";
let mut out = Vec::new();
let bytes = text.as_bytes();
let mut i = 0usize;
while let Some(open) = find_from(bytes, i, DET_OPEN) {
let mut p = open + DET_OPEN.len();
i = p; p = skip_ws(text, p);
let label_start = p;
let Some(first) = text[p..].chars().next() else {
continue;
};
if !is_ident_start(first) {
continue;
}
p += first.len_utf8();
while let Some(ch) = text[p..].chars().next() {
if !is_ident_continue(ch) {
break;
}
p += ch.len_utf8();
}
let label_end = p;
p = skip_ws(text, p);
if p >= bytes.len() || bytes[p] != b'[' {
continue;
}
let box_start = p;
p += 1;
let inner_start = p;
while p < bytes.len() && bytes[p] != b']' {
p += 1;
}
if p >= bytes.len() || p == inner_start {
continue;
}
let box_end = p + 1; p = box_end;
p = skip_ws(text, p);
if !slice_starts_with(bytes, p, DET_CLOSE) {
continue;
}
let span_end = p + DET_CLOSE.len();
let full = &text[open..span_end];
let label = &text[label_start..label_end];
let box_text = &text[box_start..box_end];
out.push(RefMatch {
full: full.to_string(),
offset: open,
label: label.to_string(),
boxes: parse_boxes(box_text),
level: None,
});
i = span_end;
}
out
}
const BLOCK_OPEN: &str = "<BLOCK>";
const BLOCK_FORK: &str = "<FORK>";
const BLOCK_LEVEL: &str = "<LEVEL";
fn scan_block_spans(text: &str) -> Vec<RefMatch> {
let mut out = Vec::new();
let bytes = text.as_bytes();
let mut i = 0usize;
while let Some(open) = find_from(bytes, i, BLOCK_OPEN) {
i = open + BLOCK_OPEN.len();
let Some((label, level, quad, end)) = block_prefix(text, i) else {
continue;
};
out.push(RefMatch {
full: text[open..end].to_string(),
offset: open,
label: label.to_string(),
boxes: vec![quad],
level,
});
i = end;
}
out
}
fn block_prefix(text: &str, at: usize) -> Option<(&str, Option<u8>, [i64; 4], usize)> {
let bytes = text.as_bytes();
let first = text[at..].chars().next()?;
if !is_ident_start(first) {
return None;
}
let mut p = at + first.len_utf8();
while let Some(ch) = text[p..].chars().next() {
if !is_ident_continue(ch) {
break;
}
p += ch.len_utf8();
}
let label = &text[at..p];
let mut level = None;
if slice_starts_with(bytes, p, BLOCK_LEVEL) {
let digits = p + BLOCK_LEVEL.len();
let window = &bytes[digits..bytes.len().min(digits + 4)]; let close = digits + window.iter().position(|&b| b == b'>')?;
let n = &text[digits..close];
if n.is_empty() || !n.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
level = Some(n.parse().ok()?);
p = close + 1;
}
if bytes.get(p) != Some(&b'[') {
return None;
}
let inner_start = p + 1;
let window = &bytes[inner_start..bytes.len().min(inner_start + 82)];
let inner_end = inner_start + window.iter().position(|&b| b == b']')?;
let mut quad = [0_i64; 4];
let mut parts = text[inner_start..inner_end].split(',');
for slot in &mut quad {
let part = parts.next()?.trim_matches(' ');
if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
*slot = part.parse().ok()?;
}
if parts.next().is_some() {
return None;
}
let fork = inner_end + 1;
if !slice_starts_with(bytes, fork, BLOCK_FORK) {
return None;
}
Some((label, level, quad, fork + BLOCK_FORK.len()))
}
fn parse_boxes(box_text: &str) -> Vec<[i64; 4]> {
let nums = extract_numbers(box_text);
let mut out = Vec::new();
for chunk in nums.chunks(4) {
if chunk.len() == 4 {
out.push([
chunk[0] as i64,
chunk[1] as i64,
chunk[2] as i64,
chunk[3] as i64,
]);
}
}
out
}
fn extract_numbers(s: &str) -> Vec<f64> {
let bytes = s.as_bytes();
let mut nums = Vec::new();
let mut i = 0usize;
while i < bytes.len() {
let c = bytes[i];
let is_num_start = c.is_ascii_digit()
|| ((c == b'-' || c == b'+' || c == b'.')
&& i + 1 < bytes.len()
&& (bytes[i + 1].is_ascii_digit() || bytes[i + 1] == b'.'));
if is_num_start {
let start = i;
if c == b'-' || c == b'+' {
i += 1;
}
let mut seen_dot = false;
while i < bytes.len() {
let d = bytes[i];
if d.is_ascii_digit() {
i += 1;
} else if d == b'.' && !seen_dot {
seen_dot = true;
i += 1;
} else {
break;
}
}
if let Ok(v) = s[start..i].parse::<f64>() {
nums.push(v);
}
} else {
i += 1;
}
}
nums
}
fn find_from(hay: &[u8], from: usize, needle: &str) -> Option<usize> {
let n = needle.as_bytes();
if n.is_empty() || from > hay.len() {
return None;
}
let last = hay.len().checked_sub(n.len())?;
(from..=last).find(|&i| &hay[i..i + n.len()] == n)
}
fn slice_starts_with(hay: &[u8], at: usize, needle: &str) -> bool {
let n = needle.as_bytes();
at + n.len() <= hay.len() && &hay[at..at + n.len()] == n
}
fn skip_ws(text: &str, mut at: usize) -> usize {
while let Some(ch) = text[at..].chars().next() {
if !ch.is_whitespace() {
break;
}
at += ch.len_utf8();
}
at
}
fn is_ident_start(ch: char) -> bool {
ch.is_ascii_alphabetic() || ch == '_'
}
fn is_ident_continue(ch: char) -> bool {
ch == '_' || ch == '-' || ch.is_alphanumeric()
}
fn assemble_page(text: &str, img_base: &str) -> String {
let matches = re_match(text);
let mut images = Vec::new();
let mut others = Vec::new();
for m in &matches {
if m.is_image() {
images.push(m);
} else {
others.push(m);
}
}
let mut out = String::with_capacity(text.len());
let mut copied = 0;
let mut idx = images.len();
for m in scan_block_spans(text) {
out.push_str(&text[copied..m.offset]);
if m.is_image() {
out.push_str(&image_md_token(img_base, idx));
out.push('\n');
idx += 1;
}
copied = m.offset + m.full.len();
}
out.push_str(&text[copied..]);
for (idx, m) in images.iter().enumerate() {
let replacement = format!("{}\n", image_md_token(img_base, idx));
out = out.replace(&m.full, &replacement);
}
for m in &others {
out = out.replace(&m.full, "");
}
normalize_colon_equals(&out)
}
fn grounding_spans(text: &str) -> Vec<RefMatch> {
let mut spans = re_match(text);
spans.extend(scan_block_spans(text));
spans
}
fn span_text(text: &str, span: &RefMatch, starts: &[usize]) -> Option<String> {
if span.is_image() {
return None;
}
let from = span.offset + span.full.len();
let next = starts.partition_point(|&start| start < from);
let to = starts.get(next).copied().unwrap_or(text.len());
let body = normalize_colon_equals(text[from..to].trim());
(!body.is_empty()).then_some(body)
}
fn image_md_token(img_base: &str, idx: usize) -> String {
format!("")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FigureRef {
pub index: usize,
pub label: String,
pub boxes: Vec<[i64; 4]>,
pub markdown_ref: String,
}
#[must_use]
pub fn figure_refs(decoded: &str, image_w: u32, image_h: u32, img_base: &str) -> Vec<FigureRef> {
let stripped = strip_eos(decoded);
grounding_spans(&stripped)
.into_iter()
.filter(RefMatch::is_image)
.enumerate()
.map(|(index, m)| FigureRef {
index,
boxes: m.rescaled_boxes(image_w, image_h),
label: m.label,
markdown_ref: image_md_token(img_base, index),
})
.collect()
}
fn normalize_colon_equals(text: &str) -> String {
text.replace("\\coloneqq", ":=").replace("\\eqqcolon", "=:")
}
pub fn finalize(decoded: &str, _image_w: u32, _image_h: u32) -> FocrResult<String> {
let stripped = strip_eos(decoded);
Ok(assemble_page(&stripped, ""))
}
pub fn finalize_multi(decoded: &str, num_pages: usize) -> FocrResult<String> {
let stripped = strip_eos(decoded);
let pages = text_page_bodies(&stripped)
.enumerate()
.map(|(page_idx, page)| {
if page_idx >= num_pages {
page.trim().to_string()
} else {
page_text(page, page_idx)
}
});
Ok(join_page_texts(pages))
}
pub fn text_page_bodies(text: &str) -> impl Iterator<Item = &str> {
let mut chunks = text.split(PAGE_MARKER);
let leading = chunks.next().filter(|c| !c.trim().is_empty());
leading.into_iter().chain(chunks)
}
fn page_text(body: &str, page_idx: usize) -> String {
assemble_page(body.trim(), &format!("page_{page_idx}_"))
}
pub fn join_page_texts<S: AsRef<str>>(pages: impl IntoIterator<Item = S>) -> String {
let mut out = format!("{PAGE_MARKER}\n");
for (i, page) in pages.into_iter().enumerate() {
if i > 0 {
out.push_str(&format!("\n{PAGE_MARKER}\n"));
}
out.push_str(page.as_ref());
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageOutput {
pub text: String,
pub layout: Vec<super::LayoutSpan>,
pub figures: Vec<FigureRef>,
}
#[must_use]
pub fn finalize_page(body: &str, page_idx: usize, (width, height): (u32, u32)) -> PageOutput {
PageOutput {
text: page_text(body, page_idx),
layout: parse_layout(body, width, height),
figures: figure_refs(body, width, height, &format!("page_{page_idx}_")),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenPages {
pub bodies: Vec<String>,
pub tokens: Vec<usize>,
pub leading_text: bool,
pub extra_markers: usize,
}
pub fn split_pages_by_token(
tok: &crate::tokenizer::Tokenizer,
ids: &[u32],
marker: u32,
pages: usize,
) -> FocrResult<TokenPages> {
let (head, mut segments) = token_segments(tok, ids, marker, false)?;
let extra_markers = segments.len().saturating_sub(pages);
if segments.len() > pages.max(1) {
let extras = segments.split_off(pages.max(1));
let last = segments.last_mut().expect("pages.max(1) >= 1");
for (text, count) in extras {
if !text.trim().is_empty() {
last.0.push('\n');
last.0.push_str(&text);
}
last.1 += count;
}
}
Ok(join_head(head, segments, extra_markers))
}
pub fn split_window_pages(
tok: &crate::tokenizer::Tokenizer,
ids: &[u32],
marker: u32,
own: usize,
leads_with_body: bool,
lookahead: bool,
) -> FocrResult<TokenPages> {
let (head, mut segments) = token_segments(tok, ids, marker, leads_with_body)?;
let expected_markers = own + usize::from(lookahead);
let extra_markers = segments.len().saturating_sub(expected_markers);
segments.truncate(own);
Ok(join_head(head, segments, extra_markers))
}
type Segment = (String, usize);
fn token_segments(
tok: &crate::tokenizer::Tokenizer,
ids: &[u32],
marker: u32,
leads_with_body: bool,
) -> FocrResult<(Segment, Vec<Segment>)> {
let mut decoder = crate::tokenizer::StreamDecoder::new();
let mut segments = vec![(String::new(), 0usize)];
if leads_with_body {
segments.push((String::new(), 0));
}
for &id in ids {
let (text, count) = segments.last_mut().expect("never empty");
if id == marker {
decoder.flush(text);
segments.push((String::new(), 0));
} else {
decoder.push(tok, id, text)?;
*count += 1;
}
}
decoder.flush(&mut segments.last_mut().expect("never empty").0);
let head = segments.remove(0);
Ok((head, segments))
}
fn join_head(
(head, head_tokens): Segment,
mut segments: Vec<Segment>,
extra_markers: usize,
) -> TokenPages {
let leading_text = !head.trim().is_empty();
match segments.first_mut() {
Some(first) => {
if leading_text {
first.0.insert_str(0, &head);
}
first.1 += head_tokens;
}
None if leading_text || head_tokens > 0 => segments.push((head, head_tokens)),
None => {}
}
let (bodies, tokens) = segments.into_iter().unzip();
TokenPages {
bodies,
tokens,
leading_text,
extra_markers,
}
}
#[derive(Debug)]
pub struct TokenPageStream<'t> {
tok: &'t crate::tokenizer::Tokenizer,
marker: u32,
pages: usize,
decoder: crate::tokenizer::StreamDecoder,
current: String,
tokens: usize,
markers: usize,
emitted: usize,
}
impl<'t> TokenPageStream<'t> {
#[must_use]
pub fn new(tok: &'t crate::tokenizer::Tokenizer, marker: u32, pages: usize) -> Self {
Self {
tok,
marker,
pages,
decoder: crate::tokenizer::StreamDecoder::new(),
current: String::new(),
tokens: 0,
markers: 0,
emitted: 0,
}
}
pub fn push(&mut self, id: u32, mut on_page: impl FnMut(usize, &str, usize)) {
if id != self.marker {
let _ = self.decoder.push(self.tok, id, &mut self.current);
self.tokens += 1;
return;
}
self.decoder.flush(&mut self.current);
self.markers += 1;
if self.markers == 1 {
if self.current.trim().is_empty() {
self.current.clear();
}
} else if self.emitted < self.pages {
self.emitted += 1;
on_page(self.emitted, self.current.trim(), self.tokens);
self.current.clear();
self.tokens = 0;
}
}
pub fn finish(mut self, mut on_page: impl FnMut(usize, &str, usize)) {
self.decoder.flush(&mut self.current);
let started = self.markers > 0 || !self.current.trim().is_empty();
if started && self.emitted < self.pages.max(1) {
on_page(self.emitted + 1, self.current.trim(), self.tokens);
}
}
}
#[must_use]
pub fn page_body_count(text: &str) -> usize {
text_page_bodies(text).count()
}
#[derive(Debug, Default)]
pub struct PageStream {
body_start: Option<usize>,
current_page: usize,
}
impl PageStream {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn feed(&mut self, full_text: &str, mut on_page: impl FnMut(usize, &str)) {
loop {
match self.body_start {
None => {
let Some(pos) = full_text.find(PAGE_MARKER) else {
return;
};
let leading = full_text[..pos].trim();
self.current_page = if leading.is_empty() {
1
} else {
on_page(1, leading);
2
};
self.body_start = Some(pos + PAGE_MARKER.len());
}
Some(start) => {
let Some(rel) = full_text[start..].find(PAGE_MARKER) else {
return;
};
let end = start + rel;
on_page(self.current_page, full_text[start..end].trim());
self.body_start = Some(end + PAGE_MARKER.len());
self.current_page += 1;
}
}
}
}
pub fn finish(self, full_text: &str, mut on_page: impl FnMut(usize, &str)) {
match self.body_start {
Some(start) => on_page(self.current_page, full_text[start..].trim()),
None => {
let body = full_text.trim();
if !body.is_empty() {
on_page(1, body);
}
}
}
}
}
#[must_use]
pub fn parse_layout(decoded: &str, image_w: u32, image_h: u32) -> Vec<super::LayoutSpan> {
let stripped = strip_eos(decoded);
let spans = grounding_spans(&stripped);
let mut starts: Vec<usize> = spans.iter().map(|m| m.offset).collect();
starts.sort_unstable();
spans
.iter()
.map(|m| super::LayoutSpan {
boxes: m.rescaled_boxes(image_w, image_h),
text: span_text(&stripped, m, &starts),
label: m.label.clone(),
level: m.level,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_body_count_matches_the_finalize_split() {
for (text, bodies) in [
("", 0),
(" ", 0),
("<PAGE>a", 1),
("<PAGE>a<PAGE>b<PAGE>c", 3),
("lead<PAGE>b<PAGE>c", 3),
(" \n<PAGE>a<PAGE>b", 2),
("no marker at all", 1),
("<PAGE>a<PAGE>", 2),
] {
assert_eq!(page_body_count(text), bodies, "{text:?}");
let assembled = finalize_multi(text, 99).unwrap();
if bodies > 0 {
assert_eq!(assembled.matches(PAGE_MARKER).count(), bodies, "{text:?}");
}
}
}
#[test]
fn strip_eos_removes_trailing_marker_and_trims() {
let raw = format!("hello world{EOS_MARKER}");
assert_eq!(strip_eos(&raw), "hello world");
let raw2 = format!(" body text {EOS_MARKER}");
assert_eq!(strip_eos(&raw2), "body text");
assert_eq!(strip_eos(" bare "), "bare");
let raw3 = format!("{EOS_MARKER}keep");
assert_eq!(strip_eos(&raw3), format!("{EOS_MARKER}keep"));
}
#[test]
fn rescale_matches_python_int_truncation() {
assert_eq!(rescale(999, 1000), 1000);
assert_eq!(rescale(0, 1000), 0);
assert_eq!(rescale(500, 1000), 500);
assert_eq!(rescale(1, 100), 0);
}
#[test]
fn extract_numbers_handles_ints_floats_signs() {
assert_eq!(extract_numbers("[1, 2, 3, 4]"), vec![1.0, 2.0, 3.0, 4.0]);
assert_eq!(
extract_numbers("[10, -5, 3.5, 0]"),
vec![10.0, -5.0, 3.5, 0.0]
);
assert!(extract_numbers("[]").is_empty());
}
#[test]
fn parse_boxes_single_quad() {
assert_eq!(
parse_boxes("[100, 200, 300, 400]"),
vec![[100, 200, 300, 400]]
);
}
#[test]
fn parse_boxes_list_of_quads() {
assert_eq!(
parse_boxes("[[1, 2, 3, 4], [5, 6, 7, 8]]"),
vec![[1, 2, 3, 4], [5, 6, 7, 8]]
);
}
#[test]
fn parse_boxes_drops_incomplete_trailing() {
assert_eq!(parse_boxes("[1, 2, 3, 4, 5]"), vec![[1, 2, 3, 4]]);
}
#[test]
fn re_match_full_ref_det_span() {
let text = "<|ref|>title<|/ref|><|det|>[10, 20, 30, 40]<|/det|>";
let ms = re_match(text);
assert!(ms.iter().any(|m| m.full == text && m.label == "title"));
let full = ms.iter().find(|m| m.label == "title").unwrap();
assert_eq!(full.boxes, vec![[10, 20, 30, 40]]);
}
#[test]
fn re_match_non_greedy_two_spans() {
let text =
"<|ref|>a<|/ref|><|det|>[1,2,3,4]<|/det|>X<|ref|>b<|/ref|><|det|>[5,6,7,8]<|/det|>";
let ms = scan_ref_spans(text);
assert_eq!(ms.len(), 2);
assert_eq!(ms[0].label, "a");
assert_eq!(ms[0].boxes, vec![[1, 2, 3, 4]]);
assert_eq!(ms[1].label, "b");
assert_eq!(ms[1].boxes, vec![[5, 6, 7, 8]]);
}
#[test]
fn re_match_bare_det_span() {
let text = "noise <|det|> figure [0, 0, 100, 100] <|/det|> tail";
let ms = scan_det_spans(text);
assert_eq!(ms.len(), 1);
assert_eq!(ms[0].label, "figure");
assert_eq!(ms[0].boxes, vec![[0, 0, 100, 100]]);
assert!(ms[0].full.starts_with("<|det|>"));
assert!(ms[0].full.ends_with("<|/det|>"));
}
#[test]
fn re_match_det_uses_python_unicode_whitespace() {
let text = "noise <|det|>\u{00a0}figure\u{00a0}[0, 0, 100, 100]\u{00a0}<|/det|> tail";
let ms = scan_det_spans(text);
assert_eq!(ms.len(), 1);
assert_eq!(ms[0].label, "figure");
assert_eq!(ms[0].boxes, vec![[0, 0, 100, 100]]);
}
#[test]
fn re_match_det_allows_unicode_word_continuation() {
let text = "noise <|det|> a\u{00e9}-label [1, 2, 3, 4] <|/det|> tail";
let ms = scan_det_spans(text);
assert_eq!(ms.len(), 1);
assert_eq!(ms[0].label, "a\u{00e9}-label");
assert_eq!(ms[0].boxes, vec![[1, 2, 3, 4]]);
}
#[test]
fn re_match_det_rejects_bad_label() {
let text = "<|det|>9bad [1,2,3,4]<|/det|>";
assert!(scan_det_spans(text).is_empty());
}
#[test]
fn is_image_predicate() {
let m = RefMatch {
full: "<|ref|>image<|/ref|><|det|>[1,2,3,4]<|/det|>".into(),
offset: 0,
label: "image".into(),
boxes: vec![[1, 2, 3, 4]],
level: None,
};
assert!(m.is_image());
let m2 = RefMatch {
full: "<|ref|>title<|/ref|><|det|>[1,2,3,4]<|/det|>".into(),
offset: 0,
label: "title".into(),
boxes: vec![[1, 2, 3, 4]],
level: None,
};
assert!(!m2.is_image());
}
#[test]
fn finalize_strips_other_spans_and_keeps_text() {
let raw =
format!("Heading\n<|ref|>title<|/ref|><|det|>[1,2,3,4]<|/det|>\nBody text{EOS_MARKER}");
let md = finalize(&raw, 1000, 1000).unwrap();
assert!(md.contains("Heading"));
assert!(md.contains("Body text"));
assert!(!md.contains("<|ref|>"));
assert!(!md.contains("<|det|>"));
assert!(!md.contains(EOS_MARKER));
}
#[test]
fn finalize_rewrites_image_spans_to_markdown() {
let raw = format!(
"Top\n<|ref|>image<|/ref|><|det|>[0,0,500,500]<|/det|>\n<|ref|>image<|/ref|><|det|>[1,1,2,2]<|/det|>End{EOS_MARKER}"
);
let md = finalize(&raw, 800, 600).unwrap();
assert!(md.contains(""));
assert!(md.contains(""));
assert!(md.contains("Top"));
assert!(md.contains("End"));
assert!(!md.contains("<|ref|>"));
}
#[test]
fn finalize_normalizes_latex_coloneqq() {
let raw = format!(
"x \\coloneqq y and a \\eqqcolon b <|ref|>note<|/ref|><|det|>[1,2,3,4]<|/det|>{EOS_MARKER}"
);
let md = finalize(&raw, 100, 100).unwrap();
assert!(md.contains("x := y"));
assert!(md.contains("a =: b"));
assert!(!md.contains("\\coloneqq"));
assert!(!md.contains("\\eqqcolon"));
}
#[test]
fn finalize_normalizes_latex_coloneqq_without_tags() {
let raw = format!("x \\coloneqq y and a \\eqqcolon b{EOS_MARKER}");
let md = finalize(&raw, 100, 100).unwrap();
assert_eq!(md, "x := y and a =: b");
}
#[test]
fn finalize_normalizes_latex_coloneqq_with_only_image_spans() {
let raw =
format!("x \\coloneqq y <|ref|>image<|/ref|><|det|>[0,0,10,10]<|/det|>{EOS_MARKER}");
let md = finalize(&raw, 100, 100).unwrap();
assert!(md.contains("x := y"));
assert!(md.contains(""));
assert!(!md.contains("\\coloneqq"));
}
#[test]
fn page_stream_streams_bodies_as_markers_arrive() {
let mut ps = PageStream::new();
let mut got: Vec<(usize, String)> = Vec::new();
ps.feed(" \n", |i, s| got.push((i, s.to_string())));
assert!(got.is_empty());
ps.feed(" \n<PAGE>\nalpha", |i, s| got.push((i, s.to_string())));
assert!(got.is_empty(), "page 1 is still in flight");
let text = " \n<PAGE>\nalpha\n<PAGE>\nbeta";
ps.feed(text, |i, s| got.push((i, s.to_string())));
assert_eq!(got, vec![(1, "alpha".to_string())]);
ps.feed(text, |i, s| got.push((i, s.to_string())));
assert_eq!(got.len(), 1, "re-feeding the same text must not re-emit");
ps.finish(text, |i, s| got.push((i, s.to_string())));
assert_eq!(
got,
vec![(1, "alpha".to_string()), (2, "beta".to_string())],
"finish flushes the final in-flight page"
);
println!(r#"{{"check":"page_stream_boundaries","pages":2,"result":"pass"}}"#);
}
#[test]
fn page_stream_without_markers_streams_whole_text_as_page_one() {
let mut ps = PageStream::new();
let mut got: Vec<(usize, String)> = Vec::new();
ps.feed("no markers at all", |i, s| got.push((i, s.to_string())));
assert!(got.is_empty(), "nothing streams before end of decode");
ps.finish(" no markers at all \n", |i, s| got.push((i, s.to_string())));
assert_eq!(got, vec![(1, "no markers at all".to_string())]);
let mut blank = 0usize;
PageStream::new().finish(" \n ", |_, _| blank += 1);
assert_eq!(blank, 0);
}
#[test]
fn page_stream_text_before_first_marker_is_page_one() {
let mut ps = PageStream::new();
let mut got: Vec<(usize, String)> = Vec::new();
ps.feed("first page", |i, s| got.push((i, s.to_string())));
assert!(got.is_empty());
let text = "first page\n<PAGE>\nsecond page";
ps.feed(text, |i, s| got.push((i, s.to_string())));
assert_eq!(got, vec![(1, "first page".to_string())]);
ps.feed(text, |i, s| got.push((i, s.to_string())));
assert_eq!(got.len(), 1, "re-feeding must not re-emit the leading page");
ps.finish(text, |i, s| got.push((i, s.to_string())));
assert_eq!(
got,
vec![
(1, "first page".to_string()),
(2, "second page".to_string())
]
);
}
#[test]
fn page_stream_marker_split_across_feeds_is_caught() {
let mut ps = PageStream::new();
let mut got: Vec<usize> = Vec::new();
ps.feed("<PAGE>one <PA", |i, _| got.push(i));
assert!(got.is_empty());
ps.feed("<PAGE>one <PAGE>two", |i, _| got.push(i));
assert_eq!(got, vec![1]);
}
#[test]
fn finalize_multi_splits_and_rejoins_pages() {
let raw = format!(" \n<PAGE>page one text<PAGE>page two text{EOS_MARKER}");
let md = finalize_multi(&raw, 2).unwrap();
assert_eq!(md, "<PAGE>\npage one text\n<PAGE>\npage two text");
}
#[test]
fn finalize_multi_keeps_text_when_model_emits_no_marker() {
let raw = format!("INVOICE #10042\nTotal due: 104.49{EOS_MARKER}");
let md = finalize_multi(&raw, 1).unwrap();
assert_eq!(md, "<PAGE>\nINVOICE #10042\nTotal due: 104.49");
}
#[test]
fn finalize_multi_keeps_text_before_first_marker_as_page_zero() {
let raw = format!(
"p0 <|ref|>image<|/ref|><|det|>[0,0,10,10]<|/det|><PAGE>p1 <|ref|>image<|/ref|><|det|>[0,0,10,10]<|/det|>{EOS_MARKER}"
);
let md = finalize_multi(&raw, 2).unwrap();
assert!(md.starts_with("<PAGE>\np0"));
assert!(md.contains(""));
assert!(md.contains(""));
assert_eq!(md.matches(PAGE_MARKER).count(), 2);
}
#[test]
fn finalize_multi_blank_decode_is_a_bare_marker() {
let md = finalize_multi(&format!(" {EOS_MARKER}"), 1).unwrap();
assert_eq!(md, "<PAGE>\n");
}
#[test]
fn finalize_multi_per_page_image_prefix() {
let raw = format!(
"<PAGE>p0 <|ref|>image<|/ref|><|det|>[0,0,10,10]<|/det|><PAGE>p1 <|ref|>image<|/ref|><|det|>[0,0,10,10]<|/det|>{EOS_MARKER}"
);
let md = finalize_multi(&raw, 2).unwrap();
assert!(md.contains(""));
assert!(md.contains(""));
}
#[test]
fn finalize_multi_passthrough_overflow_pages() {
let raw = format!(
"<PAGE>real <|ref|>title<|/ref|><|det|>[1,2,3,4]<|/det|><PAGE> extra <|ref|>x<|/ref|><|det|>[1,2,3,4]<|/det|> {EOS_MARKER}"
);
let md = finalize_multi(&raw, 1).unwrap();
assert!(!md.contains("<|ref|>title"));
assert!(md.contains("<|ref|>x<|/ref|>"));
}
#[test]
fn split_pages_by_token_keeps_n_bodies_and_reports_head_and_extras() {
use crate::tokenizer::special::PAGE;
use crate::tokenizer::tests::{byte_id, byte_tokenizer};
let t = byte_tokenizer();
let text = |s: &str| s.bytes().map(byte_id).collect::<Vec<_>>();
let zhong = "中".as_bytes();
let ids = [
text("hi "),
vec![PAGE],
text("one "),
vec![byte_id(zhong[0]), byte_id(zhong[1])],
vec![PAGE],
text("two"),
vec![PAGE],
]
.concat();
let split = split_pages_by_token(&t, &ids, PAGE, 2).unwrap();
assert_eq!(
split.bodies,
["hi one \u{FFFD}", "two"],
"the head joins body 1; the cut character ends it as U+FFFD"
);
assert_eq!(split.tokens, [3 + 4 + 2, 3]);
assert!(split.leading_text);
assert_eq!(split.extra_markers, 1, "the empty trailing body is dropped");
let md = document_markdown(&split.bodies);
assert_eq!(md, "<PAGE>\nhi one \u{FFFD}\n<PAGE>\ntwo");
assert_eq!(
finalize_multi("<PAGE>a\n<PAGE>b", 2).unwrap(),
document_markdown(&["a", "b"])
);
let ids = [text("\n"), vec![PAGE], text("x"), vec![PAGE], text("y")].concat();
let split = split_pages_by_token(&t, &ids, PAGE, 2).unwrap();
assert_eq!(split.bodies, ["x", "y"]);
assert_eq!(
split.tokens,
[2, 1],
"the blank head's token counts toward page 1"
);
assert!(!split.leading_text);
assert_eq!(split.extra_markers, 0);
let ids = [vec![PAGE], text("a"), vec![PAGE], text("z")].concat();
let split = split_pages_by_token(&t, &ids, PAGE, 1).unwrap();
assert_eq!(split.bodies, ["a\nz"]);
assert_eq!(split.extra_markers, 1);
let split = split_pages_by_token(&t, &text("abc"), PAGE, 3).unwrap();
assert_eq!(split.bodies, ["abc"]);
assert!(split.leading_text);
assert!(
split_pages_by_token(&t, &[], PAGE, 3)
.unwrap()
.bodies
.is_empty()
);
assert!(split_pages_by_token(&t, &[7], PAGE, 1).is_err());
}
#[test]
fn split_window_pages_keeps_own_pages_only() {
use crate::tokenizer::special::PAGE;
use crate::tokenizer::tests::{byte_id, byte_tokenizer};
let t = byte_tokenizer();
let text = |s: &str| s.bytes().map(byte_id).collect::<Vec<_>>();
let pg = || vec![PAGE];
let ids = [
text("h "),
pg(),
text("a"),
pg(),
text("b"),
pg(),
text("la"),
]
.concat();
let split = split_window_pages(&t, &ids, PAGE, 2, false, true).unwrap();
assert_eq!(split.bodies, ["h a", "b"]);
assert_eq!(split.tokens, [3, 1]);
assert!(split.leading_text);
assert_eq!(split.extra_markers, 0, "the lookahead's marker is the end");
let ids = [text("a"), pg(), text("b"), pg()].concat();
let split = split_window_pages(&t, &ids, PAGE, 2, true, true).unwrap();
assert_eq!(split.bodies, ["a", "b"]);
assert!(!split.leading_text);
assert_eq!(split.extra_markers, 0);
let ids = [text(" "), pg(), pg(), text("c"), pg()].concat();
let split = split_window_pages(&t, &ids, PAGE, 3, true, true).unwrap();
assert_eq!(split.bodies, [" ", "", "c"], "blank bodies keep alignment");
let ids = [pg(), text(" "), pg(), text("b"), pg(), text("z")].concat();
let split = split_window_pages(&t, &ids, PAGE, 2, false, false).unwrap();
assert_eq!(split.bodies, [" ", "b"], "no leading-blank drop");
assert_eq!(split.extra_markers, 1, "a last window's extra marker");
let split = split_window_pages(&t, &text("ab"), PAGE, 2, true, true).unwrap();
assert_eq!(split.bodies, ["ab"], "stopped inside page 1");
}
#[test]
fn token_page_stream_emits_the_split_bodies() {
use crate::tokenizer::special::PAGE;
use crate::tokenizer::tests::{byte_id, byte_tokenizer};
let t = byte_tokenizer();
let alphabet: Vec<u32> = [b'a', b' ', b'\n', 0xE4, 0xB8, 0xAD]
.into_iter()
.map(byte_id)
.collect();
let mut state = 0x005e_edb6_u64;
let mut next = |n: u64| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) % n
};
for _ in 0..2000 {
let pages = 1 + next(4) as usize;
let mut ids = Vec::new();
let mut markers = 0;
for _ in 0..next(40) {
if next(6) == 0 && markers <= pages {
ids.push(PAGE);
markers += 1;
if markers > pages {
break;
}
} else {
ids.push(alphabet[next(alphabet.len() as u64) as usize]);
}
}
let split = split_pages_by_token(&t, &ids, PAGE, pages).unwrap();
let mut stream = TokenPageStream::new(&t, PAGE, pages);
let mut got: Vec<(usize, String, usize)> = Vec::new();
for &id in &ids {
stream.push(id, |k, body, n| got.push((k, body.to_string(), n)));
}
stream.finish(|k, body, n| got.push((k, body.to_string(), n)));
let want: Vec<(usize, String, usize)> = split
.bodies
.iter()
.zip(&split.tokens)
.enumerate()
.filter(|(_, (b, _))| markers > 0 || !b.trim().is_empty())
.map(|(i, (b, &n))| (i + 1, b.trim().to_string(), n))
.collect();
assert_eq!(got, want, "{pages} pages: {ids:?}");
}
}
#[test]
fn parse_layout_rescales_boxes() {
let raw = format!("<|ref|>title<|/ref|><|det|>[0, 0, 999, 999]<|/det|>{EOS_MARKER}");
let layout = parse_layout(&raw, 1920, 1080);
let span = layout
.iter()
.find(|s| s.label == "title")
.expect("title span");
assert_eq!(span.boxes, vec![[0, 0, 1920, 1080]]);
assert_eq!(span.level, None);
}
#[test]
fn re_match_empty_when_no_tags() {
assert!(re_match("just plain markdown text\n# Heading").is_empty());
}
#[test]
fn finalize_plain_text_passthrough() {
let raw = format!("# Title\n\nSome **markdown** body.\n{EOS_MARKER}");
let md = finalize(&raw, 640, 480).unwrap();
assert_eq!(md, "# Title\n\nSome **markdown** body.");
}
#[test]
fn figure_refs_enumerate_image_spans_with_tokens_matching_assemble_page() {
let decoded = concat!(
"<|ref|>title<|/ref|><|det|>[[0,0,500,500]]<|/det|>",
"<|ref|>image<|/ref|><|det|>[[0,0,999,999]]<|/det|>",
"<|ref|>image<|/ref|><|det|>[[0,0,499,499]]<|/det|>",
);
let refs = figure_refs(decoded, 200, 100, "");
assert_eq!(refs.len(), 2, "only the two image spans are figures");
assert_eq!(refs[0].index, 0);
assert_eq!(refs[0].label, "image");
assert_eq!(refs[0].markdown_ref, "");
assert_eq!(refs[0].boxes, vec![[0, 0, 200, 100]]);
assert_eq!(refs[1].index, 1);
assert_eq!(refs[1].markdown_ref, "");
let md = finalize(decoded, 200, 100).unwrap();
assert!(md.contains(&refs[0].markdown_ref), "md: {md}");
assert!(md.contains(&refs[1].markdown_ref), "md: {md}");
}
#[test]
fn figure_refs_multipage_prefix_matches_assemble_page() {
let decoded = "<|ref|>image<|/ref|><|det|>[[1,2,3,4]]<|/det|>";
let refs = figure_refs(decoded, 999, 999, "page_0_");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].markdown_ref, "");
let md = finalize_multi(&format!("<PAGE>\n{decoded}"), 1).unwrap();
assert!(md.contains(""), "md: {md}");
}
#[test]
fn figure_refs_empty_without_image_spans() {
let decoded = "<|ref|>title<|/ref|><|det|>[[0,0,9,9]]<|/det|>plain text";
assert!(figure_refs(decoded, 100, 100, "").is_empty());
}
#[test]
fn v2_blocks_render_in_v1_shape_with_layout_and_figures() {
let page = "<BLOCK>header[391, 31, 513, 53]<FORK>( 535 )\n\
<BLOCK>title<LEVEL1>[223, 178, 656, 197]<FORK>APPENDIX\n\
<BLOCK>image[0, 0, 499, 499]<FORK>\n\
<BLOCK>image[0, 0, 499, 499]<FORK>\n\
<BLOCK>cross_text[10, 900, 990, 990]<FORK>continues";
let md = finalize(page, 1182, 1837).unwrap();
assert_eq!(
md,
"( 535 )\nAPPENDIX\n\n\n\n\ncontinues"
);
let layout = parse_layout(page, 1182, 1837);
let labels: Vec<_> = layout.iter().map(|s| (s.label.as_str(), s.level)).collect();
assert_eq!(
labels,
[
("header", None),
("title", Some(1)),
("image", None),
("image", None),
("cross_text", None),
]
);
assert_eq!(layout[0].boxes, vec![[462, 57, 606, 97]]);
assert_eq!(layout[1].boxes, vec![[263, 327, 776, 362]]);
let refs = figure_refs(page, 1182, 1837, "");
assert_eq!(refs.len(), 2);
for (i, r) in refs.iter().enumerate() {
assert_eq!(r.index, i);
assert_eq!(r.markdown_ref, format!(""));
assert_eq!(md.matches(&r.markdown_ref).count(), 1, "{md}");
}
}
#[test]
fn document_pages_number_figures_and_map_boxes_per_page() {
let bodies = [
"<BLOCK>image[100, 200, 500, 999]<FORK>\n<BLOCK>text[0, 0, 999, 499]<FORK>a",
"<BLOCK>image[100, 200, 500, 999]<FORK>",
];
let sizes = [(1998, 999), (999, 2997)];
let pages: Vec<PageOutput> = bodies
.iter()
.zip(sizes)
.enumerate()
.map(|(i, (body, size))| finalize_page(body, i, size))
.collect();
assert_eq!(
join_page_texts(pages.iter().map(|p| &p.text)),
"<PAGE>\n\n\na\n<PAGE>\n\n"
);
let boxes = |p: &PageOutput| -> Vec<(String, Vec<[i64; 4]>)> {
p.layout
.iter()
.map(|s| (s.label.clone(), s.boxes.clone()))
.collect()
};
assert_eq!(
boxes(&pages[0]),
[
("image".to_string(), vec![[200, 200, 1000, 999]]),
("text".to_string(), vec![[0, 0, 1998, 499]]),
]
);
assert_eq!(
boxes(&pages[1]),
[("image".to_string(), vec![[100, 600, 500, 2997]])]
);
for (page, want) in pages
.iter()
.zip([[200, 200, 1000, 999], [100, 600, 500, 2997]])
{
let [figure] = page.figures.as_slice() else {
panic!("one figure per page: {:?}", page.figures);
};
assert_eq!(figure.boxes, [want]);
assert_eq!(page.text.matches(&figure.markdown_ref).count(), 1);
}
}
#[test]
fn document_markdown_joins_the_page_texts() {
assert_eq!(join_page_texts(Vec::<String>::new()), "<PAGE>\n");
assert_eq!(join_page_texts(["a"]), "<PAGE>\na");
for text in [
"<PAGE>a<PAGE>b",
"lead<PAGE>b<PAGE>c",
" \n<PAGE><|ref|>image<|/ref|><|det|>[[1, 2, 3, 4]]<|/det|>\n<PAGE>x",
"no marker at all",
] {
let bodies: Vec<&str> = text_page_bodies(text).collect();
assert_eq!(bodies.len(), page_body_count(text), "{text:?}");
assert_eq!(
finalize_multi(text, bodies.len()).unwrap(),
document_markdown(&bodies),
"{text:?}"
);
}
}
fn document_markdown<S: AsRef<str>>(bodies: &[S]) -> String {
join_page_texts(
bodies
.iter()
.enumerate()
.map(|(i, b)| finalize_page(b.as_ref(), i, (999, 999)).text),
)
}
#[test]
fn v2_block_prefixes_off_the_grammar_stay_as_emitted() {
for raw in [
"<BLOCK>text[1, 2, 3]<FORK>x",
"<BLOCK>text[1, 2, 3, 4, 5]<FORK>x",
"<BLOCK>text[1, 2, 3, 4]x",
"<BLOCK>text [1, 2, 3, 4]<FORK>x",
"<BLOCK>text[1, -2, 3, 4]<FORK>x",
"<BLOCK>text[1, 2,, 4]<FORK>x",
"<BLOCK>title<LEVEL>[1, 2, 3, 4]<FORK>x",
"<BLOCK>title<LEVEL999>[1, 2, 3, 4]<FORK>x",
"<BLOCK>[1, 2, 3, 4]<FORK>x",
"<BLOCK>text[12, 3",
] {
assert_eq!(finalize(raw, 999, 999).unwrap(), raw);
assert!(parse_layout(raw, 999, 999).is_empty(), "{raw}");
}
let cut = "<BLOCK>text[1, 2, 3, 4]<FORK>a\n<BLOCK>image[12, 3";
assert_eq!(finalize(cut, 999, 999).unwrap(), "a\n<BLOCK>image[12, 3");
assert!(figure_refs(cut, 999, 999, "").is_empty());
}
#[test]
fn every_oracle_v2_block_parses_and_none_reaches_the_markdown() {
const LABELS: [&str; 13] = [
"text",
"title",
"header",
"footer",
"page_number",
"page_footnote",
"cross_text",
"append_text",
"table",
"image",
"image_caption",
"other",
"equation",
];
let dir =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/uocr2_oracle");
let mut outputs = 0;
let mut blocks = 0;
let mut images = 0;
for entry in std::fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "md") {
continue;
}
outputs += 1;
let name = path.display().to_string();
let raw = std::fs::read_to_string(&path).unwrap();
let spans = scan_block_spans(&raw);
assert_eq!(spans.len(), raw.matches(BLOCK_OPEN).count(), "{name}");
assert!(re_match(&raw).is_empty(), "{name}");
for s in &spans {
assert!(LABELS.contains(&s.label.as_str()), "{name}: {}", s.full);
assert!(
s.level.is_none() || s.label == "title",
"{name}: {}",
s.full
);
let [x1, y1, x2, y2] = s.boxes[0];
assert!(
x1 <= x2 && y1 <= y2 && x2 <= 999 && y2 <= 999,
"{name}: {}",
s.full
);
}
blocks += spans.len();
images += spans.iter().filter(|s| s.is_image()).count();
let md = finalize_multi(&raw, usize::MAX).unwrap();
for grammar in [BLOCK_OPEN, BLOCK_FORK, BLOCK_LEVEL] {
assert!(!md.contains(grammar), "{name}: {grammar} in the markdown");
}
for (page, body) in raw.split(PAGE_MARKER).skip(1).enumerate() {
let base = format!("page_{page}_");
for r in figure_refs(body, 999, 999, &base) {
assert_eq!(md.matches(&r.markdown_ref).count(), 1, "{name}");
}
}
}
assert_eq!(outputs, 10);
assert_eq!((blocks, images), (612, 5));
let raw = std::fs::read_to_string(dir.join("S1-page_0590.md")).unwrap();
let md = finalize_multi(&raw, 1).unwrap();
assert!(
md.starts_with(
"<PAGE>\n( 535 )\nAPPENDIX TO CHAPTER XXIV.\n\
LOSSES OF H.M. SHIPS FROM THE REVOLUTION TO 1714.\nNote.—This list"
),
"{md}"
);
}
fn span_texts_of(decoded: &str) -> Vec<(String, Option<String>)> {
parse_layout(decoded, 999, 999)
.into_iter()
.map(|span| (span.label, span.text))
.collect()
}
fn pair(label: &str, text: Option<&str>) -> (String, Option<String>) {
(label.to_owned(), text.map(str::to_owned))
}
#[test]
fn a_span_holds_what_the_model_wrote_after_its_marker() {
let v2 = "lead\n<BLOCK>title<LEVEL1>[1, 2, 3, 4]<FORK># Head\n\
<BLOCK>image[5, 6, 7, 8]<FORK>\
<BLOCK>text[1, 1, 2, 2]<FORK>a \\coloneqq b\n\
<BLOCK>text[1, 1, 2, 2]<FORK>second\n\
<BLOCK>footer[3, 3, 4, 4]<FORK>";
assert_eq!(
span_texts_of(v2),
[
pair("title", Some("# Head")),
pair("image", None),
pair("text", Some("a := b")),
pair("text", Some("second")),
pair("footer", None),
]
);
let v1 = "<|ref|>title<|/ref|><|det|>[[1, 2, 3, 4]]<|/det|>\n# Head\n\n\
<|ref|>image<|/ref|><|det|>[[5, 6, 7, 8]]<|/det|>\n\
<|det|>text [1, 1, 2, 2]<|/det|>\nbare det\n\
<|ref|>text<|/ref|><|det|>[[3, 3, 4, 4]]<|/det|>\nlast";
let spans = span_texts_of(v1);
assert_eq!(
spans,
[
pair("title", Some("# Head")),
pair("image", None),
pair("text", Some("last")),
pair("text", Some("bare det")),
],
"reference order, which puts bare det spans last"
);
let cut = "<BLOCK>text[1, 2, 3, 4]<FORK>a\n<BLOCK>image[12, 3";
assert_eq!(
span_texts_of(cut),
[pair("text", Some("a\n<BLOCK>image[12, 3"))]
);
}
#[test]
fn oracle_span_texts_partition_each_page() {
let words =
|text: &str| -> Vec<String> { text.split_whitespace().map(str::to_owned).collect() };
let dir =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/uocr2_oracle");
let mut pages = 0;
let mut spans = 0;
let mut with_text = 0;
for entry in std::fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "md") {
continue;
}
let raw = std::fs::read_to_string(&path).unwrap();
for (page, body) in raw.split(PAGE_MARKER).skip(1).enumerate() {
let name = format!("{} page {page}", path.display());
let out = finalize_page(body, page, (999, 999));
pages += 1;
spans += out.layout.len();
let texts: Vec<&str> = out
.layout
.iter()
.filter_map(|span| span.text.as_deref())
.collect();
with_text += texts.len();
let mut markdown = out.text.clone();
for figure in &out.figures {
markdown = markdown.replace(&figure.markdown_ref, "");
}
assert_eq!(words(&texts.join("\n")), words(&markdown), "{name}");
}
}
assert_eq!((pages, spans, with_text), (80, 612, 601));
}
}