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
|
import {getUsername} from '.';
type Source = HTMLAnchorElement | URL | string | string[][] | Record<string, string> | URLSearchParams;
const queryPartsRegExp = /(?:[^\s"]+|"[^"]*")+/g;
function splitQueryString(query: string): string[] {
return query.match(queryPartsRegExp) ?? [];
}
// Remove all keywords from array except the last occurrence of one of the keywords.
function deduplicateKeywords(array: string[], ...keywords: string[]): string[] {
const deduplicated = [];
let wasKeywordFound = false;
for (const current of array.reverse()) {
const isKeyword = keywords.includes(current);
if (!isKeyword || !wasKeywordFound) {
deduplicated.unshift(current);
wasKeywordFound = wasKeywordFound || isKeyword;
}
}
return deduplicated;
}
function cleanQueryParts(parts: string[]): string[] {
return deduplicateKeywords(parts, 'is:issue', 'is:pr');
}
/**
Parser/Mutator of GitHub's search query directly on anchors and URL-like objects.
Notice: if the <a> or `location` changes outside SearchQuery, `get()` will return an outdated value.
*/
export default class SearchQuery {
link?: HTMLAnchorElement;
searchParams: URLSearchParams;
constructor(link: Source) {
if (link instanceof HTMLAnchorElement) {
this.link = link;
this.searchParams = new URLSearchParams(link.search);
// Keep `.search` property up to date with this `searchParams`
const nativeSet = this.searchParams.set;
this.searchParams.set = (name, value) => {
nativeSet.call(this.searchParams, name, value);
link.search = String(this.searchParams);
};
} else if (link instanceof URL) {
this.searchParams = link.searchParams;
} else {
this.searchParams = new URLSearchParams(link);
}
// Ensure the query string is set and cleaned up
this.set(this.get());
}
static escapeValue(value: string): string {
return value.includes(' ') ? `"${value}"` : value;
}
get(): string {
const currentQuery = this.searchParams.get('q');
if (typeof currentQuery === 'string') {
return currentQuery;
}
if (!this.link) {
return '';
}
// Query-less URLs imply some queries.
// When we explicitly set ?q=* they're overridden, so they need to be manually added again.
const queries = [];
// Repo example: is:issue is:open
queries.push(/\/pulls\/?$/.test(this.link.pathname) ? 'is:pr' : 'is:issue', 'is:open');
// Header nav example: is:open is:issue author:you archived:false
if (this.link.pathname === '/issues' || this.link.pathname === '/pulls') {
if (this.searchParams.has('user')) { // #1211
queries.push(`user:${this.searchParams.get('user')!}`);
} else {
queries.push(`author:${getUsername()!}`);
}
queries.push('archived:false');
}
return queries.join(' ');
}
getQueryParts(): string[] {
return splitQueryString(this.get());
}
set(query: string): void {
const parts = splitQueryString(query);
const cleaned = cleanQueryParts(parts);
this.searchParams.set('q', cleaned.join(' '));
}
edit(callback: (query: string) => string): void {
this.set(callback(this.get()));
}
replace(searchValue: string | RegExp, replaceValue: string): void {
this.set(this.get().replace(searchValue, replaceValue));
}
remove(...queryPartToRemove: string[]): void {
const newQuery = this
.getQueryParts()
.filter(queryPart => !queryPartToRemove.includes(queryPart))
.join(' ');
this.set(newQuery);
}
add(...queryParts: string[]): void {
const newQuery = this.getQueryParts();
newQuery.push(...queryParts);
this.set(newQuery.join(' '));
}
includes(...searchStrings: string[]): boolean {
return this.getQueryParts().some(queryPart => searchStrings.includes(queryPart));
}
}
|