Packages

Print-quality PDF from Markdown + Elixir, powered by Typst

Current section

Files

Jump to
folio lib folio content.ex
Raw

lib/folio/content.ex

defmodule Folio.Content do
@moduledoc """
Typed content nodes that map 1:1 to Typst elements.
Every struct has matching fields in `native/folio_nif/src/types.rs`.
The Rust NifStruct `#[module = "Folio.Content.*"]` must match exactly.
"""
defmodule Text do
@moduledoc "A plain text run. Field: `text`."
defstruct [:text]
@type t :: %__MODULE__{text: String.t()}
end
defmodule Space do
@moduledoc "Inter-word space."
defstruct []
@type t :: %__MODULE__{}
end
defmodule Heading do
@moduledoc "A section heading. Fields: `body`, `level` (1-6)."
defstruct [:body, :level]
@type t :: %__MODULE__{body: [Folio.Content.t()], level: 1..6}
end
defmodule Cite do
@moduledoc "A citation from a bibliography. Fields: `key`, `supplement`, `form`, `style`."
defstruct [:key, :supplement, :form, :style]
@type t :: %__MODULE__{
key: String.t(),
supplement: [Folio.Content.t()] | nil,
form: String.t() | nil,
style: String.t() | nil
}
end
defmodule Bibliography do
@moduledoc "A bibliography listing. Fields: `sources`, `title`, `full`, `style`."
defstruct [:sources, :title, :full, :style]
@type t :: %__MODULE__{
sources: [String.t()],
title: [Folio.Content.t()] | nil,
full: boolean(),
style: String.t() | nil
}
end
defmodule Paragraph do
@moduledoc "A body paragraph. Field: `body`."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Strong do
@moduledoc "Bold text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Emph do
@moduledoc "Italic text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Strike do
@moduledoc "Strikethrough text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Underline do
@moduledoc "Underlined text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Highlight do
@moduledoc "Highlighted text."
defstruct [:body, :fill]
@type t :: %__MODULE__{body: [Folio.Content.t()], fill: String.t() | nil}
end
defmodule Super do
@moduledoc "Superscript text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Sub do
@moduledoc "Subscript text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Smallcaps do
@moduledoc "Small capitals text."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Image do
@moduledoc "An image. Fields: `src`, `width`, `height`, `fit`."
defstruct [:src, :width, :height, :fit]
@type t :: %__MODULE__{
src: String.t(),
width: String.t() | nil,
height: String.t() | nil,
fit: String.t() | nil
}
end
defmodule Figure do
@moduledoc "A figure with optional caption. Fields: `body`, `caption`, `placement`, `numbering`."
defstruct [:body, :caption, :placement, :scope, :numbering, :separator]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
caption: [Folio.Content.t()] | nil,
placement: String.t() | nil,
scope: String.t() | nil,
numbering: String.t() | nil,
separator: String.t() | nil
}
end
defmodule Table do
@moduledoc "A table grid. Fields: `children`, `gutter`, `stroke`, `align`."
defstruct [:columns, :rows, :children, :stroke, :gutter, :align]
@type t :: %__MODULE__{
columns: String.t() | nil,
rows: String.t() | nil,
children: [Folio.Content.t()],
stroke: String.t() | nil,
gutter: String.t() | nil,
align: String.t() | nil
}
end
defmodule TableHeader do
@moduledoc "Table header row."
defstruct [:children]
@type t :: %__MODULE__{children: [Folio.Content.t()]}
end
defmodule TableRow do
@moduledoc "Table data row."
defstruct [:children]
@type t :: %__MODULE__{children: [Folio.Content.t()]}
end
defmodule TableCell do
@moduledoc "Table cell."
defstruct [:body, :colspan, :rowspan, :align]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
colspan: pos_integer() | nil,
rowspan: pos_integer() | nil,
align: String.t() | nil
}
end
defmodule Columns do
@moduledoc "Multi-column layout."
defstruct [:count, :body, :gutter]
@type t :: %__MODULE__{
count: pos_integer(),
body: [Folio.Content.t()],
gutter: String.t() | nil
}
end
defmodule Colbreak do
@moduledoc "Column break."
defstruct [:weak]
@type t :: %__MODULE__{weak: boolean()}
end
defmodule Pagebreak do
@moduledoc "Page break."
defstruct [:weak]
@type t :: %__MODULE__{weak: boolean()}
end
defmodule Parbreak do
@moduledoc "Paragraph break."
defstruct []
@type t :: %__MODULE__{}
end
defmodule Linebreak do
@moduledoc "Line break."
defstruct []
@type t :: %__MODULE__{}
end
defmodule Math do
@moduledoc "Math expression."
defstruct [:content, :block]
@type t :: %__MODULE__{content: String.t(), block: boolean()}
end
defmodule Link do
@moduledoc "Hyperlink."
defstruct [:url, :body]
@type t :: %__MODULE__{url: String.t(), body: [Folio.Content.t()]}
end
defmodule Raw do
@moduledoc "Raw/code text."
defstruct [:text, :lang, :block]
@type t :: %__MODULE__{text: String.t(), lang: String.t() | nil, block: boolean()}
end
defmodule Quote do
@moduledoc "Block quote."
defstruct [:body, :block, :attribution]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
block: boolean(),
attribution: [Folio.Content.t()] | nil
}
end
defmodule List do
@moduledoc "Bullet list."
defstruct [:children, :tight, :marker]
@type t :: %__MODULE__{
children: [Folio.Content.t()],
tight: boolean(),
marker: String.t() | nil
}
end
defmodule ListItem do
@moduledoc "Bullet list item."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule EnumList do
@moduledoc "Numbered (ordered) list."
defstruct [:children, :tight, :start]
@type t :: %__MODULE__{
children: [Folio.Content.t()],
tight: boolean(),
start: pos_integer() | nil
}
end
defmodule EnumItem do
@moduledoc "Numbered list item."
defstruct [:body, :number]
@type t :: %__MODULE__{body: [Folio.Content.t()], number: pos_integer() | nil}
end
defmodule Label do
@moduledoc "Cross-reference label."
defstruct [:name]
@type t :: %__MODULE__{name: String.t()}
end
defmodule Ref do
@moduledoc "Cross-reference."
defstruct [:target, :supplement]
@type t :: %__MODULE__{target: String.t(), supplement: [Folio.Content.t()] | nil}
end
defmodule Align do
@moduledoc "Content alignment."
defstruct [:alignment, :body]
@type t :: %__MODULE__{alignment: String.t(), body: [Folio.Content.t()]}
end
defmodule Block do
@moduledoc "Block-level container."
defstruct [:body, :width, :height, :above, :below]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
width: String.t() | nil,
height: String.t() | nil,
above: String.t() | nil,
below: String.t() | nil
}
end
defmodule Hide do
@moduledoc "Hidden content."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Repeat do
@moduledoc "Repeating content."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Place do
@moduledoc "Absolute placement."
defstruct [:alignment, :body, :float]
@type t :: %__MODULE__{
alignment: String.t() | nil,
body: [Folio.Content.t()],
float: boolean() | nil
}
end
defmodule VSpace do
@moduledoc "Vertical spacing."
defstruct [:amount, :weak]
@type t :: %__MODULE__{amount: String.t(), weak: boolean()}
end
defmodule HSpace do
@moduledoc "Horizontal spacing."
defstruct [:amount, :weak]
@type t :: %__MODULE__{amount: String.t(), weak: boolean()}
end
defmodule Pad do
@moduledoc "Padding around content."
defstruct [:body, :left, :right, :top, :bottom]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
left: String.t() | nil,
right: String.t() | nil,
top: String.t() | nil,
bottom: String.t() | nil
}
end
defmodule Stack do
@moduledoc "Stacked layout."
defstruct [:dir, :children, :spacing]
@type t :: %__MODULE__{
dir: String.t(),
children: [Folio.Content.t()],
spacing: String.t() | nil
}
end
defmodule Rect do
@moduledoc "Rectangle shape."
defstruct [:body, :width, :height, :fill]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
width: String.t() | nil,
height: String.t() | nil,
fill: String.t() | nil
}
end
defmodule Square do
@moduledoc "Square shape."
defstruct [:body, :size, :fill]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
size: String.t() | nil,
fill: String.t() | nil
}
end
defmodule Circle do
@moduledoc "Circle shape."
defstruct [:body, :radius, :fill]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
radius: String.t() | nil,
fill: String.t() | nil
}
end
defmodule Ellipse do
@moduledoc "Ellipse shape."
defstruct [:body, :width, :height, :fill]
@type t :: %__MODULE__{
body: [Folio.Content.t()],
width: String.t() | nil,
height: String.t() | nil,
fill: String.t() | nil
}
end
defmodule Line do
@moduledoc "Line shape."
defstruct [:start, :end, :length, :angle, :stroke]
@type t :: %__MODULE__{
start: String.t() | nil,
end: String.t() | nil,
length: String.t() | nil,
angle: String.t() | nil,
stroke: String.t() | nil
}
end
defmodule Polygon do
@moduledoc "Polygon shape."
defstruct [:vertices, :fill, :stroke]
@type t :: %__MODULE__{
vertices: [String.t()],
fill: String.t() | nil,
stroke: String.t() | nil
}
end
defmodule Outline do
@moduledoc "Table of contents."
defstruct [:title, :indent, :depth]
@type t :: %__MODULE__{
title: String.t() | nil,
indent: String.t() | nil,
depth: pos_integer() | nil
}
end
defmodule Title do
@moduledoc "Document title."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule TermList do
@moduledoc "Definition/term list."
defstruct [:children, :tight]
@type t :: %__MODULE__{children: [Folio.Content.t()], tight: boolean()}
end
defmodule TermItem do
@moduledoc "Term list item."
defstruct [:term, :description]
@type t :: %__MODULE__{term: [Folio.Content.t()], description: [Folio.Content.t()]}
end
defmodule Footnote do
@moduledoc "Footnote."
defstruct [:body]
@type t :: %__MODULE__{body: [Folio.Content.t()]}
end
defmodule Divider do
@moduledoc "Horizontal divider."
defstruct []
@type t :: %__MODULE__{}
end
defmodule Sequence do
@moduledoc "Content sequence."
defstruct [:children]
@type t :: %__MODULE__{children: [Folio.Content.t()]}
end
@type t ::
Text.t()
| Space.t()
| Heading.t()
| Cite.t()
| Bibliography.t()
| Paragraph.t()
| Strong.t()
| Emph.t()
| Strike.t()
| Underline.t()
| Highlight.t()
| Super.t()
| Sub.t()
| Smallcaps.t()
| Image.t()
| Figure.t()
| Table.t()
| TableHeader.t()
| TableRow.t()
| TableCell.t()
| Columns.t()
| Colbreak.t()
| Pagebreak.t()
| Parbreak.t()
| Linebreak.t()
| Math.t()
| Link.t()
| Raw.t()
| Quote.t()
| List.t()
| ListItem.t()
| EnumList.t()
| EnumItem.t()
| Label.t()
| Ref.t()
| Align.t()
| Block.t()
| Hide.t()
| Repeat.t()
| Place.t()
| VSpace.t()
| HSpace.t()
| Pad.t()
| Stack.t()
| Rect.t()
| Square.t()
| Circle.t()
| Ellipse.t()
| Line.t()
| Polygon.t()
| Outline.t()
| Title.t()
| TermList.t()
| TermItem.t()
| Footnote.t()
| Divider.t()
| Sequence.t()
@doc "Flatten nested Sequences."
@spec flatten([t()]) :: [t()]
def flatten(nodes) do
Enum.flat_map(nodes, fn
%Sequence{children: children} -> flatten(children)
node -> [node]
end)
end
@doc "Convert a value to a content list."
@spec to_content(t() | String.t() | [t()] | nil) :: [t()]
def to_content(nil), do: []
def to_content(%_{} = node), do: [node]
def to_content(str) when is_binary(str), do: [%Text{text: str}]
def to_content(list) when is_list(list), do: Enum.flat_map(list, &to_content/1)
end