pagesift_frontier/focus/
url_shape.rs1use url::Url;
5
6#[derive(PartialEq, Eq, Debug)]
7pub(super) enum Section {
8 SameHost,
10 Subsite,
12}
13
14pub(super) fn section(url: &Url, anchor: Option<&str>) -> Option<Section> {
17 let mut segs: Vec<&str> = url.path().split('/').filter(|s| !s.is_empty()).collect();
18 if segs
19 .first()
20 .is_some_and(|s| is_language_code(s) || is_language_name(s))
21 {
22 segs.remove(0);
23 }
24 let segments = segs.len();
25 let short = anchor.is_some_and(|a| (1..=4).contains(&a.split_whitespace().count()));
26 if !short || url.query().is_some() {
27 return None;
28 }
29 match segments {
30 0 => Some(Section::Subsite),
31 1 => Some(Section::SameHost),
32 _ => None,
33 }
34}
35
36pub(super) fn foreign_language_segment(url: &Url) -> bool {
38 const CODES: &[&str] = &[
39 "de", "fr", "es", "it", "nl", "pt", "ru", "ja", "jp", "zh", "zh-cn", "zh-tw", "zh-hans",
40 "zh-hant", "ko", "kr", "ar", "sv", "fi", "da", "no", "nb", "pl", "tr", "cs", "el", "he",
41 "hi", "th", "vi", "id", "hu", "ro", "uk", "ca", "eu", "gl", "cy", "ga", "mi",
42 ];
43 url.path()
44 .split('/')
45 .filter(|s| !s.is_empty())
46 .any(|seg| CODES.contains(&language_code(seg).as_str()))
47}
48
49pub(super) fn language_portal(url: &Url, anchor: Option<&str>) -> bool {
51 let anchor = anchor.unwrap_or_default().trim().to_ascii_lowercase();
52 if matches!(
53 anchor.as_str(),
54 "english"
55 | "en"
56 | "eng"
57 | "in english"
58 | "english site"
59 | "english version"
60 | "english page"
61 | "english top"
62 | "english home"
63 ) {
64 return true;
65 }
66 url.path()
68 .split('/')
69 .rfind(|seg| !seg.is_empty())
70 .is_some_and(|seg| {
71 matches!(
72 language_code(seg).as_str(),
73 "en" | "english" | "en-us" | "en-gb"
74 )
75 })
76}
77
78pub(super) fn is_language_name(seg: &str) -> bool {
80 matches!(language_code(seg).as_str(), "english" | "eng")
81}
82
83pub(super) fn is_english(lang: &str) -> bool {
85 let primary = lang.split(['-', '_']).next().unwrap_or_default();
86 primary.eq_ignore_ascii_case("en")
87}
88
89pub(super) fn is_language_code(seg: &str) -> bool {
91 let code = language_code(seg);
92 let two = |p: &str| p.len() == 2 && p.bytes().all(|b| b.is_ascii_alphabetic());
93 two(&code)
94 || code
95 .split_once('-')
96 .is_some_and(|(a, b)| two(a) && (2..=4).contains(&b.len()))
97}
98
99fn language_code(seg: &str) -> String {
101 seg.split_once('.')
102 .map_or(seg, |(s, _)| s)
103 .to_ascii_lowercase()
104}
105
106pub(super) fn locator(url: &Url) -> String {
109 let mut s = String::with_capacity(url.as_str().len());
110 if let Some(host) = url.host_str() {
111 for label in host.split('.') {
112 if label != "www" {
113 s.push_str(label);
114 s.push(' ');
115 }
116 }
117 }
118 s.push_str(url.path());
119 if let Some(q) = url.query() {
120 s.push(' ');
121 s.push_str(q);
122 }
123 s
124}
125
126pub fn shape(url: &Url) -> String {
130 let mut out = String::new();
131 for seg in url.path().split('/').filter(|s| !s.is_empty()) {
132 out.push('/');
133 if seg.bytes().all(|b| b.is_ascii_digit()) {
134 out.push_str("{n}");
135 } else if seg.bytes().any(|b| b.is_ascii_digit()) {
136 out.push_str("{id}");
137 } else {
138 out.push_str(seg);
139 }
140 }
141 let mut keys: Vec<String> = url.query_pairs().map(|(k, _)| k.into_owned()).collect();
142 keys.sort();
143 keys.dedup();
144 if !keys.is_empty() {
145 out.push('?');
146 out.push_str(&keys.join("&"));
147 }
148 if out.is_empty() {
149 out.push('/');
150 }
151 out
152}