Commit 6eb06b24 authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Migrate view-filter to TypeScript

parent cf33fdc3
/** import type { Annotation } from '../../types/api';
* @typedef {import('../../types/api').Annotation} Annotation import type { Facet } from '../util/search-filter';
* @typedef {import('../util/search-filter').Facet} Facet
*/
import * as unicodeUtils from '../util/unicode'; import * as unicodeUtils from '../util/unicode';
import { quote } from './annotation-metadata'; import { quote } from './annotation-metadata';
/** type Filter = {
* @typedef Filter matches: (ann: Annotation) => boolean;
* @prop {(ann: Annotation) => boolean} matches };
*/
/** /**
* A Matcher specifies how to test whether an annotation matches a query term * A Matcher specifies how to test whether an annotation matches a query term
* for a specific field. * for a specific field.
*
* @template [T=string] - Type of parsed query terms and field values
* @typedef Matcher
* @prop {(ann: Annotation) => T[]} fieldValues - Extract the field values to be
* matched against a query term
* @prop {(value: T, term: T) => boolean} matches - Test whether a query term
* matches a field value. Both value and term will have been normalized using
* `normalize`.
* @prop {(val: T) => T} normalize - Normalize a parsed term or field value for
* comparison
*/ */
type Matcher<T = string> = {
/** Extract the field values to be matched against a query term */
fieldValues: (ann: Annotation) => T[];
/**
* Test whether a query term matches a field value. Both value and term will
* have been normalized using `normalize`.
*/
matches: (value: T, term: T) => boolean;
/** Normalize a parsed term or field value for comparison */
normalize: (val: T) => T;
};
/** /**
* Normalize a string query term or field value. * Normalize a string query term or field value.
*
* @param {string} val
*/ */
function normalizeStr(val) { function normalizeStr(val: string): string {
return unicodeUtils.fold(unicodeUtils.normalize(val)).toLowerCase(); return unicodeUtils.fold(unicodeUtils.normalize(val)).toLowerCase();
} }
...@@ -40,22 +38,19 @@ function normalizeStr(val) { ...@@ -40,22 +38,19 @@ function normalizeStr(val) {
* @template TermType * @template TermType
* @implements {Filter} * @implements {Filter}
*/ */
class TermFilter { class TermFilter<TermType extends string> implements Filter {
/** public term: TermType;
* @param {TermType} term public matcher: Matcher<TermType>;
* @param {Matcher<TermType>} matcher
*/ constructor(term: TermType, matcher: Matcher<TermType>) {
constructor(term, matcher) {
this.term = matcher.normalize(term); this.term = matcher.normalize(term);
this.matcher = matcher; this.matcher = matcher;
} }
/** /**
* Return true if an annotation matches this filter. * Return true if an annotation matches this filter.
*
* @param {Annotation} ann
*/ */
matches(ann) { matches(ann: Annotation): boolean {
const matcher = this.matcher; const matcher = this.matcher;
return matcher return matcher
.fieldValues(ann) .fieldValues(ann)
...@@ -65,25 +60,24 @@ class TermFilter { ...@@ -65,25 +60,24 @@ class TermFilter {
/** /**
* Filter that combines other filters using AND or OR combinators. * Filter that combines other filters using AND or OR combinators.
*
* @implements {Filter}
*/ */
class BooleanOpFilter { class BooleanOpFilter implements Filter {
public operator: 'and' | 'or';
public filters: Filter[];
/** /**
* @param {'and'|'or'} op - Boolean operator * @param op - Boolean operator
* @param {Filter[]} filters - Array of filters to test against * @param filters - Array of filters to test against
*/ */
constructor(op, filters) { constructor(op: 'and' | 'or', filters: Filter[]) {
this.operator = op; this.operator = op;
this.filters = filters; this.filters = filters;
} }
/** /**
* Return true if an annotation matches this filter. * Return true if an annotation matches this filter.
*
* @param {Annotation} ann
*/ */
matches(ann) { matches(ann: Annotation): boolean {
if (this.operator === 'and') { if (this.operator === 'and') {
return this.filters.every(filter => filter.matches(ann)); return this.filters.every(filter => filter.matches(ann));
} else { } else {
...@@ -95,11 +89,10 @@ class BooleanOpFilter { ...@@ -95,11 +89,10 @@ class BooleanOpFilter {
/** /**
* Create a matcher that tests whether a query term appears anywhere in a * Create a matcher that tests whether a query term appears anywhere in a
* string field value. * string field value.
*
* @param {(ann: Annotation) => string[]} fieldValues
* @return {Matcher}
*/ */
function stringFieldMatcher(fieldValues) { function stringFieldMatcher(
fieldValues: (ann: Annotation) => string[]
): Matcher {
return { return {
fieldValues, fieldValues,
matches: (value, term) => value.includes(term), matches: (value, term) => value.includes(term),
...@@ -109,20 +102,17 @@ function stringFieldMatcher(fieldValues) { ...@@ -109,20 +102,17 @@ function stringFieldMatcher(fieldValues) {
/** /**
* Map of field name (from a parsed query) to matcher for that field. * Map of field name (from a parsed query) to matcher for that field.
*
* @type {Record<string, Matcher|Matcher<number>>}
*/ */
const fieldMatchers = { const fieldMatchers: Record<string, Matcher | Matcher<number>> = {
quote: stringFieldMatcher(ann => [quote(ann) ?? '']), quote: stringFieldMatcher(ann => [quote(ann) ?? '']),
/** @type {Matcher<number>} */
since: { since: {
fieldValues: ann => [new Date(ann.updated).valueOf()], fieldValues: ann => [new Date(ann.updated).valueOf()],
matches: (updatedTime, age) => { matches: (updatedTime: number, age: number) => {
const delta = (Date.now() - updatedTime) / 1000; const delta = (Date.now() - updatedTime) / 1000;
return delta <= age; return delta <= age;
}, },
normalize: timestamp => timestamp, normalize: (timestamp: number) => timestamp,
}, },
tag: stringFieldMatcher(ann => ann.tags), tag: stringFieldMatcher(ann => ann.tags),
...@@ -137,22 +127,18 @@ const fieldMatchers = { ...@@ -137,22 +127,18 @@ const fieldMatchers = {
/** /**
* Filter a set of annotations against a parsed query. * Filter a set of annotations against a parsed query.
* *
* @param {Annotation[]} annotations * @return IDs of matching annotations.
* @param {Record<string, Facet>} filters
* @return {string[]} IDs of matching annotations.
*/ */
export function filterAnnotations(annotations, filters) { export function filterAnnotations(
/** annotations: Annotation[],
* @template TermType filters: Record<string, Facet>
* @param {string} field ): string[] {
* @param {TermType} term const makeTermFilter = <TermType>(field: string, term: TermType) =>
*/
const makeTermFilter = (field, term) =>
new TermFilter( new TermFilter(
term, term,
// Suppress error about potential mismatch of query term type // Suppress error about potential mismatch of query term type
// and what the matcher expects. We assume these match up. // and what the matcher expects. We assume these match up.
/** @type {Matcher<any>} */ (fieldMatchers[field]) fieldMatchers[field] as Matcher<any>
); );
// Convert the input filter object into a filter tree, expanding "any" // Convert the input filter object into a filter tree, expanding "any"
...@@ -182,5 +168,5 @@ export function filterAnnotations(annotations, filters) { ...@@ -182,5 +168,5 @@ export function filterAnnotations(annotations, filters) {
.filter(ann => { .filter(ann => {
return ann.id && rootFilter.matches(ann); return ann.id && rootFilter.matches(ann);
}) })
.map(ann => /** @type {string} */ (ann.id)); .map(ann => ann.id as string);
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment