Skip to content

Commit

Permalink
initial html note renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
jb55 committed Jan 2, 2024
1 parent 4e996ee commit 0d826fe
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 110 deletions.
36 changes: 36 additions & 0 deletions src/abbrev.rs
Original file line number Diff line number Diff line change
@@ -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]
}
165 changes: 165 additions & 0 deletions src/html.rs
Original file line number Diff line number Diff line change
@@ -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(&note_data.note.content, 64));
let profile_name = html_escape::encode_text(&note_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, &note_id)?;
let blocks = app.ndb.get_blocks_by_key(&txn, note.key().unwrap())?;

render_note_content(&mut data, &app.ndb, &note, &blocks);

Ok(())
})();

if let Err(err) = ok {
error!("error rendering html: {}", err);
write!(
data,
"{}",
html_escape::encode_text(&note_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)))?)
}
78 changes: 3 additions & 75 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::net::SocketAddr;

use html_escape;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::header;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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(&note.note.content, 64));
let content = html_escape::encode_text(&note.note.content);
let profile_name = html_escape::encode_text(&note.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>,
Expand Down Expand Up @@ -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, &note_rd, r),
RenderData::Note(note_rd) => html::serve_note_html(app, &nip19, &note_rd, r),
RenderData::Profile(profile_rd) => serve_profile_html(app, &nip19, &profile_rd, r),
}
}
Expand Down
38 changes: 3 additions & 35 deletions src/render.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{fonts, Error, Notecrumbs};
use crate::{abbrev::abbrev_str, fonts, Error, Notecrumbs};
use egui::epaint::Shadow;
use egui::{
pos2,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())),
Expand Down

0 comments on commit 0d826fe

Please sign in to comment.