Mercurial > games > semicongine
changeset 1371:7427925a4246
add: library to parse toml files
author | sam <sam@basx.dev> |
---|---|
date | Wed, 04 Dec 2024 16:49:38 +0700 |
parents | 18fda25d5d5f |
children | 2da623ec519b |
files | semicongine/thirdparty/attributions.txt semicongine/thirdparty/parsetoml.nim |
diffstat | 2 files changed, 1943 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/semicongine/thirdparty/attributions.txt Wed Dec 04 16:49:15 2024 +0700 +++ b/semicongine/thirdparty/attributions.txt Wed Dec 04 16:49:38 2024 +0700 @@ -2,3 +2,4 @@ win32 bindings (winim): Chen Kai-Hung, Ward (MIT) X11 bindings (x11): nim-lang (MIT) Compression library (zippy): Ryan Oldenburg (MIT) +TOML parsing (parsetoml.nim): Maurizio Tomasi and contributors (MIT)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/semicongine/thirdparty/parsetoml.nim Wed Dec 04 16:49:38 2024 +0700 @@ -0,0 +1,1942 @@ +## :License: MIT +## +## Introduction +## ============ +## This module implements a TOML parser that is compliant with v0.5.0 of its spec. +## +## Source +## ====== +## `Repo link <https://github.com/NimParsers/parsetoml>`_ +## + +# 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[]