@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 +21 -0
- package/README.md +144 -0
- package/SPECIFICATION.md +6 -0
- package/bin/synx.js +146 -0
- package/dist/browser.d.ts +17 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +72 -0
- package/dist/browser.js.map +1 -0
- package/dist/calc.d.ts +16 -0
- package/dist/calc.d.ts.map +1 -0
- package/dist/calc.js +140 -0
- package/dist/calc.js.map +1 -0
- package/dist/demo-browser.html +153 -0
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +970 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +193 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +810 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +12 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +442 -0
- package/dist/parser.js.map +1 -0
- package/dist/synx.browser.js +29 -0
- package/dist/synx.browser.js.map +7 -0
- package/dist/synx.browser.mjs +28 -0
- package/dist/synx.browser.mjs.map +7 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SYNX — @aperturesyndicate/synx
|
|
4
|
+
*
|
|
5
|
+
* The Active Data Format.
|
|
6
|
+
* Faster than JSON. Cheaper for AI tokens. Built-in logic.
|
|
7
|
+
*
|
|
8
|
+
* Auto-engine: files < 5 KB use the pure-JS parser;
|
|
9
|
+
* files >= 5 KB use the native Rust binding (if available).
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.Synx = exports.SynxError = void 0;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const parser_1 = require("./parser");
|
|
51
|
+
const engine_1 = require("./engine");
|
|
52
|
+
const types_1 = require("./types");
|
|
53
|
+
var types_2 = require("./types");
|
|
54
|
+
Object.defineProperty(exports, "SynxError", { enumerable: true, get: function () { return types_2.SynxError; } });
|
|
55
|
+
// ─── Native binding auto-detection ───────────────────────
|
|
56
|
+
const NATIVE_THRESHOLD = 5120; // 5 KB
|
|
57
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
58
|
+
let nativeBinding = null; // null = not tried, false = unavailable
|
|
59
|
+
function tryLoadNative() {
|
|
60
|
+
if (nativeBinding !== null)
|
|
61
|
+
return nativeBinding;
|
|
62
|
+
try {
|
|
63
|
+
// Walk up from synx-js to find bindings/node
|
|
64
|
+
const bindingDir = path.resolve(__dirname, '..', '..', '..', 'bindings', 'node');
|
|
65
|
+
const mod = require(bindingDir);
|
|
66
|
+
if (typeof mod.parse === 'function') {
|
|
67
|
+
nativeBinding = mod;
|
|
68
|
+
return mod;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { /* native not available */ }
|
|
72
|
+
nativeBinding = false;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const RUNTIME_ERROR_PREFIXES = [
|
|
76
|
+
'INCLUDE_ERR:',
|
|
77
|
+
'WATCH_ERR:',
|
|
78
|
+
'CALC_ERR:',
|
|
79
|
+
'SPAM_ERR:',
|
|
80
|
+
'CONSTRAINT_ERR:',
|
|
81
|
+
'ALIAS_ERR:',
|
|
82
|
+
'NESTING_ERR:',
|
|
83
|
+
];
|
|
84
|
+
function assertNoRuntimeErrors(value, path = 'root') {
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
for (const prefix of RUNTIME_ERROR_PREFIXES) {
|
|
87
|
+
if (value.startsWith(prefix)) {
|
|
88
|
+
throw new types_1.SynxError(`${value}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
for (let i = 0; i < value.length; i++) {
|
|
95
|
+
assertNoRuntimeErrors(value[i], `${path}[${i}]`);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (value && typeof value === 'object') {
|
|
100
|
+
for (const [k, v] of Object.entries(value)) {
|
|
101
|
+
assertNoRuntimeErrors(v, `${path}.${k}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
class Synx {
|
|
106
|
+
/**
|
|
107
|
+
* Parse a .synx text string into a native JS object.
|
|
108
|
+
*
|
|
109
|
+
* Automatically selects the engine:
|
|
110
|
+
* - text < 5 KB → pure-JS parser (zero startup cost)
|
|
111
|
+
* - text >= 5 KB → native Rust binding (faster on large files)
|
|
112
|
+
* Falls back to JS if the native binding is not built.
|
|
113
|
+
*
|
|
114
|
+
* @param text - The .synx file contents as a string.
|
|
115
|
+
* @param options - Optional settings (basePath, env overrides, region).
|
|
116
|
+
* @returns A plain JS object with all data resolved.
|
|
117
|
+
*/
|
|
118
|
+
static parse(text, options = {}) {
|
|
119
|
+
// Large files → try native Rust binding
|
|
120
|
+
if (text.length >= NATIVE_THRESHOLD) {
|
|
121
|
+
const native = tryLoadNative();
|
|
122
|
+
if (native) {
|
|
123
|
+
const isActive = /(?:^|\n)\s*!active\s*(?:\r?\n|$)/.test(text) ||
|
|
124
|
+
/(?:^|\n)\s*#!mode:active/.test(text);
|
|
125
|
+
const result = isActive
|
|
126
|
+
? native.parseActive(text, options)
|
|
127
|
+
: native.parse(text);
|
|
128
|
+
if (options.strict) {
|
|
129
|
+
assertNoRuntimeErrors(result);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Small files or no native binding → pure JS
|
|
135
|
+
const { root, mode, locked, includes } = (0, parser_1.parseData)(text);
|
|
136
|
+
if (mode === 'active') {
|
|
137
|
+
(0, engine_1.resolve)(root, { ...options, _includes: includes });
|
|
138
|
+
}
|
|
139
|
+
if (locked) {
|
|
140
|
+
Object.defineProperty(root, '__synx_locked', {
|
|
141
|
+
value: true,
|
|
142
|
+
enumerable: false,
|
|
143
|
+
writable: false,
|
|
144
|
+
configurable: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (options.strict) {
|
|
148
|
+
assertNoRuntimeErrors(root);
|
|
149
|
+
}
|
|
150
|
+
return root;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Load and parse a .synx file synchronously.
|
|
154
|
+
*
|
|
155
|
+
* @param filePath - Path to the .synx file.
|
|
156
|
+
* @param options - Optional settings.
|
|
157
|
+
* @returns A plain JS object.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* const config = Synx.loadSync('config.synx');
|
|
162
|
+
* console.log(config.app_name); // "TotalWario"
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
static loadSync(filePath, options = {}) {
|
|
166
|
+
const absPath = path.resolve(filePath);
|
|
167
|
+
const text = fs.readFileSync(absPath, 'utf-8');
|
|
168
|
+
// Spread to avoid mutating the caller's options object
|
|
169
|
+
const opts = options.basePath ? options : { ...options, basePath: path.dirname(absPath) };
|
|
170
|
+
return Synx.parse(text, opts);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Load and parse a .synx file asynchronously.
|
|
174
|
+
*
|
|
175
|
+
* @param filePath - Path to the .synx file.
|
|
176
|
+
* @param options - Optional settings.
|
|
177
|
+
* @returns A Promise resolving to a plain JS object.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const config = await Synx.load('config.synx');
|
|
182
|
+
* console.log(config.gameplay.boss_hp); // 500
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
static async load(filePath, options = {}) {
|
|
186
|
+
const absPath = path.resolve(filePath);
|
|
187
|
+
const text = await fs.promises.readFile(absPath, 'utf-8');
|
|
188
|
+
// Spread to avoid mutating the caller's options object
|
|
189
|
+
const opts = options.basePath ? options : { ...options, basePath: path.dirname(absPath) };
|
|
190
|
+
return Synx.parse(text, opts);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Serialize a JS object back to .synx format string.
|
|
194
|
+
*
|
|
195
|
+
* @param obj - The object to serialize.
|
|
196
|
+
* @param active - If true, prepends `!active` header.
|
|
197
|
+
* @returns A .synx formatted string.
|
|
198
|
+
*/
|
|
199
|
+
static stringify(obj, active = false) {
|
|
200
|
+
let out = '';
|
|
201
|
+
if (active) {
|
|
202
|
+
out += '!active\n';
|
|
203
|
+
}
|
|
204
|
+
out += serializeObject(obj, 0);
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
// ─── Runtime Manipulation API ─────────────────────────
|
|
208
|
+
/**
|
|
209
|
+
* Set a value on a parsed SYNX config object.
|
|
210
|
+
* Supports dot-path notation for nested keys.
|
|
211
|
+
* Throws if config has `!lock` directive.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* const config = Synx.loadSync('config.synx');
|
|
216
|
+
* Synx.set(config, 'max_players', 100);
|
|
217
|
+
* Synx.set(config, 'server.host', 'localhost');
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
static set(obj, keyPath, value) {
|
|
221
|
+
if (obj.__synx_locked) {
|
|
222
|
+
throw new Error(`SYNX: Cannot set "${keyPath}" — config is locked (!lock)`);
|
|
223
|
+
}
|
|
224
|
+
const parts = keyPath.split('.');
|
|
225
|
+
for (const p of parts)
|
|
226
|
+
if (UNSAFE_KEYS.has(p))
|
|
227
|
+
throw new Error(`SYNX: unsafe key "${p}"`);
|
|
228
|
+
let current = obj;
|
|
229
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
230
|
+
if (current[parts[i]] == null || typeof current[parts[i]] !== 'object') {
|
|
231
|
+
current[parts[i]] = {};
|
|
232
|
+
}
|
|
233
|
+
current = current[parts[i]];
|
|
234
|
+
}
|
|
235
|
+
current[parts[parts.length - 1]] = value;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get a value from a parsed SYNX config using dot-path notation.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* const port = Synx.get(config, 'server.port'); // 8080
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
static get(obj, keyPath) {
|
|
246
|
+
const parts = keyPath.split('.');
|
|
247
|
+
let current = obj;
|
|
248
|
+
for (const part of parts) {
|
|
249
|
+
if (current == null || typeof current !== 'object')
|
|
250
|
+
return undefined;
|
|
251
|
+
if (!Object.prototype.hasOwnProperty.call(current, part))
|
|
252
|
+
return undefined;
|
|
253
|
+
current = current[part];
|
|
254
|
+
}
|
|
255
|
+
return current;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Add an item to an array value in the config.
|
|
259
|
+
* Creates the array if it doesn't exist.
|
|
260
|
+
* Throws if config has `!lock` directive.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* Synx.add(config, 'your_random_name', 'Mark');
|
|
265
|
+
* // your_random_name: ["Alice", "Caroline", "Mark"]
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
static add(obj, keyPath, item) {
|
|
269
|
+
if (obj.__synx_locked) {
|
|
270
|
+
throw new Error(`SYNX: Cannot add to "${keyPath}" — config is locked (!lock)`);
|
|
271
|
+
}
|
|
272
|
+
const parts = keyPath.split('.');
|
|
273
|
+
for (const p of parts)
|
|
274
|
+
if (UNSAFE_KEYS.has(p))
|
|
275
|
+
throw new Error(`SYNX: unsafe key "${p}"`);
|
|
276
|
+
let current = obj;
|
|
277
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
278
|
+
if (current[parts[i]] == null || typeof current[parts[i]] !== 'object') {
|
|
279
|
+
current[parts[i]] = {};
|
|
280
|
+
}
|
|
281
|
+
current = current[parts[i]];
|
|
282
|
+
}
|
|
283
|
+
const finalKey = parts[parts.length - 1];
|
|
284
|
+
if (!Array.isArray(current[finalKey])) {
|
|
285
|
+
current[finalKey] = current[finalKey] != null ? [current[finalKey]] : [];
|
|
286
|
+
}
|
|
287
|
+
current[finalKey].push(item);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Remove an item from an array value, or delete a key entirely.
|
|
291
|
+
* - If value is an array and `item` is provided: removes first occurrence of `item`.
|
|
292
|
+
* - If `item` is omitted: deletes the key entirely.
|
|
293
|
+
* Throws if config has `!lock` directive.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* Synx.remove(config, 'your_random_name', 'Alice');
|
|
298
|
+
* // or delete entirely:
|
|
299
|
+
* Synx.remove(config, 'max_players');
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
static remove(obj, keyPath, item) {
|
|
303
|
+
if (obj.__synx_locked) {
|
|
304
|
+
throw new Error(`SYNX: Cannot remove "${keyPath}" — config is locked (!lock)`);
|
|
305
|
+
}
|
|
306
|
+
const parts = keyPath.split('.');
|
|
307
|
+
for (const p of parts)
|
|
308
|
+
if (UNSAFE_KEYS.has(p))
|
|
309
|
+
throw new Error(`SYNX: unsafe key "${p}"`);
|
|
310
|
+
let current = obj;
|
|
311
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
312
|
+
if (current == null || typeof current !== 'object')
|
|
313
|
+
return;
|
|
314
|
+
current = current[parts[i]];
|
|
315
|
+
}
|
|
316
|
+
if (current == null || typeof current !== 'object')
|
|
317
|
+
return;
|
|
318
|
+
const finalKey = parts[parts.length - 1];
|
|
319
|
+
if (item !== undefined && Array.isArray(current[finalKey])) {
|
|
320
|
+
const arr = current[finalKey];
|
|
321
|
+
const idx = arr.findIndex(v => v === item || String(v) === String(item));
|
|
322
|
+
if (idx !== -1)
|
|
323
|
+
arr.splice(idx, 1);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
delete current[finalKey];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check if the config is locked (`!lock` directive).
|
|
331
|
+
*/
|
|
332
|
+
static isLocked(obj) {
|
|
333
|
+
return !!obj.__synx_locked;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Reformat a .synx string into canonical form:
|
|
337
|
+
* - Keys sorted alphabetically at every nesting level
|
|
338
|
+
* - Exactly 2 spaces per indentation level
|
|
339
|
+
* - One blank line between top-level blocks (objects / lists)
|
|
340
|
+
* - Comments stripped — canonical form is comment-free
|
|
341
|
+
* - Directive lines (`!active`, `!lock`) preserved at the top
|
|
342
|
+
*
|
|
343
|
+
* The same data always produces byte-for-byte identical output,
|
|
344
|
+
* making `.synx` files deterministic and noise-free in `git diff`.
|
|
345
|
+
*
|
|
346
|
+
* @param text - Raw .synx file contents.
|
|
347
|
+
* @returns Canonical .synx string.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* const raw = fs.readFileSync('config.synx', 'utf-8');
|
|
352
|
+
* fs.writeFileSync('config.synx', Synx.format(raw));
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
static format(text) {
|
|
356
|
+
const lines = text.split(/\r?\n/);
|
|
357
|
+
const directives = [];
|
|
358
|
+
let bodyStart = 0;
|
|
359
|
+
for (let i = 0; i < lines.length; i++) {
|
|
360
|
+
const t = lines[i].trim();
|
|
361
|
+
if (t === '!active' || t === '!lock' || t === '#!mode:active') {
|
|
362
|
+
directives.push(t);
|
|
363
|
+
bodyStart = i + 1;
|
|
364
|
+
}
|
|
365
|
+
else if (!t || t.startsWith('#') || t.startsWith('//')) {
|
|
366
|
+
bodyStart = i + 1;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const [nodes] = fmtParse(lines, bodyStart, 0);
|
|
373
|
+
fmtSort(nodes);
|
|
374
|
+
let out = directives.join('\n');
|
|
375
|
+
if (directives.length)
|
|
376
|
+
out += '\n\n';
|
|
377
|
+
out += fmtEmit(nodes, 0).trimEnd();
|
|
378
|
+
return out + '\n';
|
|
379
|
+
}
|
|
380
|
+
// ─── Export Converters ──────────────────────────────────
|
|
381
|
+
/** Convert a parsed SYNX object to JSON string. @since 3.1.3 */
|
|
382
|
+
static toJSON(obj, pretty = true) {
|
|
383
|
+
return toJSONString(obj, pretty);
|
|
384
|
+
}
|
|
385
|
+
/** Convert a parsed SYNX object to YAML string. @since 3.1.3 */
|
|
386
|
+
static toYAML(obj) {
|
|
387
|
+
return toYAMLString(obj);
|
|
388
|
+
}
|
|
389
|
+
/** Convert a parsed SYNX object to TOML string. @since 3.1.3 */
|
|
390
|
+
static toTOML(obj) {
|
|
391
|
+
return toTOMLString(obj);
|
|
392
|
+
}
|
|
393
|
+
/** Convert a parsed SYNX object to .env format (KEY=VALUE lines). @since 3.1.3 */
|
|
394
|
+
static toEnv(obj, prefix = '') {
|
|
395
|
+
return toEnvString(obj, prefix);
|
|
396
|
+
}
|
|
397
|
+
/** Watch a .synx file for changes. Re-parses and calls callback on change. @since 3.1.3 */
|
|
398
|
+
static watch(filePath, callback, options = {}) {
|
|
399
|
+
if (!fs)
|
|
400
|
+
throw new Error('Synx.watch() is not supported in browser');
|
|
401
|
+
const absPath = path.resolve(filePath);
|
|
402
|
+
const opts = options.basePath ? options : { ...options, basePath: path.dirname(absPath) };
|
|
403
|
+
try {
|
|
404
|
+
const text = fs.readFileSync(absPath, 'utf-8');
|
|
405
|
+
const config = Synx.parse(text, opts);
|
|
406
|
+
callback(config);
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
callback({}, e);
|
|
410
|
+
}
|
|
411
|
+
const watcher = fs.watch(absPath, { persistent: true }, (_event) => {
|
|
412
|
+
try {
|
|
413
|
+
const text = fs.readFileSync(absPath, 'utf-8');
|
|
414
|
+
const config = Synx.parse(text, opts);
|
|
415
|
+
callback(config);
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
callback({}, e);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return { close: () => watcher.close() };
|
|
422
|
+
}
|
|
423
|
+
/** Extract a JSON Schema from SYNX constraints. @since 3.1.3 */
|
|
424
|
+
static schema(text) {
|
|
425
|
+
const { root } = (0, parser_1.parseData)(text);
|
|
426
|
+
const metaMap = root.__synx;
|
|
427
|
+
const properties = {};
|
|
428
|
+
const required = [];
|
|
429
|
+
if (metaMap) {
|
|
430
|
+
for (const [key, meta] of Object.entries(metaMap)) {
|
|
431
|
+
if (!meta.constraints)
|
|
432
|
+
continue;
|
|
433
|
+
const c = meta.constraints;
|
|
434
|
+
const prop = {};
|
|
435
|
+
if (c.type) {
|
|
436
|
+
const typeMap = {
|
|
437
|
+
int: 'integer', float: 'number', bool: 'boolean', string: 'string',
|
|
438
|
+
};
|
|
439
|
+
prop.type = typeMap[c.type] || c.type;
|
|
440
|
+
}
|
|
441
|
+
if (c.min !== undefined)
|
|
442
|
+
prop.minimum = c.min;
|
|
443
|
+
if (c.max !== undefined)
|
|
444
|
+
prop.maximum = c.max;
|
|
445
|
+
if (c.pattern)
|
|
446
|
+
prop.pattern = c.pattern;
|
|
447
|
+
if (c.enum)
|
|
448
|
+
prop.enum = c.enum;
|
|
449
|
+
if (c.required) {
|
|
450
|
+
prop.required = true;
|
|
451
|
+
required.push(key);
|
|
452
|
+
}
|
|
453
|
+
properties[key] = prop;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties,
|
|
460
|
+
required,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Structural diff between two parsed SYNX objects.
|
|
465
|
+
* Compares top-level keys and returns added, removed, changed, and unchanged.
|
|
466
|
+
*
|
|
467
|
+
* @param a - First object (before).
|
|
468
|
+
* @param b - Second object (after).
|
|
469
|
+
* @returns A SynxDiff describing the structural differences.
|
|
470
|
+
*
|
|
471
|
+
* @since 3.6.0
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```ts
|
|
475
|
+
* const before = Synx.parse('name Alice\nage 30');
|
|
476
|
+
* const after = Synx.parse('name Bob\nage 30\nrole admin');
|
|
477
|
+
* const diff = Synx.diff(before, after);
|
|
478
|
+
* // diff.added → { role: 'admin' }
|
|
479
|
+
* // diff.removed → {}
|
|
480
|
+
* // diff.changed → { name: { from: 'Alice', to: 'Bob' } }
|
|
481
|
+
* // diff.unchanged → ['age']
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
static diff(a, b) {
|
|
485
|
+
const added = {};
|
|
486
|
+
const removed = {};
|
|
487
|
+
const changed = {};
|
|
488
|
+
const unchanged = [];
|
|
489
|
+
const aKeys = new Set(Object.keys(a));
|
|
490
|
+
const bKeys = new Set(Object.keys(b));
|
|
491
|
+
for (const key of aKeys) {
|
|
492
|
+
if (!bKeys.has(key)) {
|
|
493
|
+
removed[key] = a[key];
|
|
494
|
+
}
|
|
495
|
+
else if (deepEqual(a[key], b[key])) {
|
|
496
|
+
unchanged.push(key);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
changed[key] = { from: a[key], to: b[key] };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (const key of bKeys) {
|
|
503
|
+
if (!aKeys.has(key)) {
|
|
504
|
+
added[key] = b[key];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return { added, removed, changed, unchanged };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
exports.Synx = Synx;
|
|
511
|
+
// ─── Deep equality helper ─────────────────────────────────
|
|
512
|
+
function deepEqual(a, b) {
|
|
513
|
+
if (a === b)
|
|
514
|
+
return true;
|
|
515
|
+
if (a === null || b === null)
|
|
516
|
+
return false;
|
|
517
|
+
if (typeof a !== typeof b)
|
|
518
|
+
return false;
|
|
519
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
520
|
+
if (a.length !== b.length)
|
|
521
|
+
return false;
|
|
522
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
523
|
+
}
|
|
524
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
525
|
+
const aObj = a;
|
|
526
|
+
const bObj = b;
|
|
527
|
+
const aKeys = Object.keys(aObj);
|
|
528
|
+
const bKeys = Object.keys(bObj);
|
|
529
|
+
if (aKeys.length !== bKeys.length)
|
|
530
|
+
return false;
|
|
531
|
+
return aKeys.every(k => deepEqual(aObj[k], bObj[k]));
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
// ─── Serializer ───────────────────────────────────────────
|
|
536
|
+
function serializeObject(obj, indent) {
|
|
537
|
+
let out = '';
|
|
538
|
+
const spaces = ' '.repeat(indent);
|
|
539
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
540
|
+
if (Array.isArray(val)) {
|
|
541
|
+
out += `${spaces}${key}\n`;
|
|
542
|
+
for (const item of val) {
|
|
543
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
544
|
+
const entries = Object.entries(item);
|
|
545
|
+
if (entries.length > 0) {
|
|
546
|
+
const [firstKey, firstVal] = entries[0];
|
|
547
|
+
out += `${spaces} - ${firstKey} ${firstVal}\n`;
|
|
548
|
+
for (let i = 1; i < entries.length; i++) {
|
|
549
|
+
out += `${spaces} ${entries[i][0]} ${entries[i][1]}\n`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
out += `${spaces} - ${item}\n`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else if (val && typeof val === 'object') {
|
|
559
|
+
out += `${spaces}${key}\n`;
|
|
560
|
+
out += serializeObject(val, indent + 2);
|
|
561
|
+
}
|
|
562
|
+
else if (typeof val === 'string' && val.includes('\n')) {
|
|
563
|
+
out += `${spaces}${key} |\n`;
|
|
564
|
+
for (const line of val.split('\n')) {
|
|
565
|
+
out += `${spaces} ${line}\n`;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
out += `${spaces}${key} ${val}\n`;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
function fmtParse(lines, start, base) {
|
|
575
|
+
const nodes = [];
|
|
576
|
+
let i = start;
|
|
577
|
+
while (i < lines.length) {
|
|
578
|
+
const raw = lines[i];
|
|
579
|
+
const t = raw.trim();
|
|
580
|
+
if (!t) {
|
|
581
|
+
i++;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const ind = raw.search(/\S/);
|
|
585
|
+
if (ind < base)
|
|
586
|
+
break;
|
|
587
|
+
if (ind > base) {
|
|
588
|
+
i++;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (t.startsWith('- ') || t.startsWith('#') || t.startsWith('//')) {
|
|
592
|
+
i++;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const isMultiline = t.trimEnd().endsWith(' |') || t === '|';
|
|
596
|
+
const node = { header: t, children: [], listItems: [], isMultiline };
|
|
597
|
+
i++;
|
|
598
|
+
while (i < lines.length) {
|
|
599
|
+
const cr = lines[i];
|
|
600
|
+
const ct = cr.trim();
|
|
601
|
+
if (!ct) {
|
|
602
|
+
i++;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const ci = cr.search(/\S/);
|
|
606
|
+
if (ci <= base)
|
|
607
|
+
break;
|
|
608
|
+
if (isMultiline || ct.startsWith('- ')) {
|
|
609
|
+
node.listItems.push(ct);
|
|
610
|
+
i++;
|
|
611
|
+
}
|
|
612
|
+
else if (ct.startsWith('#') || ct.startsWith('//')) {
|
|
613
|
+
i++;
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
const [subs, ni] = fmtParse(lines, i, ci);
|
|
617
|
+
node.children.push(...subs);
|
|
618
|
+
i = ni;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
nodes.push(node);
|
|
622
|
+
}
|
|
623
|
+
return [nodes, i];
|
|
624
|
+
}
|
|
625
|
+
function fmtSort(nodes) {
|
|
626
|
+
nodes.sort((a, b) => {
|
|
627
|
+
const ka = a.header.split(/[\s\[:(]/)[0].toLowerCase();
|
|
628
|
+
const kb = b.header.split(/[\s\[:(]/)[0].toLowerCase();
|
|
629
|
+
return ka.localeCompare(kb);
|
|
630
|
+
});
|
|
631
|
+
for (const n of nodes)
|
|
632
|
+
fmtSort(n.children);
|
|
633
|
+
}
|
|
634
|
+
function fmtEmit(nodes, indent) {
|
|
635
|
+
const sp = ' '.repeat(indent);
|
|
636
|
+
let out = '';
|
|
637
|
+
for (const n of nodes) {
|
|
638
|
+
out += `${sp}${n.header}\n`;
|
|
639
|
+
if (n.children.length > 0)
|
|
640
|
+
out += fmtEmit(n.children, indent + 2);
|
|
641
|
+
for (const li of n.listItems)
|
|
642
|
+
out += `${sp} ${li}\n`;
|
|
643
|
+
if (indent === 0 && (n.children.length > 0 || n.listItems.length > 0))
|
|
644
|
+
out += '\n';
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
// ─── Export Converters ────────────────────────────────────
|
|
649
|
+
function toJSONString(obj, pretty = true) {
|
|
650
|
+
return pretty ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
|
|
651
|
+
}
|
|
652
|
+
function toYAMLString(value, indent = 0) {
|
|
653
|
+
const sp = ' '.repeat(indent);
|
|
654
|
+
if (value === null || value === undefined)
|
|
655
|
+
return `${sp}null\n`;
|
|
656
|
+
if (typeof value === 'boolean' || typeof value === 'number')
|
|
657
|
+
return `${sp}${value}\n`;
|
|
658
|
+
if (typeof value === 'string') {
|
|
659
|
+
if (value.includes('\n') || value.includes(':') || value.includes('#') ||
|
|
660
|
+
value.startsWith('{') || value.startsWith('[') || value.startsWith('"') ||
|
|
661
|
+
value.startsWith("'") || /^(true|false|null|yes|no|on|off)$/i.test(value) ||
|
|
662
|
+
value === '') {
|
|
663
|
+
return `${sp}${JSON.stringify(value)}\n`;
|
|
664
|
+
}
|
|
665
|
+
return `${sp}${value}\n`;
|
|
666
|
+
}
|
|
667
|
+
if (Array.isArray(value)) {
|
|
668
|
+
if (value.length === 0)
|
|
669
|
+
return `${sp}[]\n`;
|
|
670
|
+
let out = '';
|
|
671
|
+
for (const item of value) {
|
|
672
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
673
|
+
out += `${sp}- `;
|
|
674
|
+
const entries = Object.entries(item);
|
|
675
|
+
if (entries.length > 0) {
|
|
676
|
+
const [fk, fv] = entries[0];
|
|
677
|
+
out += `${fk}: ${toYAMLValue(fv)}\n`;
|
|
678
|
+
for (let i = 1; i < entries.length; i++) {
|
|
679
|
+
out += `${sp} ${entries[i][0]}: ${toYAMLValue(entries[i][1])}\n`;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
out += `${sp}- ${toYAMLValue(item)}\n`;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
if (typeof value === 'object') {
|
|
690
|
+
let out = '';
|
|
691
|
+
for (const [k, v] of Object.entries(value)) {
|
|
692
|
+
if (k.startsWith('__synx'))
|
|
693
|
+
continue;
|
|
694
|
+
if (v && typeof v === 'object') {
|
|
695
|
+
out += `${sp}${k}:\n`;
|
|
696
|
+
out += Array.isArray(v)
|
|
697
|
+
? toYAMLString(v, indent + 2)
|
|
698
|
+
: toYAMLString(v, indent + 2);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
out += `${sp}${k}: ${toYAMLValue(v)}\n`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
return `${sp}${String(value)}\n`;
|
|
707
|
+
}
|
|
708
|
+
function toYAMLValue(v) {
|
|
709
|
+
if (v === null || v === undefined)
|
|
710
|
+
return 'null';
|
|
711
|
+
if (typeof v === 'boolean' || typeof v === 'number')
|
|
712
|
+
return String(v);
|
|
713
|
+
if (typeof v === 'string') {
|
|
714
|
+
if (v.includes('\n') || v.includes(':') || v.includes('#') ||
|
|
715
|
+
v.startsWith('{') || v.startsWith('[') || v.startsWith('"') ||
|
|
716
|
+
v.startsWith("'") || /^(true|false|null|yes|no|on|off)$/i.test(v) ||
|
|
717
|
+
v === '') {
|
|
718
|
+
return JSON.stringify(v);
|
|
719
|
+
}
|
|
720
|
+
return v;
|
|
721
|
+
}
|
|
722
|
+
return JSON.stringify(v);
|
|
723
|
+
}
|
|
724
|
+
function toTOMLString(obj, prefix = '') {
|
|
725
|
+
let out = '';
|
|
726
|
+
const simple = [];
|
|
727
|
+
const tables = [];
|
|
728
|
+
const arrays = [];
|
|
729
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
730
|
+
if (k.startsWith('__synx'))
|
|
731
|
+
continue;
|
|
732
|
+
if (Array.isArray(v)) {
|
|
733
|
+
const allObjects = v.length > 0 && v.every(i => i && typeof i === 'object' && !Array.isArray(i));
|
|
734
|
+
if (allObjects) {
|
|
735
|
+
arrays.push([k, v]);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
simple.push([k, v]);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else if (v && typeof v === 'object') {
|
|
742
|
+
tables.push([k, v]);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
simple.push([k, v]);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
for (const [k, v] of simple) {
|
|
749
|
+
out += `${k} = ${toTOMLValue(v)}\n`;
|
|
750
|
+
}
|
|
751
|
+
for (const [k, v] of tables) {
|
|
752
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
753
|
+
out += `\n[${path}]\n`;
|
|
754
|
+
out += toTOMLString(v, path);
|
|
755
|
+
}
|
|
756
|
+
for (const [k, arr] of arrays) {
|
|
757
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
758
|
+
for (const item of arr) {
|
|
759
|
+
out += `\n[[${path}]]\n`;
|
|
760
|
+
out += toTOMLString(item, path);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
function toTOMLValue(v) {
|
|
766
|
+
if (v === null || v === undefined)
|
|
767
|
+
return '""';
|
|
768
|
+
if (typeof v === 'boolean')
|
|
769
|
+
return String(v);
|
|
770
|
+
if (typeof v === 'number') {
|
|
771
|
+
if (Number.isInteger(v))
|
|
772
|
+
return String(v);
|
|
773
|
+
const s = String(v);
|
|
774
|
+
return s.includes('.') ? s : `${s}.0`;
|
|
775
|
+
}
|
|
776
|
+
if (typeof v === 'string')
|
|
777
|
+
return JSON.stringify(v);
|
|
778
|
+
if (Array.isArray(v))
|
|
779
|
+
return `[${v.map(toTOMLValue).join(', ')}]`;
|
|
780
|
+
return JSON.stringify(v);
|
|
781
|
+
}
|
|
782
|
+
function toEnvString(obj, prefix = '') {
|
|
783
|
+
let out = '';
|
|
784
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
785
|
+
if (k.startsWith('__synx'))
|
|
786
|
+
continue;
|
|
787
|
+
const envKey = prefix ? `${prefix}_${k}`.toUpperCase() : k.toUpperCase();
|
|
788
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
789
|
+
out += toEnvString(v, envKey);
|
|
790
|
+
}
|
|
791
|
+
else if (Array.isArray(v)) {
|
|
792
|
+
out += `${envKey}=${v.map(String).join(',')}\n`;
|
|
793
|
+
}
|
|
794
|
+
else if (v === null) {
|
|
795
|
+
out += `${envKey}=\n`;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
const s = String(v);
|
|
799
|
+
out += s.includes(' ') || s.includes('"') ? `${envKey}="${s}"\n` : `${envKey}=${s}\n`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return out;
|
|
803
|
+
}
|
|
804
|
+
// ─── Exports ──────────────────────────────────────────────
|
|
805
|
+
exports.default = Synx;
|
|
806
|
+
module.exports = Synx;
|
|
807
|
+
module.exports.default = Synx;
|
|
808
|
+
module.exports.Synx = Synx;
|
|
809
|
+
module.exports.SynxError = types_1.SynxError;
|
|
810
|
+
//# sourceMappingURL=index.js.map
|