@tabnas/json 0.5.12 → 0.5.14

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/dist/json.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Tabnas, TabnasError, type Plugin } from '@tabnas/parser';
2
- export declare const VERSION = "0.5.12";
2
+ export declare const VERSION = "0.5.14";
3
3
  export interface JsonGrammarOptions {
4
4
  chainOff?: boolean;
5
5
  }
package/dist/json.js CHANGED
@@ -29,7 +29,7 @@ Object.defineProperty(exports, "JsonError", { enumerable: true, get: function ()
29
29
  // VERSION is this package's version. It MUST equal package.json "version":
30
30
  // the release orchestrator rewrites both, and test/version.test.js fails the
31
31
  // build if they drift. Mirrors `const VERSION` in go/json.go.
32
- exports.VERSION = '0.5.12';
32
+ exports.VERSION = '0.5.14';
33
33
  // JSON-only lexer/parser options. Restrictive enough to mirror
34
34
  // JSON.parse: double-quoted strings only, plain decimal numbers, quoted
35
35
  // keys only, no comments, no trailing-comma map extension, no empty
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabnas/json",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "main": "dist/json.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/json.d.ts",
@@ -38,7 +38,9 @@
38
38
  },
39
39
  "files": [
40
40
  "dist",
41
+ "src",
41
42
  "bin",
43
+ "README.md",
42
44
  "LICENSE"
43
45
  ],
44
46
  "bin": {
@@ -0,0 +1,51 @@
1
+ /* Copyright (c) 2026 tabnas, MIT License */
2
+
3
+ /* json-cli.ts
4
+ *
5
+ * A tiny command-line front end: read JSON from the arguments or from
6
+ * stdin, parse it, and print the canonical re-serialized form. Exit code
7
+ * 1 (with the error on stderr) when the input is not valid JSON.
8
+ *
9
+ * The logic is split into `run` (pure: source in, exit code out, writes
10
+ * via injected sinks) and `main` (process wiring), so `run` and `main`
11
+ * are both testable in-process. `bin/json` calls `main`.
12
+ */
13
+
14
+ import { parse, TabnasError } from './json'
15
+
16
+ // Parse `src`, writing the pretty-printed value to `out` on success or
17
+ // the error message to `errOut` on failure. Returns the process exit code.
18
+ export function run(
19
+ src: string,
20
+ out: (s: string) => void,
21
+ errOut: (s: string) => void,
22
+ ): number {
23
+ try {
24
+ out(JSON.stringify(parse(src), null, 2) + '\n')
25
+ return 0
26
+ } catch (err) {
27
+ if (err instanceof TabnasError) {
28
+ errOut(err.message + '\n')
29
+ return 1
30
+ }
31
+ throw err
32
+ }
33
+ }
34
+
35
+ // Wire `run` to a process-like environment. With arguments, parse them;
36
+ // otherwise read all of stdin. Calls `exit` with the resulting code.
37
+ export function main(
38
+ argv: string[],
39
+ stdin: NodeJS.ReadableStream,
40
+ out: (s: string) => void,
41
+ errOut: (s: string) => void,
42
+ exit: (code: number) => void,
43
+ ): void {
44
+ if (argv.length > 0) {
45
+ exit(run(argv.join(' '), out, errOut))
46
+ return
47
+ }
48
+ const chunks: Buffer[] = []
49
+ stdin.on('data', (c: Buffer) => chunks.push(Buffer.from(c)))
50
+ stdin.on('end', () => exit(run(Buffer.concat(chunks).toString('utf8'), out, errOut)))
51
+ }
package/src/json.ts ADDED
@@ -0,0 +1,264 @@
1
+ /* Copyright (c) 2026 tabnas, MIT License */
2
+
3
+ /* json.ts
4
+ *
5
+ * A standard JSON grammar plugin for the `tabnas` parsing engine.
6
+ *
7
+ * The engine ships no grammar of its own; this package supplies the
8
+ * strict, standard-JSON one. The rule set (val / map / list / pair /
9
+ * elem) is the "Plain JSON" grammar from jsonic's `grammar.ts` — the
10
+ * pure-JSON core jsonic defines before extending it for the relaxed
11
+ * jsonic format. Here that core is installed on its own, with the lexer
12
+ * restricted to strict JSON, and none of jsonic's extended grammar
13
+ * (comments, unquoted keys, implicit objects/arrays, trailing commas,
14
+ * single/backtick strings, path diving).
15
+ *
16
+ * This plugin is intended to be the foundation other tabnas grammar
17
+ * plugins build on: `use` it first, then layer additional rules on the
18
+ * shared val / map / list / pair / elem rules.
19
+ */
20
+
21
+ import { Tabnas, TabnasError, type Plugin } from '@tabnas/parser'
22
+
23
+ // VERSION is this package's version. It MUST equal package.json "version":
24
+ // the release orchestrator rewrites both, and test/version.test.js fails the
25
+ // build if they drift. Mirrors `const VERSION` in go/json.go.
26
+ export const VERSION = '0.5.14'
27
+
28
+ // JSON-only lexer/parser options. Restrictive enough to mirror
29
+ // JSON.parse: double-quoted strings only, plain decimal numbers, quoted
30
+ // keys only, no comments, no trailing-comma map extension, no empty
31
+ // input, and the rule set restricted to the `json`-tagged alternates.
32
+ const JSON_OPTIONS = {
33
+ text: { lex: false },
34
+ number: {
35
+ hex: false,
36
+ oct: false,
37
+ bin: false,
38
+ sep: null,
39
+ // Reject any token the (lenient) number matcher accepts that is not a
40
+ // strict JSON number: leading `+`, a bare leading `.` (`.5`), a
41
+ // trailing `.` (`1.`), and leading zeros (`01`, `00`). The negative
42
+ // lookahead matches — and so excludes — anything not of the form
43
+ // -?(0|[1-9][0-9]*)(.[0-9]+)?([eE][+-]?[0-9]+)?.
44
+ exclude: /^(?!-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$)/,
45
+ },
46
+ string: {
47
+ chars: '"',
48
+ multiChars: '',
49
+ // Standard JSON escape handling: allowUnknown:false rejects any
50
+ // unrecognized escape (\q, \z); escapeStrict disables the engine's
51
+ // non-standard \xHH and \u{...} structural escapes (plain \uXXXX
52
+ // stays); and dropping v / ' / ` from the escape map removes the
53
+ // remaining non-standard built-ins. Result: exactly the JSON.parse
54
+ // escape set, identical to the Go engine.
55
+ allowUnknown: false,
56
+ escapeStrict: true,
57
+ escape: { v: '', "'": '', '`': '' },
58
+ },
59
+ comment: { lex: false },
60
+ map: { extend: false },
61
+ lex: { empty: false },
62
+ rule: { finish: false, include: 'json' },
63
+ result: { fail: [undefined, NaN] },
64
+ tokenSet: { KEY: ['#ST', null, null, null] },
65
+ }
66
+
67
+ // Options for `registerJsonGrammar`. The zero state -- no argument at
68
+ // all -- installs the layerable grammar, which is the safe default.
69
+ export interface JsonGrammarOptions {
70
+ // Declare that no rule in the ASSEMBLED grammar resolves `$prev`, so
71
+ // `@push$` need not re-publish the grown list back along the
72
+ // `r: 'elem'` replacement chain. The JSON core never reads a replaced
73
+ // rule, but a plugin layering its own alternates onto `elem` or `list`
74
+ // might, and only that plugin knows. So the core cannot make the claim
75
+ // on its behalf: it is off here and the complete `json` plugin -- the
76
+ // parser nobody has extended -- is what turns it on.
77
+ //
78
+ // A no-op in this runtime and in Rust, which hand out one array object
79
+ // that every view of the list already shares. It matters in Go, where a
80
+ // list is a slice VALUE and the walk it skips is O(elements^2).
81
+ chainOff?: boolean
82
+ }
83
+
84
+ // Install the pure JSON rule set (val / map / list / pair / elem) on the
85
+ // given engine instance. Exposed separately from the options so other
86
+ // grammar plugins can layer their extensions on top without re-declaring
87
+ // the JSON core. This is jsonic's "Plain JSON" grammar.
88
+ export function registerJsonGrammar(
89
+ tn: Tabnas,
90
+ opts?: JsonGrammarOptions,
91
+ ): void {
92
+ // Spread onto the two `elem` close alts. Nothing is emitted unless
93
+ // asked for, and an absent `k` is the engine default: the walk runs.
94
+ // Called per alt so each gets its OWN object, as the two literals it
95
+ // replaces did: alt config is the engine's to read, and two alts
96
+ // sharing one object would make that an assumption rather than a fact.
97
+ const pushChain = () =>
98
+ opts?.chainOff ? { k: { push$: { chain: false } } } : {}
99
+
100
+ tn.grammar({
101
+ // The schema version of the native-value builtins this grammar binds
102
+ // to (object/array/reset/key/setval/push/value).
103
+ v: 2,
104
+
105
+ // The value tree is built ENTIRELY by the engine's native-value
106
+ // `$`-builtins — referenced by name on the alts below; the engine
107
+ // merges them in at load. There are NO grammar-local closures: the
108
+ // builders are info-aware, so when info.map/list/text is enabled they
109
+ // attach the introspection marker themselves (the json plugin used to
110
+ // hand-write that as @jsonMapMark/@jsonListMark/@jsonText/@jsonSetval).
111
+ // Strict JSON containers are always explicit, so @object$/@array$ take
112
+ // the default implicit:false (no `k` config needed).
113
+ //
114
+ // The builtin actions used below, one line each:
115
+ // @reset$ — clear the parent-seeded node (so a value doesn't inherit
116
+ // the parent container).
117
+ // @object$ — allocate an empty object into the node (a MapRef under
118
+ // info.map).
119
+ // @array$ — allocate an empty array into the node (a ListRef under
120
+ // info.list).
121
+ // @key$ — capture the matched key token into a scratch slot for the
122
+ // pending @setval$.
123
+ // @setval$ — assign the just-built child value into the object under
124
+ // the captured key.
125
+ // @push$ — append the just-built child value to the array.
126
+ // @value$ — resolve the rule's value: a built child wins, else the
127
+ // scalar token (boxed with quote info under info.text).
128
+ rule: {
129
+ val: {
130
+ // Opening token alternates. @reset$ clears the parent-seeded node
131
+ // so a scalar doesn't inherit the parent container.
132
+ open: [
133
+ // A map: `{ ...`
134
+ { s: '#OB', p: 'map', b: 1, a: '@reset$', g: 'map,json' },
135
+
136
+ // A list: `[ ...`
137
+ { s: '#OS', p: 'list', b: 1, a: '@reset$', g: 'list,json' },
138
+
139
+ // A plain value: `"x"` `1` `true` ....
140
+ { s: '#VAL', a: '@reset$', g: 'val,json' },
141
+ ],
142
+
143
+ // Closing alternates. @value$ coalesces (child wins, else the
144
+ // scalar token) and, under info.text, boxes a string with its quote.
145
+ close: [
146
+ // End of input.
147
+ { s: '#ZZ', a: '@value$', g: 'end,json' },
148
+
149
+ // There's more JSON.
150
+ { b: 1, a: '@value$', g: 'more,json' },
151
+ ],
152
+ },
153
+
154
+ map: {
155
+ open: [
156
+ // An empty map: {}.
157
+ { s: '#OB #CB', b: 1, n: { pk: 0 }, a: '@object$', g: 'map,json' },
158
+
159
+ // Start matching map key-value pairs.
160
+ // Reset counter n.pk as new map (for extensions).
161
+ { s: '#OB', p: 'pair', n: { pk: 0 }, a: '@object$', g: 'map,json,pair' },
162
+ ],
163
+ close: [
164
+ // End of map.
165
+ { s: '#CB', g: 'end,json' },
166
+ ],
167
+ },
168
+
169
+ list: {
170
+ open: [
171
+ // An empty list: [].
172
+ { s: '#OS #CS', b: 1, a: '@array$', g: 'list,json' },
173
+
174
+ // Start matching list elements.
175
+ { s: '#OS', p: 'elem', a: '@array$', g: 'list,elem,json' },
176
+ ],
177
+ close: [
178
+ // End of list.
179
+ { s: '#CS', g: 'end,json' },
180
+ ],
181
+ },
182
+
183
+ // sets key:val on node
184
+ pair: {
185
+ open: [
186
+ // Match key-colon start of pair. @key$ captures the key.
187
+ {
188
+ s: '#KEY #CL',
189
+ p: 'val',
190
+ u: { pair: true },
191
+ a: '@key$',
192
+ g: 'map,pair,key,json',
193
+ },
194
+ ],
195
+ close: [
196
+ // Comma means a new pair at same pair-key level. @setval$
197
+ // assigns the pair; on every close alt (fan-out).
198
+ { s: '#CA', r: 'pair', a: '@setval$', g: 'map,pair,comma,json' },
199
+
200
+ // End of map.
201
+ { s: '#CB', b: 1, a: '@setval$', g: 'map,pair,close,json' },
202
+ ],
203
+ },
204
+
205
+ // push onto node
206
+ elem: {
207
+ open: [
208
+ // List elements are values.
209
+ { p: 'val', g: 'list,elem,val,json' },
210
+ ],
211
+ close: [
212
+ // Next element. @push$ appends the element. `r: 'elem'`
213
+ // REPLACES this rule, so the rule it displaces keeps its own
214
+ // view of the list -- which is why `chainOff` is a claim only
215
+ // the assembled grammar can make. See JsonGrammarOptions.
216
+ { s: '#CA', r: 'elem', a: '@push$',
217
+ ...pushChain(), g: 'list,elem,comma,json' },
218
+
219
+ // End of list.
220
+ { s: '#CS', b: 1, a: '@push$',
221
+ ...pushChain(), g: 'list,elem,close,json' },
222
+ ],
223
+ },
224
+ },
225
+ })
226
+ }
227
+
228
+ // The standard plugin form: apply the strict JSON options, then register
229
+ // the JSON grammar. `use` this on a bare engine, or pass it to `make`.
230
+ export const json: Plugin = function json(tn: Tabnas, _options?: any) {
231
+ tn.options(JSON_OPTIONS)
232
+ // The complete parser: these rules are the whole grammar, and none of
233
+ // them reads a replaced rule. That is what makes `chainOff` true here
234
+ // and false in the rules-only installer above.
235
+ registerJsonGrammar(tn, { chainOff: true })
236
+ }
237
+
238
+ // Create a standard-JSON parser instance: a tabnas engine with the json
239
+ // plugin installed. Extra options (e.g. info.map/list/text) are applied
240
+ // after the grammar exists, mirroring the Go `Make`.
241
+ export function make(opts?: Record<string, any>): Tabnas {
242
+ const tn = new Tabnas({ plugins: [json] })
243
+ if (opts) {
244
+ tn.options(opts)
245
+ }
246
+ return tn
247
+ }
248
+
249
+ // A lazily-created default instance reused by `parse`, so repeated calls
250
+ // don't rebuild the engine and grammar each time. Parsing creates a fresh
251
+ // context per call, so reuse is safe.
252
+ let defaultParser: Tabnas | undefined
253
+
254
+ // Parse a JSON source string with a default standard-JSON parser and
255
+ // return the resulting value. Throws a TabnasError on invalid input.
256
+ export function parse(src: string): any {
257
+ return (defaultParser ??= make()).parse(src)
258
+ }
259
+
260
+ export { Tabnas, TabnasError }
261
+ export { TabnasError as JsonError }
262
+ export { translate } from './translate'
263
+ export type { TranslationPart, TranslationParts } from './translate'
264
+ export default parse
@@ -0,0 +1,76 @@
1
+ /* Copyright (c) 2026 tabnas, MIT License */
2
+
3
+ // Generated by ../embed-translate.js. Edit tabnas.plugin.json or alchemy/*,
4
+ // then run npm run embed.
5
+
6
+ type TranslationPart = Readonly<{
7
+ entry: string
8
+ source?: string
9
+ }>
10
+
11
+ type TranslationParts = Readonly<{
12
+ manifest: string
13
+ lift?: TranslationPart
14
+ render?: TranslationPart
15
+ }>
16
+
17
+ const TRANSLATION: TranslationParts = Object.freeze({
18
+ manifest: `{
19
+ "$schema": "https://tabnas.dev/schema/plugin.schema.json",
20
+ "name": "@tabnas/json",
21
+ "go": "github.com/tabnas/json/go",
22
+ "description": "A standard JSON parser (RFC 8259) for the tabnas engine.",
23
+ "engine": "@tabnas/parser",
24
+ "extensions": [
25
+ ".json"
26
+ ],
27
+ "mediaTypes": [
28
+ "application/json"
29
+ ],
30
+ "languageId": "json",
31
+ "pluginKind": "grammar",
32
+ "grammarKind": "data",
33
+ "lexStream": "clean",
34
+ "syncGroups": [
35
+ "end",
36
+ "comma",
37
+ "close"
38
+ ],
39
+ "specDir": "test/spec",
40
+ "clib": {
41
+ "dir": "go/clib",
42
+ "library": "libtabnasjson",
43
+ "abi": "v1",
44
+ "errorCodes": [
45
+ "usage",
46
+ "grammar",
47
+ "handle",
48
+ "internal"
49
+ ]
50
+ },
51
+ "errorCodes": [],
52
+ "docs": "https://tabnas.dev/docs/packages",
53
+ "repository": "https://github.com/tabnas/json",
54
+ "versionSource": "ts/package.json",
55
+ "translate": {
56
+ "reads": "tree",
57
+ "writes": "tree",
58
+ "render": "json"
59
+ }
60
+ }
61
+ `,
62
+ render: Object.freeze({ entry: "json" })
63
+ })
64
+
65
+ function translate (): TranslationParts | undefined {
66
+ return TRANSLATION
67
+ }
68
+
69
+ export {
70
+ translate,
71
+ }
72
+
73
+ export type {
74
+ TranslationPart,
75
+ TranslationParts,
76
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "module": "nodenext",
5
+ "noEmitOnError": true,
6
+ "outDir": "../dist",
7
+ "rootDir": ".",
8
+ "resolveJsonModule": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "target": "es2021",
12
+ "declaration": true,
13
+ "declarationDir": "../dist",
14
+ "types": ["node"]
15
+ }
16
+ }