@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.
- package/LICENSE +21 -0
- package/README.md +112 -23
- package/dist/jot.cjs +446 -0
- package/dist/jot.d.ts +6 -0
- package/dist/jot.js +442 -0
- package/package.json +34 -4
- package/SUMMARY.md +0 -151
- package/TOKEN_COUNTS.md +0 -97
- package/bun.lock +0 -19
- package/jot.test.ts +0 -133
- package/jot.ts +0 -650
- package/samples/chat.jot +0 -1
- package/samples/chat.json +0 -1
- package/samples/chat.pretty.jot +0 -6
- package/samples/chat.pretty.json +0 -16
- package/samples/firewall.jot +0 -1
- package/samples/firewall.json +0 -1
- package/samples/firewall.pretty.jot +0 -235
- package/samples/firewall.pretty.json +0 -344
- package/samples/github-issue.jot +0 -1
- package/samples/github-issue.json +0 -1
- package/samples/github-issue.pretty.jot +0 -15
- package/samples/github-issue.pretty.json +0 -20
- package/samples/hikes.jot +0 -1
- package/samples/hikes.json +0 -1
- package/samples/hikes.pretty.jot +0 -14
- package/samples/hikes.pretty.json +0 -38
- package/samples/irregular.jot +0 -1
- package/samples/irregular.json +0 -1
- package/samples/irregular.pretty.jot +0 -13
- package/samples/irregular.pretty.json +0 -23
- package/samples/json-counts-cache.jot +0 -1
- package/samples/json-counts-cache.json +0 -1
- package/samples/json-counts-cache.pretty.jot +0 -26
- package/samples/json-counts-cache.pretty.json +0 -26
- package/samples/key-folding-basic.jot +0 -1
- package/samples/key-folding-basic.json +0 -1
- package/samples/key-folding-basic.pretty.jot +0 -7
- package/samples/key-folding-basic.pretty.json +0 -25
- package/samples/key-folding-mixed.jot +0 -1
- package/samples/key-folding-mixed.json +0 -1
- package/samples/key-folding-mixed.pretty.jot +0 -16
- package/samples/key-folding-mixed.pretty.json +0 -24
- package/samples/key-folding-with-array.jot +0 -1
- package/samples/key-folding-with-array.json +0 -1
- package/samples/key-folding-with-array.pretty.jot +0 -6
- package/samples/key-folding-with-array.pretty.json +0 -29
- package/samples/large.jot +0 -1
- package/samples/large.json +0 -1
- package/samples/large.pretty.jot +0 -72
- package/samples/large.pretty.json +0 -93
- package/samples/logs.jot +0 -1
- package/samples/logs.json +0 -1
- package/samples/logs.pretty.jot +0 -96
- package/samples/logs.pretty.json +0 -350
- package/samples/medium.jot +0 -1
- package/samples/medium.json +0 -1
- package/samples/medium.pretty.jot +0 -13
- package/samples/medium.pretty.json +0 -30
- package/samples/metrics.jot +0 -1
- package/samples/metrics.json +0 -1
- package/samples/metrics.pretty.jot +0 -11
- package/samples/metrics.pretty.json +0 -25
- package/samples/package.jot +0 -1
- package/samples/package.json +0 -1
- package/samples/package.pretty.jot +0 -18
- package/samples/package.pretty.json +0 -18
- package/samples/products.jot +0 -1
- package/samples/products.json +0 -1
- package/samples/products.pretty.jot +0 -69
- package/samples/products.pretty.json +0 -235
- package/samples/routes.jot +0 -1
- package/samples/routes.json +0 -1
- package/samples/routes.pretty.jot +0 -142
- package/samples/routes.pretty.json +0 -354
- package/samples/small.jot +0 -1
- package/samples/small.json +0 -1
- package/samples/small.pretty.jot +0 -8
- package/samples/small.pretty.json +0 -12
- package/samples/users-50.jot +0 -1
- package/samples/users-50.json +0 -1
- package/samples/users-50.pretty.jot +0 -53
- 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,
|
|
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
|
-
|
|
120
|
+
### Unquoted Strings
|
|
23
121
|
|
|
24
|
-
|
|
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
|
|
34
|
-
- It contains special characters: `: ; , { } [ ] "` or control characters
|
|
35
|
-
- It
|
|
36
|
-
- It
|
|
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
|
-
|
|
137
|
+
### Key Folding
|
|
47
138
|
|
|
48
|
-
When a nested object has exactly
|
|
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
|
|
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
|
-
|
|
159
|
+
### Tables
|
|
69
160
|
|
|
70
|
-
|
|
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
|
+
}
|