A Monaco-based expression editor with intellisense, syntax highlighting, and validation for a custom expression language.
- Syntax highlighting based on the Expresso language syntax
- IntelliSense with autocompletion for variables, functions, operators
- Dynamic context variables support with nested object property suggestions
- Hover documentation for functions and operators
- Real-time validation
- Custom themes and styling
- Based on Monaco Editor (same as VS Code)
npm install exsense-editor monaco-editor
yarn add exsense-editor monaco-editor
<!DOCTYPE html>
<html>
<head>
<title>ExSense Editor</title>
<!-- Include the ExSense CSS -->
<link rel="stylesheet" href="node_modules/exsense-editor/dist/exsense.css">
<style>
#editor-container {
width: 800px;
height: 300px;
margin: 20px auto;
}
#error-container {
width: 800px;
margin: 10px auto;
color: red;
}
</style>
</head>
<body>
<div id="editor-container"></div>
<div id="error-container"></div>
<!-- Include Monaco Editor first -->
<script src="node_modules/monaco-editor/min/vs/loader.js"></script>
<!-- Then include ExSense -->
<script src="node_modules/exsense-editor/dist/exsense.min.js"></script>
<script>
// Configure loader for Monaco Editor
require.config({ paths: { 'vs': 'node_modules/monaco-editor/min/vs' }});
// After Monaco is loaded, initialize ExSense
require(['vs/editor/editor.main'], function() {
// Create an instance of ExSense
const exsense = new ExSense(
'editor-container',
'error-container',
'node_modules/exsense-editor/dist/syntax-rules.json',
{
user: {
name: "John Doe",
age: 32,
roles: ["admin", "editor"]
},
settings: {
currency: "USD",
taxRate: 0.08
}
}
);
// Initialize the editor
exsense.initialize()
.then(() => {
// Set initial expression
exsense.setValue('$user.age > 18 && contains($user.roles, "admin")');
// Subscribe to validation changes
exsense.onValidationChange((markers, severity) => {
const errorCount = markers.filter(m => m.severity === severity.Error).length;
console.log(`Validation: ${errorCount} errors`);
});
// Subscribe to content changes
exsense.onContentChange((content) => {
console.log('Expression changed:', content);
});
// Subscribe to action button clicks
exsense.onAction((data) => {
console.log('Action triggered with expression:', data.expression);
});
});
});
</script>
</body>
</html>
import { ExSense } from 'exsense-editor';
import 'exsense-editor/dist/exsense.css';
// You need to ensure Monaco Editor is loaded before initializing ExSense
// This can be done using Monaco's loader or a package like monaco-editor-webpack-plugin
const initEditor = async () => {
const exsense = new ExSense(
'editor-container',
'error-container',
'/path/to/syntax-rules.json',
{
user: { name: 'John', age: 25 }
}
);
await exsense.initialize();
exsense.setValue('$user.age >= 18');
// Subscribe to events
exsense.onContentChange(content => {
console.log('Content changed:', content);
});
};
initEditor();
First, install both ExSense and Monaco Editor:
npm install exsense-editor monaco-editor
In your angular.json
, add Monaco Editor assets:
{
"projects": {
"your-project": {
"architect": {
"build": {
"options": {
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "node_modules/monaco-editor/min",
"output": "/assets/monaco/"
},
{
"glob": "syntax-rules.json",
"input": "node_modules/exsense-editor/dist",
"output": "/assets/"
}
],
"styles": [
"src/styles.css",
"node_modules/exsense-editor/dist/exsense.css"
]
}
}
}
}
}
}
// exsense.component.ts
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef } from '@angular/core';
import { ExSense } from 'exsense-editor';
@Component({
selector: 'app-exsense',
template: `
<div class="exsense-container">
<div [id]="editorId" class="monaco-editor-container"></div>
<div [id]="errorId" class="error-container"></div>
</div>
`,
styles: [`
.exsense-container {
width: 100%;
height: 300px;
}
.monaco-editor-container {
width: 100%;
height: 280px;
}
.error-container {
width: 100%;
min-height: 20px;
color: red;
}
`]
})
export class ExSenseComponent implements OnInit, OnDestroy {
@Input() value: string = '';
@Input() context: Record<string, any> = {};
@Output() valueChange = new EventEmitter<string>();
@Output() validationChange = new EventEmitter<any>();
@Output() action = new EventEmitter<any>();
private editorId = `editor-${Math.random().toString(36).substr(2, 9)}`;
private errorId = `error-${Math.random().toString(36).substr(2, 9)}`;
private editor: any;
constructor(private el: ElementRef) {}
ngOnInit() {
// Load Monaco Editor
const onGotAmdLoader = () => {
// Load monaco
(<any>window).require.config({ paths: { 'vs': 'assets/monaco/vs' } });
(<any>window).require(['vs/editor/editor.main'], () => {
this.initializeExSense();
});
};
// Load AMD loader if necessary
if (!(<any>window).require) {
const loaderScript = document.createElement('script');
loaderScript.type = 'text/javascript';
loaderScript.src = 'assets/monaco/vs/loader.js';
loaderScript.addEventListener('load', onGotAmdLoader);
document.body.appendChild(loaderScript);
} else {
onGotAmdLoader();
}
}
ngOnDestroy() {
if (this.editor) {
this.editor.dispose();
}
}
private async initializeExSense() {
this.editor = new ExSense(
this.editorId,
this.errorId,
'assets/syntax-rules.json',
this.context
);
await this.editor.initialize();
if (this.value) {
this.editor.setValue(this.value);
}
this.editor.onContentChange((content: string) => {
this.valueChange.emit(content);
});
this.editor.onValidationChange((markers: any[], severity: any) => {
this.validationChange.emit({ markers, severity });
});
this.editor.onAction((data: any) => {
this.action.emit(data);
});
}
// Public API
public setValue(value: string) {
if (this.editor) {
this.editor.setValue(value);
}
}
public getValue(): string {
return this.editor ? this.editor.getValue() : '';
}
public setContextVariables(context: Record<string, any>) {
if (this.editor) {
this.editor.setContextVariables(context);
}
}
}
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ExSenseComponent } from './exsense.component';
@NgModule({
declarations: [
AppComponent,
ExSenseComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div>
<h1>ExSense Editor</h1>
<app-exsense
[value]="expression"
[context]="contextVariables"
(valueChange)="onExpressionChange($event)"
(validationChange)="onValidationChange($event)"
(action)="onAction($event)"
></app-exsense>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="result" class="result">{{ result }}</div>
</div>
`,
styles: [`
.error { color: red; margin: 10px 0; }
.result { color: green; margin: 10px 0; }
`]
})
export class AppComponent {
expression = '$user.age > 21 && contains($user.roles, "admin")';
contextVariables = {
user: {
name: 'Alice',
age: 30,
roles: ['admin', 'user'],
preferences: {
theme: 'dark'
}
}
};
error: string | null = null;
result: string | null = null;
onExpressionChange(expression: string) {
console.log('Expression changed:', expression);
}
onValidationChange(event: any) {
const errorCount = event.markers.filter((m: any) => m.severity === event.severity.Error).length;
this.error = errorCount > 0 ? `Expression has ${errorCount} errors` : null;
}
onAction(data: any) {
this.result = `Evaluated: "${data.expression}"`;
// In a real app, you would evaluate the expression here
}
}
ExSense can be integrated with Docusaurus documentation sites to provide interactive examples.
npm install exsense-editor monaco-editor
- Create a React component in your Docusaurus project:
// src/components/ExSenseEditor.jsx
import React, { useEffect, useRef, useState } from 'react';
import BrowserOnly from '@docusaurus/BrowserOnly';
import useIsBrowser from '@docusaurus/useIsBrowser';
import { ExSense } from 'exsense-editor';
import 'exsense-editor/dist/exsense.css';
export default function ExSenseEditor({ value, context, height }) {
const isBrowser = useIsBrowser();
if (!isBrowser) {
return <div>ExSense Editor (requires browser JavaScript)</div>;
}
return (
<BrowserOnly>
{() => {
// Implementation that loads ExSense in the browser
// See full documentation for details
}}
</BrowserOnly>
);
}
- Use it in your MDX documentation:
---
title: Expression Editor Examples
---
import ExSenseEditor from '@site/src/components/ExSenseEditor';
# Expression Editor Examples
<ExSenseEditor
value="$user.age > 18"
context={{ user: { name: "John", age: 30 } }}
height="250px"
/>
For detailed Docusaurus integration instructions, see the files in this repository:
- docusaurus-integration.md - Basic integration guide
- docusaurus-integration-advanced.md - Advanced optimization techniques
- docusaurus-example.mdx - Example MDX usage
// Create a new instance
const exsense = new ExSense(
'editor-container-id', // DOM Element ID for editor
'error-container-id', // DOM Element ID for error messages
'path/to/syntax-rules.json', // Path to syntax rules file
contextVariables, // Optional object with context variables
config // Optional configuration object
);
// Initialize the editor
await exsense.initialize();
// Get the current expression
const value = exsense.getValue();
// Set an expression
exsense.setValue('$user.age > 18');
// Update context variables dynamically
exsense.setContextVariables({
user: {
name: 'John',
age: 30,
roles: ['admin', 'user']
}
});
// Subscribe to events (new subscription pattern)
const actionSubscription = exsense.onAction((data) => {
console.log('Action triggered:', data.expression);
});
// Unsubscribe when done
actionSubscription.unsubscribe();
// Dispose when done
exsense.dispose();
const config = {
// Show/hide toolbar
showToolbar: true,
// Configure which toolbar buttons to show
toolbarButtons: {
editContext: true,
examples: true,
action: true
},
// List of example expressions
examplesList: [
{ name: 'Simple Condition', expression: '$user.age > 18' },
{ name: 'Complex Logic', expression: '$user.isActive && contains($user.roles, "admin")' }
],
// Context button configuration
contextButton: {
label: 'Edit Context',
icon: '<svg>...</svg>', // SVG icon string
style: {
backgroundColor: '#f0f0f0',
color: '#333'
}
},
// Examples button configuration
examplesButton: {
label: 'Examples',
icon: '📋',
emptyText: 'No examples available',
style: {
backgroundColor: '#f0f0f0'
}
},
// Action button configuration
actionButton: {
label: 'Evaluate',
icon: '<svg>...</svg>', // SVG icon string
style: {
backgroundColor: '#4CAF50',
color: 'white'
}
}
};
MIT