storybook/lib/router/src/utils.ts

98 lines
2.8 KiB
TypeScript

import qs from 'qs';
import memoize from 'memoizerific';
import startCase from 'lodash/startCase';
interface StoryData {
viewMode?: string;
storyId?: string;
}
interface SeparatorOptions {
rootSeparator: string | RegExp;
groupSeparator: string | RegExp;
}
const splitPathRegex = /\/([^/]+)\/([^/]+)?/;
// Remove punctuation https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
export const sanitize = (string: string) => {
return (
string
.toLowerCase()
// eslint-disable-next-line no-useless-escape
.replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-')
.replace(/-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
);
};
const sanitizeSafe = (string: string, part: string) => {
const sanitized = sanitize(string);
if (sanitized === '') {
throw new Error(`Invalid ${part} '${string}', must include alphanumeric characters`);
}
return sanitized;
};
export const toId = (kind: string, name: string) =>
`${sanitizeSafe(kind, 'kind')}--${sanitizeSafe(name, 'name')}`;
export const parsePath: (path?: string) => StoryData = memoize(1000)(
(path: string | undefined | null) => {
const result: StoryData = {
viewMode: undefined,
storyId: undefined,
};
if (path) {
const [, viewMode, storyId] = path.match(splitPathRegex) || [undefined, undefined, undefined];
if (viewMode) {
Object.assign(result, {
viewMode,
storyId,
});
}
}
return result;
}
);
interface Query {
[key: string]: any;
}
export const queryFromString = memoize(1000)(
(s: string): Query => qs.parse(s, { ignoreQueryPrefix: true })
);
export const queryFromLocation = (location: { search: string }) => queryFromString(location.search);
export const stringifyQuery = (query: Query) =>
qs.stringify(query, { addQueryPrefix: true, encode: false });
export const getMatch = memoize(1000)(
(current: string, target: string, startsWith: boolean = true) => {
const startsWithTarget = current && startsWith && current.startsWith(target);
const currentIsTarget = typeof target === 'string' && current === target;
const matchTarget = current && target && current.match(target);
if (startsWithTarget || currentIsTarget || matchTarget) {
return { path: current };
}
return null;
}
);
export const parseKind = (kind: string, { rootSeparator, groupSeparator }: SeparatorOptions) => {
const [root, remainder] = kind.split(rootSeparator, 2);
const groups = (remainder || kind).split(groupSeparator).filter(i => !!i);
// when there's no remainder, it means the root wasn't found/split
return {
root: remainder ? root : null,
groups,
};
};
// Transform the CSF named export into a readable story name
export const storyNameFromExport = (key: string) => startCase(key);