diff --git a/PiPedalCommon/src/HtmlHelper.cpp b/PiPedalCommon/src/HtmlHelper.cpp index 1b887dc..52b1741 100644 --- a/PiPedalCommon/src/HtmlHelper.cpp +++ b/PiPedalCommon/src/HtmlHelper.cpp @@ -245,7 +245,7 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string &name) #define MAX_FILE_NAME_LENGHT 96 -const std::string SF_SPECIALS = " <>@;:\"\'/[]?="; +const std::string SF_SPECIALS = " <>@;:\"\'/[]?=+"; std::string HtmlHelper::SafeFileName(const std::string &name) { diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 7792535..27d65a5 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2026 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -57,6 +57,25 @@ } \ ; + +#define JSON_GETTER_SETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + decltype(name##_) &name() { return name##_; } \ + void name(const decltype(name##_) &value) { name##_ = value; } + +#define JSON_GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ + void name(decltype(name##_) value) { name##_ = value; } + + +#define JSON_GETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + decltype(name##_) &name() { return name##_; } + +#define JSON_GETTER(name) \ + decltype(name##_) name() const { return name##_; } + + namespace pipedal { class JsonException: public std::runtime_error { diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 39b95dc..1a6183d 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -124,5 +124,7 @@ namespace pipedal std::string ToLower(const std::string&value); + std::string safeFilenameToString(const std::string &filename); + std::string stringToSafeFilename(const std::string &name); } diff --git a/PiPedalCommon/src/json.cpp b/PiPedalCommon/src/json.cpp index d486bf8..a64fe2b 100644 --- a/PiPedalCommon/src/json.cpp +++ b/PiPedalCommon/src/json.cpp @@ -575,7 +575,7 @@ void json_reader::skip_object() } skip_string(); // name. consume(':'); - skip_object(); + skip_property(); if (peek() == ',') { consume(','); diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index 52838ee..7b1f89e 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -32,32 +32,29 @@ using namespace pipedal; - void pipedal::SetThreadName(const std::string &name) { std::string threadName = "ppdl_" + name; - if (threadName.length () > 15) + if (threadName.length() > 15) { - threadName = threadName.substr(0,15); + threadName = threadName.substr(0, 15); } pthread_t pid = pthread_self(); - pthread_setname_np(pid,threadName.c_str()); + pthread_setname_np(pid, threadName.c_str()); } - static const uint8_t utf8extraBytes[256] = { - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 -}; + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5}; constexpr char32_t ILLEGAL_CHAR32 = U'⊗'; -static const uint8_t utf8Offset[] = { 0,0b11000000, 0b11100000,0b11110000,0b11111000,0b11111100}; +static const uint8_t utf8Offset[] = {0, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100}; std::u32string pipedal::ToUtf32(const std::string &s) { @@ -72,16 +69,21 @@ std::u32string pipedal::ToUtf32(const std::string &s) if (c < 0x80) { result << (char32_t)c; - } else { + } + else + { auto extraBytes = utf8extraBytes[c]; if (extraBytes == 0) { result << ILLEGAL_CHAR32; - } else if (p+extraBytes > end) + } + else if (p + extraBytes > end) { result << ILLEGAL_CHAR32; break; - } else { + } + else + { char32_t cResult = c -= utf8Offset[extraBytes]; while (extraBytes != 0) { @@ -102,11 +104,10 @@ std::u32string pipedal::ToUtf32(const std::string &s) return result.str(); } - std::string pipedal::GetHostName() { char buffer[1024]; - if (gethostname(buffer,1024) != 0) + if (gethostname(buffer, 1024) != 0) { buffer[0] = '\0'; } @@ -114,7 +115,6 @@ std::string pipedal::GetHostName() return buffer; } - std::vector pipedal::split(const std::string &value, char delimiter) { size_t start = 0; @@ -138,8 +138,7 @@ std::vector pipedal::split(const std::string &value, char delimiter return result; } - -bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath) +bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path &rootPath) { auto iter = path.begin(); @@ -154,8 +153,7 @@ bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::fil return true; } - -std::string pipedal::ToLower(const std::string&value) +std::string pipedal::ToLower(const std::string &value) { std::string result; result.resize(value.length()); @@ -166,16 +164,17 @@ std::string pipedal::ToLower(const std::string&value) return result; } - -std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath) +std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path &parentPath) { - if (path.is_relative()) return path; + if (path.is_relative()) + return path; auto iter = path.begin(); - + for (auto i = parentPath.begin(); i != parentPath.end(); ++i) { - if (iter == path.end() || *iter != *i) { + if (iter == path.end() || *iter != *i) + { // not a child directory of parent directory. throw std::runtime_error("Not a child directory."); } @@ -187,7 +186,6 @@ std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &pat remander /= *iter++; } return remander; - } bool pipedal::HasDotDot(const std::filesystem::path &path) @@ -218,7 +216,7 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files if ((*i) != (*iPath)) { - if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /) + if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /) { break; } @@ -237,12 +235,106 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files return true; } - bool pipedal::HasWritePermissions(const std::filesystem::path &path) { - // posix, but may not work on windows. + // posix, but may not work on windows. // allegedly windows provies an _access function, which is probably a superset of // access. return access(path.c_str(), W_OK) == 0; } +const std::string SF_SPECIAL_CHARS = " <>@;:\"\'/[]?=+%."; + +static std::array makeSfBits() +{ + std::array result; + + for (char c : SF_SPECIAL_CHARS) + { + result[(unsigned char)c] = true; + } + for (size_t i = 128; i < 256; ++i) + { + result[i] = true; + } + return result; +} + +static std::array IS_SPECIAL_FILENAME_CHAR = makeSfBits(); + +static const char *HEX_CHARS = "0123456789abcdef"; + +static bool isHexChar(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static int fromHexChar(char c) +{ + if (c >= '0' && c <= '9') + return (int)(c - '0'); + if (c >= 'a' && c <= 'f') + return (int)(c - 'a') + 10; + if (c >= 'A' && c <= 'F') + return (int)(c - 'A') + 10; + return 0; +} +static char fromHexChars(char c1, char c2) +{ + int uc = fromHexChar(c1) * 16 + fromHexChar(c2); + return (char)uc; +} + +std::string pipedal::safeFilenameToString(const std::string &filename) +{ + size_t ix = 0; + std::ostringstream os; + + while (ix < filename.size()) + { + char c = filename[ix++]; + if (c == '%') + { + // when faced with an invalid encoding, assume that user has done it. + // So return the original. + if (ix + 2 >= filename.size()) + { + return filename; // just return the original. + } + if (!isHexChar(filename[ix]) || !isHexChar(filename[ix + 1])) + { + return filename; + } + os << fromHexChars(filename[ix], filename[ix + 1]); + ix += 2; + } + else + { + os << c; + } + } + return os.str(); +} +std::string pipedal::stringToSafeFilename(const std::string &name) +{ + std::ostringstream os; + for (char c : name) + { + unsigned char uc = (unsigned char)c; + if (uc < 0x20) + { + continue; + } + if (IS_SPECIAL_FILENAME_CHAR[uc]) + { + os << '%' + << HEX_CHARS[uc >> 4] + << HEX_CHARS[uc & 0x0F]; + } + else + { + os << c; + } + } + return os.str(); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 97a2ffb..3757c6d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -204,6 +204,10 @@ else() endif() set (PIPEDAL_SOURCES + + Tone3000Download.cpp Tone3000Download.hpp + Tone3000Downloader.cpp Tone3000Downloader.hpp + CrashGuard.cpp CrashGuard.hpp ModTemplateGenerator.cpp ModTemplateGenerator.hpp WebServerMod.cpp WebServerMod.hpp @@ -265,6 +269,7 @@ set (PIPEDAL_SOURCES GovernorSettings.cpp GovernorSettings.hpp WebServer.cpp WebServer.hpp pch.h Uri.cpp Uri.hpp + RequestHandler.hpp Scratch.cpp PluginHost.hpp PluginHost.cpp PluginType.hpp PluginType.cpp diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 7e9f4ff..1b1807f 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -46,6 +46,7 @@ #include "DummyAudioDriver.hpp" #include "AudioFiles.hpp" #include "CrashGuard.hpp" +#include "Tone3000Downloader.hpp" #ifndef NO_MLOCK #include @@ -135,10 +136,15 @@ void PiPedalModel::Close() std::unique_ptr oldAudioHost; { std::lock_guard lock(mutex); + if (closed) { return; } + if (tone3000Downloader) + { + tone3000Downloader->Close(); + } closed = true; CancelAudioRetry(); @@ -225,6 +231,38 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) #endif } + +int64_t PiPedalModel::DownloadModelsFromTone3000( + int64_t clientId, + const std::string &downloadPath, + const std::string &tone3000Url) +{ + std::lock_guard lock(mutex); + + if (!tone3000Downloader) + { + tone3000Downloader = Tone3000Downloader::Create(); + } + return tone3000Downloader->RequestDownload( + downloadPath, + tone3000Url + ); +} + +void PiPedalModel::CancelTone3000Download( + int64_t clientId, + int64_t downloadHandle +) +{ + std::lock_guard lock(mutex); + + if (tone3000Downloader) + { + tone3000Downloader->CancelDownload(downloadHandle); + } +} + + void PiPedalModel::LoadLv2PluginInfo() { // Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory. @@ -3289,23 +3327,6 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string this->FirePedalboardChanged(-1, false); } -void PiPedalModel::SetTone3000Auth(const std::string &apiKey) -{ - std::lock_guard lock(mutex); - storage.SetTone3000Auth(apiKey); - - std::vector t{subscribers.begin(), subscribers.end()}; - bool hasAuth = apiKey != ""; - for (auto &subscriber : t) - { - subscriber->OnTone3000AuthChanged(hasAuth); - } -} -bool PiPedalModel::HasTone3000Auth() const -{ - return storage.GetTone3000Auth() != ""; -} - std::vector PiPedalModel::RequestBankPresets(int64_t bankInstanceId) { std::lock_guard lock(mutex); diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 897f524..840af13 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -51,12 +51,13 @@ namespace pipedal class Updater; class AvahiService; class Lv2PluginState; + class Tone3000Downloader; class IPiPedalModelSubscriber { public: using ptr = std::shared_ptr; - + virtual int64_t GetClientId() = 0; virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; @@ -97,8 +98,6 @@ namespace pipedal virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual void Close() = 0; - virtual void OnTone3000AuthChanged(bool value) = 0; - }; class HotspotManager; @@ -113,8 +112,9 @@ namespace pipedal using NetworkChangedListener = std::function; private: - PedalboardItem* GetPedalboardItemForFileProperty(const UiFileProperty& fileProperty); + PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty); + std::shared_ptr tone3000Downloader; void CancelAudioRetry(); clock::time_point lastRestartTime = clock::time_point::min(); int audioRestartRetries = 0; @@ -207,7 +207,7 @@ namespace pipedal void FireBanksChanged(int64_t clientId); void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration); void FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State); - void UpdateDefaults(SnapshotValue&snapshotValue, const PedalboardItem*pedalboardItem); + void UpdateDefaults(SnapshotValue &snapshotValue, const PedalboardItem *pedalboardItem); void UpdateDefaults(Snapshot *snapshot, std::unordered_map &itemMap); void UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered_map &itemMap); void UpdateDefaults(Pedalboard *pedalboard); @@ -232,6 +232,7 @@ namespace pipedal IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId); std::atomic closed = false; bool SyncLv2State(); + private: // IAudioHostCallbacks virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override; virtual void OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override; @@ -241,7 +242,7 @@ namespace pipedal virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) override; virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) override; virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; - virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; + virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex, int64_t snapshotRequestId) override; virtual void OnAlsaDriverTerminatedAbnormally() override; virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override; virtual void OnAlsaSequencerDeviceRemoved(int client) override; @@ -267,8 +268,7 @@ namespace pipedal PiPedalConfiguration configuration; void CheckForResourceInitialization(Pedalboard &pedalboard); - UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); - + UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri); public: PiPedalModel(); @@ -280,7 +280,7 @@ namespace pipedal Decrease }; - Storage&GetStorage() { return storage; } + Storage &GetStorage() { return storage; } bool GetHasWifi(); void NextBank(Direction direction = Direction::Increase); @@ -288,6 +288,15 @@ namespace pipedal void NextPreset(Direction direction = Direction::Increase); void PreviousPreset() { NextPreset(Direction::Decrease); } + int64_t DownloadModelsFromTone3000( + int64_t clientId, + const std::string &downloadPath, + const std::string &tone3000Url); + + void CancelTone3000Download( + int64_t clientId, + int64_t downloadHandle + ); void RequestShutdown(bool restart); virtual PostHandle Post(PostCallback &&fn); @@ -323,7 +332,7 @@ namespace pipedal void SetRestartListener(std::function &&listener); void OnLv2PluginsChanged(); void SetOnboarding(bool value); - std::map GetWifiRegulatoryDomains(); + std::map GetWifiRegulatoryDomains(); void UpdateDnsSd(); @@ -336,7 +345,7 @@ namespace pipedal const PluginHost &GetPluginHost() const { return pluginHost; } PluginHost &GetPluginHost() { return pluginHost; } - + Pedalboard GetCurrentPedalboardCopy() { std::lock_guard guard(mutex); @@ -381,7 +390,6 @@ namespace pipedal int64_t SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId = -1); int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name); - void LoadPreset(int64_t clientId, int64_t instanceId); bool UpdatePresets(int64_t clientId, const PresetIndex &presets); void UpdatePluginPresets(const PluginUiPresets &pluginPresets); @@ -472,8 +480,8 @@ namespace pipedal void DeleteSampleFile(const std::filesystem::path &fileName); std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty); std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty); - std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty,bool overwrite); - FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath); + std::string CopyFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty, bool overwrite); + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath); bool IsInUploadsDirectory(const std::string &path); @@ -483,14 +491,11 @@ namespace pipedal bool LoadCurrentPedalboard(); void MoveAudioFile( - const std::string & path, - int32_t from, + const std::string &path, + int32_t from, int32_t to); - void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string&iconColor); - - void SetTone3000Auth(const std::string &apiKey); - bool HasTone3000Auth() const; + void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string &iconColor); void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index a3bf46b..fd02c8f 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -78,6 +78,21 @@ JSON_MAP_BEGIN(RequestBankPresetsBody) JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId) JSON_MAP_END() + +class DownloadTone3000Body { +public: + int64_t handle_; + std::string tone3000DownloadUrl_; + + DECLARE_JSON_MAP(DownloadTone3000Body); +}; + +JSON_MAP_BEGIN(DownloadTone3000Body) +JSON_MAP_REFERENCE(DownloadTone3000Body, handle) +JSON_MAP_REFERENCE(DownloadTone3000Body, tone3000DownloadUrl) +JSON_MAP_END() + + class PathPatchPropertyChangedBody { public: @@ -1494,10 +1509,6 @@ public: pReader->read(&instanceId); uint64_t result = model.DeleteBank(this->clientId, instanceId); this->Reply(replyTo, "deleteBankItem", result); - } else if (message == "getHasTone3000Auth") - { - bool result = model.HasTone3000Auth(); - this->Reply(replyTo, "getHasTone3000Auth", result); } else if (message == "renameBank") { @@ -1848,6 +1859,21 @@ public: auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); this->Reply(replyTo,"copyPresetsToBank",result); } + else if (message == "downloadModelsFromTone3000") + { + struct DownloadModelsFromTone3000Body { + std::string downloadPath_; + std::string tone3000Url_; + }; + DownloadModelsFromTone3000Body args; + pReader->read(&args); + auto result = this->model.DownloadModelsFromTone3000( + this->clientId, + args.downloadPath_, + args.tone3000Url_ + ); + this->Reply(replyTo,"downloadModelsFromTone3000",result); + } else { Lv2Log::error("Unknown message received: %s", message.c_str()); @@ -1964,10 +1990,6 @@ private: { } } - virtual void OnTone3000AuthChanged(bool value) - { - Send("onTone3000AuthChanged", value); - } virtual void OnErrorMessage(const std::string &message) { diff --git a/src/Storage.cpp b/src/Storage.cpp index 81e283b..2083059 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -44,6 +44,7 @@ #include "AtomConverter.hpp" #include "FileBrowserFilesFeature.hpp" #include +#include "util.hpp" using namespace pipedal; namespace fs = std::filesystem; @@ -375,7 +376,6 @@ void Storage::Initialize() LoadPluginPresetIndex(); LoadBankIndex(); LoadCurrentBank(); - LoadTone3000Auth(); try { LoadChannelSelection(); @@ -434,10 +434,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const return this->dataRoot / "currentPreset.json"; } -std::filesystem::path Storage::GetTone3000AuthPath() const -{ - return this->dataRoot / "tone3000.json"; -} std::filesystem::path Storage::GetChannelSelectionFileName() { @@ -2139,13 +2135,13 @@ static void AddFilesToResult( if (match && !name.starts_with(".")) { resultFiles.push_back( - FileEntry(path, name, false, false)); + FileEntry(path, safeFilenameToString(name), false, false)); } } } else if (dir_entry.is_directory()) { - resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)}); + resultFiles.push_back(FileEntry{path, safeFilenameToString(name), true, fs::is_symlink(path)}); } } } @@ -2325,7 +2321,10 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons { rootModDirectory = modDirectoryInfo; modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; - result.breadcrumbs_.push_back({modDirectoryPath.string(), modDirectoryInfo->displayName}); + result.breadcrumbs_.push_back({ + modDirectoryPath.string(), + (modDirectoryInfo->displayName)} + ); break; } } @@ -2335,7 +2334,9 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons if (IsSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory())) { modDirectoryPath = uploadsDirectory / fileProperty.directory(); - result.breadcrumbs_.push_back({modDirectoryPath.string(), fs::path(fileProperty.directory()).filename().string()}); + result.breadcrumbs_.push_back({ + modDirectoryPath.string(), + safeFilenameToString(fs::path(fileProperty.directory()).filename().string())}); } else { @@ -2360,7 +2361,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons while (iRp != rp.end()) { cumulativePath /= (*iRp); - result.breadcrumbs_.push_back({cumulativePath, *iRp}); + result.breadcrumbs_.push_back({cumulativePath, safeFilenameToString(*iRp)}); ++iRp; } } @@ -2450,7 +2451,7 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const while (iAbsolutePath != fsAbsolutePath.end()) { cumulativePath /= (*iAbsolutePath); - result.breadcrumbs_.push_back({cumulativePath.string(), iAbsolutePath->string()}); + result.breadcrumbs_.push_back({cumulativePath.string(), safeFilenameToString(iAbsolutePath->string())}); ++iAbsolutePath; } } @@ -2978,46 +2979,7 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex() return pluginPresetIndex; } -void Storage::LoadTone3000Auth() -{ - fs::path path = GetTone3000AuthPath(); - try - { - if (!fs::exists(path)) - { - this->tone3000Auth = ""; - return; - } - std::ifstream s(path); - json_reader reader(s); - reader.read(&(this->tone3000Auth)); - } - catch (const std::exception &e) - { - Lv2Log::error("Failed to load tone3000Auth: %s", e.what()); - } -} -void Storage::SetTone3000Auth(const std::string &apiKey) -{ - if (tone3000Auth != apiKey) - { - tone3000Auth = apiKey; - - pipedal::ofstream_synced os(this->GetTone3000AuthPath()); - if (!os.is_open()) - { - Lv2Log::error("Failed to open Tone3000 auth file for writing."); - return; - } - json_writer writer(os); - writer.write(apiKey); - } -} -std::string Storage::GetTone3000Auth() const -{ - return tone3000Auth; -} std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath) { diff --git a/src/Storage.hpp b/src/Storage.hpp index 21ae5ba..45b1532 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -75,7 +75,6 @@ private: BankIndex bankIndex; BankFile currentBank; PluginPresetIndex pluginPresetIndex; - std::string tone3000Auth; private: void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; @@ -90,7 +89,6 @@ private: std::filesystem::path GetChannelSelectionFileName(); std::filesystem::path GetAlsaSequencerConfigurationFileName(); std::filesystem::path GetCurrentPresetPath() const; - std::filesystem::path GetTone3000AuthPath() const; void LoadBankIndex(); void SaveBankIndex(); @@ -267,9 +265,6 @@ public: bool overwrite = false); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); - void LoadTone3000Auth(); - void SetTone3000Auth(const std::string&apiKey); - std::string GetTone3000Auth() const; std::vector RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); diff --git a/src/Tone3000Download.cpp b/src/Tone3000Download.cpp new file mode 100644 index 0000000..14bd553 --- /dev/null +++ b/src/Tone3000Download.cpp @@ -0,0 +1,612 @@ +/* + * Copyright (c) 2026 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ss.hpp" +#include "Tone3000Download.hpp" +#include "TemporaryFile.hpp" +#include +#include +#include "util.hpp" + +using namespace pipedal; +using namespace pipedal::tone3000; +namespace fs = std::filesystem; + +JSON_MAP_BEGIN(Tone3000Model) +JSON_MAP_REFERENCE(Tone3000Model, id) +JSON_MAP_REFERENCE(Tone3000Model, name) +JSON_MAP_REFERENCE(Tone3000Model, model_url) +JSON_MAP_REFERENCE(Tone3000Model, created_at) +JSON_MAP_REFERENCE(Tone3000Model, size) +JSON_MAP_REFERENCE(Tone3000Model, user_id) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000User) +JSON_MAP_REFERENCE(Tone3000User, id) +JSON_MAP_REFERENCE(Tone3000User, username) +JSON_MAP_REFERENCE(Tone3000User, avatar_url) +JSON_MAP_REFERENCE(Tone3000User, url) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000Download) +JSON_MAP_REFERENCE(Tone3000Download, id) +JSON_MAP_REFERENCE(Tone3000Download, user_id) +JSON_MAP_REFERENCE(Tone3000Download, title) +JSON_MAP_REFERENCE(Tone3000Download, description) +JSON_MAP_REFERENCE(Tone3000Download, created_at) +JSON_MAP_REFERENCE(Tone3000Download, updated_at) +JSON_MAP_REFERENCE(Tone3000Download, platform) +JSON_MAP_REFERENCE(Tone3000Download, gear) +JSON_MAP_REFERENCE(Tone3000Download, images) +JSON_MAP_REFERENCE(Tone3000Download, is_public) +JSON_MAP_REFERENCE(Tone3000Download, links) +JSON_MAP_REFERENCE(Tone3000Download, model_count) +JSON_MAP_REFERENCE(Tone3000Download, favorites_count) +JSON_MAP_REFERENCE(Tone3000Download, license) +JSON_MAP_REFERENCE(Tone3000Download, sizes) +JSON_MAP_REFERENCE(Tone3000Download, user) +JSON_MAP_REFERENCE(Tone3000Download, models) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone3000DownloadResult) +JSON_MAP_REFERENCE(Tone3000DownloadResult, success) +JSON_MAP_REFERENCE(Tone3000DownloadResult, errorMessage) +JSON_MAP_END() + +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; + +static std::string shellEscape(const std::string &input) +{ + // Escape string for safe use in shell commands by using single quotes + // and escaping any single quotes in the input + std::string result = "'"; + for (char c : input) + { + if (c == '\'') + { + // End quote, add escaped single quote, start quote again + result += "'\\''"; + } + else + { + result += c; + } + } + result += "'"; + return result; +} + +static std::string getCurlErrorFromExitCode(int exitCode) +{ + + switch (exitCode) + { + case 3 * 256: // malformed URL + return "Invalid URL."; + case 6 * 256: // could not resolve hostname + return "Could not resolve hostname. Check that the PiPedal server is connected to the Internet."; + case 7 * 256: // failed to connect to host + return "Failed to connect to host."; + case 22 * 256: // HTTP error + return "HTTP error downloading file."; + case 28 * 256: // timeout + return "Download timeout."; + case 63 * 256: // file too large + return "File too large."; + default: + return SS("Failed to download file (exit code: " << (exitCode / 256) << ")"); + } +} +static void downloadFile(const std::string &url, const fs::path &path) +{ + // Create temporary file for HTTP status code + TemporaryFile httpCodeFile{WEB_TEMP_DIR}; + fs::path httpCodePath = httpCodeFile.Path(); + + // Build curl command with error detection and proper escaping + std::ostringstream os; + os << "/usr/bin/curl -f -s -S -L --max-time 300 --max-filesize 104857600"; + os << " -w '%{http_code}' -o "; + os << shellEscape(path.string()); + os << " "; + os << shellEscape(url); + os << " > "; + os << shellEscape(httpCodePath.string()); + + std::string command = os.str(); + + // Execute curl command + int exitCode = std::system(command.c_str()); + + // Read HTTP status code + int httpCode = 0; + if (fs::exists(httpCodePath)) + { + std::ifstream httpCodeStream(httpCodePath); + std::string httpCodeStr; + std::getline(httpCodeStream, httpCodeStr); + try + { + httpCode = std::stoi(httpCodeStr); + } + catch (...) + { + // If parsing fails, keep httpCode as 0 + } + } + + if (exitCode != 0) + { + if (exitCode == 22 * 256) // HTTP error + { + throw std::runtime_error( + SS("Server was unabled to download the file. Http error: " << httpCode << " url: " << url)); + } + else + { + throw std::runtime_error( + SS("Server was unable to download the file. " + << getCurlErrorFromExitCode(exitCode) + << " " << "url: " << url)); + } + } + + // Verify file was downloaded + if (!fs::exists(path) || fs::file_size(path) == 0) + { + throw std::runtime_error("Downloaded file is empty or missing"); + } +} + + +static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") +{ + std::ostringstream os; + for (char c : str) + { + if ((unsigned char)c < 0x20) + continue; + if (c == '<') + { + os << "<"; + continue; + } + auto npos = illegalCharacters.find_first_of(c); + if (npos != std::string::npos) + { + continue; + } + os << c; + } + return os.str(); +} + + +static std::string mdDate(const tone3000_time_point &date_) +{ + std::ostringstream os; + // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" + std::string date = date_; + + // Remove timezone offset if present and replace with Z + size_t plusPos = date.find('+'); + if (plusPos != std::string::npos) + { + date = date.substr(0, plusPos) + "Z"; + } + // Parse ISO 8601 date string into tm + std::tm tm = {}; + std::istringstream ss(date); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + os << date; + return os.str(); + } + + os << std::put_time(&tm, "%x"); + return os.str(); +} + +static std::string mdEnum(const std::string &value, const std::map &enumValues) +{ + auto ff = enumValues.find(value); + if (ff == enumValues.end()) + { + return value; + } + return ff->second; +} + +static std::string mdEnumList( + const std::vector &values, + const std::map &enumValues) +{ + if (values.size() == 1) + { + return mdEnum(values[0], enumValues); + } + std::ostringstream os; + os << '['; + bool first = true; + for (const auto &value : values) + { + if (first) + { + first = false; + } + else + { + os << ", "; + } + os << mdEnum(value, enumValues); + } + os << ']'; + return os.str(); +} + +static std::map sizeEnumValues = + { + {"standard", "Standard"}, + {"lite", "Lite"}, + {"feather", "Feather"}, + {"nano", "Nano"}, + {"custom", "Custom"}}; +static std::string mdSizes(const std::vector &sizes) +{ + return mdEnumList(sizes, sizeEnumValues); +} + +static std::map gearEnumValues = + { + {"amp", "Amp only"}, + {"full-rig", "Full rig"}, + {"pedal", "Pedal"}, + {"outboard", "Outboard"}, + {"ir", "I/R"}}; + +static std::string mdGear(const std::string &gear) +{ + return mdEnum(gear, gearEnumValues); +} + +namespace +{ + enum class LicenseFlags + { + None = 0, + Cc = 1, + By = 2, + CcBy = 3, // = Cc | By + Sa = 4, + Nc = 8, + Nd = 16, + Cc0 = 32, + }; + + static bool operator&(LicenseFlags v1, LicenseFlags v2) + { + return (((int)v1) & ((int)v2)) != 0; + } + static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) + { + return (LicenseFlags)(((int)(v1)) | ((int)(v2))); + } + + struct LicenseInfo + { + std::string key; + std::string displayName; + std::string url; + LicenseFlags licenseFlags; + }; +} + +static std::vector licenses{ + { + .key = "t3k", + .displayName = "t3k", + .url = "", + .licenseFlags = LicenseFlags::None + }, + { + .key = "cc-by", + .displayName = "CC BY 4.0", + .url = "https://creativecommons.org/licenses/by/4.0/", + .licenseFlags = LicenseFlags::CcBy + }, + { + .key = "cc-by-sa", + .displayName = "CC BY-SA 4.0", + .url = "https://creativecommons.org/licenses/by-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa + }, + { + .key = "cc-by-nc", + .displayName = "CC BY-NC 4.0", + .url = "https://creativecommons.org/licenses/by-nc/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc + }, + { + .key = "cc-by-nc-sa", + .displayName = "CC BY-NC-SA 4.0", + .url = "https://creativecommons.org/licenses/by-nc-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa + }, + { + .key = "cc-by-nd", + .displayName = "CC BY-ND", + .url = "https://creativecommons.org/licenses/by-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd + }, + { + .key = "cc-by-nc-nd", + .displayName = "CC BY-NC-ND", + .url = "https://creativecommons.org/licenses/by-nc-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd + }, + { + .key = "cco", + .displayName = "CC0", + .url = "https://creativecommons.org/publicdomain/zero/1.0/", + .licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0, + + }, +}; + +static std::map makeLicenseEnum() +{ + std::map result; + for (const auto &license : licenses) + { + result[license.key] = license; + } + return result; +} + +static std::map licenseEnum = makeLicenseEnum(); + +static std::string mdLicenseIcon(LicenseFlags licenseFlag) +{ + std::ostringstream os; + std::string url; + + switch (licenseFlag) + { + case LicenseFlags::Cc: + url = "img/cc.svg"; + break; + case LicenseFlags::By: + url = "img/by.svg"; + break; + case LicenseFlags::Sa: + url = "img/sa.svg"; + break; + case LicenseFlags::Nc: + url = "img/nc.svg"; + break; + case LicenseFlags::Nd: + url = "img/nd.svg"; + break; + case LicenseFlags::Cc0: + url = "img/cc0.svg"; + break; + + case LicenseFlags::None: + throw std::runtime_error("Invalid argument."); + } + os << ""; + return os.str(); +} +static std::string mdLicense(const std::string &license) +{ + auto ff = licenseEnum.find(license); + LicenseInfo licenseInfo; + if (ff != licenseEnum.end()) + { + licenseInfo = ff->second; + } + else + { + licenseInfo.displayName = license; + } + + std::ostringstream os; + + if (licenseInfo.licenseFlags != LicenseFlags::None) { + for (LicenseFlags licenseFlag: std::vector{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) + { + if (licenseInfo.licenseFlags & licenseFlag) + { + os << mdLicenseIcon(licenseFlag); + } + } + os << " "; + } + + if (licenseInfo.url != "") + { + os << "" << mdSanitize(licenseInfo.displayName) << ""; + } + else + { + os << mdSanitize(licenseInfo.displayName); + } + return os.str(); +} +static std::map platformEnumValues = + { + {"nam", "NAM"}, + {"ir", "I/R"}, + {"aida-x", "Aida X"}, + {"aa-snapshot", "aa-snapshot"}, + {"proteus", "Proteus"}}; +static std::string mdPlatform(const std::string &platform) +{ + return mdEnum(platform, platformEnumValues); +} +static std::string mdUser(const tone3000::Tone3000User &user) +{ + // clang-format off + std::ostringstream os; + os << + "" << mdSanitize(user.username()) << ""; + return os.str(); + // clang-format on +} + +static void mdEscapeString(std::ostream &f, const std::string &str) +{ + size_t ix = 0; + while (ix < str.size()) + { + char c = str[ix++]; + if (c == '\n') + { + if (ix < str.size() && str[ix] == '\n') + { + f << "\n\n"; + } + else + { + f << " \n"; // a
in md. + } + } + else + { + f << c; + } + } +} + +static void mdLink(std::ostream &f, const std::string &label, const std::string &url) +{ + f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")"; +} + +static void mdImage(std::ostream &f, const std::string url) +{ + f << "![" << mdSanitize("Plugin Thumbname", "]") << "](" << mdSanitize(url, "]") << "#tone3000_thumbnail" << ")"; + f << "\n\n"; +} +static void writeReadme(const fs::path &path, const Tone3000Download &download) +{ + std::ofstream of{path}; + if (!of.is_open()) + { + throw std::runtime_error(SS("Unable to create file " << path)); + } + std::string imageLink; + if (download.images().size() > 0) + { + imageLink = download.images()[0]; + } + + // clang-format off + of << + + "
\n" + << " \n" + << "
\n" + << "

" << mdSanitize(download.title()) << "

\n" + << "
\n" + << "

\n" + << mdDate(download.updated_at()) + << " " + << mdUser(download.user()) + << "
\n" + << " " << mdSizes(download.sizes()) << ", " + << mdSizes(download.sizes()) << ", " + << mdGear(download.gear()) << ", " + << mdPlatform(download.platform()) + << "
\n" + << " License: xxx" << mdLicense(download.license()) << "\n" + << "

\n" + << "
\n" + << "
\n" + << "
\n"; + // clang-format on + of << "\n\n"; + mdEscapeString(of, download.description()); + + if (download.links().size() > 0) + { + of << std::endl; + of << "## Links" << std::endl + << std::endl; + for (auto &link : download.links()) + { + of << "- " << "[" << link << "](" << link << ")" << std::endl; + } + } +} + +void pipedal::tone3000::DownloadTone3000Bundle( + const std::filesystem::path &destinationFolder, + const std::string &downloadUrl) +{ + TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR}; + fs::path downloadPath = downloadTemporaryFile.Path(); + + downloadFile(downloadUrl, downloadPath); + + std::ifstream f{downloadPath}; + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open file " << downloadPath)); + } + json_reader reader(f); + + Tone3000Download tone3000Download; + reader.read(&tone3000Download); + + if (tone3000Download.platform() != "nam") + { + throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")")); + } + fs::path bundlePath = destinationFolder / stringToSafeFilename(tone3000Download.title()); + + fs::create_directories(bundlePath); + + for (auto &model : tone3000Download.models()) + { + fs::path modelPath = bundlePath / SS(stringToSafeFilename(model.name()) << ".nam"); + downloadFile(model.model_url(), modelPath); + } + fs::path readmePath = bundlePath / "README.md"; + writeReadme(readmePath, tone3000Download); +} + +Tone3000DownloadResult::Tone3000DownloadResult() + : success_(true) +{ +} +Tone3000DownloadResult::Tone3000DownloadResult(const std::exception &e) + : success_(false), errorMessage_(e.what()) +{ +} diff --git a/src/Tone3000Download.hpp b/src/Tone3000Download.hpp new file mode 100644 index 0000000..822f529 --- /dev/null +++ b/src/Tone3000Download.hpp @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once +#include +#include +#include +#include "json.hpp" +#include +#include + +namespace pipedal +{ + namespace tone3000 + { + using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones) + class Tone3000Model + { + private: + int64_t id_ = -1; + std::string name_; + std::string model_url_; + tone3000_time_point created_at_; + std::string size_; + std::string user_id_; + + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + JSON_GETTER_SETTER_REF(model_url) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(size) + JSON_GETTER_SETTER_REF(user_id) + + DECLARE_JSON_MAP(Tone3000Model); + }; + + class Tone3000User + { + private: + std::string id_; + std::string avatar_url_; + std::string username_; + std::string url_; + + public: + JSON_GETTER_SETTER_REF(id) + JSON_GETTER_SETTER(username) + JSON_GETTER_SETTER(url) + + DECLARE_JSON_MAP(Tone3000User); + }; + + class Tone3000Download + { + private: + int64_t id_ = -1; + std::string user_id_; + std::string title_; + std::string platform_; + std::string description_; + tone3000_time_point created_at_; + tone3000_time_point updated_at_; + std::string gear_; + std::vector images_; + bool is_public_ = true; + std::vector links_; + int64_t model_count_ = 0; + int64_t favorites_count_ = 0; + std::string license_; + std::vector sizes_; + Tone3000User user_; + std::vector models_; + + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(user_id) + JSON_GETTER_SETTER_REF(title) + JSON_GETTER_SETTER_REF(platform) + JSON_GETTER_SETTER_REF(description) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(updated_at) + JSON_GETTER_SETTER_REF(gear) + JSON_GETTER_SETTER_REF(images) + JSON_GETTER_SETTER(is_public) + JSON_GETTER_SETTER_REF(links) + JSON_GETTER_SETTER(model_count) + JSON_GETTER_SETTER(favorites_count) + JSON_GETTER_SETTER_REF(license) + JSON_GETTER_SETTER_REF(sizes) + JSON_GETTER_SETTER_REF(user) + JSON_GETTER_SETTER_REF(models) + + DECLARE_JSON_MAP(Tone3000Download); + }; + + class Tone3000DownloadResult { + private: + bool success_; + std::string errorMessage_; + public: + JSON_GETTER_SETTER(success); + JSON_GETTER_SETTER_REF(errorMessage); + + Tone3000DownloadResult(); + Tone3000DownloadResult(const std::exception &e); + + DECLARE_JSON_MAP(Tone3000DownloadResult); + }; + + extern void DownloadTone3000Bundle + ( + const std::filesystem::path&destinationFolder, + const std::string &downloadUrl + ); + } + +} \ No newline at end of file diff --git a/src/Tone3000Downloader.cpp b/src/Tone3000Downloader.cpp new file mode 100644 index 0000000..24f19eb --- /dev/null +++ b/src/Tone3000Downloader.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2026 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "Tone3000DownloaderImpl.hpp" + +using namespace pipedal; + +std::shared_ptr Tone3000Downloader::Create() +{ + return std::make_shared(); +} + +Tone3000Downloader::Tone3000Downloader() +{ +} + +Tone3000Downloader::~Tone3000Downloader() +{ +} + +Tone3000DownloaderImpl::~Tone3000DownloaderImpl() +{ + Close(); + this->thread = nullptr; +} + +void Tone3000DownloaderImpl::SetListener(Listener *listener) +{ + std::lock_guard lockGuard{this->mutex}; + this->listener = listener; +} + +Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::NextHandle() +{ + handle_t result = this->nextHandle++; + return result; +} +Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload( + const std::string &path, + const std::string &url) +{ + handle_t handle; + { + std::lock_guard lockGuard{this->mutex}; + handle = NextHandle(); + std::shared_ptr request = std::make_shared(false, handle, path, url); + + this->requestQueue.push_back(request); + request = nullptr; + if (!this->thread) + { + this->thread = std::make_unique([this]{ this->ThreadProc(); }); + } + } + this->thread_cv.notify_one(); + return handle; +} + +void Tone3000DownloaderImpl::CancelDownload( + handle_t handle) +{ + std::lock_guard lockGuard{this->mutex}; + for (auto it = requestQueue.begin(); it != requestQueue.end(); ++it) + { + if ((*it)->handle == handle) + { + requestQueue.erase(it); + return; + } + } + if (activeRequest) + { + if (activeRequest->handle == handle) + { + activeRequest->cancelled = true; + } + } +} + +void Tone3000DownloaderImpl::Close() { + bool notify = false; + { + std::lock_guard lockGuard{this->mutex}; + if (!closed) + { + closed = true; + notify = true; + } + + this->requestQueue.resize(0); + + DownloadProgress progress; + fgDownloadProgress = progress; + + if (listener) + { + listener->OnTone3000Progress(progress); + } + if (this->activeRequest != nullptr) + { + activeRequest->cancelled = true; + } + } + if (notify) { + thread_cv.notify_one(); + } +} +Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() { + DownloadProgress result; + { + std::lock_guard lockGuard{this->mutex}; + result = fgDownloadProgress; + } + return result; +} + +void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const DownloadProgress &progress) +{ + std::lock_guard lockGuard{this->mutex}; + if (closed) + { + return; + } + this->fgDownloadProgress = progress; + if (listener) + { + listener->OnTone3000Progress(this->fgDownloadProgress); + } + +} + +void Tone3000DownloaderImpl::ThreadProc() +{ + while (true) + { + std::shared_ptr downloadRequest; + { + std::unique_lock lock { this->mutex}; + this->activeRequest = nullptr; + + this->thread_cv.wait(lock,[this] { return this->closed || this->requestQueue.size() != 0;}); + + if (this->closed) { + return; + } + if (this->requestQueue.size() != 0) + { + this->activeRequest = this->requestQueue[0]; + this->requestQueue.erase(requestQueue.begin()); + } + downloadRequest = this->activeRequest; + } + if (!downloadRequest) + { + continue; + } + + + } +} \ No newline at end of file diff --git a/src/Tone3000Downloader.hpp b/src/Tone3000Downloader.hpp new file mode 100644 index 0000000..f57eaf3 --- /dev/null +++ b/src/Tone3000Downloader.hpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include + +namespace pipedal { + + class Tone3000Downloader { + protected: + Tone3000Downloader(); + public: + using handle_t = int64_t; + static constexpr handle_t INVALID_HANDLE = (handle_t)(int64_t)-1; + + + virtual ~Tone3000Downloader(); + + class DownloadProgress { + public: + private: + handle_t handle_ = INVALID_HANDLE; + std::string title_; + uint64_t progress_ = 0; + uint64_t total_ = 0; + }; + + class Listener { + public: + virtual void OnStartTone3000Download + ( + handle_t handle, + const std::string &title + ) = 0; + virtual void OnTone3000Progress( + const DownloadProgress& downloadProgress + ) = 0; + virtual void OnTone3000DownloadComplete( + handle_t handle + ) = 0; + + }; + + + using self = Tone3000Downloader; + static std::shared_ptr Create(); + + virtual void SetListener(Listener*listener) = 0; + + virtual handle_t RequestDownload( + const std::string &path, + const std::string &url + ) = 0; + + virtual void CancelDownload( + handle_t handle + ) = 0; + + virtual void Close() = 0; + virtual DownloadProgress GetDownloadStatus() = 0; + }; +} \ No newline at end of file diff --git a/src/Tone3000DownloaderImpl.hpp b/src/Tone3000DownloaderImpl.hpp new file mode 100644 index 0000000..be4b538 --- /dev/null +++ b/src/Tone3000DownloaderImpl.hpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include "Tone3000Downloader.hpp" +#include +#include +#include +#include + +namespace pipedal { + + class Tone3000DownloaderImpl: public Tone3000Downloader { + public: + virtual ~Tone3000DownloaderImpl(); + + virtual void SetListener(Listener*listener) override; + + virtual handle_t RequestDownload( + const std::string &path, + const std::string &url + ) override; + + virtual void CancelDownload( + handle_t handle + ) override; + + virtual void Close() override; + virtual DownloadProgress GetDownloadStatus() override; + + private: + Listener *listener = nullptr; + + void ThreadProc(); + struct DownloadRequest { + std::atomic cancelled = false; + handle_t handle; + const std::string downloadPath; + const std::string downloadUrl; + }; + + std::atomic closed = false; + handle_t nextHandle = 1; + handle_t NextHandle(); + + std::vector> requestQueue; + + DownloadProgress fgDownloadProgress; + void bgUpdateDownloadProgress(const DownloadProgress &progress); + + std::shared_ptr activeRequest; + + std::mutex mutex; + std::condition_variable thread_cv; + + + std::unique_ptr thread; + }; +} \ No newline at end of file diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 8045bd6..18f42fa 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -40,6 +40,7 @@ #include "util.hpp" #include "HtmlHelper.hpp" #include "WebServerMod.hpp" +#include "Tone3000Download.hpp" #define OLD_PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset" @@ -145,6 +146,11 @@ public: return false; } std::string segment = request_uri.segment(1); + + if (segment == "tone3000Upload") + { + return true; + } if (segment == "downloadMediaFile") { return true; @@ -189,10 +195,6 @@ public: { return true; } - else if (segment == "Tone3000Auth") - { - return true; - } else if (segment == "PluginPresets") { return true; @@ -418,11 +420,11 @@ public: return true; } auto filename = path.stem(); - if (filename == "LICENSE" || filename == "README") { + if (filename == "LICENSE" || filename == "README") + { return true; } return false; - } virtual void get_response( @@ -435,6 +437,44 @@ public: { std::string segment = request_uri.segment(1); + if (segment == "tone3000Upload") + { + + tone3000::Tone3000DownloadResult result; + try + { + fs::path downloadPath = request_uri.query("path"); + if (downloadPath.empty()) + { + throw std::runtime_error("Invalid query parameters."); + } + if (!this->model->IsInUploadsDirectory(downloadPath) || HasDotDot(downloadPath)) + { + throw std::runtime_error("Invalid path."); + } + std::string downloadUrl = request_uri.query("url"); + uri downloadUri{downloadUrl}; + if (!downloadUri.authority().ends_with(".tone3000.com")) + { + throw std::runtime_error("Invalid Tone3000 URL address."); + } + tone3000::DownloadTone3000Bundle(downloadPath, downloadUrl); + } + catch (const std::exception &e) + { + result = tone3000::Tone3000DownloadResult(e); + } + std::ostringstream os; + json_writer writer(os); + writer.write(&result); + std::string strResult = os.str(); + + res.set(HttpField::content_type,"application/json"); + res.set(HttpField::cache_control, "no-cache"); + res.setBody(strResult); + + return; + } if (segment == "downloadMediaFile") { fs::path path = request_uri.query("path"); @@ -765,19 +805,7 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "Tone3000Auth") - { - // https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true - std::string apiKey = request_uri.query("api_key"); - - model->SetTone3000Auth(apiKey); - - res.set(HttpField::content_type, "application/json"); - res.set(HttpField::cache_control, "no-cache"); - - res.setBody("\"OK\""); - } - else if (segment == "uploadPluginPresets") + if (segment == "uploadPluginPresets") { PluginPresets presets; fs::path filePath = req.get_body_temporary_file(); diff --git a/todo.txt b/todo.txt index 8423926..8431bd7 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,7 @@ - +Tooltip on File Property Select: add to release notes. +sAFEfILEnAMES: RENAME, &C. Safe bank names? +NEW BANK +WINDOW SIZE CALCUALTIONS ARE WRONG ON LINUX HIDEF DISPLAYS. ls Carla Project icons. diff --git a/vite/package-lock.json b/vite/package-lock.json index a1e21da..564c87f 100644 --- a/vite/package-lock.json +++ b/vite/package-lock.json @@ -23,6 +23,9 @@ "react-virtualized-auto-sizer": "^1.0.25", "react-window": "^1.8.11", "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "tss-react": "^4.9.15" }, "devDependencies": { @@ -3537,6 +3540,93 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-from-parse5/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-from-parse5/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", @@ -3550,6 +3640,220 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-raw/node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-sanitize/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-sanitize/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -3653,6 +3957,65 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5/node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -3666,6 +4029,53 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3691,6 +4101,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", @@ -4150,6 +4570,16 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -4192,6 +4622,76 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-from-markdown": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", @@ -4209,6 +4709,616 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-table/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-gfm/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -4940,6 +6050,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -5497,6 +6728,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5767,9 +7022,9 @@ } }, "node_modules/react-markdown/node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -6150,6 +7405,68 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/rehype-raw/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-react": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz", @@ -6164,6 +7481,233 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-gfm/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/remark-gfm/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-gfm/node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/remark-gfm/node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", @@ -6190,6 +7734,128 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-stringify/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/remark-stringify/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-stringify/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-stringify/node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-stringify/node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6813,6 +8479,67 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile-location/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", diff --git a/vite/package.json b/vite/package.json index e1c50b2..bf587ae 100644 --- a/vite/package.json +++ b/vite/package.json @@ -25,6 +25,9 @@ "react-virtualized-auto-sizer": "^1.0.25", "react-window": "^1.8.11", "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "tss-react": "^4.9.15" }, "devDependencies": { diff --git a/vite/public/handleTone3000download.html b/vite/public/handleTone3000download.html index 465155b..f16cf52 100644 --- a/vite/public/handleTone3000download.html +++ b/vite/public/handleTone3000download.html @@ -1,26 +1,42 @@ + Tone3000 Download Received + + \ No newline at end of file diff --git a/vite/public/img/by.svg b/vite/public/img/by.svg new file mode 100644 index 0000000..34099a3 --- /dev/null +++ b/vite/public/img/by.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/vite/public/img/cc.svg b/vite/public/img/cc.svg new file mode 100644 index 0000000..cb08896 --- /dev/null +++ b/vite/public/img/cc.svg @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/vite/public/img/cc0.svg b/vite/public/img/cc0.svg new file mode 100644 index 0000000..100e139 --- /dev/null +++ b/vite/public/img/cc0.svg @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/vite/public/img/nc.svg b/vite/public/img/nc.svg new file mode 100644 index 0000000..fcf2f4b --- /dev/null +++ b/vite/public/img/nc.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/vite/public/img/nd.svg b/vite/public/img/nd.svg new file mode 100644 index 0000000..9ec0da5 --- /dev/null +++ b/vite/public/img/nd.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/vite/public/img/sa.svg b/vite/public/img/sa.svg new file mode 100644 index 0000000..8d5ffde --- /dev/null +++ b/vite/public/img/sa.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/vite/public/testTone300Popup.html b/vite/public/testTone300Popup.html new file mode 100644 index 0000000..2988a91 --- /dev/null +++ b/vite/public/testTone300Popup.html @@ -0,0 +1,44 @@ + + + + Test Tone3000 Select + + + + + +

Test Tone3000 Select Page

+

This is a test page for Tone3000 select functionality.

+ + + + \ No newline at end of file diff --git a/vite/src/pipedal/AudioFileMetadata.tsx b/vite/src/pipedal/AudioFileMetadata.tsx index d38d435..28ff765 100644 --- a/vite/src/pipedal/AudioFileMetadata.tsx +++ b/vite/src/pipedal/AudioFileMetadata.tsx @@ -38,13 +38,78 @@ function getVersionSuffix(filePath: string): string | null { return match ? match[1] : null; } +export function safeFilenameDecode(filename: string): string { + let originalFilename = filename; + let pos = filename.indexOf('%'); + if (pos < 0) { + return filename; + } + let result = ""; + while (true) { + if (pos < 0) { + result += filename; + return result; + } + result += filename.substring(0, pos); + if (pos+3 >= filename.length) { + // invalid encoding. just use the original + return originalFilename; + } + let hex = filename.charAt(pos+1) + filename.charAt(pos+2); + let byte = parseInt(hex, 16); + + // Handle UTF-8 to UTF-16 conversion + if (byte < 0x80) { + // Single-byte character (ASCII) + result += String.fromCharCode(byte); + filename = filename.substring(pos+3); + } else if ((byte & 0xE0) === 0xC0) { + // 2-byte sequence + if (pos+6 > filename.length || filename.charAt(pos+3) !== '%') { + return originalFilename; // invalid encoding + } + let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16); + let codePoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F); + result += String.fromCharCode(codePoint); + filename = filename.substring(pos+6); + } else if ((byte & 0xF0) === 0xE0) { + // 3-byte sequence + if (pos+9 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%') { + return originalFilename; // invalid encoding + } + let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16); + let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16); + let codePoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F); + result += String.fromCharCode(codePoint); + filename = filename.substring(pos+9); + } else if ((byte & 0xF8) === 0xF0) { + // 4-byte sequence (surrogate pair needed) + if (pos+12 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%' || filename.charAt(pos+9) !== '%') { + return originalFilename; // invalid encoding + } + let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16); + let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16); + let byte4 = parseInt(filename.charAt(pos+10) + filename.charAt(pos+11), 16); + let codePoint = ((byte & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F); + // Convert to UTF-16 surrogate pair + codePoint -= 0x10000; + result += String.fromCharCode(0xD800 + (codePoint >> 10)); + result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF)); + filename = filename.substring(pos+12); + } else { + // Invalid UTF-8 start byte + return originalFilename; + } + pos = filename.indexOf('%'); + } +} export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string { if (!metadata) { - return pathFileName(pathname); + return safeFilenameDecode(pathFileName(pathname)); } if (metadata.title === "") { - return pathFileName(pathname); + return safeFilenameDecode(pathFileName(pathname)); } let trackDisplay = ""; if (metadata.track > 0) { @@ -56,7 +121,7 @@ export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | nu } let result = trackDisplay + metadata.title; if (result === "") { - result = pathFileName(pathname); + result = safeFilenameDecode(pathFileName(pathname)); } let versionSuffix = getVersionSuffix(pathname); if (versionSuffix) { diff --git a/vite/src/pipedal/FilePropertyControl.tsx b/vite/src/pipedal/FilePropertyControl.tsx index 5419933..df6be1a 100644 --- a/vite/src/pipedal/FilePropertyControl.tsx +++ b/vite/src/pipedal/FilePropertyControl.tsx @@ -25,6 +25,7 @@ import { Component, SyntheticEvent } from 'react'; import { Theme } from '@mui/material/styles'; import { css } from '@emotion/react'; +import ToolTipEx from './ToolTipEx'; import { UiFileProperty } from './Lv2Plugin'; import Typography from '@mui/material/Typography'; @@ -33,15 +34,16 @@ import ButtonBase from '@mui/material/ButtonBase' import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { PedalboardItem } from './Pedalboard'; import { isDarkMode } from './DarkMode'; - - -export const StandardItemSize = { width: 80, height: 140 } +import {safeFilenameDecode} from './AudioFileMetadata'; import { withStyles } from "tss-react/mui"; import WithStyles from './WithStyles'; import { withTheme } from './WithStyles'; + +export const StandardItemSize = { width: 80, height: 140 } + const styles = (theme: Theme) => { return { frame: css({ @@ -209,11 +211,13 @@ const FilePropertyControl = let fileProperty = this.props.fileProperty; let value = "\u00A0"; + let toolTipText: string | null = null; if (this.state.hasValue) { if (this.state.value.length === 0) { value = ""; } else { - value = this.fileNameOnly(this.state.value); + value = safeFilenameDecode(this.fileNameOnly(this.state.value)); + toolTipText = value; } } @@ -233,7 +237,7 @@ const FilePropertyControl = {/* CONTROL SECTION */}
- + { this.onFileClick() }} >
 
+ {/* LABEL/EDIT SECTION*/} diff --git a/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index da6b505..1b7b439 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -21,8 +21,6 @@ import React from 'react'; import { createStyles } from './WithStyles'; -import Tone3000Dialog from './Tone3000Dialog'; - import TextInfoDialog from './TextInfoDialog'; import Tone3000HelpDialog from './Tone3000HelpDialog'; import GuitarMLHelpDialog from './GuitarMlHelpDialog'; @@ -69,6 +67,7 @@ import OkCancelDialog from './OkCancelDialog'; import HomeIcon from '@mui/icons-material/Home'; import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; +import { Tone3000DownloadDialog } from './Tone3000Dialog'; const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile"; @@ -705,7 +704,7 @@ export default withStyles( if (this.state.previousSelection == selectedItem) { return; } - if (!this.isLicenseFile(selectedItem) && !this.isFolderArtwork(selectedItem)) { + if (!this.isTextFile(selectedItem) && !this.isFolderArtwork(selectedItem)) { this.props.onApply(fileProperty, selectedItem); } this.setState({ previousSelection: selectedItem }); @@ -1071,9 +1070,7 @@ export default withStyles( handleTone3000Dialog(e: React.MouseEvent) { e.stopPropagation(); e.preventDefault(); - this.setState({ openTone3000Dialog: true }); - } @@ -1743,14 +1740,13 @@ export default withStyles( ) } {this.state.openTone3000Dialog && ( - this.setState({ openTone3000Dialog: false })} - onDownload={(toneUrl: string) => { - alert("Donload tone url: " + toneUrl); + this.setState({ openTone3000Dialog: false })} + downloadPath={this.state.currentDirectory} + onDownloadComplete={() => { this.setState({ openTone3000Dialog: false }); - } - } + this.requestFiles(this.state.navDirectory); + }} /> )} {this.state.openTone3000Help && ( @@ -1774,15 +1770,11 @@ export default withStyles( ); } - isLicenseFile(fileName: string) { + isTextFile(fileName: string) { let extension = pathExtension(fileName); if (extension === ".txt" || extension === ".md") { return true; } - let fileNameOnly = pathFileNameOnly(fileName); - if (fileNameOnly.toUpperCase() === "LICENSE" || fileNameOnly.toUpperCase() === "README") { - return true; - } return false; } @@ -1801,7 +1793,7 @@ export default withStyles( this.requestFiles(this.state.selectedFile); this.setState({ navDirectory: this.state.selectedFile }); } else { - if (this.isLicenseFile(this.state.selectedFile)) { + if (this.isTextFile(this.state.selectedFile)) { this.handleShowTextFile(this.state.selectedFile); } else { this.props.onOk(this.props.fileProperty, this.state.selectedFile); diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index aaa0152..45f3c78 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -27,6 +27,7 @@ import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard import PluginClass from './PluginClass'; import ScreenOrientation from './ScreenOrientation'; import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket'; +import {Tone3000DownloadHandler} from './Tone3000Dialog'; import { nullCast } from './Utility' import { JackConfiguration, JackChannelSelection } from './Jack'; import { BankIndex } from './Banks'; @@ -60,7 +61,7 @@ export enum State { HotspotChanging, }; -function getErrorMessage(error: any) { +export function getErrorMessage(error: any) { if (error instanceof Error) { return (error as Error).message; } @@ -494,7 +495,6 @@ export class PiPedalModel //implements PiPedalModel static getInstance(): PiPedalModel { return PiPedalModelFactory.getInstance(); } - hasTone3000Auth: ObservableProperty = new ObservableProperty(false); canKeepScreenOn: boolean = false; keepScreenOn: ObservableProperty = new ObservableProperty(false); @@ -552,6 +552,8 @@ export class PiPedalModel //implements PiPedalModel uiPluginsByUri: Map = new Map(); + private tone3000DownloadHandler: Tone3000DownloadHandler | null = null; + svgImgUrl(svgImage: string): string { // return this.varServerUrl + "img/" + svgImage; return "img/" + svgImage; @@ -651,6 +653,53 @@ export class PiPedalModel //implements PiPedalModel return true; } + + async downloadModelsFromTone3000( + tone3000DownloadUrl: string, + downloadPath: string + ): Promise { + try { + if (!this.webSocket) { + return; + } + this.webSocket.send("downloadModelsFromTone3000", + { + downloadPath: downloadPath, + tone3000Url: tone3000DownloadUrl + }); + } catch (error) { + this.showAlert(getErrorMessage(error)); + } + } + + showTone3000DownloadDialog( + downloadPath: string, + onClosed: () => void, + onDownloadStarted: () => void, + onDownloadComplete: () => void + + ): void { + if (this.tone3000DownloadHandler === null) { + this.tone3000DownloadHandler = new Tone3000DownloadHandler(this); + } + this.tone3000DownloadHandler.launchTone3000Dialog( + downloadPath, + onClosed, + (errorMessage: string) => { + this.showAlert(errorMessage); + onClosed(); + }, + onDownloadStarted, + onDownloadComplete + ); + + }; + closeTone3000DownloadDialog(): void { + if (this.tone3000DownloadHandler) { + this.tone3000DownloadHandler.closeTone3000Dialog(); + } + } + private updateEnabledItems(pedalboard: Pedalboard) { for (let item of pedalboard.itemsGenerator()) { this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled); @@ -862,10 +911,7 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onErrorMessage") { this.showAlert(body as string); - } else if (message == "onTone3000AuthChanged") { - this.hasTone3000Auth.set(body as boolean); - } - else if (message === "onLv2PluginsChanging") { + } else if (message === "onLv2PluginsChanging") { this.onLv2PluginsChanging(); } else if (message === "onUpdateStatusChanged") { let updateStatus = new UpdateStatus().deserialize(body); @@ -1256,9 +1302,6 @@ export class PiPedalModel //implements PiPedalModel this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize( await this.getWebSocket().request("getAlsaSequencerConfiguration") )); - this.hasTone3000Auth.set( - await this.getWebSocket().request("getHasTone3000Auth") - ); this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request("getBankIndex"))); this.favorites.set(await this.getWebSocket().request("getFavorites")); @@ -1539,8 +1582,7 @@ export class PiPedalModel //implements PiPedalModel this.webSocket?.send("setSnapshot", index); } - private pruneSnapshotValues(pedalboard: Pedalboard) - { + private pruneSnapshotValues(pedalboard: Pedalboard) { let validPluginIds: Set = new Set(); let it = pedalboard.itemsGenerator(); @@ -1572,7 +1614,7 @@ export class PiPedalModel //implements PiPedalModel pedalboard.selectedSnapshot = selectedSnapshot; } this.pruneSnapshotValues(pedalboard); - + this.setModelPedalboard(pedalboard); this.webSocket?.send("setSnapshots", { snapshots: pedalboard.snapshots, selectedSnapshot: selectedSnapshot }); @@ -2752,6 +2794,29 @@ export class PiPedalModel //implements PiPedalModel document.body.removeChild(link); } + async uploadTone3000File(url: string) : Promise { + try { + let response = await fetch( + url, + { + method: "GET", + } + ); + + let json = await response.json(); + + if (json.success === true) { + return true; + } + if (json.errorMessage !== undefined) + { + throw new Error(json.errorMessage); + } + throw new Error("Invalid server response."); + } catch (error) { + throw new Error("Upload to PiPedal server failed. " + getErrorMessage(error)); + } + } uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { let result = new Promise((resolve, reject) => { try { diff --git a/vite/src/pipedal/TextInfoDialog.css b/vite/src/pipedal/TextInfoDialog.css new file mode 100644 index 0000000..aa5a678 --- /dev/null +++ b/vite/src/pipedal/TextInfoDialog.css @@ -0,0 +1,50 @@ + +.text-info-dialog-content #user-content-tone3000_float { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: justify; + margin-top: 8px; + margin-bottom: 32px; +} + +.text-info-dialog-content #user-content-tone3000_float_content { + flex-grow: 1; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: space-between; + +} + +.text-info-dialog-content #user-content-tone3000_title { + margin-top: 0; + margin-bottom: 12px; + padding: 0; +} +.text-info-dialog-content #user-content-tone3000_subtitle { + font-size: 0.8em; + margin-top: 0px; + margin-bottom: 0px; + padding: 0; +} + +.text-info-dialog-content #user-content-cc_img { + width: 24px; + height: 24px; +} +.text-info-dialog-content #user-content-tone3000_thumbnail { + width: 130px; + height: auto; + object-fit: contain; + margin-right: 24px; + border: 1px solid var(--pd-border-color); + border-radius: 6px; + align-self: flex-start; +} +@media (max-width: 600px) { + .text-info-dialog-content #user-content-tone3000_thumbnail { + width: 80px; + margin-right: 20px; + } +} \ No newline at end of file diff --git a/vite/src/pipedal/TextInfoDialog.tsx b/vite/src/pipedal/TextInfoDialog.tsx index 694ffb0..be9f32c 100644 --- a/vite/src/pipedal/TextInfoDialog.tsx +++ b/vite/src/pipedal/TextInfoDialog.tsx @@ -20,6 +20,7 @@ import React, { SyntheticEvent } from 'react'; +import './TextInfoDialog.css'; import IconButtonEx from './IconButtonEx'; import Typography from '@mui/material/Typography'; import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel'; @@ -27,7 +28,10 @@ import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize, {defaultSchema} from 'rehype-sanitize'; import rehypeExternalLinks from 'rehype-external-links' +import remarkGfm from 'remark-gfm'; import { PiPedalError } from './PiPedalError'; import DialogEx from './DialogEx'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; @@ -139,7 +143,6 @@ const TextInfoDialog = class extends ResizeResponsiveComponent { this.props.onClose() }} style={{ userSelect: "none" }} > -
-
- +
+ {this.state.textFileContent}
diff --git a/vite/src/pipedal/Tone3000Dialog.tsx b/vite/src/pipedal/Tone3000Dialog.tsx index d585447..7d094d4 100644 --- a/vite/src/pipedal/Tone3000Dialog.tsx +++ b/vite/src/pipedal/Tone3000Dialog.tsx @@ -1,125 +1,261 @@ +import { JSX } from "@emotion/react/jsx-dev-runtime"; +import { PiPedalModel, getErrorMessage } from "./PiPedalModel"; +import DialogEx from "./DialogEx"; +import DialogContent from "@mui/material/DialogContent"; +import DialogAction from "@mui/material/DialogActions"; +import { Button, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import LinearProgress from "@mui/material/LinearProgress"; -import React from 'react'; -import AppBar from '@mui/material/AppBar'; -import Typography from '@mui/material/Typography'; -import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; -import DialogEx from './DialogEx'; -import Toolbar from '@mui/material/Toolbar'; -import IconButtonEx from './IconButtonEx'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +export type OnTone3000DownloadHandler = (tone3000DownloadUrl: string) => void; +// used to check for online status. +let TONE3000_PING_URL = "https://www.tone3000.com/robots.txt"; +export class Tone3000DownloadHandler { -export interface Tone3000DialogProps { - open: boolean; - onCancel: () => void; - onDownload: (toneUrl: string) => void; -} + private async handleTone3000Download(tone3000DownloadUrl: string): Promise { + try { + // Dialog can close. We'll use the following await to manage + // lifecycle from here onward. + this.stopTone3000DialogMonitor(); -interface Tone3000AuthState { - authToken: string; - refreshToken: string; - expiry: number; -}; - -// function getAuthToken(): Tone3000AuthState | null { - -// let authToken = window.localStorage.getItem("tone300_auth_token"); -// if (authToken === null) { -// return null; -// } -// let refreshToken = window.localStorage.getItem("tone3000_refresh_token"); -// if (refreshToken === null) { -// return null; -// } -// let expiry = window.localStorage.getTime("tone3000_auth_expiry"); -// return { authToken: authToken, refreshToken: refreshToken, expiry: expiry }; -// } - -// function setAuthToken(authToken: string, refreshToken: string, expirySeconds: number) { -// window.localStorage.setItem("tone3000_auth_token", authToken); -// window.localStorage.setItem("tone3000_refresh_token", refreshToken); -// window.localStorage.setItem("tone3000_auth_expiry", String(Date.now() + (expirySeconds * 1000))); -// } - -// enum AuthState { -// OnBoarding, -// TokenExpired, -// Authenticated -// } - -function Tone3000Dialog(props: Tone3000DialogProps) { - const { onCancel,onDownload } = props; - - function RedirectUrl() { - let baseUrl = new URL(window.location.href); - let result = "http://" + baseUrl.hostname + ":" + baseUrl.port + "/public/handleTone3000download.html"; - return result; - } - const appId = 'pipedal_app'; - const redirectUrl = RedirectUrl(); - - let selectUrl = `https://www.tone3000.com/api/v1/select?app_id=${appId}&redirect_url=${redirectUrl}`; - - const model: PiPedalModel = PiPedalModelFactory.getInstance(); - if (model) { + this.handleTone3000DownloadComplete(); + await this.model.downloadModelsFromTone3000(tone3000DownloadUrl,this.downloadPath); + return; + } catch (e) { + this.handleTone3000DownloadError(getErrorMessage(e)); + return; + } } - React.useEffect(()=> { + private messageEventListener: ((event: MessageEvent) => void) | null = null; - let messagehandler = (event: MessageEvent) => { - // Handle messages received from the iframe - if (event.origin !== "https://www.tone3000.com") { + private model: PiPedalModel; + + public constructor(model: PiPedalModel) { + this.model = model; + this.messageEventListener = (event: MessageEvent) => { + if (event.data.type !== "tone3000Download") { return; } - const data = event.data; - if (data.type === "tone3000_selection") { - onDownload(data.toneUrl); + this.handleTone3000Download(event.data.toneUrl as string); + }; + + window.addEventListener("message", this.messageEventListener); + } + + public dispose(): void { + if (this.messageEventListener) { + window.removeEventListener("message", this.messageEventListener); + this.messageEventListener = null; + } + } + + private popupWindow: Window | null = null; + + private appId: string = "pipedal_app"; + private redirectUrl(): string { + let hostname = window.location.hostname; + let port = window.location.port; + let protocol = window.location.protocol; + let serverUrl: string; + if (protocol === "http:" && (port === "80" || port === "")) { + serverUrl = `${protocol}//${hostname}`; + } else if (protocol === "https:" && (port === "443" || port === "")) { + serverUrl = `${protocol}//${hostname}`; + } else { + serverUrl = `${protocol}//${hostname}:${port}`; + } + return `${serverUrl}/public/handleTone3000download.html`; + } + + private tone3000SelectUrl(): string { + return `https://www.tone3000.com/api/v1/select?app_id=${this.appId}&redirect_url=${this.redirectUrl()}`; + } + + + private handleTone3000DialogClosed() { + let t = this.onClosedCallback; + this.onClosedCallback = undefined; + this.onErrorCallback = undefined; + this.onDownloadCompleteCallback = undefined; + if (t) { + t(); + } + this.stopTone3000DialogMonitor(); + } + private handleTone3000Downloadstarted() { + if (this.onDownloadStartedCallback) { + this.onDownloadStartedCallback(); + } + } + private handleTone3000DownloadComplete() { + let t = this.onDownloadCompleteCallback; + this.onClosedCallback = undefined; + this.onErrorCallback = undefined; + this.onDownloadCompleteCallback = undefined; + if (t) { + t(); + } + this.stopTone3000DialogMonitor(); + } + private handleTone3000DownloadError(errorMessage: string) { + let t = this.onErrorCallback; + this.onClosedCallback = undefined; + this.onErrorCallback = undefined; + this.onDownloadCompleteCallback = undefined; + if (t) { + t(errorMessage); + } + this.stopTone3000DialogMonitor(); + } + + private tone3000DialogInterval: number | undefined = undefined; + + private stopTone3000DialogMonitor() { + if (this.tone3000DialogInterval !== undefined) { + clearInterval(this.tone3000DialogInterval); + this.tone3000DialogInterval = undefined; + } + } + private startTone3000DialogMonitor() { + this.tone3000DialogInterval = setInterval(() => { + if (this.popupWindow == null || this.popupWindow.closed) { + this.stopTone3000DialogMonitor(); + this.popupWindow = null; + this.handleTone3000DialogClosed(); + } + }, 500); + } + private downloadPath: string = ""; + private onClosedCallback: (() => void) | undefined = undefined; + private onErrorCallback: ((errorMessage: string) => void) | undefined = undefined; + private onDownloadStartedCallback: (() => void) | undefined = undefined; + private onDownloadCompleteCallback: (() => void) | undefined = undefined; + + public launchTone3000Dialog( + downloadPath: string, + onClosed: () => void, + onError: (errorMessage: string) => void, + onDownloadStarted: () => void, + onDownloadComplete: () => void + ) { + this.closeTone3000Dialog(); + this.downloadPath = downloadPath; + this.onClosedCallback = onClosed; + this.onDownloadCompleteCallback = onDownloadComplete; + this.onDownloadStartedCallback = onDownloadStarted; + this.onErrorCallback = onError; + + fetch(TONE3000_PING_URL, { cache: "no-cache" }) + .then((response) => { + if (!response.ok) { + this.handleTone3000DownloadError(`Unable to connect to Tone3000: ${response.status}${response.statusText}`); + return; + } + + // online + let popupWidth = Math.floor(window.innerWidth * 0.8); + let popupHeight = Math.floor(window.innerHeight * 0.8); + if (window.innerWidth < 410) { + popupWidth = 400; + } + + this.popupWindow = + window.open( + this.tone3000SelectUrl(), + "popup", + `innerWidth=${popupWidth},innerHeight=${popupHeight},scrollbars=yes` + ); + this.startTone3000DialogMonitor(); + + if (!this.popupWindow) { + console.error("Failed to open Tone3000 dialog popup window."); + this.handleTone3000DownloadError("Cannot open popup window."); + return; + } + }) + .catch((error) => { + // offline + this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers."); + return; + }); + + } + public closeTone3000Dialog(): void { + this.handleTone3000DialogClosed(); + this.stopTone3000DialogMonitor(); + if (this.popupWindow) { + try { + + let t = this.popupWindow; + this.popupWindow = null; + t.close(); + } catch (e) { + console.error("Error closing Tone3000 dialog popup:", e); } } - window.addEventListener("message", messagehandler); - - return () => { - window.removeEventListener("message", messagehandler); - } - }); - - - return ( - { onCancel()}} fullScreen={true} tag="tone3000Dlg" onEnterKey={()=>{}} > - -
-
- - - onCancel()} - aria-label="back" - > - - - - Tone3000 Direct Download - - - -
- - -
-
- - ); + } } -export default Tone3000Dialog; \ No newline at end of file +export interface Tone3000DownloadDialogProps { + + onClose: () => void, + onDownloadComplete: () => void, + downloadPath: string +} +export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.Element { + const model = PiPedalModel.getInstance(); + let { onClose, onDownloadComplete, downloadPath } = props; + let [downloading, setDownloading] = useState(false); + + useEffect(() => { + model.showTone3000DownloadDialog( + downloadPath, + () => { + onClose(); + }, + () => { + setDownloading(true); + }, + () => { + onDownloadComplete(); + } + ); + return () => { + model.closeTone3000DownloadDialog(); + }; + }); + return ( + { + onClose(); + }} + onEnterKey={() => { }} + > + + Downloading from Tone3000... + {downloading && +
+ +
+ } +
+ + {!downloading && ( + + )} + +
+ ); +}