-
Notifications
You must be signed in to change notification settings - Fork 5
/
index.js
298 lines (243 loc) · 7.79 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
var UNDEFINED_INDEX = -1;
var ARRAY_HOLE_INDEX = -2;
var NAN_INDEX = -3;
var POS_INF_INDEX = -4;
var NEG_INF_INDEX = -5;
var customTypes = Object.create(null);
exports.registerType = function (typeName, handlers) {
function check(methodName) {
if (typeof handlers[methodName] !== "function") {
throw new Error(
"second argument to ARSON.registerType(" +
JSON.stringify(typeName) + ", ...) " +
"must be an object with a " + methodName + " method"
);
}
}
check("deconstruct");
check("reconstruct");
customTypes[typeName] = handlers;
return exports;
};
require("./custom.js");
exports.encode = exports.stringify =
function encode(value) {
return JSON.stringify(toTable(value));
}
// This array will grow as needed so that we can slice arrays filled with
// ARRAY_HOLE_INDEX from it.
var HOLY_ARRAY = [];
// Returns an array of the given length filled with ARRAY_HOLE_INDEX.
function getArrayOfHoles(length) {
var holyLen = HOLY_ARRAY.length;
if (length > holyLen) {
HOLY_ARRAY.length = length;
for (var i = holyLen; i < length; ++i) {
HOLY_ARRAY[i] = ARRAY_HOLE_INDEX;
}
}
return HOLY_ARRAY.slice(0, length);
}
function toTable(value) {
var values = [];
var getIndex = makeGetIndexFunction(values);
function copy(value) {
var result = value;
if (value && typeof value === "object") {
var keys = Object.keys(value);
if (isPlainObject(value)) {
result = {};
} else if (Array.isArray(value)) {
result = getArrayOfHoles(value.length);
} else {
for (var typeName in customTypes) {
// If value is not a plain Object, but something exotic like a
// Date or a RegExp, serialize it as an array with typeName as
// its first element. These arrays can be distinguished from
// normal arrays, because all other non-empty arrays will be
// serialized with a numeric value as their first element.
var args = customTypes[typeName].deconstruct(value);
if (args) {
for (var i = 0; i < args.length; ++i) {
args[i] = getIndex(args[i]);
}
args.unshift(typeName);
return args;
}
}
result = {};
}
keys.forEach(function (key) {
result[key] = getIndex(value[key]);
});
}
return result;
}
// Assigns the root value to values[0].
var index0 = getIndex(value);
if (index0 < 0) {
// If value is something special that gets a negative index, then we
// don't need to build a table at all, and we can simply return that
// negative index as a complete serialization. This avoids ambiguity
// about indexes versus primitive literal values.
return index0;
}
// Note that this for loop cannot be a forEach loop, because
// values.length is expected to change during iteration.
for (var table = [], v = 0; v < values.length; ++v) {
table[v] = copy(values[v]);
}
return table;
}
function isPlainObject(value) {
var isObject = value && typeof value === "object";
if (isObject) {
var proto = Object.getPrototypeOf
? Object.getPrototypeOf(value)
: value.__proto__;
return proto === Object.prototype;
}
return false;
}
function makeGetIndexFunction(values) {
var indexMap = typeof Map === "function" && new Map;
return function getIndex(value) {
switch (typeof value) {
case "undefined":
return UNDEFINED_INDEX;
case "number":
if (isNaN(value)) {
return NAN_INDEX;
}
if (! isFinite(value)) {
return value < 0 ? NEG_INF_INDEX : POS_INF_INDEX;
}
// fall through...
}
var index;
if (indexMap) {
// If we have Map, use it instead of values.indexOf to accelerate
// object lookups.
index = indexMap.get(value);
if (typeof index === "undefined") {
index = values.push(value) - 1;
indexMap.set(value, index);
}
} else {
index = values.indexOf(value);
if (index < 0) {
index = values.push(value) - 1;
}
}
return index;
};
}
exports.decode = exports.parse =
function decode(encoding) {
return fromTable(JSON.parse(encoding));
}
function fromTable(table) {
if (typeof table === "number" && table < 0) {
return getValueWithoutCache(table);
}
var getValueCache = new Array(table.length);
function getValue(index) {
return index in getValueCache
? getValueCache[index]
: getValueCache[index] = getValueWithoutCache(index);
}
function getValueWithoutCache(index) {
if (index < 0) {
if (index === UNDEFINED_INDEX) {
return;
}
if (index === ARRAY_HOLE_INDEX) {
// Never reached because handled specially below.
return;
}
if (index === NAN_INDEX) {
return NaN;
}
if (index === POS_INF_INDEX) {
return Infinity;
}
if (index === NEG_INF_INDEX) {
return -Infinity;
}
throw new Error("invalid ARSON index: " + index);
}
var entry = table[index];
if (entry && typeof entry === "object") {
if (Array.isArray(entry)) {
var elem0 = entry[0];
if (typeof elem0 === "string" && elem0 in customTypes) {
var rec = customTypes[elem0].reconstruct;
var empty = rec();
if (empty) {
// If the reconstruct handler returns an object, treat it as
// an empty instance of the desired type, and schedule it to
// be filled in later. This two-stage process allows exotic
// container objects to contain themselves.
containers.push({
reconstruct: rec,
empty: empty,
argIndexes: entry.slice(1)
});
}
// If the reconstruct handler returned a falsy value, then we
// assume none of its arguments refer to exotic containers, so
// we can reconstruct the object immediately. Examples: Buffer,
// Date, RegExp.
return table[index] = empty || rec(entry.slice(1).map(getValue));
}
}
// Here entry is already the correct array or object reference for
// this index, but its values are still indexes that will need to be
// resolved later.
objects.push(entry);
}
return entry;
}
var containers = [];
var objects = [];
// First pass: make sure all exotic objects are deserialized fist, and
// keep track of all plain object entries for later.
table.forEach(function (entry, i) {
getValue(i);
});
// Second pass: now that we have final object references for all exotic
// objects, we can safely resolve argument indexes for the empty ones.
containers.forEach(function (c) {
c.args = c.argIndexes.map(getValue);
});
// Third pass: resolve value indexes for ordinary arrays and objects.
objects.forEach(function (obj) {
Object.keys(obj).forEach(function (key) {
var index = obj[key];
if (typeof index !== "number") {
// Leave non-numeric indexes untouched.
return;
}
if (index < 0) {
if (index === ARRAY_HOLE_INDEX) {
// Array holes have to be handled specially here, since getValue
// does not have a reference to obj.
delete obj[key];
return;
}
// This recursion is guaranteed not to add more objects, because
// we know the index is negative.
obj[key] = getValue(index);
} else {
// Non-negative indexes refer to normal table values.
obj[key] = table[index];
}
});
});
// Fourth pass: all possible object references have been established, so
// we can finally initialize the empty container objects.
containers.forEach(function (c) {
c.reconstruct.call(c.empty, c.args);
});
return table[0];
}