primitive IniIncompleteSection
"""
An `IniError` reported when a line that begins with `[` has no closing `]`.
"""
primitive IniNoDelimiter
"""
An `IniError` reported when a line that is not a comment or section header
contains neither `=` nor `:`.
"""
type IniError is
( IniIncompleteSection
| IniNoDelimiter
)
"""
The set of errors `Ini` can report while parsing.
"""
interface IniNotify
"""
Handler interface for the streaming parser `Ini`. The parser calls these
methods as it works through the input. Only `apply` must be implemented;
`add_section` and `errors` ship default implementations that accept all
sections and continue past errors.
"""
fun ref apply(section: String, key: String, value: String): Bool
"""
Called for every key/value pair. Keys that appear before any section
header are reported with `section` set to the empty string. Return
`false` to halt processing.
"""
fun ref add_section(section: String): Bool =>
"""
Called for every `[section]` header. The implicit empty-string section
that holds keys appearing before any header does not trigger this
callback; only a literal header line (including `[]`) does. Return
`false` to halt processing.
"""
true
fun ref errors(line: USize, err: IniError): Bool =>
"""
Called for each malformed line. `line` is the 1-based line number where
the error was detected. Return `false` to halt parsing immediately;
`true` to keep going. Even when this returns `true` and parsing
continues, `Ini.apply` will return `false` at the end.
"""
true
primitive Ini
"""
A streaming parser for INI formatted lines of text.
Lines are pulled from the iterator one at a time and dispatched to an
`IniNotify` handler as section headers, key/value pairs, or errors. The
current section name is the only piece of parsed input that persists
across handler calls; everything else (the line number reported with
errors, the running success/failure status returned at the end) is
internal bookkeeping the handler does not see.
"""
fun apply(lines: Iterator[String box], f: IniNotify): Bool =>
"""
Parse `lines` and call `f` for each section header and key/value pair.
Returns `true` when parsing finishes with no errors, `false` otherwise.
When the parser hits a malformed line it calls `f.errors`. If `errors`
returns `true` parsing continues, but the final return value is still
`false`. If `errors` returns `false` parsing stops immediately and
`apply` returns `false`.
`f.apply` and `f.add_section` can also stop parsing early by returning
`false`. In that case `apply` returns whatever the parse status was at
the point of the stop: `true` if no errors had been seen, `false` if any
had.
"""
var section = ""
var lineno = USize(0)
var ok = true
for line in lines do
lineno = lineno + 1
var current = line.clone()
current.strip()
if current.size() == 0 then
continue
end
try
match current(0)?
| ';' | '#' =>
// Skip comments.
continue
| '[' =>
try
current.delete(current.find("]", 1)?, -1)
current.delete(0)
current.strip()
section = consume current
if not f.add_section(section) then
return ok
end
else
ok = false
if not f.errors(lineno, IniIncompleteSection) then
return false
end
end
else
try
let delim =
try
current.find("=")?
else
current.find(":")?
end
let value = current.substring(delim + 1)
value.strip()
current.delete(delim, -1)
current.strip()
try
let comment =
try
value.find(";")?
else
value.find("#")?
end
match value(comment.usize() - 1)?
| ' ' | '\t' =>
value.delete(comment, -1)
value.rstrip()
end
end
if not f(section, consume current, consume value) then
return ok
end
else
ok = false
if not f.errors(lineno, IniNoDelimiter) then
return false
end
end
end
end
end
ok