towerops/lib/towerops_web/changelog_parser.ex
Graham McIntie 608bad0cf6 Add What's New changelog page
- ChangelogParser: parses priv/static/changelog.txt into structured entries
- ChangelogLive: timeline view with DaisyUI styling
- Route at /changelog in authenticated scope
- What's New link in org dropdown menu
2026-02-13 19:01:19 -06:00

48 lines
1.5 KiB
Elixir

defmodule ToweropsWeb.ChangelogParser do
@moduledoc "Parses priv/static/changelog.txt into structured entries."
@doc "Returns a list of %{date: Date.t(), title: String.t(), items: [String.t()]}"
def parse do
path = Application.app_dir(:towerops, "priv/static/changelog.txt")
case File.read(path) do
{:ok, content} -> parse_content(content)
{:error, _} -> []
end
end
defp parse_content(content) do
content
|> String.split("\n")
|> Enum.reduce([], fn line, acc ->
cond do
# Date header with title: "2026-02-13 — Title"
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s*[—–-]\s*.+/, line) ->
[date_str, title] = String.split(line, ~r/\s*[—–-]\s*/, parts: 2)
entry = %{date: Date.from_iso8601!(String.trim(date_str)), title: String.trim(title), items: []}
[entry | acc]
# Date header without title: "2026-02-13"
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s*$/, line) ->
date = Date.from_iso8601!(String.trim(line))
entry = %{date: date, title: nil, items: []}
[entry | acc]
# Bullet point
String.starts_with?(String.trim(line), "* ") ->
case acc do
[current | rest] ->
item = String.trim(line) |> String.trim_leading("* ")
[%{current | items: current.items ++ [item]} | rest]
[] ->
acc
end
true ->
acc
end
end)
|> Enum.reverse()
end
end