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 @@
+
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}}
+
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 @@
+
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");