1144 lines
39 KiB
D
1144 lines
39 KiB
D
// Written in the D programming language.
|
|
|
|
/**
|
|
*
|
|
* Tom's Obvious, Minimal Language (v1.0.0).
|
|
*
|
|
* License: $(HTTP https://github.com/Kripth/toml/blob/master/LICENSE, MIT)
|
|
* Authors: Kripth
|
|
* References: $(LINK https://github.com/toml-lang/toml/blob/master/README.md)
|
|
* Source: $(HTTP https://github.com/Kripth/toml/blob/master/src/toml/toml.d, toml/_toml.d)
|
|
*
|
|
*/
|
|
module toml.toml;
|
|
|
|
import std.algorithm : canFind, min, stripRight;
|
|
import std.array : Appender;
|
|
import std.ascii : newline;
|
|
import std.conv : text, to;
|
|
import std.datetime : Date, DateTimeD = DateTime, SysTime, TimeOfDayD = TimeOfDay;
|
|
import std.exception : assertThrown, enforce;
|
|
import std.string : indexOf, join, replace, strip;
|
|
import std.traits : isArray, isAssociativeArray, isFloatingPoint, isIntegral, isNumeric, KeyType;
|
|
import std.typecons : Tuple, tuple;
|
|
import std.utf : encode, UseReplacementDchar;
|
|
|
|
import toml.datetime : DateTime, TimeOfDay;
|
|
|
|
/**
|
|
* Flags that control how a TOML document is parsed and encoded.
|
|
*/
|
|
enum TOMLOptions {
|
|
none = 0x00,
|
|
unquotedStrings = 0x01, /// allow unquoted strings as values when parsing
|
|
}
|
|
|
|
/**
|
|
* TOML type enumeration.
|
|
*/
|
|
enum TOML_TYPE : byte {
|
|
STRING, /// Indicates the type of a TOMLValue.
|
|
INTEGER, /// ditto
|
|
FLOAT, /// ditto
|
|
OFFSET_DATETIME, /// ditto
|
|
LOCAL_DATETIME, /// ditto
|
|
LOCAL_DATE, /// ditto
|
|
LOCAL_TIME, /// ditto
|
|
ARRAY, /// ditto
|
|
TABLE, /// ditto
|
|
TRUE, /// ditto
|
|
FALSE /// ditto
|
|
}
|
|
|
|
alias TOMLType = TOML_TYPE;
|
|
alias TOMLfloat = TOML_TYPE.FLOAT;
|
|
|
|
/**
|
|
* Main table of a TOML document.
|
|
* It works as a TOMLValue with the TOML_TYPE.TABLE type.
|
|
*/
|
|
struct TOMLDocument {
|
|
|
|
public TOMLValue[string] table;
|
|
|
|
@safe scope:
|
|
|
|
public this(TOMLValue[string] table) pure {
|
|
this.table = table;
|
|
}
|
|
|
|
public this(TOMLValue value) pure {
|
|
this(value.table);
|
|
}
|
|
|
|
public string toString() const {
|
|
Appender!string appender;
|
|
foreach (key, value; this.table) {
|
|
appender.put(formatKey(key));
|
|
appender.put(" = ");
|
|
value.append(appender);
|
|
appender.put(newline);
|
|
}
|
|
return appender.data;
|
|
}
|
|
|
|
alias table this;
|
|
|
|
}
|
|
|
|
/**
|
|
* Value of a TOML value.
|
|
*/
|
|
struct TOMLValue {
|
|
|
|
private union Store {
|
|
string str;
|
|
long integer;
|
|
double floating;
|
|
SysTime offsetDatetime;
|
|
DateTime localDatetime;
|
|
Date localDate;
|
|
TimeOfDay localTime;
|
|
TOMLValue[] array;
|
|
TOMLValue[string] table;
|
|
}
|
|
|
|
private Store store;
|
|
private TOML_TYPE _type;
|
|
|
|
@safe scope:
|
|
|
|
public this(T)(T value) {
|
|
static if (is(T == TOML_TYPE)) {
|
|
this._type = value;
|
|
} else {
|
|
this.assign(value);
|
|
}
|
|
}
|
|
|
|
public pure nothrow @property @safe @nogc TOML_TYPE type() return const {
|
|
return this._type;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.STRING
|
|
*/
|
|
public @property @trusted string str() return const pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.STRING, "TOMLValue is not a string");
|
|
return this.store.str;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.INTEGER
|
|
*/
|
|
public @property @trusted long integer() return const pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.INTEGER, "TOMLValue is not an integer");
|
|
return this.store.integer;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.FLOAT
|
|
*/
|
|
public @property @trusted double floating() return const pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.FLOAT, "TOMLValue is not a float");
|
|
return this.store.floating;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.TRUE or TOML_TYPE.FALSE
|
|
*/
|
|
public @property @trusted bool boolean() return const pure {
|
|
switch (this._type) {
|
|
case TOML_TYPE.TRUE:
|
|
return true;
|
|
case TOML_TYPE.FALSE:
|
|
return false;
|
|
default:
|
|
throw new TOMLException("TOMLValue is not a boolean");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.OFFSET_DATETIME
|
|
*/
|
|
public @property @trusted ref inout(SysTime) offsetDatetime() return inout pure {
|
|
enforce!TOMLException(this.type == TOML_TYPE.OFFSET_DATETIME, "TOMLValue is not an offset datetime");
|
|
return this.store.offsetDatetime;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATETIME
|
|
*/
|
|
public @property @trusted ref inout(DateTime) localDatetime() return inout pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATETIME, "TOMLValue is not a local datetime");
|
|
return this.store.localDatetime;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATE
|
|
*/
|
|
public @property @trusted ref inout(Date) localDate() return inout pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATE, "TOMLValue is not a local date");
|
|
return this.store.localDate;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.LOCAL_TIME
|
|
*/
|
|
public @property @trusted ref inout(TimeOfDay) localTime() return inout pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.LOCAL_TIME, "TOMLValue is not a local time");
|
|
return this.store.localTime;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.ARRAY
|
|
*/
|
|
public @property @trusted ref inout(TOMLValue[]) array() return inout pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.ARRAY, "TOMLValue is not an array");
|
|
return this.store.array;
|
|
}
|
|
|
|
/**
|
|
* Throws: TOMLException if type is not TOML_TYPE.TABLE
|
|
*/
|
|
public @property @trusted ref inout(TOMLValue[string]) table() return inout pure {
|
|
enforce!TOMLException(this._type == TOML_TYPE.TABLE, "TOMLValue is not a table");
|
|
return this.store.table;
|
|
}
|
|
|
|
public inout(TOMLValue) opIndex(size_t index) return inout pure {
|
|
return this.array[index];
|
|
}
|
|
|
|
public inout(TOMLValue)* opBinaryRight(string op : "in")(string key) return inout pure {
|
|
return key in this.table;
|
|
}
|
|
|
|
public inout(TOMLValue) opIndex(string key) return inout pure {
|
|
return this.table[key];
|
|
}
|
|
|
|
private static enum opApplyImpl = q{
|
|
int result;
|
|
foreach (string key, ref value; this.table) {
|
|
result = dg(key, value);
|
|
if (result) {
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
static if (__VERSION__ >= 2099)
|
|
{
|
|
public int opApply(scope int delegate(string, ref TOMLValue) @safe dg) @safe { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @safe dg) @safe { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref TOMLValue) @safe dg) @safe { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe dg) @safe { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @safe dg) @safe const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe dg) @safe const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref TOMLValue) @safe pure dg) @safe pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @safe pure dg) @safe pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref TOMLValue) @safe pure dg) @safe pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe pure dg) @safe pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @safe pure dg) @safe pure const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe pure dg) @safe pure const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref TOMLValue) @system dg) @system { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @system dg) @system { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref TOMLValue) @system dg) @system { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @system dg) @system { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @system dg) @system const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @system dg) @system const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref TOMLValue) @system pure dg) @system pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @system pure dg) @system pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref TOMLValue) @system pure dg) @system pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @system pure dg) @system pure { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @system pure dg) @system pure const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, scope ref const TOMLValue) @system pure dg) @system pure const { mixin(opApplyImpl); }
|
|
}
|
|
else
|
|
{
|
|
public int opApply(scope int delegate(string, ref TOMLValue) @system dg) @system { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref TOMLValue) @safe dg) @safe { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @system dg) @system const { mixin(opApplyImpl); }
|
|
public int opApply(scope int delegate(string, ref const TOMLValue) @safe dg) @safe const { mixin(opApplyImpl); }
|
|
}
|
|
|
|
public void opAssign(T)(T value) pure {
|
|
this.assign(value);
|
|
}
|
|
|
|
private void assign(T)(T value) @trusted pure {
|
|
static if (is(T == TOMLValue)) {
|
|
this.store = value.store;
|
|
this._type = value._type;
|
|
} else static if (is(T : string)) {
|
|
this.store.str = value;
|
|
this._type = TOML_TYPE.STRING;
|
|
} else static if (isIntegral!T) {
|
|
this.store.integer = value;
|
|
this._type = TOML_TYPE.INTEGER;
|
|
} else static if (isFloatingPoint!T) {
|
|
this.store.floating = value.to!double;
|
|
this._type = TOML_TYPE.FLOAT;
|
|
} else static if (is(T == SysTime)) {
|
|
this.store.offsetDatetime = value;
|
|
this._type = TOML_TYPE.OFFSET_DATETIME;
|
|
} else static if (is(T == DateTime)) {
|
|
this.store.localDatetime = value;
|
|
this._type = TOML_TYPE.LOCAL_DATETIME;
|
|
} else static if (is(T == DateTimeD)) {
|
|
this.store.localDatetime = DateTime(value.date, TimeOfDay(value.timeOfDay));
|
|
this._type = TOML_TYPE.LOCAL_DATETIME;
|
|
} else static if (is(T == Date)) {
|
|
this.store.localDate = value;
|
|
this._type = TOML_TYPE.LOCAL_DATE;
|
|
} else static if (is(T == TimeOfDay)) {
|
|
this.store.localTime = value;
|
|
this._type = TOML_TYPE.LOCAL_TIME;
|
|
} else static if (is(T == TimeOfDayD)) {
|
|
this.store.localTime = TimeOfDay(value);
|
|
this._type = TOML_TYPE.LOCAL_TIME;
|
|
} else static if (isArray!T) {
|
|
static if (is(T == TOMLValue[])) {
|
|
if (value.length) {
|
|
// verify that every element has the same type
|
|
TOML_TYPE cmp = value[0].type;
|
|
foreach (element; value[1 .. $]) {
|
|
enforce!TOMLException(element.type == cmp, "Array's values must be of the same type");
|
|
}
|
|
}
|
|
alias data = value;
|
|
} else {
|
|
TOMLValue[] data;
|
|
foreach (element; value) {
|
|
data ~= TOMLValue(element);
|
|
}
|
|
}
|
|
this.store.array = data;
|
|
this._type = TOML_TYPE.ARRAY;
|
|
} else static if (isAssociativeArray!T && is(KeyType!T : string)) {
|
|
static if (is(T == TOMLValue[string])) {
|
|
alias data = value;
|
|
} else {
|
|
TOMLValue[string] data;
|
|
foreach (key, v; value) {
|
|
data[key] = v;
|
|
}
|
|
}
|
|
this.store.table = data;
|
|
this._type = TOML_TYPE.TABLE;
|
|
} else static if (is(T == bool)) {
|
|
_type = value ? TOML_TYPE.TRUE : TOML_TYPE.FALSE;
|
|
} else {
|
|
static assert(0);
|
|
}
|
|
}
|
|
|
|
public bool opEquals(T)(scope T value) const @trusted pure {
|
|
static if (is(T == TOMLValue)) {
|
|
if (this._type != value._type) {
|
|
return false;
|
|
}
|
|
final switch (this.type) with (TOML_TYPE) {
|
|
case STRING:
|
|
return this.store.str == value.store.str;
|
|
case INTEGER:
|
|
return this.store.integer == value.store.integer;
|
|
case FLOAT:
|
|
return this.store.floating == value.store.floating;
|
|
case OFFSET_DATETIME:
|
|
return this.store.offsetDatetime == value.store.offsetDatetime;
|
|
case LOCAL_DATETIME:
|
|
return this.store.localDatetime == value.store.localDatetime;
|
|
case LOCAL_DATE:
|
|
return this.store.localDate == value.store.localDate;
|
|
case LOCAL_TIME:
|
|
return this.store.localTime == value.store.localTime;
|
|
case ARRAY:
|
|
return this.store.array == value.store.array;
|
|
//case TABLE: return this.store.table == value.store.table; // causes errors
|
|
case TABLE:
|
|
return this.opEquals(value.store.table);
|
|
case TRUE:
|
|
case FALSE:
|
|
return true;
|
|
}
|
|
} else static if (is(T : string)) {
|
|
return this._type == TOML_TYPE.STRING && this.store.str == value;
|
|
} else static if (isNumeric!T) {
|
|
if (this._type == TOML_TYPE.INTEGER) {
|
|
return this.store.integer == value;
|
|
} else if (this._type == TOML_TYPE.FLOAT) {
|
|
return this.store.floating == value;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else static if (is(T == SysTime)) {
|
|
return this._type == TOML_TYPE.OFFSET_DATETIME && this.store.offsetDatetime == value;
|
|
} else static if (is(T == DateTime)) {
|
|
return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value.dateTime
|
|
&& this.store.localDatetime.timeOfDay.fracSecs == value.timeOfDay.fracSecs;
|
|
} else static if (is(T == DateTimeD)) {
|
|
return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value;
|
|
} else static if (is(T == Date)) {
|
|
return this._type == TOML_TYPE.LOCAL_DATE && this.store.localDate == value;
|
|
} else static if (is(T == TimeOfDay)) {
|
|
return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime.timeOfDay == value.timeOfDay
|
|
&& this.store.localTime.fracSecs == value.fracSecs;
|
|
} else static if (is(T == TimeOfDayD)) {
|
|
return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime == value;
|
|
} else static if (isArray!T) {
|
|
if (this._type != TOML_TYPE.ARRAY || this.store.array.length != value.length) {
|
|
return false;
|
|
}
|
|
foreach (i, element; this.store.array) {
|
|
if (element != value[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else static if (isAssociativeArray!T && is(KeyType!T : string)) {
|
|
if (this._type != TOML_TYPE.TABLE || this.store.table.length != value.length) {
|
|
return false;
|
|
}
|
|
foreach (key, v; this.store.table) {
|
|
auto cmp = key in value;
|
|
if (cmp is null || v != *cmp) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else static if (is(T == bool)) {
|
|
return value ? _type == TOML_TYPE.TRUE : _type == TOML_TYPE.FALSE;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
size_t toHash() const @trusted @nogc pure nothrow {
|
|
final switch (_type) with (TOML_TYPE) {
|
|
case STRING:
|
|
return hashOf(store.str);
|
|
case INTEGER:
|
|
return hashOf(store.integer);
|
|
case FLOAT:
|
|
return hashOf(store.floating);
|
|
case OFFSET_DATETIME:
|
|
return hashOf(store.offsetDatetime);
|
|
case LOCAL_DATETIME:
|
|
static if (__traits(compiles, () @nogc { hashOf(store.localDatetime); }))
|
|
return hashOf(store.localDatetime);
|
|
else
|
|
return hashOf(tuple(store.localDatetime.date, store.localDatetime.timeOfDay));
|
|
case LOCAL_DATE:
|
|
return hashOf(store.localDate);
|
|
case LOCAL_TIME:
|
|
return hashOf(store.localTime);
|
|
case ARRAY:
|
|
return hashOf(store.array);
|
|
case TABLE:
|
|
return hashOf(store.table);
|
|
case TRUE:
|
|
return hashOf(true);
|
|
case FALSE:
|
|
return hashOf(false);
|
|
}
|
|
}
|
|
|
|
public void append(Output)(scope ref Output appender) const @trusted {
|
|
final switch (this._type) with (TOML_TYPE) {
|
|
case STRING:
|
|
appender.put(formatString(this.store.str));
|
|
break;
|
|
case INTEGER:
|
|
appender.put(this.store.integer.to!string);
|
|
break;
|
|
case FLOAT:
|
|
immutable str = this.store.floating.to!string;
|
|
appender.put(str);
|
|
if (!str.canFind('.') && !str.canFind('e')) {
|
|
appender.put(".0");
|
|
}
|
|
break;
|
|
case OFFSET_DATETIME:
|
|
appender.put(this.store.offsetDatetime.toISOExtString());
|
|
break;
|
|
case LOCAL_DATETIME:
|
|
appender.put(this.store.localDatetime.toISOExtString());
|
|
break;
|
|
case LOCAL_DATE:
|
|
appender.put(this.store.localDate.toISOExtString());
|
|
break;
|
|
case LOCAL_TIME:
|
|
appender.put(this.store.localTime.toISOExtString());
|
|
break;
|
|
case ARRAY:
|
|
appender.put("[");
|
|
foreach (i, value; this.store.array) {
|
|
value.append(appender);
|
|
if (i + 1 < this.store.array.length) {
|
|
appender.put(", ");
|
|
}
|
|
}
|
|
appender.put("]");
|
|
break;
|
|
case TABLE:
|
|
// display as an inline table
|
|
appender.put("{ ");
|
|
size_t i = 0;
|
|
foreach (key, value; this.store.table) {
|
|
appender.put(formatKey(key));
|
|
appender.put(" = ");
|
|
value.append(appender);
|
|
if (++i != this.store.table.length) {
|
|
appender.put(", ");
|
|
}
|
|
}
|
|
appender.put(" }");
|
|
break;
|
|
case TRUE:
|
|
appender.put("true");
|
|
break;
|
|
case FALSE:
|
|
appender.put("false");
|
|
break;
|
|
}
|
|
}
|
|
|
|
public string toString() const {
|
|
Appender!string appender;
|
|
this.append(appender);
|
|
return appender.data;
|
|
}
|
|
|
|
}
|
|
|
|
private string formatKey(scope return string str) pure @safe {
|
|
foreach (c; str) {
|
|
if ((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && c != '-' && c != '_') {
|
|
return formatString(str);
|
|
}
|
|
}
|
|
return str;
|
|
}
|
|
|
|
private string formatString(scope return inout(char)[] str) pure @safe {
|
|
Appender!string appender;
|
|
appender.put('"');
|
|
foreach (c; str) {
|
|
switch (c) {
|
|
case '"':
|
|
appender.put("\\\"");
|
|
break;
|
|
case '\\':
|
|
appender.put("\\\\");
|
|
break;
|
|
case '\b':
|
|
appender.put("\\b");
|
|
break;
|
|
case '\f':
|
|
appender.put("\\f");
|
|
break;
|
|
case '\n':
|
|
appender.put("\\n");
|
|
break;
|
|
case '\r':
|
|
appender.put("\\r");
|
|
break;
|
|
case '\t':
|
|
appender.put("\\t");
|
|
break;
|
|
default:
|
|
appender.put(c);
|
|
}
|
|
}
|
|
appender.put('"');
|
|
return appender.data;
|
|
}
|
|
|
|
/**
|
|
* Parses a TOML document.
|
|
* Params:
|
|
* data = String in toml format to parse. Slices out of this will be returned
|
|
* _iff_ `unquotedStrings` is enabled in the options and a `string` is passed
|
|
* into this function.
|
|
* options = Parsing option
|
|
*
|
|
* Returns: a TOMLDocument with the parsed data
|
|
* Throws:
|
|
* TOMLParserException when the document's syntax is incorrect
|
|
*/
|
|
TOMLDocument parseTOML(scope const(char)[] data, TOMLOptions options = TOMLOptions.none) @safe {
|
|
return parseTOMLImpl!true(data, options);
|
|
}
|
|
/// ditto
|
|
TOMLDocument parseTOML(scope return string data, TOMLOptions options = TOMLOptions.none) @safe {
|
|
return parseTOMLImpl!false(data, options);
|
|
}
|
|
|
|
private TOMLDocument parseTOMLImpl(bool dupData)(scope const(char)[] data, TOMLOptions options = TOMLOptions.none) @safe {
|
|
size_t index = 0;
|
|
|
|
/**
|
|
* Throws a TOMLParserException at the current line and column.
|
|
*/
|
|
void error(string message) {
|
|
if (index >= data.length) {
|
|
index = data.length;
|
|
}
|
|
size_t i, line, column;
|
|
while (i < index) {
|
|
if (data[i++] == '\n') {
|
|
line++;
|
|
column = 0;
|
|
} else {
|
|
column++;
|
|
}
|
|
}
|
|
throw new TOMLParserException(message, line + 1, column);
|
|
}
|
|
|
|
/**
|
|
* Throws a TOMLParserException throught the error function if
|
|
* cond is false.
|
|
*/
|
|
void enforceParser(bool cond, lazy string message) {
|
|
if (!cond) {
|
|
error(message);
|
|
}
|
|
}
|
|
|
|
TOMLValue[string] _ret;
|
|
auto current = (() @trusted => &_ret)();
|
|
|
|
string[][] tableNames;
|
|
|
|
void setImpl(scope TOMLValue[string]* table, string[] keys, string[] original, TOMLValue value) {
|
|
auto ptr = keys[0] in *table;
|
|
if (keys.length == 1) {
|
|
// should not be there
|
|
enforceParser(ptr is null, "Key is already defined");
|
|
(*table)[keys[0]] = value;
|
|
} else {
|
|
// must be a table
|
|
if (ptr !is null) {
|
|
enforceParser((*ptr).type == TOML_TYPE.TABLE, join(original[0 .. $ - keys.length],
|
|
".") ~ " is already defined and is not a table");
|
|
} else {
|
|
(*table)[keys[0]] = (TOMLValue[string]).init;
|
|
}
|
|
setImpl((() @trusted => &((*table)[keys[0]].table()))(), keys[1 .. $], original, value);
|
|
}
|
|
}
|
|
|
|
void set(string[] keys, TOMLValue value) {
|
|
setImpl(current, keys, keys, value);
|
|
}
|
|
|
|
/**
|
|
* Removes whitespace characters and comments.
|
|
* Return: whether there's still data to read
|
|
*/
|
|
bool clear(bool clear_newline = true)() {
|
|
static if (clear_newline) {
|
|
enum chars = " \t\r\n";
|
|
} else {
|
|
enum chars = " \t\r";
|
|
}
|
|
if (index < data.length) {
|
|
if (chars.canFind(data[index])) {
|
|
index++;
|
|
return clear!clear_newline();
|
|
} else if (data[index] == '#') {
|
|
// skip until end of line
|
|
while (++index < data.length && data[index] != '\n') {
|
|
}
|
|
static if (clear_newline) {
|
|
index++; // point at the next character
|
|
return clear();
|
|
} else {
|
|
return true;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the given character is valid in an unquoted key.
|
|
*/
|
|
bool isValidKeyChar(immutable char c) {
|
|
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_';
|
|
}
|
|
|
|
string readQuotedString(bool multiline)() {
|
|
Appender!string ret;
|
|
bool backslash = false;
|
|
while (index < data.length) {
|
|
static if (!multiline) {
|
|
enforceParser(data[index] != '\n', "Unterminated quoted string");
|
|
}
|
|
if (backslash) {
|
|
void readUnicode(size_t size)() {
|
|
enforceParser(index + size < data.length, "Invalid UTF-8 sequence");
|
|
char[4] buffer;
|
|
immutable len = encode!(UseReplacementDchar.yes)(buffer, cast(dchar)to!ulong(data[index + 1 .. index + 1 + size], 16));
|
|
ret.put(buffer[0 .. len].idup);
|
|
index += size;
|
|
}
|
|
|
|
switch (data[index]) {
|
|
case '"':
|
|
ret.put('"');
|
|
break;
|
|
case '\\':
|
|
ret.put('\\');
|
|
break;
|
|
case 'b':
|
|
ret.put('\b');
|
|
break;
|
|
case 't':
|
|
ret.put('\t');
|
|
break;
|
|
case 'n':
|
|
ret.put('\n');
|
|
break;
|
|
case 'f':
|
|
ret.put('\f');
|
|
break;
|
|
case 'r':
|
|
ret.put('\r');
|
|
break;
|
|
case 'u':
|
|
readUnicode!4();
|
|
break;
|
|
case 'U':
|
|
readUnicode!8();
|
|
break;
|
|
default:
|
|
static if (multiline) {
|
|
index++;
|
|
if (clear()) {
|
|
// remove whitespace characters until next valid character
|
|
index--;
|
|
break;
|
|
}
|
|
}
|
|
enforceParser(false, "Invalid escape sequence: '\\" ~ (index < data.length ? [cast(immutable) data[index]] : "EOF") ~ "'");
|
|
}
|
|
backslash = false;
|
|
} else {
|
|
if (data[index] == '\\') {
|
|
backslash = true;
|
|
} else if (data[index] == '"') {
|
|
// string closed
|
|
index++;
|
|
static if (multiline) {
|
|
// control that the string is really closed
|
|
if (index + 2 <= data.length && data[index .. index + 2] == "\"\"") {
|
|
index += 2;
|
|
return ret.data.stripFirstLine;
|
|
} else {
|
|
ret.put("\"");
|
|
continue;
|
|
}
|
|
} else {
|
|
return ret.data;
|
|
}
|
|
} else {
|
|
static if (multiline) {
|
|
mixin(doLineConversion);
|
|
}
|
|
ret.put(data[index]);
|
|
}
|
|
}
|
|
index++;
|
|
}
|
|
error("Expecting \" (double quote) but found EOF");
|
|
assert(0);
|
|
}
|
|
|
|
string readSimpleQuotedString(bool multiline)() {
|
|
Appender!string ret;
|
|
while (index < data.length) {
|
|
static if (!multiline) {
|
|
enforceParser(data[index] != '\n', "Unterminated quoted string");
|
|
}
|
|
if (data[index] == '\'') {
|
|
// closed
|
|
index++;
|
|
static if (multiline) {
|
|
// there must be 3 of them
|
|
if (index + 2 <= data.length && data[index .. index + 2] == "''") {
|
|
index += 2;
|
|
return ret.data.stripFirstLine;
|
|
} else {
|
|
ret.put("'");
|
|
}
|
|
} else {
|
|
return ret.data;
|
|
}
|
|
} else {
|
|
static if (multiline) {
|
|
mixin(doLineConversion);
|
|
}
|
|
ret.put(data[index++]);
|
|
}
|
|
}
|
|
error("Expecting ' (single quote) but found EOF");
|
|
assert(0);
|
|
}
|
|
|
|
const(char)[] removeUnderscores(scope return const(char)[] strInput, scope const(char)[][] ranges...) @safe {
|
|
bool checkRange(char c) {
|
|
foreach (range; ranges) {
|
|
if (c >= range[0] && c <= range[1]) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
auto str = strInput;
|
|
|
|
bool underscore = false;
|
|
for (size_t i = 0; i < str.length; i++) {
|
|
if (str[i] == '_') {
|
|
if (underscore || i == 0 || i == str.length - 1 || !checkRange(str[i - 1]) || !checkRange(str[i + 1])) {
|
|
throw new Exception("");
|
|
}
|
|
str = str[0 .. i] ~ str[i + 1 .. $];
|
|
i--;
|
|
underscore = true;
|
|
} else {
|
|
underscore = false;
|
|
}
|
|
}
|
|
return str;
|
|
}
|
|
|
|
TOMLValue readSpecial() {
|
|
immutable start = index;
|
|
while (index < data.length && !"\t\r\n,]}#".canFind(data[index])) {
|
|
index++;
|
|
}
|
|
const(char)[] ret = data[start .. index].stripRight(' ');
|
|
enforceParser(ret.length > 0, "Invalid empty value");
|
|
switch (ret) {
|
|
case "true":
|
|
return TOMLValue(true);
|
|
case "false":
|
|
return TOMLValue(false);
|
|
case "inf":
|
|
case "+inf":
|
|
return TOMLValue(double.infinity);
|
|
case "-inf":
|
|
return TOMLValue(-double.infinity);
|
|
case "nan":
|
|
case "+nan":
|
|
return TOMLValue(double.nan);
|
|
case "-nan":
|
|
return TOMLValue(-double.nan);
|
|
default:
|
|
const original = ret;
|
|
try {
|
|
if (ret.length >= 10 && ret[4] == '-' && ret[7] == '-') {
|
|
// date or datetime
|
|
if (ret.length >= 19 && (ret[10] == 'T' || ret[10] == ' ') && ret[13] == ':' && ret[16] == ':') {
|
|
// datetime
|
|
if (ret[10] == ' ') {
|
|
ret = ret[0 .. 10] ~ 'T' ~ ret[11 .. $];
|
|
}
|
|
if (ret[19 .. $].canFind("-") || ret[$ - 1] == 'Z') {
|
|
// has timezone
|
|
return TOMLValue(SysTime.fromISOExtString(ret));
|
|
} else {
|
|
// is space allowed instead of T?
|
|
return TOMLValue(DateTime.fromISOExtString(ret));
|
|
}
|
|
} else {
|
|
return TOMLValue(Date.fromISOExtString(ret));
|
|
}
|
|
} else if (ret.length >= 8 && ret[2] == ':' && ret[5] == ':') {
|
|
return TOMLValue(TimeOfDay.fromISOExtString(ret));
|
|
}
|
|
if (ret.length > 2 && ret[0] == '0') {
|
|
switch (ret[1]) {
|
|
case 'x':
|
|
return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "09", "AZ", "az"), 16));
|
|
case 'o':
|
|
return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "08"), 8));
|
|
case 'b':
|
|
return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "01"), 2));
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (ret.canFind('.') || ret.canFind('e') || ret.canFind('E')) {
|
|
return TOMLValue(to!double(removeUnderscores(ret, "09")));
|
|
} else {
|
|
if (ret[0] != '0' || ret.length == 1) {
|
|
return TOMLValue(to!long(removeUnderscores(ret, "09")));
|
|
}
|
|
}
|
|
} catch (Exception) {
|
|
}
|
|
// not a valid value at this point
|
|
if (options & TOMLOptions.unquotedStrings) {
|
|
static if (dupData)
|
|
return TOMLValue(original.idup);
|
|
else
|
|
return TOMLValue((() @trusted => cast(string) original)());
|
|
} else {
|
|
error(text("Invalid type: '", original.idup, "'"));
|
|
}
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
string readKey() {
|
|
enforceParser(index < data.length, "Key declaration expected but found EOF");
|
|
string ret;
|
|
if (data[index] == '"') {
|
|
index++;
|
|
ret = readQuotedString!false();
|
|
} else if (data[index] == '\'') {
|
|
index++;
|
|
ret = readSimpleQuotedString!false();
|
|
} else {
|
|
Appender!string appender;
|
|
while (index < data.length && isValidKeyChar(data[index])) {
|
|
appender.put(data[index++]);
|
|
}
|
|
ret = appender.data;
|
|
enforceParser(ret.length != 0, "Key is empty or contains invalid characters");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
string[] readKeys() {
|
|
string[] keys;
|
|
index--;
|
|
do {
|
|
index++;
|
|
clear!false();
|
|
keys ~= readKey();
|
|
clear!false();
|
|
}
|
|
while (index < data.length && data[index] == '.');
|
|
enforceParser(keys.length != 0, "Key cannot be empty");
|
|
return keys;
|
|
}
|
|
|
|
TOMLValue readValue() {
|
|
if (index < data.length) {
|
|
switch (data[index++]) {
|
|
case '"':
|
|
if (index + 2 <= data.length && data[index .. index + 2] == "\"\"") {
|
|
index += 2;
|
|
return TOMLValue(readQuotedString!true());
|
|
} else {
|
|
return TOMLValue(readQuotedString!false());
|
|
}
|
|
case '\'':
|
|
if (index + 2 <= data.length && data[index .. index + 2] == "''") {
|
|
index += 2;
|
|
return TOMLValue(readSimpleQuotedString!true());
|
|
} else {
|
|
return TOMLValue(readSimpleQuotedString!false());
|
|
}
|
|
case '[':
|
|
clear();
|
|
TOMLValue[] array;
|
|
bool comma = true;
|
|
while (data[index] != ']') { //TODO check range error
|
|
enforceParser(comma, "Elements of the array must be separated with a comma");
|
|
array ~= readValue();
|
|
clear!false(); // spaces allowed between elements and commas
|
|
if (data[index] == ',') { //TODO check range error
|
|
index++;
|
|
comma = true;
|
|
} else {
|
|
comma = false;
|
|
}
|
|
clear(); // spaces and newlines allowed between elements
|
|
}
|
|
index++;
|
|
return TOMLValue(array);
|
|
case '{':
|
|
clear!false();
|
|
TOMLValue[string] table;
|
|
bool comma = true;
|
|
while (data[index] != '}') { //TODO check range error
|
|
enforceParser(comma, "Elements of the table must be separated with a comma");
|
|
auto keys = readKeys();
|
|
enforceParser(clear!false() && data[index++] == '=' && clear!false(), "Expected value after key declaration");
|
|
setImpl((() @trusted => &table)(), keys, keys, readValue());
|
|
enforceParser(clear!false(), "Expected ',' or '}' but found " ~ (index < data.length ? "EOL" : "EOF"));
|
|
if (data[index] == ',') {
|
|
index++;
|
|
comma = true;
|
|
} else {
|
|
comma = false;
|
|
}
|
|
clear!false();
|
|
}
|
|
index++;
|
|
return TOMLValue(table);
|
|
default:
|
|
index--;
|
|
break;
|
|
}
|
|
}
|
|
return readSpecial();
|
|
}
|
|
|
|
void readKeyValue(string[] keys) {
|
|
if (clear()) {
|
|
enforceParser(data[index++] == '=', "Expected '=' after key declaration");
|
|
if (clear!false()) {
|
|
set(keys, readValue());
|
|
// there must be nothing after the key/value declaration except comments and whitespaces
|
|
if (clear!false())
|
|
enforceParser(data[index] == '\n', "Invalid characters after value declaration: " ~ data[index]);
|
|
} else {
|
|
//TODO throw exception (missing value)
|
|
}
|
|
} else {
|
|
//TODO throw exception (missing value)
|
|
}
|
|
}
|
|
|
|
void next() @safe {
|
|
if (data[index] == '[') {
|
|
// reset base
|
|
current = (() @trusted => &_ret)();
|
|
index++;
|
|
bool array = false;
|
|
if (index < data.length && data[index] == '[') {
|
|
index++;
|
|
array = true;
|
|
}
|
|
string[] keys = readKeys();
|
|
enforceParser(index < data.length && data[index++] == ']', "Invalid " ~ (array ? "array" : "table") ~ " key declaration");
|
|
if (array) {
|
|
enforceParser(index < data.length && data[index++] == ']', "Invalid array key declaration");
|
|
}
|
|
if (!array) {
|
|
//TODO only enforce if every key is a table
|
|
enforceParser(!tableNames.canFind(keys), "Table name has already been directly defined");
|
|
tableNames ~= keys;
|
|
}
|
|
void update(string key, bool allowArray = true) {
|
|
if (key !in *current) {
|
|
set([key], TOMLValue(TOML_TYPE.TABLE));
|
|
}
|
|
auto ret = (*current)[key];
|
|
if (ret.type == TOML_TYPE.TABLE) {
|
|
current = (() @trusted => &((*current)[key].table()))();
|
|
} else if (allowArray && ret.type == TOML_TYPE.ARRAY) {
|
|
current = (() @trusted => &((*current)[key].array[$ - 1].table()))();
|
|
} else {
|
|
error("Invalid type");
|
|
}
|
|
}
|
|
|
|
foreach (immutable key; keys[0 .. $ - 1]) {
|
|
update(key);
|
|
}
|
|
if (array) {
|
|
auto exist = keys[$ - 1] in *current;
|
|
if (exist) {
|
|
//TODO must be an array
|
|
(*exist).array ~= TOMLValue(TOML_TYPE.TABLE);
|
|
} else {
|
|
set([keys[$ - 1]], TOMLValue([TOMLValue(TOML_TYPE.TABLE)]));
|
|
}
|
|
current = (() @trusted => &((*current)[keys[$ - 1]].array[$ - 1].table()))();
|
|
} else {
|
|
update(keys[$ - 1], false);
|
|
}
|
|
} else {
|
|
readKeyValue(readKeys());
|
|
}
|
|
|
|
}
|
|
|
|
while (clear()) {
|
|
next();
|
|
}
|
|
|
|
return TOMLDocument(_ret);
|
|
|
|
}
|
|
|
|
private @property string stripFirstLine(string data) pure @safe {
|
|
size_t i = 0;
|
|
while (i < data.length && data[i] != '\n') {
|
|
i++;
|
|
}
|
|
if (data[0 .. i].strip.length == 0) {
|
|
return data[i + 1 .. $];
|
|
} else {
|
|
return data;
|
|
}
|
|
}
|
|
|
|
version (Windows) {
|
|
// convert posix's line ending to windows'
|
|
private enum doLineConversion = q{
|
|
if(data[index] == '\n' && index != 0 && data[index-1] != '\r') {
|
|
index++;
|
|
ret.put("\r\n");
|
|
continue;
|
|
}
|
|
};
|
|
} else {
|
|
// convert windows' line ending to posix's
|
|
private enum doLineConversion = q{
|
|
if(data[index] == '\r' && index + 1 < data.length && data[index+1] == '\n') {
|
|
index += 2;
|
|
ret.put("\n");
|
|
continue;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Exception thrown on generic TOML errors.
|
|
*/
|
|
class TOMLException : Exception {
|
|
|
|
public this(string message, string file = __FILE__, size_t line = __LINE__) pure @safe scope {
|
|
super(message, file, line);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Exception thrown during the parsing of TOML document.
|
|
*/
|
|
class TOMLParserException : TOMLException {
|
|
|
|
private Tuple!(size_t, "line", size_t, "column") _position;
|
|
|
|
public this(string message, size_t line, size_t column, string file = __FILE__, size_t _line = __LINE__) pure @safe scope {
|
|
super(message ~ " (" ~ to!string(line) ~ ":" ~ to!string(column) ~ ")", file, _line);
|
|
this._position.line = line;
|
|
this._position.column = column;
|
|
}
|
|
|
|
/**
|
|
* Gets the position (line and column) where the parsing expection
|
|
* has occured.
|
|
*/
|
|
public pure nothrow @property @safe @nogc auto position() scope {
|
|
return this._position;
|
|
}
|
|
|
|
}
|