1 -- SPDX-License-Identifier: MIT
3 -- Copyright (c) 2017 Dominic Letz dominicletz@exosite.com
5 local table_print_value
6 table_print_value = function(value, indent, done)
9 if type(value) == "table" and not done [value] then
13 for key in pairs (value) do
16 table.sort(list, function(a, b) return tostring(a) < tostring(b) end)
17 local last = list[#list]
21 for _, key in ipairs (list) do
28 if type(key) == "number" then
31 keyRep = string.format("%q", tostring(key))
33 rep = rep .. string.format(
35 string.rep(" ", indent + 2),
37 table_print_value(value[key], indent + 2, done),
42 rep = rep .. string.rep(" ", indent) -- indent it
47 elseif type(value) == "string" then
48 return string.format("%q", value)
50 return tostring(value)
54 local table_print = function(tt)
55 print('return '..table_print_value(tt))
58 local table_clone = function(t)
60 for k,v in pairs(t) do
66 local string_trim = function(s, what)
68 return s:gsub("^[" .. what .. "]*(.-)["..what.."]*$", "%1")
71 local push = function(stack, item)
72 stack[#stack + 1] = item
75 local pop = function(stack)
76 local item = stack[#stack]
81 local context = function (str)
82 if type(str) ~= "string" then
86 str = str:sub(0,25):gsub("\n","\\n"):gsub("\"","\\\"");
87 return ", near \"" .. str .. "\""
91 function Parser.new (self, tokens)
99 local exports = {version = "1.2"}
101 local word = function(w) return "^("..w..")([%s$%c])" end
104 {"comment", "^#[^\n]*"},
105 {"indent", "^\n( *)"},
107 {"true", word("enabled"), const = true, value = true},
108 {"true", word("true"), const = true, value = true},
109 {"true", word("yes"), const = true, value = true},
110 {"true", word("on"), const = true, value = true},
111 {"false", word("disabled"), const = true, value = false},
112 {"false", word("false"), const = true, value = false},
113 {"false", word("no"), const = true, value = false},
114 {"false", word("off"), const = true, value = false},
115 {"null", word("null"), const = true, value = nil},
116 {"null", word("Null"), const = true, value = nil},
117 {"null", word("NULL"), const = true, value = nil},
118 {"null", word("~"), const = true, value = nil},
119 {"id", "^\"([^\"]-)\" *(:[%s%c])"},
120 {"id", "^'([^']-)' *(:[%s%c])"},
121 {"string", "^\"([^\"]-)\"", force_text = true},
122 {"string", "^'([^']-)'", force_text = true},
123 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)"},
124 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)"},
125 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)"},
126 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)"},
127 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)"},
128 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)"},
129 {"doc", "^%-%-%-[^%c]*"},
131 {"string", "^%b{} *[^,%c]+", noinline = true},
134 {"string", "^%b[] *[^,%c]+", noinline = true},
137 {"-", "^%-", noinline = true},
139 {"pipe", "^(|)(%d*[+%-]?)", sep = "\n"},
140 {"pipe", "^(>)(%d*[+%-]?)", sep = " "},
141 {"id", "^([%w][%w %-_]*)(:[%s%c])"},
142 {"string", "^[^%c]+", noinline = true},
143 {"string", "^[^,%]}%c ]+"}
145 exports.tokenize = function (str)
152 local indentAmount = 0
154 str = str:gsub("\r\n","\010")
157 for i in ipairs(tokens) do
159 if not inline or tokens[i].noinline == nil then
160 captures = {str:match(tokens[i][2])}
163 if #captures > 0 then
164 captures.input = str:sub(0, 25)
165 token = table_clone(tokens[i])
167 local str2 = str:gsub(tokens[i][2], "", 1)
168 token.raw = str:sub(1, #str - #str2)
171 if token[1] == "{" or token[1] == "[" then
173 elseif token.const then
174 -- Since word pattern contains last char we're re-adding it
175 str = token[2][2] .. str
176 token.raw = token.raw:sub(1, #token.raw - #token[2][2])
177 elseif token[1] == "id" then
178 -- Since id pattern contains last semi-colon we're re-adding it
179 str = token[2][2] .. str
180 token.raw = token.raw:sub(1, #token.raw - #token[2][2])
182 token[2][1] = string_trim(token[2][1])
183 elseif token[1] == "string" then
185 local snip = token[2][1]
186 if not token.force_text then
187 if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then
192 elseif token[1] == "comment" then
194 elseif token[1] == "indent" then
197 lastIndents = indents
198 if indentAmount == 0 then
199 indentAmount = #token[2][1]
202 if indentAmount ~= 0 then
203 indents = (#token[2][1] / indentAmount);
208 if indents == lastIndents then
210 elseif indents > lastIndents + 2 then
211 error("SyntaxError: invalid indentation, got " .. tostring(indents)
212 .. " instead of " .. tostring(lastIndents) .. context(token[2].input))
213 elseif indents > lastIndents + 1 then
215 elseif indents < lastIndents then
216 local input = token[2].input
217 token = {"dedent", {"", input = ""}}
219 while lastIndents > indents + 1 do
220 lastIndents = lastIndents - 1
224 end -- if token[1] == XXX
227 end -- if #captures > 0
235 error("SyntaxError " .. context(str))
245 Parser.peek = function (self, offset)
247 return self.tokens[offset + self.current]
250 Parser.advance = function (self)
251 self.current = self.current + 1
252 return self.tokens[self.current]
255 Parser.advanceValue = function (self)
256 return self:advance()[2][1]
259 Parser.accept = function (self, type)
260 if self:peekType(type) then
261 return self:advance()
265 Parser.expect = function (self, type, msg)
266 return self:accept(type) or
267 error(msg .. context(self:peek()[1].input))
270 Parser.expectDedent = function (self, msg)
271 return self:accept("dedent") or (self:peek() == nil) or
272 error(msg .. context(self:peek()[2].input))
275 Parser.peekType = function (self, val, offset)
276 return self:peek(offset) and self:peek(offset)[1] == val
279 Parser.ignore = function (self, items)
283 for _,v in pairs(items) do
284 if self:peekType(v) then
289 until advanced == false
292 Parser.ignoreSpace = function (self)
296 Parser.ignoreWhitespace = function (self)
297 self:ignore{"space", "indent", "dedent"}
300 Parser.parse = function (self)
303 if self:peekType("string") and not self:peek().force_text then
304 local char = self:peek()[2][1]:sub(1,1)
306 ref = self:peek()[2][1]:sub(2)
309 elseif char == "*" then
310 ref = self:peek()[2][1]:sub(2)
311 return self.refs[ref]
317 indent = self:accept("indent") and 1 or 0,
320 push(self.parse_stack, c)
322 if c.token[1] == "doc" then
323 result = self:parseDoc()
324 elseif c.token[1] == "-" then
325 result = self:parseList()
326 elseif c.token[1] == "{" then
327 result = self:parseInlineHash()
328 elseif c.token[1] == "[" then
329 result = self:parseInlineList()
330 elseif c.token[1] == "id" then
331 result = self:parseHash()
332 elseif c.token[1] == "string" then
333 result = self:parseString("\n")
334 elseif c.token[1] == "timestamp" then
335 result = self:parseTimestamp()
336 elseif c.token[1] == "number" then
337 result = tonumber(self:advanceValue())
338 elseif c.token[1] == "pipe" then
339 result = self:parsePipe()
340 elseif c.token.const == true then
342 result = c.token.value
344 error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input))
347 pop(self.parse_stack)
348 while c.indent > 0 do
349 c.indent = c.indent - 1
350 local term = "term "..c.token[1]..": '"..c.token[2][1].."'"
351 self:expectDedent("last ".. term .." is not properly dedented")
355 self.refs[ref] = result
360 Parser.parseDoc = function (self)
365 Parser.inline = function (self)
366 local current = self:peek(0)
374 while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do
375 inline[self:peek(i)[1]] = true
381 Parser.isInline = function (self)
382 local _, i = self:inline()
386 Parser.parent = function(self, level)
388 return self.parse_stack[#self.parse_stack - level]
391 Parser.parentType = function(self, type, level)
392 return self:parent(level) and self:parent(level).token[1] == type
395 Parser.parseString = function (self)
396 if self:isInline() then
397 local result = self:advanceValue()
404 local types = self:inline()
405 if types["id"] and types["-"] then
406 if not self:peekType("indent") or not self:peekType("indent", 2) then
418 if self:peekType("indent") then
419 self:expect("indent", "text block needs to start with indent")
420 local addtl = self:accept("indent")
422 result = result .. "\n" .. self:parseTextBlock("\n")
424 self:expectDedent("text block ending dedent missing")
426 self:expectDedent("text block ending dedent missing")
439 return self:parseTextBlock("\n")
443 Parser.parsePipe = function (self)
444 local pipe = self:expect("pipe")
445 self:expect("indent", "text block needs to start with indent")
446 local result = self:parseTextBlock(pipe.sep)
447 self:expectDedent("text block ending dedent missing")
451 Parser.parseTextBlock = function (self, sep)
452 local token = self:advance()
453 local result = string_trim(token.raw, "\n")
455 while self:peek() ~= nil and ( indents > 0 or not self:peekType("dedent") ) do
456 local newtoken = self:advance()
457 while token.row < newtoken.row do
458 result = result .. sep
459 token.row = token.row + 1
461 if newtoken[1] == "indent" then
462 indents = indents + 1
463 elseif newtoken[1] == "dedent" then
464 indents = indents - 1
466 result = result .. string_trim(newtoken.raw, "\n")
472 Parser.parseHash = function (self, hash)
476 if self:isInline() then
477 local id = self:advanceValue()
478 self:expect(":", "expected semi-colon after id")
480 if self:accept("indent") then
481 indents = indents + 1
482 hash[id] = self:parse()
484 hash[id] = self:parse()
485 if self:accept("indent") then
486 indents = indents + 1
492 while self:peekType("id") do
493 local id = self:advanceValue()
494 self:expect(":","expected semi-colon after id")
496 hash[id] = self:parse()
501 self:expectDedent("expected dedent")
502 indents = indents - 1
508 Parser.parseInlineHash = function (self)
514 while not self:accept("}") do
517 self:expect(",","expected comma")
520 self:ignoreWhitespace()
521 if self:peekType("id") then
522 id = self:advanceValue()
524 self:expect(":","expected semi-colon after id")
526 hash[id] = self:parse()
527 self:ignoreWhitespace()
536 Parser.parseList = function (self)
538 while self:accept("-") do
540 list[#list + 1] = self:parse()
547 Parser.parseInlineList = function (self)
551 while not self:accept("]") do
554 self:expect(",","expected comma")
558 list[#list + 1] = self:parse()
566 Parser.parseTimestamp = function (self)
567 local capture = self:advance()[2]
573 hour = capture[4] or 0,
574 min = capture[5] or 0,
575 sec = capture[6] or 0,
577 } - os.time{year=1970, month=1, day=1, hour=8}
580 exports.eval = function (str)
581 return Parser:new(exports.tokenize(str)):parse()
584 exports.dump = table_print