@bpmn-io/feel-editor 1.5.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 +545 -359
  3. package/dist/index.js +559 -373
  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)",
@@ -491,384 +964,78 @@ var tags = [
491
964
  description: "<p>Returns the day of the week according to the Gregorian calendar. Note that it always returns the English name of the day.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">day of week(date: date): string\n</code></pre>\n<pre><code class=\"language-feel\">day of week(date: date and time): string\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">day of week(date(&quot;2019-09-17&quot;))\n// &quot;Tuesday&quot;\n\nday of week(date and time(&quot;2019-09-17T12:00:00&quot;))\n// &quot;Tuesday&quot;\n</code></pre>\n"
492
965
  },
493
966
  {
494
- name: "day of year(date)",
495
- description: "<p>Returns the Gregorian number of the day within the year.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">day of year(date: date): number\n</code></pre>\n<pre><code class=\"language-feel\">day of year(date: date and time): number\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">day of year(date(&quot;2019-09-17&quot;))\n// 260\n\nday of year(date and time(&quot;2019-09-17T12:00:00&quot;))\n// 260\n</code></pre>\n"
496
- },
497
- {
498
- 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
- };
967
+ name: "day of year(date)",
968
+ description: "<p>Returns the Gregorian number of the day within the year.</p>\n<p><strong>Function signature</strong></p>\n<pre><code class=\"language-feel\">day of year(date: date): number\n</code></pre>\n<pre><code class=\"language-feel\">day of year(date: date and time): number\n</code></pre>\n<p><strong>Examples</strong></p>\n<pre><code class=\"language-feel\">day of year(date(&quot;2019-09-17&quot;))\n// 260\n\nday of year(date and time(&quot;2019-09-17T12:00:00&quot;))\n// 260\n</code></pre>\n"
969
+ },
970
+ {
971
+ name: "week of year(date)",
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();
872
1039
  const placeholderConf = new state.Compartment();
873
1040
 
874
1041
 
@@ -878,6 +1045,7 @@ const placeholderConf = new state.Compartment();
878
1045
  * @param {Object} config
879
1046
  * @param {DOMNode} config.container
880
1047
  * @param {Extension[]} [config.extensions]
1048
+ * @param {Dialect} [config.dialect='expression']
881
1049
  * @param {DOMNode|String} [config.tooltipContainer]
882
1050
  * @param {Function} [config.onChange]
883
1051
  * @param {Function} [config.onKeyDown]
@@ -885,11 +1053,13 @@ const placeholderConf = new state.Compartment();
885
1053
  * @param {Boolean} [config.readOnly]
886
1054
  * @param {String} [config.value]
887
1055
  * @param {Variable[]} [config.variables]
1056
+ * @param {Variable[]} [config.builtins]
888
1057
  *
889
1058
  * @returns {Object} editor
890
1059
  */
891
1060
  function FeelEditor({
892
1061
  extensions: editorExtensions = [],
1062
+ dialect = 'expression',
893
1063
  container,
894
1064
  contentAttributes = {},
895
1065
  tooltipContainer,
@@ -899,6 +1069,7 @@ function FeelEditor({
899
1069
  placeholder = '',
900
1070
  readOnly = false,
901
1071
  value = '',
1072
+ builtins = camunda,
902
1073
  variables = []
903
1074
  }) {
904
1075
 
@@ -939,22 +1110,25 @@ function FeelEditor({
939
1110
  }) : [];
940
1111
 
941
1112
  const extensions = [
942
- autocompletionConf.of(variablesFacet.of(variables)),
943
- autocompletion(),
1113
+ autocomplete.autocompletion(),
1114
+ coreConf.of(configure({
1115
+ dialect,
1116
+ builtins,
1117
+ variables
1118
+ })),
944
1119
  language$1.bracketMatching(),
945
- changeHandler,
1120
+ language$1.indentOnInput(),
946
1121
  autocomplete.closeBrackets(),
947
1122
  view.EditorView.contentAttributes.of(contentAttributes),
948
- language$1.indentOnInput(),
1123
+ changeHandler,
949
1124
  keyHandler,
950
1125
  view.keymap.of([
951
1126
  ...commands.defaultKeymap,
952
1127
  ]),
953
- language(),
954
1128
  linter,
955
1129
  lintHandler,
956
- placeholderConf.of(view.placeholder(placeholder)),
957
1130
  tooltipLayout,
1131
+ placeholderConf.of(view.placeholder(placeholder)),
958
1132
  theme,
959
1133
  ...editorExtensions
960
1134
  ];
@@ -966,7 +1140,7 @@ function FeelEditor({
966
1140
  this._cmEditor = new view.EditorView({
967
1141
  state: state.EditorState.create({
968
1142
  doc: value,
969
- extensions: extensions
1143
+ extensions
970
1144
  }),
971
1145
  parent: container
972
1146
  });
@@ -1019,19 +1193,31 @@ FeelEditor.prototype.getSelection = function() {
1019
1193
 
1020
1194
  /**
1021
1195
  * Set variables to be used for autocompletion.
1196
+ *
1022
1197
  * @param {Variable[]} variables
1023
- * @returns {void}
1024
1198
  */
1025
1199
  FeelEditor.prototype.setVariables = function(variables) {
1200
+
1201
+ const {
1202
+ dialect,
1203
+ builtins
1204
+ } = get(this._cmEditor.state);
1205
+
1026
1206
  this._cmEditor.dispatch({
1027
- effects: autocompletionConf.reconfigure(variablesFacet.of(variables))
1207
+ effects: [
1208
+ coreConf.reconfigure(configure({
1209
+ dialect,
1210
+ builtins,
1211
+ variables
1212
+ }))
1213
+ ]
1028
1214
  });
1029
1215
  };
1030
1216
 
1031
1217
  /**
1032
1218
  * Update placeholder text.
1219
+ *
1033
1220
  * @param {string} placeholder
1034
- * @returns {void}
1035
1221
  */
1036
1222
  FeelEditor.prototype.setPlaceholder = function(placeholder) {
1037
1223
  this._cmEditor.dispatch({