migrate to initial.d over dini

initial.d is my very own INI parser, see
  <https://git.sr.ht/~jeremy/initial>
This commit is contained in:
Jeremy Baxter 2024-02-13 09:31:56 +13:00
parent de9e18f06e
commit 2e93c891fd
10 changed files with 509 additions and 1642 deletions

34
COPYING
View file

@ -1,5 +1,35 @@
This software is licensed under the GNU General Public License, version 2. The file initial.d is licensed under the Boost Software License,
The license is as follows: Version 1.0. The contents of that license follows:
Copyright (c) 2024 Jeremy Baxter. All rights reserved.
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
This software (esv) is licensed under the GNU General Public License,
version 2. The contents of that license follows:
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 2, June 1991 Version 2, June 1991

23
configure vendored
View file

@ -7,9 +7,8 @@ set -e
import=import import=import
mkf=Makefile mkf=Makefile
cflags='-I'"$import" objs='esv.o esvapi.o initial.o'
objs='esv.o esvapi.o' srcs='esv.d esvapi.d initial.d'
srcs='esv.d esvapi.d'
makefile=' makefile='
IMPORT = '"$import"' IMPORT = '"$import"'
PREFIX = /usr/local PREFIX = /usr/local
@ -17,9 +16,7 @@ MANPREFIX = ${PREFIX}/man
DC = ${_DC} DC = ${_DC}
CFLAGS = ${_CFLAGS} CFLAGS = ${_CFLAGS}
OBJS = ${_OBJS} ini.a OBJS = ${_OBJS}
INIOBJS = ${IMPORT}/dini/package.o ${IMPORT}/dini/parser.o \\
${IMPORT}/dini/reader.o ${IMPORT}/dini/utils.o
all: esv all: esv
@ -31,13 +28,6 @@ esv: ${OBJS}
.d.o: .d.o:
${DC} ${CFLAGS} -c $< ${DC} ${CFLAGS} -c $<
ini.a: ${IMPORT}/dini/{package,parser,reader,utils}.d
${DC} ${CFLAGS} -of=${IMPORT}/dini/package.o -c ${IMPORT}/dini/package.d
${DC} ${CFLAGS} -of=${IMPORT}/dini/parser.o -c ${IMPORT}/dini/parser.d
${DC} ${CFLAGS} -of=${IMPORT}/dini/reader.o -c ${IMPORT}/dini/reader.d
${DC} ${CFLAGS} -of=${IMPORT}/dini/utils.o -c ${IMPORT}/dini/utils.d
ar -cr $@ ${INIOBJS}
clean: clean:
rm -f esv ${OBJS} ${INIOBJS} rm -f esv ${OBJS} ${INIOBJS}
@ -152,9 +142,6 @@ done
# creating the makefile # creating the makefile
u_cflags="$cflags"
unset cflags
gen_DC gen_DC
gen_CFLAGS gen_CFLAGS
gen_LDFLAGS gen_LDFLAGS
@ -167,7 +154,7 @@ _CFLAGS = %s
_LDFLAGS = %s _LDFLAGS = %s
' \ ' \
"$dc" \ "$dc" \
"$cflags $u_cflags" \ "$cflags" \
"$ldflags" \ "$ldflags" \
>>"$mkf" >>"$mkf"
## generate obj list ## generate obj list
@ -185,7 +172,7 @@ printf "$makefile" >>"$mkf"
printf '\n# begin generated dependencies\n' >>"$mkf" printf '\n# begin generated dependencies\n' >>"$mkf"
i=1 i=1
for obj in $objs; do for obj in $objs; do
"$dc" $u_cflags -O0 -o- -makedeps \ "$dc" -O0 -o- -makedeps \
"$(printf "$srcs" | awk '{print $'"$i"'}')" >>"$mkf" "$(printf "$srcs" | awk '{print $'"$i"'}')" >>"$mkf"
i="$(($i + 1))" i="$(($i + 1))"
done done

51
esv.d
View file

@ -32,9 +32,10 @@ import std.regex : regex, matchFirst, replaceAll, replaceFirst;
import std.stdio : writef, writeln, writefln, stderr; import std.stdio : writef, writeln, writefln, stderr;
import std.string : splitLines; import std.string : splitLines;
import initial;
import config; import config;
import esvapi; import esvapi;
import dini;
enum VERSION = "0.2.0"; enum VERSION = "0.2.0";
@ -92,7 +93,7 @@ run(string[] args)
{ {
string apiKey; string apiKey;
string configPath; string configPath;
Ini iniData; INIUnit ini;
ESVApi esv; ESVApi esv;
/* Parse command-line options */ /* Parse command-line options */
@ -177,17 +178,14 @@ key = %s
"(DEFAULT_APIKEY)); "(DEFAULT_APIKEY));
} }
} }
iniData = Ini.Parse(configPath); readINIFile(ini, configPath);
} catch (FileException e) { } catch (FileException e) {
/* filesystem syscall errors */ /* filesystem syscall errors */
throw new Exception(e.msg); throw new Exception(e.msg);
} }
try {
apiKey = iniData["api"].getKey("key"); apiKey = ini["api"].key("key");
} catch (IniException e) { enforce(apiKey != null,
apiKey = "";
}
enforce(apiKey != "",
"API key not present in configuration file; cannot proceed"); "API key not present in configuration file; cannot proceed");
esv = new ESVApi(apiKey); esv = new ESVApi(apiKey);
@ -223,38 +221,23 @@ key = %s
return true; return true;
} }
esv.extraParameters = iniData["api"].getKey("parameters"); esv.extraParameters = ini["api"].key("parameters", "");
string
returnValid(string def, string val) @safe
{
return val == "" ? def : val;
}
/* Get [passage] keys */ /* Get [passage] keys */
foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) { foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) {
try { try
esv.opts.b["include-" ~ key] = esv.opts.b["include-" ~ key] =
returnValid("true", iniData["passage"].getKey(key)).to!bool(); ini["passage"].keyAs!bool(key, "true");
} catch (ConvException e) { catch (INITypeException e)
throw new Exception(format! throw new Exception(configPath ~ ": " ~ e.msg);
"%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')"
(configPath, key)
);
} catch (IniException e) {} // just do nothing; use the default settings
} }
/* Get line-length ([passage]) */ /* Get line-length ([passage]) */
try { try {
esv.opts.i["line-length"] = esv.opts.i["line-length"] =
returnValid("0", iniData["passage"].getKey("line-length")).to!int(); ini["passage"].keyAs!int("line-length", "0");
} catch (INITypeException e) {
throw new Exception(configPath ~ ": " ~ e.msg);
} }
catch (ConvException e) {
throw new Exception(
format!"%s: illegal value '%s' -- must be an integer"(
configPath,
iniData["passage"].getKey("line-length"))
);
} catch (IniException e) {} // just do nothing; use the default setting
if (fFlag) esv.opts.b["include-footnotes"] = true; if (fFlag) esv.opts.b["include-footnotes"] = true;
if (hFlag) esv.opts.b["include-headings"] = true; if (hFlag) esv.opts.b["include-headings"] = true;
@ -279,5 +262,7 @@ extractOpt(in GetOptException e) @safe
private string private string
parseBook(in string book) @safe parseBook(in string book) @safe
{ {
return book.replaceAll(regex("[-_]"), " "); import std.string : tr;
return book.tr("-_", " ");
} }

View file

@ -1,23 +0,0 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View file

@ -1,30 +0,0 @@
This code is a modified version of dini, found here:
<https://github.com/robik/dini>
My changes can be found here:
<https://github.com/jtbx/dini>
Licensed under the Boost Software License - Version 1.0:
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View file

@ -1,3 +0,0 @@
module dini;
public import dini.parser;

View file

@ -1,681 +0,0 @@
/**
* INI parsing functionality.
*
* Examples:
* ---
* auto ini = Ini.ParseString("test = bar")
* writeln("Value of test is ", ini["test"]);
* ---
*/
module dini.parser;
import std.algorithm : min, max, countUntil;
import std.array : split, replaceInPlace, join;
import std.file : readText;
import std.stdio : File;
import std.string : strip, splitLines;
import std.traits : isSomeString;
import std.range : ElementType;
import std.conv : to;
import dini.reader : UniversalINIReader, INIException, INIToken;
/**
* Represents ini section
*
* Example:
* ---
* Ini ini = Ini.Parse("path/to/your.conf");
* string value = ini.getKey("a");
* ---
*/
struct IniSection
{
/// Section name
protected string _name = "root";
/// Parent
/// Null if none
protected IniSection* _parent;
/// Childs
protected IniSection[] _sections;
/// Keys
protected string[string] _keys;
/**
* Creates new IniSection instance
*
* Params:
* name = Section name
*/
public this(string name)
{
_name = name;
_parent = null;
}
/**
* Creates new IniSection instance
*
* Params:
* name = Section name
* parent = Section parent
*/
public this(string name, IniSection* parent)
{
_name = name;
_parent = parent;
}
/**
* Sets section key
*
* Params:
* name = Key name
* value = Value to set
*/
public void setKey(string name, string value)
{
_keys[name] = value;
}
/**
* Checks if specified key exists
*
* Params:
* name = Key name
*
* Returns:
* True if exists, false otherwise
*/
public bool hasKey(string name) @safe nothrow @nogc
{
return (name in _keys) !is null;
}
/**
* Gets key value
*
* Params:
* name = Key name
*
* Returns:
* Key value
*/
public string getKey(string name)
{
if(!hasKey(name)) {
return "";
}
return _keys[name];
}
/// ditto
alias getKey opCall;
/**
* Gets key value or defaultValue if key does not exist
*
* Params:
* name = Key name
* defaultValue = Default value
*
* Returns:
* Key value or defaultValue
*
*/
public string getKey(string name, string defaultValue) @safe nothrow
{
return hasKey(name) ? _keys[name] : defaultValue;
}
/**
* Removes key
*
* Params:
* name = Key name
*/
public void removeKey(string name)
{
_keys.remove(name);
}
/**
* Adds section
*
* Params:
* section = Section to add
*/
public void addSection(ref IniSection section)
{
_sections ~= section;
}
/**
* Checks if specified section exists
*
* Params:
* name = Section name
*
* Returns:
* True if exists, false otherwise
*/
public bool hasSection(string name)
{
foreach(ref section; _sections)
{
if(section.name() == name)
return true;
}
return false;
}
/**
* Returns reference to section
*
* Params:
* Section name
*
* Returns:
* Section with specified name
*/
public ref IniSection getSection(string name)
{
foreach(ref section; _sections)
{
if(section.name() == name)
return section;
}
throw new IniException("Section '"~name~"' does not exist");
}
/// ditto
public alias getSection opIndex;
/**
* Removes section
*
* Params:
* name = Section name
*/
public void removeSection(string name)
{
IniSection[] childs;
foreach(section; _sections)
{
if(section.name != name)
childs ~= section;
}
_sections = childs;
}
/**
* Section name
*
* Returns:
* Section name
*/
public string name() @property
{
return _name;
}
/**
* Array of keys
*
* Returns:
* Associative array of keys
*/
public string[string] keys() @property
{
return _keys;
}
/**
* Array of sections
*
* Returns:
* Array of sections
*/
public IniSection[] sections() @property
{
return _sections;
}
/**
* Root section
*/
public IniSection root() @property
{
IniSection s = this;
while(s.getParent() != null)
s = *(s.getParent());
return s;
}
/**
* Section parent
*
* Returns:
* Pointer to parent, or null if parent does not exists
*/
public IniSection* getParent()
{
return _parent;
}
/**
* Checks if current section has parent
*
* Returns:
* True if section has parent, false otherwise
*/
public bool hasParent()
{
return _parent != null;
}
/**
* Moves current section to another one
*
* Params:
* New parent
*/
public void setParent(ref IniSection parent)
{
_parent.removeSection(this.name);
_parent = &parent;
parent.addSection(this);
}
/**
* Parses filename
*
* Params:
* filename = Configuration filename
* doLookups = Should variable lookups be resolved after parsing?
*/
public void parse(string filename, bool doLookups = true)
{
parseString(readText(filename), doLookups);
}
public void parse(File* file, bool doLookups = true)
{
string data = file.byLine().join().to!string;
parseString(data, doLookups);
}
public void parseWith(Reader)(string filename, bool doLookups = true)
{
parseStringWith!Reader(readText(filename), doLookups);
}
public void parseWith(Reader)(File* file, bool doLookups = true)
{
string data = file.byLine().join().to!string;
parseStringWith!Reader(data, doLookups);
}
public void parseString(string data, bool doLookups = true)
{
parseStringWith!UniversalINIReader(data, doLookups);
}
public void parseStringWith(Reader)(string data, bool doLookups = true)
{
IniSection* section = &this;
auto reader = Reader(data);
alias KeyType = reader.KeyType;
while (reader.next()) switch (reader.type) with (INIToken) {
case SECTION:
section = &this;
string name = reader.value.get!string;
auto parts = name.split(":");
// [section : parent]
if (parts.length > 1)
name = parts[0].strip;
IniSection child = IniSection(name, section);
if (parts.length > 1) {
string parent = parts[1].strip;
child.inherit(section.getSectionEx(parent));
}
section.addSection(child);
section = &section.getSection(name);
break;
case KEY:
section.setKey(reader.value.get!KeyType.name, reader.value.get!KeyType.value);
break;
default:
break;
}
if(doLookups == true)
parseLookups();
}
/**
* Parses lookups
*/
public void parseLookups()
{
foreach (name, ref value; _keys)
{
ptrdiff_t start = -1;
char[] buf;
foreach (i, c; value) {
if (c == '%') {
if (start != -1) {
IniSection sect;
string newValue;
char[][] parts;
if (buf[0] == '.') {
parts = buf[1..$].split(".");
sect = this.root;
}
else {
parts = buf.split(".");
sect = this;
}
newValue = sect.getSectionEx(parts[0..$-1].join(".").idup).getKey(parts[$-1].idup);
value.replaceInPlace(start, i+1, newValue);
start = -1;
buf = [];
}
else {
start = i;
}
}
else if (start != -1) {
buf ~= c;
}
}
}
foreach(child; _sections) {
child.parseLookups();
}
}
/**
* Returns section by name in inheriting(names connected by dot)
*
* Params:
* name = Section name
*
* Returns:
* Section
*/
public IniSection getSectionEx(string name)
{
IniSection* root = &this;
auto parts = name.split(".");
foreach(part; parts) {
root = (&root.getSection(part));
}
return *root;
}
/**
* Inherits keys from section
*
* Params:
* Section to inherit
*/
public void inherit(IniSection sect)
{
this._keys = sect.keys().dup;
}
public void save(string filename)
{
import std.file;
if (exists(filename))
remove(filename);
File file = File(filename, "w");
foreach (section; _sections) {
file.writeln("[" ~ section.name() ~ "]");
string[string] propertiesInSection = section.keys();
foreach (key; propertiesInSection.keys) {
file.writeln(key ~ " = " ~ propertiesInSection[key]);
}
file.writeln();
}
file.close();
}
/**
* Parses Ini file
*
* Params:
* filename = Path to ini file
*
* Returns:
* IniSection root
*/
static Ini Parse(string filename, bool parseLookups = true)
{
Ini i;
i.parse(filename, parseLookups);
return i;
}
/**
* Parses Ini file with specified reader
*
* Params:
* filename = Path to ini file
*
* Returns:
* IniSection root
*/
static Ini ParseWith(Reader)(string filename, bool parseLookups = true)
{
Ini i;
i.parseWith!Reader(filename, parseLookups);
return i;
}
static Ini ParseString(string data, bool parseLookups = true)
{
Ini i;
i.parseString(data, parseLookups);
return i;
}
static Ini ParseStringWith(Reader)(string data, bool parseLookups = true)
{
Ini i;
i.parseStringWith!Reader(data, parseLookups);
return i;
}
}
// Compat
alias INIException IniException;
/// ditto
alias IniSection Ini;
///
Struct siphon(Struct)(Ini ini)
{
import std.traits;
Struct ans;
if(ini.hasSection(Struct.stringof))
foreach(ti, Name; FieldNameTuple!(Struct))
{
alias ToType = typeof(ans.tupleof[ti]);
if(ini[Struct.stringof].hasKey(Name))
ans.tupleof[ti] = to!ToType(ini[Struct.stringof].getKey(Name));
}
return ans;
}
unittest {
struct Section {
int var;
}
auto ini = Ini.ParseString("[Section]\nvar=3");
auto m = ini.siphon!Section;
assert(m.var == 3);
}
unittest {
auto data = q"(
key1 = value
# comment
test = bar ; comment
[section 1]
key1 = new key
num = 151
empty
[ various ]
"quoted key"= VALUE 123
quote_multiline = """
this is value
"""
escape_sequences = "yay\nboo"
escaped_newlines = abcd \
efg
)";
auto ini = Ini.ParseString(data);
assert(ini.getKey("key1") == "value");
assert(ini.getKey("test") == "bar ; comment");
assert(ini.hasSection("section 1"));
with (ini["section 1"]) {
assert(getKey("key1") == "new key");
assert(getKey("num") == "151");
assert(getKey("empty") == "");
}
assert(ini.hasSection("various"));
with (ini["various"]) {
assert(getKey("quoted key") == "VALUE 123");
assert(getKey("quote_multiline") == "\n this is value\n");
assert(getKey("escape_sequences") == "yay\nboo");
assert(getKey("escaped_newlines") == "abcd efg");
}
}
unittest {
auto data = q"EOF
key1 = value
# comment
test = bar ; comment
[section 1]
key1 = new key
num = 151
empty
EOF";
auto ini = Ini.ParseString(data);
assert(ini.getKey("key1") == "value");
assert(ini.getKey("test") == "bar ; comment");
assert(ini.hasSection("section 1"));
assert(ini["section 1"]("key1") == "new key");
assert(ini["section 1"]("num") == "151");
assert(ini["section 1"]("empty") == "");
}
unittest {
auto data = q"EOF
[def]
name1=value1
name2=value2
[foo : def]
name1=Name1 from foo. Lookup for def.name2: %name2%
EOF";
// Parse file
auto ini = Ini.ParseString(data, true);
assert(ini["foo"].getKey("name1")
== "Name1 from foo. Lookup for def.name2: value2");
}
unittest {
auto data = q"EOF
[section]
name=%value%
EOF";
// Create ini struct instance
Ini ini;
Ini iniSec = IniSection("section");
ini.addSection(iniSec);
// Set key value
ini["section"].setKey("value", "verify");
// Now, you can use value in ini file
ini.parseString(data);
assert(ini["section"].getKey("name") == "verify");
}
unittest {
import dini.reader;
alias MyReader = INIReader!(
UniversalINIFormat,
UniversalINIReader.CurrentFlags & ~INIFlags.ProcessEscapes,
UniversalINIReader.CurrentBoxer
);
auto ini = Ini.ParseStringWith!MyReader(`path=C:\Path`);
assert(ini("path") == `C:\Path`);
}

View file

@ -1,786 +0,0 @@
/**
* Implements INI reader.
*
* `INIReader` is fairly low-level, configurable reader for reading INI data,
* which you can use to build your own object-model.
*
* High level interface is available in `dini.parser`.
*
*
* Unless you need to change `INIReader` behaviour, you should use one of provided
* preconfigured readers:
*
* - `StrictINIReader`
*
* Lower compatibility, may be bit faster.
*
*
* - `UniversalINIReader`
*
* Higher compatibility, may be slighly slower.
*/
module dini.reader;
import std.algorithm : countUntil, canFind, map;
import std.array : array;
import std.functional : unaryFun;
import std.string : representation, assumeUTF, strip,
stripLeft, stripRight, split, join, format;
import std.range : ElementType, replace;
import std.uni : isWhite, isSpace;
import std.variant : Algebraic;
import dini.utils : isBoxer, BoxerType, parseEscapeSequences;
/**
* Represents type of current token used by INIReader.
*/
enum INIToken
{
BLANK, ///
SECTION, ///
KEY, ///
COMMENT ///
}
/**
* Represents a block definition.
*
* Block definitions are used to define new quote and comment sequences
* to be accepted by INIReader.
*
* BlockDefs can be either single line or multiline. To define new single
* line block `INIBlockDef.mutliline` must be set to `false` AND `closing`
* must be set to newline string(`"\n"`).
*/
struct INIBlockDef
{
/**
* Opening character sequence
*/
string opening;
/**
* Closing character sequence
*/
string closing;
/**
* Should newline characters be allowed?
*/
bool multiline;
}
/**
* INIReader behaviour flags.
*
* These flags can be used to modify INIReader behaviour.
*/
enum INIFlags : uint
{
/**
* Should escape sequences be translated?
*/
ProcessEscapes = 1 << 0,
/**
* Section names will be trimmed.
*/
TrimSections = 1 << 4,
/**
* Key names will be trimmed.
*/
TrimKeys = 1 << 5,
/**
* Values will be trimmed.
*/
TrimValues = 1 << 6,
/**
* Section names, keys and values will be trimmed.
*/
TrimAll = TrimSections | TrimKeys | TrimValues
}
/**
* Defines INI format.
*
* This struct defines INI comments and quotes sequences.
*
* `INIReader` adds no default quotes or comment definitions,
* and thus when defining custom format make sure to include default
* definitions to increase compatibility.
*/
struct INIFormatDescriptor
{
/**
* List of comment definitions to support.
*/
INIBlockDef[] comments;
/**
* List of quote definitions to support.
*/
INIBlockDef[] quotes;
}
/**
* Strict INI format.
*
* This format is used by `MinimalINIReader`.
*
* This format defines only `;` as comment character and `"` as only quote.
* For more universal format consider using `UniversalINIFormat`.
*/
const INIFormatDescriptor StrictINIFormat = INIFormatDescriptor(
[INIBlockDef(";", "\n", false)],
[INIBlockDef(`"`, `"`, false)]
);
/**
* Universal INI format.
*
* This format extends `StrictINIFormat` with hash-comments (`#`) and multiline
* triple-quotes (`"""`).
*/
const INIFormatDescriptor UniversalINIFormat = INIFormatDescriptor(
[INIBlockDef(";", "\n", false), INIBlockDef("#", "\n", false)],
[INIBlockDef(`"""`, `"""`, true), INIBlockDef(`"`, `"`, false)]
);
/**
* Thrown when an parsing error occurred.
*/
class INIException : Exception
{
this(string msg = null, Throwable next = null) { super(msg, next); }
this(string msg, string file, size_t line, Throwable next = null) {
super(msg, file, line, next);
}
}
/**
* Represents parsed INI key.
*
* Prefer using `YOUR_READER.KeyType` alias.
*/
struct INIReaderKey(ValueType)
{
/**
* Key name
*/
string name;
/**
* Key value (may be boxed)
*/
ValueType value;
}
/**
* Splits source into tokens.
*
* This struct requires token delimeters to be ASCII-characters,
* Unicode is not supported **only** for token delimeters.
*
* Unless you want to modify `INIReader` behaviour prefer using one of available
* preconfigured variants:
*
* - `StrictINIReader`
* - `UniversalINIReader`
*
*
* `INIReader` expects three template arguments:
*
* - `Format`
*
* Instance of `INIFormatDescriptor`, defines quote and comment sequences.
*
*
* - `Flags`
*
* `INIReaderFlags` (can be OR-ed)
*
*
* - `Boxer`
*
* Name of a function that takes `(string value, INIReader reader)` and returns a value.
* By default all readers just proxy values, doing nothing, but this can be used to e.g.
* store token values as JSONValue or other Algebraic-like type.
*
* `INIReader.BoxType` is always return type of boxer function. So if you passed a boxer that
* returns `SomeAlgebraic` then `typeof(reader.key.value)` is `SomeAlgebraic`.
*
*
* Params:
* Format - `INIFormatDescriptor` to use.
* Flags - Reader behaviour flags.
* Boxer - Function name that can optionally box values.
*
*
* Examples:
* ---
* auto reader = UniversalINIReader("key=value\n");
*
* while (reader.next) {
* writeln(reader.value);
* }
* ---
*/
struct INIReader(INIFormatDescriptor Format, ubyte Flags = 0x00, alias Boxer)
if (isBoxer!Boxer)
{
/**
* Reader's format descriptor.
*/
alias CurrentFormat = Format;
/**
* Reader's flags.
*/
alias CurrentFlags = Flags;
/**
* Reader's boxer.
*/
alias CurrentBoxer = Boxer;
/**
* Reader's Box type (boxer return type).
*/
alias BoxType = BoxerType!Boxer;
/**
* Alias for INIReaderKey!(BoxType).
*/
alias KeyType = INIReaderKey!BoxType;
/**
* Type of `value` property.
*/
alias TokenValue = Algebraic!(string, KeyType);
/**
* INI source bytes.
*/
immutable(ubyte)[] source;
/**
* INI source offset in bytes.
*/
size_t sourceOffset;
/**
* Type of current token.
*/
INIToken type;
/**
* Indicates whenever source has been exhausted.
*/
bool empty;
/**
* Used only with Key tokens.
*
* Indicates whenever current value has been quoted.
* This information can be used by Boxers to skip boxing of quoted values.
*/
bool isQuoted;
/**
* Current token's value.
*/
TokenValue value;
/**
* Creates new instance of `INIReader` from `source`.
*
* If passed source does not end with newline it is added (and thus allocates).
* To prevent allocation make sure `source` ends with new line.
*
* Params:
* source - INI source.
*/
this(string source)
{
// Make source end with newline
if (source[$-1] != '\n')
this.source = (source ~ "\n").representation;
else
this.source = source.representation;
}
/**
* Returns key token.
*
* Use this only if you know current token is KEY.
*/
KeyType key() @property {
return value.get!KeyType;
}
/**
* Returns section name.
*
* Use this only if you know current token is SECTION.
*/
string sectionName() @property {
return value.get!string;
}
/**
* Reads next token.
*
* Returns:
* True if more tokens are available, false otherwise.
*/
bool next()
{
isQuoted = false;
skipWhitespaces();
if (current.length == 0) {
empty = true;
return false;
}
int pairIndex = -1;
while(source.length - sourceOffset > 0)
{
if (findPair!`comments`(pairIndex)) {
readComment(pairIndex);
break;
}
else if (current[0] == '[') {
readSection();
break;
}
else if (isWhite(current[0])) {
skipWhitespaces();
}
else {
readEntry();
break;
}
}
return true;
}
bool findPair(string fieldName)(out int pairIndex)
{
if (source.length - sourceOffset > 0 && sourceOffset > 0 && source[sourceOffset - 1] == '\\') return false;
alias MemberType = typeof(__traits(getMember, Format, fieldName));
foreach (size_t i, ElementType!MemberType pairs; __traits(getMember, Format, fieldName)) {
string opening = pairs.tupleof[0];
if (source.length - sourceOffset < opening.length)
continue;
if (current[0..opening.length] == opening) {
pairIndex = cast(int)i;
return true;
}
}
return false;
}
void readSection()
{
type = INIToken.SECTION;
auto index = current.countUntil(']');
if (index == -1)
throw new INIException("Section not closed");
value = current[1 .. index].assumeUTF;
static if (Flags & INIFlags.TrimSections)
value = value.get!string.strip;
sourceOffset += index + 1;
}
void readComment(int pairIndex)
{
type = INIToken.COMMENT;
INIBlockDef commentDef = Format.comments[pairIndex];
sourceOffset += commentDef.opening.length;
auto index = current.countUntil(commentDef.closing);
if (index == -1)
throw new INIException("Comment not closed");
value = current[0.. index].assumeUTF;
if (commentDef.multiline == false && value.get!string.canFind('\n'))
throw new INIException("Comment not closed (multiline)");
sourceOffset += index + commentDef.closing.length;
}
void readEntry()
{
type = INIToken.KEY;
KeyType key;
readKey(key);
if (current[0] == '=') {
sourceOffset += 1;
key.value = readValue();
}
value = key;
}
void readKey(out KeyType key)
{
if (tryReadQuote(key.name)) {
isQuoted = true;
return;
}
auto newLineOffset = current.countUntil('\n');
if (newLineOffset > 0) { // read untill newline/some assign sequence
auto offset = current[0..newLineOffset].countUntil('=');
if (offset == -1)
key.name = current[0 .. newLineOffset].assumeUTF;
else
key.name = current[0 .. offset].assumeUTF;
sourceOffset += key.name.length;
key.name = key.name.stripRight;
static if (Flags & INIFlags.TrimKeys)
key.name = key.name.stripLeft;
}
}
BoxType readValue()
{
auto firstNonSpaceIndex = current.countUntil!(a => !isSpace(a));
if (firstNonSpaceIndex > 0)
sourceOffset += firstNonSpaceIndex;
string result = "";
auto indexBeforeQuotes = sourceOffset;
isQuoted = tryReadQuote(result);
auto newlineOffset = current.countUntil('\n');
string remains = current[0..newlineOffset].assumeUTF;
if (isQuoted && newlineOffset > 0) {
sourceOffset = indexBeforeQuotes;
isQuoted = false;
}
if (!isQuoted) {
bool escaped = false;
int[] newlineOffsets = [];
auto localOffset = 0;
for (; source.length - sourceOffset > 0; ++localOffset) {
if (source[sourceOffset + localOffset] == '\\') {
escaped = !escaped;
continue;
}
else if(escaped && source[sourceOffset + localOffset] == '\r')
continue;
else if(escaped && source[sourceOffset + localOffset] == '\n')
newlineOffsets ~= localOffset;
else if (!escaped && source[sourceOffset + localOffset] == '\n')
break;
escaped = false;
}
result = current[0..localOffset].assumeUTF.split("\n").map!((line) {
line = line.stripRight;
if (line[$-1] == '\\') return line[0..$-1].stripLeft;
return line.stripLeft;
}).array.join();
sourceOffset += localOffset;
}
static if (Flags & INIFlags.TrimValues)
if (!isQuoted)
result = result.strip;
static if (Flags & INIFlags.ProcessEscapes)
result = parseEscapeSequences(result);
return Boxer(result);
}
bool tryReadQuote(out string result)
{
int pairIndex;
if (findPair!`quotes`(pairIndex)) {
auto quote = Format.quotes[pairIndex];
sourceOffset += quote.opening.length;
auto closeIndex = current.countUntil(quote.closing);
if (closeIndex == -1)
throw new INIException("Unterminated string literal");
result = current[0..closeIndex].assumeUTF;
sourceOffset += result.length + quote.closing.length;
if (result.canFind('\n') && quote.multiline == false)
throw new INIException("Unterminated string literal which spans multiple lines (invalid quotes used?)");
return true;
}
return false;
}
void skipWhitespaces()
{
while (current.length && isWhite(current[0]))
sourceOffset += 1;
}
private immutable(ubyte)[] current() @property {
return source[sourceOffset..$];
}
}
/**
* Universal `INIReader` variant.
*
* Use this variant if you want to have more compatible parser.
*
* Specifics:
* - Uses `UniversalINIFormat`.
* - Trims section names, keys and values.
* - Processes escapes in values (e.g. `\n`).
*/
alias UniversalINIReader = INIReader!(UniversalINIFormat, INIFlags.TrimAll | INIFlags.ProcessEscapes, (string a) => a);
/**
* Strict `INIReader` variant.
*
* Use this variant if you want to have more strict (and bit faster) parser.
*
* Specifics:
* - Uses `StrictINIFormat`
* - Only Keys are trimmed.
* - No escape sequences are resolved.
*/
alias StrictINIReader = INIReader!(StrictINIFormat, INIFlags.TrimKeys, (string a) => a);
unittest {
auto source = `
; comment
multiline = """
this is
"""
numeric=-100000
numeric2=09843
[section (name)]
@=bar
`;
auto reader = UniversalINIReader(source);
alias Key = reader.KeyType;
assert(reader.next());
assert(reader.type == INIToken.COMMENT);
assert(reader.sectionName == " comment");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.key.name == "multiline");
assert(reader.key.value == "\n this is\n");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "numeric");
assert(reader.value.get!Key.value == "-100000");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "numeric2");
assert(reader.value.get!Key.value == "09843");
assert(reader.next());
assert(reader.type == INIToken.SECTION);
assert(reader.value.get!string == "section (name)");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "@");
assert(reader.value.get!Key.value == `bar`);
assert(!reader.next());
}
unittest {
auto source = `
####### TEST ########
numeric value=15
ThisIsMultilineValue=thisis\
verylong # comment
"Floating=Value"=1.51
[] # comment works
JustAKey
`;
auto reader = UniversalINIReader(source);
alias Key = reader.KeyType;
assert(reader.next());
assert(reader.type == INIToken.COMMENT);
assert(reader.value.get!string == "###### TEST ########");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "numeric value");
assert(reader.value.get!Key.value == `15`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "ThisIsMultilineValue");
assert(reader.value.get!Key.value == `thisisverylong # comment`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "Floating=Value");
assert(reader.value.get!Key.value == `1.51`);
assert(reader.next());
assert(reader.type == INIToken.SECTION);
assert(reader.value.get!string == "");
assert(reader.next());
assert(reader.type == INIToken.COMMENT);
assert(reader.value.get!string == " comment works");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "JustAKey");
assert(reader.value.get!Key.value == null);
assert(!reader.next());
}
unittest {
string source = `
[ Debug ]
sNumString=10Test
QuotedNum="10"
QuotedFloat="10.1"
Num=10
Float=10.1
`;
auto reader = UniversalINIReader(source);
alias Key = reader.KeyType;
assert(reader.next());
assert(reader.type == INIToken.SECTION);
assert(reader.value.get!string == "Debug");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "sNumString");
assert(reader.value.get!Key.value == `10Test`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "QuotedNum");
assert(reader.value.get!Key.value == `10`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "QuotedFloat");
assert(reader.value.get!Key.value == `10.1`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "Num");
assert(reader.value.get!Key.value == "10");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "Float");
assert(reader.value.get!Key.value == "10.1");
assert(!reader.next());
}
unittest {
string source = `
[ Debug ]
sNumString=10Test
QuotedNum="10"
QuotedFloat="10.1"
Num=10
Float=10.1
`;
auto reader = StrictINIReader(source);
alias Key = reader.KeyType;
assert(reader.next());
assert(reader.type == INIToken.SECTION);
assert(reader.value.get!string == " Debug ");
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "sNumString");
assert(reader.value.get!Key.value == `10Test`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "QuotedNum");
assert(reader.value.get!Key.value == `10`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "QuotedFloat");
assert(reader.value.get!Key.value == `10.1`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "Num");
assert(reader.value.get!Key.value == `10`);
assert(reader.next());
assert(reader.type == INIToken.KEY);
assert(reader.value.get!Key.name == "Float");
assert(reader.value.get!Key.value == `10.1`);
assert(!reader.next());
}

View file

@ -1,66 +0,0 @@
module dini.utils;
import std.format : format, formatElement, FormatSpec, FormatException, formattedRead;
import std.traits : arity, isCallable, Parameters, ReturnType;
enum bool isBoxer(alias boxer) = isCallable!boxer
&& arity!boxer == 1
&& is(Parameters!boxer[0] == string);
alias BoxerType(alias boxer) = ReturnType!boxer;
static char[char] escapeSequences;
static this() {
escapeSequences = [
'n': '\n', 'r': '\r', 't': '\t', 'b': '\b', '\\': '\\',
'#': '#', ';': ';', '=': '=', ':': ':', '"': '"', '\'': '\''
];
}
string parseEscapeSequences(string input)
{
bool inEscape;
const(char)[] result = [];
result.reserve(input.length);
for(auto i = 0; i < input.length; i++) {
char c = input[i];
if (inEscape) {
if (c in escapeSequences)
result ~= escapeSequences[c];
else if (c == 'x') {
ubyte n;
if (i + 3 > input.length)
throw new FormatException("Invalid escape sequence (\\x)");
string s = input[i+1..i+3];
if (formattedRead(s, "%x", &n) < 1)
throw new FormatException("Invalid escape sequence (\\x)");
result ~= cast(char)n;
i += 2;
}
else {
throw new FormatException("Invalid escape sequence (\\%s..)".format(c));
}
}
else if (!inEscape && c == '\\') {
inEscape = true;
continue;
}
else result ~= c;
inEscape = false;
}
return cast(string)result;
}
unittest {
assert(parseEscapeSequences("abc wef ' n r ;a") == "abc wef ' n r ;a");
assert(parseEscapeSequences(`\\n \\\\\\\\\\r`) == `\n \\\\\r`);
assert(parseEscapeSequences(`hello\nworld`) == "hello\nworld");
assert(parseEscapeSequences(`multi\r\nline \#notacomment`) == "multi\r\nline #notacomment");
assert(parseEscapeSequences(`welp \x5A\x41\x7a`) == "welp ZAz");
}

454
initial.d Normal file
View file

@ -0,0 +1,454 @@
/*
* Copyright (c) 2024 Jeremy Baxter. All rights reserved.
*
* Boost Software License - Version 1.0 - August 17th, 2003
*
* Permission is hereby granted, free of charge, to any person or organization
* obtaining a copy of the software and accompanying documentation covered by
* this license (the "Software") to use, reproduce, display, distribute,
* execute, and transmit the Software, and to prepare derivative works of the
* Software, and to permit third-parties to whom the Software is furnished to
* do so, all subject to the following:
*
* The copyright notices in the Software and this entire statement, including
* the above license grant, this restriction and the following disclaimer,
* must be included in all copies of the Software, in whole or in part, and
* all derivative works of the Software, unless such copies or derivative
* works are solely in the form of machine-executable object code generated by
* a source language processor.
*
* 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
* SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
* FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
/++
+ INI is a simple, concise configuration or data interchange
+ format. It consists of keys and values listed inside a section.
+ An INI document might look something like this:
+ ---
+ [user]
+ name = Kevin
+ age = 26
+ emailAddress = kevin@example.net
+
+ [config]
+ useBundledMalloc = false
+ forceDefaultInit = true
+ ---
+
+ $(B initial) is a library for parsing INI documents into an `INIUnit`,
+ a data structure representing an INI document. The function `readINI`
+ contains the parser; it parses the INI given to it and deserialises
+ it into the given `INIUnit`. The `readINIFile` function can be used
+ to read the INI from a file rather than a string.
+
+ $(B initial) is fully @safe, and can be used in @safe code.
+
+ License: [Boost License 1.0](https://www.boost.org/LICENSE_1_0.txt).
+ Authors: Jeremy Baxter
+/
module initial;
import std.conv : to, ConvException;
import std.exception : basicExceptionCtors, enforce;
import std.format : format;
import std.traits : isSomeChar;
private alias parserEnforce = enforce!INIParseException;
@safe:
/++
+ Structure that represents an INI file.
+
+ Contains `INISection`s in an associative array that can be
+ directly accessed if needs be, or the index operator can be
+ used to get an `INISection` and automatically initialise it
+ if needed.
+
+ The `serialise` method can be used to serialise the entire
+ structure into an INI document that can be then read again
+ by the parser and modified by humans.
+
+ Example:
+ ---
+ INIUnit ini;
+
+ /* write INI data */
+ ini["section"]["key"] = "value";
+ ini["section"]["num"] = "4.8";
+
+ /* read INI data */
+ readINI(ini, `
+ [section]
+ num = 5.3
+ `);
+
+ /* read INI from file */
+ readINIFile(ini, "config.ini");
+
+ /* write INI to file */
+ import std.file : write;
+ write("config.ini", ini.serialise());
+ ---
+/
struct INIUnit
{
INISection[string] sections; /++ Hashmap of INISections in this unit. +/
string defaultSection = "main"; /++ Name of the default section. +/
nothrow:
/++
+ Returns the value of *k* in the default section
+ or *defaultValue* if *k* is not present.
+/
string
key(string k, string defaultValue = null)
{
return this[defaultSection].key(k, defaultValue);
}
/++
+ Serialises this `INIUnit` into an INI document.
+
+ If *defaultSectionHeading* is true, includes the
+ section heading of the default section.
+ Example:
+ ---
+ INIUnit ini;
+
+ ini["sections"] = "ages suburbs";
+ ini["ages"]["John"] = "37";
+ ini["ages"]["Mary"] = "29";
+ ini["suburbs"]["John"] = "Gordonton";
+ ini["suburbs"]["Mary"] = "Ashmore";
+
+ writeln(ini.serialise());
+ /*
+ * sections = ages suburbs
+ * [ages]
+ * John = 37
+ * Mary = 29
+ * [suburbs]
+ * John = Gordonton
+ * Mary = Ashmore
+ */
+ ---
+/
char[]
serialise(bool defaultSectionHeading = false)
{
char[] buf;
buf = this[defaultSection].serialise(defaultSectionHeading);
foreach (string sect; sections.byKey()) {
if (sect == defaultSection)
continue;
buf ~= this[sect].serialise();
}
return buf;
}
/++
+ Returns the `INISection` associated with the name *sect*.
+ Initialises it if it doesn't exist.
+/
ref INISection
opIndex(string sect)
{
if (!(sect in sections))
sections[sect] = INISection(sect);
return sections[sect];
}
/++
+ Sets the value of the key *k* in the default section to *v*.
+ Example:
+ ---
+ ini["key"] = "value";
+ ---
+/
void
opIndexAssign(string v, string k)
{
sections[defaultSection][k] = v;
}
/++
+ Sets the value of the `INISection` named *sect*.
+/
void
opIndexAssign(INISection v, string sect)
{
sections[sect] = v;
}
}
/++
+ Represents an INI section.
+
+ Holds keys in the form of an associative array.
+ Index the `keys` property to get this data raw, or to avoid
+ range errors, use the `key` or `keyAs` functions.
+
+ `alias this` is applied to the `keys` associative array,
+ meaning you can index the structure to get or set keys' values.
+/
struct INISection
{
string[string] keys; /++ Hashmap of keys belonging to this section. +/
string name; /++ Name of this section. +/
/++
+ Construct a new `INISection` with the given name.
+ Called by `readINI` and `INIUnit.opIndex`.
+/
this(string name) nothrow
{
this.name = name;
keys = new string[string];
}
/+
+ Returns the value of `k` in this section or
+ `defaultValue` if the key is not present.
+/
string
key(string k, string defaultValue = null) nothrow
{
return k in keys ? keys[k] : defaultValue;
}
/++
+ Using `std.conv`, converts the value of *k* to *T*.
+ On conversion failure throws an `INITypeException`
+ with a message containing location information.
+/
T
keyAs(T)(string k, string defaultValue = null)
{
try
return key(k, defaultValue).to!T();
catch (ConvException)
throw new INITypeException(
format!"unable to convert [%s].%s to %s"(name, k, T.stringof));
}
/++
+ Serialise this section into an INI document.
+
+ If `sectionHeading` is true, includes the section heading at the top.
+/
char[]
serialise(bool sectionHeading = true) nothrow
{
char[] buf;
if (sectionHeading)
buf ~= "[" ~ name.to!(char[]) ~ "]\n";
foreach (string key; keys.byKey()) {
buf ~= key ~ " = " ~ keys[key] ~ "\n";
}
return buf;
}
alias keys this;
}
/++
+ Exception thrown when parsing failure occurs.
+/
class INIParseException : Exception
{
mixin basicExceptionCtors;
}
/++
+ Exception thrown when conversion failure occurs.
+/
class INITypeException : Exception
{
mixin basicExceptionCtors;
}
private string
locate(size_t l, size_t c)
{
return format!"%d:%d"(l, c);
}
/++
+ Parse the given INI data into an `INIUnit`.
+
+ To parse a file's contents, use readINIFile instead.
+
+ Whitespace is first stripped from the beginning and end of
+ the line before parsing it.
+
+ The following rules apply when parsing:
+ $(UL
+ $(LI if a hash character `#` is found at the beginning of a line,
+ the rest of the line is ignored)
+ $(LI if a left square bracket character `[` is found at
+ the beginning of a line, the line is considered a
+ section heading)
+ $(LI inside a section heading, any character can be used
+ inside the section heading except the right square bracket
+ character)
+ $(LI once the right square bracket character `]` is found,
+ the section heading is considered finished. Any trailing
+ characters are garbage and will trigger a parse error.)
+ $(LI if a character except the left square bracket is found
+ at the beginning of a line, the line is considered an
+ assignment in the current section)
+ $(LI once an equals character `=` is found after the first
+ part of an assignment, the rest of the line is considered
+ the "value" while the first part is the "key")
+ $(LI if whitespace is found in the first part of an assignment,
+ it is ignored and the next equals sign is looked for
+ immediately)
+ $(LI if whitespace is found after an equals character `=`
+ in an assignment, it is ignored until the first
+ non-whitespace character is found)
+ $(LI once a non-whitespace character is found after an equals
+ character `=` in an assignment, it is counted part of the
+ "value")
+/
void
readINI(ref INIUnit ini, string data)
{
import std.string : splitLines, strip;
string section; /* name of current section */
string text; /* current key, value, section, whatever */
string key; /* name of key in section */
string value; /* value */
bool inAssign; /* in key assignment? */
bool inHeading; /* in section heading? */
bool needEqls; /* require equals sign immediately? */
bool pastEqls; /* past equals sign in assignment? */
bool trailing; /* trailing garbage after heading? */
section = ini.defaultSection;
nextline:
foreach (size_t ln, string line; data.splitLines()) {
line = line.strip();
if (line.length == 0)
continue;
inAssign = inHeading = needEqls = pastEqls = trailing = false;
text = key = value = "";
foreach (size_t cn, char ch; line) {
switch (ch) {
/* comment? */
case '#':
if (cn == 0)
continue nextline;
text ~= ch;
break;
/* beginning of a section heading? */
case '[':
if (cn == 0) /* beginning of line? */
inHeading = true;
else
text ~= ch;
break;
/* end of a section heading? */
case ']':
parserEnforce(inHeading || inAssign,
locate(ln, cn) ~ ": unexpected character ']'");
if (inHeading) {
section = text;
text = "";
inHeading = false;
trailing = true;
} else if (inAssign) {
text ~= ch;
}
break;
/* middle of an assignment? */
case '=':
if (inAssign) {
if (pastEqls) {
/* count it as part of the value */
text ~= ch;
} else {
key = text;
text = "";
pastEqls = true;
needEqls = false;
}
} else {
goto default;
}
break;
/* whitespace */
case ' ':
case '\t':
case '\v':
case '\r':
case '\n':
case '\f':
if (inAssign) {
if (!pastEqls)
needEqls = true;
}
break;
default:
if (cn == 0) /* beginning of line? */
inAssign = true;
parserEnforce((inAssign || inHeading) && !trailing,
locate(ln, cn) ~ ": unexpected character '" ~ ch ~ "'");
if (inAssign)
parserEnforce(!needEqls,
locate(ln, cn) ~ ": expected '=', not '" ~ ch ~ "'");
text ~= ch;
break;
}
}
/* once the line has finished being looped over... */
if (inAssign) {
parserEnforce(key.length != 0,
locate(ln, line.length - 1) ~ ": key cannot be empty");
value = text;
text = "";
if (!(section in ini.sections))
ini[section] = INISection(section);
ini[section][key] = value;
}
}
}
/++
+ Parse INI from a file located at `fileName`.
+/
void
readINIFile(ref INIUnit ini, string fileName)
{
import std.file : readText;
readINI(ini, readText(fileName));
}