+1
Cargo.lock
+1
Cargo.lock
+1
Cargo.toml
+1
Cargo.toml
+43
-4
src/atproto/types/leaflet.rs
+43
-4
src/atproto/types/leaflet.rs
···
26
26
pub block: Block,
27
27
}
28
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
+
29
64
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
30
65
#[serde(tag = "$type")]
31
66
pub enum Block {
32
67
#[serde(rename = "pub.leaflet.blocks.blockquote")]
33
68
Blockquote {
34
69
plaintext: String,
35
-
// TODO facets: [&pub.leaflet.richtext.facet]
70
+
facets: Vec<Facet>,
36
71
},
37
72
38
73
#[serde(rename = "pub.leaflet.blocks.code")]
···
53
88
Header {
54
89
level: Option<u32>,
55
90
plaintext: String,
56
-
// TODO: facets: [&pub.leaflet.richtext.facet
91
+
facets: Vec<Facet>,
57
92
},
58
93
59
94
#[serde(rename = "pub.leaflet.blocks.horizontalRule")]
···
76
111
#[serde(rename = "pub.leaflet.blocks.text")]
77
112
Text {
78
113
plaintext: String,
79
-
// TODO: facets: [&pub.leaflet.richtext.facet]
114
+
facets: Vec<Facet>,
80
115
},
81
116
82
117
#[serde(rename = "pub.leaflet.blocks.unorderedList")]
···
95
130
#[serde(tag = "$type")]
96
131
pub enum ListItemContent {
97
132
#[serde(rename = "pub.leaflet.blocks.text")]
98
-
Text { plaintext: String },
133
+
Text {
134
+
plaintext: String,
135
+
facets: Vec<Facet>,
136
+
},
99
137
#[serde(rename = "pub.leaflet.blocks.header")]
100
138
Header {
101
139
level: Option<u32>,
102
140
plaintext: String,
141
+
facets: Vec<Facet>,
103
142
},
104
143
#[serde(rename = "pub.leaflet.blocks.image")]
105
144
Image {
+1
src/generator.rs
+1
src/generator.rs
+69
-1
src/templates/filters.rs
+69
-1
src/templates/filters.rs
···
1
+
use crate::atproto::types::leaflet::{Facet, Feature};
1
2
use std::collections::HashMap;
2
-
use tera::{Value, to_value};
3
+
use tera::{Result as TeraResult, Value, to_value};
3
4
4
5
pub fn blob_to_url_filter(value: &Value, args: &HashMap<String, Value>) -> tera::Result<Value> {
5
6
let host = args
···
39
40
40
41
Err(tera::Error::msg("Could not extract CID from BlobRef"))
41
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
+3
-3
templates/macros.html
···
4
4
{%- set block = block %}
5
5
6
6
{%- if type == "pub.leaflet.blocks.text" %}
7
-
<p>{{ block.plaintext }}</p>
7
+
<p>{{ block.plaintext | apply_facets(facets=block.facets) | safe }}</p>
8
8
{%- elif type == "pub.leaflet.blocks.code" %}
9
9
<pre><code class="{{ block.language }}">{{ block.plaintext }}</code></pre>
10
10
{%- elif type == "pub.leaflet.blocks.image" %}
···
12
12
{% if block.alt %}<small>{{ block.alt }}</small>{% endif %}
13
13
{%- elif type == "pub.leaflet.blocks.header" %}
14
14
{%- set level = block.level | default(value="3") %}
15
-
<h{{ level }}>{{ block.plaintext }}</h{{ level }}>
15
+
<h{{ level }}>{{ block.plaintext | apply_facets(facets=block.facets) | safe }}</h{{ level }}>
16
16
{%- elif type == "pub.leaflet.blocks.unorderedList" %}
17
17
<ul>
18
18
{%- for child in block.children %}
···
21
21
</ul>
22
22
{%- elif type == "pub.leaflet.blocks.blockquote" %}
23
23
<blockquote>
24
-
<p>{{ block.plaintext }}</p>
24
+
<p>{{ block.plaintext | apply_facets(facets=block.facets) | safe }}</p>
25
25
</blockquote>
26
26
{%- elif type == "pub.leaflet.blocks.horizontalRule" %}
27
27
<hr />
History
1 round
0 comments
hello.j23n.com
submitted
#0
1 commit
expand
collapse
add leaflet facets support
expand 0 comments
pull request successfully merged