///|
priv struct Parser {
chars : Array[Char]
options : ParseOptions
mut pos : Int
mut line : Int
mut column : Int
mut entry_count : Int
allow_directives : Bool
capture : Bool
path : Array[Int]
spans : Array[SourceSpan]
tracing : Bool
trace_stack : Array[Array[TraceNode]]
mut last_trace : Array[TraceNode]
}
///|
/// Parse the documented KV1 text subset; unsupported file-loading directives
/// and platform conditionals are rejected rather than silently misinterpreted.
pub fn parse(
input : String,
options? : ParseOptions = ParseOptions::default(),
) -> Document raise ParseError {
parse_internal(input, options, false).0
}
///|
fn parse_internal(
input : String,
options : ParseOptions,
allow_directives : Bool,
) -> (Document, Int) raise ParseError {
let (doc, count, _) = parse_with_spans(
input, options, allow_directives, false,
)
(doc, count)
}
///|
fn parse_with_spans(
input : String,
options : ParseOptions,
allow_directives : Bool,
capture : Bool,
trace? : Array[TraceNode]? = None,
) -> (Document, Int, Array[SourceSpan]) raise ParseError {
if options.max_depth < 0 ||
options.max_depth > 256 ||
options.max_entries < 0 ||
options.max_input_chars < 0 ||
options.max_token_chars < 0 {
raise ParseError({
code: "invalid_options",
message: "Limits must be nonnegative; max_depth must be at most 256",
offset: 0,
line: 1,
column: 1,
})
}
if input.char_length() > options.max_input_chars {
raise ParseError({
code: "input_limit",
message: "Input exceeds max_input_chars",
offset: 0,
line: 1,
column: 1,
})
}
let p = Parser::{
chars: input.to_array(),
options,
pos: 0,
line: 1,
column: 1,
entry_count: 0,
allow_directives,
capture,
path: [],
spans: [],
tracing: trace is Some(_),
trace_stack: [],
last_trace: [],
}
if p.chars.get(0) is Some(c) && c.to_int() == 0xFEFF {
p.advance()
}
let doc = Document::{ entries: p.read_entries(0, false), }
if trace is Some(output) {
for node in p.last_trace {
output.push(node)
}
}
(doc, p.entry_count, p.spans)
}
///|
fn Parser::fail(
self : Parser,
code : String,
message : String,
) -> Unit raise ParseError {
raise ParseError({
code,
message,
offset: self.pos,
line: self.line,
column: self.column,
})
}
///|
fn Parser::peek(self : Parser) -> Char? {
self.chars.get(self.pos)
}
///|
fn Parser::advance(self : Parser) -> Unit {
guard self.pos < self.chars.length() else { return }
let c = self.chars[self.pos]
self.pos = self.pos + 1
if c == '\r' {
self.line = self.line + 1
self.column = 1
} else if c == '\n' {
if self.pos < 2 || self.chars[self.pos - 2] != '\r' {
self.line = self.line + 1
}
self.column = 1
} else {
self.column = self.column + 1
}
}
///|
fn is_space(c : Char) -> Bool {
c == ' ' ||
c == '\t' ||
c == '\r' ||
c == '\n' ||
c.to_int() == 0x0B ||
c.to_int() == 0x0C
}
///|
fn Parser::skip_trivia(self : Parser) -> Unit raise ParseError {
while self.pos < self.chars.length() {
let c = self.chars[self.pos]
if c.to_int() == 0 {
self.fail("nul_character", "NUL characters are not valid KV1 text")
} else if is_space(c) {
self.advance()
} else if c == '/' && self.chars.get(self.pos + 1) == Some('/') {
while self.peek() is Some(ch) && ch != '\r' && ch != '\n' {
self.advance()
}
} else {
break
}
}
}
///|
fn Parser::read_string(self : Parser) -> String raise ParseError {
self.skip_trivia()
guard self.peek() is Some(first) else {
self.fail("expected_string", "Expected a key or string value")
return ""
}
if first == '{' || first == '}' {
self.fail("expected_string", "Expected a string, found an object delimiter")
}
let quoted = first == '"'
if quoted {
self.advance()
}
let builder = StringBuilder()
let mut count = 0
while self.peek() is Some(c) {
if quoted && c == '"' {
self.advance()
return builder.to_string()
}
if !quoted && (is_space(c) || c == '{' || c == '}' || c == '=') {
return builder.to_string()
}
if c.to_int() == 0 {
self.fail("nul_character", "NUL characters are not valid KV1 text")
}
if !quoted && c == '"' {
self.fail(
"unexpected_quote", "A quoted string must begin at a token boundary",
)
}
if count >= self.options.max_token_chars {
self.fail("token_limit", "String exceeds max_token_chars")
}
if quoted && c == '\\' && self.options.escape_sequences {
self.advance()
guard self.peek() is Some(escaped) else {
self.fail("unterminated_string", "Backslash at end of quoted string")
return ""
}
match escaped {
'n' => builder.write_char('\n')
'r' => builder.write_char('\r')
't' => builder.write_char('\t')
'\\' => builder.write_char('\\')
'"' => builder.write_char('"')
_ =>
self.fail(
"unknown_escape", "Unknown escape; use literal mode for unescaped paths",
)
}
self.advance()
} else {
builder.write_char(c)
self.advance()
}
count = count + 1
}
if quoted {
self.fail(
"unterminated_string", "Quoted string is missing its closing quote",
)
}
builder.to_string()
}
///|
fn Parser::read_entries(
self : Parser,
depth : Int,
nested : Bool,
) -> Array[Entry] raise ParseError {
let entries : Array[Entry] = []
let traces : Array[TraceNode] = []
if self.tracing {
self.trace_stack.push(traces)
}
while true {
self.skip_trivia()
match self.peek() {
None => {
if nested {
self.fail("unclosed_object", "Object is missing its closing brace")
}
break
}
Some('}') => {
if !nested {
self.fail("unexpected_close", "Closing brace has no matching object")
}
self.advance()
break
}
Some('[') =>
self.fail(
"unsupported_conditional", "Platform conditionals are not supported",
)
_ => ()
}
if self.entry_count >= self.options.max_entries {
self.fail("entry_limit", "Document exceeds max_entries")
}
let key_start = self.pos
let key_line = self.line
let key_column = self.column
let key = self.read_string()
let key_end = self.pos
if key.is_empty() && self.peek() == Some('=') {
self.fail("expected_string", "Unexpected equals sign")
}
if depth == 0 &&
!self.allow_directives &&
(
key.equal_ignore_ascii_case("#include") ||
key.equal_ignore_ascii_case("#base")
) {
self.fail(
"unsupported_directive", "Include/base directives require an explicit file resolver",
)
}
self.entry_count = self.entry_count + 1
self.skip_trivia()
let mut keep = true
while self.peek() == Some('[') {
keep = self.read_condition()
self.skip_trivia()
}
let mut override_previous = false
if self.peek() == Some('=') {
self.advance()
self.skip_trivia()
while self.peek() == Some('[') {
keep = self.read_condition()
override_previous = true
self.skip_trivia()
}
}
let value_start = self.pos
if self.capture {
self.path.push(entries.length())
}
let value = match self.peek() {
Some('{') => {
if depth >= self.options.max_depth {
self.fail("depth_limit", "Document exceeds max_depth")
}
self.advance()
Value::Object(self.read_entries(depth + 1, true))
}
None | Some('}') => {
self.fail("missing_value", "Key has no value")
Value::Text("")
}
_ => Value::Text(self.read_string())
}
let value_end = self.pos
let children = if value is Object(_) && self.tracing {
self.last_trace
} else {
[]
}
let mut entry_end = value_end
self.skip_trivia()
while self.peek() == Some('[') {
keep = self.read_condition()
entry_end = self.pos
self.skip_trivia()
}
if self.capture {
self.spans.push({
path: self.path.map(i => i),
key_start,
key_end,
value_start,
value_end,
entry_end,
})
self.path.pop() |> ignore
}
if keep {
if override_previous && !self.capture {
let mut index = 0
while index < entries.length() {
if entries[index].key.equal_ignore_ascii_case(key) {
entries.remove(index) |> ignore
if self.tracing {
traces.remove(index) |> ignore
}
} else {
index += 1
}
}
}
entries.push({ key, value, })
if self.tracing {
traces.push({
offset: key_start,
line: key_line,
column: key_column,
children,
})
}
}
}
if self.tracing {
self.last_trace = self.trace_stack.pop().unwrap()
}
entries
}
///|
fn Parser::read_condition(self : Parser) -> Bool raise ParseError {
let symbols = match self.options.conditions {
Some(symbols) => symbols
None if self.capture => []
None => {
self.fail(
"unsupported_conditional", "Pass an explicit conditions array to evaluate platform conditions",
)
return false
}
}
self.advance()
let text = StringBuilder()
let mut count = 0
while self.peek() is Some(c) && c != ']' {
if count >= 4096 {
self.fail("condition_limit", "Conditional exceeds 4096 characters")
}
text.write_char(c)
count += 1
self.advance()
}
if self.peek() != Some(']') {
self.fail("invalid_condition", "Missing closing bracket")
}
self.advance()
let result = evaluate_condition(text.to_string(), symbols) catch {
InvalidCondition(message) => {
self.fail("invalid_condition", message)
false
}
}
if self.capture {
true
} else {
result
}
}