Learning project: static site generator for ATproto PDS

Adds tangled repo, issue and PR support to mosaic #1

merged opened by hello.j23n.com targeting main from feature/tangled
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:enau5rzvrui4fx4dq5icgtle/sh.tangled.repo.pull/3m3phqhwgot22
+227 -164
Diff #0
+13 -129
src/atproto/mod.rs
··· 1 use std::path::PathBuf; 2 3 - use atrium_api::{ 4 - app::bsky::embed::defs::AspectRatio, 5 - types::{BlobRef, Unknown}, 6 - }; 7 - 8 mod client; 9 mod renderable; 10 - pub use atrium_api::app::bsky::feed::post::RecordData as BskyPost; 11 pub use client::fetch_records; 12 pub use renderable::Renderable; 13 use tera::{Context, Tera}; 14 - 15 - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 16 - pub struct Document { 17 - pub author: String, 18 - pub description: String, 19 - pub title: String, 20 - pub pages: Vec<Page>, 21 - #[serde(rename = "postRef")] 22 - pub post_ref: Option<Unknown>, 23 - pub publication: Unknown, 24 - #[serde(rename = "publishedAt")] 25 - pub published_at: String, 26 - #[serde(skip)] 27 - pub host: String, 28 - #[serde(skip)] 29 - pub did: String, 30 - } 31 - 32 - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 33 - pub struct Page { 34 - pub blocks: Vec<BlockWrapper>, 35 - } 36 - 37 - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 38 - pub struct BlockWrapper { 39 - pub block: Block, 40 - } 41 - 42 - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 43 - #[serde(tag = "$type")] 44 - pub enum Block { 45 - #[serde(rename = "pub.leaflet.blocks.blockquote")] 46 - Blockquote { 47 - plaintext: String, 48 - // TODO facets: [&pub.leaflet.richtext.facet] 49 - }, 50 - 51 - #[serde(rename = "pub.leaflet.blocks.code")] 52 - Code { 53 - plaintext: String, 54 - language: Option<String>, 55 - #[serde(rename = "syntaxHighlightingTheme")] 56 - syntax_highlighting_theme: Option<String>, 57 - }, 58 - 59 - #[serde(rename = "pub.leaflet.blocks.bskyPost")] 60 - BskyPost { 61 - #[serde(rename = "postRef")] 62 - post_ref: String, 63 - }, 64 - 65 - #[serde(rename = "pub.leaflet.blocks.header")] 66 - Header { 67 - level: Option<u32>, 68 - plaintext: String, 69 - // TODO: facets: [&pub.leaflet.richtext.facet 70 - }, 71 - 72 - #[serde(rename = "pub.leaflet.blocks.horizontalRule")] 73 - HorizontalRule {}, 74 - 75 - #[serde(rename = "pub.leaflet.blocks.iframe")] 76 - IFrame { url: String, height: Option<u32> }, 77 - 78 - #[serde(rename = "pub.leaflet.blocks.image")] 79 - Image { 80 - image: BlobRef, 81 - alt: Option<String>, 82 - #[serde(rename = "aspectRatio")] 83 - aspect_ratio: AspectRatio, 84 - }, 85 - 86 - #[serde(rename = "pub.leaflet.blocks.math")] 87 - Math { tex: String }, 88 - 89 - #[serde(rename = "pub.leaflet.blocks.text")] 90 - Text { 91 - plaintext: String, 92 - // TODO: facets: [&pub.leaflet.richtext.facet] 93 - }, 94 - 95 - #[serde(rename = "pub.leaflet.blocks.unorderedList")] 96 - UnorderedList { children: Vec<ListItem> }, 97 - 98 - #[serde(other)] 99 - Unknown, 100 - } 101 - 102 - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 103 - pub struct ListItem { 104 - content: ListItemContent, 105 - } 106 - 107 - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 108 - #[serde(tag = "$type")] 109 - pub enum ListItemContent { 110 - #[serde(rename = "pub.leaflet.blocks.text")] 111 - Text { plaintext: String }, 112 - #[serde(rename = "pub.leaflet.blocks.header")] 113 - Header { 114 - level: Option<u32>, 115 - plaintext: String, 116 - }, 117 - #[serde(rename = "pub.leaflet.blocks.image")] 118 - Image { 119 - image: BlobRef, 120 - alt: Option<String>, 121 - #[serde(rename = "aspectRatio")] 122 - aspect_ratio: AspectRatio, 123 - }, 124 - } 125 - 126 - #[derive(Debug, serde::Deserialize, serde::Serialize)] 127 - pub struct Website { 128 - src: String, 129 - description: Option<String>, 130 - title: Option<String>, 131 - #[serde(rename = "previewImage")] 132 - preview_image: BlobRef, 133 - } 134 135 #[derive(Clone, serde::Serialize)] 136 pub enum ContentType { 137 Document(Document), 138 - BskyPost(BskyPost), 139 } 140 141 #[derive(Clone, serde::Serialize)] ··· 179 fn build_name(content: &ContentType) -> String { 180 match content { 181 ContentType::Document(doc) => doc.title.clone(), 182 - ContentType::BskyPost(post) => String::from(post.created_at.as_str()), 183 } 184 } 185 } ··· 238 context.insert("host", &doc.host); 239 context.insert("did", &doc.did); 240 } 241 - ContentType::BskyPost(post) => { 242 context.insert("post", post); 243 } 244 } 245 tera.render(&self.template, &context)
··· 1 use std::path::PathBuf; 2 3 mod client; 4 mod renderable; 5 + pub mod types; 6 + 7 pub use client::fetch_records; 8 pub use renderable::Renderable; 9 use tera::{Context, Tera}; 10 + use types::bsky::Post; 11 + use types::leaflet::Document; 12 + use types::tangled::Repo; 13 14 #[derive(Clone, serde::Serialize)] 15 pub enum ContentType { 16 Document(Document), 17 + Post(Post), 18 + Repo(Repo), 19 } 20 21 #[derive(Clone, serde::Serialize)] ··· 59 fn build_name(content: &ContentType) -> String { 60 match content { 61 ContentType::Document(doc) => doc.title.clone(), 62 + ContentType::Post(post) => String::from(post.created_at.as_str()), 63 + ContentType::Repo(repo) => repo.name.clone(), 64 } 65 } 66 } ··· 119 context.insert("host", &doc.host); 120 context.insert("did", &doc.did); 121 } 122 + ContentType::Post(post) => { 123 context.insert("post", post); 124 + } 125 + ContentType::Repo(repo) => { 126 + context.insert("repo", repo); 127 } 128 } 129 tera.render(&self.template, &context)
+1
src/atproto/types/bsky.rs
···
··· 1 + pub use atrium_api::app::bsky::feed::post::RecordData as Post;
+124
src/atproto/types/leaflet.rs
···
··· 1 + use atrium_api::{ 2 + app::bsky::embed::defs::AspectRatio, 3 + types::{BlobRef, Unknown}, 4 + }; 5 + 6 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 7 + pub struct Document { 8 + pub author: String, 9 + pub description: String, 10 + pub title: String, 11 + pub pages: Vec<Page>, 12 + #[serde(rename = "postRef")] 13 + pub post_ref: Option<Unknown>, 14 + pub publication: Unknown, 15 + #[serde(rename = "publishedAt")] 16 + pub published_at: String, 17 + #[serde(skip)] 18 + pub host: String, 19 + #[serde(skip)] 20 + pub did: String, 21 + } 22 + 23 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 24 + pub struct Page { 25 + pub blocks: Vec<BlockWrapper>, 26 + } 27 + 28 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 29 + pub struct BlockWrapper { 30 + pub block: Block, 31 + } 32 + 33 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 34 + #[serde(tag = "$type")] 35 + pub enum Block { 36 + #[serde(rename = "pub.leaflet.blocks.blockquote")] 37 + Blockquote { 38 + plaintext: String, 39 + // TODO facets: [&pub.leaflet.richtext.facet] 40 + }, 41 + 42 + #[serde(rename = "pub.leaflet.blocks.code")] 43 + Code { 44 + plaintext: String, 45 + language: Option<String>, 46 + #[serde(rename = "syntaxHighlightingTheme")] 47 + syntax_highlighting_theme: Option<String>, 48 + }, 49 + 50 + #[serde(rename = "pub.leaflet.blocks.bskyPost")] 51 + BskyPost { 52 + #[serde(rename = "postRef")] 53 + post_ref: String, 54 + }, 55 + 56 + #[serde(rename = "pub.leaflet.blocks.header")] 57 + Header { 58 + level: Option<u32>, 59 + plaintext: String, 60 + // TODO: facets: [&pub.leaflet.richtext.facet 61 + }, 62 + 63 + #[serde(rename = "pub.leaflet.blocks.horizontalRule")] 64 + HorizontalRule {}, 65 + 66 + #[serde(rename = "pub.leaflet.blocks.iframe")] 67 + IFrame { url: String, height: Option<u32> }, 68 + 69 + #[serde(rename = "pub.leaflet.blocks.image")] 70 + Image { 71 + image: BlobRef, 72 + alt: Option<String>, 73 + #[serde(rename = "aspectRatio")] 74 + aspect_ratio: AspectRatio, 75 + }, 76 + 77 + #[serde(rename = "pub.leaflet.blocks.math")] 78 + Math { tex: String }, 79 + 80 + #[serde(rename = "pub.leaflet.blocks.text")] 81 + Text { 82 + plaintext: String, 83 + // TODO: facets: [&pub.leaflet.richtext.facet] 84 + }, 85 + 86 + #[serde(rename = "pub.leaflet.blocks.unorderedList")] 87 + UnorderedList { children: Vec<ListItem> }, 88 + 89 + #[serde(other)] 90 + Unknown, 91 + } 92 + 93 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 94 + pub struct ListItem { 95 + content: ListItemContent, 96 + } 97 + 98 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 99 + #[serde(tag = "$type")] 100 + pub enum ListItemContent { 101 + #[serde(rename = "pub.leaflet.blocks.text")] 102 + Text { plaintext: String }, 103 + #[serde(rename = "pub.leaflet.blocks.header")] 104 + Header { 105 + level: Option<u32>, 106 + plaintext: String, 107 + }, 108 + #[serde(rename = "pub.leaflet.blocks.image")] 109 + Image { 110 + image: BlobRef, 111 + alt: Option<String>, 112 + #[serde(rename = "aspectRatio")] 113 + aspect_ratio: AspectRatio, 114 + }, 115 + } 116 + 117 + #[derive(Debug, serde::Deserialize, serde::Serialize)] 118 + pub struct Website { 119 + src: String, 120 + description: Option<String>, 121 + title: Option<String>, 122 + #[serde(rename = "previewImage")] 123 + preview_image: BlobRef, 124 + }
+3
src/atproto/types/mod.rs
···
··· 1 + pub mod bsky; 2 + pub mod leaflet; 3 + pub mod tangled;
+10
src/atproto/types/tangled.rs
···
··· 1 + #[derive(serde::Deserialize, serde::Serialize, Clone)] 2 + #[serde(tag = "$type")] 3 + pub struct Repo { 4 + pub knot: String, 5 + pub name: String, 6 + pub labels: Vec<String>, 7 + #[serde(rename = "createdAt")] 8 + pub created_at: String, 9 + pub description: String, 10 + }
+6 -4
src/config/mod.rs
··· 13 pub host: String, 14 } 15 16 - pub fn load_config(path: &PathBuf) -> Result<Config, Box<dyn std::error::Error>> { 17 - let config_file = std::fs::read_to_string(&path)?; 18 - let config: Config = toml::from_str(&config_file).unwrap(); 19 20 - Ok(config) 21 }
··· 13 pub host: String, 14 } 15 16 + impl Config { 17 + pub fn from(path: &PathBuf) -> Result<Config, Box<dyn std::error::Error>> { 18 + let config_file = std::fs::read_to_string(&path)?; 19 + let config: Config = toml::from_str(&config_file).unwrap(); 20 21 + Ok(config) 22 + } 23 }
+5 -2
src/generator.rs
··· 7 use crate::config::Config; 8 use crate::templates::filters; 9 10 - use crate::atproto::{ 11 - BskyPost, ContentPage, ContentType, Document, IndexPage, Renderable, fetch_records, 12 }; 13 14 use std::path::{Path, PathBuf}; 15
··· 7 use crate::config::Config; 8 use crate::templates::filters; 9 10 + use crate::atproto::types::{ 11 + bsky::Post, 12 + leaflet::Document, 13 + tangled::{Issue, Repo}, 14 }; 15 + use crate::atproto::{ContentPage, ContentType, IndexPage, Renderable, fetch_records}; 16 17 use std::path::{Path, PathBuf}; 18
+46
src/init.rs
···
··· 1 + use std::{ 2 + fs::{self, create_dir}, 3 + path::Path, 4 + }; 5 + 6 + pub fn init() -> Result<(), Box<dyn std::error::Error>> { 7 + if Path::new("config.toml").exists() { 8 + return Err("Configuration file already exists, exiting.".into()); 9 + } else { 10 + let config_contents = r#"[pds] 11 + username = "your-handle.bsky.social" 12 + password = "your-app-password" 13 + host = "https://bsky.social" 14 + "#; 15 + fs::write("config.toml", config_contents)?; 16 + } 17 + 18 + match create_dir("templates") { 19 + Ok(_) => println!("Created 'templates' directory"), 20 + Err(_) => { 21 + return Err( 22 + "Could not create 'templates' directory. Are you in an empty folder?".into(), 23 + ); 24 + } 25 + } 26 + 27 + let template_base = include_str!("../templates/base.html"); 28 + std::fs::write("templates/base.html", template_base)?; 29 + 30 + let template_home = include_str!("../templates/home.html"); 31 + std::fs::write("templates/home.html", template_home)?; 32 + 33 + let template_document = include_str!("../templates/document.html"); 34 + std::fs::write("templates/document.html", template_document)?; 35 + 36 + let template_post = include_str!("../templates/post.html"); 37 + std::fs::write("templates/post.html", template_post)?; 38 + 39 + let template_index = include_str!("../templates/index.html"); 40 + std::fs::write("templates/index.html", template_index)?; 41 + 42 + let template_macros = include_str!("../templates/macros.html"); 43 + std::fs::write("templates/macros.html", template_macros)?; 44 + 45 + Ok(()) 46 + }
+6 -6
src/main.rs
··· 1 use std::path::PathBuf; 2 3 - mod generator; 4 - use generator::generate; 5 mod config; 6 - use config::load_config; 7 mod atproto; 8 mod server; 9 mod templates; 10 ··· 30 #[tokio::main] 31 async fn main() -> Result<(), Box<dyn std::error::Error>> { 32 let cli = Cli::parse(); 33 - let config = load_config(&PathBuf::from("config.toml"))?; 34 35 match &cli.command { 36 Commands::Init => { 37 - println!("init called"); 38 } 39 Commands::Build => { 40 - generate(&config).await?; 41 } 42 Commands::Serve => { 43 server::serve(&config)?;
··· 1 use std::path::PathBuf; 2 3 mod config; 4 + mod generator; 5 + use config::Config; 6 mod atproto; 7 + mod init; 8 mod server; 9 mod templates; 10 ··· 30 #[tokio::main] 31 async fn main() -> Result<(), Box<dyn std::error::Error>> { 32 let cli = Cli::parse(); 33 + let config = Config::from(&PathBuf::from("config.toml"))?; 34 35 match &cli.command { 36 Commands::Init => { 37 + init::init()?; 38 } 39 Commands::Build => { 40 + generator::generate(&config).await?; 41 } 42 Commands::Serve => { 43 server::serve(&config)?;
+13 -1
templates/base.html
··· 1 <DOCTYPE! html> 2 <html> 3 <head> 4 <title>{% block title %}{% endblock %}</title> 5 </head> 6 <body> 7 - {% block content %}{% endblock %} 8 </body> 9 </html>
··· 1 <DOCTYPE! html> 2 <html> 3 <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <meta name="color-scheme" content="light dark"> 7 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> 8 <title>{% block title %}{% endblock %}</title> 9 </head> 10 <body> 11 + <main class="container"> 12 + <header> 13 + <a href="/">Johannes</a> 14 + <nav> 15 + </nav> 16 + </header> 17 + 18 + {% block content %}{% endblock %} 19 + </main> 20 </body> 21 </html>
-22
templates/partials/pub.leaflet.blocks.html
··· 1 - {% set type = block_w.block["$type"] %} 2 - {% set block = block_w.block %} 3 - 4 - {% if type == "pub.leaflet.blocks.text" %} 5 - <p>{{ block.plaintext }}</p> 6 - {% elif type == "pub.leaflet.blocks.code" %} 7 - <pre><code class="{{ block.language }}">{{ block.plaintext }}</code></pre> 8 - {% elif type == "pub.leaflet.blocks.image" %} 9 - <p><img src="{{ block.image | blob_to_url(host=host, did=did) | safe }}"></img></p> 10 - {% if block.alt %}<small>{{ block.alt }}</small>{% endif %} 11 - {% elif type == "pub.leaflet.blocks.header" %} 12 - {% set level = block.level | default(value="3") %} 13 - <h{{ level }}>{{ block.plaintext }}</h{{ level }}> 14 - {% elif type == "pub.leaflet.blocks.unorderedList" %} 15 - <ul> 16 - {% for child in block.children %} 17 - <li>{{ child.content["$type"] }}</li> 18 - {% endfor %} 19 - </ul> 20 - {% else %} 21 - <p>Unknown block type: {{ type }}</p> 22 - {% endif %}
···

History

2 rounds 0 comments
sign up or login to add to the discussion
2 commits
expand
WIP: add tangled support
add rudimentary tangled support
expand 0 comments
pull request successfully merged
hello.j23n.com submitted #0
2 commits
expand
add init subcommand
WIP: add tangled support
expand 0 comments