Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ build/Release
node_modules/
jspm_packages/

# pnpm
pnpm-lock.yaml

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"test": "",
"lint": "npx biome lint ./src",
"lint:fix": "npx biome lint ./src --fix",
"tsc-noemit": "npx tsc -noEmit",
"build-firefox": "node ./bob.mjs -d -b firefox",
"build-firefox-release": "node ./bob.mjs -z -s -b firefox",
Expand Down
116 changes: 93 additions & 23 deletions src/background/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,30 @@ function constructRedirects(
return redirs;
}

function splitMultiSearchTerms(
text: string,
allowBracketGroups: boolean,
): string[] {
if (!allowBracketGroups) {
return text.split(/\s+/).filter((term) => term.length > 0);
}
const terms: string[] = [];
const regex = /\[([^\]]+)\]|(\S+)/g;
let match: RegExpExecArray | null = regex.exec(text);
while (match !== null) {
if (match[1] !== undefined) {
const term = match[1].trim();
if (term.length > 0) {
terms.push(term);
}
} else if (match[2] !== undefined) {
terms.push(match[2]);
}
match = regex.exec(text);
}
return terms;
}

/**
* Given a URL, construct the associated redirects, if a bang exists in the query.
* @param request The request details from a WebRequest event.
Expand Down Expand Up @@ -105,30 +129,66 @@ export async function getRedirects(

// Cut the first bang we can find from the query text, it can be anywhere in
// the string
const { trigger } = opts;
const { trigger, multiTrigger } = opts;
const enableMultiTrigger = opts.enableMultiTrigger ?? true;
const allowBracketGroups = opts.enableMultiTriggerBrackets ?? true;

const triggersToCheck = [] as Array<{
triggerStr: string;
isMulti: boolean;
}>;

if (
enableMultiTrigger &&
typeof multiTrigger === "string" &&
multiTrigger.trim() !== ""
) {
triggersToCheck.push({ triggerStr: multiTrigger, isMulti: true });
}

// Escape regex special characters in the string (e.g. ")" or "."). This
// prevents these characters from being interpreted as regex syntax. Note:
// RegExp.escape() would be cleaner but isn't supported in all browsers yet
const escapedTrigger = trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (trigger.trim() !== "") {
triggersToCheck.push({ triggerStr: trigger, isMulti: false });
}

let keywordUsed = "";
let isMulti = false;

for (const candidate of triggersToCheck) {
// Escape regex special characters in the string (e.g. ")" or "."). This
// prevents these characters from being interpreted as regex syntax. Note:
// RegExp.escape() would be cleaner but isn't supported in all browsers yet
const escapedCandidate = candidate.triggerStr.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
);

// Build a regex pattern matching three cases:
// 1. Trigger at the start of a string followed by whitespace
// 2. Trigger after whitespace in the middle of a string
// 3. Trigger at the end of a string
// Build a regex pattern matching three cases:
// 1. Trigger at the start of a string followed by whitespace
// 2. Trigger after whitespace in the middle of a string
// 3. Trigger at the end of a string

// To include variables we use a template string, annoyingly because regex
// uses lots of backslashes we have to escape them all 🤮
const matchTrigger = new RegExp(
`(^${escapedTrigger}\\S+\\s|\\s${escapedTrigger}\\S+|^${escapedTrigger}\\S+$)`,
);
// To include variables we use a template string, annoyingly because regex
// uses lots of backslashes we have to escape them all 🤮
const matchTrigger = new RegExp(
`(^${escapedCandidate}\\S+\\s|\\s${escapedCandidate}\\S+|^${escapedCandidate}\\S+$)`,
);

let keywordUsed = "";
queryText = queryText.replace(matchTrigger, (match) => {
keywordUsed = match.trim().replace(trigger, "");
// Replace bang with zero len str
return "";
});
let localKeyword = "";
const nextQueryText = queryText.replace(matchTrigger, (match) => {
localKeyword = match
.trim()
.slice(candidate.triggerStr.length)
.split(/\s+/)[0];
return "";
});

if (localKeyword.length > 0) {
keywordUsed = localKeyword;
queryText = nextQueryText.trim();
isMulti = candidate.isMulti;
break;
}
}

if (keywordUsed.length === 0) {
return [];
Expand Down Expand Up @@ -156,9 +216,19 @@ export async function getRedirects(
}

// Construct the URL(s) to redirect the user to.
const redirects: Array<string> = [];
for (const bangInfo of redirectionBangInfos) {
redirects.push(...constructRedirects(bangInfo, queryText));
const redirects = [] as Array<string>;

if (isMulti) {
const terms = splitMultiSearchTerms(queryText, allowBracketGroups);
for (const term of terms) {
for (const bangInfo of redirectionBangInfos) {
redirects.push(...constructRedirects(bangInfo, term));
}
}
} else {
for (const bangInfo of redirectionBangInfos) {
redirects.push(...constructRedirects(bangInfo, queryText));
}
}

return redirects;
Expand Down
Loading