Running XTS from Go
The xts command is a thin layer around the Go package github.com/speedata/xts/core. A Go program can call it directly, for example to make PDFs in a web service or to register its own line models.
package main
import (
"fmt"
"log"
"os"
"github.com/speedata/xts/core"
)
func main() {
layout, err := os.Open("layout.xml")
if err != nil {
log.Fatal(err)
}
defer layout.Close()
data, err := os.Open("data.xml")
if err != nil {
log.Fatal(err)
}
defer data.Close()
cfg := &core.XTSConfig{
Layoutfile: layout,
Datafile: data,
FindFile: core.FindFile,
Jobname: "report",
OutFilename: "report.pdf",
Variables: map[string]any{"title": "Monthly report"},
}
if err := core.RunXTS(cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Info.Pages, "pages,", cfg.Info.FileSize, "bytes")
}RunXTS typesets one run and writes the PDF to OutFilename. Afterwards cfg.Info holds the number of pages and the size of the file.
The configuration
| Field | Meaning | Command line |
|---|---|---|
Layoutfile, Datafile |
The layout and the data as readers | --layout, --data |
FindFile |
Turns a file name from the layout (images, fonts, XML) into a path. core.FindFile searches the directories added with core.AddDir, then the working directory, and downloads http and https URLs |
--extradir |
Jobname |
Base name of the files XTS writes besides the PDF, such as report-aux.xml |
--jobname |
OutFilename |
The PDF file | |
Variables |
Variables the layout can read, here $title |
--var |
Mode |
The modes sd:mode() tests for |
--mode |
SuppressInfo |
A reproducible PDF, with a fixed date and no random IDs | --suppressinfo |
Tracing |
grid and gridallocation draw the grid and its allocation |
--trace |
DumpFile |
Receives the XML dump of the output | --dumpoutput |
Pdfua, Pdfa, Pdfx |
The PDF standards to claim, empty for none | --pdfua, --pdfa, --pdfx |
LineModels |
Line models by name, see below | |
Breakers |
Line breakers by name, see below |
The command line does a few things around RunXTS that a Go program does itself when it needs them: it reads xts.cfg, adds the directories, runs the Lua filter, and calls RunXTS again for --runs, so that cross-references written to the aux file in one run are resolved in the next. Messages go to the default logger of log/slog; the protocol file xts-protocol.xml is written by the command line.
Line models
LineModels maps a name to an htmlbag.LineModelFunc, a function that gets the font size, line height and language of a paragraph and returns a node.LineModel from boxes and glue. A paragraph selects it with -bag-leading-model in CSS:
cfg.LineModels = map[string]htmlbag.LineModelFunc{
"word": wordLineModel,
}p { -bag-leading-model: word; }The ascent-override, descent-override and line-gap-override descriptors of @font-face are only read by such a line model. The built-in models half (CSS line boxes, the default) and trailing (TeX style) do not use them.
htmlbag.LineModelStyles also carries the paragraph’s own font in Font, with the face’s vertical metrics. A line without glyphs, such as one between two <br>, has no font of its own, and a model takes its strut from there, as CSS does. The htmlbag library reference on boxesandglue.dev shows a complete model, and Linebreaking the node.LineModel interface.
Breakers
Breakers maps a name to an htmlbag.BreakerFunc, a function that gets the name, font size and language of a paragraph (htmlbag.BreakerStyles) and returns a node.Breaker from boxes and glue. A paragraph selects it with -bag-line-breaker in CSS, and the breaker then chooses where the paragraph breaks among its legal breakpoints in place of Knuth-Plass; the lines are measured and set as before:
cfg.Breakers = map[string]htmlbag.BreakerFunc{
"greedy": func(htmlbag.BreakerStyles) node.Breaker { return firstFit{} },
}p { -bag-line-breaker: greedy; }Here firstFit is a type whose Breaks method fills each line with as many words as fit; Linebreaking on boxesandglue.dev has such a type in full and describes node.BreakProblem. A function that returns nil keeps Knuth-Plass for that paragraph. auto, the default, is Knuth-Plass; the name is reserved and cannot be registered.