Documentation
¶
Overview ¶
Package render turns a Directory into a printable document.
It holds three separable things: a render model of already-rendered strings, a generator that emits Typst markup from that model, and a wrapper around the typst binary that compiles the markup to PDF or SVG (ADR-0004).
Tier rules — withholding, suppression, age gating and date truncation (§5.2, §5.3, §5.5, §5.7) — are applied in exactly one place: Build, while the values are still rolo types. Markup and the template render exactly the strings they are handed.
Every value reaches Typst inside a string literal, so quote in markup.go is the only escaper the package needs: within a literal, Typst's markup metacharacters are ordinary text. See quote's doc comment for the evidence.
Index ¶
Constants ¶
const DefaultTitle = "Family Directory"
DefaultTitle is the Directory Title of a Directory the Editor has not named (CONTEXT.md).
const Private = "[private]"
Private is what a withheld field prints: the Editor marked it hidden, and the marker stops anyone "helpfully" re-collecting it next year (§5.5).
§5.5 fixes the literal string in preference to a lock glyph: it survives any font stack, needs no legend, and reads correctly aloud to a screen reader.
const TemplateName = "directory.typ"
TemplateName is the layout file the generated markup imports. It lives on disk rather than embedded in the binary so that a layout adjustment is a file edit and a restart rather than a rebuild (ADR-0004).
Variables ¶
var ErrTypstMissing = errors.New("typst not found on PATH")
ErrTypstMissing reports that the typst binary could not be found.
Typst is a host dependency rather than vendored (ADR-0004), so its absence is an Operator-facing deployment fault, not something the Editor should ever meet as an opaque error mid-export (§5.1a).
Functions ¶
func Encrypt ¶
Encrypt protects a rendered PDF with passphrase (§5.2, ADR-0011).
passphrase becomes the user password — the one a relative types to open the file. The owner password, which governs changing the file's permissions, also opens the file: PDF standard security derives the file's encryption key from either password, so an owner password recoverable by brute force would let an attacker open the file without ever knowing passphrase. It is therefore random, at least 128 bits, generated per call, and discarded: nobody needs it, and PDF permission flags are advisory anyway (§5.8).
An empty passphrase is an error rather than a no-op, so no caller can produce an unprotected Full-tier file by passing through an unset secret. Errors never include the passphrase.
Passphrases are ordinary phrases and may contain spaces and accents. pdfcpu encrypts them correctly; do not use its Decrypt function to verify a real passphrase, as it incorrectly applies PRECIS instead of SASLprep, which rejects spaces (its NFKC normalisation accepts accents fine).
Types ¶
type Birthday ¶
Birthday is one row of the Birthday Calendar (CONTEXT.md): a living person, surname first, and the day of their birthday in its month.
Month is a column number, 1–12, not a date: which month a person's row marks is layout, and every rule about whether they appear at all was applied in Build. Day is "15", or "?" when the month is known without its day.
HouseholdID is the ID of the Household whose block lists this person. The row links to that block and prints its page. It is never printed itself.
type Directory ¶
type Directory struct {
// Title is the Directory Title the Title page and the PDF's metadata
// carry. Build supplies DefaultTitle when the Editor has set none, so it is
// never empty in a built Directory.
Title string
// GeneratedAt is the stamp every export carries. Age gating is computed at
// export time, so the same Directory exported months apart differs as people
// turn 18 — the date is what makes that legible (§5.7).
GeneratedAt string
// Tier names the audience in the footer (§5.8). Distribution is the real
// control over an exported file, and a named tier is the social pressure
// that goes with it.
Tier string
// Restricted puts DO NOT DISTRIBUTE on every page. Only Full sets it (§5.2).
Restricted bool
Households []Household
// Birthdays is the Birthday Calendar printed after the Households, already
// filtered and ordered. Empty when nobody qualifies, and then no calendar
// prints at all.
Birthdays []Birthday
}
Directory is the whole printable document.
func Build ¶
Build renders every Household in t for one audience, in the depth-first order the printed Directory uses.
This is the tier filter (§5.2–§5.5, §5.7). Every rule about what an audience may see is applied here, while the values are still rolo types; what leaves is strings, so Markup and the template cannot see an unfiltered value by accident. There is deliberately no unfiltered constructor.
asOf is a parameter rather than time.Now so the caller owns the time dependency: age gating makes exports non-reproducible (§5.7). title is the Directory Title as the Editor set it; blank means untitled.
type Household ¶
type Household struct {
// ID is the Household's stored ID. It is never printed: the template turns
// it into a label marking where this block begins, so a Birthday Calendar
// row can link to the block and print its page.
ID string
// Name is the Household Name the block prints under, e.g.
// "Daryl & Dawn (Mitchell) Yoder" (CONTEXT.md). It is not rolo's Label:
// that compact "Daryl/Dawn" form is a Path segment, for navigation, and
// stays in the editing UI.
Name string
// Memorial marks a Household whose adults have all died. It renders more
// quietly — grey, with no contact details — because there is nothing in it
// to act on, but it must appear in every tier so that descendants group
// beneath it and no Path points at a missing node (§5.4).
Memorial bool
// Contents marks a Household the Table of Contents lists: a root, or a
// child of a root (CONTEXT.md). A deeper Household's block follows its
// ancestor's, so the reader finds it from that entry. It is a layout flag,
// not a rule about what the audience may see: every tier lists the same
// Households, because every tier prints every Household.
Contents bool
// AddressLines is the Household's own address. Empty when it has none, and
// also when it shares another Household's, in which case SharedWith is set.
AddressLines []string
// uses, matching the heading the reader will look for. A shared Address
// renders as a back-reference rather than a repeated block (§3), so the two
// fields are never both populated.
SharedWith string
Anniversary string
Adults []Person
Dependents []Person
}
Household is one block of the printed Directory.
Blocks are a flat sequence: a Household never nests inside its parent's block regardless of depth (ADR-0002, §5.1).
type Person ¶
Person is one person as the Directory prints them.
Every field is an already-rendered string, never a rolo value. That is the same discipline internal/web/view.go follows and for the same reason: a tier rule cannot be forgotten about a value that never arrives here as a date or a flag. An empty field prints nothing.
Name is the row name: the surname is dropped when the Household Name already carries it, so a row reads "Dawn" beneath "Daryl & Dawn (Mitchell) Yoder".
type Renderer ¶
Renderer produces the printed Directory and its preview.
One renderer serves both, so the in-app preview cannot drift from what prints (§5.1a).
func NewRenderer ¶
NewRenderer resolves the typst binary and checks the template is present.
Both faults are Operator-facing deployment problems, so they surface at construction — while the Operator is watching startup — rather than at export time in front of the Editor (ADR-0004).
type Tier ¶
type Tier int
Tier is the audience a Directory is built for, named for what the recipient will do with it rather than by a sensitivity number (§5.2, ADR-0003).
Tiers are progressive — each admits everything the one before it does — so the admission rules below compare against a threshold. The zero Tier is not Mail: it admits nothing and truncates every living person's dates, so a caller that forgets to choose a tier fails closed.
The four Directory tiers, least to most revealing. Proof (§5.6) is a different kind of export and is not a Tier.
func ParseTier ¶
ParseTier reads a tier from its Key. Anything else — including a tier name in any other case — is not a tier, so a hand-typed URL cannot select one by accident.
type Typst ¶
type Typst struct {
// Bin is the resolved path to the typst executable.
Bin string
}
Typst invokes the typst binary as a subprocess.
Go never performs layout: it generates markup and shells out, so that one renderer produces both the in-app preview and the printed export and the preview cannot drift from what prints (§5.1a).
func (Typst) CompilePDF ¶
CompilePDF renders markup as a single PDF, for export.
func (Typst) CompileSVG ¶
CompileSVG renders markup as one SVG per page, for the in-app preview.
Typst writes a file per page, so the result is a slice. Pages come back in document order: the filenames are numeric, and a plain directory listing sorts "out-10.svg" before "out-2.svg", so they are collected by index rather than by name.
func (Typst) Version ¶
Version returns the binary's self-reported version, e.g. "typst 0.14.2 (b33de9de)".
The application verifies presence and version at startup rather than at export time, so a missing or wrong binary fails with an Operator-facing message while the Operator is still watching the logs (ADR-0004).