mirror of https://github.com/WerWolv/ImHex
feat: Added proper Markdown renderer (#2415)
This commit is contained in:
parent
b83f3d6cbf
commit
d012ad08f8
|
|
@ -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##*/}" \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <hey@werwolv.net>
|
||||
Description: ImHex Hex Editor
|
||||
A Hex Editor for Reverse Engineers, Programmers and
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ RDEPEND="${DEPEND}
|
|||
app-arch/zstd
|
||||
app-arch/lz4
|
||||
net-libs/libssh2
|
||||
dev-libs/md4c
|
||||
"
|
||||
BDEPEND="${DEPEND}
|
||||
dev-cpp/nlohmann_json
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ pacman -S $@ --needed \
|
|||
xz \
|
||||
zstd \
|
||||
lz4 \
|
||||
libssh2
|
||||
libssh2 \
|
||||
md4c
|
||||
|
|
|
|||
|
|
@ -29,4 +29,6 @@ apt install -y \
|
|||
liblzma-dev \
|
||||
libzstd-dev \
|
||||
liblz4-dev \
|
||||
libssh2-1-dev
|
||||
libssh2-1-dev \
|
||||
libmd4c-dev \
|
||||
libmd4c-html0-dev
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ pacboy -S --needed --noconfirm \
|
|||
xz:p \
|
||||
zstd:p \
|
||||
lz4:p \
|
||||
libssh2-wincng:p
|
||||
libssh2-wincng:p \
|
||||
md4c:p
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ zypper install \
|
|||
bzip3-devel \
|
||||
xz-devel \
|
||||
lz4-dev \
|
||||
libssh2-devel
|
||||
libssh2-devel \
|
||||
md4c-devel
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ brew "xz"
|
|||
brew "bzip2"
|
||||
brew "zstd"
|
||||
brew "libssh2"
|
||||
brew "md4c"
|
||||
|
|
@ -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=()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ parts:
|
|||
- libzstd-dev
|
||||
- liblz4-dev
|
||||
- libssh2-1-dev
|
||||
- libmd4c-dev
|
||||
- libmd4c-html0-dev
|
||||
stage-packages:
|
||||
- libglfw3
|
||||
- libmagic1
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"zstd",
|
||||
"glfw3",
|
||||
"curl",
|
||||
"libssh2"
|
||||
"libssh2",
|
||||
"md4c"
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 67b9306e204ed7d3e4bd262e552d11140d1c0bed
|
||||
Subproject commit 8427d4da728c08bc8f554600c1179dd2297d9eca
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit a7280dd16f67fe6d0f45183a6eb1e3f14a7b2cb5
|
||||
Subproject commit 3e98b047c52c07bb1816bd0936f561ce7797469d
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 481fbfbdf72daab2912380d62bb5f2187d438408
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <ui/markdown.hpp>
|
||||
|
||||
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<std::string> notes;
|
||||
AutoReset<std::shared_ptr<ui::Markdown>> markdown;
|
||||
};
|
||||
|
||||
static ReleaseNotes parseReleaseNotes(const HttpRequest::Result<std::string>& 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<ui::Markdown>("## 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<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) {
|
||||
notes.notes.push_back("## Error: " + std::string(e.what()));
|
||||
notes.markdown = std::make_shared<ui::Markdown>("## 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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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++);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue