forked from toml-lang/toml
-
Notifications
You must be signed in to change notification settings - Fork 0
/
toml.co
165 lines (156 loc) · 6.02 KB
/
toml.co
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
const
ARR_OPEN = 0x5b # array open char `[`
ARR_CLOSE = 0x5d # array close char `]`
ASSIGN = 0x3d # assignment char `=`
COMMA = 0x2c # comma char `,` (in arrays)
HASH = 0x23 # comment char `#`
SKIP = /^#.*\n[^\S\n]*|^[^\S\n]+/ # whitespace & comments
NEWL = /^\n+/ # newlines (ie. separators)
STR = /^"([^\\"\n]*(?:\\[\s\S][^\\"\n]*)*)"/ # strings
GROUP = /^\[([^\[\]]+)\]/ # group names
NUM = /^-?\d+(\.\d+)?/ # ints or floats
# datetime
DATE = // ^
\d{4} - (0\d|1[012]) - ([012]\d|3[01]) T
([01]\d|2[0-4]) : ([0-5]\d) : ([0-5]\d) (\. \d{1,3})? Z
//
# an identifier / key. anything from the start of a line to the first ASSIGN.
ID = // ^ (..*?) \s* #{ String.from-char-code ASSIGN } //
# nicer error reporting
err = !(msg, tok) ->
if tok
msg += " on line #{tok.line}"
if tok.ostr
{column, line} = tok
line = (that / \\n)[<> - 1];
if line.length > 40 then let ncol = 20
line.=substr column - ncol, column + ncol
line := "$#<>"; ++ncol if column > ncol
column = ncol
msg += "\n#{ line }\n#{ \- * column }^"
e = Error msg
throw e
# recursively sets a property on an object:
# set { d: 2 } 'a.b.c' 0 => { d: 2, a: { b: { c: 0 } } }
set = !(ctx, key, val) ->
key /= \.
fkey = key.pop!
for key => ctx = ctx@[&] # autovivicate subobjects
ctx[fkey] = val
lex = (str) ->
ostr = str # keep the original source somewhere
str += \\n # lazy hack to fix comments on the last line (see SKIP regex, it needs a newline)
stack = [] # token stack
in-arr = 0 # array nesting level
l = 0 # characters just eaten
column = 0 # current column
line = 1 # current line
last = type: \newline # last token
tok = (type, value, extr = {}) ->
stack.push last := { ostr, type, value, line, column } <<< extr
value
while str.=slice l
if m = SKIP.exec str ?.0
# if this is a comment, advance a line and add a newline token if we're not in an array
l = m.length
if m.char-code-at! is HASH
then line++; column = 0; in-arr < 1 and tok \newline \\n
else column += l
else if m = NEWL.exec str ?.0
line += l = m.length
column = 0
# only add newline tokens if we're not in an array
tok \newline m if in-arr < 1
else if STR.exec str =>
tok \string,
that.1.replace(/\\n/g \\n)replace(/\\r/g \\r)replace(/\\t/g \\t)replace(/\\"/g '"')replace(/\\\\/g \\\\\)
column += l = that.0.length
else if DATE.exec str ?.0 => tok \datetime new Date that; column += l = that.length
else if m = NUM.exec str
if m.1
then tok \float parse-float m.0
else tok \integer parse-int m.0, 10
column += l = m.0.length
# boolean true/false
else if \true is str.substr 0 4 => tok \boolean true; column += l = 4
else if \false is str.substr 0 5 => tok \boolean false; column += l = 5
# keys
else if last.type is \newline and GROUP.exec str => tok \group that.1; column += l = that.0.length
else if last.type is \newline and ID.exec str => column += l = tok \id that.1 .length
# single character tokens
else
c = str.char-code-at!
if c is ARR_OPEN => in-arr++; tok \arr_open \[; l = 1; column++
else if c is ARR_CLOSE => in-arr--; tok \arr_close \]; l = 1; column++
else if c is ASSIGN => tok \assign \=; l = 1; column++
else if c is COMMA => tok \comma \,; l = 1; column++
else => err "Unexpected `#{str.char-at!}`" {line, column, ostr}
stack
# parses an array stack until the top-level closing brace
parse-array = (stack) ->
arr = []
type = void
expects = true
while tok = stack.shift!
if expects and tok.type of <[ string datetime integer float boolean ]>
# set type for this array
{type} ?= tok
# enforce single-typed array. don't expect a value
if tok.type is type
then arr.push tok.value; expects = false
else err "Unexpected #{tok.type} in array of #{type}s" tok
else if expects and tok.type is \arr_open
type ?= \array
# subarray, don't expect a value
if type is \array
then arr.push parse-array stack; expects = false
else err "Unexpected array in array of #{type}s" tok
else if not expects and tok.type is \comma
# comma, expect a value
expects = true
else if tok.type is \arr_close
return arr
else err "Unexpected #{tok.type}" tok
# unfinished array
err "Unexpected end of input"
parse = (stack, obj = {}) ->
# lex first if the stack does not look like an array
stack = lex <> unless stack instanceof Array
key = group = ''
last = type: \newline
while tok = stack.shift!
switch tok.type
case \newline
# newlines come after values or groups or other whitespace, but not assigns
if last.type is \assign => err "Expected value" (last => &column++)
case \string \integer \float \boolean \datetime
# values exist only after assigns (and in arrays but they're parsed elsewhere)
if last.type is \assign
then set obj, key, tok.value
else err "Unexpected value" tok
case \id
# IDs can only exist at the start of a line
if last.type is \newline
then key = (group or '') + tok.value
else err "Unexpected key" tok
case \assign
# assignments can only occur on IDs
last.type is \id or err "Unexpected assign" tok
case \arr_open
# arrays are values
if last.type is \assign
then set obj, key, parse-array stack
else err "Unexpected value" tok
case \group
# detect dupe prop
sub = obj; parts = tok.value / \.
for parts
switch typeof sub[&]
case \object => sub.=[&] # there's an object here: dig deeper
case \undefined => break # there's nothing here, safe to set
default => err "Duplicate property `#group#&`" tok
group = "#{tok.value}."
default => err "Unexpected `#{if tok.value is \\n then \newline else tok.value}`" tok
last = tok
obj
export lex, parse