@wanasapps/deluge-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/assets/deluge.tmLanguage.json +122 -0
- package/assets/language-configuration.json +29 -0
- package/assets/snippets.json +167 -0
- package/package.json +36 -0
- package/src/analyze/index.js +93 -0
- package/src/format/formatter.js +45 -0
- package/src/format/signature.js +70 -0
- package/src/format/source.js +63 -0
- package/src/index.js +35 -0
- package/src/language/index.js +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wanas Apps
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @wanasapps/deluge-core
|
|
2
|
+
|
|
3
|
+
Deluge language tooling for **every** Zoho product — not just CRM.
|
|
4
|
+
|
|
5
|
+
Deluge runs in Zoho CRM, Creator, Books, Inventory, Projects, Sprints, Qntrl, Catalyst and People. This package holds everything that is true about the *language*, so a CLI, a VS Code extension and a browser editor can share one implementation instead of copying it three times.
|
|
6
|
+
|
|
7
|
+
**Hard rule:** no I/O, no HTTP, no editor APIs. Pure functions over source text plus portable data assets. Anything that talks to Zoho belongs in an API layer; anything that talks to an editor belongs in that editor's adapter.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wanasapps/deluge-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Formatting and source transforms
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const deluge = require('@wanasapps/deluge-core');
|
|
17
|
+
|
|
18
|
+
deluge.format(source); // canonical indentation
|
|
19
|
+
deluge.stripLeadingComments(source); // see below — required before a push
|
|
20
|
+
deluge.parseSignature(source); // name, arguments, return type
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### `stripLeadingComments` — the one that costs people an afternoon
|
|
24
|
+
|
|
25
|
+
Zoho's raw function-code endpoints parse everything before the first `(` as the package declaration **without skipping comments**. A script that opens with a `// documentation header` is rejected with a bodyless, misleading 400 — while the same script pastes into the web editor fine. Only these endpoints choke.
|
|
26
|
+
|
|
27
|
+
So the leading comment lines are **blanked, not deleted**: blank lines are tolerated, and keeping them means the line numbers in any `COMPILATION_ERROR` Zoho returns still match the developer's local file. Interior comments are untouched.
|
|
28
|
+
|
|
29
|
+
Any tool pushing Deluge to any Zoho product needs this.
|
|
30
|
+
|
|
31
|
+
## On-disk conventions
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
deluge.namespaceForCategory('Related List'); // 'related_list'
|
|
35
|
+
deluge.dsPath('sendInvoice', 'Standalone'); // 'standalone/sendInvoice.ds'
|
|
36
|
+
deluge.apiNameFromDsPath('ns.sendInvoice.ds'); // 'sendInvoice'
|
|
37
|
+
deluge.DELUGE_RUNTIME; // 'Deluge 1.0'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Zoho names categories in prose ("Related List"); on disk they become lowercase, underscored folders. Sharing this means every tool writes the same `functions/<namespace>/` layout.
|
|
41
|
+
|
|
42
|
+
## Language assets
|
|
43
|
+
|
|
44
|
+
The same JSON files VS Code consumes, exposed as data so Monaco, Shiki, a docs site or a web editor can load them without depending on an extension.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
deluge.language.grammar(); // TextMate grammar
|
|
48
|
+
deluge.language.configuration(); // brackets, comments, auto-closing pairs
|
|
49
|
+
deluge.language.snippets(); // VS Code snippet format
|
|
50
|
+
deluge.language.EXTENSIONS; // ['.ds', '.dg', '.deluge']
|
|
51
|
+
deluge.language.assetPaths; // absolute paths to the raw files
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Analysis
|
|
55
|
+
|
|
56
|
+
Editor-agnostic: returns plain objects, so the same engine can back a `vscode.Diagnostic`, a CLI check, or a CI gate.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
deluge.analyze.lint(source);
|
|
60
|
+
// [{ line, column, message, severity: 'error'|'warning'|'info', code }]
|
|
61
|
+
|
|
62
|
+
deluge.analyze.isValid(source); // false only on errors, warnings ignored
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Checks balanced delimiters (ignoring braces inside strings and comments) and flags the leading-comment header described above.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT © Wanas Apps FZ-LLC
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
3
|
+
"name": "Deluge",
|
|
4
|
+
"patterns": [
|
|
5
|
+
{ "include": "#comments" },
|
|
6
|
+
{ "include": "#system-variables" },
|
|
7
|
+
{ "include": "#keywords" },
|
|
8
|
+
{ "include": "#constants" },
|
|
9
|
+
{ "include": "#numbers" },
|
|
10
|
+
{ "include": "#strings" },
|
|
11
|
+
{ "include": "#builtin-functions" },
|
|
12
|
+
{ "include": "#operators" }
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"keywords": {
|
|
16
|
+
"patterns": [
|
|
17
|
+
{
|
|
18
|
+
"name": "keyword.control.deluge",
|
|
19
|
+
"match": "\\b(if|else if|else|for each|break|continue|return|try|catch|throws|in)\\b"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "keyword.other.deluge",
|
|
23
|
+
"match": "\\b(info|sendmail|invokeurl|invokeUrl|invokeapi|invokeAPI|openUrl|postUrl|getUrl|insert|update|delete)\\b"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "storage.type.deluge",
|
|
27
|
+
"match": "\\b(int|long|decimal|float|double|bigint|string|text|bool|boolean|list|map|key|collection|date|time|datetime|void|file|xml)\\b"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"system-variables": {
|
|
32
|
+
"patterns": [
|
|
33
|
+
{
|
|
34
|
+
"name": "variable.language.deluge",
|
|
35
|
+
"match": "\\b(zoho\\.(currentdate|currenttime|loginuser(\\.name)?|loginuserid|adminuser|adminuserid|appname|appuri|environment|ipaddress|device\\.type))\\b"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"constants": {
|
|
40
|
+
"patterns": [
|
|
41
|
+
{
|
|
42
|
+
"name": "constant.language.deluge",
|
|
43
|
+
"match": "\\b(true|false|null)\\b"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"numbers": {
|
|
48
|
+
"patterns": [
|
|
49
|
+
{
|
|
50
|
+
"name": "constant.numeric.deluge",
|
|
51
|
+
"match": "\\b([0-9]+\\.[0-9]+|[0-9]+)\\b"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"strings": {
|
|
56
|
+
"patterns": [
|
|
57
|
+
{
|
|
58
|
+
"name": "string.quoted.double.deluge",
|
|
59
|
+
"begin": "\"",
|
|
60
|
+
"end": "\"",
|
|
61
|
+
"patterns": [
|
|
62
|
+
{
|
|
63
|
+
"name": "constant.character.escape.deluge",
|
|
64
|
+
"match": "\\\\."
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "string.quoted.single.deluge",
|
|
70
|
+
"begin": "'",
|
|
71
|
+
"end": "'",
|
|
72
|
+
"patterns": [
|
|
73
|
+
{
|
|
74
|
+
"name": "constant.character.escape.deluge",
|
|
75
|
+
"match": "\\\\."
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"comments": {
|
|
82
|
+
"patterns": [
|
|
83
|
+
{
|
|
84
|
+
"name": "comment.line.double-slash.deluge",
|
|
85
|
+
"match": "//.*$"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "comment.block.deluge",
|
|
89
|
+
"begin": "/\\*",
|
|
90
|
+
"end": "\\*/"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"builtin-functions": {
|
|
95
|
+
"patterns": [
|
|
96
|
+
{
|
|
97
|
+
"name": "support.function.deluge",
|
|
98
|
+
"match": "\\b(zoho)(\\.[a-zA-Z0-9_]+)+\\b"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"match": "\\.([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
|
102
|
+
"captures": {
|
|
103
|
+
"1": { "name": "support.function.member.deluge" }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "entity.name.function.deluge",
|
|
108
|
+
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"operators": {
|
|
113
|
+
"patterns": [
|
|
114
|
+
{
|
|
115
|
+
"name": "keyword.operator.deluge",
|
|
116
|
+
"match": "(\\+=|-=|\\*=|/=|%=|==|!=|<=|>=|&&|\\|\\||[=<>+\\-*/%!?:])"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"scopeName": "source.deluge"
|
|
122
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comments": {
|
|
3
|
+
"lineComment": "//",
|
|
4
|
+
"blockComment": [ "/*", "*/" ]
|
|
5
|
+
},
|
|
6
|
+
"brackets": [
|
|
7
|
+
["{", "}"],
|
|
8
|
+
["[", "]"],
|
|
9
|
+
["(", ")"]
|
|
10
|
+
],
|
|
11
|
+
"autoClosingPairs": [
|
|
12
|
+
{ "open": "{", "close": "}" },
|
|
13
|
+
{ "open": "[", "close": "]" },
|
|
14
|
+
{ "open": "(", "close": ")" },
|
|
15
|
+
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
|
16
|
+
{ "open": "'", "close": "'", "notIn": ["string", "comment"] }
|
|
17
|
+
],
|
|
18
|
+
"surroundingPairs": [
|
|
19
|
+
["{", "}"],
|
|
20
|
+
["[", "]"],
|
|
21
|
+
["(", ")"],
|
|
22
|
+
["\"", "\""],
|
|
23
|
+
["'", "'"]
|
|
24
|
+
],
|
|
25
|
+
"indentationRules": {
|
|
26
|
+
"increaseIndentPattern": "^.*\\{[^}\"']*$|^.*\\([^)\"']*$|^.*\\[[^\\]\"']*$",
|
|
27
|
+
"decreaseIndentPattern": "^\\s*(\\s*\\/[/*].*\\*\\/\\s*)*[})\\]]"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
{
|
|
2
|
+
"For Each Loop": {
|
|
3
|
+
"prefix": "foreach",
|
|
4
|
+
"body": [
|
|
5
|
+
"for each ${1:item} in ${2:list}",
|
|
6
|
+
"{",
|
|
7
|
+
"\t$0",
|
|
8
|
+
"}"
|
|
9
|
+
],
|
|
10
|
+
"description": "Iterate over each element of a list with a for each loop."
|
|
11
|
+
},
|
|
12
|
+
"If Block": {
|
|
13
|
+
"prefix": "if",
|
|
14
|
+
"body": [
|
|
15
|
+
"if (${1:condition})",
|
|
16
|
+
"{",
|
|
17
|
+
"\t$0",
|
|
18
|
+
"}"
|
|
19
|
+
],
|
|
20
|
+
"description": "If block."
|
|
21
|
+
},
|
|
22
|
+
"If Else Block": {
|
|
23
|
+
"prefix": "ifelse",
|
|
24
|
+
"body": [
|
|
25
|
+
"if (${1:condition})",
|
|
26
|
+
"{",
|
|
27
|
+
"\t$2",
|
|
28
|
+
"}",
|
|
29
|
+
"else",
|
|
30
|
+
"{",
|
|
31
|
+
"\t$0",
|
|
32
|
+
"}"
|
|
33
|
+
],
|
|
34
|
+
"description": "If / else block."
|
|
35
|
+
},
|
|
36
|
+
"Try Catch": {
|
|
37
|
+
"prefix": "trycatch",
|
|
38
|
+
"body": [
|
|
39
|
+
"try",
|
|
40
|
+
"{",
|
|
41
|
+
"\t$1",
|
|
42
|
+
"}",
|
|
43
|
+
"catch (${2:e})",
|
|
44
|
+
"{",
|
|
45
|
+
"\tinfo ${2:e};",
|
|
46
|
+
"\t$0",
|
|
47
|
+
"}"
|
|
48
|
+
],
|
|
49
|
+
"description": "Try / catch block with the exception logged via info."
|
|
50
|
+
},
|
|
51
|
+
"Invoke URL (GET)": {
|
|
52
|
+
"prefix": "invokeurlget",
|
|
53
|
+
"body": [
|
|
54
|
+
"${1:response} = invokeurl",
|
|
55
|
+
"[",
|
|
56
|
+
"\turl : \"${2:https://example.com/api}\"",
|
|
57
|
+
"\ttype : GET",
|
|
58
|
+
"\tconnection : \"${3:my_connection}\"",
|
|
59
|
+
"];",
|
|
60
|
+
"$0"
|
|
61
|
+
],
|
|
62
|
+
"description": "Make a GET request with invokeurl."
|
|
63
|
+
},
|
|
64
|
+
"Invoke URL (POST)": {
|
|
65
|
+
"prefix": "invokeurlpost",
|
|
66
|
+
"body": [
|
|
67
|
+
"${1:params} = Map();",
|
|
68
|
+
"${1:params}.put(\"${2:key}\", ${3:value});",
|
|
69
|
+
"${4:response} = invokeurl",
|
|
70
|
+
"[",
|
|
71
|
+
"\turl : \"${5:https://example.com/api}\"",
|
|
72
|
+
"\ttype : POST",
|
|
73
|
+
"\tparameters : ${1:params}",
|
|
74
|
+
"\tconnection : \"${6:my_connection}\"",
|
|
75
|
+
"];",
|
|
76
|
+
"$0"
|
|
77
|
+
],
|
|
78
|
+
"description": "Make a POST request with invokeurl, including parameters and a connection."
|
|
79
|
+
},
|
|
80
|
+
"Invoke API": {
|
|
81
|
+
"prefix": "invokeapi",
|
|
82
|
+
"body": [
|
|
83
|
+
"${1:response} = invokeapi",
|
|
84
|
+
"[",
|
|
85
|
+
"\tservice : ${2:ZohoCRM}",
|
|
86
|
+
"\tpath : \"${3:crm/v3/Leads}\"",
|
|
87
|
+
"\ttype : ${4:GET}",
|
|
88
|
+
"\tconnection : \"${5:my_connection}\"",
|
|
89
|
+
"];",
|
|
90
|
+
"$0"
|
|
91
|
+
],
|
|
92
|
+
"description": "Call a Zoho service with invokeapi using service and path."
|
|
93
|
+
},
|
|
94
|
+
"Send Mail": {
|
|
95
|
+
"prefix": "sendmail",
|
|
96
|
+
"body": [
|
|
97
|
+
"sendmail",
|
|
98
|
+
"[",
|
|
99
|
+
"\tfrom : zoho.adminuserid",
|
|
100
|
+
"\tto : \"${1:recipient@example.com}\"",
|
|
101
|
+
"\tsubject : \"${2:Subject}\"",
|
|
102
|
+
"\tmessage : \"${3:Message body}\"",
|
|
103
|
+
"];",
|
|
104
|
+
"$0"
|
|
105
|
+
],
|
|
106
|
+
"description": "Send an email with the sendmail task."
|
|
107
|
+
},
|
|
108
|
+
"Info": {
|
|
109
|
+
"prefix": "info",
|
|
110
|
+
"body": [
|
|
111
|
+
"info ${1:value};"
|
|
112
|
+
],
|
|
113
|
+
"description": "Print a value to the console with info."
|
|
114
|
+
},
|
|
115
|
+
"Get Record By Id": {
|
|
116
|
+
"prefix": "getrecord",
|
|
117
|
+
"body": [
|
|
118
|
+
"${3:record} = zoho.crm.getRecordById(\"${1:Module}\", ${2:recordId});",
|
|
119
|
+
"$0"
|
|
120
|
+
],
|
|
121
|
+
"description": "Fetch a single CRM record by its id."
|
|
122
|
+
},
|
|
123
|
+
"Search Records": {
|
|
124
|
+
"prefix": "searchrecords",
|
|
125
|
+
"body": [
|
|
126
|
+
"${3:records} = zoho.crm.searchRecords(\"${1:Module}\", \"${2:(Field:equals:value)}\");",
|
|
127
|
+
"$0"
|
|
128
|
+
],
|
|
129
|
+
"description": "Search CRM records using search criteria."
|
|
130
|
+
},
|
|
131
|
+
"Create Record": {
|
|
132
|
+
"prefix": "createrecord",
|
|
133
|
+
"body": [
|
|
134
|
+
"${2:dataMap} = Map();",
|
|
135
|
+
"${2:dataMap}.put(\"${3:Field}\", ${4:value});",
|
|
136
|
+
"${5:response} = zoho.crm.createRecord(\"${1:Module}\", ${2:dataMap});",
|
|
137
|
+
"$0"
|
|
138
|
+
],
|
|
139
|
+
"description": "Build a data map and create a CRM record."
|
|
140
|
+
},
|
|
141
|
+
"Update Record": {
|
|
142
|
+
"prefix": "updaterecord",
|
|
143
|
+
"body": [
|
|
144
|
+
"${4:response} = zoho.crm.updateRecord(\"${1:Module}\", ${2:recordId}, ${3:dataMap});",
|
|
145
|
+
"$0"
|
|
146
|
+
],
|
|
147
|
+
"description": "Update an existing CRM record by id."
|
|
148
|
+
},
|
|
149
|
+
"New Map": {
|
|
150
|
+
"prefix": "mapnew",
|
|
151
|
+
"body": [
|
|
152
|
+
"${1:myMap} = Map();",
|
|
153
|
+
"${1:myMap}.put(\"${2:key}\", ${3:value});",
|
|
154
|
+
"$0"
|
|
155
|
+
],
|
|
156
|
+
"description": "Create a Map and put a key/value pair."
|
|
157
|
+
},
|
|
158
|
+
"New List": {
|
|
159
|
+
"prefix": "listnew",
|
|
160
|
+
"body": [
|
|
161
|
+
"${1:myList} = List();",
|
|
162
|
+
"${1:myList}.add(${2:value});",
|
|
163
|
+
"$0"
|
|
164
|
+
],
|
|
165
|
+
"description": "Create a List and add an element."
|
|
166
|
+
}
|
|
167
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wanasapps/deluge-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deluge language tooling for every Zoho product — formatter, source transforms, TextMate grammar, snippets and an editor-agnostic linter. No I/O, no HTTP, no editor dependency.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"assets",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node test/run.js",
|
|
14
|
+
"prepublishOnly": "node test/run.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"deluge",
|
|
18
|
+
"zoho",
|
|
19
|
+
"zoho-crm",
|
|
20
|
+
"zoho-creator",
|
|
21
|
+
"formatter",
|
|
22
|
+
"linter",
|
|
23
|
+
"textmate",
|
|
24
|
+
"grammar",
|
|
25
|
+
"language"
|
|
26
|
+
],
|
|
27
|
+
"author": "Wanas Apps FZ-LLC",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/Wanas-Apps/deluge-core.git"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-agnostic analysis.
|
|
3
|
+
*
|
|
4
|
+
* Returns plain objects — `{ line, column, endLine, endColumn, message, severity, code }`
|
|
5
|
+
* — so a VS Code provider can map them to vscode.Diagnostic, a CLI can print
|
|
6
|
+
* them, and CI can fail a build on them. Nothing here imports an editor API.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SEVERITY = { ERROR: 'error', WARNING: 'warning', INFO: 'info' };
|
|
10
|
+
|
|
11
|
+
/** Strip strings and comments so brace/paren counting is not fooled by literals. */
|
|
12
|
+
function mask(source) {
|
|
13
|
+
let out = '';
|
|
14
|
+
let inS = null;
|
|
15
|
+
let inLine = false;
|
|
16
|
+
let inBlock = false;
|
|
17
|
+
for (let i = 0; i < source.length; i++) {
|
|
18
|
+
const c = source[i];
|
|
19
|
+
const n = source[i + 1];
|
|
20
|
+
if (inLine) { if (c === '\n') { inLine = false; out += c; } else out += ' '; continue; }
|
|
21
|
+
if (inBlock) { if (c === '*' && n === '/') { inBlock = false; out += ' '; i++; } else out += c === '\n' ? c : ' '; continue; }
|
|
22
|
+
if (inS) { out += c === '\n' ? c : ' '; if (c === inS && source[i - 1] !== '\\') inS = null; continue; }
|
|
23
|
+
if (c === '/' && n === '/') { inLine = true; out += ' '; i++; continue; }
|
|
24
|
+
if (c === '/' && n === '*') { inBlock = true; out += ' '; i++; continue; }
|
|
25
|
+
if (c === '"' || c === "'") { inS = c; out += ' '; continue; }
|
|
26
|
+
out += c;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Structural checks that do not need a full parser and are safe to run on any
|
|
33
|
+
* Deluge source: unbalanced delimiters, and the leading-comment header that
|
|
34
|
+
* Zoho's raw code endpoints reject.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} source
|
|
37
|
+
* @returns {Array<{line:number,column:number,message:string,severity:string,code:string}>}
|
|
38
|
+
*/
|
|
39
|
+
function lint(source) {
|
|
40
|
+
const problems = [];
|
|
41
|
+
const text = String(source == null ? '' : source);
|
|
42
|
+
const masked = mask(text);
|
|
43
|
+
const lines = masked.split('\n');
|
|
44
|
+
|
|
45
|
+
const stack = [];
|
|
46
|
+
const PAIRS = { '{': '}', '(': ')', '[': ']' };
|
|
47
|
+
const CLOSERS = { '}': '{', ')': '(', ']': '[' };
|
|
48
|
+
lines.forEach((line, li) => {
|
|
49
|
+
for (let ci = 0; ci < line.length; ci++) {
|
|
50
|
+
const ch = line[ci];
|
|
51
|
+
if (PAIRS[ch]) stack.push({ ch, line: li + 1, column: ci + 1 });
|
|
52
|
+
else if (CLOSERS[ch]) {
|
|
53
|
+
const top = stack.pop();
|
|
54
|
+
if (!top) {
|
|
55
|
+
problems.push({ line: li + 1, column: ci + 1, message: `Unmatched "${ch}"`, severity: SEVERITY.ERROR, code: 'unmatched-close' });
|
|
56
|
+
} else if (top.ch !== CLOSERS[ch]) {
|
|
57
|
+
problems.push({ line: li + 1, column: ci + 1, message: `Expected "${PAIRS[top.ch]}" to close "${top.ch}" opened at line ${top.line}, found "${ch}"`, severity: SEVERITY.ERROR, code: 'mismatched-close' });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
for (const open of stack) {
|
|
63
|
+
problems.push({ line: open.line, column: open.column, message: `Unclosed "${open.ch}"`, severity: SEVERITY.ERROR, code: 'unclosed' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The leading-header trap: harmless in the web editor, fatal on the raw code
|
|
67
|
+
// endpoints. A warning rather than an error, because pushing through a tool
|
|
68
|
+
// that calls stripLeadingComments is perfectly safe.
|
|
69
|
+
const raw = text.split('\n');
|
|
70
|
+
for (let i = 0; i < raw.length; i++) {
|
|
71
|
+
const t = raw[i].trim();
|
|
72
|
+
if (t === '') continue;
|
|
73
|
+
if (t.startsWith('//')) {
|
|
74
|
+
problems.push({
|
|
75
|
+
line: i + 1,
|
|
76
|
+
column: 1,
|
|
77
|
+
message: 'Leading comment header: Zoho\'s raw code endpoints read everything before the first "(" as the package name and reject this with a bodyless 400. Strip it on push (deluge-core does this for you).',
|
|
78
|
+
severity: SEVERITY.WARNING,
|
|
79
|
+
code: 'leading-comment-header'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return problems;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True when lint found nothing of `error` severity. */
|
|
89
|
+
function isValid(source) {
|
|
90
|
+
return !lint(source).some((p) => p.severity === SEVERITY.ERROR);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { lint, isValid, SEVERITY, _internal: { mask } };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic formatter for Zoho Deluge scripts.
|
|
3
|
+
* Adjusts indentation based on block scopes '{' and '}'.
|
|
4
|
+
* Note: This is a simple line-based formatter and does not fully parse the AST.
|
|
5
|
+
*/
|
|
6
|
+
function formatDeluge(code) {
|
|
7
|
+
if (!code || typeof code !== 'string') return code;
|
|
8
|
+
|
|
9
|
+
let formatted = '';
|
|
10
|
+
let indentLevel = 0;
|
|
11
|
+
const indentString = ' '; // 4 spaces
|
|
12
|
+
|
|
13
|
+
// Replace standard block formatting to ensure newlines before/after braces if it's on one line
|
|
14
|
+
// (We skip deep parsing, assuming the code from Zoho generally has basic newlines)
|
|
15
|
+
const lines = code.split('\n');
|
|
16
|
+
|
|
17
|
+
for (let line of lines) {
|
|
18
|
+
line = line.trim();
|
|
19
|
+
|
|
20
|
+
// Preserve empty lines
|
|
21
|
+
if (!line) {
|
|
22
|
+
formatted += '\n';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Determine if this line closes a block
|
|
27
|
+
if (line.startsWith('}') || line.startsWith(']')) {
|
|
28
|
+
indentLevel = Math.max(0, indentLevel - 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Apply indentation
|
|
32
|
+
formatted += indentString.repeat(indentLevel) + line + '\n';
|
|
33
|
+
|
|
34
|
+
// Determine if this line opens a new block
|
|
35
|
+
if (line.endsWith('{') || line.endsWith('[')) {
|
|
36
|
+
indentLevel++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return formatted.trim() + '\n';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
formatDeluge
|
|
45
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal parser for the *signature line* of a Deluge function script.
|
|
3
|
+
*
|
|
4
|
+
* Zoho stores a function's code with its signature on the first meaningful
|
|
5
|
+
* line, e.g.:
|
|
6
|
+
*
|
|
7
|
+
* string standalone.Get_Assistants()
|
|
8
|
+
* void automation.k_test(String Data)
|
|
9
|
+
*
|
|
10
|
+
* We only need the signature to learn the function's namespace + api name (to
|
|
11
|
+
* build the test URL and enforce the standalone-only rule) and its declared
|
|
12
|
+
* arguments (to prompt for values). The full script is still submitted verbatim
|
|
13
|
+
* to Zoho — this parser never rewrites the code.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse the signature of a Deluge script.
|
|
18
|
+
* @param {string} scriptText - The full Deluge source.
|
|
19
|
+
* @returns {{returnType: string|null, namespace: string|null, apiName: string|null, args: Array<{type:string,name:string}>}}
|
|
20
|
+
*/
|
|
21
|
+
function parseSignature(scriptText) {
|
|
22
|
+
const result = { returnType: null, namespace: null, apiName: null, args: [] };
|
|
23
|
+
if (!scriptText || typeof scriptText !== 'string') return result;
|
|
24
|
+
|
|
25
|
+
// Find the first non-comment, non-blank line — that's the signature.
|
|
26
|
+
let sigLine = null;
|
|
27
|
+
let inBlockComment = false;
|
|
28
|
+
for (const raw of scriptText.split('\n')) {
|
|
29
|
+
let line = raw.trim();
|
|
30
|
+
if (!line) continue;
|
|
31
|
+
|
|
32
|
+
if (inBlockComment) {
|
|
33
|
+
const end = line.indexOf('*/');
|
|
34
|
+
if (end === -1) continue;
|
|
35
|
+
line = line.slice(end + 2).trim();
|
|
36
|
+
inBlockComment = false;
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
}
|
|
39
|
+
if (line.startsWith('//')) continue;
|
|
40
|
+
if (line.startsWith('/*')) {
|
|
41
|
+
const end = line.indexOf('*/');
|
|
42
|
+
if (end === -1) { inBlockComment = true; continue; }
|
|
43
|
+
line = line.slice(end + 2).trim();
|
|
44
|
+
if (!line) continue;
|
|
45
|
+
}
|
|
46
|
+
sigLine = line;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (!sigLine) return result;
|
|
50
|
+
|
|
51
|
+
// <returnType> <namespace>.<apiName>(<args>)
|
|
52
|
+
const m = sigLine.match(/^(\w+)\s+([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\s*\(([^)]*)\)/);
|
|
53
|
+
if (!m) return result;
|
|
54
|
+
|
|
55
|
+
result.returnType = m[1];
|
|
56
|
+
result.namespace = m[2];
|
|
57
|
+
result.apiName = m[3];
|
|
58
|
+
|
|
59
|
+
const argsRaw = m[4].trim();
|
|
60
|
+
if (argsRaw) {
|
|
61
|
+
for (const part of argsRaw.split(',')) {
|
|
62
|
+
// each param is "<type> <name>" (e.g. "String Data", "int count")
|
|
63
|
+
const am = part.trim().match(/^(\S+)\s+([A-Za-z0-9_]+)$/);
|
|
64
|
+
if (am) result.args.push({ type: am[1], name: am[2] });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { parseSignature };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deluge source-text transforms that any tool pushing or storing a script needs,
|
|
3
|
+
* independent of which Zoho product the script belongs to.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Blank out a leading `//` comment header before sending a script to Zoho.
|
|
8
|
+
*
|
|
9
|
+
* Zoho's raw code endpoints parse everything before the first `(` as the package
|
|
10
|
+
* name, without skipping comments — so a script that opens with a `//` header is
|
|
11
|
+
* rejected with a generic, bodyless 400 that says nothing about the real cause.
|
|
12
|
+
* The web editor tolerates the same header; only these endpoints choke.
|
|
13
|
+
*
|
|
14
|
+
* Blank lines ARE tolerated, so the leading comment lines are emptied rather than
|
|
15
|
+
* removed: line numbers in any COMPILATION_ERROR Zoho returns then still line up
|
|
16
|
+
* with the developer's local file. Interior comments are left alone.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} script
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function stripLeadingComments(script) {
|
|
22
|
+
const lines = String(script).split('\n');
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const t = lines[i].trim();
|
|
25
|
+
if (t === '') continue;
|
|
26
|
+
if (!t.startsWith('//')) break;
|
|
27
|
+
lines[i] = lines[i].endsWith('\r') ? '\r' : '';
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Zoho names function categories in prose ("Related List"); on disk they become
|
|
34
|
+
* lowercase, underscored namespace folders. Keeping this here means every tool
|
|
35
|
+
* writes the same `functions/<namespace>/` layout.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} category
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function namespaceForCategory(category) {
|
|
41
|
+
const cat = String(category || '').toLowerCase().trim();
|
|
42
|
+
const direct = ['standalone', 'automation', 'button', 'validation', 'schedule', 'blueprint'];
|
|
43
|
+
if (direct.includes(cat)) return cat;
|
|
44
|
+
if (cat === 'related list' || cat === 'related_list') return 'related_list';
|
|
45
|
+
return cat.replace(/\s+/g, '_') || 'standalone';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Conventional on-disk path for a function's source file. */
|
|
49
|
+
function dsPath(apiName, category) {
|
|
50
|
+
if (!apiName) throw new Error('apiName is required');
|
|
51
|
+
return `${namespaceForCategory(category)}/${apiName}.ds`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Recover the function's api name from a .ds path or filename. */
|
|
55
|
+
function apiNameFromDsPath(file) {
|
|
56
|
+
const base = String(file).split(/[\\/]/).pop() || '';
|
|
57
|
+
return base.replace(/\.ds$/i, '').split('.').pop();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The runtime string Zoho requires when creating a function. */
|
|
61
|
+
const DELUGE_RUNTIME = 'Deluge 1.0';
|
|
62
|
+
|
|
63
|
+
module.exports = { stripLeadingComments, namespaceForCategory, dsPath, apiNameFromDsPath, DELUGE_RUNTIME };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wanasapps/deluge-core — Deluge language tooling, independent of any Zoho product.
|
|
3
|
+
*
|
|
4
|
+
* Deluge is not a CRM feature: it runs in CRM, Creator, Books, Inventory,
|
|
5
|
+
* Projects, Sprints, Qntrl, Catalyst and People. This package holds everything
|
|
6
|
+
* that is true about the LANGUAGE, so a CLI, a VS Code extension and a browser
|
|
7
|
+
* editor can share one implementation.
|
|
8
|
+
*
|
|
9
|
+
* Hard rule for everything in here: NO I/O, NO HTTP, NO editor APIs. Pure
|
|
10
|
+
* functions over source text, plus portable data assets. Anything that talks to
|
|
11
|
+
* Zoho belongs in the API layer, and anything that talks to an editor belongs in
|
|
12
|
+
* that editor's adapter.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { formatDeluge } = require('./format/formatter');
|
|
16
|
+
const { parseSignature } = require('./format/signature');
|
|
17
|
+
const source = require('./format/source');
|
|
18
|
+
const language = require('./language');
|
|
19
|
+
const analyze = require('./analyze');
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
// formatting
|
|
23
|
+
format: formatDeluge,
|
|
24
|
+
formatDeluge,
|
|
25
|
+
|
|
26
|
+
// source transforms + on-disk conventions
|
|
27
|
+
parseSignature,
|
|
28
|
+
...source,
|
|
29
|
+
|
|
30
|
+
// language data (builtins, catalog) and portable editor assets
|
|
31
|
+
language,
|
|
32
|
+
|
|
33
|
+
// editor-agnostic analysis engines
|
|
34
|
+
analyze
|
|
35
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable language assets.
|
|
3
|
+
*
|
|
4
|
+
* The grammar, language configuration and snippets are the same JSON files VS
|
|
5
|
+
* Code consumes, exposed as data so Monaco, Shiki, a docs site or a web editor
|
|
6
|
+
* can load them without depending on an extension.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const ASSETS = path.join(__dirname, '..', '..', 'assets');
|
|
12
|
+
const read = (f) => JSON.parse(fs.readFileSync(path.join(ASSETS, f), 'utf8'));
|
|
13
|
+
|
|
14
|
+
/** File extensions Zoho and our tooling use for Deluge source. */
|
|
15
|
+
const EXTENSIONS = ['.ds', '.dg', '.deluge'];
|
|
16
|
+
|
|
17
|
+
/** TextMate grammar — VS Code, Monaco (via monaco-textmate), Shiki. */
|
|
18
|
+
function grammar() {
|
|
19
|
+
return read('deluge.tmLanguage.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Brackets, comment tokens, auto-closing pairs. */
|
|
23
|
+
function configuration() {
|
|
24
|
+
return read('language-configuration.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Snippet definitions in the standard VS Code snippet shape. */
|
|
28
|
+
function snippets() {
|
|
29
|
+
return read('snippets.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Absolute paths, for tools that must point a consumer at the raw files. */
|
|
33
|
+
const assetPaths = {
|
|
34
|
+
grammar: path.join(ASSETS, 'deluge.tmLanguage.json'),
|
|
35
|
+
configuration: path.join(ASSETS, 'language-configuration.json'),
|
|
36
|
+
snippets: path.join(ASSETS, 'snippets.json')
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
module.exports = { EXTENSIONS, grammar, configuration, snippets, assetPaths };
|