Spaces:
Running
Running
| /* | |
| * 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, isNumber | |
| } 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 structured expression. | |
| /** | |
| * The structured expression considered: | |
| * (1) Literal simplicity | |
| * (2) Semantic displayed clearly | |
| * | |
| * Semantic 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 be misleading for sake of the difference between '==' and '===', | |
| // so don't 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 isString(type) ? this._condVal.test(lVal as string) | |
| : isNumber(type) ? 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); | |
| } | |