1use scraper::Html;
8use std::time::Instant;
9use url::Url;
10
11use crate::content::{
12 extract_code_blocks, extract_headings, extract_images, extract_lists, extract_paragraphs,
13 extract_quotes, extract_tables,
14};
15use crate::links::extract_links;
16use crate::metadata::{extract_metadata, extract_structured_data};
17use crate::text::extract_text;
18use crate::types::{
19 CodeBlock, Heading, Image, Link, ListContent, PageMetadata, ParseStats, ParsedContent,
20 ParserConfig, ParserResult, Quote, StructuredData, TableContent, TextContent,
21};
22
23#[derive(Debug, Clone)]
39pub struct HtmlParser {
40 config: ParserConfig,
41}
42
43impl HtmlParser {
44 pub fn new() -> Self {
48 Self {
49 config: ParserConfig::default(),
50 }
51 }
52
53 pub fn with_config(config: ParserConfig) -> Self {
55 Self { config }
56 }
57
58 pub fn with_base_url(url: Url) -> Self {
60 Self {
61 config: ParserConfig {
62 base_url: Some(url),
63 ..ParserConfig::default()
64 },
65 }
66 }
67
68 pub fn set_base_url(&mut self, url: Url) {
70 self.config.base_url = Some(url);
71 }
72
73 pub fn config(&self) -> &ParserConfig {
75 &self.config
76 }
77
78 pub fn config_mut(&mut self) -> &mut ParserConfig {
80 &mut self.config
81 }
82
83 pub fn parse(&self, html: &str) -> ParserResult<ParsedContent> {
89 let start = Instant::now();
90 let document = Html::parse_document(html);
91 let parse_time_us = start.elapsed().as_micros() as u64;
92
93 let html_size = html.len();
94 let node_count = count_nodes(&document);
95
96 let metadata = extract_metadata(&document, self.config.base_url.as_ref())?;
97 let text = extract_text(&document, &self.config)?;
98 let headings = extract_headings(&document);
99 let paragraphs = extract_paragraphs(&document, self.config.min_paragraph_length);
100
101 let links = if self.config.extract_links {
102 extract_links(&document, &self.config)?
103 } else {
104 Vec::new()
105 };
106
107 let images = if self.config.extract_images {
108 extract_images(&document, self.config.base_url.as_ref())
109 } else {
110 Vec::new()
111 };
112
113 let lists = extract_lists(&document, &self.config);
114
115 let tables = if self.config.extract_tables {
116 extract_tables(&document, &self.config)
117 } else {
118 Vec::new()
119 };
120
121 let code_blocks = if self.config.extract_code_blocks {
122 extract_code_blocks(&document, &self.config)
123 } else {
124 Vec::new()
125 };
126
127 let quotes = extract_quotes(&document, &self.config);
128
129 let structured_data = if self.config.extract_structured_data {
130 extract_structured_data(&document)
131 } else {
132 Vec::new()
133 };
134
135 let stats = ParseStats {
136 html_size,
137 parse_time_us,
138 node_count,
139 element_count: count_elements(&document),
140 text_node_count: count_text_nodes(&document),
141 comment_count: count_comments(&document),
142 errors: Vec::new(),
143 warnings: Vec::new(),
144 };
145
146 Ok(ParsedContent {
147 metadata,
148 text,
149 headings,
150 paragraphs,
151 links,
152 images,
153 lists,
154 tables,
155 code_blocks,
156 quotes,
157 structured_data,
158 stats,
159 })
160 }
161
162 pub fn parse_fragment(&self, html: &str) -> ParserResult<ParsedContent> {
166 let start = Instant::now();
167 let document = Html::parse_fragment(html);
168 let parse_time_us = start.elapsed().as_micros() as u64;
169
170 let html_size = html.len();
171 let metadata = PageMetadata::default();
172 let text = extract_text(&document, &self.config)?;
173 let headings = extract_headings(&document);
174 let paragraphs = extract_paragraphs(&document, self.config.min_paragraph_length);
175
176 let links = if self.config.extract_links {
177 extract_links(&document, &self.config)?
178 } else {
179 Vec::new()
180 };
181
182 let images = if self.config.extract_images {
183 extract_images(&document, self.config.base_url.as_ref())
184 } else {
185 Vec::new()
186 };
187
188 let lists = extract_lists(&document, &self.config);
189
190 let tables = if self.config.extract_tables {
191 extract_tables(&document, &self.config)
192 } else {
193 Vec::new()
194 };
195
196 let code_blocks = if self.config.extract_code_blocks {
197 extract_code_blocks(&document, &self.config)
198 } else {
199 Vec::new()
200 };
201
202 let quotes = extract_quotes(&document, &self.config);
203
204 let stats = ParseStats {
205 html_size,
206 parse_time_us,
207 node_count: count_nodes(&document),
208 element_count: count_elements(&document),
209 text_node_count: count_text_nodes(&document),
210 comment_count: count_comments(&document),
211 errors: Vec::new(),
212 warnings: Vec::new(),
213 };
214
215 Ok(ParsedContent {
216 metadata,
217 text,
218 headings,
219 paragraphs,
220 links,
221 images,
222 lists,
223 tables,
224 code_blocks,
225 quotes,
226 structured_data: Vec::new(),
227 stats,
228 })
229 }
230
231 pub fn extract_metadata(&self, html: &str) -> ParserResult<PageMetadata> {
235 let document = Html::parse_document(html);
236 extract_metadata(&document, self.config.base_url.as_ref())
237 }
238
239 pub fn extract_text(&self, html: &str) -> ParserResult<TextContent> {
241 let document = Html::parse_document(html);
242 extract_text(&document, &self.config)
243 }
244
245 pub fn extract_headings(&self, html: &str) -> Vec<Heading> {
247 let document = Html::parse_document(html);
248 extract_headings(&document)
249 }
250
251 pub fn extract_links(&self, html: &str) -> ParserResult<Vec<Link>> {
253 let document = Html::parse_document(html);
254 extract_links(&document, &self.config)
255 }
256
257 pub fn extract_images(&self, html: &str) -> Vec<Image> {
259 let document = Html::parse_document(html);
260 extract_images(&document, self.config.base_url.as_ref())
261 }
262
263 pub fn extract_lists(&self, html: &str) -> Vec<ListContent> {
265 let document = Html::parse_document(html);
266 extract_lists(&document, &self.config)
267 }
268
269 pub fn extract_tables(&self, html: &str) -> Vec<TableContent> {
271 let document = Html::parse_document(html);
272 extract_tables(&document, &self.config)
273 }
274
275 pub fn extract_code_blocks(&self, html: &str) -> Vec<CodeBlock> {
277 let document = Html::parse_document(html);
278 extract_code_blocks(&document, &self.config)
279 }
280
281 pub fn extract_quotes(&self, html: &str) -> Vec<Quote> {
283 let document = Html::parse_document(html);
284 extract_quotes(&document, &self.config)
285 }
286
287 pub fn extract_structured_data(&self, html: &str) -> Vec<StructuredData> {
289 let document = Html::parse_document(html);
290 extract_structured_data(&document)
291 }
292
293 pub fn resolve_url(&self, href: &str) -> Option<String> {
299 let base = self.config.base_url.as_ref()?;
300 resolve_relative_url(base, href)
301 }
302
303 pub fn has_base_url(&self) -> bool {
305 self.config.base_url.is_some()
306 }
307
308 pub fn base_url(&self) -> Option<&Url> {
310 self.config.base_url.as_ref()
311 }
312}
313
314impl Default for HtmlParser {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320pub fn parse(html: &str) -> ParserResult<ParsedContent> {
326 HtmlParser::new().parse(html)
327}
328
329pub fn parse_with_url(html: &str, base_url: Url) -> ParserResult<ParsedContent> {
331 let parser = HtmlParser::with_base_url(base_url);
332 parser.parse(html)
333}
334
335pub fn get_metadata(html: &str) -> ParserResult<PageMetadata> {
337 let parser = HtmlParser::new();
338 parser.extract_metadata(html)
339}
340
341pub fn get_text(html: &str) -> ParserResult<TextContent> {
343 let parser = HtmlParser::new();
344 parser.extract_text(html)
345}
346
347pub fn get_links(html: &str) -> ParserResult<Vec<Link>> {
349 let parser = HtmlParser::new();
350 parser.extract_links(html)
351}
352
353fn resolve_relative_url(base: &Url, href: &str) -> Option<String> {
359 let href = href.trim();
360 if href.is_empty() {
361 return None;
362 }
363 if href.starts_with("http://") || href.starts_with("https://") {
364 return Some(href.to_string());
365 }
366 if href.starts_with("//") {
367 let scheme = base.scheme();
368 return Some(format!("{scheme}:{href}"));
369 }
370 base.join(href).ok().map(|u| u.to_string())
371}
372
373fn count_nodes(document: &Html) -> usize {
375 document.root_element().children().count()
376}
377
378fn count_elements(document: &Html) -> usize {
380 count_elements_recursive(&document.root_element())
381}
382
383fn count_elements_recursive(element: &scraper::ElementRef) -> usize {
384 let mut count = 1;
385 for child in element.children() {
386 if let Some(child_ref) = scraper::ElementRef::wrap(child) {
387 count += count_elements_recursive(&child_ref);
388 }
389 }
390 count
391}
392
393fn count_text_nodes(document: &Html) -> usize {
395 count_text_nodes_recursive(&document.root_element())
396}
397
398fn count_text_nodes_recursive(element: &scraper::ElementRef) -> usize {
399 let mut count = 0;
400 for child in element.children() {
401 match child.value() {
402 scraper::Node::Text(_) => count += 1,
403 _ => {}
404 }
405 if let Some(child_ref) = scraper::ElementRef::wrap(child) {
406 count += count_text_nodes_recursive(&child_ref);
407 }
408 }
409 count
410}
411
412fn count_comments(document: &Html) -> usize {
414 count_comments_recursive(&document.root_element())
415}
416
417fn count_comments_recursive(element: &scraper::ElementRef) -> usize {
418 let mut count = 0;
419 for child in element.children() {
420 if matches!(child.value(), scraper::Node::Comment(_)) {
421 count += 1;
422 }
423 if let Some(child_ref) = scraper::ElementRef::wrap(child) {
424 count += count_comments_recursive(&child_ref);
425 }
426 }
427 count
428}
429
430#[cfg(test)]
435mod tests {
436 use super::*;
437
438 const SAMPLE_HTML: &str = r##"
440<!DOCTYPE html>
441<html lang="zh-CN">
442<head>
443 <meta charset="utf-8">
444 <title>示例页面 — 测试用</title>
445 <meta name="description" content="这是一个用于解析测试的示例页面">
446 <meta name="keywords" content="测试, html, 解析">
447 <meta name="author" content="测试作者">
448 <meta property="og:title" content="OG 示例标题">
449 <meta property="og:type" content="article">
450 <meta name="twitter:card" content="summary">
451 <link rel="canonical" href="https://example.com/page">
452 <link rel="icon" href="/favicon.ico">
453 <base href="https://example.com/blog/">
454</head>
455<body>
456 <h1 id="main-title">主标题</h1>
457 <h2 class="section">第一章</h2>
458 <p>这是第一段文字。包含足够长的内容来满足最小段落长度要求。</p>
459 <p>这是第二段文字,同样有足够的内容来通过过滤器。</p>
460
461 <ul>
462 <li>无序列表项一</li>
463 <li>无序列表项二</li>
464 <li>无序列表项三</li>
465 </ul>
466
467 <ol>
468 <li>有序第一</li>
469 <li>有序第二</li>
470 </ol>
471
472 <table>
473 <thead>
474 <tr><th>姓名</th><th>年龄</th></tr>
475 </thead>
476 <tbody>
477 <tr><td>张三</td><td>28</td></tr>
478 <tr><td>李四</td><td>35</td></tr>
479 </tbody>
480 </table>
481
482 <pre><code class="language-rust">fn hello() { println!("你好"); }</code></pre>
483
484 <blockquote cite="https://example.com/quote">
485 这是一段引用文字。
486 <cite>作者名</cite>
487 </blockquote>
488
489 <img src="/images/photo.jpg" alt="示例图片" width="800" height="600">
490
491 <a href="https://example.com/other">外部链接</a>
492 <a href="/relative-page">相对链接</a>
493 <a href="mailto:test@example.com">邮箱链接</a>
494
495 <script type="application/ld+json">
496 {"@type": "Article", "name": "示例文章", "description": "测试"}
497 </script>
498</body>
499</html>
500"##;
501
502 #[test]
505 fn 默认构造() {
506 let parser = HtmlParser::new();
507 assert!(parser.config().base_url.is_none());
508 }
509
510 #[test]
511 fn 带配置构造() {
512 let config = ParserConfig::minimal();
513 let parser = HtmlParser::with_config(config);
514 assert!(!parser.config().extract_images);
515 }
516
517 #[test]
518 fn 带基础URL构造() {
519 let url = Url::parse("https://example.com").unwrap();
520 let parser = HtmlParser::with_base_url(url);
521 assert!(parser.has_base_url());
522 }
523
524 #[test]
525 fn 设置基础URL() {
526 let mut parser = HtmlParser::new();
527 assert!(!parser.has_base_url());
528 parser.set_base_url(Url::parse("https://example.com").unwrap());
529 assert!(parser.has_base_url());
530 }
531
532 #[test]
533 fn 获取配置引用() {
534 let parser = HtmlParser::new();
535 let config = parser.config();
536 assert!(config.extract_images);
537 }
538
539 #[test]
540 fn 获取配置可变引用() {
541 let mut parser = HtmlParser::new();
542 parser.config_mut().extract_images = false;
543 assert!(!parser.config().extract_images);
544 }
545
546 #[test]
547 fn 默认trait实现() {
548 let parser: HtmlParser = Default::default();
549 assert!(parser.config().extract_images);
550 }
551
552 #[test]
555 fn 解析完整文档() {
556 let parser = HtmlParser::new();
557 let result = parser.parse(SAMPLE_HTML).unwrap();
558 assert!(result.metadata.title.is_some());
559 assert!(result.text.word_count > 0);
560 assert!(!result.headings.is_empty());
561 assert!(!result.paragraphs.is_empty());
562 assert!(!result.links.is_empty());
563 assert!(!result.images.is_empty());
564 assert!(!result.lists.is_empty());
565 assert!(!result.tables.is_empty());
566 assert!(!result.code_blocks.is_empty());
567 assert!(!result.quotes.is_empty());
568 assert!(!result.structured_data.is_empty());
569 }
570
571 #[test]
572 fn 解析文档统计信息() {
573 let parser = HtmlParser::new();
574 let result = parser.parse(SAMPLE_HTML).unwrap();
575 assert!(result.stats.html_size > 0);
576 assert!(result.stats.parse_time_us > 0);
577 assert!(result.stats.node_count > 0);
578 }
579
580 #[test]
581 fn 解析空文档() {
582 let parser = HtmlParser::new();
583 let result = parser.parse("").unwrap();
584 assert_eq!(result.text.word_count, 0);
585 assert!(result.headings.is_empty());
586 }
587
588 #[test]
589 fn 解析最小化文档() {
590 let parser = HtmlParser::with_config(ParserConfig::minimal());
591 let result = parser.parse(SAMPLE_HTML).unwrap();
592 assert!(result.images.is_empty());
593 assert!(result.tables.is_empty());
594 assert!(result.code_blocks.is_empty());
595 assert!(result.structured_data.is_empty());
596 assert!(result.metadata.title.is_some());
597 }
598
599 #[test]
602 fn 解析HTML片段() {
603 let parser = HtmlParser::new();
604 let fragment = "<p>这是片段内容。</p><a href=\"https://example.com\">链接</a>";
605 let result = parser.parse_fragment(fragment).unwrap();
606 assert!(result.text.cleaned_text.contains("片段内容"));
607 assert_eq!(result.links.len(), 1);
608 }
609
610 #[test]
611 fn 片段解析无元数据() {
612 let parser = HtmlParser::new();
613 let result = parser.parse_fragment("<p>纯文本</p>").unwrap();
614 assert!(result.metadata.title.is_none());
615 }
616
617 #[test]
620 fn 单项提取元数据() {
621 let parser = HtmlParser::new();
622 let meta = parser.extract_metadata(SAMPLE_HTML).unwrap();
623 assert_eq!(meta.title.as_deref(), Some("OG 示例标题"));
624 assert!(meta.description.is_some());
625 assert!(!meta.keywords.is_empty());
626 }
627
628 #[test]
629 fn 单项提取文本() {
630 let parser = HtmlParser::new();
631 let text = parser.extract_text(SAMPLE_HTML).unwrap();
632 assert!(text.cleaned_text.contains("主标题"));
633 assert!(text.word_count > 5);
634 }
635
636 #[test]
637 fn 单项提取标题() {
638 let parser = HtmlParser::new();
639 let headings = parser.extract_headings(SAMPLE_HTML);
640 assert_eq!(headings.len(), 2);
641 assert_eq!(headings[0].text, "主标题");
642 assert_eq!(headings[1].text, "第一章");
643 }
644
645 #[test]
646 fn 单项提取链接() {
647 let parser = HtmlParser::new();
648 let links = parser.extract_links(SAMPLE_HTML).unwrap();
649 assert!(!links.is_empty());
650 let has_external = links.iter().any(|l| l.href.contains("example.com/other"));
651 assert!(has_external);
652 }
653
654 #[test]
655 fn 单项提取图片() {
656 let parser = HtmlParser::new();
657 let images = parser.extract_images(SAMPLE_HTML);
658 assert_eq!(images.len(), 1);
659 assert_eq!(images[0].alt, "示例图片");
660 }
661
662 #[test]
663 fn 单项提取列表() {
664 let parser = HtmlParser::new();
665 let lists = parser.extract_lists(SAMPLE_HTML);
666 assert_eq!(lists.len(), 2);
667 let ul = lists.iter().find(|l| l.items.len() == 3);
668 assert!(ul.is_some());
669 }
670
671 #[test]
672 fn 单项提取表格() {
673 let parser = HtmlParser::new();
674 let tables = parser.extract_tables(SAMPLE_HTML);
675 assert_eq!(tables.len(), 1);
676 assert_eq!(tables[0].rows.len(), 2);
677 }
678
679 #[test]
680 fn 单项提取代码块() {
681 let parser = HtmlParser::new();
682 let blocks = parser.extract_code_blocks(SAMPLE_HTML);
683 assert_eq!(blocks.len(), 1);
684 assert_eq!(blocks[0].language.as_deref(), Some("rust"));
685 }
686
687 #[test]
688 fn 单项提取引用() {
689 let parser = HtmlParser::new();
690 let quotes = parser.extract_quotes(SAMPLE_HTML);
691 assert_eq!(quotes.len(), 1);
692 }
693
694 #[test]
695 fn 单项提取结构化数据() {
696 let parser = HtmlParser::new();
697 let data = parser.extract_structured_data(SAMPLE_HTML);
698 assert_eq!(data.len(), 1);
699 assert_eq!(data[0].schema_type.as_deref(), Some("Article"));
700 }
701
702 #[test]
705 fn 解析相对URL() {
706 let parser = HtmlParser::with_base_url(Url::parse("https://example.com").unwrap());
707 let resolved = parser.resolve_url("/path/to/page");
708 assert_eq!(
709 resolved.as_deref(),
710 Some("https://example.com/path/to/page")
711 );
712 }
713
714 #[test]
715 fn 解析绝对URL() {
716 let parser = HtmlParser::with_base_url(Url::parse("https://example.com").unwrap());
717 let resolved = parser.resolve_url("https://other.com/page");
718 assert_eq!(resolved.as_deref(), Some("https://other.com/page"));
719 }
720
721 #[test]
722 fn 无基础URL时返回None() {
723 let parser = HtmlParser::new();
724 assert!(parser.resolve_url("/page").is_none());
725 }
726
727 #[test]
728 fn 空href返回None() {
729 let parser = HtmlParser::with_base_url(Url::parse("https://example.com").unwrap());
730 assert!(parser.resolve_url("").is_none());
731 }
732
733 #[test]
734 fn 检查是否有基础URL() {
735 let parser = HtmlParser::new();
736 assert!(!parser.has_base_url());
737 let parser2 = HtmlParser::with_base_url(Url::parse("https://example.com").unwrap());
738 assert!(parser2.has_base_url());
739 }
740
741 #[test]
742 fn 获取基础URL引用() {
743 let url = Url::parse("https://example.com").unwrap();
744 let parser = HtmlParser::with_base_url(url.clone());
745 assert_eq!(parser.base_url(), Some(&url));
746 }
747
748 #[test]
751 fn 便捷解析函数() {
752 let result = parse(SAMPLE_HTML).unwrap();
753 assert!(result.metadata.title.is_some());
754 }
755
756 #[test]
757 fn 便捷解析带URL() {
758 let url = Url::parse("https://example.com").unwrap();
759 let result = parse_with_url(SAMPLE_HTML, url).unwrap();
760 assert!(result.links.iter().any(|l| l.url.is_some()));
761 }
762
763 #[test]
764 fn 便捷获取元数据() {
765 let meta = get_metadata(SAMPLE_HTML).unwrap();
766 assert_eq!(meta.author.as_deref(), Some("测试作者"));
767 }
768
769 #[test]
770 fn 便捷获取文本() {
771 let text = get_text(SAMPLE_HTML).unwrap();
772 assert!(text.word_count > 0);
773 }
774
775 #[test]
776 fn 便捷获取链接() {
777 let links = get_links(SAMPLE_HTML).unwrap();
778 assert!(!links.is_empty());
779 }
780
781 #[test]
784 fn 内部resolve相对路径() {
785 let base = Url::parse("https://example.com/base/").unwrap();
786 assert_eq!(
787 resolve_relative_url(&base, "page.html"),
788 Some("https://example.com/base/page.html".to_string())
789 );
790 }
791
792 #[test]
793 fn 内部resolve根相对路径() {
794 let base = Url::parse("https://example.com/base/").unwrap();
795 assert_eq!(
796 resolve_relative_url(&base, "/page.html"),
797 Some("https://example.com/page.html".to_string())
798 );
799 }
800
801 #[test]
802 fn 内部resolve协议相对() {
803 let base = Url::parse("https://example.com").unwrap();
804 assert_eq!(
805 resolve_relative_url(&base, "//cdn.example.com/file.js"),
806 Some("https://cdn.example.com/file.js".to_string())
807 );
808 }
809
810 #[test]
811 fn 内部resolve跳过空白() {
812 let base = Url::parse("https://example.com").unwrap();
813 assert!(resolve_relative_url(&base, "").is_none());
814 assert!(resolve_relative_url(&base, " ").is_none());
815 }
816
817 #[test]
820 fn 统计节点数() {
821 let doc = Html::parse_document("<html><body><p>文本</p></body></html>");
822 assert!(count_nodes(&doc) > 0);
823 }
824
825 #[test]
826 fn 统计元素数() {
827 let doc = Html::parse_document(
828 "<html><head></head><body><p>文本</p><div><span>内联</span></div></body></html>",
829 );
830 let count = count_elements(&doc);
831 assert_eq!(count, 6);
833 }
834
835 #[test]
836 fn 统计文本节点数() {
837 let doc = Html::parse_document("<html><body><p>Hello</p><p>World</p></body></html>");
838 let count = count_text_nodes(&doc);
839 assert_eq!(count, 2);
840 }
841
842 #[test]
843 fn 统计注释数() {
844 let doc = Html::parse_document(
845 "<html><body><!-- 注释一 --><p>文本</p><!-- 注释二 --></body></html>",
846 );
847 let count = count_comments(&doc);
848 assert_eq!(count, 2);
849 }
850
851 #[test]
854 fn 解析包含特殊字符的HTML() {
855 let html = "<html><body><p><script>alert('xss')</script></p></body></html>";
856 let parser = HtmlParser::new();
857 let result = parser.parse(html).unwrap();
858 assert!(result.text.cleaned_text.contains("alert('xss')"));
859 }
860
861 #[test]
862 fn 解析多语言混合内容() {
863 let html = "<html><body><p>Hello 世界 こんにちは</p></body></html>";
864 let parser = HtmlParser::new();
865 let result = parser.parse(html).unwrap();
866 assert!(result.text.cleaned_text.contains("Hello 世界"));
867 }
868
869 #[test]
870 fn 解析极大段落不崩溃() {
871 let long_text = "A ".repeat(10_000);
872 let html = format!("<html><body><p>{}</p></body></html>", long_text);
873 let parser = HtmlParser::new();
874 let result = parser.parse(&html).unwrap();
875 assert!(result.text.word_count > 0);
876 }
877
878 #[test]
879 fn 配置关闭所有提取() {
880 let config = ParserConfig {
881 extract_images: false,
882 extract_links: false,
883 extract_tables: false,
884 extract_code_blocks: false,
885 extract_structured_data: false,
886 ..ParserConfig::default()
887 };
888 let parser = HtmlParser::with_config(config);
889 let result = parser.parse(SAMPLE_HTML).unwrap();
890 assert!(result.images.is_empty());
891 assert!(result.tables.is_empty());
892 assert!(result.code_blocks.is_empty());
893 assert!(result.structured_data.is_empty());
894 }
895
896 #[test]
897 fn 基础URL解析图片地址() {
898 let url = Url::parse("https://example.com").unwrap();
899 let parser = HtmlParser::with_base_url(url);
900 let result = parser.parse(SAMPLE_HTML).unwrap();
901 let img = &result.images[0];
902 assert_eq!(
903 img.url.as_deref(),
904 Some("https://example.com/images/photo.jpg")
905 );
906 }
907}