+13
-129
src/atproto/mod.rs
+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
src/atproto/types/bsky.rs
···
···
1
+
pub use atrium_api::app::bsky::feed::post::RecordData as Post;
+124
src/atproto/types/leaflet.rs
+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
+
}
+45
src/atproto/types/tangled.rs
+45
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
+
}
11
+
12
+
#[derive(serde::Serialize, serde::Deserialize, Clone)]
13
+
#[serde(tag = "$type")]
14
+
pub struct Issue {
15
+
pub body: String,
16
+
pub title: String,
17
+
pub repo: String,
18
+
#[serde(rename = "createdAt")]
19
+
pub created_at: String,
20
+
}
21
+
22
+
#[derive(serde::Serialize, serde::Deserialize, Clone)]
23
+
#[serde(tag = "$type")]
24
+
pub struct Pull {
25
+
pub patch: String,
26
+
pub title: String,
27
+
pub source: PullSource,
28
+
pub target: PullTarget,
29
+
#[serde(rename = "createdAt")]
30
+
pub created_at: String,
31
+
}
32
+
33
+
#[derive(serde::Serialize, serde::Deserialize, Clone)]
34
+
#[serde(tag = "$type")]
35
+
pub struct PullSource {
36
+
pub sha: String,
37
+
pub branch: String,
38
+
}
39
+
40
+
#[derive(serde::Serialize, serde::Deserialize, Clone)]
41
+
#[serde(tag = "$type")]
42
+
pub struct PullTarget {
43
+
pub repo: String,
44
+
pub branch: String,
45
+
}
+2
-3
src/config/mod.rs
+2
-3
src/config/mod.rs
+56
-26
src/generator.rs
+56
-26
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
16
pub async fn generate(config: &Config) -> Result<usize, Box<dyn std::error::Error>> {
17
let mut tera = match Tera::new("templates/**/*.html") {
···
38
39
println!("Resolved did: {:?}", did);
40
41
-
let doc_records = fetch_records(&agent, did.clone(), "pub.leaflet.document").await?;
42
-
let documents: Vec<Document> = doc_records
43
-
.into_iter()
44
-
.filter_map(|r| match Document::try_from_unknown(r.value.clone()) {
45
-
Ok(mut doc) => {
46
-
doc.host = host.clone();
47
-
doc.did = did.as_ref().to_string();
48
-
Some(doc)
49
-
}
50
-
Err(e) => {
51
-
eprintln!("Failed to deserialize: {:?}", e);
52
-
None
53
-
}
54
-
})
55
-
.collect();
56
57
-
let post_records = fetch_records(&agent, did.clone(), "app.bsky.feed.post").await?;
58
-
let posts: Vec<BskyPost> = post_records
59
-
.into_iter()
60
-
.filter_map(|r| BskyPost::try_from_unknown(r.value.clone()).ok())
61
-
.collect();
62
63
-
let num_generated = _generate(posts, documents, &tera)?;
64
65
println!("Done! Generated {:?} files.", num_generated);
66
67
Ok(num_generated)
68
}
69
70
fn _generate(
71
-
posts: Vec<BskyPost>,
72
documents: Vec<Document>,
73
tera: &Tera,
74
) -> Result<usize, Box<dyn std::error::Error>> {
75
let mut counter = 0;
···
81
// Create ContentPages for content
82
let post_pages: Vec<ContentPage> = posts
83
.into_iter()
84
-
.map(|i| ContentPage::new(ContentType::BskyPost(i), "post.html", "posts"))
85
.collect();
86
87
let document_pages: Vec<ContentPage> = documents
···
89
.map(|i| ContentPage::new(ContentType::Document(i), "document.html", "documents"))
90
.collect();
91
92
// Render individual pages
93
-
for pages in [&document_pages, &post_pages] {
94
for page in pages.iter() {
95
let html = page.render(tera)?;
96
let mut path = PathBuf::from(output_root);
···
107
let document_index = generate_index(&document_pages, output_root, "documents", tera)?;
108
counter += 1;
109
110
let post_index = generate_index(&post_pages, output_root, "posts", tera)?;
111
counter += 1;
112
···
114
generate_home(
115
&(document_index, document_pages),
116
&(post_index, post_pages),
117
output_root,
118
tera,
119
)?;
···
124
fn generate_home(
125
documents: &(IndexPage, Vec<ContentPage>),
126
posts: &(IndexPage, Vec<ContentPage>),
127
output_root: &Path,
128
tera: &Tera,
129
) -> Result<(), tera::Error> {
···
133
let mut context = Context::new();
134
context.insert("documents", documents);
135
context.insert("posts", posts);
136
let html = tera.render("home.html", &context)?;
137
138
std::fs::write(output_path, html)?;
···
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, Pull, Repo},
14
};
15
+
use crate::atproto::{ContentPage, ContentType, IndexPage, Renderable, fetch_records};
16
17
use std::path::{Path, PathBuf};
18
+
19
+
#[derive(serde::Serialize)]
20
+
pub struct Site {
21
+
config: Config,
22
+
did: AtIdentifier,
23
+
host: String,
24
+
}
25
26
pub async fn generate(config: &Config) -> Result<usize, Box<dyn std::error::Error>> {
27
let mut tera = match Tera::new("templates/**/*.html") {
···
48
49
println!("Resolved did: {:?}", did);
50
51
+
let site = Site {
52
+
config: config.clone(),
53
+
did: did.clone(),
54
+
host: host.clone(),
55
+
};
56
57
+
// get and parse records from PDS
58
+
let documents = fetch_and_deserialize(&agent, &did, "pub.leaflet.document").await?;
59
+
let posts = fetch_and_deserialize(&agent, &did, "app.bsky.feed.post").await?;
60
+
let repos = fetch_and_deserialize(&agent, &did, "sh.tangled.repo").await?;
61
+
let issues = fetch_and_deserialize(&agent, &did, "sh.tangled.repo.issue").await?;
62
+
let pulls = fetch_and_deserialize(&agent, &did, "sh.tangled.repo.pull").await?;
63
64
+
// generate static pages
65
+
let num_generated = _generate(site, posts, documents, repos, issues, pulls, &tera)?;
66
67
println!("Done! Generated {:?} files.", num_generated);
68
69
Ok(num_generated)
70
}
71
72
+
async fn fetch_and_deserialize<T: TryFromUnknown>(
73
+
agent: &AtpAgent<MemorySessionStore, ReqwestClient>,
74
+
did: &AtIdentifier,
75
+
record_id: &str,
76
+
) -> Result<Vec<T>, Box<dyn std::error::Error>> {
77
+
let records = fetch_records(&agent, did.clone(), record_id).await?;
78
+
let parsed: Vec<T> = records
79
+
.into_iter()
80
+
.filter_map(|r| T::try_from_unknown(r.value.clone()).ok())
81
+
.collect();
82
+
Ok(parsed)
83
+
}
84
+
85
fn _generate(
86
+
site: Site,
87
+
posts: Vec<Post>,
88
documents: Vec<Document>,
89
+
repos: Vec<Repo>,
90
+
issues: Vec<Issue>,
91
+
pulls: Vec<Pull>,
92
tera: &Tera,
93
) -> Result<usize, Box<dyn std::error::Error>> {
94
let mut counter = 0;
···
100
// Create ContentPages for content
101
let post_pages: Vec<ContentPage> = posts
102
.into_iter()
103
+
.map(|i| ContentPage::new(ContentType::Post(i), "post.html", "posts"))
104
.collect();
105
106
let document_pages: Vec<ContentPage> = documents
···
108
.map(|i| ContentPage::new(ContentType::Document(i), "document.html", "documents"))
109
.collect();
110
111
+
let repo_pages: Vec<ContentPage> = repos
112
+
.into_iter()
113
+
.map(|r| ContentPage::new(ContentType::Repo(r), "repo.html", "repos"))
114
+
.collect();
115
+
116
// Render individual pages
117
+
for pages in [&document_pages, &post_pages, &repo_pages] {
118
for page in pages.iter() {
119
let html = page.render(tera)?;
120
let mut path = PathBuf::from(output_root);
···
131
let document_index = generate_index(&document_pages, output_root, "documents", tera)?;
132
counter += 1;
133
134
+
let repo_index = generate_index(&repo_pages, output_root, "repos", tera)?;
135
+
counter += 1;
136
+
137
let post_index = generate_index(&post_pages, output_root, "posts", tera)?;
138
counter += 1;
139
···
141
generate_home(
142
&(document_index, document_pages),
143
&(post_index, post_pages),
144
+
&(repo_index, repo_pages),
145
output_root,
146
tera,
147
)?;
···
152
fn generate_home(
153
documents: &(IndexPage, Vec<ContentPage>),
154
posts: &(IndexPage, Vec<ContentPage>),
155
+
repos: &(IndexPage, Vec<ContentPage>),
156
output_root: &Path,
157
tera: &Tera,
158
) -> Result<(), tera::Error> {
···
162
let mut context = Context::new();
163
context.insert("documents", documents);
164
context.insert("posts", posts);
165
+
context.insert("repos", repos);
166
let html = tera.render("home.html", &context)?;
167
168
std::fs::write(output_path, html)?;
+12
-1
templates/home.html
+12
-1
templates/home.html
···
3
{% block title %}j23n{% endblock %}
4
5
{% block content %}
6
-
{% include "home-blurb.html" %}
7
8
<section id="documents">
9
{% set index = documents.0 %}
···
12
<ul>
13
{% for document in pages %}
14
<li><a href="{{ document.url | safe }}">{{ document.name }}</a></li>
15
{% endfor %}
16
</ul>
17
</section>
···
3
{% block title %}j23n{% endblock %}
4
5
{% block content %}
6
+
{% include "home-blurb.html" ignore missing %}
7
8
<section id="documents">
9
{% set index = documents.0 %}
···
12
<ul>
13
{% for document in pages %}
14
<li><a href="{{ document.url | safe }}">{{ document.name }}</a></li>
15
+
{% endfor %}
16
+
</ul>
17
+
</section>
18
+
19
+
<section id="repos">
20
+
{% set index = repos.0 %}
21
+
{% set pages = repos.1 %}
22
+
<h2><a href="{{ index.url | safe }}">{{ index.name | title }}</a></h2>
23
+
<ul>
24
+
{% for repo in pages %}
25
+
<li><a href="{{ repo.url | safe }}">{{ repo.name }}</a></li>
26
{% endfor %}
27
</ul>
28
</section>
History
2 rounds
0 comments
hello.j23n.com
submitted
#1
expand 0 comments
pull request successfully merged
hello.j23n.com
submitted
#0