///|
pub fn parse(
source : String,
dialect? : Dialect = standard(),
limits? : ParseLimits = default_limits(),
) -> Document {
match budget_diagnostic(source, limits) {
Some(d) => return { source, dialect, limits, nodes: [], diagnostics: [d] }
None => ()
}
let lines = scan_lines(source)
if lines.length() > limits.max_lines {
let d = diagnostic(
"LIMIT003",
"source exceeds physical line budget",
span(0, source.length(), 1, 1),
)
return { source, dialect, limits, nodes: [], diagnostics: [d] }
}
let nodes : Array[Node] = []
let diagnostics : Array[Diagnostic] = []
let mut section = ""
let mut consumed_until = 0
for line in lines {
if line.start < consumed_until {
continue
}
let (a, b) = trim_bounds(source, line.start, line.content_end)
let full = line_span(line, line.start, line.end)
let empty = line_span(line, a, a)
let mut node : Node = {
kind: Invalid,
span: full,
section,
key: "",
value: "",
key_span: empty,
value_span: empty,
}
if a == b {
node = { ..node, kind: Blank }
} else if is_comment(source[a], dialect) {
node = { ..node, kind: Comment }
} else if source[a] == '[' {
let mut close = a + 1
while close < b && source[close] != ']' {
close += 1
}
if close == b {
diagnostics.push(
diagnostic("INI001", "unclosed section header", line_span(line, a, b)),
)
} else {
let (sa, sb) = trim_bounds(source, a + 1, close)
let (tail, _) = trim_bounds(source, close + 1, b)
if sa == sb {
diagnostics.push(
diagnostic("INI002", "empty section name", line_span(line, sa, sb)),
)
} else if tail < b && !is_comment(source[tail], dialect) {
diagnostics.push(
diagnostic(
"INI003",
"unexpected text after section",
line_span(line, tail, b),
),
)
} else {
section = text(source, sa, sb)
node = {
..node,
kind: Section,
section,
key_span: line_span(line, sa, sb),
}
}
}
} else {
let mut sep = a
while sep < b &&
source[sep] != '=' &&
!(dialect.allow_colon && source[sep] == ':') {
sep += 1
}
if sep == b {
diagnostics.push(
diagnostic(
"INI004",
"expected key/value separator",
line_span(line, a, b),
),
)
} else {
let (ka, kb) = trim_bounds(source, a, sep)
if ka == kb {
diagnostics.push(
diagnostic("INI005", "empty key", line_span(line, ka, kb)),
)
} else {
let (va, vb) = trim_bounds(source, sep + 1, b)
let key_span = line_span(line, ka, kb)
let (value, value_span, node_end) = if dialect.multiline &&
triple_at(source, va, vb) {
decode_multiline(source, line, va, dialect, diagnostics)
} else {
let (v, s) = decode_value(
source, line, va, vb, dialect, diagnostics,
)
(v, s, line.end)
}
consumed_until = node_end
node = {
..node,
kind: Entry,
span: { ..full, end: node_end },
key: text(source, ka, kb),
value,
key_span,
value_span,
}
}
}
}
nodes.push(node)
}
for d in duplicate_diagnostics(nodes, dialect) {
diagnostics.push(d)
}
diagnostics.sort_by((a, b) => a.span.start.compare(b.span.start))
{ source, dialect, limits, nodes, diagnostics }
}
///|
pub fn parse_with_diagnostics(
source : String,
dialect? : Dialect = standard(),
limits? : ParseLimits = default_limits(),
) -> (Document, Array[Diagnostic]) {
let doc = parse(source, dialect~, limits~)
(doc, doc.diagnostics())
}
///|
fn is_comment(c : UInt16, dialect : Dialect) -> Bool {
c == ';' || (dialect.hash_comments && c == '#')
}