add first working version
This commit is contained in:
@@ -1,2 +1,98 @@
|
|||||||
# 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 --]`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From VSIX (Local)
|
||||||
|
|
||||||
|
1. Package the extension: `npx vsce package`
|
||||||
|
2. Install in VS Code: `code --install-extension eftl-language-0.1.0.vsix`
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
1. Open this folder in VS Code
|
||||||
|
2. Press `F5` to launch Extension Development Host
|
||||||
|
3. Open any `.eftl` file to see syntax highlighting
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|||||||
@@ -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
+159
@@ -0,0 +1,159 @@
|
|||||||
|
{
|
||||||
|
"name": "eftl-language",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "eftl-language",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"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",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.75.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||||
|
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/vscode": {
|
||||||
|
"version": "1.107.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.107.0.tgz",
|
||||||
|
"integrity": "sha512-XS8YE1jlyTIowP64+HoN30OlC1H9xqSlq1eoLZUgFEC8oUTO6euYZxti1xRiLSfZocs4qytTzR6xCBYtioQTCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageclient": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^5.1.0",
|
||||||
|
"semver": "^7.3.7",
|
||||||
|
"vscode-languageserver-protocol": "3.17.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.82.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageserver-protocol": "3.17.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"installServerIntoExtension": "bin/installServerIntoExtension"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "8.2.0",
|
||||||
|
"vscode-languageserver-types": "3.17.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-textdocument": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
||||||
|
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"activationEvents": [
|
||||||
|
"onLanguage:eftl"
|
||||||
|
],
|
||||||
|
"main": "./out/extension.js",
|
||||||
|
"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",
|
||||||
|
"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();
|
||||||
|
}
|
||||||
+319
@@ -0,0 +1,319 @@
|
|||||||
|
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:
|
||||||
|
this.advance();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType.TEXT:
|
||||||
|
case TokenType.EOF:
|
||||||
|
this.advance();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown token, skip
|
||||||
|
this.advance();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushBlock(token: Token): void {
|
||||||
|
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.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,401 @@
|
|||||||
|
/**
|
||||||
|
* 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 ] or ]
|
||||||
|
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 === ']') {
|
||||||
|
// Not self-closing, error
|
||||||
|
this.errors.push({
|
||||||
|
message: `Expected self-closing tag: [${tagName} ... /]`,
|
||||||
|
line: startLine,
|
||||||
|
column: startColumn,
|
||||||
|
length: value.length
|
||||||
|
});
|
||||||
|
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,364 @@
|
|||||||
|
{
|
||||||
|
"$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": ";"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.tag.data.eftl",
|
||||||
|
"begin": "(\\[TAG\\])",
|
||||||
|
"end": "(\\[/TAG\\])",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "entity.name.tag.eftl" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"1": { "name": "entity.name.tag.eftl" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#tag-content" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tag-content": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "support.function.builtin.eftl",
|
||||||
|
"match": "\\b(GETVALUEBYTAG|SCHEMAID|PLUGIN_DOM_PARAM)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "variable.parameter.eftl",
|
||||||
|
"match": "\\b(REQUEST|MODULE|IUQOID)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "constant.numeric.eftl",
|
||||||
|
"match": "\\b[0-9]+\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "variable.other.eftl",
|
||||||
|
"match": "\\b(COL[0-9]+)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "punctuation.separator.eftl",
|
||||||
|
"match": ","
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.header.eftl",
|
||||||
|
"match": "(\\[HEADER)\\s+(name=\"[^\"]*\")\\s+(value=\"[^\"]*\")\\s+(type=\"[^\"]*\")\\s*(/\\])",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.other.header.eftl" },
|
||||||
|
"2": { "name": "entity.other.attribute-name.eftl" },
|
||||||
|
"3": { "name": "string.quoted.double.eftl" },
|
||||||
|
"4": { "name": "entity.other.attribute-name.eftl" },
|
||||||
|
"5": { "name": "punctuation.definition.tag.end.eftl" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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.declaration.eftl",
|
||||||
|
"begin": "(\\[VAR)",
|
||||||
|
"end": "(\\[/VAR\\])",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "storage.type.variable.eftl" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"1": { "name": "storage.type.variable.eftl" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#var-attributes" },
|
||||||
|
{ "include": "#expressions" },
|
||||||
|
{ "include": "#functions" },
|
||||||
|
{ "include": "#tags" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"var-attributes": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "name attribute - variable name gets special color",
|
||||||
|
"match": "(name)(=)(\"([^\"]*)\")",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "entity.other.attribute-name.name.eftl" },
|
||||||
|
"2": { "name": "punctuation.separator.key-value.eftl" },
|
||||||
|
"3": { "name": "string.quoted.double.eftl" },
|
||||||
|
"4": { "name": "variable.other.declaration.eftl" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "type attribute with valid type values",
|
||||||
|
"match": "(type)(=)(\"(string|boolean|number|date|object|iterable)\")",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "entity.other.attribute-name.type.eftl" },
|
||||||
|
"2": { "name": "punctuation.separator.key-value.eftl" },
|
||||||
|
"3": { "name": "string.quoted.double.eftl" },
|
||||||
|
"4": { "name": "support.type.primitive.eftl" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "unique attribute - boolean value",
|
||||||
|
"match": "(unique)(=)(\"(true|false)\")",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "entity.other.attribute-name.unique.eftl" },
|
||||||
|
"2": { "name": "punctuation.separator.key-value.eftl" },
|
||||||
|
"3": { "name": "string.quoted.double.eftl" },
|
||||||
|
"4": { "name": "constant.language.boolean.eftl" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "closing bracket of VAR open tag",
|
||||||
|
"match": "\\]",
|
||||||
|
"name": "punctuation.definition.tag.end.eftl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"functions": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.function.split.eftl",
|
||||||
|
"begin": "(\\[SPLIT)\\s+(regex=\"[^\"]*\")(\\])?",
|
||||||
|
"end": "(\\[/SPLIT\\])",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" },
|
||||||
|
"2": { "name": "string.regexp.eftl" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#expressions" },
|
||||||
|
{ "include": "#tags" },
|
||||||
|
{ "include": "#functions" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.function.trim.eftl",
|
||||||
|
"begin": "(\\[TRIM\\])",
|
||||||
|
"end": "(\\[/TRIM\\])",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#expressions" },
|
||||||
|
{ "include": "#tags" },
|
||||||
|
{ "include": "#functions" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.function.contains.eftl",
|
||||||
|
"begin": "(\\[CONTAINS)\\s+(varname=\"[^\"]*\")(\\])?",
|
||||||
|
"end": "(\\[/CONTAINS\\])",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" },
|
||||||
|
"2": { "name": "entity.other.attribute-name.eftl" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#functions" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.function.value-of.eftl",
|
||||||
|
"match": "(\\[VALUE_OF)\\s+(varname=\"[^\"]*\")\\s*(index=\"?[^\"]*\"?)?\\s*(/\\])",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "support.function.eftl" },
|
||||||
|
"2": { "name": "entity.other.attribute-name.eftl" },
|
||||||
|
"3": { "name": "entity.other.attribute-name.eftl" },
|
||||||
|
"4": { "name": "punctuation.definition.tag.end.eftl" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.function.size-of.eftl",
|
||||||
|
"match": "(\\[SIZE_OF)\\s+(varname=\"[^\"]*\")\\s*(/\\])",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "support.function.eftl" },
|
||||||
|
"2": { "name": "entity.other.attribute-name.eftl" },
|
||||||
|
"3": { "name": "punctuation.definition.tag.end.eftl" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.function.format.eftl",
|
||||||
|
"begin": "(\\[FORMAT)\\s+(type=\"[^\"]*\")\\s*(pattern=\"[^\"]*\")?(\\])?",
|
||||||
|
"end": "(\\[/FORMAT\\])",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" },
|
||||||
|
"2": { "name": "entity.other.attribute-name.eftl" },
|
||||||
|
"3": { "name": "string.other.pattern.eftl" }
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"1": { "name": "support.function.eftl" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#expressions" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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