1--- 2-- SPDX-License-Identifier: MIT 3-- 4-- Copyright (c) 2017 Dominic Letz dominicletz@exosite.com 5 6local table_print_value 7table_print_value = function(value, indent, done) 8 indent = indent or 0 9 done = done or {} 10 if type(value) == "table" and not done [value] then 11 done [value] = true 12 13 local list = {} 14 for key in pairs (value) do 15 list[#list + 1] = key 16 end 17 table.sort(list, function(a, b) return tostring(a) < tostring(b) end) 18 local last = list[#list] 19 20 local rep = "{\n" 21 local comma 22 for _, key in ipairs (list) do 23 if key == last then 24 comma = '' 25 else 26 comma = ',' 27 end 28 local keyRep 29 if type(key) == "number" then 30 keyRep = key 31 else 32 keyRep = string.format("%q", tostring(key)) 33 end 34 rep = rep .. string.format( 35 "%s[%s] = %s%s\n", 36 string.rep(" ", indent + 2), 37 keyRep, 38 table_print_value(value[key], indent + 2, done), 39 comma 40 ) 41 end 42 43 rep = rep .. string.rep(" ", indent) -- indent it 44 rep = rep .. "}" 45 46 done[value] = false 47 return rep 48 elseif type(value) == "string" then 49 return string.format("%q", value) 50 else 51 return tostring(value) 52 end 53end 54 55local table_print = function(tt) 56 print('return '..table_print_value(tt)) 57end 58 59local table_clone = function(t) 60 local clone = {} 61 for k,v in pairs(t) do 62 clone[k] = v 63 end 64 return clone 65end 66 67local string_trim = function(s, what) 68 what = what or " " 69 return s:gsub("^[" .. what .. "]*(.-)["..what.."]*$", "%1") 70end 71 72local push = function(stack, item) 73 stack[#stack + 1] = item 74end 75 76local pop = function(stack) 77 local item = stack[#stack] 78 stack[#stack] = nil 79 return item 80end 81 82local context = function (str) 83 if type(str) ~= "string" then 84 return "" 85 end 86 87 str = str:sub(0,25):gsub("\n","\\n"):gsub("\"","\\\""); 88 return ", near \"" .. str .. "\"" 89end 90 91local Parser = {} 92function Parser.new (self, tokens) 93 self.tokens = tokens 94 self.parse_stack = {} 95 self.refs = {} 96 self.current = 0 97 return self 98end 99 100local exports = {version = "1.2"} 101 102local word = function(w) return "^("..w..")([%s$%c])" end 103 104local tokens = { 105 {"comment", "^#[^\n]*"}, 106 {"indent", "^\n( *)"}, 107 {"space", "^ +"}, 108 {"true", word("enabled"), const = true, value = true}, 109 {"true", word("true"), const = true, value = true}, 110 {"true", word("yes"), const = true, value = true}, 111 {"true", word("on"), const = true, value = true}, 112 {"false", word("disabled"), const = true, value = false}, 113 {"false", word("false"), const = true, value = false}, 114 {"false", word("no"), const = true, value = false}, 115 {"false", word("off"), const = true, value = false}, 116 {"null", word("null"), const = true, value = nil}, 117 {"null", word("Null"), const = true, value = nil}, 118 {"null", word("NULL"), const = true, value = nil}, 119 {"null", word("~"), const = true, value = nil}, 120 {"id", "^\"([^\"]-)\" *(:[%s%c])"}, 121 {"id", "^'([^']-)' *(:[%s%c])"}, 122 {"string", "^\"([^\"]-)\"", force_text = true}, 123 {"string", "^'([^']-)'", force_text = true}, 124 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)"}, 125 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)"}, 126 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)"}, 127 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)"}, 128 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)"}, 129 {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)"}, 130 {"doc", "^%-%-%-[^%c]*"}, 131 {",", "^,"}, 132 {"string", "^%b{} *[^,%c]+", noinline = true}, 133 {"{", "^{"}, 134 {"}", "^}"}, 135 {"string", "^%b[] *[^,%c]+", noinline = true}, 136 {"[", "^%["}, 137 {"]", "^%]"}, 138 {"-", "^%-", noinline = true}, 139 {":", "^:"}, 140 {"pipe", "^(|)(%d*[+%-]?)", sep = "\n"}, 141 {"pipe", "^(>)(%d*[+%-]?)", sep = " "}, 142 {"id", "^([%w][%w %-_]*)(:[%s%c])"}, 143 {"string", "^[^%c]+", noinline = true}, 144 {"string", "^[^,%]}%c ]+"} 145}; 146exports.tokenize = function (str) 147 local token 148 local row = 0 149 local ignore 150 local indents = 0 151 local lastIndents 152 local stack = {} 153 local indentAmount = 0 154 local inline = false 155 str = str:gsub("\r\n","\010") 156 157 while #str > 0 do 158 for i in ipairs(tokens) do 159 local captures = {} 160 if not inline or tokens[i].noinline == nil then 161 captures = {str:match(tokens[i][2])} 162 end 163 164 if #captures > 0 then 165 captures.input = str:sub(0, 25) 166 token = table_clone(tokens[i]) 167 token[2] = captures 168 local str2 = str:gsub(tokens[i][2], "", 1) 169 token.raw = str:sub(1, #str - #str2) 170 str = str2 171 172 if token[1] == "{" or token[1] == "[" then 173 inline = true 174 elseif token.const then 175 -- Since word pattern contains last char we're re-adding it 176 str = token[2][2] .. str 177 token.raw = token.raw:sub(1, #token.raw - #token[2][2]) 178 elseif token[1] == "id" then 179 -- Since id pattern contains last semi-colon we're re-adding it 180 str = token[2][2] .. str 181 token.raw = token.raw:sub(1, #token.raw - #token[2][2]) 182 -- Trim 183 token[2][1] = string_trim(token[2][1]) 184 elseif token[1] == "string" then 185 -- Finding numbers 186 local snip = token[2][1] 187 if not token.force_text then 188 if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then 189 token[1] = "number" 190 end 191 end 192 193 elseif token[1] == "comment" then 194 ignore = true; 195 elseif token[1] == "indent" then 196 row = row + 1 197 inline = false 198 lastIndents = indents 199 if indentAmount == 0 then 200 indentAmount = #token[2][1] 201 end 202 203 if indentAmount ~= 0 then 204 indents = (#token[2][1] / indentAmount); 205 else 206 indents = 0 207 end 208 209 if indents == lastIndents then 210 ignore = true; 211 elseif indents > lastIndents + 2 then 212 error("SyntaxError: invalid indentation, got " .. tostring(indents) 213 .. " instead of " .. tostring(lastIndents) .. context(token[2].input)) 214 elseif indents > lastIndents + 1 then 215 push(stack, token) 216 elseif indents < lastIndents then 217 local input = token[2].input 218 token = {"dedent", {"", input = ""}} 219 token.input = input 220 while lastIndents > indents + 1 do 221 lastIndents = lastIndents - 1 222 push(stack, token) 223 end 224 end 225 end -- if token[1] == XXX 226 token.row = row 227 break 228 end -- if #captures > 0 229 end 230 231 if not ignore then 232 if token then 233 push(stack, token) 234 token = nil 235 else 236 error("SyntaxError " .. context(str)) 237 end 238 end 239 240 ignore = false; 241 end 242 243 return stack 244end 245 246Parser.peek = function (self, offset) 247 offset = offset or 1 248 return self.tokens[offset + self.current] 249end 250 251Parser.advance = function (self) 252 self.current = self.current + 1 253 return self.tokens[self.current] 254end 255 256Parser.advanceValue = function (self) 257 return self:advance()[2][1] 258end 259 260Parser.accept = function (self, type) 261 if self:peekType(type) then 262 return self:advance() 263 end 264end 265 266Parser.expect = function (self, type, msg) 267 return self:accept(type) or 268 error(msg .. context(self:peek()[1].input)) 269end 270 271Parser.expectDedent = function (self, msg) 272 return self:accept("dedent") or (self:peek() == nil) or 273 error(msg .. context(self:peek()[2].input)) 274end 275 276Parser.peekType = function (self, val, offset) 277 return self:peek(offset) and self:peek(offset)[1] == val 278end 279 280Parser.ignore = function (self, items) 281 local advanced 282 repeat 283 advanced = false 284 for _,v in pairs(items) do 285 if self:peekType(v) then 286 self:advance() 287 advanced = true 288 end 289 end 290 until advanced == false 291end 292 293Parser.ignoreSpace = function (self) 294 self:ignore{"space"} 295end 296 297Parser.ignoreWhitespace = function (self) 298 self:ignore{"space", "indent", "dedent"} 299end 300 301Parser.parse = function (self) 302 303 local ref = nil 304 if self:peekType("string") and not self:peek().force_text then 305 local char = self:peek()[2][1]:sub(1,1) 306 if char == "&" then 307 ref = self:peek()[2][1]:sub(2) 308 self:advanceValue() 309 self:ignoreSpace() 310 elseif char == "*" then 311 ref = self:peek()[2][1]:sub(2) 312 return self.refs[ref] 313 end 314 end 315 316 local result 317 local c = { 318 indent = self:accept("indent") and 1 or 0, 319 token = self:peek() 320 } 321 push(self.parse_stack, c) 322 323 if c.token[1] == "doc" then 324 result = self:parseDoc() 325 elseif c.token[1] == "-" then 326 result = self:parseList() 327 elseif c.token[1] == "{" then 328 result = self:parseInlineHash() 329 elseif c.token[1] == "[" then 330 result = self:parseInlineList() 331 elseif c.token[1] == "id" then 332 result = self:parseHash() 333 elseif c.token[1] == "string" then 334 result = self:parseString("\n") 335 elseif c.token[1] == "timestamp" then 336 result = self:parseTimestamp() 337 elseif c.token[1] == "number" then 338 result = tonumber(self:advanceValue()) 339 elseif c.token[1] == "pipe" then 340 result = self:parsePipe() 341 elseif c.token.const == true then 342 self:advanceValue(); 343 result = c.token.value 344 else 345 error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input)) 346 end 347 348 pop(self.parse_stack) 349 while c.indent > 0 do 350 c.indent = c.indent - 1 351 local term = "term "..c.token[1]..": '"..c.token[2][1].."'" 352 self:expectDedent("last ".. term .." is not properly dedented") 353 end 354 355 if ref then 356 self.refs[ref] = result 357 end 358 return result 359end 360 361Parser.parseDoc = function (self) 362 self:accept("doc") 363 return self:parse() 364end 365 366Parser.inline = function (self) 367 local current = self:peek(0) 368 if not current then 369 return {}, 0 370 end 371 372 local inline = {} 373 local i = 0 374 375 while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do 376 inline[self:peek(i)[1]] = true 377 i = i - 1 378 end 379 return inline, -i 380end 381 382Parser.isInline = function (self) 383 local _, i = self:inline() 384 return i > 0 385end 386 387Parser.parent = function(self, level) 388 level = level or 1 389 return self.parse_stack[#self.parse_stack - level] 390end 391 392Parser.parentType = function(self, type, level) 393 return self:parent(level) and self:parent(level).token[1] == type 394end 395 396Parser.parseString = function (self) 397 if self:isInline() then 398 local result = self:advanceValue() 399 400 --[[ 401 - a: this looks 402 flowing: but is 403 no: string 404 --]] 405 local types = self:inline() 406 if types["id"] and types["-"] then 407 if not self:peekType("indent") or not self:peekType("indent", 2) then 408 return result 409 end 410 end 411 412 --[[ 413 a: 1 414 b: this is 415 a flowing string 416 example 417 c: 3 418 --]] 419 if self:peekType("indent") then 420 self:expect("indent", "text block needs to start with indent") 421 local addtl = self:accept("indent") 422 423 result = result .. "\n" .. self:parseTextBlock("\n") 424 425 self:expectDedent("text block ending dedent missing") 426 if addtl then 427 self:expectDedent("text block ending dedent missing") 428 end 429 end 430 return result 431 else 432 --[[ 433 a: 1 434 b: 435 this is also 436 a flowing string 437 example 438 c: 3 439 --]] 440 return self:parseTextBlock("\n") 441 end 442end 443 444Parser.parsePipe = function (self) 445 local pipe = self:expect("pipe") 446 self:expect("indent", "text block needs to start with indent") 447 local result = self:parseTextBlock(pipe.sep) 448 self:expectDedent("text block ending dedent missing") 449 return result 450end 451 452Parser.parseTextBlock = function (self, sep) 453 local token = self:advance() 454 local result = string_trim(token.raw, "\n") 455 local indents = 0 456 while self:peek() ~= nil and ( indents > 0 or not self:peekType("dedent") ) do 457 local newtoken = self:advance() 458 while token.row < newtoken.row do 459 result = result .. sep 460 token.row = token.row + 1 461 end 462 if newtoken[1] == "indent" then 463 indents = indents + 1 464 elseif newtoken[1] == "dedent" then 465 indents = indents - 1 466 else 467 result = result .. string_trim(newtoken.raw, "\n") 468 end 469 end 470 return result 471end 472 473Parser.parseHash = function (self, hash) 474 hash = hash or {} 475 local indents = 0 476 477 if self:isInline() then 478 local id = self:advanceValue() 479 self:expect(":", "expected semi-colon after id") 480 self:ignoreSpace() 481 if self:accept("indent") then 482 indents = indents + 1 483 hash[id] = self:parse() 484 else 485 hash[id] = self:parse() 486 if self:accept("indent") then 487 indents = indents + 1 488 end 489 end 490 self:ignoreSpace(); 491 end 492 493 while self:peekType("id") do 494 local id = self:advanceValue() 495 self:expect(":","expected semi-colon after id") 496 self:ignoreSpace() 497 hash[id] = self:parse() 498 self:ignoreSpace(); 499 end 500 501 while indents > 0 do 502 self:expectDedent("expected dedent") 503 indents = indents - 1 504 end 505 506 return hash 507end 508 509Parser.parseInlineHash = function (self) 510 local id 511 local hash = {} 512 local i = 0 513 514 self:accept("{") 515 while not self:accept("}") do 516 self:ignoreSpace() 517 if i > 0 then 518 self:expect(",","expected comma") 519 end 520 521 self:ignoreWhitespace() 522 if self:peekType("id") then 523 id = self:advanceValue() 524 if id then 525 self:expect(":","expected semi-colon after id") 526 self:ignoreSpace() 527 hash[id] = self:parse() 528 self:ignoreWhitespace() 529 end 530 end 531 532 i = i + 1 533 end 534 return hash 535end 536 537Parser.parseList = function (self) 538 local list = {} 539 while self:accept("-") do 540 self:ignoreSpace() 541 list[#list + 1] = self:parse() 542 543 self:ignoreSpace() 544 end 545 return list 546end 547 548Parser.parseInlineList = function (self) 549 local list = {} 550 local i = 0 551 self:accept("[") 552 while not self:accept("]") do 553 self:ignoreSpace() 554 if i > 0 then 555 self:expect(",","expected comma") 556 end 557 558 self:ignoreSpace() 559 list[#list + 1] = self:parse() 560 self:ignoreSpace() 561 i = i + 1 562 end 563 564 return list 565end 566 567Parser.parseTimestamp = function (self) 568 local capture = self:advance()[2] 569 570 return os.time{ 571 year = capture[1], 572 month = capture[2], 573 day = capture[3], 574 hour = capture[4] or 0, 575 min = capture[5] or 0, 576 sec = capture[6] or 0, 577 isdst = false, 578 } - os.time{year=1970, month=1, day=1, hour=8} 579end 580 581exports.eval = function (str) 582 return Parser:new(exports.tokenize(str)):parse() 583end 584 585exports.dump = table_print 586 587return exports 588