mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Implement Bun.YAML.stringify (#22183)
### What does this PR do?
This PR adds `Bun.YAML.stringify`. The stringifier will double quote
strings only when necessary (looks for keywords, numbers, or containing
non-printable or escaped characters). Anchors and aliases are detected
by object equality, and anchor name is chosen from property name, array
item, or the root collection.
```js
import { YAML } from "bun"
YAML.stringify(null) // null
YAML.stringify("hello YAML"); // "hello YAML"
YAML.stringify("123.456"); // "\"123.456\""
// anchors and aliases
const userInfo = { name: "bun" };
const obj = { user1: { userInfo }, user2: { userInfo } };
YAML.stringify(obj, null, 2);
// # output
// user1:
// userInfo:
// &userInfo
// name: bun
// user2:
// userInfo:
// *userInfo
// will handle cycles
const obj = {};
obj.cycle = obj;
YAML.stringify(obj, null, 2);
// # output
// &root
// cycle:
// *root
// default no space
const obj = { one: { two: "three" } };
YAML.stringify(obj);
// # output
// {one: {two: three}}
```
### How did you verify your code works?
Added tests for basic use and edgecases
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Added YAML.stringify to the YAML API, producing YAML from JavaScript
values with quoting, anchors, and indentation support.
- Improvements
- YAML.parse now accepts a wider range of inputs, including Buffer,
ArrayBuffer, TypedArrays, DataView, Blob/File, and SharedArrayBuffer,
with better error propagation and stack protection.
- Tests
- Extensive new tests for YAML.parse and YAML.stringify across data
types, edge cases, anchors/aliases, deep nesting, and round-trip
scenarios.
- Chores
- Added a YAML stringify benchmark script covering multiple libraries
and data shapes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
407
bench/yaml/yaml-stringify.mjs
Normal file
407
bench/yaml/yaml-stringify.mjs
Normal file
@@ -0,0 +1,407 @@
|
||||
import { bench, group, run } from "../runner.mjs";
|
||||
import jsYaml from "js-yaml";
|
||||
import yaml from "yaml";
|
||||
|
||||
// Small object
|
||||
const smallObject = {
|
||||
name: "John Doe",
|
||||
age: 30,
|
||||
email: "john@example.com",
|
||||
active: true,
|
||||
};
|
||||
|
||||
// Medium object with nested structures
|
||||
const mediumObject = {
|
||||
company: "Acme Corp",
|
||||
employees: [
|
||||
{
|
||||
name: "John Doe",
|
||||
age: 30,
|
||||
position: "Developer",
|
||||
skills: ["JavaScript", "TypeScript", "Node.js"],
|
||||
},
|
||||
{
|
||||
name: "Jane Smith",
|
||||
age: 28,
|
||||
position: "Designer",
|
||||
skills: ["Figma", "Photoshop", "Illustrator"],
|
||||
},
|
||||
{
|
||||
name: "Bob Johnson",
|
||||
age: 35,
|
||||
position: "Manager",
|
||||
skills: ["Leadership", "Communication", "Planning"],
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
database: {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
name: "mydb",
|
||||
},
|
||||
cache: {
|
||||
enabled: true,
|
||||
ttl: 3600,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Large object with complex structures
|
||||
const largeObject = {
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deployment",
|
||||
metadata: {
|
||||
name: "nginx-deployment",
|
||||
labels: {
|
||||
app: "nginx",
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
replicas: 3,
|
||||
selector: {
|
||||
matchLabels: {
|
||||
app: "nginx",
|
||||
},
|
||||
},
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: "nginx",
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: "nginx",
|
||||
image: "nginx:1.14.2",
|
||||
ports: [
|
||||
{
|
||||
containerPort: 80,
|
||||
},
|
||||
],
|
||||
env: [
|
||||
{
|
||||
name: "ENV_VAR_1",
|
||||
value: "value1",
|
||||
},
|
||||
{
|
||||
name: "ENV_VAR_2",
|
||||
value: "value2",
|
||||
},
|
||||
],
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "config",
|
||||
mountPath: "/etc/nginx",
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
limits: {
|
||||
cpu: "1",
|
||||
memory: "1Gi",
|
||||
},
|
||||
requests: {
|
||||
cpu: "0.5",
|
||||
memory: "512Mi",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
volumes: [
|
||||
{
|
||||
name: "config",
|
||||
configMap: {
|
||||
name: "nginx-config",
|
||||
items: [
|
||||
{
|
||||
key: "nginx.conf",
|
||||
path: "nginx.conf",
|
||||
},
|
||||
{
|
||||
key: "mime.types",
|
||||
path: "mime.types",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
nodeSelector: {
|
||||
disktype: "ssd",
|
||||
},
|
||||
tolerations: [
|
||||
{
|
||||
key: "key1",
|
||||
operator: "Equal",
|
||||
value: "value1",
|
||||
effect: "NoSchedule",
|
||||
},
|
||||
{
|
||||
key: "key2",
|
||||
operator: "Exists",
|
||||
effect: "NoExecute",
|
||||
},
|
||||
],
|
||||
affinity: {
|
||||
nodeAffinity: {
|
||||
requiredDuringSchedulingIgnoredDuringExecution: {
|
||||
nodeSelectorTerms: [
|
||||
{
|
||||
matchExpressions: [
|
||||
{
|
||||
key: "kubernetes.io/e2e-az-name",
|
||||
operator: "In",
|
||||
values: ["e2e-az1", "e2e-az2"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
podAntiAffinity: {
|
||||
preferredDuringSchedulingIgnoredDuringExecution: [
|
||||
{
|
||||
weight: 100,
|
||||
podAffinityTerm: {
|
||||
labelSelector: {
|
||||
matchExpressions: [
|
||||
{
|
||||
key: "app",
|
||||
operator: "In",
|
||||
values: ["web-store"],
|
||||
},
|
||||
],
|
||||
},
|
||||
topologyKey: "kubernetes.io/hostname",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Object with anchors and references (after resolution)
|
||||
const objectWithAnchors = {
|
||||
defaults: {
|
||||
adapter: "postgresql",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
},
|
||||
development: {
|
||||
adapter: "postgresql",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "dev_db",
|
||||
},
|
||||
test: {
|
||||
adapter: "postgresql",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "test_db",
|
||||
},
|
||||
production: {
|
||||
adapter: "postgresql",
|
||||
host: "prod.example.com",
|
||||
port: 5432,
|
||||
database: "prod_db",
|
||||
},
|
||||
};
|
||||
|
||||
// Array of items
|
||||
const arrayObject = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Item 1",
|
||||
price: 10.99,
|
||||
tags: ["electronics", "gadgets"],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Item 2",
|
||||
price: 25.5,
|
||||
tags: ["books", "education"],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Item 3",
|
||||
price: 5.0,
|
||||
tags: ["food", "snacks"],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Item 4",
|
||||
price: 100.0,
|
||||
tags: ["electronics", "computers"],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Item 5",
|
||||
price: 15.75,
|
||||
tags: ["clothing", "accessories"],
|
||||
},
|
||||
];
|
||||
|
||||
// Multiline strings
|
||||
const multilineObject = {
|
||||
description:
|
||||
"This is a multiline string\nthat preserves line breaks\nand indentation.\n\nIt can contain multiple paragraphs\nand special characters: !@#$%^&*()\n",
|
||||
folded: "This is a folded string where line breaks are converted to spaces unless there are\nempty lines like above.",
|
||||
plain: "This is a plain string",
|
||||
quoted: 'This is a quoted string with "escapes"',
|
||||
literal: "This is a literal string with 'quotes'",
|
||||
};
|
||||
|
||||
// Numbers and special values
|
||||
const numbersObject = {
|
||||
integer: 42,
|
||||
negative: -17,
|
||||
float: 3.14159,
|
||||
scientific: 0.000123,
|
||||
infinity: Infinity,
|
||||
negativeInfinity: -Infinity,
|
||||
notANumber: NaN,
|
||||
octal: 493, // 0o755
|
||||
hex: 255, // 0xFF
|
||||
binary: 10, // 0b1010
|
||||
};
|
||||
|
||||
// Dates and timestamps
|
||||
const datesObject = {
|
||||
date: new Date("2024-01-15"),
|
||||
datetime: new Date("2024-01-15T10:30:00Z"),
|
||||
timestamp: new Date("2024-01-15T15:30:00.123456789Z"), // Adjusted for UTC-5
|
||||
canonical: new Date("2024-01-15T10:30:00.123456789Z"),
|
||||
};
|
||||
|
||||
// Stringify benchmarks
|
||||
group("stringify small object", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(smallObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(smallObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(smallObject);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify medium object", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(mediumObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(mediumObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(mediumObject);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify large object", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(largeObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(largeObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(largeObject);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify object with anchors", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(objectWithAnchors);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(objectWithAnchors);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(objectWithAnchors);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify array", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(arrayObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(arrayObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(arrayObject);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify object with multiline strings", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(multilineObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(multilineObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(multilineObject);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify object with numbers", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(numbersObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(numbersObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(numbersObject);
|
||||
});
|
||||
});
|
||||
|
||||
group("stringify object with dates", () => {
|
||||
if (typeof Bun !== "undefined" && Bun.YAML) {
|
||||
bench("Bun.YAML.stringify", () => {
|
||||
return Bun.YAML.stringify(datesObject);
|
||||
});
|
||||
}
|
||||
|
||||
bench("js-yaml.dump", () => {
|
||||
return jsYaml.dump(datesObject);
|
||||
});
|
||||
|
||||
bench("yaml.stringify", () => {
|
||||
return yaml.stringify(datesObject);
|
||||
});
|
||||
});
|
||||
|
||||
await run();
|
||||
@@ -198,6 +198,7 @@ src/bun.js/bindings/ServerRouteList.cpp
|
||||
src/bun.js/bindings/spawn.cpp
|
||||
src/bun.js/bindings/SQLClient.cpp
|
||||
src/bun.js/bindings/sqlite/JSSQLStatement.cpp
|
||||
src/bun.js/bindings/StringBuilderBinding.cpp
|
||||
src/bun.js/bindings/stripANSI.cpp
|
||||
src/bun.js/bindings/Strong.cpp
|
||||
src/bun.js/bindings/TextCodec.cpp
|
||||
|
||||
@@ -200,6 +200,7 @@ src/bun.js/bindings/sizes.zig
|
||||
src/bun.js/bindings/SourceProvider.zig
|
||||
src/bun.js/bindings/SourceType.zig
|
||||
src/bun.js/bindings/static_export.zig
|
||||
src/bun.js/bindings/StringBuilder.zig
|
||||
src/bun.js/bindings/SystemError.zig
|
||||
src/bun.js/bindings/TextCodec.zig
|
||||
src/bun.js/bindings/URL.zig
|
||||
|
||||
32
packages/bun-types/bun.d.ts
vendored
32
packages/bun-types/bun.d.ts
vendored
@@ -644,6 +644,38 @@ declare module "bun" {
|
||||
* ```
|
||||
*/
|
||||
export function parse(input: string): unknown;
|
||||
|
||||
/**
|
||||
* Convert a JavaScript value into a YAML string. Strings are double quoted if they contain keywords, non-printable or
|
||||
* escaped characters, or if a YAML parser would parse them as numbers. Anchors and aliases are inferred from objects, allowing cycles.
|
||||
*
|
||||
* @category Utilities
|
||||
*
|
||||
* @param input The JavaScript value to stringify.
|
||||
* @param replacer Currently not supported.
|
||||
* @param space A number for how many spaces each level of indentation gets, or a string used as indentation. The number is clamped between 0 and 10, and the first 10 characters of the string are used.
|
||||
* @returns A string containing the YAML document.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { YAML } from "bun";
|
||||
*
|
||||
* const input = {
|
||||
* abc: "def"
|
||||
* };
|
||||
* console.log(YAML.stringify(input));
|
||||
* // # output
|
||||
* // abc: def
|
||||
*
|
||||
* const cycle = {};
|
||||
* cycle.obj = cycle;
|
||||
* console.log(YAML.stringify(cycle));
|
||||
* // # output
|
||||
* // &root
|
||||
* // obj:
|
||||
* // *root
|
||||
*/
|
||||
export function stringify(input: unknown, replacer?: undefined | null, space?: string | number): string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
const object = JSValue.createEmptyObject(globalThis, 1);
|
||||
const object = JSValue.createEmptyObject(globalThis, 2);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
@@ -10,10 +10,898 @@ pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
parse,
|
||||
),
|
||||
);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("stringify"),
|
||||
jsc.createCallback(
|
||||
globalThis,
|
||||
ZigString.static("stringify"),
|
||||
3,
|
||||
stringify,
|
||||
),
|
||||
);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
pub fn stringify(global: *JSGlobalObject, callFrame: *jsc.CallFrame) JSError!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 global.throw("YAML.stringify does not support the replacer argument", .{});
|
||||
}
|
||||
|
||||
var scope: bun.AllocationScope = .init(bun.default_allocator);
|
||||
defer scope.deinit();
|
||||
|
||||
var stringifier: Stringifier = try .init(scope.allocator(), global, space_value);
|
||||
defer stringifier.deinit();
|
||||
|
||||
stringifier.findAnchorsAndAliases(global, value, .root) catch |err| return switch (err) {
|
||||
error.OutOfMemory, error.JSError => |js_err| js_err,
|
||||
error.StackOverflow => global.throwStackOverflow(),
|
||||
};
|
||||
|
||||
stringifier.stringify(global, value) catch |err| return switch (err) {
|
||||
error.OutOfMemory, error.JSError => |js_err| js_err,
|
||||
error.StackOverflow => global.throwStackOverflow(),
|
||||
};
|
||||
|
||||
return stringifier.builder.toString(global);
|
||||
}
|
||||
|
||||
const Stringifier = struct {
|
||||
stack_check: bun.StackCheck,
|
||||
builder: wtf.StringBuilder,
|
||||
indent: usize,
|
||||
|
||||
known_collections: std.AutoHashMap(JSValue, AnchorAlias),
|
||||
array_item_counter: usize,
|
||||
prop_names: bun.StringHashMap(usize),
|
||||
|
||||
space: Space,
|
||||
|
||||
pub const Space = union(enum) {
|
||||
minified,
|
||||
number: u32,
|
||||
str: String,
|
||||
|
||||
pub fn init(global: *JSGlobalObject, space_value: JSValue) JSError!Space {
|
||||
if (space_value.isNumber()) {
|
||||
var num = space_value.toInt32();
|
||||
num = @max(0, @min(num, 10));
|
||||
if (num == 0) {
|
||||
return .minified;
|
||||
}
|
||||
return .{ .number = @intCast(num) };
|
||||
}
|
||||
|
||||
if (space_value.isString()) {
|
||||
const str = try space_value.toBunString(global);
|
||||
if (str.length() == 0) {
|
||||
str.deref();
|
||||
return .minified;
|
||||
}
|
||||
return .{ .str = str };
|
||||
}
|
||||
|
||||
return .minified;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *const Space) void {
|
||||
switch (this.*) {
|
||||
.minified => {},
|
||||
.number => {},
|
||||
.str => |str| {
|
||||
str.deref();
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const AnchorOrigin = enum {
|
||||
root,
|
||||
array_item,
|
||||
prop_value,
|
||||
};
|
||||
|
||||
const AnchorAlias = struct {
|
||||
anchored: bool,
|
||||
used: bool,
|
||||
name: Name,
|
||||
|
||||
pub fn init(origin: ValueOrigin) AnchorAlias {
|
||||
return .{
|
||||
.anchored = false,
|
||||
.used = false,
|
||||
.name = switch (origin) {
|
||||
.root => .root,
|
||||
.array_item => .{ .array_item = 0 },
|
||||
.prop_value => .{ .prop_value = .{ .prop_name = origin.prop_value, .counter = 0 } },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub const Name = union(AnchorOrigin) {
|
||||
// only one root anchor is possible
|
||||
root,
|
||||
array_item: usize,
|
||||
prop_value: struct {
|
||||
prop_name: String,
|
||||
// added after the name
|
||||
counter: usize,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, global: *JSGlobalObject, space_value: JSValue) JSError!Stringifier {
|
||||
var prop_names: bun.StringHashMap(usize) = .init(allocator);
|
||||
// always rename anchors named "root" to avoid collision with
|
||||
// root anchor/alias
|
||||
try prop_names.put("root", 0);
|
||||
|
||||
return .{
|
||||
.stack_check = .init(),
|
||||
.builder = .init(),
|
||||
.indent = 0,
|
||||
.known_collections = .init(allocator),
|
||||
.array_item_counter = 0,
|
||||
.prop_names = prop_names,
|
||||
.space = try .init(global, space_value),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Stringifier) void {
|
||||
this.builder.deinit();
|
||||
this.known_collections.deinit();
|
||||
this.prop_names.deinit();
|
||||
this.space.deinit();
|
||||
}
|
||||
|
||||
const ValueOrigin = union(AnchorOrigin) {
|
||||
root,
|
||||
array_item,
|
||||
prop_value: String,
|
||||
};
|
||||
|
||||
pub fn findAnchorsAndAliases(this: *Stringifier, global: *JSGlobalObject, value: JSValue, origin: ValueOrigin) StringifyError!void {
|
||||
if (!this.stack_check.isSafeToRecurse()) {
|
||||
return error.StackOverflow;
|
||||
}
|
||||
|
||||
const unwrapped = try value.unwrapBoxedPrimitive(global);
|
||||
|
||||
if (unwrapped.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped.isNumber()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped.isBigInt()) {
|
||||
return global.throw("YAML.stringify cannot serialize BigInt", .{});
|
||||
}
|
||||
|
||||
if (unwrapped.isBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped.isString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comptime Environment.ci_assert) {
|
||||
bun.assertWithLocation(unwrapped.isObject(), @src());
|
||||
}
|
||||
|
||||
const object_entry = try this.known_collections.getOrPut(unwrapped);
|
||||
if (object_entry.found_existing) {
|
||||
// this will become an alias. increment counters here because
|
||||
// now the anchor/alias is confirmed used.
|
||||
|
||||
if (object_entry.value_ptr.used) {
|
||||
return;
|
||||
}
|
||||
|
||||
object_entry.value_ptr.used = true;
|
||||
|
||||
switch (object_entry.value_ptr.name) {
|
||||
.root => {
|
||||
// only one possible
|
||||
},
|
||||
.array_item => |*counter| {
|
||||
counter.* = this.array_item_counter;
|
||||
this.array_item_counter += 1;
|
||||
},
|
||||
.prop_value => |*prop_value| {
|
||||
const name_entry = try this.prop_names.getOrPut(prop_value.prop_name.byteSlice());
|
||||
if (name_entry.found_existing) {
|
||||
name_entry.value_ptr.* += 1;
|
||||
} else {
|
||||
name_entry.value_ptr.* = 0;
|
||||
}
|
||||
|
||||
prop_value.counter = name_entry.value_ptr.*;
|
||||
},
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
object_entry.value_ptr.* = .init(origin);
|
||||
|
||||
if (unwrapped.isArray()) {
|
||||
var iter = try unwrapped.arrayIterator(global);
|
||||
while (try iter.next()) |item| {
|
||||
if (item.isUndefined() or item.isSymbol() or item.isFunction()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try this.findAnchorsAndAliases(global, item, .array_item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var iter: jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }) = try .init(
|
||||
global,
|
||||
try unwrapped.toObject(global),
|
||||
);
|
||||
defer iter.deinit();
|
||||
|
||||
while (try iter.next()) |prop_name| {
|
||||
if (iter.value.isUndefined() or iter.value.isSymbol() or iter.value.isFunction()) {
|
||||
continue;
|
||||
}
|
||||
try this.findAnchorsAndAliases(global, iter.value, .{ .prop_value = prop_name });
|
||||
}
|
||||
}
|
||||
|
||||
const StringifyError = JSError || bun.StackOverflow;
|
||||
|
||||
pub fn stringify(this: *Stringifier, global: *JSGlobalObject, value: JSValue) StringifyError!void {
|
||||
if (!this.stack_check.isSafeToRecurse()) {
|
||||
return error.StackOverflow;
|
||||
}
|
||||
|
||||
const unwrapped = try value.unwrapBoxedPrimitive(global);
|
||||
|
||||
if (unwrapped.isNull()) {
|
||||
this.builder.append(.latin1, "null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped.isNumber()) {
|
||||
if (unwrapped.isInt32()) {
|
||||
this.builder.append(.int, unwrapped.asInt32());
|
||||
return;
|
||||
}
|
||||
|
||||
const num = unwrapped.asNumber();
|
||||
if (std.math.isNegativeInf(num)) {
|
||||
this.builder.append(.latin1, "-.inf");
|
||||
// } else if (std.math.isPositiveInf(num)) {
|
||||
// builder.append(.latin1, "+.inf");
|
||||
} else if (std.math.isInf(num)) {
|
||||
this.builder.append(.latin1, ".inf");
|
||||
} else if (std.math.isNan(num)) {
|
||||
this.builder.append(.latin1, ".nan");
|
||||
} else if (std.math.isNegativeZero(num)) {
|
||||
this.builder.append(.latin1, "-0");
|
||||
} else if (std.math.isPositiveZero(num)) {
|
||||
this.builder.append(.latin1, "+0");
|
||||
} else {
|
||||
this.builder.append(.double, num);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped.isBigInt()) {
|
||||
return global.throw("YAML.stringify cannot serialize BigInt", .{});
|
||||
}
|
||||
|
||||
if (unwrapped.isBoolean()) {
|
||||
if (unwrapped.asBoolean()) {
|
||||
this.builder.append(.latin1, "true");
|
||||
} else {
|
||||
this.builder.append(.latin1, "false");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped.isString()) {
|
||||
const value_str = try unwrapped.toBunString(global);
|
||||
defer value_str.deref();
|
||||
|
||||
this.appendString(value_str);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comptime Environment.ci_assert) {
|
||||
bun.assertWithLocation(unwrapped.isObject(), @src());
|
||||
}
|
||||
|
||||
const has_anchor: ?*AnchorAlias = has_anchor: {
|
||||
const anchor = this.known_collections.getPtr(unwrapped) orelse {
|
||||
break :has_anchor null;
|
||||
};
|
||||
|
||||
if (!anchor.used) {
|
||||
break :has_anchor null;
|
||||
}
|
||||
|
||||
break :has_anchor anchor;
|
||||
};
|
||||
|
||||
if (has_anchor) |anchor| {
|
||||
this.builder.append(.lchar, if (anchor.anchored) '*' else '&');
|
||||
|
||||
switch (anchor.name) {
|
||||
.root => {
|
||||
this.builder.append(.latin1, "root");
|
||||
},
|
||||
.array_item => {
|
||||
this.builder.append(.latin1, "item");
|
||||
this.builder.append(.usize, anchor.name.array_item);
|
||||
},
|
||||
.prop_value => |prop_value| {
|
||||
if (prop_value.prop_name.length() == 0) {
|
||||
this.builder.append(.latin1, "value");
|
||||
this.builder.append(.usize, prop_value.counter);
|
||||
} else {
|
||||
this.builder.append(.string, anchor.name.prop_value.prop_name);
|
||||
if (anchor.name.prop_value.counter != 0) {
|
||||
this.builder.append(.usize, anchor.name.prop_value.counter);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (anchor.anchored) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.space) {
|
||||
.minified => {
|
||||
this.builder.append(.lchar, ' ');
|
||||
},
|
||||
.number, .str => {
|
||||
this.newline();
|
||||
},
|
||||
}
|
||||
anchor.anchored = true;
|
||||
}
|
||||
|
||||
if (unwrapped.isArray()) {
|
||||
var iter = try unwrapped.arrayIterator(global);
|
||||
|
||||
if (iter.len == 0) {
|
||||
this.builder.append(.latin1, "[]");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.space) {
|
||||
.minified => {
|
||||
this.builder.append(.lchar, '[');
|
||||
var first = true;
|
||||
while (try iter.next()) |item| {
|
||||
if (item.isUndefined() or item.isSymbol() or item.isFunction()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
this.builder.append(.lchar, ',');
|
||||
}
|
||||
first = false;
|
||||
|
||||
try this.stringify(global, item);
|
||||
}
|
||||
this.builder.append(.lchar, ']');
|
||||
},
|
||||
.number, .str => {
|
||||
this.builder.ensureUnusedCapacity(iter.len * "- ".len);
|
||||
var first = true;
|
||||
while (try iter.next()) |item| {
|
||||
if (item.isUndefined() or item.isSymbol() or item.isFunction()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
this.newline();
|
||||
}
|
||||
first = false;
|
||||
|
||||
this.builder.append(.latin1, "- ");
|
||||
|
||||
// don't need to print a newline here for any value
|
||||
|
||||
this.indent += 1;
|
||||
try this.stringify(global, item);
|
||||
this.indent -= 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var iter: jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }) = try .init(
|
||||
global,
|
||||
try unwrapped.toObject(global),
|
||||
);
|
||||
defer iter.deinit();
|
||||
|
||||
if (iter.len == 0) {
|
||||
this.builder.append(.latin1, "{}");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.space) {
|
||||
.minified => {
|
||||
this.builder.append(.lchar, '{');
|
||||
var first = true;
|
||||
while (try iter.next()) |prop_name| {
|
||||
if (iter.value.isUndefined() or iter.value.isSymbol() or iter.value.isFunction()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
this.builder.append(.lchar, ',');
|
||||
}
|
||||
first = false;
|
||||
|
||||
this.appendString(prop_name);
|
||||
this.builder.append(.latin1, ": ");
|
||||
|
||||
try this.stringify(global, iter.value);
|
||||
}
|
||||
this.builder.append(.lchar, '}');
|
||||
},
|
||||
.number, .str => {
|
||||
this.builder.ensureUnusedCapacity(iter.len * ": ".len);
|
||||
|
||||
var first = true;
|
||||
while (try iter.next()) |prop_name| {
|
||||
if (iter.value.isUndefined() or iter.value.isSymbol() or iter.value.isFunction()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
this.newline();
|
||||
}
|
||||
first = false;
|
||||
|
||||
this.appendString(prop_name);
|
||||
this.builder.append(.latin1, ": ");
|
||||
|
||||
this.indent += 1;
|
||||
|
||||
if (propValueNeedsNewline(iter.value)) {
|
||||
this.newline();
|
||||
}
|
||||
|
||||
try this.stringify(global, iter.value);
|
||||
this.indent -= 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Does this object property value need a newline? True for arrays and objects.
|
||||
fn propValueNeedsNewline(value: JSValue) bool {
|
||||
return !value.isNumber() and !value.isBoolean() and !value.isNull() and !value.isString();
|
||||
}
|
||||
|
||||
fn newline(this: *Stringifier) void {
|
||||
const indent_count = this.indent;
|
||||
|
||||
switch (this.space) {
|
||||
.minified => {},
|
||||
.number => |space_num| {
|
||||
this.builder.append(.lchar, '\n');
|
||||
this.builder.ensureUnusedCapacity(indent_count * space_num);
|
||||
for (0..indent_count * space_num) |_| {
|
||||
this.builder.append(.lchar, ' ');
|
||||
}
|
||||
},
|
||||
.str => |space_str| {
|
||||
this.builder.append(.lchar, '\n');
|
||||
|
||||
const clamped = if (space_str.length() > 10)
|
||||
space_str.substringWithLen(0, 10)
|
||||
else
|
||||
space_str;
|
||||
|
||||
this.builder.ensureUnusedCapacity(indent_count * clamped.length());
|
||||
for (0..indent_count) |_| {
|
||||
this.builder.append(.string, clamped);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn appendDoubleQuotedString(this: *Stringifier, str: String) void {
|
||||
this.builder.append(.lchar, '"');
|
||||
|
||||
for (0..str.length()) |i| {
|
||||
const c = str.charAt(i);
|
||||
|
||||
switch (c) {
|
||||
0x00 => this.builder.append(.latin1, "\\0"),
|
||||
0x01 => this.builder.append(.latin1, "\\x01"),
|
||||
0x02 => this.builder.append(.latin1, "\\x02"),
|
||||
0x03 => this.builder.append(.latin1, "\\x03"),
|
||||
0x04 => this.builder.append(.latin1, "\\x04"),
|
||||
0x05 => this.builder.append(.latin1, "\\x05"),
|
||||
0x06 => this.builder.append(.latin1, "\\x06"),
|
||||
0x07 => this.builder.append(.latin1, "\\a"), // bell
|
||||
0x08 => this.builder.append(.latin1, "\\b"), // backspace
|
||||
0x09 => this.builder.append(.latin1, "\\t"), // tab
|
||||
0x0a => this.builder.append(.latin1, "\\n"), // line feed
|
||||
0x0b => this.builder.append(.latin1, "\\v"), // vertical tab
|
||||
0x0c => this.builder.append(.latin1, "\\f"), // form feed
|
||||
0x0d => this.builder.append(.latin1, "\\r"), // carriage return
|
||||
0x0e => this.builder.append(.latin1, "\\x0e"),
|
||||
0x0f => this.builder.append(.latin1, "\\x0f"),
|
||||
0x10 => this.builder.append(.latin1, "\\x10"),
|
||||
0x11 => this.builder.append(.latin1, "\\x11"),
|
||||
0x12 => this.builder.append(.latin1, "\\x12"),
|
||||
0x13 => this.builder.append(.latin1, "\\x13"),
|
||||
0x14 => this.builder.append(.latin1, "\\x14"),
|
||||
0x15 => this.builder.append(.latin1, "\\x15"),
|
||||
0x16 => this.builder.append(.latin1, "\\x16"),
|
||||
0x17 => this.builder.append(.latin1, "\\x17"),
|
||||
0x18 => this.builder.append(.latin1, "\\x18"),
|
||||
0x19 => this.builder.append(.latin1, "\\x19"),
|
||||
0x1a => this.builder.append(.latin1, "\\x1a"),
|
||||
0x1b => this.builder.append(.latin1, "\\e"), // escape
|
||||
0x1c => this.builder.append(.latin1, "\\x1c"),
|
||||
0x1d => this.builder.append(.latin1, "\\x1d"),
|
||||
0x1e => this.builder.append(.latin1, "\\x1e"),
|
||||
0x1f => this.builder.append(.latin1, "\\x1f"),
|
||||
0x22 => this.builder.append(.latin1, "\\\""), // "
|
||||
0x5c => this.builder.append(.latin1, "\\\\"), // \
|
||||
0x7f => this.builder.append(.latin1, "\\x7f"), // delete
|
||||
0x85 => this.builder.append(.latin1, "\\N"), // next line
|
||||
0xa0 => this.builder.append(.latin1, "\\_"), // non-breaking space
|
||||
0xa8 => this.builder.append(.latin1, "\\L"), // line separator
|
||||
0xa9 => this.builder.append(.latin1, "\\P"), // paragraph separator
|
||||
|
||||
0x20...0x21,
|
||||
0x23...0x5b,
|
||||
0x5d...0x7e,
|
||||
0x80...0x84,
|
||||
0x86...0x9f,
|
||||
0xa1...0xa7,
|
||||
0xaa...std.math.maxInt(u16),
|
||||
=> this.builder.append(.uchar, c),
|
||||
}
|
||||
}
|
||||
|
||||
this.builder.append(.lchar, '"');
|
||||
}
|
||||
|
||||
fn appendString(this: *Stringifier, str: String) void {
|
||||
if (stringNeedsQuotes(str)) {
|
||||
this.appendDoubleQuotedString(str);
|
||||
return;
|
||||
}
|
||||
this.builder.append(.string, str);
|
||||
}
|
||||
|
||||
fn stringNeedsQuotes(str: String) bool {
|
||||
if (str.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (str.charAt(str.length() - 1)) {
|
||||
// whitespace characters
|
||||
' ',
|
||||
'\t',
|
||||
'\n',
|
||||
'\r',
|
||||
=> return true,
|
||||
else => {},
|
||||
}
|
||||
|
||||
switch (str.charAt(0)) {
|
||||
// starting with indicators or whitespace requires quotes
|
||||
'&',
|
||||
'*',
|
||||
'?',
|
||||
'|',
|
||||
'-',
|
||||
'<',
|
||||
'>',
|
||||
'!',
|
||||
'%',
|
||||
'@',
|
||||
' ',
|
||||
'\t',
|
||||
'\n',
|
||||
'\r',
|
||||
'#',
|
||||
=> return true,
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
const keywords = &.{
|
||||
"true",
|
||||
"True",
|
||||
"TRUE",
|
||||
"false",
|
||||
"False",
|
||||
"FALSE",
|
||||
"yes",
|
||||
"Yes",
|
||||
"YES",
|
||||
"no",
|
||||
"No",
|
||||
"NO",
|
||||
"on",
|
||||
"On",
|
||||
"ON",
|
||||
"off",
|
||||
"Off",
|
||||
"OFF",
|
||||
"n",
|
||||
"N",
|
||||
"y",
|
||||
"Y",
|
||||
"null",
|
||||
"Null",
|
||||
"NULL",
|
||||
"~",
|
||||
".inf",
|
||||
".Inf",
|
||||
".INF",
|
||||
".nan",
|
||||
".NaN",
|
||||
".NAN",
|
||||
};
|
||||
|
||||
inline for (keywords) |keyword| {
|
||||
if (str.eqlComptime(keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < str.length()) {
|
||||
switch (str.charAt(i)) {
|
||||
// flow indicators need to be quoted always
|
||||
'{',
|
||||
'}',
|
||||
'[',
|
||||
']',
|
||||
',',
|
||||
=> return true,
|
||||
|
||||
':',
|
||||
=> {
|
||||
if (i + 1 < str.length()) {
|
||||
switch (str.charAt(i + 1)) {
|
||||
' ',
|
||||
'\t',
|
||||
'\n',
|
||||
'\r',
|
||||
=> return true,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
},
|
||||
|
||||
'#',
|
||||
'`',
|
||||
'\'',
|
||||
=> return true,
|
||||
|
||||
'-' => {
|
||||
if (i + 2 < str.length() and str.charAt(i + 1) == '-' and str.charAt(i + 2) == '-') {
|
||||
if (i + 3 >= str.length()) {
|
||||
return true;
|
||||
}
|
||||
switch (str.charAt(i + 3)) {
|
||||
' ',
|
||||
'\t',
|
||||
'\r',
|
||||
'\n',
|
||||
'[',
|
||||
']',
|
||||
'{',
|
||||
'}',
|
||||
',',
|
||||
=> return true,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (i == 0 and stringIsNumber(str, &i)) {
|
||||
return true;
|
||||
}
|
||||
i += 1;
|
||||
},
|
||||
'.' => {
|
||||
if (i + 2 < str.length() and str.charAt(i + 1) == '.' and str.charAt(i + 2) == '.') {
|
||||
if (i + 3 >= str.length()) {
|
||||
return true;
|
||||
}
|
||||
switch (str.charAt(i + 3)) {
|
||||
' ',
|
||||
'\t',
|
||||
'\r',
|
||||
'\n',
|
||||
'[',
|
||||
']',
|
||||
'{',
|
||||
'}',
|
||||
',',
|
||||
=> return true,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (i == 0 and stringIsNumber(str, &i)) {
|
||||
return true;
|
||||
}
|
||||
i += 1;
|
||||
},
|
||||
|
||||
'0'...'9' => {
|
||||
if (i == 0 and stringIsNumber(str, &i)) {
|
||||
return true;
|
||||
}
|
||||
i += 1;
|
||||
},
|
||||
|
||||
0x00...0x1f,
|
||||
0x22,
|
||||
0x7f,
|
||||
0x85,
|
||||
0xa0,
|
||||
0xa8,
|
||||
0xa9,
|
||||
=> return true,
|
||||
|
||||
else => {
|
||||
i += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn stringIsNumber(str: String, offset: *usize) bool {
|
||||
const start = offset.*;
|
||||
var i = start;
|
||||
|
||||
var @"+" = false;
|
||||
var @"-" = false;
|
||||
var e = false;
|
||||
var dot = false;
|
||||
|
||||
var base: enum { dec, hex, oct } = .dec;
|
||||
|
||||
next: switch (str.charAt(i)) {
|
||||
'.' => {
|
||||
if (dot or base != .dec) {
|
||||
offset.* = i;
|
||||
return false;
|
||||
}
|
||||
dot = true;
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
'+' => {
|
||||
if (@"+") {
|
||||
offset.* = i;
|
||||
return false;
|
||||
}
|
||||
@"+" = true;
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
'-' => {
|
||||
if (@"-") {
|
||||
offset.* = i;
|
||||
return false;
|
||||
}
|
||||
@"-" = true;
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
'0' => {
|
||||
if (i == start) {
|
||||
if (i + 1 < str.length()) {
|
||||
const nc = str.charAt(i + 1);
|
||||
if (nc == 'x' or nc == 'X') {
|
||||
base = .hex;
|
||||
} else if (nc == 'o' or nc == 'O') {
|
||||
base = .oct;
|
||||
} else {
|
||||
offset.* = i;
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
'e',
|
||||
'E',
|
||||
=> {
|
||||
if (base == .oct or (e and base == .dec)) {
|
||||
offset.* = i;
|
||||
return false;
|
||||
}
|
||||
e = true;
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
'a'...'d',
|
||||
'f',
|
||||
'A'...'D',
|
||||
'F',
|
||||
=> {
|
||||
if (base != .hex) {
|
||||
offset.* = i;
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
'1'...'9' => {
|
||||
i += 1;
|
||||
if (i < str.length()) {
|
||||
continue :next str.charAt(i);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
else => {
|
||||
offset.* = i;
|
||||
return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn parse(
|
||||
global: *jsc.JSGlobalObject,
|
||||
callFrame: *jsc.CallFrame,
|
||||
@@ -23,8 +911,10 @@ pub fn parse(
|
||||
|
||||
const input_value = callFrame.argumentsAsArray(1)[0];
|
||||
|
||||
const input_str = try input_value.toBunString(global);
|
||||
const input = input_str.toSlice(arena.allocator());
|
||||
const input: jsc.Node.BlobOrStringOrBuffer = try jsc.Node.BlobOrStringOrBuffer.fromJS(global, arena.allocator(), input_value) orelse input: {
|
||||
const str = try input_value.toBunString(global);
|
||||
break :input .{ .string_or_buffer = .{ .string = str.toSlice(arena.allocator()) } };
|
||||
};
|
||||
defer input.deinit();
|
||||
|
||||
var log = logger.Log.init(bun.default_allocator);
|
||||
@@ -75,12 +965,18 @@ const ParserCtx = struct {
|
||||
ctx.result = .zero;
|
||||
return;
|
||||
},
|
||||
error.StackOverflow => {
|
||||
ctx.result = ctx.global.throwStackOverflow() catch .zero;
|
||||
return;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toJS(ctx: *ParserCtx, args: *MarkedArgumentBuffer, expr: Expr) JSError!JSValue {
|
||||
const ToJSError = JSError || bun.StackOverflow;
|
||||
|
||||
pub fn toJS(ctx: *ParserCtx, args: *MarkedArgumentBuffer, expr: Expr) ToJSError!JSValue {
|
||||
if (!ctx.stack_check.isSafeToRecurse()) {
|
||||
return ctx.global.throwStackOverflow();
|
||||
return error.StackOverflow;
|
||||
}
|
||||
switch (expr.data) {
|
||||
.e_null => return .null,
|
||||
@@ -143,7 +1039,9 @@ const ParserCtx = struct {
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const JSError = bun.JSError;
|
||||
const String = bun.String;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const logger = bun.logger;
|
||||
const YAML = bun.interchange.yaml.YAML;
|
||||
@@ -156,3 +1054,4 @@ const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
const MarkedArgumentBuffer = jsc.MarkedArgumentBuffer;
|
||||
const ZigString = jsc.ZigString;
|
||||
const wtf = bun.jsc.wtf;
|
||||
|
||||
@@ -717,6 +717,21 @@ WTF::String BunString::toWTFString() const
|
||||
return WTF::String();
|
||||
}
|
||||
|
||||
void BunString::appendToBuilder(WTF::StringBuilder& builder) const
|
||||
{
|
||||
if (this->tag == BunStringTag::WTFStringImpl) {
|
||||
builder.append(this->impl.wtf);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->tag == BunStringTag::ZigString || this->tag == BunStringTag::StaticZigString) {
|
||||
Zig::appendToBuilder(this->impl.zig, builder);
|
||||
return;
|
||||
}
|
||||
|
||||
// append nothing for BunStringTag::Dead and BunStringTag::Empty
|
||||
}
|
||||
|
||||
WTF::String BunString::toWTFString(ZeroCopyTag) const
|
||||
{
|
||||
if (this->tag == BunStringTag::ZigString) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// TODO determine size and alignment automatically
|
||||
const size = 56;
|
||||
const size = if (Environment.allow_assert or Environment.enable_asan) 56 else 8;
|
||||
const alignment = 8;
|
||||
|
||||
/// Binding for JSC::CatchScope. This should be used rarely, only at translation boundaries between
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
using JSC::CatchScope;
|
||||
|
||||
#if ENABLE(EXCEPTION_SCOPE_VERIFICATION)
|
||||
#define ExpectedCatchScopeSize 56
|
||||
#define ExpectedCatchScopeAlignment 8
|
||||
#else
|
||||
#define ExpectedCatchScopeSize 8
|
||||
#define ExpectedCatchScopeAlignment 8
|
||||
#endif
|
||||
|
||||
static_assert(sizeof(CatchScope) == ExpectedCatchScopeSize, "CatchScope.zig assumes CatchScope is 56 bytes");
|
||||
static_assert(alignof(CatchScope) == ExpectedCatchScopeAlignment, "CatchScope.zig assumes CatchScope is 8-byte aligned");
|
||||
|
||||
extern "C" void CatchScope__construct(
|
||||
void* ptr,
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
|
||||
@@ -130,18 +130,19 @@ static EncodedJSValue getOwnProxyObject(JSPropertyIterator* iter, JSObject* obje
|
||||
|
||||
extern "C" EncodedJSValue Bun__JSPropertyIterator__getNameAndValue(JSPropertyIterator* iter, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, BunString* propertyName, size_t i)
|
||||
{
|
||||
const auto& prop = iter->properties->propertyNameVector()[i];
|
||||
if (iter->isSpecialProxy) [[unlikely]] {
|
||||
return getOwnProxyObject(iter, object, prop, propertyName);
|
||||
}
|
||||
|
||||
auto& vm = iter->vm;
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
const auto& prop = iter->properties->propertyNameVector()[i];
|
||||
if (iter->isSpecialProxy) [[unlikely]] {
|
||||
RELEASE_AND_RETURN(scope, getOwnProxyObject(iter, object, prop, propertyName));
|
||||
}
|
||||
|
||||
// This has to be get because we may need to call on prototypes
|
||||
// If we meant for this to only run for own keys, the property name would not be included in the array.
|
||||
PropertySlot slot(object, PropertySlot::InternalMethodType::Get);
|
||||
if (!object->getPropertySlot(globalObject, prop, slot)) {
|
||||
return {};
|
||||
RELEASE_AND_RETURN(scope, {});
|
||||
}
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
@@ -154,13 +155,14 @@ extern "C" EncodedJSValue Bun__JSPropertyIterator__getNameAndValue(JSPropertyIte
|
||||
|
||||
extern "C" EncodedJSValue Bun__JSPropertyIterator__getNameAndValueNonObservable(JSPropertyIterator* iter, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, BunString* propertyName, size_t i)
|
||||
{
|
||||
const auto& prop = iter->properties->propertyNameVector()[i];
|
||||
if (iter->isSpecialProxy) [[unlikely]] {
|
||||
return getOwnProxyObject(iter, object, prop, propertyName);
|
||||
}
|
||||
auto& vm = iter->vm;
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
const auto& prop = iter->properties->propertyNameVector()[i];
|
||||
if (iter->isSpecialProxy) [[unlikely]] {
|
||||
RELEASE_AND_RETURN(scope, getOwnProxyObject(iter, object, prop, propertyName));
|
||||
}
|
||||
|
||||
PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, vm.ptr());
|
||||
auto has = object->getNonIndexPropertySlot(globalObject, prop, slot);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
@@ -1271,6 +1271,17 @@ pub const JSValue = enum(i64) {
|
||||
return if (this.isObject()) this.uncheckedPtrCast(JSObject) else null;
|
||||
}
|
||||
|
||||
/// Unwraps Number, Boolean, String, and BigInt objects to their primitive forms.
|
||||
pub fn unwrapBoxedPrimitive(this: JSValue, global: *JSGlobalObject) JSError!JSValue {
|
||||
var scope: CatchScope = undefined;
|
||||
scope.init(global, @src());
|
||||
defer scope.deinit();
|
||||
const result = JSC__JSValue__unwrapBoxedPrimitive(global, this);
|
||||
try scope.returnIfException();
|
||||
return result;
|
||||
}
|
||||
extern fn JSC__JSValue__unwrapBoxedPrimitive(*JSGlobalObject, JSValue) JSValue;
|
||||
|
||||
extern fn JSC__JSValue__getPrototype(this: JSValue, globalObject: *JSGlobalObject) JSValue;
|
||||
pub fn getPrototype(this: JSValue, globalObject: *JSGlobalObject) JSValue {
|
||||
return JSC__JSValue__getPrototype(this, globalObject);
|
||||
|
||||
91
src/bun.js/bindings/StringBuilder.zig
Normal file
91
src/bun.js/bindings/StringBuilder.zig
Normal file
@@ -0,0 +1,91 @@
|
||||
const StringBuilder = @This();
|
||||
|
||||
const size = 24;
|
||||
const alignment = 8;
|
||||
|
||||
bytes: [size]u8 align(alignment),
|
||||
|
||||
pub inline fn init() StringBuilder {
|
||||
var this: StringBuilder = undefined;
|
||||
StringBuilder__init(&this.bytes);
|
||||
return this;
|
||||
}
|
||||
extern fn StringBuilder__init(*anyopaque) void;
|
||||
|
||||
pub fn deinit(this: *StringBuilder) void {
|
||||
StringBuilder__deinit(&this.bytes);
|
||||
}
|
||||
extern fn StringBuilder__deinit(*anyopaque) void;
|
||||
|
||||
const Append = enum {
|
||||
latin1,
|
||||
utf16,
|
||||
double,
|
||||
int,
|
||||
usize,
|
||||
string,
|
||||
lchar,
|
||||
uchar,
|
||||
quoted_json_string,
|
||||
|
||||
pub fn Type(comptime this: Append) type {
|
||||
return switch (this) {
|
||||
.latin1 => []const u8,
|
||||
.utf16 => []const u16,
|
||||
.double => f64,
|
||||
.int => i32,
|
||||
.usize => usize,
|
||||
.string => String,
|
||||
.lchar => u8,
|
||||
.uchar => u16,
|
||||
.quoted_json_string => String,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn append(this: *StringBuilder, comptime append_type: Append, value: append_type.Type()) void {
|
||||
switch (comptime append_type) {
|
||||
.latin1 => StringBuilder__appendLatin1(&this.bytes, value.ptr, value.len),
|
||||
.utf16 => StringBuilder__appendUtf16(&this.bytes, value.ptr, value.len),
|
||||
.double => StringBuilder__appendDouble(&this.bytes, value),
|
||||
.int => StringBuilder__appendInt(&this.bytes, value),
|
||||
.usize => StringBuilder__appendUsize(&this.bytes, value),
|
||||
.string => StringBuilder__appendString(&this.bytes, value),
|
||||
.lchar => StringBuilder__appendLChar(&this.bytes, value),
|
||||
.uchar => StringBuilder__appendUChar(&this.bytes, value),
|
||||
.quoted_json_string => StringBuilder__appendQuotedJsonString(&this.bytes, value),
|
||||
}
|
||||
}
|
||||
extern fn StringBuilder__appendLatin1(*anyopaque, str: [*]const u8, len: usize) void;
|
||||
extern fn StringBuilder__appendUtf16(*anyopaque, str: [*]const u16, len: usize) void;
|
||||
extern fn StringBuilder__appendDouble(*anyopaque, num: f64) void;
|
||||
extern fn StringBuilder__appendInt(*anyopaque, num: i32) void;
|
||||
extern fn StringBuilder__appendUsize(*anyopaque, num: usize) void;
|
||||
extern fn StringBuilder__appendString(*anyopaque, str: String) void;
|
||||
extern fn StringBuilder__appendLChar(*anyopaque, c: u8) void;
|
||||
extern fn StringBuilder__appendUChar(*anyopaque, c: u16) void;
|
||||
extern fn StringBuilder__appendQuotedJsonString(*anyopaque, str: String) void;
|
||||
|
||||
pub fn toString(this: *StringBuilder, global: *JSGlobalObject) JSError!JSValue {
|
||||
var scope: jsc.CatchScope = undefined;
|
||||
scope.init(global, @src());
|
||||
defer scope.deinit();
|
||||
|
||||
const result = StringBuilder__toString(&this.bytes, global);
|
||||
try scope.returnIfException();
|
||||
return result;
|
||||
}
|
||||
extern fn StringBuilder__toString(*anyopaque, global: *JSGlobalObject) JSValue;
|
||||
|
||||
pub fn ensureUnusedCapacity(this: *StringBuilder, additional: usize) void {
|
||||
StringBuilder__ensureUnusedCapacity(&this.bytes, additional);
|
||||
}
|
||||
extern fn StringBuilder__ensureUnusedCapacity(*anyopaque, usize) void;
|
||||
|
||||
const bun = @import("bun");
|
||||
const JSError = bun.JSError;
|
||||
const String = bun.String;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
81
src/bun.js/bindings/StringBuilderBinding.cpp
Normal file
81
src/bun.js/bindings/StringBuilderBinding.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "root.h"
|
||||
#include "BunString.h"
|
||||
#include "headers-handwritten.h"
|
||||
|
||||
static_assert(sizeof(WTF::StringBuilder) == 24, "StringBuilder.zig assumes WTF::StringBuilder is 24 bytes");
|
||||
static_assert(alignof(WTF::StringBuilder) == 8, "StringBuilder.zig assumes WTF::StringBuilder is 8-byte aligned");
|
||||
|
||||
extern "C" void StringBuilder__init(WTF::StringBuilder* ptr)
|
||||
{
|
||||
new (ptr) WTF::StringBuilder(OverflowPolicy::RecordOverflow);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__deinit(WTF::StringBuilder* builder)
|
||||
{
|
||||
builder->~StringBuilder();
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendLatin1(WTF::StringBuilder* builder, LChar const* ptr, size_t len)
|
||||
{
|
||||
builder->append({ ptr, len });
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendUtf16(WTF::StringBuilder* builder, UChar const* ptr, size_t len)
|
||||
{
|
||||
builder->append({ ptr, len });
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendDouble(WTF::StringBuilder* builder, double num)
|
||||
{
|
||||
builder->append(num);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendInt(WTF::StringBuilder* builder, int32_t num)
|
||||
{
|
||||
builder->append(num);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendUsize(WTF::StringBuilder* builder, size_t num)
|
||||
{
|
||||
builder->append(num);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendString(WTF::StringBuilder* builder, BunString str)
|
||||
{
|
||||
str.appendToBuilder(*builder);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendLChar(WTF::StringBuilder* builder, LChar c)
|
||||
{
|
||||
builder->append(c);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendUChar(WTF::StringBuilder* builder, UChar c)
|
||||
{
|
||||
builder->append(c);
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__appendQuotedJsonString(WTF::StringBuilder* builder, BunString str)
|
||||
{
|
||||
auto string = str.toWTFString(BunString::ZeroCopy);
|
||||
builder->appendQuotedJSONString(string);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue StringBuilder__toString(WTF::StringBuilder* builder, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (builder->hasOverflowed()) [[unlikely]] {
|
||||
JSC::throwOutOfMemoryError(globalObject, scope);
|
||||
return JSC::JSValue::encode({});
|
||||
}
|
||||
|
||||
auto str = builder->toString();
|
||||
return JSC::JSValue::encode(JSC::jsString(vm, str));
|
||||
}
|
||||
|
||||
extern "C" void StringBuilder__ensureUnusedCapacity(WTF::StringBuilder* builder, size_t additional)
|
||||
{
|
||||
builder->reserveCapacity(builder->length() + additional);
|
||||
}
|
||||
@@ -36,6 +36,8 @@ pub const WTF = struct {
|
||||
|
||||
return buffer[0..@intCast(res)];
|
||||
}
|
||||
|
||||
pub const StringBuilder = @import("./StringBuilder.zig");
|
||||
};
|
||||
|
||||
const bun = @import("bun");
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "JavaScriptCore/JSArrayBuffer.h"
|
||||
#include "JavaScriptCore/JSArrayInlines.h"
|
||||
#include "JavaScriptCore/ErrorInstanceInlines.h"
|
||||
#include "JavaScriptCore/BigIntObject.h"
|
||||
|
||||
#include "JavaScriptCore/JSCallbackObject.h"
|
||||
#include "JavaScriptCore/JSClassRef.h"
|
||||
@@ -2096,6 +2097,28 @@ BunString WebCore__DOMURL__fileSystemPath(WebCore::DOMURL* arg0, int* errorCode)
|
||||
return BunString { BunStringTag::Dead, nullptr };
|
||||
}
|
||||
|
||||
// Taken from unwrapBoxedPrimitive in JSONObject.cpp in WebKit
|
||||
extern "C" JSC::EncodedJSValue JSC__JSValue__unwrapBoxedPrimitive(JSGlobalObject* globalObject, EncodedJSValue encodedValue)
|
||||
{
|
||||
JSValue value = JSValue::decode(encodedValue);
|
||||
|
||||
if (!value.isObject()) {
|
||||
return JSValue::encode(value);
|
||||
}
|
||||
|
||||
JSObject* object = asObject(value);
|
||||
|
||||
if (object->inherits<NumberObject>()) {
|
||||
return JSValue::encode(jsNumber(object->toNumber(globalObject)));
|
||||
}
|
||||
if (object->inherits<StringObject>())
|
||||
return JSValue::encode(object->toString(globalObject));
|
||||
if (object->inherits<BooleanObject>() || object->inherits<BigIntObject>())
|
||||
return JSValue::encode(jsCast<JSWrapperObject*>(object)->internalValue());
|
||||
|
||||
return JSValue::encode(object);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue ZigString__toJSONObject(const ZigString* strPtr, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
ASSERT_NO_PENDING_EXCEPTION(globalObject);
|
||||
|
||||
@@ -81,6 +81,8 @@ typedef struct BunString {
|
||||
|
||||
bool isEmpty() const;
|
||||
|
||||
void appendToBuilder(WTF::StringBuilder& builder) const;
|
||||
|
||||
} BunString;
|
||||
|
||||
typedef struct ZigErrorType {
|
||||
|
||||
@@ -183,6 +183,24 @@ static const WTF::String toStringCopy(ZigString str)
|
||||
}
|
||||
}
|
||||
|
||||
static void appendToBuilder(ZigString str, WTF::StringBuilder& builder)
|
||||
{
|
||||
if (str.len == 0 || str.ptr == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (isTaggedUTF8Ptr(str.ptr)) [[unlikely]] {
|
||||
WTF::String converted = WTF::String::fromUTF8ReplacingInvalidSequences(std::span { untag(str.ptr), str.len });
|
||||
builder.append(converted);
|
||||
return;
|
||||
}
|
||||
if (isTaggedUTF16Ptr(str.ptr)) {
|
||||
builder.append({ reinterpret_cast<const char16_t*>(untag(str.ptr)), str.len });
|
||||
return;
|
||||
}
|
||||
|
||||
builder.append({ untag(str.ptr), str.len });
|
||||
}
|
||||
|
||||
static WTF::String toStringNotConst(ZigString str) { return toString(str); }
|
||||
|
||||
static const JSC::JSString* toJSString(ZigString str, JSC::JSGlobalObject* global)
|
||||
|
||||
@@ -3720,7 +3720,7 @@ pub noinline fn throwStackOverflow() StackOverflow!void {
|
||||
@branchHint(.cold);
|
||||
return error.StackOverflow;
|
||||
}
|
||||
const StackOverflow = error{StackOverflow};
|
||||
pub const StackOverflow = error{StackOverflow};
|
||||
|
||||
pub const S3 = @import("./s3/client.zig");
|
||||
|
||||
|
||||
@@ -866,19 +866,8 @@ pub const String = extern struct {
|
||||
bun.assert(index < this.length());
|
||||
}
|
||||
return switch (this.tag) {
|
||||
.WTFStringImpl => if (this.value.WTFStringImpl.is8Bit()) @intCast(this.value.WTFStringImpl.utf8Slice()[index]) else this.value.WTFStringImpl.utf16Slice()[index],
|
||||
.ZigString, .StaticZigString => if (!this.value.ZigString.is16Bit()) @intCast(this.value.ZigString.slice()[index]) else this.value.ZigString.utf16Slice()[index],
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn charAtU8(this: String, index: usize) u8 {
|
||||
if (comptime bun.Environment.allow_assert) {
|
||||
bun.assert(index < this.length());
|
||||
}
|
||||
return switch (this.tag) {
|
||||
.WTFStringImpl => if (this.value.WTFStringImpl.is8Bit()) this.value.WTFStringImpl.utf8Slice()[index] else @truncate(this.value.WTFStringImpl.utf16Slice()[index]),
|
||||
.ZigString, .StaticZigString => if (!this.value.ZigString.is16Bit()) this.value.ZigString.slice()[index] else @truncate(this.value.ZigString.utf16SliceAligned()[index]),
|
||||
.WTFStringImpl => if (this.value.WTFStringImpl.is8Bit()) this.value.WTFStringImpl.latin1Slice()[index] else this.value.WTFStringImpl.utf16Slice()[index],
|
||||
.ZigString, .StaticZigString => if (!this.value.ZigString.is16Bit()) this.value.ZigString.slice()[index] else this.value.ZigString.utf16Slice()[index],
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
@@ -1178,10 +1167,6 @@ pub const SliceWithUnderlyingString = struct {
|
||||
return this.utf8.slice();
|
||||
}
|
||||
|
||||
pub fn sliceZ(this: SliceWithUnderlyingString) [:0]const u8 {
|
||||
return this.utf8.sliceZ();
|
||||
}
|
||||
|
||||
pub fn format(self: SliceWithUnderlyingString, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
if (self.utf8.len == 0) {
|
||||
try self.underlying.format(fmt, opts, writer);
|
||||
|
||||
@@ -3,3 +3,8 @@ import { expectType } from "./utilities";
|
||||
expectType(Bun.YAML.parse("")).is<unknown>();
|
||||
// @ts-expect-error
|
||||
expectType(Bun.YAML.parse({})).is<unknown>();
|
||||
expectType(Bun.YAML.stringify({ abc: "def"})).is<string>();
|
||||
// @ts-expect-error
|
||||
expectType(Bun.YAML.stringify("hi", {})).is<string>();
|
||||
// @ts-expect-error
|
||||
expectType(Bun.YAML.stringify("hi", null, 123n)).is<string>();
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user