Skip to content
This repository was archived by the owner on Sep 12, 2024. It is now read-only.

0.4.0 Feature complete #34

Merged
merged 17 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions examples/basic/basic.odin
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import ini "../.."
import "core:fmt"

main :: proc() {
config := ini.read_from_file("../../tests/sections.ini")

ini.Options.Symbols.NestedSection = '/'
config := ini.new_config("config")
defer ini.destroy_config(config)

ini.set(config, "wife", "Monica")

children := ini.add_section(config, "children")
ini.set(children, "daughter", "Jessica")
ini.set(children, "son", "Billy")
section := ini.add_section(config, "children")
ini.set(section, "kevin", "kevin")

section_nested := ini.add_section(section, "grandchildren")
ini.set(section_nested, "Anna", "Anna")

section_nested2 := ini.add_section(section, "grandchildren2")
ini.set(section_nested2, "Anna2", "Anna2")

section_nested3 := ini.add_section(section_nested2, "grandchildren3")
ini.set(section_nested3, "Anna3", "Anna2")

ini.write_to_file(config, "test.ini")

s := ini.write_to_string(config)
defer delete(s)
fmt.println(s)
}
59 changes: 50 additions & 9 deletions ini.odin
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ini

import "core:fmt"
import "core:strings"
import "core:log"

// Ini Config Type
// .value is the value of an entry stored in the config (or section of the config), or the name of the config.
Expand All @@ -22,7 +23,7 @@ new_config :: proc(name: string) -> ^Config {
p := new(Config)
p.value = strings.clone(name)
p.keys = make(map[string]^Config)

log.debugf("Created a new config with the name '%s'.", name)
return p
}

Expand All @@ -44,17 +45,26 @@ destroy_config :: proc(c: ^Config) {
}
delete(c.value)
free(c)
log.debugf("Destroyed config with the name '%s'.", c.value)
}

// Adds a new section to the given config and returns a pointer to it.
add_section :: proc(c: ^Config, name: string) -> (^Config, bool) #optional_ok {
if c == nil || has_key(c, name) do return nil, false
if c == nil {
log.errorf("Provided ^Config is nil. Returning nil.")
return nil, false
} else if has_key(c, name) {
log.errorf("Provided ^Config already contains the key '%s'. Returning nil.", name)
return nil, false
}

s := new(Config)
s.value = strings.clone(name)
s.keys = make(map[string]^Config)

c.keys[name] = s

log.debugf("Added section '%s' to config '%s'.", name, c.value)
return s, true
}

Expand All @@ -63,7 +73,13 @@ set :: proc{set_key, set_section}

// Sets the value of a given key.
set_key :: proc(c: ^Config, key: string, value: string) -> bool {
if c == nil || has_key(c, key) do return false
if c == nil {
log.errorf("Provided ^Config is nil. Returning.")
return false
} else if has_key(c, key) {
log.errorf("Provided ^Config already contains the key '%s'. Returning.", key)
return false
}

c.keys[key] = new(Config)
c.keys[key].value = strings.clone(value)
Expand All @@ -72,7 +88,17 @@ set_key :: proc(c: ^Config, key: string, value: string) -> bool {

// Sets the value of a given key (specifically for sections).
set_section :: proc(c: ^Config, key: string, value: ^Config) -> bool {
if c == nil || value == nil || has_key(c, key) do return false
if c == nil {
log.errorf("Provided ^Config is nil. Returning.")
return false
} else if has_key(c, key) {
log.errorf("Provided ^Config already contains the key '%s'. Returning.", key)
return false
} else if value == nil {
log.errorf("Provided section (^Config) is nil. Returning.")
return false
}

c.keys[key] = value
return true
}
Expand All @@ -82,14 +108,20 @@ get :: proc{get_key}

// Returns the value of a key in the config. Does not support returning sections.
get_key :: proc(c: ^Config, key: string) -> (string, bool) #optional_ok {
if c == nil do return "", false
if c == nil {
log.errorf("Provided ^Config is nil. Cannot get key '%s'.", key)
return "", false
}
return c.keys[key].value, true
}

// Finds a section by name and returns a pointer to it.
// If no section matches the name, it returns nil.
get_section :: proc(c: ^Config, name: string) -> (^Config, bool) #optional_ok {
if c == nil do return nil, false
if c == nil {
log.errorf("Provided ^Config is nil. Cannot get section '%s'. Returning nil.", name)
return nil, false
}
return c.keys[name], true
}

Expand All @@ -106,7 +138,10 @@ is_section :: proc(c: ^Config, name: string) -> bool {

// Removes a key from a config/section
remove :: proc(c: ^Config, name: string) -> bool {
if !has_key(c, name) do return false
if !has_key(c, name) {
log.warnf("Cannot remove key '%s' from config because '%s' does not exist.", name, name)
return false
}
value := c.keys[name]
destroy_config(value)
delete_key(&c.keys, name)
Expand All @@ -115,7 +150,10 @@ remove :: proc(c: ^Config, name: string) -> bool {

// Removes a key from a config/section and returns the value
pop_key :: proc(c: ^Config, name: string) -> (string, bool) #optional_ok {
if !has_key(c, name) do return "", false
if !has_key(c, name) {
log.errorf("Cannot pop key '%s' from config because '%s' does not exist.", name, name)
return "", false
}
value := c.keys[name]
defer free(value)
delete_key(&c.keys, name)
Expand All @@ -124,7 +162,10 @@ pop_key :: proc(c: ^Config, name: string) -> (string, bool) #optional_ok {

// Removes a key from a config/section
pop_section :: proc(c: ^Config, name: string) -> (^Config, bool) #optional_ok {
if !has_key(c, name) do return nil, false
if !has_key(c, name) {
log.errorf("Cannot pop section '%s' from config because '%s' does not exist.", name, name)
return nil, false
}
value := c.keys[name]
delete_key(&c.keys, name)
return value, true
Expand Down
74 changes: 51 additions & 23 deletions lexer.odin
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package ini

import "core:fmt"
import "core:unicode/utf8"
import "core:log"

// Lexer will take the INI text and convert it to a list of tokens.
// "key = value" = [Token{"key", .IDENTIFIER, ...}, Token{"=", .DELIMITER, ...}, Token{"value", .IDENTIFIER, ...}]
Lexer :: struct {
tokens: [dynamic]Token,
input: []rune,
Expand All @@ -11,7 +14,9 @@ Lexer :: struct {
col: int,
}

// Creates a new lexer with the given input and returns a pointer to it.
new_lexer :: proc(input: string) -> ^Lexer {
log.debugf("Created ^Lexer with given string of length %d", len(input))
l := new(Lexer)
l.tokens = make([dynamic]Token, 0, len(input)/2)
l.input = utf8.string_to_runes(input)
Expand All @@ -21,51 +26,74 @@ new_lexer :: proc(input: string) -> ^Lexer {
return l
}

// Produces the dynamic array of tokens from the input stored in the lexer struct
lex :: proc(l: ^Lexer) -> [dynamic]Token {
c: rune
c: rune // Current character

for l.pos < len(l.input) {

// Most symbols can be directly added, identifiers and comments are lexed in their own functions
switch c = next(l); c {
case '\n': append(&l.tokens, Token{.EOL, "\n", l.line, l.col})
case ';', '#': append(&l.tokens, Token{.COMMENT, lexId(l).value, l.line, l.col})
case ']': append(&l.tokens, Token{.RSB, "]", l.line, l.col})
case '[': append(&l.tokens, Token{.LSB, "[", l.line, l.col})
case '=', ':': append(&l.tokens, Token{.DELIMITER, "=", l.line, l.col})
case ' ', '\t', '\r': break
case: append(&l.tokens, lexId(l))
}
}

append(&l.tokens, Token{.EOF, "", l.line, l.col})
case Options.Symbols.Comment: // Defaults to ;
log.debug("Found comment")
append(&l.tokens, lexId(l))

return l.tokens
}
case Options.Symbols.SectionRight: // Defaults to ]
log.debug("Found section right")
s, i := utf8.encode_rune(Options.Symbols.SectionRight)
append(&l.tokens, Token{.SECTION_RIGHT, string(s[:i]), l.line, l.col})

lexId :: proc(l: ^Lexer) -> Token {
start := l.pos - 1
case Options.Symbols.SectionLeft: // Defaults to [
log.debug("Found section left")
s, i := utf8.encode_rune(Options.Symbols.SectionLeft)
append(&l.tokens, Token{.SECTION_LEFT, string(s[:i]), l.line, l.col})

for l.pos < len(l.input) {
c := next(l)
if c == 0 || c == '\n' || c == '[' || c == ']' || c == '=' {
back(l)
case Options.Symbols.Delimiter: // Defaults to =
log.debug("Found delimiter")
s, i := utf8.encode_rune(Options.Symbols.Delimiter)
append(&l.tokens, Token{.DELIMITER, string(s[:i]), l.line, l.col})

case '\n':
append(&l.tokens, Token{.EOL, "\n", l.line, l.col})

// Ignore whitespace
case ' ', '\t', '\r':
break

case:
append(&l.tokens, lexId(l))
}
}

return Token{.ID, utf8.runes_to_string(l.input[start:l.pos]), l.line, l.col}
append(&l.tokens, Token{.EOF, "", l.line, l.col})

return l.tokens
}

lexComment :: proc(l: ^Lexer) -> Token {
// Tokenises identifiers (anything that is not whitespace or in Options.Symbols)
// This also lexs comments btw
lexId :: proc(l: ^Lexer) -> Token {
start := l.pos - 1

for l.pos < len(l.input) {
c := next(l)
if c == 0 || c == '\n' {

if c == 0 ||
c == '\n' ||
c == Options.Symbols.Delimiter ||
c == Options.Symbols.SectionRight ||
c == Options.Symbols.SectionLeft ||
c == Options.Symbols.Delimiter ||
c == Options.Symbols.Comment {
back(l)
break
}
}

return Token{.ID, utf8.runes_to_string(l.input[start:l.pos]), l.line, l.col}
str := utf8.runes_to_string(l.input[start:l.pos])
log.debugf("Found identifier '%s' @%d:%d", str, l.line, l.col)
return Token{.IDENTIFIER, str, l.line, l.col}
}

back :: proc(l: ^Lexer) {
Expand Down
37 changes: 31 additions & 6 deletions options.odin
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ini

import "core:bufio"

// Ini is not standardised. Therefore I've given the programmer the ability to interchange symbols and rules.
// Ini is not standardised. Therefore I've let the user interchange symbols and rules that will apply during pasing/serialisation.
IniOptions :: struct {

// Symbols
Expand All @@ -26,21 +26,34 @@ IniOptions :: struct {
Rules: struct {
// If you are reading these rules and think there are more that could be added open an issue (https://github.com/hrszpuk/odin-ini)

// Parsing
AllowEmptyValues: bool, // Whether empty values are allowed or not (default: true)
AllowEmptySections: bool, // Whether empty sections are allowed or not (default: true)
AllowNestedSections: bool, // Whether nested sections are allowed or not (default: true)
AllowSpacesInKeys: bool, // Whether spaces are allowed to be a part of keys or not (default: false)
AllowSpacesInValues: bool, // Whether spaces are allowed to be a part of values or not (default: true)
AllowDuplicateKeys: bool, // Whether duplicate keys are ignored or not (default: false)
AllowDuplicateSections: bool, // Whether duplicate sections are ignored or not (default: false)

IgnoreCaseSensitivity: bool, // Whether case sensitivity is ignored or not (default: false)
IgnoreDuplicateKeys: bool, // Whether duplicate keys are ignored or not (default: false)
IgnoreDuplicateSections: bool, // Whether duplicate sections are ignored or not (default: false)
IgnoreDelimiterPadding: bool, // Whether delimiter padding is ignored or not (default: true)
IgnoreSectionNamePadding: bool, // Whether section name padding is ignored or not (default: true)
IgnoreValueQuotes: bool, // Whether the quotes around a value are counted as part of the value or not (default: true)
},

Debug: bool, // Debugging mode will print debug information (default: false)
IgnoreComments: bool, // Whether to ignore comments when parsing (default: false).

// Generation
PaddingSymbol: rune, // The symbol used for padding (default: ' ')
DelimiterPaddingAmount: uint, // The amount of padding around the delimiter (default: 1)
SectionNamePaddingAmount: uint, // The amount of padding around the delimiter (default: 1)
BeforeSectionLinePaddingAmount: uint, // The amount of lines placed before a section header (default: 1)
StatementCommentPaddingAmount: uint, // The amount of padding before a comment starts after a statement (default: 1)
EmptyLineCommentPaddingAmount: uint, // The amount of padding before a comment starts on an empty line (default: 0)

GenerateComments: bool, // Whether to generate comments or not (default: true)
GenerateCommentsJson: bool, // Whether to generate comments in json or not (default: false)
GenerateCommentsNameJson: string, // The name of the comment field if generating in json (default: "__comment__")
},
}

// These options can be changed at runtime.
Expand All @@ -59,6 +72,18 @@ Options : IniOptions = {
false,
true,
true,

false,

' ',
1,
0,
1,
1,
0,

true,
false,
"__comment__",
},
false, // Debug
}
Loading
Loading