-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
index.ts
164 lines (131 loc) · 5.62 KB
/
index.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
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import {$, elementExists} from 'select-dom';
import onetime from 'onetime';
import elementReady from 'element-ready';
import compareVersions from 'tiny-version-compare';
import {RequireAtLeastOne} from 'type-fest';
import * as pageDetect from 'github-url-detection';
import mem from 'memoize';
import {branchSelector} from './selectors.js';
// This never changes, so it can be cached here
export const getUsername = onetime(pageDetect.utils.getUsername);
export const {getRepositoryInfo: getRepo, getCleanPathname} = pageDetect.utils;
export function getConversationNumber(): number | undefined {
if (pageDetect.isPR() || pageDetect.isIssue()) {
return Number(location.pathname.split('/')[4]);
}
return undefined;
}
export const isMac = navigator.userAgent.includes('Macintosh');
type Not<Yes, Not> = Yes extends Not ? never : Yes;
type UnslashedString<S extends string> = Not<S, `/${string}` | `${string}/`>;
export function buildRepoURL<S extends string>(...pathParts: RequireAtLeastOne<Array<UnslashedString<S> | number>, 0>): string {
for (const part of pathParts) {
if (typeof part === 'string' && /^\/|\/$/.test(part)) {
throw new TypeError('The path parts shouldn’t start or end with a slash: ' + part);
}
}
return [location.origin, getRepo()?.nameWithOwner, ...pathParts].join('/');
}
export function getForkedRepo(): string | undefined {
return $('meta[name="octolytics-dimension-repository_parent_nwo"]')?.content;
}
export function parseTag(tag: string): {version: string; namespace: string} {
const [, namespace = '', version = ''] = /(?:(.*)@)?([^@]+)/.exec(tag) ?? [];
return {namespace, version};
}
export function isUsernameAlreadyFullName(username: string, realname: string): boolean {
// Normalize both strings
username = username
.replaceAll('-', '')
.toLowerCase();
realname = realname
.normalize('NFD')
.replaceAll(/[\u0300-\u036F\W.]/g, '')
.toLowerCase();
return username === realname;
}
const validVersion = /^[vr]?\d+(?:\.\d+)+/;
const isPrerelease = /^[vr]?\d+(?:\.\d+)+(-\d)/;
export function getLatestVersionTag(tags: string[]): string {
// Some tags aren't valid versions; comparison is meaningless.
// Just use the latest tag returned by the API (reverse chronologically-sorted list)
if (!tags.every(tag => validVersion.test(tag))) {
return tags[0];
}
// Exclude pre-releases
let releases = tags.filter(tag => !isPrerelease.test(tag));
if (releases.length === 0) { // They were all pre-releases; undo.
releases = tags;
}
let latestVersion = releases[0];
for (const release of releases) {
if (compareVersions(latestVersion, release) < 0) {
latestVersion = release;
}
}
return latestVersion;
}
// https://github.com/idimetrix/text-case/blob/master/packages/upper-case-first/src/index.ts
export function upperCaseFirst(input: string): string {
return input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
}
const cachePerPage = {
cacheKey: () => location.pathname,
};
/** Is tag or commit, with elementReady */
export const isPermalink = mem(async () => {
// No need for getCurrentGitRef(), it's a simple and exact check
if (/^[\da-f]{40}$/.test(location.pathname.split('/')[4])) {
// It's a commit
return true;
}
// Awaiting only the branch selector means it resolves early even if the icon tag doesn't exist, whereas awaiting the icon tag would wait for the DOM ready event before resolving.
return elementExists(
'.octicon-tag', // Tags have an icon
await elementReady(branchSelector),
);
}, cachePerPage);
export function isRefinedGitHubRepo(): boolean {
return location.pathname.startsWith('/refined-github/refined-github');
}
export function isAnyRefinedGitHubRepo(): boolean {
return /^\/refined-github\/.+/.test(location.pathname);
}
export function isRefinedGitHubYoloRepo(): boolean {
return location.pathname.startsWith('/refined-github/yolo');
}
export async function isArchivedRepoAsync(): Promise<boolean> {
// Load the bare minimum for `isArchivedRepo` to work
await elementReady('main > div');
// DOM-based detection, we want awaitDomReady: false, so it needs to be here
return pageDetect.isArchivedRepo();
}
export const userCanLikelyMergePR = (): boolean => elementExists('.discussion-sidebar-item .octicon-lock');
export const cacheByRepo = (): string => getRepo()!.nameWithOwner;
// Commit lists for files and folders lack a branch selector
export const isRepoCommitListRoot = (): boolean => pageDetect.isRepoCommitList() && document.title.startsWith('Commits');
export const isUrlReachable = mem(async (url: string): Promise<boolean> => {
const {ok} = await fetch(url, {method: 'head'});
return ok;
});
// Don't make the argument optional, sometimes we really expect it to exist and want to throw an error
export function extractCurrentBranchFromBranchPicker(branchPicker: HTMLElement): string {
return branchPicker.title === 'Switch branches or tags'
? branchPicker.textContent.trim() // Branch name is shown in full
: branchPicker.title; // Branch name was clipped, so they placed it in the title attribute
}
export function addAfterBranchSelector(branchSelectorParent: HTMLDetailsElement, sibling: HTMLElement): void {
const row = branchSelectorParent.closest('.position-relative')!;
row.classList.add('d-flex', 'flex-shrink-0', 'gap-2');
row.append(sibling);
}
/** Trigger a reflow to push the right-most tab into the overflow dropdown */
export function triggerRepoNavOverflow(): void {
window.dispatchEvent(new Event('resize'));
}
export function triggerActionBarOverflow(child: Element): void {
const parent = child.closest('action-bar')!;
const placeholder = document.createElement('div');
parent.replaceWith(placeholder);
placeholder.replaceWith(parent);
}