A fast, local-first "redirection engine" for !bang users with a few extra features ^-^
1import { bangs } from "./bang";
2import "./global.css";
3
4function noSearchDefaultPageRender() {
5 const app = document.querySelector<HTMLDivElement>("#app");
6 if (!app) throw new Error("App element not found");
7 app.innerHTML = `
8 <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh;">
9 <div class="content-container">
10 <h1>┐( ˘_˘ )┌</h1>
11 <p>DuckDuckGo's bang redirects are too slow. Add the following URL as a custom search engine to your browser. Enables <a href="https://duckduckgo.com/bang.html" target="_blank">all of DuckDuckGo's bangs.</a></p>
12 <div class="url-container">
13 <input
14 type="text"
15 class="url-input"
16 value="https://unduck.link?q=%s"
17 readonly
18 />
19 <button class="copy-button">
20 <img src="/clipboard.svg" alt="Copy" />
21 </button>
22 </div>
23 </div>
24 <footer class="footer">
25 made with ♥ by <a href="https://github.com/taciturnaxolotl" target="_blank">Kieran Klukas</a> as <a href="https://github.com/taciturnaxolotl/unduck" target="_blank">open source</a> software
26 </footer>
27 </div>
28 `;
29
30 const copyButton = app.querySelector<HTMLButtonElement>(".copy-button");
31 if (!copyButton) throw new Error("Copy button not found");
32 const copyIcon = copyButton.querySelector("img");
33 if (!copyIcon) throw new Error("Copy icon not found");
34 const urlInput = app.querySelector<HTMLInputElement>(".url-input");
35 if (!urlInput) throw new Error("URL input not found");
36
37 urlInput.value = `${window.location.protocol}//${window.location.host}?q=%s`;
38
39 copyButton.addEventListener("click", async () => {
40 await navigator.clipboard.writeText(urlInput.value);
41 copyIcon.src = "/clipboard-check.svg";
42
43 setTimeout(() => {
44 copyIcon.src = "/clipboard.svg";
45 }, 2000);
46 });
47}
48
49const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? "g";
50const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG);
51
52function getBangredirectUrl() {
53 const url = new URL(window.location.href);
54 const query = url.searchParams.get("q")?.trim() ?? "";
55 if (!query) {
56 noSearchDefaultPageRender();
57 return null;
58 }
59
60 const match = query.match(/!(\S+)/i);
61
62 const bangCandidate = match?.[1]?.toLowerCase();
63 const selectedBang = bangs.find((b) => b.t === bangCandidate) ?? defaultBang;
64
65 // Remove the first bang from the query
66 const cleanQuery = query.replace(/!\S+\s*/i, "").trim();
67
68 // Format of the url is:
69 // https://www.google.com/search?q={{{s}}}
70 const searchUrl = selectedBang?.u.replace(
71 "{{{s}}}",
72 // Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
73 encodeURIComponent(cleanQuery).replace(/%2F/g, "/"),
74 );
75 if (!searchUrl) return null;
76
77 return searchUrl;
78}
79
80function doRedirect() {
81 const searchUrl = getBangredirectUrl();
82 if (!searchUrl) return;
83 window.location.replace(searchUrl);
84}
85
86doRedirect();