API reference
A map of the public surface, generated from go doc -all and grouped by package. Each section links to the page that explains it in context. Most services import only the root package, plus data for the typed values and theme for the palette types. The other packages are the stages of the pipeline (comark → ir → layout → render/html and render/text). They are exported so tools and tests can use them.
internal/htmlcheck, the structural lint for rendered HTML used by the goldens, is internal and can't be imported from outside the module.
Import lttr
import "github.com/sulv-io/lttr"
The API: compile templates, render messages, and send them.
Compile templates
func Compile[T any](reg *Registry, name string, src []byte) (*Template[T], error)
func MustCompile[T any](reg *Registry, name string, src []byte) *Template[T]
type Template[T any] struct{ /* unexported */ }
func (t *Template[T]) Render(r *Renderer, to Recipient, v T) (*Message, error)
func (t *Template[T]) Meta() TemplateMeta
func (t *Template[T]) Name() string
| Identifier | Does |
|---|---|
Compile | Compiles src called name against the data type T (which must be a struct) and the families in reg. It checks the Comark source, frontmatter, modules and props, every merge tag and binding against T and Recipient, and the family rules. It returns every error joined, each an *ir.Error with name:line:col. |
MustCompile | Compile that panics on error, for package-level template variables. |
Template[T] | A compiled template, immutable and safe for concurrent use. |
Template.Render | Renders for recipient to with data v and runs the data-dependent checks (values, URLs, subject, addresses, entity, unsubscribe URL, the 100 KB size limit). It returns a whole *Message or an error, never both. |
Template.Meta | A copy of the template's TemplateMeta. |
Template.Name | The template name. |
type TemplateMeta struct {
Name string
Family string
Stream Stream // the family's stream, or its legal stream with "legal: true"
Tags []TagInfo // in source order
Modules []string // module kinds in order, e.g. "statement"
Warnings []string // "name:line:col: warning: msg"
}
type TagInfo struct {
Path string // dotted path, e.g. "driver.first_name"
Type string // the Go type it resolves to, e.g. "string", "data.Money"
Pos string // "line:col" in the template source
Binding bool // a :key="path" binding rather than a {{path}} tag
}
TemplateMeta describes a compiled template. TagInfo is one merge tag or binding it uses, and is what lttr tags prints. See Compile errors and Merge tags and bindings.
Configure a renderer
func NewRenderer(cfg Config) (*Renderer, error)
type Renderer struct{ /* unexported */ }
type Config struct {
Domain string // sending domain; the {domain} placeholder
SiteURL string // absolute https URL; the {site} placeholder
AssetBase string // base URL of the logo files
Theme Theme // palettes, fonts and brand mark
Families *Registry // the family registry templates are compiled against
Entities map[string]Entity // region → entity, for the footer address
DefaultRegion string // region used when a recipient has none; must be in Entities
AllowRelativeURLs bool // previews only: relative asset and link URLs are allowed
}
type Entity struct {
Address string
}
| Identifier | Does |
|---|---|
NewRenderer | Validates cfg, including the theme and the registry, and compiles every family's copy. It reports every problem at once. |
Renderer | Renders compiled templates for one Config. Immutable and safe for concurrent use. |
Config | Everything a Renderer needs from the consumer. |
Entity | The legal entity a region's mail is sent on behalf of. Address fills {address}. |
type Vars struct {
Site string `json:"site"` // Config.SiteURL
Brand string `json:"brand"` // Theme.Brand.Name
Domain string `json:"domain"` // Config.Domain
Region string `json:"region"` // the recipient's region, or Config.DefaultRegion
Address string `json:"address"` // the entity address for the region
About string `json:"about"` // the F2 about value (family copy or footer.about)
Prefs string `json:"prefs"` // the recipient's preferences URL
Unsubscribe string `json:"unsubscribe"` // the recipient's unsubscribe URL
Issue string `json:"issue"` // the evaluated frontmatter issue (H3 masthead)
}
Vars are the {name} placeholders available to family copy and the footer.* frontmatter overrides. The renderer fills them per message.
type Theme = theme.Theme
type Recipient = data.Recipient
Theme is the brand a renderer draws with (see theme). Recipient is the person a message is addressed to (see data). Templates can use its fields as merge tags. See Config and the renderer.
Define families
func NewRegistry(families ...Family) *Registry
type Registry struct{ /* unexported */ }
func (r *Registry) Lookup(name string) (Family, bool)
func (r *Registry) Names() []string
func (r *Registry) Validate(domain string) error
| Identifier | Does |
|---|---|
NewRegistry | A registry of deep copies of families. It never fails, because problems are reported by Validate. |
Registry | An immutable set of families, handed to Compile and to Config.Families. |
Registry.Lookup | A copy of the family called name. With duplicate names, the first wins. |
Registry.Names | The family names, sorted, each once. |
Registry.Validate | Reports every problem for a config whose Domain is domain: duplicate or empty names, invalid streams or variants, a missing masthead (H3) or banner (H2), unknown forbidden modules, a sender outside its domain, and copy that doesn't compile. NewRenderer calls it. |
type Family struct {
Name string
From mail.Address // fixed sender; zero when FromFrontmatter
FromFrontmatter bool // the template's frontmatter "from" is required and is the sender
SenderDomain string // allowed sender domain; "" = Config.Domain; may use {domain}
Stream Stream // sending stream
Header HeaderVariant // header chrome
Footer FooterVariant // footer chrome
Copy FooterCopy // footer copy for Footer
Masthead string // H3 masthead copy, e.g. "Journal · No. {issue}"; required for H3
Banner string // H2 banner copy; required for H2
Legal *LegalOverride // what frontmatter "legal: true" switches to; nil = not allowed
Forbidden []ir.Kind // modules templates of this family may not use
Tracking bool // whether the sending service may track opens and clicks
}
type FooterCopy struct {
Nav []NavLink // F1 nav row; empty = no row
Reason string // F1 lead (Markdown)
Links string // F1 links after the reason
Note string // F2 lead (Markdown)
About string // F2 {about} value (plain text); "" drops the service line
Service string // F2 service line
OptOut string // F3 lead (Markdown)
Internal string // F3·i lead (Markdown)
Address string // every variant, plain text, e.g. "{brand} · {address}"
TextLinks []TextLink // plain-text URL lines after the address
}
type LegalOverride struct {
Stream Stream
Footer FooterVariant
Copy FooterCopy
}
type NavLink struct{ Label, Href string }
type TextLink struct{ Label, URL string }
| Identifier | Does |
|---|---|
Family | A consumer-defined kind of email: sender, stream, chrome, footer copy and forbidden modules. |
FooterCopy | A family's footer copy. Each string is one paragraph of Markdown, or plain text, with {name} placeholders and no merge tags. The footer variant decides which fields are shown. |
LegalOverride | The stream, footer and copy a family switches to when a template sets legal: true. |
NavLink | One link of the F1 nav row. Both fields may use placeholders. |
TextLink | One plain-text footer line, Label: URL. URL may use placeholders. Label is literal. |
type HeaderVariant = layout.HeaderVariant
type FooterVariant = layout.FooterVariant
const (
H1 = layout.H1 // logo
H2 = layout.H2 // logo with the internal banner (Family.Banner)
H3 = layout.H3 // newsletter masthead (Family.Masthead)
)
const (
F1 = layout.F1 // marketing: nav, reason, unsubscribe and preferences links
F2 = layout.F2 // service: note and "service email about {about}" line
F3 = layout.F3 // outreach: opt-out line
F3Internal = layout.F3Internal // internal: internal-only line
)
The header and footer chrome variants. See Families and streams.
Pick a stream
type Stream string
const (
Transactional Stream = "transactional" // mail the recipient triggered: receipts, orders, payouts
Security Stream = "security" // account security: sign-in codes, password resets
Marketing Stream = "marketing" // opted-in promotional mail; carries List-Unsubscribe
Outreach Stream = "outreach" // business-to-business mail, kept apart from Marketing
Internal Stream = "internal" // mail to the operator's own staff only
)
func (s Stream) Valid() bool
A message's sending stream. Streams keep reputations apart: a Router sends each one through its own Sender. Valid reports whether s is one of the five.
Work with a message
type Message struct {
Template, Family string // not written to the wire
Stream Stream // selects the Router's sender
From, To mail.Address // sender and the single recipient
Subject, Preheader string
HTML, Text string // the two bodies of the multipart/alternative message
Tracking bool // whether the HTML carries tracked links (the family's Tracking)
Warnings []string // non-fatal findings from compile and render
// unexported header fields
}
func (m *Message) MIME(date time.Time) ([]byte, error)
func (m *Message) SetHeader(name, value string) error
func (m *Message) SetListUnsubscribe(u string) error
func (m *Message) Header(name string) string
func (m *Message) Headers() []string
func (m *Message) Clone() *Message
| Identifier | Does |
|---|---|
Message | A rendered email: envelope, both bodies and any extra headers. |
MIME | Serialises an RFC 5322 multipart/alternative message with CRLF line endings: text part first, then HTML, both UTF-8 quoted-printable. Headers go in a fixed order, and non-ASCII subjects and names are RFC 2047 encoded. The Message-ID and boundary are random, so two calls differ. |
SetHeader | Sets a custom header. It refuses the headers MIME writes itself and List-Unsubscribe*, and refuses names or values with CR, LF, NUL or non-printable bytes. |
SetListUnsubscribe | Sets the RFC 8058 one-click List-Unsubscribe and List-Unsubscribe-Post headers. u must be absolute https. Render calls it on the marketing stream. |
Header | A custom header's value, matched case-insensitively, or "". |
Headers | The custom header names, canonical and sorted, in the order MIME writes them. |
Clone | A deep copy. Changing its headers or warnings doesn't affect the original. |
See Message and MIME.
Send
type Sender interface {
Send(ctx context.Context, m *Message) error
}
type Router struct {
Streams map[Stream]Sender
InternalAllowed func(addr string) bool // nil denies all Internal mail
Suppressed func(ctx context.Context, s Stream, addr string) (bool, error) // nil suppresses nothing
}
func (r *Router) Send(ctx context.Context, m *Message) error
type SMTP struct {
Addr string // host:port
Auth smtp.Auth // nil sends without authentication
Headers map[string]string // added to a copy of every message, e.g. X-SES-CONFIGURATION-SET
Now func() time.Time // stamps the Date header; nil means time.Now
}
func (s *SMTP) Send(ctx context.Context, m *Message) error
var ErrSuppressed = errors.New("recipient suppressed")
| Identifier | Does |
|---|---|
Sender | Delivers a message. |
Router | Sends each message through the Sender for its stream, after the internal-mail and suppression checks. It never modifies the message. |
Router.Send | Fails if the stream has no sender, if the recipient isn't a plain local@domain, if Internal mail goes to an address InternalAllowed doesn't accept, or if the recipient is suppressed. |
SMTP | A Sender that delivers each message over one SMTP session. |
SMTP.Send | Checks the message before connecting, then sends a copy with Headers added. MAIL FROM is m.From.Address and RCPT TO is m.To.Address. It uses STARTTLS when the server offers it, verified against the host of Addr. Cancelling ctx aborts the dial and the session. |
ErrSuppressed | Returned, wrapped with the address, when Suppressed reports the recipient. Test with errors.Is. |
See Router and streams and SMTP.
Version
const Version = "v0.0.0-dev"
The library's version string as written in source. The release workflow tags commits without rewriting it, so read a consumer's lttr version from its module graph instead. See Releasing.
Import lttr/comark
import "github.com/sulv-io/lttr/comark"
The parser: Markdown with frontmatter, block and inline components, spans, element attributes and merge tags, built on goldmark. See Comark syntax.
func Parse(src []byte) (*Document, error)
func ParseAttrs(s string, base Pos) (Attrs, int, error)
func ParseText(s string, base Pos) (Text, error)
func NodeAttrs(n ast.Node) (Attrs, bool)
func PlainText(n ast.Node, src []byte) string
| Function | Does |
|---|---|
Parse | Parses a template. Safe for concurrent use and never panics. Errors are *Error values (line:col: msg). A leading BOM is ignored and CRLF is accepted. |
ParseAttrs | Parses an attribute list at the start of s (which must start with {). It returns the attributes and the bytes consumed. {{ never starts attributes, and attributes never span lines. |
ParseText | Splits s into literals and merge tags. Malformed tag attempts become literal parts with Suspect set. The only error is invalid UTF-8. \{{ is a literal {{. |
NodeAttrs | The attributes of a node: those attached to a link, image, emphasis or code span, or the Attrs of a component or span. |
PlainText | The text under a node, with escapes and entities resolved and merge tags written back as {{path}}. Raw HTML is skipped. |
type Document struct {
Frontmatter map[string]any
FrontmatterPos map[string]Pos // dotted key path ("footer.note") → key position
Root ast.Node
Source []byte // the source without a leading BOM
}
func (d *Document) Pos(n ast.Node) Pos
type Pos struct{ Line, Col int }
func (p Pos) String() string // "3:7"
type Error struct {
Pos Pos
Msg string
}
func (e *Error) Error() string // "line:col: msg"
| Type | Is |
|---|---|
Document | A parsed template. Pos returns a node's line:col, falling back to its first positioned descendant and then its nearest ancestor. |
Pos | A 1-based line and byte column. |
Error | A parse error at a position. Callers prefix the template name. |
type Attrs struct {
ID string
Classes []string
Values map[string]Text // plain attributes; a flag maps to an empty Text
Flags map[string]bool // bare keys with no value
Bindings map[string]string // :key="path"
Pos Pos // position of the opening "{"
}
type Text []Part
func (t Text) String() string
type Part struct {
Lit string // set when Tag == nil
Tag *Tag
Suspect bool // Lit came from a malformed "{{" tag attempt
}
type Tag struct {
Path string // dotted, e.g. "driver.first_name"
Fallback string
HasFallback bool
Pos Pos
}
func (t Tag) String() string // {{path}} or {{path | fallback}}
Attrs is a parsed {#id .class key=value flag :binding=path} list. Text is literal text with embedded merge tags, and Part is one piece of it. Tag is one merge tag. Its String quotes the fallback when it wouldn't survive a bare round trip.
The AST nodes all implement goldmark's ast.Node, through Kind() and Dump(src []byte, level int):
| Node | Syntax | Fields |
|---|---|---|
Component | ::name{attrs} … :: | Name, Attrs, Props map[string]any (nil without a props block), PropsPos, At |
Slot | #name inside a component | Name, At |
InlineComponent | :name[label]{attrs} | Name, Attrs, At (the label is its children) |
Span | [text]{attrs} | Attrs, At |
MergeTag | {{path}}, {{path | fallback}} in prose | Tag |
var (
KindComponent = ast.NewNodeKind("Component")
KindSlot = ast.NewNodeKind("Slot")
KindInlineComponent = ast.NewNodeKind("InlineComponent")
KindSpan = ast.NewNodeKind("Span")
KindMergeTag = ast.NewNodeKind("MergeTag")
)
The node kinds those nodes return from Kind().
Import lttr/data
import "github.com/sulv-io/lttr/data"
Money, the structured types templates bind to, the recipient, and the path resolver the compiler uses to check merge tags. See Merge tags and bindings.
Money
type Money struct {
Minor int64 // minor units, e.g. cents
Currency Currency
}
func XCD(minor int64) Money
func USD(minor int64) Money
func (m Money) String() string // "EC$1,019.90", "US$378", "−US$65"
func (m Money) Approx() string // "≈ US$378"
type Exact Money
func (e Exact) String() string // always two decimals: "EC$1,085.00"
type Currency string
const (
XCDCode Currency = "XCD" // EC$
USDCode Currency = "USD" // US$
EURCode Currency = "EUR" // €
GBPCode Currency = "GBP" // £
)
| Identifier | Does |
|---|---|
Money | An amount in a currency's minor units. |
XCD, USD | Build a Money in Eastern Caribbean or US dollars. |
Money.String | A currency prefix, comma thousands separators, decimals dropped for whole amounts, and a U+2212 minus before the prefix. |
Money.Approx | Rounded half away from zero to whole units, prefixed ≈ . |
Exact | A Money that always shows two decimals, for breakdown copy that must match exactly. |
Currency | An ISO 4217-style code. The four constants have a known prefix and formatting. |
Structured types
type Receipt struct {
Lines []ReceiptLine
Fees []Fee
Total Money
TotalUSD *Money // optional; shown as ≈ US$
TotalLabel string // default "Total"
}
type ReceiptLine struct {
Name, Detail string
Qty int
Amount Money
Thumb string
Tone Tone
}
type Fee struct {
Label string
Amount Money
}
type Product struct {
Name string
Price Money
Href, Image string
Tone Tone
}
type StockItem struct {
Name, SKU string
Left int
}
type Tone string
const (
ToneNone Tone = ""
ToneWarm Tone = "warm"
ToneSea Tone = "sea"
ToneSun Tone = "sun"
ToneMap Tone = "map"
)
func (t Tone) Valid() bool
| Type | Binds to |
|---|---|
Receipt | ::receipt{:order="…"}: line items, fees and a total. |
ReceiptLine | One line of a receipt. |
Fee | A labelled charge on a receipt, such as delivery. |
Product | One tile of ::products{:items="…"} (a []Product). |
StockItem | One row of ::stock{:items="…"} (a []StockItem). |
Tone | The placeholder ground a tile or band shows with no image. Valid reports whether it is a known value. |
Recipient
type Recipient struct {
Address string // RFC 5322 address
FirstName string `json:"first_name"`
Region string `json:"region"`
EntityAddress string `json:"entity_address"`
PreferencesURL string `json:"preferences_url"`
UnsubscribeURL string `json:"unsubscribe_url"`
}
The person an email is addressed to. It is always searched second when a path is resolved, after the template's own data type, so {{first_name}} works in every template. The root package re-exports it as lttr.Recipient.
Path resolution
func NewScope(roots ...Root) Scope
func (s Scope) Resolve(path string) (Accessor, error)
type Root struct {
Slot int
Type reflect.Type
}
func (a Accessor) Get(roots []reflect.Value) (reflect.Value, bool)
func (a Accessor) Path() string
func (a Accessor) Type() reflect.Type
func IsScalar(t reflect.Type) bool
func FormatScalar(v reflect.Value) string
| Identifier | Does |
|---|---|
Scope, NewScope | An ordered, immutable set of roots. A path's first segment is looked up in each root in turn, and the first match wins. |
Root | One root of a scope: the value at Slot in the slice passed to Get. |
Scope.Resolve | Compiles a dotted path into an Accessor once, at compile time. It follows JSON names, promoted fields, pointers and map keys. |
Accessor | A precompiled path, a plain value that is safe to copy. |
Accessor.Get | Walks the path against the roots. It returns false on a nil pointer or a missing map key, and never parses or allocates path strings. |
Accessor.Path, Accessor.Type | The dotted path, and the type it resolves to with pointers dereferenced. |
IsScalar | Whether a type may appear in a merge tag: a value-receiver fmt.Stringer, a string kind or an integer kind. |
FormatScalar | A scalar's text: String(), the string itself, or the decimal integer. |
Import lttr/ir
import "github.com/sulv-io/lttr/ir"
The compiler: a parsed template becomes typed module IR, with every merge tag resolved against the data type at compile time, so rendering never parses paths. Most consumers only touch ir.Error (to read compile errors) and ir.Kind (for Family.Forbidden). See Compile errors and Modules.
Compile
func Compile(name string, src []byte, opts Options) (*Document, error)
func CompileCopy(label, src string, vars reflect.Type) (Inline, error)
func CompileCopyText(label, src string, vars reflect.Type) (Text, error)
type Options struct {
Data reflect.Type // the template's T
Recipient reflect.Type // reflect.TypeOf(data.Recipient{})
Vars reflect.Type // lttr.Vars: {name} placeholders in footer.* frontmatter
}
type Document struct {
Name string
Front Frontmatter
Modules []Module
Warnings []Diagnostic
Tags []TagUse
}
| Identifier | Does |
|---|---|
Compile | Parses and compiles src into a Document: frontmatter, implicit letters, typed modules, resolved merge tags and bindings, and every static rule. It returns the document or every error joined, never both. Family rules are checked by lttr.Compile, not here. |
CompileCopy | Compiles family copy: one paragraph of Markdown with {name} placeholders from vars and no merge tags. |
CompileCopyText | The same for plain copy with no Markdown. |
Options | The Go types a template is compiled against. |
Document | A compiled template, immutable once returned. |
Errors and diagnostics
type Error struct {
Name string // template (or copy) name
Pos Pos
Msg string
}
func (e *Error) Error() string // "name:line:col: msg", or "line:col: msg" without a name
type Diagnostic struct {
Pos Pos
Msg string
}
func (d Diagnostic) Format(name string) string // "name:line:col: warning: msg"
type Pos = comark.Pos
Error is a compile or render error at a position. Reach it through a joined error with errors.As. Diagnostic is a compile warning, and warnings never fail compilation.
Frontmatter and tags
type Frontmatter struct {
Template, Family string
Subject, Preheader, From, Issue Text
Legal bool
Footer FooterOverrides
Pos map[string]Pos // dotted key → key position
}
type FooterOverrides struct {
Note, Reason, OptOut *Inline // Markdown, one paragraph
About *Text // F2 {about}; empty drops the service line
Nav *bool // F1 nav row
}
type TagUse struct {
Path string
Type reflect.Type // the resolved type, pointers dereferenced
Pos Pos
Binding bool // a :key="path" binding rather than a {{path}} tag
}
Frontmatter is the compiled template header. FooterOverrides holds the footer.* keys, where nil means "use the family copy". TagUse records one merge tag or binding, and lttr.TagInfo is its string form.
Modules
type Module interface {
Kind() Kind
Class() Class
Pos() Pos
}
type Kind string
func Lookup(name string) (Kind, bool)
type Class uint8
const (
ClassHero Class = iota + 1
ClassBand
ClassTabular
ClassBody
ClassNotice
ClassSignoff
ClassTail
ClassRule
ClassEditorial
)
Module is one compiled top-level module. Every module type implements it with pointer receivers, so Document.Modules holds *Statement, *Figure and so on. Kind names a module. Lookup maps a component name or handoff alias to its kind, case-insensitively. Class groups modules for layout spacing.
| Type | Kind constant | Value | Alias | Class | Fields |
|---|---|---|---|---|---|
Statement | KindStatement | statement | k1 | Hero | At, Eyebrow, Orange, XL, Blocks |
Figure | KindFigure | figure | k2 | Hero | At, Eyebrow, Amount, USD, Blocks |
Offer | KindOffer | offer | e3 | Hero | At, Eyebrow, Heading, Terms |
Image | KindImage | image | k3 | Band | At, Src, Alt, Height, Tone |
Map | KindMap | map | t4 | Band | At, Src, Alt, Height |
Letter | KindLetter | letter | b1 | Body | At, Blocks |
Meta | KindMeta | meta | t3 | Body | At, Pairs []MetaPair |
Quote | KindQuote | quote | e2 | Body | At, Cite, Body |
Notice | KindNotice | notice | b2 | Notice | At, Title, Warn, Blocks |
Code | KindCode | code | b3 | Tail | At, Value, Blocks (at most one paragraph) |
Progress | KindProgress | progress | t2 | Tail | At, Steps, StepsRef, Current, CurrentRef, CurrentIndex |
Actions | KindActions | actions | c1, c2, c3 | Tail | At, Row ActionRow |
Breakdown | KindBreakdown | breakdown | b4 | Tabular | At, Rows |
Compare | KindCompare | compare | b5 | Tabular | At, Header, Rows, Align |
Table | KindTable | table | Tabular | At, Headless, Header, Rows, Align | |
Receipt | KindReceipt | receipt | t1 | Tabular | At, Order data.Accessor, TotalLabel |
Stock | KindStock | stock | t5 | Tabular | At, Items data.Accessor |
Stories | KindStories | stories | e1 | Editorial | At, Items []Story |
Products | KindProducts | products | e4 | Editorial | At, Items []ProductTile, ItemsRef, CTA |
Signature | KindSignature | signature | s1 | Signoff | At, Name, Role, Contact |
Signoff | KindSignoff | signoff | s2 | Signoff | At, Closing, Name (empty: "The {brand} team") |
Divider | KindDivider | divider | Rule | At |
Each module type has the three Module methods, Kind(), Class() and Pos(). The parts of modules:
| Type | Is |
|---|---|
MetaPair | One ### Label and its paragraph: Label, Body |
Story | One :::story tile: At, Eyebrow, Image, Alt, Href, CTA, Tone, and Title (its body) |
ProductTile | One :::product tile: At, Name, Price, Image, Alt, Href, CTA, Tone |
Action | A :button (Primary) or :link (Arrow) in an action row: Label, Href, Pos |
Row | One table row, []Inline, one per column |
Align | A GFM column alignment: AlignNone, AlignLeft, AlignCenter, AlignRight |
Blocks and inline content
type Block interface{ /* unexported */ } // Heading, Para, ActionRow or List
type InlineNode interface{ /* unexported */ } // TextRun, Strong, Em, CodeSpan, Link, Span or Break
type Inline []InlineNode
| Type | Is |
|---|---|
Heading | Level 1–6 and Body Inline |
Para | A paragraph, Body Inline |
ActionRow | A paragraph made only of :button and :link: Items []Action |
List | A bulleted or Ordered list, one paragraph per item: Items []Inline |
TextRun | Text with merge tags, Value Text. Renderers escape merge values and never parse them as Markdown. |
Strong, Em | **strong** and *emphasis*, Body Inline |
CodeSpan | `code`, Value Text. It may hold merge tags. |
Link | [body](href), an autolink or a :link in running text: Href Text, Body Inline |
Span | [body]{.class …}: Style SpanStyle, Body Inline |
Break | A line break, Hard or soft |
type SpanStyle uint8
const (
Muted SpanStyle = 1 << iota // .muted
Teal // .teal
Mono // .mono
Small // .small
Bold // .bold
)
func (s SpanStyle) Has(x SpanStyle) bool
SpanStyle is a set of span classes, one flag per allowed class. Has reports whether s includes every style in x.
Text and evaluation
type Text []Part
type Part struct {
Lit string
Ref *Ref
}
type Ref struct {
Path string
Fallback string
HasFallback bool
Pos Pos
Get data.Accessor // precompiled; indexes Env.Roots
Placeholder bool // a {name} placeholder rather than a {{path}} merge tag
}
func (r *Ref) String() string // {{path}} or {name}
type Env struct {
Name string // template name, for error positions
Roots []reflect.Value // [data, recipient, vars], indexed by the Slot constants
}
const (
SlotData = 0
SlotRecipient = 1
SlotVars = 2
)
func Lit(s string) Text
func (t Text) Literal() (string, bool)
func (t Text) Eval(env Env) (string, error)
func (t Text) EvalOptional(env Env) (string, error)
func (t Text) EvalURL(env Env) (string, error)
func (t Text) EvalURLOptional(env Env) (string, error)
| Identifier | Does |
|---|---|
Text | Literal text with references resolved at compile time. Adjacent literals are always merged. |
Part | One piece of a Text: a literal (Ref == nil) or a reference. |
Ref | A compiled merge tag or copy placeholder. |
Env | The data a Text is evaluated against. |
SlotData, SlotRecipient, SlotVars | The indexes of Env.Roots, which is always [data, recipient, vars]. |
Lit | A literal Text. The empty string gives nil. |
Literal | The text and true when t has no references. |
Eval | Renders t. A reference with no value (unresolvable, or formatting to "") uses its fallback. Without one it is an *Error "no value for /reference/api-reference". |
EvalOptional | Like Eval, but a missing value with no fallback gives "". Used for an optional image or map src. |
EvalURL | Eval for an href or src. A reference that opens the text is inserted as is, so {{track_url}} and {site}/help work. Every later value, fallback included, is percent-escaped for its place: as a query value once the URL has a ? (spaces as %20), otherwise as a path segment, so mailto:{{email}} keeps its address. A later reference inside the authority (after //, before any /, ? or #) is not escaped. It must be a single DNS label, immediately preceded by literal // or ., and immediately followed by literal text starting with ., as in https://{{shop}}.example.com/. Anything else — including no value there, even in EvalURLOptional — is an *Error at the reference, "merge value /reference/api-reference cannot form part of a url host". A value can't add a query parameter, change the path or fragment, or move the host. |
EvalURLOptional | EvalURL with EvalOptional's missing-value rule. |
Import lttr/layout
import "github.com/sulv-io/lttr/layout"
Arranges compiled modules into a document the renderers draw: header and footer chrome, and the padding above and below every module. See Layout and spacing.
func Arrange(mods []ir.Module, header HeaderVariant) []Section
type Section struct {
Module ir.Module
PadTop, PadBottom int
}
type Document struct {
Name string // template name, for render errors
Subject, Preheader string
Header Header
Sections []Section
Footer Footer
Theme theme.Theme
AllowRelativeURLs bool
}
| Identifier | Does |
|---|---|
Arrange | Lays modules into sections with their padding. Spacing is decided only here, in three passes: class defaults, then the pair rules, then first-module, last-module and header rules. It doesn't modify mods. |
Section | One module with the padding (px) above and below it. |
Document | A laid-out email, ready for the HTML and text renderers. |
type Header struct {
Variant HeaderVariant
Banner string // H2 banner text
Masthead string // H3 masthead text
LogoURL string
LogoDarkURL string
}
type Footer struct {
Variant FooterVariant
Nav []ir.Action // F1 nav row; empty = no row
Lead ir.Inline // F1 reason · F2 note · F3 opt-out · F3·i internal line
Links ir.Inline // F1 unsubscribe and preferences links
Service ir.Inline // F2 service line; empty = dropped
Address ir.Text // "{brand} · {address}"
TextURLs []TextURL // plain-text footer URL lines, in order
}
type TextURL struct {
Label string
URL ir.Text
Source string // the copy it was written in, naming render errors
}
type HeaderVariant uint8
const (
H1 HeaderVariant = iota + 1 // logo
H2 // logo with the internal banner
H3 // newsletter masthead
)
type FooterVariant uint8
const (
F1 FooterVariant = iota + 1 // marketing: nav, reason, unsubscribe and preferences
F2 // service: note, "service email about …", address
F3 // outreach: address and opt-out
F3Internal // internal: internal line and address
)
Header is the evaluated header chrome. Footer is the footer chrome as compiled copy, evaluated by the renderers against the message Env. TextURL is one plain-text footer line. The root package re-exports the variants as lttr.H1… and lttr.F1….
type Rule struct {
Prev, Next Selector
PrevBottom, NextTop *int
Source string // the handoff frame the value comes from, e.g. "Q2 (D10)"
}
type Selector struct {
Class ir.Class
Kind ir.Kind // empty matches any kind of Class
}
Rule overrides the class defaults for one adjacent pair of sections, and Selector matches a module by class and, optionally, kind. The rule table itself is unexported.
Import lttr/theme
import "github.com/sulv-io/lttr/theme"
The brand values a renderer draws with. A leaf package: it imports only the standard library. See Themes.
type Theme struct {
Light Light
Dark Dark
Fonts Fonts
Brand Brand
}
func (t Theme) Validate() error
type Color string
func (c Color) Valid() bool
type Fonts struct {
Serif, Sans, Mono string
}
type Brand struct {
Name string // header text, logo alt and the default sign-off
Logo string // file name under Config.AssetBase, e.g. "logo.png" (72×72, shown at LogoSize)
LogoDark string // white-chip version for dark mode
LogoSize int // display size in CSS pixels, 16..96
}
| Identifier | Does |
|---|---|
Theme | Everything brand-specific a renderer needs. |
Theme.Validate | Reports every invalid value, one line per problem, or nil: colours must be #RRGGBB, font stacks non-empty and made only of [A-Za-z0-9 ,'-], Brand.Name non-empty, logo names plain file names, and LogoSize 16 to 96. |
Color | A hex colour, #RRGGBB. Valid checks exactly that. |
Fonts | The three CSS font stacks. |
Brand | The sender's mark. |
type Light struct {
Ink, Body, Secondary, StrongBody Color
Action, PillText, Accent Color
Hairline, RowRule, NoticeBorder Color
Ground, Canvas Color
WarnBg, WarnBorder, WarnText Color
OutOfStock Color
BannerBg, BannerText Color
Warm, Sea, Map, Sun Color
}
type Dark struct {
Canvas, Footer, Hairline Color
Headline, Body Color
Action, PillText Color
CurrentStep, AccentText Color
BannerBg, BannerText Color
Placeholder Color
WarnBg, WarnBorder, WarnText Color
OutOfStock Color
}
Light is the light-mode palette. Dark is the dark-mode palette, applied by the head CSS under prefers-color-scheme: dark and by the Outlook.com data-ogsc/data-ogsb hooks.
Import lttr/render/html
import htmlr "github.com/sulv-io/lttr/render/html"
Renders a laid-out document as Outlook-safe, table-based HTML. The package is named html, so import it under an alias if you also use the standard library's html or html/template. See HTML output.
func Render(doc layout.Document, env ir.Env) (string, error)
func CheckURL(raw string, allowRelative bool) (template.URL, error)
type Pill struct {
Href template.URL
Label string
Fill, Text theme.Color
Width int // estimated width in px; zero means computed from Label
}
type Style struct{ /* unexported */ }
func (s Style) CSS() template.CSS
| Identifier | Does |
|---|---|
Render | Renders a complete HTML email. It returns the HTML or an error, never both. |
CheckURL | Vets a URL before it may appear in an href or src. Allowed schemes are https, http, mailto and tel, and relative URLs only with allowRelative. Whitespace or a control character anywhere is an error, and a bad URL is always an error, never a substituted #. It returns the URL unchanged as template.URL. |
Pill | A call-to-action button as Outlook draws it: a VML rounded rectangle. Href must already have passed CheckURL. |
Style | An inline style built only from package constants and validated theme colours. Its builders are unexported, so no caller can put arbitrary text into a style attribute. |
Style.CSS | The declarations as prop:value;prop:value, typed for html/template. |
Import lttr/render/text
import "github.com/sulv-io/lttr/render/text"
func Render(doc layout.Document, env ir.Env) (string, error)
Renders the plain-text part from the laid-out document, working from the typed IR rather than the HTML: blocks separated by one blank line, 70-column wrapping, uppercase eyebrows, Label: plus URL for actions, and dot-leader tables. It returns the text or an error, never both. See Plain text.
Import lttr/mapbox
import "github.com/sulv-io/lttr/mapbox"
Builds Mapbox Static Images API URLs for the map band and fetches the image. The API URL carries your access token, so it must never go into an email. Fetch the image at send time, upload it to your CDN, and put only the CDN URL in ::map{src}. See Maps.
type Static struct {
Token string
Style string // owner/id map style
Width int // default 600
Height int // default 240
Padding int // default 80
RouteColor string // route line stroke; required, six hex digits without '#'
PickupColor string // pickup pin; required
DropoffColor string // drop-off pin; required
}
func (s Static) URL(pickup, dropoff Point, route []Point) (string, error)
func (s Static) Fetch(ctx context.Context, c *http.Client, pickup, dropoff Point, route []Point) ([]byte, error)
type Point struct {
Lng, Lat float64
}
| Identifier | Does |
|---|---|
Static | Builds Static Images API requests. The three colours are the brand's, so the package has no defaults for them. quiet.Mapbox(token, style) shows one set up. |
Static.URL | The API URL for a pickup, a drop-off and a route. With an empty route, the line runs straight from pickup to drop-off. No error it returns contains the token. |
Static.Fetch | GETs the image and returns its bytes. A non-200 response is an error naming only the status code, never the URL, and a body over 8 MiB is an error. |
Point | A coordinate: longitude, latitude in decimal degrees. |