use serde::{Deserialize, Serialize};
use crate::analytics::page_stats::{PageSignature, PageStats};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PageRoleKind {
Body,
NonBody,
}
#[derive(Debug, Clone)]
pub struct PageRolesConfig {
pub strict_fit: f32,
pub lenient_min_tokens: u32,
}
impl Default for PageRolesConfig {
fn default() -> Self {
Self {
strict_fit: 0.15,
lenient_min_tokens: 100,
}
}
}
pub fn classify_page_roles(stats: &mut PageStats, config: &PageRolesConfig) {
let n = stats.pages.len();
if n == 0 {
stats.body_start_page = 0;
stats.body_end_page = 0;
return;
}
let mut start_idx: Option<usize> = None;
for i in 0..n {
if !is_non_body_strict(&stats.pages[i], config) {
start_idx = Some(i);
break;
}
}
let Some(body_start) = start_idx else {
for p in stats.pages.iter_mut() {
p.role = Some(PageRoleKind::NonBody);
}
stats.body_start_page = 0;
stats.body_end_page = 0;
return;
};
let mut body_end = body_start;
for i in (body_start..n).rev() {
let is_last_position = i == n - 1;
let non_body = if is_last_position {
is_non_body_lenient(&stats.pages[i], config)
} else {
is_non_body_strict(&stats.pages[i], config)
};
if !non_body {
body_end = i;
break;
}
}
for (i, p) in stats.pages.iter_mut().enumerate() {
p.role = Some(if i >= body_start && i <= body_end {
PageRoleKind::Body
} else {
PageRoleKind::NonBody
});
}
stats.body_start_page = stats.pages[body_start].page_number;
stats.body_end_page = stats.pages[body_end].page_number;
}
fn is_non_body_strict(page: &PageSignature, config: &PageRolesConfig) -> bool {
page.heatmap_fit < config.strict_fit
}
fn is_non_body_lenient(page: &PageSignature, config: &PageRolesConfig) -> bool {
page.heatmap_fit < config.strict_fit && page.n_tokens < config.lenient_min_tokens
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analytics::page_stats::PageSignature;
fn mk_page(page_number: u32, heatmap_fit: f32, n_tokens: u32) -> PageSignature {
PageSignature {
page_number,
n_tokens,
italic_tokens: 0,
bold_tokens: 0,
normal_tokens: n_tokens,
heatmap_fit,
n_peaks_y: 0,
y_peak_cv: 0.0,
role: None,
}
}
fn mk_stats(pages: Vec<PageSignature>) -> PageStats {
PageStats {
pages,
regions: vec![],
body_start_page: 0,
body_end_page: 0,
}
}
#[test]
fn all_body_single_page() {
let mut s = mk_stats(vec![mk_page(1, 0.85, 600)]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 1);
assert_eq!(s.body_end_page, 1);
assert_eq!(s.pages[0].role, Some(PageRoleKind::Body));
}
#[test]
fn all_body_multi_page() {
let mut s = mk_stats((1..=5).map(|p| mk_page(p, 0.80, 500)).collect());
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 1);
assert_eq!(s.body_end_page, 5);
for p in &s.pages {
assert_eq!(p.role, Some(PageRoleKind::Body));
}
}
#[test]
fn cover_page_excluded_from_body_start() {
let mut s = mk_stats(vec![
mk_page(1, 0.05, 50), mk_page(2, 0.80, 500),
mk_page(3, 0.80, 500),
mk_page(4, 0.80, 500),
]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 2);
assert_eq!(s.body_end_page, 4);
assert_eq!(s.pages[0].role, Some(PageRoleKind::NonBody));
for p in &s.pages[1..] {
assert_eq!(p.role, Some(PageRoleKind::Body));
}
}
#[test]
fn multiple_front_non_body_pages() {
let mut s = mk_stats(vec![
mk_page(1, 0.02, 20), mk_page(2, 0.10, 40), mk_page(3, 0.08, 80), mk_page(4, 0.80, 500), mk_page(5, 0.80, 500),
]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 4);
assert_eq!(s.body_end_page, 5);
for (i, p) in s.pages.iter().enumerate() {
let expected = if i < 3 {
PageRoleKind::NonBody
} else {
PageRoleKind::Body
};
assert_eq!(p.role, Some(expected), "page {}", p.page_number);
}
}
#[test]
fn trailing_sparse_last_page_is_body() {
let mut s = mk_stats(vec![
mk_page(1, 0.80, 500),
mk_page(2, 0.80, 500),
mk_page(3, 0.10, 250), ]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_end_page, 3);
assert_eq!(s.pages[2].role, Some(PageRoleKind::Body));
}
#[test]
fn trailing_empty_last_page_is_non_body() {
let mut s = mk_stats(vec![
mk_page(1, 0.80, 500),
mk_page(2, 0.80, 500),
mk_page(3, 0.05, 50), ]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 1);
assert_eq!(s.body_end_page, 2);
assert_eq!(s.pages[2].role, Some(PageRoleKind::NonBody));
}
#[test]
fn trailing_chain_of_non_body() {
let mut s = mk_stats(vec![
mk_page(1, 0.80, 500),
mk_page(2, 0.80, 500),
mk_page(3, 0.10, 80), mk_page(4, 0.10, 80), mk_page(5, 0.05, 50), ]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 1);
assert_eq!(s.body_end_page, 2);
assert_eq!(s.pages[2].role, Some(PageRoleKind::NonBody));
assert_eq!(s.pages[3].role, Some(PageRoleKind::NonBody));
assert_eq!(s.pages[4].role, Some(PageRoleKind::NonBody));
}
#[test]
fn strict_applies_to_non_end_back_pages() {
let mut s = mk_stats(vec![
mk_page(1, 0.80, 500),
mk_page(2, 0.80, 500),
mk_page(3, 0.10, 500), mk_page(4, 0.85, 600), ]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 1);
assert_eq!(s.body_end_page, 4);
assert_eq!(s.pages[2].role, Some(PageRoleKind::Body));
}
#[test]
fn all_non_body_yields_zero_extent() {
let mut s = mk_stats(vec![
mk_page(1, 0.05, 30),
mk_page(2, 0.08, 40),
mk_page(3, 0.06, 50), ]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 0);
assert_eq!(s.body_end_page, 0);
for p in &s.pages {
assert_eq!(p.role, Some(PageRoleKind::NonBody));
}
}
#[test]
fn empty_input_zero_extent() {
let mut s = mk_stats(vec![]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 0);
assert_eq!(s.body_end_page, 0);
}
#[test]
fn idempotent() {
let mut s = mk_stats(vec![
mk_page(1, 0.05, 50),
mk_page(2, 0.80, 500),
mk_page(3, 0.80, 500),
mk_page(4, 0.10, 200),
]);
let cfg = PageRolesConfig::default();
classify_page_roles(&mut s, &cfg);
let snapshot: Vec<_> = s.pages.iter().map(|p| p.role).collect();
let start1 = s.body_start_page;
let end1 = s.body_end_page;
classify_page_roles(&mut s, &cfg);
assert_eq!(s.body_start_page, start1);
assert_eq!(s.body_end_page, end1);
for (i, p) in s.pages.iter().enumerate() {
assert_eq!(p.role, snapshot[i]);
}
}
#[test]
fn non_contiguous_page_numbers() {
let mut s = mk_stats(vec![
mk_page(7, 0.05, 50),
mk_page(8, 0.80, 500),
mk_page(9, 0.80, 500),
]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 8);
assert_eq!(s.body_end_page, 9);
}
#[test]
fn front_and_back_non_body() {
let mut s = mk_stats(vec![
mk_page(1, 0.05, 50), mk_page(2, 0.80, 500),
mk_page(3, 0.80, 500),
mk_page(4, 0.80, 500),
mk_page(5, 0.05, 50), ]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 2);
assert_eq!(s.body_end_page, 4);
assert_eq!(s.pages[0].role, Some(PageRoleKind::NonBody));
assert_eq!(s.pages[4].role, Some(PageRoleKind::NonBody));
}
#[test]
fn fit_at_threshold_is_body() {
let mut s = mk_stats(vec![mk_page(1, 0.15, 500), mk_page(2, 0.80, 500)]);
classify_page_roles(&mut s, &PageRolesConfig::default());
assert_eq!(s.body_start_page, 1);
assert_eq!(s.pages[0].role, Some(PageRoleKind::Body));
}
}