mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
implemented property based access + parent
This commit is contained in:
parent
714881ad99
commit
4ea934509e
@ -18,8 +18,8 @@ describe("Lexer fulltext", () => {
|
||||
});
|
||||
|
||||
it("you can use different quotes and other special characters inside quotes", () => {
|
||||
expect(lexer("'i can use \" or ` or #@=*' without problem").fulltextTokens)
|
||||
.toEqual(["i can use \" or ` or #@=*", "without", "problem"]);
|
||||
expect(lexer("'i can use \" or ` or #~=*' without problem").fulltextTokens)
|
||||
.toEqual(["i can use \" or ` or #~=*", "without", "problem"]);
|
||||
});
|
||||
|
||||
it("if quote is not ended then it's just one long token", () => {
|
||||
@ -33,15 +33,15 @@ describe("Lexer fulltext", () => {
|
||||
});
|
||||
|
||||
it("escaping special characters", () => {
|
||||
expect(lexer("hello \\#\\@\\'").fulltextTokens)
|
||||
.toEqual(["hello", "#@'"]);
|
||||
expect(lexer("hello \\#\\~\\'").fulltextTokens)
|
||||
.toEqual(["hello", "#~'"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Lexer expression", () => {
|
||||
it("simple attribute existence", () => {
|
||||
expect(lexer("#label @relation").expressionTokens)
|
||||
.toEqual(["#label", "@relation"]);
|
||||
expect(lexer("#label ~relation").expressionTokens)
|
||||
.toEqual(["#label", "~relation"]);
|
||||
});
|
||||
|
||||
it("simple label operators", () => {
|
||||
@ -50,12 +50,17 @@ describe("Lexer expression", () => {
|
||||
});
|
||||
|
||||
it("spaces in attribute names and values", () => {
|
||||
expect(lexer(`#'long label'="hello o' world" @'long relation'`).expressionTokens)
|
||||
.toEqual(["#long label", "=", "hello o' world", "@long relation"]);
|
||||
expect(lexer(`#'long label'="hello o' world" ~'long relation'`).expressionTokens)
|
||||
.toEqual(["#long label", "=", "hello o' world", "~long relation"]);
|
||||
});
|
||||
|
||||
it("complex expressions with and, or and parenthesis", () => {
|
||||
expect(lexer(`# (#label=text OR #second=text) AND @relation`).expressionTokens)
|
||||
.toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "@relation"]);
|
||||
expect(lexer(`# (#label=text OR #second=text) AND ~relation`).expressionTokens)
|
||||
.toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "~relation"]);
|
||||
});
|
||||
|
||||
it("dot separated properties", () => {
|
||||
expect(lexer(`# ~author.title = 'Hugh Howey' AND note.title = 'Silo'`).expressionTokens)
|
||||
.toEqual(["#", "~author", ".", "title", "=", "hugh howey", "and", "note", ".", "title", "=", "silo"]);
|
||||
});
|
||||
});
|
||||
|
@ -37,7 +37,7 @@ describe("Parser", () => {
|
||||
parsingContext: new ParsingContext()
|
||||
});
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(rootExp.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(rootExp.attributeType).toEqual("label");
|
||||
expect(rootExp.attributeName).toEqual("mylabel");
|
||||
expect(rootExp.comparator).toBeTruthy();
|
||||
@ -53,10 +53,10 @@ describe("Parser", () => {
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
@ -70,27 +70,27 @@ describe("Parser", () => {
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it("simple label OR", () => {
|
||||
const rootExp = parser({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: ["#first", "=", "text", "OR", "#second", "=", "text"],
|
||||
expressionTokens: ["#first", "=", "text", "or", "#second", "=", "text"],
|
||||
parsingContext: new ParsingContext()
|
||||
});
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("OrExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
@ -107,30 +107,30 @@ describe("Parser", () => {
|
||||
expect(firstSub.constructor.name).toEqual("NoteCacheFulltextExp");
|
||||
expect(firstSub.tokens).toEqual(["hello"]);
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("mylabel");
|
||||
});
|
||||
|
||||
it("label sub-expression", () => {
|
||||
const rootExp = parser({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: ["#first", "=", "text", "OR", ["#second", "=", "text", "AND", "#third", "=", "text"]],
|
||||
expressionTokens: ["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]],
|
||||
parsingContext: new ParsingContext()
|
||||
});
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("OrExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("AndExp");
|
||||
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
||||
|
||||
expect(firstSubSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(firstSubSub.attributeName).toEqual("second");
|
||||
|
||||
expect(secondSubSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
expect(secondSubSub.attributeName).toEqual("third");
|
||||
});
|
||||
});
|
||||
|
@ -83,6 +83,31 @@ describe("Search", () => {
|
||||
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("numeric label comparison fallback to string comparison", async () => {
|
||||
rootNote.child(
|
||||
note("Europe")
|
||||
.label('country', '', true)
|
||||
.child(
|
||||
note("Austria")
|
||||
.label('established', '1955-07-27')
|
||||
)
|
||||
.child(
|
||||
note("Czech Republic")
|
||||
.label('established', '1993-01-01')
|
||||
)
|
||||
.child(
|
||||
note("Hungary")
|
||||
.label('established', '..wrong..')
|
||||
)
|
||||
);
|
||||
|
||||
const parsingContext = new ParsingContext();
|
||||
|
||||
const searchResults = await searchService.findNotesWithQuery('#established < 1990', parsingContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("logical or", async () => {
|
||||
rootNote.child(
|
||||
note("Europe")
|
||||
@ -140,6 +165,51 @@ describe("Search", () => {
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("filter by note property", async () => {
|
||||
rootNote.child(
|
||||
note("Europe")
|
||||
.child(
|
||||
note("Austria")
|
||||
)
|
||||
.child(
|
||||
note("Czech Republic")
|
||||
)
|
||||
);
|
||||
|
||||
const parsingContext = new ParsingContext();
|
||||
|
||||
const searchResults = await searchService.findNotesWithQuery('# note.title =* czech', parsingContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("filter by note's parent", async () => {
|
||||
rootNote.child(
|
||||
note("Europe")
|
||||
.child(
|
||||
note("Austria")
|
||||
)
|
||||
.child(
|
||||
note("Czech Republic")
|
||||
)
|
||||
)
|
||||
.child(
|
||||
note("Asia")
|
||||
.child(note('Taiwan'))
|
||||
);
|
||||
|
||||
const parsingContext = new ParsingContext();
|
||||
|
||||
let searchResults = await searchService.findNotesWithQuery('# note.parent.title = Europe', parsingContext);
|
||||
expect(searchResults.length).toEqual(2);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||
|
||||
searchResults = await searchService.findNotesWithQuery('# note.parent.title = Asia', parsingContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Taiwan")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
/** @return {Note} */
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
const Expression = require('./expression');
|
||||
|
||||
class AndExp extends Expression{
|
||||
class AndExp extends Expression {
|
||||
static of(subExpressions) {
|
||||
subExpressions = subExpressions.filter(exp => !!exp);
|
||||
|
||||
|
36
src/services/search/expressions/child_of.js
Normal file
36
src/services/search/expressions/child_of.js
Normal file
@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
const Expression = require('./expression');
|
||||
const NoteSet = require('../note_set');
|
||||
|
||||
class ChildOfExp extends Expression {
|
||||
constructor(subExpression) {
|
||||
super();
|
||||
|
||||
this.subExpression = subExpression;
|
||||
}
|
||||
|
||||
execute(inputNoteSet, searchContext) {
|
||||
const subInputNoteSet = new NoteSet();
|
||||
|
||||
for (const note of inputNoteSet.notes) {
|
||||
subInputNoteSet.addAll(note.parents);
|
||||
}
|
||||
|
||||
const subResNoteSet = this.subExpression.execute(subInputNoteSet, searchContext);
|
||||
|
||||
const resNoteSet = new NoteSet();
|
||||
|
||||
for (const parentNote of subResNoteSet.notes) {
|
||||
for (const childNote of parentNote.children) {
|
||||
if (inputNoteSet.hasNote(childNote)) {
|
||||
resNoteSet.add(childNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resNoteSet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChildOfExp;
|
@ -4,6 +4,7 @@ class Expression {
|
||||
/**
|
||||
* @param {NoteSet} noteSet
|
||||
* @param {object} searchContext
|
||||
* @return {NoteSet}
|
||||
*/
|
||||
execute(noteSet, searchContext) {}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ const Expression = require('./expression');
|
||||
const NoteSet = require('../note_set');
|
||||
const noteCache = require('../../note_cache/note_cache');
|
||||
|
||||
class FieldComparisonExp extends Expression {
|
||||
class LabelComparisonExp extends Expression {
|
||||
constructor(attributeType, attributeName, comparator) {
|
||||
super();
|
||||
|
||||
@ -37,4 +37,4 @@ class FieldComparisonExp extends Expression {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FieldComparisonExp;
|
||||
module.exports = LabelComparisonExp;
|
29
src/services/search/expressions/property_comparison.js
Normal file
29
src/services/search/expressions/property_comparison.js
Normal file
@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
const Expression = require('./expression');
|
||||
const NoteSet = require('../note_set');
|
||||
|
||||
class PropertyComparisonExp extends Expression {
|
||||
constructor(propertyName, comparator) {
|
||||
super();
|
||||
|
||||
this.propertyName = propertyName;
|
||||
this.comparator = comparator;
|
||||
}
|
||||
|
||||
execute(noteSet, searchContext) {
|
||||
const resNoteSet = new NoteSet();
|
||||
|
||||
for (const note of noteSet.notes) {
|
||||
const value = note[this.propertyName].toLowerCase();
|
||||
|
||||
if (this.comparator(value)) {
|
||||
resNoteSet.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
return resNoteSet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PropertyComparisonExp;
|
@ -77,7 +77,7 @@ function lexer(str) {
|
||||
continue;
|
||||
}
|
||||
else if (!quotes) {
|
||||
if (currentWord.length === 0 && (chr === '#' || chr === '@')) {
|
||||
if (currentWord.length === 0 && (chr === '#' || chr === '~')) {
|
||||
fulltextEnded = true;
|
||||
currentWord = chr;
|
||||
|
||||
@ -87,7 +87,7 @@ function lexer(str) {
|
||||
finishWord();
|
||||
continue;
|
||||
}
|
||||
else if (fulltextEnded && ['(', ')'].includes(chr)) {
|
||||
else if (fulltextEnded && ['(', ')', '.'].includes(chr)) {
|
||||
finishWord();
|
||||
currentWord += chr;
|
||||
finishWord();
|
||||
|
@ -6,11 +6,19 @@ class NoteSet {
|
||||
}
|
||||
|
||||
add(note) {
|
||||
this.notes.push(note);
|
||||
if (!this.hasNote(note)) {
|
||||
this.notes.push(note);
|
||||
}
|
||||
}
|
||||
|
||||
addAll(notes) {
|
||||
this.notes.push(...notes);
|
||||
for (const note of notes) {
|
||||
this.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
hasNote(note) {
|
||||
return this.hasNoteId(note.noteId);
|
||||
}
|
||||
|
||||
hasNoteId(noteId) {
|
||||
|
@ -3,8 +3,10 @@
|
||||
const AndExp = require('./expressions/and');
|
||||
const OrExp = require('./expressions/or');
|
||||
const NotExp = require('./expressions/not');
|
||||
const ChildOfExp = require('./expressions/child_of');
|
||||
const PropertyComparisonExp = require('./expressions/property_comparison');
|
||||
const AttributeExistsExp = require('./expressions/attribute_exists');
|
||||
const FieldComparisonExp = require('./expressions/field_comparison');
|
||||
const LabelComparisonExp = require('./expressions/label_comparison');
|
||||
const NoteCacheFulltextExp = require('./expressions/note_cache_fulltext');
|
||||
const NoteContentFulltextExp = require('./expressions/note_content_fulltext');
|
||||
const comparatorBuilder = require('./comparator_builder');
|
||||
@ -38,17 +40,50 @@ function getExpression(tokens, parsingContext) {
|
||||
const expressions = [];
|
||||
let op = null;
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let i;
|
||||
|
||||
function parseNoteProperty() {
|
||||
if (tokens[i] !== '.') {
|
||||
parsingContext.addError('Expected "." to separate field path');
|
||||
return;
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
if (tokens[i] === 'parent') {
|
||||
i += 1;
|
||||
|
||||
return new ChildOfExp(parseNoteProperty());
|
||||
}
|
||||
|
||||
if (tokens[i] === 'title') {
|
||||
const propertyName = tokens[i];
|
||||
const operator = tokens[i + 1];
|
||||
const comparedValue = tokens[i + 2];
|
||||
const comparator = comparatorBuilder(operator, comparedValue);
|
||||
|
||||
if (!comparator) {
|
||||
parsingContext.addError(`Can't find operator '${operator}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
i += 3;
|
||||
|
||||
return new PropertyComparisonExp(propertyName, comparator);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token === '#' || token === '@') {
|
||||
if (token === '#' || token === '~') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(token)) {
|
||||
expressions.push(getExpression(token, parsingContext));
|
||||
}
|
||||
else if (token.startsWith('#') || token.startsWith('@')) {
|
||||
else if (token.startsWith('#') || token.startsWith('~')) {
|
||||
const type = token.startsWith('#') ? 'label' : 'relation';
|
||||
|
||||
parsingContext.highlightedTokens.push(token.substr(1));
|
||||
@ -70,7 +105,7 @@ function getExpression(tokens, parsingContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
expressions.push(new FieldComparisonExp(type, token.substr(1), comparator));
|
||||
expressions.push(new LabelComparisonExp(type, token.substr(1), comparator));
|
||||
|
||||
i += 2;
|
||||
}
|
||||
@ -78,11 +113,18 @@ function getExpression(tokens, parsingContext) {
|
||||
expressions.push(new AttributeExistsExp(type, token.substr(1), parsingContext.fuzzyAttributeSearch));
|
||||
}
|
||||
}
|
||||
else if (['and', 'or'].includes(token.toLowerCase())) {
|
||||
else if (token === 'note') {
|
||||
i++;
|
||||
|
||||
expressions.push(parseNoteProperty(tokens));
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (['and', 'or'].includes(token)) {
|
||||
if (!op) {
|
||||
op = token.toLowerCase();
|
||||
op = token;
|
||||
}
|
||||
else if (op !== token.toLowerCase()) {
|
||||
else if (op !== token) {
|
||||
parsingContext.addError('Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.');
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user