Skip to main content

lib/langelic_epub.ex

defmodule LangelicEpub do
  @moduledoc """
  EPUB read and write for Elixir.

  ## Reading

      {:ok, doc} = LangelicEpub.parse(File.read!("book.epub"))
      doc.title         # => "The Hobbit"
      doc.language      # => "en"
      Enum.count(doc.spine)  # => 23

  ## Writing

      doc = %LangelicEpub.Document{
        title: "Translated Title",
        language: "th",
        creators: ["Original Author"],
        identifier: "urn:uuid:...",
        spine: [
          %LangelicEpub.Chapter{
            id: "ch1",
            file_name: "ch1.xhtml",
            title: "บทที่ 1",
            media_type: "application/xhtml+xml",
            data: chapter_xhtml
          }
        ]
      }

      {:ok, bytes} = LangelicEpub.build(doc)
      File.write!("translated.epub", bytes)
  """

  alias LangelicEpub.{Document, Error}

  @doc """
  Parse EPUB bytes into a `LangelicEpub.Document`.

  Accepts the raw bytes of an EPUB file. All chapters and assets are loaded
  into memory; do not call this on documents larger than your available memory.

  ## Errors

  Returns `{:error, %LangelicEpub.Error{}}` for:

    * `:invalid_zip` — bytes are not a valid ZIP archive
    * `:invalid_mimetype` — `mimetype` entry is missing or its content (after
      trimming a UTF-8 BOM and surrounding whitespace) is not
      `application/epub+zip`
    * `:missing_container` — no `META-INF/container.xml`
    * `:missing_opf` — OPF file referenced in `container.xml` not found
    * `:malformed_opf` — OPF could not be parsed
    * `:io` — internal I/O failure
    * `:panic` — Rust side panicked (should never happen; report a bug)
  """
  @spec parse(binary()) :: {:ok, Document.t()} | {:error, Error.t()}
  def parse(epub_bytes) when is_binary(epub_bytes) do
    LangelicEpub.Native.parse(epub_bytes)
  end

  @doc """
  Build a `LangelicEpub.Document` into EPUB bytes.

  The generated EPUB is always EPUB 3 with a backward-compatible `toc.ncx`.

  ## Errors

  Returns `{:error, %LangelicEpub.Error{}}` for:

    * `:missing_required_field` — title, identifier, or language is missing
    * `:invalid_chapter` — a chapter's `data` is not valid UTF-8 XHTML
    * `:duplicate_id` — two chapters or assets share the same `id`
    * `:panic` — Rust side panicked (report a bug)
  """
  @spec build(Document.t()) :: {:ok, binary()} | {:error, Error.t()}
  def build(%Document{} = doc) do
    LangelicEpub.Native.build(doc)
  end
end