use std::fmt::Write;
use jacquard::types::string::Did;
use weaver_api::pub_leaflet::blocks::{
blockquote::Blockquote,
bsky_post::BskyPost,
button::Button,
code::Code,
header::Header,
iframe::Iframe,
image::Image,
math::Math,
page::Page,
poll::Poll,
text::Text,
unordered_list::{ListItem, ListItemContent, UnorderedList},
website::Website,
};
use weaver_api::pub_leaflet::pages::linear_document::{Block, BlockBlock, LinearDocument};
use crate::facet::{NormalizedFacet, render_faceted_markdown};
pub struct LeafletMarkdownContext {
pub author_did: Did<'static>,
}
impl LeafletMarkdownContext {
pub fn new(author_did: Did<'static>) -> Self {
Self { author_did }
}
fn blob_ref(&self, cid: &jacquard::types::cid::Cid<'_>) -> String {
format!("blob:{}:{}", self.author_did.as_ref(), cid.as_ref())
}
}
pub fn convert_linear_document(doc: &LinearDocument<'_>, ctx: &LeafletMarkdownContext) -> String {
let mut md = String::new();
for block in &doc.blocks {
md.push_str(&convert_block(block, ctx, 0));
}
md
}
pub fn convert_block(block: &Block<'_>, ctx: &LeafletMarkdownContext, indent: usize) -> String {
let mut md = String::new();
match &block.block {
BlockBlock::Text(text) => {
convert_text_block(&mut md, text);
}
BlockBlock::Header(header) => {
convert_header_block(&mut md, header);
}
BlockBlock::Blockquote(quote) => {
convert_blockquote_block(&mut md, quote);
}
BlockBlock::Code(code) => {
convert_code_block(&mut md, code);
}
BlockBlock::UnorderedList(list) => {
convert_unordered_list(&mut md, list, ctx, indent);
}
BlockBlock::Image(image) => {
convert_image_block(&mut md, image, ctx);
}
BlockBlock::Website(website) => {
convert_website_block(&mut md, website);
}
BlockBlock::Iframe(iframe) => {
convert_iframe_block(&mut md, iframe);
}
BlockBlock::BskyPost(post) => {
convert_bsky_post_block(&mut md, post);
}
BlockBlock::Button(button) => {
convert_button_block(&mut md, button);
}
BlockBlock::Poll(poll) => {
convert_poll_block(&mut md, poll);
}
BlockBlock::HorizontalRule(_) => {
md.push_str("---\n\n");
}
BlockBlock::Page(page) => {
convert_page_block(&mut md, page);
}
BlockBlock::Math(math) => {
convert_math_block(&mut md, math);
}
BlockBlock::Unknown(data) => {
let _ = writeln!(md, "", data.type_discriminator());
}
}
md
}
fn convert_text_block(md: &mut String, text: &Text<'_>) {
md.push_str(&render_faceted_text(&text.plaintext, text.facets.as_deref()));
md.push_str("\n\n");
}
fn convert_header_block(md: &mut String, header: &Header<'_>) {
let level = header.level.unwrap_or(1).clamp(1, 6) as usize;
for _ in 0..level {
md.push('#');
}
md.push(' ');
md.push_str(&render_faceted_text(
&header.plaintext,
header.facets.as_deref(),
));
md.push_str("\n\n");
}
fn convert_blockquote_block(md: &mut String, quote: &Blockquote<'_>) {
let text = render_faceted_text("e.plaintext, quote.facets.as_deref());
for line in text.lines() {
md.push_str("> ");
md.push_str(line);
md.push('\n');
}
md.push('\n');
}
fn convert_code_block(md: &mut String, code: &Code<'_>) {
md.push_str("```");
if let Some(lang) = &code.language {
md.push_str(lang.as_ref());
}
md.push('\n');
md.push_str(&code.plaintext);
if !code.plaintext.ends_with('\n') {
md.push('\n');
}
md.push_str("```\n\n");
}
fn convert_unordered_list(
md: &mut String,
list: &UnorderedList<'_>,
ctx: &LeafletMarkdownContext,
indent: usize,
) {
for item in &list.children {
convert_list_item(md, item, ctx, indent);
}
if indent == 0 {
md.push('\n');
}
}
fn convert_list_item(
md: &mut String,
item: &ListItem<'_>,
ctx: &LeafletMarkdownContext,
indent: usize,
) {
let prefix: String = " ".repeat(indent);
md.push_str(&prefix);
md.push_str("- ");
match &item.content {
ListItemContent::Text(text) => {
md.push_str(&render_faceted_text(&text.plaintext, text.facets.as_deref()));
}
ListItemContent::Header(header) => {
md.push_str("**");
md.push_str(&render_faceted_text(
&header.plaintext,
header.facets.as_deref(),
));
md.push_str("**");
}
ListItemContent::Image(image) => {
md.push_str(";
md.push_str(&ctx.blob_ref(image.image.blob().cid()));
md.push(')');
}
ListItemContent::Unknown(data) => {
let _ = write!(md, "", data.type_discriminator());
}
}
md.push('\n');
if let Some(children) = &item.children {
for child in children {
convert_list_item(md, child, ctx, indent + 1);
}
}
}
fn convert_image_block(md: &mut String, image: &Image<'_>, ctx: &LeafletMarkdownContext) {
md.push_str(";
md.push_str(&ctx.blob_ref(image.image.blob().cid()));
md.push_str(")\n\n");
}
fn convert_website_block(md: &mut String, website: &Website<'_>) {
md.push('[');
if let Some(title) = &website.title {
md.push_str(title.as_ref());
} else {
md.push_str(website.src.as_ref());
}
md.push_str("](");
md.push_str(website.src.as_ref());
md.push_str(")\n\n");
}
fn convert_iframe_block(md: &mut String, iframe: &Iframe<'_>) {
let height = iframe.height.unwrap_or(400);
let _ = writeln!(
md,
"\n",
iframe.url.as_ref(),
height
);
}
fn convert_bsky_post_block(md: &mut String, post: &BskyPost<'_>) {
let _ = writeln!(md, "![[{}]]\n", post.post_ref.uri.as_ref());
}
fn convert_button_block(md: &mut String, button: &Button<'_>) {
md.push('[');
md.push_str(button.text.as_ref());
md.push_str("](");
md.push_str(button.url.as_ref());
md.push_str(")\n\n");
}
fn convert_poll_block(md: &mut String, poll: &Poll<'_>) {
md.push_str("> [!poll]\n");
let _ = writeln!(md, "> {}\n", poll.poll_ref.uri.as_ref());
}
fn convert_page_block(md: &mut String, page: &Page<'_>) {
let _ = writeln!(md, "[[{}]]\n", page.id.as_ref());
}
fn convert_math_block(md: &mut String, math: &Math<'_>) {
md.push_str("$$\n");
md.push_str(&math.tex);
if !math.tex.ends_with('\n') {
md.push('\n');
}
md.push_str("$$\n\n");
}
fn render_faceted_text(
text: &str,
facets: Option<&[weaver_api::pub_leaflet::richtext::facet::Facet<'_>]>,
) -> String {
if let Some(facets) = facets {
let normalized: Vec> =
facets.iter().map(NormalizedFacet::from).collect();
render_faceted_markdown(text, &normalized).unwrap_or_else(|_| text.to_string())
} else {
text.to_string()
}
}