From a090aeabcd350ab04eebefd5f3594acbdc9f2087 Mon Sep 17 00:00:00 2001 From: blankie Date: Thu, 23 Nov 2023 11:16:56 +1100 Subject: [PATCH] Add custom emoji support --- routes/css.cpp | 6 +++++ routes/user.cpp | 10 ++++---- servehelper.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++++---- servehelper.h | 3 ++- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/routes/css.cpp b/routes/css.cpp index 7ac06a0..e8b32c4 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -41,6 +41,11 @@ a:hover { color: var(--bright-accent-color); } +.custom_emoji { + height: 1em; + width: 1em; +} + /* ERROR PAGE */ .error { text-align: center; @@ -79,6 +84,7 @@ a:hover { .user_page-user_description { display: grid; grid-template-columns: 1fr 1fr; + grid-gap: 1em; } } diff --git a/routes/user.cpp b/routes/user.cpp index 4079f62..f3a62a6 100644 --- a/routes/user.cpp +++ b/routes/user.cpp @@ -8,7 +8,7 @@ static const char* sorting_method_suffixes[3] = {"", "/with_replies", "/media"}; static inline PostSortingMethod get_sorting_method(const std::string& method); static inline Element user_header(const httplib::Request& req, const Account& account, PostSortingMethod sorting_method); -static inline Element user_link_field(const httplib::Request& req, const std::string& domain_name, const AccountField& field); +static inline Element user_link_field(const httplib::Request& req, const Account& account, const AccountField& field); static inline Element sorting_method_link(const httplib::Request& req, const Account& account, PostSortingMethod current_method, PostSortingMethod new_method); @@ -59,7 +59,7 @@ static inline Element user_header(const httplib::Request& req, const Account& ac Element user_links("table", {{"class", "user_page-user_links"}}, {}); user_links.nodes.reserve(account.fields.size()); for (const AccountField& i : account.fields) { - user_links.nodes.push_back(user_link_field(req, account.domain_name, i)); + user_links.nodes.push_back(user_link_field(req, account, i)); } Element header("header", { @@ -82,7 +82,7 @@ static inline Element user_header(const httplib::Request& req, const Account& ac }), Element("div", {{"class", "user_page-user_description"}}, { - Element("div", {{"class", "user_page-user_bio"}}, {preprocess_html(req, account.domain_name, account.note_html)}), + Element("div", {{"class", "user_page-user_bio"}}, {preprocess_html(req, account.domain_name, account.emojis, account.note_html)}), std::move(user_links), }), @@ -96,12 +96,12 @@ static inline Element user_header(const httplib::Request& req, const Account& ac return header; } -static inline Element user_link_field(const httplib::Request& req, const std::string& domain_name, const AccountField& field) { +static inline Element user_link_field(const httplib::Request& req, const Account& account, const AccountField& field) { using namespace std::string_literals; Element tr("tr", { Element("th", {field.name}), - Element("td", {preprocess_html(req, domain_name, field.value_html)}), + Element("td", {preprocess_html(req, account.domain_name, account.emojis, field.value_html)}), }); if (field.verified_at >= 0) { struct tm verified_at; diff --git a/servehelper.cpp b/servehelper.cpp index e8e1203..e7da793 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -5,13 +5,16 @@ #include #include "config.h" +#include "models.h" #include "servehelper.h" #include "lxb_wrapper.h" #include "routes/routes.h" -static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element); +static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector& emojis, lxb_dom_element_t* element); static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element); static inline bool should_fix_link(lxb_dom_element_t* element); +static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector& emojis); +static inline std::vector emojify(lxb_dom_document_t* document, std::string str, const std::vector& emojis); class CurlUrlException : public std::exception { public: @@ -159,15 +162,15 @@ bool should_send_304(const httplib::Request& req, uint64_t hash) { return pos != std::string::npos && (pos == 0 || header[pos - 1] != '/'); } -blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const blankie::html::HTMLString& str) { +blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector& emojis, const blankie::html::HTMLString& str) { LXB::HTML::Document document(str.str); - preprocess_html(req, domain_name, document.body_element()); + preprocess_html(req, domain_name, emojis, document.body_element()); return blankie::html::HTMLString(document.serialize()); } -static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element) { +static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector& emojis, lxb_dom_element_t* element) { const char* tag_name = reinterpret_cast(lxb_dom_element_tag_name(element, nullptr)); if (strncmp(tag_name, "A", 2) == 0) { @@ -179,7 +182,9 @@ static inline void preprocess_html(const httplib::Request& req, const std::strin lxb_dom_node_t* child = lxb_dom_node_first_child(lxb_dom_interface_node(element)); while (child) { if (child->type == LXB_DOM_NODE_TYPE_ELEMENT) { - preprocess_html(req, domain_name, lxb_dom_interface_element(child)); + preprocess_html(req, domain_name, emojis, lxb_dom_interface_element(child)); + } else if (child->type == LXB_DOM_NODE_TYPE_TEXT) { + child = emojify(child, emojis); } child = lxb_dom_node_next(child); @@ -247,3 +252,55 @@ static inline bool should_fix_link(lxb_dom_element_t* element) { child = lxb_dom_node_next(child); return child == nullptr; } + +static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector& emojis) { + size_t text_content_len; + const char* text_content = reinterpret_cast(lxb_dom_node_text_content(child, &text_content_len)); + + std::vector nodes = emojify(child->owner_document, std::string(text_content, text_content_len), emojis); + for (lxb_dom_node_t* node : nodes) { + lxb_dom_node_insert_after(child, node); + lxb_dom_node_destroy(child); + child = node; + } + + return child; +} + +static std::regex shortcode_re(":([a-zA-Z0-9_]+):"); +static inline std::vector emojify(lxb_dom_document_t* document, std::string str, const std::vector& emojis) { + std::string buf; + std::smatch sm; + std::vector res; + + while (std::regex_search(str, sm, shortcode_re)) { + buf += sm.prefix(); + + std::string group_0 = sm.str(0); + auto emoji = std::find_if(emojis.begin(), emojis.end(), [&](const Emoji& i) { return i.shortcode == sm.str(1); }); + if (emoji != emojis.end()) { + res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast(buf.data()), buf.size()))); + buf.clear(); + + lxb_dom_element_t* img = lxb_dom_element_create(document, reinterpret_cast("img"), 3, nullptr, 0, nullptr, 0, nullptr, 0, false); + lxb_dom_element_set_attribute(img, reinterpret_cast("class"), 5, reinterpret_cast("custom_emoji"), 12); + lxb_dom_element_set_attribute(img, reinterpret_cast("alt"), 3, reinterpret_cast(group_0.data()), group_0.size()); + lxb_dom_element_set_attribute(img, reinterpret_cast("title"), 5, reinterpret_cast(group_0.data()), group_0.size()); + lxb_dom_element_set_attribute(img, reinterpret_cast("src"), 3, reinterpret_cast(emoji->url.data()), emoji->url.size()); + res.push_back(lxb_dom_interface_node(img)); + } else { + buf += group_0; + } + + str = sm.suffix(); + } + + if (!str.empty()) { + buf += std::move(str); + } + if (!buf.empty()) { + res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast(buf.data()), buf.size()))); + } + + return res; +} diff --git a/servehelper.h b/servehelper.h index 9bef23b..579e17a 100644 --- a/servehelper.h +++ b/servehelper.h @@ -4,6 +4,7 @@ #include #include "blankie/serializer.h" +struct Emoji; // forward declaration from models.h using Element = blankie::html::Element; using Node = blankie::html::Node; @@ -18,4 +19,4 @@ std::string get_origin(const httplib::Request& req); std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str); bool should_send_304(const httplib::Request& req, uint64_t hash); -blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const blankie::html::HTMLString& str); +blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector& emojis, const blankie::html::HTMLString& str);