add: markdown emoji parsing
This commit is contained in:
parent
d9234bf656
commit
af67077ae7
8 changed files with 243 additions and 46 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -949,6 +949,15 @@ version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "emojis"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4"
|
||||||
|
dependencies = [
|
||||||
|
"phf",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
@ -3731,6 +3740,7 @@ dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bb8-postgres",
|
"bb8-postgres",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
|
"emojis",
|
||||||
"md-5",
|
"md-5",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"redis",
|
"redis",
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod routes;
|
||||||
mod sanitize;
|
mod sanitize;
|
||||||
|
|
||||||
use assets::{init_dirs, write_assets};
|
use assets::{init_dirs, write_assets};
|
||||||
use tetratto_core::model::permissions::FinePermission;
|
use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji};
|
||||||
pub use tetratto_core::*;
|
pub use tetratto_core::*;
|
||||||
|
|
||||||
use axum::{Extension, Router};
|
use axum::{Extension, Router};
|
||||||
|
@ -24,7 +24,12 @@ use tokio::sync::RwLock;
|
||||||
pub(crate) type State = Arc<RwLock<(DataManager, Tera, Client)>>;
|
pub(crate) type State = Arc<RwLock<(DataManager, Tera, Client)>>;
|
||||||
|
|
||||||
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
Ok(tetratto_shared::markdown::render_markdown(value.as_str().unwrap()).into())
|
Ok(
|
||||||
|
CustomEmoji::replace(&tetratto_shared::markdown::render_markdown(
|
||||||
|
value.as_str().unwrap(),
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_escape(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn color_escape(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
|
|
|
@ -61,9 +61,14 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{{ components::emoji_picker(element_id="content",
|
||||||
|
render_dialog=true) }}
|
||||||
|
|
||||||
<button class="primary">
|
<button class="primary">
|
||||||
{{ text "communities:action.create" }}
|
{{ text "communities:action.create" }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1112,4 +1112,75 @@ secondary=false) -%}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}
|
||||||
|
<button
|
||||||
|
class="button small square quaternary"
|
||||||
|
onclick="window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()"
|
||||||
|
title="Emojis"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{{ icon "smile-plus" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% if render_dialog %}
|
||||||
|
<dialog id="emoji_dialog">
|
||||||
|
<div class="inner flex flex-col gap-2">
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="https://unpkg.com/emoji-picker-element@1.22.8/index.js"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<emoji-picker
|
||||||
|
style="
|
||||||
|
--border-radius: var(--radius);
|
||||||
|
--background: var(--color-super-raised);
|
||||||
|
--input-border-radiFus: var(--radius);
|
||||||
|
--input-border-color: var(--color-primary);
|
||||||
|
--indicator-color: var(--color-primary);
|
||||||
|
--emoji-padding: 0.25rem;
|
||||||
|
box-shadow: 0 0 4px var(--color-shadow);
|
||||||
|
"
|
||||||
|
class="w-full"
|
||||||
|
></emoji-picker>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document
|
||||||
|
.querySelector("emoji-picker")
|
||||||
|
.addEventListener("emoji-click", (event) => {
|
||||||
|
function gemoji() {
|
||||||
|
const use_first_shortcode = ["grinning squinting face"];
|
||||||
|
|
||||||
|
if (
|
||||||
|
use_first_shortcode.includes(
|
||||||
|
event.detail.emoji.annotation,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return event.detail.emoji.shortcodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.detail.emoji.shortcodes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
|
).value += ` :${gemoji()}:`;
|
||||||
|
document.getElementById("emoji_dialog").close();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="bold red quaternary"
|
||||||
|
onclick="document.getElementById('emoji_dialog').close()"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{{ icon "x" }} {{ text "dialog:action.close" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
{% endif %} {%- endmacro %}
|
||||||
|
|
|
@ -43,9 +43,14 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{{ components::emoji_picker(element_id="content",
|
||||||
|
render_dialog=true) }}
|
||||||
|
|
||||||
<button class="primary">
|
<button class="primary">
|
||||||
{{ text "communities:action.create" }}
|
{{ text "communities:action.create" }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -198,8 +203,8 @@
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
type="text"
|
type="text"
|
||||||
name="content"
|
name="new_content"
|
||||||
id="content"
|
id="new_content"
|
||||||
placeholder="content"
|
placeholder="content"
|
||||||
required
|
required
|
||||||
minlength="2"
|
minlength="2"
|
||||||
|
@ -209,7 +214,14 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="primary">{{ text "general:action.save" }}</button>
|
<div class="flex gap-2">
|
||||||
|
{{ components::emoji_picker(element_id="new_content",
|
||||||
|
render_dialog=false) }}
|
||||||
|
|
||||||
|
<button class="primary">
|
||||||
|
{{ text "general:action.save" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -223,7 +235,7 @@
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: e.target.content.value,
|
content: e.target.new_content.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|
|
@ -33,3 +33,4 @@ rusqlite = { version = "0.35.0", optional = true }
|
||||||
tokio-postgres = { version = "0.7.13", optional = true }
|
tokio-postgres = { version = "0.7.13", optional = true }
|
||||||
bb8-postgres = { version = "0.9.0", optional = true }
|
bb8-postgres = { version = "0.9.0", optional = true }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
emojis = "0.6.4"
|
||||||
|
|
|
@ -54,40 +54,30 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all emojis by user.
|
/// Get an emoji by community and name.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `user` - the ID of the user to fetch emojis for
|
/// * `community` - the ID of the community to fetch emoji from
|
||||||
pub async fn get_emojis_by_user(&self, user: usize) -> Result<Vec<CustomEmoji>> {
|
/// * `name` - the name of the emoji
|
||||||
let conn = match self.connect().await {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = query_rows!(
|
|
||||||
&conn,
|
|
||||||
"SELECT * FROM emojis WHERE (owner = $1 OR members LIKE $2) AND community = 0 ORDER BY created DESC",
|
|
||||||
params![&(user as i64), &format!("%{user}%")],
|
|
||||||
|x| { Self::get_emoji_from_row(x) }
|
|
||||||
);
|
|
||||||
|
|
||||||
if res.is_err() {
|
|
||||||
return Err(Error::GeneralNotFound("emoji".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res.unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a emoji given its `owner` and a member.
|
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Returns
|
||||||
/// * `owner` - the ID of the owner
|
/// `(custom emoji, emoji string)`
|
||||||
/// * `member` - the ID of the member
|
///
|
||||||
pub async fn get_emoji_by_owner_member(
|
/// Custom emoji will be none if emoji string is some, and vice versa. Emoji string
|
||||||
|
/// will only be some if the community is 0 (no community ID in parsed string, or `0.emoji_name`)
|
||||||
|
///
|
||||||
|
/// Regular unicode emojis should have a community ID of 0, since they don't belong
|
||||||
|
/// to any community and can be used by anyone.
|
||||||
|
pub async fn get_emoji_by_community_name(
|
||||||
&self,
|
&self,
|
||||||
owner: usize,
|
community: usize,
|
||||||
member: usize,
|
name: &str,
|
||||||
) -> Result<CustomEmoji> {
|
) -> Result<(Option<CustomEmoji>, Option<String>)> {
|
||||||
|
if community == 0 {
|
||||||
|
return Ok((None, Some("🐇".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
let conn = match self.connect().await {
|
let conn = match self.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
@ -95,9 +85,9 @@ impl DataManager {
|
||||||
|
|
||||||
let res = query_row!(
|
let res = query_row!(
|
||||||
&conn,
|
&conn,
|
||||||
"SELECT * FROM emojis WHERE owner = $1 AND members = $2 AND community = 0 ORDER BY created DESC",
|
"SELECT * FROM emojis WHERE community = $1 AND name = $2 ORDER BY name ASC",
|
||||||
params![&(owner as i64), &format!("[{member}]")],
|
params![&(community as i64), &name],
|
||||||
|x| { Ok(Self::get_emoji_from_row(x)) }
|
|x| { Ok((Some(Self::get_emoji_from_row(x)), None)) }
|
||||||
);
|
);
|
||||||
|
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
|
|
|
@ -48,6 +48,8 @@ pub struct CustomEmoji {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type EmojiParserResult = Vec<(String, usize, String)>;
|
||||||
|
|
||||||
impl CustomEmoji {
|
impl CustomEmoji {
|
||||||
/// Create a new [`CustomEmoji`].
|
/// Create a new [`CustomEmoji`].
|
||||||
pub fn new(owner: usize, community: usize, upload_id: usize, name: String) -> Self {
|
pub fn new(owner: usize, community: usize, upload_id: usize, name: String) -> Self {
|
||||||
|
@ -63,4 +65,105 @@ impl CustomEmoji {
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace emojis in the given input string.
|
||||||
|
pub fn replace(input: &str) -> String {
|
||||||
|
let res = Self::parse(input);
|
||||||
|
let mut out = input.to_string();
|
||||||
|
|
||||||
|
for emoji in res {
|
||||||
|
if emoji.1 == 0 {
|
||||||
|
out = out.replace(
|
||||||
|
&emoji.0,
|
||||||
|
match emojis::get_by_shortcode(&emoji.2) {
|
||||||
|
Some(e) => e.as_str(),
|
||||||
|
None => &emoji.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
out = out.replace(
|
||||||
|
&emoji.0,
|
||||||
|
&format!(
|
||||||
|
"<img class=\"emoji\" src=\"/api/v1/communities/{}/emoji/{}\" />",
|
||||||
|
emoji.1, emoji.2
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse text for emojis.
|
||||||
|
///
|
||||||
|
/// Another "great" parser, just like the mentions parser.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `(capture, community id, emoji name)`
|
||||||
|
pub fn parse(input: &str) -> EmojiParserResult {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut buffer: String = String::new();
|
||||||
|
|
||||||
|
let mut escape: bool = false;
|
||||||
|
let mut in_emoji: bool = false;
|
||||||
|
|
||||||
|
let mut chars = input.chars();
|
||||||
|
while let Some(char) = chars.next() {
|
||||||
|
if escape == true {
|
||||||
|
buffer.push(char);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == '\\' && escape == false {
|
||||||
|
escape = true;
|
||||||
|
continue;
|
||||||
|
} else if char == ':' {
|
||||||
|
let mut community_id: String = String::new();
|
||||||
|
let mut accepting_community_id_chars: bool = true;
|
||||||
|
let mut emoji_name: String = String::new();
|
||||||
|
|
||||||
|
let mut char_count: u32 = 0; // if we're past the first 4 characters and we haven't hit a digit, stop accepting community id
|
||||||
|
while let Some(char) = chars.next() {
|
||||||
|
if (char == ':') | (char == ' ') {
|
||||||
|
in_emoji = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if char.is_digit(10) && accepting_community_id_chars {
|
||||||
|
community_id.push(char);
|
||||||
|
} else if char == '.' {
|
||||||
|
// the period closes the community id
|
||||||
|
accepting_community_id_chars = false;
|
||||||
|
} else {
|
||||||
|
emoji_name.push(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
if char_count >= 4 && community_id.is_empty() {
|
||||||
|
accepting_community_id_chars = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push((
|
||||||
|
format!(
|
||||||
|
":{}{emoji_name}:",
|
||||||
|
if !community_id.is_empty() {
|
||||||
|
format!("{community_id}.")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
community_id.parse::<usize>().unwrap_or(0),
|
||||||
|
emoji_name,
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else if in_emoji {
|
||||||
|
buffer.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue