From f157f914a0b53ab41e22784b19325b8d641f034f Mon Sep 17 00:00:00 2001 From: Utopia Date: Sun, 5 Mar 2023 11:45:49 +0800 Subject: [PATCH] feat: new function: parseQuery --- README.md | 1 + packages/core/src/index.ts | 1 + packages/core/src/parseQuery.test.ts | 86 ++++++++++++++++++++++++++++ packages/core/src/parseQuery.ts | 77 +++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 packages/core/src/parseQuery.test.ts create mode 100644 packages/core/src/parseQuery.ts diff --git a/README.md b/README.md index 2a46fe9..6fa8c9f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d2330f9..5723235 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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' diff --git a/packages/core/src/parseQuery.test.ts b/packages/core/src/parseQuery.test.ts new file mode 100644 index 0000000..5502d2b --- /dev/null +++ b/packages/core/src/parseQuery.test.ts @@ -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) + }) +}) diff --git a/packages/core/src/parseQuery.ts b/packages/core/src/parseQuery.ts new file mode 100644 index 0000000..23a96eb --- /dev/null +++ b/packages/core/src/parseQuery.ts @@ -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(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 +}