Current section
Files
Jump to
Current section
Files
lib/yarn_parser.ex
defmodule YarnParser do
import NimbleParsec
newline =
choice([
concat(ascii_char([?\r]), ascii_char([?\n])),
ascii_char([?\n, ?\r])
])
|> tag(:newline)
comment =
ascii_char([?#])
|> concat(
repeat(
lookahead_not(newline)
|> utf8_char([])
)
)
|> lookahead(newline)
|> tag(:comment)
indent =
ascii_string([?\s], min: 1)
|> post_traverse({:process_indent, []})
|> tag(:indent)
defp process_indent(_rest, [indent], context, _line, _offset) do
len = String.length(indent)
level = div(len, 2)
block_level = Map.get(context, :block_level, 1)
if rem(len, 2) > 0 || level > block_level do
{:error, "Invalid indentation"}
else
context = if level < block_level do
Map.put(context, :block_level, block_level - 1)
end || context
{[{:indent, level}], context}
end
end
quoted_string =
ignore(ascii_char([?"]))
|> repeat(
lookahead_not(ascii_char([?"]))
|> utf8_char([])
)
|> ignore(ascii_char([?"]))
unquoted_string =
ascii_char([?a..?z, ?A..?Z, ?/, ?., ?-])
|> optional(repeat(
lookahead_not(ascii_char([?:, ?\s, ?\n, ?\r, ?,]))
|> ascii_char([])
))
string =
choice([quoted_string, unquoted_string])
|> tag(:string)
number =
integer(min: 1)
|> unwrap_and_tag(:number)
boolean =
choice([
string("true"),
string("false")
])
|> post_traverse({:parse_boolean, []})
|> unwrap_and_tag(:boolean)
value =
choice([boolean, number, string])
|> tag(:value)
defp parse_boolean(_, [text], context, _, _) do
bool = case text do
"true" -> true
"false" -> false
end
{[bool], context}
end
colon =
ascii_char([?:])
|> tag(:colon)
comma =
ascii_char([?,])
|> tag(:comma)
whitespace = ignore(ascii_char([?\s]))
defcombinatorp :keysequence,
choice([
string
|> concat(ignore(comma))
|> optional(whitespace)
|> concat(parsec(:keysequence)),
string
])
defcombinatorp :key,
parsec(:keysequence)
|> tag(:key)
defcombinatorp :keyvalue,
parsec(:key)
|> optional(whitespace)
|> concat(value)
|> optional(ignore(newline))
|> tag(:keyvalue)
defcombinatorp :blockstart,
parsec(:key)
|> ignore(colon)
|> ignore(newline)
|> post_traverse({:parse_blockstart, []})
|> tag(:block_key)
defp parse_blockstart(_, args, context, _, _) do
block_level = Map.get(context, :block_level, 1)
context = Map.put(context, :block_level, block_level + 1)
{args, context}
end
defcombinatorp :block,
parsec(:blockstart)
|> repeat(
indent
|> choice([
parsec(:block),
parsec(:keyvalue)
])
)
|> tag(:block)
defparsec :parse_input,
repeat(choice([
parsec(:block),
parsec(:keyvalue),
comment,
ignore(newline)
]))
def parse(input) do
with {:ok, parsed, "", _, _, _} <- parse_input(input) do
{:ok, format(parsed)}
else
_ -> {:error, :invalid_format}
end
end
defp format(elems) do
Enum.reduce(elems, %{}, fn
{:comment, value}, acc ->
value = List.to_string(value)
if Map.has_key?(acc, "comments") do
%{acc | "comments" => acc["comments"] ++ [value]}
else
Map.put(acc, "comments", [value])
end
{:indent, _}, acc -> acc
{:keyvalue, val}, acc ->
keys = Keyword.get(val, :key)
[value] = Keyword.get(val, :value)
for {_type, key} <- keys, into: %{} do
{"#{key}", format_value(value)}
end
|> Map.merge(acc)
{:block, subelems}, acc ->
[{:block_key, key: keys} | elems] = subelems
for {_type, key} <- keys, into: %{} do
{"#{key}", format(elems)}
end
|> Map.merge(acc)
end)
end
defp format_value({:string, val}), do: List.to_string(val)
defp format_value({_type, val}), do: val
end