@jtml/core 0.1.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 +370 -0
- package/dist/chunk-SHDXMADE.mjs +675 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +684 -0
- package/dist/cli.mjs +168 -0
- package/dist/index.d.mts +262 -0
- package/dist/index.d.ts +262 -0
- package/dist/index.js +726 -0
- package/dist/index.mjs +58 -0
- package/package.json +62 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_fs = require("fs");
|
|
6
|
+
|
|
7
|
+
// src/core/types.ts
|
|
8
|
+
var JTMLError = class extends Error {
|
|
9
|
+
constructor(message, code) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "JTMLError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/core/schema.ts
|
|
17
|
+
var SchemaManager = class {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.schemas = /* @__PURE__ */ new Map();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Register a schema for reuse
|
|
23
|
+
*/
|
|
24
|
+
register(schema) {
|
|
25
|
+
this.schemas.set(schema.id, schema);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get a registered schema
|
|
29
|
+
*/
|
|
30
|
+
get(id) {
|
|
31
|
+
return this.schemas.get(id);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if schema exists
|
|
35
|
+
*/
|
|
36
|
+
has(id) {
|
|
37
|
+
return this.schemas.has(id);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clear all schemas
|
|
41
|
+
*/
|
|
42
|
+
clear() {
|
|
43
|
+
this.schemas.clear();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Export all schemas
|
|
47
|
+
*/
|
|
48
|
+
export() {
|
|
49
|
+
return Array.from(this.schemas.values());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Import schemas
|
|
53
|
+
*/
|
|
54
|
+
import(schemas) {
|
|
55
|
+
schemas.forEach((schema) => this.register(schema));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function inferType(value) {
|
|
59
|
+
if (value === null || value === void 0) {
|
|
60
|
+
return { type: "n" };
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "boolean") {
|
|
63
|
+
return { type: "b" };
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "number") {
|
|
66
|
+
return Number.isInteger(value) ? { type: "i" } : { type: "f" };
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "string") {
|
|
69
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
70
|
+
return { type: "t" };
|
|
71
|
+
}
|
|
72
|
+
return { type: "s" };
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
if (value.length === 0) {
|
|
76
|
+
return { type: "a" };
|
|
77
|
+
}
|
|
78
|
+
const firstItemType = inferType(value[0]);
|
|
79
|
+
return { type: "a", arrayOf: firstItemType.type };
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === "object") {
|
|
82
|
+
return { type: "o" };
|
|
83
|
+
}
|
|
84
|
+
throw new JTMLError(`Cannot infer type for value: ${value}`, "TYPE_INFERENCE_ERROR");
|
|
85
|
+
}
|
|
86
|
+
function inferSchema(data, schemaId) {
|
|
87
|
+
if (!Array.isArray(data) && typeof data !== "object") {
|
|
88
|
+
throw new JTMLError("Schema inference requires array or object data", "INVALID_DATA");
|
|
89
|
+
}
|
|
90
|
+
const fields = [];
|
|
91
|
+
const sample = Array.isArray(data) ? data[0] : data;
|
|
92
|
+
if (!sample || typeof sample !== "object") {
|
|
93
|
+
throw new JTMLError("Cannot infer schema from empty or non-object data", "INVALID_DATA");
|
|
94
|
+
}
|
|
95
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
96
|
+
const typeInfo = inferType(value);
|
|
97
|
+
let optional = false;
|
|
98
|
+
if (Array.isArray(data)) {
|
|
99
|
+
optional = data.some(
|
|
100
|
+
(item) => item[key] === null || item[key] === void 0
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
fields.push({
|
|
104
|
+
name: key,
|
|
105
|
+
typeInfo: { ...typeInfo, optional }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
id: schemaId,
|
|
110
|
+
fields,
|
|
111
|
+
version: "1.0"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function serializeSchema(schema) {
|
|
115
|
+
const fieldDefs = schema.fields.map((field) => {
|
|
116
|
+
let def = `${field.name}:${field.typeInfo.type}`;
|
|
117
|
+
if (field.typeInfo.arrayOf) {
|
|
118
|
+
def += `[]`;
|
|
119
|
+
}
|
|
120
|
+
if (field.typeInfo.enumValues) {
|
|
121
|
+
def += `[${field.typeInfo.enumValues.join(",")}]`;
|
|
122
|
+
}
|
|
123
|
+
if (field.typeInfo.refSchema) {
|
|
124
|
+
def += `[${field.typeInfo.refSchema}]`;
|
|
125
|
+
}
|
|
126
|
+
if (field.typeInfo.optional) {
|
|
127
|
+
def += "?";
|
|
128
|
+
}
|
|
129
|
+
return def;
|
|
130
|
+
}).join(" ");
|
|
131
|
+
return `@schema ${schema.id}
|
|
132
|
+
${fieldDefs}`;
|
|
133
|
+
}
|
|
134
|
+
function parseSchema(schemaStr) {
|
|
135
|
+
const lines = schemaStr.trim().split("\n");
|
|
136
|
+
const headerMatch = lines[0].match(/^@schema\s+(\S+)/);
|
|
137
|
+
if (!headerMatch) {
|
|
138
|
+
throw new JTMLError("Invalid schema format", "SCHEMA_PARSE_ERROR");
|
|
139
|
+
}
|
|
140
|
+
const schemaId = headerMatch[1];
|
|
141
|
+
if (lines.length < 2 || !lines[1]) {
|
|
142
|
+
throw new JTMLError("Schema is missing field definitions", "SCHEMA_PARSE_ERROR");
|
|
143
|
+
}
|
|
144
|
+
const fieldLine = lines[1];
|
|
145
|
+
const fields = [];
|
|
146
|
+
const fieldDefs = fieldLine.split(/\s+/);
|
|
147
|
+
for (const fieldDef of fieldDefs) {
|
|
148
|
+
const match = fieldDef.match(/^(\w+):([ifsbtnoae]+)(\[\])?(\[([^\]]+)\])?(\?)?$/);
|
|
149
|
+
if (!match) {
|
|
150
|
+
throw new JTMLError(`Invalid field definition: ${fieldDef}`, "SCHEMA_PARSE_ERROR");
|
|
151
|
+
}
|
|
152
|
+
const [, name, type, isArray, , enumOrRef, isOptional] = match;
|
|
153
|
+
const typeInfo = {
|
|
154
|
+
type,
|
|
155
|
+
optional: !!isOptional
|
|
156
|
+
};
|
|
157
|
+
if (isArray) {
|
|
158
|
+
typeInfo.arrayOf = type;
|
|
159
|
+
}
|
|
160
|
+
if (enumOrRef) {
|
|
161
|
+
if (type === "e") {
|
|
162
|
+
typeInfo.enumValues = enumOrRef.split(",");
|
|
163
|
+
} else if (type === "ref") {
|
|
164
|
+
typeInfo.refSchema = enumOrRef;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
fields.push({ name, typeInfo });
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
id: schemaId,
|
|
171
|
+
fields,
|
|
172
|
+
version: "1.0"
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
var schemaManager = new SchemaManager();
|
|
176
|
+
|
|
177
|
+
// src/core/encoder.ts
|
|
178
|
+
var JTMLEncoder = class {
|
|
179
|
+
/**
|
|
180
|
+
* Encode JSON data to JTML format
|
|
181
|
+
*/
|
|
182
|
+
encode(data, options = {}) {
|
|
183
|
+
const {
|
|
184
|
+
schemaId = "default",
|
|
185
|
+
schemaRef,
|
|
186
|
+
autoInferTypes = true,
|
|
187
|
+
includeSchema = true
|
|
188
|
+
} = options;
|
|
189
|
+
let schema;
|
|
190
|
+
if (schemaRef) {
|
|
191
|
+
schema = schemaManager.get(schemaRef);
|
|
192
|
+
if (!schema) {
|
|
193
|
+
throw new JTMLError(`Schema not found: ${schemaRef}`, "SCHEMA_NOT_FOUND");
|
|
194
|
+
}
|
|
195
|
+
return this.encodeWithSchema(data, schema, false);
|
|
196
|
+
}
|
|
197
|
+
if (autoInferTypes) {
|
|
198
|
+
schema = inferSchema(data, schemaId);
|
|
199
|
+
schemaManager.register(schema);
|
|
200
|
+
}
|
|
201
|
+
if (schema) {
|
|
202
|
+
return this.encodeWithSchema(data, schema, includeSchema);
|
|
203
|
+
}
|
|
204
|
+
return this.encodeSimple(data);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Encode with explicit schema
|
|
208
|
+
*/
|
|
209
|
+
encodeWithSchema(data, schema, includeSchema) {
|
|
210
|
+
const parts = [];
|
|
211
|
+
if (includeSchema) {
|
|
212
|
+
parts.push(serializeSchema(schema));
|
|
213
|
+
parts.push("");
|
|
214
|
+
parts.push("@data");
|
|
215
|
+
} else {
|
|
216
|
+
parts.push(`@ref ${schema.id}`);
|
|
217
|
+
parts.push("@data");
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(data)) {
|
|
220
|
+
parts.push("@array");
|
|
221
|
+
for (const item of data) {
|
|
222
|
+
parts.push(this.encodeRow(item, schema));
|
|
223
|
+
}
|
|
224
|
+
} else if (typeof data === "object" && data !== null) {
|
|
225
|
+
parts.push(this.encodeRow(data, schema));
|
|
226
|
+
}
|
|
227
|
+
return parts.join("\n");
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Encode a single row according to schema
|
|
231
|
+
*/
|
|
232
|
+
encodeRow(item, schema) {
|
|
233
|
+
if (typeof item !== "object" || item === null) {
|
|
234
|
+
throw new JTMLError("Cannot encode non-object item", "INVALID_DATA");
|
|
235
|
+
}
|
|
236
|
+
const values = [];
|
|
237
|
+
const obj = item;
|
|
238
|
+
for (const field of schema.fields) {
|
|
239
|
+
const value = obj[field.name];
|
|
240
|
+
values.push(this.encodeValue(value));
|
|
241
|
+
}
|
|
242
|
+
return values.join("|");
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Encode a single value
|
|
246
|
+
*/
|
|
247
|
+
encodeValue(value) {
|
|
248
|
+
if (value === null || value === void 0) {
|
|
249
|
+
return "";
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === "boolean") {
|
|
252
|
+
return value ? "1" : "0";
|
|
253
|
+
}
|
|
254
|
+
if (typeof value === "number") {
|
|
255
|
+
return String(value);
|
|
256
|
+
}
|
|
257
|
+
if (typeof value === "string") {
|
|
258
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, "\\n");
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
return `[${value.map((v) => this.encodeValue(v)).join(",")}]`;
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === "object") {
|
|
264
|
+
return `{${Object.entries(value).map(([k, v]) => `${k}:${this.encodeValue(v)}`).join(",")}}`;
|
|
265
|
+
}
|
|
266
|
+
return String(value);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Simple encoding without schema (fallback)
|
|
270
|
+
*/
|
|
271
|
+
encodeSimple(data) {
|
|
272
|
+
if (Array.isArray(data)) {
|
|
273
|
+
return data.map((item) => JSON.stringify(item)).join("\n");
|
|
274
|
+
}
|
|
275
|
+
return JSON.stringify(data);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Encode with metadata
|
|
279
|
+
*/
|
|
280
|
+
encodeWithMetadata(data, metadata, options = {}) {
|
|
281
|
+
const dataEncoded = this.encode(data, options);
|
|
282
|
+
const metaParts = ["", "@meta"];
|
|
283
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
284
|
+
metaParts.push(`${key}:${this.encodeValue(value)}`);
|
|
285
|
+
}
|
|
286
|
+
return dataEncoded + "\n" + metaParts.join("\n");
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var encoder = new JTMLEncoder();
|
|
290
|
+
function encode(data, options) {
|
|
291
|
+
return encoder.encode(data, options);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/core/decoder.ts
|
|
295
|
+
var JTMLDecoder = class {
|
|
296
|
+
/**
|
|
297
|
+
* Decode JTML format to JSON
|
|
298
|
+
*/
|
|
299
|
+
decode(jtml, options = {}) {
|
|
300
|
+
const { schemaCache, strict = true } = options;
|
|
301
|
+
if (schemaCache) {
|
|
302
|
+
schemaCache.forEach((schema2, _id) => {
|
|
303
|
+
schemaManager.register(schema2);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const lines = jtml.trim().split("\n");
|
|
307
|
+
let schema;
|
|
308
|
+
let dataStartIndex = 0;
|
|
309
|
+
let metadata = {};
|
|
310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
311
|
+
const line = lines[i].trim();
|
|
312
|
+
if (line.startsWith("@schema")) {
|
|
313
|
+
if (i + 1 >= lines.length) {
|
|
314
|
+
throw new JTMLError("Incomplete schema definition: missing field definitions", "SCHEMA_PARSE_ERROR");
|
|
315
|
+
}
|
|
316
|
+
const schemaLines = [line, lines[i + 1]];
|
|
317
|
+
schema = parseSchema(schemaLines.join("\n"));
|
|
318
|
+
schemaManager.register(schema);
|
|
319
|
+
i++;
|
|
320
|
+
} else if (line.startsWith("@ref")) {
|
|
321
|
+
const schemaId = line.split(/\s+/)[1];
|
|
322
|
+
if (!schemaId) {
|
|
323
|
+
throw new JTMLError("Missing schema ID in @ref directive", "SCHEMA_PARSE_ERROR");
|
|
324
|
+
}
|
|
325
|
+
schema = schemaManager.get(schemaId);
|
|
326
|
+
if (!schema) {
|
|
327
|
+
throw new JTMLError(`Schema not found: ${schemaId}`, "SCHEMA_NOT_FOUND");
|
|
328
|
+
}
|
|
329
|
+
} else if (line === "@data") {
|
|
330
|
+
dataStartIndex = i + 1;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!schema && strict) {
|
|
335
|
+
throw new JTMLError("No schema found in JTML data", "SCHEMA_REQUIRED");
|
|
336
|
+
}
|
|
337
|
+
let metaStartIndex = -1;
|
|
338
|
+
for (let i = dataStartIndex; i < lines.length; i++) {
|
|
339
|
+
if (lines[i].trim() === "@meta") {
|
|
340
|
+
metaStartIndex = i + 1;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const dataEndIndex = metaStartIndex > 0 ? metaStartIndex - 1 : lines.length;
|
|
345
|
+
const rawDataLines = lines.slice(dataStartIndex, dataEndIndex).filter((l) => l.trim());
|
|
346
|
+
const isArrayEncoding = rawDataLines[0]?.trim() === "@array";
|
|
347
|
+
const dataLines = isArrayEncoding ? rawDataLines.slice(1) : rawDataLines;
|
|
348
|
+
const results = [];
|
|
349
|
+
if (schema) {
|
|
350
|
+
for (const line of dataLines) {
|
|
351
|
+
if (line.trim()) {
|
|
352
|
+
results.push(this.decodeRow(line, schema));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
for (const line of dataLines) {
|
|
357
|
+
if (line.trim()) {
|
|
358
|
+
results.push(JSON.parse(line));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (metaStartIndex > 0) {
|
|
363
|
+
for (let i = metaStartIndex; i < lines.length; i++) {
|
|
364
|
+
const line = lines[i].trim();
|
|
365
|
+
if (line && !line.startsWith("@")) {
|
|
366
|
+
const colonIdx = line.indexOf(":");
|
|
367
|
+
if (colonIdx === -1) continue;
|
|
368
|
+
const key = line.slice(0, colonIdx);
|
|
369
|
+
const value = line.slice(colonIdx + 1);
|
|
370
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
371
|
+
metadata[key] = this.decodeValue(value);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!isArrayEncoding && results.length === 1 && Object.keys(metadata).length === 0) {
|
|
376
|
+
return results[0];
|
|
377
|
+
}
|
|
378
|
+
if (Object.keys(metadata).length > 0) {
|
|
379
|
+
return {
|
|
380
|
+
data: results.length === 1 ? results[0] : results,
|
|
381
|
+
metadata
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return results;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Decode a single row according to schema
|
|
388
|
+
*/
|
|
389
|
+
decodeRow(line, schema) {
|
|
390
|
+
const values = this.splitRow(line);
|
|
391
|
+
const result = {};
|
|
392
|
+
if (values.length !== schema.fields.length) {
|
|
393
|
+
throw new JTMLError(
|
|
394
|
+
`Row has ${values.length} values but schema expects ${schema.fields.length}`,
|
|
395
|
+
"SCHEMA_MISMATCH"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
399
|
+
const field = schema.fields[i];
|
|
400
|
+
const rawValue = values[i];
|
|
401
|
+
if (rawValue === "" || rawValue === null) {
|
|
402
|
+
result[field.name] = null;
|
|
403
|
+
} else {
|
|
404
|
+
result[field.name] = this.decodeValue(rawValue, field.typeInfo.type);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Split row by pipe delimiter, handling escaped pipes
|
|
411
|
+
*/
|
|
412
|
+
splitRow(line) {
|
|
413
|
+
const parts = [];
|
|
414
|
+
let current = "";
|
|
415
|
+
let escaped = false;
|
|
416
|
+
for (let i = 0; i < line.length; i++) {
|
|
417
|
+
const char = line[i];
|
|
418
|
+
if (escaped) {
|
|
419
|
+
if (char === "n") {
|
|
420
|
+
current += "\n";
|
|
421
|
+
} else if (char === "\\") {
|
|
422
|
+
current += "\\";
|
|
423
|
+
} else if (char === "|") {
|
|
424
|
+
current += "|";
|
|
425
|
+
} else {
|
|
426
|
+
current += char;
|
|
427
|
+
}
|
|
428
|
+
escaped = false;
|
|
429
|
+
} else if (char === "\\") {
|
|
430
|
+
escaped = true;
|
|
431
|
+
} else if (char === "|") {
|
|
432
|
+
parts.push(current);
|
|
433
|
+
current = "";
|
|
434
|
+
} else {
|
|
435
|
+
current += char;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
parts.push(current);
|
|
439
|
+
return parts;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Decode a single value
|
|
443
|
+
*/
|
|
444
|
+
decodeValue(value, type) {
|
|
445
|
+
if (value === void 0 || value === "" || value === null) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
if (type === "b") {
|
|
449
|
+
return value === "1" || value === "true";
|
|
450
|
+
}
|
|
451
|
+
if (type === "i" || type === "f") {
|
|
452
|
+
const num = type === "i" ? parseInt(value, 10) : parseFloat(value);
|
|
453
|
+
if (!isFinite(num)) {
|
|
454
|
+
throw new JTMLError(`Invalid numeric value: ${value}`, "INVALID_VALUE");
|
|
455
|
+
}
|
|
456
|
+
return num;
|
|
457
|
+
}
|
|
458
|
+
if (!type && /^-?\d+(\.\d+)?$/.test(value)) {
|
|
459
|
+
return value.includes(".") ? parseFloat(value) : parseInt(value, 10);
|
|
460
|
+
}
|
|
461
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
462
|
+
const inner = value.slice(1, -1);
|
|
463
|
+
if (!inner) return [];
|
|
464
|
+
return inner.split(",").map((v) => this.decodeValue(v.trim(), type));
|
|
465
|
+
}
|
|
466
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
467
|
+
const inner = value.slice(1, -1);
|
|
468
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
469
|
+
if (!inner) return obj;
|
|
470
|
+
const pairs = inner.split(",");
|
|
471
|
+
for (const pair of pairs) {
|
|
472
|
+
const colonIdx = pair.indexOf(":");
|
|
473
|
+
if (colonIdx === -1) continue;
|
|
474
|
+
const k = pair.slice(0, colonIdx).trim();
|
|
475
|
+
const v = pair.slice(colonIdx + 1).trim();
|
|
476
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
477
|
+
obj[k] = this.decodeValue(v);
|
|
478
|
+
}
|
|
479
|
+
return obj;
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var decoder = new JTMLDecoder();
|
|
485
|
+
function decode(jtml, options) {
|
|
486
|
+
return decoder.decode(jtml, options);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/utils/tokenizer.ts
|
|
490
|
+
function estimateTokens(text, tokenizer = "claude") {
|
|
491
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
492
|
+
const charsPerToken = {
|
|
493
|
+
gpt: 4,
|
|
494
|
+
claude: 3.8,
|
|
495
|
+
llama: 4.2
|
|
496
|
+
};
|
|
497
|
+
const ratio = charsPerToken[tokenizer];
|
|
498
|
+
let estimate = normalized.length / ratio;
|
|
499
|
+
const structuralChars = (normalized.match(/[{}[\]":,]/g) || []).length;
|
|
500
|
+
estimate += structuralChars * 0.3;
|
|
501
|
+
const numbers = (normalized.match(/\d+/g) || []).length;
|
|
502
|
+
estimate += numbers * 0.2;
|
|
503
|
+
return Math.ceil(estimate);
|
|
504
|
+
}
|
|
505
|
+
function compareTokens(jsonText, jtmlText, tokenizer = "claude") {
|
|
506
|
+
const jsonTokens = estimateTokens(jsonText, tokenizer);
|
|
507
|
+
const jtmlTokens = estimateTokens(jtmlText, tokenizer);
|
|
508
|
+
const savings = jsonTokens - jtmlTokens;
|
|
509
|
+
const savingsPercent = jsonTokens > 0 ? savings / jsonTokens * 100 : 0;
|
|
510
|
+
return {
|
|
511
|
+
jsonTokens,
|
|
512
|
+
jtmlTokens,
|
|
513
|
+
savings,
|
|
514
|
+
savingsPercent
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function formatTokenStats(stats) {
|
|
518
|
+
return `
|
|
519
|
+
Token Comparison:
|
|
520
|
+
JSON: ${stats.jsonTokens} tokens
|
|
521
|
+
JTML: ${stats.jtmlTokens} tokens
|
|
522
|
+
Savings: ${stats.savings} tokens (${stats.savingsPercent.toFixed(2)}%)
|
|
523
|
+
`.trim();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/cli.ts
|
|
527
|
+
var args = process.argv.slice(2);
|
|
528
|
+
function printUsage() {
|
|
529
|
+
console.log(`
|
|
530
|
+
JTML - JSON Token-Minimized Language
|
|
531
|
+
|
|
532
|
+
Usage:
|
|
533
|
+
jtml encode <input.json> [output.jtml] Convert JSON to JTML
|
|
534
|
+
jtml decode <input.jtml> [output.json] Convert JTML to JSON
|
|
535
|
+
jtml compare <input.json> Compare token efficiency
|
|
536
|
+
jtml validate <input.jtml> Validate JTML format
|
|
537
|
+
jtml schema <input.json> Generate schema only
|
|
538
|
+
|
|
539
|
+
Options:
|
|
540
|
+
-h, --help Show this help message
|
|
541
|
+
-v, --version Show version
|
|
542
|
+
--schema-id <id> Set schema ID
|
|
543
|
+
--no-schema Encode without schema
|
|
544
|
+
--tokenizer <gpt|claude|llama> Set tokenizer for comparison
|
|
545
|
+
|
|
546
|
+
Examples:
|
|
547
|
+
jtml encode api-response.json # Output to stdout
|
|
548
|
+
jtml encode data.json output.jtml # Save to file
|
|
549
|
+
jtml decode data.jtml # Output to stdout
|
|
550
|
+
jtml compare large-api.json # Show token savings
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
function getVersion() {
|
|
554
|
+
try {
|
|
555
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)("./package.json", "utf-8"));
|
|
556
|
+
return pkg.version;
|
|
557
|
+
} catch {
|
|
558
|
+
return "unknown";
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function main() {
|
|
562
|
+
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
|
|
563
|
+
printUsage();
|
|
564
|
+
process.exit(0);
|
|
565
|
+
}
|
|
566
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
567
|
+
console.log(`jtml v${getVersion()}`);
|
|
568
|
+
process.exit(0);
|
|
569
|
+
}
|
|
570
|
+
const command = args[0];
|
|
571
|
+
const inputFile = args[1];
|
|
572
|
+
const outputFile = args[2];
|
|
573
|
+
const schemaIdIndex = args.indexOf("--schema-id");
|
|
574
|
+
const schemaId = schemaIdIndex >= 0 ? args[schemaIdIndex + 1] : void 0;
|
|
575
|
+
const noSchema = args.includes("--no-schema");
|
|
576
|
+
const tokenizerIndex = args.indexOf("--tokenizer");
|
|
577
|
+
const tokenizer = tokenizerIndex >= 0 ? args[tokenizerIndex + 1] : "claude";
|
|
578
|
+
try {
|
|
579
|
+
switch (command) {
|
|
580
|
+
case "encode": {
|
|
581
|
+
if (!inputFile) {
|
|
582
|
+
console.error("Error: Input file required");
|
|
583
|
+
printUsage();
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const jsonText = (0, import_fs.readFileSync)(inputFile, "utf-8");
|
|
587
|
+
const data = JSON.parse(jsonText);
|
|
588
|
+
const jtml = encode(data, {
|
|
589
|
+
schemaId,
|
|
590
|
+
includeSchema: !noSchema,
|
|
591
|
+
autoInferTypes: true
|
|
592
|
+
});
|
|
593
|
+
if (outputFile) {
|
|
594
|
+
(0, import_fs.writeFileSync)(outputFile, jtml, "utf-8");
|
|
595
|
+
console.log(`\u2713 Encoded to ${outputFile}`);
|
|
596
|
+
const stats = compareTokens(jsonText, jtml, tokenizer);
|
|
597
|
+
console.log(formatTokenStats(stats));
|
|
598
|
+
} else {
|
|
599
|
+
console.log(jtml);
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case "decode": {
|
|
604
|
+
if (!inputFile) {
|
|
605
|
+
console.error("Error: Input file required");
|
|
606
|
+
printUsage();
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
const jtml = (0, import_fs.readFileSync)(inputFile, "utf-8");
|
|
610
|
+
const data = decode(jtml);
|
|
611
|
+
const jsonText = JSON.stringify(data, null, 2);
|
|
612
|
+
if (outputFile) {
|
|
613
|
+
(0, import_fs.writeFileSync)(outputFile, jsonText, "utf-8");
|
|
614
|
+
console.log(`\u2713 Decoded to ${outputFile}`);
|
|
615
|
+
} else {
|
|
616
|
+
console.log(jsonText);
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case "compare": {
|
|
621
|
+
if (!inputFile) {
|
|
622
|
+
console.error("Error: Input file required");
|
|
623
|
+
printUsage();
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
const jsonText = (0, import_fs.readFileSync)(inputFile, "utf-8");
|
|
627
|
+
const data = JSON.parse(jsonText);
|
|
628
|
+
const jtml = encode(data);
|
|
629
|
+
const stats = compareTokens(jsonText, jtml, tokenizer);
|
|
630
|
+
console.log("\n=== Token Efficiency Comparison ===\n");
|
|
631
|
+
console.log(formatTokenStats(stats));
|
|
632
|
+
console.log(`
|
|
633
|
+
Tokenizer: ${tokenizer}`);
|
|
634
|
+
console.log(`JSON size: ${jsonText.length} chars`);
|
|
635
|
+
console.log(`JTML size: ${jtml.length} chars`);
|
|
636
|
+
console.log(`Compression: ${((jsonText.length - jtml.length) / jsonText.length * 100).toFixed(2)}%`);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "validate": {
|
|
640
|
+
if (!inputFile) {
|
|
641
|
+
console.error("Error: Input file required");
|
|
642
|
+
printUsage();
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
const jtml = (0, import_fs.readFileSync)(inputFile, "utf-8");
|
|
646
|
+
try {
|
|
647
|
+
decode(jtml);
|
|
648
|
+
console.log("\u2713 Valid JTML format");
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.error("\u2717 Invalid JTML format");
|
|
651
|
+
if (error instanceof Error) {
|
|
652
|
+
console.error(` ${error.message}`);
|
|
653
|
+
}
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case "schema": {
|
|
659
|
+
if (!inputFile) {
|
|
660
|
+
console.error("Error: Input file required");
|
|
661
|
+
printUsage();
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const jsonText = (0, import_fs.readFileSync)(inputFile, "utf-8");
|
|
665
|
+
const data = JSON.parse(jsonText);
|
|
666
|
+
const jtml = encode(data, {
|
|
667
|
+
schemaId: schemaId || "generated",
|
|
668
|
+
includeSchema: true
|
|
669
|
+
});
|
|
670
|
+
const schemaOnly = jtml.split("@data")[0].trim();
|
|
671
|
+
console.log(schemaOnly);
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
default:
|
|
675
|
+
console.error(`Error: Unknown command '${command}'`);
|
|
676
|
+
printUsage();
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
} catch (error) {
|
|
680
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
681
|
+
process.exit(1);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
main();
|