1use lazy_static::lazy_static;
7use scraper::Selector;
8use std::collections::{HashMap, HashSet};
9use std::sync::RwLock;
10
11use crate::types::{ParserError, ParserResult};
12
13pub struct CachedSelectors {
18 pub title: Selector,
20 pub meta: Selector,
22 pub link: Selector,
24 pub base: Selector,
26 pub html: Selector,
28 pub body: Selector,
30 pub article: Selector,
32 pub main: Selector,
34 pub main_role: Selector,
36 pub headings: Selector,
38 pub p: Selector,
40 pub blockquote: Selector,
42 pub pre: Selector,
44 pub pre_code: Selector,
46 pub code: Selector,
48 pub ul: Selector,
50 pub ol: Selector,
52 pub li: Selector,
54 pub dl: Selector,
56 pub dt: Selector,
58 pub dd: Selector,
60 pub table: Selector,
62 pub thead: Selector,
64 pub tbody: Selector,
66 pub tfoot: Selector,
68 pub tr: Selector,
70 pub th: Selector,
72 pub td: Selector,
74 pub caption: Selector,
76 pub a: Selector,
78 pub img: Selector,
80 pub picture: Selector,
82 pub source: Selector,
84 pub figure: Selector,
86 pub figcaption: Selector,
88 pub script: Selector,
90 pub style: Selector,
92 pub noscript: Selector,
94 pub nav: Selector,
96 pub header: Selector,
98 pub footer: Selector,
100 pub aside: Selector,
102 pub json_ld: Selector,
104 pub microdata: Selector,
106}
107
108lazy_static! {
109 pub static ref SELECTORS: CachedSelectors = CachedSelectors {
113 title: sel("title"),
114 meta: sel("meta"),
115 link: sel("link"),
116 base: sel("base"),
117 html: sel("html"),
118 body: sel("body"),
119 article: sel("article"),
120 main: sel("main"),
121 main_role: sel("[role=main]"),
122 headings: sel("h1, h2, h3, h4, h5, h6"),
123 p: sel("p"),
124 blockquote: sel("blockquote"),
125 pre: sel("pre"),
126 pre_code: sel("pre code"),
127 code: sel("code"),
128 ul: sel("ul"),
129 ol: sel("ol"),
130 li: sel("li"),
131 dl: sel("dl"),
132 dt: sel("dt"),
133 dd: sel("dd"),
134 table: sel("table"),
135 thead: sel("thead"),
136 tbody: sel("tbody"),
137 tfoot: sel("tfoot"),
138 tr: sel("tr"),
139 th: sel("th"),
140 td: sel("td"),
141 caption: sel("caption"),
142 a: sel("a"),
143 img: sel("img"),
144 picture: sel("picture"),
145 source: sel("source"),
146 figure: sel("figure"),
147 figcaption: sel("figcaption"),
148 script: sel("script"),
149 style: sel("style"),
150 noscript: sel("noscript"),
151 nav: sel("nav"),
152 header: sel("header"),
153 footer: sel("footer"),
154 aside: sel("aside"),
155 json_ld: sel(r#"script[type="application/ld+json"]"#),
156 microdata: sel("[itemscope][itemtype]"),
157 };
158
159 static ref SELECTOR_CACHE: RwLock<HashMap<String, Selector>> = RwLock::new(HashMap::new());
164}
165
166fn sel(css: &str) -> Selector {
170 Selector::parse(css).unwrap_or_else(|e| {
171 panic!("无效的 CSS 选择器 '{css}': {e}")
172 })
173}
174
175pub fn get_or_create_selector(css: &str) -> ParserResult<Selector> {
180 {
181 let cache = SELECTOR_CACHE.read().map_err(|e| {
182 ParserError::ConfigError(format!("选择器缓存读取锁失效: {e}"))
183 })?;
184 if let Some(sel) = cache.get(css) {
185 return Ok(sel.clone());
186 }
187 }
188
189 let selector = Selector::parse(css).map_err(|e| {
190 ParserError::SelectorError(format!("无法解析选择器 '{css}': {e}"))
191 })?;
192
193 let mut cache = SELECTOR_CACHE.write().map_err(|e| {
194 ParserError::ConfigError(format!("选择器缓存写入锁失效: {e}"))
195 })?;
196 cache.insert(css.to_string(), selector.clone());
197 Ok(selector)
198}
199
200pub fn parse_selector(css: &str) -> Selector {
205 Selector::parse(css).unwrap_or_else(|e| {
206 panic!("无效的 CSS 选择器 '{css}': {e}")
207 })
208}
209
210pub fn try_parse_selector(css: &str) -> Option<Selector> {
214 Selector::parse(css).ok()
215}
216
217pub fn heading_selector() -> &'static str {
221 "h1, h2, h3, h4, h5, h6"
222}
223
224pub const CONTENT_SELECTORS: &[&str] = &[
230 "article",
231 "main",
232 "[role=main]",
233 ".content",
234 ".post-content",
235 ".entry-content",
236 ".article-content",
237 ".post-body",
238 ".article-body",
239 "#content",
240 "#main-content",
241 ".main-content",
242];
243
244lazy_static! {
245 pub static ref BOILERPLATE_SELECTORS: HashSet<&'static str> = {
250 let mut set = HashSet::new();
251 set.insert("script");
252 set.insert("style");
253 set.insert("noscript");
254 set.insert("nav");
255 set.insert("header");
256 set.insert("footer");
257 set.insert("aside");
258 set.insert(".sidebar");
259 set.insert(".advertisement");
260 set.insert(".ad");
261 set.insert(".ads");
262 set.insert(".menu");
263 set.insert(".navigation");
264 set.insert(".nav");
265 set.insert(".footer");
266 set.insert(".header");
267 set.insert("[role=navigation]");
268 set.insert("[role=banner]");
269 set.insert("[role=contentinfo]");
270 set
271 };
272}
273
274pub const INLINE_ELEMENTS: &[&str] = &[
278 "a", "abbr", "acronym", "b", "bdo", "big", "br", "button", "cite",
279 "code", "dfn", "em", "i", "img", "input", "kbd", "label", "map",
280 "object", "q", "samp", "script", "select", "small", "span", "strong",
281 "sub", "sup", "textarea", "time", "tt", "var",
282];
283
284pub const BLOCK_ELEMENTS: &[&str] = &[
288 "address", "article", "aside", "blockquote", "canvas", "dd", "div",
289 "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form",
290 "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
291 "main", "nav", "noscript", "ol", "output", "p", "pre", "section",
292 "table", "tfoot", "ul", "video",
293];
294
295pub fn attr_selector(attr: &str) -> String {
301 format!("[{attr}]")
302}
303
304pub fn attr_contains_selector(attr: &str, value: &str) -> String {
308 format!(r#"[{attr}*="{value}"]"#)
309}
310
311pub fn attr_starts_with_selector(attr: &str, value: &str) -> String {
315 format!(r#"[{attr}^="{value}"]"#)
316}
317
318pub fn class_selector(class: &str) -> String {
322 format!(".{class}")
323}
324
325pub fn id_selector(id: &str) -> String {
327 format!("#{id}")
328}
329
330pub fn descendant_selector(ancestor: &str, descendant: &str) -> String {
334 format!("{ancestor} {descendant}")
335}
336
337pub fn child_selector(parent: &str, child: &str) -> String {
341 format!("{parent} > {child}")
342}
343
344pub fn multi_selector(selectors: &[&str]) -> String {
348 selectors.join(", ")
349}
350
351pub fn meta_name_selector(name: &str) -> String {
357 format!(r#"meta[name="{name}"]"#)
358}
359
360pub fn meta_property_selector(property: &str) -> String {
364 format!(r#"meta[property="{property}"]"#)
365}
366
367pub fn link_rel_selector(rel: &str) -> String {
371 format!(r#"link[rel="{rel}"]"#)
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use scraper::Html;
378
379 fn count_matches(html: &str, selector: &str) -> usize {
381 let doc = Html::parse_fragment(html);
382 let sel = Selector::parse(selector).unwrap();
383 doc.select(&sel).count()
384 }
385
386 fn count_matches_doc(html: &str, selector: &Selector) -> usize {
388 let doc = Html::parse_document(html);
389 doc.select(selector).count()
390 }
391
392 #[test]
395 fn test_precompiled_selectors_match_expected_elements() {
396 let html = r##"
397 <html><head><title>测试</title></head><body>
398 <h1>标题1</h1><h2>标题2</h2><h6>标题6</h6>
399 <p>段落</p>
400 <a href="#">链接</a>
401 <img src="test.jpg" />
402 <ul><li>列表项</li></ul>
403 <table><thead><tr><th>表头</th></tr></thead>
404 <tbody><tr><td>单元格</td></tr></tbody>
405 <tfoot><tr><td>页脚</td></tr></tfoot>
406 <caption>表格标题</caption></table>
407 <article>文章</article>
408 <main>主体</main>
409 <div role="main">角色主体</div>
410 <nav>导航</nav>
411 <footer>页脚</footer>
412 <header>页眉</header>
413 <aside>侧边栏</aside>
414 <blockquote>引用</blockquote>
415 <pre><code>代码</code></pre>
416 <code>内联代码</code>
417 <dl><dt>术语</dt><dd>定义</dd></dl>
418 <ol><li>有序项</li></ol>
419 <picture><source srcset="img.webp" /><img src="img.jpg" /></picture>
420 <figure><figcaption>图注</figcaption></figure>
421 <script>js</script>
422 <style>css</style>
423 <noscript>备选</noscript>
424 <script type="application/ld+json">{}</script>
425 <div itemscope itemtype="https://example.org/Article">微数据</div>
426 </body></html>
427 "##;
428
429 assert_eq!(count_matches_doc(html, &SELECTORS.title), 1);
430 assert_eq!(count_matches_doc(html, &SELECTORS.meta), 0);
431 assert_eq!(count_matches_doc(html, &SELECTORS.html), 1);
432 assert_eq!(count_matches_doc(html, &SELECTORS.body), 1);
433 assert_eq!(count_matches_doc(html, &SELECTORS.headings), 3);
434 assert_eq!(count_matches_doc(html, &SELECTORS.p), 1);
435 assert_eq!(count_matches_doc(html, &SELECTORS.a), 1);
436 assert_eq!(count_matches_doc(html, &SELECTORS.img), 2);
437 assert_eq!(count_matches_doc(html, &SELECTORS.ul), 1);
438 assert_eq!(count_matches_doc(html, &SELECTORS.ol), 1);
439 assert_eq!(count_matches_doc(html, &SELECTORS.li), 2);
440 assert_eq!(count_matches_doc(html, &SELECTORS.table), 1);
441 assert_eq!(count_matches_doc(html, &SELECTORS.thead), 1);
442 assert_eq!(count_matches_doc(html, &SELECTORS.tbody), 1);
443 assert_eq!(count_matches_doc(html, &SELECTORS.tfoot), 1);
444 assert_eq!(count_matches_doc(html, &SELECTORS.tr), 3);
445 assert_eq!(count_matches_doc(html, &SELECTORS.th), 1);
446 assert_eq!(count_matches_doc(html, &SELECTORS.td), 2);
447 assert_eq!(count_matches_doc(html, &SELECTORS.caption), 1);
448 assert_eq!(count_matches_doc(html, &SELECTORS.article), 1);
449 assert_eq!(count_matches_doc(html, &SELECTORS.main), 1);
450 assert_eq!(count_matches_doc(html, &SELECTORS.main_role), 1);
451 assert_eq!(count_matches_doc(html, &SELECTORS.nav), 1);
452 assert_eq!(count_matches_doc(html, &SELECTORS.footer), 1);
453 assert_eq!(count_matches_doc(html, &SELECTORS.header), 1);
454 assert_eq!(count_matches_doc(html, &SELECTORS.aside), 1);
455 assert_eq!(count_matches_doc(html, &SELECTORS.blockquote), 1);
456 assert_eq!(count_matches_doc(html, &SELECTORS.pre), 1);
457 assert_eq!(count_matches_doc(html, &SELECTORS.pre_code), 1);
458 assert_eq!(count_matches_doc(html, &SELECTORS.code), 2);
459 assert_eq!(count_matches_doc(html, &SELECTORS.dl), 1);
460 assert_eq!(count_matches_doc(html, &SELECTORS.dt), 1);
461 assert_eq!(count_matches_doc(html, &SELECTORS.dd), 1);
462 assert_eq!(count_matches_doc(html, &SELECTORS.picture), 1);
463 assert_eq!(count_matches_doc(html, &SELECTORS.source), 1);
464 assert_eq!(count_matches_doc(html, &SELECTORS.figure), 1);
465 assert_eq!(count_matches_doc(html, &SELECTORS.figcaption), 1);
466 assert_eq!(count_matches_doc(html, &SELECTORS.script), 2);
467 assert_eq!(count_matches_doc(html, &SELECTORS.style), 1);
468 assert_eq!(count_matches_doc(html, &SELECTORS.noscript), 1);
469 assert_eq!(count_matches_doc(html, &SELECTORS.json_ld), 1);
470 assert_eq!(count_matches_doc(html, &SELECTORS.microdata), 1);
471 }
472
473 #[test]
474 fn test_precompiled_link_base_selectors() {
475 let html = r#"
476 <html><head>
477 <base href="https://example.com/" />
478 <link rel="stylesheet" href="style.css" />
479 </head></html>
480 "#;
481 assert_eq!(count_matches_doc(html, &SELECTORS.link), 1);
482 assert_eq!(count_matches_doc(html, &SELECTORS.base), 1);
483 }
484
485 #[test]
488 fn test_get_or_create_selector_creates_and_caches() {
489 let sel1 = get_or_create_selector("div.content").unwrap();
490 let sel2 = get_or_create_selector("div.content").unwrap();
491
492 let html = r#"<div class="content">内容</div>"#;
494 let doc = Html::parse_fragment(html);
495 assert_eq!(doc.select(&sel1).count(), doc.select(&sel2).count());
496 assert_eq!(doc.select(&sel1).count(), 1);
497 }
498
499 #[test]
500 fn test_get_or_create_selector_returns_error_for_invalid() {
501 let result = get_or_create_selector("!!!invalid");
502 assert!(result.is_err());
503 match result {
504 Err(ParserError::SelectorError(_)) => {}
505 _ => panic!("期望 SelectorError 变体"),
506 }
507 }
508
509 #[test]
510 fn test_try_parse_selector_returns_none_for_invalid() {
511 let result = try_parse_selector("!!!invalid");
512 assert!(result.is_none());
513 }
514
515 #[test]
516 fn test_try_parse_selector_returns_some_for_valid() {
517 let result = try_parse_selector("div");
518 assert!(result.is_some());
519 }
520
521 #[test]
524 fn test_heading_selector_format() {
525 assert_eq!(heading_selector(), "h1, h2, h3, h4, h5, h6");
526 }
527
528 #[test]
529 fn test_heading_selector_matches_all_levels() {
530 let html = r"
531 <h1>一级</h1>
532 <h2>二级</h2>
533 <h3>三级</h3>
534 <h4>四级</h4>
535 <h5>五级</h5>
536 <h6>六级</h6>
537 ";
538 assert_eq!(count_matches(html, heading_selector()), 6);
539 }
540
541 #[test]
544 fn test_content_selectors_contains_core_selectors() {
545 assert!(CONTENT_SELECTORS.contains(&"article"));
546 assert!(CONTENT_SELECTORS.contains(&"main"));
547 assert!(CONTENT_SELECTORS.contains(&"[role=main]"));
548 assert!(!CONTENT_SELECTORS.is_empty());
549 }
550
551 #[test]
552 fn test_boilerplate_selectors_contains_core_selectors() {
553 assert!(BOILERPLATE_SELECTORS.contains(&"nav"));
554 assert!(BOILERPLATE_SELECTORS.contains(&"footer"));
555 assert!(BOILERPLATE_SELECTORS.contains(&"aside"));
556 assert!(!BOILERPLATE_SELECTORS.is_empty());
557 }
558
559 #[test]
560 fn test_inline_and_block_elements_are_disjoint() {
561 for elem in INLINE_ELEMENTS {
562 assert!(
563 !BLOCK_ELEMENTS.contains(elem),
564 "元素 {elem} 同时出现在 INLINE 和 BLOCK 中"
565 );
566 }
567 assert!(INLINE_ELEMENTS.contains(&"span"));
568 assert!(BLOCK_ELEMENTS.contains(&"div"));
569 }
570
571 #[test]
574 fn test_attr_selector_builders() {
575 assert_eq!(attr_selector("href"), "[href]");
576 assert_eq!(attr_contains_selector("class", "active"), r#"[class*="active"]"#);
577 assert_eq!(attr_starts_with_selector("href", "/"), r#"[href^="/"]"#);
578 }
579
580 #[test]
581 fn test_class_and_id_selector_builders() {
582 assert_eq!(class_selector("container"), ".container");
583 assert_eq!(id_selector("main"), "#main");
584 }
585
586 #[test]
587 fn test_combinator_selector_builders() {
588 assert_eq!(descendant_selector("div", "p"), "div p");
589 assert_eq!(child_selector("ul", "li"), "ul > li");
590 }
591
592 #[test]
593 fn test_multi_selector_builder() {
594 assert_eq!(multi_selector(&["h1", "h2", "h3"]), "h1, h2, h3");
595 assert_eq!(multi_selector(&[]), "");
596 }
597
598 #[test]
601 fn test_meta_name_selector_matches_meta_name() {
602 let sel = meta_name_selector("description");
603 assert_eq!(sel, r#"meta[name="description"]"#);
604 assert_eq!(count_matches(r#"<meta name="description" content="d">"#, &sel), 1);
605 assert_eq!(count_matches(r#"<meta name="keywords" content="k">"#, &sel), 0);
606 }
607
608 #[test]
609 fn test_meta_property_selector_matches_meta_property() {
610 let sel = meta_property_selector("og:title");
611 assert_eq!(sel, r#"meta[property="og:title"]"#);
612 assert_eq!(count_matches(r#"<meta property="og:title" content="标题">"#, &sel), 1);
613 }
614
615 #[test]
616 fn test_link_rel_selector_matches_link_rel() {
617 let sel = link_rel_selector("canonical");
618 assert_eq!(sel, r#"link[rel="canonical"]"#);
619 assert_eq!(count_matches(r#"<link rel="canonical" href="https://ex.com">"#, &sel), 1);
620 assert_eq!(count_matches(r#"<link rel="stylesheet" href="s.css">"#, &sel), 0);
621 }
622
623 #[test]
626 fn test_builder_functions_compose_correctly() {
627 let composed = descendant_selector("article", &class_selector("post-content"));
628 assert_eq!(composed, "article .post-content");
629
630 let html = r#"<article><div class="post-content"><p>正文</p></div></article>"#;
631 assert_eq!(count_matches(html, &composed), 1);
632 }
633
634 #[test]
635 fn test_cached_selector_matches_raw_selector() {
636 let html = r#"<div id="unique-id">唯一元素</div>"#;
637
638 let raw = Selector::parse("div#unique-id").unwrap();
639 let cached = get_or_create_selector("div#unique-id").unwrap();
640
641 let doc = Html::parse_fragment(html);
642 assert_eq!(doc.select(&raw).count(), doc.select(&cached).count());
643 }
644}