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.2 kB
JavaScript
"use strict";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,i,n,a){return{type:t.Condition,key:e,comparator:s,value:r,spacesAfterKey:o,spacesAfterComparator:i,spacesAfterValue:n,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,s,r){return{start:t,end:e,line:s,column:r}}static mergePositions(...t){if(0===t.length)return{start:0,end:0};const e=Math.min(...t.map(t=>t.start)),s=Math.max(...t.map(t=>t.end)),r=t[0]?.line,o=t[0]?.column;return{start:e,end:s,line:r,column:o}}static traverseAST(e,s,r){r(e,s)&&function e(s){switch(s.type){case t.Condition:{const t=s;r(t.key,t),r(t.comparator,t),r(t.value,t);break}case t.Group:e(s.expression);break;case t.Boolean:{const t=s;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",EOF:"EOF"},i=[">","<","=","!"],n={Colon:":",LeftParenthesis:"(",RightParenthesis:")",SingleQuote:"'",DoubleQuote:'"'};class a{input="";position=0;line=1;column=1;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.EOF,"",this.position,this.position)),e}reset(){this.position=0,this.line=1,this.column=1}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(i.includes(e)){return this.scanComparator(t)}if(e===n.Colon){this.advance();return this.createToken(o.Colon,e,t,this.position)}if(e===n.SingleQuote||e===n.DoubleQuote){return this.scanQuotedString(t)}if(e===n.LeftParenthesis){this.advance();return this.createToken(o.LeftParenthesis,e,t,this.position)}if(e===n.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,line:this.line,column:this.column-(r-s)}}}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;for(;!this.isAtEnd();){const i=this.currentChar();if(r){switch(i){case"n":s+="\n";break;case"t":s+="\t";break;case"r":s+="\r";break;case"\\":s+="\\";break;case'"':s+='"';break;case"'":s+="'";break;default:s+=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(){if(this.position<this.input.length){const t=this.input[this.position];this.position++,"\n"===t?(this.line++,this.column=1):this.column++}}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}expect(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.EOF}}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'",m="Expected expression after 'NOT'",k="Expected expression inside parentheses",f="Expected 'AND' or 'OR'",x="Expected closing parenthesis",E="Empty query",S="Empty parentheses not allowed",g="SYNTAX_ERROR",A="UNEXPECTED_TOKEN",T="MISSING_TOKEN",C="UNBALANCED_PARENS",P="EMPTY_EXPRESSION";exports.ASTBuilder=e,exports.AstTypes=t,exports.BooleanOperators=s,exports.Comparators=r,exports.ContextTypes=p,exports.QueryLexer=a,exports.QueryParser=class{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:E,position:t,code:P}),{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=[],s="spacesAfterValue"in r?r.spacesAfterValue:0;t.position.start===r.position.end+s&&this.isPartialLogicalOperator(t.value)&&e.push(p.LogicalOperator),t.context={expectedTokens:e},this.addError({message:f,position:t.position,code:A})}const o=e.createPosition(0,t.length),i=e.createQuery(r,o);return{success:0===this.errors.length,ast:i,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:g}),{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();this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis,p.Not]});const o=this.parseAndExpression();if(!o)return t;const i=e.createOperator(s.OR,r.position),n=e.mergePositions(t.position,o.position);t=e.createBooleanExpression(i,t,o,n)}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(),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:T}),t}const i=this.parseNotExpression();if(!i)return t;const n=e.createOperator(s.AND,r.position),a=e.mergePositions(t.position,i.position);t=e.createBooleanExpression(n,t,i,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(),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:m,position:e,code:T}),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]}),this.tokenStream.isCurrentAMatchWith(o.LeftParenthesis)){return this.parseGroupExpression()}return this.parseCondition()}parseGroupExpression(){const t=this.tokenStream.expect(o.LeftParenthesis);if(this.openParenthesisCount++,this.tokenStream.countAndSkipWhitespaces({expectedTokens:[p.Key,p.LeftParenthesis]}),this.tokenStream.isCurrentAMatchWith(o.RightParenthesis))return this.addError({message:S,position:t.position,code:P}),this.tokenStream.consume(),this.openParenthesisCount--,null;const s=this.parseOrExpression();if(!s)return this.addError({message:k,position:t.position,code:T}),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:C}),s;const i=this.tokenStream.consume();this.openParenthesisCount--;const n=e.mergePositions(t.position,i.position);return e.createGroup(s,n)}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:T}),null;const s=this.tokenStream.consume();s.context={expectedTokens:[p.Key,p.Colon]};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:T}),null}const i=this.tokenStream.consume();i.context={expectedTokens:[p.Comparator]};const n=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||i.position,code:T}),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),m=e.createKey(s.value,s.position),k=e.createComparator(i.value,i.position),f=e.createValue(a.value,a.position),x=e.mergePositions(s.position,a.position);return e.createCondition(m,k,f,r,n,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))}isPartialComparator(t){return[...Object.values(r),"="].some(e=>e.startsWith(t))}},exports.SpecialChars=n,exports.TokenStream=c,exports.TokenTypes=o;
//# sourceMappingURL=index.cjs.cjs.map