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_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##*/}" \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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.*
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,5 @@ pacman -S $@ --needed \
|
||||||
xz \
|
xz \
|
||||||
zstd \
|
zstd \
|
||||||
lz4 \
|
lz4 \
|
||||||
libssh2
|
libssh2 \
|
||||||
|
md4c
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,5 @@ zypper install \
|
||||||
bzip3-devel \
|
bzip3-devel \
|
||||||
xz-devel \
|
xz-devel \
|
||||||
lz4-dev \
|
lz4-dev \
|
||||||
libssh2-devel
|
libssh2-devel \
|
||||||
|
md4c-devel
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ brew "xz"
|
||||||
brew "bzip2"
|
brew "bzip2"
|
||||||
brew "zstd"
|
brew "zstd"
|
||||||
brew "libssh2"
|
brew "libssh2"
|
||||||
|
brew "md4c"
|
||||||
|
|
@ -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=()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 481fbfbdf72daab2912380d62bb5f2187d438408
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
@ -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