@agentix-e/spel-editor 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -19
- package/README.md +65 -9
- package/dist/index.js +157 -77
- package/package.json +19 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,31 +2,127 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
7
|
|
|
8
|
+
## [1.2.0] — 2026-09-13
|
|
9
|
+
### Added
|
|
10
|
+
- `docs/integration.md` — embedding, theming, accessibility, and how to call a provider
|
|
11
|
+
without shipping its key to the browser.
|
|
12
|
+
- Ten `--spel-token-*` custom properties, one per token class, so the syntax palette can be
|
|
13
|
+
replaced from CSS.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **`@agentix-e/spel-ts` moves to `^2.0.0`.** This package uses spel-ts at runtime — the
|
|
17
|
+
tokenizer and `TokenKind` for the grammar, the parser behind lint and hover, and the
|
|
18
|
+
completion, formatter and diagnostic engines — so the major bump is a real dependency
|
|
19
|
+
change rather than a formality. `TokenKind`'s ordinals are renumbered in 2.0.0 and an
|
|
20
|
+
unsuffixed integer literal above `int` range is no longer accepted. The grammar refers
|
|
21
|
+
to members by name and no literal above `int` range appears anywhere in this package's
|
|
22
|
+
sources or tests, so neither change should reach its behaviour — and the suite, not
|
|
23
|
+
that reasoning, is what establishes it.
|
|
24
|
+
- `typecheck` now also checks the tests, which `tsconfig.json` had excluded by including
|
|
25
|
+
`src` alone. Fifty-two errors surfaced, all of them real, and are fixed.
|
|
26
|
+
- Coverage thresholds raised to 95 on all four dimensions, from 85/80/85/85.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **`true`, `false`, `null`, `and`, `or`, `matches`, `between`, `instanceof` and `new`
|
|
30
|
+
were painted as ordinary variables.** spel-ts 2.0.0 moved those words out of its
|
|
31
|
+
tokenizer — it resolves them in the parser, with `equalsIgnoreCase`, which is also
|
|
32
|
+
what lets a field be called `and` — so all nine now arrive as `IDENTIFIER` instead of
|
|
33
|
+
token kinds of their own. The grammar decides them from the text, and only when they
|
|
34
|
+
are not names: `#and` is still a variable and `obj.and` still a property.
|
|
35
|
+
- **`div` was painted as nothing.** spel-ts 2.0.0 gives the textual division operator a
|
|
36
|
+
token kind of its own and the style table had no case for it, so `a div b` left it
|
|
37
|
+
unstyled. `a mod b` was already covered; the two now behave alike.
|
|
38
|
+
- **Syntax highlighting did nothing.** `spelLanguage()` wrapped the stream parser in a
|
|
39
|
+
`LanguageSupport` without a highlight style, and `LanguageSupport` installs none of its
|
|
40
|
+
own, so every token was tagged and none was coloured. The style is now part of the
|
|
41
|
+
language support.
|
|
42
|
+
- **Accepting a completion typed a placeholder into the document.** The engine describes
|
|
43
|
+
insertions as snippets (`T($1)`, `between {$1, $2}`, `$1 ? $2 : $3`), which were passed
|
|
44
|
+
to CodeMirror's `apply` as plain strings. They are translated to CodeMirror's field
|
|
45
|
+
syntax and applied as snippets, so the placeholders became tab-through fields.
|
|
46
|
+
- **A completion replaced only the ASCII tail of a reference.** The prefix was matched with
|
|
47
|
+
`\w`, so completing `#order.amo` inserted after `amo` instead of replacing the
|
|
48
|
+
reference, and a field named in Chinese was not treated as part of the token at all.
|
|
49
|
+
- **The completion list stayed open where no token continues.** `validFor` was `() => true`,
|
|
50
|
+
which claims every position — including the space that ends a name.
|
|
51
|
+
- **Assigning `value` did not reach the document.** `setValue()` dispatched, but
|
|
52
|
+
`el.value = …` and the `value` attribute only updated the element's state, so a
|
|
53
|
+
declarative binding showed the previous expression.
|
|
54
|
+
- **The highlighter's token cache was shared by every document.** The tokenizer, token list
|
|
55
|
+
and cursor lived in a closure rather than in the state CodeMirror passes per document, so
|
|
56
|
+
two parses interleaved and each replaced the other's tokens.
|
|
57
|
+
- **Two nested textboxes for one editable region.** The wrapper carried `role="textbox"`
|
|
58
|
+
and `tabindex="0"` while CodeMirror's content already carries `role="textbox"`; the label
|
|
59
|
+
and disabled state now live on the content element, and the wrapper is no longer a
|
|
60
|
+
separate tab stop.
|
|
61
|
+
- **Gutter text was below the contrast floor.** `#9ca3af` on `#f9fafb` measured 2.43:1;
|
|
62
|
+
the default is now `#5b6472`, which measures 5.72:1.
|
|
63
|
+
- **A diagnostic outside the document would have taken out every mark.** Ranges from the
|
|
64
|
+
engine are clamped to the document, which CodeMirror requires.
|
|
65
|
+
- Test fixtures that did not match the interfaces they claimed: `variables` was written as a
|
|
66
|
+
list, `MethodSchema`/`TypeSchema`/`FunctionSchema` were given a `type` field they do not
|
|
67
|
+
have, and a method key of `toString()` produced the label `toString()()`.
|
|
68
|
+
|
|
69
|
+
## [1.1.3] — 2026-07-20
|
|
70
|
+
### Changed
|
|
71
|
+
- Unified badge style with CI/Docs/Coverage/License/TypeScript/Node.js badges
|
|
72
|
+
- Added GitHub Pages deployment job to CI (TypeDoc + coverage)
|
|
73
|
+
- Added `typedoc.json` for API docs generation
|
|
74
|
+
- Added SEO-optimized GitHub Pages landing page
|
|
75
|
+
- Bumped `spel-ts` to `^1.2.2`, `nl2spel` to `^1.3.0`
|
|
76
|
+
|
|
77
|
+
## [1.1.2] — 2026-07-19
|
|
7
78
|
### Changed
|
|
8
|
-
-
|
|
9
|
-
- Add `.prettierrc` configuration for consistent formatting
|
|
10
|
-
- Tighten `tsconfig.json` strictness checks
|
|
79
|
+
- Updated `nl2spel` dependency to `^1.2.2`
|
|
11
80
|
|
|
81
|
+
## [1.1.1] — 2026-07-19
|
|
82
|
+
### Fixed
|
|
83
|
+
- Removed arbitrary depth limit
|
|
84
|
+
|
|
85
|
+
## [1.1.0] — 2026-07-18
|
|
86
|
+
### Added
|
|
87
|
+
- Recursive context schema extraction
|
|
88
|
+
- NL2SpEL integration documentation and interactive demo page
|
|
89
|
+
|
|
90
|
+
## [1.0.0] — 2026-07-17
|
|
12
91
|
### Added
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
92
|
+
- 100% language service coverage (completion, diagnostics, hover, formatting)
|
|
93
|
+
- OIDC npm provenance publishing
|
|
94
|
+
- Playwright browser tests + nl2spel integration tests
|
|
95
|
+
- `getEditorView()` method for direct CodeMirror access
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
- `strictFunctionTypes` enabled
|
|
99
|
+
- Coverage thresholds raised to 85/80/85/85
|
|
100
|
+
- Vitest upgraded to v3.x
|
|
101
|
+
- CI: fixed browser-tests job (ESM needs bundler)
|
|
102
|
+
|
|
103
|
+
## [0.1.1] — 2026-07-14
|
|
104
|
+
### Changed
|
|
105
|
+
- Code quality: unused imports, config consistency, type safety
|
|
106
|
+
- CI: explicit pnpm version 10.28.2
|
|
107
|
+
- Restored npm badge
|
|
16
108
|
|
|
17
|
-
|
|
109
|
+
### Fixed
|
|
110
|
+
- Package.json repo URL
|
|
111
|
+
- Cross-repo ecosystem coherence (links, consistency, badges)
|
|
18
112
|
|
|
113
|
+
## [0.1.0] — 2026-07-12
|
|
19
114
|
### Added
|
|
20
|
-
- Initial release
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- Programmatic API: `getValue()`, `setValue()`, `validate()`, `format()`, `insertSnippet()`
|
|
115
|
+
- Initial release: Web-embeddable SpEL editor Web Component
|
|
116
|
+
- CodeMirror 6 integration with syntax highlighting
|
|
117
|
+
- Auto-completion adapter (spel-ts powered)
|
|
118
|
+
- Real-time diagnostics / lint adapter
|
|
119
|
+
- Hover tooltips adapter
|
|
120
|
+
- SpEL grammar tokenizer (StreamLanguage)
|
|
121
|
+
- `validate` event with `SpelDiagnostic` detail
|
|
28
122
|
- `change` event with `value` and `isValid` detail
|
|
29
|
-
-
|
|
123
|
+
- 12 CSS custom properties for non-invasive theming
|
|
124
|
+
- Framework-agnostic Web Component (`<spel-editor>`)
|
|
125
|
+
- Programmatic API: `getValue()`, `setValue()`, `validate()`, `format()`, `insertSnippet()`, `getEditorView()`
|
|
30
126
|
|
|
31
|
-
|
|
32
|
-
[
|
|
127
|
+
---
|
|
128
|
+
*Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).*
|
package/README.md
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
# @agentix-e/spel-editor
|
|
2
2
|
|
|
3
3
|
> Web-embeddable Spring Expression Language (SpEL) editor.
|
|
4
|
-
> Based on CodeMirror 6 + spel-ts
|
|
4
|
+
> Based on CodeMirror 6 + spel-ts v2.0.0. Framework-agnostic Web Component.
|
|
5
5
|
|
|
6
|
-
[](https://github.com/AgentiX-E/spel-editor/actions/workflows/ci.yml)
|
|
7
|
+
[](https://www.npmjs.com/package/@agentix-e/spel-editor)
|
|
8
|
+
[](https://AgentiX-E.github.io/spel-editor/api/)
|
|
9
|
+
[](https://AgentiX-E.github.io/spel-editor/coverage/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://www.typescriptlang.org/)
|
|
12
|
+
[](https://nodejs.org/)
|
|
13
|
+
|
|
14
|
+
## What is @agentix-e/spel-editor?
|
|
15
|
+
|
|
16
|
+
`@agentix-e/spel-editor` is a **drop-in Web Component** that adds a full-featured SpEL expression editor to any web application. Built on CodeMirror 6 and powered by `@agentix-e/spel-ts` for real-time syntax validation, it provides syntax highlighting, auto-completion, diagnostics, and hover tooltips — with zero framework lock-in. Optionally integrates with `@agentix-e/nl2spel` for natural-language-to-SpEL generation.
|
|
17
|
+
|
|
18
|
+
### When should I use it?
|
|
19
|
+
|
|
20
|
+
- You're building a **rules engine UI** that users edit SpEL expressions in
|
|
21
|
+
- You need a **code editor for expression languages** (not general-purpose code)
|
|
22
|
+
- You want **natural language input** converted to valid SpEL (via optional NL2SpEL integration)
|
|
23
|
+
- You need a **framework-agnostic** component that works with React, Vue, Angular, Svelte, or plain HTML
|
|
7
24
|
|
|
8
25
|
## Quick Start
|
|
9
26
|
|
|
@@ -30,15 +47,18 @@ npm install @agentix-e/spel-editor
|
|
|
30
47
|
|
|
31
48
|
## Features
|
|
32
49
|
|
|
33
|
-
- **Syntax highlighting** —
|
|
34
|
-
- **Auto-completion** — Keyword, operator, variable, and context-aware suggestions
|
|
50
|
+
- **Syntax highlighting** — Token-level colouring powered by the spel-ts Tokenizer, with every token class themable through a CSS custom property
|
|
51
|
+
- **Auto-completion** — Keyword, operator, variable, and context-aware suggestions, inserted as CodeMirror snippets with tab-through fields
|
|
35
52
|
- **Real-time diagnostics** — Syntax, semantic, and context validation
|
|
36
53
|
- **Hover tooltips** — Node type information on hover
|
|
37
|
-
- **Non-invasive theming** —
|
|
54
|
+
- **Non-invasive theming** — 22 CSS custom properties for complete visual control
|
|
55
|
+
- **Declarative-friendly** — Assigning `value` (property or attribute) updates the visible document
|
|
38
56
|
- **Framework agnostic** — Works with React, Vue, Angular, Svelte, or plain HTML
|
|
39
57
|
|
|
40
58
|
## API Reference
|
|
41
59
|
|
|
60
|
+
> 📚 Full TypeDoc API documentation: [AgentiX-E.github.io/spel-editor/api/](https://AgentiX-E.github.io/spel-editor/api/)
|
|
61
|
+
|
|
42
62
|
### Properties (Attributes)
|
|
43
63
|
|
|
44
64
|
| Property | Attribute | Type | Default | Description |
|
|
@@ -80,11 +100,27 @@ npm install @agentix-e/spel-editor
|
|
|
80
100
|
| `--spel-line-highlight` | `#f3f4f6` | Active line highlight color |
|
|
81
101
|
| `--spel-selection-bg` | `#bfdbfe` | Selection background color |
|
|
82
102
|
| `--spel-gutter-bg` | `#f9fafb` | Gutter background |
|
|
83
|
-
| `--spel-gutter-fg` | `#
|
|
103
|
+
| `--spel-gutter-fg` | `#5b6472` | Gutter foreground (line numbers). 5.72:1 on the gutter background |
|
|
84
104
|
| `--spel-border-width` | `1px` | Editor border width |
|
|
85
105
|
| `--spel-border-color` | `#d0d5dd` | Editor border color |
|
|
86
106
|
| `--spel-border-radius` | `6px` | Editor border radius |
|
|
87
107
|
|
|
108
|
+
Every colour a token can be painted in is a custom property too, so the palette can be
|
|
109
|
+
replaced without a rebuild:
|
|
110
|
+
|
|
111
|
+
| Property | Default | Token |
|
|
112
|
+
|----------|---------|-------|
|
|
113
|
+
| `--spel-token-keyword` | `#7c3aed` | `null`, `true`, `matches`, `between`, `instanceof` |
|
|
114
|
+
| `--spel-token-bool` | `#b45309` | Boolean literals |
|
|
115
|
+
| `--spel-token-number` | `#0f766e` | Integer, long, real and hex literals |
|
|
116
|
+
| `--spel-token-string` | `#15803d` | String literals |
|
|
117
|
+
| `--spel-token-variable` | `#1d4ed8` | `#variable` |
|
|
118
|
+
| `--spel-token-property` | `#0369a1` | `.property` and `?.property` |
|
|
119
|
+
| `--spel-token-operator` | `#475569` | Arithmetic, comparison and logical operators |
|
|
120
|
+
| `--spel-token-type` | `#9333ea` | `@bean`, `&@factory`, `T` |
|
|
121
|
+
| `--spel-token-punctuation` | `#64748b` | Brackets, commas, dots |
|
|
122
|
+
| `--spel-token-operator-keyword` | `#7c3aed` | Selection and projection (`.?[ ]`, `.![ ]`, `.^[ ]`, `.*[ ]`) |
|
|
123
|
+
|
|
88
124
|
## NL Integration — Natural Language → SpEL
|
|
89
125
|
|
|
90
126
|
`@agentix-e/spel-editor` accepts `@agentix-e/nl2spel` as an **optional peer dependency**.
|
|
@@ -134,11 +170,17 @@ npm install @agentix-e/nl2spel-openai
|
|
|
134
170
|
const editor = document.querySelector('#editor');
|
|
135
171
|
const engine = new NL2SpelEngine();
|
|
136
172
|
|
|
137
|
-
//
|
|
173
|
+
// Route provider calls through your own server, which holds the API key.
|
|
174
|
+
// A key placed here would be compiled into a public bundle — see
|
|
175
|
+
// docs/integration.md §5 for the proxy contract and the browser-local alternative.
|
|
138
176
|
engine.registerProvider(
|
|
139
177
|
new OpenAICompatibleProvider({
|
|
140
|
-
|
|
141
|
-
|
|
178
|
+
custom: {
|
|
179
|
+
name: 'spel-proxy',
|
|
180
|
+
baseURL: '/api/spel-llm',
|
|
181
|
+
apiKey: 'not-a-secret',
|
|
182
|
+
model: 'deepseek-chat',
|
|
183
|
+
},
|
|
142
184
|
})
|
|
143
185
|
);
|
|
144
186
|
|
|
@@ -224,6 +266,20 @@ npm install @agentix-e/nl2spel-webllm @mlc-ai/web-llm
|
|
|
224
266
|
| **DeepSeek / OpenAI** | `+ nl2spel-openai` | ✅ Required | ✅ Yes | `npm install` + API key |
|
|
225
267
|
| **Browser-local LLM** | `+ nl2spel-webllm` | ❌ None | ❌ No | 1.5 GB model download |
|
|
226
268
|
|
|
269
|
+
## FAQ
|
|
270
|
+
|
|
271
|
+
### Which frameworks does it support?
|
|
272
|
+
All of them. `@agentix-e/spel-editor` is a native Web Component (`<spel-editor>`) that works with React, Vue, Angular, Svelte, or plain HTML with no framework-specific wrappers needed.
|
|
273
|
+
|
|
274
|
+
### Do I need to install spel-ts separately?
|
|
275
|
+
No. `@agentix-e/spel-ts` is a direct dependency and is bundled automatically. NL2SpEL integration is an optional peer dependency — install it only if you need natural language → SpEL generation.
|
|
276
|
+
|
|
277
|
+
### Can I customize the appearance?
|
|
278
|
+
Yes. 12 CSS custom properties (`--spel-bg`, `--spel-font-family`, `--spel-border-color`, etc.) give you complete visual control without forking or CSS hacks.
|
|
279
|
+
|
|
280
|
+
### How does the NL integration work?
|
|
281
|
+
Three modes: **Pattern matching** (offline, zero deps), **LLM API** (DeepSeek/OpenAI, requires API key), and **browser-local LLM** (WebGPU + Gemma, ~1.5 GB model download). Choose based on your latency, privacy, and cost requirements.
|
|
282
|
+
|
|
227
283
|
## License
|
|
228
284
|
|
|
229
285
|
MIT © AgentiX-E
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { closeBrackets, autocompletion } from "@codemirror/autocomplete";
|
|
|
27
27
|
import { linter } from "@codemirror/lint";
|
|
28
28
|
|
|
29
29
|
// src/cm6/spel-language.ts
|
|
30
|
-
import { LanguageSupport } from "@codemirror/language";
|
|
30
|
+
import { LanguageSupport, syntaxHighlighting } from "@codemirror/language";
|
|
31
31
|
|
|
32
32
|
// src/cm6/spel-grammar.ts
|
|
33
33
|
import { StreamLanguage } from "@codemirror/language";
|
|
@@ -42,6 +42,7 @@ function tokenKindToStyle(kind) {
|
|
|
42
42
|
case TokenKind.MATCHES:
|
|
43
43
|
case TokenKind.BETWEEN:
|
|
44
44
|
case TokenKind.INSTANCEOF:
|
|
45
|
+
case TokenKind.DIV:
|
|
45
46
|
case TokenKind.MOD:
|
|
46
47
|
case TokenKind.NEW:
|
|
47
48
|
return "keyword";
|
|
@@ -112,103 +113,140 @@ function tokenKindToStyle(kind) {
|
|
|
112
113
|
return "";
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
var PARSER_RESOLVED_WORDS = /* @__PURE__ */ new Map([
|
|
117
|
+
["true", "bool"],
|
|
118
|
+
["false", "bool"],
|
|
119
|
+
["null", "keyword"],
|
|
120
|
+
["and", "operator"],
|
|
121
|
+
["or", "operator"],
|
|
122
|
+
["matches", "keyword"],
|
|
123
|
+
["between", "keyword"],
|
|
124
|
+
["instanceof", "keyword"],
|
|
125
|
+
["new", "keyword"]
|
|
126
|
+
]);
|
|
115
127
|
function createSpelStreamParser() {
|
|
116
128
|
return StreamLanguage.define(createTokenParser());
|
|
117
129
|
}
|
|
130
|
+
function spansFor(line) {
|
|
131
|
+
let tokens;
|
|
132
|
+
try {
|
|
133
|
+
tokens = new SpelTokenizer(line).tokenize();
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const spans = [];
|
|
138
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
139
|
+
const token = tokens[index];
|
|
140
|
+
if (token.kind === TokenKind.EOF) break;
|
|
141
|
+
const style = styleForToken(line, tokens, index);
|
|
142
|
+
if (style === "") continue;
|
|
143
|
+
spans.push({ from: token.startPos, to: token.endPos, style });
|
|
144
|
+
}
|
|
145
|
+
return spans;
|
|
146
|
+
}
|
|
147
|
+
function styleForToken(line, tokens, index) {
|
|
148
|
+
const token = tokens[index];
|
|
149
|
+
if (token.kind === TokenKind.IDENTIFIER) {
|
|
150
|
+
const previous = index > 0 ? tokens[index - 1] : void 0;
|
|
151
|
+
if (previous?.kind === TokenKind.HASH) return "variableName";
|
|
152
|
+
if (previous?.kind === TokenKind.DOT || previous?.kind === TokenKind.SAFE_NAV) {
|
|
153
|
+
return "propertyName";
|
|
154
|
+
}
|
|
155
|
+
const word = line.slice(token.startPos, token.endPos).toLowerCase();
|
|
156
|
+
return PARSER_RESOLVED_WORDS.get(word) ?? "variableName";
|
|
157
|
+
}
|
|
158
|
+
return tokenKindToStyle(token.kind);
|
|
159
|
+
}
|
|
118
160
|
function createTokenParser() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
_tokenizer = new SpelTokenizer(stream.string);
|
|
126
|
-
let rawTokens;
|
|
127
|
-
try {
|
|
128
|
-
rawTokens = _tokenizer.tokenize();
|
|
129
|
-
} catch {
|
|
130
|
-
_tokens = [];
|
|
131
|
-
_tokenIndex = 0;
|
|
132
|
-
stream.skipToEnd();
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
_tokens = [];
|
|
136
|
-
_tokenIndex = 0;
|
|
137
|
-
for (let i = 0; i < rawTokens.length - 1; i++) {
|
|
138
|
-
const tok = rawTokens[i];
|
|
139
|
-
const style = tokenKindToStyle(tok.kind);
|
|
140
|
-
if (tok.kind === TokenKind.IDENTIFIER) {
|
|
141
|
-
const prevTok = i > 0 ? rawTokens[i - 1] : null;
|
|
142
|
-
if (prevTok?.kind === TokenKind.HASH) {
|
|
143
|
-
_tokens.push({
|
|
144
|
-
from: tok.startPos,
|
|
145
|
-
to: tok.endPos,
|
|
146
|
-
style: "variableName"
|
|
147
|
-
});
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (prevTok?.kind === TokenKind.DOT || prevTok?.kind === TokenKind.SAFE_NAV) {
|
|
151
|
-
_tokens.push({
|
|
152
|
-
from: tok.startPos,
|
|
153
|
-
to: tok.endPos,
|
|
154
|
-
style: "propertyName"
|
|
155
|
-
});
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
_tokens.push({ from: tok.startPos, to: tok.endPos, style });
|
|
160
|
-
}
|
|
161
|
+
const startState = () => ({ line: null, spans: [], index: 0 });
|
|
162
|
+
const token = (stream, state) => {
|
|
163
|
+
if (state.line !== stream.string || stream.pos === 0) {
|
|
164
|
+
state.line = stream.string;
|
|
165
|
+
state.spans = spansFor(stream.string);
|
|
166
|
+
state.index = 0;
|
|
161
167
|
}
|
|
162
|
-
while (
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
168
|
+
while (state.index < state.spans.length) {
|
|
169
|
+
const span = state.spans[state.index];
|
|
170
|
+
state.index += 1;
|
|
171
|
+
if (span.to > stream.pos) {
|
|
172
|
+
stream.pos = span.to;
|
|
173
|
+
return span.style;
|
|
168
174
|
}
|
|
169
|
-
_tokenIndex++;
|
|
170
175
|
}
|
|
171
176
|
stream.skipToEnd();
|
|
172
|
-
_tokenIndex = 0;
|
|
173
177
|
return null;
|
|
174
178
|
};
|
|
175
179
|
return { startState, token };
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// src/cm6/spel-highlight.ts
|
|
183
|
+
import { HighlightStyle } from "@codemirror/language";
|
|
184
|
+
import { tags } from "@lezer/highlight";
|
|
185
|
+
var spelHighlightStyle = HighlightStyle.define([
|
|
186
|
+
{ tag: tags.keyword, color: "var(--spel-token-keyword, #7c3aed)" },
|
|
187
|
+
{ tag: tags.bool, color: "var(--spel-token-bool, #b45309)" },
|
|
188
|
+
{ tag: tags.number, color: "var(--spel-token-number, #0f766e)" },
|
|
189
|
+
{ tag: tags.string, color: "var(--spel-token-string, #15803d)" },
|
|
190
|
+
{ tag: tags.variableName, color: "var(--spel-token-variable, #1d4ed8)" },
|
|
191
|
+
{ tag: tags.propertyName, color: "var(--spel-token-property, #0369a1)" },
|
|
192
|
+
{ tag: tags.operator, color: "var(--spel-token-operator, #475569)" },
|
|
193
|
+
{ tag: tags.typeName, color: "var(--spel-token-type, #9333ea)" },
|
|
194
|
+
{ tag: tags.punctuation, color: "var(--spel-token-punctuation, #64748b)" },
|
|
195
|
+
{
|
|
196
|
+
tag: tags.operatorKeyword,
|
|
197
|
+
color: "var(--spel-token-operator-keyword, #7c3aed)",
|
|
198
|
+
fontStyle: "italic"
|
|
199
|
+
}
|
|
200
|
+
]);
|
|
201
|
+
|
|
178
202
|
// src/cm6/spel-language.ts
|
|
179
203
|
function spelLanguage() {
|
|
180
|
-
return new LanguageSupport(createSpelStreamParser());
|
|
204
|
+
return new LanguageSupport(createSpelStreamParser(), [syntaxHighlighting(spelHighlightStyle)]);
|
|
181
205
|
}
|
|
182
206
|
|
|
183
207
|
// src/cm6/completion-source.ts
|
|
184
|
-
import
|
|
208
|
+
import {
|
|
209
|
+
snippetCompletion
|
|
210
|
+
} from "@codemirror/autocomplete";
|
|
185
211
|
import { SpelCompletionEngine } from "@agentix-e/spel-ts";
|
|
212
|
+
|
|
213
|
+
// src/cm6/snippet.ts
|
|
214
|
+
function toCm6Snippet(template) {
|
|
215
|
+
return template.replace(/\$(\d+)/g, (_match, digits) => `\${${digits}}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/cm6/completion-source.ts
|
|
219
|
+
var NAME_PREFIX = /[\p{L}\p{N}_$#@.]*$/u;
|
|
220
|
+
var NAME_VALID_FOR = /^[\p{L}\p{N}_$#@.]*$/u;
|
|
186
221
|
function spelCompletion(getContextSchema) {
|
|
187
222
|
return (context) => {
|
|
188
223
|
const expression = context.state.sliceDoc();
|
|
189
224
|
const position = context.pos;
|
|
190
225
|
const schema = getContextSchema?.() ?? void 0;
|
|
191
226
|
const items = SpelCompletionEngine.getCompletions(expression, position, schema);
|
|
192
|
-
const validFor = /\w*/;
|
|
193
227
|
return {
|
|
194
|
-
|
|
228
|
+
// `matchBefore` answers null when the pattern does not match at the cursor, so the
|
|
229
|
+
// fallback stays even though NAME_PREFIX, which may match empty, always matches
|
|
230
|
+
// today. It guards CodeMirror's contract rather than a case this adapter can reach,
|
|
231
|
+
// and a future narrowing of the pattern would make it live.
|
|
232
|
+
from: context.matchBefore(NAME_PREFIX)?.from ?? position,
|
|
195
233
|
options: items.map((item) => mapToCM6Completion(item)),
|
|
196
|
-
//
|
|
197
|
-
|
|
234
|
+
// Stated rather than omitted, so the list stays open only while the cursor is
|
|
235
|
+
// still inside a name. `() => true` claimed every position continues a token,
|
|
236
|
+
// including the space that ends one.
|
|
237
|
+
validFor: NAME_VALID_FOR
|
|
198
238
|
};
|
|
199
239
|
};
|
|
200
240
|
}
|
|
201
241
|
function mapToCM6Completion(item) {
|
|
202
|
-
|
|
203
|
-
return {
|
|
242
|
+
return snippetCompletion(toCm6Snippet(item.insertText), {
|
|
204
243
|
label: item.label,
|
|
205
|
-
type,
|
|
244
|
+
type: mapKindToCM6Type(item.kind),
|
|
206
245
|
detail: item.detail,
|
|
207
246
|
info: item.documentation,
|
|
208
|
-
apply: item.insertText,
|
|
209
247
|
// Higher priority items appear first
|
|
210
248
|
boost: item.sortPriority / 100
|
|
211
|
-
};
|
|
249
|
+
});
|
|
212
250
|
}
|
|
213
251
|
function mapKindToCM6Type(kind) {
|
|
214
252
|
switch (kind) {
|
|
@@ -243,16 +281,18 @@ function spelLint(getContextSchema) {
|
|
|
243
281
|
const schema = getContextSchema?.() ?? void 0;
|
|
244
282
|
if (expression.trim().length === 0) return [];
|
|
245
283
|
const diagnostics = SpelDiagnosticEngine.validate(expression, schema);
|
|
246
|
-
return diagnostics.map((
|
|
284
|
+
return diagnostics.map((diagnostic) => mapToCM6Diagnostic(diagnostic, view.state.doc.length));
|
|
247
285
|
};
|
|
248
286
|
}
|
|
249
|
-
function mapToCM6Diagnostic(
|
|
287
|
+
function mapToCM6Diagnostic(diagnostic, docLength) {
|
|
288
|
+
const from = Math.max(0, Math.min(diagnostic.from, docLength));
|
|
289
|
+
const to = Math.max(from, Math.min(diagnostic.to, docLength));
|
|
250
290
|
return {
|
|
251
|
-
from
|
|
252
|
-
to
|
|
253
|
-
message:
|
|
254
|
-
severity: mapSeverity(
|
|
255
|
-
source:
|
|
291
|
+
from,
|
|
292
|
+
to,
|
|
293
|
+
message: diagnostic.message,
|
|
294
|
+
severity: mapSeverity(diagnostic.severity),
|
|
295
|
+
source: diagnostic.code
|
|
256
296
|
};
|
|
257
297
|
}
|
|
258
298
|
function mapSeverity(sev) {
|
|
@@ -354,11 +394,14 @@ var EDITOR_STYLES = css`
|
|
|
354
394
|
}
|
|
355
395
|
.cm-editor .cm-gutters {
|
|
356
396
|
background: var(--spel-gutter-bg, #f9fafb);
|
|
357
|
-
|
|
397
|
+
/* 5.72:1 on the gutter background. The previous #9ca3af measured 2.43:1, under
|
|
398
|
+
the 4.5:1 WCAG AA requires for text, which made line numbers and lint markers
|
|
399
|
+
unreadable at reduced contrast sensitivity. */
|
|
400
|
+
color: var(--spel-gutter-fg, #5b6472);
|
|
358
401
|
border: none;
|
|
359
402
|
}
|
|
360
403
|
`;
|
|
361
|
-
var _editorView, _diagnosticCache, _diagnosticDebounceTimer, _SpelEditor_instances, scheduleDiagnostics_fn, runDiagnostics_fn, createEditor_fn, updateEditorState_fn, fireChange_fn;
|
|
404
|
+
var _editorView, _diagnosticCache, _diagnosticDebounceTimer, _SpelEditor_instances, reflectValue_fn, scheduleDiagnostics_fn, runDiagnostics_fn, contentAttributes_fn, createEditor_fn, updateEditorState_fn, fireChange_fn;
|
|
362
405
|
var SpelEditor = class extends LitElement {
|
|
363
406
|
constructor() {
|
|
364
407
|
super(...arguments);
|
|
@@ -377,11 +420,6 @@ var SpelEditor = class extends LitElement {
|
|
|
377
420
|
return html`
|
|
378
421
|
<div
|
|
379
422
|
class="cm-container"
|
|
380
|
-
role="textbox"
|
|
381
|
-
aria-label=${this.placeholder}
|
|
382
|
-
aria-readonly=${this.readonly ? "true" : "false"}
|
|
383
|
-
aria-disabled=${this.disabled ? "true" : "false"}
|
|
384
|
-
tabindex="0"
|
|
385
423
|
style="
|
|
386
424
|
min-height: ${this.minHeight};
|
|
387
425
|
border: var(--spel-border-width, 1px) solid var(--spel-border-color, #d0d5dd);
|
|
@@ -395,12 +433,15 @@ var SpelEditor = class extends LitElement {
|
|
|
395
433
|
__privateMethod(this, _SpelEditor_instances, createEditor_fn).call(this);
|
|
396
434
|
}
|
|
397
435
|
updated(changed) {
|
|
398
|
-
if (changed.has("disabled") || changed.has("readonly")) {
|
|
436
|
+
if (changed.has("disabled") || changed.has("readonly") || changed.has("placeholder")) {
|
|
399
437
|
__privateMethod(this, _SpelEditor_instances, updateEditorState_fn).call(this);
|
|
400
438
|
}
|
|
401
439
|
if (!__privateGet(this, _editorView) && this.containerEl) {
|
|
402
440
|
__privateMethod(this, _SpelEditor_instances, createEditor_fn).call(this);
|
|
403
441
|
}
|
|
442
|
+
if (changed.has("value")) {
|
|
443
|
+
__privateMethod(this, _SpelEditor_instances, reflectValue_fn).call(this);
|
|
444
|
+
}
|
|
404
445
|
if (changed.has("contextSchema")) {
|
|
405
446
|
__privateMethod(this, _SpelEditor_instances, scheduleDiagnostics_fn).call(this);
|
|
406
447
|
}
|
|
@@ -493,6 +534,29 @@ _editorView = new WeakMap();
|
|
|
493
534
|
_diagnosticCache = new WeakMap();
|
|
494
535
|
_diagnosticDebounceTimer = new WeakMap();
|
|
495
536
|
_SpelEditor_instances = new WeakSet();
|
|
537
|
+
/**
|
|
538
|
+
* Push the current `value` into the editor, if the two have diverged.
|
|
539
|
+
*
|
|
540
|
+
* Typing sets `value` from the document, so this is a no-op for the common case and
|
|
541
|
+
* only acts on a value assigned from outside — which is how every declarative
|
|
542
|
+
* consumer sets it. Without this, `el.value = 'a > 1'` updated the element's state
|
|
543
|
+
* while the visible document kept showing the previous expression.
|
|
544
|
+
*
|
|
545
|
+
* The guard is what keeps it from looping: the change listener writes the document
|
|
546
|
+
* text back into `value`, and a dispatch here is skipped whenever they already
|
|
547
|
+
* agree, so the cursor is never reset mid-edit.
|
|
548
|
+
*/
|
|
549
|
+
reflectValue_fn = function() {
|
|
550
|
+
const view = __privateGet(this, _editorView);
|
|
551
|
+
if (!view) return;
|
|
552
|
+
const current = view.state.sliceDoc();
|
|
553
|
+
if (current === this.value) return;
|
|
554
|
+
view.dispatch({
|
|
555
|
+
changes: { from: 0, to: view.state.doc.length, insert: this.value },
|
|
556
|
+
selection: { anchor: this.value.length }
|
|
557
|
+
});
|
|
558
|
+
__privateMethod(this, _SpelEditor_instances, scheduleDiagnostics_fn).call(this);
|
|
559
|
+
};
|
|
496
560
|
/**
|
|
497
561
|
* Schedule a debounced diagnostic run (300ms).
|
|
498
562
|
* Avoids redundant computation on rapid typing.
|
|
@@ -526,6 +590,21 @@ runDiagnostics_fn = function() {
|
|
|
526
590
|
})
|
|
527
591
|
);
|
|
528
592
|
};
|
|
593
|
+
/**
|
|
594
|
+
* Attributes for the editable region.
|
|
595
|
+
*
|
|
596
|
+
* CodeMirror already renders `.cm-content` with `role="textbox"`,
|
|
597
|
+
* `aria-multiline="true"` and — when the state is read-only — `aria-readonly`, so
|
|
598
|
+
* that element is the component's one textbox. The label and the disabled state
|
|
599
|
+
* belong on it as well: declaring them on the wrapper as well produced two nested
|
|
600
|
+
* textboxes and two tab stops for a single editable region.
|
|
601
|
+
*/
|
|
602
|
+
contentAttributes_fn = function() {
|
|
603
|
+
return {
|
|
604
|
+
"aria-label": this.placeholder,
|
|
605
|
+
...this.disabled ? { "aria-disabled": "true" } : {}
|
|
606
|
+
};
|
|
607
|
+
};
|
|
529
608
|
createEditor_fn = function() {
|
|
530
609
|
const extensions = [
|
|
531
610
|
spelLanguage(),
|
|
@@ -536,6 +615,7 @@ createEditor_fn = function() {
|
|
|
536
615
|
closeBrackets(),
|
|
537
616
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
538
617
|
cmPlaceholder(this.placeholder),
|
|
618
|
+
EditorView.contentAttributes.of(__privateMethod(this, _SpelEditor_instances, contentAttributes_fn).call(this)),
|
|
539
619
|
EditorView.updateListener.of((update) => {
|
|
540
620
|
if (update.docChanged) {
|
|
541
621
|
this.value = update.state.sliceDoc();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentix-e/spel-editor",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Web-embeddable Spring Expression Language (SpEL) editor — CodeMirror 6 + spel-ts powered",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Web-embeddable Spring Expression Language (SpEL) editor — CodeMirror 6 + spel-ts powered, framework-agnostic Web Component with NL2SpEL integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:browser:ui": "playwright test --ui",
|
|
25
25
|
"test:all": "pnpm test && pnpm test:browser",
|
|
26
26
|
"test:watch": "vitest",
|
|
27
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
28
28
|
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
|
29
29
|
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
|
|
30
30
|
"clean": "rm -rf dist",
|
|
@@ -34,13 +34,14 @@
|
|
|
34
34
|
"node": ">=18"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@agentix-e/spel-ts": "^
|
|
37
|
+
"@agentix-e/spel-ts": "^2.0.0",
|
|
38
38
|
"@codemirror/autocomplete": "^6.0.0",
|
|
39
39
|
"@codemirror/commands": "^6.0.0",
|
|
40
40
|
"@codemirror/language": "^6.0.0",
|
|
41
41
|
"@codemirror/lint": "^6.0.0",
|
|
42
42
|
"@codemirror/state": "^6.0.0",
|
|
43
43
|
"@codemirror/view": "^6.0.0",
|
|
44
|
+
"@lezer/highlight": "^1.2.3",
|
|
44
45
|
"lit": "^3.0.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@agentix-e/nl2spel": "^1.
|
|
56
|
-
"@agentix-e/nl2spel-openai": "^1.
|
|
56
|
+
"@agentix-e/nl2spel": "^1.3.0",
|
|
57
|
+
"@agentix-e/nl2spel-openai": "^1.3.0",
|
|
57
58
|
"@playwright/test": "^1.61.1",
|
|
58
59
|
"@vitest/coverage-v8": "^3.0.0",
|
|
59
60
|
"jsdom": "^29.1.1",
|
|
@@ -65,12 +66,16 @@
|
|
|
65
66
|
},
|
|
66
67
|
"keywords": [
|
|
67
68
|
"spel",
|
|
68
|
-
"editor",
|
|
69
|
+
"spel-editor",
|
|
69
70
|
"code-editor",
|
|
70
|
-
"expression-
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
71
|
+
"expression-editor",
|
|
72
|
+
"codemirror6",
|
|
73
|
+
"web-component",
|
|
74
|
+
"spring-expression-language",
|
|
75
|
+
"typescript",
|
|
76
|
+
"syntax-highlighting",
|
|
77
|
+
"autocomplete",
|
|
78
|
+
"nl2spel"
|
|
74
79
|
],
|
|
75
80
|
"license": "MIT",
|
|
76
81
|
"author": "AgentiX-E",
|
|
@@ -83,7 +88,9 @@
|
|
|
83
88
|
},
|
|
84
89
|
"homepage": "https://github.com/AgentiX-E/spel-editor#readme",
|
|
85
90
|
"pnpm": {
|
|
86
|
-
"onlyBuiltDependencies": [
|
|
91
|
+
"onlyBuiltDependencies": [
|
|
92
|
+
"esbuild"
|
|
93
|
+
]
|
|
87
94
|
},
|
|
88
95
|
"publishConfig": {
|
|
89
96
|
"access": "public"
|