xref: /freebsd/libexec/nuageinit/yaml.lua (revision b59017c5cad90d0f09a59e68c00457b7faf93e7c)
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