///|
/// Parses INI text using default tolerant options.
pub fn parse_ini(name : String, text : String) -> ParseResult {
parse_ini_with_options(name, text, default_parse_options())
}
///|
/// Parses Java-style properties text using defaults close to INI mode.
pub fn parse_properties(name : String, text : String) -> ParseResult {
let options = default_parse_options()
parse_properties_with_options(name, text, options)
}
///|
/// Parses INI text with explicit options.
pub fn parse_ini_with_options(
name : String,
text : String,
options : ParseOptions,
) -> ParseResult {
parse_lines(name, split_lines(normalize_newlines(text)), options, false)
}
///|
/// Parses properties text with explicit options.
pub fn parse_properties_with_options(
name : String,
text : String,
options : ParseOptions,
) -> ParseResult {
parse_lines(
name,
join_property_continuations(split_lines(normalize_newlines(text))),
options,
true,
)
}
///|
fn parse_lines(
name : String,
lines : Array[String],
options : ParseOptions,
properties_mode : Bool,
) -> ParseResult {
let entries : Array[ConfigEntry] = []
let diagnostics : Array[Diagnostic] = []
let mut section = ""
let mut pending_comments : Array[String] = []
let seen : Array[String] = []
let mut line_number = 1
for raw_line in lines {
let trimmed = raw_line.trim()
if trimmed == "" {
if !options.preserve_comments {
pending_comments = []
}
} else if is_comment(trimmed.to_owned(), properties_mode) {
if options.preserve_comments {
pending_comments.push(strip_comment_marker(trimmed.to_owned()))
}
} else if !properties_mode && is_section_line(trimmed.to_owned()) {
let parsed = parse_section_name(trimmed.to_owned())
if parsed == "" && options.strict_sections {
diagnostics.push(
error(
"empty-section",
"empty section names are not allowed",
span=Some(span(name, line_number, 1)),
),
)
}
section = parsed
pending_comments = []
} else {
let content = if properties_mode {
raw_line.trim().to_owned()
} else {
raw_line
}
let sep = find_separator(
content,
options.allow_colon_separator,
properties_mode,
)
match sep {
None =>
if options.allow_empty_value {
let key = trimmed.to_owned()
add_entry(
entries, diagnostics, seen, section, key, "", name, line_number, pending_comments,
)
pending_comments = []
} else {
diagnostics.push(
error(
"missing-separator",
"expected '=' or ':' between key and value",
span=Some(span(name, line_number, 1)),
),
)
}
Some(index) => {
let value_start = if properties_mode {
properties_value_start(content, index)
} else {
index + 1
}
let raw_key = content[0:index].to_owned().trim().to_owned()
let key = if properties_mode {
unescape_properties_value(raw_key)
} else {
raw_key
}
let raw_value = content[value_start:content.length()]
.to_owned()
.trim()
.to_owned()
let value = if properties_mode {
unescape_properties_value(unquote_value(raw_value))
} else {
unquote_value(raw_value)
}
if key == "" {
diagnostics.push(
error(
"empty-key",
"configuration keys must not be empty",
span=Some(span(name, line_number, 1)),
),
)
} else {
add_entry(
entries, diagnostics, seen, section, key, value, name, line_number,
pending_comments,
)
}
pending_comments = []
}
}
}
line_number = line_number + 1
}
let document = { name, entries, diagnostics }
{ document, ok: diagnostics_ok(diagnostics) }
}
///|
fn add_entry(
entries : Array[ConfigEntry],
diagnostics : Array[Diagnostic],
seen : Array[String],
section : String,
key : String,
value : String,
source : String,
line_number : Int,
comments : Array[String],
) -> Unit {
let path = entry_path(section, key)
if contains_string(seen, path) {
diagnostics.push(
warning(
"duplicate-key",
"duplicate key '" + path + "' overrides an earlier value",
span=Some(span(source, line_number, 1)),
),
)
}
seen.push(path)
entries.push({
section,
key,
value,
source,
span: span(source, line_number, 1),
comments: comments.copy(),
})
}
///|
fn normalize_newlines(text : String) -> String {
text.replace(old="\r\n", new="\n").replace(old="\r", new="\n")
}
///|
fn split_lines(text : String) -> Array[String] {
text.split("\n").map(line => line.to_owned()).collect()
}
///|
fn is_comment(text : String, properties_mode : Bool) -> Bool {
text.has_prefix(";") ||
text.has_prefix("#") ||
(properties_mode && text.has_prefix("!"))
}
///|
fn strip_comment_marker(text : String) -> String {
if text.length() <= 1 {
""
} else {
text[1:text.length()].to_owned().trim().to_owned()
}
}
///|
fn is_section_line(text : String) -> Bool {
text.has_prefix("[") && text.has_suffix("]")
}
///|
fn parse_section_name(text : String) -> String {
if text.length() <= 2 {
""
} else {
text[1:text.length() - 1].to_owned().trim().to_owned()
}
}
///|
fn find_separator(
line : String,
allow_colon : Bool,
properties_mode : Bool,
) -> Int? {
let mut index = 0
let mut escaped = false
while index < line.length() {
let ch = line[index]
if escaped {
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == '=' ||
(allow_colon && ch == ':') ||
(properties_mode && is_properties_whitespace(ch)) {
return Some(index)
}
index = index + 1
}
None
}
///|
fn properties_value_start(line : String, separator : Int) -> Int {
let mut index = separator
while index < line.length() && is_properties_whitespace(line[index]) {
index = index + 1
}
if index < line.length() && (line[index] == '=' || line[index] == ':') {
index = index + 1
}
while index < line.length() && is_properties_whitespace(line[index]) {
index = index + 1
}
index
}
///|
fn is_properties_whitespace(ch : UInt16) -> Bool {
ch == ' ' || ch == '\t'
}
///|
fn unquote_value(value : String) -> String {
if value.length() >= 2 {
if (value.has_prefix("\"") && value.has_suffix("\"")) ||
(value.has_prefix("'") && value.has_suffix("'")) {
value[1:value.length() - 1].to_owned()
} else {
value
}
} else {
value
}
}
///|
fn contains_string(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
fn join_property_continuations(lines : Array[String]) -> Array[String] {
let joined : Array[String] = []
let mut current = ""
let mut active = false
for line in lines {
if active {
current = current + line.trim().to_owned()
} else {
current = line
active = true
}
if ends_with_unescaped_backslash(current) {
current = current[0:current.length() - 1].to_owned()
} else {
joined.push(current)
current = ""
active = false
}
}
if active {
joined.push(current)
}
joined
}
///|
fn ends_with_unescaped_backslash(text : String) -> Bool {
if text.length() == 0 || !text.has_suffix("\\") {
false
} else {
let mut count = 0
let mut index = text.length() - 1
while index >= 0 && text[index] == '\\' {
count = count + 1
index = index - 1
}
count % 2 == 1
}
}