CSS support
htmlbag matches stylesheet rules against the DOM with its own CSS
parser and selector engine; both lived in the separate package
csshtml up to htmlbag v0.0.53. CSS applies through three sources,
in order of increasing specificity:
<style>blocks in<head>.<link rel="stylesheet" href="…">external files.- Inline
style="…"attributes.
Recognized properties
The CSS properties reference lists every property htmlbag reads with its accepted values. It is generated from the code, so it is the authoritative list. The sections below explain the behavior behind some of the properties.
Box model
On tables border-collapse and border-spacing have the CSS defaults
separate and 2pt, so a table wants border-collapse: collapse like in
a browser.
Typography
Fonts, alignment, decoration and spacing are in the reference. Two features deserve a closer look:
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; } /* the font's 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).
font-variant-numeric selects figures by name, which is the better
choice for them. The value is inherited and replaced as a whole, so a
child can set normal or another style:
body { font-variant-numeric: oldstyle-nums; } /* figures in running text */
table { font-variant-numeric: lining-nums tabular-nums; } /* columns line up */
.recipe { font-variant-numeric: diagonal-fractions; } /* 1/2 as a real fraction */The keywords are lining-nums or oldstyle-nums, proportional-nums
or tabular-nums, diagonal-fractions or stacked-fractions, ordinal
and slashed-zero. They switch on the OpenType features lnum, onum,
pnum, tnum, frac, afrc, ordn and zero, so a face without the
feature keeps its figures, nothing is synthesized. Crimson Pro has all of
them except afrc and zero, TeX Gyre Heros has zero but not ordn.
font-feature-settings wins over font-variant-numeric, as in the
spec, and font-variant takes the same keywords and resets the ones it
does not name.
"smcp" needs a font that has small caps. font-variant-caps: small-caps (also through font-variant and the font shorthand) works
with any font: it uses smcp when the face has it and synthesizes the
small caps otherwise, as capitals at 70% of the size;
font-synthesis-small-caps: none turns the synthesis off.
The last line (text-align-last)
text-align-last aligns the last line of a paragraph and every line
before a <br>. With auto, the default, it follows text-align, and
a justified paragraph ends at the start.
.verse { text-align: justify; text-align-last: center; }
.block { text-align: justify-all; } /* the last line as well */
.credit { text-align: left; text-align-last: right; }It aligns the line and leaves the line breaks alone, so
text-align-last: justify spreads a short last line instead of breaking
the paragraph anew. A line without a space stays at the start.
text-align leaves text-align-last alone, as in browsers, so an
inherited value stays when a child sets text-align. CSS Text 3 has it
reset to auto, which no stylesheet written for a browser expects. Only
justify-all sets both to justify.
Hyphenation
hyphens: auto breaks words with the patterns of the lang attribute.
hyphenate-limit-chars sets the characters a word needs at least, and
those before and after a hyphen, and hyphenate-character sets the text
at the end of the line:
p { hyphens: auto; hyphenate-limit-chars: 6 3 2; }
.fraktur { hyphenate-character: "="; } /* a double hyphen */
.none { hyphenate-character: ""; } /* a break without a sign */The string is shaped as the text around it, takes its letter spacing,
and counts for the line, and it holds for soft hyphens (U+00AD) as well.
A font without its glyphs keeps the hyphen, and the log says which
character is missing. auto is the hyphen of the font.
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.
Page-level layout
@page rules take the descriptors listed in the
reference: the paper size, the margins, a
border box with padding and a background.
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); } }The running element is centered vertically in its margin box, as text
content is; vertical-align: top on the margin box puts it at the top.
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,
and :first cascades over the rule of its side.
@page { size: A4; margin: 2cm; }
@page :first { margin-top: 8cm; } /* room for a letterhead on page 1 only */Size and margins resolve per page, so vertical start position, content height and the left edge follow the rule in force on each page. When a later page has a different content width, running text is set again at that width; see limitations for what keeps the width it started with.
Named pages
The page property sets a block on pages of a name, and an @page rule
of that name applies to them. It is how a wide table gets a landscape
page in the middle of a portrait document:
@page { size: A4; margin: 2cm; }
@page wide { size: A4 landscape; margin: 1.5cm; }
.wide { page: wide; }A page break comes between two neighboring blocks with different names,
so the block before .wide ends its page, and so does .wide itself.
Two .wide blocks in a row share their pages. The block is set at the
width of its page, also deep inside other blocks, which are cut in two
at the change: the sides and the margin between the parts drop out, as
at a page break, and a tagged PDF has two structure elements for such a
block. The property of the first block names the first page. A page
that holds nothing because of a forced break to a side, for which
:blank applies, already has the name of the block after it.
The named rule cascades over the rules that match a page anyway, and a
name can take a pseudo-class, as in @page wide:left or
@page wide:blank. By specificity, from low to high: @page, :left
and :right, :first and :blank, the name, the name with :left or
:right, the name with :first or :blank. A selector list such as
@page wide, tall sets the rule for each. :first is the first page of
the document, not of the name.
bleed and marks hold for the whole document, the rule of the first
page decides.
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.- Behind the scenes each page’s background resolves through the same
@pagecascade as its margins, so:first/:left/:rightall select their own background.
Page breaks
break-before: always (or page-break-before) starts an element on a
new page, break-after: always starts the content after it on one.
break-before: column and break-after: column move on to the next
column of a multi-column layout (see Columns).
break-after: avoid keeps an element on the page of the one that
follows; a chain of them moves on together, so a heading and a
subheading with avoid go to the next page with the first lines of
the text after them instead of staying alone at the foot of a page.
h1, h2 { break-after: avoid; }
figure { break-inside: avoid; }break-inside: avoid keeps a block in one piece: when it does not
fit, it starts on the next page. A block taller than a page splits all
the same, and a forced break inside it is taken. Without it, a block
breaks at any depth: a container between its children or through one
of them, with its border and padding open on the cut sides, and a
paragraph between its lines.
widows and orphans are the fewest lines of a paragraph left at the
top of the next page and at the foot of the current one. Both default
to 2; a paragraph that would leave fewer starts a line earlier or moves
on whole, and 1 switches the control off. For table rows see
Tables, for the typographic side with examples
Page breaks in the glu chapter.
Columns
column-count sets the content in columns, on body or on a direct
child of body without a border, a background or padding, whose
siblings then span the columns. Every page holds that many columns side
by side, column-gap apart (1em by default), filled one after the
other, and column-rule draws a line in the middle of the gap between
two columns that both hold content. The floats and footnotes of a
column are placed in it, at its width.
body { column-count: 2; column-gap: 6mm; column-rule: 0.5pt solid gray; }
h1 { column-span: all; }
h2 { break-before: column; }A block with column-span: all stands across the page below the
tallest column before it, and the columns go on below it. With
column-fill: balance, the default, the columns of the last page are
evened out; auto fills them one after the other. The columns above a
spanner are balanced either way. break-before: column starts the
next column, and the next page after the last one. On any other
element, column-count is set in one column with a warning, and
FlowText does not lay it out at all; see
limitations.
Insert positioning
float: top | before | bottom | after — see inserts.
The standard CSS float: left | right places a side float with the
text wrapping around it. A float without a width is as wide as its
longest line, at most as wide as its container; where it leaves no room
beside it, the text goes on below it. The float’s margin on the side of the text is
the space between them; where that margin is zero, htmlbag
leaves -bag-float-gutter (9pt by default) instead. A negative margin
lets the text run over the float by that much, as in CSS. A browser leaves
nothing there, and margin: 0 cannot be told from no margin at all, so
a stylesheet that wants the browser’s spacing switches the gutter off:
body { -bag-float-gutter: 0 } /* inherited by every float */
.figure { -bag-float-gutter: 1em }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.
display: flow-root is a block that keeps clear of a float as a whole.
A block beside a float runs on under it, with its border and
background, and only its lines give way; a box with flow-root is
set narrower beside the float instead, the way a framed box beside a
picture is usually meant.
Lists and markers
list-style-type (disc, decimal, lower-roman, upper-alpha, …)
controls the visible marker on <ol> / <ul> items.
list-style-position: outside (the 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 is accepted but not implemented yet, the marker stays outside.
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 |
The largest adjustment ratio a line may have: how far its spaces may stretch, as a multiple of their stretchability. A line never shrinks by more than its shrinkability, and below 1 the tolerance limits shrinking as well. Higher values let the line breaker accept looser lines, lower values give more even spacing and more overfull lines where no break is good enough. Unlike TeX’s \tolerance it is a ratio, not a badness: TeX’s 200 is about 1.26, its 10000 about 4.6. |
-bag-linebreak-hyphen-penalty |
int | 50 |
The penalty of a break at a hyphenation point, soft hyphens included (TeX \hyphenpenalty). As in TeX, its square goes into the demerits of the line, and 10000 or more forbids breaks at hyphenation points. 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. |
-bag-font-expansion |
percentage | 5% |
How much a glyph may be narrowed or widened to even out the spacing of justified lines (hz). The line breaker counts on both; a line gets narrower glyphs only when its spaces have shrunk all they can. 0% turns it off. |
-bag-leading-model |
half / trailing / registered name |
half |
Where the leading of a line goes: half splits it above and below the line as CSS does, trailing puts all of it below the line as TeX does. A Go program can add models of its own with CSSBuilder.RegisterLineModel and select them by name (see the library reference). See glu’s Lines and leading. |
-bag-line-breaker |
auto / registered name |
auto |
How a paragraph chooses its breaks among its legal breakpoints. auto is Knuth-Plass; a Go program can add breakers of its own with CSSBuilder.RegisterBreaker and select them by name (see the library reference). Inherited. |
-bag-tab-stops |
list / none |
none |
Positions that a tab in the text advances to, see tab stops below. Inherited. |
/* Document-wide: more even spacing, hyphenate eagerly. */
body {
-bag-linebreak-tolerance: 2;
-bag-linebreak-hyphen-penalty: 5;
}
/* Narrow column: more permissive. */
.sidenote {
-bag-linebreak-tolerance: 8;
}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.
Tab stops
With -bag-tab-stops, a tab in the text advances to the next stop past
the text before it, so columns line up without a table:
.toc li { -bag-tab-stops: 12mm, 100% end leader(dotted); }<li>1	Introduction	3</li>A stop is a length or a percentage of the line width, optionally
followed by its alignment (start, end, center, decimal or
decimal(",")) and a leader that fills the space before it
(leader(" . "), dotted, solid or space). Where stops are set,
a tab survives white-space collapsing, but whitespace with a newline in
it stays source formatting: a tab that indents a line of the source
does not count. After a <br>, a tab on the same line starts the new
line at the first stop. glu’s tab stops how-to
has a worked example.
PDF bookmarks
htmlbag builds a PDF outline (the bookmark tree a viewer shows in its
navigation panel) automatically. Every h1–h6 becomes a bookmark
labeled 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.
Named colors (@-bag-color)
@-bag-color defines a color name for the document, the way DefineColor
does in xts and the speedata Publisher. The name then works in every
property that takes a color: color, background-color, the
border-*-color family and text-decoration-color.
@-bag-color muted { value: #6A6A6A; }
@-bag-color brand { model: cmyk; c: 0; m: 80; y: 90; k: 10; }
@-bag-color spot { model: spotcolor; colorname: "PANTONE 300 C"; c: 100; m: 44; y: 0; k: 0; }
h1 { color: brand; }
p.note { color: muted; border-left: 2pt solid spot; }The descriptors mirror the DefineColor attributes:
| Descriptor | Meaning |
|---|---|
value |
Any CSS color: #hex, rgb(), cmyk(), device-cmyk(), a CSS color name or a name defined by an earlier @-bag-color. Used when no model is given. |
model |
cmyk, rgb, gray (components 0 to 100), RGB, GRAY (components 0 to 255) or spotcolor. |
c, m, y, k |
The components for cmyk; the optional fallback tint for spotcolor. Percentages are accepted too. |
r, g, b |
The components for rgb and RGB. g alone is the value for gray and GRAY. |
colorname |
The ink name of a spotcolor, written into the PDF Separation color space. Defaults to the color name. |
overprint |
yes prints the color on top of what is below it instead of knocking it out, no is the default. |
overprint keeps the color below small black text from being cut out, so a
slight misregister leaves no white edge along the letters:
@-bag-color ink { model: cmyk; c: 0; m: 0; y: 0; k: 100; overprint: yes; }
body { color: ink; }It works with CMYK and spot colors, RGB and gray colors knock out, so
black does not overprint. A CMYK color prints only on the plates of its
nonzero components, which makes white disappear. Both cases warn. A PDF viewer
shows the effect only with overprint preview switched on.
A defined name overrides a CSS color of the same name, so a house style can
redefine gold without touching the rules that use it. The rule works in
external style sheets, in <style> elements and in the StyleSheet command
of xts. Colors with a value are resolved when the style sheet is
registered with the document, so a name has to be defined before it is
referenced by another @-bag-color.
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.
:is(...) and :where(...) from Selectors Level 4 match any of the
selectors in their list. :is() takes the specificity of its most
specific argument, :where() has none, so a rule written with it is
easy to override. Both are forgiving: an argument the parser does not
understand drops out of the list instead of invalidating the rule.
:host belongs to shadow trees, which a document has none of; it
matches nothing, so stylesheets written for web components, such as
:root, :host { ... }, still apply through :root.
Custom properties and var()
A declaration whose name starts with -- defines a custom property,
and var() reads it anywhere a value goes:
:root { --accent: #c00; --gap: 6pt; }
.note { --accent: #06c; }
h2, .note strong { color: var(--accent); }
p { margin-bottom: var(--gap); padding: var(--pad, 0 4pt); }Custom properties inherit: an element sees the value its nearest
ancestor sets, a rule on the element itself overrides it, and so does
a style attribute. Pseudo-elements see the values of their element.
var(--name, fallback) uses the fallback when the property is not
set; fallbacks may contain var() themselves. A value may refer to
other custom properties. A declaration whose var() has neither a
value nor a fallback is dropped, the property then behaves as if it
were not set. The substitution happens after the cascade and before
any property is read, so var() works in shorthands such as
margin and border and in the content of ::before and
::after.
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.
auto in margin-left and margin-right takes up the room that a
block’s width leaves: margin: 0 auto centers the block and
margin-left: auto alone moves it to the right edge. A block without
a width fills the line, so auto is 0 there; a table without a
width is as wide as its content and is placed by that width. An
image with display: block is placed by its side margins the same
way, whatever text-align says. auto in margin-top and
margin-bottom is 0.
What’s resolved when
CSS is fully resolved before htmlbag walks the DOM — by the time
CSSBuilder.OutputPagesFromText 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.