Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ADD_TO_HTO_PROJECT.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458
- uses: actions/add-to-project@5b1a254a3546aef88e0a7724a77a623fa2e47c36
name: Add to project
id: add-project
with:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:

strategy:
matrix:
os: [ macos-latest, ubuntu-20.04, windows-latest ]
os: [ macos-latest, ubuntu-latest, windows-latest ]
node-version: [ 20 ]

runs-on: ${{ matrix.os }}
Expand All @@ -20,6 +20,8 @@ jobs:
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Project setup
uses: bpmn-io/actions/setup@latest
- name: Lint
run: npm run lint
- name: Build
Expand Down
13 changes: 13 additions & 0 deletions feelers-compiler-playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Feelers Compiler Playground</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions feelers-compiler-playground/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"private": true,
"name": "feelers-compiler-playground",
"version": "0.0.0",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src --ext js,jsx"
},
"devDependencies": {
"@preact/preset-vite": "^2.5.0",
"vite": "^4.4.5",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0"
},
"dependencies": {
"preact": "^10.18.1",
"@bpmn-io/feel-editor": "^1.11.0",
"feelin": "^4.0.0"
}
}
202 changes: 202 additions & 0 deletions feelers-compiler-playground/src/CompilerPlayground.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import './style.css';
import { useMemo, useState, useRef, useEffect } from 'preact/hooks';
import FeelersEditor from '../../src/editor';
import { compile } from '../../src/compiler';
import FeelEditor from '@bpmn-io/feel-editor';
import * as feelin from 'feelin';
import { initialTemplate, initialContext } from '../../test/testData';

export default function CompilerPlayground() {

const [ feelersTemplate, setFeelersTemplate ] = useState('');
const [ compiledFeel, setCompiledFeel ] = useState('');
const [ feelContext, setFeelContext ] = useState('{}');
const [ evaluationResult, setEvaluationResult ] = useState('');
const [ compileError, setCompileError ] = useState('');
const [ evalError, setEvalError ] = useState('');

const feelersEditorRef = useRef();
const feelEditorRef = useRef();
const feelersContainerRef = useRef();
const feelContainerRef = useRef();

useEffect(() => {
// Prevent double initialization
if (feelersEditorRef.current || feelEditorRef.current) {
return;
}

// Initialize Feelers editor
feelersEditorRef.current = new FeelersEditor({
container: feelersContainerRef.current,
hostLanguage: 'markdown',
darkMode: false,
value: initialTemplate,
onChange: (value) => setFeelersTemplate(value)
});

// Initialize FEEL editor - use default configuration like the tests show
feelEditorRef.current = new FeelEditor({
container: feelContainerRef.current,
value: '',
readOnly: true
});

setFeelersTemplate(initialTemplate);
setFeelContext(JSON.stringify(initialContext, null, 2));

return () => {
if (feelersEditorRef.current && typeof feelersEditorRef.current.destroy === 'function') {
feelersEditorRef.current.destroy();
feelersEditorRef.current = null;
}
if (feelEditorRef.current && typeof feelEditorRef.current.destroy === 'function') {
feelEditorRef.current.destroy();
feelEditorRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Compile Feelers template to FEEL
const compiledFeelExpression = useMemo(() => {
if (!feelersTemplate.trim()) {
setCompileError('');
return '';
}

try {
const result = compile(feelersTemplate);
setCompileError('');
return result;
} catch (error) {
setCompileError(error.message);
return '';
}
}, [ feelersTemplate ]);

// Update FEEL editor when compiled expression changes
useEffect(() => {
if (feelEditorRef.current && compiledFeelExpression !== compiledFeel) {
// Format the FEEL expression for better readability across multiple lines
let formattedFeel = compiledFeelExpression;

if (formattedFeel) {
formattedFeel = formattedFeel
// Add line breaks after string concatenations
.replace(/\s*\+\s*/g, ' +\n')
// Add line breaks after commas in function calls
.replace(/,\s*/g, ',\n ')
// Add line breaks after control keywords
.replace(/\s+(then|else|return)\s+/g, '\n$1\n ')
// Add line breaks after 'for' and 'in' keywords
.replace(/\s+(for|in)\s+/g, '\n$1 ')
// Add line breaks after 'if' keyword
.replace(/\s+if\s+/g, '\nif ')
// Clean up excessive line breaks
.replace(/\n\s*\n/g, '\n')
// Indent nested expressions
.replace(/^(.+)$/gm, (match, line) => {
const depth = (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length;
return ' '.repeat(Math.max(0, depth)) + line.trim();
});
}

feelEditorRef.current.setValue(formattedFeel);
setCompiledFeel(compiledFeelExpression);
}
}, [ compiledFeelExpression, compiledFeel ]);

// Parse context JSON
const contextJSON = useMemo(() => {
try {
return JSON.parse(feelContext);
} catch (e) {
return null;
}
}, [ feelContext ]);

// Evaluate FEEL expression
const evaluatedResult = useMemo(() => {
if (!compiledFeelExpression || !contextJSON) {
setEvalError('');
return '';
}

try {
const result = feelin.evaluate(compiledFeelExpression, contextJSON);
setEvalError('');
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
setEvalError(error.message);
return '';
}
}, [ compiledFeelExpression, contextJSON ]);

useEffect(() => {
setEvaluationResult(evaluatedResult);
}, [ evaluatedResult ]);

const handleContextChange = (e) => {
setFeelContext(e.target.value);
};

return (
<div className="playground">
<header className="playground-header">
<h1>Feelers Compiler Playground</h1>
<p>Test the Feelers template compiler and see the generated FEEL expressions</p>
</header>

<div className="playground-content">
<div className="panel">
<h3>Feelers Template</h3>
<div ref={feelersContainerRef} className="editor-container" />
{compileError && (
<div className="error-message">
<strong>Compilation Error:</strong> {compileError}
</div>
)}
</div>

<div className="panel">
<h3>Context Data (JSON)</h3>
<textarea
value={feelContext}
onChange={handleContextChange}
className={`context-editor ${!contextJSON ? 'invalid' : ''}`}
placeholder="Enter JSON context data..."
/>
{!contextJSON && feelContext.trim() && (
<div className="error-message">
<strong>Invalid JSON:</strong> Please check your context data format
</div>
)}
</div>

<div className="panel">
<h3>Compiled FEEL Expression</h3>
<div ref={feelContainerRef} className="editor-container" />
<div className="info-message">
This FEEL expression is automatically generated from your Feelers template
</div>
</div>

<div className="panel">
<h3>Evaluation Result</h3>
<textarea
value={evaluationResult}
readOnly
className={`result-output ${evalError ? 'invalid' : ''}`}
placeholder="Result will appear here..."
/>
{evalError && (
<div className="error-message">
<strong>Evaluation Error:</strong> {evalError}
</div>
)}
</div>
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions feelers-compiler-playground/src/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { render } from 'preact';
import CompilerPlayground from './CompilerPlayground';

render(<CompilerPlayground />, document.getElementById('root'));
Loading
Loading