CSS support
htmlbag uses csshtml to match stylesheet rules against the DOM. CSS
applies through three sources, in order of increasing specificity:
<style>blocks in<head>.<link rel="stylesheet" href="…">external files.- Inline
style="…"attributes.
Recognised properties
Box model
width, height, margin, margin-{top,right,bottom,left},
padding, padding-{top,right,bottom,left}, border,
border-{top,right,bottom,left}, border-width, border-style,
border-color, border-radius, box-sizing.
Typography
font-family, font-size, font-weight, font-style,
text-align, text-indent, text-decoration, line-height,
color, letter-spacing, word-spacing,
text-transform, white-space, font-feature-settings,
font-variation-settings.
OpenType features
font-feature-settings takes the standard CSS syntax and passes the
features to the shaper, so small caps, superior letters, oldstyle
figures and friends come straight from the font:
.century { font-feature-settings: "sups" 1; } /* XIXe with real superiors */
.caps { font-feature-settings: "smcp" 1; } /* true small caps */
.table { font-feature-settings: "tnum" 1, "liga" 0; }A missing value means on; on, off and integers (for alternate
selectors like "salt" 2) work as in the spec, and normal resets
the accumulated list. font-variation-settings selects variable-font
axes the same way ("wght" 650).
Dropcaps (initial-letter)
initial-letter: <n> turns the block’s first letter into a dropcap
spanning n lines: the letter’s cap height reaches from the first
line’s cap line down to the baseline of line n, and the first n
lines indent to make room. Leading punctuation (an opening quote)
hangs together with the letter, per CSS Inline Layout 3.
p.opening { initial-letter: 3; }Current limits: the size must be an integer, the optional sink
argument (raised caps) is not supported, and the property is written
on the block element itself because ::first-letter pseudo-element
matching is not implemented yet.
Backgrounds
background-color, background-image, background.
Page-level layout
@page rules support size (named like A4 or explicit
<width> <height>), margin / margin-{top,right,bottom,left},
background-color and background-image.
Page margin boxes
The margin-box at-rules from CSS Paged Media 3 place generated
content into the page margins. Supported areas: @top-left,
@top-center, @top-right, @bottom-left, @bottom-center,
@bottom-right and the four corner boxes (@top-left-corner and so
on). The content property accepts strings, counter(page) and
element(...) (see running elements below):
@page {
@bottom-center { content: counter(page); }
@top-right { content: "Draft"; font-size: 8pt; color: gray; }
}Running elements
CSS GCPM running elements move body content into the page chrome: an
element with position: running(name) is removed from the normal
flow and captured under its name; a margin box places the most recent
capture on every page with content: element(name). This is how a
footer or header shows data that lives in the document body:
footer { position: running(pagefooter); }
@page { @bottom-center { content: element(pagefooter); } }Per-page rules with :first / :left / :right
The pseudo-class page selectors from CSS Paged Media 3 cascade over the
generic @page rule: a pseudo rule inherits any size, margin or
background the generic rule sets and overrides only what it redeclares.
:first matches page 1; :right and :left match odd and even pages.
@page { size: A4; margin: 2cm; }
@page :first { margin-top: 8cm; } /* room for a letterhead on page 1 only */Margins resolve per page, so vertical start position, content height and the left edge follow the rule in force on each page. (One current limit: if a later page has a different content width, the line breaking does not reflow to it. See limitations.)
Page backgrounds (letterheads)
background-image on @page paints a raster image or an imported PDF
page across the whole sheet, behind the content. Combined with the
per-page selectors this drives a letterhead without any Go or Lua code,
straight from CSS plus Markdown:
@page { size: A4; margin: 2cm; background-image: url(letterhead.pdf); -bag-background-page: 2; }
@page :first { background-image: url(letterhead.pdf); -bag-background-page: 1; }Page 1 shows page 1 of letterhead.pdf; every following page shows page
2. The custom property -bag-background-page: <n> selects the source page
of a multi-page PDF (default 1). Two separate one-page files work the
same way, one url() per rule.
Set -bag-background-page on every rule that needs it. @page :first
cascades over the generic @page and inherits any property it does not
redeclare, so a -bag-background-page: 2 on @page would otherwise leak
onto page 1. The 1 above is therefore explicit. The default of 1
applies only when no rule in the cascade sets the property at all.
The url() path is resolved relative to the document (the Markdown or
HTML file’s directory), so a bare filename next to the source is found. A
file that cannot be loaded is logged and skipped rather than aborting the
render. See the page-background-letterhead
example
for a full two-page letterhead.
Notes and limits:
background-imageaccepts raster images (PNG, JPEG) and PDF pages. SVG page backgrounds are not yet supported.background-coloron@pagecurrently fills page 1 only.- Behind the scenes each page’s background resolves through the same
@pagecascade as its margins, so:first/:left/:rightall select their own background.
Insert positioning
float: top | before | bottom | after — see inserts.
The standard CSS float: left | right (side floats with text wrap) is
not supported.
Display
display: none hides an element entirely.
display: block and display: inline override the element’s default
formatting context. A <span class="title"> with display: block
becomes a block-level container that starts on its own line; a
<div class="badge"> with display: inline flows inline with its
surrounding text. The override is read from the resolved style, so
selector-driven and class-driven rules work the same as inline styles.
Lists and markers
list-style-type (disc, decimal, lower-roman, upper-alpha, …)
controls the visible marker on <ol> / <ul> items.
list-style-position: outside (default) places the marker in the
padding band to the left of the item’s content, so multi-line items
keep their text aligned at a single left edge. list-style-position: inside reflows the marker as part of the item’s first line.
The marker itself can be styled via the ::marker pseudo-element:
li::marker {
color: darkslateblue;
font-weight: bold;
font-family: monospace;
}
li.note::marker {
content: "→ ";
}The content property on ::marker replaces the default
list-style-type glyph for that item. See the marker-pseudo
example.
Generated content and counters
::before and ::after pseudo-elements render generated content via
the content property. Strings, attribute values (attr(name)),
counter functions and the leader(…) token are supported:
h2::before {
content: counter(section) ". ";
}
figcaption::before {
content: "Figure " counter(figure) ": ";
}CSS Counters work as in CSS Lists 3:
| Property | Effect |
|---|---|
counter-reset: name [value] |
(Re)set a named counter on entering this element. |
counter-increment: name [step] |
Bump the counter by step (default 1) when this element matches. |
counter(name) |
The current value of the counter as a string. |
counters(name, sep) |
Joined values up the ancestor stack (for nested numbering like 1.2.3). |
Counters are scoped by the element tree: a counter-reset on a section
boundary clears nested counters, exactly as the spec prescribes.
body { counter-reset: section; }
h2 { counter-reset: subsection; counter-increment: section; }
h3 { counter-increment: subsection; }
h2::before { content: counter(section) " "; }
h3::before { content: counter(section) "." counter(subsection) " "; }See the numbered-sections-counters example for a full document with hierarchical numbering.
boxesandglue extensions
Vendor-prefixed (-bag-…) properties expose typesetting knobs that
have no direct CSS-spec equivalent. They take part in the normal CSS
cascade: set them on body for a document-wide default, on a class
for per-element tuning, override with !important.
| Property | Type | Default | Effect |
|---|---|---|---|
-bag-linebreak-tolerance |
float | 4 |
Knuth-Plass badness ceiling (TeX \tolerance). Higher values let the line breaker accept looser lines. TeX uses 200 for \fussy and 10000 for \sloppy. |
-bag-linebreak-hyphen-penalty |
int | 50 |
Demerits added at a hyphenation breakpoint (TeX \hyphenpenalty). Lower values encourage hyphenation; useful for German compound words where the default prefers a slightly overfull line to a hyphenated loose one. |
-bag-italic-correction |
auto / none |
none |
Heuristic kern where a slanted run directly abuts an upright one, so an italic f no longer collides with a following parenthesis or quote. OpenType fonts carry no italic-correction metric for text; the kern is derived from the boundary glyph’s ink overhang. Inherited. |
/* Document-wide: relaxed line breaker, hyphenate eagerly. */
body {
-bag-linebreak-tolerance: 200;
-bag-linebreak-hyphen-penalty: 5;
}
/* Narrow column: even more permissive. */
.sidenote {
-bag-linebreak-tolerance: 1000;
}Hyphenation patterns themselves are selected by the standard
lang="…" HTML attribute (resolved through CSS Text 3 §6 hyphens),
or — for whole documents — by the front-matter lang: key in the
Markdown frontend. Tags without TeX patterns (Arabic, Hebrew, CJK,
unknown) resolve to a no-op hyphenator and never produce break
points; the tunables above only matter when patterns are present.
PDF bookmarks
htmlbag builds a PDF outline (the bookmark tree a viewer shows in its
navigation panel) automatically. Every h1–h6 becomes a bookmark
labelled with the heading’s text, nested strictly by level: an h2 is a
child of the preceding h1, an h3 a child of the preceding h2, and so
on. A level jump (e.g. h1 straight to h3) attaches to the nearest
shallower ancestor. Clicking a bookmark jumps to the heading’s exact
vertical position on the page. A document without headings produces no
outline.
The -bag-bookmark property overrides this per element and lets any
element take part in the outline, not just headings:
| Token | Effect |
|---|---|
<integer> |
Outline nesting level (overrides the implicit heading level). |
open / closed |
Whether the entry shows its children expanded. Default open. |
none |
Remove the element from the outline. A heading stays in the document; it is just absent from the bookmark tree. |
The value is a space-separated list; an integer level and an open/closed
state may be combined in either order.
/* An <h3> that should sit at the top level of the bookmarks: */
h3.chapter { -bag-bookmark: 1; }
/* Collapse a deep section's children by default: */
h2.reference { -bag-bookmark: 2 closed; }
/* Keep an appendix heading out of the outline entirely: */
.appendix h2 { -bag-bookmark: none; }
/* Turn a non-heading element into a bookmark: */
p.toc-title { -bag-bookmark: 1; }The bookmark label is the element’s text content. Outline generation is on by default; see the library configuration (Go API) or the bagme docs for the opt-out switch.
Selector support
The full CSS3 selector grammar works: type, class, id, attribute,
descendant, child and sibling combinators, plus structural
pseudo-classes like :first-child, :last-child, :nth-child(n),
:nth-of-type(n), :not(...). A common use case is alternating-row
styling on tables — see the zebra-table
example.
Box model details
padding-left and margin-left on a block container shift child
boxes to the right and reduce their content width accordingly, exactly
as CSS prescribes. So ul { padding-left: 20pt } indents list items,
blockquote { margin-left: 20pt } indents the quote body, and
nested containers stack their shifts.
What’s resolved when
CSS is fully resolved before htmlbag walks the DOM — by the time
CSSBuilder.OutputPages runs, every element carries its computed
properties in HTMLItem.Styles (a map[string]string). This means
selector matches and class-driven rules are picked up; you do not
need to inline styles to make them visible to htmlbag’s detection.