UNPKG

44.6 kBJavaScriptView Raw
1'use strict'
2/* eslint-disable no-new-wrappers, no-eval, camelcase, operator-linebreak */
3module.exports = makeParserClass(require('./parser.js'))
4module.exports.makeParserClass = makeParserClass
5
6class TomlError extends Error {
7 constructor (msg) {
8 super(msg)
9 /* istanbul ignore next */
10 if (Error.captureStackTrace) Error.captureStackTrace(this, TomlError)
11 this.fromTOML = true
12 this.wrapped = null
13 }
14}
15TomlError.wrap = err => {
16 const terr = new TomlError(err.message)
17 terr.code = err.code
18 terr.wrapped = err
19 return terr
20}
21module.exports.TomlError = TomlError
22
23const createDateTime = require('./create-datetime.js')
24const createDateTimeFloat = require('./create-datetime-float.js')
25const createDate = require('./create-date.js')
26const createTime = require('./create-time.js')
27
28const CTRL_I = 0x09
29const CTRL_J = 0x0A
30const CTRL_M = 0x0D
31const CTRL_CHAR_BOUNDARY = 0x1F // the last non-character in the latin1 region of unicode, except DEL
32const CHAR_SP = 0x20
33const CHAR_QUOT = 0x22
34const CHAR_NUM = 0x23
35const CHAR_APOS = 0x27
36const CHAR_PLUS = 0x2B
37const CHAR_COMMA = 0x2C
38const CHAR_HYPHEN = 0x2D
39const CHAR_PERIOD = 0x2E
40const CHAR_0 = 0x30
41const CHAR_1 = 0x31
42const CHAR_7 = 0x37
43const CHAR_9 = 0x39
44const CHAR_COLON = 0x3A
45const CHAR_EQUALS = 0x3D
46const CHAR_A = 0x41
47const CHAR_E = 0x45
48const CHAR_F = 0x46
49const CHAR_T = 0x54
50const CHAR_U = 0x55
51const CHAR_Z = 0x5A
52const CHAR_LOWBAR = 0x5F
53const CHAR_a = 0x61
54const CHAR_b = 0x62
55const CHAR_e = 0x65
56const CHAR_f = 0x66
57const CHAR_i = 0x69
58const CHAR_l = 0x6C
59const CHAR_n = 0x6E
60const CHAR_o = 0x6F
61const CHAR_r = 0x72
62const CHAR_s = 0x73
63const CHAR_t = 0x74
64const CHAR_u = 0x75
65const CHAR_x = 0x78
66const CHAR_z = 0x7A
67const CHAR_LCUB = 0x7B
68const CHAR_RCUB = 0x7D
69const CHAR_LSQB = 0x5B
70const CHAR_BSOL = 0x5C
71const CHAR_RSQB = 0x5D
72const CHAR_DEL = 0x7F
73const SURROGATE_FIRST = 0xD800
74const SURROGATE_LAST = 0xDFFF
75
76const escapes = {
77 [CHAR_b]: '\x08',
78 [CHAR_t]: '\x09',
79 [CHAR_n]: '\x0a',
80 [CHAR_f]: '\x0c',
81 [CHAR_r]: '\x0d',
82 [CHAR_QUOT]: '\x22',
83 [CHAR_BSOL]: '\x5c'
84}
85
86function isDigit (cp) {
87 return cp >= CHAR_0 && cp <= CHAR_9
88}
89function isHexit (cp) {
90 return (cp >= CHAR_A && cp <= CHAR_F) || (cp >= CHAR_a && cp <= CHAR_f) || (cp >= CHAR_0 && cp <= CHAR_9)
91}
92function isBit (cp) {
93 return cp === CHAR_1 || cp === CHAR_0
94}
95function isOctit (cp) {
96 return (cp >= CHAR_0 && cp <= CHAR_7)
97}
98function isAlphaNumQuoteHyphen (cp) {
99 return (cp >= CHAR_A && cp <= CHAR_Z)
100 || (cp >= CHAR_a && cp <= CHAR_z)
101 || (cp >= CHAR_0 && cp <= CHAR_9)
102 || cp === CHAR_APOS
103 || cp === CHAR_QUOT
104 || cp === CHAR_LOWBAR
105 || cp === CHAR_HYPHEN
106}
107function isAlphaNumHyphen (cp) {
108 return (cp >= CHAR_A && cp <= CHAR_Z)
109 || (cp >= CHAR_a && cp <= CHAR_z)
110 || (cp >= CHAR_0 && cp <= CHAR_9)
111 || cp === CHAR_LOWBAR
112 || cp === CHAR_HYPHEN
113}
114const _type = Symbol('type')
115const _declared = Symbol('declared')
116
117const INLINE_TABLE = Symbol('inline-table')
118function InlineTable () {
119 return Object.defineProperties({}, {
120 [_type]: {value: INLINE_TABLE}
121 })
122}
123function isInlineTable (obj) {
124 if (obj === null || typeof (obj) !== 'object') return false
125 return obj[_type] === INLINE_TABLE
126}
127
128const TABLE = Symbol('table')
129function Table () {
130 return Object.defineProperties({}, {
131 [_type]: {value: TABLE},
132 [_declared]: {value: false, writable: true}
133 })
134}
135function isTable (obj) {
136 if (obj === null || typeof (obj) !== 'object') return false
137 return obj[_type] === TABLE
138}
139
140const _contentType = Symbol('content-type')
141const INLINE_LIST = Symbol('inline-list')
142function InlineList (type) {
143 return Object.defineProperties([], {
144 [_type]: {value: INLINE_LIST},
145 [_contentType]: {value: type}
146 })
147}
148function isInlineList (obj) {
149 if (obj === null || typeof (obj) !== 'object') return false
150 return obj[_type] === INLINE_LIST
151}
152
153const LIST = Symbol('list')
154function List () {
155 return Object.defineProperties([], {
156 [_type]: {value: LIST}
157 })
158}
159function isList (obj) {
160 if (obj === null || typeof (obj) !== 'object') return false
161 return obj[_type] === LIST
162}
163
164// in an eval, to let bundlers not slurp in a util proxy
165const utilInspect = eval(`require('util').inspect`)
166/* istanbul ignore next */
167const _inspect = (utilInspect && utilInspect.custom) || 'inspect'
168
169class BoxedBigInt {
170 constructor (value) {
171 try {
172 this.value = global.BigInt(value)
173 } catch (_) {
174 /* istanbul ignore next */
175 this.value = null
176 }
177 Object.defineProperty(this, _type, {value: INTEGER})
178 }
179 isNaN () {
180 return this.value === null
181 }
182 /* istanbul ignore next */
183 toString () {
184 return String(this.value)
185 }
186 /* istanbul ignore next */
187 [_inspect] () {
188 return `[BigInt: ${this.toString()}]}`
189 }
190 valueOf () {
191 return this.value
192 }
193}
194
195const INTEGER = Symbol('integer')
196function Integer (value) {
197 let num = Number(value)
198 // -0 is a float thing, not an int thing
199 if (Object.is(num, -0)) num = 0
200 /* istanbul ignore else */
201 if (global.BigInt && !Number.isSafeInteger(num)) {
202 return new BoxedBigInt(value)
203 } else {
204 /* istanbul ignore next */
205 return Object.defineProperties(new Number(num), {
206 isNaN: {value: function () { return isNaN(this) }},
207 [_type]: {value: INTEGER},
208 [_inspect]: {value: () => `[Integer: ${value}]`}
209 })
210 }
211}
212function isInteger (obj) {
213 if (obj === null || typeof (obj) !== 'object') return false
214 return obj[_type] === INTEGER
215}
216
217const FLOAT = Symbol('float')
218function Float (value) {
219 /* istanbul ignore next */
220 return Object.defineProperties(new Number(value), {
221 [_type]: {value: FLOAT},
222 [_inspect]: {value: () => `[Float: ${value}]`}
223 })
224}
225function isFloat (obj) {
226 if (obj === null || typeof (obj) !== 'object') return false
227 return obj[_type] === FLOAT
228}
229
230function tomlType (value) {
231 const type = typeof value
232 if (type === 'object') {
233 /* istanbul ignore if */
234 if (value === null) return 'null'
235 if (value instanceof Date) return 'datetime'
236 /* istanbul ignore else */
237 if (_type in value) {
238 switch (value[_type]) {
239 case INLINE_TABLE: return 'inline-table'
240 case INLINE_LIST: return 'inline-list'
241 /* istanbul ignore next */
242 case TABLE: return 'table'
243 /* istanbul ignore next */
244 case LIST: return 'list'
245 case FLOAT: return 'float'
246 case INTEGER: return 'integer'
247 }
248 }
249 }
250 return type
251}
252
253function makeParserClass (Parser) {
254 class TOMLParser extends Parser {
255 constructor () {
256 super()
257 this.ctx = this.obj = Table()
258 }
259
260 /* MATCH HELPER */
261 atEndOfWord () {
262 return this.char === CHAR_NUM || this.char === CTRL_I || this.char === CHAR_SP || this.atEndOfLine()
263 }
264 atEndOfLine () {
265 return this.char === Parser.END || this.char === CTRL_J || this.char === CTRL_M
266 }
267
268 parseStart () {
269 if (this.char === Parser.END) {
270 return null
271 } else if (this.char === CHAR_LSQB) {
272 return this.call(this.parseTableOrList)
273 } else if (this.char === CHAR_NUM) {
274 return this.call(this.parseComment)
275 } else if (this.char === CTRL_J || this.char === CHAR_SP || this.char === CTRL_I || this.char === CTRL_M) {
276 return null
277 } else if (isAlphaNumQuoteHyphen(this.char)) {
278 return this.callNow(this.parseAssignStatement)
279 } else {
280 throw this.error(new TomlError(`Unknown character "${this.char}"`))
281 }
282 }
283
284 // HELPER, this strips any whitespace and comments to the end of the line
285 // then RETURNS. Last state in a production.
286 parseWhitespaceToEOL () {
287 if (this.char === CHAR_SP || this.char === CTRL_I || this.char === CTRL_M) {
288 return null
289 } else if (this.char === CHAR_NUM) {
290 return this.goto(this.parseComment)
291 } else if (this.char === Parser.END || this.char === CTRL_J) {
292 return this.return()
293 } else {
294 throw this.error(new TomlError('Unexpected character, expected only whitespace or comments till end of line'))
295 }
296 }
297
298 /* ASSIGNMENT: key = value */
299 parseAssignStatement () {
300 return this.callNow(this.parseAssign, this.recordAssignStatement)
301 }
302 recordAssignStatement (kv) {
303 let target = this.ctx
304 let finalKey = kv.key.pop()
305 for (let kw of kv.key) {
306 if (kw in target && (!isTable(target[kw]) || target[kw][_declared])) {
307 throw this.error(new TomlError("Can't redefine existing key"))
308 }
309 target = target[kw] = target[kw] || Table()
310 }
311 if (finalKey in target) {
312 throw this.error(new TomlError("Can't redefine existing key"))
313 }
314 // unbox our numbers
315 if (isInteger(kv.value) || isFloat(kv.value)) {
316 target[finalKey] = kv.value.valueOf()
317 } else {
318 target[finalKey] = kv.value
319 }
320 return this.goto(this.parseWhitespaceToEOL)
321 }
322
323 /* ASSSIGNMENT expression, key = value possibly inside an inline table */
324 parseAssign () {
325 return this.callNow(this.parseKeyword, this.recordAssignKeyword)
326 }
327 recordAssignKeyword (key) {
328 if (this.state.resultTable) {
329 this.state.resultTable.push(key)
330 } else {
331 this.state.resultTable = [key]
332 }
333 return this.goto(this.parseAssignKeywordPreDot)
334 }
335 parseAssignKeywordPreDot () {
336 if (this.char === CHAR_PERIOD) {
337 return this.next(this.parseAssignKeywordPostDot)
338 } else if (this.char !== CHAR_SP && this.char !== CTRL_I) {
339 return this.goto(this.parseAssignEqual)
340 }
341 }
342 parseAssignKeywordPostDot () {
343 if (this.char !== CHAR_SP && this.char !== CTRL_I) {
344 return this.callNow(this.parseKeyword, this.recordAssignKeyword)
345 }
346 }
347
348 parseAssignEqual () {
349 if (this.char === CHAR_EQUALS) {
350 return this.next(this.parseAssignPreValue)
351 } else {
352 throw this.error(new TomlError('Invalid character, expected "="'))
353 }
354 }
355 parseAssignPreValue () {
356 if (this.char === CHAR_SP || this.char === CTRL_I) {
357 return null
358 } else {
359 return this.callNow(this.parseValue, this.recordAssignValue)
360 }
361 }
362 recordAssignValue (value) {
363 return this.returnNow({key: this.state.resultTable, value: value})
364 }
365
366 /* COMMENTS: #...eol */
367 parseComment () {
368 do {
369 if (this.char === Parser.END || this.char === CTRL_J) {
370 return this.return()
371 }
372 } while (this.nextChar())
373 }
374
375 /* TABLES AND LISTS, [foo] and [[foo]] */
376 parseTableOrList () {
377 if (this.char === CHAR_LSQB) {
378 this.next(this.parseList)
379 } else {
380 return this.goto(this.parseTable)
381 }
382 }
383
384 /* TABLE [foo.bar.baz] */
385 parseTable () {
386 this.ctx = this.obj
387 return this.goto(this.parseTableNext)
388 }
389 parseTableNext () {
390 if (this.char === CHAR_SP || this.char === CTRL_I) {
391 return null
392 } else {
393 return this.callNow(this.parseKeyword, this.parseTableMore)
394 }
395 }
396 parseTableMore (keyword) {
397 if (this.char === CHAR_SP || this.char === CTRL_I) {
398 return null
399 } else if (this.char === CHAR_RSQB) {
400 if (keyword in this.ctx && (!isTable(this.ctx[keyword]) || this.ctx[keyword][_declared])) {
401 throw this.error(new TomlError("Can't redefine existing key"))
402 } else {
403 this.ctx = this.ctx[keyword] = this.ctx[keyword] || Table()
404 this.ctx[_declared] = true
405 }
406 return this.next(this.parseWhitespaceToEOL)
407 } else if (this.char === CHAR_PERIOD) {
408 if (!(keyword in this.ctx)) {
409 this.ctx = this.ctx[keyword] = Table()
410 } else if (isTable(this.ctx[keyword])) {
411 this.ctx = this.ctx[keyword]
412 } else if (isList(this.ctx[keyword])) {
413 this.ctx = this.ctx[keyword][this.ctx[keyword].length - 1]
414 } else {
415 throw this.error(new TomlError("Can't redefine existing key"))
416 }
417 return this.next(this.parseTableNext)
418 } else {
419 throw this.error(new TomlError('Unexpected character, expected whitespace, . or ]'))
420 }
421 }
422
423 /* LIST [[a.b.c]] */
424 parseList () {
425 this.ctx = this.obj
426 return this.goto(this.parseListNext)
427 }
428 parseListNext () {
429 if (this.char === CHAR_SP || this.char === CTRL_I) {
430 return null
431 } else {
432 return this.callNow(this.parseKeyword, this.parseListMore)
433 }
434 }
435 parseListMore (keyword) {
436 if (this.char === CHAR_SP || this.char === CTRL_I) {
437 return null
438 } else if (this.char === CHAR_RSQB) {
439 if (!(keyword in this.ctx)) {
440 this.ctx[keyword] = List()
441 }
442 if (isInlineList(this.ctx[keyword])) {
443 throw this.error(new TomlError("Can't extend an inline array"))
444 } else if (isList(this.ctx[keyword])) {
445 const next = Table()
446 this.ctx[keyword].push(next)
447 this.ctx = next
448 } else {
449 throw this.error(new TomlError("Can't redefine an existing key"))
450 }
451 return this.next(this.parseListEnd)
452 } else if (this.char === CHAR_PERIOD) {
453 if (!(keyword in this.ctx)) {
454 this.ctx = this.ctx[keyword] = Table()
455 } else if (isInlineList(this.ctx[keyword])) {
456 throw this.error(new TomlError("Can't extend an inline array"))
457 } else if (isInlineTable(this.ctx[keyword])) {
458 throw this.error(new TomlError("Can't extend an inline table"))
459 } else if (isList(this.ctx[keyword])) {
460 this.ctx = this.ctx[keyword][this.ctx[keyword].length - 1]
461 } else if (isTable(this.ctx[keyword])) {
462 this.ctx = this.ctx[keyword]
463 } else {
464 throw this.error(new TomlError("Can't redefine an existing key"))
465 }
466 return this.next(this.parseListNext)
467 } else {
468 throw this.error(new TomlError('Unexpected character, expected whitespace, . or ]'))
469 }
470 }
471 parseListEnd (keyword) {
472 if (this.char === CHAR_RSQB) {
473 return this.next(this.parseWhitespaceToEOL)
474 } else {
475 throw this.error(new TomlError('Unexpected character, expected whitespace, . or ]'))
476 }
477 }
478
479 /* VALUE string, number, boolean, inline list, inline object */
480 parseValue () {
481 if (this.char === Parser.END) {
482 throw this.error(new TomlError('Key without value'))
483 } else if (this.char === CHAR_QUOT) {
484 return this.next(this.parseDoubleString)
485 } if (this.char === CHAR_APOS) {
486 return this.next(this.parseSingleString)
487 } else if (this.char === CHAR_HYPHEN || this.char === CHAR_PLUS) {
488 return this.goto(this.parseNumberSign)
489 } else if (this.char === CHAR_i) {
490 return this.next(this.parseInf)
491 } else if (this.char === CHAR_n) {
492 return this.next(this.parseNan)
493 } else if (isDigit(this.char)) {
494 return this.goto(this.parseNumberOrDateTime)
495 } else if (this.char === CHAR_t || this.char === CHAR_f) {
496 return this.goto(this.parseBoolean)
497 } else if (this.char === CHAR_LSQB) {
498 return this.call(this.parseInlineList, this.recordValue)
499 } else if (this.char === CHAR_LCUB) {
500 return this.call(this.parseInlineTable, this.recordValue)
501 } else {
502 throw this.error(new TomlError('Unexpected character, expecting string, number, datetime, boolean, inline array or inline table'))
503 }
504 }
505 recordValue (value) {
506 return this.returnNow(value)
507 }
508
509 parseInf () {
510 if (this.char === CHAR_n) {
511 return this.next(this.parseInf2)
512 } else {
513 throw this.error(new TomlError('Unexpected character, expected "inf", "+inf" or "-inf"'))
514 }
515 }
516 parseInf2 () {
517 if (this.char === CHAR_f) {
518 if (this.state.buf === '-') {
519 return this.return(-Infinity)
520 } else {
521 return this.return(Infinity)
522 }
523 } else {
524 throw this.error(new TomlError('Unexpected character, expected "inf", "+inf" or "-inf"'))
525 }
526 }
527
528 parseNan () {
529 if (this.char === CHAR_a) {
530 return this.next(this.parseNan2)
531 } else {
532 throw this.error(new TomlError('Unexpected character, expected "nan"'))
533 }
534 }
535 parseNan2 () {
536 if (this.char === CHAR_n) {
537 return this.return(NaN)
538 } else {
539 throw this.error(new TomlError('Unexpected character, expected "nan"'))
540 }
541 }
542
543 /* KEYS, barewords or basic, literal, or dotted */
544 parseKeyword () {
545 if (this.char === CHAR_QUOT) {
546 return this.next(this.parseBasicString)
547 } else if (this.char === CHAR_APOS) {
548 return this.next(this.parseLiteralString)
549 } else {
550 return this.goto(this.parseBareKey)
551 }
552 }
553
554 /* KEYS: barewords */
555 parseBareKey () {
556 do {
557 if (this.char === Parser.END) {
558 throw this.error(new TomlError('Key ended without value'))
559 } else if (isAlphaNumHyphen(this.char)) {
560 this.consume()
561 } else if (this.state.buf.length === 0) {
562 throw this.error(new TomlError('Empty bare keys are not allowed'))
563 } else {
564 return this.returnNow()
565 }
566 } while (this.nextChar())
567 }
568
569 /* STRINGS, single quoted (literal) */
570 parseSingleString () {
571 if (this.char === CHAR_APOS) {
572 return this.next(this.parseLiteralMultiStringMaybe)
573 } else {
574 return this.goto(this.parseLiteralString)
575 }
576 }
577 parseLiteralString () {
578 do {
579 if (this.char === CHAR_APOS) {
580 return this.return()
581 } else if (this.atEndOfLine()) {
582 throw this.error(new TomlError('Unterminated string'))
583 } else if (this.char === CHAR_DEL || (this.char <= CTRL_CHAR_BOUNDARY && this.char !== CTRL_I)) {
584 throw this.errorControlCharInString()
585 } else {
586 this.consume()
587 }
588 } while (this.nextChar())
589 }
590 parseLiteralMultiStringMaybe () {
591 if (this.char === CHAR_APOS) {
592 return this.next(this.parseLiteralMultiString)
593 } else {
594 return this.returnNow()
595 }
596 }
597 parseLiteralMultiString () {
598 if (this.char === CTRL_M) {
599 return null
600 } else if (this.char === CTRL_J) {
601 return this.next(this.parseLiteralMultiStringContent)
602 } else {
603 return this.goto(this.parseLiteralMultiStringContent)
604 }
605 }
606 parseLiteralMultiStringContent () {
607 do {
608 if (this.char === CHAR_APOS) {
609 return this.next(this.parseLiteralMultiEnd)
610 } else if (this.char === Parser.END) {
611 throw this.error(new TomlError('Unterminated multi-line string'))
612 } else if (this.char === CHAR_DEL || (this.char <= CTRL_CHAR_BOUNDARY && this.char !== CTRL_I && this.char !== CTRL_J && this.char !== CTRL_M)) {
613 throw this.errorControlCharInString()
614 } else {
615 this.consume()
616 }
617 } while (this.nextChar())
618 }
619 parseLiteralMultiEnd () {
620 if (this.char === CHAR_APOS) {
621 return this.next(this.parseLiteralMultiEnd2)
622 } else {
623 this.state.buf += "'"
624 return this.goto(this.parseLiteralMultiStringContent)
625 }
626 }
627 parseLiteralMultiEnd2 () {
628 if (this.char === CHAR_APOS) {
629 return this.return()
630 } else {
631 this.state.buf += "''"
632 return this.goto(this.parseLiteralMultiStringContent)
633 }
634 }
635
636 /* STRINGS double quoted */
637 parseDoubleString () {
638 if (this.char === CHAR_QUOT) {
639 return this.next(this.parseMultiStringMaybe)
640 } else {
641 return this.goto(this.parseBasicString)
642 }
643 }
644 parseBasicString () {
645 do {
646 if (this.char === CHAR_BSOL) {
647 return this.call(this.parseEscape, this.recordEscapeReplacement)
648 } else if (this.char === CHAR_QUOT) {
649 return this.return()
650 } else if (this.atEndOfLine()) {
651 throw this.error(new TomlError('Unterminated string'))
652 } else if (this.char === CHAR_DEL || (this.char <= CTRL_CHAR_BOUNDARY && this.char !== CTRL_I)) {
653 throw this.errorControlCharInString()
654 } else {
655 this.consume()
656 }
657 } while (this.nextChar())
658 }
659 recordEscapeReplacement (replacement) {
660 this.state.buf += replacement
661 return this.goto(this.parseBasicString)
662 }
663 parseMultiStringMaybe () {
664 if (this.char === CHAR_QUOT) {
665 return this.next(this.parseMultiString)
666 } else {
667 return this.returnNow()
668 }
669 }
670 parseMultiString () {
671 if (this.char === CTRL_M) {
672 return null
673 } else if (this.char === CTRL_J) {
674 return this.next(this.parseMultiStringContent)
675 } else {
676 return this.goto(this.parseMultiStringContent)
677 }
678 }
679 parseMultiStringContent () {
680 do {
681 if (this.char === CHAR_BSOL) {
682 return this.call(this.parseMultiEscape, this.recordMultiEscapeReplacement)
683 } else if (this.char === CHAR_QUOT) {
684 return this.next(this.parseMultiEnd)
685 } else if (this.char === Parser.END) {
686 throw this.error(new TomlError('Unterminated multi-line string'))
687 } else if (this.char === CHAR_DEL || (this.char <= CTRL_CHAR_BOUNDARY && this.char !== CTRL_I && this.char !== CTRL_J && this.char !== CTRL_M)) {
688 throw this.errorControlCharInString()
689 } else {
690 this.consume()
691 }
692 } while (this.nextChar())
693 }
694 errorControlCharInString () {
695 let displayCode = '\\u00'
696 if (this.char < 16) {
697 displayCode += '0'
698 }
699 displayCode += this.char.toString(16)
700
701 return this.error(new TomlError(`Control characters (codes < 0x1f and 0x7f) are not allowed in strings, use ${displayCode} instead`))
702 }
703 recordMultiEscapeReplacement (replacement) {
704 this.state.buf += replacement
705 return this.goto(this.parseMultiStringContent)
706 }
707 parseMultiEnd () {
708 if (this.char === CHAR_QUOT) {
709 return this.next(this.parseMultiEnd2)
710 } else {
711 this.state.buf += '"'
712 return this.goto(this.parseMultiStringContent)
713 }
714 }
715 parseMultiEnd2 () {
716 if (this.char === CHAR_QUOT) {
717 return this.return()
718 } else {
719 this.state.buf += '""'
720 return this.goto(this.parseMultiStringContent)
721 }
722 }
723 parseMultiEscape () {
724 if (this.char === CTRL_M || this.char === CTRL_J) {
725 return this.next(this.parseMultiTrim)
726 } else if (this.char === CHAR_SP || this.char === CTRL_I) {
727 return this.next(this.parsePreMultiTrim)
728 } else {
729 return this.goto(this.parseEscape)
730 }
731 }
732 parsePreMultiTrim () {
733 if (this.char === CHAR_SP || this.char === CTRL_I) {
734 return null
735 } else if (this.char === CTRL_M || this.char === CTRL_J) {
736 return this.next(this.parseMultiTrim)
737 } else {
738 throw this.error(new TomlError("Can't escape whitespace"))
739 }
740 }
741 parseMultiTrim () {
742 // explicitly whitespace here, END should follow the same path as chars
743 if (this.char === CTRL_J || this.char === CHAR_SP || this.char === CTRL_I || this.char === CTRL_M) {
744 return null
745 } else {
746 return this.returnNow()
747 }
748 }
749 parseEscape () {
750 if (this.char in escapes) {
751 return this.return(escapes[this.char])
752 } else if (this.char === CHAR_u) {
753 return this.call(this.parseSmallUnicode, this.parseUnicodeReturn)
754 } else if (this.char === CHAR_U) {
755 return this.call(this.parseLargeUnicode, this.parseUnicodeReturn)
756 } else {
757 throw this.error(new TomlError('Unknown escape character: ' + this.char))
758 }
759 }
760 parseUnicodeReturn (char) {
761 try {
762 const codePoint = parseInt(char, 16)
763 if (codePoint >= SURROGATE_FIRST && codePoint <= SURROGATE_LAST) {
764 throw this.error(new TomlError('Invalid unicode, character in range 0xD800 - 0xDFFF is reserved'))
765 }
766 return this.returnNow(String.fromCodePoint(codePoint))
767 } catch (ex) {
768 throw this.error(TomlError.wrap(ex))
769 }
770 }
771 parseSmallUnicode () {
772 if (!isHexit(this.char)) {
773 throw this.error(new TomlError('Invalid character in unicode sequence, expected hex'))
774 } else {
775 this.consume()
776 if (this.state.buf.length >= 4) return this.return()
777 }
778 }
779 parseLargeUnicode () {
780 if (!isHexit(this.char)) {
781 throw this.error(new TomlError('Invalid character in unicode sequence, expected hex'))
782 } else {
783 this.consume()
784 if (this.state.buf.length >= 8) return this.return()
785 }
786 }
787
788 /* NUMBERS */
789 parseNumberSign () {
790 this.consume()
791 return this.next(this.parseMaybeSignedInfOrNan)
792 }
793 parseMaybeSignedInfOrNan () {
794 if (this.char === CHAR_i) {
795 return this.next(this.parseInf)
796 } else if (this.char === CHAR_n) {
797 return this.next(this.parseNan)
798 } else {
799 return this.callNow(this.parseNoUnder, this.parseNumberIntegerStart)
800 }
801 }
802 parseNumberIntegerStart () {
803 if (this.char === CHAR_0) {
804 this.consume()
805 return this.next(this.parseNumberIntegerExponentOrDecimal)
806 } else {
807 return this.goto(this.parseNumberInteger)
808 }
809 }
810 parseNumberIntegerExponentOrDecimal () {
811 if (this.char === CHAR_PERIOD) {
812 this.consume()
813 return this.call(this.parseNoUnder, this.parseNumberFloat)
814 } else if (this.char === CHAR_E || this.char === CHAR_e) {
815 this.consume()
816 return this.next(this.parseNumberExponentSign)
817 } else {
818 return this.returnNow(Integer(this.state.buf))
819 }
820 }
821 parseNumberInteger () {
822 if (isDigit(this.char)) {
823 this.consume()
824 } else if (this.char === CHAR_LOWBAR) {
825 return this.call(this.parseNoUnder)
826 } else if (this.char === CHAR_E || this.char === CHAR_e) {
827 this.consume()
828 return this.next(this.parseNumberExponentSign)
829 } else if (this.char === CHAR_PERIOD) {
830 this.consume()
831 return this.call(this.parseNoUnder, this.parseNumberFloat)
832 } else {
833 const result = Integer(this.state.buf)
834 /* istanbul ignore if */
835 if (result.isNaN()) {
836 throw this.error(new TomlError('Invalid number'))
837 } else {
838 return this.returnNow(result)
839 }
840 }
841 }
842 parseNoUnder () {
843 if (this.char === CHAR_LOWBAR || this.char === CHAR_PERIOD || this.char === CHAR_E || this.char === CHAR_e) {
844 throw this.error(new TomlError('Unexpected character, expected digit'))
845 } else if (this.atEndOfWord()) {
846 throw this.error(new TomlError('Incomplete number'))
847 }
848 return this.returnNow()
849 }
850 parseNumberFloat () {
851 if (this.char === CHAR_LOWBAR) {
852 return this.call(this.parseNoUnder, this.parseNumberFloat)
853 } else if (isDigit(this.char)) {
854 this.consume()
855 } else if (this.char === CHAR_E || this.char === CHAR_e) {
856 this.consume()
857 return this.next(this.parseNumberExponentSign)
858 } else {
859 return this.returnNow(Float(this.state.buf))
860 }
861 }
862 parseNumberExponentSign () {
863 if (isDigit(this.char)) {
864 return this.goto(this.parseNumberExponent)
865 } else if (this.char === CHAR_HYPHEN || this.char === CHAR_PLUS) {
866 this.consume()
867 this.call(this.parseNoUnder, this.parseNumberExponent)
868 } else {
869 throw this.error(new TomlError('Unexpected character, expected -, + or digit'))
870 }
871 }
872 parseNumberExponent () {
873 if (isDigit(this.char)) {
874 this.consume()
875 } else if (this.char === CHAR_LOWBAR) {
876 return this.call(this.parseNoUnder)
877 } else {
878 return this.returnNow(Float(this.state.buf))
879 }
880 }
881
882 /* NUMBERS or DATETIMES */
883 parseNumberOrDateTime () {
884 if (this.char === CHAR_0) {
885 this.consume()
886 return this.next(this.parseNumberBaseOrDateTime)
887 } else {
888 return this.goto(this.parseNumberOrDateTimeOnly)
889 }
890 }
891 parseNumberOrDateTimeOnly () {
892 // note, if two zeros are in a row then it MUST be a date
893 if (this.char === CHAR_LOWBAR) {
894 return this.call(this.parseNoUnder, this.parseNumberInteger)
895 } else if (isDigit(this.char)) {
896 this.consume()
897 if (this.state.buf.length > 4) this.next(this.parseNumberInteger)
898 } else if (this.char === CHAR_E || this.char === CHAR_e) {
899 this.consume()
900 return this.next(this.parseNumberExponentSign)
901 } else if (this.char === CHAR_PERIOD) {
902 this.consume()
903 return this.call(this.parseNoUnder, this.parseNumberFloat)
904 } else if (this.char === CHAR_HYPHEN) {
905 return this.goto(this.parseDateTime)
906 } else if (this.char === CHAR_COLON) {
907 return this.goto(this.parseOnlyTimeHour)
908 } else {
909 return this.returnNow(Integer(this.state.buf))
910 }
911 }
912 parseDateTimeOnly () {
913 if (this.state.buf.length < 4) {
914 if (isDigit(this.char)) {
915 return this.consume()
916 } else if (this.char === CHAR_COLON) {
917 return this.goto(this.parseOnlyTimeHour)
918 } else {
919 throw this.error(new TomlError('Expected digit while parsing year part of a date'))
920 }
921 } else {
922 if (this.char === CHAR_HYPHEN) {
923 return this.goto(this.parseDateTime)
924 } else {
925 throw this.error(new TomlError('Expected hyphen (-) while parsing year part of date'))
926 }
927 }
928 }
929 parseNumberBaseOrDateTime () {
930 if (this.char === CHAR_b) {
931 this.consume()
932 return this.call(this.parseNoUnder, this.parseIntegerBin)
933 } else if (this.char === CHAR_o) {
934 this.consume()
935 return this.call(this.parseNoUnder, this.parseIntegerOct)
936 } else if (this.char === CHAR_x) {
937 this.consume()
938 return this.call(this.parseNoUnder, this.parseIntegerHex)
939 } else if (this.char === CHAR_PERIOD) {
940 return this.goto(this.parseNumberInteger)
941 } else if (isDigit(this.char)) {
942 return this.goto(this.parseDateTimeOnly)
943 } else {
944 return this.returnNow(Integer(this.state.buf))
945 }
946 }
947 parseIntegerHex () {
948 if (isHexit(this.char)) {
949 this.consume()
950 } else if (this.char === CHAR_LOWBAR) {
951 return this.call(this.parseNoUnder)
952 } else {
953 const result = Integer(this.state.buf)
954 /* istanbul ignore if */
955 if (result.isNaN()) {
956 throw this.error(new TomlError('Invalid number'))
957 } else {
958 return this.returnNow(result)
959 }
960 }
961 }
962 parseIntegerOct () {
963 if (isOctit(this.char)) {
964 this.consume()
965 } else if (this.char === CHAR_LOWBAR) {
966 return this.call(this.parseNoUnder)
967 } else {
968 const result = Integer(this.state.buf)
969 /* istanbul ignore if */
970 if (result.isNaN()) {
971 throw this.error(new TomlError('Invalid number'))
972 } else {
973 return this.returnNow(result)
974 }
975 }
976 }
977 parseIntegerBin () {
978 if (isBit(this.char)) {
979 this.consume()
980 } else if (this.char === CHAR_LOWBAR) {
981 return this.call(this.parseNoUnder)
982 } else {
983 const result = Integer(this.state.buf)
984 /* istanbul ignore if */
985 if (result.isNaN()) {
986 throw this.error(new TomlError('Invalid number'))
987 } else {
988 return this.returnNow(result)
989 }
990 }
991 }
992
993 /* DATETIME */
994 parseDateTime () {
995 // we enter here having just consumed the year and about to consume the hyphen
996 if (this.state.buf.length < 4) {
997 throw this.error(new TomlError('Years less than 1000 must be zero padded to four characters'))
998 }
999 this.state.result = this.state.buf
1000 this.state.buf = ''
1001 return this.next(this.parseDateMonth)
1002 }
1003 parseDateMonth () {
1004 if (this.char === CHAR_HYPHEN) {
1005 if (this.state.buf.length < 2) {
1006 throw this.error(new TomlError('Months less than 10 must be zero padded to two characters'))
1007 }
1008 this.state.result += '-' + this.state.buf
1009 this.state.buf = ''
1010 return this.next(this.parseDateDay)
1011 } else if (isDigit(this.char)) {
1012 this.consume()
1013 } else {
1014 throw this.error(new TomlError('Incomplete datetime'))
1015 }
1016 }
1017 parseDateDay () {
1018 if (this.char === CHAR_T || this.char === CHAR_SP) {
1019 if (this.state.buf.length < 2) {
1020 throw this.error(new TomlError('Days less than 10 must be zero padded to two characters'))
1021 }
1022 this.state.result += '-' + this.state.buf
1023 this.state.buf = ''
1024 return this.next(this.parseStartTimeHour)
1025 } else if (this.atEndOfWord()) {
1026 return this.return(createDate(this.state.result + '-' + this.state.buf))
1027 } else if (isDigit(this.char)) {
1028 this.consume()
1029 } else {
1030 throw this.error(new TomlError('Incomplete datetime'))
1031 }
1032 }
1033 parseStartTimeHour () {
1034 if (this.atEndOfWord()) {
1035 return this.returnNow(createDate(this.state.result))
1036 } else {
1037 return this.goto(this.parseTimeHour)
1038 }
1039 }
1040 parseTimeHour () {
1041 if (this.char === CHAR_COLON) {
1042 if (this.state.buf.length < 2) {
1043 throw this.error(new TomlError('Hours less than 10 must be zero padded to two characters'))
1044 }
1045 this.state.result += 'T' + this.state.buf
1046 this.state.buf = ''
1047 return this.next(this.parseTimeMin)
1048 } else if (isDigit(this.char)) {
1049 this.consume()
1050 } else {
1051 throw this.error(new TomlError('Incomplete datetime'))
1052 }
1053 }
1054 parseTimeMin () {
1055 if (this.state.buf.length < 2 && isDigit(this.char)) {
1056 this.consume()
1057 } else if (this.state.buf.length === 2 && this.char === CHAR_COLON) {
1058 this.state.result += ':' + this.state.buf
1059 this.state.buf = ''
1060 return this.next(this.parseTimeSec)
1061 } else {
1062 throw this.error(new TomlError('Incomplete datetime'))
1063 }
1064 }
1065 parseTimeSec () {
1066 if (isDigit(this.char)) {
1067 this.consume()
1068 if (this.state.buf.length === 2) {
1069 this.state.result += ':' + this.state.buf
1070 this.state.buf = ''
1071 return this.next(this.parseTimeZoneOrFraction)
1072 }
1073 } else {
1074 throw this.error(new TomlError('Incomplete datetime'))
1075 }
1076 }
1077
1078 parseOnlyTimeHour () {
1079 /* istanbul ignore else */
1080 if (this.char === CHAR_COLON) {
1081 if (this.state.buf.length < 2) {
1082 throw this.error(new TomlError('Hours less than 10 must be zero padded to two characters'))
1083 }
1084 this.state.result = this.state.buf
1085 this.state.buf = ''
1086 return this.next(this.parseOnlyTimeMin)
1087 } else {
1088 throw this.error(new TomlError('Incomplete time'))
1089 }
1090 }
1091 parseOnlyTimeMin () {
1092 if (this.state.buf.length < 2 && isDigit(this.char)) {
1093 this.consume()
1094 } else if (this.state.buf.length === 2 && this.char === CHAR_COLON) {
1095 this.state.result += ':' + this.state.buf
1096 this.state.buf = ''
1097 return this.next(this.parseOnlyTimeSec)
1098 } else {
1099 throw this.error(new TomlError('Incomplete time'))
1100 }
1101 }
1102 parseOnlyTimeSec () {
1103 if (isDigit(this.char)) {
1104 this.consume()
1105 if (this.state.buf.length === 2) {
1106 return this.next(this.parseOnlyTimeFractionMaybe)
1107 }
1108 } else {
1109 throw this.error(new TomlError('Incomplete time'))
1110 }
1111 }
1112 parseOnlyTimeFractionMaybe () {
1113 this.state.result += ':' + this.state.buf
1114 if (this.char === CHAR_PERIOD) {
1115 this.state.buf = ''
1116 this.next(this.parseOnlyTimeFraction)
1117 } else {
1118 return this.return(createTime(this.state.result))
1119 }
1120 }
1121 parseOnlyTimeFraction () {
1122 if (isDigit(this.char)) {
1123 this.consume()
1124 } else if (this.atEndOfWord()) {
1125 if (this.state.buf.length === 0) throw this.error(new TomlError('Expected digit in milliseconds'))
1126 return this.returnNow(createTime(this.state.result + '.' + this.state.buf))
1127 } else {
1128 throw this.error(new TomlError('Unexpected character in datetime, expected period (.), minus (-), plus (+) or Z'))
1129 }
1130 }
1131
1132 parseTimeZoneOrFraction () {
1133 if (this.char === CHAR_PERIOD) {
1134 this.consume()
1135 this.next(this.parseDateTimeFraction)
1136 } else if (this.char === CHAR_HYPHEN || this.char === CHAR_PLUS) {
1137 this.consume()
1138 this.next(this.parseTimeZoneHour)
1139 } else if (this.char === CHAR_Z) {
1140 this.consume()
1141 return this.return(createDateTime(this.state.result + this.state.buf))
1142 } else if (this.atEndOfWord()) {
1143 return this.returnNow(createDateTimeFloat(this.state.result + this.state.buf))
1144 } else {
1145 throw this.error(new TomlError('Unexpected character in datetime, expected period (.), minus (-), plus (+) or Z'))
1146 }
1147 }
1148 parseDateTimeFraction () {
1149 if (isDigit(this.char)) {
1150 this.consume()
1151 } else if (this.state.buf.length === 1) {
1152 throw this.error(new TomlError('Expected digit in milliseconds'))
1153 } else if (this.char === CHAR_HYPHEN || this.char === CHAR_PLUS) {
1154 this.consume()
1155 this.next(this.parseTimeZoneHour)
1156 } else if (this.char === CHAR_Z) {
1157 this.consume()
1158 return this.return(createDateTime(this.state.result + this.state.buf))
1159 } else if (this.atEndOfWord()) {
1160 return this.returnNow(createDateTimeFloat(this.state.result + this.state.buf))
1161 } else {
1162 throw this.error(new TomlError('Unexpected character in datetime, expected period (.), minus (-), plus (+) or Z'))
1163 }
1164 }
1165 parseTimeZoneHour () {
1166 if (isDigit(this.char)) {
1167 this.consume()
1168 // FIXME: No more regexps
1169 if (/\d\d$/.test(this.state.buf)) return this.next(this.parseTimeZoneSep)
1170 } else {
1171 throw this.error(new TomlError('Unexpected character in datetime, expected digit'))
1172 }
1173 }
1174 parseTimeZoneSep () {
1175 if (this.char === CHAR_COLON) {
1176 this.consume()
1177 this.next(this.parseTimeZoneMin)
1178 } else {
1179 throw this.error(new TomlError('Unexpected character in datetime, expected colon'))
1180 }
1181 }
1182 parseTimeZoneMin () {
1183 if (isDigit(this.char)) {
1184 this.consume()
1185 if (/\d\d$/.test(this.state.buf)) return this.return(createDateTime(this.state.result + this.state.buf))
1186 } else {
1187 throw this.error(new TomlError('Unexpected character in datetime, expected digit'))
1188 }
1189 }
1190
1191 /* BOOLEAN */
1192 parseBoolean () {
1193 /* istanbul ignore else */
1194 if (this.char === CHAR_t) {
1195 this.consume()
1196 return this.next(this.parseTrue_r)
1197 } else if (this.char === CHAR_f) {
1198 this.consume()
1199 return this.next(this.parseFalse_a)
1200 }
1201 }
1202 parseTrue_r () {
1203 if (this.char === CHAR_r) {
1204 this.consume()
1205 return this.next(this.parseTrue_u)
1206 } else {
1207 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1208 }
1209 }
1210 parseTrue_u () {
1211 if (this.char === CHAR_u) {
1212 this.consume()
1213 return this.next(this.parseTrue_e)
1214 } else {
1215 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1216 }
1217 }
1218 parseTrue_e () {
1219 if (this.char === CHAR_e) {
1220 return this.return(true)
1221 } else {
1222 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1223 }
1224 }
1225
1226 parseFalse_a () {
1227 if (this.char === CHAR_a) {
1228 this.consume()
1229 return this.next(this.parseFalse_l)
1230 } else {
1231 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1232 }
1233 }
1234
1235 parseFalse_l () {
1236 if (this.char === CHAR_l) {
1237 this.consume()
1238 return this.next(this.parseFalse_s)
1239 } else {
1240 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1241 }
1242 }
1243
1244 parseFalse_s () {
1245 if (this.char === CHAR_s) {
1246 this.consume()
1247 return this.next(this.parseFalse_e)
1248 } else {
1249 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1250 }
1251 }
1252
1253 parseFalse_e () {
1254 if (this.char === CHAR_e) {
1255 return this.return(false)
1256 } else {
1257 throw this.error(new TomlError('Invalid boolean, expected true or false'))
1258 }
1259 }
1260
1261 /* INLINE LISTS */
1262 parseInlineList () {
1263 if (this.char === CHAR_SP || this.char === CTRL_I || this.char === CTRL_M || this.char === CTRL_J) {
1264 return null
1265 } else if (this.char === Parser.END) {
1266 throw this.error(new TomlError('Unterminated inline array'))
1267 } else if (this.char === CHAR_NUM) {
1268 return this.call(this.parseComment)
1269 } else if (this.char === CHAR_RSQB) {
1270 return this.return(this.state.resultArr || InlineList())
1271 } else {
1272 return this.callNow(this.parseValue, this.recordInlineListValue)
1273 }
1274 }
1275 recordInlineListValue (value) {
1276 if (this.state.resultArr) {
1277 const listType = this.state.resultArr[_contentType]
1278 const valueType = tomlType(value)
1279 if (listType !== valueType) {
1280 throw this.error(new TomlError(`Inline lists must be a single type, not a mix of ${listType} and ${valueType}`))
1281 }
1282 } else {
1283 this.state.resultArr = InlineList(tomlType(value))
1284 }
1285 if (isFloat(value) || isInteger(value)) {
1286 // unbox now that we've verified they're ok
1287 this.state.resultArr.push(value.valueOf())
1288 } else {
1289 this.state.resultArr.push(value)
1290 }
1291 return this.goto(this.parseInlineListNext)
1292 }
1293 parseInlineListNext () {
1294 if (this.char === CHAR_SP || this.char === CTRL_I || this.char === CTRL_M || this.char === CTRL_J) {
1295 return null
1296 } else if (this.char === CHAR_NUM) {
1297 return this.call(this.parseComment)
1298 } else if (this.char === CHAR_COMMA) {
1299 return this.next(this.parseInlineList)
1300 } else if (this.char === CHAR_RSQB) {
1301 return this.goto(this.parseInlineList)
1302 } else {
1303 throw this.error(new TomlError('Invalid character, expected whitespace, comma (,) or close bracket (])'))
1304 }
1305 }
1306
1307 /* INLINE TABLE */
1308 parseInlineTable () {
1309 if (this.char === CHAR_SP || this.char === CTRL_I) {
1310 return null
1311 } else if (this.char === Parser.END || this.char === CHAR_NUM || this.char === CTRL_J || this.char === CTRL_M) {
1312 throw this.error(new TomlError('Unterminated inline array'))
1313 } else if (this.char === CHAR_RCUB) {
1314 return this.return(this.state.resultTable || InlineTable())
1315 } else {
1316 if (!this.state.resultTable) this.state.resultTable = InlineTable()
1317 return this.callNow(this.parseAssign, this.recordInlineTableValue)
1318 }
1319 }
1320 recordInlineTableValue (kv) {
1321 let target = this.state.resultTable
1322 let finalKey = kv.key.pop()
1323 for (let kw of kv.key) {
1324 if (kw in target && (!isTable(target[kw]) || target[kw][_declared])) {
1325 throw this.error(new TomlError("Can't redefine existing key"))
1326 }
1327 target = target[kw] = target[kw] || Table()
1328 }
1329 if (finalKey in target) {
1330 throw this.error(new TomlError("Can't redefine existing key"))
1331 }
1332 if (isInteger(kv.value) || isFloat(kv.value)) {
1333 target[finalKey] = kv.value.valueOf()
1334 } else {
1335 target[finalKey] = kv.value
1336 }
1337 return this.goto(this.parseInlineTableNext)
1338 }
1339 parseInlineTableNext () {
1340 if (this.char === CHAR_SP || this.char === CTRL_I) {
1341 return null
1342 } else if (this.char === Parser.END || this.char === CHAR_NUM || this.char === CTRL_J || this.char === CTRL_M) {
1343 throw this.error(new TomlError('Unterminated inline array'))
1344 } else if (this.char === CHAR_COMMA) {
1345 return this.next(this.parseInlineTable)
1346 } else if (this.char === CHAR_RCUB) {
1347 return this.goto(this.parseInlineTable)
1348 } else {
1349 throw this.error(new TomlError('Invalid character, expected whitespace, comma (,) or close bracket (])'))
1350 }
1351 }
1352 }
1353 return TOMLParser
1354}