mirror of https://github.com/adamdruppe/arsd.git
2962 lines
66 KiB
D
2962 lines
66 KiB
D
/+
|
||
== arsd.ini ==
|
||
Copyright Elias Batek (0xEAB) 2025.
|
||
Distributed under the Boost Software License, Version 1.0.
|
||
+/
|
||
/++
|
||
INI configuration file support
|
||
|
||
This module provides a configurable INI parser with support for multiple
|
||
“dialects” of the format.
|
||
|
||
### Getting started
|
||
|
||
$(LIST
|
||
* [parseIniDocument] – Parse INI string into a DOM-like [IniDocument].
|
||
* [parseIniAA] – Parse INI string into an associative array.
|
||
* [stringifyIni] – Serialize an [IniDocument] or an associative array
|
||
to a string of data in INI format.
|
||
)
|
||
|
||
---
|
||
import arsd.ini;
|
||
|
||
IniDocument!string parseIniFile(string filePath) {
|
||
import std.file : readText;
|
||
return parseIniDocument(readText(filePath));
|
||
}
|
||
---
|
||
|
||
---
|
||
import arsd.ini;
|
||
|
||
void writeIniFile(string filePath, IniDocument!string document) {
|
||
import std.file : write;
|
||
return write(filePath, stringifyIni(document));
|
||
}
|
||
---
|
||
|
||
|
||
### On destructiveness and GC usage
|
||
|
||
Depending on the dialect and string type,
|
||
[IniParser] can operate in one of these three modes:
|
||
|
||
$(LIST
|
||
* Non-destructive with no heap alloc (incl. `@nogc`)
|
||
* Non-destructive (uses the GC)
|
||
* Destructive with no heap alloc (incl. `@nogc`)
|
||
)
|
||
|
||
a) If a given dialect requests no mutation of the input data
|
||
(i.e. no escape sequences, no concaternation of substrings etc.)
|
||
and is therefore possible to implement with slicing operations only,
|
||
the parser will be non-destructive and not do any heap allocations.
|
||
Such a parser is verifiably `@nogc`, too.
|
||
|
||
b) In cases where a dialect requires data-mutating operations,
|
||
there are two ways for a parser to implement them:
|
||
|
||
b.0) Either perform those mutations on the input data itself
|
||
and alter the contents of that buffer.
|
||
Because of the destructive nature of this operation,
|
||
it can be performed only once safely.
|
||
(Such an implementation could optionally fix up the modified data
|
||
to become valid and parsable again.
|
||
Though doing so would come with a performance overhead.)
|
||
|
||
b.1) Or allocate a new buffer for the result of the operation.
|
||
This also has the advantage that it works with `immutable` and `const`
|
||
input data.
|
||
For convenience reasons the GC is used to perform such allocations.
|
||
|
||
Use [IniParser.isDestructive] to check for the operating mode.
|
||
|
||
The construct a non-destructive parser despite a mutable input data,
|
||
specify `const(char)[]` as the value of the `string` template parameter.
|
||
|
||
---
|
||
char[] mutableInput = [ /* … */ ];
|
||
auto parser = makeIniParser!(dialect, const(char)[])(mutableInput);
|
||
assert(parser.isDestructive == false);
|
||
---
|
||
+/
|
||
module arsd.ini;
|
||
|
||
///
|
||
@safe unittest {
|
||
// INI example data (e.g. from an `autorun.inf` file)
|
||
static immutable string rawIniData =
|
||
"[autorun]\n"
|
||
~ "open=setup.exe\n"
|
||
~ "icon=setup.exe,0\n";
|
||
|
||
// Parse the document into an associative array:
|
||
string[string][string] data = parseIniAA(rawIniData);
|
||
|
||
string open = data["autorun"]["open"];
|
||
string icon = data["autorun"]["icon"];
|
||
|
||
assert(open == "setup.exe");
|
||
assert(icon == "setup.exe,0");
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
// INI example data (e.g. from an `autorun.inf` file):
|
||
static immutable string rawIniData =
|
||
"[autorun]\n"
|
||
~ "open=setup.exe\n"
|
||
~ "icon=setup.exe,0\n";
|
||
|
||
// Parse the document.
|
||
IniDocument!string document = parseIniDocument(rawIniData);
|
||
|
||
// Let’s search for the value of an entry `icon` in the `autorun` section.
|
||
static string searchAutorunIcon(IniDocument!string document) {
|
||
// Iterate over all sections.
|
||
foreach (IniSection!string section; document.sections) {
|
||
|
||
// Search for the `[autorun]` section.
|
||
if (section.name == "autorun") {
|
||
|
||
// Iterate over all items in the section.
|
||
foreach (IniKeyValuePair!string item; section.items) {
|
||
|
||
// Search for the `icon` entry.
|
||
if (item.key == "icon") {
|
||
// Found!
|
||
return item.value;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Not found!
|
||
return null;
|
||
}
|
||
|
||
// Call our search function.
|
||
string icon = searchAutorunIcon(document);
|
||
|
||
// Finally, verify the result.
|
||
assert(icon == "setup.exe,0");
|
||
}
|
||
|
||
/++
|
||
Determines whether a type `T` is a string type compatible with this library.
|
||
+/
|
||
enum isCompatibleString(T) = (is(T == immutable(char)[]) || is(T == const(char)[]) || is(T == char[]));
|
||
|
||
//dfmt off
|
||
/++
|
||
Feature set to be understood by the parser.
|
||
|
||
---
|
||
enum myDialect = (IniDialect.defaults | IniDialect.inlineComments);
|
||
---
|
||
+/
|
||
enum IniDialect : ulong {
|
||
/++
|
||
Minimum feature set.
|
||
|
||
No comments, no extras, no nothing.
|
||
Only sections, keys and values.
|
||
Everything fits into these categories from a certain point of view.
|
||
+/
|
||
lite = 0,
|
||
|
||
/++
|
||
Parse line comments (starting with `;`).
|
||
|
||
```ini
|
||
; This is a line comment.
|
||
;This one too.
|
||
|
||
key = value ;But this isn't one.
|
||
```
|
||
+/
|
||
lineComments = 0b_0000_0000_0000_0001,
|
||
|
||
/++
|
||
Parse inline comments (starting with `;`).
|
||
|
||
```ini
|
||
key1 = value2 ; Inline comment.
|
||
key2 = value2 ;Inline comment.
|
||
key3 = value3; Inline comment.
|
||
;Not a true inline comment (but technically equivalent).
|
||
```
|
||
+/
|
||
inlineComments = 0b_0000_0000_0000_0011,
|
||
|
||
/++
|
||
Parse line comments starting with `#`.
|
||
|
||
```ini
|
||
# This is a comment.
|
||
#Too.
|
||
key = value # Not a line comment.
|
||
```
|
||
+/
|
||
hashLineComments = 0b_0000_0000_0000_0100,
|
||
|
||
/++
|
||
Parse inline comments starting with `#`.
|
||
|
||
```ini
|
||
key1 = value2 # Inline comment.
|
||
key2 = value2 #Inline comment.
|
||
key3 = value3# Inline comment.
|
||
#Not a true inline comment (but technically equivalent).
|
||
```
|
||
+/
|
||
hashInlineComments = 0b_0000_0000_0000_1100,
|
||
|
||
/++
|
||
Parse quoted strings.
|
||
|
||
```ini
|
||
key1 = non-quoted value
|
||
key2 = "quoted value"
|
||
|
||
"quoted key" = value
|
||
non-quoted key = value
|
||
|
||
"another key" = "another value"
|
||
|
||
multi line = "line 1
|
||
line 2"
|
||
```
|
||
+/
|
||
quotedStrings = 0b_0000_0000_0001_0000,
|
||
|
||
/++
|
||
Parse quoted strings using single-quotes.
|
||
|
||
```ini
|
||
key1 = non-quoted value
|
||
key2 = 'quoted value'
|
||
|
||
'quoted key' = value
|
||
non-quoted key = value
|
||
|
||
'another key' = 'another value'
|
||
|
||
multi line = 'line 1
|
||
line 2'
|
||
```
|
||
+/
|
||
singleQuoteQuotedStrings = 0b_0000_0000_0010_0000,
|
||
|
||
/++
|
||
Parse key/value pairs separated with a colon (`:`).
|
||
|
||
```ini
|
||
key: value
|
||
key= value
|
||
```
|
||
+/
|
||
colonKeys = 0b_0000_0000_0100_0000,
|
||
|
||
/++
|
||
Concats substrings and emits them as a single token.
|
||
|
||
$(LIST
|
||
* For a mutable `char[]` input,
|
||
this will rewrite the data in the input array.
|
||
* For a non-mutable `immutable(char)[]` (=`string`) or `const(char)[]` input,
|
||
this will allocate a new array with the GC.
|
||
)
|
||
|
||
```ini
|
||
key = "Value1" "Value2"
|
||
; → Value1Value2
|
||
```
|
||
+/
|
||
concatSubstrings = 0b_0000_0001_0000_0000,
|
||
|
||
/++
|
||
Evaluates escape sequences in the input string.
|
||
|
||
$(LIST
|
||
* For a mutable `char[]` input,
|
||
this will rewrite the data in the input array.
|
||
* For a non-mutable `immutable(char)[]` (=`string`) or `const(char)[]` input,
|
||
this will allocate a new array with the GC.
|
||
)
|
||
|
||
$(SMALL_TABLE
|
||
Special escape sequences
|
||
`\\` | Backslash
|
||
`\0` | Null character
|
||
`\n` | Line feed
|
||
`\r` | Carriage return
|
||
`\t` | Tabulator
|
||
)
|
||
|
||
```ini
|
||
key1 = Line 1\nLine 2
|
||
; → Line 1
|
||
; Line 2
|
||
|
||
key2 = One \\ and one \;
|
||
; → One \ and one ;
|
||
```
|
||
+/
|
||
escapeSequences = 0b_0000_0010_0000_0000,
|
||
|
||
/++
|
||
Folds lines on escaped linebreaks.
|
||
|
||
$(LIST
|
||
* For a mutable `char[]` input,
|
||
this will rewrite the data in the input array.
|
||
* For a non-mutable `immutable(char)[]` (=`string`) or `const(char)[]` input,
|
||
this will allocate a new array with the GC.
|
||
)
|
||
|
||
```ini
|
||
key1 = word1\
|
||
word2
|
||
; → word1word2
|
||
|
||
key2 = foo \
|
||
bar
|
||
; → foo bar
|
||
```
|
||
+/
|
||
lineFolding = 0b_0000_0100_0000_0000,
|
||
|
||
/++
|
||
Imitates the behavior of the INI parser implementation found in PHP.
|
||
|
||
$(WARNING
|
||
This preset may be adjusted without further notice in the future
|
||
in cases where it increases alignment with PHP’s implementation.
|
||
)
|
||
+/
|
||
presetPhp = (
|
||
lineComments
|
||
| inlineComments
|
||
| hashLineComments
|
||
| hashInlineComments
|
||
| quotedStrings
|
||
| singleQuoteQuotedStrings
|
||
| concatSubstrings
|
||
),
|
||
|
||
///
|
||
presetDefaults = (
|
||
lineComments
|
||
| quotedStrings
|
||
| singleQuoteQuotedStrings
|
||
),
|
||
|
||
///
|
||
defaults = presetDefaults,
|
||
}
|
||
//dfmt on
|
||
|
||
private bool hasFeature(ulong dialect, ulong feature) @safe pure nothrow @nogc {
|
||
return ((dialect & feature) > 0);
|
||
}
|
||
|
||
private T[] spliceImpl(T)(T[] array, size_t at, size_t count) @safe pure nothrow @nogc
|
||
in (at < array.length)
|
||
in (count <= array.length)
|
||
in (at + count <= array.length) {
|
||
const upper = array.length - count;
|
||
|
||
for (size_t idx = at; idx < upper; ++idx) {
|
||
array[idx] = array[idx + count];
|
||
}
|
||
|
||
return array[0 .. ($ - count)];
|
||
}
|
||
|
||
private T[] splice(T)(auto ref scope T[] array, size_t at, size_t count) @safe pure nothrow @nogc {
|
||
static if (__traits(isRef, array)) {
|
||
array = spliceImpl(array, at, count); // @suppress(dscanner.suspicious.auto_ref_assignment)
|
||
return array;
|
||
} else {
|
||
return spliceImpl(array, at, count);
|
||
}
|
||
}
|
||
|
||
@safe unittest {
|
||
assert("foobar".dup.splice(0, 0) == "foobar");
|
||
assert("foobar".dup.splice(0, 6) == "");
|
||
assert("foobar".dup.splice(0, 1) == "oobar");
|
||
assert("foobar".dup.splice(1, 5) == "f");
|
||
assert("foobar".dup.splice(1, 4) == "fr");
|
||
assert("foobar".dup.splice(4, 1) == "foobr");
|
||
assert("foobar".dup.splice(4, 2) == "foob");
|
||
}
|
||
|
||
@safe unittest {
|
||
char[] array = ['a', 's', 'd', 'f'];
|
||
array.splice(1, 2);
|
||
assert(array == "af");
|
||
}
|
||
|
||
///
|
||
char resolveIniEscapeSequence(char c) @safe pure nothrow @nogc {
|
||
switch (c) {
|
||
case 'n':
|
||
return '\x0A';
|
||
case 'r':
|
||
return '\x0D';
|
||
case 't':
|
||
return '\x09';
|
||
case '\\':
|
||
return '\\';
|
||
case '0':
|
||
return '\x00';
|
||
|
||
default:
|
||
return c;
|
||
}
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
assert(resolveIniEscapeSequence('n') == '\n');
|
||
assert(resolveIniEscapeSequence('r') == '\r');
|
||
assert(resolveIniEscapeSequence('t') == '\t');
|
||
assert(resolveIniEscapeSequence('\\') == '\\');
|
||
assert(resolveIniEscapeSequence('0') == '\0');
|
||
|
||
// Unsupported characters are preserved.
|
||
assert(resolveIniEscapeSequence('a') == 'a');
|
||
assert(resolveIniEscapeSequence('Z') == 'Z');
|
||
assert(resolveIniEscapeSequence('1') == '1');
|
||
// Unsupported special characters are preserved.
|
||
assert(resolveIniEscapeSequence('@') == '@');
|
||
// Line breaks are preserved.
|
||
assert(resolveIniEscapeSequence('\n') == '\n');
|
||
assert(resolveIniEscapeSequence('\r') == '\r');
|
||
// UTF-8 is preserved.
|
||
assert(resolveIniEscapeSequence("ü"[0]) == "ü"[0]);
|
||
}
|
||
|
||
private struct StringRange {
|
||
private {
|
||
const(char)[] _data;
|
||
}
|
||
|
||
@safe pure nothrow @nogc:
|
||
|
||
public this(const(char)[] data) {
|
||
_data = data;
|
||
}
|
||
|
||
bool empty() const {
|
||
return (_data.length == 0);
|
||
}
|
||
|
||
char front() const {
|
||
return _data[0];
|
||
}
|
||
|
||
void popFront() {
|
||
_data = _data[1 .. $];
|
||
}
|
||
}
|
||
|
||
private struct StringSliceRange {
|
||
private {
|
||
const(char)[] _data;
|
||
}
|
||
|
||
@safe pure nothrow @nogc:
|
||
|
||
public this(const(char)[] data) {
|
||
_data = data;
|
||
}
|
||
|
||
bool empty() const {
|
||
return (_data.length == 0);
|
||
}
|
||
|
||
const(char)[] front() const {
|
||
return _data[0 .. 1];
|
||
}
|
||
|
||
void popFront() {
|
||
_data = _data[1 .. $];
|
||
}
|
||
}
|
||
|
||
/++
|
||
Resolves escape sequences and performs line folding.
|
||
|
||
Feature set depends on the [Dialect].
|
||
+/
|
||
string resolveIniEscapeSequences(Dialect dialect)(const(char)[] input) @safe pure nothrow {
|
||
size_t irrelevant = 0;
|
||
|
||
auto source = StringRange(input);
|
||
determineIrrelevantLoop: while (!source.empty) {
|
||
if (source.front != '\\') {
|
||
source.popFront();
|
||
continue;
|
||
}
|
||
|
||
source.popFront();
|
||
if (source.empty) {
|
||
break;
|
||
}
|
||
|
||
static if (dialect.hasFeature(Dialect.lineFolding)) {
|
||
switch (source.front) {
|
||
case '\n':
|
||
source.popFront();
|
||
irrelevant += 2;
|
||
continue determineIrrelevantLoop;
|
||
|
||
case '\r':
|
||
source.popFront();
|
||
irrelevant += 2;
|
||
if (source.empty) {
|
||
break determineIrrelevantLoop;
|
||
}
|
||
// CRLF?
|
||
if (source.front == '\n') {
|
||
source.popFront();
|
||
++irrelevant;
|
||
}
|
||
continue determineIrrelevantLoop;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
static if (dialect.hasFeature(Dialect.escapeSequences)) {
|
||
source.popFront();
|
||
++irrelevant;
|
||
}
|
||
}
|
||
|
||
const escapedSize = input.length - irrelevant;
|
||
auto result = new char[](escapedSize);
|
||
|
||
size_t cursor = 0;
|
||
source = StringRange(input);
|
||
buildResultLoop: while (!source.empty) {
|
||
if (source.front != '\\') {
|
||
result[cursor++] = source.front;
|
||
source.popFront();
|
||
continue;
|
||
}
|
||
|
||
source.popFront();
|
||
if (source.empty) {
|
||
result[cursor] = '\\';
|
||
break;
|
||
}
|
||
|
||
static if (dialect.hasFeature(Dialect.lineFolding)) {
|
||
switch (source.front) {
|
||
case '\n':
|
||
source.popFront();
|
||
continue buildResultLoop;
|
||
|
||
case '\r':
|
||
source.popFront();
|
||
if (source.empty) {
|
||
break buildResultLoop;
|
||
}
|
||
// CRLF?
|
||
if (source.front == '\n') {
|
||
source.popFront();
|
||
}
|
||
continue buildResultLoop;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
static if (dialect.hasFeature(Dialect.escapeSequences)) {
|
||
result[cursor++] = resolveIniEscapeSequence(source.front);
|
||
source.popFront();
|
||
continue;
|
||
} else {
|
||
result[cursor++] = '\\';
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
enum none = Dialect.lite;
|
||
enum escp = Dialect.escapeSequences;
|
||
enum fold = Dialect.lineFolding;
|
||
enum both = Dialect.escapeSequences | Dialect.lineFolding;
|
||
|
||
assert(resolveIniEscapeSequences!none("foo\\nbar") == "foo\\nbar");
|
||
assert(resolveIniEscapeSequences!escp("foo\\nbar") == "foo\nbar");
|
||
assert(resolveIniEscapeSequences!fold("foo\\nbar") == "foo\\nbar");
|
||
assert(resolveIniEscapeSequences!both("foo\\nbar") == "foo\nbar");
|
||
|
||
assert(resolveIniEscapeSequences!none("foo\\\nbar") == "foo\\\nbar");
|
||
assert(resolveIniEscapeSequences!escp("foo\\\nbar") == "foo\nbar");
|
||
assert(resolveIniEscapeSequences!fold("foo\\\nbar") == "foobar");
|
||
assert(resolveIniEscapeSequences!both("foo\\\nbar") == "foobar");
|
||
|
||
assert(resolveIniEscapeSequences!none("foo\\\n\\nbar") == "foo\\\n\\nbar");
|
||
assert(resolveIniEscapeSequences!escp("foo\\\n\\nbar") == "foo\n\nbar");
|
||
assert(resolveIniEscapeSequences!fold("foo\\\n\\nbar") == "foo\\nbar");
|
||
assert(resolveIniEscapeSequences!both("foo\\\n\\nbar") == "foo\nbar");
|
||
|
||
assert(resolveIniEscapeSequences!none("foobar\\") == "foobar\\");
|
||
assert(resolveIniEscapeSequences!escp("foobar\\") == "foobar\\");
|
||
assert(resolveIniEscapeSequences!fold("foobar\\") == "foobar\\");
|
||
assert(resolveIniEscapeSequences!both("foobar\\") == "foobar\\");
|
||
|
||
assert(resolveIniEscapeSequences!none("foo\\\r\nbar") == "foo\\\r\nbar");
|
||
assert(resolveIniEscapeSequences!escp("foo\\\r\nbar") == "foo\r\nbar");
|
||
assert(resolveIniEscapeSequences!fold("foo\\\r\nbar") == "foobar");
|
||
assert(resolveIniEscapeSequences!both("foo\\\r\nbar") == "foobar");
|
||
|
||
assert(resolveIniEscapeSequences!none(`\nfoobar\n`) == "\\nfoobar\\n");
|
||
assert(resolveIniEscapeSequences!escp(`\nfoobar\n`) == "\nfoobar\n");
|
||
assert(resolveIniEscapeSequences!fold(`\nfoobar\n`) == "\\nfoobar\\n");
|
||
assert(resolveIniEscapeSequences!both(`\nfoobar\n`) == "\nfoobar\n");
|
||
|
||
assert(resolveIniEscapeSequences!none("\\\nfoo \\\rba\\\r\nr") == "\\\nfoo \\\rba\\\r\nr");
|
||
assert(resolveIniEscapeSequences!escp("\\\nfoo \\\rba\\\r\nr") == "\nfoo \rba\r\nr");
|
||
assert(resolveIniEscapeSequences!fold("\\\nfoo \\\rba\\\r\nr") == "foo bar");
|
||
assert(resolveIniEscapeSequences!both("\\\nfoo \\\rba\\\r\nr") == "foo bar");
|
||
}
|
||
|
||
/++
|
||
Type of a token (as output by the parser)
|
||
+/
|
||
public enum IniTokenType {
|
||
/// indicates an error
|
||
invalid = 0,
|
||
|
||
/// insignificant whitespace
|
||
whitespace,
|
||
/// section header opening bracket
|
||
bracketOpen,
|
||
/// section header closing bracket
|
||
bracketClose,
|
||
/// key/value separator, e.g. '='
|
||
keyValueSeparator,
|
||
/// line break, i.e. LF, CRLF or CR
|
||
lineBreak,
|
||
|
||
/// text comment
|
||
comment,
|
||
|
||
/// item key data
|
||
key,
|
||
/// item value data
|
||
value,
|
||
/// section name data
|
||
sectionHeader,
|
||
}
|
||
|
||
/++
|
||
Token of INI data (as output by the parser)
|
||
+/
|
||
struct IniToken(string) if (isCompatibleString!string) {
|
||
///
|
||
IniTokenType type;
|
||
|
||
/++
|
||
Content
|
||
+/
|
||
string data;
|
||
}
|
||
|
||
private alias TokenType = IniTokenType;
|
||
private alias Dialect = IniDialect;
|
||
|
||
private enum LocationState {
|
||
newLine,
|
||
key,
|
||
preValue,
|
||
inValue,
|
||
sectionHeader,
|
||
}
|
||
|
||
private enum OperatingMode {
|
||
nonDestructive,
|
||
destructive,
|
||
}
|
||
|
||
private enum OperatingMode operatingMode(string) = (is(string == char[]))
|
||
? OperatingMode.destructive : OperatingMode.nonDestructive;
|
||
|
||
/++
|
||
Low-level INI parser
|
||
|
||
See_also:
|
||
$(LIST
|
||
* [IniFilteredParser]
|
||
* [parseIniDocument]
|
||
* [parseIniAA]
|
||
)
|
||
+/
|
||
struct IniParser(
|
||
IniDialect dialect = IniDialect.defaults,
|
||
string = immutable(char)[],
|
||
) if (isCompatibleString!string) {
|
||
|
||
public {
|
||
///
|
||
alias Token = IniToken!string;
|
||
|
||
// dfmt off
|
||
///
|
||
enum isDestructive = (
|
||
(operatingMode!string == OperatingMode.destructive)
|
||
&& (
|
||
dialect.hasFeature(Dialect.concatSubstrings)
|
||
|| dialect.hasFeature(Dialect.escapeSequences)
|
||
|| dialect.hasFeature(Dialect.lineFolding)
|
||
)
|
||
);
|
||
// dfmt on
|
||
}
|
||
|
||
private {
|
||
string _source;
|
||
Token _front;
|
||
bool _empty = true;
|
||
|
||
LocationState _locationState = LocationState.newLine;
|
||
|
||
static if (dialect.hasFeature(Dialect.concatSubstrings)) {
|
||
bool _bypassConcatSubstrings = false;
|
||
}
|
||
}
|
||
|
||
@safe pure nothrow:
|
||
|
||
///
|
||
public this(string rawIni) {
|
||
_source = rawIni;
|
||
_empty = false;
|
||
|
||
this.popFront();
|
||
}
|
||
|
||
// Range API
|
||
public {
|
||
|
||
///
|
||
bool empty() const @nogc {
|
||
return _empty;
|
||
}
|
||
|
||
///
|
||
inout(Token) front() inout @nogc {
|
||
return _front;
|
||
}
|
||
|
||
private void popFrontImpl() {
|
||
if (_source.length == 0) {
|
||
_empty = true;
|
||
return;
|
||
}
|
||
|
||
_front = this.fetchFront();
|
||
}
|
||
|
||
/*
|
||
This is a workaround.
|
||
The compiler doesn’t feel like inferring `@nogc` properly otherwise.
|
||
|
||
→ cannot call non-@nogc function
|
||
`arsd.ini.makeIniParser!(IniDialect.concatSubstrings, char[]).makeIniParser`
|
||
→ which calls
|
||
`arsd.ini.IniParser!(IniDialect.concatSubstrings, char[]).IniParser.this`
|
||
→ which calls
|
||
`arsd.ini.IniParser!(IniDialect.concatSubstrings, char[]).IniParser.popFront`
|
||
*/
|
||
static if (isDestructive) {
|
||
///
|
||
void popFront() @nogc {
|
||
popFrontImpl();
|
||
}
|
||
} else {
|
||
///
|
||
void popFront() {
|
||
popFrontImpl();
|
||
}
|
||
}
|
||
|
||
// Destructive parsers make very poor Forward Ranges.
|
||
static if (!isDestructive) {
|
||
///
|
||
inout(typeof(this)) save() inout @nogc {
|
||
return this;
|
||
}
|
||
}
|
||
}
|
||
|
||
// extras
|
||
public {
|
||
|
||
/++
|
||
Skips tokens that are irrelevant for further processing
|
||
|
||
Returns:
|
||
true = if there are no further tokens,
|
||
i.e. whether the range is empty now
|
||
+/
|
||
bool skipIrrelevant(bool skipComments = true) {
|
||
static bool isIrrelevant(const TokenType type, const bool skipComments) {
|
||
pragma(inline, true);
|
||
|
||
final switch (type) with (TokenType) {
|
||
case invalid:
|
||
return false;
|
||
|
||
case whitespace:
|
||
case bracketOpen:
|
||
case bracketClose:
|
||
case keyValueSeparator:
|
||
case lineBreak:
|
||
return true;
|
||
|
||
case comment:
|
||
return skipComments;
|
||
|
||
case sectionHeader:
|
||
case key:
|
||
case value:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
while (!this.empty) {
|
||
const irrelevant = isIrrelevant(_front.type, skipComments);
|
||
|
||
if (!irrelevant) {
|
||
return false;
|
||
}
|
||
|
||
this.popFront();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private {
|
||
|
||
bool isOnFinalChar() const @nogc {
|
||
pragma(inline, true);
|
||
return (_source.length == 1);
|
||
}
|
||
|
||
bool isAtStartOfLineOrEquivalent() @nogc {
|
||
return (_locationState == LocationState.newLine);
|
||
}
|
||
|
||
Token makeToken(TokenType type, size_t length) @nogc {
|
||
auto token = Token(type, _source[0 .. length]);
|
||
_source = _source[length .. $];
|
||
return token;
|
||
}
|
||
|
||
Token makeToken(TokenType type, size_t length, size_t skip) @nogc {
|
||
_source = _source[skip .. $];
|
||
return this.makeToken(type, length);
|
||
}
|
||
|
||
Token lexWhitespace() @nogc {
|
||
foreach (immutable idxM1, const c; _source[1 .. $]) {
|
||
switch (c) {
|
||
case '\x09':
|
||
case '\x0B':
|
||
case '\x0C':
|
||
case ' ':
|
||
break;
|
||
|
||
default:
|
||
return this.makeToken(TokenType.whitespace, (idxM1 + 1));
|
||
}
|
||
}
|
||
|
||
// all whitespace
|
||
return this.makeToken(TokenType.whitespace, _source.length);
|
||
}
|
||
|
||
Token lexComment() @nogc {
|
||
foreach (immutable idxM1, const c; _source[1 .. $]) {
|
||
switch (c) {
|
||
default:
|
||
break;
|
||
|
||
case '\x0A':
|
||
case '\x0D':
|
||
return this.makeToken(TokenType.comment, idxM1, 1);
|
||
}
|
||
}
|
||
|
||
return this.makeToken(TokenType.comment, (-1 + _source.length), 1);
|
||
}
|
||
|
||
Token lexSubstringImpl(TokenType tokenType)() {
|
||
|
||
enum Result {
|
||
end,
|
||
endChomp,
|
||
regular,
|
||
whitespace,
|
||
sequence,
|
||
}
|
||
|
||
enum QuotedString : ubyte {
|
||
none = 0,
|
||
regular,
|
||
single,
|
||
}
|
||
|
||
// dfmt off
|
||
enum bool hasAnyQuotedString = (
|
||
dialect.hasFeature(Dialect.quotedStrings)
|
||
|| dialect.hasFeature(Dialect.singleQuoteQuotedStrings)
|
||
);
|
||
|
||
enum bool hasAnyEscaping = (
|
||
dialect.hasFeature(Dialect.lineFolding)
|
||
|| dialect.hasFeature(Dialect.escapeSequences)
|
||
);
|
||
// dfmt on
|
||
|
||
static if (hasAnyQuotedString) {
|
||
auto inQuotedString = QuotedString.none;
|
||
}
|
||
static if (dialect.hasFeature(Dialect.quotedStrings)) {
|
||
if (_source[0] == '"') {
|
||
inQuotedString = QuotedString.regular;
|
||
|
||
// chomp quote initiator
|
||
_source = _source[1 .. $];
|
||
}
|
||
}
|
||
static if (dialect.hasFeature(Dialect.singleQuoteQuotedStrings)) {
|
||
if (_source[0] == '\'') {
|
||
inQuotedString = QuotedString.single;
|
||
|
||
// chomp quote initiator
|
||
_source = _source[1 .. $];
|
||
}
|
||
}
|
||
static if (!hasAnyQuotedString) {
|
||
enum inQuotedString = QuotedString.none;
|
||
}
|
||
|
||
Result nextChar(const char c) @safe pure nothrow @nogc {
|
||
pragma(inline, true);
|
||
|
||
switch (c) {
|
||
default:
|
||
return Result.regular;
|
||
|
||
case '\x09':
|
||
case '\x0B':
|
||
case '\x0C':
|
||
case ' ':
|
||
return (inQuotedString != QuotedString.none)
|
||
? Result.regular : Result.whitespace;
|
||
|
||
case '\x0A':
|
||
case '\x0D':
|
||
return (inQuotedString != QuotedString.none)
|
||
? Result.regular : Result.endChomp;
|
||
|
||
case '"':
|
||
static if (dialect.hasFeature(Dialect.quotedStrings)) {
|
||
// dfmt off
|
||
return (inQuotedString == QuotedString.regular)
|
||
? Result.end
|
||
: (inQuotedString == QuotedString.single)
|
||
? Result.regular
|
||
: Result.endChomp;
|
||
// dfmt on
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
|
||
case '\'':
|
||
static if (dialect.hasFeature(Dialect.singleQuoteQuotedStrings)) {
|
||
return (inQuotedString != QuotedString.regular)
|
||
? Result.end : Result.regular;
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
|
||
case '#':
|
||
if (dialect.hasFeature(Dialect.hashInlineComments)) {
|
||
return (inQuotedString != QuotedString.none)
|
||
? Result.regular : Result.endChomp;
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
|
||
case ';':
|
||
if (dialect.hasFeature(Dialect.inlineComments)) {
|
||
return (inQuotedString != QuotedString.none)
|
||
? Result.regular : Result.endChomp;
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
|
||
case ':':
|
||
static if (dialect.hasFeature(Dialect.colonKeys)) {
|
||
goto case '=';
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
|
||
case '=':
|
||
static if (tokenType == TokenType.key) {
|
||
return (inQuotedString != QuotedString.none)
|
||
? Result.regular : Result.end;
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
|
||
case '\\':
|
||
static if (hasAnyEscaping) {
|
||
return Result.sequence;
|
||
} else {
|
||
goto default;
|
||
}
|
||
|
||
case ']':
|
||
static if (tokenType == TokenType.sectionHeader) {
|
||
return (inQuotedString != QuotedString.none)
|
||
? Result.regular : Result.end;
|
||
} else {
|
||
return Result.regular;
|
||
}
|
||
}
|
||
|
||
assert(false, "Bug: This should have been unreachable.");
|
||
}
|
||
|
||
ptrdiff_t idxLastText = -1;
|
||
ptrdiff_t idxCutoff = -1;
|
||
|
||
for (size_t idx = 0; idx < _source.length; ++idx) {
|
||
const c = _source[idx];
|
||
const status = nextChar(c);
|
||
|
||
if (status == Result.end) {
|
||
if (idxLastText < 0) {
|
||
idxLastText = (idx - 1);
|
||
}
|
||
break;
|
||
} else if (status == Result.endChomp) {
|
||
idxCutoff = idx;
|
||
break;
|
||
} else if (status == Result.whitespace) {
|
||
continue;
|
||
} else if (status == Result.sequence) {
|
||
static if (hasAnyEscaping) {
|
||
const idxNext = idx + 1;
|
||
if (idxNext < _source.length) {
|
||
static if (dialect.hasFeature(Dialect.lineFolding)) {
|
||
size_t determineFoldingCount() {
|
||
switch (_source[idxNext]) {
|
||
case '\n':
|
||
return 2;
|
||
|
||
case '\r':
|
||
const idxAfterNext = idxNext + 1;
|
||
// CRLF?
|
||
if (idxAfterNext < _source.length) {
|
||
if (_source[idxAfterNext] == '\n') {
|
||
return 3;
|
||
}
|
||
}
|
||
return 2;
|
||
|
||
default:
|
||
return 0;
|
||
}
|
||
|
||
assert(false, "Bug: This should have been unreachable.");
|
||
}
|
||
|
||
const foldingCount = determineFoldingCount();
|
||
if (foldingCount > 0) {
|
||
static if (operatingMode!string == OperatingMode.nonDestructive) {
|
||
idx += (foldingCount - 1);
|
||
idxCutoff = idx;
|
||
}
|
||
static if (operatingMode!string == OperatingMode.destructive) {
|
||
_source.splice(idx, foldingCount);
|
||
idx -= (foldingCount - 1);
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
static if (dialect.hasFeature(Dialect.escapeSequences)) {
|
||
static if (operatingMode!string == OperatingMode.nonDestructive) {
|
||
++idx;
|
||
}
|
||
static if (operatingMode!string == OperatingMode.destructive) {
|
||
_source[idx] = resolveIniEscapeSequence(_source[idxNext]);
|
||
_source.splice(idxNext, 1);
|
||
}
|
||
|
||
idxLastText = idx;
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
idxLastText = idx;
|
||
}
|
||
|
||
const idxEOT = (idxLastText + 1);
|
||
auto token = Token(tokenType, _source[0 .. idxEOT]);
|
||
|
||
static if (hasAnyEscaping) {
|
||
static if (operatingMode!string == OperatingMode.nonDestructive) {
|
||
token.data = resolveIniEscapeSequences!dialect(token.data);
|
||
}
|
||
}
|
||
|
||
// "double-quote quoted": cut off any whitespace afterwards
|
||
if (inQuotedString == QuotedString.regular) {
|
||
const idxEOQ = (idxEOT + 1);
|
||
if (_source.length > idxEOQ) {
|
||
foreach (immutable idx, c; _source[idxEOQ .. $]) {
|
||
switch (c) {
|
||
case '\x09':
|
||
case '\x0B':
|
||
case '\x0C':
|
||
case ' ':
|
||
continue;
|
||
|
||
default:
|
||
// EOT because Q is cut off later
|
||
idxCutoff = idxEOT + idx;
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const idxNextToken = (idxCutoff >= idxLastText) ? idxCutoff : idxEOT;
|
||
_source = _source[idxNextToken .. $];
|
||
|
||
if (inQuotedString != QuotedString.none) {
|
||
if (_source.length > 0) {
|
||
// chomp quote terminator
|
||
_source = _source[1 .. $];
|
||
}
|
||
}
|
||
|
||
return token;
|
||
}
|
||
|
||
Token lexSubstring() {
|
||
final switch (_locationState) {
|
||
case LocationState.newLine:
|
||
case LocationState.key:
|
||
return this.lexSubstringImpl!(TokenType.key);
|
||
|
||
case LocationState.preValue:
|
||
_locationState = LocationState.inValue;
|
||
goto case LocationState.inValue;
|
||
|
||
case LocationState.inValue:
|
||
return this.lexSubstringImpl!(TokenType.value);
|
||
|
||
case LocationState.sectionHeader:
|
||
return this.lexSubstringImpl!(TokenType.sectionHeader);
|
||
}
|
||
}
|
||
|
||
static if (dialect.hasFeature(Dialect.concatSubstrings)) {
|
||
Token lexSubstringsImpl(TokenType tokenType)() {
|
||
static if (operatingMode!string == OperatingMode.destructive) {
|
||
auto originalSource = _source;
|
||
}
|
||
|
||
Token token = this.lexSubstringImpl!tokenType();
|
||
|
||
auto next = this; // copy
|
||
next._bypassConcatSubstrings = true;
|
||
next.popFront();
|
||
|
||
static if (operatingMode!string == OperatingMode.destructive) {
|
||
import arsd.core : isSliceOf;
|
||
|
||
if (!token.data.isSliceOf(originalSource)) {
|
||
assert(false, "Memory corruption bug.");
|
||
}
|
||
|
||
const ptrdiff_t tokenDataOffset = (() @trusted => token.data.ptr - originalSource.ptr)();
|
||
auto mutSource = originalSource[tokenDataOffset .. $];
|
||
size_t mutOffset = token.data.length;
|
||
}
|
||
|
||
while (!next.empty) {
|
||
if (next.front.type != tokenType) {
|
||
break;
|
||
}
|
||
|
||
static if (operatingMode!string == OperatingMode.nonDestructive) {
|
||
token.data ~= next.front.data;
|
||
}
|
||
static if (operatingMode!string == OperatingMode.destructive) {
|
||
foreach (const c; next.front.data) {
|
||
mutSource[mutOffset] = c;
|
||
++mutOffset;
|
||
}
|
||
token.data = mutSource[0 .. mutOffset];
|
||
}
|
||
|
||
_source = next._source;
|
||
_locationState = next._locationState;
|
||
next.popFront();
|
||
}
|
||
|
||
return token;
|
||
}
|
||
|
||
Token lexSubstrings() {
|
||
final switch (_locationState) {
|
||
case LocationState.newLine:
|
||
case LocationState.key:
|
||
return this.lexSubstringsImpl!(TokenType.key);
|
||
|
||
case LocationState.preValue:
|
||
_locationState = LocationState.inValue;
|
||
goto case LocationState.inValue;
|
||
|
||
case LocationState.inValue:
|
||
return this.lexSubstringsImpl!(TokenType.value);
|
||
|
||
case LocationState.sectionHeader:
|
||
return this.lexSubstringsImpl!(TokenType.sectionHeader);
|
||
}
|
||
}
|
||
}
|
||
|
||
Token lexText() {
|
||
static if (dialect.hasFeature(Dialect.concatSubstrings)) {
|
||
if (!_bypassConcatSubstrings) {
|
||
return this.lexSubstrings();
|
||
}
|
||
}
|
||
|
||
return this.lexSubstring();
|
||
}
|
||
|
||
Token fetchFront() {
|
||
switch (_source[0]) {
|
||
|
||
default:
|
||
return this.lexText();
|
||
|
||
case '\x0A': {
|
||
_locationState = LocationState.newLine;
|
||
return this.makeToken(TokenType.lineBreak, 1);
|
||
}
|
||
|
||
case '\x0D': {
|
||
_locationState = LocationState.newLine;
|
||
|
||
// CR<EOF>?
|
||
if (this.isOnFinalChar) {
|
||
return this.makeToken(TokenType.lineBreak, 1);
|
||
}
|
||
|
||
// CRLF?
|
||
if (_source[1] == '\x0A') {
|
||
return this.makeToken(TokenType.lineBreak, 2);
|
||
}
|
||
|
||
// CR
|
||
return this.makeToken(TokenType.lineBreak, 1);
|
||
}
|
||
|
||
case '\x09':
|
||
case '\x0B':
|
||
case '\x0C':
|
||
case ' ':
|
||
if (_locationState == LocationState.inValue) {
|
||
return this.lexText();
|
||
}
|
||
return this.lexWhitespace();
|
||
|
||
case ':':
|
||
static if (dialect.hasFeature(Dialect.colonKeys)) {
|
||
goto case '=';
|
||
} else {
|
||
return this.lexText();
|
||
}
|
||
|
||
case '=':
|
||
_locationState = LocationState.preValue;
|
||
return this.makeToken(TokenType.keyValueSeparator, 1);
|
||
|
||
case '[':
|
||
_locationState = LocationState.sectionHeader;
|
||
return this.makeToken(TokenType.bracketOpen, 1);
|
||
|
||
case ']':
|
||
_locationState = LocationState.key;
|
||
return this.makeToken(TokenType.bracketClose, 1);
|
||
|
||
case ';': {
|
||
static if (dialect.hasFeature(Dialect.inlineComments)) {
|
||
return this.lexComment();
|
||
} else static if (dialect.hasFeature(Dialect.lineComments)) {
|
||
if (this.isAtStartOfLineOrEquivalent) {
|
||
return this.lexComment();
|
||
}
|
||
return this.lexText();
|
||
} else {
|
||
return this.lexText();
|
||
}
|
||
}
|
||
|
||
case '#': {
|
||
static if (dialect.hasFeature(Dialect.hashInlineComments)) {
|
||
return this.lexComment();
|
||
} else static if (dialect.hasFeature(Dialect.hashLineComments)) {
|
||
if (this.isAtStartOfLineOrEquivalent) {
|
||
return this.lexComment();
|
||
}
|
||
return this.lexText();
|
||
} else {
|
||
return this.lexText();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/++
|
||
Low-level INI parser with filtered output
|
||
|
||
This wrapper will only supply tokens of these types:
|
||
|
||
$(LIST
|
||
* IniTokenType.key
|
||
* IniTokenType.value
|
||
* IniTokenType.sectionHeader
|
||
* IniTokenType.invalid
|
||
)
|
||
|
||
See_also:
|
||
$(LIST
|
||
* [IniParser]
|
||
* [parseIniDocument]
|
||
* [parseIniAA]
|
||
)
|
||
+/
|
||
struct IniFilteredParser(
|
||
IniDialect dialect = IniDialect.defaults,
|
||
string = immutable(char)[],
|
||
) {
|
||
///
|
||
public alias Token = IniToken!string;
|
||
|
||
///
|
||
public enum isDestructive = IniParser!(dialect, string).isDestructive;
|
||
|
||
private IniParser!(dialect, string) _parser;
|
||
|
||
public @safe pure nothrow:
|
||
|
||
///
|
||
public this(IniParser!(dialect, string) parser) {
|
||
_parser = parser;
|
||
_parser.skipIrrelevant(true);
|
||
}
|
||
|
||
///
|
||
public this(string rawIni) {
|
||
auto parser = IniParser!(dialect, string)(rawIni);
|
||
this(parser);
|
||
}
|
||
|
||
///
|
||
bool empty() const @nogc => _parser.empty;
|
||
|
||
///
|
||
inout(Token) front() inout @nogc => _parser.front;
|
||
|
||
///
|
||
void popFront() {
|
||
_parser.popFront();
|
||
_parser.skipIrrelevant(true);
|
||
}
|
||
|
||
static if (!isDestructive) {
|
||
///
|
||
inout(typeof(this)) save() inout @nogc {
|
||
return this;
|
||
}
|
||
}
|
||
}
|
||
|
||
///
|
||
@safe @nogc unittest {
|
||
// INI document (demo data)
|
||
static immutable string rawIniDocument = `; This is a comment.
|
||
[section1]
|
||
foo = bar ;another comment
|
||
oachkatzl = schwoaf ;try pronouncing that
|
||
`;
|
||
|
||
// Combine feature flags to build the required dialect.
|
||
const myDialect = (IniDialect.defaults | IniDialect.inlineComments);
|
||
|
||
// Instantiate a new parser and supply our document string.
|
||
auto parser = IniParser!(myDialect)(rawIniDocument);
|
||
|
||
int comments = 0;
|
||
int sections = 0;
|
||
int keys = 0;
|
||
int values = 0;
|
||
|
||
// Process token by token.
|
||
foreach (const parser.Token token; parser) {
|
||
if (token.type == IniTokenType.comment) {
|
||
++comments;
|
||
}
|
||
if (token.type == IniTokenType.sectionHeader) {
|
||
++sections;
|
||
}
|
||
if (token.type == IniTokenType.key) {
|
||
++keys;
|
||
}
|
||
if (token.type == IniTokenType.value) {
|
||
++values;
|
||
}
|
||
}
|
||
|
||
assert(comments == 3);
|
||
assert(sections == 1);
|
||
assert(keys == 2);
|
||
assert(values == 2);
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable string rawIniDocument = `; This is a comment.
|
||
[section1]
|
||
s1key1 = value1
|
||
s1key2 = value2
|
||
|
||
; Another comment
|
||
|
||
[section no.2]
|
||
s2key1 = "value3"
|
||
s2key2 = value no.4
|
||
`;
|
||
|
||
auto parser = IniParser!()(rawIniDocument);
|
||
alias Token = typeof(parser).Token;
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.comment, " This is a comment."));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.type == TokenType.lineBreak);
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.bracketOpen, "["));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.sectionHeader, "section1"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.bracketClose, "]"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.type == TokenType.lineBreak);
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.key, "s1key1"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.whitespace, " "));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.keyValueSeparator, "="));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.whitespace, " "));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.value, "value1"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.type == TokenType.lineBreak);
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.key, "s1key2"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.value, "value2"), parser.front.data);
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.type == TokenType.lineBreak);
|
||
}
|
||
|
||
{
|
||
assert(!parser.skipIrrelevant());
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.sectionHeader, "section no.2"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.key, "s2key1"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.value, "value3"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.key, "s2key2"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(!parser.empty);
|
||
assert(parser.front == Token(TokenType.value, "value no.4"));
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.skipIrrelevant());
|
||
assert(parser.empty());
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = "#not-a = comment";
|
||
auto parser = makeIniParser(rawIni);
|
||
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "#not-a"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "comment"));
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = "; only a comment";
|
||
|
||
auto regularParser = makeIniParser(rawIni);
|
||
auto filteredParser = makeIniFilteredParser(rawIni);
|
||
|
||
assert(!regularParser.empty);
|
||
assert(filteredParser.empty);
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = "#actually_a = comment\r\n\t#another one\r\n\t\t ; oh, and a third one";
|
||
enum dialect = (Dialect.hashLineComments | Dialect.lineComments);
|
||
auto parser = makeIniParser!dialect(rawIni);
|
||
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.comment, "actually_a = comment"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front == parser.Token(TokenType.comment, "another one"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front == parser.Token(TokenType.comment, " oh, and a third one"));
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = ";not a = line comment\nkey = value ;not-a-comment \nfoo = bar # not a comment\t";
|
||
enum dialect = Dialect.lite;
|
||
auto parser = makeIniParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, ";not a"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "line comment"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front.type == TokenType.key);
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "value ;not-a-comment"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front.type == TokenType.key);
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "bar # not a comment"));
|
||
}
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = "; line comment 0\t\n\nkey = value ; comment-1\nfoo = bar #comment 2\n";
|
||
enum dialect = (Dialect.inlineComments | Dialect.hashInlineComments);
|
||
auto parser = makeIniParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.comment, " line comment 0\t"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front.type == TokenType.key);
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front == parser.Token(TokenType.value, "value"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front == parser.Token(TokenType.comment, " comment-1"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front.type == TokenType.key);
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front == parser.Token(TokenType.value, "bar"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
assert(parser.front == parser.Token(TokenType.comment, "comment 2"));
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.skipIrrelevant(false));
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = "key = value;inline";
|
||
enum dialect = Dialect.inlineComments;
|
||
auto parser = makeIniParser!dialect(rawIni);
|
||
|
||
assert(!parser.empty);
|
||
parser.front == parser.Token(TokenType.key, "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
parser.front == parser.Token(TokenType.value, "value");
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant(false));
|
||
parser.front == parser.Token(TokenType.comment, "inline");
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni = "key: value\n"
|
||
~ "foo= bar\n"
|
||
~ "lol :rofl\n"
|
||
~ "Oachkatzl : -Schwoaf\n"
|
||
~ `"Schüler:innen": 10`;
|
||
enum dialect = (Dialect.colonKeys | Dialect.quotedStrings);
|
||
auto parser = makeIniParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "key"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "value"));
|
||
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.key, "foo"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "bar"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.key, "lol"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "rofl"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.key, "Oachkatzl"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "-Schwoaf"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.key, "Schüler:innen"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.skipIrrelevant());
|
||
assert(parser.front == parser.Token(TokenType.value, "10"));
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.skipIrrelevant());
|
||
}
|
||
|
||
@safe @nogc unittest {
|
||
static immutable rawIni =
|
||
"\"foo=bar\"=foobar\n"
|
||
~ "'foo = bar' = foo_bar\n"
|
||
~ "foo = \"bar\"\n"
|
||
~ "foo = 'bar'\n"
|
||
~ "foo = ' bar '\n"
|
||
~ "foo = \" bar \"\n"
|
||
~ "multi_line = 'line1\nline2'\n"
|
||
~ "syntax = \"error";
|
||
enum dialect = (Dialect.quotedStrings | Dialect.singleQuoteQuotedStrings);
|
||
auto parser = makeIniFilteredParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "foo=bar"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, "foobar"));
|
||
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "foo = bar"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, "foo_bar"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "foo"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, "bar"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "foo"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, "bar"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "foo"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, " bar "));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "foo"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, " bar "));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "multi_line"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, "line1\nline2"));
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.key, "syntax"));
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front == parser.Token(TokenType.value, "error"));
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe unittest {
|
||
char[] rawIni = `
|
||
key = \nvalue\n
|
||
key = foo\t bar
|
||
key\0key = value
|
||
key \= = value
|
||
`.dup;
|
||
enum dialect = Dialect.escapeSequences;
|
||
auto parser = makeIniFilteredParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "\nvalue\n");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "foo\t bar");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key\0key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key =");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value");
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable string rawIni = `
|
||
key = \nvalue\n
|
||
key = foo\t bar
|
||
key\0key = value
|
||
key \= = value
|
||
`;
|
||
enum dialect = Dialect.escapeSequences;
|
||
auto parser = makeIniFilteredParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "\nvalue\n");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "foo\t bar");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key\0key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key =");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value");
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe unittest {
|
||
char[] rawIni = "key = val\\\nue\nkey \\\n= \\\nvalue \\\rvalu\\\r\ne\n".dup;
|
||
enum dialect = Dialect.lineFolding;
|
||
auto parser = makeIniFilteredParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value value");
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable string rawIni = "key = val\\\nue\nkey \\\n= \\\nvalue \\\rvalu\\\r\ne\n";
|
||
enum dialect = Dialect.lineFolding;
|
||
auto parser = makeIniFilteredParser!dialect(rawIni);
|
||
|
||
{
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value");
|
||
}
|
||
|
||
{
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "key");
|
||
|
||
parser.popFront();
|
||
assert(!parser.empty);
|
||
assert(parser.front.data == "value value");
|
||
}
|
||
|
||
parser.popFront();
|
||
assert(parser.empty);
|
||
}
|
||
|
||
/++
|
||
Convenience function to create a low-level parser
|
||
|
||
$(TIP
|
||
Unlike with the constructor of [IniParser],
|
||
the compiler is able to infer the `string` template parameter.
|
||
)
|
||
|
||
See_also:
|
||
[makeIniFilteredParser]
|
||
+/
|
||
IniParser!(dialect, string) makeIniParser(
|
||
IniDialect dialect = IniDialect.defaults,
|
||
string,
|
||
)(
|
||
string rawIni,
|
||
) @safe pure nothrow if (isCompatibleString!string) {
|
||
return IniParser!(dialect, string)(rawIni);
|
||
}
|
||
|
||
///
|
||
@safe @nogc unittest {
|
||
string regular;
|
||
auto parser1 = makeIniParser(regular);
|
||
assert(parser1.empty); // exclude from docs
|
||
|
||
char[] mutable;
|
||
auto parser2 = makeIniParser(mutable);
|
||
assert(parser2.empty); // exclude from docs
|
||
|
||
const(char)[] constChars;
|
||
auto parser3 = makeIniParser(constChars);
|
||
assert(parser3.empty); // exclude from docs
|
||
|
||
assert(!parser1.isDestructive); // exclude from docs
|
||
assert(!parser2.isDestructive); // exclude from docs
|
||
assert(!parser3.isDestructive); // exclude from docs
|
||
}
|
||
|
||
@safe unittest {
|
||
char[] mutableInput;
|
||
enum dialect = Dialect.concatSubstrings;
|
||
|
||
auto parser1 = makeIniParser!(dialect, const(char)[])(mutableInput);
|
||
auto parser2 = (() @nogc => makeIniParser!(dialect)(mutableInput))();
|
||
|
||
assert(!parser1.isDestructive);
|
||
assert(parser2.isDestructive);
|
||
}
|
||
|
||
/++
|
||
Convenience function to create a low-level filtered parser
|
||
|
||
$(TIP
|
||
Unlike with the constructor of [IniFilteredParser],
|
||
the compiler is able to infer the `string` template parameter.
|
||
)
|
||
|
||
See_also:
|
||
[makeIniParser]
|
||
+/
|
||
IniFilteredParser!(dialect, string) makeIniFilteredParser(
|
||
IniDialect dialect = IniDialect.defaults,
|
||
string,
|
||
)(
|
||
string rawIni,
|
||
) @safe pure nothrow if (isCompatibleString!string) {
|
||
return IniFilteredParser!(dialect, string)(rawIni);
|
||
}
|
||
|
||
///
|
||
@safe @nogc unittest {
|
||
string regular;
|
||
auto parser1 = makeIniFilteredParser(regular);
|
||
assert(parser1.empty); // exclude from docs
|
||
|
||
char[] mutable;
|
||
auto parser2 = makeIniFilteredParser(mutable);
|
||
assert(parser2.empty); // exclude from docs
|
||
|
||
const(char)[] constChars;
|
||
auto parser3 = makeIniFilteredParser(constChars);
|
||
assert(parser3.empty); // exclude from docs
|
||
}
|
||
|
||
// undocumented
|
||
debug {
|
||
void writelnTokens(IniDialect dialect, string)(IniParser!(dialect, string) parser) @safe {
|
||
import std.stdio : writeln;
|
||
|
||
foreach (token; parser) {
|
||
writeln(token);
|
||
}
|
||
}
|
||
|
||
void writelnTokens(IniDialect dialect, string)(IniFilteredParser!(dialect, string) parser) @safe {
|
||
import std.stdio : writeln;
|
||
|
||
foreach (token; parser) {
|
||
writeln(token);
|
||
}
|
||
}
|
||
}
|
||
|
||
/++
|
||
Data entry of an INI document
|
||
+/
|
||
struct IniKeyValuePair(string) if (isCompatibleString!string) {
|
||
///
|
||
string key;
|
||
|
||
///
|
||
string value;
|
||
}
|
||
|
||
/++
|
||
Section of an INI document
|
||
|
||
$(NOTE
|
||
Data entries from the document’s root – i.e. those with no designated section –
|
||
are stored in a section with its `name` set to `null`.
|
||
)
|
||
+/
|
||
struct IniSection(string) if (isCompatibleString!string) {
|
||
///
|
||
alias KeyValuePair = IniKeyValuePair!string;
|
||
|
||
/++
|
||
Name of the section
|
||
|
||
Also known as “key”.
|
||
+/
|
||
string name;
|
||
|
||
/++
|
||
Data entries of the section
|
||
+/
|
||
KeyValuePair[] items;
|
||
}
|
||
|
||
/++
|
||
DOM representation of an INI document
|
||
+/
|
||
struct IniDocument(string) if (isCompatibleString!string) {
|
||
///
|
||
alias Section = IniSection!string;
|
||
|
||
/++
|
||
Sections of the document
|
||
|
||
$(NOTE
|
||
Data entries from the document’s root – i.e. those with no designated section –
|
||
are stored in a section with its `name` set to `null`.
|
||
|
||
If there are no named sections in a document, there will be only a single section with no name (`null`).
|
||
)
|
||
+/
|
||
Section[] sections;
|
||
}
|
||
|
||
/++
|
||
Parses an INI string into a document ("DOM").
|
||
|
||
See_also:
|
||
[parseIniAA]
|
||
+/
|
||
IniDocument!string parseIniDocument(IniDialect dialect = IniDialect.defaults, string)(string rawIni) @safe pure nothrow
|
||
if (isCompatibleString!string) {
|
||
alias Document = IniDocument!string;
|
||
alias Section = IniSection!string;
|
||
alias KeyValuePair = IniKeyValuePair!string;
|
||
|
||
auto parser = IniParser!(dialect, string)(rawIni);
|
||
|
||
auto document = Document(null);
|
||
auto section = Section(null, null);
|
||
auto kvp = KeyValuePair(null, null);
|
||
|
||
void commitKeyValuePair(string nextKey = null) {
|
||
if (kvp.key !is null) {
|
||
section.items ~= kvp;
|
||
}
|
||
kvp = KeyValuePair(nextKey, null);
|
||
}
|
||
|
||
void commitSection(string nextSectionName) {
|
||
commitKeyValuePair(null);
|
||
|
||
const isNamelessAndEmpty = (
|
||
(section.name is null)
|
||
&& (section.items.length == 0)
|
||
);
|
||
|
||
if (!isNamelessAndEmpty) {
|
||
document.sections ~= section;
|
||
}
|
||
|
||
if (nextSectionName !is null) {
|
||
section = Section(nextSectionName, null);
|
||
}
|
||
}
|
||
|
||
while (!parser.skipIrrelevant()) {
|
||
switch (parser.front.type) with (TokenType) {
|
||
|
||
case key:
|
||
commitKeyValuePair(parser.front.data);
|
||
break;
|
||
|
||
case value:
|
||
kvp.value = parser.front.data;
|
||
break;
|
||
|
||
case sectionHeader:
|
||
commitSection(parser.front.data);
|
||
break;
|
||
|
||
default:
|
||
assert(false, "Unexpected parsing error."); // TODO
|
||
}
|
||
|
||
parser.popFront();
|
||
}
|
||
|
||
commitSection(null);
|
||
|
||
return document;
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
// INI document (demo data)
|
||
static immutable string iniString = `; This is a comment.
|
||
|
||
Oachkatzlschwoaf = Seriously, try pronouncing this :P
|
||
|
||
[Section #1]
|
||
foo = bar
|
||
d = rocks
|
||
|
||
; Another comment
|
||
|
||
[Section No.2]
|
||
name = Walter Bright
|
||
company = "Digital Mars"
|
||
`;
|
||
|
||
// Parse the document.
|
||
auto doc = parseIniDocument(iniString);
|
||
|
||
version (none) // exclude from docs
|
||
// …is equivalent to:
|
||
auto doc = parseIniDocument!(IniDialect.defaults)(iniString);
|
||
|
||
assert(doc.sections.length == 3);
|
||
|
||
// "Root" section (no name):
|
||
assert(doc.sections[0].name is null);
|
||
assert(doc.sections[0].items == [
|
||
IniKeyValuePair!string("Oachkatzlschwoaf", "Seriously, try pronouncing this :P"),
|
||
]);
|
||
|
||
// A section with a name:
|
||
assert(doc.sections[1].name == "Section #1");
|
||
assert(doc.sections[1].items.length == 2);
|
||
assert(doc.sections[1].items[0] == IniKeyValuePair!string("foo", "bar"));
|
||
assert(doc.sections[1].items[1] == IniKeyValuePair!string("d", "rocks"));
|
||
|
||
// Another section:
|
||
assert(doc.sections[2].name == "Section No.2");
|
||
assert(doc.sections[2].items == [
|
||
IniKeyValuePair!string("name", "Walter Bright"),
|
||
IniKeyValuePair!string("company", "Digital Mars"),
|
||
]);
|
||
}
|
||
|
||
@safe unittest {
|
||
auto doc = parseIniDocument("");
|
||
assert(doc.sections == []);
|
||
|
||
doc = parseIniDocument(";Comment\n;Comment2\n");
|
||
assert(doc.sections == []);
|
||
}
|
||
|
||
@safe unittest {
|
||
char[] mutable = ['f', 'o', 'o', '=', 'b', 'a', 'r', '\n'];
|
||
|
||
auto doc = parseIniDocument(mutable);
|
||
assert(doc.sections[0].items[0].key == "foo");
|
||
assert(doc.sections[0].items[0].value == "bar");
|
||
|
||
// is mutable
|
||
static assert(is(typeof(doc.sections[0].items[0].value) == char[]));
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable demoData = `
|
||
0 = a 'b'
|
||
1 = a "b"
|
||
2 = 'a' b
|
||
3 = "a" b
|
||
`;
|
||
|
||
enum dialect = (Dialect.concatSubstrings | Dialect.quotedStrings | Dialect.singleQuoteQuotedStrings);
|
||
auto doc = parseIniDocument!dialect(demoData);
|
||
assert(doc.sections[0].items[0].value == "a b");
|
||
assert(doc.sections[0].items[1].value == "ab");
|
||
assert(doc.sections[0].items[2].value == "a b");
|
||
assert(doc.sections[0].items[3].value == "ab");
|
||
}
|
||
|
||
/++
|
||
Parses an INI string into an associate array.
|
||
|
||
$(LIST
|
||
* Duplicate keys cause values to get overwritten.
|
||
* Sections with the same name are merged.
|
||
)
|
||
|
||
See_also:
|
||
[parseIniDocument]
|
||
+/
|
||
string[immutable(char)[]][immutable(char)[]] parseIniAA(
|
||
IniDialect dialect = IniDialect.defaults,
|
||
string,
|
||
)(
|
||
string rawIni,
|
||
) @safe pure nothrow {
|
||
static if (is(string == immutable(char)[])) {
|
||
immutable(char)[] toString(string key) => key;
|
||
} else {
|
||
immutable(char)[] toString(string key) => key.idup;
|
||
}
|
||
|
||
auto parser = IniParser!(dialect, string)(rawIni);
|
||
|
||
string[immutable(char)[]][immutable(char)[]] document;
|
||
string[immutable(char)[]] section;
|
||
|
||
string sectionName = null;
|
||
string keyName = null;
|
||
string value = null;
|
||
|
||
void commitKeyValuePair(string nextKey) {
|
||
if (keyName !is null) {
|
||
section[toString(keyName)] = value;
|
||
}
|
||
|
||
keyName = nextKey;
|
||
value = null;
|
||
}
|
||
|
||
void setValue(string nextValue) {
|
||
value = nextValue;
|
||
}
|
||
|
||
void commitSection(string nextSection) {
|
||
commitKeyValuePair(null);
|
||
if ((sectionName !is null) || (section.length > 0)) {
|
||
document[toString(sectionName)] = section;
|
||
section = null;
|
||
}
|
||
|
||
if (nextSection !is null) {
|
||
auto existingSection = nextSection in document;
|
||
if (existingSection !is null) {
|
||
section = *existingSection;
|
||
}
|
||
|
||
sectionName = nextSection;
|
||
}
|
||
}
|
||
|
||
while (!parser.skipIrrelevant()) {
|
||
switch (parser.front.type) with (TokenType) {
|
||
|
||
case key:
|
||
commitKeyValuePair(parser.front.data);
|
||
break;
|
||
|
||
case value:
|
||
setValue(parser.front.data);
|
||
break;
|
||
|
||
case sectionHeader:
|
||
commitSection(parser.front.data);
|
||
break;
|
||
|
||
default:
|
||
assert(false, "Unexpected parsing error."); // TODO
|
||
}
|
||
|
||
parser.popFront();
|
||
}
|
||
|
||
commitSection(null);
|
||
|
||
return document;
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
// INI document
|
||
static immutable string demoData = `; This is a comment.
|
||
|
||
Oachkatzlschwoaf = Seriously, try pronouncing this :P
|
||
|
||
[Section #1]
|
||
foo = bar
|
||
d = rocks
|
||
|
||
; Another comment
|
||
|
||
[Section No.2]
|
||
name = Walter Bright
|
||
company = "Digital Mars"
|
||
website = <https://digitalmars.com/>
|
||
;email = "noreply@example.org"
|
||
`;
|
||
|
||
// Parse the document into an associative array.
|
||
auto aa = parseIniAA(demoData);
|
||
|
||
assert(aa.length == 3);
|
||
|
||
assert(aa[null].length == 1);
|
||
assert(aa[null]["Oachkatzlschwoaf"] == "Seriously, try pronouncing this :P");
|
||
|
||
assert(aa["Section #1"].length == 2);
|
||
assert(aa["Section #1"]["foo"] == "bar");
|
||
assert(aa["Section #1"]["d"] == "rocks");
|
||
|
||
string[string] section2 = aa["Section No.2"];
|
||
assert(section2.length == 3);
|
||
assert(section2["name"] == "Walter Bright");
|
||
assert(section2["company"] == "Digital Mars");
|
||
assert(section2["website"] == "<https://digitalmars.com/>");
|
||
|
||
// "email" is commented out
|
||
assert(!("email" in section2));
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable demoData = `[1]
|
||
key = "value1" "value2"
|
||
[2]
|
||
0 = a b
|
||
1 = 'a' b
|
||
2 = a 'b'
|
||
3 = a "b"
|
||
4 = "a" 'b'
|
||
5 = 'a' "b"
|
||
6 = "a" "b"
|
||
7 = 'a' 'b'
|
||
8 = 'a' "b" 'c'
|
||
`;
|
||
|
||
enum dialect = (Dialect.concatSubstrings | Dialect.quotedStrings | Dialect.singleQuoteQuotedStrings);
|
||
auto aa = parseIniAA!(dialect, char[])(demoData.dup);
|
||
|
||
assert(aa.length == 2);
|
||
assert(!(null in aa));
|
||
assert("1" in aa);
|
||
assert("2" in aa);
|
||
assert(aa["1"]["key"] == "value1value2");
|
||
assert(aa["2"]["0"] == "a b");
|
||
assert(aa["2"]["1"] == "a b");
|
||
assert(aa["2"]["2"] == "a b");
|
||
assert(aa["2"]["3"] == "ab");
|
||
assert(aa["2"]["4"] == "ab");
|
||
assert(aa["2"]["5"] == "ab");
|
||
assert(aa["2"]["6"] == "ab");
|
||
assert(aa["2"]["7"] == "a b");
|
||
assert(aa["2"]["8"] == "abc");
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable string demoData = `[1]
|
||
key = "value1" "value2"
|
||
[2]
|
||
0 = a b
|
||
1 = 'a' b
|
||
2 = a 'b'
|
||
3 = a "b"
|
||
4 = "a" 'b'
|
||
5 = 'a' "b"
|
||
6 = "a" "b"
|
||
7 = 'a' 'b'
|
||
8 = 'a' "b" 'c'
|
||
`;
|
||
|
||
enum dialect = (Dialect.concatSubstrings | Dialect.quotedStrings | Dialect.singleQuoteQuotedStrings);
|
||
auto aa = parseIniAA!dialect(demoData);
|
||
|
||
assert(aa.length == 2);
|
||
assert(!(null in aa));
|
||
assert("1" in aa);
|
||
assert("2" in aa);
|
||
assert(aa["1"]["key"] == "value1value2");
|
||
assert(aa["2"]["0"] == "a b");
|
||
assert(aa["2"]["1"] == "a b");
|
||
assert(aa["2"]["2"] == "a b");
|
||
assert(aa["2"]["3"] == "ab");
|
||
assert(aa["2"]["4"] == "ab");
|
||
assert(aa["2"]["5"] == "ab");
|
||
assert(aa["2"]["6"] == "ab");
|
||
assert(aa["2"]["7"] == "a b");
|
||
assert(aa["2"]["8"] == "abc");
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable string demoData = `
|
||
0 = "a" b
|
||
1 = "a" 'b'
|
||
2 = a "b"
|
||
3 = 'a' "b"
|
||
`;
|
||
|
||
enum dialect = (Dialect.concatSubstrings | Dialect.singleQuoteQuotedStrings);
|
||
auto aa = parseIniAA!dialect(demoData);
|
||
|
||
assert(aa.length == 1);
|
||
assert(aa[null]["0"] == `"a" b`);
|
||
assert(aa[null]["1"] == `"a" b`);
|
||
assert(aa[null]["2"] == `a "b"`);
|
||
assert(aa[null]["3"] == `a "b"`);
|
||
}
|
||
|
||
@safe unittest {
|
||
static immutable const(char)[] demoData = `[1]
|
||
key = original
|
||
no2 = kept
|
||
[2]
|
||
key = original
|
||
key = overwritten
|
||
[1]
|
||
key = merged and overwritten
|
||
`;
|
||
|
||
enum dialect = Dialect.concatSubstrings;
|
||
auto aa = parseIniAA!dialect(demoData);
|
||
|
||
assert(aa.length == 2);
|
||
assert(!(null in aa));
|
||
assert("1" in aa);
|
||
assert("2" in aa);
|
||
assert(aa["1"]["key"] == "merged and overwritten");
|
||
assert(aa["1"]["no2"] == "kept");
|
||
assert(aa["2"]["key"] == "overwritten");
|
||
}
|
||
|
||
private void stringifyIniString(string, OutputRange)(string data, OutputRange output) {
|
||
if (data is null) {
|
||
output.put("\"\"");
|
||
return;
|
||
}
|
||
|
||
size_t nQuotes = 0;
|
||
size_t nSingleQuotes = 0;
|
||
bool hasLineBreaks = false;
|
||
|
||
foreach (const c; data) {
|
||
switch (c) {
|
||
default:
|
||
break;
|
||
|
||
case '"':
|
||
++nQuotes;
|
||
break;
|
||
case '\'':
|
||
++nSingleQuotes;
|
||
break;
|
||
|
||
case '\n':
|
||
case '\r':
|
||
hasLineBreaks = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const hasQuotes = (nQuotes > 0);
|
||
const hasSingleQuotes = (nSingleQuotes > 0);
|
||
|
||
if (hasQuotes && !hasSingleQuotes) {
|
||
output.put("'");
|
||
output.put(data);
|
||
output.put("'");
|
||
return;
|
||
}
|
||
|
||
if (!hasQuotes && hasSingleQuotes) {
|
||
output.put("\"");
|
||
output.put(data);
|
||
output.put("\"");
|
||
return;
|
||
}
|
||
|
||
if (hasQuotes && hasSingleQuotes) {
|
||
if (nQuotes <= nSingleQuotes) {
|
||
output.put("\"");
|
||
|
||
foreach (const c; StringSliceRange(data)) {
|
||
if (c == "\"") {
|
||
output.put("\" '\"' \"");
|
||
continue;
|
||
}
|
||
|
||
output.put(c);
|
||
}
|
||
|
||
output.put("\"");
|
||
return;
|
||
}
|
||
|
||
if ( /*nQuotes > nSingleQuotes*/ true) {
|
||
output.put("'");
|
||
|
||
foreach (const c; StringSliceRange(data)) {
|
||
if (c == "'") {
|
||
output.put("' \"'\" '");
|
||
continue;
|
||
}
|
||
|
||
output.put(c);
|
||
}
|
||
|
||
output.put("'");
|
||
return;
|
||
}
|
||
}
|
||
|
||
if ( /*!hasQuotes && !hasSingleQuotes*/ true) {
|
||
if (hasLineBreaks) {
|
||
output.put("\"");
|
||
}
|
||
|
||
output.put(data);
|
||
|
||
if (hasLineBreaks) {
|
||
output.put("\"");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void stringifyIni(StringKey, StringValue, OutputRange)(StringKey key, StringValue value, OutputRange output) {
|
||
stringifyIniString(key, output);
|
||
output.put(" = ");
|
||
stringifyIniString(value, output);
|
||
output.put("\n");
|
||
}
|
||
|
||
private void stringifyIni(string, OutputRange)(const IniKeyValuePair!string kvp, OutputRange output) {
|
||
return stringifyIni(kvp.key, kvp.value, output);
|
||
}
|
||
|
||
private void stringifyIniSectionHeader(string, OutputRange)(string sectionName, OutputRange output) {
|
||
if (sectionName !is null) {
|
||
output.put("[");
|
||
stringifyIniString(sectionName, output);
|
||
output.put("]\n");
|
||
}
|
||
}
|
||
|
||
private void stringifyIni(string, OutputRange)(const IniSection!string section, OutputRange output) {
|
||
stringifyIniSectionHeader(section.name, output);
|
||
foreach (const item; section.items) {
|
||
stringifyIni(item, output);
|
||
}
|
||
}
|
||
|
||
/++
|
||
Serializes an [IniDocument] to a string in INI format.
|
||
+/
|
||
void stringifyIni(string, OutputRange)(IniDocument!string document, OutputRange output) {
|
||
bool anySectionsWritten = false;
|
||
|
||
foreach (const section; document.sections) {
|
||
if (section.name is null) {
|
||
if (anySectionsWritten) {
|
||
output.put("\n");
|
||
}
|
||
|
||
stringifyIni(section, output);
|
||
|
||
if (section.items.length > 0) {
|
||
anySectionsWritten = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (const section; document.sections) {
|
||
if (section.name is null) {
|
||
continue;
|
||
}
|
||
|
||
if (!anySectionsWritten) {
|
||
anySectionsWritten = true;
|
||
} else {
|
||
output.put("\n");
|
||
}
|
||
|
||
stringifyIni(section, output);
|
||
}
|
||
}
|
||
|
||
/// ditto
|
||
string stringifyIni(string)(IniDocument!string document) {
|
||
import std.array : appender;
|
||
|
||
auto output = appender!string();
|
||
stringifyIni(document, output);
|
||
return output[];
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
auto doc = IniDocument!string([
|
||
IniSection!string(null, [
|
||
IniKeyValuePair!string("key", "value"),
|
||
]),
|
||
IniSection!string("Section 1", [
|
||
IniKeyValuePair!string("key1", "value1"),
|
||
IniKeyValuePair!string("key2", "foo'bar"),
|
||
]),
|
||
]);
|
||
|
||
// Serialize
|
||
string ini = stringifyIni(doc);
|
||
|
||
static immutable expected =
|
||
"key = value\n"
|
||
~ "\n"
|
||
~ "[Section 1]\n"
|
||
~ "key1 = value1\n"
|
||
~ "key2 = \"foo'bar\"\n";
|
||
assert(ini == expected);
|
||
}
|
||
|
||
@safe unittest {
|
||
auto doc = IniDocument!string([
|
||
IniSection!string("Oachkatzlschwoaf", [
|
||
IniKeyValuePair!string("key1", "value1"),
|
||
IniKeyValuePair!string("key2", "value2"),
|
||
IniKeyValuePair!string("key3", "foo bar"),
|
||
]),
|
||
IniSection!string(null, [
|
||
IniKeyValuePair!string("key", "value"),
|
||
]),
|
||
IniSection!string("Kaiserschmarrn", [
|
||
IniKeyValuePair!string("1", "value\n1"),
|
||
IniKeyValuePair!string("2", "\"value\t2"),
|
||
IniKeyValuePair!string("3", "\"foo'bar\""),
|
||
IniKeyValuePair!string("4", "'foo\"bar'"),
|
||
]),
|
||
]);
|
||
|
||
string ini = stringifyIni(doc);
|
||
|
||
static immutable expected = "key = value\n"
|
||
~ "\n"
|
||
~ "[Oachkatzlschwoaf]\n"
|
||
~ "key1 = value1\n"
|
||
~ "key2 = value2\n"
|
||
~ "key3 = foo bar\n"
|
||
~ "\n"
|
||
~ "[Kaiserschmarrn]\n"
|
||
~ "1 = \"value\n1\"\n"
|
||
~ "2 = '\"value\t2'\n"
|
||
~ "3 = '\"foo' \"'\" 'bar\"'\n"
|
||
~ "4 = \"'foo\" '\"' \"bar'\"\n";
|
||
assert(ini == expected);
|
||
}
|
||
|
||
/++
|
||
Serializes an AA to a string in INI format.
|
||
+/
|
||
void stringifyIni(
|
||
StringKey,
|
||
StringValue,
|
||
OutputRange,
|
||
)(
|
||
const StringValue[StringKey] sectionItems,
|
||
OutputRange output,
|
||
) if (isCompatibleString!StringKey && isCompatibleString!StringValue) {
|
||
foreach (key, value; sectionItems) {
|
||
stringifyIni(key, value, output);
|
||
}
|
||
}
|
||
|
||
/// ditto
|
||
string stringifyIni(
|
||
StringKey,
|
||
StringValue,
|
||
)(
|
||
const StringValue[StringKey] sectionItems
|
||
) if (isCompatibleString!StringKey && isCompatibleString!StringValue) {
|
||
import std.array : appender;
|
||
|
||
auto output = appender!string();
|
||
stringifyIni(sectionItems, output);
|
||
return output[];
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
string[string] doc;
|
||
doc["1"] = "value1";
|
||
doc["2"] = "foo'bar";
|
||
|
||
// Serialize AA to INI
|
||
string ini = stringifyIni(doc);
|
||
|
||
// dfmt off
|
||
static immutable expectedEither = "1 = value1\n" ~ "2 = \"foo'bar\"\n"; // exclude from docs
|
||
static immutable expectedOr = "2 = \"foo'bar\"\n" ~ "1 = value1\n" ; // exclude from docs
|
||
// dfmt on
|
||
|
||
assert(ini == expectedEither || ini == expectedOr); // exclude from docs
|
||
}
|
||
|
||
/++
|
||
Serializes a nested AA to a string in INI format.
|
||
+/
|
||
void stringifyIni(
|
||
StringSection,
|
||
StringKey,
|
||
StringValue,
|
||
OutputRange,
|
||
)(
|
||
const StringValue[StringKey][StringSection] document,
|
||
OutputRange output,
|
||
) if (isCompatibleString!StringSection && isCompatibleString!StringKey && isCompatibleString!StringValue) {
|
||
bool anySectionsWritten = false;
|
||
|
||
const rootSection = null in document;
|
||
if (rootSection !is null) {
|
||
stringifyIni(*rootSection, output);
|
||
anySectionsWritten = true;
|
||
}
|
||
|
||
foreach (sectionName, items; document) {
|
||
if (sectionName is null) {
|
||
continue;
|
||
}
|
||
|
||
if (!anySectionsWritten) {
|
||
anySectionsWritten = true;
|
||
} else {
|
||
output.put("\n");
|
||
}
|
||
|
||
stringifyIniSectionHeader(sectionName, output);
|
||
foreach (key, value; items) {
|
||
stringifyIni(key, value, output);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// ditto
|
||
string stringifyIni(
|
||
StringSection,
|
||
StringKey,
|
||
StringValue,
|
||
)(
|
||
const StringValue[StringKey][StringSection] document,
|
||
) if (isCompatibleString!StringSection && isCompatibleString!StringKey && isCompatibleString!StringValue) {
|
||
import std.array : appender;
|
||
|
||
auto output = appender!string();
|
||
stringifyIni(document, output);
|
||
return output[];
|
||
}
|
||
|
||
///
|
||
@safe unittest {
|
||
string[string][string] doc;
|
||
|
||
doc[null]["key"] = "value";
|
||
doc[null]["foo"] = "bar";
|
||
|
||
doc["Section 1"]["firstname"] = "Walter";
|
||
doc["Section 1"]["lastname"] = "Bright";
|
||
doc["Section 1"]["language"] = "'D'";
|
||
|
||
doc["Section 2"]["Oachkatzl"] = "Schwoaf";
|
||
|
||
// Serialize AA to INI
|
||
string ini = stringifyIni(doc);
|
||
|
||
import std.string : indexOf, startsWith; // exclude from docs
|
||
|
||
assert(ini.startsWith("key = value\n") || ini.startsWith("foo = bar\n")); // exclude from docs
|
||
assert(ini.indexOf("\n[Section 1]\n") > 0); // exclude from docs
|
||
assert(ini.indexOf("\nfirstname = Walter\n") > 0); // exclude from docs
|
||
assert(ini.indexOf("\nlastname = Bright\n") > 0); // exclude from docs
|
||
assert(ini.indexOf("\nlanguage = \"'D'\"\n") > 0); // exclude from docs
|
||
assert(ini.indexOf("\n[Section 2]\n") > 0); // exclude from docs
|
||
assert(ini.indexOf("\nOachkatzl = Schwoaf\n") > 0); // exclude from docs
|
||
}
|
||
|
||
@safe unittest {
|
||
string[string][string] doc;
|
||
doc[null]["key"] = "value";
|
||
doc["S1"]["1"] = "value1";
|
||
doc["S1"]["2"] = "value2";
|
||
doc["S2"]["x"] = "foo'bar";
|
||
doc["S2"][null] = "bamboozled";
|
||
|
||
string ini = stringifyIni(doc);
|
||
|
||
import std.string : indexOf, startsWith;
|
||
|
||
assert(ini.startsWith("key = value\n"));
|
||
assert(ini.indexOf("\n[S1]\n") > 0);
|
||
assert(ini.indexOf("\n1 = value1\n") > 0);
|
||
assert(ini.indexOf("\n2 = value2\n") > 0);
|
||
assert(ini.indexOf("\n[S2]\n") > 0);
|
||
assert(ini.indexOf("\nx = \"foo'bar\"\n") > 0);
|
||
assert(ini.indexOf("\n\"\" = bamboozled\n") > 0);
|
||
}
|