package cascade
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page
CSS generation and manipulation library for OCaml
Install
dune-project
Dependency
Authors
Maintainers
Sources
cascade-1.1.0.tbz
sha256=c303651e043ed8d807d3d6f2c3745233e792ce7c30d562c9717d284e1bf5ac6a
sha512=681847993deabdc3899f62268b3cbc931623abca0413b0e8873e8568ec25a86c4d3db7f7f0e6a6a6bf24dbfdc7df314dae07ae66d49f76f3dd6953a4381bbf54
doc/CHANGES.html
1.1.0
Breaking
Css.hexraisesInvalid_argumenton a malformed hex string instead of returning opaque black;Css.hex_optreturns an option. Parsing is unaffected (#232)cascade fmtandcascade diffdrop--memtrace, and the library drops thememtracedependency (#237)- A parse failure that drops every rule makes
cascade fmtexit 1 instead of writing an empty stylesheet with a green status (#273) cascade diff --diff=canonicalexits 1 whenever the two canonical forms differ, and prints the difference. It called such a pair equivalent and exited 0 when the structural walk reached no difference, which left a missing normalisation key and a blind spot in the walk both reading as success.Css_compare.equal ~mode:`Canonicalanswers on the same bytes, andCss_compare.No_diffno longer carries the two canonical forms (#290)Tree_diff.tgainslayer_order; record expressions must set it and record patterns must bind it or use; _(#295)- Optimizer profiling is per run:
Css.optimizeandOptimize.stylesheettake?stats:Stats.tandStats.snapshotreads back an immutable record.Optimize.counters,Optimize.pass_times,Optimize.iteration_statsandOptimize.set_profileare gone, along with the per-pass table and the marginal-stop counter, which no pass had written since the DAG scheduler replaced the multi-pass fixpoint (#288) Resolve.NODEgainstext_children, the data of a node's direct child text nodes, which:emptyneeds (#286)Apply.Make(...).computetakes~sheet:Stylesheet.tinstead of~css:string. It parsed the text itself and answered an empty result when the parse failed, so a caller could not tell invalid CSS from empty CSS; the parse, and the warningsCss.of_stringcollects with it, now stay with the callerApply.result.keptcounts the rules it says it counts. A block at-rule counted once for its wrapper, so a@mediaholding three rules reported one (#287)cascade applyexits 1 when a<style>block or the supplementary stylesheet parses to nothing, and leaves such a block in the page instead of deleting it. It used to delete the block and exit 0, shipping an unstyled page under a green status. A supplementary stylesheet that cannot be read is an error rather than no stylesheet at all (#287)
Parsing
Reader.peek_utf8_atreturnsNonefor negative and out-of-range offsets instead of indexing outside the input (#308)- URL references use RFC 3986 resolution, preserving data, scheme-relative, query-only and fragment-only URLs (#304)
- Container conditions parse component values directly, so strings and escaped or case-insensitive boolean keywords keep their CSS meaning (#305)
- Media queries parse component values directly, so escaped identifiers and balanced general-enclosed values keep their CSS meaning (#306)
- Typed
-webkit-and-moz-aliases use their standard value grammar. shape-outsidereads its whole grammar. Onlynone,circle(), a non-emptyinset()and the CSS-wide keywords were accepted, somargin-box,circle(50%) content-box,url(shape.png)and every other basic shape were rejected and the declaration dropped- An identifier takes any code point at or above U+0080, so a selector such as
.text-↗parses.Css.of_stringtakes?enforce_specandcascadetakes--enforce-specto restrict identifiers to the CSS Syntax 3 range list; output is the same either way (#254) - Parsed rather than dropped with a warning:
perspective: none,text-underline-offset: autoand a negativetext-underline-offset(#212); a nested rule whose selector starts with an identifier, such ash2:where(...)(#193); and an unrecognised media query such astheme(static), kept as never-matching instead of discarding the@mediablock or the@import(#192) - The eleven
scroll-marginproperties take a negative length, as CSS Scroll Snap 1 allows; onlyscroll-paddingis non-negative (#280) - A
}ends a declaration value inParser.block_contentsinstead of being swallowed along with the rest of the block, and a bad string serialises back to source that reads as one instead of vanishing from an at-rule prelude (#284)
Printing and nesting
background-positionandmask-positionprint one position per layer, comma-separated (#209)- A non-integer above roughly 4.6e10 prints as a number again, not as
scale(1.2345678.9012e+19)(#265) - An unknown at-rule keeps the space before its prelude under
--minify;@foo bar {x:1}printed@foobar{x:1}, a different at-rule (#272) - Flattening distributes the parent over every branch of a nested selector list:
.p { a, b { ... } }is.p a, .p b(#205) - Substituting
&wraps a complex parent in:is():.a .b { .dark & { ... } }flattens to.dark :is(.a .b)(#194)
Minification
--losslesspreserves logical sizing and corner aliases in source order.- A hex colour folds to its name whenever the name is no longer, which the hand-copied inversion table missed for
bisque,indigo,orchid,salmon,sienna,tomatoandviolet:#ffe4c4stayed hex where#f0ffffalready becameazure(#289) Css.Values.read_color_namereads every namepp_color_nameprints, not 21 of the 148, andgreyreads asGreyrather than theGraythat printsgray(#289)--losslesskeeps a longhand in source order against the shorthand that resets it, whether the property is typed or not; moving the pair changed what the rule rendered (#267, #270)--losslesskeeps a colour's alpha exact, like its other channels;oklch(... / .74567)printed/.746(#278)--losslesskeeps a flow-relative property in source order against a physical one of the same family, since the writing mode decides which physical side it resolves to (#277)- A vendor-prefixed value repeated with
!importantcollapses to the later declaration; only a genuine value difference is kept as a legacy fallback.display:-webkit-box;display:-webkit-box!importantsurvived as both - A selector list mixing a vendor pseudo-element with ordinary selectors is split, so a browser that ignores
::-webkit-search-cancel-buttonkeeps the other selectors' declarations (#203) - A rule with nested children absorbs a later rule with the same selector when the two are disjoint (#203)
- SVG presentation properties are typed and minify like any other value:
stop-color:#fff,fill-opacity:.1,stroke-dashoffset:0,stroke-dasharray:4 2,paint-order: stroke fill markerstostroke, a redundantvector-effectkeyword dropped, andfill-rule,clip-rule,stroke-linecapandstroke-linejoinread as their own grammars, includingmiter-clipandarcs(#214, #228, #234, #235, #236, #240, #241, #242) - More values fold to their exact equivalent:
grid-auto-flow: row densetodense(#230); a zero angle in radians to0deg, sohue-rotate(0rad)folds like the other units (#229);hue-rotate()with a zero argument inside a custom property (#257); afont-stretchkeyword to its percentage, except in thefontshorthand (#206); and adjacent gradient stops of one colour to a double-position stop, with a0deglinear-gradient angle dropped and the stops reversed, never for the legacy prefixed gradients (#214) --log=cascade.factor:debugreports the optimizer's factoring decisions: each fixpoint iteration and every segment reverted or skipped (#239)--minifyis faster on a large stylesheet, for the same output (#221)
Custom properties and @layer
Css.inline_varsresolvesvar()across@layerboundaries and folds a custom property redefined across layers to its cascade winner, so a layered stylesheet inlines like its unlayered form (#187, #189)cascade applyprojects rules inside@layeronto elements; a fully layered stylesheet, such as Tailwind v4 output, inlined nothing (#188)cascade applyweighs cascade layers instead of ignoring them: an unlayered declaration beats a layered one,!importantreverses that, and a rule with no inline form stays inside its layer (#283)cascade applyleaves a declaration in the stylesheet when a rule inside@scope,@starting-style,@when,@elseor@-moz-documentsets the same property; it moved inline, above the rule that was kept (#286)cascade applyonly inlines a rule whose selector its matcher can represent, so[data-k="X" i], a namespaced selector and the>>>and||combinators stay in the stylesheet rather than being inlined onto nobody and dropped, and:emptycounts an element's text:<p>text</p>is not empty (#286)Css.vars_of_declarationsreports thevar()references of 39 properties it answered with none, soCss.resolve_themeemits the theme binding forinline-size: var(--w)as it does forwidth: var(--w)(#266)
Canonical diff
- The projection expands selector-list rules onto their branches (#204), folds two conditional blocks sharing a condition (#211), keys
@media not all and (X)as the@media not (X)it equals while still emitting what it read (#231), and keys a run of@propertyrules by name, keeping the last registration (#227) - It skips the rule-regrouping passes, which depend on input order;
Css.optimizetakes?regroupto turn them off (#215, #224) - It normalises the space after a top-level comma in a custom-property value, drops a declaration a later rule with the identical selector also writes (leaving
!importantalone), and pairs exactly matching rules before falling back to the property signature (#206) - A custom property keeps its
!important, its layer and its metadata through the projection, so the differ no longer calls two sheets that disagree about the flag identical (#271) - It rewrites a quoted multi-word font name in a custom property as the
<ident>sequence it unquotes to, the same family name under CSS Fonts 4 sec. 15.3:--font-sans: ui-sans-serif, "Noto Color Emoji"and--font-sans: ui-sans-serif, Noto Color Emojireach one form. The structural comparator already folded the two together; the projection did not (#290) - Under
--losslessit keys acolor(srgb ...)whose channels land on whole bytes as thergb()spelling of the same colour, socolor(srgb 1 0 0)andrgb(255 0 0)stop reading as a difference. Exact conversions only:color(display-p3 1 0 0)and an off-grid channel stay distinct, and the printer still emits the function that was written (#289)
Diff report
- Grouping repeated selectors and conditions is linear in the number of rules while preserving source order (#307)
- The cascade layer order the two sheets declare is compared, and the report names the layer pairs that swapped. Dropping an
@layer a;pin, which makes the other layer the weaker one, read as no difference at all - Every entry the report prints names what it is about. A
@propertyor@keyframessurplus reached the rule level, which has no rule to name and printed a bare tree connector while still counting the entry towards the summary;@charset,@namespaceand@layer a, b;had no name at all - A container that changed places is reported whether or not its body changed, on the same order keys the rule level uses. Three absolute-index distances gated it before, so swapping an
@mediawith the rule below it - which changes which declaration wins above the breakpoint - printedCSS files are identicaland exited 0 - A rule that writes one property more than once, as a fallback chain does, is compared occurrence by occurrence; matching by name alone made
a{color:red;color:blue}againsta{color:red;color:green}reportcolor: blue -> red, a value neither side holds (#285) - Containers are compared however deep they nest. The walk stopped at three levels, so a leaf difference under five at-rules was reported as no difference at all, exit code included (#285)
- A container entry with nothing to show under it reads as
(modified, no details); it claimed a position change, which only aReorderedentry establishes (#285) - A comparison that classified nothing says so:
Changes: none classified structurally (see report below). It readNo structural differences, which claimed equivalence over a comparison that fell through to a string diff, and over a side whose content the parser discarded (#285) - A canonical-form difference the structural walk did not reach is printed as
Canonical forms differ:above a string diff of the two forms (#290) - A string diff names the two sides with the labels it was given, so
cascade diffheads it with the two file names instead ofExpectedandActual(#290) - A declaration reorder that decides the cascade is reported inside
@media,@layerand@supports(#268), and the at-rules that carry no selector -@page,@starting-style,@counter-style,@scope, a second@font-face- are compared on their bodies (#269) - An
@propertyis compared on its whole body and the entry names the descriptors that differ (#264); a rule is reported as reordered only when it moved against another rule (#263); and the size summary lists the two files in the order of the---and+++headers, so an addition no longer reads as a shrink (#261) - A selector written by more than one rule is reported once, at the top level and inside a container; when its declarations survive on both sides, spread differently, the entry names the move and counts as a rearranged rule (#259, #260)
- Blocks sharing a condition are reconciled one for one, so three
@containerblocks against two report the removed block rather than two changed containers; the same pairing decides media, layer and supports (#258) - Every statement of an added or removed container is reported, including a nested
@media, and a container is no longer counted again as a rule difference with an empty selector (#253) cascade diffbounds its report to the deepest level that fits, with--depthto pin a level or print the tree in full; parse warnings print above the report, capped per side; and blocks that only moved are reported as a shift run (#210)
New properties and values
- Complete the logical border properties:
Css.border_block_colorand its start/end siblings, theCss.border_inline_widthandCss.border_block_widthshorthands (typelogical_border_width), and the start/end style longhands such asCss.border_inline_start_style(#197, #198, #199, #200) - Add
Css.parse_font_family,Css.parse_list_style_typeandCss.parse_list_style_image, the single-value readers behind thefontandlist-styleshorthands (#201, #202) - Add
Css.Values.oklch_none_huefor an achromatic colour with a missing hue, printed asoklch(55.6% 0 none)(#190)
Testing
- CI rejects merlint findings and incomplete library record patterns (#310)
dune testrenders a stylesheet and its optimized forms in a headless browser and compares the computed style of every element, on a document derived from the stylesheet's own selectors; it skips where no browser is installed (#275)
1.0.0
First public release. Cascade was extracted from the tw (Tailwind CSS v4 in OCaml) project as a standalone CSS command-line tool and library, then stabilised over several internal milestones.
Library
- Typed CSS AST: selectors, declarations, values, statements, and stylesheets are sealed ADTs. Invalid constructions are caught at compile time.
Single warning-aware parse entry point:
Css.of_stringruns CSS Syntax Level 3 recovery and returns(parse, Error.t) result, whereparse = { stylesheet; warnings }.~strict:truepromotes the first warning toError _.Css.of_string_exnreturns the recovered stylesheet directly and raisesError.Parse_erroronError.
- Pretty-printer with separate pretty and minified contexts (
Css.to_string ?minify), with several typed printers exposed (pp_color,pp_length, ...). - Structural transforms (
fold,map,sort,flatten_nesting,inline_imports) and structural CSS diff utilities via thecascade.diffsub-library. - Optimizer with deduplication, rule merging, selector combining, and shorthand/longhand coverage including
allreset folding. Rule merging is order-independent: rules are scheduled through a conflict DAG so cascade-safe reorderings converge on the same output regardless of source order. - Minification optimises estimated compressed (gzip) transfer size by default: a global factoring that shrinks raw bytes but would grow the compressed output is not applied. Pass
~objective:\Raw(CLI--objective=raw`) to optimise raw bytes instead, for output that ships uncompressed. Spec coverage:
- Selectors Level 4 -- including
:has(),:is(),:where(),:not(), nesting&, and full attribute syntax. - Values & Units Level 4 --
calc(),clamp(),min(),max(),minmax(), the modern length units, durations, angles. - Color Level 4 -- 15 colour spaces including
oklch(),oklab(),lch(),hwb(),color-mix(), plus the 148 named colours. - Conditional Rules Level 3-5 --
@media,@supports,@container(including typedstyle()/scroll-state()queries with range operators),@when/@else. - Cascade Level 5 --
@layerdeclarations and blocks, CSS-wide keywords, andallreset semantics in the optimizer. - Custom Properties Level 1 --
var()parsing/printing, typed fallbacks, theme/default substitution,@propertyregistration. - Fonts Level 4 (
@font-facedescriptors), Animations Level 1 (@keyframes,@starting-style).
- Selectors Level 4 -- including
- Over 400 typed properties cover box model, flexbox, grid (including
grid-template-areasvalidation), logical properties, typography (font-variant-*,text-emphasis-*,text-decoration-skip-*,initial-letter*), borders andborder-image, backgrounds and gradients, transforms (translate,scale,rotate,transform), transitions, animations (animation-range*, scroll-driven timelines), filters, masks, scroll snap, anchor positioning (position-anchor,position-area,position-try-fallbacks), view transitions, and the common vendor-prefixed properties. - Custom-property workflows: typed
<syntax>parsing for@property, registered-property substitution against an explicitCss.Context.t, and round-trip-stablevar()serialisation with literal fallbacks.
CLI tools
- CLI commands share one binary file reader that closes its descriptor when a read fails (#309)
cascade-- pretty-print and minify CSS files. It accepts stdin via-or a missing file argument, and writes output to stdout.cascade --minifyapplies the standard safe transforms, including deduplication, rule merging, selector grouping, empty-rule elimination, and nested-rule flattening, optimising estimated gzip transfer size by default (--objective=rawoptimises raw bytes instead).cascade --inline-importsresolves local@importrules relative to the input file, andcascade --inline-varssubstitutes static custom-property references.--keep-vars=NAMESpreserves selected custom properties.cascade diffprovides structural CSS diffing between two files withauto,tree,string, andcanonicalmodes; respectsNO_COLOR,CASCADE_COLOR, and--color, and colours only when stdout is a tty. Identical files exit 0 and differing files exit 1, so the command slots into CI checks and git hooks. Thecanonicalmode projects both sheets to a normal form first, so equivalent factorings -- different rule grouping, cascade-safe rule and declaration order -- compare identical rather than as spurious changes.- The CLI is installable as a binary through the Homebrew tap
samoht/tap/cascade, with opam installation still available for OCaml users.
Notes
cascadeparses already-decoded UTF-8 strings. The CSS Syntax Level 3 byte-stream decoding step (BOM handling,@charsetbyte sniffing, HTTP/environment charset fallback) is the caller's responsibility.- CSS nesting round-trips through the parser and printer, and the minifier flattens nested rules when safe.
@importrules are preserved by default. Use--inline-importsfor explicit closed-world filesystem inlining.- No source-map support.
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page