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