defmodule Yameru.Parser.Literal do @moduledoc """ Scalar, Block, and Tuple parser """ use Pelecanus, sigil_p: true alias Yameru.Parser.IndentLevel alias Yameru.Parser.Dictionary alias Yameru.Parser.Sequence alias Yameru.Parser.Text @doc """ Parses scalar; inline value. """ @spec scalar() :: Pelecanus.parser() def scalar do choice([ special(), double_quoted(), single_quoted(), plain_string() ]) end @doc """ Parses string sandwitched with double quote sign. """ @spec double_quoted() :: Pelecanus.parser() def double_quoted do seq = sequence([ ignore(~p/"/), ~p/(\\"|[^\r\n"])*/, ignore(~p/"/), ignore_trailing_spaces() ]) seq |> stlip() |> map(&Macro.unescape_string/1) end @doc """ Parses string sandwitched with single quote sign. """ @spec single_quoted() :: Pelecanus.parser() def single_quoted do seq = sequence([ ignore(~p/'/), ~p/(''|[^\r\n'])*/, ignore(~p/'/), ignore_trailing_spaces() ]) seq |> stlip() |> map(&String.replace(&1, "''", "'")) end @doc """ Parses plain_string. This function don't check that head isn't quote sign or bracket sign. """ @spec plain_string() :: Pelecanus.parser() def plain_string() do sequence([ not_predicate(~p/- +|-$/ms), not_predicate(~p/\/ *$/Ums), ~p/[^\r\n]*[^:\r\n]((?=: |:$|$)| +(?=\/ *$))/Ums ]) |> stlip() |> trim_trailing() end @doc """ Parses special scalar; sandwitched with brackets. """ @spec special() :: Pelecanus.parser() def special do # get string between brackets primary = sequence([ ignore(~p/\[/), ~p/[^\r\n\]]*/, ignore(~p/\]/), ignore_trailing_spaces() ]) |> stlip() # map it to non-string type secondary = choice([ boolean(), number(), iso8601() ]) fn state -> with {:ok, s, raw} <- primary.(state), {:ok, _, value} <- Pelecanus.parse(raw, secondary), do: {:ok, s, value} end end defp ignore_trailing_spaces do ignore(~p/ *(?=: |:$|$)| +(?=\/ *$)/Ums) end @doc """ Parses number (in brackets). Note that number literal is expected to follow SI. Decimal separator is comma or period. Digit group separator is space. """ @spec number() :: Pelecanus.parser() def number do formal = ~p/.*/ |> map(&String.replace(&1, " ", "")) choice([ formal |> try_convert(&String.to_integer/1), formal |> try_convert(&String.to_float/1) ]) end @doc """ Parses boolean value (yes / no) """ @spec boolean() :: Pelecanus.parser() def boolean do ~p/ *(yes|no) */ |> map(&String.trim/1) |> map(fn "yes" -> true "no" -> false end) end @doc """ Parses extended iso 8601 (in Elixir can parse) """ def iso8601 do formal = ~p/.*/ |> map(&String.trim/1) choice([ formal |> try_convert(&NaiveDateTime.from_iso8601!/1), formal |> try_convert(&Date.from_iso8601!/1), formal |> try_convert(&Time.from_iso8601!/1) ]) end @spec try_convert(Pelecanus.parser(), (Pelecanus.value() -> Pelecanus.value())) :: Pelecanus.parser() defp try_convert(e, fun) do fn state -> try do map(e, fun).(state) rescue _error -> {:error, "fails to convertion"} end end end @doc """ Parses any block in +1 indent level. """ @spec block() :: Pelecanus.parser() def block do choice([ dict_block(), sequence_block(), text_block() ]) end @doc false @spec dict_block() :: Pelecanus.parser() def dict_block do x_block(~p//, {Dictionary, :dictionary, []}) end @doc false @spec sequence_block() :: Pelecanus.parser() def sequence_block do x_block(~p//, {Sequence, :sequence, []}) end @doc false @spec text_block() :: Pelecanus.parser() def text_block do x_block(~p/\/ */, {Text, :text, []}) end defp x_block(delimiter, {mod, name, args}) do sequence([ ignore(with_line_end(delimiter)), IndentLevel.block(mod, name, args) ]) |> stlip() end @doc """ Parses tuple and scalar or block. In this parser any value is tuple. Just scalar is mapped to `{scalar, nil}` and just block is mapped to `{nil, block}`. When both exist, its are mapped to `{scalar, nil}` Note that this function check that either scalar or block isn't nil. """ @spec tuple() :: Pelecanus.parser() def tuple do sequence([ choice([ scalar(), ~p// |> map(fn _ -> nil end) ]), choice([ block(), ~p// |> with_line_end() |> map(fn _ -> nil end) ]) ]) |> map(fn [s, b] -> {s, b} end) |> validate(&(&1 != {nil, nil})) end @doc """ Discards line break on the heels of given parser consumed. """ @spec with_line_end(Pelecanus.parser()) :: Pelecanus.parser() def with_line_end(e) do sequence([e, ignore(~p/(\R|\z)/)]) |> stlip() end @doc """ Consumes empty (only spaces) line and replace with empty string. """ @spec empty_line() :: Pelecanus.parser() def empty_line do ~p/ *$/Ums |> with_line_end() |> map(fn _ -> "" end) end defp trim_trailing(e) do e |> map(&trim/1) end defp trim(string) do String.replace(string, ~r/ *$/, "") end end