@bpmn-io/feel-editor 1.4.0 → 1.6.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 +10 -1
- package/dist/index.es.js +555 -354
- package/dist/index.js +567 -366
- package/package.json +22 -22
package/dist/index.es.js
CHANGED
|
@@ -1,18 +1,116 @@
|
|
|
1
|
-
import { snippetCompletion, autocompletion
|
|
1
|
+
import { snippetCompletion, autocompletion, closeBrackets } from '@codemirror/autocomplete';
|
|
2
2
|
import { defaultKeymap } from '@codemirror/commands';
|
|
3
|
-
import {
|
|
3
|
+
import { syntaxHighlighting, HighlightStyle, syntaxTree, bracketMatching, indentOnInput } from '@codemirror/language';
|
|
4
4
|
import { linter as linter$1, setDiagnosticsEffect } from '@codemirror/lint';
|
|
5
5
|
import { Facet, Compartment, EditorState } from '@codemirror/state';
|
|
6
|
-
import { EditorView, tooltips, keymap } from '@codemirror/view';
|
|
7
|
-
import { snippets, keywordCompletions, feelLanguage } from 'lang-feel';
|
|
8
|
-
import { domify } from 'min-dom';
|
|
6
|
+
import { EditorView, tooltips, keymap, placeholder } from '@codemirror/view';
|
|
9
7
|
import { cmFeelLinter } from '@bpmn-io/feel-lint';
|
|
10
|
-
import { tags
|
|
8
|
+
import { tags } from '@lezer/highlight';
|
|
9
|
+
import { snippetCompletion as snippetCompletion$1, snippets, keywordCompletions, feel } from 'lang-feel';
|
|
10
|
+
import { domify } from 'min-dom';
|
|
11
|
+
|
|
12
|
+
var linter = [ linter$1(cmFeelLinter()) ];
|
|
13
|
+
|
|
14
|
+
const baseTheme = EditorView.theme({
|
|
15
|
+
'& .cm-content': {
|
|
16
|
+
padding: '0px',
|
|
17
|
+
},
|
|
18
|
+
'& .cm-line': {
|
|
19
|
+
padding: '0px',
|
|
20
|
+
},
|
|
21
|
+
'&.cm-editor.cm-focused': {
|
|
22
|
+
outline: 'none',
|
|
23
|
+
},
|
|
24
|
+
'& .cm-completionInfo': {
|
|
25
|
+
whiteSpace: 'pre-wrap',
|
|
26
|
+
overflow: 'hidden',
|
|
27
|
+
textOverflow: 'ellipsis'
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Don't wrap whitespace for custom HTML
|
|
31
|
+
'& .cm-completionInfo > *': {
|
|
32
|
+
whiteSpace: 'normal'
|
|
33
|
+
},
|
|
34
|
+
'& .cm-completionInfo ul': {
|
|
35
|
+
margin: 0,
|
|
36
|
+
paddingLeft: '15px'
|
|
37
|
+
},
|
|
38
|
+
'& .cm-completionInfo pre': {
|
|
39
|
+
marginBottom: 0,
|
|
40
|
+
whiteSpace: 'pre-wrap'
|
|
41
|
+
},
|
|
42
|
+
'& .cm-completionInfo p': {
|
|
43
|
+
marginTop: 0,
|
|
44
|
+
},
|
|
45
|
+
'& .cm-completionInfo p:not(:last-of-type)': {
|
|
46
|
+
marginBottom: 0,
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const highlightTheme = EditorView.baseTheme({
|
|
51
|
+
'& .variableName': {
|
|
52
|
+
color: '#10f'
|
|
53
|
+
},
|
|
54
|
+
'& .number': {
|
|
55
|
+
color: '#164'
|
|
56
|
+
},
|
|
57
|
+
'& .string': {
|
|
58
|
+
color: '#a11'
|
|
59
|
+
},
|
|
60
|
+
'& .bool': {
|
|
61
|
+
color: '#219'
|
|
62
|
+
},
|
|
63
|
+
'& .function': {
|
|
64
|
+
color: '#aa3731',
|
|
65
|
+
fontWeight: 'bold'
|
|
66
|
+
},
|
|
67
|
+
'& .control': {
|
|
68
|
+
color: '#708'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const syntaxClasses = syntaxHighlighting(
|
|
73
|
+
HighlightStyle.define([
|
|
74
|
+
{ tag: tags.variableName, class: 'variableName' },
|
|
75
|
+
{ tag: tags.name, class: 'variableName' },
|
|
76
|
+
{ tag: tags.number, class: 'number' },
|
|
77
|
+
{ tag: tags.string, class: 'string' },
|
|
78
|
+
{ tag: tags.bool, class: 'bool' },
|
|
79
|
+
{ tag: tags.function(tags.variableName), class: 'function' },
|
|
80
|
+
{ tag: tags.function(tags.special(tags.variableName)), class: 'function' },
|
|
81
|
+
{ tag: tags.controlKeyword, class: 'control' },
|
|
82
|
+
{ tag: tags.operatorKeyword, class: 'control' }
|
|
83
|
+
])
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
var theme = [ baseTheme, highlightTheme, syntaxClasses ];
|
|
11
87
|
|
|
12
88
|
// helpers ///////////////////////////////
|
|
13
89
|
|
|
14
|
-
function
|
|
15
|
-
return node.from === node.to;
|
|
90
|
+
function _isEmpty(node) {
|
|
91
|
+
return node && node.from === node.to;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {any} node
|
|
96
|
+
* @param {number} pos
|
|
97
|
+
*
|
|
98
|
+
* @return {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isEmpty(node, pos) {
|
|
101
|
+
|
|
102
|
+
// For the special case of empty nodes, we need to check the current node
|
|
103
|
+
// as well. The previous node could be part of another token, e.g.
|
|
104
|
+
// when typing functions "abs(".
|
|
105
|
+
const nextNode = node.nextSibling;
|
|
106
|
+
|
|
107
|
+
return _isEmpty(node) || (
|
|
108
|
+
nextNode && nextNode.from === pos && _isEmpty(nextNode)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isVariableName(node) {
|
|
113
|
+
return node && node.parent && node.parent.name === 'VariableName';
|
|
16
114
|
}
|
|
17
115
|
|
|
18
116
|
function isPathExpression(node) {
|
|
@@ -27,7 +125,382 @@ function isPathExpression(node) {
|
|
|
27
125
|
return isPathExpression(node.parent);
|
|
28
126
|
}
|
|
29
127
|
|
|
30
|
-
|
|
128
|
+
/**
|
|
129
|
+
* @typedef { import('../core').Variable } Variable
|
|
130
|
+
* @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param { {
|
|
135
|
+
* variables?: Variable[],
|
|
136
|
+
* } } options
|
|
137
|
+
*
|
|
138
|
+
* @return { CompletionSource }
|
|
139
|
+
*/
|
|
140
|
+
function pathExpressionCompletion({ variables }) {
|
|
141
|
+
|
|
142
|
+
return (context) => {
|
|
143
|
+
|
|
144
|
+
const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
|
|
145
|
+
|
|
146
|
+
if (!isPathExpression(nodeBefore)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const expression = findPathExpression(nodeBefore);
|
|
151
|
+
|
|
152
|
+
// if the cursor is directly after the `.`, variable starts at the cursor position
|
|
153
|
+
const from = nodeBefore === expression ? context.pos : nodeBefore.from;
|
|
154
|
+
|
|
155
|
+
const path = getPath(expression, context);
|
|
156
|
+
|
|
157
|
+
let options = variables;
|
|
158
|
+
for (var i = 0; i < path.length - 1; i++) {
|
|
159
|
+
var childVar = options.find(val => val.name === path[i].name);
|
|
160
|
+
|
|
161
|
+
if (!childVar) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// only suggest if variable type matches
|
|
166
|
+
if (
|
|
167
|
+
childVar.isList !== 'optional' &&
|
|
168
|
+
!!childVar.isList !== path[i].isList
|
|
169
|
+
) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
options = childVar.entries;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!options) return;
|
|
177
|
+
|
|
178
|
+
options = options.map(v => ({
|
|
179
|
+
label: v.name,
|
|
180
|
+
type: 'variable',
|
|
181
|
+
info: v.info,
|
|
182
|
+
detail: v.detail
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
const result = {
|
|
186
|
+
from: from,
|
|
187
|
+
options: options
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
function findPathExpression(node) {
|
|
196
|
+
while (node) {
|
|
197
|
+
if (node.name === 'PathExpression') {
|
|
198
|
+
return node;
|
|
199
|
+
}
|
|
200
|
+
node = node.parent;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// parses the path expression into a list of variable names with type information
|
|
205
|
+
// e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
|
|
206
|
+
function getPath(node, context) {
|
|
207
|
+
let path = [];
|
|
208
|
+
|
|
209
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
210
|
+
if (child.name === 'PathExpression') {
|
|
211
|
+
path.push(...getPath(child, context));
|
|
212
|
+
} else if (child.name === 'FilterExpression') {
|
|
213
|
+
path.push(...getFilter(child, context));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
path.push({
|
|
217
|
+
name: getNodeContent(child, context),
|
|
218
|
+
isList: false
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return path;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getFilter(node, context) {
|
|
226
|
+
const list = node.firstChild;
|
|
227
|
+
|
|
228
|
+
if (list.name === 'PathExpression') {
|
|
229
|
+
const path = getPath(list, context);
|
|
230
|
+
const last = path[path.length - 1];
|
|
231
|
+
last.isList = true;
|
|
232
|
+
|
|
233
|
+
return path;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return [ {
|
|
237
|
+
name: getNodeContent(list, context),
|
|
238
|
+
isList: true
|
|
239
|
+
} ];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getNodeContent(node, context) {
|
|
243
|
+
return context.state.sliceDoc(node.from, node.to);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @typedef { import('../core').Variable } Variable
|
|
248
|
+
* @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param { {
|
|
253
|
+
* variables?: Variable[],
|
|
254
|
+
* builtins?: Variable[]
|
|
255
|
+
* } } options
|
|
256
|
+
*
|
|
257
|
+
* @return { CompletionSource }
|
|
258
|
+
*/
|
|
259
|
+
function variableCompletion({ variables = [], builtins = [] }) {
|
|
260
|
+
|
|
261
|
+
const options = getVariableSuggestions(variables, builtins);
|
|
262
|
+
|
|
263
|
+
if (!options.length) {
|
|
264
|
+
return (context) => null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (context) => {
|
|
268
|
+
|
|
269
|
+
const {
|
|
270
|
+
pos,
|
|
271
|
+
state
|
|
272
|
+
} = context;
|
|
273
|
+
|
|
274
|
+
// in most cases, use what is typed before the cursor
|
|
275
|
+
const nodeBefore = syntaxTree(state).resolve(pos, -1);
|
|
276
|
+
|
|
277
|
+
if (isEmpty(nodeBefore, pos)) {
|
|
278
|
+
return context.explicit ? {
|
|
279
|
+
from: pos,
|
|
280
|
+
options
|
|
281
|
+
} : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// only auto-complete variables
|
|
285
|
+
if (!isVariableName(nodeBefore) || isPathExpression(nodeBefore)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
from: nodeBefore.from,
|
|
291
|
+
options
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param { Variable[] } variables
|
|
298
|
+
* @param { Variable[] } builtins
|
|
299
|
+
*
|
|
300
|
+
* @returns {import('@codemirror/autocomplete').Completion[]}
|
|
301
|
+
*/
|
|
302
|
+
function getVariableSuggestions(variables, builtins) {
|
|
303
|
+
return [].concat(
|
|
304
|
+
variables.map(v => createVariableSuggestion(v)),
|
|
305
|
+
builtins.map(b => createVariableSuggestion(b))
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {import('..').Variable} variable
|
|
311
|
+
* @param {number} boost
|
|
312
|
+
|
|
313
|
+
* @returns {import('@codemirror/autocomplete').Completion}
|
|
314
|
+
*/
|
|
315
|
+
function createVariableSuggestion(variable, boost) {
|
|
316
|
+
if (variable.type === 'function') {
|
|
317
|
+
return createFunctionVariable(variable, boost);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
label: variable.name,
|
|
322
|
+
type: 'variable',
|
|
323
|
+
info: variable.info,
|
|
324
|
+
detail: variable.detail,
|
|
325
|
+
boost
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @param {import('..').Variable} variable
|
|
331
|
+
* @param {number} boost
|
|
332
|
+
*
|
|
333
|
+
* @returns {import('@codemirror/autocomplete').Completion}
|
|
334
|
+
*/
|
|
335
|
+
function createFunctionVariable(variable, boost) {
|
|
336
|
+
const {
|
|
337
|
+
name,
|
|
338
|
+
info,
|
|
339
|
+
detail,
|
|
340
|
+
params = []
|
|
341
|
+
} = variable;
|
|
342
|
+
|
|
343
|
+
const paramsWithNames = params.map(({ name, type }, index) => ({
|
|
344
|
+
name: name || `param ${index + 1}`,
|
|
345
|
+
type
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
const template = `${name}(${paramsWithNames.map(p => '${' + p.name + '}').join(', ')})`;
|
|
349
|
+
|
|
350
|
+
const paramsSignature = paramsWithNames.map(({ name, type }) => (
|
|
351
|
+
type ? `${name}: ${type}` : name
|
|
352
|
+
)).join(', ');
|
|
353
|
+
const label = `${name}(${paramsSignature})`;
|
|
354
|
+
|
|
355
|
+
return snippetCompletion(template, {
|
|
356
|
+
label,
|
|
357
|
+
type: 'function',
|
|
358
|
+
info,
|
|
359
|
+
detail,
|
|
360
|
+
boost
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @typedef { import('../core').Variable } Variable
|
|
366
|
+
* @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param { {
|
|
371
|
+
* variables?: Variable[],
|
|
372
|
+
* builtins?: Variable[]
|
|
373
|
+
* } } options
|
|
374
|
+
*
|
|
375
|
+
* @return { CompletionSource[] }
|
|
376
|
+
*/
|
|
377
|
+
function completions({ variables = [], builtins = [] }) {
|
|
378
|
+
|
|
379
|
+
return [
|
|
380
|
+
pathExpressionCompletion({ variables }),
|
|
381
|
+
variableCompletion({ variables, builtins }),
|
|
382
|
+
snippetCompletion$1(snippets.map(snippet => ({ ...snippet, boost: -1 }))),
|
|
383
|
+
...keywordCompletions
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @typedef { 'expression' | 'unaryTests' } Dialect
|
|
389
|
+
*/
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @param { {
|
|
393
|
+
* dialect?: Dialect,
|
|
394
|
+
* context?: Record<string, any>,
|
|
395
|
+
* completions?: import('@codemirror/autocomplete').CompletionSource[]
|
|
396
|
+
* } } options
|
|
397
|
+
*
|
|
398
|
+
* @return { import('@codemirror/language').LanguageSupport }
|
|
399
|
+
*/
|
|
400
|
+
function language(options) {
|
|
401
|
+
return feel(options);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @param { import('../core').Variable[] } variables
|
|
406
|
+
*
|
|
407
|
+
* @return {Record<string, any>}
|
|
408
|
+
*/
|
|
409
|
+
function createContext(variables, builtins) {
|
|
410
|
+
return variables.slice().reverse().reduce((context, builtin) => {
|
|
411
|
+
context[builtin.name] = new Function();
|
|
412
|
+
|
|
413
|
+
return context;
|
|
414
|
+
}, {});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @typedef { 'expression' | 'unaryTests' } Dialect
|
|
419
|
+
* @typedef { import('..').Variable } Variable
|
|
420
|
+
*/
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @type {Facet<Variable[]>}
|
|
424
|
+
*/
|
|
425
|
+
const builtinsFacet = Facet.define();
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @type {Facet<Variable[]>}
|
|
429
|
+
*/
|
|
430
|
+
const variablesFacet = Facet.define();
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @type {Facet<dialect>}
|
|
434
|
+
*/
|
|
435
|
+
const dialectFacet = Facet.define();
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @typedef {object} Variable
|
|
439
|
+
* @property {string} name name or key of the variable
|
|
440
|
+
* @property {string} [info] short information about the variable, e.g. type
|
|
441
|
+
* @property {string} [detail] longer description of the variable content
|
|
442
|
+
* @property {boolean} [isList] whether the variable is a list
|
|
443
|
+
* @property {Array<Variable>} [schema] array of child variables if the variable is a context or list
|
|
444
|
+
* @property {'function'|'variable'} [type] type of the variable
|
|
445
|
+
* @property {Array<{name: string, type: string}>} [params] function parameters
|
|
446
|
+
*/
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* @typedef { {
|
|
450
|
+
* dialect?: import('../language').Dialect,
|
|
451
|
+
* variables?: Variable[],
|
|
452
|
+
* builtins?: Variable[]
|
|
453
|
+
* } } CoreConfig
|
|
454
|
+
*
|
|
455
|
+
* @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
|
|
456
|
+
* @typedef { import('@codemirror/state').Extension } Extension
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* @param { CoreConfig & { completions?: CompletionSource[] } } config
|
|
461
|
+
*
|
|
462
|
+
* @return { Extension }
|
|
463
|
+
*/
|
|
464
|
+
function configure({
|
|
465
|
+
dialect = 'expression',
|
|
466
|
+
variables = [],
|
|
467
|
+
builtins = [],
|
|
468
|
+
completions: completions$1 = completions({ builtins, variables })
|
|
469
|
+
}) {
|
|
470
|
+
|
|
471
|
+
const context = createContext([ ...variables, ...builtins ]);
|
|
472
|
+
|
|
473
|
+
return [
|
|
474
|
+
dialectFacet.of(dialect),
|
|
475
|
+
builtinsFacet.of(builtins),
|
|
476
|
+
variablesFacet.of(variables),
|
|
477
|
+
language({
|
|
478
|
+
dialect,
|
|
479
|
+
context,
|
|
480
|
+
completions: completions$1
|
|
481
|
+
})
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @param {import('@codemirror/state').EditorState } state
|
|
487
|
+
*
|
|
488
|
+
* @return { CoreConfig }
|
|
489
|
+
*/
|
|
490
|
+
function get(state) {
|
|
491
|
+
|
|
492
|
+
const builtins = state.facet(builtinsFacet)[0];
|
|
493
|
+
const variables = state.facet(variablesFacet)[0];
|
|
494
|
+
const dialect = state.facet(dialectFacet)[0];
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
builtins,
|
|
498
|
+
variables,
|
|
499
|
+
dialect
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
var camundaTags = [
|
|
31
504
|
{
|
|
32
505
|
name: "not(negand)",
|
|
33
506
|
description: "<p>Returns the logical negation of the given value.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">not(negand: boolean): boolean\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">not(true)\n// false\n\nnot(null)\n// null\n</code></pre>\n"
|
|
@@ -106,7 +579,7 @@ var tags = [
|
|
|
106
579
|
},
|
|
107
580
|
{
|
|
108
581
|
name: "date and time(from)",
|
|
109
|
-
description: "<p>Parses the given string into a date and time.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">date and time(from: string): date and time\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">date and time("2018-04-
|
|
582
|
+
description: "<p>Parses the given string into a date and time.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">date and time(from: string): date and time\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">date and time("2018-04-29T09:30:00")\n// date and time("2018-04-29T09:30:00")\n</code></pre>\n"
|
|
110
583
|
},
|
|
111
584
|
{
|
|
112
585
|
name: "date and time(date, time)",
|
|
@@ -510,363 +983,59 @@ var tags = [
|
|
|
510
983
|
}
|
|
511
984
|
];
|
|
512
985
|
|
|
513
|
-
const options = tags.map(tag => {
|
|
514
|
-
const match = tag.name.match(/^([\w\s]+)\((.*)\)$/);
|
|
515
|
-
const functionName = match[1];
|
|
516
|
-
const functionArguments = match[2];
|
|
517
|
-
|
|
518
|
-
const placeHolders = functionArguments
|
|
519
|
-
.split(', ')
|
|
520
|
-
.map((arg) => `\${${arg}}`)
|
|
521
|
-
.join(', ');
|
|
522
|
-
|
|
523
|
-
return snippetCompletion(
|
|
524
|
-
`${functionName}(${placeHolders})`,
|
|
525
|
-
{
|
|
526
|
-
label: tag.name,
|
|
527
|
-
type: 'function',
|
|
528
|
-
info: () => {
|
|
529
|
-
const html = domify(`<div class="description">${tag.description}<div>`);
|
|
530
|
-
return html;
|
|
531
|
-
},
|
|
532
|
-
boost: -1
|
|
533
|
-
}
|
|
534
|
-
);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
var builtins = context => {
|
|
538
|
-
|
|
539
|
-
let nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
|
|
540
|
-
|
|
541
|
-
// For the special case of empty nodes, we need to check the current node
|
|
542
|
-
// as well. The previous node could be part of another token, e.g.
|
|
543
|
-
// when typing functions "abs(".
|
|
544
|
-
let nextNode = nodeBefore.nextSibling;
|
|
545
|
-
const isInEmptyNode =
|
|
546
|
-
isNodeEmpty(nodeBefore) ||
|
|
547
|
-
nextNode && nextNode.from === context.pos && isNodeEmpty(nextNode);
|
|
548
|
-
|
|
549
|
-
if (isInEmptyNode) {
|
|
550
|
-
return context.explicit ? {
|
|
551
|
-
from: context.pos,
|
|
552
|
-
options: options
|
|
553
|
-
} : null;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Don't auto-complete on path expressions/context keys/...
|
|
557
|
-
if ((nodeBefore.parent && nodeBefore.parent.name !== 'VariableName') || isPathExpression(nodeBefore)) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return {
|
|
562
|
-
from: nodeBefore.from,
|
|
563
|
-
options: options
|
|
564
|
-
};
|
|
565
|
-
};
|
|
566
|
-
|
|
567
986
|
/**
|
|
568
|
-
* @
|
|
987
|
+
* @param { import('..').Builtin[] } builtins
|
|
988
|
+
*
|
|
989
|
+
* @returns {import('..').Variable[] } variable
|
|
569
990
|
*/
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
var pathExpression = context => {
|
|
573
|
-
const variables = context.state.facet(variablesFacet)[0];
|
|
574
|
-
const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
|
|
575
|
-
|
|
576
|
-
if (!isPathExpression(nodeBefore)) {
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const expression = findPathExpression(nodeBefore);
|
|
581
|
-
|
|
582
|
-
// if the cursor is directly after the `.`, variable starts at the cursor position
|
|
583
|
-
const from = nodeBefore === expression ? context.pos : nodeBefore.from;
|
|
584
|
-
|
|
585
|
-
const path = getPath(expression, context);
|
|
586
|
-
|
|
587
|
-
let options = variables;
|
|
588
|
-
for (var i = 0; i < path.length - 1; i++) {
|
|
589
|
-
var childVar = options.find(val => val.name === path[i].name);
|
|
590
|
-
|
|
591
|
-
if (!childVar) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// only suggest if variable type matches
|
|
596
|
-
if (
|
|
597
|
-
childVar.isList !== 'optional' &&
|
|
598
|
-
!!childVar.isList !== path[i].isList
|
|
599
|
-
) {
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
options = childVar.entries;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (!options) return;
|
|
607
|
-
|
|
608
|
-
options = options.map(v => ({
|
|
609
|
-
label: v.name,
|
|
610
|
-
type: 'variable',
|
|
611
|
-
info: v.info,
|
|
612
|
-
detail: v.detail
|
|
613
|
-
}));
|
|
614
|
-
|
|
615
|
-
const result = {
|
|
616
|
-
from: from,
|
|
617
|
-
options: options
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
return result;
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
function findPathExpression(node) {
|
|
625
|
-
while (node) {
|
|
626
|
-
if (node.name === 'PathExpression') {
|
|
627
|
-
return node;
|
|
628
|
-
}
|
|
629
|
-
node = node.parent;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// parses the path expression into a list of variable names with type information
|
|
634
|
-
// e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
|
|
635
|
-
function getPath(node, context) {
|
|
636
|
-
let path = [];
|
|
637
|
-
|
|
638
|
-
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
639
|
-
if (child.name === 'PathExpression') {
|
|
640
|
-
path.push(...getPath(child, context));
|
|
641
|
-
} else if (child.name === 'FilterExpression') {
|
|
642
|
-
path.push(...getFilter(child, context));
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
path.push({
|
|
646
|
-
name: getNodeContent(child, context),
|
|
647
|
-
isList: false
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
return path;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function getFilter(node, context) {
|
|
655
|
-
const list = node.firstChild;
|
|
656
|
-
|
|
657
|
-
if (list.name === 'PathExpression') {
|
|
658
|
-
const path = getPath(list, context);
|
|
659
|
-
const last = path[path.length - 1];
|
|
660
|
-
last.isList = true;
|
|
661
|
-
|
|
662
|
-
return path;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
return [ {
|
|
666
|
-
name: getNodeContent(list, context),
|
|
667
|
-
isList: true
|
|
668
|
-
} ];
|
|
991
|
+
function parseBuiltins(builtins) {
|
|
992
|
+
return builtins.map(parseBuiltin);
|
|
669
993
|
}
|
|
670
994
|
|
|
671
|
-
function getNodeContent(node, context) {
|
|
672
|
-
return context.state.sliceDoc(node.from, node.to);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* @type {import('@codemirror/autocomplete').CompletionSource}
|
|
677
|
-
*/
|
|
678
|
-
var variables = context => {
|
|
679
|
-
|
|
680
|
-
const variables = context.state.facet(variablesFacet)[0];
|
|
681
|
-
|
|
682
|
-
const options = variables.map(v => createVariableSuggestion(v));
|
|
683
|
-
|
|
684
|
-
// In most cases, use what is typed before the cursor
|
|
685
|
-
let nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
|
|
686
|
-
|
|
687
|
-
// For the special case of empty nodes, we need to check the current node
|
|
688
|
-
// as well. The previous node could be part of another token, e.g.
|
|
689
|
-
// when typing functions "abs(".
|
|
690
|
-
let nextNode = nodeBefore.nextSibling;
|
|
691
|
-
const isInEmptyNode =
|
|
692
|
-
isNodeEmpty(nodeBefore) ||
|
|
693
|
-
nextNode && nextNode.from === context.pos && isNodeEmpty(nextNode);
|
|
694
|
-
|
|
695
|
-
if (isInEmptyNode) {
|
|
696
|
-
return context.explicit ? {
|
|
697
|
-
from: context.pos,
|
|
698
|
-
options: options
|
|
699
|
-
} : null;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const result = {
|
|
703
|
-
from: nodeBefore.from,
|
|
704
|
-
options: options
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
// Only auto-complete variables
|
|
708
|
-
if ((nodeBefore.parent && nodeBefore.parent.name !== 'VariableName') || isPathExpression(nodeBefore)) {
|
|
709
|
-
return null;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
return result;
|
|
713
|
-
};
|
|
714
|
-
|
|
715
995
|
/**
|
|
716
|
-
* @param {import('..').
|
|
717
|
-
*
|
|
996
|
+
* @param { import('..').Builtin } builtin
|
|
997
|
+
*
|
|
998
|
+
* @returns { import('..').Variable } variable
|
|
718
999
|
*/
|
|
719
|
-
function
|
|
720
|
-
if (variable.type === 'function') {
|
|
721
|
-
return createFunctionVariable(variable);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
return {
|
|
725
|
-
label: variable.name,
|
|
726
|
-
type: 'variable',
|
|
727
|
-
info: variable.info,
|
|
728
|
-
detail: variable.detail
|
|
729
|
-
};
|
|
730
|
-
}
|
|
1000
|
+
function parseBuiltin(builtin) {
|
|
731
1001
|
|
|
732
|
-
/**
|
|
733
|
-
* @param {import('..').Variable} variable
|
|
734
|
-
* @returns {import('@codemirror/autocomplete').Completion}
|
|
735
|
-
*/
|
|
736
|
-
function createFunctionVariable(variable) {
|
|
737
1002
|
const {
|
|
738
1003
|
name,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
params = []
|
|
742
|
-
} = variable;
|
|
743
|
-
|
|
744
|
-
const paramsWithNames = params.map(({ name, type }, index) => ({
|
|
745
|
-
name: name || `param ${index + 1}`,
|
|
746
|
-
type
|
|
747
|
-
}));
|
|
1004
|
+
description
|
|
1005
|
+
} = builtin;
|
|
748
1006
|
|
|
749
|
-
const
|
|
1007
|
+
const match = name.match(/^([\w\s]+)\((.*)\)$/);
|
|
1008
|
+
const functionName = match[1];
|
|
1009
|
+
const functionArguments = match[2];
|
|
750
1010
|
|
|
751
|
-
const
|
|
752
|
-
type ? `${name}: ${type}` : name
|
|
753
|
-
)).join(', ');
|
|
754
|
-
const label = `${name}(${paramsSignature})`;
|
|
1011
|
+
const params = functionArguments.split(', ').map(name => ({ name }));
|
|
755
1012
|
|
|
756
|
-
return
|
|
757
|
-
|
|
1013
|
+
return {
|
|
1014
|
+
name: functionName,
|
|
758
1015
|
type: 'function',
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
return [
|
|
766
|
-
autocompletion$1({
|
|
767
|
-
override: [
|
|
768
|
-
variables,
|
|
769
|
-
builtins,
|
|
770
|
-
completeFromList(snippets.map(s => ({ ...s, boost: -1 }))),
|
|
771
|
-
pathExpression,
|
|
772
|
-
...keywordCompletions
|
|
773
|
-
]
|
|
774
|
-
})
|
|
775
|
-
];
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function language() {
|
|
779
|
-
return new LanguageSupport(feelLanguage, [ ]);
|
|
1016
|
+
params,
|
|
1017
|
+
info: () => {
|
|
1018
|
+
return domify(`<div class="description">${description}<div>`);
|
|
1019
|
+
},
|
|
1020
|
+
boost: 0
|
|
1021
|
+
};
|
|
780
1022
|
}
|
|
781
1023
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const baseTheme = EditorView.theme({
|
|
785
|
-
'& .cm-content': {
|
|
786
|
-
padding: '0px',
|
|
787
|
-
},
|
|
788
|
-
'& .cm-line': {
|
|
789
|
-
padding: '0px',
|
|
790
|
-
},
|
|
791
|
-
'&.cm-editor.cm-focused': {
|
|
792
|
-
outline: 'none',
|
|
793
|
-
},
|
|
794
|
-
'& .cm-completionInfo': {
|
|
795
|
-
whiteSpace: 'pre-wrap',
|
|
796
|
-
overflow: 'hidden',
|
|
797
|
-
textOverflow: 'ellipsis'
|
|
798
|
-
},
|
|
799
|
-
|
|
800
|
-
// Don't wrap whitespace for custom HTML
|
|
801
|
-
'& .cm-completionInfo > *': {
|
|
802
|
-
whiteSpace: 'normal'
|
|
803
|
-
},
|
|
804
|
-
'& .cm-completionInfo ul': {
|
|
805
|
-
margin: 0,
|
|
806
|
-
paddingLeft: '15px'
|
|
807
|
-
},
|
|
808
|
-
'& .cm-completionInfo pre': {
|
|
809
|
-
marginBottom: 0,
|
|
810
|
-
whiteSpace: 'pre-wrap'
|
|
811
|
-
},
|
|
812
|
-
'& .cm-completionInfo p': {
|
|
813
|
-
marginTop: 0,
|
|
814
|
-
},
|
|
815
|
-
'& .cm-completionInfo p:not(:last-of-type)': {
|
|
816
|
-
marginBottom: 0,
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
const highlightTheme = EditorView.baseTheme({
|
|
821
|
-
'& .variableName': {
|
|
822
|
-
color: '#10f'
|
|
823
|
-
},
|
|
824
|
-
'& .number': {
|
|
825
|
-
color: '#164'
|
|
826
|
-
},
|
|
827
|
-
'& .string': {
|
|
828
|
-
color: '#a11'
|
|
829
|
-
},
|
|
830
|
-
'& .bool': {
|
|
831
|
-
color: '#219'
|
|
832
|
-
},
|
|
833
|
-
'& .function': {
|
|
834
|
-
color: '#aa3731',
|
|
835
|
-
fontWeight: 'bold'
|
|
836
|
-
},
|
|
837
|
-
'& .control': {
|
|
838
|
-
color: '#708'
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
const syntaxClasses = syntaxHighlighting(
|
|
843
|
-
HighlightStyle.define([
|
|
844
|
-
{ tag: tags$1.variableName, class: 'variableName' },
|
|
845
|
-
{ tag: tags$1.name, class: 'variableName' },
|
|
846
|
-
{ tag: tags$1.number, class: 'number' },
|
|
847
|
-
{ tag: tags$1.string, class: 'string' },
|
|
848
|
-
{ tag: tags$1.bool, class: 'bool' },
|
|
849
|
-
{ tag: tags$1.function(tags$1.variableName), class: 'function' },
|
|
850
|
-
{ tag: tags$1.function(tags$1.special(tags$1.variableName)), class: 'function' },
|
|
851
|
-
{ tag: tags$1.controlKeyword, class: 'control' },
|
|
852
|
-
{ tag: tags$1.operatorKeyword, class: 'control' }
|
|
853
|
-
])
|
|
854
|
-
);
|
|
1024
|
+
const camunda = parseBuiltins(camundaTags);
|
|
855
1025
|
|
|
856
|
-
|
|
1026
|
+
/**
|
|
1027
|
+
* @typedef { import('./core').Variable } Variable
|
|
1028
|
+
*/
|
|
857
1029
|
|
|
858
1030
|
/**
|
|
859
|
-
* @typedef {object}
|
|
860
|
-
* @property {string} name
|
|
861
|
-
* @property {string}
|
|
862
|
-
* @property {string} [detail] longer description of the variable content
|
|
863
|
-
* @property {boolean} [isList] whether the variable is a list
|
|
864
|
-
* @property {Array<Variable>} [schema] array of child variables if the variable is a context or list
|
|
865
|
-
* @property {'function'|'variable'} [type] type of the variable
|
|
866
|
-
* @property {Array<{name: string, type: string}>} [params] function parameters
|
|
1031
|
+
* @typedef {object} Builtin
|
|
1032
|
+
* @property {string} name
|
|
1033
|
+
* @property {string} description
|
|
867
1034
|
*/
|
|
868
1035
|
|
|
869
|
-
const
|
|
1036
|
+
const coreConf = new Compartment();
|
|
1037
|
+
const placeholderConf = new Compartment();
|
|
1038
|
+
|
|
870
1039
|
|
|
871
1040
|
/**
|
|
872
1041
|
* Creates a FEEL editor in the supplied container
|
|
@@ -874,6 +1043,7 @@ const autocompletionConf = new Compartment();
|
|
|
874
1043
|
* @param {Object} config
|
|
875
1044
|
* @param {DOMNode} config.container
|
|
876
1045
|
* @param {Extension[]} [config.extensions]
|
|
1046
|
+
* @param {Dialect} [config.dialect='expression']
|
|
877
1047
|
* @param {DOMNode|String} [config.tooltipContainer]
|
|
878
1048
|
* @param {Function} [config.onChange]
|
|
879
1049
|
* @param {Function} [config.onKeyDown]
|
|
@@ -881,19 +1051,23 @@ const autocompletionConf = new Compartment();
|
|
|
881
1051
|
* @param {Boolean} [config.readOnly]
|
|
882
1052
|
* @param {String} [config.value]
|
|
883
1053
|
* @param {Variable[]} [config.variables]
|
|
1054
|
+
* @param {Variable[]} [config.builtins]
|
|
884
1055
|
*
|
|
885
1056
|
* @returns {Object} editor
|
|
886
1057
|
*/
|
|
887
1058
|
function FeelEditor({
|
|
888
1059
|
extensions: editorExtensions = [],
|
|
1060
|
+
dialect = 'expression',
|
|
889
1061
|
container,
|
|
890
1062
|
contentAttributes = {},
|
|
891
1063
|
tooltipContainer,
|
|
892
1064
|
onChange = () => {},
|
|
893
1065
|
onKeyDown = () => {},
|
|
894
1066
|
onLint = () => {},
|
|
1067
|
+
placeholder: placeholder$1 = '',
|
|
895
1068
|
readOnly = false,
|
|
896
1069
|
value = '',
|
|
1070
|
+
builtins = camunda,
|
|
897
1071
|
variables = []
|
|
898
1072
|
}) {
|
|
899
1073
|
|
|
@@ -934,21 +1108,25 @@ function FeelEditor({
|
|
|
934
1108
|
}) : [];
|
|
935
1109
|
|
|
936
1110
|
const extensions = [
|
|
937
|
-
autocompletionConf.of(variablesFacet.of(variables)),
|
|
938
1111
|
autocompletion(),
|
|
1112
|
+
coreConf.of(configure({
|
|
1113
|
+
dialect,
|
|
1114
|
+
builtins,
|
|
1115
|
+
variables
|
|
1116
|
+
})),
|
|
939
1117
|
bracketMatching(),
|
|
940
|
-
|
|
1118
|
+
indentOnInput(),
|
|
941
1119
|
closeBrackets(),
|
|
942
1120
|
EditorView.contentAttributes.of(contentAttributes),
|
|
943
|
-
|
|
1121
|
+
changeHandler,
|
|
944
1122
|
keyHandler,
|
|
945
1123
|
keymap.of([
|
|
946
1124
|
...defaultKeymap,
|
|
947
1125
|
]),
|
|
948
|
-
language(),
|
|
949
1126
|
linter,
|
|
950
1127
|
lintHandler,
|
|
951
1128
|
tooltipLayout,
|
|
1129
|
+
placeholderConf.of(placeholder(placeholder$1)),
|
|
952
1130
|
theme,
|
|
953
1131
|
...editorExtensions
|
|
954
1132
|
];
|
|
@@ -960,7 +1138,7 @@ function FeelEditor({
|
|
|
960
1138
|
this._cmEditor = new EditorView({
|
|
961
1139
|
state: EditorState.create({
|
|
962
1140
|
doc: value,
|
|
963
|
-
extensions
|
|
1141
|
+
extensions
|
|
964
1142
|
}),
|
|
965
1143
|
parent: container
|
|
966
1144
|
});
|
|
@@ -1013,12 +1191,35 @@ FeelEditor.prototype.getSelection = function() {
|
|
|
1013
1191
|
|
|
1014
1192
|
/**
|
|
1015
1193
|
* Set variables to be used for autocompletion.
|
|
1194
|
+
*
|
|
1016
1195
|
* @param {Variable[]} variables
|
|
1017
|
-
* @returns {void}
|
|
1018
1196
|
*/
|
|
1019
1197
|
FeelEditor.prototype.setVariables = function(variables) {
|
|
1198
|
+
|
|
1199
|
+
const {
|
|
1200
|
+
dialect,
|
|
1201
|
+
builtins
|
|
1202
|
+
} = get(this._cmEditor.state);
|
|
1203
|
+
|
|
1204
|
+
this._cmEditor.dispatch({
|
|
1205
|
+
effects: [
|
|
1206
|
+
coreConf.reconfigure(configure({
|
|
1207
|
+
dialect,
|
|
1208
|
+
builtins,
|
|
1209
|
+
variables
|
|
1210
|
+
}))
|
|
1211
|
+
]
|
|
1212
|
+
});
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Update placeholder text.
|
|
1217
|
+
*
|
|
1218
|
+
* @param {string} placeholder
|
|
1219
|
+
*/
|
|
1220
|
+
FeelEditor.prototype.setPlaceholder = function(placeholder$1) {
|
|
1020
1221
|
this._cmEditor.dispatch({
|
|
1021
|
-
effects:
|
|
1222
|
+
effects: placeholderConf.reconfigure(placeholder(placeholder$1))
|
|
1022
1223
|
});
|
|
1023
1224
|
};
|
|
1024
1225
|
|