forked from BabOuDev/JSONCrush
-
Notifications
You must be signed in to change notification settings - Fork 0
/
JSONCrush.js
187 lines (151 loc) · 6.95 KB
/
JSONCrush.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
///////////////////////////////////////////////////////////////////////
// JSONCrush by Frank Force [MIT] https://github.com/KilledByAPixel/JSONCrush
///////////////////////////////////////////////////////////////////////
'use strict';
const JSONCrush = function (string) {
const maxSubstringLength = 50; // speed it up by limiting max length
const delimiter = '\u0001'; // used to split parts of crushed string
const JSCrush = (string, replaceCharacters) => {
// JSCrush Algorithm (repleace repeated substrings with single characters)
let replaceCharacterPos = replaceCharacters.length;
let splitString = '';
const ByteLength = (string) => encodeURI(encodeURIComponent(string)).replace(/%../g, 'i').length;
const HasUnmatchedSurrogate = (string) => {
// check ends of string for unmatched surrogate pairs
let c1 = string.charCodeAt(0);
let c2 = string.charCodeAt(string.length - 1);
return (c1 >= 0xdc00 && c1 <= 0xdfff) || (c2 >= 0xd800 && c2 <= 0xdbff);
};
// count instances of substrings
let substringCount = {};
for (let substringLength = 2; substringLength < maxSubstringLength; substringLength++)
for (let i = 0; i < string.length - substringLength; ++i) {
let substring = string.substr(i, substringLength);
// don't recount if already in list
if (substringCount[substring]) continue;
// prevent breaking up unmatched surrogates
if (HasUnmatchedSurrogate(substring)) continue;
// count how many times the substring appears
let count = 1;
for (let substringPos = string.indexOf(substring, i + substringLength); substringPos >= 0; ++count)
substringPos = string.indexOf(substring, substringPos + substringLength);
// add to list if it appears multiple times
if (count > 1) substringCount[substring] = count;
}
while (true) {
// loop while string can be crushed more
// get the next character that is not in the string
for (; replaceCharacterPos-- && string.includes(replaceCharacters[replaceCharacterPos]); ) {}
if (replaceCharacterPos < 0) break; // ran out of replacement characters
let replaceCharacter = replaceCharacters[replaceCharacterPos];
// find the longest substring to replace
let bestSubstring;
let bestLengthDelta = 0;
let replaceByteLength = ByteLength(replaceCharacter);
for (let substring in substringCount) {
// calculate change in length of string if it substring was replaced
let count = substringCount[substring];
let lengthDelta = (count - 1) * ByteLength(substring) - (count + 1) * replaceByteLength;
if (!splitString.length) lengthDelta -= ByteLength(delimiter); // include the delimiter length
if (lengthDelta <= 0) delete substringCount[substring];
else if (lengthDelta > bestLengthDelta) {
bestSubstring = substring;
bestLengthDelta = lengthDelta;
}
}
if (!bestSubstring) break; // string can't be compressed further
// create new string with the split character
string = string.split(bestSubstring).join(replaceCharacter) + replaceCharacter + bestSubstring;
splitString = replaceCharacter + splitString;
// update substring count list after the replacement
let newSubstringCount = {};
for (let substring in substringCount) {
// make a new substring with the replacement
let newSubstring = substring.split(bestSubstring).join(replaceCharacter);
// count how many times the new substring appears
let count = 0;
for (let i = string.indexOf(newSubstring); i >= 0; ++count)
i = string.indexOf(newSubstring, i + newSubstring.length);
// add to list if it appears multiple times
if (count > 1) newSubstringCount[newSubstring] = count;
}
substringCount = newSubstringCount;
}
return { a: string, b: splitString };
};
// create a string of replacement characters
let characters = [];
// prefer replacing with characters that will not be escaped by encodeURIComponent
const unescapedCharacters = `-_.!~*'()`;
for (let i = 127; --i; ) {
if (
(i >= 48 && i <= 57) || // 0-9
(i >= 65 && i <= 90) || // A-Z
(i >= 97 && i <= 122) || // a-z
unescapedCharacters.includes(String.fromCharCode(i))
)
characters.push(String.fromCharCode(i));
}
// pick from extended set last
for (let i = 32; i < 255; ++i) {
let c = String.fromCharCode(i);
if (c != '\\' && !characters.includes(c)) characters.unshift(c);
}
// remove delimiter if it is found in the string
string = string.replace(new RegExp(delimiter, 'g'), '');
// swap out common json characters
string = JSONCrushSwap(string);
// crush with JS crush
const crushed = JSCrush(string, characters);
// insert delimiter between JSCrush parts
let crushedString = crushed.a;
if (crushed.b.length) crushedString += delimiter + crushed.b;
// fix issues with some links not being recognized properly
crushedString += '_';
// encode URI
return encodeURIComponent(crushedString);
}
const JSONUncrush = function(string) {
// string must be a decoded URI component, searchParams.get() does this automatically
// remove last character
string = string.substring(0, string.length - 1);
// unsplit the string using the delimiter
const stringParts = string.split('\u0001');
// JSUncrush algorithm
let uncrushedString = stringParts[0];
if (stringParts.length > 1) {
let splitString = stringParts[1];
for (let character of splitString) {
// split the string using the current splitCharacter
let splitArray = uncrushedString.split(character);
// rejoin the string with the last element from the split
uncrushedString = splitArray.join(splitArray.pop());
}
}
// unswap the json characters in reverse direction
return JSONCrushSwap(uncrushedString, 0);
}
function JSONCrushSwap(string, forward = 1) {
// swap out characters for lesser used ones that wont get escaped
const swapGroups = [
['"', "'"],
["':", '!'],
[",'", '~'],
['}', ')', '\\', '\\'],
['{', '(', '\\', '\\'],
];
const Swap = (string, g) => {
let regex = new RegExp(`${(g[2] ? g[2] : '') + g[0]}|${(g[3] ? g[3] : '') + g[1]}`, 'g');
return string.replace(regex, ($1) => ($1 === g[0] ? g[1] : g[0]));
};
// need to be able to swap characters in reverse direction for uncrush
if (forward) for (let i = 0; i < swapGroups.length; ++i) string = Swap(string, swapGroups[i]);
else for (let i = swapGroups.length; i--; ) string = Swap(string, swapGroups[i]);
return string;
}
if (typeof module !== 'undefined' && module.exports) { //Node
module.exports = {JSONCrush, JSONUncrush};
} else {
window.JSONCrush = JSONCrush;
window.JSONUncrush = JSONUncrush;
}