Packages

Yet another static website generator written in Elixir with MD parser (forked from serum)

Current section

Files

Jump to
serum_md lib serum header_parser.ex
Raw

lib/serum/header_parser.ex

defmodule Serum.HeaderParser do
@moduledoc false
_moduledocp = """
This module takes care of parsing headers of page (or post) source files.
Header is where all page or post metadata goes into, and has the following
format:
```
---
key: value
...
---
```
where `---` in the first and last line delimits the beginning and the end of
the header area, and between these two lines are one or more key-value pair
delimited by a colon, where key is the name of a metadata and value is the
actual value of a metadata.
"""
alias Serum.HeaderParser.ValueTransformer
@type options :: [{atom, value_type}]
@type value_type :: :string | :integer | :datetime | {:list, value_type}
@type value :: binary | integer | DateTime.t() | [binary] | [integer] | [DateTime.t()]
@type parse_result :: {:ok, {map(), map(), binary()}} | {:invalid, binary()}
@typep extract_ok :: {:ok, [binary], binary}
@typep extract_err :: {:error, binary}
@doc """
Reads lines from a binary `data` and extracts the header into a map.
`options` is a keyword list which specifies the name and type of metadata the
header parser expects. So the typical `options` should look like this:
[key1: type1, key2: type2, ...]
See "Types" section for available value types.
`required` argument is a list of required keys (in atom). If the header parser
cannot find required keys in the header area, it returns an error.
## Types
Currently the HeaderParser supports following types:
* `:string` - A line of string. It can contain spaces.
* `:integer` - A decimal integer.
* `:datetime` - Date and time. Must be specified in the format of
`YYYY-MM-DD hh:mm:ss`. This data will be interpreted as a local time.
* `{:list, <type>}` - A list of multiple values separated by commas. Every
value must have the same type, either `:string`, `:integer`, or `:datetime`.
You cannot make a list of lists.
"""
@spec parse_header(binary(), options(), [atom()]) :: parse_result()
def parse_header(data, options, required \\ []) do
{:ok, header_lines, rest_data} = extract_header(data)
key_strings = options |> Keyword.keys() |> Enum.map(&to_string/1)
_req_strings = Enum.map(required, &to_string/1)
kv_lists =
header_lines
|> Enum.map(&split_kv/1)
|> Enum.group_by(&(elem(&1, 0) in key_strings))
%{
true: accepted_kv,
false: extra_kv
} = Map.merge(%{true: [], false: []}, kv_lists)
case transform_values(accepted_kv, options, []) do
{:ok, parsed} ->
extras =
Enum.map(extra_kv, fn {k, v} ->
{k, ValueTransformer.transform_value(k, v, :string)}
end)
{:ok, {Map.new(parsed), Map.new(extras), rest_data}}
error ->
handle_error(error)
end
end
@spec extract_header(binary) :: extract_ok | extract_err
defp extract_header(data) do
case String.split(data, ~r/^\s*---\s*$/m, parts: 3) do
["", header, rest] -> {:ok, header |> String.trim() |> String.split(~r/\r?\n/), rest}
_ -> {:ok, [], data}
end
end
@spec split_kv(binary) :: {binary, binary}
defp split_kv(line) do
line
|> String.split(":", parts: 2)
|> Enum.map(&String.trim/1)
|> case do
[k] -> {k, ""}
[k, v] -> {k, v}
end
end
# @spec find_missing([{binary(), binary()}], [binary()]) :: [binary()]
# defp find_missing(kv_list, req_strings) do
# kv_list |> Enum.map(&elem(&1, 0)) |> do_find_missing(req_strings)
# end
# @spec do_find_missing([binary], [binary], [binary]) :: [binary]
# defp do_find_missing(keys, required, acc \\ [])
# defp do_find_missing(_keys, [], acc), do: acc
# defp do_find_missing(keys, [h | t], acc) do
# if h in keys do
# do_find_missing(keys, t, acc)
# else
# do_find_missing(keys, t, [h | acc])
# end
# end
@spec transform_values([{binary, binary}], keyword(atom), keyword(value)) ::
{:error, binary} | {:ok, keyword(value)}
defp transform_values([], _options, acc) do
{:ok, acc}
end
defp transform_values([{k, v} | rest], options, acc) do
atom_k = String.to_existing_atom(k)
case ValueTransformer.transform_value(k, v, options[atom_k]) do
{:error, _} = error -> error
value -> transform_values(rest, options, [{atom_k, value} | acc])
end
end
@spec handle_error(term) :: {:invalid, binary()}
defp handle_error(term)
defp handle_error([missing]) do
{:invalid, "`#{missing}` is required, but it's missing"}
end
defp handle_error([_ | _] = missing) do
repr = missing |> Enum.map(&"`#{&1}`") |> Enum.reverse() |> Enum.join(", ")
{:invalid, "#{repr} are required, but they are missing"}
end
defp handle_error({:error, error}) do
{:invalid, "header parse error: #{error}"}
end
end