nekrochan/src/markup.rs

217 řádky
6.4 KiB
Rust
Surový Normální zobrazení Historie

2023-12-11 15:18:43 +00:00
use std::collections::HashMap;
use fancy_regex::{Captures, Regex};
use lazy_static::lazy_static;
use sqlx::query_as;
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();
pub static ref GREENTEXT_REGEX: Regex =
Regex::new(r"(?m)^>((?!>\d+|>>/\w+(/\d*)?|>>#/).*)")
.unwrap();
pub static ref ORANGETEXT_REGEX: Regex = Regex::new(r"(?m)^<(.+)").unwrap();
pub static ref REDTEXT_REGEX: Regex = Regex::new(r"(?m)==(.+?)==").unwrap();
pub static ref BLUETEXT_REGEX: Regex = Regex::new(r"(?m)--(.+?)--").unwrap();
pub static ref GLOWTEXT_REGEX: Regex = Regex::new(r"(?m)\%\%(.+?)\%\%").unwrap();
pub static ref UH_OH_TEXT_REGEX: Regex = Regex::new(r"(?m)\(\(\((.+?)\)\)\)").unwrap();
pub static ref SPOILER_REGEX: Regex = Regex::new(r"(?m)\|\|([\s\S]+?)\|\|").unwrap();
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> {
let captures = match NAME_REGEX.captures(name)? {
Some(captures) => captures,
None => return Ok((anon_name.to_owned(), None, None)),
};
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));
}
fn capcode_fallback(owner: bool) -> Option<String> {
if owner {
Some("Admin".into())
} else {
Some("Uklízeč".into())
}
}
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() {
capcode_fallback(perms.owner())
} else {
if capcode.len() > 32 {
return Err(NekrochanError::CapcodeFormatError);
}
Some(capcode)
}
}
None => capcode_fallback(perms.owner()),
},
None => None,
};
Ok((name, tripcode, capcode))
}
pub async fn markup(
ctx: &Ctx,
board: &String,
op: Option<i32>,
text: &str,
) -> Result<String, NekrochanError> {
let text = escape_html(text);
let quoted_posts = get_quoted_posts(ctx, board, &text).await?;
let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| {
let id_raw = &captures[1];
let id = match id_raw.parse() {
Ok(id) => id,
Err(_) => return format!("<span class=\"dead-quote\">&gt;&gt;{id_raw}</span>"),
};
let post = quoted_posts.get(&id);
match post {
Some(post) => format!(
"<a class=\"quote\" href=\"{}\">&gt;&gt;{}</a>{}",
post.post_url(),
post.id,
if op == Some(post.id) {
" <span class=\"small\">(OP)</span>"
} else {
""
}
),
None => format!("<span class=\"dead-quote\">&gt;&gt;{id}</span>"),
}
});
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,
) -> Result<HashMap<i32, Post>, NekrochanError> {
let mut quoted_ids: Vec<i32> = Vec::new();
for quote in QUOTE_REGEX.captures_iter(text) {
let id_raw = &quote.unwrap()[1];
let id = match id_raw.parse() {
Ok(id) => id,
Err(_) => continue,
};
quoted_ids.push(id);
}
if quoted_ids.is_empty() {
return Ok(HashMap::new());
}
let in_list = quoted_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
let quoted_posts = query_as(&format!(
"SELECT * FROM posts_{} WHERE id IN ({})",
board, in_list
))
.fetch_all(ctx.db())
.await?
.into_iter()
.map(|post: Post| (post.id, post))
.collect::<HashMap<_, _>>();
Ok(quoted_posts)
}