sgmlish 0.1.0

Simple parsing and deserialization of SGML
Documentation
use std::mem;

use never::Never;

use crate::{Data, SgmlEvent, SgmlFragment};

use super::{extract_data_marked_section, Transform};

/// The action to be taken on the event being processed.
/// Used as a return value for closures passed to [`SgmlFragment::map_data`].
#[derive(Debug, PartialEq)]
pub enum MapDataResult<'a, E> {
    /// Keep the event, with the given data.
    Use(Data<'a>),
    /// Remove the event from the stream.
    Remove,
    /// Interrupt the transform with an error.
    Err(E),
}

impl<'a, E> MapDataResult<'a, E> {
    fn map<T, F: FnOnce(Data<'a>) -> T>(self, f: F) -> Result<Option<T>, E> {
        match self {
            MapDataResult::Use(data) => Ok(Some(f(data))),
            MapDataResult::Remove => Ok(None),
            MapDataResult::Err(err) => Err(err),
        }
    }
}

impl<'a> From<Data<'a>> for MapDataResult<'a, Never> {
    fn from(data: Data<'a>) -> Self {
        MapDataResult::Use(data)
    }
}

impl<'a> From<Option<Data<'a>>> for MapDataResult<'a, Never> {
    fn from(value: Option<Data<'a>>) -> Self {
        match value {
            Some(data) => MapDataResult::Use(data),
            None => MapDataResult::Remove,
        }
    }
}

impl<'a, E> From<Result<Data<'a>, E>> for MapDataResult<'a, E> {
    fn from(value: Result<Data<'a>, E>) -> Self {
        match value {
            Ok(data) => MapDataResult::Use(data),
            Err(err) => MapDataResult::Err(err),
        }
    }
}

impl<'a, E> From<Result<Option<Data<'a>>, E>> for MapDataResult<'a, E> {
    fn from(value: Result<Option<Data<'a>>, E>) -> Self {
        match value {
            Ok(Some(data)) => MapDataResult::Use(data),
            Ok(None) => MapDataResult::Remove,
            Err(err) => MapDataResult::Err(err),
        }
    }
}

pub(crate) fn try_map_data<'a, F, R, E>(
    mut fragment: SgmlFragment<'a>,
    mut f: F,
) -> Result<SgmlFragment<'a>, E>
where
    F: FnMut(Data<'a>, Option<&str>) -> R,
    R: Into<MapDataResult<'a, E>>,
{
    let mut transform = Transform::new();

    for (i, slot) in fragment.iter_mut().enumerate() {
        let event = mem::replace(slot, SgmlEvent::XmlCloseEmptyElement);
        let result = match event {
            SgmlEvent::Data(data) => f(data, None).into().map(SgmlEvent::Data)?,
            SgmlEvent::Attribute(key, Some(value)) => f(value, Some(&key))
                .into()
                .map(|value| SgmlEvent::Attribute(key, Some(value)))?,
            SgmlEvent::MarkedSection(status_keywords, content) => {
                match extract_data_marked_section(&status_keywords, content) {
                    Ok(data) => f(data, None).into().map(SgmlEvent::Data)?,
                    Err(content) => Some(SgmlEvent::MarkedSection(status_keywords, content)),
                }
            }
            event => Some(event),
        };
        match result {
            Some(event) => *slot = event,
            None => transform.remove_at(i),
        }
    }

    Ok(transform.apply(fragment))
}

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

    #[test]
    fn test_map() {
        let fragment = SgmlFragment::from(vec![
            SgmlEvent::Attribute("X".into(), Some(Data::RcData("value".into()))),
            SgmlEvent::Data(Data::RcData("rcdata".into())),
            SgmlEvent::Data(Data::CData("cdata".into())),
            SgmlEvent::MarkedSection("CDATA".into(), " marked cdata ".into()),
            SgmlEvent::MarkedSection("RCDATA".into(), " marked rcdata ".into()),
            // This one should not be passed
            SgmlEvent::MarkedSection("CDATA TEMP".into(), " marked cdata temp ".into()),
        ]);

        let result = try_map_data(fragment, |data, attr| {
            Data::CData(format!("{:?}={:?}", attr, data).into())
        })
        .unwrap()
        .into_vec();

        assert_eq!(result.len(), 6);
        assert_eq!(
            result[0],
            SgmlEvent::Attribute(
                "X".into(),
                Some(Data::CData(r##"Some("X")=RcData("value")"##.into()))
            )
        );
        assert_eq!(
            result[1],
            SgmlEvent::Data(Data::CData(r##"None=RcData("rcdata")"##.into()))
        );
        assert_eq!(
            result[2],
            SgmlEvent::Data(Data::CData(r##"None=CData("cdata")"##.into()))
        );
        assert_eq!(
            result[3],
            SgmlEvent::Data(Data::CData(r##"None=CData(" marked cdata ")"##.into()))
        );
        assert_eq!(
            result[4],
            SgmlEvent::Data(Data::CData(r##"None=RcData(" marked rcdata ")"##.into()))
        );
        assert_eq!(
            result[5],
            SgmlEvent::MarkedSection("CDATA TEMP".into(), " marked cdata temp ".into())
        );
    }
}