From d012ad08f874f1d90c604ec79f0741fa7fefbc71 Mon Sep 17 00:00:00 2001 From: Nik Date: Wed, 20 Aug 2025 20:37:44 +0200 Subject: [PATCH] feat: Added proper Markdown renderer (#2415) --- .github/workflows/build.yml | 3 + .gitmodules | 3 + cmake/build_helpers.cmake | 11 + dist/AppImage/AppImageBuilder.yml | 3 + dist/Arch/Dockerfile | 3 +- dist/DEBIAN/control.in | 2 +- dist/ImHex-9999.ebuild | 1 + dist/flatpak/net.werwolv.ImHex.yaml | 12 + dist/get_deps_archlinux.sh | 3 +- dist/get_deps_debian.sh | 4 +- dist/get_deps_fedora.sh | 2 +- dist/get_deps_msys2.sh | 3 +- dist/get_deps_tumbleweed.sh | 3 +- dist/macOS/Brewfile | 3 +- dist/msys2/PKGBUILD | 3 +- dist/rpm/imhex.spec | 1 + dist/snap/snapcraft.yaml | 2 + dist/vcpkg.json | 3 +- lib/external/libwolv | 2 +- lib/external/pattern_language | 2 +- lib/third_party/md4c | 1 + .../source/content/views/view_about.cpp | 68 +-- plugins/ui/CMakeLists.txt | 2 + plugins/ui/include/ui/markdown.hpp | 55 +++ plugins/ui/source/ui/markdown.cpp | 418 ++++++++++++++++++ 25 files changed, 544 insertions(+), 69 deletions(-) create mode 160000 lib/third_party/md4c create mode 100644 plugins/ui/include/ui/markdown.hpp create mode 100644 plugins/ui/source/ui/markdown.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dadd295b..dad548936 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,6 +91,7 @@ jobs: -DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \ -DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \ -DUSE_SYSTEM_CAPSTONE=ON \ + -DUSE_SYSTEM_MD4C=ON \ -DIMHEX_GENERATE_PDBS=ON \ -DIMHEX_REPLACE_DWARF_WITH_PDB=ON \ -DDOTNET_EXECUTABLE="C:/Program Files/dotnet/dotnet.exe" \ @@ -367,6 +368,7 @@ jobs: -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ -DIMHEX_USE_DEFAULT_BUILD_SETTINGS=ON \ -DUSE_SYSTEM_CAPSTONE=ON \ + -DUSE_SYSTEM_MD4C=ON \ .. ninja @@ -885,6 +887,7 @@ jobs: -DUSE_SYSTEM_YARA=ON \ -DUSE_SYSTEM_NLOHMANN_JSON=ON \ -DUSE_SYSTEM_CAPSTONE=OFF \ + -DUSE_SYSTEM_MD4C=ON \ -DIMHEX_PATTERNS_PULL_MASTER=ON \ -DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \ -DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \ diff --git a/.gitmodules b/.gitmodules index c678d3e9d..08f23d7f0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -47,3 +47,6 @@ [submodule "lib/external/disassembler"] path = lib/external/disassembler url = https://github.com/WerWolv/Disassembler +[submodule "lib/third_party/md4c"] + path = lib/third_party/md4c + url = https://github.com/mity/md4c diff --git a/cmake/build_helpers.cmake b/cmake/build_helpers.cmake index 1483bb19a..bb7721b4c 100644 --- a/cmake/build_helpers.cmake +++ b/cmake/build_helpers.cmake @@ -866,6 +866,17 @@ macro(addBundledLibraries) set(LUNASVG_LIBRARIES lunasvg::lunasvg) endif() + if (NOT USE_SYSTEM_MD4C) + set(BUILD_MD2HTML_EXECUTABLE OFF CACHE BOOL "Disable md2html executable" FORCE) + add_subdirectory(${THIRD_PARTY_LIBS_FOLDER}/md4c EXCLUDE_FROM_ALL) + add_library(md4c_lib INTERFACE) + add_library(md4c::md4c ALIAS md4c_lib) + target_include_directories(md4c_lib INTERFACE ${THIRD_PARTY_LIBS_FOLDER}/md4c/src) + target_link_libraries(md4c_lib INTERFACE md4c) + else() + find_package(md4c REQUIRED) + endif() + if (NOT USE_SYSTEM_LLVM) add_subdirectory(${THIRD_PARTY_LIBS_FOLDER}/llvm-demangle EXCLUDE_FROM_ALL) else() diff --git a/dist/AppImage/AppImageBuilder.yml b/dist/AppImage/AppImageBuilder.yml index 73a72aa36..af8b2b839 100644 --- a/dist/AppImage/AppImageBuilder.yml +++ b/dist/AppImage/AppImageBuilder.yml @@ -32,6 +32,8 @@ AppDir: - libpcre3 - libselinux1 - libtinfo6 + - libmd4c-dev + - libmd4c-html0-dev files: include: - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libLLVM-13.so.1" @@ -121,6 +123,7 @@ AppDir: - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libxml2.so.2" - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libxshmfence.so.1" - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libzstd.so.1" + - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libmd4c.so" exclude: - usr/share/man - usr/share/doc/*/README.* diff --git a/dist/Arch/Dockerfile b/dist/Arch/Dockerfile index c11c33a04..8da4c8583 100644 --- a/dist/Arch/Dockerfile +++ b/dist/Arch/Dockerfile @@ -18,7 +18,8 @@ RUN pacman -S --needed --noconfirm \ curl \ dbus \ xdg-desktop-portal \ - libssh2 + libssh2 \ + md4c # Clone ImHex RUN git clone https://github.com/WerWolv/ImHex --recurse-submodules /root/ImHex diff --git a/dist/DEBIAN/control.in b/dist/DEBIAN/control.in index 72900076c..4408d6ae6 100644 --- a/dist/DEBIAN/control.in +++ b/dist/DEBIAN/control.in @@ -4,7 +4,7 @@ Section: editors Priority: optional Architecture: amd64 License: GNU GPL-2 -Depends: libfontconfig1, libglfw3 | libglfw3-wayland, libmagic1, libmbedtls14, libfreetype6, libopengl0, libdbus-1-3, xdg-desktop-portal, libssh2-1 +Depends: libfontconfig1, libglfw3 | libglfw3-wayland, libmagic1, libmbedtls14, libfreetype6, libopengl0, libdbus-1-3, xdg-desktop-portal, libssh2-1, md4c Maintainer: WerWolv Description: ImHex Hex Editor A Hex Editor for Reverse Engineers, Programmers and diff --git a/dist/ImHex-9999.ebuild b/dist/ImHex-9999.ebuild index ffc0c6af6..8dcc15dac 100644 --- a/dist/ImHex-9999.ebuild +++ b/dist/ImHex-9999.ebuild @@ -29,6 +29,7 @@ RDEPEND="${DEPEND} app-arch/zstd app-arch/lz4 net-libs/libssh2 + dev-libs/md4c " BDEPEND="${DEPEND} dev-cpp/nlohmann_json diff --git a/dist/flatpak/net.werwolv.ImHex.yaml b/dist/flatpak/net.werwolv.ImHex.yaml index 0aa04937a..ab8f2c09a 100644 --- a/dist/flatpak/net.werwolv.ImHex.yaml +++ b/dist/flatpak/net.werwolv.ImHex.yaml @@ -100,6 +100,18 @@ modules: - /lib/cmake - /lib64/cmake + - name: md4c + buildsystem: cmake-ninja + config-opts: + - -DBUILD_SHARED_LIBS=ON + - -DMD4C_BUILD_TESTS=OFF + - -DMD4C_BUILD_EXAMPLES=OFF + - -DMD4C_BUILD_DOCS=OFF + sources: + - type: git + url: https://github.com/mity/md4c.git + tag: release-0.5.2 + - name: imhex buildsystem: cmake-ninja builddir: true diff --git a/dist/get_deps_archlinux.sh b/dist/get_deps_archlinux.sh index a41611e97..910384519 100755 --- a/dist/get_deps_archlinux.sh +++ b/dist/get_deps_archlinux.sh @@ -21,4 +21,5 @@ pacman -S $@ --needed \ xz \ zstd \ lz4 \ - libssh2 + libssh2 \ + md4c diff --git a/dist/get_deps_debian.sh b/dist/get_deps_debian.sh index 64f6e9992..3bf0925b5 100755 --- a/dist/get_deps_debian.sh +++ b/dist/get_deps_debian.sh @@ -29,4 +29,6 @@ apt install -y \ liblzma-dev \ libzstd-dev \ liblz4-dev \ - libssh2-1-dev + libssh2-1-dev \ + libmd4c-dev \ + libmd4c-html0-dev diff --git a/dist/get_deps_fedora.sh b/dist/get_deps_fedora.sh index f2caa4b08..4718ce1a5 100755 --- a/dist/get_deps_fedora.sh +++ b/dist/get_deps_fedora.sh @@ -19,4 +19,4 @@ dnf install -y \ bzip2-devel \ xz-devel \ lz4-devel \ - libssh2-devel \ No newline at end of file + libssh2-devel diff --git a/dist/get_deps_msys2.sh b/dist/get_deps_msys2.sh index 4d16d2a7f..161ed6ff7 100755 --- a/dist/get_deps_msys2.sh +++ b/dist/get_deps_msys2.sh @@ -19,4 +19,5 @@ pacboy -S --needed --noconfirm \ xz:p \ zstd:p \ lz4:p \ - libssh2-wincng:p + libssh2-wincng:p \ + md4c:p diff --git a/dist/get_deps_tumbleweed.sh b/dist/get_deps_tumbleweed.sh index a3ef5c4bf..5dff2ab87 100755 --- a/dist/get_deps_tumbleweed.sh +++ b/dist/get_deps_tumbleweed.sh @@ -19,4 +19,5 @@ zypper install \ bzip3-devel \ xz-devel \ lz4-dev \ - libssh2-devel + libssh2-devel \ + md4c-devel diff --git a/dist/macOS/Brewfile b/dist/macOS/Brewfile index 96b4ce2e7..820d35ad6 100644 --- a/dist/macOS/Brewfile +++ b/dist/macOS/Brewfile @@ -13,4 +13,5 @@ brew "zlib" brew "xz" brew "bzip2" brew "zstd" -brew "libssh2" \ No newline at end of file +brew "libssh2" +brew "md4c" \ No newline at end of file diff --git a/dist/msys2/PKGBUILD b/dist/msys2/PKGBUILD index 13d03cb05..1cab8795b 100644 --- a/dist/msys2/PKGBUILD +++ b/dist/msys2/PKGBUILD @@ -21,7 +21,8 @@ makedepends=("${MINGW_PACKAGE_PREFIX}-gcc" "${MINGW_PACKAGE_PREFIX}-bzip2" "${MINGW_PACKAGE_PREFIX}-xz" "${MINGW_PACKAGE_PREFIX}-zstd" - "${MINGW_PACKAGE_PREFIX}-libssh2-wincng") + "${MINGW_PACKAGE_PREFIX}-libssh2-wincng" + "${MINGW_PACKAGE_PREFIX}-md4c") source=() sha256sums=() diff --git a/dist/rpm/imhex.spec b/dist/rpm/imhex.spec index bac97d51d..ff672d25d 100644 --- a/dist/rpm/imhex.spec +++ b/dist/rpm/imhex.spec @@ -108,6 +108,7 @@ CXXFLAGS+=" -std=gnu++2b" -D USE_SYSTEM_FMT=ON \ -D USE_SYSTEM_CURL=ON \ -D USE_SYSTEM_LLVM=ON \ + -D USE_SYSTEM_MD4C=OFF \ %if 0%{?fedora} || 0%{?rhel} > 9 -D USE_SYSTEM_CAPSTONE=ON \ %endif diff --git a/dist/snap/snapcraft.yaml b/dist/snap/snapcraft.yaml index 958feab09..a0894115f 100644 --- a/dist/snap/snapcraft.yaml +++ b/dist/snap/snapcraft.yaml @@ -62,6 +62,8 @@ parts: - libzstd-dev - liblz4-dev - libssh2-1-dev + - libmd4c-dev + - libmd4c-html0-dev stage-packages: - libglfw3 - libmagic1 diff --git a/dist/vcpkg.json b/dist/vcpkg.json index ad3f4be61..37e935a4f 100644 --- a/dist/vcpkg.json +++ b/dist/vcpkg.json @@ -12,6 +12,7 @@ "zstd", "glfw3", "curl", - "libssh2" + "libssh2", + "md4c" ] } \ No newline at end of file diff --git a/lib/external/libwolv b/lib/external/libwolv index 67b9306e2..8427d4da7 160000 --- a/lib/external/libwolv +++ b/lib/external/libwolv @@ -1 +1 @@ -Subproject commit 67b9306e204ed7d3e4bd262e552d11140d1c0bed +Subproject commit 8427d4da728c08bc8f554600c1179dd2297d9eca diff --git a/lib/external/pattern_language b/lib/external/pattern_language index a7280dd16..3e98b047c 160000 --- a/lib/external/pattern_language +++ b/lib/external/pattern_language @@ -1 +1 @@ -Subproject commit a7280dd16f67fe6d0f45183a6eb1e3f14a7b2cb5 +Subproject commit 3e98b047c52c07bb1816bd0936f561ce7797469d diff --git a/lib/third_party/md4c b/lib/third_party/md4c new file mode 160000 index 000000000..481fbfbdf --- /dev/null +++ b/lib/third_party/md4c @@ -0,0 +1 @@ +Subproject commit 481fbfbdf72daab2912380d62bb5f2187d438408 diff --git a/plugins/builtin/source/content/views/view_about.cpp b/plugins/builtin/source/content/views/view_about.cpp index 3d2aec9e0..83617330c 100644 --- a/plugins/builtin/source/content/views/view_about.cpp +++ b/plugins/builtin/source/content/views/view_about.cpp @@ -20,6 +20,7 @@ #include #include +#include namespace hex::plugin::builtin { @@ -527,35 +528,10 @@ namespace hex::plugin::builtin { } - static void drawRegularLine(const std::string& line) { - ImGui::Bullet(); - ImGui::SameLine(); - - // Check if the line contains bold text - auto boldStart = line.find("**"); - if (boldStart == std::string::npos) { - // Draw the line normally - ImGui::TextUnformatted(line.c_str()); - - return; - } - - // Find the end of the bold text - auto boldEnd = line.find("**", boldStart + 2); - - // Draw the line with the bold text highlighted - ImGui::TextUnformatted(line.substr(0, boldStart).c_str()); - ImGui::SameLine(0, 0); - ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_Highlight), "{}", - line.substr(boldStart + 2, boldEnd - boldStart - 2).c_str()); - ImGui::SameLine(0, 0); - ImGui::TextUnformatted(line.substr(boldEnd + 2).c_str()); - } - struct ReleaseNotes { std::string title; std::string versionString; - std::vector notes; + AutoReset> markdown; }; static ReleaseNotes parseReleaseNotes(const HttpRequest::Result& response) { @@ -564,7 +540,7 @@ namespace hex::plugin::builtin { if (!response.isSuccess()) { // An error occurred, display it - notes.notes.push_back("## HTTP Error: " + std::to_string(response.getStatusCode())); + notes.markdown = std::make_shared("## HTTP Error: " + std::to_string(response.getStatusCode())); return notes; } @@ -581,9 +557,14 @@ namespace hex::plugin::builtin { // Get the release notes and split it into lines auto body = json["body"].get(); - notes.notes = wolv::util::splitString(body, "\r\n"); + + std::string content; + content += fmt::format("# {} | {}\n", notes.versionString, notes.title); + content += fmt::format("--\n"); + content += body; + notes.markdown = std::make_shared(content); } catch (std::exception &e) { - notes.notes.push_back("## Error: " + std::string(e.what())); + notes.markdown = std::make_shared("## Error: " + std::string(e.what())); } return notes; @@ -610,34 +591,7 @@ namespace hex::plugin::builtin { } } - // Draw the release title - if (!notes.title.empty()) { - auto title = fmt::format("{}: {}", notes.versionString, notes.title); - ImGuiExt::Header(title.c_str(), true); - ImGui::Separator(); - } - - // Draw the release notes and format them using parts of the GitHub Markdown syntax - // This is not a full implementation of the syntax, but it's enough to make the release notes look good. - for (const auto &line : notes.notes) { - if (line.starts_with("## ")) { - // Draw H2 Header - ImGuiExt::Header(line.substr(3).c_str()); - } else if (line.starts_with("### ")) { - // Draw H3 Header - ImGuiExt::Header(line.substr(4).c_str()); - } else if (line.starts_with("- ")) { - // Draw bullet point - drawRegularLine(line.substr(2)); - } else if (line.starts_with(" - ")) { - // Draw further indented bullet point - ImGui::Indent(); - ImGui::Indent(); - drawRegularLine(line.substr(6)); - ImGui::Unindent(); - ImGui::Unindent(); - } - } + (*notes.markdown)->draw(); } struct Commit { diff --git a/plugins/ui/CMakeLists.txt b/plugins/ui/CMakeLists.txt index ecebb3385..6994b0988 100644 --- a/plugins/ui/CMakeLists.txt +++ b/plugins/ui/CMakeLists.txt @@ -14,6 +14,7 @@ add_imhex_plugin( source/ui/menu_items.cpp source/ui/pattern_value_editor.cpp source/ui/widgets.cpp + source/ui/markdown.cpp source/ui/text_editor/editor.cpp source/ui/text_editor/highlighter.cpp source/ui/text_editor/navigate.cpp @@ -24,5 +25,6 @@ add_imhex_plugin( include LIBRARIES fonts + md4c::md4c LIBRARY_PLUGIN ) \ No newline at end of file diff --git a/plugins/ui/include/ui/markdown.hpp b/plugins/ui/include/ui/markdown.hpp new file mode 100644 index 000000000..10fc655e9 --- /dev/null +++ b/plugins/ui/include/ui/markdown.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include + +#include + +#include +#include + +namespace hex::ui { + + class Markdown { + public: + Markdown() = default; + Markdown(const std::string &text); + + void draw(); + void reset(); + + void setRomfsTextureLookupFunction(std::function(const std::string &)> romfsFileReader) { + m_romfsFileReader = std::move(romfsFileReader); + } + + private: + bool inTable() const; + std::string getElementId(); + + private: + std::string m_text; + bool m_initialized = false; + MD_RENDERER m_mdRenderer; + bool m_firstLine = true; + u32 m_elementId = 1; + std::string m_currentLink; + bool m_drawingImageAltText = false; + u32 m_listIndent = 0; + std::vector m_tableVisibleStack; + + std::map>> m_futureImages; + std::map m_images; + std::function(const std::string &)> m_romfsFileReader; + + std::vector m_quoteStarts; + std::vector m_quoteNeedsChildEnd; + bool m_quoteStart = false; + }; + +} \ No newline at end of file diff --git a/plugins/ui/source/ui/markdown.cpp b/plugins/ui/source/ui/markdown.cpp new file mode 100644 index 000000000..a335b9081 --- /dev/null +++ b/plugins/ui/source/ui/markdown.cpp @@ -0,0 +1,418 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include + +namespace hex::ui { + + Markdown::Markdown(const std::string &text) : m_text(text) { + m_mdRenderer = MD_RENDERER(); + m_initialized = true; + m_mdRenderer.flags = MD_DIALECT_GITHUB | MD_FLAG_TABLES | MD_FLAG_TASKLISTS; + m_mdRenderer.enter_block = [](MD_BLOCKTYPE type, void *detail, void *userdata) -> int { + auto &self = *static_cast(userdata); + + switch (type) { + case MD_BLOCK_DOC: + self.m_firstLine = true; + return 0; + case MD_BLOCK_H: + if (!self.m_firstLine) { + ImGui::NewLine(); + ImGui::NewLine(); + } + fonts::Default().pushBold(std::lerp(2.0F, 1.1F, ((MD_BLOCK_H_DETAIL*)detail)->level / 6.0F)); + break; + case MD_BLOCK_HR: + ImGui::Separator(); + break; + case MD_BLOCK_CODE: { + ImGui::NewLine(); + bool open = ImGui::BeginTable(self.getElementId().c_str(), 1, ImGuiTableFlags_Borders); + self.m_tableVisibleStack.emplace_back(open); + if (open) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImGui::GetColorU32(ImGuiCol_MenuBarBg)); + } + break; + } + case MD_BLOCK_TABLE: { + const auto *table = static_cast(detail); + ImGui::NewLine(); + bool open = ImGui::BeginTable(self.getElementId().c_str(), table->col_count, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX); + self.m_tableVisibleStack.emplace_back(open); + break; + } + case MD_BLOCK_TD: { + if (self.inTable()) + ImGui::TableNextColumn(); + break; + } + case MD_BLOCK_TH: { + if (self.inTable()) + ImGui::TableNextColumn(); + break; + } + case MD_BLOCK_TBODY: + if (self.inTable()) + ImGui::TableNextRow(); + break; + case MD_BLOCK_THEAD: + if (self.inTable()) + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + break; + case MD_BLOCK_QUOTE: + if (!self.m_quoteStarts.empty()) + ImGui::NewLine(); + self.m_quoteStarts.emplace_back(ImGui::GetCursorScreenPos()); + self.m_quoteStart = true; + break; + case MD_BLOCK_UL: { + ImGui::NewLine(); + if (self.m_listIndent > 0) { + ImGui::Indent(); + } + self.m_listIndent += 1; + break; + } + case MD_BLOCK_LI: { + const auto *li = static_cast(detail); + ImGui::Bullet(); + + if (li->is_task) { + bool checked = li->task_mark != ' '; + ImGui::BeginDisabled(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 }); + ImGui::Checkbox(self.getElementId().c_str(), &checked); + ImGui::PopStyleVar(); + ImGui::EndDisabled(); + ImGui::SameLine(); + } + break; + } + case MD_BLOCK_P: + ImGui::NewLine(); + break; + default: + break; + } + + self.m_firstLine = false; + + std::ignore = detail; + std::ignore = userdata; + return 0; // No special handling for block enter + }; + m_mdRenderer.leave_block = [](MD_BLOCKTYPE type, void *detail, void *userdata) -> int { + auto &self = *static_cast(userdata); + + switch (type) { + case MD_BLOCK_H: + fonts::Default().pop(); + break; + case MD_BLOCK_CODE: + if (self.inTable()) { + ImGui::EndTable(); + self.m_tableVisibleStack.pop_back(); + } + break; + case MD_BLOCK_TABLE: + if (self.inTable()) { + ImGui::EndTable(); + self.m_tableVisibleStack.pop_back(); + } + break; + case MD_BLOCK_QUOTE: + if (!self.m_quoteNeedsChildEnd.empty()) { + if (self.m_quoteNeedsChildEnd.back()) { + ImGuiExt::EndSubWindow(); + ImGui::PopStyleColor(); + } else { + ImGui::Unindent(); + ImGui::GetWindowDrawList()->AddLine( + self.m_quoteStarts.back(), + ImGui::GetCursorScreenPos() + ImVec2(0, ImGui::GetTextLineHeight()), + ImGui::GetColorU32(ImGuiCol_Separator), + 3_scaled + ); + self.m_quoteStarts.pop_back(); + } + self.m_quoteNeedsChildEnd.pop_back(); + } + + break; + case MD_BLOCK_UL: + if (self.m_listIndent > 1) { + ImGui::Unindent(); + } + self.m_listIndent -= 1; + ImGui::SameLine(); + break; + case MD_BLOCK_LI: + ImGui::NewLine(); + break; + default: + break; + } + std::ignore = detail; + std::ignore = userdata; + return 0; // No special handling for block leave + }; + m_mdRenderer.enter_span = [](MD_SPANTYPE type, void *detail, void *userdata) -> int { + auto &self = *static_cast(userdata); + + std::ignore = detail; + std::ignore = userdata; + switch (type) { + case MD_SPAN_STRONG: + fonts::Default().pushBold(); + break; + case MD_SPAN_EM: + fonts::Default().pushItalic(); + break; + case MD_SPAN_A: { + const auto *link = static_cast(detail); + self.m_currentLink = std::string(link->href.text, link->href.size); + break; + } + case MD_SPAN_IMG: { + ImGui::NewLine(); + u32 id = self.m_elementId; + if (auto futureImageIt = self.m_futureImages.find(id); futureImageIt != self.m_futureImages.end()) { + auto &[_, future] = *futureImageIt; + if (future.valid() && future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + self.m_images[id] = std::move(future.get().get()); + self.m_futureImages.erase(futureImageIt); + } else { + ImGui::TextUnformatted("Loading image..."); + } + } else if (auto imageIt = self.m_images.find(id); imageIt != self.m_images.end()) { + const auto &[_, image] = *imageIt; + if (image.isValid()) { + ImGui::Image(image, image.getSize()); + } else { + if (ImGui::BeginChild(self.getElementId().c_str(), { 100, 100 }, ImGuiChildFlags_Borders)) { + ImGui::TextUnformatted("???"); + } + ImGui::EndChild(); + } + } else { + const auto *img = static_cast(detail); + std::string path = { img->src.text, img->src.size }; + + self.m_futureImages.emplace(id, std::async(std::launch::async, [path = std::move(path), romfsLookup = self.m_romfsFileReader]() -> wolv::container::Lazy { + std::vector data; + if (path.starts_with("data:image/")) { + auto pos = path.find(';'); + if (pos != std::string::npos) { + auto base64 = path.substr(pos + 1); + if (base64.starts_with("base64,")) { + base64 = base64.substr(7); + } + data = crypt::decode64({ base64.begin(), base64.end() }); + } + } else if (path.starts_with("http://") || path.starts_with("https://")) { + HttpRequest request("GET", path); + const auto result = request.execute>().get(); + if (result.isSuccess()) { + data = result.getData(); + } + } else if (path.starts_with("romfs://")) { + if (romfsLookup) { + return romfsLookup(path.substr(7)); + } + } + + return wolv::container::Lazy([data = std::move(data)]() -> ImGuiExt::Texture { + if (data.empty()) + return ImGuiExt::Texture(); + + auto texture = ImGuiExt::Texture::fromImage(data.data(), data.size(), ImGuiExt::Texture::Filter::Linear); + if (!texture.isValid()) { + texture = ImGuiExt::Texture::fromSVG({ reinterpret_cast(data.data()), data.size() }, 0, 0, ImGuiExt::Texture::Filter::Nearest); + } + + return texture; + }); + })); + } + self.m_drawingImageAltText = true; + self.m_elementId += 1; + + break; + } + default: + break; + } + return 0; // No special handling for span enter + }; + m_mdRenderer.leave_span = [](MD_SPANTYPE type, void *detail, void *userdata) -> int { + auto &self = *static_cast(userdata); + + std::ignore = detail; + switch (type) { + case MD_SPAN_STRONG: + case MD_SPAN_EM: + fonts::Default().pop(); + break; + case MD_SPAN_IMG: + if (!self.m_currentLink.empty()) { + if (ImGui::IsItemClicked()) + hex::openWebpage(self.m_currentLink); + ImGui::SetItemTooltip("%s", self.m_currentLink.c_str()); + self.m_currentLink.clear(); + } + break; + default: + break; + } + return 0; // No special handling for span leave + + }; + m_mdRenderer.text = [](MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *userdata) -> int { + auto &self = *static_cast(userdata); + + std::ignore = userdata; + std::string_view sv(text, size); + + if (self.m_quoteStart) { + self.m_quoteStart = false; + if (sv.starts_with("[!") && sv.ends_with("]")) { + self.m_quoteNeedsChildEnd.push_back(true); + const auto quoteType = sv.substr(2, sv.size() - 3); + const char *icon = nullptr; + ImColor color; + if (quoteType == "IMPORTANT") { + icon = ICON_VS_REPORT; + color = ImGuiExt::GetCustomColorU32(ImGuiCustomCol_ToolbarRed); + } else if (quoteType == "NOTE") { + icon = ICON_VS_INFO; + color = ImGuiExt::GetCustomColorU32(ImGuiCustomCol_ToolbarBlue); + } else if (quoteType == "TIP") { + icon = ICON_VS_LIGHTBULB; + color = ImGuiExt::GetCustomColorU32(ImGuiCustomCol_ToolbarGreen); + } else if (quoteType == "WARNING") { + icon = ICON_VS_WARNING; + color = ImGuiExt::GetCustomColorU32(ImGuiCustomCol_ToolbarYellow); + } else { + icon = ICON_VS_QUESTION; + color = ImGui::GetColorU32(ImGuiCol_Separator); + } + ImGui::PushStyleColor(ImGuiCol_MenuBarBg, color.Value); + ImGuiExt::BeginSubWindow(icon); + return 0; + } else { + ImGui::Indent(); + self.m_quoteNeedsChildEnd.push_back(false); + } + } + + if (sv.size() == 1 && sv[0] == '\n') { + ImGui::NewLine(); + return 0; + } + + while (!sv.empty()) { + const auto c = sv.front(); + const bool whiteSpaces = std::isspace(static_cast(c)) != 0; + + size_t end = 0; + while (end < sv.size() && (std::isspace(static_cast(sv[end])) != 0) == whiteSpaces) { + ++end; + } + + std::string_view word = sv.substr(0, end); + + auto textSize = ImGui::CalcTextSize(word.data(), word.data() + word.size()); + if (ImGui::GetCursorPosX() > ImGui::GetStyle().WindowPadding.x && ImGui::GetCursorPosX() + textSize.x > ImGui::GetWindowSize().x && !whiteSpaces) { + ImGui::NewLine(); + } + + switch (type) { + case MD_TEXT_NORMAL: + case MD_TEXT_ENTITY: + if (!self.m_currentLink.empty()) { + if (ImGuiExt::Hyperlink(std::string(word.data(), word.data() + word.size()).c_str())) { + hex::openWebpage(self.m_currentLink); + } + ImGui::SetItemTooltip("%s", self.m_currentLink.c_str()); + self.m_currentLink.clear(); + } else if (self.m_drawingImageAltText) { + if (ImGui::IsItemHovered()) { + if (ImGui::BeginTooltip()) { + ImGui::TextUnformatted(word.data(), word.data() + word.size()); + ImGui::SameLine(0, 0); + ImGui::EndTooltip(); + } + } + } else { + ImGui::TextUnformatted(word.data(), word.data() + word.size()); + } + + self.m_drawingImageAltText = false; + break; + case MD_TEXT_NULLCHAR: + ImGui::TextUnformatted("�"); + break; + case MD_TEXT_CODE: + ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetCursorScreenPos(), ImGui::GetCursorScreenPos() + textSize, ImGui::GetColorU32(ImGuiCol_MenuBarBg)); + ImGui::TextUnformatted(word.data(), word.data() + word.size()); + break; + default: + break; + } + + ImGui::SameLine(0, 0); + + sv.remove_prefix(end); // Remove the word + } + + return 0; // Continue parsing + }; + m_mdRenderer.debug_log = [](const char *msg, void *userdata) { + std::ignore = userdata; + log::debug("Markdown debug: {}", msg); + }; + } + + void Markdown::draw() { + if (!m_initialized) + return; + + m_elementId = 1; + md_parse(m_text.c_str(), m_text.size(), &m_mdRenderer, this); + } + + void Markdown::reset() { + m_futureImages.clear(); + m_images.clear(); + } + + + bool Markdown::inTable() const { + if (m_tableVisibleStack.empty()) + return false; + return m_tableVisibleStack.back() != 0; + } + + std::string Markdown::getElementId() { + return fmt::format("##Element{}", m_elementId++); + } + +}