Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocke
option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON)
option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON)

# Sanitizers
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)
Expand Down
4 changes: 4 additions & 0 deletions Polyfills/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ endif()

if(JSRUNTIMEHOST_POLYFILL_TEXTDECODER)
add_subdirectory(TextDecoder)
endif()

if(JSRUNTIMEHOST_POLYFILL_TEXTENCODER)
add_subdirectory(TextEncoder)
endif()
15 changes: 15 additions & 0 deletions Polyfills/TextEncoder/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set(SOURCES
"Include/Babylon/Polyfills/TextEncoder.h"
"Source/TextEncoder.cpp")

add_library(TextEncoder ${SOURCES})
warnings_as_errors(TextEncoder)

target_include_directories(TextEncoder PUBLIC "Include")

target_link_libraries(TextEncoder
PUBLIC napi
PUBLIC Foundation)

set_property(TARGET TextEncoder PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
9 changes: 9 additions & 0 deletions Polyfills/TextEncoder/Include/Babylon/Polyfills/TextEncoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <napi/env.h>
#include <Babylon/Api.h>

namespace Babylon::Polyfills::TextEncoder
{
void BABYLON_API Initialize(Napi::Env env);
}
28 changes: 28 additions & 0 deletions Polyfills/TextEncoder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# TextEncoder Polyfill

A C++ implementation of the [WHATWG Encoding API](https://encoding.spec.whatwg.org/) `TextEncoder` interface for use in Babylon Native JavaScript runtimes via [Napi](https://github.com/nodejs/node-addon-api).

This is the encoding counterpart to the `TextDecoder` polyfill in this same repository. Both polyfills exist primarily for older Chakra-based runtimes where the WHATWG Encoding Standard globals are not built in. On modern V8 / JSC / Hermes runtimes the constructor is already exposed and `Initialize()` is a no-op.

## Current State

### Supported

- Constructing `TextEncoder` with no argument (UTF-8 is the only encoding the spec mandates).
- The `encoding` accessor (always returns `"utf-8"`).
- `encode(input)` — returns a `Uint8Array` containing the UTF-8 bytes of `input`. Calling with no argument or `undefined` returns an empty `Uint8Array` (matches the spec, which defaults `input` to `""`).

### Not Supported

- Encodings other than UTF-8 — the `TextEncoder` constructor in the spec only accepts UTF-8 anyway, so this is not a deviation.
- `encodeInto(source, destination)` — not implemented. Babylon.js does not call this entry point; the (substantially more involved) UTF-16-code-unit accounting it would require is not justified by any current consumer. If a future consumer needs it, it can be added at that time.

## Usage

```javascript
const encoder = new TextEncoder();
encoder.encoding; // "utf-8"

encoder.encode("Hello"); // Uint8Array(5) [72,101,108,108,111]
encoder.encode(); // Uint8Array(0) []
```
72 changes: 72 additions & 0 deletions Polyfills/TextEncoder/Source/TextEncoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include <Babylon/Polyfills/TextEncoder.h>

#include <napi/napi.h>

#include <cstring>
#include <string>

namespace
{
class TextEncoder final : public Napi::ObjectWrap<TextEncoder>
{
public:
static void Initialize(Napi::Env env)
{
Napi::HandleScope scope{env};

static constexpr auto JS_TEXTENCODER_CONSTRUCTOR_NAME = "TextEncoder";
if (env.Global().Get(JS_TEXTENCODER_CONSTRUCTOR_NAME).IsUndefined())
{
Napi::Function func = DefineClass(
env,
JS_TEXTENCODER_CONSTRUCTOR_NAME,
{
InstanceAccessor("encoding", &TextEncoder::Encoding, nullptr),
InstanceMethod("encode", &TextEncoder::Encode),
});

env.Global().Set(JS_TEXTENCODER_CONSTRUCTOR_NAME, func);
}
}

explicit TextEncoder(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<TextEncoder>{info}
{
// The TextEncoder constructor takes no arguments. The encoding is
// always UTF-8 per the WHATWG Encoding Standard.
}

private:
Napi::Value Encoding(const Napi::CallbackInfo& info)
{
return Napi::String::New(info.Env(), "utf-8");
}

// encode(input = "") - returns a Uint8Array containing the UTF-8 bytes.
// Per spec, undefined input defaults to the empty string (not "undefined").
Napi::Value Encode(const Napi::CallbackInfo& info)
{
auto env = info.Env();
std::string utf8;
if (info.Length() > 0 && !info[0].IsUndefined())
{
utf8 = info[0].ToString().Utf8Value();
}

auto bytes = Napi::Uint8Array::New(env, utf8.size());
if (!utf8.empty())
{
std::memcpy(bytes.Data(), utf8.data(), utf8.size());
}
return bytes;
}
};
}

namespace Babylon::Polyfills::TextEncoder
{
void BABYLON_API Initialize(Napi::Env env)
{
::TextEncoder::Initialize(env);
}
}
1 change: 1 addition & 0 deletions Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ target_link_libraries(UnitTestsJNI
PRIVATE gtest_main
PRIVATE Blob
PRIVATE TextDecoder
PRIVATE TextEncoder
PRIVATE Performance)
1 change: 1 addition & 0 deletions Tests/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ target_link_libraries(UnitTests
PRIVATE Blob
PRIVATE Performance
PRIVATE TextDecoder
PRIVATE TextEncoder
${ADDITIONAL_LIBRARIES})

# See https://gitlab.kitware.com/cmake/cmake/-/issues/23543
Expand Down
38 changes: 38 additions & 0 deletions Tests/UnitTests/Scripts/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,44 @@ describe("TextDecoder", function () {
});
});

describe("TextEncoder", function () {
it("should expose encoding === 'utf-8'", function () {
const encoder = new TextEncoder();
expect(encoder.encoding).to.equal("utf-8");
});

it("should encode an ASCII string into UTF-8 bytes", function () {
const encoder = new TextEncoder();
const bytes = encoder.encode("Hello");
expect(Array.from(bytes)).to.eql([72, 101, 108, 108, 111]);
});

it("should return an empty Uint8Array when called with no argument", function () {
const encoder = new TextEncoder();
const bytes = encoder.encode();
expect(bytes.length).to.equal(0);
});

it("should return an empty Uint8Array for undefined input", function () {
const encoder = new TextEncoder();
const bytes = encoder.encode(undefined);
expect(bytes.length).to.equal(0);
});

it("should encode a multi-byte UTF-8 string", function () {
const encoder = new TextEncoder();
// "é" is U+00E9 -> 0xC3 0xA9 in UTF-8
const bytes = encoder.encode("é");
expect(Array.from(bytes)).to.eql([0xC3, 0xA9]);
});

it("should encode a string containing a null byte", function () {
const encoder = new TextEncoder();
const bytes = encoder.encode("H\0i");
expect(Array.from(bytes)).to.eql([72, 0, 105]);
});
});

function runTests() {
mocha.run((failures: number) => {
// Test program will wait for code to be set before exiting
Expand Down
2 changes: 2 additions & 0 deletions Tests/UnitTests/Shared/Shared.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <Babylon/Polyfills/XMLHttpRequest.h>
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Polyfills/TextDecoder.h>
#include <Babylon/Polyfills/TextEncoder.h>
#include <gtest/gtest.h>
#include <arcana/threading/blocking_concurrent_queue.h>
#include <atomic>
Expand Down Expand Up @@ -83,6 +84,7 @@ TEST(JavaScript, All)
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
Babylon::Polyfills::Blob::Initialize(env);
Babylon::Polyfills::TextDecoder::Initialize(env);
Babylon::Polyfills::TextEncoder::Initialize(env);

auto setExitCodeCallback = Napi::Function::New(
env, [&exitCodePromise](const Napi::CallbackInfo& info) {
Expand Down
Loading