link_scraper 0.2.0

Scrape links from any document-file format
Documentation
pub mod elements;

use crate::formats::xml::xlink::elements::{
    XlinkElement, XlinkExtendedElement, XlinkSimpleElement,
};
use crate::formats::xml::xlink::XLinkFormatError::{
    ArcOutsideOfExtendedError, ExtendedInsideOfExtendedError, LocatorOutsideOfExtendedError,
    ResourceOutsideOfExtendedError, SimpleInsideOfExtendedError,
};
use crate::formats::xml::XmlStartElement;
use crate::helpers::find_urls;
use crate::{gen_scrape_from_file, gen_scrape_from_slice};
use itertools::Itertools;
use std::io::Read;
use thiserror::Error;
use xml::attribute::OwnedAttribute;
use xml::common::{Position, TextPosition};
use xml::reader::XmlEvent;
use xml::EventReader;

pub fn scrape<R>(reader: R) -> Result<Vec<XLinkLink>, XLinkFormatError>
where
    R: Read,
{
    let mut collector: Vec<XLinkLink> = vec![];

    let mut parser = EventReader::new(reader);
    while let Ok(xml_event) = &parser.next() {
        match xml_event {
            XmlEvent::StartElement {
                name,
                attributes,
                namespace,
            } => {
                let mut list = scrape_from_start_element(
                    XmlStartElement {
                        name,
                        attributes,
                        _namespace: namespace,
                    },
                    &mut parser,
                )?;
                collector.append(&mut list)
            }
            XmlEvent::EndDocument => break,
            _ => {}
        }
    }

    Ok(collector)
}
gen_scrape_from_file!(scrape(Read) -> Result<Vec<XLinkLink>, XLinkFormatError>);
gen_scrape_from_slice!(scrape(Read) -> Result<Vec<XLinkLink>, XLinkFormatError>);

#[derive(Error, Debug)]
pub enum XLinkFormatError {
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error("Unknown xlink:type value.")]
    UnknownTypeError(String),
    #[error("Xlink-element is missing a required attribute.")]
    MissingRequiredAttributeError(String),
    #[error("Found a locator-element outside of an extended element.")]
    LocatorOutsideOfExtendedError,
    #[error("Found an arc-element outside of an extended element.")]
    ArcOutsideOfExtendedError,
    #[error("Found a resource-element outside of an extended element.")]
    ResourceOutsideOfExtendedError,
    #[error("Found a simple-element inside of an extended element.")]
    SimpleInsideOfExtendedError,
    #[error("Found a extended-element inside of an extended element.")]
    ExtendedInsideOfExtendedError,
    #[error(transparent)]
    XmlReaderError(#[from] xml::reader::Error),
}

fn get_xlink_attribute_value(key: &str, attributes: &Vec<OwnedAttribute>) -> Option<String> {
    attributes
        .iter()
        .find(|attribute| {
            attribute.name.local_name == key
                && attribute.name.namespace == Some(XLINK_NAMESPACE.to_string())
        })
        .map(|href_attribute| href_attribute.value.to_string())
}

#[derive(Debug)]
pub struct XLinkLink {
    pub url: String,
    pub location: TextPosition,
    pub kind: XLinkLinkKind,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum XLinkLinkKind {
    Simple,
    Extended,
    Role,
    ArcRole,
}

static XLINK_NAMESPACE: &str = "http://www.w3.org/1999/xlink";

fn scrape_from_start_element<R>(
    xml_start_element: XmlStartElement,
    mut parser: &mut EventReader<R>,
) -> Result<Vec<XLinkLink>, XLinkFormatError>
where
    R: Read,
{
    let Some(xlink_element) = XlinkElement::try_from_xml_start_element(xml_start_element)? else {
        return Ok(vec![]);
    };

    match xlink_element {
        XlinkElement::Simple(element) => Ok(scrape_from_xlink_simple(element, &parser)),
        XlinkElement::Extended(element) => scrape_from_xlink_extended(element, &mut parser),
        XlinkElement::Locator(_) => Err(LocatorOutsideOfExtendedError),
        XlinkElement::Arc(_) => Err(ArcOutsideOfExtendedError),
        XlinkElement::Resource(_) => Err(ResourceOutsideOfExtendedError),
        XlinkElement::Title(_) => Ok(vec![]),
    }
}

fn scrape_from_option_string(
    role: Option<String>,
    link_type: XLinkLinkKind,
    position: TextPosition,
) -> Vec<XLinkLink> {
    let Some(role) = role else { return vec![] };
    let links = find_urls(&role)
        .iter()
        .map(|link| XLinkLink {
            url: link.as_str().to_string(),
            location: position,
            kind: link_type,
        })
        .collect_vec();
    links
}

fn scrape_from_xlink_extended<R>(
    xlink_extended_element: XlinkExtendedElement,
    parser: &mut EventReader<R>,
) -> Result<Vec<XLinkLink>, XLinkFormatError>
where
    R: Read,
{
    let mut ret: Vec<XLinkLink> = scrape_from_option_string(
        xlink_extended_element.role,
        XLinkLinkKind::Role,
        parser.position(),
    );

    while let Ok(xml_event) = &parser.next() {
        let mut links = match xml_event {
            XmlEvent::StartElement {
                name,
                attributes,
                namespace,
            } => {
                let Some(xlink_element) =
                    XlinkElement::try_from_xml_start_element(XmlStartElement {
                        name,
                        attributes,
                        _namespace: namespace,
                    })?
                else {
                    continue;
                };

                match xlink_element {
                    XlinkElement::Simple(_) => Err(SimpleInsideOfExtendedError),
                    XlinkElement::Extended(_) => Err(ExtendedInsideOfExtendedError),
                    XlinkElement::Locator(element) => {
                        let mut locator_links = vec![];

                        locator_links.push(XLinkLink {
                            url: element.href,
                            location: parser.position(),
                            kind: XLinkLinkKind::Extended,
                        });
                        locator_links.append(&mut scrape_from_option_string(
                            element.role,
                            XLinkLinkKind::Role,
                            parser.position(),
                        ));

                        Ok(locator_links)
                    }
                    XlinkElement::Arc(element) => Ok(scrape_from_option_string(
                        element.arcrole,
                        XLinkLinkKind::ArcRole,
                        parser.position(),
                    )),
                    XlinkElement::Resource(element) => Ok(scrape_from_option_string(
                        element.role,
                        XLinkLinkKind::Role,
                        parser.position(),
                    )),
                    XlinkElement::Title(_) => Ok(vec![]),
                }?
            }
            XmlEvent::EndElement { name } => {
                if name.eq(xlink_extended_element.xml.name) {
                    break;
                } else {
                    vec![]
                }
            }
            _ => vec![],
        };
        ret.append(&mut links);
    }

    Ok(ret)
}

fn scrape_from_xlink_simple<R>(
    xlink_element: XlinkSimpleElement,
    parser: &EventReader<R>,
) -> Vec<XLinkLink>
where
    R: Read,
{
    let mut ret =
        scrape_from_option_string(xlink_element.href, XLinkLinkKind::Simple, parser.position());
    ret.append(&mut scrape_from_option_string(
        xlink_element.arcrole,
        XLinkLinkKind::ArcRole,
        parser.position(),
    ));
    ret.append(&mut scrape_from_option_string(
        xlink_element.role,
        XLinkLinkKind::Role,
        parser.position(),
    ));
    ret
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEST_XLINK: &[u8] = include_bytes!("../../../../test_files/xml/xlink_test.xml");

    #[test]
    fn scrape_xlink_test() {
        let links = scrape(TEST_XLINK).unwrap();
        println!("{:?}", links);
        assert!(links
            .iter()
            .any(|it| it.url == "https://simple.test.com" && it.kind == XLinkLinkKind::Simple));
        assert!(
            links
                .iter()
                .any(|it| it.url == "https://extended.test.com/"
                    && it.kind == XLinkLinkKind::Extended)
        );
        assert!(links
            .iter()
            .any(|it| it.url == "https://role.test.com/" && it.kind == XLinkLinkKind::Role));
    }
}