Current section
Files
Jump to
Current section
Files
lib/yameru/parser/literal.ex
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