#[cfg(feature = "chrono")]
use chrono::{DateTime, Utc, SecondsFormat};
use quick_xml::{
events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
Writer,
};
use std::fmt::Display;
use std::io::Cursor;
use std::borrow::Cow;
#[derive(Debug)]
pub enum ChangeFreq {
Always,
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
Never,
}
impl Display for ChangeFreq {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let what = match self {
ChangeFreq::Always => "always",
ChangeFreq::Hourly => "hourly",
ChangeFreq::Daily => "daily",
ChangeFreq::Weekly => "weekly",
ChangeFreq::Monthly => "monthly",
ChangeFreq::Yearly => "yearly",
ChangeFreq::Never => "never",
};
f.write_str(what)
}
}
#[derive(Debug)]
pub struct Url {
pub loc: String,
#[cfg(feature = "chrono")]
pub lastmod: Option<DateTime<Utc>>,
#[cfg(not(feature = "chrono"))]
pub lastmod: Option<String>,
pub changefreq: Option<ChangeFreq>,
pub priority: Option<f32>,
}
impl Url {
pub fn new(loc: String) -> Self {
Self {
loc,
lastmod: None,
changefreq: None,
priority: None
}
}
}
#[derive(Debug)]
pub struct Sitemap {
pub urls: Vec<Url>,
}
fn write_tag<T: std::io::Write>(writer: &mut Writer<T>, tag: &str, text: &str) {
writer
.write_event(Event::Start(BytesStart::borrowed_name(tag.as_bytes())))
.expect(&format!("error opening {}", tag));
writer
.write_event(Event::Text(BytesText::from_plain_str(text)))
.expect(&format!("error writing text to {}", tag));
writer
.write_event(Event::End(BytesEnd::borrowed(tag.as_bytes())))
.expect(&format!("error opening {}", tag));
}
impl Sitemap {
pub fn new() -> Self {
Self {
urls: Vec::new(),
}
}
pub fn generate<T>(&self, inner_writer: T) -> T
where
T: std::io::Write,
{
let mut writer = Writer::new_with_indent(inner_writer, b' ', 4);
writer
.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"UTF-8"), None)))
.expect("error creating xml decl");
let urlset_name = b"urlset";
let mut urlset = BytesStart::borrowed_name(urlset_name);
urlset.push_attribute(("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9"));
writer
.write_event(Event::Start(urlset))
.expect("error opening urlset");
for url in self.urls.iter() {
writer
.write_event(Event::Start(BytesStart::borrowed_name(b"url")))
.expect("error opening url");
write_tag(&mut writer, "loc", &url.loc);
#[cfg(feature = "chrono")]
{
if let Some(lastmod) = &url.lastmod {
write_tag(&mut writer, "lastmod", &lastmod.to_rfc3339_opts(SecondsFormat::Secs, true));
}
}
#[cfg(not(feature = "chrono"))]
{
if let Some(lastmod) = &url.lastmod {
write_tag(&mut writer, "lastmod", lastmod);
}
}
if let Some(priority) = &url.priority {
write_tag(&mut writer, "priority", &format!("{:.1}", priority))
}
if let Some(changefreq) = &url.changefreq {
write_tag(&mut writer, "changefreq", &changefreq.to_string());
}
writer
.write_event(Event::End(BytesEnd::borrowed(b"url")))
.expect("error closing url");
}
writer
.write_event(Event::End(BytesEnd::borrowed(urlset_name)))
.expect("error closing urlset");
writer.into_inner()
}
pub fn into_bytes(&self) -> Cow<[u8]> {
let inner = Cursor::new(Vec::new());
let result = self.generate(inner);
Cow::Owned(result.into_inner())
}
pub fn into_str(&self) -> Cow<str> {
let bytes = self.into_bytes();
let res = std::str::from_utf8(&bytes).expect("error parsing sitemap bytes to str").to_owned();
Cow::Owned(res)
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[cfg(feature = "chrono")]
#[test]
fn it_works() {
use chrono::Utc;
let mut sitemap = Sitemap::new();
sitemap.urls.push(Url::new("https://domain.com/".to_owned()));
sitemap.urls.push(Url {
loc: "https://domain.com/url".to_owned(),
changefreq: Some(ChangeFreq::Daily),
priority: Some(0.8),
lastmod: Some(Utc::now())
}
);
sitemap.into_str();
}
#[cfg(not(feature = "chrono"))]
#[test]
fn it_works() {
let mut sitemap = Sitemap::new();
sitemap.urls.push(Url::new("https://domain.com/".to_owned()));
sitemap.urls.push(Url {
loc: "https://domain.com/url".to_owned(),
changefreq: Some(ChangeFreq::Daily),
priority: Some(0.8),
lastmod: Some("2020-11-22".to_owned()),
}
);
sitemap.into_str();
}
}