generated from chiffre-io/template-library
-
-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Don't URL-encode safe characters, and fix the array parser logic. Closes #355.
- Loading branch information
Showing
8 changed files
with
252 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { describe, expect, test } from 'vitest' | ||
import { encodeQueryValue, renderQueryString } from './url-encoding' | ||
|
||
describe('url-encoding/encodeQueryValue', () => { | ||
test('spaces are encoded as +', () => { | ||
expect(encodeQueryValue(' ')).toBe('+') | ||
}) | ||
test('+ are encoded', () => { | ||
expect(encodeQueryValue('+')).toBe(encodeURIComponent('+')) | ||
}) | ||
test('Hashes are encoded', () => { | ||
expect(encodeQueryValue('#')).toBe(encodeURIComponent('#')) | ||
}) | ||
test('Ampersands are encoded', () => { | ||
expect(encodeQueryValue('&')).toBe(encodeURIComponent('&')) | ||
}) | ||
test('Percent signs are encoded', () => { | ||
expect(encodeQueryValue('%')).toBe(encodeURIComponent('%')) | ||
}) | ||
test('Alphanumericals are passed through', () => { | ||
const input = 'abcdefghijklmnopqrstuvwxyz0123456789' | ||
expect(encodeQueryValue(input)).toBe(input) | ||
}) | ||
test('Other special characters are passed through', () => { | ||
const input = '-._~!$\'()*,;=:@"/?`[]{}\\|<>^' | ||
expect(encodeQueryValue(input)).toBe(input) | ||
}) | ||
test('practical use-cases', () => { | ||
const e = encodeQueryValue | ||
expect(e('a b')).toBe('a+b') | ||
expect(e('some#secret')).toBe('some%23secret') | ||
expect(e('2+2=5')).toBe('2%2B2=5') | ||
expect(e('100%')).toBe('100%25') | ||
expect(e('kool&thegang')).toBe('kool%26thegang') | ||
expect(e('a&b=c')).toBe('a%26b=c') | ||
}) | ||
}) | ||
|
||
describe('url-encoding/renderQueryString', () => { | ||
test('empty query', () => { | ||
expect(renderQueryString(new URLSearchParams())).toBe('') | ||
}) | ||
test('simple key-value pair', () => { | ||
const search = new URLSearchParams() | ||
search.set('foo', 'bar') | ||
expect(renderQueryString(search)).toBe('foo=bar') | ||
}) | ||
test('encoding', () => { | ||
const search = new URLSearchParams() | ||
search.set('test', '-._~!$\'()*,;=:@"/?`[]{}\\|<>^') | ||
expect(renderQueryString(search)).toBe( | ||
'test=-._~!$\'()*,;=:@"/?`[]{}\\|<>^' | ||
) | ||
}) | ||
test('decoding', () => { | ||
const search = new URLSearchParams() | ||
const value = '-._~!$\'()*,;=:@"/?`[]{}\\|<>^' | ||
search.set('test', value) | ||
const url = new URL('http://example.com/?' + renderQueryString(search)) | ||
expect(url.searchParams.get('test')).toBe(value) | ||
}) | ||
test('decoding plus and spaces', () => { | ||
const search = new URLSearchParams() | ||
const value = 'a b+c' | ||
search.set('test', value) | ||
const url = new URL('http://example.com/?' + renderQueryString(search)) | ||
expect(url.searchParams.get('test')).toBe(value) | ||
}) | ||
test('decoding hashes and fragment', () => { | ||
const search = new URLSearchParams() | ||
const value = 'foo#bar' | ||
search.set('test', value) | ||
const url = new URL( | ||
'http://example.com/?' + renderQueryString(search) + '#egg' | ||
) | ||
expect(url.searchParams.get('test')).toBe(value) | ||
}) | ||
test('decoding ampersands', () => { | ||
const search = new URLSearchParams() | ||
const value = 'a&b=c' | ||
search.set('test', value) | ||
const url = new URL( | ||
'http://example.com/?' + renderQueryString(search) + '&egg=spam' | ||
) | ||
expect(url.searchParams.get('test')).toBe(value) | ||
}) | ||
test('it renders query string with special characters', () => { | ||
const search = new URLSearchParams() | ||
search.set('name', 'John Doe') | ||
search.set('email', '[email protected]') | ||
search.set('message', 'Hello, world! #greeting') | ||
const query = renderQueryString(search) | ||
expect(query).toBe( | ||
'name=John+Doe&email=foo.bar%[email protected]&message=Hello,+world!+%23greeting' | ||
) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export function renderQueryString(search: URLSearchParams) { | ||
const query: string[] = [] | ||
for (const [key, value] of search.entries()) { | ||
query.push(`${key}=${encodeQueryValue(value)}`) | ||
} | ||
return query.join('&') | ||
} | ||
|
||
export function encodeQueryValue(input: string) { | ||
return ( | ||
input | ||
// Encode existing % signs first to avoid appearing | ||
// as an incomplete escape sequence: | ||
.replace(/%/g, '%25') | ||
// Note: spaces are encoded as + in RFC 3986, | ||
// so we pre-encode existing + signs to avoid confusion | ||
// before converting spaces to + signs. | ||
.replace(/\+/g, '%2B') | ||
.replace(/ /g, '+') | ||
// Encode other URI-reserved characters | ||
.replace(/#/g, '%23') | ||
.replace(/&/g, '%26') | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
packages/playground/src/app/demos/custom-parser/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
'use client' | ||
|
||
import { createParser, useQueryState } from 'next-usequerystate' | ||
|
||
type SortingState = Record<string, 'asc' | 'desc'> | ||
|
||
const parser = createParser({ | ||
parse(value) { | ||
if (value === '') { | ||
return null | ||
} | ||
const keys = value.split('|') | ||
return keys.reduce<SortingState>((acc, key) => { | ||
const [id, desc] = key.split(':') | ||
acc[id] = desc === 'desc' ? 'desc' : 'asc' | ||
return acc | ||
}, {}) | ||
}, | ||
serialize(value: SortingState) { | ||
return Object.entries(value) | ||
.map(([id, dir]) => `${id}:${dir}`) | ||
.join('|') | ||
} | ||
}) | ||
|
||
export default function BasicCounterDemoPage() { | ||
const [sort, setSort] = useQueryState('sort', parser.withDefault({})) | ||
return ( | ||
<section> | ||
<h1>Custom parser</h1> | ||
<nav style={{ display: 'flex', gap: '4px' }}> | ||
<span>Foo</span> | ||
<button | ||
style={{ padding: '2px 12px' }} | ||
onClick={() => | ||
setSort(state => ({ | ||
...state, | ||
foo: 'asc' | ||
})) | ||
} | ||
> | ||
🔼 | ||
</button> | ||
<button | ||
style={{ padding: '2px 12px' }} | ||
onClick={() => | ||
setSort(state => ({ | ||
...state, | ||
foo: 'desc' | ||
})) | ||
} | ||
> | ||
🔽 | ||
</button> | ||
<button | ||
style={{ padding: '2px 12px' }} | ||
onClick={() => | ||
setSort(({ foo: _, ...state }) => | ||
Object.keys(state).length === 0 ? null : state | ||
) | ||
} | ||
> | ||
Clear | ||
</button> | ||
<span>{sort.foo}</span> | ||
</nav> | ||
<nav style={{ display: 'flex', gap: '4px' }}> | ||
<span>Bar</span> | ||
<button | ||
style={{ padding: '2px 12px' }} | ||
onClick={() => | ||
setSort(state => ({ | ||
...state, | ||
bar: 'asc' | ||
})) | ||
} | ||
> | ||
🔼 | ||
</button> | ||
<button | ||
style={{ padding: '2px 12px' }} | ||
onClick={() => | ||
setSort(state => ({ | ||
...state, | ||
bar: 'desc' | ||
})) | ||
} | ||
> | ||
🔽 | ||
</button> | ||
<button | ||
style={{ padding: '2px 12px' }} | ||
onClick={() => | ||
setSort(({ bar: _, ...state }) => | ||
Object.keys(state).length === 0 ? null : state | ||
) | ||
} | ||
> | ||
Clear | ||
</button> | ||
<span>{sort.bar}</span> | ||
</nav> | ||
<p> | ||
<a href="https://github.com/47ng/next-usequerystate/blob/next/src/app/demos/custom-parser/page.tsx"> | ||
Source on GitHub | ||
</a> | ||
</p> | ||
</section> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters