Compare commits
5
Commits
fd8d39d07a
...
09dcfede46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09dcfede46 | ||
|
|
c3d5d3541b | ||
|
|
d963472729 | ||
|
|
1c12fd1df6 | ||
|
|
f783617393 |
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: evolve-eftl-linter
|
||||
description: Evolve the EFTL VS Code linter and language server while keeping tokenizer, parser, LSP diagnostics, navigation, TextMate grammar, editor configuration, snippets, examples, and documentation consistent. Use for EFTL feature implementation, bug fixes, refactors, new diagnostics, language-server capabilities, or support for a newly documented EFTL construct.
|
||||
---
|
||||
|
||||
# Evolve EFTL Linter
|
||||
|
||||
Implement the smallest coherent change across all affected language-support layers. Read [references/project-architecture.md](references/project-architecture.md) before a non-trivial change.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the worktree and preserve unrelated user changes.
|
||||
2. Convert the request into a language rule card. Use `$interpret-eftl-language` for new or disputed EFTL behavior.
|
||||
3. Search all support surfaces for the construct and record the required files.
|
||||
4. Add or update focused tests and fixtures before broad refactoring. If test infrastructure is absent, introduce the smallest repository-level harness that exercises tokenizer and parser without VS Code.
|
||||
5. Change recognition in `src/tokenizer.ts`. Preserve exact source spans and make a deliberate decision about case-insensitivity and malformed delimiters.
|
||||
6. Change structure in `src/parser.ts`. Validate grammar relationships and ordering, not only matching close tags. Recover after an error so one defect does not flood the document with misleading diagnostics.
|
||||
7. Change semantic analysis and LSP behavior in `src/server.ts`. Keep internal positions unambiguous and convert to zero-based LSP ranges only at the boundary.
|
||||
8. Synchronize editor assets when user-visible syntax changes: TextMate grammar, language configuration, snippets, examples, and README.
|
||||
9. Run `$verify-eftl-linter`; inspect every failure and any new diagnostic range manually.
|
||||
|
||||
## Design constraints
|
||||
|
||||
- Do not encode the same tag metadata independently in several new switch statements. Prefer a shared declarative definition when a change spans tokenization, parsing, and validation.
|
||||
- Keep parsing separate from LSP transport so core behavior is testable without starting a language client.
|
||||
- Do not claim full expression validation until the grammar supports precedence, literals, operators, and recovery defined by the runtime language.
|
||||
- Avoid static errors for values known only at runtime. Use warnings only when they are actionable and low-noise.
|
||||
- Cap published diagnostics with `eftl.maxNumberOfProblems` and make truncation deterministic when implementing server work.
|
||||
- Preserve valid user text and ElixForms TAG payloads; brackets inside comments, expressions, quoted attributes, or TAG bodies need context-aware handling.
|
||||
- Guarantee scanner progress: every tokenization step must consume input or terminate with an error. Include unknown and incomplete `[` constructs in regression tests.
|
||||
- Treat diagnostic ranges as first-class behavior. Test single-line and multiline tokens, CRLF and LF, and non-ASCII text before the error.
|
||||
|
||||
## Change-surface checklist
|
||||
|
||||
| Concern | Primary files |
|
||||
| --- | --- |
|
||||
| Token kinds, scanning, locations | `src/tokenizer.ts` |
|
||||
| Nesting, ordering, recovery, AST | `src/parser.ts` |
|
||||
| Diagnostics, config, symbols, definitions | `src/server.ts` |
|
||||
| Client activation | `src/extension.ts`, `package.json` |
|
||||
| Highlighting | `syntaxes/eftl.tmLanguage.json` |
|
||||
| Brackets, comments, folding | `language-configuration.json` |
|
||||
| Authoring examples | `snippets/eftl.json`, `examples/`, `README.md` |
|
||||
| Build and tests | `package.json`, `tsconfig.json`, test files |
|
||||
|
||||
Update only surfaces affected by the rule card, but explicitly state why an apparently related surface is unchanged.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Evolve EFTL Linter"
|
||||
short_description: "Implement coordinated EFTL linter changes"
|
||||
default_prompt: "Use $evolve-eftl-linter to implement this EFTL language-support change safely."
|
||||
@@ -0,0 +1,67 @@
|
||||
# Project architecture and evolution notes
|
||||
|
||||
## Runtime flow
|
||||
|
||||
```text
|
||||
.eftl document
|
||||
-> VS Code language client (`src/extension.ts`)
|
||||
-> LSP server (`src/server.ts`)
|
||||
-> tokenizer (`src/tokenizer.ts`)
|
||||
-> stack parser (`src/parser.ts`)
|
||||
-> diagnostics and per-document variable definitions
|
||||
```
|
||||
|
||||
Separately, VS Code loads the TextMate grammar, language configuration, and snippets directly from `package.json` contributions.
|
||||
|
||||
## Current responsibilities
|
||||
|
||||
- `src/extension.ts`: starts `out/server.js` over IPC for file-backed EFTL documents.
|
||||
- `src/tokenizer.ts`: recognizes a fixed, mostly uppercase set of tags, expressions, comments, and plain text; stores one-based line and column plus a character length.
|
||||
- `src/parser.ts`: checks stack-balanced open and close token kinds. It declares an `AstNode` interface but does not currently build an AST or enforce IF/WHILE child ordering.
|
||||
- `src/server.ts`: tokenizes and parses on document changes, validates VAR `type`, collects variable declarations, publishes diagnostics, and implements same-document go-to-definition by word matching.
|
||||
- `syntaxes/eftl.tmLanguage.json`: highlights a broader syntax independently of the TypeScript parser.
|
||||
- `language-configuration.json`: defines comment, bracket, auto-close, surrounding-pair, and folding behavior.
|
||||
- `snippets/eftl.json` and `examples/sample.eftl`: provide authoring examples and useful regression inputs.
|
||||
|
||||
## Baseline gaps to re-check
|
||||
|
||||
These observations describe the repository when this skill was created and are not permanent requirements:
|
||||
|
||||
- Documented `LOG`, `FOR`, `IS_EMPTY`, and `IS_NOT_EMPTY` constructs are not tokenized or structurally parsed.
|
||||
- The specification describes case-insensitive tags; tokenization currently uses exact uppercase matches.
|
||||
- An unrecognized `[` currently makes `scanText()` return without consuming input, so lowercase, unknown, or incomplete tags can trap tokenization in an infinite loop.
|
||||
- Prefix checks such as `[VAR` need a tag-name boundary; otherwise longer unknown names can be misclassified.
|
||||
- WHILE attributes and the attribute form of TRIM are not recognized by the current exact scanners.
|
||||
- Parser validation is balance-only; it does not enforce root scope, IF branch ordering/cardinality, WHILE shape, or allowed bodies.
|
||||
- Attribute parsing uses regular expressions in the server and validates only VAR type.
|
||||
- Source positions combine one-based line/column with token string length. Multiline diagnostic end ranges and variable-definition selections require care.
|
||||
- `eftl.maxNumberOfProblems` is declared but not consumed by the server.
|
||||
- No automated test script is declared.
|
||||
- `npm run compile` passes at the baseline. `npm run lint` fails before linting because ESLint 9 cannot find a flat `eslint.config.*` file.
|
||||
|
||||
## Preferred direction
|
||||
|
||||
Evolve incrementally toward:
|
||||
|
||||
1. a source-span model based on offsets with reliable LSP conversion;
|
||||
2. a context-aware scanner that can report malformed input and recover;
|
||||
3. structured attribute parsing with ranges;
|
||||
4. an AST or equivalent parse structure that represents documented relationships;
|
||||
5. separate semantic passes for declarations, references, types, and TAG payloads;
|
||||
6. table-driven language metadata shared where doing so reduces drift;
|
||||
7. core unit tests plus a small number of LSP integration tests.
|
||||
|
||||
Do not perform this redesign wholesale for an unrelated small fix. Introduce seams that make the next supported construct easier and safer.
|
||||
|
||||
## Completion criteria for a language construct
|
||||
|
||||
A construct is complete only when applicable layers agree on:
|
||||
|
||||
- accepted spellings and delimiters;
|
||||
- attribute names and body form;
|
||||
- nesting and ordering;
|
||||
- malformed-input recovery;
|
||||
- diagnostic message, severity, and exact range;
|
||||
- highlighting and folding;
|
||||
- snippet/example syntax;
|
||||
- focused positive, negative, boundary, and regression tests.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: interpret-eftl-language
|
||||
description: Interpret the EFTL language specification and turn the repository PDF documentation into explicit lexical, syntactic, structural, attribute, and semantic rules. Use when adding or reviewing EFTL tags, expressions, nesting constraints, ElixForms TAG payloads, examples, diagnostics, or when the documentation and current TypeScript implementation disagree.
|
||||
---
|
||||
|
||||
# Interpret EFTL Language
|
||||
|
||||
Derive a small, testable language contract before changing implementation code.
|
||||
|
||||
## Establish the source of truth
|
||||
|
||||
Use the sources in this order:
|
||||
|
||||
1. Read the relevant section of `docs/elixForms_Doc_EFTL.pdf` for EFTL behavior and examples.
|
||||
2. Read `docs/elixForms_EFTLParser_syntax_rels.pdf` for parser relationships and allowed composition.
|
||||
3. Read `docs/elixForms_TAG_Sintassi.pdf` for positional `SCHEMAID` and `GETVALUEBYTAG` payloads.
|
||||
4. Use [references/eftl-language-reference.md](references/eftl-language-reference.md) as a navigation aid and concise baseline, never as a replacement for a disputed PDF passage.
|
||||
5. Treat `src/`, `syntaxes/`, `snippets/`, `examples/`, and `README.md` as the current implementation, not as normative language documentation.
|
||||
|
||||
If the PDFs contradict one another or leave a rule unclear, report the ambiguity and preserve it in tests or design notes. Do not silently infer a restrictive diagnostic.
|
||||
|
||||
## Produce a rule card
|
||||
|
||||
For every construct being changed, record:
|
||||
|
||||
- spelling and case-sensitivity;
|
||||
- block, bodyless, expression, or comment form;
|
||||
- required and optional attributes, value types, defaults, and mutual exclusions;
|
||||
- allowed body and parent/child relationships;
|
||||
- variable scope, result type, and runtime behavior relevant to static analysis;
|
||||
- valid, invalid, and boundary examples;
|
||||
- the safest diagnostic when a property cannot be proven statically.
|
||||
|
||||
Distinguish syntax errors from semantic warnings. Runtime-only facts such as external TAG values must not become false-positive syntax errors.
|
||||
|
||||
## Reconcile with the implementation
|
||||
|
||||
Search every support surface before proposing a change:
|
||||
|
||||
```powershell
|
||||
rg -n "CONSTRUCT|TokenType" src syntaxes snippets examples README.md language-configuration.json
|
||||
```
|
||||
|
||||
Compare the rule card with:
|
||||
|
||||
- `src/tokenizer.ts` for recognition, source positions, and case handling;
|
||||
- `src/parser.ts` for nesting and ordering;
|
||||
- `src/server.ts` for semantic diagnostics and symbols;
|
||||
- `syntaxes/eftl.tmLanguage.json` for highlighting;
|
||||
- `language-configuration.json` for brackets, folding, and comments;
|
||||
- `snippets/eftl.json`, `examples/`, and `README.md` for user-facing syntax.
|
||||
|
||||
Return a change-surface list and explicit acceptance cases. Hand implementation work to `$evolve-eftl-linter` and validation work to `$verify-eftl-linter` when those skills are available.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Interpret EFTL Language"
|
||||
short_description: "Interpret EFTL syntax and semantic rules"
|
||||
default_prompt: "Use $interpret-eftl-language to derive the EFTL rules needed for this change."
|
||||
@@ -0,0 +1,101 @@
|
||||
# EFTL language reference
|
||||
|
||||
This is a compact index derived from the documents currently under `docs/`. Re-check the PDFs when exact wording, diagrams, or edge behavior matters.
|
||||
|
||||
## Documentation map
|
||||
|
||||
- `elixForms_Doc_EFTL.pdf`: EFTL specification through the documented 3.1.0 changes, language behavior, attributes, control flow, and examples.
|
||||
- `elixForms_EFTLParser_syntax_rels.pdf`: generated lexer/parser relationship diagrams.
|
||||
- `elixForms_TAG_Sintassi.pdf`: positional syntax for the ElixForms `SCHEMAID` and `GETVALUEBYTAG` plugins.
|
||||
|
||||
## Common lexical and structural rules
|
||||
|
||||
- EFTL uses square-bracket tags and requires properly nested, non-overlapping elements.
|
||||
- A bodyless element ends with `/]`; a block has matching opening and closing tags.
|
||||
- Attribute values are quoted. Attribute order is not semantically significant.
|
||||
- The main specification says uppercase and lowercase tag spellings are interpreted equally and recommends lowercase style. The current implementation is largely uppercase-only.
|
||||
- Text outside `[EFTL]...[/EFTL]` is not processed. Text inside an EFTL root but outside an executable tag is emitted as user text where the grammar permits it.
|
||||
- Comments use `[!-- ... --]` and may span lines.
|
||||
- The generated relationships include user text, ignored whitespace, CDATA, headers, comments, statements, code blocks, and output blocks. Consult the diagram PDF before enforcing a new parent/child restriction.
|
||||
|
||||
## Root, directives, and context
|
||||
|
||||
| Construct | Form | Key rules |
|
||||
| --- | --- | --- |
|
||||
| EFTL | `[EFTL] ... [/EFTL]` | Root block; no documented attributes; a document needs at least one root to be processed. |
|
||||
| HEADER | `[HEADER name="..." value="..." type="..." /]` | Declares a directive. The documented directive is `trimDocument`, with boolean `true` or `false`. |
|
||||
| LOG | `[LOG] ... [/LOG]` | Writes evaluated content to the server log; no documented attributes. |
|
||||
|
||||
Predefined execution-context names include `currentDateTime`, `defaultLocale`, `currentLocale`, and `requestId`. Other values may be supplied by the calling service.
|
||||
|
||||
## Variables and value functions
|
||||
|
||||
| Construct | Form | Key rules |
|
||||
| --- | --- | --- |
|
||||
| VAR | `[VAR name="..." type="..." unique="..." ] ... [/VAR]` | `name` identifies a context variable. Documented types: `string`, `boolean`, `number`, `date`, `object`, `iterable`. `unique` defaults to `false` and is meaningful only for `iterable`. |
|
||||
| VALUE_OF | `[VALUE_OF varname="..." index="..." /]` | Emits a variable value. `index` identifies the position for an iterable and may name another variable. |
|
||||
| SIZE_OF | `[SIZE_OF varname="..." /]` | Returns iterable size, otherwise `0`. |
|
||||
| IS_EMPTY | `[IS_EMPTY varname="..." /]` | Tests missing, null, blank string, empty iterable, or empty map according to the documented rules. |
|
||||
| IS_NOT_EMPTY | `[IS_NOT_EMPTY varname="..." /]` | Logical negative of `IS_EMPTY`. |
|
||||
| CONTAINS | `[CONTAINS varname="..." value="..." /]` or `[CONTAINS varname="..."] ... [/CONTAINS]` | Supports string or iterable input. Attribute `value` and body are alternatives; `value` has priority when both exist. |
|
||||
|
||||
Do not require `type` merely because current examples commonly include it: some examples omit it. Resolve requiredness from the parser relations or runtime contract before emitting an error.
|
||||
|
||||
## Transformations
|
||||
|
||||
| Construct | Form | Key rules |
|
||||
| --- | --- | --- |
|
||||
| FORMAT | `[FORMAT varname="..." type="..." /]` or `[FORMAT type="..." pattern="..."] ... [/FORMAT]` | `varname` takes precedence over the body. Documented formatting covers number and date families; number modes include currency, integer, double, percent, and generic. |
|
||||
| SPLIT | `[SPLIT regex="..." emptyIfBlank="true|false"] ... [/SPLIT]` | Applies a valid regular expression to a string result. `emptyIfBlank` defaults to `false`. An invalid regex is a runtime parsing error and can be checked statically only for a literal. |
|
||||
| TRIM | `[TRIM varname="..." /]` or `[TRIM] ... [/TRIM]` | Trims a named variable or evaluated body. A found variable takes precedence; otherwise the body is evaluated. |
|
||||
|
||||
## Control structures
|
||||
|
||||
- IF shape: `[IF] [CONDITION] ... [/CONDITION] [THEN] ... [/THEN] { [ELSE IF] ... [/ELSE IF] } [ELSE] ... [/ELSE] [/IF]`.
|
||||
- `CONDITION` must evaluate to boolean. There may be multiple `ELSE IF` blocks and at most one final `ELSE`; both are optional.
|
||||
- WHILE shape: `[WHILE ...] [CONDITION] ... [/CONDITION] [DO] ... [/DO] [/WHILE]`.
|
||||
- The WHILE section documents an optional `threshold` with default and maximum `32766`, but also contains a sentence saying the tag has no attributes. Treat this as an explicit documentation inconsistency.
|
||||
- FOR shape: `[FOR varName="item" iterable="items"] ... [/FOR]`. The loop variable exists during the loop and is removed afterward.
|
||||
- The FOR prose depends on both `varName` and `iterable`, but the generated relationship only shows a generic attribute node. Treat formal requiredness, attribute-name casing, duplicate/unknown attributes, null or non-iterable input, and shadowing as unresolved until confirmed against the runtime grammar.
|
||||
|
||||
## Code blocks
|
||||
|
||||
| Kind | Form | Meaning |
|
||||
| --- | --- | --- |
|
||||
| Assignment | `[% target = expression; ... %]` | Mutates variables; documented statements end with `;`. |
|
||||
| Evaluation | `[% expression %]` | Evaluates and returns a result, often boolean in `CONDITION`. |
|
||||
| Output | `[%= variable %]` | Writes a context variable; a missing variable is a runtime error. |
|
||||
|
||||
The expression language examples use assignment, equality/comparison, arithmetic, boolean operators, strings, numbers, booleans, and `null`. The PDFs do not provide a complete operator-precedence grammar; avoid inventing one without a runtime grammar source.
|
||||
|
||||
## ElixForms TAG payloads
|
||||
|
||||
`SCHEMAID` is positional:
|
||||
|
||||
- position 0: plugin name `SCHEMAID`;
|
||||
- position 1: schema ID;
|
||||
- position 2: column reference;
|
||||
- position 3: `IUQOID`;
|
||||
- position 4: line terminator, with a space selecting the default;
|
||||
- position 5: column separator;
|
||||
- position 6: default value;
|
||||
- position 7: date/time formatter;
|
||||
- position 8: language (documented as currently unused).
|
||||
|
||||
Positions 0 through 4 must be present. Preserve commas for omitted intermediate optional positions.
|
||||
|
||||
`GETVALUEBYTAG` is positional:
|
||||
|
||||
- position 0: plugin name `GETVALUEBYTAG`;
|
||||
- position 1: tag name;
|
||||
- position 2: lookup type: `REQUEST`, `MODULE`, or `USER_PROFILE`;
|
||||
- position 3: `IUQOID`;
|
||||
- position 4: weight/order option;
|
||||
- position 5: column separator;
|
||||
- position 6: format string.
|
||||
|
||||
The weight list in the prose and the final syntax example differ in spelling and membership (`UPDATED_FIST`/`UPDATED_FIRST`, `UPDATE_LAST`/`UPDATED_LAST`, and `CONCAT`). Treat exact validation as ambiguous until confirmed against the TAG runtime.
|
||||
|
||||
## Known implementation delta at skill creation
|
||||
|
||||
The current TypeScript tokenizer/parser does not yet model all documented constructs. Notably absent or incomplete are `LOG`, `FOR`, `IS_EMPTY`, `IS_NOT_EMPTY`, case-insensitive spellings, structural ordering inside IF/WHILE, attribute validation beyond VAR type, and full expression parsing. Re-check source before relying on this list because it is expected to shrink as the linter evolves.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: verify-eftl-linter
|
||||
description: Verify the EFTL linter and VS Code language support with repository checks, tokenizer/parser cases, diagnostic range review, TextMate grammar checks, snippets, examples, and LSP behavior. Use after EFTL implementation changes, while diagnosing regressions, when adding tests, or before declaring a linter feature complete.
|
||||
---
|
||||
|
||||
# Verify EFTL Linter
|
||||
|
||||
Validate the core language implementation first, then the LSP and editor surfaces affected by the change. Read [references/verification-matrix.md](references/verification-matrix.md) when selecting cases.
|
||||
|
||||
## Run the baseline check
|
||||
|
||||
From the repository root, run:
|
||||
|
||||
```powershell
|
||||
node .agents/skills/verify-eftl-linter/scripts/check-repository.mjs
|
||||
```
|
||||
|
||||
The script parses contributed JSON, checks referenced files, compiles TypeScript, runs an existing test script when present, and smoke-tests the compiled tokenizer/parser. Pass `--lint` only when the repository has a working ESLint configuration.
|
||||
|
||||
Treat this script as a floor, not as feature proof.
|
||||
|
||||
## Build a focused test set
|
||||
|
||||
For each changed rule, include:
|
||||
|
||||
- the smallest valid form;
|
||||
- every optional form or bodyless/block alternative;
|
||||
- a representative nested form;
|
||||
- missing, duplicated, invalid, and out-of-order attributes or children;
|
||||
- mismatched, missing, and unexpected closing delimiters;
|
||||
- incomplete input at end of file for live-edit recovery;
|
||||
- lowercase and mixed-case spellings when the runtime is case-insensitive;
|
||||
- LF, CRLF, multiline, and non-ASCII range cases;
|
||||
- a regression case from `examples/` or the reported bug.
|
||||
|
||||
Assert token types and spans separately from parser errors. Assert diagnostic code, severity, message, and exact LSP range; do not rely only on snapshot text.
|
||||
|
||||
## Verify by layer
|
||||
|
||||
1. Run tokenizer/parser tests without VS Code.
|
||||
2. Run server-level tests for config, diagnostic caps, declaration/reference scope, and range conversion.
|
||||
3. Parse all JSON contribution files and inspect TextMate captures for the changed construct.
|
||||
4. Open a representative `.eftl` file in an Extension Development Host when highlighting, folding, snippets, activation, or navigation changed.
|
||||
5. Confirm valid examples produce no diagnostics and each invalid example produces the intended minimal set.
|
||||
|
||||
When no automated harness exists for a required layer, add one if it is stable and proportionate; otherwise document the exact manual check and result.
|
||||
|
||||
## Interpret failures
|
||||
|
||||
- A compilation failure blocks all further verification.
|
||||
- A failure caused by a known baseline issue must be reproduced on the pre-change baseline before being labeled unrelated.
|
||||
- Diagnostic cascades usually indicate parser recovery problems; fix recovery instead of weakening assertions.
|
||||
- TextMate highlighting success does not prove tokenizer/parser support, and parser success does not prove editor integration.
|
||||
- Never declare `npm run lint` successful in the current baseline: ESLint 9 has no flat configuration until the repository adds one.
|
||||
|
||||
Report commands, pass/fail counts, skipped layers, and residual risks.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Verify EFTL Linter"
|
||||
short_description: "Test EFTL diagnostics and editor support"
|
||||
default_prompt: "Use $verify-eftl-linter to validate this EFTL linter change end to end."
|
||||
@@ -0,0 +1,54 @@
|
||||
# EFTL verification matrix
|
||||
|
||||
## Core matrix
|
||||
|
||||
| Area | Positive cases | Negative and recovery cases |
|
||||
| --- | --- | --- |
|
||||
| Roots | one and multiple EFTL fragments with surrounding user text | close without open, missing close, nested roots if forbidden by confirmed grammar |
|
||||
| Blocks | correctly nested VAR, IF, WHILE, functions, expressions, comments | crossing closes, incomplete close, unexpected close, unfinished or unknown `[` construct; assert termination |
|
||||
| Attributes | documented required and optional attributes in varied order | missing required, duplicate, unknown, unquoted, invalid enum/boolean/number |
|
||||
| IF | condition + then, repeated else-if, optional final else | missing condition/then, else before then, duplicate else, branch after else |
|
||||
| WHILE | condition + do, confirmed threshold boundaries | missing child, reversed order, invalid threshold, incomplete body |
|
||||
| FOR | iterable variable and scoped loop variable | missing attributes, unknown iterable when statically knowable, reference after scope |
|
||||
| Expressions | assignment, evaluation, output, supported literals/operators | unterminated block, malformed operator, missing semicolon where required |
|
||||
| Functions | bodyless and block variants, precedence rules | mutually exclusive inputs, wrong body shape, invalid literal regex |
|
||||
| TAG payload | minimal and full positional forms | missing mandatory position, invalid literal enum, preserved empty positions |
|
||||
| Variables | declaration, reference, shadowing policy, iterable index | invalid type, duplicate declaration policy, undefined reference, invalid index type |
|
||||
|
||||
For every tag keyword, test the exact boundary after its name so a longer unknown name such as `[VARIABLE]` is not classified as `[VAR ...]`.
|
||||
|
||||
Only assert rules confirmed by the PDFs or runtime grammar. Keep ambiguous rules as pending cases rather than opinionated errors.
|
||||
|
||||
## Range matrix
|
||||
|
||||
Exercise each diagnostic at:
|
||||
|
||||
- first character and end of file;
|
||||
- after ASCII and non-ASCII text;
|
||||
- on a later line with LF and CRLF;
|
||||
- inside a multiline token or comment;
|
||||
- after an incremental edit that changes preceding line lengths.
|
||||
|
||||
LSP positions are zero-based UTF-16 line/character pairs. Prefer storing source offsets internally and converting with the current `TextDocument` at publication time.
|
||||
|
||||
## Editor matrix
|
||||
|
||||
When syntax changes, inspect:
|
||||
|
||||
- language activation and file association;
|
||||
- TextMate scope on open name, close name, attributes, strings, and delimiters;
|
||||
- bracket matching and auto-closing;
|
||||
- folding start/end markers;
|
||||
- snippet insertion and tab stops;
|
||||
- go-to-definition selection range;
|
||||
- consistency of examples and README syntax.
|
||||
|
||||
## Release evidence
|
||||
|
||||
Record:
|
||||
|
||||
1. exact commands executed;
|
||||
2. automated pass/fail totals;
|
||||
3. Extension Development Host scenarios checked;
|
||||
4. known baseline failures reproduced independently;
|
||||
5. unverified runtime semantics or ambiguous documentation.
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const includeLint = args.includes('--lint');
|
||||
const positional = args.filter(arg => !arg.startsWith('--'));
|
||||
const root = resolve(positional[0] ?? process.cwd());
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
function readJson(relativePath) {
|
||||
const absolutePath = resolve(root, relativePath);
|
||||
if (!existsSync(absolutePath)) {
|
||||
fail(`Missing ${relativePath}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(readFileSync(absolutePath, 'utf8'));
|
||||
} catch (error) {
|
||||
fail(`Invalid JSON in ${relativePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function runNpm(script) {
|
||||
const windows = process.platform === 'win32';
|
||||
const command = windows ? (process.env.ComSpec ?? 'cmd.exe') : 'npm';
|
||||
const commandArgs = windows ? ['/d', '/s', '/c', `npm run ${script}`] : ['run', script];
|
||||
const result = spawnSync(command, commandArgs, {
|
||||
cwd: root,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (result.error) {
|
||||
fail(`Could not run npm script ${script}: ${result.error.message}`);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
fail(`npm run ${script} exited with ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertContributionPaths(packageJson) {
|
||||
const paths = [packageJson.main];
|
||||
for (const language of packageJson.contributes?.languages ?? []) {
|
||||
paths.push(language.configuration);
|
||||
}
|
||||
for (const grammar of packageJson.contributes?.grammars ?? []) {
|
||||
paths.push(grammar.path);
|
||||
}
|
||||
for (const snippet of packageJson.contributes?.snippets ?? []) {
|
||||
paths.push(snippet.path);
|
||||
}
|
||||
for (const relativePath of paths.filter(Boolean)) {
|
||||
if (!existsSync(resolve(root, relativePath))) {
|
||||
fail(`package.json references missing path ${relativePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function smokeTestCore() {
|
||||
const tokenizerPath = resolve(root, 'out/tokenizer.js');
|
||||
const parserPath = resolve(root, 'out/parser.js');
|
||||
if (!existsSync(tokenizerPath) || !existsSync(parserPath)) {
|
||||
fail('Compiled tokenizer/parser output is missing');
|
||||
}
|
||||
|
||||
const { EftlTokenizer } = require(tokenizerPath);
|
||||
const { EftlParser } = require(parserPath);
|
||||
const sample = readFileSync(resolve(root, 'examples/sample.eftl'), 'utf8');
|
||||
const sampleResult = new EftlTokenizer(sample).tokenize();
|
||||
const sampleParse = new EftlParser(sampleResult.tokens).parse();
|
||||
if (sampleResult.errors.length || sampleParse.errors.length) {
|
||||
fail(`sample.eftl is not accepted: ${sampleResult.errors.length} tokenizer and ${sampleParse.errors.length} parser errors`);
|
||||
}
|
||||
|
||||
const mismatched = new EftlTokenizer('[EFTL][IF][/EFTL]').tokenize();
|
||||
if (new EftlParser(mismatched.tokens).parse().errors.length === 0) {
|
||||
fail('Parser smoke case did not reject mismatched nesting');
|
||||
}
|
||||
|
||||
const comment = new EftlTokenizer('[EFTL][!-- unfinished').tokenize();
|
||||
if (comment.errors.length === 0) {
|
||||
fail('Tokenizer smoke case did not reject an unterminated comment');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = readJson('package.json');
|
||||
readJson('language-configuration.json');
|
||||
readJson('syntaxes/eftl.tmLanguage.json');
|
||||
readJson('snippets/eftl.json');
|
||||
|
||||
runNpm('compile');
|
||||
assertContributionPaths(packageJson);
|
||||
smokeTestCore();
|
||||
|
||||
if (packageJson.scripts?.test) {
|
||||
runNpm('test');
|
||||
} else {
|
||||
console.warn('WARN: package.json has no test script; only smoke cases were run.');
|
||||
}
|
||||
|
||||
if (includeLint) {
|
||||
runNpm('lint');
|
||||
} else if (packageJson.scripts?.lint) {
|
||||
console.warn('WARN: lint was skipped; pass --lint after configuring ESLint.');
|
||||
}
|
||||
|
||||
console.log('EFTL repository baseline checks passed.');
|
||||
} catch (error) {
|
||||
console.error(`FAIL: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -1,2 +1,148 @@
|
||||
# vscode-eftl-language
|
||||
|
||||
# EFTL Language Extension for VS Code
|
||||
|
||||
Syntax highlighting and language support for **EFTL (ElixForms Template Language)**.
|
||||
|
||||
## Features
|
||||
|
||||
- **Syntax Highlighting** for all EFTL constructs
|
||||
- **Code Snippets** for common patterns
|
||||
- **Bracket Matching** for EFTL blocks
|
||||
- **Code Folding** for block structures
|
||||
|
||||
## Supported Syntax
|
||||
|
||||
### Block Delimiters
|
||||
|
||||
- `[EFTL]...[/EFTL]` - Main EFTL block
|
||||
- `[VAR name="..." type="..."]...[/VAR]` - Variable declarations
|
||||
- `[IF]...[/IF]` - Conditionals
|
||||
- `[WHILE]...[/WHILE]` - Loops
|
||||
|
||||
### Expressions
|
||||
|
||||
- `[% expression %]` - Execute expression
|
||||
- `[%= expression %]` - Output expression value
|
||||
|
||||
### Data Tags
|
||||
|
||||
- `[TAG]GETVALUEBYTAG,TAG_NAME,REQUEST,IUQOID[/TAG]`
|
||||
- `[TAG]SCHEMAID,schemaId,COL0001,IUQOID, , [/TAG]`
|
||||
|
||||
### Functions
|
||||
|
||||
- `[SPLIT regex="..."]...[/SPLIT]` - Split string
|
||||
- `[TRIM]...[/TRIM]` - Trim whitespace
|
||||
- `[VALUE_OF varname="..." index="..." /]` - Get array element
|
||||
- `[SIZE_OF varname="..." /]` - Get array size
|
||||
- `[CONTAINS varname="..."]...[/CONTAINS]` - Check containment
|
||||
- `[FORMAT type="..." pattern="..."]...[/FORMAT]` - Format values
|
||||
|
||||
### Control Structures
|
||||
|
||||
- `[IF]`, `[CONDITION]`, `[THEN]`, `[ELSE]`, `[ELSE IF]`
|
||||
- `[WHILE]`, `[DO]`
|
||||
|
||||
### Comments
|
||||
|
||||
- `[!-- comment --]`
|
||||
|
||||
## Building and Installation
|
||||
|
||||
### 1. Build from Source
|
||||
|
||||
To compile the extension, you need [Node.js](https://nodejs.org/) installed.
|
||||
|
||||
1. Open a terminal in the project root.
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Compile the TypeScript code:
|
||||
|
||||
```bash
|
||||
npm run compile
|
||||
```
|
||||
|
||||
*Alternatively, use `npm run watch` to automatically recompile on changes.*
|
||||
|
||||
### 2. Manual Installation (for testing)
|
||||
|
||||
To make the extension available in your VS Code without packaging it as a VSIX:
|
||||
|
||||
#### Windows
|
||||
|
||||
1. Open a command prompt (as Administrator for symlinks) or PowerShell.
|
||||
2. Create a link to this folder in your VS Code extensions directory:
|
||||
|
||||
```powershell
|
||||
# PowerShell
|
||||
New-Item -ItemType Junction -Path "$env:USERPROFILE\.vscode\extensions\eftl-language" -Value "D:\__Git\vscode-eftl-language"
|
||||
```
|
||||
|
||||
*Note: Replace the value path with the actual absolute path to your project folder if it differs.*
|
||||
|
||||
#### Linux / macOS
|
||||
|
||||
1. Create a symbolic link:
|
||||
|
||||
```bash
|
||||
ln -s "$(pwd)" ~/.vscode/extensions/eftl-language
|
||||
```
|
||||
|
||||
2. **Restart VS Code**. The extension will now be loaded.
|
||||
|
||||
### 3. Development Mode (Debugging)
|
||||
|
||||
If you want to debug the extension or the language server:
|
||||
|
||||
1. Open the project folder in VS Code.
|
||||
2. Ensure you have run `npm install` and `npm run compile`.
|
||||
3. Press `F5` (or go to the **Run and Debug** view and click **Launch Extension**).
|
||||
4. A new **Extension Development Host** window will open.
|
||||
5. In that window, open any `.eftl` file to test syntax highlighting and the language server.
|
||||
|
||||
## Snippets
|
||||
|
||||
| Prefix | Description |
|
||||
| ----------- | ---------------------- |
|
||||
| `eftl` | EFTL block with header |
|
||||
| `var` | Variable declaration |
|
||||
| `vartag` | Variable from TAG |
|
||||
| `varschema` | Variable from SCHEMAID |
|
||||
| `if` | IF statement |
|
||||
| `ifelse` | IF-ELSE statement |
|
||||
| `while` | WHILE loop |
|
||||
| `split` | Split function |
|
||||
| `trim` | Trim function |
|
||||
| `valueof` | VALUE_OF function |
|
||||
| `sizeof` | SIZE_OF function |
|
||||
| `contains` | CONTAINS function |
|
||||
| `formatnum` | Format number |
|
||||
| `tag` | GETVALUEBYTAG tag |
|
||||
| `schema` | SCHEMAID tag |
|
||||
| `out` | Output expression |
|
||||
| `comment` | Comment block |
|
||||
|
||||
## Example
|
||||
|
||||
```eftl
|
||||
[EFTL][HEADER name="trimDocument" value="true" type="boolean" /]
|
||||
[VAR name="total" type="number"][% total = 0; %][/VAR]
|
||||
[VAR name="items" type="iterable"][SPLIT regex=";"][TAG]GETVALUEBYTAG,ITEMS,REQUEST,IUQOID[/TAG][/SPLIT][/VAR]
|
||||
|
||||
[!-- Calculate total --]
|
||||
[VAR name="count" type="number"][SIZE_OF varname="items" /][/VAR]
|
||||
|
||||
[IF]
|
||||
[CONDITION][% count > 0 %][/CONDITION]
|
||||
[THEN][%= count %] items found[/THEN]
|
||||
[ELSE]No items[/ELSE]
|
||||
[/IF]
|
||||
[/EFTL]
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
[!-- Example EFTL file for testing syntax highlighting --]
|
||||
|
||||
[EFTL][HEADER name="trimDocument" value="true" type="boolean" /]
|
||||
[VAR name="isRichiedenteInPartecipanti" type="string"][% isRichiedenteInPartecipanti = "Utente non ammesso!"; %][/VAR]
|
||||
[VAR name="partecipanti" type="string"][TAG]GETVALUEBYTAG,CONTRATTO_PARTECIPANTI,REQUEST,IUQOID[/TAG][/VAR]
|
||||
[VAR name="nominativoUtente" type="string"][TAG]GETVALUEBYTAG,RICHIEDENTE_NOMINATIVO,REQUEST,IUQOID[/TAG][/VAR]
|
||||
|
||||
[!-- Check if user is in participants list --]
|
||||
[% nominativoUtente = " " + nominativoUtente + ", CF: "; %]
|
||||
|
||||
[IF]
|
||||
[CONDITION][CONTAINS varname="partecipanti"][VALUE_OF varname="nominativoUtente" /][/CONTAINS][/CONDITION]
|
||||
[THEN][% isRichiedenteInPartecipanti = ""; %][/THEN]
|
||||
[ELSE IF]
|
||||
[CONDITION][% partecipanti == "" %][/CONDITION]
|
||||
[THEN][% isRichiedenteInPartecipanti = "Nessun partecipante trovato"; %][/THEN]
|
||||
[/ELSE IF]
|
||||
[/IF]
|
||||
|
||||
[%= isRichiedenteInPartecipanti %]
|
||||
[/EFTL]
|
||||
|
||||
[!-- Another example with loops and calculations --]
|
||||
|
||||
[EFTL][HEADER name="trimDocument" value="true" type="boolean" /]
|
||||
[VAR name="IDX_NOME" type="number"][% IDX_NOME = 0; %][/VAR]
|
||||
[VAR name="IDX_SEDE" type="number"][% IDX_SEDE = 2; %][/VAR]
|
||||
[VAR name="ELENCO" type="string"][% ELENCO = ""; %][/VAR]
|
||||
|
||||
[VAR name="IDX" type="number"][% IDX = 0; %][/VAR]
|
||||
[VAR name="CONTRAENTI" type="iterable"][SPLIT regex="\n"][TRIM][TAG]SCHEMAID,296,COL0004,IUQOID, , [/TAG][/TRIM][/SPLIT][/VAR]
|
||||
[VAR name="CONTRAENTI_NUM" type="number"][SIZE_OF varname="CONTRAENTI" /][/VAR]
|
||||
|
||||
[WHILE]
|
||||
[CONDITION][% IDX < CONTRAENTI_NUM %][/CONDITION]
|
||||
[DO]
|
||||
[VAR name="CONTRAENTE" type="string"][VALUE_OF varname="CONTRAENTI" index="IDX" /][/VAR]
|
||||
[VAR name="DATI" type="iterable"][SPLIT regex=" - "][TRIM][% CONTRAENTE %][/TRIM][/SPLIT][/VAR]
|
||||
[VAR name="NOME" type="string"][VALUE_OF varname="DATI" index="IDX_NOME" /][/VAR]
|
||||
[VAR name="SEDE" type="string"][VALUE_OF varname="DATI" index="IDX_SEDE" /][/VAR]
|
||||
[% ELENCO = ELENCO + NOME + " (" + SEDE + "); "; %]
|
||||
[% IDX = IDX + 1; %]
|
||||
[/DO]
|
||||
[/WHILE]
|
||||
|
||||
[FORMAT type="number" pattern="#,##0.00"][% 12345.67 %][/FORMAT]
|
||||
|
||||
[%= ELENCO %]
|
||||
[/EFTL]
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"comments": {
|
||||
"blockComment": ["[!--", "--]"]
|
||||
},
|
||||
"brackets": [
|
||||
["[EFTL]", "[/EFTL]"],
|
||||
["[VAR", "[/VAR]"],
|
||||
["[IF]", "[/IF]"],
|
||||
["[WHILE]", "[/WHILE]"],
|
||||
["[FORMAT", "[/FORMAT]"],
|
||||
["[CONDITION]", "[/CONDITION]"],
|
||||
["[THEN]", "[/THEN]"],
|
||||
["[ELSE]", "[/ELSE]"],
|
||||
["[ELSE IF]", "[/ELSE IF]"],
|
||||
["[DO]", "[/DO]"],
|
||||
["[TAG]", "[/TAG]"],
|
||||
["[SPLIT", "[/SPLIT]"],
|
||||
["[TRIM]", "[/TRIM]"],
|
||||
["[CONTAINS", "[/CONTAINS]"],
|
||||
["[HEADER", "/]"],
|
||||
["[%", "%]"],
|
||||
["[%=", "%]"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "[EFTL]", "close": "[/EFTL]" },
|
||||
{ "open": "[!--", "close": "--]" },
|
||||
{ "open": "[%", "close": " %]" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "(", "close": ")" }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["[EFTL]", "[/EFTL]"],
|
||||
["[%", "%]"],
|
||||
["\"", "\""],
|
||||
["(", ")"]
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*\\[(EFTL|IF|WHILE|VAR|FORMAT|CONDITION|THEN|ELSE|DO)\\b",
|
||||
"end": "^\\s*\\[/(EFTL|IF|WHILE|VAR|FORMAT|CONDITION|THEN|ELSE|DO)\\]"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1262
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "eftl-language",
|
||||
"displayName": "EFTL Language",
|
||||
"description": "Syntax highlighting and language support for EFTL (ElixForms Template Language)",
|
||||
"version": "0.2.0",
|
||||
"publisher": "elixforms",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"activationEvents": [
|
||||
"onLanguage:eftl"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "eftl",
|
||||
"aliases": [
|
||||
"EFTL",
|
||||
"ElixForms Template Language"
|
||||
],
|
||||
"extensions": [
|
||||
".eftl"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "eftl",
|
||||
"scopeName": "source.eftl",
|
||||
"path": "./syntaxes/eftl.tmLanguage.json"
|
||||
}
|
||||
],
|
||||
"snippets": [
|
||||
{
|
||||
"language": "eftl",
|
||||
"path": "./snippets/eftl.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "EFTL",
|
||||
"properties": {
|
||||
"eftl.maxNumberOfProblems": {
|
||||
"scope": "resource",
|
||||
"type": "number",
|
||||
"default": 100,
|
||||
"description": "Controls the maximum number of problems produced by the server."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"lint": "eslint src --ext ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1",
|
||||
"vscode-languageserver": "^9.0.1",
|
||||
"vscode-languageserver-textdocument": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/vscode": "^1.75.0",
|
||||
"eslint": "^9.39.2",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/elixforms/vscode-eftl-language"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"EFTL Block": {
|
||||
"prefix": "eftl",
|
||||
"body": [
|
||||
"[EFTL][HEADER name=\"trimDocument\" value=\"true\" type=\"boolean\" /]",
|
||||
"$0",
|
||||
"[/EFTL]"
|
||||
],
|
||||
"description": "Create an EFTL block with header"
|
||||
},
|
||||
"Variable Declaration": {
|
||||
"prefix": "var",
|
||||
"body": [
|
||||
"[VAR name=\"${1:varName}\" type=\"${2|string,number,boolean,date,object,iterable|}\"][% ${1:varName} = ${3:value}; %][/VAR]"
|
||||
],
|
||||
"description": "Declare a variable"
|
||||
},
|
||||
"Variable with Unique": {
|
||||
"prefix": "varunique",
|
||||
"body": [
|
||||
"[VAR name=\"${1:varName}\" type=\"${2|string,number,boolean,date,object,iterable|}\" unique=\"${3|true,false|}\"][% ${1:varName} = ${4:value}; %][/VAR]"
|
||||
],
|
||||
"description": "Declare a unique variable"
|
||||
},
|
||||
"Variable from TAG": {
|
||||
"prefix": "vartag",
|
||||
"body": [
|
||||
"[VAR name=\"${1:varName}\" type=\"${2|string,number,boolean,date,object,iterable|}\"][TAG]GETVALUEBYTAG,${3:TAG_NAME},REQUEST,IUQOID[/TAG][/VAR]"
|
||||
],
|
||||
"description": "Declare a variable from a TAG"
|
||||
},
|
||||
"Variable from SCHEMAID": {
|
||||
"prefix": "varschema",
|
||||
"body": [
|
||||
"[VAR name=\"${1:varName}\" type=\"${2|string,number,boolean,date,object,iterable|}\"][TAG]SCHEMAID,${3:schemaId},${4:COL0001},IUQOID, , [/TAG][/VAR]"
|
||||
],
|
||||
"description": "Declare a variable from a SCHEMAID"
|
||||
},
|
||||
"If Statement": {
|
||||
"prefix": "if",
|
||||
"body": [
|
||||
"[IF]",
|
||||
" [CONDITION][% ${1:condition} %][/CONDITION]",
|
||||
" [THEN]${2:// then block}[/THEN]",
|
||||
"[/IF]"
|
||||
],
|
||||
"description": "Create an IF statement"
|
||||
},
|
||||
"If-Else Statement": {
|
||||
"prefix": "ifelse",
|
||||
"body": [
|
||||
"[IF]",
|
||||
" [CONDITION][% ${1:condition} %][/CONDITION]",
|
||||
" [THEN]${2:// then block}[/THEN]",
|
||||
" [ELSE]${3:// else block}[/ELSE]",
|
||||
"[/IF]"
|
||||
],
|
||||
"description": "Create an IF-ELSE statement"
|
||||
},
|
||||
"Else If": {
|
||||
"prefix": "elseif",
|
||||
"body": [
|
||||
"[ELSE IF]",
|
||||
" [CONDITION][% ${1:condition} %][/CONDITION]",
|
||||
"[THEN]${2:// then block}[/THEN]",
|
||||
"[/ELSE IF]"
|
||||
],
|
||||
"description": "Add an ELSE IF clause"
|
||||
},
|
||||
"While Loop": {
|
||||
"prefix": "while",
|
||||
"body": [
|
||||
"[WHILE]",
|
||||
" [CONDITION][% ${1:condition} %][/CONDITION]",
|
||||
" [DO]",
|
||||
" $0",
|
||||
" [/DO]",
|
||||
"[/WHILE]"
|
||||
],
|
||||
"description": "Create a WHILE loop"
|
||||
},
|
||||
"Split": {
|
||||
"prefix": "split",
|
||||
"body": [
|
||||
"[SPLIT regex=\"${1:\\\\n}\"]${2:content}[/SPLIT]"
|
||||
],
|
||||
"description": "Split a string by regex"
|
||||
},
|
||||
"Trim": {
|
||||
"prefix": "trim",
|
||||
"body": [
|
||||
"[TRIM]${1:content}[/TRIM]"
|
||||
],
|
||||
"description": "Trim whitespace"
|
||||
},
|
||||
"Value Of": {
|
||||
"prefix": "valueof",
|
||||
"body": [
|
||||
"[VALUE_OF varname=\"${1:varName}\" index=\"${2:0}\" /]"
|
||||
],
|
||||
"description": "Get value from iterable at index"
|
||||
},
|
||||
"Size Of": {
|
||||
"prefix": "sizeof",
|
||||
"body": [
|
||||
"[SIZE_OF varname=\"${1:varName}\" /]"
|
||||
],
|
||||
"description": "Get size of iterable"
|
||||
},
|
||||
"Contains": {
|
||||
"prefix": "contains",
|
||||
"body": [
|
||||
"[CONTAINS varname=\"${1:varName}\"][VALUE_OF varname=\"${2:searchVar}\" /][/CONTAINS]"
|
||||
],
|
||||
"description": "Check if variable contains value"
|
||||
},
|
||||
"Format Number": {
|
||||
"prefix": "formatnum",
|
||||
"body": [
|
||||
"[FORMAT type=\"number\" pattern=\"${1:#,##0.00}\"][% ${2:varName} %][/FORMAT]"
|
||||
],
|
||||
"description": "Format a number"
|
||||
},
|
||||
"TAG GETVALUEBYTAG": {
|
||||
"prefix": "tag",
|
||||
"body": [
|
||||
"[TAG]GETVALUEBYTAG,${1:TAG_NAME},REQUEST,IUQOID[/TAG]"
|
||||
],
|
||||
"description": "Get value by tag name"
|
||||
},
|
||||
"TAG SCHEMAID": {
|
||||
"prefix": "schema",
|
||||
"body": [
|
||||
"[TAG]SCHEMAID,${1:schemaId},${2:COL0001},IUQOID, , [/TAG]"
|
||||
],
|
||||
"description": "Get value from schema field"
|
||||
},
|
||||
"Output Expression": {
|
||||
"prefix": "out",
|
||||
"body": [
|
||||
"[%= ${1:varName} %]"
|
||||
],
|
||||
"description": "Output a variable value"
|
||||
},
|
||||
"Comment": {
|
||||
"prefix": "comment",
|
||||
"body": [
|
||||
"[!-- ${1:comment} --]"
|
||||
],
|
||||
"description": "Insert a comment"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as path from 'path';
|
||||
import { ExtensionContext } from 'vscode';
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
ServerOptions,
|
||||
TransportKind
|
||||
} from 'vscode-languageclient/node';
|
||||
|
||||
let client: LanguageClient;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
// The server is implemented in node
|
||||
const serverModule = context.asAbsolutePath(path.join('out', 'server.js'));
|
||||
|
||||
// Server options - run the server in Node
|
||||
const serverOptions: ServerOptions = {
|
||||
run: { module: serverModule, transport: TransportKind.ipc },
|
||||
debug: {
|
||||
module: serverModule,
|
||||
transport: TransportKind.ipc,
|
||||
options: { execArgv: ['--nolazy', '--inspect=6009'] }
|
||||
}
|
||||
};
|
||||
|
||||
// Options to control the language client
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
// Register the server for EFTL documents
|
||||
documentSelector: [{ scheme: 'file', language: 'eftl' }]
|
||||
};
|
||||
|
||||
// Create the language client and start the client
|
||||
client = new LanguageClient(
|
||||
'eftlLanguageServer',
|
||||
'EFTL Language Server',
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
|
||||
// Start the client. This will also launch the server
|
||||
client.start();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
import { Token, TokenType, TokenizerError } from './tokenizer';
|
||||
|
||||
/**
|
||||
* AST Node types for EFTL
|
||||
*/
|
||||
export interface AstNode {
|
||||
type: string;
|
||||
line: number;
|
||||
column: number;
|
||||
children?: AstNode[];
|
||||
}
|
||||
|
||||
export interface ParserError {
|
||||
message: string;
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* EFTL Parser - validates structure and reports errors
|
||||
*/
|
||||
export class EftlParser {
|
||||
private tokens: Token[];
|
||||
private pos: number = 0;
|
||||
private errors: ParserError[] = [];
|
||||
private blockStack: { type: TokenType; token: Token }[] = [];
|
||||
|
||||
constructor(tokens: Token[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
parse(): { errors: ParserError[] } {
|
||||
this.pos = 0;
|
||||
this.errors = [];
|
||||
this.blockStack = [];
|
||||
|
||||
while (!this.isAtEnd()) {
|
||||
this.parseTopLevel();
|
||||
}
|
||||
|
||||
// Check for unclosed blocks
|
||||
for (const block of this.blockStack) {
|
||||
this.errors.push({
|
||||
message: `Unclosed block: ${block.token.value}`,
|
||||
line: block.token.line,
|
||||
column: block.token.column,
|
||||
length: block.token.length
|
||||
});
|
||||
}
|
||||
|
||||
return { errors: this.errors };
|
||||
}
|
||||
|
||||
private parseTopLevel(): void {
|
||||
const token = this.current();
|
||||
|
||||
switch (token.type) {
|
||||
case TokenType.EFTL_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.EFTL_CLOSE:
|
||||
this.popBlock(TokenType.EFTL_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.VAR_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.VAR_CLOSE:
|
||||
this.popBlock(TokenType.VAR_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.IF_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.IF_CLOSE:
|
||||
this.popBlock(TokenType.IF_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.CONDITION_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.CONDITION_CLOSE:
|
||||
this.popBlock(TokenType.CONDITION_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.THEN_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.THEN_CLOSE:
|
||||
this.popBlock(TokenType.THEN_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.ELSE_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.ELSE_CLOSE:
|
||||
this.popBlock(TokenType.ELSE_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.ELSE_IF_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.ELSE_IF_CLOSE:
|
||||
this.popBlock(TokenType.ELSE_IF_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.WHILE_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.WHILE_CLOSE:
|
||||
this.popBlock(TokenType.WHILE_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.DO_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.DO_CLOSE:
|
||||
this.popBlock(TokenType.DO_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.TAG_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.TAG_CLOSE:
|
||||
this.popBlock(TokenType.TAG_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.SPLIT_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.SPLIT_CLOSE:
|
||||
this.popBlock(TokenType.SPLIT_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.TRIM_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.TRIM_CLOSE:
|
||||
this.popBlock(TokenType.TRIM_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.CONTAINS_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.CONTAINS_CLOSE:
|
||||
this.popBlock(TokenType.CONTAINS_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.FORMAT_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.FORMAT_CLOSE:
|
||||
this.popBlock(TokenType.FORMAT_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.EXPR_OPEN:
|
||||
case TokenType.EXPR_OUTPUT_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.EXPR_CLOSE:
|
||||
// Can close either EXPR_OPEN or EXPR_OUTPUT_OPEN
|
||||
const exprBlock = this.blockStack.pop();
|
||||
if (!exprBlock || (exprBlock.type !== TokenType.EXPR_OPEN && exprBlock.type !== TokenType.EXPR_OUTPUT_OPEN)) {
|
||||
this.errors.push({
|
||||
message: `Unexpected closing: ${token.value}`,
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
length: token.length
|
||||
});
|
||||
if (exprBlock) {
|
||||
this.blockStack.push(exprBlock);
|
||||
}
|
||||
}
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.COMMENT_OPEN:
|
||||
this.pushBlock(token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.COMMENT_CLOSE:
|
||||
this.popBlock(TokenType.COMMENT_OPEN, token);
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
// Self-closing tags - no block needed
|
||||
case TokenType.VALUE_OF:
|
||||
case TokenType.SIZE_OF:
|
||||
case TokenType.HEADER:
|
||||
if (!token.value.endsWith('/]')) {
|
||||
this.pushBlock(token);
|
||||
}
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
case TokenType.TEXT:
|
||||
case TokenType.EOF:
|
||||
this.advance();
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown token, skip
|
||||
this.advance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private pushBlock(token: Token): void {
|
||||
if (token.value.endsWith('/]')) {
|
||||
return; // It's self-closing, don't push to stack
|
||||
}
|
||||
this.blockStack.push({ type: token.type, token });
|
||||
}
|
||||
|
||||
private popBlock(expectedOpenType: TokenType, closeToken: Token): void {
|
||||
if (this.blockStack.length === 0) {
|
||||
this.errors.push({
|
||||
message: `Unexpected closing tag: ${closeToken.value} (no matching opening tag)`,
|
||||
line: closeToken.line,
|
||||
column: closeToken.column,
|
||||
length: closeToken.length
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const top = this.blockStack[this.blockStack.length - 1];
|
||||
if (top.type !== expectedOpenType) {
|
||||
// Find the expected closing tag name
|
||||
const expectedClose = this.getMatchingClose(top.type);
|
||||
this.errors.push({
|
||||
message: `Mismatched closing tag: expected ${expectedClose}, got ${closeToken.value}`,
|
||||
line: closeToken.line,
|
||||
column: closeToken.column,
|
||||
length: closeToken.length
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.blockStack.pop();
|
||||
}
|
||||
|
||||
private getMatchingClose(openType: TokenType): string {
|
||||
const closeMap: { [key: string]: string } = {
|
||||
[TokenType.EFTL_OPEN]: '[/EFTL]',
|
||||
[TokenType.VAR_OPEN]: '[/VAR]',
|
||||
[TokenType.IF_OPEN]: '[/IF]',
|
||||
[TokenType.CONDITION_OPEN]: '[/CONDITION]',
|
||||
[TokenType.THEN_OPEN]: '[/THEN]',
|
||||
[TokenType.ELSE_OPEN]: '[/ELSE]',
|
||||
[TokenType.ELSE_IF_OPEN]: '[/ELSE IF]',
|
||||
[TokenType.WHILE_OPEN]: '[/WHILE]',
|
||||
[TokenType.DO_OPEN]: '[/DO]',
|
||||
[TokenType.TAG_OPEN]: '[/TAG]',
|
||||
[TokenType.SPLIT_OPEN]: '[/SPLIT]',
|
||||
[TokenType.TRIM_OPEN]: '[/TRIM]',
|
||||
[TokenType.CONTAINS_OPEN]: '[/CONTAINS]',
|
||||
[TokenType.FORMAT_OPEN]: '[/FORMAT]',
|
||||
[TokenType.VALUE_OF]: '[/VALUE_OF]',
|
||||
[TokenType.SIZE_OF]: '[/SIZE_OF]',
|
||||
[TokenType.HEADER]: '[/HEADER]',
|
||||
[TokenType.EXPR_OPEN]: '%]',
|
||||
[TokenType.EXPR_OUTPUT_OPEN]: '%]',
|
||||
[TokenType.COMMENT_OPEN]: '--]'
|
||||
};
|
||||
return closeMap[openType] || 'unknown';
|
||||
}
|
||||
|
||||
private current(): Token {
|
||||
return this.tokens[this.pos] || { type: TokenType.EOF, value: '', line: 0, column: 0, length: 0 };
|
||||
}
|
||||
|
||||
private advance(): Token {
|
||||
if (!this.isAtEnd()) {
|
||||
this.pos++;
|
||||
}
|
||||
return this.tokens[this.pos - 1];
|
||||
}
|
||||
|
||||
private isAtEnd(): boolean {
|
||||
return this.current().type === TokenType.EOF;
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
createConnection,
|
||||
TextDocuments,
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
ProposedFeatures,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
TextDocumentSyncKind,
|
||||
Location,
|
||||
Range,
|
||||
Position,
|
||||
DefinitionParams
|
||||
} from 'vscode-languageserver/node';
|
||||
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument';
|
||||
import { EftlTokenizer, TokenType } from './tokenizer';
|
||||
import { EftlParser } from './parser';
|
||||
|
||||
// Create a connection for the server
|
||||
const connection = createConnection(ProposedFeatures.all);
|
||||
|
||||
// Create a document manager
|
||||
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
|
||||
|
||||
// Store variable definitions per document
|
||||
interface VariableDefinition {
|
||||
name: string;
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
const documentVariables: Map<string, VariableDefinition[]> = new Map();
|
||||
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
return {
|
||||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
definitionProvider: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Handle "Go to Definition" requests
|
||||
connection.onDefinition((params: DefinitionParams): Location | null => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = document.getText();
|
||||
const offset = document.offsetAt(params.position);
|
||||
|
||||
// Find the word at the current position
|
||||
const wordRange = getWordRangeAtPosition(text, offset);
|
||||
if (!wordRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const word = text.substring(wordRange.start, wordRange.end);
|
||||
|
||||
// Look for variable definition
|
||||
const variables = documentVariables.get(params.textDocument.uri) || [];
|
||||
const variable = variables.find(v => v.name === word);
|
||||
|
||||
if (variable) {
|
||||
return {
|
||||
uri: params.textDocument.uri,
|
||||
range: {
|
||||
start: { line: variable.line - 1, character: variable.column - 1 },
|
||||
end: { line: variable.line - 1, character: variable.column - 1 + variable.length }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
function getWordRangeAtPosition(text: string, offset: number): { start: number; end: number } | null {
|
||||
// Find word boundaries (alphanumeric and underscore)
|
||||
let start = offset;
|
||||
let end = offset;
|
||||
|
||||
// Move start backwards
|
||||
while (start > 0 && /[a-zA-Z0-9_]/.test(text[start - 1])) {
|
||||
start--;
|
||||
}
|
||||
|
||||
// Move end forwards
|
||||
while (end < text.length && /[a-zA-Z0-9_]/.test(text[end])) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (start === end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
// Validate documents when they change
|
||||
documents.onDidChangeContent(change => {
|
||||
validateDocument(change.document);
|
||||
});
|
||||
|
||||
async function validateDocument(textDocument: TextDocument): Promise<void> {
|
||||
const text = textDocument.getText();
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
// Tokenize
|
||||
const tokenizer = new EftlTokenizer(text);
|
||||
const { tokens, errors: tokenErrors } = tokenizer.tokenize();
|
||||
|
||||
// Valid types for VAR
|
||||
const validTypes = ['string', 'boolean', 'number', 'date', 'object', 'iterable'];
|
||||
|
||||
// Extract variable definitions and validate types
|
||||
const variables: VariableDefinition[] = [];
|
||||
for (const token of tokens) {
|
||||
if (token.type === TokenType.VAR_OPEN) {
|
||||
// Parse name="varName" from the token value
|
||||
const nameMatch = token.value.match(/name="([^"]+)"/);
|
||||
if (nameMatch) {
|
||||
variables.push({
|
||||
name: nameMatch[1],
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
length: token.length
|
||||
});
|
||||
}
|
||||
|
||||
// Validate type attribute
|
||||
const typeMatch = token.value.match(/type="([^"]*)"/);
|
||||
if (typeMatch) {
|
||||
const typeValue = typeMatch[1];
|
||||
if (!validTypes.includes(typeValue)) {
|
||||
// Find position of the type value in the token
|
||||
const typeIndex = token.value.indexOf(`type="${typeValue}"`);
|
||||
const typeValueStart = typeIndex + 6; // 'type="'.length
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: { line: token.line - 1, character: token.column - 1 + typeValueStart },
|
||||
end: { line: token.line - 1, character: token.column - 1 + typeValueStart + typeValue.length }
|
||||
},
|
||||
message: `Invalid type "${typeValue}". Must be one of: ${validTypes.join(', ')}`,
|
||||
source: 'eftl'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
documentVariables.set(textDocument.uri, variables);
|
||||
|
||||
// Add tokenizer errors
|
||||
for (const error of tokenErrors) {
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: { line: error.line - 1, character: error.column - 1 },
|
||||
end: { line: error.line - 1, character: error.column - 1 + error.length }
|
||||
},
|
||||
message: error.message,
|
||||
source: 'eftl'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse
|
||||
const parser = new EftlParser(tokens);
|
||||
const { errors: parseErrors } = parser.parse();
|
||||
|
||||
// Add parser errors
|
||||
for (const error of parseErrors) {
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: { line: error.line - 1, character: error.column - 1 },
|
||||
end: { line: error.line - 1, character: error.column - 1 + error.length }
|
||||
},
|
||||
message: error.message,
|
||||
source: 'eftl'
|
||||
});
|
||||
}
|
||||
|
||||
// Send diagnostics to VS Code
|
||||
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
|
||||
}
|
||||
|
||||
// Clean up when documents are closed
|
||||
documents.onDidClose(e => {
|
||||
documentVariables.delete(e.document.uri);
|
||||
});
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
documents.listen(connection);
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* EFTL Token Types
|
||||
*/
|
||||
export enum TokenType {
|
||||
// Block delimiters
|
||||
EFTL_OPEN = 'EFTL_OPEN', // [EFTL]
|
||||
EFTL_CLOSE = 'EFTL_CLOSE', // [/EFTL]
|
||||
|
||||
// Variable
|
||||
VAR_OPEN = 'VAR_OPEN', // [VAR ...]
|
||||
VAR_CLOSE = 'VAR_CLOSE', // [/VAR]
|
||||
|
||||
// Control structures
|
||||
IF_OPEN = 'IF_OPEN', // [IF]
|
||||
IF_CLOSE = 'IF_CLOSE', // [/IF]
|
||||
CONDITION_OPEN = 'CONDITION_OPEN', // [CONDITION]
|
||||
CONDITION_CLOSE = 'CONDITION_CLOSE', // [/CONDITION]
|
||||
THEN_OPEN = 'THEN_OPEN', // [THEN]
|
||||
THEN_CLOSE = 'THEN_CLOSE', // [/THEN]
|
||||
ELSE_OPEN = 'ELSE_OPEN', // [ELSE]
|
||||
ELSE_CLOSE = 'ELSE_CLOSE', // [/ELSE]
|
||||
ELSE_IF_OPEN = 'ELSE_IF_OPEN', // [ELSE IF]
|
||||
ELSE_IF_CLOSE = 'ELSE_IF_CLOSE', // [/ELSE IF]
|
||||
WHILE_OPEN = 'WHILE_OPEN', // [WHILE]
|
||||
WHILE_CLOSE = 'WHILE_CLOSE', // [/WHILE]
|
||||
DO_OPEN = 'DO_OPEN', // [DO]
|
||||
DO_CLOSE = 'DO_CLOSE', // [/DO]
|
||||
|
||||
// Functions
|
||||
TAG_OPEN = 'TAG_OPEN', // [TAG]
|
||||
TAG_CLOSE = 'TAG_CLOSE', // [/TAG]
|
||||
SPLIT_OPEN = 'SPLIT_OPEN', // [SPLIT ...]
|
||||
SPLIT_CLOSE = 'SPLIT_CLOSE', // [/SPLIT]
|
||||
TRIM_OPEN = 'TRIM_OPEN', // [TRIM]
|
||||
TRIM_CLOSE = 'TRIM_CLOSE', // [/TRIM]
|
||||
CONTAINS_OPEN = 'CONTAINS_OPEN', // [CONTAINS ...]
|
||||
CONTAINS_CLOSE = 'CONTAINS_CLOSE', // [/CONTAINS]
|
||||
FORMAT_OPEN = 'FORMAT_OPEN', // [FORMAT ...]
|
||||
FORMAT_CLOSE = 'FORMAT_CLOSE', // [/FORMAT]
|
||||
VALUE_OF = 'VALUE_OF', // [VALUE_OF ... /]
|
||||
SIZE_OF = 'SIZE_OF', // [SIZE_OF ... /]
|
||||
HEADER = 'HEADER', // [HEADER ... /]
|
||||
|
||||
// Expressions
|
||||
EXPR_OPEN = 'EXPR_OPEN', // [%
|
||||
EXPR_CLOSE = 'EXPR_CLOSE', // %]
|
||||
EXPR_OUTPUT_OPEN = 'EXPR_OUTPUT_OPEN', // [%=
|
||||
|
||||
// Comments
|
||||
COMMENT_OPEN = 'COMMENT_OPEN', // [!--
|
||||
COMMENT_CLOSE = 'COMMENT_CLOSE', // --]
|
||||
|
||||
// Content
|
||||
TEXT = 'TEXT', // Plain text
|
||||
IDENTIFIER = 'IDENTIFIER', // Variable names
|
||||
STRING = 'STRING', // "..."
|
||||
NUMBER = 'NUMBER', // 123, 45.67
|
||||
|
||||
// Other
|
||||
EOF = 'EOF',
|
||||
ERROR = 'ERROR'
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface TokenizerError {
|
||||
message: string;
|
||||
line: number;
|
||||
column: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* EFTL Tokenizer - converts source text into tokens
|
||||
*/
|
||||
export class EftlTokenizer {
|
||||
private source: string;
|
||||
private pos: number = 0;
|
||||
private line: number = 1;
|
||||
private column: number = 1;
|
||||
private tokens: Token[] = [];
|
||||
private errors: TokenizerError[] = [];
|
||||
|
||||
constructor(source: string) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
tokenize(): { tokens: Token[]; errors: TokenizerError[] } {
|
||||
this.tokens = [];
|
||||
this.errors = [];
|
||||
this.pos = 0;
|
||||
this.line = 1;
|
||||
this.column = 1;
|
||||
|
||||
while (this.pos < this.source.length) {
|
||||
this.scanToken();
|
||||
}
|
||||
|
||||
this.tokens.push({
|
||||
type: TokenType.EOF,
|
||||
value: '',
|
||||
line: this.line,
|
||||
column: this.column,
|
||||
length: 0
|
||||
});
|
||||
|
||||
return { tokens: this.tokens, errors: this.errors };
|
||||
}
|
||||
|
||||
private scanToken(): void {
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Check for EFTL constructs starting with [
|
||||
if (this.peek() === '[') {
|
||||
if (this.match('[EFTL]')) {
|
||||
this.addToken(TokenType.EFTL_OPEN, '[EFTL]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/EFTL]')) {
|
||||
this.addToken(TokenType.EFTL_CLOSE, '[/EFTL]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[!--')) {
|
||||
this.addToken(TokenType.COMMENT_OPEN, '[!--', startLine, startColumn);
|
||||
this.scanComment();
|
||||
return;
|
||||
}
|
||||
if (this.match('[%=')) {
|
||||
this.addToken(TokenType.EXPR_OUTPUT_OPEN, '[%=', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[%')) {
|
||||
this.addToken(TokenType.EXPR_OPEN, '[%', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[VAR')) {
|
||||
this.scanVarOpen(startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/VAR]')) {
|
||||
this.addToken(TokenType.VAR_CLOSE, '[/VAR]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[IF]')) {
|
||||
this.addToken(TokenType.IF_OPEN, '[IF]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/IF]')) {
|
||||
this.addToken(TokenType.IF_CLOSE, '[/IF]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[CONDITION]')) {
|
||||
this.addToken(TokenType.CONDITION_OPEN, '[CONDITION]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/CONDITION]')) {
|
||||
this.addToken(TokenType.CONDITION_CLOSE, '[/CONDITION]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[THEN]')) {
|
||||
this.addToken(TokenType.THEN_OPEN, '[THEN]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/THEN]')) {
|
||||
this.addToken(TokenType.THEN_CLOSE, '[/THEN]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[ELSE IF]')) {
|
||||
this.addToken(TokenType.ELSE_IF_OPEN, '[ELSE IF]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/ELSE IF]')) {
|
||||
this.addToken(TokenType.ELSE_IF_CLOSE, '[/ELSE IF]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[ELSE]')) {
|
||||
this.addToken(TokenType.ELSE_OPEN, '[ELSE]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/ELSE]')) {
|
||||
this.addToken(TokenType.ELSE_CLOSE, '[/ELSE]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[WHILE]')) {
|
||||
this.addToken(TokenType.WHILE_OPEN, '[WHILE]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/WHILE]')) {
|
||||
this.addToken(TokenType.WHILE_CLOSE, '[/WHILE]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[DO]')) {
|
||||
this.addToken(TokenType.DO_OPEN, '[DO]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/DO]')) {
|
||||
this.addToken(TokenType.DO_CLOSE, '[/DO]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[TAG]')) {
|
||||
this.addToken(TokenType.TAG_OPEN, '[TAG]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/TAG]')) {
|
||||
this.addToken(TokenType.TAG_CLOSE, '[/TAG]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[SPLIT')) {
|
||||
this.scanTagWithAttributes(TokenType.SPLIT_OPEN, 'SPLIT', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/SPLIT]')) {
|
||||
this.addToken(TokenType.SPLIT_CLOSE, '[/SPLIT]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[TRIM]')) {
|
||||
this.addToken(TokenType.TRIM_OPEN, '[TRIM]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/TRIM]')) {
|
||||
this.addToken(TokenType.TRIM_CLOSE, '[/TRIM]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[CONTAINS')) {
|
||||
this.scanTagWithAttributes(TokenType.CONTAINS_OPEN, 'CONTAINS', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/CONTAINS]')) {
|
||||
this.addToken(TokenType.CONTAINS_CLOSE, '[/CONTAINS]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[FORMAT')) {
|
||||
this.scanTagWithAttributes(TokenType.FORMAT_OPEN, 'FORMAT', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[/FORMAT]')) {
|
||||
this.addToken(TokenType.FORMAT_CLOSE, '[/FORMAT]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[VALUE_OF')) {
|
||||
this.scanSelfClosingTag(TokenType.VALUE_OF, 'VALUE_OF', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[SIZE_OF')) {
|
||||
this.scanSelfClosingTag(TokenType.SIZE_OF, 'SIZE_OF', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
if (this.match('[HEADER')) {
|
||||
this.scanSelfClosingTag(TokenType.HEADER, 'HEADER', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for expression close
|
||||
if (this.match('%]')) {
|
||||
this.addToken(TokenType.EXPR_CLOSE, '%]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for comment close
|
||||
if (this.match('--]')) {
|
||||
this.addToken(TokenType.COMMENT_CLOSE, '--]', startLine, startColumn);
|
||||
return;
|
||||
}
|
||||
|
||||
// Plain text - consume until we hit a special character
|
||||
this.scanText(startLine, startColumn);
|
||||
}
|
||||
|
||||
private scanComment(): void {
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
let content = '';
|
||||
|
||||
while (this.pos < this.source.length) {
|
||||
if (this.match('--]')) {
|
||||
this.addToken(TokenType.COMMENT_CLOSE, '--]', this.line, this.column - 3);
|
||||
return;
|
||||
}
|
||||
content += this.advance();
|
||||
}
|
||||
|
||||
// Unclosed comment
|
||||
this.errors.push({
|
||||
message: 'Unclosed comment: expected "--]"',
|
||||
line: startLine,
|
||||
column: startColumn,
|
||||
length: content.length
|
||||
});
|
||||
}
|
||||
|
||||
private scanVarOpen(startLine: number, startColumn: number): void {
|
||||
let value = '[VAR';
|
||||
// Scan until we find ]
|
||||
while (this.pos < this.source.length && this.peek() !== ']') {
|
||||
value += this.advance();
|
||||
}
|
||||
if (this.peek() === ']') {
|
||||
value += this.advance();
|
||||
}
|
||||
this.addToken(TokenType.VAR_OPEN, value, startLine, startColumn);
|
||||
}
|
||||
|
||||
private scanTagWithAttributes(type: TokenType, tagName: string, startLine: number, startColumn: number): void {
|
||||
let value = '[' + tagName;
|
||||
while (this.pos < this.source.length && this.peek() !== ']') {
|
||||
value += this.advance();
|
||||
}
|
||||
if (this.peek() === ']') {
|
||||
value += this.advance();
|
||||
}
|
||||
this.addToken(type, value, startLine, startColumn);
|
||||
}
|
||||
|
||||
private scanSelfClosingTag(type: TokenType, tagName: string, startLine: number, startColumn: number): void {
|
||||
let value = '[' + tagName;
|
||||
while (this.pos < this.source.length) {
|
||||
const ch = this.advance();
|
||||
value += ch;
|
||||
if (value.endsWith('/]')) {
|
||||
break;
|
||||
}
|
||||
if (ch === ']') {
|
||||
// Could be block start, let parser handle it if it's supposed to be self-closing
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.addToken(type, value, startLine, startColumn);
|
||||
}
|
||||
|
||||
private scanText(startLine: number, startColumn: number): void {
|
||||
let text = '';
|
||||
while (this.pos < this.source.length) {
|
||||
const ch = this.peek();
|
||||
// Stop at special characters
|
||||
if (ch === '[' || (ch === '%' && this.peekNext() === ']') || (ch === '-' && this.peekNext() === '-' && this.peekAt(2) === ']')) {
|
||||
break;
|
||||
}
|
||||
text += this.advance();
|
||||
}
|
||||
if (text.length > 0) {
|
||||
this.addToken(TokenType.TEXT, text, startLine, startColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private peek(): string {
|
||||
return this.source[this.pos] || '\0';
|
||||
}
|
||||
|
||||
private peekNext(): string {
|
||||
return this.source[this.pos + 1] || '\0';
|
||||
}
|
||||
|
||||
private peekAt(offset: number): string {
|
||||
return this.source[this.pos + offset] || '\0';
|
||||
}
|
||||
|
||||
private advance(): string {
|
||||
const ch = this.source[this.pos++];
|
||||
if (ch === '\n') {
|
||||
this.line++;
|
||||
this.column = 1;
|
||||
} else {
|
||||
this.column++;
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
private match(expected: string): boolean {
|
||||
if (this.source.substring(this.pos, this.pos + expected.length) === expected) {
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
this.advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private addToken(type: TokenType, value: string, line: number, column: number): void {
|
||||
this.tokens.push({
|
||||
type,
|
||||
value,
|
||||
line,
|
||||
column,
|
||||
length: value.length
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "EFTL",
|
||||
"scopeName": "source.eftl",
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#eftl-block" },
|
||||
{ "include": "#expressions" },
|
||||
{ "include": "#tags" },
|
||||
{ "include": "#control-structures" },
|
||||
{ "include": "#variables" },
|
||||
{ "include": "#functions" },
|
||||
{ "include": "#strings" }
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.eftl",
|
||||
"begin": "\\[!--",
|
||||
"end": "--\\]",
|
||||
"captures": {
|
||||
"0": { "name": "punctuation.definition.comment.eftl" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"eftl-block": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.block.eftl",
|
||||
"begin": "(\\[EFTL\\])",
|
||||
"end": "(\\[/EFTL\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.eftl.begin" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": { "name": "keyword.control.eftl.end" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#expressions" },
|
||||
{ "include": "#tags" },
|
||||
{ "include": "#control-structures" },
|
||||
{ "include": "#variables" },
|
||||
{ "include": "#functions" },
|
||||
{ "include": "#header" },
|
||||
{ "include": "#strings" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"expressions": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.expression.output.eftl",
|
||||
"begin": "\\[%=",
|
||||
"end": "%\\]",
|
||||
"beginCaptures": {
|
||||
"0": { "name": "punctuation.section.expression.begin.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.section.expression.end.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#expression-content" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "meta.expression.eftl",
|
||||
"begin": "\\[%",
|
||||
"end": "%\\]",
|
||||
"beginCaptures": {
|
||||
"0": { "name": "punctuation.section.expression.begin.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.section.expression.end.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#expression-content" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"expression-content": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.eftl",
|
||||
"match": "\\b[0-9]+\\.?[0-9]*\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.eftl",
|
||||
"match": "="
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.eftl",
|
||||
"match": "([+\\-*%]|/(?!\\]))"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.eftl",
|
||||
"match": "(!=|==|<=|>=|<|>)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.eftl",
|
||||
"match": "(&&|\\|\\|)"
|
||||
},
|
||||
{
|
||||
"name": "constant.language.boolean.eftl",
|
||||
"match": "\\b(true|false)\\b"
|
||||
},
|
||||
{
|
||||
"name": "variable.other.eftl",
|
||||
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.double.eftl",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.eftl",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "punctuation.terminator.statement.eftl",
|
||||
"match": ";"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"patterns": [
|
||||
{
|
||||
"comment": "name attribute",
|
||||
"match": "\\b(name)\\b\\s*(=)\\s*(\"(.*?)\")",
|
||||
"captures": {
|
||||
"1": { "name": "entity.other.attribute-name.eftl" },
|
||||
"2": { "name": "punctuation.separator.key-value.eftl" },
|
||||
"3": { "name": "string.quoted.double.eftl" },
|
||||
"4": { "name": "variable.other.eftl" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "type attribute",
|
||||
"match": "\\b(type)\\b\\s*(=)\\s*(\"(string|boolean|number|date|object|iterable)\")",
|
||||
"captures": {
|
||||
"1": { "name": "entity.other.attribute-name.eftl" },
|
||||
"2": { "name": "punctuation.separator.key-value.eftl" },
|
||||
"3": { "name": "string.quoted.double.eftl" },
|
||||
"4": { "name": "support.type.primitive.eftl" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "generic attribute",
|
||||
"match": "\\b([a-zA-Z_][a-zA-Z0-9_-]*)\\b\\s*(=)\\s*(\"(.*?)\")",
|
||||
"captures": {
|
||||
"1": { "name": "entity.other.attribute-name.eftl" },
|
||||
"2": { "name": "punctuation.separator.key-value.eftl" },
|
||||
"3": { "name": "string.quoted.double.eftl" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tag-open-context": {
|
||||
"patterns": [
|
||||
{ "include": "#attributes" },
|
||||
{ "include": "#expressions" },
|
||||
{ "include": "#functions" },
|
||||
{ "include": "#tags" }
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.tag.block.eftl",
|
||||
"begin": "(\\[)(TAG)(?:\\s+[^\\x00]*?)?(?<!/)(\\])",
|
||||
"end": "(\\[/)(TAG)(\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "entity.name.tag.eftl" },
|
||||
"2": { "name": "entity.name.tag.eftl" },
|
||||
"3": { "name": "entity.name.tag.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": { "name": "entity.name.tag.eftl" },
|
||||
"2": { "name": "entity.name.tag.eftl" },
|
||||
"3": { "name": "entity.name.tag.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#expressions" },
|
||||
{ "include": "#tags" },
|
||||
{ "include": "#functions" },
|
||||
{ "include": "#control-structures" },
|
||||
{ "include": "#variables" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "meta.tag.eftl",
|
||||
"begin": "(\\[)(TAG)\\b",
|
||||
"end": "(/\\s*\\]|(?<!/)\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "entity.name.tag.eftl" },
|
||||
"2": { "name": "entity.name.tag.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "entity.name.tag.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#tag-open-context" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.header.eftl",
|
||||
"begin": "(\\[)(HEADER)\\b",
|
||||
"end": "(/\\s*\\]|(?<!/)\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.other.header.eftl" },
|
||||
"2": { "name": "keyword.other.header.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "keyword.other.header.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#tag-open-context" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"control-structures": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.conditional.eftl",
|
||||
"match": "\\[(IF|ELSE IF|ELSE|/IF|CONDITION|/CONDITION|THEN|/THEN|/ELSE IF|/ELSE)\\]"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.loop.eftl",
|
||||
"match": "\\[(WHILE|/WHILE|DO|/DO)\\]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variables": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.variable.block.eftl",
|
||||
"begin": "(\\[)(VAR)(?:\\s+[^\\x00]*?)?(?<!/)(\\])",
|
||||
"end": "(\\[/)(VAR)(\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "storage.type.variable.eftl" },
|
||||
"2": { "name": "storage.type.variable.eftl" },
|
||||
"3": { "name": "storage.type.variable.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": { "name": "storage.type.variable.eftl" },
|
||||
"2": { "name": "storage.type.variable.eftl" },
|
||||
"3": { "name": "storage.type.variable.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#expressions" },
|
||||
{ "include": "#functions" },
|
||||
{ "include": "#tags" },
|
||||
{ "include": "#control-structures" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "meta.variable.declaration.eftl",
|
||||
"begin": "(\\[)(VAR)\\b",
|
||||
"end": "(/\\s*\\]|(?<!/)\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "storage.type.variable.eftl" },
|
||||
"2": { "name": "storage.type.variable.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "storage.type.variable.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#tag-open-context" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"functions": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.function.block.eftl",
|
||||
"begin": "(\\[)(VALUE_OF|SIZE_OF|SPLIT|FORMAT|CONTAINS|TRIM)(?:\\s+[^\\x00]*?)?(?<!/)(\\])",
|
||||
"end": "(\\[/)(VALUE_OF|SIZE_OF|SPLIT|FORMAT|CONTAINS|TRIM)(\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "support.function.eftl" },
|
||||
"2": { "name": "support.function.eftl" },
|
||||
"3": { "name": "support.function.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": { "name": "support.function.eftl" },
|
||||
"2": { "name": "support.function.eftl" },
|
||||
"3": { "name": "support.function.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#expressions" },
|
||||
{ "include": "#functions" },
|
||||
{ "include": "#tags" },
|
||||
{ "include": "#control-structures" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "meta.function.eftl",
|
||||
"begin": "(\\[)(VALUE_OF|SIZE_OF|SPLIT|FORMAT|CONTAINS|TRIM)\\b",
|
||||
"end": "(/\\s*\\]|(?<!/)\\])",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "support.function.eftl" },
|
||||
"2": { "name": "support.function.eftl" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "support.function.eftl" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#tag-open-context" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.eftl",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.eftl",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
Reference in New Issue
Block a user