You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/06/10 05:11:16 UTC

(camel) branch main updated: CAMEL-23724: Machine-readable grammar for Simple language with JS validator (#23893)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 3d153fdc6f77 CAMEL-23724: Machine-readable grammar for Simple language with JS validator (#23893)
3d153fdc6f77 is described below

commit 3d153fdc6f7759324d0389d4d1fe80a450ab3ec7
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jun 10 07:11:09 2026 +0200

    CAMEL-23724: Machine-readable grammar for Simple language with JS validator (#23893)
    
    * CAMEL-23724: Add operators section to Simple language catalog
    
    Extends the Simple language JSON catalog (simple.json) with a machine-readable
    operators section documenting all 32 operators with their kind, syntax,
    precedence, descriptions, and usage examples.
    
    Co-Authored-By: Claude <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
    
    * CAMEL-23724: Add JavaScript Simple language validator proof-of-concept
    
    Self-contained JS module that validates Simple expressions in a browser
    or Node.js with no Java backend. Driven by the catalog data from simple.json,
    it checks balanced delimiters, validates function names with typo suggestions,
    verifies operator usage, and provides autocomplete. Includes 49 self-tests.
    
    Co-Authored-By: Claude <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
    
    * CAMEL-23724: Add operators field to YAML DSL Schema class
    
    The YAML DSL maven plugin deserializes simple.json via Jackson and
    needs the operators field to avoid failing on the new section.
    
    Co-Authored-By: Claude <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
    
    * CAMEL-23724: Add HTML demo page for Simple language validator
    
    Browser-based UI to validate Simple expressions and predicates
    with mode toggle, clickable examples, and caret error pointers.
    
    Co-Authored-By: Claude <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
    
    * CAMEL-23724: Add Maven Mojo to auto-generate Simple JS validator
    
    Add generateSimpleValidatorJs() to PrepareCatalogMojo that reads
    simple.json, extracts functions and operators, and generates the
    camel-simple-validator.js from a template. This ensures the JS
    validator stays in sync with the Simple language catalog.
    
    Also adds documentation, Java unit tests, and the HTML demo page.
    
    Co-Authored-By: Claude <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
    
    ---------
    
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
    Co-authored-by: Claude <no...@anthropic.com>
---
 .../org/apache/camel/catalog/languages/simple.json |  34 +
 .../catalog/simple/camel-simple-validator.html     | 241 ++++++
 .../camel/catalog/simple/camel-simple-validator.js | 809 +++++++++++++++++++++
 .../org/apache/camel/catalog/CamelCatalogTest.java |  62 ++
 .../org/apache/camel/spi/annotations/Language.java |  13 +
 .../org/apache/camel/language/simple/simple.json   |  34 +
 .../modules/languages/pages/simple-language.adoc   |  46 ++
 .../camel/language/simple/SimpleLanguage.java      |   2 +-
 .../language/simple/SimpleOperatorConstants.java   | 232 ++++++
 .../camel/maven/dsl/yaml/support/Schema.java       |   2 +
 .../org/apache/camel/tooling/model/JsonMapper.java |  65 ++
 .../apache/camel/tooling/model/LanguageModel.java  |  58 ++
 .../camel/maven/packaging/PackageLanguageMojo.java |  54 ++
 .../camel/maven/packaging/PrepareCatalogMojo.java  |  97 +++
 .../resources/camel-simple-validator-template.js   | 646 ++++++++++++++++
 .../src/main/resources/camel-simple-validator.html | 241 ++++++
 .../org/apache/camel/spi/annotations/Language.java |  13 +
 17 files changed, 2648 insertions(+), 1 deletion(-)

diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
index 1c6720fc16c3..d7224071f387 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
@@ -154,5 +154,39 @@
     "variableAs(key,type)": { "index": 126, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type",  [...]
     "variables": { "index": 127, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
     "xpath(input,exp)": { "index": 128, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
+  },
+  "operators": {
+    "==": { "index": 0, "kind": "operator", "displayName": "Eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.", "operatorKind": "binary", "operatorSyntax": "LHS == RHS", "precedence": 10, "examples": [ "${header.foo} == 'bar'", "${header.count} == 5" ] },
+    "=~": { "index": 1, "kind": "operator", "displayName": "Eq ignore", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values, ignoring case for string comparison.", "operatorKind": "binary", "operatorSyntax": "LHS =~ RHS", "precedence": 10, "examples": [ "${header.foo} =~ 'BAR'" ] },
+    ">": { "index": 2, "kind": "operator", "displayName": "Gt", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is greater than the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS > RHS", "precedence": 10, "examples": [ "${header.count} > 100" ] },
+    ">=": { "index": 3, "kind": "operator", "displayName": "Gte", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is greater than or equal to the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS >= RHS", "precedence": 10, "examples": [ "${header.count} >= 100" ] },
+    "<": { "index": 4, "kind": "operator", "displayName": "Lt", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is less than the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS < RHS", "precedence": 10, "examples": [ "${header.count} < 100" ] },
+    "<=": { "index": 5, "kind": "operator", "displayName": "Lte", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is less than or equal to the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS <= RHS", "precedence": 10, "examples": [ "${header.count} <= 100" ] },
+    "!=": { "index": 6, "kind": "operator", "displayName": "Not eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests inequality between left and right operand values.", "operatorKind": "binary", "operatorSyntax": "LHS != RHS", "precedence": 10, "examples": [ "${header.foo} != 'bar'" ] },
+    "!=~": { "index": 7, "kind": "operator", "displayName": "Not eq ignore", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests inequality between left and right operand values, ignoring case for string comparison.", "operatorKind": "binary", "operatorSyntax": "LHS !=~ RHS", "precedence": 10, "examples": [ "${header.foo} !=~ 'BAR'" ] },
+    "contains": { "index": 8, "kind": "operator", "displayName": "Contains", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string contains the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS contains RHS", "precedence": 10, "examples": [ "${header.title} contains 'Camel'" ] },
+    "!contains": { "index": 9, "kind": "operator", "displayName": "Not contains", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not contain the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !contains RHS", "precedence": 10, "examples": [ "${header.title} !contains 'Camel'" ] },
+    "~~": { "index": 10, "kind": "operator", "displayName": "Contains ignorecase", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string contains the right operand string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS ~~ RHS", "precedence": 10, "examples": [ "${header.title} ~~ 'camel'" ] },
+    "!~~": { "index": 11, "kind": "operator", "displayName": "Not contains ignorecase", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not contain the right operand string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS !~~ RHS", "precedence": 10, "examples": [ "${header.title} !~~ 'camel'" ] },
+    "regex": { "index": 12, "kind": "operator", "displayName": "Regex", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand matches the right operand as a regular expression.", "operatorKind": "binary", "operatorSyntax": "LHS regex 'pattern'", "precedence": 10, "examples": [ "${header.number} regex '\\d{4}'" ] },
+    "!regex": { "index": 13, "kind": "operator", "displayName": "Not regex", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand does not match the right operand as a regular expression.", "operatorKind": "binary", "operatorSyntax": "LHS !regex 'pattern'", "precedence": 10, "examples": [ "${header.number} !regex '\\d{4}'" ] },
+    "in": { "index": 14, "kind": "operator", "displayName": "In", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is in a set of comma-separated values.", "operatorKind": "binary", "operatorSyntax": "LHS in 'val1,val2,...'", "precedence": 10, "examples": [ "${header.type} in 'gold,silver'" ] },
+    "!in": { "index": 15, "kind": "operator", "displayName": "Not in", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not in a set of comma-separated values.", "operatorKind": "binary", "operatorSyntax": "LHS !in 'val1,val2,...'", "precedence": 10, "examples": [ "${header.type} !in 'gold,silver'" ] },
+    "is": { "index": 16, "kind": "operator", "displayName": "Is", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is an instance of the right operand type (Java classname or short name).", "operatorKind": "binary", "operatorSyntax": "LHS is 'typeName'", "precedence": 10, "examples": [ "${header.type} is 'String'", "${body} is 'java.util.List'" ] },
+    "!is": { "index": 17, "kind": "operator", "displayName": "Not is", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not an instance of the right operand type.", "operatorKind": "binary", "operatorSyntax": "LHS !is 'typeName'", "precedence": 10, "examples": [ "${header.type} !is 'String'" ] },
+    "range": { "index": 18, "kind": "operator", "displayName": "Range", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is within the numeric range specified by 'from..to'.", "operatorKind": "binary", "operatorSyntax": "LHS range 'from..to'", "precedence": 10, "examples": [ "${header.number} range '100..199'" ] },
+    "!range": { "index": 19, "kind": "operator", "displayName": "Not range", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not within the numeric range specified by 'from..to'.", "operatorKind": "binary", "operatorSyntax": "LHS !range 'from..to'", "precedence": 10, "examples": [ "${header.number} !range '100..199'" ] },
+    "startsWith": { "index": 20, "kind": "operator", "displayName": "Starts with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string starts with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS startsWith RHS", "precedence": 10, "examples": [ "${header.name} startsWith 'Camel'" ] },
+    "!startsWith": { "index": 21, "kind": "operator", "displayName": "Not starts with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not start with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !startsWith RHS", "precedence": 10, "examples": [ "${header.name} !startsWith 'Camel'" ] },
+    "endsWith": { "index": 22, "kind": "operator", "displayName": "Ends with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string ends with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS endsWith RHS", "precedence": 10, "examples": [ "${header.name} endsWith '.xml'" ] },
+    "!endsWith": { "index": 23, "kind": "operator", "displayName": "Not ends with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not end with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !endsWith RHS", "precedence": 10, "examples": [ "${header.name} !endsWith '.xml'" ] },
+    "++": { "index": 24, "kind": "operator", "displayName": "Inc", "label": "unary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Increments the numeric value by one. Must immediately follow a function closing brace.", "operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, "examples": [ "${header.count}++" ] },
+    "--": { "index": 25, "kind": "operator", "displayName": "Dec", "label": "unary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Decrements the numeric value by one. Must immediately follow a function closing brace.", "operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, "examples": [ "${header.count}--" ] },
+    "&&": { "index": 26, "kind": "operator", "displayName": "And", "label": "logical", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logical AND. Both left and right predicates must evaluate to true.", "operatorKind": "logical", "operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ "${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
+    "||": { "index": 27, "kind": "operator", "displayName": "Or", "label": "logical", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logical OR. At least one of the left or right predicates must evaluate to true.", "operatorKind": "logical", "operatorSyntax": "predicate || predicate", "precedence": 30, "examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] },
+    "? :": { "index": 28, "kind": "operator", "displayName": "Ternary", "label": "ternary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Ternary conditional operator. Evaluates the predicate and returns trueValue if true, falseValue if false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", "operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, "examples": [ "${header.foo} >  [...]
+    "~>": { "index": 29, "kind": "operator", "displayName": "Chain", "label": "chain", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pipes the result of the left expression as input body to the right expression. Use $param in the right expression to reference the piped value explicitly.", "operatorKind": "chain", "operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> ${uppercase()}", "${substrin [...]
+    "?~>": { "index": 30, "kind": "operator", "displayName": "Chain null safe", "label": "chain", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Null-safe chain operator. Same as ~> but stops chaining and returns null if the left expression evaluates to null.", "operatorKind": "chain", "operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ "${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
+    "?:": { "index": 31, "kind": "operator", "displayName": "Elvis", "label": "other", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Elvis operator (null-coalescing). Returns the left operand if it is not null\/empty, otherwise returns the right operand as a fallback value.", "operatorKind": "other", "operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ "${header.username} ?: 'Guest'", "${body} ?: $ [...]
   }
 }
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.html b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.html
new file mode 100644
index 000000000000..f71946ebc11b
--- /dev/null
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.html
@@ -0,0 +1,241 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE html>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Apache Camel Simple Language Validator</title>
+<style>
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+    background: #f5f5f5; color: #333; padding: 2rem;
+    max-width: 900px; margin: 0 auto;
+  }
+  h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
+  .subtitle { color: #666; font-size: 0.9rem; margin-bottom: 1.5rem; }
+  .input-group { margin-bottom: 1rem; }
+  label { display: block; font-weight: 600; margin-bottom: 0.3rem; font-size: 0.9rem; }
+  input[type="text"] {
+    width: 100%; padding: 0.6rem 0.8rem; font-size: 1rem;
+    font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
+    border: 2px solid #ccc; border-radius: 6px; outline: none;
+    transition: border-color 0.2s;
+  }
+  input[type="text"]:focus { border-color: #e77225; }
+  .controls { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 1.5rem; }
+  .mode-toggle { display: flex; border: 2px solid #ccc; border-radius: 6px; overflow: hidden; }
+  .mode-toggle button {
+    padding: 0.5rem 1rem; border: none; background: #fff;
+    cursor: pointer; font-size: 0.85rem; font-weight: 500; color: #555;
+    transition: all 0.2s;
+  }
+  .mode-toggle button.active { background: #e77225; color: #fff; }
+  .mode-toggle button:not(.active):hover { background: #f0f0f0; }
+  .validate-btn {
+    padding: 0.5rem 1.5rem; border: none; border-radius: 6px;
+    background: #e77225; color: #fff; font-size: 0.9rem; font-weight: 600;
+    cursor: pointer; transition: background 0.2s;
+  }
+  .validate-btn:hover { background: #c5611e; }
+  .result {
+    border-radius: 8px; padding: 1rem 1.25rem;
+    margin-bottom: 1rem; display: none;
+  }
+  .result.valid { background: #e8f5e9; border: 1px solid #a5d6a7; }
+  .result.invalid { background: #fce4ec; border: 1px solid #ef9a9a; }
+  .result.warning { background: #fff8e1; border: 1px solid #ffe082; }
+  .result-header {
+    font-weight: 700; font-size: 1rem; margin-bottom: 0.5rem;
+    display: flex; align-items: center; gap: 0.5rem;
+  }
+  .result.valid .result-header { color: #2e7d32; }
+  .result.invalid .result-header { color: #c62828; }
+  .result.warning .result-header { color: #f57f17; }
+  .diagnostic {
+    padding: 0.5rem 0.75rem; margin-top: 0.5rem;
+    background: rgba(255,255,255,0.7); border-radius: 4px;
+    font-size: 0.85rem;
+  }
+  .diagnostic .severity {
+    font-weight: 700; text-transform: uppercase; font-size: 0.75rem;
+    padding: 0.1rem 0.4rem; border-radius: 3px; margin-right: 0.4rem;
+  }
+  .diagnostic .severity.error { background: #ef9a9a; color: #b71c1c; }
+  .diagnostic .severity.warning { background: #ffe082; color: #e65100; }
+  .diagnostic .severity.info { background: #b3e5fc; color: #01579b; }
+  .diagnostic .position { color: #888; font-size: 0.8rem; margin-left: 0.5rem; }
+  .diagnostic .suggestion { color: #2e7d32; font-weight: 600; }
+  .pointer-line {
+    font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
+    font-size: 0.85rem; margin-top: 0.3rem; color: #888;
+    white-space: pre;
+  }
+  .pointer-line .caret { color: #c62828; font-weight: 700; }
+  .examples {
+    margin-top: 2rem; padding-top: 1.5rem;
+    border-top: 1px solid #ddd;
+  }
+  .examples h2 { font-size: 1.1rem; margin-bottom: 0.75rem; }
+  .example-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
+  .example-btn {
+    padding: 0.35rem 0.75rem; border: 1px solid #ccc; border-radius: 4px;
+    background: #fff; cursor: pointer; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
+    font-size: 0.8rem; color: #555; transition: all 0.2s;
+  }
+  .example-btn:hover { border-color: #e77225; color: #e77225; }
+  .example-btn.error-example { border-color: #ef9a9a; color: #c62828; }
+  .example-btn.error-example:hover { background: #fce4ec; }
+  kbd {
+    background: #eee; border: 1px solid #ccc; border-radius: 3px;
+    padding: 0.1rem 0.4rem; font-size: 0.8rem;
+  }
+  .hint { color: #888; font-size: 0.8rem; margin-top: 0.3rem; }
+</style>
+</head>
+<body>
+
+<h1>Apache Camel Simple Language Validator</h1>
+<p class="subtitle">Validate Simple expressions and predicates in the browser — no Java backend needed.</p>
+
+<div class="input-group">
+  <label for="expr">Simple Expression</label>
+  <input type="text" id="expr" placeholder="${body} == 'Hello World'" autofocus>
+  <p class="hint">Press <kbd>Enter</kbd> to validate, or click the button below.</p>
+</div>
+
+<div class="controls">
+  <div class="mode-toggle">
+    <button id="mode-expr" class="active" onclick="setMode('expression')">Expression</button>
+    <button id="mode-pred" onclick="setMode('predicate')">Predicate</button>
+  </div>
+  <button class="validate-btn" onclick="runValidation()">Validate</button>
+</div>
+
+<div id="result" class="result"></div>
+
+<div class="examples">
+  <h2>Try these examples</h2>
+  <div class="example-list">
+    <button class="example-btn" onclick="tryExample('${body}', 'expression')"><span>${body}</span></button>
+    <button class="example-btn" onclick="tryExample('${header.foo}', 'expression')">${header.foo}</button>
+    <button class="example-btn" onclick="tryExample('Hello ${header.name}', 'expression')">Hello ${header.name}</button>
+    <button class="example-btn" onclick="tryExample('${trim()} ~> ${uppercase()}', 'expression')">${trim()} ~> ${uppercase()}</button>
+    <button class="example-btn" onclick="tryExample(\"${header.name} ?: \\'Guest\\'\", 'expression')">${header.name} ?: 'Guest'</button>
+    <button class="example-btn" onclick="tryExample(\"${body} == \\'foo\\'\", 'predicate')">${body} == 'foo'</button>
+    <button class="example-btn" onclick="tryExample(\"${header.count} > 5 && ${header.type} == \\'gold\\'\", 'predicate')">${header.count} > 5 && ...</button>
+    <button class="example-btn" onclick="tryExample(\"${header.age} range \\'18..65\\'\", 'predicate')">${header.age} range '18..65'</button>
+    <button class="example-btn error-example" onclick="tryExample('${boddy}', 'expression')">${boddy}</button>
+    <button class="example-btn error-example" onclick="tryExample('${body', 'expression')">${body</button>
+    <button class="example-btn error-example" onclick="tryExample(\"${header.foo} contans \\'bar\\'\", 'predicate')">contans (typo)</button>
+    <button class="example-btn error-example" onclick="tryExample(\"${body} ==\", 'predicate')">${body} == (no RHS)</button>
+  </div>
+</div>
+
+<script src="camel-simple-validator.js"></script>
+<script>
+let mode = 'expression';
+
+function setMode(m) {
+  mode = m;
+  document.getElementById('mode-expr').className = m === 'expression' ? 'active' : '';
+  document.getElementById('mode-pred').className = m === 'predicate' ? 'active' : '';
+}
+
+function tryExample(expr, m) {
+  document.getElementById('expr').value = expr;
+  setMode(m);
+  runValidation();
+}
+
+function runValidation() {
+  const input = document.getElementById('expr').value;
+  const result = CamelSimpleValidator.validate(input, mode);
+  const el = document.getElementById('result');
+  el.style.display = 'block';
+
+  const hasErrors = result.diagnostics.some(d => d.severity === 'error');
+  const hasWarnings = result.diagnostics.some(d => d.severity === 'warning');
+
+  if (result.valid && !hasWarnings) {
+    el.className = 'result valid';
+    el.innerHTML = '<div class="result-header">✓ Valid ' + mode + '</div>';
+  } else if (result.valid && hasWarnings) {
+    el.className = 'result warning';
+    let html = '<div class="result-header">⚠ Valid with warnings</div>';
+    html += renderDiagnostics(input, result.diagnostics);
+    el.innerHTML = html;
+  } else {
+    el.className = 'result invalid';
+    let html = '<div class="result-header">✗ Invalid ' + mode + '</div>';
+    html += renderDiagnostics(input, result.diagnostics);
+    el.innerHTML = html;
+  }
+}
+
+function renderDiagnostics(input, diagnostics) {
+  let html = '';
+  for (const d of diagnostics) {
+    html += '<div class="diagnostic">';
+    html += '<span class="severity ' + d.severity + '">' + d.severity + '</span>';
+    html += escapeHtml(d.message);
+    if (d.suggestion) {
+      html += ' <span class="suggestion">→ ' + escapeHtml(d.suggestion) + '</span>';
+    }
+    html += '<span class="position">pos ' + d.start + '-' + d.end + '</span>';
+    // Pointer line
+    html += '<div class="pointer-line">';
+    html += escapeHtml(input) + '\n';
+    let pointer = '';
+    for (let i = 0; i < d.start; i++) pointer += ' ';
+    for (let i = d.start; i < d.end; i++) pointer += '^';
+    html += '<span class="caret">' + pointer + '</span>';
+    html += '</div>';
+    html += '</div>';
+  }
+  return html;
+}
+
+function escapeHtml(s) {
+  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+}
+
+document.getElementById('expr').addEventListener('keydown', function(e) {
+  if (e.key === 'Enter') runValidation();
+});
+</script>
+</body>
+</html>
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
new file mode 100644
index 000000000000..f2669bf1a1dd
--- /dev/null
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
@@ -0,0 +1,809 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ============================================================================
+// Apache Camel Simple Language Validator
+//
+// A self-contained JavaScript validator for the Camel Simple expression
+// language. Works in any browser or Node.js — no Java backend needed.
+//
+// The catalog data (FUNCTIONS, OPERATORS) is AUTO-GENERATED from simple.json.
+// The Maven build regenerates the catalog data section on each build.
+// ============================================================================
+
+// === AUTO-GENERATED CATALOG DATA — do not edit by hand ===
+
+const FUNCTIONS = {
+  'abs': true,
+  'assert': true,
+  'attachment': true,
+  'attachments': true,
+  'attachmentsContent': true,
+  'attachmentsContentAs': true,
+  'attachmentsContentAsText': true,
+  'attachmentsHeader': true,
+  'attachmentsHeaderAs': true,
+  'attachmentsKeys': true,
+  'attachmentsSize': true,
+  'average': true,
+  'base64Decode': true,
+  'base64Encode': true,
+  'bean': true,
+  'body': true,
+  'bodyAs': true,
+  'bodyOneLine': true,
+  'bodyType': true,
+  'camelContext': true,
+  'camelId': true,
+  'capitalize': true,
+  'ceil': true,
+  'clearAttachments': true,
+  'collate': true,
+  'concat': true,
+  'convertTo': true,
+  'contains': true,
+  'date': true,
+  'date-with-timezone': true,
+  'distinct': true,
+  'empty': true,
+  'env': true,
+  'exchange': true,
+  'exchangeId': true,
+  'exchangeProperty': true,
+  'exception': true,
+  'filter': true,
+  'forEach': true,
+  'floor': true,
+  'fromRouteId': true,
+  'function': true,
+  'hash': true,
+  'header': true,
+  'headerAs': true,
+  'headers': true,
+  'hostName': true,
+  'htmlClean': true,
+  'htmlDecode': true,
+  'htmlParse': true,
+  'id': true,
+  'iif': true,
+  'isAlpha': true,
+  'isAlphaNumeric': true,
+  'isEmpty': true,
+  'isNumeric': true,
+  'jq': true,
+  'jsonpath': true,
+  'join': true,
+  'length': true,
+  'list': true,
+  'listAdd': true,
+  'listRemove': true,
+  'load': true,
+  'lowercase': true,
+  'mandatoryBodyAs': true,
+  'map': true,
+  'mapAdd': true,
+  'mapRemove': true,
+  'max': true,
+  'messageAs': true,
+  'messageHistory': true,
+  'messageTimestamp': true,
+  'min': true,
+  'newEmpty': true,
+  'normalizeWhitespace': true,
+  'not': true,
+  'null': true,
+  'originalBody': true,
+  'pad': true,
+  'pretty': true,
+  'prettyBody': true,
+  'sort': true,
+  'toPrettyJson': true,
+  'toPrettyJsonBody': true,
+  'toJson': true,
+  'toJsonBody': true,
+  'properties': true,
+  'propertiesExist': true,
+  'quote': true,
+  'random': true,
+  'range': true,
+  'ref': true,
+  'replace': true,
+  'reverse': true,
+  'routeGroup': true,
+  'routeId': true,
+  'safeQuote': true,
+  'setVariable': true,
+  'setHeader': true,
+  'shuffle': true,
+  'simpleJsonpath': true,
+  'size': true,
+  'skip': true,
+  'split': true,
+  'stepId': true,
+  'substring': true,
+  'substringAfter': true,
+  'substringBefore': true,
+  'substringBetween': true,
+  'sum': true,
+  'sys': true,
+  'threadId': true,
+  'threadName': true,
+  'throwException': true,
+  'trim': true,
+  'type': true,
+  'kindOfType': true,
+  'unquote': true,
+  'uppercase': true,
+  'uuid': true,
+  'val': true,
+  'variable': true,
+  'variableAs': true,
+  'variables': true,
+  'xpath': true
+};
+
+const OPERATORS = {
+  '==': { kind: 'binary', description: 'Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.' },
+  '=~': { kind: 'binary', description: 'Tests equality between left and right operand values, ignoring case for string comparison.' },
+  '>': { kind: 'binary', description: 'Tests whether the left operand is greater than the right operand.' },
+  '>=': { kind: 'binary', description: 'Tests whether the left operand is greater than or equal to the right operand.' },
+  '<': { kind: 'binary', description: 'Tests whether the left operand is less than the right operand.' },
+  '<=': { kind: 'binary', description: 'Tests whether the left operand is less than or equal to the right operand.' },
+  '!=': { kind: 'binary', description: 'Tests inequality between left and right operand values.' },
+  '!=~': { kind: 'binary', description: 'Tests inequality between left and right operand values, ignoring case for string comparison.' },
+  'contains': { kind: 'binary', description: 'Tests whether the left operand string contains the right operand string.' },
+  '!contains': { kind: 'binary', description: 'Tests whether the left operand string does not contain the right operand string.' },
+  '~~': { kind: 'binary', description: 'Tests whether the left operand string contains the right operand string, ignoring case.' },
+  '!~~': { kind: 'binary', description: 'Tests whether the left operand string does not contain the right operand string, ignoring case.' },
+  'regex': { kind: 'binary', description: 'Tests whether the left operand matches the right operand as a regular expression.' },
+  '!regex': { kind: 'binary', description: 'Tests whether the left operand does not match the right operand as a regular expression.' },
+  'in': { kind: 'binary', description: 'Tests whether the left operand is in a set of comma-separated values.' },
+  '!in': { kind: 'binary', description: 'Tests whether the left operand is not in a set of comma-separated values.' },
+  'is': { kind: 'binary', description: 'Tests whether the left operand is an instance of the right operand type (Java classname or short name).' },
+  '!is': { kind: 'binary', description: 'Tests whether the left operand is not an instance of the right operand type.' },
+  'range': { kind: 'binary', description: 'Tests whether the left operand is within the numeric range specified by \'from..to\'.' },
+  '!range': { kind: 'binary', description: 'Tests whether the left operand is not within the numeric range specified by \'from..to\'.' },
+  'startsWith': { kind: 'binary', description: 'Tests whether the left operand string starts with the right operand string.' },
+  '!startsWith': { kind: 'binary', description: 'Tests whether the left operand string does not start with the right operand string.' },
+  'endsWith': { kind: 'binary', description: 'Tests whether the left operand string ends with the right operand string.' },
+  '!endsWith': { kind: 'binary', description: 'Tests whether the left operand string does not end with the right operand string.' },
+  '++': { kind: 'unary', description: 'Increments the numeric value by one. Must immediately follow a function closing brace.' },
+  '--': { kind: 'unary', description: 'Decrements the numeric value by one. Must immediately follow a function closing brace.' },
+  '&&': { kind: 'logical', description: 'Logical AND. Both left and right predicates must evaluate to true.' },
+  '||': { kind: 'logical', description: 'Logical OR. At least one of the left or right predicates must evaluate to true.' },
+  '? :': { kind: 'ternary', description: 'Ternary conditional operator. Evaluates the predicate and returns trueValue if true, falseValue if false. Requires spaces around both ? and : tokens.' },
+  '~>': { kind: 'chain', description: 'Pipes the result of the left expression as input body to the right expression. Use $param in the right expression to reference the piped value explicitly.' },
+  '?~>': { kind: 'chain', description: 'Null-safe chain operator. Same as ~> but stops chaining and returns null if the left expression evaluates to null.' },
+  '?:': { kind: 'other', description: 'Elvis operator (null-coalescing). Returns the left operand if it is not null/empty, otherwise returns the right operand as a fallback value.' }
+};
+
+
+
+// ========================================================================
+// Tokenizer
+// ========================================================================
+
+function tokenize(input) {
+  const tokens = [];
+  let i = 0;
+  const len = input.length;
+
+  while (i < len) {
+    // Function start: ${ or $simple{
+    if (input[i] === '$' && i + 1 < len && input[i + 1] === '{') {
+      tokens.push({ type: 'functionStart', value: '${', start: i, end: i + 2 });
+      i += 2;
+      continue;
+    }
+    if (input.startsWith('$simple{', i)) {
+      tokens.push({ type: 'functionStart', value: '$simple{', start: i, end: i + 8 });
+      i += 8;
+      continue;
+    }
+
+    // Function end
+    if (input[i] === '}') {
+      tokens.push({ type: 'functionEnd', value: '}', start: i, end: i + 1 });
+      i += 1;
+      continue;
+    }
+
+    // Escape sequences
+    if (input[i] === '\\' && i + 1 < len) {
+      const next = input[i + 1];
+      tokens.push({ type: 'escape', value: '\\' + next, start: i, end: i + 2 });
+      i += 2;
+      continue;
+    }
+
+    // Single quote
+    if (input[i] === "'") {
+      tokens.push({ type: 'singleQuote', value: "'", start: i, end: i + 1 });
+      i += 1;
+      continue;
+    }
+
+    // Double quote
+    if (input[i] === '"') {
+      tokens.push({ type: 'doubleQuote', value: '"', start: i, end: i + 1 });
+      i += 1;
+      continue;
+    }
+
+    // Whitespace
+    if (input[i] === ' ' || input[i] === '\t' || input[i] === '\n' || input[i] === '\r') {
+      let start = i;
+      while (i < len && (input[i] === ' ' || input[i] === '\t' || input[i] === '\n' || input[i] === '\r')) {
+        i++;
+      }
+      tokens.push({ type: 'whitespace', value: input.substring(start, i), start, end: i });
+      continue;
+    }
+
+    // Text (everything else — accumulated into runs)
+    let start = i;
+    while (i < len && input[i] !== '$' && input[i] !== '}' && input[i] !== '\\' &&
+           input[i] !== "'" && input[i] !== '"' &&
+           input[i] !== ' ' && input[i] !== '\t' && input[i] !== '\n' && input[i] !== '\r') {
+      i++;
+    }
+    if (i > start) {
+      tokens.push({ type: 'text', value: input.substring(start, i), start, end: i });
+    }
+  }
+
+  return tokens;
+}
+
+// ========================================================================
+// Validator
+// ========================================================================
+
+function findClosestFunction(name) {
+  const names = Object.keys(FUNCTIONS);
+  let best = null;
+  let bestDist = Infinity;
+  for (const fn of names) {
+    const d = levenshtein(name.toLowerCase(), fn.toLowerCase());
+    if (d < bestDist && d <= 3) {
+      bestDist = d;
+      best = fn;
+    }
+  }
+  return best;
+}
+
+function levenshtein(a, b) {
+  const m = a.length, n = b.length;
+  if (m === 0) return n;
+  if (n === 0) return m;
+  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
+  for (let i = 0; i <= m; i++) dp[i][0] = i;
+  for (let j = 0; j <= n; j++) dp[0][j] = j;
+  for (let i = 1; i <= m; i++) {
+    for (let j = 1; j <= n; j++) {
+      dp[i][j] = a[i-1] === b[j-1]
+        ? dp[i-1][j-1]
+        : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
+    }
+  }
+  return dp[m][n];
+}
+
+function extractBaseName(content) {
+  let base = content;
+  for (const ch of ['(', '.', ':']) {
+    const pos = base.indexOf(ch);
+    if (pos !== -1) {
+      base = base.substring(0, pos);
+    }
+  }
+  return base;
+}
+
+function isKnownOperator(word) {
+  return word in OPERATORS;
+}
+
+function findBinaryOperator(text) {
+  const binaryOps = Object.entries(OPERATORS)
+    .filter(([, op]) => op.kind === 'binary')
+    .map(([sym]) => sym)
+    .sort((a, b) => b.length - a.length);
+  for (const op of binaryOps) {
+    if (text === op) return op;
+  }
+  return null;
+}
+
+function validate(input, mode) {
+  if (mode === undefined) mode = 'expression';
+  const diagnostics = [];
+  const tokens = tokenize(input);
+
+  // Track delimiter balancing
+  let functionDepth = 0;
+  const functionStarts = [];
+  let inSingleQuote = false;
+  let singleQuoteStart = -1;
+  let inDoubleQuote = false;
+  let doubleQuoteStart = -1;
+
+  // Collect function contents for validation
+  const functionBlocks = [];
+  let currentFunctionStart = -1;
+  let currentFunctionTokenStart = -1;
+
+  for (let t = 0; t < tokens.length; t++) {
+    const tok = tokens[t];
+
+    if (tok.type === 'singleQuote' && !inDoubleQuote) {
+      if (inSingleQuote) {
+        inSingleQuote = false;
+      } else {
+        inSingleQuote = true;
+        singleQuoteStart = tok.start;
+      }
+      continue;
+    }
+
+    if (tok.type === 'doubleQuote' && !inSingleQuote) {
+      if (inDoubleQuote) {
+        inDoubleQuote = false;
+      } else {
+        inDoubleQuote = true;
+        doubleQuoteStart = tok.start;
+      }
+      continue;
+    }
+
+    // Inside quotes, only check for unmatched function braces
+    if (inSingleQuote || inDoubleQuote) {
+      if (tok.type === 'functionStart') {
+        functionDepth++;
+        functionStarts.push(tok.start);
+        currentFunctionStart = tok.start;
+        currentFunctionTokenStart = t;
+      } else if (tok.type === 'functionEnd') {
+        if (functionDepth > 0) {
+          functionDepth--;
+          const fnStart = functionStarts.pop();
+          const fnContent = input.substring(fnStart + 2, tok.start);
+          functionBlocks.push({ content: fnContent, start: fnStart, end: tok.end });
+        }
+      }
+      continue;
+    }
+
+    if (tok.type === 'functionStart') {
+      functionDepth++;
+      functionStarts.push(tok.start);
+      currentFunctionStart = tok.start;
+      currentFunctionTokenStart = t;
+    } else if (tok.type === 'functionEnd') {
+      if (functionDepth > 0) {
+        functionDepth--;
+        const fnStart = functionStarts.pop();
+        const fnContent = input.substring(fnStart + (input.startsWith('$simple{', fnStart) ? 8 : 2), tok.start);
+        functionBlocks.push({ content: fnContent, start: fnStart, end: tok.end });
+      } else {
+        diagnostics.push({
+          severity: 'error',
+          message: "Unexpected '}' without matching '${' ",
+          start: tok.start,
+          end: tok.end
+        });
+      }
+    }
+
+    // Validate escape sequences
+    if (tok.type === 'escape') {
+      const escaped = tok.value[1];
+      if (!['n', 't', 'r', '}', '\\', "'", '"'].includes(escaped)) {
+        diagnostics.push({
+          severity: 'warning',
+          message: `Unusual escape sequence '\\${escaped}'`,
+          start: tok.start,
+          end: tok.end
+        });
+      }
+    }
+  }
+
+  // Check for unclosed delimiters
+  if (functionDepth > 0) {
+    const unclosedStart = functionStarts[functionStarts.length - 1];
+    diagnostics.push({
+      severity: 'error',
+      message: "'${' has no closing '}'",
+      start: unclosedStart,
+      end: unclosedStart + 2
+    });
+  }
+
+  if (inSingleQuote) {
+    diagnostics.push({
+      severity: 'error',
+      message: "Single quote has no closing quote",
+      start: singleQuoteStart,
+      end: singleQuoteStart + 1
+    });
+  }
+
+  if (inDoubleQuote) {
+    diagnostics.push({
+      severity: 'error',
+      message: "Double quote has no closing quote",
+      start: doubleQuoteStart,
+      end: doubleQuoteStart + 1
+    });
+  }
+
+  // Validate function names
+  for (const block of functionBlocks) {
+    validateFunction(block.content, block.start, block.end, diagnostics);
+  }
+
+  // Validate predicate structure
+  if (mode === 'predicate') {
+    validatePredicate(input, tokens, diagnostics);
+  }
+
+  return {
+    valid: diagnostics.filter(d => d.severity === 'error').length === 0,
+    diagnostics
+  };
+}
+
+function validateFunction(content, blockStart, blockEnd, diagnostics) {
+  // Handle nested functions — only validate the outermost name
+  const baseName = extractBaseName(content);
+
+  if (!baseName || baseName.length === 0) {
+    diagnostics.push({
+      severity: 'error',
+      message: "Empty function reference '${}' ",
+      start: blockStart,
+      end: blockEnd
+    });
+    return;
+  }
+
+  // Skip validation for contents that start with ${ (nested function as argument)
+  if (baseName.startsWith('$')) {
+    return;
+  }
+
+  // Check if function name is known
+  if (!(baseName in FUNCTIONS)) {
+    const suggestion = findClosestFunction(baseName);
+    const msg = suggestion
+      ? `Unknown function '${baseName}', did you mean '${suggestion}'?`
+      : `Unknown function '${baseName}'`;
+    diagnostics.push({
+      severity: 'error',
+      message: msg,
+      start: blockStart,
+      end: blockEnd,
+      suggestion
+    });
+  }
+}
+
+function validatePredicate(input, tokens, diagnostics) {
+  // Build a simplified structure: segments separated by operators
+  // We look for operator tokens in the non-quoted, non-function text
+  const segments = [];
+  let current = '';
+  let currentStart = 0;
+  let depth = 0;
+  let inQuote = false;
+  let quoteChar = null;
+
+  for (let i = 0; i < input.length; i++) {
+    const ch = input[i];
+
+    // Track quote state
+    if ((ch === "'" || ch === '"') && depth === 0) {
+      if (!inQuote) {
+        inQuote = true;
+        quoteChar = ch;
+      } else if (ch === quoteChar) {
+        inQuote = false;
+        quoteChar = null;
+      }
+      current += ch;
+      continue;
+    }
+
+    if (inQuote) {
+      current += ch;
+      continue;
+    }
+
+    // Track function depth
+    if (ch === '$' && i + 1 < input.length && input[i + 1] === '{') {
+      depth++;
+      current += '${';
+      i++;
+      continue;
+    }
+    if (ch === '}') {
+      if (depth > 0) depth--;
+      current += '}';
+      continue;
+    }
+
+    // Outside functions and quotes — check for operators
+    if (depth === 0) {
+      let rest = input.substring(i).trimStart();
+      let skipWs = input.substring(i).length - rest.length;
+      let opMatch = null;
+
+      // Check all binary/logical operators (longest match first)
+      const allOps = Object.entries(OPERATORS)
+        .filter(([, op]) => op.kind === 'binary' || op.kind === 'logical')
+        .map(([sym]) => sym)
+        .sort((a, b) => b.length - a.length);
+
+      for (const op of allOps) {
+        if (rest.startsWith(op) && (rest.length === op.length || rest[op.length] === ' ')) {
+          opMatch = op;
+          break;
+        }
+      }
+
+      if (opMatch && (ch === ' ' || current.trim().length === 0)) {
+        // Found an operator — save the left segment
+        if (current.trim().length > 0) {
+          segments.push({ type: 'operand', value: current.trim(), start: currentStart });
+        }
+        const opStart = i + skipWs;
+        segments.push({ type: 'operator', value: opMatch, start: opStart, kind: OPERATORS[opMatch].kind });
+        i = opStart + opMatch.length;
+        current = '';
+        currentStart = i;
+        continue;
+      }
+    }
+
+    current += ch;
+  }
+
+  if (current.trim().length > 0) {
+    segments.push({ type: 'operand', value: current.trim(), start: currentStart });
+  }
+
+  // Check for misspelled word operators between operands
+  const wordOperators = Object.keys(OPERATORS).filter(op => /^[a-zA-Z!]/.test(op));
+  for (let s = 0; s < segments.length; s++) {
+    const seg = segments[s];
+    if (seg.type === 'operand') {
+      const words = seg.value.split(/\s+/);
+      for (const word of words) {
+        if (word.length >= 2 && /^!?[a-zA-Z]+$/.test(word) && !findBinaryOperator(word)) {
+          for (const op of wordOperators) {
+            if (levenshtein(word.toLowerCase(), op.toLowerCase()) <= 2 && word.toLowerCase() !== op.toLowerCase()) {
+              diagnostics.push({
+                severity: 'warning',
+                message: `'${word}' looks like a misspelled operator, did you mean '${op}'?`,
+                start: seg.start + seg.value.indexOf(word),
+                end: seg.start + seg.value.indexOf(word) + word.length,
+                suggestion: op
+              });
+              break;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // Validate operator usage
+  for (let s = 0; s < segments.length; s++) {
+    const seg = segments[s];
+    if (seg.type === 'operator') {
+      const opInfo = OPERATORS[seg.value];
+      if (opInfo && (opInfo.kind === 'binary' || opInfo.kind === 'logical')) {
+        // Check for LHS
+        const prev = s > 0 ? segments[s - 1] : null;
+        if (!prev || prev.type !== 'operand') {
+          diagnostics.push({
+            severity: 'error',
+            message: `Operator '${seg.value}' has no left-hand side operand`,
+            start: seg.start,
+            end: seg.start + seg.value.length
+          });
+        }
+        // Check for RHS
+        const next = s + 1 < segments.length ? segments[s + 1] : null;
+        if (!next || next.type !== 'operand') {
+          diagnostics.push({
+            severity: 'error',
+            message: `Operator '${seg.value}' has no right-hand side operand`,
+            start: seg.start,
+            end: seg.start + seg.value.length
+          });
+        }
+      }
+    }
+  }
+}
+
+// ========================================================================
+// Autocomplete
+// ========================================================================
+
+function complete(input, cursor) {
+  const suggestions = [];
+
+  // Find context at cursor position
+  const before = input.substring(0, cursor);
+  const lastDollarBrace = before.lastIndexOf('${');
+  const lastCloseBrace = before.lastIndexOf('}');
+
+  if (lastDollarBrace > lastCloseBrace) {
+    // We are inside a ${...} — suggest function names
+    const partial = before.substring(lastDollarBrace + 2);
+    const basePart = extractBaseName(partial).toLowerCase();
+
+    for (const [name, fns] of Object.entries(FUNCTIONS)) {
+      if (name.toLowerCase().startsWith(basePart)) {
+        for (const fn of fns) {
+          suggestions.push({
+            label: fn.name,
+            displayName: fn.displayName,
+            description: fn.description,
+            group: fn.group,
+            insertText: fn.name
+          });
+        }
+      }
+    }
+  } else if (before.trimEnd().endsWith('}') || /\S$/.test(before)) {
+    // After a function or operand — suggest operators
+    for (const [sym, op] of Object.entries(OPERATORS)) {
+      if (op.kind === 'binary' || op.kind === 'logical') {
+        suggestions.push({
+          label: sym,
+          displayName: op.displayName,
+          description: op.description,
+          insertText: ' ' + sym + ' '
+        });
+      }
+    }
+  }
+
+  return suggestions;
+}
+
+// ========================================================================
+// Public API
+// ========================================================================
+
+function getFunctions() {
+  return FUNCTIONS;
+}
+
+function getOperators() {
+  return OPERATORS;
+}
+
+// ========================================================================
+// Exports (works in both Node.js and browser)
+// ========================================================================
+
+const CamelSimpleValidator = { validate, complete, getFunctions, getOperators, tokenize };
+
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = CamelSimpleValidator;
+}
+if (typeof globalThis !== 'undefined') {
+  globalThis.CamelSimpleValidator = CamelSimpleValidator;
+}
+
+// ========================================================================
+// Self-test (run with: node camel-simple-validator.js)
+// ========================================================================
+
+if (typeof require !== 'undefined' && require.main === module) {
+  let passed = 0;
+  let failed = 0;
+
+  function test(name, input, mode, expectValid, expectErrorSubstring) {
+    const result = validate(input, mode);
+    let ok = result.valid === expectValid;
+    if (expectErrorSubstring) {
+      const hasMatch = result.diagnostics.some(d => d.message.includes(expectErrorSubstring));
+      ok = ok && hasMatch;
+    }
+    if (ok) {
+      passed++;
+    } else {
+      failed++;
+      console.log(`FAIL: ${name}`);
+      console.log(`  input: ${JSON.stringify(input)}`);
+      console.log(`  expected valid=${expectValid}` + (expectErrorSubstring ? `, error containing "${expectErrorSubstring}"` : ''));
+      console.log(`  got valid=${result.valid}, diagnostics:`, JSON.stringify(result.diagnostics, null, 2));
+    }
+  }
+
+  // === Valid expressions ===
+  test('simple body', '${body}', 'expression', true);
+  test('header access', '${header.foo}', 'expression', true);
+  test('template text', 'Hello ${body}', 'expression', true);
+  test('multiple functions', '${header.from} to ${header.to}', 'expression', true);
+  test('nested function in text', "Hello ${header.name}, you have ${header.count} items", 'expression', true);
+  test('exchangeId', '${exchangeId}', 'expression', true);
+  test('function with parens', '${trim()}', 'expression', true);
+  test('function with args', '${substring(1,3)}', 'expression', true);
+  test('plain text', 'Hello World', 'expression', true);
+  test('empty string', '', 'expression', true);
+  test('env variable', '${env.HOME}', 'expression', true);
+  test('sys property', '${sys.user.name}', 'expression', true);
+  test('properties', '${properties:myKey}', 'expression', true);
+  test('date function', '${date(now)}', 'expression', true);
+  test('camelId', '${camelId}', 'expression', true);
+  test('routeId', '${routeId}', 'expression', true);
+  test('exchangeProperty', '${exchangeProperty.myProp}', 'expression', true);
+  test('variable', '${variable.myVar}', 'expression', true);
+  test('bodyAs', '${bodyAs(String)}', 'expression', true);
+  test('uppercase', '${uppercase(${body})}', 'expression', true);
+  test('escape newline', 'Hello\\nWorld', 'expression', true);
+  test('escape tab', 'col1\\tcol2', 'expression', true);
+  test('$simple prefix', '$simple{body}', 'expression', true);
+
+  // === Valid predicates ===
+  test('simple equality', "${body} == 'foo'", 'predicate', true);
+  test('numeric comparison', '${header.count} > 5', 'predicate', true);
+  test('contains', "${header.title} contains 'Camel'", 'predicate', true);
+  test('logical AND', "${body} == 'foo' && ${header.bar} == 'baz'", 'predicate', true);
+  test('logical OR', "${body} == 'foo' || ${body} == 'bar'", 'predicate', true);
+  test('not equal', "${header.type} != 'test'", 'predicate', true);
+  test('regex operator', "${header.code} regex '\\d{3}'", 'predicate', true);
+  test('in operator', "${header.color} in 'red,blue,green'", 'predicate', true);
+  test('range operator', "${header.age} range '18..65'", 'predicate', true);
+  test('is operator', "${body} is 'String'", 'predicate', true);
+  test('startsWith', "${header.name} startsWith 'Camel'", 'predicate', true);
+  test('endsWith', "${header.file} endsWith '.xml'", 'predicate', true);
+  test('case-insensitive contains', "${header.title} ~~ 'camel'", 'predicate', true);
+  test('greater or equal', '${header.count} >= 10', 'predicate', true);
+  test('less or equal', '${header.count} <= 100', 'predicate', true);
+
+  // === Invalid expressions ===
+  test('unclosed function', '${body', 'expression', false, "no closing '}'");
+  test('extra closing brace', 'body}', 'expression', false, "Unexpected '}'");
+  test('unknown function', '${boddy}', 'expression', false, "Unknown function 'boddy'");
+  test('unknown function with suggestion', '${headr.foo}', 'expression', false, "did you mean 'header'");
+  test('empty function', '${}', 'expression', false, "Empty function");
+  test('unclosed single quote', "Hello '${body}", 'expression', false, "Single quote has no closing");
+  test('unclosed double quote', 'Hello "${body}', 'expression', false, "Double quote has no closing");
+  test('unknown function trim typo', '${trm()}', 'expression', false, "did you mean 'trim'");
+
+  // === Invalid predicates ===
+  test('operator no RHS', "${body} ==", 'predicate', false, "no right-hand side");
+  test('operator no LHS', "== 'foo'", 'predicate', false, "no left-hand side");
+
+  // === Predicate warnings ===
+  test('misspelled operator', "${header.foo} contans 'bar'", 'predicate', true, "did you mean 'contains'");
+
+  // === Summary ===
+  console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed`);
+  if (failed > 0) {
+    process.exit(1);
+  }
+}
diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index 2e15696f0f3d..e424c0478ed8 100644
--- a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.camel.tooling.model.ArtifactModel;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.JsonMapper;
 import org.apache.camel.tooling.model.Kind;
 import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.tooling.model.PojoBeanModel;
@@ -1819,4 +1820,65 @@ public class CamelCatalogTest {
         assertTrue(json.contains("99.0.0-test"), "summaryAsJson should contain the custom version");
     }
 
+    @Test
+    public void testSimpleValidatorJsExists() throws Exception {
+        try (InputStream is = getClass().getClassLoader()
+                .getResourceAsStream("org/apache/camel/catalog/simple/camel-simple-validator.js")) {
+            assertNotNull(is, "camel-simple-validator.js should exist in catalog");
+            String js = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+
+            // Verify catalog data sections are present
+            assertTrue(js.contains("const FUNCTIONS = {"), "JS should contain FUNCTIONS object");
+            assertTrue(js.contains("const OPERATORS = {"), "JS should contain OPERATORS object");
+
+            // Verify key functions are present (from simple.json)
+            assertTrue(js.contains("'body': true"), "JS should contain body function");
+            assertTrue(js.contains("'header': true"), "JS should contain header function");
+            assertTrue(js.contains("'exchangeProperty': true"), "JS should contain exchangeProperty function");
+
+            // Verify operators are present
+            assertTrue(js.contains("'==':"), "JS should contain == operator");
+            assertTrue(js.contains("'contains':"), "JS should contain contains operator");
+            assertTrue(js.contains("'regex':"), "JS should contain regex operator");
+
+            // Verify API functions are present
+            assertTrue(js.contains("function validate("), "JS should contain validate function");
+            assertTrue(js.contains("function complete("), "JS should contain complete function");
+
+            // Verify the function count matches simple.json
+            String langJson = catalog.languageJSonSchema("simple");
+            assertNotNull(langJson, "simple language JSON should exist");
+            LanguageModel model = JsonMapper.generateLanguageModel(langJson);
+            long expectedFunctions = model.getFunctions().stream()
+                    .map(f -> {
+                        String name = f.getName();
+                        for (char ch : new char[] { '(', '.', ':' }) {
+                            int pos = name.indexOf(ch);
+                            if (pos != -1) {
+                                name = name.substring(0, pos);
+                            }
+                        }
+                        return name;
+                    })
+                    .distinct()
+                    .count();
+            long actualFunctions = js.lines()
+                    .filter(line -> line.matches("\\s+'[^']+': true.*"))
+                    .count();
+            assertEquals(expectedFunctions, actualFunctions,
+                    "JS FUNCTIONS count should match unique base names from simple.json");
+        }
+    }
+
+    @Test
+    public void testSimpleValidatorHtmlExists() throws Exception {
+        try (InputStream is = getClass().getClassLoader()
+                .getResourceAsStream("org/apache/camel/catalog/simple/camel-simple-validator.html")) {
+            assertNotNull(is, "camel-simple-validator.html should exist in catalog");
+            String html = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+            assertTrue(html.contains("Simple Language Validator"), "HTML should contain title");
+            assertTrue(html.contains("camel-simple-validator.js"), "HTML should reference the JS file");
+        }
+    }
+
 }
diff --git a/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java b/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
index 5dc93f9f4b60..a45a9f2cef2d 100644
--- a/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
+++ b/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
@@ -43,4 +43,17 @@ public @interface Language {
      */
     Class<?> functionsClass() default void.class;
 
+    /**
+     * The class that contains all the name of operators that are supported by the language. The name of the operators
+     * are defined as {@code String} constants in the operators class.
+     *
+     * The class to provide can be any class but by convention, we would expect a class whose name is of type
+     * <i>xxxOperatorConstants</i> where <i>xxx</i> is the name of the corresponding language like for example
+     * <i>SimpleOperatorConstants</i> for the language <i>camel-simple</i>.
+     *
+     * The metadata of a given operator are retrieved directly from the annotation {@code @Metadata} added to the
+     * {@code String} constant representing its name and defined in the operators class.
+     */
+    Class<?> operatorsClass() default void.class;
+
 }
diff --git a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index 1c6720fc16c3..d7224071f387 100644
--- a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -154,5 +154,39 @@
     "variableAs(key,type)": { "index": 126, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type",  [...]
     "variables": { "index": 127, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
     "xpath(input,exp)": { "index": 128, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
+  },
+  "operators": {
+    "==": { "index": 0, "kind": "operator", "displayName": "Eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.", "operatorKind": "binary", "operatorSyntax": "LHS == RHS", "precedence": 10, "examples": [ "${header.foo} == 'bar'", "${header.count} == 5" ] },
+    "=~": { "index": 1, "kind": "operator", "displayName": "Eq ignore", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values, ignoring case for string comparison.", "operatorKind": "binary", "operatorSyntax": "LHS =~ RHS", "precedence": 10, "examples": [ "${header.foo} =~ 'BAR'" ] },
+    ">": { "index": 2, "kind": "operator", "displayName": "Gt", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is greater than the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS > RHS", "precedence": 10, "examples": [ "${header.count} > 100" ] },
+    ">=": { "index": 3, "kind": "operator", "displayName": "Gte", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is greater than or equal to the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS >= RHS", "precedence": 10, "examples": [ "${header.count} >= 100" ] },
+    "<": { "index": 4, "kind": "operator", "displayName": "Lt", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is less than the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS < RHS", "precedence": 10, "examples": [ "${header.count} < 100" ] },
+    "<=": { "index": 5, "kind": "operator", "displayName": "Lte", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is less than or equal to the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS <= RHS", "precedence": 10, "examples": [ "${header.count} <= 100" ] },
+    "!=": { "index": 6, "kind": "operator", "displayName": "Not eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests inequality between left and right operand values.", "operatorKind": "binary", "operatorSyntax": "LHS != RHS", "precedence": 10, "examples": [ "${header.foo} != 'bar'" ] },
+    "!=~": { "index": 7, "kind": "operator", "displayName": "Not eq ignore", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests inequality between left and right operand values, ignoring case for string comparison.", "operatorKind": "binary", "operatorSyntax": "LHS !=~ RHS", "precedence": 10, "examples": [ "${header.foo} !=~ 'BAR'" ] },
+    "contains": { "index": 8, "kind": "operator", "displayName": "Contains", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string contains the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS contains RHS", "precedence": 10, "examples": [ "${header.title} contains 'Camel'" ] },
+    "!contains": { "index": 9, "kind": "operator", "displayName": "Not contains", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not contain the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !contains RHS", "precedence": 10, "examples": [ "${header.title} !contains 'Camel'" ] },
+    "~~": { "index": 10, "kind": "operator", "displayName": "Contains ignorecase", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string contains the right operand string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS ~~ RHS", "precedence": 10, "examples": [ "${header.title} ~~ 'camel'" ] },
+    "!~~": { "index": 11, "kind": "operator", "displayName": "Not contains ignorecase", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not contain the right operand string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS !~~ RHS", "precedence": 10, "examples": [ "${header.title} !~~ 'camel'" ] },
+    "regex": { "index": 12, "kind": "operator", "displayName": "Regex", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand matches the right operand as a regular expression.", "operatorKind": "binary", "operatorSyntax": "LHS regex 'pattern'", "precedence": 10, "examples": [ "${header.number} regex '\\d{4}'" ] },
+    "!regex": { "index": 13, "kind": "operator", "displayName": "Not regex", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand does not match the right operand as a regular expression.", "operatorKind": "binary", "operatorSyntax": "LHS !regex 'pattern'", "precedence": 10, "examples": [ "${header.number} !regex '\\d{4}'" ] },
+    "in": { "index": 14, "kind": "operator", "displayName": "In", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is in a set of comma-separated values.", "operatorKind": "binary", "operatorSyntax": "LHS in 'val1,val2,...'", "precedence": 10, "examples": [ "${header.type} in 'gold,silver'" ] },
+    "!in": { "index": 15, "kind": "operator", "displayName": "Not in", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not in a set of comma-separated values.", "operatorKind": "binary", "operatorSyntax": "LHS !in 'val1,val2,...'", "precedence": 10, "examples": [ "${header.type} !in 'gold,silver'" ] },
+    "is": { "index": 16, "kind": "operator", "displayName": "Is", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is an instance of the right operand type (Java classname or short name).", "operatorKind": "binary", "operatorSyntax": "LHS is 'typeName'", "precedence": 10, "examples": [ "${header.type} is 'String'", "${body} is 'java.util.List'" ] },
+    "!is": { "index": 17, "kind": "operator", "displayName": "Not is", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not an instance of the right operand type.", "operatorKind": "binary", "operatorSyntax": "LHS !is 'typeName'", "precedence": 10, "examples": [ "${header.type} !is 'String'" ] },
+    "range": { "index": 18, "kind": "operator", "displayName": "Range", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is within the numeric range specified by 'from..to'.", "operatorKind": "binary", "operatorSyntax": "LHS range 'from..to'", "precedence": 10, "examples": [ "${header.number} range '100..199'" ] },
+    "!range": { "index": 19, "kind": "operator", "displayName": "Not range", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not within the numeric range specified by 'from..to'.", "operatorKind": "binary", "operatorSyntax": "LHS !range 'from..to'", "precedence": 10, "examples": [ "${header.number} !range '100..199'" ] },
+    "startsWith": { "index": 20, "kind": "operator", "displayName": "Starts with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string starts with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS startsWith RHS", "precedence": 10, "examples": [ "${header.name} startsWith 'Camel'" ] },
+    "!startsWith": { "index": 21, "kind": "operator", "displayName": "Not starts with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not start with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !startsWith RHS", "precedence": 10, "examples": [ "${header.name} !startsWith 'Camel'" ] },
+    "endsWith": { "index": 22, "kind": "operator", "displayName": "Ends with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string ends with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS endsWith RHS", "precedence": 10, "examples": [ "${header.name} endsWith '.xml'" ] },
+    "!endsWith": { "index": 23, "kind": "operator", "displayName": "Not ends with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not end with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !endsWith RHS", "precedence": 10, "examples": [ "${header.name} !endsWith '.xml'" ] },
+    "++": { "index": 24, "kind": "operator", "displayName": "Inc", "label": "unary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Increments the numeric value by one. Must immediately follow a function closing brace.", "operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, "examples": [ "${header.count}++" ] },
+    "--": { "index": 25, "kind": "operator", "displayName": "Dec", "label": "unary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Decrements the numeric value by one. Must immediately follow a function closing brace.", "operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, "examples": [ "${header.count}--" ] },
+    "&&": { "index": 26, "kind": "operator", "displayName": "And", "label": "logical", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logical AND. Both left and right predicates must evaluate to true.", "operatorKind": "logical", "operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ "${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
+    "||": { "index": 27, "kind": "operator", "displayName": "Or", "label": "logical", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logical OR. At least one of the left or right predicates must evaluate to true.", "operatorKind": "logical", "operatorSyntax": "predicate || predicate", "precedence": 30, "examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] },
+    "? :": { "index": 28, "kind": "operator", "displayName": "Ternary", "label": "ternary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Ternary conditional operator. Evaluates the predicate and returns trueValue if true, falseValue if false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", "operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, "examples": [ "${header.foo} >  [...]
+    "~>": { "index": 29, "kind": "operator", "displayName": "Chain", "label": "chain", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pipes the result of the left expression as input body to the right expression. Use $param in the right expression to reference the piped value explicitly.", "operatorKind": "chain", "operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> ${uppercase()}", "${substrin [...]
+    "?~>": { "index": 30, "kind": "operator", "displayName": "Chain null safe", "label": "chain", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Null-safe chain operator. Same as ~> but stops chaining and returns null if the left expression evaluates to null.", "operatorKind": "chain", "operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ "${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
+    "?:": { "index": 31, "kind": "operator", "displayName": "Elvis", "label": "other", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Elvis operator (null-coalescing). Returns the left operand if it is not null\/empty, otherwise returns the right operand as a fallback value.", "operatorKind": "other", "operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ "${header.username} ?: 'Guest'", "${body} ?: $ [...]
   }
 }
diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc
index 70a50c4439e8..bd7c0b6df1ff 100644
--- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc
+++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc
@@ -2247,6 +2247,52 @@ If you use standalone Camel you can add `@BindToRegistry("foo-function")` to the
 For Spring Boot use `@Component` or `@Service` and Quarkus you can for example use `@ApplicationScoped`.
 
 
+== JavaScript Validator
+
+The `camel-catalog` JAR includes a self-contained JavaScript validator for the Simple language.
+It can syntax-check expressions and predicates in any browser or Node.js environment — no Java backend required.
+
+The files are located in the JAR under:
+
+----
+org/apache/camel/catalog/simple/camel-simple-validator.js
+org/apache/camel/catalog/simple/camel-simple-validator.html
+----
+
+The `.js` file is auto-generated from `simple.json` during the Maven build, so its list of functions and operators
+always matches the current Camel version.
+
+=== API
+
+The validator exposes these functions:
+
+* `validate(input, mode)` — validate a Simple expression or predicate.
+  `mode` is `'expression'` (default) or `'predicate'`.
+  Returns `{ valid: boolean, diagnostics: [...] }`.
+* `complete(input, pos)` — return completion suggestions at the given cursor position.
+* `getFunctions()` — return the list of known Simple function names.
+* `getOperators()` — return the list of known Simple operators with their kind and description.
+
+=== Usage in Node.js
+
+[source,javascript]
+----
+const { validate, complete } = require('./camel-simple-validator.js');
+
+const result = validate("${body} == 'hello'", 'predicate');
+console.log(result.valid);       // true
+console.log(result.diagnostics); // []
+
+const bad = validate("${bdy}", 'expression');
+console.log(bad.valid);          // false
+console.log(bad.diagnostics[0].message); // "Unknown function 'bdy', did you mean 'body'?"
+----
+
+=== Usage in a browser
+
+Extract `camel-simple-validator.html` from the JAR and open it in a browser.
+It provides an interactive form where you can type Simple expressions and validate them with instant feedback.
+
 == Dependencies
 
 The Simple language is part of *camel-core*.
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
index 4be673a39504..c199f275b586 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
@@ -39,7 +39,7 @@ import org.slf4j.LoggerFactory;
 /**
  * The Camel simple language.
  */
-@Language(value = "simple", functionsClass = SimpleConstants.class)
+@Language(value = "simple", functionsClass = SimpleConstants.class, operatorsClass = SimpleOperatorConstants.class)
 public class SimpleLanguage extends LanguageSupport implements StaticService {
 
     private static final Logger LOG = LoggerFactory.getLogger(SimpleLanguage.class);
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
new file mode 100644
index 000000000000..dfbca7987d91
--- /dev/null
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.simple;
+
+import org.apache.camel.spi.Metadata;
+
+@Metadata(label = "operator")
+public final class SimpleOperatorConstants {
+
+    // --- Binary comparison operators (precedence 10) ---
+
+    @Metadata(description = "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.",
+              label = "binary",
+              examples = { "${header.foo} == 'bar'", "${header.count} == 5" },
+              annotations = { "kind=binary", "syntax=LHS == RHS", "precedence=10" })
+    public static final String EQ = "==";
+
+    @Metadata(description = "Tests equality between left and right operand values, ignoring case for string comparison.",
+              label = "binary",
+              examples = { "${header.foo} =~ 'BAR'" },
+              annotations = { "kind=binary", "syntax=LHS =~ RHS", "precedence=10" })
+    public static final String EQ_IGNORE = "=~";
+
+    @Metadata(description = "Tests whether the left operand is greater than the right operand.",
+              label = "binary",
+              examples = { "${header.count} > 100" },
+              annotations = { "kind=binary", "syntax=LHS > RHS", "precedence=10" })
+    public static final String GT = ">";
+
+    @Metadata(description = "Tests whether the left operand is greater than or equal to the right operand.",
+              label = "binary",
+              examples = { "${header.count} >= 100" },
+              annotations = { "kind=binary", "syntax=LHS >= RHS", "precedence=10" })
+    public static final String GTE = ">=";
+
+    @Metadata(description = "Tests whether the left operand is less than the right operand.",
+              label = "binary",
+              examples = { "${header.count} < 100" },
+              annotations = { "kind=binary", "syntax=LHS < RHS", "precedence=10" })
+    public static final String LT = "<";
+
+    @Metadata(description = "Tests whether the left operand is less than or equal to the right operand.",
+              label = "binary",
+              examples = { "${header.count} <= 100" },
+              annotations = { "kind=binary", "syntax=LHS <= RHS", "precedence=10" })
+    public static final String LTE = "<=";
+
+    @Metadata(description = "Tests inequality between left and right operand values.",
+              label = "binary",
+              examples = { "${header.foo} != 'bar'" },
+              annotations = { "kind=binary", "syntax=LHS != RHS", "precedence=10" })
+    public static final String NOT_EQ = "!=";
+
+    @Metadata(description = "Tests inequality between left and right operand values, ignoring case for string comparison.",
+              label = "binary",
+              examples = { "${header.foo} !=~ 'BAR'" },
+              annotations = { "kind=binary", "syntax=LHS !=~ RHS", "precedence=10" })
+    public static final String NOT_EQ_IGNORE = "!=~";
+
+    @Metadata(description = "Tests whether the left operand string contains the right operand string.",
+              label = "binary",
+              examples = { "${header.title} contains 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS contains RHS", "precedence=10" })
+    public static final String CONTAINS = "contains";
+
+    @Metadata(description = "Tests whether the left operand string does not contain the right operand string.",
+              label = "binary",
+              examples = { "${header.title} !contains 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS !contains RHS", "precedence=10" })
+    public static final String NOT_CONTAINS = "!contains";
+
+    @Metadata(description = "Tests whether the left operand string contains the right operand string, ignoring case.",
+              label = "binary",
+              examples = { "${header.title} ~~ 'camel'" },
+              annotations = { "kind=binary", "syntax=LHS ~~ RHS", "precedence=10" })
+    public static final String CONTAINS_IGNORECASE = "~~";
+
+    @Metadata(description = "Tests whether the left operand string does not contain the right operand string, ignoring case.",
+              label = "binary",
+              examples = { "${header.title} !~~ 'camel'" },
+              annotations = { "kind=binary", "syntax=LHS !~~ RHS", "precedence=10" })
+    public static final String NOT_CONTAINS_IGNORECASE = "!~~";
+
+    @Metadata(description = "Tests whether the left operand matches the right operand as a regular expression.",
+              label = "binary",
+              examples = { "${header.number} regex '\\d{4}'" },
+              annotations = { "kind=binary", "syntax=LHS regex 'pattern'", "precedence=10" })
+    public static final String REGEX = "regex";
+
+    @Metadata(description = "Tests whether the left operand does not match the right operand as a regular expression.",
+              label = "binary",
+              examples = { "${header.number} !regex '\\d{4}'" },
+              annotations = { "kind=binary", "syntax=LHS !regex 'pattern'", "precedence=10" })
+    public static final String NOT_REGEX = "!regex";
+
+    @Metadata(description = "Tests whether the left operand is in a set of comma-separated values.",
+              label = "binary",
+              examples = { "${header.type} in 'gold,silver'" },
+              annotations = { "kind=binary", "syntax=LHS in 'val1,val2,...'", "precedence=10" })
+    public static final String IN = "in";
+
+    @Metadata(description = "Tests whether the left operand is not in a set of comma-separated values.",
+              label = "binary",
+              examples = { "${header.type} !in 'gold,silver'" },
+              annotations = { "kind=binary", "syntax=LHS !in 'val1,val2,...'", "precedence=10" })
+    public static final String NOT_IN = "!in";
+
+    @Metadata(description = "Tests whether the left operand is an instance of the right operand type (Java classname or short name).",
+              label = "binary",
+              examples = { "${header.type} is 'String'", "${body} is 'java.util.List'" },
+              annotations = { "kind=binary", "syntax=LHS is 'typeName'", "precedence=10" })
+    public static final String IS = "is";
+
+    @Metadata(description = "Tests whether the left operand is not an instance of the right operand type.",
+              label = "binary",
+              examples = { "${header.type} !is 'String'" },
+              annotations = { "kind=binary", "syntax=LHS !is 'typeName'", "precedence=10" })
+    public static final String NOT_IS = "!is";
+
+    @Metadata(description = "Tests whether the left operand is within the numeric range specified by 'from..to'.",
+              label = "binary",
+              examples = { "${header.number} range '100..199'" },
+              annotations = { "kind=binary", "syntax=LHS range 'from..to'", "precedence=10" })
+    public static final String RANGE = "range";
+
+    @Metadata(description = "Tests whether the left operand is not within the numeric range specified by 'from..to'.",
+              label = "binary",
+              examples = { "${header.number} !range '100..199'" },
+              annotations = { "kind=binary", "syntax=LHS !range 'from..to'", "precedence=10" })
+    public static final String NOT_RANGE = "!range";
+
+    @Metadata(description = "Tests whether the left operand string starts with the right operand string.",
+              label = "binary",
+              examples = { "${header.name} startsWith 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS startsWith RHS", "precedence=10" })
+    public static final String STARTS_WITH = "startsWith";
+
+    @Metadata(description = "Tests whether the left operand string does not start with the right operand string.",
+              label = "binary",
+              examples = { "${header.name} !startsWith 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS !startsWith RHS", "precedence=10" })
+    public static final String NOT_STARTS_WITH = "!startsWith";
+
+    @Metadata(description = "Tests whether the left operand string ends with the right operand string.",
+              label = "binary",
+              examples = { "${header.name} endsWith '.xml'" },
+              annotations = { "kind=binary", "syntax=LHS endsWith RHS", "precedence=10" })
+    public static final String ENDS_WITH = "endsWith";
+
+    @Metadata(description = "Tests whether the left operand string does not end with the right operand string.",
+              label = "binary",
+              examples = { "${header.name} !endsWith '.xml'" },
+              annotations = { "kind=binary", "syntax=LHS !endsWith RHS", "precedence=10" })
+    public static final String NOT_ENDS_WITH = "!endsWith";
+
+    // --- Unary operators (precedence 1) ---
+
+    @Metadata(description = "Increments the numeric value by one. Must immediately follow a function closing brace.",
+              label = "unary",
+              examples = { "${header.count}++" },
+              annotations = { "kind=unary", "syntax=${fn}++", "precedence=1" })
+    public static final String INC = "++";
+
+    @Metadata(description = "Decrements the numeric value by one. Must immediately follow a function closing brace.",
+              label = "unary",
+              examples = { "${header.count}--" },
+              annotations = { "kind=unary", "syntax=${fn}--", "precedence=1" })
+    public static final String DEC = "--";
+
+    // --- Logical operators (precedence 30) ---
+
+    @Metadata(description = "Logical AND. Both left and right predicates must evaluate to true.",
+              label = "logical",
+              examples = { "${header.title} contains 'Camel' && ${header.type} == 'gold'" },
+              annotations = { "kind=logical", "syntax=predicate && predicate", "precedence=30" })
+    public static final String AND = "&&";
+
+    @Metadata(description = "Logical OR. At least one of the left or right predicates must evaluate to true.",
+              label = "logical",
+              examples = { "${header.title} contains 'Camel' || ${header.type} == 'gold'" },
+              annotations = { "kind=logical", "syntax=predicate || predicate", "precedence=30" })
+    public static final String OR = "||";
+
+    // --- Ternary operator (precedence 25) ---
+
+    @Metadata(description = "Ternary conditional operator. Evaluates the predicate and returns trueValue if true, falseValue if false. Requires spaces around both ? and : tokens.",
+              label = "ternary",
+              examples = {
+                      "${header.foo} > 0 ? 'positive' : 'negative'",
+                      "${header.score} >= 90 ? 'A' : ${header.score} >= 80 ? 'B' : 'C'" },
+              annotations = { "kind=ternary", "syntax=predicate ? trueValue : falseValue", "precedence=25" })
+    public static final String TERNARY = "? :";
+
+    // --- Chain operators (precedence 5) ---
+
+    @Metadata(description = "Pipes the result of the left expression as input body to the right expression. Use $param in the right expression to reference the piped value explicitly.",
+              label = "chain",
+              examples = { "${trim()} ~> ${uppercase()}", "${substringAfter('Hello')} ~> ${trim()} ~> ${uppercase()}" },
+              annotations = { "kind=chain", "syntax=expr ~> expr", "precedence=5" })
+    public static final String CHAIN = "~>";
+
+    @Metadata(description = "Null-safe chain operator. Same as ~> but stops chaining and returns null if the left expression evaluates to null.",
+              label = "chain",
+              examples = { "${header.name} ?~> ${trim()} ?~> ${uppercase()}" },
+              annotations = { "kind=chain", "syntax=expr ?~> expr", "precedence=5" })
+    public static final String CHAIN_NULL_SAFE = "?~>";
+
+    // --- Other operators (precedence 20) ---
+
+    @Metadata(description = "Elvis operator (null-coalescing). Returns the left operand if it is not null/empty, otherwise returns the right operand as a fallback value.",
+              label = "other",
+              examples = { "${header.username} ?: 'Guest'", "${body} ?: ${header.default}" },
+              annotations = { "kind=other", "syntax=expr ?: defaultValue", "precedence=20" })
+    public static final String ELVIS = "?:";
+
+    private SimpleOperatorConstants() {
+    }
+}
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/support/Schema.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/support/Schema.java
index 1ed7982034db..ece10f183c11 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/support/Schema.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/support/Schema.java
@@ -32,6 +32,8 @@ public class Schema {
     public ObjectNode exchangeProperties;
     @JsonProperty
     public ObjectNode functions;
+    @JsonProperty
+    public ObjectNode operators;
 
     public Schema() {
     }
diff --git a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
index b5759e911672..7d87db4c33c1 100644
--- a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
+++ b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
@@ -414,6 +414,15 @@ public final class JsonMapper {
                 model.addFunction(func);
             }
         }
+        JsonObject mpro = (JsonObject) obj.get("operators");
+        if (mpro != null) {
+            for (Map.Entry<String, Object> entry : mpro.entrySet()) {
+                JsonObject mp = (JsonObject) entry.getValue();
+                LanguageModel.LanguageOperatorModel op = new LanguageModel.LanguageOperatorModel();
+                parseOperator(mp, op, entry.getKey());
+                model.addOperator(op);
+            }
+        }
         return model;
     }
 
@@ -436,6 +445,10 @@ public final class JsonMapper {
         if (!functions.isEmpty()) {
             wrapper.put("functions", asJsonObjectFunctions(functions));
         }
+        final List<LanguageModel.LanguageOperatorModel> operators = model.getOperators();
+        if (!operators.isEmpty()) {
+            wrapper.put("operators", asJsonObjectOperators(operators));
+        }
         return wrapper;
     }
 
@@ -667,6 +680,37 @@ public final class JsonMapper {
         }
     }
 
+    private static void parseOperator(JsonObject mp, LanguageModel.LanguageOperatorModel op, String name) {
+        op.setName(name);
+        op.setConstantName(name);
+        Integer idx = mp.getInteger("index");
+        if (idx != null) {
+            op.setIndex(idx);
+        }
+        op.setKind(mp.getString("kind"));
+        op.setDisplayName(mp.getString("displayName"));
+        op.setGroup(mp.getString("group"));
+        op.setLabel(mp.getString("label"));
+        op.setJavaType(mp.getString("javaType"));
+        op.setDeprecated(mp.getBooleanOrDefault("deprecated", false));
+        op.setDeprecationNote(mp.getString("deprecationNote"));
+        op.setDescription(mp.getString("description"));
+        op.setOperatorKind(mp.getString("operatorKind"));
+        op.setOperatorSyntax(mp.getString("operatorSyntax"));
+        Integer prec = mp.getInteger("precedence");
+        if (prec != null) {
+            op.setPrecedence(prec);
+        }
+        Object examplesObj = mp.get("examples");
+        if (examplesObj instanceof JsonArray examplesArr) {
+            for (Object e : examplesArr) {
+                if (e instanceof String s) {
+                    op.addExample(s);
+                }
+            }
+        }
+    }
+
     public static JsonObject asJsonObject(List<? extends BaseOptionModel> options) {
         JsonObject json = new JsonObject();
         for (int i = 0; i < options.size(); i++) {
@@ -717,6 +761,27 @@ public final class JsonMapper {
         return json;
     }
 
+    public static JsonObject asJsonObjectOperators(List<LanguageModel.LanguageOperatorModel> options) {
+        JsonObject json = new JsonObject();
+        for (int i = 0; i < options.size(); i++) {
+            var o = options.get(i);
+            o.setIndex(i);
+            JsonObject jo = asJsonObject(o);
+            if (o.getOperatorKind() != null) {
+                jo.put("operatorKind", o.getOperatorKind());
+            }
+            if (o.getOperatorSyntax() != null) {
+                jo.put("operatorSyntax", o.getOperatorSyntax());
+            }
+            jo.put("precedence", o.getPrecedence());
+            if (!o.getExamples().isEmpty()) {
+                jo.put("examples", new JsonArray(o.getExamples()));
+            }
+            json.put(o.getName(), jo);
+        }
+        return json;
+    }
+
     public static JsonObject apiModelAsJsonObject(Collection<ApiModel> model, boolean options) {
         JsonObject root = new JsonObject();
         model.forEach(a -> {
diff --git a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
index e2264dbfafe8..ce1ba4a1e118 100644
--- a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
+++ b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
@@ -24,6 +24,7 @@ public class LanguageModel extends ArtifactModel<LanguageModel.LanguageOptionMod
     protected String modelName;
     protected String modelJavaType;
     protected final List<LanguageFunctionModel> functions = new ArrayList<>();
+    protected final List<LanguageOperatorModel> operators = new ArrayList<>();
 
     public static class LanguageOptionModel extends BaseOptionModel {
 
@@ -61,6 +62,14 @@ public class LanguageModel extends ArtifactModel<LanguageModel.LanguageOptionMod
         functions.add(function);
     }
 
+    public List<LanguageOperatorModel> getOperators() {
+        return operators;
+    }
+
+    public void addOperator(LanguageOperatorModel operator) {
+        operators.add(operator);
+    }
+
     public static class LanguageFunctionModel extends BaseOptionModel {
 
         /**
@@ -144,6 +153,55 @@ public class LanguageModel extends ArtifactModel<LanguageModel.LanguageOptionMod
         }
     }
 
+    public static class LanguageOperatorModel extends BaseOptionModel {
+
+        private String constantName;
+        private String operatorKind;
+        private String operatorSyntax;
+        private int precedence;
+        private final List<String> examples = new ArrayList<>();
+
+        public String getConstantName() {
+            return constantName;
+        }
+
+        public void setConstantName(String constantName) {
+            this.constantName = constantName;
+        }
+
+        public String getOperatorKind() {
+            return operatorKind;
+        }
+
+        public void setOperatorKind(String operatorKind) {
+            this.operatorKind = operatorKind;
+        }
+
+        public String getOperatorSyntax() {
+            return operatorSyntax;
+        }
+
+        public void setOperatorSyntax(String operatorSyntax) {
+            this.operatorSyntax = operatorSyntax;
+        }
+
+        public int getPrecedence() {
+            return precedence;
+        }
+
+        public void setPrecedence(int precedence) {
+            this.precedence = precedence;
+        }
+
+        public List<String> getExamples() {
+            return examples;
+        }
+
+        public void addExample(String example) {
+            examples.add(example);
+        }
+    }
+
     public static class FunctionParamModel {
         private String name;
         private String javaType;
diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
index 00754c6d4c39..7ae51a8bdedc 100644
--- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
+++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
@@ -286,6 +286,24 @@ public class PackageLanguageMojo extends AbstractGeneratorMojo {
             }
         }
 
+        // read class and find operatorsClass and add each field as an operator in the model
+        if (lan.operatorsClass() != void.class) {
+            classElement = loadClass(lan.operatorsClass().getName());
+            if (classElement != null) {
+                for (Field field : classElement.getFields()) {
+                    final boolean isEnum = classElement.isEnum();
+                    if ((isEnum || isStatic(field.getModifiers()) && field.getType() == String.class)
+                            && field.isAnnotationPresent(Metadata.class)) {
+                        try {
+                            addOperator(model, field);
+                        } catch (Exception e) {
+                            getLog().warn(e);
+                        }
+                    }
+                }
+            }
+        }
+
         return model;
     }
 
@@ -370,6 +388,42 @@ public class PackageLanguageMojo extends AbstractGeneratorMojo {
         model.addFunction(fun);
     }
 
+    private void addOperator(LanguageModel model, Field field) throws Exception {
+        final Metadata metadata = field.getAnnotation(Metadata.class);
+        LanguageModel.LanguageOperatorModel op = new LanguageModel.LanguageOperatorModel();
+        op.setConstantName(String.format("%s#%s", field.getDeclaringClass().getName(), field.getName()));
+        op.setName((String) field.get(null));
+        op.setDescription(metadata.description().trim());
+        op.setKind("operator");
+        String displayName = metadata.displayName();
+        if (Strings.isNullOrEmpty(displayName)) {
+            displayName = Strings.asTitle(field.getName().replace('_', ' ').toLowerCase());
+        }
+        op.setDisplayName(displayName);
+        op.setDeprecated(field.isAnnotationPresent(Deprecated.class));
+        op.setDeprecationNote(metadata.deprecationNote());
+        op.setLabel(metadata.label());
+        if (metadata.examples() != null) {
+            for (String ex : metadata.examples()) {
+                if (!Strings.isNullOrEmpty(ex)) {
+                    op.addExample(ex.trim());
+                }
+            }
+        }
+        if (metadata.annotations() != null) {
+            for (String s : metadata.annotations()) {
+                if (s.startsWith("kind=")) {
+                    op.setOperatorKind(s.substring(5));
+                } else if (s.startsWith("syntax=")) {
+                    op.setOperatorSyntax(s.substring(7));
+                } else if (s.startsWith("precedence=")) {
+                    op.setPrecedence(Integer.parseInt(s.substring(11)));
+                }
+            }
+        }
+        model.addOperator(op);
+    }
+
     private static LanguageModel.FunctionParamModel parseParam(String value) {
         // format: name:javaType:required|optional:defaultValue:description
         String[] parts = value.split(":", 5);
diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
index ac0515a96e51..ec9f14808dec 100644
--- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
+++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
@@ -18,6 +18,7 @@ package org.apache.camel.maven.packaging;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
@@ -50,6 +51,8 @@ import org.apache.camel.tooling.model.DevConsoleModel;
 import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.tooling.model.JsonMapper;
 import org.apache.camel.tooling.model.LanguageModel;
+import org.apache.camel.tooling.model.LanguageModel.LanguageFunctionModel;
+import org.apache.camel.tooling.model.LanguageModel.LanguageOperatorModel;
 import org.apache.camel.tooling.model.OtherModel;
 import org.apache.camel.tooling.model.PojoBeanModel;
 import org.apache.camel.tooling.model.TransformerModel;
@@ -753,6 +756,9 @@ public class PrepareCatalogMojo extends AbstractMojo {
 
         printLanguagesReport(jsonFiles, duplicateJsonFiles, usedLabels, missingFirstVersions);
 
+        // Generate Simple language JavaScript validator with embedded catalog data
+        generateSimpleValidatorJs(languagesOutDir);
+
         return languagesNames;
     }
 
@@ -1533,4 +1539,95 @@ public class PrepareCatalogMojo extends AbstractMojo {
                 .collect(Collectors.toCollection(TreeSet::new));
     }
 
+    private void generateSimpleValidatorJs(Path languagesOutDir) throws Exception {
+        Path simpleJson = languagesOutDir.resolve("simple.json");
+        if (!Files.exists(simpleJson)) {
+            return;
+        }
+
+        String json = loadText(simpleJson);
+        LanguageModel model = JsonMapper.generateLanguageModel(json);
+
+        // Build JS object for functions keyed by base name (what extractBaseName returns).
+        // The validator looks up baseName in FUNCTIONS, so we strip (params), .suffix, :suffix.
+        StringBuilder sb = new StringBuilder();
+        sb.append("const FUNCTIONS = {\n");
+        Set<String> seenBases = new TreeSet<>();
+        List<LanguageFunctionModel> functions = model.getFunctions();
+        for (LanguageFunctionModel fn : functions) {
+            String baseName = extractFunctionBaseName(fn.getName());
+            if (seenBases.add(baseName)) {
+                if (seenBases.size() > 1) {
+                    sb.append(",\n");
+                }
+                sb.append("  '").append(escapeJs(baseName)).append("': true");
+            }
+        }
+        sb.append("\n};\n\n");
+
+        // Build JS object for operators keyed by symbol
+        sb.append("const OPERATORS = {\n");
+        List<LanguageOperatorModel> operators = model.getOperators();
+        for (int i = 0; i < operators.size(); i++) {
+            LanguageOperatorModel op = operators.get(i);
+            sb.append("  '").append(escapeJs(op.getName()));
+            sb.append("': { kind: '").append(escapeJs(op.getOperatorKind()));
+            sb.append("', description: '").append(escapeJs(op.getDescription()));
+            sb.append("' }");
+            if (i < operators.size() - 1) {
+                sb.append(",");
+            }
+            sb.append("\n");
+        }
+        sb.append("};\n");
+
+        // Read the template from classpath
+        String template;
+        try (InputStream is = getClass().getResourceAsStream("/camel-simple-validator-template.js")) {
+            if (is == null) {
+                getLog().warn("camel-simple-validator-template.js not found on classpath, skipping JS validator generation");
+                return;
+            }
+            template = loadText(is);
+        }
+
+        // Replace the placeholder with generated catalog data
+        String output = template.replace("// @CATALOG_DATA@", sb.toString());
+
+        // Write output to the simple/ directory alongside the catalog
+        Path simpleDir = languagesOutDir.resolve("../simple");
+        Files.createDirectories(simpleDir);
+        Path jsFile = simpleDir.resolve("camel-simple-validator.js");
+        FileUtil.updateFile(jsFile, output);
+        getLog().info("Generated " + jsFile);
+
+        // Copy the HTML demo file
+        try (InputStream is = getClass().getResourceAsStream("/camel-simple-validator.html")) {
+            if (is != null) {
+                String html = loadText(is);
+                Path htmlFile = simpleDir.resolve("camel-simple-validator.html");
+                FileUtil.updateFile(htmlFile, html);
+                getLog().info("Copied " + htmlFile);
+            }
+        }
+    }
+
+    private static String extractFunctionBaseName(String name) {
+        String base = name;
+        for (char ch : new char[] { '(', '.', ':' }) {
+            int pos = base.indexOf(ch);
+            if (pos != -1) {
+                base = base.substring(0, pos);
+            }
+        }
+        return base;
+    }
+
+    private static String escapeJs(String s) {
+        if (s == null) {
+            return "";
+        }
+        return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "");
+    }
+
 }
diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/camel-simple-validator-template.js b/tooling/maven/camel-package-maven-plugin/src/main/resources/camel-simple-validator-template.js
new file mode 100644
index 000000000000..f76e51ddd11e
--- /dev/null
+++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/camel-simple-validator-template.js
@@ -0,0 +1,646 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ============================================================================
+// Apache Camel Simple Language Validator
+//
+// A self-contained JavaScript validator for the Camel Simple expression
+// language. Works in any browser or Node.js — no Java backend needed.
+//
+// The catalog data (FUNCTIONS, OPERATORS) is AUTO-GENERATED from simple.json.
+// The Maven build regenerates the catalog data section on each build.
+// ============================================================================
+
+// === AUTO-GENERATED CATALOG DATA — do not edit by hand ===
+
+// @CATALOG_DATA@
+
+
+// ========================================================================
+// Tokenizer
+// ========================================================================
+
+function tokenize(input) {
+  const tokens = [];
+  let i = 0;
+  const len = input.length;
+
+  while (i < len) {
+    // Function start: ${ or $simple{
+    if (input[i] === '$' && i + 1 < len && input[i + 1] === '{') {
+      tokens.push({ type: 'functionStart', value: '${', start: i, end: i + 2 });
+      i += 2;
+      continue;
+    }
+    if (input.startsWith('$simple{', i)) {
+      tokens.push({ type: 'functionStart', value: '$simple{', start: i, end: i + 8 });
+      i += 8;
+      continue;
+    }
+
+    // Function end
+    if (input[i] === '}') {
+      tokens.push({ type: 'functionEnd', value: '}', start: i, end: i + 1 });
+      i += 1;
+      continue;
+    }
+
+    // Escape sequences
+    if (input[i] === '\\' && i + 1 < len) {
+      const next = input[i + 1];
+      tokens.push({ type: 'escape', value: '\\' + next, start: i, end: i + 2 });
+      i += 2;
+      continue;
+    }
+
+    // Single quote
+    if (input[i] === "'") {
+      tokens.push({ type: 'singleQuote', value: "'", start: i, end: i + 1 });
+      i += 1;
+      continue;
+    }
+
+    // Double quote
+    if (input[i] === '"') {
+      tokens.push({ type: 'doubleQuote', value: '"', start: i, end: i + 1 });
+      i += 1;
+      continue;
+    }
+
+    // Whitespace
+    if (input[i] === ' ' || input[i] === '\t' || input[i] === '\n' || input[i] === '\r') {
+      let start = i;
+      while (i < len && (input[i] === ' ' || input[i] === '\t' || input[i] === '\n' || input[i] === '\r')) {
+        i++;
+      }
+      tokens.push({ type: 'whitespace', value: input.substring(start, i), start, end: i });
+      continue;
+    }
+
+    // Text (everything else — accumulated into runs)
+    let start = i;
+    while (i < len && input[i] !== '$' && input[i] !== '}' && input[i] !== '\\' &&
+           input[i] !== "'" && input[i] !== '"' &&
+           input[i] !== ' ' && input[i] !== '\t' && input[i] !== '\n' && input[i] !== '\r') {
+      i++;
+    }
+    if (i > start) {
+      tokens.push({ type: 'text', value: input.substring(start, i), start, end: i });
+    }
+  }
+
+  return tokens;
+}
+
+// ========================================================================
+// Validator
+// ========================================================================
+
+function findClosestFunction(name) {
+  const names = Object.keys(FUNCTIONS);
+  let best = null;
+  let bestDist = Infinity;
+  for (const fn of names) {
+    const d = levenshtein(name.toLowerCase(), fn.toLowerCase());
+    if (d < bestDist && d <= 3) {
+      bestDist = d;
+      best = fn;
+    }
+  }
+  return best;
+}
+
+function levenshtein(a, b) {
+  const m = a.length, n = b.length;
+  if (m === 0) return n;
+  if (n === 0) return m;
+  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
+  for (let i = 0; i <= m; i++) dp[i][0] = i;
+  for (let j = 0; j <= n; j++) dp[0][j] = j;
+  for (let i = 1; i <= m; i++) {
+    for (let j = 1; j <= n; j++) {
+      dp[i][j] = a[i-1] === b[j-1]
+        ? dp[i-1][j-1]
+        : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
+    }
+  }
+  return dp[m][n];
+}
+
+function extractBaseName(content) {
+  let base = content;
+  for (const ch of ['(', '.', ':']) {
+    const pos = base.indexOf(ch);
+    if (pos !== -1) {
+      base = base.substring(0, pos);
+    }
+  }
+  return base;
+}
+
+function isKnownOperator(word) {
+  return word in OPERATORS;
+}
+
+function findBinaryOperator(text) {
+  const binaryOps = Object.entries(OPERATORS)
+    .filter(([, op]) => op.kind === 'binary')
+    .map(([sym]) => sym)
+    .sort((a, b) => b.length - a.length);
+  for (const op of binaryOps) {
+    if (text === op) return op;
+  }
+  return null;
+}
+
+function validate(input, mode) {
+  if (mode === undefined) mode = 'expression';
+  const diagnostics = [];
+  const tokens = tokenize(input);
+
+  // Track delimiter balancing
+  let functionDepth = 0;
+  const functionStarts = [];
+  let inSingleQuote = false;
+  let singleQuoteStart = -1;
+  let inDoubleQuote = false;
+  let doubleQuoteStart = -1;
+
+  // Collect function contents for validation
+  const functionBlocks = [];
+  let currentFunctionStart = -1;
+  let currentFunctionTokenStart = -1;
+
+  for (let t = 0; t < tokens.length; t++) {
+    const tok = tokens[t];
+
+    if (tok.type === 'singleQuote' && !inDoubleQuote) {
+      if (inSingleQuote) {
+        inSingleQuote = false;
+      } else {
+        inSingleQuote = true;
+        singleQuoteStart = tok.start;
+      }
+      continue;
+    }
+
+    if (tok.type === 'doubleQuote' && !inSingleQuote) {
+      if (inDoubleQuote) {
+        inDoubleQuote = false;
+      } else {
+        inDoubleQuote = true;
+        doubleQuoteStart = tok.start;
+      }
+      continue;
+    }
+
+    // Inside quotes, only check for unmatched function braces
+    if (inSingleQuote || inDoubleQuote) {
+      if (tok.type === 'functionStart') {
+        functionDepth++;
+        functionStarts.push(tok.start);
+        currentFunctionStart = tok.start;
+        currentFunctionTokenStart = t;
+      } else if (tok.type === 'functionEnd') {
+        if (functionDepth > 0) {
+          functionDepth--;
+          const fnStart = functionStarts.pop();
+          const fnContent = input.substring(fnStart + 2, tok.start);
+          functionBlocks.push({ content: fnContent, start: fnStart, end: tok.end });
+        }
+      }
+      continue;
+    }
+
+    if (tok.type === 'functionStart') {
+      functionDepth++;
+      functionStarts.push(tok.start);
+      currentFunctionStart = tok.start;
+      currentFunctionTokenStart = t;
+    } else if (tok.type === 'functionEnd') {
+      if (functionDepth > 0) {
+        functionDepth--;
+        const fnStart = functionStarts.pop();
+        const fnContent = input.substring(fnStart + (input.startsWith('$simple{', fnStart) ? 8 : 2), tok.start);
+        functionBlocks.push({ content: fnContent, start: fnStart, end: tok.end });
+      } else {
+        diagnostics.push({
+          severity: 'error',
+          message: "Unexpected '}' without matching '${' ",
+          start: tok.start,
+          end: tok.end
+        });
+      }
+    }
+
+    // Validate escape sequences
+    if (tok.type === 'escape') {
+      const escaped = tok.value[1];
+      if (!['n', 't', 'r', '}', '\\', "'", '"'].includes(escaped)) {
+        diagnostics.push({
+          severity: 'warning',
+          message: `Unusual escape sequence '\\${escaped}'`,
+          start: tok.start,
+          end: tok.end
+        });
+      }
+    }
+  }
+
+  // Check for unclosed delimiters
+  if (functionDepth > 0) {
+    const unclosedStart = functionStarts[functionStarts.length - 1];
+    diagnostics.push({
+      severity: 'error',
+      message: "'${' has no closing '}'",
+      start: unclosedStart,
+      end: unclosedStart + 2
+    });
+  }
+
+  if (inSingleQuote) {
+    diagnostics.push({
+      severity: 'error',
+      message: "Single quote has no closing quote",
+      start: singleQuoteStart,
+      end: singleQuoteStart + 1
+    });
+  }
+
+  if (inDoubleQuote) {
+    diagnostics.push({
+      severity: 'error',
+      message: "Double quote has no closing quote",
+      start: doubleQuoteStart,
+      end: doubleQuoteStart + 1
+    });
+  }
+
+  // Validate function names
+  for (const block of functionBlocks) {
+    validateFunction(block.content, block.start, block.end, diagnostics);
+  }
+
+  // Validate predicate structure
+  if (mode === 'predicate') {
+    validatePredicate(input, tokens, diagnostics);
+  }
+
+  return {
+    valid: diagnostics.filter(d => d.severity === 'error').length === 0,
+    diagnostics
+  };
+}
+
+function validateFunction(content, blockStart, blockEnd, diagnostics) {
+  // Handle nested functions — only validate the outermost name
+  const baseName = extractBaseName(content);
+
+  if (!baseName || baseName.length === 0) {
+    diagnostics.push({
+      severity: 'error',
+      message: "Empty function reference '${}' ",
+      start: blockStart,
+      end: blockEnd
+    });
+    return;
+  }
+
+  // Skip validation for contents that start with ${ (nested function as argument)
+  if (baseName.startsWith('$')) {
+    return;
+  }
+
+  // Check if function name is known
+  if (!(baseName in FUNCTIONS)) {
+    const suggestion = findClosestFunction(baseName);
+    const msg = suggestion
+      ? `Unknown function '${baseName}', did you mean '${suggestion}'?`
+      : `Unknown function '${baseName}'`;
+    diagnostics.push({
+      severity: 'error',
+      message: msg,
+      start: blockStart,
+      end: blockEnd,
+      suggestion
+    });
+  }
+}
+
+function validatePredicate(input, tokens, diagnostics) {
+  // Build a simplified structure: segments separated by operators
+  // We look for operator tokens in the non-quoted, non-function text
+  const segments = [];
+  let current = '';
+  let currentStart = 0;
+  let depth = 0;
+  let inQuote = false;
+  let quoteChar = null;
+
+  for (let i = 0; i < input.length; i++) {
+    const ch = input[i];
+
+    // Track quote state
+    if ((ch === "'" || ch === '"') && depth === 0) {
+      if (!inQuote) {
+        inQuote = true;
+        quoteChar = ch;
+      } else if (ch === quoteChar) {
+        inQuote = false;
+        quoteChar = null;
+      }
+      current += ch;
+      continue;
+    }
+
+    if (inQuote) {
+      current += ch;
+      continue;
+    }
+
+    // Track function depth
+    if (ch === '$' && i + 1 < input.length && input[i + 1] === '{') {
+      depth++;
+      current += '${';
+      i++;
+      continue;
+    }
+    if (ch === '}') {
+      if (depth > 0) depth--;
+      current += '}';
+      continue;
+    }
+
+    // Outside functions and quotes — check for operators
+    if (depth === 0) {
+      let rest = input.substring(i).trimStart();
+      let skipWs = input.substring(i).length - rest.length;
+      let opMatch = null;
+
+      // Check all binary/logical operators (longest match first)
+      const allOps = Object.entries(OPERATORS)
+        .filter(([, op]) => op.kind === 'binary' || op.kind === 'logical')
+        .map(([sym]) => sym)
+        .sort((a, b) => b.length - a.length);
+
+      for (const op of allOps) {
+        if (rest.startsWith(op) && (rest.length === op.length || rest[op.length] === ' ')) {
+          opMatch = op;
+          break;
+        }
+      }
+
+      if (opMatch && (ch === ' ' || current.trim().length === 0)) {
+        // Found an operator — save the left segment
+        if (current.trim().length > 0) {
+          segments.push({ type: 'operand', value: current.trim(), start: currentStart });
+        }
+        const opStart = i + skipWs;
+        segments.push({ type: 'operator', value: opMatch, start: opStart, kind: OPERATORS[opMatch].kind });
+        i = opStart + opMatch.length;
+        current = '';
+        currentStart = i;
+        continue;
+      }
+    }
+
+    current += ch;
+  }
+
+  if (current.trim().length > 0) {
+    segments.push({ type: 'operand', value: current.trim(), start: currentStart });
+  }
+
+  // Check for misspelled word operators between operands
+  const wordOperators = Object.keys(OPERATORS).filter(op => /^[a-zA-Z!]/.test(op));
+  for (let s = 0; s < segments.length; s++) {
+    const seg = segments[s];
+    if (seg.type === 'operand') {
+      const words = seg.value.split(/\s+/);
+      for (const word of words) {
+        if (word.length >= 2 && /^!?[a-zA-Z]+$/.test(word) && !findBinaryOperator(word)) {
+          for (const op of wordOperators) {
+            if (levenshtein(word.toLowerCase(), op.toLowerCase()) <= 2 && word.toLowerCase() !== op.toLowerCase()) {
+              diagnostics.push({
+                severity: 'warning',
+                message: `'${word}' looks like a misspelled operator, did you mean '${op}'?`,
+                start: seg.start + seg.value.indexOf(word),
+                end: seg.start + seg.value.indexOf(word) + word.length,
+                suggestion: op
+              });
+              break;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // Validate operator usage
+  for (let s = 0; s < segments.length; s++) {
+    const seg = segments[s];
+    if (seg.type === 'operator') {
+      const opInfo = OPERATORS[seg.value];
+      if (opInfo && (opInfo.kind === 'binary' || opInfo.kind === 'logical')) {
+        // Check for LHS
+        const prev = s > 0 ? segments[s - 1] : null;
+        if (!prev || prev.type !== 'operand') {
+          diagnostics.push({
+            severity: 'error',
+            message: `Operator '${seg.value}' has no left-hand side operand`,
+            start: seg.start,
+            end: seg.start + seg.value.length
+          });
+        }
+        // Check for RHS
+        const next = s + 1 < segments.length ? segments[s + 1] : null;
+        if (!next || next.type !== 'operand') {
+          diagnostics.push({
+            severity: 'error',
+            message: `Operator '${seg.value}' has no right-hand side operand`,
+            start: seg.start,
+            end: seg.start + seg.value.length
+          });
+        }
+      }
+    }
+  }
+}
+
+// ========================================================================
+// Autocomplete
+// ========================================================================
+
+function complete(input, cursor) {
+  const suggestions = [];
+
+  // Find context at cursor position
+  const before = input.substring(0, cursor);
+  const lastDollarBrace = before.lastIndexOf('${');
+  const lastCloseBrace = before.lastIndexOf('}');
+
+  if (lastDollarBrace > lastCloseBrace) {
+    // We are inside a ${...} — suggest function names
+    const partial = before.substring(lastDollarBrace + 2);
+    const basePart = extractBaseName(partial).toLowerCase();
+
+    for (const [name, fns] of Object.entries(FUNCTIONS)) {
+      if (name.toLowerCase().startsWith(basePart)) {
+        for (const fn of fns) {
+          suggestions.push({
+            label: fn.name,
+            displayName: fn.displayName,
+            description: fn.description,
+            group: fn.group,
+            insertText: fn.name
+          });
+        }
+      }
+    }
+  } else if (before.trimEnd().endsWith('}') || /\S$/.test(before)) {
+    // After a function or operand — suggest operators
+    for (const [sym, op] of Object.entries(OPERATORS)) {
+      if (op.kind === 'binary' || op.kind === 'logical') {
+        suggestions.push({
+          label: sym,
+          displayName: op.displayName,
+          description: op.description,
+          insertText: ' ' + sym + ' '
+        });
+      }
+    }
+  }
+
+  return suggestions;
+}
+
+// ========================================================================
+// Public API
+// ========================================================================
+
+function getFunctions() {
+  return FUNCTIONS;
+}
+
+function getOperators() {
+  return OPERATORS;
+}
+
+// ========================================================================
+// Exports (works in both Node.js and browser)
+// ========================================================================
+
+const CamelSimpleValidator = { validate, complete, getFunctions, getOperators, tokenize };
+
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = CamelSimpleValidator;
+}
+if (typeof globalThis !== 'undefined') {
+  globalThis.CamelSimpleValidator = CamelSimpleValidator;
+}
+
+// ========================================================================
+// Self-test (run with: node camel-simple-validator.js)
+// ========================================================================
+
+if (typeof require !== 'undefined' && require.main === module) {
+  let passed = 0;
+  let failed = 0;
+
+  function test(name, input, mode, expectValid, expectErrorSubstring) {
+    const result = validate(input, mode);
+    let ok = result.valid === expectValid;
+    if (expectErrorSubstring) {
+      const hasMatch = result.diagnostics.some(d => d.message.includes(expectErrorSubstring));
+      ok = ok && hasMatch;
+    }
+    if (ok) {
+      passed++;
+    } else {
+      failed++;
+      console.log(`FAIL: ${name}`);
+      console.log(`  input: ${JSON.stringify(input)}`);
+      console.log(`  expected valid=${expectValid}` + (expectErrorSubstring ? `, error containing "${expectErrorSubstring}"` : ''));
+      console.log(`  got valid=${result.valid}, diagnostics:`, JSON.stringify(result.diagnostics, null, 2));
+    }
+  }
+
+  // === Valid expressions ===
+  test('simple body', '${body}', 'expression', true);
+  test('header access', '${header.foo}', 'expression', true);
+  test('template text', 'Hello ${body}', 'expression', true);
+  test('multiple functions', '${header.from} to ${header.to}', 'expression', true);
+  test('nested function in text', "Hello ${header.name}, you have ${header.count} items", 'expression', true);
+  test('exchangeId', '${exchangeId}', 'expression', true);
+  test('function with parens', '${trim()}', 'expression', true);
+  test('function with args', '${substring(1,3)}', 'expression', true);
+  test('plain text', 'Hello World', 'expression', true);
+  test('empty string', '', 'expression', true);
+  test('env variable', '${env.HOME}', 'expression', true);
+  test('sys property', '${sys.user.name}', 'expression', true);
+  test('properties', '${properties:myKey}', 'expression', true);
+  test('date function', '${date(now)}', 'expression', true);
+  test('camelId', '${camelId}', 'expression', true);
+  test('routeId', '${routeId}', 'expression', true);
+  test('exchangeProperty', '${exchangeProperty.myProp}', 'expression', true);
+  test('variable', '${variable.myVar}', 'expression', true);
+  test('bodyAs', '${bodyAs(String)}', 'expression', true);
+  test('uppercase', '${uppercase(${body})}', 'expression', true);
+  test('escape newline', 'Hello\\nWorld', 'expression', true);
+  test('escape tab', 'col1\\tcol2', 'expression', true);
+  test('$simple prefix', '$simple{body}', 'expression', true);
+
+  // === Valid predicates ===
+  test('simple equality', "${body} == 'foo'", 'predicate', true);
+  test('numeric comparison', '${header.count} > 5', 'predicate', true);
+  test('contains', "${header.title} contains 'Camel'", 'predicate', true);
+  test('logical AND', "${body} == 'foo' && ${header.bar} == 'baz'", 'predicate', true);
+  test('logical OR', "${body} == 'foo' || ${body} == 'bar'", 'predicate', true);
+  test('not equal', "${header.type} != 'test'", 'predicate', true);
+  test('regex operator', "${header.code} regex '\\d{3}'", 'predicate', true);
+  test('in operator', "${header.color} in 'red,blue,green'", 'predicate', true);
+  test('range operator', "${header.age} range '18..65'", 'predicate', true);
+  test('is operator', "${body} is 'String'", 'predicate', true);
+  test('startsWith', "${header.name} startsWith 'Camel'", 'predicate', true);
+  test('endsWith', "${header.file} endsWith '.xml'", 'predicate', true);
+  test('case-insensitive contains', "${header.title} ~~ 'camel'", 'predicate', true);
+  test('greater or equal', '${header.count} >= 10', 'predicate', true);
+  test('less or equal', '${header.count} <= 100', 'predicate', true);
+
+  // === Invalid expressions ===
+  test('unclosed function', '${body', 'expression', false, "no closing '}'");
+  test('extra closing brace', 'body}', 'expression', false, "Unexpected '}'");
+  test('unknown function', '${boddy}', 'expression', false, "Unknown function 'boddy'");
+  test('unknown function with suggestion', '${headr.foo}', 'expression', false, "did you mean 'header'");
+  test('empty function', '${}', 'expression', false, "Empty function");
+  test('unclosed single quote', "Hello '${body}", 'expression', false, "Single quote has no closing");
+  test('unclosed double quote', 'Hello "${body}', 'expression', false, "Double quote has no closing");
+  test('unknown function trim typo', '${trm()}', 'expression', false, "did you mean 'trim'");
+
+  // === Invalid predicates ===
+  test('operator no RHS', "${body} ==", 'predicate', false, "no right-hand side");
+  test('operator no LHS', "== 'foo'", 'predicate', false, "no left-hand side");
+
+  // === Predicate warnings ===
+  test('misspelled operator', "${header.foo} contans 'bar'", 'predicate', true, "did you mean 'contains'");
+
+  // === Summary ===
+  console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed`);
+  if (failed > 0) {
+    process.exit(1);
+  }
+}
diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/camel-simple-validator.html b/tooling/maven/camel-package-maven-plugin/src/main/resources/camel-simple-validator.html
new file mode 100644
index 000000000000..f71946ebc11b
--- /dev/null
+++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/camel-simple-validator.html
@@ -0,0 +1,241 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE html>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Apache Camel Simple Language Validator</title>
+<style>
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+    background: #f5f5f5; color: #333; padding: 2rem;
+    max-width: 900px; margin: 0 auto;
+  }
+  h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
+  .subtitle { color: #666; font-size: 0.9rem; margin-bottom: 1.5rem; }
+  .input-group { margin-bottom: 1rem; }
+  label { display: block; font-weight: 600; margin-bottom: 0.3rem; font-size: 0.9rem; }
+  input[type="text"] {
+    width: 100%; padding: 0.6rem 0.8rem; font-size: 1rem;
+    font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
+    border: 2px solid #ccc; border-radius: 6px; outline: none;
+    transition: border-color 0.2s;
+  }
+  input[type="text"]:focus { border-color: #e77225; }
+  .controls { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 1.5rem; }
+  .mode-toggle { display: flex; border: 2px solid #ccc; border-radius: 6px; overflow: hidden; }
+  .mode-toggle button {
+    padding: 0.5rem 1rem; border: none; background: #fff;
+    cursor: pointer; font-size: 0.85rem; font-weight: 500; color: #555;
+    transition: all 0.2s;
+  }
+  .mode-toggle button.active { background: #e77225; color: #fff; }
+  .mode-toggle button:not(.active):hover { background: #f0f0f0; }
+  .validate-btn {
+    padding: 0.5rem 1.5rem; border: none; border-radius: 6px;
+    background: #e77225; color: #fff; font-size: 0.9rem; font-weight: 600;
+    cursor: pointer; transition: background 0.2s;
+  }
+  .validate-btn:hover { background: #c5611e; }
+  .result {
+    border-radius: 8px; padding: 1rem 1.25rem;
+    margin-bottom: 1rem; display: none;
+  }
+  .result.valid { background: #e8f5e9; border: 1px solid #a5d6a7; }
+  .result.invalid { background: #fce4ec; border: 1px solid #ef9a9a; }
+  .result.warning { background: #fff8e1; border: 1px solid #ffe082; }
+  .result-header {
+    font-weight: 700; font-size: 1rem; margin-bottom: 0.5rem;
+    display: flex; align-items: center; gap: 0.5rem;
+  }
+  .result.valid .result-header { color: #2e7d32; }
+  .result.invalid .result-header { color: #c62828; }
+  .result.warning .result-header { color: #f57f17; }
+  .diagnostic {
+    padding: 0.5rem 0.75rem; margin-top: 0.5rem;
+    background: rgba(255,255,255,0.7); border-radius: 4px;
+    font-size: 0.85rem;
+  }
+  .diagnostic .severity {
+    font-weight: 700; text-transform: uppercase; font-size: 0.75rem;
+    padding: 0.1rem 0.4rem; border-radius: 3px; margin-right: 0.4rem;
+  }
+  .diagnostic .severity.error { background: #ef9a9a; color: #b71c1c; }
+  .diagnostic .severity.warning { background: #ffe082; color: #e65100; }
+  .diagnostic .severity.info { background: #b3e5fc; color: #01579b; }
+  .diagnostic .position { color: #888; font-size: 0.8rem; margin-left: 0.5rem; }
+  .diagnostic .suggestion { color: #2e7d32; font-weight: 600; }
+  .pointer-line {
+    font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
+    font-size: 0.85rem; margin-top: 0.3rem; color: #888;
+    white-space: pre;
+  }
+  .pointer-line .caret { color: #c62828; font-weight: 700; }
+  .examples {
+    margin-top: 2rem; padding-top: 1.5rem;
+    border-top: 1px solid #ddd;
+  }
+  .examples h2 { font-size: 1.1rem; margin-bottom: 0.75rem; }
+  .example-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
+  .example-btn {
+    padding: 0.35rem 0.75rem; border: 1px solid #ccc; border-radius: 4px;
+    background: #fff; cursor: pointer; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
+    font-size: 0.8rem; color: #555; transition: all 0.2s;
+  }
+  .example-btn:hover { border-color: #e77225; color: #e77225; }
+  .example-btn.error-example { border-color: #ef9a9a; color: #c62828; }
+  .example-btn.error-example:hover { background: #fce4ec; }
+  kbd {
+    background: #eee; border: 1px solid #ccc; border-radius: 3px;
+    padding: 0.1rem 0.4rem; font-size: 0.8rem;
+  }
+  .hint { color: #888; font-size: 0.8rem; margin-top: 0.3rem; }
+</style>
+</head>
+<body>
+
+<h1>Apache Camel Simple Language Validator</h1>
+<p class="subtitle">Validate Simple expressions and predicates in the browser — no Java backend needed.</p>
+
+<div class="input-group">
+  <label for="expr">Simple Expression</label>
+  <input type="text" id="expr" placeholder="${body} == 'Hello World'" autofocus>
+  <p class="hint">Press <kbd>Enter</kbd> to validate, or click the button below.</p>
+</div>
+
+<div class="controls">
+  <div class="mode-toggle">
+    <button id="mode-expr" class="active" onclick="setMode('expression')">Expression</button>
+    <button id="mode-pred" onclick="setMode('predicate')">Predicate</button>
+  </div>
+  <button class="validate-btn" onclick="runValidation()">Validate</button>
+</div>
+
+<div id="result" class="result"></div>
+
+<div class="examples">
+  <h2>Try these examples</h2>
+  <div class="example-list">
+    <button class="example-btn" onclick="tryExample('${body}', 'expression')"><span>${body}</span></button>
+    <button class="example-btn" onclick="tryExample('${header.foo}', 'expression')">${header.foo}</button>
+    <button class="example-btn" onclick="tryExample('Hello ${header.name}', 'expression')">Hello ${header.name}</button>
+    <button class="example-btn" onclick="tryExample('${trim()} ~> ${uppercase()}', 'expression')">${trim()} ~> ${uppercase()}</button>
+    <button class="example-btn" onclick="tryExample(\"${header.name} ?: \\'Guest\\'\", 'expression')">${header.name} ?: 'Guest'</button>
+    <button class="example-btn" onclick="tryExample(\"${body} == \\'foo\\'\", 'predicate')">${body} == 'foo'</button>
+    <button class="example-btn" onclick="tryExample(\"${header.count} > 5 && ${header.type} == \\'gold\\'\", 'predicate')">${header.count} > 5 && ...</button>
+    <button class="example-btn" onclick="tryExample(\"${header.age} range \\'18..65\\'\", 'predicate')">${header.age} range '18..65'</button>
+    <button class="example-btn error-example" onclick="tryExample('${boddy}', 'expression')">${boddy}</button>
+    <button class="example-btn error-example" onclick="tryExample('${body', 'expression')">${body</button>
+    <button class="example-btn error-example" onclick="tryExample(\"${header.foo} contans \\'bar\\'\", 'predicate')">contans (typo)</button>
+    <button class="example-btn error-example" onclick="tryExample(\"${body} ==\", 'predicate')">${body} == (no RHS)</button>
+  </div>
+</div>
+
+<script src="camel-simple-validator.js"></script>
+<script>
+let mode = 'expression';
+
+function setMode(m) {
+  mode = m;
+  document.getElementById('mode-expr').className = m === 'expression' ? 'active' : '';
+  document.getElementById('mode-pred').className = m === 'predicate' ? 'active' : '';
+}
+
+function tryExample(expr, m) {
+  document.getElementById('expr').value = expr;
+  setMode(m);
+  runValidation();
+}
+
+function runValidation() {
+  const input = document.getElementById('expr').value;
+  const result = CamelSimpleValidator.validate(input, mode);
+  const el = document.getElementById('result');
+  el.style.display = 'block';
+
+  const hasErrors = result.diagnostics.some(d => d.severity === 'error');
+  const hasWarnings = result.diagnostics.some(d => d.severity === 'warning');
+
+  if (result.valid && !hasWarnings) {
+    el.className = 'result valid';
+    el.innerHTML = '<div class="result-header">✓ Valid ' + mode + '</div>';
+  } else if (result.valid && hasWarnings) {
+    el.className = 'result warning';
+    let html = '<div class="result-header">⚠ Valid with warnings</div>';
+    html += renderDiagnostics(input, result.diagnostics);
+    el.innerHTML = html;
+  } else {
+    el.className = 'result invalid';
+    let html = '<div class="result-header">✗ Invalid ' + mode + '</div>';
+    html += renderDiagnostics(input, result.diagnostics);
+    el.innerHTML = html;
+  }
+}
+
+function renderDiagnostics(input, diagnostics) {
+  let html = '';
+  for (const d of diagnostics) {
+    html += '<div class="diagnostic">';
+    html += '<span class="severity ' + d.severity + '">' + d.severity + '</span>';
+    html += escapeHtml(d.message);
+    if (d.suggestion) {
+      html += ' <span class="suggestion">→ ' + escapeHtml(d.suggestion) + '</span>';
+    }
+    html += '<span class="position">pos ' + d.start + '-' + d.end + '</span>';
+    // Pointer line
+    html += '<div class="pointer-line">';
+    html += escapeHtml(input) + '\n';
+    let pointer = '';
+    for (let i = 0; i < d.start; i++) pointer += ' ';
+    for (let i = d.start; i < d.end; i++) pointer += '^';
+    html += '<span class="caret">' + pointer + '</span>';
+    html += '</div>';
+    html += '</div>';
+  }
+  return html;
+}
+
+function escapeHtml(s) {
+  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+}
+
+document.getElementById('expr').addEventListener('keydown', function(e) {
+  if (e.key === 'Enter') runValidation();
+});
+</script>
+</body>
+</html>
diff --git a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
index 5dc93f9f4b60..a45a9f2cef2d 100644
--- a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
+++ b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
@@ -43,4 +43,17 @@ public @interface Language {
      */
     Class<?> functionsClass() default void.class;
 
+    /**
+     * The class that contains all the name of operators that are supported by the language. The name of the operators
+     * are defined as {@code String} constants in the operators class.
+     *
+     * The class to provide can be any class but by convention, we would expect a class whose name is of type
+     * <i>xxxOperatorConstants</i> where <i>xxx</i> is the name of the corresponding language like for example
+     * <i>SimpleOperatorConstants</i> for the language <i>camel-simple</i>.
+     *
+     * The metadata of a given operator are retrieved directly from the annotation {@code @Metadata} added to the
+     * {@code String} constant representing its name and defined in the operators class.
+     */
+    Class<?> operatorsClass() default void.class;
+
 }