diff --git a/package.json b/package.json index 9f1d75e66..3fa7e6b57 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "chartist-plugin-legend": "^0.6.1", "chartist-plugin-tooltip": "0.0.11", "classnames": "^2.2.3", + "complex-matcher": "^0.1.0", "cookies-js": "^1.2.2", "d3": "^4.12.0", "dependency-check": "^2.9.2", diff --git a/src/common/complex-matcher/index.bench.js b/src/common/complex-matcher/index.bench.js deleted file mode 100644 index 44dc983b5..000000000 --- a/src/common/complex-matcher/index.bench.js +++ /dev/null @@ -1,12 +0,0 @@ -import { parse, toString } from './' -import { ast, pattern } from './index.fixtures' - -export default ({ benchmark }) => { - benchmark('parse', () => { - parse(pattern) - }) - - benchmark('toString', () => { - ;ast::toString() - }) -} diff --git a/src/common/complex-matcher/index.fixtures.js b/src/common/complex-matcher/index.fixtures.js deleted file mode 100644 index 0d8e57594..000000000 --- a/src/common/complex-matcher/index.fixtures.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - createAnd, - createOr, - createNot, - createProperty, - createString, - createTruthyProperty, -} from './' - -export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape?' - -export const ast = createAnd([ - createString('foo'), - createNot(createString('\\ "')), - createProperty( - 'name', - createOr([createString('wonderwoman'), createString('batman')]) - ), - createTruthyProperty('hasCape'), -]) diff --git a/src/common/complex-matcher/index.js b/src/common/complex-matcher/index.js deleted file mode 100644 index 3f108e68b..000000000 --- a/src/common/complex-matcher/index.js +++ /dev/null @@ -1,400 +0,0 @@ -import every from 'lodash/every' -import filter from 'lodash/filter' -import forEach from 'lodash/forEach' -import isArray from 'lodash/isArray' -import isPlainObject from 'lodash/isPlainObject' -import isString from 'lodash/isString' -import map from 'lodash/map' -import some from 'lodash/some' - -import filterReduce from '../filter-reduce' -import invoke from '../invoke' - -// =================================================================== - -const RAW_STRING_CHARS = invoke(() => { - const chars = { __proto__: null } - const add = (a, b = a) => { - let i = a.charCodeAt(0) - const j = b.charCodeAt(0) - while (i <= j) { - chars[String.fromCharCode(i++)] = true - } - } - add('$') - add('-') - add('.') - add('0', '9') - add('_') - add('A', 'Z') - add('a', 'z') - return chars -}) -const isRawString = string => { - const { length } = string - for (let i = 0; i < length; ++i) { - if (!RAW_STRING_CHARS[string[i]]) { - return false - } - } - return true -} - -// ------------------------------------------------------------------- - -export const createAnd = children => - children.length === 1 ? children[0] : { type: 'and', children } - -export const createOr = children => - children.length === 1 ? children[0] : { type: 'or', children } - -export const createNot = child => ({ type: 'not', child }) - -export const createProperty = (name, child) => ({ - type: 'property', - name, - child, -}) - -export const createString = value => ({ type: 'string', value }) - -export const createTruthyProperty = name => ({ type: 'truthyProperty', name }) - -// ------------------------------------------------------------------- - -// *and = terms -// terms = term+ -// term = ws (groupedAnd | or | not | property | truthyProperty | string) ws -// ws = ' '* -// groupedAnd = "(" and ")" -// *or = "|" ws "(" terms ")" -// *not = "!" term -// *property = string ws ":" term -// *truthyProperty = string ws "?" -// *string = quotedString | rawString -// quotedString = "\"" ( /[^"\]/ | "\\\\" | "\\\"" )+ -// rawString = /[a-z0-9-_.]+/i -export const parse = invoke(() => { - let i - let n - let input - - // ----- - - const backtrace = parser => () => { - const pos = i - const node = parser() - if (node != null) { - return node - } - i = pos - } - - // ----- - - const parseAnd = () => parseTerms(createAnd) - const parseTerms = fn => { - let term = parseTerm() - if (!term) { - return - } - - const terms = [term] - while ((term = parseTerm())) { - terms.push(term) - } - return fn(terms) - } - const parseTerm = () => { - parseWs() - - const child = - parseGroupedAnd() || - parseOr() || - parseNot() || - parseProperty() || - parseTruthyProperty() || - parseString() - if (child) { - parseWs() - return child - } - } - const parseWs = () => { - while (input[i] === ' ') { - ++i - } - - return true - } - const parseGroupedAnd = backtrace(() => { - let and - if (input[i++] === '(' && (and = parseAnd()) && input[i++] === ')') { - return and - } - }) - const parseOr = backtrace(() => { - let or - if ( - input[i++] === '|' && - parseWs() && - input[i++] === '(' && - (or = parseTerms(createOr)) && - input[i++] === ')' - ) { - return or - } - }) - const parseNot = backtrace(() => { - let child - if (input[i++] === '!' && (child = parseTerm())) { - return createNot(child) - } - }) - const parseProperty = backtrace(() => { - let name, child - if ( - (name = parseString()) && - parseWs() && - input[i++] === ':' && - (child = parseTerm()) - ) { - return createProperty(name.value, child) - } - }) - const parseString = () => { - let value - if ( - (value = parseQuotedString()) != null || - (value = parseRawString()) != null - ) { - return createString(value) - } - } - const parseQuotedString = backtrace(() => { - if (input[i++] !== '"') { - return - } - - const value = [] - let char - while (i < n && (char = input[i++]) !== '"') { - if (char === '\\') { - char = input[i++] - } - value.push(char) - } - - return value.join('') - }) - const parseRawString = () => { - let value = '' - let c - while ((c = input[i]) && RAW_STRING_CHARS[c]) { - ++i - value += c - } - if (value.length) { - return value - } - } - const parseTruthyProperty = backtrace(() => { - let name - if ((name = parseString()) && parseWs() && input[i++] === '?') { - return createTruthyProperty(name.value) - } - }) - - return input_ => { - if (!input_) { - return - } - - i = 0 - input = input_.split('') - n = input.length - - try { - return parseAnd() - } finally { - input = null - } - } -}) - -// ------------------------------------------------------------------- - -const _getPropertyClauseStrings = ({ child }) => { - const { type } = child - - if (type === 'or') { - const strings = [] - forEach(child.children, child => { - if (child.type === 'string') { - strings.push(child.value) - } - }) - return strings - } - - if (type === 'string') { - return [child.value] - } - - return [] -} - -// Find possible values for property clauses in a and clause. -export const getPropertyClausesStrings = function () { - if (!this) { - return {} - } - - const { type } = this - - if (type === 'property') { - return { - [this.name]: _getPropertyClauseStrings(this), - } - } - - if (type === 'and') { - const strings = {} - forEach(this.children, node => { - if (node.type === 'property') { - const { name } = node - const values = strings[name] - if (values) { - values.push.apply(values, _getPropertyClauseStrings(node)) - } else { - strings[name] = _getPropertyClauseStrings(node) - } - } - }) - return strings - } - - return {} -} - -// ------------------------------------------------------------------- - -export const removePropertyClause = function (name) { - let type - if (!this || ((type = this.type) === 'property' && this.name === name)) { - return - } - - if (type === 'and') { - return createAnd( - filter( - this.children, - node => node.type !== 'property' || node.name !== name - ) - ) - } - - return this -} - -// ------------------------------------------------------------------- - -const _addAndClause = (node, child, predicate, reducer) => - createAnd( - filterReduce( - node.type === 'and' ? node.children : [node], - predicate, - reducer, - child - ) - ) - -export const setPropertyClause = function (name, child) { - const property = createProperty( - name, - isString(child) ? createString(child) : child - ) - - if (!this) { - return property - } - - return _addAndClause( - this, - property, - node => node.type === 'property' && node.name === name - ) -} - -// ------------------------------------------------------------------- - -export const execute = invoke(() => { - const visitors = { - and: ({ children }, value) => - every(children, child => child::execute(value)), - not: ({ child }, value) => !child::execute(value), - or: ({ children }, value) => some(children, child => child::execute(value)), - property: ({ name, child }, value) => - value != null && child::execute(value[name]), - truthyProperty: ({ name }, value) => !!value[name], - string: invoke(() => { - const match = (pattern, value) => { - if (isString(value)) { - return value.toLowerCase().indexOf(pattern) !== -1 - } - - if (isArray(value) || isPlainObject(value)) { - return some(value, value => match(pattern, value)) - } - - return false - } - - return ({ value: pattern }, value) => match(pattern.toLowerCase(), value) - }), - } - - return function (value) { - return visitors[this.type](this, value) - } -}) - -// ------------------------------------------------------------------- - -export const toString = invoke(() => { - const toStringTerms = terms => map(terms, toString).join(' ') - const toStringGroup = terms => `(${toStringTerms(terms)})` - - const visitors = { - and: ({ children }) => toStringGroup(children), - not: ({ child }) => `!${toString(child)}`, - or: ({ children }) => `|${toStringGroup(children)}`, - property: ({ name, child }) => - `${toString(createString(name))}:${toString(child)}`, - string: ({ value }) => - isRawString(value) - ? value - : `"${value.replace(/\\|"/g, match => `\\${match}`)}"`, - truthyProperty: ({ name }) => `${toString(createString(name))}?`, - } - - const toString = node => visitors[node.type](node) - - // Special case for a root “and”: do not add braces. - return function () { - return !this - ? '' - : this.type === 'and' ? toStringTerms(this.children) : toString(this) - } -}) - -// ------------------------------------------------------------------- - -export const create = pattern => { - pattern = parse(pattern) - if (!pattern) { - return - } - - return value => pattern::execute(value) -} diff --git a/src/common/complex-matcher/index.spec.js b/src/common/complex-matcher/index.spec.js deleted file mode 100644 index 1c3fea83f..000000000 --- a/src/common/complex-matcher/index.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-env jest */ - -import { - getPropertyClausesStrings, - parse, - setPropertyClause, - toString, -} from './' -import { ast, pattern } from './index.fixtures' - -it('getPropertyClausesStrings', () => { - const tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings() - expect(tmp).toEqual({ - bar: ['baz'], - baz: ['foo', 'bar'], - }) -}) - -it('parse', () => { - expect(parse(pattern)).toEqual(ast) -}) - -it('setPropertyClause', () => { - expect(null::setPropertyClause('foo', 'bar')::toString()).toBe('foo:bar') - - expect( - parse('baz') - ::setPropertyClause('foo', 'bar') - ::toString() - ).toBe('baz foo:bar') - - expect( - parse('plip foo:baz plop') - ::setPropertyClause('foo', 'bar') - ::toString() - ).toBe('plip plop foo:bar') - - expect( - parse('foo:|(baz plop)') - ::setPropertyClause('foo', 'bar') - ::toString() - ).toBe('foo:bar') -}) - -it('toString', () => { - expect(pattern).toBe(ast::toString()) -}) diff --git a/src/common/home-tags.js b/src/common/home-tags.js index cb538bc06..3048b7c04 100644 --- a/src/common/home-tags.js +++ b/src/common/home-tags.js @@ -1,9 +1,9 @@ +import * as CM from 'complex-matcher' import React from 'react' import Component from './base-component' import propTypes from './prop-types-decorator' import Tags from './tags' -import { createString, createProperty, toString } from './complex-matcher' @propTypes({ labels: propTypes.arrayOf(React.PropTypes.string).isRequired, @@ -19,7 +19,7 @@ export default class HomeTags extends Component { _onClick = label => { const s = encodeURIComponent( - createProperty('tags', createString(label))::toString() + new CM.Property('tags', new CM.String(label)).toString() ) const t = encodeURIComponent(this.props.type) diff --git a/src/common/sorted-table/index.js b/src/common/sorted-table/index.js index edcde06b8..669f78140 100644 --- a/src/common/sorted-table/index.js +++ b/src/common/sorted-table/index.js @@ -1,3 +1,4 @@ +import * as CM from 'complex-matcher' import _ from 'intl' import classNames from 'classnames' import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom @@ -29,7 +30,6 @@ import SingleLineRow from '../single-line-row' import Tooltip from '../tooltip' import { BlockLink } from '../link' import { Container, Col } from '../grid' -import { create as createMatcher } from '../complex-matcher' import { Input as DebouncedInput } from '../debounce-component-decorator' import { createCounter, @@ -326,6 +326,7 @@ export default class SortedTable extends Component { this._getTotalNumberOfItems = createCounter(() => this.props.collection) + const createMatcher = str => CM.parse(str).createPredicate() this._getItems = createSort( createFilter( () => this.props.collection, diff --git a/src/xo-app/home/index.js b/src/xo-app/home/index.js index fe16f7aa0..6529807de 100644 --- a/src/xo-app/home/index.js +++ b/src/xo-app/home/index.js @@ -506,7 +506,7 @@ export default class Home extends Component { } const parsed = ComplexMatcher.parse(filter) - const properties = parsed::ComplexMatcher.getPropertyClausesStrings() + const properties = ComplexMatcher.getPropertyClausesStrings(parsed) const sort = this._getDefaultSort(props) @@ -537,14 +537,14 @@ export default class Home extends Component { _getFilterFunction = createSelector( this._getParsedFilter, - filter => filter && (value => filter::ComplexMatcher.execute(value)) + filter => filter !== undefined && filter.createPredicate() ) // Optionally can take the props to be able to use it in // componentWillReceiveProps(). _setFilter (filter, props = this.props, replace) { if (!isString(filter)) { - filter = filter::ComplexMatcher.toString() + filter = filter.toString() } const { pathname, query } = props.location @@ -600,13 +600,14 @@ export default class Home extends Component { this._setFilter( pools.length - ? filter::ComplexMatcher.setPropertyClause( + ? ComplexMatcher.setPropertyClause( + filter, '$pool', - ComplexMatcher.createOr( - map(pools, pool => ComplexMatcher.createString(pool.id)) + new ComplexMatcher.Or( + map(pools, pool => new ComplexMatcher.String(pool.id)) ) ) - : filter::ComplexMatcher.removePropertyClause('$pool') + : ComplexMatcher.setPropertyClause(filter, '$pool', undefined) ) } _updateSelectedHosts = hosts => { @@ -614,13 +615,14 @@ export default class Home extends Component { this._setFilter( hosts.length - ? filter::ComplexMatcher.setPropertyClause( + ? ComplexMatcher.setPropertyClause( + filter, '$container', - ComplexMatcher.createOr( - map(hosts, host => ComplexMatcher.createString(host.id)) + new ComplexMatcher.Or( + map(hosts, host => new ComplexMatcher.String(host.id)) ) ) - : filter::ComplexMatcher.removePropertyClause('$container') + : ComplexMatcher.setPropertyClause(filter, '$container', undefined) ) } _updateSelectedTags = tags => { @@ -628,13 +630,14 @@ export default class Home extends Component { this._setFilter( tags.length - ? filter::ComplexMatcher.setPropertyClause( + ? ComplexMatcher.setPropertyClause( + filter, 'tags', - ComplexMatcher.createOr( - map(tags, tag => ComplexMatcher.createString(tag.id)) + new ComplexMatcher.Or( + map(tags, tag => new ComplexMatcher.String(tag.id)) ) ) - : filter::ComplexMatcher.removePropertyClause('tags') + : ComplexMatcher.setPropertyClause(filter, 'tags', undefined) ) } _updateSelectedResourceSets = resourceSets => { @@ -642,13 +645,14 @@ export default class Home extends Component { this._setFilter( resourceSets.length - ? filter::ComplexMatcher.setPropertyClause( + ? ComplexMatcher.setPropertyClause( + filter, 'resourceSet', - ComplexMatcher.createOr( - map(resourceSets, set => ComplexMatcher.createString(set.id)) + new ComplexMatcher.Or( + map(resourceSets, set => new ComplexMatcher.String(set.id)) ) ) - : filter::ComplexMatcher.removePropertyClause('resourceSet') + : ComplexMatcher.setPropertyClause(filter, 'resourceSet', undefined) ) } _addCustomFilter = () => { diff --git a/src/xo-app/host/tab-general.js b/src/xo-app/host/tab-general.js index 4f094808a..0eea7cbd3 100644 --- a/src/xo-app/host/tab-general.js +++ b/src/xo-app/host/tab-general.js @@ -1,3 +1,4 @@ +import * as CM from 'complex-matcher' import _ from 'intl' import Copiable from 'copiable' import Icon from 'icon' @@ -12,7 +13,6 @@ import { FormattedRelative } from 'react-intl' import { formatSize } from 'utils' import Usage, { UsageElement } from 'usage' import { getObject } from 'selectors' -import { createString, createProperty, toString } from 'complex-matcher' import { CpuSparkLines, MemorySparkLines, @@ -30,7 +30,7 @@ export default ({ }) => { const pool = getObject(store.getState(), host.$pool) const vmsFilter = encodeURIComponent( - createProperty('$container', createString(host.id))::toString() + new CM.Property('$container', new CM.String(host.id)).toString() ) return ( diff --git a/yarn.lock b/yarn.lock index 05c880da5..a11fa5e1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,13 @@ dependencies: "@babel/types" "7.0.0-beta.31" +"@babel/polyfill@^7.0.0-beta.35": + version "7.0.0-beta.35" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.0.0-beta.35.tgz#49d033c4fdfa54a3a11e8f87239530141650d47a" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.1" + "@babel/template@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.31.tgz#577bb29389f6c497c3e7d014617e7d6713f68bda" @@ -1954,6 +1961,13 @@ commander@2.8.x: dependencies: graceful-readlink ">= 1.0.0" +complex-matcher@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/complex-matcher/-/complex-matcher-0.1.0.tgz#a26ff7c362e9f67b781374e99ea3a502cbd28191" + dependencies: + "@babel/polyfill" "^7.0.0-beta.35" + lodash "^4.17.4" + component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -7334,7 +7348,7 @@ regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" -regenerator-runtime@^0.11.0: +regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"