Tiptapex.Page (Tiptapex v0.1.2)

Copy Markdown View Source

Page setup for paginated documents: paper size, orientation, margins, running headers/footers and page numbering.

The canonical home for this configuration is the document itself — the ProseMirror doc node carries it under attrs.page:

%{
  "type" => "doc",
  "attrs" => %{
    "page" => %{
      "size" => "letter",
      "orientation" => "portrait",
      "margins" => %{"top" => 25.4, "right" => 25.4, "bottom" => 25.4, "left" => 25.4},
      "header" => %{"left" => "Acme S.A.", "center" => "", "right" => "{date}"},
      "footer" => %{"left" => "", "center" => "", "right" => ""},
      "numbering" => %{
        "enabled" => true,
        "region" => "footer",
        "align" => "center",
        "format" => "Page {page} of {pages}"
      }
    }
  },
  "content" => [...]
}

A document without attrs.page is not paginated — the editor renders the usual continuous surface and Tiptapex.Export.PDF falls back to A4-ish defaults only if you ask for them explicitly.

Sizes

Presets (millimetres, portrait):

* `:letter`  215.9 × 279.4 (8.5 × 11 in)
* `:legal`  215.9 × 355.6 (8.5 × 14 in)
* `:tabloid`  279.4 × 431.8 (11 × 17 in)
* `:executive`  184.15 × 266.7 (7.25 × 10.5 in)
* `:a3`  297 × 420
* `:a4`  210 × 297
* `:a5`  148 × 210

Anything else is a custom size: %{width: 200, height: 250}.

Units

Lengths are millimetres. Plain numbers are read as mm; strings may carry a unit — "1in", "2.5cm", "72pt", "96px" (CSS px at 96 dpi).

Tiptapex.Page.new(%{size: :letter, margins: %{top: "1in", bottom: "1in"}})

Headers and footers

Each region has three slots — :left, :center, :right — holding text and/or an image (a logo). Text supports tokens:

  • {page} — current page number
  • {pages} — total pages
  • {date} — print date
  • {time} — print time
  • {title} — the document title (:title, or the <title> of the exported HTML)

This maps 1:1 onto what both supported PDF engines can do natively, so page numbers are produced by the engine and are always correct — see Tiptapex.Export.PDF.

Page numbering is a convenience on top: enabling it drops :format into the slot named by :align of the region named by :region, unless that slot already contains a {page} token.

Logos

A slot is either a plain string (text only) or a map:

header: %{
  left: %{image: %{src: "/uploads/logo.png", height: 12}},
  center: "",
  right: "{page} / {pages}"
}

:height is millimetres (default 8, clamped to 100) and the width follows the image's aspect ratio. :src must be http(s), a relative path, or a data: URI for a raster image or SVG — anything else is dropped, the same allow-list discipline Tiptapex.Renderer applies to the document.

Normalised slots are always %{text: binary, image: map | nil}; to_map/1 writes the compact string form back out when a slot carries no image, so documents without logos keep the JSON they had.

Chrome cannot resolve relative logo URLs

ChromicPDF renders the running header/footer in a context with no base URL, so /uploads/logo.png will not load. Give the logo an absolute URL, or pass :asset_url to Tiptapex.Export.PDF to rewrite it (typically into a data: URI).

Summary

Types

A logo in a header/footer slot. :height is millimetres.

Millimetre lengths per side.

A header/footer region: three slots.

One header/footer slot: text, an image, or both.

t()

Functions

The printable box — paper minus margins — in millimetres.

A CSS margin shorthand value for the @page rule, e.g. "25.4mm 25.4mm 25.4mm 25.4mm".

A CSS size value for the @page rule, e.g. "215.9mm 279.4mm".

Paper dimensions in millimetres, with orientation applied.

Reads the page setup carried by a document, or nil when it has none.

True when any region of the page carries an image.

Builds a page configuration, accepting string- or atom-keyed maps, keyword lists, or an existing struct. Unknown values fall back to the defaults rather than raising — the input often comes from a stored document.

The built-in paper size names.

The built-in paper sizes as %{name => {width_mm, height_mm}} (portrait).

Stores a page configuration on the document's doc node, returning the updated document. Passing nil removes it (the document stops being paginated).

True when the region has anything to draw.

Replaces the {page}/{pages}/{date}/{time}/{title} tokens in text using a %{"page" => ...} map. Unlisted tokens become "".

The slots of a header/footer region as [{:left, slot}, {:center, slot}, {:right, slot}], with page numbering merged in. Each slot is %{text: binary, image: map | nil}.

Millimetres as inches — the unit Chrome's Page.printToPDF speaks.

The JSON-encodable form — string keys, millimetres, exactly what the doc node's attrs.page and the editor's data-ttx-page carry.

Types

image()

@type image() :: %{src: String.t(), alt: String.t(), height: float()}

A logo in a header/footer slot. :height is millimetres.

margins()

@type margins() :: %{top: float(), right: float(), bottom: float(), left: float()}

Millimetre lengths per side.

numbering()

@type numbering() :: %{
  enabled: boolean(),
  region: :header | :footer,
  align: :left | :center | :right,
  format: String.t()
}

region()

@type region() :: %{left: slot(), center: slot(), right: slot()}

A header/footer region: three slots.

size()

@type size() :: atom() | %{width: float(), height: float()}

slot()

@type slot() :: %{text: String.t(), image: image() | nil}

One header/footer slot: text, an image, or both.

t()

@type t() :: %Tiptapex.Page{
  footer: region(),
  header: region(),
  margins: margins(),
  numbering: numbering(),
  orientation: :portrait | :landscape,
  size: size(),
  title: String.t() | nil
}

Functions

content_box(page)

@spec content_box(t()) :: %{width: float(), height: float()}

The printable box — paper minus margins — in millimetres.

css_margin(page)

@spec css_margin(t()) :: String.t()

A CSS margin shorthand value for the @page rule, e.g. "25.4mm 25.4mm 25.4mm 25.4mm".

css_size(page)

@spec css_size(t()) :: String.t()

A CSS size value for the @page rule, e.g. "215.9mm 279.4mm".

dimensions(page)

@spec dimensions(t()) :: %{width: float(), height: float()}

Paper dimensions in millimetres, with orientation applied.

iex> Tiptapex.Page.dimensions(Tiptapex.Page.new(%{size: :letter}))
%{width: 215.9, height: 279.4}

from_doc(doc, override \\ nil)

@spec from_doc(map() | nil, t() | map() | keyword() | boolean() | nil) :: t() | nil

Reads the page setup carried by a document, or nil when it has none.

override decides what wins:

  • nil (default) — use whatever the document carries.

  • false — no page setup at all, whatever the document says.

  • true — force page setup on, using defaults for anything the document doesn't specify.

  • a map/keyword/struct — deep-merged over the document's own setup, so page: %{size: :legal} changes the paper without touching margins.

    Tiptapex.Page.from_doc(article.body) Tiptapex.Page.from_doc(article.body, %{orientation: :landscape})

images?(page)

@spec images?(t()) :: boolean()

True when any region of the page carries an image.

new(attrs \\ %{})

@spec new(t() | map() | keyword() | nil | true) :: t()

Builds a page configuration, accepting string- or atom-keyed maps, keyword lists, or an existing struct. Unknown values fall back to the defaults rather than raising — the input often comes from a stored document.

iex> page = Tiptapex.Page.new(%{"size" => "legal", "orientation" => "landscape"})
iex> Tiptapex.Page.dimensions(page)
%{width: 355.6, height: 215.9}

preset_names()

@spec preset_names() :: [atom()]

The built-in paper size names.

presets()

@spec presets() :: %{required(atom()) => {float(), float()}}

The built-in paper sizes as %{name => {width_mm, height_mm}} (portrait).

put(doc, page)

@spec put(map(), t() | map() | keyword() | nil) :: map()

Stores a page configuration on the document's doc node, returning the updated document. Passing nil removes it (the document stops being paginated).

doc = Tiptapex.Page.put(doc, %{size: :legal, numbering: %{enabled: true}})

region_used?(page, region)

@spec region_used?(t(), :header | :footer) :: boolean()

True when the region has anything to draw.

replace_tokens(text, replacements)

@spec replace_tokens(String.t(), %{optional(String.t()) => String.t()}) :: String.t()

Replaces the {page}/{pages}/{date}/{time}/{title} tokens in text using a %{"page" => ...} map. Unlisted tokens become "".

iex> Tiptapex.Page.replace_tokens("Page {page} of {pages}", %{"page" => "[page]", "pages" => "[topage]"})
"Page [page] of [topage]"

slots(page, region)

@spec slots(t(), :header | :footer) :: [{:left | :center | :right, slot()}]

The slots of a header/footer region as [{:left, slot}, {:center, slot}, {:right, slot}], with page numbering merged in. Each slot is %{text: binary, image: map | nil}.

Tokens are left untouched — the caller translates them for its engine.

to_inches(mm)

@spec to_inches(number()) :: float()

Millimetres as inches — the unit Chrome's Page.printToPDF speaks.

iex> Tiptapex.Page.to_inches(25.4)
1.0

to_map(page)

@spec to_map(t()) :: map()

The JSON-encodable form — string keys, millimetres, exactly what the doc node's attrs.page and the editor's data-ttx-page carry.