import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import kdleam/error.{type KdlError} import kdleam/scanner.{type Scanner} @internal pub fn parse_string(s: Scanner) -> Result(#(String, Scanner), KdlError) { use maybe <- result.try(parse_string_if_available(s)) case maybe { #(Some(value), next) -> Ok(#(value, next)) #(None, next) -> case scanner.peek(next) { Some(ch) -> Error(scanner.make_error(next, error.UnexpectedChar(ch))) None -> Error(scanner.make_error(next, error.UnexpectedEof)) } } } @internal pub fn parse_string_if_available( s: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { use quoted <- result.try(parse_quoted_string(s)) case quoted { #(Some(_), _) -> Ok(quoted) #(None, _) -> { use raw <- result.try(parse_raw_string(s)) case raw { #(Some(_), _) -> Ok(raw) #(None, _) -> parse_identifier_string(s) } } } } @internal pub fn parse_identifier_string( s: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.peek(s) { None -> Ok(#(None, s)) Some(ch) -> case scanner.is_identifier_char(ch) { False -> Ok(#(None, s)) True -> { let is_sign = ch == "+" || ch == "-" let is_dot = ch == "." let is_digit = scanner.is_ascii_digit(ch) case is_digit { True -> Ok(#(None, s)) False -> case is_sign || is_dot { False -> collect_identifier(s) True -> collect_signed_or_dotted(s, ch, is_sign, is_dot) } } } } } } fn collect_signed_or_dotted( s: Scanner, first: String, is_sign: Bool, is_dot: Bool, ) -> Result(#(Option(String), Scanner), KdlError) { let #(_, s1) = scanner.advance(s) case is_sign, scanner.peek(s1) { True, Some(ch2) -> case scanner.is_identifier_char(ch2) && !scanner.is_ascii_digit(ch2) && ch2 != "." { True -> collect_identifier_continue(s1, [first]) False -> case ch2 == "." { True -> { let #(_, s2) = scanner.advance(s1) collect_identifier_continue(s2, [".", first]) } False -> check_keyword(first, s, s1) } } True, _ -> check_keyword(first, s, s1) False, _ -> case is_dot { True -> case scanner.peek(s1) { Some(ch2) -> case scanner.is_identifier_char(ch2) && !scanner.is_ascii_digit(ch2) { True -> collect_identifier_continue(s1, [first]) False -> check_keyword(first, s, s1) } None -> check_keyword(first, s, s1) } False -> Ok(#(None, s)) } } } fn collect_identifier( s: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { collect_identifier_continue(s, []) } fn collect_identifier_continue( s: Scanner, rev_prefix: List(String), ) -> Result(#(Option(String), Scanner), KdlError) { let #(chars, next) = collect_chars(s, rev_prefix) let value = chars |> list.reverse |> scanner.join case value == "" { True -> Ok(#(None, s)) False -> check_keyword(value, s, next) } } fn collect_chars(s: Scanner, acc: List(String)) -> #(List(String), Scanner) { case scanner.peek(s) { Some(ch) -> case scanner.is_identifier_char(ch) { True -> { let #(_, next) = scanner.advance(s) collect_chars(next, [ch, ..acc]) } False -> #(acc, s) } _ -> #(acc, s) } } fn check_keyword( value: String, start: Scanner, next: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.is_reserved_keyword(value) { True -> Error(scanner.make_error(start, error.BareKeyword)) False -> Ok(#(Some(value), next)) } } @internal pub fn parse_quoted_string( s: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.peek(s) { Some("\"") -> { let #(_, s1) = scanner.advance(s) case scanner.peek(s1), scanner.peek_next(s1) { Some("\""), Some("\"") -> { let #(_, s2) = scanner.advance(s1) let #(_, s3) = scanner.advance(s2) parse_multiline_quoted(s3) } _, _ -> parse_quoted_body(s1, []) } } _ -> Ok(#(None, s)) } } fn parse_quoted_body( s: Scanner, acc: List(String), ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.peek(s) { None -> Error(scanner.make_error(s, error.UnclosedString)) Some("\"") -> { let #(_, next) = scanner.advance(s) Ok(#(Some(acc |> list.reverse |> scanner.join), next)) } Some("\\") -> { let #(_, s1) = scanner.advance(s) use result <- result.try(parse_escape(s1)) let #(value, s2) = result parse_quoted_body(s2, [value, ..acc]) } Some(ch) -> case scanner.is_newline(ch) { True -> Error(scanner.make_error(s, error.UnclosedString)) False -> case scanner.is_disallowed(ch) { True -> Error(scanner.make_error(s, error.DisallowedCodePoint(ch))) False -> { let #(_, next) = scanner.advance(s) parse_quoted_body(next, [ch, ..acc]) } } } } } fn parse_escape(s: Scanner) -> Result(#(String, Scanner), KdlError) { case scanner.peek(s) { Some("n") -> esc("\n", s) Some("r") -> esc("\r", s) Some("t") -> esc("\t", s) Some("b") -> esc("\u{0008}", s) Some("f") -> esc("\u{000C}", s) Some("\\") -> esc("\\", s) Some("\"") -> esc("\"", s) Some("s") -> esc(" ", s) Some("u") -> parse_unicode_escape(s) Some(ch) -> case scanner.is_unicode_space(ch) || scanner.is_newline(ch) { True -> { Ok(#("", consume_whitespace_escape(s))) } False -> Error(scanner.make_error(s, error.InvalidEscape)) } None -> Error(scanner.make_error(s, error.UnclosedString)) } } fn esc(value: String, s: Scanner) -> Result(#(String, Scanner), KdlError) { let #(_, next) = scanner.advance(s) Ok(#(value, next)) } fn parse_unicode_escape(s: Scanner) -> Result(#(String, Scanner), KdlError) { let #(_, s1) = scanner.advance(s) use s2 <- result.try(scanner.expect(s1, "{")) let #(hex, s3) = collect_until(s2, "}", []) use s4 <- result.try(scanner.expect(s3, "}")) case valid_unicode_hex(hex) { False -> Error(scanner.make_error(s, error.InvalidUnicodeEscape)) True -> case int.base_parse(hex, 16) { Ok(i) -> case string.utf_codepoint(i) { Ok(cp) -> Ok(#(string.from_utf_codepoints([cp]), s4)) Error(_) -> Error(scanner.make_error(s, error.InvalidUnicodeEscape)) } Error(_) -> Error(scanner.make_error(s, error.InvalidUnicodeEscape)) } } } fn valid_unicode_hex(hex: String) -> Bool { let chars = string.to_graphemes(hex) let len = list.length(chars) len > 0 && len <= 6 && list.all(chars, scanner.is_hex_digit) } fn collect_until( s: Scanner, delimiter: String, acc: List(String), ) -> #(String, Scanner) { case scanner.peek(s) { Some(ch) -> case ch != delimiter { True -> { let #(_, next) = scanner.advance(s) collect_until(next, delimiter, [ch, ..acc]) } False -> #(acc |> list.reverse |> scanner.join, s) } _ -> #(acc |> list.reverse |> scanner.join, s) } } fn parse_multiline_quoted( s: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { let #(had_newline, s1) = scanner.consume_newline(s) case had_newline { False -> Error(scanner.make_error(s, error.UnclosedString)) True -> parse_multiline_quoted_lines(s1, [], "") } } fn parse_multiline_quoted_lines( s: Scanner, rev_lines: List(String), current_line: String, ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.peek(s), scanner.peek_next(s), scanner.at(s.chars, s.pos + 2) { Some("\""), Some("\""), Some("\"") -> { let #(_, s1) = scanner.advance(s) let #(_, s2) = scanner.advance(s1) let #(_, s3) = scanner.advance(s2) use value <- result.try(process_multiline( [current_line, ..rev_lines] |> list.reverse, scanner.Quoted, s, )) Ok(#(Some(value), s3)) } Some("\\"), _, _ -> { let #(_, s1) = scanner.advance(s) case scanner.peek(s1) { Some(ch) -> { case scanner.is_unicode_space(ch) || scanner.is_newline(ch) { True -> parse_multiline_quoted_lines( consume_whitespace_escape(s1), rev_lines, current_line, ) False -> { let #(_, s2) = scanner.advance(s1) parse_multiline_quoted_lines( s2, rev_lines, current_line <> "\\" <> ch, ) } } } None -> Error(scanner.make_error(s1, error.InvalidEscape)) } } None, _, _ -> Error(scanner.make_error(s, error.UnclosedString)) Some(ch), _, _ -> { case scanner.is_newline(ch) { True -> { let #(_, next) = scanner.consume_newline(s) parse_multiline_quoted_lines(next, [current_line, ..rev_lines], "") } False -> case scanner.is_disallowed(ch) { True -> Error(scanner.make_error(s, error.DisallowedCodePoint(ch))) False -> { let #(_, next) = scanner.advance(s) parse_multiline_quoted_lines(next, rev_lines, current_line <> ch) } } } } } } @internal pub fn parse_raw_string( s: Scanner, ) -> Result(#(Option(String), Scanner), KdlError) { let #(hashes, after_hashes) = collect_hashes(s, []) case scanner.peek(after_hashes), scanner.peek_next(after_hashes), scanner.at(after_hashes.chars, after_hashes.pos + 2) { Some("\""), Some("\""), Some("\"") -> { let #(_, s1) = scanner.advance(after_hashes) let #(_, s2) = scanner.advance(s1) let #(_, s3) = scanner.advance(s2) parse_multiline_raw_lines(s3, hashes, [], "") } Some("\""), _, _ -> { let #(_, s1) = scanner.advance(after_hashes) parse_raw_body(s1, hashes, scanner.SingleLine, []) } _, _, _ -> Ok(#(None, s)) } } fn parse_multiline_raw_lines( s: Scanner, hashes: List(String), rev_lines: List(String), current_line: String, ) -> Result(#(Option(String), Scanner), KdlError) { let #(had_newline, s1) = scanner.consume_newline(s) case had_newline { False -> Error(scanner.make_error(s, error.UnclosedString)) True -> parse_multiline_raw_body_lines(s1, hashes, rev_lines, current_line) } } fn parse_multiline_raw_body_lines( s: Scanner, hashes: List(String), rev_lines: List(String), current_line: String, ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.peek(s) { None -> Error(scanner.make_error(s, error.UnclosedString)) Some("\"") -> case raw_closer_at(s, hashes, scanner.Multiline) { True -> { let next = consume_raw_closer(s, hashes, scanner.Multiline) use value <- result.try(process_multiline( [current_line, ..rev_lines] |> list.reverse, scanner.Raw, s, )) Ok(#(Some(value), next)) } False -> { let #(_, next) = scanner.advance(s) parse_multiline_raw_body_lines( next, hashes, rev_lines, current_line <> "\"", ) } } Some(ch) -> case scanner.is_newline(ch) { True -> { let #(_, next) = scanner.consume_newline(s) parse_multiline_raw_body_lines( next, hashes, [current_line, ..rev_lines], "", ) } False -> case scanner.is_disallowed(ch) { True -> Error(scanner.make_error(s, error.DisallowedCodePoint(ch))) False -> { let #(_, next) = scanner.advance(s) parse_multiline_raw_body_lines( next, hashes, rev_lines, current_line <> ch, ) } } } } } fn collect_hashes(s: Scanner, acc: List(String)) -> #(List(String), Scanner) { case scanner.peek(s) { Some("#") -> { let #(_, next) = scanner.advance(s) collect_hashes(next, ["#", ..acc]) } _ -> #(list.reverse(acc), s) } } fn parse_raw_body( s: Scanner, hashes: List(String), form: scanner.LineForm, acc: List(String), ) -> Result(#(Option(String), Scanner), KdlError) { case scanner.peek(s) { None -> Error(scanner.make_error(s, error.UnclosedString)) Some("\"") -> case raw_closer_at(s, hashes, form) { True -> { let next = consume_raw_closer(s, hashes, form) let value = acc |> list.reverse |> scanner.join Ok(#(Some(value), next)) } False -> { let #(_, next) = scanner.advance(s) parse_raw_body(next, hashes, form, ["\"", ..acc]) } } Some(ch) -> { case form == scanner.SingleLine && scanner.is_newline(ch) { True -> Error(scanner.make_error(s, error.UnclosedString)) False -> { let #(_, next) = scanner.advance(s) parse_raw_body(next, hashes, form, [ch, ..acc]) } } } } } fn raw_closer_at( s: Scanner, hashes: List(String), form: scanner.LineForm, ) -> Bool { let offset = case form { scanner.Multiline -> 3 scanner.SingleLine -> 1 } case form { scanner.Multiline -> scanner.peek(s) == Some("\"") && scanner.peek_next(s) == Some("\"") && scanner.at(s.chars, s.pos + 2) == Some("\"") && hashes_match(s, s.pos + offset, hashes) scanner.SingleLine -> hashes_match(s, s.pos + offset, hashes) } } fn hashes_match(s: Scanner, pos: Int, hashes: List(String)) -> Bool { case hashes { [] -> True ["#", ..rest] -> scanner.at(s.chars, pos) == Some("#") && hashes_match(s, pos + 1, rest) _ -> False } } fn consume_raw_closer( s: Scanner, hashes: List(String), form: scanner.LineForm, ) -> Scanner { let count = list.length(hashes) + case form { scanner.Multiline -> 3 scanner.SingleLine -> 1 } consume_n(s, count) } fn consume_n(s: Scanner, n: Int) -> Scanner { case n <= 0 { True -> s False -> { let #(_, next) = scanner.advance(s) consume_n(next, n - 1) } } } fn consume_whitespace_escape(s: Scanner) -> Scanner { case scanner.peek(s) { Some(ch) -> case scanner.is_unicode_space(ch) { True -> { let #(_, next) = scanner.advance(s) consume_whitespace_escape(next) } False -> case scanner.is_newline(ch) { True -> { let #(_, next) = scanner.consume_newline(s) consume_whitespace_escape(next) } False -> s } } _ -> s } } fn process_multiline( lines: List(String), kind: scanner.StringKind, error_at: Scanner, ) -> Result(String, KdlError) { case last_line(lines, []) { #(None, _) -> Ok("") #(Some(prefix), body_lines) -> process_multiline_lines(body_lines, prefix, kind, error_at, True, "") } } fn last_line( lines: List(String), rev_body: List(String), ) -> #(Option(String), List(String)) { case lines { [] -> #(None, list.reverse(rev_body)) [last] -> #(Some(last), list.reverse(rev_body)) [line, ..rest] -> last_line(rest, [line, ..rev_body]) } } fn process_multiline_lines( lines: List(String), prefix: String, kind: scanner.StringKind, error_at: Scanner, first: Bool, acc: String, ) -> Result(String, KdlError) { case lines { [] -> Ok(acc) [line, ..rest] -> { let next_acc = case first { True -> acc False -> acc <> "\n" } case line_is_unicode_space(line) { True -> process_multiline_lines(rest, prefix, kind, error_at, False, next_acc) False -> case string.starts_with(line, prefix) { False -> Error(scanner.make_error(error_at, error.InconsistentIndentation)) True -> { let stripped = case string.split_once(line, prefix) { Ok(#("", rest)) -> rest _ -> line } use resolved <- result.try(case kind { scanner.Quoted -> resolve_escapes(stripped, error_at) scanner.Raw -> Ok(stripped) }) process_multiline_lines( rest, prefix, kind, error_at, False, next_acc <> resolved, ) } } } } } } fn line_is_unicode_space(line: String) -> Bool { line |> string.to_graphemes |> list.all(scanner.is_unicode_space) } fn resolve_escapes( input: String, error_at: Scanner, ) -> Result(String, KdlError) { resolve_escapes_loop(scanner.new(input), error_at, "") } fn resolve_escapes_loop( s: Scanner, error_at: Scanner, acc: String, ) -> Result(String, KdlError) { case scanner.peek(s) { None -> Ok(acc) Some("\\") -> { let #(_, s1) = scanner.advance(s) use parsed <- result.try(parse_escape(s1)) let #(value, s2) = parsed resolve_escapes_loop(s2, error_at, acc <> value) } Some(ch) -> { case scanner.is_disallowed(ch) { True -> Error(scanner.make_error(error_at, error.DisallowedCodePoint(ch))) False -> { let #(_, next) = scanner.advance(s) resolve_escapes_loop(next, error_at, acc <> ch) } } } } }