diff --git a/spec/search/lexer.spec.js b/spec/search/lexer.spec.js index 7b148ce3b..3ca68b5ca 100644 --- a/spec/search/lexer.spec.js +++ b/spec/search/lexer.spec.js @@ -64,6 +64,11 @@ describe("Lexer expression", () => { .toEqual(["#", "~author", ".", "title", "=", "hugh howey", "and", "note", ".", "book title", "=", "silo"]); }); + it("negation of label and relation", () => { + expect(lexer(`#!capital ~!neighbor`).expressionTokens) + .toEqual(["#!capital", "~!neighbor"]); + }); + it("negation of sub-expression", () => { expect(lexer(`# not(#capital) and note.noteId != "root"`).expressionTokens) .toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]); diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js index d1ca62fbc..125bdb2d8 100644 --- a/spec/search/parser.spec.js +++ b/spec/search/parser.spec.js @@ -21,13 +21,16 @@ describe("Parser", () => { }); expect(rootExp.constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions; + const subs = rootExp.subExpressions; - expect(firstSub.constructor.name).toEqual("NoteCacheFulltextExp"); - expect(firstSub.tokens).toEqual(["hello", "hi"]); + expect(subs[0].constructor.name).toEqual("NoteCacheFulltextExp"); + expect(subs[0].tokens).toEqual(["hello", "hi"]); - expect(secondSub.constructor.name).toEqual("NoteContentFulltextExp"); - expect(secondSub.tokens).toEqual(["hello", "hi"]); + expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp"); + expect(subs[1].tokens).toEqual(["hello", "hi"]); + + expect(subs[2].constructor.name).toEqual("NoteContentUnprotectedFulltextExp"); + expect(subs[2].tokens).toEqual(["hello", "hi"]); }); it("simple label comparison", () => { @@ -43,6 +46,30 @@ describe("Parser", () => { expect(rootExp.comparator).toBeTruthy(); }); + it("simple attribute negation", () => { + let rootExp = parser({ + fulltextTokens: [], + expressionTokens: ["#!mylabel"], + parsingContext: new ParsingContext() + }); + + expect(rootExp.constructor.name).toEqual("NotExp"); + expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); + expect(rootExp.subExpression.attributeType).toEqual("label"); + expect(rootExp.subExpression.attributeName).toEqual("mylabel"); + + rootExp = parser({ + fulltextTokens: [], + expressionTokens: ["~!myrelation"], + parsingContext: new ParsingContext() + }); + + expect(rootExp.constructor.name).toEqual("NotExp"); + expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); + expect(rootExp.subExpression.attributeType).toEqual("relation"); + expect(rootExp.subExpression.attributeName).toEqual("myrelation"); + }); + it("simple label AND", () => { const rootExp = parser({ fulltextTokens: [], diff --git a/src/services/search/lexer.js b/src/services/search/lexer.js index b5dc4491b..95ee69c45 100644 --- a/src/services/search/lexer.js +++ b/src/services/search/lexer.js @@ -83,6 +83,10 @@ function lexer(str) { continue; } + else if (['#', '~'].includes(currentWord) && chr === '!') { + currentWord += chr; + continue; + } else if (chr === ' ') { finishWord(); continue; @@ -93,7 +97,10 @@ function lexer(str) { finishWord(); continue; } - else if (fulltextEnded && previousOperatorSymbol() !== isOperatorSymbol(chr)) { + else if (fulltextEnded + && !['#!', '~!'].includes(currentWord) + && previousOperatorSymbol() !== isOperatorSymbol(chr)) { + finishWord(); currentWord += chr; diff --git a/src/services/search/parser.js b/src/services/search/parser.js index c45636594..e9f9cb7a2 100644 --- a/src/services/search/parser.js +++ b/src/services/search/parser.js @@ -70,8 +70,8 @@ function getExpression(tokens, parsingContext, level = 0) { i++; return new OrExp([ - NoteContentUnprotectedFulltextExp(operator, [tokens[i]]), - NoteContentProtectedFulltextExp(operator, [tokens[i]]) + new NoteContentUnprotectedFulltextExp(operator, [tokens[i]]), + new NoteContentProtectedFulltextExp(operator, [tokens[i]]) ]); } @@ -134,6 +134,22 @@ function getExpression(tokens, parsingContext, level = 0) { parsingContext.addError(`Unrecognized note property "${tokens[i]}"`); } + 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); @@ -234,15 +250,8 @@ function getExpression(tokens, parsingContext, level = 0) { if (Array.isArray(token)) { expressions.push(getExpression(token, parsingContext, level++)); } - else if (token.startsWith('#')) { - const labelName = token.substr(1); - - expressions.push(parseLabel(labelName)); - } - else if (token.startsWith('~')) { - const relationName = token.substr(1); - - expressions.push(parseRelation(relationName)); + else if (token.startsWith('#') || token.startsWith('~')) { + expressions.push(parseAttribute(token)); } else if (['orderby', 'limit'].includes(token)) { if (level !== 0) {