summaryrefslogtreecommitdiff
path: root/source/libs/utils.ts
blob: 85886c97deb744b7d59fe369fbefb92fc3b1dd81 (plain) (blame)
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import select from 'select-dom';
import onetime from 'onetime';
import {isRepo, isPR, isIssue} from './page-detect';

export const getUsername = onetime(() => select('meta[name="user-login"]')!.getAttribute('content')!);

export const getDiscussionNumber = (): string | undefined => {
	if (isPR() || isIssue()) {
		return getCleanPathname().split('/')[3];
	}

	return undefined;
};

// Drops leading and trailing slash to avoid /\/?/ everywhere
export const getCleanPathname = (): string => location.pathname.replace(/^[/]|[/]$/g, '');

// Parses a repo's subpage, e.g.
// '/user/repo/issues/' -> 'issues'
// '/user/repo/' -> ''
// returns undefined if the path is not a repo
export const getRepoPath = (): string | undefined => {
	if (isRepo()) {
		return getCleanPathname().split('/').slice(2).join('/');
	}

	return undefined;
};

export const replaceBranch = (currentBranch: string, newBranch: string): string => {
	// `pageType` will be either `blob' or 'tree'
	const [pageType, ...branchAndPathParts] = getRepoPath()!.split('/');

	const newBranchRepoPath = branchAndPathParts.join('/').replace(currentBranch, newBranch);

	return `/${getRepoURL()}/${pageType}/${newBranchRepoPath}`;
};

export const getCurrentBranch = (): string => {
	return select<HTMLLinkElement>('link[rel="alternate"]')!
		.href
		.split('/')
		.slice(6)
		.join('/')
		.replace(/\.atom.*/, '');
};

export const isFirefox = navigator.userAgent.includes('Firefox/');

export const getRepoURL = (): string => location.pathname.slice(1).split('/', 2).join('/');
export const getRepoGQL = (): string => {
	const {ownerName, repoName} = getOwnerAndRepo();
	return `owner: "${ownerName!}", name: "${repoName!}"`;
};

export const getOwnerAndRepo = (): {
	ownerName: string | undefined;
	repoName: string | undefined;
} => {
	const [, ownerName, repoName] = location.pathname.split('/', 3);
	return {ownerName, repoName};
};

export const getReference = (): string | undefined => {
	const pathnameParts = location.pathname.split('/');
	if (['commits', 'blob', 'tree', 'blame'].includes(pathnameParts[3])) {
		return pathnameParts[4];
	}

	return undefined;
};

export const parseTag = (tag: string): {version: string; namespace: string} => {
	const [, namespace = '', version = ''] = /(?:(.*)@)?([^@]+)/.exec(tag) ?? [];
	return {namespace, version};
};

export const groupBy = (iterable: Iterable<string>, grouper: (item: string) => string): Record<string, string[]> => {
	const map: Record<string, string[]> = {};

	for (const item of iterable) {
		const key = grouper(item);
		map[key] = map[key] ?? [];
		map[key].push(item);
	}

	return map;
};

// Concats arrays but does so like a zipper instead of appending them
// [[0, 1, 2], [0, 1]] => [0, 0, 1, 1, 2]
// Like lodash.zip
export const flatZip = <T>(table: T[][], limit = Infinity): T[] => {
	const maxColumns = Math.max(...table.map(row => row.length));
	const zipped = [];
	for (let col = 0; col < maxColumns; col++) {
		for (const row of table) {
			if (row[col]) {
				zipped.push(row[col]);
				if (zipped.length === limit) {
					return zipped;
				}
			}
		}
	}

	return zipped;
};

export function getOP(): string {
	if (isPR()) {
		return /^(?:.+) by (\S+) · Pull Request #(?:\d+)/.exec(document.title)?.[1]!;
	}

	return select('.timeline-comment-header-text .author')!.textContent!;
}

export function compareNames(username: string, realname: string): boolean {
	return username.replace(/-/g, '').toLowerCase() === realname.normalize('NFD').replace(/[\u0300-\u036F\W.]/g, '').toLowerCase();
}

export async function poll<T>(callback: () => T, frequency: number): Promise<T> {
	return new Promise(resolve => {
		(function loop() {
			const result = callback();
			if (result !== null && typeof result !== undefined) {
				resolve(result);
			} else {
				setTimeout(loop, frequency);
			}
		})();
	});
}

export function reportBug(featureName: string, bugName: string): void {
	alert(`Refined GitHub: ${bugName}. Can you report this issue? You’ll find more information in the console.`);
	const issuesUrl = new URL('https://github.com/sindresorhus/refined-github/issues');
	const newIssueUrl = new URL('https://github.com/sindresorhus/refined-github/new?labels=bug&template=bug_report.md');
	issuesUrl.searchParams.set('q', `is:issue ${featureName}`);
	newIssueUrl.searchParams.set('title', `\`${featureName}\` ${bugName}`);
	console.log('Find existing issues:\n' + String(issuesUrl));
	console.log('Open new issue:\n' + String(newIssueUrl));
}