Library reference

Library reference

This section documents htmlbag’s Go API for code that imports github.com/boxesandglue/htmlbag directly. If you only consume htmlbag through bagme or glu, the content pages are what you need; this section is for embedders.

API stability: htmlbag has gone through several substantial refactors (Insert system, two-pass page assembly, CSS-property-based float detection) and more changes are expected before it stabilizes. Treat the API as moving until htmlbag (or its parent module) hits a tagged 1.0 release. The conceptual shape — CSSBuilder, OutputPagesFromText, the configuration fields — is unlikely to change dramatically, but field names and method signatures may.

Quick start

The minimal program: parse an HTML chunk, output it to a PDF.

package main

import (
    "log"

    "github.com/boxesandglue/boxesandglue/frontend"
    "github.com/boxesandglue/htmlbag"
)

func main() {
    fd, err := frontend.New("out.pdf")
    if err != nil { log.Fatal(err) }
    defer fd.Doc.Finish()

    // New loads the built-in font families (sans, serif, monospace).
    // NewCSSParserWithDefaults adds the user-agent stylesheet, so h1, p
    // and friends get their usual block behavior; NewCSSParser starts
    // from nothing and leaves all of that to you.
    cb, err := htmlbag.New(fd, htmlbag.NewCSSParserWithDefaults())
    if err != nil { log.Fatal(err) }

    if err := cb.AddCSS(`
        @page { size: A5; margin: 2cm }
        body { font-family: serif; font-size: 11pt; line-height: 1.4 }
    `); err != nil { log.Fatal(err) }

    te, err := cb.HTMLToText(`<html><body>
        <h1>Hello</h1>
        <p>World.</p>
    </body></html>`)
    if err != nil { log.Fatal(err) }

    if err := cb.OutputPagesFromText(te); err != nil { log.Fatal(err) }
}

For the canonical CSSBuilder construction inside speedata’s tools, look at how glu/lua/htmlbag/ and bagme/ set things up — they handle font loading, default styles, and structure tagging.

Core types

Type Role
CSS the stylesheet side: collects CSS from strings and files, matches the selectors against the DOM, and resolves the computed properties. Built with NewCSSParserWithDefaults (or NewCSSParser) and handed to New. Lived in the separate csshtml package until htmlbag v0.0.54
CSSBuilder central state — holds the CSS context, the in-flight page buffer, configuration knobs, and the current document accumulators (counters, headings, structure tree)
HTMLItem one node in the parsed and CSS-resolved DOM, with Styles and Attributes maps
Insert one extracted page-layer item (footnote or float) ready for placement
InsertClass distinguishes InsertFootnote, InsertFloatTop, InsertFloatBottom
HeadingEntry record of one heading found during VList construction; Page filled at output time

Core methods

Method When to use
NewCSSParserWithDefaults() *CSS build the CSS side with the user-agent stylesheet
NewCSSParser() *CSS same, but with no user-agent stylesheet
New(*frontend.Document, *CSS) (*CSSBuilder, error) construct; also loads the built-in fonts
AddCSS(string) error add CSS rules, resolving relative URLs against the working directory
ReadCSSFile(string) error add CSS from a file, resolving relative URLs against that file’s directory
HTMLToText(string) (*frontend.Text, error) parse + style HTML, return the styled tree
ParseHTMLFromNode(*html.Node) (*frontend.Text, error) same, from a pre-parsed DOM
CreateVlist(*frontend.Text, ScaledPoint) (*node.VList, error) format a styled tree into a VList without paginating
OutputPagesFromText(*frontend.Text) error paginate a styled tree, splitting at forced page breaks
FlowText(*frontend.Text, Regions) error pour a styled tree into rectangles you hand out, such as columns or frames, instead of pages (see Flowing into regions)
NewPage() error force a page break (mid-output)
InlineNode(node.Node) *html.Node an element for a DOM given to ParseHTMLFromNode that puts a node of your own into the line it is in, such as a node.StartStop whose ShipoutCallback learns the line’s page
RegisterLineModel(string, LineModelFunc) error add a value of -bag-leading-model that sets lines by a model of your own (see Line models)
RegisterBreaker(string, BreakerFunc) error add a value of -bag-line-breaker that chooses a paragraph’s breaks by a breaker of your own (see Breakers)

Line models

-bag-leading-model knows half and trailing. A program can add names of its own: RegisterLineModel maps a name to a function that returns a node.LineModel (see Linebreaking) for each paragraph that selects it. The function is told the paragraph’s font size, line height and language in a LineModelStyles. This one sets every line in a box of the line height, however large its text, as a word processor’s “exactly” line spacing does:

type exactly struct {
    height, depth bag.ScaledPoint
}

func (m exactly) LineBox(line *node.HList, settings *node.LinebreakSettings) (bag.ScaledPoint, bag.ScaledPoint) {
    return m.height, m.depth
}

func (m exactly) Leading(line *node.HList, settings *node.LinebreakSettings) *node.Glue {
    return nil
}
err := cb.RegisterLineModel("exactly", func(s htmlbag.LineModelStyles) node.LineModel {
    // 80% of the line height above the baseline, 20% below.
    return exactly{height: s.LineHeight * 8 / 10, depth: s.LineHeight * 2 / 10}
})
if err != nil { log.Fatal(err) }
cb.AddCSS(`p.exact { -bag-leading-model: exactly; line-height: 14pt }`)

LineModelStyles.Font is the paragraph’s own font at its size, with the face’s vertical metrics. A line without glyphs, such as one between two <br>, has no font of its own; a model that sizes lines by their fonts takes the strut of such a line from Font, as CSS does. It is nil when the font cannot be loaded.

Names are case-insensitive, half, trailing and the CSS-wide keywords are reserved, and a name used in CSS but never registered keeps the built-in leading with a warning. A model is also the only reader of the ascent-override, descent-override and line-gap-override descriptors of @font-face: it finds each glyph’s font, with these metrics, on the line.

Breakers

-bag-line-breaker is auto by default: the paragraph is broken by Knuth-Plass. RegisterBreaker maps a name to a function that returns a node.Breaker for each paragraph that selects it; the function is told the paragraph’s font size and language in a BreakerStyles. The breaker only chooses among the legal breakpoints. The lines are still measured, packed and set as with Knuth-Plass, and a line model still applies. A nil breaker keeps Knuth-Plass. This one fills each line with as much as fits at its natural width, as a word processor does:

type greedy struct{}

func (greedy) Breaks(p *node.BreakProblem) []int {
    var breaks []int
    from, fits := -1, -1
    for i := 0; i < len(p.Candidates); i++ {
        f := p.Fit(from, i, len(breaks))
        switch {
        case f.Natural <= f.Measure && p.Candidates[i].Forced():
            breaks, from, fits = append(breaks, i), i, -1
        case f.Natural <= f.Measure:
            fits = i
        case fits >= 0:
            // End the line at the last break that fit, measure i again.
            breaks, from, fits = append(breaks, fits), fits, -1
            i--
        default:
            // Nothing fits: the line is overfull up to i.
            breaks, from = append(breaks, i), i
        }
    }
    return breaks
}
err := cb.RegisterBreaker("greedy", func(s htmlbag.BreakerStyles) node.Breaker {
    return greedy{}
})
if err != nil { log.Fatal(err) }
cb.AddCSS(`p.code { -bag-line-breaker: greedy }`)

The property is inherited and applies to the paragraph as a whole, so it has no effect on a span. Names are case-insensitive, auto and the CSS-wide keywords are reserved, and a name used in CSS but never registered keeps Knuth-Plass with a warning.

Subsections