Skip to content

css — an MDN-typed CSS engine for nimony / Nim 3.0

▶️ Try aoughwl/css live in the Playground — clones the repo into the in-browser IDE, no install.

css parses a whole stylesheet and then validates it: every property value against its official MDN value-definition syntax, every math and functional value against its own signature, and every selector against Selectors-4 (with specificity and a source-order cascade resolver on top). It is pure logic — standard library only, no dependencies, no aoughwl substrate — so import css compiles and runs on plain nimony / Nim 3.0. It is the successor to the Nim-2 thing-king/css.

Status — Value, function-argument, selector, cascade and full-stylesheet parsing are complete and MDN-driven (Bootstrap 5.3.3: 5 542 declarations, zero false positives). Gaps: at-rule bodies are parsed but their preludes (@media/@supports/@keyframes) are not yet grammar-checked, selector functional arguments (:nth-child(An+B)) are only balance-checked, and computed-style inheritance / @import resolution are not done.

Quickstart

nim
import css

# --- single-value validation, with a readable error -----------------------
validateValue("margin", "0 auto").valid                 # true
validateValue("width",  "clamp(1rem, 2vw, 3rem)").valid # true
validateValue("color",  "rgb(255 0 0 / 50%)").valid     # true
validateValue("width",  "clamp(1rem, 2vw)").error       # "clamp() expects 3 arguments, got 2"
validateValue("color",  "10px").error                   # "at token 1: expected <color>, got '10px'"

# --- selectors, specificity, cascade --------------------------------------
validateSelector("ul > li:nth-child(2n+1)").valid       # true
echo $specificity("a.btn#go")                           # (1,1,1)

# --- parse and validate a whole stylesheet --------------------------------
let sheet = parseStylesheet(readFile("bootstrap.css"))
for rule in sheet.rules:
  for d in rule.decls:
    let r = validateValue(d.prop, d.value)
    if not r.valid:
      echo rule.prelude, " { ", d.prop, ": ", d.value, " }  -- ", r.error

# --- tune the tier: values-only (fast) vs. full (default) -----------------
setLevel(lvValues)   # whole-value grammar match only
setLevel(lvFull)     # + recursive math + strict function-argument grammars

API

import css re-exports the four pure modules below (validator, selectors, cascade, data_load). The value-definition-syntax parser (vds), math validator (math), value lexer (value_lex), stylesheet parser (parse) and the style X: DSL (style) are imported directly from their submodules.

Value validation — css/validator

symbolsignaturewhat it does
validateValueproc validateValue(prop, value: string): tuple[valid: bool, error: string]Validate value against property prop's MDN grammar; returns a readable error on failure. Accepts global keywords (inherit/initial/unset/revert), vendor-prefixed properties (uncheckable → accepted), and the color-adjust alias.
valueMatchesproc valueMatches(prop, value: string): boolBoolean form of the above — the whole-value grammar match, no error bookkeeping.
setLevelproc setLevel(l: Level)Choose the validation tier (see Level). Global, defaults to lvFull.
levelproc level(): LevelThe current tier.
Levelenum lvValues, lvFulllvValues = whole-value grammar only (the tier peers stop at); lvFull = also recursive math + strict function-argument grammars.

Selector validation — css/selectors

symbolsignaturewhat it does
validateSelectorproc validateSelector(sel: string): tuple[valid: bool, error: string]Validate a selector or comma-separated selector list against Selectors-4: type / universal / class / id / attribute / pseudo simple selectors joined by the descendant, child, next-/subsequent-sibling and column combinators. Pseudo names are checked against the MDN data.
selectorValidproc selectorValid(sel: string): boolBoolean form of validateSelector.

Specificity & cascade — css/cascade

symbolsignaturewhat it does
Specificityobject with a, b, c: intThe (a,b,c) triple — id count; class/attr/pseudo-class count; type/pseudo-element count.
specificityproc specificity(sel: string): SpecificityHighest specificity across a comma-separated selector list. A functional pseudo's argument is skipped, not recursed into (:not(.a.b) counts as one).
`<`proc <(x, y: Specificity): boolCascade ordering — compare a, then b, then c.
`==`proc ==(x, y: Specificity): boolComponent-wise equality.
`$`proc $(s: Specificity): stringRender as "(a,b,c)".
Declobject with selector, property, value: stringOne input declaration for the cascade.
Winnerobject with property, value: string; spec: Specificity; order: intThe resolved winner for a property.
cascadeproc cascade(decls: openArray[Decl]): seq[Winner]Resolve declarations to the winning value per property: higher specificity wins; on a tie, later source order wins.

Stylesheet parsing — css/parse

symbolsignaturewhat it does
parseStylesheetproc parseStylesheet(src: string): StylesheetParse a whole stylesheet into a tree of rules + declarations. Survives real CSS: comments, strings, url(data:…;base64,…), [attr="{"], !important, custom properties, and nested at-rules. Non-raising throughout.
Stylesheetobject with rules: seq[Rule]The parsed sheet.
Ruleobject with prelude: string; isAtRule: bool; atKeyword: string; decls: seq[Declaration]; children: seq[Rule]A style rule or at-rule: prelude is the selector list or at-rule head, atKeyword is e.g. "media"/"font-face" (empty for a style rule), children are nested rules.
Declarationobject with prop, value: string; important: boolOne prop: value pair; important is set when !important was present.

Value-definition syntax — css/vds

The MDN grammar parser. Turns a value-definition string such as <length-percentage>{1,4} [ / <length-percentage>{1,4} ]? into a VNode tree that the matcher walks. Combinator precedence, loosest to tightest: | < || < && < juxtaposition.

symbolsignaturewhat it does
parseSyntaxproc parseSyntax(src: string): VNodeParse a value-definition-syntax string into a grammar AST.
renderproc render(n: VNode): stringRender a VNode grammar tree back to its value-definition-syntax string.
VNoderef object (variant over NodeKind)Grammar AST node: carries mult, lo/hi, and a kind-specific payload (text, name, fname+arg, or comb+kids).
NodeKindenum nkKeyword, nkLiteral, nkType, nkProp, nkFunc, nkListNode kinds — literal keyword, literal token (/ ,), &lt;type&gt;, &lt;'prop'&gt; reference, name( arg ), or a combinator list.
Combenum cbSeq, cbOr, cbAny, cbAllThe four combinators: juxtaposition, ``
Multenum mkOne, mkOpt, mkStar, mkPlus, mkHash, mkRange, mkHashRangePostfix multipliers: none, ?, *, +, #, {m,n}, #{m,n}.
HugeNconst HugeN = 1000000Stand-in for ∞ in {m,} and unbounded ranges.

Math-function validation — css/math

Recursive-descent validator over the CSS Values-4 calc grammar, mirroring the self-nesting &lt;calc-sum&gt;&lt;calc-product&gt;&lt;calc-value&gt; structure exactly; it accepts every well-formed nesting and pinpoints the first malformed token.

symbolsignaturewhat it does
isMathFuncproc isMathFunc(name: string): boolWhether name is a known CSS math function (calc, min, max, clamp, abs, sign, sqrt, exp, hypot, mod, rem, round, log, pow, atan2, trig, …).
validateMathFuncproc validateMathFunc(name, args: string): tuple[valid: bool, error: string]Validate one math call's argument string against its exact arity and the recursive calc grammar (constants e/pi/infinity/nan, round() rounding strategy, nested math).
validateFunctionsInproc validateFunctionsIn(value: string): tuple[valid: bool, error: string]Validate every function embedded anywhere in a value string.

Value lexer — css/value_lex

symbolsignaturewhat it does
lexValueproc lexValue(s: string): seq[VTok]Tokenize a concrete CSS value; nested functions collapse to one opaque vtFunc token whose raw argument string is kept in args.
VTokobject with kind: VTokKind; text, num, args: stringA value token.
VTokKindenum vtIdent, vtNumber, vtDimension, vtPercent, vtString, vtHash, vtFunc, vtComma, vtSlash, vtDelimValue token kinds.

MDN data tables — css/data_load

Lookup tables parsed once at import time from the baked blobs in css/data.nim. The exported Table[string, string] values cssProperties, cssSyntaxes, cssTypes, cssUnits, cssAtRules, cssPseudoClasses, cssPseudoElements are also available directly.

symbolsignaturewhat it does
isPropertyproc isProperty(name: string): boolIs name a known CSS property?
propertySyntaxproc propertySyntax(name: string): stringThe property's MDN value-definition syntax ("" if unknown).
isSyntaxproc isSyntax(name: string): boolIs name a named syntax (e.g. length-percentage)?
syntaxOfproc syntaxOf(name: string): stringThe named syntax's definition ("" if unknown).
isTypeproc isType(name: string): boolIs name a basic data type (e.g. color, length)?
isUnitproc isUnit(name: string): boolIs name a known unit?
unitDimensionproc unitDimension(name: string): stringThe unit's dimension bucket (length/angle/time/…).
isAtRuleproc isAtRule(name: string): boolIs name a known at-rule (no @)?
isPseudoClass / isPseudoElementproc (name: string): boolKnown pseudo-class / pseudo-element name (no :/::).
isFunctionalPseudoClass / isFunctionalPseudoElementproc (name: string): boolWhether that pseudo takes a (…) argument.

The style X: DSL — css/style (needs plugin)

A component-style DSL that lowers each declaration to a validated, content-addressed rule. This is the substrate-free public build: it keeps the exact surface of the aoughwl version but stores styles in a plain table. It is the one part of the package that pulls a dependency — the plugin nimony plugin-authoring runtime — and only for the block macro. Plain import css validation needs nothing but the standard library.

symbolsignaturewhat it does
styletemplate style(name: string, body: untyped)The style "name": prop: value block. A compiler plugin (deps/style_plugin) lowers each line to a styleOne call.
styleOneproc styleOne(component, prop, value: string)Register one declaration: validate against the MDN grammar, then store deduped by content under a stable class name.
classOfproc classOf(prop, value: string): stringThe stable, content-derived class name (c + FNV-1a hex) for a declaration.
renderStylesheetproc renderStylesheet(): stringEmit every registered declaration as a single-declaration CSS rule.
styleErrorsproc styleErrors(): seq[string]The declarations that failed MDN validation.
whyStyleproc whyStyle(component, prop, value: string): stringExplain a component's style — which class it maps to and whether it is valid per the MDN grammar.

Design notes

  • Data-driven. Everything hangs off the MDN tables in css/data/*.json (properties, syntaxes, types, units, at-rules, pseudo-classes, pseudo-elements). Track a spec change by dropping in fresh JSON and re-running the generator; trim the data to constrain which CSS a project is allowed to use.
  • std/json-free at runtime. The generator css/tools/gen_data (run under regular Nim) flattens the JSON into compact tab/newline blobs in css/data.nim; the shipped library parses them with a tiny hand-rolled splitter, so no shipped module depends on std/json — keeping nimony compiles cheap.
  • Non-raising throughout. nimony house style: the parser, selector validator and blob loader char-walk and build substrings by hand rather than use raising string slices; validation reports status via tuple[valid, error] rather than exceptions.
  • Pay for what you check. setLevel trades coverage for speed (lvValues vs. lvFull); function/math checking is skipped for any value containing no functions, and error-message bookkeeping is skipped on the success path.
  • Real-world tolerance. var()/env() substitution, vendor prefixes, url(data:…), comments and !important are handled the way browsers do; unknown vendor-prefixed properties are accepted rather than flagged.

Requirements

  • nimony toolchain / Nim 3.0. Pure validation (import css) needs only the standard library — no C FFI, no other repos.
  • plugin — required only for the css/style DSL block macro (declared in css.nimble). Everything else is dependency-free.
  • Generator only: css/tools/gen_data runs under regular Nim (std/json, std/os, std/strutils, …) to rebuild css/data.nim from css/data/*.json; it is a build tool, not a runtime dependency.

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.