exsense-editor
TypeScript icon, indicating that this package has built-in type declarations

0.0.2 • Public • Published

ExSense Expression Editor

A Monaco-based expression editor with intellisense, syntax highlighting, and validation for a custom expression language.

Features

  • 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)

Installation

NPM

npm install exsense-editor monaco-editor

Yarn

yarn add exsense-editor monaco-editor

Basic Usage

HTML and JavaScript

<!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>

ES Modules

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();

Angular Integration

Installation

First, install both ExSense and Monaco Editor:

npm install exsense-editor monaco-editor

Step 1: Configure 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"
            ]
          }
        }
      }
    }
  }
}

Step 2: Create an ExSense Component

// 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);
    }
  }
}

Step 3: Add the Component to a Module

// 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 { }

Step 4: Use the Component

// 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
  }
}

Docusaurus Integration

ExSense can be integrated with Docusaurus documentation sites to provide interactive examples.

Installation in Docusaurus

npm install exsense-editor monaco-editor

Basic Integration

  1. 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>
  );
}
  1. 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:

Component API

// 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();

Configuration Options

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'
    }
  }
};

License

MIT

Package Sidebar

Install

npm i exsense-editor

Weekly Downloads

0

Version

0.0.2

License

MIT

Unpacked Size

264 kB

Total Files

9

Last publish

Collaborators

  • glaamouri