~vikanezrimaya/kittybox

1e815637e3e15c7eb81b45b51b40253f3ec57ebb — Vika 8 months ago 6c280c3
kittybox-html: cargo fmt
2 files changed, 184 insertions(+), 138 deletions(-)

M templates-neo/src/main.rs
M templates-neo/src/mf2.rs
M templates-neo/src/main.rs => templates-neo/src/main.rs +4 -3
@@ 2,7 2,6 @@ use std::io::Write;

use kittybox_html::mf2::Entry;


fn main() {
    let mf2 = serde_json::from_reader::<_, microformats::types::Item>(std::io::stdin()).unwrap();
    let entry = Entry::try_from(mf2).unwrap();


@@ 11,6 10,8 @@ fn main() {
    entry.build(&mut article);

    let mut stdout = std::io::stdout().lock();
    stdout.write_all(article.build().to_string().as_bytes()).unwrap();
    stdout
        .write_all(article.build().to_string().as_bytes())
        .unwrap();
    stdout.write_all(b"\n").unwrap();
}
\ No newline at end of file
}

M templates-neo/src/mf2.rs => templates-neo/src/mf2.rs +180 -135
@@ 1,21 1,27 @@
use std::{collections::HashMap, borrow::Cow};
use std::{borrow::Cow, collections::HashMap};

use html::{media::builders, inline_text::Anchor, content::builders::ArticleBuilder};
use microformats::types::{Class, KnownClass, Item, PropertyValue, temporal::Value as Temporal, Fragment};
use html::{
    content::builders::{ArticleBuilder, SectionBuilder},
    inline_text::Anchor,
    media::builders,
};
use microformats::types::{
    temporal::Value as Temporal, Class, Fragment, Item, KnownClass, PropertyValue,
};

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("wrong mf2 class, expected {expected:?}, got {got:?}")]
    WrongClass {
        expected: Vec<KnownClass>,
        got: Vec<Class>
        got: Vec<Class>,
    },
    #[error("entry lacks `uid` property")]
    NoUid,
    #[error("unexpected type of property value: expected {expected}, got {got:?}")]
    WrongValueType {
        expected: &'static str,
        got: PropertyValue
        got: PropertyValue,
    },
    #[error("missing property: {0}")]
    MissingProperty(&'static str),


@@ 23,17 29,17 @@ pub enum Error {

pub enum Image {
    Plain(url::Url),
    Accessible {
        src: url::Url,
        alt: String
    }
    Accessible { src: url::Url, alt: String },
}

impl Image {
    pub fn build(self, img: &mut html::media::builders::ImageBuilder) -> &mut html::media::builders::ImageBuilder {
    pub fn build(
        self,
        img: &mut html::media::builders::ImageBuilder,
    ) -> &mut html::media::builders::ImageBuilder {
        match self {
            Image::Plain(url) => img.src(String::from(url)),
            Image::Accessible { src, alt } => img.src(String::from(src)).alt(alt)
            Image::Accessible { src, alt } => img.src(String::from(src)).alt(alt),
        }
    }
}


@@ 44,7 50,7 @@ pub struct Card {
    name: String,
    note: Option<String>,
    photo: Image,
    pronouns: Vec<String>
    pronouns: Vec<String>,
}

impl TryFrom<Item> for Card {


@@ 54,8 60,8 @@ impl TryFrom<Item> for Card {
        if card.r#type.as_slice() != [Class::Known(KnownClass::Card)] {
            return Err(Error::WrongClass {
                expected: vec![KnownClass::Card],
                got: card.r#type
            })
                got: card.r#type,
            });
        }

        let mut props = card.properties.take();


@@ 64,19 70,26 @@ impl TryFrom<Item> for Card {
            if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() {
                uid
            } else {
                return Err(Error::NoUid)
                return Err(Error::NoUid);
            }
        };

        Ok(Self {
            uid,
            urls: props.remove("url").unwrap_or_default().into_iter()
                .filter_map(|v| if let PropertyValue::Url(url) = v {
                    Some(url)
                } else {
                    None
                }).collect(),
            name: props.remove("name")
            urls: props
                .remove("url")
                .unwrap_or_default()
                .into_iter()
                .filter_map(|v| {
                    if let PropertyValue::Url(url) = v {
                        Some(url)
                    } else {
                        None
                    }
                })
                .collect(),
            name: props
                .remove("name")
                .unwrap_or_default()
                .into_iter()
                .next()


@@ 85,10 98,11 @@ impl TryFrom<Item> for Card {
                    PropertyValue::Plain(plain) => Ok(plain),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                        got: other,
                    }),
                })?,
            note: props.remove("note")
            note: props
                .remove("note")
                .unwrap_or_default()
                .into_iter()
                .next()


@@ 96,11 110,12 @@ impl TryFrom<Item> for Card {
                    PropertyValue::Plain(plain) => Some(Ok(plain)),
                    other => Some(Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    }))
                        got: other,
                    })),
                })
                .transpose()?,
            photo: props.remove("photo")
            photo: props
                .remove("photo")
                .unwrap_or_default()
                .into_iter()
                .next()


@@ 109,58 124,66 @@ impl TryFrom<Item> for Card {
                    PropertyValue::Url(url) => Ok(Image::Plain(url)),
                    PropertyValue::Image(image) => Ok(Image::Accessible {
                        src: image.src,
                        alt: image.alt
                        alt: image.alt,
                    }),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                        got: other,
                    }),
                })?,
            pronouns: props.remove("pronoun")
            pronouns: props
                .remove("pronoun")
                .unwrap_or_default()
                .into_iter()
                .map(|v| match v {
                    PropertyValue::Plain(plain) => Ok(plain),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                        got: other,
                    }),
                })
                .collect::<Result<Vec<String>, _>>()?
                .collect::<Result<Vec<String>, _>>()?,
        })
    }
}

impl Card {
    pub fn build_section(self, section: &mut html::content::builders::SectionBuilder) -> &mut html::content::builders::SectionBuilder {
        section
            .class("mini-h-card")
            .anchor(|a| a
                .class("larger u-author")
    pub fn build_section(
        self,
        section: &mut html::content::builders::SectionBuilder,
    ) -> &mut html::content::builders::SectionBuilder {
        section.class("mini-h-card").anchor(|a| {
            a.class("larger u-author")
                .href(String::from(self.uid))
                .image(move |img| self.photo.build(img).loading("lazy"))
                .text(self.name)
            )
        })
    }

    pub fn build(self, article: &mut html::content::builders::ArticleBuilder) -> &mut html::content::builders::ArticleBuilder {
    pub fn build(
        self,
        article: &mut html::content::builders::ArticleBuilder,
    ) -> &mut html::content::builders::ArticleBuilder {
        let urls: Vec<_> = self.urls.into_iter().filter(|u| *u != self.uid).collect();

        article
            .class("h-card")
            .image(move |builder| self.photo.build(builder))
            .heading_1(move |builder| {
                builder.anchor(|builder| builder
                    .class("u-url u-uid p-name")
                    .href(String::from(self.uid))
                    .text(self.name)
                )
                builder.anchor(|builder| {
                    builder
                        .class("u-url u-uid p-name")
                        .href(String::from(self.uid))
                        .text(self.name)
                })
            });

        if !self.pronouns.is_empty() {
            article.span(move |span| {
                span.text("(");
                self.pronouns.into_iter().for_each(|p| { span.text(p); });
                self.pronouns.into_iter().for_each(|p| {
                    span.text(p);
                });
                span.text(")")
            });
        }


@@ 174,12 197,9 @@ impl Card {
            article.unordered_list(move |ul| {
                for url in urls {
                    let url = String::from(url);
                    ul.list_item(move |li| li.push({
                        Anchor::builder()
                            .href(url.clone())
                            .text(url)
                            .build()
                    }));
                    ul.list_item(move |li| {
                        li.push({ Anchor::builder().href(url.clone()).text(url).build() })
                    });
                }

                ul


@@ 196,7 216,10 @@ impl TryFrom<PropertyValue> for Card {
    fn try_from(v: PropertyValue) -> Result<Self, Self::Error> {
        match v {
            PropertyValue::Item(item) => item.take().try_into(),
            other => Err(Error::WrongValueType { expected: "h-card", got: other })
            other => Err(Error::WrongValueType {
                expected: "h-card",
                got: other,
            }),
        }
    }
}


@@ 207,7 230,7 @@ pub struct Cite {
    in_reply_to: Option<Vec<Citation>>,
    author: Card,
    published: Option<chrono::DateTime<chrono::FixedOffset>>,
    content: Content
    content: Content,
}

impl TryFrom<Item> for Cite {


@@ 217,18 240,17 @@ impl TryFrom<Item> for Cite {
        if cite.r#type.as_slice() != [Class::Known(KnownClass::Cite)] {
            return Err(Error::WrongClass {
                expected: vec![KnownClass::Cite],
                got: cite.r#type
            })
                got: cite.r#type,
            });
        }

        todo!()
    }

}

pub enum Citation {
    Brief(url::Url),
    Full(Cite)
    Full(Cite),
}

impl TryFrom<PropertyValue> for Citation {


@@ 239,8 261,8 @@ impl TryFrom<PropertyValue> for Citation {
            PropertyValue::Item(item) => Ok(Self::Full(item.take().try_into()?)),
            other => Err(Error::WrongValueType {
                expected: "url or h-cite",
                got: other
            })
                got: other,
            }),
        }
    }
}


@@ 250,9 272,7 @@ pub struct Content(Fragment);
impl From<Content> for html::content::Main {
    fn from(content: Content) -> Self {
        let mut builder = Self::builder();
        builder
            .class("e-content")
            .text(content.0.html);
        builder.class("e-content").text(content.0.html);
        if let Some(lang) = content.0.lang {
            builder.lang(Cow::Owned(lang));
        }


@@ 268,18 288,17 @@ pub struct Entry {
    category: Vec<String>,
    syndication: Vec<url::Url>,
    published: chrono::DateTime<chrono::FixedOffset>,
    content: Content
    content: Content,
}


impl TryFrom<Item> for Entry {
    type Error = Error;
    fn try_from(entry: Item) -> Result<Self, Self::Error> {
        if entry.r#type.as_slice() != [Class::Known(KnownClass::Entry)] {
            return Err(Error::WrongClass {
                expected: vec![KnownClass::Entry],
                got: entry.r#type
            })
                got: entry.r#type,
            });
        }

        let mut props = entry.properties.take();


@@ 288,75 307,96 @@ impl TryFrom<Item> for Entry {
            if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() {
                uid
            } else {
                return Err(Error::NoUid)
                return Err(Error::NoUid);
            }
        };
        Ok(Entry {
            uid,
            url: props.remove("url").unwrap_or_default().into_iter()
                .filter_map(|v| if let PropertyValue::Url(url) = v {
                    Some(url)
                } else {
                    None
                }).collect(),
            in_reply_to: props.remove("in-reply-to")
            url: props
                .remove("url")
                .unwrap_or_default()
                .into_iter()
                .filter_map(|v| {
                    if let PropertyValue::Url(url) = v {
                        Some(url)
                    } else {
                        None
                    }
                })
                .collect(),
            in_reply_to: props
                .remove("in-reply-to")
                .unwrap_or_default()
                .into_iter()
                .next()
                .map(|v| v.try_into())
                .transpose()?,
            author: props.remove("author")
            author: props
                .remove("author")
                .unwrap_or_default()
                .into_iter()
                .next()
                .map(|v| v.try_into())
                .transpose()?
                .ok_or(Error::MissingProperty("author"))?,
            category: props.remove("category")
            category: props
                .remove("category")
                .unwrap_or_default()
                .into_iter()
                .map(|v| match v {
                    PropertyValue::Plain(string) => Ok(string),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                        got: other,
                    }),
                })
                .collect::<Result<Vec<_>, _>>()?,
            syndication: props.remove("syndication")
            syndication: props
                .remove("syndication")
                .unwrap_or_default()
                .into_iter()
                .map(|v| match v {
                    PropertyValue::Url(url) => Ok(url),
                    other => Err(Error::WrongValueType {
                        expected: "link",
                        got: other
                    })
                        got: other,
                    }),
                })
                .collect::<Result<Vec<_>, _>>()?,
            published: props.remove("published")
            published: props
                .remove("published")
                .unwrap_or_default()
                .into_iter()
                .next()
                .map(|v| -> Result<chrono::DateTime<chrono::FixedOffset>, Error> {
                    match v {
                        PropertyValue::Temporal(Temporal::Timestamp(dt)) => {
                            // This is incredibly sketchy.
                            let (date, time, offset) = (dt.date.clone().unwrap().data, dt.as_time().unwrap().data.clone(), dt.as_time().unwrap().offset.unwrap().data);

                            date.and_time(time).and_local_timezone(offset).single().ok_or_else(|| Error::WrongValueType {
                                expected: "datetime with timezone",
                                got: PropertyValue::Temporal(Temporal::Timestamp(dt))
                            })
                        },
                        other => Err(Error::WrongValueType {
                            expected: "timestamp",
                            got: other
                        })
                    }
                })
                .map(
                    |v| -> Result<chrono::DateTime<chrono::FixedOffset>, Error> {
                        match v {
                            PropertyValue::Temporal(Temporal::Timestamp(dt)) => {
                                // This is incredibly sketchy.
                                let (date, time, offset) = (
                                    dt.date.clone().unwrap().data,
                                    dt.as_time().unwrap().data.clone(),
                                    dt.as_time().unwrap().offset.unwrap().data,
                                );

                                date.and_time(time)
                                    .and_local_timezone(offset)
                                    .single()
                                    .ok_or_else(|| Error::WrongValueType {
                                        expected: "datetime with timezone",
                                        got: PropertyValue::Temporal(Temporal::Timestamp(dt)),
                                    })
                            }
                            other => Err(Error::WrongValueType {
                                expected: "timestamp",
                                got: other,
                            }),
                        }
                    },
                )
                .ok_or(Error::MissingProperty("published"))??,
            content: props.remove("content")
            content: props
                .remove("content")
                .unwrap_or_default()
                .into_iter()
                .next()


@@ 365,8 405,8 @@ impl TryFrom<Item> for Entry {
                    PropertyValue::Fragment(fragment) => Ok(Content(fragment)),
                    other => Err(Error::WrongValueType {
                        expected: "html",
                        got: other
                    })
                        got: other,
                    }),
                })?,
        })
    }


@@ 376,44 416,49 @@ impl Entry {
    pub fn build(self, article: &mut ArticleBuilder) -> &mut ArticleBuilder {
        article
            .class("h-entry")
            .header(|header| header
                .class("metadata")
                .section(|section| self.author.build_section(section))
                .section(|div| {
                    div
                        .division(|div| div
                            .anchor(|a| a
                                .class("u-url u-uid")
                                .href(String::from(self.uid))
                                .push(html::inline_text::Time::builder()
                                    .text(self.published.format("%Y-%m-%d %a %H:%M:%S %z").to_string())
                                    .date_time(self.published.to_rfc3339_opts(
                                        chrono::SecondsFormat::Secs, false
                                    ))
                                    .build()
                            )))
                        .division(|div| div
                            .text("Tagged")
                            .unordered_list(|ul| {
                                for category in self.category {
                                    ul.list_item(|li| li
                                        .class("p-category")
                                        .text(category)
                                    );
                                }

                                ul
            .header(|header| {
                header
                    .class("metadata")
                    .section(|section| self.author.build_section(section))
                    .section(|section| {
                        section
                            .division(|div| {
                                div.anchor(|a| {
                                    a.class("u-url u-uid").href(String::from(self.uid)).push(
                                        html::inline_text::Time::builder()
                                            .text(
                                                self.published
                                                    .format("%Y-%m-%d %a %H:%M:%S %z")
                                                    .to_string(),
                                            )
                                            .date_time(self.published.to_rfc3339_opts(
                                                chrono::SecondsFormat::Secs,
                                                false,
                                            ))
                                            .build(),
                                    )
                                })
                            })
                            .division(|div| {
                                div.text("Tagged").unordered_list(|ul| {
                                    for category in self.category {
                                        ul.list_item(|li| li.class("p-category").text(category));
                                    }

                                    ul
                                })
                            })
                        )
                    })
            )
            })
            .main(|main| {
                if let Some(lang) = self.content.0.lang {
                    main.lang(lang);
                }

                // XXX .text() and .push() are completely equivalent
                // since .text() does no escaping
                main.push(self.content.0.html)
            })
            
            .footer(|footer| footer)
    }
}