@creationix/jot 0.0.1

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 (79) hide show
  1. package/README.md +92 -0
  2. package/SUMMARY.md +151 -0
  3. package/TOKEN_COUNTS.md +97 -0
  4. package/bun.lock +19 -0
  5. package/jot.test.ts +133 -0
  6. package/jot.ts +650 -0
  7. package/package.json +10 -0
  8. package/samples/chat.jot +1 -0
  9. package/samples/chat.json +1 -0
  10. package/samples/chat.pretty.jot +6 -0
  11. package/samples/chat.pretty.json +16 -0
  12. package/samples/firewall.jot +1 -0
  13. package/samples/firewall.json +1 -0
  14. package/samples/firewall.pretty.jot +235 -0
  15. package/samples/firewall.pretty.json +344 -0
  16. package/samples/github-issue.jot +1 -0
  17. package/samples/github-issue.json +1 -0
  18. package/samples/github-issue.pretty.jot +15 -0
  19. package/samples/github-issue.pretty.json +20 -0
  20. package/samples/hikes.jot +1 -0
  21. package/samples/hikes.json +1 -0
  22. package/samples/hikes.pretty.jot +14 -0
  23. package/samples/hikes.pretty.json +38 -0
  24. package/samples/irregular.jot +1 -0
  25. package/samples/irregular.json +1 -0
  26. package/samples/irregular.pretty.jot +13 -0
  27. package/samples/irregular.pretty.json +23 -0
  28. package/samples/json-counts-cache.jot +1 -0
  29. package/samples/json-counts-cache.json +1 -0
  30. package/samples/json-counts-cache.pretty.jot +26 -0
  31. package/samples/json-counts-cache.pretty.json +26 -0
  32. package/samples/key-folding-basic.jot +1 -0
  33. package/samples/key-folding-basic.json +1 -0
  34. package/samples/key-folding-basic.pretty.jot +7 -0
  35. package/samples/key-folding-basic.pretty.json +25 -0
  36. package/samples/key-folding-mixed.jot +1 -0
  37. package/samples/key-folding-mixed.json +1 -0
  38. package/samples/key-folding-mixed.pretty.jot +16 -0
  39. package/samples/key-folding-mixed.pretty.json +24 -0
  40. package/samples/key-folding-with-array.jot +1 -0
  41. package/samples/key-folding-with-array.json +1 -0
  42. package/samples/key-folding-with-array.pretty.jot +6 -0
  43. package/samples/key-folding-with-array.pretty.json +29 -0
  44. package/samples/large.jot +1 -0
  45. package/samples/large.json +1 -0
  46. package/samples/large.pretty.jot +72 -0
  47. package/samples/large.pretty.json +93 -0
  48. package/samples/logs.jot +1 -0
  49. package/samples/logs.json +1 -0
  50. package/samples/logs.pretty.jot +96 -0
  51. package/samples/logs.pretty.json +350 -0
  52. package/samples/medium.jot +1 -0
  53. package/samples/medium.json +1 -0
  54. package/samples/medium.pretty.jot +13 -0
  55. package/samples/medium.pretty.json +30 -0
  56. package/samples/metrics.jot +1 -0
  57. package/samples/metrics.json +1 -0
  58. package/samples/metrics.pretty.jot +11 -0
  59. package/samples/metrics.pretty.json +25 -0
  60. package/samples/package.jot +1 -0
  61. package/samples/package.json +1 -0
  62. package/samples/package.pretty.jot +18 -0
  63. package/samples/package.pretty.json +18 -0
  64. package/samples/products.jot +1 -0
  65. package/samples/products.json +1 -0
  66. package/samples/products.pretty.jot +69 -0
  67. package/samples/products.pretty.json +235 -0
  68. package/samples/routes.jot +1 -0
  69. package/samples/routes.json +1 -0
  70. package/samples/routes.pretty.jot +142 -0
  71. package/samples/routes.pretty.json +354 -0
  72. package/samples/small.jot +1 -0
  73. package/samples/small.json +1 -0
  74. package/samples/small.pretty.jot +8 -0
  75. package/samples/small.pretty.json +12 -0
  76. package/samples/users-50.jot +1 -0
  77. package/samples/users-50.json +1 -0
  78. package/samples/users-50.pretty.jot +53 -0
  79. package/samples/users-50.pretty.json +354 -0
package/jot.ts ADDED
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Jot - JSON Optimized for Tokens
3
+ *
4
+ * Features:
5
+ * 1. Unquoted strings - only quote when necessary
6
+ * 2. Key folding - {a:{b:1}} => {a.b:1}
7
+ * 3. Tables - [{a:1},{a:2}] => {{:a;1;2}} with schema changes via :newschema;
8
+ *
9
+ * Pretty-print rules (compact = braces on content lines):
10
+ *
11
+ * Objects:
12
+ * - Single key: always compact `{ key: value }`
13
+ * - Multi-key, array item: compact `{ a: 1,\n b: 2 }` unless last value is multi-line
14
+ * - Multi-key, array item with multi-line last value: expanded
15
+ * - Multi-key, other: expanded `{\n a: 1,\n b: 2\n}`
16
+ *
17
+ * Arrays:
18
+ * - Empty: `[]`
19
+ * - Single item: compact `[item]`
20
+ * - 2+ simple items: spaced `[ a, b ]`
21
+ * - 2+ complex items: expanded `[\n ...,\n ...\n]`
22
+ *
23
+ * Tables:
24
+ * - Only when 2+ consecutive objects share schema
25
+ * - Otherwise use regular array syntax
26
+ */
27
+
28
+ const RESERVED_WORDS = new Set(["true", "false", "null"])
29
+ const UNSAFE_CHARS = [':', ',', '{', '}', '[', ']', '"', ';', '\\']
30
+
31
+ function needsQuotes(str: string, unsafeChars: string[]): boolean {
32
+ if (str === "" || str.trim() !== str) return true
33
+ if (RESERVED_WORDS.has(str)) return true
34
+ if (!isNaN(Number(str))) return true
35
+ if (unsafeChars.some(c => str.includes(c))) return true
36
+ if ([...str].some(c => c.charCodeAt(0) < 32)) return true
37
+ return false
38
+ }
39
+
40
+ const quoteString = (s: string) => needsQuotes(s, UNSAFE_CHARS) ? JSON.stringify(s) : s
41
+ const quoteKey = (s: string) => needsQuotes(s, [...UNSAFE_CHARS, '.']) ? JSON.stringify(s) : s
42
+ const needsKeyQuoting = (s: string) => needsQuotes(s, [...UNSAFE_CHARS, '.'])
43
+
44
+ function getObjectKeys(obj: object): string[] {
45
+ return Object.keys(obj)
46
+ }
47
+
48
+ // Check if value is a foldable chain: single-key objects nested
49
+ // Skip folding if any key contains "." (would be ambiguous with fold syntax)
50
+ function getFoldPath(value: unknown): { path: string[], leaf: unknown } | null {
51
+ const path: string[] = []
52
+ let current = value
53
+
54
+ while (
55
+ current !== null &&
56
+ typeof current === "object" &&
57
+ !Array.isArray(current)
58
+ ) {
59
+ const keys = getObjectKeys(current)
60
+ if (keys.length !== 1) break
61
+ const key = keys[0]
62
+ // Don't fold keys containing "." - they need quoting
63
+ if (key.includes(".")) break
64
+ path.push(key)
65
+ current = (current as Record<string, unknown>)[key]
66
+ }
67
+
68
+ if (path.length < 1) return null
69
+ return { path, leaf: current }
70
+ }
71
+
72
+ // Check if array is all objects (candidate for table)
73
+ function isAllObjects(arr: unknown[]): boolean {
74
+ return arr.length >= 2 && arr.every(item =>
75
+ item !== null && typeof item === "object" && !Array.isArray(item)
76
+ )
77
+ }
78
+
79
+ // Check if table format provides benefit (at least one schema reused)
80
+ function hasSchemaReuse(arr: Record<string, unknown>[]): boolean {
81
+ const groups = groupBySchema(arr)
82
+ return groups.some(g => g.objects.length >= 2)
83
+ }
84
+
85
+ // Options for stringify
86
+ export interface StringifyOptions {
87
+ pretty?: boolean
88
+ indent?: string
89
+ }
90
+
91
+ let currentOptions: StringifyOptions = { pretty: false, indent: " " }
92
+ let depth = 0
93
+
94
+ function ind(): string {
95
+ return currentOptions.pretty ? currentOptions.indent!.repeat(depth) : ""
96
+ }
97
+
98
+ // atLineStart: true when value will be at start of a line (array items), false when after key:
99
+ function stringifyValue(value: unknown, atLineStart = false): string {
100
+ if (value === null) return "null"
101
+ if (value === true) return "true"
102
+ if (value === false) return "false"
103
+ if (typeof value === "number") return String(value)
104
+ if (typeof value === "string") return quoteString(value)
105
+
106
+ if (Array.isArray(value)) {
107
+ return stringifyArray(value)
108
+ }
109
+
110
+ if (typeof value === "object") {
111
+ return stringifyObject(value as Record<string, unknown>, atLineStart)
112
+ }
113
+
114
+ return String(value)
115
+ }
116
+
117
+ function hasComplexItems(arr: unknown[]): boolean {
118
+ return arr.some(item => item !== null && typeof item === "object")
119
+ }
120
+
121
+ // Group consecutive objects by their schema (sorted key list)
122
+ function groupBySchema(arr: Record<string, unknown>[]): { keys: string[], objects: Record<string, unknown>[] }[] {
123
+ const groups: { keys: string[], objects: Record<string, unknown>[] }[] = []
124
+
125
+ for (const obj of arr) {
126
+ const keys = getObjectKeys(obj)
127
+ const keyStr = keys.join(",")
128
+
129
+ // Check if matches current group
130
+ if (groups.length > 0) {
131
+ const lastGroup = groups[groups.length - 1]
132
+ if (lastGroup.keys.join(",") === keyStr) {
133
+ lastGroup.objects.push(obj)
134
+ continue
135
+ }
136
+ }
137
+
138
+ // Start new group
139
+ groups.push({ keys, objects: [obj] })
140
+ }
141
+
142
+ return groups
143
+ }
144
+
145
+ function stringifyArray(arr: unknown[]): string {
146
+ // Check for object array with schema reuse - use table format
147
+ if (isAllObjects(arr) && hasSchemaReuse(arr as Record<string, unknown>[])) {
148
+ return stringifyTable(arr as Record<string, unknown>[])
149
+ }
150
+
151
+ // Single-item arrays: compact
152
+ if (arr.length === 1) {
153
+ return `[${stringifyValue(arr[0])}]`
154
+ }
155
+
156
+ // Regular array formatting
157
+ if (currentOptions.pretty && arr.length > 0 && hasComplexItems(arr)) {
158
+ depth++
159
+ const items = arr.map(item => `${ind()}${stringifyValue(item, true)}`)
160
+ depth--
161
+ // Expanded format for 2+ items: ] on own line
162
+ return `[\n${items.join(",\n")}\n${ind()}]`
163
+ }
164
+
165
+ const sep = currentOptions.pretty ? ", " : ","
166
+ const items = arr.map(stringifyValue).join(sep)
167
+ return currentOptions.pretty ? `[ ${items} ]` : `[${items}]`
168
+ }
169
+
170
+ function stringifyTable(arr: Record<string, unknown>[]): string {
171
+ const groups = groupBySchema(arr)
172
+ const sep = currentOptions.pretty ? ", " : ","
173
+
174
+ if (currentOptions.pretty) {
175
+ depth++
176
+ const schemaInd = ind() // 1 level for schema rows
177
+ depth++
178
+ const dataInd = ind() // 2 levels for data rows
179
+ const rows: string[] = []
180
+
181
+ for (const group of groups) {
182
+ // Schema row
183
+ rows.push(schemaInd + `:${group.keys.map(k => quoteKey(k)).join(sep)}`)
184
+ // Data rows - stringify with depth at 2 levels
185
+ for (const obj of group.objects) {
186
+ rows.push(dataInd + group.keys.map(k => stringifyValue(obj[k])).join(sep))
187
+ }
188
+ }
189
+
190
+ depth -= 2
191
+ return `{{\n${rows.join("\n")}\n${ind()}}}`
192
+ }
193
+
194
+ // Non-pretty mode
195
+ const parts: string[] = []
196
+ for (const group of groups) {
197
+ parts.push(`:${group.keys.map(k => quoteKey(k)).join(sep)}`)
198
+ for (const obj of group.objects) {
199
+ parts.push(group.keys.map(k => stringifyValue(obj[k])).join(sep))
200
+ }
201
+ }
202
+ return `{{${parts.join(";")}}}`
203
+ }
204
+
205
+ function stringifyObject(obj: Record<string, unknown>, atLineStart = false): string {
206
+ const keys = getObjectKeys(obj)
207
+
208
+ const stringifyPair = (k: string, forPretty: boolean): string => {
209
+ const val = obj[k]
210
+ const quotedKey = quoteKey(k)
211
+ // Try to fold (only if key doesn't need quoting - has no special chars)
212
+ if (!needsKeyQuoting(k) && val !== null && typeof val === "object" && !Array.isArray(val)) {
213
+ const fold = getFoldPath(val)
214
+ if (fold) {
215
+ const foldedKey = `${k}.${fold.path.join(".")}`
216
+ if (forPretty) {
217
+ // Value after key: is not at line start
218
+ return `${foldedKey}: ${stringifyValue(fold.leaf, false)}`
219
+ }
220
+ return `${foldedKey}:${stringifyValue(fold.leaf)}`
221
+ }
222
+ }
223
+ if (forPretty) {
224
+ // Value after key: is not at line start
225
+ return `${quotedKey}: ${stringifyValue(val, false)}`
226
+ }
227
+ return `${quotedKey}:${stringifyValue(val)}`
228
+ }
229
+
230
+ if (currentOptions.pretty && keys.length > 1) {
231
+ depth++
232
+ // First, stringify all pairs to check if last is multi-line
233
+ const rawPairs = keys.map(k => stringifyPair(k, true))
234
+ const lastIsMultiLine = rawPairs[rawPairs.length - 1].endsWith('}') ||
235
+ rawPairs[rawPairs.length - 1].endsWith(']')
236
+
237
+ // Array items with simple last value: compact format
238
+ const useCompact = atLineStart && !lastIsMultiLine
239
+
240
+ const pairs: string[] = []
241
+ for (let i = 0; i < keys.length; i++) {
242
+ if (i === 0 && useCompact) {
243
+ // First pair on same line as { - no indent
244
+ pairs.push(rawPairs[i])
245
+ } else {
246
+ pairs.push(`${ind()}${rawPairs[i]}`)
247
+ }
248
+ }
249
+ depth--
250
+
251
+ if (useCompact) {
252
+ return `{ ${pairs.join(",\n")} }`
253
+ }
254
+ // Expanded format: newlines for open and close
255
+ return `{\n${pairs.join(",\n")}\n${ind()}}`
256
+ }
257
+ if (currentOptions.pretty && keys.length === 1) {
258
+ return `{ ${stringifyPair(keys[0], true)} }`
259
+ }
260
+ const pairs = keys.map(k => stringifyPair(k, false))
261
+ return `{${pairs.join(",")}}`
262
+ }
263
+
264
+ export function stringify(data: unknown, options: StringifyOptions = {}): string {
265
+ currentOptions = { pretty: false, indent: " ", ...options }
266
+ depth = 0
267
+ return stringifyValue(data)
268
+ }
269
+
270
+ // ============ PARSER ============
271
+
272
+ class JotParser {
273
+ private pos = 0
274
+
275
+ constructor(private input: string) {}
276
+
277
+ parse(): unknown {
278
+ this.skipWhitespace()
279
+ const result = this.parseValue("")
280
+ this.skipWhitespace()
281
+ if (this.pos < this.input.length) {
282
+ throw new Error(`Unexpected character at position ${this.pos}: '${this.input[this.pos]}'`)
283
+ }
284
+ return result
285
+ }
286
+
287
+ private skipWhitespace(): void {
288
+ while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
289
+ this.pos++
290
+ }
291
+ }
292
+
293
+ private peek(): string {
294
+ return this.input[this.pos] || ""
295
+ }
296
+
297
+ private parseValue(terminators = ""): unknown {
298
+ this.skipWhitespace()
299
+ const ch = this.peek()
300
+
301
+ if (ch === "{") {
302
+ if (this.input[this.pos + 1] === "{") return this.parseTable()
303
+ return this.parseObject()
304
+ }
305
+ if (ch === "[") return this.parseArray()
306
+ if (ch === '"') return this.parseQuotedString()
307
+
308
+ return this.parseAtom(terminators)
309
+ }
310
+
311
+ private parseQuotedString(): string {
312
+ if (this.peek() !== '"') {
313
+ throw new Error(`Expected '"' at position ${this.pos}`)
314
+ }
315
+ this.pos++
316
+
317
+ let result = ""
318
+ while (this.pos < this.input.length) {
319
+ const ch = this.input[this.pos]
320
+ if (ch === '"') {
321
+ this.pos++
322
+ return result
323
+ }
324
+ if (ch === "\\") {
325
+ this.pos++
326
+ if (this.pos >= this.input.length) {
327
+ throw new Error("Unexpected end of input in string escape")
328
+ }
329
+ const escaped = this.input[this.pos]
330
+ switch (escaped) {
331
+ case '"': result += '"'; break
332
+ case "\\": result += "\\"; break
333
+ case "/": result += "/"; break
334
+ case "b": result += "\b"; break
335
+ case "f": result += "\f"; break
336
+ case "n": result += "\n"; break
337
+ case "r": result += "\r"; break
338
+ case "t": result += "\t"; break
339
+ case "u": {
340
+ if (this.pos + 4 >= this.input.length) {
341
+ throw new Error("Invalid unicode escape")
342
+ }
343
+ const hex = this.input.slice(this.pos + 1, this.pos + 5)
344
+ result += String.fromCharCode(parseInt(hex, 16))
345
+ this.pos += 4
346
+ break
347
+ }
348
+ default:
349
+ throw new Error(`Invalid escape sequence '\\${escaped}'`)
350
+ }
351
+ } else {
352
+ result += ch
353
+ }
354
+ this.pos++
355
+ }
356
+ throw new Error("Unterminated string")
357
+ }
358
+
359
+ private parseAtom(terminators: string): unknown {
360
+ const start = this.pos
361
+
362
+ if (terminators === "") {
363
+ const token = this.input.slice(start).trim()
364
+ this.pos = this.input.length
365
+
366
+ if (token === "") {
367
+ throw new Error(`Unexpected end of input at position ${start}`)
368
+ }
369
+
370
+ if (token === "null") return null
371
+ if (token === "true") return true
372
+ if (token === "false") return false
373
+
374
+ const num = Number(token)
375
+ if (!isNaN(num)) return num
376
+
377
+ return token
378
+ }
379
+
380
+ while (this.pos < this.input.length) {
381
+ const ch = this.input[this.pos]
382
+ if (terminators.includes(ch)) break
383
+ this.pos++
384
+ }
385
+
386
+ const token = this.input.slice(start, this.pos).trim()
387
+ if (token === "") {
388
+ throw new Error(`Unexpected character at position ${this.pos}: '${this.peek()}'`)
389
+ }
390
+
391
+ if (token === "null") return null
392
+ if (token === "true") return true
393
+ if (token === "false") return false
394
+
395
+ const num = Number(token)
396
+ if (!isNaN(num) && token !== "") return num
397
+
398
+ return token
399
+ }
400
+
401
+ private parseArray(): unknown[] {
402
+ if (this.peek() !== "[") {
403
+ throw new Error(`Expected '[' at position ${this.pos}`)
404
+ }
405
+ this.pos++
406
+
407
+ const result: unknown[] = []
408
+ this.skipWhitespace()
409
+
410
+ while (this.peek() !== "]") {
411
+ if (this.pos >= this.input.length) {
412
+ throw new Error("Unterminated array")
413
+ }
414
+ const item = this.parseValue(",]")
415
+ result.push(item)
416
+ this.skipWhitespace()
417
+ if (this.peek() === ",") {
418
+ this.pos++
419
+ this.skipWhitespace()
420
+ }
421
+ }
422
+
423
+ this.pos++
424
+ return result
425
+ }
426
+
427
+ // Parse table: {{:schema;row;row;:newschema;row}}
428
+ private parseTable(): unknown[] {
429
+ if (this.input.slice(this.pos, this.pos + 2) !== "{{") {
430
+ throw new Error(`Expected '{{' at position ${this.pos}`)
431
+ }
432
+ this.pos += 2
433
+
434
+ const result: Record<string, unknown>[] = []
435
+ let currentSchema: string[] = []
436
+ this.skipWhitespace()
437
+
438
+ while (this.input.slice(this.pos, this.pos + 2) !== "}}") {
439
+ if (this.pos >= this.input.length) {
440
+ throw new Error("Unterminated table")
441
+ }
442
+
443
+ this.skipWhitespace()
444
+
445
+ // Check for schema row (starts with :)
446
+ if (this.peek() === ":") {
447
+ this.pos++ // skip :
448
+ currentSchema = this.parseSchemaRow()
449
+ } else {
450
+ // Data row
451
+ if (currentSchema.length === 0) {
452
+ throw new Error(`Data row without schema at position ${this.pos}`)
453
+ }
454
+ const values = this.parseDataRow(currentSchema.length)
455
+ const obj: Record<string, unknown> = {}
456
+ for (let i = 0; i < currentSchema.length; i++) {
457
+ obj[currentSchema[i]] = values[i]
458
+ }
459
+ result.push(obj)
460
+ }
461
+
462
+ this.skipWhitespace()
463
+ if (this.peek() === ";") {
464
+ this.pos++
465
+ this.skipWhitespace()
466
+ }
467
+ }
468
+
469
+ this.pos += 2 // skip }}
470
+ return result
471
+ }
472
+
473
+ private parseSchemaRow(): string[] {
474
+ const cols: string[] = []
475
+ let col = ""
476
+
477
+ while (this.pos < this.input.length) {
478
+ const ch = this.input[this.pos]
479
+ if (ch === "}" && this.input[this.pos + 1] === "}") {
480
+ if (col.trim()) cols.push(col.trim())
481
+ break
482
+ }
483
+ if (ch === ";" || ch === "\n") {
484
+ if (col.trim()) cols.push(col.trim())
485
+ break
486
+ }
487
+ if (ch === ",") {
488
+ if (col.trim()) cols.push(col.trim())
489
+ col = ""
490
+ this.pos++
491
+ continue
492
+ }
493
+ col += ch
494
+ this.pos++
495
+ }
496
+
497
+ return cols
498
+ }
499
+
500
+ private parseDataRow(colCount: number): unknown[] {
501
+ const values: unknown[] = []
502
+
503
+ for (let i = 0; i < colCount; i++) {
504
+ this.skipWhitespace()
505
+ const terminators = i < colCount - 1 ? ",;}\n" : ";}\n"
506
+ const value = this.parseTableValue(terminators)
507
+ values.push(value)
508
+ this.skipWhitespace()
509
+ if (this.peek() === ",") {
510
+ this.pos++
511
+ }
512
+ }
513
+
514
+ return values
515
+ }
516
+
517
+ private parseTableValue(terminators: string): unknown {
518
+ this.skipWhitespace()
519
+ const ch = this.peek()
520
+
521
+ if (ch === '"') return this.parseQuotedString()
522
+ if (ch === "{") {
523
+ if (this.input[this.pos + 1] === "{") return this.parseTable()
524
+ return this.parseObject()
525
+ }
526
+ if (ch === "[") return this.parseArray()
527
+
528
+ const start = this.pos
529
+ while (this.pos < this.input.length) {
530
+ const c = this.input[this.pos]
531
+ if (c === "}" && this.input[this.pos + 1] === "}") break
532
+ if (terminators.includes(c)) break
533
+ this.pos++
534
+ }
535
+
536
+ const token = this.input.slice(start, this.pos).trim()
537
+ if (token === "") return null
538
+
539
+ if (token === "null") return null
540
+ if (token === "true") return true
541
+ if (token === "false") return false
542
+
543
+ const num = Number(token)
544
+ if (!isNaN(num)) return num
545
+
546
+ return token
547
+ }
548
+
549
+ private parseObject(): Record<string, unknown> {
550
+ if (this.peek() !== "{") {
551
+ throw new Error(`Expected '{' at position ${this.pos}`)
552
+ }
553
+ this.pos++
554
+
555
+ const result: Record<string, unknown> = {}
556
+ this.skipWhitespace()
557
+
558
+ while (this.peek() !== "}") {
559
+ if (this.pos >= this.input.length) {
560
+ throw new Error("Unterminated object")
561
+ }
562
+
563
+ const { key: keyPath, quoted } = this.parseKey()
564
+ this.skipWhitespace()
565
+
566
+ if (this.peek() !== ":") {
567
+ throw new Error(`Expected ':' after key '${keyPath}' at position ${this.pos}`)
568
+ }
569
+ this.pos++
570
+
571
+ const value = this.parseValue(",}")
572
+
573
+ if (quoted) {
574
+ result[keyPath] = value
575
+ } else {
576
+ const unfolded = this.unfoldKey(keyPath, value)
577
+ this.mergeObjects(result, unfolded)
578
+ }
579
+
580
+ this.skipWhitespace()
581
+ if (this.peek() === ",") {
582
+ this.pos++
583
+ this.skipWhitespace()
584
+ }
585
+ }
586
+
587
+ this.pos++
588
+ return result
589
+ }
590
+
591
+ private parseKey(): { key: string; quoted: boolean } {
592
+ this.skipWhitespace()
593
+
594
+ if (this.peek() === '"') {
595
+ return { key: this.parseQuotedString(), quoted: true }
596
+ }
597
+
598
+ const start = this.pos
599
+ while (this.pos < this.input.length) {
600
+ const ch = this.input[this.pos]
601
+ if (/[:\,{}\[\];]/.test(ch) || /\s/.test(ch)) break
602
+ this.pos++
603
+ }
604
+ const key = this.input.slice(start, this.pos)
605
+ if (key === "") {
606
+ throw new Error(`Expected key at position ${this.pos}`)
607
+ }
608
+ return { key, quoted: false }
609
+ }
610
+
611
+ private unfoldKey(keyPath: string, value: unknown): Record<string, unknown> {
612
+ const parts = keyPath.split(".")
613
+ let result: Record<string, unknown> = {}
614
+ let current = result
615
+
616
+ for (let i = 0; i < parts.length - 1; i++) {
617
+ const nested: Record<string, unknown> = {}
618
+ current[parts[i]] = nested
619
+ current = nested
620
+ }
621
+ current[parts[parts.length - 1]] = value
622
+
623
+ return result
624
+ }
625
+
626
+ private mergeObjects(target: Record<string, unknown>, src: Record<string, unknown>): void {
627
+ for (const key of Object.keys(src)) {
628
+ if (
629
+ key in target &&
630
+ typeof target[key] === "object" &&
631
+ target[key] !== null &&
632
+ !Array.isArray(target[key]) &&
633
+ typeof src[key] === "object" &&
634
+ src[key] !== null &&
635
+ !Array.isArray(src[key])
636
+ ) {
637
+ this.mergeObjects(
638
+ target[key] as Record<string, unknown>,
639
+ src[key] as Record<string, unknown>
640
+ )
641
+ } else {
642
+ target[key] = src[key]
643
+ }
644
+ }
645
+ }
646
+ }
647
+
648
+ export function parse(input: string): unknown {
649
+ return new JotParser(input).parse()
650
+ }
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@creationix/jot",
3
+ "version": "0.0.1",
4
+ "author": "Tim Caswell <tim@creationix.com>",
5
+ "license": "MIT",
6
+ "dependencies": {},
7
+ "devDependencies": {
8
+ "@types/bun": "^1.3.5"
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ {messages:{{:role,content;user,How do I configure TypeScript for a monorepo?;assistant,"For a monorepo setup, create a base tsconfig.json at the root with shared settings, then extend it in each package.";user,What about path aliases?}}}
@@ -0,0 +1 @@
1
+ {"messages":[{"role":"user","content":"How do I configure TypeScript for a monorepo?"},{"role":"assistant","content":"For a monorepo setup, create a base tsconfig.json at the root with shared settings, then extend it in each package."},{"role":"user","content":"What about path aliases?"}]}
@@ -0,0 +1,6 @@
1
+ { messages: {{
2
+ :role, content
3
+ user, How do I configure TypeScript for a monorepo?
4
+ assistant, "For a monorepo setup, create a base tsconfig.json at the root with shared settings, then extend it in each package."
5
+ user, What about path aliases?
6
+ }} }
@@ -0,0 +1,16 @@
1
+ {
2
+ "messages": [
3
+ {
4
+ "role": "user",
5
+ "content": "How do I configure TypeScript for a monorepo?"
6
+ },
7
+ {
8
+ "role": "assistant",
9
+ "content": "For a monorepo setup, create a base tsconfig.json at the root with shared settings, then extend it in each package."
10
+ },
11
+ {
12
+ "role": "user",
13
+ "content": "What about path aliases?"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1 @@
1
+ {active:true,ruleGroups:{php_attacks:{active:true,action:deny,rules:{php_suffix:{has:[{type:path,key:null,value.suf:.php}],missing:[{type:header,key:x-secret,value.eq:bypass}],phase:finalize,system:true},jsp_suffix:{has:[{type:path,key:null,value.suf:.jsp},{type:status,value.eq:404}],missing:[{type:header,key:x-secret,value.eq:bypass}],phase:init,system:true},asp_suffix:{has:[{type:path,key:null,value.suf:.asp}],missing:[{type:header,key:x-secret,value.eq:bypass}],phase:finalize,system:true},cgi_suffix:{has:[{type:path,key:null,value.suf:.cgi}],missing:[{type:header,key:x-secret,value.eq:bypass}],phase:finalize,system:true}}},sql_injection:{active:true,action:deny,rules:{union_select:{has:[{type:query,key:null,value.contains:UNION SELECT}],phase:init,system:true},drop_table:{has:[{type:query,key:null,value.contains:DROP TABLE}],phase:init,system:true},or_1_equals_1:{has:[{type:query,key:null,value.regex:"OR\\s+1\\s*=\\s*1"}],phase:init,system:true}}},rate_limiting:{active:true,action:ratelimit,rules:{api_endpoints:{has:[{type:path,key:null,value.pre:/api/}],rateLimit:{requests:100,window:60,key:ip},phase:init,system:false},auth_endpoints:{has:[{type:path,key:null,value.pre:/auth/}],rateLimit:{requests:10,window:60,key:ip},phase:init,system:false},search_endpoint:{has:[{type:path,key:null,value.eq:/search}],rateLimit:{requests:30,window:60,key:ip},phase:init,system:false}}},geo_blocking:{active:false,action:deny,rules.blocked_countries:{has:[{type:geo,key:country,value.in:[XX,YY,ZZ]}],missing:[{type:header,key:x-bypass-geo,value.eq:allowed}],phase:init,system:false}},bot_protection:{active:true,action:challenge,rules:{missing_ua:{missing:[{type:header,key:user-agent,value.exists:true}],phase:init,system:true},known_bad_bots:{has:[{type:header,key:user-agent,value.regex:(BadBot|Scraper|Harvester)}],phase:init,system:true},suspicious_rate:{has:{{:type,key,value;rate,ip,{gt:50};header,user-agent,{regex:^Mozilla}}},phase:finalize,system:false}}},custom_rules:{active:true,action:log,rules:{admin_access:{has:[{type:path,key:null,value.pre:/admin}],missing:[{type:header,key:x-admin-token,value.exists:true}],phase:init,system:false},api_key_required:{has:[{type:path,key:null,value.pre:/api/v2/}],missing:[{type:header,key:x-api-key,value.exists:true}],phase:init,system:false}}}}}