Files
Trilium/src/services/search/services/parse.js

351 lines
11 KiB
JavaScript
Raw Normal View History

2020-05-21 11:46:01 +02:00
"use strict";
2020-07-21 00:01:07 +02:00
const AndExp = require('../expressions/and.js');
const OrExp = require('../expressions/or.js');
const NotExp = require('../expressions/not.js');
const ChildOfExp = require('../expressions/child_of.js');
const DescendantOfExp = require('../expressions/descendant_of.js');
const ParentOfExp = require('../expressions/parent_of.js');
const RelationWhereExp = require('../expressions/relation_where.js');
const PropertyComparisonExp = require('../expressions/property_comparison.js');
const AttributeExistsExp = require('../expressions/attribute_exists.js');
const LabelComparisonExp = require('../expressions/label_comparison.js');
2020-08-06 23:55:17 +02:00
const NoteCacheFulltextExp = require('../expressions/note_cache_flat_text.js');
2020-07-21 00:01:07 +02:00
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js');
const comparatorBuilder = require('./build_comparator.js');
const ValueExtractor = require('../value_extractor.js');
2020-05-19 00:00:35 +02:00
2020-05-21 11:46:01 +02:00
function getFulltext(tokens, parsingContext) {
2020-07-19 23:19:45 +02:00
tokens = tokens.map(t => t.token);
2020-05-21 11:46:01 +02:00
parsingContext.highlightedTokens.push(...tokens);
2020-05-21 11:18:15 +02:00
2020-05-20 23:20:39 +02:00
if (tokens.length === 0) {
return null;
}
2020-05-21 11:46:01 +02:00
else if (parsingContext.includeNoteContent) {
2020-05-20 23:20:39 +02:00
return new OrExp([
new NoteCacheFulltextExp(tokens),
2020-06-20 23:46:49 +02:00
new NoteContentProtectedFulltextExp('*=*', tokens),
new NoteContentUnprotectedFulltextExp('*=*', tokens)
2020-05-20 23:20:39 +02:00
]);
2020-05-19 00:00:35 +02:00
}
else {
2020-05-20 23:20:39 +02:00
return new NoteCacheFulltextExp(tokens);
2020-05-19 00:00:35 +02:00
}
}
function isOperator(str) {
2020-05-20 23:20:39 +02:00
return str.match(/^[=<>*]+$/);
2020-05-19 00:00:35 +02:00
}
2020-05-25 00:25:47 +02:00
function getExpression(tokens, parsingContext, level = 0) {
2020-05-20 23:20:39 +02:00
if (tokens.length === 0) {
return null;
}
2020-05-19 00:00:35 +02:00
const expressions = [];
let op = null;
let i;
function context(i) {
let {startIndex, endIndex} = tokens[i];
startIndex = Math.max(0, startIndex - 20);
endIndex = Math.min(parsingContext.originalQuery.length, endIndex + 20);
return '"' + (startIndex !== 0 ? "..." : "")
+ parsingContext.originalQuery.substr(startIndex, endIndex - startIndex)
+ (endIndex !== parsingContext.originalQuery.length ? "..." : "") + '"';
}
function parseNoteProperty() {
2020-07-19 23:19:45 +02:00
if (tokens[i].token !== '.') {
parsingContext.addError('Expected "." to separate field path');
return;
}
i++;
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'content') {
2020-05-26 23:25:13 +02:00
i += 1;
2020-07-19 23:19:45 +02:00
const operator = tokens[i].token;
2020-05-26 23:25:13 +02:00
if (!isOperator(operator)) {
parsingContext.addError(`After content expected operator, but got "${tokens[i].token}" in ${context(i)}`);
2020-05-26 23:25:13 +02:00
return;
}
i++;
2020-06-20 23:46:49 +02:00
return new OrExp([
2020-07-19 23:19:45 +02:00
new NoteContentUnprotectedFulltextExp(operator, [tokens[i].token]),
new NoteContentProtectedFulltextExp(operator, [tokens[i].token])
2020-06-20 23:46:49 +02:00
]);
2020-05-26 23:25:13 +02:00
}
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'parents') {
i += 1;
return new ChildOfExp(parseNoteProperty());
2020-05-23 10:36:49 +02:00
}
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'children') {
2020-05-23 10:36:49 +02:00
i += 1;
return new ParentOfExp(parseNoteProperty());
}
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'ancestors') {
2020-05-23 23:44:55 +02:00
i += 1;
return new DescendantOfExp(parseNoteProperty());
}
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'labels') {
if (tokens[i + 1].token !== '.') {
parsingContext.addError(`Expected "." to separate field path, got "${tokens[i + 1].token}" in ${context(i)}`);
return;
}
i += 2;
2020-07-19 23:19:45 +02:00
return parseLabel(tokens[i].token);
}
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'relations') {
if (tokens[i + 1].token !== '.') {
parsingContext.addError(`Expected "." to separate field path, got "${tokens[i + 1].token}" in ${context(i)}`);
return;
}
i += 2;
2020-07-19 23:19:45 +02:00
return parseRelation(tokens[i].token);
}
2020-08-19 23:00:51 +02:00
if (tokens[i].token === 'text') {
if (tokens[i + 1].token !== '*=*') {
parsingContext.addError(`Virtual attribute "note.text" supports only *=* operator, instead given "${tokens[i + 1].token}" in ${context(i)}`);
return;
}
i += 2;
return getFulltext([tokens[i]], parsingContext);
}
2020-07-19 23:19:45 +02:00
if (PropertyComparisonExp.isProperty(tokens[i].token)) {
const propertyName = tokens[i].token;
const operator = tokens[i + 1].token;
const comparedValue = tokens[i + 2].token;
const comparator = comparatorBuilder(operator, comparedValue);
if (!comparator) {
parsingContext.addError(`Can't find operator '${operator}' in ${context(i)}`);
return;
}
2020-05-25 00:25:47 +02:00
i += 2;
return new PropertyComparisonExp(propertyName, comparator);
}
2020-05-23 20:52:55 +02:00
parsingContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
}
2020-07-19 15:25:24 +02:00
function parseAttribute(name) {
const isLabel = name.startsWith('#');
name = name.substr(1);
const isNegated = name.startsWith('!');
if (isNegated) {
name = name.substr(1);
}
const subExp = isLabel ? parseLabel(name) : parseRelation(name);
return isNegated ? new NotExp(subExp) : subExp;
}
function parseLabel(labelName) {
parsingContext.highlightedTokens.push(labelName);
2020-07-19 23:19:45 +02:00
if (i < tokens.length - 2 && isOperator(tokens[i + 1].token)) {
let operator = tokens[i + 1].token;
const comparedValue = tokens[i + 2].token;
if (!tokens[i + 2].inQuotes
&& (comparedValue.startsWith('#') || comparedValue.startsWith('~') || comparedValue === 'note')) {
parsingContext.addError(`Error near token "${comparedValue}" in ${context(i)}, it's possible to compare with constant only.`);
return;
}
parsingContext.highlightedTokens.push(comparedValue);
if (parsingContext.fuzzyAttributeSearch && operator === '=') {
operator = '*=*';
}
const comparator = comparatorBuilder(operator, comparedValue);
if (!comparator) {
parsingContext.addError(`Can't find operator '${operator}' in ${context(i)}`);
} else {
i += 2;
return new LabelComparisonExp('label', labelName, comparator);
}
} else {
return new AttributeExistsExp('label', labelName, parsingContext.fuzzyAttributeSearch);
}
}
function parseRelation(relationName) {
parsingContext.highlightedTokens.push(relationName);
2020-07-19 23:19:45 +02:00
if (i < tokens.length - 2 && tokens[i + 1].token === '.') {
i += 1;
return new RelationWhereExp(relationName, parseNoteProperty());
} else {
return new AttributeExistsExp('relation', relationName, parsingContext.fuzzyAttributeSearch);
}
}
2020-05-25 00:25:47 +02:00
function parseOrderByAndLimit() {
const orderDefinitions = [];
let limit;
2020-07-19 23:19:45 +02:00
if (tokens[i].token === 'orderby') {
2020-05-25 00:25:47 +02:00
do {
const propertyPath = [];
let direction = "asc";
do {
i++;
2020-07-19 23:19:45 +02:00
propertyPath.push(tokens[i].token);
2020-05-25 00:25:47 +02:00
i++;
2020-07-19 23:19:45 +02:00
} while (i < tokens.length && tokens[i].token === '.');
2020-05-25 00:25:47 +02:00
2020-07-19 23:19:45 +02:00
if (i < tokens.length && ["asc", "desc"].includes(tokens[i].token)) {
direction = tokens[i].token;
2020-05-25 00:25:47 +02:00
i++;
}
const valueExtractor = new ValueExtractor(propertyPath);
if (valueExtractor.validate()) {
parsingContext.addError(valueExtractor.validate());
}
orderDefinitions.push({
valueExtractor,
direction
});
2020-07-19 23:19:45 +02:00
} while (i < tokens.length && tokens[i].token === ',');
2020-05-25 00:25:47 +02:00
}
2020-07-19 23:19:45 +02:00
if (i < tokens.length && tokens[i].token === 'limit') {
limit = parseInt(tokens[i + 1].token);
2020-05-25 00:25:47 +02:00
}
return new OrderByAndLimitExp(orderDefinitions, limit);
}
function getAggregateExpression() {
if (op === null || op === 'and') {
return AndExp.of(expressions);
}
else if (op === 'or') {
return OrExp.of(expressions);
}
}
for (i = 0; i < tokens.length; i++) {
2020-07-19 23:19:45 +02:00
if (Array.isArray(tokens[i])) {
expressions.push(getExpression(tokens[i], parsingContext, level++));
continue;
}
const token = tokens[i].token;
2020-05-19 00:00:35 +02:00
if (token === '#' || token === '~') {
2020-05-19 00:00:35 +02:00
continue;
}
2020-07-19 23:19:45 +02:00
if (token.startsWith('#') || token.startsWith('~')) {
2020-07-19 15:25:24 +02:00
expressions.push(parseAttribute(token));
2020-05-19 00:00:35 +02:00
}
2020-05-25 00:25:47 +02:00
else if (['orderby', 'limit'].includes(token)) {
if (level !== 0) {
parsingContext.addError('orderBy can appear only on the top expression level');
continue;
}
const exp = parseOrderByAndLimit();
if (!exp) {
continue;
}
exp.subExpression = getAggregateExpression();
return exp;
}
2020-05-27 00:09:19 +02:00
else if (token === 'not') {
i += 1;
if (!Array.isArray(tokens[i])) {
2020-07-19 23:19:45 +02:00
parsingContext.addError(`not keyword should be followed by sub-expression in parenthesis, got ${tokens[i].token} instead`);
2020-05-27 00:09:19 +02:00
continue;
}
expressions.push(new NotExp(getExpression(tokens[i], parsingContext, level++)));
}
else if (token === 'note') {
i++;
expressions.push(parseNoteProperty(tokens));
continue;
}
else if (['and', 'or'].includes(token)) {
2020-05-19 00:00:35 +02:00
if (!op) {
op = token;
2020-05-19 00:00:35 +02:00
}
else if (op !== token) {
parsingContext.addError('Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.');
2020-05-19 00:00:35 +02:00
}
}
else if (isOperator(token)) {
parsingContext.addError(`Misplaced or incomplete expression "${token}"`);
2020-05-19 00:00:35 +02:00
}
else {
parsingContext.addError(`Unrecognized expression "${token}"`);
2020-05-19 00:00:35 +02:00
}
if (!op && expressions.length > 1) {
op = 'and';
}
}
2020-05-20 00:03:33 +02:00
2020-05-25 00:25:47 +02:00
return getAggregateExpression();
2020-05-19 00:00:35 +02:00
}
2020-05-21 11:46:01 +02:00
function parse({fulltextTokens, expressionTokens, parsingContext}) {
2020-05-19 00:00:35 +02:00
return AndExp.of([
2020-05-21 11:46:01 +02:00
getFulltext(fulltextTokens, parsingContext),
getExpression(expressionTokens, parsingContext)
2020-05-19 00:00:35 +02:00
]);
}
2020-05-20 00:03:33 +02:00
module.exports = parse;