Current section

Files

Jump to
iona lib iona.ex
Raw

lib/iona.ex

defmodule Iona do
@moduledoc File.read!("#{__DIR__}/../README.md")
@type template_opts :: [
{:path, Path.t()}
]
@type source_opts :: [
{:path, Path.t()},
{:include, [Path.t()]}
]
@type supported_format_t :: atom
@type tex_t :: iodata
@type eex_tex_t :: binary
@type executable_t :: binary
@type processing_opts :: [
{:preprocess, [executable_t]}
| {:processor, executable_t}
]
@doc """
Fill in a template with assignments, with TeX escaping support
```
{:ok, template} = [title: "An Article", author: "Bruce Williams"]
|> Iona.template(path: "/path/to/article.tex")
Iona.write(template, "/path/to/article.pdf")
```
"""
@spec template(assigns :: Keyword.t() | map, criteria :: eex_tex_t) ::
{:ok, Iona.Template.t()} | {:error, term}
def template(assigns, criteria) when is_binary(criteria) do
assigns |> Iona.Template.fill(%Iona.Template{body: criteria})
end
@spec template(assigns :: Keyword.t() | map, criteria :: template_opts) ::
{:ok, Iona.Template.t()} | {:error, term}
def template(assigns, criteria) when is_list(criteria) do
template = %Iona.Template{
body_path: Keyword.get(criteria, :path),
include: Keyword.get(criteria, :include, []),
helpers: Keyword.get(criteria, :helpers, [])
}
assigns |> Iona.Template.fill(template)
end
@doc """
The same as `template/2`, but raises `Iona.ProcessingError` if it fails.
Returns the template otherwise.
```
[title: "An Article", author: "Bruce Williams"]
|> Iona.template!(path: "/path/to/article.tex")
|> Iona.write("/path/to/article.pdf")
```
"""
@spec template!(assigns :: Keyword.t() | map, criteria :: eex_tex_t | template_opts) ::
Iona.Template.t()
def template!(assigns, criteria) do
case template(assigns, criteria) do
{:ok, result} -> result
{:error, err} -> raise Iona.Processing.ProcessingError, message: err
end
end
# Note: The \\ in the example below is escaping to support ExDoc.
# In the actual LaTeX source, this would be \documentclass
@doc """
Define the document source, either as a raw TeX binary or the path to a `.tex` file.
As raw TeX:
```
Iona.source("\\documentclass[12pt]{article} ...")
```
From a file:
```
Iona.source(path: "/path/to/document.tex")
```
When providing a file path, you can also define additional files needed
for processing. They will be copied to the temporary directory where processing
will take place.
```elixir
Iona.source(path: "/path/to/document.tex",
include: ["/path/to/document.bib",
"/path/to/documentclass.sty"])
```
However, when possible, files should be placed in the search path of your TeX
installation.
"""
@spec source(criteria :: binary) :: Iona.Source.t()
def source(criteria) when is_binary(criteria) do
%Iona.Source{content: criteria}
end
@spec source(criteria :: source_opts) :: Iona.Source.t()
def source(criteria) when is_list(criteria) do
%Iona.Source{
path: Keyword.get(criteria, :path, nil),
include: Keyword.get(criteria, :include, [])
}
end
@doc """
Generate a formatted document as a string.
Without processing options:
```
{:ok, pdf_string} = Iona.source(path: "/path/to/document.tex")
|> Iona.to(:pdf)
```
With processing options:
```
{:ok, pdf_string} = Iona.source(path: "/path/to/document.tex")
|> Iona.to(:pdf, processor: "xetex")
```
"""
@spec to(
input :: Iona.Input.t(),
format :: supported_format_t,
opts :: processing_opts
) :: {:ok, binary} | {:error, binary}
def to(input, format, opts \\ []) do
case input |> Iona.Processing.process(format, opts) do
{:ok, document} -> document |> Iona.Document.read()
{:error, error} -> {:error, error}
end
end
@doc """
The same as `to/3`, but raises `Iona.ProcessingError` if it fails.
Returns the document content otherwise.
```
Iona.source(path: "/path/to/document.tex")
|> Iona.to!(:pdf)
|> MyModule.do_something_with_pdf_string
```
If writing to a file, see `write/3` and `write/4`, as they are both
shorter to type and have better performance characteristics.
"""
@spec to!(
input :: Iona.Input.t(),
format :: supported_format_t,
opts :: processing_opts
) :: binary
def to!(input, format, opts \\ []) do
case to(input, format, opts) do
{:ok, result} -> result
{:error, err} -> raise Iona.Processing.ProcessingError, message: err
end
end
@doc """
Generate a formatted document to a file path.
Without processing options:
```
:ok = Iona.source(path: "/path/to/document.tex")
|> Iona.write("/path/to/document.pdf")
```
With processing options:
```
:ok = Iona.source(path: "/path/to/document.tex")
|> Iona.write("/path/to/document.pdf",
processor: "xetex")
```
"""
@spec write(input :: Iona.Input.t(), path :: Path.t(), opts :: processing_opts) ::
:ok | {:error, term}
def write(input, path, opts \\ []) do
input
|> Iona.Processing.process(path |> Iona.Processing.to_format(), opts)
|> case do
{:ok, document} ->
Iona.Document.write(document, path)
{:error, error} ->
{:error, error}
end
end
@doc """
The same as `write/3` but raises `Iona.ProcessingError` if it fails.
Without processing options:
```
Iona.source(path: "/path/to/document.tex")
|> Iona.write!("/path/to/document.pdf")
```
With processing options:
```
Iona.source(path: "/path/to/document.tex")
|> Iona.write!("/path/to/document.pdf", processor: "xetex")
```
"""
@spec write!(input :: Iona.Input.t(), path :: Path.t(), opts :: processing_opts) :: :ok
def write!(input, path, opts \\ []) do
case write(input, path, opts) do
:ok -> :ok
{:error, err} -> raise Iona.Processing.ProcessingError, message: err
end
end
@doc """
Generate a directory with a build script that can be run to finalize the build.
## Examples
iex> Iona.source(path: "academic.tex")
iex> |> Iona.prepare!("/path/to/build/directory", :pdf, preprocess: ~w(latex bibtex latex))
:ok
"""
@spec prepare(
input :: Iona.Input.t(),
output :: Path.t(),
format :: Iona.supported_format_t(),
opts :: processing_opts
) :: :ok | {:error, term}
def prepare(input, output, format, opts \\ []) do
with {:ok, commands} <- Iona.Processing.prepare(input, output, format, opts),
:ok <- write_build_script(output, commands) do
:ok
else
{:error, error} ->
{:error, error}
end
end
@doc """
The same as `prepare/4` but raises `Iona.ProcessingError` if it fails.
"""
@spec prepare!(
input :: Iona.Input.t(),
output :: Path.t(),
format :: Iona.supported_format_t(),
opts :: processing_opts
) :: :ok
def prepare!(input, output, format, opts \\ []) do
case prepare(input, output, format, opts) do
:ok -> :ok
{:error, err} -> raise Iona.Processing.ProcessingError, message: err
end
end
@spec write_build_script(Path.t(), [String.t()]) :: :ok | {:error, File.posix()}
defp write_build_script(directory, commands) do
script = Path.join(directory, "build.sh")
with :ok <- File.write(script, script_content(commands)),
:ok <- File.chmod(script, 0o755) do
:ok
else
{:error, error} ->
{:error, error}
end
end
@spec script_content([String.t()]) :: iodata
defp script_content(commands) do
[
"#!/bin/sh\n",
Enum.intersperse(commands, " && \\\n")
]
end
end