@bpmn-io/feel-editor 0.6.0 → 0.7.1

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 CHANGED
@@ -28,8 +28,9 @@ const editor = new FeelEditor({
28
28
 
29
29
  ### Variables
30
30
 
31
- You can provide a variables array that will be used for auto completion. The Variables
32
- need to be in the following format:
31
+ You can provide a variables array that will be used for auto completion. Nested
32
+ structures are supported.
33
+ The Variables need to be in the following format:
33
34
 
34
35
  ```JavaScript
35
36
  const editor = new FeelEditor({
@@ -38,7 +39,13 @@ const editor = new FeelEditor({
38
39
  {
39
40
  name: 'variablename to match',
40
41
  detail: 'optional inline info',
41
- info: 'optional pop-out info'
42
+ info: 'optional pop-out info',
43
+ entries: [
44
+ {
45
+ name: 'nested variable',
46
+ ...
47
+ }
48
+ ]
42
49
  }
43
50
  ]
44
51
  });
@@ -51,7 +58,7 @@ editor.setVariables([
51
58
  {
52
59
  name: 'newName',
53
60
  detail: 'new variable inline info',
54
- info: 'new pop-out info
61
+ info: 'new pop-out info'
55
62
  }
56
63
  ]);
57
64
  ```
package/dist/index.es.js CHANGED
@@ -6,6 +6,7 @@ import { Facet, Compartment, EditorState } from '@codemirror/state';
6
6
  import { EditorView, tooltips, keymap } from '@codemirror/view';
7
7
  import { snippets, feelLanguage } from 'lang-feel';
8
8
  import { domify } from 'min-dom';
9
+ import { cmFeelLinter } from '@bpmn-io/feel-lint';
9
10
  import { tags as tags$1 } from '@lezer/highlight';
10
11
 
11
12
  // helpers ///////////////////////////////
@@ -371,9 +372,10 @@ const options = tags.map(tag => snippetCompletion(
371
372
  label: tag.name,
372
373
  type: 'function',
373
374
  info: () => {
374
- const html = domify(tag.description);
375
+ const html = domify(`<div class="description">${tag.description}<div>`);
375
376
  return html;
376
- }
377
+ },
378
+ boost: -1
377
379
  }
378
380
  ));
379
381
 
@@ -412,6 +414,109 @@ var builtins = context => {
412
414
  */
413
415
  const variablesFacet = Facet.define();
414
416
 
417
+ var pathExpression = context => {
418
+ const variables = context.state.facet(variablesFacet)[0];
419
+ const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);
420
+
421
+ if (!isPathExpression(nodeBefore)) {
422
+ return;
423
+ }
424
+
425
+ const expression = findPathExpression(nodeBefore);
426
+
427
+ // if the cursor is directly after the `.`, variable starts at the cursor position
428
+ const from = nodeBefore === expression ? context.pos : nodeBefore.from;
429
+
430
+ const path = getPath(expression, context);
431
+
432
+ let options = variables;
433
+ for (var i = 0; i < path.length - 1; i++) {
434
+ var childVar = options.find(val => val.name === path[i].name);
435
+
436
+ if (!childVar) {
437
+ return null;
438
+ }
439
+
440
+ // only suggest if variable type matches
441
+ if (
442
+ childVar.isList !== 'optional' &&
443
+ !!childVar.isList !== path[i].isList
444
+ ) {
445
+ return;
446
+ }
447
+
448
+ options = childVar.entries;
449
+ }
450
+
451
+ if (!options) return;
452
+
453
+ options = options.map(v => ({
454
+ label: v.name,
455
+ type: 'variable',
456
+ info: v.info,
457
+ detail: v.detail
458
+ }));
459
+
460
+ const result = {
461
+ from: from,
462
+ options: options
463
+ };
464
+
465
+ return result;
466
+ };
467
+
468
+
469
+ function findPathExpression(node) {
470
+ while (node) {
471
+ if (node.name === 'PathExpression') {
472
+ return node;
473
+ }
474
+ node = node.parent;
475
+ }
476
+ }
477
+
478
+ // parses the path expression into a list of variable names with type information
479
+ // e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
480
+ function getPath(node, context) {
481
+ let path = [];
482
+
483
+ for (let child = node.firstChild; child; child = child.nextSibling) {
484
+ if (child.name === 'PathExpression') {
485
+ path.push(...getPath(child, context));
486
+ } else if (child.name === 'FilterExpression') {
487
+ path.push(...getFilter(child, context));
488
+ }
489
+ else {
490
+ path.push({
491
+ name: getNodeContent(child, context),
492
+ isList: false
493
+ });
494
+ }
495
+ }
496
+ return path;
497
+ }
498
+
499
+ function getFilter(node, context) {
500
+ const list = node.firstChild;
501
+
502
+ if (list.name === 'PathExpression') {
503
+ const path = getPath(list, context);
504
+ const last = path[path.length - 1];
505
+ last.isList = true;
506
+
507
+ return path;
508
+ }
509
+
510
+ return [ {
511
+ name: getNodeContent(list, context),
512
+ isList: true
513
+ } ];
514
+ }
515
+
516
+ function getNodeContent(node, context) {
517
+ return context.state.sliceDoc(node.from, node.to);
518
+ }
519
+
415
520
  /**
416
521
  * @type {import('@codemirror/autocomplete').CompletionSource}
417
522
  */
@@ -463,7 +568,8 @@ function autocompletion() {
463
568
  override: [
464
569
  variables,
465
570
  builtins,
466
- completeFromList(snippets)
571
+ completeFromList(snippets.map(s => ({ ...s, boost: -1 }))),
572
+ pathExpression
467
573
  ]
468
574
  })
469
575
  ];
@@ -473,53 +579,7 @@ function language() {
473
579
  return new LanguageSupport(feelLanguage, [ ]);
474
580
  }
475
581
 
476
- const FeelLinter = function(editorView) {
477
- const messages = [];
478
-
479
- // don't lint if the Editor is empty
480
- if (editorView.state.doc.length === 0) {
481
- return messages;
482
- }
483
-
484
- const tree = syntaxTree(editorView.state);
485
-
486
- tree.iterate({
487
- enter: node => {
488
- if (node.type.isError) {
489
-
490
- const error = node.toString();
491
-
492
- /* The error has the pattern [⚠ || ⚠(NodeType)]. The regex extracts the node type from inside the brackets */
493
- const match = /\((.*?)\)/.exec(error);
494
- const nodeType = match && match[1];
495
-
496
- let message;
497
-
498
- if (nodeType) {
499
- message = 'unexpected ' + nodeType;
500
- } else {
501
- message = 'expression expected';
502
- }
503
-
504
- messages.push(
505
- {
506
- from: node.from,
507
- to: node.to,
508
- severity: 'error',
509
- message: message,
510
- source: 'syntaxError'
511
- }
512
- );
513
- }
514
- }
515
- });
516
-
517
- return messages;
518
- };
519
-
520
- var syntaxLinter = linter$1(FeelLinter);
521
-
522
- var linter = [ syntaxLinter ];
582
+ var linter = [ linter$1(cmFeelLinter()) ];
523
583
 
524
584
  const baseTheme = EditorView.theme({
525
585
  '& .cm-content': {
@@ -531,6 +591,16 @@ const baseTheme = EditorView.theme({
531
591
  '&.cm-editor.cm-focused': {
532
592
  outline: 'none',
533
593
  },
594
+ '& .cm-completionInfo': {
595
+ whiteSpace: 'pre-wrap',
596
+ overflow: 'hidden',
597
+ textOverflow: 'ellipsis'
598
+ },
599
+
600
+ // Don't wrap whitespace for custom HTML
601
+ '& .cm-completionInfo > *': {
602
+ whiteSpace: 'normal'
603
+ },
534
604
  '& .cm-completionInfo ul': {
535
605
  margin: 0,
536
606
  paddingLeft: '15px'
@@ -587,9 +657,11 @@ var theme = [ baseTheme, highlightTheme, syntaxClasses ];
587
657
 
588
658
  /**
589
659
  * @typedef {object} Variable
590
- * @property {string} name
591
- * @property {string} [info]
592
- * @property {string} [detail]
660
+ * @property {string} name name or key of the variable
661
+ * @property {string} [info] short information about the variable, e.g. type
662
+ * @property {string} [detail] longer description of the variable content
663
+ * @property {boolean} [isList] whether the variable is a list
664
+ * @property {array<Variable>} [schema] array of child variables if the variable is a context or list
593
665
  */
594
666
 
595
667
  const autocompletionConf = new Compartment();
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ var state = require('@codemirror/state');
8
8
  var view = require('@codemirror/view');
9
9
  var langFeel = require('lang-feel');
10
10
  var minDom = require('min-dom');
11
+ var feelLint = require('@bpmn-io/feel-lint');
11
12
  var highlight = require('@lezer/highlight');
12
13
 
13
14
  // helpers ///////////////////////////////
@@ -373,9 +374,10 @@ const options = tags.map(tag => autocomplete.snippetCompletion(
373
374
  label: tag.name,
374
375
  type: 'function',
375
376
  info: () => {
376
- const html = minDom.domify(tag.description);
377
+ const html = minDom.domify(`<div class="description">${tag.description}<div>`);
377
378
  return html;
378
- }
379
+ },
380
+ boost: -1
379
381
  }
380
382
  ));
381
383
 
@@ -414,6 +416,109 @@ var builtins = context => {
414
416
  */
415
417
  const variablesFacet = state.Facet.define();
416
418
 
419
+ var pathExpression = context => {
420
+ const variables = context.state.facet(variablesFacet)[0];
421
+ const nodeBefore = language$1.syntaxTree(context.state).resolve(context.pos, -1);
422
+
423
+ if (!isPathExpression(nodeBefore)) {
424
+ return;
425
+ }
426
+
427
+ const expression = findPathExpression(nodeBefore);
428
+
429
+ // if the cursor is directly after the `.`, variable starts at the cursor position
430
+ const from = nodeBefore === expression ? context.pos : nodeBefore.from;
431
+
432
+ const path = getPath(expression, context);
433
+
434
+ let options = variables;
435
+ for (var i = 0; i < path.length - 1; i++) {
436
+ var childVar = options.find(val => val.name === path[i].name);
437
+
438
+ if (!childVar) {
439
+ return null;
440
+ }
441
+
442
+ // only suggest if variable type matches
443
+ if (
444
+ childVar.isList !== 'optional' &&
445
+ !!childVar.isList !== path[i].isList
446
+ ) {
447
+ return;
448
+ }
449
+
450
+ options = childVar.entries;
451
+ }
452
+
453
+ if (!options) return;
454
+
455
+ options = options.map(v => ({
456
+ label: v.name,
457
+ type: 'variable',
458
+ info: v.info,
459
+ detail: v.detail
460
+ }));
461
+
462
+ const result = {
463
+ from: from,
464
+ options: options
465
+ };
466
+
467
+ return result;
468
+ };
469
+
470
+
471
+ function findPathExpression(node) {
472
+ while (node) {
473
+ if (node.name === 'PathExpression') {
474
+ return node;
475
+ }
476
+ node = node.parent;
477
+ }
478
+ }
479
+
480
+ // parses the path expression into a list of variable names with type information
481
+ // e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
482
+ function getPath(node, context) {
483
+ let path = [];
484
+
485
+ for (let child = node.firstChild; child; child = child.nextSibling) {
486
+ if (child.name === 'PathExpression') {
487
+ path.push(...getPath(child, context));
488
+ } else if (child.name === 'FilterExpression') {
489
+ path.push(...getFilter(child, context));
490
+ }
491
+ else {
492
+ path.push({
493
+ name: getNodeContent(child, context),
494
+ isList: false
495
+ });
496
+ }
497
+ }
498
+ return path;
499
+ }
500
+
501
+ function getFilter(node, context) {
502
+ const list = node.firstChild;
503
+
504
+ if (list.name === 'PathExpression') {
505
+ const path = getPath(list, context);
506
+ const last = path[path.length - 1];
507
+ last.isList = true;
508
+
509
+ return path;
510
+ }
511
+
512
+ return [ {
513
+ name: getNodeContent(list, context),
514
+ isList: true
515
+ } ];
516
+ }
517
+
518
+ function getNodeContent(node, context) {
519
+ return context.state.sliceDoc(node.from, node.to);
520
+ }
521
+
417
522
  /**
418
523
  * @type {import('@codemirror/autocomplete').CompletionSource}
419
524
  */
@@ -465,7 +570,8 @@ function autocompletion() {
465
570
  override: [
466
571
  variables,
467
572
  builtins,
468
- autocomplete.completeFromList(langFeel.snippets)
573
+ autocomplete.completeFromList(langFeel.snippets.map(s => ({ ...s, boost: -1 }))),
574
+ pathExpression
469
575
  ]
470
576
  })
471
577
  ];
@@ -475,53 +581,7 @@ function language() {
475
581
  return new language$1.LanguageSupport(langFeel.feelLanguage, [ ]);
476
582
  }
477
583
 
478
- const FeelLinter = function(editorView) {
479
- const messages = [];
480
-
481
- // don't lint if the Editor is empty
482
- if (editorView.state.doc.length === 0) {
483
- return messages;
484
- }
485
-
486
- const tree = language$1.syntaxTree(editorView.state);
487
-
488
- tree.iterate({
489
- enter: node => {
490
- if (node.type.isError) {
491
-
492
- const error = node.toString();
493
-
494
- /* The error has the pattern [⚠ || ⚠(NodeType)]. The regex extracts the node type from inside the brackets */
495
- const match = /\((.*?)\)/.exec(error);
496
- const nodeType = match && match[1];
497
-
498
- let message;
499
-
500
- if (nodeType) {
501
- message = 'unexpected ' + nodeType;
502
- } else {
503
- message = 'expression expected';
504
- }
505
-
506
- messages.push(
507
- {
508
- from: node.from,
509
- to: node.to,
510
- severity: 'error',
511
- message: message,
512
- source: 'syntaxError'
513
- }
514
- );
515
- }
516
- }
517
- });
518
-
519
- return messages;
520
- };
521
-
522
- var syntaxLinter = lint.linter(FeelLinter);
523
-
524
- var linter = [ syntaxLinter ];
584
+ var linter = [ lint.linter(feelLint.cmFeelLinter()) ];
525
585
 
526
586
  const baseTheme = view.EditorView.theme({
527
587
  '& .cm-content': {
@@ -533,6 +593,16 @@ const baseTheme = view.EditorView.theme({
533
593
  '&.cm-editor.cm-focused': {
534
594
  outline: 'none',
535
595
  },
596
+ '& .cm-completionInfo': {
597
+ whiteSpace: 'pre-wrap',
598
+ overflow: 'hidden',
599
+ textOverflow: 'ellipsis'
600
+ },
601
+
602
+ // Don't wrap whitespace for custom HTML
603
+ '& .cm-completionInfo > *': {
604
+ whiteSpace: 'normal'
605
+ },
536
606
  '& .cm-completionInfo ul': {
537
607
  margin: 0,
538
608
  paddingLeft: '15px'
@@ -589,9 +659,11 @@ var theme = [ baseTheme, highlightTheme, syntaxClasses ];
589
659
 
590
660
  /**
591
661
  * @typedef {object} Variable
592
- * @property {string} name
593
- * @property {string} [info]
594
- * @property {string} [detail]
662
+ * @property {string} name name or key of the variable
663
+ * @property {string} [info] short information about the variable, e.g. type
664
+ * @property {string} [detail] longer description of the variable content
665
+ * @property {boolean} [isList] whether the variable is a list
666
+ * @property {array<Variable>} [schema] array of child variables if the variable is a context or list
595
667
  */
596
668
 
597
669
  const autocompletionConf = new state.Compartment();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmn-io/feel-editor",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Editor for FEEL expressions.",
5
5
  "files": [
6
6
  "dist"
@@ -45,6 +45,7 @@
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
47
  "@babel/core": "^7.20.2",
48
+ "@bpmn-io/feel-lint": "^0.1.1",
48
49
  "@codemirror/autocomplete": "^6.3.2",
49
50
  "@codemirror/commands": "^6.1.2",
50
51
  "@codemirror/language": "^6.3.1",
@@ -80,7 +81,7 @@
80
81
  "mocha": "^10.0.0",
81
82
  "mocha-test-container-support": "^0.2.0",
82
83
  "npm-run-all": "^4.1.5",
83
- "puppeteer": "^19.2.2",
84
+ "puppeteer": "^19.3.0",
84
85
  "rollup": "^3.3.0",
85
86
  "sinon": "^14.0.0",
86
87
  "sinon-chai": "^3.7.0",