// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. // For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md #include "RichTextParser.hpp" #include "Color.hpp" #include "Font.hpp" #include "TextBlock.hpp" #include "TextDocument.hpp" #include #include #include #include #include "TextFormat.hpp" #include #include #include #ifdef DEBUG_RTP const std::string node_types[] = {"null", "document", "element", "pcdata ", "cdata", "comment", "pi", "declaration"}; #define log_parser(...) LOG_DEBUG(__VA_ARGS__) #else #define log_parser(...) #endif namespace text { class Attribute { std::string name; protected: explicit Attribute(std::string name) : name(std::move(name)){}; public: Attribute() = delete; /// return true on success, otherwise set fallback value and return false /// @note could run: preVisitHook -> visit -> postVisit hook virtual auto visit(gui::TextFormat &fmt, std::string value) -> bool = 0; [[nodiscard]] auto getName() const -> const std::string & { return name; } [[nodiscard]] auto is(const std::string &name) const { return getName() == name; } }; class AttributeAlign : public Attribute { const std::string center = gui::text::center; const std::string left = gui::text::left; const std::string right = gui::text::right; public: AttributeAlign() : Attribute(gui::text::align) {} auto visit(gui::TextFormat &fmt, std::string value) -> bool final { log_parser("append: Attr %s", getName().c_str()); using namespace gui; auto alignment = Alignment(Alignment::Horizontal::Left, Alignment::Vertical::Bottom); if (value == center) { alignment.horizontal = Alignment::Horizontal::Center; fmt.setAlignment(alignment); return true; } else if (value == right) { alignment.horizontal = Alignment::Horizontal::Right; fmt.setAlignment(alignment); return true; } else if (value == left) { alignment.horizontal = Alignment::Horizontal::Left; fmt.setAlignment(alignment); return true; } fmt.setAlignment(alignment); return false; } }; class AttributeColor : public Attribute { const int max = 15; public: AttributeColor() : Attribute(gui::text::color) {} auto visit(gui::TextFormat &fmt, std::string value) -> bool final { log_parser("append: Attr %s", getName().c_str()); using namespace gui; if (value.empty()) { fmt.setColor(ColorFullBlack); return false; } try { int val = std::stoi(value); if ((val == 0 && value.c_str()[0] != '0') || val > max) { fmt.setColor(ColorFullBlack); return false; } fmt.setColor(Color{static_cast(val), 0}); return true; } catch (const std::exception &exception) { LOG_ERROR("%s", exception.what()); return false; } return false; } }; class AttributeFont : public Attribute { public: AttributeFont() : Attribute(gui::text::font) {} auto visit(gui::TextFormat &fmt, std::string value) -> bool final { log_parser("append: Attr %s", getName().c_str()); using namespace gui; // here it's tricky -> we need to get font copy from fmt -> change it -> set it auto font = gui::Font(fmt.getFont()); font.setFont(value, font.getSize()); fmt.setFont(font.raw()); return true; } }; class AttributeSize : public Attribute { public: AttributeSize() : Attribute(gui::text::size) {} auto visit(gui::TextFormat &fmt, std::string value) -> bool final { log_parser("append: Attr %s", getName().c_str()); if (value.empty()) { return false; } try { int val = std::stoi(value); if (val == 0 && value.front() != '0') { return false; } auto font = gui::Font(fmt.getFont()); font.setSize(val); fmt.setFont(font.raw()); return true; } catch (const std::exception &exception) { LOG_ERROR("%s", exception.what()); return false; } } }; class AttributeWeight : public Attribute { protected: const std::string regular = "regular"; const std::string bold = "bold"; const std::string light = "light"; public: AttributeWeight() : Attribute(gui::text::weight) {} auto visit(gui::TextFormat &fmt, std::string value) -> bool final { log_parser("append: Attr %s", getName().c_str()); using namespace gui; auto font = gui::Font(fmt.getFont()); if (value == regular) { font.setWeight(Font::Weight::Regular); } else if (value == light) { font.setWeight(Font::Weight::Light); } else if (value == bold) { font.setWeight(Font::Weight::Bold); } else { font.setWeight(Font::Weight::Regular); fmt.setFont(font.raw()); return false; } fmt.setFont(font.raw()); return true; } }; class NodeDecor { std::list> attrs; NodeDecor() { attrs.emplace_back(std::make_unique()); attrs.emplace_back(std::make_unique()); attrs.emplace_back(std::make_unique()); attrs.emplace_back(std::make_unique()); attrs.emplace_back(std::make_unique()); } public: // for each met style -> put it on stack to be used // too deep -> can be optimized auto stack_visit(gui::TextFormat &format, const std::string &name, const std::string &value) -> bool { for (auto &attr : attrs) { if (attr->is(name)) { if (!attr->visit(format, value)) { LOG_ERROR("Attribute parsing error, default set"); } return true; } } LOG_ERROR("Attribute not found"); return false; } static auto get() -> NodeDecor & { static NodeDecor *ptr = nullptr; if (ptr == nullptr) { ptr = new NodeDecor(); } return *ptr; } }; class ShortTextNodes { using SingleAttributedNode = std::map>; static const SingleAttributedNode nodes; public: enum class AttributeContent { Name, Value }; [[nodiscard]] static auto is(const char *nodeName) -> bool { return static_cast(nodes.count(std::string(nodeName))); } [[nodiscard]] static auto get(const char *nodeName, AttributeContent content) -> std::optional { try { auto attribute = nodes.at(nodeName); return std::optional(content == AttributeContent::Name ? attribute.first : attribute.second); } catch (const std::out_of_range &) { LOG_ERROR("ShortTextNode not found"); return {}; } } }; const ShortTextNodes::SingleAttributedNode ShortTextNodes::nodes = { {gui::text::short_bold, {gui::text::weight, gui::text::bold}}}; class CustomTokens { public: using TokenMap = std::map>; explicit CustomTokens(TokenMap &&_tokens) : tokens{std::move(_tokens)} {} [[nodiscard]] static auto isCustomTokenNode(const std::string &nodeName) -> bool { return nodeName == gui::text::node_token; } [[nodiscard]] auto get(const std::string &contentName) -> std::optional { try { auto token = tokens.at(contentName); return std::visit( [](auto &&arg) { using T = std::decay_t; if constexpr (std::is_same_v) { return std::make_optional(utils::to_string(arg)); } else if constexpr (std::is_same_v) { return std::make_optional(arg); } else { return std::nullopt; } }, std::move(token)); } catch (const std::out_of_range &) { LOG_ERROR("Tokens not found"); } return std::nullopt; } private: TokenMap tokens; }; }; // namespace text struct walker : pugi::xml_tree_walker { protected: std::list blocks; std::list style_stack; text::CustomTokens tokens; bool add_empty_line = false; bool adding_tokens = false; public: walker(gui::TextFormat entry_style, ::text::CustomTokens::TokenMap &&tokens) : tokens{std::move(tokens)} { style_stack.push_back(entry_style); } enum class Action { Enter, /// enter/visit node Exit /// exit/leave node }; auto log_node(pugi::xml_node &node, Action dir) { log_parser( "%s: %s format: %s", dir == Action::Enter ? "enter" : "leave", [&]() { std::stringstream ss; ss << node_types[node.type()] << ": name='" << node.name() << "', value='" << node.value() << "'"; return ss.str(); }() .c_str(), style_stack.back().str().c_str()); } auto is_newline_node(pugi::xml_node &node) const { return std::string(node.name()) == gui::text::node_br || std::string(node.name()) == gui::text::node_p; } auto is_short_text_node(pugi::xml_node &node) const { return text::ShortTextNodes::is(node.name()); } auto is_custom_token_node(pugi::xml_node &node) const { return text::CustomTokens::isCustomTokenNode(node.name()); } auto push_text_node(pugi::xml_node &node) { auto local_style = style_stack.back(); for (auto &attribute : node.attributes()) { log_parser("attribute name: %s value: %s", attribute.name(), attribute.value()); auto &decor = text::NodeDecor::get(); decor.stack_visit(local_style, attribute.name(), attribute.value()); } style_stack.push_back(local_style); log_parser("Attr loaded: %s", style_stack.back().str().c_str()); } auto push_short_text_node(pugi::xml_node &node) { log_parser("shortened text node name: %s", node.name()); auto local_style = style_stack.back(); auto &decor = text::NodeDecor::get(); auto attrName = text::ShortTextNodes::get(node.name(), text::ShortTextNodes::AttributeContent::Name); auto attrValue = text::ShortTextNodes::get(node.name(), text::ShortTextNodes::AttributeContent::Value); if (attrName.has_value() && attrValue.has_value()) { decor.stack_visit(local_style, attrName.value(), attrValue.value()); style_stack.push_back(local_style); log_parser("Attr loaded: %s", style_stack.back().str().c_str()); } } auto push_newline_node(pugi::xml_node &) { if (!blocks.empty()) { if (blocks.back().getEnd() != gui::TextBlock::End::Newline) { blocks.back().setEnd(gui::TextBlock::End::Newline); add_empty_line = false; } else { add_empty_line = true; } } else { add_empty_line = true; } } auto start_custom_token_node(pugi::xml_node &) { adding_tokens = true; } auto push_custom_token_data_node(pugi::xml_node &node) { auto value = tokens.get(node.value()); if (value.has_value()) { blocks.emplace_back(value.value(), std::make_unique(style_stack.back())); } } auto push_data_node(pugi::xml_node &node) { blocks.emplace_back(node.value(), std::make_unique(style_stack.back())); } auto for_each(pugi::xml_node &node) -> bool final { log_node(node, Action::Enter); if (node.type() == pugi::xml_node_type::node_element) { if (std::string(node.name()) == gui::text::node_text) { push_text_node(node); return true; } if (is_short_text_node(node)) { push_short_text_node(node); return true; } if (is_newline_node(node)) { push_newline_node(node); return true; } if (is_custom_token_node(node)) { start_custom_token_node(node); return true; } } std::string to_show = node.value(); if (node.type() == pugi::xml_node_type::node_pcdata && !to_show.empty()) { if (adding_tokens) { push_custom_token_data_node(node); } else { push_data_node(node); } } return true; } auto pop_text_node(pugi::xml_node &node) { style_stack.pop_back(); } auto pop_newline_node(pugi::xml_node &node) { if (add_empty_line) { blocks.emplace_back(gui::TextBlock("", std::make_unique(style_stack.back()))); add_empty_line = false; } if (!blocks.empty()) { blocks.back().setEnd(gui::TextBlock::End::Newline); } } auto end_custom_token_node(pugi::xml_node &node) { adding_tokens = false; } auto on_leave(pugi::xml_node &node) -> bool final { log_node(node, Action::Exit); if (node.type() == pugi::xml_node_type::node_element) { if (std::string(node.name()) == gui::text::node_text || is_short_text_node(node)) { pop_text_node(node); return true; } if (is_newline_node(node)) { pop_newline_node(node); return true; } if (is_custom_token_node(node)) { end_custom_token_node(node); } } return true; } auto end(pugi::xml_node &node) -> bool final { return true; } auto souvenirs() -> std::list & { return blocks; } }; namespace gui::text { auto RichTextParser::parse(const UTF8 &text, TextFormat *base_style, TokenMap &&tokenMap) -> std::unique_ptr { log_parser("parsing: %s", text.c_str()); if (text.empty()) { log_parser("no: %s", "text"); return nullptr; } pugi::xml_document doc; walker walker(base_style == nullptr ? nullptr : *base_style, std::move(tokenMap)); doc.load_string(text.c_str()); doc.traverse(walker); return std::make_unique(walker.souvenirs()); } }; // namespace gui::text