atproto blogging
1//! WASM bindings for weaver-renderer.
2//!
3//! Exposes sync rendering functions to JS/TS apps via wasm-bindgen.
4
5use jacquard::types::string::AtUri;
6use jacquard::types::value::Data;
7use serde::Deserialize;
8use serde_wasm_bindgen::Deserializer;
9use wasm_bindgen::prelude::*;
10
11mod types;
12
13pub use types::*;
14
15/// Initialize panic hook for better error messages in console.
16#[wasm_bindgen(start)]
17pub fn init() {
18 console_error_panic_hook::set_once();
19}
20
21/// Render an AT Protocol record as HTML.
22///
23/// Takes a record URI and the record data (typically fetched from an appview).
24/// Returns the rendered HTML string.
25///
26/// # Arguments
27/// * `at_uri` - The AT Protocol URI (e.g., "at://did:plc:.../app.bsky.feed.post/...")
28/// * `record_json` - The record data as JSON
29/// * `fallback_author` - Optional author profile for records that don't include author info
30/// * `resolved_content` - Optional pre-rendered embed content
31#[wasm_bindgen]
32pub fn render_record(
33 at_uri: &str,
34 record_json: JsValue,
35 fallback_author: Option<JsValue>,
36 resolved_content: Option<JsResolvedContent>,
37) -> Result<String, JsError> {
38 let uri = AtUri::new(at_uri).map_err(|e| JsError::new(&format!("Invalid AT URI: {}", e)))?;
39
40 // Use Deserializer directly to avoid DeserializeOwned bounds (breaks Jacquard types).
41 let deserializer = Deserializer::from(record_json);
42 let data = Data::deserialize(deserializer)
43 .map_err(|e| JsError::new(&format!("Invalid record JSON: {}", e)))?;
44
45 let author: Option<weaver_api::sh_weaver::actor::ProfileDataView<'_>> = fallback_author
46 .map(|v| {
47 let deserializer = Deserializer::from(v);
48 weaver_api::sh_weaver::actor::ProfileDataView::deserialize(deserializer)
49 })
50 .transpose()
51 .map_err(|e| JsError::new(&format!("Invalid author JSON: {}", e)))?;
52
53 let resolved = resolved_content.map(|r| r.into_inner());
54
55 weaver_renderer::atproto::render_record(&uri, &data, author.as_ref(), resolved.as_ref())
56 .map_err(|e| JsError::new(&e.to_string()))
57}
58
59/// Render markdown to HTML.
60///
61/// # Arguments
62/// * `markdown` - The markdown source text
63/// * `resolved_content` - Optional pre-rendered embed content
64#[wasm_bindgen]
65pub fn render_markdown(
66 markdown: &str,
67 resolved_content: Option<JsResolvedContent>,
68) -> Result<String, JsError> {
69 use weaver_renderer::atproto::ClientWriter;
70
71 let resolved = resolved_content.map(|r| r.into_inner()).unwrap_or_default();
72
73 let parser = markdown_weaver::Parser::new_ext(markdown, weaver_renderer::default_md_options())
74 .into_offset_iter();
75 let events: Vec<_> = parser.collect();
76
77 let mut html_buf = String::new();
78 let writer = ClientWriter::new(events.into_iter(), &mut html_buf, markdown)
79 .with_embed_provider(&resolved);
80
81 writer.run().map_err(|_| JsError::new("Render error"))?;
82
83 Ok(html_buf)
84}
85
86/// Render LaTeX math to MathML.
87///
88/// # Arguments
89/// * `latex` - The LaTeX math expression
90/// * `display_mode` - true for display math (block), false for inline math
91#[wasm_bindgen]
92pub fn render_math(latex: &str, display_mode: bool) -> JsMathResult {
93 match weaver_renderer::math::render_math(latex, display_mode) {
94 weaver_renderer::math::MathResult::Success(html) => JsMathResult {
95 success: true,
96 html,
97 error: None,
98 },
99 weaver_renderer::math::MathResult::Error { html, message } => JsMathResult {
100 success: false,
101 html,
102 error: Some(message),
103 },
104 }
105}
106
107/// Render faceted text (rich text with mentions, links, etc.) to HTML.
108///
109/// Accepts facets from several AT Protocol lexicons (app.bsky, pub.leaflet, blog.pckt).
110///
111/// # Arguments
112/// * `text` - The plain text content
113/// * `facets_json` - Array of facets with `index` (byteStart/byteEnd) and `features` array
114#[wasm_bindgen]
115pub fn render_faceted_text(text: &str, facets_json: JsValue) -> Result<String, JsError> {
116 use weaver_renderer::facet::NormalizedFacet;
117
118 let deserializer = Deserializer::from(facets_json);
119 let facets = Vec::<NormalizedFacet<'_>>::deserialize(deserializer)
120 .map_err(|e| JsError::new(&format!("Invalid facets JSON: {}", e)))?;
121
122 weaver_renderer::facet::render_faceted_html(text, &facets)
123 .map_err(|e| JsError::new(&e.to_string()))
124}