| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| import { OptionDataValue, DimensionLoose, Dictionary } from './types'; |
| import { |
| keys, isArray, map, isObject, isString, HashMap, isRegExp, isArrayLike, hasOwn |
| } from 'zrender/src/core/util'; |
| import { throwError, makePrintable } from './log'; |
| import { |
| RawValueParserType, getRawValueParser, |
| RelationalOperator, FilterComparator, createFilterComparator |
| } from '../data/helper/dataValueHelper'; |
| |
| |
| // PENDING: |
| // (1) Support more parser like: `parser: 'trim'`, `parser: 'lowerCase'`, `parser: 'year'`, `parser: 'dayOfWeek'`? |
| // (2) Support piped parser ? |
| // (3) Support callback parser or callback condition? |
| // (4) At present do not support string expression yet but only stuctured expression. |
| |
| |
| /** |
| * The structured expression considered: |
| * (1) Literal simplicity |
| * (2) Sementic displayed clearly |
| * |
| * Sementic supports: |
| * (1) relational expression |
| * (2) logical expression |
| * |
| * For example: |
| * ```js |
| * { |
| * and: [{ |
| * or: [{ |
| * dimension: 'Year', gt: 2012, lt: 2019 |
| * }, { |
| * dimension: 'Year', '>': 2002, '<=': 2009 |
| * }] |
| * }, { |
| * dimension: 'Product', eq: 'Tofu' |
| * }] |
| * } |
| * |
| * { dimension: 'Product', eq: 'Tofu' } |
| * |
| * { |
| * or: [ |
| * { dimension: 'Product', value: 'Tofu' }, |
| * { dimension: 'Product', value: 'Biscuit' } |
| * ] |
| * } |
| * |
| * { |
| * and: [true] |
| * } |
| * ``` |
| * |
| * [PARSER] |
| * In an relation expression object, we can specify some built-in parsers: |
| * ```js |
| * // Trim if string |
| * { |
| * parser: 'trim', |
| * eq: 'Flowers' |
| * } |
| * // Parse as time and enable arithmetic relation comparison. |
| * { |
| * parser: 'time', |
| * lt: '2012-12-12' |
| * } |
| * // Normalize number-like string and make '-' to Null. |
| * { |
| * parser: 'time', |
| * lt: '2012-12-12' |
| * } |
| * // Normalize to number: |
| * // + number-like string (like ' 123 ') can be converted to a number. |
| * // + where null/undefined or other string will be converted to NaN. |
| * { |
| * parser: 'number', |
| * eq: 2011 |
| * } |
| * // RegExp, include the feature in SQL: `like '%xxx%'`. |
| * { |
| * reg: /^asdf$/ |
| * } |
| * { |
| * reg: '^asdf$' // Serializable reg exp, will be `new RegExp(...)` |
| * } |
| * ``` |
| * |
| * |
| * [EMPTY_RULE] |
| * (1) If a relational expression set value as `null`/`undefined` like: |
| * `{ dimension: 'Product', lt: undefined }`, |
| * The result will be `false` rather than `true`. |
| * Consider the case like "filter condition", return all result when null/undefined |
| * is probably not expected and even dangours. |
| * (2) If a relational expression has no operator like: |
| * `{ dimension: 'Product' }`, |
| * An error will be thrown. Because it is probably a mistake. |
| * (3) If a logical expression has no children like |
| * `{ and: undefined }` or `{ and: [] }`, |
| * An error will be thrown. Because it is probably an mistake. |
| * (4) If intending have a condition that always `true` or always `false`, |
| * Use `true` or `flase`. |
| * The entire condition can be `true`/`false`, |
| * or also can be `{ and: [true] }`, `{ or: [false] }` |
| */ |
| |
| |
| // -------------------------------------------------- |
| // --- Relational Expression -------------------------- |
| // -------------------------------------------------- |
| |
| /** |
| * Date string and ordinal string can be accepted. |
| */ |
| interface RelationalExpressionOptionByOp extends Record<RelationalOperator, OptionDataValue> { |
| reg?: RegExp | string; // RegExp |
| }; |
| const RELATIONAL_EXPRESSION_OP_ALIAS_MAP = { |
| value: 'eq', |
| |
| // PENDING: not good for literal semantic? |
| '<': 'lt', |
| '<=': 'lte', |
| '>': 'gt', |
| '>=': 'gte', |
| '=': 'eq', |
| '!=': 'ne', |
| '<>': 'ne' |
| |
| // Might mileading for sake of the different between '==' and '===', |
| // So dont support them. |
| // '==': 'eq', |
| // '===': 'seq', |
| // '!==': 'sne' |
| |
| // PENDING: Whether support some common alias "ge", "le", "neq"? |
| // ge: 'gte', |
| // le: 'lte', |
| // neq: 'ne', |
| } as const; |
| type RelationalExpressionOptionByOpAlias = Record<keyof typeof RELATIONAL_EXPRESSION_OP_ALIAS_MAP, OptionDataValue>; |
| |
| interface RelationalExpressionOption extends |
| RelationalExpressionOptionByOp, RelationalExpressionOptionByOpAlias { |
| dimension?: DimensionLoose; |
| parser?: RawValueParserType; |
| } |
| |
| type RelationalExpressionOpEvaluate = (tarVal: unknown, condVal: unknown) => boolean; |
| |
| |
| class RegExpEvaluator implements FilterComparator { |
| private _condVal: RegExp; |
| |
| constructor(rVal: unknown) { |
| // Support condVal: RegExp | string |
| const condValue = this._condVal = isString(rVal) ? new RegExp(rVal) |
| : isRegExp(rVal) ? rVal as RegExp |
| : null; |
| if (condValue == null) { |
| let errMsg = ''; |
| if (__DEV__) { |
| errMsg = makePrintable('Illegal regexp', rVal, 'in'); |
| } |
| throwError(errMsg); |
| } |
| } |
| |
| evaluate(lVal: unknown): boolean { |
| const type = typeof lVal; |
| return type === 'string' ? this._condVal.test(lVal as string) |
| : type === 'number' ? this._condVal.test(lVal + '') |
| : false; |
| } |
| } |
| |
| |
| |
| |
| // -------------------------------------------------- |
| // --- Logical Expression --------------------------- |
| // -------------------------------------------------- |
| |
| |
| interface LogicalExpressionOption { |
| and?: LogicalExpressionSubOption[]; |
| or?: LogicalExpressionSubOption[]; |
| not?: LogicalExpressionSubOption; |
| } |
| type LogicalExpressionSubOption = |
| LogicalExpressionOption | RelationalExpressionOption | TrueFalseExpressionOption; |
| |
| |
| |
| // ----------------------------------------------------- |
| // --- Conditional Expression -------------------------- |
| // ----------------------------------------------------- |
| |
| |
| export type TrueExpressionOption = true; |
| export type FalseExpressionOption = false; |
| export type TrueFalseExpressionOption = TrueExpressionOption | FalseExpressionOption; |
| |
| export type ConditionalExpressionOption = |
| LogicalExpressionOption |
| | RelationalExpressionOption |
| | TrueFalseExpressionOption; |
| |
| type ValueGetterParam = Dictionary<unknown>; |
| export interface ConditionalExpressionValueGetterParamGetter<VGP extends ValueGetterParam = ValueGetterParam> { |
| (relExpOption: RelationalExpressionOption): VGP |
| } |
| export interface ConditionalExpressionValueGetter<VGP extends ValueGetterParam = ValueGetterParam> { |
| (param: VGP): OptionDataValue |
| } |
| |
| interface ParsedConditionInternal { |
| evaluate(): boolean; |
| } |
| class ConstConditionInternal implements ParsedConditionInternal { |
| value: boolean; |
| evaluate(): boolean { |
| return this.value; |
| } |
| } |
| class AndConditionInternal implements ParsedConditionInternal { |
| children: ParsedConditionInternal[]; |
| evaluate() { |
| const children = this.children; |
| for (let i = 0; i < children.length; i++) { |
| if (!children[i].evaluate()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| class OrConditionInternal implements ParsedConditionInternal { |
| children: ParsedConditionInternal[]; |
| evaluate() { |
| const children = this.children; |
| for (let i = 0; i < children.length; i++) { |
| if (children[i].evaluate()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| class NotConditionInternal implements ParsedConditionInternal { |
| child: ParsedConditionInternal; |
| evaluate() { |
| return !this.child.evaluate(); |
| } |
| } |
| class RelationalConditionInternal implements ParsedConditionInternal { |
| valueGetterParam: ValueGetterParam; |
| valueParser: ReturnType<typeof getRawValueParser>; |
| // If no parser, be null/undefined. |
| getValue: ConditionalExpressionValueGetter; |
| subCondList: FilterComparator[]; |
| |
| evaluate() { |
| const needParse = !!this.valueParser; |
| // Call getValue with no `this`. |
| const getValue = this.getValue; |
| const tarValRaw = getValue(this.valueGetterParam); |
| const tarValParsed = needParse ? this.valueParser(tarValRaw) : null; |
| |
| // Relational cond follow "and" logic internally. |
| for (let i = 0; i < this.subCondList.length; i++) { |
| if (!this.subCondList[i].evaluate(needParse ? tarValParsed : tarValRaw)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| |
| function parseOption( |
| exprOption: ConditionalExpressionOption, |
| getters: ConditionalGetters |
| ): ParsedConditionInternal { |
| if (exprOption === true || exprOption === false) { |
| const cond = new ConstConditionInternal(); |
| cond.value = exprOption as boolean; |
| return cond; |
| } |
| |
| let errMsg = ''; |
| if (!isObjectNotArray(exprOption)) { |
| if (__DEV__) { |
| errMsg = makePrintable( |
| 'Illegal config. Expect a plain object but actually', exprOption |
| ); |
| } |
| throwError(errMsg); |
| } |
| |
| if ((exprOption as LogicalExpressionOption).and) { |
| return parseAndOrOption('and', exprOption as LogicalExpressionOption, getters); |
| } |
| else if ((exprOption as LogicalExpressionOption).or) { |
| return parseAndOrOption('or', exprOption as LogicalExpressionOption, getters); |
| } |
| else if ((exprOption as LogicalExpressionOption).not) { |
| return parseNotOption(exprOption as LogicalExpressionOption, getters); |
| } |
| |
| return parseRelationalOption(exprOption as RelationalExpressionOption, getters); |
| } |
| |
| function parseAndOrOption( |
| op: 'and' | 'or', |
| exprOption: LogicalExpressionOption, |
| getters: ConditionalGetters |
| ): ParsedConditionInternal { |
| const subOptionArr = exprOption[op] as ConditionalExpressionOption[]; |
| let errMsg = ''; |
| if (__DEV__) { |
| errMsg = makePrintable( |
| '"and"/"or" condition should only be `' + op + ': [...]` and must not be empty array.', |
| 'Illegal condition:', exprOption |
| ); |
| } |
| if (!isArray(subOptionArr)) { |
| throwError(errMsg); |
| } |
| if (!(subOptionArr as []).length) { |
| throwError(errMsg); |
| } |
| const cond = op === 'and' ? new AndConditionInternal() : new OrConditionInternal(); |
| cond.children = map(subOptionArr, subOption => parseOption(subOption, getters)); |
| if (!cond.children.length) { |
| throwError(errMsg); |
| } |
| return cond; |
| } |
| |
| function parseNotOption( |
| exprOption: LogicalExpressionOption, |
| getters: ConditionalGetters |
| ): ParsedConditionInternal { |
| const subOption = exprOption.not as ConditionalExpressionOption; |
| let errMsg = ''; |
| if (__DEV__) { |
| errMsg = makePrintable( |
| '"not" condition should only be `not: {}`.', |
| 'Illegal condition:', exprOption |
| ); |
| } |
| if (!isObjectNotArray(subOption)) { |
| throwError(errMsg); |
| } |
| const cond = new NotConditionInternal(); |
| cond.child = parseOption(subOption, getters); |
| if (!cond.child) { |
| throwError(errMsg); |
| } |
| return cond; |
| } |
| |
| function parseRelationalOption( |
| exprOption: RelationalExpressionOption, |
| getters: ConditionalGetters |
| ): ParsedConditionInternal { |
| let errMsg = ''; |
| |
| const valueGetterParam = getters.prepareGetValue(exprOption); |
| |
| const subCondList = [] as RelationalConditionInternal['subCondList']; |
| const exprKeys = keys(exprOption); |
| |
| const parserName = exprOption.parser; |
| const valueParser = parserName ? getRawValueParser(parserName) : null; |
| |
| for (let i = 0; i < exprKeys.length; i++) { |
| const keyRaw = exprKeys[i]; |
| if (keyRaw === 'parser' || getters.valueGetterAttrMap.get(keyRaw)) { |
| continue; |
| } |
| |
| const op: keyof RelationalExpressionOptionByOp = hasOwn(RELATIONAL_EXPRESSION_OP_ALIAS_MAP, keyRaw) |
| ? RELATIONAL_EXPRESSION_OP_ALIAS_MAP[keyRaw as keyof RelationalExpressionOptionByOpAlias] |
| : (keyRaw as keyof RelationalExpressionOptionByOp); |
| const condValueRaw = exprOption[keyRaw]; |
| const condValueParsed = valueParser ? valueParser(condValueRaw) : condValueRaw; |
| const evaluator = createFilterComparator(op, condValueParsed) |
| || (op === 'reg' && new RegExpEvaluator(condValueParsed)); |
| |
| if (!evaluator) { |
| if (__DEV__) { |
| errMsg = makePrintable( |
| 'Illegal relational operation: "' + keyRaw + '" in condition:', exprOption |
| ); |
| } |
| throwError(errMsg); |
| } |
| |
| subCondList.push(evaluator); |
| } |
| |
| if (!subCondList.length) { |
| if (__DEV__) { |
| errMsg = makePrintable( |
| 'Relational condition must have at least one operator.', |
| 'Illegal condition:', exprOption |
| ); |
| } |
| // No relational operator always disabled in case of dangers result. |
| throwError(errMsg); |
| } |
| |
| const cond = new RelationalConditionInternal(); |
| cond.valueGetterParam = valueGetterParam; |
| cond.valueParser = valueParser; |
| cond.getValue = getters.getValue; |
| cond.subCondList = subCondList; |
| |
| return cond; |
| } |
| |
| function isObjectNotArray(val: unknown): boolean { |
| return isObject(val) && !isArrayLike(val); |
| } |
| |
| |
| class ConditionalExpressionParsed { |
| |
| private _cond: ParsedConditionInternal; |
| |
| constructor( |
| exprOption: ConditionalExpressionOption, |
| getters: ConditionalGetters |
| ) { |
| this._cond = parseOption(exprOption, getters); |
| } |
| |
| evaluate(): boolean { |
| return this._cond.evaluate(); |
| } |
| }; |
| |
| interface ConditionalGetters<VGP extends ValueGetterParam = ValueGetterParam> { |
| prepareGetValue: ConditionalExpressionValueGetterParamGetter<VGP>; |
| getValue: ConditionalExpressionValueGetter<VGP>; |
| valueGetterAttrMap: HashMap<boolean, string>; |
| } |
| |
| export function parseConditionalExpression<VGP extends ValueGetterParam = ValueGetterParam>( |
| exprOption: ConditionalExpressionOption, |
| getters: ConditionalGetters<VGP> |
| ): ConditionalExpressionParsed { |
| return new ConditionalExpressionParsed(exprOption, getters); |
| } |
| |