defmodule PhoenixKitWeb.Live.Modules.Blogging.Metadata do @moduledoc """ Metadata helpers for .phk (PhoenixKit) blogging posts. Metadata is stored as a simple key-value format at the top of the file: ``` --- slug: home title: Welcome status: published published_at: 2025-10-29T18:48:00Z --- Content goes here... ``` """ @type metadata :: %{ status: String.t(), title: String.t(), description: String.t() | nil, slug: String.t(), published_at: String.t(), created_at: String.t() | nil, created_by_id: String.t() | nil, created_by_email: String.t() | nil, updated_by_id: String.t() | nil, updated_by_email: String.t() | nil } @doc """ Parses .phk content, extracting metadata from frontmatter and returning the content. """ @spec parse_with_content(String.t()) :: {:ok, metadata(), String.t()} | {:error, atom()} def parse_with_content(content) do case extract_frontmatter(content) do {:ok, metadata, body_content} -> {:ok, metadata, body_content} {:error, _} -> # Fallback: try old XML format for backwards compatibility metadata = extract_metadata_from_xml(content) {:ok, metadata, content} end end @doc """ Serializes metadata as YAML-style frontmatter. """ @spec serialize(metadata()) :: String.t() def serialize(metadata) do optional_lines = [:created_at, :created_by_id, :created_by_email, :updated_by_id, :updated_by_email] |> Enum.flat_map(fn key -> case metadata_value(metadata, key) do nil -> [] "" -> [] value -> ["#{Atom.to_string(key)}: #{value}"] end end) lines = [ "slug: #{metadata.slug}", "title: #{metadata.title || ""}", "status: #{metadata.status}", "published_at: #{metadata.published_at}" ] |> Enum.concat(optional_lines) |> Enum.join("\n") """ --- #{lines} --- """ end @doc """ Returns default metadata for a new post. """ @spec default_metadata() :: metadata() def default_metadata do now = DateTime.utc_now() |> DateTime.truncate(:second) %{ status: "draft", title: "", description: nil, slug: "", published_at: DateTime.to_iso8601(now), created_at: nil, created_by_id: nil, created_by_email: nil, updated_by_id: nil, updated_by_email: nil } end # Extract metadata from YAML-style frontmatter defp extract_frontmatter(content) do case Regex.run(~r/^---\n(.*?)\n---\n(.*)$/s, content) do [_, frontmatter, body] -> metadata = parse_frontmatter_lines(frontmatter) {:ok, metadata, String.trim(body)} _ -> {:error, :no_frontmatter} end end defp parse_frontmatter_lines(frontmatter) do default = default_metadata() lines = frontmatter |> String.split("\n") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) metadata = Enum.reduce(lines, %{}, fn line, acc -> case String.split(line, ":", parts: 2) do [key, value] -> Map.put(acc, String.trim(key), String.trim(value)) _ -> acc end end) metadata_map = %{ title: Map.get(metadata, "title", default.title), status: Map.get(metadata, "status", default.status), slug: Map.get(metadata, "slug", default.slug), published_at: Map.get(metadata, "published_at", default.published_at), description: Map.get(metadata, "description"), created_at: Map.get(metadata, "created_at", default.created_at), created_by_id: Map.get(metadata, "created_by_id", default.created_by_id), created_by_email: Map.get(metadata, "created_by_email", default.created_by_email), updated_by_id: Map.get(metadata, "updated_by_id", default.updated_by_id), updated_by_email: Map.get(metadata, "updated_by_email", default.updated_by_email) } metadata_map end # Extract metadata from element attributes (legacy XML format) defp extract_metadata_from_xml(content) do default = default_metadata() # Simple regex-based extraction (for now) title = extract_attribute(content, "title") || default.title status = extract_attribute(content, "status") || default.status slug = extract_attribute(content, "slug") || default.slug published_at = extract_attribute(content, "published_at") || default.published_at description = extract_attribute(content, "description") %{ title: title, status: status, slug: slug, published_at: published_at, description: description, created_at: nil, created_by_id: nil, created_by_email: nil, updated_by_id: nil, updated_by_email: nil } end defp extract_attribute(content, attr_name) do regex = ~r/]*\s#{attr_name}="([^"]*)"/ case Regex.run(regex, content) do [_, value] -> value _ -> nil end end defp metadata_value(metadata, key) do Map.get(metadata, key) || Map.get(metadata, Atom.to_string(key)) end end