@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/FORMAT.md +235 -0
- package/README.md +494 -0
- package/dist/types/cst.d.ts +78 -0
- package/dist/types/csv-machine.d.ts +104 -0
- package/dist/types/csv-stream.d.ts +102 -0
- package/dist/types/csv.d.ts +141 -0
- package/dist/types/errors.d.ts +79 -0
- package/dist/types/gbnf.d.ts +20 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/jsonx-scalar.d.ts +92 -0
- package/dist/types/jsonx-stream.d.ts +163 -0
- package/dist/types/jsonx.d.ts +31 -0
- package/dist/types/machine.d.ts +97 -0
- package/dist/types/parse.d.ts +24 -0
- package/dist/types/stream.d.ts +32 -0
- package/dist/types/stringify.d.ts +63 -0
- package/dist/types/util.d.ts +56 -0
- package/dist/types/values.d.ts +60 -0
- package/dist/types/write.d.ts +92 -0
- package/package.json +104 -0
- package/schemas/jaren-josl-data.schema.json +21 -0
- package/src/cst.js +256 -0
- package/src/csv-machine.js +908 -0
- package/src/csv-stream.js +196 -0
- package/src/csv.js +363 -0
- package/src/errors.js +103 -0
- package/src/gbnf.js +179 -0
- package/src/index.js +50 -0
- package/src/jsonx-scalar.js +326 -0
- package/src/jsonx-stream.js +806 -0
- package/src/jsonx.js +342 -0
- package/src/machine.js +1252 -0
- package/src/parse.js +37 -0
- package/src/stream.js +57 -0
- package/src/stringify.js +341 -0
- package/src/util.js +96 -0
- package/src/values.js +104 -0
- package/src/write.js +226 -0
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
|