towerops/lib/towerops_web/changelog_parser.ex
2026-03-29 11:03:20 -05:00

103 lines
2.7 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule ToweropsWeb.ChangelogParser do
@moduledoc """
Reads and parses priv/static/changelog.txt into structured entries.
Parses changelog entries with format:
- "YYYY-MM-DD — Title" or "YYYY-MM-DD Title" (with em/en dash)
- "YYYY-MM-DD" (date only, no title)
- "* Item text" (bullet items belonging to the most recent date)
"""
@doc "Returns a list of %{date: Date.t(), title: String.t() | nil, items: [String.t()]}"
def parse do
path = Application.app_dir(:towerops, "priv/static/changelog.txt")
case File.read(path) do
{:ok, content} ->
content
|> parse_content()
|> Enum.map(&convert_entry/1)
{:error, _} ->
[]
end
end
@doc """
Parse changelog content string into a list of entries.
Returns internal tuples: {:changelog_entry, date, title_opt, items}
"""
def parse_content(content) when is_binary(content) do
content
|> String.split("\n")
|> Enum.reduce([], fn line, acc -> parse_line(line, acc) end)
|> Enum.reverse()
|> Enum.map(fn entry ->
{:changelog_entry, entry.date, entry.title, Enum.reverse(entry.items)}
end)
end
# Parse a single line and update the accumulator
defp parse_line(line, acc) do
# Check for date with title: "YYYY-MM-DD [—–] Title"
case Regex.run(~r/^(\d{4}-\d{2}-\d{2})\s+(—|)\s+(.+)/u, line) do
[_, date_str, _dash, title] ->
add_date_entry(acc, date_str, {:some, String.trim(title)})
nil ->
parse_line_no_title(line, acc)
end
end
defp parse_line_no_title(line, acc) do
cond do
# Check for date only: "YYYY-MM-DD"
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s*$/, line) ->
add_date_entry(acc, line, :none)
# Check for bullet item: "* Item text"
String.starts_with?(String.trim(line), "* ") ->
add_item_to_current_entry(acc, line)
# Non-matching line - ignore
true ->
acc
end
end
defp add_date_entry(acc, date_str, title) do
[
%{
date: String.trim(date_str),
title: title,
items: []
}
| acc
]
end
defp add_item_to_current_entry(acc, line) do
trimmed = String.trim(line)
item = String.slice(trimmed, 2..-1//1)
case acc do
[current | rest] ->
[%{current | items: [item | current.items]} | rest]
[] ->
# Orphan item before any date header - ignore
[]
end
end
defp convert_entry({:changelog_entry, date_str, title_opt, items}) do
%{
date: Date.from_iso8601!(date_str),
title: unwrap_option(title_opt),
items: items
}
end
defp unwrap_option({:some, value}), do: value
defp unwrap_option(:none), do: nil
end