use crate::Block;
#[doc(hidden)]
pub const EXTRA: [&str; 11] = [
"leader_run",
"numeric_share",
"short_words",
"kind_is_cell",
"kind_is_paragraph",
"prev_is_cell",
"next_is_cell",
"gap_above_line",
"gap_below_line",
"neighbours_within_a_line",
"x_frac_of_widest",
];
const LEADER_RUN: usize = 3;
fn is_leader(c: char) -> bool {
matches!(c, '.' | '-' | '_' | '\u{00b7}' | '\u{2022}') || ('\u{2010}'..='\u{2015}').contains(&c)
}
fn has_leader_run(text: &str) -> bool {
let mut run = 0usize;
for c in text.chars() {
run = if is_leader(c) { run + 1 } else { 0 };
if run >= LEADER_RUN {
return true;
}
}
false
}
fn is_numeric(word: &str) -> bool {
let body = word.trim_start_matches([' ', '$', '\u{20ac}', '\u{a3}', '(']);
let body = body.trim_end_matches(['%', ')', ' ']);
let body = body.strip_prefix('-').unwrap_or(body);
!body.is_empty()
&& body
.chars()
.all(|c| c.is_ascii_digit() || c == ',' || c == '.')
}
#[doc(hidden)]
pub fn names() -> Vec<&'static str> {
let mut out: Vec<&'static str> = super::features::BLOCK_FEATURES[..24].to_vec();
out.extend(EXTRA);
out
}
#[doc(hidden)]
pub fn vector(
features: &[super::features::BlockFeatures],
blocks: &[&Block],
index: usize,
) -> [f64; 35] {
let f = &features[index];
let kinds: Vec<&str> = features.iter().map(|b| b.kind).collect();
let whole = super::features::block_vector(f);
let head: [f64; 24] = whole[..24].try_into().expect("24 of 30");
vector_of(
head,
&kinds,
f.size,
(f.gap_above_em, f.gap_below_em),
blocks,
index,
)
}
#[doc(hidden)]
pub fn vector_of(
head: [f64; 24],
kinds: &[&str],
size: f64,
gaps: (f64, f64),
blocks: &[&Block],
index: usize,
) -> [f64; 35] {
let block = blocks[index];
let text = block.text.as_str();
let words: Vec<&str> = text.split_whitespace().collect();
let numeric = words.iter().filter(|w| is_numeric(w)).count();
let short = words.iter().filter(|w| w.chars().count() <= 3).count();
let denominator = words.len().max(1) as f64;
let line = if size > 0.0 { size } else { 10.0 };
let kind_is =
|k: usize, want: &str| -> bool { kinds.get(k).map(|b| *b == want).unwrap_or(false) };
let (top, bottom) = (block.bbox[3], block.bbox[1]);
let mut close = 0usize;
for (k, other) in blocks.iter().enumerate() {
if k == index {
continue;
}
if (other.bbox[3] - top).abs() <= line || (other.bbox[1] - bottom).abs() <= line {
close += 1;
}
}
let widest = blocks
.iter()
.map(|b| b.bbox[2] - b.bbox[0])
.fold(1.0f64, f64::max);
let mine = block.bbox[2] - block.bbox[0];
let tail = [
if has_leader_run(text) { 1.0 } else { 0.0 },
numeric as f64 / denominator,
short as f64 / denominator,
if kinds[index] == "table_cell" {
1.0
} else {
0.0
},
if kinds[index] == "paragraph" {
1.0
} else {
0.0
},
if index > 0 && kind_is(index - 1, "table_cell") {
1.0
} else {
0.0
},
if kind_is(index + 1, "table_cell") {
1.0
} else {
0.0
},
gaps.0,
gaps.1,
close as f64,
mine / widest.max(1e-6),
];
let mut out = [0.0f64; 35];
out[..24].copy_from_slice(&head);
out[24..].copy_from_slice(&tail);
out
}