diff options
-rw-r--r-- | TODO | 4 | ||||
m--------- | depends/html-parser | 0 | ||||
m--------- | depends/html-search | 0 | ||||
m--------- | depends/mglpp | 0 | ||||
-rw-r--r-- | example-config.json | 1 | ||||
-rw-r--r-- | include/BodyItem.hpp | 20 | ||||
-rw-r--r-- | include/Config.hpp | 1 | ||||
-rw-r--r-- | include/ResourceLoader.hpp | 1 | ||||
-rw-r--r-- | include/Text.hpp | 43 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 3 | ||||
-rw-r--r-- | src/Body.cpp | 2 | ||||
-rw-r--r-- | src/BodyItem.cpp | 3 | ||||
-rw-r--r-- | src/Config.cpp | 1 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 41 | ||||
-rw-r--r-- | src/ResourceLoader.cpp | 21 | ||||
-rw-r--r-- | src/Text.cpp | 309 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 134 | ||||
-rw-r--r-- | tests/main.cpp | 7 |
18 files changed, 401 insertions, 190 deletions
@@ -244,4 +244,6 @@ Add ctrl+h to go back to the front page. Async load textures (not just images). This can be done efficiently by using different opengl contexts in different threads and making the context current right before a heavy opengl operation. All threads need to set their opengl context often. Downloading files should take into account the remove mime type if available. Fallback to file extension. Text images atlas. -Do not render invalid unicode.
\ No newline at end of file +Do not render invalid unicode. +Use matrix "from" with proper cache. +Text editing should take into consideration FORMATTED_TEXT_START/FORMATTED_TEXT_END.
\ No newline at end of file diff --git a/depends/html-parser b/depends/html-parser -Subproject 199ca9297d2ef4ff58db4b0c948eb384deceb61 +Subproject 684a3bc56d5c40ed3eb54ca263751fa4724e73f diff --git a/depends/html-search b/depends/html-search -Subproject d2b8fd13b03503a259275387f07210aaf27e8d9 +Subproject 3dbbcc75199bd996163500beb39a6e902bc7ddc diff --git a/depends/mglpp b/depends/mglpp -Subproject 0108af496da089dbee70ce80d778dfb5c238460 +Subproject d04c98708fd46524c0861baf65e9e4ff62d4879 diff --git a/example-config.json b/example-config.json index e6c2738..790ccc1 100644 --- a/example-config.json +++ b/example-config.json @@ -82,6 +82,7 @@ "font": { "latin": "", "latin_bold": "", + "latin_monospace": "", "cjk": "", "symbols": "" }, diff --git a/include/BodyItem.hpp b/include/BodyItem.hpp index 819b730..b7dc48a 100644 --- a/include/BodyItem.hpp +++ b/include/BodyItem.hpp @@ -96,25 +96,28 @@ namespace QuickMedia { dirty_timestamp = true; } - void set_title_color(mgl::Color new_color) { - if(new_color == title_color) + void set_title_color(mgl::Color new_color, bool new_force_color = false) { + if(new_color == title_color && new_force_color == force_description_color) return; title_color = new_color; dirty = true; + force_title_color = new_force_color; } - void set_description_color(mgl::Color new_color) { - if(new_color == description_color) + void set_description_color(mgl::Color new_color, bool new_force_color = false) { + if(new_color == description_color && new_force_color == force_description_color) return; description_color = new_color; dirty_description = true; + force_description_color = new_force_color; } - void set_author_color(mgl::Color new_color) { - if(new_color == author_color) + void set_author_color(mgl::Color new_color, bool new_force_color = false) { + if(new_color == author_color && new_force_color == force_description_color) return; author_color = new_color; dirty_author = true; + force_author_color = new_force_color; } void add_reaction(std::string text, void *userdata, mgl::Color text_color); @@ -145,6 +148,8 @@ namespace QuickMedia { void draw_list(Body *body, mgl::Window &render_target); + // TODO: Bits for bools + // TODO: Use a list of strings instead, not all plugins need all of these fields std::string url; std::string thumbnail_url; @@ -156,6 +161,9 @@ namespace QuickMedia { bool dirty_reactions; // TODO: Remove this and instead if |thumbnail_url| starts with file://, then its a local file bool thumbnail_is_local; + bool force_title_color = false; + bool force_description_color = false; + bool force_author_color = false; std::unique_ptr<Text> title_text; std::unique_ptr<Text> description_text; std::unique_ptr<Text> author_text; diff --git a/include/Config.hpp b/include/Config.hpp index bd80021..9edc44b 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -65,6 +65,7 @@ namespace QuickMedia { struct FontConfig { std::string latin; std::string latin_bold; + std::string latin_monospace; std::string cjk; std::string symbols; }; diff --git a/include/ResourceLoader.hpp b/include/ResourceLoader.hpp index c60f1f3..746208a 100644 --- a/include/ResourceLoader.hpp +++ b/include/ResourceLoader.hpp @@ -14,6 +14,7 @@ namespace QuickMedia::FontLoader { enum class FontType { LATIN, LATIN_BOLD, + LATIN_MONOSPACE, CJK, SYMBOLS }; diff --git a/include/Text.hpp b/include/Text.hpp index 1533380..73dd565 100644 --- a/include/Text.hpp +++ b/include/Text.hpp @@ -21,17 +21,26 @@ namespace mgl { namespace QuickMedia { + static constexpr size_t FONT_ARRAY_SIZE = 6; + + enum FormattedTextFlag : uint8_t { + FORMATTED_TEXT_FLAG_NONE = 0, + //FORMATTED_TEXT_FLAG_BOLD = 1 << 0, + FORMATTED_TEXT_FLAG_MONOSPACE = 1 << 1, + FORMATTED_TEXT_FLAG_COLOR = 1 << 2 + }; + struct TextElement { enum class Type { TEXT, + FORMAT_START, + FORMAT_END, IMAGE }; enum class TextType { - LATIN, - CJK, - SYMBOL, + TEXT, EMOJI }; @@ -39,7 +48,7 @@ namespace QuickMedia void create_text(std::string_view text) { this->text = text; - text_type = TextType::LATIN; + text_type = TextType::TEXT; type = Type::TEXT; } @@ -50,10 +59,14 @@ namespace QuickMedia this->size = size; type = Type::IMAGE; } + + // TODO: Remove some fields // TODO: union grouped std::string_view text; - TextType text_type = TextType::LATIN; + TextType text_type = TextType::TEXT; + mgl::Color color = mgl::Color(255, 255, 255, 255); + uint8_t text_flags = FORMATTED_TEXT_FLAG_NONE; // FormattedTextFlag // TODO: Remove these std::string url; @@ -78,17 +91,17 @@ namespace QuickMedia { public: Text(std::string str, bool bold_font, unsigned int characterSize, float maxWidth, bool highlight_urls = false); - Text(const Text &other); - Text& operator=(const Text&); void setString(std::string str); const std::string& getString() const; void appendText(const std::string &str); // size = {0, 0} = keep original image size - void append_image(const std::string &url, bool local, mgl::vec2i size); static std::string formatted_image(const std::string &url, bool local, mgl::vec2i size); - static std::string formatted_text(const std::string &text, mgl::Color color, bool bold); + // text_flags is bit-or of FormattedTextFlag + static std::string formatted_text(const std::string &text, mgl::Color color, uint8_t text_flags); void insert_text_at_caret_position(const std::string &str); + + static std::string to_printable_string(const std::string &str); void set_position(float x, float y); void set_position(const mgl::vec2f &position); @@ -104,7 +117,7 @@ namespace QuickMedia int getCaretIndex() const; int getNumLines() const; - void set_color(mgl::Color color); + void set_color(mgl::Color color, bool force_color = false); void setLineSpacing(float lineSpacing); void setCharacterSpacing(float characterSpacing); void setEditable(bool editable); @@ -151,7 +164,8 @@ namespace QuickMedia int getPreviousLineClosestPosition(int startIndex) const; int getNextLineClosestPosition(int startIndex) const; - void split_text_by_type(std::vector<TextElement> &text_elements, const std::string &str); + static void split_text_by_type(std::vector<TextElement> &text_elements, std::string_view str, float vspace); + void split_text_by_type(); float font_get_real_height(mgl::Font *font); float get_text_quad_left_side(const VertexRef &vertex_ref) const; @@ -164,14 +178,15 @@ namespace QuickMedia uint32_t get_vertex_codepoint(int index) const; size_t get_string_index_from_caret_index(size_t caret_index) const; private: - std::string str; // TODO: Remove this for non-editable text??? also replace with std::string? then we get more efficient editing of text + std::string str; // TODO: Remove this for non-editable text??? bool bold_font; unsigned int characterSize; - std::array<std::vector<mgl::Vertex>, 5> vertices; - std::array<mgl::VertexBuffer, 5> vertex_buffers; + std::array<std::vector<mgl::Vertex>, FONT_ARRAY_SIZE> vertices; + std::array<mgl::VertexBuffer, FONT_ARRAY_SIZE> vertex_buffers; float maxWidth; mgl::vec2f position; mgl::Color color; + bool force_color; bool dirty; bool dirtyText; bool dirtyCaret; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 11989e3..61814ac 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -24,6 +24,7 @@ namespace QuickMedia { std::string message_get_body_remove_formatting(Message *message); std::string extract_first_line_remove_newline_elipses(const std::string &str, size_t max_length); mgl::Color user_id_to_color(const std::string &user_id); + std::string formatted_text_to_qm_text(const char *str, size_t size); struct TimestampedDisplayData { std::string data; @@ -617,6 +618,7 @@ namespace QuickMedia { void update_room_users(RoomData *room); void append_system_message(RoomData *room_data, std::shared_ptr<Message> message); + std::string body_to_formatted_body(RoomData *room, const std::string &body); // Calls the |MatrixDelegate| pending events. // Should be called from the main (ui) thread @@ -627,7 +629,6 @@ namespace QuickMedia { void formatted_body_add_line(RoomData *room, std::string &formatted_body, const std::string &line_str); void replace_mentions(RoomData *room, std::string &text); - std::string body_to_formatted_body(RoomData *room, const std::string &body); std::string create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body); PluginResult set_pinned_events(RoomData *room, const std::vector<std::string> &pinned_events, bool is_add); diff --git a/src/Body.cpp b/src/Body.cpp index 5de0c56..32d5dd5 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -883,7 +883,7 @@ namespace QuickMedia { } else { body_item->description_text = std::make_unique<Text>(body_item->get_description(), false, std::floor(get_config().body.description_font_size * get_config().scale * get_config().font_scale), width, true); } - body_item->description_text->set_color(body_item->get_description_color()); + body_item->description_text->set_color(body_item->get_description_color(), body_item->force_description_color); body_item->description_text->updateGeometry(); } diff --git a/src/BodyItem.cpp b/src/BodyItem.cpp index dfd6302..64d18d4 100644 --- a/src/BodyItem.cpp +++ b/src/BodyItem.cpp @@ -72,6 +72,9 @@ namespace QuickMedia { title_color = other.title_color; author_color = other.author_color; description_color = other.description_color; + force_title_color = other.force_title_color; + force_description_color = other.force_description_color; + force_author_color = other.force_author_color; extra = other.extra; keep_alive_frames = other.keep_alive_frames; selectable = other.selectable; diff --git a/src/Config.cpp b/src/Config.cpp index 4ce65b5..6a73dcf 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -293,6 +293,7 @@ namespace QuickMedia { if(font_json.isObject()) { get_json_value_path(font_json, "latin", config->font.latin); get_json_value_path(font_json, "latin_bold", config->font.latin_bold); + get_json_value_path(font_json, "latin_monospace", config->font.latin_monospace); get_json_value_path(font_json, "cjk", config->font.cjk); get_json_value_path(font_json, "symbols", config->font.symbols); } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index bbff6c6..80ae146 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5308,7 +5308,7 @@ namespace QuickMedia { body_item->embedded_item->embedded_item = nullptr; body_item->embedded_item->reactions.clear(); if(message->user->user_id != my_user_id && ((related_body_item->userdata && static_cast<Message*>(related_body_item->userdata)->user.get() == me) || message_contains_user_mention(body_item->get_description(), my_display_name) || message_contains_user_mention(body_item->get_description(), my_user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; @@ -5322,7 +5322,7 @@ namespace QuickMedia { static std::shared_ptr<BodyItem> message_to_body_item(RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) { auto body_item = BodyItem::create(""); body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH)); - body_item->set_description(strip(message_get_body_remove_formatting(message))); + body_item->set_description(strip(formatted_text_to_qm_text(message->body.c_str(), message->body.size()))); body_item->set_timestamp(message->timestamp); if(!message->thumbnail_url.empty()) { body_item->thumbnail_url = message->thumbnail_url; @@ -5353,7 +5353,7 @@ namespace QuickMedia { body_item->thumbnail_url.clear(); } if(message->user->user_id != my_user_id && (message_contains_user_mention(body_item->get_description(), my_display_name) || message_contains_user_mention(body_item->get_description(), my_user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); return body_item; } @@ -5652,9 +5652,11 @@ namespace QuickMedia { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *reply_to_message = static_cast<Message*>(body_item->userdata); - body_item->set_description(strip(message_get_body_remove_formatting(message.get()))); + std::string qm_formatted_text = formatted_text_to_qm_text(reply_to_message->body.c_str(), reply_to_message->body.size()); + + body_item->set_description(std::move(qm_formatted_text)); if(message->user != me && (message_contains_user_mention(reply_to_message->body, my_display_name) || message_contains_user_mention(reply_to_message->body, me->user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); message->replaces = reply_to_message; @@ -5691,9 +5693,11 @@ namespace QuickMedia { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *reply_to_message = static_cast<Message*>(body_item->userdata); - body_item->set_description(strip(message_get_body_remove_formatting(message.get()))); + std::string qm_formatted_text = formatted_text_to_qm_text(reply_to_message->body.c_str(), reply_to_message->body.size()); + + body_item->set_description(std::move(qm_formatted_text)); if(message->user != me && (message_contains_user_mention(reply_to_message->body, my_display_name) || message_contains_user_mention(reply_to_message->body, me->user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); message->replaces = reply_to_message; @@ -6114,9 +6118,9 @@ namespace QuickMedia { auto message = std::make_shared<Message>(); message->user = matrix->get_me(current_room); if(msgtype == "m.emote") - message->body = "*" + current_room->get_user_display_name(me) + "* " + text; + message->body = "*" + current_room->get_user_display_name(me) + "* " + matrix->body_to_formatted_body(current_room, text); else - message->body = text; + message->body = matrix->body_to_formatted_body(current_room, text); message->type = MessageType::TEXT; message->timestamp = time(NULL) * 1000; @@ -6196,8 +6200,11 @@ namespace QuickMedia { size_t body_item_index = 0; auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), message->related_event_id, &body_item_index); if(body_item) { + const std::string formatted_text = matrix->body_to_formatted_body(current_room, text); + std::string qm_formatted_text = formatted_text_to_qm_text(formatted_text.c_str(), formatted_text.size()); + auto body_item_shared_ptr = tabs[MESSAGES_TAB_INDEX].body->get_item_by_index(body_item_index); - body_item_shared_ptr->set_description(text); + body_item_shared_ptr->set_description(std::move(qm_formatted_text)); body_item_shared_ptr->set_description_color(get_theme().provisional_message_color); auto edit_body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); @@ -6292,7 +6299,7 @@ namespace QuickMedia { *body_item = *related_body_item; body_item->reactions.clear(); if(message_contains_user_mention(related_body_item->get_description(), current_room->get_user_display_name(me)) || message_contains_user_mention(related_body_item->get_description(), me->user_id)) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); event_data->status = FetchStatus::FINISHED_LOADING; @@ -6928,8 +6935,10 @@ namespace QuickMedia { if(event.key.control && event.key.code == mgl::Keyboard::C) { BodyItem *selected = tabs[selected_tab].body->get_selected(); - if(selected) - set_clipboard(selected->get_description()); + if(selected) { + const std::string body_text_unformatted = Text::to_printable_string(selected->get_description()); + set_clipboard(body_text_unformatted); + } } if(selected_tab == MESSAGES_TAB_INDEX) { @@ -7058,10 +7067,12 @@ namespace QuickMedia { // TODO: Show inline notification show_notification("QuickMedia", "You can't edit a message that was posted by somebody else"); } else { + const std::string body_text_unformatted = Text::to_printable_string(selected->get_description()); + chat_state = ChatState::EDITING; currently_operating_on_item = selected; chat_input.set_editable(true); - chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited + chat_input.set_text(std::move(body_text_unformatted)); // TODO: Description? it may change in the future, in which case this should be edited chat_input.move_caret_to_end(); replying_to_text.set_string("Editing message:"); } @@ -7480,7 +7491,7 @@ namespace QuickMedia { fetch_body_item->embedded_item = message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); fetch_body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; if(fetch_message_result.message->user == me) - fetch_body_item->set_description_color(get_theme().attention_alert_text_color); + fetch_body_item->set_description_color(get_theme().attention_alert_text_color, true); } else { fetch_body_item->embedded_item_status = FetchStatus::FAILED_TO_LOAD; } diff --git a/src/ResourceLoader.cpp b/src/ResourceLoader.cpp index e086ee7..86687d2 100644 --- a/src/ResourceLoader.cpp +++ b/src/ResourceLoader.cpp @@ -12,9 +12,9 @@ #include <assert.h> static std::string resource_root; -static std::array<std::unique_ptr<mgl::MemoryMappedFile>, 4> font_file_cache; +static std::array<std::unique_ptr<mgl::MemoryMappedFile>, 5> font_file_cache; // font_cache[(unsigned int)font_type][character_size] -static std::array<std::unordered_map<unsigned int, std::unique_ptr<mgl::Font>>, 4> font_cache; +static std::array<std::unordered_map<unsigned int, std::unique_ptr<mgl::Font>>, 5> font_cache; static std::unordered_map<std::string, std::unique_ptr<mgl::Texture>> texture_cache; namespace QuickMedia { @@ -100,6 +100,23 @@ namespace QuickMedia::FontLoader { } break; } + case FontType::LATIN_MONOSPACE: { + const char *args[] = { "fc-match", "monospace:lang=en", "file", nullptr }; + if(get_config().use_system_fonts && exec_program(args, accumulate_string, &output) == 0 && output.size() > 6) { + Path path = strip(output.substr(6)); + noto_directories.push_back(path.parent().data); + font_file_name = path.filename(); + } else if(!get_config().font.latin_monospace.empty() && find_font(get_config().font.latin_monospace, found_font_filepath)) { + const Path font_path = found_font_filepath; + noto_directories.push_back(font_path.parent().data); + font_file_name = font_path.filename(); + } else { + noto_directories.push_back("/usr/share/fonts/noto"); + noto_directories.push_back("/usr/share/fonts/truetype/noto"); + font_file_name = "NotoSansMono-Regular.ttf"; + } + break; + } case FontType::CJK: { const char *args[] = { "fc-match", "sans:lang=ja", "file", nullptr }; if(get_config().use_system_fonts && exec_program(args, accumulate_string, &output) == 0 && output.size() > 6) { diff --git a/src/Text.cpp b/src/Text.cpp index 7692eef..e6fe90c 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -6,6 +6,7 @@ #include "../include/StringUtils.hpp" #include "../generated/Emoji.hpp" #include <string.h> +#include <stack> #include <mglpp/graphics/Rectangle.hpp> #include <mglpp/window/Event.hpp> #include <mglpp/window/Window.hpp> @@ -16,6 +17,8 @@ // TODO: Remove #include <sstream> +// TODO: text editing should take into consideration FORMATTED_TEXT_START/FORMATTED_TEXT_END. + namespace QuickMedia { static float floor(float v) { @@ -30,11 +33,11 @@ namespace QuickMedia static const float WORD_WRAP_MIN_SIZE = 80.0f; static const size_t FONT_INDEX_LATIN = 0; - static const size_t FONT_INDEX_CJK = 1; - static const size_t FONT_INDEX_SYMBOLS = 2; - static const size_t FONT_INDEX_EMOJI = 3; - static const size_t FONT_INDEX_IMAGE = 4; - static const size_t FONT_ARRAY_SIZE = 5; + static const size_t FONT_INDEX_MONOSPACE = 1; + static const size_t FONT_INDEX_CJK = 2; + static const size_t FONT_INDEX_SYMBOLS = 3; + static const size_t FONT_INDEX_EMOJI = 4; + static const size_t FONT_INDEX_IMAGE = 5; static const uint8_t FORMATTED_TEXT_START = '\x02'; static const uint8_t FORMATTED_TEXT_END = '\x03'; @@ -49,6 +52,7 @@ namespace QuickMedia characterSize(12), maxWidth(0.0f), color(get_theme().text_color), + force_color(false), dirty(true), dirtyText(true), dirtyCaret(true), @@ -69,6 +73,7 @@ namespace QuickMedia characterSize(characterSize), maxWidth(maxWidth), color(get_theme().text_color), + force_color(false), dirty(true), dirtyText(true), dirtyCaret(true), @@ -83,21 +88,6 @@ namespace QuickMedia { setString(std::move(_str)); } - - Text::Text(const Text &other) : Text(other.str, other.bold_font, other.characterSize, other.maxWidth, other.highlight_urls) { - - } - - Text& Text::operator=(const Text &other) { - str = other.str; - bold_font = other.bold_font; - characterSize = other.characterSize; - maxWidth = other.maxWidth; - highlight_urls = other.highlight_urls; - caretIndex = other.caretIndex; - position = other.position; - return *this; - } void Text::setString(std::string str) { @@ -126,25 +116,24 @@ namespace QuickMedia dirtyText = true; } - void Text::append_image(const std::string &url, bool local, mgl::vec2i size) { - str += Text::formatted_image(url, local, size); - } - // static std::string Text::formatted_image(const std::string &url, bool local, mgl::vec2i size) { + const uint32_t str_size = url.size(); std::string result; result += FORMATTED_TEXT_START; result += (uint8_t)FormattedTextType::IMAGE; result.append((const char*)&size.x, sizeof(size.x)); result.append((const char*)&size.y, sizeof(size.y)); result.append((const char*)&local, sizeof(local)); + result.append((const char*)&str_size, sizeof(str_size)); result.append(url); result += FORMATTED_TEXT_END; return result; } // static - std::string Text::formatted_text(const std::string &text, mgl::Color color, bool bold) { + std::string Text::formatted_text(const std::string &text, mgl::Color color, uint8_t text_flags) { + const uint32_t str_size = text.size(); std::string result; result += FORMATTED_TEXT_START; result += (uint8_t)FormattedTextType::TEXT; @@ -152,7 +141,8 @@ namespace QuickMedia result += color.g; result += color.b; result += color.a; - result += (uint8_t)bold; + result += text_flags; + result.append((const char*)&str_size, sizeof(str_size)); result.append(text); result += FORMATTED_TEXT_END; return result; @@ -163,6 +153,18 @@ namespace QuickMedia dirty = true; dirtyText = true; } + + // static + std::string Text::to_printable_string(const std::string &str) { + std::string result; + std::vector<TextElement> tmp_text_elements; + Text::split_text_by_type(tmp_text_elements, str, 0.0f); + for(auto &text_element : tmp_text_elements) { + if(text_element.type == TextElement::Type::TEXT) + result.append(text_element.text); + } + return result; + } void Text::set_position(float x, float y) { @@ -227,13 +229,17 @@ namespace QuickMedia return caretIndex; } - void Text::set_color(mgl::Color color) + void Text::set_color(mgl::Color color, bool force_color) { - if(color != this->color) - { + if(color != this->color) { this->color = color; dirty = true; } + + if(force_color != this->force_color) { + this->force_color = force_color; + dirty = true; + } } void Text::setLineSpacing(float lineSpacing) @@ -328,24 +334,6 @@ namespace QuickMedia return codepoint >= 0x2800 && codepoint <= 0x28FF; // Braille } - static size_t find_end_of_cjk(const char *str, size_t size) { - for(size_t i = 0; i < size;) { - const unsigned char *cp = (const unsigned char*)&str[i]; - uint32_t codepoint; - size_t clen; - if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { - codepoint = *cp; - clen = 1; - } - - if(!is_cjk_codepoint(codepoint)) - return i; - - i += clen; - } - return size; - } - // TODO: Optimize, dont use ostringstream static std::string codepoint_to_hex_str(uint32_t codepoint) { std::ostringstream ss; @@ -353,7 +341,11 @@ namespace QuickMedia return ss.str(); } - static size_t find_end_of_symbol(const char *str, size_t size) { + static size_t find_end_text(const char *str, size_t size) { + uint32_t emoji_sequence[32]; + size_t emoji_sequence_length = 0; + size_t emoji_byte_length = 0; + for(size_t i = 0; i < size;) { const unsigned char *cp = (const unsigned char*)&str[i]; uint32_t codepoint; @@ -363,60 +355,64 @@ namespace QuickMedia clen = 1; } - if(!is_symbol_codepoint(codepoint)) + if(codepoint == FORMATTED_TEXT_START || match_emoji_sequence(cp, size - i, emoji_sequence, emoji_sequence_length, emoji_byte_length)) return i; i += clen; } + return size; } - static size_t find_end_latin(const char *str, size_t size) { - uint32_t emoji_sequence[32]; - size_t emoji_sequence_length = 0; - size_t emoji_byte_length = 0; + static size_t parse_formatted_string(const char *str, size_t size, std::string_view &text, mgl::Color &color, uint8_t &flags) { + flags = FORMATTED_TEXT_FLAG_NONE; - for(size_t i = 0; i < size;) { - const unsigned char *cp = (const unsigned char*)&str[i]; - uint32_t codepoint; - size_t clen; - if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { - codepoint = *cp; - clen = 1; - } + if(size < 5 + sizeof(uint32_t)) + return size; - if(is_cjk_codepoint(codepoint) || is_symbol_codepoint(codepoint) || codepoint == FORMATTED_TEXT_START || match_emoji_sequence(cp, size - i, emoji_sequence, emoji_sequence_length, emoji_byte_length)) - return i; + color.r = str[0]; + color.g = str[1]; + color.b = str[2]; + color.a = str[3]; + flags |= (uint8_t)str[4]; - i += clen; - } + uint32_t text_size; + memcpy(&text_size, str + 5, sizeof(text_size)); - return size; + if(size < 5 + sizeof(uint32_t) + text_size) + return size; + + text = std::string_view(str + 5 + sizeof(uint32_t), text_size); + return std::min(5 + sizeof(uint32_t) + text_size + 1, size); // + 1 for FORMATTED_TEXT_END } -// TODO: -#if 0 static size_t parse_formatted_image(const char *str, size_t size, std::string &image_url, bool &image_local, mgl::vec2i &image_size) { image_url.clear(); image_local = true; image_size = { 0, 0 }; - for(size_t i = 0; i < size; ++i) { - const char *cp = &str[i]; - if(*cp == FORMATTED_TEXT_END) { - const size_t image_len = i; - if(image_len < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local)) - return size; - - memcpy(&image_size.x, str, sizeof(image_size.x)); - memcpy(&image_size.y, str + sizeof(image_size.x), sizeof(image_size.y)); - memcpy(&image_local, str + sizeof(image_size.x) + sizeof(image_size.y), sizeof(image_local)); - const size_t image_url_index = sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local); - image_url.assign(str + image_url_index, image_len - image_url_index); - return i + 1; - } - } - return size; + if(size < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local) + sizeof(uint32_t)) + return size; + + size_t offset = 0; + memcpy(&image_size.x, str, sizeof(image_size.x)); + offset += sizeof(image_size.x); + + memcpy(&image_size.y, str + offset, sizeof(image_size.y)); + offset += sizeof(image_size.y); + + memcpy(&image_local, str + offset, sizeof(image_local)); + offset += sizeof(image_local); + + uint32_t text_size; + memcpy(&text_size, str + offset, sizeof(text_size)); + offset += sizeof(text_size); + + if(size < offset + text_size) + return size; + + image_url.assign(str + offset, text_size); + return std::min(offset + text_size + 1, size); // + 1 for FORMATTED_TEXT_END } static size_t parse_formatted_text(const char *str, size_t size, TextElement &text_element) { @@ -427,8 +423,7 @@ namespace QuickMedia switch(formatted_text_type) { case FormattedTextType::TEXT: { text_element.type = TextElement::Type::TEXT; - // TODO: - //return parse_formatted_text(str + 1, size - 1, text_element) + return parse_formatted_string(str + 1, size - 1, text_element.text, text_element.color, text_element.text_flags); } case FormattedTextType::IMAGE: { text_element.type = TextElement::Type::IMAGE; @@ -439,17 +434,9 @@ namespace QuickMedia } return 0; } -#endif - void Text::split_text_by_type(std::vector<TextElement> &text_elements, const std::string &str) { - text_elements.clear(); - mgl::Font *latin_font; - if(bold_font) - latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize); - else - latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize); - - const float vspace = font_get_real_height(latin_font); + // static + void Text::split_text_by_type(std::vector<TextElement> &text_elements, std::string_view str, float vspace) { size_t index = 0; size_t size = str.size(); @@ -470,31 +457,27 @@ namespace QuickMedia TextElement text_element; - if(is_symbol_codepoint(codepoint)) { - offset = find_end_of_symbol(str.data() + index, size - index); - - text_element.create_text(std::string_view(str.data() + index, offset)); - text_element.text_type = TextElement::TextType::SYMBOL; - text_element.text_num_bytes = text_element.text.size(); - text_elements.push_back(std::move(text_element)); - } else if(is_cjk_codepoint(codepoint)) { - offset = find_end_of_cjk(str.data() + index, size - index); - - text_element.create_text(std::string_view(str.data() + index, offset)); - text_element.text_type = TextElement::TextType::CJK; - text_element.text_num_bytes = text_element.text.size(); - text_elements.push_back(std::move(text_element)); - } else if(codepoint == FORMATTED_TEXT_START) { - // TODO: - offset = 1; - #if 0 - text_element.type = TextElement::Type::IMAGE; - + if(codepoint == FORMATTED_TEXT_START) { index += 1; - offset = parse_formatted_text(str.data() + index, size - index, text_element); - text_element.text_num_bytes = ... // TODO - text_elements.push_back(std::move(text_element)); - #endif + offset = parse_formatted_text(str.data() + index, size - index, text_element) + 1; // TODO: + if(offset > 0) { + text_element.text_type = TextElement::TextType::TEXT; + if(text_element.type == TextElement::Type::TEXT) { + const std::string_view inside_text = text_element.text; + text_element.text = std::string_view("", 0); + text_element.text_num_bytes = 0; + text_element.type = TextElement::Type::FORMAT_START; + text_elements.push_back(text_element); + + split_text_by_type(text_elements, inside_text, vspace); + + text_element.type = TextElement::Type::FORMAT_END; + text_elements.push_back(std::move(text_element)); + } else { + text_element.text_num_bytes = 1; + text_elements.push_back(std::move(text_element)); + } + } } else if(match_emoji_sequence((const unsigned char*)str.data() + index, size - index, emoji_sequence, emoji_sequence_length, emoji_byte_length)) { offset = emoji_byte_length; @@ -510,19 +493,18 @@ namespace QuickMedia if(emoji_codepoint_combined == "1f441-fe0f-200d-1f5e8-fe0f") emoji_codepoint_combined = "1f441-200d-1f5e8"; - text_element.create_text("E"); + text_element.create_text(std::string_view(str.data() + index, offset)); text_element.text_type = TextElement::TextType::EMOJI; - text_element.text = "E"; text_element.url = "/usr/share/quickmedia/emoji/" + emoji_codepoint_combined + ".png"; text_element.local = true; text_element.size = { (int)vspace, (int)vspace }; text_element.text_num_bytes = emoji_byte_length; text_elements.push_back(std::move(text_element)); } else { - offset = find_end_latin(str.data() + index, size - index); + offset = find_end_text(str.data() + index, size - index); text_element.create_text(std::string_view(str.data() + index, offset)); - text_element.text_type = TextElement::TextType::LATIN; + text_element.text_type = TextElement::TextType::TEXT; text_element.text_num_bytes = text_element.text.size(); text_elements.push_back(std::move(text_element)); } @@ -533,6 +515,16 @@ namespace QuickMedia } } + void Text::split_text_by_type() { + mgl::Font *latin_font; + if(bold_font) + latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize); + else + latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize); + textElements.clear(); + split_text_by_type(textElements, str, font_get_real_height(latin_font)); + } + float Text::font_get_real_height(mgl::Font *font) { return font->get_glyph('|').size.y + floor(4.0f * ((float)characterSize / (float)14.0f)); } @@ -614,7 +606,7 @@ namespace QuickMedia if(dirtyText) { assert(dirty); dirtyText = false; - split_text_by_type(textElements, str); + split_text_by_type(); // TODO: Optimize if(highlight_urls) { url_ranges = extract_urls(str); @@ -646,8 +638,14 @@ namespace QuickMedia const float emoji_spacing = 2.0f; const mgl::Color url_color = get_theme().url_text_color; - size_t url_range_index = 0; + + struct TextFormat { + mgl::Color color; + uint8_t text_flags = FORMATTED_TEXT_FLAG_NONE; + }; + + std::stack<TextFormat> text_format_stack; mgl::vec2f glyphPos; uint32_t prevCodePoint = 0; @@ -656,11 +654,25 @@ namespace QuickMedia { TextElement &textElement = textElements[textElementIndex]; + mgl::Color text_element_color = color; + bool monospace = false; + if(!text_format_stack.empty()) { + if((text_format_stack.top().text_flags & FORMATTED_TEXT_FLAG_COLOR) && !force_color) + text_element_color = text_format_stack.top().color; + if(text_format_stack.top().text_flags & FORMATTED_TEXT_FLAG_MONOSPACE) + monospace = true; + } + mgl::Font *ff = latin_font; int vertices_index = FONT_INDEX_LATIN; - if(textElement.type == TextElement::Type::IMAGE) { + if(textElement.type == TextElement::Type::FORMAT_START) { + text_format_stack.push({ textElement.color, textElement.text_flags }); + } else if(textElement.type == TextElement::Type::FORMAT_END) { + if(!text_format_stack.empty()) + text_format_stack.pop(); + } else if(textElement.type == TextElement::Type::IMAGE) { vertices_index = FONT_INDEX_IMAGE; - mgl::Color image_color(255, 255, 255, color.a); + mgl::Color image_color(255, 255, 255, text_element_color.a); int vertexStart = vertices[vertices_index].size(); if(prevCodePoint != 0) @@ -696,15 +708,9 @@ namespace QuickMedia prevCodePoint = 0; continue; - } else if(textElement.text_type == TextElement::TextType::CJK) { - ff = FontLoader::get_font(FontLoader::FontType::CJK, characterSize); - vertices_index = FONT_INDEX_CJK; - } else if(textElement.text_type == TextElement::TextType::SYMBOL) { - ff = FontLoader::get_font(FontLoader::FontType::SYMBOLS, characterSize); - vertices_index = FONT_INDEX_SYMBOLS; } else if(textElement.text_type == TextElement::TextType::EMOJI) { vertices_index = FONT_INDEX_EMOJI; - mgl::Color emoji_color(255, 255, 255, color.a); + mgl::Color emoji_color(255, 255, 255, text_element_color.a); int vertexStart = vertices[vertices_index].size(); const mgl::vec2f emoji_size = { vspace, vspace }; @@ -746,12 +752,12 @@ namespace QuickMedia //vertices[vertices_index].resize(vertices[vertices_index].size() + 4 * textElement.text.size); // TODO: Precalculate for(size_t i = 0; i < textElement.text.size();) { - mgl::Color text_color = color; + mgl::Color text_color = text_element_color; if(url_range_index < url_ranges.size()) { size_t string_offset = (textElement.text.data() + i) - str.data(); if(string_offset >= url_ranges[url_range_index].start && string_offset < url_ranges[url_range_index].start + url_ranges[url_range_index].length) { text_color = url_color; - text_color.a = color.a; + text_color.a = text_element_color.a; if(string_offset + 1 == url_ranges[url_range_index].start + url_ranges[url_range_index].length) ++url_range_index; } @@ -764,6 +770,22 @@ namespace QuickMedia codepoint = *cp; clen = 1; } + + // TODO: CJK monospace + if(is_symbol_codepoint(codepoint)) { + ff = FontLoader::get_font(FontLoader::FontType::SYMBOLS, characterSize); + vertices_index = FONT_INDEX_SYMBOLS; + } else if(is_cjk_codepoint(codepoint)) { + ff = FontLoader::get_font(FontLoader::FontType::CJK, characterSize); + vertices_index = FONT_INDEX_CJK; + } else if(monospace) { + ff = FontLoader::get_font(FontLoader::FontType::LATIN_MONOSPACE, characterSize); + vertices_index = FONT_INDEX_MONOSPACE; + } else { + ff = latin_font; + vertices_index = FONT_INDEX_LATIN; + } + // TODO: Make this work when combining multiple different fonts (for example latin and japanese). // For japanese we could use a hack, because all japanese characters are monospace (exception being half-width characters). float kerning = ff->get_kerning(prevCodePoint, codepoint); @@ -1277,7 +1299,7 @@ namespace QuickMedia } std::vector<TextElement> new_text_elements; - split_text_by_type(new_text_elements, stringToAdd); + Text::split_text_by_type(new_text_elements, stringToAdd, 0.0f); for(auto &text_element : new_text_elements) { if(text_element.type == TextElement::Type::IMAGE || text_element.text_type == TextElement::TextType::EMOJI) { caretIndex += 1; @@ -1322,7 +1344,8 @@ namespace QuickMedia const float vspace = font_get_real_height(latin_font); pos.y += floor(vspace*0.25f); // Origin is at bottom left, we want it to be at top left - const FontLoader::FontType font_types[] = { latin_font_type, FontLoader::FontType::CJK, FontLoader::FontType::SYMBOLS }; + assert(FONT_ARRAY_SIZE == 6); + const FontLoader::FontType font_types[] = { latin_font_type, FontLoader::FontType::LATIN_MONOSPACE, FontLoader::FontType::CJK, FontLoader::FontType::SYMBOLS }; for(size_t i = 0; i < FONT_INDEX_EMOJI; ++i) { if(vertex_buffers[i].size() == 0) continue; @@ -1346,6 +1369,9 @@ namespace QuickMedia // TODO: Use a new vector with only the image data instead of this. // TODO: Sprite mgl::Sprite sprite; + mgl::Rectangle fallback_emoji(mgl::vec2f(vspace, vspace)); + fallback_emoji.set_color(get_theme().shade_color); + for(const TextElement &textElement : textElements) { if(textElement.text_type == TextElement::TextType::EMOJI) { auto emoji_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, { (int)vspace, (int)vspace }); @@ -1356,11 +1382,14 @@ namespace QuickMedia emoji_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; } - if(emoji_data->loading_state == LoadingState::APPLIED_TO_TEXTURE) { + if(emoji_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && emoji_data->texture.get_size().x > 0) { sprite.set_texture(&emoji_data->texture); sprite.set_position(pos + textElement.pos.to_vec2f()); sprite.set_size(textElement.size.to_vec2f()); target.draw(sprite); + } else { + fallback_emoji.set_position(pos + textElement.pos.to_vec2f()); + target.draw(fallback_emoji); } } } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 0fcc1c3..e4a7bd1 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -18,6 +18,7 @@ #include <unistd.h> #include <malloc.h> #include "../../include/QuickMedia.hpp" +#include <HtmlParser.h> // TODO: Use string assign with string length instead of assigning to c string (which calls strlen) // Show images/videos inline. @@ -598,15 +599,15 @@ namespace QuickMedia { } static std::string message_to_room_description_text(Message *message) { - std::string body = strip(message->body); + std::string body = strip(formatted_text_to_qm_text(message->body.c_str(), message->body.size())); if(message->type == MessageType::REACTION) - return "Reacted with: " + extract_first_line_remove_newline_elipses(body, 150); + return "Reacted with: " + body; else if(message->related_event_type == RelatedEventType::REPLY) - return extract_first_line_remove_newline_elipses(remove_reply_formatting(body), 150); + return body; else if(message->related_event_type == RelatedEventType::EDIT) - return "Edited: " + extract_first_line_remove_newline_elipses(remove_reply_formatting(body), 150); + return "Edited: " + body; else - return extract_first_line_remove_newline_elipses(body, 150); + return body; } void MatrixQuickMedia::update_room_description(RoomData *room, const Messages &new_messages, bool is_initial_sync, bool sync_is_cache) { @@ -672,13 +673,13 @@ namespace QuickMedia { if(!room_desc.empty()) room_desc += '\n'; room_desc += "** " + std::to_string(unread_notification_count) + " unread mention(s) **"; // TODO: Better notification? - room->body_item->set_description_color(get_theme().attention_alert_text_color); + room->body_item->set_description_color(get_theme().attention_alert_text_color, true); } else { room->body_item->set_description_color(get_theme().faded_text_color); } room->body_item->set_description(std::move(room_desc)); if(set_room_as_unread) - room->body_item->set_title_color(get_theme().attention_alert_text_color); + room->body_item->set_title_color(get_theme().attention_alert_text_color, true); room->last_message_read = false; rooms_page->move_room_to_top(room); @@ -1085,8 +1086,8 @@ namespace QuickMedia { body_item->url = notification.event_id; if(!notification.read) { - body_item->set_author_color(get_theme().attention_alert_text_color); - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_author_color(get_theme().attention_alert_text_color, true); + body_item->set_description_color(get_theme().attention_alert_text_color, true); } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; @@ -2232,6 +2233,115 @@ namespace QuickMedia { return result; } + // Returns -1 if its not a hex value + static int get_hex_value(char c) { + if(c >= '0' && c <= '9') + return c - '0'; + else if(c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + else if(c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + else + return -1; + } + + // Parses hex colors in the format #RRGGBB(AA) + static bool parse_hex_set_color(const char *str, int size, mgl::Color &color) { + if(size == 0) + return false; + + // #RRGGBB(AA), case insensitive hex + if(str[0] != '#') + return false; + + if(size - 1 != 6 && size - 1 != 8) + return false; + + mgl::Color new_color; + for(int i = 1; i < size; i += 2) { + const int c1 = get_hex_value(str[i + 0]); + const int c2 = get_hex_value(str[i + 1]); + if(c1 == -1 || c2 == -1) + return false; + (&new_color.r)[(i - 1)/2] = (c1 << 4) | c2; + } + color = new_color; + return true; + } + + struct FormattedTextParseUserdata { + std::string result; + int mx_reply_depth = 0; + bool inside_font_tag = false; + bool font_tag_has_custom_color = false; + bool inside_code_tag = false; + mgl::Color font_color = mgl::Color(255, 255, 255, 255); + }; + + // TODO: Full proper parsing with tag depth + static int formattext_text_parser_callback(HtmlParser *html_parser, HtmlParseType parse_type, void *userdata) { + FormattedTextParseUserdata &parse_userdata = *(FormattedTextParseUserdata*)userdata; + switch(parse_type) { + case HTML_PARSE_TAG_START: { + if(html_parser->tag_name.size == 2 && memcmp(html_parser->tag_name.data, "br", 2) == 0) + parse_userdata.result += '\n'; + else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "font", 4) == 0) + parse_userdata.inside_font_tag = true; + else if(html_parser->tag_name.size == 8 && memcmp(html_parser->tag_name.data, "mx-reply", 8) == 0) + ++parse_userdata.mx_reply_depth; + else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) + parse_userdata.inside_code_tag = true; + break; + } + case HTML_PARSE_TAG_END: { + /*if(html_parser->tag_name.size == 2 && memcmp(html_parser->tag_name.data, "br", 2) == 0) { + parse_userdata.result += '\n'; + } else */if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "font", 4) == 0) { + parse_userdata.inside_font_tag = false; + parse_userdata.font_tag_has_custom_color = false; + } else if(html_parser->tag_name.size == 8 && memcmp(html_parser->tag_name.data, "mx-reply", 8) == 0) { + parse_userdata.mx_reply_depth = std::max(0, parse_userdata.mx_reply_depth - 1); + } else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) { + parse_userdata.inside_code_tag = false; + } + break; + } + case HTML_PARSE_ATTRIBUTE: { + if(parse_userdata.inside_font_tag && html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "color", 5) == 0) { + if(parse_hex_set_color(html_parser->attribute_value.data, html_parser->attribute_value.size, parse_userdata.font_color)) + parse_userdata.font_tag_has_custom_color = true; + } + break; + } + case HTML_PARSE_TEXT: + case HTML_PARSE_JAVASCRIPT_CODE: { + if(parse_userdata.mx_reply_depth == 0) { + std::string text_to_add(html_parser->text.data, html_parser->text.size); + html_unescape_sequences(text_to_add); + + uint8_t formatted_text_flags = FORMATTED_TEXT_FLAG_NONE; + if(parse_userdata.font_tag_has_custom_color) + formatted_text_flags |= FORMATTED_TEXT_FLAG_COLOR; + if(parse_userdata.inside_code_tag) + formatted_text_flags |= FORMATTED_TEXT_FLAG_MONOSPACE; + + if(formatted_text_flags != FORMATTED_TEXT_FLAG_NONE) + parse_userdata.result += Text::formatted_text(text_to_add, parse_userdata.font_color, formatted_text_flags); + else + parse_userdata.result += std::move(text_to_add); + } + break; + } + } + return 0; + } + + std::string formatted_text_to_qm_text(const char *str, size_t size) { + FormattedTextParseUserdata parse_userdata; + html_parser_parse(str, size, formattext_text_parser_callback, &parse_userdata); + return std::move(parse_userdata.result); + } + std::shared_ptr<Message> Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) { if(!event_item_json.IsObject()) return nullptr; @@ -2612,6 +2722,10 @@ namespace QuickMedia { if(!body_json.IsString()) return nullptr; + const rapidjson::Value *formatted_body_json = &GetMember(*content_json, "formatted_body"); + if(!formatted_body_json->IsString()) + formatted_body_json = &body_json; + auto message = std::make_shared<Message>(); std::string prefix; @@ -2678,7 +2792,7 @@ namespace QuickMedia { message->user = user; message->event_id = event_id_str; - message->body = prefix + body_json.GetString(); + message->body = prefix + std::string(formatted_body_json->GetString(), formatted_body_json->GetStringLength()); message->related_event_id = std::move(related_event_id); message->related_event_type = related_event_type; message->timestamp = timestamp; diff --git a/tests/main.cpp b/tests/main.cpp index 41ef45d..c076e14 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -2,6 +2,7 @@ #include <string.h> #include "../include/NetUtils.hpp" #include "../plugins/utils/EpisodeNameParser.hpp" +#include "../plugins/Matrix.hpp" #include "../generated/Emoji.hpp" #define assert_fail(str) do { fprintf(stderr, "Assert failed on line %d, reason: %s\n", __LINE__, (str)); exit(1); } while(0) @@ -112,5 +113,11 @@ int main() { assert_equals(emoji_sequence_length, 4); assert_equals(emoji_sequence_byte_length, 13); + const std::string formatted_text = "<font color=\"#789922\">\n<p>>amazin<br>>asd</p>\n</font>\n<p>feaf</p>\n"; + const std::string qm_text = formatted_text_to_qm_text(formatted_text.c_str(), formatted_text.size()); + for(char c : qm_text) { + fprintf(stderr, "%c(%02x) ", c, *(uint8_t*)&c); + } + return 0; } |