CSS support

CSS support

htmlbag uses csshtml to match stylesheet rules against the DOM. CSS applies through three sources, in order of increasing specificity:

  1. <style> blocks in <head>.
  2. <link rel="stylesheet" href="…"> external files.
  3. 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-image accepts raster images (PNG, JPEG) and PDF pages. SVG page backgrounds are not yet supported.
  • background-color on @page currently fills page 1 only.
  • Behind the scenes each page’s background resolves through the same @page cascade as its margins, so :first / :left / :right all 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 h1h6 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.