From 60e0f5d9a033dd50beae8e71c7e72a98637e4f5d Mon Sep 17 00:00:00 2001 From: 0xda157 Date: Tue, 2 Jun 2026 14:37:23 -0700 Subject: [PATCH] support unicode escape codes --- CHANGELOG.md | 4 ++++ README.md | 8 -------- src/tom.gleam | 47 ++++++++++++++++++++++++++++++++++++++++++++- test/tom_test.gleam | 15 +++++++++++++++ 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84533ed..fda7ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Added support for `\x`, `\u` and `\U` escape codes + ## v2.1.0 - 2026-04-30 - Added parsing of TOML documents to `Dynamic`. diff --git a/README.md b/README.md index cca986a..3c5204a 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,3 @@ pub fn main() { ``` Further documentation can be found at . - -## Status - -The following string escape sequences are not supported yet: - -- `\xHH` -- `\uHHHH` -- `\UHHHHHHHH` diff --git a/src/tom.gleam b/src/tom.gleam index c6f6576..02672c7 100644 --- a/src/tom.gleam +++ b/src/tom.gleam @@ -631,7 +631,10 @@ fn merge( } } -fn expect_end_of_line(input: Tokens, next: fn(Tokens) -> Parsed(a)) -> Parsed(a) { +fn expect_end_of_line( + input: Tokens, + next: fn(Tokens) -> Parsed(a), +) -> Parsed(a) { case input { ["\n", ..input] -> next(input) ["\r\n", ..input] -> next(input) @@ -1113,6 +1116,21 @@ fn parse_string(input: Tokens, string: String) -> Parsed(Toml) { ["\\", "f", ..input] -> parse_string(input, string <> "\f") ["\\", "\"", ..input] -> parse_string(input, string <> "\"") ["\\", "\\", ..input] -> parse_string(input, string <> "\\") + ["\\", "x", ..input] -> { + let #(hex_codepoints, input) = list.split(input, 2) + use str <- result.try(hex_codepoint_to_string(hex_codepoints)) + parse_string(input, string <> str) + } + ["\\", "u", ..input] -> { + let #(hex_codepoints, input) = list.split(input, 4) + use str <- result.try(hex_codepoint_to_string(hex_codepoints)) + parse_string(input, string <> str) + } + ["\\", "U", ..input] -> { + let #(hex_codepoints, input) = list.split(input, 8) + use str <- result.try(hex_codepoint_to_string(hex_codepoints)) + parse_string(input, string <> str) + } [] -> Error(Unexpected("EOF", "\"")) ["\n", ..] -> Error(Unexpected("\n", "\"")) ["\r\n", ..] -> Error(Unexpected("\r\n", "\"")) @@ -1135,6 +1153,21 @@ fn parse_multi_line_string(input: Tokens, string: String) -> Parsed(Toml) { ["\\", "r", ..input] -> parse_multi_line_string(input, string <> "\r") ["\\", "\"", ..input] -> parse_multi_line_string(input, string <> "\"") ["\\", "\\", ..input] -> parse_multi_line_string(input, string <> "\\") + ["\\", "x", ..input] -> { + let #(hex_codepoints, input) = input |> skip_whitespace |> list.split(2) + use str <- result.try(hex_codepoint_to_string(hex_codepoints)) + parse_multi_line_string(input, string <> str) + } + ["\\", "u", ..input] -> { + let #(hex_codepoints, input) = input |> skip_whitespace |> list.split(4) + use str <- result.try(hex_codepoint_to_string(hex_codepoints)) + parse_multi_line_string(input, string <> str) + } + ["\\", "U", ..input] -> { + let #(hex_codepoints, input) = input |> skip_whitespace |> list.split(8) + use str <- result.try(hex_codepoint_to_string(hex_codepoints)) + parse_multi_line_string(input, string <> str) + } [] -> Error(Unexpected("EOF", "\"")) [g, ..input] -> parse_multi_line_string(input, string <> g) } @@ -1166,6 +1199,18 @@ fn parse_literal_string(input: Tokens, string: String) -> Parsed(Toml) { } } +fn hex_codepoint_to_string(input: Tokens) -> Result(String, ParseError) { + let hex_codepoints = string.concat(input) + + use codepoint <- result.map( + int.base_parse(hex_codepoints, 16) + |> result.try(string.utf_codepoint) + |> result.replace_error(Unexpected(hex_codepoints, "0 to F")), + ) + + string.from_utf_codepoints([codepoint]) +} + fn reverse_arrays_of_tables(toml: Toml) -> Toml { case toml { ArrayOfTables(tables) -> diff --git a/test/tom_test.gleam b/test/tom_test.gleam index b856649..e44ffc2 100644 --- a/test/tom_test.gleam +++ b/test/tom_test.gleam @@ -97,6 +97,21 @@ pub fn parse_string_linefeed_test() { assert tom.parse("hello = \"\\r\"\n") == Ok(expected) } +pub fn parse_string_unicode_x_escape_test() { + let expected = dict.from_list([#("hello", tom.String("\u{00E9}"))]) + assert tom.parse("hello = \"\\xE9\"\n") == Ok(expected) +} + +pub fn parse_string_unicode_escape_test() { + let expected = dict.from_list([#("hello", tom.String("\u{1111}"))]) + assert tom.parse("hello = \"\\u1111\"\n") == Ok(expected) +} + +pub fn parse_string_unicode_wide_escape_test() { + let expected = dict.from_list([#("hello", tom.String("🌟"))]) + assert tom.parse("hello = \"\\U0001F31F\"\n") == Ok(expected) +} + pub fn parse_escaped_slash_test() { let expected = dict.from_list([#("hello", tom.String("\\"))]) assert tom.parse("hello = \"\\\\\"\n") == Ok(expected)