Packages
fnord
0.9.23
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/external_configs/frontmatter.ex
defmodule ExternalConfigs.Frontmatter do
@moduledoc """
Parses YAML frontmatter out of markdown-like documents.
Supported input:
---
key: value
other: [a, b]
---
Body markdown goes here.
Returns `{:ok, %{frontmatter: map(), body: binary()}}` on success. Inputs
that do not contain a complete `---` ... `---` block (no opener, or an
opener without a closer) are returned as body with an empty frontmatter
map. The original content is preserved verbatim so callers can still
display or re-parse it.
"""
@type parsed :: %{frontmatter: map(), body: binary()}
@spec parse(binary()) :: {:ok, parsed} | {:error, term()}
def parse(content) when is_binary(content) do
case split(content) do
{:no_frontmatter, body} ->
{:ok, %{frontmatter: %{}, body: body}}
{:ok, yaml, body} ->
case decode_yaml(yaml) do
{:ok, map} -> {:ok, %{frontmatter: map, body: body}}
{:error, reason} -> {:error, {:invalid_yaml, reason}}
end
end
end
# Splits the input into (yaml, body). Recognizes a leading `---` fence
# (optionally preceded by a UTF-8 BOM and/or blank lines) and the closing
# `---` fence that terminates the frontmatter block.
@spec split(binary()) ::
{:ok, binary(), binary()} | {:no_frontmatter, binary()}
defp split(content) do
stripped = strip_bom(content)
case String.split(stripped, ~r/\r?\n/, parts: 2) do
[first, rest] ->
if fence?(first) do
case split_body(rest) do
{:ok, yaml, body} -> {:ok, yaml, body}
:no_close -> {:no_frontmatter, content}
end
else
{:no_frontmatter, content}
end
_ ->
{:no_frontmatter, content}
end
end
defp fence?(line), do: String.trim(line) == "---"
defp split_body(rest) do
lines = String.split(rest, ~r/\r?\n/)
do_split_body(lines, [])
end
defp do_split_body([], _acc), do: :no_close
defp do_split_body([line | tail], acc) do
if fence?(line) do
yaml = acc |> Enum.reverse() |> Enum.join("\n")
body = Enum.join(tail, "\n")
{:ok, yaml, body}
else
do_split_body(tail, [line | acc])
end
end
defp strip_bom(<<0xEF, 0xBB, 0xBF, rest::binary>>), do: rest
defp strip_bom(other), do: other
defp decode_yaml("") do
{:ok, %{}}
end
defp decode_yaml(yaml) do
case YamlElixir.read_from_string(yaml) do
{:ok, map} when is_map(map) -> {:ok, map}
# Empty YAML documents can decode to nil; treat that as empty frontmatter.
{:ok, nil} -> {:ok, %{}}
{:ok, other} -> {:error, {:not_a_mapping, other}}
{:error, reason} -> {:error, reason}
end
end
end