Current section
Files
Jump to
Current section
Files
lib/iona.ex
defmodule Iona do
@moduledoc File.read!("#{__DIR__}/../README.md")
use Application
@doc false
def start(_type, _args) do
Iona.Supervisor.start_link()
end
@doc """
Fill in a template with assignments, with TeX escaping support
```
[title: "An Article", author: "Bruce Williams"]
|> Iona.template(path: "/path/to/article.tex")
|> Iona.write("/path/to/article.pdf")
```
"""
@type template_opts :: [
{:path, Path.t()}
]
@spec template(assigns :: Keyword.t(), criteria :: eex_tex_t) :: Iona.Template.t()
def template(assigns, criteria) when is_binary(criteria) do
case assigns |> Iona.Template.fill(%Iona.Template{body: criteria}) do
{:ok, template} -> template
other -> other
end
end
@spec template(assigns :: Keyword.t(), criteria :: template_opts) :: Iona.Template.t()
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, [])
}
case assigns |> Iona.Template.fill(template) do
{:ok, template} -> template
other -> other
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.
"""
@type source_opts :: [
{:path, Path.t()},
{:include, [Path.t()]}
]
@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
@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},
{:prepare, Path.t()}
]
@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()
other -> other
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
result = input |> Iona.Processing.process(path |> Iona.Processing.to_format(), opts)
case result do
{:ok, document} ->
Iona.Document.write(document, path)
{:prepared, directory, commands} ->
write_build_script(directory, commands)
other ->
other
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 | no_return
def write!(input, path, opts \\ []) do
case write(input, path, 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)) do
File.chmod(script, 0o755)
end
end
@spec script_content([String.t()]) :: iodata
defp script_content(commands) do
[
"#!/bin/sh\n",
Enum.intersperse(commands, " && \\\n")
]
end
end