Skip to content

Commit d6e6bf8

Browse files
committed
Add ssp for base64 string encoding
1 parent 24de735 commit d6e6bf8

File tree

6 files changed

+105
-11
lines changed

6 files changed

+105
-11
lines changed

README.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ The simplest and most effective way to use this library is by importing the meth
5454
Your username is {$username}
5555
```
5656

57-
the function returns a store so make sure to use it with the `$` prepended to handle auto-subscriprion. In case there's not a query parameter with the chosen name it will simply be null.
57+
the function returns a store so make sure to use it with the `$` prepended to handle auto-subscription. In case there's not a query parameter with the chosen name it will simply be null.
5858

5959
### Writing to the store (single parameter)
6060

61-
Reading query parameters is cool but you know what is even cooler? Writing query parameters! With this library you can treat your store just like normal state in svelte. To update the state and conseguentely the url you can just do this
61+
Reading query parameters is cool but you know what is even cooler? Writing query parameters! With this library you can treat your store just like normal state in svelte. To update the state and consequently the url you can just do this
6262

6363
```svelte
6464
<script lang="ts">
@@ -107,7 +107,7 @@ The count is {$count}
107107
<input bind:value={$count} type="number" />
108108
```
109109

110-
this time $count would be of type number and the deconding function it's what's used to update the url when you write to the store.
110+
this time $count would be of type number and the decoding function it's what's used to update the url when you write to the store.
111111

112112
### Default values
113113

@@ -322,7 +322,7 @@ There are six helpers all exported as functions on the object ssp. To each one o
322322

323323
#### object
324324

325-
To map from a query parameter to an object. An url like this `/?obj={"isComplex":%20true,%20"nested":%20{"field":%20"value"}}` will be mapped to
325+
To map from a query parameter to an object. A url like this `/?obj={"isComplex":%20true,%20"nested":%20{"field":%20"value"}}` will be mapped to
326326

327327
```typescript
328328
$store.obj.isComplex; //true
@@ -332,7 +332,7 @@ $store.obj.nested.value; // "value"
332332

333333
#### array
334334

335-
To map from a query parameter to an array. An url like this `/?arr=[1,2,3,4]` will be mapped to
335+
To map from a query parameter to an array. A url like this `/?arr=[1,2,3,4]` will be mapped to
336336

337337
```typescript
338338
$store.arr[0]; //1
@@ -343,27 +343,27 @@ $store.arr[3]; //4
343343

344344
#### number
345345

346-
To map from a query parameter to a number. An url like this `/?num=1` will be mapped to
346+
To map from a query parameter to a number. A url like this `/?num=1` will be mapped to
347347

348348
```typescript
349349
$store.num; //1
350350
```
351351

352352
#### boolean
353353

354-
To map from a query parameter to a boolean. An url like this `/?bool=true` will be mapped to
354+
To map from a query parameter to a boolean. A url like this `/?bool=true` will be mapped to
355355

356356
```typescript
357357
$store.bool; //true
358358
```
359359

360-
as we've seen an url like this `/?bool=false` will be mapped to
360+
as we've seen a url like this `/?bool=false` will be mapped to
361361

362362
```typescript
363363
$store.bool; //false
364364
```
365365

366-
just like an url like this `/`
366+
just like a url like this `/`
367367

368368
#### string
369369

@@ -373,12 +373,22 @@ This is exported mainly for readability since all query parameters are already s
373373

374374
To map any JSON serializable state to his lz-string representation. This is a common way to store state in query parameters that will prevent the link to directly show the state.
375375

376-
An url like this `/?state=N4IgbghgNgrgpiAXCAsgTwAQGMD2OoYCO8ATpgA4QkQC2cALnCSAL5A` will map to
376+
A url like this `/?state=N4IgbghgNgrgpiAXCAsgTwAQGMD2OoYCO8ATpgA4QkQC2cALnCSAL5A` will map to
377377

378378
```typescript
379379
$store.state.value; //My cool query parameter
380380
```
381381

382+
#### base64
383+
384+
To store more complicated strings, such as those containing unicode characters, newlines, or other special characters, you can use the base64 helper. The helper follows the "Base64 URL safe" pattern described in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
385+
386+
A url like this `/?state=YSDEgCDwkICAIOaWhyDwn6aE` will map to
387+
388+
```typescript
389+
$store.state.value; //a Ā 𐀀 文 🦄
390+
```
391+
382392
## Store options
383393

384394
Both functions accept a configuration object that contains the following properties:
@@ -468,7 +478,7 @@ To set the configuration object you can pass it as a third parameter in case of
468478
</script>
469479
```
470480

471-
## Vite dependecies error
481+
## Vite dependencies error
472482

473483
If you ran into issues with vite you need to update your `vite.config.ts` or `vite.config.js` file to include the plugin exported from `sveltekit-search-params/plugin`. It's as simple as
474484

playground/src/routes/+page.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
sort: false,
1111
});
1212
const lz = queryParam('lz', ssp.lz<string>());
13+
const base64 = queryParam('base64', ssp.base64());
1314
1415
let obj_changes = 0;
1516
let arr_changes = 0;
1617
let lz_changes = 0;
18+
let base64_changes = 0;
1719
1820
$: {
1921
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
@@ -31,6 +33,11 @@
3133
$lz;
3234
lz_changes++;
3335
}
36+
$: {
37+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
38+
$base64;
39+
base64_changes++;
40+
}
3441
</script>
3542

3643
<input data-testid="str-input" bind:value={$str} />
@@ -87,6 +94,9 @@
8794
<input data-testid="lz-input" bind:value={$lz} />
8895
<div data-testid="lz">{$lz}</div>
8996

97+
<input data-testid="base64-input" bind:value={$base64} />
98+
<div data-testid="base64">{$base64}</div>
99+
90100
<button
91101
data-testid="change-two"
92102
on:click={() => {
@@ -98,3 +108,4 @@
98108
<p data-testid="how-many-obj-changes">{obj_changes}</p>
99109
<p data-testid="how-many-arr-changes">{arr_changes}</p>
100110
<p data-testid="how-many-lz-changes">{lz_changes}</p>
111+
<p data-testid="how-many-base64-changes">{base64_changes}</p>

playground/src/routes/queryparameters/+page.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
obj: ssp.object<{ str: string }>(),
99
arr: ssp.array<number>(),
1010
lz: ssp.lz<string>(),
11+
base64: ssp.base64(),
1112
});
1213
1314
const unordered_store = queryParameters(
@@ -84,6 +85,9 @@
8485
<input data-testid="lz-input" bind:value={$store.lz} />
8586
<div data-testid="lz">{$store.lz}</div>
8687

88+
<input data-testid="base64-input" bind:value={$store.base64} />
89+
<div data-testid="base64">{$store.base64}</div>
90+
8791
<button
8892
data-testid="change-two"
8993
on:click={() => {

src/lib/ssp/base64.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { EncodeAndDecodeOptions } from '$lib/types';
2+
3+
/**
4+
* Encodes and decodes a Unicode string as utf-8 Base64 URL safe.
5+
*
6+
* See: https://developer.mozilla.org/en-US/docs/Glossary/Base64
7+
*/
8+
function sspBase64(
9+
defaultValue: string,
10+
): EncodeAndDecodeOptions<string> & { defaultValue: string };
11+
function sspBase64(): EncodeAndDecodeOptions<string> & {
12+
defaultValue: undefined;
13+
};
14+
function sspBase64(defaultValue?: string): EncodeAndDecodeOptions<string> {
15+
return {
16+
encode: (value: string) => {
17+
if (value === '') return undefined;
18+
const bytes = new TextEncoder().encode(value);
19+
const binString = Array.from(bytes, (byte) =>
20+
String.fromCodePoint(byte),
21+
).join('');
22+
return btoa(binString)
23+
.replace(/\+/g, '-')
24+
.replace(/\//g, '_')
25+
.replace(/=/g, '');
26+
},
27+
decode: (value: string | null) => {
28+
if (value === null) return '';
29+
const binString = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
30+
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)!);
31+
return new TextDecoder().decode(bytes);
32+
},
33+
defaultValue,
34+
};
35+
}
36+
37+
export { sspBase64 };

src/lib/ssp/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { sspBase64 } from './base64';
12
import {
23
arrayEncodeAndDecodeOptions,
34
objectEncodeAndDecodeOptions,
@@ -21,4 +22,5 @@ export default {
2122
object: objectEncodeAndDecodeOptions,
2223
array: arrayEncodeAndDecodeOptions,
2324
lz: lzEncodeAndDecodeOptions,
25+
base64: sspBase64,
2426
} as const;

tests/index.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ test.describe('queryParam', () => {
106106
await expect(lz_changes).toHaveText('2');
107107
});
108108

109+
test('works as expected with base64', async ({ page }) => {
110+
await page.goto('/');
111+
const input = page.getByTestId('base64-input');
112+
await input.fill('a Ā 𐀀 文 🦄');
113+
const str = page.getByTestId('base64');
114+
await expect(str).toHaveText('a Ā 𐀀 文 🦄');
115+
const url = new URL(page.url());
116+
expect(url.searchParams.get('base64')).toBe('YSDEgCDwkICAIOaWhyDwn6aE');
117+
});
118+
119+
test("changing a base64 doesn't trigger reactivity multiple times", async ({
120+
page,
121+
}) => {
122+
await page.goto('/');
123+
const input = page.getByTestId('base64-input');
124+
await input.fill('a');
125+
const base64_changes = page.getByTestId('how-many-base64-changes');
126+
await expect(base64_changes).toHaveText('2');
127+
});
128+
109129
test("changing one parameter doesn't interfere with the rest", async ({
110130
page,
111131
}) => {
@@ -262,6 +282,16 @@ test.describe('queryParameters', () => {
262282
expect(url.searchParams.get('lz')).toBe('EQGwXsQ');
263283
});
264284

285+
test('works as expected with base64', async ({ page }) => {
286+
await page.goto('/queryparameters');
287+
const input = page.getByTestId('base64-input');
288+
await input.fill('a Ā 𐀀 文 🦄');
289+
const str = page.getByTestId('base64');
290+
await expect(str).toHaveText('a Ā 𐀀 文 🦄');
291+
const url = new URL(page.url());
292+
expect(url.searchParams.get('base64')).toBe('YSDEgCDwkICAIOaWhyDwn6aE');
293+
});
294+
265295
test("changes to the store doesn't trigger reactivity multiple times", async ({
266296
page,
267297
}) => {

0 commit comments

Comments
 (0)