From 14dc95ba05345f129116f64b45f229602cede698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 Feb 2019 16:29:18 -0500 Subject: [PATCH 1/7] column name with special characters --- dash_table/DataTable.py | 9 +-- dash_table/metadata.json | 2 +- demo/AppMode.ts | 18 ++++- demo/data.ts | 57 +++++++++++++ src/core/syntax-tree/lexicon.ts | 25 ++++-- src/dash-table/DataTable.js | 2 +- src/dash-table/components/FilterFactory.tsx | 4 +- tests/cypress/src/DashTable.ts | 8 ++ .../tests/standalone/filtering_test.ts | 35 ++++++++ .../cypress/tests/unit/syntactic_tree_test.ts | 80 ++++++++++++++++++- 10 files changed, 217 insertions(+), 23 deletions(-) create mode 100644 tests/cypress/tests/standalone/filtering_test.ts diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 3685f230e..9f9d601ec 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -148,7 +148,7 @@ class DataTable(Component): - `current_page` represents which page the user is on. Use this property to index through data in your callbacks with backend paging.. pagination_settings has the following type: dict containing keys 'current_page', 'page_size'. -Those keys have the following types: +Those keys have the following types: - current_page (number; required) - page_size (number; required) - navigation (string; optional): DEPRECATED @@ -191,7 +191,7 @@ class DataTable(Component): Alternatively, the value of the property can also be a plain string. The `text` syntax will be used in that case.. column_static_tooltip has the following type: dict with strings as keys and values of type dict containing keys 'delay', 'duration', 'type', 'value'. -Those keys have the following types: +Those keys have the following types: - delay (number; optional) - duration (number; optional) - type (a value equal to: 'text', 'markdown'; optional) @@ -385,16 +385,13 @@ class DataTable(Component): `selected_rows` from the perspective of the `derived_virtual_indices`. - dropdown_properties (boolean | number | string | dict | list; optional): DEPRECATED Subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) -for updates on the dropdown API. - -Available events: """ +for updates on the dropdown API.""" @_explicitize_args def __init__(self, active_cell=Component.UNDEFINED, columns=Component.UNDEFINED, content_style=Component.UNDEFINED, css=Component.UNDEFINED, data=Component.UNDEFINED, data_previous=Component.UNDEFINED, data_timestamp=Component.UNDEFINED, editable=Component.UNDEFINED, end_cell=Component.UNDEFINED, id=Component.UNDEFINED, is_focused=Component.UNDEFINED, merge_duplicate_headers=Component.UNDEFINED, n_fixed_columns=Component.UNDEFINED, n_fixed_rows=Component.UNDEFINED, row_deletable=Component.UNDEFINED, row_selectable=Component.UNDEFINED, selected_cells=Component.UNDEFINED, selected_rows=Component.UNDEFINED, start_cell=Component.UNDEFINED, style_as_list_view=Component.UNDEFINED, pagination_mode=Component.UNDEFINED, pagination_settings=Component.UNDEFINED, navigation=Component.UNDEFINED, column_conditional_dropdowns=Component.UNDEFINED, column_static_dropdown=Component.UNDEFINED, column_static_tooltip=Component.UNDEFINED, column_conditional_tooltips=Component.UNDEFINED, tooltips=Component.UNDEFINED, tooltip_delay=Component.UNDEFINED, tooltip_duration=Component.UNDEFINED, filtering=Component.UNDEFINED, filtering_settings=Component.UNDEFINED, filtering_type=Component.UNDEFINED, filtering_types=Component.UNDEFINED, sorting=Component.UNDEFINED, sorting_type=Component.UNDEFINED, sorting_settings=Component.UNDEFINED, sorting_treat_empty_string_as_none=Component.UNDEFINED, style_table=Component.UNDEFINED, style_cell=Component.UNDEFINED, style_data=Component.UNDEFINED, style_filter=Component.UNDEFINED, style_header=Component.UNDEFINED, style_cell_conditional=Component.UNDEFINED, style_data_conditional=Component.UNDEFINED, style_filter_conditional=Component.UNDEFINED, style_header_conditional=Component.UNDEFINED, virtualization=Component.UNDEFINED, derived_viewport_data=Component.UNDEFINED, derived_viewport_indices=Component.UNDEFINED, derived_viewport_selected_rows=Component.UNDEFINED, derived_virtual_data=Component.UNDEFINED, derived_virtual_indices=Component.UNDEFINED, derived_virtual_selected_rows=Component.UNDEFINED, dropdown_properties=Component.UNDEFINED, **kwargs): self._prop_names = ['active_cell', 'columns', 'content_style', 'css', 'data', 'data_previous', 'data_timestamp', 'editable', 'end_cell', 'id', 'is_focused', 'merge_duplicate_headers', 'n_fixed_columns', 'n_fixed_rows', 'row_deletable', 'row_selectable', 'selected_cells', 'selected_rows', 'start_cell', 'style_as_list_view', 'pagination_mode', 'pagination_settings', 'navigation', 'column_conditional_dropdowns', 'column_static_dropdown', 'column_static_tooltip', 'column_conditional_tooltips', 'tooltips', 'tooltip_delay', 'tooltip_duration', 'filtering', 'filtering_settings', 'filtering_type', 'filtering_types', 'sorting', 'sorting_type', 'sorting_settings', 'sorting_treat_empty_string_as_none', 'style_table', 'style_cell', 'style_data', 'style_filter', 'style_header', 'style_cell_conditional', 'style_data_conditional', 'style_filter_conditional', 'style_header_conditional', 'virtualization', 'derived_viewport_data', 'derived_viewport_indices', 'derived_viewport_selected_rows', 'derived_virtual_data', 'derived_virtual_indices', 'derived_virtual_selected_rows', 'dropdown_properties'] self._type = 'DataTable' self._namespace = 'dash_table' self._valid_wildcard_attributes = [] - self.available_events = [] self.available_properties = ['active_cell', 'columns', 'content_style', 'css', 'data', 'data_previous', 'data_timestamp', 'editable', 'end_cell', 'id', 'is_focused', 'merge_duplicate_headers', 'n_fixed_columns', 'n_fixed_rows', 'row_deletable', 'row_selectable', 'selected_cells', 'selected_rows', 'start_cell', 'style_as_list_view', 'pagination_mode', 'pagination_settings', 'navigation', 'column_conditional_dropdowns', 'column_static_dropdown', 'column_static_tooltip', 'column_conditional_tooltips', 'tooltips', 'tooltip_delay', 'tooltip_duration', 'filtering', 'filtering_settings', 'filtering_type', 'filtering_types', 'sorting', 'sorting_type', 'sorting_settings', 'sorting_treat_empty_string_as_none', 'style_table', 'style_cell', 'style_data', 'style_filter', 'style_header', 'style_cell_conditional', 'style_data_conditional', 'style_filter_conditional', 'style_header_conditional', 'virtualization', 'derived_viewport_data', 'derived_viewport_indices', 'derived_viewport_selected_rows', 'derived_virtual_data', 'derived_virtual_indices', 'derived_virtual_selected_rows', 'dropdown_properties'] self.available_wildcard_properties = [] diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 5d089cfbe..b361b919f 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -652,7 +652,7 @@ "required": false, "description": "`column_static_tooltip` represents the tooltip shown\nfor different columns.\nThe `property` name refers to the column ID.\nThe `type` refers to the type of tooltip syntax used\nfor the tooltip generation. Can either be `markdown`\nor `text`. Defaults to `text`.\nThe `value` refers to the syntax-based content of\nthe tooltip. This value is required.\nThe `delay` represents the delay in milliseconds before\nthe tooltip is shown when hovering a cell. This overrides\nthe table's `tooltip_delay` property. If set to `null`,\nthe tooltip will be shown immediately.\nThe `duration` represents the duration in milliseconds\nduring which the tooltip is shown when hovering a cell.\nThis overrides the table's `tooltip_duration` property.\nIf set to `null`, the tooltip will not disappear.\n\nAlternatively, the value of the property can also be\na plain string. The `text` syntax will be used in\nthat case.", "defaultValue": { - "value": "[]", + "value": "{}", "computed": false } }, diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 1215fdf98..80ce89b2e 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -2,7 +2,7 @@ import * as R from 'ramda'; import Environment from 'core/environment'; -import { generateMockData, IDataMock } from './data'; +import { generateMockData, IDataMock, generateSpaceMockData } from './data'; import { ContentStyle, PropsWithDefaults, @@ -16,6 +16,7 @@ export enum AppMode { Default = 'default', FixedVirtualized = 'fixed,virtualized', ReadOnly = 'readonly', + ColumnsInSpace = 'columnsInSpace', Tooltips = 'tooltips', Typed = 'typed', Virtualized = 'virtualized' @@ -63,11 +64,13 @@ function getBaseTableProps(mock: IDataMock) { }; } -function getDefaultState(): { +function getDefaultState( + generateData: Function = generateMockData +): { filter: string, tableProps: Partial } { - const mock = generateMockData(5000); + const mock = generateData(5000); return { filter: '', @@ -98,6 +101,13 @@ function getReadonlyState() { return state; } +function getSpaceInColumn() { + const state = getDefaultState(generateSpaceMockData); + state.tableProps.filtering = true; + + return state; +} + function getTooltipsState() { const state = getDefaultState(); @@ -202,6 +212,8 @@ function getState() { return getFixedVirtualizedState(); case AppMode.ReadOnly: return getReadonlyState(); + case AppMode.ColumnsInSpace: + return getSpaceInColumn(); case AppMode.Tooltips: return getTooltipsState(); case AppMode.Virtualized: diff --git a/demo/data.ts b/demo/data.ts index 0754d429a..c38b8a2d2 100644 --- a/demo/data.ts +++ b/demo/data.ts @@ -92,6 +92,63 @@ export const generateMockData = (rows: number) => unpackIntoColumnsAndData([ } ]); +export const generateSpaceMockData = (rows: number) => unpackIntoColumnsAndData([ + { + id: 'rows', + type: ColumnType.Numeric, + editable: false, + data: gendata(i => i, rows) + }, + + { + id: 'c cc', + name: ['City', 'Canada', 'Toronto'], + type: ColumnType.Numeric, + data: gendata(i => i, rows) + }, + + { + id: 'd:dd', + name: ['City', 'Canada', 'Montréal'], + type: ColumnType.Numeric, + data: gendata(i => i * 100, rows) + }, + + { + id: 'e-ee', + name: ['City', 'America', 'New York City'], + type: ColumnType.Numeric, + data: gendata(i => i, rows) + }, + + { + id: 'f_ff', + name: ['City', 'America', 'Boston'], + type: ColumnType.Numeric, + data: gendata(i => i + 1, rows) + }, + + { + id: 'g.gg', + name: ['City', 'France', 'Paris'], + type: ColumnType.Numeric, + editable: true, + data: gendata(i => i * 10, rows) + }, + + { + id: 'b+bb', + name: ['', 'Weather', 'Climate'], + type: ColumnType.Text, + presentation: 'dropdown', + clearable: true, + data: gendata( + i => ['Humid', 'Wet', 'Snowy', 'Tropical Beaches'][i % 4], + rows + ) + } +]); + export const mockDataSimple = (rows: number) => unpackIntoColumnsAndData([ { id: 'aaa', diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 1a699bfd8..f76c6f4b5 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -31,10 +31,21 @@ const isPrime = (c: number) => { return true; }; -const baseOperand = { +const operand = { resolve: (target: any, tree: ISyntaxTree) => { - Logger.trace('resolve -> exp', target, tree); + if (/^('.*')|(".*")$/.test(tree.value)) { + return target[ + tree.value.slice(1, tree.value.length - 1) + ]; + } else if (/^(\w|[:.\-_+])+$/.test(tree.value)) { + return target[tree.value]; + } + }, + regexp: /^('([^()']|\\')+'|"([^()"]|\\")+"|(\w|[:.\-_+])+)/ +}; +const expression = { + resolve: (target: any, tree: ISyntaxTree) => { if (/^('.*')|(".*")$/.test(tree.value)) { return tree.value.slice(1, tree.value.length - 1); } else if (/^\w+\(.*\)$/.test(tree.value)) { @@ -122,9 +133,10 @@ const lexicon: ILexeme[] = [ }, when: [LexemeType.UnaryNot] }, - Object.assign({ + { + ...operand, name: LexemeType.Operand - }, baseOperand), + }, { evaluate: (target, tree) => { Logger.trace('evaluate -> binary', target, tree); @@ -224,10 +236,11 @@ const lexicon: ILexeme[] = [ }, when: [LexemeType.UnaryNot] }, - Object.assign({ + { + ...expression, name: LexemeType.Expression, when: [LexemeType.BinaryOperator] - }, baseOperand) + } ]; export default lexicon; \ No newline at end of file diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 0eb7ba5b8..be52eedd3 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -71,7 +71,7 @@ export const defaultProps = { column_conditional_dropdowns: [], column_static_dropdown: [], - column_static_tooltip: [], + column_static_tooltip: {}, column_conditional_tooltips: [], tooltip_delay: 350, tooltip_duration: 2000, diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 6b744f97b..5413c72a7 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -55,7 +55,7 @@ export default class FilterFactory { } setFilter(R.map( - ([cId, filter]) => `${cId} ${filter}`, + ([cId, filter]) => `"${cId}" ${filter}`, R.filter( ([cId]) => this.isFragmentValid(cId), Array.from(ops.entries()) @@ -159,7 +159,7 @@ export default class FilterFactory { private isFragmentValid(columnId: ColumnId) { const op = this.ops.get(columnId.toString()); - const lexerResult = lexer(`${columnId} ${op}`); + const lexerResult = lexer(`"${columnId}" ${op}`); const syntaxerResult = syntaxer(lexerResult); return syntaxerResult.valid && this.isBasicFilter(lexerResult, syntaxerResult, false); diff --git a/tests/cypress/src/DashTable.ts b/tests/cypress/src/DashTable.ts index 46de51786..254a31d48 100644 --- a/tests/cypress/src/DashTable.ts +++ b/tests/cypress/src/DashTable.ts @@ -7,6 +7,14 @@ export default class DashTable { return cy.get(`#table tbody tr td[data-dash-column="${column}"]`).eq(row); } + static getFilter(column: number) { + return cy.get(`#table tbody tr th.dash-filter.column-${column}`); + } + + static getFilterById(column: number | string) { + return cy.get(`#table tbody tr th.dash-filter[data-dash-column="${column}"]`); + } + static getDelete(row: number) { return cy.get(`#table tbody tr td.dash-delete-cell`).eq(row); } diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts new file mode 100644 index 000000000..851efcec2 --- /dev/null +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -0,0 +1,35 @@ +import DashTable from 'cypress/DashTable'; +import DOM from 'cypress/DOM'; +import Key from 'cypress/Key'; + +import { AppMode } from 'demo/AppMode'; + +describe(`filter`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.ColumnsInSpace}`); + DashTable.toggleScroll(false); + }); + + it('can filter on special column id', () => { + DashTable.getFilterById('c cc').click(); + DOM.focused.type(`gt num(90)${Key.Enter}`); + + DashTable.getFilterById('d:dd').click(); + DOM.focused.type(`lt num(12500)${Key.Enter}`); + + DashTable.getFilterById('e-ee').click(); + DOM.focused.type(`is prime${Key.Enter}`); + + DashTable.getFilterById('f_ff').click(); + DOM.focused.type(`le num(106)${Key.Enter}`); + + DashTable.getFilterById('g.gg').click(); + DOM.focused.type(`gt num(1000)${Key.Enter}`); + + DashTable.getFilterById('b+bb').click(); + DOM.focused.type(`eq "Wet"${Key.Enter}`); + + DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '101')); + DashTable.getCellById(1, 'rows').should('not.exist'); + }); +}); \ No newline at end of file diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/syntactic_tree_test.ts index c2634020c..b2e2caf88 100644 --- a/tests/cypress/tests/unit/syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/syntactic_tree_test.ts @@ -1,10 +1,82 @@ import SyntaxTree from 'core/syntax-tree'; describe('Syntax Tree', () => { - const data0 = { a: '0', b: '0', c: 0, d: null }; - const data1 = { a: '1', b: '0', c: 1, d: 0 }; - const data2 = { a: '2', b: '1', c: 2, d: '' }; - const data3 = { a: '3', b: '1', c: 3, d: false }; + const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot' }; + const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot' }; + const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot' }; + const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot' }; + + describe('operands', () => { + it('support column name with "."', () => { + const tree = new SyntaxTree('a.dot eq "1.dot" || a.dot eq "2.dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support column name with "-"', () => { + const tree = new SyntaxTree('a-dot eq "1-dot" || a-dot eq "2-dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support column name with "_"', () => { + const tree = new SyntaxTree('a_dot eq "1_dot" || a_dot eq "2_dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support column name with "+"', () => { + const tree = new SyntaxTree('a+dot eq "1+dot" || a+dot eq "2+dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support column name with ":"', () => { + const tree = new SyntaxTree('a:dot eq "1:dot" || a:dot eq "2:dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support double quoted column name with " " (space)', () => { + const tree = new SyntaxTree('"a dot" eq "1 dot" || "a dot" eq "2 dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support single quoted column name with " " (space)', () => { + const tree = new SyntaxTree('\'a dot\' eq "1 dot" || \'a dot\' eq "2 dot"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + }); describe('&& and ||', () => { it('can || two conditions', () => { From d4a3f0c2391ea61d12a5d71513cc8799e3b93caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 Feb 2019 16:38:19 -0500 Subject: [PATCH 2/7] - changelog - clean up lexicon selector - arbitrary id test --- CHANGELOG.md | 10 ++++++++++ src/core/syntax-tree/lexicon.ts | 4 ++-- .../cypress/tests/unit/syntactic_tree_test.ts | 18 ++++++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf1cbae4..a06938762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### Changed +[#224](https://github.com/plotly/dash-table/issues/224) +- Added support for unquoted column id with + - letters, numbers, [-+:.] +- Added support for single and double quoted column id with arbitrary name + +### Fixed +- Incorrect default value for `column_static_tooltip` changed from [] to {} + ## [3.3.0] - 2019-02-01 ### Added [#307](https://github.com/plotly/dash-core/issues/307) diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index f76c6f4b5..91f830902 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -37,11 +37,11 @@ const operand = { return target[ tree.value.slice(1, tree.value.length - 1) ]; - } else if (/^(\w|[:.\-_+])+$/.test(tree.value)) { + } else if (/^(\w|[:.\-+])+$/.test(tree.value)) { return target[tree.value]; } }, - regexp: /^('([^()']|\\')+'|"([^()"]|\\")+"|(\w|[:.\-_+])+)/ + regexp: /^('([^()']|\\')+'|"([^()"]|\\")+"|(\w|[:.\-+])+)/ }; const expression = { diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/syntactic_tree_test.ts index b2e2caf88..5ab317f75 100644 --- a/tests/cypress/tests/unit/syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/syntactic_tree_test.ts @@ -1,12 +1,22 @@ import SyntaxTree from 'core/syntax-tree'; describe('Syntax Tree', () => { - const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot' }; - const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot' }; - const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot' }; - const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot' }; + const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot' }; + const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot' }; + const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot' }; + const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot' }; describe('operands', () => { + it('support arbitrary quoted column name', () => { + const tree = new SyntaxTree(`'_-6.:+** *@$' eq "1*dot" || '_-6.:+** *@$' eq "2*dot"`); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + it('support column name with "."', () => { const tree = new SyntaxTree('a.dot eq "1.dot" || a.dot eq "2.dot"'); From 1782dc41332a1dfbf3b52176a9495d8d10783571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 Feb 2019 16:52:26 -0500 Subject: [PATCH 3/7] - sanity test against latest dash release --- requirements-v0.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-v0.txt b/requirements-v0.txt index 8a978093d..c39323129 100644 --- a/requirements-v0.txt +++ b/requirements-v0.txt @@ -1,4 +1,4 @@ -git+git://github.com/plotly/dash@master#egg=dash git+git://github.com/plotly/dash-core-components@master#egg=dash_core_components git+git://github.com/plotly/dash-html-components@master#egg=dash_html_components -git+git://github.com/plotly/dash-renderer@master#egg=dash_renderer \ No newline at end of file +dash==0.36.0 +dash-renderer==0.17.0 \ No newline at end of file From 2d5c2c0523516d6d3a8f607c358c284b9fda9d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 Feb 2019 17:07:40 -0500 Subject: [PATCH 4/7] bump panda version --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index cba539c3f..c977f32a8 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -28,7 +28,7 @@ MarkupSafe==1.0 mccabe==0.6.1 nbformat==4.4.0 numpy==1.15.1 -pandas==0.23.4 +pandas==0.24.1 parso==0.3.1 percy==2.0.0 pexpect==4.6.0 From 3bfe23fea727b9c2f6d8f454989d872c480c700a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 Feb 2019 17:13:58 -0500 Subject: [PATCH 5/7] head of master once again --- requirements-v0.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-v0.txt b/requirements-v0.txt index c39323129..8a978093d 100644 --- a/requirements-v0.txt +++ b/requirements-v0.txt @@ -1,4 +1,4 @@ +git+git://github.com/plotly/dash@master#egg=dash git+git://github.com/plotly/dash-core-components@master#egg=dash_core_components git+git://github.com/plotly/dash-html-components@master#egg=dash_html_components -dash==0.36.0 -dash-renderer==0.17.0 \ No newline at end of file +git+git://github.com/plotly/dash-renderer@master#egg=dash_renderer \ No newline at end of file From 77d8a55d9a758afe07e7be085e24f91163a44c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 5 Feb 2019 12:01:47 -0500 Subject: [PATCH 6/7] - allow parantheses in quotes - test nested quotes - add `` to '' and "" support --- src/core/syntax-tree/lexicon.ts | 10 +++++----- .../cypress/tests/unit/syntactic_tree_test.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 91f830902..be5ef7db1 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -33,7 +33,7 @@ const isPrime = (c: number) => { const operand = { resolve: (target: any, tree: ISyntaxTree) => { - if (/^('.*')|(".*")$/.test(tree.value)) { + if (/^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))$/.test(tree.value)) { return target[ tree.value.slice(1, tree.value.length - 1) ]; @@ -41,14 +41,14 @@ const operand = { return target[tree.value]; } }, - regexp: /^('([^()']|\\')+'|"([^()"]|\\")+"|(\w|[:.\-+])+)/ + regexp: /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`)|(\w|[:.\-+])+)/ }; const expression = { resolve: (target: any, tree: ISyntaxTree) => { - if (/^('.*')|(".*")$/.test(tree.value)) { + if (/^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))$/.test(tree.value)) { return tree.value.slice(1, tree.value.length - 1); - } else if (/^\w+\(.*\)$/.test(tree.value)) { + } else if (/^(num|str)\(.*\)$/.test(tree.value)) { const res = tree.value.match(/^(\w+)\((.*)\)$/); if (res) { const [, op, value] = res; @@ -67,7 +67,7 @@ const expression = { return target[tree.value]; } }, - regexp: /^(((num|str)\([^()]*\))|'([^()']|\\')+'|"([^()"]|\\")+"|\w+)/ + regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`)|(\w|[:.\-+])+)/ }; const lexicon: ILexeme[] = [ diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/syntactic_tree_test.ts index 5ab317f75..cf99073d1 100644 --- a/tests/cypress/tests/unit/syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/syntactic_tree_test.ts @@ -1,10 +1,10 @@ import SyntaxTree from 'core/syntax-tree'; describe('Syntax Tree', () => { - const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot' }; - const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot' }; - const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot' }; - const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot' }; + const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '\'""\'': '0\'"dot' }; + const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '\'""\'': '1\'"dot' }; + const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '\'""\'': '2\'"dot' }; + const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '\'""\'': '3\'"dot' }; describe('operands', () => { it('support arbitrary quoted column name', () => { @@ -86,6 +86,16 @@ describe('Syntax Tree', () => { expect(tree.evaluate(data2)).to.equal(true); expect(tree.evaluate(data3)).to.equal(false); }); + + it('support nesting in quotes', () => { + const tree = new SyntaxTree(`\`'""'\` eq \`1'"dot\` || \`'""'\` eq \`2'"dot\``); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); }); describe('&& and ||', () => { From 3566036fb2d4a9ee062f4b7617964cf4ee9016b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 5 Feb 2019 18:10:40 -0500 Subject: [PATCH 7/7] - improve operand & expression regex - add tests for badly formed operands --- src/core/syntax-tree/lexicon.ts | 8 +++---- .../cypress/tests/unit/syntactic_tree_test.ts | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index be5ef7db1..7a58857ec 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -33,7 +33,7 @@ const isPrime = (c: number) => { const operand = { resolve: (target: any, tree: ISyntaxTree) => { - if (/^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))$/.test(tree.value)) { + if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.")+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { return target[ tree.value.slice(1, tree.value.length - 1) ]; @@ -41,12 +41,12 @@ const operand = { return target[tree.value]; } }, - regexp: /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`)|(\w|[:.\-+])+)/ + regexp: /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/ }; const expression = { resolve: (target: any, tree: ISyntaxTree) => { - if (/^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))$/.test(tree.value)) { + if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { return tree.value.slice(1, tree.value.length - 1); } else if (/^(num|str)\(.*\)$/.test(tree.value)) { const res = tree.value.match(/^(\w+)\((.*)\)$/); @@ -67,7 +67,7 @@ const expression = { return target[tree.value]; } }, - regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`)|(\w|[:.\-+])+)/ + regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/ }; const lexicon: ILexeme[] = [ diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/syntactic_tree_test.ts index cf99073d1..83003e5f7 100644 --- a/tests/cypress/tests/unit/syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/syntactic_tree_test.ts @@ -7,6 +7,30 @@ describe('Syntax Tree', () => { const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '\'""\'': '3\'"dot' }; describe('operands', () => { + it('does not support badly formed operands', () => { + expect(new SyntaxTree(`'myField' eq num(0)`).isValid).to.equal(true); + expect(new SyntaxTree(`"myField" eq num(0)`).isValid).to.equal(true); + expect(new SyntaxTree('`myField` eq num(0)').isValid).to.equal(true); + expect(new SyntaxTree(`'myField\\' eq num(0)`).isValid).to.equal(false); + expect(new SyntaxTree(`"myField\\" eq num(0)`).isValid).to.equal(false); + expect(new SyntaxTree('`myField\\` eq num(0)').isValid).to.equal(false); + expect(new SyntaxTree(`\\'myField' eq num(0)`).isValid).to.equal(false); + expect(new SyntaxTree(`\\"myField" eq num(0)`).isValid).to.equal(false); + expect(new SyntaxTree('\\`myField` eq num(0)').isValid).to.equal(false); + }); + + it('does not support badly formed expression', () => { + expect(new SyntaxTree(`myField eq 'value'`).isValid).to.equal(true); + expect(new SyntaxTree(`myField eq "value"`).isValid).to.equal(true); + expect(new SyntaxTree('myField eq `value`').isValid).to.equal(true); + expect(new SyntaxTree(`myField eq 'value\\'`).isValid).to.equal(false); + expect(new SyntaxTree(`myField eq "value\\"`).isValid).to.equal(false); + expect(new SyntaxTree('myField eq `value\\`').isValid).to.equal(false); + expect(new SyntaxTree(`myField eq \\'value'`).isValid).to.equal(false); + expect(new SyntaxTree(`myField eq \\"value"`).isValid).to.equal(false); + expect(new SyntaxTree('myField eq \\`value`').isValid).to.equal(false); + }); + it('support arbitrary quoted column name', () => { const tree = new SyntaxTree(`'_-6.:+** *@$' eq "1*dot" || '_-6.:+** *@$' eq "2*dot"`);