-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
parseArgsParam.ts
74 lines (70 loc) · 3.15 KB
/
parseArgsParam.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import qs from 'qs';
import dedent from 'ts-dedent';
import type { Args } from '@storybook/addons';
import { once } from '@storybook/client-logger';
import isPlainObject from 'lodash/isPlainObject';
// Keep this in sync with validateArgs in router/src/utils.ts
const VALIDATION_REGEXP = /^[a-zA-Z0-9 _-]*$/;
const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/;
const HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i;
const COLOR_REGEXP =
/^(rgba?|hsla?)\(([0-9]{1,3}),\s?([0-9]{1,3})%?,\s?([0-9]{1,3})%?,?\s?([0-9](\.[0-9]{1,2})?)?\)$/i;
const validateArgs = (key = '', value: unknown): boolean => {
if (key === null) return false;
if (key === '' || !VALIDATION_REGEXP.test(key)) return false;
if (value === null || value === undefined) return true; // encoded as `!null` or `!undefined`
if (value instanceof Date) return true; // encoded as modified ISO string
if (typeof value === 'number' || typeof value === 'boolean') return true;
if (typeof value === 'string') {
return (
VALIDATION_REGEXP.test(value) ||
NUMBER_REGEXP.test(value) ||
HEX_REGEXP.test(value) ||
COLOR_REGEXP.test(value)
);
}
if (Array.isArray(value)) return value.every((v) => validateArgs(key, v));
if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v));
return false;
};
const QS_OPTIONS = {
delimiter: ';', // we're parsing a single query param
allowDots: true, // objects are encoded using dot notation
allowSparse: true, // arrays will be merged on top of their initial value
decoder(
str: string,
defaultDecoder: (str: string, decoder?: any, charset?: string) => string,
charset: string,
type: 'key' | 'value'
) {
if (type === 'value' && str.startsWith('!')) {
if (str === '!undefined') return undefined;
if (str === '!null') return null;
if (str.startsWith('!date(') && str.endsWith(')')) return new Date(str.slice(6, -1));
if (str.startsWith('!hex(') && str.endsWith(')')) return `#${str.slice(5, -1)}`;
const color = str.slice(1).match(COLOR_REGEXP);
if (color) {
if (str.startsWith('!rgba'))
return `${color[1]}(${color[2]}, ${color[3]}, ${color[4]}, ${color[5]})`;
if (str.startsWith('!hsla'))
return `${color[1]}(${color[2]}, ${color[3]}%, ${color[4]}%, ${color[5]})`;
return str.startsWith('!rgb')
? `${color[1]}(${color[2]}, ${color[3]}, ${color[4]})`
: `${color[1]}(${color[2]}, ${color[3]}%, ${color[4]}%)`;
}
}
if (type === 'value' && NUMBER_REGEXP.test(str)) return Number(str);
return defaultDecoder(str, defaultDecoder, charset);
},
};
export const parseArgsParam = (argsString: string): Args => {
const parts = argsString.split(';').map((part) => part.replace('=', '~').replace(':', '='));
return Object.entries(qs.parse(parts.join(';'), QS_OPTIONS)).reduce((acc, [key, value]) => {
if (validateArgs(key, value)) return Object.assign(acc, { [key]: value });
once.warn(dedent`
Omitted potentially unsafe URL args.
More info: https://storybook.js.org/docs/react/writing-stories/args#setting-args-through-the-url
`);
return acc;
}, {} as Args);
};