@1771technologies/lytenyte-pro 2.0.4-dev.4 → 2.1.1-dev.0
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/README.md +55 -12
- package/css/components/expression-editor.css +2 -1
- package/dist/components/column-manager/column-manager.d.ts +2 -1
- package/dist/components/column-manager/column-manager.js +2 -2
- package/dist/components/expressions-editor/create-completion-provider.d.ts +8 -1
- package/dist/components/expressions-editor/create-completion-provider.js +273 -43
- package/dist/components/expressions-editor/cursor-position/use-cursor-position.d.ts +2 -2
- package/dist/components/expressions-editor/cursor-position/use-cursor-position.js +24 -8
- package/dist/components/expressions-editor/expression-editor.js +29 -9
- package/dist/components/expressions-editor/index.d.ts +1 -0
- package/dist/components/expressions-editor/intellisence/completion-context.d.ts +3 -2
- package/dist/components/expressions-editor/intellisence/completion-popover.js +20 -3
- package/dist/components/expressions-editor/token-highlighter-default.js +2 -0
- package/dist/components/expressions-editor/types.d.ts +1 -0
- package/dist/components/expressions-editor/use-keyboard-navigation.d.ts +2 -2
- package/dist/components/expressions-editor/use-keyboard-navigation.js +25 -4
- package/dist/data-source-client/hooks/use-flattened-groups.js +2 -1
- package/dist/expressions/index.d.ts +3 -1
- package/dist/expressions/index.js +3 -1
- package/dist/expressions/parser/types.d.ts +5 -1
- package/dist/expressions/plugins/date-identifier.d.ts +4 -0
- package/dist/expressions/plugins/date-identifier.js +29 -0
- package/dist/expressions/plugins/date-literal.d.ts +2 -0
- package/dist/expressions/plugins/date-literal.js +71 -0
- package/dist/expressions/plugins/quoted-identifier.d.ts +2 -0
- package/dist/expressions/plugins/quoted-identifier.js +35 -0
- package/dist/expressions/plugins/standard.d.ts +3 -1
- package/dist/expressions/plugins/standard.js +5 -1
- package/package.json +6 -5
- package/dist/components/expressions-editor/(expressions)/expression.play.d.ts +0 -4
- package/dist/components/expressions-editor/(expressions)/expression.play.js +0 -69
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ to **LyteNyte Grid PRO** for advanced enterprise features and support.
|
|
|
35
35
|
[](https://www.npmjs.com/package/@1771technologies/lytenyte-core)
|
|
36
36
|
[](https://github.com/1771-Technologies/lytenyte/tree/main)
|
|
37
37
|
[](https://github.com/1771-Technologies/lytenyte)
|
|
38
|
-
[](
|
|
38
|
+
[](https://github.com/1771-Technologies/lytenyte/blob/main/CODE_OF_CONDUCT.md)
|
|
39
39
|
[](https://github.com/1771-Technologies/lytenyte/actions/workflows/release.yml?query=branch%3Amain)
|
|
40
40
|
[](https://x.com/1771tech)
|
|
41
41
|
[](https://www.linkedin.com/company/1771technologies)
|
|
@@ -55,14 +55,34 @@ to **LyteNyte Grid PRO** for advanced enterprise features and support.
|
|
|
55
55
|
|
|
56
56
|
LyteNyte Grid is available in two editions:
|
|
57
57
|
|
|
58
|
-
1. **LyteNyte Grid Core
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
1. **LyteNyte Grid Core:** Free, **open-source edition** for building high-performance data tables. Includes sorting,
|
|
59
|
+
filtering, editing, row selection, detail views, and more. Also includes advanced features such
|
|
60
|
+
as aggregation, row grouping, and cell range selection, often locked behind paywalls in other grids.
|
|
61
61
|
|
|
62
|
-
2. **LyteNyte Grid PRO: Commercial edition** that extends Core with powerful enterprise
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
2. **LyteNyte Grid PRO: Commercial edition** that extends Core with powerful enterprise features,
|
|
63
|
+
including server-side data loading, pivoting, tree data, component managers, and advanced
|
|
64
|
+
filtering. Build custom menus, popovers, expressions, and workflow-driven UIs with ease.
|
|
65
|
+
|
|
66
|
+
## AI-Enhanced Workflows
|
|
67
|
+
|
|
68
|
+
**LyteNyte Grid AI Skills** are structured context files for Claude Code, Windsurf, Cursor, and other AI coding agents.
|
|
69
|
+
Skills provide agents with accurate, curated context for LyteNyte Grid, allowing them to generate complex
|
|
70
|
+
grid implementations with greater speed and reliability.
|
|
71
|
+
|
|
72
|
+
Instead of building the grid manually, developers can describe the
|
|
73
|
+
desired result and let the agent generate the implementation.
|
|
74
|
+
|
|
75
|
+
**Install LyteNyte Grid AI Skills**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx skills add 1771-Technologies/lytenyte
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Once installed, the Skill activates automatically whenever you work on a LyteNyte Grid task. No
|
|
82
|
+
special prompting is needed. Skills are actively maintained by the 1771 Technologies
|
|
83
|
+
team and versioned alongside each LyteNyte Grid release.
|
|
84
|
+
|
|
85
|
+
See our [AI Skills Overview guide](https://1771technologies.com/docs/ai-skills-overview) to learn more.
|
|
66
86
|
|
|
67
87
|
## Features
|
|
68
88
|
|
|
@@ -79,6 +99,16 @@ LyteNyte Grid is available in two editions:
|
|
|
79
99
|
|
|
80
100
|
</details>
|
|
81
101
|
|
|
102
|
+
<details>
|
|
103
|
+
<summary><b>🤖 AI Enhanced Workflows</b></summary>
|
|
104
|
+
<br>
|
|
105
|
+
|
|
106
|
+
| Feature | Core | PRO |
|
|
107
|
+
| --------------------------------------------------------------------- | :--: | :-: |
|
|
108
|
+
| [AI Skills](https://www.1771technologies.com/docs/ai-skills-overview) | ✅ | ✅ |
|
|
109
|
+
|
|
110
|
+
</details>
|
|
111
|
+
|
|
82
112
|
<details>
|
|
83
113
|
<summary><b>🔌 API Extensions</b></summary>
|
|
84
114
|
<br>
|
|
@@ -198,8 +228,8 @@ LyteNyte Grid is available in two editions:
|
|
|
198
228
|
| ↳ [Checkbox Row Selection](https://www.1771technologies.com/docs/row-selection#checkbox-selection) | ✅ | ✅ |
|
|
199
229
|
| ↳ [Isolated Row Selection](https://www.1771technologies.com/docs/row-selection#isolated-row-selection) | ✅ | ✅ |
|
|
200
230
|
| ↳ [Linked Row Selection](https://www.1771technologies.com/docs/row-selection#linked-row-selection) | ✅ | ✅ |
|
|
201
|
-
| [Cell Selection](https://www.1771technologies.com/docs/cell-selection#single-range-selection) |
|
|
202
|
-
| [Cell Range Selection](https://www.1771technologies.com/docs/cell-selection#multi-range-selection) |
|
|
231
|
+
| [Cell Selection](https://www.1771technologies.com/docs/cell-selection#single-range-selection) | ✅ | ✅ |
|
|
232
|
+
| [Cell Range Selection](https://www.1771technologies.com/docs/cell-selection#multi-range-selection) | ✅ | ✅ |
|
|
203
233
|
|
|
204
234
|
</details>
|
|
205
235
|
|
|
@@ -239,6 +269,19 @@ LyteNyte Grid is available in two editions:
|
|
|
239
269
|
|
|
240
270
|
</details>
|
|
241
271
|
|
|
272
|
+
<details>
|
|
273
|
+
<summary><b>🧮 Expressions</b> </summary>
|
|
274
|
+
<br>
|
|
275
|
+
|
|
276
|
+
| Feature | Core | PRO |
|
|
277
|
+
| -------------------------------------------------------------------------------- | :--: | :-: |
|
|
278
|
+
| [Expressions Engine](https://www.1771technologies.com/docs/expressions-overview) | — | ✅ |
|
|
279
|
+
| [Filter Expressions](https://www.1771technologies.com/docs/expression-filters) | — | ✅ |
|
|
280
|
+
| [Expression Editor](https://www.1771technologies.com/docs/expression-editor) | — | ✅ |
|
|
281
|
+
| [Expression Plugins](https://www.1771technologies.com/docs/expression-plugins) | — | ✅ |
|
|
282
|
+
|
|
283
|
+
</details>
|
|
284
|
+
|
|
242
285
|
<details>
|
|
243
286
|
<summary><b>📦 Grouping & Aggregations</b></summary>
|
|
244
287
|
<br>
|
|
@@ -336,7 +379,7 @@ LyteNyte Grid is available in two editions:
|
|
|
336
379
|
| ↳ [CSV Export](https://www.1771technologies.com/docs/export-csv) | ✅ | ✅ |
|
|
337
380
|
| ↳ [Parquet Export](https://www.1771technologies.com/docs/export-parquet) | ✅ | ✅ |
|
|
338
381
|
| ↳ [Arrow Export](https://www.1771technologies.com/docs/export-arrow) | ✅ | ✅ |
|
|
339
|
-
| [Clipboard Operations](https://www.1771technologies.com/docs/export-clipboard) |
|
|
382
|
+
| [Clipboard Operations](https://www.1771technologies.com/docs/export-clipboard) | ✅ | ✅ |
|
|
340
383
|
|
|
341
384
|
</details>
|
|
342
385
|
|
|
@@ -377,7 +420,7 @@ npm install --save @1771technologies/lytenyte-pro
|
|
|
377
420
|
|
|
378
421
|
> [!NOTE]
|
|
379
422
|
> You can install and use LyteNyte Grid PRO
|
|
380
|
-
> without license but a watermark will be displayed.
|
|
423
|
+
> without a license, but a watermark will be displayed.
|
|
381
424
|
|
|
382
425
|
**Core:**
|
|
383
426
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
[data-ln-expression-editor] {
|
|
3
3
|
--ln-expr-number: light-dark(#b45309, #fab387);
|
|
4
4
|
--ln-expr-string: light-dark(#15803d, #a6e3a1);
|
|
5
|
+
--ln-expr-date: light-dark(#0e7490, #89dceb);
|
|
5
6
|
--ln-expr-keyword: light-dark(#7c3aed, #cba6f7);
|
|
6
7
|
--ln-expr-identifier: light-dark(#0369a1, #89b4fa);
|
|
7
8
|
--ln-expr-operator: light-dark(#be185d, #f38ba8);
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
[data-ln-expression-editor] {
|
|
14
15
|
font-family:
|
|
15
16
|
ui-monospace, "Cascadia Code", "Segoe UI Mono", "Liberation Mono", Menlo, Monaco, Consolas, monospace;
|
|
16
|
-
font-size:
|
|
17
|
+
font-size: var(--ln-font-sm);
|
|
17
18
|
white-space: nowrap;
|
|
18
19
|
word-break: normal;
|
|
19
20
|
overflow-wrap: normal;
|
|
@@ -5,6 +5,7 @@ export interface ColumnManagerProps<Spec extends GridSpec = GridSpec> {
|
|
|
5
5
|
readonly columns: Column<Spec>[];
|
|
6
6
|
readonly onColumnsChange: (change: Column<Spec>[]) => void;
|
|
7
7
|
readonly base?: Grid.ColumnBase<Spec>;
|
|
8
|
+
readonly rowHeight?: number;
|
|
8
9
|
readonly endElement?: (params: {
|
|
9
10
|
columns: Column<Spec>[];
|
|
10
11
|
row: RowNode<Spec["data"]>;
|
|
@@ -15,4 +16,4 @@ export interface ColumnManagerProps<Spec extends GridSpec = GridSpec> {
|
|
|
15
16
|
*/
|
|
16
17
|
readonly getPillManagerTag?: (column: Column<Spec>) => Record<string, string>;
|
|
17
18
|
}
|
|
18
|
-
export declare function ColumnManager<Spec extends GridSpec = GridSpec>({ columns: provided, base, onColumnsChange, endElement, getPillManagerTag, }: ColumnManagerProps<Spec>): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function ColumnManager<Spec extends GridSpec = GridSpec>({ columns: provided, base, rowHeight, onColumnsChange, endElement, getPillManagerTag, }: ColumnManagerProps<Spec>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -3,7 +3,7 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { TreeView } from "../tree-view/index.js";
|
|
4
4
|
import { Checkbox } from "../checkbox/checkbox.js";
|
|
5
5
|
import { computePathMatrix } from "@1771technologies/lytenyte-shared";
|
|
6
|
-
export function ColumnManager({ columns: provided, base, onColumnsChange, endElement, getPillManagerTag, }) {
|
|
6
|
+
export function ColumnManager({ columns: provided, base, rowHeight = 30, onColumnsChange, endElement, getPillManagerTag, }) {
|
|
7
7
|
const nonAdjacentSplit = useMemo(() => {
|
|
8
8
|
const paths = computePathMatrix(provided);
|
|
9
9
|
const adjusted = [];
|
|
@@ -45,7 +45,7 @@ export function ColumnManager({ columns: provided, base, onColumnsChange, endEle
|
|
|
45
45
|
};
|
|
46
46
|
};
|
|
47
47
|
}, [getPillManagerTag]);
|
|
48
|
-
return (_jsx(TreeView, { items: items, rowSelectionEnabled: false, draggable: true, rowHeight:
|
|
48
|
+
return (_jsx(TreeView, { items: items, rowSelectionEnabled: false, draggable: true, rowHeight: rowHeight, rowSelectAllShow: false, getDragTags: getDragItems, defaultExpansion: true, onItemsReordered: (x) => {
|
|
49
49
|
const columns = x.map((x) => provided.find((p) => p.id === x.column.id));
|
|
50
50
|
onColumnsChange(columns);
|
|
51
51
|
}, children: ({ row, leafs, toggle, dragProps, isOver, isBefore }) => {
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { Token } from "../../expressions/lexer/types.js";
|
|
2
2
|
import type { CompletionItem } from "./types.js";
|
|
3
|
-
export
|
|
3
|
+
export type ContextEntry = {
|
|
4
|
+
value: unknown;
|
|
5
|
+
type: "string" | "number" | "array" | "object" | "function" | "boolean" | "date";
|
|
6
|
+
return?: "string" | "number" | "array" | "object" | "date";
|
|
7
|
+
};
|
|
8
|
+
type CompletionContext = Record<string, ContextEntry>;
|
|
9
|
+
export declare function createCompletionProvider(context: CompletionContext): (tokens: Token[], cursorPosition: number) => CompletionItem[];
|
|
10
|
+
export {};
|
|
@@ -48,28 +48,250 @@ const ARRAY_METHODS = [
|
|
|
48
48
|
{ label: "sort", kind: "function" },
|
|
49
49
|
{ label: "values", kind: "function" },
|
|
50
50
|
];
|
|
51
|
+
const DATE_METHODS = [
|
|
52
|
+
{ label: "getTime", kind: "function" },
|
|
53
|
+
{ label: "toISOString", kind: "function" },
|
|
54
|
+
];
|
|
55
|
+
const NUMBER_METHODS = [
|
|
56
|
+
{ label: "toExponential", kind: "function" },
|
|
57
|
+
{ label: "toFixed", kind: "function" },
|
|
58
|
+
{ label: "toLocaleString", kind: "function" },
|
|
59
|
+
{ label: "toPrecision", kind: "function" },
|
|
60
|
+
{ label: "toString", kind: "function" },
|
|
61
|
+
{ label: "valueOf", kind: "function" },
|
|
62
|
+
];
|
|
51
63
|
function isDot(token) {
|
|
52
64
|
return (token.type === "Punctuation" && token.value === ".") || token.type === "OptionalChain";
|
|
53
65
|
}
|
|
54
66
|
function isClosingBracket(token) {
|
|
55
67
|
return token.type === "Punctuation" && token.value === "]";
|
|
56
68
|
}
|
|
69
|
+
function isCompleteValue(token) {
|
|
70
|
+
return (token.type === "Identifier" ||
|
|
71
|
+
token.type === "QuotedIdentifier" ||
|
|
72
|
+
token.type === "Number" ||
|
|
73
|
+
token.type === "String" ||
|
|
74
|
+
token.type === "TemplateLiteral" ||
|
|
75
|
+
(token.type === "Punctuation" && (token.value === ")" || token.value === "]")));
|
|
76
|
+
}
|
|
57
77
|
function isStringLiteral(token) {
|
|
58
78
|
return token.type === "String" || token.type === "TemplateLiteral";
|
|
59
79
|
}
|
|
60
|
-
|
|
80
|
+
const BINARY_OPERATORS = [
|
|
81
|
+
{ label: "+ Plus", kind: "operator", value: "+" },
|
|
82
|
+
{ label: "- Minus", kind: "operator", value: "-" },
|
|
83
|
+
{ label: "* Multiply", kind: "operator", value: "*" },
|
|
84
|
+
{ label: "/ Divide", kind: "operator", value: "/" },
|
|
85
|
+
{ label: "% Modulus", kind: "operator", value: "%" },
|
|
86
|
+
{ label: "** Exponentiation", kind: "operator", value: "**" },
|
|
87
|
+
{ label: "== Equal To", kind: "operator", value: "==" },
|
|
88
|
+
{ label: "!= Not Equal To", kind: "operator", value: "!=" },
|
|
89
|
+
{ label: "< Less Than", kind: "operator", value: "<" },
|
|
90
|
+
{ label: "<= Less Than Or Equal To", kind: "operator", value: "<=" },
|
|
91
|
+
{ label: "> Greater Than", kind: "operator", value: ">" },
|
|
92
|
+
{ label: ">= Greater Than Or Equal To", kind: "operator", value: ">=" },
|
|
93
|
+
{ label: "&& AND", kind: "operator", value: "&&" },
|
|
94
|
+
{ label: "|| OR", kind: "operator", value: "||" },
|
|
95
|
+
{ label: "?? OR if Null", kind: "operator", value: "??" },
|
|
96
|
+
{ label: "|> Pipe", kind: "operator", value: "|>" },
|
|
97
|
+
];
|
|
98
|
+
const STRING_METHOD_RETURNS = {
|
|
99
|
+
at: "string",
|
|
100
|
+
charAt: "string",
|
|
101
|
+
charCodeAt: "number",
|
|
102
|
+
endsWith: "unknown",
|
|
103
|
+
includes: "unknown",
|
|
104
|
+
indexOf: "number",
|
|
105
|
+
lastIndexOf: "number",
|
|
106
|
+
match: "array",
|
|
107
|
+
padEnd: "string",
|
|
108
|
+
padStart: "string",
|
|
109
|
+
repeat: "string",
|
|
110
|
+
replace: "string",
|
|
111
|
+
replaceAll: "string",
|
|
112
|
+
slice: "string",
|
|
113
|
+
split: "array",
|
|
114
|
+
startsWith: "unknown",
|
|
115
|
+
substring: "string",
|
|
116
|
+
toLowerCase: "string",
|
|
117
|
+
toUpperCase: "string",
|
|
118
|
+
trim: "string",
|
|
119
|
+
trimEnd: "string",
|
|
120
|
+
trimStart: "string",
|
|
121
|
+
};
|
|
122
|
+
const ARRAY_METHOD_RETURNS = {
|
|
123
|
+
at: "unknown",
|
|
124
|
+
concat: "array",
|
|
125
|
+
entries: "unknown",
|
|
126
|
+
every: "unknown",
|
|
127
|
+
filter: "array",
|
|
128
|
+
find: "unknown",
|
|
129
|
+
findIndex: "number",
|
|
130
|
+
flat: "array",
|
|
131
|
+
flatMap: "array",
|
|
132
|
+
forEach: "unknown",
|
|
133
|
+
includes: "unknown",
|
|
134
|
+
indexOf: "number",
|
|
135
|
+
join: "string",
|
|
136
|
+
keys: "unknown",
|
|
137
|
+
map: "array",
|
|
138
|
+
reduce: "unknown",
|
|
139
|
+
reduceRight: "unknown",
|
|
140
|
+
reverse: "array",
|
|
141
|
+
slice: "array",
|
|
142
|
+
some: "unknown",
|
|
143
|
+
sort: "array",
|
|
144
|
+
values: "unknown",
|
|
145
|
+
};
|
|
146
|
+
const DATE_METHOD_RETURNS = {
|
|
147
|
+
getTime: "number",
|
|
148
|
+
toISOString: "string",
|
|
149
|
+
};
|
|
150
|
+
function kindOf(value) {
|
|
151
|
+
if (typeof value === "function")
|
|
152
|
+
return "function";
|
|
153
|
+
if (Array.isArray(value))
|
|
154
|
+
return "array";
|
|
155
|
+
if (typeof value === "string")
|
|
156
|
+
return "string";
|
|
157
|
+
if (typeof value === "number")
|
|
158
|
+
return "number";
|
|
159
|
+
if (typeof value === "boolean")
|
|
160
|
+
return "boolean";
|
|
161
|
+
if (typeof value === "object" && value !== null)
|
|
162
|
+
return "object";
|
|
163
|
+
return "unknown";
|
|
164
|
+
}
|
|
165
|
+
function isContextEntry(v) {
|
|
166
|
+
return v != null && typeof v === "object" && "type" in v && "value" in v;
|
|
167
|
+
}
|
|
168
|
+
function resolveEntry(context, path) {
|
|
169
|
+
if (path.length === 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
let entry = context[path[0]];
|
|
172
|
+
for (let i = 1; i < path.length; i++) {
|
|
173
|
+
if (!entry || entry.value == null || typeof entry.value !== "object")
|
|
174
|
+
return undefined;
|
|
175
|
+
const nested = entry.value[path[i]];
|
|
176
|
+
if (isContextEntry(nested)) {
|
|
177
|
+
entry = nested;
|
|
178
|
+
}
|
|
179
|
+
else if (nested !== undefined) {
|
|
180
|
+
entry = { value: nested, type: kindOf(nested) };
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return entry;
|
|
187
|
+
}
|
|
188
|
+
function findMatchingOpenParen(relevant, closeIndex) {
|
|
189
|
+
let depth = 1;
|
|
190
|
+
let i = closeIndex - 1;
|
|
191
|
+
while (i >= 0) {
|
|
192
|
+
if (relevant[i].type === "Punctuation") {
|
|
193
|
+
if (relevant[i].value === ")")
|
|
194
|
+
depth++;
|
|
195
|
+
else if (relevant[i].value === "(") {
|
|
196
|
+
if (--depth === 0)
|
|
197
|
+
return i;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
i--;
|
|
201
|
+
}
|
|
202
|
+
return -1;
|
|
203
|
+
}
|
|
204
|
+
function buildPathFromTokens(relevant, i) {
|
|
205
|
+
const tokenName = (t) => (t.type === "QuotedIdentifier" ? t.value.slice(2, -1) : t.value);
|
|
206
|
+
const token = relevant[i];
|
|
207
|
+
if (!token || (token.type !== "Identifier" && token.type !== "QuotedIdentifier"))
|
|
208
|
+
return null;
|
|
209
|
+
const path = [tokenName(token)];
|
|
210
|
+
let j = i - 1;
|
|
211
|
+
while (j >= 1 && isDot(relevant[j])) {
|
|
212
|
+
j--;
|
|
213
|
+
if (relevant[j].type !== "Identifier" && relevant[j].type !== "QuotedIdentifier")
|
|
214
|
+
break;
|
|
215
|
+
path.unshift(tokenName(relevant[j]));
|
|
216
|
+
j--;
|
|
217
|
+
}
|
|
218
|
+
return path;
|
|
219
|
+
}
|
|
220
|
+
function resolveTypeOf(relevant, i, context) {
|
|
221
|
+
const token = relevant[i];
|
|
222
|
+
if (!token)
|
|
223
|
+
return "unknown";
|
|
224
|
+
if (token.type === "DateLiteral")
|
|
225
|
+
return "date";
|
|
226
|
+
if (isStringLiteral(token))
|
|
227
|
+
return "string";
|
|
228
|
+
if (isClosingBracket(token))
|
|
229
|
+
return "array";
|
|
230
|
+
if (token.type === "Punctuation" && token.value === ")") {
|
|
231
|
+
const openIdx = findMatchingOpenParen(relevant, i);
|
|
232
|
+
if (openIdx < 1)
|
|
233
|
+
return "unknown";
|
|
234
|
+
const methodToken = relevant[openIdx - 1];
|
|
235
|
+
if (methodToken?.type !== "Identifier")
|
|
236
|
+
return "unknown";
|
|
237
|
+
const hasDot = openIdx >= 2 && isDot(relevant[openIdx - 2]);
|
|
238
|
+
if (!hasDot) {
|
|
239
|
+
// Top-level function call: fn(...)
|
|
240
|
+
const funcEntry = context[methodToken.value];
|
|
241
|
+
if (funcEntry?.type === "function" && funcEntry.return)
|
|
242
|
+
return funcEntry.return;
|
|
243
|
+
return "unknown";
|
|
244
|
+
}
|
|
245
|
+
// Method call: receiver.method(...)
|
|
246
|
+
// First try resolving the receiver as a context object to find a typed method entry
|
|
247
|
+
const receiverPath = buildPathFromTokens(relevant, openIdx - 3);
|
|
248
|
+
if (receiverPath) {
|
|
249
|
+
const receiverEntry = resolveEntry(context, receiverPath);
|
|
250
|
+
if (receiverEntry?.type === "object" && receiverEntry.value != null) {
|
|
251
|
+
const methodVal = receiverEntry.value[methodToken.value];
|
|
252
|
+
const methodEntry = isContextEntry(methodVal) ? methodVal : undefined;
|
|
253
|
+
if (methodEntry?.type === "function" && methodEntry.return)
|
|
254
|
+
return methodEntry.return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Fall back to builtin method return-type maps (string/array prototype methods)
|
|
258
|
+
const receiverType = resolveTypeOf(relevant, openIdx - 3, context);
|
|
259
|
+
if (receiverType === "string")
|
|
260
|
+
return STRING_METHOD_RETURNS[methodToken.value] ?? "unknown";
|
|
261
|
+
if (receiverType === "array")
|
|
262
|
+
return ARRAY_METHOD_RETURNS[methodToken.value] ?? "unknown";
|
|
263
|
+
if (receiverType === "date")
|
|
264
|
+
return DATE_METHOD_RETURNS[methodToken.value] ?? "unknown";
|
|
265
|
+
return "unknown";
|
|
266
|
+
}
|
|
267
|
+
if (token.type === "Identifier" || token.type === "QuotedIdentifier") {
|
|
268
|
+
const path = buildPathFromTokens(relevant, i);
|
|
269
|
+
if (!path)
|
|
270
|
+
return "unknown";
|
|
271
|
+
const entry = resolveEntry(context, path);
|
|
272
|
+
const kind = entry?.type ?? "unknown";
|
|
273
|
+
if (kind === "string" || kind === "number" || kind === "array" || kind === "object")
|
|
274
|
+
return kind;
|
|
275
|
+
return "unknown";
|
|
276
|
+
}
|
|
277
|
+
return "unknown";
|
|
278
|
+
}
|
|
279
|
+
function analyzeTokens(tokens, cursorPosition, context) {
|
|
61
280
|
const relevant = tokens.filter((t) => t.end <= cursorPosition && t.type !== "EOF" && t.type !== "Whitespace");
|
|
62
281
|
if (relevant.length === 0)
|
|
63
282
|
return { kind: "top-level" };
|
|
64
283
|
let i = relevant.length - 1;
|
|
65
|
-
// Skip the partial word the user is currently typing
|
|
66
|
-
if (relevant[i].type === "Identifier")
|
|
284
|
+
// Skip the partial word the user is currently typing (only if cursor is at its end)
|
|
285
|
+
if (relevant[i].type === "Identifier" && relevant[i].end === cursorPosition)
|
|
67
286
|
i--;
|
|
68
287
|
if (i < 0)
|
|
69
288
|
return { kind: "top-level" };
|
|
70
|
-
// If the token before the word isn't a dot,
|
|
71
|
-
if (!isDot(relevant[i]))
|
|
289
|
+
// If the token before the word isn't a dot, check if it's a complete value
|
|
290
|
+
if (!isDot(relevant[i])) {
|
|
291
|
+
if (isCompleteValue(relevant[i]))
|
|
292
|
+
return { kind: "after-value" };
|
|
72
293
|
return { kind: "top-level" };
|
|
294
|
+
}
|
|
73
295
|
// Step over the dot
|
|
74
296
|
i--;
|
|
75
297
|
if (i < 0)
|
|
@@ -81,79 +303,87 @@ function analyzeTokens(tokens, cursorPosition) {
|
|
|
81
303
|
// [1,2,3]. → array methods
|
|
82
304
|
if (isClosingBracket(beforeDot))
|
|
83
305
|
return { kind: "array-literal" };
|
|
84
|
-
//
|
|
85
|
-
if (beforeDot.type === "
|
|
86
|
-
|
|
306
|
+
// d"2023-02-11". → date methods
|
|
307
|
+
if (beforeDot.type === "DateLiteral")
|
|
308
|
+
return { kind: "date-value" };
|
|
309
|
+
// fn(). → resolve call return type recursively
|
|
310
|
+
if (beforeDot.type === "Punctuation" && beforeDot.value === ")") {
|
|
311
|
+
const resultType = resolveTypeOf(relevant, i, context);
|
|
312
|
+
if (resultType === "string")
|
|
313
|
+
return { kind: "string-literal" };
|
|
314
|
+
if (resultType === "number")
|
|
315
|
+
return { kind: "number-value" };
|
|
316
|
+
if (resultType === "array")
|
|
317
|
+
return { kind: "array-literal" };
|
|
318
|
+
if (resultType === "date")
|
|
319
|
+
return { kind: "date-value" };
|
|
320
|
+
return { kind: "none" };
|
|
321
|
+
}
|
|
322
|
+
// identifier chain: walk back through alternating Identifier / QuotedIdentifier / dot tokens
|
|
323
|
+
if (beforeDot.type === "Identifier" || beforeDot.type === "QuotedIdentifier") {
|
|
324
|
+
const tokenName = (t) => (t.type === "QuotedIdentifier" ? t.value.slice(2, -1) : t.value);
|
|
325
|
+
const path = [tokenName(beforeDot)];
|
|
87
326
|
i--;
|
|
88
327
|
while (i >= 1) {
|
|
89
328
|
if (!isDot(relevant[i]))
|
|
90
329
|
break;
|
|
91
330
|
i--;
|
|
92
|
-
if (relevant[i].type !== "Identifier")
|
|
331
|
+
if (relevant[i].type !== "Identifier" && relevant[i].type !== "QuotedIdentifier")
|
|
93
332
|
break;
|
|
94
|
-
path.unshift(relevant[i]
|
|
333
|
+
path.unshift(tokenName(relevant[i]));
|
|
95
334
|
i--;
|
|
96
335
|
}
|
|
97
336
|
return { kind: "context-path", path };
|
|
98
337
|
}
|
|
99
338
|
return { kind: "none" };
|
|
100
339
|
}
|
|
101
|
-
|
|
102
|
-
let current = context;
|
|
103
|
-
for (const key of path) {
|
|
104
|
-
if (current == null || typeof current !== "object")
|
|
105
|
-
return undefined;
|
|
106
|
-
current = current[key];
|
|
107
|
-
}
|
|
108
|
-
return current;
|
|
109
|
-
}
|
|
110
|
-
function kindOf(value) {
|
|
111
|
-
if (typeof value === "function")
|
|
112
|
-
return "function";
|
|
113
|
-
if (Array.isArray(value))
|
|
114
|
-
return "array";
|
|
115
|
-
if (typeof value === "string")
|
|
116
|
-
return "string";
|
|
117
|
-
if (typeof value === "number")
|
|
118
|
-
return "number";
|
|
119
|
-
if (typeof value === "boolean")
|
|
120
|
-
return "boolean";
|
|
121
|
-
if (typeof value === "object" && value !== null)
|
|
122
|
-
return "object";
|
|
123
|
-
return "unknown";
|
|
124
|
-
}
|
|
340
|
+
const VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
125
341
|
function objectCompletions(obj) {
|
|
126
342
|
return Object.entries(obj).map(([key, val]) => ({
|
|
127
343
|
label: key,
|
|
128
|
-
kind: kindOf(val),
|
|
344
|
+
kind: isContextEntry(val) ? val.type : kindOf(val),
|
|
129
345
|
id: key,
|
|
346
|
+
value: VALID_IDENTIFIER.test(key) ? undefined : `@"${key}"`,
|
|
130
347
|
}));
|
|
131
348
|
}
|
|
132
349
|
function builtinCompletions(methods) {
|
|
133
|
-
return methods.map((m) => ({ label: m.label, kind: m.kind, id: m.label }));
|
|
350
|
+
return methods.map((m) => ({ label: m.label, kind: m.kind, id: m.label, value: m.value }));
|
|
351
|
+
}
|
|
352
|
+
function binaryOperatorCompletions() {
|
|
353
|
+
return builtinCompletions(BINARY_OPERATORS);
|
|
134
354
|
}
|
|
135
355
|
export function createCompletionProvider(context) {
|
|
136
356
|
return function completionProvider(tokens, cursorPosition) {
|
|
137
|
-
const analysis = analyzeTokens(tokens, cursorPosition);
|
|
357
|
+
const analysis = analyzeTokens(tokens, cursorPosition, context);
|
|
138
358
|
switch (analysis.kind) {
|
|
139
359
|
case "top-level":
|
|
140
360
|
return objectCompletions(context);
|
|
141
361
|
case "string-literal":
|
|
142
362
|
return builtinCompletions(STRING_METHODS);
|
|
363
|
+
case "number-value":
|
|
364
|
+
return builtinCompletions(NUMBER_METHODS);
|
|
365
|
+
case "date-value":
|
|
366
|
+
return builtinCompletions(DATE_METHODS);
|
|
143
367
|
case "array-literal":
|
|
144
368
|
return builtinCompletions(ARRAY_METHODS);
|
|
145
369
|
case "context-path": {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
370
|
+
const entry = resolveEntry(context, analysis.path);
|
|
371
|
+
if (!entry)
|
|
148
372
|
return [];
|
|
149
|
-
if (
|
|
373
|
+
if (entry.type === "string")
|
|
150
374
|
return builtinCompletions(STRING_METHODS);
|
|
151
|
-
if (
|
|
375
|
+
if (entry.type === "number")
|
|
376
|
+
return builtinCompletions(NUMBER_METHODS);
|
|
377
|
+
if (entry.type === "array")
|
|
152
378
|
return builtinCompletions(ARRAY_METHODS);
|
|
153
|
-
if (
|
|
154
|
-
return
|
|
379
|
+
if (entry.type === "date")
|
|
380
|
+
return builtinCompletions(DATE_METHODS);
|
|
381
|
+
if (entry.type === "object" && entry.value != null && typeof entry.value === "object")
|
|
382
|
+
return objectCompletions(entry.value);
|
|
155
383
|
return [];
|
|
156
384
|
}
|
|
385
|
+
case "after-value":
|
|
386
|
+
return binaryOperatorCompletions();
|
|
157
387
|
case "none":
|
|
158
388
|
return [];
|
|
159
389
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VirtualElement } from "../../external/floating-ui.js";
|
|
2
2
|
export declare function useCursorPosition(textareaRef: React.RefObject<HTMLTextAreaElement | null>): {
|
|
3
|
-
|
|
3
|
+
virtualElement: VirtualElement;
|
|
4
4
|
update: () => void;
|
|
5
5
|
};
|
|
@@ -1,17 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef, useCallback } from "react";
|
|
2
2
|
import { measureCursorPosition } from "./measure-cursor-at-pointer.js";
|
|
3
3
|
export function useCursorPosition(textareaRef) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const cursorIndexRef = useRef(0);
|
|
5
|
+
const virtualElement = useRef({
|
|
6
|
+
getBoundingClientRect() {
|
|
7
|
+
const textarea = textareaRef.current;
|
|
8
|
+
if (!textarea)
|
|
9
|
+
return { top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, width: 0, height: 0 };
|
|
10
|
+
const { top, left, lineHeight } = measureCursorPosition(textarea, cursorIndexRef.current);
|
|
11
|
+
return {
|
|
12
|
+
top,
|
|
13
|
+
left,
|
|
14
|
+
right: left,
|
|
15
|
+
bottom: top + lineHeight,
|
|
16
|
+
x: left,
|
|
17
|
+
y: top,
|
|
18
|
+
width: 0,
|
|
19
|
+
height: lineHeight,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
get contextElement() {
|
|
23
|
+
return textareaRef.current ?? undefined;
|
|
24
|
+
},
|
|
8
25
|
});
|
|
9
26
|
const update = useCallback(() => {
|
|
10
27
|
const textarea = textareaRef.current;
|
|
11
28
|
if (!textarea)
|
|
12
29
|
return;
|
|
13
|
-
|
|
14
|
-
setCoordinates(coords);
|
|
30
|
+
cursorIndexRef.current = textarea.selectionStart;
|
|
15
31
|
}, [textareaRef]);
|
|
16
|
-
return {
|
|
32
|
+
return { virtualElement: virtualElement.current, update };
|
|
17
33
|
}
|
|
@@ -17,7 +17,7 @@ const sharedFontStyle = {
|
|
|
17
17
|
lineHeight: "inherit",
|
|
18
18
|
padding: "0",
|
|
19
19
|
};
|
|
20
|
-
const triggerCharacters = ["."];
|
|
20
|
+
const triggerCharacters = [".", " "];
|
|
21
21
|
export function ExpressionEditor({ value, onChange: onValueChange, tokenize, highlight: Highlighter = DefaultTokenHighlighter, completionProvider, placeholder, disabled, readOnly, className, style, children, }) {
|
|
22
22
|
// TODO @Lee: we should eventually allow multiline expressions but for now there isn't really a valid
|
|
23
23
|
// use case that requires multiline expressions without also allow variable support.
|
|
@@ -27,11 +27,6 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
|
|
|
27
27
|
const tokens = useMemo(() => tokenize(value), [value, tokenize]);
|
|
28
28
|
const completions = useCompletions(completionProvider);
|
|
29
29
|
const cursorPosition = useCursorPosition(textareaRef);
|
|
30
|
-
const navigation = useKeyboardNavigation({
|
|
31
|
-
onValueChange,
|
|
32
|
-
onDismiss: completions.dismiss,
|
|
33
|
-
textareaRef,
|
|
34
|
-
});
|
|
35
30
|
const trigger = useCompletionTrigger({
|
|
36
31
|
triggerCharacters,
|
|
37
32
|
onTrigger: (toks, pos, word) => {
|
|
@@ -40,6 +35,18 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
|
|
|
40
35
|
},
|
|
41
36
|
tokenize,
|
|
42
37
|
});
|
|
38
|
+
const navigation = useKeyboardNavigation({
|
|
39
|
+
onValueChange,
|
|
40
|
+
onAccepted: (value, cursorPosition) => {
|
|
41
|
+
if (completionProvider) {
|
|
42
|
+
trigger.triggerManually(value, cursorPosition + 1);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
completions.dismiss();
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
textareaRef,
|
|
49
|
+
});
|
|
43
50
|
depsRef.current = {
|
|
44
51
|
isOpen: completions.isOpen,
|
|
45
52
|
selectedItem: completions.selectedItem,
|
|
@@ -80,7 +87,20 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
|
|
|
80
87
|
deps.onValueChange(newValue);
|
|
81
88
|
const word = getWordAtCursor(newValue, selectionStart);
|
|
82
89
|
if (deps.isOpen) {
|
|
83
|
-
|
|
90
|
+
const charBefore = newValue[selectionStart - 1] || "";
|
|
91
|
+
if (word.word.length > 0) {
|
|
92
|
+
deps.updateFilter(word.word);
|
|
93
|
+
}
|
|
94
|
+
else if (charBefore === '"' || charBefore === "'") {
|
|
95
|
+
// String delimiter typed — close the popover rather than re-triggering.
|
|
96
|
+
deps.dismiss();
|
|
97
|
+
deps.cancel();
|
|
98
|
+
}
|
|
99
|
+
else if (!triggerCharacters.includes(charBefore)) {
|
|
100
|
+
// Non-word, non-trigger char (e.g. an operator) - re-fetch so the list
|
|
101
|
+
// reflects the new syntactic context rather than keeping stale results.
|
|
102
|
+
deps.triggerManually(newValue, selectionStart);
|
|
103
|
+
}
|
|
84
104
|
}
|
|
85
105
|
deps.handleInputChange(newValue, selectionStart);
|
|
86
106
|
}, []);
|
|
@@ -107,8 +127,8 @@ export function ExpressionEditor({ value, onChange: onValueChange, tokenize, hig
|
|
|
107
127
|
boxSizing: "border-box",
|
|
108
128
|
};
|
|
109
129
|
const popoverValue = useMemo(() => {
|
|
110
|
-
return { isOpen: completions.isOpen,
|
|
111
|
-
}, [completions.isOpen, cursorPosition.
|
|
130
|
+
return { isOpen: completions.isOpen, referenceElement: cursorPosition.virtualElement };
|
|
131
|
+
}, [completions.isOpen, cursorPosition.virtualElement]);
|
|
112
132
|
const listValue = useMemo(() => {
|
|
113
133
|
return {
|
|
114
134
|
items: completions.filteredItems,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VirtualElement } from "../../external/floating-ui.js";
|
|
2
|
+
import type { CompletionItem } from "../types";
|
|
2
3
|
interface ContextPopover {
|
|
3
4
|
readonly isOpen: boolean;
|
|
4
|
-
readonly
|
|
5
|
+
readonly referenceElement: VirtualElement | null;
|
|
5
6
|
}
|
|
6
7
|
export declare const CompletionPopoverProvider: import("react").Provider<ContextPopover>;
|
|
7
8
|
export declare const useCompletionPopover: () => ContextPopover;
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useRef, useEffect, forwardRef } from "react";
|
|
3
3
|
import { useCompletionPopover } from "./completion-context.js";
|
|
4
4
|
import { useCombinedRefs } from "@1771technologies/lytenyte-core/internal";
|
|
5
|
+
import { autoUpdate, computePosition, offset, shift } from "../../external/floating-ui.js";
|
|
5
6
|
function CompletionPopoverImpl({ children, ...props }, ref) {
|
|
6
|
-
const { isOpen,
|
|
7
|
+
const { isOpen, referenceElement } = useCompletionPopover();
|
|
7
8
|
const popoverRef = useRef(null);
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
const el = popoverRef.current;
|
|
@@ -18,11 +19,27 @@ function CompletionPopoverImpl({ children, ...props }, ref) {
|
|
|
18
19
|
el.hidePopover();
|
|
19
20
|
}
|
|
20
21
|
}, [isOpen]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const el = popoverRef.current;
|
|
24
|
+
if (!isOpen || !el || !referenceElement)
|
|
25
|
+
return;
|
|
26
|
+
function update() {
|
|
27
|
+
computePosition(referenceElement, el, {
|
|
28
|
+
strategy: "fixed",
|
|
29
|
+
placement: "bottom-start",
|
|
30
|
+
middleware: [offset(4), shift({ padding: 8 })],
|
|
31
|
+
}).then(({ x, y }) => {
|
|
32
|
+
el.style.left = `${x}px`;
|
|
33
|
+
el.style.top = `${y}px`;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return autoUpdate(referenceElement, el, update);
|
|
37
|
+
}, [isOpen, referenceElement]);
|
|
21
38
|
const combined = useCombinedRefs(ref, popoverRef);
|
|
22
39
|
return (_jsx("div", { ...props, ref: combined, popover: "manual", "data-ln-expression-popover": true, style: {
|
|
23
40
|
position: "fixed",
|
|
24
|
-
top:
|
|
25
|
-
left:
|
|
41
|
+
top: 0,
|
|
42
|
+
left: 0,
|
|
26
43
|
margin: 0,
|
|
27
44
|
inset: "unset",
|
|
28
45
|
}, children: isOpen ? children : null }));
|
|
@@ -3,12 +3,14 @@ const tokenToSyntaxColor = {
|
|
|
3
3
|
Number: "number",
|
|
4
4
|
String: "string",
|
|
5
5
|
TemplateLiteral: "string",
|
|
6
|
+
DateLiteral: "date",
|
|
6
7
|
Spread: "operator",
|
|
7
8
|
Punctuation: "operator",
|
|
8
9
|
Pipe: "operator",
|
|
9
10
|
Operator: "operator",
|
|
10
11
|
Arrow: "operator",
|
|
11
12
|
Identifier: "identifier",
|
|
13
|
+
QuotedIdentifier: "identifier",
|
|
12
14
|
ExpressionError: "error",
|
|
13
15
|
Unparsed: "punctuation",
|
|
14
16
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { CompletionItem, WordAtCursor } from "./types";
|
|
2
2
|
interface NavigationDeps {
|
|
3
3
|
readonly onValueChange: (value: string) => void;
|
|
4
|
-
readonly
|
|
4
|
+
readonly onAccepted: (value: string, cursorPosition: number) => void;
|
|
5
5
|
readonly textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
6
6
|
}
|
|
7
|
-
export declare function useKeyboardNavigation({ onValueChange,
|
|
7
|
+
export declare function useKeyboardNavigation({ onValueChange, onAccepted, textareaRef }: NavigationDeps): {
|
|
8
8
|
acceptCompletion: (item: CompletionItem, word: WordAtCursor) => void;
|
|
9
9
|
};
|
|
10
10
|
export {};
|
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { replaceWordAtCursor } from "./replace-word-at-cursor.js";
|
|
3
|
-
export function useKeyboardNavigation({ onValueChange,
|
|
3
|
+
export function useKeyboardNavigation({ onValueChange, onAccepted, textareaRef }) {
|
|
4
4
|
const acceptCompletion = useCallback((item, word) => {
|
|
5
5
|
const textarea = textareaRef.current;
|
|
6
6
|
if (!textarea)
|
|
7
7
|
return;
|
|
8
|
-
|
|
8
|
+
// Extend the replacement range to consume any @"..." / @'...' prefix the user already typed.
|
|
9
|
+
// The partial word may contain spaces (e.g. @"Age Gr → word is "Gr"), so scan backwards
|
|
10
|
+
// through word chars and spaces to find the opening @" / @'.
|
|
11
|
+
let adjustedWord = word;
|
|
12
|
+
const text = textarea.value;
|
|
13
|
+
let scan = word.start - 1;
|
|
14
|
+
// skip back over any chars that could be part of a quoted identifier value (spaces included)
|
|
15
|
+
while (scan > 0 && text[scan] !== '"' && text[scan] !== "'")
|
|
16
|
+
scan--;
|
|
17
|
+
if (scan >= 1 && (text[scan] === '"' || text[scan] === "'") && text[scan - 1] === "@") {
|
|
18
|
+
adjustedWord = { ...word, start: scan - 1 };
|
|
19
|
+
}
|
|
20
|
+
const charBeforeWord = adjustedWord.start > 0 ? text[adjustedWord.start - 1] : "";
|
|
21
|
+
const noSpaceBefore = charBeforeWord === "" ||
|
|
22
|
+
charBeforeWord === " " ||
|
|
23
|
+
charBeforeWord === "." ||
|
|
24
|
+
charBeforeWord === "(" ||
|
|
25
|
+
charBeforeWord === "[" ||
|
|
26
|
+
charBeforeWord === "{";
|
|
27
|
+
const rawInsertText = item.value ?? item.label;
|
|
28
|
+
const insertText = noSpaceBefore ? rawInsertText : " " + rawInsertText;
|
|
29
|
+
const { value, cursorPosition } = replaceWordAtCursor(text, adjustedWord, insertText);
|
|
9
30
|
textarea.value = value;
|
|
10
31
|
textarea.selectionStart = cursorPosition;
|
|
11
32
|
textarea.selectionEnd = cursorPosition;
|
|
12
33
|
onValueChange(value);
|
|
13
|
-
|
|
14
|
-
}, [onValueChange,
|
|
34
|
+
onAccepted(value, cursorPosition);
|
|
35
|
+
}, [onValueChange, onAccepted, textareaRef]);
|
|
15
36
|
return { acceptCompletion };
|
|
16
37
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { Evaluator } from "./evaluator/evaluate.js";
|
|
2
2
|
export type { Plugin, EvalFn } from "./plugin.js";
|
|
3
3
|
export type { Token } from "./lexer/types.js";
|
|
4
|
-
export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, } from "./plugins/standard.js";
|
|
4
|
+
export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, quotedIdentifierPlugin, } from "./plugins/standard.js";
|
|
5
5
|
export { createResolvedIdentifierPlugin } from "./plugins/resolved-identifier.js";
|
|
6
|
+
export { dateLiteralPlugin } from "./plugins/date-literal.js";
|
|
7
|
+
export { createDateIdentifierPlugin } from "./plugins/date-identifier.js";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { Evaluator } from "./evaluator/evaluate.js";
|
|
2
|
-
export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, } from "./plugins/standard.js";
|
|
2
|
+
export { accessPlugin, arrowsPlugin, booleansPlugin, collectionsPlugin, comparisonPlugin, logicalPlugin, membershipPlugin, pipePlugin, standardPlugins, stringsPlugin, ternaryPlugin, quotedIdentifierPlugin, } from "./plugins/standard.js";
|
|
3
3
|
export { createResolvedIdentifierPlugin } from "./plugins/resolved-identifier.js";
|
|
4
|
+
export { dateLiteralPlugin } from "./plugins/date-literal.js";
|
|
5
|
+
export { createDateIdentifierPlugin } from "./plugins/date-identifier.js";
|
|
@@ -93,5 +93,9 @@ export interface ArrowFunctionExpression extends BaseNode {
|
|
|
93
93
|
params: string[];
|
|
94
94
|
body: ASTNode;
|
|
95
95
|
}
|
|
96
|
-
export
|
|
96
|
+
export interface DateLiteralNode extends BaseNode {
|
|
97
|
+
type: "DateLiteralNode";
|
|
98
|
+
value: string;
|
|
99
|
+
}
|
|
100
|
+
export type ASTNode = NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral | UndefinedLiteral | Identifier | BinaryExpression | UnaryExpression | ConditionalExpression | MemberExpression | OptionalMemberExpression | ArrayLiteral | ObjectLiteral | CallExpression | PipeExpression | TemplateLiteral | SpreadElement | ArrowFunctionExpression | DateLiteralNode;
|
|
97
101
|
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function toDate(value) {
|
|
2
|
+
if (value instanceof Date)
|
|
3
|
+
return value;
|
|
4
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
5
|
+
const d = new Date(value);
|
|
6
|
+
return isNaN(d.getTime()) ? null : d;
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
export function createDateIdentifierPlugin(options) {
|
|
11
|
+
const identifierSet = new Set(options.identifiers);
|
|
12
|
+
return {
|
|
13
|
+
name: "date-identifier",
|
|
14
|
+
evaluate: (node, context) => {
|
|
15
|
+
if (node.type !== "Identifier")
|
|
16
|
+
return null;
|
|
17
|
+
if (!identifierSet.has(node.name))
|
|
18
|
+
return null;
|
|
19
|
+
const raw = context[node.name];
|
|
20
|
+
const date = toDate(raw);
|
|
21
|
+
if (date === null) {
|
|
22
|
+
if (raw === undefined || raw === null)
|
|
23
|
+
return { value: null };
|
|
24
|
+
throw new Error(`Context value for "${node.name}" cannot be coerced to a Date`);
|
|
25
|
+
}
|
|
26
|
+
return { value: date };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { scanString } from "../lexer/tokenize/scan-string.js";
|
|
2
|
+
import { current, advance } from "../parser/parser-context.js";
|
|
3
|
+
import { ExpressionError } from "../errors/expression-error.js";
|
|
4
|
+
const DATE_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
|
|
5
|
+
export const dateLiteralPlugin = {
|
|
6
|
+
name: "date-literal",
|
|
7
|
+
scan(source, pos) {
|
|
8
|
+
if (source[pos] !== "d")
|
|
9
|
+
return null;
|
|
10
|
+
const next = source[pos + 1];
|
|
11
|
+
if (next !== '"' && next !== "'")
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const r = scanString(source, pos + 1);
|
|
15
|
+
// Store the full source text (d"2023-02-11") so the editor overlay renders it correctly.
|
|
16
|
+
return { type: "DateLiteral", value: source.slice(pos, r.end), end: r.end };
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
if (e instanceof ExpressionError) {
|
|
20
|
+
throw new ExpressionError(e.rawMessage, { source, start: pos, end: e.end });
|
|
21
|
+
}
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
parsePrefix(ctx) {
|
|
26
|
+
const tok = current(ctx);
|
|
27
|
+
if (tok.type !== "DateLiteral")
|
|
28
|
+
return null;
|
|
29
|
+
advance(ctx);
|
|
30
|
+
// tok.value is the full d"2023-02-11" — strip the d" prefix and closing quote for the AST.
|
|
31
|
+
const dateStr = tok.value.slice(2, -1);
|
|
32
|
+
return { type: "DateLiteralNode", value: dateStr, start: tok.start, end: tok.end };
|
|
33
|
+
},
|
|
34
|
+
evaluate(node, context, evalFn) {
|
|
35
|
+
if (node.type === "DateLiteralNode") {
|
|
36
|
+
const d = new Date(node.value);
|
|
37
|
+
if (isNaN(d.getTime())) {
|
|
38
|
+
throw new Error(`Invalid date literal: "${node.value}"`);
|
|
39
|
+
}
|
|
40
|
+
return { value: d };
|
|
41
|
+
}
|
|
42
|
+
// Intercept comparison operators when at least one operand is a Date.
|
|
43
|
+
// This must run before comparisonPlugin (place dateLiteralPlugin first in the array).
|
|
44
|
+
if (node.type === "BinaryExpression") {
|
|
45
|
+
const op = node.operator;
|
|
46
|
+
if (!DATE_OPS.has(op))
|
|
47
|
+
return null;
|
|
48
|
+
const left = evalFn(node.left, context);
|
|
49
|
+
const right = evalFn(node.right, context);
|
|
50
|
+
if (!(left instanceof Date) && !(right instanceof Date))
|
|
51
|
+
return null;
|
|
52
|
+
const lt = left instanceof Date ? left.getTime() : Number(left);
|
|
53
|
+
const rt = right instanceof Date ? right.getTime() : Number(right);
|
|
54
|
+
switch (op) {
|
|
55
|
+
case "==":
|
|
56
|
+
return { value: lt === rt };
|
|
57
|
+
case "!=":
|
|
58
|
+
return { value: lt !== rt };
|
|
59
|
+
case "<":
|
|
60
|
+
return { value: lt < rt };
|
|
61
|
+
case ">":
|
|
62
|
+
return { value: lt > rt };
|
|
63
|
+
case "<=":
|
|
64
|
+
return { value: lt <= rt };
|
|
65
|
+
case ">=":
|
|
66
|
+
return { value: lt >= rt };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { scanString } from "../lexer/tokenize/scan-string.js";
|
|
2
|
+
import { current, advance } from "../parser/parser-context.js";
|
|
3
|
+
import { ExpressionError } from "../errors/expression-error.js";
|
|
4
|
+
export const quotedIdentifierPlugin = {
|
|
5
|
+
name: "quoted-identifier",
|
|
6
|
+
scan(source, pos) {
|
|
7
|
+
if (source[pos] !== "@")
|
|
8
|
+
return null;
|
|
9
|
+
const next = source[pos + 1];
|
|
10
|
+
if (next !== '"' && next !== "'")
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const r = scanString(source, pos + 1);
|
|
14
|
+
// Store the full source text (@"Age Group") so the token overlay renders correctly.
|
|
15
|
+
return { type: "QuotedIdentifier", value: source.slice(pos, r.end), end: r.end };
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
// Rethrow with start=pos so the ExpressionError token covers the leading @ character.
|
|
19
|
+
// Without this, scanString reports start at the opening quote (pos+1), leaving @ uncovered.
|
|
20
|
+
if (e instanceof ExpressionError) {
|
|
21
|
+
throw new ExpressionError(e.rawMessage, { source, start: pos, end: e.end });
|
|
22
|
+
}
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
parsePrefix(ctx) {
|
|
27
|
+
const tok = current(ctx);
|
|
28
|
+
if (tok.type !== "QuotedIdentifier")
|
|
29
|
+
return null;
|
|
30
|
+
advance(ctx);
|
|
31
|
+
// Strip the leading @" (or @') and the trailing quote to get the bare identifier name.
|
|
32
|
+
const name = tok.value.slice(2, -1);
|
|
33
|
+
return { type: "Identifier", name, start: tok.start, end: tok.end };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -9,5 +9,7 @@ import { membershipPlugin } from "./membership.js";
|
|
|
9
9
|
import { accessPlugin } from "./access.js";
|
|
10
10
|
import { arrowsPlugin } from "./arrows.js";
|
|
11
11
|
import { collectionsPlugin } from "./collections.js";
|
|
12
|
+
import { quotedIdentifierPlugin } from "./quoted-identifier.js";
|
|
13
|
+
import { dateLiteralPlugin } from "./date-literal.js";
|
|
12
14
|
export declare const standardPlugins: Plugin[];
|
|
13
|
-
export { stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, };
|
|
15
|
+
export { dateLiteralPlugin, stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, quotedIdentifierPlugin, };
|
|
@@ -8,7 +8,10 @@ import { membershipPlugin } from "./membership.js";
|
|
|
8
8
|
import { accessPlugin } from "./access.js";
|
|
9
9
|
import { arrowsPlugin } from "./arrows.js";
|
|
10
10
|
import { collectionsPlugin } from "./collections.js";
|
|
11
|
+
import { quotedIdentifierPlugin } from "./quoted-identifier.js";
|
|
12
|
+
import { dateLiteralPlugin } from "./date-literal.js";
|
|
11
13
|
export const standardPlugins = [
|
|
14
|
+
dateLiteralPlugin,
|
|
12
15
|
arrowsPlugin,
|
|
13
16
|
collectionsPlugin,
|
|
14
17
|
stringsPlugin,
|
|
@@ -19,5 +22,6 @@ export const standardPlugins = [
|
|
|
19
22
|
ternaryPlugin,
|
|
20
23
|
pipePlugin,
|
|
21
24
|
accessPlugin,
|
|
25
|
+
quotedIdentifierPlugin,
|
|
22
26
|
];
|
|
23
|
-
export { stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, };
|
|
27
|
+
export { dateLiteralPlugin, stringsPlugin, booleansPlugin, comparisonPlugin, logicalPlugin, ternaryPlugin, pipePlugin, membershipPlugin, accessPlugin, arrowsPlugin, collectionsPlugin, quotedIdentifierPlugin, };
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1771technologies/lytenyte-pro",
|
|
3
3
|
"description": "Blazingly fast headless React data grid with 100s of features.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.1-dev.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "COMMERCIAL",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
9
|
"css",
|
|
10
|
+
"skills",
|
|
10
11
|
"LICENSE",
|
|
11
12
|
"SECURITY.md",
|
|
12
13
|
"README.md"
|
|
@@ -85,12 +86,12 @@
|
|
|
85
86
|
"access": "public"
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
|
-
"@1771technologies/lytenyte-shared": "2.
|
|
89
|
-
"@1771technologies/lytenyte-core": "2.
|
|
90
|
-
"@1771technologies/lytenyte-design": "2.
|
|
89
|
+
"@1771technologies/lytenyte-shared": "2.1.1-dev.0",
|
|
90
|
+
"@1771technologies/lytenyte-core": "2.1.1-dev.0",
|
|
91
|
+
"@1771technologies/lytenyte-design": "2.1.1-dev.0"
|
|
91
92
|
},
|
|
92
93
|
"devDependencies": {
|
|
93
|
-
"@1771technologies/grid-sample-data": "2.0
|
|
94
|
+
"@1771technologies/grid-sample-data": "2.1.0"
|
|
94
95
|
},
|
|
95
96
|
"peerDependencies": {
|
|
96
97
|
"react": "18.0.0 || ^19.0.0",
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useMemo } from "react";
|
|
3
|
-
import { createCompletionProvider, ExpressionEditor } from "../index.js";
|
|
4
|
-
import { Evaluator, standardPlugins } from "../../../expressions/index.js";
|
|
5
|
-
import "../../../../css/light-dark.css";
|
|
6
|
-
import "../../../../css/components/expression-editor.css";
|
|
7
|
-
import "./expression.css";
|
|
8
|
-
import { CompletionPopover } from "../intellisence/completion-popover.js";
|
|
9
|
-
import { CompletionList } from "../intellisence/completion-list.js";
|
|
10
|
-
import { CompletionListItem } from "../intellisence/completion-item.js";
|
|
11
|
-
const CONTEXT = {
|
|
12
|
-
user: {
|
|
13
|
-
name: "Alice",
|
|
14
|
-
age: 30,
|
|
15
|
-
email: "alice@example.com",
|
|
16
|
-
address: {
|
|
17
|
-
city: "Portland",
|
|
18
|
-
state: "OR",
|
|
19
|
-
zip: "97201",
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
order: {
|
|
23
|
-
total: 99.99,
|
|
24
|
-
status: "shipped",
|
|
25
|
-
items: 3,
|
|
26
|
-
},
|
|
27
|
-
Math: {
|
|
28
|
-
PI: 3.14159,
|
|
29
|
-
abs: "function",
|
|
30
|
-
max: "function",
|
|
31
|
-
min: "function",
|
|
32
|
-
round: "function",
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
const EXAMPLES = [
|
|
36
|
-
{
|
|
37
|
-
label: "Property access",
|
|
38
|
-
value: "user.address.city",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
label: "Math",
|
|
42
|
-
value: "Math.round(order.total * 1.08)",
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
label: "Conditional",
|
|
46
|
-
value: "if order.total > 100 and user.age >= 18",
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
label: "Object literal",
|
|
50
|
-
value: "{name: user.name, total: order.total, tax: order.total * 0.08}",
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
label: "Error token",
|
|
54
|
-
value: "user.name or undefined",
|
|
55
|
-
},
|
|
56
|
-
];
|
|
57
|
-
export default function App() {
|
|
58
|
-
const [value, setValue] = useState("user.name");
|
|
59
|
-
const evaluator = useMemo(() => new Evaluator(standardPlugins), []);
|
|
60
|
-
const tokenize = useCallback((input) => evaluator.tokensSafe(input, true), [evaluator]);
|
|
61
|
-
const provider = useMemo(() => {
|
|
62
|
-
return createCompletionProvider(CONTEXT);
|
|
63
|
-
}, []);
|
|
64
|
-
return (_jsxs("div", { className: "demo-container", children: [_jsx("label", { className: "demo-label", children: "Single-line" }), _jsx(ExpressionEditor.Root, { value: value, onChange: setValue, tokenize: tokenize, completionProvider: provider, placeholder: "Type an expression...", children: _jsx(CompletionPopover, { children: _jsx(CompletionList, { children: ({ items }) => {
|
|
65
|
-
return items.map((item, index) => {
|
|
66
|
-
return (_jsxs(CompletionListItem, { item: item, index: index, className: "completion-item", children: [_jsx("span", { className: "completion-kind", children: item.kind }), _jsx("span", { className: "completion-label", children: item.label })] }, item.id));
|
|
67
|
-
});
|
|
68
|
-
} }) }) }), _jsx("div", { className: "examples-row", children: EXAMPLES.map((ex) => (_jsx("button", { className: `example-btn${value === ex.value ? "active" : ""}`, onClick: () => setValue(ex.value), children: ex.label }, ex.label))) })] }));
|
|
69
|
-
}
|