diff --git a/.Rbuildignore b/.Rbuildignore index b127c8e..11828ff 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -12,3 +12,4 @@ ^cran-comments\.md$ ^revdep$ ^CRAN-SUBMISSION$ +^\.Rproj\.user$ diff --git a/.gitignore b/.gitignore index d911262..c859c3b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ local/quarto/*.html local/quarto/*_files/ local/quarto/*.js local/quarto/_extensions/ +.Rproj.user diff --git a/DESCRIPTION b/DESCRIPTION index 2eb88bb..e4c829d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,7 +13,7 @@ Description: Exporting 'shiny' applications with 'shinylive' allows you to run t License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 BugReports: https://github.com/posit-dev/r-shinylive/issues URL: https://posit-dev.github.io/r-shinylive/, https://github.com/posit-dev/r-shinylive Imports: @@ -33,3 +33,5 @@ Suggests: Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Language: en-US +LinkingTo: + cpp11 diff --git a/NAMESPACE b/NAMESPACE index 6a56160..cd3285f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,3 +10,4 @@ export(assets_remove) export(assets_version) export(export) importFrom(rlang,is_interactive) +useDynLib(shinylive, .registration = TRUE) diff --git a/R/cpp11.R b/R/cpp11.R new file mode 100644 index 0000000..47a4f0c --- /dev/null +++ b/R/cpp11.R @@ -0,0 +1,9 @@ +# Generated by cpp11: do not edit by hand + +compressToEncodedURIComponent <- function(uncompressed8) { + .Call(`_shinylive_compressToEncodedURIComponent`, uncompressed8) +} + +decompressFromEncodedURIComponent <- function(compressed8) { + .Call(`_shinylive_decompressFromEncodedURIComponent`, compressed8) +} diff --git a/R/utils.R b/R/utils.R index 6eb6f79..f7d0e46 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,3 +1,5 @@ +#' @useDynLib shinylive, .registration = TRUE + assert_nzchar_string <- function(x) { stopifnot(is.character(x) && nchar(x) > 0) invisible(TRUE) diff --git a/shinylive.Rproj b/shinylive.Rproj index aaa62a5..69fafd4 100644 --- a/shinylive.Rproj +++ b/shinylive.Rproj @@ -5,8 +5,13 @@ SaveWorkspace: No AlwaysSaveHistory: Default EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 Encoding: UTF-8 +RnwWeave: Sweave +LaTeX: pdfLaTeX + AutoAppendNewline: Yes StripTrailingWhitespace: Yes LineEndingConversion: Posix diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..22034c4 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,3 @@ +*.o +*.so +*.dll diff --git a/src/code.cpp b/src/code.cpp new file mode 100644 index 0000000..0472a4b --- /dev/null +++ b/src/code.cpp @@ -0,0 +1,31 @@ +#include +#include +#include "lz-string.hpp" +using namespace cpp11; + +[[cpp11::register]] +std::string compressToEncodedURIComponent(std::string uncompressed8) { + std::wstring_convert, char16_t> converter_8_to_16; + std::u16string uncompressed16 = converter_8_to_16.from_bytes(uncompressed8); + + auto compressed16 = lzstring::compressToEncodedURIComponent(uncompressed16); + + std::wstring_convert, char16_t> converter_16_to_8; + std::string compressed8 = converter_16_to_8.to_bytes(compressed16); + + return compressed8; +} + + +[[cpp11::register]] +std::string decompressFromEncodedURIComponent(std::string compressed8) { + std::wstring_convert, char16_t> converter_8_to_16; + std::u16string compressed16 = converter_8_to_16.from_bytes(compressed8); + + auto uncompressed16 = lzstring::decompressFromEncodedURIComponent(compressed16); + + std::wstring_convert, char16_t> converter_16_to_8; + std::string uncompressed8 = converter_16_to_8.to_bytes(uncompressed16); + + return uncompressed8; +} diff --git a/src/cpp11.cpp b/src/cpp11.cpp new file mode 100644 index 0000000..0b1b695 --- /dev/null +++ b/src/cpp11.cpp @@ -0,0 +1,35 @@ +// Generated by cpp11: do not edit by hand +// clang-format off + + +#include "cpp11/declarations.hpp" +#include + +// code.cpp +std::string compressToEncodedURIComponent(std::string uncompressed8); +extern "C" SEXP _shinylive_compressToEncodedURIComponent(SEXP uncompressed8) { + BEGIN_CPP11 + return cpp11::as_sexp(compressToEncodedURIComponent(cpp11::as_cpp>(uncompressed8))); + END_CPP11 +} +// code.cpp +std::string decompressFromEncodedURIComponent(std::string compressed8); +extern "C" SEXP _shinylive_decompressFromEncodedURIComponent(SEXP compressed8) { + BEGIN_CPP11 + return cpp11::as_sexp(decompressFromEncodedURIComponent(cpp11::as_cpp>(compressed8))); + END_CPP11 +} + +extern "C" { +static const R_CallMethodDef CallEntries[] = { + {"_shinylive_compressToEncodedURIComponent", (DL_FUNC) &_shinylive_compressToEncodedURIComponent, 1}, + {"_shinylive_decompressFromEncodedURIComponent", (DL_FUNC) &_shinylive_decompressFromEncodedURIComponent, 1}, + {NULL, NULL, 0} +}; +} + +extern "C" attribute_visible void R_init_shinylive(DllInfo* dll){ + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); + R_forceSymbols(dll, TRUE); +} diff --git a/src/lz-string.hpp b/src/lz-string.hpp new file mode 100644 index 0000000..41f0b4b --- /dev/null +++ b/src/lz-string.hpp @@ -0,0 +1,583 @@ +#pragma once + +/* + * C++ implementation of LZ-String, version 1.4.4 + * https://github.com/pieroxy/lz-string + * https://github.com/andykras/lz-string-cpp + * + * MIT License + * + * Copyright (c) 2021 Andrey Krasnov + * + */ + +#include +#include + +// preserve all original comments and naming from +// https://github.com/pieroxy/lz-string/blob/master/libs/lz-string.js +namespace lzstring +{ +#ifdef _MSC_VER +using string = std::wstring; +# ifndef _U +# define _U(x) L##x +# endif +#else +using string = std::u16string; +#include +# ifndef _U +# define _U(x) u##x +# endif +#endif +namespace __inner +{ + const string keyStrBase64{_U("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")}; + const string keyStrUriSafe{_U("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$")}; + const string::value_type equal{_U('=')}; + + int charCodeAt(const string& str, int pos) + { + return static_cast(str.at(pos)); + } + + string f(int ascii) + { + return {static_cast(ascii)}; + } + + template + string _compress(const string& uncompressed, int bitsPerChar, Fn getCharFromInt) + { + int i = 0; + int value = 0; + + std::unordered_map context_dictionary; + std::unordered_map context_dictionaryToCreate; + + string context_c; + string context_wc; + string context_w; + + int context_enlargeIn = 2; // Compensate for the first entry which should not count + int context_dictSize = 3; + int context_numBits = 2; + + string context_data; + int context_data_val = 0; + int context_data_position = 0; + + for (size_t ii = 0; ii < uncompressed.length(); ++ii) + { + context_c = uncompressed.at(ii); + if (context_dictionary.count(context_c) == 0) + { + context_dictionary[context_c] = context_dictSize++; + context_dictionaryToCreate[context_c] = true; + } + + context_wc = context_w + context_c; + if (context_dictionary.count(context_wc) > 0) + { + context_w = context_wc; + } + else + { + auto context_w_it = context_dictionaryToCreate.find(context_w); + if (context_w_it != context_dictionaryToCreate.end()) + { + if (charCodeAt(context_w, 0) < 256) + { + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + } + value = charCodeAt(context_w, 0); + for (i = 0; i < 8; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + } + else + { + value = 1; + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1) | value; + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = 0; + } + value = charCodeAt(context_w, 0); + for (i = 0; i < 16; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + } + if (--context_enlargeIn == 0) + { + context_enlargeIn = 1 << context_numBits; // Math.pow(2, context_numBits); + ++context_numBits; + } + context_dictionaryToCreate.erase(context_w_it); // delete context_dictionaryToCreate[context_w]; + } + else + { + value = context_dictionary[context_w]; + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + } + if (--context_enlargeIn == 0) + { + context_enlargeIn = 1 << context_numBits; // Math.pow(2, context_numBits); + ++context_numBits; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = context_c; // context_w = String(context_c); + } + } + + // Output the code for w. + if (!context_w.empty()) + { + auto context_w_it = context_dictionaryToCreate.find(context_w); + if (context_w_it != context_dictionaryToCreate.end()) + { + if (charCodeAt(context_w, 0) < 256) + { + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + } + value = charCodeAt(context_w, 0); + for (i = 0; i < 8; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + } + else + { + value = 1; + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1) | value; + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = 0; + } + value = charCodeAt(context_w, 0); + for (i = 0; i < 16; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + } + if (--context_enlargeIn == 0) + { + context_enlargeIn = 1 << context_numBits; // Math.pow(2, context_numBits); + ++context_numBits; + } + context_dictionaryToCreate.erase(context_w_it); // delete context_dictionaryToCreate[context_w]; + } + else + { + value = context_dictionary[context_w]; + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + } + if (--context_enlargeIn == 0) + { + context_enlargeIn = 1 << context_numBits; // Math.pow(2, context_numBits); + ++context_numBits; + } + } + + // Mark the end of the stream + value = 2; + for (i = 0; i < context_numBits; ++i) + { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) + { + context_data_position = 0; + context_data.push_back(getCharFromInt(context_data_val)); + context_data_val = 0; + } + else + { + ++context_data_position; + } + value = value >> 1; + } + + // Flush the last char + while (true) + { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar - 1) + { + context_data.push_back(getCharFromInt(context_data_val)); + break; + } + else + { + ++context_data_position; + } + } + + return context_data; + } + + template + string _decompress(int length, int resetValue, Fn getNextValue) + { + std::unordered_map dictionary; + + int next = 0; + int enlargeIn = 4; + int dictSize = 4; + int numBits = 3; + string entry; + string result; + string w; + int bits, resb, maxpower, power; + string c; + + struct + { + int val, position, index; + } data{getNextValue(0), resetValue, 1}; + + bits = 0; + maxpower = 4; // Math.pow(2, 2); + power = 1; + + while (power != maxpower) + { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) + { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb > 0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) + { + case 0: + bits = 0; + maxpower = 256; // Math.pow(2, 8); + power = 1; + while (power != maxpower) + { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) + { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb > 0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + + case 1: + bits = 0; + maxpower = 65536; // Math.pow(2, 16); + power = 1; + while (power != maxpower) + { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) + { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb > 0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + + case 2: + return {}; + } + + dictionary[3] = c; + w = c; + result += c; + + while (true) + { + if (data.index > length) + { + return {}; + } + + bits = 0; + maxpower = 1 << numBits; // Math.pow(2, numBits); + power = 1; + while (power != maxpower) + { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) + { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb > 0 ? 1 : 0) * power; + power <<= 1; + } + + int c; + switch (c = bits) + { + case 0: + bits = 0; + maxpower = 256; // Math.pow(2, 8); + power = 1; + while (power != maxpower) + { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) + { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb > 0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize - 1; + enlargeIn--; + break; + + case 1: + bits = 0; + maxpower = 65536; // Math.pow(2, 16); + power = 1; + while (power != maxpower) + { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) + { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb > 0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize - 1; + enlargeIn--; + break; + + case 2: + return result; + } + + if (enlargeIn == 0) + { + enlargeIn = 1 << numBits; // Math.pow(2, numBits); + numBits++; + } + + if (!dictionary[c].empty()) + { + entry = dictionary[c]; + } + else + { + if (c == dictSize) + { + entry = w + w.at(0); + } + else + { + return {}; + } + } + result += entry; + + // Add w+entry[0] to the dictionary. + dictionary[dictSize++] = w + entry.at(0); + enlargeIn--; + + w = entry; + + if (enlargeIn == 0) + { + enlargeIn = 1 << numBits; // Math.pow(2, numBits); + numBits++; + } + } + + return {}; + } + +} // namespace __inner + +string compressToEncodedURIComponent(const string& input) +{ + if (input.empty()) return {}; + using namespace __inner; + auto res = _compress(input, 6, [](int a) { return keyStrBase64.at(a); }); + return res; +} + + +string decompressFromEncodedURIComponent(const string& input) +{ + if (input.empty()) return {}; + using namespace __inner; + string input1 = input; + // std::replace(input1.begin(), input1.end(), ' ', '+'); + std::unordered_map baseReverseDic; + for (int i = 0; i < keyStrUriSafe.length(); ++i) baseReverseDic[keyStrUriSafe.at(i)] = i; + return _decompress(input1.length(), 32, [&](int index) { return baseReverseDic[input.at(index)]; }); +} + + +// clang-format off +string compressToBase64(const string& input) +{ + if (input.empty()) return {}; + using namespace __inner; + auto res = _compress(input, 6, [](int a) { return keyStrBase64.at(a); }); + switch (res.length() % 4) { // To produce valid Base64 + default: // When could this happen ? + case 0 : return res; + case 1 : return res+string(3,equal); + case 2 : return res+string(2,equal); + case 3 : return res+string(1,equal); + } +} + +string decompressFromBase64(const string& input) +{ + if (input.empty()) return {}; + using namespace __inner; + std::unordered_map baseReverseDic; + for (int i = 0; i < keyStrBase64.length(); ++i) baseReverseDic[keyStrBase64.at(i)] = i; + return _decompress(input.length(), 32, [&](int index) { return baseReverseDic[input.at(index)]; }); +} +// clang-format on + +} // namespace lzstring