From 0d826fe5d928b490b4643406bda914d7062b16e8 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Tue, 2 Jan 2024 09:13:09 -0800 Subject: [PATCH] initial html note renderer --- src/abbrev.rs | 36 +++++++++++ src/html.rs | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 78 +----------------------- src/render.rs | 38 +----------- 4 files changed, 207 insertions(+), 110 deletions(-) create mode 100644 src/abbrev.rs create mode 100644 src/html.rs diff --git a/src/abbrev.rs b/src/abbrev.rs new file mode 100644 index 0000000..1f7d5e6 --- /dev/null +++ b/src/abbrev.rs @@ -0,0 +1,36 @@ +#[inline] +fn floor_char_boundary(s: &str, index: usize) -> usize { + if index >= s.len() { + s.len() + } else { + let lower_bound = index.saturating_sub(3); + let new_index = s.as_bytes()[lower_bound..=index] + .iter() + .rposition(|b| is_utf8_char_boundary(*b)); + + // SAFETY: we know that the character boundary will be within four bytes + unsafe { lower_bound + new_index.unwrap_unchecked() } + } +} + +#[inline] +fn is_utf8_char_boundary(c: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (c as i8) >= -0x40 +} + +const ABBREV_SIZE: usize = 10; + +pub fn abbrev_str(name: &str) -> String { + if name.len() > ABBREV_SIZE { + let closest = floor_char_boundary(name, ABBREV_SIZE); + format!("{}...", &name[..closest]) + } else { + name.to_owned() + } +} + +pub fn abbreviate<'a>(text: &'a str, len: usize) -> &'a str { + let closest = floor_char_boundary(text, len); + &text[..closest] +} diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..46a081c --- /dev/null +++ b/src/html.rs @@ -0,0 +1,165 @@ +use crate::Error; +use crate::{ + abbrev::{abbrev_str, abbreviate}, + render, Notecrumbs, +}; +use html_escape; +use http_body_util::Full; +use hyper::{ + body::Bytes, header, server::conn::http1, service::service_fn, Request, Response, StatusCode, +}; +use hyper_util::rt::TokioIo; +use log::error; +use nostr_sdk::prelude::{Nip19, ToBech32}; +use nostrdb::{BlockType, Blocks, Mention, Ndb, Note, Transaction}; +use std::io::Write; + +pub fn render_note_content(body: &mut Vec, ndb: &Ndb, note: &Note, blocks: &Blocks) { + for block in blocks.iter(note) { + let blocktype = block.blocktype(); + + match block.blocktype() { + BlockType::Url => { + let url = html_escape::encode_text(block.as_str()); + write!(body, r#"{}"#, url, url); + } + + BlockType::Hashtag => { + let hashtag = html_escape::encode_text(block.as_str()); + write!(body, r#"#{}"#, hashtag); + } + + BlockType::Text => { + let text = html_escape::encode_text(block.as_str()); + write!(body, r"{}", text); + } + + BlockType::Invoice => { + write!(body, r"{}", block.as_str()); + } + + BlockType::MentionIndex => { + write!(body, r"@nostrich"); + } + + BlockType::MentionBech32 => { + let pk = match block.as_mention().unwrap() { + Mention::Event(_) + | Mention::Note(_) + | Mention::Profile(_) + | Mention::Pubkey(_) + | Mention::Secret(_) + | Mention::Addr(_) => { + write!( + body, + r#"@{}"#, + block.as_str(), + &abbrev_str(block.as_str()) + ); + } + + Mention::Relay(relay) => { + write!( + body, + r#"{}"#, + block.as_str(), + &abbrev_str(relay.as_str()) + ); + } + }; + } + }; + } +} + +pub fn serve_note_html( + app: &Notecrumbs, + nip19: &Nip19, + note_data: &render::NoteRenderData, + r: Request, +) -> Result>, Error> { + let mut data = Vec::new(); + + // indices + // + // 0: name + // 1: abbreviated description + // 2: hostname + // 3: bech32 entity + // 4: Full content + + let hostname = "https://damus.io"; + let abbrev_content = html_escape::encode_text(abbreviate(¬e_data.note.content, 64)); + let profile_name = html_escape::encode_text(¬e_data.profile.name); + + write!( + data, + r#" + + + {0} on nostr + + + + + + + + + + + + + + + + + + + + + +

Note!

+
+
"#, + profile_name, + abbrev_content, + hostname, + nip19.to_bech32().unwrap() + )?; + + let ok = (|| -> Result<(), nostrdb::Error> { + let txn = Transaction::new(&app.ndb)?; + let note_id = note_data.note.id.ok_or(nostrdb::Error::NotFound)?; + let note = app.ndb.get_note_by_id(&txn, ¬e_id)?; + let blocks = app.ndb.get_blocks_by_key(&txn, note.key().unwrap())?; + + render_note_content(&mut data, &app.ndb, ¬e, &blocks); + + Ok(()) + })(); + + if let Err(err) = ok { + error!("error rendering html: {}", err); + write!( + data, + "{}", + html_escape::encode_text(¬e_data.note.content) + ); + } + + write!( + data, + " +
+
+ + + " + ); + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "text/html") + .status(StatusCode::OK) + .body(Full::new(Bytes::from(data)))?) +} diff --git a/src/main.rs b/src/main.rs index 2f1b0bc..66cd6d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use std::net::SocketAddr; -use html_escape; use http_body_util::Full; use hyper::body::Bytes; use hyper::header; @@ -21,9 +20,11 @@ use std::time::Duration; use lru::LruCache; +mod abbrev; mod error; mod fonts; mod gradient; +mod html; mod nip19; mod pfp; mod render; @@ -129,11 +130,6 @@ fn is_utf8_char_boundary(c: u8) -> bool { (c as i8) >= -0x40 } -fn abbreviate<'a>(text: &'a str, len: usize) -> &'a str { - let closest = floor_char_boundary(text, len); - &text[..closest] -} - fn serve_profile_html( app: &Notecrumbs, nip: &Nip19, @@ -149,74 +145,6 @@ fn serve_profile_html( .body(Full::new(Bytes::from(data)))?) } -fn serve_note_html( - app: &Notecrumbs, - nip19: &Nip19, - note: &render::NoteRenderData, - r: Request, -) -> Result>, Error> { - let mut data = Vec::new(); - - // indices - // - // 0: name - // 1: abbreviated description - // 2: hostname - // 3: bech32 entity - // 4: Full content - - let hostname = "https://damus.io"; - let abbrev_content = html_escape::encode_text(abbreviate(¬e.note.content, 64)); - let content = html_escape::encode_text(¬e.note.content); - let profile_name = html_escape::encode_text(¬e.profile.name); - - write!( - data, - r#" - - - {0} on nostr - - - - - - - - - - - - - - - - - - - - - -

Note!

-
-
{4}
-
- - - "#, - profile_name, - abbrev_content, - hostname, - nip19.to_bech32().unwrap(), - content - )?; - - Ok(Response::builder() - .header(header::CONTENT_TYPE, "text/html") - .status(StatusCode::OK) - .body(Full::new(Bytes::from(data)))?) -} - async fn serve( app: &Notecrumbs, r: Request, @@ -258,7 +186,7 @@ async fn serve( .body(Full::new(Bytes::from(data)))?) } else { match render_data { - RenderData::Note(note_rd) => serve_note_html(app, &nip19, ¬e_rd, r), + RenderData::Note(note_rd) => html::serve_note_html(app, &nip19, ¬e_rd, r), RenderData::Profile(profile_rd) => serve_profile_html(app, &nip19, &profile_rd, r), } } diff --git a/src/render.rs b/src/render.rs index 6a56d7b..dfa3604 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,4 +1,4 @@ -use crate::{fonts, Error, Notecrumbs}; +use crate::{abbrev::abbrev_str, fonts, Error, Notecrumbs}; use egui::epaint::Shadow; use egui::{ pos2, @@ -313,38 +313,6 @@ fn push_job_text(job: &mut LayoutJob, s: &str, color: Color32) { ) } -#[inline] -pub fn floor_char_boundary(s: &str, index: usize) -> usize { - if index >= s.len() { - s.len() - } else { - let lower_bound = index.saturating_sub(3); - let new_index = s.as_bytes()[lower_bound..=index] - .iter() - .rposition(|b| is_utf8_char_boundary(*b)); - - // SAFETY: we know that the character boundary will be within four bytes - unsafe { lower_bound + new_index.unwrap_unchecked() } - } -} - -#[inline] -fn is_utf8_char_boundary(c: u8) -> bool { - // This is bit magic equivalent to: b < 128 || b >= 192 - (c as i8) >= -0x40 -} - -const ABBREV_SIZE: usize = 10; - -fn abbrev_str(name: &str) -> String { - if name.len() > ABBREV_SIZE { - let closest = floor_char_boundary(name, ABBREV_SIZE); - format!("{}...", &name[..closest]) - } else { - name.to_owned() - } -} - fn push_job_user_mention( job: &mut LayoutJob, ndb: &Ndb, @@ -393,12 +361,12 @@ fn wrapped_body_blocks( BlockType::MentionBech32 => { let pk = match block.as_mention().unwrap() { - Mention::Event(ev) => push_job_text( + Mention::Event(_ev) => push_job_text( &mut job, &format!("@{}", &abbrev_str(block.as_str())), PURPLE, ), - Mention::Note(ev) => { + Mention::Note(_ev) => { push_job_text( &mut job, &format!("@{}", &abbrev_str(block.as_str())),