First Commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
## CHOC: licensing
|
||||
|
||||
I'd like anyone to feel able to use this library code without worrying about the legal implications, so it's released under the permissive ISC license:
|
||||
|
||||
----
|
||||
|
||||
**Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.**
|
||||
|
||||
**THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.**
|
||||
|
||||
https://en.wikipedia.org/wiki/ISC_license
|
||||
|
||||
----
|
||||
|
||||
Note that if you use the `choc::ui::WebView` class on Windows, it embeds some Microsoft redistributable code which has its own (permissive) license that you should be aware of. See inside the file `choc_WebView.h` for more details.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,364 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_JAVASCRIPT_HEADER_INCLUDED
|
||||
#define CHOC_JAVASCRIPT_HEADER_INCLUDED
|
||||
|
||||
#include <stdexcept>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include "../containers/choc_Value.h"
|
||||
#include "../text/choc_JSON.h"
|
||||
|
||||
/**
|
||||
Wrapper classes for encapsulating different javascript engines such as
|
||||
Duktape and QuickJS.
|
||||
|
||||
Just use one of the context-creation functions such as
|
||||
choc::javascript::createQuickJSContext() to create a context for running
|
||||
javascript code.
|
||||
*/
|
||||
namespace choc::javascript
|
||||
{
|
||||
/// This is thrown by any javascript functions that need to report an error
|
||||
struct Error : public std::runtime_error
|
||||
{
|
||||
Error (const std::string& error) : std::runtime_error (error) {}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/// Helper class to hold and provide access to the arguments in a javascript
|
||||
/// function callback.
|
||||
struct ArgumentList
|
||||
{
|
||||
const choc::value::Value* args = nullptr;
|
||||
size_t numArgs = 0;
|
||||
|
||||
/// Returns the number of arguments
|
||||
size_t size() const noexcept { return numArgs; }
|
||||
/// Returns true if there are no arguments
|
||||
bool empty() const noexcept { return numArgs == 0; }
|
||||
|
||||
/// Returns an argument, or a nullptr if the index is out of range.
|
||||
const choc::value::Value* operator[] (size_t index) const { return index < numArgs ? args + index : nullptr; }
|
||||
|
||||
/// Gets an argument as a primitive type (or a string).
|
||||
/// If the index is out of range or the object isn't a suitable type,
|
||||
/// then the default value provided will be returned instead.
|
||||
template <typename PrimitiveType>
|
||||
PrimitiveType get (size_t argIndex, PrimitiveType defaultValue = {}) const;
|
||||
|
||||
/// Standard iterator
|
||||
const choc::value::Value* begin() const noexcept { return args; }
|
||||
const choc::value::Value* end() const noexcept { return args + numArgs; }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
An execution context which you use for running javascript code.
|
||||
|
||||
These are really simple to use: call one of the creation functions such
|
||||
as choc::javascript::createQuickJSContext() which will give you a shared_ptr to
|
||||
a context. Then you can add any native bindings that you need with
|
||||
registerFunction(), and call evaluate() or invoke() to execute code or call
|
||||
functions directly.
|
||||
|
||||
These contexts are not thread-safe, so it's up to the caller to handle thread
|
||||
synchronisation if using a single context from multiple threads.
|
||||
|
||||
They're also definitely not realtime-safe: any of the methods may allocate,
|
||||
block, or make system calls.
|
||||
*/
|
||||
class Context
|
||||
{
|
||||
public:
|
||||
/// To create a Context, use a function such as choc::javascript::createQuickJSContext();
|
||||
Context() = default;
|
||||
Context (Context&&);
|
||||
Context& operator= (Context&&);
|
||||
~Context();
|
||||
|
||||
/// Returns true if the context is valid.
|
||||
operator bool() const { return pimpl != nullptr; }
|
||||
|
||||
//==============================================================================
|
||||
/// This callback is used by the run() method.
|
||||
using CompletionHandler = std::function<void(const std::string& error,
|
||||
const choc::value::ValueView& result)>;
|
||||
|
||||
/// Executes some javascript asynchronously.
|
||||
/// If a CompletionHandler callback is provided, it will be called asynchronously
|
||||
/// with the return value and any errors that occurred. Note that if you want to execute
|
||||
/// the script as a module, use runModule() instead.
|
||||
void run (const std::string& javascriptCode,
|
||||
CompletionHandler handleResult = {});
|
||||
|
||||
/// When parsing modules, this function is expected to take a path to a module, and
|
||||
/// to return the content of that module, or an empty optional if not found.
|
||||
using ReadModuleContentFn = std::function<std::optional<std::string>(std::string_view)>;
|
||||
|
||||
/// This callback will asynchronously parse the script as a module, and will use the
|
||||
/// ReadModuleContentFn functor to resolve any imported modules that it needs. If a
|
||||
/// CompletionHandler callback is provided, it will be called asynchronously with the
|
||||
/// return value and any errors that occurred.
|
||||
/// NB: Not all engines support modules.
|
||||
void runModule (const std::string& moduleCode,
|
||||
ReadModuleContentFn,
|
||||
CompletionHandler handleResult = {});
|
||||
|
||||
/// Evaluates a javascript expression synchronously, and returns the result.
|
||||
/// If there are any parse errors, this will throw a choc::javascript::Error exception.
|
||||
/// Note that if you want to execute the script as a module, use runModule() instead.
|
||||
choc::value::Value evaluateExpression (const std::string& javascriptCode);
|
||||
|
||||
/// Attempts to synchronously invoke a global function with no arguments.
|
||||
/// Any errors will throw a choc::javascript::Error exception.
|
||||
/// None of the methods in this class are either thread-safe or realtime-safe, so you'll
|
||||
/// need to organise your own locking if you're calling into a single Context from
|
||||
/// multiple threads.
|
||||
choc::value::Value invoke (std::string_view functionName);
|
||||
|
||||
/// Attempts to invoke a global function with the arguments provided.
|
||||
/// The arguments can be primitives, strings, choc::value::ValueView or
|
||||
/// choc::value::Value types.
|
||||
/// Any errors will throw a choc::javascript::Error exception.
|
||||
/// None of the methods in this class are either thread-safe or realtime-safe, so you'll
|
||||
/// need to organise your own locking if you're calling into a single Context from
|
||||
/// multiple threads.
|
||||
template <typename... Args>
|
||||
choc::value::Value invoke (std::string_view functionName, Args&&... args);
|
||||
|
||||
/// Attempts to invoke a global function with an array of arguments.
|
||||
/// The objects in the argument list can be primitives, strings, choc::value::ValueView
|
||||
/// or choc::value::Value types.
|
||||
/// Any errors will throw a choc::javascript::Error exception.
|
||||
/// None of the methods in this class are either thread-safe or realtime-safe, so you'll
|
||||
/// need to organise your own locking if you're calling into a single Context from
|
||||
/// multiple threads.
|
||||
template <typename ArgList>
|
||||
choc::value::Value invokeWithArgList (std::string_view functionName, const ArgList& args);
|
||||
|
||||
/// This is the prototype for a lambda which can be bound as a javascript function.
|
||||
using NativeFunction = std::function<choc::value::Value(ArgumentList)>;
|
||||
|
||||
/// Binds a lambda function to a global name so that javascript code can invoke it.
|
||||
void registerFunction (const std::string& name, NativeFunction fn);
|
||||
|
||||
/// Pumps the message loop in an engine-specific way - may have no effect on some platforms.
|
||||
void pumpMessageLoop();
|
||||
|
||||
//==============================================================================
|
||||
/// @internal
|
||||
struct Pimpl;
|
||||
/// @internal
|
||||
Context (std::unique_ptr<Pimpl>);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Pimpl> pimpl;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/// Creates a QuickJS-based context. If you call this, then you'll need to
|
||||
/// include choc_javascript_QuickJS.h in one (and only one!) of your source files.
|
||||
Context createQuickJSContext();
|
||||
|
||||
/// Creates a Duktape-based context. If you call this, then you'll need to
|
||||
/// include choc_javascript_Duktape.h in one (and only one!) of your source files.
|
||||
Context createDuktapeContext();
|
||||
|
||||
/// Creates a V8-based context. If you call this, then you'll need to
|
||||
/// make sure that your project also has the V8 header folder in its
|
||||
/// search path, and that you statically link the appropriate V8 libs.
|
||||
Context createV8Context();
|
||||
|
||||
//==============================================================================
|
||||
/// Sanitises a string to provide a version of it that is safe for use as a
|
||||
/// javascript identifier. This involves removing/replacing any illegal
|
||||
/// characters and modifying the string to avoid clashes with reserved words.
|
||||
std::string makeSafeIdentifier (std::string name);
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
// _ _ _ _
|
||||
// __| | ___ | |_ __ _ (_)| | ___
|
||||
// / _` | / _ \| __| / _` || || |/ __|
|
||||
// | (_| || __/| |_ | (_| || || |\__ \ _ _ _
|
||||
// \__,_| \___| \__| \__,_||_||_||___/(_)(_)(_)
|
||||
//
|
||||
// Code beyond this point is implementation detail...
|
||||
//
|
||||
//==============================================================================
|
||||
|
||||
namespace choc::javascript
|
||||
{
|
||||
|
||||
template <typename PrimitiveType>
|
||||
PrimitiveType ArgumentList::get (size_t index, PrimitiveType defaultValue) const
|
||||
{
|
||||
if (auto a = (*this)[index])
|
||||
return a->getWithDefault<PrimitiveType> (defaultValue);
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
struct Context::Pimpl
|
||||
{
|
||||
Pimpl() = default;
|
||||
virtual ~Pimpl() = default;
|
||||
|
||||
virtual void registerFunction (const std::string&, NativeFunction) = 0;
|
||||
virtual choc::value::Value evaluateExpression (const std::string&) = 0;
|
||||
virtual void run (const std::string&, ReadModuleContentFn*, CompletionHandler) = 0;
|
||||
virtual void prepareForCall (std::string_view, uint32_t numArgs) = 0;
|
||||
virtual choc::value::Value performCall() = 0;
|
||||
virtual void pushObjectOrArray (const choc::value::ValueView&) = 0;
|
||||
virtual void pushArg (std::string_view) = 0;
|
||||
virtual void pushArg (int32_t) = 0;
|
||||
virtual void pushArg (int64_t) = 0;
|
||||
virtual void pushArg (uint32_t) = 0;
|
||||
virtual void pushArg (double) = 0;
|
||||
virtual void pushArg (bool) = 0;
|
||||
virtual void pumpMessageLoop() = 0;
|
||||
|
||||
void pushArg (const std::string& v) { pushArg (std::string_view (v)); }
|
||||
void pushArg (const char* v) { pushArg (std::string_view (v)); }
|
||||
void pushArg (uint64_t v) { pushArg (static_cast<int64_t> (v)); }
|
||||
void pushArg (float v) { pushArg (static_cast<double> (v)); }
|
||||
|
||||
void pushArg (const choc::value::ValueView& v)
|
||||
{
|
||||
if (v.isInt32()) return pushArg (v.getInt32());
|
||||
if (v.isInt64()) return pushArg (v.getInt64());
|
||||
if (v.isFloat32()) return pushArg (v.getFloat32());
|
||||
if (v.isFloat64()) return pushArg (v.getFloat64());
|
||||
if (v.isString()) return pushArg (v.getString());
|
||||
if (v.isBool()) return pushArg (v.getBool());
|
||||
if (v.isVoid()) throw Error ("Function arguments cannot be void!");
|
||||
|
||||
pushObjectOrArray (v);
|
||||
}
|
||||
};
|
||||
|
||||
inline Context::Context (std::unique_ptr<Pimpl> p) : pimpl (std::move (p)) {}
|
||||
inline Context::~Context() = default;
|
||||
inline Context::Context (Context&&) = default;
|
||||
inline Context& Context::operator= (Context&&) = default;
|
||||
|
||||
inline choc::value::Value Context::invoke (std::string_view functionName)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->prepareForCall (functionName, 0);
|
||||
return pimpl->performCall();
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
choc::value::Value Context::invoke (std::string_view functionName, Args&&... args)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->prepareForCall (functionName, sizeof...(args));
|
||||
(pimpl->pushArg (std::forward<Args> (args)), ...);
|
||||
return pimpl->performCall();
|
||||
}
|
||||
|
||||
template <typename ArgList>
|
||||
choc::value::Value Context::invokeWithArgList (std::string_view functionName, const ArgList& args)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->prepareForCall (functionName, static_cast<uint32_t> (args.size()));
|
||||
|
||||
for (auto& arg : args)
|
||||
pimpl->pushArg (arg);
|
||||
|
||||
return pimpl->performCall();
|
||||
}
|
||||
|
||||
inline void Context::registerFunction (const std::string& name, NativeFunction fn)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->registerFunction (name, std::move (fn));
|
||||
}
|
||||
|
||||
inline choc::value::Value Context::evaluateExpression (const std::string& javascriptCode)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
return pimpl->evaluateExpression (javascriptCode);
|
||||
}
|
||||
|
||||
inline void Context::run (const std::string& javascriptCode, CompletionHandler handleResult)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->run (javascriptCode, nullptr, std::move (handleResult));
|
||||
}
|
||||
|
||||
inline void Context::runModule (const std::string& moduleCode, ReadModuleContentFn readModule, CompletionHandler handleResult)
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->run (moduleCode, std::addressof (readModule), std::move (handleResult));
|
||||
}
|
||||
|
||||
inline void Context::pumpMessageLoop()
|
||||
{
|
||||
CHOC_ASSERT (pimpl != nullptr); // cannot call this on a moved-from context!
|
||||
pimpl->pumpMessageLoop();
|
||||
}
|
||||
|
||||
inline std::string makeSafeIdentifier (std::string s)
|
||||
{
|
||||
constexpr static std::string_view reservedWords[] =
|
||||
{
|
||||
"abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch",
|
||||
"char", "class", "const", "continue", "debugger", "default", "delete", "do",
|
||||
"double", "else", "enum", "eval", "export", "extends", "false", "final",
|
||||
"finally", "float", "for", "function", "goto", "if", "implements", "import",
|
||||
"in", "instanceof", "int", "interface", "let", "long", "native", "new",
|
||||
"null", "package", "private", "protected", "public", "return", "short", "static",
|
||||
"super", "switch", "synchronized", "this", "throw", "throws", "transient", "true",
|
||||
"try", "typeof", "var", "void", "volatile", "while", "with", "yield"
|
||||
};
|
||||
|
||||
for (auto& c : s)
|
||||
if (std::string_view (" ,./;:").find (c) != std::string_view::npos)
|
||||
c = '_';
|
||||
|
||||
s.erase (std::remove_if (s.begin(), s.end(), [&] (char c)
|
||||
{
|
||||
return ! ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9'));
|
||||
}), s.end());
|
||||
|
||||
if (s[0] >= '0' && s[0] <= '9') // Identifiers can't start with a digit
|
||||
s = "_" + s;
|
||||
|
||||
for (auto keyword : reservedWords)
|
||||
if (s == keyword)
|
||||
return s + "_";
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace choc::javascript
|
||||
|
||||
#endif // CHOC_JAVASCRIPT_HEADER_INCLUDED
|
||||
|
||||
#ifdef CHOC_JAVASCRIPT_IMPLEMENTATION
|
||||
// The way the javascript classes work has changed: instead of
|
||||
// setting CHOC_JAVASCRIPT_IMPLEMENTATION in one of your source files, just
|
||||
// include the actual engine that you want to use, e.g. choc_javascript_QuickJS.h
|
||||
// in (only!) one of your source files, and use that to create instances of that engine.
|
||||
#error "CHOC_JAVASCRIPT_IMPLEMENTATION is deprecated"
|
||||
#endif
|
||||
+64367
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_MATH_HELPERS_HEADER_INCLUDED
|
||||
#define CHOC_MATH_HELPERS_HEADER_INCLUDED
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <intrin.h>
|
||||
#pragma intrinsic (_BitScanReverse)
|
||||
|
||||
#ifdef _WIN64
|
||||
#pragma intrinsic (_BitScanReverse64)
|
||||
#endif
|
||||
|
||||
#if defined (_M_X64) && ! defined (_M_ARM64EC)
|
||||
#pragma intrinsic (_umul128)
|
||||
#define CHOC_HAS_UMUL128 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace choc::math
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/// Returns true if the given value is 2^something
|
||||
template <typename Integer>
|
||||
constexpr bool isPowerOf2 (Integer n) { return n > 0 && (n & (n - 1)) == 0; }
|
||||
|
||||
// Returns the number of contiguously-clear upper bits in a 32-bit value
|
||||
/// Note this operation is undefined for value == 0!
|
||||
inline uint32_t countUpperClearBits (uint32_t value)
|
||||
{
|
||||
#ifdef _MSC_VER
|
||||
unsigned long result = 0;
|
||||
_BitScanReverse (&result, static_cast<unsigned long> (value));
|
||||
return static_cast<uint32_t> (31u - result);
|
||||
#else
|
||||
return static_cast<uint32_t> (__builtin_clz (value));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Returns the number of contiguously-clear upper bits in a 64-bit value.
|
||||
/// Note this operation is undefined for value == 0!
|
||||
inline uint32_t countUpperClearBits (uint64_t value)
|
||||
{
|
||||
#ifdef _MSC_VER
|
||||
unsigned long result = 0;
|
||||
#ifdef _WIN64
|
||||
_BitScanReverse64 (&result, value);
|
||||
#else
|
||||
if (_BitScanReverse (&result, static_cast<unsigned long> (value >> 32))) return static_cast<uint32_t> (31u - result);
|
||||
_BitScanReverse (&result, static_cast<unsigned long> (value));
|
||||
#endif
|
||||
return static_cast<uint32_t> (63u - result);
|
||||
#else
|
||||
return static_cast<uint32_t> (__builtin_clzll (value));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Returns the number of decimal digits required to print a given unsigned number
|
||||
inline int getNumDecimalDigits (uint32_t n)
|
||||
{
|
||||
return n < 1000 ? (n < 10 ? 1 : (n < 100 ? 2 : 3))
|
||||
: n < 1000000 ? (n < 10000 ? 4 : (n < 100000 ? 5 : 6))
|
||||
: n < 100000000 ? (n < 10000000 ? 7 : 8)
|
||||
: n < 1000000000 ? 9 : 10;
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/// Used as a return type for multiply128()
|
||||
struct Int128
|
||||
{
|
||||
uint64_t high, low;
|
||||
};
|
||||
|
||||
/// A cross-platform function to multiply two 64-bit numbers and return a 128-bit result
|
||||
inline Int128 multiply128 (uint64_t a, uint64_t b)
|
||||
{
|
||||
#if CHOC_HAS_UMUL128
|
||||
Int128 result;
|
||||
result.low = _umul128 (a, b, &result.high);
|
||||
return result;
|
||||
#elif __LP64__
|
||||
#if __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#endif
|
||||
auto total = static_cast<unsigned __int128> (a) * static_cast<unsigned __int128> (b);
|
||||
#if __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
return { static_cast<uint64_t> (total >> 64), static_cast<uint64_t> (total) };
|
||||
#else
|
||||
uint64_t a0 = static_cast<uint32_t> (a), a1 = a >> 32,
|
||||
b0 = static_cast<uint32_t> (b), b1 = b >> 32;
|
||||
auto p10 = a1 * b0, p00 = a0 * b0,
|
||||
p11 = a1 * b1, p01 = a0 * b1;
|
||||
auto middleBits = p10 + static_cast<uint32_t> (p01) + (p00 >> 32);
|
||||
return { p11 + (middleBits >> 32) + (p01 >> 32), (middleBits << 32) | static_cast<uint32_t> (p00) };
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
} // namespace choc::math
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_ASSERT_HEADER_INCLUDED
|
||||
#define CHOC_ASSERT_HEADER_INCLUDED
|
||||
|
||||
// If the project doesn't define a custom implementation for CHOC_ASSERT, the default
|
||||
// behaviour is just to call the normal system assert()
|
||||
#ifndef CHOC_ASSERT
|
||||
#include <cassert>
|
||||
#define CHOC_ASSERT(x) assert(x);
|
||||
#endif
|
||||
|
||||
// It's never a smart idea to include any C headers before your C++ ones, as they
|
||||
// often pollute your namespace with all kinds of dangerous macros like these ones.
|
||||
// This file is included by many of the choc headers, so is a convenient place to
|
||||
// undef these.
|
||||
#undef max
|
||||
#undef min
|
||||
|
||||
#endif // CHOC_ASSERT_HEADER_INCLUDED
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
/*
|
||||
Sometimes you inevitably have to include some 3rd-party headers in your code, and it's
|
||||
depressing how often even the most widely-used libraries are full of poorly-written
|
||||
code that triggers all kinds of compiler warnings.
|
||||
|
||||
Obviously you're a proper programmer who always has warnings turned up to full, and "-Werror"
|
||||
enabled, so rather than having to compromise your own standards to work around other people's
|
||||
laziness, this header provides an easy way to locally disable warnings by sandwiching your
|
||||
3rd-party headers between includes of these two files, e.g:
|
||||
|
||||
#include "choc_DisableAllWarnings.h"
|
||||
#include "SloppilyWrittenButEssentialLibraryCode.h"
|
||||
#include "choc_ReenableAllWarnings.h"
|
||||
*/
|
||||
|
||||
#if __clang__
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Weverything"
|
||||
#elif __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wall"
|
||||
#pragma GCC diagnostic ignored "-Wpragmas"
|
||||
#pragma GCC diagnostic ignored "-Wextra"
|
||||
#pragma GCC diagnostic ignored "-Wshadow"
|
||||
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
||||
#pragma GCC diagnostic ignored "-Wconversion"
|
||||
#pragma GCC diagnostic ignored "-Wsign-conversion"
|
||||
#pragma GCC diagnostic ignored "-Wsign-compare"
|
||||
#pragma GCC diagnostic ignored "-Wfloat-conversion"
|
||||
#pragma GCC diagnostic ignored "-Wswitch-enum"
|
||||
#pragma GCC diagnostic ignored "-Wswitch"
|
||||
#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
|
||||
#pragma GCC diagnostic ignored "-Wunused-variable"
|
||||
#pragma GCC diagnostic ignored "-Wredundant-decls"
|
||||
#pragma GCC diagnostic ignored "-Wsubobject-linkage"
|
||||
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
|
||||
#pragma GCC diagnostic ignored "-Wredundant-move"
|
||||
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
|
||||
#pragma GCC diagnostic ignored "-Woverloaded-virtual"
|
||||
#pragma GCC diagnostic ignored "-Wc99-extensions"
|
||||
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
|
||||
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
|
||||
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
|
||||
#pragma GCC diagnostic ignored "-Wcast-function-type"
|
||||
#pragma GCC diagnostic ignored "-Wunused-label"
|
||||
#pragma GCC diagnostic ignored "-Wnarrowing"
|
||||
#pragma GCC diagnostic ignored "-Wparentheses"
|
||||
#pragma GCC diagnostic ignored "-Wwrite-strings"
|
||||
#pragma GCC diagnostic ignored "-Wformat-overflow"
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-copy-with-dtor"
|
||||
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated"
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#pragma GCC diagnostic ignored "-Wuse-after-free"
|
||||
#pragma GCC diagnostic ignored "-Warray-bounds"
|
||||
#pragma GCC diagnostic ignored "-Wvolatile"
|
||||
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
|
||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#ifndef __MINGW32__
|
||||
#pragma GCC diagnostic ignored "-Wredundant-move"
|
||||
#endif
|
||||
#else
|
||||
#pragma warning (push, 0)
|
||||
#pragma warning (disable: 2440)
|
||||
#pragma warning (disable: 2664)
|
||||
#pragma warning (disable: 4244)
|
||||
#pragma warning (disable: 4701)
|
||||
#pragma warning (disable: 4702)
|
||||
#pragma warning (disable: 4706)
|
||||
#pragma warning (disable: 4722)
|
||||
#pragma warning (disable: 6011)
|
||||
#pragma warning (disable: 6246)
|
||||
#pragma warning (disable: 6255)
|
||||
#pragma warning (disable: 6262)
|
||||
#pragma warning (disable: 6297)
|
||||
#pragma warning (disable: 6308)
|
||||
#pragma warning (disable: 6323)
|
||||
#pragma warning (disable: 6340)
|
||||
#pragma warning (disable: 6385)
|
||||
#pragma warning (disable: 6386)
|
||||
#pragma warning (disable: 28182)
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
// This file re-enables the warnings that were turned off by choc_DisableAllWarnings.h
|
||||
// So when you have to include some crappy 3rd-party code which is full of warnings, you
|
||||
// can sandwich it between includes of choc_DisableAllWarnings.h and choc_ReenableAllWarnings.h
|
||||
|
||||
#if __clang__
|
||||
#pragma clang diagnostic pop
|
||||
#elif __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#else
|
||||
#pragma warning (pop)
|
||||
#endif
|
||||
@@ -0,0 +1,397 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_FLOAT_TO_STRING_HEADER_INCLUDED
|
||||
#define CHOC_FLOAT_TO_STRING_HEADER_INCLUDED
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "../math/choc_MathHelpers.h"
|
||||
|
||||
namespace choc::text
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/** Converts a 32-bit float to an accurate, round-trip-safe string.
|
||||
|
||||
The algorithm used is "Grisu3" from the paper "Printing Floating-Point Numbers
|
||||
Quickly and Accurately with Integers" by Florian Loitsch.
|
||||
*/
|
||||
std::string floatToString (float value);
|
||||
|
||||
/** Converts a 64-bit double to an accurate, round-trip-safe string.
|
||||
|
||||
The algorithm used is "Grisu3" from the paper "Printing Floating-Point Numbers
|
||||
Quickly and Accurately with Integers" by Florian Loitsch.
|
||||
*/
|
||||
std::string floatToString (double value);
|
||||
|
||||
//==============================================================================
|
||||
/** Converts a 32-bit float to an accurate, round-trip-safe string.
|
||||
If maxDecimalPlaces is -1, a default is used.
|
||||
If omitDecimalPointForRoundNumbers is true, then values such as "2.0" are returned
|
||||
without the decimal point, e.g. simply "2".
|
||||
|
||||
The algorithm used is "Grisu3" from the paper "Printing Floating-Point Numbers
|
||||
Quickly and Accurately with Integers" by Florian Loitsch.
|
||||
*/
|
||||
std::string floatToString (float value, int maxDecimalPlaces, bool omitDecimalPointForRoundNumbers = false);
|
||||
|
||||
/** Converts a 64-bit double to an accurate, round-trip-safe string.
|
||||
If maxDecimalPlaces is -1, a default is used.
|
||||
If omitDecimalPointForRoundNumbers is true, then values such as "2.0" are returned
|
||||
without the decimal point, e.g. simply "2".
|
||||
|
||||
The algorithm used is "Grisu3" from the paper "Printing Floating-Point Numbers
|
||||
Quickly and Accurately with Integers" by Florian Loitsch.
|
||||
*/
|
||||
std::string floatToString (double value, int maxDecimalPlaces, bool omitDecimalPointForRoundNumbers = false);
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** Helper class containing its own buffer for converting a float or double to a string.
|
||||
|
||||
The algorithm is "Grisu3" from the paper "Printing Floating-Point Numbers
|
||||
Quickly and Accurately with Integers" by Florian Loitsch.
|
||||
|
||||
To use, just construct a FloatToStringBuffer with the value, and use its begin()/end()
|
||||
methods to iterate the result. Or use the floatToString() functions to just convert a
|
||||
value directly to a std::string.
|
||||
*/
|
||||
template <typename FloatOrDouble>
|
||||
struct FloatToStringBuffer
|
||||
{
|
||||
FloatToStringBuffer (FloatOrDouble value, int maxDecimalPlaces, bool omitPointIfPossible)
|
||||
: stringEnd (writeAndGetEnd (storage, value, maxDecimalPlaces, omitPointIfPossible)) {}
|
||||
|
||||
const char* begin() const { return storage; }
|
||||
const char* end() const { return stringEnd; }
|
||||
|
||||
std::string toString() const { return std::string (begin(), end()); }
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
static_assert (std::is_same<const float, const FloatOrDouble>::value || std::is_same<const double, const FloatOrDouble>::value,
|
||||
"This class can only handle float or double template types");
|
||||
|
||||
char storage[32];
|
||||
const char* stringEnd;
|
||||
|
||||
struct MantissaAndExponent
|
||||
{
|
||||
uint64_t mantissa;
|
||||
int32_t exponent;
|
||||
|
||||
static constexpr MantissaAndExponent create (uint64_t floatBits, uint64_t significand)
|
||||
{
|
||||
constexpr int exponentBias = (sizeof (FloatOrDouble) == 8 ? 0x3ff : 0x7f) + numSignificandBits;
|
||||
auto explonentPlusBias = static_cast<int> ((floatBits & exponentMask) >> numSignificandBits);
|
||||
|
||||
return explonentPlusBias == 0 ? MantissaAndExponent { significand, 1 - exponentBias }
|
||||
: MantissaAndExponent { significand + hiddenBit, explonentPlusBias - exponentBias };
|
||||
}
|
||||
|
||||
constexpr MantissaAndExponent operator* (MantissaAndExponent rhs) const
|
||||
{
|
||||
auto mantissaProduct = math::multiply128 (mantissa, rhs.mantissa);
|
||||
return { mantissaProduct.high + (mantissaProduct.low >> 63), exponent + rhs.exponent + 64 };
|
||||
}
|
||||
|
||||
constexpr MantissaAndExponent shiftedUp (int numBits) const { return { mantissa << numBits, exponent - numBits }; }
|
||||
constexpr MantissaAndExponent normalized() const { return shiftedUp (static_cast<int> (math::countUpperClearBits (mantissa))); }
|
||||
};
|
||||
|
||||
static uint32_t generateDigits (char* buffer, MantissaAndExponent upperBound, uint64_t mantissaDiff, uint64_t delta, int& K)
|
||||
{
|
||||
uint32_t length = 0;
|
||||
const auto one = MantissaAndExponent { 1ull << -upperBound.exponent, upperBound.exponent };
|
||||
auto p1 = static_cast<uint32_t> (upperBound.mantissa >> -one.exponent);
|
||||
auto p2 = upperBound.mantissa & (one.mantissa - 1);
|
||||
auto numDigits = math::getNumDecimalDigits (p1);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto digit = p1;
|
||||
|
||||
switch (--numDigits)
|
||||
{
|
||||
case 0: p1 = 0; break;
|
||||
case 1: digit /= powersOf10[1]; p1 %= powersOf10[1]; break;
|
||||
case 2: digit /= powersOf10[2]; p1 %= powersOf10[2]; break;
|
||||
case 3: digit /= powersOf10[3]; p1 %= powersOf10[3]; break;
|
||||
case 4: digit /= powersOf10[4]; p1 %= powersOf10[4]; break;
|
||||
case 5: digit /= powersOf10[5]; p1 %= powersOf10[5]; break;
|
||||
case 6: digit /= powersOf10[6]; p1 %= powersOf10[6]; break;
|
||||
case 7: digit /= powersOf10[7]; p1 %= powersOf10[7]; break;
|
||||
case 8: digit /= powersOf10[8]; p1 %= powersOf10[8]; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
writeDigitIfNotLeadingZero (buffer, length, digit);
|
||||
auto rest = p2 + (static_cast<uint64_t> (p1) << -one.exponent);
|
||||
|
||||
if (rest <= delta)
|
||||
{
|
||||
K += numDigits;
|
||||
roundFinalDigit (buffer, length, delta, rest, static_cast<uint64_t> (powersOf10[numDigits]) << -one.exponent, mantissaDiff);
|
||||
return length;
|
||||
}
|
||||
|
||||
if (numDigits == 0)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
delta *= 10;
|
||||
p2 *= 10;
|
||||
--numDigits;
|
||||
writeDigitIfNotLeadingZero (buffer, length, static_cast<uint32_t> (p2 >> -one.exponent));
|
||||
p2 &= one.mantissa - 1;
|
||||
|
||||
if (p2 < delta)
|
||||
{
|
||||
K += numDigits;
|
||||
roundFinalDigit (buffer, length, delta, p2, one.mantissa, numDigits > -9 ? mantissaDiff * powersOf10[-numDigits] : 0);
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void roundFinalDigit (char* buffer, uint32_t length, uint64_t delta, uint64_t rest, uint64_t tenToPowerNumDigits, uint64_t diff)
|
||||
{
|
||||
while (rest < diff && delta - rest >= tenToPowerNumDigits
|
||||
&& (rest + tenToPowerNumDigits < diff || diff - rest > rest + tenToPowerNumDigits - diff))
|
||||
{
|
||||
--(buffer[length - 1]);
|
||||
rest += tenToPowerNumDigits;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] static char* write (char* dest, char c) { *dest = c; return dest + 1; }
|
||||
template <typename... Chars> static char* write (char* dest, char first, Chars... others) { return write (write (dest, first), others...); }
|
||||
[[nodiscard]] static char* writeDigit (char* dest, int digit) { return write (dest, static_cast<char> (digit + '0')); }
|
||||
template <typename... Chars> static char* writeDigit (char* dest, int d, Chars... others) { return writeDigit (writeDigit (dest, d), others...); }
|
||||
[[nodiscard]] static char* writeZero (char* dest) { return write (dest, '0', '.', '0'); }
|
||||
[[nodiscard]] static char* writeExponent (char* dest, int e) { return writeShortInteger (write (dest, 'e'), e); }
|
||||
static void writeDigitIfNotLeadingZero (char* dest, uint32_t& length, uint32_t digit) { if (digit != 0 || length != 0) dest[length++] = static_cast<char> (digit + '0'); }
|
||||
|
||||
[[nodiscard]] static char* writeShortInteger (char* dest, int n)
|
||||
{
|
||||
if (n < 0) return writeShortInteger (write (dest, '-'), -n);
|
||||
if (n >= 100) return writeDigit (dest, n / 100, (n / 10) % 10, n % 10);
|
||||
if (n >= 10) return writeDigit (dest, n / 10, n % 10);
|
||||
|
||||
return writeDigit (dest, n);
|
||||
}
|
||||
|
||||
static void insertChar (char* dest, uint32_t length, char charToInsert, uint32_t numRepetitions)
|
||||
{
|
||||
std::memmove (dest + numRepetitions, dest, (size_t) length);
|
||||
|
||||
for (uint32_t i = 0; i < numRepetitions; ++i)
|
||||
dest[i] = charToInsert;
|
||||
}
|
||||
|
||||
static char* writeAsExponentNotation (char* dest, uint32_t totalLength, int exponent)
|
||||
{
|
||||
if (totalLength == 1)
|
||||
return writeExponent (dest + 1, exponent);
|
||||
|
||||
insertChar (dest + 1, totalLength - 1, '.', 1);
|
||||
|
||||
while (dest[totalLength] == '0' && totalLength > 2)
|
||||
--totalLength;
|
||||
|
||||
return writeExponent (dest + (totalLength + 1), exponent);
|
||||
}
|
||||
|
||||
static char* writeWithoutExponentLessThan1 (char* dest, uint32_t length, int mantissaDigits, int maxDecimalPlaces)
|
||||
{
|
||||
auto numPaddingZeros = static_cast<uint32_t> (2 - mantissaDigits);
|
||||
insertChar (dest, length, '0', numPaddingZeros);
|
||||
dest[1] = '.';
|
||||
|
||||
if (static_cast<int> (length) > maxDecimalPlaces + mantissaDigits)
|
||||
{
|
||||
for (int i = maxDecimalPlaces + 1; i > 2; --i)
|
||||
if (dest[i] != '0')
|
||||
return dest + (i + 1);
|
||||
|
||||
return dest + 3;
|
||||
}
|
||||
|
||||
length += numPaddingZeros;
|
||||
|
||||
while (dest[length - 1] == '0' && length > 3)
|
||||
--length;
|
||||
|
||||
return dest + length;
|
||||
}
|
||||
|
||||
static char* writeWithoutExponentGreaterThan1 (char* dest, uint32_t totalLength, uint32_t mantissaLength, int maxDecimalPlaces, int K)
|
||||
{
|
||||
if (K >= 0)
|
||||
{
|
||||
dest += totalLength;
|
||||
|
||||
for (auto i = totalLength; i < mantissaLength; ++i)
|
||||
dest = write (dest, '0');
|
||||
|
||||
return write (dest, '.', '0');
|
||||
}
|
||||
|
||||
insertChar (dest + mantissaLength, totalLength - mantissaLength, '.', 1);
|
||||
|
||||
if (K + maxDecimalPlaces >= 0)
|
||||
return dest + (totalLength + 1);
|
||||
|
||||
for (auto i = static_cast<int> (mantissaLength) + maxDecimalPlaces; i > static_cast<int> (mantissaLength + 1); --i)
|
||||
if (dest[i] != '0')
|
||||
return dest + (i + 1);
|
||||
|
||||
return dest + (mantissaLength + 2);
|
||||
}
|
||||
|
||||
struct Limits
|
||||
{
|
||||
constexpr Limits (MantissaAndExponent value)
|
||||
{
|
||||
upper = { (value.mantissa << 1) + 1, value.exponent - 1 };
|
||||
|
||||
while ((upper.mantissa & (hiddenBit << 1)) == 0)
|
||||
upper = upper.shiftedUp (1);
|
||||
|
||||
upper = upper.shiftedUp (static_cast<int> (sizeof (upper.mantissa) * 8 - numSignificandBits - 2));
|
||||
|
||||
lower = value.mantissa == hiddenBit ? MantissaAndExponent { (value.mantissa << 2) - 1, value.exponent - 2 }
|
||||
: MantissaAndExponent { (value.mantissa << 1) - 1, value.exponent - 1 };
|
||||
lower.mantissa <<= lower.exponent - upper.exponent;
|
||||
lower.exponent = upper.exponent;
|
||||
}
|
||||
|
||||
MantissaAndExponent lower, upper;
|
||||
};
|
||||
|
||||
static const char* writeAndGetEnd (char* pos, FloatOrDouble value, int maxDecimalPlaces, bool omitPointIfPossible)
|
||||
{
|
||||
auto startPos = pos;
|
||||
auto floatBits = getFloatBits (value);
|
||||
|
||||
if ((floatBits & signMask) == 0)
|
||||
{
|
||||
if (isZero (floatBits)) return writeZero (pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = write (pos, '-');
|
||||
|
||||
if (isZero (floatBits)) return writeZero (pos);
|
||||
|
||||
value = -value;
|
||||
floatBits &= ~signMask;
|
||||
}
|
||||
|
||||
if (floatBits == nanBits) return write (pos, 'n', 'a', 'n');
|
||||
if (floatBits == infBits) return write (pos, 'i', 'n', 'f');
|
||||
|
||||
auto v = MantissaAndExponent::create (floatBits, floatBits & significandMask);
|
||||
Limits limits (v);
|
||||
|
||||
int K;
|
||||
auto powerOf10 = createPowerOf10 (limits.upper.exponent, K);
|
||||
auto w = powerOf10 * v.normalized();
|
||||
auto upperBound = powerOf10 * limits.upper;
|
||||
upperBound.mantissa--;
|
||||
auto lowerBound = powerOf10 * limits.lower;
|
||||
lowerBound.mantissa++;
|
||||
|
||||
auto totalLength = generateDigits (pos, upperBound, upperBound.mantissa - w.mantissa, upperBound.mantissa - lowerBound.mantissa, K);
|
||||
auto end = addDecimalPointAndExponent (pos, totalLength, K, maxDecimalPlaces < 0 ? defaultNumDecimalPlaces : maxDecimalPlaces);
|
||||
|
||||
if (omitPointIfPossible && end > startPos + 1 && end[-1] == '0' && end[-2] == '.')
|
||||
end -= 2;
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
static const char* addDecimalPointAndExponent (char* pos, uint32_t totalLength, int K, int maxDecimalPlaces)
|
||||
{
|
||||
auto mantissaDigits = static_cast<int> (totalLength) + K;
|
||||
|
||||
if (mantissaDigits < -maxDecimalPlaces) return writeZero (pos);
|
||||
if (mantissaDigits <= 0 && mantissaDigits > -6) return writeWithoutExponentLessThan1 (pos, totalLength, mantissaDigits, maxDecimalPlaces);
|
||||
if (mantissaDigits > 0 && mantissaDigits <= 21) return writeWithoutExponentGreaterThan1 (pos, totalLength, static_cast<uint32_t> (mantissaDigits), maxDecimalPlaces, K);
|
||||
|
||||
return writeAsExponentNotation (pos, totalLength, mantissaDigits - 1);
|
||||
}
|
||||
|
||||
static uint64_t getFloatBits (double value) { uint64_t i; memcpy (&i, &value, sizeof (i)); return i; }
|
||||
static uint64_t getFloatBits (float value) { uint32_t i; memcpy (&i, &value, sizeof (i)); return i; }
|
||||
static bool isZero (uint64_t floatBits) { return (floatBits & (exponentMask | significandMask)) == 0; }
|
||||
|
||||
static constexpr int defaultNumDecimalPlaces = 324;
|
||||
static constexpr int numSignificandBits = sizeof (FloatOrDouble) == 8 ? 52 : 23;
|
||||
static constexpr uint64_t signMask = 1ull << (sizeof (FloatOrDouble) * 8 - 1);
|
||||
static constexpr uint64_t hiddenBit = 1ull << numSignificandBits;
|
||||
static constexpr uint64_t significandMask = hiddenBit - 1;
|
||||
static constexpr uint64_t exponentMask = sizeof (FloatOrDouble) == 8 ? 0x7ff0000000000000ull : 0x7f800000ull;
|
||||
static constexpr uint64_t nanBits = sizeof (FloatOrDouble) == 8 ? 0x7ff8000000000000ull : 0x7fc00000ull;
|
||||
static constexpr uint64_t infBits = sizeof (FloatOrDouble) == 8 ? 0x7ff0000000000000ull : 0x7f800000ull;
|
||||
static constexpr uint32_t powersOf10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };
|
||||
|
||||
static MantissaAndExponent createPowerOf10 (int exponentBase2, int& K)
|
||||
{
|
||||
static constexpr MantissaAndExponent powerOf10List[] =
|
||||
{
|
||||
{ 0xfa8fd5a0081c0288ull, -1220 }, { 0xbaaee17fa23ebf76ull, -1193 }, { 0x8b16fb203055ac76ull, -1166 }, { 0xcf42894a5dce35eaull, -1140 }, { 0x9a6bb0aa55653b2dull, -1113 },
|
||||
{ 0xe61acf033d1a45dfull, -1087 }, { 0xab70fe17c79ac6caull, -1060 }, { 0xff77b1fcbebcdc4full, -1034 }, { 0xbe5691ef416bd60cull, -1007 }, { 0x8dd01fad907ffc3cull, -980 },
|
||||
{ 0xd3515c2831559a83ull, -954 }, { 0x9d71ac8fada6c9b5ull, -927 }, { 0xea9c227723ee8bcbull, -901 }, { 0xaecc49914078536dull, -874 }, { 0x823c12795db6ce57ull, -847 },
|
||||
{ 0xc21094364dfb5637ull, -821 }, { 0x9096ea6f3848984full, -794 }, { 0xd77485cb25823ac7ull, -768 }, { 0xa086cfcd97bf97f4ull, -741 }, { 0xef340a98172aace5ull, -715 },
|
||||
{ 0xb23867fb2a35b28eull, -688 }, { 0x84c8d4dfd2c63f3bull, -661 }, { 0xc5dd44271ad3cdbaull, -635 }, { 0x936b9fcebb25c996ull, -608 }, { 0xdbac6c247d62a584ull, -582 },
|
||||
{ 0xa3ab66580d5fdaf6ull, -555 }, { 0xf3e2f893dec3f126ull, -529 }, { 0xb5b5ada8aaff80b8ull, -502 }, { 0x87625f056c7c4a8bull, -475 }, { 0xc9bcff6034c13053ull, -449 },
|
||||
{ 0x964e858c91ba2655ull, -422 }, { 0xdff9772470297ebdull, -396 }, { 0xa6dfbd9fb8e5b88full, -369 }, { 0xf8a95fcf88747d94ull, -343 }, { 0xb94470938fa89bcfull, -316 },
|
||||
{ 0x8a08f0f8bf0f156bull, -289 }, { 0xcdb02555653131b6ull, -263 }, { 0x993fe2c6d07b7facull, -236 }, { 0xe45c10c42a2b3b06ull, -210 }, { 0xaa242499697392d3ull, -183 },
|
||||
{ 0xfd87b5f28300ca0eull, -157 }, { 0xbce5086492111aebull, -130 }, { 0x8cbccc096f5088ccull, -103 }, { 0xd1b71758e219652cull, -77 }, { 0x9c40000000000000ull, -50 },
|
||||
{ 0xe8d4a51000000000ull, -24 }, { 0xad78ebc5ac620000ull, 3 }, { 0x813f3978f8940984ull, 30 }, { 0xc097ce7bc90715b3ull, 56 }, { 0x8f7e32ce7bea5c70ull, 83 },
|
||||
{ 0xd5d238a4abe98068ull, 109 }, { 0x9f4f2726179a2245ull, 136 }, { 0xed63a231d4c4fb27ull, 162 }, { 0xb0de65388cc8ada8ull, 189 }, { 0x83c7088e1aab65dbull, 216 },
|
||||
{ 0xc45d1df942711d9aull, 242 }, { 0x924d692ca61be758ull, 269 }, { 0xda01ee641a708deaull, 295 }, { 0xa26da3999aef774aull, 322 }, { 0xf209787bb47d6b85ull, 348 },
|
||||
{ 0xb454e4a179dd1877ull, 375 }, { 0x865b86925b9bc5c2ull, 402 }, { 0xc83553c5c8965d3dull, 428 }, { 0x952ab45cfa97a0b3ull, 455 }, { 0xde469fbd99a05fe3ull, 481 },
|
||||
{ 0xa59bc234db398c25ull, 508 }, { 0xf6c69a72a3989f5cull, 534 }, { 0xb7dcbf5354e9beceull, 561 }, { 0x88fcf317f22241e2ull, 588 }, { 0xcc20ce9bd35c78a5ull, 614 },
|
||||
{ 0x98165af37b2153dfull, 641 }, { 0xe2a0b5dc971f303aull, 667 }, { 0xa8d9d1535ce3b396ull, 694 }, { 0xfb9b7cd9a4a7443cull, 720 }, { 0xbb764c4ca7a44410ull, 747 },
|
||||
{ 0x8bab8eefb6409c1aull, 774 }, { 0xd01fef10a657842cull, 800 }, { 0x9b10a4e5e9913129ull, 827 }, { 0xe7109bfba19c0c9dull, 853 }, { 0xac2820d9623bf429ull, 880 },
|
||||
{ 0x80444b5e7aa7cf85ull, 907 }, { 0xbf21e44003acdd2dull, 933 }, { 0x8e679c2f5e44ff8full, 960 }, { 0xd433179d9c8cb841ull, 986 }, { 0x9e19db92b4e31ba9ull, 1013 },
|
||||
{ 0xeb96bf6ebadf77d9ull, 1039 }, { 0xaf87023b9bf0ee6bull, 1066 }
|
||||
};
|
||||
|
||||
auto dk = (exponentBase2 + 61) * -0.30102999566398114;
|
||||
auto ik = static_cast<int> (dk);
|
||||
auto index = ((ik + (dk > ik ? 348 : 347)) >> 3) + 1;
|
||||
K = 348 - (index << 3);
|
||||
return powerOf10List[index];
|
||||
}
|
||||
};
|
||||
|
||||
inline std::string floatToString (float value) { return FloatToStringBuffer<float> (value, -1, false).toString(); }
|
||||
inline std::string floatToString (double value) { return FloatToStringBuffer<double> (value, -1, false).toString(); }
|
||||
inline std::string floatToString (float value, int maxDecimals, bool omitPointIfPossible) { return FloatToStringBuffer<float> (value, maxDecimals, omitPointIfPossible).toString(); }
|
||||
inline std::string floatToString (double value, int maxDecimals, bool omitPointIfPossible) { return FloatToStringBuffer<double> (value, maxDecimals, omitPointIfPossible).toString(); }
|
||||
|
||||
} // namespace choc::text
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,561 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_JSON_HEADER_INCLUDED
|
||||
#define CHOC_JSON_HEADER_INCLUDED
|
||||
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "choc_UTF8.h"
|
||||
#include "choc_FloatToString.h"
|
||||
#include "../containers/choc_Value.h"
|
||||
|
||||
#undef max // It's never a smart idea to include any C headers before your C++ ones, as it
|
||||
#undef min // risks polluting your namespace with all kinds of dangerous macros like these ones.
|
||||
|
||||
namespace choc::json
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/// A parse exception, thrown by choc::json::parse() as needed.
|
||||
struct ParseError : public std::runtime_error
|
||||
{
|
||||
ParseError (const char* message, choc::text::LineAndColumn lc)
|
||||
: std::runtime_error (message), lineAndColumn (lc) {}
|
||||
|
||||
choc::text::LineAndColumn lineAndColumn;
|
||||
};
|
||||
|
||||
/// Parses some JSON text into a choc::value::Value object, using the given pool.
|
||||
/// Any errors will result in a ParseError exception being thrown.
|
||||
[[nodiscard]] value::Value parse (text::UTF8Pointer);
|
||||
|
||||
/// Parses some JSON text into a choc::value::Value object, using the given pool.
|
||||
/// Any errors will result in a ParseError exception being thrown.
|
||||
[[nodiscard]] value::Value parse (std::string_view);
|
||||
|
||||
/// Attempts to parse a bare JSON value such as a number, string, object etc
|
||||
[[nodiscard]] value::Value parseValue (std::string_view);
|
||||
|
||||
/// A helper function to create a JSON-friendly Value object with a set of properties.
|
||||
/// The argument list must be contain pairs of names and values, e.g.
|
||||
///
|
||||
/// auto myObject = choc::json::create ("property1", 1234,
|
||||
/// "property2", "hello",
|
||||
/// "property3", 100.0f);
|
||||
///
|
||||
/// Essentially, this is a shorthand for calling choc::value::createObject()
|
||||
/// and passing it an empty type name.
|
||||
template <typename... Properties>
|
||||
[[nodiscard]] value::Value create (Properties&&... propertyNamesAndValues);
|
||||
|
||||
//==============================================================================
|
||||
/// Formats a value as a JSON string.
|
||||
/// If useLineBreaks is true, it'll be formatted as multi-line JSON, if false it'll
|
||||
/// just be returned as a single line.
|
||||
[[nodiscard]] std::string toString (const value::ValueView&, bool useLineBreaks = false);
|
||||
|
||||
/// Writes a version of a string to an output stream, with any illegal or non-ascii
|
||||
/// written as their equivalent JSON escape sequences.
|
||||
template <typename OutputStreamType>
|
||||
void writeWithEscapeCharacters (OutputStreamType&, text::UTF8Pointer sourceString);
|
||||
|
||||
/// Returns a version of a string with illegal or non-ascii converted into the
|
||||
/// equivalent JSON escape sequences.
|
||||
[[nodiscard]] std::string addEscapeCharacters (text::UTF8Pointer sourceString);
|
||||
|
||||
/// Returns a version of a string with illegal or non-ascii converted into the
|
||||
/// equivalent JSON escape sequences.
|
||||
[[nodiscard]] std::string addEscapeCharacters (std::string_view sourceString);
|
||||
|
||||
/// Returns a version of a string with illegal or non-ascii converted into the
|
||||
/// equivalent JSON escape sequences.
|
||||
[[nodiscard]] std::string getEscapedQuotedString (std::string_view sourceString);
|
||||
|
||||
/// Converts a double to a JSON-format string representation.
|
||||
std::string doubleToString (double value);
|
||||
|
||||
|
||||
|
||||
//==============================================================================
|
||||
// _ _ _ _
|
||||
// __| | ___ | |_ __ _ (_)| | ___
|
||||
// / _` | / _ \| __| / _` || || |/ __|
|
||||
// | (_| || __/| |_ | (_| || || |\__ \ _ _ _
|
||||
// \__,_| \___| \__| \__,_||_||_||___/(_)(_)(_)
|
||||
//
|
||||
// Code beyond this point is implementation detail...
|
||||
//
|
||||
//==============================================================================
|
||||
|
||||
template <typename OutputStreamType>
|
||||
void writeWithEscapeCharacters (OutputStreamType& out, text::UTF8Pointer source)
|
||||
{
|
||||
auto writeUnicode = [] (OutputStreamType& o, auto digit)
|
||||
{
|
||||
auto hexDigit = [] (auto value) -> char { return "0123456789abcdef"[value & 15]; };
|
||||
|
||||
o << "\\u" << hexDigit (digit >> 12) << hexDigit (digit >> 8) << hexDigit (digit >> 4) << hexDigit (digit);
|
||||
};
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto c = *source;
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case 0: return;
|
||||
|
||||
case '\"': out << "\\\""; break;
|
||||
case '\\': out << "\\\\"; break;
|
||||
case '\n': out << "\\n"; break;
|
||||
case '\r': out << "\\r"; break;
|
||||
case '\t': out << "\\t"; break;
|
||||
case '\a': out << "\\a"; break;
|
||||
case '\b': out << "\\b"; break;
|
||||
case '\f': out << "\\f"; break;
|
||||
|
||||
default:
|
||||
if (c > 31 && c < 127)
|
||||
{
|
||||
out << (char) c;
|
||||
break;
|
||||
}
|
||||
|
||||
if (c >= 0x10000)
|
||||
{
|
||||
auto pair = choc::text::splitCodePointIntoSurrogatePair (c);
|
||||
writeUnicode (out, pair.high);
|
||||
writeUnicode (out, pair.low);
|
||||
break;
|
||||
}
|
||||
|
||||
writeUnicode (out, c);
|
||||
break;
|
||||
}
|
||||
|
||||
++source;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string addEscapeCharacters (text::UTF8Pointer source)
|
||||
{
|
||||
std::ostringstream result (std::ios::binary);
|
||||
writeWithEscapeCharacters (result, source);
|
||||
return result.str();
|
||||
}
|
||||
|
||||
inline std::string addEscapeCharacters (std::string_view source)
|
||||
{
|
||||
return addEscapeCharacters (text::UTF8Pointer (std::string (source).c_str()));
|
||||
}
|
||||
|
||||
inline std::string getEscapedQuotedString (std::string_view s)
|
||||
{
|
||||
std::ostringstream result (std::ios::binary);
|
||||
result << '"';
|
||||
writeWithEscapeCharacters (result, text::UTF8Pointer (std::string (s).c_str()));
|
||||
result << '"';
|
||||
return result.str();
|
||||
}
|
||||
|
||||
inline std::string doubleToString (double value)
|
||||
{
|
||||
if (std::isfinite (value)) return choc::text::floatToString (value, -1, true);
|
||||
if (std::isnan (value)) return "\"NaN\"";
|
||||
|
||||
return value >= 0 ? "\"Infinity\""
|
||||
: "\"-Infinity\"";
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
template <typename Stream>
|
||||
struct Writer
|
||||
{
|
||||
Stream& out;
|
||||
uint32_t indentSize, currentIndent = 0;
|
||||
static constexpr const char newLine = '\n';
|
||||
|
||||
std::string getIndent() const { return std::string (currentIndent, ' '); }
|
||||
void startIndent() { currentIndent += indentSize; out << newLine << getIndent(); }
|
||||
void endIndent() { currentIndent -= indentSize; out << newLine << getIndent(); }
|
||||
|
||||
void dump (const value::ValueView& v)
|
||||
{
|
||||
if (v.isVoid()) { out << "null"; return; }
|
||||
if (v.isString()) { out << getEscapedQuotedString (v.getString()); return; }
|
||||
if (v.isBool()) { out << (v.getBool() ? "true" : "false"); return; }
|
||||
if (v.isFloat()) { out << doubleToString (v.get<double>()); return; }
|
||||
if (v.isInt()) { out << v.get<int64_t>(); return; }
|
||||
if (v.isObject()) return dumpObject (v);
|
||||
if (v.isArray() || v.isVector()) return dumpArrayOrVector (v);
|
||||
}
|
||||
|
||||
void dumpArrayOrVector (const value::ValueView& v)
|
||||
{
|
||||
out << '[';
|
||||
auto numElements = v.size();
|
||||
|
||||
if (indentSize != 0 && numElements != 0)
|
||||
{
|
||||
startIndent();
|
||||
|
||||
for (uint32_t i = 0; i < numElements; ++i)
|
||||
{
|
||||
dump (v[i]);
|
||||
|
||||
if (i != numElements - 1)
|
||||
out << "," << newLine << getIndent();
|
||||
}
|
||||
|
||||
endIndent();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint32_t i = 0; i < numElements; ++i)
|
||||
{
|
||||
if (i != 0) out << ", ";
|
||||
dump (v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
out << ']';
|
||||
}
|
||||
|
||||
void dumpObject (const value::ValueView& object)
|
||||
{
|
||||
out << '{';
|
||||
auto numMembers = object.size();
|
||||
|
||||
if (indentSize != 0 && numMembers != 0)
|
||||
{
|
||||
startIndent();
|
||||
|
||||
for (uint32_t i = 0; i < numMembers; ++i)
|
||||
{
|
||||
auto member = object.getObjectMemberAt (i);
|
||||
out << getEscapedQuotedString (member.name) << ": ";
|
||||
dump (member.value);
|
||||
|
||||
if (i != numMembers - 1)
|
||||
out << "," << newLine << getIndent();
|
||||
}
|
||||
|
||||
endIndent();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint32_t i = 0; i < numMembers; ++i)
|
||||
{
|
||||
if (i != 0) out << ", ";
|
||||
|
||||
auto member = object.getObjectMemberAt (i);
|
||||
out << getEscapedQuotedString (member.name) << ": ";
|
||||
dump (member.value);
|
||||
}
|
||||
}
|
||||
|
||||
out << '}';
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Stream>
|
||||
void writeAsJSON (Stream& output, const value::ValueView& value, bool useMultipleLines)
|
||||
{
|
||||
Writer<Stream> { output, useMultipleLines ? 2u : 0u }.dump (value);
|
||||
}
|
||||
|
||||
inline std::string toString (const value::ValueView& v, bool useLineBreaks)
|
||||
{
|
||||
std::ostringstream out (std::ios::binary);
|
||||
writeAsJSON (out, v, useLineBreaks);
|
||||
return out.str();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
[[noreturn]] static inline void throwParseError (const char* error, text::UTF8Pointer source, text::UTF8Pointer errorPos)
|
||||
{
|
||||
throw ParseError (error, text::findLineAndColumn (source, errorPos));
|
||||
}
|
||||
|
||||
inline value::Value parse (text::UTF8Pointer text, bool parseBareValue)
|
||||
{
|
||||
struct Parser
|
||||
{
|
||||
text::UTF8Pointer source, current;
|
||||
|
||||
bool isEOF() const { return current.empty(); }
|
||||
uint32_t peek() const { return *current; }
|
||||
uint32_t pop() { return current.popFirstChar(); }
|
||||
bool popIf (char c) { return current.skipIfStartsWith (c); }
|
||||
bool popIf (const char* c) { return current.skipIfStartsWith (c); }
|
||||
|
||||
static bool isWhitespace (uint32_t c) { return c == ' ' || (c <= 13 && c >= 9); }
|
||||
void skipWhitespace() { auto p = current; while (isWhitespace (p.popFirstChar())) current = p; }
|
||||
|
||||
[[noreturn]] void throwError (const char* error, text::UTF8Pointer errorPos) { throwParseError (error, source, errorPos); }
|
||||
[[noreturn]] void throwError (const char* error) { throwError (error, current); }
|
||||
|
||||
value::Value parseTopLevel()
|
||||
{
|
||||
skipWhitespace();
|
||||
|
||||
if (popIf ('[')) return parseArray();
|
||||
if (popIf ('{')) return parseObject();
|
||||
if (! isEOF()) throwError ("Expected an object or array");
|
||||
return {};
|
||||
}
|
||||
|
||||
value::Value parseArray()
|
||||
{
|
||||
auto result = value::createEmptyArray();
|
||||
auto arrayStart = current;
|
||||
|
||||
skipWhitespace();
|
||||
if (popIf (']')) return result;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (isEOF()) throwError ("Unexpected EOF in array declaration", arrayStart);
|
||||
|
||||
result.addArrayElement (parseValue());
|
||||
skipWhitespace();
|
||||
|
||||
if (popIf (',')) continue;
|
||||
if (popIf (']')) break;
|
||||
throwError ("Expected ',' or ']'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
value::Value parseObject()
|
||||
{
|
||||
auto result = value::createObject ({});
|
||||
auto objectStart = current;
|
||||
|
||||
skipWhitespace();
|
||||
if (popIf ('}')) return result;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (isEOF()) throwError ("Unexpected EOF in object declaration", objectStart);
|
||||
|
||||
if (! popIf ('"')) throwError ("Expected a name");
|
||||
auto errorPos = current;
|
||||
auto name = parseString();
|
||||
|
||||
if (name.empty())
|
||||
throwError ("Property names cannot be empty", errorPos);
|
||||
|
||||
skipWhitespace();
|
||||
errorPos = current;
|
||||
if (! popIf (':')) throwError ("Expected ':'");
|
||||
result.addMember (std::move (name), parseValue());
|
||||
skipWhitespace();
|
||||
|
||||
if (popIf (',')) continue;
|
||||
if (popIf ('}')) break;
|
||||
throwError ("Expected ',' or '}'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
value::Value parseValue()
|
||||
{
|
||||
skipWhitespace();
|
||||
auto startPos = current;
|
||||
|
||||
switch (pop())
|
||||
{
|
||||
case '[': return parseArray();
|
||||
case '{': return parseObject();
|
||||
case '"': return value::createString (parseString());
|
||||
case '-': skipWhitespace(); return parseNumber (true);
|
||||
case 'n': if (popIf ("ull")) return {}; break;
|
||||
case 't': if (popIf ("rue")) return value::createBool (true); break;
|
||||
case 'f': if (popIf ("alse")) return value::createBool (false); break;
|
||||
|
||||
case '0': case '1': case '2': case '3': case '4':
|
||||
case '5': case '6': case '7': case '8': case '9':
|
||||
current = startPos;
|
||||
return parseNumber (false);
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
throwError ("Syntax error", startPos);
|
||||
}
|
||||
|
||||
value::Value parseNumber (bool negate)
|
||||
{
|
||||
auto startPos = current;
|
||||
bool hadDot = false, hadExponent = false;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto lastPos = current;
|
||||
auto c = pop();
|
||||
|
||||
if (c >= '0' && c <= '9') continue;
|
||||
if (c == '.' && ! hadDot) { hadDot = true; continue; }
|
||||
|
||||
if (! hadExponent && (c == 'e' || c == 'E'))
|
||||
{
|
||||
hadDot = true;
|
||||
hadExponent = true;
|
||||
popIf ('-');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isWhitespace (c) || c == ',' || c == '}' || c == ']' || c == 0)
|
||||
{
|
||||
current = lastPos;
|
||||
char* endOfParsedNumber = nullptr;
|
||||
|
||||
if (! (hadDot || hadExponent))
|
||||
{
|
||||
auto v = std::strtoll (startPos.data(), &endOfParsedNumber, 10);
|
||||
|
||||
if (endOfParsedNumber == lastPos.data()
|
||||
&& v != std::numeric_limits<long long>::max()
|
||||
&& v != std::numeric_limits<long long>::min())
|
||||
return value::createInt64 (static_cast<int64_t> (negate ? -v : v));
|
||||
}
|
||||
|
||||
auto v = std::strtod (startPos.data(), &endOfParsedNumber);
|
||||
|
||||
if (endOfParsedNumber == lastPos.data())
|
||||
return value::createFloat64 (negate ? -v : v);
|
||||
}
|
||||
|
||||
throwError ("Syntax error in number", lastPos);
|
||||
}
|
||||
}
|
||||
|
||||
std::string parseString()
|
||||
{
|
||||
std::ostringstream s (std::ios::binary);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto c = pop();
|
||||
|
||||
if (c == '"')
|
||||
break;
|
||||
|
||||
if (c == '\\')
|
||||
{
|
||||
auto errorPos = current;
|
||||
c = pop();
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case 'a': c = '\a'; break;
|
||||
case 'b': c = '\b'; break;
|
||||
case 'f': c = '\f'; break;
|
||||
case 'n': c = '\n'; break;
|
||||
case 'r': c = '\r'; break;
|
||||
case 't': c = '\t'; break;
|
||||
case 'u': c = parseUnicodeCharacterNumber (false); break;
|
||||
case 0: throwError ("Unexpected EOF in string constant", errorPos);
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
char utf8Bytes[8];
|
||||
auto numBytes = text::convertUnicodeCodepointToUTF8 (utf8Bytes, c);
|
||||
|
||||
for (uint32_t i = 0; i < numBytes; ++i)
|
||||
s << utf8Bytes[i];
|
||||
}
|
||||
|
||||
return s.str();
|
||||
}
|
||||
|
||||
uint32_t parseUnicodeCharacterNumber (bool isLowSurrogate)
|
||||
{
|
||||
uint32_t result = 0;
|
||||
|
||||
for (int i = 4; --i >= 0;)
|
||||
{
|
||||
auto errorPos = current;
|
||||
auto digit = pop();
|
||||
|
||||
if (digit >= '0' && digit <= '9') digit -= '0';
|
||||
else if (digit >= 'a' && digit <= 'f') digit = 10 + (digit - 'a');
|
||||
else if (digit >= 'A' && digit <= 'F') digit = 10 + (digit - 'A');
|
||||
else throwError ("Syntax error in unicode character", errorPos);
|
||||
|
||||
result = (result << 4) + digit;
|
||||
}
|
||||
|
||||
if (isLowSurrogate && ! text::isUnicodeLowSurrogate (result))
|
||||
throwError ("Expected a unicode low surrogate codepoint");
|
||||
|
||||
if (text::isUnicodeHighSurrogate (result))
|
||||
{
|
||||
if (! isLowSurrogate && popIf ("\\u"))
|
||||
return text::createUnicodeFromHighAndLowSurrogates ({ result, parseUnicodeCharacterNumber (true) });
|
||||
|
||||
throwError ("Expected a unicode low surrogate codepoint");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
Parser p { text, text };
|
||||
return parseBareValue ? p.parseValue()
|
||||
: p.parseTopLevel();
|
||||
}
|
||||
|
||||
inline value::Value parse (const char* text, size_t numbytes, bool parseBareValue)
|
||||
{
|
||||
if (text == nullptr)
|
||||
{
|
||||
text = "";
|
||||
numbytes = 0;
|
||||
}
|
||||
|
||||
if (auto error = text::findInvalidUTF8Data (text, numbytes))
|
||||
throwParseError ("Illegal UTF8 data", text::UTF8Pointer (text), text::UTF8Pointer (error));
|
||||
|
||||
return parse (text::UTF8Pointer (text), parseBareValue);
|
||||
}
|
||||
|
||||
inline value::Value parse (std::string_view text) { return parse (text.data(), text.length(), false); }
|
||||
inline value::Value parseValue (std::string_view text) { return parse (text.data(), text.length(), true); }
|
||||
|
||||
template <typename... Properties>
|
||||
value::Value create (Properties&&... properties)
|
||||
{
|
||||
static_assert ((sizeof...(properties) & 1) == 0, "The arguments must be a sequence of name, value pairs");
|
||||
return choc::value::createObject ({}, std::forward<Properties> (properties)...);
|
||||
}
|
||||
|
||||
|
||||
} // namespace choc::json
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,600 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_STRING_UTILS_HEADER_INCLUDED
|
||||
#define CHOC_STRING_UTILS_HEADER_INCLUDED
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include "../platform/choc_Assert.h"
|
||||
|
||||
namespace choc::text
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
inline bool isWhitespace (char c) { return c == ' ' || (c <= 13 && c >= 9); }
|
||||
inline bool isDigit (char c) { return static_cast<uint32_t> (c - '0') < 10; }
|
||||
|
||||
/// Replaces all occurrences of a one or more substrings.
|
||||
/// The arguments must be a sequence of pairs of strings, where the first of each pair is the string to
|
||||
/// look for, followed by its replacement.
|
||||
template <typename StringType, typename... OtherReplacements>
|
||||
[[nodiscard]] std::string replace (StringType textToSearch,
|
||||
std::string_view firstSubstringToReplace, std::string_view firstReplacement,
|
||||
OtherReplacements&&... otherPairsOfStringsToReplace);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its start and end.
|
||||
[[nodiscard]] std::string trim (std::string textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its start and end.
|
||||
[[nodiscard]] std::string_view trim (std::string_view textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its start and end.
|
||||
[[nodiscard]] std::string_view trim (const char* textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its start.
|
||||
[[nodiscard]] std::string trimStart (std::string textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its start.
|
||||
[[nodiscard]] std::string_view trimStart (std::string_view textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its start.
|
||||
[[nodiscard]] std::string_view trimStart (const char* textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its end.
|
||||
[[nodiscard]] std::string trimEnd (std::string textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its end.
|
||||
[[nodiscard]] std::string_view trimEnd (std::string_view textToTrim);
|
||||
|
||||
/// Returns a string with any whitespace trimmed from its end.
|
||||
[[nodiscard]] std::string_view trimEnd (const char* textToTrim);
|
||||
|
||||
/// If the string begins with one or more instances of the given character, this
|
||||
/// skips past them, returning the remainder of the string.
|
||||
[[nodiscard]] std::string_view trimCharacterAtStart (std::string_view textToTrim, char characterToSkip);
|
||||
|
||||
/// If the given character is at the start and end of the string, it trims it away.
|
||||
[[nodiscard]] std::string removeOuterCharacter (std::string text, char outerChar);
|
||||
|
||||
[[nodiscard]] inline std::string removeDoubleQuotes (std::string text) { return removeOuterCharacter (std::move (text), '"'); }
|
||||
[[nodiscard]] inline std::string removeSingleQuotes (std::string text) { return removeOuterCharacter (std::move (text), '\''); }
|
||||
|
||||
[[nodiscard]] inline std::string addDoubleQuotes (std::string text) { return "\"" + std::move (text) + "\""; }
|
||||
[[nodiscard]] inline std::string addSingleQuotes (std::string text) { return "'" + std::move (text) + "'"; }
|
||||
|
||||
[[nodiscard]] std::string toLowerCase (std::string);
|
||||
[[nodiscard]] std::string toUpperCase (std::string);
|
||||
|
||||
template <typename IsDelimiterChar>
|
||||
[[nodiscard]] std::vector<std::string> splitString (std::string_view textToSplit,
|
||||
IsDelimiterChar&& isDelimiterChar,
|
||||
bool includeDelimitersInResult);
|
||||
|
||||
template <typename CharStartsDelimiter, typename CharIsInDelimiterBody>
|
||||
[[nodiscard]] std::vector<std::string> splitString (std::string_view textToSplit,
|
||||
CharStartsDelimiter&& isDelimiterStart,
|
||||
CharIsInDelimiterBody&& isDelimiterBody,
|
||||
bool includeDelimitersInResult);
|
||||
|
||||
[[nodiscard]] std::vector<std::string> splitString (std::string_view textToSplit,
|
||||
char delimiterCharacter,
|
||||
bool includeDelimitersInResult);
|
||||
|
||||
[[nodiscard]] std::vector<std::string> splitAtWhitespace (std::string_view text,
|
||||
bool keepDelimiters = false);
|
||||
|
||||
/// Splits a string at newline characters, returning an array of strings.
|
||||
[[nodiscard]] std::vector<std::string> splitIntoLines (std::string_view text,
|
||||
bool includeNewLinesInResult);
|
||||
|
||||
/// Joins some kind of array of strings into a single string, adding the given separator
|
||||
/// between them (but not adding it at the start or end)
|
||||
template <typename ArrayOfStrings>
|
||||
[[nodiscard]] std::string joinStrings (const ArrayOfStrings& strings,
|
||||
std::string_view separator);
|
||||
|
||||
/// Returns true if this text contains the given sub-string.
|
||||
bool contains (std::string_view text, std::string_view possibleSubstring);
|
||||
/// Returns true if this text starts with the given character.
|
||||
bool startsWith (std::string_view text, char possibleStart);
|
||||
/// Returns true if this text starts with the given sub-string.
|
||||
bool startsWith (std::string_view text, std::string_view possibleStart);
|
||||
/// Returns true if this text ends with the given sub-string.
|
||||
bool endsWith (std::string_view text, char possibleEnd);
|
||||
/// Returns true if this text ends with the given sub-string.
|
||||
bool endsWith (std::string_view text, std::string_view possibleEnd);
|
||||
|
||||
/// Calculates the Levenstein distance between two strings.
|
||||
template <typename StringType>
|
||||
size_t getLevenshteinDistance (const StringType& string1,
|
||||
const StringType& string2);
|
||||
|
||||
/// Converts a hex character to a number 0-15, or -1 if it's not a valid hex digit.
|
||||
int hexDigitToInt (uint32_t unicodeChar);
|
||||
|
||||
/// Returns a hex string for the given value.
|
||||
/// If the minimum number of digits is non-zero, it will be zero-padded to fill this length;
|
||||
template <typename IntegerType>
|
||||
std::string createHexString (IntegerType value, int minNumDigits = 0);
|
||||
|
||||
/// Returns a truncated, easy-to-read version of a time as hours, seconds or milliseconds,
|
||||
/// depending on its magnitude. The use-cases include things like logging or console app output.
|
||||
std::string getDurationDescription (std::chrono::duration<double, std::micro>);
|
||||
|
||||
/// Returns an easy-to-read description of a size in bytes. Depending on the magnitude,
|
||||
/// it might choose different units such as GB, MB, KB or just bytes.
|
||||
std::string getByteSizeDescription (uint64_t sizeInBytes);
|
||||
|
||||
/// Encodes a string as a legal URI, using percent-encoding (aka URL encoding)
|
||||
std::string percentEncodeURI (std::string_view text);
|
||||
|
||||
|
||||
//==============================================================================
|
||||
// _ _ _ _
|
||||
// __| | ___ | |_ __ _ (_)| | ___
|
||||
// / _` | / _ \| __| / _` || || |/ __|
|
||||
// | (_| || __/| |_ | (_| || || |\__ \ _ _ _
|
||||
// \__,_| \___| \__| \__,_||_||_||___/(_)(_)(_)
|
||||
//
|
||||
// Code beyond this point is implementation detail...
|
||||
//
|
||||
//==============================================================================
|
||||
|
||||
inline int hexDigitToInt (uint32_t c)
|
||||
{
|
||||
auto d1 = c - static_cast<uint32_t> ('0'); if (d1 < 10u) return static_cast<int> (d1);
|
||||
auto d2 = d1 + static_cast<uint32_t> ('0' - 'a'); if (d2 < 6u) return static_cast<int> (d2 + 10);
|
||||
auto d3 = d2 + static_cast<uint32_t> ('a' - 'A'); if (d3 < 6u) return static_cast<int> (d3 + 10);
|
||||
return -1;
|
||||
}
|
||||
|
||||
template <typename IntegerType>
|
||||
std::string createHexString (IntegerType v, int minNumDigits)
|
||||
{
|
||||
static_assert (std::is_integral<IntegerType>::value, "Need to pass integers into this method");
|
||||
auto value = static_cast<typename std::make_unsigned<IntegerType>::type> (v);
|
||||
CHOC_ASSERT (minNumDigits <= 32);
|
||||
|
||||
char hex[40];
|
||||
const auto end = hex + sizeof (hex) - 1;
|
||||
auto d = end;
|
||||
*d = 0;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
*--d = "0123456789abcdef"[static_cast<uint32_t> (value) & 15u];
|
||||
value = static_cast<decltype (value)> (value >> 4);
|
||||
--minNumDigits;
|
||||
|
||||
if (value == 0 && minNumDigits <= 0)
|
||||
return std::string (d, end);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename StringType, typename... OtherReplacements>
|
||||
std::string replace (StringType textToSearch, std::string_view firstToReplace, std::string_view firstReplacement,
|
||||
OtherReplacements&&... otherPairsOfStringsToReplace)
|
||||
{
|
||||
static_assert ((sizeof... (otherPairsOfStringsToReplace) & 1u) == 0,
|
||||
"This function expects a list of pairs of strings as its arguments");
|
||||
|
||||
if constexpr (std::is_same<const StringType, const std::string_view>::value || std::is_same<const StringType, const char* const>::value)
|
||||
{
|
||||
return replace (std::string (textToSearch), firstToReplace, firstReplacement,
|
||||
std::forward<OtherReplacements> (otherPairsOfStringsToReplace)...);
|
||||
}
|
||||
else if constexpr (sizeof... (otherPairsOfStringsToReplace) == 0)
|
||||
{
|
||||
size_t pos = 0;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
pos = textToSearch.find (firstToReplace, pos);
|
||||
|
||||
if (pos == std::string::npos)
|
||||
return textToSearch;
|
||||
|
||||
textToSearch.replace (pos, firstToReplace.length(), firstReplacement);
|
||||
pos += firstReplacement.length();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return replace (replace (std::move (textToSearch), firstToReplace, firstReplacement),
|
||||
std::forward<OtherReplacements> (otherPairsOfStringsToReplace)...);
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string trim (std::string text) { return trimStart (trimEnd (std::move (text))); }
|
||||
inline std::string_view trim (std::string_view text) { return trimStart (trimEnd (std::move (text))); }
|
||||
|
||||
inline std::string_view trim (const char* text) { return trim (std::string_view (text)); }
|
||||
inline std::string_view trimStart (const char* text) { return trimStart (std::string_view (text)); }
|
||||
inline std::string_view trimEnd (const char* text) { return trimEnd (std::string_view (text)); }
|
||||
|
||||
inline std::string trimStart (std::string text)
|
||||
{
|
||||
auto i = text.begin();
|
||||
|
||||
if (i == text.end()) return {};
|
||||
if (! isWhitespace (*i)) return text;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
++i;
|
||||
|
||||
if (i == text.end()) return {};
|
||||
if (! isWhitespace (*i)) return { i, text.end() };
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string_view trimStart (std::string_view text)
|
||||
{
|
||||
size_t i = 0;
|
||||
|
||||
for (auto c : text)
|
||||
{
|
||||
if (! isWhitespace (c))
|
||||
{
|
||||
text.remove_prefix (i);
|
||||
return text;
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::string trimEnd (std::string text)
|
||||
{
|
||||
for (auto i = text.end();;)
|
||||
{
|
||||
if (i == text.begin())
|
||||
return {};
|
||||
|
||||
--i;
|
||||
|
||||
if (! isWhitespace (*i))
|
||||
{
|
||||
text.erase (i + 1, text.end());
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string_view trimEnd (std::string_view text)
|
||||
{
|
||||
for (auto i = text.length(); i != 0; --i)
|
||||
if (! isWhitespace (text[i - 1]))
|
||||
return text.substr (0, i);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::string_view trimCharacterAtStart (std::string_view textToTrim, char characterToSkip)
|
||||
{
|
||||
for (size_t i = 0; i < textToTrim.length(); ++i)
|
||||
if (textToTrim[i] != characterToSkip)
|
||||
return textToTrim.substr (i);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::string removeOuterCharacter (std::string t, char outerChar)
|
||||
{
|
||||
if (t.length() >= 2 && t.front() == outerChar && t.back() == outerChar)
|
||||
return t.substr (1, t.length() - 2);
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
inline std::string toLowerCase (std::string s)
|
||||
{
|
||||
std::transform (s.begin(), s.end(), s.begin(), [] (auto c) { return static_cast<char> (std::tolower (static_cast<unsigned char> (c))); });
|
||||
return s;
|
||||
}
|
||||
|
||||
inline std::string toUpperCase (std::string s)
|
||||
{
|
||||
std::transform (s.begin(), s.end(), s.begin(), [] (auto c) { return static_cast<char> (std::toupper (static_cast<unsigned char> (c))); });
|
||||
return s;
|
||||
}
|
||||
|
||||
template <typename CharStartsDelimiter, typename CharIsInDelimiterBody>
|
||||
std::vector<std::string> splitString (std::string_view source,
|
||||
CharStartsDelimiter&& isDelimiterStart,
|
||||
CharIsInDelimiterBody&& isDelimiterBody,
|
||||
bool keepDelimiters)
|
||||
{
|
||||
std::vector<std::string> tokens;
|
||||
auto tokenStart = source.begin();
|
||||
auto pos = tokenStart;
|
||||
|
||||
while (pos != source.end())
|
||||
{
|
||||
if (isDelimiterStart (*pos))
|
||||
{
|
||||
auto delimiterStart = pos++;
|
||||
|
||||
while (pos != source.end() && isDelimiterBody (*pos))
|
||||
++pos;
|
||||
|
||||
if (pos != source.begin())
|
||||
tokens.push_back ({ tokenStart, keepDelimiters ? pos : delimiterStart });
|
||||
|
||||
tokenStart = pos;
|
||||
}
|
||||
else
|
||||
{
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos != source.begin())
|
||||
tokens.push_back ({ tokenStart, pos });
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
template <typename IsDelimiterChar>
|
||||
std::vector<std::string> splitString (std::string_view source, IsDelimiterChar&& isDelimiterChar, bool keepDelimiters)
|
||||
{
|
||||
std::vector<std::string> tokens;
|
||||
auto tokenStart = source.begin();
|
||||
auto pos = tokenStart;
|
||||
|
||||
while (pos != source.end())
|
||||
{
|
||||
if (isDelimiterChar (*pos))
|
||||
{
|
||||
tokens.push_back ({ tokenStart, keepDelimiters ? pos + 1 : pos });
|
||||
tokenStart = ++pos;
|
||||
}
|
||||
else
|
||||
{
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos != source.begin())
|
||||
tokens.push_back ({ tokenStart, pos });
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
inline std::vector<std::string> splitString (std::string_view text, char delimiterCharacter, bool keepDelimiters)
|
||||
{
|
||||
return splitString (text, [=] (char c) { return c == delimiterCharacter; }, keepDelimiters);
|
||||
}
|
||||
|
||||
inline std::vector<std::string> splitAtWhitespace (std::string_view text, bool keepDelimiters)
|
||||
{
|
||||
return splitString (text,
|
||||
[] (char c) { return isWhitespace (c); },
|
||||
[] (char c) { return isWhitespace (c); },
|
||||
keepDelimiters);
|
||||
}
|
||||
|
||||
inline std::vector<std::string> splitIntoLines (std::string_view text, bool includeNewLinesInResult)
|
||||
{
|
||||
return splitString (text, '\n', includeNewLinesInResult);
|
||||
}
|
||||
|
||||
template <typename ArrayOfStrings>
|
||||
inline std::string joinStrings (const ArrayOfStrings& strings, std::string_view sep)
|
||||
{
|
||||
if (strings.empty())
|
||||
return {};
|
||||
|
||||
auto spaceNeeded = sep.length() * strings.size();
|
||||
|
||||
for (auto& s : strings)
|
||||
spaceNeeded += s.length();
|
||||
|
||||
std::string result (strings.front());
|
||||
result.reserve (spaceNeeded);
|
||||
|
||||
for (size_t i = 1; i < strings.size(); ++i)
|
||||
{
|
||||
result += sep;
|
||||
result += strings[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline bool contains (std::string_view t, std::string_view s) { return t.find (s) != std::string::npos; }
|
||||
inline bool startsWith (std::string_view t, char s) { return ! t.empty() && t.front() == s; }
|
||||
inline bool endsWith (std::string_view t, char s) { return ! t.empty() && t.back() == s; }
|
||||
|
||||
inline bool startsWith (std::string_view t, std::string_view s)
|
||||
{
|
||||
auto len = s.length();
|
||||
return t.length() >= len && t.substr (0, len) == s;
|
||||
}
|
||||
|
||||
inline bool endsWith (std::string_view t, std::string_view s)
|
||||
{
|
||||
auto len1 = t.length(), len2 = s.length();
|
||||
return len1 >= len2 && t.substr (len1 - len2) == s;
|
||||
}
|
||||
|
||||
inline std::string getDurationDescription (std::chrono::duration<double, std::micro> d)
|
||||
{
|
||||
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds> (d).count();
|
||||
|
||||
if (microseconds < 0) return "-" + getDurationDescription (-d);
|
||||
if (microseconds == 0) return "0 sec";
|
||||
|
||||
std::string result;
|
||||
|
||||
auto addLevel = [&] (int64_t size, std::string_view units, int64_t decimalScale, int64_t modulo) -> bool
|
||||
{
|
||||
if (microseconds < size)
|
||||
return false;
|
||||
|
||||
if (! result.empty())
|
||||
result += ' ';
|
||||
|
||||
auto scaled = (microseconds * decimalScale + size / 2) / size;
|
||||
auto whole = scaled / decimalScale;
|
||||
|
||||
if (modulo != 0)
|
||||
whole = whole % modulo;
|
||||
|
||||
result += std::to_string (whole);
|
||||
|
||||
if (auto fraction = scaled % decimalScale)
|
||||
{
|
||||
result += '.';
|
||||
result += static_cast<char> ('0' + (fraction / 10));
|
||||
|
||||
if (fraction % 10 != 0)
|
||||
result += static_cast<char> ('0' + (fraction % 10));
|
||||
}
|
||||
|
||||
result += (whole == 1 && units.length() > 3 && units.back() == 's') ? units.substr (0, units.length() - 1) : units;
|
||||
return true;
|
||||
};
|
||||
|
||||
bool hours = addLevel (60000000ll * 60ll, " hours", 1, 0);
|
||||
bool mins = addLevel (60000000ll, " min", 1, hours ? 60 : 0);
|
||||
|
||||
if (hours)
|
||||
return result;
|
||||
|
||||
if (mins)
|
||||
{
|
||||
addLevel (1000000, " sec", 1, 60);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (! addLevel (1000000, " sec", 100, 0))
|
||||
if (! addLevel (1000, " ms", 100, 0))
|
||||
addLevel (1, " microseconds", 100, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename StringType>
|
||||
size_t getLevenshteinDistance (const StringType& string1, const StringType& string2)
|
||||
{
|
||||
if (string1.empty()) return string2.length();
|
||||
if (string2.empty()) return string1.length();
|
||||
|
||||
auto calculate = [] (size_t* costs, size_t numCosts, const StringType& s1, const StringType& s2) -> size_t
|
||||
{
|
||||
for (size_t i = 0; i < numCosts; ++i)
|
||||
costs[i] = i;
|
||||
|
||||
size_t p1 = 0;
|
||||
|
||||
for (auto c1 : s1)
|
||||
{
|
||||
auto corner = p1;
|
||||
*costs = p1 + 1;
|
||||
size_t p2 = 0;
|
||||
|
||||
for (auto c2 : s2)
|
||||
{
|
||||
auto upper = costs[p2 + 1];
|
||||
costs[p2 + 1] = c1 == c2 ? corner : (std::min (costs[p2], std::min (upper, corner)) + 1);
|
||||
++p2;
|
||||
corner = upper;
|
||||
}
|
||||
|
||||
++p1;
|
||||
}
|
||||
|
||||
return costs[numCosts - 1];
|
||||
};
|
||||
|
||||
auto sizeNeeded = string2.length() + 1;
|
||||
constexpr size_t maxStackSize = 96;
|
||||
|
||||
if (sizeNeeded <= maxStackSize)
|
||||
{
|
||||
size_t costs[maxStackSize];
|
||||
return calculate (costs, sizeNeeded, string1, string2);
|
||||
}
|
||||
|
||||
std::unique_ptr<size_t[]> costs (new size_t[sizeNeeded]);
|
||||
return calculate (costs.get(), sizeNeeded, string1, string2);
|
||||
}
|
||||
|
||||
inline std::string getByteSizeDescription (uint64_t size)
|
||||
{
|
||||
auto intToStringWith1DecPlace = [] (uint64_t n, uint64_t divisor) -> std::string
|
||||
{
|
||||
auto scaled = (n * 10 + divisor / 2) / divisor;
|
||||
auto result = std::to_string (scaled / 10);
|
||||
|
||||
if (auto fraction = scaled % 10)
|
||||
{
|
||||
result += '.';
|
||||
result += static_cast<char> ('0' + fraction);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
static constexpr uint64_t maxValue = std::numeric_limits<uint64_t>::max() / 10;
|
||||
|
||||
if (size >= 0x40000000) return intToStringWith1DecPlace (std::min (maxValue, size), 0x40000000) + " GB";
|
||||
if (size >= 0x100000) return intToStringWith1DecPlace (size, 0x100000) + " MB";
|
||||
if (size >= 0x400) return intToStringWith1DecPlace (size, 0x400) + " KB";
|
||||
if (size != 1) return std::to_string (size) + " bytes";
|
||||
|
||||
return "1 byte";
|
||||
}
|
||||
|
||||
inline std::string percentEncodeURI (std::string_view text)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve (text.length());
|
||||
|
||||
for (auto c : text)
|
||||
{
|
||||
if (std::string_view ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.~").find (c) != std::string_view::npos)
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += '%';
|
||||
result += "0123456789abcdef"[static_cast<uint8_t> (c) >> 4];
|
||||
result += "0123456789abcdef"[static_cast<uint8_t> (c) & 15u];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
} // namespace choc::text
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,655 @@
|
||||
//
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
// ██ ██ ██ ██ ██ ██ ** Classy Header-Only Classes **
|
||||
// ██ ███████ ██ ██ ██
|
||||
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||
// ██████ ██ ██ ██████ ██████
|
||||
//
|
||||
// CHOC is (C)2022 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#ifndef CHOC_UTF8_HEADER_INCLUDED
|
||||
#define CHOC_UTF8_HEADER_INCLUDED
|
||||
|
||||
#include <cstddef>
|
||||
#include "choc_StringUtilities.h"
|
||||
|
||||
namespace choc::text
|
||||
{
|
||||
|
||||
/// An integer type to represent a unicode code-point.
|
||||
using UnicodeChar = uint32_t;
|
||||
|
||||
//==============================================================================
|
||||
/** A non-owning pointer which can iterate over a chunk of null-terminated UTF-8 text
|
||||
and read it as wide unicode characters.
|
||||
*/
|
||||
struct UTF8Pointer
|
||||
{
|
||||
explicit constexpr UTF8Pointer (const char* utf8Text) noexcept : text (utf8Text) {}
|
||||
|
||||
UTF8Pointer() = default;
|
||||
UTF8Pointer (const UTF8Pointer&) = default;
|
||||
UTF8Pointer& operator= (const UTF8Pointer&) = default;
|
||||
|
||||
/// Returns the raw data that this points to.
|
||||
const char* data() const noexcept { return text; }
|
||||
|
||||
/// Returns true if the pointer is not null.
|
||||
operator bool() const noexcept { return text != nullptr; }
|
||||
|
||||
/// Returns true if the pointer is either null or points to a null terminator char.
|
||||
bool empty() const { return text == nullptr || *text == 0; }
|
||||
|
||||
/// Returns the length by iterating all unicode chars and counting them.
|
||||
/// Note that this is slow, and is not a count of the number of bytes in the string!
|
||||
size_t length() const;
|
||||
|
||||
//==============================================================================
|
||||
/// Returns the first unicode character in the string.
|
||||
UnicodeChar operator*() const;
|
||||
|
||||
/// Skips past the first unicode character.
|
||||
/// Moving beyond the end of the string is undefined behaviour and will trigger an assertion.
|
||||
UTF8Pointer& operator++();
|
||||
|
||||
/// Skips past the first unicode character.
|
||||
/// Moving beyond the end of the string is undefined behaviour and will trigger an assertion.
|
||||
UTF8Pointer operator++ (int);
|
||||
|
||||
/// Moves backwards to the previous unicode character.
|
||||
/// Moving beyond the end of the string is undefined behaviour.
|
||||
UTF8Pointer operator--();
|
||||
|
||||
/// Skips past the given number of unicode characters.
|
||||
/// Moving beyond the end of the string is undefined behaviour and will trigger an assertion.
|
||||
UTF8Pointer& operator+= (size_t numCharsToSkip);
|
||||
|
||||
/// Returns a pointer which points to the n-th unicode character in the text
|
||||
/// Reading beyond the end of the string is undefined behaviour and may trigger an assertion.
|
||||
UTF8Pointer operator+ (size_t numCharsToSkip) const;
|
||||
|
||||
/// Returns a pointer which points to the n-th unicode character in the text.
|
||||
/// Reading beyond the end of the string is undefined behaviour and may trigger an assertion.
|
||||
UTF8Pointer operator+ (int numCharsToSkip) const;
|
||||
|
||||
/// Skips past the first unicode character and returns it as a code-point.
|
||||
/// Calling this when the current character is the terminator will leave the pointer in an
|
||||
/// invalid state.
|
||||
UnicodeChar popFirstChar();
|
||||
|
||||
/// Finds the next occurrence of the given string, or return a nullptr if not found.
|
||||
UTF8Pointer find (const char* textToFind) const;
|
||||
|
||||
/// Returns true if the text starts with this string
|
||||
bool startsWith (const char* textToMatch) const;
|
||||
|
||||
/// If the first character matches the given one, this will advance the pointer and return true.
|
||||
bool skipIfStartsWith (char charToMatch);
|
||||
|
||||
/// If the start of the text matches the given string, this will advance this pointer to skip
|
||||
/// past it, and return true. If not, it will return false without modifying this pointer.
|
||||
bool skipIfStartsWith (const char* textToMatch);
|
||||
|
||||
/// Returns a pointer to the first non-whitespace character in the given string (which may
|
||||
/// be the terminating null character if it's all whitespace).
|
||||
[[nodiscard]] UTF8Pointer findEndOfWhitespace() const;
|
||||
|
||||
/// Iterates backwards from this position to find the first character that follows
|
||||
/// a new-line. The pointer provided marks the furthest back that the function should search
|
||||
[[nodiscard]] UTF8Pointer findStartOfLine (UTF8Pointer startOfValidText) const;
|
||||
|
||||
/// Searches forwards for the next character that is followed by a new-line or a null-terminator.
|
||||
[[nodiscard]] UTF8Pointer findEndOfLine() const;
|
||||
|
||||
//==============================================================================
|
||||
struct EndIterator {};
|
||||
|
||||
struct Iterator
|
||||
{
|
||||
explicit constexpr Iterator (const char* t) : text (t) {}
|
||||
Iterator (const Iterator&) = default;
|
||||
Iterator& operator= (const Iterator&) = default;
|
||||
|
||||
UnicodeChar operator*() const { return *UTF8Pointer (text); }
|
||||
Iterator& operator++() { UTF8Pointer p (text); ++p; text = p.text; return *this; }
|
||||
Iterator operator++ (int) { auto old = *this; ++*this; return old; }
|
||||
bool operator== (EndIterator) const { return *text == 0; }
|
||||
bool operator!= (EndIterator) const { return *text != 0; }
|
||||
|
||||
private:
|
||||
const char* text;
|
||||
};
|
||||
|
||||
Iterator begin() const;
|
||||
EndIterator end() const;
|
||||
|
||||
//==============================================================================
|
||||
/// This does a pointer comparison, NOT a comparison of the text itself!
|
||||
bool operator== (UTF8Pointer other) const noexcept { return text == other.text; }
|
||||
/// This does a pointer comparison, NOT a comparison of the text itself!
|
||||
bool operator!= (UTF8Pointer other) const noexcept { return text != other.text; }
|
||||
/// This does a pointer comparison, NOT a comparison of the text itself!
|
||||
bool operator< (UTF8Pointer other) const noexcept { return text < other.text; }
|
||||
/// This does a pointer comparison, NOT a comparison of the text itself!
|
||||
bool operator> (UTF8Pointer other) const noexcept { return text > other.text; }
|
||||
/// This does a pointer comparison, NOT a comparison of the text itself!
|
||||
bool operator<= (UTF8Pointer other) const noexcept { return text <= other.text; }
|
||||
/// This does a pointer comparison, NOT a comparison of the text itself!
|
||||
bool operator>= (UTF8Pointer other) const noexcept { return text >= other.text; }
|
||||
|
||||
bool operator== (decltype(nullptr)) const noexcept { return text == nullptr; }
|
||||
bool operator!= (decltype(nullptr)) const noexcept { return text != nullptr; }
|
||||
|
||||
private:
|
||||
const char* text = nullptr;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/// Checks a given chunk of data to see whether it's valid UTF-8.
|
||||
/// If no errors are found, this returns nullptr. If an error is found, it returns the address
|
||||
/// of the offending byte. Note that zero bytes in the data are considered to be valid UTF-8.
|
||||
const char* findInvalidUTF8Data (const void* dataToCheck, size_t numBytesToRead);
|
||||
|
||||
/// Writes the bytes for a unicode character, and returns the number of bytes that were needed.
|
||||
/// The buffer passed in needs to have at least 4 bytes capacity.
|
||||
uint32_t convertUnicodeCodepointToUTF8 (char* dest, UnicodeChar codepoint);
|
||||
|
||||
/// Appends a unicode codepoint to a std::string as a sequence of UTF-8 bytes.
|
||||
void appendUTF8 (std::string& target, UnicodeChar codepoint);
|
||||
|
||||
/// Checks whether a given codepoint is a high-surrogate
|
||||
bool isUnicodeHighSurrogate (UnicodeChar codepoint);
|
||||
|
||||
/// Checks whether a given codepoint is a low-surrogate
|
||||
bool isUnicodeLowSurrogate (UnicodeChar codepoint);
|
||||
|
||||
struct SurrogatePair
|
||||
{
|
||||
UnicodeChar high = 0, low = 0;
|
||||
};
|
||||
|
||||
/// For a codepoint >= 0x10000, this will return a surrogate pair to represent it.
|
||||
SurrogatePair splitCodePointIntoSurrogatePair (UnicodeChar fullCodePoint);
|
||||
|
||||
/// Combines a high and low surrogate into a single codepoint.
|
||||
UnicodeChar createUnicodeFromHighAndLowSurrogates (SurrogatePair);
|
||||
|
||||
/// Checks a UTF-8/CESU-8 string to see if it contains any surrogate pairs.
|
||||
/// If it does, then to use it as UTF-8 you'll probably need to run it through
|
||||
/// convertSurrogatePairsToUTF8().
|
||||
bool containsSurrogatePairs (UTF8Pointer);
|
||||
|
||||
/// Returns a string where any surrogate pairs have been converted to UTF-8 codepoints.
|
||||
std::string convertSurrogatePairsToUTF8 (UTF8Pointer);
|
||||
|
||||
/// Returns true if the given UTF-8 string can be used as CESU-8 without conversion. If not,
|
||||
/// you'll need to run it through convertUTF8ToCESU8() to convert the 32-bit code-points
|
||||
/// to surrogate pairs.
|
||||
bool isValidCESU8 (std::string_view utf8);
|
||||
|
||||
/// Converts any 32-bit characters in this UTF-8 string to surrogate pairs, which makes
|
||||
/// the resulting string suitable for use at CESU-8.
|
||||
[[nodiscard]] std::string convertUTF8ToCESU8 (UTF8Pointer);
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/// Represents a line and column index within a block of text.
|
||||
struct LineAndColumn
|
||||
{
|
||||
/// Valid line and column values start at 1.
|
||||
/// If either is 0, it means that the LineAndColumn object is uninitialised.
|
||||
size_t line = 0, column = 0;
|
||||
|
||||
/// Returns true if neither the line nor column is zero.
|
||||
bool isValid() const noexcept { return line != 0 && column != 0; }
|
||||
|
||||
/// Turns this location into a [line]:[col] string suitable for use in a
|
||||
/// standard compiler error message format.
|
||||
std::string toString() const;
|
||||
};
|
||||
|
||||
/// Given a block of text and a position within it, this will work out the
|
||||
/// line and column of that position.
|
||||
LineAndColumn findLineAndColumn (UTF8Pointer fullText,
|
||||
UTF8Pointer targetPosition);
|
||||
|
||||
|
||||
//==============================================================================
|
||||
// _ _ _ _
|
||||
// __| | ___ | |_ __ _ (_)| | ___
|
||||
// / _` | / _ \| __| / _` || || |/ __|
|
||||
// | (_| || __/| |_ | (_| || || |\__ \ _ _ _
|
||||
// \__,_| \___| \__| \__,_||_||_||___/(_)(_)(_)
|
||||
//
|
||||
// Code beyond this point is implementation detail...
|
||||
//
|
||||
//==============================================================================
|
||||
|
||||
inline size_t UTF8Pointer::length() const
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
if (text != nullptr)
|
||||
for (auto p = *this; *p.text != 0; ++p)
|
||||
++count;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
inline const char* findInvalidUTF8Data (const void* dataToCheck, size_t numBytes)
|
||||
{
|
||||
CHOC_ASSERT (dataToCheck != nullptr);
|
||||
auto source = static_cast<const char*> (dataToCheck);
|
||||
const auto end = source + numBytes;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (source >= end)
|
||||
return nullptr;
|
||||
|
||||
auto byte = static_cast<signed char> (*source);
|
||||
|
||||
if (byte >= 0)
|
||||
{
|
||||
++source;
|
||||
continue;
|
||||
}
|
||||
|
||||
int testBit = 0x40, numExtraBytes = 0;
|
||||
|
||||
while ((byte & testBit) != 0)
|
||||
{
|
||||
testBit >>= 1;
|
||||
++numExtraBytes;
|
||||
|
||||
if (numExtraBytes > 3
|
||||
|| source + static_cast<size_t> (numExtraBytes) >= end
|
||||
|| (numExtraBytes == 3 && *UTF8Pointer (source) > 0x10ffff))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
if (numExtraBytes == 0)
|
||||
return source;
|
||||
|
||||
++source;
|
||||
|
||||
for (int i = 0; i < numExtraBytes; ++i)
|
||||
{
|
||||
if ((*source & 0xc0) != 0x80)
|
||||
return source;
|
||||
|
||||
++source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline UnicodeChar UTF8Pointer::operator*() const
|
||||
{
|
||||
return UTF8Pointer (*this).popFirstChar();
|
||||
}
|
||||
|
||||
inline UTF8Pointer& UTF8Pointer::operator++()
|
||||
{
|
||||
CHOC_ASSERT (! empty()); // can't advance past the zero-terminator
|
||||
auto firstByte = static_cast<signed char> (*text++);
|
||||
|
||||
if (firstByte >= 0)
|
||||
return *this;
|
||||
|
||||
uint32_t testBit = 0x40, unicodeChar = static_cast<unsigned char> (firstByte);
|
||||
|
||||
while ((unicodeChar & testBit) != 0 && testBit > 8)
|
||||
{
|
||||
++text;
|
||||
testBit >>= 1;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::operator++ (int)
|
||||
{
|
||||
auto prev = *this;
|
||||
operator++();
|
||||
return prev;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::operator--()
|
||||
{
|
||||
CHOC_ASSERT (text != nullptr); // mustn't use this on nullptrs
|
||||
uint32_t bytesSkipped = 0;
|
||||
|
||||
while ((*--text & 0xc0) == 0x80)
|
||||
{
|
||||
if (bytesSkipped > 2)
|
||||
{
|
||||
CHOC_ASSERT (bytesSkipped <= 2);
|
||||
break;
|
||||
}
|
||||
|
||||
++bytesSkipped;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline UTF8Pointer& UTF8Pointer::operator+= (size_t numCharsToSkip)
|
||||
{
|
||||
while (numCharsToSkip != 0)
|
||||
{
|
||||
--numCharsToSkip;
|
||||
operator++();
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::operator+ (size_t numCharsToSkip) const
|
||||
{
|
||||
auto p = *this;
|
||||
p += numCharsToSkip;
|
||||
return p;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::operator+ (int numCharsToSkip) const
|
||||
{
|
||||
CHOC_ASSERT (numCharsToSkip >= 0);
|
||||
return operator+ (static_cast<size_t> (numCharsToSkip));
|
||||
}
|
||||
|
||||
inline UnicodeChar UTF8Pointer::popFirstChar()
|
||||
{
|
||||
CHOC_ASSERT (text != nullptr); // mustn't use this on nullptrs
|
||||
auto firstByte = static_cast<signed char> (*text++);
|
||||
UnicodeChar unicodeChar = static_cast<unsigned char> (firstByte);
|
||||
|
||||
if (firstByte < 0)
|
||||
{
|
||||
uint32_t bitMask = 0x7f, numExtraBytes = 0;
|
||||
|
||||
for (uint32_t testBit = 0x40; (unicodeChar & testBit) != 0 && testBit > 8; ++numExtraBytes)
|
||||
{
|
||||
bitMask >>= 1;
|
||||
testBit >>= 1;
|
||||
}
|
||||
|
||||
unicodeChar &= bitMask;
|
||||
|
||||
for (uint32_t i = 0; i < numExtraBytes; ++i)
|
||||
{
|
||||
uint32_t nextByte = static_cast<unsigned char> (*text);
|
||||
|
||||
CHOC_ASSERT ((nextByte & 0xc0) == 0x80); // error in the data - you should always make sure the source
|
||||
// gets validated before iterating a UTF8Pointer over it
|
||||
|
||||
unicodeChar = (unicodeChar << 6) | (nextByte & 0x3f);
|
||||
++text;
|
||||
}
|
||||
}
|
||||
|
||||
return unicodeChar;
|
||||
}
|
||||
|
||||
inline bool UTF8Pointer::startsWith (const char* textToMatch) const
|
||||
{
|
||||
CHOC_ASSERT (textToMatch != nullptr);
|
||||
|
||||
if (auto p = text)
|
||||
{
|
||||
while (*textToMatch != 0)
|
||||
if (*textToMatch++ != *p++)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::find (const char* textToFind) const
|
||||
{
|
||||
CHOC_ASSERT (textToFind != nullptr);
|
||||
|
||||
for (auto t = *this;; ++t)
|
||||
if (t.startsWith (textToFind) || t.empty())
|
||||
return t;
|
||||
}
|
||||
|
||||
inline bool UTF8Pointer::skipIfStartsWith (char charToMatch)
|
||||
{
|
||||
if (text != nullptr && *text == charToMatch && charToMatch != 0)
|
||||
{
|
||||
++text;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool UTF8Pointer::skipIfStartsWith (const char* textToMatch)
|
||||
{
|
||||
CHOC_ASSERT (textToMatch != nullptr);
|
||||
|
||||
if (auto p = text)
|
||||
{
|
||||
while (*textToMatch != 0)
|
||||
if (*textToMatch++ != *p++)
|
||||
return false;
|
||||
|
||||
text = p;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::findEndOfWhitespace() const
|
||||
{
|
||||
auto p = *this;
|
||||
|
||||
if (p.text != nullptr)
|
||||
while (choc::text::isWhitespace (*p.text))
|
||||
++p;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::findStartOfLine (UTF8Pointer start) const
|
||||
{
|
||||
if (text == nullptr)
|
||||
return {};
|
||||
|
||||
auto l = *this;
|
||||
CHOC_ASSERT (l.text >= start.text && start.text != nullptr);
|
||||
|
||||
while (l.text > start.text)
|
||||
{
|
||||
auto prev = l;
|
||||
auto c = *--prev;
|
||||
|
||||
if (c == '\r' || c == '\n')
|
||||
break;
|
||||
|
||||
l = prev;
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
inline UTF8Pointer UTF8Pointer::findEndOfLine() const
|
||||
{
|
||||
if (text == nullptr)
|
||||
return {};
|
||||
|
||||
auto l = *this;
|
||||
|
||||
while (! l.empty())
|
||||
{
|
||||
auto c = l.popFirstChar();
|
||||
|
||||
if (c == '\r' || c == '\n')
|
||||
break;
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
inline UTF8Pointer::Iterator UTF8Pointer::begin() const { CHOC_ASSERT (text != nullptr); return Iterator (text); }
|
||||
inline UTF8Pointer::EndIterator UTF8Pointer::end() const { return EndIterator(); }
|
||||
|
||||
inline LineAndColumn findLineAndColumn (UTF8Pointer start, UTF8Pointer targetPosition)
|
||||
{
|
||||
if (start == nullptr || targetPosition == nullptr)
|
||||
return {};
|
||||
|
||||
CHOC_ASSERT (start <= targetPosition);
|
||||
LineAndColumn lc { 1, 1 };
|
||||
|
||||
while (start < targetPosition && ! start.empty())
|
||||
{
|
||||
++lc.column;
|
||||
if (*start++ == '\n') { lc.line++; lc.column = 1; }
|
||||
}
|
||||
|
||||
return lc;
|
||||
}
|
||||
|
||||
inline std::string LineAndColumn::toString() const { return std::to_string (line) + ':' + std::to_string (column); }
|
||||
|
||||
//==============================================================================
|
||||
inline uint32_t convertUnicodeCodepointToUTF8 (char* dest, UnicodeChar unicodeChar)
|
||||
{
|
||||
if (unicodeChar < 0x80)
|
||||
{
|
||||
*dest = static_cast<char> (unicodeChar);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t extraBytes = 1;
|
||||
|
||||
if (unicodeChar >= 0x800)
|
||||
{
|
||||
++extraBytes;
|
||||
|
||||
if (unicodeChar >= 0x10000)
|
||||
++extraBytes;
|
||||
}
|
||||
|
||||
dest[0] = static_cast<char> ((0xffu << (7 - extraBytes)) | (unicodeChar >> (extraBytes * 6)));
|
||||
|
||||
for (uint32_t i = 1; i <= extraBytes; ++i)
|
||||
dest[i] = static_cast<char> (0x80u | (0x3fu & (unicodeChar >> ((extraBytes - i) * 6))));
|
||||
|
||||
return extraBytes + 1;
|
||||
}
|
||||
|
||||
inline void appendUTF8 (std::string& target, UnicodeChar unicodeChar)
|
||||
{
|
||||
char bytes[4];
|
||||
auto num = convertUnicodeCodepointToUTF8 (bytes, unicodeChar);
|
||||
target.append (bytes, num);
|
||||
}
|
||||
|
||||
inline bool isUnicodeHighSurrogate (UnicodeChar codepoint) { return codepoint >= 0xd800 && codepoint <= 0xdbff; }
|
||||
inline bool isUnicodeLowSurrogate (UnicodeChar codepoint) { return codepoint >= 0xdc00 && codepoint <= 0xdfff; }
|
||||
|
||||
inline UnicodeChar createUnicodeFromHighAndLowSurrogates (SurrogatePair pair)
|
||||
{
|
||||
if (! isUnicodeHighSurrogate (pair.high)) return pair.high;
|
||||
if (! isUnicodeLowSurrogate (pair.low)) return 0;
|
||||
|
||||
return (pair.high << 10) + pair.low - 0x35fdc00u;
|
||||
}
|
||||
|
||||
inline bool containsSurrogatePairs (UTF8Pointer text)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
auto c = text.popFirstChar();
|
||||
|
||||
if (c == 0)
|
||||
return false;
|
||||
|
||||
if (isUnicodeHighSurrogate (c))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string convertSurrogatePairsToUTF8 (UTF8Pointer text)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto c = text.popFirstChar();
|
||||
|
||||
if (choc::text::isUnicodeHighSurrogate (c))
|
||||
c = createUnicodeFromHighAndLowSurrogates ({ c, text.popFirstChar() });
|
||||
|
||||
if (c == 0)
|
||||
return result;
|
||||
|
||||
appendUTF8 (result, c);
|
||||
}
|
||||
}
|
||||
|
||||
inline SurrogatePair splitCodePointIntoSurrogatePair (UnicodeChar fullCodePoint)
|
||||
{
|
||||
CHOC_ASSERT (fullCodePoint >= 0x10000);
|
||||
|
||||
return { static_cast<UnicodeChar> (0xd800u + ((fullCodePoint - 0x10000u) >> 10)),
|
||||
static_cast<UnicodeChar> (0xdc00u + (fullCodePoint & 0x3ffu)) };
|
||||
}
|
||||
|
||||
inline bool isValidCESU8 (std::string_view utf8)
|
||||
{
|
||||
for (auto c : utf8)
|
||||
if (static_cast<uint8_t> (c) >= 0xe8)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string convertUTF8ToCESU8 (UTF8Pointer utf8)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto c = utf8.popFirstChar();
|
||||
|
||||
if (c == 0)
|
||||
return result;
|
||||
|
||||
if (c < 128)
|
||||
{
|
||||
result += (char) c;
|
||||
}
|
||||
else if (c >= 0x10000)
|
||||
{
|
||||
auto pair = splitCodePointIntoSurrogatePair (c);
|
||||
appendUTF8 (result, pair.high);
|
||||
appendUTF8 (result, pair.low);
|
||||
}
|
||||
else
|
||||
{
|
||||
appendUTF8 (result, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace choc::text
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,626 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
// On Linux int64 and int64_t don't resolve to the same type and don't have C style casts between
|
||||
// each other, hence we need a two-step conversion.
|
||||
template <typename T>
|
||||
static int64_t fromJuceInt64 (const T& convertible) { return (int64_t) (int64) convertible; }
|
||||
|
||||
template <typename T>
|
||||
static int64_t toJuceInt64 (const T& convertible) { return (int64) (int64_t) convertible; }
|
||||
|
||||
//==============================================================================
|
||||
namespace qjs = choc::javascript::quickjs;
|
||||
|
||||
using VarOrError = std::variant<var, String>;
|
||||
|
||||
static var discardError (VarOrError variant)
|
||||
{
|
||||
const auto* v = std::get_if<var> (&variant);
|
||||
return v != nullptr ? *v : var::undefined();
|
||||
}
|
||||
|
||||
static VarOrError quickJSToJuce (const qjs::QuickJSContext::ValuePtr& ptr);
|
||||
|
||||
static std::vector<var> quickJSToJuce (Span<qjs::JSValueConst> args, qjs::JSContext* ctx)
|
||||
{
|
||||
std::vector<var> argList;
|
||||
argList.reserve (args.size());
|
||||
|
||||
for (const auto& arg : args)
|
||||
argList.push_back (discardError (quickJSToJuce ({ qjs::JS_DupValue (ctx, arg), ctx })));
|
||||
|
||||
return argList;
|
||||
}
|
||||
|
||||
static qjs::JSValue juceToQuickJs (const var& v, qjs::JSContext* ctx)
|
||||
{
|
||||
using namespace qjs;
|
||||
|
||||
if (v.isVoid())
|
||||
return JS_NULL;
|
||||
|
||||
if (v.isUndefined())
|
||||
return JS_UNDEFINED;
|
||||
|
||||
if (v.isInt())
|
||||
return JS_NewInt32 (ctx, static_cast<int> (v));
|
||||
|
||||
if (v.isInt64())
|
||||
return JS_NewInt64 (ctx, static_cast<int64> (v));
|
||||
|
||||
if (v.isDouble())
|
||||
return JS_NewFloat64 (ctx, static_cast<double> (v));
|
||||
|
||||
if (v.isBool())
|
||||
return JS_NewBool (ctx, static_cast<bool> (v));
|
||||
|
||||
if (v.isString())
|
||||
{
|
||||
const String x = v;
|
||||
return JS_NewStringLen (ctx, x.toRawUTF8(), x.getNumBytesAsUTF8());
|
||||
}
|
||||
|
||||
if (auto fn = v.getNativeFunction())
|
||||
{
|
||||
using Fn = var::NativeFunction;
|
||||
static constexpr auto size = sizeof (fn);
|
||||
|
||||
const auto cb = [] (JSContext* localContext,
|
||||
JSValueConst thisVal,
|
||||
int argc,
|
||||
JSValueConst* argv,
|
||||
int,
|
||||
JSValue* funcData) -> JSValue
|
||||
{
|
||||
if (funcData == nullptr)
|
||||
{
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t bufferSize{};
|
||||
void* buffer = qjs::JS_GetArrayBuffer (localContext, &bufferSize, *funcData);
|
||||
|
||||
if (buffer == nullptr || bufferSize != size)
|
||||
{
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto thisConverted = discardError (quickJSToJuce ({ qjs::JS_DupValue (localContext, thisVal), localContext }));
|
||||
const auto argsConverted = quickJSToJuce ({ argv, (size_t) argc }, localContext);
|
||||
const var::NativeFunctionArgs args { thisConverted, argsConverted.data(), (int) argsConverted.size() };
|
||||
|
||||
const auto resultVar = (*static_cast<Fn*> (buffer)) (args);
|
||||
|
||||
return juceToQuickJs (resultVar, localContext);
|
||||
};
|
||||
|
||||
const auto free = [] (JSRuntime*, void*, void* buffer)
|
||||
{
|
||||
auto* localFn = static_cast<Fn*> (buffer);
|
||||
localFn->~Fn();
|
||||
delete[] static_cast<uint8_t*> (buffer);
|
||||
};
|
||||
|
||||
std::unique_ptr<uint8_t[]> storage { new uint8_t[size] };
|
||||
new (storage.get()) Fn { std::move (fn) };
|
||||
|
||||
qjs::QuickJSContext::ValuePtr callbackAsData { qjs::JS_NewArrayBuffer (ctx,
|
||||
storage.release(),
|
||||
size,
|
||||
free,
|
||||
nullptr,
|
||||
false),
|
||||
ctx };
|
||||
return JS_NewCFunctionData (ctx, cb, 0, 0, 1, &callbackAsData.value);
|
||||
}
|
||||
|
||||
if (auto* array = v.getArray())
|
||||
{
|
||||
auto result = JS_NewArray (ctx);
|
||||
|
||||
for (const auto [index, value] : enumerate (*array, uint32_t{}))
|
||||
JS_SetPropertyUint32 (ctx, result, index, juceToQuickJs (value, ctx));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (auto* obj = v.getDynamicObject())
|
||||
{
|
||||
auto result = JS_NewObject (ctx);
|
||||
|
||||
for (const auto& pair : obj->getProperties())
|
||||
{
|
||||
const auto name = pair.name.toString();
|
||||
JS_SetPropertyStr (ctx, result, name.toRawUTF8(), juceToQuickJs (pair.value, ctx));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
jassertfalse;
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wsubobject-linkage")
|
||||
struct JSFunctionArguments
|
||||
{
|
||||
explicit JSFunctionArguments (qjs::JSContext* contextIn)
|
||||
: context (contextIn)
|
||||
{
|
||||
}
|
||||
|
||||
JSFunctionArguments (qjs::JSContext* contextIn, const var::NativeFunctionArgs& args)
|
||||
: JSFunctionArguments (contextIn, Span { args.arguments, (size_t) args.numArguments })
|
||||
{
|
||||
}
|
||||
|
||||
JSFunctionArguments (qjs::JSContext* contextIn, Span<const var> args)
|
||||
: context (contextIn)
|
||||
{
|
||||
values.reserve (args.size());
|
||||
|
||||
for (const auto& arg : args)
|
||||
values.push_back (juceToQuickJs (arg, context));
|
||||
}
|
||||
|
||||
~JSFunctionArguments()
|
||||
{
|
||||
for (const auto& value : values)
|
||||
qjs::JS_FreeValue (context, value);
|
||||
}
|
||||
|
||||
void add (const var& arg)
|
||||
{
|
||||
values.push_back (juceToQuickJs (arg, context));
|
||||
}
|
||||
|
||||
qjs::JSValue* getArguments()
|
||||
{
|
||||
return values.data();
|
||||
}
|
||||
|
||||
int getSize() const
|
||||
{
|
||||
return (int) values.size();
|
||||
}
|
||||
|
||||
private:
|
||||
qjs::JSContext* context;
|
||||
std::vector<qjs::JSValue> values;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (JSFunctionArguments)
|
||||
JUCE_DECLARE_NON_MOVEABLE (JSFunctionArguments)
|
||||
};
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
//==============================================================================
|
||||
// Throws on failure
|
||||
static var tryQuickJSToJuce (const qjs::QuickJSContext::ValuePtr& ptr,
|
||||
const qjs::JSValue* parent = nullptr)
|
||||
{
|
||||
using namespace qjs;
|
||||
|
||||
jassert (ptr.context != nullptr);
|
||||
|
||||
if (JS_IsUndefined (ptr.value))
|
||||
return var::undefined();
|
||||
|
||||
if (JS_IsNull (ptr.value))
|
||||
return var{};
|
||||
|
||||
if (JS_IsNumber (ptr.value))
|
||||
{
|
||||
double d = 0;
|
||||
JS_ToFloat64 (ptr.context, std::addressof (d), ptr.value);
|
||||
return d;
|
||||
}
|
||||
|
||||
if (JS_IsBool (ptr.value))
|
||||
return JS_ToBool (ptr.context, ptr.value) != 0;
|
||||
|
||||
if (JS_IsString (ptr.value))
|
||||
{
|
||||
size_t len = 0;
|
||||
const auto* s = JS_ToCStringLen2 (ptr.context, std::addressof (len), ptr.value, false);
|
||||
const ScopeGuard scope { [&] { JS_FreeCString (ptr.context, s); } };
|
||||
return String::fromUTF8 (s, (int) len);
|
||||
}
|
||||
|
||||
if (JS_IsArray (ptr.context, ptr.value))
|
||||
{
|
||||
const auto lengthProp = ptr["length"];
|
||||
uint32_t len = 0;
|
||||
JS_ToUint32 (ptr.context, &len, lengthProp.get());
|
||||
|
||||
Array<var> result;
|
||||
result.ensureStorageAllocated ((int) len);
|
||||
|
||||
for (auto i = decltype (len){}; i < len; ++i)
|
||||
result.add (tryQuickJSToJuce (ptr[i], &ptr.value));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (JS_IsFunction (ptr.context, ptr.value))
|
||||
{
|
||||
// ValuePtr is move-only, so can't be captured into a std::function.
|
||||
// Use a custom copyable callable instead.
|
||||
struct Callable
|
||||
{
|
||||
Callable (JSContext* ctxIn, JSValue fnIn, JSValue selfIn)
|
||||
: ctx (ctxIn),
|
||||
fn (JS_DupValue (ctx, fnIn)),
|
||||
self (JS_DupValue (ctx, selfIn))
|
||||
{
|
||||
}
|
||||
|
||||
Callable (const Callable& other)
|
||||
: ctx (other.ctx),
|
||||
fn (JS_DupValue (ctx, other.fn)),
|
||||
self (JS_DupValue (ctx, other.self))
|
||||
{
|
||||
}
|
||||
|
||||
Callable& operator= (const Callable& other)
|
||||
{
|
||||
Callable { other }.swap (*this);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Callable()
|
||||
{
|
||||
JS_FreeValue (ctx, fn);
|
||||
JS_FreeValue (ctx, self);
|
||||
}
|
||||
|
||||
void swap (Callable& other) noexcept
|
||||
{
|
||||
std::swap (other.ctx, ctx);
|
||||
std::swap (other.fn, fn);
|
||||
std::swap (other.self, self);
|
||||
}
|
||||
|
||||
var operator() (const var::NativeFunctionArgs& args) const
|
||||
{
|
||||
JSFunctionArguments convertedArgs { ctx, args };
|
||||
|
||||
const qjs::QuickJSContext::ValuePtr result { qjs::JS_Call (ctx,
|
||||
fn,
|
||||
self,
|
||||
(int) convertedArgs.getSize(),
|
||||
convertedArgs.getArguments()),
|
||||
ctx };
|
||||
|
||||
return discardError (quickJSToJuce (result));
|
||||
}
|
||||
|
||||
JSContext* ctx{};
|
||||
JSValue fn, self;
|
||||
};
|
||||
|
||||
const qjs::QuickJSContext::ValuePtr parentToUse { parent != nullptr ? JS_DupValue (ptr.context, *parent)
|
||||
: JS_GetGlobalObject (ptr.context),
|
||||
ptr.context };
|
||||
|
||||
return var::NativeFunction { Callable { ptr.context, ptr.value, parentToUse.value } };
|
||||
}
|
||||
|
||||
if (JS_IsObject (ptr.value))
|
||||
{
|
||||
std::vector<std::string> propNames;
|
||||
|
||||
for (auto obj = ptr.takeValue (JS_DupValue (ptr.context, ptr.value));;)
|
||||
{
|
||||
JSPropertyEnum* properties = nullptr;
|
||||
uint32_t numProps = 0;
|
||||
|
||||
if (JS_GetOwnPropertyNames (ptr.context, &properties, &numProps, obj.get(), JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) != 0
|
||||
|| properties == nullptr)
|
||||
{
|
||||
return new DynamicObject;
|
||||
}
|
||||
|
||||
const ScopeGuard scope { [&] { js_free (ptr.context, properties); } };
|
||||
|
||||
propNames.reserve (numProps);
|
||||
|
||||
for (uint32_t i = 0; i < numProps; ++i)
|
||||
{
|
||||
const auto* name = JS_AtomToCString (ptr.context, properties[i].atom);
|
||||
std::string nameString (name);
|
||||
|
||||
if (nameString != QuickJSContext::objectNameAttribute)
|
||||
propNames.push_back (std::move (nameString));
|
||||
|
||||
JS_FreeCString (ptr.context, name);
|
||||
JS_FreeAtom (ptr.context, properties[i].atom);
|
||||
}
|
||||
|
||||
auto proto = ptr.takeValue (JS_GetPrototype (ptr.context, obj.get()));
|
||||
|
||||
if (! JS_IsObject (proto.get()))
|
||||
break;
|
||||
|
||||
obj = std::move (proto);
|
||||
}
|
||||
|
||||
DynamicObject::Ptr result = new DynamicObject;
|
||||
|
||||
for (auto& propName : propNames)
|
||||
result->setProperty (String (propName), tryQuickJSToJuce (ptr[propName.c_str()], &ptr.value));
|
||||
|
||||
return result.get();
|
||||
}
|
||||
|
||||
ptr.throwIfError();
|
||||
return {};
|
||||
}
|
||||
|
||||
static VarOrError quickJSToJuce (const qjs::QuickJSContext::ValuePtr& ptr)
|
||||
{
|
||||
try
|
||||
{
|
||||
return tryQuickJSToJuce (ptr);
|
||||
}
|
||||
catch (const choc::javascript::Error& error)
|
||||
{
|
||||
return String (error.what());
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Any type that references the QuickJS types inside the anonymous namespace added by us requires
|
||||
// this with GCC. Suppressing this warning is fine, since these classes are only visible and used
|
||||
// in a single translation unit.
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wsubobject-linkage")
|
||||
class QuickJSWrapper
|
||||
{
|
||||
public:
|
||||
explicit QuickJSWrapper (const RelativeTime* maximumExecutionTimeIn)
|
||||
: maximumExecutionTime { *maximumExecutionTimeIn }
|
||||
{
|
||||
qjs::JS_SetInterruptHandler (getQuickJSRuntime(), handleInterrupt, (void*) this);
|
||||
}
|
||||
|
||||
qjs::JSContext* getQuickJSContext() const
|
||||
{
|
||||
return impl->context;
|
||||
}
|
||||
|
||||
qjs::JSRuntime* getQuickJSRuntime() const
|
||||
{
|
||||
return impl->runtime;
|
||||
}
|
||||
|
||||
void resetTimeout()
|
||||
{
|
||||
timeout = (int64) Time::getMillisecondCounterHiRes() + maximumExecutionTime.inMilliseconds();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
timeout = (int64) Time::getMillisecondCounterHiRes();
|
||||
}
|
||||
|
||||
private:
|
||||
static int handleInterrupt (qjs::JSRuntime*, void* opaque)
|
||||
{
|
||||
auto& self = *static_cast<QuickJSWrapper*> (opaque);
|
||||
return (int64) Time::getMillisecondCounterHiRes() >= self.timeout;
|
||||
}
|
||||
|
||||
std::unique_ptr<qjs::QuickJSContext> impl = std::make_unique<qjs::QuickJSContext>();
|
||||
const RelativeTime& maximumExecutionTime;
|
||||
std::atomic<int64> timeout{};
|
||||
};
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
using SetterFn = qjs::JSValue (*) (qjs::JSContext* ctx,
|
||||
qjs::JSValueConst thisVal,
|
||||
qjs::JSValueConst val,
|
||||
int magic);
|
||||
using GetterFn = qjs::JSValue (*) (qjs::JSContext* ctx, qjs::JSValueConst thisVal, int magic);
|
||||
|
||||
// A replacement for the JS_CGETSET_MAGIC_DEF macro in QuickJS
|
||||
static qjs::JSCFunctionListEntry makeFunctionListEntry (const char* name,
|
||||
GetterFn getter,
|
||||
SetterFn setter,
|
||||
int16_t magic)
|
||||
{
|
||||
qjs::JSCFunctionListEntry e { name, JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE, qjs::JS_DEF_CGETSET_MAGIC, magic, {} };
|
||||
e.u.getset.get.getter_magic = getter;
|
||||
e.u.getset.set.setter_magic = setter;
|
||||
return e;
|
||||
}
|
||||
|
||||
// A replacement for the JS_UNDEFINED macro in QuickJS
|
||||
static qjs::JSValue makeUndefined()
|
||||
{
|
||||
#if defined(JS_NAN_BOXING) // Differentiates between 32 and 64 bit builds
|
||||
return (((uint64_t)(qjs::JS_TAG_UNDEFINED) << 32) | (uint32_t)(0));
|
||||
#else
|
||||
return qjs::JSValue (static_cast<int32_t> (0), qjs::JS_TAG_UNDEFINED);
|
||||
#endif
|
||||
}
|
||||
|
||||
static qjs::JSClassID createClassId()
|
||||
{
|
||||
// A passed in value of 0 asks QuickJS to allocate us a new unique ID. QuickJS uses global
|
||||
// variables for the bookkeeping, so it's safe to use this function to initialise globals.
|
||||
qjs::JSClassID newId = 0;
|
||||
return qjs::JS_NewClassID (&newId);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/* Attached as an opaque pointer to the corresponding JS object. Its lifetime is managed by the
|
||||
QuickJS engine, which calls the finalise function when the corresponding JSValue is deleted.
|
||||
*/
|
||||
struct DynamicObjectWrapper
|
||||
{
|
||||
DynamicObjectWrapper (detail::QuickJSWrapper& engineIn, DynamicObject::Ptr objectIn)
|
||||
: engine (engineIn), object (objectIn)
|
||||
{
|
||||
getDynamicObjects().insert (this);
|
||||
}
|
||||
|
||||
int16_t getOrdinal (const Identifier& identifier)
|
||||
{
|
||||
if (const auto& it = ordinals.find (identifier); it != ordinals.end())
|
||||
return it->second;
|
||||
|
||||
identifiers.emplace_back (identifier);
|
||||
const auto newSize = identifiers.size() - 1;
|
||||
jassert (newSize <= (size_t) std::numeric_limits<int16_t>::max());
|
||||
const auto newOrdinal = (int16_t) newSize;
|
||||
ordinals[identifier] = newOrdinal;
|
||||
return newOrdinal;
|
||||
}
|
||||
|
||||
auto getIdentifier (int ordinal) const
|
||||
{
|
||||
jassert ((size_t) ordinal < identifiers.size());
|
||||
return identifiers[(size_t) ordinal];
|
||||
}
|
||||
|
||||
NamedValueSet& getProperties() const
|
||||
{
|
||||
return object->getProperties();
|
||||
}
|
||||
|
||||
static void finaliser (qjs::JSRuntime*, qjs::JSValue val)
|
||||
{
|
||||
auto* wrapper = static_cast<DynamicObjectWrapper*> (qjs::JS_GetOpaque (val, getClassId()));
|
||||
wrapper->finalise();
|
||||
}
|
||||
|
||||
void finalise()
|
||||
{
|
||||
getDynamicObjects().erase (this);
|
||||
delete this;
|
||||
}
|
||||
|
||||
static void createClass (qjs::JSRuntime* runtime)
|
||||
{
|
||||
qjs::JSClassDef classDef {};
|
||||
classDef.class_name = "juce_DynamicObject";
|
||||
classDef.finalizer = finaliser;
|
||||
qjs::JS_NewClass (runtime, getClassId(), &classDef);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static qjs::JSValue callDispatcher (qjs::JSContext* ctx,
|
||||
qjs::JSValueConst thisValue,
|
||||
int numArgs,
|
||||
qjs::JSValueConst* args,
|
||||
int ordinal)
|
||||
{
|
||||
auto& self = *static_cast<DynamicObjectWrapper*> (qjs::JS_GetOpaque2 (ctx, thisValue, getClassId()));
|
||||
const auto argList = quickJSToJuce (Span { args, (size_t) numArgs }, ctx);
|
||||
const auto identifier = self.getIdentifier (ordinal);
|
||||
auto result = self.object->invokeMethod (identifier,
|
||||
{ self.object.get(), argList.data(), (int) argList.size() });
|
||||
return juceToQuickJs (result, ctx);
|
||||
}
|
||||
|
||||
static qjs::JSValue setDispatcher (qjs::JSContext* ctx,
|
||||
qjs::JSValueConst thisVal,
|
||||
qjs::JSValueConst val,
|
||||
int ordinal)
|
||||
{
|
||||
auto& self = *static_cast<DynamicObjectWrapper*> (qjs::JS_GetOpaque2 (ctx, thisVal, getClassId()));
|
||||
self.object->setProperty (self.getIdentifier (ordinal), discardError (quickJSToJuce ({ qjs::JS_DupValue (ctx, val), ctx })));
|
||||
|
||||
// In case there is a problem we could return e.g. `JS_EXCEPTION` or
|
||||
// `JS_ThrowRangeError(ctx, "invalid precision");` here.
|
||||
return makeUndefined();
|
||||
}
|
||||
|
||||
static qjs::JSValue getDispatcher (qjs::JSContext* ctx, qjs::JSValueConst thisVal, int ordinal)
|
||||
{
|
||||
auto& self = *static_cast<DynamicObjectWrapper*> (qjs::JS_GetOpaque2 (ctx, thisVal, getClassId()));
|
||||
return juceToQuickJs (self.object->getProperty (self.getIdentifier (ordinal)), ctx);
|
||||
}
|
||||
|
||||
static qjs::JSClassID getClassId()
|
||||
{
|
||||
static qjs::JSClassID classId = createClassId();
|
||||
return classId;
|
||||
}
|
||||
|
||||
static std::set<void*>& getDynamicObjects()
|
||||
{
|
||||
// Used to check if an opaque ptr attached to a JSValue is a DynamicObjectWrapper
|
||||
static std::set<void*> dynamicObjects;
|
||||
return dynamicObjects;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
QuickJSWrapper& engine;
|
||||
DynamicObject::Ptr object;
|
||||
std::map<Identifier, int16_t> ordinals;
|
||||
std::vector<Identifier> identifiers;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
static bool hasProperty (qjs::JSContext* ctx, qjs::JSValueConst object, const char* name)
|
||||
{
|
||||
qjs::JSAtom atom = JS_NewAtom (ctx, name);
|
||||
ScopeGuard freeAtom { [&] { qjs::JS_FreeAtom (ctx, atom); } };
|
||||
|
||||
return JS_HasProperty (ctx, object, atom) > 0;
|
||||
}
|
||||
|
||||
static qjs::JSValue getOrCreateProperty (qjs::JSContext* ctx, qjs::JSValueConst object, const char* name)
|
||||
{
|
||||
if (! hasProperty (ctx, object, name))
|
||||
qjs::JS_SetPropertyStr (ctx, object, name, JS_NewObject (ctx));
|
||||
|
||||
return qjs::JS_GetPropertyStr (ctx, object, name);
|
||||
}
|
||||
|
||||
static uint32_t toUint32 (int64 value)
|
||||
{
|
||||
jassert (0 <= value && value <= (int64) std::numeric_limits<uint32_t>::max());
|
||||
return (uint32_t) value;
|
||||
}
|
||||
|
||||
} // namespace juce::detail
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
JSCursor::JSCursor (JSObject rootIn) : root (std::move (rootIn))
|
||||
{
|
||||
}
|
||||
|
||||
var JSCursor::get() const
|
||||
{
|
||||
if (const auto resolved = getFullResolution())
|
||||
return resolved->get();
|
||||
|
||||
return var::undefined();
|
||||
}
|
||||
|
||||
void JSCursor::set (const var& value) const
|
||||
{
|
||||
const auto resolved = getPartialResolution();
|
||||
|
||||
if (! resolved.has_value())
|
||||
{
|
||||
jassertfalse; // Can't resolve an Object to change along the path stored in the cursor
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& [object, property] = *resolved;
|
||||
|
||||
if (! property.has_value())
|
||||
{
|
||||
jassertfalse; // Can't set the value of the root Object
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* prop = std::get_if<Identifier> (&(*property)))
|
||||
{
|
||||
object.setProperty (*prop, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* prop = std::get_if<int64> (&(*property)))
|
||||
{
|
||||
object.setProperty (*prop, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JSCursor JSCursor::getChild (const Identifier& name) const
|
||||
{
|
||||
auto copy = *this;
|
||||
copy.path.emplace_back (name);
|
||||
return copy;
|
||||
}
|
||||
|
||||
JSCursor JSCursor::operator[] (const Identifier& name) const
|
||||
{
|
||||
return getChild (name);
|
||||
}
|
||||
|
||||
JSCursor JSCursor::getChild (int64 index) const
|
||||
{
|
||||
auto copy = *this;
|
||||
copy.path.emplace_back (index);
|
||||
return copy;
|
||||
}
|
||||
|
||||
JSCursor JSCursor::operator[] (int64 index) const
|
||||
{
|
||||
return getChild (index);
|
||||
}
|
||||
|
||||
JSObject JSCursor::getOrCreateObject() const
|
||||
{
|
||||
const auto resolved = getPartialResolution();
|
||||
jassert (resolved.has_value());
|
||||
|
||||
const auto& [object, property] = *resolved;
|
||||
|
||||
if (! property.has_value())
|
||||
return object;
|
||||
|
||||
auto* integerValue = std::get_if<int64> (&(*property));
|
||||
|
||||
jassert (integerValue == nullptr
|
||||
|| (object.isArray() && (*integerValue) < object.getSize()));
|
||||
|
||||
if (integerValue != nullptr)
|
||||
return object[*integerValue];
|
||||
|
||||
auto* prop = std::get_if<Identifier> (&(*property));
|
||||
jassert(prop != nullptr);
|
||||
return object[*prop];
|
||||
}
|
||||
|
||||
bool JSCursor::isValid() const
|
||||
{
|
||||
return getPartialResolution().has_value();
|
||||
}
|
||||
|
||||
bool JSCursor::isArray() const
|
||||
{
|
||||
if (auto resolved = getFullResolution())
|
||||
return resolved->isArray();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var JSCursor::invoke (Span<const var> args, Result* result) const
|
||||
{
|
||||
const auto resolved = getPartialResolution();
|
||||
|
||||
if (! resolved.has_value())
|
||||
{
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto& [object, property] = *resolved;
|
||||
if (! property.has_value())
|
||||
{
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
return object.invokeMethod (*std::get_if<Identifier> (&(*property)), args, result);
|
||||
}
|
||||
|
||||
std::optional<JSObject> JSCursor::resolve (JSObject object, Property property)
|
||||
{
|
||||
if (auto* index = std::get_if<int64> (&property))
|
||||
{
|
||||
if (! object.isArray())
|
||||
return std::nullopt;
|
||||
|
||||
if (! (*index < object.getSize()))
|
||||
return std::nullopt;
|
||||
|
||||
return object[*index];
|
||||
}
|
||||
|
||||
if (auto* key = std::get_if<Identifier> (&property))
|
||||
{
|
||||
if (! object.hasProperty (*key))
|
||||
return std::nullopt;
|
||||
|
||||
return object[*key];
|
||||
}
|
||||
|
||||
jassertfalse;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<JSCursor::PartialResolution> JSCursor::getPartialResolution() const
|
||||
{
|
||||
auto object = root;
|
||||
|
||||
for (int i = 0, iEnd = (int) path.size() - 1; i < iEnd; ++i)
|
||||
{
|
||||
const auto& property = path[(size_t) i];
|
||||
auto objectOpt = resolve (object, property);
|
||||
|
||||
if (! objectOpt.has_value())
|
||||
return std::nullopt;
|
||||
|
||||
object = *objectOpt;
|
||||
}
|
||||
|
||||
return std::make_optional<PartialResolution> (std::move (object),
|
||||
path.empty() ? std::nullopt
|
||||
: std::make_optional (path.back()));
|
||||
}
|
||||
|
||||
std::optional<JSObject> JSCursor::getFullResolution() const
|
||||
{
|
||||
if (auto partiallyResolved = getPartialResolution())
|
||||
{
|
||||
if (! partiallyResolved->second.has_value())
|
||||
return partiallyResolved->first;
|
||||
|
||||
return resolve (partiallyResolved->first, *(partiallyResolved->second));
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
A high-level wrapper around an owning root JSObject and a hierarchical path relative to it.
|
||||
|
||||
It can be used to query and manipulate the location relative to the root JSObject in the
|
||||
Javascript Object graph. A cursor only maintains ownership of the root Object. So as long as a
|
||||
cursor points at the root it will always remain in a valid state, and isValid will return true.
|
||||
|
||||
Using getChild you can add elements to the cursor's relative path. You need to ensure that the
|
||||
cursor is in a valid state when calling get or set in such cases. You can use the isValid
|
||||
function to determine if the cursor currently points to a reachable location.
|
||||
|
||||
@tags{Core}
|
||||
*/
|
||||
class JUCE_API JSCursor
|
||||
{
|
||||
public:
|
||||
/** Creates a JSCursor that points to the provided root object and also participates in its
|
||||
ownership. This guarantees that this root object will remain valid for the lifetime of
|
||||
this cursor.
|
||||
|
||||
Child JSCursors created by getChild() will contain this same root object and each will
|
||||
further ensure that this root remains valid through reference counting.
|
||||
|
||||
While the validity of the root is ensured through shared ownership, the JSCursor itself is
|
||||
not guaranteed to be valid, unless its also pointing directly at the root.
|
||||
|
||||
@see isValid
|
||||
*/
|
||||
explicit JSCursor (JSObject root);
|
||||
|
||||
/** Returns an owning reference to the Javascript Object at the cursor's location. If there is
|
||||
no Object at the location but the cursor is valid, a new Object will be created.
|
||||
|
||||
You must only call this function on a valid JSCursor.
|
||||
|
||||
By creating an owning reference, you can create a new JSCursor object that owns the
|
||||
underlying object and is guaranteed to remain in a valid state e.g.
|
||||
|
||||
@code
|
||||
JSCursor rootCursor { engine.getRootObject() };
|
||||
auto nonOwningCursor = rootCursor["path"]["to"]["object"];
|
||||
|
||||
jassert (nonOwningCursor.isValid());
|
||||
|
||||
JSCursor owningCursor { nonOwningCursor.getOrCreateObject(); };
|
||||
engine.execute (arbitraryScript);
|
||||
|
||||
// owningCursor is guaranteed to remain valid even after subsequent script evaluations
|
||||
jassert (owningCursor.isValid());
|
||||
@endcode
|
||||
|
||||
@see isValid
|
||||
*/
|
||||
JSObject getOrCreateObject() const;
|
||||
|
||||
/** Returns the value corresponding to the Object that the cursor points to. If there is no
|
||||
Object at the cursor's location var::undefined() is returned.
|
||||
|
||||
This function is safe to call for invalid cursors.
|
||||
|
||||
@see isValid
|
||||
*/
|
||||
var get() const;
|
||||
|
||||
/** Sets the Object under the cursor's location to the specified value.
|
||||
|
||||
You must only call this function for valid cursors.
|
||||
|
||||
@see isValid
|
||||
*/
|
||||
void set (const var& value) const;
|
||||
|
||||
/** Invokes this node as though it were a method. If the optional Result pointer is provided it
|
||||
will contain Result::ok() in case of success, or an error message in case an exception was
|
||||
thrown during evaluation.
|
||||
|
||||
You must only call this function for valid cursors.
|
||||
*/
|
||||
var invoke (Span<const var> args, Result* result = nullptr) const;
|
||||
|
||||
/** Equivalent to invoke(). */
|
||||
var operator() (Span<const var> args, Result* result = nullptr) const
|
||||
{
|
||||
return invoke (args, result);
|
||||
}
|
||||
|
||||
/** Returns a new cursor that has the same root Object as the parent and has the name parameter
|
||||
appended to the cursor's location.
|
||||
|
||||
If the new path points to a location unreachable from the root, the resulting JSCursor
|
||||
object will be invalid. This however can change due to subsequent script executions.
|
||||
*/
|
||||
JSCursor getChild (const Identifier& name) const;
|
||||
|
||||
/** Returns a new cursor that has the same root Object as the parent and has the name parameter
|
||||
appended to the cursor's location.
|
||||
|
||||
If the new path points to a location unreachable from the root, the resulting JSCursor
|
||||
object will be invalid. This however can change due to subsequent script executions.
|
||||
Shorthand for getChild.
|
||||
*/
|
||||
JSCursor operator[] (const Identifier& name) const;
|
||||
|
||||
/** Returns a new cursor that has the same root Object as the parent and has the index parameter
|
||||
appended to the cursor's location. This overload will create a path that indexes into an
|
||||
Array.
|
||||
|
||||
If the new path points to a location unreachable from the root, the resulting JSCursor
|
||||
object will be invalid. This however can change due to subsequent script executions.
|
||||
*/
|
||||
JSCursor getChild (int64 index) const;
|
||||
|
||||
/** Returns a new cursor that has the same root Object as the parent and has the index parameter
|
||||
appended to the cursor's location. This overload will create a path that indexes into an
|
||||
Array.
|
||||
|
||||
If the new path points to a location unreachable from the root, the resulting JSCursor
|
||||
object will be invalid. This however can change due to subsequent script executions.
|
||||
Shorthand for getChild.
|
||||
*/
|
||||
JSCursor operator[] (int64 index) const;
|
||||
|
||||
/** Returns true if the location of the cursor is reachable from the cursor's JSObject root.
|
||||
This means it is safe to call set on this JSCursor and the location will then point to an
|
||||
Object corresponding to the supplied value.
|
||||
|
||||
It isn't guaranteed that there is already an Object at this location, in which case calling
|
||||
get will return var::undefined().
|
||||
*/
|
||||
bool isValid() const;
|
||||
|
||||
/** Returns true if there is an Array under the cursor's location.
|
||||
|
||||
It is safe to call this function on an invalid cursor.
|
||||
*/
|
||||
bool isArray() const;
|
||||
|
||||
private:
|
||||
using Property = std::variant<Identifier, int64>;
|
||||
using PartialResolution = std::pair<JSObject, std::optional<Property>>;
|
||||
|
||||
static std::optional<JSObject> resolve (JSObject reference, Property property);
|
||||
|
||||
// Resolves the path to the second to last element. By taking ownership (creating an object for)
|
||||
// of the second to last element, the result of a successful partial resolution can be used to
|
||||
// construct the last element if it doesn't yet exist.
|
||||
std::optional<PartialResolution> getPartialResolution() const;
|
||||
|
||||
// Fully resolves the path and takes ownership of the object that was specified by it.
|
||||
std::optional<JSObject> getFullResolution() const;
|
||||
|
||||
JSObject root;
|
||||
std::vector<Property> path;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wsubobject-linkage")
|
||||
class JSObject::Impl
|
||||
{
|
||||
public:
|
||||
using ValuePtr = detail::qjs::QuickJSContext::ValuePtr;
|
||||
|
||||
explicit Impl (detail::QuickJSWrapper* engineIn)
|
||||
: Impl (engineIn,
|
||||
{ detail::qjs::JS_GetGlobalObject (engineIn->getQuickJSContext()), engineIn->getQuickJSContext() })
|
||||
{
|
||||
}
|
||||
|
||||
Impl (const Impl& other)
|
||||
: Impl (other.engine,
|
||||
{ detail::qjs::JS_DupValue (other.engine->getQuickJSContext(), other.valuePtr.get()),
|
||||
other.engine->getQuickJSContext() })
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<Impl> getChild (const Identifier& prop) const
|
||||
{
|
||||
return rawToUniquePtr (new Impl (engine, { detail::getOrCreateProperty (engine->getQuickJSContext(),
|
||||
valuePtr.get(),
|
||||
prop.toString().toRawUTF8()),
|
||||
engine->getQuickJSContext() }));
|
||||
}
|
||||
|
||||
std::unique_ptr<Impl> getChild (int64 index) const
|
||||
{
|
||||
jassert (isArray());
|
||||
return rawToUniquePtr (new Impl (engine, valuePtr[detail::toUint32 (index)]));
|
||||
}
|
||||
|
||||
bool hasProperty (const Identifier& name) const
|
||||
{
|
||||
return detail::hasProperty (engine->getQuickJSContext(), valuePtr.get(), name.toString().toRawUTF8());
|
||||
}
|
||||
|
||||
void setProperty (const Identifier& name, const var& value) const
|
||||
{
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
|
||||
detail::qjs::JS_SetPropertyStr (ctx, valuePtr.get(), name.toString().toRawUTF8(), detail::juceToQuickJs (value, ctx));
|
||||
}
|
||||
|
||||
void setProperty (int64 index, const var& value) const
|
||||
{
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
|
||||
detail::qjs::JS_SetPropertyInt64 (ctx, valuePtr.get(), index, detail::juceToQuickJs (value, ctx));
|
||||
}
|
||||
|
||||
var get() const
|
||||
{
|
||||
if (auto* opaque = detail::qjs::JS_GetOpaque (valuePtr.get(), detail::DynamicObjectWrapper::getClassId()))
|
||||
if (detail::DynamicObjectWrapper::getDynamicObjects().count (opaque) != 0)
|
||||
return { static_cast<detail::DynamicObjectWrapper*> (opaque)->object.get() };
|
||||
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
return detail::discardError (detail::quickJSToJuce ({ detail::qjs::JS_DupValue (ctx, valuePtr.get()), ctx }));
|
||||
}
|
||||
|
||||
detail::VarOrError invokeMethod (const Identifier& methodName, Span<const var> args) const
|
||||
{
|
||||
engine->resetTimeout();
|
||||
|
||||
if (! hasProperty (methodName))
|
||||
{
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
const auto methodAtom = JS_NewAtom (ctx, methodName.toString().toRawUTF8());
|
||||
ScopeGuard scope { [&] { detail::qjs::JS_FreeAtom (ctx, methodAtom); } };
|
||||
|
||||
detail::JSFunctionArguments arguments { ctx, args };
|
||||
|
||||
ValuePtr returnVal { detail::qjs::JS_Invoke (ctx,
|
||||
valuePtr.get(),
|
||||
methodAtom,
|
||||
arguments.getSize(),
|
||||
arguments.getArguments()),
|
||||
ctx };
|
||||
|
||||
return detail::quickJSToJuce (returnVal);
|
||||
}
|
||||
|
||||
NamedValueSet getProperties() const
|
||||
{
|
||||
NamedValueSet result;
|
||||
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
ValuePtr names { detail::qjs::JS_GetOwnPropertyNames2 (ctx,
|
||||
valuePtr.get(),
|
||||
detail::qjs::JS_GPN_ENUM_ONLY | detail::qjs::JS_GPN_STRING_MASK,
|
||||
detail::qjs::JS_ITERATOR_KIND_KEY),
|
||||
ctx };
|
||||
|
||||
|
||||
if (auto v = detail::discardError (detail::quickJSToJuce (names)); const auto* propertyNames = v.getArray())
|
||||
{
|
||||
for (const auto& name : *propertyNames)
|
||||
{
|
||||
if (name.isString())
|
||||
{
|
||||
const Identifier prop { name.toString() };
|
||||
result.set (prop, getChild (prop)->get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isArray() const
|
||||
{
|
||||
return detail::qjs::JS_IsArray (engine->getQuickJSContext(), valuePtr.get());
|
||||
}
|
||||
|
||||
int64 getSize() const
|
||||
{
|
||||
if (! isArray())
|
||||
{
|
||||
jassertfalse;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto lengthProp = valuePtr["length"];
|
||||
uint32_t length = 0;
|
||||
detail::qjs::JS_ToUint32 (engine->getQuickJSContext(), &length, lengthProp.get());
|
||||
return (int64) length;
|
||||
}
|
||||
|
||||
private:
|
||||
Impl (detail::QuickJSWrapper* e, ValuePtr&& ptr)
|
||||
: engine (e), valuePtr (std::move (ptr))
|
||||
{
|
||||
}
|
||||
|
||||
detail::QuickJSWrapper* engine = nullptr;
|
||||
ValuePtr valuePtr;
|
||||
};
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
JSObject::JSObject (detail::QuickJSWrapper* engine)
|
||||
: impl (new Impl (engine))
|
||||
{
|
||||
}
|
||||
|
||||
JSObject::JSObject (std::unique_ptr<Impl> implIn)
|
||||
: impl (std::move (implIn))
|
||||
{
|
||||
}
|
||||
|
||||
JSObject::JSObject (const JSObject& other)
|
||||
: impl (new Impl (*other.impl))
|
||||
{
|
||||
}
|
||||
|
||||
JSObject::~JSObject() = default;
|
||||
|
||||
JSObject::JSObject (JSObject&&) noexcept = default;
|
||||
|
||||
JSObject& JSObject::operator= (const JSObject& other)
|
||||
{
|
||||
JSObject { other }.swap (*this);
|
||||
return *this;
|
||||
}
|
||||
|
||||
JSObject& JSObject::operator= (JSObject&& other) noexcept = default;
|
||||
|
||||
JSObject JSObject::getChild (const Identifier& name) const
|
||||
{
|
||||
return JSObject { impl->getChild (name) };
|
||||
}
|
||||
|
||||
JSObject JSObject::operator[] (const Identifier& name) const
|
||||
{
|
||||
return getChild (name);
|
||||
}
|
||||
|
||||
bool JSObject::isArray() const
|
||||
{
|
||||
return impl->isArray();
|
||||
}
|
||||
|
||||
int64 JSObject::getSize() const
|
||||
{
|
||||
return impl->getSize();
|
||||
}
|
||||
|
||||
JSObject JSObject::getChild (int64 index) const
|
||||
{
|
||||
jassert (isArray());
|
||||
return JSObject { impl->getChild (index) };
|
||||
}
|
||||
|
||||
JSObject JSObject::operator[] (int64 index) const
|
||||
{
|
||||
return getChild (index);
|
||||
}
|
||||
|
||||
bool JSObject::hasProperty (const Identifier& name) const
|
||||
{
|
||||
return impl->hasProperty (name);
|
||||
}
|
||||
|
||||
var JSObject::get() const
|
||||
{
|
||||
return impl->get();
|
||||
}
|
||||
|
||||
void JSObject::setProperty (const Identifier& name, const var& value) const
|
||||
{
|
||||
impl->setProperty (name, value);
|
||||
}
|
||||
|
||||
void JSObject::setProperty (int64 index, const var& value) const
|
||||
{
|
||||
impl->setProperty (index, value);
|
||||
}
|
||||
|
||||
var JSObject::invokeMethod (const Identifier& methodName,
|
||||
Span<const var> args,
|
||||
Result* result) const
|
||||
{
|
||||
const auto varOrError = impl->invokeMethod (methodName, args);
|
||||
|
||||
if (result != nullptr)
|
||||
{
|
||||
const auto* e = std::get_if<String> (&varOrError);
|
||||
*result = e != nullptr ? Result::fail (*e) : Result::ok();
|
||||
}
|
||||
|
||||
return detail::discardError (varOrError);
|
||||
}
|
||||
|
||||
NamedValueSet JSObject::getProperties() const
|
||||
{
|
||||
return impl->getProperties();
|
||||
}
|
||||
|
||||
void JSObject::swap (JSObject& other) noexcept
|
||||
{
|
||||
std::swap (impl, other.impl);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
A JSObject represents an owning reference to the underlying JS object, meaning it will remain
|
||||
valid even if a subsequent script execution deletes other handles to it.
|
||||
|
||||
Objects of this class can be used to traverse the current object graph inside the specified
|
||||
Javascript engine.
|
||||
|
||||
This is a low-level providing only operations that map directly to the underlying Javascript
|
||||
Object implementation. The JSCursor class generally provides a more convenient interface with
|
||||
functions that may fail based on the Javascript engine's current state.
|
||||
|
||||
@see JSCursor
|
||||
@tags{Core}
|
||||
*/
|
||||
class JUCE_API JSObject
|
||||
{
|
||||
public:
|
||||
/** Constructor, used internally by the JavascriptEngine implementation.
|
||||
|
||||
To create a new JSObject pointing at the root object of the engine's context use
|
||||
JavascriptEngine::getRootObject().
|
||||
*/
|
||||
explicit JSObject (detail::QuickJSWrapper* engine);
|
||||
|
||||
/** Destructor. */
|
||||
~JSObject();
|
||||
|
||||
/** Copy constructor. */
|
||||
JSObject (const JSObject&);
|
||||
|
||||
/** Move constructor. */
|
||||
JSObject (JSObject&&) noexcept;
|
||||
|
||||
/** Copy assignment operator. */
|
||||
JSObject& operator= (const JSObject&);
|
||||
|
||||
/** Move assignment operator. */
|
||||
JSObject& operator= (JSObject&&) noexcept;
|
||||
|
||||
/** Returns a new cursor pointing to a JS object that is a property of the parent cursor's
|
||||
underlying object and has the provided name.
|
||||
|
||||
You can use hasProperty() to check if such a property exists prior to the creation of this
|
||||
cursor. If no such property exists, this constructor will create a new JS Object and attach
|
||||
it to the parent under the specified name. This can be used to manipulate the object graph.
|
||||
*/
|
||||
JSObject getChild (const Identifier& name) const;
|
||||
|
||||
/** Returns a cursor object pointing to the property with the given name. If such property
|
||||
doesn't exist it will be created as an empty JS Object. Shorthand for getChild.
|
||||
*/
|
||||
JSObject operator[] (const Identifier& name) const;
|
||||
|
||||
/** Returns a new cursor object pointing to the specified element in an Array.
|
||||
|
||||
You must ensure that the cursor points to an Array before calling this function.
|
||||
|
||||
@see isArray
|
||||
*/
|
||||
JSObject getChild (int64 index) const;
|
||||
|
||||
/** Returns a new cursor object pointing to the specified element in an Array. This function is
|
||||
a shorthand for getChild (int64).
|
||||
|
||||
You must ensure that the cursor points to an Array before calling this function.
|
||||
|
||||
@see isArray
|
||||
*/
|
||||
JSObject operator[] (int64 index) const;
|
||||
|
||||
/** Returns true if the JS Object under the cursor is an Array.
|
||||
|
||||
You can use getChild() or operator[]() to get a cursor to individual elements in the
|
||||
array or get() to obtain a JUCE variant wrapping all array elements.
|
||||
*/
|
||||
bool isArray() const;
|
||||
|
||||
/** Returns the size of the underlying JS Array.
|
||||
|
||||
You must ensure that the cursor points to an Array before calling this function.
|
||||
|
||||
@see isArray
|
||||
*/
|
||||
int64 getSize() const;
|
||||
|
||||
/** Returns true if the object under the cursor has a property with the given name. */
|
||||
bool hasProperty (const Identifier& name) const;
|
||||
|
||||
/** Returns a variant with a value of the property under the given name. If no such property
|
||||
exists an undefined variant is returned.
|
||||
|
||||
If this property points to an object created by JavascriptEngine::registerNativeObject(),
|
||||
then the returned variant will contain a pointer to the original object and can be acquired
|
||||
by variant::getDynamicObject().
|
||||
*/
|
||||
var get() const;
|
||||
|
||||
/** Adds a named property to the underlying Object with the provided value, or assigns this
|
||||
value to an existing property with this name.
|
||||
*/
|
||||
void setProperty (const Identifier& name, const var& value) const;
|
||||
|
||||
/** Adds a property with an integral identifier and the provided value to the underlying Object,
|
||||
or assigns the value to an existing property.
|
||||
|
||||
If the underlying Object is also an Array, then the provided value will be assigned to the
|
||||
specified element of this Array, and ensure that it will have a size of at least index - 1.
|
||||
*/
|
||||
void setProperty (int64 index, const var& value) const;
|
||||
|
||||
/** Invokes this node as though it were a method.
|
||||
|
||||
If the optional Result pointer is provided it will contain Result::ok() in case of success,
|
||||
or an error message in case an exception was thrown during evaluation.
|
||||
*/
|
||||
var invokeMethod (const Identifier& methodName, Span<const var> args, Result* result = nullptr) const;
|
||||
|
||||
/** Returns all properties of the current object that are own properties, i.e. not inherited. */
|
||||
NamedValueSet getProperties() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
|
||||
explicit JSObject (std::unique_ptr<Impl> implIn);
|
||||
|
||||
void swap (JSObject& other) noexcept;
|
||||
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
class JavascriptEngine::Impl
|
||||
{
|
||||
public:
|
||||
using ValuePtr = detail::qjs::QuickJSContext::ValuePtr;
|
||||
|
||||
//==============================================================================
|
||||
explicit Impl (const RelativeTime* maximumExecutionTimeIn)
|
||||
: engine (std::make_unique<detail::QuickJSWrapper> (maximumExecutionTimeIn))
|
||||
{
|
||||
detail::DynamicObjectWrapper::createClass (engine->getQuickJSRuntime());
|
||||
}
|
||||
|
||||
void registerNativeObject (const Identifier& name,
|
||||
DynamicObject::Ptr dynamicObject,
|
||||
std::optional<detail::qjs::JSValue> parent = std::nullopt)
|
||||
{
|
||||
auto wrapper = std::make_unique<detail::DynamicObjectWrapper> (*engine, dynamicObject);
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
auto jsObject = JS_NewObjectClass (ctx, (int) detail::DynamicObjectWrapper::getClassId());
|
||||
detail::qjs::JS_SetOpaque (jsObject, (void*) wrapper.get());
|
||||
|
||||
std::vector<detail::qjs::JSCFunctionListEntry> propertyFunctionList;
|
||||
|
||||
for (const auto& [identifier, prop] : wrapper->getProperties())
|
||||
{
|
||||
auto* jsIdentifier = identifier.toString().toRawUTF8();
|
||||
|
||||
if (prop.isMethod())
|
||||
{
|
||||
detail::qjs::JS_SetPropertyStr (ctx,
|
||||
jsObject,
|
||||
jsIdentifier,
|
||||
JS_NewCFunctionMagic (ctx,
|
||||
detail::DynamicObjectWrapper::callDispatcher,
|
||||
jsIdentifier,
|
||||
0,
|
||||
detail::qjs::JS_CFUNC_generic_magic,
|
||||
wrapper->getOrdinal (identifier)));
|
||||
}
|
||||
else if (prop.isObject())
|
||||
{
|
||||
if (auto* embeddedObject = prop.getDynamicObject())
|
||||
registerNativeObject (identifier, embeddedObject, jsObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto entry = detail::makeFunctionListEntry (jsIdentifier,
|
||||
detail::DynamicObjectWrapper::getDispatcher,
|
||||
detail::DynamicObjectWrapper::setDispatcher,
|
||||
wrapper->getOrdinal (identifier));
|
||||
propertyFunctionList.push_back (entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (! propertyFunctionList.empty())
|
||||
{
|
||||
detail::qjs::JS_SetPropertyFunctionList (ctx,
|
||||
jsObject,
|
||||
propertyFunctionList.data(),
|
||||
(int) propertyFunctionList.size());
|
||||
}
|
||||
|
||||
const auto jsObjectName = name.toString().toRawUTF8();
|
||||
|
||||
if (parent.has_value())
|
||||
{
|
||||
detail::qjs::JS_SetPropertyStr (ctx, *parent, jsObjectName, jsObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
ValuePtr globalObject { detail::qjs::JS_GetGlobalObject (ctx), ctx };
|
||||
detail::qjs::JS_SetPropertyStr (ctx, globalObject.get(), jsObjectName, jsObject);
|
||||
}
|
||||
|
||||
wrapper.release();
|
||||
}
|
||||
|
||||
var evaluate (const String& code, Result* errorMessage)
|
||||
{
|
||||
engine->resetTimeout();
|
||||
|
||||
if (errorMessage != nullptr)
|
||||
*errorMessage = Result::ok();
|
||||
|
||||
const auto result = detail::quickJSToJuce ({ JS_Eval (engine->getQuickJSContext(),
|
||||
code.toRawUTF8(),
|
||||
code.getNumBytesAsUTF8(),
|
||||
"",
|
||||
JS_EVAL_TYPE_GLOBAL),
|
||||
engine->getQuickJSContext() });
|
||||
|
||||
if (auto* v = std::get_if<var> (&result))
|
||||
return *v;
|
||||
|
||||
if (auto* e = std::get_if<String> (&result))
|
||||
if (errorMessage != nullptr)
|
||||
*errorMessage = Result::fail (*e);
|
||||
|
||||
return var::undefined();
|
||||
}
|
||||
|
||||
Result execute (const String& code)
|
||||
{
|
||||
auto result = Result::ok();
|
||||
evaluate (code, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
var callFunction (const Identifier& function,
|
||||
const var::NativeFunctionArgs& args,
|
||||
Result* errorMessage)
|
||||
{
|
||||
engine->resetTimeout();
|
||||
|
||||
auto* ctx = engine->getQuickJSContext();
|
||||
const auto functionStr = function.toString();
|
||||
|
||||
const auto fn = detail::qjs::JS_NewAtomLen (ctx, functionStr.toRawUTF8(), functionStr.getNumBytesAsUTF8());
|
||||
|
||||
detail::JSFunctionArguments argList { ctx, args };
|
||||
|
||||
detail::qjs::QuickJSContext::ValuePtr global { JS_GetGlobalObject (ctx), ctx };
|
||||
detail::qjs::QuickJSContext::ValuePtr returnVal { JS_Invoke (ctx, global.get(), fn, argList.getSize(), argList.getArguments()), ctx };
|
||||
|
||||
JS_FreeAtom (ctx, fn);
|
||||
|
||||
if (errorMessage != nullptr)
|
||||
*errorMessage = Result::ok();
|
||||
|
||||
const auto result = detail::quickJSToJuce (returnVal);
|
||||
|
||||
if (auto* v = std::get_if<var> (&result))
|
||||
return *v;
|
||||
|
||||
if (auto* e = std::get_if<String> (&result))
|
||||
if (errorMessage != nullptr)
|
||||
*errorMessage = Result::fail (*e);
|
||||
|
||||
return var::undefined();
|
||||
}
|
||||
|
||||
void stop() noexcept
|
||||
{
|
||||
engine->stop();
|
||||
}
|
||||
|
||||
JSObject getRootObject() const
|
||||
{
|
||||
return JSObject { engine.get() };
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<detail::QuickJSWrapper> engine;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
JavascriptEngine::JavascriptEngine()
|
||||
: maximumExecutionTime (15.0),
|
||||
impl (std::make_unique<Impl> (&maximumExecutionTime))
|
||||
{
|
||||
}
|
||||
|
||||
JavascriptEngine::~JavascriptEngine() = default;
|
||||
|
||||
void JavascriptEngine::registerNativeObject (const Identifier& name, DynamicObject* object)
|
||||
{
|
||||
impl->registerNativeObject (name, object);
|
||||
}
|
||||
|
||||
Result JavascriptEngine::execute (const String& javascriptCode)
|
||||
{
|
||||
return impl->execute (javascriptCode);
|
||||
}
|
||||
|
||||
var JavascriptEngine::evaluate (const String& javascriptCode, Result* errorMessage)
|
||||
{
|
||||
return impl->evaluate (javascriptCode, errorMessage);
|
||||
}
|
||||
|
||||
var JavascriptEngine::callFunction (const Identifier& function,
|
||||
const var::NativeFunctionArgs& args,
|
||||
Result* errorMessage)
|
||||
{
|
||||
return impl->callFunction (function, args, errorMessage);
|
||||
}
|
||||
|
||||
void JavascriptEngine::stop() noexcept
|
||||
{
|
||||
impl->stop();
|
||||
}
|
||||
|
||||
JSObject JavascriptEngine::getRootObject() const
|
||||
{
|
||||
return impl->getRootObject();
|
||||
}
|
||||
|
||||
NamedValueSet JavascriptEngine::getRootObjectProperties() const
|
||||
{
|
||||
return getRootObject().getProperties();
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
This class is a wrapper around QuickJS, an ES2023 compliant, embeddable javascript
|
||||
engine. It may not be as fast as the fancy JIT-compiled
|
||||
engines that you get in browsers, but this is an extremely compact, low-overhead javascript
|
||||
interpreter, which is integrated with the juce var and DynamicObject classes. It allows you
|
||||
to easily let the JS work with native objects defined as DynamicObject subclasses.
|
||||
|
||||
To use, simply create an instance of this class and call execute() to run your code.
|
||||
Variables that the script sets can be retrieved with evaluate(), and if you need to provide
|
||||
native objects for the script to use, you can add them with registerNativeObject().
|
||||
|
||||
@tags{Core}
|
||||
*/
|
||||
class JUCE_API JavascriptEngine final
|
||||
{
|
||||
public:
|
||||
/** Creates an instance of the engine.
|
||||
*/
|
||||
JavascriptEngine();
|
||||
|
||||
/** Destructor. */
|
||||
~JavascriptEngine();
|
||||
|
||||
/** Attempts to parse and run a block of javascript code.
|
||||
If there's a parse or execution error, the error description is returned in
|
||||
the result.
|
||||
You can specify a maximum time for which the program is allowed to run, and
|
||||
it'll return with an error message if this time is exceeded.
|
||||
*/
|
||||
Result execute (const String& javascriptCode);
|
||||
|
||||
/** Attempts to parse and run a javascript expression, and returns the result.
|
||||
If there's a syntax error, or the expression can't be evaluated, the return value
|
||||
will be var::undefined(). The errorMessage parameter gives you a way to find out
|
||||
any parsing errors.
|
||||
If the expression is successfully evaluated but yields no result the return value
|
||||
will be a void var.
|
||||
You can specify a maximum time for which the program is allowed to run, and
|
||||
it'll return with an error message if this time is exceeded.
|
||||
*/
|
||||
var evaluate (const String& javascriptCode,
|
||||
Result* errorMessage = nullptr);
|
||||
|
||||
/** Calls a function in the root namespace, and returns the result.
|
||||
The function arguments are passed in the same format as used by native
|
||||
methods in the var class.
|
||||
*/
|
||||
var callFunction (const Identifier& function,
|
||||
const var::NativeFunctionArgs& args,
|
||||
Result* errorMessage = nullptr);
|
||||
|
||||
/** Adds a native object to the root namespace.
|
||||
The object passed-in is reference-counted, and will be retained by the
|
||||
engine until the engine is deleted. The name must be a simple JS identifier,
|
||||
without any dots.
|
||||
*/
|
||||
void registerNativeObject (const Identifier& objectName, DynamicObject* object);
|
||||
|
||||
/** This value indicates how long a call to one of the evaluate methods is permitted
|
||||
to run before timing-out and failing.
|
||||
The default value is a number of seconds, but you can change this to whatever value
|
||||
suits your application.
|
||||
*/
|
||||
RelativeTime maximumExecutionTime;
|
||||
|
||||
/** When called from another thread, causes the interpreter to time-out as soon as possible */
|
||||
void stop() noexcept;
|
||||
|
||||
/** Returns the object from which all Javascript objects are reachable in the engine's context.
|
||||
*/
|
||||
JSObject getRootObject() const;
|
||||
|
||||
/** Provides access to the set of properties of the root namespace object. */
|
||||
NamedValueSet getRootObjectProperties() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JavascriptEngine)
|
||||
JUCE_DECLARE_NON_MOVEABLE (JavascriptEngine)
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
static constexpr const char javascriptTestSource[] = R"x(
|
||||
var testObject = new Object();
|
||||
testObject.value = 9;
|
||||
testObject.add = function(a, b)
|
||||
{
|
||||
return a + b;
|
||||
};
|
||||
var array = [1.1, 1.9, -1.25, -1.9];
|
||||
)x";
|
||||
|
||||
static constexpr const char accessNewObject[] = R"x(
|
||||
var ref = newObject;
|
||||
)x";
|
||||
|
||||
static constexpr const char createAccumulator[] = R"x(
|
||||
class CommunicationsObject
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class DataAccumulator
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.commObject = new CommunicationsObject();
|
||||
this.sum = 0;
|
||||
}
|
||||
|
||||
getCommObject()
|
||||
{
|
||||
return this.commObject;
|
||||
}
|
||||
|
||||
accumulate()
|
||||
{
|
||||
this.sum += this.commObject.value;
|
||||
this.commObject.value = 0;
|
||||
return this.sum;
|
||||
}
|
||||
}
|
||||
|
||||
var accumulator = new DataAccumulator();
|
||||
var commObject = accumulator.getCommObject();
|
||||
)x";
|
||||
|
||||
static constexpr const char replaceObjectAtCommHandleLocation[] = R"x(
|
||||
var commObject = new CommunicationsObject();
|
||||
)x";
|
||||
|
||||
|
||||
class JavascriptTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
JavascriptTests() : UnitTest ("Javascript", UnitTestCategories::javascript)
|
||||
{
|
||||
}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
JavascriptEngine engine;
|
||||
engine.maximumExecutionTime = RelativeTime::seconds (5);
|
||||
|
||||
beginTest ("Basic evaluations");
|
||||
{
|
||||
auto result = Result::ok();
|
||||
|
||||
auto value = engine.evaluate ("[]", &result);
|
||||
expect (result.wasOk() && value == var { Array<var>{} }, "An empty array literal should evaluate correctly");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
engine.evaluate (javascriptTestSource);
|
||||
|
||||
beginTest ("JSCursor::invokeMethod");
|
||||
{
|
||||
JSCursor root { engine.getRootObject() };
|
||||
const auto result = root["testObject"]["add"] (Span { std::initializer_list<var> { 5, 2 } });
|
||||
expect (result.isDouble());
|
||||
expect (exactlyEqual ((double) result, 7.0));
|
||||
}
|
||||
|
||||
beginTest ("JSCursor Array access");
|
||||
{
|
||||
JSCursor root { engine.getRootObject() };
|
||||
expect (root["array"].isArray());
|
||||
expectEquals ((double) root["array"][2].get(), -1.25);
|
||||
}
|
||||
|
||||
beginTest ("JSObjectCursor references");
|
||||
{
|
||||
auto rootObject = engine.getRootObject();
|
||||
rootObject["child"]["value"];
|
||||
|
||||
JSCursor root { rootObject };
|
||||
auto child = root["child"];
|
||||
auto value = child["value"];
|
||||
value.set (9);
|
||||
|
||||
auto directReference = value;
|
||||
directReference.set (10);
|
||||
expectEquals ((double) value.get(), 10.0);
|
||||
|
||||
auto indirectReference = child["value"];
|
||||
indirectReference.set (11);
|
||||
expectEquals ((double) value.get(), 11.0);
|
||||
|
||||
auto indirectReference2 = root["child"]["value"];
|
||||
indirectReference2.set (12);
|
||||
expectEquals ((double) value.get(), 12.0);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
beginTest ("The object referenced by the cursor should be accessible from Javascript");
|
||||
{
|
||||
auto rooObject = engine.getRootObject();
|
||||
auto newObject = rooObject["newObject"];
|
||||
|
||||
auto result = Result::ok();
|
||||
engine.evaluate (accessNewObject, &result);
|
||||
expect (result.wasOk(), "Failed to access newObject: " + result.getErrorMessage());
|
||||
}
|
||||
|
||||
beginTest ("The object referenced by the cursor shouldn't disappear/change");
|
||||
{
|
||||
engine.execute (createAccumulator);
|
||||
JSCursor rootCursor { engine.getRootObject() };
|
||||
auto commObjectCursor = rootCursor["commObject"];
|
||||
commObjectCursor["value"].set (5);
|
||||
auto accumulatorCursor = rootCursor["accumulator"];
|
||||
|
||||
// The Accumulator and our cursor refer to the same object, through which they can
|
||||
// communicate.
|
||||
expectEquals ((int) accumulatorCursor["accumulate"]({}), 5);
|
||||
|
||||
// A cursor contains an owning reference to the Object passed into its constructor. We
|
||||
// can bind a cursor to the Object at the current location by reseating it. Without this
|
||||
// step the test would fail.
|
||||
commObjectCursor = JSCursor { commObjectCursor.getOrCreateObject() };
|
||||
|
||||
// This changes the object under the previous location.
|
||||
engine.execute (replaceObjectAtCommHandleLocation);
|
||||
commObjectCursor["value"].set (2);
|
||||
|
||||
expectEquals ((int) accumulatorCursor["accumulate"]({}), 7,
|
||||
"We aren't referring to the Accumulator's object anymore");
|
||||
}
|
||||
|
||||
beginTest ("A JSCursor instance can be used to retrieve whatever value is at a given location");
|
||||
{
|
||||
engine.execute ("var path = new Object();"
|
||||
"path.to = new Object();"
|
||||
"path.to.location = 5;");
|
||||
|
||||
auto cursor = JSCursor { engine.getRootObject() }["path"]["to"]["location"];
|
||||
|
||||
expectEquals ((int) cursor.get(), 5);
|
||||
|
||||
engine.execute ("path.to = new Object();"
|
||||
"path.to.location = 6;");
|
||||
|
||||
expectEquals ((int) cursor.get(), 6);
|
||||
}
|
||||
|
||||
beginTest ("Native functions returning objects with native functions work as expected");
|
||||
{
|
||||
JavascriptEngine temporaryEngine;
|
||||
|
||||
temporaryEngine.registerNativeObject ("ObjGetter", [&]
|
||||
{
|
||||
auto* objGetter = new DynamicObject();
|
||||
|
||||
objGetter->setMethod ("getObj", [&] (const auto&)
|
||||
{
|
||||
auto* obj = new DynamicObject();
|
||||
|
||||
obj->setMethod ("getVal", [] (const auto&)
|
||||
{
|
||||
return 42;
|
||||
});
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
return objGetter;
|
||||
}());
|
||||
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto val = temporaryEngine.evaluate ("let objGetter = ObjGetter; let obj = objGetter.getObj(); obj.getVal();", &res);
|
||||
expect (res.wasOk());
|
||||
expect (static_cast<int> (val) == 42);
|
||||
}
|
||||
|
||||
beginTest ("Methods of javascript objects can be called from C++");
|
||||
{
|
||||
JavascriptEngine temporaryEngine;
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto val = temporaryEngine.evaluate ("var result = { bar: 5, foo (a) { return a + this.bar; } }; result;", &res);
|
||||
expect (res.wasOk());
|
||||
|
||||
auto* obj = val.getDynamicObject();
|
||||
|
||||
if (obj == nullptr)
|
||||
{
|
||||
expect (false);
|
||||
return;
|
||||
}
|
||||
|
||||
expect (obj->hasMethod ("foo"));
|
||||
expect (obj->hasProperty ("bar"));
|
||||
|
||||
expect (obj->getProperty ("bar") == var (5));
|
||||
|
||||
const var a[] { var { 10 } };
|
||||
const auto aResult = obj->invokeMethod ("foo", { val, std::data (a), (int) std::size (a) });
|
||||
expect (aResult == var (15));
|
||||
|
||||
temporaryEngine.evaluate ("result.bar = -5;", &res);
|
||||
expect (res.wasOk());
|
||||
|
||||
const var b[] { var { -10 } };
|
||||
const auto bResult = obj->invokeMethod ("foo", { val, std::data (b), (int) std::size (b) });
|
||||
expect (bResult == var (-15));
|
||||
}
|
||||
|
||||
beginTest ("Destructors of custom callables are called, eventually");
|
||||
{
|
||||
struct CustomCallable
|
||||
{
|
||||
explicit CustomCallable (int& instances)
|
||||
: liveInstances (instances)
|
||||
{
|
||||
++liveInstances;
|
||||
}
|
||||
|
||||
CustomCallable (const CustomCallable& other)
|
||||
: liveInstances (other.liveInstances)
|
||||
{
|
||||
++liveInstances;
|
||||
}
|
||||
|
||||
CustomCallable (CustomCallable&& other) noexcept
|
||||
: liveInstances (other.liveInstances)
|
||||
{
|
||||
++liveInstances;
|
||||
}
|
||||
|
||||
~CustomCallable()
|
||||
{
|
||||
--liveInstances;
|
||||
}
|
||||
|
||||
CustomCallable& operator= (const CustomCallable&) = delete;
|
||||
CustomCallable& operator= (CustomCallable&&) noexcept = delete;
|
||||
|
||||
var operator() (const var::NativeFunctionArgs&) const { return "hello world"; }
|
||||
|
||||
int& liveInstances;
|
||||
};
|
||||
|
||||
int methodInstances = 0;
|
||||
|
||||
{
|
||||
JavascriptEngine temporaryEngine;
|
||||
|
||||
temporaryEngine.registerNativeObject ("ObjGetter", [&]
|
||||
{
|
||||
auto* objGetter = new DynamicObject();
|
||||
|
||||
objGetter->setMethod ("getObj", [&] (const auto&)
|
||||
{
|
||||
auto* obj = new DynamicObject;
|
||||
obj->setMethod ("getVal", CustomCallable { methodInstances });
|
||||
return obj;
|
||||
});
|
||||
|
||||
return objGetter;
|
||||
}());
|
||||
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto value = temporaryEngine.evaluate ("ObjGetter.getObj().getVal();", &res);
|
||||
expect (res.wasOk());
|
||||
expect (value == "hello world");
|
||||
}
|
||||
|
||||
expect (methodInstances == 0);
|
||||
}
|
||||
|
||||
beginTest ("null and undefined return values are distinctly represented");
|
||||
{
|
||||
JavascriptEngine temporaryEngine;
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto val = temporaryEngine.evaluate ("var result = { returnsNull (a) { return null; }, returnsUndefined (a) { 5 + 2; } }; result;", &res);
|
||||
expect (res.wasOk());
|
||||
|
||||
auto* obj = val.getDynamicObject();
|
||||
|
||||
if (obj == nullptr)
|
||||
{
|
||||
expect (false);
|
||||
return;
|
||||
}
|
||||
|
||||
expect (obj->hasMethod ("returnsNull"));
|
||||
const auto aResult = obj->invokeMethod ("returnsNull", { val, nullptr, 0 });
|
||||
expect (aResult.isVoid());
|
||||
|
||||
expect (obj->hasMethod ("returnsUndefined"));
|
||||
const auto bResult = obj->invokeMethod ("returnsUndefined", { val, nullptr, 0 });
|
||||
expect (bResult.isUndefined());
|
||||
}
|
||||
|
||||
beginTest ("calling a C function that returns void is converted correctly");
|
||||
{
|
||||
int numCalls = 0;
|
||||
|
||||
JavascriptEngine temporaryEngine;
|
||||
|
||||
temporaryEngine.registerNativeObject ("Obj", [&]
|
||||
{
|
||||
auto* objGetter = new DynamicObject();
|
||||
|
||||
objGetter->setMethod ("getObj", [&] (const auto&)
|
||||
{
|
||||
auto* obj = new DynamicObject;
|
||||
|
||||
obj->setMethod ("mutate", [&] (const auto&)
|
||||
{
|
||||
++numCalls;
|
||||
return var{};
|
||||
});
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
return objGetter;
|
||||
}());
|
||||
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto val = temporaryEngine.evaluate ("let foo = Obj.getObj(); foo.mutate(); foo.mutate();", &res);
|
||||
expect (res.wasOk());
|
||||
|
||||
expect (numCalls == 2);
|
||||
}
|
||||
|
||||
beginTest ("Properties of registered native objects are enumerable");
|
||||
{
|
||||
auto obj = rawToUniquePtr (new DynamicObject);
|
||||
obj->setMethod ("methodA", nullptr);
|
||||
obj->setProperty ("one", 1);
|
||||
obj->setMethod ("methodB", nullptr);
|
||||
obj->setProperty ("hello", "world");
|
||||
obj->setMethod ("methodC", nullptr);
|
||||
obj->setProperty ("nested",
|
||||
std::invoke ([]
|
||||
{
|
||||
auto result = rawToUniquePtr (new DynamicObject);
|
||||
result->setProperty ("present", true);
|
||||
return result.release();
|
||||
}));
|
||||
|
||||
JavascriptEngine temporaryEngine;
|
||||
temporaryEngine.registerNativeObject ("obj", obj.release());
|
||||
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto val = temporaryEngine.evaluate ("JSON.stringify (obj);", &res);
|
||||
expect (res.wasOk());
|
||||
expectEquals (val.toString(), String (R"({"nested":{"present":true},"one":1,"hello":"world"})"));
|
||||
}
|
||||
|
||||
beginTest ("native objects survive being passed as arguments and return values");
|
||||
{
|
||||
JavascriptEngine temporaryEngine;
|
||||
|
||||
int numCalls = 0;
|
||||
|
||||
auto objWithProps = rawToUniquePtr (new DynamicObject);
|
||||
objWithProps->setProperty ("one", 1);
|
||||
objWithProps->setProperty ("hello", "world");
|
||||
objWithProps->setMethod ("nativeFn", [&numCalls] (const auto&)
|
||||
{
|
||||
++numCalls;
|
||||
return "called a native fn";
|
||||
});
|
||||
|
||||
auto objWithFn = rawToUniquePtr (new DynamicObject);
|
||||
|
||||
var passedToFn;
|
||||
objWithFn->setMethod ("fn", [&passedToFn] (const auto& v)
|
||||
{
|
||||
passedToFn = v.arguments[0];
|
||||
return passedToFn;
|
||||
});
|
||||
|
||||
temporaryEngine.registerNativeObject ("withProps", objWithProps.release());
|
||||
temporaryEngine.registerNativeObject ("withFn", objWithFn.release());
|
||||
|
||||
auto res = juce::Result::fail ("");
|
||||
const auto val = temporaryEngine.evaluate ("withFn.fn (withProps);", &res);
|
||||
expect (res.wasOk());
|
||||
|
||||
for (auto& v : { val, passedToFn })
|
||||
{
|
||||
expect (v.getProperty ("one", 0) == var { 1 });
|
||||
expect (v.getProperty ("hello", "") == var { "world" });
|
||||
expect (v.call ("nativeFn") == var ("called a native fn"));
|
||||
}
|
||||
|
||||
expect (numCalls == 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static JavascriptTests javascriptTests;
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#ifdef JUCE_JAVASCRIPT_H_INCLUDED
|
||||
/* When you add this cpp file to your project, you mustn't include it in a file where you've
|
||||
already included any other headers - just put it inside a file on its own, possibly with your config
|
||||
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
|
||||
header files that the compiler may be using.
|
||||
*/
|
||||
#error "Incorrect use of JUCE cpp file"
|
||||
#endif
|
||||
|
||||
#include "juce_javascript.h"
|
||||
|
||||
#ifdef CONFIG_BIGNUM
|
||||
JUCE_COMPILER_WARNING ("The QuickJS version embedded inside of JUCE does not support the CONFIG_BIGNUM setting.")
|
||||
#undef CONFIG_BIGNUM
|
||||
#endif
|
||||
|
||||
#define choc juce::detail::choc
|
||||
#include <juce_javascript/choc/javascript/choc_javascript_QuickJS.h>
|
||||
#undef choc
|
||||
|
||||
#include "detail/juce_QuickJSHelpers.h"
|
||||
|
||||
#include "javascript/juce_JSObject.cpp"
|
||||
#include "javascript/juce_JSCursor.cpp"
|
||||
#include "javascript/juce_JavascriptEngine.cpp"
|
||||
|
||||
#if JUCE_UNIT_TESTS
|
||||
#include "javascript/juce_Javascript_test.cpp"
|
||||
#endif
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this module, and is read by
|
||||
the Projucer to automatically generate project code that uses it.
|
||||
For details about the syntax and how to create or use a module, see the
|
||||
JUCE Module Format.md file.
|
||||
|
||||
|
||||
BEGIN_JUCE_MODULE_DECLARATION
|
||||
|
||||
ID: juce_javascript
|
||||
vendor: juce
|
||||
version: 8.0.7
|
||||
name: JUCE Javascript interpreter
|
||||
description: Javascript interpreter.
|
||||
website: http://www.juce.com/juce
|
||||
license: AGPLv3/Commercial
|
||||
minimumCppStandard: 17
|
||||
|
||||
dependencies: juce_core
|
||||
|
||||
END_JUCE_MODULE_DECLARATION
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
#define JUCE_JAVASCRIPT_H_INCLUDED
|
||||
|
||||
#include <juce_core/juce_core.h>
|
||||
|
||||
#include "javascript/juce_JSObject.h"
|
||||
#include "javascript/juce_JSCursor.h"
|
||||
#include "javascript/juce_JavascriptEngine.h"
|
||||
Reference in New Issue
Block a user