ZStyle — API Reference
The .zstyle parser and its supporting utilities. This document is the
API reference for the parsing layer; for the grammar itself, see
ZStyle (guide).
- Headers:
StyleParser.hpp,StyleAttr.hpp - Namespace:
ZenitUI::ZMarkup(parser) andZenitUI(attribute parsers)
1. Overview
The .zstyle pipeline has three stages:
- Tokenize and parse — read the source, extract rules, keyframes,
and
:rootdeclarations into an intermediateStyleSheetAST. - Convert — walk the AST and produce
ThemeRule,Style, andKeyframeAnimationobjects. - Apply — append the results to a
Theme.
The public entry points wrap all three:
ZMarkup::loadStyleString(source, theme, fileName);
ZMarkup::loadStyleFile(path, theme);
Everything else is exposed for advanced use (custom parsing flows, unit tests, tooling).
2. Public entry points
loadStyleString
inline bool loadStyleString(std::string_view src,
Theme& theme = Theme::get(),
const std::string& fileName = "<string>");
Parses src and applies it to theme. Returns true on success.
Parse errors are logged (via Logger) but don't abort the whole
parse — the offending rule is skipped, and the rest continues.
ZMarkup::loadStyleString(R"(
Button { background: red; }
.btn-primary { background: blue; }
)");
loadStyleFile
inline bool loadStyleFile(const std::filesystem::path& path,
Theme& theme = Theme::get());
Reads the file at path and calls loadStyleString with the file's
content and name. Returns false if the file can't be opened.
if (!ZMarkup::loadStyleFile("assets/game.zstyle")) {
std::fprintf(stderr, "Failed to load game.zstyle\n");
}
applyStyleSheet
inline void applyStyleSheet(const StyleSheet& sheet,
const std::string& fileName,
Theme& theme);
Applies an already-parsed StyleSheet to a Theme. Used internally by
loadStyleString. Public for advanced flows where you parse once and
apply to multiple themes.
3. AST types
StyleSheet
struct StyleSheet {
struct Rule {
std::vector<SimpleSelector> chain;
std::string part; // non-empty for ::part
std::vector<Declaration> decls;
int line{0};
int col{0};
std::optional<MediaQuery> media;
};
std::vector<Rule> rules;
struct KfFrame {
std::string ts; // "0%", "from", "100%", etc.
std::vector<Declaration> decls;
int line{0};
int col{0};
};
struct KfRule {
std::string name;
std::vector<KfFrame> frames;
int line{0};
int col{0};
};
std::vector<KfRule> keyframes;
std::unordered_map<std::string, std::string> vars; // --name → value
std::vector<Declaration> rootDecls; // :root { ... }
};
The AST output of StyleParser::parse(). Three top-level sections:
rules— one entry per selector (a comma-separated selector list produces multiple rules). Each has a chain, an optional::partname, declarations, and an optional media query.keyframes— one entry per@keyframesblock. Frames carry a timestamp string ("0%","from") and declarations.rootDecls— the:root { ... }block's declarations.vars— custom properties extracted from:root(a subset ofrootDecls, kept for backward compatibility).
Declaration
struct Declaration {
std::string prop;
std::string value;
int line{0};
int col{0};
};
A single property: value; declaration. The line / col are the
source position, used for logging.
ParseLoc
struct ParseLoc {
std::string file;
int line{0};
int col{0};
};
A location used in log messages. Constructed from the parser's current position and the source file name.
4. StyleParser
The parser class.
class StyleParser {
public:
StyleParser(std::string_view src, std::string fileName);
StyleSheet parse();
};
Construction
StyleParser(src, fileName) strips comments from src, stores it, and
remembers the file name for diagnostics.
Note: the constructor stores a copy of the stripped source. The
original std::string_view doesn't need to outlive the parser, but the
parser itself holds the whole source in memory.
parse()
StyleSheet parse();
Parses the source and returns the AST. Idempotent — calling it twice on the same parser produces the same result (the parser's internal position is reset).
The parser handles:
:root { ... }blocks.@keyframes name { ... }blocks.@media (cond) { ... }blocks (containing rules).- Regular
selector { declarations }rules. - Comments:
//(line) and/* ... */(block). - Multiple selectors via comma.
Example
ZenitUI::ZMarkup::StyleParser parser(R"(
Button { background: red; }
Button:hover { background: blue; }
)", "test.zstyle");
auto sheet = parser.parse();
// sheet.rules.size() == 2
// sheet.rules[0].chain[0].name == "Button"
// sheet.rules[1].chain[0].requireHover == true
Line / column tracking
The parser maintains a line and col that advance as it consumes
characters. Each Declaration in the AST carries these, and error
messages use them via ParseLoc.
For a multi-line source, error messages point to the correct line:
[WARN ] [StyleParser] assets/game.zstyle(45:10): unknown property 'foo'. Ignored.
5. Attribute parsers (StyleAttr.hpp)
Low-level parsers for individual values. Used by applyStyleAttr, and
available for custom parsing flows.
parseValueToken
inline std::optional<Value> parseValueToken(std::string_view s);
Parses a value string into a Value. Handles:
auto→Value::autoSize().calc(...)→ evaluated viaCalcParser.10px/10/10%/10vw/10vh/10pw/10ph.- Bare numbers (treated as pixels).
Returns std::nullopt on parse failure.
auto v = parseValueToken("calc(100% - 20px)");
// v.value() resolves to 100% - 20px
parseColorToken
inline std::optional<Color> parseColorToken(std::string_view s);
Parses a color string. Supports:
- Named colors:
white,black,transparent,blank,red,maroon,green,blue,yellow,gray,darkgray,lightgray. - Hex:
#RGB,#RGBA,#RRGGBB,#RRGGBBAA.
Returns std::nullopt for unknown colors.
auto c = parseColorToken("#FF000080");
// c->r == 0xFF, c->g == 0x00, c->b == 0x00, c->a == 0x80
parseSpacingToken
inline std::optional<Spacing> parseSpacingToken(std::string_view s);
Parses 1, 2, 3, or 4 whitespace-separated values into a Spacing.
parseSpacingToken("10px"); // all sides
parseSpacingToken("10px 20px"); // vertical | horizontal
parseSpacingToken("1px 2px 3px"); // top | horizontal | bottom
parseSpacingToken("1px 2px 3px 4px"); // top | right | bottom | left
parseAlignToken
inline std::optional<Align> parseAlignToken(std::string_view s);
Parses one of: auto, start, center, end, stretch.
parseJustifyToken
inline std::optional<Justify> parseJustifyToken(std::string_view s);
Parses one of: start, center, end, space-between.
applyStyleAttr
inline bool applyStyleAttr(Style& st,
const std::string& key,
const std::string& val);
Applies a single key: value declaration to a Style. This is the
shared routine used by:
- The
.zstyleparser'sapplyStyleDeclaration. - ZMarkup's inline attribute application.
- The
var()resolution path.
Returns true if the key was recognized, false otherwise. Unknown
keys are not an error — the caller decides whether to log.
The handled keys are the kebab-case versions of the
BUBBLE_STYLE_PROPS fields, plus a few special cases:
| Key | Effect |
|---|---|
width, height, min-width, max-width, min-height, max-height |
Sets the corresponding Value field. |
gap, padding, margin |
gap is Value, the others are Spacing. |
grow, shrink |
Parses a float. |
background, color, tint, border-color |
Parses a color. |
border-width, radius |
Parses a Value. |
opacity, scale, rotation |
Parses a float. |
translate-x, translate-y, top, left, right, bottom |
Parses a Value. |
font-size, letter-spacing |
Parses a Value. |
items-h, items-v, align-h, align-v, text-align |
Parses an Align. |
justify |
Parses a Justify. |
transition-time |
Parses a float. |
ease |
Parses an easing name. |
position |
static / relative / absolute. |
z-index |
auto or integer. |
font |
Sets the string. |
background-texture |
Parses name [l t r b]. |
effect |
Sets the string. |
wrap, checked, value, options, passthrough |
Recognized but ignored (widget-specific, handled by factories). |
Everything else returns false.
6. var() substitution
Two functions, both in StyleAttr.hpp.
substituteVarRefs
inline std::string substituteVarRefs(
std::string v,
const std::unordered_map<std::string, std::string>& customProps);
Replaces var(--name) references in v with the values from
customProps. Iterates up to 10 times to handle chained references
(--a: var(--b)).
If a var() reference doesn't resolve, it's left as-is (the caller
decides what to do — usually nothing, and the resulting string fails to
parse).
std::unordered_map<std::string, std::string> props{
{"--primary", "#FF0000"},
{"--size", "100px"}
};
substituteVarRefs("var(--size) - 20px", props); // "100px - 20px"
substituteVarRefs("var(--missing)", props); // "var(--missing)"
The 10-iteration limit prevents infinite loops on circular references
(--a: var(--b); --b: var(--a);). After 10 passes, the remaining
var() calls are left unresolved.
resolveUnresolvedProps
inline Style resolveUnresolvedProps(
const std::unordered_map<std::string, std::string>& unresolved,
const std::unordered_map<std::string, std::string>& customProps);
Resolves a map of property → raw value (where the value contains
var()) into a typed Style. Each property is substituted and then
parsed via applyStyleAttr.
Returns a Style with only the properties that successfully parsed.
std::unordered_map<std::string, std::string> unresolved{
{"background", "var(--primary)"}
};
std::unordered_map<std::string, std::string> props{
{"--primary", "#FF0000"}
};
auto s = resolveUnresolvedProps(unresolved, props);
// s.background.is_set == true, value == red
7. Parsing utilities
Free functions used by the parser and exposed for reuse.
trim
inline std::string trim(std::string s);
Removes leading and trailing whitespace.
trim(" hello "); // "hello"
stripComments
inline std::string stripComments(std::string_view src);
Removes // line comments and /* ... */ block comments. Preserves
comments inside string literals (respects "..." boundaries).
stripComments("a // comment\nb"); // "a \nb"
stripComments("a /* block */ b"); // "a b"
stripComments(R"("a // b")"); // "\"a // b\"" — comment preserved
The string awareness is what makes it safe to have // or /* inside
text content.
splitBy
inline std::vector<std::string> splitBy(std::string_view s, char sep);
Splits on a character, no whitespace trimming.
splitBy("a,b,c", ','); // ["a", "b", "c"]
splitBy("a,b,", ','); // ["a", "b", ""]
splitBy("", ','); // [""]
Trailing separators produce an empty final element.
splitWs
inline std::vector<std::string> splitWs(std::string_view s);
Splits on whitespace, collapsing consecutive spaces.
splitWs("a b c"); // ["a", "b", "c"]
splitWs(" a b "); // ["a", "b"]
splitWs(""); // []
parseDurationSec
inline float parseDurationSec(std::string_view s);
Parses a duration string into seconds. Handles "1s", "500ms", and
bare numbers (interpreted as seconds).
parseDurationSec("1s"); // 1.0
parseDurationSec("500ms"); // 0.5
parseDurationSec("2"); // 2.0
Throws std::invalid_argument / std::out_of_range on invalid input
(uses std::stof internally). The caller is expected to catch.
parseAspectRatio
static float parseAspectRatio(const std::string& s);
Parses "16/9" or a decimal number into a float. Used by the media
query parser.
parseAspectRatio("16/9"); // 1.777...
parseAspectRatio("1.77"); // 1.77
normalizeKeyframes
inline void normalizeKeyframes(KeyframeAnimation& anim,
const ParseLoc& loc);
Fills in missing properties in a keyframe animation. For each property that appears in some frames but not others, the missing values are copied from the nearest neighbor (previous frame first, then next).
Logs a warning for each auto-filled property, with the source location.
@keyframes example {
0% { scale: 1.0; }
50% { scale: 1.2; opacity: 0.5; }
100% { scale: 1.0; }
}
Here opacity is set only at 50%. normalizeKeyframes copies 0.5
into the 0% and 100% frames, logging two warnings.
The name "normalize" is a bit misleading — the animation would work without this step (the evaluator's per-property fallback handles missing endpoints). Normalization just makes the behavior explicit and consistent.
applyStyleDeclaration
inline void applyStyleDeclaration(Style& st,
const std::string& keyRaw,
const std::string& valRaw,
const ParseLoc& loc);
The high-level declaration applier. Handles:
- Custom properties (
--name) — stored inst.customProps. - Properties with
var()— stored inst.unresolvedProps. transition,transition-*shorthands and longhands.animation,animation-*shorthands and longhands.filter— parsed into aFilterReflist.box-shadow— parsed into aBoxShadow.overflow,overflow-x,overflow-y— parsed into the enum.- Fallback to
applyStyleAttrfor the standard properties.
Logs a warning for unknown properties and invalid values.
This is the function called for each declaration in a rule, a
keyframe, or :root. It's public because it's used by the parser and
by some test files, but for most use cases you'd just call
loadStyleFile / loadStyleString.
8. calc() parsing
The calc() support is implemented via an internal tokenizer and
recursive descent parser. Both are in namespace ZenitUI::detail and
not part of the public API, but documented here for completeness.
CalcToken
struct CalcToken {
enum class Kind {
Number, Ident, Percent,
Plus, Minus, Star, Slash,
LParen, RParen, End
};
Kind kind{Kind::End};
float num{0.0f};
std::string ident;
};
A single token in a calc() expression.
tokenizeCalc
inline std::vector<CalcToken> tokenizeCalc(std::string_view s);
Splits a calc() body into tokens. Numbers are read as floats, units
as identifiers (px, vw, %), operators as single characters.
CalcParser
class CalcParser {
public:
explicit CalcParser(const std::vector<CalcToken>& toks);
bool parse(Value& out);
};
Recursive descent parser for calc() expressions. Grammar:
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := '-' factor
| '+' factor
| '(' expr ')'
| number ('%' | ident)?
Units are recognized: px, vw, vh, pw, ph, and %. An
unknown unit falls back to pixels.
The result is a Value — possibly with multiple terms for mixed-unit
expressions like 100% - 20px.
Note: * and / require one side to be a bare number. calc(50% * 2)
works (50% * 2.0), but calc(50% * 100%) doesn't (multiplication of
two units is not meaningful and falls back to the left operand).
9. Complete example
Parsing a stylesheet manually and inspecting the AST:
#include "StyleParser.hpp"
using namespace ZenitUI;
const char* src = R"(
:root {
--primary: #0079F1;
}
Button {
background: var(--primary);
padding: 8px 16px;
transition: background 0.25s ease-out;
}
Button:hover {
background: #3296FF;
}
@keyframes pulse {
0% { scale: 1.0; }
50% { scale: 1.2; }
100% { scale: 1.0; }
}
)";
ZMarkup::StyleParser parser(src, "example.zstyle");
auto sheet = parser.parse();
// sheet.rules.size() == 2
// sheet.rules[0].chain[0].kind == SimpleSelector::Kind::Tag
// sheet.rules[0].chain[0].name == "Button"
// sheet.rules[0].decls.size() == 3
// sheet.rules[1].chain[0].requireHover == true
// sheet.keyframes.size() == 1
// sheet.keyframes[0].name == "pulse"
// sheet.keyframes[0].frames.size() == 3
// sheet.rootDecls.size() == 1
// sheet.rootDecls[0].prop == "--primary"
// sheet.rootDecls[0].value == "#0079F1"
// Apply to a theme
Theme t;
ZMarkup::applyStyleSheet(sheet, "example.zstyle", t);
For most use cases, you'd skip the manual parsing and call
loadStyleString / loadStyleFile directly.
10. Pitfalls
StyleParser copies the source.
The constructor takes a std::string_view, but stores a stripped
std::string copy. This is safe (no lifetime issue with the view), but
it means the full source is in memory for the parser's lifetime. For
very large stylesheets, this is fine — parsing is a one-time cost.
parse() can be called multiple times.
It resets the internal position and re-parses. Useful for tests, but in practice you'd call it once.
Unknown properties are logged but not errors.
applyStyleDeclaration logs a warning and continues. The rest of the
rule is still applied. This is intentional — a typo doesn't break the
whole stylesheet.
calc() with malformed input returns nullopt from the outer
parseValueToken.
The inner CalcParser sets an error flag and returns a best-effort
value. The outer check !p.parse(out) decides whether to accept the
result. A malformed calc() is treated as a parse failure, and the
property is left unset.
var() substitution happens at cascade time, not parse time.
The parser stores unresolvedProps with the raw var() expression.
StyleResolver::resolveFor substitutes at resolution time, when the
cascade has been resolved and the final customProps are known. This
is what makes --x inheritable and overridable.
substituteVarRefs has a 10-iteration cap.
Circular references (--a: var(--b); --b: var(--a);) will leave the
var() unresolved after 10 passes. The resulting string fails to
parse, and the property is left unset. No warning is logged for the
cycle itself — the failure manifests as a parse warning.
applyStyleAttr returns bool, but applyStyleDeclaration doesn't.
applyStyleDeclaration handles more cases (shorthands, custom props,
etc.) and always logs a warning for unknown keys. If you need the
boolean "was this recognized?" check, use applyStyleAttr directly
on standard properties.
trim is by-value.
trim(std::string s) takes by value. It modifies the copy and returns
it. If you already have a std::string, this moves efficiently. If
you have a std::string_view, it copies.
stripComments preserves newlines for //.
A line comment is replaced by a single newline, so line numbers in the remaining source stay correct. Block comments are removed entirely, which can shift subsequent line numbers if the block spans multiple lines. The parser doesn't track this — line numbers in error messages might be slightly off after a multi-line block comment.
The vars map in StyleSheet is a subset of rootDecls.
Custom properties from :root appear in both. The vars map is kept
for backward compatibility with code that reads it directly. New code
should use rootDecls (which applyStyleSheet processes via
applyStyleDeclaration, routing custom props into theme.root.customProps).
normalizeKeyframes logs per-property per-keyframe.
A keyframe animation with 5 frames and 3 properties missing in 2 frames produces up to 6 warnings. For large animations, the log can be noisy. Filter by category or level if needed.
parseDurationSec throws on invalid input.
Unlike most parsers in the framework, this one uses std::stof
directly and lets exceptions propagate. The callers in
applyStyleDeclaration wrap it in try/catch, but a direct call site
must handle the exception.
Media query conditions with unsupported keys are skipped.
If a media query contains a key the parser doesn't recognize
(e.g. resolution), the condition is skipped (not the whole query).
The remaining conditions are still evaluated.
The parser doesn't support nested rules.
/* Not supported */
.card {
Button { color: red; }
}
Rules can't be nested inside other rules. Use descendant selectors
(.card Button { ... }) instead.
@media blocks are expanded inline.
Rules inside @media { ... } are added to sheet.rules with the
media field set. They aren't kept as a separate section. This means
the ordering in sheet.rules interleaves media rules with regular
ones, which affects the declaration order (and thus the cascade).
:root variables must be declared before use.
applyStyleSheet processes rootDecls before rules, so :root
custom properties are available when rules are resolved. But the
parser stores them in source order — if you manually inspect the
sheet, rootDecls might not be first. It doesn't matter for
applyStyleSheet, which processes rootDecls explicitly first.
Selectors with : in a class name break parsing.
The parser splits on : for state detection, so a class name
containing a colon isn't supported. This is a rare case — CSS also
doesn't allow colons in class names.
The part field is only set for ::part rules.
A ::part suffix on the last selector token moves the name into
rule.part. If you have a rule like Slider::knob:checked, the parser
might misinterpret :checked as part of the part name (since it
appears after ::). Use the state before :::
Slider:hover::knob is correct; Slider::knob:hover isn't parsed as
a state.
Unknown states are silently ignored.
Button:foo { ... } parses, but requireFoo isn't a field, so the
:foo is silently dropped. The rule matches all buttons. No warning.
This is intentional (forward compatibility) but can hide typos.
Attribute values are not validated at parse time.
applyStyleDeclaration parses each value, but a value that doesn't
parse is logged and ignored — the property is left unset. This means a
typo (background: blu;) doesn't break the rule, but silently leaves
the property unchanged.
11. See also
- ZStyle (guide) — the grammar and semantics.
- Theme — where parsed rules end up.
- Style — the
Stylestruct modified by the parser. - Unit — the
Valuetype produced byparseValueToken. - ZMarkup — the DSL that uses
applyStyleAttrfor inline attributes.