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