render

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
const DefaultTitle = "Family Directory"

DefaultTitle is the Directory Title of a Directory the Editor has not named (CONTEXT.md).

View Source
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.

View Source
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

View Source
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

func Encrypt(pdf []byte, passphrase string) ([]byte, error)

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

func Markup

func Markup(d Directory) string

Markup renders d as Typst source.

The output is deterministic for a given Directory: nothing here reads the clock, the filesystem or a map in range order, so a golden comparison is a stable test.

Types

type Birthday

type Birthday struct {
	Name        string
	HouseholdID string
	Month       int
	Day         string
}

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

func Build(t *rolo.Tree, tier Tier, asOf time.Time, title string) Directory

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

	// SharedWith is the Household Name of the Household whose address this one
	// 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

type Person struct {
	Name  string
	Birth string
	Death string
	Phone string
	Email string
}

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

type Renderer struct {
	Typst       Typst
	TemplateDir string
}

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

func NewRenderer(templateDir string) (Renderer, error)

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

func (Renderer) PDF

func (r Renderer) PDF(ctx context.Context, d Directory) ([]byte, error)

PDF renders d for export.

func (Renderer) SVG

func (r Renderer) SVG(ctx context.Context, d Directory) ([][]byte, error)

SVG renders d for the in-app preview, one entry per page.

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.

const (
	Mail Tier = iota + 1
	Call
	Digital
	Full
)

The four Directory tiers, least to most revealing. Proof (§5.6) is a different kind of export and is not a Tier.

func ParseTier

func ParseTier(s string) (Tier, bool)

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.

func (Tier) Key

func (t Tier) Key() string

Key is the tier's name as a URL and a filename carry it: "mail", "call", "digital" or "full". The zero tier has no key.

func (Tier) String

func (t Tier) String() string

String is the tier's name as the footer prints it.

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 NewTypst

func NewTypst() (Typst, error)

NewTypst resolves typst on PATH.

func (Typst) CompilePDF

func (t Typst) CompilePDF(ctx context.Context, markup, templateDir string) ([]byte, error)

CompilePDF renders markup as a single PDF, for export.

func (Typst) CompileSVG

func (t Typst) CompileSVG(ctx context.Context, markup, templateDir string) ([][]byte, error)

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

func (t Typst) Version(ctx context.Context) (string, error)

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL