styleengine

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

styleengine

Typed, composable, fast Go-native CSS construction and emission.

styleengine replaces ad-hoc text/template or string concatenation with a structured intermediate representation (IR) that validates inputs at construction time, deduplicates rules, enforces a byte-size safety ceiling, and emits either pretty-printed or minified CSS through a single Render call.

It is a great building block for design system compilers, theme engines, plugin and customization systems, and server-side rendering pipelines.


Who calls it

  • Design-system token compilers — emit :root custom-property sheets from design-token sources.
  • Theme / experience composers — assemble layered overrides (brand, customer, user) into a single merged Sheet.
  • Untrusted CSS validators — call [Sanitize] on user-, plugin-, or customer-supplied CSS fragments before accepting them.
  • Server renderers and email generators — build and emit CSS safely without template string hacks.
  • Custom tooling and CLIs that need reproducible, validated CSS output.

Hello world

package main

import (
	"fmt"
	"github.com/septagon-oss/styleengine"
)

func main() {
	sheet, err := styleengine.New().
		Var("font-display", "swap").
		Var("motion-duration-marquee", "8s").
		Rule(":root").
			Decl("font-display", styleengine.VarRef("font-display", "swap")).
			Done().
		Rule(".marquee").
			Decl("animation-duration", styleengine.VarRef("motion-duration-marquee", "6s")).
			Decl("overflow", styleengine.Literal("hidden")).
			Done().
		Build().
		Render(styleengine.RenderOptions{Pretty: true})
	if err != nil {
		panic(err)
	}
	fmt.Println(sheet)
}

Var registers a --font-display custom property in :root. VarRef emits var(--font-display, swap). Both enforce the [a-z][a-z0-9_-]* naming rule at call time, so mis-spellings are caught immediately rather than silently emitted.


Public API surface

Symbol Kind Role
Builder struct Fluent entry point; wraps a Sheet
Sheet struct Root IR — ordered, deduped rule + at-rule collection
Rule struct (Selector, []Declaration) pair
Selector struct Normalized CSS selector
Declaration struct property: value [!important]
Value interface Typed CSS RHS; self-renders via .CSS()
Literal func Wraps a trusted string as a Value
VarRef func Emits var(--name[, fallback]) as a Value
Decl func Constructor for Declaration
RuleBuilder struct Accumulates declarations; .Done() commits to parent
KeyframesBuilder struct Accumulates @keyframes stops
FontFaceDecl struct Structured @font-face descriptor
RenderOptions struct Controls pretty/minify/MaxBytes for Render. Pretty and Minify are mutually exclusive; negative MaxBytes disables the ceiling.

RenderOptions

type RenderOptions struct {
	Pretty   bool  // emit indented multi-line output
	Minify   bool  // run tdewolff/minify (mutually exclusive with Pretty; both returns error)
	MaxBytes int64 // 0 → DefaultMaxBytes (1 MiB)
}

Render enforces a byte ceiling (negative MaxBytes disables). When MaxBytes is zero it falls back to DefaultMaxBytes (1 MiB = 1 << 20). If the rendered output exceeds the limit Render returns an error rather than a truncated string, so callers cannot silently serve corrupt CSS.


Variable safety

Var and VarRef both enforce [a-z][a-z0-9_-]* on the custom-property name and panic immediately if the constraint is violated. The leading -- is added automatically — callers never write it.

UndefinedVars() []string scans the sheet for var(--x) references that have no matching Var registration and returns the sorted list of undefined names. Use this in compiler pipelines to surface missing token definitions before emitting CSS to a browser.


Sanitize

Before accepting CSS text from untrusted sources (user input, customer overrides, third-party plugins, etc.), pass it through Sanitize:

clean, err := styleengine.Sanitize(userInput)
if err != nil {
	// reject the request
}

Sanitize trims whitespace and rejects (case-insensitively) any fragment containing dangerous patterns that could break out of the expected stylesheet context or introduce security issues.


Parse roundtrip

Parse(css string) (*Sheet, error) ingests a CSS string via github.com/tdewolff/parse/v2/css and reconstructs a Sheet IR.

For the rule + custom-property subset supported in v1, the roundtrip is idempotent. This property is verified in the test suite and is useful for overlay / customization pipelines.


At-rules supported

At-rule Status
@media Supported
@keyframes Supported
@font-face Supported
@layer Supported
@supports Supported
@scope Deferred
@container Deferred

Nested at-rules are composed via closures that receive a child Sheet.


Performance

Both pretty and minified render are well under the 500 µs target for a full-page token sheet. See benchmarks in the package for current numbers on your hardware.


Dependencies

Module Role
github.com/tdewolff/minify/v2 CSS minification (direct)
github.com/tdewolff/parse/v2 CSS tokenizer used by Parse (direct)

Both are pure Go with no cgo requirements.


Status

The core library (IR, builder, render, parse, sanitize, variables, at-rules) is stable and suitable for general use.

The package has comprehensive tests, fuzzing, and benchmarks. It has minimal dependencies.

See the godoc for the full API and the Example functions (including roundtrips and at-rule usage) that render in documentation and pass go test -run Example.

Documentation

Overview

Package styleengine provides a typed, safe, and fast Go-native CSS construction and emission engine.

It is useful for design systems, theme compilers, server-rendered UIs, email templates, and any situation where you need to build or sanitize CSS programmatically without string concatenation or ad-hoc templates.

A Sheet is the root IR: an ordered, deduped, mergeable collection of [Rule]s and [AtRule]s. The fluent Builder produces a Sheet from typed inputs; [Render] emits CSS (pretty or minified); Parse reverses the process (e.g. for validating or normalizing untrusted CSS fragments).

styleengine is intentionally narrow: no CSS-in-JS, no preprocessor syntax, no third-party language extensions. Modern native CSS only.

Basic usage

Create a sheet with variables and rules, then render:

sheet, err := styleengine.New().
    Var("color-primary", "#0a84ff").
    Rule(":root").
        Decl("color", styleengine.VarRef("color-primary", "black")).
        Done().
    Build().
    Render(styleengine.RenderOptions{Pretty: true})

Sanitizing untrusted input

Always sanitize CSS that comes from users, plugins, or customer overrides:

clean, err := styleengine.Sanitize(userSuppliedCSS)

Parsing existing CSS

Round-trip existing fragments for validation or normalization:

parsed, err := styleengine.Parse(someCSS)

See the [examples] directory and the README for more complete programs.

Example

Example shows basic construction of a sheet with variables and rules, then pretty render.

package main

import (
	"fmt"

	"github.com/septagon-oss/styleengine"
)

func main() {
	sheet, err := styleengine.New().
		Var("color-primary", "#0a84ff").
		Rule(":root").
		Decl("color", styleengine.VarRef("color-primary", "black")).
		Done().
		Build().
		Render(styleengine.RenderOptions{Pretty: true})
	if err != nil {
		panic(err)
	}
	fmt.Println(sheet)
}
Output:
:root {
  --color-primary: #0a84ff;
  color: var(--color-primary, black);
}
Example (AtruleMedia)

Example_atruleMedia demonstrates a nested @media at-rule (black-box coverage for at-rules claimed in README).

package main

import (
	"fmt"

	"github.com/septagon-oss/styleengine"
)

func main() {
	sheet, _ := styleengine.New().
		Media("(prefers-color-scheme: dark)", func(b *styleengine.Builder) {
			b.Rule("body").Decl("background", styleengine.Literal("#000")).Done()
		}).
		Build().
		Render(styleengine.RenderOptions{Pretty: true})
	fmt.Println(sheet)
}
Output:
@media (prefers-color-scheme: dark) {
  body {
    background: #000;
  }
}
Example (ParseRoundtrip)

Example_parseRoundtrip shows a simple parse + render cycle.

package main

import (
	"fmt"

	"github.com/septagon-oss/styleengine"
)

func main() {
	parsed, err := styleengine.Parse(`:root { --foo: red; } .bar { color: var(--foo); }`)
	if err != nil {
		panic(err)
	}
	out, _ := parsed.Render(styleengine.RenderOptions{Pretty: false})
	fmt.Println("roundtrip ok:", len(out) > 0)
}
Output:
roundtrip ok: true
Example (Sanitize)

Example_sanitize demonstrates rejecting dangerous untrusted CSS.

package main

import (
	"fmt"

	"github.com/septagon-oss/styleengine"
)

func main() {
	_, err := styleengine.Sanitize(`body { background: url("javascript:alert(1)"); }`)
	fmt.Println("sanitized err:", err != nil)
}
Output:
sanitized err: true

Index

Examples

Constants

View Source
const DefaultMaxBytes int64 = 1 << 20

DefaultMaxBytes is the default Render byte ceiling (1 MiB).

View Source
const (
	// DiagUndefinedVar — a var(--x) reference has no matching Var
	// registration in the Sheet. Subject is the missing name.
	DiagUndefinedVar = "SE001"
)

Stable diagnostic codes. Adding a new code is additive; reusing or reassigning an existing one breaks downstream tooling.

Variables

This section is empty.

Functions

func Sanitize

func Sanitize(css string) (string, error)

Sanitize trims and validates a CSS declaration-block fragment. It rejects tokens that could escape the containing rule, import remote resources, or introduce script execution.

Call Sanitize on any CSS that comes from outside your trusted source tree (user input, customer theme overrides, plugin-provided fragments, etc.) before passing it to Literal or embedding it.

Trusted, in-tree CSS (your design system tokens, official themes) should NOT go through Sanitize — it intentionally rejects url(), @font-face, and other constructs that are valid and useful inside first-party stylesheets.

CSS escape sequences (e.g. `\75 rl(`) are rejected outright. Untrusted input has no legitimate need for them; allowing them would create bypasses.

Types

type AtRule

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

AtRule is opaque; consumers construct via Sheet.Media/Keyframes/Layer/Supports.

func (AtRule) CSSPretty

func (a AtRule) CSSPretty() string

CSSPretty emits the at-rule in pretty form.

type Builder

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

Builder is the fluent surface over Sheet.

func New

func New() *Builder

New returns a fresh Builder backed by an empty Sheet.

func (*Builder) Build

func (b *Builder) Build() *Sheet

Build returns the underlying Sheet. Subsequent builder calls continue to mutate the same Sheet.

func (*Builder) FontFace

func (b *Builder) FontFace(ff FontFaceDecl) *Builder

FontFace appends a font-face declaration.

func (*Builder) Keyframes

func (b *Builder) Keyframes(name string, fn func(*KeyframesBuilder)) *Builder

Keyframes wraps Sheet.Keyframes.

func (*Builder) Layer

func (b *Builder) Layer(name string, fn func(*Builder)) *Builder

Layer wraps Sheet.Layer.

func (*Builder) Media

func (b *Builder) Media(query string, fn func(*Builder)) *Builder

Media wraps Sheet.Media.

func (*Builder) Rule

func (b *Builder) Rule(selector string) *RuleBuilder

Rule starts a RuleBuilder for the given selector.

func (*Builder) Supports

func (b *Builder) Supports(condition string, fn func(*Builder)) *Builder

Supports wraps Sheet.Supports.

func (*Builder) Var

func (b *Builder) Var(name, value string) *Builder

Var delegates to Sheet.Var.

type Declaration

type Declaration struct {
	Property string
	Value    Value
	// contains filtered or unexported fields
}

Declaration is property: value [!important].

func Decl

func Decl(property string, value Value) Declaration

Decl is a constructor for Declaration.

func (Declaration) CSS

func (d Declaration) CSS() string

func (Declaration) Important

func (d Declaration) Important() Declaration

Important returns a copy of the declaration with !important set.

type Diagnostic

type Diagnostic struct {
	Code    string         // stable code (e.g., "SE001")
	Kind    DiagnosticKind // severity
	Message string         // human-readable description
	Subject string         // optional reference: selector, var name, etc.
}

Diagnostic is a stable, typed finding produced by Sheet.Diagnostics. Downstream linters, compilers, and validation pipelines can consume these directly instead of regex-scanning rendered CSS text.

type DiagnosticKind

type DiagnosticKind int

DiagnosticKind classifies the severity of a Diagnostic.

const (
	// DiagnosticError indicates a finding that must be fixed; emitting the
	// CSS as-is would produce broken or unsafe output.
	DiagnosticError DiagnosticKind = iota
	// DiagnosticWarning indicates a likely bug or stylistic issue that
	// should be reviewed but does not block emission.
	DiagnosticWarning
	// DiagnosticHint indicates an opportunity for improvement; safe to ignore.
	DiagnosticHint
)

func (DiagnosticKind) String

func (k DiagnosticKind) String() string

type FontFace

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

FontFace is the runtime IR for @font-face.

func (FontFace) CSSPretty

func (f FontFace) CSSPretty() string

CSSPretty emits the @font-face block in pretty form.

type FontFaceDecl

type FontFaceDecl struct {
	Family       string
	Src          string
	Weight       string
	Style        string
	Display      string
	UnicodeRange string
}

FontFaceDecl is the structured form of an @font-face block.

type KeyframesBuilder

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

KeyframesBuilder accumulates animation stops.

func (*KeyframesBuilder) At

func (k *KeyframesBuilder) At(offset string, decls ...Declaration) *KeyframesBuilder

At adds a keyframe stop ("0%", "50%", "from", "to", etc.).

type RenderOptions

type RenderOptions struct {
	Pretty   bool
	Minify   bool
	MaxBytes int64
}

RenderOptions configures Sheet.Render.

Pretty and Minify are mutually exclusive: setting both returns an error. MaxBytes caps the final output size; 0 means DefaultMaxBytes.

type Rule

type Rule struct {
	Selector Selector
	Decls    []Declaration
}

Rule is a (selector, []declaration) pair.

func (Rule) CSSPretty

func (r Rule) CSSPretty() string

CSSPretty emits an indented multi-line form. Custom-property declarations (--foo) are emitted first in alphabetical order so that diffs are stable; other declarations preserve insertion order.

type RuleBuilder

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

RuleBuilder accumulates declarations for one selector.

func (*RuleBuilder) Decl

func (r *RuleBuilder) Decl(property string, value Value) *RuleBuilder

Decl adds a declaration.

func (*RuleBuilder) Done

func (r *RuleBuilder) Done() *Builder

Done commits the rule into the parent Builder's Sheet.

func (*RuleBuilder) ImportantDecl

func (r *RuleBuilder) ImportantDecl(property string, value Value) *RuleBuilder

ImportantDecl adds an !important declaration.

type Selector

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

Selector is a normalized CSS selector. Use NewSelector or MustSelector.

func MustSelector

func MustSelector(s string) Selector

MustSelector panics on error.

func NewSelector

func NewSelector(s string) (Selector, error)

NewSelector parses and normalizes a selector. Returns error if empty.

func (Selector) String

func (s Selector) String() string

type Sheet

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

Sheet is the root IR. It is an ordered, deduped, mergeable collection of Rules and AtRules.

Sheet is NOT safe for concurrent mutation. The design intent is single-writer construction: a Sheet is constructed in one goroutine, possibly merged with other partial Sheets, then rendered. If a future consumer needs concurrent mutation, wrap calls in a mutex at the caller — Sheet itself stays lock-free to keep render hot-paths fast.

func NewSheet

func NewSheet() *Sheet

NewSheet returns an empty Sheet.

func Parse

func Parse(src string) (*Sheet, error)

Parse converts a CSS string into a Sheet. Only top-level qualified rules (selector { ... }) and their declarations are recognized in v1. At-rules and other grammar constructs are skipped without error.

func (*Sheet) AddFontFace

func (s *Sheet) AddFontFace(ff FontFaceDecl) *Sheet

AddFontFace appends an @font-face block.

func (*Sheet) AddRule

func (s *Sheet) AddRule(r Rule)

AddRule adds (or merges) a Rule. If a rule with the same normalized selector already exists, the new declarations merge into it; duplicates within a (selector, property, !important) triple are resolved last-write-wins.

func (*Sheet) Diagnostics

func (s *Sheet) Diagnostics() []Diagnostic

Diagnostics returns the current set of typed findings for the Sheet. The result is sorted by (Code, Subject) for deterministic output.

v1 reports SE001 for undefined var refs. Future codes follow the "SE0xx" prefix; consumers should treat unknown codes as informational.

func (*Sheet) Keyframes

func (s *Sheet) Keyframes(name string, fn func(*KeyframesBuilder)) *Sheet

Keyframes adds a @keyframes block.

func (*Sheet) Layer

func (s *Sheet) Layer(name string, fn func(*Sheet)) *Sheet

Layer nests a @layer block.

func (*Sheet) Media

func (s *Sheet) Media(query string, fn func(*Sheet)) *Sheet

Media nests a @media block. The closure receives a child Sheet that may contain rules, vars, and nested at-rules.

func (*Sheet) Merge

func (s *Sheet) Merge(other *Sheet) *Sheet

Merge appends every rule, at-rule, and font-face from other into s, preserving Sheet's dedup invariants. The other Sheet is unchanged.

Merge deep-copies AtRule and keyframesBody contents so that subsequent mutation of the source sheet (or its captured inner builders) does not leak into the merged result. Composing several partial Sheets (e.g., token-derived + layered overrides) is safe even if the partial Sheets are reused after merging.

func (*Sheet) RegisteredVars

func (s *Sheet) RegisteredVars() []string

RegisteredVars returns the sorted list of custom-property names registered via Var (without the "--" prefix).

func (*Sheet) Render

func (s *Sheet) Render(opts RenderOptions) (string, error)

Render emits CSS per options. Returns an error if MaxBytes is exceeded or if both Pretty and Minify are set.

func (*Sheet) RenderPretty

func (s *Sheet) RenderPretty() string

RenderPretty emits the Sheet as indented CSS. AtRules and FontFaces render after top-level rules in insertion order.

func (*Sheet) Supports

func (s *Sheet) Supports(condition string, fn func(*Sheet)) *Sheet

Supports nests a @supports block.

func (*Sheet) UndefinedVars

func (s *Sheet) UndefinedVars() []string

UndefinedVars returns var(--x) references in the Sheet that have no matching Var registration. Output is sorted. The scan covers:

  • Top-level rules (excluding the :root rule itself)
  • Nested rules inside [@media], [@layer], [@supports] at-rules
  • Keyframe-stop declarations inside [@keyframes]

Both typed VarRef values (constructed by the Builder) and var() references that came in through Parse as opaque Literal text are detected.

func (*Sheet) Var

func (s *Sheet) Var(name, value string) *Sheet

Var registers a CSS custom property to be emitted in :root. Names must match [a-z][a-z0-9-]*. The leading "--" is automatic. Re-registering a name is last-write-wins.

type Value

type Value interface {
	CSS() string
	// contains filtered or unexported methods
}

Value is the typed RHS of a declaration. Values self-render to CSS.

func Literal

func Literal(s string) Value

Literal returns a Value that emits the given string verbatim. Use only with trusted input (token values, programmatically built strings). For untrusted input go through Sanitize.

func VarRef

func VarRef(name, fallback string) Value

VarRef returns a Value referencing a CSS custom property. The leading "--" is added automatically. Panics if name is not a valid identifier, or if fallback contains characters that could escape the var() expression — the unmatched grouping/structural characters `)`, `(`, `{`, `}`, `;`, or a comment delimiter `/*`/`*/`. Callers that need a nested var() fallback must compose values through the Builder/Value API rather than embed raw CSS through this helper.

Jump to

Keyboard shortcuts

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