-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
165 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { parseQuery } from './parseQuery' | ||
|
||
describe('parseQuery', () => { | ||
it('works with leading ?', () => { | ||
expect(parseQuery('?foo=a')).toEqual({ | ||
foo: 'a', | ||
}) | ||
}) | ||
|
||
it('works without leading ?', () => { | ||
expect(parseQuery('foo=a')).toEqual({ | ||
foo: 'a', | ||
}) | ||
}) | ||
|
||
it('works with an empty string', () => { | ||
const emptyQuery = parseQuery('') | ||
expect(Object.keys(emptyQuery)).toHaveLength(0) | ||
expect(emptyQuery).toEqual({}) | ||
expect(parseQuery('?')).toEqual({}) | ||
}) | ||
|
||
it('decodes values in query', () => { | ||
expect(parseQuery('e=%25')).toEqual({ | ||
e: '%', | ||
}) | ||
}) | ||
|
||
it('parses empty string values', () => { | ||
expect(parseQuery('e=&c=a')).toEqual({ | ||
e: '', | ||
c: 'a', | ||
}) | ||
}) | ||
|
||
it('allows = inside values', () => { | ||
expect(parseQuery('e=c=a')).toEqual({ | ||
e: 'c=a', | ||
}) | ||
}) | ||
|
||
it('parses empty values as null', () => { | ||
expect(parseQuery('e&b&c=a')).toEqual({ | ||
e: null, | ||
b: null, | ||
c: 'a', | ||
}) | ||
}) | ||
|
||
it('parses empty values as null in arrays', () => { | ||
expect(parseQuery('e&e&e=a')).toEqual({ | ||
e: [null, null, 'a'], | ||
}) | ||
}) | ||
|
||
it('decodes array values in query', () => { | ||
expect(parseQuery('e=%25&e=%22')).toEqual({ | ||
e: ['%', '"'], | ||
}) | ||
expect(parseQuery('e=%25&e=a')).toEqual({ | ||
e: ['%', 'a'], | ||
}) | ||
}) | ||
|
||
it('decodes the + as space', () => { | ||
expect(parseQuery('a+b=c+d')).toEqual({ | ||
'a b': 'c d', | ||
}) | ||
}) | ||
|
||
it('decodes the encoded + as +', () => { | ||
expect(parseQuery('a%2Bb=c%2Bd')).toEqual({ | ||
'a+b': 'c+d', | ||
}) | ||
}) | ||
|
||
// this is for browsers like IE that allow invalid characters | ||
it('keep invalid values as is', () => { | ||
const mockWarn = vi.spyOn(console, 'warn') | ||
expect(parseQuery('e=%&e=%25')).toEqual({ | ||
e: ['%', '%'], | ||
}) | ||
|
||
expect(mockWarn).toHaveBeenCalledTimes(1) | ||
}) | ||
}) |
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,77 @@ | ||
import { isArray } from '@utopia-utils/share' | ||
export type LocationQueryValue = string | null | ||
|
||
export type LocationQuery = Record< | ||
string, | ||
LocationQueryValue | LocationQueryValue[] | ||
> | ||
|
||
const PLUS_RE = /\+/g // %2B | ||
|
||
/** | ||
* Decode text using `decodeURIComponent`. Returns the original text if it | ||
* fails. | ||
* | ||
* @param text - string to decode | ||
* @returns decoded string | ||
*/ | ||
export function decode(text: string | number): string { | ||
try { | ||
return decodeURIComponent(`${text}`) | ||
} | ||
catch (err) { | ||
console.warn(`Error decoding "${text}". Using original value`) | ||
} | ||
return `${text}` | ||
} | ||
|
||
/** | ||
* Transforms a queryString into a {@link LocationQuery} object. Accept both, a | ||
* version with the leading `?` and without Should work as URLSearchParams | ||
* @forked from vue-router https://github.com/vuejs/router/blob/main/packages/router/src/query.ts#L93 | ||
* | ||
* @param search - search string to parse | ||
* @returns a query object | ||
* @example | ||
* ``` | ||
const params = parseQuery<{ | ||
id: string | ||
name: string[] | ||
}>('?id=2&name=Adam&name=Smith&sex') | ||
expect(params.id).toBe('2') | ||
expect(params.name).toEqual(['Adam', 'Smith']) | ||
expect(params.sex).toBe(null) | ||
``` | ||
*/ | ||
export function parseQuery<T extends LocationQuery>(search: string) { | ||
const query: LocationQuery = {} | ||
// avoid creating an object with an empty key and empty value | ||
// because of split('&') | ||
if (search === '' || search === '?') | ||
return query as T | ||
const hasLeadingIM = search[0] === '?' | ||
const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&') | ||
for (let i = 0; i < searchParams.length; ++i) { | ||
// pre decode the + into space | ||
const searchParam = searchParams[i].replace(PLUS_RE, ' ') | ||
// allow the = character | ||
const eqPos = searchParam.indexOf('=') | ||
const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos)) | ||
const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1)) | ||
|
||
if (key in query) { | ||
// an extra variable for ts types | ||
let currentValue = query[key] | ||
if (!isArray(currentValue)) | ||
currentValue = query[key] = [currentValue] | ||
|
||
// we force the modification | ||
;(currentValue as LocationQueryValue[]).push(value) | ||
} | ||
else { | ||
query[key] = value | ||
} | ||
} | ||
return query as T | ||
} |