@creationix/jot 0.0.1 → 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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -23
  3. package/dist/jot.cjs +446 -0
  4. package/dist/jot.d.ts +6 -0
  5. package/dist/jot.js +442 -0
  6. package/package.json +34 -4
  7. package/SUMMARY.md +0 -151
  8. package/TOKEN_COUNTS.md +0 -97
  9. package/bun.lock +0 -19
  10. package/jot.test.ts +0 -133
  11. package/jot.ts +0 -650
  12. package/samples/chat.jot +0 -1
  13. package/samples/chat.json +0 -1
  14. package/samples/chat.pretty.jot +0 -6
  15. package/samples/chat.pretty.json +0 -16
  16. package/samples/firewall.jot +0 -1
  17. package/samples/firewall.json +0 -1
  18. package/samples/firewall.pretty.jot +0 -235
  19. package/samples/firewall.pretty.json +0 -344
  20. package/samples/github-issue.jot +0 -1
  21. package/samples/github-issue.json +0 -1
  22. package/samples/github-issue.pretty.jot +0 -15
  23. package/samples/github-issue.pretty.json +0 -20
  24. package/samples/hikes.jot +0 -1
  25. package/samples/hikes.json +0 -1
  26. package/samples/hikes.pretty.jot +0 -14
  27. package/samples/hikes.pretty.json +0 -38
  28. package/samples/irregular.jot +0 -1
  29. package/samples/irregular.json +0 -1
  30. package/samples/irregular.pretty.jot +0 -13
  31. package/samples/irregular.pretty.json +0 -23
  32. package/samples/json-counts-cache.jot +0 -1
  33. package/samples/json-counts-cache.json +0 -1
  34. package/samples/json-counts-cache.pretty.jot +0 -26
  35. package/samples/json-counts-cache.pretty.json +0 -26
  36. package/samples/key-folding-basic.jot +0 -1
  37. package/samples/key-folding-basic.json +0 -1
  38. package/samples/key-folding-basic.pretty.jot +0 -7
  39. package/samples/key-folding-basic.pretty.json +0 -25
  40. package/samples/key-folding-mixed.jot +0 -1
  41. package/samples/key-folding-mixed.json +0 -1
  42. package/samples/key-folding-mixed.pretty.jot +0 -16
  43. package/samples/key-folding-mixed.pretty.json +0 -24
  44. package/samples/key-folding-with-array.jot +0 -1
  45. package/samples/key-folding-with-array.json +0 -1
  46. package/samples/key-folding-with-array.pretty.jot +0 -6
  47. package/samples/key-folding-with-array.pretty.json +0 -29
  48. package/samples/large.jot +0 -1
  49. package/samples/large.json +0 -1
  50. package/samples/large.pretty.jot +0 -72
  51. package/samples/large.pretty.json +0 -93
  52. package/samples/logs.jot +0 -1
  53. package/samples/logs.json +0 -1
  54. package/samples/logs.pretty.jot +0 -96
  55. package/samples/logs.pretty.json +0 -350
  56. package/samples/medium.jot +0 -1
  57. package/samples/medium.json +0 -1
  58. package/samples/medium.pretty.jot +0 -13
  59. package/samples/medium.pretty.json +0 -30
  60. package/samples/metrics.jot +0 -1
  61. package/samples/metrics.json +0 -1
  62. package/samples/metrics.pretty.jot +0 -11
  63. package/samples/metrics.pretty.json +0 -25
  64. package/samples/package.jot +0 -1
  65. package/samples/package.json +0 -1
  66. package/samples/package.pretty.jot +0 -18
  67. package/samples/package.pretty.json +0 -18
  68. package/samples/products.jot +0 -1
  69. package/samples/products.json +0 -1
  70. package/samples/products.pretty.jot +0 -69
  71. package/samples/products.pretty.json +0 -235
  72. package/samples/routes.jot +0 -1
  73. package/samples/routes.json +0 -1
  74. package/samples/routes.pretty.jot +0 -142
  75. package/samples/routes.pretty.json +0 -354
  76. package/samples/small.jot +0 -1
  77. package/samples/small.json +0 -1
  78. package/samples/small.pretty.jot +0 -8
  79. package/samples/small.pretty.json +0 -12
  80. package/samples/users-50.jot +0 -1
  81. package/samples/users-50.json +0 -1
  82. package/samples/users-50.pretty.jot +0 -53
  83. package/samples/users-50.pretty.json +0 -354
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tim Caswell
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 CHANGED
@@ -1,6 +1,104 @@
1
1
  # Jot Format
2
2
 
3
- Jot is a compact, LLM friendly JSON variant designed to use fewer tokens while remaining easy to read and write.
3
+ Jot is a compact, human-readable JSON variant that uses fewer tokens for LLM applications.
4
+
5
+ **JSON:**
6
+
7
+ ```json
8
+ {"context":{"task":"Our favorite hikes together","location":"Boulder"},"friends":["ana","luis","sam"],"hikes":[{"id":1,"name":"Blue Lake Trail","km":7.5},{"id":2,"name":"Ridge Overlook","km":9.2}]}
9
+ ```
10
+
11
+ **Jot:**
12
+
13
+ ```jot
14
+ {context:{task:Our favorite hikes together,location:Boulder},friends:[ana,luis,sam],hikes:{{:id,name,km;1,Blue Lake Trail,7.5;2,Ridge Overlook,9.2}}}
15
+ ```
16
+
17
+ Same data, 26% fewer tokens, still readable.
18
+
19
+ ## Why Jot?
20
+
21
+ - **Save on LLM costs** — Fewer tokens = lower API bills
22
+ - **Fit more in context** — Get more data into your prompts
23
+ - **Human readable** — Unlike binary formats, you can read and write it directly
24
+ - **JSON compatible** — Parses to the same JavaScript objects
25
+
26
+ ## Token Savings
27
+
28
+ <!-- START TOKEN SAVINGS -->
29
+ Across 18 sample files, Jot averages **13% token savings**.
30
+
31
+ | Sample | JSON | Jot | Savings |
32
+ |--------|------|-----|---------|
33
+ | [users-50](samples/users-50.pretty.jot) | 1327 | 837 | 37% |
34
+ | [products](samples/products.pretty.jot) | 772 | 613 | 21% |
35
+ | [large](samples/large.pretty.jot) | 240 | 221 | 8% |
36
+ | [small](samples/small.pretty.jot) | 37 | 36 | 3% |
37
+ | [irregular](samples/irregular.pretty.jot) | 49 | 49 | 0% |
38
+
39
+ [Full report →](TOKEN_SAVINGS.md)
40
+ <!-- END TOKEN SAVINGS -->
41
+
42
+ ## Installation
43
+
44
+ ### npm / pnpm / yarn
45
+
46
+ ```sh
47
+ npm i --save @creationix/jot
48
+ ```
49
+
50
+ ### CommonJS
51
+
52
+ ```js
53
+ const { parse, stringify } = require("@creationix/jot");
54
+ ```
55
+
56
+ ### ES Modules
57
+
58
+ ```js
59
+ import { parse, stringify } from "@creationix/jot";
60
+ ```
61
+
62
+ ### Browser
63
+
64
+ Copy [dist/jot.js](dist/jot.js) to your project and import as a native ES module:
65
+
66
+ ```html
67
+ <script type="module">
68
+ import { parse, stringify } from "./jot.js";
69
+ </script>
70
+ ```
71
+
72
+ ### TypeScript
73
+
74
+ TypeScript definitions are included. You can also import [src/jot.ts](src/jot.ts) directly into your project.
75
+
76
+ ## Usage
77
+
78
+ ```js
79
+ import { parse, stringify } from "@creationix/jot";
80
+
81
+ // Parse Jot to JavaScript
82
+ const data = parse("{name:Alice,scores:[98,87,92]}");
83
+ // { name: "Alice", scores: [98, 87, 92] }
84
+
85
+ // Stringify JavaScript to Jot
86
+ const jot = stringify({ name: "Bob", active: true });
87
+ // {name:Bob,active:true}
88
+
89
+ // Pretty print with options
90
+ const pretty = stringify(data, { pretty: true, indent: " " });
91
+ ```
92
+
93
+ ## Syntax
94
+
95
+ Jot is JSON with three optimizations:
96
+
97
+ 1. **Unquoted strings** — Strings are only quoted if necessary
98
+ 2. **Key folding** — Single-key nested objects collapse: `{a:{b:1}}` → `{a.b:1}`
99
+ 3. **Tables** — Arrays of objects with the same schema use `{{:cols;row;row}}` syntax
100
+
101
+ Here's a complete example showing all three:
4
102
 
5
103
  ```jot
6
104
  {
@@ -19,21 +117,14 @@ Jot is a compact, LLM friendly JSON variant designed to use fewer tokens while r
19
117
  }
20
118
  ```
21
119
 
22
- It is JSON with three optimizations:
120
+ ### Unquoted Strings
23
121
 
24
- 1. **Unquoted strings** — Strings are only quoted if necessary.
25
- 2. **Key folding** — Single-key nested objects collapse: `{a:{b:1}}` → `{a.b:1}`
26
- if normal keys contain dots, keep quotes: `{"a.b":1}`
27
- 3. **Tables** — Object arrays with repeating schemas use `{{:cols;row;row}}` syntax
28
-
29
- ## Unquoted Strings
30
-
31
- The only times that you need to quote a string are:
122
+ Quote a string only when:
32
123
 
33
- - It is a valid JSON value (`true`, `false`, `null`, or a number like `42`, `3.14`, `-0.5`, or `1e10`)
34
- - It contains special characters: `: ; , { } [ ] "` or control characters (newline, tab, etc)
35
- - It is empty or has leading or trailing whitespace
36
- - It being used as a key in an object and contains `.` (to distinguish from folded keys)
124
+ - It's a reserved value (`true`, `false`, `null`) or a number (`42`, `3.14`, `-0.5`, `1e10`)
125
+ - It contains special characters: `: ; , { } [ ] "` or control characters
126
+ - It's empty or has leading/trailing whitespace
127
+ - It's a key containing `.` (to distinguish from folded keys)
37
128
 
38
129
  ```json
39
130
  {"name":"Alice","city":"New York","count":"42"}
@@ -43,9 +134,9 @@ The only times that you need to quote a string are:
43
134
  {name:Alice,city:New York,count:"42"}
44
135
  ```
45
136
 
46
- ## Key Folding
137
+ ### Key Folding
47
138
 
48
- When a nested object has exactly ONE key, fold it:
139
+ When a nested object has exactly one key, fold it:
49
140
 
50
141
  ```json
51
142
  {"server":{"host":"localhost"}}
@@ -55,7 +146,7 @@ When a nested object has exactly ONE key, fold it:
55
146
  {server.host:localhost}
56
147
  ```
57
148
 
58
- If normal keys contain dots, keep quotes to avoid confusion:
149
+ If a key itself contains dots, quote it to avoid confusion:
59
150
 
60
151
  ```json
61
152
  {"data.point":{"x":10,"y":20}}
@@ -65,13 +156,9 @@ If normal keys contain dots, keep quotes to avoid confusion:
65
156
  {"data.point":{x:10,y:20}}
66
157
  ```
67
158
 
68
- ## Tables
159
+ ### Tables
69
160
 
70
- One common shape in data is a table an array of multiple objects with the same schema.
71
-
72
- Object arrays use `{{:schema;row;row;...}}` when schemas repeat. Start with `:` followed by column names:
73
-
74
- Don't use tables when there's no schema reuse (each object unique) — regular arrays are more compact.
161
+ Arrays of objects with repeating schemas become tables. Start with `:` followed by column names:
75
162
 
76
163
  ```json
77
164
  [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
@@ -90,3 +177,5 @@ To change schema mid-table, add another `:schema;` row:
90
177
  ```jot
91
178
  {{:id,name;1,Alice;2,Bob;:x,y;10,20;30,40}}
92
179
  ```
180
+
181
+ Don't use tables when there's no schema reuse — regular arrays are more compact.
package/dist/jot.cjs ADDED
@@ -0,0 +1,446 @@
1
+ "use strict"
2
+ Object.defineProperty(exports, "__esModule", { value: true })
3
+ exports.stringify = stringify
4
+ exports.parse = parse
5
+ const RESERVED = new Set(["true", "false", "null"])
6
+ const UNSAFE = [":", ",", "{", "}", "[", "]", '"', ";", "\\"]
7
+ const WS_RE = /\s/
8
+ const KEY_TERM_RE = /[:\,{}\[\];]|\s/
9
+ function needsQuotes(s, extra = []) {
10
+ const chars = [...UNSAFE, ...extra]
11
+ return (
12
+ s === "" ||
13
+ s.trim() !== s ||
14
+ RESERVED.has(s) ||
15
+ !Number.isNaN(Number(s)) ||
16
+ chars.some((c) => s.includes(c)) ||
17
+ [...s].some((c) => c.charCodeAt(0) < 32)
18
+ )
19
+ }
20
+ const quote = (s) => (needsQuotes(s) ? JSON.stringify(s) : s)
21
+ const quoteKey = (s) => (needsQuotes(s, ["."]) ? JSON.stringify(s) : s)
22
+ function getFoldPath(value) {
23
+ const path = []
24
+ let current = value
25
+ while (current !== null && typeof current === "object" && !Array.isArray(current)) {
26
+ const keys = Object.keys(current)
27
+ if (keys.length !== 1 || keys[0].includes(".")) {
28
+ break
29
+ }
30
+ path.push(keys[0])
31
+ current = current[keys[0]]
32
+ }
33
+ return path.length > 0 ? { path, leaf: current } : null
34
+ }
35
+ function groupBySchema(arr) {
36
+ const groups = []
37
+ for (const obj of arr) {
38
+ const keys = Object.keys(obj)
39
+ const last = groups.at(-1)
40
+ if (last && last.keys.join(",") === keys.join(",")) {
41
+ last.objects.push(obj)
42
+ } else {
43
+ groups.push({ keys, objects: [obj] })
44
+ }
45
+ }
46
+ return groups
47
+ }
48
+ let opts = {}
49
+ let depth = 0
50
+ const ind = () => (opts.pretty ? (opts.indent ?? " ").repeat(depth) : "")
51
+ function stringifyValue(value, atLineStart = false) {
52
+ if (value === null) {
53
+ return "null"
54
+ }
55
+ if (typeof value === "boolean") {
56
+ return String(value)
57
+ }
58
+ if (typeof value === "number") {
59
+ return String(value)
60
+ }
61
+ if (typeof value === "string") {
62
+ return quote(value)
63
+ }
64
+ if (Array.isArray(value)) {
65
+ return stringifyArray(value)
66
+ }
67
+ if (typeof value === "object") {
68
+ return stringifyObject(value, atLineStart)
69
+ }
70
+ return String(value)
71
+ }
72
+ function stringifyArray(arr) {
73
+ const isTable = arr.length >= 2 && arr.every((i) => i !== null && typeof i === "object" && !Array.isArray(i))
74
+ if (isTable) {
75
+ const groups = groupBySchema(arr)
76
+ if (groups.some((g) => g.objects.length >= 2)) {
77
+ return stringifyTable(groups)
78
+ }
79
+ }
80
+ if (arr.length === 1) {
81
+ return `[${stringifyValue(arr[0])}]`
82
+ }
83
+ const hasComplex = arr.some((i) => i !== null && typeof i === "object")
84
+ if (opts.pretty && arr.length > 0 && hasComplex) {
85
+ depth++
86
+ const items = arr.map((i) => `${ind()}${stringifyValue(i, true)}`)
87
+ depth--
88
+ return `[\n${items.join(",\n")}\n${ind()}]`
89
+ }
90
+ const sep = opts.pretty ? ", " : ","
91
+ const items = arr.map((v) => stringifyValue(v)).join(sep)
92
+ return opts.pretty ? `[ ${items} ]` : `[${items}]`
93
+ }
94
+ function stringifyTable(groups) {
95
+ const sep = opts.pretty ? ", " : ","
96
+ if (opts.pretty) {
97
+ depth++
98
+ const schemaInd = ind()
99
+ depth++
100
+ const dataInd = ind()
101
+ const rows = []
102
+ for (const { keys, objects } of groups) {
103
+ rows.push(`${schemaInd}:${keys.map((k) => quoteKey(k)).join(sep)}`)
104
+ for (const obj of objects) rows.push(`${dataInd}${keys.map((k) => stringifyValue(obj[k])).join(sep)}`)
105
+ }
106
+ depth -= 2
107
+ return `{{\n${rows.join("\n")}\n${ind()}}}`
108
+ }
109
+ const parts = []
110
+ for (const { keys, objects } of groups) {
111
+ parts.push(`:${keys.map((k) => quoteKey(k)).join(sep)}`)
112
+ for (const obj of objects) {
113
+ parts.push(keys.map((k) => stringifyValue(obj[k])).join(sep))
114
+ }
115
+ }
116
+ return `{{${parts.join(";")}}}`
117
+ }
118
+ function stringifyObject(obj, atLineStart = false) {
119
+ const keys = Object.keys(obj)
120
+ const pair = (k, pretty) => {
121
+ const val = obj[k]
122
+ if (!needsQuotes(k, ["."]) && val !== null && typeof val === "object" && !Array.isArray(val)) {
123
+ const fold = getFoldPath(val)
124
+ if (fold) {
125
+ const foldedKey = `${k}.${fold.path.join(".")}`
126
+ return pretty ? `${foldedKey}: ${stringifyValue(fold.leaf)}` : `${foldedKey}:${stringifyValue(fold.leaf)}`
127
+ }
128
+ }
129
+ const qk = quoteKey(k)
130
+ return pretty ? `${qk}: ${stringifyValue(val)}` : `${qk}:${stringifyValue(val)}`
131
+ }
132
+ if (opts.pretty && keys.length > 1) {
133
+ depth++
134
+ const rawPairs = keys.map((k) => pair(k, true))
135
+ const lastMulti = rawPairs.at(-1)?.endsWith("}") || rawPairs.at(-1)?.endsWith("]")
136
+ const compact = atLineStart && !lastMulti
137
+ const pairs = rawPairs.map((p, i) => (i === 0 && compact ? p : `${ind()}${p}`))
138
+ depth--
139
+ return compact ? `{ ${pairs.join(",\n")} }` : `{\n${pairs.join(",\n")}\n${ind()}}`
140
+ }
141
+ if (opts.pretty && keys.length === 1) {
142
+ return `{ ${pair(keys[0], true)} }`
143
+ }
144
+ return `{${keys.map((k) => pair(k, false)).join(",")}}`
145
+ }
146
+ function stringify(data, options = {}) {
147
+ opts = { pretty: false, indent: " ", ...options }
148
+ depth = 0
149
+ return stringifyValue(data)
150
+ }
151
+ // Parser
152
+ class JotParser {
153
+ input
154
+ pos = 0
155
+ constructor(input) {
156
+ this.input = input
157
+ }
158
+ parse() {
159
+ this.ws()
160
+ const result = this.value("")
161
+ this.ws()
162
+ if (this.pos < this.input.length) {
163
+ throw new Error(`Unexpected character at position ${this.pos}: '${this.input[this.pos]}'`)
164
+ }
165
+ return result
166
+ }
167
+ ws() {
168
+ while (this.pos < this.input.length && WS_RE.test(this.input[this.pos])) this.pos++
169
+ }
170
+ peek = () => this.input[this.pos] || ""
171
+ value(terminators = "") {
172
+ this.ws()
173
+ const ch = this.peek()
174
+ if (ch === "{") {
175
+ return this.input[this.pos + 1] === "{" ? this.table() : this.object()
176
+ }
177
+ if (ch === "[") {
178
+ return this.array()
179
+ }
180
+ if (ch === '"') {
181
+ return this.quoted()
182
+ }
183
+ return this.atom(terminators)
184
+ }
185
+ quoted() {
186
+ this.pos++
187
+ let result = ""
188
+ while (this.pos < this.input.length) {
189
+ const ch = this.input[this.pos]
190
+ if (ch === '"') {
191
+ this.pos++
192
+ return result
193
+ }
194
+ if (ch === "\\") {
195
+ this.pos++
196
+ const esc = this.input[this.pos]
197
+ const escMap = {
198
+ '"': '"',
199
+ "\\": "\\",
200
+ "/": "/",
201
+ b: "\b",
202
+ f: "\f",
203
+ n: "\n",
204
+ r: "\r",
205
+ t: "\t",
206
+ }
207
+ if (esc in escMap) {
208
+ result += escMap[esc]
209
+ } else if (esc === "u") {
210
+ result += String.fromCharCode(Number.parseInt(this.input.slice(this.pos + 1, this.pos + 5), 16))
211
+ this.pos += 4
212
+ } else {
213
+ throw new Error(`Invalid escape sequence '\\${esc}'`)
214
+ }
215
+ } else {
216
+ result += ch
217
+ }
218
+ this.pos++
219
+ }
220
+ throw new Error("Unterminated string")
221
+ }
222
+ parseToken(terminators) {
223
+ const start = this.pos
224
+ if (terminators === "") {
225
+ const token = this.input.slice(start).trim()
226
+ this.pos = this.input.length
227
+ if (token === "") {
228
+ throw new Error(`Unexpected end of input at position ${start}`)
229
+ }
230
+ return token
231
+ }
232
+ while (this.pos < this.input.length && !terminators.includes(this.input[this.pos])) {
233
+ this.pos++
234
+ }
235
+ const token = this.input.slice(start, this.pos).trim()
236
+ if (token === "") {
237
+ throw new Error(`Unexpected character at position ${this.pos}: '${this.peek()}'`)
238
+ }
239
+ return token
240
+ }
241
+ tokenToValue(token) {
242
+ if (token === "null") {
243
+ return null
244
+ }
245
+ if (token === "true") {
246
+ return true
247
+ }
248
+ if (token === "false") {
249
+ return false
250
+ }
251
+ const num = Number(token)
252
+ if (!Number.isNaN(num) && token !== "") {
253
+ return num
254
+ }
255
+ return token
256
+ }
257
+ atom(terminators) {
258
+ return this.tokenToValue(this.parseToken(terminators))
259
+ }
260
+ array() {
261
+ this.pos++
262
+ const result = []
263
+ this.ws()
264
+ while (this.peek() !== "]") {
265
+ if (this.pos >= this.input.length) {
266
+ throw new Error("Unterminated array")
267
+ }
268
+ result.push(this.value(",]"))
269
+ this.ws()
270
+ if (this.peek() === ",") {
271
+ this.pos++
272
+ this.ws()
273
+ }
274
+ }
275
+ this.pos++
276
+ return result
277
+ }
278
+ table() {
279
+ this.pos += 2
280
+ const result = []
281
+ let schema = []
282
+ this.ws()
283
+ while (this.input.slice(this.pos, this.pos + 2) !== "}}") {
284
+ if (this.pos >= this.input.length) {
285
+ throw new Error("Unterminated table")
286
+ }
287
+ this.ws()
288
+ if (this.peek() === ":") {
289
+ this.pos++
290
+ schema = this.schemaRow()
291
+ } else {
292
+ if (schema.length === 0) {
293
+ throw new Error(`Data row without schema at position ${this.pos}`)
294
+ }
295
+ const values = this.dataRow(schema.length)
296
+ const obj = {}
297
+ for (let i = 0; i < schema.length; i++) {
298
+ obj[schema[i]] = values[i]
299
+ }
300
+ result.push(obj)
301
+ }
302
+ this.ws()
303
+ if (this.peek() === ";") {
304
+ this.pos++
305
+ this.ws()
306
+ }
307
+ }
308
+ this.pos += 2
309
+ return result
310
+ }
311
+ schemaRow() {
312
+ const cols = []
313
+ let col = ""
314
+ while (this.pos < this.input.length) {
315
+ const ch = this.input[this.pos]
316
+ if ((ch === "}" && this.input[this.pos + 1] === "}") || ch === ";" || ch === "\n") {
317
+ if (col.trim()) {
318
+ cols.push(col.trim())
319
+ }
320
+ break
321
+ }
322
+ if (ch === ",") {
323
+ if (col.trim()) {
324
+ cols.push(col.trim())
325
+ }
326
+ col = ""
327
+ this.pos++
328
+ continue
329
+ }
330
+ col += ch
331
+ this.pos++
332
+ }
333
+ return cols
334
+ }
335
+ dataRow(colCount) {
336
+ const values = []
337
+ for (let i = 0; i < colCount; i++) {
338
+ this.ws()
339
+ values.push(this.tableValue(i < colCount - 1 ? ",;}\n" : ";}\n"))
340
+ this.ws()
341
+ if (this.peek() === ",") {
342
+ this.pos++
343
+ }
344
+ }
345
+ return values
346
+ }
347
+ tableValue(terminators) {
348
+ this.ws()
349
+ const ch = this.peek()
350
+ if (ch === '"') {
351
+ return this.quoted()
352
+ }
353
+ if (ch === "{") {
354
+ return this.input[this.pos + 1] === "{" ? this.table() : this.object()
355
+ }
356
+ if (ch === "[") {
357
+ return this.array()
358
+ }
359
+ const start = this.pos
360
+ while (this.pos < this.input.length) {
361
+ const c = this.input[this.pos]
362
+ if ((c === "}" && this.input[this.pos + 1] === "}") || terminators.includes(c)) {
363
+ break
364
+ }
365
+ this.pos++
366
+ }
367
+ const token = this.input.slice(start, this.pos).trim()
368
+ return token === "" ? null : this.tokenToValue(token)
369
+ }
370
+ object() {
371
+ this.pos++
372
+ const result = {}
373
+ this.ws()
374
+ while (this.peek() !== "}") {
375
+ if (this.pos >= this.input.length) {
376
+ throw new Error("Unterminated object")
377
+ }
378
+ const { key, quoted } = this.parseKey()
379
+ this.ws()
380
+ if (this.peek() !== ":") {
381
+ throw new Error(`Expected ':' after key '${key}' at position ${this.pos}`)
382
+ }
383
+ this.pos++
384
+ const value = this.value(",}")
385
+ if (quoted) {
386
+ result[key] = value
387
+ } else {
388
+ this.merge(result, this.unfold(key, value))
389
+ }
390
+ this.ws()
391
+ if (this.peek() === ",") {
392
+ this.pos++
393
+ this.ws()
394
+ }
395
+ }
396
+ this.pos++
397
+ return result
398
+ }
399
+ parseKey() {
400
+ this.ws()
401
+ if (this.peek() === '"') {
402
+ return { key: this.quoted(), quoted: true }
403
+ }
404
+ const start = this.pos
405
+ while (this.pos < this.input.length && !KEY_TERM_RE.test(this.input[this.pos])) this.pos++
406
+ const key = this.input.slice(start, this.pos)
407
+ if (key === "") {
408
+ throw new Error(`Expected key at position ${this.pos}`)
409
+ }
410
+ return { key, quoted: false }
411
+ }
412
+ unfold(keyPath, value) {
413
+ const parts = keyPath.split(".")
414
+ const result = {}
415
+ let current = result
416
+ for (let i = 0; i < parts.length - 1; i++) {
417
+ const nested = {}
418
+ current[parts[i]] = nested
419
+ current = nested
420
+ }
421
+ current[parts.at(-1)] = value
422
+ return result
423
+ }
424
+ merge(target, src) {
425
+ for (const key of Object.keys(src)) {
426
+ const tv = target[key]
427
+ const sv = src[key]
428
+ if (
429
+ key in target &&
430
+ typeof tv === "object" &&
431
+ tv !== null &&
432
+ !Array.isArray(tv) &&
433
+ typeof sv === "object" &&
434
+ sv !== null &&
435
+ !Array.isArray(sv)
436
+ ) {
437
+ this.merge(tv, sv)
438
+ } else {
439
+ target[key] = sv
440
+ }
441
+ }
442
+ }
443
+ }
444
+ function parse(input) {
445
+ return new JotParser(input).parse()
446
+ }
package/dist/jot.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export interface StringifyOptions {
2
+ pretty?: boolean
3
+ indent?: string
4
+ }
5
+ export declare function stringify(data: unknown, options?: StringifyOptions): string
6
+ export declare function parse(input: string): unknown