@effected/toml 0.1.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/LICENSE +21 -0
- package/README.md +140 -0
- package/Toml.js +165 -0
- package/TomlDateTime.js +147 -0
- package/TomlDiagnostic.js +106 -0
- package/TomlDocument.js +133 -0
- package/TomlEdit.js +52 -0
- package/TomlFormat.js +611 -0
- package/TomlNode.js +212 -0
- package/TomlVisitor.js +152 -0
- package/index.d.ts +897 -0
- package/index.js +10 -0
- package/internal/diagnostics.js +53 -0
- package/internal/limits.js +24 -0
- package/internal/parser.js +408 -0
- package/internal/scanner.js +493 -0
- package/internal/semantic.js +253 -0
- package/internal/stringifyValue.js +191 -0
- package/package.json +45 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { RawTomlError } from "./diagnostics.js";
|
|
2
|
+
import { TomlLocalDate, TomlLocalDateTime, TomlLocalTime, TomlOffsetDateTime } from "../TomlDateTime.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/scanner.ts
|
|
5
|
+
const NUL = 0;
|
|
6
|
+
const TAB = 9;
|
|
7
|
+
const LF = 10;
|
|
8
|
+
const CR = 13;
|
|
9
|
+
const SPACE = 32;
|
|
10
|
+
const QUOTE = 34;
|
|
11
|
+
const HASH = 35;
|
|
12
|
+
const APOSTROPHE = 39;
|
|
13
|
+
const COMMA = 44;
|
|
14
|
+
const HYPHEN = 45;
|
|
15
|
+
const BACKSLASH = 92;
|
|
16
|
+
const RIGHT_BRACKET = 93;
|
|
17
|
+
const RIGHT_BRACE = 125;
|
|
18
|
+
const LOWER_U = 117;
|
|
19
|
+
const UPPER_U = 85;
|
|
20
|
+
const BOM = 65279;
|
|
21
|
+
const raise = (code, message, offset, length) => {
|
|
22
|
+
throw new RawTomlError({
|
|
23
|
+
code,
|
|
24
|
+
message,
|
|
25
|
+
offset,
|
|
26
|
+
length
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** `U+XXXX` display form of a char code. */
|
|
30
|
+
const codeLabel = (code) => `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
31
|
+
/** TOML control characters (tab excluded): U+0000-U+0008, U+000A-U+001F, U+007F. */
|
|
32
|
+
const isControlChar = (code) => code <= 8 || code >= 10 && code <= 31 || code === 127;
|
|
33
|
+
const isDigit = (code) => code >= 48 && code <= 57;
|
|
34
|
+
/** Whether `code` is a bare-key character: `[A-Za-z0-9_-]`. */
|
|
35
|
+
const isBareKeyChar = (code) => code >= 97 && code <= 122 || code >= 65 && code <= 90 || isDigit(code) || code === 95 || code === HYPHEN;
|
|
36
|
+
/** The number of code units to skip for a single leading U+FEFF BOM. */
|
|
37
|
+
const skipBom = (source) => source.charCodeAt(0) === BOM ? 1 : 0;
|
|
38
|
+
/**
|
|
39
|
+
* Reject a document carrying U+FFFD REPLACEMENT CHARACTER. TOML 1.0.0 requires
|
|
40
|
+
* a valid UTF-8 document, but this engine receives an already-decoded JS
|
|
41
|
+
* string — after Node's lossy utf8 decode, U+FFFD is the only surviving
|
|
42
|
+
* evidence of a malformed byte sequence. The grammar technically admits
|
|
43
|
+
* U+FFFD inside strings and comments, so this trades away a pathological
|
|
44
|
+
* legal character to honor the encoding rule the toml-test corpus pins
|
|
45
|
+
* (invalid/encoding/bad-utf8-in-string.toml, bad-utf8-in-comment.toml,
|
|
46
|
+
* bad-codepoint.toml and friends).
|
|
47
|
+
*/
|
|
48
|
+
const assertValidUnicode = (source) => {
|
|
49
|
+
const index = source.indexOf("�");
|
|
50
|
+
if (index !== -1) raise("InvalidUtf8", "invalid UTF-8: U+FFFD REPLACEMENT CHARACTER marks a malformed byte sequence in the decoded input", index, 1);
|
|
51
|
+
};
|
|
52
|
+
/** Skip spaces and tabs; returns the position of the first other character. */
|
|
53
|
+
const scanWhitespace = (source, pos) => {
|
|
54
|
+
let i = pos;
|
|
55
|
+
while (i < source.length) {
|
|
56
|
+
const code = source.charCodeAt(i);
|
|
57
|
+
if (code !== SPACE && code !== TAB) break;
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
return i;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Consume one `\n` or `\r\n` newline. A lone `\r` throws BareCarriageReturn;
|
|
64
|
+
* no newline at `pos` returns `pos` unchanged.
|
|
65
|
+
*/
|
|
66
|
+
const scanNewline = (source, pos) => {
|
|
67
|
+
const code = source.charCodeAt(pos);
|
|
68
|
+
if (code === LF) return pos + 1;
|
|
69
|
+
if (code === CR) {
|
|
70
|
+
if (source.charCodeAt(pos + 1) === LF) return pos + 2;
|
|
71
|
+
return raise("BareCarriageReturn", "carriage return not followed by a line feed", pos, 1);
|
|
72
|
+
}
|
|
73
|
+
return pos;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Scan a comment starting at `#` through end of line or EOF. The value
|
|
77
|
+
* excludes the `#`; control characters other than tab are rejected.
|
|
78
|
+
*/
|
|
79
|
+
const scanComment = (source, pos) => {
|
|
80
|
+
let i = pos + 1;
|
|
81
|
+
while (i < source.length) {
|
|
82
|
+
const code = source.charCodeAt(i);
|
|
83
|
+
if (code === LF || code === CR) break;
|
|
84
|
+
if (isControlChar(code)) return raise("ControlCharacterInComment", `control character ${codeLabel(code)} in comment`, i, 1);
|
|
85
|
+
i += 1;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
value: source.slice(pos + 1, i),
|
|
89
|
+
end: i
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/** Scan a run of bare-key characters; the value may be empty. */
|
|
93
|
+
const scanBareKey = (source, pos) => {
|
|
94
|
+
let i = pos;
|
|
95
|
+
while (i < source.length && isBareKeyChar(source.charCodeAt(i))) i += 1;
|
|
96
|
+
return {
|
|
97
|
+
value: source.slice(pos, i),
|
|
98
|
+
end: i
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/** The decoded character for a simple escape code, or undefined. */
|
|
102
|
+
const simpleEscape = (code) => {
|
|
103
|
+
switch (code) {
|
|
104
|
+
case 98: return "\b";
|
|
105
|
+
case 116: return " ";
|
|
106
|
+
case 110: return "\n";
|
|
107
|
+
case 102: return "\f";
|
|
108
|
+
case 114: return "\r";
|
|
109
|
+
case QUOTE: return "\"";
|
|
110
|
+
case BACKSLASH: return "\\";
|
|
111
|
+
default: return;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const HEX_DIGITS = /^[0-9A-Fa-f]+$/;
|
|
115
|
+
/**
|
|
116
|
+
* Decode a `\uXXXX` / `\UXXXXXXXX` escape at `backslash`. The code point must
|
|
117
|
+
* be a Unicode scalar value: surrogates and values above U+10FFFF are errors.
|
|
118
|
+
*/
|
|
119
|
+
const decodeUnicodeEscape = (source, backslash, width) => {
|
|
120
|
+
const start = backslash + 2;
|
|
121
|
+
const hex = source.slice(start, start + width);
|
|
122
|
+
if (hex.length < width || !HEX_DIGITS.test(hex)) return raise("InvalidUnicodeEscape", `\\${width === 4 ? "u" : "U"} escape requires ${width} hexadecimal digits`, backslash, width + 2);
|
|
123
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
124
|
+
if (codePoint >= 55296 && codePoint <= 57343 || codePoint > 1114111) return raise("InvalidUnicodeEscape", `${codeLabel(codePoint)} is not a Unicode scalar value`, backslash, width + 2);
|
|
125
|
+
return {
|
|
126
|
+
codePoint,
|
|
127
|
+
end: start + width
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
/** Scan a single-line basic string starting at the opening `"`. */
|
|
131
|
+
const scanBasicString = (source, pos) => {
|
|
132
|
+
let out = "";
|
|
133
|
+
let chunkStart = pos + 1;
|
|
134
|
+
let i = pos + 1;
|
|
135
|
+
while (i < source.length) {
|
|
136
|
+
const code = source.charCodeAt(i);
|
|
137
|
+
if (code === QUOTE) return {
|
|
138
|
+
value: out + source.slice(chunkStart, i),
|
|
139
|
+
end: i + 1
|
|
140
|
+
};
|
|
141
|
+
if (code === BACKSLASH) {
|
|
142
|
+
if (i + 1 >= source.length) break;
|
|
143
|
+
out += source.slice(chunkStart, i);
|
|
144
|
+
const next = source.charCodeAt(i + 1);
|
|
145
|
+
const mapped = simpleEscape(next);
|
|
146
|
+
if (mapped !== void 0) {
|
|
147
|
+
out += mapped;
|
|
148
|
+
i += 2;
|
|
149
|
+
} else if (next === LOWER_U || next === UPPER_U) {
|
|
150
|
+
const decoded = decodeUnicodeEscape(source, i, next === LOWER_U ? 4 : 8);
|
|
151
|
+
out += String.fromCodePoint(decoded.codePoint);
|
|
152
|
+
i = decoded.end;
|
|
153
|
+
} else return raise("InvalidEscape", `invalid escape sequence \\${source[i + 1] ?? ""}`, i, 2);
|
|
154
|
+
chunkStart = i;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (code === LF || code === CR && source.charCodeAt(i + 1) === LF) return raise("UnterminatedString", "basic string not closed before end of line", pos, i - pos);
|
|
158
|
+
if (code === CR) return raise("BareCarriageReturn", "carriage return not followed by a line feed", i, 1);
|
|
159
|
+
if (isControlChar(code)) return raise("ControlCharacterInString", `control character ${codeLabel(code)} in basic string`, i, 1);
|
|
160
|
+
i += 1;
|
|
161
|
+
}
|
|
162
|
+
return raise("UnterminatedString", "unterminated basic string", pos, source.length - pos);
|
|
163
|
+
};
|
|
164
|
+
/** Scan a single-line literal string starting at the opening `'`. */
|
|
165
|
+
const scanLiteralString = (source, pos) => {
|
|
166
|
+
let i = pos + 1;
|
|
167
|
+
while (i < source.length) {
|
|
168
|
+
const code = source.charCodeAt(i);
|
|
169
|
+
if (code === APOSTROPHE) return {
|
|
170
|
+
value: source.slice(pos + 1, i),
|
|
171
|
+
end: i + 1
|
|
172
|
+
};
|
|
173
|
+
if (code === LF || code === CR && source.charCodeAt(i + 1) === LF) return raise("UnterminatedString", "literal string not closed before end of line", pos, i - pos);
|
|
174
|
+
if (code === CR) return raise("BareCarriageReturn", "carriage return not followed by a line feed", i, 1);
|
|
175
|
+
if (isControlChar(code)) return raise("ControlCharacterInString", `control character ${codeLabel(code)} in literal string`, i, 1);
|
|
176
|
+
i += 1;
|
|
177
|
+
}
|
|
178
|
+
return raise("UnterminatedString", "unterminated literal string", pos, source.length - pos);
|
|
179
|
+
};
|
|
180
|
+
/** Skip a newline immediately after a multiline opening delimiter. */
|
|
181
|
+
const skipLeadingNewline = (source, pos) => {
|
|
182
|
+
if (source.charCodeAt(pos) === LF) return pos + 1;
|
|
183
|
+
if (source.charCodeAt(pos) === CR && source.charCodeAt(pos + 1) === LF) return pos + 2;
|
|
184
|
+
return pos;
|
|
185
|
+
};
|
|
186
|
+
/** Skip whitespace and newlines after a line-ending backslash's newline. */
|
|
187
|
+
const skipLineEndingTrim = (source, pos) => {
|
|
188
|
+
let i = pos;
|
|
189
|
+
while (i < source.length) {
|
|
190
|
+
const code = source.charCodeAt(i);
|
|
191
|
+
if (code === SPACE || code === TAB || code === LF) {
|
|
192
|
+
i += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (code === CR) {
|
|
196
|
+
if (source.charCodeAt(i + 1) === LF) {
|
|
197
|
+
i += 2;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
return raise("BareCarriageReturn", "carriage return not followed by a line feed", i, 1);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return i;
|
|
205
|
+
};
|
|
206
|
+
/** Scan a multiline basic string starting at the opening `"""`. */
|
|
207
|
+
const scanMultilineBasicString = (source, pos) => {
|
|
208
|
+
let i = skipLeadingNewline(source, pos + 3);
|
|
209
|
+
let out = "";
|
|
210
|
+
let chunkStart = i;
|
|
211
|
+
while (i < source.length) {
|
|
212
|
+
const code = source.charCodeAt(i);
|
|
213
|
+
if (code === QUOTE) {
|
|
214
|
+
let runEnd = i;
|
|
215
|
+
while (runEnd < source.length && source.charCodeAt(runEnd) === QUOTE) runEnd += 1;
|
|
216
|
+
const run = runEnd - i;
|
|
217
|
+
if (run < 3) {
|
|
218
|
+
i = runEnd;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const contentQuotes = Math.min(run - 3, 2);
|
|
222
|
+
return {
|
|
223
|
+
value: out + source.slice(chunkStart, i + contentQuotes),
|
|
224
|
+
end: i + contentQuotes + 3
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (code === BACKSLASH) {
|
|
228
|
+
out += source.slice(chunkStart, i);
|
|
229
|
+
const next = source.charCodeAt(i + 1);
|
|
230
|
+
const mapped = simpleEscape(next);
|
|
231
|
+
if (mapped !== void 0) {
|
|
232
|
+
out += mapped;
|
|
233
|
+
i += 2;
|
|
234
|
+
chunkStart = i;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (next === LOWER_U || next === UPPER_U) {
|
|
238
|
+
const decoded = decodeUnicodeEscape(source, i, next === LOWER_U ? 4 : 8);
|
|
239
|
+
out += String.fromCodePoint(decoded.codePoint);
|
|
240
|
+
i = decoded.end;
|
|
241
|
+
chunkStart = i;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let probe = i + 1;
|
|
245
|
+
while (probe < source.length) {
|
|
246
|
+
const probeCode = source.charCodeAt(probe);
|
|
247
|
+
if (probeCode !== SPACE && probeCode !== TAB) break;
|
|
248
|
+
probe += 1;
|
|
249
|
+
}
|
|
250
|
+
if (probe >= source.length) break;
|
|
251
|
+
const probeCode = source.charCodeAt(probe);
|
|
252
|
+
if (probeCode === LF || probeCode === CR && source.charCodeAt(probe + 1) === LF) {
|
|
253
|
+
i = skipLineEndingTrim(source, probe);
|
|
254
|
+
chunkStart = i;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
return raise("InvalidEscape", `invalid escape sequence \\${source[i + 1] ?? ""}`, i, probe - i + 1);
|
|
258
|
+
}
|
|
259
|
+
if (code === LF) {
|
|
260
|
+
i += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (code === CR) {
|
|
264
|
+
if (source.charCodeAt(i + 1) === LF) {
|
|
265
|
+
i += 2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
return raise("BareCarriageReturn", "carriage return not followed by a line feed", i, 1);
|
|
269
|
+
}
|
|
270
|
+
if (isControlChar(code)) return raise("ControlCharacterInString", `control character ${codeLabel(code)} in multiline basic string`, i, 1);
|
|
271
|
+
i += 1;
|
|
272
|
+
}
|
|
273
|
+
return raise("UnterminatedString", "unterminated multiline basic string", pos, source.length - pos);
|
|
274
|
+
};
|
|
275
|
+
/** Scan a multiline literal string starting at the opening `'''`. */
|
|
276
|
+
const scanMultilineLiteralString = (source, pos) => {
|
|
277
|
+
const contentStart = skipLeadingNewline(source, pos + 3);
|
|
278
|
+
let i = contentStart;
|
|
279
|
+
while (i < source.length) {
|
|
280
|
+
const code = source.charCodeAt(i);
|
|
281
|
+
if (code === APOSTROPHE) {
|
|
282
|
+
let runEnd = i;
|
|
283
|
+
while (runEnd < source.length && source.charCodeAt(runEnd) === APOSTROPHE) runEnd += 1;
|
|
284
|
+
const run = runEnd - i;
|
|
285
|
+
if (run < 3) {
|
|
286
|
+
i = runEnd;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const contentQuotes = Math.min(run - 3, 2);
|
|
290
|
+
return {
|
|
291
|
+
value: source.slice(contentStart, i + contentQuotes),
|
|
292
|
+
end: i + contentQuotes + 3
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (code === LF) {
|
|
296
|
+
i += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (code === CR) {
|
|
300
|
+
if (source.charCodeAt(i + 1) === LF) {
|
|
301
|
+
i += 2;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
return raise("BareCarriageReturn", "carriage return not followed by a line feed", i, 1);
|
|
305
|
+
}
|
|
306
|
+
if (isControlChar(code)) return raise("ControlCharacterInString", `control character ${codeLabel(code)} in multiline literal string`, i, 1);
|
|
307
|
+
i += 1;
|
|
308
|
+
}
|
|
309
|
+
return raise("UnterminatedString", "unterminated multiline literal string", pos, source.length - pos);
|
|
310
|
+
};
|
|
311
|
+
/** Scan one raw token span; NUL anywhere is a lex error. */
|
|
312
|
+
const scanTokenSpan = (source, pos) => {
|
|
313
|
+
let i = pos;
|
|
314
|
+
while (i < source.length) {
|
|
315
|
+
const code = source.charCodeAt(i);
|
|
316
|
+
if (code === NUL) return raise("InvalidCharacter", "NUL character in document", i, 1);
|
|
317
|
+
if (code === SPACE || code === TAB || code === LF || code === CR || code === COMMA || code === RIGHT_BRACKET || code === RIGHT_BRACE || code === HASH) break;
|
|
318
|
+
i += 1;
|
|
319
|
+
}
|
|
320
|
+
return i;
|
|
321
|
+
};
|
|
322
|
+
const OFFSET_DATE_TIME = /^([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?([Zz]|[+-][0-9]{2}:[0-9]{2})$/;
|
|
323
|
+
const LOCAL_DATE_TIME = /^([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?$/;
|
|
324
|
+
const LOCAL_DATE = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/;
|
|
325
|
+
const LOCAL_TIME = /^([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?$/;
|
|
326
|
+
/**
|
|
327
|
+
* Scan a non-string scalar value token. Stops at whitespace, newlines, `,`,
|
|
328
|
+
* `]`, `}` and `#` — with the G5 extension: a token that scanned as a full
|
|
329
|
+
* date followed by a single space and a digit continues through the time
|
|
330
|
+
* part, so `1979-05-27 07:32:00Z` is one token.
|
|
331
|
+
*/
|
|
332
|
+
const scanValueToken = (source, pos) => {
|
|
333
|
+
let end = scanTokenSpan(source, pos);
|
|
334
|
+
if (LOCAL_DATE.test(source.slice(pos, end)) && source.charCodeAt(end) === SPACE && isDigit(source.charCodeAt(end + 1))) end = scanTokenSpan(source, end + 1);
|
|
335
|
+
return {
|
|
336
|
+
value: source.slice(pos, end),
|
|
337
|
+
end
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
const INTEGER_DEC = /^[+-]?(?:0|[1-9](?:_?[0-9])*)$/;
|
|
341
|
+
const INTEGER_HEX = /^0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*$/;
|
|
342
|
+
const INTEGER_OCT = /^0o[0-7](?:_?[0-7])*$/;
|
|
343
|
+
const INTEGER_BIN = /^0b[01](?:_?[01])*$/;
|
|
344
|
+
const FLOAT = /^[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\.[0-9](?:_?[0-9])*(?:[eE][+-]?[0-9](?:_?[0-9])*)?|[eE][+-]?[0-9](?:_?[0-9])*)$/;
|
|
345
|
+
const FLOAT_SPECIAL = /^[+-]?(?:inf|nan)$/;
|
|
346
|
+
/** A token that starts number-shaped fails as InvalidNumber, not InvalidValue. */
|
|
347
|
+
const NUMERIC_LOOKING = /^[+-]?[0-9.]/;
|
|
348
|
+
const INT64_MIN = -(2n ** 63n);
|
|
349
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
350
|
+
const MAX_SAFE_BIG = 2n ** 53n - 1n;
|
|
351
|
+
/** Range-check against int64 and narrow to number when within 2^53-1. */
|
|
352
|
+
const narrowInteger = (big, token, offset) => {
|
|
353
|
+
if (big < INT64_MIN || big > INT64_MAX) return raise("IntegerOutOfRange", `${token} does not fit in a signed 64-bit integer`, offset, token.length);
|
|
354
|
+
return big >= -MAX_SAFE_BIG && big <= MAX_SAFE_BIG ? Number(big) : big;
|
|
355
|
+
};
|
|
356
|
+
const decodeDecimalInteger = (token, offset) => {
|
|
357
|
+
let digits = token.replace(/_/g, "");
|
|
358
|
+
if (digits.startsWith("+")) digits = digits.slice(1);
|
|
359
|
+
return narrowInteger(BigInt(digits), token, offset);
|
|
360
|
+
};
|
|
361
|
+
const decodePrefixedInteger = (token, offset) => narrowInteger(BigInt(token.replace(/_/g, "")), token, offset);
|
|
362
|
+
const MONTH_LENGTHS = [
|
|
363
|
+
31,
|
|
364
|
+
28,
|
|
365
|
+
31,
|
|
366
|
+
30,
|
|
367
|
+
31,
|
|
368
|
+
30,
|
|
369
|
+
31,
|
|
370
|
+
31,
|
|
371
|
+
30,
|
|
372
|
+
31,
|
|
373
|
+
30,
|
|
374
|
+
31
|
|
375
|
+
];
|
|
376
|
+
const isLeapYear = (year) => year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
377
|
+
const validateDate = (year, month, day, offset, length) => {
|
|
378
|
+
if (month < 1 || month > 12) raise("InvalidDateTime", `month ${month} is out of range 1-12`, offset, length);
|
|
379
|
+
const max = month === 2 && isLeapYear(year) ? 29 : MONTH_LENGTHS[month - 1] ?? 0;
|
|
380
|
+
if (day < 1 || day > max) raise("InvalidDateTime", `day ${day} does not exist in month ${month} of ${year}`, offset, length);
|
|
381
|
+
};
|
|
382
|
+
const validateTime = (hour, minute, second, offset, length) => {
|
|
383
|
+
if (hour > 23) raise("InvalidDateTime", `hour ${hour} is out of range 0-23`, offset, length);
|
|
384
|
+
if (minute > 59) raise("InvalidDateTime", `minute ${minute} is out of range 0-59`, offset, length);
|
|
385
|
+
if (second > 60) raise("InvalidDateTime", `second ${second} is out of range 0-60`, offset, length);
|
|
386
|
+
};
|
|
387
|
+
/** Right-pad or truncate fractional-second digits to exactly nine → nanoseconds. */
|
|
388
|
+
const decodeNanosecond = (fraction) => fraction === void 0 ? 0 : Number(`${fraction}000000000`.slice(0, 9));
|
|
389
|
+
/** Decode `Z` / `z` / `[+-]hh:mm` to signed minutes, validating hh and mm. */
|
|
390
|
+
const decodeOffsetMinutes = (text, offset, length) => {
|
|
391
|
+
if (text === "Z" || text === "z") return 0;
|
|
392
|
+
const hh = Number(text.slice(1, 3));
|
|
393
|
+
const mm = Number(text.slice(4, 6));
|
|
394
|
+
if (hh > 23) raise("InvalidDateTime", `offset hour ${hh} is out of range 0-23`, offset, length);
|
|
395
|
+
if (mm > 59) raise("InvalidDateTime", `offset minute ${mm} is out of range 0-59`, offset, length);
|
|
396
|
+
const total = hh * 60 + mm;
|
|
397
|
+
if (total === 0) return 0;
|
|
398
|
+
return text.charCodeAt(0) === HYPHEN ? -total : total;
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Classify a scanned value token per G4-G6: booleans, the four datetime
|
|
402
|
+
* shapes (validated against the Gregorian calendar and clock ranges before
|
|
403
|
+
* construction), integers across four radixes with int64 range checking and
|
|
404
|
+
* number/bigint narrowing, and floats including the special spellings.
|
|
405
|
+
*/
|
|
406
|
+
const classifyValueToken = (token, offset) => {
|
|
407
|
+
if (token === "true") return true;
|
|
408
|
+
if (token === "false") return false;
|
|
409
|
+
const length = Math.max(token.length, 1);
|
|
410
|
+
let match = OFFSET_DATE_TIME.exec(token);
|
|
411
|
+
if (match !== null) {
|
|
412
|
+
const [, y = "", mo = "", d = "", h = "", mi = "", s = "", fraction, offsetText = ""] = match;
|
|
413
|
+
const year = Number(y);
|
|
414
|
+
const month = Number(mo);
|
|
415
|
+
const day = Number(d);
|
|
416
|
+
const hour = Number(h);
|
|
417
|
+
const minute = Number(mi);
|
|
418
|
+
const second = Number(s);
|
|
419
|
+
validateDate(year, month, day, offset, length);
|
|
420
|
+
validateTime(hour, minute, second, offset, length);
|
|
421
|
+
const offsetMinutes = decodeOffsetMinutes(offsetText, offset, length);
|
|
422
|
+
return new TomlOffsetDateTime({
|
|
423
|
+
year,
|
|
424
|
+
month,
|
|
425
|
+
day,
|
|
426
|
+
hour,
|
|
427
|
+
minute,
|
|
428
|
+
second,
|
|
429
|
+
nanosecond: decodeNanosecond(fraction),
|
|
430
|
+
offsetMinutes
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
match = LOCAL_DATE_TIME.exec(token);
|
|
434
|
+
if (match !== null) {
|
|
435
|
+
const [, y = "", mo = "", d = "", h = "", mi = "", s = "", fraction] = match;
|
|
436
|
+
const year = Number(y);
|
|
437
|
+
const month = Number(mo);
|
|
438
|
+
const day = Number(d);
|
|
439
|
+
const hour = Number(h);
|
|
440
|
+
const minute = Number(mi);
|
|
441
|
+
const second = Number(s);
|
|
442
|
+
validateDate(year, month, day, offset, length);
|
|
443
|
+
validateTime(hour, minute, second, offset, length);
|
|
444
|
+
return new TomlLocalDateTime({
|
|
445
|
+
year,
|
|
446
|
+
month,
|
|
447
|
+
day,
|
|
448
|
+
hour,
|
|
449
|
+
minute,
|
|
450
|
+
second,
|
|
451
|
+
nanosecond: decodeNanosecond(fraction)
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
match = LOCAL_DATE.exec(token);
|
|
455
|
+
if (match !== null) {
|
|
456
|
+
const [, y = "", mo = "", d = ""] = match;
|
|
457
|
+
const year = Number(y);
|
|
458
|
+
const month = Number(mo);
|
|
459
|
+
const day = Number(d);
|
|
460
|
+
validateDate(year, month, day, offset, length);
|
|
461
|
+
return new TomlLocalDate({
|
|
462
|
+
year,
|
|
463
|
+
month,
|
|
464
|
+
day
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
match = LOCAL_TIME.exec(token);
|
|
468
|
+
if (match !== null) {
|
|
469
|
+
const [, h = "", mi = "", s = "", fraction] = match;
|
|
470
|
+
const hour = Number(h);
|
|
471
|
+
const minute = Number(mi);
|
|
472
|
+
const second = Number(s);
|
|
473
|
+
validateTime(hour, minute, second, offset, length);
|
|
474
|
+
return new TomlLocalTime({
|
|
475
|
+
hour,
|
|
476
|
+
minute,
|
|
477
|
+
second,
|
|
478
|
+
nanosecond: decodeNanosecond(fraction)
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (INTEGER_DEC.test(token)) return decodeDecimalInteger(token, offset);
|
|
482
|
+
if (INTEGER_HEX.test(token) || INTEGER_OCT.test(token) || INTEGER_BIN.test(token)) return decodePrefixedInteger(token, offset);
|
|
483
|
+
if (FLOAT.test(token)) return Number(token.replace(/_/g, ""));
|
|
484
|
+
if (FLOAT_SPECIAL.test(token)) {
|
|
485
|
+
if (token.endsWith("nan")) return NaN;
|
|
486
|
+
return token.charCodeAt(0) === HYPHEN ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
487
|
+
}
|
|
488
|
+
if (NUMERIC_LOOKING.test(token)) return raise("InvalidNumber", `${token} is not a valid TOML number or date-time`, offset, length);
|
|
489
|
+
return raise("InvalidValue", `${token} is not a valid TOML value`, offset, length);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
//#endregion
|
|
493
|
+
export { assertValidUnicode, classifyValueToken, isBareKeyChar, scanBareKey, scanBasicString, scanComment, scanLiteralString, scanMultilineBasicString, scanMultilineLiteralString, scanNewline, scanValueToken, scanWhitespace, skipBom };
|