diff --git a/Cargo.lock b/Cargo.lock index b95604f..0fb10a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,19 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "atom_syndication" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21fb6a0b39c6517edafe46f8137e53c51742425a4dae1c73ee12264a37ad7541" +dependencies = [ + "chrono", + "derive_builder", + "diligent-date-parser", + "never", + "quick-xml", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -95,6 +108,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "codemap" version = "0.1.3" @@ -153,6 +176,72 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -176,6 +265,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "diligent-date-parser" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d0fd95c7c02e2d6c588c6c5628466fff9bdde4b8c6196465e087b08e792720" +dependencies = [ + "chrono", +] + [[package]] name = "dtoa" version = "0.4.8" @@ -191,6 +289,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -595,6 +699,12 @@ dependencies = [ "want", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -653,6 +763,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -857,6 +976,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + [[package]] name = "nodrop" version = "0.1.14" @@ -1163,6 +1288,16 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quote" version = "1.0.21" @@ -1288,6 +1423,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rss" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acaf1331b7fc4edc3c2920819fee1766c27e8d40da593155832db3d6dea64e92" +dependencies = [ + "atom_syndication", + "chrono", + "derive_builder", + "mime", + "never", + "quick-xml", + "url", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1484,6 +1634,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.105" @@ -1535,6 +1691,33 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa 1.0.4", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1743,6 +1926,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1952,11 +2136,15 @@ dependencies = [ "gray_matter", "handlebars", "hotwatch", + "itertools", "lol_html", "minifier", + "percent-encoding", "pulldown-cmark", + "rss", "serde", "serde_yaml", + "time", "tokio", "url", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index 1ef1f05..42ae303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,16 +12,20 @@ grass = { version = "0.11", default-features = false } gray_matter = "0.2" handlebars = "4.1" hotwatch = { version = "0.4", optional = true } +itertools = "0.10" lol_html = "0.3" minifier = { version = "0.2", features = ["html"] } +percent-encoding = { version = "2", optional = true } pulldown-cmark = { version = "0.9", default-features = false, features = ["simd"] } +rss = { version = "2", features = ["validation"] } serde = { version = "1", features = ["derive"] } serde_yaml = "0.9" +time = { version = "0.3", features = ["serde-human-readable"] } tokio = { version = "1.10", features = ["macros", "rt-multi-thread"], optional = true } -url = "2.2" +url = { version = "2.2", features = ["serde"] } walkdir = "2" warp = { version = "0.3", optional = true } [features] default = ["serve"] -serve = ["futures", "hotwatch", "tokio", "warp"] +serve = ["futures", "hotwatch", "percent-encoding", "tokio", "warp"] diff --git a/site/config.yaml b/site/config.yaml index 4c50fff..6cbce57 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -2,3 +2,6 @@ base_url: "https://zyl.gay" title: Zyllian description: "Zoey's website." sass_styles: [index.scss] +images_per_page: 10 +cdn_url: "https://i.zyl.gay" +s3_prefix: z/ diff --git a/site/images/amogus.yml b/site/images/amogus.yml new file mode 100644 index 0000000..87321d4 --- /dev/null +++ b/site/images/amogus.yml @@ -0,0 +1,6 @@ +title: among us 😱 +timestamp: 2022-12-14T00:00:00.00Z +alt: Screenshot of Pokémon White on an evolution screen. Text reads "Congratulations! Your amogus evolved into Amoonguss!" +desc: aaahhhh +file: amogus.png +tags: [pokémon, sussy] diff --git a/site/images/cat.yml b/site/images/cat.yml new file mode 100644 index 0000000..89b402b --- /dev/null +++ b/site/images/cat.yml @@ -0,0 +1,5 @@ +title: cat +timestamp: 2022-12-14T00:00:00.00Z +alt: Picture of my cat sleeping curled up on top of some pillows. +file: cat.jpeg +tags: [cat] diff --git a/site/images/cat2.yml b/site/images/cat2.yml new file mode 100644 index 0000000..8bd0359 --- /dev/null +++ b/site/images/cat2.yml @@ -0,0 +1,5 @@ +title: cat 2 +timestamp: 2022-12-14T00:00:00.00Z +alt: Close up picture of my cat laying on a shelf while staring not quite at the camera. +file: cat2.jpeg +tags: [cat] diff --git a/site/images/cat3.yml b/site/images/cat3.yml new file mode 100644 index 0000000..df3b94c --- /dev/null +++ b/site/images/cat3.yml @@ -0,0 +1,5 @@ +title: cat 3 +timestamp: 2022-12-14T00:00:00.00Z +alt: Picture of my cat sleeping in a box barely large enough for them. Their head is reasting on one edge of the box. +file: cat/boxtop.jpeg +tags: [cat] diff --git a/site/images/trans-comfy.yml b/site/images/trans-comfy.yml new file mode 100644 index 0000000..f8d398f --- /dev/null +++ b/site/images/trans-comfy.yml @@ -0,0 +1,6 @@ +title: shorts to dresses +timestamp: 2022-12-14T00:00:00.00Z +alt: Screenshot from Pokémon Black 2 of an NPC saying "This dress is comfy and easy to wear..." +desc: yooo they're turning the comfy shorts kid trans +file: trans-comfy.png +tags: [pokémon, trans] diff --git a/site/sass/index.scss b/site/sass/index.scss index 08574e5..12c554b 100644 --- a/site/sass/index.scss +++ b/site/sass/index.scss @@ -51,3 +51,44 @@ main.page { abbr { cursor: help; } + +.images-list { + display: flex; + flex-wrap: wrap; + + .image { + position: relative; + padding: 4px; + height: auto; + + .image-actual { + display: block; + width: 300px; + height: 100%; + object-fit: cover; + } + + .title { + position: absolute; + bottom: 0; + left: 0; + padding: 4px; + margin: 4px; + background-color: rgba(0, 0, 0, 0.7); + color: white; + } + } +} + +.image-full { + .title, .tags-title { + margin-bottom: 0; + } + + .image-actual { + width: 100%; + max-height: 80vh; + object-fit: contain; + background-color: rgba(0, 0, 0, 0.3); + } +} diff --git a/site/templates/base.hbs b/site/templates/base.hbs index 22fe147..1573c56 100644 --- a/site/templates/base.hbs +++ b/site/templates/base.hbs @@ -2,7 +2,6 @@ - {{{head}}} @@ -14,6 +13,7 @@ she/they + Images | Source
diff --git a/site/templates/basic-link-list.hbs b/site/templates/basic-link-list.hbs new file mode 100644 index 0000000..88607ca --- /dev/null +++ b/site/templates/basic-link-list.hbs @@ -0,0 +1,8 @@ +

{{title}}

+ diff --git a/site/templates/image.hbs b/site/templates/image.hbs new file mode 100644 index 0000000..13567e1 --- /dev/null +++ b/site/templates/image.hbs @@ -0,0 +1,15 @@ +
+

{{title}}

+ {{timestamp}} + {{alt}} + {{#if desc}} +

{{desc}}

+ {{/if}} +

View full size image

+

Tags

+
+ {{#each tags}} + {{this}}{{#unless @last}},{{/unless}} + {{/each}} +
+
diff --git a/site/templates/images.hbs b/site/templates/images.hbs new file mode 100644 index 0000000..fc43c4c --- /dev/null +++ b/site/templates/images.hbs @@ -0,0 +1,22 @@ +{{#if tag}} +

Images tagged {{tag}}

+

View all images

+{{else}} +

Images

+

View image tags

+{{/if}} +

Page {{page}}/{{page_max}}

+{{#if previous}} +Previous page +{{/if}} +{{#if next}} +Next page +{{/if}} +
+ {{#each images}} + + {{alt}} + {{title}} + + {{/each}} +
diff --git a/site/templates/rss/image.hbs b/site/templates/rss/image.hbs new file mode 100644 index 0000000..b520216 --- /dev/null +++ b/site/templates/rss/image.hbs @@ -0,0 +1 @@ +{{alt}} diff --git a/src/builder.rs b/src/builder.rs index 2c58a4d..6a89c8c 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,7 +11,7 @@ use serde::Serialize; use url::Url; use walkdir::WalkDir; -use crate::{util, PageMetadata, Site, ROOT_PATH, SASS_PATH, STATIC_PATH}; +use crate::{images::ImageMetadata, util, PageMetadata, Site, ROOT_PATH, SASS_PATH, STATIC_PATH}; /// Struct containing data to be sent to templates when rendering them. #[derive(Debug, Serialize)] @@ -102,29 +102,23 @@ impl<'a> SiteBuilder<'a> { .context("Failed to copy static directory")?; } + let images_path = self.build_path.join(crate::images::IMAGES_OUT_PATH); + if !images_path.exists() { + std::fs::create_dir(images_path).context("Failed to create images path")?; + } + Ok(self) } - /// Builds a page. - pub fn build_page(&self, page_name: &str) -> anyhow::Result<()> { - let page_path = self.site.page_index.get(page_name).unwrap(); - - let input = std::fs::read_to_string(page_path) - .with_context(|| format!("Failed to read page at {}", page_path.display()))?; - let page = self.matter.parse(&input); - let page_metadata = if let Some(data) = page.data { - data.deserialize()? - } else { - PageMetadata::default() - }; - - let parser = Parser::new_ext(&page.content, Options::all()); - let mut page_html = String::new(); - pulldown_cmark::html::push_html(&mut page_html, parser); - + /// Helper to build a page without writing it to disk. + pub fn build_page_raw( + &self, + page_metadata: PageMetadata, + page_html: &str, + ) -> anyhow::Result { let out = self.reg.render( &page_metadata.template.unwrap_or_else(|| "base".to_string()), - &TemplateData { page: &page_html }, + &TemplateData { page: page_html }, )?; let title = match &page_metadata.title { @@ -184,6 +178,28 @@ impl<'a> SiteBuilder<'a> { out = minifier::html::minify(&out); } + Ok(out) + } + + /// Builds a page. + pub fn build_page(&self, page_name: &str) -> anyhow::Result<()> { + let page_path = self.site.page_index.get(page_name).expect("Missing page"); + + let input = std::fs::read_to_string(page_path) + .with_context(|| format!("Failed to read page at {}", page_path.display()))?; + let page = self.matter.parse(&input); + let page_metadata = if let Some(data) = page.data { + data.deserialize()? + } else { + PageMetadata::default() + }; + + let parser = Parser::new_ext(&page.content, Options::all()); + let mut page_html = String::new(); + pulldown_cmark::html::push_html(&mut page_html, parser); + + let out = self.build_page_raw(page_metadata, &page_html)?; + let out_path = self.build_path.join(page_name).with_extension("html"); std::fs::create_dir_all(out_path.parent().unwrap()) .with_context(|| format!("Failed to create directory for page {}", page_name))?; @@ -239,4 +255,14 @@ impl<'a> SiteBuilder<'a> { Ok(()) } + + /// Builds the site's various image pages. + pub fn build_images(&self) -> anyhow::Result<()> { + let images = ImageMetadata::load_all(&self.site.site_path)?; + ImageMetadata::build_lists(self, &images)?; + for (id, image) in images { + image.build(self, id)?; + } + Ok(()) + } } diff --git a/src/images.rs b/src/images.rs new file mode 100644 index 0000000..9f72723 --- /dev/null +++ b/src/images.rs @@ -0,0 +1,292 @@ +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use anyhow::Context; +use itertools::Itertools; +use rss::{validation::Validate, ChannelBuilder, ItemBuilder}; +use serde::{Deserialize, Serialize}; +use time::{format_description::well_known::Rfc2822, OffsetDateTime}; +use url::Url; + +use crate::{builder::SiteBuilder, link_list::Link, PageMetadata, SiteConfig}; + +pub(crate) const IMAGES_PATH: &str = "images"; +pub(crate) const IMAGES_OUT_PATH: &str = "i"; + +/// Definition for a remote image. +#[derive(Debug, Deserialize, Serialize)] +pub struct ImageMetadata { + /// The image's title. + pub title: String, + /// The image's timestamp. + #[serde(with = "time::serde::rfc3339")] + pub timestamp: OffsetDateTime, + /// The image's alt text. + pub alt: String, + /// The image's extra description, if any. + pub desc: Option, + /// The image's file path. + pub file: String, + /// The image's tags. + pub tags: Vec, +} + +impl ImageMetadata { + /// Gets an image's ID from its path. + pub fn get_id(path: &Path) -> String { + path.with_extension("") + .file_name() + .expect("Should never fail") + .to_string_lossy() + .into_owned() + } + + /// Loads an image's ID and metadata. + pub fn load(path: &Path) -> anyhow::Result<(String, Self)> { + let id = Self::get_id(path); + let metadata: ImageMetadata = serde_yaml::from_str(&std::fs::read_to_string(path)?)?; + Ok((id, metadata)) + } + + /// Loads all available images. + pub fn load_all(site_path: &Path) -> anyhow::Result> { + let images_path = site_path.join(IMAGES_PATH); + let mut images = Vec::new(); + for e in images_path.read_dir()? { + let p = e?.path(); + if let Some(ext) = p.extension() { + if ext == "yml" { + let (id, metadata) = Self::load(&p)?; + images.push((id, metadata)); + } + } + } + images.sort_by(|a, b| a.1.timestamp.cmp(&b.1.timestamp).reverse()); + Ok(images) + } + + /// Builds an image's page. + pub fn build(self, builder: &SiteBuilder, id: String) -> anyhow::Result<()> { + let out = { + let data = ImageTemplateData { + image: &self, + src: self.cdn_url(&builder.site.config)?.to_string(), + id: &id, + }; + builder.reg.render("image", &data)? + }; + let out = builder.build_page_raw( + PageMetadata { + title: Some(self.title), + ..Default::default() + }, + &out, + )?; + let out_path = Self::build_path(&builder.build_path, &id); + std::fs::write(out_path, out)?; + Ok(()) + } + + /// Gets an image's CDN url. + pub fn cdn_url(&self, config: &SiteConfig) -> anyhow::Result { + Ok(config.cdn_url.join(&config.s3_prefix)?.join(&self.file)?) + } + + /// Gets an image's build path. + pub fn build_path(build_path: &Path, id: &str) -> PathBuf { + build_path + .join(IMAGES_OUT_PATH) + .join(id) + .with_extension("html") + } + + /// Builds the various list pages for images. + pub fn build_lists(builder: &SiteBuilder, metadata: &[(String, Self)]) -> anyhow::Result<()> { + let mut data = Vec::with_capacity(metadata.len()); + for (id, metadata) in metadata { + data.push(ImageTemplateData { + image: metadata, + src: metadata.cdn_url(&builder.site.config)?.to_string(), + id, + }); + } + + fn build_list( + builder: &SiteBuilder, + list: Vec<&ImageTemplateData>, + title: &str, + tag: Option<&str>, + out_path: &Path, + items_per_page: usize, + ) -> anyhow::Result<()> { + if !out_path.exists() { + std::fs::create_dir_all(out_path)?; + } + + let page_max = list.len() / items_per_page + + if list.len() % items_per_page == 0 { + 0 + } else { + 1 + }; + let mut previous = None; + let mut next; + for (page, iter) in list.iter().chunks(items_per_page).into_iter().enumerate() { + next = (page + 1 != page_max).then_some(page + 2); + let data = ImageListTemplateData { + images: iter.copied().collect(), + tag, + page: page + 1, + page_max, + previous, + next, + }; + let out = builder.reg.render("images", &data)?; + let out = builder.build_page_raw( + PageMetadata { + title: Some(title.to_owned()), + ..Default::default() + }, + &out, + )?; + if page == 0 { + std::fs::write(out_path.join("index.html"), &out)?; + } + std::fs::write( + out_path.join(out_path.join((page + 1).to_string()).with_extension("html")), + out, + )?; + previous = Some(page + 1); + } + + Ok(()) + } + + build_list( + builder, + data.iter().collect(), + "Images", + None, + &builder.build_path.join("images"), + builder.site.config.images_per_page, + )?; + + let mut tags: BTreeMap> = BTreeMap::new(); + for image in &data { + for tag in image.image.tags.iter().cloned() { + tags.entry(tag).or_default().push(image); + } + } + + { + let links = tags + .iter() + .map(|(tag, data)| { + let count = data.len(); + ( + Link::new( + format!("/{IMAGES_OUT_PATH}/tag/{tag}/"), + format!("{tag} ({count})"), + ), + count, + ) + }) + .sorted_by(|(_, a), (_, b)| a.cmp(b).reverse()) + .map(|(l, _)| l) + .collect(); + let out = crate::link_list::render_basic_link_list(builder, links, "Image Tags")?; + std::fs::write( + builder.build_path.join(IMAGES_OUT_PATH).join("tags.html"), + out, + )?; + } + + for (tag, data) in tags { + build_list( + builder, + data, + &format!("Images tagged {tag}"), + Some(tag.as_str()), + &builder + .build_path + .join(IMAGES_OUT_PATH) + .join("tag") + .join(&tag), + builder.site.config.images_per_page, + )?; + } + + let mut items = Vec::with_capacity(data.len()); + for image in data { + items.push( + ItemBuilder::default() + .title(Some(image.image.title.to_owned())) + .link(Some( + builder + .site + .config + .base_url + .join(&format!("{IMAGES_OUT_PATH}/{}", image.id))? + .to_string(), + )) + .description(image.image.desc.to_owned()) + .pub_date(Some(image.image.timestamp.format(&Rfc2822)?)) + .content(Some(builder.reg.render("rss/image", &image)?)) + .build(), + ); + } + + // Build RSS feed + let channel = ChannelBuilder::default() + .title("Zyllian's images".to_string()) + .link( + builder + .site + .config + .base_url + .join("images/") + .expect("Should never fail"), + ) + .description("Feed of newly uploaded images from Zyllian's website.".to_string()) + .last_build_date(Some(OffsetDateTime::now_utc().format(&Rfc2822)?)) + .items(items) + .build(); + channel.validate().context("Failed to validate RSS feed")?; + let out = channel.to_string(); + std::fs::write(builder.build_path.join("images").join("rss.xml"), out)?; + + Ok(()) + } +} + +/// Template data for a specific image. +#[derive(Debug, Serialize)] +struct ImageTemplateData<'i> { + /// The image's regular metadata. + #[serde(flatten)] + image: &'i ImageMetadata, + /// Direct URL to the image's CDN location. + /// TODO: link to smaller versions on list pages + src: String, + /// The image's ID. + id: &'i str, +} + +/// Template data for image lists. +#[derive(Debug, Serialize)] +struct ImageListTemplateData<'i> { + /// The list of images to display. + images: Vec<&'i ImageTemplateData<'i>>, + /// The current tag, if any. + tag: Option<&'i str>, + /// The current page. + page: usize, + /// The total number of pages. + page_max: usize, + /// The previous page, if any. + previous: Option, + /// The next page, if any. + next: Option, +} diff --git a/src/lib.rs b/src/lib.rs index ef6afaf..e44663d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ mod builder; +mod images; +mod link_list; #[cfg(feature = "serve")] pub mod serving; mod util; @@ -10,6 +12,7 @@ use std::{ use anyhow::Context; use serde::Deserialize; +use url::Url; use walkdir::WalkDir; use builder::SiteBuilder; @@ -24,7 +27,7 @@ const ROOT_PATH: &str = "root"; #[derive(Debug, Deserialize)] pub struct SiteConfig { /// The location the site is at. - pub base_url: String, + pub base_url: Url, /// The site's title. pub title: String, /// The site's description? Not sure if this will actually be used or not @@ -33,6 +36,12 @@ pub struct SiteConfig { pub build: Option, /// A list of Sass stylesheets that will be built. pub sass_styles: Vec, + /// The number of images to display on a single page of an image list. + pub images_per_page: usize, + /// URL to the CDN used for the site's images. + pub cdn_url: Url, + /// Prefix applied to all files uploaded to the site's S3 space. + pub s3_prefix: String, } /// Struct for the front matter in templates. (nothing here yet) @@ -124,6 +133,7 @@ impl Site { builder.site.build_all_pages(&builder)?; builder.build_sass()?; + builder.build_images()?; Ok(()) } diff --git a/src/link_list.rs b/src/link_list.rs new file mode 100644 index 0000000..27d4b9e --- /dev/null +++ b/src/link_list.rs @@ -0,0 +1,50 @@ +use std::borrow::Cow; + +use serde::Serialize; + +use crate::{builder::SiteBuilder, PageMetadata}; + +/// Helper for links. +#[derive(Debug, Serialize)] +pub struct Link<'l> { + /// The link's actual link. + pub link: Cow<'l, str>, + /// The link's title. + pub title: Cow<'l, str>, +} + +impl<'l> Link<'l> { + /// Creates a new link. + pub fn new(link: impl Into>, title: impl Into>) -> Self { + let link = link.into(); + let title = title.into(); + Self { link, title } + } +} + +/// Renders a basic list of links. +pub fn render_basic_link_list( + builder: &SiteBuilder, + links: Vec, + title: &str, +) -> anyhow::Result { + let data = LinkTemplateData { links, title }; + let out = builder.reg.render("basic-link-list", &data)?; + let out = builder.build_page_raw( + PageMetadata { + title: Some(title.to_owned()), + ..Default::default() + }, + &out, + )?; + Ok(out) +} + +/// Template data for a list of links. +#[derive(Debug, Serialize)] +struct LinkTemplateData<'l> { + /// The actual links. + links: Vec>, + /// The title for the page. + title: &'l str, +} diff --git a/src/serving.rs b/src/serving.rs index eb5d43a..2f94418 100644 --- a/src/serving.rs +++ b/src/serving.rs @@ -18,7 +18,10 @@ use warp::{ Filter, }; -use crate::{Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, STATIC_PATH, TEMPLATES_PATH}; +use crate::{ + images::ImageMetadata, Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, STATIC_PATH, + TEMPLATES_PATH, +}; fn with_build_path( build_path: PathBuf, @@ -63,6 +66,7 @@ fn create( builder.refresh_template(&template_name_str, path)?; if build { builder.site.build_all_pages(builder)?; + builder.build_images()?; } } else if let Ok(_static_path) = relative_path.strip_prefix(STATIC_PATH) { std::fs::copy(path, builder.build_path.join(relative_path))?; @@ -76,6 +80,9 @@ fn create( } } else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) { std::fs::copy(path, builder.build_path.join(root_path))?; + } else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) { + // HACK: this could get very inefficient with a larger number of images. should definitely optimize + builder.build_images()?; } Ok(()) @@ -108,6 +115,11 @@ fn remove(builder: &mut SiteBuilder, path: &Path, relative_path: &Path) -> anyho builder.build_sass().context("Failed to rebuild Sass")?; } else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) { std::fs::remove_file(builder.build_path.join(root_path))?; + } else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) { + let p = ImageMetadata::build_path(&builder.build_path, &ImageMetadata::get_id(path)); + std::fs::remove_file(p)?; + // HACK: same as in `create` + builder.build_images()?; } Ok(()) @@ -134,6 +146,9 @@ impl Site { } } builder.build_sass().context("Failed to build Sass")?; + builder + .build_images() + .context("Failed to build image pages")?; // Map of websocket connections let peers: Arc>> = @@ -238,13 +253,16 @@ impl Site { .and_then(move |path: FullPath, build_path: PathBuf| async move { // Serve static files let p = &path.as_str()[1..]; + let p = percent_encoding::percent_decode_str(p) + .decode_utf8() + .expect("Failed to decode URL"); if p == "static/_dev.js" { let res = Response::new(include_str!("./refresh_websocket.js").into()); return Ok(res); } - let mut p = build_path.join(p); + let mut p = build_path.join(p.as_ref()); if !p.exists() { p = p.with_extension("html");