Skip to content

Commit

Permalink
feat: new function: parseQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
GreatAuk committed Mar 5, 2023
1 parent 9a0e9b7 commit f157f91
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pnpm add @utopia-utils/dom
* compose: 函数组合, 从右到左执行。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/compose.ts)
* pipe: 函数组合, 从左到右执行。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/pipe.ts)
* onlyResolvesLast: 解决竞态问题,只保留最后一次调用的结果。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/onlyResolvesLast.ts)
* parseQuery: 解析 url query。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/parseQuery.ts)
### 类型判断

```bash
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from './objectKeys'
export * from './omit'
export * from './once'
export * from './onlyResolvesLast'
export * from './parseQuery'
export * from './pick'
export * from './pipe'
export * from './randomInt'
Expand Down
86 changes: 86 additions & 0 deletions packages/core/src/parseQuery.test.ts
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)
})
})
77 changes: 77 additions & 0 deletions packages/core/src/parseQuery.ts
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
}

0 comments on commit f157f91

Please sign in to comment.