feat: Added proper Markdown renderer (#2415)

This commit is contained in:
Nik 2025-08-20 20:37:44 +02:00 committed by GitHub
parent b83f3d6cbf
commit d012ad08f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 544 additions and 69 deletions

View File

@ -91,6 +91,7 @@ jobs:
-DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \ -DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \
-DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \ -DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \
-DUSE_SYSTEM_CAPSTONE=ON \ -DUSE_SYSTEM_CAPSTONE=ON \
-DUSE_SYSTEM_MD4C=ON \
-DIMHEX_GENERATE_PDBS=ON \ -DIMHEX_GENERATE_PDBS=ON \
-DIMHEX_REPLACE_DWARF_WITH_PDB=ON \ -DIMHEX_REPLACE_DWARF_WITH_PDB=ON \
-DDOTNET_EXECUTABLE="C:/Program Files/dotnet/dotnet.exe" \ -DDOTNET_EXECUTABLE="C:/Program Files/dotnet/dotnet.exe" \
@ -367,6 +368,7 @@ jobs:
-DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \
-DIMHEX_USE_DEFAULT_BUILD_SETTINGS=ON \ -DIMHEX_USE_DEFAULT_BUILD_SETTINGS=ON \
-DUSE_SYSTEM_CAPSTONE=ON \ -DUSE_SYSTEM_CAPSTONE=ON \
-DUSE_SYSTEM_MD4C=ON \
.. ..
ninja ninja
@ -885,6 +887,7 @@ jobs:
-DUSE_SYSTEM_YARA=ON \ -DUSE_SYSTEM_YARA=ON \
-DUSE_SYSTEM_NLOHMANN_JSON=ON \ -DUSE_SYSTEM_NLOHMANN_JSON=ON \
-DUSE_SYSTEM_CAPSTONE=OFF \ -DUSE_SYSTEM_CAPSTONE=OFF \
-DUSE_SYSTEM_MD4C=ON \
-DIMHEX_PATTERNS_PULL_MASTER=ON \ -DIMHEX_PATTERNS_PULL_MASTER=ON \
-DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \ -DIMHEX_COMMIT_HASH_LONG="${GITHUB_SHA}" \
-DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \ -DIMHEX_COMMIT_BRANCH="${GITHUB_REF##*/}" \

3
.gitmodules vendored
View File

@ -47,3 +47,6 @@
[submodule "lib/external/disassembler"] [submodule "lib/external/disassembler"]
path = lib/external/disassembler path = lib/external/disassembler
url = https://github.com/WerWolv/Disassembler url = https://github.com/WerWolv/Disassembler
[submodule "lib/third_party/md4c"]
path = lib/third_party/md4c
url = https://github.com/mity/md4c

View File

@ -866,6 +866,17 @@ macro(addBundledLibraries)
set(LUNASVG_LIBRARIES lunasvg::lunasvg) set(LUNASVG_LIBRARIES lunasvg::lunasvg)
endif() 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) if (NOT USE_SYSTEM_LLVM)
add_subdirectory(${THIRD_PARTY_LIBS_FOLDER}/llvm-demangle EXCLUDE_FROM_ALL) add_subdirectory(${THIRD_PARTY_LIBS_FOLDER}/llvm-demangle EXCLUDE_FROM_ALL)
else() else()

View File

@ -32,6 +32,8 @@ AppDir:
- libpcre3 - libpcre3
- libselinux1 - libselinux1
- libtinfo6 - libtinfo6
- libmd4c-dev
- libmd4c-html0-dev
files: files:
include: include:
- "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libLLVM-13.so.1" - "/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/libxml2.so.2"
- "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libxshmfence.so.1" - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libxshmfence.so.1"
- "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libzstd.so.1" - "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libzstd.so.1"
- "/lib/{{ARCHITECTURE_APPIMAGE_BUILDER}}-linux-gnu/libmd4c.so"
exclude: exclude:
- usr/share/man - usr/share/man
- usr/share/doc/*/README.* - usr/share/doc/*/README.*

View File

@ -18,7 +18,8 @@ RUN pacman -S --needed --noconfirm \
curl \ curl \
dbus \ dbus \
xdg-desktop-portal \ xdg-desktop-portal \
libssh2 libssh2 \
md4c
# Clone ImHex # Clone ImHex
RUN git clone https://github.com/WerWolv/ImHex --recurse-submodules /root/ImHex RUN git clone https://github.com/WerWolv/ImHex --recurse-submodules /root/ImHex

View File

@ -4,7 +4,7 @@ Section: editors
Priority: optional Priority: optional
Architecture: amd64 Architecture: amd64
License: GNU GPL-2 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 <hey@werwolv.net> Maintainer: WerWolv <hey@werwolv.net>
Description: ImHex Hex Editor Description: ImHex Hex Editor
A Hex Editor for Reverse Engineers, Programmers and A Hex Editor for Reverse Engineers, Programmers and

View File

@ -29,6 +29,7 @@ RDEPEND="${DEPEND}
app-arch/zstd app-arch/zstd
app-arch/lz4 app-arch/lz4
net-libs/libssh2 net-libs/libssh2
dev-libs/md4c
" "
BDEPEND="${DEPEND} BDEPEND="${DEPEND}
dev-cpp/nlohmann_json dev-cpp/nlohmann_json

View File

@ -100,6 +100,18 @@ modules:
- /lib/cmake - /lib/cmake
- /lib64/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 - name: imhex
buildsystem: cmake-ninja buildsystem: cmake-ninja
builddir: true builddir: true

View File

@ -21,4 +21,5 @@ pacman -S $@ --needed \
xz \ xz \
zstd \ zstd \
lz4 \ lz4 \
libssh2 libssh2 \
md4c

View File

@ -29,4 +29,6 @@ apt install -y \
liblzma-dev \ liblzma-dev \
libzstd-dev \ libzstd-dev \
liblz4-dev \ liblz4-dev \
libssh2-1-dev libssh2-1-dev \
libmd4c-dev \
libmd4c-html0-dev

View File

@ -19,4 +19,5 @@ pacboy -S --needed --noconfirm \
xz:p \ xz:p \
zstd:p \ zstd:p \
lz4:p \ lz4:p \
libssh2-wincng:p libssh2-wincng:p \
md4c:p

View File

@ -19,4 +19,5 @@ zypper install \
bzip3-devel \ bzip3-devel \
xz-devel \ xz-devel \
lz4-dev \ lz4-dev \
libssh2-devel libssh2-devel \
md4c-devel

1
dist/macOS/Brewfile vendored
View File

@ -14,3 +14,4 @@ brew "xz"
brew "bzip2" brew "bzip2"
brew "zstd" brew "zstd"
brew "libssh2" brew "libssh2"
brew "md4c"

3
dist/msys2/PKGBUILD vendored
View File

@ -21,7 +21,8 @@ makedepends=("${MINGW_PACKAGE_PREFIX}-gcc"
"${MINGW_PACKAGE_PREFIX}-bzip2" "${MINGW_PACKAGE_PREFIX}-bzip2"
"${MINGW_PACKAGE_PREFIX}-xz" "${MINGW_PACKAGE_PREFIX}-xz"
"${MINGW_PACKAGE_PREFIX}-zstd" "${MINGW_PACKAGE_PREFIX}-zstd"
"${MINGW_PACKAGE_PREFIX}-libssh2-wincng") "${MINGW_PACKAGE_PREFIX}-libssh2-wincng"
"${MINGW_PACKAGE_PREFIX}-md4c")
source=() source=()
sha256sums=() sha256sums=()

1
dist/rpm/imhex.spec vendored
View File

@ -108,6 +108,7 @@ CXXFLAGS+=" -std=gnu++2b"
-D USE_SYSTEM_FMT=ON \ -D USE_SYSTEM_FMT=ON \
-D USE_SYSTEM_CURL=ON \ -D USE_SYSTEM_CURL=ON \
-D USE_SYSTEM_LLVM=ON \ -D USE_SYSTEM_LLVM=ON \
-D USE_SYSTEM_MD4C=OFF \
%if 0%{?fedora} || 0%{?rhel} > 9 %if 0%{?fedora} || 0%{?rhel} > 9
-D USE_SYSTEM_CAPSTONE=ON \ -D USE_SYSTEM_CAPSTONE=ON \
%endif %endif

View File

@ -62,6 +62,8 @@ parts:
- libzstd-dev - libzstd-dev
- liblz4-dev - liblz4-dev
- libssh2-1-dev - libssh2-1-dev
- libmd4c-dev
- libmd4c-html0-dev
stage-packages: stage-packages:
- libglfw3 - libglfw3
- libmagic1 - libmagic1

3
dist/vcpkg.json vendored
View File

@ -12,6 +12,7 @@
"zstd", "zstd",
"glfw3", "glfw3",
"curl", "curl",
"libssh2" "libssh2",
"md4c"
] ]
} }

@ -1 +1 @@
Subproject commit 67b9306e204ed7d3e4bd262e552d11140d1c0bed Subproject commit 8427d4da728c08bc8f554600c1179dd2297d9eca

@ -1 +1 @@
Subproject commit a7280dd16f67fe6d0f45183a6eb1e3f14a7b2cb5 Subproject commit 3e98b047c52c07bb1816bd0936f561ce7797469d

1
lib/third_party/md4c vendored Submodule

@ -0,0 +1 @@
Subproject commit 481fbfbdf72daab2912380d62bb5f2187d438408

View File

@ -20,6 +20,7 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <string> #include <string>
#include <ui/markdown.hpp>
namespace hex::plugin::builtin { 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 { struct ReleaseNotes {
std::string title; std::string title;
std::string versionString; std::string versionString;
std::vector<std::string> notes; AutoReset<std::shared_ptr<ui::Markdown>> markdown;
}; };
static ReleaseNotes parseReleaseNotes(const HttpRequest::Result<std::string>& response) { static ReleaseNotes parseReleaseNotes(const HttpRequest::Result<std::string>& response) {
@ -564,7 +540,7 @@ namespace hex::plugin::builtin {
if (!response.isSuccess()) { if (!response.isSuccess()) {
// An error occurred, display it // An error occurred, display it
notes.notes.push_back("## HTTP Error: " + std::to_string(response.getStatusCode())); notes.markdown = std::make_shared<ui::Markdown>("## HTTP Error: " + std::to_string(response.getStatusCode()));
return notes; return notes;
} }
@ -581,9 +557,14 @@ namespace hex::plugin::builtin {
// Get the release notes and split it into lines // Get the release notes and split it into lines
auto body = json["body"].get<std::string>(); auto body = json["body"].get<std::string>();
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<ui::Markdown>(content);
} catch (std::exception &e) { } catch (std::exception &e) {
notes.notes.push_back("## Error: " + std::string(e.what())); notes.markdown = std::make_shared<ui::Markdown>("## Error: " + std::string(e.what()));
} }
return notes; return notes;
@ -610,34 +591,7 @@ namespace hex::plugin::builtin {
} }
} }
// Draw the release title (*notes.markdown)->draw();
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();
}
}
} }
struct Commit { struct Commit {

View File

@ -14,6 +14,7 @@ add_imhex_plugin(
source/ui/menu_items.cpp source/ui/menu_items.cpp
source/ui/pattern_value_editor.cpp source/ui/pattern_value_editor.cpp
source/ui/widgets.cpp source/ui/widgets.cpp
source/ui/markdown.cpp
source/ui/text_editor/editor.cpp source/ui/text_editor/editor.cpp
source/ui/text_editor/highlighter.cpp source/ui/text_editor/highlighter.cpp
source/ui/text_editor/navigate.cpp source/ui/text_editor/navigate.cpp
@ -24,5 +25,6 @@ add_imhex_plugin(
include include
LIBRARIES LIBRARIES
fonts fonts
md4c::md4c
LIBRARY_PLUGIN LIBRARY_PLUGIN
) )

View File

@ -0,0 +1,55 @@
#pragma once
#include <hex.hpp>
#include <imgui.h>
#include <hex/ui/imgui_imhex_extensions.h>
#include <string>
#include <future>
#include <map>
#include <md4c.h>
#include <wolv/container/lazy.hpp>
#include <romfs/romfs.hpp>
namespace hex::ui {
class Markdown {
public:
Markdown() = default;
Markdown(const std::string &text);
void draw();
void reset();
void setRomfsTextureLookupFunction(std::function<wolv::container::Lazy<ImGuiExt::Texture>(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<u8> m_tableVisibleStack;
std::map<u32, std::future<wolv::container::Lazy<ImGuiExt::Texture>>> m_futureImages;
std::map<u32, ImGuiExt::Texture> m_images;
std::function<wolv::container::Lazy<ImGuiExt::Texture>(const std::string &)> m_romfsFileReader;
std::vector<ImVec2> m_quoteStarts;
std::vector<u8> m_quoteNeedsChildEnd;
bool m_quoteStart = false;
};
}

View File

@ -0,0 +1,418 @@
#include <ui/markdown.hpp>
#include <imgui.h>
#include <fonts/fonts.hpp>
#include <hex/api/task_manager.hpp>
#include <hex/helpers/fmt.hpp>
#include <hex/helpers/logger.hpp>
#include <hex/ui/imgui_imhex_extensions.h>
#include <romfs/romfs.hpp>
#include <chrono>
#include <fonts/vscode_icons.hpp>
#include <hex/helpers/crypto.hpp>
#include <hex/helpers/http_requests.hpp>
#include <hex/helpers/scaling.hpp>
#include <hex/helpers/utils.hpp>
#include <md4c.h>
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<Markdown*>(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<MD_BLOCK_TABLE_DETAIL*>(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<MD_BLOCK_LI_DETAIL*>(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<Markdown*>(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<Markdown*>(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<MD_SPAN_A_DETAIL*>(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<MD_SPAN_IMG_DETAIL*>(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<ImGuiExt::Texture> {
std::vector<u8> 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<std::vector<u8>>().get();
if (result.isSuccess()) {
data = result.getData();
}
} else if (path.starts_with("romfs://")) {
if (romfsLookup) {
return romfsLookup(path.substr(7));
}
}
return wolv::container::Lazy<ImGuiExt::Texture>([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<const std::byte*>(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<Markdown*>(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<Markdown*>(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<u8>(c)) != 0;
size_t end = 0;
while (end < sv.size() && (std::isspace(static_cast<u8>(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("<EFBFBD>");
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++);
}
}