Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
liamwhite committed Mar 10, 2024
1 parent 886539c commit 38340ce
Show file tree
Hide file tree
Showing 11 changed files with 872 additions and 877 deletions.
877 changes: 0 additions & 877 deletions assets/js/match_query.js

This file was deleted.

218 changes: 218 additions & 0 deletions assets/js/match_query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* booru.match_query: A port and modification of the search_parser library for
* performing client-side filtering.
*/

function parseTokens(lexicalArray) {
const operandStack = [];
let negate, op1, op2;
lexicalArray.forEach((token, i) => {
if (token !== 'not_op') {
negate = lexicalArray[i + 1] === 'not_op';

if (typeof token === 'string') {
op2 = operandStack.pop();
op1 = operandStack.pop();

if (typeof op1 === 'undefined' || typeof op2 === 'undefined') {
throw new Error('Missing operand.');
}

operandStack.push(new SearchAST(token, negate, op1, op2));

Check failure on line 21 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

'SearchAST' was used before it was defined
}
else {
if (negate) {
operandStack.push(new SearchAST(null, true, token));

Check failure on line 25 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

'SearchAST' was used before it was defined
}
else {
operandStack.push(token);
}
}
}
});

if (operandStack.length > 1) {
throw new Error('Missing operator.');
}

op1 = operandStack.pop();

if (typeof op1 === 'undefined') {
return new SearchAST();

Check failure on line 41 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

'SearchAST' was used before it was defined
}

if (isTerminal(op1)) {
return new SearchAST(null, false, op1);

Check failure on line 45 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

'SearchAST' was used before it was defined
}

return op1;
}

function parseSearch(searchStr) {
return parseTokens(generateLexArray(searchStr));
}

function isTerminal(operand) {
// Whether operand is a terminal SearchTerm.
return typeof operand.term !== 'undefined';
}

class SearchAST {
negate: boolean;
leftOperand: any;

Check warning on line 62 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Unexpected any. Specify a different type
op: any;

Check warning on line 63 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Unexpected any. Specify a different type
rightOperand: any;

Check warning on line 64 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Unexpected any. Specify a different type
constructor(op, negate, leftOperand, rightOperand) {
this.negate = Boolean(negate);
this.leftOperand = leftOperand || null;
this.op = op || null;
this.rightOperand = rightOperand || null;
}
// Evaluation of the AST in regard to a target image
hitsImage(image) {
const treeStack = [];
// Left side node.
// eslint-disable-next-line @typescript-eslint/no-this-alias,consistent-this
let ast1 = this,
// Right side node.

Check failure on line 77 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Expected indentation of 8 spaces but found 6
ast2,

Check failure on line 78 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Expected indentation of 8 spaces but found 6
// Parent node of the current subtree.

Check failure on line 79 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Expected indentation of 8 spaces but found 6
parentAST;

Check failure on line 80 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Expected indentation of 8 spaces but found 6

// Build the initial tree node traversal stack, of the "far left" side.
// The general idea is to accumulate from the bottom and make stacks
// of right-hand subtrees that themselves accumulate upward. The left
// side node, ast1, will always be a Boolean representing the left-side
// evaluated value, up to the current subtree (parentAST).
while (!isTerminal(ast1)) {
treeStack.push(ast1);
ast1 = ast1.leftOperand;

if (!ast1) {
// Empty tree.
return false;
}
}

ast1 = ast1.match(image);
treeStack.push(ast1);

while (treeStack.length > 0) {
parentAST = treeStack.pop();

if (parentAST === null) {
// We are at the end of a virtual stack for a right node
// subtree. We switch the result of this stack from left
// (ast1) to right (ast2), pop the original left node,
// and finally pop the parent subtree itself. See near the
// end of this function to view how this is populated.
ast2 = ast1;
ast1 = treeStack.pop();
parentAST = treeStack.pop();
}
else {
// First, check to see if we can do a short-circuit
// evaluation to skip evaluating the right side entirely.
if (!ast1 && parentAST.op === 'and_op') {
ast1 = parentAST.negate;
continue;
}

if (ast1 && parentAST.op === 'or_op') {
ast1 = !parentAST.negate;
continue;
}

// If we are not at the end of a stack, grab the right
// node. The left node (ast1) is currently a terminal Boolean.
ast2 = parentAST.rightOperand;
}

if (typeof ast2 === 'boolean') {
ast1 = combineOperands(ast1, ast2, parentAST);
}
else if (!ast2) {
// A subtree with a single node. This is generally the case
// for negated tokens.
if (parentAST.negate) {
ast1 = !ast1;
}
}
else if (isTerminal(ast2)) {
// We are finally at a leaf and can evaluate.
ast2 = ast2.match(image);
ast1 = combineOperands(ast1, ast2, parentAST);
}
else {
// We are at a node whose right side is a new subtree.
// We will build a new "virtual" stack, but instead of
// building a new Array, we can insert a null object as a
// marker.
treeStack.push(parentAST, ast1, null);

do {
treeStack.push(ast2);
ast2 = ast2.leftOperand;
} while (!isTerminal(ast2));

ast1 = ast2.match(image);
}
}

return ast1;
}
match(image: any): this {

Check failure on line 164 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

'image' is defined but never used

Check warning on line 164 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Unexpected any. Specify a different type
throw new Error("Method not implemented.");

Check failure on line 165 in assets/js/match_query.ts

View workflow job for this annotation

GitHub Actions / JavaScript Linting and Unit Tests

Strings must use singlequote
}
dumpTree() {
// Dumps to string a simple diagram of the syntax tree structure
// (starting with this object as the root) for debugging purposes.
const retStrArr = [], treeQueue = [['', this]];
let treeArr, prefix, tree;

while (treeQueue.length > 0) {
treeArr = treeQueue.shift();
prefix = treeArr[0];
tree = treeArr[1];

if (isTerminal(tree)) {
retStrArr.push(`${prefix}-> ${tree.term}`);
}
else {
if (tree.negate) {
retStrArr.push(`${prefix}+ NOT_OP`);
prefix += '\t';
}
if (tree.op) {
retStrArr.push(`${prefix}+ ${tree.op.toUpperCase()}`);
prefix += '\t';
treeQueue.unshift([prefix, tree.rightOperand]);
treeQueue.unshift([prefix, tree.leftOperand]);
}
else {
treeQueue.unshift([prefix, tree.leftOperand]);
}
}
}

return retStrArr.join('\n');
}
}

function combineOperands(ast1, ast2, parentAST) {
let localAst1;
if (parentAST.op === 'and_op') {
localAst1 = ast1 && ast2;
}
else {
localAst1 = ast1 || ast2;
}

if (parentAST.negate) {
return !localAst1;
}

return localAst1;
}

export default parseSearch;
145 changes: 145 additions & 0 deletions assets/js/query/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Matcher, RangeEqualQualifier } from './types';

type Year = number;
type Month = number;
type Day = number;
type Hours = number;
type Minutes = number;
type Seconds = number;
type AbsoluteDate = [Year, Month, Day, Hours, Minutes, Seconds];
type TimeZoneOffset = [Hours, Minutes];
type PosixTimeMs = number;

function makeMatcher(bottomDate: PosixTimeMs, topDate: PosixTimeMs, qual: RangeEqualQualifier): Matcher {
// The open-left, closed-right date range specified by the
// date/time format limits the types of comparisons that are
// done compared to numeric ranges.
switch (qual) {
case 'lte':
return (v) => new Date(v).getTime() < topDate;
case 'gte':
return (v) => new Date(v).getTime() >= bottomDate;
case 'lt':
return (v) => new Date(v).getTime() < bottomDate;
case 'gt':
return (v) => new Date(v).getTime() >= topDate;
case 'eq':
default:
return (v) => {
const t = new Date(v).getTime();
return t >= bottomDate && t < topDate;
};
}
}

function makeRelativeDateMatcher(dateVal: string, qual: RangeEqualQualifier): Matcher {
const match = /(\d+) (second|minute|hour|day|week|month|year)s? ago/.exec(dateVal);
const bounds: Record<string, number> = {
second: 1000,
minute: 60000,
hour: 3600000,
day: 86400000,
week: 604800000,
month: 2592000000,
year: 31536000000
};

if (match) {
const amount = parseInt(match[1], 10);
const scale = bounds[match[2]];

const now = new Date().getTime();
const bottomDate = new Date(now - (amount * scale)).getTime();
const topDate = new Date(now - ((amount - 1) * scale)).getTime();

return makeMatcher(bottomDate, topDate, qual);
}
else {
throw new Error(`Cannot parse date string: ${dateVal}`);
}
}

function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): Matcher {
const parseRes: RegExp[] = [
/^(\d{4})/,
/^-(\d{2})/,
/^-(\d{2})/,
/^(?:\s+|T|t)(\d{2})/,
/^:(\d{2})/,
/^:(\d{2})/
];
const timeZoneOffset: TimeZoneOffset = [0, 0];
const timeData: AbsoluteDate = [0, 0, 1, 0, 0, 0];

const origDateVal: string = dateVal;
let localDateVal = origDateVal;

const offsetMatch = /([+-])(\d{2}):(\d{2})$/.exec(localDateVal);
if (offsetMatch) {
timeZoneOffset[0] = parseInt(offsetMatch[2], 10);
timeZoneOffset[1] = parseInt(offsetMatch[3], 10);
if (offsetMatch[1] === '-') {
timeZoneOffset[0] *= -1;
timeZoneOffset[1] *= -1;
}
localDateVal = localDateVal.substr(0, localDateVal.length - 6);
}
else {
localDateVal = localDateVal.replace(/[Zz]$/, '');
}

let matchIndex = 0;
for (; matchIndex < parseRes.length; matchIndex += 1) {
if (localDateVal.length === 0) {
break;
}

const componentMatch = parseRes[matchIndex].exec(localDateVal);
if (componentMatch) {
if (matchIndex === 1) {
// Months are offset by 1.
timeData[matchIndex] = parseInt(componentMatch[1], 10) - 1;
}
else {
// All other components are not offset.
timeData[matchIndex] = parseInt(componentMatch[1], 10);
}

// Slice string.
localDateVal = localDateVal.substr(
componentMatch[0].length, localDateVal.length - componentMatch[0].length
);
}
else {
throw new Error(`Cannot parse date string: ${origDateVal}`);
}
}

if (localDateVal.length > 0) {
throw new Error(`Cannot parse date string: ${origDateVal}`);
}

// Apply the user-specified time zone offset. The JS Date constructor
// is very flexible here.
timeData[3] -= timeZoneOffset[0];
timeData[4] -= timeZoneOffset[1];

const asPosix = (data: AbsoluteDate) => {
return new Date(Date.UTC.apply(Date, data)).getTime();
}

const bottomDate = asPosix(timeData);
timeData[matchIndex - 1] += 1;
const topDate = asPosix(timeData);

return makeMatcher(bottomDate, topDate, qual);
}

export function makeDateMatcher(dateVal: string, qual: RangeEqualQualifier): Matcher {
try {
return makeAbsoluteDateMatcher(dateVal, qual);
}
catch (_) {
return makeRelativeDateMatcher(dateVal, qual);
}
}
Loading

0 comments on commit 38340ce

Please sign in to comment.