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 | rest] = String.split(line, ~r/\s+[—–]\s+/, parts: 2) title = List.first(rest, "") 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), "* ") -> add_item_to_current(acc, line |> String.trim() |> String.trim_leading("* ")) true -> acc end end) |> Enum.reverse() end defp add_item_to_current([current | rest], item), do: [%{current | items: current.items ++ [item]} | rest] defp add_item_to_current([], _item), do: [] end