zustand-querystring 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 nitedani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -59,9 +59,9 @@ export const useStore = create<Store>()(
59
59
  querystring options:
60
60
 
61
61
  - <b>select</b> - the select option controls what part of the state is synced with the query string
62
- - <b>key: string</b> - the key option controls how the state is stored in the querystring (default: 'state')
62
+ - <b>key: string | false</b> - the key option controls how the state is stored in the querystring (default: 'state'). Set to `false` for standalone mode where each state key becomes a separate query parameter.
63
63
  - <b>url</b> - the url option is used to provide the request url on the server side render
64
- - <b>format</b> - custom format for stringify/parse (default: JSON-based format)
64
+ - <b>format</b> - custom format for stringify/parse (default: JSON-based format). Use `readable` from `zustand-querystring/format/readable` for human-readable URLs.
65
65
  - <b>syncNull: boolean</b> - when true, null values that differ from initial state are synced to URL (default: false)
66
66
  - <b>syncUndefined: boolean</b> - when true, undefined values that differ from initial state are synced to URL (default: false)
67
67
 
@@ -1,4 +1,14 @@
1
- declare function stringify(input: unknown, recursive?: boolean): string;
2
- declare function parse<T = unknown>(str: string): T;
1
+ /**
2
+ * URL-Safe Serialization
3
+ *
4
+ * Inspired by URLON (https://github.com/cerebral/urlon)
5
+ * Copyright (c) 2021 Cerebral - MIT License
6
+ */
7
+ declare function stringify(value: unknown, standalone?: boolean): string;
8
+ declare function parse<T = unknown>(input: string, standalone?: boolean): T;
9
+ declare const readable: {
10
+ stringify: typeof stringify;
11
+ parse: typeof parse;
12
+ };
3
13
 
4
- export { parse, stringify };
14
+ export { parse, readable, stringify };
@@ -1,4 +1,14 @@
1
- declare function stringify(input: unknown, recursive?: boolean): string;
2
- declare function parse<T = unknown>(str: string): T;
1
+ /**
2
+ * URL-Safe Serialization
3
+ *
4
+ * Inspired by URLON (https://github.com/cerebral/urlon)
5
+ * Copyright (c) 2021 Cerebral - MIT License
6
+ */
7
+ declare function stringify(value: unknown, standalone?: boolean): string;
8
+ declare function parse<T = unknown>(input: string, standalone?: boolean): T;
9
+ declare const readable: {
10
+ stringify: typeof stringify;
11
+ parse: typeof parse;
12
+ };
3
13
 
4
- export { parse, stringify };
14
+ export { parse, readable, stringify };
@@ -20,145 +20,219 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var readable_exports = {};
21
21
  __export(readable_exports, {
22
22
  parse: () => parse,
23
+ readable: () => readable,
23
24
  stringify: () => stringify
24
25
  });
25
26
  module.exports = __toCommonJS(readable_exports);
26
- var keyStringifyRegexp = /([=:@$/.])/g;
27
- var valueStringifyRegexp = /([.~/])/g;
28
- var keyParseRegexp = /[=:@$.]/;
29
- var valueParseRegexp = /[.~]/;
30
- function encodeString(str, regexp) {
31
- return encodeURI(str.replace(regexp, "/$1"));
27
+ var TYPE_OBJECT = ".";
28
+ var TYPE_ARRAY = "@";
29
+ var TYPE_STRING = "=";
30
+ var TYPE_PRIMITIVE = ":";
31
+ var SEPARATOR = ",";
32
+ var TERMINATOR = "~";
33
+ var ESCAPE = "/";
34
+ var DATE_PREFIX = "D";
35
+ var esc = (c) => /[.$^*+?()[\]{}|\\]/.test(c) ? `\\${c}` : c;
36
+ var KEY_STOP = new RegExp(
37
+ `[${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${esc(SEPARATOR)}]`
38
+ );
39
+ var VALUE_STOP = new RegExp(`[${esc(SEPARATOR)}${TERMINATOR}]`);
40
+ var KEY_ESCAPE = new RegExp(
41
+ `([${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${ESCAPE}${esc(SEPARATOR)}])`,
42
+ "g"
43
+ );
44
+ var VALUE_ESCAPE = new RegExp(
45
+ `([${esc(SEPARATOR)}${TERMINATOR}${ESCAPE}])`,
46
+ "g"
47
+ );
48
+ function escapeStr(str, pattern) {
49
+ return encodeURI(str.replace(pattern, `${ESCAPE}$1`));
32
50
  }
33
- function trim(res) {
34
- return typeof res === "string" ? res.replace(/~+$/g, "").replace(/^\$/, "") : res;
35
- }
36
- function stringify(input, recursive) {
37
- if (!recursive) {
38
- return trim(stringify(input, true));
51
+ function cleanResult(str, standalone) {
52
+ while (str.endsWith(TERMINATOR)) str = str.slice(0, -1);
53
+ const datePattern = new RegExp(`^${esc(TYPE_STRING)}${DATE_PREFIX}-?\\d+$`);
54
+ if (standalone && datePattern.test(str)) {
55
+ return str.slice(1);
56
+ }
57
+ if (!standalone && str.startsWith(TYPE_OBJECT)) {
58
+ return str.slice(1);
39
59
  }
40
- if (typeof input === "function") {
41
- return "";
60
+ return str;
61
+ }
62
+ function stringify(value, standalone = false) {
63
+ return cleanResult(serialize(value, standalone), standalone);
64
+ }
65
+ function serialize(value, standalone, inArray = false) {
66
+ if (value === null) return `${TYPE_PRIMITIVE}null`;
67
+ if (value === void 0) return `${TYPE_PRIMITIVE}undefined`;
68
+ if (typeof value === "function") return "";
69
+ if (typeof value === "number") {
70
+ return `${TYPE_PRIMITIVE}${String(value).replace(/\./g, `${ESCAPE}.`)}`;
42
71
  }
43
- if (typeof input === "number" || input === true || input === false || input === null) {
44
- const value = String(input);
45
- return ":" + value.replace(/\./g, "/.");
72
+ if (typeof value === "boolean") {
73
+ return `${TYPE_PRIMITIVE}${value}`;
46
74
  }
47
- const res = [];
48
- if (Array.isArray(input)) {
49
- for (const elem of input) {
50
- typeof elem === "undefined" ? res.push(":null") : res.push(stringify(elem, true));
51
- }
52
- return "@" + res.join("..") + "~";
75
+ if (value instanceof Date) {
76
+ return `${TYPE_STRING}${DATE_PREFIX}${value.getTime()}`;
53
77
  }
54
- if (input instanceof Date) {
55
- return "=!Date:" + encodeString(input.toISOString(), valueStringifyRegexp);
78
+ if (Array.isArray(value)) {
79
+ const items = value.map((v) => serialize(v, standalone, true));
80
+ return `${TYPE_ARRAY}${items.join(SEPARATOR)}${TERMINATOR}`;
56
81
  }
57
- if (typeof input === "object") {
58
- for (const [key, value] of Object.entries(input)) {
59
- const stringifiedValue = stringify(value, true);
60
- if (stringifiedValue) {
61
- res.push(encodeString(key, keyStringifyRegexp) + stringifiedValue);
82
+ if (typeof value === "object") {
83
+ const entries = [];
84
+ for (const [k, v] of Object.entries(value)) {
85
+ const val = serialize(v, false, false);
86
+ if (val || v === void 0) {
87
+ entries.push(`${escapeStr(k, KEY_ESCAPE)}${val}`);
62
88
  }
63
89
  }
64
- return "$" + res.join("..") + "~";
90
+ return `${TYPE_OBJECT}${entries.join(SEPARATOR)}${TERMINATOR}`;
65
91
  }
66
- if (typeof input === "undefined") {
67
- return "";
92
+ const strVal = String(value);
93
+ let escaped = escapeStr(strVal, VALUE_ESCAPE);
94
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d`);
95
+ if (datePattern.test(strVal)) {
96
+ escaped = ESCAPE + escaped;
68
97
  }
69
- return "=" + encodeString(input.toString(), valueStringifyRegexp);
98
+ return standalone || inArray ? escaped : `${TYPE_STRING}${escaped}`;
70
99
  }
71
- function parse(str) {
72
- if (!str.startsWith("$")) {
73
- str = "$" + str;
100
+ function parse(input, standalone = false) {
101
+ const str = decodeURI(input);
102
+ if (str.length === 0) {
103
+ return standalone ? "" : {};
104
+ }
105
+ const first = str[0];
106
+ const hasMarker = first === TYPE_STRING || first === TYPE_PRIMITIVE || first === TYPE_ARRAY || first === TYPE_OBJECT;
107
+ let source;
108
+ if (hasMarker) {
109
+ source = str;
110
+ } else if (standalone) {
111
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
112
+ if (datePattern.test(str)) {
113
+ const timestamp = parseInt(str.slice(DATE_PREFIX.length), 10);
114
+ if (!isNaN(timestamp)) {
115
+ return new Date(timestamp);
116
+ }
117
+ }
118
+ return str;
119
+ } else {
120
+ const markers = `${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${TYPE_OBJECT}`;
121
+ const objectPattern = new RegExp(
122
+ `[^${ESCAPE}${markers}${SEPARATOR}${TERMINATOR}][${markers}]`
123
+ );
124
+ source = objectPattern.test(str) ? `${TYPE_OBJECT}${str}` : `${TYPE_STRING}${str}`;
74
125
  }
75
126
  let pos = 0;
76
- str = decodeURI(str);
77
- function readToken(regexp) {
78
- let token = "";
79
- for (; pos !== str.length; ++pos) {
80
- if (str.charAt(pos) === "/") {
81
- pos += 1;
82
- if (pos === str.length) {
83
- token += "~";
84
- break;
127
+ function peek() {
128
+ return source[pos] || "";
129
+ }
130
+ function advance() {
131
+ pos++;
132
+ }
133
+ function readUntil(stopPattern, checkEscape = false) {
134
+ let result = "";
135
+ let wasEscaped = false;
136
+ while (pos < source.length) {
137
+ const ch = peek();
138
+ if (ch === ESCAPE) {
139
+ if (checkEscape && pos + 1 < source.length && source[pos + 1] === DATE_PREFIX) {
140
+ wasEscaped = true;
141
+ }
142
+ advance();
143
+ if (pos < source.length) {
144
+ result += peek();
145
+ advance();
85
146
  }
86
- } else if (str.charAt(pos).match(regexp)) {
87
- break;
147
+ continue;
88
148
  }
89
- token += str.charAt(pos);
149
+ if (stopPattern.test(ch)) break;
150
+ result += ch;
151
+ advance();
90
152
  }
91
- return token;
92
- }
93
- function parseToken() {
94
- const type = str.charAt(pos++);
95
- if (type === "=") {
96
- const value = readToken(valueParseRegexp);
97
- if (value.startsWith("!Date:")) {
98
- return new Date(value.slice("!Date:".length));
153
+ return { value: result, wasEscaped };
154
+ }
155
+ function parseString() {
156
+ const { value, wasEscaped } = readUntil(VALUE_STOP, true);
157
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
158
+ if (!wasEscaped && datePattern.test(value)) {
159
+ const timestamp = parseInt(value.slice(DATE_PREFIX.length), 10);
160
+ if (!isNaN(timestamp)) {
161
+ return new Date(timestamp);
99
162
  }
100
- return value;
101
163
  }
102
- if (type === ":") {
103
- const value = readToken(valueParseRegexp);
104
- if (value === "true") {
105
- return true;
164
+ return value;
165
+ }
166
+ function parsePrimitive() {
167
+ const { value } = readUntil(VALUE_STOP, false);
168
+ if (value === "null") return null;
169
+ if (value === "undefined") return void 0;
170
+ if (value === "true") return true;
171
+ if (value === "false") return false;
172
+ return parseFloat(value);
173
+ }
174
+ function parseArray() {
175
+ const result = [];
176
+ let lastWasSeparator = false;
177
+ while (pos < source.length && peek() !== TERMINATOR) {
178
+ if (peek() === SEPARATOR) {
179
+ result.push("");
180
+ advance();
181
+ lastWasSeparator = true;
182
+ continue;
106
183
  }
107
- if (value === "false") {
108
- return false;
184
+ lastWasSeparator = false;
185
+ const ch = peek();
186
+ if (ch === TYPE_PRIMITIVE || ch === TYPE_ARRAY || ch === TYPE_OBJECT || ch === TYPE_STRING) {
187
+ result.push(parseValue());
188
+ } else {
189
+ result.push(parseString());
109
190
  }
110
- const parsedValue = parseFloat(value);
111
- return isNaN(parsedValue) ? null : parsedValue;
112
- }
113
- if (type === "@") {
114
- const res = [];
115
- loop: {
116
- if (pos >= str.length || str.charAt(pos) === "~") {
117
- break loop;
118
- }
119
- while (true) {
120
- res.push(parseToken());
121
- if (pos >= str.length || str.charAt(pos) === "~") {
122
- break loop;
123
- }
124
- if (str.charAt(pos) === "." && str.charAt(pos + 1) === ".") {
125
- pos += 2;
126
- } else {
127
- pos += 1;
128
- }
129
- }
191
+ if (pos < source.length && peek() === SEPARATOR) {
192
+ advance();
193
+ lastWasSeparator = true;
130
194
  }
131
- pos += 1;
132
- return res;
133
195
  }
134
- if (type === "$") {
135
- const res = {};
136
- loop: {
137
- if (pos >= str.length || str.charAt(pos) === "~") {
138
- break loop;
139
- }
140
- while (true) {
141
- const name = readToken(keyParseRegexp);
142
- res[name] = parseToken();
143
- if (pos >= str.length || str.charAt(pos) === "~") {
144
- break loop;
145
- }
146
- if (str.charAt(pos) === "." && str.charAt(pos + 1) === ".") {
147
- pos += 2;
148
- } else {
149
- pos += 1;
150
- }
151
- }
196
+ if (lastWasSeparator && (peek() === TERMINATOR || pos >= source.length)) {
197
+ result.push("");
198
+ }
199
+ if (peek() === TERMINATOR) advance();
200
+ return result;
201
+ }
202
+ function parseObject() {
203
+ const result = {};
204
+ while (pos < source.length && peek() !== TERMINATOR) {
205
+ const { value: key } = readUntil(KEY_STOP, false);
206
+ result[key] = parseValue();
207
+ if (pos < source.length && peek() !== TERMINATOR && peek() === SEPARATOR) {
208
+ advance();
152
209
  }
153
- pos += 1;
154
- return res;
155
210
  }
156
- throw new Error('Unexpected char "' + type + '" at position ' + (pos - 1));
211
+ if (peek() === TERMINATOR) advance();
212
+ return result;
213
+ }
214
+ function parseValue() {
215
+ const type = peek();
216
+ advance();
217
+ switch (type) {
218
+ case TYPE_PRIMITIVE:
219
+ return parsePrimitive();
220
+ case TYPE_ARRAY:
221
+ return parseArray();
222
+ case TYPE_OBJECT:
223
+ return parseObject();
224
+ case TYPE_STRING:
225
+ return parseString();
226
+ default:
227
+ throw new Error(`Unexpected type "${type}" at position ${pos}`);
228
+ }
157
229
  }
158
- return parseToken();
230
+ return parseValue();
159
231
  }
232
+ var readable = { stringify, parse };
160
233
  // Annotate the CommonJS export names for ESM import in node:
161
234
  0 && (module.exports = {
162
235
  parse,
236
+ readable,
163
237
  stringify
164
238
  });
@@ -1,139 +1,212 @@
1
1
  // src/format/readable.ts
2
- var keyStringifyRegexp = /([=:@$/.])/g;
3
- var valueStringifyRegexp = /([.~/])/g;
4
- var keyParseRegexp = /[=:@$.]/;
5
- var valueParseRegexp = /[.~]/;
6
- function encodeString(str, regexp) {
7
- return encodeURI(str.replace(regexp, "/$1"));
2
+ var TYPE_OBJECT = ".";
3
+ var TYPE_ARRAY = "@";
4
+ var TYPE_STRING = "=";
5
+ var TYPE_PRIMITIVE = ":";
6
+ var SEPARATOR = ",";
7
+ var TERMINATOR = "~";
8
+ var ESCAPE = "/";
9
+ var DATE_PREFIX = "D";
10
+ var esc = (c) => /[.$^*+?()[\]{}|\\]/.test(c) ? `\\${c}` : c;
11
+ var KEY_STOP = new RegExp(
12
+ `[${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${esc(SEPARATOR)}]`
13
+ );
14
+ var VALUE_STOP = new RegExp(`[${esc(SEPARATOR)}${TERMINATOR}]`);
15
+ var KEY_ESCAPE = new RegExp(
16
+ `([${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${ESCAPE}${esc(SEPARATOR)}])`,
17
+ "g"
18
+ );
19
+ var VALUE_ESCAPE = new RegExp(
20
+ `([${esc(SEPARATOR)}${TERMINATOR}${ESCAPE}])`,
21
+ "g"
22
+ );
23
+ function escapeStr(str, pattern) {
24
+ return encodeURI(str.replace(pattern, `${ESCAPE}$1`));
8
25
  }
9
- function trim(res) {
10
- return typeof res === "string" ? res.replace(/~+$/g, "").replace(/^\$/, "") : res;
11
- }
12
- function stringify(input, recursive) {
13
- if (!recursive) {
14
- return trim(stringify(input, true));
26
+ function cleanResult(str, standalone) {
27
+ while (str.endsWith(TERMINATOR)) str = str.slice(0, -1);
28
+ const datePattern = new RegExp(`^${esc(TYPE_STRING)}${DATE_PREFIX}-?\\d+$`);
29
+ if (standalone && datePattern.test(str)) {
30
+ return str.slice(1);
31
+ }
32
+ if (!standalone && str.startsWith(TYPE_OBJECT)) {
33
+ return str.slice(1);
15
34
  }
16
- if (typeof input === "function") {
17
- return "";
35
+ return str;
36
+ }
37
+ function stringify(value, standalone = false) {
38
+ return cleanResult(serialize(value, standalone), standalone);
39
+ }
40
+ function serialize(value, standalone, inArray = false) {
41
+ if (value === null) return `${TYPE_PRIMITIVE}null`;
42
+ if (value === void 0) return `${TYPE_PRIMITIVE}undefined`;
43
+ if (typeof value === "function") return "";
44
+ if (typeof value === "number") {
45
+ return `${TYPE_PRIMITIVE}${String(value).replace(/\./g, `${ESCAPE}.`)}`;
18
46
  }
19
- if (typeof input === "number" || input === true || input === false || input === null) {
20
- const value = String(input);
21
- return ":" + value.replace(/\./g, "/.");
47
+ if (typeof value === "boolean") {
48
+ return `${TYPE_PRIMITIVE}${value}`;
22
49
  }
23
- const res = [];
24
- if (Array.isArray(input)) {
25
- for (const elem of input) {
26
- typeof elem === "undefined" ? res.push(":null") : res.push(stringify(elem, true));
27
- }
28
- return "@" + res.join("..") + "~";
50
+ if (value instanceof Date) {
51
+ return `${TYPE_STRING}${DATE_PREFIX}${value.getTime()}`;
29
52
  }
30
- if (input instanceof Date) {
31
- return "=!Date:" + encodeString(input.toISOString(), valueStringifyRegexp);
53
+ if (Array.isArray(value)) {
54
+ const items = value.map((v) => serialize(v, standalone, true));
55
+ return `${TYPE_ARRAY}${items.join(SEPARATOR)}${TERMINATOR}`;
32
56
  }
33
- if (typeof input === "object") {
34
- for (const [key, value] of Object.entries(input)) {
35
- const stringifiedValue = stringify(value, true);
36
- if (stringifiedValue) {
37
- res.push(encodeString(key, keyStringifyRegexp) + stringifiedValue);
57
+ if (typeof value === "object") {
58
+ const entries = [];
59
+ for (const [k, v] of Object.entries(value)) {
60
+ const val = serialize(v, false, false);
61
+ if (val || v === void 0) {
62
+ entries.push(`${escapeStr(k, KEY_ESCAPE)}${val}`);
38
63
  }
39
64
  }
40
- return "$" + res.join("..") + "~";
65
+ return `${TYPE_OBJECT}${entries.join(SEPARATOR)}${TERMINATOR}`;
41
66
  }
42
- if (typeof input === "undefined") {
43
- return "";
67
+ const strVal = String(value);
68
+ let escaped = escapeStr(strVal, VALUE_ESCAPE);
69
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d`);
70
+ if (datePattern.test(strVal)) {
71
+ escaped = ESCAPE + escaped;
44
72
  }
45
- return "=" + encodeString(input.toString(), valueStringifyRegexp);
73
+ return standalone || inArray ? escaped : `${TYPE_STRING}${escaped}`;
46
74
  }
47
- function parse(str) {
48
- if (!str.startsWith("$")) {
49
- str = "$" + str;
75
+ function parse(input, standalone = false) {
76
+ const str = decodeURI(input);
77
+ if (str.length === 0) {
78
+ return standalone ? "" : {};
79
+ }
80
+ const first = str[0];
81
+ const hasMarker = first === TYPE_STRING || first === TYPE_PRIMITIVE || first === TYPE_ARRAY || first === TYPE_OBJECT;
82
+ let source;
83
+ if (hasMarker) {
84
+ source = str;
85
+ } else if (standalone) {
86
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
87
+ if (datePattern.test(str)) {
88
+ const timestamp = parseInt(str.slice(DATE_PREFIX.length), 10);
89
+ if (!isNaN(timestamp)) {
90
+ return new Date(timestamp);
91
+ }
92
+ }
93
+ return str;
94
+ } else {
95
+ const markers = `${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${TYPE_OBJECT}`;
96
+ const objectPattern = new RegExp(
97
+ `[^${ESCAPE}${markers}${SEPARATOR}${TERMINATOR}][${markers}]`
98
+ );
99
+ source = objectPattern.test(str) ? `${TYPE_OBJECT}${str}` : `${TYPE_STRING}${str}`;
50
100
  }
51
101
  let pos = 0;
52
- str = decodeURI(str);
53
- function readToken(regexp) {
54
- let token = "";
55
- for (; pos !== str.length; ++pos) {
56
- if (str.charAt(pos) === "/") {
57
- pos += 1;
58
- if (pos === str.length) {
59
- token += "~";
60
- break;
102
+ function peek() {
103
+ return source[pos] || "";
104
+ }
105
+ function advance() {
106
+ pos++;
107
+ }
108
+ function readUntil(stopPattern, checkEscape = false) {
109
+ let result = "";
110
+ let wasEscaped = false;
111
+ while (pos < source.length) {
112
+ const ch = peek();
113
+ if (ch === ESCAPE) {
114
+ if (checkEscape && pos + 1 < source.length && source[pos + 1] === DATE_PREFIX) {
115
+ wasEscaped = true;
116
+ }
117
+ advance();
118
+ if (pos < source.length) {
119
+ result += peek();
120
+ advance();
61
121
  }
62
- } else if (str.charAt(pos).match(regexp)) {
63
- break;
122
+ continue;
64
123
  }
65
- token += str.charAt(pos);
124
+ if (stopPattern.test(ch)) break;
125
+ result += ch;
126
+ advance();
66
127
  }
67
- return token;
68
- }
69
- function parseToken() {
70
- const type = str.charAt(pos++);
71
- if (type === "=") {
72
- const value = readToken(valueParseRegexp);
73
- if (value.startsWith("!Date:")) {
74
- return new Date(value.slice("!Date:".length));
128
+ return { value: result, wasEscaped };
129
+ }
130
+ function parseString() {
131
+ const { value, wasEscaped } = readUntil(VALUE_STOP, true);
132
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
133
+ if (!wasEscaped && datePattern.test(value)) {
134
+ const timestamp = parseInt(value.slice(DATE_PREFIX.length), 10);
135
+ if (!isNaN(timestamp)) {
136
+ return new Date(timestamp);
75
137
  }
76
- return value;
77
138
  }
78
- if (type === ":") {
79
- const value = readToken(valueParseRegexp);
80
- if (value === "true") {
81
- return true;
139
+ return value;
140
+ }
141
+ function parsePrimitive() {
142
+ const { value } = readUntil(VALUE_STOP, false);
143
+ if (value === "null") return null;
144
+ if (value === "undefined") return void 0;
145
+ if (value === "true") return true;
146
+ if (value === "false") return false;
147
+ return parseFloat(value);
148
+ }
149
+ function parseArray() {
150
+ const result = [];
151
+ let lastWasSeparator = false;
152
+ while (pos < source.length && peek() !== TERMINATOR) {
153
+ if (peek() === SEPARATOR) {
154
+ result.push("");
155
+ advance();
156
+ lastWasSeparator = true;
157
+ continue;
82
158
  }
83
- if (value === "false") {
84
- return false;
159
+ lastWasSeparator = false;
160
+ const ch = peek();
161
+ if (ch === TYPE_PRIMITIVE || ch === TYPE_ARRAY || ch === TYPE_OBJECT || ch === TYPE_STRING) {
162
+ result.push(parseValue());
163
+ } else {
164
+ result.push(parseString());
85
165
  }
86
- const parsedValue = parseFloat(value);
87
- return isNaN(parsedValue) ? null : parsedValue;
88
- }
89
- if (type === "@") {
90
- const res = [];
91
- loop: {
92
- if (pos >= str.length || str.charAt(pos) === "~") {
93
- break loop;
94
- }
95
- while (true) {
96
- res.push(parseToken());
97
- if (pos >= str.length || str.charAt(pos) === "~") {
98
- break loop;
99
- }
100
- if (str.charAt(pos) === "." && str.charAt(pos + 1) === ".") {
101
- pos += 2;
102
- } else {
103
- pos += 1;
104
- }
105
- }
166
+ if (pos < source.length && peek() === SEPARATOR) {
167
+ advance();
168
+ lastWasSeparator = true;
106
169
  }
107
- pos += 1;
108
- return res;
109
170
  }
110
- if (type === "$") {
111
- const res = {};
112
- loop: {
113
- if (pos >= str.length || str.charAt(pos) === "~") {
114
- break loop;
115
- }
116
- while (true) {
117
- const name = readToken(keyParseRegexp);
118
- res[name] = parseToken();
119
- if (pos >= str.length || str.charAt(pos) === "~") {
120
- break loop;
121
- }
122
- if (str.charAt(pos) === "." && str.charAt(pos + 1) === ".") {
123
- pos += 2;
124
- } else {
125
- pos += 1;
126
- }
127
- }
171
+ if (lastWasSeparator && (peek() === TERMINATOR || pos >= source.length)) {
172
+ result.push("");
173
+ }
174
+ if (peek() === TERMINATOR) advance();
175
+ return result;
176
+ }
177
+ function parseObject() {
178
+ const result = {};
179
+ while (pos < source.length && peek() !== TERMINATOR) {
180
+ const { value: key } = readUntil(KEY_STOP, false);
181
+ result[key] = parseValue();
182
+ if (pos < source.length && peek() !== TERMINATOR && peek() === SEPARATOR) {
183
+ advance();
128
184
  }
129
- pos += 1;
130
- return res;
131
185
  }
132
- throw new Error('Unexpected char "' + type + '" at position ' + (pos - 1));
186
+ if (peek() === TERMINATOR) advance();
187
+ return result;
188
+ }
189
+ function parseValue() {
190
+ const type = peek();
191
+ advance();
192
+ switch (type) {
193
+ case TYPE_PRIMITIVE:
194
+ return parsePrimitive();
195
+ case TYPE_ARRAY:
196
+ return parseArray();
197
+ case TYPE_OBJECT:
198
+ return parseObject();
199
+ case TYPE_STRING:
200
+ return parseString();
201
+ default:
202
+ throw new Error(`Unexpected type "${type}" at position ${pos}`);
203
+ }
133
204
  }
134
- return parseToken();
205
+ return parseValue();
135
206
  }
207
+ var readable = { stringify, parse };
136
208
  export {
137
209
  parse,
210
+ readable,
138
211
  stringify
139
212
  };
package/dist/index.d.mts CHANGED
@@ -3,17 +3,15 @@ import { StoreMutatorIdentifier, StateCreator } from 'zustand/vanilla';
3
3
  type DeepSelect<T> = T extends object ? {
4
4
  [P in keyof T]?: DeepSelect<T[P]> | boolean;
5
5
  } : boolean;
6
- type DeepPartial<T> = T extends object ? {
7
- [P in keyof T]?: DeepPartial<T[P]>;
8
- } : T;
6
+ interface QueryStringFormat {
7
+ stringify: (value: any, standalone?: boolean) => string;
8
+ parse: (value: string, standalone?: boolean) => any;
9
+ }
9
10
  interface QueryStringOptions<T> {
10
11
  url?: string;
11
12
  select?: (pathname: string) => DeepSelect<T>;
12
- key?: string;
13
- format?: {
14
- stringify: (value: DeepPartial<T>) => string;
15
- parse: (value: string) => DeepPartial<T>;
16
- };
13
+ key?: string | false;
14
+ format?: QueryStringFormat;
17
15
  syncNull?: boolean;
18
16
  syncUndefined?: boolean;
19
17
  }
package/dist/index.d.ts CHANGED
@@ -3,17 +3,15 @@ import { StoreMutatorIdentifier, StateCreator } from 'zustand/vanilla';
3
3
  type DeepSelect<T> = T extends object ? {
4
4
  [P in keyof T]?: DeepSelect<T[P]> | boolean;
5
5
  } : boolean;
6
- type DeepPartial<T> = T extends object ? {
7
- [P in keyof T]?: DeepPartial<T[P]>;
8
- } : T;
6
+ interface QueryStringFormat {
7
+ stringify: (value: any, standalone?: boolean) => string;
8
+ parse: (value: string, standalone?: boolean) => any;
9
+ }
9
10
  interface QueryStringOptions<T> {
10
11
  url?: string;
11
12
  select?: (pathname: string) => DeepSelect<T>;
12
- key?: string;
13
- format?: {
14
- stringify: (value: DeepPartial<T>) => string;
15
- parse: (value: string) => DeepPartial<T>;
16
- };
13
+ key?: string | false;
14
+ format?: QueryStringFormat;
17
15
  syncNull?: boolean;
18
16
  syncUndefined?: boolean;
19
17
  }
package/dist/index.js CHANGED
@@ -40,22 +40,25 @@ function parse(str) {
40
40
  // src/middleware.ts
41
41
  var compact = (newState, initialState, syncNull = false, syncUndefined = false) => {
42
42
  const output = {};
43
+ const removed = [];
43
44
  Object.keys(newState).forEach((key) => {
44
45
  const newValue = newState[key];
45
46
  const initialValue = initialState[key];
46
47
  if (typeof newValue !== "function" && !(0, import_lodash_es.isEqual)(newValue, initialValue) && (syncNull || newValue !== null) && (syncUndefined || newValue !== void 0)) {
47
48
  const isPlainObject = typeof newValue === "object" && newValue !== null && newValue !== void 0 && !Array.isArray(newValue) && newValue.constructor === Object;
48
49
  if (isPlainObject && initialValue && typeof initialValue === "object") {
49
- const value = compact(newValue, initialValue, syncNull, syncUndefined);
50
- if (value && Object.keys(value).length > 0) {
51
- output[key] = value;
50
+ const result = compact(newValue, initialValue, syncNull, syncUndefined);
51
+ if (result.output && Object.keys(result.output).length > 0) {
52
+ output[key] = result.output;
52
53
  }
53
54
  } else {
54
55
  output[key] = newValue;
55
56
  }
57
+ } else {
58
+ removed.push(key);
56
59
  }
57
60
  });
58
- return output;
61
+ return { output, removed };
59
62
  };
60
63
  var translateSelectionToState = (selection, state) => {
61
64
  if (typeof state !== "object" || !state) {
@@ -87,7 +90,16 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
87
90
  syncUndefined: false,
88
91
  ...options
89
92
  };
93
+ const standalone = !defaultedOptions.key;
90
94
  const getStateFromUrl = (url) => {
95
+ if (standalone) {
96
+ const params2 = new URLSearchParams(url.search);
97
+ const state = {};
98
+ params2.forEach((value, key) => {
99
+ state[key] = defaultedOptions.format.parse(value, true);
100
+ });
101
+ return Object.keys(state).length > 0 ? state : null;
102
+ }
91
103
  const params = url.search.slice(1).split("&");
92
104
  for (const param of params) {
93
105
  const eqIndex = param.indexOf("=");
@@ -95,7 +107,7 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
95
107
  const key = param.slice(0, eqIndex);
96
108
  if (key === defaultedOptions.key) {
97
109
  const value = param.slice(eqIndex + 1);
98
- return value ? defaultedOptions.format.parse(value) : null;
110
+ return value ? defaultedOptions.format.parse(value, false) : null;
99
111
  }
100
112
  }
101
113
  return null;
@@ -137,7 +149,7 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
137
149
  const setQuery = () => {
138
150
  const url = new URL(window.location.href);
139
151
  const selectedState = getSelectedState(get(), url.pathname);
140
- const newCompacted = compact(
152
+ const { output: newCompacted } = compact(
141
153
  selectedState,
142
154
  initialState,
143
155
  defaultedOptions.syncNull,
@@ -145,21 +157,31 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
145
157
  );
146
158
  const previous = url.search;
147
159
  const params = url.search.slice(1).split("&").filter(Boolean);
148
- let stateIndex = -1;
149
- const otherParams = params.filter((p, i) => {
150
- const [key] = p.split("=", 1);
151
- if (key === defaultedOptions.key) {
152
- stateIndex = i;
153
- return false;
160
+ const managedKeys = standalone ? new Set(Object.keys(selectedState).map(encodeURI)) : /* @__PURE__ */ new Set([defaultedOptions.key]);
161
+ const valuesToWrite = standalone ? new Map(
162
+ Object.entries(newCompacted).map(([k, v]) => [encodeURI(k), v])
163
+ ) : Object.keys(newCompacted).length ? /* @__PURE__ */ new Map([[defaultedOptions.key, newCompacted]]) : /* @__PURE__ */ new Map();
164
+ const result = [];
165
+ params.forEach((p) => {
166
+ const key = p.split("=")[0];
167
+ if (!managedKeys.has(key)) {
168
+ result.push(p);
169
+ return;
170
+ }
171
+ if (valuesToWrite.has(key)) {
172
+ const encoded = defaultedOptions.format.stringify(
173
+ valuesToWrite.get(key),
174
+ standalone
175
+ );
176
+ result.push(`${key}=${encoded}`);
177
+ valuesToWrite.delete(key);
154
178
  }
155
- return true;
156
179
  });
157
- if (Object.keys(newCompacted).length) {
158
- const value = defaultedOptions.format.stringify(newCompacted);
159
- const position = stateIndex === -1 ? otherParams.length : stateIndex;
160
- otherParams.splice(position, 0, `${defaultedOptions.key}=${value}`);
161
- }
162
- url.search = otherParams.length ? "?" + otherParams.join("&") : "";
180
+ valuesToWrite.forEach((value, key) => {
181
+ const encoded = defaultedOptions.format.stringify(value, standalone);
182
+ result.push(`${key}=${encoded}`);
183
+ });
184
+ url.search = result.length ? "?" + result.join("&") : "";
163
185
  if (url.search !== previous) {
164
186
  history.replaceState(history.state, "", url);
165
187
  }
package/dist/index.mjs CHANGED
@@ -12,22 +12,25 @@ function parse(str) {
12
12
  // src/middleware.ts
13
13
  var compact = (newState, initialState, syncNull = false, syncUndefined = false) => {
14
14
  const output = {};
15
+ const removed = [];
15
16
  Object.keys(newState).forEach((key) => {
16
17
  const newValue = newState[key];
17
18
  const initialValue = initialState[key];
18
19
  if (typeof newValue !== "function" && !isEqual(newValue, initialValue) && (syncNull || newValue !== null) && (syncUndefined || newValue !== void 0)) {
19
20
  const isPlainObject = typeof newValue === "object" && newValue !== null && newValue !== void 0 && !Array.isArray(newValue) && newValue.constructor === Object;
20
21
  if (isPlainObject && initialValue && typeof initialValue === "object") {
21
- const value = compact(newValue, initialValue, syncNull, syncUndefined);
22
- if (value && Object.keys(value).length > 0) {
23
- output[key] = value;
22
+ const result = compact(newValue, initialValue, syncNull, syncUndefined);
23
+ if (result.output && Object.keys(result.output).length > 0) {
24
+ output[key] = result.output;
24
25
  }
25
26
  } else {
26
27
  output[key] = newValue;
27
28
  }
29
+ } else {
30
+ removed.push(key);
28
31
  }
29
32
  });
30
- return output;
33
+ return { output, removed };
31
34
  };
32
35
  var translateSelectionToState = (selection, state) => {
33
36
  if (typeof state !== "object" || !state) {
@@ -59,7 +62,16 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
59
62
  syncUndefined: false,
60
63
  ...options
61
64
  };
65
+ const standalone = !defaultedOptions.key;
62
66
  const getStateFromUrl = (url) => {
67
+ if (standalone) {
68
+ const params2 = new URLSearchParams(url.search);
69
+ const state = {};
70
+ params2.forEach((value, key) => {
71
+ state[key] = defaultedOptions.format.parse(value, true);
72
+ });
73
+ return Object.keys(state).length > 0 ? state : null;
74
+ }
63
75
  const params = url.search.slice(1).split("&");
64
76
  for (const param of params) {
65
77
  const eqIndex = param.indexOf("=");
@@ -67,7 +79,7 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
67
79
  const key = param.slice(0, eqIndex);
68
80
  if (key === defaultedOptions.key) {
69
81
  const value = param.slice(eqIndex + 1);
70
- return value ? defaultedOptions.format.parse(value) : null;
82
+ return value ? defaultedOptions.format.parse(value, false) : null;
71
83
  }
72
84
  }
73
85
  return null;
@@ -109,7 +121,7 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
109
121
  const setQuery = () => {
110
122
  const url = new URL(window.location.href);
111
123
  const selectedState = getSelectedState(get(), url.pathname);
112
- const newCompacted = compact(
124
+ const { output: newCompacted } = compact(
113
125
  selectedState,
114
126
  initialState,
115
127
  defaultedOptions.syncNull,
@@ -117,21 +129,31 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
117
129
  );
118
130
  const previous = url.search;
119
131
  const params = url.search.slice(1).split("&").filter(Boolean);
120
- let stateIndex = -1;
121
- const otherParams = params.filter((p, i) => {
122
- const [key] = p.split("=", 1);
123
- if (key === defaultedOptions.key) {
124
- stateIndex = i;
125
- return false;
132
+ const managedKeys = standalone ? new Set(Object.keys(selectedState).map(encodeURI)) : /* @__PURE__ */ new Set([defaultedOptions.key]);
133
+ const valuesToWrite = standalone ? new Map(
134
+ Object.entries(newCompacted).map(([k, v]) => [encodeURI(k), v])
135
+ ) : Object.keys(newCompacted).length ? /* @__PURE__ */ new Map([[defaultedOptions.key, newCompacted]]) : /* @__PURE__ */ new Map();
136
+ const result = [];
137
+ params.forEach((p) => {
138
+ const key = p.split("=")[0];
139
+ if (!managedKeys.has(key)) {
140
+ result.push(p);
141
+ return;
142
+ }
143
+ if (valuesToWrite.has(key)) {
144
+ const encoded = defaultedOptions.format.stringify(
145
+ valuesToWrite.get(key),
146
+ standalone
147
+ );
148
+ result.push(`${key}=${encoded}`);
149
+ valuesToWrite.delete(key);
126
150
  }
127
- return true;
128
151
  });
129
- if (Object.keys(newCompacted).length) {
130
- const value = defaultedOptions.format.stringify(newCompacted);
131
- const position = stateIndex === -1 ? otherParams.length : stateIndex;
132
- otherParams.splice(position, 0, `${defaultedOptions.key}=${value}`);
133
- }
134
- url.search = otherParams.length ? "?" + otherParams.join("&") : "";
152
+ valuesToWrite.forEach((value, key) => {
153
+ const encoded = defaultedOptions.format.stringify(value, standalone);
154
+ result.push(`${key}=${encoded}`);
155
+ });
156
+ url.search = result.length ? "?" + result.join("&") : "";
135
157
  if (url.search !== previous) {
136
158
  history.replaceState(history.state, "", url);
137
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zustand-querystring",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",