Changelog
All notable changes to this project are documented in this file.
The format follows Keep a Changelog,
and this project adheres to Semantic Versioning.
While the major version is 0, a breaking change bumps the minor version.
accent-sass is a fork of connorskees/grass.
Published to crates.io as accent-sass, accent_sass_compiler and
accent-sass-macro, which share a version and are released together. Entries
at 0.13.4 and below are upstream's and are kept for lineage.
[Unreleased]
Added
- the browser package is published to npm as
@zoosky/accent-sass, at the same version as the crates, starting with0.16.0.release.shbuilds it, runs the WebAssembly smoke scripts against it and checks what it would ship before anything is published, then publishes it after the crates;--npm-onlypublishes it alone for a version whose crates are already out - the crates and the npm package name the documentation site, https://zoosky.github.io/accent-sass/, as their homepage, so crates.io and npm link to it. It takes effect with the next published version
[0.16.0] - 2026-09-13
Sixty merged pull requests since 0.15.0: a JavaScript API and a C ABI for the
WebAssembly builds, --check on the command line, the move of the reference
implementation to dart-sass 1.104.1, the first deprecation warning, and a run
of fixes that bring @extend, builtin calls, selectors and error positions in
line with dart-sass.
The minor version rises because four changes are breaking while the major
version is 0: @extend across a media query is an error, builtin calls are
checked against dart-sass's parameter lists, seven inputs dart-sass refuses are
rejected, and a selector whose combinators cannot match is omitted.
Against the pinned sass-spec revision (b39c32768) the suite stands at 14,147
of 14,266 passing, measured on macOS 2026-09-13; the Linux CI runner reports
two fewer, the same offset as before. This release moved the pin
from 4a9eea66, which added 48 runs, so the count is not directly comparable
with 0.15.0's 13,926 of 14,218. Bulma, Pico, Foundation and USWDS compile
byte-identically to dart-sass 1.104.1.
Added
- a C ABI for WebAssembly hosts, behind the
wasi-exportsfeature.accent_sass_alloc,accent_sass_dealloc,accent_sass_compile_string,accent_sass_compile_pathandaccent_sass_result_freeturn thewasm32-wasip1cdylib into a reactor a plugin host instantiates once and calls many times, instead of paying process startup per stylesheet. Strings cross as UTF-8 in linear memory and a result is three 32-bit words: status, pointer, length. Built with the workspace'ssmallprofile, the module is 1.31 MiB against 2.42 MiB underrelease - options for that C ABI.
accent_sass_options_newreturns an opaque handle a host fills in and passes toaccent_sass_compile_string_with_optionsoraccent_sass_compile_path_with_options, which covers output style, entry point syntax, load paths,charset,alertAsciiandquiet. The setters are named after dart-sass's JavaScript API so this ABI and the browser binding do not drift apart; a value the ABI does not define is refused rather than rounded to a default.accent_sass_compile_stringandaccent_sass_compile_pathare unchanged and still compile with defaults - a JavaScript API for the browser build.
compileString(source, options)andcompile(path, options)joinfrom_string, which keeps working. The options arestyle,syntax,loadPaths,files,url,charset,alertAscii,quietandlogger, named after dart-sass's JavaScript API wherever it has a name for the same knob; a failed compile throws anErrorcarryingmessage,formatted,file,lineandcolumn, andindex.d.tsdeclares the surface with real types. Before this the package exported one function withStdFsbehind it, so every@usefailed at run time withCan't find stylesheet to import.and a page could compile a single stylesheet and nothing else MemoryFsandfrom_string_with_file_nameare public. The browser binding resolves imports throughMemoryFs, and making it a public type rather than something private to the binding is what letscargo testreach it: the bindings themselves exist only onwasm32-unknown-unknown. It also stands on its own for a host that already holds its stylesheets, such as a CMS compiling a theme out of a database--checkon the command line, which compiles and writes nothing. Given an output file it compares the CSS it produced against what that file already holds, so a job can ask whether committed CSS is still what the Sass produces without a build step, a temporary file or adiff. It exits3on a stale or missing output file and1on a stylesheet that does not compile, which keeps "your CSS is out of date" and "your Sass is broken" apart in a log;2is still clap's usage error. Writing nothing is the point of the mode: it reports whether a build is current without being able to change the answer- deprecation warnings, starting with
bogus-combinators. A style rule whose selector has a leading, trailing or repeated combinator, and an@extendinside one, now warn as dart-sass 1.104.1 does: the same message, and a source frame that marks the selector and the declaration, comment or at-rule that makes it a problem.a > {b: c}was silently dropped from the output before, and now says why. After five warnings of one deprecation the rest are counted, and the count is reported at the end Logger::deprecationandLogger::repetitive_deprecations_omitted, with theDeprecationandDeprecationWarningtypes they receive. Both methods have defaults, so an existing logger keeps compiling: a deprecation reaches it throughLogger::warnwith the message alone, and the count is dropped.DeprecationWarning::formattedholds the full textStdLoggerprintsOptions::verboseand--verbose, which report every deprecation warning instead of counting the ones after the fifth.--verbosewas accepted and hidden before, and did nothing--indentedworks, and is no longer hidden. It reads the entry point as the indented syntax whatever the file is called. That matters most for--stdin, which is the one input route with no extension to infer a syntax from, and the route the flag exists to serve
Changed
- the reference implementation moves from dart-sass 1.103.1 to 1.104.1, and
the changes of both releases are followed. A loud comment written above
@useor@forwardis written once, where it stands. Exact negative zero prints as-0rather than0, so it keeps its sign when used in a CSS calculation;math.round,math.ceil,math.floorand the calculationround()still return0, because dart-sass rounds to an integer. A colour channel that isNaNor negative zero becomes0, and so does a hue that is infinite:hsl(math.div(1, 0), 50%, 50%)is nowhsl(0, 50%, 50%)rather thanhsl(calc(NaN), 50%, 50%). As in dart-sass,color.change()on an rgb colour keeps such a channel until the colour is converted to another space, andred(),green()andblue()read a-0channel back as0 - Breaking:
@extendacross a media query is an error. An@extendwritten inside@mediamay only extend a selector in the identical query; extending across queries would need the extended rule to exist in a context it was never written for. It extended silently before, which is worse than refusing: the check was commented out and the store never recorded a selector's media context at all, so every selector looked top-level.!optionaldoes not silence it, since it says the extension need not match anything rather than that a wrong match is allowed - Breaking: builtin calls are checked against dart-sass's parameter lists.
Builtins read their arguments by position with no declared parameter list, so
an unknown name could not be rejected and a real one could not be found:
map.remove($key: 1)andlist.join(c, d, $invalid: true)compiled. All 196 registrations now carry the parameter list dart-sass 1.103.1 declares, overloads included, and a call picks its overload, is verified against it, and has its named arguments bound to their declared positions. The unknown-name error says "parameter", as dart-sass does, for user-defined functions too - Breaking: seven inputs dart-sass refuses are rejected. A style rule
inside a keyframe block,
@mixin --aand@include --a(plain CSS is getting mixins of its own and--names are reserved for them), a custom property nested beneath another declaration, and an unbalanced bracket in a selector. The bracket tracking also closes ten fixtures in the indented syntax, where a newline inside a bracket no longer ends the selector - Breaking: a selector whose combinators cannot match is omitted. Two
combinators in a row, more than one leading combinator, or a trailing one
make a selector bogus, so a rule left with nothing visible is not printed and
a useless selector can no longer act as an extender. The rules are ported
from the dart-sass 1.104.0 source, which hides such selectors when writing
CSS rather than in the evaluator, so
selector.parse(":is(.a > + .b)")still keeps its argument.selector.replace()raises dart-sass's "components may not be empty" where it used to panic - Breaking: two parse errors say what dart-sass 1.104.1 says. Where a
selector should start but nothing does, the error is
expected end of rule.rather thanexpected "}"., and text that starts no rule, such as! {}or a!optionalon a line of its own in the indented syntax, isunrecognized syntaxover that text. An unclosed block reports what its contents expected, so@function foo() {anda {foo: {bar: redsayExpected identifier.as dart-sass does
Fixed
- an error points where dart-sass points. Input that ends early was reported
one column short, on its last character rather than just past it, and an
input ending in a newline highlighted the newline across the line. A missing
expression highlighted everything back to the start of the expression, so
a {b: 1 + ;}underlined1 +where dart-sass puts one caret at the;. As in dart-sass, an "expected" error whose place is the start of a line now points at the end of the line before it, and a missing expression directly after#{highlights the#{. Only the caret moved; no message changed - ten command-line flags consumed the argument after them.
--indented,--update,--no-error-css,--no-source-map,--embed-sources,--embed-source-map,--watch,--poll,--no-stop-on-errorand-i/--interactivewere each declared with no action, and clap 4 defaults to an action that takes a value, soaccent-sass --watch style.scssreadstyle.scssas the value of--watchand then failed withthe following required arguments were not provided: <INPUT>. A script written against dart-sass's command line failed on the argument parse rather than on the missing feature, and the error named the input file rather than the flag that ate it. All ten are switches now. Six of them ----update,--watch,--poll,-i/--interactive,--embed-sourcesand--embed-source-map-- say they are ignored rather than passing in silence, because each would change what the program does if it worked;--no-error-css,--no-source-mapand--no-stop-on-erroralready describe what this binary does and stay quiet --stdinwith a named file writes to it. The lone positional was read as the input, soaccent-sass --stdin out.csstried to compileout.cssand failed withNo such file or directoryrather than writing the CSS it read from standard input, which is what dart-sass does. A second positional alongside--stdinis refused now rather than silently ignored. This overwrites the named file, which is what dart-sass does and what the old behaviour did not:cat x.scss | accent-sass --stdin style.scssused to compilestyle.scssand ignore standard input, and now truncates it and writes the compiled CSS there. Redirect to the destination instead if the positional was standing in for the input- a failed compile truncated the output file.
accent-sass style.scss app.cssopenedapp.csswithtruncate(true)before running the compile, so a stylesheet that stopped compiling left an emptyapp.cssbehind. It destroyed the last good CSS at the moment you most want to keep it, and a missing input file did the same. The output file is opened only after the compile succeeds @warnand@debugreport a string message as its text rather than with its quotes, as dart-sass does:@warn "careful"sayscareful, not"careful". Only the outermost value is unwrapped, so a string inside a list still prints quoted, and every other value is reported as before. The command line and theLoggertrait are both affected, because the quoting happened in the evaluator%and the calculationmod()with an infinite divisor no longer count positive zero as negative.0 % infinityis0rather thanNaN, and0 % -infinityisNaNrather than0, as dart-sass gives- unit names are case-sensitive, as they are in dart-sass. Only the canonical
spelling names a known unit, so
1Qnow prints as1Qrather than1q,math.div(1kHz, 1hz)no longer simplifies, and1PX + 1pxis an error about incompatible units. The one place case is ignored is the check that decides whether two units could ever be compatible, which is what makescalc(1Q + 1deg)an error andcalc(1Q + 1mm)legal - calculation errors say what dart-sass says. An operation a calculation
cannot perform is now distinguished from an expression it cannot hold and
reported at the operator, a rest argument is named as one, a bare list is
parenthesised in
Value (1 2 3) can't be used in a calculation., and the parser reportsexpected ")".,Expected expression.orExpected identifier.by position instead of the staleExpected number, variable, function, or calculation., which dart-sass no longer emits anywhere.calc("a"),calc(()),calc(#fff),calc(/ 1px)andcalc(1px % 2px)are rejected as the values and operators they are rather than as parse failures, and a keyword argument is named as one:sqrt($x: 1)reportsKeyword arguments can't be used with calculations. - a stylesheet may use
&at the top level. CSS nesting made&meaningful to the browser, so dart-sass passes a top-level one through as text rather than raising "Top-level selectors may not contain the parent selector", which it no longer emits anywhere. A parent selector carrying a suffix is still an error, with dart-sass's message, and the check looks inside a pseudo-selector's argument, because:is(&--x)is an error too - five defects in
sass:math. A fuzzyis_zero()stood in for an exact zero inasin,atanandlog, somath.log(0.000000000001)returnedcalc(-infinity)where its value is-27.63, andacoshad the same defect throughis_one().clamp()had no case for$min >= $maxand returned$numberon a tie, soclamp(1, 2, 0)gave0rather than1; it also compared$minagainst$numberand$minagainst$max, which letclamp(0, 1, 2px)through.$min-numberwas the smallest normal double where Dart'sdouble.minPositiveis the smallest subnormal, 16 orders of magnitude apart.math.unit()bracketed a denominator only when the numerator was empty, somath.unit(1px * 1em / 1rad / 1s)printedpx*em/rad*s - three defects in
string.split(). Rust'sstr::split("")matches at every character boundary, so an empty separator returned an empty piece at each end; it now splits into code points, and$limitis ignored on that path as in dart-sass. An empty string yields an empty list rather than one empty piece, and each piece keeps the input's quotedness rather than being quoted regardless - a map, and an arglist, is the list of elements each is.
Value::separatorreported a comma for every map,list.joinhard-coded one, andlist.appendturned a map or an arglist into a one-element list holding a container -- which then refused to serialize with "isn't a valid CSS value". An empty map, an empty arglist and an empty list are now equal in every direction,!=included, while an empty arglist is still not equal to(), the one pairing where the separator counts - an arglist takes the separator of the list splatted into it, a comma only
when nothing decided one, so
@include foo(1, 2, $list...)with a space-separated$listprints2 3 4 5rather than2, 3, 4, 5. The separator was lost in four places, and equality compared an arglist to any comma-separated list whatever its own separator. An arglist nested inside another list keeps the parentheses that keep the two separators apart - a childless at-rule keeps its place in the rule. It went into the tree
without the check that splits a style rule when a nested rule comes between
two of its children, so
a { b {c: d} @e f; }emitteda {@e f}first; source order decides the cascade if()expands a rest argument before verifying the call, soif(true, b, c...)no longer fails withMissing argument $if-false.The arguments written in the call stay unevaluated, so the branch not taken still never runs- a hex colour written with an alpha channel drops its source spelling:
#0123prints asrgba(0, 17, 34, 0.2), and an opaque#abcfas#aabbcc. Four- and eight-digit hex is not yet well supported in browsers, so dart-sass lets the serializer infer the form. The trigger is that an alpha channel was written, not that it is transparent - a private-use character is written back as an escape in expanded mode, as
dart-sass does, so
content: "\e600"no longer emits the character itself and no longer adds a@charset "UTF-8"the reference does not emit. Compressed mode writes the character. Escaping is a serialization concern, so such a character is still one character tostring.split() - the comments above a plain CSS
@importstay with it. Imports were collected and emitted ahead of the whole document, leaving the comments written between them behind. An@importwritten after a rule still moves up, but to the end of the import block rather than to the top of the document @extendproduces dart-sass's selector set.ComplexSelector::is_super_selectoris now a port of dart-sass 1.103.1'scomplexIsSuperselector, whose old check rejected.d > .eas a superselector of.b .d > .e;extend_complexreturns a single-selector path's selector itself, so it keeps its identity; andadd_extensionreads the target's extensions-by-extender list after adding the new extenders, so an extension loop closes- nested
@mediaqueries merge and are kept the way dart-sass does.MediaQuery::mergecompared the modifiers where dart-sass compares the types in the branch where exactly one query is negated, sonot screeninsidescreencame out asscreen; and the following-sibling test counted invisible siblings, so an empty bubbled@mediasplit its parent - a plain CSS function keeps its spelling. Interning rewrites
_to-, which is right for looking a function up and loses the spelling of a call that finds nothing and is written back as plain CSS, sofile_join(...)printed asfile-join(...). The spelling survivesmeta.callwith a string,get-function($css: true)and inspecting the reference - whitespace inside an unknown at-rule's value is collapsed, so
@apply ( --bar );prints@apply ( --bar );. The value was read with dart-sass's reader for selectors, which does not collapse, rather than its reader for declaration values, which does - a style rule's selector is read before
@extendrewrites it, for&in SassScript and for resolving nested rules, so--&no longer prints the extender that@extendadded. dart-sass keeps each rule's original selector beside the live one for exactly this;@extendstill reads the live one @at-rootinside an unknown at-rule no longer leaves an empty copy of it beside the one holding the body. dart-sass's_trimIncludedremoves the trailing run of included parents that already enclose the rule; this port returned the innermost node but never removed the run- a selector list keeps its line break wherever the newline falls, not only
when the whitespace after a comma holds one, so
a\n, bnested underz &prints on two lines as dart-sass does. The check searches tokens rather than building a string at every comma, which had made a long single-line list parse in quadratic time: 40,000 selectors took 8 s and now take 0.04 s - units convert part by part when either side is complex, so
(23in/2fu) > (23cm/2fu)istruerather thanIncompatible units cm/fu and in/fu.Each unit in the target's numerator pairs with the first convertible unit left in the source's numerator, and likewise for denominators; comparison, equality,+,-,%,clampandmath.compatibleall go through it - an inlined nested
calc()keeps parentheses around a leadingvar()call, socalc(1 + calc(var(--c)))printscalc(1 + (var(--c))). A variable may expand to anything, which is why dart-sass keeps them - a nested
@font-facedoes not get the enclosing selector. A nested at-rule normally gets a copy of the style rule so that declarations written inside it have somewhere to go;@font-faceis the exception, because its descriptors belong to the at-rule itself. The comparison is on the plain name, so@-moz-font-faceand@FONT-FACEstill bubble
[0.15.0] - 2026-09-08
Twenty-two merged pull requests since 0.14.0: plain CSS parity (nesting, the
@function rule, if()), the special CSS functions, the indented syntax's
newline rules, dart-sass's per-module @extend model, and the calculation
work.
Against the pinned sass-spec revision (4a9eea66) this takes the suite to
13,926 of 14,218 passing, measured on macOS 2026-09-08; the Linux CI runner
reports two fewer, an offset that is stable across commits.
Added
- the CSS
if()function in plain CSS files.if()with CSS-style conditions landed for Sass earlier; the plain CSS parser did not reach that code path and stopped at the first:. Asass()condition, which is settled at compile time, is rejected there - the plain CSS
@functionrule.@functionwhose name begins with--declares a CSS custom function rather than a Sass one, so Sass passes the rule through untouched -- parameters,returnsclause, nested rules and all. Itsresultdescriptor is parsed like a custom property, taking its value verbatim instead of as SassScript, and--a()at a call site always names a CSS custom function even where identifier normalisation would otherwise reach@function __a() - CSS nesting in plain CSS files. A
.cssfile may nest style rules; Sass no longer rejects them and no longer resolves them, since CSS nesting is the browser's job. The rule keeps its own selector and stays nested,&is written out unresolved, and at-rules stop bubbling out of a rule once nesting has been passed through - a bare
%is a value.a {b: %}and$x: %were parse errors; dart-sass takes a lone%as an unquoted string, and reads%as the modulo operator only when an operand follows it. A%value is rejected once the expression has consumed a comma, which is what dart-sass does attr()and the CSSif()count as special variable strings, so the colour functions leave them unevaluated instead of rejecting them:rgb(attr(c))passes through the wayrgb(var(--c))already did. Only the browser can resolve either. Sass's ownif($cond, $a, $b)is untouched, since it is evaluated long before a value is inspectedtype()takes the special-function text path, so its argument is text rather than SassScript and reaches the output as written, and a baretype(lowercases.-a-type(is not special, matching dart-sass: its argument stays a Sass expression and its quotes are normalized
Changed
-
Breaking: which names a
@functionmay have, following the function-name proposal and dart-sass 1.103.1.calcandclampare ordinary names now, and a function of either name wins over the calculation.expression,url,and,orandnotare reserved only as spelled, so-a-and()is a legal name;elementstays reserved through any vendor prefix.typeis newly reserved for the plain-CSS function. The name is checked as written rather than with_normalised to-, so-moz_elementis legal and-moz-elementis not -
Breaking: the minimum supported Rust version rises from
1.85.0to1.96.1, normalising the floor across the Accent crates. Per this project's policy, that makes the next release a minor version bump. The gating CI jobs move with it.Clippy reads
rust-version, so the bump turned lints on with no code change:collapsible_ifbegan suggesting let chains, which need 1.88, at 23 sites. All 23 are collapsed. No comment was displaced -- each sat above the outerif. -
Breaking:
@extendis scoped to the extending module's upstream closure, porting dart-sass's per-module extension model. A module loaded by@useor@forwardgets its own extension store, and one loaded in an import context shares the enclosing store, because@importmeans "as if written here". An@extendno longer reaches CSS in a sibling module that never loaded it, and a mandatory@extendwhose target is out of scope now errors instead of silently succeeding. Closes connorskees/grass#104 for this fork -
Breaking: a space-separated calculation is stricter. Values written next to each other are only meaningful when a neighbour is text the compiler cannot resolve, and the check for that looked inside an operation rather than at the operation itself.
calc(1 px + 2px),calc(1 px * 2),calc(1 (px + 2px)),calc(1 var(--c) + 2px),calc(var(--c) + 1px 2px)andcalc(1 min(var(--c), 2px))compiled here and are "Missing math operator." in dart-sass; they now error too. An operation is not text however much text it holds, which is what separates the rejectedcalc(1 px + 2px)from the legalcalc(#{$a} px + 2px) -
Breaking:
CalculationArggains aParenvariant, public throughsass_value, so an exhaustive match on it needs a new arm. Parentheses around a space-separated group are now recorded rather than inferred from position:calc(1 (2 var(--c)) 3)keeps them instead of flattening tocalc(1 2 var(--c) 3), which this compiler itself rejects on the way back in. Every pair the source wrote is kept, as dart-sass keeps them -
a declaration written after a nested rule splits the parent rule instead of being hoisted back up beside the earlier declarations, which is what dart-sass does. Where both set the same property, that changes the cascade
-
a loud comment does the same: one written after a nested rule holds its place in source order rather than hoisting up beside an earlier comment
-
an interpolation inside a calculation is one operand of the expression rather than opaque text for the whole argument, unless it is the whole argument. The source's whitespace is re-serialized (
calc(#{$a} + 2px)iscalc(1px + 2px)), and an inlined nestedcalc()keeps only the parentheses its precedence needs. An interpolation written against an identifier belongs to that identifier --calc(x#{$a})iscalc(x1)-- and such an identifier is opaque text, so it names neither a calc constant nor a function -
adjacency in a calculation binds looser than any operator, as it does in dart-sass, so
calc(#{$a} px + 2px)is1besidepx + 2pxrather than(1 px)plus2px
Fixed
- the three crate manifests declare
rust-version = "1.96.1", the MSRV this project documents and gates on. They said1.96, so cargo accepted a build on 1.96.0 -- a release this project has never tested against and does not support. The supported floor does not move; the manifests now say what the README, this file and the three gating CI jobs already said - an at-rule header in the indented syntax may be split across lines wherever
dart-sass allows it. A newline ends a statement in
.sass, but not at a position where a statement cannot end -- between@functionand its name, between@forand its variable, afterfrom, after a binary operator, inside an argument list or a bracketed list. This fork modelled that with a parenthesis depth counter, which only covered the parentheses; it now carries dart-sass'sconsumeNewlinesvalue at each call site, which closed 114 sass-spec failures across 26 areas. Seespecs/docs/features/10-indented-newlines.md //inside a plain CSS value is two slashes, not the start of a comment plain CSS forbids.a {b: 1///bar}in a.cssfile was rejected- whitespace may fall anywhere inside
@import ... supports(...), including a newline in the indented syntax: the whole query sits inside parentheses, so it is whitespace there the way it is in an argument list. The same applies to a non-supportsmodifier's arguments - the indented syntax tolerates a trailing
;at the end of a statement, and says "multiple statements on one line are not supported in the indented syntax." when a second statement follows it. A;was rejected outright @importends with a statement separator, so@import "a.css" band an indented block beneath an@importare errors, and a trailing;after it in the indented syntax is not- a custom property may have an empty value (
--a:;), per the CSS spec and dart-sass 1.103.1. It was an error, and--a:{b: c}was accepted where dart-sass expects a; - a declaration whose value has no CSS representation -- an empty list, say -- now raises the error dart-sass raises instead of being dropped from the output silently
- the indented syntax names what a stray indented block sits beneath, as
dart-sass does:
Nothing may be indented beneath a @import rule.rather thanNothing may be indented here selector.replace()rejects a parent selector in any of its three arguments, matchingselector.extend()and dart-sass. It previously accepted&and panicked while serializing the result@extendis deterministic.ExtendedSelectorhashed its pointer but compared its value, so two rules with equal selectors could hash differently, collide in a hash set, and leave one of them without its extension -- wrong output in 2 of 400 runs of the same input- a silent comment inside a special function is dropped rather than copied into
the output, and a quoted string inside one keeps the source's quote character
(
-a-calc('x')stays'x', whileunknown('x')is still normalized). A custom property keeps its//, since its value is raw CSS where//is two slashes, andurl()andurl-prefix()hold a raw URL for the same reason
[0.14.0] - 2026-09-04
The first release under the accent-sass name, and the first whose version
is the fork's own rather than inherited from upstream. It collects twenty
merged pull requests: the complete CSS Color 4 model, the CSS math functions,
the CSS if() function, first-class mixins, and the module-system work.
Against the pinned sass-spec revision (4a9eea66) this takes the suite to
13,560 of 14,218 passing, measured 2026-09-04.
Added
- CSS math functions inside calculations --
round(),mod(),rem(),abs(),sign(),log(),exp(),pow(),sqrt(),hypot(),clamp(),min(),max(), and the trigonometric functionssin(),cos(),tan(),asin(),acos(),atan(),atan2()-- and the calculation constantspi,e,infinity,-infinityandNaN - the CSS
if()function - first-class mixins, and
meta.load-css()with configuration - support every CSS Color 4 color space, matching Dart Sass 1.79+: colors can be written with the plain-CSS
lab(),lch(),oklab(),oklch(), andcolor()functions (color(display-p3 1 0 0),color(xyz 0.3 0.2 0.1), and thesrgb,srgb-linear,display-p3-linear,a98-rgb,prophoto-rgb,rec2020,xyz-d50, andxyz-d65spaces), any channel or the alpha can benone, and a color remembers its space:color.to-space(),color.channel(),color.adjust(),color.change(),color.scale(),color.mix()(with a$method),color.invert()andcolor.complement()(with a$space),color.grayscale(),color.is-in-gamut(),color.to-gamut()(clipandlocal-minde),color.same(),color.is-legacy(), andcolor.ie-hex-str()accept and produce colors in every space, with every conversion computed in the same operation order as Dart Sass - add
color.is-missing()andcolor.is-powerless(), andcolor.opacity()to thesass:colormodule - track the legacy color space (
rgb,hsl, orhwb) a color was written in or last converted to, matching Dart Sass 1.79+:color.space()reports it, an hsl-space color always serializes ashsl(..)(socolor.adjust(hsl(120 100% 50%), $lightness: -50%)printshsl(120, 100%, 0%)instead ofblack), an hwb-space color serializes as a name or hex code when its rgb channels are whole numbers and ashsl(..)otherwise,color.mix()without a$methodandrgba($color, $alpha)produce rgb-space colors, and compressed output takes the shorter of the rgb and hsl forms - add
color.to-space(),color.is-legacy(),color.is-in-gamut(),color.to-gamut()(both theclipandlocal-mindemethods), andcolor.same()for the legacy spaces - accept the
$spaceargument tocolor.invert()andcolor.complement(), and the$methodargument tocolor.mix()(rgb,hsl, orhwb, optionally with ashorter,longer,increasing, ordecreasinghue interpolation method);color.mix()with a$methodinterpolates with premultiplied alpha per CSS Color 4 - support missing hues: converting an achromatic color into hsl or hwb (with an explicit
$space, or implicitly incolor.invert()) yields a color whose hue isnone, which serializes ashsl(none 0% 50%)and whichcolor.adjust(),color.scale(),color.invert(), andcolor.complement()refuse to modify with Dart Sass's error - add the plain-CSS
hwb()function (space-separated syntax) as a color;hwb()no longer range-checks whiteness or blackness, scaling a sum above 100% like Dart Sass - add
color.channel($color, $channel, $space: null)for the legacy rgb/hsl/hwb spaces - accept the
$spaceargument tocolor.adjust(),color.change(), andcolor.scale()(rgb/hsl/hwb only) - store legacy color channels as floats instead of rounding to integers, matching Dart Sass 1.79+; non-integral rgb colors serialize as
rgb(R%, G%, B%), colors written as or derived fromhsl()/hwb()serialize inhsl(..)form (without adegsuffix), and a NaN hue serializes ascalc(NaN) - support unquoted imports in the indented/SASS syntax
Changed
- Breaking: the crates move to the Rust 2024 edition, which raises the minimum supported Rust version from
1.70.0to1.85.0. The workspace setsresolver = "3"to match, and formatting follows the 2024 style edition -- reordered imports and collapsedif/elseacross the tree - Breaking: the proc-macro crate is renamed from
include_sasstoaccent-sass-macro.include_sasson crates.io belongs to upstreamgrass, so the fork cannot publish under it. Users ofaccent_sass::include!are unaffected; the dependency is internal - Breaking: the crates are renamed.
grassis nowaccent-sass,grass_compileris nowaccent_sass_compiler, and the binary isaccent-sass. Rust paths move fromgrass::toaccent_sass::. The upstream this forks is stillconnorskees/grass @forwardvisibility (show/hide) and module member conflicts are enforcedmeta.load-css()and@useshare one module instance, and the module cache takes part in@importof configured modules- the sibling-combinator compounds move instead of cloning during selector unification
- serialize non-legacy colors as Dart Sass does:
lab(50% 10 20),oklch(50% 0.1 20deg),color(srgb 1 0 0 / 0.5),nonefor missing channels (alsorgb(none 1 2)andhsl(120deg none 50%)for legacy colors with a missing channel), and a lab-family color whose lightness is out of range ascolor-mix(in lab, color(xyz ...) 100%, black)or with a relativefrom blackprefix - the legacy functions (
red(),hue(),lighten(),opacify(),alpha(),rgba($color, $alpha),mix()without a$method,invert()without a$space, ...) reject non-legacy colors with Dart Sass's errors;color.mix()andcolor.invert()name the color that needs a$methodor$space - rewrite the
$channelsparsing ofrgb(),hsl(), andhwb()as a port of Dart Sass's_parseChannels: the error messages match ($channels: Expected a space- or slash-separated list, was (1, 2, 3),The rgb color space has 3 channels but (1 2 3 4) has 4.,$red: Expected 10px to have unit "%" or no units.),var()andcalc()fall back to the plain-CSS function call (color.hwb(var(--foo))ishwb(var(--foo))), and a%alpha or channel scales like Dart Sass - match Dart Sass's number output exactly:
inspect()prints full precision (0.6666666666666666), a whole number prints its exact integer digits, a number is rounded to ten decimal places by decimal digit (so0.99999999999999prints1instead of0in compressed mode), and compressed output keeps the zero of a short negative number (-0.5) rgb($color, $alpha)with a NaN alpha yieldsrgba(255, 0, 0, 0)like Dart Sass, and the plain-CSSinvert(),grayscale(), andopacity()write a NaN argument ascalc(NaN)color.channel()reads the channels of the color's own space (or of$space), errors on a channel the space does not have, and is case-sensitive;color.adjust(),color.change(), andcolor.scale()pick the space from the channel keywords, error on a keyword the space does not have ($red: Color space hsl doesn't have a channel with this name.), clamp adjusted rgb channels instead of erroring on the range, leave changed channels unclamped (color.change(#cc0f35, $red: 300)is out of gamut), and scale a channel that is already out of range no further- match Dart Sass's hsl/hwb conversion bit for bit (plain multiply-add operation order instead of FMA, hue scaled as
(hue / 360) % 1), round the legacyred()/green()/blue()results like Dart Sass does, and keep out-of-gamut legacy hsl colors unclamped (hsl(-1 -1 -1)round-trips ashsl(359, 0%, -1%); saturation is lower-clamped at 0 like the CSS channel) - serialize an interpolated
calc()without the source's leading/trailing whitespace inside the parentheses, matching Dart Sass - keep a trailing loud comment on the same output line as the declaration it follows, matching Dart Sass
- apply
@extendacross@use/@forwardboundaries: extending a placeholder defined in another module now emits the extended rule instead of nothing (the shared-store approximation of connorskees/grass#104) - indent continuation lines of a multi-line selector list to the current level, matching Dart Sass
color.adjust()no longer clamps lightness (and no longer upper-clamps saturation), matching Dart Sass 1.79+; a legacy color pushed out of the rgb gamut serializes inhsl(..)form- serialize numbers that have no plain-CSS representation as
calc(), matching Dart Sass: a non-finite value becomescalc(infinity),calc(-infinity), orcalc(NaN)(with units as factors, e.g.calc(infinity * 1px)), and complex units become e.g.calc(1px / 1em)instead of erroring with "isn't a valid CSS value"
Fixed
- selector unification ordering, and namespace superselectors
- comments and newlines are accepted everywhere the grammar already permits them
- the MSRV build, and user-defined functions whose names collide with the new math functions
- fix
%with an infinite operand: an infinite dividend is NaN, and an infinite divisor keeps the dividend when the operands share a sign and is NaN otherwise - error when
@extendis used across@mediaboundaries - more robust support for NaN in builtin functions
Upstream history (grass)
Everything below is upstream's changelog. Only the heading levels changed, so the versions nest under this document's title; the text is untouched.
0.13.4
- support
...$keysargument tomap-has-key(..)/map.has-key(..) - parse aliased colors (e.g.
cyanforaqua) as colors rather than identifiers
0.13.3
- implement builtin string-module function
string.split(..)(#96) by @xpe - implement functionality for intercepting logs (#93) by cryocz
0.13.2
- update rustix dependency to silence security warning
- fix @forward statement altering the scope of the forwarded module (#85) by @kketch
- bump MSRV to 1.70.0
0.13.1
- update
clapdependency to 4.x.x to silenceattysecurity warning - bump MSRV to 1.64.0 for new
clapversion - fix bug in which
--no-charsetflag wasn't respected
0.13.0
- fix various module system bugs when combined with
@import. this is potentially breaking in rare cases where users were relying on the incorrect behavior - expose more AST internals in
grass_compiler - allow building docs with stable/beta rust compiler
- support
...$keysargument tomap-get(..)/map.get(..)(#83)
0.12.4
- implement builtin map-module functions
map.deep-merge(..)andmap.deep-remove(..)
0.12.3
No visible changes for users of the grass crate
Exposes more internals of the grass_compiler crate, allowing for custom functions implemented in rust to be accessed from Sass.
0.12.2
- implement an import cache, significantly improving the performance of certain pathological cases
- slash lists can be compared using
== - resolve rounding errors for extremely large numbers
- potentially breaking bug fixes in certain color functions
color.hwb(..)no longer allows whiteness or blackness values outside the bounds 0% to 100%scale-color(..)no longer allows the$hueargument. previously it was ignoredscale-color(..),change-color(..), andadjust-color(..)no longer allow invalid combinations of arguments or unknown named arguments- many functions that accept hues now convert other angle units (
rad,grad,turn) todeg. previously the unit was ignored
- improve compressed output of selectors containing newlines and
rgba(..)colors - improve resolution of imports containing explicit file extensions, e.g.
@import "foo.scss" - fix bug in which whitespace was not emitted between
+or-inside calc for compressed output (#71 by @ModProg)
0.12.1
- add
grass::include!macro to make it easier to include CSS at compile time - various optimizations improving the bootstrap benchmark by ~30% and the bulma benchmark by ~15%
- improve error message for complex units in calculations
- more accurate formatting of named arguments in arglists when passed to
inspect(..) - more accurate formatting of nested lists with different separators when passed to
inspect(..) - support
$whitenessand$blacknessas arguments toscale-color(..) - more accurate list separator from
join(..) - resolve unicode edge cases in
str-index(..) - more robust support for
@forwardprefixes - allow strings as the first argument to
call(..) - bug fix: add back support for the
$cssargument toget-function(..). regressed in 0.12.0
0.12.0
- complete rewrite of parsing, evaluation, and serialization steps
- implement the indented syntax
- implement plain CSS imports
- support for custom properties
- represent all numbers as f64, rather than using arbitrary precision
- implement media query merging
- implement builtin function
keywords - implement Infinity and -Infinity
- implement the
@forwardrule - feature complete parsing of
@supportsconditions - support media queries level 4
- implement calculation simplification and the calculation value type
- implement builtin fns
calc-args,calc-name - add builtin math module variables
$epsilon,$max-safe-integer,$min-safe-integer,$max-number,$min-number - allow angle units
turnandgradin builtin trigonometry functions - implement
@at-rootconditions - implement
@importconditions - remove dependency on
num-rationalandbeef - support control flow inside declaration blocks For example:
a {
-webkit-: {
@if 1 == 1 {
scrollbar: red;
}
}
}
will now emit
a { -webkit-scrollbar: red; }
- always emit
rgb/rgba/hsl/hslafor colors declared as such in expanded mode - more efficiently compress colors in compressed mode
- treat
:wherethe same as:isin extension - support "import-only" files
- treat
@elseifthe same as@else if - implement division of non-comparable units and feature complete support for complex units
- support 1 arg color.hwb()
0.11.2
- make
grass::ErroraSendtype - expose more internals of
grass::Error, allowing for custom formatting - fix WASM builds
0.11.1
- fix load path bug in which paths were searched for relative to the SCSS file, not the executable (#57)
0.11.0
fsoption added to allow interception and reimplementation of all file system operations (such as imports)wasmfeature renamed to/replaced withwasm-exports, which no longer materially alters the API:from_pathis reinstated, andfrom_stringonce again returns the full error type; but the WASM exportfrom_string(which returns a string error) is now a new functionfrom_string_js. (It was renamed fromwasmtowasm-exportsbecause the name was misleading; Rust code that uses grass doesn’t need this feature, it’s solely to get thisfrom_stringWASM export.)
0.10.8
- bugfix: properly emit the number
0in compressed mode (#53)
0.10.7
- special case plain CSS fn
clamp - support more uses of plain CSS fns inside
rgb/rgba/hsl/hsla - better support for
@at-rootat the toplevel and inside media queries - bugfixes for the module system
- more robust handling of load paths that are directories
0.10.6
- feature complete, byte-for-byte support for bootstrap
- add bootstrap v5.0.2 to ci
- run script to verify output against the last 2,500 commits to bootstrap
- feature complete
min/maxsupport -- special functions andmin/maxare now allowed as arguments - removed dependency on
peekmore, which sped up parsing and simplified lookahead - emit comments inside the
@ifrule body - fix bug in
hue(...)function in which the value would be incorrect when theredchannel was the highest and the green channel was lower than the blue channel - no longer round output from
saturation(...)function - improve handling of newlines for
@media,@supports,@at-root, placeholder selectors, unrelated style rules, and unknown @-rules - arglists can be equal to comma separated lists
- throw error for invalid uses of
@charset - more robustly parse
@else if, allowing escaped and uppercase characters - resolve two
@extendbugs -- one in which we would incorrectly emita b, a > bas a selector, even thougha bis a superselector ofa > b, and a feature called "three-level extend loop", in which a stylesheet whereaextendsb,bextendsc, andcextendsawould fail to include all 3 selectors in certain places - support compressed values for comma separated lists and numbers
- more robustly parse unknown @-rules
0.10.5
- support compressed output
- support new builtin functions
math.div,map.set - support the HWB colorspace and builtin functions
color.hwb,color.blackness,color.whiteness :ispseudo selector is now considered an alias of:matchesin@extend- support
$keys...argument inmap.merge %now implements the modulo operation, rather than finding the remainder. this largely affects negative numbers- fix parsing bug in which
/***/in a selector would miss the closing/
0.10.4
- plain css
invert(..)accepts numbers with any unit - plain css imports (e.g.
@import url(foo)or@import "foo.css") are now emitted at the top of documents
0.10.3
- hyphen followed by interpolation is not treated as subtraction, e.g.
10-#{10}=>10 -10rather than0 - function arguments do not affect variables in outer scopes (fixes #37)
- improve error messages for NaN with units passed to builtin functions
0.10.2
- use
std::fs::OpenOptionsto open files (#35 by @MidasLamb)
0.10.1
- implement
@useand the module system - support the filter syntax for function arguments, e.g.
alpha(opacity=1) - disallow certain at-rules in functions, resolving several panics
- allow vendor-prefixed special CSS functions, e.g.
-webkit-calc(...) - allow decimal percent selectors inside
@keyframes - allow vendor-prefixed
@keyframes - resolve parsing bug for maps involving silent comments
- allow escaped
!in selectors - allow multiline comments in functions
- resolve several panics on malformed input when parsing bracketed lists
- support NaN in all contexts
- add support for unicode ranges
- recognize plain CSS imports beginning with
//, e.g.@import "//fonts.googleapis.com/css?family=Droid+Sans"; - resolve integer overflows in
@forwhen bounds were equal toi32::MINandi32::MAX - allow quoted strings in default function arguments
0.10.0
- bugfixes for
@mediaquery regressions - bugfixes for maps, arglists, and
@each - implement string interning for identifiers and style properties
- implement spec-compliant variable scoping
- emit
@importwhen importingurl(...)or*.css - resolve all panics for malformed
@import - various optimizations that now allow us to compile bootstrap 10% faster than
libsass - errors inside builtin functions use
inspectto print values - bugfixes for color and map equality (e.g.
red==#ff0000) - hide unimplemented command line flags
- implement CLI options for
--quiet,--load-path(#22 by @JosephLing),--no-charset,--stdin, and--no-unicode - use unicode characters in error messages by default
- allow comma separated
@importstatements (#23 by @JosephLing) - implement and correctly parse
!optionalin@extend - lazily evaluate
!defaultvariable values - disallow interpolation in mixin and function names
- improve parsing for
@supportsand unknown at-rules
Breaking
- functions now take an
Optionsstruct
0.9.5
A small release fixing potential build issues and improving documentation.
This release is not published to NPM due to a bug
in wasm-pack.
0.9.4
- implement
@keyframes - don't strip newlines following comments in selectors
0.9.3
- fix parsing bugs for empty bracketed lists
- partially implement inverse units
- remove all remaining
todo!()s from binary and unary ops - parse keywords case sensitively
- various optimizations that make bulma about 6x faster to compile
0.9.2
- implement builtin functions
minandmax - bugfixes for
@extendandselector-unify - allow
@contentto take arguments - bugfixes for
@content, for example it will no longer infinitely recurse for chained mixins - better support queries in
@media - bugfixes for
@media - add support for splats, e.g.
rgba([1, 2, 3, 4]...) - resolve a number of parsing bugs for
@for, variable declarations, selectors, and maps - completely rewrite how styles are evaluated, allowing short circuiting of values like
false and unit(foo)andif(true, foo, unit(foo)
0.9.1
This release is largely focused on @extend, but it also resolves some regressions resulting from the new parser.
- implement
@extend - properly document new API
- MVP implementation of
@supports - fix regression in which
@at-rootwould panic when placed after a ruleset - fix regression related to
@mixinand@functionscoping when combined with outer, local variables - remove most remaining
unwraps that could result in a panic
0.9.0
This release is focused on setting up the groundwork for implementing @extend as well
as being able to compile Bootstrap.
- implement all builtin selector functions
selector-appendselector-extendselector-nestselector-parseselector-replaceselector-unifysimple-selectorsis-superselector
- implement builtin function
content-exists - allow
@import,@warn, and@debugin all contexts, such as inside@mixin - refactor control flow evaluation, resolving some issues blocking Bootstrap
Breaking Changes
- remove the
StyleSheetstruct in favor of freestanding functions,from_stringandfrom_path
0.8.3
This release is largely focused on performance and robustness
- implement smallint optimization for numbers, making some benchmarks 50% faster
- remove
bimapas a dependency for storing named colors in favor of an ad hoc, more specialized data structure - remove dozens of panics on malformed input
- use
beef::Cowinstead ofstd::borrow::Cow - increase code coverage to 80%
0.8.2
This release contains significant (>10x) improvements for WASM speed.
Performance is now comparable to libsass bindings with node-sass as
well as dart-sass with dart2js. It is, however, roughly 4x slower than
native grass.