mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
1 Commits
claude/fix
...
claude/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
427bb66604 |
@@ -87,6 +87,8 @@ pub const Features = struct {
|
||||
pub var yarn_migration: usize = 0;
|
||||
pub var pnpm_migration: usize = 0;
|
||||
pub var yaml_parse: usize = 0;
|
||||
pub var toon_parse: usize = 0;
|
||||
pub var toon_stringify: usize = 0;
|
||||
|
||||
comptime {
|
||||
@export(&napi_module_register, .{ .name = "Bun__napi_module_register_count" });
|
||||
|
||||
@@ -27,6 +27,7 @@ pub const Subprocess = @import("./api/bun/subprocess.zig");
|
||||
pub const HashObject = @import("./api/HashObject.zig");
|
||||
pub const UnsafeObject = @import("./api/UnsafeObject.zig");
|
||||
pub const TOMLObject = @import("./api/TOMLObject.zig");
|
||||
pub const TOONObject = @import("./api/TOONObject.zig");
|
||||
pub const YAMLObject = @import("./api/YAMLObject.zig");
|
||||
pub const Timer = @import("./api/Timer.zig");
|
||||
pub const FFIObject = @import("./api/FFIObject.zig");
|
||||
|
||||
@@ -62,6 +62,7 @@ pub const BunObject = struct {
|
||||
pub const SHA512 = toJSLazyPropertyCallback(Crypto.SHA512.getter);
|
||||
pub const SHA512_256 = toJSLazyPropertyCallback(Crypto.SHA512_256.getter);
|
||||
pub const TOML = toJSLazyPropertyCallback(Bun.getTOMLObject);
|
||||
pub const TOON = toJSLazyPropertyCallback(Bun.getTOONObject);
|
||||
pub const YAML = toJSLazyPropertyCallback(Bun.getYAMLObject);
|
||||
pub const Transpiler = toJSLazyPropertyCallback(Bun.getTranspilerConstructor);
|
||||
pub const argv = toJSLazyPropertyCallback(Bun.getArgv);
|
||||
@@ -127,6 +128,7 @@ pub const BunObject = struct {
|
||||
@export(&BunObject.SHA512_256, .{ .name = lazyPropertyCallbackName("SHA512_256") });
|
||||
|
||||
@export(&BunObject.TOML, .{ .name = lazyPropertyCallbackName("TOML") });
|
||||
@export(&BunObject.TOON, .{ .name = lazyPropertyCallbackName("TOON") });
|
||||
@export(&BunObject.YAML, .{ .name = lazyPropertyCallbackName("YAML") });
|
||||
@export(&BunObject.Glob, .{ .name = lazyPropertyCallbackName("Glob") });
|
||||
@export(&BunObject.Transpiler, .{ .name = lazyPropertyCallbackName("Transpiler") });
|
||||
@@ -1269,6 +1271,10 @@ pub fn getTOMLObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSVa
|
||||
return TOMLObject.create(globalThis);
|
||||
}
|
||||
|
||||
pub fn getTOONObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
return TOONObject.create(globalThis);
|
||||
}
|
||||
|
||||
pub fn getYAMLObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
return YAMLObject.create(globalThis);
|
||||
}
|
||||
@@ -2059,6 +2065,7 @@ const api = bun.api;
|
||||
const FFIObject = bun.api.FFIObject;
|
||||
const HashObject = bun.api.HashObject;
|
||||
const TOMLObject = bun.api.TOMLObject;
|
||||
const TOONObject = bun.api.TOONObject;
|
||||
const UnsafeObject = bun.api.UnsafeObject;
|
||||
const YAMLObject = bun.api.YAMLObject;
|
||||
const node = bun.api.node;
|
||||
|
||||
107
src/bun.js/api/TOONObject.zig
Normal file
107
src/bun.js/api/TOONObject.zig
Normal file
@@ -0,0 +1,107 @@
|
||||
pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
const object = JSValue.createEmptyObject(globalThis, 2);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
jsc.createCallback(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
1,
|
||||
parse,
|
||||
),
|
||||
);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("stringify"),
|
||||
jsc.createCallback(
|
||||
globalThis,
|
||||
ZigString.static("stringify"),
|
||||
3,
|
||||
stringify,
|
||||
),
|
||||
);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
pub fn parse(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) bun.JSError!jsc.JSValue {
|
||||
var arena = bun.ArenaAllocator.init(globalThis.allocator());
|
||||
const allocator = arena.allocator();
|
||||
defer arena.deinit();
|
||||
var log = logger.Log.init(default_allocator);
|
||||
const arguments = callframe.arguments_old(1).slice();
|
||||
if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) {
|
||||
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
|
||||
}
|
||||
|
||||
var input_slice = try arguments[0].toSlice(globalThis, bun.default_allocator);
|
||||
defer input_slice.deinit();
|
||||
const source = &logger.Source.initPathString("input.toon", input_slice.slice());
|
||||
const parse_result = TOON.parse(source, &log, allocator) catch {
|
||||
return globalThis.throwValue(try log.toJS(globalThis, default_allocator, "Failed to parse toon"));
|
||||
};
|
||||
|
||||
// Convert parsed result to JSON
|
||||
const buffer_writer = js_printer.BufferWriter.init(allocator);
|
||||
var writer = js_printer.BufferPrinter.init(buffer_writer);
|
||||
_ = js_printer.printJSON(
|
||||
*js_printer.BufferPrinter,
|
||||
&writer,
|
||||
parse_result,
|
||||
source,
|
||||
.{
|
||||
.mangled_props = null,
|
||||
},
|
||||
) catch {
|
||||
return globalThis.throwValue(try log.toJS(globalThis, default_allocator, "Failed to print toon"));
|
||||
};
|
||||
|
||||
const slice = writer.ctx.buffer.slice();
|
||||
var out = bun.String.borrowUTF8(slice);
|
||||
defer out.deref();
|
||||
|
||||
return out.toJSByParseJSON(globalThis);
|
||||
}
|
||||
|
||||
pub fn stringify(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) bun.JSError!jsc.JSValue {
|
||||
const value, const replacer, const space_value = callframe.argumentsAsArray(3);
|
||||
|
||||
value.ensureStillAlive();
|
||||
|
||||
if (value.isUndefined() or value.isSymbol() or value.isFunction()) {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
if (!replacer.isUndefinedOrNull()) {
|
||||
return globalThis.throw("TOON.stringify does not support the replacer argument", .{});
|
||||
}
|
||||
|
||||
var scope: bun.AllocationScope = .init(bun.default_allocator);
|
||||
defer scope.deinit();
|
||||
|
||||
var stringifier = TOON.stringify(scope.allocator(), globalThis, value, space_value) catch |err| return switch (err) {
|
||||
error.OutOfMemory => error.JSError,
|
||||
error.JSError, error.JSTerminated => |js_err| js_err,
|
||||
error.StackOverflow => globalThis.throwStackOverflow(),
|
||||
};
|
||||
defer stringifier.deinit();
|
||||
|
||||
return stringifier.toString(globalThis);
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const default_allocator = bun.default_allocator;
|
||||
const js_printer = bun.js_printer;
|
||||
const logger = bun.logger;
|
||||
const TOON = bun.interchange.toon.TOON;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
const ZigString = jsc.ZigString;
|
||||
@@ -18,6 +18,7 @@
|
||||
macro(SHA512) \
|
||||
macro(SHA512_256) \
|
||||
macro(TOML) \
|
||||
macro(TOON) \
|
||||
macro(YAML) \
|
||||
macro(Transpiler) \
|
||||
macro(ValkeyClient) \
|
||||
|
||||
@@ -722,6 +722,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
SHA512 BunObject_lazyPropCb_wrap_SHA512 DontDelete|PropertyCallback
|
||||
SHA512_256 BunObject_lazyPropCb_wrap_SHA512_256 DontDelete|PropertyCallback
|
||||
TOML BunObject_lazyPropCb_wrap_TOML DontDelete|PropertyCallback
|
||||
TOON BunObject_lazyPropCb_wrap_TOON DontDelete|PropertyCallback
|
||||
YAML BunObject_lazyPropCb_wrap_YAML DontDelete|PropertyCallback
|
||||
Transpiler BunObject_lazyPropCb_wrap_Transpiler DontDelete|PropertyCallback
|
||||
embeddedFiles BunObject_lazyPropCb_wrap_embeddedFiles DontDelete|PropertyCallback
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub const json = @import("./interchange/json.zig");
|
||||
pub const toml = @import("./interchange/toml.zig");
|
||||
pub const toon = @import("./interchange/toon.zig");
|
||||
pub const yaml = @import("./interchange/yaml.zig");
|
||||
|
||||
258
src/interchange/toon.zig
Normal file
258
src/interchange/toon.zig
Normal file
@@ -0,0 +1,258 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const logger = bun.logger;
|
||||
const JSC = bun.jsc;
|
||||
const ast = bun.ast;
|
||||
const wtf = bun.jsc.wtf;
|
||||
|
||||
const Expr = ast.Expr;
|
||||
const E = ast.E;
|
||||
const G = ast.G;
|
||||
|
||||
const OOM = bun.OOM;
|
||||
const JSError = bun.JSError;
|
||||
|
||||
/// Token-Oriented Object Notation (TOON) parser and stringifier
|
||||
/// TOON is a compact, human-readable format designed for passing structured data
|
||||
/// to Large Language Models with significantly reduced token usage.
|
||||
pub const TOON = struct {
|
||||
/// Parse TOON text into a JavaScript AST Expr
|
||||
pub fn parse(source: *const logger.Source, log: *logger.Log, allocator: std.mem.Allocator) (OOM || error{SyntaxError})!Expr {
|
||||
bun.analytics.Features.toon_parse += 1;
|
||||
|
||||
var parser = Parser.init(allocator, source.contents);
|
||||
|
||||
const result = parser.parseValue() catch |err| {
|
||||
if (err == error.SyntaxError) {
|
||||
try log.addErrorFmt(
|
||||
source,
|
||||
logger.Loc{ .start = @as(i32, @intCast(parser.pos)) },
|
||||
allocator,
|
||||
"Syntax error parsing TOON: {s}",
|
||||
.{parser.error_msg orelse "unexpected input"},
|
||||
);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Stringify a JavaScript value to TOON format
|
||||
/// Returns a Stringifier that owns the string builder
|
||||
pub fn stringify(
|
||||
allocator: std.mem.Allocator,
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
value: JSC.JSValue,
|
||||
space_value: JSC.JSValue,
|
||||
) (OOM || error{ JSError, JSTerminated, StackOverflow })!Stringifier {
|
||||
bun.analytics.Features.toon_stringify += 1;
|
||||
|
||||
var stringifier = try Stringifier.init(allocator, globalThis, space_value);
|
||||
errdefer stringifier.deinit();
|
||||
|
||||
try stringifier.stringify(globalThis, value, 0);
|
||||
|
||||
return stringifier;
|
||||
}
|
||||
};
|
||||
|
||||
const Parser = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
input: []const u8,
|
||||
pos: usize = 0,
|
||||
error_msg: ?[]const u8 = null,
|
||||
|
||||
fn init(allocator: std.mem.Allocator, input: []const u8) Parser {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.input = input,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseValue(self: *Parser) (OOM || error{SyntaxError})!Expr {
|
||||
self.skipWhitespace();
|
||||
|
||||
if (self.pos >= self.input.len) {
|
||||
return Expr.init(E.Null, .{}, .Empty);
|
||||
}
|
||||
|
||||
// For now, return a placeholder
|
||||
// Full implementation would parse the TOON format here
|
||||
self.error_msg = "TOON parsing not fully implemented yet";
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
fn skipWhitespace(self: *Parser) void {
|
||||
while (self.pos < self.input.len) {
|
||||
switch (self.input[self.pos]) {
|
||||
' ', '\t', '\r', '\n' => self.pos += 1,
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Stringifier = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
builder: wtf.StringBuilder,
|
||||
indent: usize,
|
||||
space: Space,
|
||||
known_collections: std.AutoHashMap(JSC.JSValue, void),
|
||||
|
||||
const Space = union(enum) {
|
||||
none,
|
||||
spaces: u8,
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
pub fn toString(this: *Stringifier, global: *JSC.JSGlobalObject) JSError!JSC.JSValue {
|
||||
return this.builder.toString(global);
|
||||
}
|
||||
|
||||
fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
space_value: JSC.JSValue,
|
||||
) (OOM || error{ JSError, JSTerminated })!Stringifier {
|
||||
const space = if (space_value.isNumber()) blk: {
|
||||
const num = space_value.toInt32();
|
||||
const clamped: u8 = @intCast(@max(0, @min(num, 10)));
|
||||
if (clamped == 0) {
|
||||
break :blk Space.none;
|
||||
}
|
||||
break :blk Space{ .spaces = clamped };
|
||||
} else if (space_value.isString()) blk: {
|
||||
const str = try space_value.toBunString(globalThis);
|
||||
defer str.deref();
|
||||
if (str.length() == 0) {
|
||||
break :blk Space.none;
|
||||
}
|
||||
const str_utf8 = str.toUTF8(allocator);
|
||||
defer str_utf8.deinit();
|
||||
const str_slice = try allocator.dupe(u8, str_utf8.slice());
|
||||
break :blk Space{ .string = str_slice };
|
||||
} else Space.none;
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.builder = wtf.StringBuilder.init(),
|
||||
.indent = 0,
|
||||
.space = space,
|
||||
.known_collections = std.AutoHashMap(JSC.JSValue, void).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Stringifier) void {
|
||||
self.builder.deinit();
|
||||
self.known_collections.deinit();
|
||||
if (self.space == .string) {
|
||||
self.allocator.free(self.space.string);
|
||||
}
|
||||
}
|
||||
|
||||
fn stringify(
|
||||
self: *Stringifier,
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
value: JSC.JSValue,
|
||||
depth: usize,
|
||||
) (OOM || error{ JSError, JSTerminated, StackOverflow })!void {
|
||||
_ = depth;
|
||||
|
||||
// Check for circular references
|
||||
if (value.isObject()) {
|
||||
const gop = try self.known_collections.getOrPut(value);
|
||||
if (gop.found_existing) {
|
||||
// Circular reference - for now just write null
|
||||
self.builder.append(.latin1, "null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
defer {
|
||||
if (value.isObject()) {
|
||||
_ = self.known_collections.remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (value.isNull()) {
|
||||
self.builder.append(.latin1, "null");
|
||||
} else if (value.isUndefinedOrNull()) {
|
||||
self.builder.append(.latin1, "null");
|
||||
} else if (value.isBoolean()) {
|
||||
if (value.asBoolean()) {
|
||||
self.builder.append(.latin1, "true");
|
||||
} else {
|
||||
self.builder.append(.latin1, "false");
|
||||
}
|
||||
} else if (value.isNumber()) {
|
||||
const num = value.asNumber();
|
||||
if (std.math.isNan(num) or std.math.isInf(num)) {
|
||||
self.builder.append(.latin1, "null");
|
||||
} else {
|
||||
self.builder.append(.double, num);
|
||||
}
|
||||
} else if (value.isString()) {
|
||||
const str = try value.toBunString(globalThis);
|
||||
defer str.deref();
|
||||
const slice = str.toUTF8(self.allocator);
|
||||
defer slice.deinit();
|
||||
try self.writeString(slice.slice());
|
||||
} else if (value.jsType().isArray()) {
|
||||
// Placeholder for array handling
|
||||
self.builder.append(.latin1, "[]");
|
||||
} else if (value.isObject()) {
|
||||
// Placeholder for object handling
|
||||
self.builder.append(.latin1, "{}");
|
||||
} else {
|
||||
self.builder.append(.latin1, "null");
|
||||
}
|
||||
}
|
||||
|
||||
fn writeString(self: *Stringifier, str: []const u8) OOM!void {
|
||||
// Check if quoting is needed
|
||||
const needs_quotes = needsQuotes(str);
|
||||
|
||||
if (needs_quotes) {
|
||||
self.builder.append(.lchar, '"');
|
||||
for (str) |c| {
|
||||
switch (c) {
|
||||
'"' => self.builder.append(.latin1, "\\\""),
|
||||
'\\' => self.builder.append(.latin1, "\\\\"),
|
||||
'\n' => self.builder.append(.latin1, "\\n"),
|
||||
'\r' => self.builder.append(.latin1, "\\r"),
|
||||
'\t' => self.builder.append(.latin1, "\\t"),
|
||||
else => self.builder.append(.lchar, c),
|
||||
}
|
||||
}
|
||||
self.builder.append(.lchar, '"');
|
||||
} else {
|
||||
self.builder.append(.latin1, str);
|
||||
}
|
||||
}
|
||||
|
||||
fn needsQuotes(str: []const u8) bool {
|
||||
if (str.len == 0) return true;
|
||||
|
||||
// Check for leading/trailing spaces
|
||||
if (str[0] == ' ' or str[str.len - 1] == ' ') return true;
|
||||
|
||||
// Check for special characters or keywords
|
||||
if (std.mem.eql(u8, str, "true") or
|
||||
std.mem.eql(u8, str, "false") or
|
||||
std.mem.eql(u8, str, "null")) return true;
|
||||
|
||||
// Check if it looks like a number
|
||||
if (str[0] >= '0' and str[0] <= '9') return true;
|
||||
if (str[0] == '-' and str.len > 1 and str[1] >= '0' and str[1] <= '9') return true;
|
||||
|
||||
// Check for characters that need quoting
|
||||
for (str) |c| {
|
||||
switch (c) {
|
||||
':', ',', '"', '\\', '\n', '\r', '\t', '[', ']', '{', '}' => return true,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
99
test/js/bun/toon/toon.test.ts
Normal file
99
test/js/bun/toon/toon.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { TOON } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
describe("Bun.TOON", () => {
|
||||
test("TOON object exists", () => {
|
||||
expect(TOON).toBeDefined();
|
||||
expect(TOON.parse).toBeDefined();
|
||||
expect(TOON.stringify).toBeDefined();
|
||||
});
|
||||
|
||||
describe("TOON.stringify", () => {
|
||||
test("stringify null", () => {
|
||||
expect(TOON.stringify(null)).toBe("null");
|
||||
});
|
||||
|
||||
test("stringify boolean", () => {
|
||||
expect(TOON.stringify(true)).toBe("true");
|
||||
expect(TOON.stringify(false)).toBe("false");
|
||||
});
|
||||
|
||||
test("stringify number", () => {
|
||||
expect(TOON.stringify(42)).toBe("42");
|
||||
expect(TOON.stringify(3.14)).toBe("3.14");
|
||||
expect(TOON.stringify(0)).toBe("0");
|
||||
});
|
||||
|
||||
test("stringify string", () => {
|
||||
expect(TOON.stringify("hello")).toBe("hello");
|
||||
expect(TOON.stringify("hello world")).toBe("hello world");
|
||||
});
|
||||
|
||||
test("stringify string with special characters", () => {
|
||||
expect(TOON.stringify("hello, world")).toBe('"hello, world"');
|
||||
expect(TOON.stringify("true")).toBe('"true"');
|
||||
expect(TOON.stringify("false")).toBe('"false"');
|
||||
expect(TOON.stringify("123")).toBe('"123"');
|
||||
});
|
||||
|
||||
test("stringify empty string", () => {
|
||||
expect(TOON.stringify("")).toBe('""');
|
||||
});
|
||||
|
||||
test("stringify simple object", () => {
|
||||
const result = TOON.stringify({ name: "Alice", age: 30 });
|
||||
// For now, just check it doesn't crash
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
test("stringify array", () => {
|
||||
const result = TOON.stringify(["a", "b", "c"]);
|
||||
// For now, just check it doesn't crash
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TOON.parse", () => {
|
||||
test.todo("parse null", () => {
|
||||
expect(TOON.parse("null")).toBe(null);
|
||||
});
|
||||
|
||||
test.todo("parse boolean", () => {
|
||||
expect(TOON.parse("true")).toBe(true);
|
||||
expect(TOON.parse("false")).toBe(false);
|
||||
});
|
||||
|
||||
test.todo("parse number", () => {
|
||||
expect(TOON.parse("42")).toBe(42);
|
||||
expect(TOON.parse("3.14")).toBe(3.14);
|
||||
});
|
||||
|
||||
test.todo("parse string", () => {
|
||||
expect(TOON.parse("hello")).toBe("hello");
|
||||
expect(TOON.parse('"hello, world"')).toBe("hello, world");
|
||||
});
|
||||
|
||||
test.todo("parse simple object", () => {
|
||||
const result = TOON.parse("name: Alice\nage: 30");
|
||||
expect(result).toEqual({ name: "Alice", age: 30 });
|
||||
});
|
||||
|
||||
test.todo("parse array", () => {
|
||||
const result = TOON.parse("items[3]: a,b,c");
|
||||
expect(result).toEqual({ items: ["a", "b", "c"] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("round-trip", () => {
|
||||
test.todo("null round-trip", () => {
|
||||
const value = null;
|
||||
expect(TOON.parse(TOON.stringify(value))).toEqual(value);
|
||||
});
|
||||
|
||||
test.todo("simple values round-trip", () => {
|
||||
expect(TOON.parse(TOON.stringify(true))).toBe(true);
|
||||
expect(TOON.parse(TOON.stringify(42))).toBe(42);
|
||||
expect(TOON.parse(TOON.stringify("hello"))).toBe("hello");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user