@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 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
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # CHSL (Chethana's Human-readable Simple Language)
2
+
3
+ ![npm](https://img.shields.io/npm/v/chsl?color=red&logo=npm)
4
+ ![npm downloads](https://img.shields.io/npm/dt/chsl?logo=npm)
5
+ ![PyPI](https://img.shields.io/pypi/v/chsl?color=blue&logo=python)
6
+ ![PyPI downloads](https://img.shields.io/pypi/dm/chsl?logo=python)
7
+ ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
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
+ ]