use serde::{Deserialize, Serialize};
use crate::accessibility::AXTree;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PageIntent {
Shop,
LeadGen,
Editorial,
Marketing,
Corporate,
Hub,
Unknown,
}
impl PageIntent {
pub fn weights(&self) -> (f64, f64, f64, f64, f64) {
match self {
PageIntent::Shop => (0.15, 0.15, 0.20, 0.20, 0.30),
PageIntent::LeadGen => (0.15, 0.15, 0.15, 0.25, 0.30),
PageIntent::Editorial => (0.20, 0.20, 0.25, 0.15, 0.20),
PageIntent::Marketing => (0.25, 0.15, 0.15, 0.15, 0.30),
PageIntent::Corporate => (0.20, 0.20, 0.25, 0.20, 0.15),
PageIntent::Hub => (0.15, 0.25, 0.30, 0.15, 0.15),
PageIntent::Unknown => (0.20, 0.20, 0.25, 0.20, 0.15),
}
}
pub fn label(&self) -> &'static str {
match self {
PageIntent::Shop => "Shop / E-Commerce",
PageIntent::LeadGen => "Lead-Generierung",
PageIntent::Editorial => "Redaktionell / Blog",
PageIntent::Marketing => "Marketing / Landing Page",
PageIntent::Corporate => "Unternehmensseite",
PageIntent::Hub => "Hub / Portal",
PageIntent::Unknown => "Nicht erkannt",
}
}
}
const SHOP_KEYWORDS: &[&str] = &[
"cart",
"shop",
"product",
"price",
"order",
"buy",
"add to cart",
"checkout",
"quantity",
"stock",
"in stock",
"warenkorb",
"produkt",
"preis",
"bestellen",
"kaufen",
"in den warenkorb",
"kasse",
"artikel",
"menge",
"vorrat",
"auf lager",
"panier",
"produit",
"prix",
"commander",
"acheter",
"ajouter au panier",
"passer la commande",
"quantité",
"en stock",
"carrito",
"producto",
"precio",
"pedir",
"comprar",
"añadir al carrito",
"pagar",
"cantidad",
"en stock",
"carrello",
"prodotto",
"prezzo",
"ordinare",
"acquistare",
"aggiungi al carrello",
"cassa",
"quantità",
"disponibile",
"carrinho",
"produto",
"preço",
"pedir",
"comprar",
"adicionar ao carrinho",
"pagamento",
"quantidade",
"em estoque",
"winkelwagen",
"product",
"prijs",
"bestellen",
"kopen",
"in winkelwagen",
"afrekenen",
"hoeveelheid",
"op voorraad",
"kundvagn",
"produkt",
"pris",
"beställa",
"köpa",
"lägg i kundvagn",
"kassa",
"antal",
"i lager",
"koszyk",
"produkt",
"cena",
"zamówić",
"kupić",
"dodaj do koszyka",
"kasa",
"ilość",
"sepet",
"ürün",
"fiyat",
"sipariş ver",
"satın al",
"sepete ekle",
"ödeme",
"adet",
"stokta",
];
const LEADGEN_KEYWORDS: &[&str] = &[
"contact form",
"request",
"quote",
"appointment",
"consultation",
"newsletter",
"subscribe",
"sign up",
"demo",
"free trial",
"get in touch",
"kontaktformular",
"anfrage",
"angebot",
"termin",
"beratung",
"registrieren",
"kostenlos testen",
"rückruf",
"formulaire de contact",
"demande",
"devis",
"rendez-vous",
"consultation",
"s'abonner",
"s'inscrire",
"essai gratuit",
"nous contacter",
"formulario de contacto",
"solicitud",
"presupuesto",
"cita",
"asesoramiento",
"suscribirse",
"registrarse",
"prueba gratuita",
"contactar",
"modulo di contatto",
"richiesta",
"preventivo",
"appuntamento",
"consulenza",
"iscriversi",
"registrarsi",
"prova gratuita",
"contattaci",
"formulário de contato",
"solicitação",
"orçamento",
"consulta",
"assinar",
"registrar",
"teste gratuito",
"entrar em contato",
"contactformulier",
"aanvraag",
"offerte",
"afspraak",
"advies",
"abonneren",
"registreren",
"gratis proberen",
"contact opnemen",
"kontaktformulär",
"förfrågan",
"offert",
"möte",
"rådgivning",
"prenumerera",
"registrera",
"gratis provperiod",
"formularz kontaktowy",
"zapytanie",
"wycena",
"spotkanie",
"konsultacja",
"subskrybować",
"rejestracja",
"bezpłatna wersja próbna",
"iletişim formu",
"talep",
"teklif",
"randevu",
"danışmanlık",
"abone ol",
"kayıt ol",
"ücretsiz deneme",
];
const EDITORIAL_KEYWORDS: &[&str] = &[
"article",
"blog",
"post",
"author",
"published",
"reading time",
"comment",
"category",
"tag",
"min read",
"related articles",
"artikel",
"beitrag",
"autor",
"veröffentlicht",
"lesezeit",
"kommentar",
"kategorie",
"ähnliche artikel",
"weiterlesen",
"article",
"billet",
"auteur",
"publié",
"temps de lecture",
"commentaire",
"catégorie",
"articles similaires",
"lire la suite",
"artículo",
"entrada",
"autor",
"publicado",
"tiempo de lectura",
"comentario",
"categoría",
"artículos relacionados",
"leer más",
"articolo",
"post",
"autore",
"pubblicato",
"tempo di lettura",
"commento",
"categoria",
"articoli correlati",
"leggi di più",
"artigo",
"postagem",
"autor",
"publicado",
"tempo de leitura",
"comentário",
"categoria",
"artigos relacionados",
"leia mais",
"artikel",
"bericht",
"auteur",
"gepubliceerd",
"leestijd",
"reactie",
"categorie",
"gerelateerde artikelen",
"lees meer",
"artikel",
"inlägg",
"författare",
"publicerad",
"lästid",
"kommentar",
"kategori",
"relaterade artiklar",
"läs mer",
"artykuł",
"wpis",
"autor",
"opublikowano",
"czas czytania",
"komentarz",
"kategoria",
"powiązane artykuły",
"czytaj więcej",
"makale",
"gönderi",
"yazar",
"yayınlandı",
"okuma süresi",
"yorum",
"kategori",
"ilgili makaleler",
"devamını oku",
];
const CORPORATE_KEYWORDS: &[&str] = &[
"about us",
"team",
"career",
"company",
"mission",
"vision",
"location",
"partner",
"reference",
"history",
"values",
"leadership",
"press",
"investor",
"über uns",
"karriere",
"unternehmen",
"standort",
"referenz",
"geschichte",
"werte",
"führung",
"presse",
"investor",
"à propos",
"équipe",
"carrière",
"entreprise",
"mission",
"vision",
"emplacement",
"partenaire",
"référence",
"histoire",
"valeurs",
"direction",
"presse",
"sobre nosotros",
"equipo",
"carrera",
"empresa",
"misión",
"visión",
"ubicación",
"socio",
"referencia",
"historia",
"valores",
"dirección",
"prensa",
"chi siamo",
"team",
"carriera",
"azienda",
"missione",
"visione",
"sede",
"partner",
"riferimento",
"storia",
"valori",
"leadership",
"stampa",
"sobre nós",
"equipe",
"carreira",
"empresa",
"missão",
"visão",
"localização",
"parceiro",
"referência",
"história",
"valores",
"imprensa",
"over ons",
"team",
"carrière",
"bedrijf",
"missie",
"visie",
"locatie",
"partner",
"referentie",
"geschiedenis",
"waarden",
"leiderschap",
"pers",
"om oss",
"team",
"karriär",
"företag",
"mission",
"vision",
"plats",
"partner",
"referens",
"historia",
"värderingar",
"press",
"o nas",
"zespół",
"kariera",
"firma",
"misja",
"wizja",
"lokalizacja",
"partner",
"referencje",
"historia",
"wartości",
"prasa",
"hakkımızda",
"ekip",
"kariyer",
"şirket",
"misyon",
"vizyon",
"konum",
"ortak",
"referans",
"tarihçe",
"değerler",
"basın",
];
pub fn detect_page_intent(tree: &AXTree) -> PageIntent {
let mut shop_score = 0u32;
let mut leadgen_score = 0u32;
let mut editorial_score = 0u32;
let mut corporate_score = 0u32;
let links = tree.links();
let buttons = tree.nodes_with_role("button");
let headings = tree.headings();
let form_controls = tree.form_controls();
for node in tree.iter() {
let name = match node.name.as_deref() {
Some(n) if !n.is_empty() => n.to_lowercase(),
_ => continue,
};
for kw in SHOP_KEYWORDS {
if name.contains(kw) {
shop_score += 1;
}
}
for kw in LEADGEN_KEYWORDS {
if name.contains(kw) {
leadgen_score += 1;
}
}
for kw in EDITORIAL_KEYWORDS {
if name.contains(kw) {
editorial_score += 1;
}
}
for kw in CORPORATE_KEYWORDS {
if name.contains(kw) {
corporate_score += 1;
}
}
}
if form_controls.len() >= 3 {
leadgen_score += 3;
}
let link_count = links.len();
if link_count > 80 {
return PageIntent::Hub;
}
let text_len: usize = tree
.iter()
.filter(|n| matches!(n.role.as_deref(), Some("StaticText" | "paragraph")))
.filter_map(|n| n.name.as_ref())
.map(|n| n.len())
.sum();
let approx_words = text_len / 6;
if approx_words > 500 && editorial_score >= 2 {
editorial_score += 5;
}
if buttons.len() < 3 && form_controls.is_empty() && headings.len() > 4 {
editorial_score += 3;
}
let scores = [
(shop_score, PageIntent::Shop),
(leadgen_score, PageIntent::LeadGen),
(editorial_score, PageIntent::Editorial),
(corporate_score, PageIntent::Corporate),
];
let (best_score, best_intent) = scores
.iter()
.max_by_key(|(s, _)| *s)
.copied()
.unwrap_or((0, PageIntent::Unknown));
if best_score >= 3 {
best_intent
} else {
if link_count < 30 && !buttons.is_empty() && approx_words < 300 {
PageIntent::Marketing
} else {
PageIntent::Unknown
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accessibility::AXTree;
#[test]
fn test_unknown_for_empty_tree() {
let tree = AXTree::new();
let intent = detect_page_intent(&tree);
assert!(matches!(
intent,
PageIntent::Unknown | PageIntent::Marketing
));
}
#[test]
fn test_weights_sum_to_one() {
for intent in &[
PageIntent::Shop,
PageIntent::LeadGen,
PageIntent::Editorial,
PageIntent::Marketing,
PageIntent::Corporate,
PageIntent::Hub,
PageIntent::Unknown,
] {
let (a, b, c, d, e) = intent.weights();
let sum = a + b + c + d + e;
assert!(
(sum - 1.0).abs() < 0.001,
"{:?} weights sum to {} instead of 1.0",
intent,
sum
);
}
}
}