Current section
Files
Jump to
Current section
Files
src/kdleam/string.gleam
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)
}
}
}
}
}