nekrochan/src/markup.rs

211 řádky
6.1 KiB
Rust
Surový Normální zobrazení Historie

2023-12-11 15:18:43 +00:00
use lazy_static::lazy_static;
2023-12-18 22:48:05 +00:00
use regex::{Captures, Regex};
2023-12-11 15:18:43 +00:00
use sqlx::query_as;
2023-12-12 17:54:48 +00:00
use std::collections::HashMap;
2023-12-11 15:18:43 +00:00
use crate::{
ctx::Ctx,
db::models::Post,
error::NekrochanError,
perms::PermissionWrapper,
trip::{secure_tripcode, tripcode},
};
lazy_static! {
pub static ref NAME_REGEX: Regex =
Regex::new(r"^([^#].*?)?(?:(##([^ ].*?)|#([^#].*?)))?(##( .*?)?)?$").unwrap();
pub static ref QUOTE_REGEX: Regex = Regex::new(r">>(\d+)").unwrap();
2023-12-18 22:48:05 +00:00
pub static ref GREENTEXT_REGEX: Regex = Regex::new(r"(?mR)^>(.*)$").unwrap();
pub static ref ORANGETEXT_REGEX: Regex = Regex::new(r"(?mR)^<(.*)$").unwrap();
pub static ref REDTEXT_REGEX: Regex = Regex::new(r"==(.+?)==").unwrap();
pub static ref BLUETEXT_REGEX: Regex = Regex::new(r"--(.+?)--").unwrap();
pub static ref GLOWTEXT_REGEX: Regex = Regex::new(r"\%\%(.+?)\%\%").unwrap();
pub static ref UH_OH_TEXT_REGEX: Regex = Regex::new(r"\(\(\((.+?)\)\)\)").unwrap();
pub static ref SPOILER_REGEX: Regex = Regex::new(r"\|\|([\s\S]+?)\|\|").unwrap();
2023-12-11 15:18:43 +00:00
pub static ref URL_REGEX: Regex =
Regex::new(r"https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^]+").unwrap();
}
pub fn parse_name(
ctx: &Ctx,
perms: &PermissionWrapper,
anon_name: &str,
name: &str,
) -> Result<(String, Option<String>, Option<String>), NekrochanError> {
2023-12-18 22:48:05 +00:00
let Some(captures) = NAME_REGEX.captures(name) else {
2023-12-11 15:59:32 +00:00
return Ok((anon_name.to_owned(), None, None));
2023-12-11 15:18:43 +00:00
};
let name = match captures.get(1) {
Some(name) => {
let name = name.as_str().to_owned();
if name.len() > 32 {
return Err(NekrochanError::PostNameFormatError);
}
name
}
None => anon_name.to_owned(),
};
let tripcode = match captures.get(2) {
Some(_) => {
let strip = captures.get(3);
let itrip = captures.get(4);
if let Some(strip) = strip {
let trip = secure_tripcode(strip.as_str(), &ctx.cfg.secrets.secure_trip);
Some(format!("!!{trip}"))
} else if let Some(itrip) = itrip {
let trip = tripcode(itrip.as_str());
Some(format!("!{trip}"))
} else {
None
}
}
None => None,
};
if !(perms.owner() || perms.capcodes()) {
return Ok((name, tripcode, None));
}
let capcode = match captures.get(5) {
Some(_) => match captures.get(6) {
Some(capcode) => {
let capcode: String = capcode.as_str().trim().into();
if capcode.is_empty() {
2023-12-11 15:59:32 +00:00
Some(capcode_fallback(perms.owner()))
2023-12-11 15:18:43 +00:00
} else {
if capcode.len() > 32 {
return Err(NekrochanError::CapcodeFormatError);
}
Some(capcode)
}
}
2023-12-11 15:59:32 +00:00
None => Some(capcode_fallback(perms.owner())),
2023-12-11 15:18:43 +00:00
},
None => None,
};
Ok((name, tripcode, capcode))
}
2023-12-11 15:59:32 +00:00
fn capcode_fallback(owner: bool) -> String {
if owner {
"Admin".into()
} else {
"Uklízeč".into()
}
}
2023-12-11 15:18:43 +00:00
pub async fn markup(
ctx: &Ctx,
board: &String,
2024-01-02 11:58:47 +00:00
op: Option<i64>,
2023-12-11 15:18:43 +00:00
text: &str,
) -> Result<String, NekrochanError> {
2023-12-18 22:48:05 +00:00
let text = escape_html(&text);
2023-12-11 15:18:43 +00:00
let quoted_posts = get_quoted_posts(ctx, board, &text).await?;
let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| {
let id_raw = &captures[1];
2023-12-11 15:59:32 +00:00
let Ok(id) = id_raw.parse() else {
return format!("<span class=\"dead-quote\">&gt;&gt;{id_raw}</span>");
2023-12-11 15:18:43 +00:00
};
let post = quoted_posts.get(&id);
2023-12-11 15:59:32 +00:00
if let Some(post) = post {
format!(
2023-12-11 15:18:43 +00:00
"<a class=\"quote\" href=\"{}\">&gt;&gt;{}</a>{}",
post.post_url(),
post.id,
if op == Some(post.id) {
" <span class=\"small\">(OP)</span>"
} else {
""
}
2023-12-11 15:59:32 +00:00
)
} else {
format!("<span class=\"dead-quote\">&gt;&gt;{id}</span>")
2023-12-11 15:18:43 +00:00
}
});
let text = GREENTEXT_REGEX.replace_all(&text, "<span class=\"greentext\">&gt;$1</span>");
let text = ORANGETEXT_REGEX.replace_all(&text, "<span class=\"orangetext\">&lt;$1</span>");
let text = REDTEXT_REGEX.replace_all(&text, "<span class=\"redtext\">$1</span>");
let text = BLUETEXT_REGEX.replace_all(&text, "<span class=\"bluetext\">$1</span>");
let text = GLOWTEXT_REGEX.replace_all(&text, "<span class=\"glowtext\">$1</span>");
let text = SPOILER_REGEX.replace_all(&text, "<span class=\"spoiler\">$1</span>");
let text = UH_OH_TEXT_REGEX.replace_all(&text, |captures: &Captures| {
format!(
"<span class=\"uh-oh-text\">((( {} )))</span>",
captures[1].trim()
)
});
let text = URL_REGEX.replace_all(&text, |captures: &Captures| {
let url = &captures[0];
format!("<a rel=\"nofollow\" href=\"{url}\">{url}</a>")
});
Ok(text.to_string())
}
fn escape_html(text: &str) -> String {
text.replace('&', "&amp;")
.replace('\'', "&#39;")
.replace('/', "&#x2F;")
.replace('`', "&#x60;")
.replace('=', "&#x3D;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
async fn get_quoted_posts(
ctx: &Ctx,
board: &String,
text: &str,
2024-01-02 11:58:47 +00:00
) -> Result<HashMap<i64, Post>, NekrochanError> {
let mut quoted_ids: Vec<i64> = Vec::new();
2023-12-11 15:18:43 +00:00
for quote in QUOTE_REGEX.captures_iter(text) {
2023-12-18 22:48:05 +00:00
let id_raw = &quote[1];
2023-12-11 15:59:32 +00:00
let Ok(id) = id_raw.parse() else { continue };
2023-12-11 15:18:43 +00:00
quoted_ids.push(id);
}
if quoted_ids.is_empty() {
return Ok(HashMap::new());
}
let in_list = quoted_ids
.iter()
2023-12-11 15:59:32 +00:00
.map(std::string::ToString::to_string)
2023-12-11 15:18:43 +00:00
.collect::<Vec<_>>()
.join(",");
let quoted_posts = query_as(&format!(
2023-12-11 15:59:32 +00:00
"SELECT * FROM posts_{board} WHERE id IN ({in_list})"
2023-12-11 15:18:43 +00:00
))
.fetch_all(ctx.db())
.await?
.into_iter()
.map(|post: Post| (post.id, post))
.collect::<HashMap<_, _>>();
Ok(quoted_posts)
}