// =============================================================================
// Main Content Selection
// =============================================================================
///|
fn find_best_readability_candidate(
candidates : Array[@aom.AccessibilityNode],
scores : Map[String, Double],
) -> @aom.AccessibilityNode? {
let mut best : @aom.AccessibilityNode? = None
let mut best_score = 0.0
for candidate in candidates {
if !is_readability_selection_candidate(candidate) {
continue
}
let score = get_readability_score(scores, candidate)
if score > best_score {
best_score = score
best = Some(candidate)
}
}
best
}
///|
fn is_readability_selection_candidate(node : @aom.AccessibilityNode) -> Bool {
if node.role == @aom.Document {
return false
}
match node.tag_name {
Some(tag) => if tag == "html" || tag == "body" { false } else { true }
None => true
}
}
///|
fn find_best_readability_block(
blocks : Array[ContentBlock],
scores : Map[String, Double],
) -> ContentBlock? {
let mut best : ContentBlock? = None
let mut best_score = 0.0
for block in blocks {
let score = get_readability_score(scores, block.node)
if score > best_score {
best_score = score
best = Some(block)
}
}
best
}
///|
fn select_base_main_content(
article_node : @aom.AccessibilityNode?,
top_block : ContentBlock?,
config : ExtractConfig,
readability_scores : Map[String, Double],
) -> @aom.AccessibilityNode? {
match article_node {
Some(node) => {
let article_text = get_text_content(node)
let article_len = article_text.length()
match top_block {
Some(block) => {
let block_text = get_text_content(block.node)
let block_len = block_text.length()
let article_readability = get_readability_score(
readability_scores, node,
)
let block_readability = get_readability_score(
readability_scores,
block.node,
)
// Prefer high-readability candidate if article/main looks weak
if block_readability > article_readability * 1.4 &&
block_len >= config.min_text_length {
return Some(block.node)
}
// Choose based on relative text lengths:
// - If content_block is 20x+ larger, it's likely the real content
// - Otherwise prefer semantic article/main
if block_len > article_len * 20 {
Some(block.node)
} else if article_len >= config.min_text_length {
Some(node)
} else {
Some(block.node)
}
}
None =>
if article_len >= config.min_text_length {
Some(node)
} else {
None
}
}
}
None =>
match top_block {
Some(block) => Some(block.node)
None => None
}
}
}
///|
fn refine_main_content(
base_main : @aom.AccessibilityNode?,
best_readability_block : ContentBlock?,
best_readability_candidate : @aom.AccessibilityNode?,
config : ExtractConfig,
readability_scores : Map[String, Double],
) -> @aom.AccessibilityNode? {
let readability_node = select_readability_node(
best_readability_block, best_readability_candidate, readability_scores,
)
match (base_main, readability_node) {
(None, Some(candidate)) => Some(candidate)
(Some(main), Some(candidate)) => {
let main_text = get_text_content(main)
let main_len = main_text.length()
let read_text = get_text_content(candidate)
let read_len = read_text.length()
if main.id == candidate.id {
return Some(main)
}
let main_readability = get_readability_score(readability_scores, main)
let read_readability = get_readability_score(
readability_scores, candidate,
)
let main_nav_penalty = calculate_navigation_penalty(main, main_text)
let read_nav_penalty = calculate_navigation_penalty(candidate, read_text)
let main_share_penalty = calculate_share_penalty(main_text)
let read_share_penalty = calculate_share_penalty(read_text)
let main_caption_penalty = calculate_caption_penalty(main_text)
let read_caption_penalty = calculate_caption_penalty(read_text)
let main_related_penalty = calculate_related_penalty(main, main_text)
let read_related_penalty = calculate_related_penalty(candidate, read_text)
let main_layout_bonus = calculate_layout_bonus(main, config)
let read_layout_bonus = calculate_layout_bonus(candidate, config)
if read_len >= config.min_text_length {
if main_len < config.min_text_length {
return Some(candidate)
}
let main_probably = is_probably_content(main)
let read_probably = is_probably_content(candidate)
if read_probably &&
is_descendant(main, candidate.id) &&
should_prefer_descendant(main, candidate, main_text, read_text) {
return Some(candidate)
}
if read_probably {
if !main_probably {
return Some(candidate)
}
if main_share_penalty <= 0.4 && read_share_penalty >= 0.8 {
return Some(candidate)
}
if main_caption_penalty <= 0.4 && read_caption_penalty >= 0.8 {
return Some(candidate)
}
if main_related_penalty <= 0.4 && read_related_penalty >= 0.8 {
return Some(candidate)
}
if read_layout_bonus > main_layout_bonus * 1.2 &&
read_layout_bonus >= 0.95 {
return Some(candidate)
}
if main_nav_penalty < 0.6 && read_nav_penalty >= 0.9 {
return Some(candidate)
}
if should_prefer_smaller_container(
main_len, read_len, main_readability, read_readability,
) {
return Some(candidate)
}
if read_readability > main_readability * 1.6 {
return Some(candidate)
}
} else if !main_probably && read_readability > main_readability * 1.8 {
return Some(candidate)
}
}
if should_promote_readability_candidate(
main, candidate, main_text, read_text,
) {
return Some(candidate)
}
Some(main)
}
(Some(main), None) => Some(main)
(None, None) => None
}
}
///|
fn should_prefer_descendant(
main : @aom.AccessibilityNode,
candidate : @aom.AccessibilityNode,
main_text : String,
candidate_text : String,
) -> Bool {
let main_len = main_text.length()
let candidate_len = candidate_text.length()
if candidate_len == 0 || main_len == 0 {
return false
}
if main_len < candidate_len * 2 {
return false
}
let main_density = calculate_readability_text_density(main)
let cand_density = calculate_readability_text_density(candidate)
let main_link_density = calculate_link_density(main, main_text)
let cand_link_density = calculate_link_density(candidate, candidate_text)
let main_punct = get_punctuation_density(main_text)
let cand_punct = get_punctuation_density(candidate_text)
let main_rep = calculate_repetition_penalty(main_text)
let cand_rep = calculate_repetition_penalty(candidate_text)
let density_better = cand_density >= main_density * 1.2
let link_better = cand_link_density <= main_link_density * 0.85
let punct_better = cand_punct >= 1.0 && cand_punct >= main_punct * 1.2
let repetition_better = cand_rep >= main_rep + 0.2
if density_better && link_better {
return true
}
if main_link_density > 0.3 && cand_link_density < 0.2 {
return true
}
if punct_better && (density_better || link_better || repetition_better) {
return true
}
if repetition_better && (cand_punct >= main_punct || density_better) {
return true
}
if main_rep < 0.4 && cand_rep >= 0.6 {
return true
}
if main_punct < 0.8 && cand_punct >= 1.2 && cand_link_density < 0.25 {
return true
}
if main_len >= candidate_len * 3 &&
candidate_len >= 800 &&
cand_punct >= 1.0 &&
cand_link_density <= 0.4 &&
cand_rep >= main_rep {
return true
}
false
}
///|
fn should_promote_readability_candidate(
main : @aom.AccessibilityNode,
candidate : @aom.AccessibilityNode,
main_text : String,
candidate_text : String,
) -> Bool {
let main_len = main_text.length()
let candidate_len = candidate_text.length()
if candidate_len == 0 || main_len == 0 {
return false
}
if candidate_len < main_len * 3 {
return false
}
let candidate_link_density = calculate_link_density(candidate, candidate_text)
if candidate_link_density > 0.4 {
return false
}
let candidate_nav_penalty = calculate_navigation_penalty(
candidate, candidate_text,
)
if candidate_nav_penalty < 0.7 {
return false
}
let candidate_rep = calculate_repetition_penalty(candidate_text)
if candidate_rep < 0.35 {
return false
}
let candidate_numeric = calculate_numeric_penalty(candidate_text)
if candidate_numeric < 0.4 {
return false
}
let main_nav_penalty = calculate_navigation_penalty(main, main_text)
let main_link_density = calculate_link_density(main, main_text)
let main_rep = calculate_repetition_penalty(main_text)
if main_nav_penalty < 0.6 || main_link_density > 0.35 || main_rep < 0.4 {
return true
}
false
}
///|
fn is_descendant(root : @aom.AccessibilityNode, target_id : String) -> Bool {
if root.id == target_id {
return true
}
for child in root.children {
if is_descendant(child, target_id) {
return true
}
}
false
}
///|
fn select_readability_node(
best_readability_block : ContentBlock?,
best_readability_candidate : @aom.AccessibilityNode?,
scores : Map[String, Double],
) -> @aom.AccessibilityNode? {
match (best_readability_block, best_readability_candidate) {
(None, None) => None
(Some(block), None) => Some(block.node)
(None, Some(candidate)) => Some(candidate)
(Some(block), Some(candidate)) => {
if block.node.id == candidate.id {
return Some(candidate)
}
let block_score = get_readability_score(scores, block.node)
let cand_score = get_readability_score(scores, candidate)
if block_score > cand_score * 1.05 {
Some(block.node)
} else if cand_score > block_score * 1.05 {
Some(candidate)
} else {
let block_len = get_text_content(block.node).length()
let cand_len = get_text_content(candidate).length()
if cand_len <= block_len {
Some(candidate)
} else {
Some(block.node)
}
}
}
}
}
///|
fn should_prefer_smaller_container(
main_len : Int,
candidate_len : Int,
main_readability : Double,
candidate_readability : Double,
) -> Bool {
if candidate_len == 0 {
return false
}
let ratio = main_len.to_double() / candidate_len.to_double()
if candidate_len < 600 &&
ratio >= 6.0 &&
candidate_readability >= main_readability {
true
} else if candidate_len < 1200 &&
ratio >= 7.0 &&
candidate_readability >= main_readability * 0.9 {
true
} else {
false
}
}