mirror of
https://github.com/damus-io/notecrumbs.git
synced 2024-09-28 17:40:45 +00:00
initial html note renderer
This commit is contained in:
parent
4e996ee480
commit
0d826fe5d9
36
src/abbrev.rs
Normal file
36
src/abbrev.rs
Normal file
@ -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
src/html.rs
Normal file
165
src/html.rs
Normal file
@ -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)))?)
|
||||
}
|
78
src/main.rs
78
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),
|
||||
}
|
||||
}
|
||||
|
@ -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())),
|
||||
|
Loading…
Reference in New Issue
Block a user