@aperturesyndicate/synx-format 3.6.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) 2025 APERTURESyndicate
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,144 @@
1
+ # SYNX for JS/TS — @aperturesyndicate/synx
2
+
3
+ The official JavaScript & TypeScript parser for the SYNX format.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @aperturesyndicate/synx
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ const Synx = require('@aperturesyndicate/synx');
15
+
16
+ // Load from file
17
+ const data = Synx.loadSync('config.synx');
18
+ console.log(data.server.port); // 8080
19
+
20
+ // Parse from string
21
+ const data2 = Synx.parse('name Wario\nage 30');
22
+ console.log(data2.name); // "Wario"
23
+ ```
24
+
25
+ ### TypeScript
26
+
27
+ ```typescript
28
+ import Synx from '@aperturesyndicate/synx';
29
+
30
+ interface Config {
31
+ server: { port: number; host: string };
32
+ app_name: string;
33
+ }
34
+
35
+ const data = Synx.loadSync<Config>('config.synx');
36
+ console.log(data.server.port); // typed as number
37
+ ```
38
+
39
+ ## API
40
+
41
+ | Method | Description |
42
+ |---|---|
43
+ | `Synx.parse<T>(text, options?)` | Parse a .synx string → object |
44
+ | `Synx.loadSync<T>(path, options?)` | Load & parse file (sync) |
45
+ | `Synx.load<T>(path, options?)` | Load & parse file (async, returns Promise) |
46
+ | `Synx.stringify(obj, active?)` | Serialize object → .synx string |
47
+
48
+ ### Options
49
+
50
+ ```typescript
51
+ {
52
+ basePath?: string; // For :include resolution
53
+ env?: Record<string, string>; // Override env vars
54
+ region?: string; // For :geo ("RU", "US", etc.)
55
+ strict?: boolean; // Throw on INCLUDE_ERR/WATCH_ERR/CALC_ERR/CONSTRAINT_ERR
56
+ }
57
+ ```
58
+
59
+ `strict: true` enables fail-fast behavior for production: marker runtime errors throw instead of silently remaining in the output object as error strings.
60
+
61
+ ## CLI
62
+
63
+ Install globally:
64
+
65
+ ```bash
66
+ npm install -g @aperturesyndicate/synx
67
+ ```
68
+
69
+ ### Commands
70
+
71
+ ```bash
72
+ # Convert to JSON/YAML/TOML/.env
73
+ synx convert config.synx --format json
74
+ synx convert config.synx --format yaml > values.yaml
75
+ synx convert config.synx --format toml
76
+ synx convert config.synx --format env > .env
77
+
78
+ # Validate (strict mode, for CI/CD)
79
+ synx validate config.synx --strict
80
+
81
+ # Watch for changes
82
+ synx watch config.synx --format json
83
+ synx watch config.synx --exec "nginx -s reload"
84
+
85
+ # Extract JSON Schema from constraints
86
+ synx schema config.synx
87
+ ```
88
+
89
+ ## Export Formats
90
+
91
+ Convert parsed SYNX to other formats programmatically:
92
+
93
+ ```typescript
94
+ const config = Synx.loadSync('config.synx');
95
+
96
+ Synx.toJSON(config); // JSON (pretty)
97
+ Synx.toJSON(config, false); // JSON (compact)
98
+ Synx.toYAML(config); // YAML
99
+ Synx.toTOML(config); // TOML
100
+ Synx.toEnv(config); // KEY=VALUE
101
+ Synx.toEnv(config, 'PREFIX'); // PREFIX_KEY=VALUE
102
+ ```
103
+
104
+ ## File Watcher
105
+
106
+ ```typescript
107
+ const handle = Synx.watch('config.synx', (config, error) => {
108
+ if (error) return console.error(error);
109
+ console.log('Config reloaded:', config);
110
+ }, { strict: true });
111
+
112
+ handle.close(); // stop watching
113
+ ```
114
+
115
+ ## Schema Export
116
+
117
+ Extract constraints as JSON Schema:
118
+
119
+ ```typescript
120
+ const schema = Synx.schema(`
121
+ !active
122
+ app_name[required, min:3, max:30] TotalWario
123
+ volume[min:0, max:100, type:int] 75
124
+ `);
125
+ // { "$schema": "...", "properties": { "app_name": { ... } }, "required": ["app_name"] }
126
+ ```
127
+
128
+ ## Other Languages
129
+
130
+ - **Python** — [synx-format](https://pypi.org/project/synx-format/) on PyPI
131
+ - **Rust** — [synx](https://crates.io/crates/synx) on crates.io
132
+ ```bash
133
+ cargo add synx
134
+ ```
135
+ ```rust
136
+ use synx::Synx;
137
+
138
+ let data = Synx::load("config.synx")?;
139
+ println!("{}", &data["server"]["port"]);
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT — © APERTURESyndicate
@@ -0,0 +1,6 @@
1
+ # SYNX specification
2
+
3
+ The normative specification files live in the monorepo (not duplicated here for npm):
4
+
5
+ - English: [`docs/spec/SPECIFICATION_EN.md`](https://github.com/kaiserrberg/synx-format/blob/main/docs/spec/SPECIFICATION_EN.md)
6
+ - Russian: [`docs/spec/SPECIFICATION_RU.md`](https://github.com/kaiserrberg/synx-format/blob/main/docs/spec/SPECIFICATION_RU.md)
package/bin/synx.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const Synx = require('../dist/index');
7
+
8
+ const args = process.argv.slice(2);
9
+ const command = args[0];
10
+
11
+ function usage() {
12
+ console.log(`
13
+ SYNX CLI — The Active Data Format (v3.6.0)
14
+
15
+ DEPRECATED: This Node.js CLI is superseded by the native Rust CLI.
16
+ Install: cargo install synx-cli
17
+ Docs: https://github.com/kaiserrberg/synx-format#cli-rust
18
+
19
+ Usage:
20
+ synx convert <file.synx> [--format json|yaml|toml|env] [--strict] [--active]
21
+ synx validate <file.synx> [--strict]
22
+ synx watch <file.synx> [--format json] [--exec <command>]
23
+ synx schema <file.synx>
24
+
25
+ Commands:
26
+ convert Parse .synx and output in another format (default: json)
27
+ validate Parse with strict mode — exits 1 on any marker error
28
+ watch Watch file for changes, re-parse and print or exec command
29
+ schema Extract constraints as JSON Schema
30
+
31
+ Options:
32
+ --format, -f Output format: json (default), yaml, toml, env
33
+ --strict Throw on INCLUDE_ERR / WATCH_ERR / CALC_ERR / CONSTRAINT_ERR
34
+ --active Force active mode resolution
35
+ --exec, -e Command to run after each re-parse (watch mode)
36
+ --help, -h Show this help
37
+ `);
38
+ process.exit(0);
39
+ }
40
+
41
+ function getFlag(flag, alias) {
42
+ const idx = args.indexOf(flag);
43
+ const aidx = alias ? args.indexOf(alias) : -1;
44
+ return idx !== -1 || aidx !== -1;
45
+ }
46
+
47
+ function getFlagValue(flag, alias, defaultVal) {
48
+ let idx = args.indexOf(flag);
49
+ if (idx === -1 && alias) idx = args.indexOf(alias);
50
+ if (idx === -1 || idx + 1 >= args.length) return defaultVal;
51
+ return args[idx + 1];
52
+ }
53
+
54
+ if (!command || command === '--help' || command === '-h') usage();
55
+
56
+ const file = args[1];
57
+ if (!file) {
58
+ console.error('Error: no file specified');
59
+ process.exit(1);
60
+ }
61
+
62
+ const absPath = path.resolve(file);
63
+ if (!fs.existsSync(absPath)) {
64
+ console.error(`Error: file not found: ${absPath}`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const format = getFlagValue('--format', '-f', 'json');
69
+ const strict = getFlag('--strict');
70
+ const execCmd = getFlagValue('--exec', '-e', null);
71
+
72
+ function parseFile(filePath) {
73
+ const text = fs.readFileSync(filePath, 'utf-8');
74
+ return Synx.parse(text, {
75
+ basePath: path.dirname(filePath),
76
+ strict,
77
+ });
78
+ }
79
+
80
+ function formatOutput(obj) {
81
+ switch (format) {
82
+ case 'json': return Synx.toJSON(obj);
83
+ case 'yaml': return Synx.toYAML(obj);
84
+ case 'toml': return Synx.toTOML(obj);
85
+ case 'env': return Synx.toEnv(obj);
86
+ default:
87
+ console.error(`Unknown format: ${format}. Use json, yaml, toml, or env.`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+
92
+ switch (command) {
93
+ case 'convert': {
94
+ try {
95
+ const obj = parseFile(absPath);
96
+ process.stdout.write(formatOutput(obj));
97
+ } catch (e) {
98
+ console.error(e.message);
99
+ process.exit(1);
100
+ }
101
+ break;
102
+ }
103
+
104
+ case 'validate': {
105
+ try {
106
+ const text = fs.readFileSync(absPath, 'utf-8');
107
+ Synx.parse(text, { basePath: path.dirname(absPath), strict: true });
108
+ console.log('OK');
109
+ } catch (e) {
110
+ console.error('FAIL:', e.message);
111
+ process.exit(1);
112
+ }
113
+ break;
114
+ }
115
+
116
+ case 'watch': {
117
+ console.error(`[synx] watching ${absPath}`);
118
+ const handle = Synx.watch(absPath, (config, error) => {
119
+ if (error) {
120
+ console.error(`[synx] error: ${error.message}`);
121
+ return;
122
+ }
123
+ if (execCmd) {
124
+ const { execSync } = require('child_process');
125
+ try {
126
+ execSync(execCmd, { stdio: 'inherit' });
127
+ } catch { /* command failed */ }
128
+ } else {
129
+ process.stdout.write(formatOutput(config));
130
+ }
131
+ }, { basePath: path.dirname(absPath), strict });
132
+ process.on('SIGINT', () => { handle.close(); process.exit(0); });
133
+ break;
134
+ }
135
+
136
+ case 'schema': {
137
+ const text = fs.readFileSync(absPath, 'utf-8');
138
+ const schema = Synx.schema(text);
139
+ console.log(JSON.stringify(schema, null, 2));
140
+ break;
141
+ }
142
+
143
+ default:
144
+ console.error(`Unknown command: ${command}`);
145
+ usage();
146
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * SYNX Browser Bundle — @aperturesyndicate/synx
3
+ *
4
+ * Lightweight browser-compatible build.
5
+ * No Node.js dependencies (fs, path).
6
+ * Provides: parse, stringify.
7
+ */
8
+ import type { SynxObject, SynxOptions } from './types';
9
+ export type { SynxObject, SynxOptions, SynxValue, SynxArray, SynxPrimitive } from './types';
10
+ export { SynxError } from './types';
11
+ declare class Synx {
12
+ static parse<T extends SynxObject = SynxObject>(text: string, options?: SynxOptions): T;
13
+ static stringify(obj: SynxObject, active?: boolean): string;
14
+ }
15
+ export default Synx;
16
+ export { Synx };
17
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEvD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,cAAM,IAAI;IACR,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,CAAC;IAQ3F,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,UAAQ,GAAG,MAAM;CAQ1D;AAuCD,eAAe,IAAI,CAAC;AACpB,OAAO,EAAE,IAAI,EAAE,CAAC"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * SYNX Browser Bundle — @aperturesyndicate/synx
4
+ *
5
+ * Lightweight browser-compatible build.
6
+ * No Node.js dependencies (fs, path).
7
+ * Provides: parse, stringify.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.Synx = exports.SynxError = void 0;
11
+ const parser_1 = require("./parser");
12
+ const engine_1 = require("./engine");
13
+ var types_1 = require("./types");
14
+ Object.defineProperty(exports, "SynxError", { enumerable: true, get: function () { return types_1.SynxError; } });
15
+ class Synx {
16
+ static parse(text, options = {}) {
17
+ const { root, mode } = (0, parser_1.parseData)(text);
18
+ if (mode === 'active') {
19
+ (0, engine_1.resolve)(root, options);
20
+ }
21
+ return root;
22
+ }
23
+ static stringify(obj, active = false) {
24
+ let out = '';
25
+ if (active) {
26
+ out += '!active\n';
27
+ }
28
+ out += serializeObject(obj, 0);
29
+ return out;
30
+ }
31
+ }
32
+ exports.Synx = Synx;
33
+ function serializeObject(obj, indent) {
34
+ let out = '';
35
+ const spaces = ' '.repeat(indent);
36
+ for (const [key, val] of Object.entries(obj)) {
37
+ if (Array.isArray(val)) {
38
+ out += `${spaces}${key}\n`;
39
+ for (const item of val) {
40
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
41
+ const entries = Object.entries(item);
42
+ if (entries.length > 0) {
43
+ const [firstKey, firstVal] = entries[0];
44
+ out += `${spaces} - ${firstKey} ${firstVal}\n`;
45
+ for (let i = 1; i < entries.length; i++) {
46
+ out += `${spaces} ${entries[i][0]} ${entries[i][1]}\n`;
47
+ }
48
+ }
49
+ }
50
+ else {
51
+ out += `${spaces} - ${item}\n`;
52
+ }
53
+ }
54
+ }
55
+ else if (val && typeof val === 'object') {
56
+ out += `${spaces}${key}\n`;
57
+ out += serializeObject(val, indent + 2);
58
+ }
59
+ else if (typeof val === 'string' && val.includes('\n')) {
60
+ out += `${spaces}${key} |\n`;
61
+ for (const line of val.split('\n')) {
62
+ out += `${spaces} ${line}\n`;
63
+ }
64
+ }
65
+ else {
66
+ out += `${spaces}${key} ${val}\n`;
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ exports.default = Synx;
72
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
package/dist/calc.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SYNX Safe Calculator — @aperturesyndicate/synx
3
+ *
4
+ * Evaluates arithmetic expressions WITHOUT eval() or new Function().
5
+ * Supports: +, -, *, /, %, parentheses, and numeric literals.
6
+ * Variable references are resolved before this stage.
7
+ */
8
+ /**
9
+ * Safely evaluate an arithmetic expression.
10
+ * All variable names must be substituted with numbers before calling this.
11
+ *
12
+ * @param expr - e.g. "100 * 5 + (20 / 4)"
13
+ * @returns The computed number
14
+ */
15
+ export declare function safeCalc(expr: string): number;
16
+ //# sourceMappingURL=calc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calc.d.ts","sourceRoot":"","sources":["../src/calc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA8IH;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7C"}
package/dist/calc.js ADDED
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * SYNX Safe Calculator — @aperturesyndicate/synx
4
+ *
5
+ * Evaluates arithmetic expressions WITHOUT eval() or new Function().
6
+ * Supports: +, -, *, /, %, parentheses, and numeric literals.
7
+ * Variable references are resolved before this stage.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.safeCalc = safeCalc;
11
+ /**
12
+ * Tokenize an arithmetic expression string.
13
+ */
14
+ function tokenize(expr) {
15
+ const tokens = [];
16
+ let i = 0;
17
+ while (i < expr.length) {
18
+ const ch = expr[i];
19
+ // Skip whitespace
20
+ if (ch === ' ' || ch === '\t') {
21
+ i++;
22
+ continue;
23
+ }
24
+ // Number (integer or float, including negative after operator/start)
25
+ if ((ch >= '0' && ch <= '9') ||
26
+ (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9') ||
27
+ (ch === '-' && (tokens.length === 0 || tokens[tokens.length - 1].type === 'op' || (tokens[tokens.length - 1].type === 'paren' && tokens[tokens.length - 1].value === '(')))) {
28
+ let num = '';
29
+ if (ch === '-') {
30
+ num += '-';
31
+ i++;
32
+ }
33
+ while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) {
34
+ num += expr[i];
35
+ i++;
36
+ }
37
+ tokens.push({ type: 'number', value: parseFloat(num) });
38
+ continue;
39
+ }
40
+ // Operators
41
+ if ('+-*/%'.includes(ch)) {
42
+ tokens.push({ type: 'op', value: ch });
43
+ i++;
44
+ continue;
45
+ }
46
+ // Parentheses
47
+ if (ch === '(' || ch === ')') {
48
+ tokens.push({ type: 'paren', value: ch });
49
+ i++;
50
+ continue;
51
+ }
52
+ throw new Error(`SYNX :calc — unexpected character: '${ch}' in expression "${expr}"`);
53
+ }
54
+ return tokens;
55
+ }
56
+ /**
57
+ * Recursive descent parser for arithmetic.
58
+ * Grammar:
59
+ * expr → term (('+' | '-') term)*
60
+ * term → factor (('*' | '/' | '%') factor)*
61
+ * factor → NUMBER | '(' expr ')'
62
+ */
63
+ class ExprParser {
64
+ constructor(tokens) {
65
+ this.tokens = tokens;
66
+ this.pos = 0;
67
+ }
68
+ parse() {
69
+ const result = this.expr();
70
+ if (this.pos < this.tokens.length) {
71
+ throw new Error(`SYNX :calc — unexpected token at position ${this.pos}`);
72
+ }
73
+ return result;
74
+ }
75
+ expr() {
76
+ let left = this.term();
77
+ while (this.pos < this.tokens.length && this.tokens[this.pos].type === 'op' && (this.tokens[this.pos].value === '+' || this.tokens[this.pos].value === '-')) {
78
+ const op = this.tokens[this.pos].value;
79
+ this.pos++;
80
+ const right = this.term();
81
+ left = op === '+' ? left + right : left - right;
82
+ }
83
+ return left;
84
+ }
85
+ term() {
86
+ let left = this.factor();
87
+ while (this.pos < this.tokens.length && this.tokens[this.pos].type === 'op' && ('*/%'.includes(this.tokens[this.pos].value))) {
88
+ const op = this.tokens[this.pos].value;
89
+ this.pos++;
90
+ const right = this.factor();
91
+ if (op === '*')
92
+ left = left * right;
93
+ else if (op === '/') {
94
+ if (right === 0)
95
+ throw new Error('SYNX :calc — division by zero');
96
+ left = left / right;
97
+ }
98
+ else {
99
+ if (right === 0)
100
+ throw new Error('SYNX :calc — division by zero');
101
+ left = left % right;
102
+ }
103
+ }
104
+ return left;
105
+ }
106
+ factor() {
107
+ const tok = this.tokens[this.pos];
108
+ if (!tok) {
109
+ throw new Error('SYNX :calc — unexpected end of expression');
110
+ }
111
+ if (tok.type === 'number') {
112
+ this.pos++;
113
+ return tok.value;
114
+ }
115
+ if (tok.type === 'paren' && tok.value === '(') {
116
+ this.pos++; // skip '('
117
+ const val = this.expr();
118
+ if (!this.tokens[this.pos] || this.tokens[this.pos].type !== 'paren' || this.tokens[this.pos].value !== ')') {
119
+ throw new Error('SYNX :calc — missing closing parenthesis');
120
+ }
121
+ this.pos++; // skip ')'
122
+ return val;
123
+ }
124
+ throw new Error(`SYNX :calc — unexpected token: ${JSON.stringify(tok)}`);
125
+ }
126
+ }
127
+ /**
128
+ * Safely evaluate an arithmetic expression.
129
+ * All variable names must be substituted with numbers before calling this.
130
+ *
131
+ * @param expr - e.g. "100 * 5 + (20 / 4)"
132
+ * @returns The computed number
133
+ */
134
+ function safeCalc(expr) {
135
+ const tokens = tokenize(expr.trim());
136
+ if (tokens.length === 0)
137
+ return 0;
138
+ return new ExprParser(tokens).parse();
139
+ }
140
+ //# sourceMappingURL=calc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calc.js","sourceRoot":"","sources":["../src/calc.ts"],"names":[],"mappings":"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"}