microformats-types 0.15.0

A representation of the known objects of Microformats
Documentation
//! Link relation types for Microformats2 documents.

use std::collections::BTreeMap;

/// Represents a link relation with its attributes.
#[derive(
    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, serde::Serialize, serde::Deserialize,
)]
pub struct Relation {
    /// The relation types (e.g., "alternate", "author").
    pub rels: Vec<String>,

    /// The language of the linked resource.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hreflang: Option<String>,
    /// The media type for which the link is intended.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub media: Option<String>,
    /// The title of the link.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// The MIME type of the linked resource.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#type: Option<String>,
    /// The text content of the link element.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

fn expand_text<T: std::fmt::Display>(value: &Option<T>, attr: &str) -> String {
    if let Some(value) = value {
        format!(" {attr}=\"{value}\"")
    } else {
        Default::default()
    }
}

impl Relation {
    /// Merges another relation into this one, combining rels and filling in missing attributes.
    pub fn merge_with(&mut self, other: Self) {
        self.rels.extend_from_slice(&other.rels);
        self.rels.sort();
        self.rels.dedup();

        if self.hreflang.is_none() {
            self.hreflang = other.hreflang;
        }

        if self.media.is_none() {
            self.media = other.media;
        }
        if self.title.is_none() {
            self.title = other.title;
        }
        if self.r#type.is_none() {
            self.r#type = other.r#type;
        }
        if self.text.is_none() {
            self.text = other.text;
        }
    }

    /// Converts this relation to an HTTP Link header value.
    pub fn to_header_value(&self, base_url: &url::Url) -> String {
        todo!("convert this relation to a header value with its URLs resolved to {base_url}")
    }

    /// Converts this relation to an HTML link element.
    pub fn to_html(&self, rel_url: &str) -> String {
        format!(
            "<link href=\"{rel_url}\"{hreflang}{rel}{type_}{title}{media} />",
            hreflang = expand_text(&self.hreflang, "hreflang"),
            rel = expand_text(&Some(self.rels.join(" ")), "rel"),
            type_ = expand_text(&self.r#type, "type"),
            title = expand_text(&self.title, "title"),
            media = expand_text(&self.media, "media")
        )
    }
}

/// A collection of relations indexed by URL.
#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
pub struct Relations {
    /// The relations indexed by their target URL.
    #[serde(flatten)]
    pub items: BTreeMap<url::Url, Relation>,
}

impl Relations {
    /// Returns a map from relation type to URLs that have that relation.
    pub fn by_rels(&self) -> BTreeMap<String, Vec<url::Url>> {
        let mut rels: BTreeMap<String, Vec<url::Url>> = BTreeMap::default();
        self.items
            .iter()
            .flat_map(|(u, rel)| {
                rel.rels
                    .iter()
                    .map(move |rel_name| (rel_name.to_owned(), u.to_owned()))
            })
            .for_each(|(rel_name, url)| {
                if let Some(rel_urls) = rels.get_mut(&rel_name) {
                    rel_urls.push(url);
                } else {
                    rels.insert(rel_name, vec![url]);
                }
            });

        rels.iter_mut().for_each(|(_, urls)| {
            urls.dedup();
            urls.sort()
        });

        rels
    }
}