1pub mod compression;
13pub mod error;
14pub mod models;
15pub mod parser;
16
17#[cfg(feature = "fetch")]
18pub mod fetcher;
19
20pub use compression::{decode_if_gzipped, decode_if_gzipped_with_limit};
21pub use error::SitemapError;
22pub use models::*;
23pub use parser::{parse_datetime, parse_priority, parse_sitemap};
24
25#[cfg(feature = "fetch")]
26pub use fetcher::SitemapFetcher;
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31
32 #[test]
33 fn test_parse_standard_urlset() {
34 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
35 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
36 <url>
37 <loc>https://example.com/page-1</loc>
38 <lastmod>2026-03-30T10:00:00Z</lastmod>
39 <changefreq>weekly</changefreq>
40 <priority>0.8</priority>
41 </url>
42 <url>
43 <loc>https://example.com/page-2</loc>
44 <lastmod>2026-03-29</lastmod>
45 <changefreq>monthly</changefreq>
46 <priority>0.5</priority>
47 </url>
48 </urlset>"#;
49
50 match parse_sitemap(xml) {
51 ParsedSitemap::UrlSet { entries } => {
52 assert_eq!(entries.len(), 2);
53
54 let first = &entries[0];
55 assert_eq!(first.loc, "https://example.com/page-1");
56 assert_eq!(first.changefreq, Some(ChangeFreq::Weekly));
57 assert_eq!(first.priority, Some(0.8));
58 assert!(first.lastmod.is_some());
59
60 let second = &entries[1];
61 assert_eq!(second.loc, "https://example.com/page-2");
62 assert_eq!(second.changefreq, Some(ChangeFreq::Monthly));
63 assert_eq!(second.priority, Some(0.5));
64 }
65 _ => panic!("Expected ParsedSitemap::UrlSet"),
66 }
67 }
68
69 #[test]
70 fn test_parse_sitemap_index() {
71 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
72 <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
73 <sitemap>
74 <loc>https://example.com/sitemap-posts.xml</loc>
75 <lastmod>2026-03-30</lastmod>
76 </sitemap>
77 <sitemap>
78 <loc>https://example.com/sitemap-tags.xml</loc>
79 <lastmod>2026-03-29</lastmod>
80 </sitemap>
81 </sitemapindex>"#;
82
83 match parse_sitemap(xml) {
84 ParsedSitemap::Index { child_urls } => {
85 assert_eq!(child_urls.len(), 2);
86 assert_eq!(child_urls[0], "https://example.com/sitemap-posts.xml");
87 assert_eq!(child_urls[1], "https://example.com/sitemap-tags.xml");
88 }
89 _ => panic!("Expected ParsedSitemap::Index"),
90 }
91 }
92
93 #[test]
94 fn test_parse_google_extensions() {
95 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
96 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
97 xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
98 xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
99 xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
100 xmlns:xhtml="http://www.w3.org/1999/xhtml">
101 <url>
102 <loc>https://example.com/post-1</loc>
103 <lastmod>2026-03-30T10:00:00Z</lastmod>
104 <changefreq>daily</changefreq>
105 <priority>0.8</priority>
106 <xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/post-1"/>
107 <image:image>
108 <image:loc>https://example.com/cover.jpg</image:loc>
109 <image:title>Post Cover</image:title>
110 <image:caption>A descriptive caption</image:caption>
111 </image:image>
112 <video:video>
113 <video:thumbnail_loc>https://example.com/thumb.jpg</video:thumbnail_loc>
114 <video:title>Tutorial Video</video:title>
115 <video:description>How to build with Rust</video:description>
116 <video:duration>600</video:duration>
117 <video:family_friendly>yes</video:family_friendly>
118 </video:video>
119 <news:news>
120 <news:publication>
121 <news:name>Tech Daily</news:name>
122 <news:language>en</news:language>
123 </news:publication>
124 <news:publication_date>2026-03-30</news:publication_date>
125 <news:title>Rust Monolith in 2026</news:title>
126 </news:news>
127 </url>
128 </urlset>"#;
129
130 match parse_sitemap(xml) {
131 ParsedSitemap::UrlSet { entries } => {
132 assert_eq!(entries.len(), 1);
133 let entry = &entries[0];
134 assert_eq!(entry.loc, "https://example.com/post-1");
135
136 assert_eq!(entry.hreflangs.len(), 1);
137 assert_eq!(entry.hreflangs[0].lang, "zh");
138 assert_eq!(entry.hreflangs[0].href, "https://example.com/zh/post-1");
139
140 assert_eq!(entry.images.len(), 1);
141 assert_eq!(entry.images[0].loc, "https://example.com/cover.jpg");
142 assert_eq!(entry.images[0].title.as_deref(), Some("Post Cover"));
143
144 assert_eq!(entry.videos.len(), 1);
145 assert_eq!(entry.videos[0].title, "Tutorial Video");
146 assert_eq!(entry.videos[0].duration_seconds, Some(600));
147 assert_eq!(entry.videos[0].family_friendly, Some(true));
148
149 assert!(entry.news.is_some());
150 let news = entry.news.as_ref().unwrap();
151 assert_eq!(news.publication_name, "Tech Daily");
152 assert_eq!(news.title, "Rust Monolith in 2026");
153 }
154 _ => panic!("Expected ParsedSitemap::UrlSet"),
155 }
156 }
157
158 #[test]
159 fn test_parse_cdata_and_bom_tolerance() {
160 let xml_cdata = "\u{feff}<?xml version=\"1.0\" encoding=\"UTF-8\"?>
161 <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">
162 <url>
163 <loc><![CDATA[https://example.com/article?id=100&type=tech]]></loc>
164 <lastmod><![CDATA[2026-03-30T12:00:00Z]]></lastmod>
165 </url>
166 </urlset>";
167
168 match parse_sitemap(xml_cdata) {
169 ParsedSitemap::UrlSet { entries } => {
170 assert_eq!(entries.len(), 1);
171 assert_eq!(
172 entries[0].loc,
173 "https://example.com/article?id=100&type=tech"
174 );
175 assert!(entries[0].lastmod.is_some());
176 }
177 _ => panic!("Expected ParsedSitemap::UrlSet from CDATA"),
178 }
179 }
180
181 #[test]
182 fn test_parse_plain_text_sitemap() {
183 let text_content = r#"
184 # Main Sitemap URLs
185 https://example.com/home
186 https://example.com/about
187
188 # Invalid Line Ignored
189 not-a-valid-url
190 https://example.com/contact
191 "#;
192
193 match parse_sitemap(text_content) {
194 ParsedSitemap::PlainText { urls } => {
195 assert_eq!(urls.len(), 3);
196 assert_eq!(urls[0], "https://example.com/home");
197 assert_eq!(urls[1], "https://example.com/about");
198 assert_eq!(urls[2], "https://example.com/contact");
199 }
200 _ => panic!("Expected ParsedSitemap::PlainText"),
201 }
202 }
203
204 #[test]
205 fn test_empty_sitemap_detection() {
206 let empty_urlset = r#"<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>"#;
207 let parsed = parse_sitemap(empty_urlset);
208 assert!(parsed.is_empty());
209
210 let empty_index =
211 r#"<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></sitemapindex>"#;
212 let parsed_idx = parse_sitemap(empty_index);
213 assert!(parsed_idx.is_empty());
214 }
215}