Safe, deterministic notification templates for Elixir.
Letterpress is the public facade for discovering template variables,
validating source, compiling immutable artifacts, rendering stored artifacts,
formatting source, and applying translations.
Compilation belongs in an authoring or publication path. It uses the bundled
Node/MJML compiler through a caller-owned Letterpress.Compiler.Supervisor.
Rendering belongs in the delivery path: it needs only a verified artifact and
runs Liquid in an isolated BEAM process with explicit limits.
Expected template, schema, artifact, and value failures return tagged errors
with Letterpress.Diagnostic structs. Callers should branch on diagnostic
codes, not English messages.
Example
iex> schema = %{
...> "version" => 1,
...> "variables" => %{
...> "name" => %{"type" => "string", "context" => "text"}
...> }
...> }
iex> {:ok, artifact, []} =
...> Letterpress.compile("text/liquid@1", "Hello {{ name }}", schema)
iex> Letterpress.render(artifact, %{"name" => "Ada"})
{:ok, %{text: "Hello Ada"}}
Summary
Functions
Validates source against a typed schema without creating an artifact.
Applies translations after checking source and placeholder identity.
Compiles source and a schema into an immutable Letterpress.Artifact.
Returns the generated contract shared by the Elixir and browser packages.
Decodes and verifies canonical artifact JSON or a decoded map.
Discovers variables, dependencies, contexts, and translation units in source.
Encodes a verified artifact as canonical JSON.
Returns the stable translatable units discovered in source.
Formats source with the formatter pinned to the profile contract.
Applies translations to every configured email channel atomically.
Returns the supported profile identifiers in lexical order.
Renders every channel in an artifact atomically in pure BEAM code.
Returns the Letterpress version used to stamp new artifacts.
Functions
@spec analyze(String.t(), String.t(), map(), keyword()) :: {:ok, map(), [Letterpress.Diagnostic.t()]} | {:error, [Letterpress.Diagnostic.t()]}
Validates source against a typed schema without creating an artifact.
Analysis performs profile parsing, schema checks, context checks, dependency
discovery, and non-error schema diagnostics. Use compile/4 when the source
is ready to become a persistable delivery artifact.
Accepts the same :document_version, :compiler_pool, and
:compiler_timeout options as discover/3. The email profile also accepts
:subject and :text, which are analyzed with the primary MJML source as
one atomic template.
@spec apply_translations(String.t(), String.t(), map(), map(), keyword()) :: {:ok, String.t(), [Letterpress.Diagnostic.t()]} | {:error, [Letterpress.Diagnostic.t()]}
Applies translations after checking source and placeholder identity.
translations is a JSON object keyed by translation-unit ID. A value may be
the translated string or an object containing a "text" string. Missing
units, changed Liquid placeholders, or changed protected markup return error
diagnostics and leave the source unpublished.
@spec compile(String.t(), String.t(), map(), keyword()) :: {:ok, Letterpress.Artifact.t(), [Letterpress.Diagnostic.t()]} | {:error, [Letterpress.Diagnostic.t()]}
Compiles source and a schema into an immutable Letterpress.Artifact.
A successful call returns {:ok, artifact, diagnostics}. The artifact is
emitted only when no error diagnostic exists. Store the complete artifact;
extracting generated HTML or text discards the schema, provenance, and
integrity data needed by render/3.
Options
:subject- email subject template:text- email plain-text alternative:compile_values- JSON object containing compile-phase values:document_version,:compiler_pool, and:compiler_timeout- seediscover/3
:subject and :text are valid only for the email profile. All configured
channels are schema-checked and later rendered atomically.
@spec contract() :: map()
Returns the generated contract shared by the Elixir and browser packages.
The map describes profiles, schema types, diagnostics, limits, grammar, and editor metadata for the current contract version.
Example
iex> Letterpress.contract()["contract_version"]
1
@spec decode_artifact(binary() | map()) :: {:ok, Letterpress.Artifact.t()} | {:error, term()}
Decodes and verifies canonical artifact JSON or a decoded map.
Verification covers the artifact version, exact shape, profile, compiler
provenance, hashes, schema entries, channels, and content hash. Invalid input
returns {:error, reason}.
@spec discover(String.t(), String.t(), keyword()) :: {:ok, map(), [Letterpress.Diagnostic.t()]} | {:error, [Letterpress.Diagnostic.t()]}
Discovers variables, dependencies, contexts, and translation units in source.
Discovery does not require a schema and never infers one. The successful
result is {:ok, analysis, diagnostics}; diagnostics may contain non-error
hints or warnings. Any error diagnostic changes the return to
{:error, diagnostics}.
Options
:document_version- caller-owned revision echoed in diagnostics; defaults to0:compiler_pool- registered compiler pool; defaults toLetterpress.Compiler.Pool:compiler_timeout- request deadline in milliseconds; defaults to15_000
@spec encode_artifact(Letterpress.Artifact.t()) :: {:ok, binary()} | {:error, term()}
Encodes a verified artifact as canonical JSON.
The artifact's content hash is checked before encoding. The resulting bytes are suitable for persistence or transport across runtimes.
@spec extract_translation_units(String.t(), String.t(), map(), keyword()) :: {:ok, [map()], [Letterpress.Diagnostic.t()]} | {:error, [Letterpress.Diagnostic.t()]}
Returns the stable translatable units discovered in source.
Each unit carries an ID, source range, context, source hash, and original
text. Email units also carry a "channel" of "html", "subject", or
"text". Pass email :subject and :text sources as options to extract all
configured channels in one call. Unit IDs are derived from the profile,
channel, and source structure and are inputs to localize/5.
@spec format(String.t(), String.t(), keyword()) :: {:ok, String.t()} | {:error, [Letterpress.Diagnostic.t()]}
Formats source with the formatter pinned to the profile contract.
Formatting is deterministic for the same source and pinned compiler bundle. Text-profile formatting removes trailing whitespace; email formatting uses the bundled Liquid/HTML formatter.
Example
iex> Letterpress.format("text/liquid@1", "Hello ")
{:ok, "Hello"}
@spec localize(String.t(), String.t(), map(), map(), keyword()) :: {:ok, %{source: String.t(), subject: String.t() | nil, text: String.t() | nil}, [Letterpress.Diagnostic.t()]} | {:error, [Letterpress.Diagnostic.t()]}
Applies translations to every configured email channel atomically.
Pass the original email :subject and :text sources as options. The
translation object is keyed by the channel-aware unit IDs returned from
extract_translation_units/4. Success returns all localized authoring
sources as %{source: source, subject: subject, text: text}. If any channel
is missing a unit or changes protected Liquid/markup, no localized source is
returned.
Use apply_translations/5 for the source-only HTML and text profiles.
@spec profiles() :: [String.t()]
Returns the supported profile identifiers in lexical order.
Profile identifiers are versioned. An existing identifier does not change meaning after release.
Example
iex> Letterpress.profiles()
["email/mjml-liquid@1", "html/liquid@1", "text/liquid@1"]
@spec render(Letterpress.Artifact.t() | map(), map(), keyword()) :: {:ok, %{optional(atom()) => String.t()}} | {:error, [Letterpress.Diagnostic.t()]}
Renders every channel in an artifact atomically in pure BEAM code.
The values must form a JSON object and match the artifact's delivery-phase schema. No channel is returned if another channel fails. Rendering does not use the compiler pool, Node, or MJML.
See Letterpress.Renderer.render/3 for resource and validation options.
@spec version() :: String.t()
Returns the Letterpress version used to stamp new artifacts.
Artifact provenance also records the pinned compiler components. See
Letterpress.Artifact for the complete serialized contract.