/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
const KNOWN_FILTER_PREFIXES = new Set([
'from:',
'-from:',
'mentions:',
'-mentions:',
'in:',
'-in:',
'before:',
'during:',
'on:',
'after:',
'has:',
'-has:',
'pinned:',
'author-type:',
'sort:',
'order:',
'nsfw:',
'embed-type:',
'-embed-type:',
'embed-provider:',
'-embed-provider:',
'link:',
'-link:',
'filename:',
'-filename:',
'ext:',
'-ext:',
'last:',
'beforeid:',
'afterid:',
'any:',
'scope:',
]);
const isFilterPrefix = (text: string): boolean => {
const lower = text.toLowerCase();
for (const prefix of KNOWN_FILTER_PREFIXES) {
if (lower.startsWith(prefix)) {
return true;
}
}
return false;
};
const skipFilterValue = (query: string, startIndex: number): number => {
let i = startIndex;
const n = query.length;
let inQuotes = false;
let escaped = false;
while (i < n) {
const ch = query[i];
if (escaped) {
escaped = false;
i++;
continue;
}
if (ch === '\\') {
escaped = true;
i++;
continue;
}
if (ch === '"') {
inQuotes = !inQuotes;
i++;
continue;
}
if (!inQuotes && ch === ' ') {
break;
}
i++;
}
return i;
};
export const tokenizeSearchQuery = (query: string): Array => {
const tokens: Array = [];
const n = query.length;
let i = 0;
while (i < n) {
while (i < n && query[i] === ' ') {
i++;
}
if (i >= n) {
break;
}
const remaining = query.slice(i);
if (isFilterPrefix(remaining)) {
const colonIndex = remaining.indexOf(':');
if (colonIndex !== -1) {
i = skipFilterValue(query, i + colonIndex + 1);
continue;
}
}
if (query[i] === '"') {
i++;
let token = '';
let escaped = false;
while (i < n) {
const ch = query[i];
if (escaped) {
token += ch;
escaped = false;
i++;
continue;
}
if (ch === '\\') {
escaped = true;
i++;
continue;
}
if (ch === '"') {
i++;
break;
}
token += ch;
i++;
}
const trimmed = token.trim();
if (trimmed) {
tokens.push(trimmed);
}
continue;
}
let token = '';
while (i < n && query[i] !== ' ' && query[i] !== '"') {
token += query[i];
i++;
}
const trimmed = token.trim();
if (trimmed) {
tokens.push(trimmed);
}
}
return tokens;
};