Skip to content

Commit 0bbcc96

Browse files
authored
Merge pull request #69 from LorisSigrist/master
Add Support for ArrayBuffer and the various TypedArrays
2 parents bd31380 + a4fe876 commit 0bbcc96

File tree

7 files changed

+216
-0
lines changed

7 files changed

+216
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Like `JSON.stringify`, but handles
99
- dates
1010
- `Map` and `Set`
1111
- `BigInt`
12+
- `ArrayBuffer` and Typed Arrays
1213
- custom types via replacers, reducers and revivers
1314

1415
Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0).

src/base64.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Base64 Encodes an arraybuffer
3+
* @param {ArrayBuffer} arraybuffer
4+
* @returns {string}
5+
*/
6+
export function encode64(arraybuffer) {
7+
const dv = new DataView(arraybuffer);
8+
let binaryString = "";
9+
10+
for (let i = 0; i < arraybuffer.byteLength; i++) {
11+
binaryString += String.fromCharCode(dv.getUint8(i));
12+
}
13+
14+
return binaryToAscii(binaryString);
15+
}
16+
17+
/**
18+
* Decodes a base64 string into an arraybuffer
19+
* @param {string} string
20+
* @returns {ArrayBuffer}
21+
*/
22+
export function decode64(string) {
23+
const binaryString = asciiToBinary(string);
24+
const arraybuffer = new ArrayBuffer(binaryString.length);
25+
const dv = new DataView(arraybuffer);
26+
27+
for (let i = 0; i < arraybuffer.byteLength; i++) {
28+
dv.setUint8(i, binaryString.charCodeAt(i));
29+
}
30+
31+
return arraybuffer;
32+
}
33+
34+
const KEY_STRING =
35+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
36+
37+
/**
38+
* Substitute for atob since it's deprecated in node.
39+
* Does not do any input validation.
40+
*
41+
* @see https://github.com/jsdom/abab/blob/master/lib/atob.js
42+
*
43+
* @param {string} data
44+
* @returns {string}
45+
*/
46+
function asciiToBinary(data) {
47+
if (data.length % 4 === 0) {
48+
data = data.replace(/==?$/, "");
49+
}
50+
51+
let output = "";
52+
let buffer = 0;
53+
let accumulatedBits = 0;
54+
55+
for (let i = 0; i < data.length; i++) {
56+
buffer <<= 6;
57+
buffer |= KEY_STRING.indexOf(data[i]);
58+
accumulatedBits += 6;
59+
if (accumulatedBits === 24) {
60+
output += String.fromCharCode((buffer & 0xff0000) >> 16);
61+
output += String.fromCharCode((buffer & 0xff00) >> 8);
62+
output += String.fromCharCode(buffer & 0xff);
63+
buffer = accumulatedBits = 0;
64+
}
65+
}
66+
if (accumulatedBits === 12) {
67+
buffer >>= 4;
68+
output += String.fromCharCode(buffer);
69+
} else if (accumulatedBits === 18) {
70+
buffer >>= 2;
71+
output += String.fromCharCode((buffer & 0xff00) >> 8);
72+
output += String.fromCharCode(buffer & 0xff);
73+
}
74+
return output;
75+
}
76+
77+
/**
78+
* Substitute for btoa since it's deprecated in node.
79+
* Does not do any input validation.
80+
*
81+
* @see https://github.com/jsdom/abab/blob/master/lib/btoa.js
82+
*
83+
* @param {string} str
84+
* @returns {string}
85+
*/
86+
function binaryToAscii(str) {
87+
let out = "";
88+
for (let i = 0; i < str.length; i += 3) {
89+
/** @type {[number, number, number, number]} */
90+
const groupsOfSix = [undefined, undefined, undefined, undefined];
91+
groupsOfSix[0] = str.charCodeAt(i) >> 2;
92+
groupsOfSix[1] = (str.charCodeAt(i) & 0x03) << 4;
93+
if (str.length > i + 1) {
94+
groupsOfSix[1] |= str.charCodeAt(i + 1) >> 4;
95+
groupsOfSix[2] = (str.charCodeAt(i + 1) & 0x0f) << 2;
96+
}
97+
if (str.length > i + 2) {
98+
groupsOfSix[2] |= str.charCodeAt(i + 2) >> 6;
99+
groupsOfSix[3] = str.charCodeAt(i + 2) & 0x3f;
100+
}
101+
for (let j = 0; j < groupsOfSix.length; j++) {
102+
if (typeof groupsOfSix[j] === "undefined") {
103+
out += "=";
104+
} else {
105+
out += KEY_STRING[groupsOfSix[j]];
106+
}
107+
}
108+
}
109+
return out;
110+
}

src/parse.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { decode64 } from './base64.js';
12
import {
23
HOLE,
34
NAN,
@@ -101,6 +102,32 @@ export function unflatten(parsed, revivers) {
101102
}
102103
break;
103104

105+
case "Int8Array":
106+
case "Uint8Array":
107+
case "Uint8ClampedArray":
108+
case "Int16Array":
109+
case "Uint16Array":
110+
case "Int32Array":
111+
case "Uint32Array":
112+
case "Float32Array":
113+
case "Float64Array":
114+
case "BigInt64Array":
115+
case "BigUint64Array": {
116+
const TypedArrayConstructor = globalThis[type];
117+
const base64 = value[1];
118+
const arraybuffer = decode64(base64);
119+
const typedArray = new TypedArrayConstructor(arraybuffer);
120+
hydrated[index] = typedArray;
121+
break;
122+
}
123+
124+
case "ArrayBuffer": {
125+
const base64 = value[1];
126+
const arraybuffer = decode64(base64);
127+
hydrated[index] = arraybuffer;
128+
break;
129+
}
130+
104131
default:
105132
throw new Error(`Unknown type ${type}`);
106133
}

src/stringify.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
POSITIVE_INFINITY,
1616
UNDEFINED
1717
} from './constants.js';
18+
import { encode64 } from './base64.js';
1819

1920
/**
2021
* Turn a value into a JSON string that can be parsed with `devalue.parse`
@@ -137,6 +138,33 @@ export function stringify(value, reducers) {
137138
str += ']';
138139
break;
139140

141+
case "Int8Array":
142+
case "Uint8Array":
143+
case "Uint8ClampedArray":
144+
case "Int16Array":
145+
case "Uint16Array":
146+
case "Int32Array":
147+
case "Uint32Array":
148+
case "Float32Array":
149+
case "Float64Array":
150+
case "BigInt64Array":
151+
case "BigUint64Array": {
152+
/** @type {import("./types.js").TypedArray} */
153+
const typedArray = thing;
154+
const base64 = encode64(typedArray.buffer);
155+
str = '["' + type + '","' + base64 + '"]';
156+
break;
157+
}
158+
159+
case "ArrayBuffer": {
160+
/** @type {ArrayBuffer} */
161+
const arraybuffer = thing;
162+
const base64 = encode64(arraybuffer);
163+
164+
str = `["ArrayBuffer","${base64}"]`;
165+
break;
166+
}
167+
140168
default:
141169
if (!is_plain_object(thing)) {
142170
throw new DevalueError(

src/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;

src/uneval.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ export function uneval(value, replacer) {
8282
keys.pop();
8383
}
8484
break;
85+
86+
case "Int8Array":
87+
case "Uint8Array":
88+
case "Uint8ClampedArray":
89+
case "Int16Array":
90+
case "Uint16Array":
91+
case "Int32Array":
92+
case "Uint32Array":
93+
case "Float32Array":
94+
case "Float64Array":
95+
case "BigInt64Array":
96+
case "BigUint64Array":
97+
return;
98+
99+
case "ArrayBuffer":
100+
return;
85101

86102
default:
87103
if (!is_plain_object(thing)) {
@@ -161,6 +177,27 @@ export function uneval(value, replacer) {
161177
case 'Set':
162178
case 'Map':
163179
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`;
180+
181+
case "Int8Array":
182+
case "Uint8Array":
183+
case "Uint8ClampedArray":
184+
case "Int16Array":
185+
case "Uint16Array":
186+
case "Int32Array":
187+
case "Uint32Array":
188+
case "Float32Array":
189+
case "Float64Array":
190+
case "BigInt64Array":
191+
case "BigUint64Array": {
192+
/** @type {import("./types.js").TypedArray} */
193+
const typedArray = thing;
194+
return `new ${type}([${typedArray.toString()}])`;
195+
}
196+
197+
case "ArrayBuffer": {
198+
const ui8 = new Uint8Array(thing);
199+
return `new Uint8Array([${ui8.toString()}]).buffer`;
200+
}
164201

165202
default:
166203
const obj = `{${Object.keys(thing)

test/test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ const fixtures = {
159159
value: BigInt('1'),
160160
js: '1n',
161161
json: '[["BigInt","1"]]'
162+
},
163+
{
164+
name: 'Uint8Array',
165+
value: new Uint8Array([1, 2, 3]),
166+
js: 'new Uint8Array([1,2,3])',
167+
json: '[["Uint8Array","AQID"]]'
168+
},
169+
{
170+
name: "ArrayBuffer",
171+
value: new Uint8Array([1, 2, 3]).buffer,
172+
js: 'new Uint8Array([1,2,3]).buffer',
173+
json: '[["ArrayBuffer","AQID"]]'
162174
}
163175
],
164176

0 commit comments

Comments
 (0)