UNPKG

7.55 kBJavaScriptView Raw
1const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
2const unsafeChars = /[<>\b\f\n\r\t\0\u2028\u2029]/g;
3const reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
4const escaped = {
5 "<": "\\u003C",
6 ">": "\\u003E",
7 "/": "\\u002F",
8 "\\": "\\\\",
9 "\b": "\\b",
10 "\f": "\\f",
11 "\n": "\\n",
12 "\r": "\\r",
13 " ": "\\t",
14 "\0": "\\0",
15 "\u2028": "\\u2028",
16 "\u2029": "\\u2029"
17};
18const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join("\0");
19function devalue(value) {
20 const counts = new Map();
21 let logNum = 0;
22 function log(message) {
23 if (logNum < 100) {
24 console.warn(message);
25 logNum += 1;
26 }
27 }
28 function walk(thing) {
29 if (typeof thing === "function") {
30 log(`Cannot stringify a function ${thing.name}`);
31 return;
32 }
33 if (counts.has(thing)) {
34 counts.set(thing, counts.get(thing) + 1);
35 return;
36 }
37 counts.set(thing, 1);
38 if (!isPrimitive(thing)) {
39 const type = getType(thing);
40 switch (type) {
41 case "Number":
42 case "String":
43 case "Boolean":
44 case "Date":
45 case "RegExp":
46 return;
47 case "Array":
48 thing.forEach(walk);
49 break;
50 case "Set":
51 case "Map":
52 Array.from(thing).forEach(walk);
53 break;
54 default:
55 const proto = Object.getPrototypeOf(thing);
56 if (proto !== Object.prototype && proto !== null && Object.getOwnPropertyNames(proto).sort().join("\0") !== objectProtoOwnPropertyNames) {
57 if (typeof thing.toJSON !== "function") {
58 log(`Cannot stringify arbitrary non-POJOs ${thing.constructor.name}`);
59 }
60 } else if (Object.getOwnPropertySymbols(thing).length > 0) {
61 log(`Cannot stringify POJOs with symbolic keys ${Object.getOwnPropertySymbols(thing).map((symbol) => symbol.toString())}`);
62 } else {
63 Object.keys(thing).forEach((key) => walk(thing[key]));
64 }
65 }
66 }
67 }
68 walk(value);
69 const names = new Map();
70 Array.from(counts).filter((entry) => entry[1] > 1).sort((a, b) => b[1] - a[1]).forEach((entry, i) => {
71 names.set(entry[0], getName(i));
72 });
73 function stringify(thing) {
74 if (names.has(thing)) {
75 return names.get(thing);
76 }
77 if (isPrimitive(thing)) {
78 return stringifyPrimitive(thing);
79 }
80 const type = getType(thing);
81 switch (type) {
82 case "Number":
83 case "String":
84 case "Boolean":
85 return `Object(${stringify(thing.valueOf())})`;
86 case "RegExp":
87 return thing.toString();
88 case "Date":
89 return `new Date(${thing.getTime()})`;
90 case "Array":
91 const members = thing.map((v, i) => i in thing ? stringify(v) : "");
92 const tail = thing.length === 0 || thing.length - 1 in thing ? "" : ",";
93 return `[${members.join(",")}${tail}]`;
94 case "Set":
95 case "Map":
96 return `new ${type}([${Array.from(thing).map(stringify).join(",")}])`;
97 default:
98 if (thing.toJSON) {
99 let json = thing.toJSON();
100 if (getType(json) === "String") {
101 try {
102 json = JSON.parse(json);
103 } catch (e) {
104 }
105 }
106 return stringify(json);
107 }
108 if (Object.getPrototypeOf(thing) === null) {
109 if (Object.keys(thing).length === 0) {
110 return "Object.create(null)";
111 }
112 return `Object.create(null,{${Object.keys(thing).map((key) => `${safeKey(key)}:{writable:true,enumerable:true,value:${stringify(thing[key])}}`).join(",")}})`;
113 }
114 return `{${Object.keys(thing).map((key) => `${safeKey(key)}:${stringify(thing[key])}`).join(",")}}`;
115 }
116 }
117 const str = stringify(value);
118 if (names.size) {
119 const params = [];
120 const statements = [];
121 const values = [];
122 names.forEach((name, thing) => {
123 params.push(name);
124 if (isPrimitive(thing)) {
125 values.push(stringifyPrimitive(thing));
126 return;
127 }
128 const type = getType(thing);
129 switch (type) {
130 case "Number":
131 case "String":
132 case "Boolean":
133 values.push(`Object(${stringify(thing.valueOf())})`);
134 break;
135 case "RegExp":
136 values.push(thing.toString());
137 break;
138 case "Date":
139 values.push(`new Date(${thing.getTime()})`);
140 break;
141 case "Array":
142 values.push(`Array(${thing.length})`);
143 thing.forEach((v, i) => {
144 statements.push(`${name}[${i}]=${stringify(v)}`);
145 });
146 break;
147 case "Set":
148 values.push("new Set");
149 statements.push(`${name}.${Array.from(thing).map((v) => `add(${stringify(v)})`).join(".")}`);
150 break;
151 case "Map":
152 values.push("new Map");
153 statements.push(`${name}.${Array.from(thing).map(([k, v]) => `set(${stringify(k)}, ${stringify(v)})`).join(".")}`);
154 break;
155 default:
156 values.push(Object.getPrototypeOf(thing) === null ? "Object.create(null)" : "{}");
157 Object.keys(thing).forEach((key) => {
158 statements.push(`${name}${safeProp(key)}=${stringify(thing[key])}`);
159 });
160 }
161 });
162 statements.push(`return ${str}`);
163 return `(function(${params.join(",")}){${statements.join(";")}}(${values.join(",")}))`;
164 } else {
165 return str;
166 }
167}
168function getName(num) {
169 let name = "";
170 do {
171 name = chars[num % chars.length] + name;
172 num = ~~(num / chars.length) - 1;
173 } while (num >= 0);
174 return reserved.test(name) ? `${name}0` : name;
175}
176function isPrimitive(thing) {
177 return Object(thing) !== thing;
178}
179function stringifyPrimitive(thing) {
180 if (typeof thing === "string") {
181 return stringifyString(thing);
182 }
183 if (thing === void 0) {
184 return "void 0";
185 }
186 if (thing === 0 && 1 / thing < 0) {
187 return "-0";
188 }
189 const str = String(thing);
190 if (typeof thing === "number") {
191 return str.replace(/^(-)?0\./, "$1.");
192 }
193 return str;
194}
195function getType(thing) {
196 return Object.prototype.toString.call(thing).slice(8, -1);
197}
198function escapeUnsafeChar(c) {
199 return escaped[c] || c;
200}
201function escapeUnsafeChars(str) {
202 return str.replace(unsafeChars, escapeUnsafeChar);
203}
204function safeKey(key) {
205 return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? key : escapeUnsafeChars(JSON.stringify(key));
206}
207function safeProp(key) {
208 return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? `.${key}` : `[${escapeUnsafeChars(JSON.stringify(key))}]`;
209}
210function stringifyString(str) {
211 let result = '"';
212 for (let i = 0; i < str.length; i += 1) {
213 const char = str.charAt(i);
214 const code = char.charCodeAt(0);
215 if (char === '"') {
216 result += '\\"';
217 } else if (char in escaped) {
218 result += escaped[char];
219 } else if (code >= 55296 && code <= 57343) {
220 const next = str.charCodeAt(i + 1);
221 if (code <= 56319 && (next >= 56320 && next <= 57343)) {
222 result += char + str[++i];
223 } else {
224 result += `\\u${code.toString(16).toUpperCase()}`;
225 }
226 } else {
227 result += char;
228 }
229 }
230 result += '"';
231 return result;
232}
233
234export default devalue;