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/dist/lexer.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A terminal as written in jawn.g: alternatives of items, each a regex
|
|
3
|
+
* literal (`/.../flags`), a string literal (`"..."`), or the name of
|
|
4
|
+
* another terminal. These are transcribed verbatim from the grammar, and
|
|
5
|
+
* a test checks that they still match it.
|
|
6
|
+
*/
|
|
7
|
+
export interface TerminalDef {
|
|
8
|
+
priority?: number;
|
|
9
|
+
alts: string[][];
|
|
10
|
+
}
|
|
11
|
+
export declare const TERMINALS: Record<string, TerminalDef>;
|
|
12
|
+
/** Terminals written as string literals within the grammar's rules. */
|
|
13
|
+
export declare const ANONYMOUS: Record<string, string>;
|
|
14
|
+
/** Terminals skipped between tokens. */
|
|
15
|
+
export declare const IGNORE: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* The terminals the lexer produces, in the order Lark tries them: by
|
|
18
|
+
* descending priority, then (for those of equal priority, which never
|
|
19
|
+
* overlap) by descending maximum match length.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ORDER: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* The JavaScript regex source for a terminal, composed as Lark does:
|
|
24
|
+
* items are concatenated and alternatives grouped. Regex flags are
|
|
25
|
+
* applied to every terminal, which is equivalent as an unescaped `.` only
|
|
26
|
+
* occurs in the grammar's `/s` patterns.
|
|
27
|
+
*/
|
|
28
|
+
export declare function terminalSource(name: string): string;
|
|
29
|
+
export declare const FLAGS = "suy";
|
|
30
|
+
export interface Token {
|
|
31
|
+
type: string;
|
|
32
|
+
value: string;
|
|
33
|
+
start: number;
|
|
34
|
+
line: number;
|
|
35
|
+
column: number;
|
|
36
|
+
}
|
|
37
|
+
/** No terminal the parser accepts matches here, but this other one does. */
|
|
38
|
+
export declare class UnexpectedToken {
|
|
39
|
+
readonly token: Token;
|
|
40
|
+
constructor(token: Token);
|
|
41
|
+
}
|
|
42
|
+
/** No terminal matches here at all. */
|
|
43
|
+
export declare class UnexpectedCharacters {
|
|
44
|
+
readonly pos: number;
|
|
45
|
+
readonly line: number;
|
|
46
|
+
readonly column: number;
|
|
47
|
+
constructor(pos: number, line: number, column: number);
|
|
48
|
+
}
|
|
49
|
+
/** A set of terminal names, plus `$END` where the end of input is accepted. */
|
|
50
|
+
export type Context = ReadonlySet<string>;
|
|
51
|
+
export declare class Lexer {
|
|
52
|
+
readonly text: string;
|
|
53
|
+
private pos;
|
|
54
|
+
private line;
|
|
55
|
+
private column;
|
|
56
|
+
private last;
|
|
57
|
+
constructor(text: string);
|
|
58
|
+
/** The next token, which must be of a type in accept. */
|
|
59
|
+
next(accept: Context): Token;
|
|
60
|
+
private match;
|
|
61
|
+
private advance;
|
|
62
|
+
}
|
package/dist/lexer.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// A contextual lexer for jawn text, equivalent to the one Lark builds
|
|
2
|
+
// from src/jawn.g for its LALR parser: at each point only the terminals
|
|
3
|
+
// the parser can accept there are considered, tried in priority order,
|
|
4
|
+
// and the first to match wins.
|
|
5
|
+
const R = String.raw;
|
|
6
|
+
export const TERMINALS = {
|
|
7
|
+
_EMPTYSET: { priority: 15, alts: [[R `/_/`, '_VALUE_END']] },
|
|
8
|
+
_KEY_FOLLOWS: { alts: [[R `/(?=[ \t\f\r\n]*(?::(?!\/\/)|=))/`]] },
|
|
9
|
+
KEY_QUOT: { priority: 25, alts: [['_QUOTED', R `/(?=[ \t\f\r\n]*[:=])/`]] },
|
|
10
|
+
KEY_BARE: {
|
|
11
|
+
priority: 35,
|
|
12
|
+
alts: [[R `/(?!(?:[_+-]|[Nn][Uu][Ll][Ll]|[Nn][Ii][Ll]|[Nn][Oo][Nn][Ee]|[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])(?![^\x00-\x20\x7f-\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000"'{}\[\],:=]))[^\x00-\x20\x7f-\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000"'{}\[\],:=]+/`,
|
|
13
|
+
'_KEY_FOLLOWS']],
|
|
14
|
+
},
|
|
15
|
+
TEXT: {
|
|
16
|
+
priority: 40,
|
|
17
|
+
alts: [
|
|
18
|
+
[R `/"""(?:[^"\\]|\\.|"(?!""))*"{0,2}"""/s`, '_NO_ASGN', '_VALUE_END'],
|
|
19
|
+
[R `/'''(?:[^'\\]|\\.|'(?!''))*'{0,2}'''/s`, '_NO_ASGN', '_VALUE_END'],
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
_NO_ASGN: { alts: [[R `/(?![ \t\f\r\n]*[:=])/`]] },
|
|
23
|
+
QUOT_STR: { priority: 20, alts: [['_QUOTED', '_NO_ASGN', '_VALUE_END']] },
|
|
24
|
+
BARE_STR: {
|
|
25
|
+
priority: 10,
|
|
26
|
+
alts: [[R `/(?!(?:[_+-]|[:=]+)(?![^\x00-\x20\x7f-\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000"'{}\[\],]))[^\x00-\x20\x7f-\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000"'{}\[\],]+(?<!:)/`,
|
|
27
|
+
'_VALUE_END']],
|
|
28
|
+
},
|
|
29
|
+
_VALUE_END: { alts: [[R `/(?![^ \t\f\r\n,\]}])/`]] },
|
|
30
|
+
NUMBER: { priority: 30, alts: [[R `/[+-]?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?/`, '_VALUE_END']] },
|
|
31
|
+
NULL: { priority: 50, alts: [[R `/(?:[Nn][Uu][Ll][Ll]|[Nn][Ii][Ll]|[Nn][Oo][Nn][Ee])/`, '_VALUE_END']] },
|
|
32
|
+
TRUE: { priority: 60, alts: [[R `/[Tt][Rr][Uu][Ee]/`, '_VALUE_END']] },
|
|
33
|
+
FALSE: { priority: 70, alts: [[R `/[Ff][Aa][Ll][Ss][Ee]/`, '_VALUE_END']] },
|
|
34
|
+
_RSQB: { alts: [[R `/\]/`, '_VALUE_END']] },
|
|
35
|
+
_RBRACE: { alts: [[R `/\}/`, '_VALUE_END']] },
|
|
36
|
+
_QUOTED: { alts: [[R `/"(?:[^"\\]|\\.)*"/s`], [R `/'(?:[^'\\]|\\.)*'/s`]] },
|
|
37
|
+
COMMENT: { priority: 80, alts: [[R `/(?:(?<=[ \t\f\r\n,\[{])|^)\/\/[^\n]*/`]] },
|
|
38
|
+
_ASGN: { alts: [['":"'], ['"="']] },
|
|
39
|
+
_SEP: { alts: [['","']] },
|
|
40
|
+
WS: { alts: [[R `/[ \t\f\r\n]+/`]] },
|
|
41
|
+
};
|
|
42
|
+
/** Terminals written as string literals within the grammar's rules. */
|
|
43
|
+
export const ANONYMOUS = { LBRACE: '{', LSQB: '[' };
|
|
44
|
+
/** Terminals skipped between tokens. */
|
|
45
|
+
export const IGNORE = ['WS', 'COMMENT'];
|
|
46
|
+
/**
|
|
47
|
+
* The terminals the lexer produces, in the order Lark tries them: by
|
|
48
|
+
* descending priority, then (for those of equal priority, which never
|
|
49
|
+
* overlap) by descending maximum match length.
|
|
50
|
+
*/
|
|
51
|
+
export const ORDER = [
|
|
52
|
+
'COMMENT', 'FALSE', 'TRUE', 'NULL', 'TEXT', 'KEY_BARE', 'NUMBER', 'KEY_QUOT', 'QUOT_STR',
|
|
53
|
+
'_EMPTYSET', 'BARE_STR', 'WS', '_RBRACE', '_RSQB', '_ASGN', 'LBRACE', 'LSQB', '_SEP',
|
|
54
|
+
];
|
|
55
|
+
function escapeRegExp(s) {
|
|
56
|
+
return s.replace(/[\\^$.*+?()[\]{}|\/]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The JavaScript regex source for a terminal, composed as Lark does:
|
|
60
|
+
* items are concatenated and alternatives grouped. Regex flags are
|
|
61
|
+
* applied to every terminal, which is equivalent as an unescaped `.` only
|
|
62
|
+
* occurs in the grammar's `/s` patterns.
|
|
63
|
+
*/
|
|
64
|
+
export function terminalSource(name) {
|
|
65
|
+
if (name in ANONYMOUS) {
|
|
66
|
+
return escapeRegExp(ANONYMOUS[name]);
|
|
67
|
+
}
|
|
68
|
+
const def = TERMINALS[name];
|
|
69
|
+
if (!def) {
|
|
70
|
+
throw new Error(`Unknown terminal ${name}`);
|
|
71
|
+
}
|
|
72
|
+
const alts = def.alts.map((items) => items.map((item) => {
|
|
73
|
+
if (item.startsWith('/')) {
|
|
74
|
+
const end = item.lastIndexOf('/');
|
|
75
|
+
const body = item.slice(1, end);
|
|
76
|
+
return end < item.length - 1 ? `(?:${body})` : body;
|
|
77
|
+
}
|
|
78
|
+
if (item.startsWith('"')) {
|
|
79
|
+
return escapeRegExp(JSON.parse(item));
|
|
80
|
+
}
|
|
81
|
+
return terminalSource(item);
|
|
82
|
+
}).join(''));
|
|
83
|
+
return alts.length === 1 ? alts[0] : `(?:${alts.join('|')})`;
|
|
84
|
+
}
|
|
85
|
+
export const FLAGS = 'suy';
|
|
86
|
+
const PATTERNS = new Map(ORDER.map((name) => [name, new RegExp(terminalSource(name), FLAGS)]));
|
|
87
|
+
/** No terminal the parser accepts matches here, but this other one does. */
|
|
88
|
+
export class UnexpectedToken {
|
|
89
|
+
token;
|
|
90
|
+
constructor(token) {
|
|
91
|
+
this.token = token;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** No terminal matches here at all. */
|
|
95
|
+
export class UnexpectedCharacters {
|
|
96
|
+
pos;
|
|
97
|
+
line;
|
|
98
|
+
column;
|
|
99
|
+
constructor(pos, line, column) {
|
|
100
|
+
this.pos = pos;
|
|
101
|
+
this.line = line;
|
|
102
|
+
this.column = column;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// The candidate patterns for each context, in matching order.
|
|
106
|
+
const candidates = new WeakMap();
|
|
107
|
+
function candidatesFor(accept) {
|
|
108
|
+
let c = candidates.get(accept);
|
|
109
|
+
if (!c) {
|
|
110
|
+
c = [...PATTERNS].filter(([name]) => accept.has(name) || IGNORE.includes(name));
|
|
111
|
+
candidates.set(accept, c);
|
|
112
|
+
}
|
|
113
|
+
return c;
|
|
114
|
+
}
|
|
115
|
+
const ROOT = new Set(ORDER);
|
|
116
|
+
// The number of codepoints in s[from:to].
|
|
117
|
+
function codepoints(s, from = 0, to = s.length) {
|
|
118
|
+
let n = 0;
|
|
119
|
+
for (let i = from; i < to; i++) {
|
|
120
|
+
const c = s.charCodeAt(i);
|
|
121
|
+
if (!(c >= 0xdc00 && c <= 0xdfff && i > from && isHigh(s.charCodeAt(i - 1)))) {
|
|
122
|
+
n++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
127
|
+
function isHigh(c) {
|
|
128
|
+
return c >= 0xd800 && c <= 0xdbff;
|
|
129
|
+
}
|
|
130
|
+
export class Lexer {
|
|
131
|
+
text;
|
|
132
|
+
pos = 0;
|
|
133
|
+
// Lines and columns count from 1, columns by codepoint.
|
|
134
|
+
line = 1;
|
|
135
|
+
column = 1;
|
|
136
|
+
last;
|
|
137
|
+
constructor(text) {
|
|
138
|
+
this.text = text;
|
|
139
|
+
}
|
|
140
|
+
/** The next token, which must be of a type in accept. */
|
|
141
|
+
next(accept) {
|
|
142
|
+
for (;;) {
|
|
143
|
+
if (this.pos >= this.text.length) {
|
|
144
|
+
// The end of input is located at the last token, as in Lark.
|
|
145
|
+
const t = this.last;
|
|
146
|
+
const end = { type: '$END', value: '', start: t?.start ?? 0, line: t?.line ?? 1,
|
|
147
|
+
column: t?.column ?? 1 };
|
|
148
|
+
if (!accept.has('$END')) {
|
|
149
|
+
throw new UnexpectedToken(end);
|
|
150
|
+
}
|
|
151
|
+
return end;
|
|
152
|
+
}
|
|
153
|
+
const t = this.match(candidatesFor(accept));
|
|
154
|
+
if (!t) {
|
|
155
|
+
// Matching against every terminal gives a more useful error.
|
|
156
|
+
const other = this.match(candidatesFor(ROOT));
|
|
157
|
+
if (other) {
|
|
158
|
+
throw new UnexpectedToken(other);
|
|
159
|
+
}
|
|
160
|
+
throw new UnexpectedCharacters(this.pos, this.line, this.column);
|
|
161
|
+
}
|
|
162
|
+
this.advance(t.value);
|
|
163
|
+
if (!IGNORE.includes(t.type)) {
|
|
164
|
+
this.last = t;
|
|
165
|
+
return t;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
match(candidates) {
|
|
170
|
+
for (const [type, re] of candidates) {
|
|
171
|
+
re.lastIndex = this.pos;
|
|
172
|
+
const m = re.exec(this.text);
|
|
173
|
+
if (m) {
|
|
174
|
+
return { type, value: m[0], start: this.pos, line: this.line, column: this.column };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
advance(value) {
|
|
180
|
+
const nl = value.lastIndexOf('\n');
|
|
181
|
+
if (nl >= 0) {
|
|
182
|
+
this.line += value.split('\n').length - 1;
|
|
183
|
+
this.column = 1 + codepoints(value, nl + 1);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.column += codepoints(value);
|
|
187
|
+
}
|
|
188
|
+
this.pos += value.length;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Jawn } from './types.js';
|
|
2
|
+
/** Parse a jawn, which is a map. */
|
|
3
|
+
export declare function parse(text: string): Jawn;
|
|
4
|
+
/** Parse a list of jawns, a square-bracketed list of maps. */
|
|
5
|
+
export declare function parseList(text: string): Jawn[];
|
|
6
|
+
/** A string as Python's repr() writes it. */
|
|
7
|
+
export declare function pyRepr(s: string): string;
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { JawnError } from './errors.js';
|
|
2
|
+
import { JawnSet } from './jawnset.js';
|
|
3
|
+
import { Lexer, UnexpectedCharacters, UnexpectedToken } from './lexer.js';
|
|
4
|
+
// The contexts below are the sets of terminals accepted by the states of
|
|
5
|
+
// the LALR parser Lark builds from jawn.g, which the contextual lexer
|
|
6
|
+
// considers. Where LALR merges states their sets are unions, e.g. after
|
|
7
|
+
// a list element a `}` is lexed, and then rejected by the parser.
|
|
8
|
+
const KEYS = ['KEY_QUOT', 'KEY_BARE'];
|
|
9
|
+
const VALUES = ['NULL', 'TRUE', 'FALSE', 'NUMBER', 'QUOT_STR', 'BARE_STR', 'TEXT', 'LSQB', 'LBRACE'];
|
|
10
|
+
const IS_VALUE = new Set(VALUES);
|
|
11
|
+
function ctx(...names) {
|
|
12
|
+
return new Set(names.flat());
|
|
13
|
+
}
|
|
14
|
+
const START = ctx(KEYS, 'LBRACE', 'LSQB', '$END');
|
|
15
|
+
const AFTER_KEY = ctx('_ASGN');
|
|
16
|
+
const MAP_VALUE = ctx(VALUES);
|
|
17
|
+
// After a map's opening bracket, where it can't be a set.
|
|
18
|
+
const MAP_OPEN = ctx(KEYS, '_RBRACE');
|
|
19
|
+
// After a map value, whether in a bare or bracketed map, and after its separator.
|
|
20
|
+
const AFTER_VALUE = ctx(KEYS, '_SEP', '_RBRACE', '$END');
|
|
21
|
+
const MAP_NEXT = ctx(KEYS, '_RBRACE', '$END');
|
|
22
|
+
// After an opening bracket that may be a map or a set.
|
|
23
|
+
const BRACE_OPEN = ctx(KEYS, VALUES, '_EMPTYSET', '_RBRACE');
|
|
24
|
+
// After a list or set element, and after their separators.
|
|
25
|
+
const AFTER_ELEM = ctx(VALUES, '_SEP', '_RBRACE', '_RSQB');
|
|
26
|
+
const LIST_NEXT = ctx(VALUES, '_RSQB');
|
|
27
|
+
const SET_NEXT = ctx(VALUES, '_RBRACE');
|
|
28
|
+
const AFTER_EMPTYSET = ctx('_SEP', '_RBRACE');
|
|
29
|
+
const SET_CLOSE = ctx('_RBRACE');
|
|
30
|
+
// Within a list of jawns.
|
|
31
|
+
const JAWNS_NEXT = ctx('LBRACE', '_RSQB');
|
|
32
|
+
const AFTER_JAWN = ctx('LBRACE', '_SEP', '_RSQB');
|
|
33
|
+
const END = ctx('$END');
|
|
34
|
+
/** An error found in converting a value, reported at its token. */
|
|
35
|
+
class SourceError {
|
|
36
|
+
msg;
|
|
37
|
+
token;
|
|
38
|
+
constructor(msg, token) {
|
|
39
|
+
this.msg = msg;
|
|
40
|
+
this.token = token;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Whitespace per the spec; other Unicode whitespace is not whitespace.
|
|
44
|
+
const WS = ' \t\f\r\n';
|
|
45
|
+
function lstrip(s) {
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < s.length && WS.includes(s[i])) {
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
return s.slice(i);
|
|
51
|
+
}
|
|
52
|
+
function isBlank(s) {
|
|
53
|
+
return lstrip(s) === '';
|
|
54
|
+
}
|
|
55
|
+
// Escapes per the spec, those of JSON less \b plus \' and \U; a
|
|
56
|
+
// backslash beginning anything else is kept.
|
|
57
|
+
// A \u surrogate pair is combined into one codepoint.
|
|
58
|
+
const ESCAPE = /\\(?:u([dD][89abAB][0-9a-fA-F]{2})\\u([dD][c-fC-F][0-9a-fA-F]{2})|u([0-9a-fA-F]{4})|U([0-9a-fA-F]{8})|(.))/gsu;
|
|
59
|
+
const SIMPLE = new Map([['n', '\n'], ['t', '\t'], ['r', '\r'], ['f', '\f'],
|
|
60
|
+
['"', '"'], ["'", "'"], ['\\', '\\'], ['/', '/']]);
|
|
61
|
+
function unescape(s) {
|
|
62
|
+
return s.replace(ESCAPE, (m, hi, lo, u4, u8, c) => {
|
|
63
|
+
if (hi) {
|
|
64
|
+
return String.fromCodePoint(0x10000 + ((parseInt(hi, 16) - 0xd800) << 10)
|
|
65
|
+
+ (parseInt(lo, 16) - 0xdc00));
|
|
66
|
+
}
|
|
67
|
+
if (u4 || u8) {
|
|
68
|
+
// Out of range codepoints and lone surrogates are kept as written.
|
|
69
|
+
const cp = parseInt((u4 || u8), 16);
|
|
70
|
+
return cp <= 0x10ffff && !(cp >= 0xd800 && cp <= 0xdfff) ? String.fromCodePoint(cp) : m;
|
|
71
|
+
}
|
|
72
|
+
return SIMPLE.get(c) ?? m;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// The content of a quoted string or key. CRLF linebreaks are
|
|
76
|
+
// normalized, as in texts, before escapes so that an escaped `\r\n`
|
|
77
|
+
// survives.
|
|
78
|
+
function quoted(s) {
|
|
79
|
+
return unescape(s.slice(1, -1).replaceAll('\r\n', '\n'));
|
|
80
|
+
}
|
|
81
|
+
function text(s) {
|
|
82
|
+
// CRLF linebreaks are normalized so that CRs don't trail lines.
|
|
83
|
+
const lines = s.slice(3, -3).replaceAll('\r\n', '\n').split('\n');
|
|
84
|
+
// Content on the opening line is only stripped of its leading
|
|
85
|
+
// whitespace and doesn't count toward the common indentation.
|
|
86
|
+
const head = [];
|
|
87
|
+
if (!isBlank(lines[0])) {
|
|
88
|
+
head.push(lstrip(lines.shift()));
|
|
89
|
+
}
|
|
90
|
+
while (lines.length > 0 && head.length === 0 && isBlank(lines[0])) {
|
|
91
|
+
lines.shift();
|
|
92
|
+
}
|
|
93
|
+
while (lines.length > 0 && isBlank(lines[lines.length - 1])) {
|
|
94
|
+
lines.pop();
|
|
95
|
+
}
|
|
96
|
+
// Remove the leading whitespace common to all non-blank lines;
|
|
97
|
+
// blank lines don't count toward it and become empty.
|
|
98
|
+
const indents = lines.filter((l) => !isBlank(l)).map((l) => l.slice(0, l.length - lstrip(l).length));
|
|
99
|
+
const prefix = commonPrefix(indents);
|
|
100
|
+
const res = head.concat(lines.map((l) => isBlank(l) ? '' : l.slice(prefix.length)));
|
|
101
|
+
// Dedent before unescaping so escaped linebreaks don't affect it.
|
|
102
|
+
return unescape(res.join('\n'));
|
|
103
|
+
}
|
|
104
|
+
function commonPrefix(ss) {
|
|
105
|
+
if (ss.length === 0) {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
let p = ss[0];
|
|
109
|
+
for (const s of ss) {
|
|
110
|
+
let i = 0;
|
|
111
|
+
while (i < p.length && i < s.length && p[i] === s[i]) {
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
p = p.slice(0, i);
|
|
115
|
+
}
|
|
116
|
+
return p;
|
|
117
|
+
}
|
|
118
|
+
function setKey(o, k, v) {
|
|
119
|
+
if (k === '__proto__') {
|
|
120
|
+
Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
o[k] = v;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
class Parser {
|
|
127
|
+
lexer;
|
|
128
|
+
// Checks made as the LALR parser reduces a value, which it does only
|
|
129
|
+
// once it has lexed the token following that value; a syntax error
|
|
130
|
+
// there takes precedence.
|
|
131
|
+
pending = [];
|
|
132
|
+
constructor(text) {
|
|
133
|
+
this.lexer = new Lexer(text);
|
|
134
|
+
}
|
|
135
|
+
next(accept) {
|
|
136
|
+
const t = this.lexer.next(accept);
|
|
137
|
+
this.flush();
|
|
138
|
+
return t;
|
|
139
|
+
}
|
|
140
|
+
flush() {
|
|
141
|
+
const pending = this.pending;
|
|
142
|
+
this.pending = [];
|
|
143
|
+
for (const check of pending) {
|
|
144
|
+
check();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A jawn or a list of jawns, as the grammar's start rule permits both. */
|
|
148
|
+
parse() {
|
|
149
|
+
const t = this.next(START);
|
|
150
|
+
if (t.type === 'LBRACE') {
|
|
151
|
+
const m = this.map(this.next(MAP_OPEN), '_RBRACE');
|
|
152
|
+
this.next(END);
|
|
153
|
+
return m;
|
|
154
|
+
}
|
|
155
|
+
if (t.type === 'LSQB') {
|
|
156
|
+
const l = this.jawns();
|
|
157
|
+
this.next(END);
|
|
158
|
+
return l;
|
|
159
|
+
}
|
|
160
|
+
const m = this.map(t, '$END');
|
|
161
|
+
this.flush();
|
|
162
|
+
return m;
|
|
163
|
+
}
|
|
164
|
+
jawns() {
|
|
165
|
+
const items = [];
|
|
166
|
+
let t = this.next(JAWNS_NEXT);
|
|
167
|
+
while (t.type !== '_RSQB') {
|
|
168
|
+
if (t.type !== 'LBRACE') {
|
|
169
|
+
throw new UnexpectedToken(t);
|
|
170
|
+
}
|
|
171
|
+
items.push(this.map(this.next(MAP_OPEN), '_RBRACE'));
|
|
172
|
+
t = this.next(AFTER_JAWN);
|
|
173
|
+
if (t.type === '_SEP') {
|
|
174
|
+
t = this.next(JAWNS_NEXT);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return items;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A map's pairs, from its first token through its closing one. Like
|
|
181
|
+
* other values, it's checked once the token following it is lexed.
|
|
182
|
+
*/
|
|
183
|
+
map(t, close) {
|
|
184
|
+
const o = {};
|
|
185
|
+
let duplicate;
|
|
186
|
+
let duplicateKey = '';
|
|
187
|
+
while (t.type !== close) {
|
|
188
|
+
if (!KEYS.includes(t.type)) {
|
|
189
|
+
throw new UnexpectedToken(t);
|
|
190
|
+
}
|
|
191
|
+
const kt = t;
|
|
192
|
+
const k = t.type === 'KEY_BARE' ? t.value : quoted(t.value);
|
|
193
|
+
this.next(AFTER_KEY);
|
|
194
|
+
let v;
|
|
195
|
+
[v, t] = this.value(this.next(MAP_VALUE), AFTER_VALUE);
|
|
196
|
+
if (!Object.hasOwn(o, k)) {
|
|
197
|
+
setKey(o, k, v);
|
|
198
|
+
}
|
|
199
|
+
else if (!duplicate) {
|
|
200
|
+
duplicate = kt;
|
|
201
|
+
duplicateKey = k;
|
|
202
|
+
}
|
|
203
|
+
if (t.type === '_SEP') {
|
|
204
|
+
t = this.next(MAP_NEXT);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (duplicate) {
|
|
208
|
+
const d = duplicate;
|
|
209
|
+
this.pending.push(() => {
|
|
210
|
+
throw new SourceError(`Duplicate key ${pyRepr(duplicateKey)}`, d);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return o;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* A value from its first token, along with the token following it,
|
|
217
|
+
* which is lexed in context follow.
|
|
218
|
+
*/
|
|
219
|
+
value(t, follow) {
|
|
220
|
+
let v;
|
|
221
|
+
switch (t.type) {
|
|
222
|
+
case 'NULL':
|
|
223
|
+
v = null;
|
|
224
|
+
break;
|
|
225
|
+
case 'TRUE':
|
|
226
|
+
v = true;
|
|
227
|
+
break;
|
|
228
|
+
case 'FALSE':
|
|
229
|
+
v = false;
|
|
230
|
+
break;
|
|
231
|
+
case 'NUMBER':
|
|
232
|
+
v = Number(t.value);
|
|
233
|
+
if (!Number.isFinite(v)) {
|
|
234
|
+
this.pending.push(() => {
|
|
235
|
+
throw new SourceError(`Number ${t.value} out of range`, t);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case 'QUOT_STR':
|
|
240
|
+
v = quoted(t.value);
|
|
241
|
+
break;
|
|
242
|
+
case 'BARE_STR':
|
|
243
|
+
v = t.value;
|
|
244
|
+
break;
|
|
245
|
+
case 'TEXT':
|
|
246
|
+
v = text(t.value);
|
|
247
|
+
break;
|
|
248
|
+
case 'LSQB':
|
|
249
|
+
v = this.list();
|
|
250
|
+
break;
|
|
251
|
+
case 'LBRACE':
|
|
252
|
+
v = this.brace();
|
|
253
|
+
break;
|
|
254
|
+
default:
|
|
255
|
+
throw new UnexpectedToken(t);
|
|
256
|
+
}
|
|
257
|
+
return [v, this.next(follow)];
|
|
258
|
+
}
|
|
259
|
+
list() {
|
|
260
|
+
const items = [];
|
|
261
|
+
let t = this.next(LIST_NEXT);
|
|
262
|
+
while (t.type !== '_RSQB') {
|
|
263
|
+
if (!IS_VALUE.has(t.type)) {
|
|
264
|
+
throw new UnexpectedToken(t);
|
|
265
|
+
}
|
|
266
|
+
let v;
|
|
267
|
+
[v, t] = this.value(t, AFTER_ELEM);
|
|
268
|
+
items.push(v);
|
|
269
|
+
if (t.type === '_SEP') {
|
|
270
|
+
t = this.next(LIST_NEXT);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return items;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* A map or set: a map if its first element is a key, and otherwise a
|
|
277
|
+
* set.
|
|
278
|
+
*/
|
|
279
|
+
brace() {
|
|
280
|
+
let t = this.next(BRACE_OPEN);
|
|
281
|
+
if (t.type === '_RBRACE' || KEYS.includes(t.type)) {
|
|
282
|
+
return this.map(t, '_RBRACE');
|
|
283
|
+
}
|
|
284
|
+
const s = new JawnSet();
|
|
285
|
+
if (t.type === '_EMPTYSET') {
|
|
286
|
+
t = this.next(AFTER_EMPTYSET);
|
|
287
|
+
if (t.type === '_SEP') {
|
|
288
|
+
this.next(SET_CLOSE);
|
|
289
|
+
}
|
|
290
|
+
return s;
|
|
291
|
+
}
|
|
292
|
+
while (t.type !== '_RBRACE') {
|
|
293
|
+
if (!IS_VALUE.has(t.type)) {
|
|
294
|
+
throw new UnexpectedToken(t);
|
|
295
|
+
}
|
|
296
|
+
let v;
|
|
297
|
+
[v, t] = this.value(t, AFTER_ELEM);
|
|
298
|
+
s.add(v);
|
|
299
|
+
if (t.type === '_SEP') {
|
|
300
|
+
t = this.next(SET_NEXT);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return s;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function parseText(s) {
|
|
307
|
+
// A leading byte order mark, as some editors write, is ignored.
|
|
308
|
+
if (s.startsWith('\ufeff')) {
|
|
309
|
+
s = s.slice(1);
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
return new Parser(s).parse();
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
if (e instanceof UnexpectedToken || e instanceof UnexpectedCharacters) {
|
|
316
|
+
const pos = e instanceof UnexpectedToken ? e.token.start : e.pos;
|
|
317
|
+
throw new JawnError(`Unexpected ${describe(e, s)}\n${context(s, pos)}`);
|
|
318
|
+
}
|
|
319
|
+
if (e instanceof SourceError) {
|
|
320
|
+
const t = e.token;
|
|
321
|
+
throw new JawnError(`${e.msg} on line ${t.line}, column ${t.column}\n${context(s, t.start)}`);
|
|
322
|
+
}
|
|
323
|
+
throw e;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** Parse a jawn, which is a map. */
|
|
327
|
+
export function parse(text) {
|
|
328
|
+
const r = parseText(text);
|
|
329
|
+
if (Array.isArray(r)) {
|
|
330
|
+
throw new JawnError('A jawn is a map, not a list; use parseList to read a list of jawns');
|
|
331
|
+
}
|
|
332
|
+
return r;
|
|
333
|
+
}
|
|
334
|
+
/** Parse a list of jawns, a square-bracketed list of maps. */
|
|
335
|
+
export function parseList(text) {
|
|
336
|
+
const r = parseText(text);
|
|
337
|
+
if (!Array.isArray(r)) {
|
|
338
|
+
throw new JawnError('Expected a list of jawns, a square-bracketed list of maps');
|
|
339
|
+
}
|
|
340
|
+
return r;
|
|
341
|
+
}
|
|
342
|
+
// Error messages, which match those of the Python implementation.
|
|
343
|
+
// A run of characters up to the next separator, for describing values
|
|
344
|
+
// that aren't separated from what follows them.
|
|
345
|
+
const RUN = /.[^ \t\f\r\n,\]}]*/suy;
|
|
346
|
+
function describe(e, s) {
|
|
347
|
+
if (e instanceof UnexpectedCharacters) {
|
|
348
|
+
// A run with a quote or bracket past its start, or a closing
|
|
349
|
+
// bracket at its start, is values missing a separator.
|
|
350
|
+
RUN.lastIndex = e.pos;
|
|
351
|
+
const run = [...RUN.exec(s)[0]];
|
|
352
|
+
if (run.length > 1 && (']}'.includes(run[0]) || run.slice(1).some((c) => `"'{[`.includes(c)))) {
|
|
353
|
+
return `${pyRepr(run.join(''))} on line ${e.line}, column ${e.column};`
|
|
354
|
+
+ ' values must be separated by whitespace or a comma';
|
|
355
|
+
}
|
|
356
|
+
return `character ${pyRepr(run[0])} on line ${e.line}, column ${e.column}`;
|
|
357
|
+
}
|
|
358
|
+
const t = e.token;
|
|
359
|
+
return `${describeToken(t)} on line ${t.line}, column ${t.column}`;
|
|
360
|
+
}
|
|
361
|
+
function describeToken(t) {
|
|
362
|
+
if (t.type === '$END') {
|
|
363
|
+
return 'end of input';
|
|
364
|
+
}
|
|
365
|
+
if (KEYS.includes(t.type)) {
|
|
366
|
+
return `key ${pyRepr(t.value)}`;
|
|
367
|
+
}
|
|
368
|
+
return pyRepr(t.value);
|
|
369
|
+
}
|
|
370
|
+
// The line around a position with a caret beneath it, as Lark does,
|
|
371
|
+
// spanning 40 codepoints either side.
|
|
372
|
+
function context(s, pos, span = 40) {
|
|
373
|
+
const before = s.slice(back(s, pos, span), pos).split('\n').pop();
|
|
374
|
+
const after = s.slice(pos, forward(s, pos, span)).split('\n', 1)[0];
|
|
375
|
+
return before + after + '\n' + ' '.repeat(expandedLength(before)) + '^';
|
|
376
|
+
}
|
|
377
|
+
function back(s, pos, n) {
|
|
378
|
+
for (; n > 0 && pos > 0; n--) {
|
|
379
|
+
pos -= pos > 1 && isLow(s.charCodeAt(pos - 1)) && isHigh(s.charCodeAt(pos - 2)) ? 2 : 1;
|
|
380
|
+
}
|
|
381
|
+
return pos;
|
|
382
|
+
}
|
|
383
|
+
function forward(s, pos, n) {
|
|
384
|
+
for (; n > 0 && pos < s.length; n--) {
|
|
385
|
+
pos += isHigh(s.charCodeAt(pos)) && isLow(s.charCodeAt(pos + 1)) ? 2 : 1;
|
|
386
|
+
}
|
|
387
|
+
return pos;
|
|
388
|
+
}
|
|
389
|
+
function isHigh(c) {
|
|
390
|
+
return c >= 0xd800 && c <= 0xdbff;
|
|
391
|
+
}
|
|
392
|
+
function isLow(c) {
|
|
393
|
+
return c >= 0xdc00 && c <= 0xdfff;
|
|
394
|
+
}
|
|
395
|
+
// The length of s with tabs expanded to multiples of 8 columns, as
|
|
396
|
+
// Python's str.expandtabs().
|
|
397
|
+
function expandedLength(s) {
|
|
398
|
+
let length = 0;
|
|
399
|
+
let column = 0;
|
|
400
|
+
for (const c of s) {
|
|
401
|
+
if (c === '\t') {
|
|
402
|
+
const n = 8 - (column % 8);
|
|
403
|
+
length += n;
|
|
404
|
+
column += n;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
length++;
|
|
408
|
+
column = c === '\n' || c === '\r' ? 0 : column + 1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return length;
|
|
412
|
+
}
|
|
413
|
+
const NONPRINTABLE = /^[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]$/u;
|
|
414
|
+
/** A string as Python's repr() writes it. */
|
|
415
|
+
export function pyRepr(s) {
|
|
416
|
+
const q = s.includes("'") && !s.includes('"') ? '"' : "'";
|
|
417
|
+
let r = q;
|
|
418
|
+
for (const c of s) {
|
|
419
|
+
if (c === q || c === '\\') {
|
|
420
|
+
r += '\\' + c;
|
|
421
|
+
}
|
|
422
|
+
else if (c === '\t') {
|
|
423
|
+
r += '\\t';
|
|
424
|
+
}
|
|
425
|
+
else if (c === '\n') {
|
|
426
|
+
r += '\\n';
|
|
427
|
+
}
|
|
428
|
+
else if (c === '\r') {
|
|
429
|
+
r += '\\r';
|
|
430
|
+
}
|
|
431
|
+
else if (c !== ' ' && NONPRINTABLE.test(c)) {
|
|
432
|
+
const cp = c.codePointAt(0);
|
|
433
|
+
const hex = cp.toString(16);
|
|
434
|
+
r += cp < 0x100 ? '\\x' + hex.padStart(2, '0')
|
|
435
|
+
: cp < 0x10000 ? '\\u' + hex.padStart(4, '0')
|
|
436
|
+
: '\\U' + hex.padStart(8, '0');
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
r += c;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return r + q;
|
|
443
|
+
}
|