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
74 changes: 65 additions & 9 deletions src/app/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,22 @@ function checkInvalidQueryTerms(terms: string[]) {
}
}

function getSearchRegex(term: string) {
function isValidRegex(pattern: string): [boolean, RegExp | null] {
try {
const regex = new RegExp(pattern, 'i');
return [true, regex];
} catch {
return [false, null];
}
}

function getSearchRegex(term: string): RegExp | null {
const pattern = term
.replace(/([.*+?^=!:${}()|[]\/\\])/g, '\\$1')
.replace(/x/g, '[\\dx]');

return new RegExp(`${pattern}`, 'i');
const [isValid, regex] = isValidRegex(pattern);
return isValid ? regex : null;
}

function fullId(subject: string, ...number: string[]) {
Expand All @@ -118,7 +128,19 @@ function scheduleCourseId(course: ScheduleCourse) {

function search(searchThrough: string, term: string) {
const st = searchThrough.toLowerCase().replace(/_/g, ' ');
return term.split(' ').every((t) => getSearchRegex(t).test(st));

// true = valid regex, match; false = valid regex, no match; 'invalid_regex' = invalid regex
const regexStateList: Array<boolean | 'invalid_regex'> = term
.split(' ')
.map((t) => {
const regex = getSearchRegex(t);
return regex ? regex.test(st) : 'invalid_regex';
});

const isAllRegexMatches = regexStateList.every((t) => t === true);
const isSomeInvalidRegex = regexStateList.some((t) => t === 'invalid_regex');

return isSomeInvalidRegex ? 'invalid_regex' : isAllRegexMatches;
}

export function searchPlan(
Expand Down Expand Up @@ -156,15 +178,30 @@ export function searchPlan(
const id = fullId(subject, ...rest);

for (const term of terms) {
if (search(id, term)) {
const searchIdResult = search(id, term);
const searchCourseResult = search(course.name, term);

if (
searchIdResult == 'invalid_regex' ||
searchCourseResult == 'invalid_regex'
) {
return 'invalid_regex';
}

if (searchIdResult) {
courseIdResults.push(course);
} else if (search(course.name, term)) {
} else if (searchCourseResult) {
courseNameResults.push(course);
}
}
};

courseData.courses.forEach(checkCourse);
for (const course of courseData.courses) {
const result = checkCourse(course);
if (result == 'invalid_regex') {
return 'invalid_regex';
}
}

if (filter?.include?.includes('Legacy Courses')) {
courseData.legacy.forEach(checkCourse);
Expand Down Expand Up @@ -240,18 +277,37 @@ export function searchSchedule(

const id = fullId(course.subject, course.number);
for (const term of terms) {
if (search(id, term)) {
const searchIdResult = search(id, term);
const searchCourseResult = search(course.title, term);
const isSectionsContainsInvalidRegex = course.sections.some(
(s) => s.topic && search(s.topic, term) == 'invalid_regex'
);

if (
searchIdResult == 'invalid_regex' ||
searchCourseResult == 'invalid_regex' ||
isSectionsContainsInvalidRegex
) {
return 'invalid_regex';
}

if (searchIdResult) {
courseIdResults.push(course);
} else if (
search(course.title, term) ||
searchCourseResult ||
course.sections.some((s) => s.topic && search(s.topic, term))
) {
courseNameResults.push(course);
}
}
};

courseData.forEach(checkCourse);
for (const course of courseData) {
const result = checkCourse(course);
if (result == 'invalid_regex') {
return 'invalid_regex';
}
}

const total = courseIdResults.length + courseNameResults.length;
if (total === 0) return 'no_results';
Expand Down
11 changes: 11 additions & 0 deletions src/components/search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ export default function Search({
};
}

if (results === 'invalid_regex') {
return {
placeholder: (
<SearchMessage
title="Invalid regular expression!"
subtitle="Your search filter may contain a regular expression. Make sure to double check your syntax!"
/>
),
};
}

if (results === 'no_results') {
return {
placeholder: (
Expand Down
3 changes: 2 additions & 1 deletion src/types/SearchTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export type SearchError =
| 'no_query'
| 'too_short'
| 'no_results'
| 'not_loaded';
| 'not_loaded'
| 'invalid_regex';

export interface SearchFilter {
get: FilterOptions;
Expand Down