Letterpress (Letterpress v0.1.0)

Copy Markdown View Source

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

analyze(profile, source, schema, opts \\ [])

@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.

apply_translations(profile, source, schema, translations, opts \\ [])

@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.

compile(profile, source, schema, opts \\ [])

@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 - see discover/3

:subject and :text are valid only for the email profile. All configured channels are schema-checked and later rendered atomically.

contract()

@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

decode_artifact(value)

@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}.

discover(profile, source, opts \\ [])

@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 to 0
  • :compiler_pool - registered compiler pool; defaults to Letterpress.Compiler.Pool
  • :compiler_timeout - request deadline in milliseconds; defaults to 15_000

encode_artifact(value)

@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.

extract_translation_units(profile, source, schema, opts \\ [])

@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.

format(profile, source, opts \\ [])

@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"}

localize(profile, source, schema, translations, opts \\ [])

@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.

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"]

render(artifact, values, opts \\ [])

@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.

version()

@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.