Skip to content

Commit

Permalink
Merge pull request #812 from obgnail/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
obgnail authored Oct 24, 2024
2 parents 1cffb18 + 16bcd7d commit 0e05280
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 124 deletions.
160 changes: 61 additions & 99 deletions plugin/global/core/utils/mixin/searchStringParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
* <query> ::= <expr>
* <expr> ::= <term> ( <or> <term> )*
* <term> ::= <factor> ( <not_and> <factor> )*
* <factor> ::= '"' <keyword> '"' | <keyword> | '/' <regexp> '/' | '(' <expr> ')'
* <factor> ::= <qualifier>? <match>
* <qualifier> ::= <scope> <operator>
* <match> ::= <keyword> | '"'<keyword>'"' | '/'<regexp>'/' | '('<expr>')'
* <not_and> ::= '-' | ' '
* <or> ::= 'OR' | '|'
* <keyword> ::= [^"]+
* <regexp> ::= [^/]+
*/
* <operator> ::= ':' | '=' | '>=' | '<=' | '>' | '<'
* <scope> ::= 'default' | 'file' | 'path' | 'ext' | 'content' | 'size' | 'time'
* */
class searchStringParser {
constructor(utils) {
this.utils = utils;
constructor() {
this.TYPE = {
OR: "OR",
AND: "AND",
Expand All @@ -21,6 +24,7 @@ class searchStringParser {
KEYWORD: "KEYWORD",
PHRASE: "PHRASE",
REGEXP: "REGEXP",
QUALIFIER: "QUALIFIER",
}
this.TOKEN = {
OR: { type: this.TYPE.OR, value: "OR" },
Expand All @@ -31,12 +35,22 @@ class searchStringParser {
PHRASE: value => ({ type: this.TYPE.PHRASE, value }),
KEYWORD: value => ({ type: this.TYPE.KEYWORD, value }),
REGEXP: value => ({ type: this.TYPE.REGEXP, value }),
QUALIFIER: (scope, operator) => ({ type: this.TYPE.QUALIFIER, scope, operator }),
}
this.setQualifier();
}

setQualifier(
scope = ["default", "file", "path", "ext", "content", "time", "size"],
operator = [">=", "<=", ":", "=", ">", "<"],
) {
this.qualifierRegExp = new RegExp(`^(?<scope>${scope.join('|')})(?<operator>${operator.join('|')})`, "i");
}

_tokenize(query) {
const tokens = [];
let i = 0;
let qualifierMatch = null;
while (i < query.length) {
if (query[i] === '"') {
const start = i + 1;
Expand All @@ -63,6 +77,10 @@ class searchStringParser {
i++;
} else if (/\s/.test(query[i])) {
i++; // skip whitespace
} else if (qualifierMatch = query.substring(i).match(this.qualifierRegExp)) {
const { scope, operator } = qualifierMatch.groups;
tokens.push(this.TOKEN.QUALIFIER(scope, operator));
i += scope.length + operator.length;
} else if (query[i] === "/") {
const regexpStart = i;
i++;
Expand All @@ -85,12 +103,12 @@ class searchStringParser {
}

const result = [];
const l1 = [this.TYPE.NOT, this.TYPE.OR, this.TYPE.PAREN_OPEN];
const l2 = [this.TYPE.NOT, this.TYPE.OR, this.TYPE.PAREN_CLOSE];
const invalidPre = [this.TYPE.NOT, this.TYPE.OR, this.TYPE.PAREN_OPEN, this.TYPE.QUALIFIER];
const invalidCur = [this.TYPE.NOT, this.TYPE.OR, this.TYPE.PAREN_CLOSE];
for (let i = 0; i < tokens.length; i++) {
const current = tokens[i];
const previous = tokens[i - 1];
const should = previous && !l1.includes(previous.type) && !l2.includes(current.type);
const should = previous && !invalidPre.includes(previous.type) && !invalidCur.includes(current.type);
if (should) {
result.push(this.TOKEN.AND);
}
Expand Down Expand Up @@ -130,6 +148,14 @@ class searchStringParser {
}

_parseFactor(tokens) {
const qualifier = (tokens[0].type === this.TYPE.QUALIFIER)
? tokens.shift()
: this.TOKEN.QUALIFIER("default", ":");
const node = this._parseMatch(tokens);
return this._setQualifier(node, qualifier);
}

_parseMatch(tokens) {
const type = tokens[0].type;
if (type === this.TYPE.PHRASE || type === this.TYPE.KEYWORD || type === this.TYPE.REGEXP) {
return { type, value: tokens.shift().value };
Expand All @@ -143,119 +169,55 @@ class searchStringParser {
}
}

_withNotification(func) {
try {
return func();
} catch (e) {
this.utils.notification.show("语法解析错误,请检查输入内容", "error");
_setQualifier(node, qualifier) {
if (!node) return;
const type = node.type;
const isLeaf = type === this.TYPE.PHRASE || type === this.TYPE.KEYWORD || type === this.TYPE.REGEXP;
if (isLeaf && (!node.scope || node.scope === "default")) {
node.scope = qualifier.scope;
node.operator = qualifier.operator;
} else {
this._setQualifier(node.left, qualifier);
this._setQualifier(node.right, qualifier);
}
}

showGrammar() {
const table1 = `
<table>
<tr><th>Token</th><th>Description</th></tr>
<tr><td>whitespace</td><td>表示与,文档应该同时包含全部关键词</td></tr>
<tr><td>OR</td><td>表示或,文档应该包含关键词之一</td></tr>
<tr><td>-</td><td>表示非,文档不能包含关键词</td></tr>
<tr><td>""</td><td>词组</td></tr>
<tr><td>//</td><td>正则表达式</td></tr>
<tr><td>()</td><td>调整运算顺序</td></tr>
</table>
`
const table2 = `
<table>
<tr><th>Example</th><th>Description</th></tr>
<tr><td>foo bar</td><td>搜索包含 foo 和 bar 的文档</td></tr>
<tr><td>foo OR bar</td><td>搜索包含 foo 或 bar 的文档</td></tr>
<tr><td>foo -bar</td><td>搜索包含 foo 但不包含 bar 的文档</td></tr>
<tr><td>"foo bar"</td><td>搜索包含 foo bar 这一词组的文档</td></tr>
<tr><td>foo /bar\\d/ baz</td><td>搜索包含 foo 和 正则 bar\\d 和 baz 的文档</td></tr>
<tr><td>(a OR b) (c OR d)</td><td>搜索包含 a 或 b,且包含 c 或 d 的文档</td></tr>
</table>
`
const content = `
<query> ::= <expr>
<expr> ::= <term> ( <or> <term> )*
<term> ::= <factor> ( <not_and> <factor> )*
<factor> ::= '"'<keyword>'"' | <keyword> | '/'<regexp>'/' | '('<expr>')'
<not_and> ::= '-' | ' '
<or> ::= 'OR' | '|'
<keyword> ::= [^"]+
<regexp> ::= [^/]+`
const title = "你可以将这段内容塞给AI,它会为你解释";
const components = [{ label: table1, type: "p" }, { label: table2, type: "p" }, { label: "", type: "textarea", rows: 8, content, title }];
this.utils.dialog.modal({ title: "搜索语法", width: "550px", components });
return node
}

parse(query) {
return this._withNotification(() => {
const tokens = this._tokenize(query);
if (tokens.length === 0) {
return this.TOKEN.KEYWORD("")
}
const result = this._parseExpression(tokens);
if (tokens.length !== 0) {
throw new Error(`parse error. remind tokens: ${tokens}`)
}
return result
})
}

checkByAST(ast, content) {
const { KEYWORD, PHRASE, REGEXP, OR, AND, NOT } = this.TYPE;

function evaluate({ type, left, right, value }) {
switch (type) {
case KEYWORD:
case PHRASE:
return content.includes(value);
case REGEXP:
return new RegExp(value).test(content);
case OR:
return evaluate(left) || evaluate(right);
case AND:
return evaluate(left) && evaluate(right);
case NOT:
return (left ? evaluate(left) : true) && !evaluate(right);
default:
throw new Error(`Unknown AST node type: ${type}`);
}
const tokens = this._tokenize(query);
if (tokens.length === 0) {
return this.TOKEN.KEYWORD("")
}

return evaluate(ast);
const result = this._parseExpression(tokens);
if (tokens.length !== 0) {
throw new Error(`parse error. remind tokens: ${tokens}`)
}
return result
}

getQueryTokens(query) {
evaluate(ast, { keyword, phrase, regexp }) {
const { KEYWORD, PHRASE, REGEXP, OR, AND, NOT } = this.TYPE;

function evaluate({ type, left, right, value }) {
function _eval({ type, left, right, scope, operator, value }) {
switch (type) {
case KEYWORD:
return keyword(scope, operator, value);
case PHRASE:
return [value];
return phrase(scope, operator, value);
case REGEXP:
return [];
return regexp(scope, operator, value);
case OR:
return _eval(left) || _eval(right);
case AND:
return [...evaluate(left), ...evaluate(right)];
return _eval(left) && _eval(right);
case NOT:
const wont = evaluate(right);
return (left ? evaluate(left) : []).filter(e => !wont.includes(e));
return (left ? _eval(left) : true) && !_eval(right);
default:
throw new Error(`Unknown AST node type: ${type}`);
}
}

return evaluate(this.parse(query));
}

check(query, content, option = {}) {
if (!option.caseSensitive) {
query = query.toLowerCase();
content = content.toLowerCase();
}
return this.checkByAST(this.parse(query), content);
return _eval(ast);
}
}

Expand Down
2 changes: 0 additions & 2 deletions plugin/global/settings/settings.default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ SHOW_MTIME = false
REFOUCE_WHEN_OPEN_FILE = false
# 搜索内容时大小写敏感(此处配置的是默认值,使用时可以在页面通过点击按钮重新设置)
CASE_SENSITIVE = false
# 将文件路径加入搜索内容(此处配置的是默认值,使用时可以在页面通过点击按钮重新设置)
INCLUDE_FILE_PATH = true
# 搜索时联动其他插件(此处配置的是默认值,使用时可以在页面通过点击按钮重新设置)
LINK_OTHER_PLUGIN = true
# 查找文件的最大尺寸。大于MAX_SIZE的文件在搜索时将被忽略。若<0则不忽略任何文件
Expand Down
2 changes: 1 addition & 1 deletion plugin/global/styles/search_multi.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
border-radius: 2px;
padding-left: 5px;
padding-right: 110px;
padding-right: 80px;
}

#plugin-search-multi-input svg {
Expand Down
Loading

0 comments on commit 0e05280

Please sign in to comment.