Learning project: static site generator for ATproto PDS

Adds support for leaflet facets #4

merged opened by hello.j23n.com targeting main from feature/leaflet-facets
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:enau5rzvrui4fx4dq5icgtle/sh.tangled.repo.pull/3m3q4rrjyoa22
+118 -8
Diff #0
+1
Cargo.lock
··· 1281 "clap", 1282 "dotenvy", 1283 "serde", 1284 "tera", 1285 "tiny_http", 1286 "tokio",
··· 1281 "clap", 1282 "dotenvy", 1283 "serde", 1284 + "serde_json", 1285 "tera", 1286 "tiny_http", 1287 "tokio",
+1
Cargo.toml
··· 11 clap = { version = "4.5.49", features = ["derive"] } 12 dotenvy = "0.15.7" 13 serde = { version = "1.0.228", features = ["derive"] } 14 tera = "1.20.0" 15 tiny_http = "0.12.0" 16 tokio = { version = "1.47.1", features = ["full"] }
··· 11 clap = { version = "4.5.49", features = ["derive"] } 12 dotenvy = "0.15.7" 13 serde = { version = "1.0.228", features = ["derive"] } 14 + serde_json = "1.0.145" 15 tera = "1.20.0" 16 tiny_http = "0.12.0" 17 tokio = { version = "1.47.1", features = ["full"] }
+43 -4
src/atproto/types/leaflet.rs
··· 26 pub block: Block, 27 } 28 29 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 30 #[serde(tag = "$type")] 31 pub enum Block { 32 #[serde(rename = "pub.leaflet.blocks.blockquote")] 33 Blockquote { 34 plaintext: String, 35 - // TODO facets: [&pub.leaflet.richtext.facet] 36 }, 37 38 #[serde(rename = "pub.leaflet.blocks.code")] ··· 53 Header { 54 level: Option<u32>, 55 plaintext: String, 56 - // TODO: facets: [&pub.leaflet.richtext.facet 57 }, 58 59 #[serde(rename = "pub.leaflet.blocks.horizontalRule")] ··· 76 #[serde(rename = "pub.leaflet.blocks.text")] 77 Text { 78 plaintext: String, 79 - // TODO: facets: [&pub.leaflet.richtext.facet] 80 }, 81 82 #[serde(rename = "pub.leaflet.blocks.unorderedList")] ··· 95 #[serde(tag = "$type")] 96 pub enum ListItemContent { 97 #[serde(rename = "pub.leaflet.blocks.text")] 98 - Text { plaintext: String }, 99 #[serde(rename = "pub.leaflet.blocks.header")] 100 Header { 101 level: Option<u32>, 102 plaintext: String, 103 }, 104 #[serde(rename = "pub.leaflet.blocks.image")] 105 Image {
··· 26 pub block: Block, 27 } 28 29 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 30 + pub struct Facet { 31 + pub index: Index, 32 + pub features: Vec<Feature>, 33 + } 34 + 35 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 36 + pub struct Index { 37 + #[serde(rename = "byteEnd")] 38 + pub byte_end: u32, 39 + #[serde(rename = "byteStart")] 40 + pub byte_start: u32, 41 + } 42 + 43 + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 44 + #[serde(tag = "$type")] 45 + pub enum Feature { 46 + #[serde(rename = "pub.leaflet.richtext.facet#code")] 47 + Code, 48 + #[serde(rename = "pub.leaflet.richtext.facet#highlight")] 49 + Highlight, 50 + #[serde(rename = "pub.leaflet.richtext.facet#link")] 51 + Link { uri: String }, 52 + #[serde(rename = "pub.leaflet.richtext.facet#underline")] 53 + Underline, 54 + #[serde(rename = "pub.leaflet.richtext.facet#strikethrough")] 55 + StrikeThrough, 56 + #[serde(rename = "pub.leaflet.richtext.facet#id")] 57 + Id { id: String }, 58 + #[serde(rename = "pub.leaflet.richtext.facet#bold")] 59 + Bold, 60 + #[serde(rename = "pub.leaflet.richtext.facet#italic")] 61 + Italic, 62 + } 63 + 64 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 65 #[serde(tag = "$type")] 66 pub enum Block { 67 #[serde(rename = "pub.leaflet.blocks.blockquote")] 68 Blockquote { 69 plaintext: String, 70 + facets: Vec<Facet>, 71 }, 72 73 #[serde(rename = "pub.leaflet.blocks.code")] ··· 88 Header { 89 level: Option<u32>, 90 plaintext: String, 91 + facets: Vec<Facet>, 92 }, 93 94 #[serde(rename = "pub.leaflet.blocks.horizontalRule")] ··· 111 #[serde(rename = "pub.leaflet.blocks.text")] 112 Text { 113 plaintext: String, 114 + facets: Vec<Facet>, 115 }, 116 117 #[serde(rename = "pub.leaflet.blocks.unorderedList")] ··· 130 #[serde(tag = "$type")] 131 pub enum ListItemContent { 132 #[serde(rename = "pub.leaflet.blocks.text")] 133 + Text { 134 + plaintext: String, 135 + facets: Vec<Facet>, 136 + }, 137 #[serde(rename = "pub.leaflet.blocks.header")] 138 Header { 139 level: Option<u32>, 140 plaintext: String, 141 + facets: Vec<Facet>, 142 }, 143 #[serde(rename = "pub.leaflet.blocks.image")] 144 Image {
+1
src/generator.rs
··· 48 Some("home-blurb.html"), 49 )])?; 50 tera.register_filter("blob_to_url", filters::blob_to_url_filter); 51 52 let username = config.pds.username.clone(); 53 let password = config.pds.password.clone();
··· 48 Some("home-blurb.html"), 49 )])?; 50 tera.register_filter("blob_to_url", filters::blob_to_url_filter); 51 + tera.register_filter("apply_facets", filters::apply_facets); 52 53 let username = config.pds.username.clone(); 54 let password = config.pds.password.clone();
+69 -1
src/templates/filters.rs
··· 1 use std::collections::HashMap; 2 - use tera::{Value, to_value}; 3 4 pub fn blob_to_url_filter(value: &Value, args: &HashMap<String, Value>) -> tera::Result<Value> { 5 let host = args ··· 39 40 Err(tera::Error::msg("Could not extract CID from BlobRef")) 41 }
··· 1 + use crate::atproto::types::leaflet::{Facet, Feature}; 2 use std::collections::HashMap; 3 + use tera::{Result as TeraResult, Value, to_value}; 4 5 pub fn blob_to_url_filter(value: &Value, args: &HashMap<String, Value>) -> tera::Result<Value> { 6 let host = args ··· 40 41 Err(tera::Error::msg("Could not extract CID from BlobRef")) 42 } 43 + 44 + pub fn apply_facets(value: &Value, args: &HashMap<String, Value>) -> TeraResult<Value> { 45 + let plaintext = value.as_str().unwrap(); 46 + 47 + let facets_value = args 48 + .get("facets") 49 + .ok_or_else(|| tera::Error::msg("apply_facets: missing 'facets' argument"))?; 50 + 51 + let facets: Vec<Facet> = serde_json::from_value(facets_value.clone()) 52 + .map_err(|e| tera::Error::msg(format!("Failed to parse facets: {}", e)))?; 53 + 54 + let html = render_text_with_facets(plaintext, facets); 55 + 56 + Ok(tera::Value::String(html)) 57 + } 58 + 59 + fn render_text_with_facets(plaintext: &str, facets: Vec<Facet>) -> String { 60 + let mut tokens: Vec<(u32, String)> = facets 61 + .iter() 62 + .flat_map(|f| { 63 + f.features.iter().flat_map(move |feat| { 64 + let token_close = match feat { 65 + Feature::Code => String::from("</code>"), 66 + Feature::Highlight => String::from("</mark>"), 67 + Feature::Link { uri } => String::from("</a>"), 68 + Feature::Underline => String::from("</u>"), 69 + Feature::StrikeThrough => String::from("</s>"), 70 + Feature::Id { id } => String::from("</a>"), 71 + Feature::Bold => String::from("</b>"), 72 + Feature::Italic => String::from("</i>"), 73 + }; 74 + let token_open = match feat { 75 + Feature::Code => String::from("<code>"), 76 + Feature::Highlight => String::from("<mark>"), 77 + Feature::Link { uri } => format!("<a href='{}'>", uri), 78 + Feature::Underline => String::from("<u>"), 79 + Feature::StrikeThrough => String::from("<s>"), 80 + Feature::Id { id } => format!("<a href='{}'>", id), 81 + Feature::Bold => String::from("<b>"), 82 + Feature::Italic => String::from("<i>"), 83 + }; 84 + [ 85 + (f.index.byte_end, token_close), 86 + (f.index.byte_start, token_open), 87 + ] 88 + }) 89 + }) 90 + .collect(); 91 + 92 + let mut rendered_html = String::from(plaintext); 93 + 94 + // it's possible for one facet to have a startByte equal to the endByte 95 + // of another facet. To avoid things like <b>asdf<i></b>, we need to 96 + // sort again. 97 + tokens.sort_by(|a, b| match b.0.cmp(&a.0) { 98 + std::cmp::Ordering::Equal => { 99 + let a_is_closing = a.1.starts_with("</"); 100 + let b_is_closing = b.1.starts_with("</"); 101 + b_is_closing.cmp(&a_is_closing).reverse() 102 + } 103 + other => other, 104 + }); 105 + tokens 106 + .iter() 107 + .for_each(|pair| rendered_html.insert_str(pair.0 as usize, &pair.1)); 108 + rendered_html 109 + }
+3 -3
templates/macros.html
··· 4 {%- set block = block %} 5 6 {%- if type == "pub.leaflet.blocks.text" %} 7 - <p>{{ block.plaintext }}</p> 8 {%- elif type == "pub.leaflet.blocks.code" %} 9 <pre><code class="{{ block.language }}">{{ block.plaintext }}</code></pre> 10 {%- elif type == "pub.leaflet.blocks.image" %} ··· 12 {% if block.alt %}<small>{{ block.alt }}</small>{% endif %} 13 {%- elif type == "pub.leaflet.blocks.header" %} 14 {%- set level = block.level | default(value="3") %} 15 - <h{{ level }}>{{ block.plaintext }}</h{{ level }}> 16 {%- elif type == "pub.leaflet.blocks.unorderedList" %} 17 <ul> 18 {%- for child in block.children %} ··· 21 </ul> 22 {%- elif type == "pub.leaflet.blocks.blockquote" %} 23 <blockquote> 24 - <p>{{ block.plaintext }}</p> 25 </blockquote> 26 {%- elif type == "pub.leaflet.blocks.horizontalRule" %} 27 <hr />
··· 4 {%- set block = block %} 5 6 {%- if type == "pub.leaflet.blocks.text" %} 7 + <p>{{ block.plaintext | apply_facets(facets=block.facets) | safe }}</p> 8 {%- elif type == "pub.leaflet.blocks.code" %} 9 <pre><code class="{{ block.language }}">{{ block.plaintext }}</code></pre> 10 {%- elif type == "pub.leaflet.blocks.image" %} ··· 12 {% if block.alt %}<small>{{ block.alt }}</small>{% endif %} 13 {%- elif type == "pub.leaflet.blocks.header" %} 14 {%- set level = block.level | default(value="3") %} 15 + <h{{ level }}>{{ block.plaintext | apply_facets(facets=block.facets) | safe }}</h{{ level }}> 16 {%- elif type == "pub.leaflet.blocks.unorderedList" %} 17 <ul> 18 {%- for child in block.children %} ··· 21 </ul> 22 {%- elif type == "pub.leaflet.blocks.blockquote" %} 23 <blockquote> 24 + <p>{{ block.plaintext | apply_facets(facets=block.facets) | safe }}</p> 25 </blockquote> 26 {%- elif type == "pub.leaflet.blocks.horizontalRule" %} 27 <hr />

History

1 round 0 comments
sign up or login to add to the discussion
hello.j23n.com submitted #0
1 commit
expand
add leaflet facets support
expand 0 comments
pull request successfully merged