@vuu-ui/vuu-filters 0.5.13 → 0.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -13
- package/src/filter-input/FilterInput.css +47 -0
- package/src/filter-input/FilterInput.tsx +44 -0
- package/src/filter-input/codemirror-basic-setup.ts +100 -0
- package/src/filter-input/filter-language-parser/FilterLanguage.ts +21 -0
- package/src/filter-input/filter-language-parser/generated/filter-parser.js +16 -0
- package/src/filter-input/filter-language-parser/generated/filter-parser.terms.js +32 -0
- package/src/filter-input/filter-language-parser/grammar/filter.grammar +53 -0
- package/src/filter-input/filter-language-parser/index.ts +1 -0
- package/src/filter-input/filter-language-parser/walkTree.ts +237 -0
- package/src/filter-input/highlighting.ts +9 -0
- package/src/filter-input/index.ts +2 -0
- package/src/filter-input/theme.ts +50 -0
- package/src/filter-input/useCodeMirrorEditor.ts +189 -0
- package/src/filter-input/useFilterAutoComplete.ts +292 -0
- package/src/filter-toolbar/FilterDropdown.tsx +60 -0
- package/src/filter-toolbar/FilterDropdownMultiSelect.tsx +64 -0
- package/src/filter-toolbar/FilterToolbar.css +11 -0
- package/src/filter-toolbar/FilterToolbar.tsx +26 -0
- package/src/filter-toolbar/index.ts +1 -0
- package/src/filter-toolbar/useFilterToolbar.tsx +85 -0
- package/src/filter-utils.ts +440 -0
- package/src/filterTypes.ts +84 -0
- package/src/index.ts +4 -0
- package/tsconfig-emit-types.json +10 -0
- package/LICENSE +0 -201
- package/cjs/index.js +0 -181
- package/cjs/index.js.map +0 -7
- package/esm/index.js +0 -181
- package/esm/index.js.map +0 -7
- package/index.css +0 -2
- package/index.css.map +0 -7
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vuu-ui/vuu-filters",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.15",
|
|
4
|
+
"main": "src/index.ts",
|
|
4
5
|
"author": "heswell",
|
|
5
6
|
"license": "Apache-2.0",
|
|
6
7
|
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "node ../../scripts/run-build.mjs",
|
|
10
|
+
"lezer-generate:filter": "lezer-generator --output ./src/filter-input/filter-language-parser/generated/filter-parser.js ./src/filter-input/filter-language-parser/grammar/filter.grammar",
|
|
11
|
+
"lezer-generate:column": "lezer-generator --output ./src/column-expression-input/column-language-parser/generated/column-parser.js ./src/column-expression-input/column-language-parser/grammar/column.grammar",
|
|
12
|
+
"type-defs": "node ../../scripts/build-type-defs.mjs"
|
|
13
|
+
},
|
|
7
14
|
"devDependencies": {
|
|
8
15
|
"@lezer/generator": "^1.1.1"
|
|
9
16
|
},
|
|
@@ -16,18 +23,10 @@
|
|
|
16
23
|
"@lezer/common": "^1.0.1"
|
|
17
24
|
},
|
|
18
25
|
"peerDependencies": {
|
|
19
|
-
"@vuu-ui/vuu-data": "0.5.
|
|
20
|
-
"@vuu-ui/vuu-utils": "0.5.
|
|
26
|
+
"@vuu-ui/vuu-data": "0.5.15",
|
|
27
|
+
"@vuu-ui/vuu-utils": "0.5.15",
|
|
21
28
|
"@heswell/salt-lab": "1.0.0-alpha.0",
|
|
22
29
|
"react": "^17.0.2",
|
|
23
30
|
"react-dom": "^17.0.2"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
"cjs",
|
|
27
|
-
"esm",
|
|
28
|
-
"index.css",
|
|
29
|
-
"index.css.map"
|
|
30
|
-
],
|
|
31
|
-
"module": "esm/index.js",
|
|
32
|
-
"main": "cjs/index.js"
|
|
33
|
-
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
.salt-theme {
|
|
3
|
+
--vuuFilterEditor-lineHeight: 28px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.salt-density-high {
|
|
7
|
+
--vuuFilterEditor-buttonWidth: 20px;
|
|
8
|
+
--vuuFilterEditor-height: 22px;
|
|
9
|
+
--vuuFilterEditor-lineHeight: 20px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.vuuFilterInput {
|
|
13
|
+
--vuuFilterEditor-background: var(--salt-container-primary-background);
|
|
14
|
+
--vuuFilterEditor-color: var(--salt-text-primary-foreground);
|
|
15
|
+
--vuuFilterEditor-fontFamily: var(--salt-typography-fontFamily);
|
|
16
|
+
--vuuFilterEditor-fontSize: var(--salt-text-fontSize);
|
|
17
|
+
--vuuFilterEditor-cursorColor: var(--salt-text-secondary-foreground);
|
|
18
|
+
--vuuFilterEditor-selectionBackground: var(--salt-text-background-selected);
|
|
19
|
+
--vuuFilterEditor-tooltipBackground: var(--salt-container-primary-background);
|
|
20
|
+
--vuuFilterEditor-tooltipBorder: var(--tooltip-status-borderColor) var(--salt-container-borderWidth) var(--salt-container-borderStyle);
|
|
21
|
+
--vuuFilterEditor-tooltipElevation: var(--salt-overlayable-shadow-popout);
|
|
22
|
+
--vuuFilterEditor-suggestion-selectedBackground: var(--salt-selectable-background-selected);
|
|
23
|
+
--vuuFilterEditor-suggestion-selectedColor: var(--vuuFilterEditor-color);
|
|
24
|
+
--vuuFilterEditor-suggestion-height: 24px;
|
|
25
|
+
--vuuFilterEditor-variableColor: blue;
|
|
26
|
+
|
|
27
|
+
align-items: center;
|
|
28
|
+
border-color: var(--salt-container-borderColor-medium);
|
|
29
|
+
border-style: solid none;
|
|
30
|
+
border-width: 1px;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
display: flex;
|
|
33
|
+
height: var(--vuuFilterEditor-height, 30px);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.vuuFilterInput-Editor {
|
|
37
|
+
flex: 1 1 auto;
|
|
38
|
+
height: 100%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.vuuFilterInput-FilterButton,
|
|
42
|
+
.vuuFilterInput-ClearButton {
|
|
43
|
+
--vuu-icon-size: 12px;
|
|
44
|
+
--saltButton-width: var(--vuuFilterEditor-buttonWidth, 28px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Filter } from "@vuu-ui/vuu-filter-types";
|
|
2
|
+
import { Button } from "@salt-ds/core";
|
|
3
|
+
import { HTMLAttributes } from "react";
|
|
4
|
+
import { SuggestionConsumer, useCodeMirrorEditor } from "./useCodeMirrorEditor";
|
|
5
|
+
|
|
6
|
+
import "./FilterInput.css";
|
|
7
|
+
|
|
8
|
+
const classBase = "vuuFilterInput";
|
|
9
|
+
|
|
10
|
+
export interface FilterInputProps
|
|
11
|
+
extends SuggestionConsumer,
|
|
12
|
+
HTMLAttributes<HTMLDivElement> {
|
|
13
|
+
existingFilter?: Filter;
|
|
14
|
+
onSubmitFilter?: (
|
|
15
|
+
filter: Filter | undefined,
|
|
16
|
+
filterQuery: string,
|
|
17
|
+
filterName?: string
|
|
18
|
+
) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const FilterInput = ({
|
|
22
|
+
existingFilter,
|
|
23
|
+
onSubmitFilter,
|
|
24
|
+
suggestionProvider,
|
|
25
|
+
...props
|
|
26
|
+
}: FilterInputProps) => {
|
|
27
|
+
const { editorRef, clearInput } = useCodeMirrorEditor({
|
|
28
|
+
existingFilter,
|
|
29
|
+
onSubmitFilter,
|
|
30
|
+
suggestionProvider,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div {...props} className={classBase}>
|
|
35
|
+
<Button className={`${classBase}-FilterButton`} data-icon="filter" />
|
|
36
|
+
<div className={`${classBase}-Editor`} ref={editorRef} />
|
|
37
|
+
<Button
|
|
38
|
+
className={`${classBase}-ClearButton`}
|
|
39
|
+
data-icon="close-circle"
|
|
40
|
+
onClick={clearInput}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
2
|
+
import {
|
|
3
|
+
defaultHighlightStyle,
|
|
4
|
+
syntaxHighlighting
|
|
5
|
+
} from "@codemirror/language";
|
|
6
|
+
import { Extension } from "@codemirror/state";
|
|
7
|
+
import {
|
|
8
|
+
drawSelection, highlightSpecialChars, keymap
|
|
9
|
+
} from "@codemirror/view";
|
|
10
|
+
// import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
|
|
11
|
+
// import {
|
|
12
|
+
// autocompletion,
|
|
13
|
+
// completionKeymap,
|
|
14
|
+
// closeBrackets,
|
|
15
|
+
// closeBracketsKeymap,
|
|
16
|
+
// } from "@codemirror/autocomplete";
|
|
17
|
+
// import { lintKeymap } from "@codemirror/lint";
|
|
18
|
+
|
|
19
|
+
// (The superfluous function calls around the list of extensions work
|
|
20
|
+
// around current limitations in tree-shaking software.)
|
|
21
|
+
|
|
22
|
+
/// This is an extension value that just pulls together a number of
|
|
23
|
+
/// extensions that you might want in a basic editor. It is meant as a
|
|
24
|
+
/// convenient helper to quickly set up CodeMirror without installing
|
|
25
|
+
/// and importing a lot of separate packages.
|
|
26
|
+
///
|
|
27
|
+
/// Specifically, it includes...
|
|
28
|
+
///
|
|
29
|
+
/// - [the default command bindings](#commands.defaultKeymap)
|
|
30
|
+
/// - [line numbers](#view.lineNumbers)
|
|
31
|
+
/// - [special character highlighting](#view.highlightSpecialChars)
|
|
32
|
+
/// - [the undo history](#commands.history)
|
|
33
|
+
/// - [a fold gutter](#language.foldGutter)
|
|
34
|
+
/// - [custom selection drawing](#view.drawSelection)
|
|
35
|
+
/// - [drop cursor](#view.dropCursor)
|
|
36
|
+
/// - [multiple selections](#state.EditorState^allowMultipleSelections)
|
|
37
|
+
/// - [reindentation on input](#language.indentOnInput)
|
|
38
|
+
/// - [the default highlight style](#language.defaultHighlightStyle) (as fallback)
|
|
39
|
+
/// - [bracket matching](#language.bracketMatching)
|
|
40
|
+
/// - [bracket closing](#autocomplete.closeBrackets)
|
|
41
|
+
/// - [autocompletion](#autocomplete.autocompletion)
|
|
42
|
+
/// - [rectangular selection](#view.rectangularSelection) and [crosshair cursor](#view.crosshairCursor)
|
|
43
|
+
/// - [active line highlighting](#view.highlightActiveLine)
|
|
44
|
+
/// - [active line gutter highlighting](#view.highlightActiveLineGutter)
|
|
45
|
+
/// - [selection match highlighting](#search.highlightSelectionMatches)
|
|
46
|
+
/// - [search](#search.searchKeymap)
|
|
47
|
+
/// - [linting](#lint.lintKeymap)
|
|
48
|
+
///
|
|
49
|
+
/// (You'll probably want to add some language package to your setup
|
|
50
|
+
/// too.)
|
|
51
|
+
///
|
|
52
|
+
/// This extension does not allow customization. The idea is that,
|
|
53
|
+
/// once you decide you want to configure your editor more precisely,
|
|
54
|
+
/// you take this package's source (which is just a bunch of imports
|
|
55
|
+
/// and an array literal), copy it into your own code, and adjust it
|
|
56
|
+
/// as desired.
|
|
57
|
+
// export const basicSetup: Extension = (() => [
|
|
58
|
+
// lineNumbers(),
|
|
59
|
+
// highlightActiveLineGutter(),
|
|
60
|
+
// highlightSpecialChars(),
|
|
61
|
+
// history(),
|
|
62
|
+
// foldGutter(),
|
|
63
|
+
// drawSelection(),
|
|
64
|
+
// dropCursor(),
|
|
65
|
+
// EditorState.allowMultipleSelections.of(true),
|
|
66
|
+
// indentOnInput(),
|
|
67
|
+
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
68
|
+
// bracketMatching(),
|
|
69
|
+
// closeBrackets(),
|
|
70
|
+
// autocompletion(),
|
|
71
|
+
// rectangularSelection(),
|
|
72
|
+
// crosshairCursor(),
|
|
73
|
+
// highlightActiveLine(),
|
|
74
|
+
// highlightSelectionMatches(),
|
|
75
|
+
// keymap.of([
|
|
76
|
+
// ...closeBracketsKeymap,
|
|
77
|
+
// ...defaultKeymap,
|
|
78
|
+
// ...searchKeymap,
|
|
79
|
+
// ...historyKeymap,
|
|
80
|
+
// ...foldKeymap,
|
|
81
|
+
// ...completionKeymap,
|
|
82
|
+
// ...lintKeymap,
|
|
83
|
+
// ]),
|
|
84
|
+
// ])();
|
|
85
|
+
|
|
86
|
+
/// A minimal set of extensions to create a functional editor. Only
|
|
87
|
+
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
|
88
|
+
/// history](#commands.history), [special character
|
|
89
|
+
/// highlighting](#view.highlightSpecialChars), [custom selection
|
|
90
|
+
/// drawing](#view.drawSelection), and [default highlight
|
|
91
|
+
/// style](#language.defaultHighlightStyle).
|
|
92
|
+
export const minimalSetup: Extension = (() => [
|
|
93
|
+
highlightSpecialChars(),
|
|
94
|
+
history(),
|
|
95
|
+
drawSelection(),
|
|
96
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
97
|
+
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
98
|
+
])();
|
|
99
|
+
|
|
100
|
+
export { EditorView } from "@codemirror/view";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
|
2
|
+
import { styleTags, tags as tag } from "@lezer/highlight";
|
|
3
|
+
import { parser } from "./generated/filter-parser";
|
|
4
|
+
|
|
5
|
+
const filterLanguage = LRLanguage.define({
|
|
6
|
+
name: "VuuFilterQuery",
|
|
7
|
+
parser: parser.configure({
|
|
8
|
+
props: [
|
|
9
|
+
styleTags({
|
|
10
|
+
Identifier: tag.variableName,
|
|
11
|
+
String: tag.string,
|
|
12
|
+
Or: tag.emphasis,
|
|
13
|
+
Operator: tag.operator,
|
|
14
|
+
}),
|
|
15
|
+
],
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const filterLanguageSupport = () => {
|
|
20
|
+
return new LanguageSupport(filterLanguage);
|
|
21
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
|
2
|
+
import {LRParser} from "@lezer/lr"
|
|
3
|
+
export const parser = LRParser.deserialize({
|
|
4
|
+
version: 14,
|
|
5
|
+
states: "%jOVQPOOOOQO'#Cc'#CcO[QQO'#CbOOQO'#Ca'#CaOvQQO'#C`O!UQQO'#C_O!aQQO'#C^QOQPOOOOQO'#Ce'#CeO!iQPO,58|O!tQPO,59]OVQPO'#C|O!yQQO,58zOVQPO'#C}O#XQQO,58yO#dQPO'#CyOOQO,58x,58xO#iQPO'#CoOOQO'#Cm'#CmOOQO1G.h1G.hO!iQPO1G.wOOQO,59h,59hOOQO-E6z-E6zOOQO,59i,59iOOQO-E6{-E6{OOQO,59e,59eO#nQPO,59ZO#sQPO'#CtO#{QPO7+$cOOQO1G.u1G.uO!iQPO'#C{O$QQPO,59`OOQO<<G}<<G}OOQO,59g,59gOOQO-E6y-E6y",
|
|
6
|
+
stateData: "$Y~OtOS~OWPO~OYWOZWO[WO]WO^WO_WO`WOfYO~OkZOlSXnSXrSX~Ol]OnRXrRX~On_OrQX~OWbObbOdaO~OgdO~OkZOlSanSarSa~Ol]OnRarRa~OWiO~OWjO~OdmO~OinOjhX~OjpO~OinOjha~O",
|
|
7
|
+
goto: "#]rPPsvy!P!W!]P!bPPPPPPP!eP!nP!WPP!sPPPP!vP!y#P#VRVORUOQTORg]SSO]ReZVROZ]VQOZ]RXQQcXQkdRqnVbXdnRldR`UQokRroQ[SRf[Q^TRh^",
|
|
8
|
+
nodeNames: "⚠ Filter Expression OrExpression AndExpression FilterClause ColumnValueExpression Column Identifier Operator Eq NotEq Gt Lt Contains Starts Ends Value Int String Quote ColumnSetExpression In LBrack Values Comma RBrack And Or AsClause As",
|
|
9
|
+
maxTerm: 36,
|
|
10
|
+
skippedNodes: [0],
|
|
11
|
+
repeatNodeCount: 3,
|
|
12
|
+
tokenData: "1U~RkXY!vYZ!v]^!vpq!vqr#Xrs#d|}#i!Q![#n!^!_#v!_!`#{!`!a$Q!c!}$V!}#O$h#P#Q$m#R#S$V#T#U$r#U#V$V#V#W&p#W#X$V#X#Y*i#Y#]$V#]#^,e#^#c$V#c#d-a#d#g$V#g#h.]#h#o$V~!{St~XY!vYZ!v]^!vpq!v~#[P!_!`#_~#dOZ~~#iOd~~#nOi~~#sPb~!Q![#n~#{O]~~$QOY~~$VO[~P$[SWP!Q![$V!c!}$V#R#S$V#T#o$V~$mOg~~$rOj~R$wWWP!Q![$V!c!}$V#R#S$V#T#b$V#b#c%a#c#g$V#g#h&]#h#o$VR%fUWP!Q![$V!c!}$V#R#S$V#T#W$V#W#X%x#X#o$VR&PSkQWP!Q![$V!c!}$V#R#S$V#T#o$VR&dSnQWP!Q![$V!c!}$V#R#S$V#T#o$VR&uUWP!Q![$V!c!}$V#R#S$V#T#c$V#c#d'X#d#o$VR'^UWP!Q![$V!c!}$V#R#S$V#T#b$V#b#c'p#c#o$VR'uUWP!Q![$V!c!}$V#R#S$V#T#h$V#h#i(X#i#o$VR(^TWP!Q![$V!c!}$V#R#S$V#T#U(m#U#o$VR(rUWP!Q![$V!c!}$V#R#S$V#T#]$V#]#^)U#^#o$VR)ZUWP!Q![$V!c!}$V#R#S$V#T#b$V#b#c)m#c#o$VR)rUWP!Q![$V!c!}$V#R#S$V#T#g$V#g#h*U#h#o$VR*]S^QWP!Q![$V!c!}$V#R#S$V#T#o$VR*nUWP!Q![$V!c!}$V#R#S$V#T#b$V#b#c+Q#c#o$VR+VUWP!Q![$V!c!}$V#R#S$V#T#W$V#W#X+i#X#o$VR+nUWP!Q![$V!c!}$V#R#S$V#T#g$V#g#h,Q#h#o$VR,XS`QWP!Q![$V!c!}$V#R#S$V#T#o$VR,jUWP!Q![$V!c!}$V#R#S$V#T#b$V#b#c,|#c#o$VR-TSWPfQ!Q![$V!c!}$V#R#S$V#T#o$VR-fUWP!Q![$V!c!}$V#R#S$V#T#f$V#f#g-x#g#o$VR.PSWPlQ!Q![$V!c!}$V#R#S$V#T#o$VR.bUWP!Q![$V!c!}$V#R#S$V#T#h$V#h#i.t#i#o$VR.yTWP!Q![$V!c!}$V#R#S$V#T#U/Y#U#o$VR/_UWP!Q![$V!c!}$V#R#S$V#T#f$V#f#g/q#g#o$VR/vUWP!Q![$V!c!}$V#R#S$V#T#h$V#h#i0Y#i#o$VR0_UWP!Q![$V!c!}$V#R#S$V#T#g$V#g#h0q#h#o$VR0xSWP_Q!Q![$V!c!}$V#R#S$V#T#o$V",
|
|
13
|
+
tokenizers: [0, 1],
|
|
14
|
+
topRules: {"Filter":[0,1]},
|
|
15
|
+
tokenPrec: 0
|
|
16
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
|
2
|
+
export const
|
|
3
|
+
Filter = 1,
|
|
4
|
+
Expression = 2,
|
|
5
|
+
OrExpression = 3,
|
|
6
|
+
AndExpression = 4,
|
|
7
|
+
FilterClause = 5,
|
|
8
|
+
ColumnValueExpression = 6,
|
|
9
|
+
Column = 7,
|
|
10
|
+
Identifier = 8,
|
|
11
|
+
Operator = 9,
|
|
12
|
+
Eq = 10,
|
|
13
|
+
NotEq = 11,
|
|
14
|
+
Gt = 12,
|
|
15
|
+
Lt = 13,
|
|
16
|
+
Contains = 14,
|
|
17
|
+
Starts = 15,
|
|
18
|
+
Ends = 16,
|
|
19
|
+
Value = 17,
|
|
20
|
+
Int = 18,
|
|
21
|
+
String = 19,
|
|
22
|
+
Quote = 20,
|
|
23
|
+
ColumnSetExpression = 21,
|
|
24
|
+
In = 22,
|
|
25
|
+
LBrack = 23,
|
|
26
|
+
Values = 24,
|
|
27
|
+
Comma = 25,
|
|
28
|
+
RBrack = 26,
|
|
29
|
+
And = 27,
|
|
30
|
+
Or = 28,
|
|
31
|
+
AsClause = 29,
|
|
32
|
+
As = 30
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@top Filter { Expression }
|
|
2
|
+
|
|
3
|
+
Expression { OrExpression AsClause?}
|
|
4
|
+
|
|
5
|
+
OrExpression { AndExpression (Or AndExpression)* }
|
|
6
|
+
|
|
7
|
+
AndExpression { FilterClause ( And FilterClause )* }
|
|
8
|
+
|
|
9
|
+
AsClause { As Identifier }
|
|
10
|
+
|
|
11
|
+
FilterClause { ColumnValueExpression | ColumnSetExpression }
|
|
12
|
+
|
|
13
|
+
ColumnValueExpression { Column Operator Value }
|
|
14
|
+
|
|
15
|
+
ColumnSetExpression { Column In LBrack Values RBrack }
|
|
16
|
+
|
|
17
|
+
NumericColumn { "~"+ }
|
|
18
|
+
|
|
19
|
+
StringColumn { "$"+ }
|
|
20
|
+
|
|
21
|
+
Column { Identifier }
|
|
22
|
+
|
|
23
|
+
Operator { Eq | NotEq | Gt | Lt | Contains | Starts | Ends }
|
|
24
|
+
|
|
25
|
+
Values { Value (Comma Value)* }
|
|
26
|
+
|
|
27
|
+
Value { Identifier | Int | String }
|
|
28
|
+
|
|
29
|
+
String { Quote Identifier Quote }
|
|
30
|
+
|
|
31
|
+
@skip { space }
|
|
32
|
+
|
|
33
|
+
@tokens {
|
|
34
|
+
Contains { "contains" }
|
|
35
|
+
Starts { "starts" }
|
|
36
|
+
Ends { "ends" }
|
|
37
|
+
And { "and" }
|
|
38
|
+
Or { "or" }
|
|
39
|
+
As { "as" }
|
|
40
|
+
In { "in" }
|
|
41
|
+
Eq { "=" }
|
|
42
|
+
NotEq { "!=" }
|
|
43
|
+
Gt { ">" }
|
|
44
|
+
Lt { "<" }
|
|
45
|
+
Quote { "\""}
|
|
46
|
+
Comma { "," }
|
|
47
|
+
LBrack { "[" }
|
|
48
|
+
RBrack { "]" }
|
|
49
|
+
Identifier { $[a-zA-Z_]$[a-zA-Z_0-9]* }
|
|
50
|
+
Int { std.digit+ }
|
|
51
|
+
// String { '"' (!["\\] | "\\" _)* '"' }
|
|
52
|
+
space { $[ \t\n\r]+ }
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./FilterLanguage";
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Tree } from "@lezer/common";
|
|
2
|
+
import { Filter as FilterType } from "../../filterTypes";
|
|
3
|
+
|
|
4
|
+
class Filter {
|
|
5
|
+
#name: string | undefined;
|
|
6
|
+
expression?: OrExpression;
|
|
7
|
+
get lastExpression(): AndExpression | undefined {
|
|
8
|
+
if (this.expression) {
|
|
9
|
+
const { filters: f } = this.expression;
|
|
10
|
+
return f[f.length - 1];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
get lastFilterClause(): FilterClause | undefined {
|
|
14
|
+
const andExpression = this.lastExpression;
|
|
15
|
+
if (andExpression) {
|
|
16
|
+
const { filters: f } = andExpression;
|
|
17
|
+
return f[f.length - 1];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get name(): string | undefined {
|
|
22
|
+
return this.#name;
|
|
23
|
+
}
|
|
24
|
+
setName(value: string) {
|
|
25
|
+
this.#name = value;
|
|
26
|
+
}
|
|
27
|
+
toJson(): FilterType {
|
|
28
|
+
return this.expression?.toJson(this.#name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class OrExpression {
|
|
32
|
+
add(expression: AndExpression) {
|
|
33
|
+
this.filters.push(expression);
|
|
34
|
+
}
|
|
35
|
+
filters: any[] = [];
|
|
36
|
+
toJson(name?: string) {
|
|
37
|
+
if (this.filters.length === 1) {
|
|
38
|
+
return this.filters[0].toJson(name);
|
|
39
|
+
} else if (this.filters.length > 1) {
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
op: "or",
|
|
43
|
+
filters: this.filters.map((f) => f.toJson()),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class AndExpression {
|
|
49
|
+
add(expression: FilterClause) {
|
|
50
|
+
this.filters.push(expression);
|
|
51
|
+
}
|
|
52
|
+
filters: FilterClause[] = [];
|
|
53
|
+
toJson(name?: string) {
|
|
54
|
+
if (this.filters.length === 1) {
|
|
55
|
+
return this.filters[0].toJson(name);
|
|
56
|
+
} else if (this.filters.length > 1) {
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
op: "and",
|
|
60
|
+
filters: this.filters.map((f) => f.toJson()),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
class ColumnValueExpression {
|
|
66
|
+
column?: Column;
|
|
67
|
+
op?: string;
|
|
68
|
+
value?: string | number;
|
|
69
|
+
toJson(name?: string) {
|
|
70
|
+
return {
|
|
71
|
+
column: this.column?.name,
|
|
72
|
+
name,
|
|
73
|
+
op: this.op,
|
|
74
|
+
value: this.value,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class ColumnSetExpression {
|
|
79
|
+
column?: Column;
|
|
80
|
+
op?: "in";
|
|
81
|
+
values: (string | number)[] = [];
|
|
82
|
+
toJson(name?: string) {
|
|
83
|
+
return {
|
|
84
|
+
column: this.column?.name,
|
|
85
|
+
name,
|
|
86
|
+
op: "in",
|
|
87
|
+
values: this.values,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type FilterClause = ColumnValueExpression | ColumnSetExpression;
|
|
93
|
+
|
|
94
|
+
class Column {
|
|
95
|
+
constructor(public name: string, private from: number, private to: number) {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const peek = <T>(arr: unknown[]) => arr[arr.length - 1] as T;
|
|
99
|
+
|
|
100
|
+
export const walkTree = (tree: Tree, source: string) => {
|
|
101
|
+
const queue: unknown[] = [];
|
|
102
|
+
const cursor = tree.cursor();
|
|
103
|
+
next: do {
|
|
104
|
+
const { name, from, to } = cursor;
|
|
105
|
+
// console.log(
|
|
106
|
+
// `Node ${name} [${from}:${to}] '${source.substring(
|
|
107
|
+
// cursor.from,
|
|
108
|
+
// cursor.to
|
|
109
|
+
// )}'`
|
|
110
|
+
// );
|
|
111
|
+
|
|
112
|
+
switch (name) {
|
|
113
|
+
case "Filter":
|
|
114
|
+
queue.push(new Filter());
|
|
115
|
+
break;
|
|
116
|
+
case "OrExpression":
|
|
117
|
+
peek<Filter>(queue).expression = new OrExpression();
|
|
118
|
+
break;
|
|
119
|
+
case "AndExpression":
|
|
120
|
+
peek<Filter>(queue).expression?.add(new AndExpression());
|
|
121
|
+
break;
|
|
122
|
+
case "And":
|
|
123
|
+
case "In":
|
|
124
|
+
case "LBrack":
|
|
125
|
+
case "RBrack":
|
|
126
|
+
case "Quote":
|
|
127
|
+
case "Comma":
|
|
128
|
+
case "Value":
|
|
129
|
+
case "Values":
|
|
130
|
+
case "FilterClause":
|
|
131
|
+
// nothing to do
|
|
132
|
+
break;
|
|
133
|
+
case "ColumnValueExpression":
|
|
134
|
+
{
|
|
135
|
+
const lastExpression = peek<Filter>(queue).lastExpression;
|
|
136
|
+
if (lastExpression) {
|
|
137
|
+
lastExpression.add(new ColumnValueExpression());
|
|
138
|
+
} else {
|
|
139
|
+
throw Error("boo");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case "ColumnSetExpression":
|
|
145
|
+
{
|
|
146
|
+
const lastExpression = peek<Filter>(queue).lastExpression;
|
|
147
|
+
if (lastExpression) {
|
|
148
|
+
lastExpression.add(new ColumnSetExpression());
|
|
149
|
+
} else {
|
|
150
|
+
throw Error("hoo");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case "Column":
|
|
156
|
+
{
|
|
157
|
+
const lastFilterClause = peek<Filter>(queue).lastFilterClause;
|
|
158
|
+
if (lastFilterClause) {
|
|
159
|
+
const value = source.substring(from, to);
|
|
160
|
+
lastFilterClause.column = new Column(value, from, to);
|
|
161
|
+
cursor.next();
|
|
162
|
+
} else {
|
|
163
|
+
throw Error("wah");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case "Operator":
|
|
169
|
+
{
|
|
170
|
+
const lastFilterClause = peek<Filter>(queue).lastFilterClause;
|
|
171
|
+
if (lastFilterClause) {
|
|
172
|
+
const value = source.substring(from, to);
|
|
173
|
+
lastFilterClause.op = value;
|
|
174
|
+
cursor.next();
|
|
175
|
+
} else {
|
|
176
|
+
throw Error("wah");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
case "String":
|
|
184
|
+
// skip the open and close quote
|
|
185
|
+
cursor.next();
|
|
186
|
+
cursor.next();
|
|
187
|
+
cursor.next();
|
|
188
|
+
// fall thru, intentional
|
|
189
|
+
|
|
190
|
+
// eslint-disable-next-line no-fallthrough
|
|
191
|
+
case "Int":
|
|
192
|
+
{
|
|
193
|
+
const lastFilterClause = peek<Filter>(queue).lastFilterClause;
|
|
194
|
+
if (lastFilterClause) {
|
|
195
|
+
const value = source.substring(from, to);
|
|
196
|
+
const typedValue = name === "Int" ? parseInt(value, 10) : value;
|
|
197
|
+
if (lastFilterClause instanceof ColumnSetExpression) {
|
|
198
|
+
lastFilterClause.values.push(typedValue);
|
|
199
|
+
} else {
|
|
200
|
+
lastFilterClause.value = typedValue;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
throw Error("wah");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case "Or":
|
|
209
|
+
{
|
|
210
|
+
// lets p0retend for now that Or clauses only occur at te top
|
|
211
|
+
peek<Filter>(queue).expression?.add(new AndExpression());
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case "AsClause":
|
|
216
|
+
{
|
|
217
|
+
cursor.next();
|
|
218
|
+
const { name: nm1 } = cursor;
|
|
219
|
+
if (nm1 === "As") {
|
|
220
|
+
cursor.next();
|
|
221
|
+
const { name: nm2, from: f2, to: t2 } = cursor;
|
|
222
|
+
if (nm2 === "Identifier") {
|
|
223
|
+
peek<Filter>(queue)?.setName(source.substring(f2, t2));
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
continue next;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
default:
|
|
232
|
+
// console.log(`%cwhat do we do with a ${name}`, "color:red");
|
|
233
|
+
}
|
|
234
|
+
} while (cursor.next());
|
|
235
|
+
|
|
236
|
+
return queue[0] as Filter;
|
|
237
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
|
2
|
+
import { tags } from "@lezer/highlight";
|
|
3
|
+
|
|
4
|
+
const myHighlightStyle = HighlightStyle.define([
|
|
5
|
+
{ tag: tags.variableName, color: "var(--vuuFilterEditor-variableColor)" },
|
|
6
|
+
{ tag: tags.comment, color: "green", fontStyle: "italic" },
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const vuuHighlighting = syntaxHighlighting(myHighlightStyle);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
|
|
3
|
+
export const vuuTheme = EditorView.theme(
|
|
4
|
+
{
|
|
5
|
+
"&": {
|
|
6
|
+
color: "var(--vuuFilterEditor-color)",
|
|
7
|
+
backgroundColor: "var(--vuuFilterEditor-background)",
|
|
8
|
+
fontSize: "var(--vuuFilterEditor-fontSize)",
|
|
9
|
+
},
|
|
10
|
+
".cm-content": {
|
|
11
|
+
caretColor: "var(--vuuFilterEditor-cursorColor)",
|
|
12
|
+
padding: 0,
|
|
13
|
+
},
|
|
14
|
+
".cm-line": {
|
|
15
|
+
lineHeight: "var(--vuuFilterEditor-lineHeight)",
|
|
16
|
+
},
|
|
17
|
+
"&.cm-focused .cm-cursor": {
|
|
18
|
+
borderLeftColor: "var(--vuuFilterEditor-cursorColor)",
|
|
19
|
+
},
|
|
20
|
+
"&.cm-focused .cm-selectionBackground, ::selection": {
|
|
21
|
+
backgroundColor: "var(--vuuFilterEditor-selectionBackground)",
|
|
22
|
+
},
|
|
23
|
+
".cm-selectionBackground, ::selection": {
|
|
24
|
+
backgroundColor: "var(--vuuFilterEditor-selectionBackground)",
|
|
25
|
+
},
|
|
26
|
+
".cm-scroller": {
|
|
27
|
+
fontFamily: "var(--vuuFilterEditor-fontFamily)",
|
|
28
|
+
},
|
|
29
|
+
".cm-tooltip": {
|
|
30
|
+
background: "var(--vuuFilterEditor-tooltipBackground)",
|
|
31
|
+
border: "var(--vuuFilterEditor-tooltipBorder)",
|
|
32
|
+
boxShadow: "var(--vuuFilterEditor-tooltipElevation)",
|
|
33
|
+
"&.cm-tooltip-autocomplete > ul": {
|
|
34
|
+
fontFamily: "var(--vuuFilterEditor-fontFamily)",
|
|
35
|
+
fontSize: "var(--vuuFilterEditor-fontSize)",
|
|
36
|
+
maxHeight: "240px",
|
|
37
|
+
},
|
|
38
|
+
"&.cm-tooltip-autocomplete > ul > li": {
|
|
39
|
+
height: "var(--vuuFilterEditor-suggestion-height)",
|
|
40
|
+
padding: "0 3px",
|
|
41
|
+
lineHeight: "var(--vuuFilterEditor-suggestion-height)",
|
|
42
|
+
},
|
|
43
|
+
"&.cm-tooltip-autocomplete li[aria-selected]": {
|
|
44
|
+
background: "var(--vuuFilterEditor-suggestion-selectedBackground)",
|
|
45
|
+
color: "var(--vuuFilterEditor-suggestion-selectedColor)",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ dark: false }
|
|
50
|
+
);
|