Revision history for CSS::Minifier
0.0.9 2026-06-06T05:43:28+03:00
- Refactor: consolidate color/unit normalization helpers in Util.rakumod
(NAMED-RE/HEX-TO-NAMED-RE, strip-zero-units, normalize-hex-value);
$UNITS now derives from $LENGTH-UNITS via BEGIN block
- Refactor: method cleanup — !same-selectors→eqv, !make-declaration helper,
!url-quote→starts-with/ends-with, inline !decl-key, remove clear-comb-cache
from parse, return-type annotations on Merging, rename $space→$brace-space
(Writer), $p→$sel (Parser)
- CQ: minor cleanup — comment $UNITS vs $LENGTH-UNITS asymmetry, replace
magic 12 with anchored regex, remove redundant !apply-font-weight guard,
fix !make-declaration indent, remove unused *%opts from minify()
- Fix: parser edge cases — has-brace off-by-one and bare-/ double-increment,
!parse-at-rule indent, '@' in extract-braceless respects $in-license,
limit substr peek to 12 chars, typed $buf/$in-license
- Fix: normalization correctness — hex shortening/folding in
declarations-to-key, !normalize-quotes escaped \" protection,
!strip-zero-units length-only guard, !url-quote-value quoted-paren fix,
!url-quote-selector single-quote guard, rgb-values-to-hex float percentage
and :i guard, empty background longhand guard, remove
text-decoration-thickness/underline-offset from %COLOR-PROPS
- Fix: dedup/merge — Dedup returns result instead of mutating param,
bold/normal word boundaries (Bug 1), selector identity over .fc (Bug 2),
revert-layer (Bug 3), strip-zero-units 0+ match (Bug 4),
add default branch, align AtRule barrier check
- Perf: fast-path guards on declarations-to-key/rgb-values-to-hex/
strip-zero-units; combine grep+map in !merge-decls; remove 100KB
comb-cached bypass so large files benefit from cache
- Test: delete 01-basic (merged into golden), qqx→run, add comb-cache
eviction, --output, !important precedence, empty-ruleset :sep tests
- Fix: percentage rgb() now rounds (.round) instead of truncating
(.Int) — 50% → #80 not #7F per CSS Color spec
- Fix: selector comparison uses identity over .fc — class/ID
selectors are case-sensitive per spec (Merging + Dedup)
- Fix: CLI --level without value always errors (last-arg edge case)
- Perf: gradual cache eviction (delete oldest 250 keys) instead
of full hash flush; skip color-masks pass when color-names enabled
- CQ: declarations-to-key skips normalization for --custom props
- CQ: COMB-CACHE-MAX configurable via CSS_MINIFIER_CACHE_MAX env var
- CQ: !assemble-background-multi uses manual max loop instead of
%.values».elems intermediate arrays
0.0.8 2026-06-05T07:46:49+03:00
- ~15× performance improvement: comb-cached .comb with binding
(:=) at all call sites, skip-quoted copy elimination, .chars
caching in hot loops
- Color masking fixes: url() quarantine (prevents filename
corruption), rgb()→named-color without :color-masks,
%COLOR-PROPS fast-path skip, %NON-COLOR-PROPS font-family guard,
custom property bypass, :i flag + .lc lookup in named-color
alternation, leading-space rgb/rgba, hex→name word-boundary
match, rgba 1.0 fix, CSS Color Level 4 space-separated rgb
- Parser fixes: OOB edge cases in !extract-braceless,
find-semicolon $i++ consistency, bare / fallthrough,
depth-tracked @import extraction, %NON-COLOR-PROPS font-family
skip in !apply-color-names
- Writer fixes: strip-zero-units .0 edge case, null-byte sentinel
\x[1]→\x0, url-quote non-greedy backtracking, rgba 1.0 fix,
Level 4 space-sep rgb, UNITS constant (one-time compilation),
url-quote-value helper extraction
- Plugin fixes: Dedup added to pipeline (was unused),
extra-plugins unconditional die guard, at-rule-decl-key case
sensitivity, custom property !important bypass, .lc cache in
!same-selectors, dead Values plugin removed
- Dedup/Merging enhancements: normalize equivalent colors
(named→hex, bold→700) in declarations-to-key, Unicode .fc in
selector dedup, rgb→hex in declarations-to-key
- Refactoring: declarations-to-key shared helper, %DISPATCH
Callable table, helper extraction (background-layer,
origin-clip-parts, url-quote-value, normalize-selector split),
dead return cleanup, README/POD updates
- Other: CLI level validation, @@name edge case, modern CSS units
(cap,ic, lh,rlh,vb,vi,cq*), custom property merge (later-wins),
descendant combinator normalization, comb-cache 500-cap + Lock
thread safety, UNITS regex one-time compilation
0.0.7 2026-06-04T03:35:20+03:00
- AST rewrite: replace CSS::Stylesheet with own brace-counting
parser (Parser.rakumod); own Document tree (Document.rakumod)
replaces CSS::Writer output; Normalizer.rakumod replaces
CSS::Properties normalization
- Zero dependencies: remove CSS::Stylesheet, CSS::Properties,
CSS::Writer — META6.json depends is now []
- Drop :$preserve-unknown option and the pre-scan/re-injection
system it depended on
- Add !dedup-declarations: remove duplicate property:value pairs
within a ruleset
- Add !consolidate-shorthands: consolidate longhands to shorthand
for margin, padding, border, border-{top,right,bottom,left},
outline, list-style, background, and font; skip values
containing var()/calc()/ env()/min()/max()/clamp()
- Fix !consolidate-shorthands duplicate-declaration bug: use
%consolidated set to distinguish merger-created shorthands from
original declarations
- Fix !consolidate-shorthands ordering: emit shorthand at the
position of its last longhand (was first), preserving cascade
order across interleaved non-group properties
- Fix !consolidate-shorthands mixed !important: skip consolidation
when longhands disagree on importance (was applying !important
to all sides if any longhand was important)
- Fix !write-at-rule space: use $ar.prelude ?? ' ' !! '' instead
of hardcoded media/font-face check, so @supports, @keyframes,
@layer, @container also get space before {
- Fix !extract-braceless quote walkers: use skip-quoted (handles
backslash escapes) instead of bare while loops
- Fix Writer quote normalization: skip "→' conversion when value
already contains single quotes
- Fix Writer hex color uppercasing: general hex-uppercase step so
#fff→#FFF (no longer relies on CSS::Properties)
- Add !important to Dedup !at-rule-decl-key for consistency with
!ruleset-key
- Add background and font shorthand consolidation: background
consolidated for single and multi-layer (comma-separated)
declarations with optional background-size via position / size
syntax and optional background-origin/background-clip as box
values; multi-layer handles cycling per CSS spec when layer
counts differ across longhands; font consolidated with optional
font-stretch, requiring only font-size and font-family;
line-height consumed if present
- Add %CUSTOM dispatch table in Normalizer for shorthands requiring
value-level assembly (background, font); simple shorthands
(margin, padding, border, etc.) stay on the join(' ') path
- Add !assemble-background and !assemble-font methods to Normalizer;
!assemble-font normalizes bold→700, normal→400 and skips
font-weight:400, font-style:normal, font-variant:normal as CSS
shorthand default values
- Add has-top-level-comma() and split-top-level() to Util.rakumod
— comma-at-depth-0 detection and splitting; used for multi-layer
background dispatch
- Add $CSS-WIDE-RE guard (initial|inherit|unset|revert) — prevents
consolidation when any longhand has a CSS-wide keyword value
0.0.6 2026-06-03T21:12:55+03:00
- Fix re-minification growth (final): colon in media-feature regex
`/\(.*:.*\)/` was being interpreted as a Raku regex adverb (`:`)
instead of a literal colon, so the denormalize-prelude fallback
in !find-at-rule-block was never triggered. This caused
!prop-exists to always return False for any @media qualifier
whose prelude had been space-normalized by !walk-decls, because
the exact prelude search (with space after `:`) never matched
the pipeline output (without space after `:`). Consequence:
every property inside @media was re-injected on every pass,
causing unbounded growth.
- Add normalize-prelude / denormalize-prelude subroutines to
bridge the two prelude formats
- Normalize preludes in !walk-decls so qualifier keys are
consistent
- Fix !find-at-rule-block to try both normalized and denormalized
forms during block search
- Fix !prop-exists to scan ALL at-rule blocks with the same
prelude (not just the first) for the inner selector
- Fix !merge-qualified to find the at-rule block that actually
contains the target inner selector; add idempotency guard to
skip re-injection when the exact ruleset already exists in the
block
- Fix normalize/denormalize-prelude nested-parens handling
(e.g. calc(), min(), max()) — use a token with &val to match
balanced inner parens instead of a bare <-[):]>+ capture
0.0.5 2026-06-03T06:24:07+03:00
- Fix !normalize-block trailing semicolon: always omit (rebuilds
from scratch), making output consistent with pipeline blocks
- Fix !find-qual-end depth counting: O(n) single-pass with
quote/comment skipping instead of O(n²) rescan from 0
- Fix !normalize-selector quote walking: use skip-quoted for
backslash-escape handling
- Fix split-selectors quote walking: use skip-quoted for
backslash-escape handling (same bug, Util.rakumod)
- Update Merging.rakumod POD: propertyless rules are skipped
(not barriers)
- Update README Level 2: add propertyless-rules note
- Add test coverage: escaped-quote attribute selector, content
brace depth guard, @keyframes margin:0px, no-trailing-newline
- Fix !normalize-all-decls inline quote walkers: use skip-quoted
(fixes double-backslash \" edge case)
- Fix split-xpath quote walking: use skip-quoted for consistency
(Util.rakumod)
0.0.4 2026-06-03T04:48:31+03:00
- Fix order stability: insert re-injected rulesets at their
natural @order position (after predecessor) instead of appending
at end, so first-pass and second-pass selector order is
identical
- Add rgb()/rgba() → hex shortening to !minify-value
(post-processing pass), closing the gap where rgb() values
inside @keyframes and other pipeline-bypassed blocks stayed
unshortened
- Remove stale Merging-plugin !important warning from README —
!important is part of the Str(:optimize) grouping key, so mixed
groups never form
- Fix re-minification growth (remaining): normalize combinator
spacing (+ / > / ~) in !normalize-selector so selectors like
.foo+.bar (no space before +) match the pipeline output .foo +
.bar (with space). Always call !normalize-selector (not just
for comma-containing selectors) to catch this case. Prevents
runaway re-injection on subsequent passes for any selector
containing tight combinators.
0.0.3 2026-06-03T03:39:52+03:00
- Fix braceless at-rules (@import, @charset, @namespace) silently
dropped — extract before parse, prepend to output
- Fix braceless at-rules trailing newline — suppress separator
when no ruleset follows
- Fix @view-transitions and other declaration-only at-rules
dropped by pipeline
- Fix @font-face descriptors dropped by !walk-decls
(e.g. font-display)
- Fix !merge-props to respect !important cascade
- Fix :has()/:is()/:where() duplicate rulesets — strip spurious
truncated prefix ruleset
- Fix prefers-color-scheme/contrast bogus @media not all — replace
with original query
- Fix !replace-bogus-not-all genuine-guard (space before { in
%by-sel key was missing)
- Fix !find-at-rule-block and !prop-exists to target correct Nth
block for duplicate at-rules (e.g. multiple @font-face); fall
back to block 0 when Nth block was deduped away
- Fix !find-at-rule-block advance past brace instead of past
prelude (fragile with space-before-{ variant)
- Fix !merge-qualified double-semicolon when block body already
ends with semicolon
- Fix !prop-exists to scan ALL rulesets for a selector (not just
the first one), preventing spurious re-injection when
duplicate-selector rulesets exist at level 1
- Fix !prop-exists quote normalisation (pre-scanned selectors use
`"`, pipeline output uses `'`)
- Fix !prop-exists boundary guard — skip substring matches
(e.g. `#header` inside `[data-bs-theme='dark'] #header`) while
allowing valid merged-group matches (e.g. `h1` after `, `)
- Rewrite 01-basic and 02-plugins with exact golden output (was
loose regex/count checks)
- Add 03-edge-cases — 11 subtests covering empty input,
!important, custom properties, braceless at-rules, attribute
selectors, nested at-rules, chained merging, @font-face, and more
- Add 04-golden — 57 exact-output regression tests covering the
full minification surface including @keyframes, combinators,
rgb(), @view-transitions, :has()/:is()/:where(),
prefers-color-scheme, and multi-block @font-face
- Fix !has-selector-in regex injection — wrap $sel in quotemeta to
escape metacharacters
- Fix !merge-qualified inner quote mismatch — normalise `"` → `'`
for at-rule inner selectors
- Fix !replace-bogus-not-all inner quote mismatch — same
normalisation for the media-query path
- Fix !remove-selector boundary guard — walk backwards past \s
chars (not just `}`) to handle merged output with :sep("\n")
separators
- Fix !walk-decls NUL-counter grep — use explicit block ({ ... })
instead of broken WhateverCode || to count existing NUL-suffixed
keys for 3+ duplicate at-rule blocks (e.g. three @font-face)
- Fix !dedup-selector-lists split on ', ' breaking with attribute
selectors containing commas — add bracket/paren/string-aware
split-xpath sub to Util; dump naive split(', ') in Merging; use
.splice instead of = for AST mutation
- Fix !merge-decls spurious re-injection after pipeline dedup —
dedup selector lists in qualifier before prop-exists check via
new split-selectors sub; re-inject using deduped qualifier
- Fix upstream xpath crash on pseudo-elements (::before, ::after,
etc.) — monkey-patch missing `xpath-pseudo-elem` method onto
CSS::Selector::To::XPath
- Fix idempotency — strip CSS comments before declaration
extraction in !walk-decls so re-minified output doesn't grow
- Fix non-adjacent same-declaration merging — propertyless rules
are now treated as barriers (last), preserving cascade order
- Fix propertyless rules break non-adjacent merging — change from
`last` to `$i++; next` so parser-artifact propertyless rulesets
are skipped instead of acting as hard barriers
- Fix cascade-order change during same-declaration merging — track
last-merged position for each group and sort by it before
assigning to $stylesheet.rules, preserving which rule wins by
cascade order
- Fix @keyframes keyframe selector order — add @order array
alongside %by-sel in !walk-decls, iterate @order in !merge-decls
Phase 2 instead of %by-sel.kv (Raku Hash iteration is
randomized)
- Remove dead `@skipped` / `@merged.append` code from Merging
plugin
- Update documentation: README and POD limitations reflect fixed
bugs
- Fix !walk-decls regex fragility — replace <-[:;]>+ / <-[;}]>+
regex with parse-body character-walker that tracks strings,
paren depth, and comments, correctly handling ; and } inside
values
- Fix /*! */ license extraction inside string values — replace
naive regex with !extract-licenses character-walking state
machine
- Normalize declaration values in !merge-decls re-injection path —
add !minify-value method that shortens colors, removes
zero-units, normalizes font-weight, and normalizes quotes (" →
') for re-injected declarations
- Add !normalize-all-decls post-processing pass — walks the output
string and normalizes values + deduplicates declarations in ALL
{…} blocks, catching @keyframes keyframes, nested at-rules, and
other blocks the pipeline can't reach (recursive leaf-block
walker)
- Fix broken hex shortening regex in !minify-value (was using
self- referential captures that never matched; now properly
backrefs each hex digit pair) so 6-char colors are shortened in
@keyframes keyframe blocks and other post-processed blocks
- Fix zero-unit regex in !minify-value — added ``
negative lookbehind to prevent stripping units from non-zero
values containing a `0` digit (e.g. `30px` → `30`); now only
standalone `0px` → `0`
- Fix golden test 70 for @keyframes normalization — accept both
keyframe selector orders (from/to vs to/from) since hash
iteration order in %by-sel is non-deterministic between Raku
runs
- Fix parse-body double-increment when : or ; found inside parens
(skipped one character after :/; inside functions — never fires
in practice since : in property names is invalid CSS)
- Fix has-brace check in !walk-decls — use quote-aware walker
instead of naive / '{' / regex that would false-positive on {
inside quoted strings (theoretical, extremely rare)
- Fix braceless at-rule extraction — replace .*?; regex with
quote/paren-aware find-semicolon walker so ; inside url() or
strings is correctly skipped (theoretical, ; inside URL is rare)
- Fix find-semicolon double-increment on (/) — the when blocks
were incrementing $i inside AND falling through to $i++ at the
while-loop bottom, skipping one character after each paren
- Remove dead code (= Empty) in Merging Plugin line 98 — $next is
about to be dropped, the assignment had no effect
- Fix re-minification growth — normalize selectors (commas → `, `)
in !walk-decls and media-query preludes (colons → `: `) in
!find-at-rule-block so extracted qualifiers match pipeline
output format, preventing runaway re-injection on subsequent
passes
0.0.2 2026-05-21T16:27:43+03:00
- Fix `1em` → `em` regression from CSS::Writer (override write-num
for `em`/`ex` in Writer.rakumod)
- Remove %KNOWN-PROPS filter in !walk-decls so all known
properties are pre-scanned (catches CSS3 values like `position:
sticky`, `overflow: clip` that the grammar drops)
- Fix falsy-zero bug in !prop-exists (Raku `0` is falsy, causing
all rulesets at position 0 to always re-inject)
- Add %SHORTHAND map in !prop-exists to avoid re-injecting
longhands consolidated to shorthands by CSS::Properties
(e.g. `background-color` → `background`)
- Fix same falsy-zero bug in at-rule path of !prop-exists
0.0.1 2026-05-21T01:30:31+03:00
- Initial version