UNPKG

create-query-language

Version:

A flexible TypeScript library for parsing and building query languages with support for lexical analysis, AST generation, and token stream processing

3 lines (2 loc) 13.4 kB
const t={Key:"key",Value:"value",Comparator:"comparator",LogicalOperator:"logical-operator",Condition:"condition",Group:"group",Boolean:"boolean",Not:"not",Query:"query"};class e{static createQuery(e,s){return{type:t.Query,expression:e,position:s}}static createBooleanExpression(e,s,r,o){return{type:t.Boolean,operator:e,left:s,right:r,position:o}}static createKey(e,s){return{type:t.Key,value:e,position:s}}static createComparator(e,s){return{type:t.Comparator,value:e,position:s}}static createOperator(e,s){return{type:t.LogicalOperator,value:e,position:s}}static createValue(e,s){return{type:t.Value,value:e,position:s}}static createCondition(e,s,r,o,n,i,a){return{type:t.Condition,key:e,comparator:s,value:r,spacesAfterKey:o,spacesAfterComparator:n,spacesAfterValue:i,position:a}}static createGroup(e,s){return{type:t.Group,expression:e,position:s}}static createNotExpression(e,s){return{type:t.Not,expression:e,position:s}}static createPosition(t,e){return{start:t,end:e}}static mergePositions(...t){if(0===t.length)return{start:0,end:0};return{start:Math.min(...t.map(t=>t.start)),end:Math.max(...t.map(t=>t.end))}}static traverseAST(e,s,r){r(e,s)&&function e(o){switch(o.type){case t.Query:e(o.expression);break;case t.Condition:r(o,s);break;case t.Group:case t.Not:e(o.expression);break;case t.Boolean:{const t=o;e(t.left),e(t.right);break}}}(e)}}const s={AND:"AND",OR:"OR",NOT:"NOT"},r={">":">","<":"<",">=":">=","<=":"<=","!=":"!=","==":"=="},o={Whitespace:"WHITESPACE",LeftParenthesis:"LEFT_PARENTHESIS",RightParenthesis:"RIGHT_PARENTHESIS",AND:"AND",OR:"OR",NOT:"NOT",Comparator:"COMPARATOR",Colon:"COLON",QuotedString:"QUOTED_STRING",Identifier:"IDENTIFIER",Invalid:"INVALID",EndOfLine:"END_OF_LINE"},n=[">","<","=","!"],i={Colon:":",LeftParenthesis:"(",RightParenthesis:")",SingleQuote:"'",DoubleQuote:'"'};class a{input="";position=0;options;constructor(t={}){this.options=t}tokenize(t){this.reset(),this.input=t;const e=[];for(;!this.isAtEnd();){const t=this.nextToken();t&&e.push(t)}return e.push(this.createToken(o.EndOfLine,"",this.position,this.position)),this.linkTokensAsLinkedList(e),e}reset(){this.position=0}linkTokensAsLinkedList(t){for(let e=0;e<t.length;e++){const s=t[e],r=e>0?t[e-1]:null,o=e<t.length-1?t[e+1]:null;s.prev=r,s.next=o}}isAtEnd(){return this.position>=this.input.length}nextToken(){if(this.isAtEnd())return null;const t=this.position,e=this.currentChar();if(this.getIsWhitespace(e)){return this.scanWhitespace(t)}if(n.includes(e)){return this.scanComparator(t)}if(e===i.Colon){this.advance();return this.createToken(o.Colon,e,t,this.position)}if(e===i.SingleQuote||e===i.DoubleQuote){return this.scanQuotedString(t)}if(e===i.LeftParenthesis){this.advance();return this.createToken(o.LeftParenthesis,e,t,this.position)}if(e===i.RightParenthesis){this.advance();return this.createToken(o.RightParenthesis,e,t,this.position)}if(this.isIdentifierStart(e)){return this.scanWord(t)}this.advance();return this.createToken(o.Invalid,e,t,this.position)}currentChar(){return this.input[this.position]||""}createToken(t,e,s,r){return{type:t,value:e,position:{start:s,end:r},prev:null,next:null}}scanWhitespace(t){for(;!this.isAtEnd()&&this.getIsWhitespace(this.currentChar());)this.advance();const e=this.input.slice(t,this.position);return this.createToken(o.Whitespace,e,t,this.position)}scanQuotedString(t){const e=this.currentChar();this.advance();let s="",r=!1;const n={n:"\n",t:"\t",r:"\r","\\":"\\",'"':'"',"'":"'"};for(;!this.isAtEnd();){const i=this.currentChar();if(r)s+=n[i]??i,r=!1;else if("\\"===i)r=!0;else{if(i===e){this.advance();return this.createToken(o.QuotedString,s,t,this.position)}s+=i}this.advance()}const i=this.input.slice(t,this.position);return this.createToken(o.Invalid,i,t,this.position)}scanComparator(t){const e=this.currentChar();this.advance();const s=this.currentChar();if("="===s){this.advance();const r=`${e}${s}`;return this.createToken(o.Comparator,r,t,this.position)}let r=o.Comparator;"!"!==e&&"="!==e||(r=o.Invalid);return this.createToken(r,e,t,this.position)}scanWord(t){for(;!this.isAtEnd()&&this.isPartOfIdentifier(this.currentChar());)this.advance();const e=this.input.slice(t,this.position),r=this.options.caseSensitiveOperators?e:e.toUpperCase();if(s[r]){return this.scanLogicalOperator(t,r,e)}return this.createToken(o.Identifier,e,t,this.position)}scanLogicalOperator(t,e,s){let r;r=e===o.AND?o.AND:e===o.OR?o.OR:e===o.NOT?o.NOT:o.Identifier;return this.createToken(r,s,t,this.position)}advance(){this.position<this.input.length&&this.position++}getIsWhitespace(t){return/^\s+$/.test(t)}isIdentifierStart(t){return/[a-zA-Z0-9_]/.test(t)}isPartOfIdentifier(t){return/[a-zA-Z0-9_]/.test(t)}static getTokenAtPosition(t,e){for(const s of t)if(e>s.position.start&&e<=s.position.end)return s;return null}}class c{tokens;position=0;constructor(t){this.tokens=t}current(){return this.tokens[this.position]||null}consume(){const t=this.current();return t&&this.position++,t}tryGetCurrent(t){const e=this.current();if(!e)throw new Error(`Expected ${t} but reached end of input`);if(e.type!==t)throw new Error(`Expected ${t} but got ${e.type} at position ${e.position.start}`);return this.consume()}isCurrentAMatchWith(t){const e=this.current();return!!e&&e.type===t}matchAny(...t){const e=this.current();if(!e)return!1;return t.includes(e.type)}countAndSkipWhitespaces(t){let e=0;for(;this.current()?.type===o.Whitespace;){const s=this.current();s.context=t,e+=s.value.length,this.advance()}return e}advance(){this.position<this.tokens.length&&this.position++}isAtEnd(){const t=this.current();return!t||t.type===o.EndOfLine}}const p={Key:"key",Value:"value",QuotedString:"quoted-string",LogicalOperator:"operator",Comparator:"comparator",Colon:"colon",LeftParenthesis:"left-parenthesis",RightParenthesis:"right-parenthesis",Not:"not"},h="Expected key name",u="Expected comparator (i.e. :,<,>,=) after key",d="Expected value after comparator",l="Expected expression after 'AND'",k="Expected expression after 'NOT'",m="Expected expression inside parentheses",f="Expected 'AND' or 'OR'",x="Expected closing parenthesis",g="Empty query",E="Empty parentheses not allowed",S="SYNTAX_ERROR",T="UNEXPECTED_TOKEN",A="MISSING_TOKEN",P="UNBALANCED_PARENS",C="EMPTY_EXPRESSION";class O{tokenStream=new c([]);errors=[];openParenthesisCount=0;options;queryLexer;constructor(t={}){this.options={maxErrors:10,...t},this.queryLexer=new a}parse(t){try{this.reset();const s=this.queryLexer.tokenize(t);if(this.tokenStream=new c(s),this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]}),this.tokenStream.isAtEnd()){const t=e.createPosition(0,0);return this.addError({message:g,position:t,code:C}),{success:!1,errors:this.errors,tokens:s}}const r=this.parseOrExpression();if(!r)return{success:!1,errors:this.errors,tokens:s};if(this.tokenStream.countAndSkipWhitespaces({expectedTokens:[]}),!this.tokenStream.isAtEnd()){const t=this.tokenStream.current(),e=[];this.isPartialLogicalOperator(t.value)&&e.push(p.LogicalOperator),0===this.errors.length&&this.addError({message:f,position:t.position,code:T}),t.context={expectedTokens:e}}const o=e.createPosition(0,t.length),n=e.createQuery(r,o);return{success:0===this.errors.length,ast:n,errors:this.errors,tokens:s}}catch(s){const r=e.createPosition(0,t.length);return this.addError({message:s instanceof Error?s.message:"Unknown parsing error",position:r,code:S}),{success:!1,errors:this.errors,tokens:[]}}}reset(){this.errors=[],this.openParenthesisCount=0}parseOrExpression(){let t=this.parseAndExpression();if(!t)return null;for(;this.matchLogicalOperatorOR();){const r=this.tokenStream.consume();r.context={expectedTokens:[p.LogicalOperator]},this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]});const o=this.parseAndExpression();if(!o)return t;const n=e.createOperator(s.OR,r.position),i=e.mergePositions(t.position,o.position);t=e.createBooleanExpression(n,t,o,i)}return t}parseAndExpression(){let t=this.parseNotExpression();if(!t)return null;const r={expectedTokens:[p.LogicalOperator]};for(this.openParenthesisCount>0&&r.expectedTokens.push(p.RightParenthesis),this.tokenStream.countAndSkipWhitespaces(r);this.matchLogicalOperatorAND();){const r=this.tokenStream.consume();r.context={expectedTokens:[p.LogicalOperator]};const o=this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]});if(this.tokenStream.isAtEnd()){const e=this.getPositionAfterToken(r);return e.end+=o,this.addError({message:l,position:e,code:A}),t}const n=this.parseNotExpression();if(!n)return t;const i=e.createOperator(s.AND,r.position),a=e.mergePositions(t.position,n.position);t=e.createBooleanExpression(i,t,n,a);const c={expectedTokens:[p.LogicalOperator]};this.openParenthesisCount>0&&c.expectedTokens.push(p.RightParenthesis),this.tokenStream.countAndSkipWhitespaces(c)}return t}parseNotExpression(){if(this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]}),this.matchLogicalOperatorNOT()){const t=this.tokenStream.consume();t.context={expectedTokens:[p.Not]};const s=this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis]});if(this.tokenStream.isAtEnd()){const e=this.getPositionAfterToken(t);return e.end+=s,this.addError({message:k,position:e,code:A}),null}const r=this.parsePrimaryExpression();if(!r)return null;const o=e.mergePositions(t.position,r.position);return e.createNotExpression(r,o)}return this.parsePrimaryExpression()}parsePrimaryExpression(){if(this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]}),this.tokenStream.isCurrentAMatchWith(o.LeftParenthesis)){return this.parseGroupExpression()}return this.parseCondition()}parseGroupExpression(){const t=this.tokenStream.tryGetCurrent(o.LeftParenthesis);if(t.context={expectedTokens:[p.Key,p.LeftParenthesis,p.Not]},this.openParenthesisCount++,this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]}),this.tokenStream.isCurrentAMatchWith(o.RightParenthesis))return this.addError({message:E,position:t.position,code:C}),this.tokenStream.consume(),this.openParenthesisCount--,null;const s=this.parseOrExpression();if(!s)return this.addError({message:m,position:t.position,code:A}),null;const r={expectedTokens:[p.Comparator]};if(this.openParenthesisCount>1&&r.expectedTokens.push(p.RightParenthesis),this.tokenStream.countAndSkipWhitespaces(r),!this.tokenStream.isCurrentAMatchWith(o.RightParenthesis))return this.addError({message:x,position:s.position,code:P}),s;const n=this.tokenStream.consume();n.context={expectedTokens:[p.RightParenthesis]},this.openParenthesisCount--;const i=e.mergePositions(t.position,n.position);return e.createGroup(s,i)}parseCondition(){const t=this.tokenStream.current();if(!(this.tokenStream.isCurrentAMatchWith(o.Identifier)&&!/^\d/.test(t.value)||this.tokenStream.isCurrentAMatchWith(o.QuotedString)))return this.addError({message:h,position:t?.position||e.createPosition(0,0),code:A}),null;const s=this.tokenStream.consume();s.context={expectedTokens:[p.Key]},this.isPartialNotOperator(s.value)&&s.context.expectedTokens.push(p.Not);const r=this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Comparator,p.Colon]});if(!this.tokenStream.matchAny(o.Colon,o.Comparator)){const t=this.tokenStream.current(),e=[];return this.isPartialComparator(t.value)&&e.push(p.Comparator),t.context={expectedTokens:e},this.addError({message:u,position:t?.position||s.position,code:A}),null}const n=this.tokenStream.consume();n.context={expectedTokens:[p.Comparator,p.Colon]};const i=this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Value],key:s.value});if(!this.tokenStream.matchAny(o.Identifier,o.QuotedString)){const t=this.tokenStream.current();return this.addError({message:d,position:t?.position||n.position,code:A}),null}const a=this.tokenStream.consume();a.context={expectedTokens:[p.Value],key:s.value};const c={expectedTokens:[p.LogicalOperator]};this.openParenthesisCount>0&&c.expectedTokens.push(p.RightParenthesis);const l=this.tokenStream.countAndSkipWhitespaces(c),k=e.createKey(s.value,s.position),m=e.createComparator(n.value,n.position),f=e.createValue(a.value,a.position),x=e.mergePositions(s.position,a.position);return e.createCondition(k,m,f,r,i,l,x)}matchLogicalOperatorAND(){const t=this.tokenStream.current();if(!t)return!1;return t.type===o.AND}matchLogicalOperatorOR(){const t=this.tokenStream.current();if(!t)return!1;return t.type===o.OR}matchLogicalOperatorNOT(){const t=this.tokenStream.current();if(!t)return!1;return t.type===o.NOT}addError(t){const{code:e,message:s,position:r}=t,o={message:s,position:{start:r.start,end:r.end},recoverable:!0};if(o.code=e,this.errors.push(o),this.errors.length>=this.options.maxErrors)throw new Error(`Too many parse errors (${this.options.maxErrors})`)}getPositionAfterToken(t){return{start:t.position.end,end:t.position.end}}isPartialLogicalOperator(t){const e=t.toLowerCase();return[o.AND,o.OR,o.NOT].some(t=>t.toLowerCase().startsWith(e))}isPartialNotOperator(t){const e=t.toLowerCase();return o.NOT.toLowerCase().startsWith(e)}isPartialComparator(t){return Object.values(r).some(e=>e.startsWith(t))}}export{e as ASTUtils,t as AstTypes,r as Comparators,p as ContextTypes,s as LogicalOperators,a as QueryLexer,O as QueryParser,i as SpecialChars,c as TokenStream,o as TokenTypes}; //# sourceMappingURL=index.esm.mjs.map