@jarenjs/josl 0.34.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/src/gbnf.js ADDED
@@ -0,0 +1,179 @@
1
+ //#region GBNF grammar for raw JOSL / TOML text
2
+ // A character-level grammar for engines that constrain token sampling
3
+ // directly (the llama.cpp family), as opposed to the hosted `json_schema`
4
+ // providers the data-model twin in schemas/ serves. The two are answers to
5
+ // the same question at different layers: this one constrains the *text* a
6
+ // model may emit, so the output is syntactically valid JOSL by
7
+ // construction rather than by a post-hoc parse.
8
+ //
9
+ // A context-free grammar can only carry syntax. Duplicate keys, a header
10
+ // that reopens a table, a dotted key that collides with a section — every
11
+ // rule that needs to remember what the document already said stays the
12
+ // parser's job. Constrained sampling narrows the model to well-formed
13
+ // text; it does not make `parseJosl` unnecessary.
14
+ //
15
+ // The rules follow the TOML 1.0 ABNF closely enough to be read next to it.
16
+ // Where JOSL adds a value form (null, bigint, regexp, a `[[]]` root array)
17
+ // the extra rule is gated on `mode`.
18
+
19
+ // One deliberate narrowing: `regexp-body` treats an unescaped '/' as the
20
+ // closing delimiter unless it sits inside a character class, so a pattern
21
+ // that would need deeper nesting is not expressible here. Constrained
22
+ // output is a subset of what the parser accepts, never a superset.
23
+
24
+ const CORE = [
25
+ ['root', 'expression ( newline expression )*'],
26
+ ['expression', 'ws ( keyval ws | table ws )? comment?'],
27
+
28
+ ['ws', 'wschar*'],
29
+ ['wschar', '[ \\t]'],
30
+ ['newline', '"\\r"? "\\n"'],
31
+ ['comment', '"#" non-eol*'],
32
+ ['non-eol', '[\\t\\u0020-\\u007E] | non-ascii'],
33
+ // U+007F and the surrogate block are not scalar values TOML admits
34
+ // anywhere, so no character rule may reach them
35
+ ['non-ascii', '[\\u0080-\\uD7FF\\uE000-\\U0010FFFF]'],
36
+
37
+ ['keyval', 'key ws "=" ws val'],
38
+ ['key', 'simple-key ( ws "." ws simple-key )*'],
39
+ ['simple-key', 'quoted-key | unquoted-key'],
40
+ ['unquoted-key', '[A-Za-z0-9_-]+'],
41
+ ['quoted-key', 'basic-string | literal-string'],
42
+
43
+ ['string', 'ml-basic-string | basic-string | ml-literal-string | literal-string'],
44
+ ['basic-string', '"\\"" basic-char* "\\""'],
45
+ ['basic-char', 'basic-unescaped | escaped'],
46
+ ['basic-unescaped', '[\\t\\u0020-\\u0021\\u0023-\\u005B\\u005D-\\u007E] | non-ascii'],
47
+ ['escaped', '"\\\\" escape-seq-char'],
48
+ // no "\\/": JSON allows it, TOML does not
49
+ ['escape-seq-char', '[\\"\\\\bfnrt] | "u" unicode-scalar-4 | "U" unicode-scalar-8'],
50
+ // Only escapes that name a Unicode scalar value. Spelling the excluded
51
+ // ranges out in hex digits is what keeps a constrained sampler from
52
+ // being steered into a surrogate or a code point past U+10FFFF.
53
+ ['unicode-scalar-4', '[0-9A-CEFa-cef] HEXDIG HEXDIG HEXDIG | [Dd] [0-7] HEXDIG HEXDIG'],
54
+ ['unicode-scalar-8', '"0000" unicode-scalar-4 | "000" HEXNZ hex4 | "0010" hex4'],
55
+ ['hex4', 'HEXDIG HEXDIG HEXDIG HEXDIG'],
56
+ ['HEXDIG', '[0-9A-Fa-f]'],
57
+ ['HEXNZ', '[1-9A-Fa-f]'],
58
+
59
+ ['ml-basic-string', '"\\"\\"\\"" newline? ml-basic-body "\\"\\"\\""'],
60
+ ['ml-basic-body', 'mlb-content* ( mlb-quotes mlb-content+ )* mlb-quotes?'],
61
+ ['mlb-content', 'mlb-char | newline | mlb-escaped-nl'],
62
+ ['mlb-char', 'mlb-unescaped | escaped'],
63
+ ['mlb-quotes', '"\\"" "\\""?'],
64
+ ['mlb-unescaped', '[\\t\\u0020-\\u0021\\u0023-\\u005B\\u005D-\\u007E] | non-ascii'],
65
+ ['mlb-escaped-nl', '"\\\\" ws newline ( wschar | newline )*'],
66
+
67
+ ['literal-string', '"\'" literal-char* "\'"'],
68
+ ['literal-char', '[\\t\\u0020-\\u0026\\u0028-\\u007E] | non-ascii'],
69
+ ['ml-literal-string', '"\'\'\'" newline? ml-literal-body "\'\'\'"'],
70
+ ['ml-literal-body', 'mll-content* ( mll-quotes mll-content+ )* mll-quotes?'],
71
+ ['mll-content', 'literal-char | newline'],
72
+ ['mll-quotes', '"\'" "\'"?'],
73
+
74
+ ['boolean', '"true" | "false"'],
75
+
76
+ ['integer', 'hex-int | oct-int | bin-int | dec-int'],
77
+ ['dec-int', '( "+" | "-" )? unsigned-dec-int'],
78
+ ['unsigned-dec-int', '[1-9] ( digit | "_" digit )+ | digit'],
79
+ ['digit', '[0-9]'],
80
+ ['hex-int', '"0x" HEXDIG ( HEXDIG | "_" HEXDIG )*'],
81
+ ['oct-int', '"0o" [0-7] ( [0-7] | "_" [0-7] )*'],
82
+ ['bin-int', '"0b" [01] ( [01] | "_" [01] )*'],
83
+
84
+ ['float', 'dec-int ( exp | frac exp? ) | special-float'],
85
+ ['frac', '"." zero-prefixable-int'],
86
+ ['zero-prefixable-int', 'digit ( digit | "_" digit )*'],
87
+ ['exp', '( "e" | "E" ) ( "+" | "-" )? zero-prefixable-int'],
88
+ ['special-float', '( "+" | "-" )? ( "inf" | "nan" )'],
89
+
90
+ ['date-time', 'offset-date-time | local-date-time | local-date | local-time'],
91
+ ['date-fullyear', 'digit digit digit digit'],
92
+ ['date-month', 'digit digit'],
93
+ ['date-mday', 'digit digit'],
94
+ ['time-delim', '"T" | "t" | " "'],
95
+ ['time-hour', 'digit digit'],
96
+ ['time-minute', 'digit digit'],
97
+ ['time-second', 'digit digit'],
98
+ ['time-secfrac', '"." digit+'],
99
+ ['time-numoffset', '( "+" | "-" ) time-hour ":" time-minute'],
100
+ ['time-offset', '"Z" | "z" | time-numoffset'],
101
+ ['partial-time', 'time-hour ":" time-minute ":" time-second time-secfrac?'],
102
+ ['full-date', 'date-fullyear "-" date-month "-" date-mday'],
103
+ ['full-time', 'partial-time time-offset'],
104
+ ['offset-date-time', 'full-date time-delim full-time'],
105
+ ['local-date-time', 'full-date time-delim partial-time'],
106
+ ['local-date', 'full-date'],
107
+ ['local-time', 'partial-time'],
108
+
109
+ ['array', '"[" array-values? ws-comment-newline "]"'],
110
+ ['array-values',
111
+ 'ws-comment-newline val ws-comment-newline "," array-values'
112
+ + ' | ws-comment-newline val ws-comment-newline ","?'],
113
+ ['ws-comment-newline', '( wschar | comment? newline )*'],
114
+
115
+ ['inline-table', '"{" ws inline-table-keyvals? ws "}"'],
116
+ ['inline-table-keyvals', 'keyval ( ws "," ws keyval )*'],
117
+
118
+ ['std-table', '"[" ws key ws "]"'],
119
+ ['array-table', '"[[" ws key ws "]]"'],
120
+ ];
121
+
122
+ // Value alternatives and table forms, in the order a reader should try
123
+ // them: the longest, most specific token first.
124
+ const VAL_CORE = 'string | boolean | array | inline-table | date-time | float | integer';
125
+ const VAL_JOSL = `${VAL_CORE} | null-lit | bigint | regexp`;
126
+
127
+ const JOSL_ONLY = [
128
+ ['null-lit', '"null"'],
129
+ ['bigint', '( hex-int | oct-int | bin-int | unsigned-dec-int ) "n"'],
130
+ ['regexp', '"/" regexp-body "/" regexp-flags'],
131
+ ['regexp-body', 'regexp-atom+'],
132
+ ['regexp-atom', 'regexp-char | "[" regexp-class-char* "]"'],
133
+ ['regexp-char', '[^/\\\\\\[\\n] | "\\\\" [^\\n]'],
134
+ ['regexp-class-char', '[^\\]\\\\\\n] | "\\\\" [^\\n]'],
135
+ ['regexp-flags', '[a-z]*'],
136
+ ['root-item-table', '"[[" ws "]]"'],
137
+ ];
138
+
139
+ /**
140
+ * Build a GBNF grammar for raw JOSL or TOML text.
141
+ *
142
+ * The result is a complete grammar string with `root` as its entry rule,
143
+ * ready to hand to a llama.cpp-family sampler. It constrains syntax only —
144
+ * a document it accepts still has to go through `parseJosl` for the rules
145
+ * a context-free grammar cannot express (duplicate keys, table conflicts).
146
+ * @param {object} [options] - Grammar options
147
+ * @param {'josl'|'toml'} [options.mode] - 'toml' omits the JOSL-only value
148
+ * forms (null, bigint, regexp) and the `[[]]` root-array header
149
+ * @returns {string} The GBNF grammar
150
+ */
151
+ export function toGbnf(options = undefined) {
152
+ const toml = options?.mode === 'toml';
153
+ const rules = [
154
+ ...CORE,
155
+ ['val', toml ? VAL_CORE : VAL_JOSL],
156
+ ['table', toml ? 'array-table | std-table' : 'root-item-table | array-table | std-table'],
157
+ ...(toml ? [] : JOSL_ONLY),
158
+ ];
159
+ const order = new Map(rules.map(([name], i) => [name, i]));
160
+ const header = toml
161
+ ? '# GBNF grammar for TOML 1.0 text (@jarenjs/josl)'
162
+ : '# GBNF grammar for JOSL text (@jarenjs/josl)';
163
+ const body = rules
164
+ .slice()
165
+ .sort((a, b) => order.get(a[0]) - order.get(b[0]))
166
+ .map(([name, def]) => `${name} ::= ${def}`)
167
+ .join('\n');
168
+ return `${header}\n# syntax only; semantic rules stay with the parser\n\n${body}\n`;
169
+ }
170
+
171
+ /**
172
+ * Build a GBNF grammar for strict TOML 1.0 text.
173
+ * @returns {string} The GBNF grammar
174
+ */
175
+ export function tomlToGbnf() {
176
+ return toGbnf({ mode: 'toml' });
177
+ }
178
+
179
+ //#endregion
package/src/index.js ADDED
@@ -0,0 +1,50 @@
1
+ //#region @jarenjs/josl
2
+ // JOSL - JavaScript Obvious Streaming Language.
3
+ //
4
+ // A TOML 1.0 backward-compatible data language with JavaScript's obvious
5
+ // value types as first-class citizens (null, bigint, regexp, datetimes)
6
+ // plus a streamable root array, and JSONX, the same extensions over JSON.
7
+ // See FORMAT.md for the language delta and design rationale.
8
+
9
+ export { parseJosl, parseToml } from './parse.js';
10
+ export { parseJoslCst, parseTomlCst, JoslCstDocument } from './cst.js';
11
+ export { createStreamReader, parseJoslStream } from './stream.js';
12
+ export {
13
+ stringifyJosl,
14
+ stringifyToml,
15
+ formatKey,
16
+ formatKeyPath,
17
+ formatValue,
18
+ formatSection,
19
+ } from './stringify.js';
20
+ export { createStreamWriter, stringifyJoslChunks } from './write.js';
21
+ export { toGbnf, tomlToGbnf } from './gbnf.js';
22
+ export { parseJsonx, stringifyJsonx } from './jsonx.js';
23
+ export { createJsonxStreamReader, parseJsonxStream } from './jsonx-stream.js';
24
+ export {
25
+ parseCsv,
26
+ parseCsvDocument,
27
+ stringifyCsv,
28
+ stringifyCsvChunks,
29
+ sniffCsvDialect,
30
+ formatCsvValue,
31
+ coerceCsvValue,
32
+ CSV_CODES,
33
+ } from './csv.js';
34
+ export {
35
+ createCsvStreamReader,
36
+ parseCsvStream,
37
+ iterateCsvStream,
38
+ createCsvStreamWriter,
39
+ CsvStreamWriter,
40
+ } from './csv-stream.js';
41
+ export { JoslSyntaxError, JoslStringifyError, JsonxSyntaxError, CsvSyntaxError } from './errors.js';
42
+ export {
43
+ LocalDate,
44
+ LocalTime,
45
+ LocalDateTime,
46
+ isValidDateParts,
47
+ isValidTimeParts,
48
+ } from './values.js';
49
+
50
+ //#endregion
@@ -0,0 +1,326 @@
1
+ //#region JSONX scalar decoders
2
+ // Pure, position-based scalar decoders shared by the full-text parser
3
+ // (jsonx.js) and the incremental reader (jsonx-stream.js). Every function
4
+ // takes the source text plus an offset and reports errors through an
5
+ // `err(pos, message, hint)` callback that must throw; none of them keep
6
+ // state, so both parsers decode every scalar through a single code path.
7
+ //
8
+ // The `checkEnd(pos)` callback lets the caller assert that the character
9
+ // after a match is a legal value terminator *at the same point in the
10
+ // grammar* where the full-text parser checks it — error ordering between
11
+ // 'unexpected character after value' and the value-specific validations
12
+ // is part of the observable behavior and must match in both parsers.
13
+
14
+ import {
15
+ CC_LF,
16
+ CC_MINUS,
17
+ CC_PLUS,
18
+ CC_SLASH,
19
+ CC_COMMA,
20
+ CC_SPACE,
21
+ CC_TAB,
22
+ CC_CR,
23
+ CC_DQUOTE,
24
+ CC_BACKSLASH,
25
+ CC_LBRACKET,
26
+ CC_RBRACKET,
27
+ CC_RBRACE,
28
+ isAsciiLetterCode,
29
+ } from '@jarenjs/core/scan';
30
+
31
+ import {
32
+ LocalDate,
33
+ LocalTime,
34
+ LocalDateTime,
35
+ isValidDateParts,
36
+ isValidTimeParts,
37
+ } from './values.js';
38
+ // the date-time token patterns are shared with the JOSL machine
39
+ import { RE_DATETIME, RE_TIMEONLY, stickyExec } from './util.js';
40
+
41
+ const RE_NUM_JSON = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
42
+ const RE_NUM_JSONX = /[+-]?(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?(n?)/y;
43
+
44
+ /**
45
+ * @callback JsonxErrCallback
46
+ * @param {number} pos - Offset of the error in the source text
47
+ * @param {string} message - What is wrong
48
+ * @param {string} [hint] - Repair suggestion
49
+ * @returns {never} Must throw
50
+ */
51
+
52
+ /**
53
+ * @callback JsonxCheckEndCallback
54
+ * @param {number} pos - Offset of the first character after the value
55
+ * @returns {void} Must throw when the character cannot follow a value
56
+ */
57
+
58
+ /**
59
+ * Whether a char code may legally follow a completed JSON/JSONX value
60
+ * (whitespace, `,`, `]` or `}`).
61
+ * @param {number} c - The char code
62
+ * @returns {boolean}
63
+ */
64
+ export function isValueEndCode(c) {
65
+ return c === CC_SPACE || c === CC_TAB || c === CC_LF || c === CC_CR
66
+ || c === CC_COMMA || c === CC_RBRACKET || c === CC_RBRACE;
67
+ }
68
+
69
+ /**
70
+ * Decode one escape sequence.
71
+ * @param {string} text - Source text
72
+ * @param {number} pos - Offset of the backslash
73
+ * @param {JsonxErrCallback} err - Error reporter
74
+ * @returns {[string, number]} The decoded text and the offset after it
75
+ */
76
+ export function decodeEscape(text, pos, err) {
77
+ if (pos + 1 >= text.length)
78
+ err(pos, 'unterminated escape sequence');
79
+ const c = text.charCodeAt(pos + 1);
80
+ switch (c) {
81
+ case CC_DQUOTE: return ['"', pos + 2];
82
+ case CC_BACKSLASH: return ['\\', pos + 2];
83
+ case CC_SLASH: return ['/', pos + 2];
84
+ case 0x62: return ['\b', pos + 2];
85
+ case 0x66: return ['\f', pos + 2];
86
+ case 0x6E: return ['\n', pos + 2];
87
+ case 0x72: return ['\r', pos + 2];
88
+ case 0x74: return ['\t', pos + 2];
89
+ case 0x75: {
90
+ const hex = text.slice(pos + 2, pos + 6);
91
+ if (hex.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hex))
92
+ err(pos, "expected 4 hex digits after '\\u'");
93
+ return [String.fromCharCode(parseInt(hex, 16)), pos + 6];
94
+ }
95
+ default:
96
+ err(pos, `invalid escape '\\${text[pos + 1]}'`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Decode a double-quoted string.
102
+ * @param {string} text - Source text
103
+ * @param {number} pos - Offset of the opening quote
104
+ * @param {JsonxErrCallback} err - Error reporter
105
+ * @returns {[string, number]} The value and the offset after the close quote
106
+ */
107
+ export function decodeString(text, pos, err) {
108
+ const [out, p] = decodeStringSpan(text, pos + 1, text.length, err, false);
109
+ if (p < text.length) // stopped on the closing quote
110
+ return [out, p + 1];
111
+ err(p, 'unterminated string', "close the string with '\"'");
112
+ }
113
+
114
+ /**
115
+ * Decode the body of a string between two offsets, stopping at the closing
116
+ * quote (which it does not consume) or at `stop`. In `partial` mode it also
117
+ * stops before an escape that is not complete within the span, so a
118
+ * still-arriving string can be decoded as far as it is safe to.
119
+ * @param {string} text - Source text
120
+ * @param {number} from - Offset of the first body character
121
+ * @param {number} stop - Exclusive end offset
122
+ * @param {JsonxErrCallback} err - Error reporter
123
+ * @param {boolean} partial - Whether the span may end mid-escape
124
+ * @returns {[string, number]} The decoded text and the offset reached
125
+ */
126
+ export function decodeStringSpan(text, from, stop, err, partial) {
127
+ let p = from;
128
+ let out = '';
129
+ let chunk = p;
130
+ while (p < stop) {
131
+ const c = text.charCodeAt(p);
132
+ if (c === CC_DQUOTE)
133
+ break;
134
+ if (c === CC_BACKSLASH) {
135
+ // the longest escape is \uXXXX; anything shorter than that near the
136
+ // span's end may simply not have arrived yet
137
+ if (partial && p + (text.charCodeAt(p + 1) === 0x75 ? 6 : 2) > stop)
138
+ break;
139
+ out += text.slice(chunk, p);
140
+ const [dec, np] = decodeEscape(text, p, err);
141
+ out += dec;
142
+ p = np;
143
+ chunk = p;
144
+ continue;
145
+ }
146
+ if (c < 0x20)
147
+ err(p, 'control characters must be escaped in strings');
148
+ p++;
149
+ }
150
+ return [out + text.slice(chunk, p), p];
151
+ }
152
+
153
+ /**
154
+ * Try to match an RFC 3339 datetime token (date, time, local or offset
155
+ * date-time; `T`, `t` or a single space may separate date and time).
156
+ * @param {string} text - Source text
157
+ * @param {number} pos - Offset of the first digit
158
+ * @param {JsonxErrCallback} err - Error reporter
159
+ * @param {JsonxCheckEndCallback} checkEnd - Value-terminator check
160
+ * @returns {[*, number]|null} The value and end offset, or null if the
161
+ * text at `pos` is not a datetime
162
+ */
163
+ export function matchDateTime(text, pos, err, checkEnd) {
164
+ let m = stickyExec(RE_DATETIME, text, pos);
165
+ if (m !== null) {
166
+ const year = Number(m[1]);
167
+ const month = Number(m[2]);
168
+ const day = Number(m[3]);
169
+ if (!isValidDateParts(year, month, day))
170
+ err(pos, `invalid date '${m[0]}'`);
171
+ const date = new LocalDate(year, month, day);
172
+ const end = pos + m[0].length;
173
+ checkEnd(end);
174
+ if (m[4] === undefined)
175
+ return [date, end];
176
+ const hour = Number(m[4]);
177
+ const minute = Number(m[5]);
178
+ const second = Number(m[6]);
179
+ if (!isValidTimeParts(hour, minute, second))
180
+ err(end, `invalid time '${m[0]}'`);
181
+ const time = new LocalTime(hour, minute, second, m[7] ?? '');
182
+ if (m[8] === undefined)
183
+ return [new LocalDateTime(date, time), end];
184
+ const offset = m[8] === 'z' || m[8] === 'Z' ? 'Z' : m[8];
185
+ const instant = new Date(`${date.toString()}T${time.toString()}${offset}`);
186
+ if (Number.isNaN(instant.getTime()))
187
+ err(end, `invalid date-time '${m[0]}'`);
188
+ return [instant, end];
189
+ }
190
+ m = stickyExec(RE_TIMEONLY, text, pos);
191
+ if (m !== null) {
192
+ const hour = Number(m[1]);
193
+ const minute = Number(m[2]);
194
+ const second = Number(m[3]);
195
+ if (!isValidTimeParts(hour, minute, second))
196
+ err(pos, `invalid time '${m[0]}'`);
197
+ const end = pos + m[0].length;
198
+ checkEnd(end);
199
+ return [new LocalTime(hour, minute, second, m[4] ?? ''), end];
200
+ }
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Match a number token. Strict JSON follows RFC 8259; JSONX adds a
206
+ * leading `+`, `_` separators, an `n` bigint suffix, and auto-promotes
207
+ * unsafe integers to BigInt.
208
+ * @param {string} text - Source text
209
+ * @param {number} pos - Offset of the first sign or digit
210
+ * @param {'jsonx'|'json'} mode - Dialect
211
+ * @param {JsonxErrCallback} err - Error reporter
212
+ * @param {JsonxCheckEndCallback} checkEnd - Value-terminator check
213
+ * @returns {[number|bigint, number]} The value and end offset
214
+ */
215
+ export function matchNumber(text, pos, mode, err, checkEnd) {
216
+ const m = stickyExec(mode === 'json' ? RE_NUM_JSON : RE_NUM_JSONX, text, pos);
217
+ if (m === null || m[0].length === 0)
218
+ err(pos, 'invalid number');
219
+ const end = pos + m[0].length;
220
+ checkEnd(end);
221
+ if (mode === 'json')
222
+ return [Number(m[0]), end];
223
+ const big = m[1] === 'n';
224
+ const token = big ? m[0].slice(0, -1) : m[0];
225
+ const isFloat = /[.eE]/.test(token);
226
+ if (isFloat) {
227
+ if (big)
228
+ err(pos, 'bigint literals cannot have a fraction or exponent');
229
+ return [Number(token.replace(/_/g, '')), end];
230
+ }
231
+ const stripped = token.replace(/[_+]/g, '');
232
+ if (big)
233
+ return [BigInt(stripped), end];
234
+ const value = Number(stripped);
235
+ return [Number.isSafeInteger(value) ? value : BigInt(stripped), end];
236
+ }
237
+
238
+ /**
239
+ * Match a word token (`true`, `false`, `null`, and in JSONX the
240
+ * optionally signed non-finite spellings `inf`/`Infinity`/`nan`/`NaN`).
241
+ * The caller performs the value-terminator check.
242
+ * @param {string} text - Source text
243
+ * @param {number} pos - Offset of the first sign or letter
244
+ * @param {'jsonx'|'json'} mode - Dialect
245
+ * @param {JsonxErrCallback} err - Error reporter
246
+ * @returns {[boolean|null|number, number]} The value and end offset
247
+ */
248
+ export function matchWord(text, pos, mode, err) {
249
+ let p = pos;
250
+ const c0 = text.charCodeAt(p);
251
+ if (c0 === CC_MINUS || c0 === CC_PLUS)
252
+ p++;
253
+ const neg = c0 === CC_MINUS;
254
+ const wordStart = p;
255
+ while (p < text.length && isAsciiLetterCode(text.charCodeAt(p)))
256
+ p++;
257
+ const word = text.slice(wordStart, p);
258
+ const signed = wordStart !== pos;
259
+ switch (word) {
260
+ case 'true':
261
+ if (signed)
262
+ break;
263
+ return [true, p];
264
+ case 'false':
265
+ if (signed)
266
+ break;
267
+ return [false, p];
268
+ case 'null':
269
+ if (signed)
270
+ break;
271
+ return [null, p];
272
+ case 'inf':
273
+ case 'Infinity':
274
+ if (mode === 'json')
275
+ err(pos, `'${word}' is a JSONX extension`, 'JSON cannot represent non-finite numbers');
276
+ return [neg ? -Infinity : Infinity, p];
277
+ case 'nan':
278
+ case 'NaN':
279
+ if (mode === 'json')
280
+ err(pos, `'${word}' is a JSONX extension`, 'JSON cannot represent non-finite numbers');
281
+ return [NaN, p];
282
+ }
283
+ err(pos, `invalid value '${text.slice(pos, p)}'`);
284
+ }
285
+
286
+ /**
287
+ * Match a regexp literal. The caller performs the value-terminator check.
288
+ * @param {string} text - Source text
289
+ * @param {number} pos - Offset of the opening slash
290
+ * @param {JsonxErrCallback} err - Error reporter
291
+ * @returns {[RegExp, number]} The value and end offset
292
+ */
293
+ export function matchRegExp(text, pos, err) {
294
+ let p = pos + 1;
295
+ let inClass = false;
296
+ for (;;) {
297
+ if (p >= text.length || text.charCodeAt(p) === CC_LF)
298
+ err(pos, 'unterminated regexp literal', "close the regexp with '/'");
299
+ const c = text.charCodeAt(p);
300
+ if (c === CC_BACKSLASH) {
301
+ p += 2;
302
+ continue;
303
+ }
304
+ if (c === CC_LBRACKET)
305
+ inClass = true;
306
+ else if (c === CC_RBRACKET)
307
+ inClass = false;
308
+ else if (c === CC_SLASH && !inClass)
309
+ break;
310
+ p++;
311
+ }
312
+ const body = text.slice(pos + 1, p);
313
+ p++;
314
+ const flagStart = p;
315
+ while (p < text.length && isAsciiLetterCode(text.charCodeAt(p)))
316
+ p++;
317
+ const flags = text.slice(flagStart, p);
318
+ try {
319
+ return [new RegExp(body, flags), p];
320
+ }
321
+ catch (e) {
322
+ err(pos, `invalid regexp literal: ${e.message}`);
323
+ }
324
+ }
325
+
326
+ //#endregion