import gleam/list import gleam_community/ansi import houdini import splitter.{type Splitter} /// HighlightTokens stringify, classify and flatten TOML tokens into groups. pub type HighlightToken { HighlightError(str: String) HighlightKey(str: String) HighlightTable(str: String) HighlightString(str: String) HighlightNumber(str: String) HighlightDateTime(str: String) HighlightLiteral(str: String) HighlightOperator(str: String) HighlightPunctuation(str: String) HighlightComment(str: String) HighlightWhitespace(str: String) } type HighlightContext { Normal TableHeader } /// Parse TOML source code into a list of `HighlightToken` using default /// settings. /// /// The resulting token list can be highlighted by using functions like /// `to_ansi`, `to_html`, or by pattern matching on it yourself. pub fn highlight(source: String) -> List(HighlightToken) { to_highlight(tokenise(new(), source)) } /// Convert a list of lexer tokens into a list of `HighlightToken`. pub fn to_highlight(tokens: List(Token)) -> List(HighlightToken) { let tokens = highlight_loop(tokens, Normal, []) list.reverse(tokens) } fn highlight_loop( tokens: List(Token), context: HighlightContext, acc: List(HighlightToken), ) -> List(HighlightToken) { case tokens { [] -> acc [token, ..tokens] -> { let #(highlight, context) = highlight_token(token, context) highlight_loop(tokens, context, [highlight, ..acc]) } } } /// Format a list of `HighlightToken` into ANSI highlighting for display in a /// terminal. pub fn to_ansi(tokens: List(HighlightToken)) -> String { use acc, token <- list.fold(tokens, "") let highlighted = case token { HighlightComment(str:) -> ansi.italic(ansi.gray(str)) HighlightDateTime(str:) -> ansi.blue(str) HighlightError(str:) -> ansi.bg_bright_red(ansi.white(str)) HighlightKey(str:) -> ansi.yellow(str) HighlightLiteral(str:) -> ansi.green(str) HighlightNumber(str:) -> ansi.green(str) HighlightOperator(str:) -> ansi.magenta(str) HighlightPunctuation(str:) -> str HighlightString(str:) -> ansi.green(str) HighlightTable(str:) -> ansi.cyan(str) HighlightWhitespace(str:) -> str } acc <> highlighted } /// Format a list of `HighlightToken` as HTML. /// /// Each non-whitespace token is wrapped inside a `` tag with a class /// indicating the type. /// /// Class names are based on [`contour`](https://hexdocs.pm/contour): /// /// | Token | CSS class | /// | ----------- | -------------- | /// | Comment | hl-comment | /// | Date/time | hl-function | /// | Error | hl-error | /// | Key | hl-attribute | /// | Literal | hl-literal | /// | Number | hl-number | /// | Operator | hl-operator | /// | Punctuation | hl-punctuation | /// | String | hl-string | /// | Table name | hl-module | /// | Whitespace | no class | /// /// Place the output within /// `
...
` and add styling for /// these CSS classes to get highlighting on your website. Here's some CSS you /// could use: /// /// ```css /// pre code { /// .hl-comment { color: #d4d4d4; font-style: italic } /// .hl-function { color: #9ce7ff } /// .hl-attribute { color: #ffd596 } /// .hl-operator { color: #ffaff3 } /// .hl-string { color: #c8ffa7 } /// .hl-number { color: #c8ffa7 } /// .hl-literal { color: #c8ffa7 } /// .hl-module { color: #ffddfa } /// .hl-punctuation { color: inherit } /// .hl-error { background: red; color: white } /// } /// ``` pub fn to_html(tokens: List(HighlightToken)) -> String { use acc, token <- list.fold(tokens, "") let highlighted = case token { HighlightComment(str:) -> span("hl-comment", str) HighlightDateTime(str:) -> span("hl-function", str) HighlightError(str:) -> span("hl-error", str) HighlightKey(str:) -> span("hl-attribute", str) HighlightLiteral(str:) -> span("hl-literal", str) HighlightNumber(str:) -> span("hl-number", str) HighlightOperator(str:) -> span("hl-operator", str) HighlightPunctuation(str:) -> span("hl-punctuation", str) HighlightString(str:) -> span("hl-string", str) HighlightTable(str:) -> span("hl-module", str) HighlightWhitespace(str:) -> str } acc <> highlighted } fn highlight_token( token: Token, context: HighlightContext, ) -> #(HighlightToken, HighlightContext) { case context, token { _, OpenTable -> #(HighlightPunctuation("["), TableHeader) _, OpenArrayTable -> #(HighlightPunctuation("[["), TableHeader) TableHeader, CloseTable -> #(HighlightPunctuation("]"), Normal) TableHeader, CloseArrayTable -> #(HighlightPunctuation("]]"), Normal) TableHeader, BareKey(name) -> #(HighlightTable(name), TableHeader) TableHeader, String(delimiter:, value:) -> #( HighlightTable( string_delimiter(delimiter) <> value <> string_delimiter(delimiter), ), TableHeader, ) _, _ -> #(highlight_normal_token(token), context) } } fn highlight_normal_token(token: Token) -> HighlightToken { case token { BareKey(name:) -> HighlightKey(name) Boolean(value:) -> HighlightLiteral(value) CloseArrayTable -> HighlightPunctuation("]]") CloseBrace -> HighlightPunctuation("}") CloseBracket -> HighlightPunctuation("]") CloseTable -> HighlightPunctuation("]") Comma -> HighlightPunctuation(",") Comment(str:) -> HighlightComment(str) DateTime(value:) -> HighlightDateTime(value) Dot -> HighlightOperator(".") EndOfLine(str:) -> HighlightWhitespace(str) Equal -> HighlightOperator("=") Float(value:) -> HighlightNumber(value) Integer(base: _, value:) -> HighlightNumber(value) InvalidNumber(str:) -> HighlightError(str) OpenArrayTable -> HighlightPunctuation("[[") OpenBrace -> HighlightPunctuation("{") OpenBracket -> HighlightPunctuation("[") OpenTable -> HighlightPunctuation("[") String(delimiter:, value:) -> HighlightString( string_delimiter(delimiter) <> value <> string_delimiter(delimiter), ) Unexpected(str:) -> HighlightError(str) UnterminatedString(delimiter:, value:) -> HighlightError(string_delimiter(delimiter) <> value) Whitespace(str:) -> HighlightWhitespace(str) } } fn span(class: String, text: String) -> String { " class <> "\">" <> houdini.escape(text) <> "" } /// Convert a list of tokens back into their source representation. pub fn to_source(tokens: List(Token)) -> String { use acc, token <- list.fold(tokens, "") acc <> token_source(token) } fn token_source(token: Token) -> String { case token { BareKey(name:) -> name Boolean(value:) -> value CloseArrayTable -> "]]" CloseBrace -> "}" CloseBracket -> "]" CloseTable -> "]" Comma -> "," Comment(str:) -> str DateTime(value:) -> value Dot -> "." EndOfLine(str:) -> str Equal -> "=" Float(value:) -> value Integer(base: _, value:) -> value InvalidNumber(str:) -> str OpenArrayTable -> "[[" OpenBrace -> "{" OpenBracket -> "[" OpenTable -> "[" String(delimiter:, value:) -> string_delimiter(delimiter) <> value <> string_delimiter(delimiter) Unexpected(str:) -> str UnterminatedString(delimiter:, value:) -> string_delimiter(delimiter) <> value Whitespace(str:) -> str } } /// A token useful for parsing or processing TOML source code. pub type Token { // Whitespace EndOfLine(str: String) Whitespace(str: String) Comment(str: String) // Keys and values BareKey(name: String) String(delimiter: StringDelimiter, value: String) Boolean(value: String) Integer(base: Base, value: String) Float(value: String) DateTime(value: String) // Operators and punctuation Equal Dot Comma OpenTable CloseTable OpenArrayTable CloseArrayTable OpenBracket CloseBracket OpenBrace CloseBrace // Errors Unexpected(str: String) InvalidNumber(str: String) UnterminatedString(delimiter: StringDelimiter, value: String) } pub type Base { Binary Octal Decimal Hexadecimal } pub type StringDelimiter { BasicString LiteralString MultilineBasicString MultilineLiteralString } type Mode { KeyMode ValueMode AfterValueMode TableMode } type Container { ArrayContainer InlineTableContainer } pub opaque type Lexer { Lexer( skip_whitespace: Bool, skip_comments: Bool, line: Splitter, value_atom: Splitter, unexpected: Splitter, basic_string: Splitter, literal_string: Splitter, multiline_basic_string: Splitter, multiline_literal_string: Splitter, ) } /// Create a new Lexer with default settings. /// /// Lexers can be cached and reused to parse many source files efficiently. pub fn new() -> Lexer { Lexer( skip_whitespace: False, skip_comments: False, line: splitter.new(["\r\n", "\n", "\r"]), value_atom: splitter.new([ "\r\n", "\n", "\r", " ", "\t", "#", ",", "]", "}", "[", "{", "=", "\"", "'", ]), unexpected: splitter.new(["\r\n", "\n", "\r", " ", "\t", "#", ",", "]", "}"]), basic_string: splitter.new(["\r\n", "\n", "\r", "\\", "\""]), literal_string: splitter.new(["\r\n", "\n", "\r", "'"]), multiline_basic_string: splitter.new([ "\"\"\"\"\"", "\"\"\"\"", "\"\"\"", "\\", ]), multiline_literal_string: splitter.new(["'''''", "''''", "'''"]), ) } /// Skip whitespace and end-of-line tokens in the resulting token list. pub fn ignore_whitespace(lexer: Lexer) -> Lexer { Lexer(..lexer, skip_whitespace: True) } /// Skip comment tokens in the resulting token list. pub fn ignore_comments(lexer: Lexer) -> Lexer { Lexer(..lexer, skip_comments: True) } /// Parse TOML source code into a list of lexer tokens. pub fn tokenise(lexer: Lexer, input: String) -> List(Token) { start(lexer, input, [], KeyMode, [], True) } fn start( lexer: Lexer, input: String, acc: List(Token), mode: Mode, containers: List(Container), at_line_start: Bool, ) -> List(Token) { case input { "" -> list.reverse(acc) "\r\n" <> rest -> { let acc = emit(lexer, EndOfLine("\r\n"), acc) let mode = reset_mode_after_newline(mode, containers) start(lexer, rest, acc, mode, containers, True) } "\n" <> rest -> { let acc = emit(lexer, EndOfLine("\n"), acc) let mode = reset_mode_after_newline(mode, containers) start(lexer, rest, acc, mode, containers, True) } "\r" <> rest -> { let acc = emit(lexer, EndOfLine("\r"), acc) let mode = reset_mode_after_newline(mode, containers) start(lexer, rest, acc, mode, containers, True) } " " <> rest -> { let #(token, rest) = whitespace(rest, " ") let acc = emit(lexer, token, acc) start(lexer, rest, acc, mode, containers, at_line_start) } "\t" <> rest -> { let #(token, rest) = whitespace(rest, "\t") let acc = emit(lexer, token, acc) start(lexer, rest, acc, mode, containers, at_line_start) } "#" <> rest -> { let #(token, rest) = comment(lexer, rest) let acc = emit(lexer, token, acc) start(lexer, rest, acc, mode, containers, at_line_start) } "[[" <> rest if at_line_start -> { case containers { [] -> { let acc = emit(lexer, OpenArrayTable, acc) start(lexer, rest, acc, TableMode, containers, False) } _ -> { let acc = emit(lexer, OpenBracket, acc) let input = "[" <> rest let containers = [ArrayContainer, ..containers] start(lexer, input, acc, ValueMode, containers, False) } } } "[" <> rest if at_line_start -> { case containers { [] -> { let acc = emit(lexer, OpenTable, acc) start(lexer, rest, acc, TableMode, containers, False) } _ -> { let acc = emit(lexer, OpenBracket, acc) let containers = [ArrayContainer, ..containers] start(lexer, rest, acc, ValueMode, containers, False) } } } "]]" <> rest -> { case mode { TableMode -> { let acc = emit(lexer, CloseArrayTable, acc) start(lexer, rest, acc, AfterValueMode, containers, False) } _ -> { let acc = emit(lexer, CloseBracket, acc) let acc = emit(lexer, CloseBracket, acc) let containers = close_array(containers) start(lexer, rest, acc, AfterValueMode, containers, False) } } } "]" <> rest -> { case mode { TableMode -> { let acc = emit(lexer, CloseTable, acc) start(lexer, rest, acc, AfterValueMode, containers, False) } _ -> { let acc = emit(lexer, CloseBracket, acc) let containers = close_array(containers) start(lexer, rest, acc, AfterValueMode, containers, False) } } } "[" <> rest -> { let acc = emit(lexer, OpenBracket, acc) start(lexer, rest, acc, ValueMode, [ArrayContainer, ..containers], False) } "{" <> rest -> { let acc = emit(lexer, OpenBrace, acc) let containers = [InlineTableContainer, ..containers] start(lexer, rest, acc, KeyMode, containers, False) } "}" <> rest -> { let acc = emit(lexer, CloseBrace, acc) let containers = close_inline_table(containers) start(lexer, rest, acc, AfterValueMode, containers, False) } "," <> rest -> { let acc = emit(lexer, Comma, acc) start(lexer, rest, acc, mode_after_comma(containers), containers, False) } "=" <> rest -> { let acc = emit(lexer, Equal, acc) start(lexer, rest, acc, ValueMode, containers, False) } "." <> rest -> { case mode { KeyMode | TableMode -> { let acc = emit(lexer, Dot, acc) start(lexer, rest, acc, mode, containers, False) } ValueMode -> value_token(lexer, input, acc, containers) AfterValueMode -> unexpected_token(lexer, input, acc, containers) } } "\"\"\"" <> rest -> { let #(token, rest) = multiline_basic_string(lexer, rest, "") let mode = mode_after_string(mode) start(lexer, rest, emit(lexer, token, acc), mode, containers, False) } "\"" <> rest -> { let #(token, rest) = basic_string(lexer, rest, "") let mode = mode_after_string(mode) start(lexer, rest, emit(lexer, token, acc), mode, containers, False) } "'''" <> rest -> { let #(token, rest) = multiline_literal_string(lexer, rest, "") let mode = mode_after_string(mode) start(lexer, rest, emit(lexer, token, acc), mode, containers, False) } "'" <> rest -> { let #(token, rest) = literal_string(lexer, rest, "") let mode = mode_after_string(mode) start(lexer, rest, emit(lexer, token, acc), mode, containers, False) } _ -> { case mode { KeyMode | TableMode -> key_token(lexer, input, acc, mode, containers) ValueMode -> value_token(lexer, input, acc, containers) AfterValueMode -> unexpected_token(lexer, input, acc, containers) } } } } fn key_token( lexer: Lexer, input: String, acc: List(Token), mode: Mode, containers: List(Container), ) { let #(key, rest) = bare_key(input, "") case key { "" -> unexpected_token(lexer, input, acc, containers) _ -> { let acc = emit(lexer, BareKey(key), acc) start(lexer, rest, acc, mode, containers, False) } } } fn value_token( lexer: Lexer, input: String, acc: List(Token), containers: List(Container), ) { let #(token, rest) = value(lexer, input) start(lexer, rest, emit(lexer, token, acc), AfterValueMode, containers, False) } fn unexpected_token( lexer: Lexer, input: String, acc: List(Token), containers: List(Container), ) { let #(token, rest) = unexpected(lexer, input, "") start(lexer, rest, emit(lexer, token, acc), AfterValueMode, containers, False) } fn emit(lexer: Lexer, token: Token, acc: List(Token)) -> List(Token) { case token { Whitespace(_) | EndOfLine(_) -> case lexer.skip_whitespace { True -> acc False -> [token, ..acc] } Comment(_) -> case lexer.skip_comments { True -> acc False -> [token, ..acc] } _ -> [token, ..acc] } } fn reset_mode_after_newline(mode: Mode, containers: List(Container)) -> Mode { case containers { [] -> KeyMode _ -> mode } } fn mode_after_string(mode: Mode) -> Mode { case mode { ValueMode -> AfterValueMode _ -> mode } } fn close_array(containers: List(Container)) -> List(Container) { case containers { [ArrayContainer, ..containers] -> containers _ -> containers } } fn close_inline_table(containers: List(Container)) -> List(Container) { case containers { [InlineTableContainer, ..containers] -> containers _ -> containers } } fn mode_after_comma(containers: List(Container)) -> Mode { case containers { [InlineTableContainer, ..] -> KeyMode [ArrayContainer, ..] -> ValueMode [] -> ValueMode } } fn whitespace(input: String, acc: String) -> #(Token, String) { case input { " " <> rest -> whitespace(rest, acc <> " ") "\t" <> rest -> whitespace(rest, acc <> "\t") _ -> #(Whitespace(acc), input) } } fn comment(lexer: Lexer, input: String) -> #(Token, String) { let #(body, rest) = splitter.split_before(lexer.line, input) #(Comment("#" <> body), rest) } fn bare_key(input: String, acc: String) -> #(String, String) { case input { "A" <> rest -> bare_key(rest, acc <> "A") "B" <> rest -> bare_key(rest, acc <> "B") "C" <> rest -> bare_key(rest, acc <> "C") "D" <> rest -> bare_key(rest, acc <> "D") "E" <> rest -> bare_key(rest, acc <> "E") "F" <> rest -> bare_key(rest, acc <> "F") "G" <> rest -> bare_key(rest, acc <> "G") "H" <> rest -> bare_key(rest, acc <> "H") "I" <> rest -> bare_key(rest, acc <> "I") "J" <> rest -> bare_key(rest, acc <> "J") "K" <> rest -> bare_key(rest, acc <> "K") "L" <> rest -> bare_key(rest, acc <> "L") "M" <> rest -> bare_key(rest, acc <> "M") "N" <> rest -> bare_key(rest, acc <> "N") "O" <> rest -> bare_key(rest, acc <> "O") "P" <> rest -> bare_key(rest, acc <> "P") "Q" <> rest -> bare_key(rest, acc <> "Q") "R" <> rest -> bare_key(rest, acc <> "R") "S" <> rest -> bare_key(rest, acc <> "S") "T" <> rest -> bare_key(rest, acc <> "T") "U" <> rest -> bare_key(rest, acc <> "U") "V" <> rest -> bare_key(rest, acc <> "V") "W" <> rest -> bare_key(rest, acc <> "W") "X" <> rest -> bare_key(rest, acc <> "X") "Y" <> rest -> bare_key(rest, acc <> "Y") "Z" <> rest -> bare_key(rest, acc <> "Z") "a" <> rest -> bare_key(rest, acc <> "a") "b" <> rest -> bare_key(rest, acc <> "b") "c" <> rest -> bare_key(rest, acc <> "c") "d" <> rest -> bare_key(rest, acc <> "d") "e" <> rest -> bare_key(rest, acc <> "e") "f" <> rest -> bare_key(rest, acc <> "f") "g" <> rest -> bare_key(rest, acc <> "g") "h" <> rest -> bare_key(rest, acc <> "h") "i" <> rest -> bare_key(rest, acc <> "i") "j" <> rest -> bare_key(rest, acc <> "j") "k" <> rest -> bare_key(rest, acc <> "k") "l" <> rest -> bare_key(rest, acc <> "l") "m" <> rest -> bare_key(rest, acc <> "m") "n" <> rest -> bare_key(rest, acc <> "n") "o" <> rest -> bare_key(rest, acc <> "o") "p" <> rest -> bare_key(rest, acc <> "p") "q" <> rest -> bare_key(rest, acc <> "q") "r" <> rest -> bare_key(rest, acc <> "r") "s" <> rest -> bare_key(rest, acc <> "s") "t" <> rest -> bare_key(rest, acc <> "t") "u" <> rest -> bare_key(rest, acc <> "u") "v" <> rest -> bare_key(rest, acc <> "v") "w" <> rest -> bare_key(rest, acc <> "w") "x" <> rest -> bare_key(rest, acc <> "x") "y" <> rest -> bare_key(rest, acc <> "y") "z" <> rest -> bare_key(rest, acc <> "z") "0" <> rest -> bare_key(rest, acc <> "0") "1" <> rest -> bare_key(rest, acc <> "1") "2" <> rest -> bare_key(rest, acc <> "2") "3" <> rest -> bare_key(rest, acc <> "3") "4" <> rest -> bare_key(rest, acc <> "4") "5" <> rest -> bare_key(rest, acc <> "5") "6" <> rest -> bare_key(rest, acc <> "6") "7" <> rest -> bare_key(rest, acc <> "7") "8" <> rest -> bare_key(rest, acc <> "8") "9" <> rest -> bare_key(rest, acc <> "9") "-" <> rest -> bare_key(rest, acc <> "-") "_" <> rest -> bare_key(rest, acc <> "_") _ -> #(acc, input) } } fn value(lexer: Lexer, input: String) -> #(Token, String) { case input { "true" <> rest -> literal_or_unexpected(lexer, rest, Boolean("true"), input) "false" <> rest -> literal_or_unexpected(lexer, rest, Boolean("false"), input) "inf" <> rest -> literal_or_unexpected(lexer, rest, Float("inf"), input) "+inf" <> rest -> literal_or_unexpected(lexer, rest, Float("+inf"), input) "-inf" <> rest -> literal_or_unexpected(lexer, rest, Float("-inf"), input) "nan" <> rest -> literal_or_unexpected(lexer, rest, Float("nan"), input) "+nan" <> rest -> literal_or_unexpected(lexer, rest, Float("+nan"), input) "-nan" <> rest -> literal_or_unexpected(lexer, rest, Float("-nan"), input) "+0b" <> _ | "+0o" <> _ | "+0x" <> _ -> invalid_number(lexer, input, "") "-0b" <> _ | "-0o" <> _ | "-0x" <> _ -> invalid_number(lexer, input, "") "+" <> rest -> case take_digit(rest) { Ok(#(digit, rest)) -> decimal(lexer, rest, "+" <> digit, 1, False) Error(_) -> invalid_number(lexer, rest, "+") } "-" <> rest -> case take_digit(rest) { Ok(#(digit, rest)) -> decimal(lexer, rest, "-" <> digit, 1, False) Error(_) -> invalid_number(lexer, rest, "-") } "." <> _ -> invalid_number(lexer, input, "") "0b" <> rest -> binary(lexer, rest, "0b", True) "0o" <> rest -> octal(lexer, rest, "0o", True) "0x" <> rest -> hexadecimal(lexer, rest, "0x", True) "0" <> _ | "1" <> _ | "2" <> _ | "3" <> _ | "4" <> _ | "5" <> _ | "6" <> _ | "7" <> _ | "8" <> _ | "9" <> _ -> case take_digit(input) { Ok(#(digit, rest)) -> decimal(lexer, rest, digit, 1, True) Error(_) -> invalid_number(lexer, input, "") } _ -> unexpected(lexer, input, "") } } fn literal_or_unexpected( lexer: Lexer, rest: String, token: Token, original: String, ) -> #(Token, String) { case is_value_terminator(rest) { True -> #(token, rest) False -> unexpected(lexer, original, "") } } fn unexpected(lexer: Lexer, input: String, acc: String) -> #(Token, String) { let #(unexpected, rest) = splitter.split_before(lexer.unexpected, input) #(Unexpected(acc <> unexpected), rest) } fn basic_string(lexer: Lexer, input: String, acc: String) -> #(Token, String) { let #(before, delimiter, rest) = splitter.split(lexer.basic_string, input) case delimiter { "\"" -> #(String(BasicString, acc <> before), rest) "\r\n" | "\n" | "\r" -> #( UnterminatedString(BasicString, acc <> before), delimiter <> rest, ) "\\" -> basic_string_escape(lexer, rest, acc <> before <> "\\") "" -> #(UnterminatedString(BasicString, acc <> before), "") _ -> #(UnterminatedString(BasicString, acc <> before <> delimiter), rest) } } fn basic_string_escape( lexer: Lexer, input: String, acc: String, ) -> #(Token, String) { case input { "" -> #(UnterminatedString(BasicString, acc), "") "\r\n" <> _ | "\n" <> _ | "\r" <> _ -> #( UnterminatedString(BasicString, acc), input, ) "\"" <> rest -> basic_string(lexer, rest, acc <> "\"") "\\" <> rest -> basic_string(lexer, rest, acc <> "\\") _ -> { let #(escaped, delimiter, rest) = splitter.split(lexer.basic_string, input) case escaped, delimiter { "", "" -> #(UnterminatedString(BasicString, acc), "") _, "" -> #(UnterminatedString(BasicString, acc <> escaped), "") "", _ -> basic_string(lexer, rest, acc <> delimiter) _, _ -> basic_string(lexer, delimiter <> rest, acc <> escaped) } } } } fn literal_string( lexer: Lexer, input: String, acc: String, ) -> #(Token, String) { let #(before, delimiter, rest) = splitter.split(lexer.literal_string, input) case delimiter { "'" -> #(String(LiteralString, acc <> before), rest) "\r\n" | "\n" | "\r" -> #( UnterminatedString(LiteralString, acc <> before), delimiter <> rest, ) "" -> #(UnterminatedString(LiteralString, acc <> before), "") _ -> #(UnterminatedString(LiteralString, acc <> before <> delimiter), rest) } } fn multiline_basic_string( lexer: Lexer, input: String, acc: String, ) -> #(Token, String) { let #(before, delimiter, rest) = splitter.split(lexer.multiline_basic_string, input) case delimiter { "\"\"\"\"\"" -> #( String(MultilineBasicString, acc <> before <> "\"\""), rest, ) "\"\"\"\"" -> #(String(MultilineBasicString, acc <> before <> "\""), rest) "\"\"\"" -> #(String(MultilineBasicString, acc <> before), rest) "\\" -> multiline_basic_string_escape(lexer, rest, acc <> before <> "\\") "" -> #(UnterminatedString(MultilineBasicString, acc <> before), "") _ -> #( UnterminatedString(MultilineBasicString, acc <> before <> delimiter), rest, ) } } fn multiline_basic_string_escape( lexer: Lexer, input: String, acc: String, ) -> #(Token, String) { case input { "" -> #(UnterminatedString(MultilineBasicString, acc), "") "\"" <> rest -> multiline_basic_string(lexer, rest, acc <> "\"") "\\" <> rest -> multiline_basic_string(lexer, rest, acc <> "\\") _ -> { let #(escaped, delimiter, rest) = splitter.split(lexer.multiline_basic_string, input) case escaped, delimiter { "", "" -> #(UnterminatedString(MultilineBasicString, acc), "") _, "" -> #(UnterminatedString(MultilineBasicString, acc <> escaped), "") "", _ -> multiline_basic_string(lexer, rest, acc <> delimiter) _, _ -> multiline_basic_string(lexer, delimiter <> rest, acc <> escaped) } } } } fn multiline_literal_string( lexer: Lexer, input: String, acc: String, ) -> #(Token, String) { let #(before, delimiter, rest) = splitter.split(lexer.multiline_literal_string, input) case delimiter { "'''''" -> #(String(MultilineLiteralString, acc <> before <> "''"), rest) "''''" -> #(String(MultilineLiteralString, acc <> before <> "'"), rest) "'''" -> #(String(MultilineLiteralString, acc <> before), rest) "" -> #(UnterminatedString(MultilineLiteralString, acc <> before), "") _ -> #( UnterminatedString(MultilineLiteralString, acc <> before <> delimiter), rest, ) } } fn binary( lexer: Lexer, input: String, acc: String, last_was_underscore: Bool, ) -> #(Token, String) { case input { "0" <> rest -> binary(lexer, rest, acc <> "0", False) "1" <> rest -> binary(lexer, rest, acc <> "1", False) "_" <> rest if !last_was_underscore -> { let acc = acc <> "_" binary(lexer, rest, acc, True) } _ -> based_integer_end(lexer, input, acc, Binary, last_was_underscore) } } fn octal( lexer: Lexer, input: String, acc: String, last_was_underscore: Bool, ) -> #(Token, String) { case input { "0" <> rest -> octal(lexer, rest, acc <> "0", False) "1" <> rest -> octal(lexer, rest, acc <> "1", False) "2" <> rest -> octal(lexer, rest, acc <> "2", False) "3" <> rest -> octal(lexer, rest, acc <> "3", False) "4" <> rest -> octal(lexer, rest, acc <> "4", False) "5" <> rest -> octal(lexer, rest, acc <> "5", False) "6" <> rest -> octal(lexer, rest, acc <> "6", False) "7" <> rest -> octal(lexer, rest, acc <> "7", False) "_" <> rest if !last_was_underscore -> { let acc = acc <> "_" octal(lexer, rest, acc, True) } _ -> based_integer_end(lexer, input, acc, Octal, last_was_underscore) } } fn hexadecimal( lexer: Lexer, input: String, acc: String, last_was_underscore: Bool, ) -> #(Token, String) { case input { "0" <> rest -> hexadecimal(lexer, rest, acc <> "0", False) "1" <> rest -> hexadecimal(lexer, rest, acc <> "1", False) "2" <> rest -> hexadecimal(lexer, rest, acc <> "2", False) "3" <> rest -> hexadecimal(lexer, rest, acc <> "3", False) "4" <> rest -> hexadecimal(lexer, rest, acc <> "4", False) "5" <> rest -> hexadecimal(lexer, rest, acc <> "5", False) "6" <> rest -> hexadecimal(lexer, rest, acc <> "6", False) "7" <> rest -> hexadecimal(lexer, rest, acc <> "7", False) "8" <> rest -> hexadecimal(lexer, rest, acc <> "8", False) "9" <> rest -> hexadecimal(lexer, rest, acc <> "9", False) "A" <> rest -> hexadecimal(lexer, rest, acc <> "A", False) "B" <> rest -> hexadecimal(lexer, rest, acc <> "B", False) "C" <> rest -> hexadecimal(lexer, rest, acc <> "C", False) "D" <> rest -> hexadecimal(lexer, rest, acc <> "D", False) "E" <> rest -> hexadecimal(lexer, rest, acc <> "E", False) "F" <> rest -> hexadecimal(lexer, rest, acc <> "F", False) "a" <> rest -> hexadecimal(lexer, rest, acc <> "a", False) "b" <> rest -> hexadecimal(lexer, rest, acc <> "b", False) "c" <> rest -> hexadecimal(lexer, rest, acc <> "c", False) "d" <> rest -> hexadecimal(lexer, rest, acc <> "d", False) "e" <> rest -> hexadecimal(lexer, rest, acc <> "e", False) "f" <> rest -> hexadecimal(lexer, rest, acc <> "f", False) "_" <> rest if !last_was_underscore -> { let acc = acc <> "_" hexadecimal(lexer, rest, acc, True) } _ -> based_integer_end(lexer, input, acc, Hexadecimal, last_was_underscore) } } fn based_integer_end( lexer: Lexer, input: String, acc: String, base: Base, last_was_underscore: Bool, ) -> #(Token, String) { case last_was_underscore { False -> { let to_token = fn(value) { Integer(base, value) } number_end(lexer, input, acc, to_token) } True -> invalid_number(lexer, input, acc) } } fn decimal( lexer: Lexer, input: String, acc: String, digits: Int, date_time: Bool, ) -> #(Token, String) { case input { "0" <> rest -> decimal(lexer, rest, acc <> "0", digits + 1, date_time) "1" <> rest -> decimal(lexer, rest, acc <> "1", digits + 1, date_time) "2" <> rest -> decimal(lexer, rest, acc <> "2", digits + 1, date_time) "3" <> rest -> decimal(lexer, rest, acc <> "3", digits + 1, date_time) "4" <> rest -> decimal(lexer, rest, acc <> "4", digits + 1, date_time) "5" <> rest -> decimal(lexer, rest, acc <> "5", digits + 1, date_time) "6" <> rest -> decimal(lexer, rest, acc <> "6", digits + 1, date_time) "7" <> rest -> decimal(lexer, rest, acc <> "7", digits + 1, date_time) "8" <> rest -> decimal(lexer, rest, acc <> "8", digits + 1, date_time) "9" <> rest -> decimal(lexer, rest, acc <> "9", digits + 1, date_time) "_0" <> rest -> decimal(lexer, rest, acc <> "_0", digits + 1, False) "_1" <> rest -> decimal(lexer, rest, acc <> "_1", digits + 1, False) "_2" <> rest -> decimal(lexer, rest, acc <> "_2", digits + 1, False) "_3" <> rest -> decimal(lexer, rest, acc <> "_3", digits + 1, False) "_4" <> rest -> decimal(lexer, rest, acc <> "_4", digits + 1, False) "_5" <> rest -> decimal(lexer, rest, acc <> "_5", digits + 1, False) "_6" <> rest -> decimal(lexer, rest, acc <> "_6", digits + 1, False) "_7" <> rest -> decimal(lexer, rest, acc <> "_7", digits + 1, False) "_8" <> rest -> decimal(lexer, rest, acc <> "_8", digits + 1, False) "_9" <> rest -> decimal(lexer, rest, acc <> "_9", digits + 1, False) ".0" <> rest -> float(lexer, rest, acc <> ".0") ".1" <> rest -> float(lexer, rest, acc <> ".1") ".2" <> rest -> float(lexer, rest, acc <> ".2") ".3" <> rest -> float(lexer, rest, acc <> ".3") ".4" <> rest -> float(lexer, rest, acc <> ".4") ".5" <> rest -> float(lexer, rest, acc <> ".5") ".6" <> rest -> float(lexer, rest, acc <> ".6") ".7" <> rest -> float(lexer, rest, acc <> ".7") ".8" <> rest -> float(lexer, rest, acc <> ".8") ".9" <> rest -> float(lexer, rest, acc <> ".9") "e+" <> rest -> exponent_start(lexer, input, rest, acc, "e+") "e-" <> rest -> exponent_start(lexer, input, rest, acc, "e-") "e" <> rest -> exponent_start(lexer, input, rest, acc, "e") "E+" <> rest -> exponent_start(lexer, input, rest, acc, "E+") "E-" <> rest -> exponent_start(lexer, input, rest, acc, "E-") "E" <> rest -> exponent_start(lexer, input, rest, acc, "E") ":" <> rest if date_time && digits == 2 -> time_minutes(lexer, rest, acc <> ":", False) "-" <> rest if date_time && digits == 4 -> date(lexer, rest, acc <> "-") _ -> { let to_token = fn(value) { Integer(Decimal, value) } number_end(lexer, input, acc, to_token) } } } fn float(lexer: Lexer, input: String, acc: String) -> #(Token, String) { case input { "0" <> rest -> float(lexer, rest, acc <> "0") "1" <> rest -> float(lexer, rest, acc <> "1") "2" <> rest -> float(lexer, rest, acc <> "2") "3" <> rest -> float(lexer, rest, acc <> "3") "4" <> rest -> float(lexer, rest, acc <> "4") "5" <> rest -> float(lexer, rest, acc <> "5") "6" <> rest -> float(lexer, rest, acc <> "6") "7" <> rest -> float(lexer, rest, acc <> "7") "8" <> rest -> float(lexer, rest, acc <> "8") "9" <> rest -> float(lexer, rest, acc <> "9") "_0" <> rest -> float(lexer, rest, acc <> "_0") "_1" <> rest -> float(lexer, rest, acc <> "_1") "_2" <> rest -> float(lexer, rest, acc <> "_2") "_3" <> rest -> float(lexer, rest, acc <> "_3") "_4" <> rest -> float(lexer, rest, acc <> "_4") "_5" <> rest -> float(lexer, rest, acc <> "_5") "_6" <> rest -> float(lexer, rest, acc <> "_6") "_7" <> rest -> float(lexer, rest, acc <> "_7") "_8" <> rest -> float(lexer, rest, acc <> "_8") "_9" <> rest -> float(lexer, rest, acc <> "_9") "e+" <> rest -> exponent_start(lexer, input, rest, acc, "e+") "e-" <> rest -> exponent_start(lexer, input, rest, acc, "e-") "e" <> rest -> exponent_start(lexer, input, rest, acc, "e") "E+" <> rest -> exponent_start(lexer, input, rest, acc, "E+") "E-" <> rest -> exponent_start(lexer, input, rest, acc, "E-") "E" <> rest -> exponent_start(lexer, input, rest, acc, "E") _ -> number_end(lexer, input, acc, Float) } } fn exponent_start( lexer: Lexer, before_marker: String, input: String, acc: String, marker: String, ) -> #(Token, String) { case input { "0" <> rest -> exponent(lexer, rest, acc <> marker <> "0") "1" <> rest -> exponent(lexer, rest, acc <> marker <> "1") "2" <> rest -> exponent(lexer, rest, acc <> marker <> "2") "3" <> rest -> exponent(lexer, rest, acc <> marker <> "3") "4" <> rest -> exponent(lexer, rest, acc <> marker <> "4") "5" <> rest -> exponent(lexer, rest, acc <> marker <> "5") "6" <> rest -> exponent(lexer, rest, acc <> marker <> "6") "7" <> rest -> exponent(lexer, rest, acc <> marker <> "7") "8" <> rest -> exponent(lexer, rest, acc <> marker <> "8") "9" <> rest -> exponent(lexer, rest, acc <> marker <> "9") _ -> invalid_number(lexer, before_marker, acc) } } fn exponent(lexer: Lexer, input: String, acc: String) -> #(Token, String) { case input { "0" <> rest -> exponent(lexer, rest, acc <> "0") "1" <> rest -> exponent(lexer, rest, acc <> "1") "2" <> rest -> exponent(lexer, rest, acc <> "2") "3" <> rest -> exponent(lexer, rest, acc <> "3") "4" <> rest -> exponent(lexer, rest, acc <> "4") "5" <> rest -> exponent(lexer, rest, acc <> "5") "6" <> rest -> exponent(lexer, rest, acc <> "6") "7" <> rest -> exponent(lexer, rest, acc <> "7") "8" <> rest -> exponent(lexer, rest, acc <> "8") "9" <> rest -> exponent(lexer, rest, acc <> "9") "_0" <> rest -> exponent(lexer, rest, acc <> "_0") "_1" <> rest -> exponent(lexer, rest, acc <> "_1") "_2" <> rest -> exponent(lexer, rest, acc <> "_2") "_3" <> rest -> exponent(lexer, rest, acc <> "_3") "_4" <> rest -> exponent(lexer, rest, acc <> "_4") "_5" <> rest -> exponent(lexer, rest, acc <> "_5") "_6" <> rest -> exponent(lexer, rest, acc <> "_6") "_7" <> rest -> exponent(lexer, rest, acc <> "_7") "_8" <> rest -> exponent(lexer, rest, acc <> "_8") "_9" <> rest -> exponent(lexer, rest, acc <> "_9") _ -> number_end(lexer, input, acc, Float) } } fn date(lexer: Lexer, input: String, acc: String) -> #(Token, String) { case take_two_digits(input) { Ok(#(month, "-" <> rest)) -> case take_two_digits(rest) { Ok(#(day, rest)) -> date_tail(lexer, rest, acc <> month <> "-" <> day) _ -> invalid_number(lexer, input, acc) } _ -> invalid_number(lexer, input, acc) } } fn date_tail(lexer: Lexer, input: String, acc: String) -> #(Token, String) { case input { "T" <> rest -> date_time(lexer, rest, acc <> "T") "t" <> rest -> date_time(lexer, rest, acc <> "t") " " <> rest -> case take_two_digits(rest) { Ok(#(hour, ":" <> rest)) -> time_minutes(lexer, rest, acc <> " " <> hour <> ":", True) _ -> number_end(lexer, input, acc, DateTime) } _ -> number_end(lexer, input, acc, DateTime) } } fn date_time(lexer: Lexer, input: String, acc: String) -> #(Token, String) { case take_two_digits(input) { Ok(#(hour, ":" <> rest)) -> time_minutes(lexer, rest, acc <> hour <> ":", True) _ -> invalid_number(lexer, input, acc) } } fn time_minutes( lexer: Lexer, input: String, acc: String, allow_offset: Bool, ) -> #(Token, String) { case take_two_digits(input) { Ok(#(minute, rest)) -> { let acc = acc <> minute case rest { ":" <> rest -> time_seconds(lexer, rest, acc <> ":", allow_offset) _ -> date_time_end(lexer, rest, acc, allow_offset) } } _ -> invalid_number(lexer, input, acc) } } fn time_seconds( lexer: Lexer, input: String, acc: String, allow_offset: Bool, ) -> #(Token, String) { case take_two_digits(input) { Ok(#(second, "." <> rest)) -> fraction(lexer, rest, acc <> second <> ".", allow_offset) Ok(#(second, rest)) -> date_time_end(lexer, rest, acc <> second, allow_offset) _ -> invalid_number(lexer, input, acc) } } fn fraction( lexer: Lexer, input: String, acc: String, allow_offset: Bool, ) -> #(Token, String) { case input { "0" <> rest -> fraction_digits(lexer, rest, acc <> "0", allow_offset) "1" <> rest -> fraction_digits(lexer, rest, acc <> "1", allow_offset) "2" <> rest -> fraction_digits(lexer, rest, acc <> "2", allow_offset) "3" <> rest -> fraction_digits(lexer, rest, acc <> "3", allow_offset) "4" <> rest -> fraction_digits(lexer, rest, acc <> "4", allow_offset) "5" <> rest -> fraction_digits(lexer, rest, acc <> "5", allow_offset) "6" <> rest -> fraction_digits(lexer, rest, acc <> "6", allow_offset) "7" <> rest -> fraction_digits(lexer, rest, acc <> "7", allow_offset) "8" <> rest -> fraction_digits(lexer, rest, acc <> "8", allow_offset) "9" <> rest -> fraction_digits(lexer, rest, acc <> "9", allow_offset) _ -> invalid_number(lexer, input, acc) } } fn fraction_digits( lexer: Lexer, input: String, acc: String, allow_offset: Bool, ) -> #(Token, String) { case input { "0" <> rest -> fraction_digits(lexer, rest, acc <> "0", allow_offset) "1" <> rest -> fraction_digits(lexer, rest, acc <> "1", allow_offset) "2" <> rest -> fraction_digits(lexer, rest, acc <> "2", allow_offset) "3" <> rest -> fraction_digits(lexer, rest, acc <> "3", allow_offset) "4" <> rest -> fraction_digits(lexer, rest, acc <> "4", allow_offset) "5" <> rest -> fraction_digits(lexer, rest, acc <> "5", allow_offset) "6" <> rest -> fraction_digits(lexer, rest, acc <> "6", allow_offset) "7" <> rest -> fraction_digits(lexer, rest, acc <> "7", allow_offset) "8" <> rest -> fraction_digits(lexer, rest, acc <> "8", allow_offset) "9" <> rest -> fraction_digits(lexer, rest, acc <> "9", allow_offset) _ -> date_time_end(lexer, input, acc, allow_offset) } } fn date_time_end( lexer: Lexer, input: String, acc: String, allow_offset: Bool, ) -> #(Token, String) { case input { "Z" <> rest if allow_offset -> number_end(lexer, rest, acc <> "Z", DateTime) "z" <> rest if allow_offset -> number_end(lexer, rest, acc <> "z", DateTime) "+" <> rest if allow_offset -> offset(lexer, rest, acc <> "+") "-" <> rest if allow_offset -> offset(lexer, rest, acc <> "-") _ -> number_end(lexer, input, acc, DateTime) } } fn offset(lexer: Lexer, input: String, acc: String) -> #(Token, String) { case take_two_digits(input) { Ok(#(hour, ":" <> rest)) -> case take_two_digits(rest) { Ok(#(minute, rest)) -> number_end(lexer, rest, acc <> hour <> ":" <> minute, DateTime) _ -> invalid_number(lexer, input, acc) } _ -> invalid_number(lexer, input, acc) } } fn take_two_digits(input: String) -> Result(#(String, String), Nil) { case take_digit(input) { Ok(#(first, rest)) -> case take_digit(rest) { Ok(#(second, rest)) -> Ok(#(first <> second, rest)) Error(_) -> Error(Nil) } Error(_) -> Error(Nil) } } fn take_digit(input: String) -> Result(#(String, String), Nil) { case input { "0" <> rest -> Ok(#("0", rest)) "1" <> rest -> Ok(#("1", rest)) "2" <> rest -> Ok(#("2", rest)) "3" <> rest -> Ok(#("3", rest)) "4" <> rest -> Ok(#("4", rest)) "5" <> rest -> Ok(#("5", rest)) "6" <> rest -> Ok(#("6", rest)) "7" <> rest -> Ok(#("7", rest)) "8" <> rest -> Ok(#("8", rest)) "9" <> rest -> Ok(#("9", rest)) _ -> Error(Nil) } } fn number_end( lexer: Lexer, input: String, acc: String, token: fn(String) -> Token, ) -> #(Token, String) { case is_value_terminator(input) { True -> #(token(acc), input) False -> invalid_number(lexer, input, acc) } } fn invalid_number( lexer: Lexer, input: String, acc: String, ) -> #(Token, String) { let #(unexpected, rest) = splitter.split_before(lexer.value_atom, input) #(InvalidNumber(acc <> unexpected), rest) } fn is_value_terminator(input: String) -> Bool { case input { "" | " " <> _ | "\t" <> _ | "\r\n" <> _ | "\n" <> _ | "\r" <> _ -> True "#" <> _ | "," <> _ | "]" <> _ | "}" <> _ -> True _ -> False } } fn string_delimiter(delimiter: StringDelimiter) -> String { case delimiter { BasicString -> "\"" LiteralString -> "'" MultilineBasicString -> "\"\"\"" MultilineLiteralString -> "'''" } }