Help us shape the future of Go! Take the 2026 Go Developer Survey

render

package
v0.0.0-...-db6e04a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 17 Imported by: 0

README

render

internal/render owns htmlterm's HTML/CSS layout engine and render-state production: turning a parsed HTML tree plus resolved CSS into ANSI-styled terminal text, plus the position/scroll metadata the document package needs to support interactivity.

It is internal so the public root package can stay a small, generic renderer facade (Options, Renderer, New, SizeAutomatic, SizeNatural) — all of the actual layout logic lives here instead.

Who uses this

  • github.com/client9/htmlterm (root package) constructs an Engine and calls Render for one-shot rendering.
  • github.com/client9/htmlterm/document calls RenderNode directly (not Render) so it can capture the Result's Positions/ScrollOffsets/ScrollViewport/ContentOffsets/ContentOffsetsX — the data a Document needs for hit-testing, focus, scrolling, and cursor placement.

Entry points

type Options struct {
    CSS, Width, Height, IgnoreDocumentCSS, Profile,
    NoOSC8Links, MaxBlankLines, StripHiddenInline, FocusAttr /* ... */
}

func New(opts Options) (*Engine, error)

func (e *Engine) Render(htmlStr string) (string, error)
func (e *Engine) RenderHTML(htmlStr string) (Result, error)
func (e *Engine) RenderNode(doc *html.Node, req Request) Result
func (e *Engine) DocumentRules(doc *html.Node) []cssengine.Rule
  • Render/RenderHTML parse HTML themselves, for callers with no need to keep the tree around (the root package's one-shot Renderer.Render).
  • RenderNode takes an already-parsed tree plus a Request (width/height/rules/scroll offsets for this frame) and returns a Result carrying both the rendered Output string and the layout metadata (Positions, ScrollOffsets, ScrollViewport, ContentOffsets, ContentOffsetsX) needed by a live Document. This is the seam document.Document.Render uses to re-render the same tree repeatedly without losing element positions between frames.
  • DocumentRules extracts <style>-element rules from a tree, so a caller (again, document.Document) can merge them with the engine's own base rules once and cache the result instead of re-parsing on every render.

Layout and rendering

  • engine.go, render.go — Engine/Options/Viewport/Request/Result and root-level render dispatch.
  • block.go, inline.go, list.go, table.go, table_render.go — block, inline, list, and table layout.
  • box.go, wraptoken.go, blankcap.go — position tracking (Rect, mergePositions), token-level word wrapping, and blank-line-run capping.
  • cascade.go, style.go, color.go, strip.go, formcontrol.go, counter.go, csstext.go — CSS resolution on top of cssengine (shorthand expansion and declaration parsing itself live in internal/cssengine/css.go, not here), style synthesis, color parsing, hidden-inline stripping, form control rendering, list/heading counters (roman numerals included), and CSS-level text semantics. See "Not in this package" below for where column/width math went.
  • attrs.go — small attribute-lookup helpers shared across the above, plus the reserved data-htmlterm-* marker attribute names (focus, select-open, select-highlight, selection start/end) that document sets and this package reads.
  • defaultstylesheet.go — DefaultStylesheet, the user-agent sheet, parsed once per Engine by New and re-exported as htmlterm.DefaultStylesheet. Its header comment covers what belongs in a rule and what is engine behavior instead.

Key invariants

These are load-bearing enough that changing them without updating every call site will produce subtly wrong output — see docs/ARCHITECTURE.md for the full list, but the ones specific to this package:

  • Position tracking is incremental, not a final pass. Every box-producing call returns its own local map[*html.Node]Rect, relative to its own origin; a parent shifts and merges a child's map (mergePositions) as it embeds that child, one level at a time, until the walk reaches the document root.
  • Rect is the CSS border box, not the margin box. Vertical margin is injected as blank lines around a box (already excluded from Rect); horizontal margin is baked into a box's own lines like padding and is not currently subtracted back out.
  • A trackable element loses its position if flattened to a string before its wrapping box's wordWrapTokens/root dispatch call runs. Plain inline dispatch cases must splice a child's own []wrapToken rather than rendering it to a string first; <a> and display:inline-block children are the accepted exceptions.
  • Column alignment: pad plain text to fixed width first, then apply color — color escape codes break fmt.Printf width specifiers.
  • Block border box model: rendering order is margin-left | border-left | padding-left | content | padding-right | border-right | margin-right; width: 100% subtracts margins and border characters so the total visual line equals exactly the specified width.

Testing

  • box_internal_test.go, wrap_token_internal_test.go, blank_cap_internal_test.go — unit tests for box.go's primitives, wraptoken.go's token wrapping/position tracking, and blankcap.go's blank-run capping/rowRemap.
  • helpers_internal_test.go, color_test.go, list_test.go, wrap_ansi_carry_internal_test.go — counters, roman numerals, column sizing, color parsing, list rendering, and ANSI-carry-aware splitting.
  • scrollbar_internal_test.go, strip_internal_test.go — block.go's appendScrollbarColumn and strip.go's hidden-inline stripping.
  • Higher-level layout behavior (full HTML documents through to rendered output) is covered by the root package's black-box tests instead (htmlterm_test.go, layout_test.go, table_test.go, text_test.go, formcontrol_test.go), since that's the level at which "does this render correctly" is meaningful.

See also

  • internal/cssengine — CSS parsing, selector matching, and cascade resolution this package builds on.
  • RENDERING.md — design history and rationale for the layout engine.
  • CSS.md — the full supported CSS surface as seen by users of htmlterm.

Not in this package

Column/width measurement lives in internal/textcell — a leaf package this one, tui, and document all depend on, so all three agree exactly on how wide a string is. csstext.go — what's left of the old textutil.go — keeps only CSS-level text semantics (white-space normalization, text-transform, tab expansion, terminal-escape sanitization): things that need to know what the text means, rather than how much room it takes.

Documentation

Overview

Package render owns htmlterm's HTML/CSS layout engine and render-state production. It is internal so the public root package can stay a small generic renderer facade.

Index

Constants

View Source
const DefaultStylesheet = `` /* 5832-byte string literal not displayed */

DefaultStylesheet is the built-in default stylesheet, of lowest priority, with Options.CSS and Options.Stylesheets layered above it. Re-exported by the root package as htmlterm.DefaultStylesheet, so callers can inspect it or diff their own overrides against it. Parsed once per Engine by New.

This is the user-agent sheet, the terminal analogue of a browser's own default styles. It carries only what a rule can express: display types, font weight and style, margins and padding, borders, generated content. Anything needing engine behavior instead of a declaration is not here and cannot be moved here by adding a rule.

Two categories in particular are handled in Go, not CSS:

  • Head-level elements never reach the CSS cascade at all. render.go's isSkippedContentElement drops <style>, <script>, <meta>, <link>, and <template> wherever they appear, and its root dispatch skips <head> outright, which covers <title>. A `display: none` rule for any of them would be dead weight, so there isn't one.
  • Form controls and widgets get their structure from their renderers (formcontrol.go, select_popup.go, table*.go). The rules below set only the parts a stylesheet owns, such as a control's outer display type or a button's bracket affixes.
View Source
const ScrollbarGutterHeight = 1

ScrollbarGutterHeight is ScrollbarGutterWidth's horizontal-scrollbar counterpart: the default row count reserved for a horizontal scrollbar gutter when overflow-x:scroll is set, and no ::scrollbar-x height declaration overrides it, on an element with an explicit width and no height of its own. See the "scroll"/"auto" case in renderBlockContentBox's overflow-x handling, and docs/SCROLLING.md.

View Source
const ScrollbarGutterWidth = 1

ScrollbarGutterWidth is the default column width reserved for the scrollbar gutter when overflow-y:scroll is set and no ::scrollbar width declaration overrides it. See docs/SCROLLING.md's "Scrollbar gutter and indicator" and docs/SCROLLBARS.md. It is exported, via htmlterm.ScrollbarGutterWidth, so callers who pre-render content outside a scrollable Document or Renderer pass — to cache an expensive layout, say, then splice it into a live scrollable pane via Document.SetPreRendered — can reserve the same column up front. Otherwise the pre-rendered content is wrapped 1 column wider than the live pane's actual content width once the gutter is reserved there, desyncing the two and producing exactly the scrollbar-off-by-one symptom this constant exists to prevent. A caller that also sets a custom ::scrollbar { width } on the live pane must account for that override itself; this constant only reflects the built-in default.

Variables

This section is empty.

Functions

This section is empty.

Types

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine renders already-parsed HTML trees or HTML strings to terminal output.

func New

func New(opts Options) (*Engine, error)

New parses opts.CSS/opts.Stylesheets and returns a reusable render engine.

Cascade order, lowest priority first: the built-in default stylesheet (DefaultStylesheet), opts.CSS, then each of opts.Stylesheets in order, mirroring how a page's own stylesheet is followed by however many <link> sheets it loads. Document <style> elements and inline style= attributes are layered on top of all of these at render time (see DocumentRules and cssengine.Cascade). DefaultStylesheet's own parsed rules are also kept separately, as uaRules, purely to give the "revert" cascade-wide keyword a user-agent-only rule set to fall back to (see cssengine.Cascade.UARules).

func (*Engine) DocumentRules

func (e *Engine) DocumentRules(doc *html.Node) []cssengine.Rule

DocumentRules returns the final rule set for doc: engine rules plus any active <style> rules, unless document CSS is ignored.

func (*Engine) Render

func (e *Engine) Render(htmlStr string) (string, error)

func (*Engine) RenderHTML

func (e *Engine) RenderHTML(htmlStr string) (Result, error)

RenderHTML parses htmlStr and renders it. It returns layout metadata for callers that need it; the public root facade discards everything except Output.

func (*Engine) RenderNode

func (e *Engine) RenderNode(doc *html.Node, req Request) Result

RenderNode renders an already-parsed document using request-specific state.

type Options

type Options struct {
	CSS                 string
	Stylesheets         []string
	Width               int
	Height              int
	IgnoreDocumentCSS   bool
	Profile             colorprofile.Profile
	NoOSC8Links         bool
	MaxBlankLines       int
	StripHiddenInline   bool
	FocusAttr           string
	SelectOpenAttr      string
	SelectHighlightAttr string
	SelectionStartAttr  string
	SelectionEndAttr    string
	DialogModalAttr     string
	DatalistOpenAttr    string
	DatalistMatchAttr   string
}

Options configures the internal render engine. The public htmlterm.Options type is converted to this type by the root package facade.

type Rect

type Rect struct {
	Row, Col      int
	Width, Height int
}

Rect is a box's position and size relative to whatever coordinate space it was recorded in. See docs/RENDERING.md's "Position tracking" section. It approximates the CSS border box (content+padding+border): horizontal margin, when an element sets margin-left/right explicitly, is baked into its box the same way padding is (see renderBlockContentBox) and is not currently subtracted back out, so Rect may include a few extra columns of margin overlap for such elements. Vertical margin is unaffected, since it is injected as separate blank lines around a box, never baked into it. This is an accepted simplification: the primary motivating use is hit-testing form controls (see docs/INTERACTIVE.md), and those essentially never set margin on input/button/textarea.

type Request

type Request struct {
	Width          int
	Height         int
	Rules          []cssengine.Rule
	ScrollOffsets  map[*html.Node]int
	ScrollOffsetsX map[*html.Node]int
}

Request supplies per-frame state for RenderNode.

type Result

type Result struct {
	Output          string
	Positions       map[*html.Node]Rect
	ScrollOffsets   map[*html.Node]int
	ScrollViewport  map[*html.Node]Viewport
	ScrollOffsetsX  map[*html.Node]int
	ScrollViewportX map[*html.Node]ViewportX
	ContentOffsets  map[*html.Node]int
	ContentOffsetsX map[*html.Node]int
}

Result is the rendered output plus layout metadata needed by interactive document hosts.

type Viewport

type Viewport struct {
	Height      int
	TopOffset   int
	GutterCol   int
	GutterWidth int
	CapStart    bool
	CapEnd      bool
}

Viewport records a scroll container's visible content-area geometry. GutterCol, GutterWidth, CapStart, and CapEnd are only meaningful when GutterWidth > 0, meaning an overflow-y:scroll gutter really was reserved this frame (see block.go's hasScrollbarGutter). document.go's tryScrollCapClick uses them, combined with the element's own Rect, whose Col and Row this Viewport's offsets are relative to, same as TopOffset already is, to hit-test a click against the cap buttons' cells.

CapStart and CapEnd reflect whether that cap was drawn this frame, not just whether a ::scrollbar-cap-start or ::scrollbar-cap-end rule set content: a cap dropped for lack of room (see appendScrollbarColumn) must not be clickable.

type ViewportX

type ViewportX struct {
	Width        int
	LeftOffset   int
	GutterRow    int
	GutterHeight int
	CapStart     bool
	CapEnd       bool
}

ViewportX is Viewport's horizontal counterpart, recorded for an overflow-x:scroll|auto element with an explicit width. See block.go's hasScrollbarGutterX and docs/SCROLLING.md's horizontal-scrolling addendum.

Width and LeftOffset are the transpose of Height and TopOffset: LeftOffset is the column shift from the element's own Rect.Col to where its content starts, the same relationship colShift already has to Rect.Col elsewhere. GutterRow, GutterHeight, CapStart, and CapEnd are the transpose of GutterCol, GutterWidth, CapStart, and CapEnd. GutterRow is the row shift from Rect.Row to the gutter row, only meaningful when GutterHeight > 0, meaning a visible ::scrollbar-track-x or ::scrollbar-thumb-x row was drawn this frame. CapStart and CapEnd here mean the *left* and *right* cap, ::scrollbar-cap-start-x and ::scrollbar-cap-end-x, not top and bottom.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL