flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/web/app/lib/search.ts

119 lines4,221 bytesCodeBlame
1/**
2 * Building search queries and addresses on the site: the filters on the
3 * search page change the query people can see and edit, never something
4 * hidden, so a search is always one shareable address.
5 */
6import type { SearchType } from "@g1t/contracts";
7
8/** The qualifiers a search understands, as the help lists them. */
9export const QUALIFIERS: { example: string; means: string }[] = [
10 { example: "repo:acme/web", means: "Only in one repository" },
11 { example: "org:acme", means: "Only in one workspace (also workspace:acme)" },
12 { example: "language:rust", means: "Code in a language, or repositories mostly in it" },
13 { example: "path:src/", means: "Code under a path; path:*.rs for a pattern" },
14 { example: "is:issue is:pr", means: "Only issues, or only pull requests" },
15 { example: "is:open is:closed is:merged", means: "Issues and pull requests by state" },
16 { example: "author:ana", means: "Opened by someone" },
17 { example: "label:bug", means: "Issues and pull requests with a label" },
18 { example: "\"exact phrase\"", means: "These words together, in this order" },
19 { example: "-word -label:wontfix", means: "Leave out what has it" },
20];
21
22/** Languages offered by the language filter; any other can be typed. */
23export const LANGUAGES = [
24 "rust",
25 "typescript",
26 "tsx",
27 "javascript",
28 "python",
29 "go",
30 "ruby",
31 "java",
32 "kotlin",
33 "swift",
34 "c",
35 "c++",
36 "c#",
37 "php",
38 "elixir",
39 "shell",
40 "sql",
41 "html",
42 "css",
43 "markdown",
44 "yaml",
45 "toml",
46 "json",
47];
48
49/** What each tab is called. */
50export const TYPE_LABELS: Record<SearchType, string> = {
51 repositories: "Repositories",
52 code: "Code",
53 issues: "Issues",
54 pulls: "Pull requests",
55 people: "People",
56};
57
58/** Splits a query into its tokens, keeping quoted runs whole. */
59export function tokens(query: string): string[] {
60 return query.match(/(?:[^\s"]+|"[^"]*"?)+/g) ?? [];
61}
62
63/** The value of a qualifier in a query, the first if it is given more than once. */
64export function qualifier(query: string, key: string): string | null {
65 for (const token of tokens(query)) {
66 const [name, ...rest] = token.split(":");
67 if (name?.toLowerCase() === key && rest.length) return rest.join(":").replace(/^"|"$/g, "") || null;
68 }
69 return null;
70}
71
72/**
73 * The query with `key:value` in place of any `key:` it had; with `value`
74 * null, without it. `aliases` are other names for the same qualifier
75 * (`org` and `workspace`), removed as well. Kept in the order written.
76 */
77export function withQualifier(query: string, key: string, value: string | null, aliases: string[] = []): string {
78 const names = new Set([key, ...aliases]);
79 const kept = tokens(query).filter((token) => {
80 const name = token.split(":")[0]?.toLowerCase() ?? "";
81 return !(token.includes(":") && names.has(name));
82 });
83 if (value) kept.push(`${key}:${/\s/.test(value) ? `"${value}"` : value}`);
84 return kept.join(" ");
85}
86
87/** `is:` values of one family set to `value`, the family's others removed. */
88export function withIs(query: string, family: string[], value: string | null): string {
89 const kept = tokens(query).filter((token) => {
90 const [name, rest] = token.split(":");
91 return !(name?.toLowerCase() === "is" && family.includes((rest ?? "").toLowerCase()));
92 });
93 if (value) kept.push(`is:${value}`);
94 return kept.join(" ");
95}
96
97/** The `is:` value of a family the query has, if any. */
98export function isValue(query: string, family: string[]): string | null {
99 for (const token of tokens(query)) {
100 const [name, rest] = token.split(":");
101 if (name?.toLowerCase() === "is" && family.includes((rest ?? "").toLowerCase())) return rest!.toLowerCase();
102 }
103 return null;
104}
105
106/** The address of a search. */
107export function searchHref(query: string, type?: SearchType | null, page?: number): string {
108 const params = new URLSearchParams();
109 params.set("q", query.trim());
110 if (type) params.set("type", type);
111 if (page && page > 1) params.set("page", String(page));
112 return `/search?${params.toString()}`;
113}
114
115/** A count as a tab shows it: up to the cap, then "1k+". */
116export function shortCount(count: number, cap = 1000): string {
117 if (count > cap - 1) return "1k+";
118 return String(count);
119}