///|
fn edit_problem(code : String, message : String) -> Diagnostic {
diagnostic(code, message, span(0, 0, 1, 1))
}
///|
fn editable(doc : Document) -> Bool {
!doc.diagnostics.any(d => d.severity == Error && d.code != "INI009")
}
///|
fn encode_quoted(value : String) -> String {
let out = StringBuilder()
out.write_string("\"")
for c in value.iter() {
match c {
'\\' => out.write_string("\\\\")
'"' => out.write_string("\\\"")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => out.write_char(c)
}
}
out.write_string("\"")
out.to_string()
}
///|
fn encode_value(value : String, previous : String, dialect : Dialect) -> String {
if dialect.multiline &&
previous.has_prefix("\"\"\"") &&
!value.contains("\"\"\"") &&
!value.has_suffix("\"") {
return "\"\"\"" + value + "\"\"\""
}
if previous.has_prefix("'") &&
!value.contains("'") &&
!value.contains("\n") &&
!value.contains("\r") {
return "'" + value + "'"
}
if value == "" {
return encode_quoted(value)
}
if previous.has_prefix("\"") {
return encode_quoted(value)
}
// Probe the actual selected dialect, not a divergent second set of quoting rules.
if !value.contains("\n") && !value.contains("\r") {
let probe = parse("x=" + value, dialect~)
if !probe.has_errors() &&
probe.get("", "x") == Some(value) &&
probe.nodes.length() == 1 {
return value
}
}
encode_quoted(value)
}
///|
/// Replace one duplicate occurrence (zero-based) without touching any surroundings.
/// Syntax errors block edits; duplicate diagnostics alone do not.
pub fn Document::set_at(
self : Document,
section : String,
key : String,
occurrence : Int,
value : String,
) -> Result[Document, Diagnostic] {
if !editable(self) {
return Err(edit_problem("EDIT001", "repair syntax errors before editing"))
}
let entries = self.entries(section, key)
if occurrence < 0 || occurrence >= entries.length() {
return Err(edit_problem("EDIT002", "occurrence does not exist"))
}
let node = entries[occurrence]
if node.value == value {
return Ok(self)
}
let old = text(self.source, node.value_span.start, node.value_span.end)
let mut token = encode_value(value, old, self.dialect)
// An empty bare value may share its insertion point with the comment marker.
// Add a delimiter, keeping every original comment character untouched.
if node.value_span.start == node.value_span.end &&
node.value_span.end < self.source.length() &&
self.dialect.inline_comments &&
is_comment(self.source[node.value_span.end], self.dialect) {
token += " "
}
let next_source = text(self.source, 0, node.value_span.start) +
token +
text(self.source, node.value_span.end, self.source.length())
let next = parse(next_source, dialect=self.dialect, limits=self.limits)
let updated = next.entries(section, key)
if !editable(next) ||
updated.length() != entries.length() ||
updated[occurrence].value != value {
return Err(
edit_problem("EDIT003", "replacement failed parse-back verification"),
)
}
Ok(next)
}
///|
/// Set the effective existing occurrence; Reject refuses ambiguous duplicates.
pub fn Document::set(
self : Document,
section : String,
key : String,
value : String,
) -> Result[Document, Diagnostic] {
let entries = self.entries(section, key)
if entries.is_empty() {
return self.insert(section, key, value)
}
if entries.length() > 1 && self.dialect.duplicates == Reject {
return Err(edit_problem("EDIT004", "ambiguous duplicate; use set_at"))
}
let index = if self.dialect.duplicates == LastWins {
entries.length() - 1
} else {
0
}
self.set_at(section, key, index, value)
}