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