indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
use crate::mf2;

use mf2::types::PropertyValue;
use mf2::types::{Fragment, Item};
use serde::{Deserialize, Serialize};

/// Logic around finding the representative h-card of a URL.
///
/// This kind of parsing allows for deeper introspection and fuller discovery
/// of how and where entities represent themselves. Learn more at
/// <https://microformats.org/wiki/representative-h-card-parsing>.
pub mod representative_hcard;

/// Logic for running post type discovery.
///
/// This module provides a means of detecting the known and experimental
/// post types provided by the IndieWeb community.
pub mod ptd;

/// Logic around discerning relationships between two URLs.
///
/// This module provides an implementation for resolving and
/// discovering the advertised link relationships. This is one
/// of the more popular methods of resource discovery in the IndieWeb
/// of providers of the [standards][crate::standards].
pub mod link_rel;

/// Logic around discovering the author of posts.
///
/// This module provides an implementation of the [IndieWeb authorship algorithm][authorship-algorithm]
/// which deterministically discovers who the author of a post is based on microformats markup.
///
/// [authorship-algorithm]: https://indieweb.org/authorship-spec
#[cfg(feature = "experimental_authorship")]
pub mod authorship;

/// A normalized representation of properties from Microformats2 JSON.
///
/// This represents a "middle" type for converting Microformats2 JSON into
/// something more structured like an [item][microformats::types::Item].
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Properties(pub serde_json::Map<String, serde_json::Value>);

impl std::ops::Deref for Properties {
    type Target = serde_json::Map<String, serde_json::Value>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::ops::DerefMut for Properties {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl From<Properties> for serde_json::Value {
    fn from(val: Properties) -> Self {
        serde_json::Value::Object(val.0)
    }
}

impl TryFrom<serde_json::Value> for Properties {
    type Error = serde_json::Error;

    fn try_from(val: serde_json::Value) -> Result<Self, Self::Error> {
        serde_json::from_value::<Self>(val)
    }
}

impl Properties {
    /// Creates a copy of this set of properties that's been normalized.
    ///
    /// # Examples
    /// ```
    /// # use indieweb::algorithms::Properties;
    /// # use serde_json::json;
    /// #
    /// assert_eq!(
    ///     Properties::try_from(json!({"properties": {"actual": "value"}})).unwrap().normalize(),
    ///     Properties::try_from(json!({"actual": "value"})).unwrap(),
    ///     "use 'properties' as the value"
    /// );
    /// #
    /// assert_eq!(
    ///     Properties::try_from(json!({"actual": "value"})).unwrap().normalize(),
    ///     Properties::try_from(json!({"actual": "value"})).unwrap(),
    ///     "returns the keys and values as they are"
    /// );
    /// #
    /// ```
    pub fn normalize(&self) -> Properties {
        if self.contains_key("properties") {
            Properties(
                self.get("properties")
                    .and_then(|p| p.as_object().cloned())
                    .unwrap_or_default(),
            )
        } else {
            self.clone()
        }
    }
}

/// Pulls all of the URLs from this item.
///
/// This extracts all of the discoverable URLs from an item. This will
/// pull from:
///
/// * [item.value][microformats::types::Item::value] if it's a [URL value][microformats::types::ValueKind::Url]
/// * any property whose values are hinted as a [URL][microformats::types::ValueKind::Url]
/// * the declared [links of a HTML fragment][microformats::types::Fragment]
pub fn extract_urls(item: &Item) -> Vec<url::Url> {
    let mut all_urls = vec![];

    all_urls.extend(
        item.children
            .iter()
            .filter_map(|child| child.value.to_owned())
            .filter_map(|v| match v {
                microformats::types::ValueKind::Url(u) => Some(u),
                microformats::types::ValueKind::Plain(_) => None,
            }),
    );

    for property_values in item.properties.values() {
        for v in property_values {
            match v {
                PropertyValue::Url(u) => {
                    all_urls.push(url::Url::clone(u));
                }
                PropertyValue::Item(i) => {
                    all_urls.extend(extract_urls(i));
                }
                PropertyValue::Fragment(Fragment { links, .. }) => {
                    all_urls.extend(links.iter().filter_map(|v| v.parse().ok()));
                }
                _ => {}
            }
        }
    }

    all_urls
}

#[test]
fn extract_urls_test() {
    let item_result = Item::try_from(serde_json::json!({
        "type": ["h-entry"],
        "properties": {
            "content": [{"html": "Well this is a link <a href='http://example.com/3'>fooo</a>", "value": "Well this is a link fooo"}],
            "like-of": ["http://example.com/", "http://example.com/2"]
        }
    }));

    assert_eq!(item_result.as_ref().err(), None);
    let item = item_result.unwrap();

    // This _should_ be three but it's two because the Microformats library doesn't do extra
    // procesing yet on deserializing of values.
    assert_eq!(extract_urls(&item).len(), 2);
}