tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
js renderer wrapper crate
Orual
1 month ago
5ba1942a
1ce85230
+423
-28
7 changed files
expand all
collapse all
unified
split
Cargo.lock
crates
weaver-renderer
Cargo.toml
src
facet
processor.rs
types.rs
weaver-renderer-js
Cargo.toml
src
lib.rs
types.rs
+45
Cargo.lock
···
11399
11399
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
11400
11400
11401
11401
[[package]]
11402
11402
+
name = "tsify-next"
11403
11403
+
version = "0.5.6"
11404
11404
+
source = "registry+https://github.com/rust-lang/crates.io-index"
11405
11405
+
checksum = "7d0f2208feeb5f7a6edb15a2389c14cd42480ef6417318316bb866da5806a61d"
11406
11406
+
dependencies = [
11407
11407
+
"gloo-utils",
11408
11408
+
"serde",
11409
11409
+
"serde_json",
11410
11410
+
"tsify-next-macros",
11411
11411
+
"wasm-bindgen",
11412
11412
+
]
11413
11413
+
11414
11414
+
[[package]]
11415
11415
+
name = "tsify-next-macros"
11416
11416
+
version = "0.5.6"
11417
11417
+
source = "registry+https://github.com/rust-lang/crates.io-index"
11418
11418
+
checksum = "f81253930d0d388a3ab8fa4ae56da9973ab171ef833d1be2e9080fc3ce502bd6"
11419
11419
+
dependencies = [
11420
11420
+
"proc-macro2",
11421
11421
+
"quote",
11422
11422
+
"serde_derive_internals",
11423
11423
+
"syn 2.0.111",
11424
11424
+
]
11425
11425
+
11426
11426
+
[[package]]
11402
11427
name = "ttf-parser"
11403
11428
version = "0.24.1"
11404
11429
source = "registry+https://github.com/rust-lang/crates.io-index"
···
12350
12375
"regex",
12351
12376
"regex-lite",
12352
12377
"reqwest",
12378
12378
+
"serde",
12379
12379
+
"serde_json",
12353
12380
"smol_str",
12354
12381
"syntect",
12355
12382
"thiserror 2.0.17",
···
12362
12389
"weaver-api",
12363
12390
"weaver-common",
12364
12391
"yaml-rust2",
12392
12392
+
]
12393
12393
+
12394
12394
+
[[package]]
12395
12395
+
name = "weaver-renderer-js"
12396
12396
+
version = "0.1.0"
12397
12397
+
dependencies = [
12398
12398
+
"console_error_panic_hook",
12399
12399
+
"jacquard",
12400
12400
+
"js-sys",
12401
12401
+
"markdown-weaver",
12402
12402
+
"markdown-weaver-escape",
12403
12403
+
"serde",
12404
12404
+
"serde-wasm-bindgen 0.6.5",
12405
12405
+
"tsify-next",
12406
12406
+
"wasm-bindgen",
12407
12407
+
"weaver-api",
12408
12408
+
"weaver-common",
12409
12409
+
"weaver-renderer",
12365
12410
]
12366
12411
12367
12412
[[package]]
+29
crates/weaver-renderer-js/Cargo.toml
···
1
1
+
[package]
2
2
+
name = "weaver-renderer-js"
3
3
+
version.workspace = true
4
4
+
edition.workspace = true
5
5
+
license.workspace = true
6
6
+
authors.workspace = true
7
7
+
description = "WASM bindings for weaver-renderer"
8
8
+
9
9
+
[lib]
10
10
+
crate-type = ["cdylib", "rlib"]
11
11
+
12
12
+
[dependencies]
13
13
+
weaver-renderer = { path = "../weaver-renderer" }
14
14
+
weaver-common = { path = "../weaver-common" }
15
15
+
weaver-api = { path = "../weaver-api" }
16
16
+
jacquard = { workspace = true }
17
17
+
markdown-weaver = { workspace = true }
18
18
+
markdown-weaver-escape = { workspace = true }
19
19
+
20
20
+
wasm-bindgen = "0.2"
21
21
+
serde = { workspace = true }
22
22
+
serde-wasm-bindgen = "0.6"
23
23
+
tsify-next = "0.5"
24
24
+
js-sys = "0.3"
25
25
+
console_error_panic_hook = "0.1"
26
26
+
27
27
+
[profile.release]
28
28
+
lto = true
29
29
+
opt-level = "z"
+124
crates/weaver-renderer-js/src/lib.rs
···
1
1
+
//! WASM bindings for weaver-renderer.
2
2
+
//!
3
3
+
//! Exposes sync rendering functions to JS/TS apps via wasm-bindgen.
4
4
+
5
5
+
use jacquard::types::string::AtUri;
6
6
+
use jacquard::types::value::Data;
7
7
+
use serde::Deserialize;
8
8
+
use serde_wasm_bindgen::Deserializer;
9
9
+
use wasm_bindgen::prelude::*;
10
10
+
11
11
+
mod types;
12
12
+
13
13
+
pub use types::*;
14
14
+
15
15
+
/// Initialize panic hook for better error messages in console.
16
16
+
#[wasm_bindgen(start)]
17
17
+
pub fn init() {
18
18
+
console_error_panic_hook::set_once();
19
19
+
}
20
20
+
21
21
+
/// Render an AT Protocol record as HTML.
22
22
+
///
23
23
+
/// Takes a record URI and the record data (typically fetched from an appview).
24
24
+
/// Returns the rendered HTML string.
25
25
+
///
26
26
+
/// # Arguments
27
27
+
/// * `at_uri` - The AT Protocol URI (e.g., "at://did:plc:.../app.bsky.feed.post/...")
28
28
+
/// * `record_json` - The record data as JSON
29
29
+
/// * `fallback_author` - Optional author profile for records that don't include author info
30
30
+
/// * `resolved_content` - Optional pre-rendered embed content
31
31
+
#[wasm_bindgen]
32
32
+
pub fn render_record(
33
33
+
at_uri: &str,
34
34
+
record_json: JsValue,
35
35
+
fallback_author: Option<JsValue>,
36
36
+
resolved_content: Option<JsResolvedContent>,
37
37
+
) -> Result<String, JsError> {
38
38
+
let uri = AtUri::new(at_uri).map_err(|e| JsError::new(&format!("Invalid AT URI: {}", e)))?;
39
39
+
40
40
+
// Use Deserializer directly to avoid DeserializeOwned bounds (breaks Jacquard types).
41
41
+
let deserializer = Deserializer::from(record_json);
42
42
+
let data = Data::deserialize(deserializer)
43
43
+
.map_err(|e| JsError::new(&format!("Invalid record JSON: {}", e)))?;
44
44
+
45
45
+
let author: Option<weaver_api::sh_weaver::actor::ProfileDataView<'_>> = fallback_author
46
46
+
.map(|v| {
47
47
+
let deserializer = Deserializer::from(v);
48
48
+
weaver_api::sh_weaver::actor::ProfileDataView::deserialize(deserializer)
49
49
+
})
50
50
+
.transpose()
51
51
+
.map_err(|e| JsError::new(&format!("Invalid author JSON: {}", e)))?;
52
52
+
53
53
+
let resolved = resolved_content.map(|r| r.into_inner());
54
54
+
55
55
+
weaver_renderer::atproto::render_record(&uri, &data, author.as_ref(), resolved.as_ref())
56
56
+
.map_err(|e| JsError::new(&e.to_string()))
57
57
+
}
58
58
+
59
59
+
/// Render markdown to HTML.
60
60
+
///
61
61
+
/// # Arguments
62
62
+
/// * `markdown` - The markdown source text
63
63
+
/// * `resolved_content` - Optional pre-rendered embed content
64
64
+
#[wasm_bindgen]
65
65
+
pub fn render_markdown(
66
66
+
markdown: &str,
67
67
+
resolved_content: Option<JsResolvedContent>,
68
68
+
) -> Result<String, JsError> {
69
69
+
use weaver_renderer::atproto::ClientWriter;
70
70
+
71
71
+
let resolved = resolved_content.map(|r| r.into_inner()).unwrap_or_default();
72
72
+
73
73
+
let parser = markdown_weaver::Parser::new_ext(markdown, weaver_renderer::default_md_options())
74
74
+
.into_offset_iter();
75
75
+
let events: Vec<_> = parser.collect();
76
76
+
77
77
+
let mut html_buf = String::new();
78
78
+
let writer = ClientWriter::new(events.into_iter(), &mut html_buf, markdown)
79
79
+
.with_embed_provider(&resolved);
80
80
+
81
81
+
writer.run().map_err(|_| JsError::new("Render error"))?;
82
82
+
83
83
+
Ok(html_buf)
84
84
+
}
85
85
+
86
86
+
/// Render LaTeX math to MathML.
87
87
+
///
88
88
+
/// # Arguments
89
89
+
/// * `latex` - The LaTeX math expression
90
90
+
/// * `display_mode` - true for display math (block), false for inline math
91
91
+
#[wasm_bindgen]
92
92
+
pub fn render_math(latex: &str, display_mode: bool) -> JsMathResult {
93
93
+
match weaver_renderer::math::render_math(latex, display_mode) {
94
94
+
weaver_renderer::math::MathResult::Success(html) => JsMathResult {
95
95
+
success: true,
96
96
+
html,
97
97
+
error: None,
98
98
+
},
99
99
+
weaver_renderer::math::MathResult::Error { html, message } => JsMathResult {
100
100
+
success: false,
101
101
+
html,
102
102
+
error: Some(message),
103
103
+
},
104
104
+
}
105
105
+
}
106
106
+
107
107
+
/// Render faceted text (rich text with mentions, links, etc.) to HTML.
108
108
+
///
109
109
+
/// Accepts facets from several AT Protocol lexicons (app.bsky, pub.leaflet, blog.pckt).
110
110
+
///
111
111
+
/// # Arguments
112
112
+
/// * `text` - The plain text content
113
113
+
/// * `facets_json` - Array of facets with `index` (byteStart/byteEnd) and `features` array
114
114
+
#[wasm_bindgen]
115
115
+
pub fn render_faceted_text(text: &str, facets_json: JsValue) -> Result<String, JsError> {
116
116
+
use weaver_renderer::facet::NormalizedFacet;
117
117
+
118
118
+
let deserializer = Deserializer::from(facets_json);
119
119
+
let facets = Vec::<NormalizedFacet<'_>>::deserialize(deserializer)
120
120
+
.map_err(|e| JsError::new(&format!("Invalid facets JSON: {}", e)))?;
121
121
+
122
122
+
weaver_renderer::facet::render_faceted_html(text, &facets)
123
123
+
.map_err(|e| JsError::new(&e.to_string()))
124
124
+
}
+73
crates/weaver-renderer-js/src/types.rs
···
1
1
+
//! Types exposed to JavaScript via wasm-bindgen.
2
2
+
3
3
+
use jacquard::IntoStatic;
4
4
+
use wasm_bindgen::prelude::*;
5
5
+
use weaver_common::ResolvedContent;
6
6
+
7
7
+
/// Result from rendering LaTeX math.
8
8
+
#[wasm_bindgen]
9
9
+
pub struct JsMathResult {
10
10
+
pub success: bool,
11
11
+
#[wasm_bindgen(getter_with_clone)]
12
12
+
pub html: String,
13
13
+
#[wasm_bindgen(getter_with_clone)]
14
14
+
pub error: Option<String>,
15
15
+
}
16
16
+
17
17
+
/// Pre-rendered embed content for synchronous rendering.
18
18
+
///
19
19
+
/// Build this by calling `create_resolved_content()` and adding embeds
20
20
+
/// with `resolved_content_add_embed()`.
21
21
+
#[wasm_bindgen]
22
22
+
pub struct JsResolvedContent {
23
23
+
inner: ResolvedContent,
24
24
+
}
25
25
+
26
26
+
#[wasm_bindgen]
27
27
+
impl JsResolvedContent {
28
28
+
/// Create an empty resolved content container.
29
29
+
#[wasm_bindgen(constructor)]
30
30
+
pub fn new() -> Self {
31
31
+
Self {
32
32
+
inner: ResolvedContent::new(),
33
33
+
}
34
34
+
}
35
35
+
36
36
+
/// Add pre-rendered embed HTML for an AT URI.
37
37
+
///
38
38
+
/// # Arguments
39
39
+
/// * `at_uri` - The AT Protocol URI (e.g., "at://did:plc:.../app.bsky.feed.post/...")
40
40
+
/// * `html` - The pre-rendered HTML for this embed
41
41
+
#[wasm_bindgen(js_name = addEmbed)]
42
42
+
pub fn add_embed(&mut self, at_uri: &str, html: &str) -> Result<(), JsError> {
43
43
+
use jacquard::types::string::AtUri;
44
44
+
use jacquard::CowStr;
45
45
+
46
46
+
let uri = AtUri::new(at_uri)
47
47
+
.map_err(|e| JsError::new(&format!("Invalid AT URI: {}", e)))?
48
48
+
.into_static();
49
49
+
50
50
+
self.inner.add_embed(uri, CowStr::from(html.to_string()), None);
51
51
+
Ok(())
52
52
+
}
53
53
+
}
54
54
+
55
55
+
impl JsResolvedContent {
56
56
+
pub fn into_inner(self) -> ResolvedContent {
57
57
+
self.inner
58
58
+
}
59
59
+
}
60
60
+
61
61
+
impl Default for JsResolvedContent {
62
62
+
fn default() -> Self {
63
63
+
Self::new()
64
64
+
}
65
65
+
}
66
66
+
67
67
+
/// Create an empty resolved content container.
68
68
+
///
69
69
+
/// Use this to pre-render embeds before calling render functions.
70
70
+
#[wasm_bindgen]
71
71
+
pub fn create_resolved_content() -> JsResolvedContent {
72
72
+
JsResolvedContent::new()
73
73
+
}
+2
crates/weaver-renderer/Cargo.toml
···
14
14
weaver-common = { path = "../weaver-common" }
15
15
weaver-api = { path = "../weaver-api" }
16
16
jacquard.workspace = true
17
17
+
serde.workspace = true
17
18
markdown-weaver = { workspace = true }
18
19
http = "1.3.1"
19
20
url = "2.5.4"
···
48
49
49
50
[dev-dependencies]
50
51
insta = { version = "1.40", features = ["yaml"] }
52
52
+
serde_json = "1.0"
+7
-7
crates/weaver-renderer/src/facet/processor.rs
···
68
68
let mut events: Vec<FacetEvent<'a>> = Vec::new();
69
69
70
70
for (idx, facet) in facets.iter().enumerate() {
71
71
-
if facet.range.is_empty() {
71
71
+
if facet.index.is_empty() {
72
72
continue;
73
73
}
74
74
for feature in &facet.features {
75
75
-
events.push(FacetEvent::start(facet.range.start, feature.clone(), idx));
76
76
-
events.push(FacetEvent::end(facet.range.end, feature.clone(), idx));
75
75
+
events.push(FacetEvent::start(facet.index.start(), feature.clone(), idx));
76
76
+
events.push(FacetEvent::end(facet.index.end(), feature.clone(), idx));
77
77
}
78
78
}
79
79
···
190
190
fn test_simple_bold() {
191
191
let text = "hello world";
192
192
let facets = vec![NormalizedFacet {
193
193
-
range: ByteRange::new(0, 5),
193
193
+
index: ByteRange::new(0, 5),
194
194
features: vec![FacetFeature::Bold],
195
195
}];
196
196
···
208
208
let text = "bold and italic just italic";
209
209
let facets = vec![
210
210
NormalizedFacet {
211
211
-
range: ByteRange::new(0, 15),
211
211
+
index: ByteRange::new(0, 15),
212
212
features: vec![FacetFeature::Bold],
213
213
},
214
214
NormalizedFacet {
215
215
-
range: ByteRange::new(5, 27),
215
215
+
index: ByteRange::new(5, 27),
216
216
features: vec![FacetFeature::Italic],
217
217
},
218
218
];
···
242
242
fn test_link_facet() {
243
243
let text = "click here for more";
244
244
let facets = vec![NormalizedFacet {
245
245
-
range: ByteRange::new(6, 10),
245
245
+
index: ByteRange::new(6, 10),
246
246
features: vec![FacetFeature::Link {
247
247
uri: "https://example.com",
248
248
}],
+143
-21
crates/weaver-renderer/src/facet/types.rs
···
1
1
+
use serde::{Deserialize, Serialize};
1
2
use std::ops::Range;
2
3
3
3
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4
4
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5
5
+
#[serde(rename_all = "camelCase")]
4
6
pub struct ByteRange {
5
5
-
pub start: usize,
6
6
-
pub end: usize,
7
7
+
pub byte_start: usize,
8
8
+
pub byte_end: usize,
7
9
}
8
10
9
11
impl ByteRange {
10
12
pub fn new(start: i64, end: i64) -> Self {
11
13
Self {
12
12
-
start: start.max(0) as usize,
13
13
-
end: end.max(0) as usize,
14
14
+
byte_start: start.max(0) as usize,
15
15
+
byte_end: end.max(0) as usize,
14
16
}
15
17
}
16
18
17
19
pub fn to_range(self) -> Range<usize> {
18
18
-
self.start..self.end
20
20
+
self.byte_start..self.byte_end
19
21
}
20
22
21
23
pub fn is_empty(&self) -> bool {
22
22
-
self.start >= self.end
24
24
+
self.byte_start >= self.byte_end
25
25
+
}
26
26
+
27
27
+
pub fn start(&self) -> usize {
28
28
+
self.byte_start
29
29
+
}
30
30
+
31
31
+
pub fn end(&self) -> usize {
32
32
+
self.byte_end
23
33
}
24
34
}
25
35
26
26
-
#[derive(Debug, Clone, PartialEq, Eq)]
36
36
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37
37
+
#[serde(tag = "$type")]
27
38
pub enum FacetFeature<'a> {
39
39
+
#[serde(rename = "pub.leaflet.richtext.facet#bold")]
40
40
+
#[serde(alias = "blog.pckt.richtext.facet#bold")]
28
41
Bold,
42
42
+
#[serde(rename = "pub.leaflet.richtext.facet#italic")]
43
43
+
#[serde(alias = "blog.pckt.richtext.facet#italic")]
29
44
Italic,
45
45
+
#[serde(rename = "pub.leaflet.richtext.facet#code")]
46
46
+
#[serde(alias = "blog.pckt.richtext.facet#code")]
30
47
Code,
48
48
+
#[serde(rename = "pub.leaflet.richtext.facet#underline")]
49
49
+
#[serde(alias = "blog.pckt.richtext.facet#underline")]
31
50
Underline,
51
51
+
#[serde(rename = "pub.leaflet.richtext.facet#strikethrough")]
52
52
+
#[serde(alias = "blog.pckt.richtext.facet#strikethrough")]
32
53
Strikethrough,
54
54
+
#[serde(rename = "pub.leaflet.richtext.facet#highlight")]
55
55
+
#[serde(alias = "blog.pckt.richtext.facet#highlight")]
33
56
Highlight,
34
34
-
Link { uri: &'a str },
35
35
-
DidMention { did: &'a str },
36
36
-
AtMention { at_uri: &'a str },
37
37
-
Tag { tag: &'a str },
38
38
-
Id { id: Option<&'a str> },
57
57
+
#[serde(rename = "pub.leaflet.richtext.facet#link")]
58
58
+
#[serde(alias = "blog.pckt.richtext.facet#link")]
59
59
+
#[serde(alias = "app.bsky.richtext.facet#link")]
60
60
+
Link {
61
61
+
#[serde(borrow)]
62
62
+
uri: &'a str,
63
63
+
},
64
64
+
#[serde(rename = "pub.leaflet.richtext.facet#didMention")]
65
65
+
#[serde(alias = "blog.pckt.richtext.facet#didMention")]
66
66
+
#[serde(alias = "app.bsky.richtext.facet#mention")]
67
67
+
DidMention {
68
68
+
#[serde(borrow)]
69
69
+
did: &'a str,
70
70
+
},
71
71
+
#[serde(rename = "pub.leaflet.richtext.facet#atMention")]
72
72
+
#[serde(alias = "blog.pckt.richtext.facet#atMention")]
73
73
+
AtMention {
74
74
+
#[serde(borrow, rename = "atUri")]
75
75
+
at_uri: &'a str,
76
76
+
},
77
77
+
#[serde(rename = "app.bsky.richtext.facet#tag")]
78
78
+
Tag {
79
79
+
#[serde(borrow)]
80
80
+
tag: &'a str,
81
81
+
},
82
82
+
#[serde(rename = "pub.leaflet.richtext.facet#id")]
83
83
+
#[serde(alias = "blog.pckt.richtext.facet#id")]
84
84
+
Id {
85
85
+
#[serde(borrow)]
86
86
+
id: Option<&'a str>,
87
87
+
},
39
88
}
40
89
41
41
-
#[derive(Debug, Clone)]
90
90
+
#[derive(Debug, Clone, Serialize, Deserialize)]
42
91
pub struct NormalizedFacet<'a> {
43
43
-
pub range: ByteRange,
92
92
+
pub index: ByteRange,
93
93
+
#[serde(borrow)]
44
94
pub features: Vec<FacetFeature<'a>>,
45
95
}
46
96
···
48
98
fn from(facet: &'a weaver_api::app_bsky::richtext::facet::Facet<'a>) -> Self {
49
99
use weaver_api::app_bsky::richtext::facet::FacetFeaturesItem;
50
100
51
51
-
let range = ByteRange::new(facet.index.byte_start, facet.index.byte_end);
101
101
+
let index = ByteRange::new(facet.index.byte_start, facet.index.byte_end);
52
102
53
103
let features = facet
54
104
.features
···
67
117
})
68
118
.collect();
69
119
70
70
-
Self { range, features }
120
120
+
Self { index, features }
71
121
}
72
122
}
73
123
···
75
125
fn from(facet: &'a weaver_api::pub_leaflet::richtext::facet::Facet<'a>) -> Self {
76
126
use weaver_api::pub_leaflet::richtext::facet::FacetFeaturesItem;
77
127
78
78
-
let range = ByteRange::new(facet.index.byte_start, facet.index.byte_end);
128
128
+
let index = ByteRange::new(facet.index.byte_start, facet.index.byte_end);
79
129
80
130
let features = facet
81
131
.features
···
103
153
})
104
154
.collect();
105
155
106
106
-
Self { range, features }
156
156
+
Self { index, features }
107
157
}
108
158
}
109
159
···
111
161
fn from(facet: &'a weaver_api::blog_pckt::richtext::facet::Facet<'a>) -> Self {
112
162
use weaver_api::blog_pckt::richtext::facet::FacetFeaturesItem;
113
163
114
114
-
let range = ByteRange::new(facet.index.byte_start, facet.index.byte_end);
164
164
+
let index = ByteRange::new(facet.index.byte_start, facet.index.byte_end);
115
165
116
166
let features = facet
117
167
.features
···
139
189
})
140
190
.collect();
141
191
142
142
-
Self { range, features }
192
192
+
Self { index, features }
193
193
+
}
194
194
+
}
195
195
+
196
196
+
#[cfg(test)]
197
197
+
mod tests {
198
198
+
use super::*;
199
199
+
200
200
+
#[test]
201
201
+
fn test_deserialize_leaflet_facet() {
202
202
+
let json = r#"{
203
203
+
"index": {"byteStart": 0, "byteEnd": 5},
204
204
+
"features": [
205
205
+
{"$type": "pub.leaflet.richtext.facet#bold"},
206
206
+
{"$type": "pub.leaflet.richtext.facet#italic"}
207
207
+
]
208
208
+
}"#;
209
209
+
210
210
+
let facet: NormalizedFacet = serde_json::from_str(json).unwrap();
211
211
+
assert_eq!(facet.index.byte_start, 0);
212
212
+
assert_eq!(facet.index.byte_end, 5);
213
213
+
assert_eq!(facet.features.len(), 2);
214
214
+
assert!(matches!(facet.features[0], FacetFeature::Bold));
215
215
+
assert!(matches!(facet.features[1], FacetFeature::Italic));
216
216
+
}
217
217
+
218
218
+
#[test]
219
219
+
fn test_deserialize_bsky_facet() {
220
220
+
let json = r#"{
221
221
+
"index": {"byteStart": 0, "byteEnd": 10},
222
222
+
"features": [
223
223
+
{"$type": "app.bsky.richtext.facet#link", "uri": "https://example.com"},
224
224
+
{"$type": "app.bsky.richtext.facet#mention", "did": "did:plc:abc123"}
225
225
+
]
226
226
+
}"#;
227
227
+
228
228
+
let facet: NormalizedFacet = serde_json::from_str(json).unwrap();
229
229
+
assert_eq!(facet.index.byte_start, 0);
230
230
+
assert_eq!(facet.index.byte_end, 10);
231
231
+
assert_eq!(facet.features.len(), 2);
232
232
+
assert!(matches!(facet.features[0], FacetFeature::Link { uri: "https://example.com" }));
233
233
+
assert!(matches!(facet.features[1], FacetFeature::DidMention { did: "did:plc:abc123" }));
234
234
+
}
235
235
+
236
236
+
#[test]
237
237
+
fn test_deserialize_pckt_facet() {
238
238
+
let json = r#"{
239
239
+
"index": {"byteStart": 5, "byteEnd": 15},
240
240
+
"features": [
241
241
+
{"$type": "blog.pckt.richtext.facet#code"},
242
242
+
{"$type": "blog.pckt.richtext.facet#strikethrough"}
243
243
+
]
244
244
+
}"#;
245
245
+
246
246
+
let facet: NormalizedFacet = serde_json::from_str(json).unwrap();
247
247
+
assert_eq!(facet.index.byte_start, 5);
248
248
+
assert_eq!(facet.index.byte_end, 15);
249
249
+
assert_eq!(facet.features.len(), 2);
250
250
+
assert!(matches!(facet.features[0], FacetFeature::Code));
251
251
+
assert!(matches!(facet.features[1], FacetFeature::Strikethrough));
252
252
+
}
253
253
+
254
254
+
#[test]
255
255
+
fn test_deserialize_tag_facet() {
256
256
+
let json = r#"{
257
257
+
"index": {"byteStart": 0, "byteEnd": 8},
258
258
+
"features": [
259
259
+
{"$type": "app.bsky.richtext.facet#tag", "tag": "rust"}
260
260
+
]
261
261
+
}"#;
262
262
+
263
263
+
let facet: NormalizedFacet = serde_json::from_str(json).unwrap();
264
264
+
assert!(matches!(facet.features[0], FacetFeature::Tag { tag: "rust" }));
143
265
}
144
266
}