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:
parent
de9e18f06e
commit
2e93c891fd
10 changed files with 509 additions and 1642 deletions
34
COPYING
34
COPYING
|
@ -1,5 +1,35 @@
|
|||
This software is licensed under the GNU General Public License, version 2.
|
||||
The license is as follows:
|
||||
The file initial.d is licensed under the Boost Software License,
|
||||
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
|
||||
Version 2, June 1991
|
||||
|
|
23
configure
vendored
23
configure
vendored
|
@ -7,9 +7,8 @@ set -e
|
|||
import=import
|
||||
|
||||
mkf=Makefile
|
||||
cflags='-I'"$import"
|
||||
objs='esv.o esvapi.o'
|
||||
srcs='esv.d esvapi.d'
|
||||
objs='esv.o esvapi.o initial.o'
|
||||
srcs='esv.d esvapi.d initial.d'
|
||||
makefile='
|
||||
IMPORT = '"$import"'
|
||||
PREFIX = /usr/local
|
||||
|
@ -17,9 +16,7 @@ MANPREFIX = ${PREFIX}/man
|
|||
|
||||
DC = ${_DC}
|
||||
CFLAGS = ${_CFLAGS}
|
||||
OBJS = ${_OBJS} ini.a
|
||||
INIOBJS = ${IMPORT}/dini/package.o ${IMPORT}/dini/parser.o \\
|
||||
${IMPORT}/dini/reader.o ${IMPORT}/dini/utils.o
|
||||
OBJS = ${_OBJS}
|
||||
|
||||
all: esv
|
||||
|
||||
|
@ -31,13 +28,6 @@ esv: ${OBJS}
|
|||
.d.o:
|
||||
${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:
|
||||
rm -f esv ${OBJS} ${INIOBJS}
|
||||
|
||||
|
@ -152,9 +142,6 @@ done
|
|||
|
||||
# creating the makefile
|
||||
|
||||
u_cflags="$cflags"
|
||||
unset cflags
|
||||
|
||||
gen_DC
|
||||
gen_CFLAGS
|
||||
gen_LDFLAGS
|
||||
|
@ -167,7 +154,7 @@ _CFLAGS = %s
|
|||
_LDFLAGS = %s
|
||||
' \
|
||||
"$dc" \
|
||||
"$cflags $u_cflags" \
|
||||
"$cflags" \
|
||||
"$ldflags" \
|
||||
>>"$mkf"
|
||||
## generate obj list
|
||||
|
@ -185,7 +172,7 @@ printf "$makefile" >>"$mkf"
|
|||
printf '\n# begin generated dependencies\n' >>"$mkf"
|
||||
i=1
|
||||
for obj in $objs; do
|
||||
"$dc" $u_cflags -O0 -o- -makedeps \
|
||||
"$dc" -O0 -o- -makedeps \
|
||||
"$(printf "$srcs" | awk '{print $'"$i"'}')" >>"$mkf"
|
||||
i="$(($i + 1))"
|
||||
done
|
||||
|
|
51
esv.d
51
esv.d
|
@ -32,9 +32,10 @@ import std.regex : regex, matchFirst, replaceAll, replaceFirst;
|
|||
import std.stdio : writef, writeln, writefln, stderr;
|
||||
import std.string : splitLines;
|
||||
|
||||
import initial;
|
||||
|
||||
import config;
|
||||
import esvapi;
|
||||
import dini;
|
||||
|
||||
enum VERSION = "0.2.0";
|
||||
|
||||
|
@ -92,7 +93,7 @@ run(string[] args)
|
|||
{
|
||||
string apiKey;
|
||||
string configPath;
|
||||
Ini iniData;
|
||||
INIUnit ini;
|
||||
ESVApi esv;
|
||||
|
||||
/* Parse command-line options */
|
||||
|
@ -177,17 +178,14 @@ key = %s
|
|||
"(DEFAULT_APIKEY));
|
||||
}
|
||||
}
|
||||
iniData = Ini.Parse(configPath);
|
||||
readINIFile(ini, configPath);
|
||||
} catch (FileException e) {
|
||||
/* filesystem syscall errors */
|
||||
throw new Exception(e.msg);
|
||||
}
|
||||
try {
|
||||
apiKey = iniData["api"].getKey("key");
|
||||
} catch (IniException e) {
|
||||
apiKey = "";
|
||||
}
|
||||
enforce(apiKey != "",
|
||||
|
||||
apiKey = ini["api"].key("key");
|
||||
enforce(apiKey != null,
|
||||
"API key not present in configuration file; cannot proceed");
|
||||
|
||||
esv = new ESVApi(apiKey);
|
||||
|
@ -223,38 +221,23 @@ key = %s
|
|||
return true;
|
||||
}
|
||||
|
||||
esv.extraParameters = iniData["api"].getKey("parameters");
|
||||
|
||||
string
|
||||
returnValid(string def, string val) @safe
|
||||
{
|
||||
return val == "" ? def : val;
|
||||
}
|
||||
esv.extraParameters = ini["api"].key("parameters", "");
|
||||
|
||||
/* Get [passage] keys */
|
||||
foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) {
|
||||
try {
|
||||
try
|
||||
esv.opts.b["include-" ~ key] =
|
||||
returnValid("true", iniData["passage"].getKey(key)).to!bool();
|
||||
} catch (ConvException e) {
|
||||
throw new Exception(format!
|
||||
"%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
|
||||
ini["passage"].keyAs!bool(key, "true");
|
||||
catch (INITypeException e)
|
||||
throw new Exception(configPath ~ ": " ~ e.msg);
|
||||
}
|
||||
/* Get line-length ([passage]) */
|
||||
try {
|
||||
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 (hFlag) esv.opts.b["include-headings"] = true;
|
||||
|
@ -279,5 +262,7 @@ extractOpt(in GetOptException e) @safe
|
|||
private string
|
||||
parseBook(in string book) @safe
|
||||
{
|
||||
return book.replaceAll(regex("[-_]"), " ");
|
||||
import std.string : tr;
|
||||
|
||||
return book.tr("-_", " ");
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -1,3 +0,0 @@
|
|||
module dini;
|
||||
|
||||
public import dini.parser;
|
|
@ -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 = §ion.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`);
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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
454
initial.d
Normal 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));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue