-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
872 additions
and
877 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
else { | ||
if (negate) { | ||
operandStack.push(new SearchAST(null, true, token)); | ||
} | ||
else { | ||
operandStack.push(token); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
if (operandStack.length > 1) { | ||
throw new Error('Missing operator.'); | ||
} | ||
|
||
op1 = operandStack.pop(); | ||
|
||
if (typeof op1 === 'undefined') { | ||
return new SearchAST(); | ||
} | ||
|
||
if (isTerminal(op1)) { | ||
return new SearchAST(null, false, op1); | ||
} | ||
|
||
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; | ||
op: any; | ||
rightOperand: any; | ||
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. | ||
ast2, | ||
// Parent node of the current subtree. | ||
parentAST; | ||
|
||
// 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 GitHub Actions / JavaScript Linting and Unit Tests
|
||
throw new Error("Method not implemented."); | ||
} | ||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.