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
- Configuration — footnote and float layout fields, counters, page dimensions.
- Callbacks —
ElementCallback,PageInitCallback. - Flowing into regions:
FlowText, columns and frames that the caller places. - PDF/UA tagging — accessibility output via structure elements.