Add source code

Added:
 - esv.d: reusable D interface to the ESV web API
 - main.d: the main program
 - Makefile
 - README.md
 - modified version of dini (dependency)
 - man pages
 - licenses
This commit is contained in:
Jeremy Baxter 2023-03-23 15:59:09 +13:00
commit a0341e72d7
14 changed files with 2882 additions and 0 deletions

23
import/dini/LICENSE Normal file
View file

@ -0,0 +1,23 @@
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.

30
import/dini/README Normal file
View file

@ -0,0 +1,30 @@
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.

3
import/dini/package.d Normal file
View file

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

681
import/dini/parser.d Normal file
View file

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

786
import/dini/reader.d Normal file
View file

@ -0,0 +1,786 @@
/**
* 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());
}

66
import/dini/utils.d Normal file
View file

@ -0,0 +1,66 @@
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");
}