v0.3.0 initial atom feed

This commit is contained in:
Penelope Gwen 2026-04-06 16:50:38 -07:00
parent 763f6ea969
commit 266097752d
7 changed files with 390 additions and 114 deletions

293
Cargo.lock generated
View file

@ -62,15 +62,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "ansi_colours"
version = "1.2.3"
@ -194,6 +185,19 @@ dependencies = [
"syn",
]
[[package]]
name = "atom_syndication"
version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3"
dependencies = [
"chrono",
"derive_builder",
"diligent-date-parser",
"never",
"quick-xml",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -404,11 +408,7 @@ version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
@ -541,12 +541,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core2"
version = "0.4.0"
@ -651,6 +645,81 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@ -683,6 +752,15 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "diligent-date-parser"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9"
dependencies = [
"chrono",
]
[[package]]
name = "directories"
version = "5.0.1"
@ -1178,36 +1256,18 @@ dependencies = [
"tower-service",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "image"
version = "0.25.9"
@ -1308,16 +1368,6 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "json5"
version = "0.4.1"
@ -1441,10 +1491,10 @@ dependencies = [
[[package]]
name = "mdws"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"anyhow",
"chrono",
"atom_syndication",
"dirs",
"http",
"lipgloss",
@ -1457,9 +1507,11 @@ dependencies = [
"serde_json",
"strip-ansi-escapes",
"text-template",
"time",
"tokio",
"toml 1.0.1+spec-1.1.0",
"toml-frontmatter",
"walkdir",
"warp",
]
@ -1594,6 +1646,12 @@ dependencies = [
"viuer",
]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@ -1635,6 +1693,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-derive"
version = "0.4.2"
@ -1938,6 +2002,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -2024,6 +2094,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quote"
version = "1.0.44"
@ -2307,6 +2387,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@ -2682,6 +2771,37 @@ dependencies = [
"zune-jpeg 0.4.21",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tint"
version = "1.0.1"
@ -3106,6 +3226,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "warp"
version = "0.4.2"
@ -3275,65 +3405,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View file

@ -1,6 +1,6 @@
[package]
name = "mdws"
version = "0.2.1"
version = "0.3.0"
edition = "2024"
authors = ["Penelope Gwen <support@pogmom.me>"]
license-file = "LICENSE.md"
@ -27,8 +27,10 @@ rand = "0.10.0"
strip-ansi-escapes = "0.2.1"
ptree = "0.5.2"
dirs = "6.0.0"
chrono = "0.4.44"
serde_json = "1.0.149"
atom_syndication = "0.12.7"
walkdir = "2.5.0"
time = { version = "0.3.47", features = ["formatting"] }
[package.metadata.deb]
changelog = "debian/changelog"

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
mdws 0.3.0-1 semistable; urgency=medium
* initial atom feed implementation
-- Penelope Gwen <support@pogmom.me> Tue, 06 Apr 2026 16:46:56 -0700
mdws 0.2.1-1 semistable; urgency=medium
* enable listing page contents

View file

@ -2,12 +2,26 @@ use dirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Clone)]
pub struct AtomAuthor {
pub name: String,
pub website: String,
pub email: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct AtomConfig {
pub author: AtomAuthor,
pub feed_name: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerConfig {
pub server_root: PathBuf,
pub listen_port: u16,
pub bind_address: [u8; 4],
pub server_domain: String,
pub atom_config: AtomConfig,
}
impl ServerConfig {
@ -42,6 +56,14 @@ impl ServerConfig {
listen_port: 3030,
bind_address: [127, 0, 0, 1],
server_domain: "127.0.0.1:3030".to_string(),
atom_config: AtomConfig {
author: AtomAuthor {
name: "NAME".to_string(),
website: "WEBSITE".to_string(),
email: "EMAIL".to_string(),
},
feed_name: "FEED".to_string(),
},
},
}
}

153
src/lib/feed.rs Normal file
View file

@ -0,0 +1,153 @@
//use atom_syndication::Feed;
use atom_syndication::{Entry, FixedDateTime};
use time::{OffsetDateTime, format_description};
use walkdir::DirEntry;
use warp::reply::Reply;
use crate::{
config::ServerConfig,
markdowner::{self, MarkdownModule},
};
fn is_hidden(entry: &DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| s.starts_with(".") || s.starts_with("assets"))
.unwrap_or(false)
}
fn is_markdown(entry: &DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| s.ends_with("md") || s.ends_with("header.md"))
.unwrap_or(false)
}
pub fn feed(server_config: ServerConfig) -> Box<dyn Reply> {
let atom_config = server_config.atom_config;
let mut wd: Vec<MarkdownModule> = walkdir::WalkDir::new(server_config.server_root.clone())
.max_depth(5)
.into_iter()
.filter_entry(|e| {
!is_hidden(e)
&& if e.path().is_file() {
is_markdown(e)
} else {
true
}
})
.filter_map(|e| e.ok())
.filter(|e| e.metadata().is_ok())
.filter(|e| {
e.path()
.extension()
.unwrap_or_default()
.to_str()
.unwrap()
.eq("md")
})
.filter(|e| {
!e.path()
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.eq("header")
})
.filter_map(|e| markdowner::build_markdown_module(&e.path().to_path_buf()))
.collect();
wd.sort_by_key(|k| k.metadata.date_created.clone());
let mut atom_entries: Vec<Entry> = vec![];
let author = atom_syndication::PersonBuilder::default()
.name(atom_config.author.name.clone())
.uri(atom_config.author.website)
.email(atom_config.author.email)
.build();
println!("{}", atom_config.author.name);
for i in wd {
let content = atom_syndication::ContentBuilder::default()
.content_type("html".to_string())
.src(format!(
"http://{}{}",
server_config.server_domain,
i.path
.parent()
.unwrap()
.strip_prefix(server_config.server_root.clone())
.unwrap()
.to_str()
.unwrap_or_default()
))
.value(
markdown::to_html_with_options(i.content.as_str(), &markdown::Options::gfm())
.unwrap_or_default(),
)
.base(server_config.server_domain.clone())
.build();
let timestamp = if let Some(updated) = i.metadata.date_updated {
updated
} else if let Some(created) = i.metadata.date_created {
created
} else {
let dt: OffsetDateTime = i
.path
.metadata()
.expect("could not get metadata")
.modified()
.expect("could not parse modify time")
.into();
let f = format_description::parse("[year]-[month]-[day]").unwrap();
dt.format(&f).unwrap()
};
println!("{}", timestamp);
let entry = atom_syndication::EntryBuilder::default()
.title(i.metadata.title.clone())
.updated(
FixedDateTime::parse_from_str(
format!("{} 00:00:00.000 +0000", &timestamp).as_str(),
"%Y-%m-%d %H:%M:%S%.3f %z",
)
.unwrap(),
)
.id(i.path.to_str().unwrap_or_default())
.author(author.clone())
.content(content)
.link(atom_syndication::Link {
href: format!(
"http://{}/{}#{}",
server_config.server_domain,
i.path
.parent()
.unwrap()
.strip_prefix(server_config.server_root.clone())
.unwrap()
.to_str()
.unwrap_or_default(),
i.path.file_stem().unwrap().to_str().unwrap_or_default(),
),
rel: "canonical".to_string(),
hreflang: None,
mime_type: None,
title: Some(i.metadata.title),
length: None,
})
.build();
atom_entries.push(entry);
}
let feed = atom_syndication::FeedBuilder::default()
.entries(atom_entries)
.author(author)
.title(atom_config.feed_name)
.build();
Box::new(warp::reply::with_status(
feed.to_string(),
warp::http::StatusCode::OK,
))
}

View file

@ -14,7 +14,7 @@ pub struct MarkdownModule {
pub metadata: FrontMatter,
}
fn build_markdown_module(markdown_path: &PathBuf) -> Option<MarkdownModule> {
pub fn build_markdown_module(markdown_path: &PathBuf) -> Option<MarkdownModule> {
std::fs::read_to_string(markdown_path).ok().map(|t| {
match toml_frontmatter::parse::<FrontMatter>(t.as_str()) {
Ok((fm, md)) => MarkdownModule {

View file

@ -10,6 +10,8 @@ use warp::{Filter, filters::path::FullPath};
mod config;
#[path = "lib/curl.rs"]
mod curl;
#[path = "lib/feed.rs"]
mod feed;
#[path = "lib/html.rs"]
mod html;
#[path = "lib/markdowner.rs"]
@ -98,7 +100,14 @@ fn renderer(
x_forwarded_for.clone().unwrap_or_default()
);
let request_path: PathBuf = path.as_str().strip_prefix("/").unwrap_or_default().into();
let target_path = config.server_root.join(request_path.clone());
let target_path = if request_path.ends_with("feed") {
config
.server_root
.join(request_path.clone().parent().unwrap())
} else {
config.server_root.join(request_path.clone())
};
if !target_path.exists()
|| target_path.is_file()
|| (((request_path.starts_with("assets") || request_path.starts_with(".git"))
@ -116,7 +125,9 @@ fn renderer(
return llm_refusal;
}
let counter = if let Some(remote_address) = x_forwarded_for {
let counter = if !request_path.ends_with("feed")
&& let Some(remote_address) = x_forwarded_for
{
count_visit(
host,
target_path.clone(),
@ -131,6 +142,11 @@ fn renderer(
println!("└serving path: {} to {}", path.as_str(), user_agent);
let page_contents = markdowner::get_markdown_modules(&target_path);
if request_path.ends_with("feed") {
return feed::feed(config);
}
let sidebar_dir = PathBuf::from("assets/sidebar/");
let sidebar_contents = sidebar_content(
@ -181,9 +197,9 @@ async fn main() {
let favicon = warp::path("favicon.ico").and(warp::fs::file(
config.server_root.join("assets/img/favicon.ico"),
));
// let feed = warp::path("feed").map(move || todo!());
let robots =
warp::path("robots.txt").and(warp::fs::file(config.server_root.join("assets/robots.txt")));
let config_clone = config.clone();
let markdowns = warp::any()