tjawn 0.0.0-stage → 2.0.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/README.md +230 -2
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +10 -0
- package/dist/files.d.ts +10 -0
- package/dist/files.js +52 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/jawnset.d.ts +55 -0
- package/dist/jawnset.js +250 -0
- package/dist/lexer.d.ts +62 -0
- package/dist/lexer.js +190 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +443 -0
- package/dist/serializer.d.ts +14 -0
- package/dist/serializer.js +110 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/values.d.ts +16 -0
- package/dist/values.js +105 -0
- package/package.json +38 -4
package/README.md
CHANGED
|
@@ -1,3 +1,231 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tjawn <img src="https://github.com/tjkopena/tjawn/raw/main/docs/shark.png" height="50" title="tjawn" alt='Drawing of a shark stabbing out of the water and growling "Rwaa rwaa, rwaa rwaa!" as it tries to eat you.' style="vertical-align: middle" />
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This is an implementation of *jawns*, a human friendly lightweight
|
|
4
|
+
object notation. A jawn is a Python dict literal or a slight superset
|
|
5
|
+
of JSON but with more flexible formatting. An example:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
protocol: 1.0,
|
|
9
|
+
token: 854c37ee-517c-4ba6-b56b-06475ad24041
|
|
10
|
+
conn: {
|
|
11
|
+
offer= { 'TLS1.2', TLS1.3 "TLS 1.17" }
|
|
12
|
+
"dns:resolver" = 10.0.0.1
|
|
13
|
+
cert: """
|
|
14
|
+
-----BEGIN CERTIFICATE-----
|
|
15
|
+
MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL
|
|
16
|
+
MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC
|
|
17
|
+
VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx
|
|
18
|
+
...
|
|
19
|
+
"""
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Which parses to the equivalent of this Python literal:
|
|
24
|
+
```python
|
|
25
|
+
{
|
|
26
|
+
"protocol": 1.0,
|
|
27
|
+
"token": "854c37ee-517c-4ba6-b56b-06475ad24041",
|
|
28
|
+
"conn": {
|
|
29
|
+
"offer": {"TLS1.2", "TLS1.3", "TLS 1.17"},
|
|
30
|
+
"dns:resolver": "10.0.0.1",
|
|
31
|
+
"cert": """-----BEGIN CERTIFICATE-----
|
|
32
|
+
MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL
|
|
33
|
+
MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC
|
|
34
|
+
VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx
|
|
35
|
+
..."""
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Some features:
|
|
41
|
+
* Strings without whitespace generally do not need to be quoted.
|
|
42
|
+
* Single or double quotes used interchangeably (in matched pairs).
|
|
43
|
+
* Multiline whitespace trimming with relative indentation preservation.
|
|
44
|
+
* Key/value pairs interchangeably denoted by either ':' or '='.
|
|
45
|
+
* Commas largely optional but permitted (e.g., on closing elements).
|
|
46
|
+
* Objects, sets, and lists are delimited by brackets, not whitespace.
|
|
47
|
+
* Sets are supported.
|
|
48
|
+
|
|
49
|
+
> _"jawn?"_
|
|
50
|
+
|
|
51
|
+
JSON Alternative Written Notation?\
|
|
52
|
+
Joe's Awesome Wacky ... Notation?\
|
|
53
|
+
[Don't think about it too hard.](https://en.wikipedia.org/wiki/Jawn)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
> _"But why?"_
|
|
57
|
+
|
|
58
|
+
I (tjkopena) find it _almost_ convenient to write simple config files
|
|
59
|
+
or parameter sets and so on in JSON. I don't implement such often
|
|
60
|
+
enough to bother to remember how to format TOML or YAML or whateverML,
|
|
61
|
+
and in any event I dislike their whitespace-based formatting, nesting,
|
|
62
|
+
and other aspects. I know JSON, use it all the time, I just want to
|
|
63
|
+
write a little object file in it. But it's _not actually_ convenient
|
|
64
|
+
to do so because JSON is a picky, pedantic pain in the ass---double
|
|
65
|
+
quotes on everything, no trailing commas, blah blah blah GTFO. Hence
|
|
66
|
+
jawns, a similarly simple, straightforward object model and syntax
|
|
67
|
+
that's not too fussed about the details.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## jawns
|
|
71
|
+
|
|
72
|
+
jawns and their closely associated human-friendly text syntax are
|
|
73
|
+
specified in [](docs/jawn.md). The core description is pretty readable.
|
|
74
|
+
A grammar is in [](src/jawn.g).
|
|
75
|
+
|
|
76
|
+
In sum: A jawn is a map; its values are recursive collections maps,
|
|
77
|
+
lists, or sets, or primitives strings, numbers, booleans, or null; you
|
|
78
|
+
can write them down without lots of double quotes and without having to
|
|
79
|
+
precisely maintain whitespace.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Implementations
|
|
83
|
+
|
|
84
|
+
This repository includes several implementations, outlined here.
|
|
85
|
+
|
|
86
|
+
### Python
|
|
87
|
+
|
|
88
|
+
#### Setup
|
|
89
|
+
|
|
90
|
+
tjawn's Python module has no dependencies. It is [listed in
|
|
91
|
+
PyPi](https://pypi.org/project/tjawn/) and should be includable in
|
|
92
|
+
all the usual ways.
|
|
93
|
+
|
|
94
|
+
#### Usage
|
|
95
|
+
|
|
96
|
+
The `loads` and `dumps` functions respectively provide for reading and
|
|
97
|
+
writing jawns from a string.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
import tjawn
|
|
101
|
+
|
|
102
|
+
o = tjawn.loads('a: "Four score and seven..."')
|
|
103
|
+
print(tjawn.dumps(o))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A jawn is a map. As a convenience, a list of jawns, e.g. a file of
|
|
107
|
+
several records, can be read and written with separate functions:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
l = tjawn.loads_list('[ {name: alice} {name: bob} ]')
|
|
111
|
+
print(tjawn.dumps_list(l))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `loadf`, `dumpf`, `loadf_list`, and `dumpf_list` functions do the
|
|
115
|
+
same for UTF-8 files named by a filename parameter:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
tjawn.dumpf(o, 'config.jawn')
|
|
119
|
+
o = tjawn.loadf('config.jawn')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Development Notes
|
|
123
|
+
|
|
124
|
+
Internally, tjawn's Python module is implemented using Lark to
|
|
125
|
+
generate the lexer and parser, which are then vendored in the project
|
|
126
|
+
rather than incurring a dependency. From the `src/py` directory:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
poetry install --no-root # install pytest and lark for development
|
|
130
|
+
make parser # regenerate tjawn/lark_parser.py from src/jawn.g
|
|
131
|
+
make test # run the tests
|
|
132
|
+
make build # build the package, including this README
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The tests check that the vendored parser matches `src/jawn.g`.
|
|
136
|
+
|
|
137
|
+
### TypeScript
|
|
138
|
+
|
|
139
|
+
#### Setup
|
|
140
|
+
|
|
141
|
+
tjawn's TypeScript package, in `src/ts`, has no runtime dependencies.
|
|
142
|
+
It is an ES module with type declarations. Parsing and serializing
|
|
143
|
+
work in any ES2022 environment, including browsers; the file functions
|
|
144
|
+
require Node.js 20.16 or later.
|
|
145
|
+
|
|
146
|
+
#### Usage
|
|
147
|
+
|
|
148
|
+
The `parse` and `stringify` functions respectively provide for reading
|
|
149
|
+
and writing jawns from a string, as `JSON.parse` and `JSON.stringify`
|
|
150
|
+
do for JSON.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
import * as tjawn from 'tjawn';
|
|
154
|
+
|
|
155
|
+
const o = tjawn.parse('a: "Four score and seven..."');
|
|
156
|
+
console.log(tjawn.stringify(o));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Maps are parsed to plain objects and lists to arrays. Sets are parsed
|
|
160
|
+
to `JawnSet`s, which compare their elements by value, e.g. `{ {a: 1} }`
|
|
161
|
+
holds one map that `has({ a: 1 })`, and keep `true` distinct from `1`.
|
|
162
|
+
Numbers are JavaScript numbers, so `1.0` is read as `1` and written that
|
|
163
|
+
way. `jawnEquals` compares two values as jawns.
|
|
164
|
+
|
|
165
|
+
`stringify` writes objects, `Map`s, arrays, `JawnSet`s, and `Set`s.
|
|
166
|
+
Class instances are written as maps of their own enumerable properties
|
|
167
|
+
not starting with `_`, and properties whose value is `undefined` are
|
|
168
|
+
skipped. Options `indent` and `eoln` set the indentation and
|
|
169
|
+
linebreak; with an empty `eoln` all is written on one line.
|
|
170
|
+
|
|
171
|
+
A jawn is a map. As a convenience, a list of jawns, e.g. a file of
|
|
172
|
+
several records, can be read and written with separate functions:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
const l = tjawn.parseList('[ {name: alice} {name: bob} ]');
|
|
176
|
+
console.log(tjawn.stringifyList(l));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `loadFile`, `dumpFile`, `loadFileList`, and `dumpFileList`
|
|
180
|
+
functions do the same for UTF-8 files named by a path or file URL:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
tjawn.dumpFile(o, 'config.jawn');
|
|
184
|
+
const o = tjawn.loadFile('config.jawn');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Errors are thrown as `JawnError`, or its subclass `JawnTypeError` for
|
|
188
|
+
values that can't be written as jawns.
|
|
189
|
+
|
|
190
|
+
#### Development Notes
|
|
191
|
+
|
|
192
|
+
The parser is written by hand, with a contextual lexer whose terminals
|
|
193
|
+
are transcribed from `src/jawn.g` and that mirrors Lark's, so that it
|
|
194
|
+
accepts exactly what the Python module does and reports errors the
|
|
195
|
+
same way. From the `src/ts` directory:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm install # install TypeScript and Vitest for development
|
|
199
|
+
npm test # run the tests
|
|
200
|
+
npm run typecheck # check types, including the tests'
|
|
201
|
+
npm run build # build the package into dist
|
|
202
|
+
npm pack # package it, including this README
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The tests check that the lexer's terminals match `src/jawn.g`.
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
tjawn is released under MIT-0:
|
|
211
|
+
|
|
212
|
+
> Copyright 2023, 2026 tjkopena
|
|
213
|
+
>
|
|
214
|
+
> Permission is hereby granted, free of charge, to any person
|
|
215
|
+
> obtaining a copy of this software and associated documentation files
|
|
216
|
+
> (the "Software"), to deal in the Software without restriction,
|
|
217
|
+
> including without limitation the rights to use, copy, modify, merge,
|
|
218
|
+
> publish, distribute, sublicense, and/or sell copies of the Software,
|
|
219
|
+
> and to permit persons to whom the Software is furnished to do so.
|
|
220
|
+
>
|
|
221
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
222
|
+
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
223
|
+
> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
224
|
+
> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
225
|
+
> BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
226
|
+
> ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
227
|
+
> CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
228
|
+
> SOFTWARE.
|
|
229
|
+
|
|
230
|
+
[Shark icon](https://thenounproject.com/icon/shark-1257563/) by Luis
|
|
231
|
+
Prado for The Noun Project, CCBY3.0.
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Thrown for text that isn't a valid jawn, or data that can't be one. */
|
|
2
|
+
export declare class JawnError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Thrown for data of a type that can't be a jawn value. */
|
|
6
|
+
export declare class JawnTypeError extends JawnError {
|
|
7
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Thrown for text that isn't a valid jawn, or data that can't be one. */
|
|
2
|
+
export class JawnError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Thrown for data of a type that can't be a jawn value. */
|
|
9
|
+
export class JawnTypeError extends JawnError {
|
|
10
|
+
}
|
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StringifyOptions } from './serializer.js';
|
|
2
|
+
import type { Jawn } from './types.js';
|
|
3
|
+
/** Parse a jawn from a UTF-8 file. */
|
|
4
|
+
export declare function loadFile(path: string | URL): Jawn;
|
|
5
|
+
/** Parse a list of jawns from a UTF-8 file. */
|
|
6
|
+
export declare function loadFileList(path: string | URL): Jawn[];
|
|
7
|
+
/** Serialize a jawn to a UTF-8 file, ending with a linebreak. */
|
|
8
|
+
export declare function dumpFile(o: unknown, path: string | URL, options?: StringifyOptions): void;
|
|
9
|
+
/** Serialize a list of jawns to a UTF-8 file, ending with a linebreak. */
|
|
10
|
+
export declare function dumpFileList(o: unknown, path: string | URL, options?: StringifyOptions): void;
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { JawnError } from './errors.js';
|
|
2
|
+
import { parse, parseList } from './parser.js';
|
|
3
|
+
import { stringify, stringifyList } from './serializer.js';
|
|
4
|
+
// Node's modules are obtained when called rather than imported, so that
|
|
5
|
+
// the rest of the package works where they're absent, e.g. browsers.
|
|
6
|
+
function node() {
|
|
7
|
+
const getBuiltinModule = globalThis.process?.getBuiltinModule;
|
|
8
|
+
if (!getBuiltinModule) {
|
|
9
|
+
throw new Error('Reading and writing jawn files requires Node.js 20.16 or later');
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
fs: getBuiltinModule('node:fs'),
|
|
13
|
+
url: getBuiltinModule('node:url'),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Parse a jawn from a UTF-8 file. */
|
|
17
|
+
export function loadFile(path) {
|
|
18
|
+
return load(parse, path);
|
|
19
|
+
}
|
|
20
|
+
/** Parse a list of jawns from a UTF-8 file. */
|
|
21
|
+
export function loadFileList(path) {
|
|
22
|
+
return load(parseList, path);
|
|
23
|
+
}
|
|
24
|
+
function load(f, path) {
|
|
25
|
+
// Linebreaks are read untranslated, as the parser handles them, and
|
|
26
|
+
// errors name the file.
|
|
27
|
+
const { fs, url } = node();
|
|
28
|
+
const text = fs.readFileSync(path, 'utf8');
|
|
29
|
+
try {
|
|
30
|
+
return f(text);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (e instanceof JawnError) {
|
|
34
|
+
const name = path instanceof URL ? url.fileURLToPath(path) : path;
|
|
35
|
+
throw new JawnError(`${name}: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Serialize a jawn to a UTF-8 file, ending with a linebreak. */
|
|
41
|
+
export function dumpFile(o, path, options = {}) {
|
|
42
|
+
dump(stringify(o, options), path, options);
|
|
43
|
+
}
|
|
44
|
+
/** Serialize a list of jawns to a UTF-8 file, ending with a linebreak. */
|
|
45
|
+
export function dumpFileList(o, path, options = {}) {
|
|
46
|
+
dump(stringifyList(o, options), path, options);
|
|
47
|
+
}
|
|
48
|
+
function dump(s, path, options) {
|
|
49
|
+
// The text is serialized before the file is opened so that an error
|
|
50
|
+
// leaves any existing file untouched.
|
|
51
|
+
node().fs.writeFileSync(path, s + (options.eoln || '\n'), 'utf8');
|
|
52
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { JawnError, JawnTypeError } from './errors.js';
|
|
2
|
+
export { JawnSet, jawnEquals } from './jawnset.js';
|
|
3
|
+
export { parse, parseList } from './parser.js';
|
|
4
|
+
export { stringify, stringifyList } from './serializer.js';
|
|
5
|
+
export type { StringifyOptions } from './serializer.js';
|
|
6
|
+
export { loadFile, loadFileList, dumpFile, dumpFileList } from './files.js';
|
|
7
|
+
export type { Jawn, JawnMap, JawnPrimitive, JawnValue } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { JawnError, JawnTypeError } from './errors.js';
|
|
2
|
+
export { JawnSet, jawnEquals } from './jawnset.js';
|
|
3
|
+
export { parse, parseList } from './parser.js';
|
|
4
|
+
export { stringify, stringifyList } from './serializer.js';
|
|
5
|
+
export { loadFile, loadFileList, dumpFile, dumpFileList } from './files.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JawnValue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The canonical form of a jawn value, a string that's equal for two
|
|
4
|
+
* values exactly when they're equal as jawns: maps and sets compare
|
|
5
|
+
* regardless of order, numbers by value (so -0 is 0), and booleans are
|
|
6
|
+
* distinct from numbers. Throws a JawnError for a value that can't be a
|
|
7
|
+
* jawn value.
|
|
8
|
+
*/
|
|
9
|
+
export declare function jawnKey(v: unknown): string;
|
|
10
|
+
/** Whether two values are equal as jawn values; false if either isn't one. */
|
|
11
|
+
export declare function jawnEquals(a: unknown, b: unknown): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* A set of jawn values compared by value and type.
|
|
14
|
+
*
|
|
15
|
+
* Unlike the builtin Set, elements are compared by their content rather
|
|
16
|
+
* than identity, so maps and lists may be elements, and booleans are
|
|
17
|
+
* distinct from numbers. Elements are compared by their content when
|
|
18
|
+
* added, so mutating one afterwards leaves the set inconsistent. Methods
|
|
19
|
+
* taking another collection compare by jawn value too, e.g.
|
|
20
|
+
* `new JawnSet([true]).isSubsetOf(new Set([1]))` is false.
|
|
21
|
+
*/
|
|
22
|
+
export declare class JawnSet<T = JawnValue> implements Iterable<T> {
|
|
23
|
+
private items;
|
|
24
|
+
constructor(items?: Iterable<T> | null);
|
|
25
|
+
get size(): number;
|
|
26
|
+
get [Symbol.toStringTag](): string;
|
|
27
|
+
/** Adds an element, which must be a jawn value. */
|
|
28
|
+
add(x: T): this;
|
|
29
|
+
/** Like an absent element, a non-jawn value is simply not present. */
|
|
30
|
+
has(x: unknown): boolean;
|
|
31
|
+
delete(x: unknown): boolean;
|
|
32
|
+
clear(): void;
|
|
33
|
+
values(): IterableIterator<T>;
|
|
34
|
+
keys(): IterableIterator<T>;
|
|
35
|
+
entries(): IterableIterator<[T, T]>;
|
|
36
|
+
[Symbol.iterator](): IterableIterator<T>;
|
|
37
|
+
forEach(f: (value: T, value2: T, set: this) => void, thisArg?: unknown): void;
|
|
38
|
+
copy(): JawnSet<T>;
|
|
39
|
+
/** Whether other is a set with the same elements, compared as jawns. */
|
|
40
|
+
equals(other: unknown): boolean;
|
|
41
|
+
union<U>(...others: Iterable<U>[]): JawnSet<T | U>;
|
|
42
|
+
intersection(...others: Iterable<unknown>[]): JawnSet<T>;
|
|
43
|
+
difference(...others: Iterable<unknown>[]): JawnSet<T>;
|
|
44
|
+
symmetricDifference<U>(other: Iterable<U>): JawnSet<T | U>;
|
|
45
|
+
isSubsetOf(other: Iterable<unknown>): boolean;
|
|
46
|
+
isSupersetOf(other: Iterable<unknown>): boolean;
|
|
47
|
+
isDisjointFrom(other: Iterable<unknown>): boolean;
|
|
48
|
+
update(...others: Iterable<T>[]): this;
|
|
49
|
+
intersectionUpdate(...others: Iterable<unknown>[]): this;
|
|
50
|
+
differenceUpdate(...others: Iterable<unknown>[]): this;
|
|
51
|
+
symmetricDifferenceUpdate(other: Iterable<T>): this;
|
|
52
|
+
private within;
|
|
53
|
+
/** The elements as an array, so that JSON.stringify writes sets as lists. */
|
|
54
|
+
toJSON(): T[];
|
|
55
|
+
}
|
package/dist/jawnset.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { JawnError, JawnTypeError } from './errors.js';
|
|
2
|
+
import { classify, mapEntries, nested, toNumber, typeName } from './values.js';
|
|
3
|
+
/**
|
|
4
|
+
* The canonical form of a jawn value, a string that's equal for two
|
|
5
|
+
* values exactly when they're equal as jawns: maps and sets compare
|
|
6
|
+
* regardless of order, numbers by value (so -0 is 0), and booleans are
|
|
7
|
+
* distinct from numbers. Throws a JawnError for a value that can't be a
|
|
8
|
+
* jawn value.
|
|
9
|
+
*/
|
|
10
|
+
export function jawnKey(v) {
|
|
11
|
+
return key(v, new Set());
|
|
12
|
+
}
|
|
13
|
+
function key(v, stack) {
|
|
14
|
+
switch (classify(v)) {
|
|
15
|
+
case 'null':
|
|
16
|
+
return 'z';
|
|
17
|
+
case 'boolean':
|
|
18
|
+
return v ? 't' : 'f';
|
|
19
|
+
case 'number': {
|
|
20
|
+
const n = toNumber(v);
|
|
21
|
+
return 'n' + (n === 0 ? '0' : String(n));
|
|
22
|
+
}
|
|
23
|
+
case 'string':
|
|
24
|
+
return 's' + JSON.stringify(v);
|
|
25
|
+
case 'list':
|
|
26
|
+
return nested(v, stack, () => '[' + Array.from(v, (x) => key(x, stack)).join(',') + ']');
|
|
27
|
+
case 'set':
|
|
28
|
+
if (v instanceof JawnSet) {
|
|
29
|
+
return '<' + [...v.keys_()].sort().join(',') + '>';
|
|
30
|
+
}
|
|
31
|
+
return nested(v, stack, () => '<' + [...new Set(Array.from(v, (x) => key(x, stack)))].sort().join(',') + '>');
|
|
32
|
+
case 'map':
|
|
33
|
+
return nested(v, stack, () => '{' + mapEntries(v).map(([k, x]) => JSON.stringify(k) + ':' + key(x, stack))
|
|
34
|
+
.sort().join(',') + '}');
|
|
35
|
+
}
|
|
36
|
+
throw new JawnTypeError(`Not a jawn value: ${typeName(v)}`);
|
|
37
|
+
}
|
|
38
|
+
/** Whether two values are equal as jawn values; false if either isn't one. */
|
|
39
|
+
export function jawnEquals(a, b) {
|
|
40
|
+
try {
|
|
41
|
+
return jawnKey(a) === jawnKey(b);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e instanceof JawnError) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Canonical forms of another collection's elements, for comparing by jawn
|
|
51
|
+
// rather than JavaScript equality. A non-jawn element gets a form of its
|
|
52
|
+
// own that equals no other; canonical forms never begin with \0.
|
|
53
|
+
let unique = 0;
|
|
54
|
+
function keysOf(o) {
|
|
55
|
+
if (o instanceof JawnSet) {
|
|
56
|
+
return new Set(o.keys_());
|
|
57
|
+
}
|
|
58
|
+
const ks = new Set();
|
|
59
|
+
for (const x of o) {
|
|
60
|
+
try {
|
|
61
|
+
ks.add(jawnKey(x));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (!(e instanceof JawnError)) {
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
ks.add(`\0${unique++}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return ks;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A set of jawn values compared by value and type.
|
|
74
|
+
*
|
|
75
|
+
* Unlike the builtin Set, elements are compared by their content rather
|
|
76
|
+
* than identity, so maps and lists may be elements, and booleans are
|
|
77
|
+
* distinct from numbers. Elements are compared by their content when
|
|
78
|
+
* added, so mutating one afterwards leaves the set inconsistent. Methods
|
|
79
|
+
* taking another collection compare by jawn value too, e.g.
|
|
80
|
+
* `new JawnSet([true]).isSubsetOf(new Set([1]))` is false.
|
|
81
|
+
*/
|
|
82
|
+
export class JawnSet {
|
|
83
|
+
items = new Map();
|
|
84
|
+
constructor(items) {
|
|
85
|
+
if (items) {
|
|
86
|
+
for (const x of items) {
|
|
87
|
+
this.add(x);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** @internal The canonical forms of the elements. */
|
|
92
|
+
keys_() {
|
|
93
|
+
return this.items.keys();
|
|
94
|
+
}
|
|
95
|
+
get size() {
|
|
96
|
+
return this.items.size;
|
|
97
|
+
}
|
|
98
|
+
get [Symbol.toStringTag]() {
|
|
99
|
+
return 'JawnSet';
|
|
100
|
+
}
|
|
101
|
+
/** Adds an element, which must be a jawn value. */
|
|
102
|
+
add(x) {
|
|
103
|
+
const k = jawnKey(x);
|
|
104
|
+
if (!this.items.has(k)) {
|
|
105
|
+
this.items.set(k, x);
|
|
106
|
+
}
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
/** Like an absent element, a non-jawn value is simply not present. */
|
|
110
|
+
has(x) {
|
|
111
|
+
const k = tryKey(x);
|
|
112
|
+
return k !== undefined && this.items.has(k);
|
|
113
|
+
}
|
|
114
|
+
delete(x) {
|
|
115
|
+
const k = tryKey(x);
|
|
116
|
+
return k !== undefined && this.items.delete(k);
|
|
117
|
+
}
|
|
118
|
+
clear() {
|
|
119
|
+
this.items.clear();
|
|
120
|
+
}
|
|
121
|
+
values() {
|
|
122
|
+
return this.items.values();
|
|
123
|
+
}
|
|
124
|
+
keys() {
|
|
125
|
+
return this.items.values();
|
|
126
|
+
}
|
|
127
|
+
*entries() {
|
|
128
|
+
for (const x of this.items.values()) {
|
|
129
|
+
yield [x, x];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
[Symbol.iterator]() {
|
|
133
|
+
return this.items.values();
|
|
134
|
+
}
|
|
135
|
+
forEach(f, thisArg) {
|
|
136
|
+
for (const x of this.items.values()) {
|
|
137
|
+
f.call(thisArg, x, x, this);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
copy() {
|
|
141
|
+
return new JawnSet(this);
|
|
142
|
+
}
|
|
143
|
+
/** Whether other is a set with the same elements, compared as jawns. */
|
|
144
|
+
equals(other) {
|
|
145
|
+
if (!(other instanceof JawnSet || other instanceof Set)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const ks = keysOf(other);
|
|
149
|
+
return ks.size === this.items.size && this.within(ks);
|
|
150
|
+
}
|
|
151
|
+
// Set operations, named as the builtin Set's, which accept any
|
|
152
|
+
// iterables and return new sets.
|
|
153
|
+
union(...others) {
|
|
154
|
+
const r = new JawnSet(this);
|
|
155
|
+
return r.update(...others);
|
|
156
|
+
}
|
|
157
|
+
intersection(...others) {
|
|
158
|
+
return this.copy().intersectionUpdate(...others);
|
|
159
|
+
}
|
|
160
|
+
difference(...others) {
|
|
161
|
+
return this.copy().differenceUpdate(...others);
|
|
162
|
+
}
|
|
163
|
+
symmetricDifference(other) {
|
|
164
|
+
return new JawnSet(this).symmetricDifferenceUpdate(other);
|
|
165
|
+
}
|
|
166
|
+
isSubsetOf(other) {
|
|
167
|
+
return this.within(keysOf(other));
|
|
168
|
+
}
|
|
169
|
+
isSupersetOf(other) {
|
|
170
|
+
for (const k of keysOf(other)) {
|
|
171
|
+
if (!this.items.has(k)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
isDisjointFrom(other) {
|
|
178
|
+
for (const k of keysOf(other)) {
|
|
179
|
+
if (this.items.has(k)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
// In-place counterparts, which return this set.
|
|
186
|
+
update(...others) {
|
|
187
|
+
for (const o of others) {
|
|
188
|
+
for (const x of o) {
|
|
189
|
+
this.add(x);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
intersectionUpdate(...others) {
|
|
195
|
+
for (const o of others) {
|
|
196
|
+
const keep = keysOf(o);
|
|
197
|
+
for (const k of [...this.items.keys()]) {
|
|
198
|
+
if (!keep.has(k)) {
|
|
199
|
+
this.items.delete(k);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
differenceUpdate(...others) {
|
|
206
|
+
for (const o of others) {
|
|
207
|
+
for (const x of o) {
|
|
208
|
+
this.delete(x);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
symmetricDifferenceUpdate(other) {
|
|
214
|
+
for (const x of new JawnSet(other)) {
|
|
215
|
+
if (this.has(x)) {
|
|
216
|
+
this.delete(x);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.add(x);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
within(ks) {
|
|
225
|
+
for (const k of this.items.keys()) {
|
|
226
|
+
if (!ks.has(k)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
/** The elements as an array, so that JSON.stringify writes sets as lists. */
|
|
233
|
+
toJSON() {
|
|
234
|
+
return [...this.items.values()];
|
|
235
|
+
}
|
|
236
|
+
[Symbol.for('nodejs.util.inspect.custom')](_depth, options, inspect) {
|
|
237
|
+
return `JawnSet(${inspect([...this.items.values()], options)})`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function tryKey(x) {
|
|
241
|
+
try {
|
|
242
|
+
return jawnKey(x);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
if (e instanceof JawnError) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
throw e;
|
|
249
|
+
}
|
|
250
|
+
}
|