diff --git a/COPYING b/COPYING index f93c1b2..de62dba 100644 --- a/COPYING +++ b/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 diff --git a/configure b/configure index 481c82a..deb818c 100755 --- a/configure +++ b/configure @@ -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 diff --git a/esv.d b/esv.d index 0c2cf67..a03c586 100644 --- a/esv.d +++ b/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("-_", " "); } diff --git a/import/dini/LICENSE b/import/dini/LICENSE deleted file mode 100644 index 36b7cd9..0000000 --- a/import/dini/LICENSE +++ /dev/null @@ -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. diff --git a/import/dini/README b/import/dini/README deleted file mode 100644 index 6d53e26..0000000 --- a/import/dini/README +++ /dev/null @@ -1,30 +0,0 @@ -This code is a modified version of dini, found here: - -My changes can be found here: - - -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. diff --git a/import/dini/package.d b/import/dini/package.d deleted file mode 100644 index 309d84e..0000000 --- a/import/dini/package.d +++ /dev/null @@ -1,3 +0,0 @@ -module dini; - -public import dini.parser; \ No newline at end of file diff --git a/import/dini/parser.d b/import/dini/parser.d deleted file mode 100644 index 66ba3fb..0000000 --- a/import/dini/parser.d +++ /dev/null @@ -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`); -} \ No newline at end of file diff --git a/import/dini/reader.d b/import/dini/reader.d deleted file mode 100644 index 6a03199..0000000 --- a/import/dini/reader.d +++ /dev/null @@ -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()); -} \ No newline at end of file diff --git a/import/dini/utils.d b/import/dini/utils.d deleted file mode 100644 index 9dc9cf2..0000000 --- a/import/dini/utils.d +++ /dev/null @@ -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"); -} diff --git a/initial.d b/initial.d new file mode 100644 index 0000000..7f4a585 --- /dev/null +++ b/initial.d @@ -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)); +}