defmodule Toml.Parser do @moduledoc false alias Toml.Document alias Toml.Lexer @compile :inline_list_funs @compile inline: [pop_skip: 2, peek_skip: 2, iodata_to_str: 1, iodata_to_integer: 1, iodata_to_float: 1] @doc """ Parses the given binary as TOML. You can provide an optional filename for error messages, "nofile" is used by default. Returns the parsed content as a map, wrapped in an :ok tuple. """ @spec parse(binary) :: {:ok, map} | {:error, term} @spec parse(binary, String.t) :: {:ok, map} | {:error, term} def parse(bin, filename \\ "nofile") when is_binary(bin) do {:ok, lexer} = Lexer.new(bin) try do with {:ok, %Document{} = doc} <- do_parse(lexer, bin, Document.new()) do Document.to_map(doc) else {:error, reason, skip, lines} -> {:error, {:invalid_toml, format_error(reason, bin, filename, skip, lines)}} end catch :throw, {:error, {:invalid_toml, reason}} = err when is_binary(reason) -> err :throw, {:error, {:invalid_toml, reason}} -> {:error, {:invalid_toml, format_reason(reason)}} after Lexer.stop(lexer) end end @doc """ Parses the given stream as TOML. You can provide an optional filename for error messages, "nofile" is used by default. Returns the parsed content as a map, wrapped in an :ok tuple. """ @spec parse_stream(Enumerable.t) :: {:ok, map} | {:error, term} @spec parse_stream(Enumerable.t, String.t) :: {:ok, map} | {:error, term} def parse_stream(stream, filename \\ "nofile") do bin = Enum.into(stream, <<>>) {:ok, lexer} = Lexer.new(bin) try do with {:ok, %Document{} = doc} <- do_parse(lexer, bin, Document.new()) do Document.to_map(doc) else {:error, reason, skip, lines} -> filename = Path.relative_to_cwd(filename) {:error, {:invalid_toml, format_error(reason, bin, filename, skip, lines)}} end catch :throw, {:error, {:invalid_toml, reason}} = err when is_binary(reason) -> err :throw, {:error, {:invalid_toml, reason}} -> {:error, {:invalid_toml, format_reason(reason)}} after Lexer.stop(lexer) end end @doc """ Parses the given file as TOML. Returns the parsed content as a map, wrapped in an :ok tuple. """ @spec parse_file(String.t) :: {:ok, map} | {:error, term} def parse_file(path) when is_binary(path) do with {:ok, bin} <- File.read(path), {:ok, _} = result <- parse(bin, path) do result end end ## Parser Implementation defp do_parse(_lexer, _original, {:error, _, _, _} = err), do: err defp do_parse(lexer, original, doc) do case Lexer.pop(lexer) do {:error, _reason, _skip, _lines} = err -> err {:ok, {type, skip, data, lines}}-> handle_token(lexer, original, doc, type, skip, data, lines) end end # Converts an error into a friendly, printable representation defp format_error(reason, original, filename, skip, lines) do msg = "#{format_reason(reason)} in #{Path.relative_to_cwd(filename)} on line #{lines}" {ctx, pos} = seek_line(original, skip, lines) """ #{msg}: #{ctx} #{String.duplicate(" ", pos+1)}^ """ end # Finds the line of context for display in a formatted error defp seek_line(original, skip, lines) do seek_line(original, original, 0, 0, skip, lines-1) end defp seek_line(original, rest, lastnl, from, len, 0) do case seek_to_eol(rest, 0) do 0 -> {binary_part(original, lastnl, from-lastnl), from-lastnl} len_to_eol -> {binary_part(original, from, len_to_eol), len} end end defp seek_line(original, <>, lastnl, from, len, 1) when len <= 0 do # Content occurred on the last line right before the newline seek_line(original, rest, lastnl, from+2, 0, 0) end defp seek_line(original, <>, _, from, len, lines) do seek_line(original, rest, from+2, from+2, len-2, lines-1) end defp seek_line(original, <> = rest, lastnl, from, len, 1) when len <= 0 do # Content occurred on the last line right before the newline seek_line(original, rest, lastnl, from+1, 0, 0) end defp seek_line(original, <>, _, from, len, lines) do seek_line(original, rest, from+1, from+1, len-1, lines-1) end defp seek_line(original, <<_::utf8, rest::binary>>, lastnl, from, len, lines) do seek_line(original, rest, lastnl, from+1, len-1, lines) end # Find the number of bytes to the end of the current line in the input defp seek_to_eol(<<>>, len), do: len defp seek_to_eol(<>, len), do: len defp seek_to_eol(<>, len), do: len defp seek_to_eol(<<_::utf8, rest::binary>>, len) do seek_to_eol(rest, len+1) end # Convert internal error reasons into friendly, printable form defp format_reason({:expected, token, other}), do: "expected #{format_token(token)}, but got #{format_token(other)}" defp format_reason({:invalid_token, token}), do: "invalid token #{format_token(token)}" defp format_reason({:invalid_integer, :leading_zero}), do: "invalid integer syntax, leading zeros are not allowed" defp format_reason({:invalid_float, :leading_zero}), do: "invalid float syntax, leading zeros are not allowed" defp format_reason({:invalid_float, token}), do: "invalid float syntax, unexpected token #{format_token(token)}" defp format_reason({:invalid_datetime, reason}), do: "invalid datetime syntax, #{inspect reason}" defp format_reason({:invalid_datetime_offset, reason}), do: "invalid datetime offset syntax, #{inspect reason}" defp format_reason({:invalid_date, reason}), do: "invalid date syntax, #{inspect reason}" defp format_reason({:invalid_time, reason}), do: "invalid time syntax, #{inspect reason}" defp format_reason({:invalid_key_value, token}), do: "invalid key/value syntax, unexpected token #{format_token(token)}" defp format_reason({:unclosed_table_array_name, token}), do: "unclosed table array name at #{format_token(token)}" defp format_reason({:unclosed_array, {oline, ocol}}), do: "unclosed array started at #{oline}:#{ocol}" defp format_reason({:unclosed_inline_table, {oline, ocol}}), do: "unclosed inline table started at #{oline}:#{ocol}" defp format_reason(:unclosed_quote), do: "unclosed quoted string" defp format_reason(:unexpected_newline), do: "unexpected newline in single-quoted string" defp format_reason(:unexpected_eof), do: "unexpected end of file" defp format_reason({:invalid_escape, char}), do: "illegal escape sequence #{char}" defp format_reason({:invalid_char, char}) do if String.printable?(char) do "illegal character #{inspect char, base: :hex} ('#{char}')" else "illegal character #{inspect char, base: :hex}" end end defp format_reason({:invalid_unicode, message}) do "illegal unicode escape, #{message}" end defp format_reason({:key_exists, key}) do "cannot redefine key '#{key}'" end defp format_reason(reason), do: "#{inspect reason}" # Format a token in `{type, data}`, or `type` form into printable representation defp format_token({true, _}), do: "'true'" defp format_token({false, _}), do: "'false'" defp format_token(:newline), do: "'\\n'" defp format_token({_, data}) when is_binary(data), do: "'#{data}'" defp format_token({token, data}) when is_atom(token), do: "'#{data}' (#{token})" defp format_token({token, _}), do: "'#{<>}'" defp format_token(token), do: "'#{token}'" # Skip top-level whitespace and newlines defp handle_token(lexer, original, doc, :whitespace, _skip, _data, _lines), do: do_parse(lexer, original, doc) defp handle_token(lexer, original, doc, :newline, _skip, _data, _lines), do: do_parse(lexer, original, doc) # Push comments on the comment stack defp handle_token(lexer, original, doc, :comment, skip, size, _lines) do comment = binary_part(original, skip-size, size) do_parse(lexer, original, Document.push_comment(doc, comment)) end # Handle valid top-level entities # - array of tables # - table # - key/value defp handle_token(lexer, original, doc, ?\[, skip, data, lines) do case peek_skip(lexer, [:whitespace]) do {:error, _, _, _} = err -> err {:ok, {?\[, _, _, _}} -> # Push opening bracket, second was peeked, so no need Lexer.push(lexer, {?\[, skip, data, lines}) handle_table_array(lexer, original, doc) {:ok, {_, _, _, _}} -> Lexer.push(lexer, {?\[, skip, data, lines}) handle_table(lexer, original, doc) end end defp handle_token(lexer, original, doc, type, skip, data, lines) when type in [:digits, :alpha, :string] do Lexer.push(lexer, {type, skip, data, lines}) with {:ok, key, _skip, _lines} <- key(lexer) do handle_key(lexer, original, doc, key) end end defp handle_token(lexer, original, doc, type, skip, _data, lines) when type in '-_' do handle_token(lexer, original, doc, :string, skip, <>, lines) end # We're done defp handle_token(_lexer, _original, doc, :eof, _skip, _data, _lines) do {:ok, doc} end # Anything else at top-level is invalid defp handle_token(_lexer, _original, _doc, type, skip, data, lines) do {:error, {:invalid_token, {type, data}}, skip, lines} end defp handle_key(lexer, original, doc, key) do with {:ok, {?=, _, _, _}} <- pop_skip(lexer, [:whitespace]), {:ok, value} <- value(lexer), {:ok, doc} <- Document.push_key(doc, key, value) do # Make sure key/value pairs are separated by a newline case peek_skip(lexer, [:whitespace]) do {:error, _, _, _} = err -> err {:ok, {:comment, _, _, _}} -> # Implies newline do_parse(lexer, original, doc) {:ok, {type, _, _, _}} when type in [:newline, :eof] -> do_parse(lexer, original, doc) {:ok, {type, skip, data, lines}} -> {:error, {:expected, :newline, {type, data}}, skip, lines} end else {:error, _, _, _} = err -> err {:ok, {type, skip, data, lines}} -> {:error, {:expected, ?=, {type, data}}, skip, lines} end end defp handle_table(lexer, original, doc) do # Guaranteed to have an open bracket with {:ok, {?\[, _, _, _}} <- pop_skip(lexer, [:whitespace]), {:ok, key, _line, _col} <- key(lexer), {:ok, {?\], _, _, _}} <- pop_skip(lexer, [:whitespace]), {:ok, doc} <- Document.push_table(doc, key) do # Make sure table opening is followed by newline case peek_skip(lexer, [:whitespace]) do {:error, _, _, _} = err -> err {:ok, {:comment, _, _, _}} -> do_parse(lexer, original, doc) {:ok, {type, _, _, _}} when type in [:newline, :eof] -> do_parse(lexer, original, doc) {:ok, {type, skip, data, lines}} -> {:error, {:expected, :newline, {type, data}}, skip, lines} end else {:error, _, _, _} = err -> err {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end end defp handle_table_array(lexer, original, doc) do # Guaranteed to have two open brackets with {:ok, {?\[, _, _, _}} <- pop_skip(lexer, [:whitespace]), {:ok, {?\[, _, _, _}} <- pop_skip(lexer, [:whitespace]), {:ok, key, _, _} <- key(lexer), {_, {:ok, {?\], _, _, _}}} <- {:close, pop_skip(lexer, [:whitespace])}, {_, {:ok, {?\], _, _, _}}} <- {:close, pop_skip(lexer, [:whitespace])}, {:ok, doc} <- Document.push_table_array(doc, key) do # Make sure table opening is followed by newline case peek_skip(lexer, [:whitespace]) do {:error, _, _, _} = err -> err {:ok, {:comment, _, _, _}} -> do_parse(lexer, original, doc) {:ok, {type, _, _, _}} when type in [:newline, :eof] -> do_parse(lexer, original, doc) {:ok, {type, skip, data, lines}} -> {:error, {:expected, :newline, {type, data}}, skip, lines} end else {:error, _, _, _} = err -> err {_, {:error, _, _, _} = err} -> err {:close, {:ok, {type, skip, data, lines}}} -> {:error, {:unclosed_table_array_name, {type, data}}, skip, lines} end end defp maybe_integer(lexer) do case pop_skip(lexer, [:whitespace]) do {:ok, {type, _skip, _data, _lines}} when type in '-+' -> # Can be integer, float case Lexer.peek(lexer) do {:error, _, _, _} = err -> err {:ok, {:digits, _, d, _}} -> # Appears to be integer or float, continue by accumulating Lexer.advance(lexer) maybe_integer(lexer, [d, type]) {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end {:ok, {:digits, skip, <> = d, lines} = token} -> # Could be a datetime case Lexer.peek(lexer) do {:ok, {?-, _, _, _}} -> # This is a date or datetime Lexer.push(lexer, token) maybe_datetime(lexer) {:ok, {?., _, _, _}} -> # Float Lexer.advance(lexer) float(lexer, ?., [?., d]) {:ok, {:alpha, <>}, _, _} when c in 'eE' -> # Float Lexer.advance(lexer) float(lexer, ?e, [?e, ?0, ?., d]) {:ok, {?_, _, _, _}} -> # Integer maybe_integer(lexer, [d]) _ -> # Just an integer if leader == ?0 do # Leading zeroes not allowed {:error, {:invalid_integer, :leading_zero}, skip, lines} else {:ok, String.to_integer(d)} end end {:ok, {:digits, skip, <> = d, lines} = token} -> # Could be a time case Lexer.peek(lexer) do {:ok, {?:, _, _, _}} -> # This is a local time Lexer.push(lexer, token) time(lexer) _ -> # It's just an integer if leader == ?0 do # Leading zeros not allowed {:error, {:invalid_integer, :leading_zero}, skip, lines} else {:ok, String.to_integer(d)} end end {:ok, {:digits, _, d, _}} -> # Just a integer or float maybe_integer(lexer, [d]) {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end end defp maybe_integer(lexer, parts) do case Lexer.pop(lexer) do {:error, _, skip, lines} -> # Integer with {:ok, _} = result <- iodata_to_integer(parts) do result else {:error, reason} -> {:error, reason, skip, lines} end {:ok, {?., _, _, _}} -> # Float float(lexer, ?., [?. | parts]) {:ok, {:alpha, _, <>, _}} when c in 'eE' -> # Float, need to add .0 before e, or String.to_float fails float(lexer, ?e, [?e, ?0, ?. | parts]) {:ok, {?_, _, _, _}} -> case Lexer.peek(lexer) do {:ok, {:digits, _, d, _}} -> # Allowed, just skip the underscore Lexer.advance(lexer) maybe_integer(lexer, [d | parts]) {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} {:error, _, _, _} = err -> err end {:ok, {_, skip, _, lines} = token} -> # Just an integer Lexer.push(lexer, token) with {:ok, _} = result <- iodata_to_integer(parts) do result else {:error, reason} -> {:error, reason, skip, lines} end end end defp float(lexer, signal, [last | _] = parts) do case Lexer.pop(lexer) do {:error, _, _, _} = err -> err {:ok, {?., skip, _, lines}} -> # Always an error at this point, as either duplicate or after E {:error, {:invalid_float, {?., 0}}, skip, lines} {:ok, {sign, _, _, _}} when sign in '-+' and last == ?e -> # +/- are allowed after e/E float(lexer, signal, [sign | parts]) {:ok, {:alpha, _, <>, _}} when c in 'eE' and signal == ?. -> # Valid if after a dot float(lexer, ?e, [?e | parts]) {:ok, {?_, skip, _, lines}} when last not in '_e.' -> # Valid only when surrounded by digits with {:ok, {:digits, _, d, _}} <- Lexer.peek(lexer), _ = Lexer.advance(lexer) do float(lexer, signal, [d | parts]) else {:error, _, _, _} = err -> err {:ok, {_, _, _, _}} -> {:error, {:invalid_float, {?_, 0}}, skip, lines} end {:ok, {:digits, _, d, _}} -> float(lexer, signal, [d | parts]) {:ok, {type, skip, data, lines}} when last in 'e.' -> # Incomplete float {:error, {:invalid_float, {type, data}}, skip, lines} {:ok, {_type, skip, _data, lines} = token} when last not in '_e.' -> # Done Lexer.push(lexer, token) with {:ok, _} = result <- iodata_to_float(parts) do result else {:error, reason} -> {:error, reason, skip, lines} end {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end end defp time(lexer) do # At this point we know we have at least HH: with {:ok, {:digits, skip, <<_::utf8, _::utf8>> = hh, lines}} <- Lexer.pop(lexer), {:ok, {?:, _, _, _}} <- Lexer.pop(lexer), {:ok, {:digits, _, <<_::utf8, _::utf8>> = mm, _}} <- Lexer.pop(lexer), {:ok, {?:, _, _, _}} <- Lexer.pop(lexer), {:ok, {:digits, _, <<_::utf8, _::utf8>> = ss, _}} <- Lexer.pop(lexer) do # Check for fractional parts = [ss, ?:, mm, ?:, hh] parts = case Lexer.peek(lexer) do {:ok, {?., _, _, _}} -> Lexer.advance(lexer) case Lexer.pop(lexer) do {:ok, {:digits, _, d, _}} -> [d, ?. | parts] {:ok, {type, skip, data, lines}} -> # Invalid throw {:error, {:invalid_token, {type, data}}, skip, lines} {:error, reason, skip, lines} -> throw {:error, {:invalid_fractional_seconds, reason}, skip, lines} end {:ok, _} -> parts end case Time.from_iso8601(iodata_to_str(parts)) do {:ok, _} = result -> result {:error, :invalid_time} -> {:error, :invalid_time, skip, lines} {:error, reason} -> {:error, {:invalid_time, reason}, skip, lines} end else {:error, _, _, _} = err -> err {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end catch :throw, {:error, _, _, _} = err -> err end defp maybe_datetime(lexer) do # At this point we have at least YYYY- with {:ok, {:digits, _, <<_::utf8,_::utf8,_::utf8,_::utf8>> = yy, _}} <- Lexer.pop(lexer), {:ok, {?-, _, _, _}} <- Lexer.pop(lexer), {:ok, {:digits, _, <<_::utf8,_::utf8>> = mm, _}} <- Lexer.pop(lexer), {:ok, {?-, _, _, _}} <- Lexer.pop(lexer), {:ok, {:digits, skip, <<_::utf8, _::utf8>> = dd, lines}} <- Lexer.pop(lexer) do # At this point we have a full date, check for time case Lexer.pop(lexer) do {:ok, {:alpha, _, "T", _}} -> # Expecting a time with {:ok, time} <- time(lexer) do datetime(lexer, [dd, ?-, mm, ?-, yy], time) end {:ok, {:whitespace, _, _, _}} -> case Lexer.peek(lexer) do {:ok, {:digits, _, <<_::utf8, _::utf8>>, _}} -> # Expecting a time with {:ok, time} <- time(lexer) do datetime(lexer, [dd, ?-, mm, ?-, yy], time) end _ -> # Just a date case Date.from_iso8601(iodata_to_str([dd, ?-, mm, ?-, yy])) do {:ok, _} = result -> result {:error, :invalid_date} -> {:error, :invalid_date, skip, lines} {:error, reason} -> {:error, {:invalid_date, reason}, skip, lines} end end {:ok, {_type, skip, _data, lines} = token} -> # Just a date Lexer.push(lexer, token) case Date.from_iso8601(iodata_to_str([dd, ?-, mm, ?-, yy])) do {:ok, _} = result -> result {:error, :invalid_date} -> {:error, :invalid_date, skip, lines} {:error, reason} -> {:error, {:invalid_date, reason}, skip, lines} end end else {:error, _, _, _} = err -> err {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end end defp datetime(lexer, parts, time) do # At this point we have at least YYYY-mm-dd and a fully parsed time with {:ok, date} <- Date.from_iso8601(iodata_to_str(parts)), {:ok, naive} <- NaiveDateTime.new(date, time) do # We just need to check for Z or UTC offset case Lexer.pop(lexer) do {:ok, {:alpha, skip, "Z", lines}} -> case DateTime.from_naive(naive, "Etc/UTC") do {:error, reason} -> {:error, {:invalid_datetime, reason}, skip, lines} {:ok, _} = result -> result end {:ok, {sign, skip, _, lines}} when sign in '-+' -> # We have an offset with {:ok, {:digits, _, <<_::utf8,_::utf8>> = hh, _}} <- Lexer.pop(lexer), {:ok, {?:, _, _, _}} <- Lexer.pop(lexer), {:ok, {:digits, _, <<_::utf8,_::utf8>> = mm, _}} <- Lexer.pop(lexer) do # Shift naive to account for offset hours = String.to_integer(hh) mins = String.to_integer(mm) offset = (hours * 60 * 60) + (mins * 60) naive = case sign do ?- -> NaiveDateTime.add(naive, offset * -1, :second) ?+ -> NaiveDateTime.add(naive, offset, :second) end case DateTime.from_naive(naive, "Etc/UTC") do {:error, reason} -> {:error, {:invalid_datetime, reason}, skip, lines} {:ok, _} = result -> result end else {:error, _, _, _} = err -> err {:ok, {type, skip, data, lines}} -> {:error, {:invalid_datetime_offset, {type, data}}, skip, lines} end {:ok, {type, _, _, _}} when type in [:eof, :whitespace, :newline] -> # Just a local date/time {:ok, naive} end else {:error, _, _, _} = err -> err {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end end # Allowed values # - Array # - Inline table # - Integer (in all forms) # - Float # - String # - DateTime defp value(lexer) do case peek_skip(lexer, [:whitespace]) do {:error, _, _, _} = err -> err {:ok, {:comment, _, _, _}} -> Lexer.advance(lexer) value(lexer) {:ok, {?\[, skip, _, lines}} -> # Need to embellish some errors with line/col with {:ok, _} = ok <- array(lexer) do ok else {:error, _, _, _} = err -> err {:error, {:invalid_array, _} = reason} -> {:error, reason, skip, lines} end {:ok, {?\{, _, _, _}} -> inline_table(lexer) {:ok, {:hex, _, v, _}} -> Lexer.advance(lexer) {:ok, String.to_integer(v, 16)} {:ok, {:octal, _, v, _}} -> Lexer.advance(lexer) {:ok, String.to_integer(v, 8)} {:ok, {:binary, _, v, _}} -> Lexer.advance(lexer) {:ok, String.to_integer(v, 2)} {:ok, {true, _, _, _}} -> Lexer.advance(lexer) {:ok, true} {:ok, {false, _, _, _}} -> Lexer.advance(lexer) {:ok, false} {:ok, {type, _, v, _}} when type in [:string, :multiline_string] -> Lexer.advance(lexer) {:ok, v} {:ok, {sign, _, _, _}} when sign in '-+' -> maybe_integer(lexer) {:ok, {:digits, skip, <>, lines}} when byte_size(rest) > 0 -> {:error, {:invalid_integer, :leading_zero}, skip, lines} {:ok, {:digits, _, _, _}} -> maybe_integer(lexer) {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end end defp array(lexer) do with {:ok, {?\[, skip, _, lines}} <- pop_skip(lexer, [:whitespace]), {:ok, elements} <- accumulate_array_elements(lexer), {:valid?, true} <- {:valid?, valid_array?(elements)}, {_, _, {:ok, {?\], _, _, _}}} <- {:close, {skip, lines}, pop_skip(lexer, [:whitespace, :newline, :comment])} do {:ok, elements} else {:error, _, _, _} = err -> err {:close, {:error, _, _, _} = err} -> err {:close, {_oline, _ocol} = opened, {:ok, {_, eskip, _, elines}}} -> {:error, {:unclosed_array, opened}, eskip, elines} {:valid?, err} -> err end end defp valid_array?([]), do: true defp valid_array?([h | t]), do: valid_array?(t, typeof(h)) defp valid_array?([], _type), do: true defp valid_array?([h | t], type) do if typeof(h) == type do valid_array?(t, type) else {:error, {:invalid_array, {:expected_type, t, h}}} end end defp typeof(v) when is_integer(v), do: :integer defp typeof(v) when is_float(v), do: :float defp typeof(v) when is_binary(v), do: :string defp typeof(%Time{}), do: :time defp typeof(%Date{}), do: :date defp typeof(%DateTime{}), do: :datetime defp typeof(%NaiveDateTime{}), do: :datetime defp typeof(v) when is_list(v), do: :list defp typeof(v) when is_map(v), do: :map defp inline_table(lexer) do with {:ok, {?\{, skip, _, lines}} <- pop_skip(lexer, [:whitespace]), {:ok, elements} <- accumulate_table_elements(lexer), {_, _, {:ok, {?\}, _, _, _}}} <- {:close, {skip, lines}, pop_skip(lexer, [:whitespace])} do {:ok, elements} else {:error, _, _, _} = err -> err {:close, {:error, _, _, _} = err} -> err {:close, {_oskip, _olines} = opened, {:ok, {_, eskip, _, elines}}} -> {:error, {:unclosed_inline_table, opened}, eskip, elines} end end defp accumulate_array_elements(lexer) do accumulate_array_elements(lexer, []) end defp accumulate_array_elements(lexer, acc) do with {:ok, {type, _, _, _}} <- peek_skip(lexer, [:whitespace, :newline, :comment]), {_, false} <- {:trailing_comma, type == ?\]}, {:ok, value} <- value(lexer), {:ok, {next, _, _, _}} <- peek_skip(lexer, [:whitespace]) do if next == ?, do Lexer.advance(lexer) accumulate_array_elements(lexer, [value | acc]) else {:ok, [value | acc]} end else {:error, _, _, _} = err -> err {:trailing_comma, true} -> {:ok, acc} end end defp accumulate_table_elements(lexer) do accumulate_table_elements(lexer, %{}) end defp accumulate_table_elements(lexer, acc) do with {:ok, {type, _, _, _}} <- peek_skip(lexer, [:whitespace, :newline, :comment]), {_, false} <- {:trailing_comma, type == ?\}}, {:ok, key, skip, lines} <- key(lexer), {_, _, false, _, _} <- {:key_exists, key, Map.has_key?(acc, key), skip, lines}, {:ok, {?=, _, _, _}} <- pop_skip(lexer, [:whitespace, :comments]), {:ok, value} <- value(lexer), {_, {:ok, acc2}} <- {key, Document.push_key_into_table(acc, key, value)}, {:ok, {next, _, _, _}} <- peek_skip(lexer, [:whitespace, :comments]) do if next == ?, do Lexer.advance(lexer) accumulate_table_elements(lexer, acc2) else {:ok, acc2} end else {:error, _, _, _} = err -> err {:key_exists, key, true, line, col} -> {:error, {:key_exists, key}, line, col} {table, {:error, :key_exists}} -> {:error, {:key_exists_in_table, table}, -1, -1} {:trailing_comma, true} -> {:ok, acc} {:ok, {type, skip, data, lines} = token} -> Lexer.push(lexer, token) {:error, {:invalid_key_value, {type, data}}, skip, lines} end end defp key(lexer) do result = case pop_skip(lexer, [:whitespace]) do {:error, _, _, _} = err -> err {:ok, {type, skip, s, lines}} when type in [:digits, :alpha, :string] -> {key(lexer, s, []), skip, lines} {:ok, {type, skip, _, lines}} when type in '-_' -> {key(lexer, <>, []), skip, lines} {:ok, {type, skip, data, lines} = token} -> Lexer.push(lexer, token) {:error, {:invalid_token, {type, data}}, skip, lines} end case result do {:error, _, _, _} = err -> err {{:ok, key}, skip, lines} -> {:ok, key, skip, lines} {{:error, _, _, _} = err, _, _} -> err end end defp key(lexer, word, acc) do case Lexer.peek(lexer) do {:error, _, _, _} = err -> err {:ok, {type, _, s, _}} when type in [:digits, :alpha, :string] -> Lexer.advance(lexer) key(lexer, word <> s, acc) {:ok, {type, _, _, _}} when type in '-_' -> Lexer.advance(lexer) key(lexer, word <> iodata_to_str([type]), acc) {:ok, {?., _, _, _}} -> Lexer.advance(lexer) case Lexer.peek(lexer) do {:error, _, _, _} = err -> err {:ok, {type, _, _, _}} when type in [:digits, :alpha, :string] -> key(lexer, "", [word | acc]) {:ok, {type, _, _, _}} when type in '-_' -> key(lexer, "", [word | acc]) {:ok, {type, skip, data, lines}} -> {:error, {:invalid_token, {type, data}}, skip, lines} end {:ok, _} -> {:ok, Enum.reverse([word | acc])} end end defp iodata_to_integer(data) do case iodata_to_str(data) do <> when byte_size(rest) > 0 -> {:error, {:invalid_integer, :leading_zero}} <> when (sign == ?- or sign == ?+) and byte_size(rest) > 0 -> {:error, {:invalid_integer, :leading_zero}} s -> {:ok, String.to_integer(s)} end end defp iodata_to_float(data) do case iodata_to_str(data) do <> when next != ?. -> {:error, {:invalid_float, :leading_zero}} <> when (sign == ?- or sign == ?+) and next != ?. -> {:error, {:invalid_float, :leading_zero}} s -> {:ok, String.to_float(s)} end end defp iodata_to_str(parts) do parts |> Enum.reverse |> IO.iodata_to_binary() end defp pop_skip(lexer, skip) do case Lexer.pop(lexer) do {:error, _, _, _} = err -> err {:ok, {type, _, _, _}} = result -> if :lists.member(type, skip) do pop_skip(lexer, skip) else result end end end defp peek_skip(lexer, skip) do case Lexer.peek(lexer) do {:error, _, _, _} = err -> err {:ok, {type, _, _, _}} = result -> if :lists.member(type, skip) do Lexer.advance(lexer) peek_skip(lexer, skip) else result end end end end