module

Hwaro::Assets::Sass::Expr

Constants

RAW_SPAN_FNS = ["url", "env", "attr", "counter", "counters", "expression", "format", "local"] of ::String

Function names whose parenthesized span belongs to CSS and must pass through verbatim, never parsed as Sass arguments. var() is deliberately NOT here: dart-sass parses its fallback argument as SassScript (var(--x, escape-svg($d)) must call the function), and the computes? gate still passes plain var(--x, 10px) through verbatim.

UNPARSEABLE = Set(String).new

Storage text that failed to parse or evaluate. Coercion runs against a context-free CoerceHost, so failure depends on the text alone; remembering it spares a raised SoftEvalError per member each time a stored list is read back — an append(..., comma) loop over unparseable strings re-coerces every member per iteration.

UNPARSEABLE_LIMIT = 50000
UNPARSEABLE_LOCK = Mutex.new

Class methods

coerce(text : String) : Value

Coerces stored text back into a typed value ("1px" -> Number, "(a: 1)" -> Map). Unparseable text stays Raw — lazily typed strings are the storage model.

Source
computes?(node : Node, host : Host, force_div : Bool = false, fold_calc : Bool = true, amp : Bool = false) : Bool

True when evaluating the tree would do real work: any operator, not, or a call that resolves to a known (user or built-in) function. Bare literals/variables/lists resolve identically via the legacy path, so they don't count — that is what keeps existing output byte-identical.

force_div marks a division-forcing context (variable values, interpolation): there a literal-operand / counts as work too.

amp marks a value context, where a bare & is the parent selector list (or null at the root) and must be evaluated. In selector templates it stays verbatim for substitute_parent.

Source
div_operand_forces?(node : Node) : Bool

True when this node, used as a / operand, forces the slash to divide (the classic Sass rule): a variable reference, a function call, or grouping parens. Unary wrappers force iff their operand does (-1/2 stays literal, -$a/2 divides — dart-sass parity).

Source
divide_numbers(ln : Number, rn : Number) : Tuple(Float64, String)
Source
fold_calc(text : String) : Number | Nil

Division with the math.div unit rules plus convertible-group cancelling: same units (or convertible ones) cancel, a unitless divisor keeps the numerator's unit. Folds a verbatim calc(...) span to the single number dart-sass would emit (calc(10px + 5px * 2) → 20px, calc(9 / 21 * 100%) → 42.8571428571%). nil when the contents aren't statically foldable — unknown functions (var(), env()), incompatible or unitless↔united addition, keywords — in which case the span passes through verbatim as before (valid CSS the browser computes itself; a partial rewrite of working CSS buys nothing).

Source
parse(template : Ast::TextTemplate) : Node | Nil

Parses a template into an expression tree; nil when the text is outside the expression grammar (lenient callers then use the legacy verbatim path).

Source
parse!(template : Ast::TextTemplate) : Node

Strict parse for control-flow contexts. Every ParseFailure — the lexer's included — must convert to SoftEvalError here: a raw ParseFailure escaping the compiler bypasses error classification entirely and surfaces as an unlocated crash.

Source
slash_division?(list : ListV) : Number | Nil

A slash run that was PARSED as a / b but never forced to divide (min(10 / 2, 8)) folds to its division when every element is a plain number — dart-sass's lazy-slash numbers divide the moment a numeric context coerces them. nil for constructed slash lists (list.slash; only lazy_slash pairs divide — dart errors on math over a real slash list) and when any element isn't a number. A zero divisor folds to Infinity, exactly as dart's lazy division does (min(10 / 0, 8) is 8).

Source

Nested types