2018-07-17 15:21:03 +03:00
|
|
|
import slugify from 'slugify';
|
|
|
|
|
2017-10-12 00:01:37 +03:00
|
|
|
/**
|
2019-12-10 09:13:37 +03:00
|
|
|
* Maps over array passing `isLast` bool to iterator as the second argument
|
2017-10-12 00:01:37 +03:00
|
|
|
*/
|
|
|
|
export function mapWithLast<T, P>(array: T[], iteratee: (item: T, isLast: boolean) => P) {
|
|
|
|
const res: P[] = [];
|
|
|
|
for (let i = 0; i < array.length - 1; i++) {
|
|
|
|
res.push(iteratee(array[i], false));
|
|
|
|
}
|
|
|
|
if (array.length !== 0) {
|
|
|
|
res.push(iteratee(array[array.length - 1], true));
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates an object with the same keys as object and values generated by running each
|
|
|
|
* own enumerable string keyed property of object thru iteratee.
|
|
|
|
* The iteratee is invoked with three arguments: (value, key, object).
|
|
|
|
*
|
|
|
|
* @param object the object to iterate over
|
|
|
|
* @param iteratee the function invoked per iteration.
|
|
|
|
*/
|
|
|
|
export function mapValues<T, P>(
|
2020-05-25 16:37:38 +03:00
|
|
|
object: Record<string, T>,
|
|
|
|
iteratee: (val: T, key: string, obj: Record<string, T>) => P,
|
|
|
|
): Record<string, P> {
|
2017-10-12 00:01:37 +03:00
|
|
|
const res: { [key: string]: P } = {};
|
2018-01-22 21:30:53 +03:00
|
|
|
for (const key in object) {
|
2017-10-12 00:01:37 +03:00
|
|
|
if (object.hasOwnProperty(key)) {
|
|
|
|
res[key] = iteratee(object[key], key, object);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* flattens collection using `prop` field as a children
|
2018-01-22 21:30:53 +03:00
|
|
|
* @param collectionItems collection items
|
2017-10-12 00:01:37 +03:00
|
|
|
* @param prop item property with child elements
|
|
|
|
*/
|
2018-01-22 21:30:53 +03:00
|
|
|
export function flattenByProp<T extends object, P extends keyof T>(
|
|
|
|
collectionItems: T[],
|
|
|
|
prop: P,
|
|
|
|
): T[] {
|
2017-10-12 00:01:37 +03:00
|
|
|
const res: T[] = [];
|
|
|
|
const iterate = (items: T[]) => {
|
2018-01-22 21:30:53 +03:00
|
|
|
for (const item of items) {
|
2017-10-12 00:01:37 +03:00
|
|
|
res.push(item);
|
|
|
|
if (item[prop]) {
|
2021-11-24 17:11:45 +03:00
|
|
|
iterate(item[prop] as any as T[]);
|
2017-10-12 00:01:37 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2018-01-22 21:30:53 +03:00
|
|
|
iterate(collectionItems);
|
2017-10-12 00:01:37 +03:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function stripTrailingSlash(path: string): string {
|
|
|
|
if (path.endsWith('/')) {
|
|
|
|
return path.substring(0, path.length - 1);
|
|
|
|
}
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2018-01-22 21:30:53 +03:00
|
|
|
export function isNumeric(n: any): n is number {
|
2017-11-20 19:02:49 +03:00
|
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
|
|
}
|
2017-11-21 17:33:22 +03:00
|
|
|
|
|
|
|
export function appendToMdHeading(md: string, heading: string, content: string) {
|
|
|
|
// if heading is already in md and append to the end of it
|
|
|
|
const testRegex = new RegExp(`(^|\\n)#\\s?${heading}\\s*\\n`, 'i');
|
|
|
|
const replaceRegex = new RegExp(`((\\n|^)#\\s*${heading}\\s*(\\n|$)(?:.|\\n)*?)(\\n#|$)`, 'i');
|
|
|
|
if (testRegex.test(md)) {
|
|
|
|
return md.replace(replaceRegex, `$1\n\n${content}\n$4`);
|
|
|
|
} else {
|
|
|
|
// else append heading itself
|
|
|
|
const br = md === '' || md.endsWith('\n\n') ? '' : md.endsWith('\n') ? '\n' : '\n\n';
|
|
|
|
return `${md}${br}# ${heading}\n\n${content}`;
|
|
|
|
}
|
|
|
|
}
|
2017-11-22 15:00:42 +03:00
|
|
|
|
|
|
|
// credits https://stackoverflow.com/a/46973278/1749888
|
2019-09-30 10:34:49 +03:00
|
|
|
export const mergeObjects = (target: any, ...sources: any[]): any => {
|
2017-11-22 15:00:42 +03:00
|
|
|
if (!sources.length) {
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
const source = sources.shift();
|
|
|
|
if (source === undefined) {
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isMergebleObject(target) && isMergebleObject(source)) {
|
2018-01-22 21:30:53 +03:00
|
|
|
Object.keys(source).forEach((key: string) => {
|
2017-11-22 15:00:42 +03:00
|
|
|
if (isMergebleObject(source[key])) {
|
|
|
|
if (!target[key]) {
|
|
|
|
target[key] = {};
|
|
|
|
}
|
|
|
|
mergeObjects(target[key], source[key]);
|
|
|
|
} else {
|
|
|
|
target[key] = source[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return mergeObjects(target, ...sources);
|
|
|
|
};
|
|
|
|
|
2022-05-23 11:32:15 +03:00
|
|
|
export const isObject = (item: unknown): item is Record<string, unknown> => {
|
2017-11-22 15:00:42 +03:00
|
|
|
return item !== null && typeof item === 'object';
|
|
|
|
};
|
|
|
|
|
|
|
|
const isMergebleObject = (item): boolean => {
|
2022-05-06 19:01:47 +03:00
|
|
|
return isObject(item) && !isArray(item);
|
2017-11-22 15:00:42 +03:00
|
|
|
};
|
2018-07-17 15:21:03 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* slugify() returns empty string when failed to slugify.
|
2019-12-10 09:13:37 +03:00
|
|
|
* so try to return minimum slugified-string with failed one which keeps original value
|
2018-07-17 15:21:03 +03:00
|
|
|
* the regex codes are referenced with https://gist.github.com/mathewbyrne/1280286
|
|
|
|
*/
|
|
|
|
export function safeSlugify(value: string): string {
|
2018-07-23 11:17:31 +03:00
|
|
|
return (
|
|
|
|
slugify(value) ||
|
|
|
|
value
|
|
|
|
.toString()
|
|
|
|
.toLowerCase()
|
|
|
|
.replace(/\s+/g, '-') // Replace spaces with -
|
|
|
|
.replace(/&/g, '-and-') // Replace & with 'and'
|
|
|
|
.replace(/\--+/g, '-') // Replace multiple - with single -
|
|
|
|
.replace(/^-+/, '') // Trim - from start of text
|
|
|
|
.replace(/-+$/, '')
|
|
|
|
); // Trim - from end of text
|
2018-07-17 15:21:03 +03:00
|
|
|
}
|
2018-07-23 12:00:29 +03:00
|
|
|
|
|
|
|
export function isAbsoluteUrl(url: string) {
|
|
|
|
return /(?:^[a-z][a-z0-9+.-]*:|\/\/)/i.test(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* simple resolve URL which doesn't break on strings with url fragments
|
|
|
|
* e.g. resolveUrl('http://test.com:{port}', 'path') results in http://test.com:{port}/path
|
|
|
|
*/
|
|
|
|
export function resolveUrl(url: string, to: string) {
|
|
|
|
let res;
|
|
|
|
if (to.startsWith('//')) {
|
2022-05-10 14:50:50 +03:00
|
|
|
try {
|
|
|
|
res = `${new URL(url).protocol || 'https:'}${to}`;
|
|
|
|
} catch {
|
|
|
|
res = `https:${to}`;
|
|
|
|
}
|
2018-07-23 12:00:29 +03:00
|
|
|
} else if (isAbsoluteUrl(to)) {
|
|
|
|
res = to;
|
|
|
|
} else if (!to.startsWith('/')) {
|
|
|
|
res = stripTrailingSlash(url) + '/' + to;
|
|
|
|
} else {
|
2022-05-10 14:50:50 +03:00
|
|
|
try {
|
|
|
|
const urlObj = new URL(url);
|
|
|
|
urlObj.pathname = to;
|
|
|
|
res = urlObj.href;
|
|
|
|
} catch {
|
|
|
|
res = to;
|
|
|
|
}
|
2018-07-23 12:00:29 +03:00
|
|
|
}
|
|
|
|
return stripTrailingSlash(res);
|
|
|
|
}
|
2018-11-05 16:53:42 +03:00
|
|
|
|
|
|
|
export function getBasePath(serverUrl: string): string {
|
2019-05-13 15:49:54 +03:00
|
|
|
try {
|
|
|
|
return parseURL(serverUrl).pathname;
|
|
|
|
} catch (e) {
|
|
|
|
// when using with redoc-cli serverUrl can be empty resulting in crash
|
|
|
|
return serverUrl;
|
|
|
|
}
|
2018-11-05 16:53:42 +03:00
|
|
|
}
|
2019-05-12 22:10:49 +03:00
|
|
|
|
2019-05-12 22:34:10 +03:00
|
|
|
export function titleize(text: string) {
|
|
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
|
|
}
|
|
|
|
|
2022-07-22 18:33:58 +03:00
|
|
|
export function removeQueryStringAndHash(serverUrl: string): string {
|
2019-05-13 15:49:54 +03:00
|
|
|
try {
|
|
|
|
const url = parseURL(serverUrl);
|
|
|
|
url.search = '';
|
2022-07-22 18:33:58 +03:00
|
|
|
url.hash = '';
|
2019-05-13 15:49:54 +03:00
|
|
|
return url.toString();
|
|
|
|
} catch (e) {
|
|
|
|
// when using with redoc-cli serverUrl can be empty resulting in crash
|
|
|
|
return serverUrl;
|
|
|
|
}
|
2019-05-12 22:10:49 +03:00
|
|
|
}
|
2019-05-13 12:07:31 +03:00
|
|
|
|
|
|
|
function parseURL(url: string) {
|
2019-05-13 13:08:26 +03:00
|
|
|
if (typeof URL === 'undefined') {
|
2019-05-13 12:07:31 +03:00
|
|
|
// node
|
2020-03-17 13:01:32 +03:00
|
|
|
return new (require('url').URL)(url);
|
2019-05-13 12:07:31 +03:00
|
|
|
} else {
|
|
|
|
return new URL(url);
|
|
|
|
}
|
|
|
|
}
|
2019-07-07 21:26:27 +03:00
|
|
|
|
2022-04-15 16:53:36 +03:00
|
|
|
export function escapeHTMLAttrChars(str: string): string {
|
|
|
|
return str.replace(/["\\]/g, '\\$&');
|
|
|
|
}
|
|
|
|
|
2019-07-07 21:26:27 +03:00
|
|
|
export function unescapeHTMLChars(str: string): string {
|
2020-05-10 21:56:05 +03:00
|
|
|
return str
|
|
|
|
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)))
|
2022-04-15 16:53:36 +03:00
|
|
|
.replace(/&/g, '&')
|
|
|
|
.replace(/"/g, '"');
|
2019-07-07 21:26:27 +03:00
|
|
|
}
|
2022-05-06 19:01:47 +03:00
|
|
|
|
2022-05-23 11:32:15 +03:00
|
|
|
export function isArray(value: unknown): value is any[] {
|
2022-05-06 19:01:47 +03:00
|
|
|
return Array.isArray(value);
|
|
|
|
}
|
2022-05-23 11:32:15 +03:00
|
|
|
|
|
|
|
export function isBoolean(value: unknown): value is boolean {
|
|
|
|
return typeof value === 'boolean';
|
|
|
|
}
|