@kcvabeysinghe/chsl 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/MANIFEST.in +2 -0
- package/README.md +139 -0
- package/chsl.js +258 -0
- package/chsl.py +273 -0
- package/package.json +21 -0
- package/pyproject.toml +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chethana
|
|
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/MANIFEST.in
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# CHSL (Chethana's Human-readable Simple Language)
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
**The configuration language designed for humans, not parsers.**
|
|
10
|
+
|
|
11
|
+
CHSL is a lightweight, strictly-typed, and highly readable configuration language. It was built from the ground up to be as simple and predictable as possible for humans to read and write.
|
|
12
|
+
|
|
13
|
+
## Why use CHSL?
|
|
14
|
+
|
|
15
|
+
1. **No quotes, no syntax headaches** — just write your values naturally and CHSL figures out the type automatically.
|
|
16
|
+
2. **Comments that actually work** — document your config files properly with `NOTE:` so future you knows why something is set.
|
|
17
|
+
3. **Secrets done right** — pull API keys and passwords directly from the operating system with `COPY_ENV`. If the secret is missing, it crashes immediately before anything goes wrong.
|
|
18
|
+
4. **Write once, reuse anywhere** — the `COPY` system lets you define a value once and reference it everywhere. Change it in one place, it updates everywhere.
|
|
19
|
+
5. **Readable multi-line text** — write long text blocks with numbered lines instead of cramming everything into one unreadable line.
|
|
20
|
+
6. **Protect special strings with PIN** — passwords, zip codes, and ID numbers that look like numbers stay exactly as you wrote them.
|
|
21
|
+
7. **Split big configs into smaller files** — use `LOAD` to break massive configuration files into clean, logical chunks.
|
|
22
|
+
8. **Never lose track of nesting** — the `0Group0` bracket system makes it visually impossible to lose track of which scope you're in, no matter how deep.
|
|
23
|
+
9. **Short or long arrays, your choice** — write quick inline arrays with `ARE` or detailed bullet lists with `#` depending on what's more readable.
|
|
24
|
+
10. **Built-in safety** — circular file dependencies and directory traversal attacks are blocked at the language level, not the application level.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features & Syntax
|
|
29
|
+
|
|
30
|
+
### 1. Variables and Data Types
|
|
31
|
+
No quotes are required. CHSL automatically understands text, numbers, booleans (YES/NO), and empty values.
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
NOTE: This is a comment.
|
|
35
|
+
server_name = Oxide Main Server
|
|
36
|
+
is_active = YES
|
|
37
|
+
api_token = EMPTY
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. The PIN System (Strict Text)
|
|
41
|
+
Keep leading zeros on passwords, phone numbers, or ZIP codes by using `PIN`.
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
admin_password = PIN 0042
|
|
45
|
+
```
|
|
46
|
+
*(Parsed as the exact string `"0042"`, not the number `42`)*
|
|
47
|
+
|
|
48
|
+
### 3. Arrays in CHSL (Two Ways)
|
|
49
|
+
CHSL provides two ways to write lists, designed for different situations.
|
|
50
|
+
|
|
51
|
+
**Method A: Smart Lists (The `#` bullet points)**
|
|
52
|
+
* **Best for:** Long lists, or strings that naturally contain spaces and commas.
|
|
53
|
+
```text
|
|
54
|
+
allowed_ports =
|
|
55
|
+
#8080
|
|
56
|
+
#8081
|
|
57
|
+
#PIN 0021
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Method B: Inline Arrays (The `ARE` keyword)**
|
|
61
|
+
* **Best for:** Short, simple data on a single line.
|
|
62
|
+
* **Note:** Commas strictly separate items. If your text naturally contains spaces or commas, use Method A (`#` bullet lists) instead. If you use `Smith_John` to avoid spaces, it will be parsed literally as `"Smith_John"`.
|
|
63
|
+
```text
|
|
64
|
+
0Users0
|
|
65
|
+
name ARE Alice, Bob, Charlie
|
|
66
|
+
age ARE 30, EMPTY, 25
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Explicit Multi-line Text (Paragraphs)
|
|
70
|
+
Write multi-line text naturally using explicit, numbered lines.
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
welcome_message = LINE
|
|
74
|
+
1 = Hello there!
|
|
75
|
+
2 = Welcome to the server.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. Infinite, Explicit Nesting
|
|
79
|
+
CHSL uses numbered headers to change the current group scope. You don't need closing tags; declaring a new group automatically shifts the level.
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
0Network0
|
|
83
|
+
bind_ip = 192.168.1.1
|
|
84
|
+
|
|
85
|
+
1Access1
|
|
86
|
+
block_guest = YES
|
|
87
|
+
|
|
88
|
+
0Database0
|
|
89
|
+
port = 5432
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 6. DRY Configs (The COPY System)
|
|
93
|
+
Don't repeat yourself. CHSL allows you to copy values from other keys in the file.
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
active_port = 8080
|
|
97
|
+
backup_port = COPY active_port
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 7. Native OS Secrets
|
|
101
|
+
Never hardcode API keys in your config files again. CHSL securely pulls secrets directly from the operating system environment.
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
stripe_api_key = COPY_ENV STRIPE_SECRET_KEY
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 8. Modularity (File Loading)
|
|
108
|
+
Split massive configuration files into smaller chunks. Circular dependency detection and directory traversal protection are built-in automatically.
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
LOAD weapons.chsl
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Usage
|
|
117
|
+
CHSL provides perfect compatibility across **Python** and **JavaScript (Node.js)**.
|
|
118
|
+
|
|
119
|
+
### Python (Install via PyPI)
|
|
120
|
+
```bash
|
|
121
|
+
pip install chsl
|
|
122
|
+
```
|
|
123
|
+
```python
|
|
124
|
+
from chsl import parse_chsl_file, write_chsl_file
|
|
125
|
+
|
|
126
|
+
config = parse_chsl_file("settings.chsl")
|
|
127
|
+
write_chsl_file(config, "new_settings.chsl")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### JavaScript / Node.js (Install via npm)
|
|
131
|
+
```bash
|
|
132
|
+
npm install chsl
|
|
133
|
+
```
|
|
134
|
+
```javascript
|
|
135
|
+
const CHSL = require('chsl');
|
|
136
|
+
|
|
137
|
+
const config = CHSL.parseCHSLFile('settings.chsl');
|
|
138
|
+
CHSL.writeCHSLFile(config, 'new_settings.chsl');
|
|
139
|
+
```
|
package/chsl.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// ==========================================
|
|
5
|
+
// PHASE 1: THE CHSL READER (Parser)
|
|
6
|
+
// ==========================================
|
|
7
|
+
function parseCHSL(text, currentDir = ".", _visited = new Set()) {
|
|
8
|
+
const lines = text.split('\n');
|
|
9
|
+
const config = {};
|
|
10
|
+
const scopePath = { "-1": config };
|
|
11
|
+
let currentLevel = -1;
|
|
12
|
+
|
|
13
|
+
let state = "NORMAL";
|
|
14
|
+
let currentKey = null;
|
|
15
|
+
let currentList = [];
|
|
16
|
+
let currentLines = [];
|
|
17
|
+
let expectedLine = 1;
|
|
18
|
+
|
|
19
|
+
// Absolute path for security with trailing separator
|
|
20
|
+
let baseDir = path.resolve(currentDir);
|
|
21
|
+
if (!baseDir.endsWith(path.sep)) baseDir += path.sep;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i].trim();
|
|
25
|
+
const actualLineNum = i + 1;
|
|
26
|
+
|
|
27
|
+
// Ignore empty lines and comments
|
|
28
|
+
if (!line || line.startsWith("NOTE:")) continue;
|
|
29
|
+
|
|
30
|
+
// Handle Multi-line Bullet Lists (#)
|
|
31
|
+
if (state === "IN_LIST") {
|
|
32
|
+
if (line.startsWith("#")) {
|
|
33
|
+
const itemVal = line.substring(1).trim();
|
|
34
|
+
if (itemVal === "YES") currentList.push(true);
|
|
35
|
+
else if (itemVal === "NO") currentList.push(false);
|
|
36
|
+
else if (itemVal === "EMPTY") currentList.push(null);
|
|
37
|
+
else if (itemVal.startsWith("PIN ")) currentList.push(itemVal.substring(4).trim());
|
|
38
|
+
else {
|
|
39
|
+
if (/^-?\d+(\.\d+)?$/.test(itemVal)) currentList.push(Number(itemVal));
|
|
40
|
+
else currentList.push(itemVal);
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
} else {
|
|
44
|
+
scopePath[currentLevel][currentKey] = currentList;
|
|
45
|
+
state = "NORMAL";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle Explicit Multi-line Text (LINE)
|
|
50
|
+
if (state === "IN_LINE") {
|
|
51
|
+
const match = line.match(/^(\d+)\s*=\s*(.*)/);
|
|
52
|
+
if (match) {
|
|
53
|
+
const num = parseInt(match[1], 10);
|
|
54
|
+
if (num !== expectedLine) throw new SyntaxError(`CHSL ERROR (Line ${actualLineNum}): Expected '${expectedLine} =', got '${num} ='`);
|
|
55
|
+
currentLines.push(match[2].trim());
|
|
56
|
+
expectedLine++;
|
|
57
|
+
continue;
|
|
58
|
+
} else {
|
|
59
|
+
scopePath[currentLevel][currentKey] = currentLines.join('\n');
|
|
60
|
+
state = "NORMAL";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle File Loading (LOAD)
|
|
65
|
+
if (line.startsWith("LOAD ")) {
|
|
66
|
+
const filename = line.substring(5).trim();
|
|
67
|
+
const filepath = path.resolve(baseDir, filename);
|
|
68
|
+
|
|
69
|
+
// Security: Prevent directory traversal
|
|
70
|
+
if (!filepath.startsWith(baseDir)) {
|
|
71
|
+
throw new Error(`CHSL SECURITY ERROR (Line ${actualLineNum}): Cannot LOAD files outside the current directory!`);
|
|
72
|
+
}
|
|
73
|
+
// Security: Prevent circular dependencies
|
|
74
|
+
if (_visited.has(filepath)) {
|
|
75
|
+
throw new Error(`CHSL ERROR (Line ${actualLineNum}): Circular LOAD detected for '${filename}'`);
|
|
76
|
+
}
|
|
77
|
+
if (!fs.existsSync(filepath)) {
|
|
78
|
+
throw new Error(`CHSL ERROR (Line ${actualLineNum}): LOAD failed for '${filename}'`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const newVisited = new Set(_visited);
|
|
82
|
+
newVisited.add(filepath);
|
|
83
|
+
|
|
84
|
+
const loadedText = fs.readFileSync(filepath, 'utf8');
|
|
85
|
+
Object.assign(scopePath[currentLevel], parseCHSL(loadedText, path.dirname(filepath), newVisited));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle Explicit Nesting Scopes (e.g., 0Group0)
|
|
90
|
+
const groupMatch = line.match(/^(\d+)([a-zA-Z0-9_]+)(\d+)$/);
|
|
91
|
+
if (groupMatch) {
|
|
92
|
+
const levelStart = parseInt(groupMatch[1], 10);
|
|
93
|
+
const name = groupMatch[2];
|
|
94
|
+
const levelEnd = parseInt(groupMatch[3], 10);
|
|
95
|
+
|
|
96
|
+
if (levelStart !== levelEnd) throw new SyntaxError(`CHSL ERROR (Line ${actualLineNum}): Group numbers do not match!`);
|
|
97
|
+
if (levelStart > currentLevel + 1) throw new SyntaxError(`CHSL ERROR (Line ${actualLineNum}): Skipped a group depth!`);
|
|
98
|
+
|
|
99
|
+
const newScope = {};
|
|
100
|
+
scopePath[levelStart - 1][name] = newScope;
|
|
101
|
+
scopePath[levelStart] = newScope;
|
|
102
|
+
currentLevel = levelStart;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle Inline Arrays (ARE) - Anchored strictly to prevent false positives
|
|
107
|
+
const areMatch = line.match(/^([^\s=]+)\s+ARE\s+(.*)/);
|
|
108
|
+
if (areMatch) {
|
|
109
|
+
const key = areMatch[1];
|
|
110
|
+
const valPart = areMatch[2];
|
|
111
|
+
const rawVals = valPart.split(",");
|
|
112
|
+
const parsedList = [];
|
|
113
|
+
|
|
114
|
+
for (let rv of rawVals) {
|
|
115
|
+
let v = rv.trim();
|
|
116
|
+
if (v === "YES") parsedList.push(true);
|
|
117
|
+
else if (v === "NO") parsedList.push(false);
|
|
118
|
+
else if (v === "EMPTY") parsedList.push(null);
|
|
119
|
+
else if (v.startsWith("PIN ")) parsedList.push(v.substring(4).trim());
|
|
120
|
+
else {
|
|
121
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) parsedList.push(Number(v));
|
|
122
|
+
else parsedList.push(v);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
scopePath[currentLevel][key] = parsedList;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle Standard Key-Value Pairs (=)
|
|
130
|
+
if (line.includes("=")) {
|
|
131
|
+
const splitIdx = line.indexOf("=");
|
|
132
|
+
const key = line.substring(0, splitIdx).trim();
|
|
133
|
+
const val = line.substring(splitIdx + 1).trim();
|
|
134
|
+
|
|
135
|
+
if (val === "") {
|
|
136
|
+
state = "IN_LIST";
|
|
137
|
+
currentKey = key;
|
|
138
|
+
currentList = [];
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (val === "LINE") {
|
|
142
|
+
state = "IN_LINE";
|
|
143
|
+
currentKey = key;
|
|
144
|
+
currentLines = [];
|
|
145
|
+
expectedLine = 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let finalVal = null;
|
|
150
|
+
if (val === "YES") finalVal = true;
|
|
151
|
+
else if (val === "NO") finalVal = false;
|
|
152
|
+
else if (val === "EMPTY") finalVal = null;
|
|
153
|
+
else if (val.startsWith("PIN ")) finalVal = val.substring(4).trim();
|
|
154
|
+
else if (val.startsWith("COPY_ENV ")) {
|
|
155
|
+
const envVar = val.substring(9).trim();
|
|
156
|
+
if (!(envVar in process.env)) throw new Error(`CHSL ERROR (Line ${actualLineNum}): Missing Required OS Secret '${envVar}'!`);
|
|
157
|
+
finalVal = process.env[envVar];
|
|
158
|
+
} else if (val.startsWith("COPY ")) {
|
|
159
|
+
const refKey = val.substring(5).trim();
|
|
160
|
+
let found = false;
|
|
161
|
+
for (let lvl = currentLevel; lvl >= -1; lvl--) {
|
|
162
|
+
if (scopePath[lvl] && refKey in scopePath[lvl]) {
|
|
163
|
+
finalVal = scopePath[lvl][refKey];
|
|
164
|
+
found = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!found) throw new Error(`CHSL ERROR (Line ${actualLineNum}): Tried to COPY '${refKey}', but it doesn't exist!`);
|
|
169
|
+
} else {
|
|
170
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) finalVal = Number(val);
|
|
171
|
+
else finalVal = val;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
scopePath[currentLevel][key] = finalVal;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new SyntaxError(`CHSL ERROR (Line ${actualLineNum}): Unrecognized syntax -> '${line}'`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Flush remaining state at end of file
|
|
182
|
+
if (state === "IN_LIST") scopePath[currentLevel][currentKey] = currentList;
|
|
183
|
+
else if (state === "IN_LINE") scopePath[currentLevel][currentKey] = currentLines.join('\n');
|
|
184
|
+
|
|
185
|
+
return config;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseCHSLFile(filepath) {
|
|
189
|
+
const absolutePath = path.resolve(filepath);
|
|
190
|
+
return parseCHSL(fs.readFileSync(absolutePath, 'utf8'), path.dirname(absolutePath));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ==========================================
|
|
194
|
+
// PHASE 2: THE CHSL WRITER (Serializer)
|
|
195
|
+
// ==========================================
|
|
196
|
+
function dumpCHSL(configDict, level = 0) {
|
|
197
|
+
const lines = [];
|
|
198
|
+
for (const [key, value] of Object.entries(configDict)) {
|
|
199
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
200
|
+
lines.push(`\n${level}${key}${level}`);
|
|
201
|
+
lines.push(...dumpCHSL(value, level + 1));
|
|
202
|
+
|
|
203
|
+
} else if (Array.isArray(value)) {
|
|
204
|
+
// Smart Array Serializer: Choose 'ARE' if safe, otherwise fallback to '#'
|
|
205
|
+
const isSafeForAre = value.every(i => typeof i !== 'object' && !(typeof i === 'string' && (i.includes('\n') || i.includes(','))));
|
|
206
|
+
|
|
207
|
+
if (isSafeForAre && value.length > 0) {
|
|
208
|
+
const areItems = value.map(item => {
|
|
209
|
+
if (typeof item === 'boolean') return item ? "YES" : "NO";
|
|
210
|
+
if (item === null) return "EMPTY";
|
|
211
|
+
if (typeof item === 'number') return `${item}`;
|
|
212
|
+
if (typeof item === 'string' && /^-?\d+(\.\d+)?$/.test(item)) return `PIN ${item}`;
|
|
213
|
+
return `${item}`;
|
|
214
|
+
});
|
|
215
|
+
lines.push(`${key} ARE ${areItems.join(', ')}`);
|
|
216
|
+
} else {
|
|
217
|
+
lines.push(`${key} =`);
|
|
218
|
+
for (const item of value) {
|
|
219
|
+
if (typeof item === 'boolean') lines.push(item ? "#YES" : "#NO");
|
|
220
|
+
else if (item === null) lines.push("#EMPTY");
|
|
221
|
+
else if (typeof item === 'number') lines.push(`#${item}`);
|
|
222
|
+
else if (typeof item === 'string' && /^-?\d+(\.\d+)?$/.test(item)) lines.push(`#PIN ${item}`);
|
|
223
|
+
else lines.push(`#${item}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
} else if (typeof value === 'boolean') {
|
|
228
|
+
lines.push(`${key} = ${value ? 'YES' : 'NO'}`);
|
|
229
|
+
|
|
230
|
+
} else if (value === null) {
|
|
231
|
+
lines.push(`${key} = EMPTY`);
|
|
232
|
+
|
|
233
|
+
} else if (typeof value === 'number') {
|
|
234
|
+
lines.push(`${key} = ${value}`);
|
|
235
|
+
|
|
236
|
+
} else if (typeof value === 'string') {
|
|
237
|
+
if (value.includes('\n')) {
|
|
238
|
+
lines.push(`${key} = LINE`);
|
|
239
|
+
const textLines = value.split('\n');
|
|
240
|
+
for (let i = 0; i < textLines.length; i++) lines.push(`${i + 1} = ${textLines[i]}`);
|
|
241
|
+
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
242
|
+
lines.push(`${key} = PIN ${value}`);
|
|
243
|
+
} else {
|
|
244
|
+
lines.push(`${key} = ${value}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function writeCHSLFile(configDict, filename = "output.chsl") {
|
|
252
|
+
const lines = ["NOTE: Automatically generated by the CHSL Engine\n", ...dumpCHSL(configDict)];
|
|
253
|
+
const finalText = lines.join('\n');
|
|
254
|
+
fs.writeFileSync(filename, finalText, 'utf8');
|
|
255
|
+
return finalText;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { parseCHSL, parseCHSLFile, dumpCHSL, writeCHSLFile };
|
package/chsl.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
# ==========================================
|
|
5
|
+
# PHASE 1: THE CHSL READER (Parser)
|
|
6
|
+
# ==========================================
|
|
7
|
+
def parse_chsl(text, current_dir=".", _visited=None):
|
|
8
|
+
if _visited is None:
|
|
9
|
+
_visited = set()
|
|
10
|
+
|
|
11
|
+
lines = text.split('\n')
|
|
12
|
+
config = {}
|
|
13
|
+
scope_path = {-1: config}
|
|
14
|
+
current_level = -1
|
|
15
|
+
|
|
16
|
+
state = "NORMAL"
|
|
17
|
+
current_key = None
|
|
18
|
+
current_list = []
|
|
19
|
+
current_lines = []
|
|
20
|
+
expected_line = 1
|
|
21
|
+
|
|
22
|
+
# Absolute path for security with trailing separator
|
|
23
|
+
base_dir = os.path.abspath(current_dir)
|
|
24
|
+
if not base_dir.endswith(os.sep):
|
|
25
|
+
base_dir += os.sep
|
|
26
|
+
|
|
27
|
+
for line_idx, raw_line in enumerate(lines):
|
|
28
|
+
line = raw_line.strip()
|
|
29
|
+
actual_line_num = line_idx + 1
|
|
30
|
+
|
|
31
|
+
# Ignore empty lines and comments
|
|
32
|
+
if not line or line.startswith("NOTE:"):
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# Handle Multi-line Bullet Lists (#)
|
|
36
|
+
if state == "IN_LIST":
|
|
37
|
+
if line.startswith("#"):
|
|
38
|
+
item_val = line[1:].strip()
|
|
39
|
+
if item_val == "YES":
|
|
40
|
+
current_list.append(True)
|
|
41
|
+
elif item_val == "NO":
|
|
42
|
+
current_list.append(False)
|
|
43
|
+
elif item_val == "EMPTY":
|
|
44
|
+
current_list.append(None)
|
|
45
|
+
elif item_val.startswith("PIN "):
|
|
46
|
+
current_list.append(item_val[4:].strip())
|
|
47
|
+
else:
|
|
48
|
+
try:
|
|
49
|
+
parsed_num = float(item_val) if "." in item_val else int(item_val)
|
|
50
|
+
current_list.append(parsed_num)
|
|
51
|
+
except ValueError:
|
|
52
|
+
current_list.append(item_val)
|
|
53
|
+
continue
|
|
54
|
+
else:
|
|
55
|
+
scope_path[current_level][current_key] = current_list
|
|
56
|
+
state = "NORMAL"
|
|
57
|
+
|
|
58
|
+
# Handle Explicit Multi-line Text (LINE)
|
|
59
|
+
if state == "IN_LINE":
|
|
60
|
+
match = re.match(r"^(\d+)\s*=\s*(.*)", line)
|
|
61
|
+
if match:
|
|
62
|
+
num = int(match.group(1))
|
|
63
|
+
if num != expected_line:
|
|
64
|
+
raise SyntaxError(f"CHSL ERROR (Line {actual_line_num}): Expected '{expected_line} =', got '{num} ='")
|
|
65
|
+
current_lines.append(match.group(2).strip())
|
|
66
|
+
expected_line += 1
|
|
67
|
+
continue
|
|
68
|
+
else:
|
|
69
|
+
scope_path[current_level][current_key] = "\n".join(current_lines)
|
|
70
|
+
state = "NORMAL"
|
|
71
|
+
|
|
72
|
+
# Handle File Loading (LOAD)
|
|
73
|
+
if line.startswith("LOAD "):
|
|
74
|
+
filename = line[5:].strip()
|
|
75
|
+
filepath = os.path.abspath(os.path.join(base_dir, filename))
|
|
76
|
+
|
|
77
|
+
# Security: Prevent directory traversal
|
|
78
|
+
if not filepath.startswith(base_dir):
|
|
79
|
+
raise PermissionError(f"CHSL SECURITY ERROR (Line {actual_line_num}): Cannot LOAD files outside directory!")
|
|
80
|
+
|
|
81
|
+
# Security: Prevent circular dependencies
|
|
82
|
+
if filepath in _visited:
|
|
83
|
+
raise RecursionError(f"CHSL ERROR (Line {actual_line_num}): Circular LOAD detected for '{filename}'")
|
|
84
|
+
|
|
85
|
+
if not os.path.exists(filepath):
|
|
86
|
+
raise FileNotFoundError(f"CHSL ERROR (Line {actual_line_num}): LOAD failed. File '{filename}' not found.")
|
|
87
|
+
|
|
88
|
+
_new_visited = _visited.copy()
|
|
89
|
+
_new_visited.add(filepath)
|
|
90
|
+
|
|
91
|
+
with open(filepath, 'r') as f:
|
|
92
|
+
loaded_config = parse_chsl(f.read(), os.path.dirname(filepath), _new_visited)
|
|
93
|
+
|
|
94
|
+
scope_path[current_level].update(loaded_config)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Handle Explicit Nesting Scopes (e.g., 0Group0)
|
|
98
|
+
group_match = re.match(r"^(\d+)([a-zA-Z0-9_]+)(\d+)$", line)
|
|
99
|
+
if group_match:
|
|
100
|
+
level_start = int(group_match.group(1))
|
|
101
|
+
name = group_match.group(2)
|
|
102
|
+
level_end = int(group_match.group(3))
|
|
103
|
+
|
|
104
|
+
if level_start != level_end:
|
|
105
|
+
raise SyntaxError(f"CHSL ERROR (Line {actual_line_num}): Group numbers do not match!")
|
|
106
|
+
if level_start > current_level + 1:
|
|
107
|
+
raise SyntaxError(f"CHSL ERROR (Line {actual_line_num}): Skipped a group depth!")
|
|
108
|
+
|
|
109
|
+
new_scope = {}
|
|
110
|
+
scope_path[level_start - 1][name] = new_scope
|
|
111
|
+
scope_path[level_start] = new_scope
|
|
112
|
+
current_level = level_start
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Handle Inline Arrays (ARE) - Anchored strictly to prevent false positives
|
|
116
|
+
are_match = re.match(r"^([^\s=]+)\s+ARE\s+(.*)", line)
|
|
117
|
+
if are_match:
|
|
118
|
+
key = are_match.group(1)
|
|
119
|
+
raw_vals = are_match.group(2).split(",")
|
|
120
|
+
parsed_list = []
|
|
121
|
+
|
|
122
|
+
for rv in raw_vals:
|
|
123
|
+
v = rv.strip()
|
|
124
|
+
if v == "YES": parsed_list.append(True)
|
|
125
|
+
elif v == "NO": parsed_list.append(False)
|
|
126
|
+
elif v == "EMPTY": parsed_list.append(None)
|
|
127
|
+
elif v.startswith("PIN "): parsed_list.append(v[4:].strip())
|
|
128
|
+
else:
|
|
129
|
+
try:
|
|
130
|
+
parsed_list.append(float(v) if "." in v else int(v))
|
|
131
|
+
except ValueError:
|
|
132
|
+
parsed_list.append(v)
|
|
133
|
+
|
|
134
|
+
scope_path[current_level][key] = parsed_list
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Handle Standard Key-Value Pairs (=)
|
|
138
|
+
if "=" in line:
|
|
139
|
+
key_part, val_part = line.split("=", 1)
|
|
140
|
+
key = key_part.strip()
|
|
141
|
+
val = val_part.strip()
|
|
142
|
+
|
|
143
|
+
if val == "":
|
|
144
|
+
state = "IN_LIST"
|
|
145
|
+
current_key = key
|
|
146
|
+
current_list = []
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if val == "LINE":
|
|
150
|
+
state = "IN_LINE"
|
|
151
|
+
current_key = key
|
|
152
|
+
current_lines = []
|
|
153
|
+
expected_line = 1
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
final_val = None
|
|
157
|
+
if val == "YES":
|
|
158
|
+
final_val = True
|
|
159
|
+
elif val == "NO":
|
|
160
|
+
final_val = False
|
|
161
|
+
elif val == "EMPTY":
|
|
162
|
+
final_val = None
|
|
163
|
+
elif val.startswith("PIN "):
|
|
164
|
+
final_val = val[4:].strip()
|
|
165
|
+
elif val.startswith("COPY_ENV "):
|
|
166
|
+
env_var = val[9:].strip()
|
|
167
|
+
if env_var not in os.environ:
|
|
168
|
+
raise EnvironmentError(f"CHSL ERROR (Line {actual_line_num}): Missing Required OS Secret '{env_var}'!")
|
|
169
|
+
final_val = os.environ[env_var]
|
|
170
|
+
elif val.startswith("COPY "):
|
|
171
|
+
ref_key = val[5:].strip()
|
|
172
|
+
found = False
|
|
173
|
+
for lvl in range(current_level, -2, -1):
|
|
174
|
+
if ref_key in scope_path[lvl]:
|
|
175
|
+
final_val = scope_path[lvl][ref_key]
|
|
176
|
+
found = True
|
|
177
|
+
break
|
|
178
|
+
if not found:
|
|
179
|
+
raise NameError(f"CHSL ERROR (Line {actual_line_num}): Tried to COPY '{ref_key}', but it doesn't exist!")
|
|
180
|
+
else:
|
|
181
|
+
try:
|
|
182
|
+
final_val = float(val) if "." in val else int(val)
|
|
183
|
+
except ValueError:
|
|
184
|
+
final_val = val
|
|
185
|
+
|
|
186
|
+
scope_path[current_level][key] = final_val
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
raise SyntaxError(f"CHSL ERROR (Line {actual_line_num}): Unrecognized syntax -> '{line}'")
|
|
190
|
+
|
|
191
|
+
# Flush remaining state at end of file
|
|
192
|
+
if state == "IN_LIST":
|
|
193
|
+
scope_path[current_level][current_key] = current_list
|
|
194
|
+
elif state == "IN_LINE":
|
|
195
|
+
scope_path[current_level][current_key] = "\n".join(current_lines)
|
|
196
|
+
|
|
197
|
+
return config
|
|
198
|
+
|
|
199
|
+
def parse_chsl_file(filepath):
|
|
200
|
+
abs_path = os.path.abspath(filepath)
|
|
201
|
+
with open(abs_path, 'r') as f:
|
|
202
|
+
return parse_chsl(f.read(), os.path.dirname(abs_path))
|
|
203
|
+
|
|
204
|
+
# ==========================================
|
|
205
|
+
# PHASE 2: THE CHSL WRITER (Serializer)
|
|
206
|
+
# ==========================================
|
|
207
|
+
def dump_chsl(config_dict, level=0):
|
|
208
|
+
lines = []
|
|
209
|
+
for key, value in config_dict.items():
|
|
210
|
+
if isinstance(value, dict):
|
|
211
|
+
lines.append(f"\n{level}{key}{level}")
|
|
212
|
+
lines.extend(dump_chsl(value, level + 1))
|
|
213
|
+
|
|
214
|
+
elif isinstance(value, list):
|
|
215
|
+
# Smart Array Serializer: Choose 'ARE' if safe, otherwise fallback to '#'
|
|
216
|
+
is_safe_for_are = all(not isinstance(i, (dict, list)) and (not isinstance(i, str) or ("\n" not in i and "," not in i)) for i in value)
|
|
217
|
+
|
|
218
|
+
if is_safe_for_are and len(value) > 0:
|
|
219
|
+
are_items = []
|
|
220
|
+
for item in value:
|
|
221
|
+
if isinstance(item, bool):
|
|
222
|
+
are_items.append("YES" if item else "NO")
|
|
223
|
+
elif item is None:
|
|
224
|
+
are_items.append("EMPTY")
|
|
225
|
+
elif isinstance(item, (int, float)):
|
|
226
|
+
are_items.append(str(item))
|
|
227
|
+
elif isinstance(item, str) and re.match(r"^-?\d+(\.\d+)?$", item):
|
|
228
|
+
are_items.append(f"PIN {item}")
|
|
229
|
+
else:
|
|
230
|
+
are_items.append(str(item))
|
|
231
|
+
lines.append(f"{key} ARE {', '.join(are_items)}")
|
|
232
|
+
else:
|
|
233
|
+
lines.append(f"{key} =")
|
|
234
|
+
for item in value:
|
|
235
|
+
if isinstance(item, bool):
|
|
236
|
+
lines.append(f"#YES" if item else f"#NO")
|
|
237
|
+
elif item is None:
|
|
238
|
+
lines.append(f"#EMPTY")
|
|
239
|
+
elif isinstance(item, (int, float)):
|
|
240
|
+
lines.append(f"#{item}")
|
|
241
|
+
elif isinstance(item, str) and re.match(r"^-?\d+(\.\d+)?$", item):
|
|
242
|
+
lines.append(f"#PIN {item}")
|
|
243
|
+
else:
|
|
244
|
+
lines.append(f"#{item}")
|
|
245
|
+
|
|
246
|
+
elif isinstance(value, bool):
|
|
247
|
+
lines.append(f"{key} = {'YES' if value else 'NO'}")
|
|
248
|
+
|
|
249
|
+
elif value is None:
|
|
250
|
+
lines.append(f"{key} = EMPTY")
|
|
251
|
+
|
|
252
|
+
elif isinstance(value, (int, float)):
|
|
253
|
+
lines.append(f"{key} = {value}")
|
|
254
|
+
|
|
255
|
+
elif isinstance(value, str):
|
|
256
|
+
if "\n" in value:
|
|
257
|
+
lines.append(f"{key} = LINE")
|
|
258
|
+
for i, text_line in enumerate(value.split("\n"), 1):
|
|
259
|
+
lines.append(f"{i} = {text_line}")
|
|
260
|
+
elif re.match(r"^-?\d+(\.\d+)?$", value):
|
|
261
|
+
lines.append(f"{key} = PIN {value}")
|
|
262
|
+
else:
|
|
263
|
+
lines.append(f"{key} = {value}")
|
|
264
|
+
|
|
265
|
+
return lines
|
|
266
|
+
|
|
267
|
+
def write_chsl_file(config_dict, filename="output.chsl"):
|
|
268
|
+
lines = ["NOTE: Automatically generated by the CHSL Engine\n"]
|
|
269
|
+
lines.extend(dump_chsl(config_dict))
|
|
270
|
+
final_text = "\n".join(lines)
|
|
271
|
+
with open(filename, "w") as f:
|
|
272
|
+
f.write(final_text)
|
|
273
|
+
return final_text
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kcvabeysinghe/chsl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Chethana's Human-readable Simple Language parser and serializer.",
|
|
5
|
+
"main": "chsl.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"No tests yet\""
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/kcvabeysinghe/chsl"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chsl",
|
|
15
|
+
"configuration",
|
|
16
|
+
"parser",
|
|
17
|
+
"settings"
|
|
18
|
+
],
|
|
19
|
+
"author": "Chethana",
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chsl"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Chethana" }
|
|
10
|
+
]
|
|
11
|
+
description = "Chethana's Human-readable Simple Language parser and serializer."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.6"
|
|
14
|
+
license = {text = "MIT"}
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|