lib/ratex.ex

defmodule Ratex do
  @moduledoc """
  This library provides an Elixir wrapper around the Rust
  [RaTeX](https://github.com/erweixin/RaTeX) library. This allows you
  render SVGs and PNGs of mathematical expressions right in Elixir.
  """

  alias Ratex.Error
  alias Ratex.Native
  alias Ratex.Options

  @all_opts [
    font_size: [
      doc: "The base font size in pixels.",
      type: :float,
      default: 32.0
    ],
    pixel_ratio: [
      doc: "The device pixel ratio for retina rasters.",
      type: :float,
      default: 2.0
    ],
    color: [
      doc: "The color of the glpyhs as hexadecimal (e.g. `#112233`).",
      type: :string,
      default: "#000000"
    ],
    inline: [
      doc: "Render the expression as inline text as opposed to display.",
      type: :boolean,
      default: false
    ],
    unicode_font_path: [
      doc:
        "The absolute path to a TTF font file for non-ASCII text that may be used inside of an `\\\\text{...}` expression. If a font path is not provided the underlying RaTeX Rust library will attempt to use the platform fallback.",
      type: :string
    ]
  ]

  @png_opts @all_opts
  @svg_opts Keyword.delete(@all_opts, :pixel_ratio)

  @type png_opts() :: unquote(NimbleOptions.option_typespec(@png_opts))

  @type svg_opts() :: unquote(NimbleOptions.option_typespec(@svg_opts))

  @doc """
  Generate a PNG binary from a given LaTeX math expression.

  ## Options

    #{NimbleOptions.docs(@png_opts)}
  """
  @spec render_png(expression :: String.t(), opts :: png_opts()) :: {:ok, binary()} | {:error, term()}
  def render_png(expression, opts \\ []) do
    with {:ok, opts} <- validate_opts(opts, @png_opts) do
      Native.render_png(expression, Options.new(opts))
    end
  end

  @doc """
  Same as `render_png/2` but raises on error.
  """
  @spec render_png!(expression :: String.t(), opts :: png_opts()) :: binary()
  def render_png!(expression, opts \\ []) do
    case render_png(expression, opts) do
      {:ok, png} ->
        png

      {:error, reason} ->
        raise "Ratex render failed: #{inspect(reason)}"
    end
  end

  @doc """
  Generate an SVG from a given LaTeX math expression.

  ## Options

    #{NimbleOptions.docs(@svg_opts)}
  """
  @spec render_svg(expression :: String.t(), opts :: svg_opts()) :: {:ok, binary()} | {:error, term()}
  def render_svg(expression, opts \\ []) do
    with {:ok, opts} <- validate_opts(opts, @svg_opts) do
      Native.render_svg(expression, Options.new(opts))
    end
  end

  @doc """
  Same as `render_svg/2` but raises on error.
  """
  @spec render_svg!(expression :: String.t(), opts :: svg_opts()) :: binary()
  def render_svg!(expression, opts \\ []) do
    case render_svg(expression, opts) do
      {:ok, svg} ->
        svg

      {:error, reason} ->
        raise "Ratex render failed: #{inspect(reason)}"
    end
  end

  defp validate_opts(opts, schema) do
    case NimbleOptions.validate(opts, schema) do
      {:ok, _validated_opts} = result ->
        result

      {:error, %NimbleOptions.ValidationError{} = error} ->
        message = Exception.message(error)
        {:error, Error.new(:option_error, message)}
    end
  end
end