## :License: MIT ## ## Introduction ## ============ ## This module implements a TOML parser that is compliant with v0.5.0 of its spec. ## ## Source ## ====== ## `Repo link `_ ## # Copyright (c) 2015 Maurizio Tomasi and contributors # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import math import streams import strutils import tables import unicode from parseutils import parseFloat export tables when (NimMajor, NimMinor, NimPatch) < (1, 4, 0): type IndexDefect* = IndexError OverflowDefect* = OverflowError type Sign* = enum None, Pos, Neg TomlValueKind* {.pure.} = enum None Int, Float, Bool, Datetime, Date, Time, String, Array, Table TomlDate* = object year*: int month*: int day*: int TomlTime* = object hour*: int minute*: int second*: int subsecond*: int TomlDateTime* = object date*: TomlDate time*: TomlTime case shift*: bool of true: isShiftPositive*: bool zoneHourShift*: int zoneMinuteShift*: int of false: nil TomlTable* = OrderedTable[string, TomlValueRef] TomlTableRef* = ref TomlTable TomlValueRef* = ref TomlValue TomlValue* = object case kind*: TomlValueKind of TomlValueKind.None: nil of TomlValueKind.Int: intVal*: int64 of TomlValueKind.Float: floatVal*: float64 forcedSign*: Sign of TomlValueKind.Bool: boolVal*: bool of TomlValueKind.Datetime: dateTimeVal*: TomlDateTime of TomlValueKind.Date: dateVal*: TomlDate of TomlValueKind.Time: timeVal*: TomlTime of TomlValueKind.String: stringVal*: string of TomlValueKind.Array: arrayVal*: seq[TomlValueRef] of TomlValueKind.Table: tableVal*: TomlTableRef ParserState = object fileName*: string line*: int column*: int pushback: char stream*: streams.Stream curTableRef*: TomlTableRef TomlError* = object of ValueError location*: ParserState NumberBase = enum base10, base16, base8, base2 StringType {.pure.} = enum Basic, # Enclosed within double quotation marks Literal # Enclosed within single quotation marks const defaultStringCapacity = 256 ctrlChars = {'\x00' .. '\x08', '\x0A' .. '\x1F', '\x7F'} # '\x09' - TAB is not counted as control char ctrlCharsExclCrLf = ctrlChars - {'\x0A', '\x0D'} proc newTomlError(location: ParserState, msg: string): ref TomlError = result = newException(TomlError, location.fileName & "(" & $location.line & ":" & $location.column & ")" & " " & msg) result.location = location proc getNextChar(state: var ParserState): char = # Return the next available char from the stream associate with # the parser state, or '\0' if there are no characters left. if state.pushback != '\0': # If we've just read a character without having interpreted # it, just return it result = state.pushback state.pushback = '\0' else: if state.stream.atEnd(): return '\0' result = state.stream.readChar() # Update the line and column number if result == '\l': inc(state.line) state.column = 1 elif result != '\r': inc(state.column) proc pushBackChar(state: var ParserState, c: char) {.inline.} = state.pushback = c type LfSkipMode = enum skipLf, skipNoLf proc getNextNonWhitespace(state: var ParserState, skip: LfSkipMode): char = # Note: this procedure does *not* consider a newline as a # "whitespace". Since newlines are often mandatory in TOML files # (e.g. after a key/value specification), we do not want to miss # them... let whitespaces = (case skip of skipLf: {' ', '\t', '\r', '\l'} of skipNoLf: {' ', '\t', '\r'}) var nextChar: char while true: nextChar = state.getNextChar() if nextChar == '#': # Skip the comment up to the newline, but do not jump over it while nextChar != '\l' and nextChar != '\0': nextChar = state.getNextChar() # https://toml.io/en/v1.0.0#comment # Control characters other than tab (U+0009) are not permitted in comments. # Invalid control characters: U+0000 to U+0008, U+000A to U+001F, U+007F if nextChar in ctrlCharsExclCrLf: raise newTomlError(state, "invalid control char 0x$# found in a comment" % [nextChar.ord.toHex(2)]) if nextChar notin whitespaces: break result = nextChar proc charToInt(c: char, base: NumberBase): int {.inline, noSideEffect.} = case base of base10, base8, base2: result = int(c) - int('0') of base16: if c in strutils.Digits: result = charToInt(c, base10) else: result = 10 + int(toUpperAscii(c)) - int('A') type LeadingChar {.pure.} = enum AllowZero, DenyZero proc parseInt(state: var ParserState, base: NumberBase, leadingChar: LeadingChar): int64 = var nextChar: char firstPos = true negative = false wasUnderscore = false let baseNum = (case base of base2: 2 of base8: 8 of base10: 10 of base16: 16) digits = (case base of base2: {'0', '1'} of base8: {'0', '1', '2', '3', '4', '5', '6', '7'} of base10: strutils.Digits of base16: strutils.HexDigits) result = 0 while true: wasUnderscore = nextChar == '_' nextChar = state.getNextChar() if nextChar == '_': if firstPos or wasUnderscore: raise(newTomlError(state, "underscore must be surrounded by digit")) continue if nextChar in {'+', '-'} and firstPos: firstPos = false if nextChar == '-': negative = true continue if nextChar == '0' and firstPos and leadingChar == LeadingChar.DenyZero: # TOML specifications forbid this var upcomingChar = state.getNextChar() if upcomingChar in Digits: raise(newTomlError(state, "leading zeroes are not allowed in integers")) else: state.pushBackChar(upcomingChar) if nextChar notin digits: if wasUnderscore: raise(newTomlError(state, "underscore must be surrounded by digit")) state.pushBackChar(nextChar) break try: result = result * baseNum - charToInt(nextChar, base) except OverflowDefect: raise(newTomlError(state, "integer numbers wider than 64 bits not allowed")) firstPos = false if not negative: try: result = -result except OverflowDefect: raise(newTomlError(state, "integer numbers wider than 64 bits not allowed")) proc parseEncoding(state: var ParserState): TomlValueRef = let nextChar = state.getNextChar() case nextChar: of 'b': return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base2, LeadingChar.AllowZero)) of 'o': return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base8, LeadingChar.AllowZero)) of 'x': return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base16, LeadingChar.AllowZero)) else: raise newTomlError(state, "illegal character") proc parseDecimalPart(state: var ParserState): float64 = var nextChar: char firstPos = true wasUnderscore = false decimalPartStr = "0." while true: wasUnderscore = nextChar == '_' nextChar = state.getNextChar() if nextChar == '_': if firstPos or wasUnderscore: raise(newTomlError(state, "underscore must be surrounded by digit")) continue if nextChar notin strutils.Digits: if wasUnderscore: raise(newTomlError(state, "underscore must be surrounded by digit")) state.pushBackChar(nextChar) if firstPos: raise newTomlError(state, "decimal part empty") break decimalPartStr.add(nextChar) firstPos = false doAssert decimalPartStr.len > 2 # decimalPartStr shouldn't still be "0." at this point discard parseutils.parseFloat(decimalPartStr, result) proc stringDelimiter(kind: StringType): char {.inline, noSideEffect.} = result = (case kind of StringType.Basic: '\"' of StringType.Literal: '\'') proc parseUnicode(state: var ParserState): string = let escapeKindChar = state.getNextChar() oldState = (column: state.column, line: state.line) code = parseInt(state, base16, LeadingChar.AllowZero) if state.line != oldState.line: raise newTomlError(state, "invalid Unicode codepoint, can't span lines") if escapeKindChar == 'u' and state.column - 5 != oldState.column: raise newTomlError(state, "invalid Unicode codepoint, 'u' must have " & "four character value") if escapeKindChar == 'U' and state.column - 9 != oldState.column: raise newTomlError(state, "invalid Unicode codepoint, 'U' must have " & "eight character value") if code notin 0'i64..0xD7FF and code notin 0xE000'i64..0x10FFFF: raise(newTomlError(state, "invalid Unicode codepoint, " & "must be a Unicode scalar value")) return unicode.toUTF8(Rune(code)) proc parseEscapeChar(state: var ParserState, escape: char): string = case escape of 'b': result = "\b" of 't': result = "\t" of 'n': result = "\l" of 'f': result = "\f" of 'r': result = "\r" of '\'': result = "\'" of '\"': result = "\"" of '\\': result = "\\" of 'u', 'U': state.pushBackChar(escape) result = parseUnicode(state) else: raise(newTomlError(state, "unknown escape " & "sequence \"\\" & escape & "\"")) proc parseSingleLineString(state: var ParserState, kind: StringType): string = # This procedure parses strings enclosed within single/double # quotation marks. It assumes that the quotation mark has already # been consumed by the "state" variable, which therefore is ready # to read the first character of the string. result = newStringOfCap(defaultStringCapacity) let delimiter = stringDelimiter(kind) var nextChar: char while true: nextChar = state.getNextChar() if nextChar == delimiter: break if nextChar == '\0': raise(newTomlError(state, "unterminated string")) # https://toml.io/en/v1.0.0#string # Any Unicode character may be used except those that must be escaped: # quotation mark, backslash, and the control characters other than tab # (U+0000 to U+0008, U+000A to U+001F, U+007F). if nextChar in ctrlChars: raise(newTomlError(state, "invalid character in string: 0x$#" % nextChar.ord.toHex(2))) if nextChar == '\\' and kind == StringType.Basic: nextChar = state.getNextChar() result.add(state.parseEscapeChar(nextChar)) continue result.add(nextChar) proc parseMultiLineString(state: var ParserState, kind: StringType): string = # This procedure parses strings enclosed within three consecutive # sigle/double quotation marks. It assumes that all the quotation # marks have already been consumed by the "state" variable, which # therefore is ready to read the first character of the string. result = newStringOfCap(defaultStringCapacity) let delimiter = stringDelimiter(kind) var isFirstChar = true nextChar: char while true: nextChar = state.getNextChar() # Skip the first newline, if it comes immediately after the # quotation marks if isFirstChar and (nextChar == '\l'): isFirstChar = false continue if nextChar == delimiter: # Are we done? nextChar = state.getNextChar() if nextChar == delimiter: nextChar = state.getNextChar() if nextChar == delimiter: # Done with this string return else: # Just got a double delimiter result.add(delimiter & delimiter) state.pushBackChar(nextChar) continue else: # Just got a lone delimiter result.add(delimiter) state.pushBackChar(nextChar) continue if nextChar == '\\' and kind == StringType.Basic: # This can either be an escape sequence or a end-of-line char nextChar = state.getNextChar() if nextChar in {'\l', '\r', ' '}: # We're at the end of a line: skip everything till the # next non-whitespace character while nextChar in {'\l', '\r', ' ', '\t'}: nextChar = state.getNextChar() state.pushBackChar(nextChar) continue else: # This is just an escape sequence (like "\t") #nextChar = state.getNextChar() result.add(state.parseEscapeChar(nextChar)) continue if nextChar == '\0': raise(newTomlError(state, "unterminated string")) # https://toml.io/en/v1.0.0#string # Any Unicode character may be used except those that must be # escaped: backslash and the control characters other than tab, # line feed, and carriage return (U+0000 to U+0008, U+000B, # U+000C, U+000E to U+001F, U+007F). if nextChar in ctrlCharsExclCrLf: raise(newTomlError(state, "invalid character in string: 0x$#" % nextChar.ord.toHex(2))) result.add(nextChar) isFirstChar = false proc parseString(state: var ParserState, kind: StringType): string = ## This function assumes that "state" has already consumed the ## first character (either \" or \', which is passed in the ## "openChar" parameter). let delimiter = stringDelimiter(kind) var nextChar: char = state.getNextChar() if nextChar == delimiter: # We have two possibilities here: (1) the empty string, or (2) # "long" multi-line strings. nextChar = state.getNextChar() if nextChar == delimiter: return parseMultiLineString(state, kind) else: # Empty string. This was easy! state.pushBackChar(nextChar) return "" else: state.pushBackChar(nextChar) return parseSingleLineString(state, kind) # Forward declaration proc parseValue(state: var ParserState): TomlValueRef proc parseInlineTable(state: var ParserState): TomlValueRef proc parseArray(state: var ParserState): seq[TomlValueRef] = # This procedure assumes that "state" has already consumed the '[' # character result = newSeq[TomlValueRef](0) while true: var nextChar: char = state.getNextNonWhitespace(skipLf) case nextChar of ']': return of ',': if len(result) == 0: # This happens with "[, 1, 2]", for instance raise(newTomlError(state, "first array element missing")) # Check that this is not a terminating comma (like in # "[b,]") nextChar = state.getNextNonWhitespace(skipLf) if nextChar == ']': return state.pushBackChar(nextChar) else: let oldState = state # Saved for error messages var newValue: TomlValueRef if nextChar != '{': state.pushBackChar(nextChar) newValue = parseValue(state) else: newValue = parseInlineTable(state) if len(result) > 0: # Check that the type of newValue is compatible with the # previous ones if newValue.kind != result[low(result)].kind: raise(newTomlError(oldState, "array members with incompatible types")) result.add(newValue) proc parseStrictNum(state: var ParserState, minVal: int, maxVal: int, count: Slice[int], msg: string): int = var nextChar: char parsedChars = 0 result = 0 while true: nextChar = state.getNextChar() if nextChar notin strutils.Digits: state.pushBackChar(nextChar) break try: result = result * 10 + charToInt(nextChar, base10) parsedChars += 1 except OverflowDefect: raise(newTomlError(state, "integer numbers wider than 64 bits not allowed")) if parsedChars notin count: raise(newTomlError(state, "too few or too many characters in digit, expected " & $count & " got " & $parsedChars)) if result < minVal or result > maxVal: raise(newTomlError(state, msg & " (" & $result & ")")) template parseStrictNum(state: var ParserState, minVal: int, maxVal: int, count: int, msg: string): int = parseStrictNum(state, minVal, maxVal, (count..count), msg) proc parseTimePart(state: var ParserState, val: var TomlTime) = var nextChar: char curLine = state.line # Parse the minutes val.minute = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2, "number out of range for minutes") if curLine != state.line: raise(newTomlError(state, "invalid date field, stopped in or after minutes field")) nextChar = state.getNextChar() if nextChar != ':': raise(newTomlError(state, "\":\" expected after the number of seconds")) # Parse the second. Note that seconds=60 *can* happen (leap second) val.second = parseStrictNum(state, minVal = 0, maxVal = 60, count = 2, "number out of range for seconds") nextChar = state.getNextChar() if nextChar == '.': val.subsecond = parseInt(state, base10, LeadingChar.AllowZero).int else: state.pushBackChar(nextChar) proc parseDateTimePart(state: var ParserState, dateTime: var TomlDateTime): bool = # This function is called whenever a datetime object is found. They follow # an ISO convention and can use one of the following format: # # - YYYY-MM-DDThh:mm:ss[+-]hh:mm # - YYYY-MM-DDThh:mm:ssZ # # where the "T" and "Z" letters are literals, [+-] indicates # *either* "+" or "-", YYYY is the 4-digit year, MM is the 2-digit # month, DD is the 2-digit day, hh is the 2-digit hour, mm is the # 2-digit minute, and ss is the 2-digit second. The hh:mm after # the +/- character is the timezone; a literal "Z" indicates the # local timezone. # This function assumes that the "YYYY-" part has already been # parsed (this happens because during parsing, finding a 4-digit # number like "YYYY" might just indicate the presence of an # integer or a floating-point number; it's the following "-" that # tells the parser that the value is a datetime). As a consequence # of this, we assume that "dateTime.year" has already been set. var nextChar: char curLine = state.line # Parse the month dateTime.date.month = parseStrictNum(state, minVal = 1, maxVal = 12, count = 2, "number out of range for the month") if curLine != state.line: raise(newTomlError(state, "invalid date field, stopped in or after month field")) nextChar = state.getNextChar() if nextChar != '-': raise(newTomlError(state, "\"-\" expected after the month number")) # Parse the day dateTime.date.day = parseStrictNum(state, minVal = 1, maxVal = 31, count = 2, "number out of range for the day") if curLine != state.line: return false else: nextChar = state.getNextChar() if nextChar notin {'t', 'T', ' '}: raise(newTomlError(state, "\"T\", \"t\", or space expected after the day number")) # Parse the hour dateTime.time.hour = parseStrictNum(state, minVal = 0, maxVal = 23, count = 2, "number out of range for the hours") if curLine != state.line: raise(newTomlError(state, "invalid date field, stopped in or after hours field")) nextChar = state.getNextChar() if nextChar != ':': raise(newTomlError(state, "\":\" expected after the number of hours")) # Parse the minutes dateTime.time.minute = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2, "number out of range for minutes") if curLine != state.line: raise(newTomlError(state, "invalid date field, stopped in or after minutes field")) nextChar = state.getNextChar() if nextChar != ':': raise(newTomlError(state, "\":\" expected after the number of seconds")) # Parse the second. Note that seconds=60 *can* happen (leap second) dateTime.time.second = parseStrictNum(state, minVal = 0, maxVal = 60, count = 2, "number out of range for seconds") nextChar = state.getNextChar() if nextChar == '.': dateTime.time.subsecond = parseInt(state, base10, LeadingChar.AllowZero).int else: state.pushBackChar(nextChar) nextChar = state.getNextChar() case nextChar of 'z', 'Z': dateTime = TomlDateTime( time: dateTime.time, date: dateTime.date, shift: true, isShiftPositive: true, zoneHourShift: 0, zoneMinuteShift: 0 ) of '+', '-': dateTime = TomlDateTime( time: dateTime.time, date: dateTime.date, shift: true, isShiftPositive: (nextChar == '+') ) dateTime.zoneHourShift = parseStrictNum(state, minVal = 0, maxVal = 23, count = 2, "number out of range for shift hours") if curLine != state.line: raise(newTomlError(state, "invalid date field, stopped in or after shift hours field")) nextChar = state.getNextChar() if nextChar != ':': raise(newTomlError(state, "\":\" expected after the number of shift hours")) dateTime.zoneMinuteShift = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2, "number out of range for shift minutes") else: if curLine == state.line: raise(newTomlError(state, "unexpected character " & escape($nextChar) & " instead of the time zone")) else: # shift is automatically initialized to false state.pushBackChar(nextChar) return true proc parseDateOrTime(state: var ParserState, digits: int, yearOrHour: int): TomlValueRef = var nextChar: char yoh = yearOrHour d = digits while true: nextChar = state.getNextChar() case nextChar: of ':': if d != 2: raise newTomlError(state, "wrong number of characters for hour") var val: TomlTime val.hour = yoh parseTimePart(state, val) return TomlValueRef(kind: TomlValueKind.Time, timeVal: val) of '-': if d != 4: raise newTomlError(state, "wrong number of characters for year") var val: TomlDateTime val.date.year = yoh let fullDate = parseDateTimePart(state, val) if fullDate: return TomlValueRef(kind: TomlValueKind.DateTime, dateTimeVal: val) else: return TomlValueRef(kind: TomlValueKind.Date, dateVal: val.date) of strutils.Digits: if d == 4: raise newTomlError(state, "leading zero not allowed") try: yoh *= 10 yoh += ord(nextChar) - ord('0') d += 1 except OverflowDefect: raise newTomlError(state, "number larger than 64 bits wide") continue of strutils.Whitespace: raise newTomlError(state, "leading zero not allowed") else: raise newTomlError(state, "illegal character") break proc parseFloat(state: var ParserState, intPart: int, forcedSign: Sign): TomlValueRef = var decimalPart = parseDecimalPart(state) nextChar = state.getNextChar() exponent: int64 = 0 if nextChar in {'e', 'E'}: exponent = parseInt(state, base10, LeadingChar.AllowZero) else: state.pushBackChar(nextChar) let value = if intPart <= 0: pow(10.0, exponent.float64) * (float64(intPart) - decimalPart) else: pow(10.0, exponent.float64) * (float64(intPart) + decimalPart) return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign != Neg: -value else: value, forcedSign: forcedSign) proc parseNumOrDate(state: var ParserState): TomlValueRef = var nextChar: char forcedSign: Sign = None while true: nextChar = state.getNextChar() case nextChar: of '0': nextChar = state.getNextChar() if forcedSign == None: if nextChar in {'b', 'x', 'o'}: state.pushBackChar(nextChar) return parseEncoding(state) else: # This must now be a float or a date/time, or a sole 0 case nextChar: of '.': return parseFloat(state, 0, forcedSign) of strutils.Whitespace: state.pushBackChar(nextChar) return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) of strutils.Digits: # This must now be a date/time return parseDateOrTime(state, digits = 2, yearOrHour = ord(nextChar) - ord('0')) else: # else is a sole 0 return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) else: # This must now be a float, or a sole 0 case nextChar: of '.': return parseFloat(state, 0, forcedSign) of strutils.Whitespace: state.pushBackChar(nextChar) return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) else: # else is a sole 0 return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) of strutils.Digits - {'0'}: # This might be a date/time, or an int or a float var digits = 1 curSum = ord('0') - ord(nextChar) wasUnderscore = false while true: nextChar = state.getNextChar() if wasUnderscore and nextChar notin strutils.Digits: raise newTomlError(state, "underscores must be surrounded by digits") case nextChar: of ':': if digits != 2: raise newTomlError(state, "wrong number of characters for hour") var val: TomlTime val.hour = -curSum parseTimePart(state, val) return TomlValueRef(kind: TomlValueKind.Time, timeVal: val) of '-': if digits != 4: raise newTomlError(state, "wrong number of characters for year") var val: TomlDateTime val.date.year = -curSum let fullDate = parseDateTimePart(state, val) if fullDate: return TomlValueRef(kind: TomlValueKind.DateTime, dateTimeVal: val) else: return TomlValueRef(kind: TomlValueKind.Date, dateVal: val.date) of '.': return parseFloat(state, curSum, forcedSign) of 'e', 'E': var exponent = parseInt(state, base10, LeadingChar.AllowZero) let value = pow(10.0, exponent.float64) * float64(curSum) return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign != Neg: -value else: value) of strutils.Digits: try: curSum *= 10 curSum += ord('0') - ord(nextChar) digits += 1 except OverflowDefect: raise newTomlError(state, "number larger than 64 bits wide") wasUnderscore = false continue of '_': wasUnderscore = true continue of strutils.Whitespace: state.pushBackChar(nextChar) return TomlValueRef(kind: TomlValueKind.Int, intVal: if forcedSign != Neg: -curSum else: curSum) else: state.pushBackChar(nextChar) return TomlValueRef(kind: TomlValueKind.Int, intVal: if forcedSign != Neg: -curSum else: curSum) break of '+', '-': forcedSign = if nextChar == '+': Pos else: Neg continue of 'i': # Is this "inf"? let oldState = state if state.getNextChar() != 'n' or state.getNextChar() != 'f': raise(newTomlError(oldState, "unknown identifier")) return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign == Neg: NegInf else: Inf, forcedSign: forcedSign) of 'n': # Is this "nan"? let oldState = state if state.getNextChar() != 'a' or state.getNextChar() != 'n': raise(newTomlError(oldState, "unknown identifier")) return TomlValueRef(kind: TomlValueKind.Float, floatVal: NaN, forcedSign: forcedSign) else: raise newTomlError(state, "illegal character " & escape($nextChar)) break proc parseValue(state: var ParserState): TomlValueRef = var nextChar: char nextChar = state.getNextNonWhitespace(skipNoLf) case nextChar of strutils.Digits, '+', '-', 'i', 'n': state.pushBackChar(nextChar) return parseNumOrDate(state) of 't': # Is this "true"? let oldState = state # Only used for error messages if state.getNextChar() != 'r' or state.getNextChar() != 'u' or state.getNextChar() != 'e': raise(newTomlError(oldState, "unknown identifier")) result = TomlValueRef(kind: TomlValueKind.Bool, boolVal: true) of 'f': # Is this "false"? let oldState = state # Only used for error messages if state.getNextChar() != 'a' or state.getNextChar() != 'l' or state.getNextChar() != 's' or state.getNextChar() != 'e': raise(newTomlError(oldState, "unknown identifier")) result = TomlValueRef(kind: TomlValueKind.Bool, boolVal: false) of '\"': # A basic string (accepts \ escape codes) result = TomlValueRef(kind: TomlValueKind.String, stringVal: parseString(state, StringType.Basic)) of '\'': # A literal string (does not accept \ escape codes) result = TomlValueRef(kind: TomlValueKind.String, stringVal: parseString(state, StringType.Literal)) of '[': # An array result = TomlValueRef(kind: TomlValueKind.Array, arrayVal: parseArray(state)) else: raise(newTomlError(state, "unexpected character " & escape($nextChar))) proc parseName(state: var ParserState): string = # This parses the name of a key or a table result = newStringOfCap(defaultStringCapacity) var nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar == '\"': return state.parseString(StringType.Basic) elif nextChar == '\'': return state.parseString(StringType.Literal) state.pushBackChar(nextChar) while true: nextChar = state.getNextChar() if (nextChar in {'=', '.', '[', ']', '\0', ' ', '\t'}): # Any of the above characters marks the end of the name state.pushBackChar(nextChar) break elif (nextChar notin {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}): raise(newTomlError(state, "bare key has illegal character: " & escape($nextChar))) else: result.add(nextChar) type BracketType {.pure.} = enum single, double proc parseTableName(state: var ParserState, brackets: BracketType): seq[string] = # This code assumes that '[' has already been consumed result = newSeq[string](0) while true: #let partName = state.parseName(SpecialChars.AllowNumberSign) var nextChar = state.getNextChar() partName: string if nextChar == '"': partName = state.parseString(StringType.Basic) else: state.pushBackChar(nextChar) partName = state.parseName() result.add(partName) nextChar = state.getNextNonWhitespace(skipNoLf) case nextChar of ']': if brackets == BracketType.double: nextChar = state.getNextChar() if nextChar != ']': raise(newTomlError(state, "\"]]\" expected")) # We must check that there is nothing else in this line nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar notin {'\l', '\0'}: raise(newTomlError(state, "unexpected character " & escape($nextChar))) break of '.': continue else: raise(newTomlError(state, "unexpected character " & escape($nextChar))) proc setEmptyTableVal(val: var TomlValueRef) = val = TomlValueRef(kind: TomlValueKind.Table) new(val.tableVal) val.tableVal[] = initOrderedTable[string, TomlValueRef]() proc parseInlineTable(state: var ParserState): TomlValueRef = new(result) setEmptyTableVal(result) var firstComma = true while true: var nextChar = state.getNextNonWhitespace(skipNoLf) case nextChar of '}': return of ',': if firstComma: raise(newTomlError(state, "first inline table element missing")) # Check that this is not a terminating comma (like in # "[b,]") nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar == '}': return state.pushBackChar(nextChar) of '\n': raise(newTomlError(state, "inline tables cannot contain newlines")) else: firstComma = false state.pushBackChar(nextChar) var key = state.parseName() nextChar = state.getNextNonWhitespace(skipNoLf) var curTable = result.tableVal while nextChar == '.': var deepestTable = new(TomlTableRef) deepestTable[] = initOrderedTable[string, TomlValueRef]() curTable[key] = TomlValueRef(kind: TomlValueKind.Table, tableVal: deepestTable) curTable = deepestTable key = state.parseName() nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar != '=': raise(newTomlError(state, "key names cannot contain spaces")) nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar == '{': curTable[key] = state.parseInlineTable() else: state.pushBackChar(nextChar) curTable[key] = state.parseValue() proc createTableDef(state: var ParserState, tableNames: seq[string], dotted = false) proc parseKeyValuePair(state: var ParserState) = var tableKeys: seq[string] key: string nextChar: char oldTableRef = state.curTableRef while true: let subkey = state.parseName() nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar == '.': tableKeys.add subkey else: if tableKeys.len != 0: createTableDef(state, tableKeys, dotted = true) key = subkey break if nextChar != '=': raise(newTomlError(state, "key names cannot contain character \"" & nextChar & "\"")) nextChar = state.getNextNonWhitespace(skipNoLf) # Check that this is a regular value and not an inline table if nextChar != '{': state.pushBackChar(nextChar) let value = state.parseValue() # We must check that there is nothing else in this line nextChar = state.getNextNonWhitespace(skipNoLf) if nextChar != '\l' and nextChar != '\0': raise(newTomlError(state, "unexpected character " & escape($nextChar))) if state.curTableRef.hasKey(key): raise(newTomlError(state, "duplicate key, \"" & key & "\" already in table")) state.curTableRef[key] = value else: #createTableDef(state, @[key]) if key.len == 0: raise newTomlError(state, "empty key not allowed") if state.curTableRef.hasKey(key): raise newTomlError(state, "duplicate table key not allowed") state.curTableRef[key] = parseInlineTable(state) state.curTableRef = oldTableRef proc newParserState(s: streams.Stream, fileName: string = ""): ParserState = result = ParserState(fileName: fileName, line: 1, column: 1, stream: s) proc setArrayVal(val: var TomlValueRef, numOfElems: int = 0) = val = TomlValueRef(kind: TomlValueKind.Array) val.arrayVal = newSeq[TomlValueRef](numOfElems) proc advanceToNextNestLevel(state: var ParserState, tableName: string) = let target = state.curTableRef[tableName] case target.kind of TomlValueKind.Table: state.curTableRef = target.tableVal of TomlValueKind.Array: let arr = target.arrayVal[high(target.arrayVal)] if arr.kind != TomlValueKind.Table: raise(newTomlError(state, "\"" & tableName & "\" elements are not tables")) state.curTableRef = arr.tableVal else: raise(newTomlError(state, "\"" & tableName & "\" is not a table")) # This function is called by the TOML parser whenever a # "[[table.name]]" line is encountered in the parsing process. Its # purpose is to make sure that all the parent nodes in "table.name" # exist and are tables, and that a terminal node of the correct type # is created. # # Starting from "curTableRef" (which is usually the root object), # traverse the object tree following the names in "tableNames" and # create a new TomlValueRef object of kind "TomlValueKind.Array" at # the terminal node. This array is going to be an array of tables: the # function will create an element and will make "curTableRef" # reference it. Example: if tableNames == ["a", "b", "c"], the code # will look for the "b" table that is child of "a", and then it will # check if "c" is a child of "b". If it is, it must be an array of # tables, and a new element will be appended. Otherwise, a new "c" # array is created, and an empty table element is added in "c". In # either cases, curTableRef will refer to the last element of "c". proc createOrAppendTableArrayDef(state: var ParserState, tableNames: seq[string]) = # This is a table array entry (e.g. "[[entry]]") for idx, tableName in tableNames: if tableName.len == 0: raise(newTomlError(state, "empty key not allowed")) let lastTableInChain = idx == high(tableNames) var newValue: TomlValueRef if not state.curTableRef.hasKey(tableName): # If this element does not exist, create it new(newValue) # If this is the last name in the chain (e.g., # "c" in "a.b.c"), its value should be an # array of tables, otherwise just a table if lastTableInChain: setArrayVal(newValue, 1) new(newValue.arrayVal[0]) setEmptyTableVal(newValue.arrayVal[0]) state.curTableRef[tableName] = newValue state.curTableRef = newValue.arrayVal[0].tableVal else: setEmptyTableVal(newValue) # Add the newly created object to the current table state.curTableRef[tableName] = newValue # Update the pointer to the current table state.curTableRef = newValue.tableVal else: # The element exists: is it of the right type? let target = state.curTableRef[tableName] if lastTableInChain: if target.kind != TomlValueKind.Array: raise(newTomlError(state, "\"" & tableName & "\" is not an array")) var newValue: TomlValueRef new(newValue) setEmptyTableVal(newValue) target.arrayVal.add(newValue) state.curTableRef = newValue.tableVal else: advanceToNextNestLevel(state, tableName) # Starting from "curTableRef" (which is usually the root object), # traverse the object tree following the names in "tableNames" and # create a new TomlValueRef object of kind "TomlValueKind.Table" at # the terminal node. Example: if tableNames == ["a", "b", "c"], the # code will look for the "b" table that is child of "a" and it will # create a new table "c" which is "b"'s children. proc createTableDef(state: var ParserState, tableNames: seq[string], dotted = false) = var newValue: TomlValueRef # This starts a new table (e.g. "[table]") for i, tableName in tableNames: if tableName.len == 0: raise(newTomlError(state, "empty key not allowed")) if not state.curTableRef.hasKey(tableName): new(newValue) setEmptyTableVal(newValue) # Add the newly created object to the current table state.curTableRef[tableName] = newValue # Update the pointer to the current table state.curTableRef = newValue.tableVal else: if i == tableNames.high and state.curTableRef.hasKey(tableName) and state.curTableRef[tableName].kind == TomlValueKind.Table: if state.curTableRef[tableName].tableVal.len == 0: raise newTomlError(state, "duplicate table key not allowed") elif not dotted: for value in state.curTableRef[tableName].tableVal.values: if value.kind != TomlValueKind.Table: raise newTomlError(state, "duplicate table key not allowed") advanceToNextNestLevel(state, tableName) proc parseStream*(inputStream: streams.Stream, fileName: string = ""): TomlValueRef = ## Parses a stream of TOML formatted data into a TOML table. The optional ## filename is used for error messages. if inputStream == nil: raise newException(IOError, "Unable to read from the stream created from: \"" & fileName & "\", " & "possibly a missing file") var state = newParserState(inputStream, fileName) result = TomlValueRef(kind: TomlValueKind.Table) new(result.tableVal) result.tableVal[] = initOrderedTable[string, TomlValueRef]() # This pointer will always point to the table that should get new # key/value pairs found in the TOML file during parsing state.curTableRef = result.tableVal # Unlike "curTableRef", this pointer never changes: it always # points to the uppermost table in the tree let baseTable = result.tableVal var nextChar: char while true: nextChar = state.getNextNonWhitespace(skipLf) case nextChar of '[': # A new section/table begins. We'll have to start again # from the uppermost level, so let's rewind curTableRef to # the root node state.curTableRef = baseTable # First, decompose the table name into its part (e.g., # "a.b.c" -> ["a", "b", "c"]) nextChar = state.getNextChar() let isTableArrayDef = nextChar == '[' var tableNames: seq[string] if isTableArrayDef: tableNames = state.parseTableName(BracketType.double) else: state.pushBackChar(nextChar) tableNames = state.parseTableName(BracketType.single) # Now create the proper (empty) data structure: either a # table or an array of tables. Note that both functions # update the "curTableRef" variable: they have to, since # the TOML specification says that any "key = value" # statement that follows is a child of the table we're # defining right now, and we use "curTableRef" as a # reference to the table that gets every next key/value # definition. if isTableArrayDef: createOrAppendTableArrayDef(state, tableNames) else: createTableDef(state, tableNames) of '=': raise(newTomlError(state, "key name missing")) of '#', '.', ']': raise(newTomlError(state, "unexpected character " & escape($nextChar))) of '\0': # EOF return else: # Everything else marks the presence of a "key = value" pattern state.pushBackChar(nextChar) parseKeyValuePair(state) proc parseString*(tomlStr: string, fileName: string = ""): TomlValueRef = ## Parses a string of TOML formatted data into a TOML table. The optional ## filename is used for error messages. let strStream = newStringStream(tomlStr) try: result = parseStream(strStream, fileName) finally: strStream.close() proc parseFile*(f: File, fileName: string = ""): TomlValueRef = ## Parses a file of TOML formatted data into a TOML table. The optional ## filename is used for error messages. let fStream = newFileStream(f) try: result = parseStream(fStream, fileName) finally: fStream.close() proc parseFile*(fileName: string): TomlValueRef = ## Parses the file found at fileName with TOML formatted data into a TOML ## table. let fStream = newFileStream(fileName, fmRead) if not isNil(fStream): try: result = parseStream(fStream, fileName) finally: fStream.close() else: raise newException(IOError, "cannot open: " & fileName) proc `$`*(val: TomlDate): string = ## Converts the TOML date object into the ISO format read by the parser result = ($val.year).align(4, '0') & "-" & ($val.month).align(2, '0') & "-" & ($val.day).align(2, '0') proc `$`*(val: TomlTime): string = ## Converts the TOML time object into the ISO format read by the parser result = ($val.hour).align(2, '0') & ":" & ($val.minute).align(2, '0') & ":" & ($val.second).align(2, '0') & (if val.subsecond > 0: ("." & $val.subsecond) else: "") proc `$`*(val: TomlDateTime): string = ## Converts the TOML date-time object into the ISO format read by the parser result = $val.date & "T" & $val.time & (if not val.shift: "" else: ( (if val.zoneHourShift == 0 and val.zoneMinuteShift == 0: "Z" else: ( ((if val.isShiftPositive: "+" else: "-") & ($val.zoneHourShift).align(2, '0') & ":" & ($val.zoneMinuteShift).align(2, '0')) )) )) proc toTomlString*(value: TomlValueRef): string proc `$`*(val: TomlValueRef): string = ## Turns whatever value into a regular Nim value representtation case val.kind of TomlValueKind.None: result = "nil" of TomlValueKind.Int: result = $val.intVal of TomlValueKind.Float: result = $val.floatVal of TomlValueKind.Bool: result = $val.boolVal of TomlValueKind.Datetime: result = $val.dateTimeVal of TomlValueKind.Date: result = $val.dateVal of TomlValueKind.Time: result = $val.timeVal of TomlValueKind.String: result = $val.stringVal of TomlValueKind.Array: result = "" for elem in val.arrayVal: result.add($(elem[])) of TomlValueKind.Table: result = val.toTomlString proc `$`*(val: TomlValue): string = ## Turns whatever value into a type and value representation, used by ``dump`` case val.kind of TomlValueKind.None: result = "none()" of TomlValueKind.Int: result = "int(" & $val.intVal & ")" of TomlValueKind.Float: result = "float(" & $val.floatVal & ")" of TomlValueKind.Bool: result = "boolean(" & $val.boolVal & ")" of TomlValueKind.Datetime: result = "datetime(" & $val.dateTimeVal & ")" of TomlValueKind.Date: result = "date(" & $val.dateVal & ")" of TomlValueKind.Time: result = "time(" & $val.timeVal & ")" of TomlValueKind.String: result = "string(\"" & $val.stringVal & "\")" of TomlValueKind.Array: result = "array(" for elem in val.arrayVal: result.add($(elem[])) result.add(")") of TomlValueKind.Table: result = "table(" & $(len(val.tableVal)) & " elements)" proc dump*(table: TomlTableRef, indentLevel: int = 0) = ## Dump out the entire table as it was parsed. This procedure is mostly ## useful for debugging purposes let space = spaces(indentLevel) for key, val in pairs(table): if val.kind == TomlValueKind.Table: echo space & key & " = table" dump(val.tableVal, indentLevel + 4) elif (val.kind == TomlValueKind.Array and val.arrayVal[0].kind == TomlValueKind.Table): for idx, val in val.arrayVal: echo space & key & "[" & $idx & "] = table" dump(val.tableVal, indentLevel + 4) else: echo space & key & " = " & $(val[]) import json, sequtils proc toJson*(value: TomlValueRef): JsonNode proc toJson*(table: TomlTableRef): JsonNode = ## Converts a TOML table to a JSON node. This uses the format specified in ## the validation suite for it's output: ## https://github.com/BurntSushi/toml-test#example-json-encoding result = newJObject() for key, value in pairs(table): result[key] = value.toJson proc toJson*(value: TomlValueRef): JsonNode = ## Converts a TOML value to a JSON node. This uses the format specified in ## the validation suite for it's output: ## https://github.com/BurntSushi/toml-test#example-json-encoding case value.kind: of TomlValueKind.Int: %*{"type": "integer", "value": $value.intVal} of TomlValueKind.Float: if classify(value.floatVal) == fcNan: if value.forcedSign != Pos: %*{"type": "float", "value": $value.floatVal} else: %*{"type": "float", "value": "+" & $value.floatVal} else: %*{"type": "float", "value": $value.floatVal} of TomlValueKind.Bool: %*{"type": "bool", "value": $value.boolVal} of TomlValueKind.Datetime: if value.dateTimeVal.shift == false: %*{"type": "datetime-local", "value": $value.dateTimeVal} else: %*{"type": "datetime", "value": $value.dateTimeVal} of TomlValueKind.Date: %*{"type": "date", "value": $value.dateVal} of TomlValueKind.Time: %*{"type": "time", "value": $value.timeVal} of TomlValueKind.String: %*{"type": "string", "value": newJString(value.stringVal)} of TomlValueKind.Array: if value.arrayVal.len == 0: when defined(newtestsuite): %[] else: %*{"type": "array", "value": []} elif value.arrayVal[0].kind == TomlValueKind.Table: %value.arrayVal.map(toJson) else: when defined(newtestsuite): %*value.arrayVal.map(toJson) else: %*{"type": "array", "value": value.arrayVal.map(toJson)} of TomlValueKind.Table: value.tableVal.toJson of TomlValueKind.None: %*{"type": "ERROR"} proc toKey(str: string): string = for c in str: if (c notin {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}): return "\"" & str & "\"" str proc toTomlString*(value: TomlTableRef, parents = ""): string = ## Converts a TOML table to a TOML formatted string for output to a file. result = "" var subtables: seq[tuple[key: string, value: TomlValueRef]] = @[] for key, value in pairs(value): block outer: if value.kind == TomlValueKind.Table: subtables.add((key: key, value: value)) elif value.kind == TomlValueKind.Array and value.arrayVal.len > 0 and value.arrayVal[0].kind == TomlValueKind.Table: let tables = value.arrayVal.map(toTomlString) for table in tables: result = result & "[[" & key & "]]\n" & table & "\n" else: result = result & key.toKey & " = " & toTomlString(value) & "\n" for kv in subtables: let fullKey = (if parents.len > 0: parents & "." else: "") & kv.key.toKey block outer: for ikey, ivalue in pairs(kv.value.tableVal): if ivalue.kind != TomlValueKind.Table: result = result & "[" & fullKey & "]\n" & kv.value.tableVal.toTomlString(fullKey) & "\n" break outer result = result & kv.value.tableVal.toTomlString(fullKey) proc toTomlString*(value: TomlValueRef): string = ## Converts a TOML value to a TOML formatted string for output to a file. case value.kind: of TomlValueKind.Int: $value.intVal of TomlValueKind.Float: $value.floatVal of TomlValueKind.Bool: $value.boolVal of TomlValueKind.Datetime: $value.dateTimeVal of TomlValueKind.String: "\"" & value.stringVal & "\"" of TomlValueKind.Array: if value.arrayVal.len == 0: "[]" elif value.arrayVal[0].kind == TomlValueKind.Table: value.arrayVal.map(toTomlString).join("\n") else: "[" & value.arrayVal.map(toTomlString).join(", ") & "]" of TomlValueKind.Table: value.tableVal.toTomlString else: "UNKNOWN" proc newTString*(s: string): TomlValueRef = ## Creates a new `TomlValueKind.String TomlValueRef`. TomlValueRef(kind: TomlValueKind.String, stringVal: s) proc newTInt*(n: int64): TomlValueRef = ## Creates a new `TomlValueKind.Int TomlValueRef`. TomlValueRef(kind: TomlValueKind.Int, intVal: n) proc newTFloat*(n: float): TomlValueRef = ## Creates a new `TomlValueKind.Float TomlValueRef`. TomlValueRef(kind: TomlValueKind.Float, floatVal: n) proc newTBool*(b: bool): TomlValueRef = ## Creates a new `TomlValueKind.Bool TomlValueRef`. TomlValueRef(kind: TomlValueKind.Bool, boolVal: b) proc newTNull*(): TomlValueRef = ## Creates a new `JNull TomlValueRef`. TomlValueRef(kind: TomlValueKind.None) proc newTTable*(): TomlValueRef = ## Creates a new `TomlValueKind.Table TomlValueRef` result = TomlValueRef(kind: TomlValueKind.Table) new(result.tableVal) result.tableVal[] = initOrderedTable[string, TomlValueRef](4) proc newTArray*(): TomlValueRef = ## Creates a new `TomlValueKind.Array TomlValueRef` TomlValueRef(kind: TomlValueKind.Array, arrayVal: @[]) proc getStr*(n: TomlValueRef, default: string = ""): string = ## Retrieves the string value of a `TomlValueKind.String TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.String``, or if ``n`` is nil. if n.isNil or n.kind != TomlValueKind.String: return default else: return n.stringVal proc getInt*(n: TomlValueRef, default: int = 0): int = ## Retrieves the int value of a `TomlValueKind.Int TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Int``, or if ``n`` is nil. if n.isNil or n.kind != TomlValueKind.Int: return default else: return int(n.intVal) proc getBiggestInt*(n: TomlValueRef, default: int64 = 0): int64 = ## Retrieves the int64 value of a `TomlValueKind.Int TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Int``, or if ``n`` is nil. if n.isNil or n.kind != TomlValueKind.Int: return default else: return n.intVal proc getFloat*(n: TomlValueRef, default: float = 0.0): float = ## Retrieves the float value of a `TomlValueKind.Float TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Float`` or ``TomlValueKind.Int``, or if ``n`` is nil. if n.isNil: return default case n.kind of TomlValueKind.Float: return n.floatVal of TomlValueKind.Int: return float(n.intVal) else: return default proc getBool*(n: TomlValueRef, default: bool = false): bool = ## Retrieves the bool value of a `TomlValueKind.Bool TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Bool``, or if ``n`` is nil. if n.isNil or n.kind != TomlValueKind.Bool: return default else: return n.boolVal proc getTable*(n: TomlValueRef, default = new(TomlTableRef)): TomlTableRef = ## Retrieves the key, value pairs of a `TomlValueKind.Table TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Table``, or if ``n`` is nil. if n.isNil or n.kind != TomlValueKind.Table: return default else: return n.tableVal proc getElems*(n: TomlValueRef, default: seq[TomlValueRef] = @[]): seq[TomlValueRef] = ## Retrieves the int value of a `TomlValueKind.Array TomlValueRef`. ## ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Array``, or if ``n`` is nil. if n.isNil or n.kind != TomlValueKind.Array: return default else: return n.arrayVal proc add*(father, child: TomlValueRef) = ## Adds `child` to a TomlValueKind.Array node `father`. assert father.kind == TomlValueKind.Array father.arrayVal.add(child) proc add*(obj: TomlValueRef, key: string, val: TomlValueRef) = ## Sets a field from a `TomlValueKind.Table`. assert obj.kind == TomlValueKind.Table obj.tableVal[key] = val proc `?`*(s: string): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.String TomlValueRef`. TomlValueRef(kind: TomlValueKind.String, stringVal: s) proc `?`*(n: int64): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Int TomlValueRef`. TomlValueRef(kind: TomlValueKind.Int, intVal: n) proc `?`*(n: float): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Float TomlValueRef`. TomlValueRef(kind: TomlValueKind.Float, floatVal: n) proc `?`*(b: bool): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Bool TomlValueRef`. TomlValueRef(kind: TomlValueKind.Bool, boolVal: b) proc `?`*(keyVals: openArray[tuple[key: string, val: TomlValueRef]]): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef` if keyVals.len == 0: return newTArray() result = newTTable() for key, val in items(keyVals): result.tableVal[key] = val template `?`*(j: TomlValueRef): TomlValueRef = j proc `?`*[T](elements: openArray[T]): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Array TomlValueRef` result = newTArray() for elem in elements: result.add(?elem) when false: # For 'consistency' we could do this, but that only pushes people further # into that evil comfort zone where they can use Nim without understanding it # causing problems later on. proc `?`*(elements: set[bool]): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef`. ## This can only be used with the empty set ``{}`` and is supported ## to prevent the gotcha ``%*{}`` which used to produce an empty ## TOML array. result = newTTable() assert false notin elements, "usage error: only empty sets allowed" assert true notin elements, "usage error: only empty sets allowed" proc `?`*(o: object): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef` result = newTTable() for k, v in o.fieldPairs: result[k] = ?v proc `?`*(o: ref object): TomlValueRef = ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef` if o.isNil: result = newTNull() else: result = ?(o[]) proc `?`*(o: enum): TomlValueRef = ## Construct a TomlValueRef that represents the specified enum value as a ## string. Creates a new ``TomlValueKind.String TomlValueRef``. result = ?($o) import macros proc toToml(x: NimNode): NimNode {.compileTime.} = case x.kind of nnkBracket: # array if x.len == 0: return newCall(bindSym"newTArray") result = newNimNode(nnkBracket) for i in 0 ..< x.len: result.add(toToml(x[i])) result = newCall(bindSym("?", brOpen), result) of nnkTableConstr: # object if x.len == 0: return newCall(bindSym"newTTable") result = newNimNode(nnkTableConstr) for i in 0 ..< x.len: x[i].expectKind nnkExprColonExpr result.add newTree(nnkExprColonExpr, x[i][0], toToml(x[i][1])) result = newCall(bindSym("?", brOpen), result) of nnkCurly: # empty object x.expectLen(0) result = newCall(bindSym"newTTable") of nnkNilLit: result = newCall(bindSym"newTNull") else: result = newCall(bindSym("?", brOpen), x) macro `?*`*(x: untyped): untyped = ## Convert an expression to a TomlValueRef directly, without having to specify ## `?` for every element. result = toToml(x) echo result.repr proc toTomlValue(x: NimNode): NimNode {.compileTime.} = newCall(bindSym("?", brOpen), x) proc toTomlNew(x: NimNode): NimNode {.compileTime.} = echo x.treeRepr var i = 0 curTable: NimNode = nil while i < x.len: echo x[i].kind case x[i].kind: of nnkAsgn: if curTable.isNil: curTable = newNimNode(nnkTableConstr) result = curTable curTable.add newTree(nnkExprColonExpr, newLit($x[i][0]), toTomlValue(x[i][1])) of nnkBracket: if curTable.isNil: curTable = newNimNode(nnkTableConstr) result = curTable else: var table = newNimNode(nnkTableConstr) result.add newTree(nnkExprColonExpr, newLit($x[i][0]), newCall(bindSym("?", brOpen), table)) curTable = table else: discard i += 1 result = newCall(bindSym("?", brOpen), result) macro `parseToml`*(x: untyped): untyped = ## Convert an expression to a TomlValueRef directly, without having to specify ## `?` for every element. result = toTomlNew(x) echo result.repr func `==`* (a, b: TomlValueRef): bool = ## Check two nodes for equality if a.isNil: if b.isNil: return true return false elif b.isNil or a.kind != b.kind: return false else: case a.kind of TomlValueKind.String: result = a.stringVal == b.stringVal of TomlValueKind.Int: result = a.intVal == b.intVal of TomlValueKind.Float: result = a.floatVal == b.floatVal of TomlValueKind.Bool: result = a.boolVal == b.boolVal of TomlValueKind.None: result = true of TomlValueKind.Array: result = a.arrayVal == b.arrayVal of TomlValueKind.Table: # we cannot use OrderedTable's equality here as # the order does not matter for equality here. if a.tableVal.len != b.tableVal.len: return false for key, val in a.tableVal: if not b.tableVal.hasKey(key): return false {.noSideEffect.}: if b.tableVal[key] != val: return false result = true of TomlValueKind.DateTime: result = a.dateTimeVal.date.year == b.dateTimeVal.date.year and a.dateTimeVal.date.month == b.dateTimeVal.date.month and a.dateTimeVal.date.day == b.dateTimeVal.date.day and a.dateTimeVal.time.hour == b.dateTimeVal.time.hour and a.dateTimeVal.time.minute == b.dateTimeVal.time.minute and a.dateTimeVal.time.second == b.dateTimeVal.time.second and a.dateTimeVal.time.subsecond == b.dateTimeVal.time.subsecond and a.dateTimeVal.shift == b.dateTimeVal.shift and (a.dateTimeVal.shift == true and (a.dateTimeVal.isShiftPositive == b.dateTimeVal.isShiftPositive and a.dateTimeVal.zoneHourShift == b.dateTimeVal.zoneHourShift and a.dateTimeVal.zoneMinuteShift == b.dateTimeVal.zoneMinuteShift)) or a.dateTimeVal.shift == false of TomlValueKind.Date: result = a.dateVal.year == b.dateVal.year and a.dateVal.month == b.dateVal.month and a.dateVal.day == b.dateVal.day of TomlValueKind.Time: result = a.timeVal.hour == b.timeVal.hour and a.timeVal.minute == b.timeVal.minute and a.timeVal.second == b.timeVal.second and a.timeVal.subsecond == b.timeVal.subsecond import hashes proc hash*(n: OrderedTable[string, TomlValueRef]): Hash {.noSideEffect.} proc hash*(n: TomlValueRef): Hash {.noSideEffect.} = ## Compute the hash for a TOML node case n.kind of TomlValueKind.Array: result = hash(n.arrayVal) of TomlValueKind.Table: result = hash(n.tableVal[]) of TomlValueKind.Int: result = hash(n.intVal) of TomlValueKind.Float: result = hash(n.floatVal) of TomlValueKind.Bool: result = hash(n.boolVal.int) of TomlValueKind.String: result = hash(n.stringVal) of TomlValueKind.None: result = Hash(0) of TomlValueKind.DateTime: result = hash($n.dateTimeVal) of TomlValueKind.Date: result = hash($n.dateVal) of TomlValueKind.Time: result = hash($n.timeVal) proc hash*(n: OrderedTable[string, TomlValueRef]): Hash = for key, val in n: result = result xor (hash(key) !& hash(val)) result = !$result proc len*(n: TomlValueRef): int = ## If `n` is a `TomlValueKind.Array`, it returns the number of elements. ## If `n` is a `TomlValueKind.Table`, it returns the number of pairs. ## Else it returns 0. case n.kind of TomlValueKind.Array: result = n.arrayVal.len of TomlValueKind.Table: result = n.tableVal.len else: discard proc `[]`*(node: TomlValueRef, name: string): TomlValueRef {.inline.} = ## Gets a field from a `TomlValueKind.Table`, which must not be nil. ## If the value at `name` does not exist, raises KeyError. assert(not isNil(node)) assert(node.kind == TomlValueKind.Table) result = node.tableVal[name] proc `[]`*(node: TomlValueRef, index: int): TomlValueRef {.inline.} = ## Gets the node at `index` in an Array. Result is undefined if `index` ## is out of bounds, but as long as array bound checks are enabled it will ## result in an exception. assert(not isNil(node)) assert(node.kind == TomlValueKind.Array) return node.arrayVal[index] proc hasKey*(node: TomlValueRef, key: string): bool = ## Checks if `key` exists in `node`. assert(node.kind == TomlValueKind.Table) result = node.tableVal.hasKey(key) proc contains*(node: TomlValueRef, key: string): bool = ## Checks if `key` exists in `node`. assert(node.kind == TomlValueKind.Table) node.tableVal.hasKey(key) proc contains*(node: TomlValueRef, val: TomlValueRef): bool = ## Checks if `val` exists in array `node`. assert(node.kind == TomlValueKind.Array) find(node.arrayVal, val) >= 0 proc existsKey*(node: TomlValueRef, key: string): bool {.deprecated.} = node.hasKey(key) ## Deprecated for `hasKey` proc `[]=`*(obj: TomlValueRef, key: string, val: TomlValueRef) {.inline.} = ## Sets a field from a `TomlValueKind.Table`. assert(obj.kind == TomlValueKind.Table) obj.tableVal[key] = val proc `{}`*(node: TomlValueRef, keys: varargs[string]): TomlValueRef = ## Traverses the node and gets the given value. If any of the ## keys do not exist, returns ``nil``. Also returns ``nil`` if one of the ## intermediate data structures is not an object. result = node for key in keys: if isNil(result) or result.kind != TomlValueKind.Table: return nil result = result.tableVal.getOrDefault(key) proc getOrDefault*(node: TomlValueRef, key: string): TomlValueRef = ## Gets a field from a `node`. If `node` is nil or not an object or ## value at `key` does not exist, returns nil if not isNil(node) and node.kind == TomlValueKind.Table: result = node.tableVal.getOrDefault(key) template simpleGetOrDefault*{`{}`(node, [key])}(node: TomlValueRef, key: string): TomlValueRef = node.getOrDefault(key) proc `{}=`*(node: TomlValueRef, keys: varargs[string], value: TomlValueRef) = ## Traverses the node and tries to set the value at the given location ## to ``value``. If any of the keys are missing, they are added. var node = node for i in 0..(keys.len-2): if not node.hasKey(keys[i]): node[keys[i]] = newTTable() node = node[keys[i]] node[keys[keys.len-1]] = value proc delete*(obj: TomlValueRef, key: string) = ## Deletes ``obj[key]``. assert(obj.kind == TomlValueKind.Table) if not obj.tableVal.hasKey(key): raise newException(IndexDefect, "key not in object") obj.tableVal.del(key) proc copy*(p: TomlValueRef): TomlValueRef = ## Performs a deep copy of `a`. case p.kind of TomlValueKind.String: result = newTString(p.stringVal) of TomlValueKind.Int: result = newTInt(p.intVal) of TomlValueKind.Float: result = newTFloat(p.floatVal) of TomlValueKind.Bool: result = newTBool(p.boolVal) of TomlValueKind.None: result = newTNull() of TomlValueKind.Table: result = newTTable() for key, val in pairs(p.tableVal): result.tableVal[key] = copy(val) of TomlValueKind.Array: result = newTArray() for i in items(p.arrayVal): result.arrayVal.add(copy(i)) of TomlValueKind.DateTime: new(result) result[] = p[] of TomlValueKind.Date: new(result) result[] = p[] of TomlValueKind.Time: new(result) result[] = p[]