commit 0d826fe5d928b490b4643406bda914d7062b16e8
parent 4e996ee4807793f35691d8ace8fcc62883f41774
Author: William Casarin <jb55@jb55.com>
Date: Tue, 2 Jan 2024 09:13:09 -0800
initial html note renderer
Diffstat:
A | src/abbrev.rs | | | 36 | ++++++++++++++++++++++++++++++++++++ |
A | src/html.rs | | | 165 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/main.rs | | | 78 | +++--------------------------------------------------------------------------- |
M | src/render.rs | | | 38 | +++----------------------------------- |
4 files changed, 207 insertions(+), 110 deletions(-)
diff --git a/src/abbrev.rs 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
@@ -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<u8>, 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#"<a href="{}">{}</a>"#, url, url);
+ }
+
+ BlockType::Hashtag => {
+ let hashtag = html_escape::encode_text(block.as_str());
+ write!(body, r#"<span class="hashtag">#{}</span>"#, 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#"<a href="/{}">@{}</a>"#,
+ block.as_str(),
+ &abbrev_str(block.as_str())
+ );
+ }
+
+ Mention::Relay(relay) => {
+ write!(
+ body,
+ r#"<a href="/{}">{}</a>"#,
+ block.as_str(),
+ &abbrev_str(relay.as_str())
+ );
+ }
+ };
+ }
+ };
+ }
+}
+
+pub fn serve_note_html(
+ app: &Notecrumbs,
+ nip19: &Nip19,
+ note_data: &render::NoteRenderData,
+ r: Request<hyper::body::Incoming>,
+) -> Result<Response<Full<Bytes>>, 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#"
+ <html>
+ <head>
+ <title>{0} on nostr</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta charset="UTF-8">
+
+ <meta property="og:description" content="{1}" />
+ <meta property="og:image" content="{2}/{3}.png"/>
+ <meta property="og:image:alt" content="{0}: {1}" />
+ <meta property="og:image:height" content="600" />
+ <meta property="og:image:width" content="1200" />
+ <meta property="og:image:type" content="image/png" />
+ <meta property="og:site_name" content="Damus" />
+ <meta property="og:title" content="{0} on nostr" />
+ <meta property="og:url" content="{2}/{3}"/>
+ <meta name="og:type" content="website"/>
+ <meta name="twitter:image:src" content="{2}/{3}.png" />
+ <meta name="twitter:site" content="@damusapp" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:title" content="{0} on nostr" />
+ <meta name="twitter:description" content="{1}" />
+
+ </head>
+ <body>
+ <h3>Note!</h3>
+ <div class="note">
+ <div class="note-content">"#,
+ 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,
+ "
+ </div>
+ </div>
+ </body>
+ </html>
+ "
+ );
+
+ 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
@@ -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<hyper::body::Incoming>,
-) -> Result<Response<Full<Bytes>>, 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#"
- <html>
- <head>
- <title>{0} on nostr</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta charset="UTF-8">
-
- <meta property="og:description" content="{1}" />
- <meta property="og:image" content="{2}/{3}.png"/>
- <meta property="og:image:alt" content="{0}: {1}" />
- <meta property="og:image:height" content="600" />
- <meta property="og:image:width" content="1200" />
- <meta property="og:image:type" content="image/png" />
- <meta property="og:site_name" content="Damus" />
- <meta property="og:title" content="{0} on nostr" />
- <meta property="og:url" content="{2}/{3}"/>
- <meta name="og:type" content="website"/>
- <meta name="twitter:image:src" content="{2}/{3}.png" />
- <meta name="twitter:site" content="@damusapp" />
- <meta name="twitter:card" content="summary_large_image" />
- <meta name="twitter:title" content="{0} on nostr" />
- <meta name="twitter:description" content="{1}" />
-
- </head>
- <body>
- <h3>Note!</h3>
- <div class="note">
- <div class="note-content">{4}</div>
- </div>
- </body>
- </html>
- "#,
- 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<hyper::body::Incoming>,
@@ -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
@@ -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())),