UNPKG

foam-framework

Version:
373 lines (297 loc) 11.3 kB
/** * @license * Copyright 2012 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // TODO: move to package /** * Generic Google-like query-language parser generator. * * key:value key contains "value" * key=value key exactly matches "value" * key:value1,value2 key contains "value1" OR "value2" * key:(value1|value2) " * key1:value key2:value key1 contains value AND key2 contains "value" * key1:value AND key2:value " * key1:value and key2:value " * key1:value OR key2:value key1 contains value OR key2 contains "value" * key1:value or key2:value " * key:(-value) key does not contain "value" * (expr) groups expression * -expr not expression, ie. -pri:1 * NOT expr not expression, ie. NOT pri:1 * has:key key has a value * is:key key is a boolean TRUE value * key>value key is greater than value * key-after:value " * key<value key is less than value * key-before:value " * date:YY/MM/DD date specified * date:today date of today * date-after:today-7 date newer than 7 days ago * date:d1..d2 date within range d1 to d2, inclusive * key:me key is the current user * * Date formats: * YYYY * YYYY-MM * YYYY-MM-DD * YYYY-MM-DDTHH * YYYY-MM-DDTHH:MM */ var QueryParserFactory = function(model, opt_enableKeyword) { var g = { __proto__: grammar, START: sym('query'), query: sym('or'), or: repeat(sym('and'), alt(literal_ic(' OR '), literal(' | ')), 1), and: repeat( sym('expr'), alt(literal_ic('AND '), not(alt(literal_ic(' OR'), literal(' |')), ' ')), 1), expr: alt( sym('paren'), sym('negate'), sym('has'), sym('is'), sym('equals'), sym('before'), sym('after'), sym('id'), sym('keyword') ), paren: seq1(1, '(', sym('query'), ')'), negate: alt( seq('-', sym('expr')), seq('NOT ', sym('expr')) ), id: sym('number'), has: seq(literal_ic('has:'), sym('fieldname')), is: seq(literal_ic('is:'), sym('fieldname')), equals: seq(sym('fieldname'), alt(':', '='), sym('valueList')), // TODO: merge with 'equals' before: seq(sym('fieldname'), alt('<=', '<', '-before:'), sym('value')), // TODO: merge with 'equals' after: seq(sym('fieldname'), alt('>=', '>', '-after:'), sym('value')), value: alt( sym('me'), sym('date'), sym('string'), sym('number')), compoundValue: alt( sym('negateValue'), sym('orValue'), sym('andValue')), negateValue: seq('(', alt('-', literal_ic('not ')), sym('value'), ')'), orValue: seq( '(', repeat(sym('value'), alt('|', literal_ic(' or '), ' | '), 1), ')'), andValue: seq( '(', repeat(sym('value'), alt(literal_ic(' and '), ' '), 1), ')'), valueList: alt(sym('compoundValue'), repeat(sym('value'), ',', 1)), keyword: (function() { var keyword_ = sym('keyword_'); return function(ps) { return opt_enableKeyword && this.parse(keyword_, ps); } })(), keyword_: str(plus(notChar(' '))), me: seq(literal_ic('me'), lookahead(not(sym('char')))), date: alt( sym('range date'), sym('literal date'), sym('relative date')), 'range date': seq(sym('literal date'), '..', sym('literal date')), 'literal date': alt( // YYYY-MM-DDTHH:MM seq(sym('number'), '-', sym('number'), '-', sym('number'), 'T', sym('number'), ':', sym('number')), // YYYY-MM-DDTHH seq(sym('number'), '-', sym('number'), '-', sym('number'), 'T', sym('number')), // YYYY-MM-DD seq(sym('number'), '-', sym('number'), '-', sym('number')), // YYYY-MM seq(sym('number'), '-', sym('number')), // YY/MM/DD seq(sym('number'), '/', sym('number'), '/', sym('number'))), 'relative date': seq(literal_ic('today'), optional(seq('-', sym('number')))), string: alt( sym('word'), sym('quoted string')), 'quoted string': str(seq1(1, '"', repeat(alt(literal('\\"', '"'), notChar('"'))), '"')), word: str(plus(sym('char'))), char: alt(range('a','z'), range('A', 'Z'), range('0', '9'), '-', '^', '_', '@', '%', '.'), number: str(plus(range('0', '9'))) }; var fields = []; var properties = model.getRuntimeProperties() for ( var i = 0 ; i < properties.length ; i++ ) { var prop = properties[i]; fields.push(literal_ic(prop.name, prop)); for ( var j = 0 ; j < prop.aliases.length ; j++ ) if ( prop.aliases[j] ) fields.push(literal_ic(prop.aliases[j], prop)); if ( prop.shortName ) fields.push(literal_ic(prop.shortName, prop)); } fields.sort(function(a, b) { var d = a.length - b.length; if ( d !== 0 ) return d; if ( a == b ) return 0; return a < b ? 1 : -1; }); g.fieldname = alt.apply(null, fields); g.maybeConvertYearToDateRange = function(prop, num) { var isDateField = DateProperty.isInstance(prop) || DateTimeProperty.isInstance(prop); var isDateRange = Array.isArray(num) && num[0] instanceof Date; if ( isDateField && ! isDateRange ) { // Convert the number, a single year, into a date. Fortunately, years // are easy to add. var start = new Date(0); // Jan 1 1970 at midnight UTC. var end = new Date(0); start.setUTCFullYear(num); end.setUTCFullYear(+num + 1); return [start, end]; } return num; }; g.addActions({ id: function(v) { return EQ(model.ID, v); }, or: function(v) { return OR.apply(OR, v); }, and: function(v) { return AND.apply(AND, v); }, negate: function(v) { return NOT(v[1]); }, number: function(v) { return parseInt(v); }, me: function() { return this.ME || this.X.ME || ""; }, has: function(v) { return HAS(v[1]); }, is: function(v) { return EQ(v[1], TRUE); }, before: function(v) { // If the property (v[0]) is a DateProperty, and the value (v[2]) is a // single number, expand it into a Date range for that year. v[2] = this.maybeConvertYearToDateRange(v[0], v[2]); // If the value (v[2]) is a Date range, we take the appropriate end. if ( Array.isArray(v[2]) && v[2][0] instanceof Date ) { v[2] = v[1] === '<=' ? v[2][1] : v[2][0]; } return (v[1] === '<=' ? LTE : LT) (v[0], v[2]); }, after: function(v) { // If the property (v[0]) is a DateProperty, and the value (v[2]) is a // single number, expand it into a Date range for that year. v[2] = this.maybeConvertYearToDateRange(v[0], v[2]); // If the value (v[2]) is a Date range, we take the appropriate end. if ( Array.isArray(v[2]) && v[2][0] instanceof Date ) { v[2] = v[1] === '>=' ? v[2][0] : v[2][1]; } return (v[1] === '>=' ? GTE : GT) (v[0], v[2]); }, equals: function(v) { // v[2], the values, is an array, which might have an 'and', 'or', or // 'negated' property on it. The default is 'or'. The partial evaluator // will simplify if these are needlessly complex. var prop = v[0]; var values = v[2]; var isInt = IntProperty.isInstance(prop); var isNum = isInt || FloatProperty.isInstance(prop); var isDateField = DateProperty.isInstance(prop) || DateTimeProperty.isInstance(prop); var isDateRange = Array.isArray(values[0]) && values[0][0] instanceof Date; if ( isDateField || isDateRange ) { if ( ! isDateRange ) { // Convert the number, a single year, into a date. Fortunately, years // are easy to add. var start = new Date(0); // Jan 1 1970 at midnight UTC. var end = new Date(0); start.setUTCFullYear(values[0]); end.setUTCFullYear(+values[0] + 1); values = [[start, end]]; } var q = AND(GTE(prop, values[0][0]), LT(prop, values[0][1])); return q; } var expr; if ( isNum ) { for ( var i = 0 ; i < values.length ; i++ ) values[i] = isInt ? parseInt(values[i]) : parseFloat(values[i]); expr = IN(v[0], values); } else { expr = ( v[1] === '=' ) ? IN_IC(v[0], values) : ContainedInICExpr.create({arg1: compile_(prop), arg2: values}) ; } if ( values.negated ) { return NOT(expr); } else if ( values.and ) { return AndExpr.create({ args: values.map(function(x) { return expr.model_.create({ arg1: expr.arg1, arg2: [x] }); }) }); } else { return expr; } }, keyword: function(v) { return KEYWORD(v); }, negateValue: function(v) { v.negated = true; return v; }, orValue: function(v) { v = v[1]; v.or = true; return v; }, andValue: function(v) { v = v[1]; v.and = true; return v; }, // All dates are actually treated as ranges. These are arrays of Date // objects: [start, end]. The start is inclusive and the end exclusive. // Using these objects, both ranges (date:2014, date:2014-05..2014-06) and // open-ended ranges (date>2014-01-01) can be computed higher up. // Date formats: // YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH, YYYY-MM-DD, YYYY-MM, YY/MM/DD, YYYY 'literal date': function(v) { var start, end, interval; start = new Date(); end = new Date(); var ops = ['FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds']; var defaults = [0, 1, 1, 0, 0, 0]; for (var i = 0; i < ops.length; i++) { var x = i*2 > v.length ? defaults[i] : v[i*2]; // Adjust for months being 0-based. start['setUTC' + ops[i]](x - (i == 1 ? 1 : 0)); end['setUTC' + ops[i]](x - (i == 1 ? 1 : 0)); } // Start and end are currently clones of each other. // Bump the last portion of the date and set it in end. var last = Math.floor(v.length / 2); var op = 'UTC' + ops[last]; end['set' + op](end['get' + op]() + 1); return [start, end]; }, 'relative date': function(v) { var d = new Date(); if ( v[1] ) d.setDate(d.getDate() - v[1][1]); return d; }, 'range date': function(v) { // This gives two dates, and we combined them, the range is from the start // of the first date to the end of the second. return [v[0][0], v[2][1]]; } }); return g; };