lib/bylaw/html/check/require_image_alt.ex

defmodule Bylaw.HTML.Check.RequireImageAlt do
  @moduledoc """
  Validates that rendered image tags define an `alt` attribute.

  This check inspects rendered `<img>` elements and flags images without `alt`.
  Use `alt=""` for decorative images.

  ## Examples

  Bad:

      <img src="/logo.svg">

  Why this is bad:

  An image without `alt` has no explicit accessible text alternative. Screen
  readers may announce the file path or omit useful content.

  Better:

      <img src="/logo.svg" alt="Company logo">

  Why this is better:

  The image has an accessible text alternative.

  Bad:

      <img src="/spacer.svg">

  Why this is bad:

  Decorative images should be intentionally hidden from assistive technology
  rather than left ambiguous.

  Better:

      <img src="/spacer.svg" alt="">

  Why this is better:

  Empty `alt` communicates that the image is decorative.

  ## Notes

  The check only verifies that an `alt` attribute is present. It allows
  `alt=""` because empty alt text is the expected markup for decorative images.
  It does not judge whether non-empty alt text is descriptive enough.

  This check runs on rendered HTML, so dynamic `alt` attributes are evaluated
  after rendering.

  ## Options

  This check has no check-specific options. Add the module directly to the
  explicit checks list:

      Bylaw.HTML.Check.RequireImageAlt

  ## Usage

  Add this module to the explicit check list passed through `Bylaw.HTML`.
  See `Bylaw.HTML` for the full rendered HTML validation setup.
  """

  @behaviour Bylaw.HTML.Check

  alias Bylaw.HTML.Issue

  @doc """
  Implements the `Bylaw.HTML.Check` validation callback.
  """
  @impl Bylaw.HTML.Check
  @spec validate(Bylaw.HTML.Check.context()) :: Bylaw.HTML.Check.result()
  def validate(%{document: document}) do
    document
    |> LazyHTML.query("img")
    |> Enum.filter(&missing_alt?/1)
    |> Enum.map(&issue_for/1)
    |> result()
  end

  def validate(context) do
    raise ArgumentError,
          "expected context to be a map with a parsed document, got: #{inspect(context)}"
  end

  defp missing_alt?(element) do
    element
    |> LazyHTML.attribute("alt")
    |> Enum.empty?()
  end

  defp issue_for(element) do
    %Issue{
      check: __MODULE__,
      message: ~s(expected <img> to define alt; use alt="" for decorative images),
      tag: "img",
      snippet: LazyHTML.to_html(element)
    }
  end

  defp result([]), do: :ok
  defp result(issues), do: {:error, issues}
end